사용자:Hsl0/common.js
< 사용자:Hsl0
참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.
- 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
- 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
- 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
- 오페라: Ctrl-F5를 입력.
var fetchScript = fetch? function fetchScript(url, integrity) {
return fetch(url, {
header: {
Accept: [
'application/javascript',
'application/ecmascript',
'text/javascript',
'application/x-javascript',
'*/*'
]
},
integrity: integrity
}).then(function(res) {
return Promise.all([res.text(), res]);
}).then(function(text, res) {
new Function(text)();
return Promise.resolve(text, res.statusText, res);
});
} : $.getScript;
Promise.all([
mw.loader.using([
'mediawiki.api.options',
'mediawiki.notification',
'oojs-ui-core'
]),
fetchScript('https://cdnjs.cloudflare.com/ajax/libs/punycode/1.4.1/punycode.min.js', 'sha256-I5XOWZu6gbewMSB9UR88y0GmiJi9AsQcCzUpA/MBNnA=')
]).then(function() {
var title = mw.config.get('wgPageName').split('/')[0];
var noti;
/* option key 인코딩
퓨니코드 + url인코딩
% = _
_ = __
*/
function encode(key) {
return encodeURIComponent(punycode.toASCII(key))
.replace(/\./g, '%2E')
.replace(/!/g, '%21')
.replace(/~/g, '%7E')
.replace(/\*/g, '%2A')
.replace(/'/g, '%27')
.replace(/\(/g, '%28')
.replace(/\)/g, '%29')
.replace(/_/g, '__')
.replace(/%/g, '_');
}
function decode(key) {
return punycode.toUnicode(decodeURIComponent(key.replace(/_(?=[a-zA-Z0-9]{2})/g, '%').replace(/__/g, '_')));
}
// hybridStorage 서브셋 생성
window.setStoragePrefix = function(storage, prefix, except) {
var spp = new Proxy(storage, {
get: function(target, prop, receiver) {
function keys() {
return Object.keys(target).filter(function(key) {
return key.startsWith(prefix);
});
}
return Reflect.get(Object.assign({
length: (prop === 'length') && keys().length,
getItem: function getItem(key) {
return target.getItem(prefix + key);
},
key: function key(index) {
return keys()[index].slice(prefix.length);
},
removeItem: function removeItem(key) {
return target.removeItem(prefix + key).then(function() {
return spp;
});
},
setItem: function setItem(key, value) {
return target.setItem(prefix + key, value).then(function() {
return spp;
});
}
}, except), prop, receiver) || target.getItem(prefix + prop);
},
set: function(target, prop, value) {
return target.setItem(prefix + prop, value);
},
deleteProperty: function(target, prop) {
return target.removeItem(prefix + prop);
},
has: function(target, prop) {
Reflect.has(target, prefix + prop);
},
ownKeys: function(target) {
return Reflect.ownKeys(target).filter(function(key) {
return key.startsWith(prefix);
}).map(function(key) {
return key.slice(prefix.length);
});
}
});
return spp;
}
/*
anon = localStorage[*]
user = mw.user.options[userjs-*]
*/
window.hybridStorage = (function() {
var action;
var api = new mw.Api();
var storage, action;
var saveOption = (function() {
var options = {};
var deferred = new $.Deferred();
var timeout = null;
return function saveOption(key, value) {
if(timeout) clearTimeout(timeout);
options[key] = value;
timeout = setTimeout(function() {
deferred.resolve(api.saveOptions(options));
options = {};
timeout = null;
deferred = new $.Deferred();
}, 100);
return deferred.promise();
};
})();
if(mw.user.isAnon()) {
storage = new Proxy(localStorage, {
get: function(target, prop, receiver) {
var value = Reflect.get({
length: target.length,
getItem: target.getItem,
key: target.key,
removeItem: function removeItem(key) {
return Promise.resolve(target.removeItem(key));
},
setItem: function setItem(key, value) {
return Promise.resolve(target.setItem(key, value));
},
refresh: function refresh() {
return Promise.resolve(storage);
},
needRefresh: false,
[Symbol.toStringTag]: "hybridStorage"
}, prop, receiver) || target.getItem(prop);
return (value === null)? undefined : value;
},
set: function(target, prop, value, receiver) {
return Promise.resolve(target.setItem(prop, value));
},
deleteProperty: function(target, prop) {
return Promise.resolve(target.removeItem(prop));
}
});
} else {
action = {
removeItem: function removeItem(key) {
if(key) return saveOption("userjs-" + key, null).then(action.refresh);
else throw new TypeError("Failed to execute 'removeItem' on 'Storage': 1 argument required, but only 0 present.");
},
setItem: function setItem(key, value) {
if(key) return saveOption("userjs-" + key, value).then(action.refresh);
else throw new TypeError("Failed to execute 'removeItem' on 'Storage': 1 argument required, but only 0 present.");
},
refresh: (function() {
var deferred = new $.Deferred();
var timeout = null;
return function refresh() {
if(timeout) clearTimeout(timeout);
timeout = setTimeout(function() {
setTimeout(function() {
deferred.resolve(api.get({
action: 'query',
meta: 'userinfo',
uiprop: 'options'
}, {
cache: false
}).then(function(response) {
mw.user.options.values = response.query.userinfo.options;
return storage;
}));
timeout = null;
deferred = new $.Deferred();
}, 300);
}, 100);
return deferred.promise();
};
})()
};
storage = new Proxy(mw.user.options, {
get: function(target, prop, receiver) {
function keys() {
return Object.keys(target.values).filter(function(key) {
return key.startsWith('userjs-');
});
}
return Reflect.get({
length: prop === 'length' && keys().length,
getItem: function getItem(key) {
if(key) return target.get("userjs-" + encode(key));
else throw new TypeError("Failed to execute 'getItem' on 'Storage': 1 argument required, but only 0 present.");
},
key: function key(index) {
return decode(keys()[index].slice(7));
},
removeItem: function removeItem(key) {
key = encode(key);
Reflect.deleteProperty(target.values, key);
return action.removeItem(key);
},
setItem: function setItem(key, value) {
key = encode(key);
target.set(key, value);
return action.setItem(key, value);
},
refresh: action.refresh,
needRefresh: true,
[Symbol.toStringTag]: "hybridStorage"
}, prop, receiver) || Reflect.get(target.values, "userjs-" + encode(prop), receiver);
},
set: function(target, prop, value, receiver) {
prop = encode(prop);
Reflect.set(target.values, prop, value, receiver);
return action.setItem(encode(prop, value));
},
deleteProperty: function(target, prop) {
prop = encode(prop);
Reflect.deleteProperty(target.values, prop);
return action.removeItem(prop);
},
has: function(target, prop) {
return Reflect.has(target.values, "userjs-" + encode(prop));
},
ownKeys: function(target) {
return Reflect.ownKeys(target.values).filter(function(key) {
return key.startsWith("userjs-");
}).map(function(key) {
return decode(key.slice(7));
});
}
});
}
return storage;
})();
// local + global 슈퍼셋 (hybridStorage[gamedb-*])
var rootGameDB = setStoragePrefix(hybridStorage, 'gamedb-', {
refresh: function() {
return hybridStorage.refresh().then(function() {
return rootGameDB;
});
},
needRefresh: hybridStorage.needRefresh,
[Symbol.toStringTag]: 'gameDB'
});
// 게임별로 할당되는 영역 (rootGameDB[{게임}/*])
window.localGameDB = setStoragePrefix(rootGameDB, mw.config.get('wgPageName').split('/')[0] + '/', {
refresh: function() {
return rootGameDB.refresh().then(function() {
return localGameDB;
});
},
needRefresh: rootGameDB.needRefresh,
});
// 모든 게임이 공유하는 영역 (rootGameDB[#*])
window.globalGameDB = setStoragePrefix(rootGameDB, '#', {
refresh: function() {
return rootGameDB.refresh().then(function() {
return globalGameDB;
});
},
needRefresh: rootGameDB.needRefresh,
});
function DataChange() {
this.params = geturlSearch();
this.local = {};
this.global = {};
this.root = {};
}
DataChange.prototype.refresh = false;
function control(element, change) {
var base, key, storage, params;
var data = element.dataset;
/* 저장할 키
local: 키 지정
global: 전역 키 지정
local global: 키 지정
(없음): 기본 키
*/
if('local' in data) {
storage = localGameDB;
base = change.local;
key = data.local;
} else if('global' in data) {
storage = globalGameDB;
base = change.global;
key = data.global;
} else {
storage = rootGameDB;
base = change.root;
key = title;
}
switch(data.action) {
// 호환
case '저장':
case 'save':
/*
reset: DB 덮어쓰기
true: 기존 데이터를 지우고 현재 상태를 그대로 저장
false: 현재 상태를 저장하되, 없는 키는 존치
*/
if(!location.search) base[key] = JSON.stringify('reset' in data? geturlSearch() : Object.assign(JSON.parse(storage.getItem(key)), geturlSearch()));
break;
case '로드':
case 'load':
/*
live: 항상 최신 데이터 사용
true: 링크 누를때 동기화
false: 항상 캐시된 데이터 사용
reset: 파라미터 강제 덮어쓰기
ture: 파라미터가 있어도 강제로 불러와 덮어쓰기
false: 파라미터가 있으면 아무 동작도 하지 않음
*/
if('live' in data) change.refresh = true;
if(!location.search || 'reset' in data) Object.assign(change.params, JSON.parse(storage.getItem(key)));
break;
// 기본
case '호출':
case 'load':
/*
live: 항상 최신 데이터 사용
true: 링크 누를때 동기화
false: 항상 캐시된 데이터 사용
*/
if('live' in data) change.refresh = true;
change.params[data.arg] = storage.getItem(key);
break;
case '수정':
case 'set':
base[key] = data.arg;
break;
case '삭제':
case 'del':
delete base[key];
break;
// JSON
case 'JSON':
case 'json':
/*
live: 항상 최신 데이터 사용
true: 링크 누를때 동기화
false: 항상 캐시된 데이터 사용
reset: 파라미터 덮어쓰기
true: 파라미터를 초기화하고 새로 가져온 값으로 덮어쓰기
false: 변동된 파라미터만 수정하기
*/
if('live' in data) change.refresh = true;
if('reset' in data) change.params = {};
change.data[key] = JSON.stringify(new CGI2Parser({
get: function(args) {
if(typeof args === 'object') Object.pairs(args).forEach(function(pair) {
params[pair[0]] = this[pair[1]];
});
else Array.from(arguments).forEach(function(key) {
params[key] = this[key];
});
},
set: function(args) {
Object.pairs(args).forEach(function(pair) {
this[pair[0]] = pair[1];
});
},
del: function() {
Array.from(arguments).forEach(function(key) {
delete this[key];
});
},
def: function(args) {
Object.pairs(args).forEach(function(pair) {
if(!(pair[0] in params)) params[pair[0]] = this[pair[1]];
});
},
sav: function(args) {
if(typeof args === 'object') Object.pairs(args).forEach(function(pair) {
this[pair[0]] = params[pair[1]];
});
else Array.from(arguments).forEach(function(key) {
this[key] = params[key];
});
}
}).parse(JSON.parse(base.getItem(key)), data.arg));
Object.assign(change.params, params);
break;
}
}
// TODO: .gameDB-control, .gameDB-container, .gameDB-link click 이벤트 리스너 구현
/**
* DB 저장
* @function
* @param {DataChange} change
*/
function save(change) {
var key;
var promises = [];
if(title in change.default) {
promises.push(rootGameDB.setItem(key, change.default[title]));
}
for(key in change.local) {
promises.push(localGameDB.setItem(key, change.local[key]));
}
for(key in change.global) {
promises.push(globalGameDB.setItem(key, change.global[key]));
}
if(promises.length && !mw.user.isAnon()) noti = mw.notification.notify('데이터를 저장하는 중입니다...', {
autoHide: false,
tag: 'gameDB',
type: 'pending'
});
return new Promise.all(promises);
}
function handleError(code, object) {
mw.notification.notify(
code === 'http'?
$('<span />')
.append($('<p />', {class: 'gameDB-noti-errmsg'}).text(object.xhr.responseText))
.append($('<code />', {class: 'gameDB-noti-errcode'}).text('HTTP ' + object.xhr.status)) :
$('<span />')
.append($('<p />', {class: 'gameDB-noti-errmsg'}).text(object.error.info))
.append($('<code />', {class: 'gameDB-noti-errcode'}).text(code)),
{
title: '데이터 저장에 실패하였습니다.',
type: 'error',
tag: 'gameDB'
}
);
}
// 즉시
(function() {
var instant = new DataChange();
$('.gameDB-control').each(function() {
control(this, instant);
});
save(instant).catch(handleError).then(function() {
instant.params = searchParamsToString(instant.params);
if(instant.params.length > 1) Promise.all(promises).then(function() {
location.search = instant.params;
});
else noti.close();
});
})();
// 링크
(function() {
function process(link, controllers, change) {
var href = new URL(link.href);
controllers.get().forEach(function(controller) {
control(controller, change);
});
href.search = searchParamsToString(change.params);
link.href = href;
}
$('.gameDB-container').not(':has(a, .gameDB-container)').each(function() {
$(this).html($('<a />').text(this.innerText));
});
$('.gameDB-container a').each(function(link) {
var change = new DataChange();
var controllers = $(this).parents().filter('.gameDB-container');
if(!controllers.filter('[data-live]').length) process(this, controllers, change);
$(this).data('change', change);
$(this).addClass('gameDB-link');
});
$('.gameDB-link').click(function(event) {
var link = this;
var change = $(this).data('change');
var promise;
if(!('done' in this.dataset)) event.preventDefault();
if('live' in this.dataset) {
mw.notification.notify('데이터를 가져오는 중입니다...', {
tag: 'gameDB',
type: 'pending'
});
promise = hybridStorage.refresh().then(function() {
process(link, $(link).parents().filter('.gameDB-container'), change);
return save(change);
});
} else promise = save(change);
promise.catch(handleError).then(function() {
link.dataset.done = '';
if(link.href) $(link).click();
else mw.notification.notify('데이터가 저장되었습니다.', {
tag: 'gameDB'
});
});
});
})();
});