|
| 1 | +import { ChangeDetectorRef, Component, Input, OnChanges, OnInit } from '@angular/core'; |
| 2 | + |
| 3 | +import * as _ from 'lodash'; |
| 4 | + |
| 5 | +import { JsonSchemaFormService } from '../../json-schema-form.service'; |
| 6 | +import { |
| 7 | + addClasses, hasOwn, inArray, isArray, JsonPointer, toTitleCase |
| 8 | +} from '../../shared'; |
| 9 | + |
| 10 | +/** |
| 11 | + * Bootstrap 3 framework for Angular JSON Schema Form. |
| 12 | + * |
| 13 | + */ |
| 14 | +@Component({ |
| 15 | + selector: 'bootstrap-3-framework', |
| 16 | + template: ` |
| 17 | + <div |
| 18 | + [class]="options?.htmlClass" |
| 19 | + [class.has-feedback]="options?.feedback && options?.isInputWidget && |
| 20 | + (formControl?.dirty || options?.feedbackOnRender)" |
| 21 | + [class.has-error]="options?.enableErrorState && formControl?.errors && |
| 22 | + (formControl?.dirty || options?.feedbackOnRender)" |
| 23 | + [class.has-success]="options?.enableSuccessState && !formControl?.errors && |
| 24 | + (formControl?.dirty || options?.feedbackOnRender)"> |
| 25 | +
|
| 26 | + <button *ngIf="showRemoveButton" |
| 27 | + class="close pull-right" |
| 28 | + type="button" |
| 29 | + (click)="removeItem()"> |
| 30 | + <span aria-hidden="true">×</span> |
| 31 | + <span class="sr-only">Close</span> |
| 32 | + </button> |
| 33 | + <div *ngIf="options?.messageLocation === 'top'"> |
| 34 | + <p *ngIf="options?.helpBlock" |
| 35 | + class="help-block" |
| 36 | + [innerHTML]="options?.helpBlock"></p> |
| 37 | + </div> |
| 38 | +
|
| 39 | + <label *ngIf="options?.title && layoutNode?.type !== 'tab'" |
| 40 | + [attr.for]="'control' + layoutNode?._id" |
| 41 | + [class]="options?.labelHtmlClass" |
| 42 | + [class.sr-only]="options?.notitle" |
| 43 | + [innerHTML]="options?.title"></label> |
| 44 | + <p *ngIf="layoutNode?.type === 'submit' && jsf?.formOptions?.fieldsRequired"> |
| 45 | + <strong class="text-danger">*</strong> = required fields |
| 46 | + </p> |
| 47 | + <div [class.input-group]="options?.fieldAddonLeft || options?.fieldAddonRight"> |
| 48 | + <span *ngIf="options?.fieldAddonLeft" |
| 49 | + class="input-group-addon" |
| 50 | + [innerHTML]="options?.fieldAddonLeft"></span> |
| 51 | +
|
| 52 | + <select-widget-widget |
| 53 | + [layoutNode]="widgetLayoutNode" |
| 54 | + [dataIndex]="dataIndex" |
| 55 | + [layoutIndex]="layoutIndex"></select-widget-widget> |
| 56 | +
|
| 57 | + <span *ngIf="options?.fieldAddonRight" |
| 58 | + class="input-group-addon" |
| 59 | + [innerHTML]="options?.fieldAddonRight"></span> |
| 60 | + </div> |
| 61 | +
|
| 62 | + <span *ngIf="options?.feedback && options?.isInputWidget && |
| 63 | + !options?.fieldAddonRight && !layoutNode.arrayItem && |
| 64 | + (formControl?.dirty || options?.feedbackOnRender)" |
| 65 | + [class.glyphicon-ok]="options?.enableSuccessState && !formControl?.errors" |
| 66 | + [class.glyphicon-remove]="options?.enableErrorState && formControl?.errors" |
| 67 | + aria-hidden="true" |
| 68 | + class="form-control-feedback glyphicon"></span> |
| 69 | + <div *ngIf="options?.messageLocation !== 'top'"> |
| 70 | + <p *ngIf="options?.helpBlock" |
| 71 | + class="help-block" |
| 72 | + [innerHTML]="options?.helpBlock"></p> |
| 73 | + </div> |
| 74 | + </div> |
| 75 | +
|
| 76 | + <div *ngIf="debug && debugOutput">debug: <pre>{{debugOutput}}</pre></div> |
| 77 | + `, |
| 78 | + styles: [` |
| 79 | + :host /deep/ .list-group-item .form-control-feedback { top: 40px; } |
| 80 | + :host /deep/ .checkbox, |
| 81 | + :host /deep/ .radio { margin-top: 0; margin-bottom: 0; } |
| 82 | + :host /deep/ .checkbox-inline, |
| 83 | + :host /deep/ .checkbox-inline + .checkbox-inline, |
| 84 | + :host /deep/ .checkbox-inline + .radio-inline, |
| 85 | + :host /deep/ .radio-inline, |
| 86 | + :host /deep/ .radio-inline + .radio-inline, |
| 87 | + :host /deep/ .radio-inline + .checkbox-inline { margin-left: 0; margin-right: 10px; } |
| 88 | + :host /deep/ .checkbox-inline:last-child, |
| 89 | + :host /deep/ .radio-inline:last-child { margin-right: 0; } |
| 90 | + `], |
| 91 | +}) |
| 92 | +export class Bootstrap4FrameworkComponent implements OnInit, OnChanges { |
| 93 | + frameworkInitialized = false; |
| 94 | + widgetOptions: any; // Options passed to child widget |
| 95 | + widgetLayoutNode: any; // layoutNode passed to child widget |
| 96 | + options: any; // Options used in this framework |
| 97 | + formControl: any = null; |
| 98 | + debugOutput: any = ''; |
| 99 | + debug: any = ''; |
| 100 | + parentArray: any = null; |
| 101 | + isOrderable = false; |
| 102 | + @Input() layoutNode: any; |
| 103 | + @Input() layoutIndex: number[]; |
| 104 | + @Input() dataIndex: number[]; |
| 105 | + |
| 106 | + constructor( |
| 107 | + public changeDetector: ChangeDetectorRef, |
| 108 | + public jsf: JsonSchemaFormService |
| 109 | + ) { } |
| 110 | + |
| 111 | + get showRemoveButton(): boolean { |
| 112 | + if (!this.options.removable || this.options.readonly || |
| 113 | + this.layoutNode.type === '$ref' |
| 114 | + ) { return false; } |
| 115 | + if (this.layoutNode.recursiveReference) { return true; } |
| 116 | + if (!this.layoutNode.arrayItem || !this.parentArray) { return false; } |
| 117 | + // If array length <= minItems, don't allow removing any items |
| 118 | + return this.parentArray.items.length - 1 <= this.parentArray.options.minItems ? false : |
| 119 | + // For removable list items, allow removing any item |
| 120 | + this.layoutNode.arrayItemType === 'list' ? true : |
| 121 | + // For removable tuple items, only allow removing last item in list |
| 122 | + this.layoutIndex[this.layoutIndex.length - 1] === this.parentArray.items.length - 2; |
| 123 | + } |
| 124 | + |
| 125 | + ngOnInit() { |
| 126 | + this.initializeFramework(); |
| 127 | + if (this.layoutNode.arrayItem && this.layoutNode.type !== '$ref') { |
| 128 | + this.parentArray = this.jsf.getParentNode(this); |
| 129 | + if (this.parentArray) { |
| 130 | + this.isOrderable = this.layoutNode.arrayItemType === 'list' && |
| 131 | + !this.options.readonly && this.parentArray.options.orderable; |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + ngOnChanges() { |
| 137 | + if (!this.frameworkInitialized) { this.initializeFramework(); } |
| 138 | + } |
| 139 | + |
| 140 | + initializeFramework() { |
| 141 | + if (this.layoutNode) { |
| 142 | + this.options = _.cloneDeep(this.layoutNode.options); |
| 143 | + this.widgetLayoutNode = { |
| 144 | + ...this.layoutNode, |
| 145 | + options: _.cloneDeep(this.layoutNode.options) |
| 146 | + }; |
| 147 | + this.widgetOptions = this.widgetLayoutNode.options; |
| 148 | + this.formControl = this.jsf.getFormControl(this); |
| 149 | + |
| 150 | + this.options.isInputWidget = inArray(this.layoutNode.type, [ |
| 151 | + 'button', 'checkbox', 'checkboxes-inline', 'checkboxes', 'color', |
| 152 | + 'date', 'datetime-local', 'datetime', 'email', 'file', 'hidden', |
| 153 | + 'image', 'integer', 'month', 'number', 'password', 'radio', |
| 154 | + 'radiobuttons', 'radios-inline', 'radios', 'range', 'reset', 'search', |
| 155 | + 'select', 'submit', 'tel', 'text', 'textarea', 'time', 'url', 'week' |
| 156 | + ]); |
| 157 | + |
| 158 | + this.options.title = this.setTitle(); |
| 159 | + |
| 160 | + this.options.htmlClass = |
| 161 | + addClasses(this.options.htmlClass, 'schema-form-' + this.layoutNode.type); |
| 162 | + this.options.htmlClass = |
| 163 | + this.layoutNode.type === 'array' ? |
| 164 | + addClasses(this.options.htmlClass, 'list-group') : |
| 165 | + this.layoutNode.arrayItem && this.layoutNode.type !== '$ref' ? |
| 166 | + addClasses(this.options.htmlClass, 'list-group-item') : |
| 167 | + addClasses(this.options.htmlClass, 'form-group'); |
| 168 | + this.widgetOptions.htmlClass = ''; |
| 169 | + this.options.labelHtmlClass = |
| 170 | + addClasses(this.options.labelHtmlClass, 'control-label'); |
| 171 | + this.widgetOptions.activeClass = |
| 172 | + addClasses(this.widgetOptions.activeClass, 'active'); |
| 173 | + this.options.fieldAddonLeft = |
| 174 | + this.options.fieldAddonLeft || this.options.prepend; |
| 175 | + this.options.fieldAddonRight = |
| 176 | + this.options.fieldAddonRight || this.options.append; |
| 177 | + |
| 178 | + // Add asterisk to titles if required |
| 179 | + if (this.options.title && this.layoutNode.type !== 'tab' && |
| 180 | + !this.options.notitle && this.options.required && |
| 181 | + !this.options.title.includes('*') |
| 182 | + ) { |
| 183 | + this.options.title += ' <strong class="text-danger">*</strong>'; |
| 184 | + } |
| 185 | + // Set miscelaneous styles and settings for each control type |
| 186 | + switch (this.layoutNode.type) { |
| 187 | + // Checkbox controls |
| 188 | + case 'checkbox': case 'checkboxes': |
| 189 | + this.widgetOptions.htmlClass = addClasses( |
| 190 | + this.widgetOptions.htmlClass, 'checkbox'); |
| 191 | + break; |
| 192 | + case 'checkboxes-inline': |
| 193 | + this.widgetOptions.htmlClass = addClasses( |
| 194 | + this.widgetOptions.htmlClass, 'checkbox'); |
| 195 | + this.widgetOptions.itemLabelHtmlClass = addClasses( |
| 196 | + this.widgetOptions.itemLabelHtmlClass, 'checkbox-inline'); |
| 197 | + break; |
| 198 | + // Radio controls |
| 199 | + case 'radio': case 'radios': |
| 200 | + this.widgetOptions.htmlClass = addClasses( |
| 201 | + this.widgetOptions.htmlClass, 'radio'); |
| 202 | + break; |
| 203 | + case 'radios-inline': |
| 204 | + this.widgetOptions.htmlClass = addClasses( |
| 205 | + this.widgetOptions.htmlClass, 'radio'); |
| 206 | + this.widgetOptions.itemLabelHtmlClass = addClasses( |
| 207 | + this.widgetOptions.itemLabelHtmlClass, 'radio-inline'); |
| 208 | + break; |
| 209 | + // Button sets - checkboxbuttons and radiobuttons |
| 210 | + case 'checkboxbuttons': case 'radiobuttons': |
| 211 | + this.widgetOptions.htmlClass = addClasses( |
| 212 | + this.widgetOptions.htmlClass, 'btn-group'); |
| 213 | + this.widgetOptions.itemLabelHtmlClass = addClasses( |
| 214 | + this.widgetOptions.itemLabelHtmlClass, 'btn'); |
| 215 | + this.widgetOptions.itemLabelHtmlClass = addClasses( |
| 216 | + this.widgetOptions.itemLabelHtmlClass, this.options.style || 'btn-default'); |
| 217 | + this.widgetOptions.fieldHtmlClass = addClasses( |
| 218 | + this.widgetOptions.fieldHtmlClass, 'sr-only'); |
| 219 | + break; |
| 220 | + // Single button controls |
| 221 | + case 'button': case 'submit': |
| 222 | + this.widgetOptions.fieldHtmlClass = addClasses( |
| 223 | + this.widgetOptions.fieldHtmlClass, 'btn'); |
| 224 | + this.widgetOptions.fieldHtmlClass = addClasses( |
| 225 | + this.widgetOptions.fieldHtmlClass, this.options.style || 'btn-info'); |
| 226 | + break; |
| 227 | + // Containers - arrays and fieldsets |
| 228 | + case 'array': case 'fieldset': case 'section': case 'conditional': |
| 229 | + case 'advancedfieldset': case 'authfieldset': |
| 230 | + case 'selectfieldset': case 'optionfieldset': |
| 231 | + this.options.messageLocation = 'top'; |
| 232 | + break; |
| 233 | + case 'tabarray': case 'tabs': |
| 234 | + this.widgetOptions.htmlClass = addClasses( |
| 235 | + this.widgetOptions.htmlClass, 'tab-content'); |
| 236 | + this.widgetOptions.fieldHtmlClass = addClasses( |
| 237 | + this.widgetOptions.fieldHtmlClass, 'tab-pane'); |
| 238 | + this.widgetOptions.labelHtmlClass = addClasses( |
| 239 | + this.widgetOptions.labelHtmlClass, 'nav nav-tabs'); |
| 240 | + break; |
| 241 | + // 'Add' buttons - references |
| 242 | + case '$ref': |
| 243 | + this.widgetOptions.fieldHtmlClass = addClasses( |
| 244 | + this.widgetOptions.fieldHtmlClass, 'btn pull-right'); |
| 245 | + this.widgetOptions.fieldHtmlClass = addClasses( |
| 246 | + this.widgetOptions.fieldHtmlClass, this.options.style || 'btn-default'); |
| 247 | + this.options.icon = 'glyphicon glyphicon-plus'; |
| 248 | + break; |
| 249 | + // Default - including regular inputs |
| 250 | + default: |
| 251 | + this.widgetOptions.fieldHtmlClass = addClasses( |
| 252 | + this.widgetOptions.fieldHtmlClass, 'form-control'); |
| 253 | + } |
| 254 | + |
| 255 | + if (this.formControl) { |
| 256 | + this.updateHelpBlock(this.formControl.status); |
| 257 | + this.formControl.statusChanges.subscribe(status => this.updateHelpBlock(status)); |
| 258 | + |
| 259 | + if (this.options.debug) { |
| 260 | + let vars: any[] = []; |
| 261 | + this.debugOutput = _.map(vars, thisVar => JSON.stringify(thisVar, null, 2)).join('\n'); |
| 262 | + } |
| 263 | + } |
| 264 | + this.frameworkInitialized = true; |
| 265 | + } |
| 266 | + |
| 267 | + } |
| 268 | + |
| 269 | + updateHelpBlock(status) { |
| 270 | + this.options.helpBlock = status === 'INVALID' && |
| 271 | + this.options.enableErrorState && this.formControl.errors && |
| 272 | + (this.formControl.dirty || this.options.feedbackOnRender) ? |
| 273 | + this.jsf.formatErrors(this.formControl.errors, this.options.errorMessages) : |
| 274 | + this.options.description || this.options.help || null; |
| 275 | + } |
| 276 | + |
| 277 | + setTitle(): string { |
| 278 | + switch (this.layoutNode.type) { |
| 279 | + case 'button': case 'checkbox': case 'help': case 'msg': |
| 280 | + case 'message': case 'submit': case 'tabarray': case '$ref': |
| 281 | + return null; |
| 282 | + case 'advancedfieldset': |
| 283 | + this.widgetOptions.expandable = true; |
| 284 | + this.widgetOptions.title = 'Advanced options'; |
| 285 | + return null; |
| 286 | + case 'authfieldset': |
| 287 | + this.widgetOptions.expandable = true; |
| 288 | + this.widgetOptions.title = 'Authentication settings'; |
| 289 | + return null; |
| 290 | + case 'tabs': case 'section': |
| 291 | + return null; |
| 292 | + default: |
| 293 | + let thisTitle = this.options.title || |
| 294 | + (isNaN(this.layoutNode.name) && this.layoutNode.name !== '-' ? |
| 295 | + toTitleCase(this.layoutNode.name) : null); |
| 296 | + this.widgetOptions.title = null; |
| 297 | + return !thisTitle ? null : |
| 298 | + thisTitle.indexOf('{{') === -1 || !this.formControl || !this.dataIndex ? |
| 299 | + thisTitle : |
| 300 | + this.jsf.parseText( |
| 301 | + thisTitle, |
| 302 | + this.jsf.getFormControlValue(this), |
| 303 | + this.jsf.getFormControlGroup(this).value, |
| 304 | + this.dataIndex[this.dataIndex.length - 1] |
| 305 | + ); |
| 306 | + } |
| 307 | + } |
| 308 | + |
| 309 | + removeItem() { |
| 310 | + this.jsf.removeItem(this); |
| 311 | + } |
| 312 | +} |
0 commit comments