highcharts
Version:
JavaScript charting framework
373 lines (372 loc) • 14.5 kB
JavaScript
/* *
*
* Popup generator for Stock tools
*
* (c) 2009-2026 Highsoft AS
* Author: Sebastian Bochan
*
* A commercial license may be required depending on use.
* See www.highcharts.com/license
*
*
* */
;
import BaseForm from '../../../Shared/BaseForm.js';
import Color from '../../../Core/Color/Color.js';
import H from '../../../Core/Globals.js';
const { doc } = H;
import D from '../../../Core/Defaults.js';
const { getOptions } = D;
import PopupAnnotations from './PopupAnnotations.js';
import PopupIndicators from './PopupIndicators.js';
import PopupTabs from './PopupTabs.js';
import { addEvent, clamp, createElement, extend, fireEvent, pick } from '../../../Shared/Utilities.js';
/* *
*
* Functions
*
* */
/**
* Get values from all inputs and selections then create JSON.
*
* @internal
*
* @param {Highcharts.HTMLDOMElement} parentDiv
* The container where inputs and selections are created.
*
* @param {string} type
* Type of the popup bookmark (add|edit|remove).
*/
function getFields(parentDiv, type) {
const inputList = Array.prototype.slice.call(parentDiv.querySelectorAll('input')), selectList = Array.prototype.slice.call(parentDiv.querySelectorAll('select')), optionSeries = '#highcharts-select-series > option:checked', optionVolume = '#highcharts-select-volume > option:checked', linkedTo = parentDiv.querySelectorAll(optionSeries)[0], volumeTo = parentDiv.querySelectorAll(optionVolume)[0];
const fieldsOutput = {
actionType: type,
linkedTo: linkedTo && linkedTo.getAttribute('value') || '',
fields: {}
};
inputList.forEach((input) => {
const param = input.getAttribute('highcharts-data-name'), seriesId = input.getAttribute('highcharts-data-series-id');
// Params
if (seriesId) {
fieldsOutput.seriesId = input.value;
}
else if (param) {
const wrapper = input.closest('.highcharts-popup-color-wrapper'), opacityInput = wrapper?.querySelector('.highcharts-popup-opacity-percentage'), opacity = opacityInput ?
Number(opacityInput.value) / 100 : 1;
if (opacityInput) {
const rgba = Color.parse(input.value).rgba;
fieldsOutput.fields[param] = !Number.isNaN(rgba[0]) ?
`rgba(${rgba[0]},${rgba[1]},${rgba[2]},${opacity})` :
input.value;
}
else {
fieldsOutput.fields[param] = input.value;
}
}
else {
// Type like sma / ema
fieldsOutput.type = input.value;
}
});
selectList.forEach((select) => {
const id = select.id;
// Get inputs only for the parameters, not for series and volume.
if (id !== 'highcharts-select-series' &&
id !== 'highcharts-select-volume') {
const parameter = id.split('highcharts-select-')[1];
fieldsOutput.fields[parameter] = select.value;
}
});
if (volumeTo) {
fieldsOutput.fields['params.volumeSeriesID'] = volumeTo
.getAttribute('value') || '';
}
return fieldsOutput;
}
/**
* Resolve CSS 'var()', 'color-mix()' and 'rgba()' values to hex and alpha.
* @internal
*/
function resolveColorValue(value, contextElement) {
const toHex = (n) => ('0' + Math.round(n).toString(16)).slice(-2).toUpperCase();
const rgbaToHex = (value) => {
const [r, g, b, a] = Color.parse(value).rgba;
return { value: '#' + toHex(r) + toHex(g) + toHex(b), alpha: a };
};
// If 'rgb()' or 'rgba()', use built-in parser.
if (value.startsWith('rgb') || value.startsWith('rgba')) {
return rgbaToHex(value);
}
// If 'color()' or 'var()', use a dummy element and getComputedStyle.
if (value.startsWith('color') || value.startsWith('var')) {
// Create a dummy span element and get the computed style from it.
const dummy = doc.createElement('span');
dummy.style.setProperty('color', value);
contextElement.appendChild(dummy);
const computed = window.getComputedStyle(dummy).color;
contextElement.removeChild(dummy);
// Parse color(srgb r g b / a) directly to hex and alpha.
if (computed.startsWith('color')) {
const srgbMatch = computed.match(new RegExp('color\\s*\\(\\s*srgb\\s+([\\d.]+)\\s+([\\d.]+)\\s+' +
'([\\d.]+)\\s+(?:\\s*\\/\\s*([\\d.]+))?\\s*\\)'));
if (srgbMatch) {
const r = Math.round(parseFloat(srgbMatch[1]) * 255), g = Math.round(parseFloat(srgbMatch[2]) * 255), b = Math.round(parseFloat(srgbMatch[3]) * 255), alpha = srgbMatch[4] ? parseFloat(srgbMatch[4]) : 1;
return { value: '#' + toHex(r) + toHex(g) + toHex(b), alpha };
}
}
// If 'rgb()' or 'rgba()', use built-in parser.
if (computed.startsWith('rgb') || computed.startsWith('rgba')) {
return rgbaToHex(computed);
}
}
// Don't parse hex colors and other non-color values like contrast, none.
return { value, alpha: 1 };
}
/* *
*
* Class
*
* */
/** @internal */
class Popup extends BaseForm {
/* *
*
* Constructor
*
* */
constructor(parentDiv, iconsURL, chart) {
super(parentDiv, iconsURL);
this.chart = chart;
this.lang = (getOptions().lang.navigation || {}).popup || {};
addEvent(this.container, 'mousedown', () => {
const activeAnnotation = chart &&
chart.navigationBindings &&
chart.navigationBindings.activeAnnotation;
if (activeAnnotation) {
activeAnnotation.cancelClick = true;
const unbind = addEvent(doc, 'click', () => {
setTimeout(() => {
activeAnnotation.cancelClick = false;
}, 0);
unbind();
});
}
});
}
/* *
*
* Functions
*
* */
/**
* Create input with label.
*
* @param {string} option
* Chain of fields i.e params.styles.fontSize separated by the dot.
*
* @param {string} indicatorType
* Type of the indicator i.e. sma, ema...
*
* @param {HTMLDOMElement} parentDiv
* HTML parent element.
*
* @param {Highcharts.InputAttributes} inputAttributes
* Attributes of the input.
*
* @return {HTMLInputElement}
* Return created input element.
*/
addInput(option, indicatorType, parentDiv, inputAttributes) {
const optionParamList = option.split('.'), optionName = optionParamList[optionParamList.length - 1], lang = this.lang, inputName = 'highcharts-' + indicatorType + '-' + pick(inputAttributes.htmlFor, optionName);
if (!optionName.match(/^\d+$/)) {
// Add label
createElement('label', {
htmlFor: inputName,
className: inputAttributes.labelClassName
}, void 0, parentDiv).appendChild(doc.createTextNode(lang[optionName] || optionName));
}
if (inputAttributes.type === 'color' && this.chart?.container) {
return this.createColorInput(option, inputName, inputAttributes, parentDiv, this.chart.container);
}
// Add input
const input = createElement('input', {
name: inputName,
value: inputAttributes.value,
type: inputAttributes.type,
className: 'highcharts-popup-field'
}, void 0, parentDiv);
input.setAttribute('highcharts-data-name', option);
return input;
}
/**
* Create color input group with color picker, text field and opacity
* controls.
*/
createColorInput(option, inputName, inputAttributes, parentDiv, container) {
const { value, alpha } = resolveColorValue(inputAttributes.value || '', container);
const parsedOpacity = Color.parse(inputAttributes.value || '').rgba[3], opacity = isNaN(parsedOpacity) ? alpha : parsedOpacity;
const wrapper = createElement('div', { className: 'highcharts-popup-color-wrapper' }, void 0, parentDiv);
const colorInput = createElement('input', {
type: 'color',
value,
className: ('highcharts-popup-field highcharts-popup-field-color')
}, void 0, wrapper);
const textInput = createElement('input', {
name: inputName,
id: inputName,
value,
type: 'text',
className: ('highcharts-popup-field highcharts-popup-field-text')
}, void 0, wrapper);
textInput.setAttribute('highcharts-data-name', option);
const separator = createElement('span', { className: 'highcharts-popup-color-separator' }, void 0, wrapper);
const opacityPercentInput = createElement('input', {
type: 'number',
value: String(Math.round(opacity * 100)),
className: ('highcharts-popup-field highcharts-popup-opacity-percentage'),
min: '0',
max: '100',
step: '1'
}, void 0, wrapper);
const opacityPercentSuffix = createElement('span', { className: 'highcharts-popup-opacity-percent-suffix' }, void 0, wrapper);
opacityPercentSuffix.appendChild(doc.createTextNode(' %'));
const opacitySlider = createElement('input', {
type: 'range',
value: String(Math.round(opacity * 100)),
className: 'highcharts-popup-opacity-slider',
min: '0',
max: '100',
step: '1'
}, void 0, parentDiv);
opacitySlider.style.setProperty('--highcharts-popup-opacity-track-color', value);
opacitySlider.style.setProperty('display', 'none');
const setOpacityGroupVisibility = () => {
const isHex = /^#[0-9A-Fa-f]{6}$/.test(textInput.value);
separator.style.display = isHex ? '' : 'none';
opacityPercentInput.style.display = isHex ? '' : 'none';
opacityPercentSuffix.style.display = isHex ? '' : 'none';
};
setOpacityGroupVisibility();
const syncOpacityInputs = (e) => {
const target = e.target, val = clamp(Number(target.value), 0, 100);
opacitySlider.value = String(val);
opacityPercentInput.value = String(Math.round(val));
};
const syncColorInputs = (e) => {
if (e.target === colorInput) {
textInput.value = colorInput.value.toUpperCase();
}
else {
colorInput.value = textInput.value;
}
opacitySlider.style.setProperty('--highcharts-popup-opacity-track-color', colorInput.value);
setOpacityGroupVisibility();
};
addEvent(parentDiv, 'mousedown', (e) => {
if (e.target !== opacityPercentInput &&
e.target !== opacitySlider) {
opacitySlider.style.display = 'none';
}
});
addEvent(opacityPercentInput, 'focus', () => {
opacitySlider.style.display = '';
});
addEvent(opacityPercentInput, 'input', syncOpacityInputs);
addEvent(opacitySlider, 'input', syncOpacityInputs);
addEvent(colorInput, 'input', syncColorInputs);
addEvent(textInput, 'input', syncColorInputs);
return wrapper;
}
closeButtonEvents() {
if (this.chart) {
const navigationBindings = this.chart.navigationBindings;
fireEvent(navigationBindings, 'closePopup');
if (navigationBindings &&
navigationBindings.selectedButtonElement) {
fireEvent(navigationBindings, 'deselectButton', { button: navigationBindings.selectedButtonElement });
}
}
else {
super.closeButtonEvents();
}
}
/**
* Create button.
*
* @param {Highcharts.HTMLDOMElement} parentDiv
* Container where elements should be added
* @param {string} label
* Text placed as button label
* @param {string} type
* add | edit | remove
* @param {Highcharts.HTMLDOMElement} fieldsDiv
* Container where inputs are generated
* @param {Function} callback
* On click callback
* @return {Highcharts.HTMLDOMElement}
* HTML button
*/
addButton(parentDiv, label, type, fieldsDiv, callback) {
const button = createElement('button', void 0, void 0, parentDiv);
button.appendChild(doc.createTextNode(label));
if (callback) {
['click', 'touchstart'].forEach((eventName) => {
addEvent(button, eventName, () => {
this.closePopup();
return callback(getFields(fieldsDiv, type));
});
});
}
return button;
}
/**
* Create content and show popup.
*
* @param {string} type
* Type of popup i.e indicators
* @param {Highcharts.Chart} chart
* Chart instance
* @param {Highcharts.AnnotationsOptions} options
* Annotation options
* @param {Function} callback
* On click callback
*/
showForm(type, chart, options, callback) {
if (!chart) {
return;
}
// Show blank popup
this.showPopup();
// Indicator form
if (type === 'indicators') {
this.indicators.addForm.call(this, chart, options, callback);
}
// Annotation small toolbar
if (type === 'annotation-toolbar') {
this.annotations.addToolbar.call(this, chart, options, callback);
}
// Annotation edit form
if (type === 'annotation-edit') {
this.annotations.addForm.call(this, chart, options, callback);
}
// Flags form - add / edit
if (type === 'flag') {
this.annotations.addForm.call(this, chart, options, callback, true);
}
this.type = type;
// Explicit height is needed to make inner elements scrollable
this.container.style.height = this.container.offsetHeight + 'px';
}
}
extend(Popup.prototype, {
annotations: PopupAnnotations,
indicators: PopupIndicators,
tabs: PopupTabs
});
/* *
*
* Default Export
*
* */
/** @internal */
export default Popup;