@openremote/or-dashboard-builder 1.2.0-snapshot.20240512160221

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.
Files changed (167) hide show
  1. package/README.md +156 -0
  2. package/build.gradle +15 -0
  3. package/lib/controls/dashboard-refresh-controls.d.ts +17 -0
  4. package/lib/controls/dashboard-refresh-controls.js +17 -0
  5. package/lib/controls/dashboard-refresh-controls.js.map +1 -0
  6. package/lib/index.d.ts +71 -0
  7. package/lib/index.js +285 -0
  8. package/lib/index.js.map +1 -0
  9. package/lib/or-dashboard-boardsettings.d.ts +19 -0
  10. package/lib/or-dashboard-boardsettings.js +121 -0
  11. package/lib/or-dashboard-boardsettings.js.map +1 -0
  12. package/lib/or-dashboard-browser.d.ts +9 -0
  13. package/lib/or-dashboard-browser.js +35 -0
  14. package/lib/or-dashboard-browser.js.map +1 -0
  15. package/lib/or-dashboard-engine.d.ts +4 -0
  16. package/lib/or-dashboard-engine.js +1 -0
  17. package/lib/or-dashboard-engine.js.map +1 -0
  18. package/lib/or-dashboard-keyhandler.d.ts +6 -0
  19. package/lib/or-dashboard-keyhandler.js +1 -0
  20. package/lib/or-dashboard-keyhandler.js.map +1 -0
  21. package/lib/or-dashboard-preview.d.ts +70 -0
  22. package/lib/or-dashboard-preview.js +155 -0
  23. package/lib/or-dashboard-preview.js.map +1 -0
  24. package/lib/or-dashboard-settingspanel.d.ts +44 -0
  25. package/lib/or-dashboard-settingspanel.js +216 -0
  26. package/lib/or-dashboard-settingspanel.js.map +1 -0
  27. package/lib/or-dashboard-tree.d.ts +25 -0
  28. package/lib/or-dashboard-tree.js +62 -0
  29. package/lib/or-dashboard-tree.js.map +1 -0
  30. package/lib/or-dashboard-widget.d.ts +18 -0
  31. package/lib/or-dashboard-widget.js +15 -0
  32. package/lib/or-dashboard-widget.js.map +1 -0
  33. package/lib/or-dashboard-widgetcontainer.d.ts +23 -0
  34. package/lib/or-dashboard-widgetcontainer.js +20 -0
  35. package/lib/or-dashboard-widgetcontainer.js.map +1 -0
  36. package/lib/or-dashboard-widgetsettings.d.ts +15 -0
  37. package/lib/or-dashboard-widgetsettings.js +16 -0
  38. package/lib/or-dashboard-widgetsettings.js.map +1 -0
  39. package/lib/panels/assettypes-panel.d.ts +44 -0
  40. package/lib/panels/assettypes-panel.js +51 -0
  41. package/lib/panels/assettypes-panel.js.map +1 -0
  42. package/lib/panels/attributes-panel.d.ts +41 -0
  43. package/lib/panels/attributes-panel.js +126 -0
  44. package/lib/panels/attributes-panel.js.map +1 -0
  45. package/lib/panels/thresholds-panel.d.ts +39 -0
  46. package/lib/panels/thresholds-panel.js +129 -0
  47. package/lib/panels/thresholds-panel.js.map +1 -0
  48. package/lib/service/dashboard-service.d.ts +13 -0
  49. package/lib/service/dashboard-service.js +1 -0
  50. package/lib/service/dashboard-service.js.map +1 -0
  51. package/lib/service/widget-service.d.ts +8 -0
  52. package/lib/service/widget-service.js +1 -0
  53. package/lib/service/widget-service.js.map +1 -0
  54. package/lib/settings/attribute-input-settings.d.ts +13 -0
  55. package/lib/settings/attribute-input-settings.js +36 -0
  56. package/lib/settings/attribute-input-settings.js.map +1 -0
  57. package/lib/settings/chart-settings.d.ts +30 -0
  58. package/lib/settings/chart-settings.js +144 -0
  59. package/lib/settings/chart-settings.js.map +1 -0
  60. package/lib/settings/gauge-settings.d.ts +15 -0
  61. package/lib/settings/gauge-settings.js +37 -0
  62. package/lib/settings/gauge-settings.js.map +1 -0
  63. package/lib/settings/image-settings.d.ts +17 -0
  64. package/lib/settings/image-settings.js +52 -0
  65. package/lib/settings/image-settings.js.map +1 -0
  66. package/lib/settings/kpi-settings.d.ts +15 -0
  67. package/lib/settings/kpi-settings.js +47 -0
  68. package/lib/settings/kpi-settings.js.map +1 -0
  69. package/lib/settings/map-settings.d.ts +21 -0
  70. package/lib/settings/map-settings.js +72 -0
  71. package/lib/settings/map-settings.js.map +1 -0
  72. package/lib/settings/table-settings.d.ts +14 -0
  73. package/lib/settings/table-settings.js +33 -0
  74. package/lib/settings/table-settings.js.map +1 -0
  75. package/lib/style.d.ts +1 -0
  76. package/lib/style.js +99 -0
  77. package/lib/style.js.map +1 -0
  78. package/lib/util/or-asset-widget.d.ts +20 -0
  79. package/lib/util/or-asset-widget.js +1 -0
  80. package/lib/util/or-asset-widget.js.map +1 -0
  81. package/lib/util/or-widget.d.ts +31 -0
  82. package/lib/util/or-widget.js +1 -0
  83. package/lib/util/or-widget.js.map +1 -0
  84. package/lib/util/settings-panel.d.ts +9 -0
  85. package/lib/util/settings-panel.js +56 -0
  86. package/lib/util/settings-panel.js.map +1 -0
  87. package/lib/util/widget-config.d.ts +2 -0
  88. package/lib/util/widget-config.js +1 -0
  89. package/lib/util/widget-config.js.map +1 -0
  90. package/lib/util/widget-settings.d.ts +23 -0
  91. package/lib/util/widget-settings.js +1 -0
  92. package/lib/util/widget-settings.js.map +1 -0
  93. package/lib/widgets/attribute-input-widget.d.ts +24 -0
  94. package/lib/widgets/attribute-input-widget.js +31 -0
  95. package/lib/widgets/attribute-input-widget.js.map +1 -0
  96. package/lib/widgets/chart-widget.d.ts +25 -0
  97. package/lib/widgets/chart-widget.js +15 -0
  98. package/lib/widgets/chart-widget.js.map +1 -0
  99. package/lib/widgets/gauge-widget.d.ts +22 -0
  100. package/lib/widgets/gauge-widget.js +12 -0
  101. package/lib/widgets/gauge-widget.js.map +1 -0
  102. package/lib/widgets/image-widget.d.ts +25 -0
  103. package/lib/widgets/image-widget.js +54 -0
  104. package/lib/widgets/image-widget.js.map +1 -0
  105. package/lib/widgets/kpi-widget.d.ts +21 -0
  106. package/lib/widgets/kpi-widget.js +8 -0
  107. package/lib/widgets/kpi-widget.js.map +1 -0
  108. package/lib/widgets/map-widget.d.ts +39 -0
  109. package/lib/widgets/map-widget.js +9 -0
  110. package/lib/widgets/map-widget.js.map +1 -0
  111. package/lib/widgets/or-base-widget.d.ts +15 -0
  112. package/lib/widgets/or-base-widget.js +1 -0
  113. package/lib/widgets/or-base-widget.js.map +1 -0
  114. package/lib/widgets/or-chart-widget.d.ts +36 -0
  115. package/lib/widgets/or-chart-widget.js +112 -0
  116. package/lib/widgets/or-chart-widget.js.map +1 -0
  117. package/lib/widgets/or-gauge-widget.d.ts +46 -0
  118. package/lib/widgets/or-gauge-widget.js +60 -0
  119. package/lib/widgets/or-gauge-widget.js.map +1 -0
  120. package/lib/widgets/or-kpi-widget.d.ts +44 -0
  121. package/lib/widgets/or-kpi-widget.js +63 -0
  122. package/lib/widgets/or-kpi-widget.js.map +1 -0
  123. package/lib/widgets/or-map-widget.d.ts +49 -0
  124. package/lib/widgets/or-map-widget.js +75 -0
  125. package/lib/widgets/or-map-widget.js.map +1 -0
  126. package/lib/widgets/table-widget.d.ts +25 -0
  127. package/lib/widgets/table-widget.js +12 -0
  128. package/lib/widgets/table-widget.js.map +1 -0
  129. package/package.json +32 -0
  130. package/src/controls/dashboard-refresh-controls.ts +100 -0
  131. package/src/index.ts +731 -0
  132. package/src/or-dashboard-boardsettings.ts +249 -0
  133. package/src/or-dashboard-browser.ts +160 -0
  134. package/src/or-dashboard-engine.ts +17 -0
  135. package/src/or-dashboard-keyhandler.ts +25 -0
  136. package/src/or-dashboard-preview.ts +713 -0
  137. package/src/or-dashboard-tree.ts +304 -0
  138. package/src/or-dashboard-widgetcontainer.ts +155 -0
  139. package/src/or-dashboard-widgetsettings.ts +91 -0
  140. package/src/panels/assettypes-panel.ts +311 -0
  141. package/src/panels/attributes-panel.ts +304 -0
  142. package/src/panels/thresholds-panel.ts +285 -0
  143. package/src/service/dashboard-service.ts +89 -0
  144. package/src/service/widget-service.ts +48 -0
  145. package/src/settings/attribute-input-settings.ts +79 -0
  146. package/src/settings/chart-settings.ts +306 -0
  147. package/src/settings/gauge-settings.ts +93 -0
  148. package/src/settings/image-settings.ts +175 -0
  149. package/src/settings/kpi-settings.ts +106 -0
  150. package/src/settings/map-settings.ts +185 -0
  151. package/src/settings/table-settings.ts +92 -0
  152. package/src/style.ts +104 -0
  153. package/src/util/or-asset-widget.ts +110 -0
  154. package/src/util/or-widget.ts +60 -0
  155. package/src/util/settings-panel.ts +93 -0
  156. package/src/util/widget-config.ts +2 -0
  157. package/src/util/widget-settings.ts +58 -0
  158. package/src/widgets/attribute-input-widget.ts +143 -0
  159. package/src/widgets/chart-widget.ts +203 -0
  160. package/src/widgets/gauge-widget.ts +111 -0
  161. package/src/widgets/image-widget.ts +180 -0
  162. package/src/widgets/kpi-widget.ts +97 -0
  163. package/src/widgets/map-widget.ts +187 -0
  164. package/src/widgets/table-widget.ts +157 -0
  165. package/tsconfig.json +15 -0
  166. package/tsconfig.tsbuildinfo +1 -0
  167. package/webpack.config.js +10 -0
