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;
          }

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

沒有留言 :