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 + }` + ) + ) + : container({ padding: 0 })( + text()(`<${tagName}>`), + dropzone(values.tagUseRepeat ? values.tagContentRepeatContainer : values.tagContentContainer), + text()(``) + ); + + 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[]; }