diff --git a/packages/pluggableWidgets/html-element-web/src/HTMLElement.editorConfig.ts b/packages/pluggableWidgets/html-element-web/src/HTMLElement.editorConfig.ts
index 35c078284e..cd23cd6397 100644
--- a/packages/pluggableWidgets/html-element-web/src/HTMLElement.editorConfig.ts
+++ b/packages/pluggableWidgets/html-element-web/src/HTMLElement.editorConfig.ts
@@ -1,26 +1,17 @@
import { AttributeValueTypeEnum, HTMLElementPreviewProps } from "../typings/HTMLElementProps";
import { hideNestedPropertiesIn, hidePropertiesIn, Problem, Properties } from "@mendix/pluggable-widgets-tools";
+import {
+ container,
+ ContainerProps,
+ datasource,
+ dropzone,
+ StructurePreviewProps,
+ text
+} from "@mendix/pluggable-widgets-commons";
+import { isVoidElement, prepareTag } from "./utils/props-utils";
type TagAttributeValuePropName = keyof HTMLElementPreviewProps["attributes"][number];
-const voidElements = [
- "area",
- "base",
- "br",
- "col",
- "embed",
- "hr",
- "img",
- "input",
- "link",
- "meta",
- "source",
- "track",
- "wbr",
- // react specific, it uses `value` prop
- "textarea"
-];
-
const disabledElements = ["script"];
function isValidHtmlTagName(name: string): boolean {
@@ -84,7 +75,7 @@ export function getProperties(values: HTMLElementPreviewProps, defaultProperties
const tagName = values.tagName === "__customTag__" ? values.tagNameCustom : values.tagName;
- if (voidElements.includes(tagName)) {
+ if (isVoidElement(tagName)) {
// void elements don't allow children, hide all content props and the content mode switch
propsToHide.push(
"tagContentMode",
@@ -139,47 +130,65 @@ export function check(values: HTMLElementPreviewProps): Problem[] {
}
}
- if (values.tagUseRepeat && values.tagContentRepeatDataSource === null) {
- // make date source required if set to repeat
- errors.push({
- severity: "error",
- property: "tagContentRepeatDataSource",
- message: "Property 'Data source' is required."
- });
- } else {
- const existingAttributeNames = new Set();
- values.attributes.forEach((attr, i) => {
- if (existingAttributeNames.has(attr.attributeName)) {
- errors.push({
- severity: "error",
- property: `attributes/${i + 1}/attributeName`,
- message: `Attribute with name '${attr.attributeName}' already exists.`
- });
- }
- existingAttributeNames.add(attr.attributeName);
-
- const attributePropName = attributeValuePropNameFor(values, attr.attributeValueType);
- if (!attr[attributePropName].length) {
- errors.push({
- severity: "warning",
- property: `attributes/${i + 1}/${attributePropName}`,
- message: `Value is not specified for attribute '${attr.attributeName}'.`
- });
- }
- });
-
- const existingEventNames = new Set();
- values.events.forEach((attr, i) => {
- if (existingEventNames.has(attr.eventName)) {
- errors.push({
- severity: "error",
- property: `attributes/${i + 1}/eventName`,
- message: `Event with name '${attr.eventName}' already exists.`
- });
- }
- existingEventNames.add(attr.eventName);
- });
- }
+ const existingAttributeNames = new Set();
+ values.attributes.forEach((attr, i) => {
+ if (existingAttributeNames.has(attr.attributeName)) {
+ errors.push({
+ severity: "error",
+ property: `attributes/${i + 1}/attributeName`,
+ message: `Attribute with name '${attr.attributeName}' already exists.`
+ });
+ }
+ existingAttributeNames.add(attr.attributeName);
+
+ const attributePropName = attributeValuePropNameFor(values, attr.attributeValueType);
+ if (!attr[attributePropName].length) {
+ errors.push({
+ severity: "warning",
+ property: `attributes/${i + 1}/${attributePropName}`,
+ message: `Value is not specified for attribute '${attr.attributeName}'.`
+ });
+ }
+ });
+
+ const existingEventNames = new Set();
+ values.events.forEach((attr, i) => {
+ if (existingEventNames.has(attr.eventName)) {
+ errors.push({
+ severity: "error",
+ property: `attributes/${i + 1}/eventName`,
+ message: `Event with name '${attr.eventName}' already exists.`
+ });
+ }
+ existingEventNames.add(attr.eventName);
+ });
return errors;
}
+
+export function getPreview(values: HTMLElementPreviewProps, _isDarkMode: boolean): StructurePreviewProps | null {
+ const tagName = prepareTag(values.tagName, values.tagNameCustom);
+
+ const voidElementPreview = (tagName: keyof JSX.IntrinsicElements): ContainerProps =>
+ container({ padding: 4 })(text()(`<${tagName} />`));
+
+ const flowElementPreview = (): ContainerProps =>
+ values.tagContentMode === "innerHTML"
+ ? container({ padding: 4 })(
+ text()(
+ `<${tagName}>${
+ values.tagUseRepeat ? values.tagContentRepeatHTML : values.tagContentHTML
+ }${tagName}>`
+ )
+ )
+ : container({ padding: 0 })(
+ text()(`<${tagName}>`),
+ dropzone(values.tagUseRepeat ? values.tagContentRepeatContainer : values.tagContentContainer),
+ text()(`${tagName}>`)
+ );
+
+ return container({ grow: 1, borders: true, borderWidth: 1 })(
+ values.tagContentRepeatDataSource ? datasource(values.tagContentRepeatDataSource)() : container()(),
+ isVoidElement(tagName) ? voidElementPreview(tagName) : flowElementPreview()
+ );
+}
diff --git a/packages/pluggableWidgets/html-element-web/src/HTMLElement.editorPreview.tsx b/packages/pluggableWidgets/html-element-web/src/HTMLElement.editorPreview.tsx
index d2f8a61821..71b5fe0cff 100644
--- a/packages/pluggableWidgets/html-element-web/src/HTMLElement.editorPreview.tsx
+++ b/packages/pluggableWidgets/html-element-web/src/HTMLElement.editorPreview.tsx
@@ -1,8 +1,41 @@
-import { ReactElement, createElement } from "react";
+import { ReactElement, createElement, Fragment } from "react";
import { HTMLElementPreviewProps } from "../typings/HTMLElementProps";
+import { HTMLTag } from "./components/HTMLTag";
+import { isVoidElement, prepareTag } from "./utils/props-utils";
export function preview(props: HTMLElementPreviewProps): ReactElement {
- return
HTML Element
;
+ console.dir(props, { depth: 4 });
+ const tag = prepareTag(props.tagName, props.tagNameCustom);
+
+ const items = props.tagUseRepeat ? [1, 2, 3] : [1];
+
+ return (
+
+ {items.map(i =>
+ isVoidElement(tag) ? (
+ createElement(tag, { className: props.className, style: props.styleObject })
+ ) : (
+
+ {props.tagContentRepeatHTML}
+ {props.tagContentHTML}
+
+
+
+
+
+
+
+ )
+ )}
+
+ );
}
export function getPreviewCss(): string {
diff --git a/packages/pluggableWidgets/html-element-web/src/HTMLElement.xml b/packages/pluggableWidgets/html-element-web/src/HTMLElement.xml
index 1eafeb34b5..5330a2e153 100644
--- a/packages/pluggableWidgets/html-element-web/src/HTMLElement.xml
+++ b/packages/pluggableWidgets/html-element-web/src/HTMLElement.xml
@@ -5,7 +5,7 @@
-
+
Tag name
@@ -27,67 +27,23 @@
Use custom name
-
Custom tag
-
-
- Attributes
-
-
-
-
- Name
-
-
-
-
- Value based on
-
-
- Expression
- Text template
-
-
-
- Value
-
-
-
- Value
-
-
-
-
- Value
-
-
-
- Value
-
-
-
-
-
-
-
-
-
Repeat element
Repeat element for each item in data source.
-
-
+
Data source
-
-
+
+
+
Content
@@ -97,7 +53,6 @@
-
HTML
@@ -123,6 +78,48 @@
+
+
+ Attributes
+ The HTML attributes that are added to the HTML element. For example: ‘title‘, ‘href‘. If ‘class’ or ‘style’ is added as attribute this is merged with the widget class/style property. For events (e.g. onClick) use the Events section.
+
+
+
+ Name
+
+
+
+
+ Value based on
+
+
+ Expression
+ Text template
+
+
+
+
+ Value
+
+
+
+ Value
+
+
+
+
+ Value
+
+
+
+ Value
+
+
+
+
+
+
+
diff --git a/packages/pluggableWidgets/html-element-web/src/utils/props-utils.ts b/packages/pluggableWidgets/html-element-web/src/utils/props-utils.ts
index 167ea5ac68..eb119529dd 100644
--- a/packages/pluggableWidgets/html-element-web/src/utils/props-utils.ts
+++ b/packages/pluggableWidgets/html-element-web/src/utils/props-utils.ts
@@ -116,3 +116,26 @@ export function prepareChildren(
return props.tagContentRepeatContainer?.get(item);
}
+
+const voidElements = [
+ "area",
+ "base",
+ "br",
+ "col",
+ "embed",
+ "hr",
+ "img",
+ "input",
+ "link",
+ "meta",
+ "source",
+ "track",
+ "wbr",
+ "textarea"
+] as const;
+
+export type VoidElement = typeof voidElements[number];
+
+export function isVoidElement(tag: unknown): tag is VoidElement {
+ return voidElements.includes(tag as VoidElement);
+}
diff --git a/packages/pluggableWidgets/html-element-web/src/utils/style-utils.ts b/packages/pluggableWidgets/html-element-web/src/utils/style-utils.ts
index eb4488a719..e5f69b7487 100644
--- a/packages/pluggableWidgets/html-element-web/src/utils/style-utils.ts
+++ b/packages/pluggableWidgets/html-element-web/src/utils/style-utils.ts
@@ -1,11 +1,18 @@
import React from "react";
+// We need regexp to split rows in prop/value pairs
+// split by : not work for all cases, eg "background-image: url(http://localhost:8080);"
+const cssPropRegex = /(?[^:]+):\s+?(?[^;]+);?/m;
+
export function convertInlineCssToReactStyle(inlineStyle: string): React.CSSProperties {
return Object.fromEntries(
inlineStyle
.split(";") // split by ;
.filter(r => r.length) // filter out empty
- .map(r => r.split(":").map(v => v.trim())) // split by key and value by :
+ .map(r => {
+ const { prop = "", value = "" } = cssPropRegex.exec(r.trim())?.groups ?? {};
+ return [prop, value];
+ })
.filter(v => v.length === 2 && v[0].length && v[1].length) // filter out broken lines
.map(([key, value]) => [convertStylePropNameToReactPropName(key), value] as [string, string])
);
diff --git a/packages/pluggableWidgets/html-element-web/typings/HTMLElementProps.d.ts b/packages/pluggableWidgets/html-element-web/typings/HTMLElementProps.d.ts
index cce14817c0..9c8c60ea65 100644
--- a/packages/pluggableWidgets/html-element-web/typings/HTMLElementProps.d.ts
+++ b/packages/pluggableWidgets/html-element-web/typings/HTMLElementProps.d.ts
@@ -8,6 +8,8 @@ import { ActionValue, DynamicValue, ListValue, ListActionValue, ListExpressionVa
export type TagNameEnum = "div" | "span" | "p" | "ul" | "ol" | "li" | "a" | "img" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "__customTag__";
+export type TagContentModeEnum = "container" | "innerHTML";
+
export type AttributeValueTypeEnum = "expression" | "template";
export interface AttributesType {
@@ -19,8 +21,6 @@ export interface AttributesType {
attributeValueExpressionRepeat?: ListExpressionValue;
}
-export type TagContentModeEnum = "container" | "innerHTML";
-
export type EventNameEnum = "onClick" | "onChange" | "onFocus" | "onLeave" | "onSubmit" | "onKeyDown" | "onKeyUp" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture";
export interface EventsType {
@@ -55,14 +55,14 @@ export interface HTMLElementContainerProps {
tabIndex?: number;
tagName: TagNameEnum;
tagNameCustom: string;
- attributes: AttributesType[];
tagUseRepeat: boolean;
- tagContentRepeatDataSource?: ListValue;
+ tagContentRepeatDataSource: ListValue;
tagContentMode: TagContentModeEnum;
tagContentHTML?: DynamicValue;
tagContentContainer?: ReactNode;
tagContentRepeatHTML?: ListExpressionValue;
tagContentRepeatContainer?: ListWidgetValue;
+ attributes: AttributesType[];
events: EventsType[];
}
@@ -73,7 +73,6 @@ export interface HTMLElementPreviewProps {
readOnly: boolean;
tagName: TagNameEnum;
tagNameCustom: string;
- attributes: AttributesPreviewType[];
tagUseRepeat: boolean;
tagContentRepeatDataSource: {} | { type: string } | null;
tagContentMode: TagContentModeEnum;
@@ -81,5 +80,6 @@ export interface HTMLElementPreviewProps {
tagContentContainer: { widgetCount: number; renderer: ComponentType<{ caption?: string }> };
tagContentRepeatHTML: string;
tagContentRepeatContainer: { widgetCount: number; renderer: ComponentType<{ caption?: string }> };
+ attributes: AttributesPreviewType[];
events: EventsPreviewType[];
}