google / blockly

The web-based visual programming editor.
https://developers.google.com/blockly/
Apache License 2.0
12.38k stars 3.7k forks source link

Support injecting Blockly into shadowDom #1114

Open bamartin-ua opened 7 years ago

bamartin-ua commented 7 years ago

So I'm trying to put blockly in a webcomponent, and I'm getting Uncaught Error: container is not in current document. Looking at the source code, this appears to be a deliberate line in Blockly.inject() that checks to make sure you're not using shadow-dom.

// Verify that the container is in document.
if (!goog.dom.contains(document, container)) {
  throw 'Error: container is not in current document.';
}

what's the reason for this? is there some principled reason we shouldn't be using blockly in a webcomponent?

rachel-fenichel commented 7 years ago

Here's an old forum discussion from people trying to do the same thing.

bamartin-ua commented 7 years ago

@rachel-fenichel it looks like the consensus they came to was "it's impossible just put it in an iframe" which doesn't sound like much of a solution.

rachel-fenichel commented 7 years ago

I linked to the forum post because they discussed specific things that broke when you put Blockly in a shadow DOM.

First things first though - I've discovered the rendering issue is almost certainly due to Blockly inserting its CSS into the document head, where it has no effect on the contents of a shadowroot.

While trying to debug my attempt to fix that I've also discovered I can't HTML Import 'blockly_uncompressed.js' as it has a trio of 'document.write' commands at the end which don't work because HTML Imports are async (well this is optional with native support but polymer's polyfill uses xhr anyway). Even if these didn't error they still wouldn't work with blockly in a shadowroot.

Strangely I don't see this problem with the compressed code - are these statements optimised out by Closure I wonder? In which case how does Blockly get initialised? (The answer is yes, using compressed Blockly means it doesn't use document.write.)

and

The biggest stumbling block has been trying to get categories to work - unfortunately the Closure TreeControl isn't expecting to be used within a shadowRoot, so I've had to put in a bit of a hack with a DomHelper to work around the limitations. This mostly works, the only thing that doesn't is <hr>separators - I haven't managed to track down why that is yet.

Quincy515 commented 7 years ago

can you help me? I don't know how to fix my code. thank you

import React from 'react'
import Blockly from 'node-blockly'
const toolbox = `
         <xml>
           <block type="controls_if"></block>
           <block type="controls_whileUntil"></block>
         </xml>` 
class BlocklyDiv extends React.Component {
    componentDidMount() {
        var workspace = Blockly.inject(this.blocklyDiv,{toolbox: toolbox});
    }
    render() {
        return (
            <div>
                <h2>BlocklyDiv</h2>
                <div id="blocklyContainer">
                    <div id="blocklyDiv" ref={ref => this.blocklyDiv = ref} ></div>
                </div>
            </div>
        )
    }
}
export default BlocklyDiv
BusbyActual commented 6 years ago

I'm experiencing this issue in angularJS as well.

arunsoman commented 6 years ago

Same issue

  Blockly.inject @ blockly_compressed.js:1645
  (anonymous) @ VM663:1
  connectedCallback @ blocklyBlock.html:31
  (anonymous) @ blocklyBlock.html:7

Code below

window.customElements.define('blockly-block' ,class extends HTMLElement{ constructor(){ super(); const template = document.createElement('template'); template.innerHTML=`

`; this.attachShadow({mode: 'open'}); this.shadowRoot.appendChild(template.content.cloneNode(true)); } connectedCallback(){ console.log("connected"); this.workspace = Blockly.inject(this.shadowRoot.getElementById('blocklyDiv'),{toolbox:this.shadowRoot.getElementById('toolbox')}) } });
0xAnon101 commented 3 years ago

any progress yet on this one ?

abserari commented 3 years ago
workspace = {
  const container = document.querySelector('#blockly_div');
  removeAllChildNodes(container);

  const workspace = Blockly.inject('blockly_div', {
    media: 'https://unpkg.com/blockly/media/',
    toolbox: defaultToolboxConfiguration(Blockly),
    grid: { spacing: 20, length: 3, colour: '#ccc', snap: true },
    zoom: {
      controls: true,
      wheel: true,
      startScale: 1.0,
      maxScale: 2.0,
      minScale: 1.0,
      scaleSpeed: 0.4
    },
    trashcan: true
  });

  return workspace;
}
letrthang commented 3 years ago

@BeksOmega please put this issue to higher priority. It is more than 4 years old :)

