@mintjamsinc/ichigojs 0.1.54 → 0.1.56
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/ichigo.cjs +507 -40
- package/dist/ichigo.cjs.map +1 -1
- package/dist/ichigo.esm.js +506 -41
- package/dist/ichigo.esm.js.map +1 -1
- package/dist/ichigo.esm.min.js +1 -1
- package/dist/ichigo.min.cjs +1 -1
- package/dist/ichigo.umd.js +507 -40
- package/dist/ichigo.umd.js.map +1 -1
- package/dist/ichigo.umd.min.js +1 -1
- package/dist/types/ichigo/VDOM.d.ts +1 -0
- package/dist/types/ichigo/components/IchigoComponentOptions.d.ts +19 -0
- package/dist/types/ichigo/components/IchigoElement.d.ts +43 -0
- package/dist/types/ichigo/components/VComponent.d.ts +1 -0
- package/dist/types/ichigo/components/VComponentRegistry.d.ts +1 -0
- package/dist/types/ichigo/components/defineComponent.d.ts +34 -0
- package/dist/types/ichigo/util/ExpressionUtils.d.ts +10 -2
- package/dist/types/index.d.ts +3 -0
- package/package.json +1 -1
package/dist/ichigo.cjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
8
8
|
/**
|
|
9
9
|
* Represents a reusable component definition.
|
|
10
|
+
* @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
|
|
10
11
|
*/
|
|
11
12
|
class VComponent {
|
|
12
13
|
/**
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
45
46
|
/**
|
|
46
47
|
* A registry for managing component definitions.
|
|
48
|
+
* @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
|
|
47
49
|
*/
|
|
48
50
|
class VComponentRegistry {
|
|
49
51
|
/**
|
|
@@ -6735,11 +6737,18 @@
|
|
|
6735
6737
|
* Extracts variable and function names used in the expression.
|
|
6736
6738
|
* @param expression The expression string to analyze.
|
|
6737
6739
|
* @param functionDependencies A dictionary mapping function names to their dependencies.
|
|
6740
|
+
* @param options Optional parsing options.
|
|
6741
|
+
* - asScript: If true, parse the input as a Script (allows multi-statement source with semicolons,
|
|
6742
|
+
* declarations, control-flow, etc.). If false/omitted, parse as a single expression (the default,
|
|
6743
|
+
* for backward compatibility with interpolation and binding directives).
|
|
6738
6744
|
* @returns An array of identifier names.
|
|
6739
6745
|
*/
|
|
6740
|
-
static extractIdentifiers(expression, functionDependencies) {
|
|
6746
|
+
static extractIdentifiers(expression, functionDependencies, options) {
|
|
6741
6747
|
const identifiers = new Set();
|
|
6742
|
-
|
|
6748
|
+
// In expression mode we wrap in parens so acorn parses the source as a single expression.
|
|
6749
|
+
// In script mode we parse as a Program so that multi-statement bodies (e.g. "a=1; b=2") work.
|
|
6750
|
+
const source = options?.asScript ? expression : `(${expression})`;
|
|
6751
|
+
const ast = parse(source, { ecmaVersion: "latest" });
|
|
6743
6752
|
// Use walk.full instead of walk.simple to visit ALL nodes including assignment LHS
|
|
6744
6753
|
full(ast, (node) => {
|
|
6745
6754
|
if (node.type === 'Identifier') {
|
|
@@ -6971,10 +6980,10 @@
|
|
|
6971
6980
|
* @param identifiers The list of identifiers that are available in bindings.
|
|
6972
6981
|
* @returns The rewritten expression.
|
|
6973
6982
|
*/
|
|
6974
|
-
static rewriteExpression(expression, identifiers) {
|
|
6983
|
+
static rewriteExpression(expression, identifiers, options) {
|
|
6975
6984
|
// Reserved words and built-in objects that should not be prefixed with 'this.'
|
|
6976
6985
|
const reserved = new Set([
|
|
6977
|
-
'event', '$ctx', '$newValue',
|
|
6986
|
+
'event', '$event', '$ctx', '$newValue',
|
|
6978
6987
|
'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
|
|
6979
6988
|
'Math', 'Date', 'String', 'Number', 'Boolean', 'Array', 'Object',
|
|
6980
6989
|
'JSON', 'console', 'window', 'document', 'navigator',
|
|
@@ -6986,7 +6995,7 @@
|
|
|
6986
6995
|
// identifiers that are used (right-hand side), not assigned to (left-hand side)
|
|
6987
6996
|
let allIdentifiersInExpression;
|
|
6988
6997
|
try {
|
|
6989
|
-
allIdentifiersInExpression = ExpressionUtils.extractIdentifiers(expression, {});
|
|
6998
|
+
allIdentifiersInExpression = ExpressionUtils.extractIdentifiers(expression, {}, options);
|
|
6990
6999
|
}
|
|
6991
7000
|
catch (error) {
|
|
6992
7001
|
console.warn('[ichigo.js] Failed to extract identifiers from expression:', expression, error);
|
|
@@ -7008,7 +7017,26 @@
|
|
|
7008
7017
|
try {
|
|
7009
7018
|
// Build a map of positions to replace: { start: number, end: number, name: string }[]
|
|
7010
7019
|
const replacements = [];
|
|
7011
|
-
|
|
7020
|
+
// In script mode we must not wrap in parens (that would make multi-statement input invalid).
|
|
7021
|
+
// Offsets from the parser therefore refer directly to the original expression, so no shift.
|
|
7022
|
+
const asScript = options?.asScript === true;
|
|
7023
|
+
const source = asScript ? expression : `(${expression})`;
|
|
7024
|
+
const offsetShift = asScript ? 0 : 1;
|
|
7025
|
+
const parsedAst = parse(source, { ecmaVersion: 'latest' });
|
|
7026
|
+
// Track identifiers that are locally declared within the handler body (let/const/var, function
|
|
7027
|
+
// params) so we don't rewrite them to `this.xxx`. Only relevant in script mode, where the user
|
|
7028
|
+
// can write declarations; in expression mode there are no declarations to track.
|
|
7029
|
+
const locallyDeclared = new Set();
|
|
7030
|
+
if (asScript) {
|
|
7031
|
+
full(parsedAst, (node) => {
|
|
7032
|
+
if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') {
|
|
7033
|
+
locallyDeclared.add(node.id.name);
|
|
7034
|
+
}
|
|
7035
|
+
else if (node.type === 'FunctionDeclaration' && node.id?.type === 'Identifier') {
|
|
7036
|
+
locallyDeclared.add(node.id.name);
|
|
7037
|
+
}
|
|
7038
|
+
});
|
|
7039
|
+
}
|
|
7012
7040
|
// Collect all identifier nodes that should be replaced
|
|
7013
7041
|
// Use walk.fullAncestor to visit ALL nodes (including assignment LHS) while tracking ancestors
|
|
7014
7042
|
fullAncestor(parsedAst, (node, _state, ancestors) => {
|
|
@@ -7019,6 +7047,10 @@
|
|
|
7019
7047
|
if (!bindingIdentifiers.has(node.name)) {
|
|
7020
7048
|
return;
|
|
7021
7049
|
}
|
|
7050
|
+
// Skip identifiers that were declared locally in the handler body
|
|
7051
|
+
if (locallyDeclared.has(node.name)) {
|
|
7052
|
+
return;
|
|
7053
|
+
}
|
|
7022
7054
|
// Check if this identifier is a property of a MemberExpression
|
|
7023
7055
|
// (e.g., in 'obj.prop', we should skip 'prop')
|
|
7024
7056
|
if (ancestors.length >= 1) {
|
|
@@ -7030,10 +7062,10 @@
|
|
|
7030
7062
|
}
|
|
7031
7063
|
}
|
|
7032
7064
|
}
|
|
7033
|
-
// Add to replacements list (adjust for the wrapping parentheses)
|
|
7065
|
+
// Add to replacements list (adjust for the wrapping parentheses in expression mode)
|
|
7034
7066
|
replacements.push({
|
|
7035
|
-
start: node.start -
|
|
7036
|
-
end: node.end -
|
|
7067
|
+
start: node.start - offsetShift,
|
|
7068
|
+
end: node.end - offsetShift,
|
|
7037
7069
|
name: node.name
|
|
7038
7070
|
});
|
|
7039
7071
|
});
|
|
@@ -7474,6 +7506,14 @@
|
|
|
7474
7506
|
else if (attributeName === 'style') {
|
|
7475
7507
|
this.#updateStyle(element, value);
|
|
7476
7508
|
}
|
|
7509
|
+
else if (this.#isCustomElementProperty(element, attributeName, value)) {
|
|
7510
|
+
// Custom elements: set objects/arrays (and declared props) as properties
|
|
7511
|
+
// so complex values are not serialized to strings via setAttribute.
|
|
7512
|
+
// HTML attributes are lowercased by the browser, so resolve to the
|
|
7513
|
+
// actual camelCase property name declared on the custom element.
|
|
7514
|
+
const propName = this.#resolveCustomElementPropertyName(element, attributeName);
|
|
7515
|
+
element[propName] = value;
|
|
7516
|
+
}
|
|
7477
7517
|
else if (this.#isDOMProperty(attributeName)) {
|
|
7478
7518
|
this.#updateProperty(element, attributeName, value);
|
|
7479
7519
|
}
|
|
@@ -7557,6 +7597,46 @@
|
|
|
7557
7597
|
#camelToKebab(str) {
|
|
7558
7598
|
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
7559
7599
|
}
|
|
7600
|
+
/**
|
|
7601
|
+
* Returns true when the target is a custom element and the binding should be
|
|
7602
|
+
* delivered as a property rather than an HTML attribute.
|
|
7603
|
+
*
|
|
7604
|
+
* Two conditions trigger property delivery:
|
|
7605
|
+
* 1. The value is an object or array — serialising these to a string attribute
|
|
7606
|
+
* would lose type information.
|
|
7607
|
+
* 2. The element exposes a matching property accessor (e.g. a prop declared via
|
|
7608
|
+
* defineComponent), even when the value is a primitive.
|
|
7609
|
+
*/
|
|
7610
|
+
#isCustomElementProperty(element, name, value) {
|
|
7611
|
+
if (!element.tagName.includes('-')) {
|
|
7612
|
+
return false;
|
|
7613
|
+
}
|
|
7614
|
+
const isObjectOrArray = Array.isArray(value) || (typeof value === 'object' && value !== null);
|
|
7615
|
+
return isObjectOrArray || name in element;
|
|
7616
|
+
}
|
|
7617
|
+
/**
|
|
7618
|
+
* Resolves the actual property name on a custom element for a given
|
|
7619
|
+
* (lowercased) HTML attribute name. HTML attributes are always lowercase,
|
|
7620
|
+
* but custom element props are typically camelCase. This method checks the
|
|
7621
|
+
* element's declared _props (set by defineComponent) for a case-insensitive
|
|
7622
|
+
* match, falling back to scanning the prototype's own property descriptors.
|
|
7623
|
+
*/
|
|
7624
|
+
#resolveCustomElementPropertyName(element, name) {
|
|
7625
|
+
// Fast path: exact match already exists
|
|
7626
|
+
if (name in element) {
|
|
7627
|
+
return name;
|
|
7628
|
+
}
|
|
7629
|
+
// Check declared _props from defineComponent / IchigoElement
|
|
7630
|
+
const props = element.constructor._props;
|
|
7631
|
+
if (Array.isArray(props)) {
|
|
7632
|
+
const lowerName = name.toLowerCase();
|
|
7633
|
+
const match = props.find(p => p.toLowerCase() === lowerName);
|
|
7634
|
+
if (match) {
|
|
7635
|
+
return match;
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7638
|
+
return name;
|
|
7639
|
+
}
|
|
7560
7640
|
/**
|
|
7561
7641
|
* Checks if the attribute should be set as a DOM property.
|
|
7562
7642
|
*/
|
|
@@ -8937,6 +9017,18 @@
|
|
|
8937
9017
|
});
|
|
8938
9018
|
}
|
|
8939
9019
|
}
|
|
9020
|
+
else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
9021
|
+
// If the node is a DocumentFragment, create child VNodes for its children
|
|
9022
|
+
// DocumentFragment is not an Element so it has no directives itself.
|
|
9023
|
+
this.#childVNodes = [];
|
|
9024
|
+
for (const childNode of Array.from(this.#node.childNodes)) {
|
|
9025
|
+
new VNode({
|
|
9026
|
+
node: childNode,
|
|
9027
|
+
vApplication: this.#vApplication,
|
|
9028
|
+
parentVNode: this
|
|
9029
|
+
});
|
|
9030
|
+
}
|
|
9031
|
+
}
|
|
8940
9032
|
// Register this node as a dependent of the parent node, if any
|
|
8941
9033
|
this.#closers = this.#parentVNode?.addDependent(this);
|
|
8942
9034
|
}
|
|
@@ -9191,6 +9283,16 @@
|
|
|
9191
9283
|
});
|
|
9192
9284
|
}
|
|
9193
9285
|
}
|
|
9286
|
+
else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
9287
|
+
// For document fragments (e.g. from <template v-if>), propagate updates
|
|
9288
|
+
// to dependent virtual nodes so that child bindings stay reactive.
|
|
9289
|
+
this.#dependents?.forEach(dependentNode => {
|
|
9290
|
+
const changed = dependentNode.dependentIdentifiers.some(id => changes.some(change => dependentNode.bindings.doesChangeMatchIdentifier(change, id)));
|
|
9291
|
+
if (changed) {
|
|
9292
|
+
dependentNode.update();
|
|
9293
|
+
}
|
|
9294
|
+
});
|
|
9295
|
+
}
|
|
9194
9296
|
}
|
|
9195
9297
|
/**
|
|
9196
9298
|
* Forces an update of the virtual node and its children, regardless of changed identifiers.
|
|
@@ -9240,6 +9342,12 @@
|
|
|
9240
9342
|
});
|
|
9241
9343
|
}
|
|
9242
9344
|
}
|
|
9345
|
+
else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
9346
|
+
// For document fragments, recursively force update child VNodes
|
|
9347
|
+
this.#childVNodes?.forEach(childVNode => {
|
|
9348
|
+
childVNode.forceUpdate();
|
|
9349
|
+
});
|
|
9350
|
+
}
|
|
9243
9351
|
}
|
|
9244
9352
|
/**
|
|
9245
9353
|
* Adds a child virtual node to this virtual node.
|
|
@@ -9617,9 +9725,11 @@
|
|
|
9617
9725
|
}
|
|
9618
9726
|
// Clone the original node and create a new VNode for it
|
|
9619
9727
|
const clone = this.#cloneNode();
|
|
9620
|
-
//
|
|
9621
|
-
|
|
9622
|
-
//
|
|
9728
|
+
// Create a new VNode for the cloned element BEFORE inserting into DOM.
|
|
9729
|
+
// This prevents custom elements (Web Components) from having their
|
|
9730
|
+
// connectedCallback fire before VNode processes their children, which
|
|
9731
|
+
// would cause the parent VApplication to incorrectly adopt the custom
|
|
9732
|
+
// element's internal template content as its own VNode tree.
|
|
9623
9733
|
// Pass the current bindings to ensure loop variables from v-for are available
|
|
9624
9734
|
const vNode = new VNode({
|
|
9625
9735
|
node: clone,
|
|
@@ -9627,6 +9737,29 @@
|
|
|
9627
9737
|
parentVNode: this.#vNode,
|
|
9628
9738
|
bindings: this.#vNode.bindings
|
|
9629
9739
|
});
|
|
9740
|
+
// Insert after the anchor node AFTER VNode creation
|
|
9741
|
+
const anchorParent = this.#vNode.anchorNode?.parentNode;
|
|
9742
|
+
const nextSibling = this.#vNode.anchorNode?.nextSibling ?? null;
|
|
9743
|
+
if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE && anchorParent) {
|
|
9744
|
+
const startMarker = document.createComment('#vif-fragment-start');
|
|
9745
|
+
const endMarker = document.createComment('#vif-fragment-end');
|
|
9746
|
+
if (nextSibling) {
|
|
9747
|
+
anchorParent.insertBefore(startMarker, nextSibling);
|
|
9748
|
+
}
|
|
9749
|
+
else {
|
|
9750
|
+
anchorParent.appendChild(startMarker);
|
|
9751
|
+
}
|
|
9752
|
+
anchorParent.insertBefore(endMarker, startMarker.nextSibling);
|
|
9753
|
+
anchorParent.insertBefore(clone, endMarker);
|
|
9754
|
+
// Store markers for later removal
|
|
9755
|
+
vNode.userData.set('vif_fragment_start', startMarker);
|
|
9756
|
+
vNode.userData.set('vif_fragment_end', endMarker);
|
|
9757
|
+
this.#renderedVNode = vNode;
|
|
9758
|
+
this.#renderedVNode.forceUpdate();
|
|
9759
|
+
return;
|
|
9760
|
+
}
|
|
9761
|
+
const nodeToInsert = vNode.anchorNode || clone;
|
|
9762
|
+
anchorParent?.insertBefore(nodeToInsert, nextSibling);
|
|
9630
9763
|
this.#renderedVNode = vNode;
|
|
9631
9764
|
this.#renderedVNode.forceUpdate();
|
|
9632
9765
|
}
|
|
@@ -9641,17 +9774,40 @@
|
|
|
9641
9774
|
}
|
|
9642
9775
|
// Destroy VNode first (calls @unmount hooks while DOM is still accessible)
|
|
9643
9776
|
this.#renderedVNode.destroy();
|
|
9644
|
-
// Then remove from DOM
|
|
9645
|
-
this.#renderedVNode.
|
|
9777
|
+
// Then remove from DOM. Handle fragment markers if present
|
|
9778
|
+
const startMarker = this.#renderedVNode.userData.get?.('vif_fragment_start');
|
|
9779
|
+
const endMarker = this.#renderedVNode.userData.get?.('vif_fragment_end');
|
|
9780
|
+
if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
|
|
9781
|
+
const parentNode = startMarker.parentNode;
|
|
9782
|
+
let node = startMarker;
|
|
9783
|
+
while (node) {
|
|
9784
|
+
const next = node.nextSibling;
|
|
9785
|
+
parentNode.removeChild(node);
|
|
9786
|
+
if (node === endMarker)
|
|
9787
|
+
break;
|
|
9788
|
+
node = next;
|
|
9789
|
+
}
|
|
9790
|
+
this.#renderedVNode = undefined;
|
|
9791
|
+
return;
|
|
9792
|
+
}
|
|
9793
|
+
const parent = this.#renderedVNode.node.parentNode;
|
|
9794
|
+
if (parent) {
|
|
9795
|
+
parent.removeChild(this.#renderedVNode.node);
|
|
9796
|
+
}
|
|
9646
9797
|
this.#renderedVNode = undefined;
|
|
9647
9798
|
}
|
|
9648
9799
|
/**
|
|
9649
9800
|
* Clones the original node of the directive's virtual node.
|
|
9650
|
-
*
|
|
9651
|
-
*
|
|
9801
|
+
* When the source element is a <template>, returns a DocumentFragment
|
|
9802
|
+
* cloned from the template's content.
|
|
9803
|
+
* @returns The cloned Node (HTMLElement or DocumentFragment).
|
|
9652
9804
|
*/
|
|
9653
9805
|
#cloneNode() {
|
|
9654
9806
|
const element = this.#vNode.node;
|
|
9807
|
+
if (element instanceof HTMLTemplateElement) {
|
|
9808
|
+
// Return a DocumentFragment cloned from the template content
|
|
9809
|
+
return element.content.cloneNode(true);
|
|
9810
|
+
}
|
|
9655
9811
|
return element.cloneNode(true);
|
|
9656
9812
|
}
|
|
9657
9813
|
/**
|
|
@@ -9969,10 +10125,28 @@
|
|
|
9969
10125
|
vNode.destroy();
|
|
9970
10126
|
}
|
|
9971
10127
|
}
|
|
9972
|
-
// Then remove from DOM
|
|
10128
|
+
// Then remove from DOM. Handle both Element nodes and fragment-marked ranges.
|
|
9973
10129
|
for (const vNode of nodesToRemove) {
|
|
9974
|
-
|
|
9975
|
-
|
|
10130
|
+
// If this VNode stored fragment markers, remove the range between them
|
|
10131
|
+
const startMarker = vNode.userData.get?.('vfor_fragment_start');
|
|
10132
|
+
const endMarker = vNode.userData.get?.('vfor_fragment_end');
|
|
10133
|
+
if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
|
|
10134
|
+
const parentNode = startMarker.parentNode;
|
|
10135
|
+
let node = startMarker;
|
|
10136
|
+
// Remove nodes from startMarker up to and including endMarker
|
|
10137
|
+
while (node) {
|
|
10138
|
+
const next = node.nextSibling;
|
|
10139
|
+
parentNode.removeChild(node);
|
|
10140
|
+
if (node === endMarker)
|
|
10141
|
+
break;
|
|
10142
|
+
node = next;
|
|
10143
|
+
}
|
|
10144
|
+
continue;
|
|
10145
|
+
}
|
|
10146
|
+
// Fallback: remove the node itself if it's attached
|
|
10147
|
+
const parentOfNode = vNode.node.parentNode;
|
|
10148
|
+
if (parentOfNode) {
|
|
10149
|
+
parentOfNode.removeChild(vNode.node);
|
|
9976
10150
|
}
|
|
9977
10151
|
}
|
|
9978
10152
|
// Add or reorder items
|
|
@@ -10006,6 +10180,28 @@
|
|
|
10006
10180
|
bindings,
|
|
10007
10181
|
dependentIdentifiers: depIds,
|
|
10008
10182
|
});
|
|
10183
|
+
// If clone is a DocumentFragment, insert it between start/end comment markers
|
|
10184
|
+
if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
10185
|
+
const startMarker = document.createComment('#vfor-fragment-start');
|
|
10186
|
+
const endMarker = document.createComment('#vfor-fragment-end');
|
|
10187
|
+
// Insert start and end markers and the fragment's children between them
|
|
10188
|
+
if (prevNode.nextSibling) {
|
|
10189
|
+
parent.insertBefore(startMarker, prevNode.nextSibling);
|
|
10190
|
+
}
|
|
10191
|
+
else {
|
|
10192
|
+
parent.appendChild(startMarker);
|
|
10193
|
+
}
|
|
10194
|
+
parent.insertBefore(endMarker, startMarker.nextSibling);
|
|
10195
|
+
parent.insertBefore(clone, endMarker);
|
|
10196
|
+
// Store markers on the VNode for later removal/movement
|
|
10197
|
+
vNode.userData.set('vfor_fragment_start', startMarker);
|
|
10198
|
+
vNode.userData.set('vfor_fragment_end', endMarker);
|
|
10199
|
+
newRenderedItems.set(key, vNode);
|
|
10200
|
+
vNode.forceUpdate();
|
|
10201
|
+
// Use endMarker as prevNode for subsequent insertions
|
|
10202
|
+
prevNode = endMarker;
|
|
10203
|
+
continue;
|
|
10204
|
+
}
|
|
10009
10205
|
// Determine what to insert: anchor node (if exists) or the clone itself
|
|
10010
10206
|
const nodeToInsert = vNode.anchorNode || clone;
|
|
10011
10207
|
// Insert after previous node
|
|
@@ -10023,8 +10219,9 @@
|
|
|
10023
10219
|
newRenderedItems.set(key, vNode);
|
|
10024
10220
|
// Update bindings
|
|
10025
10221
|
this.#updateItemBindings(vNode, context);
|
|
10026
|
-
// Determine the actual node in DOM: anchor node
|
|
10027
|
-
const
|
|
10222
|
+
// Determine the actual node in DOM: prefer fragment end marker, then anchor node, then vNode.node
|
|
10223
|
+
const fragmentEnd = vNode.userData.get?.('vfor_fragment_end');
|
|
10224
|
+
const actualNode = fragmentEnd || vNode.anchorNode || vNode.node;
|
|
10028
10225
|
// Move to correct position if needed
|
|
10029
10226
|
if (prevNode.nextSibling !== actualNode) {
|
|
10030
10227
|
if (prevNode.nextSibling) {
|
|
@@ -10035,8 +10232,9 @@
|
|
|
10035
10232
|
}
|
|
10036
10233
|
}
|
|
10037
10234
|
}
|
|
10038
|
-
// Use
|
|
10039
|
-
|
|
10235
|
+
// Use fragment end marker > anchor node > vNode.node as prevNode
|
|
10236
|
+
const fragmentEndForPrev = vNode.userData.get?.('vfor_fragment_end');
|
|
10237
|
+
prevNode = fragmentEndForPrev || vNode.anchorNode || vNode.node;
|
|
10040
10238
|
}
|
|
10041
10239
|
// Update rendered items map
|
|
10042
10240
|
this.#renderedItems = newRenderedItems;
|
|
@@ -10215,12 +10413,17 @@
|
|
|
10215
10413
|
}
|
|
10216
10414
|
/**
|
|
10217
10415
|
* Clones the original node of the directive's virtual node.
|
|
10218
|
-
*
|
|
10219
|
-
*
|
|
10416
|
+
* When the source element is a <template>, its children (stored in .content)
|
|
10417
|
+
* are cloned into a DocumentFragment so that multiple root nodes can be
|
|
10418
|
+
* managed without adding an extra wrapper element.
|
|
10419
|
+
* @returns The cloned Node (Element or DocumentFragment).
|
|
10220
10420
|
*/
|
|
10221
10421
|
#cloneNode() {
|
|
10222
|
-
// Clone the original element
|
|
10223
10422
|
const element = this.#vNode.node;
|
|
10423
|
+
if (element instanceof HTMLTemplateElement) {
|
|
10424
|
+
// Return a cloned DocumentFragment containing the template content
|
|
10425
|
+
return element.content.cloneNode(true);
|
|
10426
|
+
}
|
|
10224
10427
|
return element.cloneNode(true);
|
|
10225
10428
|
}
|
|
10226
10429
|
/**
|
|
@@ -11061,10 +11264,12 @@
|
|
|
11061
11264
|
this.#eventName = parts[0];
|
|
11062
11265
|
parts.slice(1).forEach(mod => this.#modifiers.add(mod));
|
|
11063
11266
|
}
|
|
11064
|
-
// Parse the expression to extract identifiers and create the handler wrapper
|
|
11267
|
+
// Parse the expression to extract identifiers and create the handler wrapper.
|
|
11268
|
+
// Event handlers are parsed in script mode so that users can write multi-statement bodies
|
|
11269
|
+
// (e.g. "a=1; b=2"), declarations, and control-flow constructs — matching Vue semantics.
|
|
11065
11270
|
const expression = context.attribute.value;
|
|
11066
11271
|
if (expression) {
|
|
11067
|
-
this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
|
|
11272
|
+
this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies, { asScript: true });
|
|
11068
11273
|
}
|
|
11069
11274
|
// Check if this is a lifecycle hook or a regular event
|
|
11070
11275
|
if (this.#eventName && this.#isLifecycleHook(this.#eventName)) {
|
|
@@ -11278,10 +11483,11 @@
|
|
|
11278
11483
|
// This allows the method to access the DOM element, VNode, and userData
|
|
11279
11484
|
return originalMethod($ctx);
|
|
11280
11485
|
}
|
|
11281
|
-
// For inline
|
|
11282
|
-
// This allows assignments like "currentTab = 'shop'" to work correctly
|
|
11283
|
-
|
|
11284
|
-
const
|
|
11486
|
+
// For inline bodies, rewrite to use 'this' context
|
|
11487
|
+
// This allows assignments like "currentTab = 'shop'" to work correctly.
|
|
11488
|
+
// Script mode allows multi-statement bodies (e.g. "a=1; init()") and control-flow.
|
|
11489
|
+
const rewrittenExpr = this.#rewriteExpression(expression, identifiers, { asScript: true });
|
|
11490
|
+
const funcBody = rewrittenExpr;
|
|
11285
11491
|
const func = new Function('$ctx', funcBody);
|
|
11286
11492
|
return func.call(bindings?.raw, $ctx);
|
|
11287
11493
|
};
|
|
@@ -11311,12 +11517,15 @@
|
|
|
11311
11517
|
// Pass event as first argument and $ctx as second argument
|
|
11312
11518
|
return originalMethod(event, $ctx);
|
|
11313
11519
|
}
|
|
11314
|
-
// For inline
|
|
11315
|
-
// This allows assignments like "currentTab = 'shop'" to work correctly
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
const
|
|
11319
|
-
|
|
11520
|
+
// For inline bodies, rewrite to use 'this' context
|
|
11521
|
+
// This allows assignments like "currentTab = 'shop'" to work correctly.
|
|
11522
|
+
// Script mode allows multi-statement bodies (e.g. "a=1; b=2") and control-flow,
|
|
11523
|
+
// so we emit the rewritten source directly as the function body (no `return (...)`).
|
|
11524
|
+
const rewrittenExpr = this.#rewriteExpression(expression, identifiers, { asScript: true });
|
|
11525
|
+
const funcBody = rewrittenExpr;
|
|
11526
|
+
// '$event' is an alias for 'event' for Vue compatibility
|
|
11527
|
+
const func = new Function('event', '$event', '$ctx', funcBody);
|
|
11528
|
+
return func.call(bindings?.raw, event, event, $ctx);
|
|
11320
11529
|
};
|
|
11321
11530
|
}
|
|
11322
11531
|
/**
|
|
@@ -11327,8 +11536,8 @@
|
|
|
11327
11536
|
* @param identifiers The list of identifiers that are available in bindings.
|
|
11328
11537
|
* @returns The rewritten expression.
|
|
11329
11538
|
*/
|
|
11330
|
-
#rewriteExpression(expression, identifiers) {
|
|
11331
|
-
return ExpressionUtils.rewriteExpression(expression, identifiers);
|
|
11539
|
+
#rewriteExpression(expression, identifiers, options) {
|
|
11540
|
+
return ExpressionUtils.rewriteExpression(expression, identifiers, options);
|
|
11332
11541
|
}
|
|
11333
11542
|
}
|
|
11334
11543
|
|
|
@@ -13026,6 +13235,7 @@
|
|
|
13026
13235
|
/**
|
|
13027
13236
|
* Gets the component registry.
|
|
13028
13237
|
* @return {VComponentRegistry} The component registry.
|
|
13238
|
+
* @deprecated This method is deprecated and will be removed in a future release. Please use the new component registration system instead.
|
|
13029
13239
|
*/
|
|
13030
13240
|
static get componentRegistry() {
|
|
13031
13241
|
return this.#componentRegistry;
|
|
@@ -13069,11 +13279,268 @@
|
|
|
13069
13279
|
}
|
|
13070
13280
|
}
|
|
13071
13281
|
|
|
13282
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
13283
|
+
/**
|
|
13284
|
+
* Base class for ichigo.js-backed Web Components (Light DOM, no Shadow DOM).
|
|
13285
|
+
*
|
|
13286
|
+
* Mount timing:
|
|
13287
|
+
* - If the component declares no props, the VApplication is mounted synchronously
|
|
13288
|
+
* at the end of connectedCallback (the template is known, no props to wait for).
|
|
13289
|
+
* - If the component declares props, the VApplication is mounted the first time
|
|
13290
|
+
* _setProp() is called after connectedCallback has prepared the DOM. This
|
|
13291
|
+
* guarantees that the parent framework (e.g. ichigo.js VBindDirective) has
|
|
13292
|
+
* already delivered at least one prop value before data() is evaluated.
|
|
13293
|
+
*
|
|
13294
|
+
* Subclasses must set the static fields _template, _props and _buildOptions before
|
|
13295
|
+
* calling customElements.define(). defineComponent() handles this automatically.
|
|
13296
|
+
*/
|
|
13297
|
+
class IchigoElement extends HTMLElement {
|
|
13298
|
+
/**
|
|
13299
|
+
* The mounted VApplication instance, present only while connected to the DOM.
|
|
13300
|
+
*/
|
|
13301
|
+
#app;
|
|
13302
|
+
/**
|
|
13303
|
+
* Stores prop values received at any time (before or after mount).
|
|
13304
|
+
*/
|
|
13305
|
+
#propValues = {};
|
|
13306
|
+
/**
|
|
13307
|
+
* The root element cloned from the template, ready for mounting.
|
|
13308
|
+
* Set by connectedCallback; cleared on disconnect.
|
|
13309
|
+
*/
|
|
13310
|
+
#mountRoot;
|
|
13311
|
+
/**
|
|
13312
|
+
* Whether a mount microtask is already queued to avoid double-mounting.
|
|
13313
|
+
*/
|
|
13314
|
+
#mountScheduled = false;
|
|
13315
|
+
connectedCallback() {
|
|
13316
|
+
// --- 0. Guard: skip if template not yet loaded ---
|
|
13317
|
+
// customElements.define() may run before loadComponent() completes (e.g. when
|
|
13318
|
+
// dynamic imports are inlined by the bundler). The static placeholder element
|
|
13319
|
+
// in the HTML will be removed and replaced by v-for once the template is ready,
|
|
13320
|
+
// so it is safe to do nothing here.
|
|
13321
|
+
const ctor = this.constructor;
|
|
13322
|
+
const templateEl = document.querySelector(ctor._template);
|
|
13323
|
+
if (!templateEl || !(templateEl instanceof HTMLTemplateElement)) {
|
|
13324
|
+
return;
|
|
13325
|
+
}
|
|
13326
|
+
// --- 1. Capture slot content before clearing children ---
|
|
13327
|
+
const defaultSlotNodes = [];
|
|
13328
|
+
const namedSlotNodes = new Map();
|
|
13329
|
+
for (const child of Array.from(this.childNodes)) {
|
|
13330
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
13331
|
+
const el = child;
|
|
13332
|
+
const slotName = el.getAttribute('slot');
|
|
13333
|
+
if (slotName) {
|
|
13334
|
+
if (!namedSlotNodes.has(slotName)) {
|
|
13335
|
+
namedSlotNodes.set(slotName, []);
|
|
13336
|
+
}
|
|
13337
|
+
namedSlotNodes.get(slotName).push(el);
|
|
13338
|
+
// Remove slot attribute so ichigo.js doesn't try to bind it
|
|
13339
|
+
el.removeAttribute('slot');
|
|
13340
|
+
}
|
|
13341
|
+
else {
|
|
13342
|
+
defaultSlotNodes.push(el);
|
|
13343
|
+
}
|
|
13344
|
+
}
|
|
13345
|
+
else if (child.nodeType === Node.TEXT_NODE) {
|
|
13346
|
+
if ((child.textContent ?? '').trim()) {
|
|
13347
|
+
defaultSlotNodes.push(child);
|
|
13348
|
+
}
|
|
13349
|
+
}
|
|
13350
|
+
}
|
|
13351
|
+
// Clear host element so we can append the cloned template
|
|
13352
|
+
while (this.firstChild) {
|
|
13353
|
+
this.removeChild(this.firstChild);
|
|
13354
|
+
}
|
|
13355
|
+
// --- 2. Clone the component template ---
|
|
13356
|
+
const fragment = templateEl.content.cloneNode(true);
|
|
13357
|
+
const root = this.#findRootElement(fragment);
|
|
13358
|
+
// --- 3. Distribute named slot content ---
|
|
13359
|
+
for (const [name, nodes] of namedSlotNodes) {
|
|
13360
|
+
const slot = root.querySelector(`slot[name="${name}"]`);
|
|
13361
|
+
if (slot) {
|
|
13362
|
+
slot.replaceWith(...nodes);
|
|
13363
|
+
}
|
|
13364
|
+
}
|
|
13365
|
+
// --- 4. Distribute default slot content ---
|
|
13366
|
+
const defaultSlot = root.querySelector('slot:not([name])');
|
|
13367
|
+
if (defaultSlot && defaultSlotNodes.length > 0) {
|
|
13368
|
+
defaultSlot.replaceWith(...defaultSlotNodes);
|
|
13369
|
+
}
|
|
13370
|
+
// Attach the populated template to the host element
|
|
13371
|
+
this.appendChild(root);
|
|
13372
|
+
this.#mountRoot = root;
|
|
13373
|
+
// If props were set before connectedCallback, ensure mount is scheduled
|
|
13374
|
+
this.#scheduleMountIfNeeded();
|
|
13375
|
+
// --- 5. Mount strategy ---
|
|
13376
|
+
// If this component has no declared props, mount immediately.
|
|
13377
|
+
// If it has props, mount will be triggered from _setProp() once the parent
|
|
13378
|
+
// delivers the first prop value (via VBindDirective / forceUpdate).
|
|
13379
|
+
if (ctor._props.length === 0) {
|
|
13380
|
+
this.#doMount();
|
|
13381
|
+
}
|
|
13382
|
+
// else: wait for _setProp() to trigger #scheduleMountIfNeeded()
|
|
13383
|
+
}
|
|
13384
|
+
disconnectedCallback() {
|
|
13385
|
+
this.#mountRoot = undefined;
|
|
13386
|
+
this.#mountScheduled = false;
|
|
13387
|
+
if (this.#app) {
|
|
13388
|
+
this.#app.unmount();
|
|
13389
|
+
this.#app = undefined;
|
|
13390
|
+
}
|
|
13391
|
+
}
|
|
13392
|
+
/**
|
|
13393
|
+
* Called by the property setters generated by defineComponent().
|
|
13394
|
+
* Before mount: stores the value and schedules a mount microtask.
|
|
13395
|
+
* After mount: pushes the value directly into the reactive bindings.
|
|
13396
|
+
*/
|
|
13397
|
+
_setProp(name, value) {
|
|
13398
|
+
this.#propValues[name] = value;
|
|
13399
|
+
if (this.#app) {
|
|
13400
|
+
this.#app.bindings?.set(name, value);
|
|
13401
|
+
}
|
|
13402
|
+
else {
|
|
13403
|
+
this.#scheduleMountIfNeeded();
|
|
13404
|
+
}
|
|
13405
|
+
}
|
|
13406
|
+
/**
|
|
13407
|
+
* Called by the property getters generated by defineComponent().
|
|
13408
|
+
*/
|
|
13409
|
+
_getProp(name) {
|
|
13410
|
+
return this.#propValues[name];
|
|
13411
|
+
}
|
|
13412
|
+
// --- Static fields set by defineComponent() ---
|
|
13413
|
+
/**
|
|
13414
|
+
* CSS selector for the component's <template> element (e.g. '#my-card').
|
|
13415
|
+
*/
|
|
13416
|
+
static _template;
|
|
13417
|
+
/**
|
|
13418
|
+
* List of declared prop names. Used to decide whether to defer mounting.
|
|
13419
|
+
*/
|
|
13420
|
+
static _props = [];
|
|
13421
|
+
/**
|
|
13422
|
+
* Factory that builds VApplicationOptions from the current prop values.
|
|
13423
|
+
* Implemented by defineComponent() as a closure that captures the user's options.
|
|
13424
|
+
*/
|
|
13425
|
+
static _buildOptions;
|
|
13426
|
+
// --- Private helpers ---
|
|
13427
|
+
/**
|
|
13428
|
+
* Schedules a mount microtask if the DOM root is ready and no mount is pending.
|
|
13429
|
+
* Called from _setProp() so the mount happens after the prop value is stored.
|
|
13430
|
+
*/
|
|
13431
|
+
#scheduleMountIfNeeded() {
|
|
13432
|
+
if (this.#mountScheduled || this.#app || !this.#mountRoot) {
|
|
13433
|
+
return;
|
|
13434
|
+
}
|
|
13435
|
+
this.#mountScheduled = true;
|
|
13436
|
+
queueMicrotask(() => {
|
|
13437
|
+
this.#mountScheduled = false;
|
|
13438
|
+
this.#doMount();
|
|
13439
|
+
});
|
|
13440
|
+
}
|
|
13441
|
+
#doMount() {
|
|
13442
|
+
if (this.#app || !this.#mountRoot) {
|
|
13443
|
+
return;
|
|
13444
|
+
}
|
|
13445
|
+
const ctor = this.constructor;
|
|
13446
|
+
const options = ctor._buildOptions(this.#propValues);
|
|
13447
|
+
this.#app = VDOM.createApp(options);
|
|
13448
|
+
this.#app.mount(this.#mountRoot);
|
|
13449
|
+
}
|
|
13450
|
+
#findRootElement(fragment) {
|
|
13451
|
+
for (const node of Array.from(fragment.childNodes)) {
|
|
13452
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
13453
|
+
return node;
|
|
13454
|
+
}
|
|
13455
|
+
}
|
|
13456
|
+
throw new Error(`IchigoElement: no root element found in template '${this.constructor._template}'`);
|
|
13457
|
+
}
|
|
13458
|
+
}
|
|
13459
|
+
|
|
13460
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
13461
|
+
/**
|
|
13462
|
+
* Defines and registers a custom element backed by ichigo.js reactivity.
|
|
13463
|
+
*
|
|
13464
|
+
* Usage:
|
|
13465
|
+
* ```html
|
|
13466
|
+
* <template id="my-list">
|
|
13467
|
+
* <div>
|
|
13468
|
+
* <ul v-if="items.length > 0">
|
|
13469
|
+
* <li v-for="item of items">{{item.name}}</li>
|
|
13470
|
+
* </ul>
|
|
13471
|
+
* <slot></slot>
|
|
13472
|
+
* </div>
|
|
13473
|
+
* </template>
|
|
13474
|
+
* ```
|
|
13475
|
+
* ```typescript
|
|
13476
|
+
* defineComponent('my-list', {
|
|
13477
|
+
* template: '#my-list',
|
|
13478
|
+
* props: ['items'],
|
|
13479
|
+
* data() {
|
|
13480
|
+
* return { items: this.items ?? [] };
|
|
13481
|
+
* },
|
|
13482
|
+
* });
|
|
13483
|
+
* ```
|
|
13484
|
+
* ```html
|
|
13485
|
+
* <my-list :items="searchResults">
|
|
13486
|
+
* <span slot="empty">No results.</span>
|
|
13487
|
+
* </my-list>
|
|
13488
|
+
* ```
|
|
13489
|
+
*
|
|
13490
|
+
* @param tagName Custom element tag name (must contain a hyphen, e.g. 'my-card').
|
|
13491
|
+
* @param options Component options including template selector and optional props.
|
|
13492
|
+
*/
|
|
13493
|
+
function defineComponent(tagName, options) {
|
|
13494
|
+
const { props = [], template, data, computed, methods, watch, logLevel } = options;
|
|
13495
|
+
// Build a subclass of IchigoElement specific to this component
|
|
13496
|
+
class ComponentElement extends IchigoElement {
|
|
13497
|
+
static _template = template;
|
|
13498
|
+
static _props = props;
|
|
13499
|
+
static _buildOptions(propValues) {
|
|
13500
|
+
return {
|
|
13501
|
+
data() {
|
|
13502
|
+
// 'this' is the $ctx object provided by VApplication ({ $markRaw }).
|
|
13503
|
+
// We extend it with prop values so the user's data() can reference them
|
|
13504
|
+
// via 'this.propName' and supply defaults (e.g. `this.items ?? []`).
|
|
13505
|
+
const ctx = { $markRaw: ReactiveProxy.markRaw.bind(ReactiveProxy), ...propValues };
|
|
13506
|
+
const userData = data
|
|
13507
|
+
? data.call(ctx)
|
|
13508
|
+
: {};
|
|
13509
|
+
// Props are always included in data so they are reactive from the start.
|
|
13510
|
+
// User-returned values take precedence (allow transforming/defaulting props).
|
|
13511
|
+
return { ...propValues, ...userData };
|
|
13512
|
+
},
|
|
13513
|
+
computed,
|
|
13514
|
+
methods,
|
|
13515
|
+
watch,
|
|
13516
|
+
logLevel,
|
|
13517
|
+
};
|
|
13518
|
+
}
|
|
13519
|
+
}
|
|
13520
|
+
// Generate a property getter/setter for each declared prop.
|
|
13521
|
+
// This enables the parent VApplication to push updates via `element.propName = value`.
|
|
13522
|
+
for (const prop of props) {
|
|
13523
|
+
Object.defineProperty(ComponentElement.prototype, prop, {
|
|
13524
|
+
get() {
|
|
13525
|
+
return this._getProp(prop);
|
|
13526
|
+
},
|
|
13527
|
+
set(value) {
|
|
13528
|
+
this._setProp(prop, value);
|
|
13529
|
+
},
|
|
13530
|
+
configurable: true,
|
|
13531
|
+
enumerable: true,
|
|
13532
|
+
});
|
|
13533
|
+
}
|
|
13534
|
+
customElements.define(tagName, ComponentElement);
|
|
13535
|
+
}
|
|
13536
|
+
|
|
13072
13537
|
exports.ExpressionUtils = ExpressionUtils;
|
|
13538
|
+
exports.IchigoElement = IchigoElement;
|
|
13073
13539
|
exports.ReactiveProxy = ReactiveProxy;
|
|
13074
13540
|
exports.VComponent = VComponent;
|
|
13075
13541
|
exports.VComponentRegistry = VComponentRegistry;
|
|
13076
13542
|
exports.VDOM = VDOM;
|
|
13543
|
+
exports.defineComponent = defineComponent;
|
|
13077
13544
|
|
|
13078
13545
|
}));
|
|
13079
13546
|
//# sourceMappingURL=ichigo.cjs.map
|