@reldens/cms 0.21.0 → 0.24.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Reldens CMS
4
4
 
5
- A powerful, flexible Content Management System built with Node.js, featuring an admin panel, multi-domain frontend support, enhanced templating with reusable content blocks, system variables, internationalization, template reloading, and automated installation.
5
+ A powerful, flexible Content Management System built with Node.js, featuring an admin panel, multi-domain frontend support, enhanced templating with reusable content blocks, system variables, internationalization, template reloading, dynamic forms, and automated installation.
6
6
 
7
7
  ## Features
8
8
 
@@ -30,6 +30,7 @@ A powerful, flexible Content Management System built with Node.js, featuring an
30
30
  - **Event-driven rendering** with hooks for customization
31
31
  - **Custom 404 handling**
32
32
  - **Advanced search functionality** with template data support
33
+ - **Dynamic forms system** with template transformers and security features
33
34
  - **Template reloading** for development with configurable reload strategies
34
35
 
35
36
  ### - Admin Panel
@@ -49,6 +50,7 @@ A powerful, flexible Content Management System built with Node.js, featuring an
49
50
  - **Translation support** for entity labels and properties
50
51
  - **Content blocks management** via cms_blocks table
51
52
  - **Entity access control** via entities_access table
53
+ - **Dynamic forms storage** via cms_forms and cms_forms_submitted tables
52
54
 
53
55
  ### - Configuration & Architecture
54
56
  - **Environment-based configuration** (.env file)
@@ -74,6 +76,7 @@ The CMS uses a modular architecture with specialized classes:
74
76
  **Request Processing:**
75
77
  - `RequestProcessor` - HTTP request routing and path handling
76
78
  - `SearchRequestHandler` - Dedicated search request processing
79
+ - `DynamicFormRequestHandler` - Form submission processing
77
80
 
78
81
  **Content Management:**
79
82
  - `ContentRenderer` - Content generation and template processing
@@ -85,6 +88,11 @@ The CMS uses a modular architecture with specialized classes:
85
88
  **Template Processing:**
86
89
  - `TemplateEngine` - Core template rendering with enhanced context
87
90
  - `SystemVariablesProvider` - System variables for templates
91
+ - `FormsTransformer` - Dynamic forms template transformer
92
+
93
+ **Forms System:**
94
+ - `DynamicForm` - Form validation and data processing
95
+ - `DynamicFormRenderer` - Template-based form rendering
88
96
 
89
97
  This architecture follows SOLID principles, providing better:
90
98
  - **Testability** - Individual components can be tested in isolation
@@ -195,6 +203,349 @@ const cms = new Manager({
195
203
  });
