// utility function written by: Michał Perłakowski
// (https://stackoverflow.com/users/3853934/)
// taken from:
// https://stackoverflow.com/a/39977764/82548
// used because the behaviour of Object.assign() doesn't
// work well in merging objects with unspecifed values/keys:
const assign = (target, ...sources) =>
Object.assign(target, ...sources.map(x =>
Object.entries(x)
.filter(([key, value]) => value !== undefined)
.reduce((obj, [key, value]) => (obj[key] = value, obj), {})
));
// new named function, set up in the same way as above,
// albeit with new arguments:
function insertTextIntoAttribute(opts = {}) {
let defaults = {
attribute: 'id',
elements: 'a',
// Boolean, do you wish to insert the new String
// at the end of the current value?
endWith: false,
// String, the string you wish to insert:
insert: 'user-content-',
// Boolean, do you wish to insert the new String
// at the start of the current value?
startWith: true,
},
settings = assign({}, defaults, opts);
const {
elements,
attribute,
insert,
startWith,
endWith
} = settings,
// using a template literal to create a simple selector
// to find the elements that match your requirements:
selectorString = `${elements}`;
// using document.querySelectorAll() to retrieve a
// NodeList of elements that match the selector
// passed to the function:
const haystack = document.querySelectorAll(
selectorString
);
// NodeList.prototype.forEach() to iterate over the
// returned NodeList:
haystack.forEach(
(el) => {
// we retrieve the current attribute-value of the
// relevant element:
let currentValue = el.getAttribute(attribute),
// because a hash requires some special consideration
// (the '#' character has to be at the beginning) we
// initialise this variable to false:
isHash = false;
// we use Element.matches to see if the current element
// of the NodeList is an <a> element (we could have instead
// used el.tagName === 'A') but Element.matches is
// more concise, easier to read and doesn't require a
// comparison), it is we then check if the current attribute-
// value matches the <a> element's hash:
if (el.matches('a') && currentValue === el.hash) {
// if it does we then update the isHash variable to true:
isHash = true;
}
// here we use Element.setAttribute() to update the named
// attribute (first argument) to a new value:
el.setAttribute(attribute,
// this is perhaps a little confusing to read, as we're
// taking advantage of Template strings' ability to
// interpolate a variable into the String, and we're
// using conditional operators to do so. In order:
// 1. ${isHash ? '#' : ''}
// we test isHash; if true/truthy
// the expression returns the '#' character, if false/falsey
// the expression returns the empty String ''.
// 2. ${startWith ? insert : ''}
// we test startWith; if true/truthy the expression returns
// the 'insert' variable's value, otherwise if startWith is
// false/falsey the expression returns the empty-string.
// 3. ${isHash ? currentValue.replace('#','')
// here we again test the isHash variable, and if true/truthy
// the expression returns the result of calling
// String.prototype.replace() on the current attribute-value
// of the element; if isHash is false/falsey then it simply
// returns the current attribute-value.
// 4. ${endWith ? insert : ''}
// this is exactly the same as the earlier assessment for
// the startWith variable, if true/truthy we return the
// content of the insert variable, otherwise if false/falsey
// we return an empty String:
`${isHash ? '#' : ''}${startWith ? insert : ''}${isHash ? currentValue.replace('#','') : currentValue}${endWith ? insert : ''}`
);
});
}
// here we call the function, specifying our
// options:
insertTextIntoAttribute({
// we wish to modify the 'href' attribute:
attribute: 'href',
// and we're selecing the <a> elements inside of <li> elements
// inside of the <nav> element:
elements: 'nav li a',
});
:root {
--color: #000f;
--backgroundColor: #fff;
}
*,
::before,
::after {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: sans-serif;
line-height: 1.5;
}
nav ul {
display: flex;
justify-content: space-around;
list-style-type: none;
}
nav a:is(:link, :visited) {
color: var(--color);
background-color: var(--backgroundColor);
}
nav a:is(:hover, :active, :focus) {
color: var(--backgroundColor);
background-color: var(--color);
}
h2 {
margin-block: 3em;
}
h2 a:is(:link, :visited) {
background: linear-gradient(90deg, lime, #ffff);
display: block;
color: var(--color);
text-decoration: none;
}
h2 a:is(:hover, :active, :focus) {
text-decoration: underline;
text-decoration-thickness: 3px;
}
h2 a:target {
background: linear-gradient(90deg, #f90, #ffff);
}
h2 a::after {
content: ' (#' attr(id) ').';
}
<nav id="toc">
<ul>
<li><a href="#test1">Link to "test1"</a></li>
<li><a href="#best2">Link to "best2"</a></li>
<li><a href="#nest3">Link to "nest3"</a></li>
<li><a href="#rest4">Link to "rest4"</a></li>
</ul>
</nav>
<h2>
<a id="user-content-test1" href="https://www.example.com">
Anything
</a>
</h2>
<h2>
<a id="user-content-best2" href="https://www.example.com">
Anything
</a>
</h2>
<h2>
<a id="user-content-nest3" href="https://www.example.com">
Anything
</a>
</h2>
<h2>
<a id="user-content-rest4" href="https://www.example.com">
Anything
</a>
</h2>