measuredco / puck

The visual editor for React
https://puckeditor.com
MIT License
4.8k stars 243 forks source link

iframe style injection #409

Open chrisvxd opened 3 months ago

chrisvxd commented 3 months ago

Description

Users want to be able to inject styles into iframes for some use-cases.

We should leverage the iframe API to support this.

Proposals

Option 1

Consider leveraging the initialContent API from auto-frame-component:

<Puck iframe={{
  initialContent="<!DOCTYPE html><html><head><style>body { background: hotpink; }</style></head><body><div></div></body></html>"
}} {/* ... */} />

Option 2

Use a stripped back version of initialContent API from auto-frame-component:

<Puck iframe={{
  head="<style>body { background: hotpink; }</style>"
}} {/* ... */} />

Option 3

Add a dedicated parameters

<Puck iframe={{
  style="body { background: hotpink; }"
  styleUrl="https://www.example.com/styles.css"
}} {/* ... */} />
nicolas-angelo commented 2 months ago

My first pick would be Opt 1 for maximum flexibility, though I'd be okay with Opt 2 as well.

4leite commented 1 month ago

Ideally for my use-case, this would be dynamic, so updating the props would work (this is isn't clear given the name 'initialContent').

nicolas-angelo commented 1 month ago

Considering opening a PR for this, but I came across some potential challenges based on my use case, particularly for working with tailwind.

I looked at the code where the styles are being mirrored, and after testing, realized that puck.css and global.css all get injected together in a style tag in the host's layout file.

For my use case to work, I'd need style isolation. My assumption is that shadow dom would cause problems with drag/drop (I already tried this a few months ago).

Then I tried adding to @measured/puck's build script by additionally generating a module that exports a raw string of puck.css to do something like this:

import puckStylesRaw from "../dist/raw-styles"

const AutoFrameComponent = React.forwardRef<HTMLIFrameElement, AutoFrameProps>(
  function ({ mirrorHostStyles: boolean, ...props }: AutoFrameProps, ref) {

    return (
      <Frame {...props} ref={ref}>
        <CopyHostStyles puckStyles={puckStylesRaw} mirrorHostStyles={mirrorHostStyles}... >

Ultimately, my goal was to keep the puck.css styles in the frame since they're needed, while making the rest of the host styles optional to include. But it started feeling clunky and I realized I may be overcomplicating it. 😅

chrisvxd commented 1 month ago

@nicolas-angelo @4leite do you need style isolation in the Puck editor, or just in the final rendered output?

nicolas-angelo commented 1 month ago

@chrisvxd both. Although I know it's easier to manage styles for rendered output. The problem is the mirrored styles showing up in editor.

I plan on loading the tailwind play CDN in the editor and letting users enter dynamic classes, so I don't want the tailwind classes (or any classes from my host global stylesheet) ending up in the editor.

Im less concerned about isolating the puck.css classes because I don't really expect that to clash.

But preferably the editor can be used as a clean slate for styles.

4leite commented 1 month ago

I have the above working. (Tailwind play CDN and users entering dynamic classes). I actually don't need style isolation as I deal with that using tailwinds class prefix option.

nicolas-angelo commented 1 month ago

@4leite that dawned on me the other day but I still felt uneasy having the host styles mirrored in by default, even styles that weren't specifically tailwind generated.

Awesome that you got it working though! Feel free to share you're approach if you don't mind it being public 😅

4leite commented 1 month ago

You can see the gist of it there, although I haven't merged the class isolation and some bug fixes. https://github.com/Tohuhono/Oberon/tree/main/packages/ui/src/theme

4leite commented 1 month ago

@chrisvxd This is not a priority feature for me personally (I have solved it for my use case), I was more chiming in to document how I solved it for what seems like a similar use case.

nicolas-angelo commented 1 month ago

@chrisvxd yea I think I'm going to work off @4leite solution. But as far as general style injection feedback, I'd be happy with:

And nice to have but not critical:

chrisvxd commented 1 month ago

Thanks for the feedback both. I'll factor this all in when solving for that.

jarrrgh commented 1 month ago

Option 4

Include CSS in the Puck data? Something like:

{
  "content": [],
  "root": {
    "props": {
      "styles": "body { background: hotpink; }"
    }
  },
  "zones": {}
}

Reasoning: We are planning to have a CSS editor, which would update the styles in the data object. Currently we grab the styles prop and apply it inside the iframe body by overriding root render and placing a style element before children.

const config: Config = {
  root: {
    render: ({ children, styles }) => {
      return (
        <div>
          <style>{styles}</style>
          {children}
        </div>
      );
    },
  },

While this works, it would be better, if the styles got updated in the iframe head. In case of no iframe the styles could just go in the main window head I suppose...

chrisvxd commented 1 month ago

Great workaround @jarrrgh!