UNPKG

@amcharts/amcharts5

Version:
561 lines 24.4 kB
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