Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
725c5bd
feat: migrate image to ES6
thibaultzanini Jul 23, 2025
b1a5751
feat: Added teach for Image component
thibaultzanini Jul 23, 2025
40cdccb
feat: add slotContentDidFirstDraw delegate method
thibaultzanini Jul 24, 2025
d3229f2
feat: enhance delegate method handling with caching and new response …
thibaultzanini Jul 24, 2025
772c60b
refactor: migrate Slot class to ES6 and improve delegate method hand…
thibaultzanini Jul 24, 2025
7e2ed6d
feat: add loading attribute to Image component
thibaultzanini Jul 25, 2025
5de0483
feat: added VisualShape enum
thibaultzanini Jul 18, 2025
9047f75
feat: added VisualSize enum
thibaultzanini Jul 18, 2025
5d85b89
feat: added values getter on enum
thibaultzanini Jul 18, 2025
138b47b
chore: add prettierrc config
thibaultzanini Jul 18, 2025
423b0d6
feat: added SegmentedControl component
thibaultzanini Jul 18, 2025
5f8e82c
chore: add fixme comments
thibaultzanini Jul 18, 2025
70b7aee
feat: bunch of improvements for the SegmentedControl component
thibaultzanini Jul 21, 2025
9ee0180
feat: bunch of improvements for the SegmentedControl component
thibaultzanini Jul 24, 2025
9e4a565
feat: minor improvements
thibaultzanini Jul 25, 2025
87ffe0d
feat: minor css optimizations
thibaultzanini Jul 25, 2025
2299e6e
feat: enable animations after the first draw in SegmentedControl
thibaultzanini Jul 25, 2025
2d6d86f
feat: add user-drag prevention for images in SegmentedControl
thibaultzanini Jul 25, 2025
d3b99d8
chore: minor improvements
thibaultzanini Jul 25, 2025
0c6cae6
feat: minor css improvements
thibaultzanini Jul 28, 2025
38c0e57
refactor: enum usage in visual shape and size
thibaultzanini Jul 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"tabWidth": 4,
"printWidth": 120
}
146 changes: 99 additions & 47 deletions core/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -1280,9 +1280,9 @@ Montage.defineProperty(Montage.prototype, "version", {
* This is an evolution to progressively remove the reliance on the additional
* serializable property set on JS PropertyDescritpors, and instead relay on setting in ObjectDescriptors
* property descriptors. The range of value is unusual as it is a blend of string and boolean....
*
*
* Posible values: "reference" | "value" | "auto" | false,
*
*
* @type {string | boolean} .
*/

Expand All @@ -1303,7 +1303,7 @@ Montage.defineProperty(Montage.prototype, "_buildSerializablePropertyNames", {

let _serializablePropertyNames,
legacySerializablePropertyNames = Montage.getSerializablePropertyNames(this);

Montage.defineProperty(Object.getPrototypeOf(this), "_serializablePropertyNames", {
value: (_serializablePropertyNames = this.objectDescriptor
? this.objectDescriptor.serializablePropertyDescriptors.map((aPropertyDescriptor) => {
Expand Down Expand Up @@ -1356,60 +1356,112 @@ Montage.defineProperty(Montage, "equals", {
});

/**
* This method calls the method named with the identifier prefix if it exists.
* Example: If the name parameter is "shouldDoSomething" and the caller's identifier is "bob", then
* this method will try and call "bobShouldDoSomething"
* Calls the delegate method with the specified name if it exists on the delegate object.
* Uses caching to avoid repeated method lookups since delegate methods are unlikely to be removed dynamically.
*
* TODO: Cache!!!! We're unlikely to remove a delegate method dynamically, so we should avoid checking all
* that and just cache the function found, using a weak map, so don't retain delegates.
* The method first attempts to find a method with the pattern `{identifier}{Name}` on the delegate,
* where the first letter of the name parameter is capitalized. If not found, it falls back to
* looking for a method with the exact name.
*
* @function Montage#callDelegateMethod
* @param {string} name
*/
* @param {string} name - The name of the delegate method to call
* @param {...*} args - Additional arguments to pass to the delegate method
* @returns {*} The return value of the delegate method, or undefined if no method was found or no delegate exists
*
* @example
* // If this.identifier is "bob" and name is "shouldDoSomething"
* // This will try to call "bobShouldDoSomething" on the delegate
* const result = this.callDelegateMethod("shouldDoSomething", arg1, arg2);
*/
Montage.defineProperty(Montage.prototype, "callDelegateMethod", {
value: function (name) {
var delegate = this.delegate, delegateFunction;
const delegateFunction = this.getDelegateMethod(name);

if (delegate) {
if (this.delegate && delegateFunction) {
const [, ...rest] = arguments;
return delegateFunction.call(this.delegate, ...rest);
}
}
});

var delegateFunctionName = this.identifier;
delegateFunctionName += name.toCapitalized();
/**
* Checks whether the delegate object has a method that responds to the specified name.
* This method uses the same lookup logic as callDelegateMethod and getDelegateMethod.
*
* @function Montage#respondsToDelegateMethod
* @param {string} name - The name of the delegate method to check for
* @returns {boolean} True if the delegate has a method that responds to the given name, false otherwise
*
* @example
* // Check if delegate can handle "shouldDoSomething"
* if (this.respondsToDelegateMethod("shouldDoSomething")) {
* this.callDelegateMethod("shouldDoSomething", data);
* }
*/
Montage.defineProperty(Montage.prototype, "respondsToDelegateMethod", {
value: function (name) {
return typeof this.getDelegateMethod(name) === FUNCTION;
}
});

if (
typeof this.identifier === "string" &&
typeof delegate[delegateFunctionName] === FUNCTION
) {
delegateFunction = delegate[delegateFunctionName];
} else if (typeof delegate[name] === FUNCTION) {
delegateFunction = delegate[name];
}
// WeakMap to cache delegate methods - won't retain delegates when they're garbage collected
const delegateMethodCache = new WeakMap();

if (delegateFunction) {
//Using modern JS:
// Destructure the array to skip the first element
const [, ...rest] = arguments;
return delegateFunction.call(delegate, ...rest);

// if(arguments.length === 2) {
// return delegateFunction.call(delegate,arguments[1]);
// }
// else if(arguments.length === 3) {
// return delegateFunction.call(delegate,arguments[1],arguments[2]);
// }
// else if(arguments.length === 4) {
// return delegateFunction.call(delegate,arguments[1],arguments[2],arguments[3]);
// }
// else if(arguments.length === 5) {
// return delegateFunction.call(delegate,arguments[1],arguments[2],arguments[3],arguments[4]);
// }
// else {
// // remove first argument
// ARRAY_PROTOTYPE.shift.call(arguments);
// return delegateFunction.apply(delegate, arguments);
// }
}
/**
* Retrieves a delegate method by name, using caching for performance optimization.
*
* The method searches for delegate methods in the following order:
* 1. First tries `{identifier}{Name}` where Name is the capitalized version of the name parameter
* 2. Falls back to the exact method name if the prefixed version doesn't exist
*
* Results are cached using a WeakMap to avoid repeated lookups while ensuring
* delegates can still be garbage collected when no longer referenced.
*
* @function Montage#getDelegateMethod
* @param {string} name - The name of the delegate method to retrieve
* @returns {Function|undefined} The delegate method function if found, undefined otherwise
*
* @example
* // If this.identifier is "list" and name is "shouldSelectItem"
* // This will look for "listShouldSelectItem" first, then "shouldSelectItem"
* const method = this.getDelegateMethod("shouldSelectItem");
* if (method) {
* method.call(this.delegate, item);
* }
*/
Montage.defineProperty(Montage.prototype, "getDelegateMethod", {
value: function (name) {
if (!this.delegate) return;

const delegate = this.delegate;
let delegateCache = delegateMethodCache.get(delegate);

if (!delegateCache) {
delegateCache = new Map();
delegateMethodCache.set(delegate, delegateCache);
}
}

// Check if we already have the function cached
if (delegateCache.has(name)) return delegateCache.get(name);

let delegateFunctionName = this.identifier;
let delegateFunction;

delegateFunctionName += name.toCapitalized();

if (typeof this.identifier === "string" && typeof delegate[delegateFunctionName] === FUNCTION) {
delegateFunction = delegate[delegateFunctionName];
} else if (typeof delegate[name] === FUNCTION) {
delegateFunction = delegate[name];
}

// Cache the delegate function if it exists
if (delegateFunction) {
delegateCache.set(name, delegateFunction);
}

return delegateFunction;
},
});

// Property Changes
Expand Down
6 changes: 6 additions & 0 deletions core/enum.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ exports.Enum = Montage.specialize( /** @lends Enum# */ {
}
}
}
},

values: {
get: function () {
return this._members.map((member) => this[member]);
}
}
});

14 changes: 14 additions & 0 deletions core/enums/visual-shape.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { Enum } = require("../enum");

const shapes = ["rectangle", "rounded", "pill"];
const classNames = shapes.map((shape) => `mod--shape-${shape}`);

/**
* @typedef {"rectangle"|"rounded"|'pill'} VisualShape
*/
const VisualShape = new Enum().initWithMembersAndValues(shapes, shapes);

const VisualShapeClassNames = new Enum().initWithMembersAndValues(shapes, classNames);

exports.VisualShapeClassNames = VisualShapeClassNames;
exports.VisualShape = VisualShape;
14 changes: 14 additions & 0 deletions core/enums/visual-size.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { Enum } = require("../enum");

const sizes = ["small", "medium", "large"];
const classNames = sizes.map((size) => `mod--size-${size}`);

/**
* @typedef {"small"|"medium"|'large'} VisualSize
*/
const VisualSize = new Enum().initWithMembersAndValues(sizes, sizes);

const VisualSizeClassNames = new Enum().initWithMembersAndValues(sizes, classNames);

exports.VisualSizeClassNames = VisualSizeClassNames;
exports.VisualSize = VisualSize;
2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ <h4>Components:</h4>
<a href="ui/toggle.mod/teach/index.html">Toggle</a>
<a href="ui/tree-list.mod/teach/index.html">TreeList</a>
<a href="ui/virtual-list.mod/teach/index.html">VirtualList</a>
<a href="ui/image.mod/teach/index.html">Image</a>
<a href="ui/segmented-control.mod/teach/index.html">Segmented bar</a>

<h4>Managers:</h4>
<a href="core/drag/drag.mod/teach/index.html">Drag & Drop</a>
Expand Down
93 changes: 52 additions & 41 deletions ui/image.mod/image.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,59 @@
/**
@module "mod/ui/native/image.mod"
@requires mod/ui/component
@requires mod/ui/native-control
*/
var Component = require("ui/component").Component;
* @module "mod/ui/native/image.mod"
* @requires mod/ui/component
* @requires mod/ui/native-control
*/
const { Component } = require("ui/component");

/**
* Wraps the a &lt;img> element with binding support for its standard attributes.
@class module:"mod/ui/native/image.mod".Image
@extends module:mod/ui/control.Control
* @class module:"mod/ui/native/image.mod".Image
* @extends module:mod/ui/control.Control
*/
exports.Image = Component.specialize({
hasTemplate: {value: true }
const Image = class Image extends Component {
hasTemplate = true;
};

/** @lends module:"mod/ui/native/image.mod".Image */
Image.addAttributes({
/**
* A text description to display in place of the image.
* @type {string}
* @default null
*/
alt: null,

/**
* The height of the image in CSS pixels.
* @type {number}
* @default null
*/
height: null,

/**
* The URL where the image is located.
* @type {string}
* @default null
*/
src: null,

/**
* The width of the image in CSS pixels.
* @type {number}
* @default null
*/
width: null,

/**
* The loading strategy for the image.
* @type {string}
* @default "eager"
* @values ["eager", "lazy"]
*/
loading: {
dataType: "string",
value: "eager",
},
});

exports.Image.addAttributes(/** @lends module:"mod/ui/native/image.mod".Image */{

/**
A text description to display in place of the image.
@type {string}
@default null
*/
alt: null,

/**
The height of the image in CSS pixels.
@type {number}
@default null
*/
height: null,

/**
The URL where the image is located.
@type {string}
@default null
*/
src: null,

/**
The width of the image in CSS pixels.
@type {number}
@default null
*/
width: null


});
exports.Image = Image;
1 change: 1 addition & 0 deletions ui/image.mod/teach/assets/banana.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions ui/image.mod/teach/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Teach SegmentControl Mod</title>

<script src="../../../montage.js"></script>
<script type="text/mod-serialization">
{
"owner": {
"prototype": "mod/ui/loader.mod"
}
}
</script>
</head>
<body>
<span class="loading"></span>
</body>
</html>
10 changes: 10 additions & 0 deletions ui/image.mod/teach/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "teach-segmented-bar-mod",
"private": true,
"dependencies": {
"mod": "*"
},
"mappings": {
"mod": "../../../"
}
}
Loading