Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8691

Electronでタスク管理アプリ作ってみた

$
0
0

本記事について

こんにちは。あかいです。
この記事は、勉強を兼ねてElectronでタスク管理アプリを作成した際の備忘録です。
環境構築はすでに記事が出回っているので、作成までの検討事項を簡単にまとめます。
なお、簡単のため、アプリはローカルに閉じるものとし、Exe化までは行いません。

作成物

まず、今回作成したのは以下です。よくあるKanbanboardをイメージしています。
初心者なので、できるだけシンプルな構成となるよう1ページにしています。
test1.gif
test2.gif

環境

以下の通りです。

  • Node.js   : v12.19.0
  • jquery   : v3.5.1(CDN)
  • jquery-ui : v1.12.1(CDN)
  • Electron   : v10.1.3(ローカルインストール)
  • Bootstrap : v4.5.0(CDN)

(参考:Electronの環境構築(for Windows))

フレームワーク、ライブラリは、機能が実現でき、できるだけ学習コストが低そうな、環境構築の手間の少ないもの、を選んでいます。
WebまわりのGUIはReactやViewの記事が多くヒットしますが、初学ということで、長年使用されているjqueryとしました。

Electron概要

ドキュメント:https://www.electronjs.org/docs

Electron は ChromiumとNode.jsを利用しているため、HTML, CSS, JavaScriptを利用してアプリを開発することができます。

Electronではフロントエンドの技術でデスクトップアプリを作成することができます。
プロセスは、メインプロセスとレンダラープロセスに分けられます。
メインプロセスはpacakge.jsonにおいて、mainで指定したエントリポイントを起点として起動します。

package.json
{"name":"taskboard","version":"1.0.0","main":"index.js","author":"","description":""}

例えば上記のpackage.jsonであれば、index.js(任意名称、main.jsでも可)を起点として起動しますので、このindex.jsにElectronのAPIによるメインウィンドウの起動などを記述します。ここではElectron APIやNode.jsが使用できます。

index.js(一部)
"use strct";// Electronのモジュールconstelectron=require("electron");// アプリケーションをコントロールするモジュールconstapp=electron.app;// ウィンドウを作成するモジュールconstBrowserWindow=electron.BrowserWindow;// Electronの初期化完了後に実行app.on("ready",()=>{// ウィンドウサイズを1000*800(フレームサイズを含まない)に設定する// HTML側でNode.js使用可能とする(レンダラープロセスで使用可能)mainWindow=newBrowserWindow({width:1000,height:800,minWidth:700,minHeight:700,webPreferences:{nodeIntegration:true,nodeIntegrationInWorker:true}});// <= レンダラープロセス(mainWindow)}

レンダラープロセスはメインプロセスから呼び出され(BrowserWindow インスタンスとして生成され)ます。
したがって、メインプロセスは必ず1つですが、レンダラープロセスは複数存在しえます。
(本家のドキュメントが参考になります。https://www.electronjs.org/docs/tutorial/quick-start#create-a-basic-application)

レンダラープロセスは、メインプロセスのBrowserWindowで指定したHTMLや、そのHTML内で読み込んだcss, jsファイルで記述します。
通常のWebページと同じですが、Electronでは、レンダラープロセスのjsファイル内では(制限付きの)Electron APIやNode.jsが使用できます※。

※ElectronAPI自体の制限やデフォルトでは使えない場合があります。つまったところを参照してください。

作成までの流れ

1. 「タスク」でブレスト
2. タスク管理の仕様をまとめる
3. 実装のポリシー決め
4. 画面のワイヤーフレーム
5. 画面実装
6. 機能実装
7. テスト・要件の確認

1.「タスク」でブレスト

仕様を決定するにあたって、まずタスクとは何かを5分間で考えました。
「タスク」とは
 ・期限がある
 ・ステータスがある(登録済み、開始、待ち、終了)
 ・階層構造(プロジェクト→大タスク→中タスク...)がある。(際限がない)
 ・ステークホルダがある
 ・リマインドされる
 ・一覧がある(一意に特定される)
 ・詳細がある(メモ、関連)
 ・関連がある
 ・分類がある(習慣、臨時)
 ・進捗率がある
 ・所有者がある
 ・アウトプットがある(完了状態率などの統計情報、報告書など)

2.タスク管理の仕様をまとめる

ブレインストーミングの結果をグルーピングし、要件としてまとめます。

 ●構造

 ・(ルート→)プロジェクト→タスク→サブタスク(打ち止めとする)
 ・タスク間関連(フローチャート。今回は対象外とする。)

 ●要素

要素プロジェクトタスクサブタスク
id
名称
日付(開始、完了、期限)
詳細(メモ)
ステークホルダ
下位の統計情報
状態(待ち、実行中、完了)
分類(タグ)
関連(方向、プロパティ)
 ex)タスクA→タスクB、順序
添付ファイル

(凡例)
 ○:要素として持ちうる
 ✕:要素として持たない
 ◎:今回の対象とする
 △:一部、今回の対象とする

 ●機能

機能プロジェクトタスクサブタスク
要素の変更
追加
削除
統計情報の計算
フローの出力
報告書の出力
添付のアップ・ダウンロード

(凡例)
 ○:機能として持ちうる
 ✕:機能として持たない
 ◎:今回の対象とする

3.実装のポリシー決め

  • まずは動くものを作成し、作りながら改善する →CSS設計は考えず、必要あればリファクタリング
  • 機能はメインプロセスに集約し、レンダラープロセスは表示に専念
    • データ管理はメインプロセスで行い、レンダラープロセスはメインプロセスに問い合わせて表示を更新する
    • 簡単のため、レンダラープロセスの画面描画は一部の変更であってもすべて再描画する
    • 問い合わせはIPC通信を用いて、API的に使用する
    • IPC通信は非同期には行わず、すべて同期通信とする(後述のデータ保存の独立性のため)
  • 簡単のため、シングルページとする
  • データの保存にはファイルを利用する
    • JSON形式で保存し、簡単のため、毎回フルでの書き出しとする
    • 独立性のため、データの保存は並行して行わない

以下にポリシーに基づく処理の概略図を示します。
ElectronはElectronの概要に示す通り、メインプロセスとレンダラープロセスで動作します。今回はシングルページのためレンダラープロセスは一つだけです。メインプロセスからBrowserWindow()でウィンドウを作成し、mainWindow.loadURL(\`file://${__dirname}/index.html\`)でindex.htmlを読み込みます。
index.html内で指定したcss, jsをCDNおよびローカルから読み込み、メインプロセスにデータを要求し、メインプロセスはデータが存在しなければ、ローカルのjsonファイル(data.json)を読み込みます。(data.jsonには後述のデータ構造のjsonデータが保存されています。)
レンダラープロセスは、メインプロセスから返却されたデータに基づき、画面を描画します。
ユーザーが画面入力した場合は、起動時と同様に、レンダラープロセスからIPC同期通信で、入力に基づく要求をメインプロセス送り、メインプロセス側でデータ変更処理をかけた後、jsonファイルを更新し、レンダラープロセスに成功可否を連絡します。
レンダラープロセスでは、成功を受けた場合にメインプロセスにデータ要求をし、応答に基づいてページを再描画します。

image.png

次に示すのは、メインプロセスで操作し、ファイルに保存するオブジェクトの形式です。
メインプロセスにてこのオブジェクトを操作・保存し、レンダラプロセスで受け取って描画します。

data.json(作成例)
{"projects":[{"id":"project1","name":"プロジェクト1","tasks":[{"id":"task1","name":"タスク名","status":"Wait","start_date":"2020/11/12","due_date":"","end_date":"","detail":"","subtasks":[]},{"id":"task2","name":"タスク名","status":"Wait","start_date":"2020/11/12","due_date":"","end_date":"2020/11/12","detail":"","subtasks":[]},{"id":"task3","name":"タスク名","status":"Doing","start_date":"2020/11/12","due_date":"","end_date":"","detail":"","subtasks":[]}]},{"id":"project2","name":"プロジェクト2","tasks":[]},{"id":"project3","name":"プロジェクト名","tasks":[]}]}

4.画面のワイヤーフレーム

image.png
Googleスライドで作成しました。タスクを示すカードの下部に
・ステークホルダの表示
・タスク同士の関連を示す前後のタスク(前:タスク0、後:1)
がありますが、後々簡単のために取りやめました。

5.画面実装

画面表示は作成物と同様のためここでは省略します。
BootstrapやCSS、jqueryの実装例を検索しつつ、つぎはぎしました。ソースは最後に載せます。
今回はBEMなどのcss設計は全く意識していません。命名や実装に一貫性がないかもしれませんが、とりあえず動く、が目標のためご容赦ください。
(次回があればCSS設計完全ガイドを参考にしようと思っています。)

ファイル構成は次です。前述のindex.htmlを起点とし、画面左部(left_menu.css, left_menu.js)と画面右部(right_body.css, right_body.js)、共通処理(common.css, renderer.js)に分けて記載しています。
(data.json, index.js, pachage.jsonは前述。start.batはelectronの起動コマンドを記述しているだけです。)
image.png

(参考)

start.bat
rem .\node_modules\.bin\electron . --inspect-brk
.\node_modules\.bin\electron .
exit0



では、画面周りの処理内容を下記のタスク名を例に簡単に解説します。

HTML

タスク名は以下のようなHTMLです。なお、タスク全体はBootstrapのcardで作成しています。

index.html(一部)
<!-- カードタイトルここから --><divclass="card_title col-8"><divclass="wrap_task_name"><inputtype="text"class="task_name"></div><divclass="task_name_toolchip"></div></div><!-- カードタイトルここまで -->

表示最大幅を超えた入力に備えて、「タスク名…」で表示するためにwrap要素を追加しています。
また、「タスク名…」表記時にツールチップですべてを表示するようにします。

CSS

right_body.css(一部)
.task_name{font-size:1.5rem;margin:autoauto;width:100%;background-color:whitesmoke;border-radius:0.3rem;border:hidden;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.wrap_task_name{display:inline-block;overflow:hidden}.card_title{position:relative;/* for toolchip */}.task_name_toolchip{max-width:26rem;display:none;position:absolute;top:3.5em;left:5rem;z-index:9999;padding:0.3em0.5em;color:#FFFFFF;background:rgb(124,124,124);border-radius:0.5em;}.task_name_toolchip:after{width:100%;content:"";display:block;position:absolute;left:0.5em;top:-0.8rem;border-top:0.8remsolidtransparent;border-left:0.8remsolidrgb(124,124,124);}

overflow: hidden;
text-overflow: ellipsis;
が「タスク名…」表記のための部分です。また、wrapにもoverflow: hiddenを設定する必要があります。
(参考:入らなかった文字を三点リーダで省略表示)

javascript

タスク名の編集のためのjavascriptです。大きく分けて、フォーカス時操作とフォーカスアウト時操作,ツールチップ表示の3つを記述しています。

right_body.js(一部)
// イベント操作// 対象:タスク名// 動作:フォーカス// 内容:編集があった場合にタスク名を変更する。$(document).on('focus','.task_name',(e)=>{$(e.target).select();// エンター押下時にフォーカスアウト(Shift+EnterはOK)$(e.target).keypress(function(e){if(!event.shiftKey){if(e.keyCode==13){$(e.target).blur();}}});});// フォーカスアウト時に変更反映$(document).on('blur','.task_name',(e)=>{// 空なら変更せず再ロード。空でなければ変更を反映する。if($(e.target).val()){project_id=$('#left_menu div.project.active').attr("id");task_id=$(e.target).parents(".card").attr("id");task_name=$(e.target).val();rc=ipcRenderer.sendSync('changeTaskName',{project_id:project_id,task_id:task_id,task_name:task_name});if(rc){$(e.target).scrollLeft(0);// はみ出た分表示がずれるので最初の位置に戻す}}LOAD_TASKS();});// イベント操作// 対象:タスク名// 動作:マウスオーバー// 内容:ツールチップを表示する。$(document).on('mouseover','.task_name',(e)=>{if($(e.target).parent().width()<e.target.scrollWidth){vartask_name=$(e.target).val();$(e.target).closest(".card_title").find(".task_name_toolchip").text(task_name);$(e.target).closest(".card_title").find(".task_name_toolchip").css("display","block");$(e.target).on('mouseleave',()=>{$(e.target).closest(".card_title").find(".task_name_toolchip").css("display","none");})}});

まず、$(document).on('focus', '.task_name', (e) => {}のアロー関数に、task_nameクラスにフォーカスがあった場合のイベント処理を記述しています。
DOM変更時にイベントを反映させるため、$(セレクタ).on("イベント名", function())でなく、$(document).on("セレクタ", "イベント名", function())としています。
なお、task_nameクラスはindex.htmlで<input type="text" class="task_name">と定義しています。
ここのアロー関数内で記述しているのは2点で、
 ・ $(e.target).select();で、画面からユーザが編集しやすいように、タスク名を全選択する
 ・$(e.target).keypress(function(e){}でエンター(Shiftとの同時押しを除く)押下時にフォーカスアウトする
です。

次にフォーカスアウト時の操作を$(document).on('blur', '.task_name', (e) => {}に記述しています。
フォーカスアウト時には、タスク名が空でなければ変更を反映するようにしています。空の状態で編集を終えた場合は、LOAD_TASKS();(rendere.jsに記載)のところでタスク一覧の再読み込みを実施しているため、編集前の状態に戻ります。
タスク名が空でなければ、プロジェクトID、タスクID、変更したタスク名を取得し、rc = ipcRenderer.sendSync('changeTaskName', {project_id: project_id, task_id: task_id, task_name: task_name});の部分で、メインプロセスにタスク名変更処理をIPC同期通信で要求しています。プロジェクトIDは画面左部のプロジェクト一覧のうちアクティブな要素から、タスクIDは自身の親要素のうちタスクIDを要素のIDとして持っているcardクラスから取得しています。なお、一つ一つのタスクはBootstrapのcardで作成しています。
メインプロセスから正常終了処理が返ってきた場合にはタスク名の表示位置を戻す処理を$(e.target).scrollLeft(0); // はみ出た分表示がずれるので最初の位置に戻すで行っています。

ツールチップの表示はマウスオーバー時にCSSのdisplayをBlockに変更、マウスリムーブ時にCSSのdisplayをNoneに変更しています。
if ($(e.target).parent().width() < e.target.scrollWidth) {}この部分で表示最大幅を超えているかのチェックをしています。

画面の参考

また、ここでは特に触れませんでしたが、以下2点の実装時の参考先を載せておきます。

6.機能実装

機能はメインプロセスに実装します。既述のように、レンダラープロセスからAPI的にIPC通信を行います。
したがって、メインプロセス側ではレンダラープロセスからの通信要求を待ち受けて処理を返すような記述になります。
以下にメインプロセス側でのIPC通信の待ち受けの例を挙げます。

index.js(一部)
// on change taskName.ipcMain.on('changeTaskName',(event,arg)=>{// "arg" is "{project_id: , task_id: , task_name:}"[index_project,index_task]=searchTask(arg.project_id,arg.task_id);if(index_project!==-1&&index_task!==-1){projects[index_project].tasks[index_task].name=arg.task_name;event.returnValue=true;// if successfull, return true.data_control.set("projects",projects);}else{event.returnValue=false;// if failed, return false.}return});

ここではタスク名の変更を例に見ていきます。
メイン側ではipcMainという名称のAPIです。ipcMain.on('changeTaskName', (event, arg) => {}この部分のアロー関数内で、処理内容を記述しています。
同期通信の場合は、必ずreturnが必要となるので、event.returnValue = true; // if successfull, return true.の部分などでリターン内容を設定して、returnしています。
処理内容を簡単に見ていくと、
[index_project, index_task] = searchTask(arg.project_id, arg.task_id);この部分は、別途実装したsearchTask関数で、プロジェクトID、タスクIDから、projectsオブジェクト(プロジェクトすべての情報が入ったオブジェクト、data.jsonで読み書きするのもこのオブジェクト)内のアレイのインデックスを取得しています。
searchTask関数では、対象が見つからなかった場合に-1をインデックスとして返しますので、次の行のif (index_project !== -1 && index_task !== -1) {では対象タスクが見つかった場合に処理を続行するようなif文としています。
対象が見つかった場合は、projectsオブジェクトのタスク名をprojects[index_project].tasks[index_task].name = arg.task_name;で書き換えて、returnに成功を意図するtrueを返すよう設定しています。ちなみに、returnはオブジェクトも返せます。
最後に、次の行のdata_control.set("projects", projects);でdata.jsonにキー:projects、バリュー:projectsオブジェクト、として書き込みます。

次にdata_controlを確認します。

data_control.js(一部)
// data_control// require.constfs=require('fs');letdata_json={}// set as key-value. value can be json, string or list. exports.set=function(key,value){// set.data_json[key]=value;// rewrite into file.constdata_string=JSON.stringify(data_json);fs.writeFile(file_path,data_string,(err)=>{if(err)throwerr;});}

data_controlはデータのファイル書き込み、読み出し用の自作モジュールです。
data_controlは以下の実装ポリシーのもと、書き込み・読み出し時の、排他処理 と 検索処理を省いています。

  • IPC通信は非同期には行わず、すべて同期通信とする(後述のデータ保存の独立性のため)
  • JSON形式で保存し、簡単のため、毎回フルでの書き出しとする
  • 独立性のため、データの保存は並行して行わない

上のスクリプトでは、データ保存時の処理を記述しています。
ElectronではNode.jsのモジュールが使用可能です。ファイル操作のためconst fs = require('fs');で、fsをrequireします。
exports.set = function (key, value) {}にset関数を記述しています。set関数では、data_json[key] = value;でdata_json(書き出し用データ)にKey-value形式でデータを挿入し、const data_string = JSON.stringify(data_json);でjsonオブジェクトから文字列に変換、fs.writeFile(file_path, data_string, (err) => {}で書き込みの流れとなっています。書き込みエラー時には単にエラーを投げるようにしています。

(参考:Node.jsのfsモジュールの使い方)

7.テスト・要件の確認

軽く動かしてみて異常がないか、2. タスク管理の要件をまとめるを満たしているかを確認しました。
本来は、異常ケース含めてテストすべきですが、今回は省略しました。
また、性能は体感で問題なければOKとし、セキュリティもローカルで閉じることから問題なしとしました。

つまったところ

2点あります。
レンダラープロセスでは、

  • Node.jsが使えない
  • jqueryが使えない

使用したElectronのバージョンでは、デフォルト設定でレンダラプロセスを起こすと(Windowを作ると)、レンダラープロセス内でNode.jsが使えません。すなわち、require('electron')できないので、ElectronのAPIも使えません。
これは、セキュリティ対策のようで、以下のようにnodeIntegration: trueを設定してあげればOKです。

index.js(一部)
mainWindow=newBrowserWindow({width:1000,height:800,webPreferences:{nodeIntegration:true}});

(参考:【エラー対処】Electronでレンダラープロセスrequireができない)

これで、レンダラープロセスでもNode.jsは使えますが、まだjqueryが使えません。
これはjqueryの既知の問題のようで、

jQuery contains something along this lines:

if(typeofmodule==="object"&&typeofmodule.exports==="object"){// set jQuery in \`module\`}else{// set jQuery in \`window\`}

module is defined, even in the browser-side scripts. This causes jQuery to ignore the window object and use module, so >the other scripts won't find $ nor jQuery in global scope..

の通り、jqueryのソース内に、moduleが存在すればjqueryをwindowに設定しない分岐があり、このために$, jqueryがグローバルに設定されません。以下の参考記事では、

<script>if(typeofmodule==='object'){window.module=module;module=undefined;}</script>
jqueryなどの読み込み<script>if(window.module){module=window.module;}</script>

のように、jquery読み込み前にmoduleオブジェクトを退避し、moduleオブジェクトをundefinedに設定してから、jqueryを読み込んでグローバルに設定させ、読み込み後に退避したmoduleをもとに戻す作業で解決しています。
これにならって作成し、問題なく挙動しています。

(参考:ElectronでjQueryが読み込めない問題の解決策と原因)
(参考:jQuery isn't set globally because "module" is defined #254)

以上の2点ように、レンダラープロセスでモジュールが使えるような設定をしましたが、これはアプリがローカルに閉じることを前提としているために可能な対処です。
nodeIntegration: trueを設定すると、レンダラープロセスでネイティブな操作が可能になります。
したがって、ローカルでないアプリケーションを作成する場合、レンダラープロセスでのディレクトリ操作などが可能となるため、クロスサイトスクリプティングの危険度が上がります。

今回は特に実施していませんが、セキュアな通信には以下が参考になりそうです。
(参考:ElectronでcontextBridgeによる安全なIPC通信)
(参考:Electron(v10.1.5現在)の IPC 通信入門 - よりセキュアな方法への変遷)

なお、nodeIntegration: falseの場合、jqueryの読み込みのための処理も不要となるようですので、contextBrideを使用した通信にすると、nodeIntegrationをtrueにする必要がなくなるので、jqueryの読み込みの問題も発生しないかもしれません。
(参考:ElectronでjQueryがundefinedになる)

ソース

index.js
index.js
"use strct";// Electronのモジュールconstelectron=require("electron");// data_controlモジュールconstdata_control=require("./js/data_control")data_control.init(`${__dirname}/data`)// アプリケーションをコントロールするモジュールconstapp=electron.app;// ウィンドウを作成するモジュールconstBrowserWindow=electron.BrowserWindow;// ダイアログを作成するモジュールconstdialog=electron.dialog;// 通信用constipcMain=electron.ipcMain// メインウィンドウはGCされないようにグローバル宣言letmainWindow=null;// project作成用functionmakeProject(project_id){letproject={id:project_id,name:"プロジェクト名",tasks:[]}returnproject;}// task作成用functionmakeTask(task_id){lettask={id:task_id,name:"タスク名",status:"Wait",start_date:"",due_date:"",end_date:"",detail:"",subtasks:[]}returntask;}// subtask作成用functionmakeSubtask(subtask_id){letsubtask={id:subtask_id,name:"サブタスク",checked:false,}returnsubtask;}// 日付取得用functiontoday(){vardt=newDate();vary=dt.getFullYear();varm=("00"+(dt.getMonth()+1)).slice(-2);vard=("00"+dt.getDate()).slice(-2);varresult=y+"/"+m+"/"+d;returnresult;}// データ取得letprojects=data_control.get("projects");if(!projects.length){letproject=makeProject("project1");projects=[project];data_control.set("projects",projects);}// 全てのウィンドウが閉じたら終了app.on("window-all-closed",()=>{if(process.platform!="darwin"){app.quit();}});// Electronの初期化完了後に実行app.on("ready",()=>{// ウィンドウサイズを1000*800(フレームサイズを含まない)に設定する// HTML側でNode.js使用可能とする(レンダラープロセスで使用可能)mainWindow=newBrowserWindow({width:1000,height:800,minWidth:700,minHeight:700,webPreferences:{nodeIntegration:true}});//使用するhtmlファイルを指定するmainWindow.loadURL(`file://${__dirname}/index.html`);// ウィンドウが閉じられたらアプリも終了mainWindow.on("closed",()=>{mainWindow=null;});});////////////// IPC //////////////// on data.// return data to renderer.ipcMain.on('data',(event,arg)=>{letdata=data_control.get(arg);event.returnValue=data;return})// on message.// console.log(arg)ipcMain.on('message',(event,arg)=>{console.log(arg);})// on confirm.ipcMain.on('confirm',(event,arg)=>{// arg = {title: , message: }varoptions={type:'info',buttons:["OK","Cancel"],title:arg.title,message:arg.message};index=dialog.showMessageBoxSync(mainWindow,options);if(index===0){// "OK"event.returnValue=true;}elseif(index===1){// "Cancel"event.returnValue=false;}return})// on add project.ipcMain.on('addProject',(event)=>{// projectId採番for(leti=1;true;i++){if(projects.findIndex((project)=>{returnproject.id===`project${i}`;})===-1){PROJECT_ID=i;break;}}// project作成letproject=makeProject(`project${PROJECT_ID}`);// project追加projects.push(project);// データ保存data_control.set("projects",projects);// 成功時はtrueをreturnevent.returnValue=true;return})// on delete project.ipcMain.on('deleteProject',(event,arg)=>{// "arg" is "project_id"projects=projects.filter((project)=>{returnproject.id!==arg;});data_control.set("projects",projects);event.returnValue=true;return})// on change projectName.ipcMain.on('changeProjectName',(event,arg)=>{// "arg" : {project_id: , project_name: }varindex=-1;index=projects.findIndex((project)=>{returnproject.id===arg.project_id;})if(index!==-1){project=projects[index];project.name=arg.project_name;projects[index]=project;data_control.set("projects",projects);event.returnValue=true;}else{event.returnValue=false;}return})// on add task.ipcMain.on('addTask',(event,arg)=>{// "arg" : project_idvarindex=-1;index=projects.findIndex((project)=>{returnproject.id===arg;})if(index!==-1){project=projects[index];tasks=project.tasks;// taskId採番for(leti=1;true;i++){if(tasks.findIndex((task)=>{returntask.id===`task${i}`;})===-1){TASK_ID=i;break;}}// task作成lettask=makeTask(`task${TASK_ID}`);task.start_date=today();// task追加tasks.push(task);projects[index].tasks=tasks;// データ保存data_control.set("projects",projects);// 作成したtaskをreturnにセットevent.returnValue=true;}else{event.returnValue=false;}return});// on delete task.ipcMain.on('deleteTask',(event,arg)=>{// "arg" is "{project_id: , task_id: }"varindex=-1;index=projects.findIndex((project)=>{returnproject.id===arg.project_id;});if(index!==-1){projects[index].tasks=projects[index].tasks.filter((task)=>{returntask.id!==arg.task_id;});event.returnValue=true;data_control.set("projects",projects);}else{event.returnValue=false;}return});// on change taskName.ipcMain.on('changeTaskName',(event,arg)=>{// "arg" is "{project_id: , task_id: , task_name:}"[index_project,index_task]=searchTask(arg.project_id,arg.task_id);if(index_project!==-1&&index_task!==-1){projects[index_project].tasks[index_task].name=arg.task_name;event.returnValue=true;// if successfull, return true.data_control.set("projects",projects);}else{event.returnValue=false;// if failed, return false.}return});// on change taskStatus.ipcMain.on('changeTaskStatus',(event,arg)=>{// "arg" is "{project_id: , task_id: , task_status:}"[index_project,index_task]=searchTask(arg.project_id,arg.task_id);if(index_project!==-1&&index_task!==-1){projects[index_project].tasks[index_task].status=arg.task_status;if(arg.task_status==="Done"){projects[index_project].tasks[index_task].end_date=today();}event.returnValue=true;// if successfull, return true.data_control.set("projects",projects);}else{event.returnValue=false;// if failed, return false.}return});// on change taskDetail.ipcMain.on('changeTaskDetail',(event,arg)=>{// "arg" is "{project_id: , task_id: , task_detail:}"[index_project,index_task]=searchTask(arg.project_id,arg.task_id);if(index_project!==-1&&index_task!==-1){projects[index_project].tasks[index_task].detail=arg.task_detail;event.returnValue=true;// if successfull, return true.data_control.set("projects",projects);}else{event.returnValue=false;// if failed, return false.}return});// on change TaskStartDate.ipcMain.on('changeTaskDate',(event,arg)=>{// "arg" is "{project_id: , task_id: , task_(start|due|end)_date:}"varrc=true;[index_project,index_task]=searchTask(arg.project_id,arg.task_id);if(index_project!==-1&&index_task!==-1){if("task_start_date"inarg){projects[index_project].tasks[index_task].start_date=arg.task_start_date;}elseif("task_due_date"inarg){projects[index_project].tasks[index_task].due_date=arg.task_due_date;}elseif("task_end_date"inarg){projects[index_project].tasks[index_task].end_date=arg.task_end_date;}else{rc=false;// if failed, return false.}if(rc){data_control.set("projects",projects);}}else{rc=false;// if failed, return false.}event.returnValue=rc;return});// on add subtask.ipcMain.on('addSubtask',(event,arg)=>{// "arg" is "{project_id: , task_id:}"[index_project,index_task]=searchTask(arg.project_id,arg.task_id);if(index_project!==-1&&index_task!==-1){subtasks=projects[index_project].tasks[index_task].subtasks;// subtaskId採番for(leti=1;true;i++){if(subtasks.findIndex((subtask)=>{returnsubtask.id===`subtask${i}`;})===-1){SUBTASK_ID=i;break;}}// subtask作成letsubtask=makeSubtask(`subtask${SUBTASK_ID}`);// task追加subtasks.push(subtask);projects[index_project].tasks[index_task].subtasks=subtasks;// データ保存data_control.set("projects",projects);// 成否をreturnにセットevent.returnValue=true;}else{event.returnValue=false;}return});// on delete subtask.ipcMain.on('deleteSubtask',(event,arg)=>{// "arg" is "{project_id: , task_id: , subtask_id:}"[index_project,index_task]=searchTask(arg.project_id,arg.task_id);if(index_project!==-1&&index_task!==-1){subtasks=projects[index_project].tasks[index_task].subtasks;projects[index_project].tasks[index_task].subtasks=subtasks.filter((subtask)=>{returnsubtask.id!==arg.subtask_id;});data_control.set("projects",projects);event.returnValue=true;}else{event.returnValue=false;}return});// on change subtask_name.ipcMain.on('changeSubtaskName',(event,arg)=>{// "arg" is "{project_id: , task_id: , subtask_id: , subtask_name:}"[index_project,index_task,index_subtask]=searchSubtask(arg.project_id,arg.task_id,arg.subtask_id);if(index_project!==-1&&index_task!==-1&&index_subtask!==-1){projects[index_project].tasks[index_task].subtasks[index_subtask].name=arg.subtask_name;data_control.set("projects",projects);event.returnValue=true;}else{event.returnValue=false;}return});// on change subtask_checked.ipcMain.on('changeSubtaskChecked',(event,arg)=>{// "arg" is "{project_id: , task_id: , subtask_id: , subtask_checked:}"[index_project,index_task,index_subtask]=searchSubtask(arg.project_id,arg.task_id,arg.subtask_id);if(index_project!==-1&&index_task!==-1&&index_subtask!==-1){projects[index_project].tasks[index_task].subtasks[index_subtask].checked=arg.subtask_checked;data_control.set("projects",projects);event.returnValue=true;}else{event.returnValue=false;}return});///////////////// function /////////////////// Search the index of task from project_id and task_id.functionsearchTask(project_id,task_id){varindex_project=-1;varindex_task=-1;index_project=projects.findIndex((project)=>{returnproject.id===project_id;});if(index_project!==-1){index_task=projects[index_project].tasks.findIndex((task)=>{returntask.id===task_id;});}return[index_project,index_task];}// Search the index of subtask from project_id, task_id and subtask_id.functionsearchSubtask(project_id,task_id,subtask_id){varindex_project=-1;varindex_task=-1;varindex_subtask=-1;index_project=projects.findIndex((project)=>{returnproject.id===project_id;});if(index_project!==-1){index_task=projects[index_project].tasks.findIndex((task)=>{returntask.id===task_id;});if(index_task!==-1){index_subtask=projects[index_project].tasks[index_task].subtasks.findIndex((subtask)=>{returnsubtask.id===subtask_id;});}}return[index_project,index_task,index_subtask];}

data_control.js
data_control.js
// data_control// require.constfs=require('fs');// vers.letdata_json={}letfile_path=""exports.init=function(data_dir){file_path=data_dir+"/data.json"// load data file as json.letdata_string="";if(fs.existsSync(file_path)){data_string=fs.readFileSync(file_path,'utf8');if(data_string){data_json=JSON.parse(data_string)}}}// set as key-value. value can be json, string or list. exports.set=function(key,value){// set.data_json[key]=value;// rewrite into file.constdata_string=JSON.stringify(data_json);fs.writeFile(file_path,data_string,(err)=>{if(err)throwerr;});}exports.get=function(key){// get.constvalue=data_json[key]if(value){returnvalue;}else{returnundefined;}}

index.html
index.html
<!doctype html><htmllang="ja"><head><!-- Required meta tags --><metacharset="utf-8"><metaname="viewport"content="width=device-width, initial-scale=1, shrink-to-fit=no"><!-- Bootstrap CSS --><linkrel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"crossorigin="anonymous"><!-- Jquery UI theme CSS --><linkrel="stylesheet"href="http://ajax.googleapis.com/ajax/libs/jqueryui/1/themes/smoothness/jquery-ui.css"><!-- My CSS --><linkrel="stylesheet"href="css/common.css"><linkrel="stylesheet"href="css/left_menu.css"><linkrel="stylesheet"href="css/right_body.css"><linkrel="stylesheet"href="css/icons.css"><title>タスク管理</title></head><body><header><h1id="title">Task Board</h1></header><divclass="container-fluid"id="contents"><!----------------------- モーダルエリアここから -----------------------><sectionid="modalArea"class="modalArea fixed-top"><divid="modalBg"class="modalBg"></div><divclass="modalWrapper"><divclass="modalContents"><h1>削除しますか?</h1><buttonid="delete_project">削除</button></div><divid="closeModal"class="closeModal">×
          </div></div></section><!----------------------- モーダルエリアここまで -----------------------><divclass="row"id="main_content"><!----------------------- サイドバーここから -----------------------><divclass="col-3"id="left_menu"><h3>Projects</h3><hrsize=1color="white"><divclass="rounded-pill shadow"id="add_project"></div><ulid="menu_list"><templateid="project_template"><li><divclass="project rounded"id="project"contenteditable="false">project</div></li></template></ul></div><!----------------------- サイドバーここまで -----------------------><!----------------------- コンテンツここから -----------------------><divclass="col-9"id="right_body"><!-- カード一覧ここから --><!-- カード追加ここから --><divclass="rounded-pill shadow"id="add_task">+ タスク追加</div><!-- カード追加ここまで --><divclass="cards_list row"><divclass="board col-4"><divclass="board_name rounded">Wait</div><divclass="cards rounded"id="cards_wait"><!-- コンテンツテンプレート挿入位置1 --></div></div><divclass="board col-4"><divclass="board_name rounded">Doing</div><divclass="cards rounded"id="cards_doing"><!-- コンテンツテンプレート挿入位置2 --></div></div><divclass="board col-4"><divclass="board_name rounded">Done</div><divclass="cards rounded"id="cards_done"><!-- コンテンツテンプレート挿入位置3 --></div></div></div><!-- カード一覧ここまで --></div><!----------------------- コンテンツここまで -----------------------></div><!----------------------- テンプレートここから -----------------------><!-- タスクテンプレートここから --><templateid="task_template"><!-- カードここから --><divclass="card shadow"id="task"><!-- カードヘッダーここから --><divclass="card-header"><divclass="row"><!-- カードタイトルここから --><divclass="card_title col-8"><divclass="wrap_task_name"><inputtype="text"class="task_name"></div><divclass="task_name_toolchip"></div></div><!-- カードタイトルここまで --><!-- statusトグルここから --><divclass="col-4"id="status"><inputtype="radio"class="status_input Wait"name="status_radio"id="Wait"value="Wait"checked><labelclass = "status_label"for="Wait">Wait</label><inputtype="radio"class="status_input Doing"name="status_radio"id="Doing"value="Doing"><labelclass = "status_label"for="Doing">Doing</label><inputtype="radio"class="status_input Done"name="status_radio"id="Done"value="Done"><labelclass = "status_label"for="Done">Done</label></div><!-- statusトグルここまで --></div></div><!-- カードヘッダーここまで --><divclass="card-body"><!-- 日付表示ここから --><divclass="row"><divclass="col"id="start_date"><labelclass="date_label"for="Start">開始日:</label><inputtype="text"id="Start"class="date_input Start form-control"placeholder="日付を選択"readonly></div><divclass="col"id="due_date"><labelclass="date_label"for="Due">期日:</label><inputtype="text"id="Due"class="date_input Due form-control"placeholder="日付を選択"readonly></div><divclass="col"id="end_date"><labelclass="date_label"for="End">完了日:</label><inputtype="text"id="End"class="date_input End form-control"placeholder="日付を選択"readonly></div></div><!-- 日付表示ここまで --><!-- 詳細ここから --><textareatype="textarea"class="form-control rounded task_detail"id="detail_textarea"placeholder="詳細入力"></textarea><!-- 詳細ここまで --><!-- チェックリストここから --><!-- サブタスク追加ここから --><divclass="rounded-pill"id="add_subtask">+ サブタスク追加</div><!-- サブタスク追加ここまで --><divclass="checklist"id="test"><!-- チェックボックステンプレート挿入位置 --></div><!-- チェックリストここまで --><!-- カード削除ボタンここから --><divclass="wrap_delete_task col"><divclass="rounded-pill float-right"id="delete_task"></div></div><!-- カード削除ボタンここまで --></div></div><!-- カードここまで --></template><!-- タスクテンプレートここまで --><!-- チェックボックステンプレートここから --><templateid="subtask_template"><divclass="checkbox row"><divclass="wrap_checkbox_input"><inputtype="checkbox"class="checkbox_input"id="checkbox_input"><labelclass="checkbox_label"for="checkbox_input"></label></div><divclass="wrap_subtask_name"><inputtype="text"class="subtask_name"placeholder="サブタスク"></div><divclass="subtask_name_toolchip"></div><divid="delete_subtask">削除</div></div></template><!-- チェックボックステンプレートここまで --><!----------------------- テンプレートここまで -----------------------></div><!-- Optional JavaScript --><script>if(typeofmodule==='object'){window.module=module;module=undefined;}</script><!-- jQuery first, then Popper.js, then Bootstrap JS --><script  src="https://code.jquery.com/jquery-3.5.1.js"integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc="crossorigin="anonymous"></script><!-- minified jquery --><!--
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
    --><script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script><script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1/i18n/jquery.ui.datepicker-ja.min.js"></script><script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI"crossorigin="anonymous"></script><script>if(window.module){module=window.module;}</script><!-- My JavaScript --><script src="js/renderer.js"></script><!-- rendere.js first. --><script src="js/left_menu.js"></script><script src="js/right_body.js"></script></body></html>

common.css
common.css
html{font-size:62.5%;}header{background:whitesmoke;margin:0;}#title{margin:01rem;height:5vh;}body{background:whitesmoke;overflow:hidden;}#contents{height:95vh;}#main_content{background:linear-gradient(-120deg,#584a3d,#453650);}/*スクロールバー*/::-webkit-scrollbar{width:1rem;height:1rem;}::-webkit-scrollbar-track{border-radius:1rem;background-color:rgba(245,245,245,0.1);}::-webkit-scrollbar-thumb{border-radius:1rem;background-color:rgba(128,128,128,0.5);}input[type="text"]:focus{border:0.2remsolidlightskyblue;outline:0;}

left_menu.css
left_menu.css
#left_menu{background-color:rgba(30,30,30,0.5);color:white;height:95vh;overflow:auto;max-width:30rem;}#left_menuul{display:flex;flex-flow:column;padding-left:0;margin:0;list-style:none;}#left_menuli{text-align:center;}#left_menudiv{font-size:1.5rem;transform:scale(0.8,0.8);margin:0.1rem;}#left_menudiv:hover{background-color:rgb(56,56,56);cursor:pointer;transition:.2s;transform:scale(0.9,0.9);}#left_menudiv[class*="active"]{background-color:rgba(109,131,68);transition:.2s;transform:scale(1,1);}.project:focus{border:0.2remsolidlightskyblue;outline:0;}#add_project{width:5rem;margin:0.3remauto!important;display:block;text-align:center;border:0.15remsolidwhite;color:white;background-color:grey!important;font-weight:bold;transform:scale(0.8,0.8);user-select:none;}#add_project:hover{background-color:#754775!important;}/* モーダルCSS */.modalArea{display:none;position:fixed;z-index:10;/*サイトによってここの数値は調整 */top:0;left:0;width:100%;height:100%;}.modalBg{width:100%;height:100%;background-color:rgba(30,30,30,0.9);}.modalWrapper{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:70%;padding:5vh10vw;background-color:#fff;}.closeModal{position:absolute;top:1rem;right:2rem;font-size:2rem;cursor:pointer;}

right_body.css
right_body.css
#right_body{height:95vh;margin:0;padding:0;scroll-behavior:smooth;overflow-x:auto;overflow-y:hidden;}/* カード */.card{min-width:35rem;max-width:35rem;margin:1remauto1remauto;}.card.Done{opacity:0.5!important;}.board{min-height:97vh;margin:0;}.board_name{min-width:40rem;max-width:40rem;font-size:3vh;margin:0auto;text-align:center;color:white;}.cards{border:0.2remsolidgrey;min-width:40rem;max-width:40rem;height:88vh;margin-bottom:3rem;padding-bottom:3rem;align-items:center;overflow:auto;margin:0auto;}.cards_list{margin:0;min-width:130rem;max-width:150rem;}#add_task{padding:0.5rem;font-size:1.5rem;width:11rem;display:block;text-align:center;border:0.15remsolidwhite;color:white;background-color:grey;font-weight:bold;transform:scale(0.9,0.9);position:fixed;right:2rem;bottom:3rem;z-index:1;user-select:none;}#add_task:hover{background-color:#754775;cursor:pointer;transition:.2s;transform:scale(1,1);}#delete_task{font-size:1rem;width:2rem;height:2rem;display:flex;justify-content:center;align-items:center;margin:0;padding:0;color:grey;font-weight:bold;transform:scale(1,1);z-index:1;border:0.1remsolidgrey;}#delete_task:hover{cursor:pointer;transition:.2s;transform:scale(1.2,1.2);color:red;border-color:red;}.wrap_delete_task{margin:1rem;}/* タスク名 */.task_name{font-size:1.5rem;margin:autoauto;width:100%;background-color:whitesmoke;border-radius:0.3rem;border:hidden;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.wrap_task_name{display:inline-block;overflow:hidden}.card_title{position:relative;/* for toolchip */}.task_name_toolchip{max-width:26rem;display:none;position:absolute;top:3.5em;left:5rem;z-index:9999;padding:0.3em0.5em;color:#FFFFFF;background:rgb(124,124,124);border-radius:0.5em;}.task_name_toolchip:after{width:100%;content:"";display:block;position:absolute;left:0.5em;top:-0.8rem;border-top:0.8remsolidtransparent;border-left:0.8remsolidrgb(124,124,124);}/* statusトグル */#status{display:flex;justify-content:flex-end;}.status_input{display:none;border:0.1remsolidgrey;}/* statusトグル 選択なし */.status_input+label{display:inline-block;height:2.5rem;opacity:0.7;cursor:pointer;transition:.2s;transform:scale(0.9,0.9);border:0.1remsolidgrey;border-radius:0.5rem;padding:0.3rem0.6rem;}/* statusトグル 選択あり */.status_input:checked+label{opacity:1;transform:scale(1,1);}.status_input+label:hover{transform:scale(1,1);}.Wait+label{color:orange;border:0.1remsolidorange;}.Doing+label{color:rgb(0,156,0);border:0.1remsolidrgb(0,156,0);}.Done+label{color:rgb(37,21,21);border:0.1remsolidrgb(37,21,21);}.Wait:checked+label{background:orange!important;color:white!important;}.Doing:checked+label{background:rgb(0,156,0)!important;color:white!important;}.Done:checked+label{background:rgb(37,21,21)!important;color:white!important;}/* タスク詳細説明 */#detail_textarea{max-height:20rem;margin:1remauto;}/* チェックリスト */.checkbox{padding-top:1rem;position:relative;/* for toolchip */}.wrap_checkbox_input{display:inline-block;padding-left:2rem;padding-right:1rem;height:1.5rem;}input[type="checkbox"]{display:none;}input[type="checkbox"]+label{position:relative;padding-left:1.5rem;padding-bottom:1.5rem;cursor:pointer;}input[type="checkbox"]+label:before{content:'';width:1.5rem;height:1.5rem;border:0.1remsolidgrey;border-radius:1rem;position:absolute;left:0;top:0;transition:all0.2s;}input[type="checkbox"]:checked+label:before{width:0.75rem;height:1.5rem;top:-0.5rem;left:0.5rem;border-radius:0;border-color:green;border-top-color:transparent;border-left-color:transparent;transform:rotate(60deg);}.subtask_name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:hidden;}.wrap_subtask_name{display:inline-block;height:2rem;text-align:center;width:26rem;border:0.1remsolid#ddd;border-radius:0.3rem;background-color:white;overflow:hidden}.subtask_name_toolchip{max-width:26rem;display:none;position:absolute;top:3.5em;left:5rem;z-index:9999;padding:0.3em0.5em;color:#FFFFFF;background:rgb(124,124,124);border-radius:0.5em;}.subtask_name_toolchip:after{width:100%;content:"";display:block;position:absolute;left:0.5em;top:-0.8rem;border-top:0.8remsolidtransparent;border-left:0.8remsolidrgb(124,124,124);}#add_subtask{width:10rem;margin:0.3remauto;display:block;text-align:center;border:0.15remsolidgrey;color:grey;font-weight:bold;transform:scale(0.9,0.9);user-select:none;}#add_subtask:hover{border-color:#bb6dbb;color:#bb6dbb;cursor:pointer;transition:.2s;transform:scale(1,1);}#delete_subtask{width:3rem;height:1.5rem;color:grey;display:flex;justify-content:center;align-items:center;padding-left:1rem;transform:scale(0.9,0.9);}#delete_subtask:hover{transform:scale(1,1)!important;color:red!important;cursor:pointer;font-weight:bold;}

renderer.js
renderer.js
// VarsletPROJECT_TEXT;letPROJECT_ID=0;letPROJECT_ELEMENT="";letproject={};letTASK_ID=0;letCARDS_SCROLL_TOP=0;letCARDS_SCROLL_LEFT=0;lettop_wait;lettop_doing;lettop_done;// Electronのモジュールconstelectron=require("electron");constipcRenderer=electron.ipcRenderer;// Rem取得functionconvertRemToPx(rem){constfontSize=getComputedStyle(document.documentElement).fontSize;returnrem*parseFloat(fontSize);}// subtask function.functionADD_SUBTASK(clone,task,subtask){varclone_subtask=$($('#subtask_template').html());clone_subtask.attr("id",subtask.id);// checkbox input 設定clone_subtask.find("input[type=checkbox]").each((index,element)=>{element.id=`${element.id}_${task.id}_${subtask.id}`;if(subtask.checked){$(element).prop('checked',true);}});// checkbox label 設定clone_subtask.find("label.checkbox_label").each((index,element)=>{$(element).attr('for',`${$(element).attr('for')}_${task.id}_${subtask.id}`);});// checkbox content 設定clone_subtask.find(".subtask_name").val(subtask.name);clone_subtask.find(".checkbox_input").prop('checked',subtask.checked);if(clone_subtask.find(".checkbox_input").prop('checked')){// 取り消し線追加clone_subtask.find(".checkbox_input").closest(".checkbox").find(".subtask_name").css("text-decoration","line-through");}// サブタスク追加clone.find(".checklist").append(clone_subtask);}functionLOAD_SUBTASKS(clone,task){varsubtasks=task.subtasks;// クリアclone.find(".checkbox").each((index,element)=>{$(element).remove();});// 再描画for(leti=0;i<subtasks.length;i++){ADD_SUBTASK(clone,task,subtasks[i]);}}// task function.functionADD_TASK(task){varclone=$($('#task_template').html());// task_id 設定clone.attr("id",task.id);// task_name 設定clone.find(".task_name").val(task.name);// task_detail 設定clone.find(".task_detail").val(task.detail);// status input 設定clone.find("input[type=radio]").each((index,element)=>{element.name=`${element.name}_${task.id}`;element.id=`${element.id}_${task.id}`;if($(element).attr("value")==task.status){$(element).prop('checked',true);}if(task.status==="Done"){clone.addClass("Done");}});// status label 設定clone.find("label.status_label").each((index,element)=>{$(element).attr('for',`${$(element).attr('for')}_${task.id}`);});// date input 設定clone.find("input.date_input").each((index,element)=>{element.id=`${element.id}_${task.id}`;if(element.id==`Start_${task.id}`){$(element).attr("value",task.start_date);}if(element.id==`Due_${task.id}`){$(element).attr("value",task.due_date);}if(element.id==`End_${task.id}`){$(element).attr("value",task.end_date);}});// date label 設定clone.find("label.date_label").each((index,element)=>{$(element).attr('for',`${$(element).attr('for')}_${task.id}`);});// subtask 設定LOAD_SUBTASKS(clone,task);// タスク追加if(task.status==="Wait"){$('#cards_wait').append(clone);}elseif(task.status==="Doing"){$('#cards_doing').append(clone);}elseif(task.status==="Done"){$('#cards_done').append(clone);}// Callender. 追加時に設定が必要$(function(){$.datepicker.setDefaults($.datepicker.regional["ja"]);$(".date_input").datepicker();});}functionSHOW_TASKS(tasks){// スクロール位置一時保存$('.cards').each((index,element)=>{switch($(element).attr('id')){case"cards_wait":top_wait=$(element).scrollTop();break;case"cards_doing":top_doing=$(element).scrollTop();break;case"cards_done":top_done=$(element).scrollTop();break;}});// クリア$('.card').each((index,element)=>{$(element).remove();});// 再描画for(leti=0;i<tasks.length;i++){ADD_TASK(tasks[i]);}}functionLOAD_TASKS(){// projects再取得projects=ipcRenderer.sendSync('data','projects');// activeなproject取得project_id=$('#left_menu div.project.active').attr("id");// tasks取得varindex=-1;index=projects.findIndex((project)=>{returnproject.id===project_id;})vartasks;if(index!==-1){tasks=projects[index].tasks;}else{tasks=[];}SHOW_TASKS(tasks);// タスク詳細の高さ調節。読み込み後でないとscrollHeightが使えない。$(".task_detail").each((index,element)=>{constminHeight=convertRemToPx(5);// 5rem$(element).height(minHeight);if(element.scrollHeight>element.clientHeight){$(element).height(element.scrollHeight);}});// タスク詳細の高さ調節後にスクロールをリロードRELOAD_SCROLL()// ソート可能にする。ロード時に読み込みが必要。$(function(){$('.cards').sortable({// オプションconnectWith:'.cards',revert:100,cursor:'move',delay:100,// ドロップした時のイベント(e.targetに受け取り側が、ui.itemにドラッグした要素がはいる)receive:(e,ui)=>{// ステータスを受け取り側に合わせて変更vartask_status;switch($(e.target).attr("id")){case"cards_wait":task_status="Wait";break;case"cards_doing":task_status="Doing";break;case"cards_done":task_status="Done";break;default:returnfalse;}project_id=$('#left_menu div.project.active').attr("id");task_id=$(ui.item).attr("id");rc=ipcRenderer.sendSync("changeTaskStatus",{project_id:project_id,task_id:task_id,task_status:task_status});if(rc){// ステータストグルの変更$(e.target).find(`input[type=radio].${task_status}`).prop('checked',true);// Doneクラスのリセット後、ステータスがDoneなら追加(ユーザ入力でないためchangeで発火されない)$(ui.item).removeClass("Done");if(task_status==="Done"){$(ui.item).addClass("Done");}}returnrc}});});}// スクロール位置リストアfunctionRELOAD_SCROLL(){$('.cards').each((index,element)=>{switch($(element).attr('id')){case"cards_wait":$(element).scrollTop(top_wait);break;case"cards_doing":$(element).scrollTop(top_doing);break;case"cards_done":$(element).scrollTop(top_done);break;}});}// project function.functionADD_PROJECT(project){// テンプレートからclone作成letclone=$($('#project_template').html());// cloneにid, textを設定clone.find("#project").each((index,element)=>{element.id=project.id;element.textContent=project.name;});// cloneを一覧に追加$('#menu_list').append(clone);}functionSHOW_PROJECTS(projects){// クリアactive_project_id="";$('#left_menu div.project').each((index,element)=>{if(element.classList.contains("active")){active_project_id=element.id;}$(element).parent().remove();});// 再描画for(leti=0;i<projects.length;i++){ADD_PROJECT(projects[i]);if(i==0){$('#left_menu div.project').addClass("active");}if(projects[i].id==active_project_id){$('#left_menu div.project').removeClass("active")$(`#left_menu div#${active_project_id}`).addClass("active");}}}functionLOAD_PROJECTS(){// projects取得projects=ipcRenderer.sendSync('data','projects');SHOW_PROJECTS(projects);LOAD_TASKS();}// テキスト全選択jQuery.fn.selectText=function(){vardoc=document;varelement=this[0];if(window.getSelection){varselection=window.getSelection();varrange=document.createRange();range.selectNodeContents(element);selection.removeAllRanges();selection.addRange(range);}};

left_menu.js
left_menu.js
'user strict'// 読み込み操作if(!$('#left_menu div.project').length){LOAD_PROJECTS();}// イベント操作// 対象:プロジェクト追加ボタン// 動作:シングルクリック// 内容:直前にプロジェクトを追加する。$(document).on('click','#add_project',function(){// project追加要求、returnされるproject取得rc=ipcRenderer.sendSync('addProject');// 画面に表示LOAD_PROJECTS();});// 対象:プロジェクト名// 動作:シングルクリック// 内容:プロジェクトを選択する。$(document).on('click','#left_menu div.project',function(){$('#left_menu div.project').removeClass("active")$('#left_menu div.project').each(function(){$(this).attr('contenteditable','false');if(!$(this).text()){$(this).text(PROJECT_TEXT);}})$(this).addClass("active");});// 対象:プロジェクト名// 動作:ダブルクリック// 内容:プロジェクト名を編集可能にする。エンターキーまたはダブルクリックで終了する。$(document).on('dblclick','#left_menu div.project',function(){letcontenteditable=$(this).attr('contenteditable');if(contenteditable=='false'){PROJECT_TEXT=$(this).text();// すべて編集不可に$('#left_menu div.project').attr('contenteditable','false');// 対象だけ編集可に$(this).attr('contenteditable','true')$(this).focus();$(this).selectText();// エンター押下時にフォーカスアウト(Shift+EnterはOK)$(this).keypress(function(e){if(!event.shiftKey){if(e.keyCode==13){$(this).focusout();}}});// フォーカスアウト時に編集不可にしてMainプロセスに変更要求$(this).focusout(()=>{$(this).attr('contenteditable','false');if(!$(this).text()){$(this).text(PROJECT_TEXT);}else{project_id=$(this).attr('id');project_name=$(this).text();rc=ipcRenderer.sendSync('changeProjectName',{project_id:project_id,project_name:project_name});if(!rc){$(this).text(PROJECT_TEXT);}}})}});// 対象:プロジェクト名// 動作:右クリック// 内容:削除ポップアップを出す。$(document).on('contextmenu','#left_menu div.project',function(){// reset$('.delete').removeClass('delete');$('p.delete_target').remove();// 対象追加$(this).addClass('delete');$('<p class="delete_target">削除対象:'+$(this).text()+'</p>').insertBefore($('#delete_project'));$('#modalArea').fadeIn();returnfalse});// 対象:モーダル// 動作:右クリック// 内容:モーダルを閉じる$('#closeModal , #modalBg').click(function(){$('#modalArea').fadeOut();$('p.delete_target').remove();});// 対象:削除ボタン(モーダル)// 動作:クリック// 内容:プロジェクトを削除$('#delete_project').click(function(){project_id=$('#left_menu div.project.delete').attr("id");rc=ipcRenderer.sendSync('deleteProject',project_id);$('#modalArea').fadeOut();LOAD_PROJECTS();$('p.delete_target').remove();});

right_body.js
right_body.js
'user strict'// 読み込み操作// LOAD_PROJECTS中で読み込むため不要//if (! $('#cards.card').length) {//    LOAD_TASKS();//}// イベント操作// 対象:タスク削除ボタン// 動作:シングルクリック// 内容:選択したタスクを削除する。$(document).on('click','#delete_task',(e)=>{project_id=$('#left_menu div.project.active').attr("id");task_id=$(e.target).parents(".card").attr("id");rc=ipcRenderer.sendSync("confirm",{title:"タスク削除",message:"削除しますか?"});if(rc){res=ipcRenderer.sendSync("deleteTask",{project_id:project_id,task_id:task_id});LOAD_TASKS();}});// イベント操作// 対象:タスク追加ボタン// 動作:シングルクリック// 内容:最後尾に新規タスクを追加する。$(document).on('click','#add_task',()=>{project_id=$('#left_menu div.project.active').attr("id");rc=ipcRenderer.sendSync('addTask',project_id);LOAD_TASKS();$('#cards_wait').animate({scrollTop:$('#cards_wait').get(0).scrollHeight},2000);});// イベント操作// 対象:タスク状態ラジオボタン// 動作:シングルクリック// 内容:タスク状態を変更する。$(document).on('click','.status_input',(e)=>{project_id=$('#left_menu div.project.active').attr("id");task_id=$(e.target).parents(".card").attr("id");if($(e.target).prop('checked')){task_status=$(e.target).attr("value");rc=ipcRenderer.sendSync("changeTaskStatus",{project_id:project_id,task_id:task_id,task_status:task_status});if(rc){if(task_status!=="Done"){rc=ipcRenderer.sendSync('changeTaskDate',{project_id:project_id,task_id:task_id,task_end_date:""});}if(rc){LOAD_TASKS();}}}})// イベント操作// 対象:タスク名// 動作:フォーカス// 内容:編集があった場合にタスク名を変更する。$(document).on('focus','.task_name',(e)=>{$(e.target).select();// エンター押下時にフォーカスアウト(Shift+EnterはOK)$(e.target).keypress(function(e){if(!event.shiftKey){if(e.keyCode==13){$(e.target).blur();}}});});// フォーカスアウト時に変更反映$(document).on('blur','.task_name',(e)=>{// 空なら変更せず再ロード。空でなければ変更を反映する。if($(e.target).val()){project_id=$('#left_menu div.project.active').attr("id");task_id=$(e.target).parents(".card").attr("id");task_name=$(e.target).val();rc=ipcRenderer.sendSync('changeTaskName',{project_id:project_id,task_id:task_id,task_name:task_name});if(rc){$(e.target).scrollLeft(0);// はみ出た分表示がずれるので最初の位置に戻す}}LOAD_TASKS();});// イベント操作// 対象:タスク詳細// 動作:入力// 内容:入力に合わせてサイズを調整する。$(document).on('input','textarea',(e)=>{constminHeight=convertRemToPx(5);// 5rem$(e.target).height(minHeight)if(e.target.scrollHeight>e.target.clientHeight){$(e.target).height(e.target.scrollHeight);}});// イベント操作// 対象:タスク詳細// 動作:内容が修正されたとき// 内容:タスク詳細を変更する。$(document).on('change','textarea',(e)=>{project_id=$('#left_menu div.project.active').attr("id");task_id=$(e.target).parents(".card").attr("id");vartask_detail=$(e.target).val();rc=ipcRenderer.sendSync('changeTaskDetail',{project_id:project_id,task_id:task_id,task_detail:task_detail});LOAD_TASKS();});// イベント操作// 対象:タスク開始日// 動作:内容が修正されたとき// 内容:タスク開始日を変更する。$(document).on('change','.date_input.Start',(e)=>{project_id=$('#left_menu div.project.active').attr("id");task_id=$(e.target).parents(".card").attr("id");vartask_start_date=$(e.target).val();rc=ipcRenderer.sendSync('changeTaskDate',{project_id:project_id,task_id:task_id,task_start_date:task_start_date});});// イベント操作// 対象:タスク期日// 動作:内容が修正されたとき// 内容:タスク期日を変更する。$(document).on('change','.date_input.Due',(e)=>{project_id=$('#left_menu div.project.active').attr("id");task_id=$(e.target).parents(".card").attr("id");vartask_due_date=$(e.target).val();rc=ipcRenderer.sendSync('changeTaskDate',{project_id:project_id,task_id:task_id,task_due_date:task_due_date});});// イベント操作// 対象:タスク完了日// 動作:内容が修正されたとき// 内容:タスク完了日を変更する。$(document).on('change','.date_input.End',(e)=>{project_id=$('#left_menu div.project.active').attr("id");task_id=$(e.target).parents(".card").attr("id");vartask_end_date=$(e.target).val();rc=ipcRenderer.sendSync('changeTaskDate',{project_id:project_id,task_id:task_id,task_end_date:task_end_date});});// イベント操作// 対象:サブタスク追加ボタン// 動作:シングルクリック// 内容:最後尾に新規サブタスクを追加する。$(document).on('click','#add_subtask',(e)=>{project_id=$('#left_menu div.project.active').attr("id");task_id=$(e.target).parents(".card").attr("id");rc=ipcRenderer.sendSync('addSubtask',{project_id:project_id,task_id:task_id});LOAD_TASKS();});// イベント操作// 対象:サブタスク削除ボタン// 動作:シングルクリック// 内容:選択したタスクを削除する。$(document).on('click','#delete_subtask',(e)=>{project_id=$('#left_menu div.project.active').attr("id");task_id=$(e.target).parents(".card").attr("id");subtask_id=$(e.target).parents(".checkbox").attr("id");rc=ipcRenderer.sendSync("confirm",{title:"サブタスク削除",message:"削除しますか?"});if(rc){res=ipcRenderer.sendSync("deleteSubtask",{project_id:project_id,task_id:task_id,subtask_id:subtask_id});LOAD_TASKS();}});// イベント操作// 対象:サブタスク名// 動作:フォーカス// 内容:編集があった場合にタスク名を変更する。$(document).on('focus','.subtask_name',(e)=>{$(e.target).select();// エンター押下時にフォーカスアウト(Shift+EnterはOK)$(e.target).keypress(function(e){if(!event.shiftKey){if(e.keyCode==13){$(e.target).blur();}}});});// フォーカスアウト時に変更反映$(document).on('blur','.subtask_name',(e)=>{// 空なら変更せず再ロード。空でなければ変更を反映する。if($(e.target).val()){project_id=$('#left_menu div.project.active').attr("id");task_id=$(e.target).parents(".card").attr("id");subtask_id=$(e.target).parents(".checkbox").attr("id");subtask_name=$(e.target).val();rc=ipcRenderer.sendSync('changeSubtaskName',{project_id:project_id,task_id:task_id,subtask_id:subtask_id,subtask_name:subtask_name});if(rc){$(e.target).scrollLeft(0);// はみ出た分表示がずれるので最初の位置に戻す}}LOAD_TASKS();});// イベント操作// 対象:サブタスク状態チェックボックス// 動作:シングルクリック// 内容:タスク状態を変更する。$(document).on('click','.checkbox_input',(e)=>{project_id=$('#left_menu div.project.active').attr("id");task_id=$(e.target).parents(".card").attr("id");subtask_id=$(e.target).parents(".checkbox").attr("id");subtask_checked=$(e.target).prop('checked');rc=ipcRenderer.sendSync("changeSubtaskChecked",{project_id:project_id,task_id:task_id,subtask_id:subtask_id,subtask_checked:subtask_checked});if(!rc){$(e.target).prop('checked',!subtask_checked);}else{LOAD_TASKS();}})// イベント操作// 対象:タスク名// 動作:マウスオーバー// 内容:ツールチップを表示する。$(document).on('mouseover','.task_name',(e)=>{if($(e.target).parent().width()<e.target.scrollWidth){vartask_name=$(e.target).val();$(e.target).closest(".card_title").find(".task_name_toolchip").text(task_name);$(e.target).closest(".card_title").find(".task_name_toolchip").css("display","block");$(e.target).on('mouseleave',()=>{$(e.target).closest(".card_title").find(".task_name_toolchip").css("display","none");})}});// イベント操作// 対象:サブタスク名// 動作:マウスオーバー// 内容:ツールチップを表示する。$(document).on('mouseover','.subtask_name',(e)=>{if($(e.target).parent().width()<e.target.scrollWidth){varsubtask_name=$(e.target).val();$(e.target).closest(".checkbox").find(".subtask_name_toolchip").text(subtask_name);$(e.target).closest(".checkbox").find(".subtask_name_toolchip").css("display","block");$(e.target).on('mouseleave',()=>{$(e.target).closest(".checkbox").find(".subtask_name_toolchip").css("display","none");})}});// イベント操作// 対象:プロジェクト名// 動作:シングルクリック// 内容:タスク一覧を再読み込みする。$(document).on('click','#left_menu div.project',()=>{LOAD_TASKS();});


Viewing all articles
Browse latest Browse all 8691

Trending Articles