@@ -0,0 +1,713 @@
1
+ import manager, {DefaultColor4, DefaultColor5} from "@openremote/core";
2
+ import {css, html, LitElement, TemplateResult, unsafeCSS} from "lit";
3
+ import {customElement, property, state} from "lit/decorators.js";
4
+ import {style} from "./style";
5
+ import "./or-dashboard-widgetcontainer";
6
+ import {debounce} from "lodash";
7
+ import {DashboardGridItem, DashboardScalingPreset, DashboardScreenPreset, DashboardTemplate, DashboardWidget} from "@openremote/model";
8
+ import {getActivePreset} from "./index";
9
+ import {InputType, OrInputChangedEvent} from "@openremote/or-mwc-components/or-mwc-input";
10
+ import "@openremote/or-components/or-loading-indicator";
11
+ import {repeat} from 'lit/directives/repeat.js';
12
+ import {GridItemHTMLElement, GridStack, GridStackElement, GridStackNode} from "gridstack";
13
+ import {showSnackbar} from "@openremote/or-mwc-components/or-mwc-snackbar";
14
+ import {i18next} from "@openremote/or-translate";
15
+ import {when} from "lit/directives/when.js";
16
+ import {cache} from "lit/directives/cache.js";
17
+ import {guard} from "lit/directives/guard.js";
18
+ import {OrDashboardEngine} from "./or-dashboard-engine";
19
+ import {WidgetService} from "./service/widget-service";
20
+ import {OrDashboardWidgetContainer} from "./or-dashboard-widgetcontainer";
21
+
22
+ // TODO: Add webpack/rollup to build so consumers aren't forced to use the same tooling
23
+ const gridcss = require('gridstack/dist/gridstack.min.css');
24
+ const extracss = require('gridstack/dist/gridstack-extra.css');
25
+
26
+ //language=css
27
+ const editorStyling = css`
28
+
29
+ #loadingContainer {
30
+ position: absolute;
31
+ width: 100%;
32
+ height: 100%;
33
+ display: flex;
34
+ justify-content: center;
35
+ align-items: center;
36
+ }
37
+
38
+ #view-options {
39
+ padding: 24px;
40
+ display: flex;
41
+ justify-content: center;
42
+ align-items: center;
43
+ }
44
+ /* Margins on view options */
45
+ #fit-btn { margin-right: 10px; }
46
+ #view-preset-select { margin-left: 20px; }
47
+ #width-input { margin-left: 20px; }
48
+ #height-input { margin-left: 10px; }
49
+ #rotate-btn { margin-left: 10px; }
50
+
51
+ .maingridContainer {
52
+ position: absolute;
53
+ padding-bottom: 32px;
54
+ }
55
+ .maingridContainer__fullscreen {
56
+ width: 100%;
57
+ }
58
+
59
+ .maingrid {
60
+ border: 3px solid #909090;
61
+ background: #FFFFFF;
62
+ border-radius: 8px;
63
+ overflow-x: hidden;
64
+ overflow-y: scroll;
65
+ padding: 4px;
66
+ z-index: 0;
67
+ }
68
+ .maingrid__fullscreen {
69
+ border: none;
70
+ background: transparent;
71
+ border-radius: 0;
72
+ overflow-x: hidden;
73
+ overflow-y: scroll;
74
+ height: 100% !important; /* To override .maingrid */
75
+ width: 100% !important; /* To override .maingrid */
76
+ padding: 0;
77
+ /*pointer-events: none;*/
78
+ position: relative;
79
+ z-index: 0;
80
+ }
81
+ .maingrid__disabled {
82
+ pointer-events: none;
83
+ opacity: 40%;
84
+ }
85
+ .grid-stack-item-content {
86
+ background: white;
87
+ box-sizing: border-box;
88
+ border: 1px solid var(--or-app-color5, ${unsafeCSS(DefaultColor5)});
89
+ border-radius: 4px;
90
+ }
91
+ .grid-stack > .grid-stack-item > .grid-stack-item-content {
92
+ overflow: visible;
93
+ }
94
+ .grid-stack-item-content__active {
95
+ border: 2px solid var(--or-app-color4, ${unsafeCSS(DefaultColor4)});
96
+ margin: -1px !important; /* to compromise with the extra pixel of border. */
97
+ }
98
+
99
+ /* Grid lines on the background of the grid */
100
+ .grid-element {
101
+ background-image:
102
+ linear-gradient(90deg, #E0E0E0, transparent 1px),
103
+ linear-gradient(90deg, transparent calc(100% - 1px), #E0E0E0),
104
+ linear-gradient(#E0E0E0, transparent 1px),
105
+ linear-gradient(transparent calc(100% - 1px), #E0E0E0 100%);
106
+ }
107
+ `
108
+
109
+ /* -------------------------------------------------- */
110
+
111
+ export interface DashboardGridNode extends GridStackNode {
112
+ widgetTypeId: string;
113
+ }
114
+ export interface DashboardPreviewSize {
115
+ displayName: string;
116
+ width?: number,
117
+ height?: number,
118
+ }
119
+
120
+ /* ------------------------------------------------------------ */
121
+
122
+ @customElement("or-dashboard-preview")
123
+ export class OrDashboardPreview extends LitElement {
124
+
125
+ // Monitoring the changes in the template, save the changes to this.latestChanges,
126
+ // so we can check afterwards which changes are made. Used for
127
+ @property({ hasChanged(oldValue, newValue) { return JSON.stringify(oldValue) != JSON.stringify(newValue); }})
128
+ set template(newValue: DashboardTemplate) {
129
+ const currentValue = this._template;
130
+ if(currentValue != undefined) {
131
+ const changes = {
132
+ changedKeys: Object.keys(newValue).filter(key => (JSON.stringify(newValue[key as keyof DashboardTemplate]) !== JSON.stringify(currentValue[key as keyof DashboardTemplate]))),
133
+ oldValue: currentValue,
134
+ newValue: newValue
135
+ };
136
+ this._template = JSON.parse(JSON.stringify(newValue));
137
+ this.latestChanges = changes;
138
+ this.requestUpdate("template", currentValue);
139
+
140
+ // If there is no value yet, do initial setup:
141
+ } else if(newValue != undefined) {
142
+ this._template = newValue;
143
+ this.setupGrid(false, false);
144
+ }
145
+ }
146
+
147
+ private _template?: DashboardTemplate;
148
+
149
+ get template() {
150
+ return this._template!;
151
+ }
152
+
153
+ /* ------------------------ */
154
+
155
+ @property() // Optional alternative for template
156
+ protected readonly dashboardId?: string;
157
+
158
+ @property() // Normally manager.displayRealm
159
+ protected realm?: string;
160
+
161
+ @property({type: Object})
162
+ protected selectedWidget: DashboardWidget | undefined;
163
+
164
+ @property()
165
+ protected editMode: boolean = false;
166
+
167
+ @property() // For example when no permission
168
+ protected readonly: boolean = true;
169
+
170
+ @property()
171
+ protected previewWidth?: string;
172
+
173
+ @property()
174
+ protected previewHeight?: string;
175
+
176
+ @property()
177
+ protected previewZoom: number = 1;
178
+
179
+ @property()
180
+ protected previewSize?: DashboardPreviewSize;
181
+
182
+ @property()
183
+ protected availablePreviewSizes?: DashboardPreviewSize[];
184
+
185
+ @property()
186
+ protected fullscreen: boolean = true;
187
+
188
+ /* -------------- */
189
+
190
+ @state() // State where the changes of the template are saved temporarily (for comparison with incoming data)
191
+ protected latestChanges?: {
192
+ changedKeys: string[],
193
+ oldValue: DashboardTemplate,
194
+ newValue: DashboardTemplate
195
+ }
196
+
197
+ @state()
198
+ protected activePreset?: DashboardScreenPreset;
199
+
200
+ @state()
201
+ private rerenderActive: boolean = false;
202
+
203
+ @state()
204
+ private isLoading: boolean = false;
205
+
206
+
207
+ protected grid?: GridStack;
208
+ protected latestDragWidgetStart?: Date;
209
+
210
+
211
+ /* ------------------------------------------- */
212
+
213
+ // Using constructor to set initial values
214
+ constructor() {
215
+ super();
216
+ if(!this.realm) { this.realm = manager.displayRealm; }
217
+ if(!this.availablePreviewSizes) {
218
+ this.availablePreviewSizes = [
219
+ { displayName: "4k Television", width: 3840, height: 2160 },
220
+ { displayName: "Desktop", width: 1920, height: 1080 },
221
+ { displayName: "Small desktop", width: 1280, height: 720 },
222
+ { displayName: "Phone", width: 360, height: 800 },
223
+ { displayName: "Custom" }
224
+ ]
225
+ }
226
+ // Defaulting to a Phone view
227
+ if(!this.previewSize) { this.previewSize = this.availablePreviewSizes[3]; }
228
+
229
+ // Register custom override functions for GridStack
230
+ GridStack.registerEngine(OrDashboardEngine);
231
+ }
232
+
233
+ static get styles() {
234
+ return [unsafeCSS(gridcss), unsafeCSS(extracss), editorStyling, style];
235
+ }
236
+
237
+ /* ------------------------------ */
238
+
239
+ // Checking whether actual changes have been made; if not, prevent updating.
240
+ shouldUpdate(changedProperties: Map<PropertyKey, unknown>): boolean {
241
+ const changed = changedProperties;
242
+
243
+ if(changedProperties.has('latestChanges')
244
+ && this.latestChanges?.changedKeys.length == 0
245
+ && (JSON.stringify((changedProperties.get('latestChanges') as any)?.oldValue)) == (JSON.stringify((changedProperties.get('latestChanges') as any)?.newValue))) {
246
+ changed.delete('latestChanges');
247
+ }
248
+
249
+ // Do not update UI if the preview size has changed while being fullscreen,
250
+ // since it is only used when in "responsive mode".
251
+ if(this.fullscreen && changedProperties.has('previewWidth')) {
252
+ changed.delete('previewWidth');
253
+ }
254
+ if(this.fullscreen && changedProperties.has('previewHeight')) {
255
+ changed.delete('previewHeight');
256
+ }
257
+
258
+ return (changed.size === 0 ? false : super.shouldUpdate(changedProperties));
259
+ }
260
+
261
+
262
+ // Main method for executing actions after property changes
263
+ updated(changedProperties: Map<string, any>) {
264
+ super.updated(changedProperties);
265
+
266
+ if(this.realm == undefined) { this.realm = manager.displayRealm; }
267
+
268
+ // Setup template (list of widgets and properties)
269
+ if(!this.template && this.dashboardId) {
270
+ manager.rest.api.DashboardResource.get(this.realm, this.dashboardId)
271
+ .then((response) => { this.template = response.data.template!; })
272
+ .catch((reason) => { console.error(reason); showSnackbar(undefined, "errorOccurred"); });
273
+ } else if(this.template == null && this.dashboardId == null) {
274
+ console.warn("Neither the template nor dashboardId attributes have been specified!");
275
+ }
276
+
277
+ // If changes to the template have been made
278
+ if(changedProperties.has("latestChanges")) {
279
+ if(this.latestChanges) {
280
+ this.processTemplateChanges(this.latestChanges);
281
+ this.latestChanges = undefined;
282
+ }
283
+ }
284
+
285
+ if(changedProperties.has("selectedWidget")) {
286
+ if(this.selectedWidget) {
287
+ if(changedProperties.get("selectedWidget") != undefined) { // if previous selected state was a different widget, dispatch event as well
288
+ this.dispatchEvent(new CustomEvent("deselected", { detail: changedProperties.get("selectedWidget") as DashboardWidget }));
289
+ }
290
+ if(this.grid?.el != null) {
291
+ const foundItem = this.grid?.getGridItems().find((item) => item.gridstackNode?.id == this.selectedWidget?.gridItem?.id);
292
+ if(foundItem != null) { this.selectGridItem(foundItem); }
293
+ this.dispatchEvent(new CustomEvent("selected", { detail: this.selectedWidget }));
294
+ }
295
+
296
+ } else {
297
+ // Checking whether the mainGrid is not destroyed and there are Items to deselect...
298
+ if(this.grid?.el != undefined && this.grid?.getGridItems() != null) {
299
+ this.deselectGridItems(this.grid.getGridItems());
300
+ }
301
+ this.dispatchEvent(new CustomEvent("deselected", { detail: changedProperties.get("selectedWidget") as DashboardWidget }));
302
+ }
303
+ }
304
+
305
+ // Switching edit/view mode needs recreation of Grid
306
+ if(changedProperties.has("editMode")) {
307
+ if(changedProperties.get('editMode') != undefined) {
308
+ this.setupGrid(true, true);
309
+ }
310
+ }
311
+
312
+ // Adjusting previewSize when manual pixels control changes
313
+ if(changedProperties.has("previewWidth") || changedProperties.has("previewHeight")) {
314
+ if(this.template?.screenPresets) {
315
+ this.previewSize = this.availablePreviewSizes?.find(s => ((s.width + "px" == this.previewWidth) && (s.height + "px" == this.previewHeight)));
316
+ }
317
+ }
318
+
319
+ // Adjusting pixels control when previewSize changes.
320
+ if(changedProperties.has('previewSize')) {
321
+ if(this.previewSize) {
322
+ this.previewWidth = this.previewSize.width + "px";
323
+ this.previewHeight = this.previewSize.height + "px";
324
+ }
325
+ }
326
+
327
+ // When parent component requests a forced rerender
328
+ if(changedProperties.has("rerenderActive")) {
329
+ if(this.rerenderActive) {
330
+ this.rerenderActive = false;
331
+ }
332
+ }
333
+ }
334
+
335
+
336
+ /* ---------------------------------------- */
337
+
338
+ // Main setup Grid method (often used)
339
+ async setupGrid(recreate: boolean, force: boolean = false) {
340
+ this.isLoading = true;
341
+ await this.updateComplete;
342
+ let gridElement = this.shadowRoot?.getElementById("gridElement");
343
+ if(gridElement != null) {
344
+ if(recreate && this.grid != null) {
345
+ this.grid.destroy(false);
346
+
347
+ if(force) { // Fully rerender the grid by switching rerenderActive on and off, and continue after that.
348
+ this.rerenderActive = true;
349
+ await this.updateComplete;
350
+ await this.waitUntil((_: any) => !this.rerenderActive);
351
+ gridElement = this.shadowRoot?.getElementById("gridElement");
352
+ this.grid = undefined;
353
+ }
354
+ }
355
+ const width: number = (this.fullscreen ? this.clientWidth : (+(this.previewWidth?.replace(/\D/g, "")!)));
356
+ const newPreset = getActivePreset(width, this.template.screenPresets!);
357
+ if(newPreset?.scalingPreset != this.activePreset?.scalingPreset) {
358
+ if(!(recreate && force)) { // Fully rerender the grid by switching rerenderActive on and off, and continue after that.
359
+ if(!recreate) { // If not destroyed yet, destroy first.
360
+ this.grid?.destroy(false);
361
+ }
362
+ this.rerenderActive = true;
363
+ await this.updateComplete;
364
+ await this.waitUntil((_: any) => !this.rerenderActive);
365
+ gridElement = this.shadowRoot?.getElementById("gridElement");
366
+ this.grid = undefined;
367
+ }
368
+ }
369
+ this.activePreset = newPreset;
370
+
371
+
372
+ // If grid got reset, setup the ResizeObserver again.
373
+ if(this.grid == null) {
374
+ const gridHTML = this.shadowRoot?.querySelector(".maingrid");
375
+ this.setupResizeObserver(gridHTML!);
376
+ }
377
+
378
+ gridElement!.style.maxWidth = this.template.maxScreenWidth + "px";
379
+
380
+ this.grid = GridStack.init({
381
+ acceptWidgets: (this.editMode),
382
+ animate: true,
383
+ cellHeight: (this.activePreset?.scalingPreset === DashboardScalingPreset.WRAP_TO_SINGLE_COLUMN ? (width / (this.template?.columns ? (this.template.columns / 4) : 2)) : 'initial'),
384
+ column: this.template?.columns,
385
+ disableOneColumnMode: (this.activePreset?.scalingPreset !== DashboardScalingPreset.WRAP_TO_SINGLE_COLUMN),
386
+ oneColumnModeDomSort: true,
387
+ draggable: {
388
+ appendTo: 'parent', // Required to work, seems to be Shadow DOM related.
389
+ },
390
+ float: true,
391
+ margin: 5,
392
+ resizable: {
393
+ handles: 'all'
394
+ },
395
+ staticGrid: (this.activePreset?.scalingPreset === DashboardScalingPreset.WRAP_TO_SINGLE_COLUMN ? true : (!this.editMode)),
396
+ styleInHead: false
397
+ }, gridElement!);
398
+
399
+ gridElement!.style.backgroundSize = "" + this.grid.cellWidth() + "px " + this.grid.getCellHeight() + "px";
400
+ gridElement!.style.height = "100%";
401
+ gridElement!.style.minHeight = "100%";
402
+
403
+ // When an item gets dropped ontop of the grid. GridStack docs say:
404
+ // "called when an item has been dropped and accepted over a grid. If the item came from another grid, the previous widget node info will also be sent (but dom item long gone)."
405
+ this.grid.on('dropped', (ev: Event, prevWidget: any, newWidget: GridStackNode | undefined) => this.onWidgetDrop(ev, prevWidget, newWidget as DashboardGridNode));
406
+ this.grid.on('change', (_event: Event, items: any) => {
407
+ if(this.template != null && this.template.widgets != null) {
408
+ (items as GridStackNode[]).forEach(node => {
409
+ const foundWidget: DashboardWidget | undefined = this.template?.widgets?.find(widget => { return widget.gridItem?.id == node.id; });
410
+ foundWidget!.gridItem!.x = node.x;
411
+ foundWidget!.gridItem!.y = node.y;
412
+ foundWidget!.gridItem!.w = node.w;
413
+ foundWidget!.gridItem!.h = node.h;
414
+ });
415
+ this.dispatchEvent(new CustomEvent("changed", {detail: { template: this.template }}));
416
+ }
417
+ });
418
+ this.grid.on('resizestart', (_event: Event) => {
419
+ this.latestDragWidgetStart = new Date();
420
+ });
421
+ this.grid.on('resizestop', (_event: Event) => {
422
+ setTimeout(() => { this.latestDragWidgetStart = undefined; }, 200);
423
+ });
424
+ }
425
+ this.isLoading = false;
426
+ }
427
+
428
+
429
+ /* ------------------------------- */
430
+
431
+ public refreshPreview() {
432
+ this.setupGrid(true, true);
433
+ }
434
+
435
+ public refreshWidgets() {
436
+ this.grid?.getGridItems().forEach(gridItem => {
437
+ const widgetContainer = gridItem.querySelector(OrDashboardWidgetContainer.tagName) as OrDashboardWidgetContainer | null;
438
+ if(widgetContainer) {
439
+ widgetContainer.refreshContent(false);
440
+ }
441
+ })
442
+ }
443
+
444
+ protected selectGridItem(gridItem: GridItemHTMLElement) {
445
+ if(this.grid != null) {
446
+ this.deselectGridItems(this.grid.getGridItems()); // deselecting all other items
447
+ gridItem.querySelectorAll<HTMLElement>(".grid-stack-item-content").forEach((item: HTMLElement) => {
448
+ item.classList.add('grid-stack-item-content__active'); // Apply active CSS class
449
+ });
450
+ }
451
+ }
452
+
453
+ protected deselectGridItem(gridItem: GridItemHTMLElement) {
454
+ gridItem.querySelectorAll<HTMLElement>(".grid-stack-item-content").forEach((item: HTMLElement) => {
455
+ item.classList.remove('grid-stack-item-content__active'); // Remove active CSS class
456
+ });
457
+ }
458
+
459
+ protected deselectGridItems(gridItems: GridItemHTMLElement[]) {
460
+ gridItems.forEach(item => {
461
+ this.deselectGridItem(item);
462
+ })
463
+ }
464
+
465
+ protected onGridItemClick(gridItem: DashboardGridItem | undefined) {
466
+ if(!this.latestDragWidgetStart && !this.grid?.opts.staticGrid) {
467
+ if(!gridItem) {
468
+ this.selectedWidget = undefined;
469
+ } else if(this.selectedWidget?.gridItem?.id != gridItem.id) {
470
+ this.selectedWidget = this.template?.widgets?.find(widget => { return widget.gridItem?.id == gridItem.id; });
471
+ }
472
+ }
473
+ }
474
+
475
+ protected onFitToScreenClick() {
476
+ const container = this.shadowRoot?.querySelector('#container');
477
+ if(container) {
478
+ const zoomWidth = +((0.95 * container.clientWidth) / +this.previewWidth!.replace('px', '')).toFixed(2);
479
+ this.previewZoom = (zoomWidth > 1 ? 1 : zoomWidth);
480
+ }
481
+ }
482
+
483
+ protected isPreviewVisible(): boolean {
484
+ return !this.isLoading && this.activePreset?.scalingPreset != DashboardScalingPreset.BLOCK_DEVICE;
485
+ }
486
+
487
+ // Render
488
+ protected render() {
489
+
490
+ try { // to correct the list of gridItems each render (Hopefully temporarily since it's quite compute heavy)
491
+ if(this.grid?.el && this.grid?.getGridItems()) {
492
+ this.grid?.getGridItems().forEach((gridItem: GridItemHTMLElement) => {
493
+ if(this.template?.widgets?.find((widget) => widget.id == gridItem.id) == undefined) {
494
+ this.grid?.removeWidget(gridItem);
495
+ }
496
+ })
497
+ }
498
+ } catch (e) { console.error(e); }
499
+
500
+ const customPreset = "Custom";
501
+ let screenPresets = this.template?.screenPresets?.map(s => s.displayName);
502
+ screenPresets?.push(customPreset);
503
+ return html`
504
+ <div id="buildingArea" style="display: flex; flex-direction: column; height: 100%; position: relative;" @click="${(event: PointerEvent) => { if((event.composedPath()[1] as HTMLElement).id === 'buildingArea') { this.onGridItemClick(undefined); }}}">
505
+ ${this.editMode && !this.fullscreen ? html`
506
+ <div id="view-options">
507
+ <or-mwc-input id="fit-btn" type="${InputType.BUTTON}" icon="fit-to-screen"
508
+ @or-mwc-input-changed="${() => this.onFitToScreenClick()}">
509
+ </or-mwc-input>
510
+ <or-mwc-input id="zoom-input" type="${InputType.NUMBER}" outlined label="${i18next.t('dashboard.zoomPercent')}" min="25" .value="${(this.previewZoom * 100)}" style="width: 90px"
511
+ @or-mwc-input-changed="${debounce((event: OrInputChangedEvent) => { this.previewZoom = event.detail.value / 100; }, 50)}"
512
+ ></or-mwc-input>
513
+ <or-mwc-input id="view-preset-select" type="${InputType.SELECT}" outlined label="${i18next.t('dashboard.presetSize')}" style="min-width: 220px;"
514
+ .value="${this.previewSize == undefined ? customPreset : this.previewSize.displayName}" .options="${this.availablePreviewSizes?.map((x) => x.displayName)}"
515
+ @or-mwc-input-changed="${(event: OrInputChangedEvent) => { this.previewSize = this.availablePreviewSizes?.find(s => s.displayName == event.detail.value); }}"
516
+ ></or-mwc-input>
517
+ <or-mwc-input id="width-input" type="${InputType.NUMBER}" outlined label="${i18next.t('width')}" min="100" .value="${this.previewWidth?.replace('px', '')}" style="width: 90px"
518
+ @or-mwc-input-changed="${debounce((event: OrInputChangedEvent) => { this.previewWidth = event.detail.value + 'px'; }, 550)}"
519
+ ></or-mwc-input>
520
+ <or-mwc-input id="height-input" type="${InputType.NUMBER}" outlined label="${i18next.t('height')}" min="100" .value="${this.previewHeight?.replace('px', '')}" style="width: 90px;"
521
+ @or-mwc-input-changed="${(event: OrInputChangedEvent) => { this.previewHeight = event.detail.value + 'px'; }}"
522
+ ></or-mwc-input>
523
+ <or-mwc-input id="rotate-btn" type="${InputType.BUTTON}" icon="screen-rotation"
524
+ @or-mwc-input-changed="${() => { const newWidth = this.previewHeight; const newHeight = this.previewWidth; this.previewWidth = newWidth; this.previewHeight = newHeight; }}">
525
+ </or-mwc-input>
526
+ </div>
527
+ ` : undefined}
528
+ ${this.rerenderActive ? html`
529
+ <div id="container" style="justify-content: center; align-items: center;">
530
+ <span><or-translate value="dashboard.renderingGrid"></or-translate></span>
531
+ </div>
532
+ ` : html`
533
+ <div id="container" style="justify-content: center; position: relative;">
534
+ ${when(this.isLoading, () => html`
535
+ <div style="position: absolute; z-index: 3; height: 100%; display: flex; align-items: center;">
536
+ <or-loading-indicator></or-loading-indicator>
537
+ </div>
538
+ `, () => html`
539
+ ${this.activePreset?.scalingPreset == DashboardScalingPreset.BLOCK_DEVICE ? html`
540
+ <div style="position: absolute; z-index: 3; height: 100%; display: flex; align-items: center;">
541
+ <span><or-translate value="dashboard.deviceNotSupported"></or-translate></span>
542
+ </div>
543
+ ` : undefined}
544
+ `)}
545
+ <!-- The grid itself. Will also show during isLoading, but will be invisible through CSS -->
546
+ <div class="${this.fullscreen ? 'maingridContainer__fullscreen' : 'maingridContainer'}" style="${this.isLoading ? 'visibility: hidden;' : ''}">
547
+ <div class="maingrid ${this.fullscreen ? 'maingrid__fullscreen' : undefined}"
548
+ style="width: ${this.previewWidth}; height: ${this.previewHeight}; visibility: ${this.isPreviewVisible() ? 'visible' : 'hidden'}; zoom: ${this.editMode && !this.fullscreen ? this.previewZoom : 'normal'}; ${this.editMode && !this.fullscreen ? ('-moz-transform: scale(' + this.previewZoom + ')') : undefined}; transform-origin: top;"
549
+ @click="${(ev: MouseEvent) => {
550
+ if ((ev.composedPath()[0] as HTMLElement).id == 'gridElement') {
551
+ this.onGridItemClick(undefined)
552
+ }
553
+ }}">
554
+ ${guard([this.editMode, this.template], () => html`
555
+ <!-- Gridstack element on which the Grid will be rendered -->
556
+ <div id="gridElement" class="grid-stack ${this.editMode ? 'grid-element' : undefined}" style="margin: auto;">
557
+ ${this.template?.widgets ? repeat(this.template.widgets, (item) => item.id, (widget) => {
558
+ return html`
559
+ <div class="grid-stack-item" id="${widget.id}" gs-id="${widget.gridItem?.id}" gs-x="${widget.gridItem?.x}" gs-y="${widget.gridItem?.y}"
560
+ gs-w="${widget.gridItem?.w}" gs-h="${widget.gridItem?.h}" gs-min-w="${widget.gridItem?.minW}" gs-min-h="${widget.gridItem?.minH}"
561
+ @click="${() => {
562
+ this.onGridItemClick(widget.gridItem);
563
+ }}">
564
+ <div class="grid-stack-item-content" style="display: flex;">
565
+ <or-dashboard-widget-container .widget="${widget}" .editMode="${this.editMode}" style="width: 100%; height: auto; border-radius: 4px;"></or-dashboard-widget-container>
566
+ </div>
567
+ </div>
568
+ `
569
+ }) : undefined}
570
+ </div>
571
+ `)}
572
+ </div>
573
+ </div>
574
+ </div>
575
+ `}
576
+ </div>
577
+ <style>
578
+ ${cache(when(this.isExtraLargeGrid(),
579
+ () => this.applyCustomGridstackGridCSS(this.getGridstackColumns(this.grid) ? this.getGridstackColumns(this.grid)! : this.template.columns!)
580
+ ))}
581
+ </style>
582
+ `
583
+ }
584
+
585
+ protected getGridstackColumns(grid: GridStack | undefined): number | undefined {
586
+ try { return grid?.getColumn(); }
587
+ catch (e) { return undefined; }
588
+ }
589
+
590
+ protected isExtraLargeGrid(): boolean {
591
+ return !!this.grid && (
592
+ (this.getGridstackColumns(this.grid) && this.getGridstackColumns(this.grid)! > 12)
593
+ || !!(this.template?.columns && this.template.columns > 12)
594
+ );
595
+ }
596
+
597
+
598
+
599
+ private cachedGridstackCSS: Map<number, TemplateResult[]> = new Map<number, TemplateResult[]>();
600
+
601
+ // Provides support for > 12 columns in GridStack (which requires manual css edits)
602
+ //language=html
603
+ protected applyCustomGridstackGridCSS(columns: number): TemplateResult {
604
+ if(this.cachedGridstackCSS.has(columns)) {
605
+ return html`${this.cachedGridstackCSS.get(columns)!.map((x) => x)}`;
606
+ } else {
607
+ const htmls: TemplateResult[] = [];
608
+ for(let i = 0; i < (columns + 1); i++) {
609
+ htmls.push(html`
610
+ <style>
611
+ .grid-stack > .grid-stack-item[gs-w="${i}"]:not(.ui-draggable-dragging):not(.ui-resizable-resizing) { width: ${100 - (columns - i) * (100 / columns)}% !important; }
612
+ .grid-stack > .grid-stack-item[gs-x="${i}"]:not(.ui-draggable-dragging):not(.ui-resizable-resizing) { left: ${100 - (columns - i) * (100 / columns)}% !important; }
613
+ </style>
614
+ `);
615
+ }
616
+ this.cachedGridstackCSS.set(columns, htmls);
617
+ return html`${htmls.map((x) => x)}`;
618
+ }
619
+ }
620
+
621
+
622
+
623
+ /* ---------------------------------------------- */
624
+
625
+ protected resizeObserver?: ResizeObserver;
626
+ protected previousObserverEntry?: ResizeObserverEntry;
627
+
628
+ disconnectedCallback() {
629
+ super.disconnectedCallback()
630
+ this.resizeObserver?.disconnect();
631
+ }
632
+
633
+ // Triggering a Grid rerender on every time the element resizes.
634
+ // In fullscreen, debounce (only trigger after 550ms of no changes) to limit amount of rerenders.
635
+ protected setupResizeObserver(element: Element): ResizeObserver {
636
+ this.resizeObserver?.disconnect();
637
+ if(this.fullscreen) {
638
+ this.resizeObserver = new ResizeObserver(debounce(this.resizeObserverCallback, 200));
639
+ } else {
640
+ this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
641
+ }
642
+ this.resizeObserver.observe(element);
643
+ return this.resizeObserver;
644
+ }
645
+
646
+ protected resizeObserverCallback: ResizeObserverCallback = (entries: ResizeObserverEntry[]) => {
647
+ if((this.previousObserverEntry?.contentRect.width + "px") !== (entries[0].contentRect.width + "px")) {
648
+ this._onGridResize();
649
+ }
650
+ this.previousObserverEntry = entries[0];
651
+ }
652
+
653
+ protected _onGridResize() {
654
+ this.setupGrid(true, false);
655
+ }
656
+
657
+ /* --------------------------------------- */
658
+
659
+ protected processTemplateChanges(changes: { changedKeys: string[], oldValue: DashboardTemplate, newValue: DashboardTemplate }) {
660
+
661
+ // If only columns property changed, change columns through the framework and then recreate grid.
662
+ if(changes.changedKeys.length == 1 && changes.changedKeys.includes('columns') && this.grid) {
663
+ this.grid.column(changes.newValue.columns!);
664
+ let maingrid = this.shadowRoot?.querySelector(".maingrid");
665
+ let gridElement = this.shadowRoot?.getElementById("gridElement");
666
+ gridElement!.style.backgroundSize = "" + this.grid.cellWidth() + "px " + this.grid.getCellHeight() + "px";
667
+ gridElement!.style.height = maingrid!.scrollHeight + 'px';
668
+ this.setupGrid(true, false);
669
+ }
670
+
671
+ // If multiple properties changed, just force rerender all of it.
672
+ else if(changes.changedKeys.length > 1) {
673
+ this.setupGrid(true, true);
674
+ }
675
+
676
+ // On widgets change, check whether they are programmatically added to GridStack. If not, adding them.
677
+ else if(changes.changedKeys.includes('widgets')) {
678
+ if(this.grid?.el != null) {
679
+ this.grid.getGridItems().forEach((gridElement) => {
680
+ if(!gridElement.classList.contains('ui-draggable')) {
681
+ this.grid?.makeWidget(gridElement);
682
+ }
683
+ })
684
+ }
685
+ }
686
+ else if(changes.changedKeys.includes('screenPresets')) {
687
+ this.setupGrid(true, true);
688
+ }
689
+ }
690
+
691
+ // Wait until function that waits until a boolean returns differently
692
+ // TODO: Remove this, and replace 'waiting' functionality with observer pattern principles.
693
+ protected waitUntil(conditionFunction: any) {
694
+ const poll = (resolve: any) => {
695
+ if(conditionFunction()) resolve();
696
+ else setTimeout(_ => poll(resolve), 400);
697
+ }
698
+ return new Promise(poll);
699
+ }
700
+
701
+ // Callback method for GridStack Grid 'dropped' event. GridStack docs say:
702
+ // called when an item has been dropped and accepted over a grid. If the item came from another grid, the previous widget node info will also be sent (but dom item long gone).
703
+ protected onWidgetDrop(_ev: Event, _prevWidget: any, newWidget: DashboardGridNode | undefined) {
704
+
705
+ // When a "Widget Card" gets dropped onto the grid, we create a new widget on those coordinates.
706
+ if(this.grid && newWidget) {
707
+ this.grid.removeWidget((newWidget.el) as GridStackElement, true, false); // Removes dragged widget first
708
+ WidgetService.placeNew(newWidget.widgetTypeId, newWidget.x!, newWidget.y!).then((widget) => {
709
+ this.dispatchEvent(new CustomEvent("created", { detail: widget }));
710
+ });
711
+ }
712
+ }
713
+ }