見出し画像

GASでシンプルな新聞エディタを作る①

学校のGIGA端末では、
縦書きの新聞を作るのが難しかったため、
じゃあGASで作ってしまおうと考えました。

今回は、試用できるように、
webアプリにリンクを張っておきます。
※Googleアカウントでのサインインが必要です。

今回は、GAS側でHTMLを表示しているだけなので、
index.htmlのコードをテキストエディタに張り付けて、
拡張子を.htmlにしても試用ができます。

主な機能は、次の通りです。
①新規作成・ファイルを開く・保存する
②編集内容の自動保存(1分ごと)
③フォントの変更(明朝体 or ゴシック体)
④フォントサイズの変更
⑤太字・斜体・下線
⑥背景色の変更
⑦文字色の変更

早速、コードです。

コード.gs

function doGet(e) {
  const template = HtmlService.createTemplateFromFile('index');
  const htmlOutput = template.evaluate();
  htmlOutput.setTitle('シンプルな新聞エディタ');
  return htmlOutput;
}


index.html

<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<link rel="preconnect" href="https://fonts.googleapis.com">
	<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
	<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP&family=Noto+Serif+JP&display=swap" rel="stylesheet">
	<title>新聞作成</title>
	<style>
		* {
			margin: 0;
			padding: 0;
			box-sizing: border-box;
		}

		body {
			display: flex;
			flex-direction: column;
			align-items: center;
			background-color: #7f7f7f;
			}

		.all {
			display: flex;
			flex-direction: column;
			width: 794px;
			background: #FFF;
		}

		.toolbar {
			width: 100%;
			display: flex;
			align-items: center;
			justify-content: space-between;
			background: #c0c0c0;
			padding: 10px 15px;
			border-bottom: 1px solid #dcdcdc;
			box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
		}

		.toolbar select {
			margin-right: 8px;
			padding: 4px 8px;
			border: 1px solid #ccc;
			border-radius: 4px;
			font-size: 12px;
		}

		.toolbar input[type="color"] {
			margin-right: 8px;
			padding: 1px 2px;
			background-color: #cdcdcd;
			border: 1px solid #c0c0c0;
			border-radius: 4px;
			font-size: 14px;
		}


		.toolbar button {
			background-color: #c0c0c0;
			border: none;
			padding: 4px 8px;
			border-radius: 4px;
			cursor: pointer;
		}

		.page {
			display: grid;
			width: 100%;
			height: 1123px;
			grid-template-rows: 336px 336px 336px;
			grid-template-columns: 1fr;
			gap: 57px, 57px, 0px, 0px;
			padding: 57px;
		}

		.part1 {
			display: grid;
			grid-template-columns: auto 108px;
			grid-template-rows: 271px 65px;
			gap: 0px;
			border: 1px solid #7F7F7F;
		}

		.article {
			grid-column: 1 / 2;
			grid-row: 1 / 3;
			border: 1px solid #7F7F7F;
		}

		.title {
			grid-column: 2 / 3;
			grid-row: 1 / 2;
			border: 1px solid #7F7F7F;
		}

		.author {
			grid-column: 2 / 3;
			grid-row: 2 / 3;
			border: 1px solid #7F7F7F;
		}

		.part2 {
			border: solid #7F7F7F;
			border-width: 1px 2px;
		}

		.part3	{
			border: solid #7F7F7F;
			border-width: 1px 2px 2px 2px;
		}

		.e_title {
			width: 100%;
			height: 100%;
			border: none;
			outline: none;
			writing-mode: vertical-rl;
			text-orientation: upright;
			text-combine-upright: digits 2;
			letter-spacing: 0.1em;
			line-height: 1.8;
			padding: 5px;
			box-sizing: border-box;
			font-family: "Noto Serif JP", serif;
			font-size: 50px;
			text-align: center;
			padding: 1px 6px;
			overflow: hidden;
		}

		.e_autor {
			width: 100%;
			height: 100%;
			border: none;
			outline: none;
			line-height: 1.0;
			padding: 2px;
			box-sizing: border-box;
			font-family: "Arial", "Noto Serif JP", serif;
			font-size: 16px;
			text-align: start;
		}

		.e_artcle {
			width: 100%;
			height: 100%;
			border: none;
			outline: none;
			writing-mode: vertical-rl;
			text-orientation: upright;
			text-combine-upright: digits 2;
			letter-spacing: 0.1em;
			line-height: 1.8;
			padding: 5px;
			box-sizing: border-box;
			font-family: "Arial", "Noto Serif JP", serif;
			font-size: 20px;
			text-align: start;
		}

		.e_part2 {
			width: 100%;
			height: 100%;
			border: none;
			outline: none;
			writing-mode: vertical-rl;
			text-orientation: upright;
			text-combine-upright: digits 2;
			letter-spacing: 0.1em;
			line-height: 1.8;
			padding: 5px;
			box-sizing: border-box;
			font-family: "Arial", "Noto Serif JP", serif;
			font-size: 20px;
			text-align: start;
		}

		.e_part3 {
			width: 100%;
			height: 100%;
			border: none;
			outline: none;
			writing-mode: vertical-rl;
			text-orientation: upright;
			text-combine-upright: digits 2;
			letter-spacing: 0.1em;
			line-height: 1.8;
			padding: 5px;
			box-sizing: border-box;
			font-family: "Arial", "Noto Serif JP", serif;
			font-size: 20px;
			text-align: start;
		}

		[contenteditable] {
			outline: none;
		}

		@media print {
			@page {
			margin: 0mm;
			}

			* {
				overflow: hidden;
			}

			.toolbar {
				display: none; /* ツールバーを非表示にする */
			}

			body {
				background-color: #fff; /* 白背景に設定 */
				overflow: hidden;
			}

			.all {
				width: 680px;
				height: 1009px; /* A4サイズの縦方向に収める */
				margin: 0 auto; /* 中央揃え */
			}

			.page {
				height: 1009px; /* 縦方向を超えないよう調整 */
				padding: 0;
				gap: 0;
			}
		}

	</style>
