From 06b9e676410f33c317dbfcb6b4f6a7c7a36c6d7b Mon Sep 17 00:00:00 2001 From: Chris Villa Date: Tue, 27 Feb 2024 12:05:01 +0100 Subject: [PATCH] feat: implement auto-frame-component --- README.md | 33 +++++++++- index.ts | 1 + index.tsx | 5 -- package.json | 3 + src/AutoFrameComponent.tsx | 129 +++++++++++++++++++++++++++++++++++++ yarn.lock | 28 +++++++- 6 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 index.ts delete mode 100644 index.tsx create mode 100644 src/AutoFrameComponent.tsx diff --git a/README.md b/README.md index 282557a..f55fb07 100644 --- a/README.md +++ b/README.md @@ -1 +1,32 @@ -# React lib +# auto-frame-component + +An iframe component that automatically syncs styles from the host. + +An implementation of [react-frame-component](https://github.com/ryanseddon/react-frame-component). + +## Quick start + +```sh +npm i @measured/auto-frame-component +``` + +```jsx +import AutoFrame from "@measured/auto-frame-component"; + +export function Page() { + return ( + + {/* Hero class exists on parent */} +
Hello, world
+
+ ); +} +``` + +## API + +Shares an API with [react-frame-component](https://github.com/ryanseddon/react-frame-component). + +## License + +MIT © [Measured Corporation Ltd](https://measured.co) diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..bd4a012 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +export { default } from "./src/AutoFrameComponent"; diff --git a/index.tsx b/index.tsx deleted file mode 100644 index 93b9f69..0000000 --- a/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from "react"; - -export default function () { - return
; -} diff --git a/package.json b/package.json index f2968b6..a34d48d 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,11 @@ "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "eslint": "^7.32.0", + "object-hash": "^3.0.0", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-frame-component": "5.2.6", "tsup": "^6.7.0", "typescript": "^4.5.2" }, diff --git a/src/AutoFrameComponent.tsx b/src/AutoFrameComponent.tsx new file mode 100644 index 0000000..f916dcd --- /dev/null +++ b/src/AutoFrameComponent.tsx @@ -0,0 +1,129 @@ +import React, { ReactNode, useLayoutEffect } from "react"; +import Frame, { FrameComponentProps, useFrame } from "react-frame-component"; +import hash from "object-hash"; + +const styleSelector = 'style, link[as="style"], link[rel="stylesheet"]'; + +const collectStyles = (doc: Document) => { + const collected: Node[] = []; + + doc.head.querySelectorAll(styleSelector).forEach((style) => { + collected.push(style); + }); + + return collected; +}; + +const CopyHostStyles = ({ children }: { children: ReactNode }) => { + const { document: doc, window: win } = useFrame(); + + useLayoutEffect(() => { + if (!win) { + return () => {}; + } + + const add = (el: HTMLElement, contentHash: string = hash(el.outerHTML)) => { + if (doc?.head.querySelector(`[data-content-hash="${contentHash}"]`)) { + console.log( + `Style tag with same content (${contentHash}) already exists, skpping...` + ); + + return; + } + + console.log( + `Added style node with content hash ${contentHash} ${el.innerHTML}` + ); + + const frameStyles = el.cloneNode(true); + + (frameStyles as HTMLElement).setAttribute( + "data-content-hash", + contentHash + ); + + doc?.head.append(frameStyles); + }; + + const remove = (el: HTMLElement) => { + const contentHash = hash(el.textContent); + const frameStyles = el.cloneNode(true); + + (frameStyles as HTMLElement).setAttribute( + "data-content-hash", + contentHash + ); + + console.log( + `Removing node with content hash ${contentHash} as no longer present in parent` + ); + + doc?.head.querySelector(`[data-content-hash="${contentHash}"]`)?.remove(); + }; + + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === "childList") { + mutation.addedNodes.forEach((node) => { + if ( + node.nodeType === Node.TEXT_NODE || + node.nodeType === Node.ELEMENT_NODE + ) { + const el = + node.nodeType === Node.TEXT_NODE + ? node.parentElement + : (node as HTMLElement); + + if (el && el.matches(styleSelector)) { + add(el); + } + } + }); + + mutation.removedNodes.forEach((node) => { + if ( + node.nodeType === Node.TEXT_NODE || + node.nodeType === Node.ELEMENT_NODE + ) { + const el = + node.nodeType === Node.TEXT_NODE + ? node.parentElement + : (node as HTMLElement); + + if (el && el.matches(styleSelector)) { + remove(el); + } + } + }); + } + }); + }); + + const parentDocument = win!.parent.document; + + observer.observe(parentDocument.head, { childList: true, subtree: true }); + + const collectedStyles = collectStyles(parentDocument); + + // Add new style tags + collectedStyles.forEach((styleNode) => { + add(styleNode as HTMLElement); + }); + + return () => { + observer.disconnect(); + }; + }, []); + + return <>{children}; +}; + +export default React.forwardRef( + function ({ children, ...props }: FrameComponentProps, ref) { + return ( + + {children} + + ); + } +); diff --git a/yarn.lock b/yarn.lock index be036bb..5e4191a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1022,7 +1022,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== -loose-envify@^1.1.0: +loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -1114,11 +1114,16 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -object-assign@^4.0.1: +object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1203,6 +1208,15 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -1221,6 +1235,16 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-frame-component@5.2.6: + version "5.2.6" + resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-5.2.6.tgz#0d9991d251ff1f7177479d8f370deea06b824b79" + integrity sha512-CwkEM5VSt6nFwZ1Op8hi3JB5rPseZlmnp5CGiismVTauE6S4Jsc4TNMlT0O7Cts4WgIC3ZBAQ2p1Mm9XgLbj+w== + +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"