diff --git a/packages/form-js-editor/src/features/properties-panel/entries/SpacerEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/HeightEntry.js similarity index 77% rename from packages/form-js-editor/src/features/properties-panel/entries/SpacerEntry.js rename to packages/form-js-editor/src/features/properties-panel/entries/HeightEntry.js index a2870534e..985277093 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/SpacerEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/HeightEntry.js @@ -3,30 +3,34 @@ import { NumberFieldEntry, isNumberFieldEntryEdited } from '@bpmn-io/properties- import { get } from 'min-dash'; import { useService } from '../hooks'; -export default function SpacerEntry(props) { +export default function HeightEntry(props) { const { editField, field, - id + id, + description, + isDefaultVisible = () => {} } = props; const entries = []; entries.push({ id: id + '-height', - component: SpacerHeight, + component: Height, + description, isEdited: isNumberFieldEntryEdited, editField, field, - isDefaultVisible: (field) => field.type === 'spacer' + isDefaultVisible: (field) => field.type === 'spacer' || isDefaultVisible(field) }); return entries; } -function SpacerHeight(props) { +function Height(props) { const { + description, editField, field, id @@ -46,6 +50,7 @@ function SpacerHeight(props) { return NumberFieldEntry({ debounce, + description, label: 'Height', element: field, id, diff --git a/packages/form-js-editor/src/features/properties-panel/entries/IFrameHeightEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/IFrameHeightEntry.js new file mode 100644 index 000000000..96e5f7346 --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/entries/IFrameHeightEntry.js @@ -0,0 +1,11 @@ +import HeightEntry from './HeightEntry'; + +export default function IFrameHeightEntry(props) { + return [ + ...HeightEntry({ + ...props, + description: 'Height of the container in pixels.', + isDefaultVisible: (field) => field.type === 'iframe' + }) + ]; +} \ No newline at end of file diff --git a/packages/form-js-editor/src/features/properties-panel/entries/IFrameUrlEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/IFrameUrlEntry.js new file mode 100644 index 000000000..3db0b23bd --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/entries/IFrameUrlEntry.js @@ -0,0 +1,74 @@ +import { get } from 'min-dash'; + +import { useService, useVariables } from '../hooks'; + +import { FeelTemplatingEntry, isFeelEntryEdited } from '@bpmn-io/properties-panel'; + +export default function IFrameUrlEntry(props) { + const { + editField, + field + } = props; + + const entries = []; + entries.push({ + id: 'url', + component: Url, + editField: editField, + field: field, + isEdited: isFeelEntryEdited, + isDefaultVisible: (field) => field.type === 'iframe' + }); + + return entries; +} + +function Url(props) { + const { + editField, + field, + id + } = props; + + const debounce = useService('debounce'); + + const variables = useVariables().map(name => ({ name })); + + const path = [ 'url' ]; + + const getValue = () => { + return get(field, path, ''); + }; + + const setValue = (value) => { + return editField(field, path, value); + }; + + return FeelTemplatingEntry({ + debounce, + element: field, + feel: 'optional', + getValue, + id, + label: 'URL', + setValue, + singleLine: true, + tooltip: getTooltip(), + variables + }); +} + +// helper ////////////////////// + +function getTooltip() { + return ( + <> +
+ Enter a URL to an external source or populate it dynamically via a template or a FEEL expression (e.g., to pass a value from the variable). +
++ However, not all external sources can be displayed. Read more about it in the X-FRAME-OPTIONS documentation. +
+ > + ); +} \ No newline at end of file diff --git a/packages/form-js-editor/src/features/properties-panel/entries/LabelEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/LabelEntry.js index b565dec3e..8b704016b 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/LabelEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/LabelEntry.js @@ -51,7 +51,12 @@ export default function LabelEntry(props) { editField, field, isEdited: isFeelEntryEdited, - isDefaultVisible: (field) => INPUTS.includes(field.type) || field.type === 'button' || field.type === 'group' + isDefaultVisible: (field) => ( + INPUTS.includes(field.type) || + field.type === 'button' || + field.type === 'group' || + field.type === 'iframe' + ) } ); @@ -79,7 +84,7 @@ function Label(props) { return editField(field, path, value || ''); }; - const label = field.type === 'group' ? 'Group label' : 'Field label'; + const label = getLabelText(field); return FeelTemplatingEntry({ debounce, @@ -157,4 +162,20 @@ function TimeLabel(props) { setValue, variables }); +} + +// helpers ////////// + +function getLabelText(field) { + const { type } = field; + + if (type === 'group') { + return 'Group label'; + } + + if (type === 'iframe') { + return 'Title'; + } + + return 'Field label'; } \ No newline at end of file diff --git a/packages/form-js-editor/src/features/properties-panel/entries/index.js b/packages/form-js-editor/src/features/properties-panel/entries/index.js index d643e5f60..91c87916c 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/index.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/index.js @@ -9,9 +9,11 @@ export { default as KeyEntry } from './KeyEntry'; export { default as PathEntry } from './PathEntry'; export { default as GroupEntries } from './GroupEntries'; export { default as LabelEntry } from './LabelEntry'; +export { default as IFrameHeightEntry } from './IFrameHeightEntry'; +export { default as IFrameUrlEntry } from './IFrameUrlEntry'; export { default as ImageSourceEntry } from './ImageSourceEntry'; export { default as TextEntry } from './TextEntry'; -export { default as SpacerEntry } from './SpacerEntry'; +export { default as HeightEntry } from './HeightEntry'; export { default as NumberEntries } from './NumberEntries'; export { default as NumberSerializationEntry } from './NumberSerializationEntry'; export { default as DateTimeEntry } from './DateTimeEntry'; diff --git a/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js b/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js index 4aac5bf3b..8b5c96ccb 100644 --- a/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js +++ b/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js @@ -5,6 +5,8 @@ import { DefaultValueEntry, DisabledEntry, IdEntry, + IFrameUrlEntry, + IFrameHeightEntry, ImageSourceEntry, KeyEntry, PathEntry, @@ -13,7 +15,7 @@ import { ReadonlyEntry, SelectEntries, TextEntry, - SpacerEntry, + HeightEntry, NumberEntries, DateTimeEntry } from '../entries'; @@ -32,7 +34,9 @@ export default function GeneralGroup(field, editField, getService) { ...ActionEntry({ field, editField }), ...DateTimeEntry({ field, editField }), ...TextEntry({ field, editField, getService }), - ...SpacerEntry({ field, editField }), + ...IFrameUrlEntry({ field, editField }), + ...IFrameHeightEntry({ field, editField }), + ...HeightEntry({ field, editField }), ...NumberEntries({ field, editField }), ...ImageSourceEntry({ field, editField }), ...AltTextEntry({ field, editField }), diff --git a/packages/form-js-editor/src/render/components/editor-form-fields/EditorIFrame.js b/packages/form-js-editor/src/render/components/editor-form-fields/EditorIFrame.js new file mode 100644 index 000000000..086d90505 --- /dev/null +++ b/packages/form-js-editor/src/render/components/editor-form-fields/EditorIFrame.js @@ -0,0 +1,14 @@ +import { + IFrame +} from '@bpmn-io/form-js-viewer'; + + +export default function EditorIFrame(props) { + const { field } = props; + + // remove url to display placeholder + return ; + +} + +EditorIFrame.config = IFrame.config; diff --git a/packages/form-js-editor/src/render/components/editor-form-fields/index.js b/packages/form-js-editor/src/render/components/editor-form-fields/index.js index 00faa5a9b..2dbafe714 100644 --- a/packages/form-js-editor/src/render/components/editor-form-fields/index.js +++ b/packages/form-js-editor/src/render/components/editor-form-fields/index.js @@ -1,5 +1,7 @@ +import EditorIFrame from './EditorIFrame'; import EditorText from './EditorText'; export const editorFormFields = [ + EditorIFrame, EditorText ]; \ No newline at end of file diff --git a/packages/form-js-playground/test/spec/form.json b/packages/form-js-playground/test/spec/form.json index 7239602b2..df86e14d7 100644 --- a/packages/form-js-playground/test/spec/form.json +++ b/packages/form-js-playground/test/spec/form.json @@ -9,6 +9,10 @@ "columns": 10 } }, + { + "type": "iframe", + "label": "A google doc" + }, { "id": "Group_1", "type": "group", diff --git a/packages/form-js-viewer/assets/form-js-base.css b/packages/form-js-viewer/assets/form-js-base.css index 8e4498527..3e401ea89 100644 --- a/packages/form-js-viewer/assets/form-js-base.css +++ b/packages/form-js-viewer/assets/form-js-base.css @@ -965,6 +965,27 @@ margin: 4px 0; } +.fjs-container .fjs-iframe { + margin: 4px 0; + width: 100%; + border: 1px solid var(--color-borders-readonly); +} + +.fjs-container .fjs-iframe-placeholder { + margin: 4px 0; + height: 90px; + display: flex; + justify-content: center; + background: var(--color-background-readonly); + color: var(--color-text-light); + border: 1px solid var(--color-borders-readonly); +} + +.fjs-container .fjs-iframe-placeholder .fjs-iframe-placeholder-text { + display: flex; + align-items: center; +} + /** * Flatpickr style adjustments */ diff --git a/packages/form-js-viewer/src/render/components/Sanitizer.js b/packages/form-js-viewer/src/render/components/Sanitizer.js index dc85df7c5..7c046941f 100644 --- a/packages/form-js-viewer/src/render/components/Sanitizer.js +++ b/packages/form-js-viewer/src/render/components/Sanitizer.js @@ -45,6 +45,7 @@ const ALLOWED_ATTRIBUTES = [ const ALLOWED_URI_PATTERN = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i; // eslint-disable-line no-useless-escape const ALLOWED_IMAGE_SRC_PATTERN = /^(https?|data):.*/i; // eslint-disable-line no-useless-escape +const ALLOWED_IFRAME_SRC_PATTERN = /^(https?):.*/i; // eslint-disable-line no-useless-escape const ATTR_WHITESPACE_PATTERN = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g; // eslint-disable-line no-control-regex const FORM_ELEMENT = document.createElement('form'); @@ -95,6 +96,20 @@ export function sanitizeImageSource(src) { return valid ? src : ''; } +/** + * Sanitizes an iframe source to ensure we only allow for links + * that start with http(s). + * + * @param {string} src + * @returns {string} + */ +export function sanitizeIFrameSource(src) { + const valid = ALLOWED_IFRAME_SRC_PATTERN.test(src); + + return valid ? src : ''; +} + + /** * Recursively sanitize a HTML node, potentially * removing it, its children or attributes. diff --git a/packages/form-js-viewer/src/render/components/form-fields/IFrame.js b/packages/form-js-viewer/src/render/components/form-fields/IFrame.js new file mode 100644 index 000000000..e3b5dc7c5 --- /dev/null +++ b/packages/form-js-viewer/src/render/components/form-fields/IFrame.js @@ -0,0 +1,76 @@ +import { useContext, useMemo } from 'preact/hooks'; + +import { FormContext } from '../../context'; + +import { useSingleLineTemplateEvaluation } from '../../hooks'; +import { sanitizeIFrameSource } from '../Sanitizer'; + +import Label from '../Label'; + +import { + formFieldClasses, + prefixId +} from '../Util'; + +import { iconsByType } from '../icons'; + +const type = 'iframe'; + +const DEFAULT_HEIGHT = 300; + +export default function IFrame(props) { + const { + field, + disabled, + readonly + } = props; + + const { + height = DEFAULT_HEIGHT, + id, + label, + url + } = field; + + const evaluatedUrl = useSingleLineTemplateEvaluation(url, { debug: true }); + + const safeUrl = useMemo(() => sanitizeIFrameSource(evaluatedUrl), [ evaluatedUrl ]); + + const evaluatedLabel = useSingleLineTemplateEvaluation(label, { debug: true }); + + const { formId } = useContext(FormContext); + + return