@elementor/editor-components 3.32.0-87 → 3.32.0-90

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/dist/index.js CHANGED
@@ -35,8 +35,9 @@ __export(index_exports, {
35
35
  module.exports = __toCommonJS(index_exports);
36
36
 
37
37
  // src/init.ts
38
+ var import_editor = require("@elementor/editor");
38
39
  var import_editor_elements_panel = require("@elementor/editor-elements-panel");
39
- var import_i18n = require("@wordpress/i18n");
40
+ var import_i18n3 = require("@wordpress/i18n");
40
41
 
41
42
  // src/components/components-tab.tsx
42
43
  var React = __toESM(require("react"));
@@ -45,13 +46,293 @@ function ComponentsTab() {
45
46
  return /* @__PURE__ */ React.createElement(import_ui.Box, { px: 2 }, "This is the Components tab.");
46
47
  }
47
48
 
49
+ // src/components/create-component-form/create-component-form.tsx
50
+ var React2 = __toESM(require("react"));
51
+ var import_react2 = require("react");
52
+ var import_editor_elements2 = require("@elementor/editor-elements");
53
+ var import_editor_ui = require("@elementor/editor-ui");
54
+ var import_icons = require("@elementor/icons");
55
+ var import_ui2 = require("@elementor/ui");
56
+ var import_i18n2 = require("@wordpress/i18n");
57
+
58
+ // src/hooks/use-components.ts
59
+ var import_query = require("@elementor/query");
60
+
61
+ // src/api.ts
62
+ var import_http_client = require("@elementor/http-client");
63
+ var BASE_URL = "elementor/v1/components";
64
+ var apiClient = {
65
+ get: () => (0, import_http_client.httpService)().get(`${BASE_URL}`).then((res) => res.data.data),
66
+ create: (payload) => (0, import_http_client.httpService)().post(`${BASE_URL}`, payload).then((res) => res.data.data)
67
+ };
68
+
69
+ // src/hooks/use-components.ts
70
+ var COMPONENTS_QUERY_KEY = "components";
71
+ var useComponents = () => {
72
+ return (0, import_query.useQuery)({
73
+ queryKey: [COMPONENTS_QUERY_KEY],
74
+ queryFn: apiClient.get,
75
+ staleTime: Infinity
76
+ });
77
+ };
78
+
79
+ // src/hooks/use-create-component.ts
80
+ var import_query2 = require("@elementor/query");
81
+ var useCreateComponentMutation = () => {
82
+ const queryClient = (0, import_query2.useQueryClient)();
83
+ return (0, import_query2.useMutation)({
84
+ mutationFn: apiClient.create,
85
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: [COMPONENTS_QUERY_KEY] })
86
+ });
87
+ };
88
+
89
+ // src/components/create-component-form/hooks/use-form.ts
90
+ var import_react = require("react");
91
+ var useForm = (initialValues) => {
92
+ const [values, setValues] = (0, import_react.useState)(initialValues);
93
+ const [errors, setErrors] = (0, import_react.useState)({});
94
+ const isValid = (0, import_react.useMemo)(() => {
95
+ return !Object.values(errors).some((error) => error);
96
+ }, [errors]);
97
+ const handleChange = (e, field, validationSchema) => {
98
+ const updated = { ...values, [field]: e.target.value };
99
+ setValues(updated);
100
+ const { success, errors: validationErrors } = validateForm(updated, validationSchema);
101
+ if (!success) {
102
+ setErrors(validationErrors);
103
+ } else {
104
+ setErrors({});
105
+ }
106
+ };
107
+ const validate = (validationSchema) => {
108
+ const { success, errors: validationErrors, parsedValues } = validateForm(values, validationSchema);
109
+ if (!success) {
110
+ setErrors(validationErrors);
111
+ return { success };
112
+ }
113
+ setErrors({});
114
+ return { success, parsedValues };
115
+ };
116
+ return {
117
+ values,
118
+ errors,
119
+ isValid,
120
+ handleChange,
121
+ validateForm: validate
122
+ };
123
+ };
124
+ var validateForm = (values, schema) => {
125
+ const result = schema.safeParse(values);
126
+ if (result.success) {
127
+ return { success: true, parsedValues: result.data };
128
+ }
129
+ const errors = {};
130
+ Object.entries(result.error.formErrors.fieldErrors).forEach(
131
+ ([field, error]) => {
132
+ errors[field] = error[0];
133
+ }
134
+ );
135
+ return { success: false, errors };
136
+ };
137
+
138
+ // src/components/create-component-form/utils/component-form-schema.ts
139
+ var import_schema = require("@elementor/schema");
140
+ var import_i18n = require("@wordpress/i18n");
141
+ var MIN_NAME_LENGTH = 2;
142
+ var MAX_NAME_LENGTH = 50;
143
+ var createBaseComponentSchema = (existingNames) => {
144
+ return import_schema.z.object({
145
+ componentName: import_schema.z.string().trim().max(
146
+ MAX_NAME_LENGTH,
147
+ (0, import_i18n.__)("Component name is too long. Please keep it under 50 characters.", "elementor")
148
+ ).refine((value) => !existingNames.includes(value), {
149
+ message: (0, import_i18n.__)("Component name already exists", "elementor")
150
+ })
151
+ });
152
+ };
153
+ var createSubmitComponentSchema = (existingNames) => {
154
+ const baseSchema = createBaseComponentSchema(existingNames);
155
+ return baseSchema.extend({
156
+ componentName: baseSchema.shape.componentName.refine((value) => value.length > 0, {
157
+ message: (0, import_i18n.__)("Component name is required.", "elementor")
158
+ }).refine((value) => value.length >= MIN_NAME_LENGTH, {
159
+ message: (0, import_i18n.__)("Component name is too short. Please enter at least 2 characters.", "elementor")
160
+ })
161
+ });
162
+ };
163
+
164
+ // src/components/create-component-form/utils/replace-element-with-component.ts
165
+ var import_editor_elements = require("@elementor/editor-elements");
166
+ var import_editor_props = require("@elementor/editor-props");
167
+ var replaceElementWithComponent = async (element, componentId) => {
168
+ (0, import_editor_elements.replaceElement)({
169
+ currentElement: element,
170
+ newElement: {
171
+ elType: "widget",
172
+ widgetType: "e-component",
173
+ settings: {
174
+ component_id: import_editor_props.numberPropTypeUtil.create(componentId)
175
+ }
176
+ },
177
+ withHistory: false
178
+ });
179
+ };
180
+
181
+ // src/components/create-component-form/create-component-form.tsx
182
+ function CreateComponentForm() {
183
+ const [element, setElement] = (0, import_react2.useState)(null);
184
+ const [anchorPosition, setAnchorPosition] = (0, import_react2.useState)();
185
+ const [resultNotification, setResultNotification] = (0, import_react2.useState)(null);
186
+ const { mutate: createComponent, isPending } = useCreateComponentMutation();
187
+ (0, import_react2.useEffect)(() => {
188
+ const OPEN_SAVE_AS_COMPONENT_FORM_EVENT = "elementor/editor/open-save-as-component-form";
189
+ const openPopup = (event) => {
190
+ setElement({ element: event.detail.element, elementLabel: (0, import_editor_elements2.getElementLabel)(event.detail.element.id) });
191
+ setAnchorPosition(event.detail.anchorPosition);
192
+ };
193
+ window.addEventListener(OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup);
194
+ return () => {
195
+ window.removeEventListener(OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup);
196
+ };
197
+ }, []);
198
+ const handleSave = async (values) => {
199
+ if (!element) {
200
+ throw new Error(`Can't save element as component: element not found`);
201
+ }
202
+ createComponent(
203
+ {
204
+ name: values.componentName,
205
+ content: [element.element.model.toJSON({ remove: ["default"] })]
206
+ },
207
+ {
208
+ onSuccess: (result) => {
209
+ if (!element) {
210
+ throw new Error(`Can't replace element with component: element not found`);
211
+ }
212
+ replaceElementWithComponent(element.element, result.component_id);
213
+ setResultNotification({
214
+ show: true,
215
+ // Translators: %1$s: Component name, %2$s: Component ID
216
+ message: (0, import_i18n2.__)("Component saved successfully as: %1$s (ID: %2$s)", "elementor").replace("%1$s", values.componentName).replace("%2$s", result.component_id.toString()),
217
+ type: "success"
218
+ });
219
+ resetAndClosePopup();
220
+ },
221
+ onError: () => {
222
+ const errorMessage = (0, import_i18n2.__)("Failed to save component. Please try again.", "elementor");
223
+ setResultNotification({
224
+ show: true,
225
+ message: errorMessage,
226
+ type: "error"
227
+ });
228
+ }
229
+ }
230
+ );
231
+ };
232
+ const resetAndClosePopup = () => {
233
+ setElement(null);
234
+ setAnchorPosition(void 0);
235
+ };
236
+ return /* @__PURE__ */ React2.createElement(import_editor_ui.ThemeProvider, null, /* @__PURE__ */ React2.createElement(
237
+ import_ui2.Popover,
238
+ {
239
+ open: element !== null,
240
+ onClose: resetAndClosePopup,
241
+ anchorReference: "anchorPosition",
242
+ anchorPosition
243
+ },
244
+ element !== null && /* @__PURE__ */ React2.createElement(
245
+ Form,
246
+ {
247
+ initialValues: { componentName: element.elementLabel },
248
+ handleSave,
249
+ isSubmitting: isPending,
250
+ closePopup: resetAndClosePopup
251
+ }
252
+ )
253
+ ), /* @__PURE__ */ React2.createElement(import_ui2.Snackbar, { open: resultNotification?.show, onClose: () => setResultNotification(null) }, /* @__PURE__ */ React2.createElement(
254
+ import_ui2.Alert,
255
+ {
256
+ onClose: () => setResultNotification(null),
257
+ severity: resultNotification?.type,
258
+ sx: { width: "100%" }
259
+ },
260
+ resultNotification?.message
261
+ )));
262
+ }
263
+ var FONT_SIZE = "tiny";
264
+ var Form = ({
265
+ initialValues,
266
+ handleSave,
267
+ isSubmitting,
268
+ closePopup
269
+ }) => {
270
+ const { values, errors, isValid, handleChange, validateForm: validateForm2 } = useForm(initialValues);
271
+ const { data: components } = useComponents();
272
+ const existingComponentNames = (0, import_react2.useMemo)(() => {
273
+ return components?.map((component) => component.name) ?? [];
274
+ }, [components]);
275
+ const changeValidationSchema = (0, import_react2.useMemo)(
276
+ () => createBaseComponentSchema(existingComponentNames),
277
+ [existingComponentNames]
278
+ );
279
+ const submitValidationSchema = (0, import_react2.useMemo)(
280
+ () => createSubmitComponentSchema(existingComponentNames),
281
+ [existingComponentNames]
282
+ );
283
+ const handleSubmit = () => {
284
+ const { success, parsedValues } = validateForm2(submitValidationSchema);
285
+ if (success) {
286
+ handleSave(parsedValues);
287
+ }
288
+ };
289
+ return /* @__PURE__ */ React2.createElement(import_ui2.Stack, { alignItems: "start", width: "268px" }, /* @__PURE__ */ React2.createElement(
290
+ import_ui2.Stack,
291
+ {
292
+ direction: "row",
293
+ alignItems: "center",
294
+ py: 1,
295
+ px: 1.5,
296
+ sx: { columnGap: 0.5, borderBottom: "1px solid", borderColor: "divider", width: "100%" }
297
+ },
298
+ /* @__PURE__ */ React2.createElement(import_icons.StarIcon, { fontSize: FONT_SIZE }),
299
+ /* @__PURE__ */ React2.createElement(import_ui2.Typography, { variant: "caption", sx: { color: "text.primary", fontWeight: "500", lineHeight: 1 } }, (0, import_i18n2.__)("Save as a component", "elementor"))
300
+ ), /* @__PURE__ */ React2.createElement(import_ui2.Grid, { container: true, gap: 0.75, alignItems: "start", p: 1.5 }, /* @__PURE__ */ React2.createElement(import_ui2.Grid, { item: true, xs: 12 }, /* @__PURE__ */ React2.createElement(import_ui2.FormLabel, { htmlFor: "component-name", size: "tiny" }, (0, import_i18n2.__)("Name", "elementor"))), /* @__PURE__ */ React2.createElement(import_ui2.Grid, { item: true, xs: 12 }, /* @__PURE__ */ React2.createElement(
301
+ import_ui2.TextField,
302
+ {
303
+ id: "component-name",
304
+ size: FONT_SIZE,
305
+ fullWidth: true,
306
+ value: values.componentName,
307
+ onChange: (e) => handleChange(e, "componentName", changeValidationSchema),
308
+ inputProps: { style: { color: "text.primary", fontWeight: "600" } },
309
+ error: Boolean(errors.componentName),
310
+ helperText: errors.componentName
311
+ }
312
+ ))), /* @__PURE__ */ React2.createElement(import_ui2.Stack, { direction: "row", justifyContent: "flex-end", alignSelf: "end", py: 1, px: 1.5 }, /* @__PURE__ */ React2.createElement(import_ui2.Button, { onClick: closePopup, disabled: isSubmitting, color: "secondary", variant: "text", size: "small" }, (0, import_i18n2.__)("Cancel", "elementor")), /* @__PURE__ */ React2.createElement(
313
+ import_ui2.Button,
314
+ {
315
+ onClick: handleSubmit,
316
+ disabled: isSubmitting || !isValid,
317
+ variant: "contained",
318
+ color: "primary",
319
+ size: "small"
320
+ },
321
+ isSubmitting ? (0, import_i18n2.__)("Creating\u2026", "elementor") : (0, import_i18n2.__)("Create", "elementor")
322
+ )));
323
+ };
324
+
48
325
  // src/init.ts
