顯示具有 jQuery Mobile 標籤的文章。 顯示所有文章
顯示具有 jQuery Mobile 標籤的文章。 顯示所有文章

2022年1月26日 星期三

jQuery Mobile 學習筆記 (二十三) : 用 Web Storage 製作備忘錄 App (中)

今天同事安裝了這款 App 試用後說還不錯, 但少了一個備忘分類的欄位, 這在以前工作日誌系統中可以很方便地用下拉式選單選取, 也可以做為搜尋欄位, 但 localStorage 就很陽春的 key:value 物件資料庫啊! 沒辦法多出其他欄位. 不過仔細想想也不是沒辦法, 至少有兩個解決方案 :
  1. 將多出來的分類欄位與備忘記事欄以字串方式串接存入 value.
  2. 將分類與備忘記事以物件方式儲存, 然後用 JSON.stringify() 轉成字串存入 value.
於是晚上便動手來試試看. 首先測試第一個方案, 參考上一篇來改 :


這次想連同 render_memo_list() 都改成用 "|" 號來串接, 因為此符號較少用, 拆分字串時誤拆的機率比較低. 儲存在 localStorage 中的物件以寫入的日期時間當作 key, 分類與備忘記事以 "|" 符號串接後做為 value, 例如 :

{"2022-01-24T08:45:27": "問候|Hello World"}
{"2022-01-24T09:31:27": "問候|你是在說哈囉嗎?"}

在函式 sort_local_storage() 中, 也是將 key 與 value 用 "|" 符號串接, 其傳回陣列例如 :

[ "2022-01-24T09:31:27|問候|你是在說哈囉嗎?", "2022-01-24T08:45:27|問候|Hello World"]

由於 key=日期時間放在最前面, 故會先以日期時間排序, 先呼叫 sort() 再呼叫 reverse() 結果最近的時間會排前面. 完整的網頁與程式碼如下 : 



