import { Helper, SPTypes, Types } from "gd-sprest"; import { Components } from "../core"; import { DateTimeControlType } from "../datetime"; import { IFormControlPropsDateTime } from "../datetime/types"; import { Field } from "../field"; import { IField, IFieldImageInfo, IFieldImageValue } from "../field/types"; import { RichTextBoxControlType, RichTextBoxTypes } from "../richTextBox" import { IFormControlPropsRichTextBox } from "../richTextBox/types"; import { IListForm, IListFormDisplayProps, IListFormEdit, IListFormEditProps } from "./types"; // Extend the list form export const ListForm: IListForm = Helper.ListForm as any; // Method to get the fields to render let getFieldsToRender = (props: IListFormDisplayProps | IListFormEditProps): Array => { let fieldNames = []; // See if the "include" fields property is defined if (props.includeFields) { // Set the field names fieldNames = props.includeFields; } else { // Parse the fields for (let fieldName in props.info.fields) { // See if the "exclude" property is set if (props.excludeFields) { let renderFl = true; // Parse the fields for (let i = 0; i < props.excludeFields.length; i++) { // See if we are excluding this field if (props.excludeFields[i] == fieldName) { // Set the flag renderFl = false; break; } } // Skip this field, if we are not rendering it if (!renderFl) { continue; } } // Add the field name fieldNames.push(fieldName); } } // See if an event exists if (props.onGetFields) { // Call the event fieldNames = props.onGetFields(fieldNames); } // Return the field names return fieldNames; } // Method to render the display control let renderDisplay = (fieldName: string, props: IListFormDisplayProps): Components.IFormControlProps => { let control: Components.IFormControlProps = null; let field = props.info.fields[fieldName]; let isRichText = field ? (field as Types.SP.FieldMultiLineText).RichText : false; let value = props.info.fieldValuesAsText ? props.info.fieldValuesAsText[fieldName] || "" : ""; let html: string = props.info.fieldValuesAsHtml ? props.info.fieldValuesAsHtml[fieldName] || props.info.fieldValuesAsHtml[fieldName.replace(/\_/g, "_x005f_")] || "" : ""; // Ensure the field exists if (field == null) { // See if a value was found if (value || html) { // Return a read-only control return { data: value || html, isDisabled: true, label: fieldName, name: fieldName, type: Components.FormControlTypes.TextField, value: value || html }; } // Log console.warn("[List Form] Field '" + fieldName + "' does not exist. Check the list or query."); return control; } // See if we are hiding the field if (field.SchemaXml.indexOf('ShowInDisplayForm="FALSE"') > 0) { return control; } // See if this is an image field if (field.FieldTypeKind == SPTypes.FieldType.Image) { // Ensure a value exists if (value) { // Update the html try { let imgInfo: IFieldImageValue = JSON.parse(value); html = "" + imgInfo.fileName + ""; } catch { } } } // Else, see if this is a note field else if (field.FieldTypeKind == SPTypes.FieldType.Note) { // Update the html html = html.replace(/\r?\n/g, '
') .replace(/"/g, '"'); } // Else, see if this is a user field else if (field.FieldTypeKind == SPTypes.FieldType.User) { // See if this is a multi-user selection if ((field as Types.SP.FieldUser).AllowMultipleValues) { let userNames = []; // Parse the users let users: Types.SP.User[] = (props.info.item[fieldName] ? props.info.item[fieldName].results : null) || []; for (let j = 0; j < users.length; j++) { // Append the user name userNames.push(users[j].Title); } // Set the html value html = userNames.join('
\n'); } else { // Extract the text only for single selections let elUser = document.createElement("div"); elUser.innerHTML = html; html = elUser.innerText; } } // Else, see if this is a choice field else if (field.FieldTypeKind == SPTypes.FieldType.Choice || field.FieldTypeKind == SPTypes.FieldType.MultiChoice) { // Update the html html = value; } // Set the control control = { data: html, description: field.Description, isDisabled: true, label: field.Title, name: field.InternalName, type: Components.FormControlTypes.TextField, value: html }; // Update the type, based on the field switch (field.FieldTypeKind) { case SPTypes.FieldType.DateTime: // Set the time flag (control as IFormControlPropsDateTime).showTime = (field as Types.SP.FieldDateTime).DisplayFormat == SPTypes.DateFormat.DateTime ? true : false; // Set the type control.type = DateTimeControlType; break; case SPTypes.FieldType.Lookup: // Ensure a value exists if (html) { // Create an element to store the html let elLookup = document.createElement("div"); elLookup.innerHTML = html; // Update the value to be text html = elLookup.innerText control.data = html; control.value = html; } break; case SPTypes.FieldType.Note: // See if this is a rich text field if (isRichText) { // Set the properties (control as IFormControlPropsRichTextBox).toolbarType = RichTextBoxTypes.None; control.type = RichTextBoxControlType; } else { // Set the type control.type = Components.FormControlTypes.TextArea; } break; case SPTypes.FieldType.URL: // Set the value let urlValue = props.info.item[fieldName] as Types.SP.FieldUrlValue; html = urlValue ? urlValue.Url : html; control.value = html; break; case SPTypes.FieldType.User: // Set the type control.type = (field as Types.SP.FieldLookup).AllowMultipleValues ? Components.FormControlTypes.TextArea : control.type; break; } // Detect html if (/<*>/g.test(html)) { let isMultiLine = html.indexOf("
") >= 0 ? true : false; // See if it's an image if (field.FieldTypeKind == SPTypes.FieldType.Image) { // Set the rendered event control.onControlRendered = control => { // Override the html rendered control.el.innerHTML = html; } } // Else, ensure this isn't a rich text field or multi-line else if (!isRichText && !isMultiLine) { // Update the control to be read-only control.type = Components.FormControlTypes.Readonly; // Set the rendered event control.onControlRendered = control => { // Get the target element let elTarget = control.el.querySelector("input") || control.el; // Override the html rendered elTarget.innerHTML = control.props.data; } } else { // Update the html break line html = html.replace(/
/g, '\n'); control.data = html; control.value = html; } } // Else, detect xml else if (/<|>|&|"/i.test(html)) { // Update the value control.value = html.replace(/</ig, '<') .replace(/>/ig, '>') .replace(/&/ig, '&') .replace(/"/ig, '"'); } // Return the control return control; } // Method to generate the attachments row let generateAttachmentsControl = (props: IListFormDisplayProps): Components.IFormControlProps => { // See if we are rendering attachments let displayAttachments = typeof (props.displayAttachments) === "boolean" ? props.displayAttachments : true; if (props.info.attachments && displayAttachments) { // Render the attachments return { id: "ListFormAttachments", label: "Attachments", name: "Attachments", onControlRendered: control => { let items: Array = []; // Parse the attachments for (let i = 0; i < props.info.attachments.length; i++) { let attachment = props.info.attachments[i]; // Add the item items.push({ buttons: [{ className: "me-1", data: attachment.ServerRelativeUrl, isSmall: true, text: attachment.FileName, onClick: (btn) => { // Open the attachment in a new tab window.open(btn.data, "_blank"); } }] }); } // Render a toolbar Components.Toolbar({ el: control.el, items }); } }; } } // Method to render a display form for an item ListForm.renderDisplayForm = (props: IListFormDisplayProps) => { let form: Components.IForm = null; let totalControls = 0; // Render a loading message let progress = Components.Progress({ el: props.el, isAnimated: true, isStriped: true, label: "Loading the Form", size: 100 }); let mapper: { [key: string]: Components.IFormControlProps } = {}; let rows: Array = []; // Get the fields to render let fieldNames = getFieldsToRender(props); // Parse the fields to render for (let i = 0; i < fieldNames.length; i++) { let fieldName = fieldNames[i]; // See if this is the attachment field if (fieldName == "Attachments") { // Generate the attachments control let ctrlAttachments = generateAttachmentsControl(props); if (ctrlAttachments) { rows.push({ columns: [{ control: ctrlAttachments }] }); } // Increment the total controls totalControls++; // Continue the loop continue; } // Generate the control let control = renderDisplay(fieldName, props); if (control) { // Update the mapper mapper[fieldName] = control; // Add the row rows.push({ columns: [{ control }] }); // Increment the total controls totalControls++; } } // See if there is a template if (props.template) { // Reset the total controls totalControls = 0; // Updates the control let updateControl = (refControl) => { // Get the control from the mapper let control = refControl ? mapper[refControl.name] : null; // Ensure the controls exists if (control && refControl) { // Parse the control keys for (let key in control) { // Skip if a value is already defined if (refControl[key]) { continue; } // Update the property refControl[key] = control[key]; } // Update the mapper mapper[refControl.name] = refControl; } } // Parse the template for (let i = 0; i < props.template.length; i++) { let row = props.template[i]; // Parse the columns if there are columns let columns = row.columns || []; for (let j = 0; j < columns.length; j++) { let column = columns[j]; // Update the control updateControl(column.control); // Increment the total controls totalControls++; } } } // Remove the progress bar progress.el.parentElement ? progress.el.parentElement.removeChild(progress.el) : null; // Counter for each control let ctrlCounter = 0; // Render the form form = Components.Form({ el: props.el, className: props.className, groupClassName: props.groupClassName, rowClassName: props.rowClassName, onControlRendered: control => { // See if all of the controls have been rendered if (++ctrlCounter == totalControls) { // See if an event exists if (props.onFormRendered) { // Execute the form rendered event in another thread setTimeout(() => { props.onFormRendered(form); }, 10); } } // Return the control rendered event return props.onControlRendered ? props.onControlRendered(control, props.info.fields[control.props.name]) : null; }, onControlRendering: control => { return props.onControlRendering ? props.onControlRendering(control, props.info.fields[control.name]) : null; }, rows: props.template || rows }); // Execute the assign to event props.assignTo ? props.assignTo(form) : null; // Return the form informaiton return { get el() { return form ? form.el as HTMLFormElement : null; }, getControl(fieldName: string) { return form ? form.getControl(fieldName) : null; } }; }; // Render the edit form ListForm.renderEditForm = (props: IListFormEditProps): IListFormEdit => { let customControls: Components.IFormControl[] = []; let mapper: { [key: string]: IField } = {}; let rows: Array = []; let totalControls = 0; let value = {}; let attachments: { delete: Array; new: Array; } = { delete: [], new: [] }; // Method to add a refresh alert let addRefreshLink = () => { // Ensure the link doesn't already exist if (props.el.querySelector(".refresh-btn")) { return; } // Create the refresh button let alert = Components.ButtonGroup({ className: "refresh-btn", buttonType: Components.ButtonTypes.Danger, buttons: [ { text: "Refresh Form", onClick: () => { // Clear the element and reload the form props.el.innerHTML = ""; // Render the form ListForm.renderEditForm(props); } }, { text: "Refresh Page", onClick: () => { // Refresh the page document.location.href = document.location.href; } } ] }); // Add the element at the top props.el.insertBefore(alert.el, props.el.children[0]); } // Method to remove the attachments let removeAttachments = (info: Helper.IListFormResult): PromiseLike => { // Return a promise return new Promise((resolve, reject) => { // Ensure attachments exists if (attachments.delete.length == 0) { resolve(); return; } // Get the web props.info.list.ParentWeb().execute(web => { // Parse the attachments Helper.Executor(attachments.delete, attachment => { // Get the attachment file web.getFileByServerRelativeUrl(attachment.ServerRelativeUrl).delete().execute(); // Parse the attachments for (let i = 0; i < props.info.attachments.length; i++) { // See if this is the target attachment if (props.info.attachments[i].ServerRelativeUrl == attachment.ServerRelativeUrl) { // Remove this item props.info.attachments.splice(i, 1); break; } } }).then(() => { // Wait for the files to be deleted (web as any as Types.SP.IWeb).done(() => { // Clear the attachments attachments.delete = []; // Resolve the promise resolve(); }); }); }); }); } // Method to save the attachments let saveAttachments = (info: Helper.IListFormResult): PromiseLike => { // Return a promise return new Promise((resolve, reject) => { // Ensure attachments exists if (attachments.new.length == 0) { resolve(); return; } // Parse the attachments Helper.Executor(attachments.new, attachment => { // Get the item's attachments props.info.list.Items(info.item.Id).AttachmentFiles() // Add the file .add(attachment.name, attachment.data) // Execute the request .execute(attachment => { // Ensure attachments exist info.attachments = info.attachments || []; // Append the attachment info.attachments.push(attachment); }); }).then(() => { // Wait for the files to upload (props.info.list as any as Types.SP.IList).done(() => { // Clear the attachments attachments.new = []; // Resolve the promise resolve(); }); }); }); } // Method to upload the images let images: IFieldImageInfo[] = []; let uploadImages = (info: Helper.IListFormResult): PromiseLike => { // Return a promise return new Promise((resolve) => { let values = {}; // Ensure we have images if (images.length == 0) { // Do nothing resolve(values); return; } // Get the list folder Helper.ListFormField.getOrCreateImageFolder(info).then(fld => { // Removes the existing image let removeExisting = (value: string) => { // Return a promise return new Promise(resolve => { // Try to get the image info let imageInfo: IFieldImageValue = null; try { imageInfo = JSON.parse(value); } catch { } // Ensure the info exists if (imageInfo) { // See if the file exists fld.Files(imageInfo.fileName).execute( // Exists file => { // Delete the file file.delete().execute(() => { // Resolve the request resolve(null); }); }, // Doesn't exist () => { // Resolve the request resolve(null); } ); } else { // Resolve the request resolve(null); } }); } // Validates the list name let validateFileName = (fileName: string): PromiseLike => { // Return a promise return new Promise(resolve => { // Get the file name w/out the extension let info = fileName.toLowerCase().split('.'); let fileExt = info[info.length - 1]; let fileNameNoExt = ""; for (let i = 0; i < info.length - 1; i++) { fileNameNoExt += info[i]; } // Get the files with a similar name fld.Files().query({ Filter: "startswith(Name, '" + fileNameNoExt + "')", Top: 5000 }).execute(files => { let isValid = true; let counter = -1; let validFileName = null; // See if no files were found if (files.results.length == 0) { // Resolve the request resolve(fileName); return; } // Loop until it's valid do { // Reset the flag and file name validFileName = fileNameNoExt + (++counter == 0 ? "" : counter) + "." + fileExt; isValid = true; // Parse the files for (let i = 0; i < files.results.length; i++) { let file = files.results[i]; // See if there is a match if (file.Name.toLowerCase() == validFileName) { // Set the flag isValid = false; break; } } } while (!isValid) // Resolve the request resolve(validFileName); }); }); } // Parse the images Helper.Executor(images, imageInfo => { // See if this is a file that needs to be uploaded if (imageInfo.name && imageInfo.data && imageInfo.fieldName) { // Return a promise return new Promise(resolve => { // Remove the existing image removeExisting(info.item ? info.item[imageInfo.fieldName] : null).then(() => { // Validate the name validateFileName(imageInfo.name).then(fileName => { // Upload the file fld.Files().add(fileName, true, imageInfo.data).execute(file => { // Update the field value values[imageInfo.fieldName] = JSON.stringify({ fieldId: imageInfo.fieldId, fieldName: imageInfo.fieldName, fileName: file.Name, id: file.UniqueId, nativeFile: {}, serverRelativeUrl: file.ServerRelativeUrl, type: "thumbnail" } as IFieldImageValue); // Resolve the request resolve(null); }); }) }); }); } }).then(() => { // Resolve the request resolve(values); }); }); }); } // Render a loading message let progress = Components.Progress({ el: props.el, isAnimated: true, isStriped: true, label: "Loading the Form", size: 100 }); // Generates the attachments row let controlAttachments: Components.IFormControlProps = null; let generateAttachmentsRow = () => { // See if we are rendering attachments let displayAttachments = typeof (props.displayAttachments) === "boolean" ? props.displayAttachments : true; let attachmentsExist = props.info.item == null ? true : (props.info.attachments ? true : false); if (attachmentsExist && displayAttachments) { // Set a default field // This will help w/ the onControlRendering/ed events to not have a null value for this parameter props.info.fields["Attachments"] = {} as any; // Create the attachments control controlAttachments = { id: "ListFormAttachments", label: "Attachments", name: "Attachments", onControlRendered: control => { // Render a toolbar let toolbar = Components.Toolbar({ el: control.el, items: [{ buttons: [{ className: "upload-btn me-1", isSmall: true, text: "Upload", type: Components.ButtonTypes.Secondary, onClick: (btn, ev) => { let elUpload = ev.currentTarget as HTMLButtonElement; // Display an upload dialog Helper.ListForm.showFileDialog().then(fileInfo => { // Get the buttons and remove any duplicates let buttons = elUpload.parentElement.querySelectorAll(".btn"); for (let i = 0; i < buttons.length; i++) { let button = buttons[i] as HTMLButtonElement; // See if this is the associated button if (button.innerText.replace(/X$/, '') == fileInfo.name) { // Get the badge let badge = button.querySelector(".badge") as HTMLSpanElement; if (badge) { // Remove the button badge.click(); } break; } } // Save the file information attachments.new.push(fileInfo); // Append the attachment elUpload.parentElement.appendChild(Components.Popover({ isDismissible: true, type: Components.PopoverPlacements.Bottom, btnProps: { className: "me-1 file-attachment", isSmall: true, text: fileInfo.name }, options: { content: Components.Button({ data: fileInfo, isSmall: true, text: "Remove", type: Components.ButtonTypes.Danger, onClick: (btn, ev) => { let fileName = (btn.data as Helper.IListFormAttachmentInfo).name; // Parse the array for (let i = 0; i < attachments.new.length; i++) { // See if this is the target attachment if (attachments.new[i].name == fileName) { // Remove this attachment attachments.new.splice(i, 1); break; } } // Get the files let files = btnGroup.querySelectorAll(".btn.file-attachment"); for (let i = 0; i < files.length; i++) { let file = files[i] as HTMLAnchorElement; // See if this is the target button if (file.innerText == fileName) { // Remove this popover file.parentElement.removeChild(file); break; } } } }).el } }).el); }); } }] }] }); // Get the button group let btnGroup = toolbar.el.querySelector(".btn-group"); if (btnGroup) { // Parse the attachments let itemAttachments = props.info.attachments || []; for (let i = 0; i < itemAttachments.length; i++) { let attachment = itemAttachments[i]; // Add the attachment btnGroup.appendChild(Components.Popover({ isDismissible: true, type: Components.PopoverPlacements.Bottom, btnProps: { className: "me-1 file-attachment", isSmall: true, text: attachment.FileName, }, options: { content: Components.ButtonGroup({ buttons: [ { data: attachment, isSmall: true, text: "Remove", type: Components.ButtonTypes.Danger, onClick: (btn, ev) => { let attachment = btn.data as Types.SP.Attachment; // Add this file for deletion attachments.delete.push(attachment); // Get the files let files = btnGroup.querySelectorAll(".btn.file-attachment"); for (let i = 0; i < files.length; i++) { let file = files[i] as HTMLAnchorElement; // See if this is the target button if (file.innerText == attachment.FileName) { // Remove this popover file.parentElement.removeChild(file); break; } } } }, { data: attachment, isDisabled: attachment.ServerRelativeUrl ? false : true, isSmall: true, text: "View", type: Components.ButtonTypes.Primary, onClick: (btn, ev) => { let fileUrl = (btn.data as Types.SP.Attachment).ServerRelativeUrl; // Show the file in a new tab window.open(fileUrl, "_blank"); } } ] }).el } }).el); } } } }; // Render the attachments rows.push({ columns: [{ control: controlAttachments }] }); } } // Get the fields to render let fieldNames = getFieldsToRender(props); // Parse the fields to render for (let i = 0; i < fieldNames.length; i++) { let fieldName = fieldNames[i]; let field = props.info.fields[fieldName]; // See if this is the attachment field if (fieldName == "Attachments") { // Generate the attachments row generateAttachmentsRow(); // Increment the total controls totalControls++; // Check the next field continue; } // Ensure the field exists if (field == null) { // Log console.error("[List Form] Field '" + fieldName + "' does not exist. Check the list or query."); continue; } // See if the item exists value[fieldName] = null; if (props.info.item) { // Set the value value[fieldName] = props.info.item[fieldName]; // See if this is a lookup or user field if (field.FieldTypeKind == SPTypes.FieldType.Lookup || field.FieldTypeKind == SPTypes.FieldType.User) { // Update the value value[fieldName] = value[fieldName + "Id"] || (value[fieldName] ? value[fieldName].Id : null) || value[fieldName]; } // See if this is a file leaf ref if (fieldName == "FileLeafRef") { // Update the value value[fieldName] = value[fieldName] || props.info.item.Title; } } // Determine the control mode let controlMode = props.controlMode || (props.info.item ? SPTypes.ControlMode.Edit : SPTypes.ControlMode.New); // See if this is an edit form and we are hiding this field if (controlMode == SPTypes.ControlMode.Edit && field.SchemaXml.indexOf('ShowInEditForm="FALSE"') > 0) { continue; } // See if this is a new form and we are hiding this field if (controlMode == SPTypes.ControlMode.New && field.SchemaXml.indexOf('ShowInNewForm="FALSE"') > 0) { continue; } // See if thi sis a new form and this is an associated lookup field if (controlMode == SPTypes.ControlMode.New && (field as Types.SP.FieldLookup).IsDependentLookup) { continue; } // See if this is a display form and we are hiding this field if (controlMode == SPTypes.ControlMode.Display && field.SchemaXml.indexOf('ShowInDisplayForm="FALSE"') > 0) { continue; } // See if this is a read-only field if (field.ReadOnlyField) { // Do not render in the new form if (props.controlMode == SPTypes.ControlMode.New) { continue; } } // Do not render a hidden taxonomy field if (field.Hidden && field.FieldTypeKind == SPTypes.FieldType.Note && /_0$/.test(field.Title)) { continue; } // See if this is an invalid field type if (field.FieldTypeKind == SPTypes.FieldType.Invalid) { // Ensure it's not a taxonomy field if (!/^TaxonomyFieldType/.test(field.TypeAsString)) { continue; } } // Else, see if this is a calculated column else if (field.FieldTypeKind == SPTypes.FieldType.Calculated) { // Do not render in the new/edit forms if (props.controlMode != SPTypes.ControlMode.Display) { continue; } } // See if this is a lookup field let lookupFilter = null; if (field.FieldTypeKind == SPTypes.FieldType.Lookup) { // Call the filter event lookupFilter = props.onFilterLookupField ? props.onFilterLookupField(field) : null; } // See if there is a custom event for setting the value if (props.onSetFieldDefaultValue) { // Call the event to override the value value[fieldName] = props.onSetFieldDefaultValue(field, value[fieldName]); } // Create the control let fieldControl = Field({ controlMode: props.controlMode, field, listInfo: props.info, lookupFilter, value: value[fieldName], onControlRendered: (control, field) => { // Update the mapper mapper[field.InternalName].control = control; }, onValidate: props.onValidate, onError: msg => { // Add the refresh link addRefreshLink(); // Call the event props.onError ? props.onError(msg) : null; } }); // Update the mapper mapper[fieldName] = fieldControl; // Add the row rows.push({ columns: [{ control: fieldControl.controlProps }] }); // Increment the total controls totalControls++; } // See if there is a template let templateFieldNames = null; if (props.template) { // Clear the field names for this template templateFieldNames = []; // Reset the total controls totalControls = 0; // Method to handle internal and custom events let createEvent = (event: string, control: Components.IFormControlProps, templateControl: Components.IFormControlProps) => { let templateEvent = templateControl[event]; // Set the event return (...args) => { let controlProps: Components.IFormControlProps = args[0]; // Call the events control[event](...args); templateEvent(...args); // Update the mapper mapper[controlProps.name] ? mapper[controlProps.name].controlProps = controlProps : null; } } // Method to update the template control let updateControl = (templateControl: Components.IFormControlProps) => { // Get the control from the mapper let control = templateControl && mapper[templateControl.name] ? mapper[templateControl.name].controlProps : null; // See if this is a reference to the attachments if (templateControl && templateControl.name == "Attachments") { control = templateControl.isDisabled || templateControl.isReadonly ? generateAttachmentsControl(props) : controlAttachments; } // Ensure the controls exists if (control && templateControl) { // Parse the control keys for (let key in control) { // Skip if a value is already defined if (typeof (templateControl[key]) !== "undefined") { // See if this is an internal event if (key == "onControlRendering" || key == "onControlRendered") { // Create a new event to call both internal and custom events templateControl[key] = createEvent(key, control, templateControl); } // Skip this property continue; } // Update the property templateControl[key] = control[key]; } // Add the template field name templateFieldNames.push(templateControl.name); // Update the mapper mapper[templateControl.name] ? mapper[templateControl.name].controlProps = templateControl : null; } } // Parse the template for (let i = 0; i < props.template.length; i++) { let row = props.template[i]; // Parse the columns if there are columns let columns = row.columns || []; for (let j = 0; j < columns.length; j++) { let column = columns[j]; // Update the control updateControl(column.control); // Increment the total controls totalControls++; } } } // Remove the progress bar progress.el.parentElement ? progress.el.parentElement.removeChild(progress.el) : null; // Counter for each control let ctrlCounter = 0; // Render the form let form = Components.Form({ el: props.el, className: props.className, groupClassName: props.groupClassName, rowClassName: props.rowClassName, onControlRendered: control => { // Ensure the control is set let field = mapper[control.props?.name]; if (field && field.control == null) { field.setControl(control); } // See if all of the controls have been rendered if (++ctrlCounter == totalControls) { // See if an event exists if (props.onFormRendered) { // Execute the form rendered event in another thread setTimeout(() => { props.onFormRendered(form); }, 10); } } // Return the event return props.onControlRendered ? props.onControlRendered(control, props.info.fields[control.props.name]) : null; }, onControlRendering: control => { let findTemplateControl = (ctrlName: string) => { // Parse the template for (let i = 0; i < props.template.length; i++) { let row = props.template[i]; // Parse the columns if there are columns let columns = row.columns || []; for (let j = 0; j < columns.length; j++) { let column = columns[j]; // See if this is the control if (column.control && column.control.name == ctrlName) { return column.control; } } } // Not found return null; } let updateReadOnly = (control: Components.IFormControlProps) => { // See if this control is readonly if (control.isReadonly && control.name) { // Get the control display properties let dispControl = control.name == "Attachments" ? generateAttachmentsControl(props) : renderDisplay(control.name, props); if (dispControl) { let ctrlTemplate = props.template ? findTemplateControl(control.name) : null; // Update the properties control.data = ctrlTemplate ? ctrlTemplate.data : dispControl.data; control.label = ctrlTemplate ? ctrlTemplate.label : dispControl.label; (control as IFormControlPropsDateTime).showTime = ctrlTemplate ? (ctrlTemplate as IFormControlPropsDateTime).showTime : (dispControl as IFormControlPropsDateTime).showTime; control.type = ctrlTemplate ? ctrlTemplate.type : dispControl.type; control.value = ctrlTemplate ? ctrlTemplate.value : dispControl.value; } } } // Execute the rendering event let field = props.info.fields[control.name] || {} as any; let returnVal = props.onControlRendering ? props.onControlRendering(control, field) : null; if (returnVal && returnVal.then) { // Wait for the event to complete returnVal.then((ctrlProps) => { // Update the properties updateReadOnly(ctrlProps || control); }); } else { // Update the properties updateReadOnly(control); } }, rows: props.template || rows, value }); // Method to get the values let getValues = () => { let values = {}; // See if the content type was set if (props.info.contentType) { // Set the content type id values["ContentTypeId"] = props.info.contentType.Id.StringValue; } // Clear the image values images = []; // Parse the fields for (let fieldName in props.info.fields) { // Get the form field and skip disabled/readonly fields let formField = mapper[fieldName]; if (formField == null || formField.controlProps.isDisabled || formField.controlProps.isReadonly) { continue; } // Get the field value let fieldValue = formField.getValue(); // Set the item value values[fieldValue.name] = fieldValue.value; // See if this is the file leaf ref if (fieldValue.name == "FileLeafRef") { // Update the 'Title' values["Title"] = values["Title"] || values[fieldValue.name]; } // See if this is an image field let field = props.info.fields[fieldName]; if (field && field.FieldTypeKind == SPTypes.FieldType.Image) { // Add the value if it exists fieldValue.value ? images.push(fieldValue.value) : null; } } // Return the form values return values; }; // Create the form object let formObj: any = { appendControls: (controls: Components.IFormControlProps[]) => { // Append the controls form.appendControls(controls); // Wait for the controls to be loaded setTimeout(() => { // Parse the new controls for (let i = 0; i < controls.length; i++) { let control = controls[i].name ? form.getControl(controls[i].name) : null; if (control) { // Append the control customControls.push(control); } } }, 10); }, appendRows: (rows: Components.IFormRow[]) => { // Append the controls form.appendRows(rows); // Wait for the controls to be loaded setTimeout(() => { // Parse the rows for (let i = 0; i < rows.length; i++) { // Parse the columns let columns = rows[i].columns; for (let j = 0; j < columns.length; j++) { // Get the control let control = columns[j].control && columns[j].control.name ? form.getControl(columns[j].control.name) : null; if (control) { // Append the control customControls.push(control); } } } }, 10); }, el: form.el as HTMLFormElement, getControl: (fieldName: string) => { // See if it's in the mapper if (mapper[fieldName]) { // Return the control return mapper[fieldName].control; } // Parse the custom controls for (let i = 0; i < customControls.length; i++) { let control = customControls[i]; // See if this is the target control if (control.props.name == fieldName) { // Return the control return control; } } // Not found return null; }, getItem: () => { return props.info.item; }, getValues, hasAttachments: () => { // Return if attachments exist return attachments && (attachments.delete.length > 0 || attachments.new.length > 0); }, insertControl: (idx: number, control: Components.IFormControlProps) => { // Append the controls form.insertControl(idx, control); // Wait for the controls to be loaded setTimeout(() => { let newControl = control.name ? form.getControl(control.name) : null; if (newControl) { // Append the control customControls.push(newControl); } }, 10); }, isValid: () => { let isValid = true; // Parse the fields for (let fieldName in props.info.fields) { // See if we are showing a specific set of fields if (props.includeFields && props.includeFields.indexOf(fieldName) < 0) { continue; } // See if we are using a template, and ensure the field is in it if (templateFieldNames && templateFieldNames.indexOf(fieldName) < 0) { continue; } // Skip readonly fields let formField = mapper[fieldName]; if (formField == null || formField.controlProps.isReadonly) { continue; } // Validate the form field and update the status flag let controlIsValid = formField.isValid(); // Update the flag isValid = isValid && controlIsValid; // See if the control is not valid if (!controlIsValid) { // Log the information console.info("Form Control '" + fieldName + "' is not valid.", formField); } } // Parse the custom controls for (let i = 0; i < customControls.length; i++) { let customControl = customControls[i]; // Validate the form field and update the status flag let controlIsValid = customControl.isValid; isValid = isValid && controlIsValid; // See if the control is not valid if (!controlIsValid) { // Log the information console.info("Custom Form Control '" + customControl.props.name + "' is not valid.", customControl); } } // Return the flag return isValid; }, refreshRequestDigest: () => { return ListForm.refreshRequestDigest(props.info); }, save: (customValues: any = {}, checkItemVersion?: boolean) => { let onSaving = (values) => { return new Promise((resolve) => { // See if a save event exists let returnVal = props.onSaving ? props.onSaving(values) : null; if (returnVal && returnVal.then) { // Wait for the promise to complete returnVal.then(newValues => { // Resolve the promise resolve(newValues || values); }); } else { // Resolve the promise resolve(values); } }); } // Return a promise return new Promise((resolve, reject) => { let formValues = getValues(); // Upload the images uploadImages(props.info).then((imageValues) => { // Call the saving event onSaving({ ...formValues, ...imageValues, ...customValues }).then(values => { // Update the item ListForm.saveItem(props.info, values, checkItemVersion).then(info => { // Remove the attachments removeAttachments(info).then(() => { // Save the attachments saveAttachments(info).then(() => { // Update the info props.info = info; // Resolve the promise resolve(props.info.item as any); }); }); }, reject); }); }); }); } }; // Execute the assign to event props.assignTo ? props.assignTo(formObj) : null; // Return the form return formObj; };