@webstudio-is/react-sdk 0.50.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app/custom-components/image.js +2 -4
- package/lib/app/custom-components/shared/remix-link.js +7 -23
- package/lib/cjs/app/custom-components/image.cjs +1 -3
- package/lib/cjs/app/custom-components/shared/remix-link.cjs +6 -22
- package/lib/cjs/components/__generated__/link-block.props.cjs +5 -5
- package/lib/cjs/components/__generated__/link.props.cjs +6 -6
- package/lib/cjs/components/__generated__/rich-text-link.props.cjs +5 -5
- package/lib/cjs/components/link-block.ws.cjs +6 -2
- package/lib/cjs/components/link.cjs +12 -5
- package/lib/cjs/components/link.ws.cjs +8 -1
- package/lib/cjs/components/rich-text-link.ws.cjs +5 -2
- package/lib/cjs/context.cjs +2 -1
- package/lib/cjs/props.cjs +43 -2
- package/lib/cjs/tree/create-elements-tree.cjs +8 -1
- package/lib/cjs/tree/root.cjs +1 -0
- package/lib/components/__generated__/link-block.props.js +5 -5
- package/lib/components/__generated__/link.props.js +6 -6
- package/lib/components/__generated__/rich-text-link.props.js +5 -5
- package/lib/components/link-block.ws.js +6 -2
- package/lib/components/link.js +12 -5
- package/lib/components/link.ws.js +8 -1
- package/lib/components/rich-text-link.ws.js +6 -3
- package/lib/context.js +2 -1
- package/lib/props.js +43 -2
- package/lib/tree/create-elements-tree.js +8 -1
- package/lib/tree/root.js +1 -0
- package/package.json +9 -9
- package/src/app/custom-components/image.tsx +4 -7
- package/src/app/custom-components/shared/remix-link.tsx +12 -48
- package/src/components/__generated__/link-block.props.ts +5 -5
- package/src/components/__generated__/link.props.ts +6 -6
- package/src/components/__generated__/rich-text-link.props.ts +5 -5
- package/src/components/link-block.ws.tsx +6 -2
- package/src/components/link.tsx +12 -6
- package/src/components/link.ws.tsx +8 -1
- package/src/components/rich-text-link.ws.tsx +6 -3
- package/src/context.tsx +3 -1
- package/src/props.test.ts +95 -0
- package/src/props.ts +59 -3
- package/src/tree/create-elements-tree.tsx +6 -2
- package/src/tree/root.ts +2 -0
|
@@ -5,14 +5,12 @@ import {
|
|
|
5
5
|
} from "react";
|
|
6
6
|
import { Image as WebstudioImage, loaders } from "@webstudio-is/image";
|
|
7
7
|
import { Image as SdkImage } from "../../components/image";
|
|
8
|
-
import { usePropAsset } from "../../props";
|
|
9
|
-
import { idAttribute } from "../../tree/webstudio-component";
|
|
8
|
+
import { usePropAsset, getInstanceIdFromComponentProps } from "../../props";
|
|
10
9
|
import { getParams } from "../params";
|
|
11
10
|
const defaultTag = "img";
|
|
12
11
|
const Image = forwardRef(
|
|
13
12
|
(props, ref) => {
|
|
14
|
-
const
|
|
15
|
-
const asset = usePropAsset(componentId, "src");
|
|
13
|
+
const asset = usePropAsset(getInstanceIdFromComponentProps(props), "src");
|
|
16
14
|
const params = getParams();
|
|
17
15
|
const loader = useMemo(() => {
|
|
18
16
|
if (asset === void 0) {
|
|
@@ -1,30 +1,14 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Link } from "@remix-run/react";
|
|
2
|
+
import { Link as RemixLink } from "@remix-run/react";
|
|
3
3
|
import { forwardRef } from "react";
|
|
4
|
-
|
|
5
|
-
try {
|
|
6
|
-
new URL(href);
|
|
7
|
-
return true;
|
|
8
|
-
} catch {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
const isAbsoluteUrlRemix = (href) => /^[a-z+]+:\/\//i.test(href) || href.startsWith("//");
|
|
4
|
+
import { usePropUrl, getInstanceIdFromComponentProps } from "../../../props";
|
|
13
5
|
const wrapLinkComponent = (BaseLink) => {
|
|
14
|
-
const Component = forwardRef((
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return /* @__PURE__ */ jsx(
|
|
19
|
-
BaseLink,
|
|
20
|
-
{
|
|
21
|
-
...props,
|
|
22
|
-
href: willRemixTryToTreatAsAbsoluteAndCrash ? "" : href,
|
|
23
|
-
ref
|
|
24
|
-
}
|
|
25
|
-
);
|
|
6
|
+
const Component = forwardRef((props, ref) => {
|
|
7
|
+
const href = usePropUrl(getInstanceIdFromComponentProps(props), "href");
|
|
8
|
+
if (typeof href === "string" || href === void 0) {
|
|
9
|
+
return /* @__PURE__ */ jsx(BaseLink, { ...props, ref });
|
|
26
10
|
}
|
|
27
|
-
return /* @__PURE__ */ jsx(
|
|
11
|
+
return /* @__PURE__ */ jsx(RemixLink, { ...props, to: href.path, ref });
|
|
28
12
|
});
|
|
29
13
|
Component.displayName = BaseLink.displayName;
|
|
30
14
|
return Component;
|
|
@@ -26,13 +26,11 @@ var import_react = require("react");
|
|
|
26
26
|
var import_image = require("@webstudio-is/image");
|
|
27
27
|
var import_image2 = require("../../components/image");
|
|
28
28
|
var import_props = require("../../props");
|
|
29
|
-
var import_webstudio_component = require("../../tree/webstudio-component");
|
|
30
29
|
var import_params = require("../params");
|
|
31
30
|
const defaultTag = "img";
|
|
32
31
|
const Image = (0, import_react.forwardRef)(
|
|
33
32
|
(props, ref) => {
|
|
34
|
-
const
|
|
35
|
-
const asset = (0, import_props.usePropAsset)(componentId, "src");
|
|
33
|
+
const asset = (0, import_props.usePropAsset)((0, import_props.getInstanceIdFromComponentProps)(props), "src");
|
|
36
34
|
const params = (0, import_params.getParams)();
|
|
37
35
|
const loader = (0, import_react.useMemo)(() => {
|
|
38
36
|
if (asset === void 0) {
|
|
@@ -24,30 +24,14 @@ module.exports = __toCommonJS(remix_link_exports);
|
|
|
24
24
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
25
25
|
var import_react = require("@remix-run/react");
|
|
26
26
|
var import_react2 = require("react");
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
new URL(href);
|
|
30
|
-
return true;
|
|
31
|
-
} catch {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
const isAbsoluteUrlRemix = (href) => /^[a-z+]+:\/\//i.test(href) || href.startsWith("//");
|
|
27
|
+
var import_props = require("../../../props");
|
|
36
28
|
const wrapLinkComponent = (BaseLink) => {
|
|
37
|
-
const Component = (0, import_react2.forwardRef)((
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
42
|
-
BaseLink,
|
|
43
|
-
{
|
|
44
|
-
...props,
|
|
45
|
-
href: willRemixTryToTreatAsAbsoluteAndCrash ? "" : href,
|
|
46
|
-
ref
|
|
47
|
-
}
|
|
48
|
-
);
|
|
29
|
+
const Component = (0, import_react2.forwardRef)((props, ref) => {
|
|
30
|
+
const href = (0, import_props.usePropUrl)((0, import_props.getInstanceIdFromComponentProps)(props), "href");
|
|
31
|
+
if (typeof href === "string" || href === void 0) {
|
|
32
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BaseLink, { ...props, ref });
|
|
49
33
|
}
|
|
50
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Link, { ...props, to: href, ref });
|
|
34
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Link, { ...props, to: href.path, ref });
|
|
51
35
|
});
|
|
52
36
|
Component.displayName = BaseLink.displayName;
|
|
53
37
|
return Component;
|
|
@@ -25,18 +25,18 @@ const props = {
|
|
|
25
25
|
slot: { required: false, control: "text", type: "string" },
|
|
26
26
|
style: { required: false, control: "text", type: "string" },
|
|
27
27
|
title: { required: false, control: "text", type: "string" },
|
|
28
|
-
download: { required: false, control: "text", type: "string" },
|
|
29
28
|
href: { required: false, control: "text", type: "string" },
|
|
30
|
-
hrefLang: { required: false, control: "text", type: "string" },
|
|
31
|
-
media: { required: false, control: "text", type: "string" },
|
|
32
|
-
ping: { required: false, control: "text", type: "string" },
|
|
33
|
-
rel: { required: false, control: "text", type: "string" },
|
|
34
29
|
target: {
|
|
35
30
|
required: false,
|
|
36
31
|
control: "select",
|
|
37
32
|
type: "string",
|
|
38
33
|
options: ["_self", "_blank", "_parent", "_top"]
|
|
39
34
|
},
|
|
35
|
+
download: { required: false, control: "text", type: "string" },
|
|
36
|
+
hrefLang: { required: false, control: "text", type: "string" },
|
|
37
|
+
media: { required: false, control: "text", type: "string" },
|
|
38
|
+
ping: { required: false, control: "text", type: "string" },
|
|
39
|
+
rel: { required: false, control: "text", type: "string" },
|
|
40
40
|
type: { required: false, control: "text", type: "string" },
|
|
41
41
|
referrerPolicy: {
|
|
42
42
|
required: false,
|
|
@@ -25,18 +25,18 @@ const props = {
|
|
|
25
25
|
slot: { required: false, control: "text", type: "string" },
|
|
26
26
|
style: { required: false, control: "text", type: "string" },
|
|
27
27
|
title: { required: false, control: "text", type: "string" },
|
|
28
|
-
|
|
29
|
-
href: { required: false, control: "text", type: "string", defaultValue: "" },
|
|
30
|
-
hrefLang: { required: false, control: "text", type: "string" },
|
|
31
|
-
media: { required: false, control: "text", type: "string" },
|
|
32
|
-
ping: { required: false, control: "text", type: "string" },
|
|
33
|
-
rel: { required: false, control: "text", type: "string" },
|
|
28
|
+
href: { required: false, control: "text", type: "string" },
|
|
34
29
|
target: {
|
|
35
30
|
required: false,
|
|
36
31
|
control: "select",
|
|
37
32
|
type: "string",
|
|
38
33
|
options: ["_self", "_blank", "_parent", "_top"]
|
|
39
34
|
},
|
|
35
|
+
download: { required: false, control: "text", type: "string" },
|
|
36
|
+
hrefLang: { required: false, control: "text", type: "string" },
|
|
37
|
+
media: { required: false, control: "text", type: "string" },
|
|
38
|
+
ping: { required: false, control: "text", type: "string" },
|
|
39
|
+
rel: { required: false, control: "text", type: "string" },
|
|
40
40
|
type: { required: false, control: "text", type: "string" },
|
|
41
41
|
referrerPolicy: {
|
|
42
42
|
required: false,
|
|
@@ -25,18 +25,18 @@ const props = {
|
|
|
25
25
|
slot: { required: false, control: "text", type: "string" },
|
|
26
26
|
style: { required: false, control: "text", type: "string" },
|
|
27
27
|
title: { required: false, control: "text", type: "string" },
|
|
28
|
-
download: { required: false, control: "text", type: "string" },
|
|
29
28
|
href: { required: false, control: "text", type: "string" },
|
|
30
|
-
hrefLang: { required: false, control: "text", type: "string" },
|
|
31
|
-
media: { required: false, control: "text", type: "string" },
|
|
32
|
-
ping: { required: false, control: "text", type: "string" },
|
|
33
|
-
rel: { required: false, control: "text", type: "string" },
|
|
34
29
|
target: {
|
|
35
30
|
required: false,
|
|
36
31
|
control: "select",
|
|
37
32
|
type: "string",
|
|
38
33
|
options: ["_self", "_blank", "_parent", "_top"]
|
|
39
34
|
},
|
|
35
|
+
download: { required: false, control: "text", type: "string" },
|
|
36
|
+
hrefLang: { required: false, control: "text", type: "string" },
|
|
37
|
+
media: { required: false, control: "text", type: "string" },
|
|
38
|
+
ping: { required: false, control: "text", type: "string" },
|
|
39
|
+
rel: { required: false, control: "text", type: "string" },
|
|
40
40
|
type: { required: false, control: "text", type: "string" },
|
|
41
41
|
referrerPolicy: {
|
|
42
42
|
required: false,
|
|
@@ -24,6 +24,7 @@ __export(link_block_ws_exports, {
|
|
|
24
24
|
module.exports = __toCommonJS(link_block_ws_exports);
|
|
25
25
|
var import_icons = require("@webstudio-is/icons");
|
|
26
26
|
var import_link_block = require("./__generated__/link-block.props");
|
|
27
|
+
var import_link = require("./link.ws");
|
|
27
28
|
const presetStyle = {
|
|
28
29
|
boxSizing: {
|
|
29
30
|
type: "keyword",
|
|
@@ -42,6 +43,9 @@ const meta = {
|
|
|
42
43
|
presetStyle
|
|
43
44
|
};
|
|
44
45
|
const propsMeta = {
|
|
45
|
-
props:
|
|
46
|
-
|
|
46
|
+
props: {
|
|
47
|
+
...import_link_block.props,
|
|
48
|
+
href: import_link.propsMeta.props.href
|
|
49
|
+
},
|
|
50
|
+
initialProps: import_link.propsMeta.initialProps
|
|
47
51
|
};
|
|
@@ -23,9 +23,16 @@ __export(link_exports, {
|
|
|
23
23
|
module.exports = __toCommonJS(link_exports);
|
|
24
24
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
25
25
|
var import_react = require("react");
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
var import_props = require("../props");
|
|
27
|
+
const Link = (0, import_react.forwardRef)((props, ref) => {
|
|
28
|
+
const href = (0, import_props.usePropUrl)((0, import_props.getInstanceIdFromComponentProps)(props), "href");
|
|
29
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
30
|
+
"a",
|
|
31
|
+
{
|
|
32
|
+
...props,
|
|
33
|
+
href: typeof href === "string" ? href : href?.path,
|
|
34
|
+
ref
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
});
|
|
31
38
|
Link.displayName = "Link";
|
|
@@ -44,6 +44,13 @@ const meta = {
|
|
|
44
44
|
children: ["Link text you can edit"]
|
|
45
45
|
};
|
|
46
46
|
const propsMeta = {
|
|
47
|
-
props:
|
|
47
|
+
props: {
|
|
48
|
+
...import_link.props,
|
|
49
|
+
href: {
|
|
50
|
+
type: "string",
|
|
51
|
+
control: "url",
|
|
52
|
+
required: false
|
|
53
|
+
}
|
|
54
|
+
},
|
|
48
55
|
initialProps: ["href", "target", "prefetch"]
|
|
49
56
|
};
|
|
@@ -31,6 +31,9 @@ const meta = {
|
|
|
31
31
|
children: []
|
|
32
32
|
};
|
|
33
33
|
const propsMeta = {
|
|
34
|
-
|
|
35
|
-
props:
|
|
34
|
+
initialProps: import_link.propsMeta.initialProps,
|
|
35
|
+
props: {
|
|
36
|
+
...import_rich_text_link.props,
|
|
37
|
+
href: import_link.propsMeta.props.href
|
|
38
|
+
}
|
|
36
39
|
};
|
package/lib/cjs/context.cjs
CHANGED
|
@@ -25,5 +25,6 @@ var import_nanostores = require("nanostores");
|
|
|
25
25
|
var import_react = require("react");
|
|
26
26
|
const ReactSdkContext = (0, import_react.createContext)({
|
|
27
27
|
propsByInstanceIdStore: (0, import_nanostores.atom)(/* @__PURE__ */ new Map()),
|
|
28
|
-
assetsStore: (0, import_nanostores.atom)(/* @__PURE__ */ new Map())
|
|
28
|
+
assetsStore: (0, import_nanostores.atom)(/* @__PURE__ */ new Map()),
|
|
29
|
+
pagesStore: (0, import_nanostores.atom)(/* @__PURE__ */ new Map())
|
|
29
30
|
});
|
package/lib/cjs/props.cjs
CHANGED
|
@@ -18,15 +18,19 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var props_exports = {};
|
|
20
20
|
__export(props_exports, {
|
|
21
|
+
getInstanceIdFromComponentProps: () => getInstanceIdFromComponentProps,
|
|
21
22
|
getPropsByInstanceId: () => getPropsByInstanceId,
|
|
23
|
+
resolveUrlProp: () => resolveUrlProp,
|
|
22
24
|
useInstanceProps: () => useInstanceProps,
|
|
23
|
-
usePropAsset: () => usePropAsset
|
|
25
|
+
usePropAsset: () => usePropAsset,
|
|
26
|
+
usePropUrl: () => usePropUrl
|
|
24
27
|
});
|
|
25
28
|
module.exports = __toCommonJS(props_exports);
|
|
26
29
|
var import_react = require("react");
|
|
27
30
|
var import_nanostores = require("nanostores");
|
|
28
31
|
var import_react2 = require("@nanostores/react");
|
|
29
32
|
var import_context = require("./context");
|
|
33
|
+
var import_webstudio_component = require("./tree/webstudio-component");
|
|
30
34
|
const getPropsByInstanceId = (props) => {
|
|
31
35
|
const propsByInstanceId = /* @__PURE__ */ new Map();
|
|
32
36
|
for (const prop of props.values()) {
|
|
@@ -61,7 +65,7 @@ const usePropAsset = (instanceId, name) => {
|
|
|
61
65
|
(propsByInstanceId, assets) => {
|
|
62
66
|
const instanceProps = propsByInstanceId.get(instanceId);
|
|
63
67
|
if (instanceProps === void 0) {
|
|
64
|
-
return
|
|
68
|
+
return;
|
|
65
69
|
}
|
|
66
70
|
for (const prop of instanceProps) {
|
|
67
71
|
if (prop.type === "asset" && prop.name === name) {
|
|
@@ -75,3 +79,40 @@ const usePropAsset = (instanceId, name) => {
|
|
|
75
79
|
const asset = (0, import_react2.useStore)(assetStore);
|
|
76
80
|
return asset;
|
|
77
81
|
};
|
|
82
|
+
const resolveUrlProp = (instanceId, name, propsByInstanceId, pages) => {
|
|
83
|
+
const instanceProps = propsByInstanceId.get(instanceId);
|
|
84
|
+
if (instanceProps === void 0) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
for (const prop of instanceProps) {
|
|
88
|
+
if (prop.name !== name) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (prop.type === "page") {
|
|
92
|
+
return pages.get(prop.value);
|
|
93
|
+
}
|
|
94
|
+
if (prop.type === "string") {
|
|
95
|
+
for (const page of pages.values()) {
|
|
96
|
+
if (page.path === prop.value) {
|
|
97
|
+
return page;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return prop.value;
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const usePropUrl = (instanceId, name) => {
|
|
106
|
+
const { propsByInstanceIdStore, pagesStore } = (0, import_react.useContext)(import_context.ReactSdkContext);
|
|
107
|
+
const pageStore = (0, import_react.useMemo)(
|
|
108
|
+
() => (0, import_nanostores.computed)(
|
|
109
|
+
[propsByInstanceIdStore, pagesStore],
|
|
110
|
+
(propsByInstanceId, pages) => resolveUrlProp(instanceId, name, propsByInstanceId, pages)
|
|
111
|
+
),
|
|
112
|
+
[propsByInstanceIdStore, pagesStore, instanceId, name]
|
|
113
|
+
);
|
|
114
|
+
return (0, import_react2.useStore)(pageStore);
|
|
115
|
+
};
|
|
116
|
+
const getInstanceIdFromComponentProps = (props) => {
|
|
117
|
+
return props[import_webstudio_component.idAttribute];
|
|
118
|
+
};
|
|
@@ -31,6 +31,7 @@ const createElementsTree = ({
|
|
|
31
31
|
instance,
|
|
32
32
|
propsByInstanceIdStore,
|
|
33
33
|
assetsStore,
|
|
34
|
+
pagesStore,
|
|
34
35
|
Component,
|
|
35
36
|
getComponent
|
|
36
37
|
}) => {
|
|
@@ -55,7 +56,13 @@ const createElementsTree = ({
|
|
|
55
56
|
],
|
|
56
57
|
getComponent
|
|
57
58
|
});
|
|
58
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
59
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
60
|
+
import_context.ReactSdkContext.Provider,
|
|
61
|
+
{
|
|
62
|
+
value: { propsByInstanceIdStore, assetsStore, pagesStore },
|
|
63
|
+
children: root
|
|
64
|
+
}
|
|
65
|
+
);
|
|
59
66
|
};
|
|
60
67
|
const createInstanceChildrenElements = ({
|
|
61
68
|
instanceSelector,
|
package/lib/cjs/tree/root.cjs
CHANGED
|
@@ -75,6 +75,7 @@ const InstanceRoot = ({
|
|
|
75
75
|
(0, import_props.getPropsByInstanceId)(new Map(data.build.props))
|
|
76
76
|
),
|
|
77
77
|
assetsStore: (0, import_nanostores.atom)(new Map(data.assets.map((asset) => [asset.id, asset]))),
|
|
78
|
+
pagesStore: (0, import_nanostores.atom)(new Map(data.pages.map((page) => [page.id, page]))),
|
|
78
79
|
Component: Component ?? import_webstudio_component.WebstudioComponent,
|
|
79
80
|
getComponent
|
|
80
81
|
});
|
|
@@ -2,18 +2,18 @@ const props = {
|
|
|
2
2
|
slot: { required: false, control: "text", type: "string" },
|
|
3
3
|
style: { required: false, control: "text", type: "string" },
|
|
4
4
|
title: { required: false, control: "text", type: "string" },
|
|
5
|
-
download: { required: false, control: "text", type: "string" },
|
|
6
5
|
href: { required: false, control: "text", type: "string" },
|
|
7
|
-
hrefLang: { required: false, control: "text", type: "string" },
|
|
8
|
-
media: { required: false, control: "text", type: "string" },
|
|
9
|
-
ping: { required: false, control: "text", type: "string" },
|
|
10
|
-
rel: { required: false, control: "text", type: "string" },
|
|
11
6
|
target: {
|
|
12
7
|
required: false,
|
|
13
8
|
control: "select",
|
|
14
9
|
type: "string",
|
|
15
10
|
options: ["_self", "_blank", "_parent", "_top"]
|
|
16
11
|
},
|
|
12
|
+
download: { required: false, control: "text", type: "string" },
|
|
13
|
+
hrefLang: { required: false, control: "text", type: "string" },
|
|
14
|
+
media: { required: false, control: "text", type: "string" },
|
|
15
|
+
ping: { required: false, control: "text", type: "string" },
|
|
16
|
+
rel: { required: false, control: "text", type: "string" },
|
|
17
17
|
type: { required: false, control: "text", type: "string" },
|
|
18
18
|
referrerPolicy: {
|
|
19
19
|
required: false,
|
|
@@ -2,18 +2,18 @@ const props = {
|
|
|
2
2
|
slot: { required: false, control: "text", type: "string" },
|
|
3
3
|
style: { required: false, control: "text", type: "string" },
|
|
4
4
|
title: { required: false, control: "text", type: "string" },
|
|
5
|
-
|
|
6
|
-
href: { required: false, control: "text", type: "string", defaultValue: "" },
|
|
7
|
-
hrefLang: { required: false, control: "text", type: "string" },
|
|
8
|
-
media: { required: false, control: "text", type: "string" },
|
|
9
|
-
ping: { required: false, control: "text", type: "string" },
|
|
10
|
-
rel: { required: false, control: "text", type: "string" },
|
|
5
|
+
href: { required: false, control: "text", type: "string" },
|
|
11
6
|
target: {
|
|
12
7
|
required: false,
|
|
13
8
|
control: "select",
|
|
14
9
|
type: "string",
|
|
15
10
|
options: ["_self", "_blank", "_parent", "_top"]
|
|
16
11
|
},
|
|
12
|
+
download: { required: false, control: "text", type: "string" },
|
|
13
|
+
hrefLang: { required: false, control: "text", type: "string" },
|
|
14
|
+
media: { required: false, control: "text", type: "string" },
|
|
15
|
+
ping: { required: false, control: "text", type: "string" },
|
|
16
|
+
rel: { required: false, control: "text", type: "string" },
|
|
17
17
|
type: { required: false, control: "text", type: "string" },
|
|
18
18
|
referrerPolicy: {
|
|
19
19
|
required: false,
|
|
@@ -2,18 +2,18 @@ const props = {
|
|
|
2
2
|
slot: { required: false, control: "text", type: "string" },
|
|
3
3
|
style: { required: false, control: "text", type: "string" },
|
|
4
4
|
title: { required: false, control: "text", type: "string" },
|
|
5
|
-
download: { required: false, control: "text", type: "string" },
|
|
6
5
|
href: { required: false, control: "text", type: "string" },
|
|
7
|
-
hrefLang: { required: false, control: "text", type: "string" },
|
|
8
|
-
media: { required: false, control: "text", type: "string" },
|
|
9
|
-
ping: { required: false, control: "text", type: "string" },
|
|
10
|
-
rel: { required: false, control: "text", type: "string" },
|
|
11
6
|
target: {
|
|
12
7
|
required: false,
|
|
13
8
|
control: "select",
|
|
14
9
|
type: "string",
|
|
15
10
|
options: ["_self", "_blank", "_parent", "_top"]
|
|
16
11
|
},
|
|
12
|
+
download: { required: false, control: "text", type: "string" },
|
|
13
|
+
hrefLang: { required: false, control: "text", type: "string" },
|
|
14
|
+
media: { required: false, control: "text", type: "string" },
|
|
15
|
+
ping: { required: false, control: "text", type: "string" },
|
|
16
|
+
rel: { required: false, control: "text", type: "string" },
|
|
17
17
|
type: { required: false, control: "text", type: "string" },
|
|
18
18
|
referrerPolicy: {
|
|
19
19
|
required: false,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BoxLinkIcon } from "@webstudio-is/icons";
|
|
2
2
|
import { props } from "./__generated__/link-block.props";
|
|
3
|
+
import { propsMeta as linkPropsMeta } from "./link.ws";
|
|
3
4
|
const presetStyle = {
|
|
4
5
|
boxSizing: {
|
|
5
6
|
type: "keyword",
|
|
@@ -18,8 +19,11 @@ const meta = {
|
|
|
18
19
|
presetStyle
|
|
19
20
|
};
|
|
20
21
|
const propsMeta = {
|
|
21
|
-
props
|
|
22
|
-
|
|
22
|
+
props: {
|
|
23
|
+
...props,
|
|
24
|
+
href: linkPropsMeta.props.href
|
|
25
|
+
},
|
|
26
|
+
initialProps: linkPropsMeta.initialProps
|
|
23
27
|
};
|
|
24
28
|
export {
|
|
25
29
|
meta,
|
package/lib/components/link.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import { usePropUrl, getInstanceIdFromComponentProps } from "../props";
|
|
4
|
+
const Link = forwardRef((props, ref) => {
|
|
5
|
+
const href = usePropUrl(getInstanceIdFromComponentProps(props), "href");
|
|
6
|
+
return /* @__PURE__ */ jsx(
|
|
7
|
+
"a",
|
|
8
|
+
{
|
|
9
|
+
...props,
|
|
10
|
+
href: typeof href === "string" ? href : href?.path,
|
|
11
|
+
ref
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
});
|
|
8
15
|
Link.displayName = "Link";
|
|
9
16
|
export {
|
|
10
17
|
Link
|
|
@@ -20,7 +20,14 @@ const meta = {
|
|
|
20
20
|
children: ["Link text you can edit"]
|
|
21
21
|
};
|
|
22
22
|
const propsMeta = {
|
|
23
|
-
props
|
|
23
|
+
props: {
|
|
24
|
+
...props,
|
|
25
|
+
href: {
|
|
26
|
+
type: "string",
|
|
27
|
+
control: "url",
|
|
28
|
+
required: false
|
|
29
|
+
}
|
|
30
|
+
},
|
|
24
31
|
initialProps: ["href", "target", "prefetch"]
|
|
25
32
|
};
|
|
26
33
|
export {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { props } from "./__generated__/rich-text-link.props";
|
|
2
|
-
import { meta as linkMeta, propsMeta as
|
|
2
|
+
import { meta as linkMeta, propsMeta as linkPropsMeta } from "./link.ws";
|
|
3
3
|
const { category, ...linkMetaRest } = linkMeta;
|
|
4
4
|
const meta = {
|
|
5
5
|
...linkMetaRest,
|
|
@@ -7,8 +7,11 @@ const meta = {
|
|
|
7
7
|
children: []
|
|
8
8
|
};
|
|
9
9
|
const propsMeta = {
|
|
10
|
-
|
|
11
|
-
props
|
|
10
|
+
initialProps: linkPropsMeta.initialProps,
|
|
11
|
+
props: {
|
|
12
|
+
...props,
|
|
13
|
+
href: linkPropsMeta.props.href
|
|
14
|
+
}
|
|
12
15
|
};
|
|
13
16
|
export {
|
|
14
17
|
meta,
|
package/lib/context.js
CHANGED
|
@@ -2,7 +2,8 @@ import { atom } from "nanostores";
|
|
|
2
2
|
import { createContext } from "react";
|
|
3
3
|
const ReactSdkContext = createContext({
|
|
4
4
|
propsByInstanceIdStore: atom(/* @__PURE__ */ new Map()),
|
|
5
|
-
assetsStore: atom(/* @__PURE__ */ new Map())
|
|
5
|
+
assetsStore: atom(/* @__PURE__ */ new Map()),
|
|
6
|
+
pagesStore: atom(/* @__PURE__ */ new Map())
|
|
6
7
|
});
|
|
7
8
|
export {
|
|
8
9
|
ReactSdkContext
|
package/lib/props.js
CHANGED
|
@@ -2,6 +2,7 @@ import { useContext, useMemo } from "react";
|
|
|
2
2
|
import { computed } from "nanostores";
|
|
3
3
|
import { useStore } from "@nanostores/react";
|
|
4
4
|
import { ReactSdkContext } from "./context";
|
|
5
|
+
import { idAttribute } from "./tree/webstudio-component";
|
|
5
6
|
const getPropsByInstanceId = (props) => {
|
|
6
7
|
const propsByInstanceId = /* @__PURE__ */ new Map();
|
|
7
8
|
for (const prop of props.values()) {
|
|
@@ -36,7 +37,7 @@ const usePropAsset = (instanceId, name) => {
|
|
|
36
37
|
(propsByInstanceId, assets) => {
|
|
37
38
|
const instanceProps = propsByInstanceId.get(instanceId);
|
|
38
39
|
if (instanceProps === void 0) {
|
|
39
|
-
return
|
|
40
|
+
return;
|
|
40
41
|
}
|
|
41
42
|
for (const prop of instanceProps) {
|
|
42
43
|
if (prop.type === "asset" && prop.name === name) {
|
|
@@ -50,8 +51,48 @@ const usePropAsset = (instanceId, name) => {
|
|
|
50
51
|
const asset = useStore(assetStore);
|
|
51
52
|
return asset;
|
|
52
53
|
};
|
|
54
|
+
const resolveUrlProp = (instanceId, name, propsByInstanceId, pages) => {
|
|
55
|
+
const instanceProps = propsByInstanceId.get(instanceId);
|
|
56
|
+
if (instanceProps === void 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (const prop of instanceProps) {
|
|
60
|
+
if (prop.name !== name) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (prop.type === "page") {
|
|
64
|
+
return pages.get(prop.value);
|
|
65
|
+
}
|
|
66
|
+
if (prop.type === "string") {
|
|
67
|
+
for (const page of pages.values()) {
|
|
68
|
+
if (page.path === prop.value) {
|
|
69
|
+
return page;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return prop.value;
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const usePropUrl = (instanceId, name) => {
|
|
78
|
+
const { propsByInstanceIdStore, pagesStore } = useContext(ReactSdkContext);
|
|
79
|
+
const pageStore = useMemo(
|
|
80
|
+
() => computed(
|
|
81
|
+
[propsByInstanceIdStore, pagesStore],
|
|
82
|
+
(propsByInstanceId, pages) => resolveUrlProp(instanceId, name, propsByInstanceId, pages)
|
|
83
|
+
),
|
|
84
|
+
[propsByInstanceIdStore, pagesStore, instanceId, name]
|
|
85
|
+
);
|
|
86
|
+
return useStore(pageStore);
|
|
87
|
+
};
|
|
88
|
+
const getInstanceIdFromComponentProps = (props) => {
|
|
89
|
+
return props[idAttribute];
|
|
90
|
+
};
|
|
53
91
|
export {
|
|
92
|
+
getInstanceIdFromComponentProps,
|
|
54
93
|
getPropsByInstanceId,
|
|
94
|
+
resolveUrlProp,
|
|
55
95
|
useInstanceProps,
|
|
56
|
-
usePropAsset
|
|
96
|
+
usePropAsset,
|
|
97
|
+
usePropUrl
|
|
57
98
|
};
|
|
@@ -8,6 +8,7 @@ const createElementsTree = ({
|
|
|
8
8
|
instance,
|
|
9
9
|
propsByInstanceIdStore,
|
|
10
10
|
assetsStore,
|
|
11
|
+
pagesStore,
|
|
11
12
|
Component,
|
|
12
13
|
getComponent
|
|
13
14
|
}) => {
|
|
@@ -32,7 +33,13 @@ const createElementsTree = ({
|
|
|
32
33
|
],
|
|
33
34
|
getComponent
|
|
34
35
|
});
|
|
35
|
-
return /* @__PURE__ */ jsx(
|
|
36
|
+
return /* @__PURE__ */ jsx(
|
|
37
|
+
ReactSdkContext.Provider,
|
|
38
|
+
{
|
|
39
|
+
value: { propsByInstanceIdStore, assetsStore, pagesStore },
|
|
40
|
+
children: root
|
|
41
|
+
}
|
|
42
|
+
);
|
|
36
43
|
};
|
|
37
44
|
const createInstanceChildrenElements = ({
|
|
38
45
|
instanceSelector,
|
package/lib/tree/root.js
CHANGED
|
@@ -52,6 +52,7 @@ const InstanceRoot = ({
|
|
|
52
52
|
getPropsByInstanceId(new Map(data.build.props))
|
|
53
53
|
),
|
|
54
54
|
assetsStore: atom(new Map(data.assets.map((asset) => [asset.id, asset]))),
|
|
55
|
+
pagesStore: atom(new Map(data.pages.map((page) => [page.id, page]))),
|
|
55
56
|
Component: Component ?? WebstudioComponent,
|
|
56
57
|
getComponent
|
|
57
58
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webstudio-is/react-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"description": "Webstudio JavaScript / TypeScript API",
|
|
5
5
|
"author": "Webstudio <github@webstudio.is>",
|
|
6
6
|
"homepage": "https://webstudio.is",
|
|
@@ -40,14 +40,14 @@
|
|
|
40
40
|
"mitt": "^3.0.0",
|
|
41
41
|
"nanostores": "^0.7.1",
|
|
42
42
|
"warn-once": "^0.1.1",
|
|
43
|
-
"@webstudio-is/asset-uploader": "^0.
|
|
44
|
-
"@webstudio-is/css-data": "^0.
|
|
45
|
-
"@webstudio-is/
|
|
46
|
-
"@webstudio-is/
|
|
47
|
-
"@webstudio-is/icons": "^0.
|
|
48
|
-
"@webstudio-is/image": "^0.
|
|
49
|
-
"@webstudio-is/prisma-client": "^0.
|
|
50
|
-
"@webstudio-is/project-build": "^0.
|
|
43
|
+
"@webstudio-is/asset-uploader": "^0.51.0",
|
|
44
|
+
"@webstudio-is/css-data": "^0.51.0",
|
|
45
|
+
"@webstudio-is/css-vars": "^0.51.0",
|
|
46
|
+
"@webstudio-is/generate-arg-types": "^0.51.0",
|
|
47
|
+
"@webstudio-is/icons": "^0.51.0",
|
|
48
|
+
"@webstudio-is/image": "^0.51.0",
|
|
49
|
+
"@webstudio-is/prisma-client": "^0.51.0",
|
|
50
|
+
"@webstudio-is/project-build": "^0.51.0"
|
|
51
51
|
},
|
|
52
52
|
"exports": {
|
|
53
53
|
".": {
|
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
forwardRef,
|
|
3
3
|
useMemo,
|
|
4
|
-
type
|
|
4
|
+
type ComponentPropsWithoutRef,
|
|
5
5
|
type ElementRef,
|
|
6
6
|
} from "react";
|
|
7
7
|
import { Image as WebstudioImage, loaders } from "@webstudio-is/image";
|
|
8
8
|
import { Image as SdkImage } from "../../components/image";
|
|
9
|
-
import { usePropAsset } from "../../props";
|
|
10
|
-
import { idAttribute } from "../../tree/webstudio-component";
|
|
9
|
+
import { usePropAsset, getInstanceIdFromComponentProps } from "../../props";
|
|
11
10
|
import { getParams } from "../params";
|
|
12
11
|
|
|
13
12
|
const defaultTag = "img";
|
|
14
13
|
|
|
15
|
-
type Props =
|
|
14
|
+
type Props = ComponentPropsWithoutRef<typeof WebstudioImage>;
|
|
16
15
|
|
|
17
16
|
export const Image = forwardRef<ElementRef<typeof defaultTag>, Props>(
|
|
18
17
|
(props, ref) => {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const asset = usePropAsset(componentId, "src");
|
|
18
|
+
const asset = usePropAsset(getInstanceIdFromComponentProps(props), "src");
|
|
22
19
|
const params = getParams();
|
|
23
20
|
|
|
24
21
|
const loader = useMemo(() => {
|
|
@@ -1,59 +1,23 @@
|
|
|
1
|
-
import { Link } from "@remix-run/react";
|
|
2
|
-
import type {
|
|
3
|
-
ElementRef,
|
|
4
|
-
ComponentProps,
|
|
5
|
-
RefAttributes,
|
|
6
|
-
ForwardRefExoticComponent,
|
|
7
|
-
} from "react";
|
|
1
|
+
import { Link as RemixLink } from "@remix-run/react";
|
|
2
|
+
import type { ComponentPropsWithoutRef } from "react";
|
|
8
3
|
import { forwardRef } from "react";
|
|
9
|
-
import type { Link
|
|
4
|
+
import type { Link } from "../../../components/link";
|
|
5
|
+
import { usePropUrl, getInstanceIdFromComponentProps } from "../../../props";
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
new URL(href);
|
|
14
|
-
return true;
|
|
15
|
-
} catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
// Remix's check for absolute URL copied from here:
|
|
21
|
-
// https://github.com/remix-run/react-router/blob/react-router-dom%406.8.0/packages/react-router-dom/index.tsx#L423-L424
|
|
22
|
-
const isAbsoluteUrlRemix = (href: string) =>
|
|
23
|
-
/^[a-z+]+:\/\//i.test(href) || href.startsWith("//");
|
|
24
|
-
|
|
25
|
-
type Props = ComponentProps<typeof BaseLink>;
|
|
26
|
-
|
|
27
|
-
type Ref = ElementRef<"a">;
|
|
28
|
-
|
|
29
|
-
export const wrapLinkComponent = (
|
|
30
|
-
BaseLink: ForwardRefExoticComponent<Props & RefAttributes<Ref>>
|
|
31
|
-
) => {
|
|
32
|
-
// We're not actually wrapping BaseLink (no way to wrap with Remix's Link),
|
|
33
|
-
// but this is still useful because we're making sure that props/ref types are compatible
|
|
34
|
-
const Component = forwardRef<Ref, Props>(({ href = "", ...props }, ref) => {
|
|
35
|
-
const isAbsolute = isAbsoluteUrl(href);
|
|
7
|
+
type LinkComponent = typeof Link;
|
|
8
|
+
type LinkProps = ComponentPropsWithoutRef<LinkComponent>;
|
|
36
9
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const willRemixTryToTreatAsAbsoluteAndCrash =
|
|
41
|
-
isAbsolute === false && isAbsoluteUrlRemix(href);
|
|
10
|
+
export const wrapLinkComponent = (BaseLink: LinkComponent) => {
|
|
11
|
+
const Component: LinkComponent = forwardRef((props: LinkProps, ref) => {
|
|
12
|
+
const href = usePropUrl(getInstanceIdFromComponentProps(props), "href");
|
|
42
13
|
|
|
43
|
-
if (
|
|
44
|
-
return
|
|
45
|
-
<BaseLink
|
|
46
|
-
{...props}
|
|
47
|
-
href={willRemixTryToTreatAsAbsoluteAndCrash ? "" : href}
|
|
48
|
-
ref={ref}
|
|
49
|
-
/>
|
|
50
|
-
);
|
|
14
|
+
if (typeof href === "string" || href === undefined) {
|
|
15
|
+
return <BaseLink {...props} ref={ref} />;
|
|
51
16
|
}
|
|
52
17
|
|
|
53
|
-
return <
|
|
18
|
+
return <RemixLink {...props} to={href.path} ref={ref} />;
|
|
54
19
|
});
|
|
55
20
|
|
|
56
|
-
// This is the only part that we use from BaseLink at runtime
|
|
57
21
|
Component.displayName = BaseLink.displayName;
|
|
58
22
|
|
|
59
23
|
return Component;
|
|
@@ -4,18 +4,18 @@ export const props: Record<string, PropMeta> = {
|
|
|
4
4
|
slot: { required: false, control: "text", type: "string" },
|
|
5
5
|
style: { required: false, control: "text", type: "string" },
|
|
6
6
|
title: { required: false, control: "text", type: "string" },
|
|
7
|
-
download: { required: false, control: "text", type: "string" },
|
|
8
7
|
href: { required: false, control: "text", type: "string" },
|
|
9
|
-
hrefLang: { required: false, control: "text", type: "string" },
|
|
10
|
-
media: { required: false, control: "text", type: "string" },
|
|
11
|
-
ping: { required: false, control: "text", type: "string" },
|
|
12
|
-
rel: { required: false, control: "text", type: "string" },
|
|
13
8
|
target: {
|
|
14
9
|
required: false,
|
|
15
10
|
control: "select",
|
|
16
11
|
type: "string",
|
|
17
12
|
options: ["_self", "_blank", "_parent", "_top"],
|
|
18
13
|
},
|
|
14
|
+
download: { required: false, control: "text", type: "string" },
|
|
15
|
+
hrefLang: { required: false, control: "text", type: "string" },
|
|
16
|
+
media: { required: false, control: "text", type: "string" },
|
|
17
|
+
ping: { required: false, control: "text", type: "string" },
|
|
18
|
+
rel: { required: false, control: "text", type: "string" },
|
|
19
19
|
type: { required: false, control: "text", type: "string" },
|
|
20
20
|
referrerPolicy: {
|
|
21
21
|
required: false,
|
|
@@ -4,18 +4,18 @@ export const props: Record<string, PropMeta> = {
|
|
|
4
4
|
slot: { required: false, control: "text", type: "string" },
|
|
5
5
|
style: { required: false, control: "text", type: "string" },
|
|
6
6
|
title: { required: false, control: "text", type: "string" },
|
|
7
|
-
|
|
8
|
-
href: { required: false, control: "text", type: "string", defaultValue: "" },
|
|
9
|
-
hrefLang: { required: false, control: "text", type: "string" },
|
|
10
|
-
media: { required: false, control: "text", type: "string" },
|
|
11
|
-
ping: { required: false, control: "text", type: "string" },
|
|
12
|
-
rel: { required: false, control: "text", type: "string" },
|
|
7
|
+
href: { required: false, control: "text", type: "string" },
|
|
13
8
|
target: {
|
|
14
9
|
required: false,
|
|
15
10
|
control: "select",
|
|
16
11
|
type: "string",
|
|
17
12
|
options: ["_self", "_blank", "_parent", "_top"],
|
|
18
13
|
},
|
|
14
|
+
download: { required: false, control: "text", type: "string" },
|
|
15
|
+
hrefLang: { required: false, control: "text", type: "string" },
|
|
16
|
+
media: { required: false, control: "text", type: "string" },
|
|
17
|
+
ping: { required: false, control: "text", type: "string" },
|
|
18
|
+
rel: { required: false, control: "text", type: "string" },
|
|
19
19
|
type: { required: false, control: "text", type: "string" },
|
|
20
20
|
referrerPolicy: {
|
|
21
21
|
required: false,
|
|
@@ -4,18 +4,18 @@ export const props: Record<string, PropMeta> = {
|
|
|
4
4
|
slot: { required: false, control: "text", type: "string" },
|
|
5
5
|
style: { required: false, control: "text", type: "string" },
|
|
6
6
|
title: { required: false, control: "text", type: "string" },
|
|
7
|
-
download: { required: false, control: "text", type: "string" },
|
|
8
7
|
href: { required: false, control: "text", type: "string" },
|
|
9
|
-
hrefLang: { required: false, control: "text", type: "string" },
|
|
10
|
-
media: { required: false, control: "text", type: "string" },
|
|
11
|
-
ping: { required: false, control: "text", type: "string" },
|
|
12
|
-
rel: { required: false, control: "text", type: "string" },
|
|
13
8
|
target: {
|
|
14
9
|
required: false,
|
|
15
10
|
control: "select",
|
|
16
11
|
type: "string",
|
|
17
12
|
options: ["_self", "_blank", "_parent", "_top"],
|
|
18
13
|
},
|
|
14
|
+
download: { required: false, control: "text", type: "string" },
|
|
15
|
+
hrefLang: { required: false, control: "text", type: "string" },
|
|
16
|
+
media: { required: false, control: "text", type: "string" },
|
|
17
|
+
ping: { required: false, control: "text", type: "string" },
|
|
18
|
+
rel: { required: false, control: "text", type: "string" },
|
|
19
19
|
type: { required: false, control: "text", type: "string" },
|
|
20
20
|
referrerPolicy: {
|
|
21
21
|
required: false,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BoxLinkIcon } from "@webstudio-is/icons";
|
|
2
2
|
import type { WsComponentMeta, WsComponentPropsMeta } from "./component-meta";
|
|
3
3
|
import { props } from "./__generated__/link-block.props";
|
|
4
|
+
import { propsMeta as linkPropsMeta } from "./link.ws";
|
|
4
5
|
|
|
5
6
|
const presetStyle = {
|
|
6
7
|
boxSizing: {
|
|
@@ -22,6 +23,9 @@ export const meta: WsComponentMeta = {
|
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
export const propsMeta: WsComponentPropsMeta = {
|
|
25
|
-
props
|
|
26
|
-
|
|
26
|
+
props: {
|
|
27
|
+
...props,
|
|
28
|
+
href: linkPropsMeta.props.href,
|
|
29
|
+
},
|
|
30
|
+
initialProps: linkPropsMeta.initialProps,
|
|
27
31
|
};
|
package/src/components/link.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { forwardRef, type
|
|
1
|
+
import { forwardRef, type ComponentProps } from "react";
|
|
2
|
+
import { usePropUrl, getInstanceIdFromComponentProps } from "../props";
|
|
2
3
|
|
|
3
4
|
// @todo props that come from remix link, shouldn't be here at all
|
|
4
5
|
// - prefetch should be only on remix component and it already is
|
|
@@ -10,10 +11,15 @@ type Props = Omit<ComponentProps<"a">, "href" | "target"> & {
|
|
|
10
11
|
prefetch?: "none" | "intent" | "render";
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
export const Link = forwardRef<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
export const Link = forwardRef<HTMLAnchorElement, Props>((props, ref) => {
|
|
15
|
+
const href = usePropUrl(getInstanceIdFromComponentProps(props), "href");
|
|
16
|
+
return (
|
|
17
|
+
<a
|
|
18
|
+
{...props}
|
|
19
|
+
href={typeof href === "string" ? href : href?.path}
|
|
20
|
+
ref={ref}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
});
|
|
18
24
|
|
|
19
25
|
Link.displayName = "Link";
|
|
@@ -24,6 +24,13 @@ export const meta: WsComponentMeta = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export const propsMeta: WsComponentPropsMeta = {
|
|
27
|
-
props
|
|
27
|
+
props: {
|
|
28
|
+
...props,
|
|
29
|
+
href: {
|
|
30
|
+
type: "string",
|
|
31
|
+
control: "url",
|
|
32
|
+
required: false,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
28
35
|
initialProps: ["href", "target", "prefetch"],
|
|
29
36
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WsComponentMeta, WsComponentPropsMeta } from "./component-meta";
|
|
2
2
|
import { props } from "./__generated__/rich-text-link.props";
|
|
3
|
-
import { meta as linkMeta, propsMeta as
|
|
3
|
+
import { meta as linkMeta, propsMeta as linkPropsMeta } from "./link.ws";
|
|
4
4
|
|
|
5
5
|
const { category, ...linkMetaRest } = linkMeta;
|
|
6
6
|
|
|
@@ -11,6 +11,9 @@ export const meta: WsComponentMeta = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
export const propsMeta: WsComponentPropsMeta = {
|
|
14
|
-
|
|
15
|
-
props
|
|
14
|
+
initialProps: linkPropsMeta.initialProps,
|
|
15
|
+
props: {
|
|
16
|
+
...props,
|
|
17
|
+
href: linkPropsMeta.props.href,
|
|
18
|
+
},
|
|
16
19
|
};
|
package/src/context.tsx
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type ReadableAtom, atom } from "nanostores";
|
|
2
2
|
import { createContext } from "react";
|
|
3
|
-
import type { Assets, PropsByInstanceId } from "./props";
|
|
3
|
+
import type { Assets, Pages, PropsByInstanceId } from "./props";
|
|
4
4
|
|
|
5
5
|
export const ReactSdkContext = createContext<{
|
|
6
6
|
propsByInstanceIdStore: ReadableAtom<PropsByInstanceId>;
|
|
7
7
|
assetsStore: ReadableAtom<Assets>;
|
|
8
|
+
pagesStore: ReadableAtom<Pages>;
|
|
8
9
|
}>({
|
|
9
10
|
propsByInstanceIdStore: atom(new Map()),
|
|
10
11
|
assetsStore: atom(new Map()),
|
|
12
|
+
pagesStore: atom(new Map()),
|
|
11
13
|
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, test, expect } from "@jest/globals";
|
|
2
|
+
import { resolveUrlProp, type Pages, type PropsByInstanceId } from "./props";
|
|
3
|
+
import type { Page, Prop } from "@webstudio-is/project-build";
|
|
4
|
+
|
|
5
|
+
const unique = () => Math.random().toString();
|
|
6
|
+
|
|
7
|
+
describe("resolveUrlProp", () => {
|
|
8
|
+
const instanceId = unique();
|
|
9
|
+
|
|
10
|
+
const page1: Page = {
|
|
11
|
+
id: unique(),
|
|
12
|
+
path: `/${unique()}`,
|
|
13
|
+
name: "",
|
|
14
|
+
title: "",
|
|
15
|
+
meta: {},
|
|
16
|
+
rootInstanceId: "0",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const page2: Page = {
|
|
20
|
+
id: unique(),
|
|
21
|
+
path: `/${unique()}`,
|
|
22
|
+
name: "",
|
|
23
|
+
title: "",
|
|
24
|
+
meta: {},
|
|
25
|
+
rootInstanceId: "0",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const pageByIdProp: Prop = {
|
|
29
|
+
type: "page",
|
|
30
|
+
id: unique(),
|
|
31
|
+
instanceId,
|
|
32
|
+
name: unique(),
|
|
33
|
+
value: page1.id,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const pageByPathProp: Prop = {
|
|
37
|
+
type: "string",
|
|
38
|
+
id: unique(),
|
|
39
|
+
instanceId,
|
|
40
|
+
name: unique(),
|
|
41
|
+
value: page2.path,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const arbitraryUrlProp: Prop = {
|
|
45
|
+
type: "string",
|
|
46
|
+
id: unique(),
|
|
47
|
+
instanceId,
|
|
48
|
+
name: unique(),
|
|
49
|
+
value: unique(),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const propsByInstanceId: PropsByInstanceId = new Map([
|
|
53
|
+
[instanceId, [pageByIdProp, pageByPathProp, arbitraryUrlProp]],
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const pages: Pages = new Map([
|
|
57
|
+
[page1.id, page1],
|
|
58
|
+
[page2.id, page2],
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
test("if instanceId is unknown returns undefined", () => {
|
|
62
|
+
expect(
|
|
63
|
+
resolveUrlProp("unknown", pageByIdProp.name, propsByInstanceId, pages)
|
|
64
|
+
).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("if prop name is unknown returns undefined", () => {
|
|
68
|
+
expect(
|
|
69
|
+
resolveUrlProp(instanceId, "unknown", propsByInstanceId, pages)
|
|
70
|
+
).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("page by id", () => {
|
|
74
|
+
expect(
|
|
75
|
+
resolveUrlProp(instanceId, pageByIdProp.name, propsByInstanceId, pages)
|
|
76
|
+
).toBe(page1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("page by path", () => {
|
|
80
|
+
expect(
|
|
81
|
+
resolveUrlProp(instanceId, pageByPathProp.name, propsByInstanceId, pages)
|
|
82
|
+
).toBe(page2);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("arbitrary url", () => {
|
|
86
|
+
expect(
|
|
87
|
+
resolveUrlProp(
|
|
88
|
+
instanceId,
|
|
89
|
+
arbitraryUrlProp.name,
|
|
90
|
+
propsByInstanceId,
|
|
91
|
+
pages
|
|
92
|
+
)
|
|
93
|
+
).toBe(arbitraryUrlProp.value);
|
|
94
|
+
});
|
|
95
|
+
});
|
package/src/props.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { useContext, useMemo } from "react";
|
|
2
2
|
import { computed } from "nanostores";
|
|
3
3
|
import { useStore } from "@nanostores/react";
|
|
4
|
-
import type { Instance, Prop, Props } from "@webstudio-is/project-build";
|
|
4
|
+
import type { Instance, Page, Prop, Props } from "@webstudio-is/project-build";
|
|
5
5
|
import type { Asset } from "@webstudio-is/asset-uploader";
|
|
6
6
|
import { ReactSdkContext } from "./context";
|
|
7
|
+
import { idAttribute } from "./tree/webstudio-component";
|
|
7
8
|
|
|
8
9
|
export type PropsByInstanceId = Map<Instance["id"], Prop[]>;
|
|
9
10
|
|
|
10
11
|
export type Assets = Map<Asset["id"], Asset>;
|
|
12
|
+
export type Pages = Map<Page["id"], Page>;
|
|
11
13
|
|
|
12
14
|
export const getPropsByInstanceId = (props: Props) => {
|
|
13
15
|
const propsByInstanceId: PropsByInstanceId = new Map();
|
|
@@ -39,7 +41,7 @@ export const useInstanceProps = (instanceId: Instance["id"]) => {
|
|
|
39
41
|
return instancePropsObject;
|
|
40
42
|
};
|
|
41
43
|
|
|
42
|
-
// this utility is
|
|
44
|
+
// this utility is used for image component in both builder and preview
|
|
43
45
|
// so need to optimize rerenders with computed
|
|
44
46
|
export const usePropAsset = (instanceId: Instance["id"], name: string) => {
|
|
45
47
|
const { propsByInstanceIdStore, assetsStore } = useContext(ReactSdkContext);
|
|
@@ -49,7 +51,7 @@ export const usePropAsset = (instanceId: Instance["id"], name: string) => {
|
|
|
49
51
|
(propsByInstanceId, assets) => {
|
|
50
52
|
const instanceProps = propsByInstanceId.get(instanceId);
|
|
51
53
|
if (instanceProps === undefined) {
|
|
52
|
-
return
|
|
54
|
+
return;
|
|
53
55
|
}
|
|
54
56
|
for (const prop of instanceProps) {
|
|
55
57
|
if (prop.type === "asset" && prop.name === name) {
|
|
@@ -63,3 +65,57 @@ export const usePropAsset = (instanceId: Instance["id"], name: string) => {
|
|
|
63
65
|
const asset = useStore(assetStore);
|
|
64
66
|
return asset;
|
|
65
67
|
};
|
|
68
|
+
|
|
69
|
+
export const resolveUrlProp = (
|
|
70
|
+
instanceId: Instance["id"],
|
|
71
|
+
name: string,
|
|
72
|
+
propsByInstanceId: PropsByInstanceId,
|
|
73
|
+
pages: Pages
|
|
74
|
+
): Page | string | undefined => {
|
|
75
|
+
const instanceProps = propsByInstanceId.get(instanceId);
|
|
76
|
+
if (instanceProps === undefined) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (const prop of instanceProps) {
|
|
80
|
+
if (prop.name !== name) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (prop.type === "page") {
|
|
85
|
+
return pages.get(prop.value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (prop.type === "string") {
|
|
89
|
+
for (const page of pages.values()) {
|
|
90
|
+
if (page.path === prop.value) {
|
|
91
|
+
return page;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return prop.value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// this utility is used for link component in both builder and preview
|
|
102
|
+
// so need to optimize rerenders with computed
|
|
103
|
+
export const usePropUrl = (instanceId: Instance["id"], name: string) => {
|
|
104
|
+
const { propsByInstanceIdStore, pagesStore } = useContext(ReactSdkContext);
|
|
105
|
+
const pageStore = useMemo(
|
|
106
|
+
() =>
|
|
107
|
+
computed(
|
|
108
|
+
[propsByInstanceIdStore, pagesStore],
|
|
109
|
+
(propsByInstanceId, pages) =>
|
|
110
|
+
resolveUrlProp(instanceId, name, propsByInstanceId, pages)
|
|
111
|
+
),
|
|
112
|
+
[propsByInstanceIdStore, pagesStore, instanceId, name]
|
|
113
|
+
);
|
|
114
|
+
return useStore(pageStore);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const getInstanceIdFromComponentProps = (
|
|
118
|
+
props: Record<string, unknown>
|
|
119
|
+
) => {
|
|
120
|
+
return props[idAttribute] as string;
|
|
121
|
+
};
|
|
@@ -4,7 +4,7 @@ import { Scripts, ScrollRestoration } from "@remix-run/react";
|
|
|
4
4
|
import type { Instance } from "@webstudio-is/project-build";
|
|
5
5
|
import type { GetComponent } from "../components/components-utils";
|
|
6
6
|
import { ReactSdkContext } from "../context";
|
|
7
|
-
import type { Assets, PropsByInstanceId } from "../props";
|
|
7
|
+
import type { Assets, Pages, PropsByInstanceId } from "../props";
|
|
8
8
|
import type { WebstudioComponent } from "./webstudio-component";
|
|
9
9
|
import { SessionStoragePolyfill } from "./session-storage-polyfill";
|
|
10
10
|
|
|
@@ -15,6 +15,7 @@ export const createElementsTree = ({
|
|
|
15
15
|
instance,
|
|
16
16
|
propsByInstanceIdStore,
|
|
17
17
|
assetsStore,
|
|
18
|
+
pagesStore,
|
|
18
19
|
Component,
|
|
19
20
|
getComponent,
|
|
20
21
|
}: {
|
|
@@ -22,6 +23,7 @@ export const createElementsTree = ({
|
|
|
22
23
|
instance: Instance;
|
|
23
24
|
propsByInstanceIdStore: ReadableAtom<PropsByInstanceId>;
|
|
24
25
|
assetsStore: ReadableAtom<Assets>;
|
|
26
|
+
pagesStore: ReadableAtom<Pages>;
|
|
25
27
|
Component: (props: ComponentProps<typeof WebstudioComponent>) => JSX.Element;
|
|
26
28
|
getComponent: GetComponent;
|
|
27
29
|
}) => {
|
|
@@ -47,7 +49,9 @@ export const createElementsTree = ({
|
|
|
47
49
|
getComponent,
|
|
48
50
|
});
|
|
49
51
|
return (
|
|
50
|
-
<ReactSdkContext.Provider
|
|
52
|
+
<ReactSdkContext.Provider
|
|
53
|
+
value={{ propsByInstanceIdStore, assetsStore, pagesStore }}
|
|
54
|
+
>
|
|
51
55
|
{root}
|
|
52
56
|
</ReactSdkContext.Provider>
|
|
53
57
|
);
|
package/src/tree/root.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { GetComponent } from "../components/components-utils";
|
|
|
18
18
|
|
|
19
19
|
export type Data = {
|
|
20
20
|
page: Page;
|
|
21
|
+
pages: Array<Page>;
|
|
21
22
|
build: Build;
|
|
22
23
|
assets: Array<Asset>;
|
|
23
24
|
params?: Params;
|
|
@@ -86,6 +87,7 @@ export const InstanceRoot = ({
|
|
|
86
87
|
getPropsByInstanceId(new Map(data.build.props))
|
|
87
88
|
),
|
|
88
89
|
assetsStore: atom(new Map(data.assets.map((asset) => [asset.id, asset]))),
|
|
90
|
+
pagesStore: atom(new Map(data.pages.map((page) => [page.id, page]))),
|
|
89
91
|
Component: Component ?? WebstudioComponent,
|
|
90
92
|
getComponent,
|
|
91
93
|
});
|