🦁

js メモ管理システムのコードを作成したので、参考用に公開しておきます。

に公開

相互リンク可能な文書管理システムを作成したので、ここに張り付けときます。
間違っている所は、指摘していただけるとうれしいです。

このシステムは、
データとして、

 * @property {number} id
 * @property {string} title
 * @property {string} content
 * @property {number[]} relatedIds
 * @property {number} createdAt - 作成日時 (UTCミリ秒タイムスタンプ)
 * @property {number} updatedAt - 最終更新日時 (UTCミリ秒タイムスタンプ)

のデータを持ちます。
このようなデータを持つことで、タグの機能とメモの機能を一つのデータ構造としてあらわすことができます。
以下に使用例を示すので、それを参考にしてください。



// --- 使用例 ---


const dataManager = new SimpleDataManager('mySimpleAppDataWithRelations_v3_timestamp_example');
//上でコンストラクタでストレージキーの設定をしています。お好きなものに変更してください。

// アイテム追加
console.log("\n--- Adding Items ---");
//引数 ( title,content )
const Item1 = dataManager.addItem("会議メモ", "会議で何をメモするん??");
const Item2 = dataManager.addItem("アイデア", "アイデアふってこ---い");
const Item3 = dataManager.addItem("テストタイトル", "内容内容内容内容テストテストテスト");


dataManager.linkItems(Item1.id, Item2.id);
dataManager.linkItems(Item1.id, Item3.id);
var p = dataManager.getRelatedItems(Item1.id);
console.log(p);
for(var i = 0;i < p.length;i++){
  console.log("タイトルを取得できるか確かめる " + p[i].title);
  p[i].content = "勝手に変更してみた。へへへ。";
  dataManager.updateItem(p[i].id,p[i]);

}
console.log("内容の一覧を表示");
console.log(" ");
console.log("");
p = dataManager.getAllItems();
for(var i = 0;i < p.length;i++){
  console.log(p[i].title +"   "+p[i].content);
  

}
var _inst = dataManager.getItemById(Item1.id);
if(_inst.id == Item1.id){

  console.log("検索が正常に動作していると思われます");
}else{

  console.log("あれ?検索うまくいってないぞ");
}


// 少し待ってから更新日時を確認するために追加
setTimeout(() => {
    console.log("\n--- Updating Item (ID: 1) after delay ---");
    const updatedItem1 = dataManager.updateItem(Item1.id, { content: "プロジェクトAの進捗確認と次のアクション" });

    // 追加・取得したアイテムの時刻を確認
    console.log("\n--- Checking Timestamps ---");
    const fetchedItem1 = dataManager.getItemById(Item1?.id);
    const fetchedItem2 = dataManager.getItemById(Item2?.id);

    if (fetchedItem1) {
        console.log(`Item 1 (ID: ${fetchedItem1.id}):`);
        console.log("  createdAt:", fetchedItem1.createdAt); // Dateオブジェクト
        console.log("  updatedAt:", fetchedItem1.updatedAt); // Dateオブジェクト
        console.log("  createdAt (Locale):", fetchedItem1.createdAt.toLocaleString()); // ローカル時刻表示
        console.log("  updatedAt (Locale):", fetchedItem1.updatedAt.toLocaleString()); // ローカル時刻表示
    }
    if (fetchedItem2) {
        console.log(`Item 2 (ID: ${fetchedItem2.id}):`);
        console.log("  createdAt:", fetchedItem2.createdAt); // Dateオブジェクト
        console.log("  updatedAt:", fetchedItem2.updatedAt); // Dateオブジェクト
        console.log("  createdAt (Locale):", fetchedItem2.createdAt.toLocaleString()); // ローカル時刻表示
        console.log("  updatedAt (Locale):", fetchedItem2.updatedAt.toLocaleString()); // ローカル時刻表示
    }


}, 3000); // 3秒待つ


/**
 * @typedef {object} DataItem
 * @property {number} id - 一意な数値ID
 * @property {string} title - 項目のタイトル
 * @property {string} content - 項目の内容
 * @property {number[]} relatedIds - 関連する他の項目のID配列
 * @property {Date} createdAt - 作成日時 (内部ではDateオブジェクトとして扱う)
 * @property {Date} updatedAt - 最終更新日時 (内部ではDateオブジェクトとして扱う)
 */

