import API from '../api/ecosystem-base.js';
import {Menu_A} from './as-menu.js';
import {Component_A} from './basic/as-component.js';
import {FP_Hamburger_A} from './fp-ext/fp-hamburger-ext.js';
import {ErrorCode} from './../exception/exceptionDesc.js';
/**
* @description Properties accepted by the Hamburger component, defining its appearance, behavior, and lifecycle hooks.
* This class focuses on the specific properties of the Hamburger component.
* Since it inherits from Accessor_A, all basic properties (e.g., height, width) are available but documented in the Accessor_A part.
* @typedef {object} TComponents.HamburgerProps
* @prop {object} [options] Additional options for the hamburger component.
* Items in this object include:
* - **responsive** (boolean, default: false): Whether the hamburger should be responsive.
* @prop {string} [tips] Tooltip text for the component.
* @prop {Function} [onCreated] Lifecycle hook invoked after component instantiation.
* @prop {Function} [onMounted] Lifecycle hook invoked after component is attached to the DOM.
* @prop {string} [position] CSS `position` property.
* @prop {number} [width] Width of the menu container.
* @prop {number} [height] Height of the menu container.
* @prop {number} [top] Top offset in pixels.
* @prop {number} [left] Left offset in pixels.
* @prop {number} [borderRadius] Border radius in pixels.
* @prop {number} [rotation] Rotation in degrees.
* @prop {number} [zIndex] CSS `z-index`.
* @prop {string} [border] CSS border shorthand.
* @prop {string} [color] CSS text color.
* @prop {string} [backgroundColor] CSS background color.
* @prop {object} [font] Font configuration.
* This object controls text appearance:
* - **fontSize** (number, default: 12): Font size in pixels.
* - **fontFamily** (string, default: 'Segoe UI'): Font family name.
* - **style** (object): Font style configuration, containing `fontStyle`, `fontWeight`, `textDecoration`.
* @prop {string} [size] Preset size key for the component.
* @prop {string} [styleTemplate] Key of a style template to apply.
* @prop {boolean} [useTitle] Whether to reserve space for the title.
* @prop {string} [title] Title displayed next to the hamburger icon when the menu is opened.
* @prop {boolean} [alwaysVisible] Whether the hamburger menu stays visible even when closed.
* @prop {number} [activeViewIndex] Zero-based index of the currently active view.
* @prop {Function} [onChange] Callback triggered when the active view changes.
* @prop {boolean} [useViewIcon] Whether to display an icon alongside each view label.
* @prop {TComponents.ViewProps[]} [views] Array of view objects to populate the menu.
* @prop {string} [defaultState] Default state of the component.
* @prop {boolean} [expandHeightToParentBottom] Whether to expand the height to the parent's bottom.
* @memberof TComponents
*/
/**
* @ignore
*/
const logModule = 'as-hamburger';
/**
* @description Represents a Hamburger menu component.
* @class TComponents.Hamburger
* @extends TComponents.Menu_A
* @memberof TComponents
* @param {HTMLElement} parent - The parent element to which the hamburger menu is attached..
* @param {TComponents.HamburgerProps} props - The properties of the hamburger menu.
* @example
* const hamburgerInstance = new TComponents.Hamburger(document.body, {
* position: 'absolute',
* width: 200,
* height: 200,
* zIndex: 1000,
* views: [
* {
* name: 'View 1',
* icon: 'abb-icon abb-icon-home-house_32',
* content: `view_${API.generateUUID()}`,
* children: [],
* },
* ],
* activeViewIndex: 0,
* });
*
* // Render the hamburger menu
* hamburgerInstance.render();
*/
export class Hamburger extends Menu_A {
constructor(parent, props) {
super(parent, props);
/**
* @instance
* @private
* @type {TComponents.HamburgerProps}
*/
this._props;
}
/**
* @description An array of Hamburger's views, each containing properties
* like `id`, `name`, `content`, `active`, and `child`.
* @member {any[]} TComponents.Hamburger#views
* @instance
* @example
* const hamburgerInstance = new TComponents.Hamburger(document.body, {
* position: 'absolute',
* width: 200,
* height: 200,
* zIndex: 1000,
* views: [
* {
* name: 'View 1',
* icon: 'abb-icon abb-icon-home-house_32',
* content: `view_${API.generateUUID()}`,
* children: [],
* },
* {
* name: 'View 2',
* icon: 'abb-icon abb-icon-folder-open_32',
* content: `view_${API.generateUUID()}`,
* children: [],
* },
* ],
* activeViewIndex: 0,
* });
*
* // Render the hamburger menu
* hamburgerInstance.render();
*
* // Get the views
* const views = hamburgerInstance.views;
* console.log(views);
*/
get views() {
return this._views;
}
/**
* @returns {boolean}
*/
get alwaysVisible() {
return this._props.alwaysVisible;
}
/**
* @description The visibility state of the hamburger menu.
* @member {boolean} TComponents.Hamburger#alwaysVisible
* @instance
* @example
* const hamburgerInstance = new TComponents.Hamburger(document.body, {
* position: 'absolute',
* width: 200,
* height: 200,
* zIndex: 1000,
* views: [
* {
* name: 'View 1',
* icon: 'abb-icon abb-icon-home-house_32',
* content: `view_${API.generateUUID()}`,
* children: [],
* },
* ],
* activeViewIndex: 0,
* });
*
* // Render the hamburger menu
* hamburgerInstance.render();
*
* // Get the current visibility state
* const isAlwaysVisible = hamburgerInstance.alwaysVisible;
* console.log(isAlwaysVisible); // true
*
* // Set the visibility state to false
* hamburgerInstance.alwaysVisible = false;
*/
set alwaysVisible(b) {
this.setProps({alwaysVisible: b});
}
/**
* @deprecated
*/
get activeView() {
return this.activeViewName;
}
/**
* @deprecated Use `activeViewName` instead {@link TComponents.Hamburger#activeViewName}.
* @description Gets the currently active view.
* @member {string} TComponents.Hamburger#activeView
* @instance
* @throws Will throw an error if the hamburger menu is not initialized.
* @example
* const hamburgerInstance = new TComponents.Hamburger(document.body, {
* position: 'absolute',
* width: 200,
* height: 200,
* zIndex: 1000,
* views: [
* {
* name: 'View 1',
* icon: 'abb-icon abb-icon-home-house_32',
* content: `view_${API.generateUUID()}`,
* children: [],
* },
* ],
* activeViewIndex: 0,
* });
*
* // Render the hamburger menu
* hamburgerInstance.render();
*
* // Get the current active view
* const currentActiveView = hamburgerInstance.activeView;
* console.log(currentActiveView); // 'View 1'
*/
set activeView(name) {
this.activeViewName = name;
}
/**
* @returns {string}
*/
get activeViewName() {
const index = this._props.activeViewIndex;
if (index >= 0) {
const view = this.views[index];
return view.name;
} else {
return '';
}
}
/**
* @description Sets the active view by its display name.
* The comparison is performed against the resolved text from Component_A.dynamicText.
* @member {string} TComponents.Hamburger#activeViewName
* @instance
* @param {string} name - The target view's display name.
* @throws {Error} If the hamburger menu is not initialized (render not completed).
* @example
* const hamburgerInstance = new TComponents.Hamburger(document.body, {
* position: 'absolute',
* width: 200,
* height: 200,
* zIndex: 1000,
* views: [
* {
* name: 'View 1',
* icon: 'abb-icon abb-icon-home-house_32',
* content: `view_${API.generateUUID()}`,
* children: [],
* },
* ],
* activeViewIndex: 0,
* });
*
* // Render the hamburger menu
* hamburgerInstance.render();
*
* // Get the current active view name
* const currentActiveView = hamburgerInstance.activeViewName;
* console.log(currentActiveView); // 'View 1'
*/
set activeViewName(name) {
if (!this.hamburgerMenu) throw new Error('hamburgerMenu not initialized yet. Please render the component first');
const index = this.views.findIndex((item) => Component_A.dynamicText(item.name, this) === name);
this.activeViewIndex = index;
}
/**
* @returns {number}
*/
get activeViewIndex() {
return this._props.activeViewIndex;
}
/**
* @description Sets the active view index and updates the active view in the Hamburger component.
* @member {number} TComponents.Hamburger#activeViewIndex
* @instance
* @param {number} t - The new active view index.
* @throws {Error} If the new index is invalid or if the active view content is invalid.
* @example
* const hamburgerInstance = new TComponents.Hamburger(document.body, {
* position: 'absolute',
* width: 200,
* height: 200,
* zIndex: 1000,
* views: [
* {
* name: 'View 1',
* icon: 'abb-icon abb-icon-home-house_32',
* content: `view_${API.generateUUID()}`,
* children: [],
* },
* ],
* activeViewIndex: 0,
* });
*
* // Render the hamburger menu
* hamburgerInstance.render();
*
* // Get the current active view index
* const currentActiveViewIndex = hamburgerInstance.activeViewIndex;
* console.log(currentActiveViewIndex); // 0
*/
set activeViewIndex(t) {
const index = this.checkActiveViewIndex(t);
if (index === null) return;
// Set up the hamburger active view.
this.setProps({activeViewIndex: index});
}
/**
* @description Returns default properties for the hamburger menu.
* @member TComponents.Hamburger#defaultProps
* @method
* @protected
* @returns {TComponents.HamburgerProps} The default properties.
*/
defaultProps() {
return {
options: {
responsive: false,
},
tips: '',
onCreated: '',
onMounted: '',
position: 'static',
width: 200,
height: 200,
top: 0,
left: 0,
borderRadius: 4,
rotation: 0,
zIndex: 0,
border: '1px solid #dbdbdb',
color: 'rgba(0,0,0,1)',
backgroundColor: 'rgba(255,255,255,1)',
font: {
fontSize: 12,
fontFamily: 'Segoe UI',
style: {
fontStyle: 'normal',
fontWeight: 'normal',
textDecoration: 'none',
},
},
size: '',
styleTemplate: '',
useTitle: true,
title: '',
alwaysVisible: true,
activeViewIndex: 0,
onChange: '',
useViewIcon: true,
views: [
{
name: Component_A.dynamicProperty('Item 0'),
content: `View_${API.generateUUID()}`,
id: null,
icon: 'abb-icon abb-icon-abb_robot-tool_32',
children: [],
},
],
defaultState: 'show_enable',
expandHeightToParentBottom: false,
};
}
/**
* @description Initializes the hamburger menu.
* @member TComponents.Hamburger#onInit
* @method
* @returns {void}
*/
onInit() {
// Process the views prior to instantiating the new hamburger object.
this._initViews();
// Initialize the hamburger menu.
this.hamburgerMenu = new FP_Hamburger_A();
this.viewId.clear();
this.views.forEach((view) => {
const dom = this._getDom(view.content, view.id, view.name);
view.id = this.hamburgerMenu.addView(Component_A.dynamicProperty(view.name, this), dom, view.icon);
this.viewId.set(view.id, view.name);
});
}
/**
* @description Generates the HTML markup for the hamburger menu.
* @member TComponents.Hamburger#markup
* @method
* @returns {string} The HTML markup.
*/
markup() {
return /*html*/ `<div class="tc-hamburger"></div>`;
}
/**
* @description Maps components to their identifiers.
* @member TComponents.Hamburger#mapComponents
* @method
* @returns {object}
*/
mapComponents() {
if (!Array.isArray(this._children)) return {};
for (let i = 0; i < this._children.length; i++) {
if (i === this._props.activeViewIndex) {
const instance = this._children[i];
return {children: instance};
}
}
return {};
}
/**
* @description Renders the hamburger menu.
* @member TComponents.Hamburger#onRender
* @method
* @returns {void}
*/
onRender() {
try {
this.hamburgerMenu.attachToElement(this.find('.tc-hamburger'));
this.hamburgerMenu.title = Component_A.dynamicProperty(this._props.title, this);
this.hamburgerMenu.alwaysVisible = this._props.alwaysVisible;
this.hamburgerMenu.onchange = this._cbOnChange.bind(this);
this.hamburgerMenu.onclickmenu = this._cbOnClickMenu.bind(this);
// Refine the style of the hamburger menu.
this.find('.fp-components-hamburgermenu-a-menu__container').style.setProperty('z-index', '3');
this.find('.fp-components-hamburgermenu-a-button-container').style.setProperty('z-index', '99');
this.find('.fp-components-hamburgermenu-a-container').style.cssText = `
align-items: normal;
border: ${this._props.border};
border-radius: ${this._props.borderRadius}px;
`;
this.find('.fp-components-hamburgermenu-a-menu__wrapper').style.cssText = `
color: ${this._props.color};
font-family: ${this._props.font.fontFamily};
font-size: ${this._props.font.fontSize}px;
font-style: ${this._props.font.style.fontStyle};
font-weight: ${this._props.font.style.fontWeight};
text-decoration: ${this._props.font.style.textDecoration};
`;
// fix: the delayed rendering of menu icon
this.all('.fp-components-hamburgermenu-a-menu__button').forEach((btn) => {
btn.style.color = this._props.color;
});
this.find('.fp-components-hamburgermenu-a-container__content').classList.add(
'hamburger__container',
'flex-row',
'justify-stretch',
);
this.find('.fp-components-hamburgermenu-a-container__content').style.backgroundColor =
this._props.backgroundColor;
this.container.classList.add('tc-container');
this.container.classList.add('is-container');
this.container.classList.add('hamburger-base-style');
// Set up the hamburger active view.
this._setActiveView();
this._addTips();
} catch (e) {
// Runtime errors: write specific content to the log, throw error code
Logger.e(
logModule,
`${ErrorCode.FailedToRunOnRender}`,
`Error happens on onRender of hamburger component ${this.compId}.`,
e,
);
throw new Error(ErrorCode.FailedToRunOnRender, {cause: e});
}
}
/**
* @description Handles clicks on the menu bar items, updating the active view.
* @member TComponents.Hamburger~_cbOnClickMenu
* @method
* @private
* @param {string} oldId - The previous id of the active view.
* @param {string} id - The current id of the active view.
* @returns {void}
*/
_cbOnClickMenu(oldId, id) {
if (oldId === id) return;
else {
// If the clicked item is already active, no change is needed.
const index = this.views.findIndex((v) => v.id === id);
this.activeViewIndex = index;
}
}
/**
* @description Callback invoked when the active view of the hamburger menu changes.
* @member TComponents.Hamburger~_cbOnChange
* @method
* @private
* @param {string} oldId - The previous id of the active view.
* @param {string} id - The current id of the active view.
* @returns {void}
*/
_cbOnChange(oldId, id) {
// If oldId is invalid, it means the hamburger container has just been initialized.
if (!oldId) return;
try {
var fn = Component_A.genFuncTemplateWithPopup(this._props.onChange, this);
} catch (errorCode) {
return;
}
try {
if (typeof fn === 'function') fn(oldId, id);
} catch (e) {
// User operation failed, log detailed error stack information to the console
Logger.e(
logModule,
`${ErrorCode.FailedToExecuteEvent}`,
`Failed to execute the onChange event of component ${this.compId} defined by user.`,
e,
);
Component_A.processEventError(e, 'onChange');
}
}
/**
* @description Removes the “active” style from all hamburger menu buttons, then activates the view at the specified index.
* @member TComponents.Hamburger#_setActiveView
* @method
* @protected
* @throws {Error} If the hamburger menu is not initialized or the target view cannot be found.
* @return {void}
*/
_setActiveView() {
if (!this.hamburgerMenu) {
Logger.e(logModule, ErrorCode.FailedToInitComponent, 'The hamburger component is not initialized.');
throw ErrorCode.FailedToInitComponent;
}
const idx = this._props.activeViewIndex || 0;
const view = this.views[idx];
if (!view || !view.id) {
Logger.e(logModule, ErrorCode.NotfoundTargetView, 'The target view is not found or active index is invalid.');
throw ErrorCode.NotfoundTargetView;
}
if (this.hamburgerMenu.activeView === view.id) return;
this.all('.fp-components-hamburgermenu-a-menu__button--active').forEach((el) => {
el.classList.remove('fp-components-hamburgermenu-a-menu__button--active');
});
this.hamburgerMenu.activeView = view.id;
}
}
/**
* @description Add css properties to the component
* @alias loadCssClassFromString
* @member TComponents.Hamburger.loadCssClassFromString
* @method
* @static
* @param {string} css - The css string to be loaded into style tag
* @example
* TComponents.Hamburger.loadCssClassFromString(/*css* / `
* .tc-hamburger {
* height: 100%;
* width: 100%;
* }`
* );
*/
Hamburger.loadCssClassFromString(/*css*/ `
.tc-hamburger {
height: 100%;
width: 100%;
min-width: 0px;
min-height: 0px;
padding: 0px;
margin: 0px;
}
.tc-hamburger .fp-components-hamburgermenu-a-container {
height: 100%;
width: 100%;
min-width: 0px;
min-height: 0px;
padding: 0px;
margin: 0px;
overflow: hidden;
}
.tc-hamburger .hamburger-base-style {
background-color: transparent !important;
border: 1px solid var(--t-color-GRAY-30);
border-top: none;
border-buttom: none;
border-right: none;
}
.tc-hamburger .hamburger__container {
position: relative;
max-width: inherit;
overflow-y: auto;
}
.tc-hamburger .fp-components-hamburgermenu-a-menu__container {
text-decoration: inherit;
}
.tc-hamburger .fp-components-hamburgermenu-a-menu__title-container {
min-height: unset;
padding-left: 48px;
}
.tc-hamburger .fp-components-hamburgermenu-a-menu {
min-height: unset;
padding-right: 0px;
}
.tc-hamburger .fp-components-hamburgermenu-a-button-container {
position: absolute;
}
.tc-hamburger .fp-components-hamburgermenu-a-menu__button {
min-height: 48px;
}
.tc-hamburger .fp-components-hamburgermenu-a-menu__wrapper {
z-index: 2;
}
.tc-hamburger .fp-components-hamburgermenu-a-container__content {
z-index: 1;
}
.tc-hamburger .fp-components-hamburgermenu-a-menu__button-icon {
display: flex;
justify-content: center;
align-items: center;
height: 32px;
width: 32px;
min-height: unset;
}
.tc-hamburger .fp-components-hamburgermenu-a-button-wrap {
width: 48px;
height: 48px;
}
`);
/**
* Add disabled css properties to hamburger-type components.
*/
Hamburger.loadCssClassFromString(`
.hamburgermenu-a-disabled {
opacity: 0.7;
cursor: not-allowed !important;
}
.hamburgermenu-a-disabled .fp-components-hamburgermenu-a-menu__wrapper,
.hamburgermenu-a-disabled .fp-components-hamburgermenu-a-container__content {
pointer-events: none;
}
`);