</head>
<body>
	<div class="all">
		<div class="toolbar">
			<button id="new-file-button">新規作成</button>
			<input type="file" id="load-file" accept=".json" style="display: none;">
			<button id="load-file-button">開く</button>
			<button id="save-file">保存</button>
			<select id="font-family">
				<option value="Noto Serif JP" selected>Noto Serif JP (デフォルト)</option>
				<option value="Noto Sans JP">Noto Sans JP</option>
			</select>

			<select id="font-size">
				<option value="">サイズ選択</option>
				<option value="12px">12px</option>
				<option value="14px">14px</option>
				<option value="16px">16px</option>
				<option value="18px">18px</option>
				<option value="20px">20px</option>
				<option value="24px">24px</option>
				<option value="28px">28px</option>
				<option value="32px">32px</option>
				<option value="36px">36px</option>
				<option value="40px">40px</option>
				<option value="44px">44px</option>
				<option value="48px">48px</option>
				<option value="50px">50px</option>
				<option value="52px">52px</option>
				<option value="56px">56px</option>
			</select>

			<button id="bold">B</button>
			<button id="italic">I</button>
			<button id="underline">U</button>

			<input type="color" id="bg-color" title="背景色">
			<input type="color" id="text-color" title="文字色">

		</div>

		<div class="page">
			<div class="part1">
				<div class="article"><div class="e_artcle" contenteditable="true">本文1</div></div>
				<div class="title"><div class="e_title" contenteditable="true">〇〇新聞</div></div>
				<div class="author"><div class="e_autor" contenteditable="true">作成者</div></div>
			</div>
			<div class="part2"><div class="e_part2" contenteditable="true">本文2</div></div>
			<div class="part3"><div class="e_part3" contenteditable="true">本文3</div></div>
		</div>
	</div>
	<script>
		const fontSizeSelect = document.getElementById('font-size');
		const fontFamilySelect = document.getElementById('font-family');

		// 選択されたテキストのフォントサイズを取得し、セレクトボックスに反映
		function updateFontSizeSelect() {
			const selection = window.getSelection();
			if (selection.rangeCount > 0) {
				const range = selection.getRangeAt(0);
				const parentElement = range.commonAncestorContainer.nodeType === 1 
					? range.commonAncestorContainer 
					: range.commonAncestorContainer.parentElement;

				const computedStyle = window.getComputedStyle(parentElement);
				const fontSize = computedStyle.fontSize;

				// セレクトボックスの値を一致させる
				const fontSizeOption = Array.from(fontSizeSelect.options).find(
					(option) => option.value === fontSize
				);
				if (fontSizeOption) {
					fontSizeSelect.value = fontSizeOption.value;
				} else {
					fontSizeSelect.value = ""; // 一致する値がない場合はリセット
				}
			}
		}

		// 選択されたテキストのフォントファミリーを取得し、セレクトボックスに反映
		function updateFontFamilySelect() {
			const selection = window.getSelection();
			if (selection.rangeCount > 0) {
				const range = selection.getRangeAt(0);
				const parentElement = range.commonAncestorContainer.nodeType === 1 
					? range.commonAncestorContainer 
					: range.commonAncestorContainer.parentElement;

				const computedStyle = window.getComputedStyle(parentElement);
				const fontFamily = computedStyle.fontFamily.replace(/["']/g, ""); // "ダブルクオートを除去

				// セレクトボックスの値を一致させる
				const fontFamilyOption = Array.from(fontFamilySelect.options).find(
					(option) => fontFamily.includes(option.value)
				);
				if (fontFamilyOption) {
					fontFamilySelect.value = fontFamilyOption.value;
				} else {
					fontFamilySelect.value = ""; // 一致する値がない場合はリセット
				}
			}
		}

		// テキスト選択が変更されたときにフォントサイズとフォントファミリーを更新
		document.addEventListener('selectionchange', () => {
			updateFontSizeSelect();
			updateFontFamilySelect();
		});

		// フォントサイズを変更する処理
		fontSizeSelect.addEventListener('change', function () {
			document.execCommand('fontSize', false, '7');
			document.querySelectorAll('[contenteditable] font[size="7"]').forEach((font) => {
				font.removeAttribute('size');
				font.style.fontSize = this.value;
			});
		});

		// フォントファミリーを変更する処理
		fontFamilySelect.addEventListener('change', function () {
			document.execCommand('fontName', false, this.value);
		});

		// 背景色を変更する処理
		document.getElementById('bg-color').addEventListener('input', function () {
			document.execCommand('backColor', false, this.value);
		});

		// テキスト色を変更する処理
		document.getElementById('text-color').addEventListener('input', function () {
			document.execCommand('foreColor', false, this.value);
		});

		// 太字ボタン
		document.getElementById('bold').addEventListener('click', function () {
			document.execCommand('bold');
		});

		// イタリック
		document.getElementById('italic').addEventListener('click', function () {
			document.execCommand('italic');
		});

		// 下線
		document.getElementById('underline').addEventListener('click', function () {
			document.execCommand('underline');
		});

		// 新規作成
		document.getElementById('new-file-button').addEventListener('click', () => {
			if (confirm('現在の編集内容が失われます。新規作成しますか?')) {
				const editableElements = document.querySelectorAll('[contenteditable]');
				
				// 編集エリアを初期化
				editableElements.forEach((element, index) => {
					if (index === 0) {
						element.innerHTML = "本文1"; // 初期値を設定
					} else if (index === 1) {
						element.innerHTML = "〇〇新聞";
					} else if (index === 2) {
						element.innerHTML = "作成者";
					} else if (index === 3){
						element.innerHTML = "本文2";
					} else if (index === 4){
						element.innerHTML = "本文3";
					} else {
						element.innerHTML = ""; // その他のセクションは空に
					}
				});

				alert('新規作成を開始しました。');
			}
		});

		// 開く(JSONファイルを開く)
		document.getElementById('load-file-button').addEventListener('click', () => {
			document.getElementById('load-file').click();
		});

		// JSONファイルを読み込んで編集エリアに復元
		document.getElementById('load-file').addEventListener('change', function () {
			const file = this.files[0];
			if (file && file.type === 'application/json') {
				const reader = new FileReader();
				reader.onload = function (e) {
					try {
						const content = JSON.parse(e.target.result); // JSONを解析
						const editableElements = document.querySelectorAll('[contenteditable]');

						// JSONの内容を各セクションに復元
						editableElements.forEach((element, index) => {
							const key = `section${index + 1}`;
							if (content[key]) {
								element.innerHTML = content[key];
							}
						});

						alert('編集内容を復元しました。');
					} catch (error) {
						alert('JSONファイルの形式が正しくありません。');
						console.error('Error parsing JSON:', error);
					}
				};
				reader.readAsText(file); // ファイルをテキストとして読み込む
			} else {
				alert('JSONファイルを選択してください。');
			}
		});

		// 保存(JSONファイルを保存する)
		document.getElementById('save-file').addEventListener('click', function () {
			const editableElements = document.querySelectorAll('[contenteditable]');
			const content = {};

			editableElements.forEach((element, index) => {
				content[`section${index + 1}`] = element.innerHTML.trim();
			});

			// JSON文字列を生成
			const jsonString = JSON.stringify(content, null, 2); // Pretty-print JSON with 2 spaces
			const blob = new Blob([jsonString], { type: 'application/json' });
			const a = document.createElement('a');
			a.href = URL.createObjectURL(blob);
			a.download = '新聞.json'; // 保存するファイル名
			a.click();
			URL.revokeObjectURL(a.href); // メモリ解放
		});

		// 自動保存機能
		function saveContent() {
			const editableElements = document.querySelectorAll('[contenteditable]');
			const content = {};
			editableElements.forEach((element, index) => {
				content[`editable${index}`] = element.innerHTML;
			});
			localStorage.setItem('savedContent', JSON.stringify(content));
			console.log('Content saved at', new Date().toLocaleTimeString());
		}

		// 自動保存したデータを復元
		function restoreContent() {
			const savedContent = JSON.parse(localStorage.getItem('savedContent'));
			if (savedContent) {
				const editableElements = document.querySelectorAll('[contenteditable]');
				editableElements.forEach((element, index) => {
					if (savedContent[`editable${index}`]) {
						element.innerHTML = savedContent[`editable${index}`];
					}
				});
				console.log('Content restored.');
			}
		}

		// ページ読み込み時に復元
		window.addEventListener('load', restoreContent);

		// 1分ごとに自動保存
		setInterval(saveContent, 60000);
	</script>
</body>
</html>

ブラウザで印刷をしたときに、
とりあえず初期設定で印刷しても、A4用紙にうまくはまるように
設定はしています。(小学生でも自分で印刷できるはず)

ブラウザによって、余白の設定がまちまちなので、
上部の余白を0mmにしてあります。
もし、気になる用であれば、
ブラウザの印刷設定を少しいじってみて下さい。

活用していただければ
幸いです。

(以下、R7.1.5に追記)
※GASの使い方については、
GAS(Google Apps Script)の始め方①(準備編)と②(はじめてのwebアプリ作成編)をご覧ください。


いいなと思ったら応援しよう!