目次
はじめに
こんにちは。クラウド事業部の丸山です。
技術ブログを書き始めて感じたことの一つに、操作性の悪さがあります。
ただでさえ業務の隙間時間での記事作成をする場面が多い中で、
操作性が悪いことによる煩雑な操作が、記事作成を後回しにしてしまう要因となることを感じました。
弊社ははてなブログを利用していますが、WordPressなどに比べるとどうしてもカスタマイズ性が不足していると感じます。
これは既存のブログシステムを利用する上では切り離せない課題となるため、
自分で使いやすいように解決しようといくつかの機能を作成してみました。
今回は第一弾として、テンプレート管理機能をご紹介します。
定型文機能・投稿記事テンプレート管理機能について
企業として記載するブログの場合、統一感を出すために同じようなフォーマットを利用して作成することが多いかと思いますが、
実際の記事を作成する際に、同じフォーマットを利用するために毎回内容をコピペして準備することが手間でした。
既存の定型文機能
はてなブログにも定型文機能がありますが、階層をたどる手間と、タイトルが定型文の対象となっていない点、
非常に細かい部分ではありますが、毎回使う機能であることを考えたときに個人的に使いづらいと感じました。
help.hatenablog.com
投稿記事テンプレート管理機能
今回実装したテンプレート管理機能としてはテンプレートとしての利用だけではなく、ローカル下書きの様な用途でも利用できるため、
企業でのブログ共用利用においては、ブログ上での下書き作成における心理的なハードルを下げることが期待できます。
誰の目にも触れずにプレビューを確認しながら下書きを作成できることは記事作成の上で大きなメリットになるため、
操作性の悪さを改善しつつ、ブログ記事作成の裾野を広げられるのではないかと考えています。
Tampermonkeyを利用した実装としておりますので、基本的な内容などについては過去記事も合わせてご参照ください。
techblog.ap-com.co.jp
非常に前置きが長くなってしまいましたが、こちらの記事では今回作成したテンプレートマネージャーの
コード共有がメインのコンテンツとなりますので、簡単に機能説明とコード共有だけさせていただければと思います。
前提
文字情報の保存をしているため、Markdown形式編集画面での動作確認をしております。
そのため、その他の編集画面をご利用の際には利用できない可能性がありますので、あらかじめご了承ください。
機能紹介
テンプレートマネージャーの外観
サイドメニューの一番下に配置されたアイコンからアクセスできます。
また、クリックした際に以下のような画面が表示されます。
- テンプレートタイトル: テンプレートにするタイトルを入力します。
- テンプレート内容: テンプレートにする内容を入力します。
- 保存: 編集中のテンプレートタイトル・テンプレート内容を保存します。頻繁に使用するフォーマットを簡単に再利用できます。
- 読み込み: 編集中のブログ記事をテンプレートマネージャーのテンプレートタイトル・テンプレート内容に読み込みます。
- エクスポート: テンプレートリストをJSON形式でエクスポートし、外部でのバックアップや他のデバイスでの使用が可能です。
- インポート: JSON形式のファイルを読み込み、テンプレートリストに追加します。これにより、テンプレートを簡単に復元または追加できます。
- 並べ替え: ドラッグ&ドロップでテンプレートの順序を変更できます。≡ アイコンを使用してリスト内で上下に移動します。
- 削除: テンプレートを選択し、削除を確認後に実行します。× アイコンをクリック後、確認画面で削除を承認します。
※通常はJavaScriptのlocalStrageとして保持されるため、ブラウザキャッシュを削除するまではデータが維持されます。
データの保管などをする際にエクスポート、インポートをする想定です。
利用コード
// ==UserScript==
// @name はてなブログテンプレートマネージャー
// @namespace http://tampermonkey.net/
// @version 1.0
// @description ローカルストレージおよびエクスポート/インポート機能を備えたブログテンプレートを管理します。クリックで特定のフィールドにテンプレート内容を直接挿入します。テンプレートの削除、並べ替え機能、およびインポート時の上書きまたは追記オプションを提供します。
// @match https://blog.hatena.ne.jp/*edit*
// @grant GM_download
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @require https://code.jquery.com/ui/1.12.1/jquery-ui.min.js
// ==/UserScript==
(function() {
'use strict';
// 指定された要素の内部にボタンを追加
const targetContainerSelector = '.curation-add';
const targetContainer = $(targetContainerSelector);
if (targetContainer.length) {
const templateButton = $('<div class="curation-itemlist tipsy-left" id="template-button" style="width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; cursor: pointer; border: none; margin-top: 10px;"></div>');
targetContainer.append(templateButton);
}
// テンプレートマネージャーUIを作成(最初は非表示)
const templateManagerUI = $(`
<div id="template-manager" style="position: fixed; bottom: 10px; right: 10px; background: white; padding: 10px; border: 1px solid black; z-index: 9999; display: none;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3>テンプレートマネージャー</h3>
<button id="close-template-manager" style="background: red; color: white; border: none;">×</button>
</div>
<input type="hidden" id="template-index" />
<input type="text" id="template-title" placeholder="テンプレートタイトル" style="width: 100%; margin-bottom: 5px;" />
<textarea id="template-body" placeholder="テンプレート内容" style="width: 100%; height: 100px; margin-bottom: 5px;"></textarea>
<button id="save-template">保存</button>
<button id="load-template">読み込み</button>
<button id="export-templates">エクスポート</button>
<input type="file" id="import-templates" style="display: none;" />
<button id="import-button">インポート</button>
<div id="template-list" style="margin-top: 10px; max-height: 200px; overflow-y: auto; user-select: none;"></div>
</div>
`);
$('body').append(templateManagerUI);
// ローカルストレージからテンプレートを読み込み
let templates = JSON.parse(localStorage.getItem('templates') || '[]');
function updateTemplateList() {
const templateList = $('#template-list');
templateList.empty();
templates.forEach((template, index) => {
const templateItem = $(`
<div data-index="${index}" style="border: 1px solid #ccc; padding: 5px; margin-bottom: 5px; cursor: pointer; display: flex; align-items: center;">
<div class="drag-handle" style="cursor: move; margin-right: 10px;">☰</div>
<strong style="flex-grow: 1;">${template.title}</strong>
<button class="delete-button" style="background: red; color: white; border: none; margin-left: 10px;">×</button>
</div>
`);
templateItem.find('.delete-button').on('click', function(event) {
event.stopPropagation();
const confirmDelete = confirm('このテンプレートを削除してもよろしいですか?');
if (confirmDelete) {
templates.splice(index, 1);
localStorage.setItem('templates', JSON.stringify(templates));
updateTemplateList();
}
});
templateItem.find('.drag-handle').on('mousedown', function(event) {
$(this).parent().css('cursor', 'move');
}).on('mouseup', function(event) {
$(this).parent().css('cursor', 'pointer');
});
templateItem.on('click', function() {
const titleField = $('#title');
const bodyField = $('#body');
if (titleField.length && bodyField.length) {
titleField.val(template.title);
bodyField.val(template.body);
} else {
alert('タイトルまたは本文フィールドが見つかりません!');
}
});
templateItem.on('dblclick', function() {
setTimeout(() => {
$('#template-index').val(index);
$('#template-title').val(template.title);
$('#template-body').val(template.body);
$('#template-manager').show();
}, 200); // 編集画面が表示されるまで少し待機
});
templateList.append(templateItem);
});
// テンプレートリストを並べ替え可能にする
templateList.sortable({
handle: '.drag-handle',
update: function(event, ui) {
const sortedIndexes = $(this).sortable('toArray', { attribute: 'data-index' });
const newTemplates = [];
sortedIndexes.forEach(index => {
newTemplates.push(templates[parseInt(index)]);
});
templates = newTemplates;
localStorage.setItem('templates', JSON.stringify(templates));
updateTemplateList();
}
});
}
updateTemplateList();
// ボタンをクリックしてテンプレートマネージャーを表示
$('#template-button').on('click', function(event) {
event.preventDefault(); // 投稿のデフォルトアクションを防ぐ
$('#template-manager').toggle();
});
// 閉じるボタンをクリックしてテンプレートマネージャーを閉じる
$('#close-template-manager').on('click', function(event) {
$('#template-manager').hide();
});
// テンプレートを保存
$('#save-template').on('click', function() {
const index = $('#template-index').val();
const title = $('#template-title').val();
const body = $('#template-body').val();
if (title && body) {
const existingIndex = templates.findIndex(t => t.title === title);
if (existingIndex !== -1 && existingIndex !== parseInt(index)) {
const confirmOverwrite = confirm('同じタイトルのテンプレートが存在します。上書きしますか?');
if (!confirmOverwrite) {
return;
}
templates[existingIndex] = { title, body };
} else {
if (index) {
// 既存のテンプレートを更新
templates[index] = { title, body };
} else {
// 新しいテンプレートを追加
templates.push({ title, body });
}
}
localStorage.setItem('templates', JSON.stringify(templates));
updateTemplateList();
$('#template-index').val('');
$('#template-title').val('');
$('#template-body').val('');
} else {
alert('タイトルと本文の両方を入力してください!');
}
});
// タイトルと本文を読み込んでマネージャーのフィールドにコピー
$('#load-template').on('click', function() {
const titleField = $('#title');
const bodyField = $('#body');
if (titleField.length && bodyField.length) {
const title = titleField.val();
const body = bodyField.val();
if (title || body) {
$('#template-title').val(title);
$('#template-body').val(body);
alert('タイトルと本文がテンプレートフィールドにコピーされました。');
} else {
alert('タイトルまたは本文のどちらかを入力してください。');
}
} else {
alert('タイトルまたは本文フィールドが見つかりません!');
}
});
// テンプレートをエクスポート
$('#export-templates').on('click', function() {
const blob = new Blob([JSON.stringify(templates)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'templates.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
// テンプレートをインポート
$('#import-button').on('click', function() {
$('#import-templates').click();
});
$('#import-templates').on('change', function(event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = function(e) {
const importedTemplates = JSON.parse(e.target.result);
const importAction = confirm('既存のテンプレートを上書きしますか?「キャンセル」をクリックすると追記されます。');
if (importAction) {
// 既存のテンプレートを上書き
templates = importedTemplates;
} else {
// 既存のテンプレートに追記
templates = templates.concat(importedTemplates);
}
localStorage.setItem('templates', JSON.stringify(templates));
updateTemplateList();
};
reader.readAsText(file);
});
})();