Skip to content

Instantly share code, notes, and snippets.

  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save hcmiya/3645e7ef572685c56e58fca5e1375e0e to your computer and use it in GitHub Desktop.
マストドンに定型文機能を付けるやつ
// ==UserScript==
// @name マストドンに定型文機能を付けるやつ
// @namespace https://js4.in/ns/
// @include *
// @version 1.0.11
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @author Miyagi Hikaru
// @website https://gist.github.com/hcmiya/3645e7ef572685c56e58fca5e1375e0e
// @updateURL https://gist.github.com/hcmiya/3645e7ef572685c56e58fca5e1375e0e/raw/mastodon-ni-teikeibun-kino-wo-tukeru-yatu.user.js
// ==/UserScript==
// Copyright: 2018 Miyagi Hikaru https://mastodon.home.js4.in/@hcm
// License: zlib License https://www.zlib.net/zlib_license.html
const xml = {
style: `<style xmlns="http://www.w3.org/1999/xhtml" id="js4us-teikei-style"><![CDATA[
@namespace svg "http://www.w3.org/2000/svg";
.js4us-teikei-hontai {
background: white;
color: black;
margin: 10px;
border-radius: 4px;
}
.js4us-teikei-kakusi.js4us-teikei-hontai {
display: none;
}
.js4us-teikei button {
background: transparent;
margin: 0;
border: 0;
padding: 0;
}
.js4us-teikei-list {
max-height: 150px;
overflow-x: hidden;
overflow-y: auto;
}
.js4us-teikei-komoku {
padding: 4px;
border-width: 0 0 1px 0;
border-style: solid;
border-color: silver;
}
.js4us-teikei-komoku-sosa button {
padding: 1px 3px;
}
.js4us-teikei-hontai svg|path.js4us-teikei-svg-stroke {
stroke: black;
}
.js4us-teikei-hontai svg|path.js4us-teikei-svg-fill {
fill: black;
}
.js4us-teikei-komoku-naiyo {
white-space: pre-wrap;
}
.js4us-teikei-komoku-sosa {
display: none;
}
.js4us-teikei-sentaku .js4us-teikei-komoku-sosa {
display: block;
}
.js4us-teikei-del-kakunin::before {
content: "マジで";
}
.js4us-teikei-nyuryoku {
box-sizing: border-box;
width: 100%;
}
.js4us-teikei-nyuryoku:disabled {
display: none;
}
.js4us-teikei-add {
display: block;
padding: 1px 3px;
text-align: center;
box-sizing: border-box;
width: 100%;
}
#js4us-teikei-category > p {
display: table;
width: 100%;
}
#js4us-teikei-category > p.js4us-teikei-kakusi {
display: none;
}
.js4us-teikei-category-col1, .js4us-teikei-category-col2 {
display: table-cell;
}
.js4us-teikei-category-col1 {
width: 80%;
}
.js4us-teikei-category-col2 {
text-align: center;
}
#js4us-teikei-category-sentaku select, #js4us-teikei-category-hensyu input {
box-sizing: border-box;
width: 100%;
}
]]></style>`,
root: `
<div class="js4us-teikei js4us-teikei-kakusi js4us-teikei-hontai" xmlns="http://www.w3.org/1999/xhtml" xmlns:g="http://www.w3.org/2000/svg">
<div id="js4us-teikei-category">
<p id="js4us-teikei-category-hensyu" class="js4us-teikei-kakusi">
<span class="js4us-teikei-category-col1">
<input list="js4us-teikei-category-list"/><datalist id="js4us-teikei-category-list"/>
</span>
<span class="js4us-teikei-category-col2">
<button id="js4us-teikei-category-add">
<g:svg viewBox="-7 -7 14 14" width="14" height="14">
<g:path class="js4us-teikei-svg-stroke" d="M-6,0H6M0-6V6" stroke-width="2"/></g:svg></button>
<button id="js4us-teikei-category-del">
<g:svg viewBox="-7 -7 14 14" width="14" height="14">
<g:path class="js4us-teikei-svg-stroke" d="M-4.2-4.2 4.2,4.2 M4.2-4.2 -4.2,4.2" stroke-width="2"/></g:svg></button>
</span>
</p>
<p id="js4us-teikei-category-sentaku">
<span class="js4us-teikei-category-col1"><select/></span>
<span class="js4us-teikei-category-col2"><button id="js4us-teikei-category-hensyu-kirikae">編集</button></span>
</p>
</div>
<ul class="js4us-teikei-list"/>
<p><textarea class="js4us-teikei-nyuryoku"/></p>
<p><button class="js4us-teikei-add">追加</button></p>
</div>
`,
komoku_tmpl: `
<li class="js4us-teikei-komoku" xmlns="http://www.w3.org/1999/xhtml" xmlns:g="http://www.w3.org/2000/svg">
<p class="js4us-teikei-komoku-naiyo"/>
<p class="js4us-teikei-komoku-sosa">
<button class="js4us-teikei-copy">投稿欄へ追記</button>
<button class="js4us-teikei-edit">定型文欄へ複製</button>
<button class="js4us-teikei-ue">↑</button>
<button class="js4us-teikei-sita">↓</button>
<button class="js4us-teikei-del">削除</button>
</p>
</li>
`,
compose_button: `<button class="text-icon-button" title="定型文" >定</button>`,
};
function jtid (id) {
return document.getElementById("js4us-teikei-" + id);
}
HTMLElement.prototype.jtsel = function (cls) {
return this.querySelector(".js4us-teikei-" + cls);
};
HTMLElement.prototype.jtselall = function (cls) {
return this.querySelectorAll(".js4us-teikei-" + cls);
};
HTMLElement.prototype.jthas = function (cls) {
return this.classList.contains('js4us-teikei-' + cls);
};
HTMLElement.prototype.jtrm = function (cls) {
this.classList.remove('js4us-teikei-' + cls);
};
HTMLElement.prototype.jttgl = function (cls, f) {
return this.classList.toggle('js4us-teikei-' + cls, f);
};
HTMLElement.prototype.jtadd = function (cls) {
this.classList.add('js4us-teikei-' + cls);
};
const node = {};
const data = {};
const parser = new DOMParser();
function tonode(str) {
return parser.parseFromString(str, 'text/xml').documentElement;
}
function save() {
GM_setValue("data", Object.keys(data).map(cat => [cat].concat(data[cat]).join('\u001f')).join('\u001e'));
}
save.li2data = function() {
data[node.cat.sentaku.list.value] = Array.from(node.list_root.jtselall('komoku-naiyo')).map(e => e.textContent.replace(/[\u001e-\u001f]/g, '\ufffd'));
save();
};
function idogo_button(li) {
if (!li) return;
li.jtsel('ue').disabled = !li.previousSibling;
li.jtsel('sita').disabled = !li.nextSibling;
}
function del_kakunin_torikesi(li) {
li.jtsel('del').jtrm('del-kakunin');
}
function nyuryoku_kaihei() {
node.nyuryoku.disabled = !node.nyuryoku.value && !!data[node.cat.sentaku.list.value].length;
}
function cat_nyuryoku_kaihei() {
node.cat.sentaku.root.jttgl('kakusi');
node.cat.hensyu.root.jttgl('kakusi');
nyuryoku_kaihei();
}
const ev = {
sentaku: ev => {
let prevli = node.list_root.jtsel('sentaku');
let li = ev.target.parentNode;
if (prevli) {
prevli.jtrm('sentaku');
del_kakunin_torikesi(prevli);
}
if (li == prevli) return;
li.jtadd('sentaku');
},
copy: ev => {
node.form_text.value += ev.target.parentNode.parentNode.jtsel("komoku-naiyo").textContent;
},
edit: ev => {
node.nyuryoku.value = ev.target.parentNode.parentNode.jtsel("komoku-naiyo").textContent;
node.nyuryoku.disabled = false;
},
ue: ev => {
const koko = ev.target.parentNode.parentNode;
const ue = koko.previousSibling;
koko.parentNode.insertBefore(koko, ue);
del_kakunin_torikesi(koko);
idogo_button(koko);
idogo_button(ue);
save.li2data();
},
sita: ev => {
const koko = ev.target.parentNode.parentNode;
const sita = koko.nextSibling;
koko.parentNode.insertBefore(koko, sita.nextSibling);
del_kakunin_torikesi(koko);
idogo_button(koko);
idogo_button(sita);
save.li2data();
},
del: ev => {
const btn = ev.target;
if (!btn.jthas('del-kakunin')) {
btn.jtadd('del-kakunin');
return;
}
const n = btn.parentNode.parentNode;
const prev = n.previousSibling;
const next = n.nextSibling;
n.parentNode.removeChild(n);
idogo_button(prev);
idogo_button(next);
save.li2data();
},
add: ev => {
const text = node.nyuryoku.value;
if (node.nyuryoku.disabled || !text) {
node.nyuryoku.disabled = false;
node.nyuryoku.focus();
return;
}
try {
const list = Array.from(node.list_root.jtselall('komoku-naiyo')).map(e => {
if (text == e.textContent) throw null;
return e.textContent;
});
list.push(text);
data[node.cat.sentaku.list.value] = list;
save();
const li = teikei_gen_list_item(text);
idogo_button(li);
idogo_button(li.previousSibling);
node.nyuryoku.value = "";
} catch (e) {}
},
cat: {
kirikae: ev => {
node.cat.sentaku.root.jttgl('kakusi');
node.cat.hensyu.root.jttgl('kakusi');
node.cat.hensyu.list.textContent = "";
node.cat.hensyu.nyuryoku.value = node.cat.sentaku.list.value;
Array.from(node.cat.sentaku.list.options).forEach(o => {
node.cat.hensyu.list.appendChild(o.cloneNode(true));
});
},
add: ev => {
const sel = node.cat.sentaku.list;
const o = document.createElement("option");
// option.text にテキストを突っ込むことで空白をノーマライズ
// https://html.spec.whatwg.org/multipage/form-elements.html#dom-option-text
o.text = node.cat.hensyu.nyuryoku.value.replace(/[\u001e-\u001f]/g, '\ufffd');
const cat = o.text;
try {
Array.from(sel.options).forEach((o, i, a) => {
if (o.text == cat) {
sel.selectedIndex = i;
throw null;
}
});
const len = sel.length;
data[cat] = [];
sel.add(o);
sel.selectedIndex = len;
save();
}
catch (e) {}
naiyo_seisei(cat);
cat_nyuryoku_kaihei();
},
del: ev => {
const sel = node.cat.sentaku.list;
const o = document.createElement("option");
o.text = node.cat.hensyu.nyuryoku.value.replace(/[\u001e-\u001f]/g, '\ufffd');
const cat = o.text;
try {
Array.from(sel.options).forEach((o, i, a) => {
if (o.text == cat) {
throw i;
}
});
}
catch (i) {
if (confirm(`本当に${cat}を削除しますか`)) {
sel.remove(i);
delete data[cat];
if (!sel.length) {
o.text = '';
sel.add(o);
data[''] = [];
}
save();
if (i == sel.length) i--;
sel.selectedIndex = i;
naiyo_seisei(sel.value);
}
}
cat_nyuryoku_kaihei();
},
},
};
function teikei_gen_list_item(text) {
const li = node.komoku_tmpl.cloneNode(true);
const naiyo = li.jtsel("komoku-naiyo");
naiyo.textContent = text;
naiyo.addEventListener('click', ev.sentaku, true);
["copy", "edit", "del", "ue", "sita"].forEach(e => {
li.jtsel(e).addEventListener('click', ev[e], true);
});
return node.list_root.appendChild(li);
}
const data_ikou = [
function() {
const serdata = GM_getValue("js4us_teikei");
GM_deleteValue("js4us_teikei");
if (serdata) {
GM_setValue("data", '\u001f' + serdata.replace(/\u001e/g, "\u001f"));
}
else {
throw null;
}
},
];
const data_version_saisin = data_ikou.length;
function yomikomi() {
const data_version = GM_getValue("version") || 0;
try {
for (let i = data_version; i < data_version_saisin; i++) {
data_ikou[i]();
}
}
catch (e) {}
GM_setValue("version", data_version_saisin);
(GM_getValue("data") || '').split('\u001e').forEach(sercat => {
const cat = sercat.split('\u001f');
const head = cat.shift();
data[head] = cat;
const catopt = node.cat.sentaku.list.appendChild(document.createElement('option'));
catopt.text = head;
});
node.cat.sentaku.list.selectedIndex = 0;
}
function naiyo_seisei(cat) {
node.list_root.textContent = '';
data[cat].forEach(e => {
teikei_gen_list_item(e);
});
idogo_button(node.root.querySelector('ul > li:first-child'));
idogo_button(node.root.querySelector('ul > li:last-child'));
}
function node_syutoku() {
node.list_root = node.root.querySelector('ul');
node.nyuryoku = node.root.querySelector('textarea');
node.form_text = document.body.querySelector(".autosuggest-textarea__textarea");
node.cat = {};
node.cat.hensyu = {};
node.cat.hensyu.root = jtid('category-hensyu');
node.cat.hensyu.list = jtid('category-list');
node.cat.hensyu.nyuryoku = node.cat.hensyu.root.querySelector('input');
node.cat.hensyu.add = jtid('category-add');
node.cat.hensyu.del = jtid('category-del');
node.cat.sentaku = {};
node.cat.sentaku.root = jtid('category-sentaku');
node.cat.sentaku.list = node.cat.sentaku.root.querySelector('select');
node.cat.sentaku.edit = jtid('category-hensyu-kirikae');
}
function node_event_settei() {
node.nyuryoku.addEventListener('blur', ev => {
nyuryoku_kaihei();
});
node.compose_button.addEventListener('click', ev => {
node.root.jttgl('kakusi');
node.compose_button.classList.toggle('active');
});
node.root.jtsel("add").addEventListener('click', ev.add);
node.cat.sentaku.list.addEventListener("input", ev => {
naiyo_seisei(node.cat.sentaku.list.value);
nyuryoku_kaihei();
});
node.cat.sentaku.edit.addEventListener("click", ev.cat.kirikae);
node.cat.hensyu.add.addEventListener("click", ev.cat.add);
node.cat.hensyu.del.addEventListener("click", ev.cat.del);
}
function teikei_main(compose_form) {
for (let k in xml) {
node[k] = tonode(xml[k]);
}
document.body.querySelector('.compose-form__buttons').appendChild(node.compose_button);
document.head.appendChild(node.style);
compose_form.parentNode.insertBefore(node.root, compose_form.nextSibling);
node_syutoku();
node_event_settei();
yomikomi();
naiyo_seisei(node.cat.sentaku.list.value);
node.nyuryoku.disabled = !!data[node.cat.sentaku.list.value].length;
}
function main(mainnode) {
if (!mainnode || !mainnode.dataset.props) return;
const mo = new MutationObserver(mrs => {
try {
mrs.forEach(mr => {
Array.from(mr.addedNodes).forEach(e => {
const compose_form = e.querySelector('.compose-form');
if (!compose_form) return;
mo.disconnect();
teikei_main(compose_form);
throw null;
});
});
}
catch (e) {}
});
mo.observe(mainnode, {childList: true, subtree: true});
setTimeout(() => {mo.disconnect();}, 2000);
}
main(document.getElementById('mastodon'));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment