Electron 18 を使って200行以内でファイルの入出力ができるようなテキストエディタを作ってみる。

## 初めに

実用を目的とはしていません。`Electron`の構造を理解するために、どれだけ骨格を抜き出せるかを目的にしています。

というわけでスクラッチから書いてみます。

`html`中に`style`と`javascript`を直書きしているので、以下のワーニングが出ます。
`Electron Security Warning (Insecure Content-Security-Policy)`

以下のレポジトリにワーニングの出ない、他の機能も入ったバージョンを置いておきます。
ワーニングの消し方や結果だけを知りたい方はこちらで！
https://github.com/Satachito/electron-quick-start-MOD


`npm`,`npx`は入っている前提です。(`node`が入っていれば通常入っています)


## 準備

フォルダを作ってその中に`package.json`を作って`npm`で`Electron`をインストールします。

```
$ mkdir TE
$ cd TE
```
```package.json
{	"main": "main.js"
,	"devDependencies": { "electron": "^18.0.0-alpha.4" }
}
```
```
$ npm i
```

## preload.js

`ipcRenderer`をレンダラプロセスが使えるようにすることによって、メインプロセスにメッセージを送れるようにします。

`contextBridge`を使って、レンダラプロセスがメインプロセスからの`data`と`menu`メッセージを受け取れるようにしています。


* `data`メッセージはメインプロセスがファイルを開けたらその内容を送ってきます
* `menu`メッセージはメインプロセスが選択されたメニューを送ってきます

```preload.js
const { contextBridge, ipcRenderer } = require( 'electron' )
contextBridge.exposeInMainWorld(
	'ipcRenderer'
,	ipcRenderer
)
contextBridge.exposeInMainWorld(
	'onData'
,   $ => ipcRenderer.on( 'data', $ )
)
contextBridge.exposeInMainWorld(
	'onMenu'
,   $ => ipcRenderer.on( 'menu', $ )
)
```

## index.html

`index.html`に編集用の`textarea`とレンダラプロセスを記述します。

`preload.js`で見えるようになった`onMenu`は`Save`と`SaveAs`メッセージに反応し、編集中のデータ（`TextArea.value`）をメインプロセスに送ります。メインプロセスは保存したらファイル名を、しなかったら`undefined`を戻します。

`onData`はメインプロセスから送られてきたファイルの内容を`textarea`にセットし、ファイル名をドキュメントタイトルにセットします。

ウインドウが閉じられる時、元のデータと編集中のデータが違っていたら、`ev.returnValue`に何かセットして、メインプロセスで`BrowserWindow`の`webContents`が`will-prevent-unload`を検知できるようにします。

```index.html
<textarea id=TextArea style="resize: none; width: 100%; height: 100%"></textarea>
<script type=module>
let
prevData = TextArea.value
onData(
	( ev, $, file ) => (
		prevData = $
	,	TextArea.value = $
	,	document.title = file
	)
)
onMenu(
	( ev, $ ) => {
		const
		Save = _ => ipcRenderer.invoke( _, TextArea.value ).then(
			file => file && (
				prevData = TextArea.value
			,	document.title = file
			)
		)
		switch ( $ ) {
		case 'Save':
			Save( 'save' )
			break
		case 'SaveAs':
			Save( 'saveAs' )
			break
		}
	}
)
onbeforeunload = ev => TextArea.value != prevData
?	ev.returnValue = ''	// for Chrome/Electron
:	undefined
</script>
```

## main.js
メインプロセスです。

`CreateWindow`で新しい`BrowserWindow`を開きます。
`file`名が引数で渡されたら、`BrowserWindow`の`webContents`が`did-finish-load`を検知したらレンダラプロセスにファイルの内容を送ります。
利用者が編集済みのウインドウを閉じようとしたとき、`webContents`は`will-prevent-unload`を検知します。編集を継続するか破棄するかを選ばせます。
ファイルメニューに `New`,`Open`,`Save`,`SaveAs`を付け加えます。
* 利用者が`New`を選んだら`CreateWindow`を呼びます。
* 利用者が`Open`を選んだら`openDialog`を出して`file`が選ばれたらその`file`名を引数に`CreateWindow`を呼びます。
* 利用者が`Save`を選んだらレンダラプロセスに`menu`,`save`と送ります。レンダラプロセスは`onMenu`でこれを検知すると、メインプロセスに`Save`でデータを送ってきます。無事保存したら保存した`file`名をレンダラプロセスに返します。保存しなかったら`undefine`が返されます。
* 利用者が`SaveAs`を選んだらレンダラプロセスに`menu`,`saveAs`と送ります。レンダラプロセスは`onMenu`でこれを検知すると、メインプロセスに`SaveAs`でデータを送ってきます。無事保存したら保存した`file`名をレンダラプロセスに返します。保存しなかったら`undefine`が返されます。