EmilePerron commented 2 years ago

This issue is indeed a major blocker for every project that uses web components (which are being made more popular by Google's own Lit).

Just removing the validation check in the original post won't fix the issue however. Blockly injects styles and scripts into the document, and these won't work if the editor is within the shadow DOM.

Are there any plans for fixing this issue? Or any avenues of development to help contributors work on a PR?

pauceano commented 2 years ago

With a small hack, it looks as if blockly works well in a shadow DOM. Blockly saves all styles in an array: Blockly.Css.CONTENT They are 'registered' in the array by the different modules and they are injected in the document when the workspace is injected with the function Blockly.Css.inject() in css.js. If, before injecting the workspace, you call a function like this one, in this.blocklyStyle you will have all the styles:

getBlocklyCSS() { var text = Blockly.Css.CONTENT.join('\n'); const pathToMedia = "/media"; // If you are interested in sounds define here the path for media var mediaPath = pathToMedia.replace(/[\/]$/, ''); this.blocklyStyle = text.replace(/<<>>/g, mediaPath);
};

and in the render() function you can do something like:

return html`

  `

I am pretty sure that this solution is not optimized at all, but, together with the removal of the check for the blockly container in Blockly.inject makes a non invasive hack. Actually, you can do a new Blockly.inject function and then you do not need to change any code. So it works in the compressed version without any compilation.

jogibear9988 commented 9 months ago

I solved it atm. with a hack like this:

   const workspace = Blockly.inject(this.blocklyDiv, { toolbox: toolbox,  /* renderer: 'zelos' */ });

    let style1 = document.getElementById('blockly-renderer-style-geras-classic');
    this.shadowRoot.appendChild(style1);
    let style2 = document.getElementById('blockly-common-style');
    this.shadowRoot.appendChild(style2);

means after injecting blockly, I search the document for the styles and move them to my component

jogibear9988 commented 9 months ago

I already did a little bit better fix:

    if (!IobrokerWebuiBlocklyScriptEditor.blocklyStyle1) {
        IobrokerWebuiBlocklyScriptEditor.blocklyStyle1 = <HTMLStyleElement>document.getElementById('blockly-renderer-style-zelos-classic');
        this.shadowRoot.appendChild(IobrokerWebuiBlocklyScriptEditor.blocklyStyle1);
        IobrokerWebuiBlocklyScriptEditor.blocklyStyle2 = <HTMLStyleElement>document.getElementById('blockly-common-style');
        this.shadowRoot.appendChild(IobrokerWebuiBlocklyScriptEditor.blocklyStyle2);
    } else {
        this.shadowRoot.appendChild(IobrokerWebuiBlocklyScriptEditor.blocklyStyle1.cloneNode(true));
        this.shadowRoot.appendChild(IobrokerWebuiBlocklyScriptEditor.blocklyStyle2.cloneNode(true));
    }

but there are also problems when using blockly in shadow dom, for example the overlay input for enter text is at the wrong position. There may be more, this is my first day testing this.

But at least I could get it to render and I could drag/drop items...

See in the image blockly inside of shadow dom:

image
jogibear9988 commented 9 months ago

There are 2 problems at the moment with blockly in ShadowRoot:

ToolTips, they are shown at a complete wrong position:

image

Input fields for texts, are shown at wrong position, and maybe below the component in the dom:

image

(you see the textbox where I enter "lll" is below the element wich hosts the blockly component

BeksOmega commented 9 months ago

From @jogibear9988 on https://github.com/google/blockly/issues/7717#issuecomment-1857665570 Question:

I'd like to work on official webcomponent support. But there will be refactorings needed, cause you could have multiple blockly instance with different containers for example for tooltips, ....)

Is this something wich will be accepted later? What are the opinions about this?

Maybe look at my draft with already some changes : #7718

Could you provide some more details about why shadow dom is important to your project? We have a lot of things we are planning to do in Q1 and I'm not sure that we have time to prioritize this :/ (including review). It would be very helpful to have more details about if and where this is blocking you!

jogibear9988 commented 9 months ago

ShadowDom is important for me, as my complete UI is based on WebComponents wich use shadowdom.

At the moment I could solve my usage with this code...

  import { BaseCustomWebComponentConstructorAppend, html, css } from '@node-projects/base-custom-webcomponent';
  import toolbox from './IobrokerWebuiBlocklyToolbox.js'

  export class IobrokerWebuiBlocklyScriptEditor extends BaseCustomWebComponentConstructorAppend {

      static readonly template = html`
          <div id="blocklyDiv" style="position: absolute; width: 100%; height: 100%;"></div>
      `;

      static readonly style = css`
          :host {
              box-sizing: border-box;
              position: absolute;
              height: 100%;
              width: 100%;
          }`;

      static readonly is = 'iobroker-webui-blockly-script-editor';

      blocklyDiv: HTMLDivElement;
      workspace: any;
      static blocklyStyle1: CSSStyleSheet;
      static blocklyStyle2: CSSStyleSheet;
      resizeObserver: ResizeObserver;

      constructor() {
          super();
          super._restoreCachedInititalValues();

          this.blocklyDiv = this._getDomElement<HTMLDivElement>('blocklyDiv');

          this.createBlockly();
      }

      createBlockly() {
          //@ts-ignore
          this.workspace = Blockly.inject(this.blocklyDiv, {
              toolbox: toolbox,
              renderer: 'zelos',
              trashcan: true,
              zoom: {
                  controls: true,
                  wheel: false,
                  startScale: 0.7,
                  maxScale: 3,
                  minScale: 0.3,
                  scaleSpeed: 1.2,
                  pinch: false
              },
              move: {
                  scrollbars: {
                      horizontal: true,
                      vertical: true
                  },
                  drag: true,
                  wheel: true
              }
          });

          if (!IobrokerWebuiBlocklyScriptEditor.blocklyStyle1) {
              IobrokerWebuiBlocklyScriptEditor.blocklyStyle1 = new CSSStyleSheet();
              //@ts-ignore
              IobrokerWebuiBlocklyScriptEditor.blocklyStyle1.replaceSync(<HTMLStyleElement>document.getElementById('blockly-renderer-style-zelos-classic').innerText);
              IobrokerWebuiBlocklyScriptEditor.blocklyStyle2 = new CSSStyleSheet();
              //@ts-ignore
              IobrokerWebuiBlocklyScriptEditor.blocklyStyle2.replaceSync(<HTMLStyleElement>document.getElementById('blockly-common-style').innerText);
          }
          this.shadowRoot.adoptedStyleSheets = [IobrokerWebuiBlocklyScriptEditor.blocklyStyle1, IobrokerWebuiBlocklyScriptEditor.blocklyStyle2, IobrokerWebuiBlocklyScriptEditor.style];

          //@ts-ignore
          const zoomToFit = new ZoomToFitControl(this.workspace);
          zoomToFit.init();
      }

      ready() {
          //@ts-ignore
          Blockly.svgResize(this.workspace);

          this.resizeObserver = new ResizeObserver((entries) => {
              //@ts-ignore
              Blockly.svgResize(this.workspace)
          });
          this.resizeObserver.observe(this);
      }

      public save(): any {
          //@ts-ignore
          const state = Blockly.serialization.workspaces.save(this.workspace);
          return state;
      }

      public load(data: any) {
          //@ts-ignore
          Blockly.serialization.workspaces.load(data, this.workspace);
      }
  }
  customElements.define(IobrokerWebuiBlocklyScriptEditor.is, IobrokerWebuiBlocklyScriptEditor)

but I feels very hacky for me atm. So it is not so critical as it works . I could work on this, the Blockly Codebase seems very easy to understand, but at first there needs to be a consense how some of the issues could & should be solved (I created a list of the TODOs in the other issue)

BeksOmega commented 8 months ago

Hiya @jogibear9988 so it sounds like this isn't strictly blocking you. So we probably won't get to it in the first 3 months of 2024. But I've added it to the triage list so the triage team can get back to you and confirm!

Really appreciate you offering to work on this! But like you said first there needs to be a consensus, and I don't think we have bandwidth to make those design decisions at the moment.

jogibear9988 commented 8 months ago

Yeah, atm. it is workin with the dirty hacks. If there is time for discussions, I'm glad to help and discuss whats needed for webcomponents

maribethb commented 7 months ago

Hi @jogibear9988 unfortunately our team doesn't have the cycles right now to look at webcomponents. We love that you're willing to help with this issue though. If you'd like to put together a proposal or other information about what you'd need for webcomponents, that would be great, but we also won't have the cycles to review that kind of proposal right now either. It would be a while before we are able to review any kind of proposal and take action on it. Thanks again for your interest in this issue. For now, it's going back to our backlog.