marko
Version:
UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.
151 lines (129 loc) • 4.14 kB
JavaScript
import {
getTagDef,
importDefault,
isNativeTag,
} from "@marko/compiler/babel-utils";
import directives from "./directives";
import modifiers from "./modifiers";
const EMPTY_ARRAY = [];
const EVENT_REG = /^(on(?:ce)?)(-)?(.*)$/;
const attachedDetachedLoaded = new WeakSet();
export default {
enter(attr) {
const {
hub: { file },
} = attr;
const tag = attr.parentPath;
const value = attr.get("value");
const { name, arguments: args } = attr.node;
const isVDOM = file.markoOpts.output !== "html";
if (execModifiersAndDirectives("enter", tag, attr, value)) {
return;
}
// Event handlers.
let [, eventType, isDash, eventName] = EVENT_REG.exec(name) || EMPTY_ARRAY;
if (eventType && args) {
if (!args.length) {
throw attr.buildCodeFrameError("Event handler is missing arguments.");
}
if (!value.isBooleanLiteral(true)) {
throw value.buildCodeFrameError(
`"${name}(handler, ...args)" does not accept a value.`,
);
}
if (!isDash) {
// When the event is not in dash case we normalized differently for html tags and custom tags.
if (isNativeTag(tag)) {
// Lowercase the string
// Example: onMouseOver → mouseover
eventName = eventName.toLowerCase();
} else {
// Convert first character to lower case:
// Example: onBeforeShow → beforeShow
eventName = eventName.charAt(0).toLowerCase() + eventName.slice(1);
}
}
const handlers = (tag.node.handlers = tag.node.handlers || {});
if (handlers[eventName]) {
throw attr.buildCodeFrameError(
"Duplicate event handlers are not supported.",
);
}
handlers[eventName] = {
arguments: args,
once: eventType === "once",
};
if (isVDOM) {
if (eventName === "attach" || eventName === "detach") {
if (!attachedDetachedLoaded.has(file)) {
// Pull in helper for element attach/detach;
attachedDetachedLoaded.add(file);
importDefault(
file,
"marko/src/runtime/components/attach-detach.js",
);
}
}
}
attr.remove();
return;
}
},
exit(attr) {
const tag = attr.parentPath;
const { name, arguments: args } = attr.node;
const value = attr.get("value");
if (execModifiersAndDirectives("exit", tag, attr, value)) {
return;
}
const tagDef = getTagDef(tag);
if (tagDef) {
if (!tagDef.html && !tagDef.getAttribute(name)) {
throw attr.buildCodeFrameError(
`<${
tag.get("name.value").node
}> does not support the "${name}" attribute.`,
);
}
}
if (args && args.length) {
throw attr.buildCodeFrameError(
`Unsupported arguments on the "${name}" attribute.`,
);
}
if (attr.node.bound) {
throw attr.buildCodeFrameError(
`The binding syntax (:=) is only supported when using the "Tags API".`,
);
}
},
};
function execModifiersAndDirectives(type, tag, attr, value) {
const { node } = attr;
const { name, modifier } = node;
if (modifier) {
const modifierTranslate = modifiers[modifier];
if (modifierTranslate) {
if (modifierTranslate[type]) {
const tagNode = tag.node;
const attrNode = attr.node;
modifierTranslate[type](tag, attr, value);
if (tag.node !== tagNode || attr.node !== attrNode) return true;
}
} else if (name === "xlink" && modifier === "href" && isNativeTag(tag)) {
node.name += `:${modifier}`;
node.modifier = undefined;
} else {
throw attr.buildCodeFrameError(`Unsupported modifier "${modifier}".`);
}
}
const directiveTranslate = directives[name];
if (directiveTranslate) {
if (directiveTranslate[type]) {
const tagNode = tag.node;
const attrNode = attr.node;
directiveTranslate[type](tag, attr, value);
if (tag.node !== tagNode || attr.node !== attrNode) return true;
}
}
}