@beforesemicolon/site-builder 0.35.0 → 0.36.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,249 @@
1
+ import { renderFileUploadControl } from './file.control.js'
2
+ import { renderQuillEditor } from './html.control.js'
3
+ import { renderCodeEditor } from './code.control.js'
4
+ import { renderMarkdownEditor } from './markdown.control.js'
5
+ import {
6
+ currentWidget,
7
+ currentWidgetId,
8
+ originalWidgetValues,
9
+ getPendingChange,
10
+ removePendingChange,
11
+ addPendingChange,
12
+ panel,
13
+ currentPage,
14
+ } from '../data.js'
15
+
16
+ const { html, repeat, pick, when, is } = window.BFS.MARKUP
17
+
18
+ const FILE_TYPE_PATTERN = /image|video|audio|file|font|icon|pdf/
19
+
20
+ const resolveChangeType = () => {
21
+ if (panel() === 'page') {
22
+ return currentWidgetId() ? 'page-widget' : 'page'
23
+ }
24
+
25
+ return 'widget'
26
+ }
27
+
28
+ const resolveChangeId = (changeType, widgetId, pageId) => {
29
+ if (changeType === 'page') {
30
+ return pageId
31
+ }
32
+ return widgetId
33
+ }
34
+
35
+ const handleControlValueChange = (
36
+ path,
37
+ value,
38
+ inputType = 'text',
39
+ fileData = null
40
+ ) => {
41
+ const pathIndexes = String(path).split('.')
42
+ const widget = { ...currentWidget() }
43
+ const widgetId = currentWidgetId()
44
+ const pageId = currentPage()?.id ?? ''
45
+ const changeType = resolveChangeType()
46
+ const changeId = resolveChangeId(changeType, widgetId, pageId)
47
+ const originalKey =
48
+ changeType === 'page-widget' ? `${pageId}:${widgetId}` : widgetId
49
+
50
+ const i = pathIndexes.shift()
51
+
52
+ // copy to force update
53
+ widget.inputs[i] = {
54
+ ...widget.inputs[i],
55
+ }
56
+
57
+ // use pathIndexes to locate the input nested items
58
+ let currentInput = widget.inputs[i]
59
+
60
+ while (pathIndexes.length) {
61
+ if (!currentInput.definitions) {
62
+ currentInput = null
63
+ break
64
+ }
65
+
66
+ currentInput = currentInput.definitions[pathIndexes.shift()]
67
+ }
68
+
69
+ if (currentInput) {
70
+ // Get the original value for this path
71
+ const originalValue =
72
+ originalWidgetValues.get(originalKey)?.[path] ?? null
73
+ // update in place
74
+ currentInput.value = value
75
+
76
+ // Get current widget changes or create new one
77
+ const currentWidgetChanges = getPendingChange(changeId) ?? {
78
+ type: changeType,
79
+ id: changeId,
80
+ changes: {},
81
+ }
82
+
83
+ // Check if the new value matches the original value
84
+ if (value === originalValue) {
85
+ // Value reverted to original - remove from pending changes
86
+ delete currentWidgetChanges.changes[path]
87
+
88
+ // If no more changes for this widget, remove it entirely
89
+ if (Object.keys(currentWidgetChanges.changes).length === 0) {
90
+ removePendingChange(changeId)
91
+ } else {
92
+ addPendingChange(changeId, currentWidgetChanges)
93
+ }
94
+ } else {
95
+ // Value is different from original - add/update pending change
96
+ const changeEntry = {
97
+ originalValue,
98
+ newValue: value,
99
+ inputType,
100
+ }
101
+
102
+ // Add file data if this is a file upload (Requirements 1.3, 3.4)
103
+ if (fileData && FILE_TYPE_PATTERN.test(inputType)) {
104
+ changeEntry.fileData = fileData
105
+ }
106
+
107
+ currentWidgetChanges.changes[path] = changeEntry
108
+ addPendingChange(changeId, currentWidgetChanges)
109
+ }
110
+ }
111
+ }
112
+
113
+ const inputControl = ({ type, value, name, dir, readonly, ...otherProps }) => {
114
+ return html` <label class="control-field-block">
115
+ <span class="label">${name}</span>
116
+ <input ${otherProps} type="${type}" value="${value}" name="${name}" />
117
+ </label>`
118
+ }
119
+
120
+ const renderInputControl = (
121
+ input,
122
+ idx = 0,
123
+ disabled,
124
+ onChange = handleControlValueChange
125
+ ) => {
126
+ const { type, value, name } = input
127
+
128
+ if (input.readonly) {
129
+ return ''
130
+ }
131
+
132
+ // convert name from camel case to space between words as label
133
+ const label = (name ?? '')
134
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
135
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
136
+
137
+ switch (type) {
138
+ case 'text':
139
+ case 'email':
140
+ case 'url':
141
+ case 'tel':
142
+ case 'number':
143
+ case 'color':
144
+ case 'date':
145
+ case 'datetime-local':
146
+ case 'month':
147
+ case 'time':
148
+ case 'week':
149
+ case 'password':
150
+ return inputControl({
151
+ ...input,
152
+ name: label,
153
+ oninput: (e) => onChange(idx, e.target.value, type),
154
+ disabled,
155
+ })
156
+ case 'html':
157
+ return renderQuillEditor(input, idx, disabled, label, onChange)
158
+ case 'code':
159
+ return renderCodeEditor(input, idx, disabled, label, onChange)
160
+ case 'markdown':
161
+ return renderMarkdownEditor(input, idx, disabled, label, onChange)
162
+ case 'textarea':
163
+ return html` <label class="control-field-block">
164
+ <span class="label">${label}</span>
165
+ <textarea
166
+ rows="3"
167
+ cols="30"
168
+ disabled="${disabled}"
169
+ name="${name}"
170
+ oninput="${(e) => onChange(idx, e.target.value, type)}"
171
+ >
172
+ ${value}</textarea
173
+ >
174
+ </label>`
175
+ case 'list':
176
+ case 'group':
177
+ case 'options':
178
+ return html`
179
+ <div class="control-field-block">
180
+ ${name ? html`<h4 class="label">${label}</h4>` : ''}
181
+ <div class="nested-control">
182
+ ${input.definitions.map((item, i) =>
183
+ renderInputControl(
184
+ {
185
+ readonly: input.readonly ?? item.readonly,
186
+ ...item,
187
+ },
188
+ `${idx}.${i}`,
189
+ disabled,
190
+ onChange
191
+ )
192
+ )}
193
+ </div>
194
+ </div>
195
+ `
196
+ case 'boolean':
197
+ return inputControl({
198
+ ...input,
199
+ type: 'checkbox',
200
+ name: label,
201
+ oninput: (e) => onChange(idx, e.target.checked, type),
202
+ disabled,
203
+ })
204
+ case 'embed':
205
+ return inputControl({
206
+ ...input,
207
+ type: 'url',
208
+ name: label,
209
+ oninput: (e) => onChange(idx, e.target.value, type),
210
+ disabled,
211
+ })
212
+ case 'image':
213
+ case 'video':
214
+ case 'audio':
215
+ case 'file':
216
+ case 'font':
217
+ case 'icon':
218
+ case 'pdf':
219
+ return renderFileUploadControl(
220
+ input,
221
+ idx,
222
+ (path, value, inputType, fileData) =>
223
+ onChange(path, value, inputType, fileData)
224
+ )
225
+ default:
226
+ return ''
227
+ }
228
+ }
229
+
230
+ export const Controls = ({ disabled }) => html`
231
+ <header>
232
+ ${when(
233
+ is(panel, 'page'),
234
+ html`<div class="info-alert">
235
+ All changes done to this page will only apply to this page and
236
+ its content.
237
+ </div>`,
238
+ html`<div class="info-warning">
239
+ All changes done to this widget will apply to everywhere this
240
+ widget is used.
241
+ </div>`
242
+ )}
243
+ </header>
244
+ <div class="content">
245
+ ${repeat(pick(currentWidget, 'inputs'), (input, idx) =>
246
+ renderInputControl(input, idx, disabled)
247
+ )}
248
+ </div>
249
+ `