User:ネイ/NotifyWatchlistUpdate.js
外观
< User:ネイ
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
//<syntaxhighlight lang=js>
/*
[[User:Waiesu/NotifyWatchlistUpdate.js]]
開発者: [[User:Waiesu]]
導入方法:
[[Special:MyPage/common.js]]または[[Special:MyPage/vector.js]]に
importScript('User:Waiesu/NotifyWatchlistUpdate.js');
を追加してください
*/
(function ($, mw, OO) {
'use strict';
/* スタイル指定 */
var style = '<style>[role="button"]{cursor:pointer;}#pt-watchlist,#pt-watchlist li{white-space:normal;}#pt-watchlist .hidden{display:none;}#ptwl-container{padding-left:.25em;white-space:normal;}#pt-watchlist #ptwl-smallwl{font-size:120%;padding:0 .5em 0 0;text-align:right;}#pt-watchlist #ptwl-smallwl li{display:list-item;float:none;margin:0;}#pt-watchlist #ptwl-smallwl dl{display:inline-block;margin-bottom:0;}#pt-watchlist #ptwl-smallwl dt{font-weight:normal;}#pt-watchlist #ptwl-smallwl dd{border:1px solid #aaa;margin:0;padding:.25em;}#ptwl-container #ptwl-setting-title{font-size:120%;line-height:1.6;}#ptwl-container #ptwl-setting-title:after{content:":ウォッチリストの絞り込み";}#ptwl-container #ptwl-setting-title.setting-style:after{content:":スタイル";}#ptwl-container .oo-ui-radioOptionWidget{display:inline-block;padding-right:.5em;}#ptwl-container .oo-ui-buttonGroupWidget{width:50%;margin:.25em 0;}#ptwl-container .oo-ui-checkboxMultiselectWidget{margin-top:.5em;}#ptwl-container .oo-ui-checkboxMultioptionWidget{display:inline-block;padding:0;}#ptwl-container .oo-ui-fieldLayout-header,#ptwl-container .oo-ui-checkboxMultioptionWidget:nth-child(2n+1){margin:0;width:9em;}#ptwl-container .oo-ui-fieldLayout-field,#ptwl-container .oo-ui-checkboxMultioptionWidget:nth-child(2n){margin:0;width:13em;}</style>';
document.head.insertAdjacentHTML('beforeend', style);
var $body = $(document.body);
var $ptwl = $('#pt-watchlist');
if (!$ptwl[0]) return false; // ウォッチリストがないとき
var $container, $numdisplay; // コンテナ, 更新数ディスプレイ
var ooPopupWatchlist, ooPopupSettings; // ウォッチリストポップアップ, 設定ポップアップ
var $ul = $('<ul id=ptwl-smallwl></ul>'); //ウォッチリスト本体
/* 変数 */
// 名前空間 - ウィキごとに要設定
var namespaces = {
0: '(標準)',
1: '討論',
2: '用戶',
3: '用戶討論',
4: '計劃',
5: '計劃討論',
6: '檔案',
7: '檔案討論',
8: 'MediaWiki',
9: 'MediaWiki討論',
10: '模板',
11: '模板討論',
12: '幫助',
13: '幫助討論',
14: '分類',
15: '分類討論',
100: '主題',
101: '主題討論',
118: '草稿',
119: '草稿討論',
828: '模組',
829: '模組討論'
};
// 設定(キャッシュ)
var settings, styles;
var storage = JSON.parse(mw.storage.get('userjsNotifyWatchlistUpdate') || '{}');
if (storage.version === 2) {
settings = storage.data;
styles = storage.style;
} else {
mw.storage.remove('jawpDisplayWatchlistUpdate'); // 旧バージョンのキャッシュを消す
settings = {
wlallrev: 0,
wldir: 'older',
wltype: ['edit'],
wlshow: ['unread'],
wlnamespace: Object.keys(namespaces),
wlexcludeuser: '',
wllimit: 0, // 0 -> max
wldays: 0 // custom
};
styles = {
displayBgcolor: 'inherit',
displayBorderColor: '#8b0000',
displayColor: '#8b0000',
};
}
// トークン保存用
var tokens;
/* すべて訪問 */
function visitAll() {
$body.addClass('waiting-api');
var query = {
action: 'setnotificationtimestamp',
entirewatchlist: 1,
timestamp: new Date().toISOString(), // 現在のタイムスタンプ
token: tokens.csrftoken,
};
new mw.Api().post(query).then(function () {
$body.removeClass('waiting-api');
mw.notify('すべての更新を訪問済みにしました');
$ul.empty();
$numdisplay.text(0);
// ポップアップを無効にする
$ptwl.children().eq(0).off('mouseenter.notifywl');
ooPopupWatchlist.toggle(false);
});
return false;
}
/* 訪問 */
function visit(e) {
$body.addClass('waiting-api');
var li = document.getElementById(e.originalEvent.dataTransfer.getData('text')); // ドロップされたli
var title = li.dataset.title;
var epoch = Number(li.dataset.epoch) + 1000; // ページ更新日時+1秒
var timestamp = new Date(epoch).toISOString().replace('.000Z', 'Z');
var query = {
action: 'setnotificationtimestamp',
titles: [title],
timestamp: timestamp,
token: tokens.csrftoken,
};
new mw.Api().post(query).then(function () {
$body.removeClass('waiting-api');
mw.notify('「' + title + '」を' + timestamp +'に訪問済みにしました');
// ドロップされた更新より古い同一ページをリストから消す
var $older = $ul.children('[data-title="' + title + '"]').filter(function () {
return this.dataset.epoch < epoch;
}).remove();
$numdisplay.text($numdisplay.text() - $older.length);
});
return false;
}
/* ウォッチを外す */
function unwatch(e) {
$body.addClass('waiting-api');
var li = document.getElementById(e.originalEvent.dataTransfer.getData('text')); // ドロップされたli
var title = li.dataset.title;
var query = {
action: 'watch',
titles: [title],
token: tokens.watchtoken,
unwatch: 1,
};
new mw.Api().post(query).then(function () {
$body.removeClass('waiting-api');
mw.notify('「' + title + '」をウォッチリストから外しました');
// 同一ページをリストから消す
var $same = $ul.children('[data-title="' + title + '"]').remove();
$numdisplay.text($numdisplay.text() - $same.length);
});
return false;
}
/*
セットアップ
*/
function setup() {
var inputs = {}; // OO.ui.Widgetを格納
/*
フィルター設定フィールドセット
*/
// 最新の版のみ(スイッチ)
inputs.allrev = new OO.ui.ToggleSwitchWidget({
value: !settings.wlallrev
}).on('change', function (value) {
settings.wlallrev = Number(!value);
});
// 並び順(ラジオボタン)
inputs.dir = new OO.ui.RadioSelectInputWidget({
options: [
{ data: 'older', label: '新しい順' },
{ data: 'newer', label: '古い順' }
]
}).setValue(settings.wldir).on('change', function (value) {
settings.wldir = value;
});
// 編集の種類(チェックボックス)
var type = {
widget: OO.ui.CheckboxMultioptionWidget,
label: {
edit: '通常の編集',
'new': 'ページの作成',
log: '各種記録',
categorize: 'カテゴリの変更',
external: 'ウィキデータ'
}
};
inputs.type = new OO.ui.CheckboxMultiselectWidget({
items: (function () {
var _tmp = [];
for (var key in type.label) {
_tmp.push(new type.widget({
data: key,
label: type.label[key],
selected: ~settings.wltype.indexOf(key)
}));
}
return _tmp;
})()
}).on('change', function () {
settings.wltype = inputs.type.getSelectedItemsData();
});
// 編集のフラグ(トグルボタン)
var show = {
widget: OO.ui.ToggleButtonWidget,
label: {
'!minor': '細部でない',
minor: '細部',
'!bot': 'bot以外',
bot: 'bot',
'!anon': '登録利用者',
anon: 'IP',
'!patrolled': '未巡回',
patrolled: '巡回済み',
'!unread': '訪問済み',
unread: '未訪問',
},
onchange: function (flag) {
var _this = inputs.show[flag];
if (!_this) {
} else if (_this.getValue()) {
// すでに一方のボタンが選択されているときに両方選択されないようにする
var revflag = flag.charAt(0) === '!' ? flag.substring(1) : '!' + flag;
var indexR = settings.wlshow.indexOf(revflag);
if (~indexR) inputs.show[revflag].setValue(false);
settings.wlshow.push(flag);
} else {
var index = settings.wlshow.indexOf(flag);
if (~index) settings.wlshow.splice(index, 1);
}
return false;
}
};
inputs.show = {};
for (var flag in show.label) {
inputs.show[flag] = new show.widget({
label: show.label[flag],
value: ~settings.wlshow.indexOf(flag)
}).on('change', show.onchange, [flag]);
}
// 名前空間(チェックボックス)
var namespace = {
widget: OO.ui.CheckboxMultioptionWidget,
label: namespaces
};
inputs.namespace = new OO.ui.CheckboxMultiselectWidget({
items: (function () {
var _tmp = [];
for (var id in namespace.label) {
_tmp.push(new namespace.widget({
data: Number(id),
label: namespace.label[id],
selected: ~settings.wlnamespace.indexOf(id)
}));
}
return _tmp;
})()
}).on('change', function () {
settings.wlnamespace = inputs.namespace.getSelectedItemsData();
});
// 取得日数(テキストボックス:数字)
inputs.days = new OO.ui.NumberInputWidget({
data: 'watchlistdays',
min: 0,
title: '0を指定すると全期間表示します',
value: settings.wldays
}).on('change', function (value) {
settings.wldays = Number(value);
});
// 取得日数(テキストボックス:利用者名)
inputs.excludeuser = new mw.widgets.UserInputWidget({
data: 'wlexcludeuser',
value: settings.wlexcludeuser
}).on('change', function (value) {
settings.wlexcludeuser = value;
});
// 最大取得数(テキストボックス:数字)
inputs.limit = new OO.ui.NumberInputWidget({
data: 'wllimit',
max: 5000,
min: 0,
title: '0を指定すると最大数取得します',
required: true,
value: settings.wllimit
}).on('change', function (value) {
settings.wllimit = Number(value);
});
// 名前空間選択ボタン用
function toggleNamespace(bool) {
var namespaces = inputs.namespace.getItems();
var func = (typeof bool === 'boolean') ? function () { return bool; } : function (i) { return !namespaces[i].isSelected(); };
for (var i = 0, len = namespaces.length; i < len; i++) {
namespaces[i].setSelected(func(i));
}
}
// 生成した要素をフィールドセットに
var ooFieldsetFilter = new OO.ui.FieldsetLayout({
items: [
new OO.ui.FieldsetLayout({
items: [
new OO.ui.FieldLayout(inputs.allrev, { label: '最新の版のみ' }),
new OO.ui.FieldLayout(inputs.dir, { label: '並び順' })
]
}),
new OO.ui.FieldsetLayout({
label: '編集の種類',
items: [inputs.type]
}),
new OO.ui.FieldsetLayout({
label: '編集のフラグ',
items: [
new OO.ui.ButtonGroupWidget({
items: [inputs.show['!minor'], inputs.show.minor]
}),
new OO.ui.ButtonGroupWidget({
items: [inputs.show['!bot'], inputs.show.bot]
}),
new OO.ui.ButtonGroupWidget({
items: [inputs.show['!anon'], inputs.show.anon]
}),
new OO.ui.ButtonGroupWidget({
items: [inputs.show['!patrolled'], inputs.show.patrolled]
}),
new OO.ui.ButtonGroupWidget({
items: [inputs.show.unread, inputs.show['!unread']]
})
],
text: '複数選択の場合は論理積になります'
}),
new OO.ui.FieldsetLayout({
label: '名前空間',
items: [
// ボタン
new OO.ui.ButtonInputWidget({
label: '全選択'
}).on('click', toggleNamespace, [true]),
new OO.ui.ButtonInputWidget({
label: '全解除'
}).on('click', toggleNamespace, [false]),
new OO.ui.ButtonInputWidget({
label: '選択反転'
}).on('click', toggleNamespace, []),
inputs.namespace
]
}),
new OO.ui.FieldsetLayout({
items: [
new OO.ui.FieldLayout(inputs.days, { label: '取得日数' }),
new OO.ui.FieldLayout(inputs.excludeuser, { label: '表示しない利用者' }),
new OO.ui.FieldLayout(inputs.limit, { label: '最大取得数' }),
]
})
]
});
/*
スタイル設定フィールドセット
*/
// 背景色(テキストボックス)
inputs.displayBgcolor = new OO.ui.TextInputWidget({
placeholder: 'CSS形式で入力',
value: styles.displayBgcolor
}).on('change', function (value) {
styles.displayBgcolor = value;
$numdisplay.css({ backgroundColor: value }); // 変更されたらリアルタイムで反映
});
// ボーダー色(テキストボックス)
inputs.displayBorderColor = new OO.ui.TextInputWidget({
placeholder: 'CSS形式で入力',
value: styles.displayBorderColor
}).on('change', function (value) {
styles.displayBorderColor = value;
$numdisplay.css({ borderColor: value }); // 変更されたらリアルタイムで反映
});
// 文字色(テキストボックス)
inputs.displayColor = new OO.ui.TextInputWidget({
placeholder: 'CSS形式で入力',
value: styles.displayColor
}).on('change', function (value) {
styles.displayColor = value;
$numdisplay.css({ color: value }); // 変更されたらリアルタイムで反映
});
// 生成した要素をフィールドセットに
var ooFieldsetStyle = new OO.ui.FieldsetLayout({
items: [
new OO.ui.FieldsetLayout({
label: '更新数ディスプレイ',
items: [
new OO.ui.FieldLayout(inputs.displayBgcolor, { label: '背景色' }),
new OO.ui.FieldLayout(inputs.displayBorderColor, { label: 'ボーダー色' }),
new OO.ui.FieldLayout(inputs.displayColor, { label: '文字色' }),
]
})
]
}).toggle(false);
/*
フォーム
*/
var ooForm = new OO.ui.FormLayout({
items: [
ooFieldsetFilter,
ooFieldsetStyle,
new OO.ui.FieldsetLayout({
items: [
// 各種ボタン
new OO.ui.ButtonInputWidget({
flags: ['progressive', 'primary'],
label: '保存',
type: 'submit'
}),
new OO.ui.ButtonInputWidget({
label: '初期化'
}).on('click', function () {
// 初期化ボタンクリック時
OO.ui.confirm('設定を初期化しますか?').then(function (confirmed) {
if (confirmed) {
mw.storage.remove('userjsNotifyWatchlistUpdate');
return OO.ui.confirm('設定を初期化しました。変更を反映するためにページを更新しますか?');
}
}).then(function (confirmed) {
if (confirmed) location.reload();
});
})
]
})
]
}).on('submit', function () {
// 保存ボタンクリック時
OO.ui.confirm('設定を保存しますか?').then(function (confirmed) {
if (confirmed) {
mw.storage.set('userjsNotifyWatchlistUpdate', JSON.stringify({
version: 2,
data: settings,
style: styles
}));
// ウォッチリストを再生成
getwl().then(makewl).then(function () {
mw.notify('設定を保存しました');
});
}
});
return false; // ページ遷移を防ぐ
});
/*
設定ポップアップ
*/
ooPopupSettings = new OO.ui.PopupWidget({
$content: ooForm.$element.css('overflowX', 'hidden'), //横に伸びるのを防ぐ
align: 'forwards',
autoClose: true,
head: true,
hideWhenOutOfView: false,
label: $('<b id=ptwl-setting-title role=button title="設定切り替え">設定</b>').on('click', function () {
// クリックでフィールドセット切り替え
$(this).toggleClass('setting-style');
ooFieldsetFilter.toggle();
ooFieldsetStyle.toggle();
return false;
}),
padded: true
}).toggle(false);
/*
更新数ディスプレイ
*/
$numdisplay = $('<b id=ptwl-num role=button title="クリックで設定を表示します">...</b>').css({
backgroundColor: styles.displayBgcolor,
border: '1px solid',
borderColor: styles.displayBorderColor,
borderRadius: '.5em',
color: styles.displayColor,
padding: '0 .25em',
}).on('click', function () {
// クリックで設定ポップアップを表示
ooPopupSettings.toggle();
return false;
});
// コンテナに更新数ディスプレイと設定ポップアップを追加
$container = $('<span id=ptwl-container></span>').append($numdisplay, ooPopupSettings.$element);
/*
ウォッチリストポップアップ
*/
var ooPopupWatchlistHead = $(document.createDocumentFragment());
var ooButtonAllVisit = new OO.ui.ButtonInputWidget({
icon: 'doubleCheck',
label: 'すべて訪問済みにする'
});
var ooIconVisit = new OO.ui.IconWidget( {
icon: 'check',
iconTitle: 'ここにドラッグ&ドロップすると訪問済みにします',
disabled: true,
flags: ['progressive']
});
var ooIconUnwatch = new OO.ui.IconWidget( {
icon: 'halfStar',
iconTitle: 'ここにドラッグ&ドロップするとウォッチリストから外します',
disabled: true,
flags: ['progressive']
});
ooPopupWatchlist = new OO.ui.PopupWidget({
$content: $ul,
align: 'backwards',
autoClose: true,
autoFlip: false,
head: true,
hideWhenOutOfView: false,
label: ooPopupWatchlistHead.append(
ooButtonAllVisit.$element
.on('click.notifywl', visitAll),
ooIconVisit.$element
.on('dragenter.notifywl', function () { ooIconVisit.setDisabled(false); })
.on('dragleave.notifywl drop.notifywl', function () { ooIconVisit.setDisabled(true); })
.on('dragover.notifywl', false)
.on('drop.notifywl', visit),
ooIconUnwatch.$element
.on('dragenter.notifywl', function () { ooIconUnwatch.setDisabled(false); })
.on('dragleave.notifywl drop.notifywl', function () { ooIconUnwatch.setDisabled(true); })
.on('dragover.notifywl', false)
.on('drop.notifywl', unwatch)
),
padded: true
}).toggle(false);
$ptwl.append(ooPopupWatchlist.$element, $container);
}
/* セットアップここまで */
/*
ウォッチリスト取得
*/
function getwl() {
$body.addClass('waiting-api');
var query = {
formatversion: 2,
// Get tokens
meta: 'tokens',
type: ['csrf', 'watch'],
// Get watchlist
action: 'query',
list: 'watchlist',
wldir: settings.wldir,
wltype: settings.wltype,
wlshow: settings.wlshow,
wlnamespace: settings.wlnamespace,
wllimit: settings.wllimit || 'max', // 0 -> max
wlprop: ['ids', 'title', 'flags', 'user', 'parsedcomment', 'timestamp', 'sizes', 'loginfo']
};
// セットされているときのみ
if (settings.wlallrev) query.wlallrev = 1;
if (settings.wldays) query.wlstart = new Date(Date.now() - settings.wldays * 86400000).toISOString(); // 24 * 3600 * 1000
if (settings.excludeuser) query.excludeuser = settings.excludeuser;
// thenにつなげるために$.ajaxを返す
return new mw.Api({
ajax: { headers: { 'Api-User-Agent': 'Example/1.0' } } // これで最大取得数が増えるらしい
}).post(query);
}
/*
ウォッチリスト生成
*/
function makewl(data) {
$body.removeClass('waiting-api');
tokens = data.query && data.query.tokens;
var list = data.query && data.query.watchlist;
var listlen = list && list.length;
// 更新数ディスプレイにリストの長さを反映
$numdisplay.text(listlen);
var $a = $ptwl.children().eq(0);
if (listlen) {
// リンクにカーソルをのせるとウォッチリストポップアップを表示
$a.on('mouseenter.notifywl', function () {
ooPopupWatchlist.toggle(true);
});
} else {
// リストがないときウォッチリストポップアップを非表示
$a.off('mouseenter.notifywl');
// トークン・リストを取得できないときはrejectを返す
return new $.Deferred()[tokens && list ? 'resolve' : 'reject']().promise();
}
/* フラグ情報を返す */
function switchFlag(item) {
var flag = '';
if (item.minor) flag += '<abbr class=minoredit title="細部の編集">m</abbr>';
if (item.bot) flag += '<abbr class=botedit title="ボットによる編集">b</abbr>';
if (item['new']) flag += '<abbr class=newpage title="新規作成">N</abbr>';
switch (item.type) {
case 'log':
switch (item.logtype) {
case 'block':
if (item.logaction === 'unblock') {
flag += '<abbr class=minoredit title="投稿ブロック解除">解</abbr>';
} else {
flag += '<abbr class=minoredit title="投稿ブロック">ブ</abbr>';
}
break;
case 'delete':
switch (item.logaction) {
case 'restore': flag += '<abbr class=minoredit title="復帰">復</abbr>'; break;
case 'revision': flag += '<abbr class=minoredit title="版指定削除">版削</abbr>'; break;
default: flag += '<abbr class=minoredit title="削除">削</abbr>';
}
break;
case 'move': flag += '<abbr class=minoredit title="「' + item.logparams.target_title + '」へ移動">移</abbr>'; break;
case 'protect':
if (item.logaction === 'move_prot') {
flag += '<abbr class=minoredit title="移動保護">移保</abbr>';
} else if (item.logparams.details[0].level === 'autoconfirmed') {
flag += '<abbr class=minoredit title="半保護">半保</abbr>';
} else {
flag += '<abbr class=minoredit title="保護">保</abbr>';
}
break;
}
break;
case 'external': flag += '<abbr class=minoredit title="ウィキデータ">D</abbr>'; break;
case 'categorize': flag += '<abbr class=minoredit title="カテゴライズ">C</abbr>'; break;
}
return flag;
}
/* 更新日時を計算して返す */
var now = Date.now();
function calcTime(timestamp, now) {
var start = new Date(timestamp).getTime();
var min = (now - start) / 60000,
hour = min / 60,
day = hour / 24,
month = day / 30;
var rslt;
if (month > 1) {
rslt = Math.round(month) + 'か月';
} else if (day > 1) {
rslt = Math.round(day) + '日';
} else if (hour > 1) {
rslt = Math.round(hour) + '時間';
} else {
rslt = Math.round(min) + '分';
}
return '<span title="' + timestamp + '">' + rslt + '前</span>';
}
/* 変更のサイズを計算して返す */
function calcDiff(item) {
var diffsize = item.newlen - (item.oldlen || 0);
var abssize = Math.abs(diffsize);
var sign = (diffsize > 0) && '+' || (diffsize < 0) && '-' || '';
var color = (diffsize > 0) && '#006400' || (diffsize < 0) && '#8b0000' || '#a2a9b1';
var weight = (abssize > 500) && 'bold' || 'normal';
return '<a href="/w/index.php?diff=' + item.revid + '" title="変更後のサイズは' + item.newlen + 'バイト" style="color:' + color + ';font-weight:' + weight + ';">(' + sign + abssize + ')</a>';
}
var flagment = document.createDocumentFragment();
// テンプレートli
var li = document.createElement('li');
li.setAttribute('ondragstart', 'event.dataTransfer.setData("text",this.id)'); // cloneNodeでイベントをコピーするため
li.draggable = true; // ドラッグ可能にする
for (var i = 0; i < listlen; i++) {
// テンプレートliを複製
var _li = li.cloneNode();
_li.id = 'ptwl' + list[i].revid; //ドロップ時に検索するためidを指定
_li.dataset.title = list[i].title;
_li.dataset.epoch = new Date(list[i].timestamp).getTime();
var pagename = '<a href="/wiki/' + encodeURIComponent(list[i].title) + '" draggable=false>' + list[i].title + '</a>';
var user = '<a href="/wiki/User:' + encodeURIComponent(list[i].user) + '">' + list[i].user + '</a>';
// dtを右クリックでddを表示
var dt = '<dt oncontextmenu="this.nextElementSibling.classList.toggle(\'hidden\');return false;">' + switchFlag(list[i]) + ' ' + pagename + '</dt>';
var dd = '<dd class=hidden>' + calcTime(list[i].timestamp, now) + ' ' + user + ' ' + calcDiff(list[i]) + '<br>' + list[i].parsedcomment + '</dd>';
_li.insertAdjacentHTML('beforeend', '<dl>' + dt + dd + '</dl>');
flagment.appendChild(_li);
}
// ウォッチリスト本体に上書き
$ul.html(flagment);
}
mw.loader.using([
'mediawiki.api',
'mediawiki.widgets.UserInputWidget',
'oojs-ui'
]).then(setup).then(getwl).then(makewl).fail(function (jqXHR, status) {
$body.removeClass('waiting-api');
if (!status || status !== 'abort') mw.notify('ウォッチリストの読み込みに失敗しました');
$numdisplay.text('x');
});
})(jQuery, mediaWiki, OO);
//</syntaxhighlight>