履歴スライダー
参考:
code:script.js
/*
LineId --- 24文字のstring
interface commit.changes {
_update: LineId; // 更新する行
lines: {
origText: string; // 元のテキスト
text: string; // 新しいテキスト
};
} | {
_insert: LineId | "_end"; // 行を入れる場所の後ろ側の行
lines: {
id: LineId; // 入れる行
text: string; // 入れるテキスト
};
} | {
_delete: LineId; // 消す行
lines: {
origText: string; // 消す前にあったテキスト
};
} | {
title: string; // 新しいタイトル
titleLc: string; // 新しいタイトル(小文字)
} | {
descriptions: string[]; // 新しいページ説明文
} | {
links: string[]; // 新しいリンク
}
*/
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 getHrefInfo = href => {
const a = document.createElement('a');
a.href = href;
return {
host: a.host,
project,
page,
};
};
const fetchCommitDataOfHref = href => {
const { host, project, page } = getHrefInfo(href);
return fetch(https://${host}/api/pages/${project}/${page})
.then(res => res.json())
.then(pageData => {
const pageId = pageData.id;
const commitUrl = https://${host}/api/commits/${project}/${pageId};
return fetch(commitUrl);
})
.then(res => res.json());
};
const 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;
};
const onHistorySliderClick = () => {
fetchCommitDataOfHref(location.href)
.then(commitData => {
// returns all changes of all commits
return commitData.commits.reduce((all, rawCommit) => {
return [
...all,
...rawCommit.changes.map(makeChangeInfo),
];
}, []);
})
.then(changes => {
// 行の文字列のコレクション
// - すべての履歴それぞれの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,
};
if (change.targetId === '_end') {
// 最後の行なら単に1行足す
history.push(line);
} else {
// 最後の行じゃないなら入れるべき場所に差し込む
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;
}
return history;
console.debug({ allHistory });
return { texts, allHistory };
})
.then(({ texts, allHistory }) => {
// 履歴連番から履歴を得る
// - その時点で存在した行の履歴
const getHistoryByTime = time => {
return allHistory.reduce((all, history) => {
// これまでの更新のうち最新の履歴連番
const updatedAt = history.updatedAt.filter(u => u <= time).pop();
// もう出現しているかどうか
const appeared = updatedAt !== undefined;
// もう消えてるかどうか
const deleted = history.deletedAt !== undefined && history.deletedAt <= time;
// まだ出現していない or もう消えてるなら, その行は返さない
if (!appeared || deleted) {
return all;
}
// 出現している(かつ消えてない)ならその行を返す
}, []);
};
// 履歴連番に対応するテキストを得る
const getTextByTime = time => getHistoryByTime(time).map(history => textshistory.textIndex).join('\n'); // 操作系を表示してスライダーで操作できるようにする
// - 指定された履歴連番からテキストを得て表示する
const historyContainer = document.createElement('div');
historyContainer.classList.add('history-container');
Object.entries({
'background-color': '#555555',
position: 'fixed',
'z-index': '90000',
top: '5px',
left: '5px',
width: 'calc( 100% - 10px )',
'max-height': '100%',
overflow: 'scroll',
}).forEach((key, value) => historyContainer.style.setProperty(key, value)); document.querySelector('body').appendChild(historyContainer);
const historyCloseButton = document.createElement('button');
historyCloseButton.classList.add('history-close-button');
Object.entries({
'font-size': '30px',
'line-height': '1em',
padding: '0',
position: 'fixed',
'z-index': '90001',
top: '10px',
right: '10px',
width: '30px',
height: '30px',
}).forEach((key, value) => historyCloseButton.style.setProperty(key, value)); historyContainer.appendChild(historyCloseButton);
historyCloseButton.insertAdjacentText('beforeend', '×');
const historyBackground = document.createElement('div');
historyBackground.classList.add('history-background');
Object.entries({
position: 'fixed',
'z-index': '89999',
top: '0',
right: '0',
width: '100%',
height: '100%',
'background-color': 'rgba(255, 255, 255, 0.4)',
}).forEach((key, value) => historyBackground.style.setProperty(key, value)); document.querySelector('body').appendChild(historyBackground);
const historyPre = document.createElement('pre');
historyPre.classList.add('history-pre');
Object.entries({
width: '100%',
'max-height': '100%',
}).forEach((key, value) => historyPre.style.setProperty(key, value)); historyPre.insertAdjacentHTML('beforeend', getTextByTime(texts.length));
historyContainer.appendChild(historyPre);
const historySlider = document.createElement('input');
historySlider.classList.add('history-slider');
Object.entries({
position: 'fixed',
'z-index': '99999',
top: '10px',
width: '90%',
}).forEach((key, value) => historySlider.style.setProperty(key, value)); historySlider.type = 'range';
historySlider.min = '0';
historySlider.max = ${texts.length};
historySlider.value = ${texts.length};
historySlider.step = 1;
historyContainer.appendChild(historySlider);
const removeHistoryContainer = () => {
historyContainer.parentNode.removeChild(historyContainer);
historyBackground.parentNode.removeChild(historyBackground);
};
historyCloseButton.addEventListener('click', removeHistoryContainer);
historyBackground.addEventListener('click', removeHistoryContainer);
const updateHistory = e => {
const time = parseInt(e.target.value, 10);
historyPre.innerHTML = getTextByTime(time);
console.debug({ time, html: historyPre.innerHTML });
};
historySlider.addEventListener('input', updateHistory);
historySlider.addEventListener('change', updateHistory);
});
};
scrapbox.PageMenu.addMenu({
title: '履歴スライダー',
onClick: onHistorySliderClick,
});
code:history_slider.js
// 旧名(互換用)
import '/api/code/hogashi-pub/履歴スライダー/history_slider.js';