Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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()
);
}
Original file line number Diff line number Diff line change
@@ -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 <div className={props.className}>HTML Element</div>;
console.dir(props, { depth: 4 });
const tag = prepareTag(props.tagName, props.tagNameCustom);

const items = props.tagUseRepeat ? [1, 2, 3] : [1];

return (
<Fragment>
{items.map(i =>
isVoidElement(tag) ? (
createElement(tag, { className: props.className, style: props.styleObject })
) : (
<HTMLTag
key={i}
tagName={tag}
attributes={{
className: props.className,
style: props.styleObject
}}
>
{props.tagContentRepeatHTML}
{props.tagContentHTML}
<props.tagContentRepeatContainer.renderer>
<div />
</props.tagContentRepeatContainer.renderer>
<props.tagContentContainer.renderer>
<div />
</props.tagContentContainer.renderer>
</HTMLTag>
)
)}
</Fragment>
);
}

export function getPreviewCss(): string {
Expand Down
97 changes: 47 additions & 50 deletions packages/pluggableWidgets/html-element-web/src/HTMLElement.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<icon />
<properties>
<propertyGroup caption="General">
<propertyGroup caption="Tag">
<propertyGroup caption="HTML Element">
<property key="tagName" type="enumeration" defaultValue="div" required="true">
<caption>Tag name</caption>
<description />
Expand All @@ -27,67 +27,23 @@
<enumerationValue key="__customTag__">Use custom name</enumerationValue>
</enumerationValues>
</property>

<property key="tagNameCustom" type="string" defaultValue="div" required="false">
<caption>Custom tag</caption>
<description />
</property>
<!-- **************************** TAG ATTRIBUTES ****************************** -->
<property key="attributes" type="object" isList="true" required="false">
<caption>Attributes</caption>
<description />
<properties>
<propertyGroup caption="Attributes">
<property key="attributeName" type="string" required="true">
<caption>Name</caption>
<description />
</property>

<property key="attributeValueType" type="enumeration" defaultValue="expression">
<caption>Value based on</caption>
<description />
<enumerationValues>
<enumerationValue key="expression">Expression</enumerationValue>
<enumerationValue key="template">Text template</enumerationValue>
</enumerationValues>
</property>

<property key="attributeValueTemplate" type="textTemplate" required="false">
<caption>Value</caption>
<description />
</property>
<property key="attributeValueExpression" type="expression" required="false">
<caption>Value</caption>
<description />
<returnType type="String" />
</property>
<property key="attributeValueTemplateRepeat" type="textTemplate" dataSource="../tagContentRepeatDataSource" required="false">
<caption>Value</caption>
<description />
</property>
<property key="attributeValueExpressionRepeat" type="expression" dataSource="../tagContentRepeatDataSource" required="false">
<caption>Value</caption>
<description />
<returnType type="String" />
</property>
</propertyGroup>
</properties>
</property>
<!-- **************************** END TAG ATTRIBUTES ****************************** -->
</propertyGroup>
<propertyGroup caption="Repeat">
<property key="tagUseRepeat" type="boolean" defaultValue="false">
<caption>Repeat element</caption>
<description>Repeat element for each item in data source.</description>
</property>

<!-- Data source for repeating content -->
<property key="tagContentRepeatDataSource" type="datasource" required="false" isList="true">
<property key="tagContentRepeatDataSource" type="datasource" required="true" isList="true">
<caption>Data source</caption>
<description />
</property>
</propertyGroup>
<propertyGroup caption="Content">

<!-- **************************** TAG CONTENT ****************************** -->
<!-- Content mode -->
<property key="tagContentMode" type="enumeration" defaultValue="container">
<caption>Content</caption>
<description />
Expand All @@ -97,7 +53,6 @@
</enumerationValues>
</property>

<!-- **************************** TAG CONTENT ****************************** -->
<!-- HTML content, non-repeating -->
<property key="tagContentHTML" type="textTemplate" multiline="true" required="false">
<caption>HTML</caption>
Expand All @@ -123,6 +78,48 @@
</property>
<!-- **************************** END TAG CONTENT ****************************** -->
</propertyGroup>
<propertyGroup caption="HTML Attributes">
<property key="attributes" type="object" isList="true" required="false">
<caption>Attributes</caption>
<description>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.</description>
<properties>
<propertyGroup caption="Attributes">
<property key="attributeName" type="string" required="true">
<caption>Name</caption>
<description />
</property>

<property key="attributeValueType" type="enumeration" defaultValue="expression">
<caption>Value based on</caption>
<description />
<enumerationValues>
<enumerationValue key="expression">Expression</enumerationValue>
<enumerationValue key="template">Text template</enumerationValue>
</enumerationValues>
</property>

<property key="attributeValueTemplate" type="textTemplate" required="false">
<caption>Value</caption>
<description />
</property>
<property key="attributeValueExpression" type="expression" required="false">
<caption>Value</caption>
<description />
<returnType type="String" />
</property>
<property key="attributeValueTemplateRepeat" type="textTemplate" dataSource="../tagContentRepeatDataSource" required="false">
<caption>Value</caption>
<description />
</property>
<property key="attributeValueExpressionRepeat" type="expression" dataSource="../tagContentRepeatDataSource" required="false">
<caption>Value</caption>
<description />
<returnType type="String" />
</property>
</propertyGroup>
</properties>
</property>
</propertyGroup>
</propertyGroup>
<propertyGroup caption="Events">
<property key="events" type="object" isList="true" required="false">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -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 = /(?<prop>[^:]+):\s+?(?<value>[^;]+);?/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])
);
Expand Down
Loading