broadlistening-viewer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,250 @@
1
+ import Plotly from "./plotly_shim";
2
+ import { getClusterColor, INACTIVE_COLOR } from "./colors";
3
+ import { escapeHtml } from "./decidim_core_shim";
4
+ import { t } from "./i18n";
5
+ import { wrapText, wrapTextWithLimit } from "./utils";
6
+ class ScatterChart {
7
+ constructor(container, data, options = {}) {
8
+ this.container = container;
9
+ this.arguments = data.arguments || [];
10
+ this.clusters = data.clusters || [];
11
+ this.options = {
12
+ targetLevel: 1,
13
+ ...options
14
+ };
15
+ if (this.options.clusterById) {
16
+ this.clusterById = this.options.clusterById;
17
+ this.childrenByParent = this.options.childrenByParent;
18
+ this.clustersByLevel = this.options.clustersByLevel;
19
+ this.argumentsByClusterId = this.options.argumentsByClusterId;
20
+ } else {
21
+ this.clusterById = /* @__PURE__ */ new Map();
22
+ this.childrenByParent = /* @__PURE__ */ new Map();
23
+ this.clustersByLevel = /* @__PURE__ */ new Map();
24
+ this.argumentsByClusterId = /* @__PURE__ */ new Map();
25
+ this.buildIndexes();
26
+ }
27
+ }
28
+ buildIndexes() {
29
+ this.clusterById = new Map(this.clusters.map((c) => [c.id, c]));
30
+ this.childrenByParent = /* @__PURE__ */ new Map();
31
+ this.clustersByLevel = /* @__PURE__ */ new Map();
32
+ for (const cluster of this.clusters) {
33
+ if (cluster.parent) {
34
+ if (!this.childrenByParent.has(cluster.parent)) {
35
+ this.childrenByParent.set(cluster.parent, []);
36
+ }
37
+ this.childrenByParent.get(cluster.parent).push(cluster);
38
+ }
39
+ const level = cluster.level ?? 0;
40
+ if (!this.clustersByLevel.has(level)) {
41
+ this.clustersByLevel.set(level, []);
42
+ }
43
+ this.clustersByLevel.get(level).push(cluster);
44
+ }
45
+ this.argumentsByClusterId = /* @__PURE__ */ new Map();
46
+ for (const arg of this.arguments) {
47
+ for (const clusterId of arg.cluster_ids || []) {
48
+ if (!this.argumentsByClusterId.has(clusterId)) {
49
+ this.argumentsByClusterId.set(clusterId, []);
50
+ }
51
+ this.argumentsByClusterId.get(clusterId).push(arg);
52
+ }
53
+ }
54
+ }
55
+ render() {
56
+ if (this.arguments.length === 0) {
57
+ this.container.innerHTML = `<p class="text-gray text-center py-8">${escapeHtml(t("common.no_data"))}</p>`;
58
+ return;
59
+ }
60
+ const trace = this.buildTrace();
61
+ const layout = this.buildLayout();
62
+ const config = {
63
+ responsive: true,
64
+ displayModeBar: true,
65
+ modeBarButtonsToRemove: ["select2d", "lasso2d", "resetScale2d", "toImage", "zoom2d", "pan2d"],
66
+ displaylogo: false,
67
+ scrollZoom: true
68
+ };
69
+ Plotly.newPlot(this.container, [trace], layout, config);
70
+ }
71
+ buildTrace() {
72
+ const colors = this.getPointColors();
73
+ return {
74
+ x: this.arguments.map((a) => a.x),
75
+ y: this.arguments.map((a) => a.y),
76
+ mode: "markers",
77
+ type: "scattergl",
78
+ marker: {
79
+ color: colors,
80
+ size: 8,
81
+ opacity: 0.7
82
+ },
83
+ text: this.arguments.map((a) => this.formatHoverText(a)),
84
+ hoverinfo: "text",
85
+ hovertemplate: "%{text}<extra></extra>",
86
+ hoverlabel: {
87
+ align: "left",
88
+ bgcolor: "white",
89
+ bordercolor: "#ccc",
90
+ font: { size: 12, family: "sans-serif" }
91
+ }
92
+ };
93
+ }
94
+ buildLayout() {
95
+ const annotations = this.getClusterAnnotations();
96
+ return {
97
+ showlegend: false,
98
+ hovermode: "closest",
99
+ dragmode: "pan",
100
+ xaxis: {
101
+ showgrid: false,
102
+ zeroline: false,
103
+ showticklabels: false,
104
+ title: ""
105
+ },
106
+ yaxis: {
107
+ showgrid: false,
108
+ zeroline: false,
109
+ showticklabels: false,
110
+ title: ""
111
+ },
112
+ margin: { l: 10, r: 10, t: 10, b: 10 },
113
+ annotations,
114
+ paper_bgcolor: "rgba(0,0,0,0)",
115
+ plot_bgcolor: "rgba(0,0,0,0)"
116
+ };
117
+ }
118
+ getClusterIdAtLevel(clusterIds, level) {
119
+ return clusterIds.find((id) => id.startsWith(`${level}_`));
120
+ }
121
+ getPointColors() {
122
+ const { targetLevel, selectedClusterId, filteredArgumentIds, filteredClusterIds, maxLevel, clusterColorMap } = this.options;
123
+ const colorOf = (id) => clusterColorMap?.get(id) || getClusterColor(id);
124
+ let childIds;
125
+ if (selectedClusterId) {
126
+ const childClusters = this.childrenByParent.get(selectedClusterId) || [];
127
+ childIds = new Set(childClusters.map((c) => c.id));
128
+ }
129
+ return this.arguments.map((arg) => {
130
+ if (filteredArgumentIds && !filteredArgumentIds.has(arg.arg_id)) {
131
+ return INACTIVE_COLOR;
132
+ }
133
+ const clusterIds = arg.cluster_ids || [];
134
+ if (filteredClusterIds && maxLevel != null) {
135
+ const deepestClusterId = this.getClusterIdAtLevel(clusterIds, maxLevel);
136
+ if (!deepestClusterId || !filteredClusterIds.has(deepestClusterId)) {
137
+ return INACTIVE_COLOR;
138
+ }
139
+ return colorOf(deepestClusterId);
140
+ }
141
+ if (selectedClusterId && childIds) {
142
+ const childId = clusterIds.find((id) => childIds.has(id));
143
+ if (childId) {
144
+ return colorOf(childId);
145
+ }
146
+ return INACTIVE_COLOR;
147
+ }
148
+ const clusterId = this.getClusterIdAtLevel(clusterIds, targetLevel);
149
+ if (!clusterId) {
150
+ return INACTIVE_COLOR;
151
+ }
152
+ return colorOf(clusterId);
153
+ });
154
+ }
155
+ calculateCentroid(points) {
156
+ if (points.length === 0) return void 0;
157
+ const sumX = points.reduce((acc, p) => acc + p.x, 0);
158
+ const sumY = points.reduce((acc, p) => acc + p.y, 0);
159
+ return {
160
+ x: sumX / points.length,
161
+ y: sumY / points.length
162
+ };
163
+ }
164
+ getClusterAnnotations() {
165
+ const { targetLevel, selectedClusterId, filteredClusterIds, maxLevel } = this.options;
166
+ let clustersToShow;
167
+ if (filteredClusterIds && maxLevel != null) {
168
+ const maxLevelClusters = this.clustersByLevel.get(maxLevel) || [];
169
+ clustersToShow = maxLevelClusters.filter((c) => filteredClusterIds.has(c.id));
170
+ } else if (selectedClusterId) {
171
+ clustersToShow = this.childrenByParent.get(selectedClusterId) || [];
172
+ } else {
173
+ clustersToShow = this.clustersByLevel.get(targetLevel) || [];
174
+ }
175
+ return clustersToShow.filter((cluster) => {
176
+ const points = this.argumentsByClusterId.get(cluster.id);
177
+ return points && points.length > 0;
178
+ }).map((cluster) => {
179
+ const points = this.argumentsByClusterId.get(cluster.id);
180
+ const centroid = this.calculateCentroid(points);
181
+ if (!centroid) return void 0;
182
+ return {
183
+ x: centroid.x,
184
+ y: centroid.y,
185
+ text: wrapText(cluster.label, 16),
186
+ showarrow: false,
187
+ font: {
188
+ size: 11,
189
+ color: "#333",
190
+ family: "sans-serif"
191
+ },
192
+ bgcolor: "rgba(255, 255, 255, 0.85)",
193
+ borderpad: 4,
194
+ borderwidth: 0
195
+ };
196
+ }).filter((a) => a !== void 0);
197
+ }
198
+ formatHoverText(argument) {
199
+ const lines = [];
200
+ const text = escapeHtml(argument.argument || "");
201
+ const wrapped = wrapTextWithLimit(text, 30);
202
+ lines.push(wrapped);
203
+ const { selectedClusterId, targetLevel, filteredClusterIds, maxLevel } = this.options;
204
+ if (filteredClusterIds && maxLevel != null) {
205
+ const clusterId = this.getClusterIdAtLevel(argument.cluster_ids || [], maxLevel);
206
+ if (clusterId) {
207
+ const cluster = this.clusterById.get(clusterId);
208
+ if (cluster) {
209
+ lines.push(`<b>[${escapeHtml(cluster.label)}]</b>`);
210
+ }
211
+ }
212
+ } else if (selectedClusterId) {
213
+ const childClusters = this.childrenByParent.get(selectedClusterId) || [];
214
+ const childIds = new Set(childClusters.map((c) => c.id));
215
+ const clusterIds = argument.cluster_ids || [];
216
+ const childId = clusterIds.find((id) => childIds.has(id));
217
+ if (childId) {
218
+ const cluster = this.clusterById.get(childId);
219
+ if (cluster) {
220
+ lines.push(`<b>[${escapeHtml(cluster.label)}]</b>`);
221
+ }
222
+ }
223
+ } else {
224
+ const clusterIds = argument.cluster_ids || [];
225
+ const clusterId = this.getClusterIdAtLevel(clusterIds, targetLevel);
226
+ if (clusterId) {
227
+ const cluster = this.clusterById.get(clusterId);
228
+ if (cluster) {
229
+ lines.push(`<b>[${escapeHtml(cluster.label)}]</b>`);
230
+ }
231
+ }
232
+ }
233
+ return lines.join("<br>");
234
+ }
235
+ update(newOptions) {
236
+ this.options = { ...this.options, ...newOptions };
237
+ if (this.arguments.length === 0) {
238
+ return;
239
+ }
240
+ const trace = this.buildTrace();
241
+ const layout = this.buildLayout();
242
+ Plotly.react(this.container, [trace], layout);
243
+ }
244
+ destroy() {
245
+ Plotly.purge(this.container);
246
+ }
247
+ }
248
+ export {
249
+ ScatterChart as default
250
+ };
@@ -0,0 +1,154 @@
1
+ import { icon, escapeHtml, Dialogs } from "./decidim_core_shim";
2
+ import { t } from "./i18n";
3
+ class SettingsDialog {
4
+ constructor(options = {}) {
5
+ this.options = {
6
+ maxDensity: 0.2,
7
+ minValue: 5,
8
+ ...options
9
+ };
10
+ this.isOpen = false;
11
+ }
12
+ open() {
13
+ if (this.isOpen) return;
14
+ this.isOpen = true;
15
+ const { maxDensity, minValue } = this.options;
16
+ const dialogId = `blv-settings-${Math.random().toString(36).slice(2)}`;
17
+ this.element = document.createElement("div");
18
+ this.element.dataset.dialog = dialogId;
19
+ this.element.className = "blv-settings-dialog";
20
+ this.element.innerHTML = `
21
+ <div id="${escapeHtml(dialogId)}-content" class="relative w-full max-w-[400px] bg-white rounded-xl shadow-lg overflow-hidden">
22
+ <button type="button"
23
+ class="absolute top-3 right-3 flex items-center justify-center w-8 h-8 p-0 bg-transparent border-none rounded-md cursor-pointer text-gray-500 transition-all duration-150 hover:bg-gray-100 hover:text-gray-900 focus:outline-2 focus:outline-sky-600 focus:outline-offset-2 [&_svg]:w-5 [&_svg]:h-5"
24
+ data-dialog-close="${escapeHtml(dialogId)}"
25
+ data-dialog-closable
26
+ aria-label="${escapeHtml(t("common.close"))}">
27
+ ${icon("close-line")}
28
+ </button>
29
+ <div data-dialog-container class="blv-settings-dialog__container">
30
+ <h3 id="dialog-title-${escapeHtml(dialogId)}" data-dialog-title class="m-0 px-5 py-4 pr-12 text-base font-semibold text-gray-900 border-b border-gray-200">
31
+ ${escapeHtml(t("settings.title"))}
32
+ </h3>
33
+ <div id="dialog-desc-${escapeHtml(dialogId)}" class="p-5">
34
+ <div class="mb-6 last:mb-0 [&_label]:block [&_label]:mb-3 [&_label]:text-sm [&_label]:font-medium [&_label]:text-gray-700">
35
+ <label for="${escapeHtml(dialogId)}-maxDensity">${escapeHtml(t("settings.max_density_label"))}</label>
36
+ <div class="blv-slider">
37
+ <input type="range"
38
+ id="${escapeHtml(dialogId)}-maxDensity"
39
+ min="0.1" max="1" step="0.1"
40
+ value="${escapeHtml(String(maxDensity))}"
41
+ data-setting="maxDensity" />
42
+ <div class="flex justify-between mt-2 text-xs text-gray-500">
43
+ <span>10%</span>
44
+ <span data-blv="slider-value" class="font-semibold text-sky-600">${escapeHtml(String(Math.round(maxDensity * 100)))}%</span>
45
+ <span>100%</span>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ <div class="mb-6 last:mb-0 [&_label]:block [&_label]:mb-3 [&_label]:text-sm [&_label]:font-medium [&_label]:text-gray-700">
50
+ <label for="${escapeHtml(dialogId)}-minValue">${escapeHtml(t("settings.min_value_label"))}</label>
51
+ <div class="blv-slider">
52
+ <input type="range"
53
+ id="${escapeHtml(dialogId)}-minValue"
54
+ min="0" max="10" step="1"
55
+ value="${escapeHtml(String(minValue))}"
56
+ data-setting="minValue" />
57
+ <div class="flex justify-between mt-2 text-xs text-gray-500">
58
+ <span>0</span>
59
+ <span data-blv="slider-value" class="font-semibold text-sky-600">${escapeHtml(t("common.items_count", { count: minValue }))}</span>
60
+ <span>10</span>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ <div data-dialog-actions class="px-5 py-4 border-t border-gray-200 flex justify-end gap-2">
67
+ <button type="button"
68
+ class="button button__sm button__transparent-secondary"
69
+ data-dialog-close="${escapeHtml(dialogId)}">
70
+ ${escapeHtml(t("common.cancel"))}
71
+ </button>
72
+ <button type="button"
73
+ class="button button__sm button__secondary"
74
+ data-action="apply">
75
+ ${escapeHtml(t("common.apply"))}
76
+ </button>
77
+ </div>
78
+ </div>
79
+ `;
80
+ document.body.appendChild(this.element);
81
+ this.dialog = new Dialogs(`[data-dialog="${dialogId}"]`, {
82
+ closingSelector: `[data-dialog-close="${dialogId}"]`,
83
+ backdropSelector: `[data-dialog="${dialogId}"]`,
84
+ labelledby: `dialog-title-${dialogId}`,
85
+ describedby: `dialog-desc-${dialogId}`,
86
+ enableAutoFocus: false,
87
+ onOpen: () => {
88
+ setTimeout(() => this.focusFirstInput(), 0);
89
+ },
90
+ onClose: () => {
91
+ setTimeout(() => this.handleClose(), 0);
92
+ }
93
+ });
94
+ this.bindEvents();
95
+ this.dialog.open();
96
+ }
97
+ bindEvents() {
98
+ if (!this.element) return;
99
+ this.element.querySelectorAll("input[type='range']").forEach((input) => {
100
+ input.addEventListener("input", (e) => {
101
+ const target = e.target;
102
+ const value = parseFloat(target.value);
103
+ const valueDisplay = target.parentElement.querySelector('[data-blv="slider-value"]');
104
+ if (target.dataset.setting === "maxDensity") {
105
+ valueDisplay.textContent = `${Math.round(value * 100)}%`;
106
+ } else {
107
+ valueDisplay.textContent = t("common.items_count", { count: value });
108
+ }
109
+ });
110
+ });
111
+ this.element.querySelector("[data-action='apply']").addEventListener("click", () => {
112
+ this.apply();
113
+ });
114
+ }
115
+ focusFirstInput() {
116
+ const firstInput = this.element?.querySelector("input[type='range']");
117
+ if (firstInput) {
118
+ firstInput.focus();
119
+ }
120
+ }
121
+ apply() {
122
+ if (!this.element) return;
123
+ const maxDensityInput = this.element.querySelector("[data-setting='maxDensity']");
124
+ const minValueInput = this.element.querySelector("[data-setting='minValue']");
125
+ const newSettings = {
126
+ maxDensity: parseFloat(maxDensityInput.value),
127
+ minValue: parseInt(minValueInput.value, 10)
128
+ };
129
+ this.options.onApply?.(newSettings);
130
+ this.dialog.close();
131
+ }
132
+ handleClose() {
133
+ this.destroy();
134
+ this.options.onClose?.();
135
+ }
136
+ close() {
137
+ if (!this.isOpen || !this.dialog) return;
138
+ this.dialog.close();
139
+ }
140
+ destroy() {
141
+ this.isOpen = false;
142
+ if (this.dialog) {
143
+ this.dialog.destroy();
144
+ this.dialog = void 0;
145
+ }
146
+ if (this.element) {
147
+ this.element.remove();
148
+ this.element = void 0;
149
+ }
150
+ }
151
+ }
152
+ export {
153
+ SettingsDialog as default
154
+ };
@@ -0,0 +1,102 @@
1
+ import { icon, escapeHtml } from "./decidim_core_shim";
2
+ import { t } from "./i18n";
3
+ const VIEW_MODES = {
4
+ SCATTER_ALL: "scatterAll",
5
+ SCATTER_DENSITY: "scatterDensity",
6
+ TREEMAP: "treemap"
7
+ };
8
+ class Toolbar {
9
+ constructor(options = {}) {
10
+ this.options = {
11
+ viewMode: VIEW_MODES.SCATTER_ALL,
12
+ hasDensityData: false,
13
+ isDenseGroupEnabled: true,
14
+ showSettings: true,
15
+ showFullscreen: true,
16
+ ...options
17
+ };
18
+ }
19
+ render() {
20
+ const { viewMode, hasDensityData, isDenseGroupEnabled, showSettings, showFullscreen } = this.options;
21
+ const densityBtnDisabled = !hasDensityData || !isDenseGroupEnabled;
22
+ const densityBtnTitle = densityBtnDisabled ? t("toolbar.density_disabled_title") : t("toolbar.density_title");
23
+ const activeClass = (mode) => viewMode === mode ? "blv-active" : "";
24
+ return `
25
+ <div class="inline-flex bg-gray-200 rounded-md p-0.5 gap-0.5">
26
+ <button class="blv-segment-btn inline-flex items-center gap-1.5 px-3 py-1.5 text-[0.8125rem] font-medium text-gray-500 bg-transparent border-none rounded cursor-pointer transition-all duration-150 whitespace-nowrap disabled:text-gray-400 disabled:cursor-not-allowed disabled:opacity-60 [&_svg]:w-3.5 [&_svg]:h-3.5 [&_svg]:shrink-0 ${escapeHtml(activeClass(VIEW_MODES.SCATTER_ALL))}"
27
+ data-view-mode="${escapeHtml(VIEW_MODES.SCATTER_ALL)}"
28
+ title="${escapeHtml(t("toolbar.all_title"))}">
29
+ ${icon("bubble-chart-line")}
30
+ <span>${escapeHtml(t("toolbar.all"))}</span>
31
+ </button>
32
+ <button class="blv-segment-btn inline-flex items-center gap-1.5 px-3 py-1.5 text-[0.8125rem] font-medium text-gray-500 bg-transparent border-none rounded cursor-pointer transition-all duration-150 whitespace-nowrap disabled:text-gray-400 disabled:cursor-not-allowed disabled:opacity-60 [&_svg]:w-3.5 [&_svg]:h-3.5 [&_svg]:shrink-0 ${escapeHtml(activeClass(VIEW_MODES.SCATTER_DENSITY))}"
33
+ data-view-mode="${escapeHtml(VIEW_MODES.SCATTER_DENSITY)}"
34
+ title="${escapeHtml(densityBtnTitle)}"
35
+ ${densityBtnDisabled ? "disabled" : ""}>
36
+ ${icon("focus-3-line")}
37
+ <span>${escapeHtml(t("toolbar.density"))}</span>
38
+ </button>
39
+ <button class="blv-segment-btn inline-flex items-center gap-1.5 px-3 py-1.5 text-[0.8125rem] font-medium text-gray-500 bg-transparent border-none rounded cursor-pointer transition-all duration-150 whitespace-nowrap disabled:text-gray-400 disabled:cursor-not-allowed disabled:opacity-60 [&_svg]:w-3.5 [&_svg]:h-3.5 [&_svg]:shrink-0 ${escapeHtml(activeClass(VIEW_MODES.TREEMAP))}"
40
+ data-view-mode="${escapeHtml(VIEW_MODES.TREEMAP)}"
41
+ title="${escapeHtml(t("toolbar.treemap_title"))}">
42
+ ${icon("layout-grid-line")}
43
+ <span>${escapeHtml(t("toolbar.treemap"))}</span>
44
+ </button>
45
+ </div>
46
+ <div class="flex gap-2">
47
+ ${showSettings && hasDensityData ? `
48
+ <button class="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-500 bg-transparent border border-transparent rounded-md cursor-pointer transition-all duration-150 hover:bg-gray-200 hover:text-gray-700 [&_svg]:w-4 [&_svg]:h-4 [&_svg]:shrink-0"
49
+ data-action="settings"
50
+ title="${escapeHtml(t("toolbar.settings_title"))}">
51
+ ${icon("settings-3-line")}
52
+ <span>${escapeHtml(t("toolbar.settings"))}</span>
53
+ </button>
54
+ ` : ""}
55
+ ${showFullscreen ? `
56
+ <button class="inline-flex items-center gap-1.5 p-2 text-sm font-medium text-gray-500 bg-transparent border border-transparent rounded-md cursor-pointer transition-all duration-150 hover:bg-gray-200 hover:text-gray-700 [&_svg]:w-4 [&_svg]:h-4 [&_svg]:shrink-0 [&_span]:hidden"
57
+ data-action="fullscreen"
58
+ title="${escapeHtml(t("toolbar.fullscreen_title"))}">
59
+ ${icon("fullscreen-line")}
60
+ </button>
61
+ ` : ""}
62
+ </div>
63
+ `;
64
+ }
65
+ bindEvents(container) {
66
+ const { onViewModeChange, onSettingsClick, onFullscreenClick } = this.options;
67
+ container.querySelectorAll("[data-view-mode]").forEach((btn) => {
68
+ btn.addEventListener("click", (e) => {
69
+ if (e.currentTarget.disabled) return;
70
+ const mode = e.currentTarget.dataset.viewMode;
71
+ if (onViewModeChange) {
72
+ onViewModeChange(mode);
73
+ }
74
+ });
75
+ });
76
+ const settingsBtn = container.querySelector("[data-action='settings']");
77
+ if (settingsBtn && onSettingsClick) {
78
+ settingsBtn.addEventListener("click", onSettingsClick);
79
+ }
80
+ const fullscreenBtn = container.querySelector("[data-action='fullscreen']");
81
+ if (fullscreenBtn && onFullscreenClick) {
82
+ fullscreenBtn.addEventListener("click", onFullscreenClick);
83
+ }
84
+ }
85
+ updateState(container, state) {
86
+ const { viewMode, hasDensityData, isDenseGroupEnabled } = { ...this.options, ...state };
87
+ container.querySelectorAll("[data-view-mode]").forEach((btn) => {
88
+ const isActive = btn.dataset.viewMode === viewMode;
89
+ btn.classList.toggle("blv-active", isActive);
90
+ if (btn.dataset.viewMode === VIEW_MODES.SCATTER_DENSITY) {
91
+ const isDisabled = !hasDensityData || !isDenseGroupEnabled;
92
+ btn.disabled = isDisabled;
93
+ btn.title = isDisabled ? t("toolbar.density_disabled_title") : t("toolbar.density_title");
94
+ }
95
+ });
96
+ Object.assign(this.options, state);
97
+ }
98
+ }
99
+ export {
100
+ VIEW_MODES,
101
+ Toolbar as default
102
+ };