Add the Miscellaneous features category with the Comment Anchor Fix to start.
This commit is contained in:
parent
84c69eaab3
commit
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 "./anonymize-usernames.js";
|
||||||
export * from "./autocomplete.js";
|
export * from "./autocomplete.js";
|
||||||
export * from "./back-to-top.js";
|
export * from "./back-to-top.js";
|
||||||
|
export * from "./comment-anchor-fix.js";
|
||||||
export * from "./hide-votes.js";
|
export * from "./hide-votes.js";
|
||||||
export * from "./jump-to-new-comment.js";
|
export * from "./jump-to-new-comment.js";
|
||||||
export * from "./markdown-toolbar.js";
|
export * from "./markdown-toolbar.js";
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
import {type JSX, render} from "preact";
|
import {type JSX, render} from "preact";
|
||||||
import {extractGroups, initializeGlobals, log} from "../utilities/exports.js";
|
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 {
|
import {
|
||||||
AutocompleteFeature,
|
AutocompleteFeature,
|
||||||
BackToTopFeature,
|
BackToTopFeature,
|
||||||
JumpToNewCommentFeature,
|
JumpToNewCommentFeature,
|
||||||
UserLabelsFeature,
|
UserLabelsFeature,
|
||||||
runAnonymizeUsernamesFeature,
|
runAnonymizeUsernamesFeature,
|
||||||
|
runCommentAnchorFixFeature,
|
||||||
runHideTopicsFeature,
|
runHideTopicsFeature,
|
||||||
runHideVotesFeature,
|
runHideVotesFeature,
|
||||||
runMarkdownToolbarFeature,
|
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.
|
// Initialize all the observer-dependent features first.
|
||||||
await Promise.all(observerFeatures.map(async (feature) => feature()));
|
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 {HideVotesSetting} from "./hide-votes.js";
|
||||||
export {JumpToNewCommentSetting} from "./jump-to-new-comment.js";
|
export {JumpToNewCommentSetting} from "./jump-to-new-comment.js";
|
||||||
export {MarkdownToolbarSetting} from "./markdown-toolbar.js";
|
export {MarkdownToolbarSetting} from "./markdown-toolbar.js";
|
||||||
|
export {MiscellaneousSetting} from "./miscellaneous.js";
|
||||||
export {ThemedLogoSetting} from "./themed-logo.js";
|
export {ThemedLogoSetting} from "./themed-logo.js";
|
||||||
export {UserLabelsSetting} from "./user-labels.js";
|
export {UserLabelsSetting} from "./user-labels.js";
|
||||||
export {UsernameColorsSetting} from "./username-colors.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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import {
|
||||||
HideVotesSetting,
|
HideVotesSetting,
|
||||||
JumpToNewCommentSetting,
|
JumpToNewCommentSetting,
|
||||||
MarkdownToolbarSetting,
|
MarkdownToolbarSetting,
|
||||||
|
MiscellaneousSetting,
|
||||||
ThemedLogoSetting,
|
ThemedLogoSetting,
|
||||||
UserLabelsSetting,
|
UserLabelsSetting,
|
||||||
UsernameColorsSetting,
|
UsernameColorsSetting,
|
||||||
|
@ -71,6 +72,13 @@ export const features: FeatureData[] = [
|
||||||
title: "Markdown Toolbar",
|
title: "Markdown Toolbar",
|
||||||
component: MarkdownToolbarSetting,
|
component: MarkdownToolbarSetting,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
availableSince: new Date("2023-07-16"),
|
||||||
|
index: 0,
|
||||||
|
key: Feature.Miscellaneous,
|
||||||
|
title: "Miscellaneous",
|
||||||
|
component: MiscellaneousSetting,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
availableSince: new Date("2022-02-27"),
|
availableSince: new Date("2022-02-27"),
|
||||||
index: 0,
|
index: 0,
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
@import "shared";
|
@import "shared";
|
||||||
@import "settings";
|
@import "settings";
|
||||||
@import "settings/hide-topics";
|
@import "settings/hide-topics";
|
||||||
|
@import "settings/miscellaneous-features";
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 62.5%;
|
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",
|
HideVotes = "hide-votes",
|
||||||
JumpToNewComment = "jump-to-new-comment",
|
JumpToNewComment = "jump-to-new-comment",
|
||||||
MarkdownToolbar = "markdown-toolbar",
|
MarkdownToolbar = "markdown-toolbar",
|
||||||
|
Miscellaneous = "miscellaneous-features",
|
||||||
ThemedLogo = "themed-logo",
|
ThemedLogo = "themed-logo",
|
||||||
UserLabels = "user-labels",
|
UserLabels = "user-labels",
|
||||||
UsernameColors = "username-colors",
|
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 {
|
export enum Data {
|
||||||
EnabledFeatures = "enabled-features",
|
EnabledFeatures = "enabled-features",
|
||||||
KnownGroups = "known-groups",
|
KnownGroups = "known-groups",
|
||||||
LatestActiveFeatureTab = "latest-active-feature-tab",
|
LatestActiveFeatureTab = "latest-active-feature-tab",
|
||||||
|
MiscellaneousEnabledFeatures = "miscellaneous-enabled-features",
|
||||||
RandomizeUsernameColors = "randomize-username-colors",
|
RandomizeUsernameColors = "randomize-username-colors",
|
||||||
Version = "data-version",
|
Version = "data-version",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {createValue, type Value} from "@holllo/webextension-storage";
|
import {createValue, type Value} from "@holllo/webextension-storage";
|
||||||
import browser from "webextension-polyfill";
|
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 {collectHideTopicsData} from "./hide-topics.js";
|
||||||
import {defaultKnownGroups} from "./known-groups.js";
|
import {defaultKnownGroups} from "./known-groups.js";
|
||||||
import {collectUsernameColors} from "./username-color.js";
|
import {collectUsernameColors} from "./username-color.js";
|
||||||
|
@ -51,6 +51,14 @@ export const storageValues = {
|
||||||
value: Feature.Debug,
|
value: Feature.Debug,
|
||||||
storage: browser.storage.sync,
|
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({
|
[Data.Version]: createValue({
|
||||||
deserialize: (input) => JSON.parse(input) as string,
|
deserialize: (input) => JSON.parse(input) as string,
|
||||||
serialize: (input) => JSON.stringify(input),
|
serialize: (input) => JSON.stringify(input),
|
||||||
|
@ -85,7 +93,7 @@ export const storageValues = {
|
||||||
/**
|
/**
|
||||||
* Shorthand for the inferred shape of {@link 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.
|
* Return the {@link Value}-wrapped data associated with a particular key.
|
||||||
|
|
Loading…
Reference in New Issue