361 lines
10 KiB
TypeScript
361 lines
10 KiB
TypeScript
|
import {Except} from 'type-fest';
|
||
|
import debounce from 'debounce';
|
||
|
import {ColorKey, ThemeKey, themeColors} from '../theme-colors';
|
||
|
import {
|
||
|
getSettings,
|
||
|
Settings,
|
||
|
log,
|
||
|
createElementFromString,
|
||
|
UserLabel,
|
||
|
isInTopicListing,
|
||
|
getCurrentThemeKey,
|
||
|
isColorBright,
|
||
|
setSettings,
|
||
|
appendStyleAttribute,
|
||
|
querySelector
|
||
|
} from '../utilities';
|
||
|
import {
|
||
|
getLabelForm,
|
||
|
getLabelFormValues,
|
||
|
hideLabelForm,
|
||
|
getLabelFormID
|
||
|
} from './user-labels/label-form';
|
||
|
import {
|
||
|
editLabelHandler,
|
||
|
addLabelHandler,
|
||
|
labelTextInputHandler,
|
||
|
presetColorSelectHandler,
|
||
|
labelColorInputHandler
|
||
|
} from './user-labels/handlers';
|
||
|
|
||
|
let theme: typeof themeColors[ThemeKey];
|
||
|
|
||
|
(async (): Promise<void> => {
|
||
|
const settings: Settings = await getSettings();
|
||
|
if (!settings.features.userLabels) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const themeKey: ThemeKey = getCurrentThemeKey();
|
||
|
theme = themeColors[themeKey];
|
||
|
addLabelsToUsernames(settings);
|
||
|
const existingLabelForm: HTMLElement | null = document.querySelector(
|
||
|
'#trx-user-label-form'
|
||
|
);
|
||
|
if (existingLabelForm !== null) {
|
||
|
existingLabelForm.remove();
|
||
|
}
|
||
|
|
||
|
const themeSelectOptions: string[] = [];
|
||
|
for (const color in theme) {
|
||
|
if (Object.hasOwnProperty.call(theme, color)) {
|
||
|
themeSelectOptions.push(
|
||
|
`<option value="${theme[color as ColorKey]}">${color}</option>`
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const labelFormTemplate = `<form id="trx-user-label-form" class="trx-hidden">
|
||
|
<div>
|
||
|
<label for="trx-user-label-form-username">Add New Label</label>
|
||
|
<label for="trx-user-label-priority">Priority</label>
|
||
|
</div>
|
||
|
<div>
|
||
|
<input type="text" id="trx-user-label-form-username" class="form-input" placeholder="Username">
|
||
|
<input id="trx-user-label-priority" type="number" class="form-input" value="0">
|
||
|
</div>
|
||
|
<label>Pick A Color</label>
|
||
|
<div id="trx-user-label-form-color">
|
||
|
<input type="text" class="form-input" placeholder="Color">
|
||
|
<select class="form-select">
|
||
|
${themeSelectOptions.join('\n')}
|
||
|
</select>
|
||
|
</div>
|
||
|
<label>Label</label>
|
||
|
<div id="trx-user-label-input">
|
||
|
<input type="text" class="form-input" placeholder="Label">
|
||
|
<div id="trx-user-label-preview">
|
||
|
<p></p>
|
||
|
</div>
|
||
|
</div>
|
||
|
<div id="trx-user-label-actions">
|
||
|
<a id="trx-user-label-save" class="btn-post-action">Save</a>
|
||
|
<a id="trx-user-label-close" class="btn-post-action">Close</a>
|
||
|
<a id="trx-user-label-remove" class="btn-post-action">Remove</a>
|
||
|
</div>
|
||
|
</form>`;
|
||
|
const labelForm: HTMLFormElement = createElementFromString(labelFormTemplate);
|
||
|
document.body.append(labelForm);
|
||
|
labelForm.setAttribute(
|
||
|
'style',
|
||
|
`background-color: ${theme.background}; border-color: ${theme.foregroundAlt};`
|
||
|
);
|
||
|
|
||
|
const labelColorInput: HTMLInputElement = querySelector(
|
||
|
'#trx-user-label-form-color > input'
|
||
|
);
|
||
|
labelColorInput.addEventListener(
|
||
|
'keyup',
|
||
|
debounce(labelColorInputHandler, 250)
|
||
|
);
|
||
|
|
||
|
const presetColorSelect: HTMLSelectElement = querySelector(
|
||
|
'#trx-user-label-form-color > select'
|
||
|
);
|
||
|
presetColorSelect.addEventListener('change', presetColorSelectHandler);
|
||
|
presetColorSelect.value = theme.backgroundAlt;
|
||
|
|
||
|
const labelTextInput: HTMLInputElement = querySelector(
|
||
|
'#trx-user-label-input > input'
|
||
|
);
|
||
|
labelTextInput.addEventListener('keyup', labelTextInputHandler);
|
||
|
|
||
|
const labelPreview: HTMLDivElement = querySelector('#trx-user-label-preview');
|
||
|
labelPreview.setAttribute(
|
||
|
'style',
|
||
|
`background-color: ${theme.background};` +
|
||
|
`border-color: ${theme.foregroundAlt};`
|
||
|
);
|
||
|
|
||
|
const formSaveButton: HTMLAnchorElement = querySelector(
|
||
|
'#trx-user-label-save'
|
||
|
);
|
||
|
formSaveButton.addEventListener('click', saveUserLabel);
|
||
|
|
||
|
const formCloseButton: HTMLAnchorElement = querySelector(
|
||
|
'#trx-user-label-close'
|
||
|
);
|
||
|
formCloseButton.addEventListener('click', hideLabelForm);
|
||
|
|
||
|
const formRemoveButton: HTMLAnchorElement = querySelector(
|
||
|
'#trx-user-label-remove'
|
||
|
);
|
||
|
formRemoveButton.addEventListener('click', removeUserLabel);
|
||
|
|
||
|
const commentObserver = new window.MutationObserver(
|
||
|
async (mutations: MutationRecord[]): Promise<void> => {
|
||
|
const commentElements: HTMLElement[] = mutations
|
||
|
.map(val => val.target as HTMLElement)
|
||
|
.filter(
|
||
|
val =>
|
||
|
val.classList.contains('comment-itself') ||
|
||
|
val.classList.contains('comment')
|
||
|
);
|
||
|
if (commentElements.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
commentObserver.disconnect();
|
||
|
addLabelsToUsernames(await getSettings());
|
||
|
startObserver();
|
||
|
}
|
||
|
);
|
||
|
|
||
|
function startObserver(): void {
|
||
|
const topicComments: HTMLElement | null = document.querySelector(
|
||
|
'.topic-comments'
|
||
|
);
|
||
|
if (topicComments !== null) {
|
||
|
commentObserver.observe(topicComments, {
|
||
|
childList: true,
|
||
|
subtree: true
|
||
|
});
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const postListing: HTMLElement | null = document.querySelector(
|
||
|
'.post-listing'
|
||
|
);
|
||
|
if (postListing !== null) {
|
||
|
commentObserver.observe(postListing, {
|
||
|
childList: true,
|
||
|
subtree: true
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
startObserver();
|
||
|
})();
|
||
|
|
||
|
// TODO: Refactor this function to be able to only add labels to specific
|
||
|
// elements. At the moment it goes through all `.link-user` elements which is
|
||
|
// inefficient.
|
||
|
function addLabelsToUsernames(settings: Settings): void {
|
||
|
for (const element of [
|
||
|
...document.querySelectorAll('.trx-user-label'),
|
||
|
...document.querySelectorAll('.trx-user-label-add')
|
||
|
]) {
|
||
|
element.remove();
|
||
|
}
|
||
|
|
||
|
for (const element of document.querySelectorAll('.link-user')) {
|
||
|
const username: string = element
|
||
|
.textContent!.replace(/@/g, '')
|
||
|
.toLowerCase();
|
||
|
const addLabelSpan: HTMLSpanElement = createElementFromString(
|
||
|
`<span class="trx-user-label-add" data-trx-username="${username}">[+]</span>`
|
||
|
);
|
||
|
addLabelSpan.addEventListener('click', (event: MouseEvent): void =>
|
||
|
addLabelHandler(event, addLabelSpan)
|
||
|
);
|
||
|
if (!isInTopicListing()) {
|
||
|
element.insertAdjacentElement('afterend', addLabelSpan);
|
||
|
appendStyleAttribute(addLabelSpan, `color: ${theme.foreground};`);
|
||
|
}
|
||
|
|
||
|
const userLabels: UserLabel[] = settings.data.userLabels.filter(
|
||
|
val => val.username === username
|
||
|
);
|
||
|
if (userLabels.length === 0) {
|
||
|
if (
|
||
|
isInTopicListing() &&
|
||
|
(element.nextElementSibling === null ||
|
||
|
!element.nextElementSibling.className.includes('trx-user-label'))
|
||
|
) {
|
||
|
element.insertAdjacentElement('afterend', addLabelSpan);
|
||
|
appendStyleAttribute(addLabelSpan, `color: ${theme.foreground};`);
|
||
|
}
|
||
|
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (isInTopicListing()) {
|
||
|
userLabels.sort((a, b) => b.priority - a.priority);
|
||
|
} else {
|
||
|
userLabels.sort((a, b): number => {
|
||
|
if (a.priority !== b.priority) {
|
||
|
return a.priority - b.priority;
|
||
|
}
|
||
|
|
||
|
return b.text.localeCompare(a.text);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
for (const userLabel of userLabels) {
|
||
|
const userLabelSpan: HTMLSpanElement = createElementFromString(
|
||
|
`<span class="trx-user-label" data-trx-user-label-id="${userLabel.id}">${userLabel.text}</span>`
|
||
|
);
|
||
|
userLabelSpan.addEventListener(
|
||
|
'click',
|
||
|
async (event: MouseEvent): Promise<void> =>
|
||
|
editLabelHandler(event, userLabelSpan)
|
||
|
);
|
||
|
element.insertAdjacentElement('afterend', userLabelSpan);
|
||
|
// Set the inline-style after the element gets added to the DOM, this
|
||
|
// will prevent a CSP error saying inline-styles aren't permitted.
|
||
|
userLabelSpan.setAttribute(
|
||
|
'style',
|
||
|
`background-color: ${userLabel.color};`
|
||
|
);
|
||
|
if (isColorBright(userLabel.color)) {
|
||
|
userLabelSpan.classList.add('trx-bright');
|
||
|
} else {
|
||
|
userLabelSpan.classList.remove('trx-bright');
|
||
|
}
|
||
|
|
||
|
if (isInTopicListing()) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async function saveUserLabel(): Promise<void> {
|
||
|
const settings: Settings = await getSettings();
|
||
|
const labelForm: HTMLFormElement = getLabelForm();
|
||
|
const labelNoID: Except<UserLabel, 'id'> | undefined = getLabelFormValues();
|
||
|
if (typeof labelNoID === 'undefined') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const existingIDString: string | null = labelForm.getAttribute(
|
||
|
'data-trx-user-label-id'
|
||
|
);
|
||
|
if (existingIDString === null) {
|
||
|
settings.data.userLabels.push({
|
||
|
id: (await getHighestLabelID(settings)) + 1,
|
||
|
...labelNoID
|
||
|
});
|
||
|
await setSettings(settings);
|
||
|
hideLabelForm();
|
||
|
addLabelsToUsernames(settings);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const existingID = Number(existingIDString);
|
||
|
const existingLabel: UserLabel | undefined = settings.data.userLabels.find(
|
||
|
val => val.id === existingID
|
||
|
);
|
||
|
if (typeof existingLabel === 'undefined') {
|
||
|
log(`Tried to find label with ID that doesn't exist: ${existingID}`, true);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const existingLabelIndex: number = settings.data.userLabels.findIndex(
|
||
|
val => val.id === existingID
|
||
|
);
|
||
|
settings.data.userLabels.splice(existingLabelIndex, 1);
|
||
|
settings.data.userLabels.push({
|
||
|
id: existingID,
|
||
|
...labelNoID
|
||
|
});
|
||
|
await setSettings(settings);
|
||
|
hideLabelForm();
|
||
|
addLabelsToUsernames(settings);
|
||
|
}
|
||
|
|
||
|
async function removeUserLabel(): Promise<void> {
|
||
|
const labelNoID: Except<UserLabel, 'id'> | undefined = getLabelFormValues();
|
||
|
if (typeof labelNoID === 'undefined') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const id: number | undefined = getLabelFormID();
|
||
|
if (typeof id === 'undefined') {
|
||
|
log('Attempted to remove user label without an ID.');
|
||
|
hideLabelForm();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const settings: Settings = await getSettings();
|
||
|
const labelIndex: number = settings.data.userLabels.findIndex(
|
||
|
val => val.id === id
|
||
|
);
|
||
|
if (typeof findLabelByID(id) === 'undefined') {
|
||
|
log(
|
||
|
`Attempted to remove user label with an ID that doesn't exist ${id}.`,
|
||
|
true
|
||
|
);
|
||
|
hideLabelForm();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
settings.data.userLabels.splice(labelIndex, 1);
|
||
|
await setSettings(settings);
|
||
|
hideLabelForm();
|
||
|
addLabelsToUsernames(settings);
|
||
|
}
|
||
|
|
||
|
export async function findLabelByID(
|
||
|
id: number,
|
||
|
settings?: Settings
|
||
|
): Promise<UserLabel | undefined> {
|
||
|
if (typeof settings === 'undefined') {
|
||
|
settings = await getSettings();
|
||
|
}
|
||
|
|
||
|
return settings.data.userLabels.find(val => val.id === id);
|
||
|
}
|
||
|
|
||
|
async function getHighestLabelID(settings?: Settings): Promise<number> {
|
||
|
if (typeof settings === 'undefined') {
|
||
|
settings = await getSettings();
|
||
|
}
|
||
|
|
||
|
if (settings.data.userLabels.length === 0) {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
return Math.max(...settings.data.userLabels.map(val => val.id));
|
||
|
}
|