Skip to content

Commit 6b9c753

Browse files
author
Niklas Kiefer
committed
feat(viewer): support image view
1 parent 5b7dffa commit 6b9c753

File tree

13 files changed

+424
-6
lines changed

13 files changed

+424
-6
lines changed

packages/form-js-viewer/assets/form-js.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,3 +482,17 @@
482482
padding: 6px 8px;
483483
color: var(--color-text-lighter);
484484
}
485+
486+
.fjs-container .fjs-image-container {
487+
width: fit-content;
488+
height: fit-content;
489+
}
490+
491+
.fjs-container .fjs-image {
492+
object-fit: contain;
493+
}
494+
495+
.fjs-container .fjs-image-placeholder {
496+
height: 64px;
497+
margin: 2px 0;
498+
}

packages/form-js-viewer/src/render/components/Sanitizer.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const ALLOWED_ATTRIBUTES = [
3737
];
3838

3939
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
40+
const ALLOWED_IMAGE_SRC_PATTERN = /^(https?|data):.*/i; // eslint-disable-line no-useless-escape
4041
const ATTR_WHITESPACE_PATTERN = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g; // eslint-disable-line no-control-regex
4142

4243
const FORM_ELEMENT = document.createElement('form');
@@ -70,6 +71,12 @@ export function sanitizeHTML(html) {
7071
}
7172
}
7273

74+
export function sanitizeImageSource(src) {
75+
const valid = ALLOWED_IMAGE_SRC_PATTERN.test(src);
76+
77+
return valid ? src : '';
78+
}
79+
7380
/**
7481
* Recursively sanitize a HTML node, potentially
7582
* removing it, its children or attributes.

packages/form-js-viewer/src/render/components/Util.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import snarkdown from '@bpmn-io/snarkdown';
22
import { get } from 'min-dash';
33
import classNames from 'classnames';
44

5-
import { sanitizeHTML } from './Sanitizer';
5+
import { sanitizeHTML, sanitizeImageSource } from './Sanitizer';
66

77
export function formFieldClasses(type, { errors = [], disabled = false } = {}) {
88
if (!type) {
@@ -43,6 +43,19 @@ export function safeMarkdown(markdown) {
4343
return sanitizeHTML(html);
4444
}
4545

46+
/**
47+
* Sanitizes an image source to ensure we only allow for data URI and links
48+
* that start with http(s).
49+
*
50+
* Note: Most browsers anyway do not support script execution in <img> elements.
51+
*
52+
* @param {string} src
53+
* @returns {string}
54+
*/
55+
export function safeImageSource(src) {
56+
return sanitizeImageSource(src);
57+
}
58+
4659
export function sanitizeSingleSelectValue(options) {
4760
const {
4861
formField,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useContext } from 'preact/hooks';
2+
3+
import { FormContext } from '../../context';
4+
5+
import { useExpressionValue } from '../../hooks/useExpressionValue';
6+
7+
import {
8+
formFieldClasses,
9+
prefixId,
10+
safeImageSource
11+
} from '../Util';
12+
13+
import ImagePlaceholder from './icons/ImagePlaceholder.svg';
14+
15+
const type = 'image';
16+
17+
18+
export default function Image(props) {
19+
const {
20+
field
21+
} = props;
22+
23+
const {
24+
alt,
25+
id,
26+
source
27+
} = field;
28+
29+
const safeSource = safeImageSource(useExpressionValue(source));
30+
const altText = useExpressionValue(alt);
31+
32+
const { formId } = useContext(FormContext);
33+
34+
return <div class={ formFieldClasses(type) }>
35+
<div class="fjs-image-container">
36+
{
37+
safeSource &&
38+
<img
39+
alt={ altText }
40+
src={ safeSource }
41+
class="fjs-image"
42+
id={ prefixId(id, formId) } />
43+
}
44+
{ !safeSource &&
45+
<div class="fjs-image-placeholder">
46+
<ImagePlaceholder alt="This is an image placeholder" />
47+
</div>
48+
}
49+
</div>
50+
</div>;
51+
}
52+
53+
Image.create = function(options = {}) {
54+
return {
55+
...options
56+
};
57+
};
58+
59+
Image.type = type;
60+
Image.keyed = false;
Lines changed: 8 additions & 0 deletions
Loading

packages/form-js-viewer/src/render/components/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Checkbox from './form-fields/Checkbox';
33
import Checklist from './form-fields/Checklist';
44
import Default from './form-fields/Default';
55
import FormComponent from './FormComponent';
6+
import Image from './form-fields/Image';
67
import Numberfield from './form-fields/Number';
78
import Radio from './form-fields/Radio';
89
import Select from './form-fields/Select';
@@ -17,6 +18,7 @@ export {
1718
Checklist,
1819
Default,
1920
FormComponent,
21+
Image,
2022
Numberfield,
2123
Radio,
2224
Select,
@@ -31,6 +33,7 @@ export const formFields = [
3133
Checkbox,
3234
Checklist,
3335
Default,
36+
Image,
3437
Numberfield,
3538
Radio,
3639
Select,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import useService from './useService.js';
2+
3+
/**
4+
*
5+
* @param {string | undefined} expression
6+
* @param {import('../../types').Data} data
7+
*/
8+
export function useEvaluation(expression, data) {
9+
const conditionChecker = useService('conditionChecker', false);
10+
11+
if (!conditionChecker) {
12+
return null;
13+
}
14+
15+
return conditionChecker.evaluate(expression, data);
16+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { isString } from 'min-dash';
2+
3+
import useService from './useService';
4+
import { useEvaluation } from './useEvaluation';
5+
6+
/**
7+
*
8+
* @param {string} value
9+
*/
10+
export function useExpressionValue(value) {
11+
const formData = useService('form')._getState().data;
12+
13+
if (!isExpression(value)) {
14+
return value;
15+
}
16+
17+
// We can ignore this hook rule as we do not use
18+
// state or effects in our custom hooks
19+
/* eslint-disable-next-line react-hooks/rules-of-hooks */
20+
return useEvaluation(value, formData);
21+
}
22+
23+
24+
// helper ///////////////
25+
26+
function isExpression(value) {
27+
return isString(value) && value.startsWith('=');
28+
}

packages/form-js-viewer/test/spec/Form.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe('Form', function() {
9595
});
9696

9797
// then
98-
expect(form.get('formFieldRegistry').getAll()).to.have.length(14);
98+
expect(form.get('formFieldRegistry').getAll()).to.have.length(15);
9999
});
100100

101101

@@ -174,7 +174,7 @@ describe('Form', function() {
174174
await form.importSchema(schemaNoIds, data);
175175

176176
// then
177-
expect(form.get('formFieldRegistry').getAll()).to.have.length(14);
177+
expect(form.get('formFieldRegistry').getAll()).to.have.length(15);
178178

179179
form.get('formFieldRegistry').forEach(field => {
180180
expect(field.id).to.exist;
@@ -212,7 +212,7 @@ describe('Form', function() {
212212
await form.importSchema(schema, data);
213213

214214
// then
215-
expect(form.get('formFieldRegistry').getAll()).to.have.length(14);
215+
expect(form.get('formFieldRegistry').getAll()).to.have.length(15);
216216
});
217217

218218

packages/form-js-viewer/test/spec/form.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@
121121
}
122122
]
123123
},
124+
{
125+
"alt": "The bpmn.io logo",
126+
"type": "image"
127+
},
124128
{
125129
"key": "submit",
126130
"label": "Submit",

0 commit comments

Comments
 (0)