scrapbox-history-slider
/icons/TODO.icon refactoringしつつ、programの構造を理解する
スライダーをホバーすると、履歴の時刻が表示されるようにしたい
2021-04-14 19:35:18 一部の関数を後置した
2021-05-23 06:18:56 refactoring失敗してバグってる
2021/3/31
いらないコメントを削った
一部のreduce()をflatMap()で書き換えた
then()をasync/awaitで書き換えた
APIの書式はここに書いてある
code:js
import('/api/code/programming-notes/scrapbox-history-slider/script.js');
code:script.js
import {div, button, pre, input} from '../easyDOMgenerator/script.js';
const CHANGE_TYPE_INSERT = '_insert';
const CHANGE_TYPE_UPDATE = '_update';
const CHANGE_TYPE_DELETE = '_delete';
const CHANGE_TYPE_TITLE = 'title';
const CHANGE_TYPE_DESCRIPTIONS = 'descriptions';
const CHANGE_TYPE_LINKS = 'links';
const onHistorySliderClick = async () => {
const res = await fetch(/api/commits/${scrapbox.Project.name}/${scrapbox.Page.id});
const commitData = await res.json();
// all changes of all commits
const changes = commitData.commits.flatMap(({changes}) => changes.map(makeChangeInfo));
// 行の文字列のコレクション
// - すべての履歴それぞれのtextを順に入れておく
const texts = [];
// 行ごとの履歴情報を配列にする
// id: LindeId, updatedAt: idx1, ... , deletedAt?: idx } // - 行ごとに id, 更新された履歴連番, 削除された履歴連番を持つ
// - 出現した行すべてが入るので, ページの行とは対応していないことに注意
// 1個目の履歴
let initHistory;
console.debug({ initChange, restChanges, changes });
// 最初のイベントは _insert か _update のはずなのでとにかく履歴をつくる
if (initChange.type === CHANGE_TYPE_INSERT) {
// タイトルの次の行に書くとき insert
// TODO: このときタイトルは title という change に入るので取りこぼしている
texts.push(initChange.lines.text);
initHistory = {
id: initChange.lines.id,
};
} else if (initChange.type === CHANGE_TYPE_UPDATE) {
// タイトル行に書くとき
texts.push(initChange.lines.text);
initHistory = {
id: initChange.targetId,
};
} else {
console.log('init change neither _insert nor _update');
}
const allHistory = restChanges.reduce((history, change, i) => {
console.debug({ text: change.lines && change.lines.text, texts, history });
const lineIndex = history.findIndex(h => h.id === change.targetId);
switch (change.type) {
case CHANGE_TYPE_INSERT:
texts.push(change.lines.text);
const line = {
id: change.lines.id,
};
// 最後の行なら単に1行足す
if (change.targetId === '_end') {
history.push(line);
break;
}
// 最後の行じゃないなら入れるべき場所に差し込む
if (lineIndex === -1) {
console.info(_insert: line index not found. commit change (${i}):, change);
return history;
}
history.splice(lineIndex, 0, line);
break;
case CHANGE_TYPE_UPDATE:
texts.push(change.lines.text);
if (lineIndex === -1) {
console.info(_update: line index not found. commit change (${i}):, change);
return history;
}
historylineIndex.updatedAt.push(texts.length - 1); break;
case CHANGE_TYPE_DELETE:
texts.push(null);
if (lineIndex === -1) {
console.info(_delete: line index not found. commit change (${i}):, change);
return history;
}
historylineIndex.deletedAt = texts.length - 1; break;
// 上記以外のchangesは全て無視する
default:
break;
}
return history;
console.debug({ allHistory });
// 履歴連番から履歴を得る
// - その時点で存在した行の履歴
const getHistoryByTime = time => allHistory.flatMap(({updatedAt, deletedAt, id}) => {
const lastUpdatedAt = updatedAt.filter(u => u <= time).pop(); // これまでの更新のうち最新の履歴連番
// まだ出現していない or もう消えてるなら, その行は返さない
if (lastUpdatedAt === undefined // まだ出現していない
|| (deletedAt !== undefined && deletedAt <= time) // もう消えている
) return [];
// 出現している(かつ消えてない)ならその行を返す
return id, textIndex: lastUpdatedAt };
});
render(
// 履歴連番に対応するテキストを得る
time => getHistoryByTime(time)
);
};
scrapbox.PageMenu.addMenu({
title: '履歴スライダー',
onClick: onHistorySliderClick,
});
changesのデータを変換する関数
{_update: 'xxx', lines,}を{type: '_update', targetId: 'xxx', lines,}に変える
code:script.js
function makeChangeInfo(rawChange) {
const change = {};
if (rawChange._insert) {
change.type = CHANGE_TYPE_INSERT;
} else if (rawChange._update) {
change.type = CHANGE_TYPE_UPDATE;
} else if (rawChange._delete) {
change.type = CHANGE_TYPE_DELETE;
} else if (rawChange.title) {
change.type = CHANGE_TYPE_TITLE;
} else if (rawChange.descriptions) {
change.type = CHANGE_TYPE_DESCRIPTIONS;
} else if (rawChange.links) {
change.type = CHANGE_TYPE_LINKS;
} else {
console.info('unknown type. raw commit change: ', rawChange);
}
if (rawChange.lines) change.lines = rawChange.lines;
return change;
}
UIを作る
code:script.js
function render(getTextByTime) {
// 操作系を表示してスライダーで操作できるようにする
// - 指定された履歴連番からテキストを得て表示する
const background = div({
className: 'history-background',
css: {
position: 'fixed',
'z-index': '89999',
top: '0',
right: '0',
width: '100%',
height: '100%',
'background-color': 'rgba(255, 255, 255, 0.4)',
},
});
const content = pre({
className: 'history-pre',
css: {
width: '100%',
'max-height': '100%',
},
}, getTextByTime(texts.length));
const closeButton = button({
className: 'history-close-button',
css: {
'font-size': '30px',
'line-height': '1em',
padding: '0',
position: 'fixed',
'z-index': '90001',
top: '10px',
right: '10px',
width: '30px',
height: '30px',
},
}, 'x');
const updateHistory = ({target: {value}}) => {
const time = parseInt(value, 10);
content.textContent = getTextByTime(time);
console.debug({ time, html: content.textContent });
};
const container = div({
className: 'history-container',
css: {
'background-color': '#555555',
position: 'fixed',
'z-index': '90000',
top: '5px',
left: '5px',
width: 'calc( 100% - 10px )',
'max-height': '100%',
overflow: 'scroll',
},
},
closeButton,
content,
input({
className: 'history-slider',
type: 'range', max: texts.length, min: 0, step: 1,
value: texts.length,
css: {
position: 'fixed',
'z-index': '99999',
top: '10px',
width: '90%',
},
oninput: updateHistory,
onchange: updateHistory,
}),
);
const removeHistoryContainer = () => {
container.remove();
background.remove();
};
closeButton.addEventListener('click', removeHistoryContainer);
background.addEventListener('click', removeHistoryContainer);
document.body.append(container, background);
}