UNPKG

@camunda/form-playground

Version:
549 lines (443 loc) 15.5 kB
import { html, createContext, useRef, useEffect, useState, render } from 'diagram-js/lib/ui'; import { domify, query } from 'min-dom'; import mitt from 'mitt'; import { get, assign, isArray, set } from 'min-dash'; import classNames from 'classnames'; import { FormPlayground } from '@bpmn-io/form-js'; const noop = () => {}; function CollapsiblePanel(props) { const { children, collapseTo, idx, onToggle, open, title } = props; const toggle = (event) => { event.stopPropagation(); onToggle(open); }; return html` <div data-idx="${ idx }" class="${classNames('cfp-collapsible-panel', { open: !collapseTo || open })}" > <div onClick="${ collapseTo ? toggle : noop }" class="cfp-collapsible-panel-title"> <h1>${ title }</h1> ${ collapseTo && html` <div class="cfp-collapsible-panel-actions"> <button title="${ open ? 'Close' : 'Open' } ${ title }" class="cfp-collapsible-panel-action" onClick="${ toggle }" > ${getIcon(collapseTo, open)} </button> </div> `} </div> <div class="cfp-collapsible-panel-content"> ${ children } </div> </div> `; } // helper //////////////// function getIcon(collapseTo, open) { if (collapseTo === 'right') { return open ? html`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M4.41421356,12.9494942 L3,11.5352806 L6.53553391,7.99974671 L3,4.4642128 L4.41421356,3.04999924 L9.36396103,7.99974671 L4.41421356,12.9494942 Z M9.91421356,3 L11.9142136,3 L11.9142136,13 L9.91421356,13 L9.91421356,3 Z"/> </svg>` : html`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M11.5,12.9494942 L6.55025253,7.99974671 L11.5,3.04999924 L12.9142136,4.4642128 L9.37867966,7.99974671 L12.9142136,11.5352806 L11.5,12.9494942 Z M6,3 L6,13 L4,13 L4,3 L6,3 Z"/> </svg>`; } if (collapseTo === 'bottom') { return open ? html`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M12.9494942,4.41421356 L7.99974671,9.36396103 L3.04999924,4.41421356 L4.4642128,3 L7.99974671,6.53553391 L11.5352806,3 L12.9494942,4.41421356 Z M3,9.91421356 L13,9.91421356 L13,11.9142136 L3,11.9142136 L3,9.91421356 Z"/> </svg>` : html`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M12.9494942,11.5 L11.5352806,12.9142136 L7.99974671,9.37867966 L4.4642128,12.9142136 L3.04999924,11.5 L7.99974671,6.55025253 L12.9494942,11.5 Z M3,6 L3,4 L13,4 L13,6 L3,6 Z"/> </svg>`; } } function CollapsedPreview(props) { const { idx, onTogglePreview, previewContainerRef, open } = props; const onToggle = () => { onTogglePreview(); }; return html` <div class="${classNames( 'cfp-collapsed-preview', { visible: !open })}"> <div class="cfp-collapsed-preview-title" onClick=${ onToggle }> <h1>Form Preview</h1> </div> <div class="cfp-collapsed-preview-actions"> <button title="Open Form Preview" class="cfp-collapsed-preview-action" onClick=${ onToggle }> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M11.5,12.9494942 L6.55025253,7.99974671 L11.5,3.04999924 L12.9142136,4.4642128 L9.37867966,7.99974671 L12.9142136,11.5352806 L11.5,12.9494942 Z M6,3 L6,13 L4,13 L4,3 L6,3 Z"/> </svg> </button> </div> </div> <!-- This is not visible when preview not open --> <${CollapsiblePanel} idx="${ idx }" open=${open} onToggle=${ onToggle } title="Form Preview" collapseTo="right"> <div class="cfp-preview-container" ref=${ previewContainerRef }></div> </${CollapsiblePanel}>`; } const LayoutContext = createContext({ layout: {}, setLayout: () => {}, getLayoutForKey: () => {}, setLayoutForKey: () => {} }); const DEFAULT_LAYOUT = {}; const FORM_DEFINITION_IDX = 'form-definition'; const FORM_PREVIEW_IDX = 'form-preview'; const FORM_INPUT_IDX = 'form-input'; const FORM_OUTPUT_IDX = 'form-output'; const TOGGLABLE_CONTAINERS = [ FORM_PREVIEW_IDX, FORM_INPUT_IDX, FORM_OUTPUT_IDX ]; const PIPE_EVENTS = [ 'formPlayground.init', 'formPlayground.rendered' ]; function PlaygroundComponent(props) { const { data, emitter, layoutConfig = {}, onInit, schema, ...restProps } = props; let playgroundRef = useRef(null); const paletteContainerRef = useRef(null); const editorContainerRef = useRef(null); const dataContainerRef = useRef(null); const previewContainerRef = useRef(null); const resultContainerRef = useRef(null); const propertiesContainerRef = useRef(null); const rootRef = useRef(null); // (1) initialize playground orchestrator useEffect(() => { playgroundRef.current = new FormPlayground({ data, schema, editor: { inlinePropertiesPanel: false }, propertiesPanel: { feelPopupContainer: rootRef.current, getDocumentationRef }, ...restProps }); }, []); // (1.1) pipe playground API useEffect(() => { playgroundRef.current.on('formPlayground.init', () => { onInit({ ...playgroundRef.current, collapse: this.collapse, open: this.open, // @pinussilvestrus: this is added for testing purposes _ref: playgroundRef.current }); }); }, []); // (1.2) pipe core playground events useEffect(() => { const emit = (type, e) => { return emitter.emit(type, e); }; PIPE_EVENTS.forEach(type => { playgroundRef.current.on(type, event => emit(type, event)); }); return () => { PIPE_EVENTS.forEach(type => { playgroundRef.current.off(type, event => emit(type, event)); }); }; }, [ playgroundRef ]); // (2) set-up layout context const [ layout, setLayout ] = useState(createLayout(layoutConfig)); const getLayoutForKey = (key, defaultValue) => { return get(layout, key, defaultValue); }; const setLayoutForKey = (key, config) => { const newLayout = assign({}, layout); set(newLayout, key, config); setLayout(newLayout); }; const layoutContext = { layout, setLayout, getLayoutForKey, setLayoutForKey }; const inputOpen = getLayoutForKey([ FORM_INPUT_IDX, 'open' ], false); const toggleInput = () => { inputOpen ? this.collapse([ FORM_INPUT_IDX ]) : this.open([ FORM_INPUT_IDX ]); }; const outputOpen = getLayoutForKey([ FORM_OUTPUT_IDX, 'open' ], false); const toggleOutput = () => { outputOpen ? this.collapse([ FORM_OUTPUT_IDX ]) : this.open([ FORM_OUTPUT_IDX ]); }; const previewOpen = getLayoutForKey([ FORM_PREVIEW_IDX, 'open' ], false); const togglePreview = () => { previewOpen ? this.collapse() : this.open(); }; const setContainersLayout = (containers, open) => { let newLayout = assign({}, layout); if (!containers) { containers = TOGGLABLE_CONTAINERS; } if (!isArray(containers)) { containers = [ containers ]; } containers.map(idx => { set(newLayout, [ idx, 'open' ], open); }); setLayout(newLayout); }; // (2.1) notify interested parties on layout changes useEffect(() => { emitter.emit('formPlayground.layoutChanged', { layout }); }, [ emitter, layout ]); // (3) attach all component containers useEffect(() => { const playground = playgroundRef.current; function attachComponents() { playground.attachPaletteContainer(paletteContainerRef.current); playground.attachEditorContainer(editorContainerRef.current); playground.attachDataContainer(dataContainerRef.current); playground.attachPreviewContainer(previewContainerRef.current); playground.attachResultContainer(resultContainerRef.current); playground.attachPropertiesPanelContainer(propertiesContainerRef.current); } playground.on('formPlayground.rendered', attachComponents); return () => playground.off('formPlayground.rendered', attachComponents); }, []); // (4) provide toggle API /** * @param {string|Array<string>} [containers] */ this.open = function(containers) { setContainersLayout(containers, true); }; /** * @param {string|Array<string>} [containers] */ this.collapse = function(containers) { return setContainersLayout(containers, false); }; // (5) listen on dragging events to provide drop feedback useEffect(() => { const bindDropTargetListeners = () => { const editor = playgroundRef.current.getEditor(); editor.on('drag.hover', function(event) { const { container } = event; if ( container.classList.contains('fjs-drop-container-horizontal') || container.classList.contains('fjs-drop-container-vertical') ) { rootRef.current && rootRef.current.classList.add('cfp-dragging'); } }); editor.on('drag.out', function(event) { rootRef.current && rootRef.current.classList.remove('cfp-dragging'); }); }; const playground = playgroundRef.current; if (playground) { playground.on('formPlayground.rendered', bindDropTargetListeners); } return () => { if (playground) { playground.off('formPlayground.rendered', bindDropTargetListeners); } }; }); // (6) render return html` <${LayoutContext.Provider} value=${ layoutContext }> <div ref=${rootRef} class="${classNames('cfp-root', { 'cfp-open-preview': previewOpen })}"> <div class="cfp-palette" ref=${ paletteContainerRef }></div> <div class="cfp-left"> <${CollapsiblePanel} idx="${ FORM_DEFINITION_IDX }" title="Form Definition"> <div class="cfp-editor-container" ref=${ editorContainerRef }></div> </${CollapsiblePanel}> <${CollapsiblePanel} open=${inputOpen} idx="${ FORM_INPUT_IDX }" title="Form Input" collapseTo="bottom" onToggle=${toggleInput}> <div class="cfp-data-container" ref=${ dataContainerRef }></div> </${CollapsiblePanel}> </div> <div class="cfp-right"> <${CollapsedPreview} idx=${ FORM_PREVIEW_IDX } open=${ previewOpen } previewContainerRef=${ previewContainerRef } onTogglePreview="${ togglePreview }" /> <${CollapsiblePanel} open=${outputOpen} idx="${ FORM_OUTPUT_IDX }" title="Form Output" collapseTo="bottom" onToggle=${toggleOutput}> <div class="cfp-result-container" ref=${ resultContainerRef }></div> </${CollapsiblePanel}> </div> <div class="cfp-properties" ref=${ propertiesContainerRef }></div> </div> </${LayoutContext.Provider}> `; } // helper /////////////// function createLayout(overrides) { return { ...DEFAULT_LAYOUT, ...overrides }; } function getDocumentationRef(field) { if (!field) { return; } if (field.type === 'default') { return 'https://docs.camunda.io/docs/components/modeler/forms/camunda-forms-reference/'; } return `https://docs.camunda.io/docs/components/modeler/forms/form-element-library/forms-element-library-${field.type}/`; } /** * @typedef { import('@bpmn-io/form-js-viewer').FormProperties } FormProperties * @typedef { import('@bpmn-io/form-js-editor').FormEditorProperties } FormEditorProperties * * @typedef { { * container?: Element, * data: any, * editorAdditionalModules?: Array<any>, * editorProperties?: FormEditorProperties * exporter?: { name: string, version: string }, * layout?: any, * propertiesPanel?: { parent: Element, feelPopupContainer: Element }, * schema: any, * viewerAdditionalModules?: Array<any>, * viewerProperties?: FormProperties, * } } CamundaFormPlaygroundOptions */ /** * @param {CamundaFormPlaygroundOptions} options */ function CamundaFormPlayground(options) { const { container: parent, data, schema, layout: layoutConfig, ...restProps } = options; const emitter = mitt(); const safeRef = function(fn) { return function(...args) { if (!playgroundRef) { throw new Error('Camunda Form Playground is not initialized.'); } return fn(...args); }; }; const container = this._container = domify('<div class="cfp-container"></div>'); let playgroundRef; if (parent) { parent.appendChild(container); } // API ////////// this.on = emitter.on; this.off = emitter.off; this.fire = emitter.emit; // added due to current limitation of <mitt.once> // cf. https://github.com/developit/mitt/issues/136 this.once = function(type, fn) { emitter.on(type, fn); emitter.on(type, emitter.off.bind(emitter, type, fn)); }; this.destroy = function() { const parent = container.parentNode; render(null, container); parent && parent.removeChild(container); }; /** * @param {HTMLElement} parent */ this.attachTo = function(parent) { if (!parent) { throw new Error('parent required'); } if (typeof parent === 'string') { parent = query(parent); } this.detach(); parent.appendChild(this._container); }; this.detach = function() { const parentNode = container.parentNode; if (parentNode) { parentNode.removeChild(container); } }; this.get = safeRef((module, strict) => playgroundRef.get(module, strict)); /** * @param {string|Array<string>} [containers] */ this.open = safeRef((containers) => playgroundRef.open(containers)); /** * @param {string|Array<string>} [containers] */ this.collapse = safeRef((containers) => playgroundRef.collapse(containers)); this.setSchema = safeRef((schema) => playgroundRef.setSchema(schema)); this.getSchema = safeRef(() => playgroundRef.getSchema()); this.saveSchema = safeRef(() => playgroundRef.saveSchema()); this.getDataEditor = safeRef(() => playgroundRef.getDataEditor()); this.getEditor = safeRef(() => playgroundRef.getEditor()); this.getForm = safeRef((name, strict) => playgroundRef.getForm(name, strict)); this.getResultView = safeRef(() => playgroundRef.getResultView()); this._onInit = function(_ref) { playgroundRef = _ref; }; render(html` <${PlaygroundComponent} data=${ data } emitter=${ emitter } layoutConfig=${ layoutConfig } onInit=${ this._onInit } schema=${ schema } ...${ restProps } /> `, container ); } /** * @param {CamundaFormPlaygroundOptions} options * * @return {CamundaFormPlayground} */ async function createCamundaFormPlayground(options) { const playground = new CamundaFormPlayground(options); return new Promise((resolve, reject) => { const onInit = () => { return resolve(playground); }; try { playground.once('formPlayground.init', onInit); } catch (err) { return reject(err); } }); } export { CamundaFormPlayground, createCamundaFormPlayground }; //# sourceMappingURL=index.es.js.map