// ==UserScript==
// @name Fullchan X
// @namespace Violentmonkey Scripts
// @match https://8chan.moe/*/res/*
// @match https://8chan.se/*/res/*
// @match https://8chan.moe/*/catalog*
// @match https://8chan.se/*/catalog*
// @run-at document-end
// @grant none
// @version 1.12.5
// @author vfyxe
// @description 8chan features script
// ==/UserScript==
class fullChanX extends HTMLElement {
constructor() {
super();
this.settingsEl = document.querySelector('fullchan-x-settings');
this.settingsAll = this.settingsEl.settings;
this.settings = this.settingsAll.main;
this.settingsThreadBanisher = this.settingsAll.threadBanisher;
this.settingsMascot = this.settingsAll.mascot;
this.isThread = !!document.querySelector('.opCell');
this.isDisclaimer = window.location.href.includes('disclaimer');
Object.keys(this.settings).forEach(key => {
this[key] = this.settings[key]?.value;
});
}
init() {
this.settingsButton = this.querySelector('#fcx-settings-btn');
this.settingsButton.addEventListener('click', () => this.settingsEl.toggle());
this.handleBoardLinks();
if (!this.isThread) {
if (this.settingsThreadBanisher.enableThreadBanisher.value) this.banishThreads(this.settingsThreadBanisher);
return;
}
this.quickReply = document.querySelector('#quick-reply');
this.qrbody = document.querySelector('#qrbody');
this.threadParent = document.querySelector('#divThreads');
this.threadId = this.threadParent.querySelector('.opCell').id;
this.thread = this.threadParent.querySelector('.divPosts');
this.posts = [...this.thread.querySelectorAll('.postCell')];
this.postOrder = 'default';
this.postOrderSelect = this.querySelector('#thread-sort');
this.myYousLabel = this.querySelector('.my-yous__label');
this.yousContainer = this.querySelector('#my-yous');
this.gallery = document.querySelector('fullchan-x-gallery');
this.galleryButton = this.querySelector('#fcx-gallery-btn');
this.updateYous();
this.observers();
if (this.enableFileExtensions) this.handleTruncatedFilenames();
if (this.settingsMascot.enableMascot.value) this.showMascot(this.settingsMascot);
}
styleUI () {
this.style.setProperty('--top', this.uiTopPosition);
this.style.setProperty('--right', this.uiRightPosition);
this.classList.toggle('fcx-in-nav', this.moveToNave)
this.classList.toggle('fcx--dim', this.uiDimWhenInactive && !this.moveToNave);
this.classList.toggle('page-thread', this.isThread);
const style = document.createElement('style');
if (this.hideDefaultBoards !== '') {
style.textContent += '#navTopBoardsSpan{display:block!important;}'
}
document.body.appendChild(style);
}
checkRegexList(string, regexList) {
const regexObjects = regexList.map(r => {
const match = r.match(/^\/(.*)\/([gimsuy]*)$/);
return match ? new RegExp(match[1], match[2]) : null;
}).filter(Boolean);
return regexObjects.some(regex => regex.test(string));
}
banishThreads(banisher) {
this.threadsContainer = document.querySelector('#divThreads');
if (!this.threadsContainer) return;
this.threadsContainer.classList.add('fcx-threads');
const currentBoard = document.querySelector('#labelBoard')?.textContent.replace(/\//g,'');
const boards = banisher.boards.value?.split(',') || [''];
if (!boards.includes(currentBoard)) return;
const minCharacters = banisher.minimumCharacters.value || 0;
const banishTerms = banisher.banishTerms.value?.split('\n') || [];
const banishAnchored = banisher.banishAnchored.value;
const wlCyclical = banisher.whitelistCyclical.value;
const wlReplyCount = parseInt(banisher.whitelistReplyCount.value);
const banishSorter = (thread) => {
if (thread.querySelector('.pinIndicator') || thread.classList.contains('fcx-sorted')) return;
let shouldBanish = false;
const isAnchored = thread.querySelector('.bumpLockIndicator');
const isCyclical = thread.querySelector('.cyclicIndicator');
const replyCount = parseInt(thread.querySelector('.labelReplies')?.textContent?.trim()) || 0;
const threadSubject = thread.querySelector('.labelSubject')?.textContent?.trim() || '';
const threadMessage = thread.querySelector('.divMessage')?.textContent?.trim() || '';
const threadContent = threadSubject + ' ' + threadMessage;
const hasMinChars = threadMessage.length > minCharacters;
const hasWlReplyCount = replyCount > wlReplyCount;
if (!hasMinChars) shouldBanish = true;
if (isAnchored && banishAnchored) shouldBanish = true;
if (isCyclical && wlCyclical) shouldBanish = false;
if (hasWlReplyCount) shouldBanish = false;
// run heavy regex process only if needed
if (!shouldBanish && this.checkRegexList(threadContent, banishTerms)) shouldBanish = true;
if (shouldBanish) thread.classList.add('shit-thread');
thread.classList.add('fcx-sorted');
};
const banishThreads = () => {
this.threads = this.threadsContainer.querySelectorAll('.catalogCell');
this.threads.forEach(thread => banishSorter(thread));
};
banishThreads();
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
banishThreads();
break;
}
}
});
observer.observe(this.threadsContainer, { childList: true });
}
handleBoardLinks () {
const navBoards = document.querySelector('#navTopBoardsSpan');
const customBoardLinks = this.customBoardLinks?.toLowerCase().replace(/\s/g,'').split(',');
let hideDefaultBoards = this.hideDefaultBoards?.toLowerCase().replace(/\s/g,'') || '';
const urlCatalog = this.catalogBoardLinks ? '/catalog.html' : '';
if (hideDefaultBoards === 'all') {
navBoards.classList.add('hidden');
} else {
const waitForNavBoards = setInterval(() => {
const navBoards = document.querySelector('#navTopBoardsSpan');
if (!navBoards || !navBoards.querySelector('a')) return;
clearInterval(waitForNavBoards);
hideDefaultBoards = hideDefaultBoards.split(',');
const defaultLinks = [...navBoards.querySelectorAll('a')];
defaultLinks.forEach(link => {
link.href += urlCatalog;
const linkText = link.textContent;
const shouldHide = hideDefaultBoards.includes(linkText) || customBoardLinks.includes(linkText);
link.classList.toggle('hidden', shouldHide);
});
}, 50);
}
if (this.customBoardLinks.length > 0) {
const customNav = document.createElement('span');
customNav.classList = 'nav-boards nav-boards--custom';
customNav.innerHTML = '<span>[</span>';
customBoardLinks.forEach((board, index) => {
const link = document.createElement('a');
link.href = '/' + board + urlCatalog;
link.textContent = board;
customNav.appendChild(link);
if (index < customBoardLinks.length - 1) customNav.innerHTML += '<span>/</span>';
});
customNav.innerHTML += '<span>]</span>';
navBoards.parentNode.insertBefore(customNav, navBoards);
}
}
observers () {
this.postOrderSelect.addEventListener('change', (event) => {
this.postOrder = event.target.value;
this.assignPostOrder();
});
const observerCallback = (mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
this.posts = [...this.thread.querySelectorAll('.postCell')];
if (this.postOrder !== 'default') this.assignPostOrder();
this.updateYous();
this.gallery.updateGalleryImages();
if (this.settings.enableFileExtensions) this.handleTruncatedFilenames();
}
}
};
const threadObserver = new MutationObserver(observerCallback);
threadObserver.observe(this.thread, { childList: true, subtree: false });
if (this.enableNestedQuotes) {
this.thread.addEventListener('click', event => {
this.handleClick(event);
});
}
this.galleryButton.addEventListener('click', () => this.gallery.open());
this.myYousLabel.addEventListener('click', (event) => {
if (this.myYousLabel.classList.contains('unseen')) {
this.yousContainer.querySelector('.unseen').click();
}
});
}
handleClick (event) {
const clicked = event.target;
const post = clicked.closest('.innerPost') || clicked.closest('.innerOP');
if (!post) return;
const isNested = !!post.closest('.innerNested');
const nestQuote = clicked.closest('.quoteLink') || clicked.closest('.panelBacklinks a');
const postMedia = clicked.closest('a[data-filemime]');
const postId = clicked.closest('.linkQuote');
const anonId = clicked.closest('.labelId');
if (nestQuote) {
if (event.target.closest('.fcx-prevent-nesting')) return;
event.preventDefault();
this.nestQuote(nestQuote, post);
} else if (postMedia && isNested) {
this.handleMediaClick(event, postMedia);
} else if (postId && isNested) {
this.handleIdClick(postId);
} else if (anonId) {
this.handleAnonIdClick(anonId, event);
}
}
handleAnonIdClick (anonId, event) {
this.anonIdPosts?.remove();
if (anonId === this.anonId) {
this.anonId = null;
return;
}
this.anonId = anonId;
const anonIdText = anonId.textContent.split(' ')[0];
this.anonIdPosts = document.createElement('div');
this.anonIdPosts.classList = 'fcx-id-posts fcx-prevent-nesting';
const match = window.location.pathname.match(/^\/[^/]+\/res\/\d+\.html/);
const prepend = match ? `${match[0]}#` : '';
const selector = `.postInfo:has(.labelId[style="background-color: #${anonIdText}"]) .linkQuote`;
const postIds = [...this.threadParent.querySelectorAll(selector)].map(link => {
const postId = link.getAttribute('href').split('#q').pop();
const newLink = document.createElement('a');
newLink.className = 'quoteLink';
newLink.href = prepend + postId;
newLink.textContent = `>>${postId}`;
console.log('newLink',newLink)
return newLink;
});
postIds.forEach(postId => this.anonIdPosts.appendChild(postId));
anonId.insertAdjacentElement('afterend', this.anonIdPosts);
this.setPostListeners(this.anonIdPosts);
}
handleMediaClick (event, postMedia) {
if (postMedia.dataset.filemime === "video/webm") return;
event.preventDefault();
const imageSrc = `${postMedia.href}`;
const imageEl = postMedia.querySelector('img');
if (!postMedia.dataset.thumbSrc) postMedia.dataset.thumbSrc = `${imageEl.src}`;
const isExpanding = imageEl.src !== imageSrc;
if (isExpanding) {
imageEl.src = imageSrc;
imageEl.classList
}
imageEl.src = isExpanding ? imageSrc : postMedia.dataset.thumbSrc;
imageEl.classList.toggle('imgExpanded', isExpanding);
}
handleIdClick (postId) {
const idNumber = '>>' + postId.textContent;
this.quickReply.style.display = 'block';
this.qrbody.value += idNumber + '\n';
}
handleTruncatedFilenames () {
this.postFileNames = [...this.threadParent.querySelectorAll('.originalNameLink[download]:not([data-file-ext])')];
this.postFileNames.forEach(fileName => {
if (!fileName.textContent.includes('.')) return;
const strings = fileName.textContent.split('.');
const typeStr = `.${strings.pop()}`;
const typeEl = document.createElement('a');
typeEl.classList = ('file-ext originalNameLink');
typeEl.textContent = typeStr;
fileName.dataset.fileExt = typeStr;
fileName.textContent = strings.join('.');
fileName.parentNode.insertBefore(typeEl, fileName.nextSibling);
});
}
assignPostOrder () {
const postOrderReplies = (post) => {
const replyCount = post.querySelectorAll('.panelBacklinks a').length;
post.style.order = 100 - replyCount;
}
const postOrderCatbox = (post) => {
const postContent = post.querySelector('.divMessage').textContent;
const matches = postContent.match(/catbox\.moe/g);
const catboxCount = matches ? matches.length : 0;
post.style.order = 100 - catboxCount;
}
if (this.postOrder === 'default') {
this.thread.style.display = 'block';
return;
}
this.thread.style.display = 'flex';
if (this.postOrder === 'replies') {
this.posts.forEach(post => postOrderReplies(post));
} else if (this.postOrder === 'catbox') {
this.posts.forEach(post => postOrderCatbox(post));
}
}
updateYous () {
this.yous = this.posts.filter(post => post.querySelector('.quoteLink.you'));
this.yousLinks = this.yous.map(you => {
const youLink = document.createElement('a');
youLink.textContent = '>>' + you.id;
youLink.href = '#' + you.id;
return youLink;
})
let hasUnseenYous = false;
this.setUnseenYous();
this.yousContainer.innerHTML = '';
this.yousLinks.forEach(you => {
const youId = you.textContent.replace('>>', '');
if (!this.seenYous.includes(youId)) {
you.classList.add('unseen');
hasUnseenYous = true
}
this.yousContainer.appendChild(you)
});
this.myYousLabel.classList.toggle('unseen', hasUnseenYous);
if (this.replyTabIcon === '') return;
const icon = this.replyTabIcon;
document.title = hasUnseenYous
? document.title.startsWith(`${icon} `)
? document.title
: `${icon} ${document.title}`
: document.title.replace(new RegExp(`^${icon} `), '');
}
observeUnseenYou(you) {
you.classList.add('observe-you');
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = you.id;
you.classList.remove('observe-you');
if (!this.seenYous.includes(id)) {
this.seenYous.push(id);
localStorage.setItem(this.seenKey, JSON.stringify(this.seenYous));
}
observer.unobserve(you);
this.updateYous();
}
});
}, { rootMargin: '0px', threshold: 0.1 });
observer.observe(you);
}
setUnseenYous() {
this.seenKey = `${this.threadId}-seen-yous`;
this.seenYous = JSON.parse(localStorage.getItem(this.seenKey));
if (!this.seenYous) {
this.seenYous = [];
localStorage.setItem(this.seenKey, JSON.stringify(this.seenYous));
}
this.unseenYous = this.yous.filter(you => !this.seenYous.includes(you.id));
this.unseenYous.forEach(you => {
if (!you.classList.contains('observe-you')) {
this.observeUnseenYou(you);
}
});
}
nestQuote(quoteLink, parentPost) {
const parentPostMessage = parentPost.querySelector('.divMessage');
const quoteId = quoteLink.href.split('#').pop();
const quotePost = document.getElementById(quoteId);
if (!quotePost) return;
const quotePostContent = quotePost.querySelector('.innerOP') || quotePost.querySelector('.innerPost');
if (!quotePostContent) return;
const existing = parentPost.querySelector(`.nestedPost[data-quote-id="${quoteId}"]`);
if (existing) {
existing.remove();
return;
}
const isReply = !quoteLink.classList.contains('quoteLink');
const wrapper = document.createElement('div');
wrapper.classList.add('nestedPost');
wrapper.setAttribute('data-quote-id', quoteId);
const clone = quotePostContent.cloneNode(true);
clone.style.whiteSpace = 'unset';
clone.classList.add('innerNested');
wrapper.appendChild(clone);
if (isReply) {
parentPostMessage.insertBefore(wrapper, parentPostMessage.firstChild);
} else {
quoteLink.insertAdjacentElement('afterend', wrapper);
}
this.setPostListeners(wrapper);
}
setPostListeners(parentPost) {
const postLinks = [
...parentPost.querySelectorAll('.quoteLink'),
...parentPost.querySelectorAll('.panelBacklinks a')
];
const hoverPost = (event, link) => {
const quoteId = link.href.split('#')[1];
let existingPost = document.querySelector(`.nestedPost[data-quote-id="${quoteId}"]`)
|| link.closest(`.postCell[id="${quoteId}"]`);
if (existingPost) {
this.markedPost = existingPost.querySelector('.innerPost') || existingPost.querySelector('.innerOP');
this.markedPost?.classList.add('markedPost');
return;
}
const quotePost = document.getElementById(quoteId);
tooltips.removeIfExists();
const tooltip = document.createElement('div');
tooltip.className = 'quoteTooltip';
document.body.appendChild(tooltip);
const rect = link.getBoundingClientRect();
if (!api.mobile) {
if (rect.left > window.innerWidth / 2) {
const right = window.innerWidth - rect.left - window.scrollX;
tooltip.style.right = `${right}px`;
} else {
const left = rect.right + 10 + window.scrollX;
tooltip.style.left = `${left}px`;
}
}
tooltip.style.top = `${rect.top + window.scrollY}px`;
tooltip.style.display = 'inline';
tooltips.loadTooltip(tooltip, link.href, quoteId);
tooltips.currentTooltip = tooltip;
}
const unHoverPost = (event, link) => {
if (!tooltips.currentTooltip) {
this.markedPost?.classList.remove('markedPost');
return false;
}
if (tooltips.unmarkReply) {
tooltips.currentTooltip.classList.remove('markedPost');
Array.from(tooltips.currentTooltip.getElementsByClassName('replyUnderline'))
.forEach((a) => a.classList.remove('replyUnderline'))
tooltips.unmarkReply = false;
} else {
tooltips.currentTooltip.remove();
}
tooltips.currentTooltip = null;
}
const addHoverPost = (link => {
link.addEventListener('mouseenter', (event) => hoverPost(event, link));
link.addEventListener('mouseleave', (event) => unHoverPost(event, link));
});
postLinks.forEach(link => addHoverPost(link));
}
showMascot(settings) {
const mascot = document.createElement('img');
mascot.classList.add('fcx-mascot');
mascot.src = settings.image.value;
mascot.style.opacity = settings.opacity.value * 0.01;
mascot.style.top = settings.top.value;
mascot.style.left = settings.left.value;
mascot.style.right = settings.right.value;
mascot.style.bottom = settings.bottom.value;
mascot.style.height = settings.height.value;
mascot.style.width = settings.width.value;
document.body.appendChild(mascot);
}
};
window.customElements.define('fullchan-x', fullChanX);
class fullChanXGallery extends HTMLElement {
constructor() {
super();
}
init() {
this.fullchanX = document.querySelector('fullchan-x');
this.imageContainer = this.querySelector('.gallery__images');
this.mainImageContainer = this.querySelector('.gallery__main-image');
this.mainImage = this.mainImageContainer.querySelector('img');
this.sizeButtons = [...this.querySelectorAll('.gallery__scale-options .scale-option')];
this.closeButton = this.querySelector('.gallery__close');
this.listeners();
this.addGalleryImages();
this.initalized = true;
}
addGalleryImages () {
this.thumbs = [...this.fullchanX.threadParent.querySelectorAll('.imgLink')].map(thumb => {
return thumb.cloneNode(true);
});
this.thumbs.forEach(thumb => {
this.imageContainer.appendChild(thumb);
});
}
updateGalleryImages () {
if (!this.initalized) return;
const newThumbs = [...this.fullchanX.threadParent.querySelectorAll('.imgLink')].filter(thumb => {
return !this.thumbs.find(thisThumb.href === thumb.href);
}).map(thumb => {
return thumb.cloneNode(true);
});
newThumbs.forEach(thumb => {
this.thumbs.push(thumb);
this.imageContainer.appendChild(thumb);
});
}
listeners () {
this.addEventListener('click', event => {
const clicked = event.target;
let imgLink = clicked.closest('.imgLink');
if (imgLink?.dataset.filemime === 'video/webm') return;
if (imgLink) {
event.preventDefault();
this.mainImage.src = imgLink.href;
}
this.mainImageContainer.classList.toggle('active', !!imgLink);
const scaleButton = clicked.closest('.scale-option');
if (scaleButton) {
const scale = parseFloat(getComputedStyle(this.imageContainer).getPropertyValue('--scale')) || 1;
const delta = scaleButton.id === 'fcxg-smaller' ? -0.1 : 0.1;
const newScale = Math.max(0.1, scale + delta);
this.imageContainer.style.setProperty('--scale', newScale.toFixed(2));
}
if (clicked.closest('.gallery__close')) this.close();
});
}
open () {
if (!this.initalized) this.init();
this.classList.add('open');
document.body.classList.add('fct-gallery-open');
}
close () {
this.classList.remove('open');
document.body.classList.remove('fct-gallery-open');
}
}
window.customElements.define('fullchan-x-gallery', fullChanXGallery);
class fullChanXSettings extends HTMLElement {
constructor() {
super();
this.settingsKey = 'fullchan-x-settings';
this.inputs = [];
this.settings = {};
this.settingsTemplate = {
main: {
moveToNav: {
info: 'Move Fullchan-X controls into the navbar.',
type: 'checkbox',
value: true
},
enableNestedQuotes: {
info: 'Nest posts when clicking backlinks.',
type: 'checkbox',
value: true
},
enableFileExtensions: {
info: 'Always show filetype on shortened file names.',
type: 'checkbox',
value: true
},
customBoardLinks: {
info: 'List of custom boards in nav (seperate by comma)',
type: 'input',
value: 'v,a,b'
},
hideDefaultBoards: {
info: 'List of boards to remove from nav (seperate by comma). Set as "all" to remove all.',
type: 'input',
value: 'interracial,mlp'
},
catalogBoardLinks: {
info: 'Redirect nav board links to catalog pages.',
type: 'checkbox',
value: true
},
uiTopPosition: {
info: 'Position from top of screen e.g. 100px',
type: 'input',
value: '50px'
},
uiRightPosition: {
info: 'Position from right of screen e.g. 100px',
type: 'input',
value: '25px'
},
uiDimWhenInactive: {
info: 'Dim UI when not hovering with mouse.',
type: 'checkbox',
value: true
},
replyTabIcon: {
info: 'Set the icon/text added to tab title when you get a new (You).',
type: 'input',
value: '❗'
}
},
mascot: {
enableMascot: {
info: 'Enable mascot image.',
type: 'checkbox',
value: false
},
image: {
info: 'Image URL (8chan image recommended).',
type: 'input',
value: '/.static/logo.png'
},
opacity: {
info: 'Opacity (1 to 100)',
type: 'input',
inputType: 'number',
value: '75'
},
width: {
info: 'Width of image.',
type: 'input',
value: '300px'
},
height: {
info: 'Height of image.',
type: 'input',
value: 'auto'
},
bottom: {
info: 'Bottom position.',
type: 'input',
value: '0px'
},
right: {
info: 'Right position.',
type: 'input',
value: '0px'
},
top: {
info: 'Top position.',
type: 'input',
value: ''
},
left: {
info: 'Left position.',
type: 'input',
value: ''
}
},
threadBanisher: {
enableThreadBanisher: {
info: 'Banish shit threads to the bottom of the calalog.',
type: 'checkbox',
value: true
},
boards: {
info: 'Banish theads on these boards (seperated by comma).',
type: 'input',
value: 'v,a'
},
minimumCharacters: {
info: 'Minimum character requirements',
type: 'input',
inputType: 'number',
value: 100
},
banishTerms: {
info: `<p>Banish threads with these terms to the bottom of the catalog (new line per term).</p>
<p>How to use regex: <a href="https://www.datacamp.com/cheat-sheet/regular-expresso" target="__blank">Regex Cheatsheet</a>.</p>
<p>NOTE: word breaks (\\b) MUST be entered as double escapes (\\\\b), they will appear as (\\b) when saved.</p>
`,
type: 'textarea',
value: '/\\bcuck\\b/i\n/\\bchud\\b/i\n/\\bblacked\\b/i\n/\\bnormie\\b/i\n/\\bincel\\b/i\n/\\btranny\\b/i\n/\\bslop\\b/i\n'
},
whitelistCyclical: {
info: 'Whitelist cyclical threads.',
type: 'checkbox',
value: true
},
banishAnchored: {
info: 'Banish anchored threads that are under minimum reply count.',
type: 'checkbox',
value: true
},
whitelistReplyCount: {
info: 'Threads above this reply count (excluding those with banish terms) will be whitelisted.',
type: 'input',
inputType: 'number',
value: 100
},
}
};
}
init() {
this.settingsMain = this.querySelector('.fcxs-main');
this.settingsThreadBanisher = this.querySelector('.fcxs-thread-banisher');
this.settingsMascot = this.querySelector('.fcxs-mascot');
this.getSavedSettings();
this.buildSettingsOptions('main', this.settingsMain);
this.buildSettingsOptions('threadBanisher', this.settingsThreadBanisher);
this.buildSettingsOptions('mascot', this.settingsMascot);
this.listeners();
this.querySelector('.fcx-settings__close').addEventListener('click', () => this.close());
}
setSavedSettings(updated) {
localStorage.setItem(this.settingsKey, JSON.stringify(this.settings));
if (updated) this.classList.add('fcxs-updated');
}
getSavedSettings() {
let saved = JSON.parse(localStorage.getItem(this.settingsKey));
if (!saved) return;
// Ensure all top-level keys exist
for (const key in this.settingsTemplate) {
if (!saved[key]) saved[key] = {};
}
this.settings = saved;
}
listeners() {
this.inputs.forEach(input => {
input.addEventListener('change', () => {
const section = input.dataset.section;
const key = input.name;
const value = input.type === 'checkbox' ? input.checked : input.value;
this.settings[section][key].value = value;
this.setSavedSettings(true);
});
});
}
buildSettingsOptions(subSettings, parent) {
if (!this.settings[subSettings]) this.settings[subSettings] = {}
Object.entries(this.settingsTemplate[subSettings]).forEach(([key, config]) => {
const wrapper = document.createElement('div');
const infoWrapper = document.createElement('div');
wrapper.classList.add('fcx-setting');
infoWrapper.classList.add('fcx-setting__info');
wrapper.appendChild(infoWrapper);
const label = document.createElement('label');
label.textContent = key
.replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase());
label.setAttribute('for', key);
infoWrapper.appendChild(label);
if (config.info) {
const info = document.createElement('p');
info.innerHTML = config.info;
infoWrapper.appendChild(info);
}
const savedValue = this.settings[subSettings][key]?.value ?? config.value;
let input;
if (config.type === 'checkbox') {
input = document.createElement('input');
input.type = 'checkbox';
input.checked = savedValue;
} else if (config.type === 'textarea') {
input = document.createElement('textarea');
input.value = savedValue;
} else if (config.type === 'input') {
input = document.createElement('input');
input.type = config.inputType || 'text';
input.value = savedValue;
} else if (config.type === 'select' && config.options) {
input = document.createElement('select');
const options = config.options.split(',');
options.forEach(opt => {
const option = document.createElement('option');
option.value = opt;
option.textContent = opt;
if (opt === savedValue) option.selected = true;
input.appendChild(option);
});
}
if (input) {
input.id = key;
input.name = key;
input.dataset.section = subSettings;
wrapper.appendChild(input);
this.inputs.push(input);
this.settings[subSettings][key] = {
value: input.type === 'checkbox' ? input.checked : input.value
};
}
parent.appendChild(wrapper);
});
}
open() {
this.classList.add('open');
}
close() {
this.classList.remove('open');
}
toggle() {
this.classList.toggle('open');
}
}
window.customElements.define('fullchan-x-settings', fullChanXSettings);
class ToggleButton extends HTMLElement {
constructor() {
super();
const data = this.dataset;
this.onclick = () => {
const target = data.target ? document.querySelector(data.target) : this;
const value = data.value || 'active';
!!data.set ? target.dataset[data.set] = value : target.classList.toggle(value);
}
}
}
window.customElements.define('toggle-button', ToggleButton);
// Create fullchan-x settings
const fcxs = document.createElement('fullchan-x-settings');
fcxs.innerHTML = `
<div class="fcx-settings fcxs" data-tab="main">
<header>
<div class="fcxs__heading">
<span class="fcx-settings__title">
Fullchan-X Settings
</span>
<button class="fcx-settings__close fullchan-x__option">Close</button>
</div>
<div class="fcx-settings__tab-buttons">
<toggle-button data-target=".fcxs" data-set="tab" data-value="main">
Main
</toggle-button>
<toggle-button data-target=".fcxs" data-set="tab" data-value="catalog">
catalog
</toggle-button>
<toggle-button data-target=".fcxs" data-set="tab" data-value="mascot">
Mascot
</toggle-button>
</div>
</header>
<main>
<div class="fcxs__updated-message">
<p>Settings updated, refresh page to apply</p>
<button class="fullchan-x__option" onClick="location.reload()">Reload page</button>
</div>
<div class="fcx-settings__settings">
<div class="fcxs-main fcxs-tab"></div>
<div class="fcxs-mascot fcxs-tab"></div>
<div class="fcxs-catalog fcxs-tab">
<div class="fcxs-thread-banisher"></div>
</div>
</div>
</main>
<footer>
</footer>
</div>
`;
document.body.appendChild(fcxs);
fcxs.init();
// Create fullchan-x gallery
const fcxg = document.createElement('fullchan-x-gallery');
fcxg.innerHTML = `
<div class="fcxg gallery">
<button id="fcxg-close" class="gallery__close fullchan-x__option">Close</button>
<div class="gallery__scale-options">
<button id="fcxg-smaller" class="scale-option fullchan-x__option">-</button>
<button id="fcxg-larger" class="scale-option fullchan-x__option">+</button>
</div>
<div id="fcxg-images" class="gallery__images" style="--scale:1.0"></div>
<div id="fcxg-main-image" class="gallery__main-image">
<img src="" />
</div>
</div>
`;
document.body.appendChild(fcxg);
// Create fullchan-x element
const fcx = document.createElement('fullchan-x');
fcx.innerHTML = `
<div class="fcx__controls">
<button id="fcx-settings-btn" class="fullchan-x__option fcx-settings-toggle">
<a>⚙️</a><span>Settings</span>
</button>
<div class="fullchan-x__option fullchan-x__sort thread-only">
<a>☰</a>
<select id="thread-sort">
<option value="default">Default</option>
<option value="replies">Replies</option>
<option value="catbox">Catbox</option>
</select>
</div>
<button id="fcx-gallery-btn" class="gallery__toggle fullchan-x__option thread-only">
<a>🖼️</a><span>Gallery</span>
</button>
<div class="fcx__my-yous thread-only">
<p class="my-yous__label fullchan-x__option"><a>💬</a><span>My (You)s</span></p>
<div class="my-yous__yous fcx-prevent-nesting" id="my-yous"></div>
</div>
</div>
`;
(document.querySelector('.navHeader') || document.body).appendChild(fcx);
fcx.styleUI()
onload = (event) => fcx.init();
// Styles
const style = document.createElement('style');
style.innerHTML = `
fullchan-x {
--top: 50px;
--right: 25px;
background: var(--background-color);
border: 1px solid var(--navbar-text-color);
color: var(--link-color);
font-size: 14px;
z-index: 3;
}
toggle-button {
cursor: pointer;
}
/* Fullchan-X in nav styles */
.fcx-in-nav {
padding: 0;
border-width: 0;
line-height: 20px;
margin-right: 2px;
background: none;
}
.fcx-in-nav .fcx__controls:before,
.fcx-in-nav .fcx__controls:after {
color: var(--navbar-text-color);
font-size: 85%;
}
.fcx-in-nav .fcx__controls:before {
content: "]";
}
.fcx-in-nav .fcx__controls:after {
content: "[";
}
.fcx-in-nav .fcx__controls,
.fcx-in-nav:hover .fcx__controls:hover {
flex-direction: row-reverse;
}
.fcx-in-nav .fcx__controls .fullchan-x__option {
padding: 0!important;
justify-content: center;
background: none;
line-height: 0;
max-width: 20px;
min-width: 20px;
translate: 0 1px;
border: solid var(--navbar-text-color) 1px !important;
}
.fcx-in-nav .fcx__controls .fullchan-x__option:hover {
border: solid var(--subject-color) 1px !important;
}
.fcx-in-nav .fullchan-x__sort > a {
margin-bottom: 1px;
}
.fcx-in-nav .fcx__controls > * {
position: relative;
}
.fcx-in-nav .fcx__controls .fullchan-x__option > span,
.fcx-in-nav .fcx__controls .fullchan-x__option:not(:hover) > select {
display: none;
}
.fcx-in-nav .fcx__controls .fullchan-x__option > select {
appearance: none;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
font-size: 0;
}
.fcx-in-nav .fcx__controls .fullchan-x__option > select option {
font-size: 12px;
}
.fcx-in-nav .my-yous__yous {
position: absolute;
left: 50%;
translate: -50%;
background: var(--background-color);
border: 1px solid var(--navbar-text-color);
padding: 14px;
}
/* Fullchan-X main styles */
fullchan-x:not(.fcx-in-nav) {
top: var(--top);
right: var(--right);
display: block;
padding: 10px;
position: fixed;
display: block;
}
fullchan-x:not(.page-thread) .thread-only,
fullchan-x:not(.page-catalog) .catalog-only {
display: none!important;
}
fullchan-x:hover {
z-index: 1000!important;
}
.navHeader:has(fullchan-x:hover) {
z-index: 1000!important;
}
fullchan-x.fcx--dim:not(:hover) {
opacity: 0.6;
}
.divPosts {
flex-direction: column;
}
.fcx__controls {
display: flex;
flex-direction: column;
gap: 6px;
}
fullchan-x:not(:hover):not(:has(select:focus)) span,
fullchan-x:not(:hover):not(:has(select:focus)) select {
display: none;
margin-left: 5px;
z-index:3;
}
.fcx__controls span,
.fcx__controls select {
margin-left: 5px;
}
.fcx__controls select {
cursor: pointer;
}
#thread-sort {
border: none;
background: none;
}
.my-yous__yous {
display: none;
flex-direction: column;
padding-top: 10px;
max-height: calc(100vh - 220px - var(--top));
overflow: auto;
}
.fcx__my-yous:hover .my-yous__yous {
display: flex;
}
.fullchan-x__option {
display: flex;
padding: 6px 8px;
background: white;
border: none !important;
border-radius: 0.2rem;
transition: all ease 150ms;
cursor: pointer;
margin: 0;
text-align: left;
min-width: 18px;
min-height: 18px;
align-items: center;
}
.fullchan-x__option,
.fullchan-x__option select {
font-size: 12px;
font-weight: 400;
color: #374369;
}
fullchan-x:not(:hover):not(:has(select:focus)) .fullchan-x__option {
display: flex;
justify-content: center;
}
#thread-sort {
padding-right: 0;
}
#thread-sort:hover {
display: block;
}
.innerPost:has(.quoteLink.you) {
border-left: solid #dd003e 6px;
}
.innerPost:has(.youName) {
border-left: solid #68b723 6px;
}
/* --- Nested quotes --- */
.divMessage .nestedPost {
display: inline-block;
width: 100%;
margin-bottom: 14px;
white-space: normal!important;
overflow-wrap: anywhere;
margin-top: 0.5em;
border: 1px solid var(--navbar-text-color);
}
.nestedPost .innerPost,
.nestedPost .innerOP {
width: 100%;
}
.nestedPost .imgLink .imgExpanded {
width: auto!important;
height: auto!important;
}
.my-yous__label.unseen {
background: var(--link-hover-color)!important;
color: white;
}
.my-yous__yous .unseen {
font-weight: 900;
color: var(--link-hover-color);
}
/*--- Settings --- */
.fcx-settings {
display: block;
position: fixed;
top: 50vh;
left: 50vw;
translate: -50% -50%;
padding: 20px 0;
background: var(--background-color);
border: 1px solid var(--navbar-text-color);
color: var(--link-color);
font-size: 14px;
max-width: 480px;
max-height: 80vh;
overflow: scroll;
min-width: 500px;
z-index: 1000;
}
fullchan-x-settings:not(.open) {
display: none;
}
.fcxs__heading,
.fcxs-tab,
.fcxs footer {
padding: 0 20px;
}
.fcx-settings header {
margin: 0 0 15px;
border-bottom: 1px solid var(--navbar-text-color);
}
.fcxs__heading {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 20px;
}
.fcx-settings__title {
font-size: 24px;
font-size: 24px;
letter-spacing: 0.04em;
}
.fcx-settings__tab-buttons {
border-top: 1px solid var(--navbar-text-color);
display: flex;
align-items: center;
}
.fcx-settings__tab-buttons toggle-button {
flex: 1;
padding: 15px;
font-size: 14px;
}
.fcx-settings__tab-buttons toggle-button + toggle-button {
border-left: 1px solid var(--navbar-text-color);
}
.fcx-settings__tab-buttons toggle-button:hover {
color: var(--role-color);
}
fullchan-x-settings:not(.fcxs-updated) .fcxs__updated-message {
display: none;
}
.fcxs:not([data-tab="main"]) .fcxs-main,
.fcxs:not([data-tab="catalog"]) .fcxs-catalog,
.fcxs:not([data-tab="mascot"]) .fcxs-mascot {
display: none;
}
.fcxs[data-tab="main"] [data-value="main"],
.fcxs[data-tab="catalog"] [data-value="catalog"],
.fcxs[data-tab="mascot"] [data-value="mascot"] {
font-weight: 700;
}
.fcx-setting {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
}
.fcx-setting__info {
max-width: 60%;
}
.fcx-setting input[type="text"],
.fcx-setting input[type="number"],
.fcx-setting select,
.fcx-setting textarea {
padding: 4px 6px;
min-width: 35%;
}
.fcx-setting textarea {
min-height: 100px;
}
.fcx-setting label {
font-weight: 600;
}
.fcx-setting p {
margin: 6px 0 0;
font-size: 12px;
}
.fcx-setting + .fcx-setting {
border-top: 1px solid var(--navbar-text-color);
}
.fcxs__updated-message {
margin: 10px 0;
text-align: center;
}
.fcxs__updated-message p {
font-size: 14px;
color: var(--error);
}
.fcxs__updated-message button {
margin: 14px auto 0;
}
/* --- Gallery --- */
.fct-gallery-open,
body.fct-gallery-open,
body.fct-gallery-open #mainPanel {
overflow: hidden!important;
}
body.fct-gallery-open fullchan-x:not(.fcx-in-nav),
body.fct-gallery-open #quick-reply {
display: none!important;
}
fullchan-x-gallery {
position: fixed;
top: 0;
left: 0;
width: 100%;
background: rgba(0,0,0,0.9);
display: none;
height: 100%;
overflow: auto;
}
fullchan-x-gallery.open {
display: block;
}
fullchan-x-gallery .gallery {
padding: 50px 10px 0
}
fullchan-x-gallery .gallery__images {
--scale: 1.0;
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-content: flex-start;
gap: 4px 8px;
flex-wrap: wrap;
}
fullchan-x-gallery .imgLink {
float: unset;
display: block;
zoom: var(--scale);
}
fullchan-x-gallery .imgLink img {
border: solid white 1px;
}
fullchan-x-gallery .imgLink[data-filemime="video/webm"] img {
border: solid #68b723 4px;
}
fullchan-x-gallery .gallery__close {
border: solid 1px var(--background-color)!important;
position: fixed;
top: 60px;
right: 35px;
padding: 6px 14px;
min-height: 30px;
z-index: 10;
}
.fcxg .gallery__scale-options {
position: fixed;
bottom: 30px;
right: 35px;
display: flex;
gap: 14px;
z-index: 10;
}
.fcxg .gallery__scale-options .fullchan-x__option {
border: solid 1px var(--background-color)!important;
width: 35px;
height: 35px;
font-size: 18px;
display: flex;
justify-content: center;
}
.gallery__main-image {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
justify-content: center;
align-content: center;
background: rgba(0,0,0,0.5);
}
.gallery__main-image img {
padding: 40px 10px 15px;
height: auto;
max-width: calc(100% - 20px);
object-fit: contain;
}
.gallery__main-image.active {
display: flex;
}
/*-- Truncated file extentions --*/
.originalNameLink[data-file-ext] {
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 65px;
}
.originalNameLink[data-file-ext]:hover {
max-width: unset;
white-space: normal;
display: inline;
}
a[data-file-ext]:hover:after {
content: attr(data-file-ext);
}
a[data-file-ext] + .file-ext {
pointer-events: none;
}
a[data-file-ext]:hover + .file-ext {
display: none;
}
/*-- Nav Board Links --*/
.nav-boards--custom {
display: flex;
gap: 3px;
}
#navTopBoardsSpan.hidden ~ #navBoardsSpan,
#navTopBoardsSpan.hidden ~ .nav-fade,
#navTopBoardsSpan a.hidden + span {
display: none;
}
/*-- Anon Unique ID posts --*/
.postInfo .spanId {
position: relative;
}
.fcx-id-posts {
position: absolute;
top: 0;
left: 20px;
translate: 0 calc(-100% - 5px);
display: flex;
flex-direction: column;
padding: 10px;
background: var(--background-color);
border: 1px solid var(--navbar-text-color);
width: max-content;
max-width: 500px;
max-height: 500px;
overflow: auto;
z-index: 1000;
}
.fcx-id-posts .nestedPost {
pointer-events: none;
width: auto;
}
/* mascot */
.fcx-mascot {
position: fixed;
z-index: 0;
}
.fct-gallery-open .fcx-mascot {
display: none;
}
/*-- Thread sorting --*/
#divThreads.fcx-threads {
display: flex!important;
flex-wrap: wrap;
justify-content: center;
}
.catalogCell.shit-thread {
order: 10;
}
.catalogCell.shit-thread .labelPage:after {
content: " 💩"
}
`;
document.head.appendChild(style);
// Asuka and Eris (fantasy Asuka) are best girls