diff --git a/compiler/plugins/vue.plugin.js b/compiler/plugins/vue.plugin.js index 3e20a64d..f8567ed9 100644 --- a/compiler/plugins/vue.plugin.js +++ b/compiler/plugins/vue.plugin.js @@ -19,13 +19,14 @@ module.exports = function vueCompilerPlugin() { code: { // Happens before formatting pre: (codeStr) => { - return [fixVueClassName, fixVueEventHandlers].reduce( - (acc, transform) => { - acc = transform(acc); - return acc; - }, - codeStr, - ); + return [ + fixVueClassName, + fixVueEventHandlers, + // Hello + ].reduce((acc, transform) => { + acc = transform(acc); + return acc; + }, codeStr); }, }, }; @@ -123,10 +124,17 @@ function fixVueEventHandlers(codeStr) { onTouchCancel: "touchcancel", }; - // Match all template sections, including named slots - const templateMatches = - codeStr.match(/]*>[\s\S]*?<\/template>/g) || []; - const scriptMatch = codeStr.match(/]*>([\s\S]*?)<\/script>/); + // Match the entire content, including multiple template and script sections + const fullMatch = codeStr.match( + /([\s\S]*]*>)([\s\S]*?)(<\/template>[\s\S]*)/, + ); + + if (!fullMatch) { + return codeStr; // If there's no match, return the original code + } + + const [, beforeTemplate, templateContent, afterTemplate] = fullMatch; + const scriptMatch = afterTemplate.match(/]*>([\s\S]*?)<\/script>/); if (!scriptMatch) { return codeStr; // If there's no script section, return the original code @@ -141,11 +149,14 @@ function fixVueEventHandlers(codeStr) { const detectedEvents = new Set(); + // Add all possible events to detectedEvents + Object.values(eventMappings).forEach((event) => detectedEvents.add(event)); + traverse(ast, { // Detect events in JSX attributes JSXAttribute(path) { const name = path.node.name; - if (t.isJSXIdentifier(name) && name.name in eventMappings) { + if (t.isJSXIdentifier(name) && name.name && name.name in eventMappings) { const vueEvent = eventMappings[name.name]; detectedEvents.add(vueEvent); name.name = `@${vueEvent}`; @@ -155,6 +166,7 @@ function fixVueEventHandlers(codeStr) { Property(path) { if ( t.isIdentifier(path.node.key) && + path.node.key.name && path.node.key.name in eventMappings ) { detectedEvents.add(eventMappings[path.node.key.name]); @@ -169,21 +181,111 @@ function fixVueEventHandlers(codeStr) { }); } }, - // Convert props event handlers to emit calls + // Merged CallExpression visitor CallExpression(path) { if ( t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, { name: "props" }) && t.isIdentifier(path.node.callee.property) && + path.node.callee.property.name && path.node.callee.property.name in eventMappings ) { const vueEvent = eventMappings[path.node.callee.property.name]; - path.replaceWith( - t.callExpression(t.identifier("emit"), [ - t.stringLiteral(vueEvent), - ...path.node.arguments, - ]), - ); + if (vueEvent) { + path.replaceWith( + t.callExpression(t.identifier("emit"), [ + t.stringLiteral(vueEvent), + ...path.node.arguments, + ]), + ); + } + } else if ( + t.isIdentifier(path.node.callee, { name: "computed" }) && + path.node.arguments.length === 1 && + t.isArrowFunctionExpression(path.node.arguments[0]) + ) { + const functionBody = path.node.arguments[0].body; + if (t.isBlockStatement(functionBody)) { + // Check if this computed property is for event handlers + const isEventHandlers = functionBody.body.some( + (node) => + t.isVariableDeclaration(node) && + node.declarations.some((decl) => + t.isIdentifier(decl.id, { name: "handlers" }), + ), + ); + + if (isEventHandlers) { + const newBody = t.blockStatement([ + t.variableDeclaration("const", [ + t.variableDeclarator( + t.identifier("handlers"), + t.objectExpression([]), + ), + ]), + t.expressionStatement( + t.callExpression( + t.memberExpression( + t.arrayExpression( + Object.keys(eventMappings).map((event) => + t.stringLiteral(event), + ), + ), + t.identifier("forEach"), + ), + [ + t.arrowFunctionExpression( + [t.identifier("eventName")], + t.blockStatement([ + t.ifStatement( + t.binaryExpression( + "in", + t.identifier("eventName"), + t.identifier("props"), + ), + t.blockStatement([ + t.expressionStatement( + t.assignmentExpression( + "=", + t.memberExpression( + t.identifier("handlers"), + t.identifier("eventName"), + true, + ), + t.arrowFunctionExpression( + [t.identifier("event")], + t.callExpression(t.identifier("emit"), [ + t.conditionalExpression( + t.binaryExpression( + "in", + t.identifier("eventName"), + t.identifier("eventMappings"), + ), + t.memberExpression( + t.identifier("eventMappings"), + t.identifier("eventName"), + true, + ), + t.identifier("eventName"), + ), + t.identifier("event"), + ]), + ), + ), + ), + ]), + ), + ]), + ), + ], + ), + ), + t.returnStatement(t.identifier("handlers")), + ]); + + functionBody.body = newBody.body; + } + } } }, }); @@ -208,25 +310,62 @@ function fixVueEventHandlers(codeStr) { const updatedScriptContent = generate(ast).code; - // Update template event bindings for all template sections - const updatedTemplates = templateMatches.map((template) => { - let updatedTemplate = template; - Object.entries(eventMappings).forEach(([mitosisEvent, vueEvent]) => { - const regex = new RegExp(`:(${mitosisEvent})\\s*=\\s*"([^"]*)"`, "g"); - updatedTemplate = updatedTemplate.replace(regex, `@${vueEvent}="$2"`); - }); - return updatedTemplate; + // Update template event bindings + let updatedTemplateContent = templateContent; + Object.entries(eventMappings).forEach(([mitosisEvent, vueEvent]) => { + const regex = new RegExp(`:(${mitosisEvent})\\s*=\\s*"([^"]*)"`, "g"); + updatedTemplateContent = updatedTemplateContent.replace( + regex, + `@${vueEvent}="$2"`, + ); }); - // Reassemble the Vue file - const updatedCode = ` - ${updatedTemplates.join("\n")} - - + // Add eventMappings object to emit manually all events + const eventMappingsObject = ` +const eventMappings = { + onClick: "click", + onDoubleClick: "dblclick", + onMouseDown: "mousedown", + onMouseUp: "mouseup", + onMouseEnter: "mouseenter", + onMouseLeave: "mouseleave", + onMouseMove: "mousemove", + onMouseOver: "mouseover", + onMouseOut: "mouseout", + onKeyDown: "keydown", + onKeyUp: "keyup", + onKeyPress: "keypress", + onFocus: "focus", + onBlur: "blur", + onInput: "input", + onChange: "change", + onSubmit: "submit", + onReset: "reset", + onScroll: "scroll", + onWheel: "wheel", + onDragStart: "dragstart", + onDrag: "drag", + onDragEnd: "dragend", + onDragEnter: "dragenter", + onDragLeave: "dragleave", + onDragOver: "dragover", + onDrop: "drop", + onTouchStart: "touchstart", + onTouchMove: "touchmove", + onTouchEnd: "touchend", + onTouchCancel: "touchcancel", +}; `; + // Reassemble the Vue file, keeping template and script separate + const updatedCode = `${beforeTemplate}${updatedTemplateContent}${afterTemplate.replace( + /]*>[\s\S]*?<\/script>/, + ``, + )}`; + return updatedCode; } diff --git a/compiler/scaffold.config.js b/compiler/scaffold.config.js index 83de2b78..038efb35 100644 --- a/compiler/scaffold.config.js +++ b/compiler/scaffold.config.js @@ -145,43 +145,44 @@ module.exports.scaffoldConfig = { module.exports.compileAllowList = { react: null, vue: [ - // "avatar", - // "avatar-badge", - // "avatar-image", - // "avatar-name", + "avatar", + "avatar-badge", + "avatar-image", + "avatar-name", "box", "theme-provider", - // "tooltip", - // "animate-layout", - // "text", + "text", "button", - // "callout", - // "stack", - // "center", - // "container", - // "divider", - // "fade-in", - // "field-label", - // "icon", + "callout", + "stack", + "center", "icon-button", - // "link", - // "qrcode", - // "reveal", - // "skeleton", - // "spinner", - // "breadcrumb", - // "clipboard-copy-text", - // "connect-modal", - // "connect-modal-head", - // "connect-modal-install-button", - // "connect-modal-qrcode", - // "connect-modal-qrcode-error", - // "connect-modal-qrcode-skeleton", - // "connect-modal-status", - // "connect-modal-wallet-button", - // "connect-modal-wallet-list", + "spinner", + "tooltip", + "animate-layout", + "container", + "divider", + "fade-in", + "field-label", + "icon", + "link", + "qrcode", + "reveal", + "skeleton", + "breadcrumb", + "clipboard-copy-text", + "toast", + "connect-modal", + "connect-modal-head", + "connect-modal-install-button", + "connect-modal-qrcode", + "connect-modal-qrcode-error", + "connect-modal-qrcode-skeleton", + "connect-modal-status", + "connect-modal-wallet-button", + "connect-modal-wallet-list", + // ==== // "interchain-ui-provider", // "basic-modal", - // "toast", ], }; diff --git a/packages/vue/stories/core/Box.stories.ts b/packages/vue/stories/core/Box.stories.ts index b0f9d0ce..a39c368a 100644 --- a/packages/vue/stories/core/Box.stories.ts +++ b/packages/vue/stories/core/Box.stories.ts @@ -33,9 +33,13 @@ export const Default: Story = { render: (args) => ({ components: { Box }, setup() { - return { args }; + const handleClick = () => { + console.log("Box clicked"); + }; + return { args, handleClick }; }, - template: 'This is a Box component', + template: + 'This is a Box component', }), }; diff --git a/src/ui/button/button.lite.tsx b/src/ui/button/button.lite.tsx index 3a045924..c923f300 100644 --- a/src/ui/button/button.lite.tsx +++ b/src/ui/button/button.lite.tsx @@ -49,6 +49,7 @@ export default function Button(props: ButtonProps) { getStoreState: () => any; combinedClassName: string; spreadAttributes: UnknownRecord; + eventHandlers: Record void>; }>({ _overrideManager: null, _theme: "light", @@ -109,6 +110,50 @@ export default function Button(props: ButtonProps) { }, ); }, + get eventHandlers() { + const handlers: Record void> = {}; + const eventProps = [ + "onClick", + "onDoubleClick", + "onMouseDown", + "onMouseUp", + "onMouseEnter", + "onMouseLeave", + "onMouseMove", + "onMouseOver", + "onMouseOut", + "onKeyDown", + "onKeyUp", + "onKeyPress", + "onFocus", + "onBlur", + "onInput", + "onChange", + "onSubmit", + "onReset", + "onScroll", + "onWheel", + "onDragStart", + "onDrag", + "onDragEnd", + "onDragEnter", + "onDragLeave", + "onDragOver", + "onDrop", + "onTouchStart", + "onTouchMove", + "onTouchEnd", + "onTouchCancel", + ]; + + eventProps.forEach((eventName) => { + if (props[eventName]) { + handlers[eventName] = (event: any) => props[eventName](event); + } + }); + + return handlers; + }, }); let cleanupRef = useRef<() => void>(null); @@ -137,8 +182,8 @@ export default function Button(props: ButtonProps) { props.onClick(event)} {...state.spreadAttributes} + {...state.eventHandlers} > void; buttonRef?: any; attributes?: Sprinkles; domAttributes?: any; isLoading?: boolean; spinnerPlacement?: "start" | "end"; + onClick?: (event: any) => void; + onDoubleClick?: (event: any) => void; + onMouseDown?: (event: any) => void; + onMouseUp?: (event: any) => void; + onMouseEnter?: (event: any) => void; + onMouseLeave?: (event: any) => void; + onMouseMove?: (event: any) => void; + onMouseOver?: (event: any) => void; + onMouseOut?: (event: any) => void; + onKeyDown?: (event: any) => void; + onKeyUp?: (event: any) => void; + onKeyPress?: (event: any) => void; + onFocus?: (event: any) => void; + onBlur?: (event: any) => void; + onInput?: (event: any) => void; + onChange?: (event: any) => void; + onSubmit?: (event: any) => void; + onReset?: (event: any) => void; + onScroll?: (event: any) => void; + onWheel?: (event: any) => void; + onDragStart?: (event: any) => void; + onDrag?: (event: any) => void; + onDragEnd?: (event: any) => void; + onDragEnter?: (event: any) => void; + onDragLeave?: (event: any) => void; + onDragOver?: (event: any) => void; + onDrop?: (event: any) => void; + onTouchStart?: (event: any) => void; + onTouchMove?: (event: any) => void; + onTouchEnd?: (event: any) => void; + onTouchCancel?: (event: any) => void; } diff --git a/src/ui/icon-button/icon-button.lite.tsx b/src/ui/icon-button/icon-button.lite.tsx index 482a7744..5f27ef39 100644 --- a/src/ui/icon-button/icon-button.lite.tsx +++ b/src/ui/icon-button/icon-button.lite.tsx @@ -13,10 +13,49 @@ useMetadata({ export default function IconButton(props: IconButtonProps) { const state = useStore({ - handleClick: (e?: any) => { - if (props.onClick) { - props.onClick(e); - } + get eventHandlers() { + const handlers: Record void> = {}; + const eventProps = [ + "onClick", + "onDoubleClick", + "onMouseDown", + "onMouseUp", + "onMouseEnter", + "onMouseLeave", + "onMouseMove", + "onMouseOver", + "onMouseOut", + "onKeyDown", + "onKeyUp", + "onKeyPress", + "onFocus", + "onBlur", + "onInput", + "onChange", + "onSubmit", + "onReset", + "onScroll", + "onWheel", + "onDragStart", + "onDrag", + "onDragEnd", + "onDragEnter", + "onDragLeave", + "onDragOver", + "onDrop", + "onTouchStart", + "onTouchMove", + "onTouchEnd", + "onTouchCancel", + ]; + + eventProps.forEach((eventName) => { + if (props[eventName]) { + handlers[eventName] = (event: any) => props[eventName](event); + } + }); + + return handlers; }, }); @@ -31,7 +70,7 @@ export default function IconButton(props: IconButtonProps) { intent={props.intent} size={props.size} disabled={props.disabled} - onClick={state.handleClick} + {...state.eventHandlers} > diff --git a/src/ui/icon-button/icon-button.types.tsx b/src/ui/icon-button/icon-button.types.tsx index 173f17c1..f228578a 100644 --- a/src/ui/icon-button/icon-button.types.tsx +++ b/src/ui/icon-button/icon-button.types.tsx @@ -3,9 +3,45 @@ import type { IconProps } from "../icon/icon.types"; type OmittedProps = "leftIcon" | "rightIcon"; +// Extract event handler types from ButtonProps +type ButtonEventHandlers = Pick< + ButtonProps, + | "onClick" + | "onDoubleClick" + | "onMouseDown" + | "onMouseUp" + | "onMouseEnter" + | "onMouseLeave" + | "onMouseMove" + | "onMouseOver" + | "onMouseOut" + | "onKeyDown" + | "onKeyUp" + | "onKeyPress" + | "onFocus" + | "onBlur" + | "onInput" + | "onChange" + | "onSubmit" + | "onReset" + | "onScroll" + | "onWheel" + | "onDragStart" + | "onDrag" + | "onDragEnd" + | "onDragEnter" + | "onDragLeave" + | "onDragOver" + | "onDrop" + | "onTouchStart" + | "onTouchMove" + | "onTouchEnd" + | "onTouchCancel" +>; + interface BaseButtonProps extends Omit {} -export interface IconButtonProps extends BaseButtonProps { +export interface IconButtonProps extends BaseButtonProps, ButtonEventHandlers { isRound?: boolean; icon: IconProps["name"]; }