196
204
  ```
197
205
 
206
+ ## Dynamic Forms System
207
+
208
+ ### Basic Form Usage
209
+ Create forms in your templates using the `<cmsForm>` tag:
210
+
211
+ ```html
212
+ <!-- Render all form fields -->
213
+ <cmsForm key="contactForm"/>
214
+
215
+ <!-- Render specific fields only -->
216
+ <cmsForm key="contactForm" fields="name,email,subject,message"/>
217
+
218
+ <!-- Custom form attributes -->
219
+ <cmsForm key="newsletterSignup"
220
+ fields="email,name"
221
+ submitButtonText="Subscribe Now"
222
+ cssClass="newsletter-form"
223
+ successRedirect="/thank-you"
224
+ errorRedirect="/contact-error"/>
225
+ ```
226
+
227
+ ### Form Configuration in Database
228
+ Forms are configured in the `cms_forms` table via the admin panel:
229
+
230
+ ```sql
231
+ -- Example form configuration
232
+ INSERT INTO cms_forms (form_key, fields_schema, enabled) VALUES
233
+ ('contactForm', '[
234
+ {
235
+ "name": "name",
236
+ "type": "text",
237
+ "label": "Full Name",
238
+ "required": true,
239
+ "maxLength": 100,
240
+ "placeholder": "Enter your full name"
241
+ },
242
+ {
243
+ "name": "email",
244
+ "type": "email",
245
+ "label": "Email Address",
246
+ "required": true,
247
+ "placeholder": "your@email.com"
248
+ },
249
+ {
250
+ "name": "subject",
251
+ "type": "select",
252
+ "label": "Subject",
253
+ "required": true,
254
+ "options": [
255
+ {"value": "general", "label": "General Inquiry"},
256
+ {"value": "support", "label": "Technical Support"},
257
+ {"value": "sales", "label": "Sales Question"}
258
+ ]
259
+ },
260
+ {
261
+ "name": "message",
262
+ "type": "textarea",
263
+ "label": "Message",
264
+ "required": true,
265
+ "maxLength": 1000,
266
+ "placeholder": "Enter your message here..."
267
+ }
268
+ ]', 1);
269
+ ```
270
+
271
+ ### Supported Field Types
272
+ The forms system supports various field types with validation:
273
+
274
+ - **text** - Basic text input with maxLength, pattern validation
275
+ - **email** - Email input with built-in email validation
276
+ - **number** - Numeric input with min/max validation
277
+ - **textarea** - Multi-line text with maxLength
278
+ - **select** - Dropdown with options array
279
+ - **password** - Password input (masked)
280
+ - **tel** - Phone number input
281
+ - **url** - URL input with validation
282
+ - **date** - Date picker input
283
+
284
+ ### Field Schema Properties
285
+ Each field in the `fields_schema` JSON supports:
286
+
287
+ ```json
288
+ {
289
+ "name": "fieldName", // Required: Field identifier
290
+ "type": "text", // Required: Field type
291
+ "label": "Field Label", // Display label
292
+ "required": true, // Validation: required field
293
+ "placeholder": "Enter text...", // Input placeholder
294
+ "helpText": "Additional help", // Help text below field
295
+ "maxLength": 100, // String length limit
296
+ "minLength": 3, // Minimum string length
297
+ "pattern": "^[A-Za-z]+$", // Regex validation pattern
298
+ "min": 0, // Number minimum value
299
+ "max": 100, // Number maximum value
300
+ "step": 1, // Number step increment
301
+ "defaultValue": "default", // Default field value
302
+ "options": [ // Select/radio options
303
+ {"value": "val1", "label": "Option 1"},
304
+ {"value": "val2", "label": "Option 2"}
305
+ ]
306
+ }
307
+ ```
308
+
309
+ ### Security Features
310
+ The form system includes comprehensive security measures:
311
+
312
+ #### 1. Honeypot Protection
313
+ Automatic bot detection using invisible fields:
314
+ ```html
315
+ <!-- Automatically added to all forms -->
316
+ <div class="hidden">
317
+ <input type="text" name="website_url" value="" />
318
+ </div>
319
+ ```
320
+
321
+ #### 2. Server-Side Validation
322
+ - **SchemaValidator integration** - Uses `@reldens/utils` SchemaValidator
323
+ - **Required field validation** - Ensures all required fields are provided
324
+ - **Type validation** - Email, number, string validation with patterns
325
+ - **Length limits** - Configurable per field via schema
326
+ - **Custom validation** - Extensible validation rules
327
+
328
+ #### 3. Data Sanitization
329
+ - **XSS protection** - Handled by `@reldens/server-utils` SecurityConfigurer
330
+ - **Input normalization** - Type-specific data processing
331
+ - **Length truncation** - Based on field schema maxLength
332
+
333
+ #### 4. Rate Limiting
334
+ - **AppServerFactory integration** - Uses existing rate limiting from server-utils
335
+ - **No duplicate implementation** - Leverages proven security measures
336
+
337
+ ### Template Customization
338
+ Forms use a domain-aware template fallback system:
339
+
340
+ ```
341
+ templates/
342
+ ├── domains/
343
+ │ └── example.com/
344
+ │ └── cms_forms/
345
+ │ ├── form.html # Domain-specific form wrapper
346
+ │ ├── field_text.html # Domain-specific text field
347
+ │ └── field_email.html # Domain-specific email field
348
+ └── cms_forms/ # Default templates
349
+ ├── form.html # Main form wrapper
350
+ ├── field_text.html # Text input template
351
+ ├── field_email.html # Email input template
352
+ ├── field_textarea.html # Textarea template
353
+ ├── field_select.html # Select dropdown template
354
+ └── field_number.html # Number input template
355
+ ```
356
+
357
+ ### Custom Field Templates
358
+ Create custom field templates for specific types:
359
+
360
+ **templates/cms_forms/field_text.html:**
361
+ ```html
362
+ <div class="form-field {{errorClass}} {{requiredClass}}">
363
+ <label for="{{fieldName}}" class="form-label">
364
+ {{fieldLabel}}{{#isRequired}} <span class="required-indicator">*</span>{{/isRequired}}
365
+ </label>
366
+ <input type="{{fieldType}}"
367
+ name="submittedValues[{{fieldName}}]"
368
+ id="{{fieldName}}"
369
+ value="{{fieldValue}}"
370
+ class="form-control {{#hasError}}is-invalid{{/hasError}}"
371
+ {{#isRequired}}required{{/isRequired}}
372
+ {{#placeholder}}placeholder="{{placeholder}}"{{/placeholder}}
373
+ {{#maxLength}}maxlength="{{maxLength}}"{{/maxLength}}
374
+ {{#pattern}}pattern="{{pattern}}"{{/pattern}} />
375
+ {{#helpText}}<div class="form-text">{{helpText}}</div>{{/helpText}}
376
+ {{#hasError}}<div class="invalid-feedback">{{fieldError}}</div>{{/hasError}}
377
+ </div>
378
+ ```
379
+
380
+ **templates/cms_forms/form.html:**
381
+ ```html
382
+ <form method="POST" action="{{submitUrl}}" class="{{cssClass}}">
383
+ <input type="hidden" name="formKey" value="{{formKey}}" />
384
+ <input type="hidden" name="successRedirect" value="{{successRedirect}}" />
385
+ <input type="hidden" name="errorRedirect" value="{{errorRedirect}}" />
386
+ <div class="hidden">
387
+ <input type="text" name="{{honeypotFieldName}}" value="" />
388
+ </div>
389
+ {{&formFields}}
390
+ <div class="form-submit">
391
+ <button type="submit" class="btn btn-primary">{{submitButtonText}}</button>
392
+ </div>
393
+ </form>
394
+ ```
395
+
396
+ ### Forms with System Variables
397
+ Forms can access system variables and enhanced data in templates:
398
+
399
+ ```html
400
+ <!-- Form with the current user context -->
401
+ <cmsForm key="userProfile" fields="name,email,bio"/>
402
+
403
+ <!-- In the form template, access system variables: -->
404
+ <form method="POST" action="{{submitUrl}}" class="{{cssClass}}">
405
+ <h2>Update Profile for {{currentRequest.host}}</h2>
406
+ <p>Current time: {{systemInfo.timestamp}}</p>
407
+ {{&formFields}}
408
+ <button type="submit">Update Profile</button>
409
+ </form>
410
+ ```
411
+
412
+ ### Event System Integration
413
+ The forms system provides comprehensive event hooks:
414
+
415
+ ```javascript
416
+ // Listen for form events
417
+ cms.events.on('reldens.formsTransformer.beforeRender', (eventData) => {
418
+ console.log('Rendering form:', eventData.formKey);
419
+ // Modify form attributes or fields before rendering
420
+ eventData.formAttributes.cssClass += ' custom-form';
421
+ });
422
+
423
+ cms.events.on('reldens.dynamicForm.beforeValidation', (eventData) => {
424
+ console.log('Validating form:', eventData.formKey);
425
+ // Add custom validation logic
426
+ });
427
+
428
+ cms.events.on('reldens.dynamicForm.afterSave', (eventData) => {
429
+ console.log('Form saved:', eventData.result.id);
430
+ // Send notifications, trigger workflows, etc.
431
+ });
432
+
433
+ cms.events.on('reldens.dynamicFormRequestHandler.beforeSave', (eventData) => {
434
+ // Modify prepared values before saving
435
+ eventData.preparedValues.submissionDate = new Date().toISOString();
436
+ });
437
+ ```
438
+
439
+ ### Available Form Events
440
+ - `reldens.formsTransformer.beforeRender` - Before form rendering
441
+ - `reldens.formsTransformer.afterRender` - After form rendering
442
+ - `reldens.dynamicForm.beforeValidation` - Before form validation
443
+ - `reldens.dynamicForm.afterValidation` - After form validation
444
+ - `reldens.dynamicForm.beforeSave` - Before saving to the database
445
+ - `reldens.dynamicForm.afterSave` - After successful save
446
+ - `reldens.dynamicFormRenderer.beforeFieldsRender` - Before rendering fields
447
+ - `reldens.dynamicFormRenderer.afterFieldsRender` - After rendering fields
448
+ - `reldens.dynamicFormRequestHandler.beforeValidation` - Before request validation
449
+ - `reldens.dynamicFormRequestHandler.beforeSave` - Before save process
450
+ - `reldens.dynamicFormRequestHandler.afterSave` - After successful save
451
+
452
+ ### Database Tables
453
+ The forms system uses two main tables:
454
+
455
+ #### cms_forms Table
456
+ Store form configurations:
457
+ ```sql
458
+ CREATE TABLE `cms_forms` (
459
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
460
+ `form_key` VARCHAR(255) NOT NULL UNIQUE,
461
+ `fields_schema` JSON NOT NULL,
462
+ `enabled` TINYINT UNSIGNED NOT NULL DEFAULT '0',
463
+ `created_at` TIMESTAMP NOT NULL DEFAULT (NOW()),
464
+ `updated_at` TIMESTAMP NOT NULL DEFAULT (NOW()) ON UPDATE CURRENT_TIMESTAMP,
465
+ PRIMARY KEY (`id`)
466
+ );
467
+ ```
468
+
469
+ #### cms_forms_submitted Table
470
+ Store form submissions:
471
+ ```sql
472
+ CREATE TABLE `cms_forms_submitted` (
473
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
474
+ `form_id` INT UNSIGNED NOT NULL,
475
+ `submitted_values` JSON NOT NULL,
476
+ `created_at` TIMESTAMP NOT NULL DEFAULT (NOW()),
477
+ PRIMARY KEY (`id`),
478
+ FOREIGN KEY (`form_id`) REFERENCES `cms_forms`(`id`)
479
+ );
480
+ ```
481
+
482
+ ### Form Processing Flow
483
+ 1. **Template Processing** - `FormsTransformer` finds `<cmsForm>` tags
484
+ 2. **Form Loading** - Loads form configuration from database
485
+ 3. **Field Filtering** - Applies field filter if specified
486
+ 4. **Template Rendering** - Renders form using domain-aware templates
487
+ 5. **Form Submission** - POST request to `/dynamic-form` endpoint
488
+ 6. **Validation** - Honeypot, required fields, and schema validation
489
+ 7. **Data Processing** - Input sanitization and normalization
490
+ 8. **Database Storage** - Save to `cms_forms_submitted` table
491
+ 9. **Response** - Redirect with success/error parameters
492
+
493
+ ### Advanced Form Usage
494
+
495
+ #### Multi-Step Forms
496
+ ```html
497
+ <!-- Step 1: Basic info -->
498
+ <cmsForm key="applicationForm" fields="name,email,phone"/>
499
+
500
+ <!-- Step 2: Details (separate form) -->
501
+ <cmsForm key="applicationDetails" fields="experience,portfolio"/>
502
+ ```
503
+
504
+ #### Conditional Field Display
505
+ Use JavaScript to show/hide fields based on selections:
506
+ ```html
507
+ <cmsForm key="surveyForm" fields="age,experience,expertise"/>
508
+
509
+ <script>
510
+ document.addEventListener('DOMContentLoaded', function() {
511
+ const ageField = document.getElementById('age');
512
+ const experienceField = document.getElementById('experience');
513
+
514
+ ageField.addEventListener('change', function() {
515
+ if(parseInt(this.value) >= 18) {
516
+ experienceField.parentElement.style.display = 'block';
517
+ return;
518
+ }
519
+ experienceField.parentElement.style.display = 'none';
520
+ });
521
+ });
522
+ </script>
523
+ ```
524
+
525
+ #### AJAX Form Submissions
526
+ Enable JSON responses for AJAX handling:
527
+ ```javascript
528
+ const cms = new Manager({
529
+ enableJsonResponse: true // Enable JSON responses for forms
530
+ });
531
+ ```
532
+
533
+ ```javascript
534
+ // Frontend AJAX handling
535
+ document.querySelector('.dynamic-form').addEventListener('submit', async function(e) {
536
+ e.preventDefault();
537
+
538
+ const response = await fetch('/dynamic-form', {method: 'POST', body: new FormData(this)});
539
+
540
+ const result = await response.json();
541
+ if(result.success) {
542
+ alert('Form submitted successfully!');
543
+ return;
544
+ }
545
+ alert('Error: ' + result.error);
546
+ });
547
+ ```
548
+
198
549
  ## Search Functionality
199
550
 
200
551
  ### Basic Search
@@ -656,7 +1007,7 @@ The CMS uses a two-tier layout system:
656
1007
 
657
1008
  Pages can use different layouts by setting the `layout` field in `cms_pages`:
658
1009
  - `default` - Header, sidebar, main content, footer
659
- - `full-width` - Full width without sidebars
1010
+ - `full-width` - Full width without sidebars
660
1011
  - `minimal` - Basic layout with minimal styling
661
1012
 
662
1013
  ### Content Blocks
@@ -695,6 +1046,9 @@ templates/
695
1046
  │ │ ├── partials/
696
1047
  │ │ │ ├── header.html
697
1048
  │ │ │ └── footer.html
1049
+ │ │ ├── cms_forms/ # Domain-specific form templates
1050
+ │ │ │ ├── form.html
1051
+ │ │ │ └── field_text.html
698
1052
  │ │ ├── page.html # Domain-specific page wrapper
699
1053
  │ │ └── index.html
700
1054
  │ └── dev.example.com/
@@ -702,6 +1056,10 @@ templates/
702
1056
  ├── partials/
703
1057
  │ ├── header.html (default)
704
1058
  │ └── footer.html (default)
1059
+ ├── cms_forms/ # Default form templates
1060
+ │ ├── form.html
1061
+ │ ├── field_text.html
1062
+ │ └── field_email.html
705
1063
  ├── translations/
706
1064
  │ ├── en.json
707
1065
  │ ├── es.json
@@ -766,6 +1124,12 @@ cms.events.on('reldens.afterContentProcess', (eventData) => {
766
1124
  cms.events.on('reldens.templateReloader.templatesChanged', (eventData) => {
767
1125
  console.log('Templates changed:', eventData.changedFiles);
768
1126
  });
1127
+
1128
+ // Listen for form events
1129
+ cms.events.on('reldens.dynamicForm.afterSave', (eventData) => {
1130
+ // Send email notifications, trigger workflows, etc.
1131
+ console.log('Form submission received:', eventData.result.id);
1132
+ });
769
1133
  ```
770
1134
 
771
1135
  ### Custom Authentication
@@ -822,6 +1186,10 @@ cms.events.on('adminEntityExtraData', ({entitySerializedData, entity}) => {
822
1186
  - `entities_meta` - Generic metadata storage
823
1187
  - `cms_pages_meta` - Page-specific metadata
824
1188
 
1189
+ ### Forms Tables
1190
+ - `cms_forms` - Form configurations with JSON schema
1191
+ - `cms_forms_submitted` - Form submissions with JSON data
1192
+
825
1193
  ### Installation Options
826
1194
  The installer provides checkboxes for:
827
1195
  - CMS core tables
@@ -830,6 +1198,7 @@ The installer provides checkboxes for:
830
1198
  - Default homepage
831
1199
  - Default content blocks
832
1200
  - Entity access control rules
1201
+ - Dynamic forms system
833
1202
 
834
1203
  ## API Reference
835
1204
 
@@ -907,6 +1276,36 @@ The installer provides checkboxes for:
907
1276
  - `Search.executeSearch(config)` - Execute search with configuration
908
1277
  - `SearchRenderer.renderSearchResults(searchResults, config, domain, req)` - Render search results with template data
909
1278
 
1279
+ ### Forms System Classes
1280
+
1281
+ #### DynamicForm Class
1282
+ - `validateFormSubmission(formKey, submittedValues, req)` - Validate form submission
1283
+ - `getFormConfig(formKey)` - Load form configuration from database
1284
+ - `validateHoneypot(submittedValues)` - Check honeypot field for bots
1285
+ - `validateFields(fieldsSchema, submittedValues)` - Schema-based field validation
1286
+ - `prepareSubmittedValues(submittedValues, fieldsSchema)` - Process and normalize values
1287
+ - `saveFormSubmission(formConfig, preparedValues)` - Save to database
1288
+
1289
+ #### DynamicFormRenderer Class
1290
+ - `renderForm(formConfig, fieldsToRender, domain, req, attributes)` - Render complete form
1291
+ - `renderFormFields(fieldsToRender, domain, req)` - Render field set
1292
+ - `renderFormField(field, domain, submittedValues, errors)` - Render individual field
1293
+ - `loadFormTemplate(templateName, domain)` - Load form template with domain fallback
1294
+ - `findFormTemplate(templateName, domain)` - Template discovery for forms
1295
+
1296
+ #### DynamicFormRequestHandler Class
1297
+ - `handleFormSubmission(req, res)` - Process POST form submissions
1298
+ - `handleBadRequest(res, message)` - Handle validation errors
1299
+ - `handleSuccessResponse(req, res, formKey, result)` - Handle successful submissions
1300
+ - `buildErrorRedirectPath(req, error, formKey)` - Build error redirect URLs
1301
+ - `buildSuccessRedirectPath(successRedirect, formKey)` - Build success redirect URLs
1302
+
1303
+ #### FormsTransformer Class
1304
+ - `transform(template, domain, req, systemVariables, enhancedData)` - Process cmsForm tags
1305
+ - `findAllFormTags(template)` - Find cmsForm tags in template
1306
+ - `parseFormAttributes(fullTag)` - Parse tag attributes
1307
+ - `parseFieldsFilter(attributes, formConfig)` - Filter fields based on attributes
1308
+
910
1309
  ### AdminManager Class
911
1310
  - `setupAdmin()` - Initialize admin panel
912
1311
  - `generateListRouteContent()` - Entity list pages
@@ -932,16 +1331,28 @@ project/
932
1331
  │ │ ├── entity-access-manager.js
933
1332
  │ │ ├── content-renderer.js
934
1333
  │ │ └── response-manager.js
1334
+ │ ├── template-engine/ # Template processing classes
1335
+ │ │ └── forms-transformer.js
935
1336
  │ ├── frontend.js # Main Frontend orchestrator
936
1337
  │ ├── template-reloader.js # Template reloading functionality
937
1338
  │ ├── search-request-handler.js
938
1339
  │ ├── search.js # Search functionality
939
1340
  │ ├── search-renderer.js # Search result rendering
1341
+ │ ├── dynamic-form.js # Forms validation and processing
1342
+ │ ├── dynamic-form-renderer.js # Forms template rendering
1343
+ │ ├── dynamic-form-request-handler.js # Forms request handling
940
1344
  │ └── template-engine.js # Core template processing
941
1345
  ├── templates/
942
1346
  │ ├── layouts/ # Body content layouts
943
1347
  │ ├── domains/ # Domain-specific templates
1348
+ │ │ └── example.com/
1349
+ │ │ └── cms_forms/ # Domain-specific form templates
944
1350
  │ ├── partials/ # Shared template partials
1351
+ │ ├── cms_forms/ # Default form templates
1352
+ │ │ ├── form.html # Main form wrapper
1353
+ │ │ ├── field_text.html # Text field template
1354
+ │ │ ├── field_email.html # Email field template
1355
+ │ │ └── field_select.html # Select field template
945
1356
  │ ├── page.html # Base HTML wrapper
946
1357
  │ └── 404.html # Error page
947
1358
  ├── translations/
@@ -607,6 +607,7 @@
607
607
  width: 34px;
608
608
  height: auto;
609
609
  margin-right: 0.5rem;
610
+ opacity: 0.7;
610
611
  }
611
612
 
612
613
  .search-controls {
@@ -639,132 +640,6 @@
639
640
  flex-shrink: 0;
640
641
  }
641
642
 
642
- .maps-wizard {
643
- .main-action-container.maps-selection {
644
- width: 100%;
645
-
646
- .wizard-options-container {
647
- display: flex;
648
- justify-content: space-between;
649
- flex-wrap: wrap;
650
- width: 100%;
651
-
652
- /* 3 or more items => 4 columns */
653
- .wizard-map-option-container {
654
- flex: 0 0 22%;
655
- padding: 1rem 1% 0;
656
- margin: 1.5rem 0 0;
657
- border: 1px solid #ccc;
658
- }
659
-
660
- /* 1 child => 1 columns */
661
- .wizard-map-option-container:only-child {
662
- flex: 0 0 100%;
663
- padding: 1rem 0 0;
664
- border: none;
665
- }
666
-
667
- /* 2 children => 2 columns */
668
- .wizard-map-option-container:first-child:nth-last-child(2),
669
- .wizard-map-option-container:last-child:nth-child(2) {
670
- flex: 0 0 47%;
671
- }
672
-
673
- & input.map-wizard-option {
674
- top: 2px;
675
- margin-right: 0;
676
- }
677
- }
678
- }
679
-
680
- .checkbox-container {
681
- display: flex;
682
- flex-direction: row;
683
- font-weight: var(--bold);
684
- }
685
-
686
- .wizard-options-container {
687
- padding: 0;
688
- margin-bottom: 1rem;
689
-
690
- & input.map-wizard-option {
691
- position: relative;
692
- top: -2px;
693
- margin-right: 6px;
694
- }
695
-
696
- .wizard-map-option-container {
697
- display: flex;
698
- flex-direction: column;
699
- list-style: none;
700
- padding-top: 1rem;
701
- margin-top: 1.5rem;
702
- border-top: 1px solid #ccc;
703
-
704
- & label {
705
- cursor: pointer;
706
- }
707
-
708
- & canvas {
709
- width: 100%;
710
- margin-top: 1rem;
711
- }
712
- }
713
-
714
- .wizard-option-container {
715
- list-style: none;
716
- margin-bottom: 1rem;
717
-
718
- .main-option {
719
- display: inline-block;
720
- cursor: pointer;
721
- }
722
-
723
- .maps-wizard-option-content {
724
- display: none;
725
- padding-left: 1.6rem;
726
- }
727
-
728
- &.active {
729
- .maps-wizard-option-content {
730
- display: block;
731
- }
732
- }
733
- }
734
- }
735
-
736
- .maps-wizard-form {
737
- align-items: flex-start;
738
- flex-direction: column;
739
- justify-content: flex-start;
740
-
741
- .submit-container {
742
- display: flex;
743
- }
744
- }
745
-
746
- .sub-map-option-description {
747
- display: flex;
748
- align-items: center;
749
-
750
- & img.icon-sm {
751
- margin-right: 1rem;
752
- }
753
- }
754
-
755
- .sub-maps-container {
756
- display: block;
757
-
758
- &.hidden {
759
- display: none;
760
- }
761
-
762
- & p {
763
- margin: 1.5rem 0 0;
764
- }
765
- }
766
- }
767
-
768
643
  .loading {
769
644
  max-width: 50px;
770
645
 
@@ -820,8 +695,15 @@
820
695
  }
821
696
  }
822
697
 
823
- .view-field .field-value img {
824
- max-width: 200px;
698
+ .view-field {
699
+ .field-value img {
700
+ max-width: 200px;
701
+ }
702
+
703
+ .admin-audio-player {
704
+ width: 100%;
705
+ max-width: 400px;
706
+ }
825
707
  }
826
708
 
827
709
  .edit-field {
@@ -0,0 +1,7 @@
1
+ {{#fieldValue}}
2
+ <audio controls class="admin-audio-player">
3
+ <source src="{{&fieldValue}}">
4
+ Your browser does not support the audio element.
5
+ </audio>
6
+ <p><a href="{{&fieldValue}}"{{&target}}>{{&fieldValue}}</a></p>
7
+ {{/fieldValue}}
@@ -0,0 +1,8 @@
1
+ {{#fieldValue}}
2
+ <audio controls class="admin-audio-player multiple-files">
3
+ <source src="{{&fieldValuePart}}">
4
+ Your browser does not support the audio element.
5
+ </audio>
6
+ <p><a href="{{&fieldValuePart}}"{{&target}}>{{&fieldOriginalValuePart}}</a></p>
7
+ <br/>
8
+ {{/fieldValue}}