@amcharts/amcharts5
Version:
amCharts 5
561 lines • 24.4 kB
JavaScript
import { CandlestickSeries } from "../xy/series/CandlestickSeries";
import { LineSeries } from "../xy/series/LineSeries";
import { Modal } from "../../core/util/Modal";
import { ColorControl } from "./toolbar/ColorControl";
import * as $array from "../../core/util/Array";
import * as $object from "../../core/util/Object";
import * as $utils from "../../core/util/Utils";
import * as $type from "../../core/util/Type";
/**
* Used to display a modal dialog with HTML content.
*/
export class SettingsModal extends Modal {
constructor() {
super(...arguments);
this._updatedSettings = {};
}
_afterNew() {
super._afterNew();
this._defaultThemes = this.get("stockChart")._defaultThemes;
super._afterNewApplyThemes();
}
_beforeChanged() {
super._beforeChanged();
// if (this.isDirty("content")) {
// this.getPrivate("content").innerHTML = this.get("content", "");
// }
}
/**
* Opens settings modal for an [[Indicator]].
*
* @param target Target indicator
*/
openIndicator(target) {
this._settingsTarget = target;
this.initContent(target._editableSettings, target);
}
/**
* Opens settings editing for the any [[XYSeries]].
*
* @param series target series
*/
openSeries(series) {
this._settingsTarget = series;
const l = this._root.language;
const stockChart = this.get("stockChart");
const isline = series instanceof LineSeries;
let settings = [];
if (series == stockChart.get("stockSeries") && !isline) {
settings.push({
id: "stockPositiveColor",
key: "stockPositiveColor",
name: l.translateAny("Increase"),
type: "color",
currentValue: stockChart.get("stockPositiveColor", this._root.interfaceColors.get("positive")),
target: stockChart
});
settings.push({
id: "stockNegativeColor",
key: "stockNegativeColor",
name: l.translateAny("Decrease"),
type: "color",
currentValue: stockChart.get("stockNegativeColor", this._root.interfaceColors.get("negative")),
target: stockChart
});
}
else if (series == stockChart.get("volumeSeries") && !isline) {
settings.push({
id: "volumePositiveColor",
key: "volumePositiveColor",
name: l.translateAny("Increase"),
type: "color",
currentValue: stockChart.get("volumePositiveColor", this._root.interfaceColors.get("positive")),
target: stockChart
});
settings.push({
id: "volumeNegativeColor",
key: "volumeNegativeColor",
name: l.translateAny("Decrease"),
type: "color",
currentValue: stockChart.get("volumeNegativeColor", this._root.interfaceColors.get("negative")),
target: stockChart
});
}
else if (series instanceof CandlestickSeries && series.columns.length) {
const column = series.columns.getIndex(0);
settings.push({
id: "riseFromOpen.fill",
key: "fill",
additionalKeys: ["stroke"],
name: l.translateAny("Increase"),
type: "color",
currentValue: column.states.lookup("riseFromOpen").get("fill"),
target: series.columns.template.states.create("riseFromOpen", {}),
invalidateTarget: series
});
settings.push({
id: "dropFromOpen.fill",
key: "fill",
additionalKeys: ["stroke"],
name: l.translateAny("Decrease"),
type: "color",
currentValue: column.states.lookup("dropFromOpen").get("fill"),
target: series.columns.template.states.create("dropFromOpen", {}),
invalidateTarget: series
});
}
else if (isline) {
const widths = this.get("strokeWidths", [1, 2, 4, 10]);
const widthOptions = widths.map((width) => {
return { value: width, text: width + "px" };
});
settings = [{
key: "stroke",
name: l.translateAny("Line"),
type: "color",
additionalKeys: ["fill"],
target: series,
}, {
key: "strokeWidth",
name: l.translateAny("Line"),
type: "dropdown",
options: widthOptions,
currentValue: series.strokes.template.get("strokeWidth", 1),
target: series.strokes.template,
invalidateTarget: series
}];
if (series.fills.template.get("visible")) {
settings.push({
key: "fill",
name: l.translateAny("Fill"),
type: "color"
});
}
}
else {
settings = [{
key: "stroke",
name: l.translateAny("Line"),
type: "color"
}, {
key: "fill",
name: l.translateAny("Fill"),
type: "color"
}];
}
this.initContent(settings, series);
}
initContent(settings, target) {
this._updatedSettings = {};
const content = this.getPrivate("content");
// Clear
this.clear();
// Init started event
this.events.dispatch("initstarted", {
type: "initstarted",
settings: settings,
settingsTarget: target,
target: this
});
// Title
const title = document.createElement("h1");
title.innerText = target.get("name");
content.appendChild(title);
// Add fields
const table = document.createElement("div");
table.className = "am5-modal-table";
content.appendChild(table);
// Log defaults
let populateDefaults = false;
let defaults = {};
if (!target.get("userData")) {
target.set("userData", {});
}
if (!target.get("userData")["__defaults"]) {
target.get("userData")["__defaults"] = defaults;
populateDefaults = true;
}
else {
defaults = target.get("userData")["__defaults"];
}
const settingInputs = {};
const controls = {};
const settingsWithTarget = {};
let prevName = "";
let prevLine;
$array.each(settings, (setting) => {
const key = this._getSettingKey(setting);
const keyTarget = setting.target || target;
const currentValue = setting.currentValue || keyTarget.get(setting.key);
if (populateDefaults) {
defaults[key] = currentValue;
}
if (setting.target) {
settingsWithTarget[key] = setting;
}
let element;
switch (setting.type) {
case "dropdown":
element = this.getDropdown(setting, currentValue);
settingInputs[key] = element;
this._disposers.push($utils.addEventListener(element, "change", (ev) => {
const input = ev.target;
const option = input.options[input.selectedIndex];
const extTarget = option.getAttribute("data-target");
if (extTarget && settingInputs[extTarget]) {
const targetInput = settingInputs[extTarget];
// Apply each retargeted attribute only when the option carries it,
// so a value-less retarget doesn't stringify "null" onto the input.
const applyAttr = (attr, prop, source) => {
const v = option.getAttribute(source);
if (v != null) {
targetInput.setAttribute(attr, v);
if (prop) {
targetInput[prop] = v;
}
}
else {
targetInput.removeAttribute(attr);
// "any" (not removed) so a step-less mode keeps decimals valid.
if (prop == "step") {
targetInput.step = "any";
}
else if (prop) {
targetInput.removeAttribute(prop);
}
}
};
const tv = option.getAttribute("data-target-value");
if (tv != null) {
targetInput.value = tv;
}
applyAttr("data-min-value", "min", "data-target-min-value");
applyAttr("data-max-value", "max", "data-target-max-value");
applyAttr("data-step", "step", "data-target-step");
this._clampNumberInput(targetInput);
}
}));
break;
case "number":
element = this.getNumber(setting, currentValue);
settingInputs[key] = element;
// Data attributes (and the clamp) work in DISPLAY units, so scale
// the bounds to match the scaled value shown by getNumber.
const numScale = setting.scale != null ? setting.scale : 1;
if (numScale !== 1) {
element.setAttribute("data-scale", numScale + "");
}
if (setting.minValue != null) {
element.setAttribute("data-min-value", (setting.minValue * numScale) + "");
}
if (setting.maxValue != null) {
element.setAttribute("data-max-value", (setting.maxValue * numScale) + "");
}
if (setting.step != null) {
element.setAttribute("data-step", (setting.step * numScale) + "");
}
if (setting.minValue != null || setting.maxValue != null || setting.step != null) {
this._disposers.push($utils.addEventListener(element, "change", (ev) => {
this._clampNumberInput(ev.target);
}));
}
break;
case "color":
const control = this.getColor(setting, currentValue);
element = control.getPrivate("button");
controls[key] = control;
break;
case "checkbox":
element = this.getCheckbox(setting, currentValue);
settingInputs[key] = element;
break;
case "text":
element = this.getText(setting, currentValue);
settingInputs[key] = element;
break;
}
if (element) {
let line;
if (setting.name == prevName && prevLine) {
line = prevLine;
}
else {
line = document.createElement("div");
line.className = "am5-modal-table-row";
table.appendChild(line);
const heading = document.createElement("div");
heading.className = "am5-modal-table-heading";
heading.innerHTML = setting.name;
line.appendChild(heading);
}
const cell = document.createElement("div");
cell.className = "am5-modal-table-cell";
line.appendChild(cell);
cell.appendChild(element);
prevName = setting.name;
prevLine = line;
}
});
// Reset
if (this.get("showResetLink", true)) {
const resetLink = document.createElement("a");
resetLink.className = "am5-modal-link am5-modal-table-cell am5-modal-link-reset";
resetLink.innerHTML = this._root.language.translateAny("Reset to default");
table.appendChild(resetLink);
this._disposers.push($utils.addEventListener(resetLink, "click", () => {
const defaults = target.get("userData")["__defaults"];
$object.each(settingInputs, (key, element) => {
if (element.type === "checkbox") {
element.checked = defaults[key];
}
else {
element.value = $type.isNumber(defaults[key]) ? (defaults[key] * (Number(element.getAttribute("data-scale")) || 1)) + "" : defaults[key];
}
});
$object.each(controls, (key, control) => {
control.setColor(defaults[key]);
});
// Re-apply any mode dropdown's retargeted bounds to its number input,
// so a restored default isn't later mis-clamped on save by stale
// min/max/step left over from a previous mode selection.
$object.each(settingInputs, (_key, element) => {
if (element.tagName == "SELECT") {
element.dispatchEvent(new Event("change"));
}
});
}));
}
// Buttons
const saveButton = document.createElement("input");
saveButton.type = "button";
saveButton.value = this._root.language.translateAny("Save");
saveButton.className = "am5-modal-button am5-modal-primary";
content.appendChild(saveButton);
this._disposers.push($utils.addEventListener(saveButton, "click", () => {
$object.each(settingInputs, (key, element) => {
if (element.type == "checkbox") {
this._updatedSettings[key] = element.checked;
}
else if (element.type == "number") {
// Clamp on save too — covers commit without a blur/change event.
this._clampNumberInput(element);
this._updatedSettings[key] = $type.toNumber(element.value) / (Number(element.getAttribute("data-scale")) || 1);
}
else {
this._updatedSettings[key] = element.value;
}
});
$object.each(this._updatedSettings, (key, value) => {
const targetKey = key.split(".").pop();
if (targetKey && ["fill", "stroke"].indexOf(targetKey) != -1 && target.className == "ColumnSeries") {
target.columns.template.remove(targetKey);
}
if ($type.isObject(value) && value.value) {
if (value.setting && value.setting.target) {
value.setting.target.set(targetKey, value.value);
if (value.setting.additionalKeys) {
$array.each(value.setting.additionalKeys, (additionalKey) => {
value.setting.target.set(additionalKey, value.value);
});
}
}
else {
target.set(targetKey, value.value);
}
}
else if (settingsWithTarget[targetKey]) {
settingsWithTarget[targetKey].target.set(targetKey, value);
}
else {
target.set(targetKey, value);
}
if (value.setting && value.setting.invalidateTarget) {
value.setting.invalidateTarget.markDirtyValues();
}
});
this.close();
}));
const cancelButton = document.createElement("input");
cancelButton.type = "button";
cancelButton.value = this._root.language.translateAny("Cancel");
cancelButton.className = "am5-modal-button am5-modal-scondary";
content.appendChild(cancelButton);
this._disposers.push($utils.addEventListener(cancelButton, "click", () => {
this.cancel();
}));
// Open modal
this.open();
// Auto-focus Save button
this.setTimeout(() => {
// Delayed so that pressing ENTER in the list does not trigger save, too
saveButton.focus();
}, 10);
}
getDropdown(setting, currentValue) {
const element = document.createElement("select");
$array.each(setting.options, (option) => {
if (option) {
const optionElement = document.createElement("option");
let value;
if ($type.isObject(option)) {
optionElement.value = (option.value);
optionElement.text = (option.text);
value = (option.value);
if (option.minValue != null) {
optionElement.setAttribute("data-min-value", option.minValue);
}
if (option.extTarget != null) {
optionElement.setAttribute("data-target", option.extTarget);
}
if (option.extTargetValue != null) {
optionElement.setAttribute("data-target-value", option.extTargetValue);
}
if (option.extTargetMinValue != null) {
optionElement.setAttribute("data-target-min-value", option.extTargetMinValue);
}
if (option.extTargetMaxValue != null) {
optionElement.setAttribute("data-target-max-value", option.extTargetMaxValue);
}
if (option.extTargetStep != null) {
optionElement.setAttribute("data-target-step", option.extTargetStep);
}
}
else {
optionElement.value = option;
optionElement.text = option;
value = option;
}
if (value == currentValue) {
optionElement.selected = true;
}
element.appendChild(optionElement);
}
});
return element;
}
getNumber(setting, currentValue) {
const element = document.createElement("input");
element.type = "number";
// `scale` shows an awkward stored unit as a friendlier number in the UI
// (e.g. Acceleration Bands factor 0.001 shown as 1): the value and bounds
// are multiplied for display and divided back on save.
const scale = (setting && setting.scale != null) ? setting.scale : 1;
element.value = $type.isNumber(currentValue) ? (currentValue * scale) + "" : currentValue;
element.className = "am5-modal-input-narrow";
// Native bounds for the spinner arrows (min can be 0/negative, so guard
// with != null, not truthiness).
if (setting && setting.minValue != null) {
element.min = (setting.minValue * scale) + "";
}
if (setting && setting.maxValue != null) {
element.max = (setting.maxValue * scale) + "";
}
// "any" when unspecified, so a step-less (e.g. fractional ticks) value isn't
// forced to whole numbers by the native implicit step of 1.
element.step = (setting && setting.step != null) ? (setting.step * scale) + "" : "any";
return element;
}
/**
* Clamps a number input to its `data-min-value`/`data-max-value` and rounds to
* an integer when `data-step` is `1`. Attribute-driven so it also works after
* a mode dropdown retargets the input's bounds. Tolerant of missing / empty /
* literal "null" attribute values.
*/
_clampNumberInput(input) {
const attrNum = (name) => {
const s = input.getAttribute(name);
if (s == null || s === "" || s === "null") {
return undefined;
}
const n = Number(s);
return isNaN(n) ? undefined : n;
};
const value = Number(input.value);
if (isNaN(value)) {
return;
}
const step = attrNum("data-step");
const min = attrNum("data-min-value");
const max = attrNum("data-max-value");
let clamped = value;
if (step != null && step >= 1) {
clamped = Math.round(clamped);
}
if (min != null && clamped < min) {
clamped = min;
}
if (max != null && clamped > max) {
clamped = max;
}
if (clamped !== value) {
input.value = clamped + "";
}
}
getText(_setting, currentValue) {
const element = document.createElement("input");
element.type = "text";
element.value = currentValue;
element.className = "am5-modal-input";
return element;
}
getCheckbox(_setting, currentValue) {
const element = document.createElement("input");
element.type = "checkbox";
element.checked = currentValue === true;
return element;
}
getColor(setting, currentValue) {
const control = ColorControl.new(this.root, {
stockChart: this.get("stockChart"),
useOpacity: false
});
control.setPrivate("color", currentValue);
control.events.on("selected", (ev) => {
this._updatedSettings[this._getSettingKey(setting)] = {
value: ev.color,
setting: setting
};
});
this._disposers.push(control);
return control;
}
/**
* Closes the modal, saving settings.
*/
close() {
super.close();
this.events.dispatch("done", {
type: "done",
settings: this._updatedSettings,
settingsTarget: this._settingsTarget,
target: this
});
}
/**
* Closes the modal without applying any changes.
*/
cancel() {
super.cancel();
this.events.dispatch("done", {
type: "done",
settings: null,
target: this
});
}
/**
* Clears contents of the modal.
*/
clear() {
const content = this.getPrivate("content");
content.innerHTML = "";
if (this._colorControl) {
this._colorControl.dispose();
}
}
_getSettingKey(setting) {
return setting.id || setting.key;
}
}
SettingsModal.className = "SettingsModal";
SettingsModal.classNames = Modal.classNames.concat([SettingsModal.className]);
//# sourceMappingURL=SettingsModal.js.map