imean-service-engine-htmx-plugin 2.2.0 → 2.3.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/dist/index.d.mts +54 -65
- package/dist/index.d.ts +54 -65
- package/dist/index.js +502 -111
- package/dist/index.mjs +501 -110
- package/docs/README.md +22 -8
- package/docs/alpinejs-interactive-components.md +653 -0
- package/docs/hono-html-best-practices.md +509 -0
- package/package.json +1 -1
- package/docs/jsx-alpine-best-practices.md +0 -197
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { promises } from 'fs';
|
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { jsx, jsxs, Fragment } from 'hono/jsx/jsx-runtime';
|
|
5
5
|
import { getCookie } from 'hono/cookie';
|
|
6
|
+
import { html } from 'hono/html';
|
|
6
7
|
|
|
7
8
|
var __defProp = Object.defineProperty;
|
|
8
9
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -1757,18 +1758,21 @@ function Button(props) {
|
|
|
1757
1758
|
disabled ? "opacity-50 cursor-not-allowed" : "",
|
|
1758
1759
|
className
|
|
1759
1760
|
].filter(Boolean).join(" ");
|
|
1761
|
+
const isNewWindow = rest.target === "_blank";
|
|
1760
1762
|
const htmxAttrs = {};
|
|
1761
|
-
if (
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1763
|
+
if (!isNewWindow) {
|
|
1764
|
+
if (hxGet) htmxAttrs["hx-get"] = hxGet;
|
|
1765
|
+
if (hxPost) htmxAttrs["hx-post"] = hxPost;
|
|
1766
|
+
if (hxPut) htmxAttrs["hx-put"] = hxPut;
|
|
1767
|
+
if (hxDelete) htmxAttrs["hx-delete"] = hxDelete;
|
|
1768
|
+
if (hxTarget) htmxAttrs["hx-target"] = hxTarget;
|
|
1769
|
+
if (hxSwap) htmxAttrs["hx-swap"] = hxSwap;
|
|
1770
|
+
if (hxPushUrl !== void 0)
|
|
1771
|
+
htmxAttrs["hx-push-url"] = hxPushUrl === true ? "true" : hxPushUrl;
|
|
1772
|
+
if (hxIndicator) htmxAttrs["hx-indicator"] = hxIndicator;
|
|
1773
|
+
if (hxConfirm) htmxAttrs["hx-confirm"] = hxConfirm;
|
|
1774
|
+
if (hxHeaders) htmxAttrs["hx-headers"] = hxHeaders;
|
|
1775
|
+
}
|
|
1772
1776
|
const href = rest.href ?? hxGet ?? "#";
|
|
1773
1777
|
const { className: _, ...otherRest } = rest;
|
|
1774
1778
|
return /* @__PURE__ */ jsx(
|
|
@@ -1966,20 +1970,24 @@ function ActionLink(props) {
|
|
|
1966
1970
|
const { action, item } = props;
|
|
1967
1971
|
const hrefValue = action.href(item);
|
|
1968
1972
|
const isDelete = action.method === "delete";
|
|
1973
|
+
const isNewWindow = action.target === "_blank";
|
|
1969
1974
|
const className = `${STYLES.actionLink.base} ${isDelete ? STYLES.actionLink.delete : STYLES.actionLink.default}`;
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1975
|
+
const linkProps = {
|
|
1976
|
+
href: hrefValue,
|
|
1977
|
+
className,
|
|
1978
|
+
"data-testid": `table-action-${action.label}`,
|
|
1979
|
+
"aria-label": `${action.label}\uFF1A${item.id || item}`
|
|
1980
|
+
};
|
|
1981
|
+
if (isNewWindow) {
|
|
1982
|
+
linkProps.target = "_blank";
|
|
1983
|
+
linkProps.rel = "noopener noreferrer";
|
|
1984
|
+
} else if (isDelete) {
|
|
1985
|
+
linkProps["hx-delete"] = hrefValue;
|
|
1986
|
+
linkProps["hx-confirm"] = "\u786E\u5B9A\u5220\u9664\u5417\uFF1F";
|
|
1987
|
+
} else {
|
|
1988
|
+
linkProps["hx-get"] = hrefValue;
|
|
1989
|
+
}
|
|
1990
|
+
return /* @__PURE__ */ jsx("a", { ...linkProps, children: action.label });
|
|
1983
1991
|
}
|
|
1984
1992
|
function ActionCell(props) {
|
|
1985
1993
|
const { actions, item, actionStyle } = props;
|
|
@@ -2000,15 +2008,18 @@ function ActionCell(props) {
|
|
|
2000
2008
|
) : /* @__PURE__ */ jsx("div", { className: STYLES.actionButton, "data-testid": "table-actions", children: actions.map((action, idx) => {
|
|
2001
2009
|
const hrefValue = action.href(item);
|
|
2002
2010
|
const isDelete = action.method === "delete";
|
|
2011
|
+
const isNewWindow = action.target === "_blank";
|
|
2003
2012
|
return /* @__PURE__ */ jsx(
|
|
2004
2013
|
Button,
|
|
2005
2014
|
{
|
|
2006
2015
|
variant: isDelete ? "danger" : "secondary",
|
|
2007
2016
|
size: "sm",
|
|
2008
2017
|
href: hrefValue,
|
|
2009
|
-
"hx-get": !isDelete ? hrefValue : void 0,
|
|
2018
|
+
"hx-get": !isDelete && !isNewWindow ? hrefValue : void 0,
|
|
2010
2019
|
"hx-delete": isDelete ? hrefValue : void 0,
|
|
2011
2020
|
"hx-confirm": isDelete ? "\u786E\u5B9A\u5220\u9664\u5417\uFF1F" : void 0,
|
|
2021
|
+
target: isNewWindow ? "_blank" : void 0,
|
|
2022
|
+
rel: isNewWindow ? "noopener noreferrer" : void 0,
|
|
2012
2023
|
className: action.class,
|
|
2013
2024
|
"data-testid": `table-action-${action.label}`,
|
|
2014
2025
|
"aria-label": `${action.label}\uFF1A${item.id || item}`,
|
|
@@ -2105,19 +2116,24 @@ function ListPage(props) {
|
|
|
2105
2116
|
editPath,
|
|
2106
2117
|
deletePath,
|
|
2107
2118
|
listPath,
|
|
2108
|
-
filterFields
|
|
2119
|
+
filterFields,
|
|
2120
|
+
openMode
|
|
2109
2121
|
} = props;
|
|
2110
2122
|
const actions = [];
|
|
2111
2123
|
if (detailPath) {
|
|
2124
|
+
const mode = openMode?.detail || "dialog";
|
|
2112
2125
|
actions.push({
|
|
2113
2126
|
label: "\u67E5\u770B",
|
|
2114
|
-
href: detailPath
|
|
2127
|
+
href: detailPath,
|
|
2128
|
+
target: mode === "newWindow" ? "_blank" : void 0
|
|
2115
2129
|
});
|
|
2116
2130
|
}
|
|
2117
2131
|
if (editPath) {
|
|
2132
|
+
const mode = openMode?.edit || "dialog";
|
|
2118
2133
|
actions.push({
|
|
2119
2134
|
label: "\u7F16\u8F91",
|
|
2120
|
-
href: editPath
|
|
2135
|
+
href: editPath,
|
|
2136
|
+
target: mode === "newWindow" ? "_blank" : void 0
|
|
2121
2137
|
});
|
|
2122
2138
|
}
|
|
2123
2139
|
if (deletePath) {
|
|
@@ -2162,7 +2178,8 @@ function ListPage(props) {
|
|
|
2162
2178
|
actions: actions.length > 0 ? actions.map((action) => ({
|
|
2163
2179
|
label: action.label,
|
|
2164
2180
|
href: (item) => action.href(item),
|
|
2165
|
-
method: action.method
|
|
2181
|
+
method: action.method,
|
|
2182
|
+
target: action.target
|
|
2166
2183
|
})) : void 0,
|
|
2167
2184
|
pagination: {
|
|
2168
2185
|
page: result.page,
|
|
@@ -2203,6 +2220,7 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
2203
2220
|
listFieldNames;
|
|
2204
2221
|
filterSchema;
|
|
2205
2222
|
columnRenderers;
|
|
2223
|
+
openMode;
|
|
2206
2224
|
constructor(options) {
|
|
2207
2225
|
super({
|
|
2208
2226
|
name: "list",
|
|
@@ -2218,6 +2236,7 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
2218
2236
|
this.listFieldNames = options.listFieldNames;
|
|
2219
2237
|
this.filterSchema = options.filterSchema;
|
|
2220
2238
|
this.columnRenderers = options.columnRenderers;
|
|
2239
|
+
this.openMode = options.openMode;
|
|
2221
2240
|
}
|
|
2222
2241
|
getRoutes() {
|
|
2223
2242
|
return [{ method: "get", path: "/list" }];
|
|
@@ -2238,9 +2257,12 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
2238
2257
|
const prefix = context.prefix || "";
|
|
2239
2258
|
const basePath = `${prefix}/${model.modelName}`;
|
|
2240
2259
|
const listPath = `${basePath}/list`;
|
|
2241
|
-
const
|
|
2242
|
-
const
|
|
2243
|
-
const
|
|
2260
|
+
const createMode = this.openMode?.create || "dialog";
|
|
2261
|
+
const detailMode = this.openMode?.detail || "dialog";
|
|
2262
|
+
const editMode = this.openMode?.edit || "dialog";
|
|
2263
|
+
const createPath = model.features.get("create") ? createMode === "dialog" ? `${basePath}/new?dialog=true` : `${basePath}/new` : void 0;
|
|
2264
|
+
const detailPath = model.features.get("detail") ? (item) => detailMode === "dialog" ? `${basePath}/detail/${item.id}?dialog=true` : `${basePath}/detail/${item.id}` : void 0;
|
|
2265
|
+
const editPath = model.features.get("edit") ? (item) => editMode === "dialog" ? `${basePath}/edit/${item.id}?dialog=true` : `${basePath}/edit/${item.id}` : void 0;
|
|
2244
2266
|
const deletePath = this.deleteItem && model.features.get("delete") ? (item) => `${basePath}/${item.id}` : void 0;
|
|
2245
2267
|
return /* @__PURE__ */ jsx(
|
|
2246
2268
|
ListPage,
|
|
@@ -2253,7 +2275,8 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
2253
2275
|
editPath,
|
|
2254
2276
|
deletePath,
|
|
2255
2277
|
listPath,
|
|
2256
|
-
filterFields
|
|
2278
|
+
filterFields,
|
|
2279
|
+
openMode: this.openMode
|
|
2257
2280
|
}
|
|
2258
2281
|
);
|
|
2259
2282
|
}
|
|
@@ -2264,11 +2287,22 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
2264
2287
|
const hasCreate = model.features.get("create") !== void 0;
|
|
2265
2288
|
const actions = [];
|
|
2266
2289
|
if (hasCreate) {
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2290
|
+
const createMode = this.openMode?.create || "dialog";
|
|
2291
|
+
const createUrl = createMode === "dialog" ? `${basePath}/new?dialog=true` : `${basePath}/new`;
|
|
2292
|
+
if (createMode === "newWindow") {
|
|
2293
|
+
actions.push({
|
|
2294
|
+
label: "\u65B0\u5EFA",
|
|
2295
|
+
href: createUrl,
|
|
2296
|
+
variant: "primary",
|
|
2297
|
+
target: "_blank"
|
|
2298
|
+
});
|
|
2299
|
+
} else {
|
|
2300
|
+
actions.push({
|
|
2301
|
+
label: "\u65B0\u5EFA",
|
|
2302
|
+
hxGet: createUrl,
|
|
2303
|
+
variant: "primary"
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2272
2306
|
}
|
|
2273
2307
|
return actions;
|
|
2274
2308
|
}
|
|
@@ -3124,7 +3158,8 @@ function renderActionButton2(action, index) {
|
|
|
3124
3158
|
submit,
|
|
3125
3159
|
formId,
|
|
3126
3160
|
onClick,
|
|
3127
|
-
className = ""
|
|
3161
|
+
className = "",
|
|
3162
|
+
target
|
|
3128
3163
|
} = action;
|
|
3129
3164
|
if (submit && formId) {
|
|
3130
3165
|
const variantStyles = {
|
|
@@ -3176,18 +3211,21 @@ function renderActionButton2(action, index) {
|
|
|
3176
3211
|
} else if (label === "\u53D6\u6D88") {
|
|
3177
3212
|
testId = "cancel-button";
|
|
3178
3213
|
}
|
|
3214
|
+
const isNewWindow = target === "_blank";
|
|
3179
3215
|
return /* @__PURE__ */ jsx(
|
|
3180
3216
|
Button,
|
|
3181
3217
|
{
|
|
3182
3218
|
variant,
|
|
3183
3219
|
href,
|
|
3184
|
-
hxGet,
|
|
3185
|
-
hxPost,
|
|
3186
|
-
hxPut,
|
|
3187
|
-
hxDelete,
|
|
3188
|
-
hxConfirm: confirm,
|
|
3220
|
+
hxGet: !isNewWindow ? hxGet : void 0,
|
|
3221
|
+
hxPost: !isNewWindow ? hxPost : void 0,
|
|
3222
|
+
hxPut: !isNewWindow ? hxPut : void 0,
|
|
3223
|
+
hxDelete: !isNewWindow ? hxDelete : void 0,
|
|
3224
|
+
hxConfirm: !isNewWindow ? confirm : void 0,
|
|
3189
3225
|
className,
|
|
3190
3226
|
"data-testid": testId,
|
|
3227
|
+
target,
|
|
3228
|
+
rel: target === "_blank" ? "noopener noreferrer" : void 0,
|
|
3191
3229
|
children: label
|
|
3192
3230
|
},
|
|
3193
3231
|
index
|
|
@@ -3979,80 +4017,433 @@ var HtmxAdminPlugin = class {
|
|
|
3979
4017
|
registerHomeRedirect(this.pages, routeOptions);
|
|
3980
4018
|
}
|
|
3981
4019
|
};
|
|
3982
|
-
function
|
|
3983
|
-
const {
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4020
|
+
function ObjectEditor(props) {
|
|
4021
|
+
const { value, fieldName, objectSchema } = props;
|
|
4022
|
+
if (!objectSchema) {
|
|
4023
|
+
return /* @__PURE__ */ jsx("div", { className: "p-4 border border-yellow-300 rounded-lg bg-yellow-50 text-yellow-800 text-sm", children: "\u8BF7\u63D0\u4F9B objectSchema \u53C2\u6570\u4EE5\u4F7F\u7528\u5BF9\u8C61\u7F16\u8F91\u5668" });
|
|
4024
|
+
}
|
|
4025
|
+
const fields = parseSchemaToFields(objectSchema);
|
|
4026
|
+
const initialObject = value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
|
|
4027
|
+
fields.forEach((field) => {
|
|
4028
|
+
if (!(field.name in initialObject)) {
|
|
4029
|
+
if (field.type === "number") {
|
|
4030
|
+
initialObject[field.name] = field.required ? 0 : void 0;
|
|
4031
|
+
} else if (field.type === "checkbox") {
|
|
4032
|
+
initialObject[field.name] = field.required ? false : void 0;
|
|
4033
|
+
} else {
|
|
4034
|
+
initialObject[field.name] = field.required ? "" : void 0;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
});
|
|
4038
|
+
const fieldNames = fields.map((f) => f.name);
|
|
4039
|
+
const fieldNamesJson = JSON.stringify(fieldNames);
|
|
4040
|
+
const initialValueJson = JSON.stringify(initialObject);
|
|
4041
|
+
const xDataContent = `{
|
|
4042
|
+
obj: {},
|
|
4043
|
+
init() {
|
|
4044
|
+
const dataAttr = this.$el.getAttribute('data-initial-value');
|
|
4045
|
+
if (dataAttr) {
|
|
4046
|
+
try {
|
|
4047
|
+
this.obj = JSON.parse(dataAttr);
|
|
4048
|
+
} catch (e) {
|
|
4049
|
+
console.error('Failed to parse initial value:', e);
|
|
4050
|
+
this.obj = {};
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
const fieldNames = ${fieldNamesJson};
|
|
4054
|
+
fieldNames.forEach(fieldName => {
|
|
4055
|
+
if (!(fieldName in this.obj)) {
|
|
4056
|
+
this.obj[fieldName] = undefined;
|
|
4057
|
+
}
|
|
4058
|
+
});
|
|
4059
|
+
this.updateHiddenField();
|
|
4060
|
+
},
|
|
4061
|
+
updateHiddenField() {
|
|
4062
|
+
const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
|
|
4063
|
+
if (hiddenInput) {
|
|
4064
|
+
hiddenInput.value = JSON.stringify(this.obj);
|
|
4004
4065
|
}
|
|
4066
|
+
},
|
|
4067
|
+
updateField(fieldName, value, fieldType, required) {
|
|
4068
|
+
let convertedValue = value;
|
|
4069
|
+
if (fieldType === 'number') {
|
|
4070
|
+
convertedValue = value === '' ? (required ? 0 : undefined) : Number(value);
|
|
4071
|
+
if (isNaN(convertedValue)) convertedValue = required ? 0 : undefined;
|
|
4072
|
+
} else if (fieldType === 'checkbox') {
|
|
4073
|
+
convertedValue = value === 'true' || value === true || value === '1' || value === 1;
|
|
4074
|
+
} else {
|
|
4075
|
+
convertedValue = value || (required ? '' : undefined);
|
|
4076
|
+
}
|
|
4077
|
+
if (convertedValue === undefined && !required) {
|
|
4078
|
+
delete this.obj[fieldName];
|
|
4079
|
+
} else {
|
|
4080
|
+
this.obj[fieldName] = convertedValue;
|
|
4081
|
+
}
|
|
4082
|
+
this.updateHiddenField();
|
|
4005
4083
|
}
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
const
|
|
4010
|
-
|
|
4011
|
-
|
|
4084
|
+
}`;
|
|
4085
|
+
const generateField = (field) => {
|
|
4086
|
+
const fieldId = `${fieldName}-${field.name}`;
|
|
4087
|
+
const fieldNameVar = `obj.${field.name}`;
|
|
4088
|
+
const requiredValue = field.required ? "true" : "false";
|
|
4089
|
+
const fieldNameForJs = JSON.stringify(field.name);
|
|
4090
|
+
let inputElement;
|
|
4091
|
+
if (field.type === "text") {
|
|
4092
|
+
inputElement = html`
|
|
4093
|
+
<input
|
|
4094
|
+
type="text"
|
|
4095
|
+
id="${fieldId}"
|
|
4096
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4097
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4098
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4099
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4100
|
+
${field.required ? "required" : ""}
|
|
4101
|
+
/>
|
|
4102
|
+
`;
|
|
4103
|
+
} else if (field.type === "textarea") {
|
|
4104
|
+
inputElement = html`
|
|
4105
|
+
<textarea
|
|
4106
|
+
id="${fieldId}"
|
|
4107
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4108
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4109
|
+
rows="4"
|
|
4110
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y"
|
|
4111
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4112
|
+
${field.required ? "required" : ""}
|
|
4113
|
+
></textarea>
|
|
4114
|
+
`;
|
|
4115
|
+
} else if (field.type === "number") {
|
|
4116
|
+
const step = field.step || (field.step === void 0 ? "1" : "any");
|
|
4117
|
+
inputElement = html`
|
|
4118
|
+
<input
|
|
4119
|
+
type="number"
|
|
4120
|
+
id="${fieldId}"
|
|
4121
|
+
x-bind:value="${fieldNameVar} != null ? ${fieldNameVar} : ''"
|
|
4122
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'number', ${requiredValue})"
|
|
4123
|
+
step="${step}"
|
|
4124
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4125
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4126
|
+
${field.required ? "required" : ""}
|
|
4127
|
+
/>
|
|
4128
|
+
`;
|
|
4129
|
+
} else if (field.type === "date") {
|
|
4130
|
+
inputElement = html`
|
|
4131
|
+
<input
|
|
4132
|
+
type="date"
|
|
4133
|
+
id="${fieldId}"
|
|
4134
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4135
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'date', ${requiredValue})"
|
|
4136
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4137
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4138
|
+
${field.required ? "required" : ""}
|
|
4139
|
+
/>
|
|
4140
|
+
`;
|
|
4141
|
+
} else if (field.type === "email") {
|
|
4142
|
+
inputElement = html`
|
|
4143
|
+
<input
|
|
4144
|
+
type="email"
|
|
4145
|
+
id="${fieldId}"
|
|
4146
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4147
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4148
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4149
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4150
|
+
${field.required ? "required" : ""}
|
|
4151
|
+
/>
|
|
4152
|
+
`;
|
|
4153
|
+
} else if (field.type === "select" && field.options) {
|
|
4154
|
+
inputElement = html`
|
|
4155
|
+
<select
|
|
4156
|
+
id="${fieldId}"
|
|
4157
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4158
|
+
x-on:change="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4159
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
|
|
4160
|
+
data-testid="${fieldName}-select-${field.name}"
|
|
4161
|
+
${field.required ? "required" : ""}
|
|
4162
|
+
>
|
|
4163
|
+
${!field.required ? html`<option value="">请选择</option>` : ""}
|
|
4164
|
+
${field.options.map(
|
|
4165
|
+
(option) => html`
|
|
4166
|
+
<option value="${String(option.value)}">${option.label}</option>
|
|
4167
|
+
`
|
|
4168
|
+
)}
|
|
4169
|
+
</select>
|
|
4170
|
+
`;
|
|
4171
|
+
} else if (field.type === "checkbox") {
|
|
4172
|
+
inputElement = html`
|
|
4173
|
+
<div class="flex items-center">
|
|
4174
|
+
<input
|
|
4175
|
+
type="checkbox"
|
|
4176
|
+
id="${fieldId}"
|
|
4177
|
+
x-bind:checked="${fieldNameVar} === true || ${fieldNameVar} === 'true' || ${fieldNameVar} === 1 || ${fieldNameVar} === '1'"
|
|
4178
|
+
x-on:change="updateField(${fieldNameForJs}, $event.target.checked, 'checkbox', ${requiredValue})"
|
|
4179
|
+
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
4180
|
+
data-testid="${fieldName}-checkbox-${field.name}"
|
|
4181
|
+
/>
|
|
4182
|
+
<label for="${fieldId}" class="ml-2 text-sm text-gray-700">
|
|
4183
|
+
${field.label}
|
|
4184
|
+
</label>
|
|
4185
|
+
</div>
|
|
4186
|
+
`;
|
|
4012
4187
|
}
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
}
|
|
4188
|
+
return html`
|
|
4189
|
+
<div class="space-y-2" data-testid="${fieldName}-field-${field.name}">
|
|
4190
|
+
${field.type !== "checkbox" ? html`
|
|
4191
|
+
<label
|
|
4192
|
+
for="${fieldId}"
|
|
4193
|
+
class="block text-sm font-semibold text-gray-700"
|
|
4194
|
+
data-testid="${fieldName}-label-${field.name}"
|
|
4195
|
+
>
|
|
4196
|
+
${field.label}
|
|
4197
|
+
${field.required ? html`<span class="text-red-500 ml-1">*</span>` : ""}
|
|
4198
|
+
</label>
|
|
4199
|
+
` : ""}
|
|
4200
|
+
${inputElement}
|
|
4201
|
+
</div>
|
|
4202
|
+
`;
|
|
4203
|
+
};
|
|
4204
|
+
return html`
|
|
4205
|
+
<div
|
|
4206
|
+
x-data="${xDataContent}"
|
|
4207
|
+
data-initial-value="${initialValueJson}"
|
|
4208
|
+
x-init="init()"
|
|
4209
|
+
class="space-y-4"
|
|
4210
|
+
>
|
|
4211
|
+
<input
|
|
4212
|
+
type="hidden"
|
|
4213
|
+
name="${fieldName}"
|
|
4214
|
+
value=""
|
|
4215
|
+
data-testid="hidden-${fieldName}"
|
|
4216
|
+
/>
|
|
4217
|
+
<div class="space-y-4">
|
|
4218
|
+
${fields.map((field) => generateField(field))}
|
|
4219
|
+
</div>
|
|
4220
|
+
</div>
|
|
4221
|
+
`;
|
|
4023
4222
|
}
|
|
4024
|
-
function
|
|
4223
|
+
function StringArrayEditor(props) {
|
|
4025
4224
|
const {
|
|
4225
|
+
value,
|
|
4026
4226
|
fieldName,
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
autoSync = false,
|
|
4030
|
-
children,
|
|
4031
|
-
className = "space-y-4"
|
|
4227
|
+
placeholder = "\u8BF7\u8F93\u5165\u5185\u5BB9",
|
|
4228
|
+
allowEmpty = false
|
|
4032
4229
|
} = props;
|
|
4033
|
-
const
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
{
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4230
|
+
const initialItems = value || [];
|
|
4231
|
+
const initialValueJson = JSON.stringify(initialItems);
|
|
4232
|
+
const xDataContent = `{
|
|
4233
|
+
items: ${initialValueJson},
|
|
4234
|
+
draggedIndex: null,
|
|
4235
|
+
draggedOverIndex: null,
|
|
4236
|
+
fieldName: ${JSON.stringify(fieldName)},
|
|
4237
|
+
placeholder: ${JSON.stringify(placeholder)},
|
|
4238
|
+
allowEmpty: ${allowEmpty},
|
|
4239
|
+
init() {
|
|
4240
|
+
const dataAttr = this.$el.getAttribute('data-initial-value');
|
|
4241
|
+
if (dataAttr) {
|
|
4242
|
+
try {
|
|
4243
|
+
const parsed = JSON.parse(dataAttr);
|
|
4244
|
+
if (Array.isArray(parsed)) {
|
|
4245
|
+
this.items = parsed;
|
|
4246
|
+
} else {
|
|
4247
|
+
this.items = [];
|
|
4050
4248
|
}
|
|
4051
|
-
)
|
|
4052
|
-
|
|
4053
|
-
|
|
4249
|
+
} catch (e) {
|
|
4250
|
+
console.error('Failed to parse initial value:', e);
|
|
4251
|
+
this.items = [];
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
this.updateHiddenField();
|
|
4255
|
+
},
|
|
4256
|
+
updateHiddenField() {
|
|
4257
|
+
const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
|
|
4258
|
+
if (hiddenInput) {
|
|
4259
|
+
hiddenInput.value = JSON.stringify(this.items);
|
|
4260
|
+
}
|
|
4261
|
+
},
|
|
4262
|
+
addItem() {
|
|
4263
|
+
this.items.push('');
|
|
4264
|
+
this.updateHiddenField();
|
|
4265
|
+
this.$nextTick(() => {
|
|
4266
|
+
const keyInputs = this.$el.querySelectorAll('input[data-testid*="-input-"]');
|
|
4267
|
+
if (keyInputs.length > 0) {
|
|
4268
|
+
const lastInput = keyInputs[keyInputs.length - 1];
|
|
4269
|
+
if (lastInput && lastInput.focus) {
|
|
4270
|
+
lastInput.focus();
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
});
|
|
4274
|
+
},
|
|
4275
|
+
removeItem(index) {
|
|
4276
|
+
this.items.splice(index, 1);
|
|
4277
|
+
this.updateHiddenField();
|
|
4278
|
+
},
|
|
4279
|
+
updateItem(index, value) {
|
|
4280
|
+
this.items[index] = value;
|
|
4281
|
+
this.updateHiddenField();
|
|
4282
|
+
},
|
|
4283
|
+
handleDragStart(index, event) {
|
|
4284
|
+
this.draggedIndex = index;
|
|
4285
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
4286
|
+
event.dataTransfer.setData('text/plain', index.toString());
|
|
4287
|
+
const target = event.currentTarget || event.target.closest('[draggable="true"]');
|
|
4288
|
+
if (target) {
|
|
4289
|
+
target.style.opacity = '0.5';
|
|
4290
|
+
}
|
|
4291
|
+
},
|
|
4292
|
+
handleDragEnd(event) {
|
|
4293
|
+
const target = event.currentTarget || event.target.closest('[draggable="true"]');
|
|
4294
|
+
if (target) {
|
|
4295
|
+
target.style.opacity = '';
|
|
4296
|
+
}
|
|
4297
|
+
this.draggedIndex = null;
|
|
4298
|
+
this.draggedOverIndex = null;
|
|
4299
|
+
},
|
|
4300
|
+
handleDragOver(index, event) {
|
|
4301
|
+
event.preventDefault();
|
|
4302
|
+
event.dataTransfer.dropEffect = 'move';
|
|
4303
|
+
this.draggedOverIndex = index;
|
|
4304
|
+
},
|
|
4305
|
+
handleDragLeave() {
|
|
4306
|
+
this.draggedOverIndex = null;
|
|
4307
|
+
},
|
|
4308
|
+
handleDrop(index, event) {
|
|
4309
|
+
event.preventDefault();
|
|
4310
|
+
if (this.draggedIndex !== null && this.draggedIndex !== index) {
|
|
4311
|
+
const draggedItem = this.items[this.draggedIndex];
|
|
4312
|
+
this.items.splice(this.draggedIndex, 1);
|
|
4313
|
+
this.items.splice(index, 0, draggedItem);
|
|
4314
|
+
this.updateHiddenField();
|
|
4315
|
+
}
|
|
4316
|
+
this.draggedIndex = null;
|
|
4317
|
+
this.draggedOverIndex = null;
|
|
4054
4318
|
}
|
|
4055
|
-
|
|
4319
|
+
}`;
|
|
4320
|
+
return html`
|
|
4321
|
+
<div
|
|
4322
|
+
x-data="${xDataContent}"
|
|
4323
|
+
data-initial-value="${initialValueJson}"
|
|
4324
|
+
x-init="init()"
|
|
4325
|
+
class="space-y-3"
|
|
4326
|
+
>
|
|
4327
|
+
<input
|
|
4328
|
+
type="hidden"
|
|
4329
|
+
name="${fieldName}"
|
|
4330
|
+
value=""
|
|
4331
|
+
data-testid="hidden-${fieldName}"
|
|
4332
|
+
/>
|
|
4333
|
+
<div class="space-y-3">
|
|
4334
|
+
<!-- 头部:显示数量和添加按钮 -->
|
|
4335
|
+
<div class="flex items-center justify-between">
|
|
4336
|
+
<span class="text-sm text-gray-600">
|
|
4337
|
+
共 <span x-text="items.length">0</span> 项
|
|
4338
|
+
</span>
|
|
4339
|
+
<button
|
|
4340
|
+
type="button"
|
|
4341
|
+
x-on:click="addItem()"
|
|
4342
|
+
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2"
|
|
4343
|
+
data-testid="${fieldName}-add-button"
|
|
4344
|
+
>
|
|
4345
|
+
<svg
|
|
4346
|
+
class="w-4 h-4"
|
|
4347
|
+
fill="none"
|
|
4348
|
+
stroke="currentColor"
|
|
4349
|
+
viewBox="0 0 24 24"
|
|
4350
|
+
>
|
|
4351
|
+
<path
|
|
4352
|
+
stroke-linecap="round"
|
|
4353
|
+
stroke-linejoin="round"
|
|
4354
|
+
stroke-width="2"
|
|
4355
|
+
d="M12 4v16m8-8H4"
|
|
4356
|
+
/>
|
|
4357
|
+
</svg>
|
|
4358
|
+
添加项
|
|
4359
|
+
</button>
|
|
4360
|
+
</div>
|
|
4361
|
+
|
|
4362
|
+
<!-- 列表项 -->
|
|
4363
|
+
<div class="space-y-2" x-show="items.length > 0">
|
|
4364
|
+
<template x-for="(item, index) in items" x-bind:key="index">
|
|
4365
|
+
<div
|
|
4366
|
+
class="flex items-center gap-2 group"
|
|
4367
|
+
x-bind:class="{
|
|
4368
|
+
'opacity-50': draggedIndex === index,
|
|
4369
|
+
'border-blue-300 bg-blue-50': draggedOverIndex === index && draggedIndex !== null && draggedIndex !== index
|
|
4370
|
+
}"
|
|
4371
|
+
draggable="true"
|
|
4372
|
+
x-on:dragstart="handleDragStart(index, $event)"
|
|
4373
|
+
x-on:dragend="handleDragEnd($event)"
|
|
4374
|
+
x-on:dragover="handleDragOver(index, $event)"
|
|
4375
|
+
x-on:dragleave="handleDragLeave()"
|
|
4376
|
+
x-on:drop="handleDrop(index, $event)"
|
|
4377
|
+
x-bind:data-testid="fieldName + '-item-' + index"
|
|
4378
|
+
>
|
|
4379
|
+
<!-- 拖拽手柄 -->
|
|
4380
|
+
<div
|
|
4381
|
+
class="flex-shrink-0 cursor-move text-gray-400 hover:text-gray-600 transition-colors p-1"
|
|
4382
|
+
x-bind:data-testid="fieldName + '-drag-handle-' + index"
|
|
4383
|
+
title="拖拽排序"
|
|
4384
|
+
>
|
|
4385
|
+
<svg
|
|
4386
|
+
class="w-5 h-5"
|
|
4387
|
+
fill="none"
|
|
4388
|
+
stroke="currentColor"
|
|
4389
|
+
viewBox="0 0 24 24"
|
|
4390
|
+
>
|
|
4391
|
+
<path
|
|
4392
|
+
stroke-linecap="round"
|
|
4393
|
+
stroke-linejoin="round"
|
|
4394
|
+
stroke-width="2"
|
|
4395
|
+
d="M4 8h16M4 16h16"
|
|
4396
|
+
/>
|
|
4397
|
+
</svg>
|
|
4398
|
+
</div>
|
|
4399
|
+
|
|
4400
|
+
<!-- 输入框 -->
|
|
4401
|
+
<input
|
|
4402
|
+
type="text"
|
|
4403
|
+
x-bind:value="items[index] || ''"
|
|
4404
|
+
x-on:input="updateItem(index, $event.target.value)"
|
|
4405
|
+
x-bind:placeholder="placeholder + ' ' + (index + 1)"
|
|
4406
|
+
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4407
|
+
x-bind:data-testid="fieldName + '-input-' + index"
|
|
4408
|
+
x-bind:required="!allowEmpty"
|
|
4409
|
+
/>
|
|
4410
|
+
|
|
4411
|
+
<!-- 删除按钮 -->
|
|
4412
|
+
<button
|
|
4413
|
+
type="button"
|
|
4414
|
+
x-on:click="removeItem(index)"
|
|
4415
|
+
class="flex-shrink-0 px-3 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
4416
|
+
x-bind:data-testid="fieldName + '-remove-button-' + index"
|
|
4417
|
+
title="删除此项"
|
|
4418
|
+
>
|
|
4419
|
+
<svg
|
|
4420
|
+
class="w-5 h-5"
|
|
4421
|
+
fill="none"
|
|
4422
|
+
stroke="currentColor"
|
|
4423
|
+
viewBox="0 0 24 24"
|
|
4424
|
+
>
|
|
4425
|
+
<path
|
|
4426
|
+
stroke-linecap="round"
|
|
4427
|
+
stroke-linejoin="round"
|
|
4428
|
+
stroke-width="2"
|
|
4429
|
+
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
4430
|
+
/>
|
|
4431
|
+
</svg>
|
|
4432
|
+
</button>
|
|
4433
|
+
</div>
|
|
4434
|
+
</template>
|
|
4435
|
+
</div>
|
|
4436
|
+
|
|
4437
|
+
<!-- 空状态提示 -->
|
|
4438
|
+
<div
|
|
4439
|
+
x-show="items.length === 0"
|
|
4440
|
+
class="text-center py-8 text-gray-400 text-sm border border-dashed border-gray-300 rounded-lg"
|
|
4441
|
+
>
|
|
4442
|
+
暂无项,点击"添加项"按钮添加
|
|
4443
|
+
</div>
|
|
4444
|
+
</div>
|
|
4445
|
+
</div>
|
|
4446
|
+
`;
|
|
4056
4447
|
}
|
|
4057
4448
|
|
|
4058
|
-
export { BaseFeature, CustomFeature, DefaultCreateFeature, DefaultDeleteFeature, DefaultDetailFeature, DefaultEditFeature, DefaultListFeature, Dialog, ErrorAlert,
|
|
4449
|
+
export { BaseFeature, CustomFeature, DefaultCreateFeature, DefaultDeleteFeature, DefaultDetailFeature, DefaultEditFeature, DefaultListFeature, Dialog, ErrorAlert, HtmxAdminPlugin, LoadingBar, ObjectEditor, PageModel, StringArrayEditor, checkUserPermission, getUserInfo, modelNameToPath, parseListParams };
|