import {LitElement, html, css, TemplateResult, nothing} from 'lit'; import {customElement, property, state} from 'lit/decorators.js'; import {classMap} from 'lit/directives/class-map.js'; function isJson(str: string) { try { JSON.parse(str); } catch (e) { return false; } return true; } @customElement('n8n-demo') export class N8NDemo extends LitElement { /** * Workflow json to load. */ @property({type: String}) workflow = '{}'; /** * Whether to add frame around canvas with code and copy button */ @property({type: String}) frame = 'false'; /** * URL for n8n instance to load workflow. */ @property({type: String}) src = 'https://n8n-preview-service.internal.n8n.cloud/workflows/demo'; /** * Whether to collapse on mobile, so that scrolling on mobile is easier. */ @property({type: String}) collapseformobile = 'true'; /** * Add button before users can interact with canvas. * Makes scrolling through page easier without getting bugged down. */ @property({type: String}) clicktointeract = 'false'; /** * Hide node errors on the canvas */ @property({type: String}) hidecanvaserrors = 'false'; /** * Disable interactivity entirely. This will prevent the user from * interacting with the workflow. */ @property({type: String}) disableinteractivity = 'false'; /** * Whether to force a theme on n8n. * Accepts 'light' and 'dark' */ @property({type: String}) theme?: 'light' | 'dark'; @state() showCode = false; @state() showPreview = true; @state() fullscreen = false; @state() insideIframe = false; @state() copyText = 'Copy'; @state() isMobileView = false; @state() error = false; @state() interactive = true; scrollX = 0; scrollY = 0; n8nReady = false; iframeVisible = false; observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const iframe = this.shadowRoot!.getElementById( 'int_iframe' ) as HTMLIFrameElement; this.observer.unobserve(iframe); this.iframeVisible = true; if (this.n8nReady) { this.loadWorkflow(); } } }); }); override firstUpdated() { const iframe = this.shadowRoot!.getElementById( 'int_iframe' ) as HTMLIFrameElement; if (!iframe) { return; } this.observer.observe(iframe); } override connectedCallback(): void { super.connectedCallback(); try { if ( typeof this.workflow === 'string' && this.workflow.startsWith('%7B%') ) { this.workflow = decodeURIComponent(this.workflow); } // eslint-disable-next-line no-empty } catch (e) {} if ( this.clicktointeract === 'true' || this.disableinteractivity === 'true' ) { this.interactive = false; } if (window.matchMedia('only screen and (max-width: 760px)').matches) { this.isMobileView = true; } if (this.collapseformobile === 'true' && this.isMobileView) { this.showPreview = false; } window.addEventListener('message', this.receiveMessage); document.addEventListener('scroll', this.onDocumentScroll); } override disconnectedCallback(): void { window.removeEventListener('message', this.receiveMessage); document.removeEventListener('scroll', this.onDocumentScroll); super.disconnectedCallback(); } private receiveMessage = ({data, source}: MessageEvent) => { const iframe = this.shadowRoot!.getElementById( 'int_iframe' ) as HTMLIFrameElement; if (!iframe) { return; } if (isJson(data) && iframe.contentWindow === source) { const json = JSON.parse(data); if (json.command === 'n8nReady') { this.n8nReady = true; if (this.iframeVisible) { this.loadWorkflow(); } } else if (json.command === 'openNDV') { // expand iframe this.fullscreen = true; } else if (json.command === 'closeNDV') { // close iframe this.fullscreen = false; } else if (json.command === 'error') { this.error = true; this.showPreview = false; } } }; private onDocumentScroll = () => { if ( this.interactive && this.insideIframe && !('ontouchstart' in window || navigator.maxTouchPoints) ) { window.scrollTo(this.scrollX, this.scrollY); } }; loadWorkflow() { try { const workflow = JSON.parse(this.workflow); // Workflow Check if (!workflow) { throw new Error('Missing workflow'); } if (!workflow.nodes || !Array.isArray(workflow.nodes)) { throw new Error('Must have an array of nodes'); } const iframe = this.shadowRoot!.getElementById( 'int_iframe' ) as HTMLIFrameElement; // set workflow in canvas if (iframe.contentWindow) { iframe.contentWindow.postMessage( JSON.stringify({ command: 'openWorkflow', workflow, hideNodeIssues: this.hidecanvaserrors === 'true', }), '*' ); } } catch { this.error = true; } } toggleCode() { this.showCode = !this.showCode; } onMouseEnter() { this.insideIframe = true; this.scrollX = window.scrollX; this.scrollY = window.scrollY; } onMouseLeave() { this.insideIframe = false; } onOverlayClick() { if (this.disableinteractivity !== 'true') { this.interactive = true; } } copyClipboard() { navigator.clipboard.writeText(this.workflow); this.copyText = 'Copied'; setTimeout(() => { this.copyText = 'Copy'; }, 1500); } toggleView() { this.showPreview = true; } static override styles = css` :host { --n8n-color-primary-h: 6.9; --n8n-color-primary-s: 100%; --n8n-color-primary-l: 67.6%; --n8n-color-primary: hsl( var(--n8n-color-primary-h), var(--n8n-color-primary-s), var(--n8n-color-primary-l) ); } *, *::before, *::after { box-sizing: border-box; } button { outline: none; text-decoration: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-appearance: none; } .workflow_json { height: 300px; padding-left: 10px; overflow: auto; background-color: var(--n8n-json-background-color, hsl(260deg 100% 99%)); word-wrap: normal; font-family: 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace; font-size: 1.2em; color: hsl(0, 0%, 20%); } .overlay { height: 100%; width: 100%; position: absolute; top: 0; left: 0; background: var(--n8n-overlay-background, hsla(232, 48%, 12%, 0.1)); display: flex; align-items: center; justify-content: center; opacity: 0; } .overlay:hover { opacity: 1; transition: 250ms opacity; } .overlay > button { padding: 20px 40px; border-radius: 8px; font-weight: 600; font-size: 18px; line-height: 24px; border: var(--n8n-overlay-border, none); background-color: var( --n8n-overlay-background-color, var(--n8n-color-primary) ); color: var(--n8n-interact-button-color, white); } .overlay > button:hover { filter: brightness(85%); cursor: pointer; } .canvas-container { height: var(--n8n-workflow-min-height, 300px); position: relative; } .embedded_workflow.frame { padding: 10px; background-color: var(--n8n-frame-background-color, hsl(260, 11%, 95%)); } .embedded_workflow .embedded_tip_error { color: hsl(0, 0%, 40%); text-align: center; font-size: 0.9em; } .embedded_workflow .embedded_tip_error_with_code { margin-bottom: 10px; } .embedded_workflow .embedded_tip { margin-top: 7px; color: hsl(0, 0%, 40%); text-align: center; font-size: 0.9em; } .embedded_workflow .embedded_tip_with_code { margin-top: 7px; margin-bottom: 10px; } .embedded_workflow_iframe { width: 100%; min-height: var(--n8n-workflow-min-height, 300px); border: 0; border-radius: var(--n8n-iframe-border-radius, 0px); } .embedded_workflow_iframe_node_view { position: fixed; top: 0; left: 0; height: 100%; width: 100%; z-index: 9999999; } .code_toggle { background: none; border: none; padding: 0px; margin: -1px; cursor: pointer; color: var(--n8n-color-primary); font-size: 1em; } .copy_button { display: none; /* Hide button */ } .workflow_json:hover .copy_button { display: block; float: right; right: 0px; margin-top: 10px; margin-right: 10px; padding: 5px; font-family: 'Arial', 'sans-serif'; font-size: 0.8em; color: #646464; background: var(--n8n-copy-button-background-color, rgb(239, 239, 239)); cursor: pointer; } .non_interactive { pointer-events: none; } `; renderIframe() { if (!this.showPreview || this.error) { return html``; } const query = this.theme ? `?theme=${this.theme}` : ''; const url = `${this.src}${query}`; const interactivityDisabled = this.disableinteractivity === 'true'; const canvas = html``; let overlay: string | TemplateResult = ''; if (interactivityDisabled) { overlay = html`
`; } else if (!this.interactive) { overlay = html``; } return html`