(function() {
/**
* @namespace bubble
* @memberof Help4.control2
*/
Help4.control2.bubble = {};
/**
* @namespace header
* @memberof Help4.control2.bubble
*/
Help4.control2.bubble.header = {};
/**
* @namespace content
* @memberof Help4.control2.bubble
*/
Help4.control2.bubble.content = {};
/**
* @namespace footer
* @memberof Help4.control2.bubble
*/
Help4.control2.bubble.footer = {};
/** @enum {string} */
Help4.control2.bubble.ANIMATION_TYPES = {
'default': 'default', // XXX: rename to "none"
expand: 'expand',
fade: 'fade',
pulse: 'pulse',
scaleup: 'scaleup',
shake: 'shake',
shrink: 'shrink',
wobble: 'wobble'
}
/** @enum {string} */
Help4.control2.bubble.SIZES = {
xs: 'xs',
s: 's',
m: 'm',
l: 'l',
xl: 'xl'
}
/** @enum {string} */
Help4.control2.bubble.HEADER_LAYOUT = {
CaptionTranslateClose: 'CaptionTranslateClose',
Panel: 'Panel'
}
/** @enum {string} */
Help4.control2.bubble.CONTENT_LAYOUT = {
Html: 'Html',
Panel: 'Panel',
Selection: 'Selection'
}
/** @enum {string} */
Help4.control2.bubble.FOOTER_LAYOUT = {
Checkbox: 'Checkbox',
Panel: 'Panel',
Close: 'Close',
Apply: 'Apply'
}
/**
* @typedef {Object} Help4.control2.bubble.AlignParams
* @property {Help4.control2.AreaXYWH} [here] - possible static position
* @property {boolean} [ignoreArea] - do ignore the area of the container
* @property {*} [orientation] - orientation
* @property {boolean} [allowFallbacks] - allow to use fallbacks
*/
/**
* @typedef {Object} Help4.control2.bubble.Alignment
* @property {Help4.control2.PositionXY} point
* @property {string} orientation
*/
/**
* @typedef {Help4.control2.Control.Params} Help4.control2.bubble.Bubble.Params
* @property {boolean} [showArrow = false] - whether to show the arrow
* @property {boolean} [showAfterAlign = true] - automatically set visible after align
* @property {boolean} [resizableAlign = false] - allow content resize during align*
* @property {Help4.control2.bubble.ANIMATION_TYPES} [animationType = 'default'] - the start animation type
* @property {boolean} [enableTranslation = false] - whether to show translate button
* @property {boolean} [activeTranslation = false] - whether translate button is active
* @property {?Help4.control2.bubble.HEADER_LAYOUT} [headerLayout = null] - layout template for header
* @property {?Help4.control2.bubble.CONTENT_LAYOUT} [contentLayout = null] - layout template for content
* @property {?Help4.control2.bubble.FOOTER_LAYOUT} [footerLayout = null] - layout template for footer
* @property {?Object} [parent = null] - parent bubble control
* @property {Help4.control2.bubble.SIZES} [size] - bubble size
* @property {boolean} [modal = false] - whether bubble is modal
*/
/**
* Base bubble control.
* @abstract
* @augments Help4.control2.Control
* @property {boolean} showArrow - whether to show the arrow
* @property {boolean} showAfterAlign - automatically set visible after align
* @property {boolean} resizableAlign - allow content resize during align
* @property {Help4.control2.bubble.ANIMATION_TYPES} animationType - the start animation type
* @property {boolean} enableTranslation - whether to show translate button
* @property {boolean} activeTranslation - whether translate button is active
* @property {Help4.service.zoom.ZoomService} zoomService
* @property {?Help4.control2.bubble.HEADER_LAYOUT} headerLayout - layout template for header
* @property {?Help4.control2.bubble.CONTENT_LAYOUT} contentLayout - layout template for content
* @property {?Help4.control2.bubble.FOOTER_LAYOUT} footerLayout - layout template for footer
* @property {?Object} parent - parent bubble control
* @property {Help4.control2.bubble.SIZES} size - bubble size
* @property {boolean} modal - whether bubble is modal
* @property {?Help4.control2.bubble.header.BubbleHeader} _header
* @property {?Help4.control2.bubble.content.BubbleContent} _content
* @property {?Help4.control2.bubble.footer.BubbleFooter} _footer
* @property {?Help4.control2.bubble.BubbleCover} _cover
* @property {?Help4.control2.bubble.BubbleArrow} _arrow
* @property {?Help4.control2.bubble.Bubble} _child
* @property {Function} _resizeObserver
*/
Help4.control2.bubble.Bubble = class extends Help4.control2.Control {
/**
* @override
* @param {Help4.control2.bubble.Bubble.Params} [params]
* @param {Help4.jscore.ControlBase.Params} [derived]
*/
constructor(params, derived) {
const {ANIMATION_TYPES} = Help4.control2.bubble;
params ||= {};
params.animationType ||= ANIMATION_TYPES.default;
const size = params.size || derived?.params?.size?.init || Help4.DEFAULTS.bubbleSize.d;
let css = `control-bubble size-${size} ani-${params.animationType}`;
if (params.enableTranslation) css += ' mltranslation';
const resizeObserver = new ResizeObserver(() => this.resetAlignmentCache());
const T = Help4.jscore.ControlBase.TYPES;
super(params, {
params: {
role: {init: 'dialog'},
tabIndex: {init: 0},
visible: {init: false}, // will most often be shown after align to avoid jumping on screen
showArrow: {type: T.boolean},
showAfterAlign: {type: T.boolean, init: true, readonly: true}, // auto show after align
resizableAlign: {type: T.boolean, readonly: true}, // resizable bubble-h during align
animationType: {type: T.string, readonly: true},
// keep this information in sync with header, content, footer!
enableTranslation: {type: T.boolean},
activeTranslation: {type: T.boolean},
zoomService: {type: T.instance, init: null, readonly: true},
headerLayout: {type: T.string_null, readonly: true},
contentLayout: {type: T.string_null, readonly: true},
footerLayout: {type: T.string_null, readonly: true},
parent: {type: T.instance, readonly: true}, // parent bubble
size: {type: T.string, init: size, readonly: true},
modal: {type: T.boolean, readonly: true},
autoFocus: {init: true},
autoAlign: {type: T.boolean, readonly: true}
},
statics: {
_header: {},
_content: {},
_footer: {},
_cover: {},
_arrow: {},
_child: {},
_resizeObserver: {init: resizeObserver, destroy: false}
},
config: {
css
},
derived
});
}
/**
* @param {Help4.service.zoom.ZoomService} zoomService
* @param {Help4.control2.bubble.Bubble} bubble
* @return {Promise<void>}
*/
static async activateZoom(zoomService, bubble) {
const {MediaWatcher} = Help4.jscore;
const dom = bubble.getDom();
const result = await MediaWatcher.observe(dom); // XRAY-1180
const images = /** @type {HTMLImageElement[]} */ result.filter(elem => elem.nodeName === 'IMG');
zoomService.activate(images);
}
/**
* @param {Help4.service.zoom.ZoomService} zoomService
* @param {Help4.control2.bubble.Bubble} bubble
* @return {Promise<void>}
*/
static deactivateZoom(zoomService, bubble) {
const images = /** @type {HTMLImageElement[]} */ [...bubble.getDom().getElementsByTagName('img')];
zoomService.deactivate(images);
}
/** @override */
_onBeforeDestroy() {
const {_onWindowClick, _onScroll} = this;
if (_onWindowClick) {
Help4.Event.stopObserving(window, {
manual: true,
eventType: 'click',
callback: _onWindowClick
});
delete this._onWindowClick;
}
if (_onScroll) {
Help4.Event.stopObserving(this.getDom(), {
manual: true,
eventType: 'scroll',
capture: true,
callback: _onScroll
});
delete this._onScroll;
}
this._resizeObserver.disconnect();
this.resetAlignmentCache();
_registerAtParent.call(this, null);
const {zoomService, constructor} = this;
zoomService && constructor.deactivateZoom(zoomService, this);
}
/**
* @override
* @param {HTMLElement} dom - control DOM
*/
_onDomCreated(dom) {
const onOutsideClick = data => {
data.type = 'outsideClick';
this._fireEvent(data);
}
const {BubbleCover, BubbleArrow} = Help4.control2.bubble;
if (this.modal) {
this._cover = this._createControl(BubbleCover, {bubble: this, visible: this.visible})
.addListener('click', onOutsideClick);
} else {
Help4.Event.observe(window, {
manual: true,
eventType: 'click',
callback: this._onWindowClick = event => {
event = Help4.Event.normalize(event);
if (!this.visible || !event) return;
if (this.getDom().contains(event.target)) {
this._fireEvent({type: 'insideClick', data: event});
} else {
onOutsideClick({data: event});
}
}
});
}
Help4.Event.observe(dom, {
manual: true,
eventType: 'scroll',
capture: true,
callback: this._onScroll = event => void this._fireEvent({type: 'scroll', data: event})
});
this._resizeObserver.observe(dom);
this._arrow = this._createControl(BubbleArrow, {
bubble: this,
visible: this.showArrow
});
this._onCreateHeader();
this._onCreateContent();
this._onCreateFooter();
dom.setAttribute('data-sap-ui-integration-popup-content', ''); // XRAY-2011
_registerAtParent.call(this, this);
}
/**
* @override
*/
_onReady() {
const {zoomService, constructor} = this;
zoomService && constructor.activateZoom(zoomService, this);
}
/**
* @param {Object} [derivedHeaderParams]
* @protected
*/
_onCreateHeader(derivedHeaderParams) {
const {headerLayout, enableTranslation} = this;
if (!headerLayout) return;
const layoutClass = _getLayoutClass(headerLayout, Help4.control2.bubble.header) || _getLayoutClass(headerLayout, window);
if (!layoutClass) return;
this._header = this._createControl(layoutClass, {
bubble: this,
showTranslation: enableTranslation,
...derivedHeaderParams
})
.addListener(['click', 'mouseover', 'mouseout'], event => void this._fireEvent(event)); // XRAY-2615, XRAY-4628
}
/**
* @param {Object} [derivedContentParams]
* @protected
*/
_onCreateContent(derivedContentParams) {
const {contentLayout} = this;
if (!contentLayout) return;
const layoutClass = _getLayoutClass(contentLayout, Help4.control2.bubble.content) || _getLayoutClass(contentLayout, window);
if (!layoutClass) return;
this._content = this._createControl(layoutClass, {
bubble: this,
...derivedContentParams
})
.addListener(['click', 'mouseover', 'mouseout'], event => void this._fireEvent(event)) // XRAY-2615, XRAY-4628
.addListener('afterSetControlTexts', (event) => {
// XRAY-5754: in case content is replaces by MLT translation we need to re-enable zoom functionality
const {zoomService, constructor} = this;
zoomService && constructor.activateZoom(zoomService, this);
});
}
/**
* @param {Object} [derivedFooterParams]
* @protected
*/
_onCreateFooter(derivedFooterParams) {
const {footerLayout} = this;
if (!footerLayout) return;
const layoutClass = _getLayoutClass(footerLayout, Help4.control2.bubble.footer) || _getLayoutClass(footerLayout, window);
if (!layoutClass) return;
this._footer = this._createControl(layoutClass, {
bubble: this,
...derivedFooterParams
})
.addListener(['mouseover', 'mouseout'], event => void this._fireEvent(event)); // XRAY-2615, XRAY-4628
}
/**
* @override
* @param {Help4.jscore.ControlBase.PropertyChangeEvent} event - the change event
*/
_applyPropertyToDom({name, value, oldValue}) {
const {_header} = this;
switch (name) {
case 'activeTranslation':
if (_header) _header[name] = value;
break;
case 'enableTranslation':
if (_header) _header.showTranslation = value;
break;
case 'showArrow':
const {_arrow} = this;
if (_arrow) _arrow.visible = value;
break;
case 'visible':
const {_cover} = this;
if (this._child) {
// only show my bubble if I have no open child
if (_cover) _cover[name] = false;
super._applyPropertyToDom({name, value: false, oldValue: false});
} else {
if (_cover) _cover[name] = value;
super._applyPropertyToDom({name, value, oldValue});
}
break;
default:
super._applyPropertyToDom({name, value, oldValue});
break;
}
}
/**
* @param {Help4.control2.bubble.Bubble} child
* @returns {Help4.control2.bubble.Bubble}
*/
setChild(child) {
if (this._child !== child) {
if (child == null || child instanceof Help4.control2.bubble.Bubble) {
this._child = child;
const value = this.visible; // do not invoke getter twice
this._applyPropertyToDom({name: 'visible', value: value, oldValue: value});
} else {
throw new Error('Child needs to be a Help4.control2.bubble.Bubble!');
}
}
return this;
}
/**
* @returns {boolean}
*/
handleESC() {
// forward ESC to child if available; otherwise close myself
const {_child} = this;
return _child
? _child.handleESC()
: (void this._fireEvent({type: 'close'})) || true;
}
/**
* @param {Help4.control2.ConnectionPoints} [connectionPoints]
* @param {Help4.control2.bubble.AlignParams} [params]
* @returns {?Help4.control2.bubble.Alignment}
*/
align(connectionPoints, params) {
connectionPoints ||= {};
const {_content, _cover} = this;
const {here, ignoreArea, orientation, allowFallbacks} = params || {};
const contentDom = _content?.getDom();
const scrollTop = contentDom ? contentDom.scrollTop : null; // XRAY-1802
let alignment = null;
if (here) {
// align exactly to a predefined area
Help4.control2.bubble.alignToArea.call(this, here);
} else {
// align to one of a list of points
alignment = Help4.control2.bubble.alignToPoints.call(this, {
connectionPoints,
ignoreArea: !!ignoreArea,
orientation,
allowFallbacks: allowFallbacks !== false
});
}
if (scrollTop !== null) contentDom.scrollTop = scrollTop; // XRAY-1802
if (_cover) _cover.align();
return alignment;
}
/**
* @override
* @returns {Help4.control2.bubble.Bubble}
*/
focus() {
if (this.mobile) return this;
const {_child} = this;
if (_child) return (void _child.focus()) || this;
// prefer to focus content for screen readers
// over just focussing the bubble
const dom = this.getDom();
Help4.Element.execAfterTransition(dom, () => !this.isDestroyed() && (this._content || dom).focus());
return this;
}
/** @returns {Help4.control2.bubble.header.BubbleHeader} */
getHeaderInstance() {
return this._header;
}
/** @returns {Help4.control2.bubble.content.BubbleContent} */
getContentInstance() {
return this._content;
}
/** @returns {Help4.control2.bubble.footer.BubbleFooter} */
getFooterInstance() {
return this._footer;
}
}
/**
* @memberof Help4.control2.bubble.Bubble#
* @param {string} layout
* @param {Object} object
* @returns {?Help4.control2.Control}
* @private
*/
function _getLayoutClass(layout, object) {
const path = layout.split('.');
let item;
while (object && (item = path.shift())) {
object = object[item];
}
return object;
}
/**
* @memberof Help4.control2.bubble.Bubble#
* @param {Help4.control2.bubble.Bubble} [bubble]
* @private
*/
function _registerAtParent(bubble) {
const {parent} = this;
if (!parent) return;
if (parent instanceof Help4.control2.bubble.Bubble) {
parent.setChild(bubble);
} else if (typeof parent === 'string') { // legacy
const instance = Help4.control.Store.get(parent);
instance?._setChild(bubble?.id || bubble);
} else {
throw new Error('Parent needs to be a Help4.control2.bubble.Bubble!');
}
}
})();