49
326
  function init() {
50
327
  (0, import_editor_elements_panel.injectTab)({
51
328
  id: "components",
52
- label: (0, import_i18n.__)("Components", "elementor"),
329
+ label: (0, import_i18n3.__)("Components", "elementor"),
53
330
  component: ComponentsTab
54
331
  });
332
+ (0, import_editor.injectIntoTop)({
333
+ id: "create-component-popup",
334
+ component: CreateComponentForm
335
+ });
55
336
  }
56
337
  // Annotate the CommonJS export names for ESM import in node:
57
338
  0 && (module.exports = {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/init.ts","../src/components/components-tab.tsx"],"sourcesContent":["export { init } from './init';\n","import { injectTab } from '@elementor/editor-elements-panel';\nimport { __ } from '@wordpress/i18n';\n\nimport { ComponentsTab } from './components/components-tab';\n\nexport function init() {\n\tinjectTab( {\n\t\tid: 'components',\n\t\tlabel: __( 'Components', 'elementor' ),\n\t\tcomponent: ComponentsTab,\n\t} );\n}\n","import * as React from 'react';\nimport { Box } from '@elementor/ui';\n\nexport function ComponentsTab() {\n\treturn <Box px={ 2 }>This is the Components tab.</Box>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mCAA0B;AAC1B,kBAAmB;;;ACDnB,YAAuB;AACvB,gBAAoB;AAEb,SAAS,gBAAgB;AAC/B,SAAO,oCAAC,iBAAI,IAAK,KAAI,6BAA2B;AACjD;;;ADAO,SAAS,OAAO;AACtB,8CAAW;AAAA,IACV,IAAI;AAAA,IACJ,WAAO,gBAAI,cAAc,WAAY;AAAA,IACrC,WAAW;AAAA,EACZ,CAAE;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/init.ts","../src/components/components-tab.tsx","../src/components/create-component-form/create-component-form.tsx","../src/hooks/use-components.ts","../src/api.ts","../src/hooks/use-create-component.ts","../src/components/create-component-form/hooks/use-form.ts","../src/components/create-component-form/utils/component-form-schema.ts","../src/components/create-component-form/utils/replace-element-with-component.ts"],"sourcesContent":["export { init } from './init';\n","import { injectIntoTop } from '@elementor/editor';\nimport { injectTab } from '@elementor/editor-elements-panel';\nimport { __ } from '@wordpress/i18n';\n\nimport { ComponentsTab } from './components/components-tab';\nimport { CreateComponentForm } from './components/create-component-form/create-component-form';\n\nexport function init() {\n\tinjectTab( {\n\t\tid: 'components',\n\t\tlabel: __( 'Components', 'elementor' ),\n\t\tcomponent: ComponentsTab,\n\t} );\n\n\tinjectIntoTop( {\n\t\tid: 'create-component-popup',\n\t\tcomponent: CreateComponentForm,\n\t} );\n}\n","import * as React from 'react';\nimport { Box } from '@elementor/ui';\n\nexport function ComponentsTab() {\n\treturn <Box px={ 2 }>This is the Components tab.</Box>;\n}\n","import * as React from 'react';\nimport { useEffect, useMemo, useState } from 'react';\nimport { getElementLabel, type V1Element } from '@elementor/editor-elements';\nimport { ThemeProvider } from '@elementor/editor-ui';\nimport { StarIcon } from '@elementor/icons';\nimport { Alert, Button, FormLabel, Grid, Popover, Snackbar, Stack, TextField, Typography } from '@elementor/ui';\nimport { __ } from '@wordpress/i18n';\n\nimport { type CreateComponentResponse } from '../../api';\nimport { useComponents } from '../../hooks/use-components';\nimport { useCreateComponentMutation } from '../../hooks/use-create-component';\nimport { type ComponentFormValues } from '../../types';\nimport { useForm } from './hooks/use-form';\nimport { createBaseComponentSchema, createSubmitComponentSchema } from './utils/component-form-schema';\nimport { replaceElementWithComponent } from './utils/replace-element-with-component';\n\ntype SaveAsComponentEventData = {\n\telement: V1Element;\n\tanchorPosition: { top: number; left: number };\n};\n\ntype ResultNotification = {\n\tshow: boolean;\n\tmessage: string;\n\ttype: 'success' | 'error';\n};\n\nexport function CreateComponentForm() {\n\tconst [ element, setElement ] = useState< {\n\t\telement: V1Element;\n\t\telementLabel: string;\n\t} | null >( null );\n\n\tconst [ anchorPosition, setAnchorPosition ] = useState< { top: number; left: number } >();\n\n\tconst [ resultNotification, setResultNotification ] = useState< ResultNotification | null >( null );\n\n\tconst { mutate: createComponent, isPending } = useCreateComponentMutation();\n\n\tuseEffect( () => {\n\t\tconst OPEN_SAVE_AS_COMPONENT_FORM_EVENT = 'elementor/editor/open-save-as-component-form';\n\n\t\tconst openPopup = ( event: CustomEvent< SaveAsComponentEventData > ) => {\n\t\t\tsetElement( { element: event.detail.element, elementLabel: getElementLabel( event.detail.element.id ) } );\n\t\t\tsetAnchorPosition( event.detail.anchorPosition );\n\t\t};\n\n\t\twindow.addEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );\n\t\t};\n\t}, [] );\n\n\tconst handleSave = async ( values: ComponentFormValues ) => {\n\t\tif ( ! element ) {\n\t\t\tthrow new Error( `Can't save element as component: element not found` );\n\t\t}\n\n\t\tcreateComponent(\n\t\t\t{\n\t\t\t\tname: values.componentName,\n\t\t\t\tcontent: [ element.element.model.toJSON( { remove: [ 'default' ] } ) ],\n\t\t\t},\n\t\t\t{\n\t\t\t\tonSuccess: ( result: CreateComponentResponse ) => {\n\t\t\t\t\tif ( ! element ) {\n\t\t\t\t\t\tthrow new Error( `Can't replace element with component: element not found` );\n\t\t\t\t\t}\n\n\t\t\t\t\treplaceElementWithComponent( element.element, result.component_id );\n\n\t\t\t\t\tsetResultNotification( {\n\t\t\t\t\t\tshow: true,\n\t\t\t\t\t\t// Translators: %1$s: Component name, %2$s: Component ID\n\t\t\t\t\t\tmessage: __( 'Component saved successfully as: %1$s (ID: %2$s)', 'elementor' )\n\t\t\t\t\t\t\t.replace( '%1$s', values.componentName )\n\t\t\t\t\t\t\t.replace( '%2$s', result.component_id.toString() ),\n\t\t\t\t\t\ttype: 'success',\n\t\t\t\t\t} );\n\n\t\t\t\t\tresetAndClosePopup();\n\t\t\t\t},\n\t\t\t\tonError: () => {\n\t\t\t\t\tconst errorMessage = __( 'Failed to save component. Please try again.', 'elementor' );\n\t\t\t\t\tsetResultNotification( {\n\t\t\t\t\t\tshow: true,\n\t\t\t\t\t\tmessage: errorMessage,\n\t\t\t\t\t\ttype: 'error',\n\t\t\t\t\t} );\n\t\t\t\t},\n\t\t\t}\n\t\t);\n\t};\n\n\tconst resetAndClosePopup = () => {\n\t\tsetElement( null );\n\t\tsetAnchorPosition( undefined );\n\t};\n\n\treturn (\n\t\t<ThemeProvider>\n\t\t\t<Popover\n\t\t\t\topen={ element !== null }\n\t\t\t\tonClose={ resetAndClosePopup }\n\t\t\t\tanchorReference=\"anchorPosition\"\n\t\t\t\tanchorPosition={ anchorPosition }\n\t\t\t>\n\t\t\t\t{ element !== null && (\n\t\t\t\t\t<Form\n\t\t\t\t\t\tinitialValues={ { componentName: element.elementLabel } }\n\t\t\t\t\t\thandleSave={ handleSave }\n\t\t\t\t\t\tisSubmitting={ isPending }\n\t\t\t\t\t\tclosePopup={ resetAndClosePopup }\n\t\t\t\t\t/>\n\t\t\t\t) }\n\t\t\t</Popover>\n\t\t\t<Snackbar open={ resultNotification?.show } onClose={ () => setResultNotification( null ) }>\n\t\t\t\t<Alert\n\t\t\t\t\tonClose={ () => setResultNotification( null ) }\n\t\t\t\t\tseverity={ resultNotification?.type }\n\t\t\t\t\tsx={ { width: '100%' } }\n\t\t\t\t>\n\t\t\t\t\t{ resultNotification?.message }\n\t\t\t\t</Alert>\n\t\t\t</Snackbar>\n\t\t</ThemeProvider>\n\t);\n}\n\nconst FONT_SIZE = 'tiny';\n\nconst Form = ( {\n\tinitialValues,\n\thandleSave,\n\tisSubmitting,\n\tclosePopup,\n}: {\n\tinitialValues: ComponentFormValues;\n\thandleSave: ( values: ComponentFormValues ) => void;\n\tisSubmitting: boolean;\n\tclosePopup: () => void;\n} ) => {\n\tconst { values, errors, isValid, handleChange, validateForm } = useForm< ComponentFormValues >( initialValues );\n\n\tconst { data: components } = useComponents();\n\n\tconst existingComponentNames = useMemo( () => {\n\t\treturn components?.map( ( component ) => component.name ) ?? [];\n\t}, [ components ] );\n\n\tconst changeValidationSchema = useMemo(\n\t\t() => createBaseComponentSchema( existingComponentNames ),\n\t\t[ existingComponentNames ]\n\t);\n\tconst submitValidationSchema = useMemo(\n\t\t() => createSubmitComponentSchema( existingComponentNames ),\n\t\t[ existingComponentNames ]\n\t);\n\n\tconst handleSubmit = () => {\n\t\tconst { success, parsedValues } = validateForm( submitValidationSchema );\n\n\t\tif ( success ) {\n\t\t\thandleSave( parsedValues );\n\t\t}\n\t};\n\n\treturn (\n\t\t<Stack alignItems=\"start\" width=\"268px\">\n\t\t\t<Stack\n\t\t\t\tdirection=\"row\"\n\t\t\t\talignItems=\"center\"\n\t\t\t\tpy={ 1 }\n\t\t\t\tpx={ 1.5 }\n\t\t\t\tsx={ { columnGap: 0.5, borderBottom: '1px solid', borderColor: 'divider', width: '100%' } }\n\t\t\t>\n\t\t\t\t<StarIcon fontSize={ FONT_SIZE } />\n\t\t\t\t<Typography variant=\"caption\" sx={ { color: 'text.primary', fontWeight: '500', lineHeight: 1 } }>\n\t\t\t\t\t{ __( 'Save as a component', 'elementor' ) }\n\t\t\t\t</Typography>\n\t\t\t</Stack>\n\t\t\t<Grid container gap={ 0.75 } alignItems=\"start\" p={ 1.5 }>\n\t\t\t\t<Grid item xs={ 12 }>\n\t\t\t\t\t<FormLabel htmlFor={ 'component-name' } size=\"tiny\">\n\t\t\t\t\t\t{ __( 'Name', 'elementor' ) }\n\t\t\t\t\t</FormLabel>\n\t\t\t\t</Grid>\n\t\t\t\t<Grid item xs={ 12 }>\n\t\t\t\t\t<TextField\n\t\t\t\t\t\tid={ 'component-name' }\n\t\t\t\t\t\tsize={ FONT_SIZE }\n\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\tvalue={ values.componentName }\n\t\t\t\t\t\tonChange={ ( e: React.ChangeEvent< HTMLInputElement > ) =>\n\t\t\t\t\t\t\thandleChange( e, 'componentName', changeValidationSchema )\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinputProps={ { style: { color: 'text.primary', fontWeight: '600' } } }\n\t\t\t\t\t\terror={ Boolean( errors.componentName ) }\n\t\t\t\t\t\thelperText={ errors.componentName }\n\t\t\t\t\t/>\n\t\t\t\t</Grid>\n\t\t\t</Grid>\n\t\t\t<Stack direction=\"row\" justifyContent=\"flex-end\" alignSelf=\"end\" py={ 1 } px={ 1.5 }>\n\t\t\t\t<Button onClick={ closePopup } disabled={ isSubmitting } color=\"secondary\" variant=\"text\" size=\"small\">\n\t\t\t\t\t{ __( 'Cancel', 'elementor' ) }\n\t\t\t\t</Button>\n\t\t\t\t<Button\n\t\t\t\t\tonClick={ handleSubmit }\n\t\t\t\t\tdisabled={ isSubmitting || ! isValid }\n\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\tcolor=\"primary\"\n\t\t\t\t\tsize=\"small\"\n\t\t\t\t>\n\t\t\t\t\t{ isSubmitting ? __( 'Creating…', 'elementor' ) : __( 'Create', 'elementor' ) }\n\t\t\t\t</Button>\n\t\t\t</Stack>\n\t\t</Stack>\n\t);\n};\n","import { useQuery } from '@elementor/query';\n\nimport { apiClient } from '../api';\n\nexport const COMPONENTS_QUERY_KEY = 'components';\n\nexport const useComponents = () => {\n\treturn useQuery( {\n\t\tqueryKey: [ COMPONENTS_QUERY_KEY ],\n\t\tqueryFn: apiClient.get,\n\t\tstaleTime: Infinity,\n\t} );\n};\n","import { type V1ElementModelProps } from '@elementor/editor-elements';\nimport { type HttpResponse, httpService } from '@elementor/http-client';\n\nconst BASE_URL = 'elementor/v1/components';\n\ntype CreateComponentPayload = {\n\tname: string;\n\tcontent: V1ElementModelProps[];\n};\n\ntype GetComponentResponse = Array< {\n\tcomponent_id: number;\n\tname: string;\n} >;\n\nexport type CreateComponentResponse = {\n\tcomponent_id: number;\n};\n\nexport const apiClient = {\n\tget: () =>\n\t\thttpService()\n\t\t\t.get< HttpResponse< GetComponentResponse > >( `${ BASE_URL }` )\n\t\t\t.then( ( res ) => res.data.data ),\n\tcreate: ( payload: CreateComponentPayload ) =>\n\t\thttpService()\n\t\t\t.post< HttpResponse< CreateComponentResponse > >( `${ BASE_URL }`, payload )\n\t\t\t.then( ( res ) => res.data.data ),\n};\n","import { useMutation, useQueryClient } from '@elementor/query';\n\nimport { apiClient } from '../api';\nimport { COMPONENTS_QUERY_KEY } from './use-components';\n\nexport const useCreateComponentMutation = () => {\n\tconst queryClient = useQueryClient();\n\n\treturn useMutation( {\n\t\tmutationFn: apiClient.create,\n\t\tonSuccess: () => queryClient.invalidateQueries( { queryKey: [ COMPONENTS_QUERY_KEY ] } ),\n\t} );\n};\n","import { useMemo, useState } from 'react';\nimport { type z } from '@elementor/schema';\n\nexport const useForm = < TValues extends Record< string, unknown > >( initialValues: TValues ) => {\n\tconst [ values, setValues ] = useState< TValues >( initialValues );\n\tconst [ errors, setErrors ] = useState< Partial< Record< keyof TValues, string > > >( {} );\n\n\tconst isValid = useMemo( () => {\n\t\treturn ! Object.values( errors ).some( ( error ) => error );\n\t}, [ errors ] );\n\n\tconst handleChange = (\n\t\te: React.ChangeEvent< HTMLInputElement >,\n\t\tfield: keyof TValues,\n\t\tvalidationSchema: z.ZodType< TValues >\n\t) => {\n\t\tconst updated = { ...values, [ field ]: e.target.value };\n\t\tsetValues( updated );\n\n\t\tconst { success, errors: validationErrors } = validateForm( updated, validationSchema );\n\n\t\tif ( ! success ) {\n\t\t\tsetErrors( validationErrors );\n\t\t} else {\n\t\t\tsetErrors( {} );\n\t\t}\n\t};\n\n\tconst validate = (\n\t\tvalidationSchema: z.ZodType< TValues >\n\t): { success: true; parsedValues: TValues } | { success: false; parsedValues?: never } => {\n\t\tconst { success, errors: validationErrors, parsedValues } = validateForm( values, validationSchema );\n\n\t\tif ( ! success ) {\n\t\t\tsetErrors( validationErrors );\n\t\t\treturn { success };\n\t\t}\n\t\tsetErrors( {} );\n\t\treturn { success, parsedValues };\n\t};\n\n\treturn {\n\t\tvalues,\n\t\terrors,\n\t\tisValid,\n\t\thandleChange,\n\t\tvalidateForm: validate,\n\t};\n};\n\nconst validateForm = < TValues extends Record< string, unknown > >(\n\tvalues: TValues,\n\tschema: z.ZodType< TValues >\n):\n\t| { success: false; parsedValues?: never; errors: Partial< Record< keyof TValues, string > > }\n\t| { success: true; parsedValues: TValues; errors?: never } => {\n\tconst result = schema.safeParse( values );\n\n\tif ( result.success ) {\n\t\treturn { success: true, parsedValues: result.data };\n\t}\n\n\tconst errors = {} as Partial< Record< keyof TValues, string > >;\n\n\t( Object.entries( result.error.formErrors.fieldErrors ) as Array< [ keyof TValues, string[] ] > ).forEach(\n\t\t( [ field, error ] ) => {\n\t\t\terrors[ field ] = error[ 0 ];\n\t\t}\n\t);\n\n\treturn { success: false, errors };\n};\n","import { z } from '@elementor/schema';\nimport { __ } from '@wordpress/i18n';\n\nconst MIN_NAME_LENGTH = 2;\nconst MAX_NAME_LENGTH = 50;\n\nexport const createBaseComponentSchema = ( existingNames: string[] ) => {\n\treturn z.object( {\n\t\tcomponentName: z\n\t\t\t.string()\n\t\t\t.trim()\n\t\t\t.max(\n\t\t\t\tMAX_NAME_LENGTH,\n\t\t\t\t__( 'Component name is too long. Please keep it under 50 characters.', 'elementor' )\n\t\t\t)\n\t\t\t.refine( ( value ) => ! existingNames.includes( value ), {\n\t\t\t\tmessage: __( 'Component name already exists', 'elementor' ),\n\t\t\t} ),\n\t} );\n};\n\nexport const createSubmitComponentSchema = ( existingNames: string[] ) => {\n\tconst baseSchema = createBaseComponentSchema( existingNames );\n\n\treturn baseSchema.extend( {\n\t\tcomponentName: baseSchema.shape.componentName\n\t\t\t.refine( ( value ) => value.length > 0, {\n\t\t\t\tmessage: __( 'Component name is required.', 'elementor' ),\n\t\t\t} )\n\t\t\t.refine( ( value ) => value.length >= MIN_NAME_LENGTH, {\n\t\t\t\tmessage: __( 'Component name is too short. Please enter at least 2 characters.', 'elementor' ),\n\t\t\t} ),\n\t} );\n};\n","import { replaceElement, type V1Element } from '@elementor/editor-elements';\nimport { numberPropTypeUtil } from '@elementor/editor-props';\n\nexport const replaceElementWithComponent = async ( element: V1Element, componentId: number ) => {\n\treplaceElement( {\n\t\tcurrentElement: element,\n\t\tnewElement: {\n\t\t\telType: 'widget',\n\t\t\twidgetType: 'e-component',\n\t\t\tsettings: {\n\t\t\t\tcomponent_id: numberPropTypeUtil.create( componentId ),\n\t\t\t},\n\t\t},\n\t\twithHistory: false,\n\t} );\n};\n"],"mappings":"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","names":["import_i18n","React","import_react","import_editor_elements","import_ui","import_i18n","import_query","validateForm"]}
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/init.ts
2
+ import { injectIntoTop } from "@elementor/editor";
2
3
  import { injectTab } from "@elementor/editor-elements-panel";
3
- import { __ } from "@wordpress/i18n";
4
+ import { __ as __3 } from "@wordpress/i18n";
4
5
 
5
6
  // src/components/components-tab.tsx
6
7
  import * as React from "react";
@@ -9,13 +10,293 @@ function ComponentsTab() {
9
10
  return /* @__PURE__ */ React.createElement(Box, { px: 2 }, "This is the Components tab.");
10
11
  }
11
12
 
13
+ // src/components/create-component-form/create-component-form.tsx
14
+ import * as React2 from "react";
15
+ import { useEffect, useMemo as useMemo2, useState as useState2 } from "react";
16
+ import { getElementLabel } from "@elementor/editor-elements";
17
+ import { ThemeProvider } from "@elementor/editor-ui";
18
+ import { StarIcon } from "@elementor/icons";
19
+ import { Alert, Button, FormLabel, Grid, Popover, Snackbar, Stack, TextField, Typography } from "@elementor/ui";
20
+ import { __ as __2 } from "@wordpress/i18n";
21
+
22
+ // src/hooks/use-components.ts
23
+ import { useQuery } from "@elementor/query";
24
+
25
+ // src/api.ts
26
+ import { httpService } from "@elementor/http-client";
27
+ var BASE_URL = "elementor/v1/components";
28
+ var apiClient = {
29
+ get: () => httpService().get(`${BASE_URL}`).then((res) => res.data.data),
30
+ create: (payload) => httpService().post(`${BASE_URL}`, payload).then((res) => res.data.data)
31
+ };
32
+
33
+ // src/hooks/use-components.ts
34
+ var COMPONENTS_QUERY_KEY = "components";
35
+ var useComponents = () => {
36
+ return useQuery({
37
+ queryKey: [COMPONENTS_QUERY_KEY],
38
+ queryFn: apiClient.get,
39
+ staleTime: Infinity
40
+ });
41
+ };
42
+
43
+ // src/hooks/use-create-component.ts
44
+ import { useMutation, useQueryClient } from "@elementor/query";
45
+ var useCreateComponentMutation = () => {
46
+ const queryClient = useQueryClient();
47
+ return useMutation({
48
+ mutationFn: apiClient.create,
49
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: [COMPONENTS_QUERY_KEY] })
50
+ });
51
+ };
52
+
53
+ // src/components/create-component-form/hooks/use-form.ts
54
+ import { useMemo, useState } from "react";
55
+ var useForm = (initialValues) => {
56
+ const [values, setValues] = useState(initialValues);
57
+ const [errors, setErrors] = useState({});
58
+ const isValid = useMemo(() => {
59
+ return !Object.values(errors).some((error) => error);
60
+ }, [errors]);
61
+ const handleChange = (e, field, validationSchema) => {
62
+ const updated = { ...values, [field]: e.target.value };
63
+ setValues(updated);
64
+ const { success, errors: validationErrors } = validateForm(updated, validationSchema);
65
+ if (!success) {
66
+ setErrors(validationErrors);
67
+ } else {
68
+ setErrors({});
69
+ }
70
+ };
71
+ const validate = (validationSchema) => {
72
+ const { success, errors: validationErrors, parsedValues } = validateForm(values, validationSchema);
73
+ if (!success) {
74
+ setErrors(validationErrors);
75
+ return { success };
76
+ }
77
+ setErrors({});
78
+ return { success, parsedValues };
79
+ };
80
+ return {
81
+ values,
82
+ errors,
83
+ isValid,
84
+ handleChange,
85
+ validateForm: validate
86
+ };
87
+ };
88
+ var validateForm = (values, schema) => {
89
+ const result = schema.safeParse(values);
90
+ if (result.success) {
91
+ return { success: true, parsedValues: result.data };
92
+ }
93
+ const errors = {};
94
+ Object.entries(result.error.formErrors.fieldErrors).forEach(
95
+ ([field, error]) => {
96
+ errors[field] = error[0];
97
+ }
98
+ );
99
+ return { success: false, errors };
100
+ };
101
+
102
+ // src/components/create-component-form/utils/component-form-schema.ts
103
+ import { z } from "@elementor/schema";
104
+ import { __ } from "@wordpress/i18n";
105
+ var MIN_NAME_LENGTH = 2;
106
+ var MAX_NAME_LENGTH = 50;
107
+ var createBaseComponentSchema = (existingNames) => {
108
+ return z.object({
109
+ componentName: z.string().trim().max(
110
+ MAX_NAME_LENGTH,
111
+ __("Component name is too long. Please keep it under 50 characters.", "elementor")
112
+ ).refine((value) => !existingNames.includes(value), {
113
+ message: __("Component name already exists", "elementor")
114
+ })
115
+ });
116
+ };
117
+ var createSubmitComponentSchema = (existingNames) => {
118
+ const baseSchema = createBaseComponentSchema(existingNames);
119
+ return baseSchema.extend({
120
+ componentName: baseSchema.shape.componentName.refine((value) => value.length > 0, {
121
+ message: __("Component name is required.", "elementor")
122
+ }).refine((value) => value.length >= MIN_NAME_LENGTH, {
123
+ message: __("Component name is too short. Please enter at least 2 characters.", "elementor")
124
+ })
125
+ });
126
+ };
127
+
128
+ // src/components/create-component-form/utils/replace-element-with-component.ts
129
+ import { replaceElement } from "@elementor/editor-elements";
130
+ import { numberPropTypeUtil } from "@elementor/editor-props";
131
+ var replaceElementWithComponent = async (element, componentId) => {
132
+ replaceElement({
133
+ currentElement: element,
134
+ newElement: {
135
+ elType: "widget",
136
+ widgetType: "e-component",
137
+ settings: {
138
+ component_id: numberPropTypeUtil.create(componentId)
139
+ }
140
+ },
141
+ withHistory: false
142
+ });
143
+ };
144
+
145
+ // src/components/create-component-form/create-component-form.tsx
146
+ function CreateComponentForm() {
147
+ const [element, setElement] = useState2(null);
148
+ const [anchorPosition, setAnchorPosition] = useState2();
149
+ const [resultNotification, setResultNotification] = useState2(null);
150
+ const { mutate: createComponent, isPending } = useCreateComponentMutation();
151
+ useEffect(() => {
152
+ const OPEN_SAVE_AS_COMPONENT_FORM_EVENT = "elementor/editor/open-save-as-component-form";
153
+ const openPopup = (event) => {
154
+ setElement({ element: event.detail.element, elementLabel: getElementLabel(event.detail.element.id) });
155
+ setAnchorPosition(event.detail.anchorPosition);
156
+ };
157
+ window.addEventListener(OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup);
158
+ return () => {
159
+ window.removeEventListener(OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup);
160
+ };
161
+ }, []);
162
+ const handleSave = async (values) => {
163
+ if (!element) {
164
+ throw new Error(`Can't save element as component: element not found`);
165
+ }
166
+ createComponent(
167
+ {
168
+ name: values.componentName,
169
+ content: [element.element.model.toJSON({ remove: ["default"] })]
170
+ },
171
+ {
172
+ onSuccess: (result) => {
173
+ if (!element) {
174
+ throw new Error(`Can't replace element with component: element not found`);
175
+ }
176
+ replaceElementWithComponent(element.element, result.component_id);
177
+ setResultNotification({
178
+ show: true,
179
+ // Translators: %1$s: Component name, %2$s: Component ID
180
+ message: __2("Component saved successfully as: %1$s (ID: %2$s)", "elementor").replace("%1$s", values.componentName).replace("%2$s", result.component_id.toString()),
181
+ type: "success"
182
+ });
183
+ resetAndClosePopup();
184
+ },
185
+ onError: () => {
186
+ const errorMessage = __2("Failed to save component. Please try again.", "elementor");
187
+ setResultNotification({
188
+ show: true,
189
+ message: errorMessage,
190
+ type: "error"
191
+ });
192
+ }
193
+ }
194
+ );
195
+ };
196
+ const resetAndClosePopup = () => {
197
+ setElement(null);
198
+ setAnchorPosition(void 0);
199
+ };
200
+ return /* @__PURE__ */ React2.createElement(ThemeProvider, null, /* @__PURE__ */ React2.createElement(
201
+ Popover,
202
+ {
203
+ open: element !== null,
204
+ onClose: resetAndClosePopup,
205
+ anchorReference: "anchorPosition",
206
+ anchorPosition
207
+ },
208
+ element !== null && /* @__PURE__ */ React2.createElement(
209
+ Form,
210
+ {
211
+ initialValues: { componentName: element.elementLabel },
212
+ handleSave,
213
+ isSubmitting: isPending,
214
+ closePopup: resetAndClosePopup
215
+ }
216
+ )
217
+ ), /* @__PURE__ */ React2.createElement(Snackbar, { open: resultNotification?.show, onClose: () => setResultNotification(null) }, /* @__PURE__ */ React2.createElement(
218
+ Alert,
219
+ {
220
+ onClose: () => setResultNotification(null),
221
+ severity: resultNotification?.type,
222
+ sx: { width: "100%" }
223
+ },
224
+ resultNotification?.message
225
+ )));
226
+ }
227
+ var FONT_SIZE = "tiny";
228
+ var Form = ({
229
+ initialValues,
230
+ handleSave,
231
+ isSubmitting,
232
+ closePopup
233
+ }) => {
234
+ const { values, errors, isValid, handleChange, validateForm: validateForm2 } = useForm(initialValues);
235
+ const { data: components } = useComponents();
236
+ const existingComponentNames = useMemo2(() => {
237
+ return components?.map((component) => component.name) ?? [];
238
+ }, [components]);
239
+ const changeValidationSchema = useMemo2(
240
+ () => createBaseComponentSchema(existingComponentNames),
241
+ [existingComponentNames]
242
+ );
243
+ const submitValidationSchema = useMemo2(
244
+ () => createSubmitComponentSchema(existingComponentNames),
245
+ [existingComponentNames]
246
+ );
247
+ const handleSubmit = () => {
248
+ const { success, parsedValues } = validateForm2(submitValidationSchema);
249
+ if (success) {
250
+ handleSave(parsedValues);
251
+ }
252
+ };
253
+ return /* @__PURE__ */ React2.createElement(Stack, { alignItems: "start", width: "268px" }, /* @__PURE__ */ React2.createElement(
254
+ Stack,
255
+ {
256
+ direction: "row",
257
+ alignItems: "center",
258
+ py: 1,
259
+ px: 1.5,
260
+ sx: { columnGap: 0.5, borderBottom: "1px solid", borderColor: "divider", width: "100%" }
261
+ },
262
+ /* @__PURE__ */ React2.createElement(StarIcon, { fontSize: FONT_SIZE }),
263
+ /* @__PURE__ */ React2.createElement(Typography, { variant: "caption", sx: { color: "text.primary", fontWeight: "500", lineHeight: 1 } }, __2("Save as a component", "elementor"))
264
+ ), /* @__PURE__ */ React2.createElement(Grid, { container: true, gap: 0.75, alignItems: "start", p: 1.5 }, /* @__PURE__ */ React2.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React2.createElement(FormLabel, { htmlFor: "component-name", size: "tiny" }, __2("Name", "elementor"))), /* @__PURE__ */ React2.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React2.createElement(
265
+ TextField,
266
+ {
267
+ id: "component-name",
268
+ size: FONT_SIZE,
269
+ fullWidth: true,
270
+ value: values.componentName,
271
+ onChange: (e) => handleChange(e, "componentName", changeValidationSchema),
272
+ inputProps: { style: { color: "text.primary", fontWeight: "600" } },
273
+ error: Boolean(errors.componentName),
274
+ helperText: errors.componentName
275
+ }
276
+ ))), /* @__PURE__ */ React2.createElement(Stack, { direction: "row", justifyContent: "flex-end", alignSelf: "end", py: 1, px: 1.5 }, /* @__PURE__ */ React2.createElement(Button, { onClick: closePopup, disabled: isSubmitting, color: "secondary", variant: "text", size: "small" }, __2("Cancel", "elementor")), /* @__PURE__ */ React2.createElement(
277
+ Button,
278
+ {
279
+ onClick: handleSubmit,
280
+ disabled: isSubmitting || !isValid,
281
+ variant: "contained",
282
+ color: "primary",
283
+ size: "small"
284
+ },
285
+ isSubmitting ? __2("Creating\u2026", "elementor") : __2("Create", "elementor")
286
+ )));
287
+ };
288
+
12
289
  // src/init.ts
13
290
  function init() {
14
291
  injectTab({
15
292
  id: "components",
16
- label: __("Components", "elementor"),
293
+ label: __3("Components", "elementor"),
17
294
  component: ComponentsTab
18
295
  });
296
+ injectIntoTop({
297
+ id: "create-component-popup",
298
+ component: CreateComponentForm
299
+ });
19
300
  }
20
301
  export {
21
302
  init
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/init.ts","../src/components/components-tab.tsx"],"sourcesContent":["import { injectTab } from '@elementor/editor-elements-panel';\nimport { __ } from '@wordpress/i18n';\n\nimport { ComponentsTab } from './components/components-tab';\n\nexport function init() {\n\tinjectTab( {\n\t\tid: 'components',\n\t\tlabel: __( 'Components', 'elementor' ),\n\t\tcomponent: ComponentsTab,\n\t} );\n}\n","import * as React from 'react';\nimport { Box } from '@elementor/ui';\n\nexport function ComponentsTab() {\n\treturn <Box px={ 2 }>This is the Components tab.</Box>;\n}\n"],"mappings":";AAAA,SAAS,iBAAiB;AAC1B,SAAS,UAAU;;;ACDnB,YAAY,WAAW;AACvB,SAAS,WAAW;AAEb,SAAS,gBAAgB;AAC/B,SAAO,oCAAC,OAAI,IAAK,KAAI,6BAA2B;AACjD;;;ADAO,SAAS,OAAO;AACtB,YAAW;AAAA,IACV,IAAI;AAAA,IACJ,OAAO,GAAI,cAAc,WAAY;AAAA,IACrC,WAAW;AAAA,EACZ,CAAE;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/init.ts","../src/components/components-tab.tsx","../src/components/create-component-form/create-component-form.tsx","../src/hooks/use-components.ts","../src/api.ts","../src/hooks/use-create-component.ts","../src/components/create-component-form/hooks/use-form.ts","../src/components/create-component-form/utils/component-form-schema.ts","../src/components/create-component-form/utils/replace-element-with-component.ts"],"sourcesContent":["import { injectIntoTop } from '@elementor/editor';\nimport { injectTab } from '@elementor/editor-elements-panel';\nimport { __ } from '@wordpress/i18n';\n\nimport { ComponentsTab } from './components/components-tab';\nimport { CreateComponentForm } from './components/create-component-form/create-component-form';\n\nexport function init() {\n\tinjectTab( {\n\t\tid: 'components',\n\t\tlabel: __( 'Components', 'elementor' ),\n\t\tcomponent: ComponentsTab,\n\t} );\n\n\tinjectIntoTop( {\n\t\tid: 'create-component-popup',\n\t\tcomponent: CreateComponentForm,\n\t} );\n}\n","import * as React from 'react';\nimport { Box } from '@elementor/ui';\n\nexport function ComponentsTab() {\n\treturn <Box px={ 2 }>This is the Components tab.</Box>;\n}\n","import * as React from 'react';\nimport { useEffect, useMemo, useState } from 'react';\nimport { getElementLabel, type V1Element } from '@elementor/editor-elements';\nimport { ThemeProvider } from '@elementor/editor-ui';\nimport { StarIcon } from '@elementor/icons';\nimport { Alert, Button, FormLabel, Grid, Popover, Snackbar, Stack, TextField, Typography } from '@elementor/ui';\nimport { __ } from '@wordpress/i18n';\n\nimport { type CreateComponentResponse } from '../../api';\nimport { useComponents } from '../../hooks/use-components';\nimport { useCreateComponentMutation } from '../../hooks/use-create-component';\nimport { type ComponentFormValues } from '../../types';\nimport { useForm } from './hooks/use-form';\nimport { createBaseComponentSchema, createSubmitComponentSchema } from './utils/component-form-schema';\nimport { replaceElementWithComponent } from './utils/replace-element-with-component';\n\ntype SaveAsComponentEventData = {\n\telement: V1Element;\n\tanchorPosition: { top: number; left: number };\n};\n\ntype ResultNotification = {\n\tshow: boolean;\n\tmessage: string;\n\ttype: 'success' | 'error';\n};\n\nexport function CreateComponentForm() {\n\tconst [ element, setElement ] = useState< {\n\t\telement: V1Element;\n\t\telementLabel: string;\n\t} | null >( null );\n\n\tconst [ anchorPosition, setAnchorPosition ] = useState< { top: number; left: number } >();\n\n\tconst [ resultNotification, setResultNotification ] = useState< ResultNotification | null >( null );\n\n\tconst { mutate: createComponent, isPending } = useCreateComponentMutation();\n\n\tuseEffect( () => {\n\t\tconst OPEN_SAVE_AS_COMPONENT_FORM_EVENT = 'elementor/editor/open-save-as-component-form';\n\n\t\tconst openPopup = ( event: CustomEvent< SaveAsComponentEventData > ) => {\n\t\t\tsetElement( { element: event.detail.element, elementLabel: getElementLabel( event.detail.element.id ) } );\n\t\t\tsetAnchorPosition( event.detail.anchorPosition );\n\t\t};\n\n\t\twindow.addEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );\n\t\t};\n\t}, [] );\n\n\tconst handleSave = async ( values: ComponentFormValues ) => {\n\t\tif ( ! element ) {\n\t\t\tthrow new Error( `Can't save element as component: element not found` );\n\t\t}\n\n\t\tcreateComponent(\n\t\t\t{\n\t\t\t\tname: values.componentName,\n\t\t\t\tcontent: [ element.element.model.toJSON( { remove: [ 'default' ] } ) ],\n\t\t\t},\n\t\t\t{\n\t\t\t\tonSuccess: ( result: CreateComponentResponse ) => {\n\t\t\t\t\tif ( ! element ) {\n\t\t\t\t\t\tthrow new Error( `Can't replace element with component: element not found` );\n\t\t\t\t\t}\n\n\t\t\t\t\treplaceElementWithComponent( element.element, result.component_id );\n\n\t\t\t\t\tsetResultNotification( {\n\t\t\t\t\t\tshow: true,\n\t\t\t\t\t\t// Translators: %1$s: Component name, %2$s: Component ID\n\t\t\t\t\t\tmessage: __( 'Component saved successfully as: %1$s (ID: %2$s)', 'elementor' )\n\t\t\t\t\t\t\t.replace( '%1$s', values.componentName )\n\t\t\t\t\t\t\t.replace( '%2$s', result.component_id.toString() ),\n\t\t\t\t\t\ttype: 'success',\n\t\t\t\t\t} );\n\n\t\t\t\t\tresetAndClosePopup();\n\t\t\t\t},\n\t\t\t\tonError: () => {\n\t\t\t\t\tconst errorMessage = __( 'Failed to save component. Please try again.', 'elementor' );\n\t\t\t\t\tsetResultNotification( {\n\t\t\t\t\t\tshow: true,\n\t\t\t\t\t\tmessage: errorMessage,\n\t\t\t\t\t\ttype: 'error',\n\t\t\t\t\t} );\n\t\t\t\t},\n\t\t\t}\n\t\t);\n\t};\n\n\tconst resetAndClosePopup = () => {\n\t\tsetElement( null );\n\t\tsetAnchorPosition( undefined );\n\t};\n\n\treturn (\n\t\t<ThemeProvider>\n\t\t\t<Popover\n\t\t\t\topen={ element !== null }\n\t\t\t\tonClose={ resetAndClosePopup }\n\t\t\t\tanchorReference=\"anchorPosition\"\n\t\t\t\tanchorPosition={ anchorPosition }\n\t\t\t>\n\t\t\t\t{ element !== null && (\n\t\t\t\t\t<Form\n\t\t\t\t\t\tinitialValues={ { componentName: element.elementLabel } }\n\t\t\t\t\t\thandleSave={ handleSave }\n\t\t\t\t\t\tisSubmitting={ isPending }\n\t\t\t\t\t\tclosePopup={ resetAndClosePopup }\n\t\t\t\t\t/>\n\t\t\t\t) }\n\t\t\t</Popover>\n\t\t\t<Snackbar open={ resultNotification?.show } onClose={ () => setResultNotification( null ) }>\n\t\t\t\t<Alert\n\t\t\t\t\tonClose={ () => setResultNotification( null ) }\n\t\t\t\t\tseverity={ resultNotification?.type }\n\t\t\t\t\tsx={ { width: '100%' } }\n\t\t\t\t>\n\t\t\t\t\t{ resultNotification?.message }\n\t\t\t\t</Alert>\n\t\t\t</Snackbar>\n\t\t</ThemeProvider>\n\t);\n}\n\nconst FONT_SIZE = 'tiny';\n\nconst Form = ( {\n\tinitialValues,\n\thandleSave,\n\tisSubmitting,\n\tclosePopup,\n}: {\n\tinitialValues: ComponentFormValues;\n\thandleSave: ( values: ComponentFormValues ) => void;\n\tisSubmitting: boolean;\n\tclosePopup: () => void;\n} ) => {\n\tconst { values, errors, isValid, handleChange, validateForm } = useForm< ComponentFormValues >( initialValues );\n\n\tconst { data: components } = useComponents();\n\n\tconst existingComponentNames = useMemo( () => {\n\t\treturn components?.map( ( component ) => component.name ) ?? [];\n\t}, [ components ] );\n\n\tconst changeValidationSchema = useMemo(\n\t\t() => createBaseComponentSchema( existingComponentNames ),\n\t\t[ existingComponentNames ]\n\t);\n\tconst submitValidationSchema = useMemo(\n\t\t() => createSubmitComponentSchema( existingComponentNames ),\n\t\t[ existingComponentNames ]\n\t);\n\n\tconst handleSubmit = () => {\n\t\tconst { success, parsedValues } = validateForm( submitValidationSchema );\n\n\t\tif ( success ) {\n\t\t\thandleSave( parsedValues );\n\t\t}\n\t};\n\n\treturn (\n\t\t<Stack alignItems=\"start\" width=\"268px\">\n\t\t\t<Stack\n\t\t\t\tdirection=\"row\"\n\t\t\t\talignItems=\"center\"\n\t\t\t\tpy={ 1 }\n\t\t\t\tpx={ 1.5 }\n\t\t\t\tsx={ { columnGap: 0.5, borderBottom: '1px solid', borderColor: 'divider', width: '100%' } }\n\t\t\t>\n\t\t\t\t<StarIcon fontSize={ FONT_SIZE } />\n\t\t\t\t<Typography variant=\"caption\" sx={ { color: 'text.primary', fontWeight: '500', lineHeight: 1 } }>\n\t\t\t\t\t{ __( 'Save as a component', 'elementor' ) }\n\t\t\t\t</Typography>\n\t\t\t</Stack>\n\t\t\t<Grid container gap={ 0.75 } alignItems=\"start\" p={ 1.5 }>\n\t\t\t\t<Grid item xs={ 12 }>\n\t\t\t\t\t<FormLabel htmlFor={ 'component-name' } size=\"tiny\">\n\t\t\t\t\t\t{ __( 'Name', 'elementor' ) }\n\t\t\t\t\t</FormLabel>\n\t\t\t\t</Grid>\n\t\t\t\t<Grid item xs={ 12 }>\n\t\t\t\t\t<TextField\n\t\t\t\t\t\tid={ 'component-name' }\n\t\t\t\t\t\tsize={ FONT_SIZE }\n\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\tvalue={ values.componentName }\n\t\t\t\t\t\tonChange={ ( e: React.ChangeEvent< HTMLInputElement > ) =>\n\t\t\t\t\t\t\thandleChange( e, 'componentName', changeValidationSchema )\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinputProps={ { style: { color: 'text.primary', fontWeight: '600' } } }\n\t\t\t\t\t\terror={ Boolean( errors.componentName ) }\n\t\t\t\t\t\thelperText={ errors.componentName }\n\t\t\t\t\t/>\n\t\t\t\t</Grid>\n\t\t\t</Grid>\n\t\t\t<Stack direction=\"row\" justifyContent=\"flex-end\" alignSelf=\"end\" py={ 1 } px={ 1.5 }>\n\t\t\t\t<Button onClick={ closePopup } disabled={ isSubmitting } color=\"secondary\" variant=\"text\" size=\"small\">\n\t\t\t\t\t{ __( 'Cancel', 'elementor' ) }\n\t\t\t\t</Button>\n\t\t\t\t<Button\n\t\t\t\t\tonClick={ handleSubmit }\n\t\t\t\t\tdisabled={ isSubmitting || ! isValid }\n\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\tcolor=\"primary\"\n\t\t\t\t\tsize=\"small\"\n\t\t\t\t>\n\t\t\t\t\t{ isSubmitting ? __( 'Creating…', 'elementor' ) : __( 'Create', 'elementor' ) }\n\t\t\t\t</Button>\n\t\t\t</Stack>\n\t\t</Stack>\n\t);\n};\n","import { useQuery } from '@elementor/query';\n\nimport { apiClient } from '../api';\n\nexport const COMPONENTS_QUERY_KEY = 'components';\n\nexport const useComponents = () => {\n\treturn useQuery( {\n\t\tqueryKey: [ COMPONENTS_QUERY_KEY ],\n\t\tqueryFn: apiClient.get,\n\t\tstaleTime: Infinity,\n\t} );\n};\n","import { type V1ElementModelProps } from '@elementor/editor-elements';\nimport { type HttpResponse, httpService } from '@elementor/http-client';\n\nconst BASE_URL = 'elementor/v1/components';\n\ntype CreateComponentPayload = {\n\tname: string;\n\tcontent: V1ElementModelProps[];\n};\n\ntype GetComponentResponse = Array< {\n\tcomponent_id: number;\n\tname: string;\n} >;\n\nexport type CreateComponentResponse = {\n\tcomponent_id: number;\n};\n\nexport const apiClient = {\n\tget: () =>\n\t\thttpService()\n\t\t\t.get< HttpResponse< GetComponentResponse > >( `${ BASE_URL }` )\n\t\t\t.then( ( res ) => res.data.data ),\n\tcreate: ( payload: CreateComponentPayload ) =>\n\t\thttpService()\n\t\t\t.post< HttpResponse< CreateComponentResponse > >( `${ BASE_URL }`, payload )\n\t\t\t.then( ( res ) => res.data.data ),\n};\n","import { useMutation, useQueryClient } from '@elementor/query';\n\nimport { apiClient } from '../api';\nimport { COMPONENTS_QUERY_KEY } from './use-components';\n\nexport const useCreateComponentMutation = () => {\n\tconst queryClient = useQueryClient();\n\n\treturn useMutation( {\n\t\tmutationFn: apiClient.create,\n\t\tonSuccess: () => queryClient.invalidateQueries( { queryKey: [ COMPONENTS_QUERY_KEY ] } ),\n\t} );\n};\n","import { useMemo, useState } from 'react';\nimport { type z } from '@elementor/schema';\n\nexport const useForm = < TValues extends Record< string, unknown > >( initialValues: TValues ) => {\n\tconst [ values, setValues ] = useState< TValues >( initialValues );\n\tconst [ errors, setErrors ] = useState< Partial< Record< keyof TValues, string > > >( {} );\n\n\tconst isValid = useMemo( () => {\n\t\treturn ! Object.values( errors ).some( ( error ) => error );\n\t}, [ errors ] );\n\n\tconst handleChange = (\n\t\te: React.ChangeEvent< HTMLInputElement >,\n\t\tfield: keyof TValues,\n\t\tvalidationSchema: z.ZodType< TValues >\n\t) => {\n\t\tconst updated = { ...values, [ field ]: e.target.value };\n\t\tsetValues( updated );\n\n\t\tconst { success, errors: validationErrors } = validateForm( updated, validationSchema );\n\n\t\tif ( ! success ) {\n\t\t\tsetErrors( validationErrors );\n\t\t} else {\n\t\t\tsetErrors( {} );\n\t\t}\n\t};\n\n\tconst validate = (\n\t\tvalidationSchema: z.ZodType< TValues >\n\t): { success: true; parsedValues: TValues } | { success: false; parsedValues?: never } => {\n\t\tconst { success, errors: validationErrors, parsedValues } = validateForm( values, validationSchema );\n\n\t\tif ( ! success ) {\n\t\t\tsetErrors( validationErrors );\n\t\t\treturn { success };\n\t\t}\n\t\tsetErrors( {} );\n\t\treturn { success, parsedValues };\n\t};\n\n\treturn {\n\t\tvalues,\n\t\terrors,\n\t\tisValid,\n\t\thandleChange,\n\t\tvalidateForm: validate,\n\t};\n};\n\nconst validateForm = < TValues extends Record< string, unknown > >(\n\tvalues: TValues,\n\tschema: z.ZodType< TValues >\n):\n\t| { success: false; parsedValues?: never; errors: Partial< Record< keyof TValues, string > > }\n\t| { success: true; parsedValues: TValues; errors?: never } => {\n\tconst result = schema.safeParse( values );\n\n\tif ( result.success ) {\n\t\treturn { success: true, parsedValues: result.data };\n\t}\n\n\tconst errors = {} as Partial< Record< keyof TValues, string > >;\n\n\t( Object.entries( result.error.formErrors.fieldErrors ) as Array< [ keyof TValues, string[] ] > ).forEach(\n\t\t( [ field, error ] ) => {\n\t\t\terrors[ field ] = error[ 0 ];\n\t\t}\n\t);\n\n\treturn { success: false, errors };\n};\n","import { z } from '@elementor/schema';\nimport { __ } from '@wordpress/i18n';\n\nconst MIN_NAME_LENGTH = 2;\nconst MAX_NAME_LENGTH = 50;\n\nexport const createBaseComponentSchema = ( existingNames: string[] ) => {\n\treturn z.object( {\n\t\tcomponentName: z\n\t\t\t.string()\n\t\t\t.trim()\n\t\t\t.max(\n\t\t\t\tMAX_NAME_LENGTH,\n\t\t\t\t__( 'Component name is too long. Please keep it under 50 characters.', 'elementor' )\n\t\t\t)\n\t\t\t.refine( ( value ) => ! existingNames.includes( value ), {\n\t\t\t\tmessage: __( 'Component name already exists', 'elementor' ),\n\t\t\t} ),\n\t} );\n};\n\nexport const createSubmitComponentSchema = ( existingNames: string[] ) => {\n\tconst baseSchema = createBaseComponentSchema( existingNames );\n\n\treturn baseSchema.extend( {\n\t\tcomponentName: baseSchema.shape.componentName\n\t\t\t.refine( ( value ) => value.length > 0, {\n\t\t\t\tmessage: __( 'Component name is required.', 'elementor' ),\n\t\t\t} )\n\t\t\t.refine( ( value ) => value.length >= MIN_NAME_LENGTH, {\n\t\t\t\tmessage: __( 'Component name is too short. Please enter at least 2 characters.', 'elementor' ),\n\t\t\t} ),\n\t} );\n};\n","import { replaceElement, type V1Element } from '@elementor/editor-elements';\nimport { numberPropTypeUtil } from '@elementor/editor-props';\n\nexport const replaceElementWithComponent = async ( element: V1Element, componentId: number ) => {\n\treplaceElement( {\n\t\tcurrentElement: element,\n\t\tnewElement: {\n\t\t\telType: 'widget',\n\t\t\twidgetType: 'e-component',\n\t\t\tsettings: {\n\t\t\t\tcomponent_id: numberPropTypeUtil.create( componentId ),\n\t\t\t},\n\t\t},\n\t\twithHistory: false,\n\t} );\n};\n"],"mappings":"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","names":["__","React","useMemo","useState","__","useState","__","validateForm","useMemo","__"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-components",
3
3
  "description": "Elementor editor components",
4
- "version": "3.32.0-87",
4
+ "version": "3.32.0-90",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,8 +40,16 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
+ "@elementor/editor": "3.32.0-90",
44
+ "@elementor/editor-elements": "3.32.0-90",
45
+ "@elementor/editor-elements-panel": "3.32.0-90",
46
+ "@elementor/editor-props": "3.32.0-90",
47
+ "@elementor/editor-ui": "3.32.0-90",
48
+ "@elementor/http-client": "3.32.0-90",
49
+ "@elementor/icons": "1.46.0",
50
+ "@elementor/query": "3.32.0-90",
51
+ "@elementor/schema": "3.32.0-90",
43
52
  "@elementor/ui": "1.36.8",
44
- "@elementor/editor-elements-panel": "3.32.0-87",
45
53
  "@wordpress/i18n": "^5.13.0"
46
54
  },
47
55
  "peerDependencies": {
package/src/api.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { type V1ElementModelProps } from '@elementor/editor-elements';
2
+ import { type HttpResponse, httpService } from '@elementor/http-client';
3
+
4
+ const BASE_URL = 'elementor/v1/components';
5
+
6
+ type CreateComponentPayload = {
7
+ name: string;
8
+ content: V1ElementModelProps[];
9
+ };
10
+
11
+ type GetComponentResponse = Array< {
12
+ component_id: number;
13
+ name: string;
14
+ } >;
15
+
16
+ export type CreateComponentResponse = {
17
+ component_id: number;
18
+ };
19
+
20
+ export const apiClient = {
21
+ get: () =>
22
+ httpService()
23
+ .get< HttpResponse< GetComponentResponse > >( `${ BASE_URL }` )
24
+ .then( ( res ) => res.data.data ),
25
+ create: ( payload: CreateComponentPayload ) =>
26
+ httpService()
27
+ .post< HttpResponse< CreateComponentResponse > >( `${ BASE_URL }`, payload )
28
+ .then( ( res ) => res.data.data ),
29
+ };
@@ -0,0 +1,220 @@
1
+ import * as React from 'react';
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { getElementLabel, type V1Element } from '@elementor/editor-elements';
4
+ import { ThemeProvider } from '@elementor/editor-ui';
5
+ import { StarIcon } from '@elementor/icons';
6
+ import { Alert, Button, FormLabel, Grid, Popover, Snackbar, Stack, TextField, Typography } from '@elementor/ui';
7
+ import { __ } from '@wordpress/i18n';
8
+
9
+ import { type CreateComponentResponse } from '../../api';
10
+ import { useComponents } from '../../hooks/use-components';
11
+ import { useCreateComponentMutation } from '../../hooks/use-create-component';
12
+ import { type ComponentFormValues } from '../../types';
13
+ import { useForm } from './hooks/use-form';
14
+ import { createBaseComponentSchema, createSubmitComponentSchema } from './utils/component-form-schema';
15
+ import { replaceElementWithComponent } from './utils/replace-element-with-component';
16
+
17
+ type SaveAsComponentEventData = {
18
+ element: V1Element;
19
+ anchorPosition: { top: number; left: number };
20
+ };
21
+
22
+ type ResultNotification = {
23
+ show: boolean;
24
+ message: string;
25
+ type: 'success' | 'error';
26
+ };
27
+
28
+ export function CreateComponentForm() {
29
+ const [ element, setElement ] = useState< {
30
+ element: V1Element;
31
+ elementLabel: string;
32
+ } | null >( null );
33
+
34
+ const [ anchorPosition, setAnchorPosition ] = useState< { top: number; left: number } >();
35
+
36
+ const [ resultNotification, setResultNotification ] = useState< ResultNotification | null >( null );
37
+
38
+ const { mutate: createComponent, isPending } = useCreateComponentMutation();
39
+
40
+ useEffect( () => {
41
+ const OPEN_SAVE_AS_COMPONENT_FORM_EVENT = 'elementor/editor/open-save-as-component-form';
42
+
43
+ const openPopup = ( event: CustomEvent< SaveAsComponentEventData > ) => {
44
+ setElement( { element: event.detail.element, elementLabel: getElementLabel( event.detail.element.id ) } );
45
+ setAnchorPosition( event.detail.anchorPosition );
46
+ };
47
+
48
+ window.addEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );
49
+
50
+ return () => {
51
+ window.removeEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );
52
+ };
53
+ }, [] );
54
+
55
+ const handleSave = async ( values: ComponentFormValues ) => {
56
+ if ( ! element ) {
57
+ throw new Error( `Can't save element as component: element not found` );
58
+ }
59
+
60
+ createComponent(
61
+ {
62
+ name: values.componentName,
63
+ content: [ element.element.model.toJSON( { remove: [ 'default' ] } ) ],
64
+ },
65
+ {
66
+ onSuccess: ( result: CreateComponentResponse ) => {
67
+ if ( ! element ) {
68
+ throw new Error( `Can't replace element with component: element not found` );
69
+ }
70
+
71
+ replaceElementWithComponent( element.element, result.component_id );
72
+
73
+ setResultNotification( {
74
+ show: true,
75
+ // Translators: %1$s: Component name, %2$s: Component ID
76
+ message: __( 'Component saved successfully as: %1$s (ID: %2$s)', 'elementor' )
77
+ .replace( '%1$s', values.componentName )
78
+ .replace( '%2$s', result.component_id.toString() ),
79
+ type: 'success',
80
+ } );
81
+
82
+ resetAndClosePopup();
83
+ },
84
+ onError: () => {
85
+ const errorMessage = __( 'Failed to save component. Please try again.', 'elementor' );
86
+ setResultNotification( {
87
+ show: true,
88
+ message: errorMessage,
89
+ type: 'error',
90
+ } );
91
+ },
92
+ }
93
+ );
94
+ };
95
+
96
+ const resetAndClosePopup = () => {
97
+ setElement( null );
98
+ setAnchorPosition( undefined );
99
+ };
100
+
101
+ return (
102
+ <ThemeProvider>
103
+ <Popover
104
+ open={ element !== null }
105
+ onClose={ resetAndClosePopup }
106
+ anchorReference="anchorPosition"
107
+ anchorPosition={ anchorPosition }
108
+ >
109
+ { element !== null && (
110
+ <Form
111
+ initialValues={ { componentName: element.elementLabel } }
112
+ handleSave={ handleSave }
113
+ isSubmitting={ isPending }
114
+ closePopup={ resetAndClosePopup }
115
+ />
116
+ ) }
117
+ </Popover>
118
+ <Snackbar open={ resultNotification?.show } onClose={ () => setResultNotification( null ) }>
119
+ <Alert
120
+ onClose={ () => setResultNotification( null ) }
121
+ severity={ resultNotification?.type }
122
+ sx={ { width: '100%' } }
123
+ >
124
+ { resultNotification?.message }
125
+ </Alert>
126
+ </Snackbar>
127
+ </ThemeProvider>
128
+ );
129
+ }
130
+
131
+ const FONT_SIZE = 'tiny';
132
+
133
+ const Form = ( {
134
+ initialValues,
135
+ handleSave,
136
+ isSubmitting,
137
+ closePopup,
138
+ }: {
139
+ initialValues: ComponentFormValues;
140
+ handleSave: ( values: ComponentFormValues ) => void;
141
+ isSubmitting: boolean;
142
+ closePopup: () => void;
143
+ } ) => {
144
+ const { values, errors, isValid, handleChange, validateForm } = useForm< ComponentFormValues >( initialValues );
145
+
146
+ const { data: components } = useComponents();
147
+
148
+ const existingComponentNames = useMemo( () => {
149
+ return components?.map( ( component ) => component.name ) ?? [];
150
+ }, [ components ] );
151
+
152
+ const changeValidationSchema = useMemo(
153
+ () => createBaseComponentSchema( existingComponentNames ),
154
+ [ existingComponentNames ]
155
+ );
156
+ const submitValidationSchema = useMemo(
157
+ () => createSubmitComponentSchema( existingComponentNames ),
158
+ [ existingComponentNames ]
159
+ );
160
+
161
+ const handleSubmit = () => {
162
+ const { success, parsedValues } = validateForm( submitValidationSchema );
163
+
164
+ if ( success ) {
165
+ handleSave( parsedValues );
166
+ }
167
+ };
168
+
169
+ return (
170
+ <Stack alignItems="start" width="268px">
171
+ <Stack
172
+ direction="row"
173
+ alignItems="center"
174
+ py={ 1 }
175
+ px={ 1.5 }
176
+ sx={ { columnGap: 0.5, borderBottom: '1px solid', borderColor: 'divider', width: '100%' } }
177
+ >
178
+ <StarIcon fontSize={ FONT_SIZE } />
179
+ <Typography variant="caption" sx={ { color: 'text.primary', fontWeight: '500', lineHeight: 1 } }>
180
+ { __( 'Save as a component', 'elementor' ) }
181
+ </Typography>
182
+ </Stack>
183
+ <Grid container gap={ 0.75 } alignItems="start" p={ 1.5 }>
184
+ <Grid item xs={ 12 }>
185
+ <FormLabel htmlFor={ 'component-name' } size="tiny">
186
+ { __( 'Name', 'elementor' ) }
187
+ </FormLabel>
188
+ </Grid>
189
+ <Grid item xs={ 12 }>
190
+ <TextField
191
+ id={ 'component-name' }
192
+ size={ FONT_SIZE }
193
+ fullWidth
194
+ value={ values.componentName }
195
+ onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) =>
196
+ handleChange( e, 'componentName', changeValidationSchema )
197
+ }
198
+ inputProps={ { style: { color: 'text.primary', fontWeight: '600' } } }
199
+ error={ Boolean( errors.componentName ) }
200
+ helperText={ errors.componentName }
201
+ />
202
+ </Grid>
203
+ </Grid>
204
+ <Stack direction="row" justifyContent="flex-end" alignSelf="end" py={ 1 } px={ 1.5 }>
205
+ <Button onClick={ closePopup } disabled={ isSubmitting } color="secondary" variant="text" size="small">
206
+ { __( 'Cancel', 'elementor' ) }
207
+ </Button>
208
+ <Button
209
+ onClick={ handleSubmit }
210
+ disabled={ isSubmitting || ! isValid }
211
+ variant="contained"
212
+ color="primary"
213
+ size="small"
214
+ >
215
+ { isSubmitting ? __( 'Creating…', 'elementor' ) : __( 'Create', 'elementor' ) }
216
+ </Button>
217
+ </Stack>
218
+ </Stack>
219
+ );
220
+ };
@@ -0,0 +1,72 @@
1
+ import { useMemo, useState } from 'react';
2
+ import { type z } from '@elementor/schema';
3
+
4
+ export const useForm = < TValues extends Record< string, unknown > >( initialValues: TValues ) => {
5
+ const [ values, setValues ] = useState< TValues >( initialValues );
6
+ const [ errors, setErrors ] = useState< Partial< Record< keyof TValues, string > > >( {} );
7
+
8
+ const isValid = useMemo( () => {
9
+ return ! Object.values( errors ).some( ( error ) => error );
10
+ }, [ errors ] );
11
+
12
+ const handleChange = (
13
+ e: React.ChangeEvent< HTMLInputElement >,
14
+ field: keyof TValues,
15
+ validationSchema: z.ZodType< TValues >
16
+ ) => {
17
+ const updated = { ...values, [ field ]: e.target.value };
18
+ setValues( updated );
19
+
20
+ const { success, errors: validationErrors } = validateForm( updated, validationSchema );
21
+
22
+ if ( ! success ) {
23
+ setErrors( validationErrors );
24
+ } else {
25
+ setErrors( {} );
26
+ }
27
+ };
28
+
29
+ const validate = (
30
+ validationSchema: z.ZodType< TValues >
31
+ ): { success: true; parsedValues: TValues } | { success: false; parsedValues?: never } => {
32
+ const { success, errors: validationErrors, parsedValues } = validateForm( values, validationSchema );
33
+
34
+ if ( ! success ) {
35
+ setErrors( validationErrors );
36
+ return { success };
37
+ }
38
+ setErrors( {} );
39
+ return { success, parsedValues };
40
+ };
41
+
42
+ return {
43
+ values,
44
+ errors,
45
+ isValid,
46
+ handleChange,
47
+ validateForm: validate,
48
+ };
49
+ };
50
+
51
+ const validateForm = < TValues extends Record< string, unknown > >(
52
+ values: TValues,
53
+ schema: z.ZodType< TValues >
54
+ ):
55
+ | { success: false; parsedValues?: never; errors: Partial< Record< keyof TValues, string > > }
56
+ | { success: true; parsedValues: TValues; errors?: never } => {
57
+ const result = schema.safeParse( values );
58
+
59
+ if ( result.success ) {
60
+ return { success: true, parsedValues: result.data };
61
+ }
62
+
63
+ const errors = {} as Partial< Record< keyof TValues, string > >;
64
+
65
+ ( Object.entries( result.error.formErrors.fieldErrors ) as Array< [ keyof TValues, string[] ] > ).forEach(
66
+ ( [ field, error ] ) => {
67
+ errors[ field ] = error[ 0 ];
68
+ }
69
+ );
70
+
71
+ return { success: false, errors };
72
+ };
@@ -0,0 +1,34 @@
1
+ import { z } from '@elementor/schema';
2
+ import { __ } from '@wordpress/i18n';
3
+
4
+ const MIN_NAME_LENGTH = 2;
5
+ const MAX_NAME_LENGTH = 50;
6
+
7
+ export const createBaseComponentSchema = ( existingNames: string[] ) => {
8
+ return z.object( {
9
+ componentName: z
10
+ .string()
11
+ .trim()
12
+ .max(
13
+ MAX_NAME_LENGTH,
14
+ __( 'Component name is too long. Please keep it under 50 characters.', 'elementor' )
15
+ )
16
+ .refine( ( value ) => ! existingNames.includes( value ), {
17
+ message: __( 'Component name already exists', 'elementor' ),
18
+ } ),
19
+ } );
20
+ };
21
+
22
+ export const createSubmitComponentSchema = ( existingNames: string[] ) => {
23
+ const baseSchema = createBaseComponentSchema( existingNames );
24
+
25
+ return baseSchema.extend( {
26
+ componentName: baseSchema.shape.componentName
27
+ .refine( ( value ) => value.length > 0, {
28
+ message: __( 'Component name is required.', 'elementor' ),
29
+ } )
30
+ .refine( ( value ) => value.length >= MIN_NAME_LENGTH, {
31
+ message: __( 'Component name is too short. Please enter at least 2 characters.', 'elementor' ),
32
+ } ),
33
+ } );
34
+ };
@@ -0,0 +1,16 @@
1
+ import { replaceElement, type V1Element } from '@elementor/editor-elements';
2
+ import { numberPropTypeUtil } from '@elementor/editor-props';
3
+
4
+ export const replaceElementWithComponent = async ( element: V1Element, componentId: number ) => {
5
+ replaceElement( {
6
+ currentElement: element,
7
+ newElement: {
8
+ elType: 'widget',
9
+ widgetType: 'e-component',
10
+ settings: {
11
+ component_id: numberPropTypeUtil.create( componentId ),
12
+ },
13
+ },
14
+ withHistory: false,
15
+ } );
16
+ };
@@ -0,0 +1,13 @@
1
+ import { useQuery } from '@elementor/query';
2
+
3
+ import { apiClient } from '../api';
4
+
5
+ export const COMPONENTS_QUERY_KEY = 'components';
6
+
7
+ export const useComponents = () => {
8
+ return useQuery( {
9
+ queryKey: [ COMPONENTS_QUERY_KEY ],
10
+ queryFn: apiClient.get,
11
+ staleTime: Infinity,
12
+ } );
13
+ };
@@ -0,0 +1,13 @@
1
+ import { useMutation, useQueryClient } from '@elementor/query';
2
+
3
+ import { apiClient } from '../api';
4
+ import { COMPONENTS_QUERY_KEY } from './use-components';
5
+
6
+ export const useCreateComponentMutation = () => {
7
+ const queryClient = useQueryClient();
8
+
9
+ return useMutation( {
10
+ mutationFn: apiClient.create,
11
+ onSuccess: () => queryClient.invalidateQueries( { queryKey: [ COMPONENTS_QUERY_KEY ] } ),
12
+ } );
13
+ };
package/src/init.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import { injectIntoTop } from '@elementor/editor';
1
2
  import { injectTab } from '@elementor/editor-elements-panel';
2
3
  import { __ } from '@wordpress/i18n';
3
4
 
4
5
  import { ComponentsTab } from './components/components-tab';
6
+ import { CreateComponentForm } from './components/create-component-form/create-component-form';
5
7
 
6
8
  export function init() {
7
9
  injectTab( {
@@ -9,4 +11,9 @@ export function init() {
9
11
  label: __( 'Components', 'elementor' ),
10
12
  component: ComponentsTab,
11
13
  } );
14
+
15
+ injectIntoTop( {
16
+ id: 'create-component-popup',
17
+ component: CreateComponentForm,
18
+ } );
12
19
  }
package/src/types.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type ComponentFormValues = {
2
+ componentName: string;
3
+ };