Widget:CollectionTrackerJS
Jump to navigation
Jump to search
<script type="application/javascript">
// Widget:CollectionTrackerJS
;(function() {
'use strict';
if (document.querySelectorAll('.tracker-wrap').length < 1)
return;
const fixedHash = '';
let lastHash = '';
let knownObtains = [];
const addEventForChild = function(parent, eventName, childSelector, callback){
parent.addEventListener(eventName, function(event){
const clickedElement = event.target;
const matchingChild = clickedElement.closest(childSelector);
if (matchingChild)
callback(event, matchingChild);
});
};
const isNumeric = function(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};
const toggleVisible = function(node, visible) {
node.style.display = (visible ? '' : 'none');
};
/**
* @return {string}
*/
const Uint8ArrayToBase64 = function(bytes) {
let binary = '';
let len = bytes.byteLength;
for (let i = 0; i < len; i++)
binary += String.fromCharCode(bytes[i]);
let result = window.btoa(binary);
result = result.replace('=', ''); // Remove any trailing '='s
result = result.replace('+', '-'); // 62nd char of encoding
result = result.replace('/', '_'); // 63rd char of encoding
if (result === 'AAAA')
return '';
return result;
};
const Base64ToUint8Array = function(text) {
let s = text.replace('_', '/'); // 63rd char of encoding
s = s.replace('-', '+'); // 62nd char of encoding
let pl = (s.length % 4);
if (pl > 0)
s += '='.repeat(4-pl); // Restore any trailing '='s
let binary = window.atob(s);
let len = binary.length;
let result = new Uint8Array(len);
for (let i = 0; i < len; i++)
result[i] = binary.charCodeAt(i);
return result;
};
const replaceHash = function(newHash) {
lastHash = newHash;
if ((''+newHash).charAt(0) !== '#')
newHash = '#' + newHash;
history.replaceState('', '', newHash);
};
const readHash = function() {
// format: Options.SSR Characters.SR Characters.R Characters.SSR Summons.SR Summons.R Summons
// - Options is a base64 encoded json object
// - Character/Summons are base64 encoded sets of bits, where each character is 3 bits offset by the counter in its ID
lastHash = window.location.hash;
let hash = (fixedHash) ? fixedHash : window.location.hash.substr(1);
// Backward compatibility with URL using semicolon (;) as separator
const separator = (hash.indexOf(";") > 0) ? ";" : ".";
let parts = hash.split(separator);
for (let i = 1; i <= 7; i++)
if (typeof parts[i] !== 'string')
parts[i] = '';
let strings = {
'c': {2: parts[3], 3: parts[2], 4: parts[1]},
's': {2: parts[6], 3: parts[5], 4: parts[4]},
};
// Reset current options
document.querySelectorAll('.tracker-filter-options input[type="checkbox"]').forEach(function(checkbox) { checkbox.checked = false; });
document.querySelectorAll('.tracker-filter-group label.mw-ui-progressive').forEach(function(label) { label.classList.remove('mw-ui-progressive'); });
document.querySelectorAll('.tracker-filter-group label[data-value="*"]').forEach(function(label) { label.classList.add('mw-ui-progressive'); });
// load options
let reOptions = /[g-z][0-9a-f]{1,8}/ig;
let options = parts[0].match(reOptions);
if (options != null) {
for (let i = 0; i < options.length; i++) {
let text = options[i];
let bits = parseInt(text.substr(1), 16);
let optionSelector = 'div[data-option="'+text.charAt(0)+'"]';
let option = document.querySelector(optionSelector);
if (option == null)
continue;
let optionBits = option.querySelectorAll('[data-bit]');
if (optionBits.length <= 0)
continue;
option.querySelectorAll('label').forEach(function(label) {
label.classList.remove('mw-ui-progressive');
});
optionBits.forEach(function(optionBit) {
let selected = ((1 << optionBit.dataset.bit) & bits) > 0;
if (optionBit.tagName.toLowerCase() === 'input') {
optionBit.checked = selected;
} else if (selected) {
optionBit.classList.add('mw-ui-progressive');
}
});
// verify we actually selected something
if (option.querySelectorAll('.mw-ui-progressive').length > 0)
continue;
// we didn't so select All label
let all = option.querySelector('label[data-value="*"]');
if (all != null)
all.classList.add('mw-ui-progressive');
}
}
// reset items
document.querySelectorAll('.tracker-item').forEach(function(item) {
item.dataset.evo = '0';
item.dataset.owned = 'false';
evolve(item, false);
});
// load items
['c', 's'].forEach(function(type) {
[2, 3, 4].forEach(function(rarity) {
let str = strings[type][rarity];
if(str.length < 1)
return;
let buffer = Base64ToUint8Array(str);
let len = buffer.length / 3;
for (let i = 0; i < len; i++) {
let evos = 0;
evos |= (buffer[i*3 ] << 0);
evos |= (buffer[i*3+1] << 8);
evos |= (buffer[i*3+2] << 16);
for (let j = 0; j < 8; j++) {
let evo = (evos >> (j*3)) & 0x07;
if (evo <= 0)
continue;
let short_id = '' + rarity + ('000'+(i*8+j)).slice(-3);
let itemSelector = '.tracker-item[data-type="'+type+'"][data-short_id="'+short_id+'"]';
let items = document.querySelectorAll(itemSelector);
items.forEach(function(item) {
evolve(item, evo);
});
}
}
});
});
};
const makeHash = function() {
// for format see readHash
let strings = {
'c': {2:'',3:'',4:''},
's': {2:'',3:'',4:''},
};
let selected = {
'c': {2:{},3:{},4:{}},
's': {2:{},3:{},4:{}},
};
// store which items have been selected
document.querySelectorAll('.tracker-item.selected').forEach(function(item) {
let short_id = item.dataset.short_id;
if (short_id.length === 4) {
let type = item.dataset.type;
let rarity = parseInt(short_id[0], 10);
if ((rarity < 2) || (rarity > 4))
return;
let index = parseInt(short_id.substr(1), 10);
selected[type][rarity][index] = parseInt(item.dataset.evo,10);
}
});
// convert to a bit array
['c', 's'].forEach(function(type) {
[2, 3, 4].forEach(function(rarity) {
let high_id = 0;
let obj = selected[type][rarity];
for (let index in obj)
high_id = Math.max(high_id, index);
// Group 8 items with 3 bits each
let parts = Math.floor(high_id / 8) + 1;
let size = parts * 3;
let buffer = new Uint8Array(size);
for (let i = 0; i <= Math.floor(high_id / 8); i++) {
let evos = 0x000000;
for (let j = 0; j < 8; j++) {
let evo = obj[i*8+j];
if (evo == undefined)
evo = 0;
evos |= (evo << (j*3));
}
buffer[i*3] = (evos >> 0) & 0xFF;
buffer[i*3+1] = (evos >> 8) & 0xFF;
buffer[i*3+2] = (evos >> 16) & 0xFF;
}
strings[type][rarity] += Uint8ArrayToBase64(buffer);
});
});
// Options are stored as hex encoded bit array
let options = '';
document.querySelectorAll('div[data-option]').forEach(function(div) {
let active = div.querySelectorAll('[data-bit].mw-ui-progressive, input[data-bit]:checked');
if (active.length > 0) {
let option = 0;
active.forEach(function(bit) {
option |= 1 << bit.dataset.bit;
});
options += div.dataset.option + option.toString(16);
}
});
let hash = "";
hash += `${options}.`;
hash += `${strings.c[4]}.`;
hash += `${strings.c[3]}.`;
hash += `${strings.c[2]}.`;
hash += `${strings.s[4]}.`;
hash += `${strings.s[3]}.`;
hash += `${strings.s[2]}`;
replaceHash(hash);
};
const evolve = function(node, levels) {
let toggle = node.parentElement.classList.contains('tracker-hide-uncap');
let evo = parseInt(node.dataset.evo, 10);
let evoMax = parseInt(node.dataset.evoMax, 10);
if (levels === false) {
evo = 0;
} else if (toggle) {
evo = evo > 0 ? 0 : 1;
} else if (levels === true) {
evo += levels;
if (evo > Math.max(evoMax+1, 1))
evo = 0;
} else {
evo = levels;
}
node.dataset.evo = evo.toString(10);
node.dataset.owned = evo > 0 ? 'true' : 'false';
node.classList.toggle('selected', evo > 0);
evo -= 1;
node.querySelectorAll('.tracker-uncap-star').forEach(function(star) {
star.classList.toggle('selected', evo > 0);
evo -= 1;
});
};
const updateUncap = function() {
let cb = document.querySelector('#tracker-character-uncap');
document.querySelectorAll('.tracker-box[id$="-characters"]').forEach(function(box) {
box.classList.toggle('tracker-hide-uncap', !cb.checked);
});
cb = document.querySelector('#tracker-summon-uncap');
document.querySelectorAll('.tracker-box[id$="-summons"]').forEach(function(box) {
box.classList.toggle('tracker-hide-uncap', !cb.checked);
});
makeHash();
};
const moveItems = function(type, element, rarity) {
let divSelector = '#tracker-'+element+'-' + (type==='c'?'characters':'summons');
let tracker = document.querySelector(divSelector);
if (tracker == null)
return;
let selector = '.tracker-item[data-type="'+type+'"][data-element="'+element+'"][data-rarity="'+rarity+'"]';
document.querySelectorAll(selector).forEach(function(node) {
node.dataset.owned = 'false';
node.dataset.evo = '0';
if (isNumeric(node.dataset.baseevo) && isNumeric(node.dataset.maxevo)) {
node.dataset.evoBase = parseInt(node.dataset.baseevo, 10).toString(10);
node.dataset.evoMax = parseInt(node.dataset.maxevo, 10).toString(10);
} else {
node.dataset.evoBase = '0';
node.dataset.evoMax = '0';
}
node.parentElement.removeChild(node);
tracker.insertBefore(node, null);
let evoBase = parseInt(node.dataset.evoBase);
let evoMax = parseInt(node.dataset.evoMax);
let uncapBrown = '<div class="tracker-uncap-star tracker-uncap-base"></div>'.repeat(evoBase);
let uncapBlue = '<div class="tracker-uncap-star tracker-uncap-max"></div>'.repeat(evoMax-evoBase);
let uncap = `<div class="tracker-uncap">${uncapBrown}${uncapBlue}</div>`;
node.insertAdjacentHTML('beforeend', uncap);
});
};
const getFilter = function(name) {
let result = [];
let selector = '#tracker-filter-'+name+' label.mw-ui-progressive';
let filters = document.querySelectorAll(selector);
for (let i = 0; i < filters.length; i++) {
let filter = filters[i];
if (filter.dataset.value === '*')
return true;
let values = filter.dataset.value.split(';');
values.forEach(function(value) {
result.push(value);
});
}
return result;
};
const updateItems = function() {
console.time('updating');
let rarity = getFilter('rarity');
let type = getFilter('type');
let element = getFilter('element');
let obtain = getFilter('obtain');
let style = getFilter('style');
let race = getFilter('race');
let gender = getFilter('gender');
let maxevo = getFilter('maxevo');
let owned = getFilter('owned');
let weapon = getFilter('weapon');
let released = getFilter('released');
let search = document.querySelector('#tracker-search').value;
let reSearch;
try {
reSearch = new RegExp('.*'+search+'.*','i');
} catch (e) {
reSearch = new RegExp('.*'+search.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")+'.*','i');
}
document.querySelectorAll('.tracker-item').forEach(function(item) {
let visible = true;
if ((rarity !== true) && (rarity.indexOf(item.dataset.rarity) === -1)) {
visible = false;
} else if ((type !== true) && (type.indexOf(item.dataset.type) === -1)) {
visible = false;
} else if ((element !== true) && (element.indexOf(item.dataset.element) === -1)) {
visible = false;
} else if ((style !== true) && (style.indexOf(item.dataset.style) === -1)) {
visible = false;
} else if ((maxevo !== true) && (maxevo.indexOf(item.dataset.maxevo) === -1)) {
visible = false;
} else if ((released !== true) && (released.indexOf(item.dataset.released) === -1)) {
visible = false;
} else if ((owned !== true) && (owned.indexOf(item.dataset.owned) === -1)) {
visible = false;
}
if (visible && (gender !== true)) {
visible = false;
for (let i=0; i<gender.length; i++) {
if (item.dataset.gender.indexOf(gender[i]) !== -1)
visible = true;
}
}
if (visible && (obtain !== true)) {
let obtain_arr = item.dataset.obtain.split(';');
if (obtain === 'other') {
let intersect = knownObtains.filter(value => -1 !== obtain_arr.indexOf(value));
visible = (intersect.length === 0);
} else {
let intersect = obtain.filter(value => -1 !== obtain_arr.indexOf(value));
visible = (intersect.length > 0);
}
}
if (visible && (weapon !== true)) {
let weapon_arr = item.dataset.weapon.split(',');
let intersect = weapon.filter(value => -1 !== weapon_arr.indexOf(value));
visible = (intersect.length > 0);
}
if (visible && (race !== true)) {
let race_arr = item.dataset.race.split(',');
let intersect = race.filter(value => -1 !== race_arr.indexOf(value));
visible = (intersect.length > 0);
}
if (visible && (search.length > 0)) {
visible = reSearch.test(item.dataset.id) || reSearch.test(item.dataset.name);
}
toggleVisible(item, visible);
});
document.querySelectorAll('.tracker-box').forEach(function(box) {
let visible = false;
for (let i = 0; i < box.childNodes.length; i++)
if (box.childNodes[i].style.display !== 'none') {
visible = true;
break;
}
toggleVisible(box, visible);
});
console.timeEnd('updating');
};
// Save valid obtain values for filtering
document.querySelectorAll('#tracker-filter-obtain .items > label').forEach((label) => {
if ('value' in label.dataset)
knownObtains.push(label.dataset.value);
});
window.addEventListener('hashchange', function() {
if (window.location.hash !== lastHash) {
readHash();
updateUncap();
updateItems();
}
}, false);
console.time('moving');
['fire','water','earth','wind','light','dark','any'].forEach(function(element) {
['ssr','sr','r'].forEach(function(rarity) {
moveItems('c', element, rarity);
moveItems('s', element, rarity);
});
});
console.timeEnd('moving');
// Clicking a tracker item
addEventForChild(document, 'click', '.tracker-item', function(event, item) {
if (event.button !== 0) return; // Do nothing unless it's a left-mouse-button click
event.preventDefault();
if (event.target.classList.contains('tracker-uncap-star'))
return;
console.time('Click Item');
evolve(item, true);
makeHash();
console.timeEnd('Click Item');
});
// Clicking the star of a tracker item
addEventForChild(document, 'click', '.tracker-uncap-star', function(event, star) {
event.preventDefault();
let level = [...star.parentElement.children].indexOf(star) + 2;
let item = star.closest('.tracker-item');
if (star.classList.contains('selected') && ((star.nextElementSibling == null) || !star.nextElementSibling.classList.contains('selected')))
level -= 1;
evolve(item, level);
});
addEventForChild(document, 'click', '.mw-ui-button-group > label:not(.mw-ui-disabled)', function(event, label) {
console.time('Click Label');
let group = label.parentElement;
let all = group.querySelector('label[data-value="*"]');
if (label === all) {
if (!all.classList.contains('mw-ui-progressive')) {
group.querySelectorAll('label').forEach(function(sibling) {
sibling.classList.remove('mw-ui-progressive');
});
all.classList.add('mw-ui-progressive');
}
} else if (group.childElementCount === 3) {
group.querySelectorAll('label').forEach(function(sibling) {
sibling.classList.remove('mw-ui-progressive');
});
label.classList.add('mw-ui-progressive');
} else {
all.classList.remove('mw-ui-progressive');
label.classList.toggle('mw-ui-progressive');
if (group.querySelectorAll('.mw-ui-progressive').length < 1)
all.classList.add('mw-ui-progressive');
}
setTimeout(function() {
console.time('Click Label Update');
updateItems();
makeHash();
console.timeEnd('Click Label Update');
}, 0);
console.timeEnd('Click Label');
});
addEventForChild(document, 'click', '#tracker-character-uncap, #tracker-summon-uncap', function() { updateUncap(); });
addEventForChild(document, 'change', '#tracker-search', function() { updateItems(); });
addEventForChild(document, 'keyup', '#tracker-search', function() { updateItems(); });
addEventForChild(document, 'click', '#tracker-local-save', function() {
makeHash();
localStorage.collectionTrackerState = window.location.hash;
});
addEventForChild(document, 'click', '#tracker-local-load', function() {
window.location.hash = localStorage.collectionTrackerState;
setTimeout(function() {
window.location.hash = localStorage.collectionTrackerState;
}, 0);
});
readHash();
updateUncap();
updateItems();
})();
</script>