Compare commits
2 Commits
84c69eaab3
...
b8ff891c31
Author | SHA1 | Date |
---|---|---|
Bauke | b8ff891c31 | |
Bauke | 2666168b02 |
|
@ -0,0 +1,70 @@
|
|||
import {log, pluralize} from "../../utilities/exports.js";
|
||||
|
||||
export function runCommentAnchorFixFeature(): void {
|
||||
const count = commentAnchorFix();
|
||||
if (count > 0) {
|
||||
const pluralized = `${count} ${pluralize(count, "comment")}`;
|
||||
log(`Comment Anchor Fix applied, uncollapsed ${pluralized}.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the comment anchor fix, uncollapsing any collapsed comments and
|
||||
* scrolling the linked comment into view.
|
||||
*/
|
||||
function commentAnchorFix(): number {
|
||||
const anchor = window.location.hash;
|
||||
|
||||
// Linked comments follow the `#comment-<base 36 ID>` pattern.
|
||||
if (!/^#comment-[a-z\d]+$/i.test(anchor)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Conveniently, the anchor including the leading hash is a valid CSS selector
|
||||
// for the target comment, so we can directly select it.
|
||||
const targetComment =
|
||||
document.querySelector<HTMLElement>(anchor) ?? undefined;
|
||||
if (targetComment === undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const count = recursiveUncollapseParent(
|
||||
// Start from the comment's first child so the target comment also has the
|
||||
// collapse classes removed. The collapse styling accomodates for `:target`
|
||||
// so this isn't technically necessary, but this way we make sure it stays
|
||||
// uncollapsed even if the `:target` changes. It also makes the returned
|
||||
// count correct as otherwise this comment wouldn't be included.
|
||||
targetComment.firstElementChild ?? targetComment,
|
||||
0,
|
||||
);
|
||||
if (count > 0) {
|
||||
targetComment.scrollIntoView({behavior: "smooth"});
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively go up the chain of comments uncollapsing each one until it
|
||||
* reaches the `ol#comments` list or no parents remain, returning how many
|
||||
* comments have been uncollapsed.
|
||||
* @param target The current target to select its parent from.
|
||||
*/
|
||||
function recursiveUncollapseParent(target: Element, count: number): number {
|
||||
const parent = target.parentElement ?? undefined;
|
||||
if (parent === undefined || parent.id === "comments") {
|
||||
return count;
|
||||
}
|
||||
|
||||
for (const collapsedClass of [
|
||||
"is-comment-collapsed",
|
||||
"is-comment-collapsed-individual",
|
||||
]) {
|
||||
if (parent.classList.contains(collapsedClass)) {
|
||||
parent.classList.remove(collapsedClass);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return recursiveUncollapseParent(parent, count);
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
export * from "./anonymize-usernames.js";
|
||||
export * from "./autocomplete.js";
|
||||
export * from "./back-to-top.js";
|
||||
export * from "./comment-anchor-fix.js";
|
||||
export * from "./hide-votes.js";
|
||||
export * from "./jump-to-new-comment.js";
|
||||
export * from "./markdown-toolbar.js";
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import {type JSX, render} from "preact";
|
||||
import {extractGroups, initializeGlobals, log} from "../utilities/exports.js";
|
||||
import {Feature, fromStorage, Data} from "../storage/exports.js";
|
||||
import {
|
||||
Data,
|
||||
Feature,
|
||||
MiscellaneousFeature,
|
||||
fromStorage,
|
||||
} from "../storage/exports.js";
|
||||
import {
|
||||
AutocompleteFeature,
|
||||
BackToTopFeature,
|
||||
JumpToNewCommentFeature,
|
||||
UserLabelsFeature,
|
||||
runAnonymizeUsernamesFeature,
|
||||
runCommentAnchorFixFeature,
|
||||
runHideTopicsFeature,
|
||||
runHideVotesFeature,
|
||||
runMarkdownToolbarFeature,
|
||||
|
@ -106,6 +112,11 @@ async function initialize() {
|
|||
});
|
||||
}
|
||||
|
||||
const miscEnabled = await fromStorage(Data.MiscellaneousEnabledFeatures);
|
||||
if (miscEnabled.value.has(MiscellaneousFeature.CommentAnchorFix)) {
|
||||
runCommentAnchorFixFeature();
|
||||
}
|
||||
|
||||
// Initialize all the observer-dependent features first.
|
||||
await Promise.all(observerFeatures.map(async (feature) => feature()));
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ export {HideTopicsSetting} from "./hide-topics.js";
|
|||
export {HideVotesSetting} from "./hide-votes.js";
|
||||
export {JumpToNewCommentSetting} from "./jump-to-new-comment.js";
|
||||
export {MarkdownToolbarSetting} from "./markdown-toolbar.js";
|
||||
export {MiscellaneousSetting} from "./miscellaneous.js";
|
||||
export {ThemedLogoSetting} from "./themed-logo.js";
|
||||
export {UserLabelsSetting} from "./user-labels.js";
|
||||
export {UsernameColorsSetting} from "./username-colors.js";
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
import {Component, type JSX} from "preact";
|
||||
import {
|
||||
type StorageValues,
|
||||
fromStorage,
|
||||
Data,
|
||||
MiscellaneousFeature,
|
||||
} from "../../storage/exports.js";
|
||||
import {Setting, type SettingProps} from "./index.js";
|
||||
|
||||
type State = {
|
||||
enabledFeatures: Awaited<StorageValues[Data.MiscellaneousEnabledFeatures]>;
|
||||
};
|
||||
|
||||
function FeatureDescription({
|
||||
feature,
|
||||
}: {
|
||||
feature: MiscellaneousFeature;
|
||||
}): JSX.Element {
|
||||
if (feature === MiscellaneousFeature.CommentAnchorFix) {
|
||||
return (
|
||||
<p class="description">
|
||||
Uncollapses the linked-to comment if it is collapsed,{" "}
|
||||
<a href="https://gitlab.com/tildes/tildes/-/issues/256">#256</a>.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export class MiscellaneousSetting extends Component<SettingProps, State> {
|
||||
constructor(props: SettingProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
enabledFeatures: undefined!,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this.setState({
|
||||
enabledFeatures: await fromStorage(Data.MiscellaneousEnabledFeatures),
|
||||
});
|
||||
}
|
||||
|
||||
toggleFeature = async (feature: MiscellaneousFeature) => {
|
||||
const {enabledFeatures} = this.state;
|
||||
if (enabledFeatures.value.has(feature)) {
|
||||
enabledFeatures.value.delete(feature);
|
||||
} else {
|
||||
enabledFeatures.value.add(feature);
|
||||
}
|
||||
|
||||
this.setState({enabledFeatures});
|
||||
await enabledFeatures.save();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {enabledFeatures} = this.state;
|
||||
if (enabledFeatures === undefined) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const checkboxes = Object.values(MiscellaneousFeature).map((feature) => {
|
||||
const enabled = enabledFeatures.value.has(feature);
|
||||
const clickHandler = async () => {
|
||||
await this.toggleFeature(feature);
|
||||
};
|
||||
|
||||
return (
|
||||
<li class={enabled ? "enabled" : ""}>
|
||||
<label for={feature}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={feature}
|
||||
name={feature}
|
||||
checked={enabled}
|
||||
onClick={clickHandler}
|
||||
/>
|
||||
{feature
|
||||
.replace(/-/g, " ")
|
||||
.replace(/(\b[a-z])/gi, (character) => character.toUpperCase())}
|
||||
</label>
|
||||
<FeatureDescription feature={feature} />
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Setting {...this.props}>
|
||||
<p class="info">
|
||||
Miscellaneous features and fixes, each one can be toggled individually
|
||||
by checking their respective checkbox.
|
||||
</p>
|
||||
|
||||
<ul class="miscellaneous-features-list">{checkboxes}</ul>
|
||||
</Setting>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -185,9 +185,9 @@ export class UsernameColorsSetting extends Component<SettingProps, State> {
|
|||
You can enter multiple usernames separated by a comma if you want them
|
||||
to use the same color.
|
||||
<br />
|
||||
If randomize is selected then all usernames will be given a random
|
||||
background color. This will not override colors you have manually
|
||||
assigned.
|
||||
If randomize is enabled then all usernames will be given a random
|
||||
background color based on a hash of the username. Manually assigned
|
||||
colors will be applied normally.
|
||||
</p>
|
||||
|
||||
<div class="username-colors-controls">
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
HideVotesSetting,
|
||||
JumpToNewCommentSetting,
|
||||
MarkdownToolbarSetting,
|
||||
MiscellaneousSetting,
|
||||
ThemedLogoSetting,
|
||||
UserLabelsSetting,
|
||||
UsernameColorsSetting,
|
||||
|
@ -71,6 +72,13 @@ export const features: FeatureData[] = [
|
|||
title: "Markdown Toolbar",
|
||||
component: MarkdownToolbarSetting,
|
||||
},
|
||||
{
|
||||
availableSince: new Date("2023-07-16"),
|
||||
index: 0,
|
||||
key: Feature.Miscellaneous,
|
||||
title: "Miscellaneous",
|
||||
component: MiscellaneousSetting,
|
||||
},
|
||||
{
|
||||
availableSince: new Date("2022-02-27"),
|
||||
index: 0,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
@import "shared";
|
||||
@import "settings";
|
||||
@import "settings/hide-topics";
|
||||
@import "settings/miscellaneous-features";
|
||||
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
.miscellaneous-features-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
--border-color: var(--red);
|
||||
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
&.enabled {
|
||||
--border-color: var(--green);
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
|
@ -10,18 +10,27 @@ export enum Feature {
|
|||
HideVotes = "hide-votes",
|
||||
JumpToNewComment = "jump-to-new-comment",
|
||||
MarkdownToolbar = "markdown-toolbar",
|
||||
Miscellaneous = "miscellaneous-features",
|
||||
ThemedLogo = "themed-logo",
|
||||
UserLabels = "user-labels",
|
||||
UsernameColors = "username-colors",
|
||||
}
|
||||
|
||||
/**
|
||||
* Keys of miscellaneous data stored in WebExtension storage.
|
||||
* Keys of miscellaneous feature names.
|
||||
*/
|
||||
export enum MiscellaneousFeature {
|
||||
CommentAnchorFix = "comment-anchor-fix",
|
||||
}
|
||||
|
||||
/**
|
||||
* Keys of data stored in WebExtension storage.
|
||||
*/
|
||||
export enum Data {
|
||||
EnabledFeatures = "enabled-features",
|
||||
KnownGroups = "known-groups",
|
||||
LatestActiveFeatureTab = "latest-active-feature-tab",
|
||||
MiscellaneousEnabledFeatures = "miscellaneous-enabled-features",
|
||||
RandomizeUsernameColors = "randomize-username-colors",
|
||||
Version = "data-version",
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {createValue, type Value} from "@holllo/webextension-storage";
|
||||
import browser from "webextension-polyfill";
|
||||
import {Data, Feature} from "./enums.js";
|
||||
import {Data, Feature, MiscellaneousFeature} from "./enums.js";
|
||||
import {collectHideTopicsData} from "./hide-topics.js";
|
||||
import {defaultKnownGroups} from "./known-groups.js";
|
||||
import {collectUsernameColors} from "./username-color.js";
|
||||
|
@ -51,6 +51,14 @@ export const storageValues = {
|
|||
value: Feature.Debug,
|
||||
storage: browser.storage.sync,
|
||||
}),
|
||||
[Data.MiscellaneousEnabledFeatures]: createValue({
|
||||
deserialize: (input) =>
|
||||
new Set(JSON.parse(input) as MiscellaneousFeature[]),
|
||||
serialize: (input) => JSON.stringify(Array.from(input)),
|
||||
key: Data.MiscellaneousEnabledFeatures,
|
||||
value: new Set([MiscellaneousFeature.CommentAnchorFix]),
|
||||
storage: browser.storage.sync,
|
||||
}),
|
||||
[Data.Version]: createValue({
|
||||
deserialize: (input) => JSON.parse(input) as string,
|
||||
serialize: (input) => JSON.stringify(input),
|
||||
|
@ -85,7 +93,7 @@ export const storageValues = {
|
|||
/**
|
||||
* Shorthand for the inferred shape of {@link storageValues}.
|
||||
*/
|
||||
type StorageValues = typeof storageValues;
|
||||
export type StorageValues = typeof storageValues;
|
||||
|
||||
/**
|
||||
* Return the {@link Value}-wrapped data associated with a particular key.
|
||||
|
|
Loading…
Reference in New Issue