/** * @fileoverview * Comex Client Module: initialize and expose as *cmxClt* var * -> shared vars for css * -> shared vars and functions for all user forms in *cmxClt.uform* submodule * * @todo * - package.json * * @version 1 * @copyright ISCPIF-CNRS 2016 * @author romain.loth@iscpif.fr * */ // initialize and export cmxClt module var cmxClt = (function() { let cC = {} // cf corresponding css classes cC.colorWhite = '#fff' cC.colorRed = '#910' cC.colorGreen = '#161' cC.colorGrey = '#554' cC.colorLGrey = '#ddd' cC.colorOrange = '#F96' cC.colorBlue = '#23A' cC.strokeWhite = ".8px .8px #fff, -.8px -.8px #fff, -.8px .8px #fff, .8px -.8px #fff" cC.strokeGrey = ".8px .8px #333, -.8px -.8px #333, -.8px .8px #333, .8px -.8px #333" cC.strokeBlack = ".5px .5px #000, -.5px -.5px #000, -.5px .5px #000, .5px -.5px #000" cC.strokeDeepGrey = "3px 3px 4px #333,-3px 3px 4px #333,-3px -3px 4px #333,3px -3px 4px #333" // the target columns in DB: tuple (name, mandatory, group, type, section) cC.COLS = [ ["keywords", true, "plsfill", "at", "map_infos"], // ==> *keywords* table ["hashtags", false, "plsfill", "at", "map_infos"], // ==> *hashtags* table ["doors_uid", true, "auto" , "t", null], ["last_modified_date", true, "auto" , "d", null], ["hon_title", false, "plsfill", "t", "basic_infos"], ["email", true, "plsfill", "t", "login_infos"], ["first_name", true, "plsfill", "t", "basic_infos"], ["middle_name", false, "pref", "t", "basic_infos"], ["last_name", true, "plsfill", "t", "basic_infos"], ["country", true, "plsfill", "t", "basic_infos"], ["initials", true, "pref", "t", null], ["position", true, "plsfill", "t", "map_infos"], ["interests_text", false, "pref", "t", "other_infos"], ["gender", false, "plsfill", "m", "other_infos"], ["job_looking_date", false, "pref" , "d", "map_infos"], ["home_url", false, "plsfill", "t", "other_infos"], ["pic_url", false, "pref" , "t", "other_infos"], ["pic_file", false, "pref" , "f", "other_infos"], // ==> *scholars* table ["org", false, "plsfill", "t", "org_infos"], ["org_type", false, "plsfill", "m", "org_infos"], ["team_lab", true, "plsfill", "t", "map_infos"], ["org_city", false, "pref" , "t", "org_infos"] // ==> *affiliations* table ] // group "auto" === filled by controllers // group "plsfill" === filled by user, ideally needed for a complete profile // group "pref" === filled by user but not needed at all cC.miniSanitize = function(aString) { return aString.replace(/[^A-z0-9, :\(\)-]/, ' ').replace(/^ +| +$/, '') } cC.makeRandomString = function (nChars) { var rando = "" var possible = "abcdefghijklmnopqrstuvwxyz0123456789"; var len = possible.length for( var i=0; i < nChars; i++ ) rando += possible.charAt(Math.floor(Math.random() * len)); return rando } cC.ulListFromLabelsArray = function (cplArray, ulClassList, message) { ulClasses=["minilabels"].concat(ulClassList).join(" ") var resultHtml = "" if (message) { resultHtml = cC.miniSanitize(message) } resultHtml += '<ul class="'+ulClasses+'">' for (var i in cplArray) { var fname = cplArray[i][0] var flabel = cplArray[i][1] // to open any collapsible containing the label and input var openFun = 'return cmxClt.uform.gotoField(\''+fname+'\')' // debug onclick fun // console.log("openFun", openFun) // link works if anchorLabels was run resultHtml += '<li class="minilabel"><div onclick="'+openFun+'">'+flabel+'</div></li>' } resultHtml += '</ul>' return resultHtml } // basic inputs get normal on focus cC.makeNormal = function (elt) { elt.style.fontWeight = "normal" } // basic inputs get bold on blur cC.makeBold = function (elt){ if (elt.value != "") elt.style.fontWeight = "bold" } // insert after // cf. stackoverflow.com/questions/4793604 cC.insertAfter = function(referenceNode, newNode) { referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling ) } // find ancestor // cf. stackoverflow.com/questions/22119673 cC.findAncestor = function(elt, cls) { // console.log("findAncestor starting from", elt.id) while ((elt = elt.parentElement) && !elt.classList.contains(cls)); // console.log("findAncestor returning", elt) return elt } // ============================================ // cmxClt.uform: user forms class and functions // ============================================ cC.uform = {} // a class var with all initialized forms on the page cC.uform.allForms = {} // functions cC.uform.initialize cC.uform.testFillField cC.uform.simpleValidateAndMessage cC.uform.gotoField cC.uform.multiTextinput cC.uform.stampTime // <= POSS replace by sql stamp // dates up to 2049/12/31 cC.uform.validDate = new RegExp( /^20[0-4][0-9]\/(?:0?[1-9]|1[0-2])\/(?:0?[1-9]|[1-2][0-9]|3[0-1])$/) // function definitions // ===================== // multiTextinput // // stub for multiple textinput like keywords // => UX shows newInput where user enters words one by one // => validate words become removable "pills" // => result is concatenated texts in hidden input.#fName // TODO finalize and add to initialize cC.uform.multiTextinput = function (fName, otherMtiParams, aUForm) { // console.debug ("multiTextinput args:", fName, otherMtiParams, aUForm) var perhapsPreviousValues = otherMtiParams.prevals var perhapsColor = otherMtiParams.color var perhapsReadonly = otherMtiParams.readonly // HTML elt to insert tag boxes around var refElt = null // new array for full results aUForm.mtiStock[fName] = [] // there must be a normal text input var normalInput = document.getElementById(fName) // POSS use autocomp // perhaps surrounding input group useful if we want to insertBefore // var inputWrap = cC.findAncestor(normalInput, 'question') // if (inputWrap) { // refElt = inputWrap // } // else { refElt = normalInput // } refElt.style.marginBottom = 0 // shows a box and saves the input value in mtiStock var pushTagbox = function(event) { // debug // console.log ('pushTagbox from event' + event.type) var asIsValue = normalInput.value if (asIsValue != '') { // maybe several values from cache (comma is reserved as separator) var subValues = asIsValue.split(/,/) for (var i in subValues) { var newValue = subValues[i] // "let" so that it's unique for each i // (this unique pointer is useful in newBoxClose) let newBox = document.createElement('div') // move the value normalInput.value = '' newBox.textContent = newValue // and save it var nSaved = aUForm.mtiStock[fName].push(newValue) // create a close elt for the box var newBoxClose = document.createElement('div') newBoxClose.classList.add('box-highlight-close') newBoxClose.classList.add('operation') newBoxClose.innerHTML = '<span class="glyphicon glyphicon-remove"></span>' var closeBox = function() { // start transition newBox.style.opacity = 0 // remove value from stock var i = 0 for (i in aUForm.mtiStock[fName]){ if (aUForm.mtiStock[fName][i] == newValue) { break ; } } aUForm.mtiStock[fName].splice(i, 1) // signal form change aUForm.elForm.dispatchEvent(new CustomEvent('change')) // remove box setTimeout(function(){newBox.style.display = 'none'}, 300) // console.debug("droptagbox", aUForm.id aUForm.mtiStock[fName].length, aUForm.mtiStock[fName]) } // /!\ null is like true here if (perhapsReadonly != false) { newBoxClose.onclick = closeBox } newBox.insertBefore(newBoxClose, newBox.firstChild) // show the box newBox.classList.add("box-highlight") if (perhapsColor) { newBox.style.backgroundColor = perhapsColor } cC.insertAfter(refElt, newBox) // console.debug("pushTagbox", aUForm.id, aUForm.mtiStock[fName].length, cC.uform.mtiStock[fName]) } } } // when re-entering previously saved values at init if ( perhapsPreviousValues && perhapsPreviousValues.length) { for (var i in perhapsPreviousValues) { normalInput.value = perhapsPreviousValues[i] pushTagbox() } } // bind it to 'blur', 'change' and ENTER normalInput.onblur = pushTagbox normalInput.onchange = pushTagbox normalInput.onkeydown = function (ev) { // perhaps now there's a jquery-ui autocomplete var hasAutocomplete = $(normalInput).data('ui-autocomplete') if (ev.keyCode == 13) { // ENTER if ( !hasAutocomplete || !hasAutocomplete.menu.active) { pushTagbox() } } else if (ev.which == 27) { // ESC normalInput.value = "" } } } // initialize // ----------- cC.uform.Form = function(aFormId, aValidationFun, fParams) { // new "obj" var myUform = {} // exposed vars that may be used during the interaction myUform.id = aFormId myUform.elForm = document.getElementById(aFormId) myUform.asFormData = function() { return new FormData(myUform.elForm) } // keep a ref to it in global scope cC.uform.allForms[aFormId] = myUform // events if (aValidationFun) { myUform.elForm.onkeyup = function(event) { // console.info('..elForm '+myUform.id+' event:'+event.type) return aValidationFun(myUform) } myUform.elForm.onchange = myUform.elForm.onkeyup myUform.elForm.onblur = myUform.elForm.onkeyup } // main interaction elements, if present // ------------------------ if (!fParams) fParams = {} var mainMessageId, timestampId, buttonId mainMessageId = fParams.mainMessageId || 'main_message' timestampId = fParams.timestampId || 'last_modified_date' submitBtnId = fParams.submitBtnId || 'form_submit' myUform.elMainMessage = document.getElementById(mainMessageId) myUform.elTimestamp = document.getElementById(timestampId) myUform.elSubmitBtn = document.getElementById(submitBtnId) // optional: init mtis if (fParams.multiTextinputs) { myUform.mtiStock = {} // arrays of inputs per fieldName for (var i in fParams.multiTextinputs) { // creates mtiStock entries and mti elements and events cC.uform.multiTextinput( fParams.multiTextinputs[i].id, // ex "keywords" fParams.multiTextinputs[i], // ex color, previous values myUform ) } // NB overloading the submit() function seems more practical than the submit *event* but it implies to use this function each time // (if impossible, collect values manually) // NB2 we save it as a property of the html form (and not indep var or object property otherwise the invocation context is illegal) myUform.elForm.classicSubmit = myUform.elForm.submit myUform.elForm.submit = function() { // collect multiTextinput values for (var field in myUform.mtiStock) { document.getElementById(field).value = myUform.mtiStock[field].join(',') // console.debug(" mti collected field '"+field+"', new value =", document.getElementById(field).value) } console.log("go classicSubmit") // proceed with normal submit myUform.elForm.classicSubmit() } } return myUform } // testFillField // -------------- // diagnostic over COLS, good to use in validation funs // // checks if mandatory fields are filled // checks if other plsfill ones are filled // highlights labels of missing mandatory fields cC.uform.testFillField = function (aUForm, params, cols) { // "private" copy var wholeFormData = new FormData(aUForm.elForm) // our return values var valid = true var mandatoryMissingFields = [] var otherMissingFields = [] // default params if (!params) params = {} // bool if (params.doHighlight == undefined) params.doHighlight = true if (params.fixResidue == undefined) params.fixResidue = false // $ if (!params.filterGroup) params.filterGroup = "plsfill" // @ if (!params.ignore) params.ignore = [] if (!params.cols) params.cols = cC.COLS // let's go for (var i in params.cols) { // console.info("testFillField COLS["+i+"]", cC.COLS[i]) var fieldName = params.cols[i][0] var mandatory = params.cols[i][1] var fieldGroup = params.cols[i][2] var fieldType = params.cols[i][3] var actualValue = wholeFormData.get(fieldName) // python residue ~~~> can correct on the fly // -------------- // POSS better strategy ? if (params.fixResidue) { // "None" as a string if (actualValue == "None") { actualValue = null document.getElementById(fieldName).value = "" } // arrays of text: rm brackets and any squotes if (fieldType == "at" && actualValue && actualValue.charAt(0) == '[' && actualValue.charAt(actualValue.length-1) == "]") { actualValue = actualValue.replace(/[\[\]']/g,'') document.getElementById(fieldName).value = actualValue } } // filled or not filled // -------------------- // skipping params.ignore and non-filterGroup elements var ignoreFlag = false for (var j in params.ignore) { if (fieldName == params.ignore[j]) { ignoreFlag = true break } } if (ignoreFlag || fieldGroup != params.filterGroup) continue ; // skip // otherwise get a human-readable label var labelElt = document.querySelector('label[for='+fieldName+']') var fieldLabel = labelElt ? labelElt.innerText : fieldName // console.warn('>>testFillField mtiStock:', aUForm.mtiStock) // alternative filled values from storage lists // POSS: do this only at the end before submit ? // POSS: (in that case the testFillField would only look at array) if ( (actualValue == null || actualValue == "" ) && aUForm.mtiStock[fieldName] && aUForm.mtiStock[fieldName].length) { actualValue = aUForm.mtiStock[fieldName].join(',') // debug // console.log('recreated multiTextinput value', actualValue) } // alternative null values if (actualValue == "") { actualValue = null } // debug // console.log( // "cmxClt.testFillField: field", fieldName, // "actualValue:", actualValue // ) // test mandatory ----------------- if (mandatory && actualValue == null) { // todo human-readable fieldName here mandatoryMissingFields.push([fieldName, fieldLabel]) valid = false // console.log("mandatoryMissingFields", fieldName) if (params.doHighlight && labelElt) { labelElt.style.backgroundColor = cC.colorOrange } } // test benign // may be missing but doesn't affect valid else if (actualValue == null) { otherMissingFields.push([fieldName, fieldLabel]) // console.log("otherMissingField", fieldName) } else if (params.doHighlight && labelElt) { labelElt.style.backgroundColor = "" } } // end for val in params.cols // return full form diagnostic and field census return [ valid, mandatoryMissingFields, otherMissingFields ] } // simple timestamp on #last_modified_date element // ------------------ cC.uform.stampTime = function (aUForm) { var now = new Date() aUForm.elTimestamp.value = now.toISOString() } // diagnosticParams are optional // cC.uform.simpleValidateAndMessage = function (aUform, diagnosticParams) { // console.log(">> simpleValidateAndMessage on ", aUform.id) var diagnostic = cmxClt.uform.testFillField(aUform, diagnosticParams) var isValid = diagnostic[0] var mandatoryMissingFields = diagnostic[1] var optionalMissingFields = diagnostic[2] if (isValid) { aUform.elMainMessage.innerHTML = "<span class='green glyphicon glyphicon-check glyphicon-float-left' style='float:left;'></span><p>OK thank you! <br/>(we have all the fields needed for the mapping!)<br/>(don't forget to SAVE!)</p>" aUform.elMainMessage.classList.add('faded') } else { aUform.elMainMessage.innerHTML = "<span class='orange glyphicon glyphicon-exclamation-sign glyphicon-float-left'></span><p>There are some <br/> important missing fields</p>" aUform.elMainMessage.classList.remove('faded') } // list of missing fields aUform.elMainMessage.innerHTML += cmxClt.ulListFromLabelsArray(mandatoryMissingFields, ['orange']) if (optionalMissingFields.length) { aUform.elMainMessage.innerHTML += cmxClt.ulListFromLabelsArray( optionalMissingFields, ['white'], "You may also want to fill:" ) } } // gotoField // (assumes nothing) // (side-effect: opens the corresponding panel) cC.uform.gotoField = function (fName) { // debug // console.log('goto fName', fName) var fieldElt = document.getElementById(fName) // open panel if it is closed var ourPanel = cC.findAncestor(fieldElt, "panel-collapse") if (ourPanel && ! ourPanel.classList.contains('in')) { // POSS use cols with key/value structure to use cols[fName] instead of looking for i var theCol = -1 for (var i in cC.COLS) { if (fName == cC.COLS[i][0]) { theCol = i break } } var ccSection = cC.COLS[i][4] // debug // console.log('ccSection', ccSection) if (ccSection) { // click the corresponding toggler document.getElementById('ccsection_toggle_'+ccSection).click() } } // now go to the field itself (actually, 120px above) // -------------------------------------------------- fieldElt.scrollIntoView(true) window.scrollTo(window.scrollX, window.scrollY - 120) } // =================================================================== // additional controllers for detailed forms like /register, /profile // =================================================================== // exposed functions and vars cC.uform.checkShowPic cC.uform.createInitials cC.uform.checkJobDateStatus cC.uform.fName = document.getElementById('first_name') cC.uform.mName = document.getElementById('middle_name') cC.uform.lName = document.getElementById('last_name') cC.uform.jobLookingDateStatus = false // function definitions, private vars and event handlers // ====================================================== // image fileInput ~~~> display image // ---------------------------------- var fileInput = document.getElementById('pic_file') var showPicImg = document.getElementById('show_pic') var boxShowPicImg = document.getElementById('box_show_pic') var picMsg = document.getElementById('picture_message') var imgReader = new FileReader(); cC.uform.showPic = function(aSrc) { showPicImg.src = aSrc // prepare max size while preserving ratio var realValues = window.getComputedStyle(showPicImg) var imgW = realValues.getPropertyValue("width") var imgH = realValues.getPropertyValue("height") // debug // console.log("img wid", imgW) // console.log("img hei", imgH) if (imgW > imgH) { showPicImg.style.width = "100%" showPicImg.style.height = "auto" } else { showPicImg.style.width = "auto" showPicImg.style.height = "100%" } // now show it boxShowPicImg.style.display = 'block' // possible re-adjust outerbox ? } cC.uform.checkShowPic = function (aForm, doHighlight) { // TEMPORARY initial size already 200 kB, user has to do it himself var max_size = 204800 // TODO max source image size before resizing // see libs or stackoverflow.com/a/24015367 // 4 MB // var max_size = 4194304 // always reset style and width/height calculations boxShowPicImg.style.display = 'none' showPicImg.style.display = "" showPicImg.style.width = "" showPicImg.style.height = "" if (fileInput.files) { var theFile = fileInput.files[0] // debug console.log(theFile.name, "size", theFile.size, theFile.lastModifiedDate) if (theFile.size > max_size) { // msg pb picMsg.innerHTML = "The picture is too big (200kB max)!" picMsg.style.color = cmxClt.colorRed } else { // msg ok picMsg.innerHTML = "Picture ok" picMsg.style.color = cmxClt.colorGreen // to show the pic when readAsDataURL imgReader.onload = function() { cC.uform.showPic(imgReader.result) } // create fake src url & trigger the onload imgReader.readAsDataURL(theFile); } } else { console.warn("skipping testPictureBlob called w/o picture in fileInput") } } // first, middle & last name ~~~> initials // ---------------------------------------- var nameInputs = [cC.uform.fName, cC.uform.mName, cC.uform.lName] var initialsInput = document.getElementById('initials') cC.uform.createInitials = function() { var apparentInitials = "" nameInputs.forEach ( function(nameInput) { var txt = nameInput.value if (txt.length) { if(/[A-Z]/.test(txt)) { var capsArr = txt.match(/[A-Z]/g) for (var i in capsArr) { apparentInitials += capsArr[i] } } else { apparentInitials += txt.charAt(0) } } }) ; // update the displayed value initialsInput.value = apparentInitials } // handlers: names to initials nameInputs.forEach ( function(nameInput) { if (nameInput) { nameInput.onkeyup = cC.uform.createInitials nameInput.onchange = cC.uform.createInitials } }) // handler: show middlename button var mnDiv = document.getElementById('group-midname') if (mnDiv) { var mnLabel = mnDiv.querySelector('label') var mnBtn = document.getElementById('btn-midname') var mnBtnIcon = document.getElementById('btn-midname-icon') if(!mnBtn) { console.warn('group-midname without btn-midname') mnDiv.style.display = 'block' } else { mnBtn.onclick= function() { if (mnDiv.style.display == 'none') { mnDiv.style.display = 'table' mnLabel.style.color="#23A" setTimeout(function(){mnLabel.style.color=""}, 2000) mnBtnIcon.classList.remove("glyphicon-plus") mnBtnIcon.classList.add("glyphicon-arrow-down") mnBtnIcon.style.color="#23A" mnBtnIcon.style.textShadow = cC.strokeBlack } else { mnDiv.style.display = 'none' mnBtnIcon.classList.remove("glyphicon-arrow-down") mnBtnIcon.classList.add("glyphicon-plus") mnBtnIcon.style.color="" mnBtnIcon.style.textShadow = "" } } } } cC.uform.displayMidName = function() { mnDiv.style.display = 'table' mnLabel.style.color="#23A" setTimeout(function(){mnLabel.style.color=""}, 2000) mnBtnIcon.classList.remove("glyphicon-plus") mnBtnIcon.classList.add("glyphicon-arrow-down") mnBtnIcon.style.color="#23A" mnBtnIcon.style.textShadow = cC.strokeBlack } cC.uform.hideMidName = function() { mnDiv.style.display = 'none' mnBtnIcon.classList.remove("glyphicon-arrow-down") mnBtnIcon.classList.add("glyphicon-plus") mnBtnIcon.style.color="" mnBtnIcon.style.textShadow = "" } // jobLookingDateStatus ~~~> is job date a valid date? // --------------------------------------------------- var jobBool = document.getElementById('job_bool') var jobDate = document.getElementById('job_looking_date') var jobDateMsg = document.getElementById('job_date_message') var jobLookingDiv = document.getElementById('job_looking_div') cC.uform.checkJobDateStatus = function () { cC.uform.jobLookingDateStatus = (jobBool.value == "No" || cC.uform.validDate.test(jobDate.value)) if (!cC.uform.jobLookingDateStatus) { jobDateMsg.style.color = "#888" jobDateMsg.innerHTML = '<small>format is YYYY/MM/DD</small>' } else { jobDateMsg.style.color = cmxClt.colorGreen jobDateMsg.innerHTML = '<small>Ok valid date!</small>' } } // handler: show jobLookingDiv if (jobBool && jobDate) { jobBool.onchange = function() { // shows "Until when" if(this.value=='Yes'){ jobLookingDiv.style.display = 'block' } else { jobLookingDiv.style.display='none' jobDate.value='' } } jobDate.onkeyup = cC.uform.checkJobDateStatus jobDate.onchange = cC.uform.checkJobDateStatus } // ========= end of advanced form controls =========== return cC }()) ; console.log("user shared load OK")