🦁
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