/* * OPTIONS DEFINED FROM CONFIGURATION FILE */ var _CONF_FORCE_HTTPS = _CONF_FORCE_HTTPS || false; var _CONF_DISPLAY_EDIT_ICON = _CONF_DISPLAY_EDIT_ICON || "single"; var _CONF_LISTENER_TYPE_VALUE = _CONF_LISTENER_TYPE_VALUE || "click"; var _CONF_LISTENER_TYPE_ICON = _CONF_LISTENER_TYPE_ICON || "none"; var _CONF_LISTENER_TARGET = _CONF_LISTENER_TARGET || "value"; var _CONF_EXCLUDED_FIELD_ID = _CONF_EXCLUDED_FIELD_ID || []; var _CONF_CHECK_ISSUE_UPDATE_CONFLICT = _CONF_CHECK_ISSUE_UPDATE_CONFLICT || false; _CONF_LISTENER_TARGET = _CONF_LISTENER_TARGET === "all" ? "*" : _CONF_LISTENER_TARGET; /* * SVG ICONS * Source : https://www.iconfinder.com/iconsets/glyphs */ const SVG_EDIT = ''; const SVG_VALID = ''; const SVG_CANCEL = ''; /* * Allow inclusion from other page * See https://github.com/Ilogeek/redmine_issue_dynamic_edit/commit/26684a2dd9b12dcc7377afd79e9fe5c142d26ebd for more info */ let LOCATION_HREF = typeof custom_location_href !== 'undefined' ? custom_location_href : window.location.href; if (_CONF_FORCE_HTTPS) { LOCATION_HREF = LOCATION_HREF.replace(/^http:\/\//i, 'https://'); } /* Check if admin want to display all editable fields when hovering the whole details block * or if user has to hover every element to discover if (s)he can edit it */ if (_CONF_DISPLAY_EDIT_ICON === "block"){ document.querySelectorAll('body.controller-issues.action-show .issue.details').forEach((elt) => elt.classList.add('showPencils')); } const updateCSRFToken = function(token){ document.querySelectorAll('input[name="authenticity_token"]').forEach((elt) => elt.value = token); document.querySelector('meta[name="csrf-token"]').setAttribute("content", token); } const setCSRFTokenInput = function(token){ document.querySelectorAll('form[method="post"]').forEach((elt) => { if(!elt.querySelectorAll('input[name="authenticity_token"]').length){ const input = document.createElement("input"); input.setAttribute("type", "hidden"); input.setAttribute("name", "authenticity_token"); input.value = token; elt.insertBefore(input, null); } }); } /* Generate edit block */ const getEditFormHTML = function(attribute){ let formElement = document.querySelector('#issue_' + attribute + "_id"); formElement = formElement ? formElement : document.querySelector('#issue_' + attribute); formElement = formElement ? formElement : document.querySelector('#' + attribute); // Checkbox specific case let is_checkboxes = false; let is_file = false; let is_list = false; let CF_ID = false; if(!formElement && attribute.startsWith("custom_field_values_")){ CF_ID = attribute.split("custom_field_values_")[1]; /* Is it a checkbox block ? */ formElement = document.querySelector('#issue_custom_field_values_' + CF_ID); if(formElement){ formElement = formElement.closest('.check_box_group'); is_checkboxes = CF_ID; } else { /* Is it a file block ? */ formElement = document.querySelector('#issue_custom_field_values_' + CF_ID + '_blank'); if(formElement){ formElement = formElement.closest('p'); formElement.removeChild(formElement.querySelector('label')); is_file = CF_ID; } else { /* Is it a checkbox/radio group ? */ formElement = document.querySelector('#issue-form .cf_' + CF_ID + '.check_box_group'); is_list = CF_ID; } } } if(formElement){ const clone = formElement.cloneNode(true); if(clone.matches('select') && !clone.hasAttribute('multiple')) { clone.addEventListener('change', function(e){ sendData([{"name" : clone.getAttribute('name'), "value" : clone.value}]); }); } if(is_checkboxes || is_file || is_list) { clone.setAttribute('id', "issue_custom_field_values_" + CF_ID + "_dynamic"); } else { clone.setAttribute('id', formElement.getAttribute('id') + "_dynamic"); } const wrapper = document.createElement('div'); wrapper.classList.add('dynamicEditField'); wrapper.insertBefore(clone, null); if(!clone.matches('select') || clone.hasAttribute('multiple')) { let btn_valid = document.createElement('button'); btn_valid.classList.add('action', 'valid'); btn_valid.innerHTML = SVG_VALID; wrapper.insertBefore(btn_valid, null); } const btn_refuse = document.createElement('button'); btn_refuse.classList.add('action', 'refuse'); btn_refuse.innerHTML = SVG_CANCEL; wrapper.insertBefore(btn_refuse, null); return wrapper; } return null; } /* Loop over all form attribute and clone them into details part */ const cloneEditForm = function(){ const btn_refresh = document.createElement('button'); btn_refresh.classList.add('refreshData'); btn_refresh.innerHTML = "⟳"; document.querySelector('.issue.details div.subject').insertBefore(btn_refresh, null); const wrapper = document.createElement('form'); wrapper.setAttribute('id', 'fakeDynamicForm'); document.querySelector('.issue.details').parentNode.insertBefore(wrapper, document.querySelector('.issue.details')); wrapper.appendChild(document.querySelector('.issue.details')); document.querySelectorAll('div.issue.details .attribute').forEach(function(elt){ const classList = elt.classList.value.split(/\s+/); let attributes = classList.filter(function(elem) { return elem != "attribute"; }); // Specific case : all "-" are replaced by "_" into form id attributes = attributes.map((attr) => attr.replaceAll('-', '_')); let custom_field = false; attributes.forEach(function(part, index, arr) { if(arr[index] === "progress") arr[index] = "done_ratio"; if(arr[index].startsWith('cf_')) { arr[index] = arr[index].replace('cf', 'custom_field_values'); custom_field = arr[index]; } }); attributes = attributes.join(" "); let selected_elt = custom_field ? custom_field : attributes; if(attributes && !_CONF_EXCLUDED_FIELD_ID.includes(selected_elt)){ let dynamicEditField = getEditFormHTML(selected_elt); if(dynamicEditField){ let btn_edit = document.createElement('span'); btn_edit.classList.add('iconEdit'); btn_edit.innerHTML = SVG_EDIT; elt.querySelector('.value').insertBefore(btn_edit, null); elt.querySelector('.value').insertBefore(dynamicEditField, null); } } }); // Specific Case : Description field if(!_CONF_EXCLUDED_FIELD_ID.includes("description") && document.querySelectorAll('div.issue.details .description').length){ const btn_edit = document.createElement('span'); btn_edit.classList.add('iconEdit'); btn_edit.innerHTML = SVG_EDIT; document.querySelector('div.issue.details .description > p strong').insertAdjacentElement("afterend", btn_edit); const formDescription = getEditFormHTML("description"); formDescription.querySelector("#issue_description_dynamic").removeAttribute('data-tribute'); document.querySelector('div.issue.details .description').insertBefore(formDescription, null); if ( typeof(CKEDITOR) === "object" && typeof(CKEDITOR.instances['issue_description'] !== "undefined") && typeof(CKEDITOR.instances['issue_description'].getData) === typeof(Function) ) { const cfg = CKEDITOR.instances['issue_description'].config; cfg.height = 100; CKEDITOR.replace("issue_description_dynamic", cfg) }else if (typeof(jsToolBar) === typeof(Function)) { const DynamicDescriptionToolbar = new jsToolBar(document.querySelector('#issue_description_dynamic')); DynamicDescriptionToolbar.setHelpLink('/help/en/wiki_syntax_common_mark.html'); DynamicDescriptionToolbar.setPreviewUrl('/issues/preview?issue_id=' + _ISSUE_ID + '&project_id=' + _PROJECT_ID); DynamicDescriptionToolbar.draw(); } } // Specific Case : Title field if(!_CONF_EXCLUDED_FIELD_ID.includes("subject")){ const btn_edit = document.createElement('span'); btn_edit.classList.add('iconEdit'); btn_edit.innerHTML = SVG_EDIT; document.querySelector('div.issue.details div.subject h3').insertBefore(btn_edit, null); const formTitle = getEditFormHTML("issue_subject"); document.querySelector('div.issue.details div.subject').insertBefore(formTitle, null); } } /* Perform action on .value (display edit form) */ document.querySelector('body').addEventListener(_CONF_LISTENER_TYPE_VALUE, function(e){ let is_attribute = e.target.matches('div.issue.details .attributes .attribute .' + _CONF_LISTENER_TARGET) || e.target.closest('div.issue.details .attributes .attribute .' + _CONF_LISTENER_TARGET); let is_description = e.target.matches('div.issue.details div.description > p') || e.target.closest('div.issue.details div.description > p'); let is_subject = e.target.matches('div.issue.details div.subject') || e.target.closest('div.issue.details div.subject'); if(is_attribute || is_description || is_subject ){ if(e.target.closest('.dynamicEditField')) return; /* We're already into a dynamic field, ignore */ document.querySelectorAll('.dynamicEditField').forEach(function(elt){ elt.classList.remove('open'); }); if(!e.target.closest('a') && !e.target.closest('button')){ let selector = e.target.closest('.value'); if(is_description) selector = e.target.closest('.description'); if(is_subject) selector = e.target.closest('.subject'); selector.querySelector('.dynamicEditField').classList.add('open'); } } }); /* Perform action on .iconEdit (display edit form) */ document.querySelector('body').addEventListener(_CONF_LISTENER_TYPE_ICON, function(e){ let is_attribute = e.target.matches('div.issue.details .attributes .attribute .' + _CONF_LISTENER_TARGET) || e.target.closest('div.issue.details .attributes .attribute .' + _CONF_LISTENER_TARGET); let is_description = e.target.matches('div.issue.details div.description > p') || e.target.closest('div.issue.details div.description > p'); let is_subject = e.target.matches('div.issue.details div.subject') || e.target.closest('div.issue.details div.subject'); if(e.target.matches('.iconEdit') || e.target.closest('.iconEdit')){ document.querySelectorAll('.dynamicEditField').forEach(function(elt){ elt.classList.remove('open'); }); let selector = e.target.closest('.value'); if(is_description) selector = e.target.closest('.description'); if(is_subject) selector = e.target.closest('.subject'); selector.querySelector('.dynamicEditField').classList.add('open'); } }); /* Perform data update when clicking on valid button from edit form */ document.querySelector('body').addEventListener('click', function(e){ if(e.target.matches('.dynamicEditField .action.valid') || e.target.closest('.dynamicEditField .action.valid')){ e.preventDefault(); let inputs = e.target.closest('.dynamicEditField').querySelectorAll('*[name]'); let formData = []; let existingIndex = []; inputs.forEach(elt => { let not_multiple = !elt.matches('input[type="radio"]') && !elt.matches('input[type="checkbox"]'); if(elt.matches('input[type="radio"]:checked') || elt.matches('input[type="checkbox"]:checked') || not_multiple){ if(!existingIndex.includes(elt.getAttribute('name'))){ existingIndex.push(elt.getAttribute('name')); formData.push({"name" : elt.getAttribute('name'), "value" : elt.value}) } } }); sendData(formData); e.target.closest('.dynamicEditField').classList.remove('open'); } }); /* Hide edit form when clicking on cancel button */ document.querySelector('body').addEventListener('click', function(e){ if(e.target.matches('.dynamicEditField .action.refuse') || e.target.closest('.dynamicEditField .action.refuse')){ e.preventDefault(); e.target.closest('.dynamicEditField').classList.remove('open'); } }); /* Update whole .details block + history + form with global refresh button */ document.querySelector('body').addEventListener('click', function(e){ if(e.target.matches('.refreshData') || e.target.closest('.refreshData')){ e.preventDefault(); sendData(); } }); /* Listen on esc key press to close opened dialog box */ document.onkeydown = function(evt) { evt = evt || window.event; let isEscape = false; if ("key" in evt) { isEscape = (evt.key === "Escape" || evt.key === "Esc"); } else { isEscape = (evt.keyCode === 27); } if (isEscape) { document.querySelectorAll('.dynamicEditField').forEach(function(elt){ elt.classList.remove('open'); }); } }; const getVersion = function(callback){ fetch(LOCATION_HREF, { method: 'GET', crossDomain: true, }).then(res => res.text()).then(data => { const parser = new DOMParser(); const doc = parser.parseFromString(data, 'text/html'); const distant_version = doc.querySelector('#issue_lock_version').value; if(callback) callback(distant_version); return distant_version; }).catch(err => { console.warn('Issue while trying to get version (avoiding conflict)'); console.log(err); }); } let loadedDate = new Date(); const checkVersion = function(callback){ fetch(LOCATION_HREF + ".json", { method: 'GET', crossDomain: true, }).then(res => res.text()).then(data => { try { const parsedData = JSON.parse(data); const lastUpdate = new Date(parsedData.issue.updated_on); if(lastUpdate > loadedDate){ loadedDate = lastUpdate; if(!document.querySelectorAll('#content .conflict').length){ let msg = document.createElement('div'); msg.classList.add('conflict'); msg.innerHTML = `${_TXT_CONFLICT_TITLE}
${_TXT_CONFLICT_LINK} ${_TXT_CONFLICT_TXT}