Skip to content

Commit

Permalink
Merge branch 'main' into no-canonifyurls
Browse files Browse the repository at this point in the history
  • Loading branch information
zecakeh committed Nov 13, 2024
2 parents e9914bf + b1f66d1 commit 1bc409e
Show file tree
Hide file tree
Showing 16 changed files with 166 additions and 120 deletions.
225 changes: 119 additions & 106 deletions assets/js/toc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,137 +15,150 @@ limitations under the License.
*/

/*
Set a new ToC entry.
Clear any previously highlighted ToC items, set the new one,
and adjust the ToC scroll position.
Only call the given function once every 250 milliseconds to avoid impacting
the performance of the browser.
Source: https://remysharp.com/2010/07/21/throttling-function-calls
*/
function setTocEntry(newEntry) {
const activeEntries = document.querySelectorAll("#toc a.active");
for (const activeEntry of activeEntries) {
activeEntry.classList.remove('active');
}

newEntry.classList.add('active');
// don't scroll the sidebar nav if the main content is not scrolled
const nav = document.querySelector("#td-section-nav");
const content = document.querySelector("html");
if (content.scrollTop !== 0) {
nav.scrollTop = newEntry.offsetTop - 100;
} else {
nav.scrollTop = 0;
function throttle(fn) {
const threshold = 250;
let last = null;
let deferTimer = null;

return function (...args) {
const now = new Date();

if (last && now < last + threshold) {
// Hold on to it.
clearTimeout(deferTimer);
deferTimer = setTimeout(() => {
last = now;
fn.apply(this, args);
}, threshold);
} else {
last = now;
fn.apply(this, args);
}
}
}

/*
Test whether a node is in the viewport
Get the list of headings that appear in the ToC.
This is not as simple as querying all the headings in the content, because
some headings are not rendered in the ToC (e.g. in the endpoint definitions).
*/
function isInViewport(node) {
const rect = node.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
function getHeadings() {
let headings = [];

// First get the anchors in the ToC.
const toc_anchors = document.querySelectorAll("#toc nav a");

for (const anchor of toc_anchors) {
// Then get the heading from its selector in the anchor's href.
const selector = anchor.getAttribute("href");
if (!selector) {
console.error("Got ToC anchor without href");
continue;
}

const heading = document.querySelector(selector);
if (!heading) {
console.error("Heading not found for selector:", selector);
continue;
}

headings.push(heading);
}

return headings;
}

/*
The callback we pass to the IntersectionObserver constructor.
Called when any of our observed nodes starts or stops intersecting
with the viewport.
Get the heading of the text visible at the top of the viewport.
This is the first heading above or at the top of the viewport.
*/
function handleIntersectionUpdate(entries) {

/*
Special case: If the current URL hash matches a ToC entry, and
the corresponding heading is visible in the viewport, then that is
made the current ToC entry, and we don't even look at the intersection
observer data.
This means that if the user has clicked on a ToC entry,
we won't unselect it through the intersection observer.
*/
const hash = document.location.hash;
if (hash) {
let tocEntryForHash = document.querySelector(`nav li a[href="${hash}"]`);
// if the hash isn't a direct match for a ToC item, check the data attributes
if (!tocEntryForHash) {
const fragment = hash.substring(1);
tocEntryForHash = document.querySelector(`nav li a[data-${fragment}]`);
function getCurrentHeading(headings, headerOffset) {
const scrollTop = document.documentElement.scrollTop;
let prevHeading = null;
let currentHeading = null;
let index = 0;

for (const heading of headings) {
// Compute the position compared to the viewport.
const rect = heading.getBoundingClientRect();

if (rect.top >= headerOffset && rect.top <= headerOffset + 30) {
// This heading is at the top of the viewport, this is the current heading.
currentHeading = heading;
break;
}
if (tocEntryForHash) {
const headingForHash = document.querySelector(hash);
if (headingForHash && isInViewport(headingForHash)) {
setTocEntry(tocEntryForHash);
return;
if (rect.top >= headerOffset) {
// This is in or below the viewport, the current heading should be the
// previous one.
if (prevHeading) {
currentHeading = prevHeading;
} else {
// The first heading does not have a prevHeading.
currentHeading = heading;
}
break;
}

prevHeading = heading;
index += 1;
}

let newEntry = null;

for (const entry of entries) {
if (entry.intersectionRatio > 0) {
const heading = entry.target;
/*
This sidebar nav consists of two sections:
* at the top, a sitenav containing links to other pages
* under that, the ToC for the current page
Since the sidebar scrolls to match the document position,
the sitenav will tend to scroll off the screen.
If the user has scrolled up to (or near) the top of the page,
we want to show the sitenav so.
So: if the H1 (title) for the current page has started
intersecting, then always scroll the sidebar back to the top.
*/
if (heading.tagName === "H1" && heading.parentNode.tagName === "DIV") {
const nav = document.querySelector("#td-section-nav");
nav.scrollTop = 0;
return;
}
/*
Otherwise, get the ToC entry for the first entry that
entered the viewport, if there was one.
*/
const id = entry.target.getAttribute('id');
let tocEntry = document.querySelector(`nav li a[href="#${id}"]`);
// if the id isn't a direct match for a ToC item,
// check the ToC entry's `data-*` attributes
if (!tocEntry) {
tocEntry = document.querySelector(`nav li a[data-${id}]`);
}
if (tocEntry && !newEntry) {
newEntry = tocEntry;
}
}
return currentHeading;
}

/*
Select the ToC entry that points to the given ID.
Clear any previously highlighted ToC items, select the new one,
and adjust the ToC scroll position.
*/
function selectTocEntry(id) {
// Deselect previously selected entries.
const activeEntries = document.querySelectorAll("#toc nav a.active");
for (const activeEntry of activeEntries) {
activeEntry.classList.remove('active');
}

if (newEntry) {
setTocEntry(newEntry);
// Find the new entry and select it.
const newEntry = document.querySelector(`#toc nav a[href="#${id}"]`);
if (!newEntry) {
console.error("ToC entry not found for ID:", id);
return;
}
newEntry.classList.add('active');

// Don't scroll the sidebar nav if the main content is not scrolled
const nav = document.querySelector("#td-section-nav");
const content = document.querySelector("html");
if (content.scrollTop !== 0) {
nav.scrollTop = newEntry.offsetTop - 100;
} else {
nav.scrollTop = 0;
}
}

/*
Track when headings enter the viewport, and use this to update the highlight
for the corresponding ToC entry.
Track when the view is scrolled, and use this to update the highlight for the
corresponding ToC entry.
*/
window.addEventListener('DOMContentLoaded', () => {

const toc = document.querySelector("#toc");
toc.addEventListener("click", event => {
if (event.target.tagName === "A") {
setTocEntry(event.target);
}
// Part of the viewport is below the header so we should take it into account.
const headerOffset = document.querySelector("body > header > nav").clientHeight;
const headings = getHeadings();

const onScroll = throttle((_e) => {
// Update the ToC.
let heading = getCurrentHeading(headings, headerOffset);
selectTocEntry(heading.id);
});

const observer = new IntersectionObserver(handleIntersectionUpdate);

document.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((section) => {
observer.observe(section);
});
// Initialize the state of the ToC.
onScroll();

// Listen to scroll and resizing changes.
document.addEventListener('scroll', onScroll, false);
document.addEventListener('resize', onScroll, false);
});
1 change: 1 addition & 0 deletions changelogs/client_server/newsfragments/1947.clarification
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Correct OpenAPI specification for query parameters to `GET /_matrix/client/v3/thirdparty/location/{protocol}` endpoint.
1 change: 1 addition & 0 deletions changelogs/client_server/newsfragments/1978.clarification
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Clarify formats of string types.
1 change: 1 addition & 0 deletions changelogs/client_server/newsfragments/1979.clarification
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Clarify formats of string types.
1 change: 1 addition & 0 deletions changelogs/client_server/newsfragments/1980.clarification
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Clarify formats of string types.
1 change: 1 addition & 0 deletions changelogs/client_server/newsfragments/1983.clarification
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Clarify that the async upload endpoint will return 404 in some cases.
1 change: 1 addition & 0 deletions changelogs/internal/newsfragments/1991.clarification
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve the JS script to highlight the current ToC entry.
1 change: 1 addition & 0 deletions changelogs/push_gateway/newsfragments/1974.clarification
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The path of HTTP pusher URLs is fixed to `/_matrix/push/v1/notify`.
19 changes: 19 additions & 0 deletions data/api/client-server/content-repo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,25 @@ paths:
"errcode": "M_FORBIDDEN",
"error": "Cannot upload this content"
}
"404":
description: |-
The user has provided an invalid MXC ID. Some reasons for this error include:
- The MXC ID was not created with [POST /_matrix/media/v1/create](/client-server-api/#post_matrixmediav1create).
- The MXC ID has expired.
A [standard error response](/client-server-api/#standard-error-response)
will be returned with the `errcode` `M_NOT_FOUND`.
content:
application/json:
schema:
$ref: definitions/errors/error.yaml
examples:
response:
value: {
"errcode": "M_NOT_FOUND",
"error": "Unknown media ID"
}
"409":
description: |-
The endpoint was called with a media ID that already has content. A
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ properties:
arbitrary string with no specified maximum length.
url:
type: string
format: uri
description: |
A link to the text of this document, in the appropriate
language. MUST be a valid URI with scheme `https://` or
Expand Down
3 changes: 3 additions & 0 deletions data/api/client-server/login.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ paths:
properties:
user_id:
type: string
format: mx-user-id
pattern: "^@"
description: The fully-qualified Matrix ID for the account.
access_token:
type: string
Expand All @@ -197,6 +199,7 @@ paths:
x-addedInMatrixVersion: "1.3"
home_server:
type: string
format: mx-server-name
deprecated: true
description: |-
The server_name of the homeserver on which the account has
Expand Down
4 changes: 4 additions & 0 deletions data/api/client-server/support.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ paths:
properties:
matrix_id:
type: string
format: mx-user-id
pattern: "^@"
description: |-
A [Matrix User ID](/appendices/#user-identifiers)
representing the administrator.
Expand All @@ -66,6 +68,7 @@ paths:
required.
email_address:
type: string
format: email
description: |-
An email address to reach the administrator.
Expand Down Expand Up @@ -95,6 +98,7 @@ paths:
}
support_page:
type: string
format: uri
description: |-
The URL of a page to give users help specific to the
homeserver, like extra login/registration steps.
Expand Down
6 changes: 4 additions & 2 deletions data/api/client-server/third_party_lookup.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,14 @@ paths:
schema:
type: string
- in: query
name: searchFields
name: fields
description: |-
One or more custom fields to help identify the third-party
location.
schema:
type: string
type: object
additionalProperties:
type: string
responses:
"200":
description: At least one portal room was found.
Expand Down
6 changes: 3 additions & 3 deletions data/api/push-gateway/push_notifier.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ paths:
updating counts of unread notifications should be idempotent and
therefore do not require duplicate suppression.
Notifications are sent to the URL configured when the pusher is created.
This means that the HTTP path may be different depending on the push
gateway.
Clients interested in receiving notifications via this endpoint MUST
configure its full URI when creating the associated pusher via
[`/_matrix/client/v3/pushers/set`](/client-server-api/#post_matrixclientv3pushersset).
operationId: notify
requestBody:
content:
Expand Down
4 changes: 4 additions & 0 deletions data/string-formats.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,7 @@ mx-mxc-uri:
uri:
title: URI
url: https://datatracker.ietf.org/doc/html/rfc3986

email:
title: Email Address
url: https://datatracker.ietf.org/doc/html/rfc5321#section-4.1.2
Loading

0 comments on commit 1bc409e

Please sign in to comment.