Version 3 #40
@ -18,13 +18,12 @@ open NodaTime.Serialization.SystemTextJson
|
||||
/// JsonSerializer options that use the custom converters
|
||||
let options =
|
||||
let opts = JsonSerializerOptions ()
|
||||
[ WrappedJsonConverter (CitizenId.ofString, CitizenId.toString) :> JsonConverter
|
||||
WrappedJsonConverter (ContactType.parse, ContactType.toString)
|
||||
WrappedJsonConverter (ContinentId.ofString, ContinentId.toString)
|
||||
WrappedJsonConverter (ListingId.ofString, ListingId.toString)
|
||||
WrappedJsonConverter (Text, MarkdownString.toString)
|
||||
WrappedJsonConverter (OtherContactId.ofString, OtherContactId.toString)
|
||||
WrappedJsonConverter (SuccessId.ofString, SuccessId.toString)
|
||||
[ WrappedJsonConverter (CitizenId.ofString, CitizenId.toString) :> JsonConverter
|
||||
WrappedJsonConverter (ContactType.parse, ContactType.toString)
|
||||
WrappedJsonConverter (ContinentId.ofString, ContinentId.toString)
|
||||
WrappedJsonConverter (ListingId.ofString, ListingId.toString)
|
||||
WrappedJsonConverter (Text, MarkdownString.toString)
|
||||
WrappedJsonConverter (SuccessId.ofString, SuccessId.toString)
|
||||
JsonFSharpConverter ()
|
||||
]
|
||||
|> List.iter opts.Converters.Add
|
||||
|
@ -4,47 +4,6 @@ module JobsJobsJobs.Domain.SharedTypes
|
||||
open JobsJobsJobs.Domain
|
||||
open NodaTime
|
||||
|
||||
/// The data to add or update an other contact
|
||||
type OtherContactForm =
|
||||
{ /// The ID of the contact
|
||||
Id : string
|
||||
|
||||
/// The type of the contact
|
||||
ContactType : string
|
||||
|
||||
/// The name of the contact
|
||||
Name : string option
|
||||
|
||||
/// The value of the contact (URL, e-mail address, phone, etc.)
|
||||
Value : string
|
||||
|
||||
/// Whether this contact is displayed for public employment profiles and job listings
|
||||
IsPublic : bool
|
||||
}
|
||||
|
||||
|
||||
/// The data available to update an account profile
|
||||
type AccountProfileForm =
|
||||
{ /// The first name of the citizen
|
||||
FirstName : string
|
||||
|
||||
/// The last name of the citizen
|
||||
LastName : string
|
||||
|
||||
/// The display name for the citizen
|
||||
DisplayName : string option
|
||||
|
||||
/// The citizen's new password
|
||||
NewPassword : string option
|
||||
|
||||
/// Confirmation of the citizen's new password
|
||||
NewPasswordConfirm : string option
|
||||
|
||||
/// The contacts for this profile
|
||||
Contacts : OtherContactForm list
|
||||
}
|
||||
|
||||
|
||||
/// The data needed to display a listing
|
||||
[<NoComparison; NoEquality>]
|
||||
type ListingForView =
|
||||
|
@ -78,22 +78,6 @@ module MarkdownString =
|
||||
let toString = function Text text -> text
|
||||
|
||||
|
||||
/// The ID of an other contact
|
||||
type OtherContactId = OtherContactId of Guid
|
||||
|
||||
/// Support functions for other contact IDs
|
||||
module OtherContactId =
|
||||
|
||||
/// Create a new job listing ID
|
||||
let create () = (Guid.NewGuid >> OtherContactId) ()
|
||||
|
||||
/// A string representation of a listing ID
|
||||
let toString = function OtherContactId it -> ShortGuid.fromGuid it
|
||||
|
||||
/// Parse a string into a listing ID
|
||||
let ofString = ShortGuid.toGuid >> OtherContactId
|
||||
|
||||
|
||||
/// Types of contacts supported by Jobs, Jobs, Jobs
|
||||
type ContactType =
|
||||
/// E-mail addresses
|
||||
@ -124,10 +108,7 @@ module ContactType =
|
||||
|
||||
/// Another way to contact a citizen from this site
|
||||
type OtherContact =
|
||||
{ /// The ID of the contact
|
||||
Id : OtherContactId
|
||||
|
||||
/// The type of contact
|
||||
{ /// The type of contact
|
||||
ContactType : ContactType
|
||||
|
||||
/// The name of the contact (Email, No Agenda Social, LinkedIn, etc.)
|
||||
|
@ -251,7 +251,16 @@ module Citizen =
|
||||
|> Array.ofSeq
|
||||
challenges <- Some qAndA
|
||||
qAndA
|
||||
|
||||
|
||||
// GET: /citizen/account
|
||||
let account : HttpHandler = fun next ctx -> task {
|
||||
match! Citizens.findById (currentCitizenId ctx) with
|
||||
| Some citizen ->
|
||||
return!
|
||||
Citizen.account (AccountProfileForm.fromCitizen citizen) (csrf ctx) |> render "Account Profile" next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// GET: /citizen/confirm/[token]
|
||||
let confirm token next ctx = task {
|
||||
let! isConfirmed = Citizens.confirmAccount token
|
||||
@ -387,44 +396,52 @@ module Citizen =
|
||||
return! refreshPage () next ctx
|
||||
}
|
||||
|
||||
// POST: /citizen/save-account
|
||||
let saveAccount : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task {
|
||||
let! theForm = ctx.BindFormAsync<AccountProfileForm> ()
|
||||
let form = { theForm with Contacts = theForm.Contacts |> Array.filter (box >> isNull >> not) }
|
||||
let errors = [
|
||||
if form.FirstName = "" then "First Name is required"
|
||||
if form.LastName = "" then "Last Name is required"
|
||||
if form.NewPassword <> form.NewPassword then "New passwords do not match"
|
||||
if form.Contacts |> Array.exists (fun c -> c.ContactType = "") then "All Contact Types are required"
|
||||
if form.Contacts |> Array.exists (fun c -> c.Value = "") then "All Contacts are required"
|
||||
]
|
||||
if List.isEmpty errors then
|
||||
match! Citizens.findById (currentCitizenId ctx) with
|
||||
| Some citizen ->
|
||||
let password =
|
||||
if form.NewPassword = "" then citizen.PasswordHash
|
||||
else Auth.Passwords.hash citizen form.NewPassword
|
||||
do! Citizens.save
|
||||
{ citizen with
|
||||
FirstName = form.FirstName
|
||||
LastName = form.LastName
|
||||
DisplayName = noneIfEmpty form.DisplayName
|
||||
PasswordHash = password
|
||||
OtherContacts = form.Contacts
|
||||
|> Array.map (fun c ->
|
||||
{ OtherContact.Name = noneIfEmpty c.Name
|
||||
ContactType = ContactType.parse c.ContactType
|
||||
Value = c.Value
|
||||
IsPublic = c.IsPublic
|
||||
})
|
||||
|> List.ofArray
|
||||
}
|
||||
let extraMsg = if form.NewPassword = "" then "" else " and password changed"
|
||||
do! addSuccess $"Account profile updated{extraMsg} successfully" ctx
|
||||
return! redirectToGet "/citizen/account" next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
else
|
||||
do! addErrors errors ctx
|
||||
return! Citizen.account form (csrf ctx) |> render "Account Profile" next ctx
|
||||
}
|
||||
|
||||
// GET: /citizen/so-long
|
||||
let soLong : HttpHandler = requireUser >=> fun next ctx ->
|
||||
Citizen.deletionOptions (csrf ctx) |> render "Account Deletion Options" next ctx
|
||||
|
||||
|
||||
/// Handlers for /api/citizen routes
|
||||
[<RequireQualifiedAccess>]
|
||||
module CitizenApi =
|
||||
|
||||
// PATCH: /api/citizen/account
|
||||
let account : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let! form = ctx.BindJsonAsync<AccountProfileForm> ()
|
||||
match! Citizens.findById (currentCitizenId ctx) with
|
||||
| Some citizen ->
|
||||
let password =
|
||||
if defaultArg form.NewPassword "" = "" then citizen.PasswordHash
|
||||
else Auth.Passwords.hash citizen form.NewPassword.Value
|
||||
do! Citizens.save
|
||||
{ citizen with
|
||||
FirstName = form.FirstName
|
||||
LastName = form.LastName
|
||||
DisplayName = noneIfBlank form.DisplayName
|
||||
PasswordHash = password
|
||||
OtherContacts = form.Contacts
|
||||
|> List.map (fun c -> {
|
||||
Id = if c.Id.StartsWith "new" then OtherContactId.create ()
|
||||
else OtherContactId.ofString c.Id
|
||||
ContactType = ContactType.parse c.ContactType
|
||||
Name = noneIfBlank c.Name
|
||||
Value = c.Value
|
||||
IsPublic = c.IsPublic
|
||||
})
|
||||
}
|
||||
return! ok next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
|
||||
/// Handlers for the home page, legal stuff, and help
|
||||
[<RequireQualifiedAccess>]
|
||||
module Home =
|
||||
@ -792,6 +809,7 @@ let allEndpoints = [
|
||||
]
|
||||
subRoute "/citizen" [
|
||||
GET_HEAD [
|
||||
route "/account" Citizen.account
|
||||
routef "/confirm/%s" Citizen.confirm
|
||||
route "/dashboard" Citizen.dashboard
|
||||
routef "/deny/%s" Citizen.deny
|
||||
@ -801,9 +819,10 @@ let allEndpoints = [
|
||||
route "/so-long" Citizen.soLong
|
||||
]
|
||||
POST [
|
||||
route "/delete" Citizen.delete
|
||||
route "/log-on" Citizen.doLogOn
|
||||
route "/register" Citizen.doRegistration
|
||||
route "/delete" Citizen.delete
|
||||
route "/log-on" Citizen.doLogOn
|
||||
route "/register" Citizen.doRegistration
|
||||
route "/save-account" Citizen.saveAccount
|
||||
]
|
||||
]
|
||||
subRoute "/listing" [
|
||||
@ -838,13 +857,7 @@ let allEndpoints = [
|
||||
]
|
||||
POST [ route "y/save" Success.save ]
|
||||
]
|
||||
|
||||
subRoute "/api" [
|
||||
subRoute "/citizen" [
|
||||
PATCH [
|
||||
route "/account" CitizenApi.account
|
||||
]
|
||||
]
|
||||
POST [ route "/markdown-preview" Api.markdownPreview ]
|
||||
]
|
||||
]
|
||||
|
@ -3,6 +3,70 @@ module JobsJobsJobs.ViewModels
|
||||
|
||||
open JobsJobsJobs.Domain
|
||||
|
||||
/// The data to add or update an other contact
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type OtherContactForm =
|
||||
{ /// The type of the contact
|
||||
ContactType : string
|
||||
|
||||
/// The name of the contact
|
||||
Name : string
|
||||
|
||||
/// The value of the contact (URL, e-mail address, phone, etc.)
|
||||
Value : string
|
||||
|
||||
/// Whether this contact is displayed for public employment profiles and job listings
|
||||
IsPublic : bool
|
||||
}
|
||||
|
||||
/// Support functions for the contact form
|
||||
module OtherContactForm =
|
||||
|
||||
/// Create a contact form from a contact
|
||||
let fromContact (contact : OtherContact) =
|
||||
{ ContactType = ContactType.toString contact.ContactType
|
||||
Name = defaultArg contact.Name ""
|
||||
Value = contact.Value
|
||||
IsPublic = contact.IsPublic
|
||||
}
|
||||
|
||||
|
||||
/// The data available to update an account profile
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type AccountProfileForm =
|
||||
{ /// The first name of the citizen
|
||||
FirstName : string
|
||||
|
||||
/// The last name of the citizen
|
||||
LastName : string
|
||||
|
||||
/// The display name for the citizen
|
||||
DisplayName : string
|
||||
|
||||
/// The citizen's new password
|
||||
NewPassword : string
|
||||
|
||||
/// Confirmation of the citizen's new password
|
||||
NewPasswordConfirm : string
|
||||
|
||||
/// The contacts for this profile
|
||||
Contacts : OtherContactForm array
|
||||
}
|
||||
|
||||
/// Support functions for the account profile form
|
||||
module AccountProfileForm =
|
||||
|
||||
/// Create an account profile form from a citizen
|
||||
let fromCitizen (citizen : Citizen) =
|
||||
{ FirstName = citizen.FirstName
|
||||
LastName = citizen.LastName
|
||||
DisplayName = defaultArg citizen.DisplayName ""
|
||||
NewPassword = ""
|
||||
NewPasswordConfirm = ""
|
||||
Contacts = citizen.OtherContacts |> List.map OtherContactForm.fromContact |> Array.ofList
|
||||
}
|
||||
|
||||
|
||||
/// The fields required for a skill
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type SkillForm =
|
||||
|
@ -7,6 +7,116 @@ open Giraffe.ViewEngine.Htmx
|
||||
open JobsJobsJobs.Domain
|
||||
open JobsJobsJobs.ViewModels
|
||||
|
||||
/// The form to add or edit a means of contact
|
||||
let contactEdit (contacts : OtherContactForm array) =
|
||||
let mapToInputs (idx : int) (contact : OtherContactForm) =
|
||||
div [ _id $"contactRow{idx}"; _class "row pb-3" ] [
|
||||
div [ _class "col-2 col-md-1" ] [
|
||||
button [ _type "button"; _class "btn btn-sm btn-outline-danger rounded-pill mt-3"; _title "Delete"
|
||||
_onclick $"jjj.citizen.removeContact({idx})" ] [
|
||||
rawText " − "
|
||||
]
|
||||
]
|
||||
div [ _class "col-10 col-md-4 col-xl-3" ] [
|
||||
div [ _class "form-floating" ] [
|
||||
select [ _id $"contactType{idx}"; _name $"Contacts[{idx}].ContactType"; _class "form-control"
|
||||
_value contact.ContactType; _placeholder "Type"; _required ] [
|
||||
option [ _value "Website" ] [ rawText "Website" ]
|
||||
option [ _value "Email" ] [ rawText "E-mail Address" ]
|
||||
option [ _value "Phone" ] [ rawText "Phone Number" ]
|
||||
]
|
||||
label [ _class "jjj-required"; _for $"contactType{idx}" ] [ rawText "Type" ]
|
||||
]
|
||||
]
|
||||
div [ _class "col-12 col-md-4 col-xl-3" ] [
|
||||
div [ _class "form-floating" ] [
|
||||
input [ _type "text"; _id $"contactName{idx}"; _name $"Contacts[{idx}].Name"; _class "form-control"
|
||||
_maxlength "1000"; _value contact.Name; _placeholder "Name" ]
|
||||
label [ _class "jjj-label"; _for $"contactName{idx}" ] [ rawText "Name" ]
|
||||
]
|
||||
if idx < 1 then
|
||||
div [ _class "form-text" ] [ rawText "Optional; will link sites and e-mail, qualify phone numbers" ]
|
||||
]
|
||||
div [ _class "col-12 col-md-7 offset-md-1 col-xl-4 offset-xl-0" ] [
|
||||
div [ _class "form-floating" ] [
|
||||
input [ _type "text"; _id $"contactValue{idx}"; _name $"Contacts[{idx}].Value"
|
||||
_class "form-control"; _maxlength "1000"; _value contact.Value; _placeholder "Contact"
|
||||
_required ]
|
||||
label [ _class "jjj-required"; _for "contactValue{idx}" ] [ rawText "Contact" ]
|
||||
]
|
||||
if idx < 1 then div [ _class "form-text"] [ rawText "The URL, e-mail address, or phone number" ]
|
||||
]
|
||||
div [ _class "col-12 col-md-3 offset-md-1 col-xl-1 offset-xl-0" ] [
|
||||
div [ _class "form-check mt-3" ] [
|
||||
input [ _type "checkbox"; _id $"contactIsPublic{idx}"; _name $"Contacts[{idx}].IsPublic";
|
||||
_class "form-check-input"; _value "true"; if contact.IsPublic then _checked ]
|
||||
label [ _class "form-check-label"; _for $"contactIsPublic{idx}" ] [ rawText "Public" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
template [ _id "newContact" ] [
|
||||
mapToInputs -1 { ContactType = "Website"; Name = ""; Value = ""; IsPublic = false }
|
||||
]
|
||||
:: (contacts |> Array.mapi mapToInputs |> List.ofArray)
|
||||
|
||||
/// The account edit page
|
||||
let account (m : AccountProfileForm) csrf =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Account Profile" ]
|
||||
p [] [
|
||||
rawText "This information is visible to all fellow logged-on citizens. For publicly-visible employment "
|
||||
rawText "profiles and job listings, the “Display Name” fields and any public contacts will be "
|
||||
rawText "displayed."
|
||||
]
|
||||
form [ _class "row g-3"; _method "POST"; _action "/citizen/save-account" ] [
|
||||
antiForgery csrf
|
||||
div [ _class "col-6 col-xl-4" ] [
|
||||
textBox [ _type "text"; _autofocus ] (nameof m.FirstName) m.FirstName "First Name" true
|
||||
]
|
||||
div [ _class "col-6 col-xl-4" ] [
|
||||
textBox [ _type "text" ] (nameof m.LastName) m.LastName "Last Name" true
|
||||
]
|
||||
div [ _class "col-6 col-xl-4" ] [
|
||||
textBox [ _type "text" ] (nameof m.DisplayName) m.DisplayName "Display Name" false
|
||||
div [ _class "form-text" ] [ em [] [ rawText "Optional; overrides first/last for display" ] ]
|
||||
]
|
||||
div [ _class "col-6 col-xl-4" ] [
|
||||
textBox [ _type "password"; _minlength "8" ] (nameof m.NewPassword) "" "New Password" false
|
||||
div [ _class "form-text" ] [ rawText "Leave blank to keep your current password" ]
|
||||
]
|
||||
div [ _class "col-6 col-xl-4" ] [
|
||||
textBox [ _type "password"; _minlength "8" ] (nameof m.NewPasswordConfirm) "" "Confirm New Password"
|
||||
false
|
||||
div [ _class "form-text" ] [ rawText "Leave blank to keep your current password" ]
|
||||
]
|
||||
div [ _class "col-12" ] [
|
||||
hr []
|
||||
h4 [ _class "pb-2" ] [
|
||||
rawText "Ways to Be Contacted "
|
||||
button [ _type "button"; _class "btn btn-sm btn-outline-primary rounded-pill"
|
||||
_onclick "jjj.citizen.addContact()" ] [
|
||||
rawText "Add a Contact Method"
|
||||
]
|
||||
]
|
||||
]
|
||||
yield! contactEdit m.Contacts
|
||||
div [ _class "col-12" ] [
|
||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
||||
i [ _class "mdi mdi-content-save-outline" ] [ rawText " Save" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
hr []
|
||||
p [ _class "text-muted fst-italic" ] [
|
||||
rawText "(If you want to delete your profile, or your entire account, "
|
||||
a [ _href "/citizen/so-long" ] [ rawText "see your deletion options here" ]; rawText ".)"
|
||||
]
|
||||
jsOnLoad $"
|
||||
jjj.citizen.nextIndex = {m.Contacts.Length}
|
||||
jjj.citizen.validatePasswords('{nameof m.NewPassword}', '{nameof m.NewPasswordConfirm}', false)"
|
||||
]
|
||||
|
||||
|
||||
/// The account confirmation page
|
||||
let confirmAccount isConfirmed =
|
||||
article [] [
|
||||
@ -231,19 +341,7 @@ let register q1 q2 (m : RegisterViewModel) csrf =
|
||||
i [ _class "mdi mdi-content-save-outline" ] []; rawText " Save"
|
||||
]
|
||||
]
|
||||
script [] [ rawText """
|
||||
const pw = document.getElementById("Password")
|
||||
const pwConfirm = document.getElementById("ConfirmPassword")
|
||||
pwConfirm.addEventListener("input", () => {
|
||||
if (!pw.validity.valid) {
|
||||
pwConfirm.setCustomValidity("")
|
||||
} else if (!pwConfirm.validity.valueMissing && pw.value !== pwConfirm.value) {
|
||||
pwConfirm.setCustomValidity("Confirmation password does not match")
|
||||
} else {
|
||||
pwConfirm.setCustomValidity("")
|
||||
}
|
||||
})"""
|
||||
]
|
||||
jsOnLoad $"jjj.citizen.validatePasswords('{nameof m.Password}', 'ConfirmPassword', true)"
|
||||
]
|
||||
]
|
||||
|
||||
|
@ -49,6 +49,12 @@ let continentList attrs name (continents : Continent list) emptyLabel selectedVa
|
||||
label [ _class (if isRequired then "jjj-required" else "jjj-label"); _for name ] [ rawText "Continent" ]
|
||||
]
|
||||
|
||||
/// Register JavaScript code to run in the DOMContentLoaded event on the page
|
||||
let jsOnLoad js =
|
||||
script [] [
|
||||
rawText """document.addEventListener("DOMContentLoaded", function () { """; rawText js; rawText " })"
|
||||
]
|
||||
|
||||
/// Create a Markdown editor
|
||||
let markdownEditor attrs name value editorLabel =
|
||||
div [ _class "col-12"; _id $"{name}EditRow" ] [
|
||||
@ -71,11 +77,7 @@ let markdownEditor attrs name value editorLabel =
|
||||
]
|
||||
label [ _for name ] [ rawText editorLabel ]
|
||||
]
|
||||
script [] [
|
||||
rawText """document.addEventListener("DOMContentLoaded", function () {"""
|
||||
rawText $" jjj.markdownOnLoad('{name}') "
|
||||
rawText "})"
|
||||
]
|
||||
jsOnLoad $"jjj.markdownOnLoad('{name}')"
|
||||
]
|
||||
|
||||
/// Wrap content in a collapsing panel
|
||||
|
@ -72,9 +72,7 @@ let expire (m : ExpireListingForm) (listing : Listing) csrf =
|
||||
]
|
||||
]
|
||||
]
|
||||
script [] [
|
||||
rawText """document.addEventListener("DOMContentLoaded", function () { jjj.listing.toggleFromHere() })"""
|
||||
]
|
||||
jsOnLoad "jjj.listing.toggleFromHere()"
|
||||
]
|
||||
|
||||
|
||||
|
@ -119,11 +119,7 @@ let edit (m : EditProfileViewModel) continents isNew citizenId csrf =
|
||||
rawText "(If you want to delete your profile, or your entire account, "
|
||||
a [ _href "/citizen/so-long" ] [ rawText "see your deletion options here" ]; rawText ".)"
|
||||
]
|
||||
script [] [
|
||||
rawText """addEventListener("DOMContentLoaded", function () {"""
|
||||
rawText $" jjj.profile.nextIndex = {m.Skills.Length} "
|
||||
rawText "})"
|
||||
]
|
||||
jsOnLoad $"jjj.profile.nextIndex = {m.Skills.Length}"
|
||||
]
|
||||
|
||||
|
||||
|
@ -130,6 +130,89 @@ this.jjj = {
|
||||
editDiv.classList.add("jjj-shown")
|
||||
},
|
||||
|
||||
citizen: {
|
||||
|
||||
/**
|
||||
* The next index for a newly-added contact
|
||||
* @type {number}
|
||||
*/
|
||||
nextIndex: 0,
|
||||
|
||||
/**
|
||||
* Add a contact to the account form
|
||||
*/
|
||||
addContact() {
|
||||
const next = this.nextIndex
|
||||
|
||||
/** @type {HTMLTemplateElement} */
|
||||
const newContactTemplate = document.getElementById("newContact")
|
||||
/** @type {HTMLDivElement} */
|
||||
const newContact = newContactTemplate.content.firstElementChild.cloneNode(true)
|
||||
newContact.setAttribute("id", `contactRow${next}`)
|
||||
|
||||
const cols = newContact.children
|
||||
// Button column
|
||||
cols[0].querySelector("button").setAttribute("onclick", `jjj.citizen.removeContact(${next})`)
|
||||
// Contact Type column
|
||||
const typeField = cols[1].querySelector("select")
|
||||
typeField.setAttribute("id", `contactType${next}`)
|
||||
typeField.setAttribute("name", `Contacts[${this.nextIndex}].ContactType`)
|
||||
cols[1].querySelector("label").setAttribute("for", `contactType${next}`)
|
||||
// Name column
|
||||
const nameField = cols[2].querySelector("input")
|
||||
nameField.setAttribute("id", `contactName${next}`)
|
||||
nameField.setAttribute("name", `Contacts[${this.nextIndex}].Name`)
|
||||
cols[2].querySelector("label").setAttribute("for", `contactName${next}`)
|
||||
if (next > 0) cols[2].querySelector("div.form-text").remove()
|
||||
// Value column
|
||||
const valueField = cols[3].querySelector("input")
|
||||
valueField.setAttribute("id", `contactValue${next}`)
|
||||
valueField.setAttribute("name", `Contacts[${this.nextIndex}].Value`)
|
||||
cols[3].querySelector("label").setAttribute("for", `contactName${next}`)
|
||||
if (next > 0) cols[3].querySelector("div.form-text").remove()
|
||||
// Is Public column
|
||||
const isPublicField = cols[4].querySelector("input")
|
||||
isPublicField.setAttribute("id", `contactIsPublic${next}`)
|
||||
isPublicField.setAttribute("name", `Contacts[${this.nextIndex}].IsPublic`)
|
||||
cols[4].querySelector("label").setAttribute("for", `contactIsPublic${next}`)
|
||||
|
||||
// Add the row
|
||||
const contacts = document.querySelectorAll("div[id^=contactRow]")
|
||||
const sibling = contacts.length > 0 ? contacts[contacts.length - 1] : newContactTemplate
|
||||
sibling.insertAdjacentElement('afterend', newContact)
|
||||
|
||||
this.nextIndex++
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a contact row from the profile form
|
||||
* @param {number} idx The index of the contact row to remove
|
||||
*/
|
||||
removeContact(idx) {
|
||||
document.getElementById(`contactRow${idx}`).remove()
|
||||
},
|
||||
|
||||
/**
|
||||
* Register a comparison validation between a password and a "confirm password" field
|
||||
* @param {string} pwId The ID for the password field
|
||||
* @param {string} confirmId The ID for the "confirm password" field
|
||||
* @param {boolean} isRequired Whether these fields are required
|
||||
*/
|
||||
validatePasswords(pwId, confirmId, isRequired) {
|
||||
const pw = document.getElementById(pwId)
|
||||
const pwConfirm = document.getElementById(confirmId)
|
||||
pwConfirm.addEventListener("input", () => {
|
||||
if (!pw.validity.valid) {
|
||||
pwConfirm.setCustomValidity("")
|
||||
} else if ((!pwConfirm.validity.valueMissing || !isRequired) && pw.value !== pwConfirm.value) {
|
||||
pwConfirm.setCustomValidity("Confirmation password does not match")
|
||||
} else {
|
||||
pwConfirm.setCustomValidity("")
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Script for listing pages
|
||||
*/
|
||||
@ -172,7 +255,7 @@ this.jjj = {
|
||||
|
||||
const cols = newSkill.children
|
||||
// Button column
|
||||
cols[0].querySelector("button").setAttribute("onclick", `jjj.profile.removeSkill('${next}')`)
|
||||
cols[0].querySelector("button").setAttribute("onclick", `jjj.profile.removeSkill(${next})`)
|
||||
// Skill column
|
||||
const skillField = cols[1].querySelector("input")
|
||||
skillField.setAttribute("id", `skillDesc${next}`)
|
||||
@ -196,10 +279,10 @@ this.jjj = {
|
||||
|
||||
/**
|
||||
* Remove a skill row from the profile form
|
||||
* @param {number} id The ID of the skill row to remove
|
||||
* @param {number} idx The index of the skill row to remove
|
||||
*/
|
||||
removeSkill(id) {
|
||||
document.getElementById(`skillRow${id}`).remove()
|
||||
removeSkill(idx) {
|
||||
document.getElementById(`skillRow${idx}`).remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user