/**
 * @typedef {object} StoredDataItem
 * @property {number} id
 * @property {string} title
 * @property {string} content
 * @property {number[]} relatedIds
 * @property {number} createdAt - 作成日時 (UTCミリ秒タイムスタンプ)
 * @property {number} updatedAt - 最終更新日時 (UTCミリ秒タイムスタンプ)
 */

/**
 * シンプルなデータ管理を行うクラス(関連付け機能付き・時刻ずれ対策)
 * ローカルストレージにデータを永続化する
 */
class SimpleDataManager {
    /** @type {string} ローカルストレージで使用するキー */
    #storageKey;
    /** @type {Array<DataItem>} データ項目の内部配列 (Dateオブジェクトを保持) */
    #items;
    /** @type {number} 次に割り当てるID */
    #nextId;
  
    /**
     * @param {string} storageKey ローカルストレージで使用するキー
     */
    constructor(storageKey = 'simpleDataWithRelations_v3_timestamp') { // キー名を変更
      if (typeof storageKey !== 'string' || storageKey.trim() === '') {
          throw new Error("ストレージキーは空でない文字列である必要があります。");
      }
      this.#storageKey = storageKey;
      this.#items = this.#loadFromLocalStorage(); // Dateオブジェクトに変換して読み込む
      this.#nextId = this.#calculateNextId();
      console.log(`SimpleDataManager initialized. Storage key: "${this.#storageKey}", Items loaded: ${this.#items.length}`);
    }
  
    // --- Private Helper Methods ---
  
    /**
     * ローカルストレージからデータを読み込み、Dateオブジェクトに変換する
     * @returns {Array<DataItem>} 読み込んだデータの配列 (Dateオブジェクトを含む)
     * @private
     */
    #loadFromLocalStorage() {
      try {
        const data = localStorage.getItem(this.#storageKey);
        if (!data) {
          return [];
        }
        /** @type {Array<StoredDataItem>} */
        const parsedData = JSON.parse(data);
  
        if (!Array.isArray(parsedData)) {
            console.warn("ローカルストレージのデータ形式が不正です(配列ではありません)。空のデータとして扱います。");
            return [];
        }
  
        return parsedData.map(item => {
          // 基本的な型チェック
          const id = typeof item.id === 'number' && Number.isInteger(item.id) ? item.id : 0;
          const title = typeof item.title === 'string' ? item.title : '';
          const content = typeof item.content === 'string' ? item.content : '';
          const relatedIds = Array.isArray(item.relatedIds) ? item.relatedIds.filter(rid => typeof rid === 'number' && Number.isInteger(rid)) : [];
  
          // --- 時刻データの読み込みとDateオブジェクトへの変換 ---
          // タイムスタンプが数値であることを確認
          const createdAtTimestamp = typeof item.createdAt === 'number' ? item.createdAt : 0;
          const updatedAtTimestamp = typeof item.updatedAt === 'number' ? item.updatedAt : 0;
  
          // タイムスタンプからDateオブジェクトを生成
          const createdAt = new Date(createdAtTimestamp);
          const updatedAt = new Date(updatedAtTimestamp);
          // ----------------------------------------------------
  
          if (id <= 0) {
              console.warn("不正なIDを持つアイテムをスキップしました:", item);
              return null;
          }
  
          // 内部的にはDateオブジェクトとして保持
          return { id, title, content, relatedIds, createdAt, updatedAt };
        }).filter(item => item !== null);
  
      } catch (e) {
        console.error(`ローカルストレージからのデータ読み込みに失敗しました (key: ${this.#storageKey}):`, e);
        return [];
      }
    }
  
