@camunda/form-playground
Version:
Camunda Form Playground.
549 lines (443 loc) • 15.5 kB
JavaScript
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