<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta http-equiv="cache-control" content="no-cache">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
    <link href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" rel="stylesheet">
    <style>
      table {
          color: black;
          background: #fff;
          border: 1px solid #b4b4b4;
          padding: 0;
          margin-top:10px;
          width: 100%;
          -webkit-border-radius: 8px;
          }
      table tr td {
          color: #666;
          border-bottom: 1px solid #b4b4b4;
          border-right: 1px solid #b4b4b4;
          padding: 5px 5px 5px 5px;
          background-image: -webkit-linear-gradient(top, #fdfdfd, #eee);
          }
      table tr:first-child td {
          border-top: 1px solid #b4b4b4;
          }
      table tr td:last-child {
          border-right: none;
          }
      table tr:last-child td {
          border-bottom: none;
          }
    </style>
  </head>
  <body>
    <!-- 第一頁頁面 (顯示備忘事項列表) -->
    <section data-role="page" id="page1">
      <header data-role="header" data-position="fixed">
        <h1>我的備忘錄</h1>
        <a href="#confirm_clear_dialog" id="clear_memo_btn" class="ui-btn-right" data-icon="recycle" data-rel="dialog">清除</a>
      </header>
      <article data-role="content">
        <a href="#page2" data-role="button" data-icon="plus">新增事項</a>
        <table>
          <thead>
            <tr>
              <th>事項</th>
              <th>類別</th>
              <th>編輯</th>
            </tr>
          </thead>
          <tbody id="memo_list">
          </tbody>
        </table>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 第二頁頁面 (新增備忘事項) -->
    <section data-role="page" id="page2">
      <header data-role="header" data-position="fixed" data-add-back-btn="true" data-back-btn-text="返回">
        <h1>新增事項</h1>
      </header>
      <article data-role="content">
        <form data-ajax="false">
          <div data-role="fieldcontain">
            <label for="add_memo">事項 : </label>
            <textarea id="add_memo"></textarea>
            <label for="add_category">類別 : </label>
            <input type="text" id="add_category">
          </div>
          <input type="button" id="add_memo_btn" value="儲存" data-icon="check">
        </form>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 第三頁頁面 (編輯備忘事項) -->
    <section data-role="page" id="page3">
      <header data-role="header" data-position="fixed" data-add-back-btn="true" data-back-btn-text="返回">
        <h1>編輯事項</h1>
      </header>
      <article data-role="content">
        <form data-ajax="false">
          <div data-role="fieldcontain">          
            <input type="hidden" id="edit_memo_key">
            <label for="edit_memo_value">事項 : </label>
            <textarea id="edit_memo_value"></textarea>
            <label for="edit_memo_category">類別 : </label>
            <input type="text" id="edit_memo_category">
          </div>
          <div data-role="controlgroup" data-type="horizontal">
            <a href="#" data-role='button' data-rel="back" data-icon="back" data-inline="true">取消</a>
            <a href="#confirm_remove_dialog" id="remove_memo_btn" data-role='button' data-rel="dialog" data-icon="delete" data-inline="true">刪除</a>
            <a href="#" id="update_memo_btn" data-role='button' data-icon="check" data-inline="true">更新</a>
          </div>
        </form>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 對話框頁面 1 (清除全部資料) -->
    <section data-role="dialog" id="confirm_clear_dialog" data-overlay-theme="b">
      <header data-role="header">
        <h1>請確認</h1>
      </header>
      <article data-role="content">
        <p>確定要清除全部資料嗎?</p>
        <div data-role="controlgroup" data-type="horizontal">
          <a href="#" data-role='button' data-icon='back'  data-inline="true" data-rel="back">取消</a>
          <a href="#" id="confirm_clear_btn" data-role='button' data-icon='check'  data-inline="true">確定</a>
        </div>
      </article>
    </section>
    <!-- 對話框頁面 2 (刪除單筆資料) -->
    <section data-role="dialog" id="confirm_remove_dialog" data-overlay-theme="b">
      <header data-role="header">
        <h1>請確認</h1>
      </header>
      <article data-role="content">
        <p>確定要刪除這筆事項嗎?</p>
        <div data-role="controlgroup" data-type="horizontal">
          <a href="#" data-role='button' data-icon='back' data-inline="true" data-rel="back">取消</a>
          <a href="#" id="confirm_remove_btn" data-role='button' data-icon='check'  data-inline="true">確定</a>
        </div>
      </article>
    </section>
    <script>
      function sort_local_storage(){
        var arr=[];
        for (var i=0; i<localStorage.length; i++){
          var k=localStorage.key(i);
          var v=localStorage.getItem(k); 
          arr.push(k + "|" + v);
          }
        arr.sort();
        arr.reverse();
        return arr;
        }
      function set_edit_memo(k){
        var cv=localStorage.getItem(k);
        $("#edit_memo_key").val(k);
        var arr=cv.split("|");
        var c=arr[0];
        var v=arr[1];
        $("#edit_memo_value").val(v);
        $("#edit_memo_category").val(c);
        render_memo_list();
        $.mobile.changePage($("#page3"));
        }
      function render_memo_list(){
        $("#memo_list").empty();
        var memo_arr=sort_local_storage();
        var item=[];
        if (memo_arr.length == 0){
          item.push("<tr><td colspan='3' style='text-align:center;'>" + 
                    "資料庫內容為空, 請新增事項</td></tr>");
          }
        else {
          for (var i=0; i<memo_arr.length; i++){
            var arr=memo_arr[i].split("|");
            var k=arr[0];
            var c=arr[1];
            var v=arr[2];
            item.push("<tr><td>" + v + "</td><td>" + c + "</td>" + 
                      "<td style='text-align: center;'>" + 
                      "<a href='#' data-role='button' " + 
                      "data-icon='edit' data-iconpos='notext' " + 
                      "data-mini='true' data-inline='true' " + 
                      "onclick='set_edit_memo(" + '"' + k + '"' +
                      ")'>編輯</a></td></tr>");            
            }
          }
        $(item.join("")).appendTo("#memo_list").trigger("create");
        }
      $(document).one("pageinit", function(){
        render_memo_list();
        $("#add_memo_btn").on("click", function(e) {
          var k=JSON.stringify(new Date()).substr(1,19);
          var v=$("#add_category").val() + "|" + $("#add_memo").val();
          localStorage.setItem(k, v); 
          $("#add_memo").val("");
          $("#add_category").val("");
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#confirm_clear_btn").on("click", function(e) {
          localStorage.clear();
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#confirm_remove_btn").on("click", function(e) {
          var k=$("#edit_memo_key").val();
          localStorage.removeItem(k);
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#update_memo_btn").on("click", function(e) {
          var k=$("#edit_memo_key").val();
          var v=$("#edit_memo_category").val() + "|" + 
                $("#edit_memo_value").val();
          localStorage.setItem(k, v);
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        }); 
    </script>
  </body>
</html>

程式部分與前一篇的主要差別是新增了記事的類別欄位與其配套處理, 主要就是將分類冠在記事內容前面用 "|" 串接當作 value 存入 localStorage, 顯示或編輯時用 split("|") 將 key (日期時間), 分類與記事內容拆出來. 另外也改寫了 render_memo_list() 等函式, 簡化了資料庫為空時的處理方式, 結果如下 : 




 


測試 OK 後用線上服務 appgeyser 轉成 apk 檔, 可從 GitHub 下載 : 





不過這個第二版是利用 "|" 字元來將分類欄位與記事欄位串起來當作 value 存入 localStorage, 雖然 "|" 字元很少人會用到, 但萬一用在分類欄位上就會拆解時出現分類被裁減掉 "|" 字元後面的部分 (被歸入記事欄位), 所以並不完美. 

較安全的處理方式是將分類與記事兩個欄位存入物件中, 然後用 JSON.stringify() 方法轉成字串, 取出時再呼叫 JSON.parse() 方法還原成物件. 分類欄位 key 可用 category, 記事欄位 key 可用 memo,  例如 : 

{catelogue: "問候", memo: "Hello World"}
{catelogue: "問候", memo: "你是在說哈囉嗎?"}

將上面第二版程式碼中的欄位合併與拆解方式修改為物件存取方式, 成為如下之第三版 : 



<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta http-equiv="cache-control" content="no-cache">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
    <link href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" rel="stylesheet">
    <style>
      table {
          color: black;
          background: #fff;
          border: 1px solid #b4b4b4;
          padding: 0;
          margin-top:10px;
          width: 100%;
          -webkit-border-radius: 8px;
          }
      table tr td {
          color: #666;
          border-bottom: 1px solid #b4b4b4;
          border-right: 1px solid #b4b4b4;
          padding: 5px 5px 5px 5px;
          background-image: -webkit-linear-gradient(top, #fdfdfd, #eee);
          }
      table tr:first-child td {
          border-top: 1px solid #b4b4b4;
          }
      table tr td:last-child {
          border-right: none;
          }
      table tr:last-child td {
          border-bottom: none;
          }
    </style>
  </head>
  <body>
    <!-- 第一頁頁面 (顯示備忘事項列表) -->
    <section data-role="page" id="page1">
      <header data-role="header" data-position="fixed">
        <h1>我的備忘錄</h1>
        <a href="#confirm_clear_dialog" id="clear_memo_btn" class="ui-btn-right" data-icon="recycle" data-rel="dialog">清除</a>
      </header>
      <article data-role="content">
        <a href="#page2" data-role="button" data-icon="plus">新增事項</a>
        <table>
          <thead>
            <tr>
              <th>事項</th>
              <th>類別</th>
              <th>編輯</th>
            </tr>
          </thead>
          <tbody id="memo_list">
          </tbody>
        </table>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 第二頁頁面 (新增備忘事項) -->
    <section data-role="page" id="page2">
      <header data-role="header" data-position="fixed" data-add-back-btn="true" data-back-btn-text="返回">
        <h1>新增事項</h1>
      </header>
      <article data-role="content">
        <form data-ajax="false">
          <div data-role="fieldcontain">
            <label for="add_memo">事項 : </label>
            <textarea id="add_memo"></textarea>
            <label for="add_category">類別 : </label>
            <input type="text" id="add_category">
          </div>
          <input type="button" id="add_memo_btn" value="儲存" data-icon="check">
        </form>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 第三頁頁面 (編輯備忘事項) -->
    <section data-role="page" id="page3">
      <header data-role="header" data-position="fixed" data-add-back-btn="true" data-back-btn-text="返回">
        <h1>編輯事項</h1>
      </header>
      <article data-role="content">
        <form data-ajax="false">
          <div data-role="fieldcontain">          
            <input type="hidden" id="edit_memo_key">
            <label for="edit_memo_value">事項 : </label>
            <textarea id="edit_memo_value"></textarea>
            <label for="edit_memo_category">類別 : </label>
            <input type="text" id="edit_memo_category">
          </div>
          <div data-role="controlgroup" data-type="horizontal">
            <a href="#" data-role='button' data-rel="back" data-icon="back" data-inline="true">取消</a>
            <a href="#confirm_remove_dialog" id="remove_memo_btn" data-role='button' data-rel="dialog" data-icon="delete" data-inline="true">刪除</a>
            <a href="#" id="update_memo_btn" data-role='button' data-icon="check" data-inline="true">更新</a>
          </div>
        </form>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 對話框頁面 1 (清除全部資料) -->
    <section data-role="dialog" id="confirm_clear_dialog" data-overlay-theme="b">
      <header data-role="header">
        <h1>請確認</h1>
      </header>
      <article data-role="content">
        <p>確定要清除全部資料嗎?</p>
        <div data-role="controlgroup" data-type="horizontal">
          <a href="#" data-role='button' data-icon='back'  data-inline="true" data-rel="back">取消</a>
          <a href="#" id="confirm_clear_btn" data-role='button' data-icon='check'  data-inline="true">確定</a>
        </div>
      </article>
    </section>
    <!-- 對話框頁面 2 (刪除單筆資料) -->
    <section data-role="dialog" id="confirm_remove_dialog" data-overlay-theme="b">
      <header data-role="header">
        <h1>請確認</h1>
      </header>
      <article data-role="content">
        <p>確定要刪除這筆事項嗎?</p>
        <div data-role="controlgroup" data-type="horizontal">
          <a href="#" data-role='button' data-icon='back' data-inline="true" data-rel="back">取消</a>
          <a href="#" id="confirm_remove_btn" data-role='button' data-icon='check'  data-inline="true">確定</a>
        </div>
      </article>
    </section>
    <script>
      function sort_local_storage(){
        var arr=[];
        for (var i=0; i<localStorage.length; i++){
          var k=localStorage.key(i);
          var v=localStorage.getItem(k); 
          arr.push(k + "|" + v);
          }
        arr.sort();
        arr.reverse();
        return arr;
        }
      function set_edit_memo(k){
        var cv=localStorage.getItem(k);
        $("#edit_memo_key").val(k);
        var obj=JSON.parse(cv);   
        var c=obj.category;
        var v=obj.memo;
        $("#edit_memo_value").val(v);
        $("#edit_memo_category").val(c);
        render_memo_list();
        $.mobile.changePage($("#page3"));
        }
      function render_memo_list(){
        $("#memo_list").empty();
        var memo_arr=sort_local_storage();
        var item=[];
        if (memo_arr.length == 0){
          item.push("<tr><td colspan='3' style='text-align:center;'>" + 
                    "資料庫內容為空, 請新增事項</td></tr>");
          }
        else {
          for (var i=0; i<memo_arr.length; i++){
            var arr=memo_arr[i].split("|");
            var k=arr[0];
            var obj=JSON.parse(arr[1]);
            var c=obj.category;
            var v=obj.memo;
            item.push("<tr><td>" + v + "</td><td>" + c + "</td>" + 
                      "<td style='text-align: center;'>" + 
                      "<a href='#' data-role='button' " + 
                      "data-icon='edit' data-iconpos='notext' " + 
                      "data-mini='true' data-inline='true' " + 
                      "onclick='set_edit_memo(" + '"' + k + '"' +
                      ")'>編輯</a></td></tr>");            
            }
          }
        $(item.join("")).appendTo("#memo_list").trigger("create");
        }
      $(document).one("pageinit", function(){
        render_memo_list();
        $("#add_memo_btn").on("click", function(e) {
          var k=JSON.stringify(new Date()).substr(1,19);
          var v={category:$("#add_category").val(),
                 memo:$("#add_memo").val()};
          localStorage.setItem(k, JSON.stringify(v)); 
          $("#add_memo").val("");
          $("#add_category").val("");
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#confirm_clear_btn").on("click", function(e) {
          localStorage.clear();
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#confirm_remove_btn").on("click", function(e) {
          var k=$("#edit_memo_key").val();
          localStorage.removeItem(k);
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#update_memo_btn").on("click", function(e) {
          var k=$("#edit_memo_key").val();
          var v={category:$("#edit_memo_category").val(),
                 memo:$("#edit_memo_value").val()};
          localStorage.setItem(k, JSON.stringify(v)); 
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        }); 
    </script>
  </body>
</html>

藍色部分是與上面第 2 版不同之處, 差別只在於可防止使用者的記事類別含有 "|" 字元而已, 但缺點是會占較大儲存空間, 因為每一筆資料都會重複 catelogue 與 memo 這兩個 key, 頗為冗餘, 所以 我覺得其實用第 2 版就可以啦. 

# 下載我的備忘錄 v3 apk 檔 (使用字串化物件) 




2022-01-31 補充 :

今天發現當事項欄位很長時, 類別欄位嘿被擠壓也跳行了 : 




可以用 CSS 的 td:nth-child(2) 選取第二個 td 後設定 "white-space: nowrap;" 屬性即可, 參考 :


所以我又將上面的 v2, v3 程式碼中的樣式添加了如下設定 : 

      table tr td:nth-child(2) {
          white-space: nowrap;
          }

這樣就可以解決此問題了. 

2022年1月25日 星期二

jQuery Mobile 學習筆記 (二十二) : 用 Web Storage 製作備忘錄 App (上)

上週因為要因應 IE 走入歷史須將輔助工作自動化的網頁程式改版, 在翻閱 HTML5 書籍尋求 ActiveX 的替代技術時看到 localStorage 物件, 覺得是可行方案之一, 於是便做了一些測試, 覺得雖然僅有約 5MB 容量, 倒也足以應付工作需求. 

做完測試後剛好想起同事去年底的囑託, 希望我可以將我十幾年前幫單位寫的工作日誌系統原始碼給他, 因為覺得很好用想要做為自己的私人資訊系統, 但我說那系統太大也太舊了不合用, 現在是行動通訊時代人手一機, 有空我寫個微小版的個人工作日誌系統給他. 既然應承了, 剛好又學完 localStorage, 不如打鐵趁熱, 結合 jQuery Mobile 與 localStorage 寫個網頁應用程式吧! 還可以用線上服務轉成 App. 

同事的要求很簡單, 就是可以新增, 編輯, 以及刪除記事, 相當於以前工作日誌系統裡面的 CRUD 最基本功能, 但去掉可追蹤工作流程的堆疊式文章架構, 當然也不需要稽核要看的主管閱覽等管理功能, 純粹就是表格式的記事本. 希望保留記事類別欄位, 還可以搜尋云云, 但這對 localStoage 來說有點過分了, 等後續我測試完 indexedDB 再看看唄! 

本篇先來完成備忘錄的 CRUD 基本功能就好, 順便複習一下 jQuery Mobile 的用法. 比起現在紅到發紫的 React 等, jQuery Mobile 算是上個世紀的技術了, 雖然老狗玩不出新把戲, 但老把戲還是能用的. 

關於 jQuery Mobile 參考 : 


我的規劃是使用單頁應用架構 (SPA), 所以 CRUD 似乎需要四個 page, 但其實 Delete 動作是透過函式完成, 刪除完就跳回備忘錄列表頁面, 所以其實只需要三個 Page (C, R, 與 U), 另外加上兩個 Dialog 對話框頁面 (負責確認 removeItem 與 clear). 

在這三天測試過程中才發現我對 jQuery Mobile 的操控還是有許多盲點, 例如用程式碼 $.mobile.changePage() 換頁, 以及用 trigger("create") 來動態新增 DOM 元素, 這是這幾天才學會的新用法. 

記事列表我使用 TABLE 表格來呈現, 原本想用 ListView, 但發現要放編輯 icon 有困難, 所以還是用表格唄, 這也跟之前的工作日誌系統較像. 在 jQuery 中使用表格的方法參考 :


不過本篇用的是下列這篇文章中所使用的特殊樣式表格, 它的功能較簡單, 就是單純顯示表列資料而已, 沒有分頁功能, 下回再試試有分頁的表格 :



一. 製作 jQuery Mobile 網頁程式 : 

儲存在 localStorage 中的備忘記事以格式為 YYYY-MM-DDTHH:mm:SS 的日期時間為鍵, 以記事內容為值, 例如 :

{"2022-01-24T08:45:27": "Hello World"}
{"2022-01-24T09:31:27": "你是在說哈囉嗎?"}

其中 key 是用呼叫 new Date() 得到的日期時間物件傳給 JSON.stringify() 轉成字串後再取出前 19 個字元而得. 省略秒後面的資訊, 畢竟一般不會在同一秒內輸入兩筆備忘事項. 

底下先列出整個 SPA 專案的完整原始碼, 然後再進行說明 :



<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta http-equiv="cache-control" content="no-cache">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
    <link href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" rel="stylesheet">
    <style>
      table {
          color: black;
          background: #fff;
          border: 1px solid #b4b4b4;
          padding: 0;
          margin-top:10px;
          width: 100%;
          -webkit-border-radius: 8px;
          }
      table tr td {
          color: #666;
          border-bottom: 1px solid #b4b4b4;
          border-right: 1px solid #b4b4b4;
          padding: 5px 5px 5px 5px;
          background-image: -webkit-linear-gradient(top, #fdfdfd, #eee);
          }
      table tr:first-child td {
          border-top: 1px solid #b4b4b4;
          }
      table tr td:last-child {
          border-right: none;
          }
      table tr:last-child td {
          border-bottom: none;
          }
    </style>
  </head>
  <body>
    <!-- 第一頁頁面 (顯示備忘事項列表) -->
    <section data-role="page" id="page1">
      <header data-role="header" data-position="fixed">
        <h1>我的備忘錄</h1>
        <a href="#confirm_clear_dialog" id="clear_memo_btn" class="ui-btn-right" data-rel="dialog">清除</a>
      </header>
      <article data-role="content">
        <a href="#page2" data-role="button" data-icon="plus">新增</a>
        <table>
          <thead>
            <tr>
              <th>備忘事項</th>
              <th>編輯</th>
            </tr>
          </thead>
          <tbody id="memo_list">
          </tbody>
        </table>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 第二頁頁面 (新增備忘事項) -->
    <section data-role="page" id="page2">
      <header data-role="header" data-position="fixed" data-add-back-btn="true" data-back-btn-text="返回">
        <h1>新增備忘事項</h1>
      </header>
      <article data-role="content">
        <form data-ajax="false">
          <div data-role="fieldcontain">
            <label for="add_memo">備忘事項 : </label>
            <textarea id="add_memo"></textarea>
          </div>
          <input type="button" id="add_memo_btn" value="儲存" data-icon="check">
        </form>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 第三頁頁面 (編輯備忘事項) -->
    <section data-role="page" id="page3">
      <header data-role="header" data-position="fixed" data-add-back-btn="true" data-back-btn-text="返回">
        <h1>編輯備忘事項</h1>
      </header>
      <article data-role="content">
        <form data-ajax="false">
          <div data-role="fieldcontain">          
            <input type="hidden" id="edit_memo_key">
            <label for="edit_memo_value">備忘事項 : </label>
            <textarea id="edit_memo_value"></textarea>
          </div>
          <div data-role="controlgroup" data-type="horizontal">
            <a href="#" data-role='button' data-rel="back" data-icon="back" data-inline="true">取消</a>
            <a href="#confirm_remove_dialog" id="remove_memo_btn" data-role='button' data-rel="dialog" data-icon="delete" data-inline="true">刪除</a>
            <a href="#" id="update_memo_btn" data-role='button' data-icon="check" data-inline="true">更新</a>
          </div>
        </form>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 對話框頁面 1 (清除全部資料) -->
    <section data-role="dialog" id="confirm_clear_dialog" data-overlay-theme="b">
      <header data-role="header">
        <h1>請確認</h1>
      </header>
      <article data-role="content">
        <p>確定要清除全部資料嗎?</p>
        <div data-role="controlgroup" data-type="horizontal">
          <a href="#" data-role='button' data-icon='back'  data-inline="true" data-rel="back">取消</a>
          <a href="#" id="confirm_clear_btn" data-role='button' data-icon='check'  data-inline="true">確定</a>
        </div>
      </article>
    </section>
    <!-- 對話框頁面 2 (刪除單筆資料) -->
    <section data-role="dialog" id="confirm_remove_dialog" data-overlay-theme="b">
      <header data-role="header">
        <h1>請確認</h1>
      </header>
      <article data-role="content">
        <p>確定要刪除這筆備忘事項嗎?</p>
        <div data-role="controlgroup" data-type="horizontal">
          <a href="#" data-role='button' data-icon='back' data-inline="true" data-rel="back">取消</a>
          <a href="#" id="confirm_remove_btn" data-role='button' data-icon='check'  data-inline="true">確定</a>
        </div>
      </article>
    </section>
    <script>
      function sort_local_storage(){
        var arr=[];
        for (var i=0; i<localStorage.length; i++){
          var k=localStorage.key(i);
          var v=localStorage.getItem(k); 
          arr.push(k + "+" + v);
          }
        arr.sort();
        arr.reverse();
        return arr;
        }
      function set_edit_memo(k){
        var v=localStorage.getItem(k);
        $("#edit_memo_key").val(k);
        $("#edit_memo_value").val(v);
        render_memo_list();
        $.mobile.changePage($("#page3"));
        }
      function render_memo_list(){
        $("#memo_list").empty();
        var memo_arr=sort_local_storage();
        for (var i=0; i<memo_arr.length; i++){
          var arr=memo_arr[i].split("+");
          var k=arr[0];
          var v=arr[1];
          var item="<tr><td>" + v + "</td>" +
                   "<td style='text-align: center;'>" + 
                   "<a href='#' data-role='button' " + 
                   "data-icon='edit' data-iconpos='notext' " + 
                   "data-mini='true' data-inline='true' " + 
                   "onclick='set_edit_memo(" + '"' + k + '"' +
                   ")'>編輯</a></td></tr>";
          $(item).appendTo("#memo_list").trigger("create");
          }
        }
      $(document).one("pageinit", function(){
        if (localStorage.length == 0){
          var item="<tr><td colspan=2>無備忘事項</td></tr>";
          $(item).appendTo("#memo_list").trigger("create");
          }
        else {render_memo_list();}
        $("#add_memo_btn").on("click", function(e) {
          var k=JSON.stringify(new Date()).substr(1,19);
          var v=$("#add_memo").val();
          localStorage.setItem(k, v); 
          $("#add_memo").val("");
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#confirm_clear_btn").on("click", function(e) {
          localStorage.clear();
          $("#memo_list").empty();
          var item="<tr><td colspan=2>無備忘事項</td></tr>";
          $(item).appendTo("#memo_list").trigger("create");
          $.mobile.changePage($("#page1"));
          });
        $("#edit_memo_btn").on("click", function(e) {
          $.mobile.changePage($("#page3"));
          });
        $("#confirm_remove_btn").on("click", function(e) {
          var k=$("#edit_memo_key").val();
          localStorage.removeItem(k);
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#update_memo_btn").on("click", function(e) {
          var k=$("#edit_memo_key").val();
          var v=$("#edit_memo_value").val();
          localStorage.setItem(k, v);
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        }); 
    </script>
  </body>
</html>

上面的網頁程式原始碼中有三個 page 與兩個 dialog, 功能說明如下 : 
  • page1 :
    以表格顯示備忘記事列表 (上方有新增按鈕至 page2, 表格第二欄有編輯鈕至 page3.
  • page2 : 
    新增記事表單, 按 "儲存" 鈕將記事存入 localStorage 後自動返回 page1 記事列表.
  • page3 :
    編輯記事表單, 更改記事內容後按 "確定" 更新 localStorage 後自動返回 page1 記事列表; 按 "刪除" 則從 localStorage 刪除該資料後自動返回 page1 記事列表.
  • dialog1 : 
    詢問是否清除全部 localStorage 資料的對話框, 按 "確定" 清除 localStorage 全部資料後自動返回 page1 記事列表.
  • dialog2 :
    詢問是否刪除 localStorage 中指定 key 資料的對話框, 按 "確定" 刪除 localStorage 該筆資料後自動返回 page1 記事列表 
Javascript 程式碼部分有三個函式 :
  • sort_local_storage() :
    此函式用來將 localStorage 內儲存的資料進行排序, 由於物件的內容 (鍵值對) 是無序的, 因此沒有 sort() 方法可用來排序, 此函式的作法是用迴圈走訪 localStorage 儲存區的全部資料, 然後將 key 與 value 用 "+" 串接後存入空陣列, 再用陣列的 sort() 方法做升序排序, 最後呼叫 reverse() 做降序排序, 再將此陣列傳回. 呼叫者走訪此陣列, 將元素以 "+" 拆分即可得到排序後的 key 與 value 了, 參考 :
    # HTML5 local storage sort
  • set_edit_memo(k) : 
    此函式是使用者在按下記事列表中的編輯鈕後被呼叫, 目的是將傳進來的鍵 (k) 與查得之值設定到 page2 的編輯欄位中 (key 用隱藏欄位儲存), 以便稍後切換到 page2 時會顯示被編輯的記事內容. 
  • render_memo_list() : 
    此函式會先呼叫 sort_local_storage() 取得排序後的資料陣列, 然後在走訪此陣列的迴圈中以 "+" 為界拆解出 key 與 value, 用來產生表格內容 (tbody 元素的子元素 tr 與 td), 然後呼叫 appendTo() 方法將這些內容掛到 tbody 底下, 接著呼叫 trigger("create") 方法更新 DOM 樹. 每次做新增與刪除這兩種改變儲存區內容的動作後都會先呼叫此函式來更新內容, 再切換到 page1 去. 
其他在 pageinit 事件內部的程式碼都是用 jQuery 來處理元件觸發的事件. 結果如下 : 

剛使用此網頁 App 時因為 localStorage 為空, 故記事列表亦為空 :




按 "新增" 鈕會切換到記事輸入頁 (page2), 填寫後按 "確定" 會將此資料存入 localStorage 儲存區, 並轉回記事列表頁 (page1) : 





繼續輸入第二筆備忘記事 : 





按下第二欄的編輯按鈕會切換到 page3, 可對記事內容進行修改, 按 "確定" 會更新 localStorage 內的資料, 然後轉回記事列表頁 (page1) : 





若在編輯頁面按下 "刪除" 鈕, 則會跳出確認對話框 (dialog2), 按 "確定" 會從 localStorage 內刪除該筆資料, 然後轉到記事列表 (page1) 頁面 : 





按頁面右上角的 "清除" 鈕也會跳出對話框 (dialog1), 詢問是否要清除全部 localStorage 資料, 按 "確定" 會刪除全部資料後跳轉 page1, 這時就會回復到空的記事列表了 : 




感覺似乎與後端資料庫的 CRUD 操作一模一樣, 但其實是在存取本機客戶端的 localStorage 而已, 資料並沒有透過網路傳輸, 亦即完全是是離線作業, 非常適合用來儲存個人資料. 


二. 將 jQuery Mobile 行動網頁程式轉成 Android App (apk 檔) : 

完成 jQuery Mobile 網頁應用程式設計後即可利用 appsgeyser 這個免費的線上轉換服務將行動網頁 App 轉成 Android 的 App, 其網址為 :


作法參考 :


為了讓 appgeyser 能讀取網頁原始檔, 要先將上面的 jQuery Mobile 網頁放到一個伺服器上, 最方便的是靜態網頁伺服器 GitHub, 參考 : 


然後還要幫 App 準備一個 icon, 我搜尋 "free memo icon" 找到一個適合的免費圖檔來修改, appgeyser 要求圖檔大小不可超過 512*512. 準備好就可以按下 "CREATE APP NOW" 鈕了 : 




在 "Website URL" 欄填入行動網頁 App 的網址後按 "NEXT" : 




在 "APP NAME" 欄填入 App 名稱 (中英文皆可, 不要太長), 按 "NEXT" :




點選 "Custom icon", 再按右邊的 "Upload 512*512" 上傳圖檔 : 



在彈出頁面中移動底下的滑桿選擇範圍, 然後按底下的 "Crop" 鈕裁切並返回設定頁面 : 




按 "NEXT" 鈕 :




按 "CREATE" 鈕進行 Apk 檔製作, 這時會要求註冊帳號 (過程略) : 




Apk 檔建立完成顯示如下頁面, 按最上方的下載按鈕即可下載 apk 檔 : 

 


也可以用手機掃 QR code 下載或郵寄到註冊信箱 : 






下載後開啟 apk 檔安裝 : 






彈出安全性提示, 選擇 "仍要安裝" 與 "不傳送" : 






完成安裝後桌面就出現此 App 了, 點選執行結果與上面行動網頁 App 完全一樣 : 





OK, 總算完工啦!

參考資料 : 


2021年3月8日 星期一

jQuery Mobile 學習筆記 (二十一) : 用 jqPlot 繪圖 (2) : 長條圖

在上一篇測試中已完成 jqPlot 預設的折線圖測試, 參考 : 


本篇記錄長條圖之測試, 這必須另外匯入 barRenderer 與 categoryAxisRenderer 函式庫, 由於前一篇測試所使用的 CDN 網站 cdn.jsdelivr.net 並未提供這兩個函式庫, 因此本篇開始改用 cdnjs.cloudflare.com 所提供的 CDN 服務, 參考 :


以下是長條圖測試所需繪入的全部函式庫如下 : 

    <link href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/jquery.jqplot.min.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/jquery.jqplot.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.barRenderer.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.categoryAxisRenderer.min.js"></script>

jqPlot 的長條圖教學文件參考:


下面是基本長條圖的範例, 此圖只有一組資料, 但選項設定重點是在 renderer 屬性 :



<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta http-equiv="cache-control" content="no-cache">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/jquery.jqplot.min.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/jquery.jqplot.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.barRenderer.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.categoryAxisRenderer.min.js"></script>
  </head>
  <style>
  </style>
  <body>
    <!-- 第一頁頁面 -->
    <section data-role="page" id="page1">
      <header data-role="header">
        <h1>jqPlot 長條圖</h1>
      </header>
      <article data-role="content">
        <div id="chart"></div>
      </article>
      <footer data-role="footer">
        <h3>頁尾</h3>
      </footer>
    </section>
    <script> 
      $(function(){
        var data=[[608590, 5522119, 8170231]];
        var ticks=["宋楚瑜", "韓國瑜", "蔡英文"];
        var options={title: "2020 台灣總統大選得票數",
                     seriesDefaults:{renderer: $.jqplot.BarRenderer,
                                     pointLabels: {show: true}},
                     axes: {xaxis: {renderer: $.jqplot.CategoryAxisRenderer,
                                    ticks: ticks}}
                     };
        $.jqplot('chart', data, options);
        });
    </script>
  </body>
</html>

結果如下 : 




測試網頁 QR code 如下 :




上例只有一組序列 (series) 資料, 即各候選人之得票數, 如果有兩組以上資料要用長條圖呈現做比較, 只要在 data 參數傳入多組資料即可, 另外, 對於多組資料必須附上圖例 (legend), 否則會讓人搞不清楚哪個柱狀圖代表哪一組資料, 例如比較近五年元大 ETF 0050 與 0056 之配息 : 



<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta http-equiv="cache-control" content="no-cache">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/jquery.jqplot.min.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/jquery.jqplot.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.barRenderer.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.categoryAxisRenderer.min.js"></script>
  </head>
  <style>
  </style>
  <body>
    <!-- 第一頁頁面 -->
    <section data-role="page" id="page1">
      <header data-role="header">
        <h1>jqPlot 長條圖</h1>
      </header>
      <article data-role="content">
        <div id="chart"></div>
      </article>
      <footer data-role="footer">
        <h3>頁尾</h3>
      </footer>
    </section>
    <script> 
      $(function(){
        var div0050=[0.85, 2.4, 2.9, 3, 3.6];
        var div0056=[1.3, 0.95, 1.45, 1.8, 1.6];
        var ticks=["2016","2017", "2018", "2019", "2020"];
        var options={title: "元大 0050 vs 0056 配息 (元)",
                     animate: !$.jqplot.use_excanvas,
                     seriesDefaults:{renderer: $.jqplot.BarRenderer,
                                     pointLabels: {show: true}},
                     axes: {xaxis: {renderer: $.jqplot.CategoryAxisRenderer,
                                    ticks: ticks}},
                     series: [{label: "0056"},  
                              {label: "0050"}],  
                     legend: {show: true,  
                              location: "nw"}  
                     };
        $.jqplot('chart', [div0056, div0050], options);
        });
    </script>
  </body>
</html>

此處為了清楚起見, 將兩組資料 s1, s2 各以一維陣列表示, 然後外面再套一層 [s1, s2] 變成二維陣列, 結果如下 : 




測試網頁 QR code 如下 :




如果希望在點擊柱狀時得知其值, 可將畫布綁定 jqplotDataClick 事件, 從其事件處理回呼函數的 data 參數即可取得該圖形中被點擊之長條之值, 例如 :



<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta http-equiv="cache-control" content="no-cache">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/jquery.jqplot.min.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/jquery.jqplot.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.barRenderer.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.categoryAxisRenderer.min.js"></script>
  </head>
  <style>
  </style>
  <body>
    <!-- 第一頁頁面 -->
    <section data-role="page" id="page1">
      <header data-role="header">
        <h1>jqPlot 長條圖</h1>
      </header>
      <article data-role="content">
        <div id="chart"></div>
        <div id="info"></div>
      </article>
      <footer data-role="footer">
        <h3>頁尾</h3>
      </footer>
    </section>
    <script> 
      $(function(){
        var div0050=[0.85, 2.4, 2.9, 3, 3.6];
        var div0056=[1.3, 0.95, 1.45, 1.8, 1.6];
        var ticks=["2016","2017", "2018", "2019", "2020"];
        var options={title: "元大 0050 vs 0056 配息 (元)",
                     seriesDefaults:{renderer: $.jqplot.BarRenderer,
                                     pointLabels: {show: true}},
                     axes: {xaxis: {renderer: $.jqplot.CategoryAxisRenderer,
                                    ticks: ticks}},
                     series: [{label: "0056"},
                              {label: "0050"}],
                     legend: {show: true,
                              location: "nw"}
                     };
        $.jqplot('chart', [div0056, div0050], options);
        $('#chart').on('jqplotDataClick', function (e, series, point, data) {  
          var html='series:' + series + ', point: ' + point + ', data: ' + data;  
          $('#info').html(html);   
          });
        });
    </script>
  </body>
</html>

此例是從上面測試 2 修改而來, 主要是在畫布下添加一個 id="info" 的 div 元素, 以及為畫布綁定 jqplotDataClick 事件, 其回呼函數匯傳入四個參數, e 為事件物件, series 為資料組別索引 (0 起始), point 是資料點的索引 (0 起始), data 是資料, 但它是由 [序號 (1 起始), 資料] 組成的陣列, 所以真正的資料本身是 data[1], 結果如下 : 




此圖為點擊最右邊 2020 年 0050 配息之長條, 顯示其配息為 3.6 元. 測試網頁 QR code 如下 :




長條圖也可以水平呈現, 只要將 X, Y 軸交換即可, 作法上需將 barDirection 屬性設為 horizontal, 同時 axes 屬性要將 Y 軸繪圖器指定為 CategoryAxisRenderer, 例如 : 



<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta http-equiv="cache-control" content="no-cache">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/jquery.jqplot.min.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/jquery.jqplot.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.barRenderer.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.categoryAxisRenderer.min.js"></script>
  </head>
  <style>
  </style>
  <body>
    <!-- 第一頁頁面 -->
    <section data-role="page" id="page1">
      <header data-role="header">
        <h1>jqPlot 長條圖</h1>
      </header>
      <article data-role="content">
        <div id="chart"></div>
        <div id="info"></div>
      </article>
      <footer data-role="footer">
        <h3>頁尾</h3>
      </footer>
    </section>
    <script> 
      $(function(){
        var div0050=[0.85, 2.4, 2.9, 3, 3.6];
        var div0056=[1.3, 0.95, 1.45, 1.8, 1.6];
        var ticks=["2016","2017", "2018", "2019", "2020"];
        var options={title: "元大 0050 vs 0056 配息 (元)",
                     seriesDefaults:{renderer: $.jqplot.BarRenderer,
                                     pointLabels: {show: true},
                                     rendererOptions: {barDirection: 'horizontal'}},
                     axes: {yaxis: {renderer: $.jqplot.CategoryAxisRenderer,
                                    ticks: ticks}},
                     series: [{label: "0056"},
                              {label: "0050"}],
                     legend: {show: true,
                              location: "se"}
                     };
        $.jqplot('chart', [div0056, div0050], options);
        $('#chart').on('jqplotDataClick', function (e, series, point, data) {
          var html='series:' + series + ', point: ' + point + ', data: ' + data;
          $('#info').html(html);
          });
        });
    </script>
  </body>
</html>

結果如下 : 




測試網頁 QR code 如下 :




參考 :