    /**
     * 現在のデータをタイムスタンプ形式でローカルストレージに保存する
     * @private
     */
    #saveToLocalStorage() {
      try {
        // --- 保存するデータ形式に変換 (Date -> Timestamp) ---
        /** @type {Array<StoredDataItem>} */
        const dataToStore = this.#items.map(item => ({
            id: item.id,
            title: item.title,
            content: item.content,
            relatedIds: item.relatedIds,
            createdAt: item.createdAt.getTime(), // Dateオブジェクトからタイムスタンプを取得
            updatedAt: item.updatedAt.getTime()  // Dateオブジェクトからタイムスタンプを取得
        }));
        // ----------------------------------------------------
  
        localStorage.setItem(this.#storageKey, JSON.stringify(dataToStore));
      } catch (e) {
        console.error(`ローカルストレージへのデータ保存に失敗しました (key: ${this.#storageKey}):`, e);
      }
    }
  
    /**
     * 次に使用するIDを計算する
     * @returns {number} 次のID
     * @private
     */
    #calculateNextId() {
      if (this.#items.length === 0) {
        return 1;
      }
      return Math.max(...this.#items.map(item => item.id)) + 1;
    }
  
    /**
     * IDに基づいてアイテムのインデックスを検索する
     * @param {number} id 検索するID
     * @returns {number} 見つかったアイテムのインデックス、見つからなければ -1
     * @private
     */
    #findIndexById(id) {
       return this.#items.findIndex(item => item.id === id);
    }
  
    /**
     * アイテムオブジェクトの安全なコピーを作成する (Dateオブジェクトもコピー)
     * @param {DataItem} item コピー元のアイテム
     * @returns {DataItem} コピーされたアイテム
     * @private
     */
    #cloneItem(item) {
        return {
            ...item,
            relatedIds: [...item.relatedIds],
            // Dateオブジェクトは不変ではないため、コピーを作成する
            createdAt: new Date(item.createdAt.getTime()),
            updatedAt: new Date(item.updatedAt.getTime())
         };
    }
  
    // --- Public API Methods (変更なし、内部でDateオブジェクトを扱う) ---
  
    /**
     * 新しいデータ項目を追加する
     * @param {string} title 項目のタイトル (空でない文字列)
     * @param {string} content 項目の内容 (文字列)
     * @returns {DataItem | null} 追加されたデータ項目オブジェクト(コピー)、失敗した場合はnull
     */
    addItem(title, content) {
      if (typeof title !== 'string' || title.trim() === '') {
        console.error("addItem Error: タイトルは空でない文字列である必要があります。");
        return null;
      }
      if (typeof content !== 'string') {
        console.error("addItem Error: 内容は文字列である必要があります。");
        return null;
      }
  
      const now = new Date(); // 現在時刻でDateオブジェクト生成
      const newItem = {
        id: this.#nextId++,
        title: title.trim(),
        content: content,
        relatedIds: [],
        createdAt: now, // Dateオブジェクトを保持
        updatedAt: now, // Dateオブジェクトを保持
      };
  
      this.#items.push(newItem);
      this.#saveToLocalStorage(); // 保存時にタイムスタンプに変換される
      console.log(`Item added (ID: ${newItem.id})`);
      return this.#cloneItem(newItem);
    }
  
    /**
     * IDを指定してデータ項目を取得する
     * @param {number} id 検索する項目のID (整数)
     * @returns {DataItem | undefined} 見つかった項目オブジェクト(コピー)、見つからなければundefined
     */
    getItemById(id) {
      if (typeof id !== 'number' || !Number.isInteger(id)) {
         console.warn("getItemById Warning: 検索するIDは整数である必要があります。");
         return undefined;
      }
      const item = this.#items.find(item => item.id === id);
      return item ? this.#cloneItem(item) : undefined;
    }
  
    /**
     * タイトルに含まれる文字列で項目を検索する(大文字小文字を区別しない)
     * @param {string} query 検索クエリ(タイトルの一部)
     * @returns {Array<DataItem>} 見つかった項目の配列(各要素はコピー)
     */
    searchByTitle(query) {
      if (typeof query !== 'string') {
          console.warn("searchByTitle Warning: 検索クエリは文字列である必要があります。");
          return [];
      }
      if (query.trim() === '') {
          return this.getAllItems();
      }
      const lowerCaseQuery = query.toLowerCase();
      return this.#items
        .filter(item => item.title.toLowerCase().includes(lowerCaseQuery))
        .map(item => this.#cloneItem(item));
    }
  
    /**
     * IDを指定して項目を更新する
     * @param {number} id 更新する項目のID (整数)
     * @param {object} updates 更新する内容 { title?: string, content?: string }
     * @returns {DataItem | null} 更新された項目オブジェクト(コピー)、IDが見つからない/更新がない場合はnull
     */
    updateItem(id, updates) {
      if (typeof id !== 'number' || !Number.isInteger(id)) {
          console.error("updateItem Error: 更新するIDは整数である必要があります。");
          return null;
      }
      if (typeof updates !== 'object' || updates === null) {
          console.error("updateItem Error: 更新内容はオブジェクトである必要があります。");
          return null;
      }
  
      const itemIndex = this.#findIndexById(id);
      if (itemIndex === -1) {
        console.warn(`updateItem Warning: ID ${id} の項目が見つかりませんでした。`);
        return null;
      }
  
      const itemToUpdate = this.#items[itemIndex];
      let updated = false;
  
      if (updates.hasOwnProperty('title')) {
          if (typeof updates.title === 'string' && updates.title.trim() !== '' && itemToUpdate.title !== updates.title.trim()) {
              itemToUpdate.title = updates.title.trim();
              updated = true;
          } else if (typeof updates.title !== 'string' || updates.title.trim() === '') {
               console.warn(`updateItem Warning (ID: ${id}): titleは空でない文字列である必要があります。更新はスキップされました。`);
          }
      }
      if (updates.hasOwnProperty('content')) {
           if (typeof updates.content === 'string' && itemToUpdate.content !== updates.content) {
              itemToUpdate.content = updates.content;
              updated = true;
          } else if (typeof updates.content !== 'string') {
              console.warn(`updateItem Warning (ID: ${id}): contentは文字列である必要があります。更新はスキップされました。`);
          }
      }
  
      if (updated) {
          itemToUpdate.updatedAt = new Date(); // 更新日時を現在のDateオブジェクトで更新
          this.#saveToLocalStorage(); // 保存時にタイムスタンプに変換
          console.log(`Item updated (ID: ${id})`);
          return this.#cloneItem(itemToUpdate);
      } else {
          console.log(`Item not updated (ID: ${id}) - No changes provided or necessary.`);
          return null;
      }
    }
  
    /**
     * 二つの項目を相互に関連付ける
     * @param {number} id1 項目1のID (整数)
     * @param {number} id2 項目2のID (整数)
     * @returns {boolean} 関連付けに成功した場合はtrue、失敗した場合はfalse
     */
    linkItems(id1, id2) {
      if (typeof id1 !== 'number' || !Number.isInteger(id1) || typeof id2 !== 'number' || !Number.isInteger(id2)) {
          console.error("linkItems Error: IDは整数である必要があります。");
          return false;
      }
      if (id1 === id2) {
          console.warn("linkItems Warning: 同じ項目同士を関連付けることはできません。");
          return false;
      }
  
      const index1 = this.#findIndexById(id1);
      const index2 = this.#findIndexById(id2);
  
      if (index1 === -1 || index2 === -1) {
        console.error(`linkItems Error: 関連付けようとした項目が見つかりません (ID1: ${id1} ${index1 === -1 ? 'not found' : ''}, ID2: ${id2} ${index2 === -1 ? 'not found' : ''})`);
        return false;
      }
  
      const item1 = this.#items[index1];
      const item2 = this.#items[index2];
      let updated = false;
      const now = new Date(); // 現在時刻のDateオブジェクト
  
      if (!item1.relatedIds.includes(id2)) {
        item1.relatedIds.push(id2);
        item1.updatedAt = now; // Dateオブジェクトで更新
        updated = true;
      }
      if (!item2.relatedIds.includes(id1)) {
        item2.relatedIds.push(id1);
        item2.updatedAt = now; // Dateオブジェクトで更新
        updated = true;
      }
  
      if (updated) {
        this.#saveToLocalStorage(); // 保存時にタイムスタンプに変換
        console.log(`Items linked (ID: ${id1} <-> ID: ${id2})`);
        return true;
      } else {
        console.log(`Items already linked or no update needed (ID: ${id1}, ID: ${id2})`);
        return false;
      }
    }
  
    /**
     * 二つの項目の関連付けを解除する
     * @param {number} id1 項目1のID (整数)
     * @param {number} id2 項目2のID (整数)
     * @returns {boolean} 関連付け解除に成功した場合はtrue、失敗した場合はfalse
     */
    unlinkItems(id1, id2) {
      if (typeof id1 !== 'number' || !Number.isInteger(id1) || typeof id2 !== 'number' || !Number.isInteger(id2)) {
          console.error("unlinkItems Error: IDは整数である必要があります。");
          return false;
      }
       if (id1 === id2) {
          console.warn("unlinkItems Warning: 同じ項目同士の関連付け解除はできません。");
          return false;
      }
  
      const index1 = this.#findIndexById(id1);
      const index2 = this.#findIndexById(id2);
      let updated = false;
      const now = new Date(); // 現在時刻のDateオブジェクト
  
      if (index1 !== -1) {
          const item1 = this.#items[index1];
          const relatedIndex = item1.relatedIds.indexOf(id2);
          if (relatedIndex > -1) {
              item1.relatedIds.splice(relatedIndex, 1);
              item1.updatedAt = now; // Dateオブジェクトで更新
              updated = true;
          }
      } else {
           console.warn(`unlinkItems Warning: ID ${id1} の項目が見つかりません。`);
      }
  
      if (index2 !== -1) {
          const item2 = this.#items[index2];
          const relatedIndex = item2.relatedIds.indexOf(id1);
          if (relatedIndex > -1) {
              item2.relatedIds.splice(relatedIndex, 1);
              item2.updatedAt = now; // Dateオブジェクトで更新
              updated = true;
          }
      } else {
          console.warn(`unlinkItems Warning: ID ${id2} の項目が見つかりません。`);
      }
  
      if (updated) {
        this.#saveToLocalStorage(); // 保存時にタイムスタンプに変換
        console.log(`Items unlinked (ID: ${id1}, ID: ${id2})`);
        return true;
      } else {
        console.log(`No link found or no update needed between items (ID: ${id1}, ID: ${id2})`);
        return false;
      }
    }
  
    /**
     * 指定されたIDの項目に関連付けられているすべての項目を取得する
     * @param {number} id 基準となる項目のID (整数)
     * @returns {Array<DataItem>} 関連する項目の配列(各要素はコピー)
     */
    getRelatedItems(id) {
      if (typeof id !== 'number' || !Number.isInteger(id)) {
         console.warn("getRelatedItems Warning: IDは整数である必要があります。");
         return [];
      }
      const item = this.getItemById(id); // コピーが返る
      if (!item) {
        return [];
      }
      return item.relatedIds
        .map(relatedId => this.getItemById(relatedId)) // コピーが返る
        .filter(relatedItem => relatedItem !== undefined);
    }
  
  
    /**
     * IDを指定して項目を削除する(関連付けも解除する)
     * @param {number} id 削除する項目のID (整数)
     * @returns {boolean} 削除が成功した場合はtrue、失敗した場合はfalse
     */
    deleteItem(id) {
      if (typeof id !== 'number' || !Number.isInteger(id)) {
         console.error("deleteItem Error: 削除するIDは整数である必要があります。");
         return false;
      }
  
      const itemIndex = this.#findIndexById(id);
      if (itemIndex === -1) {
        console.warn(`deleteItem Warning: ID ${id} の項目が見つかりませんでした。`);
        return false;
      }
  
      const itemToDelete = this.#items[itemIndex];
  
      // 関連アイテムからのリンク解除 (unlinkItemsがsaveを呼ぶ)
      if (itemToDelete.relatedIds.length > 0) {
          console.log(`Removing links from related items to the item being deleted (ID: ${id})...`);
          [...itemToDelete.relatedIds].forEach(relatedId => {
              this.unlinkItems(id, relatedId); // 内部でupdatedAt更新と保存が行われる
          });
      }
  
      // アイテム自体を削除
      this.#items.splice(itemIndex, 1);
  
      // 最終的な状態を保存 (unlinkItemsで既に保存されているが念のため)
      this.#saveToLocalStorage();
      console.log(`Item deleted (ID: ${id})`);
      return true;
    }
  
    /**
     * すべての項目を取得する
     * @returns {Array<DataItem>} すべての項目の配列(各要素はコピー)
     */
    getAllItems() {
      return this.#items.map(item => this.#cloneItem(item));
    }
  
    /**
     * すべての項目を削除する(注意して使用!)
     * @returns {void}
     */
    clearAllItems() {
      const itemCount = this.#items.length;
      if (itemCount > 0) {
          this.#items = [];
          this.#nextId = 1;
          this.#saveToLocalStorage(); // 空の状態を保存
          console.log(`All ${itemCount} items have been cleared.`);
      } else {
          console.log("No items to clear.");
      }
    }
  }

Discussion