Last active
June 23, 2018 14:20
-
-
Save hcmiya/3645e7ef572685c56e58fca5e1375e0e to your computer and use it in GitHub Desktop.
マストドンに定型文機能を付けるやつ
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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