最後に引数でファイルが渡された時の処理をしています。

```main.js
const
{ app, BrowserWindow, Menu, MenuItem, ipcMain, dialog } = require( 'electron' )
const
Send = ( ...$ ) => {
	const _ = BrowserWindow.getFocusedWindow()
	_ && _.send( ...$ )
}
const
CreateWindow = file => {
	const 
	$ = new BrowserWindow(
		{	width			: 1600
		,	height			: 800
		,	webPreferences	: {
				preload		: require( 'path' ).join( __dirname, 'preload.js' )
			}
		}
	)
	$.loadFile( 'index.html' )
	file && (
		$.webContents.on(
			'did-finish-load'
		,	() => $.send( 'data', require( 'fs' ).readFileSync( file, 'utf8' ), file )
		)
	,	$.webContents.file = file
	)
	$.webContents.on(
		'will-prevent-unload'
	,	ev => dialog.showMessageBoxSync(
			$
		,	{	type: 'question'
			,	buttons: [ 'Discard change and close', 'No' ]
			,	message: 'Do you really want to close this window?\nChanges you made may not be saved.'
			}
		) === 0 && ev.preventDefault()
	)
}
const
SaveAs = ( ev, $ ) => {
	const file = dialog.showSaveDialogSync(
		{	properties	: [ 'openFile', 'openDirectory' ]
		,	defaultPath	: ev.sender.file
		}
	)
	file && (
		require( 'fs' ).writeFileSync( file, $ )
	,	ev.sender.file = file
	)
	return file
}
ipcMain.handle( 'saveAs', SaveAs )
ipcMain.handle(
	'save'
,	( ev, $ ) => {
		const file = ev.sender.file
		return file
		?	(	require( 'fs' ).writeFileSync( file, $ )
			,	file
			)
		:	SaveAs( ev, $ )
	}
)
const
isMac = process.platform === 'darwin'
app.on(
	'window-all-closed'
,	() => isMac || app.quit()
)
app.on(
	'activate'
,	( event, hasVisibleWindows ) => hasVisibleWindows || CreateWindow()
)
app.on(
	'open-file'
,	( ev, _ ) => (
		ev.preventDefault()
	,	CreateWindow( _ )
	)
)
app.whenReady().then(
	() => {
		const
		menu = Menu.getApplicationMenu()
		const
		fileMenu = menu.items.find( $ => $.role === 'filemenu' ).submenu
		fileMenu.insert(
			0
		,	new MenuItem( 
				{	label		: 'Save as...'
				,	click		: ev => Send( 'menu', 'SaveAs' )
				}
			)
		)
		fileMenu.insert(
			0
		,	new MenuItem( 
				{	label		: 'Save'
				,	accelerator	: 'CmdOrCtrl+S'
				,	click		: ev => Send( 'menu', 'Save' )
				}
			)
		)
		fileMenu.insert( 0, new MenuItem( {	type: 'separator' } ) )
		fileMenu.insert(
			0
		,	new MenuItem( 
				{	label		: 'Open...'
				,	accelerator	: 'CmdOrCtrl+O'
				,	click		: ev => {
									const _ = dialog.showOpenDialogSync( { properties: [ 'openFile', 'openDirectory' ] } )
									_ && _.forEach( $ => CreateWindow( $ ) )
								}

				}
			)
		)
		fileMenu.insert(
			0
		,	new MenuItem( 
				{	label		: 'New'
				,	accelerator	: 'CmdOrCtrl+N'
				,	click		: ev => CreateWindow()
				}
			)
		)
		Menu.setApplicationMenu( menu )
		const _ = process.argv.slice(
			isMac
			?	process.argv[ 0 ].split( '/' ).pop()	=== 'Electron'		? 2 : 1
			:	process.argv[ 0 ].split( '\\' ).pop()	=== 'electron.exe'	? 2 : 1
		)
		_.length
		?	_.forEach( _ => CreateWindow( _ ) )
		:	CreateWindow()
	}
)
```

## 実行

```
$ npx electron .
```

## 最後に

パック前で以下の感じです。目的は果たせた気がします。

```
% wc *.js *.html package.json
     136     446    2852 main.js
      13      34     289 preload.js
      34     102     640 index.html
       3      10      77 package.json
     186     592    3858 total
```