diff --git a/components/Accordion.jsx b/components/Accordion.jsx index b8a8916..795538e 100644 --- a/components/Accordion.jsx +++ b/components/Accordion.jsx @@ -1,10 +1,61 @@ -import React from "react"; +import React, { useRef, useEffect } from "react"; -const Accordion = ({ children, title = "More" }) => ( -
- {title} - {children} -
-); +const Accordion = ({ children, title = "More" }) => { + const detailsRef = useRef(null); + + function openAccordionOnHash() { + const hash = location.hash.substring(1); + if (hash.length === 0) { + return; + } + let targetElement; + targetElement = document.getElementById(hash); + if (targetElement === null) { + return; + } + + // 1. If target is within current details element + const detailsElement = detailsRef.current; + if ( + detailsElement !== null && + detailsElement.contains(targetElement) + ) { + // 2. and it is not open, + if (!detailsElement.open) { + // 3. Then open it + detailsElement.open = true; + } + // 4. and scroll into view and focus + targetElement.focus(); + targetElement.scrollIntoView({ block: "center" }); + // Increased for mobile browsers + const scrollRestorationDelay = 1000; // 1s + setTimeout(() => { + if ("scrollRestoration" in history) { + history.scrollRestoration = "auto"; + } + }, scrollRestorationDelay); + } + } + + useEffect(() => { + // 1. Add event listener for future hash changes + window.addEventListener("hashchange", openAccordionOnHash); + // 2. Do it anyway now if initial url has hash + openAccordionOnHash() + + // Cleanup - remove listener + return () => { + window.removeEventListener("hashchange", openAccordionOnHash); + }; + }, []); + + return ( +
+ {title} + {children} +
+ ); +}; export default Accordion; diff --git a/components/Admonition.jsx b/components/Admonition.jsx index 83bf86d..53aa382 100644 --- a/components/Admonition.jsx +++ b/components/Admonition.jsx @@ -2,7 +2,7 @@ import React, { Component } from "react"; export default class Admonition extends Component { render() { - const { type, iconType, title, children, addIfmClass } = this.props; + const { type, iconType, title, id, children, addIfmClass } = this.props; let wrapperClasses = ["admonition", `admonition-${type}`]; if (addIfmClass) { @@ -11,7 +11,7 @@ export default class Admonition extends Component { } return ( -
+
{returnIcon(type, iconType)}
diff --git a/components/InternalLink.jsx b/components/InternalLink.jsx new file mode 100644 index 0000000..43d0a4b --- /dev/null +++ b/components/InternalLink.jsx @@ -0,0 +1,24 @@ +import React from "react"; + +/* +InternalLink should be only used to link to fragment urls placed within a Accordion, in the same page. +`href` props should only contain a fragment prefixed with "link-" (start with "#link-") +It allows all the same props as tag. +*/ +const InternalLink = ({ href, className = "navlink", ...otherProps }) => { + if (!href.startsWith("#link-")) { + throw new TypeError( + "InternalLink - href must start with #. It should only be used for same-page fragment links." + ); + } + + const handleLink = () => { + // manually change hash to trigger hashChange event. + window.location.hash = "#"; + window.location.hash = href; + }; + + return ; +}; + +export default InternalLink; diff --git a/docs/create-modify-element.mdx b/docs/create-modify-element.mdx index 9b6aafa..a8f5f0b 100644 --- a/docs/create-modify-element.mdx +++ b/docs/create-modify-element.mdx @@ -7,8 +7,14 @@ slug: / import Admonition from "../components/Admonition" import Accordion from "../components/Accordion" +import InternalLink from "../components/InternalLink" import MDN from "../components/MDNBadge" +:::note DOM interfaces +Every kind of **DOM** node is represented by an interface based on **Node** interface. +**Element, Document and DocumentFragment** interfaces inherit from Node. More about DOM interfaces [here](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) +::: + ## Finding elements ### 1. querySelector() @@ -24,19 +30,19 @@ document.querySelector(".not-found"); // null ``` - - This method is also available on all DOM nodes. If you want to look within a specific element, just use `ele.querySelector()` on the parent element. + + Besides Document, this method is also available on Element and DocumentFragment.
+ If you want to look within a specific element, just use ele.querySelector() on the parent element.
+ If you want to look within a specific fragment, just use df.querySelector() on the parent fragment. ```js - // Example - .outer > .inner - outerEle.addEventListener((ev) => { - let innerEle = ev.target.querySelector(".inner"); - }); + // Example - outer is a parent element/fragment. + let innerEle = outer.querySelector(".inner"); ```
- + ### 2. querySelectorAll() @@ -60,17 +66,21 @@ for (let p of paras) { } ``` - +`querySelectorAll` is also available on Element and DocumentFragment, in the same way as querySelector. + + ## Create / insert element ### 3. createElement() -It creates a new HTML element, taking the tag name (`"div"`) as argument. +`document.createElement(tagName) : Element` + +This method creates a new HTML element, taking the specified HTML tag name as argument. ```js document.createElement("div") -// creates
+// creates a
element ``` @@ -94,7 +104,7 @@ document.createElement("div") `Element.setAttribute(name, value)` -It sets a attribute on the current element. +This method sets an attribute on the current element. `name` - name of the attribute to add. Ex- "type". `value` - value of the attribute. Ex- "text". @@ -110,9 +120,9 @@ ele.setAttribute("type", "text") ### 5. appendChild() -`Element.appendChild(newElement)` +`Node.appendChild(newNode)` -It inserts a new element as the last child of the parent element. +This method inserts a new node as the last child of the parent node. ```js let eleDiv = document.createElement("div") @@ -124,13 +134,13 @@ eleDiv.appendChild(eleH1) - If newElement is already a child of another element, + If newNode is already a child of another node, then it is removed from its existing parent and then added to the new parent. - - If newElement is a DocumentFragment, then - instead of adding the fragment itself as a child, - the entire content of the fragment is added as children of the parent element. + + If newNode is a DocumentFragment, then - instead of adding the fragment itself as a child, + the entire content of the fragment is added as children of the parent node. @@ -150,11 +160,11 @@ eleDiv.appendChild(eleH1) ### 6. removeChild() -`Element.removeChild(childElement)` +`Node.removeChild(childNode)` -This method tries to remove a child element from the parent and returns the removed node. +This method tries to remove a child node from the parent and returns the removed node. -If the element to be removed is not a child of the parent element, it throws a `NotFoundError` DOMException. +If the node to be removed is not a child of the parent node, it throws a `NotFoundError` DOMException. ```js //
@@ -168,13 +178,15 @@ parentEle.removeChild(childEle) ### 7. insertBefore() -`Element.insertBefore(newEle, refEle)` +`Node.insertBefore(newEle, refEle)` -This method inserts a new child element before another child within a parent. +This method inserts a new child node before another node within a parent. + +It also works with **DocumentFragment**, by inserting all of its content in that position. `newEle` - The node to be inserted. -`refEle` - The reference element before which newEle should be inserted. If refEle is `null`, then newEle is added as the last child. +`refEle` - The reference node before which newEle should be inserted. If refEle is `null`, then newEle is added as the last child. ```js //
    @@ -204,18 +216,14 @@ parent.insertBefore(newEle, ref.nextSibling) ### 8. Document Fragment +`document.createDocumentFragment() : DocumentFragment` + +This method is used to create a new empty document fragment. + DocumentFragment can be considered to be a off-screen lightweight `document` object. It is generally used for batching multiple operations - by doing them in the fragment over time and then rendering the whole content to the visible DOM in one shot . -Because fragment is not rendered, making changes to it doesn't cause any performance impact (no reflow). - -**Methods** - - -`document.createDocumentFragment()` - creates a new empty fragment. - -`.appendChild()` / `.insertBefore()` - can be used to move the whole content of the fragment to the actual DOM. - -**Example** - +Because fragment is not rendered, making changes to it doesn't cause any performance impact (no reflow). To **actually "render" the fragment** in the visible document, use appendChild or `insertBefore`. ```js let fragment = document.createDocumentFragment() @@ -236,4 +244,4 @@ outerEle.appendChild(fragment) -------- -In the next section, we'll see how to read information from a DOM element. \ No newline at end of file +In the next section, we'll see how to read information from a DOM element. diff --git a/docusaurus.config.js b/docusaurus.config.js index 69faacd..1c9b7a7 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -102,4 +102,10 @@ module.exports = { }, ], ], + scripts: [ + { + src: + '/scripts/handleScrollRestoration.js', + }, + ] }; diff --git a/src/css/custom.css b/src/css/custom.css index 5ed4a52..86277d8 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -32,4 +32,16 @@ .admonition h5 { text-transform: unset; +} + +.navlink{ + cursor: pointer; +} + +.navlink:hover{ +text-decoration: underline; +} + +summary { + cursor: pointer; } \ No newline at end of file diff --git a/static/scripts/handleScrollRestoration.js b/static/scripts/handleScrollRestoration.js new file mode 100644 index 0000000..bd93e7c --- /dev/null +++ b/static/scripts/handleScrollRestoration.js @@ -0,0 +1,8 @@ +var hash = window.location.hash; +var hashName = hash.substring(1, hash.length); + +if (hashName.startsWith("link-")) { + if ("scrollRestoration" in history) { + history.scrollRestoration = "manual"; + } +}