@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.esm.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
2
2
|
/**
|
|
3
3
|
* Represents a reusable component definition.
|
|
4
|
+
* @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
|
|
4
5
|
*/
|
|
5
6
|
class VComponent {
|
|
6
7
|
/**
|
|
@@ -38,6 +39,7 @@ class VComponent {
|
|
|
38
39
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
39
40
|
/**
|
|
40
41
|
* A registry for managing component definitions.
|
|
42
|
+
* @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
|
|
41
43
|
*/
|
|
42
44
|
class VComponentRegistry {
|
|
43
45
|
/**
|
|
@@ -6729,11 +6731,18 @@ class ExpressionUtils {
|
|
|
6729
6731
|
* Extracts variable and function names used in the expression.
|
|
6730
6732
|
* @param expression The expression string to analyze.
|
|
6731
6733
|
* @param functionDependencies A dictionary mapping function names to their dependencies.
|
|
6734
|
+
* @param options Optional parsing options.
|
|
6735
|
+
* - asScript: If true, parse the input as a Script (allows multi-statement source with semicolons,
|
|
6736
|
+
* declarations, control-flow, etc.). If false/omitted, parse as a single expression (the default,
|
|
6737
|
+
* for backward compatibility with interpolation and binding directives).
|
|
6732
6738
|
* @returns An array of identifier names.
|
|
6733
6739
|
*/
|
|
6734
|
-
static extractIdentifiers(expression, functionDependencies) {
|
|
6740
|
+
static extractIdentifiers(expression, functionDependencies, options) {
|
|
6735
6741
|
const identifiers = new Set();
|
|
6736
|
-
|
|
6742
|
+
// In expression mode we wrap in parens so acorn parses the source as a single expression.
|
|
6743
|
+
// In script mode we parse as a Program so that multi-statement bodies (e.g. "a=1; b=2") work.
|
|
6744
|
+
const source = options?.asScript ? expression : `(${expression})`;
|
|
6745
|
+
const ast = parse(source, { ecmaVersion: "latest" });
|
|
6737
6746
|
// Use walk.full instead of walk.simple to visit ALL nodes including assignment LHS
|
|
6738
6747
|
full(ast, (node) => {
|
|
6739
6748
|
if (node.type === 'Identifier') {
|
|
@@ -6965,10 +6974,10 @@ class ExpressionUtils {
|
|
|
6965
6974
|
* @param identifiers The list of identifiers that are available in bindings.
|
|
6966
6975
|
* @returns The rewritten expression.
|
|
6967
6976
|
*/
|
|
6968
|
-
static rewriteExpression(expression, identifiers) {
|
|
6977
|
+
static rewriteExpression(expression, identifiers, options) {
|
|
6969
6978
|
// Reserved words and built-in objects that should not be prefixed with 'this.'
|
|
6970
6979
|
const reserved = new Set([
|
|
6971
|
-
'event', '$ctx', '$newValue',
|
|
6980
|
+
'event', '$event', '$ctx', '$newValue',
|
|
6972
6981
|
'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
|
|
6973
6982
|
'Math', 'Date', 'String', 'Number', 'Boolean', 'Array', 'Object',
|
|
6974
6983
|
'JSON', 'console', 'window', 'document', 'navigator',
|
|
@@ -6980,7 +6989,7 @@ class ExpressionUtils {
|
|
|
6980
6989
|
// identifiers that are used (right-hand side), not assigned to (left-hand side)
|
|
6981
6990
|
let allIdentifiersInExpression;
|
|
6982
6991
|
try {
|
|
6983
|
-
allIdentifiersInExpression = ExpressionUtils.extractIdentifiers(expression, {});
|
|
6992
|
+
allIdentifiersInExpression = ExpressionUtils.extractIdentifiers(expression, {}, options);
|
|
6984
6993
|
}
|
|
6985
6994
|
catch (error) {
|
|
6986
6995
|
console.warn('[ichigo.js] Failed to extract identifiers from expression:', expression, error);
|
|
@@ -7002,7 +7011,26 @@ class ExpressionUtils {
|
|
|
7002
7011
|
try {
|
|
7003
7012
|
// Build a map of positions to replace: { start: number, end: number, name: string }[]
|
|
7004
7013
|
const replacements = [];
|
|
7005
|
-
|
|
7014
|
+
// In script mode we must not wrap in parens (that would make multi-statement input invalid).
|
|
7015
|
+
// Offsets from the parser therefore refer directly to the original expression, so no shift.
|
|
7016
|
+
const asScript = options?.asScript === true;
|
|
7017
|
+
const source = asScript ? expression : `(${expression})`;
|
|
7018
|
+
const offsetShift = asScript ? 0 : 1;
|
|
7019
|
+
const parsedAst = parse(source, { ecmaVersion: 'latest' });
|
|
7020
|
+
// Track identifiers that are locally declared within the handler body (let/const/var, function
|
|
7021
|
+
// params) so we don't rewrite them to `this.xxx`. Only relevant in script mode, where the user
|
|
7022
|
+
// can write declarations; in expression mode there are no declarations to track.
|
|
7023
|
+
const locallyDeclared = new Set();
|
|
7024
|
+
if (asScript) {
|
|
7025
|
+
full(parsedAst, (node) => {
|
|
7026
|
+
if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') {
|
|
7027
|
+
locallyDeclared.add(node.id.name);
|
|
7028
|
+
}
|
|
7029
|
+
else if (node.type === 'FunctionDeclaration' && node.id?.type === 'Identifier') {
|
|
7030
|
+
locallyDeclared.add(node.id.name);
|
|
7031
|
+
}
|
|
7032
|
+
});
|
|
7033
|
+
}
|
|
7006
7034
|
// Collect all identifier nodes that should be replaced
|
|
7007
7035
|
// Use walk.fullAncestor to visit ALL nodes (including assignment LHS) while tracking ancestors
|
|
7008
7036
|
fullAncestor(parsedAst, (node, _state, ancestors) => {
|
|
@@ -7013,6 +7041,10 @@ class ExpressionUtils {
|
|
|
7013
7041
|
if (!bindingIdentifiers.has(node.name)) {
|
|
7014
7042
|
return;
|
|
7015
7043
|
}
|
|
7044
|
+
// Skip identifiers that were declared locally in the handler body
|
|
7045
|
+
if (locallyDeclared.has(node.name)) {
|
|
7046
|
+
return;
|
|
7047
|
+
}
|
|
7016
7048
|
// Check if this identifier is a property of a MemberExpression
|
|
7017
7049
|
// (e.g., in 'obj.prop', we should skip 'prop')
|
|
7018
7050
|
if (ancestors.length >= 1) {
|
|
@@ -7024,10 +7056,10 @@ class ExpressionUtils {
|
|
|
7024
7056
|
}
|
|
7025
7057
|
}
|
|
7026
7058
|
}
|
|
7027
|
-
// Add to replacements list (adjust for the wrapping parentheses)
|
|
7059
|
+
// Add to replacements list (adjust for the wrapping parentheses in expression mode)
|
|
7028
7060
|
replacements.push({
|
|
7029
|
-
start: node.start -
|
|
7030
|
-
end: node.end -
|
|
7061
|
+
start: node.start - offsetShift,
|
|
7062
|
+
end: node.end - offsetShift,
|
|
7031
7063
|
name: node.name
|
|
7032
7064
|
});
|
|
7033
7065
|
});
|
|
@@ -7468,6 +7500,14 @@ class VBindDirective {
|
|
|
7468
7500
|
else if (attributeName === 'style') {
|
|
7469
7501
|
this.#updateStyle(element, value);
|
|
7470
7502
|
}
|
|
7503
|
+
else if (this.#isCustomElementProperty(element, attributeName, value)) {
|
|
7504
|
+
// Custom elements: set objects/arrays (and declared props) as properties
|
|
7505
|
+
// so complex values are not serialized to strings via setAttribute.
|
|
7506
|
+
// HTML attributes are lowercased by the browser, so resolve to the
|
|
7507
|
+
// actual camelCase property name declared on the custom element.
|
|
7508
|
+
const propName = this.#resolveCustomElementPropertyName(element, attributeName);
|
|
7509
|
+
element[propName] = value;
|
|
7510
|
+
}
|
|
7471
7511
|
else if (this.#isDOMProperty(attributeName)) {
|
|
7472
7512
|
this.#updateProperty(element, attributeName, value);
|
|
7473
7513
|
}
|
|
@@ -7551,6 +7591,46 @@ class VBindDirective {
|
|
|
7551
7591
|
#camelToKebab(str) {
|
|
7552
7592
|
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
7553
7593
|
}
|
|
7594
|
+
/**
|
|
7595
|
+
* Returns true when the target is a custom element and the binding should be
|
|
7596
|
+
* delivered as a property rather than an HTML attribute.
|
|
7597
|
+
*
|
|
7598
|
+
* Two conditions trigger property delivery:
|
|
7599
|
+
* 1. The value is an object or array — serialising these to a string attribute
|
|
7600
|
+
* would lose type information.
|
|
7601
|
+
* 2. The element exposes a matching property accessor (e.g. a prop declared via
|
|
7602
|
+
* defineComponent), even when the value is a primitive.
|
|
7603
|
+
*/
|
|
7604
|
+
#isCustomElementProperty(element, name, value) {
|
|
7605
|
+
if (!element.tagName.includes('-')) {
|
|
7606
|
+
return false;
|
|
7607
|
+
}
|
|
7608
|
+
const isObjectOrArray = Array.isArray(value) || (typeof value === 'object' && value !== null);
|
|
7609
|
+
return isObjectOrArray || name in element;
|
|
7610
|
+
}
|
|
7611
|
+
/**
|
|
7612
|
+
* Resolves the actual property name on a custom element for a given
|
|
7613
|
+
* (lowercased) HTML attribute name. HTML attributes are always lowercase,
|
|
7614
|
+
* but custom element props are typically camelCase. This method checks the
|
|
7615
|
+
* element's declared _props (set by defineComponent) for a case-insensitive
|
|
7616
|
+
* match, falling back to scanning the prototype's own property descriptors.
|
|
7617
|
+
*/
|
|
7618
|
+
#resolveCustomElementPropertyName(element, name) {
|
|
7619
|
+
// Fast path: exact match already exists
|
|
7620
|
+
if (name in element) {
|
|
7621
|
+
return name;
|
|
7622
|
+
}
|
|
7623
|
+
// Check declared _props from defineComponent / IchigoElement
|
|
7624
|
+
const props = element.constructor._props;
|
|
7625
|
+
if (Array.isArray(props)) {
|
|
7626
|
+
const lowerName = name.toLowerCase();
|
|
7627
|
+
const match = props.find(p => p.toLowerCase() === lowerName);
|
|
7628
|
+
if (match) {
|
|
7629
|
+
return match;
|
|
7630
|
+
}
|
|
7631
|
+
}
|
|
7632
|
+
return name;
|
|
7633
|
+
}
|
|
7554
7634
|
/**
|
|
7555
7635
|
* Checks if the attribute should be set as a DOM property.
|
|
7556
7636
|
*/
|
|
@@ -8931,6 +9011,18 @@ class VNode {
|
|
|
8931
9011
|
});
|
|
8932
9012
|
}
|
|
8933
9013
|
}
|
|
9014
|
+
else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
9015
|
+
// If the node is a DocumentFragment, create child VNodes for its children
|
|
9016
|
+
// DocumentFragment is not an Element so it has no directives itself.
|
|
9017
|
+
this.#childVNodes = [];
|
|
9018
|
+
for (const childNode of Array.from(this.#node.childNodes)) {
|
|
9019
|
+
new VNode({
|
|
9020
|
+
node: childNode,
|
|
9021
|
+
vApplication: this.#vApplication,
|
|
9022
|
+
parentVNode: this
|
|
9023
|
+
});
|
|
9024
|
+
}
|
|
9025
|
+
}
|
|
8934
9026
|
// Register this node as a dependent of the parent node, if any
|
|
8935
9027
|
this.#closers = this.#parentVNode?.addDependent(this);
|
|
8936
9028
|
}
|
|
@@ -9185,6 +9277,16 @@ class VNode {
|
|
|
9185
9277
|
});
|
|
9186
9278
|
}
|
|
9187
9279
|
}
|
|
9280
|
+
else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
9281
|
+
// For document fragments (e.g. from <template v-if>), propagate updates
|
|
9282
|
+
// to dependent virtual nodes so that child bindings stay reactive.
|
|
9283
|
+
this.#dependents?.forEach(dependentNode => {
|
|
9284
|
+
const changed = dependentNode.dependentIdentifiers.some(id => changes.some(change => dependentNode.bindings.doesChangeMatchIdentifier(change, id)));
|
|
9285
|
+
if (changed) {
|
|
9286
|
+
dependentNode.update();
|
|
9287
|
+
}
|
|
9288
|
+
});
|
|
9289
|
+
}
|
|
9188
9290
|
}
|
|
9189
9291
|
/**
|
|
9190
9292
|
* Forces an update of the virtual node and its children, regardless of changed identifiers.
|
|
@@ -9234,6 +9336,12 @@ class VNode {
|
|
|
9234
9336
|
});
|
|
9235
9337
|
}
|
|
9236
9338
|
}
|
|
9339
|
+
else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
9340
|
+
// For document fragments, recursively force update child VNodes
|
|
9341
|
+
this.#childVNodes?.forEach(childVNode => {
|
|
9342
|
+
childVNode.forceUpdate();
|
|
9343
|
+
});
|
|
9344
|
+
}
|
|
9237
9345
|
}
|
|
9238
9346
|
/**
|
|
9239
9347
|
* Adds a child virtual node to this virtual node.
|
|
@@ -9611,9 +9719,11 @@ class VConditionalDirective {
|
|
|
9611
9719
|
}
|
|
9612
9720
|
// Clone the original node and create a new VNode for it
|
|
9613
9721
|
const clone = this.#cloneNode();
|
|
9614
|
-
//
|
|
9615
|
-
|
|
9616
|
-
//
|
|
9722
|
+
// Create a new VNode for the cloned element BEFORE inserting into DOM.
|
|
9723
|
+
// This prevents custom elements (Web Components) from having their
|
|
9724
|
+
// connectedCallback fire before VNode processes their children, which
|
|
9725
|
+
// would cause the parent VApplication to incorrectly adopt the custom
|
|
9726
|
+
// element's internal template content as its own VNode tree.
|
|
9617
9727
|
// Pass the current bindings to ensure loop variables from v-for are available
|
|
9618
9728
|
const vNode = new VNode({
|
|
9619
9729
|
node: clone,
|
|
@@ -9621,6 +9731,29 @@ class VConditionalDirective {
|
|
|
9621
9731
|
parentVNode: this.#vNode,
|
|
9622
9732
|
bindings: this.#vNode.bindings
|
|
9623
9733
|
});
|
|
9734
|
+
// Insert after the anchor node AFTER VNode creation
|
|
9735
|
+
const anchorParent = this.#vNode.anchorNode?.parentNode;
|
|
9736
|
+
const nextSibling = this.#vNode.anchorNode?.nextSibling ?? null;
|
|
9737
|
+
if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE && anchorParent) {
|
|
9738
|
+
const startMarker = document.createComment('#vif-fragment-start');
|
|
9739
|
+
const endMarker = document.createComment('#vif-fragment-end');
|
|
9740
|
+
if (nextSibling) {
|
|
9741
|
+
anchorParent.insertBefore(startMarker, nextSibling);
|
|
9742
|
+
}
|
|
9743
|
+
else {
|
|
9744
|
+
anchorParent.appendChild(startMarker);
|
|
9745
|
+
}
|
|
9746
|
+
anchorParent.insertBefore(endMarker, startMarker.nextSibling);
|
|
9747
|
+
anchorParent.insertBefore(clone, endMarker);
|
|
9748
|
+
// Store markers for later removal
|
|
9749
|
+
vNode.userData.set('vif_fragment_start', startMarker);
|
|
9750
|
+
vNode.userData.set('vif_fragment_end', endMarker);
|
|
9751
|
+
this.#renderedVNode = vNode;
|
|
9752
|
+
this.#renderedVNode.forceUpdate();
|
|
9753
|
+
return;
|
|
9754
|
+
}
|
|
9755
|
+
const nodeToInsert = vNode.anchorNode || clone;
|
|
9756
|
+
anchorParent?.insertBefore(nodeToInsert, nextSibling);
|
|
9624
9757
|
this.#renderedVNode = vNode;
|
|
9625
9758
|
this.#renderedVNode.forceUpdate();
|
|
9626
9759
|
}
|
|
@@ -9635,17 +9768,40 @@ class VConditionalDirective {
|
|
|
9635
9768
|
}
|
|
9636
9769
|
// Destroy VNode first (calls @unmount hooks while DOM is still accessible)
|
|
9637
9770
|
this.#renderedVNode.destroy();
|
|
9638
|
-
// Then remove from DOM
|
|
9639
|
-
this.#renderedVNode.
|
|
9771
|
+
// Then remove from DOM. Handle fragment markers if present
|
|
9772
|
+
const startMarker = this.#renderedVNode.userData.get?.('vif_fragment_start');
|
|
9773
|
+
const endMarker = this.#renderedVNode.userData.get?.('vif_fragment_end');
|
|
9774
|
+
if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
|
|
9775
|
+
const parentNode = startMarker.parentNode;
|
|
9776
|
+
let node = startMarker;
|
|
9777
|
+
while (node) {
|
|
9778
|
+
const next = node.nextSibling;
|
|
9779
|
+
parentNode.removeChild(node);
|
|
9780
|
+
if (node === endMarker)
|
|
9781
|
+
break;
|
|
9782
|
+
node = next;
|
|
9783
|
+
}
|
|
9784
|
+
this.#renderedVNode = undefined;
|
|
9785
|
+
return;
|
|
9786
|
+
}
|
|
9787
|
+
const parent = this.#renderedVNode.node.parentNode;
|
|
9788
|
+
if (parent) {
|
|
9789
|
+
parent.removeChild(this.#renderedVNode.node);
|
|
9790
|
+
}
|
|
9640
9791
|
this.#renderedVNode = undefined;
|
|
9641
9792
|
}
|
|
9642
9793
|
/**
|
|
9643
9794
|
* Clones the original node of the directive's virtual node.
|
|
9644
|
-
*
|
|
9645
|
-
*
|
|
9795
|
+
* When the source element is a <template>, returns a DocumentFragment
|
|
9796
|
+
* cloned from the template's content.
|
|
9797
|
+
* @returns The cloned Node (HTMLElement or DocumentFragment).
|
|
9646
9798
|
*/
|
|
9647
9799
|
#cloneNode() {
|
|
9648
9800
|
const element = this.#vNode.node;
|
|
9801
|
+
if (element instanceof HTMLTemplateElement) {
|
|
9802
|
+
// Return a DocumentFragment cloned from the template content
|
|
9803
|
+
return element.content.cloneNode(true);
|
|
9804
|
+
}
|
|
9649
9805
|
return element.cloneNode(true);
|
|
9650
9806
|
}
|
|
9651
9807
|
/**
|
|
@@ -9963,10 +10119,28 @@ class VForDirective {
|
|
|
9963
10119
|
vNode.destroy();
|
|
9964
10120
|
}
|
|
9965
10121
|
}
|
|
9966
|
-
// Then remove from DOM
|
|
10122
|
+
// Then remove from DOM. Handle both Element nodes and fragment-marked ranges.
|
|
9967
10123
|
for (const vNode of nodesToRemove) {
|
|
9968
|
-
|
|
9969
|
-
|
|
10124
|
+
// If this VNode stored fragment markers, remove the range between them
|
|
10125
|
+
const startMarker = vNode.userData.get?.('vfor_fragment_start');
|
|
10126
|
+
const endMarker = vNode.userData.get?.('vfor_fragment_end');
|
|
10127
|
+
if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
|
|
10128
|
+
const parentNode = startMarker.parentNode;
|
|
10129
|
+
let node = startMarker;
|
|
10130
|
+
// Remove nodes from startMarker up to and including endMarker
|
|
10131
|
+
while (node) {
|
|
10132
|
+
const next = node.nextSibling;
|
|
10133
|
+
parentNode.removeChild(node);
|
|
10134
|
+
if (node === endMarker)
|
|
10135
|
+
break;
|
|
10136
|
+
node = next;
|
|
10137
|
+
}
|
|
10138
|
+
continue;
|
|
10139
|
+
}
|
|
10140
|
+
// Fallback: remove the node itself if it's attached
|
|
10141
|
+
const parentOfNode = vNode.node.parentNode;
|
|
10142
|
+
if (parentOfNode) {
|
|
10143
|
+
parentOfNode.removeChild(vNode.node);
|
|
9970
10144
|
}
|
|
9971
10145
|
}
|
|
9972
10146
|
// Add or reorder items
|
|
@@ -10000,6 +10174,28 @@ class VForDirective {
|
|
|
10000
10174
|
bindings,
|
|
10001
10175
|
dependentIdentifiers: depIds,
|
|
10002
10176
|
});
|
|
10177
|
+
// If clone is a DocumentFragment, insert it between start/end comment markers
|
|
10178
|
+
if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
10179
|
+
const startMarker = document.createComment('#vfor-fragment-start');
|
|
10180
|
+
const endMarker = document.createComment('#vfor-fragment-end');
|
|
10181
|
+
// Insert start and end markers and the fragment's children between them
|
|
10182
|
+
if (prevNode.nextSibling) {
|
|
10183
|
+
parent.insertBefore(startMarker, prevNode.nextSibling);
|
|
10184
|
+
}
|
|
10185
|
+
else {
|
|
10186
|
+
parent.appendChild(startMarker);
|
|
10187
|
+
}
|
|
10188
|
+
parent.insertBefore(endMarker, startMarker.nextSibling);
|
|
10189
|
+
parent.insertBefore(clone, endMarker);
|
|
10190
|
+
// Store markers on the VNode for later removal/movement
|
|
10191
|
+
vNode.userData.set('vfor_fragment_start', startMarker);
|
|
10192
|
+
vNode.userData.set('vfor_fragment_end', endMarker);
|
|
10193
|
+
newRenderedItems.set(key, vNode);
|
|
10194
|
+
vNode.forceUpdate();
|
|
10195
|
+
// Use endMarker as prevNode for subsequent insertions
|
|
10196
|
+
prevNode = endMarker;
|
|
10197
|
+
continue;
|
|
10198
|
+
}
|
|
10003
10199
|
// Determine what to insert: anchor node (if exists) or the clone itself
|
|
10004
10200
|
const nodeToInsert = vNode.anchorNode || clone;
|
|
10005
10201
|
// Insert after previous node
|
|
@@ -10017,8 +10213,9 @@ class VForDirective {
|
|
|
10017
10213
|
newRenderedItems.set(key, vNode);
|
|
10018
10214
|
// Update bindings
|
|
10019
10215
|
this.#updateItemBindings(vNode, context);
|
|
10020
|
-
// Determine the actual node in DOM: anchor node
|
|
10021
|
-
const
|
|
10216
|
+
// Determine the actual node in DOM: prefer fragment end marker, then anchor node, then vNode.node
|
|
10217
|
+
const fragmentEnd = vNode.userData.get?.('vfor_fragment_end');
|
|
10218
|
+
const actualNode = fragmentEnd || vNode.anchorNode || vNode.node;
|
|
10022
10219
|
// Move to correct position if needed
|
|
10023
10220
|
if (prevNode.nextSibling !== actualNode) {
|
|
10024
10221
|
if (prevNode.nextSibling) {
|
|
@@ -10029,8 +10226,9 @@ class VForDirective {
|
|
|
10029
10226
|
}
|
|
10030
10227
|
}
|
|
10031
10228
|
}
|
|
10032
|
-
// Use
|
|
10033
|
-
|
|
10229
|
+
// Use fragment end marker > anchor node > vNode.node as prevNode
|
|
10230
|
+
const fragmentEndForPrev = vNode.userData.get?.('vfor_fragment_end');
|
|
10231
|
+
prevNode = fragmentEndForPrev || vNode.anchorNode || vNode.node;
|
|
10034
10232
|
}
|
|
10035
10233
|
// Update rendered items map
|
|
10036
10234
|
this.#renderedItems = newRenderedItems;
|
|
@@ -10209,12 +10407,17 @@ class VForDirective {
|
|
|
10209
10407
|
}
|
|
10210
10408
|
/**
|
|
10211
10409
|
* Clones the original node of the directive's virtual node.
|
|
10212
|
-
*
|
|
10213
|
-
*
|
|
10410
|
+
* When the source element is a <template>, its children (stored in .content)
|
|
10411
|
+
* are cloned into a DocumentFragment so that multiple root nodes can be
|
|
10412
|
+
* managed without adding an extra wrapper element.
|
|
10413
|
+
* @returns The cloned Node (Element or DocumentFragment).
|
|
10214
10414
|
*/
|
|
10215
10415
|
#cloneNode() {
|
|
10216
|
-
// Clone the original element
|
|
10217
10416
|
const element = this.#vNode.node;
|
|
10417
|
+
if (element instanceof HTMLTemplateElement) {
|
|
10418
|
+
// Return a cloned DocumentFragment containing the template content
|
|
10419
|
+
return element.content.cloneNode(true);
|
|
10420
|
+
}
|
|
10218
10421
|
return element.cloneNode(true);
|
|
10219
10422
|
}
|
|
10220
10423
|
/**
|
|
@@ -11055,10 +11258,12 @@ class VOnDirective {
|
|
|
11055
11258
|
this.#eventName = parts[0];
|
|
11056
11259
|
parts.slice(1).forEach(mod => this.#modifiers.add(mod));
|
|
11057
11260
|
}
|
|
11058
|
-
// Parse the expression to extract identifiers and create the handler wrapper
|
|
11261
|
+
// Parse the expression to extract identifiers and create the handler wrapper.
|
|
11262
|
+
// Event handlers are parsed in script mode so that users can write multi-statement bodies
|
|
11263
|
+
// (e.g. "a=1; b=2"), declarations, and control-flow constructs — matching Vue semantics.
|
|
11059
11264
|
const expression = context.attribute.value;
|
|
11060
11265
|
if (expression) {
|
|
11061
|
-
this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
|
|
11266
|
+
this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies, { asScript: true });
|
|
11062
11267
|
}
|
|
11063
11268
|
// Check if this is a lifecycle hook or a regular event
|
|
11064
11269
|
if (this.#eventName && this.#isLifecycleHook(this.#eventName)) {
|
|
@@ -11272,10 +11477,11 @@ class VOnDirective {
|
|
|
11272
11477
|
// This allows the method to access the DOM element, VNode, and userData
|
|
11273
11478
|
return originalMethod($ctx);
|
|
11274
11479
|
}
|
|
11275
|
-
// For inline
|
|
11276
|
-
// This allows assignments like "currentTab = 'shop'" to work correctly
|
|
11277
|
-
|
|
11278
|
-
const
|
|
11480
|
+
// For inline bodies, rewrite to use 'this' context
|
|
11481
|
+
// This allows assignments like "currentTab = 'shop'" to work correctly.
|
|
11482
|
+
// Script mode allows multi-statement bodies (e.g. "a=1; init()") and control-flow.
|
|
11483
|
+
const rewrittenExpr = this.#rewriteExpression(expression, identifiers, { asScript: true });
|
|
11484
|
+
const funcBody = rewrittenExpr;
|
|
11279
11485
|
const func = new Function('$ctx', funcBody);
|
|
11280
11486
|
return func.call(bindings?.raw, $ctx);
|
|
11281
11487
|
};
|
|
@@ -11305,12 +11511,15 @@ class VOnDirective {
|
|
|
11305
11511
|
// Pass event as first argument and $ctx as second argument
|
|
11306
11512
|
return originalMethod(event, $ctx);
|
|
11307
11513
|
}
|
|
11308
|
-
// For inline
|
|
11309
|
-
// This allows assignments like "currentTab = 'shop'" to work correctly
|
|
11310
|
-
|
|
11311
|
-
|
|
11312
|
-
const
|
|
11313
|
-
|
|
11514
|
+
// For inline bodies, rewrite to use 'this' context
|
|
11515
|
+
// This allows assignments like "currentTab = 'shop'" to work correctly.
|
|
11516
|
+
// Script mode allows multi-statement bodies (e.g. "a=1; b=2") and control-flow,
|
|
11517
|
+
// so we emit the rewritten source directly as the function body (no `return (...)`).
|
|
11518
|
+
const rewrittenExpr = this.#rewriteExpression(expression, identifiers, { asScript: true });
|
|
11519
|
+
const funcBody = rewrittenExpr;
|
|
11520
|
+
// '$event' is an alias for 'event' for Vue compatibility
|
|
11521
|
+
const func = new Function('event', '$event', '$ctx', funcBody);
|
|
11522
|
+
return func.call(bindings?.raw, event, event, $ctx);
|
|
11314
11523
|
};
|
|
11315
11524
|
}
|
|
11316
11525
|
/**
|
|
@@ -11321,8 +11530,8 @@ class VOnDirective {
|
|
|
11321
11530
|
* @param identifiers The list of identifiers that are available in bindings.
|
|
11322
11531
|
* @returns The rewritten expression.
|
|
11323
11532
|
*/
|
|
11324
|
-
#rewriteExpression(expression, identifiers) {
|
|
11325
|
-
return ExpressionUtils.rewriteExpression(expression, identifiers);
|
|
11533
|
+
#rewriteExpression(expression, identifiers, options) {
|
|
11534
|
+
return ExpressionUtils.rewriteExpression(expression, identifiers, options);
|
|
11326
11535
|
}
|
|
11327
11536
|
}
|
|
11328
11537
|
|
|
@@ -13020,6 +13229,7 @@ class VDOM {
|
|
|
13020
13229
|
/**
|
|
13021
13230
|
* Gets the component registry.
|
|
13022
13231
|
* @return {VComponentRegistry} The component registry.
|
|
13232
|
+
* @deprecated This method is deprecated and will be removed in a future release. Please use the new component registration system instead.
|
|
13023
13233
|
*/
|
|
13024
13234
|
static get componentRegistry() {
|
|
13025
13235
|
return this.#componentRegistry;
|
|
@@ -13063,5 +13273,260 @@ class VDOM {
|
|
|
13063
13273
|
}
|
|
13064
13274
|
}
|
|
13065
13275
|
|
|
13066
|
-
|
|
13276
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
13277
|
+
/**
|
|
13278
|
+
* Base class for ichigo.js-backed Web Components (Light DOM, no Shadow DOM).
|
|
13279
|
+
*
|
|
13280
|
+
* Mount timing:
|
|
13281
|
+
* - If the component declares no props, the VApplication is mounted synchronously
|
|
13282
|
+
* at the end of connectedCallback (the template is known, no props to wait for).
|
|
13283
|
+
* - If the component declares props, the VApplication is mounted the first time
|
|
13284
|
+
* _setProp() is called after connectedCallback has prepared the DOM. This
|
|
13285
|
+
* guarantees that the parent framework (e.g. ichigo.js VBindDirective) has
|
|
13286
|
+
* already delivered at least one prop value before data() is evaluated.
|
|
13287
|
+
*
|
|
13288
|
+
* Subclasses must set the static fields _template, _props and _buildOptions before
|
|
13289
|
+
* calling customElements.define(). defineComponent() handles this automatically.
|
|
13290
|
+
*/
|
|
13291
|
+
class IchigoElement extends HTMLElement {
|
|
13292
|
+
/**
|
|
13293
|
+
* The mounted VApplication instance, present only while connected to the DOM.
|
|
13294
|
+
*/
|
|
13295
|
+
#app;
|
|
13296
|
+
/**
|
|
13297
|
+
* Stores prop values received at any time (before or after mount).
|
|
13298
|
+
*/
|
|
13299
|
+
#propValues = {};
|
|
13300
|
+
/**
|
|
13301
|
+
* The root element cloned from the template, ready for mounting.
|
|
13302
|
+
* Set by connectedCallback; cleared on disconnect.
|
|
13303
|
+
*/
|
|
13304
|
+
#mountRoot;
|
|
13305
|
+
/**
|
|
13306
|
+
* Whether a mount microtask is already queued to avoid double-mounting.
|
|
13307
|
+
*/
|
|
13308
|
+
#mountScheduled = false;
|
|
13309
|
+
connectedCallback() {
|
|
13310
|
+
// --- 0. Guard: skip if template not yet loaded ---
|
|
13311
|
+
// customElements.define() may run before loadComponent() completes (e.g. when
|
|
13312
|
+
// dynamic imports are inlined by the bundler). The static placeholder element
|
|
13313
|
+
// in the HTML will be removed and replaced by v-for once the template is ready,
|
|
13314
|
+
// so it is safe to do nothing here.
|
|
13315
|
+
const ctor = this.constructor;
|
|
13316
|
+
const templateEl = document.querySelector(ctor._template);
|
|
13317
|
+
if (!templateEl || !(templateEl instanceof HTMLTemplateElement)) {
|
|
13318
|
+
return;
|
|
13319
|
+
}
|
|
13320
|
+
// --- 1. Capture slot content before clearing children ---
|
|
13321
|
+
const defaultSlotNodes = [];
|
|
13322
|
+
const namedSlotNodes = new Map();
|
|
13323
|
+
for (const child of Array.from(this.childNodes)) {
|
|
13324
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
13325
|
+
const el = child;
|
|
13326
|
+
const slotName = el.getAttribute('slot');
|
|
13327
|
+
if (slotName) {
|
|
13328
|
+
if (!namedSlotNodes.has(slotName)) {
|
|
13329
|
+
namedSlotNodes.set(slotName, []);
|
|
13330
|
+
}
|
|
13331
|
+
namedSlotNodes.get(slotName).push(el);
|
|
13332
|
+
// Remove slot attribute so ichigo.js doesn't try to bind it
|
|
13333
|
+
el.removeAttribute('slot');
|
|
13334
|
+
}
|
|
13335
|
+
else {
|
|
13336
|
+
defaultSlotNodes.push(el);
|
|
13337
|
+
}
|
|
13338
|
+
}
|
|
13339
|
+
else if (child.nodeType === Node.TEXT_NODE) {
|
|
13340
|
+
if ((child.textContent ?? '').trim()) {
|
|
13341
|
+
defaultSlotNodes.push(child);
|
|
13342
|
+
}
|
|
13343
|
+
}
|
|
13344
|
+
}
|
|
13345
|
+
// Clear host element so we can append the cloned template
|
|
13346
|
+
while (this.firstChild) {
|
|
13347
|
+
this.removeChild(this.firstChild);
|
|
13348
|
+
}
|
|
13349
|
+
// --- 2. Clone the component template ---
|
|
13350
|
+
const fragment = templateEl.content.cloneNode(true);
|
|
13351
|
+
const root = this.#findRootElement(fragment);
|
|
13352
|
+
// --- 3. Distribute named slot content ---
|
|
13353
|
+
for (const [name, nodes] of namedSlotNodes) {
|
|
13354
|
+
const slot = root.querySelector(`slot[name="${name}"]`);
|
|
13355
|
+
if (slot) {
|
|
13356
|
+
slot.replaceWith(...nodes);
|
|
13357
|
+
}
|
|
13358
|
+
}
|
|
13359
|
+
// --- 4. Distribute default slot content ---
|
|
13360
|
+
const defaultSlot = root.querySelector('slot:not([name])');
|
|
13361
|
+
if (defaultSlot && defaultSlotNodes.length > 0) {
|
|
13362
|
+
defaultSlot.replaceWith(...defaultSlotNodes);
|
|
13363
|
+
}
|
|
13364
|
+
// Attach the populated template to the host element
|
|
13365
|
+
this.appendChild(root);
|
|
13366
|
+
this.#mountRoot = root;
|
|
13367
|
+
// If props were set before connectedCallback, ensure mount is scheduled
|
|
13368
|
+
this.#scheduleMountIfNeeded();
|
|
13369
|
+
// --- 5. Mount strategy ---
|
|
13370
|
+
// If this component has no declared props, mount immediately.
|
|
13371
|
+
// If it has props, mount will be triggered from _setProp() once the parent
|
|
13372
|
+
// delivers the first prop value (via VBindDirective / forceUpdate).
|
|
13373
|
+
if (ctor._props.length === 0) {
|
|
13374
|
+
this.#doMount();
|
|
13375
|
+
}
|
|
13376
|
+
// else: wait for _setProp() to trigger #scheduleMountIfNeeded()
|
|
13377
|
+
}
|
|
13378
|
+
disconnectedCallback() {
|
|
13379
|
+
this.#mountRoot = undefined;
|
|
13380
|
+
this.#mountScheduled = false;
|
|
13381
|
+
if (this.#app) {
|
|
13382
|
+
this.#app.unmount();
|
|
13383
|
+
this.#app = undefined;
|
|
13384
|
+
}
|
|
13385
|
+
}
|
|
13386
|
+
/**
|
|
13387
|
+
* Called by the property setters generated by defineComponent().
|
|
13388
|
+
* Before mount: stores the value and schedules a mount microtask.
|
|
13389
|
+
* After mount: pushes the value directly into the reactive bindings.
|
|
13390
|
+
*/
|
|
13391
|
+
_setProp(name, value) {
|
|
13392
|
+
this.#propValues[name] = value;
|
|
13393
|
+
if (this.#app) {
|
|
13394
|
+
this.#app.bindings?.set(name, value);
|
|
13395
|
+
}
|
|
13396
|
+
else {
|
|
13397
|
+
this.#scheduleMountIfNeeded();
|
|
13398
|
+
}
|
|
13399
|
+
}
|
|
13400
|
+
/**
|
|
13401
|
+
* Called by the property getters generated by defineComponent().
|
|
13402
|
+
*/
|
|
13403
|
+
_getProp(name) {
|
|
13404
|
+
return this.#propValues[name];
|
|
13405
|
+
}
|
|
13406
|
+
// --- Static fields set by defineComponent() ---
|
|
13407
|
+
/**
|
|
13408
|
+
* CSS selector for the component's <template> element (e.g. '#my-card').
|
|
13409
|
+
*/
|
|
13410
|
+
static _template;
|
|
13411
|
+
/**
|
|
13412
|
+
* List of declared prop names. Used to decide whether to defer mounting.
|
|
13413
|
+
*/
|
|
13414
|
+
static _props = [];
|
|
13415
|
+
/**
|
|
13416
|
+
* Factory that builds VApplicationOptions from the current prop values.
|
|
13417
|
+
* Implemented by defineComponent() as a closure that captures the user's options.
|
|
13418
|
+
*/
|
|
13419
|
+
static _buildOptions;
|
|
13420
|
+
// --- Private helpers ---
|
|
13421
|
+
/**
|
|
13422
|
+
* Schedules a mount microtask if the DOM root is ready and no mount is pending.
|
|
13423
|
+
* Called from _setProp() so the mount happens after the prop value is stored.
|
|
13424
|
+
*/
|
|
13425
|
+
#scheduleMountIfNeeded() {
|
|
13426
|
+
if (this.#mountScheduled || this.#app || !this.#mountRoot) {
|
|
13427
|
+
return;
|
|
13428
|
+
}
|
|
13429
|
+
this.#mountScheduled = true;
|
|
13430
|
+
queueMicrotask(() => {
|
|
13431
|
+
this.#mountScheduled = false;
|
|
13432
|
+
this.#doMount();
|
|
13433
|
+
});
|
|
13434
|
+
}
|
|
13435
|
+
#doMount() {
|
|
13436
|
+
if (this.#app || !this.#mountRoot) {
|
|
13437
|
+
return;
|
|
13438
|
+
}
|
|
13439
|
+
const ctor = this.constructor;
|
|
13440
|
+
const options = ctor._buildOptions(this.#propValues);
|
|
13441
|
+
this.#app = VDOM.createApp(options);
|
|
13442
|
+
this.#app.mount(this.#mountRoot);
|
|
13443
|
+
}
|
|
13444
|
+
#findRootElement(fragment) {
|
|
13445
|
+
for (const node of Array.from(fragment.childNodes)) {
|
|
13446
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
13447
|
+
return node;
|
|
13448
|
+
}
|
|
13449
|
+
}
|
|
13450
|
+
throw new Error(`IchigoElement: no root element found in template '${this.constructor._template}'`);
|
|
13451
|
+
}
|
|
13452
|
+
}
|
|
13453
|
+
|
|
13454
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
13455
|
+
/**
|
|
13456
|
+
* Defines and registers a custom element backed by ichigo.js reactivity.
|
|
13457
|
+
*
|
|
13458
|
+
* Usage:
|
|
13459
|
+
* ```html
|
|
13460
|
+
* <template id="my-list">
|
|
13461
|
+
* <div>
|
|
13462
|
+
* <ul v-if="items.length > 0">
|
|
13463
|
+
* <li v-for="item of items">{{item.name}}</li>
|
|
13464
|
+
* </ul>
|
|
13465
|
+
* <slot></slot>
|
|
13466
|
+
* </div>
|
|
13467
|
+
* </template>
|
|
13468
|
+
* ```
|
|
13469
|
+
* ```typescript
|
|
13470
|
+
* defineComponent('my-list', {
|
|
13471
|
+
* template: '#my-list',
|
|
13472
|
+
* props: ['items'],
|
|
13473
|
+
* data() {
|
|
13474
|
+
* return { items: this.items ?? [] };
|
|
13475
|
+
* },
|
|
13476
|
+
* });
|
|
13477
|
+
* ```
|
|
13478
|
+
* ```html
|
|
13479
|
+
* <my-list :items="searchResults">
|
|
13480
|
+
* <span slot="empty">No results.</span>
|
|
13481
|
+
* </my-list>
|
|
13482
|
+
* ```
|
|
13483
|
+
*
|
|
13484
|
+
* @param tagName Custom element tag name (must contain a hyphen, e.g. 'my-card').
|
|
13485
|
+
* @param options Component options including template selector and optional props.
|
|
13486
|
+
*/
|
|
13487
|
+
function defineComponent(tagName, options) {
|
|
13488
|
+
const { props = [], template, data, computed, methods, watch, logLevel } = options;
|
|
13489
|
+
// Build a subclass of IchigoElement specific to this component
|
|
13490
|
+
class ComponentElement extends IchigoElement {
|
|
13491
|
+
static _template = template;
|
|
13492
|
+
static _props = props;
|
|
13493
|
+
static _buildOptions(propValues) {
|
|
13494
|
+
return {
|
|
13495
|
+
data() {
|
|
13496
|
+
// 'this' is the $ctx object provided by VApplication ({ $markRaw }).
|
|
13497
|
+
// We extend it with prop values so the user's data() can reference them
|
|
13498
|
+
// via 'this.propName' and supply defaults (e.g. `this.items ?? []`).
|
|
13499
|
+
const ctx = { $markRaw: ReactiveProxy.markRaw.bind(ReactiveProxy), ...propValues };
|
|
13500
|
+
const userData = data
|
|
13501
|
+
? data.call(ctx)
|
|
13502
|
+
: {};
|
|
13503
|
+
// Props are always included in data so they are reactive from the start.
|
|
13504
|
+
// User-returned values take precedence (allow transforming/defaulting props).
|
|
13505
|
+
return { ...propValues, ...userData };
|
|
13506
|
+
},
|
|
13507
|
+
computed,
|
|
13508
|
+
methods,
|
|
13509
|
+
watch,
|
|
13510
|
+
logLevel,
|
|
13511
|
+
};
|
|
13512
|
+
}
|
|
13513
|
+
}
|
|
13514
|
+
// Generate a property getter/setter for each declared prop.
|
|
13515
|
+
// This enables the parent VApplication to push updates via `element.propName = value`.
|
|
13516
|
+
for (const prop of props) {
|
|
13517
|
+
Object.defineProperty(ComponentElement.prototype, prop, {
|
|
13518
|
+
get() {
|
|
13519
|
+
return this._getProp(prop);
|
|
13520
|
+
},
|
|
13521
|
+
set(value) {
|
|
13522
|
+
this._setProp(prop, value);
|
|
13523
|
+
},
|
|
13524
|
+
configurable: true,
|
|
13525
|
+
enumerable: true,
|
|
13526
|
+
});
|
|
13527
|
+
}
|
|
13528
|
+
customElements.define(tagName, ComponentElement);
|
|
13529
|
+
}
|
|
13530
|
+
|
|
13531
|
+
export { ExpressionUtils, IchigoElement, ReactiveProxy, VComponent, VComponentRegistry, VDOM, defineComponent };
|
|
13067
13532
|
//# sourceMappingURL=ichigo.esm.js.map
|