From 1a67ae665003ef16fab68962f5674c98e7a52fc1 Mon Sep 17 00:00:00 2001 From: Sundraiz-Shah Date: Thu, 12 Jun 2025 12:40:24 +0200 Subject: [PATCH 01/26] Console Errors resolved --- templates/base.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/base.html b/templates/base.html index 86b6f0f..cd946ae 100644 --- a/templates/base.html +++ b/templates/base.html @@ -45,17 +45,17 @@ {% block content %}{% endblock %} - + From ab54a24683a14f971dbeb96c7f7ef36755d6d90c Mon Sep 17 00:00:00 2001 From: Sundraiz-Shah Date: Mon, 16 Jun 2025 12:51:52 +0200 Subject: [PATCH 02/26] Spliting Foundation.JS WIP --- static/foundation/js/vendor/download.js | 25 +++ static/foundation/js/vendor/dropdown.js | 24 +++ static/foundation/js/vendor/foundation.js | 206 +++++++++++++--------- static/foundation/js/vendor/init.js | 41 +++++ static/foundation/js/vendor/main.js | 1 + static/foundation/js/vendor/popup.js | 85 +++++++++ static/foundation/js/vendor/schema.js | 75 ++++++++ static/foundation/js/vendor/tableUtils.js | 100 +++++++++++ static/foundation/js/vendor/tagging.js | 169 ++++++++++++++++++ static/foundation/js/vendor/utils.js | 18 ++ 10 files changed, 656 insertions(+), 88 deletions(-) create mode 100644 static/foundation/js/vendor/download.js create mode 100644 static/foundation/js/vendor/dropdown.js create mode 100644 static/foundation/js/vendor/init.js create mode 100644 static/foundation/js/vendor/main.js create mode 100644 static/foundation/js/vendor/popup.js create mode 100644 static/foundation/js/vendor/schema.js create mode 100644 static/foundation/js/vendor/tableUtils.js create mode 100644 static/foundation/js/vendor/tagging.js create mode 100644 static/foundation/js/vendor/utils.js diff --git a/static/foundation/js/vendor/download.js b/static/foundation/js/vendor/download.js new file mode 100644 index 0000000..3a7a733 --- /dev/null +++ b/static/foundation/js/vendor/download.js @@ -0,0 +1,25 @@ +// src/download.js +/** + * Grab the JSON from the textarea and trigger a download as a .json file. + */ +export function downloadFile() { + // 1. Read the metadata JSON text from the textarea + const textarea = document.getElementById("metadata-json"); + if (!textarea) return; + const metadata = textarea.value; + + // 2. Create a Blob and an object URL + const blob = new Blob([metadata], { type: "application/json" }); + const url = URL.createObjectURL(blob); + + // 3. Create a temporary element to trigger the download + const a = document.createElement("a"); + a.href = url; + a.download = "metadata.json"; // default filename + document.body.appendChild(a); + a.click(); + + // 4. Cleanup + document.body.removeChild(a); + URL.revokeObjectURL(url); + } \ No newline at end of file diff --git a/static/foundation/js/vendor/dropdown.js b/static/foundation/js/vendor/dropdown.js new file mode 100644 index 0000000..8fa18fb --- /dev/null +++ b/static/foundation/js/vendor/dropdown.js @@ -0,0 +1,24 @@ +// src/dropdown.js +// Dynamic dropdown population + +import { JsonSchemaUrl } from './schema.js'; + +export class DynamicDropdown { + constructor(dropdownId, schemaProperty) { + this.dropdownId = dropdownId; + this.schemaProperty = schemaProperty; + } + populateDropdown() { + fetch(JsonSchemaUrl) + .then(res=>res.json()) + .then(schema=>{ + const values = schema.properties?.[this.schemaProperty]?.enum||[]; + const dd = document.getElementById(this.dropdownId); + if (!dd) return; + dd.innerHTML = ''; + values.forEach(v=>{ + const opt = document.createElement('option'); opt.value=v; opt.textContent=v; dd.appendChild(opt); + }); + }); + } +} \ No newline at end of file diff --git a/static/foundation/js/vendor/foundation.js b/static/foundation/js/vendor/foundation.js index 651867c..d1f6c4f 100644 --- a/static/foundation/js/vendor/foundation.js +++ b/static/foundation/js/vendor/foundation.js @@ -33,7 +33,7 @@ document.addEventListener("DOMContentLoaded", function () { const recommended = schema.recommended || []; return { required, recommended }; } - + /////////_______schema_________//////// // Helper: Get the field key for an input element function getFieldKey(input) { // Try to get the name attribute, fallback to id @@ -129,6 +129,8 @@ document.addEventListener("DOMContentLoaded", function () { }); } + //table//// + // pop-up message for Contributor and Author tabs function showPopup() { document.getElementById('popup').style.display = 'block'; @@ -153,6 +155,8 @@ document.addEventListener("DOMContentLoaded", function () { } } + // tooltip icon logic// + document.querySelectorAll('.custom-tooltip-metadata').forEach(function (element) { const tooltip = element.querySelector('.tooltip-text-metadata'); const icon = element.querySelector('i'); @@ -179,7 +183,7 @@ document.addEventListener("DOMContentLoaded", function () { }); }); - +//---------------------------------------Tags-------------------------------// // show highlighted tag for keywords // Tagging Logic @@ -365,7 +369,7 @@ document.addEventListener("DOMContentLoaded", function () { } }); - // Add tag on Enter + // Add tag on pressing Enter key input.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); @@ -459,7 +463,9 @@ document.addEventListener("DOMContentLoaded", function () { } }); } +//------------------------------------tags end------------------------------// +//-------------------------------JSON text area updates via a simple text box-------/// // Create a function of a nested single input function setupSingleInputObject({ containerId, @@ -518,6 +524,9 @@ document.addEventListener("DOMContentLoaded", function () { jsonKey: key }); }); + //-----------------end--------------// + + //---------------------------Dropdown --------------------------------------// // Create a general dropdown class class DynamicDropdown { @@ -577,7 +586,9 @@ document.addEventListener("DOMContentLoaded", function () { console.error(`Dropdown with ID "${dropdownId}" is missing required attributes.`); } }); +//-----------------------------------Dropdown ends --------------------------------// +//---------------------------------------contributors table-------------------------------------// // New table function updateTableHiddenInput(key) { // Get all rows of the table @@ -1199,8 +1210,10 @@ document.addEventListener("DOMContentLoaded", function () { input.placeholder = 'Add tag and press Enter'; cell.appendChild(input); } +// ------------------------------Contributors table ends----------------------------//// + +//--------------- --- Autocomplete logic: fetch source and setup --------------// - // --- Autocomplete logic: fetch source and setup --- // You can set data-autocomplete-source on the cell or column header, or fetch from schema const col = cell.getAttribute('data-col'); @@ -1442,6 +1455,7 @@ document.addEventListener("DOMContentLoaded", function () { container: cell }); } + // Hide all tag-inputs when clicking outside document.addEventListener('click', function () { @@ -1449,7 +1463,10 @@ document.addEventListener("DOMContentLoaded", function () { input.style.display = 'none'; }); }); + //---------------------------Auto complete tagging ends-----------------------// + + //------------------------------------------------UI elements------------------------// // copy button for json copyBtn.addEventListener('click', function (event) { event.preventDefault(); @@ -1567,6 +1584,14 @@ document.addEventListener("DOMContentLoaded", function () { }); } } + function toggleCollapse() { + const content = document.getElementById('contributor-explanation'); + if (content) { + content.style.display = (content.style.display === 'none' || content.style.display === '') ? 'block' : 'none'; + } + } + window.toggleCollapse = toggleCollapse; + // Initialize the state on page load window.onload = function () { toggleSection(); @@ -1592,6 +1617,9 @@ document.addEventListener("DOMContentLoaded", function () { if (skipValidationIds.includes(input.id)) { return; // Skip validation for the specified inputs } +//-----------------------------UI elements ends---------------------------------// + +//---------------------------------Schema fecting ---------------------------------------// // Fetch schema and validate only if field is required or recommended fetch(JsonSchema) @@ -1655,7 +1683,7 @@ document.addEventListener("DOMContentLoaded", function () { // Initialize tables on load highlightEmptyAddRowControls(); - + //----------------------------------- JSON UPDATE-----------------------// inputs.forEach(input => validateInput(input)); inputs.forEach((input) => { @@ -1711,6 +1739,86 @@ document.addEventListener("DOMContentLoaded", function () { return !!element.closest('.add-row-controls'); } + function getNestedExpectedKeys(schema, typeName) { + // For JSON Schema Draft-07 and later, use $defs; for older, use definitions + const defs = schema.$defs || schema.definitions || {}; + const typeDef = defs[typeName]; + if (!typeDef || !typeDef.properties) { + return []; + } + // Exclude @type if you want + return Object.keys(typeDef.properties).filter(key => key !== "@type"); + } + + function matchKeys(allowedKeys, requiredKeys, jsonKeys) { + // Ensure "@type" is always allowed + if (!allowedKeys.includes("@type")) { + allowedKeys = allowedKeys.concat("@type"); + } + const lowerAllowedKeys = allowedKeys.map(key => key.toLowerCase()); + const lowerRequiredKeys = requiredKeys.map(key => key.toLowerCase()); + const lowerJsonKeys = jsonKeys.map(key => key.toLowerCase()); + + const missingKeys = lowerRequiredKeys.filter(key => !lowerJsonKeys.includes(key)); + const extraKeys = lowerJsonKeys.filter(key => !lowerAllowedKeys.includes(key)); + + return { missingKeys, extraKeys }; + } + + function keysMatchRecursive(allowedKeys, requiredKeys, jsonObject, schema) { + const jsonKeys = Object.keys(jsonObject); + const { missingKeys, extraKeys } = matchKeys(allowedKeys, requiredKeys, jsonKeys); + + let nestedErrors = []; + + for (const key of jsonKeys) { + const value = jsonObject[key]; + if (Array.isArray(value)) { + value.forEach((item, idx) => { + if (item && typeof item === "object") { + const typeName = item["@type"] || key; + const expectedKeys = getNestedExpectedKeys(schema, typeName); + const requiredNested = []; // Optionally, get required keys for this type from schema + const result = keysMatchRecursive(expectedKeys, requiredNested, item, schema); + if (!result.isMatch) { + nestedErrors.push( + `In ${key}[${idx}] with ${typeName}: Missing Keys: ${result.missingKeys.join(", ")}, Extra Keys: ${result.extraKeys.join(", ")}` + ); + if (result.nestedErrors.length > 0) { + nestedErrors = nestedErrors.concat(result.nestedErrors); + } + } + } + }); + } else if (value && typeof value === "object") { + const typeName = value["@type"] || key; + const expectedKeys = getNestedExpectedKeys(schema, typeName); + const requiredNested = []; + const result = keysMatchRecursive(expectedKeys, requiredNested, value, schema); + if (!result.isMatch) { + nestedErrors.push( + `In ${key}: Missing Keys: ${result.missingKeys.join(", ")}, Extra Keys: ${result.extraKeys.join(", ")}` + ); + if (result.nestedErrors.length > 0) { + nestedErrors = nestedErrors.concat(result.nestedErrors); + } + } + } + } + + return { + isMatch: missingKeys.length === 0 && extraKeys.length === 0 && nestedErrors.length === 0, + missingKeys, + extraKeys, + nestedErrors + }; + } + + +// -------------------------------------------JSON Update ends--------------------------// + +//----------------------bidirection-------------------// + function updateFormFromJson(jsonObject) { inputs.forEach((input) => { @@ -1793,89 +1901,10 @@ document.addEventListener("DOMContentLoaded", function () { }); } - function getNestedExpectedKeys(schema, typeName) { - // For JSON Schema Draft-07 and later, use $defs; for older, use definitions - const defs = schema.$defs || schema.definitions || {}; - const typeDef = defs[typeName]; - if (!typeDef || !typeDef.properties) { - return []; - } - // Exclude @type if you want - return Object.keys(typeDef.properties).filter(key => key !== "@type"); - } - - function matchKeys(allowedKeys, requiredKeys, jsonKeys) { - // Ensure "@type" is always allowed - if (!allowedKeys.includes("@type")) { - allowedKeys = allowedKeys.concat("@type"); - } - const lowerAllowedKeys = allowedKeys.map(key => key.toLowerCase()); - const lowerRequiredKeys = requiredKeys.map(key => key.toLowerCase()); - const lowerJsonKeys = jsonKeys.map(key => key.toLowerCase()); - - const missingKeys = lowerRequiredKeys.filter(key => !lowerJsonKeys.includes(key)); - const extraKeys = lowerJsonKeys.filter(key => !lowerAllowedKeys.includes(key)); - - return { missingKeys, extraKeys }; - } - - function keysMatchRecursive(allowedKeys, requiredKeys, jsonObject, schema) { - const jsonKeys = Object.keys(jsonObject); - const { missingKeys, extraKeys } = matchKeys(allowedKeys, requiredKeys, jsonKeys); - - let nestedErrors = []; - - for (const key of jsonKeys) { - const value = jsonObject[key]; - if (Array.isArray(value)) { - value.forEach((item, idx) => { - if (item && typeof item === "object") { - const typeName = item["@type"] || key; - const expectedKeys = getNestedExpectedKeys(schema, typeName); - const requiredNested = []; // Optionally, get required keys for this type from schema - const result = keysMatchRecursive(expectedKeys, requiredNested, item, schema); - if (!result.isMatch) { - nestedErrors.push( - `In ${key}[${idx}] with ${typeName}: Missing Keys: ${result.missingKeys.join(", ")}, Extra Keys: ${result.extraKeys.join(", ")}` - ); - if (result.nestedErrors.length > 0) { - nestedErrors = nestedErrors.concat(result.nestedErrors); - } - } - } - }); - } else if (value && typeof value === "object") { - const typeName = value["@type"] || key; - const expectedKeys = getNestedExpectedKeys(schema, typeName); - const requiredNested = []; - const result = keysMatchRecursive(expectedKeys, requiredNested, value, schema); - if (!result.isMatch) { - nestedErrors.push( - `In ${key}: Missing Keys: ${result.missingKeys.join(", ")}, Extra Keys: ${result.extraKeys.join(", ")}` - ); - if (result.nestedErrors.length > 0) { - nestedErrors = nestedErrors.concat(result.nestedErrors); - } - } - } - } - - return { - isMatch: missingKeys.length === 0 && extraKeys.length === 0 && nestedErrors.length === 0, - missingKeys, - extraKeys, - nestedErrors - }; - } - - function toggleCollapse() { - const content = document.getElementById('contributor-explanation'); - if (content) { - content.style.display = (content.style.display === 'none' || content.style.display === '') ? 'block' : 'none'; - } - } - window.toggleCollapse = toggleCollapse; + // -------------------bidirection ends--------------------// + +// -------------------------downloadfile-----------------------// function downloadFile(event) { event.preventDefault(); @@ -1991,4 +2020,5 @@ document.addEventListener("DOMContentLoaded", function () { downloadBtn.addEventListener("click", (event) => { downloadFile(event); }); -}); \ No newline at end of file +}); +// ----------------------------downloadfile ends-----------------------// \ No newline at end of file diff --git a/static/foundation/js/vendor/init.js b/static/foundation/js/vendor/init.js new file mode 100644 index 0000000..537afb6 --- /dev/null +++ b/static/foundation/js/vendor/init.js @@ -0,0 +1,41 @@ +// src/init.js +// Main bootstrap + +import { setMandatoryFieldsFromSchema } from './schema.js'; +import { validateMandatoryFields } from './schema.js'; +import { initializeTaggingFields, setupSingleInputObject } from './tagging.js'; +import { initializeTableTaggingCells, updateTableHiddenInput } from './tableUtils.js'; +import { DynamicDropdown } from './dropdown.js'; +import { showPopup, checkAndShowPopup, setupTabs, setupNavButtons, toggleSection } from './popup.js'; +import { downloadFile } from './download.js'; + +// Wire up on DOMContentLoaded +export function init() { + document.addEventListener('DOMContentLoaded', () => { + setMandatoryFieldsFromSchema(); + initializeTaggingFields(); + document.querySelectorAll('.single-input-object-label[data-single-input-object]').forEach(label => { + const key = label.dataset.singleInputObject; + setupSingleInputObject({ + containerId: key+'Object', hiddenInputId:key+'HiddenInput', inputId:key+'Input', jsonKey:key + }); + }); + document.querySelectorAll('table.auto-property-table').forEach(table=>{ + const key=table.id.replace(/Table$/, ''); updateTableHiddenInput(key); + }); + initializeTableTaggingCells(); + document.querySelectorAll('select[data-dropdown-schema]').forEach(dd=>{ + new DynamicDropdown(dd.id, dd.dataset.dropdownSchema).populateDropdown(); + }); + setupTabs(document.querySelectorAll('.tab-links_ext a'), document.querySelectorAll('.tab-content_ext .tab')); + setupNavButtons(); + checkAndShowPopup(window.location.hash, document.title); + toggleSection(); + const ts=document.getElementById('toggleSwitch'); if(ts) ts.addEventListener('change', toggleSection); + document.getElementById('copy-button')?.addEventListener('click', e=>{ e.preventDefault(); document.getElementById('metadata-json').select(); document.execCommand('copy'); }); + document.getElementById('downloadButton')?.addEventListener('click', downloadFile); + document.getElementById('downloadBtn')?.addEventListener('click', downloadFile); + }); +} + +init(); \ No newline at end of file diff --git a/static/foundation/js/vendor/main.js b/static/foundation/js/vendor/main.js new file mode 100644 index 0000000..0450fe0 --- /dev/null +++ b/static/foundation/js/vendor/main.js @@ -0,0 +1 @@ +import './init.js'; \ No newline at end of file diff --git a/static/foundation/js/vendor/popup.js b/static/foundation/js/vendor/popup.js new file mode 100644 index 0000000..5a3a2c0 --- /dev/null +++ b/static/foundation/js/vendor/popup.js @@ -0,0 +1,85 @@ +// src/popup.js +// Popup display, tooltip handlers, and tab navigation + +/** Show the metadata popup dialog. */ +export function showPopup() { + const popup = document.getElementById('popup'); + if (popup) popup.style.display = 'block'; +} + +// Close button handler +const closeBtn = document.getElementById('closePopup'); +if (closeBtn) { + closeBtn.onclick = () => { + const popup = document.getElementById('popup'); + if (popup) popup.style.display = 'none'; + }; +} + +// Click outside popup closes it +window.onclick = event => { + const popup = document.getElementById('popup'); + if (event.target === popup) { + popup.style.display = 'none'; + } +}; + +/** Remember and show popup per-repo and tab */ +export function checkAndShowPopup(tab, repo) { + const key = `popupShown-${repo}-${tab}`; + if (!localStorage.getItem(key)) { + showPopup(); + localStorage.setItem(key, 'true'); + } +} + +// Tooltip hover for .custom-tooltip-metadata +document.querySelectorAll('.custom-tooltip-metadata').forEach(el => { + const tooltip = el.querySelector('.tooltip-text-metadata'); + const icon = el.querySelector('i'); + if (!tooltip || !icon) return; + el.addEventListener('mouseenter', () => { + Object.assign(tooltip.style, { + display: 'block', + visibility: 'visible', + opacity: '1', + position: 'fixed', + zIndex: '9999' + }); + const rect = icon.getBoundingClientRect(); + tooltip.style.left = `${rect.right}px`; + tooltip.style.top = `${rect.top + 16}px`; + }); + el.addEventListener('mouseleave', () => { + tooltip.style.display = ''; + }); +}); + +/** Toggle collapse sections by class */ +export function toggleSection() { + document.querySelectorAll('.collapsible').forEach(section => { + section.classList.toggle('collapsed'); + }); +} + +/** Setup tab navigation */ +export function setupTabs(tabs, contents) { + tabs.forEach(tab => { + tab.addEventListener('click', e => { + e.preventDefault(); + const target = document.querySelector(tab.getAttribute('href')); + contents.forEach(c => c.classList.remove('active')); + tabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + if (target) target.classList.add('active'); + }); + }); +} + +/** Setup next/prev nav buttons if any */ +export function setupNavButtons() { + const prev = document.getElementById('prev-btn'); + const next = document.getElementById('next-btn'); + if (prev) prev.addEventListener('click', () => window.history.back()); + if (next) next.addEventListener('click', () => window.history.forward()); +} diff --git a/static/foundation/js/vendor/schema.js b/static/foundation/js/vendor/schema.js new file mode 100644 index 0000000..888f377 --- /dev/null +++ b/static/foundation/js/vendor/schema.js @@ -0,0 +1,75 @@ +// src/schema.js +// JSON-Schema loading and mandatory field handling + +export const JsonSchemaUrl = '/static/schema/codemeta_schema.json'; + +// /** +// * Fetch required and recommended fields from JSON schema. +// * @returns {Promise<{required: string[], recommended: string[]}>} +// */ +export async function fetchRequiredAndRecommendedFields() { + const response = await fetch(JsonSchemaUrl); + const schema = await response.json(); + return { + required: schema.required || [], + recommended: schema.recommended || [] + }; +} + +/** + * Dynamically mark schema-required fields with 'required' attribute and a red asterisk. + */ +export async function setMandatoryFieldsFromSchema() { + const { required } = await fetchRequiredAndRecommendedFields(); + const inputs = document.querySelectorAll("#metadata-form input, #metadata-form select"); + + required.forEach(fieldKey => { + inputs.forEach(input => { + if (input.name === fieldKey || input.id === fieldKey) { + input.setAttribute('required', ''); + } + }); + + const hiddenInput = document.getElementById(fieldKey + 'HiddenInput'); + if (hiddenInput) { + hiddenInput.setAttribute('required', ''); + } + + let label = document.querySelector(`label[for="${fieldKey}"]`); + if (!label) { + label = document.querySelector(`.tagging-label[for="${fieldKey}Input"]`); + } + if (label && !label.innerHTML.includes('*')) { + const asterisk = document.createElement('span'); + asterisk.style.color = 'red'; + asterisk.style.fontSize = '18px'; + asterisk.textContent = '*'; + label.appendChild(document.createTextNode(' ')); + label.appendChild(asterisk); + } + }); +} + +// /** +// * Validate that all required fields in formData JSON string are populated. +// * @param {string} formData +// * @returns {Promise} +// */ +export function validateMandatoryFields(formData) { + return new Promise((resolve, reject) => { + fetch(JsonSchemaUrl) + .then(res => res.json()) + .then(schema => { + const required = schema.required || []; + let parsed; + try { + parsed = JSON.parse(formData); + } catch { + return reject('Invalid JSON'); + } + const ok = required.every(f => parsed[f] && parsed[f].toString().trim() !== ''); + resolve(ok); + }) + .catch(err => reject(err)); + }); +} \ No newline at end of file diff --git a/static/foundation/js/vendor/tableUtils.js b/static/foundation/js/vendor/tableUtils.js new file mode 100644 index 0000000..4767446 --- /dev/null +++ b/static/foundation/js/vendor/tableUtils.js @@ -0,0 +1,100 @@ +// src/tableUtils.js +// Table row add/remove and hidden-input sync + +import { createSuggestionsBox, setupTagging } from './tagging.js'; +import { JsonSchemaUrl } from './schema.js'; + +/** Extract cell value based on type */ +export function extractCellValue(cell, coltype) { + if (coltype === 'checkbox') + return cell.querySelector('input').checked; + if (coltype === 'dropdown') + return cell.querySelector('select').value; + if (coltype === 'tagging' || coltype === 'tagging_autocomplete') + return cell.querySelector('input').value; + return cell.textContent.trim(); +} + +/** Update hidden input for a table */ +export function updateTableHiddenInput(key) { + const table = document.getElementById(key + 'Table'); + const hiddenInput = document.getElementById(key + 'TableHiddenInput'); + if (!table || !hiddenInput) return; + + const atType = table.dataset.atType; + const headers = Array.from(table.querySelectorAll('thead th')) + .map(th => ({ name: th.dataset.col, coltype: th.dataset.coltype })); + + // Prepare a container for arrays of elements + const data = {}; + headers + .filter(h => h.coltype === 'element') + .forEach(h => { data[h.name] = []; }); + + // Build objects from each row + Array.from(table.querySelectorAll('tbody tr')) + .filter(row => !row.classList.contains('add-row-controls')) + .forEach(row => { + const cells = Array.from(row.cells); + const obj = { '@type': atType }; + headers + .filter(h => h.coltype !== 'element' && h.coltype !== 'delete') + .forEach((h, i) => { + obj[h.name] = extractCellValue(cells[i], h.coltype); + }); + headers + .filter(h => h.coltype === 'element') + .forEach(h => { + data[h.name].push(obj); + }); + }); + + // Merge into metadata JSON + const meta = JSON.parse(document.getElementById('metadata-json').value); + Object.keys(data).forEach(k => meta[k] = data[k]); + document.getElementById('metadata-json').value = JSON.stringify(meta, null, 2); +} + +/** Initialize add-row controls and tagging in tables */ +export function initializeTableTaggingCells() { + // --- Row addition buttons --- + document.querySelectorAll('.add-row-controls').forEach(ctrl => { + const addButton = ctrl.querySelector('button.add-row'); + if (!addButton) return; // ← guard to avoid null + addButton.addEventListener('click', () => { + const tableId = ctrl.dataset.for; + const table = document.getElementById(tableId + 'Table'); + const newRow = table.insertRow(table.rows.length - 1); + + // Clone each input/select from the controls row into the new row + Array.from(ctrl.querySelectorAll('input, select')) + .forEach((inp, i) => { + const cell = newRow.insertCell(i); + cell.appendChild(inp.cloneNode()); + }); + + updateTableHiddenInput(tableId); + }); + }); + + // --- Initialize tagging/autocomplete in table cells --- + document.querySelectorAll('table.auto-property-table td').forEach(cell => { + const coltype = cell.dataset.coltype; + if (coltype === 'tagging_autocomplete') { + fetch(JsonSchemaUrl) + .then(r => r.json()) + .then(schema => { + const key = cell.dataset.col; + const list = schema.properties?.[key]?.items?.enum || []; + setupTagging({ + containerId: cell.id + 'Tags', + hiddenInputId: cell.id + 'HiddenInput', + inputId: cell.id + 'Input', + useAutocomplete: true, + autocompleteSource: list, + jsonKey: key + }); + }); + } + }); +} diff --git a/static/foundation/js/vendor/tagging.js b/static/foundation/js/vendor/tagging.js new file mode 100644 index 0000000..99f6ab5 --- /dev/null +++ b/static/foundation/js/vendor/tagging.js @@ -0,0 +1,169 @@ +// src/tagging.js +// Tagging and autocomplete for multi-value fields and single-object inputs + +import { getFieldKey } from './utils.js'; + +/** Show invalid tag error */ +function showInvalidTagMessage(container, input, message) { + const msg = document.createElement('div'); + msg.className = 'invalid-tag-message'; + msg.textContent = message; + container.appendChild(msg); + setTimeout(() => msg.remove(), 2000); +} + +/** Update suggestions box position */ +function updateSuggestionsBoxPosition(input, box) { + const rect = input.getBoundingClientRect(); + Object.assign(box.style, { + position: 'fixed', + left: `${rect.left}px`, top: `${rect.bottom}px`, + width: `${rect.width}px` + }); +} + +/** Create empty suggestions box */ +export function createSuggestionsBox(container) { + const box = document.createElement('div'); + box.className = 'suggestions-box'; + container.appendChild(box); + return box; +} + +/** Core tagging initializer */ +export function setupTagging({ containerId, hiddenInputId, inputId, suggestionsId = null, jsonKey, useAutocomplete = false, autocompleteSource = [] }) { + const container = document.getElementById(containerId); + const hiddenInput = document.getElementById(hiddenInputId); + const input = document.getElementById(inputId); + const suggestionsBox = suggestionsId ? document.getElementById(suggestionsId) : createSuggestionsBox(container); + const metadataJson = document.getElementById('metadata-json'); + + let selectedTags = []; + const label = document.querySelector(`.tagging-label[for="${inputId}"]`); + const taggingType = label ? label.getAttribute('data-tagging-type') : null; + const constantType = label ? label.getAttribute('data-constant-type') : null; + + if (taggingType === 'tagging_object') { + try { selectedTags = JSON.parse(hiddenInput.value) || []; } catch { selectedTags = []; } + } else { + selectedTags = hiddenInput.value.split(',').map(v => v.trim()).filter(Boolean); + } + + function renderTags() { + container.querySelectorAll('.tag').forEach(t => t.remove()); + selectedTags.forEach(item => { + const tag = document.createElement('span'); + tag.className = 'tag'; + const value = taggingType === 'tagging_object' ? item.identifier : item; + tag.dataset.value = value; + tag.innerHTML = `${value}×`; + container.insertBefore(tag, input); + }); + } + + function updateHidden() { + hiddenInput.value = taggingType === 'tagging_object' + ? JSON.stringify(selectedTags) + : selectedTags.join(', '); + const jsonObj = JSON.parse(metadataJson.value); + jsonObj[jsonKey] = selectedTags; + metadataJson.value = JSON.stringify(jsonObj, null, 2); + } + + function addTag(tagValue) { + if (!tagValue) return; + if (taggingType === 'tagging_object') { + if (!selectedTags.find(i => i.identifier === tagValue)) { + selectedTags.push({ '@type': constantType || 'ScholarlyArticle', identifier: tagValue }); + } + } else { + if (!selectedTags.includes(tagValue)) { + selectedTags.push(tagValue); + } + } + renderTags(); updateHidden(); input.value = ''; + suggestionsBox.style.display = 'none'; + } + + // Initial render + renderTags(); + + if (useAutocomplete) { + input.addEventListener('input', () => { + const query = input.value.trim().toLowerCase(); + suggestionsBox.innerHTML = ''; + if (!query) return (suggestionsBox.style.display = 'none'); + const filtered = autocompleteSource.filter(tag => + tag.toLowerCase().startsWith(query) && !selectedTags.some(i => (taggingType==='tagging_object'?i.identifier:i) === tag) + ); + if (filtered.length === 0) return (suggestionsBox.style.display = 'none'); + filtered.forEach(tag => { + const div = document.createElement('div'); div.className='suggestion-item'; div.textContent = tag; + div.onclick = () => addTag(tag); + suggestionsBox.appendChild(div); + }); + suggestionsBox.style.display = 'block'; + updateSuggestionsBoxPosition(input, suggestionsBox); + }); + } + + // Remove tag + container.addEventListener('click', e => { + if (e.target.classList.contains('remove-tag')) { + selectedTags = selectedTags.filter(i => (taggingType==='tagging_object'?i.identifier:i) !== e.target.dataset.value); + e.target.parentElement.remove(); updateHidden(); + } + }); + + input.addEventListener('keydown', e => { + if (e.key === 'Enter') { + e.preventDefault(); const val = input.value.trim(); + if (useAutocomplete && !autocompleteSource.includes(val)) { + showInvalidTagMessage(container, input, 'Please select a value from the list.'); + } else addTag(val); + } + }); +} + +/** Initialize all tagging fields on page */ +export function initializeTaggingFields() { + document.querySelectorAll('.tagging-label[data-tagging]').forEach(label => { + const key = label.getAttribute('data-tagging'); + const type = label.getAttribute('data-tagging-type'); + const setup = { + containerId: key + 'Tags', + hiddenInputId: key + 'HiddenInput', + inputId: key + 'Input', + suggestionsId: key + 'Suggestions', + jsonKey: key, + useAutocomplete: type === 'tagging_autocomplete', + autocompleteSource: [] // will be fetched in init + }; + setupTagging(setup); + }); +} + +/** Single-object input helper */ +export function setupSingleInputObject({ containerId, hiddenInputId, inputId, jsonKey }) { + const input = document.getElementById(inputId); + const hiddenInput = document.getElementById(hiddenInputId); + const metadataJson = document.getElementById('metadata-json'); + const label = document.querySelector(`.single-input-object-label[for="${inputId}"]`); + const constantType = label ? label.getAttribute('data-single-input-object-type') : null; + + let obj = {}; + try { obj = JSON.parse(hiddenInput.value); } catch {} + if (obj.identifier) input.value = obj.identifier; + + function updateObj() { + const identifier = input.value.trim(); + const newObj = { '@type': constantType || 'ScholarlyArticle', identifier }; + hiddenInput.value = JSON.stringify(newObj); + const jsonObj = JSON.parse(metadataJson.value); + jsonObj[jsonKey] = newObj; + metadataJson.value = JSON.stringify(jsonObj, null, 2); + } + + input.addEventListener('input', updateObj); + input.addEventListener('change', updateObj); +} diff --git a/static/foundation/js/vendor/utils.js b/static/foundation/js/vendor/utils.js new file mode 100644 index 0000000..abc6bc6 --- /dev/null +++ b/static/foundation/js/vendor/utils.js @@ -0,0 +1,18 @@ +// src/utils.js +// Utility functions: key extraction + +// /** +// * Get field key for an input element, stripping array notation and hidden suffix. +// * @param {HTMLInputElement|HTMLSelectElement} input +// * @returns {string} +// */ +export function getFieldKey(input) { + let key = input.name || input.id || ""; + if (key.includes("[")) { + key = key.split("[")[0]; + } + if (key.endsWith("HiddenInput")) { + key = key.replace(/HiddenInput$/, ""); + } + return key; +} From db9a720fcd70890c826c543ea8482bb4dfc71a3b Mon Sep 17 00:00:00 2001 From: Sundraiz-Shah Date: Fri, 20 Jun 2025 22:40:42 +0200 Subject: [PATCH 03/26] tagging , schema, dropdown, table spiliting are done --- static/foundation/js/vendor/download.js | 149 +++- static/foundation/js/vendor/dropdown-utils.js | 61 ++ static/foundation/js/vendor/dropdown.js | 24 - static/foundation/js/vendor/form-utils.js | 91 +++ static/foundation/js/vendor/init.js | 60 +- static/foundation/js/vendor/main.js | 1 - static/foundation/js/vendor/popup.js | 85 -- static/foundation/js/vendor/schema-utils.js | 150 ++++ static/foundation/js/vendor/schema.js | 75 -- static/foundation/js/vendor/table-utils.js | 744 ++++++++++++++++++ static/foundation/js/vendor/tableUtils.js | 100 --- static/foundation/js/vendor/tagging.js | 463 ++++++++--- static/foundation/js/vendor/ui.js | 138 ++++ static/foundation/js/vendor/utils.js | 18 - templates/base.html | 2 +- 15 files changed, 1671 insertions(+), 490 deletions(-) create mode 100644 static/foundation/js/vendor/dropdown-utils.js delete mode 100644 static/foundation/js/vendor/dropdown.js create mode 100644 static/foundation/js/vendor/form-utils.js delete mode 100644 static/foundation/js/vendor/main.js delete mode 100644 static/foundation/js/vendor/popup.js create mode 100644 static/foundation/js/vendor/schema-utils.js delete mode 100644 static/foundation/js/vendor/schema.js create mode 100644 static/foundation/js/vendor/table-utils.js delete mode 100644 static/foundation/js/vendor/tableUtils.js create mode 100644 static/foundation/js/vendor/ui.js delete mode 100644 static/foundation/js/vendor/utils.js diff --git a/static/foundation/js/vendor/download.js b/static/foundation/js/vendor/download.js index 3a7a733..a2f18dc 100644 --- a/static/foundation/js/vendor/download.js +++ b/static/foundation/js/vendor/download.js @@ -1,25 +1,124 @@ -// src/download.js -/** - * Grab the JSON from the textarea and trigger a download as a .json file. - */ -export function downloadFile() { - // 1. Read the metadata JSON text from the textarea - const textarea = document.getElementById("metadata-json"); - if (!textarea) return; - const metadata = textarea.value; - - // 2. Create a Blob and an object URL - const blob = new Blob([metadata], { type: "application/json" }); - const url = URL.createObjectURL(blob); - - // 3. Create a temporary element to trigger the download - const a = document.createElement("a"); - a.href = url; - a.download = "metadata.json"; // default filename - document.body.appendChild(a); - a.click(); - - // 4. Cleanup - document.body.removeChild(a); - URL.revokeObjectURL(url); - } \ No newline at end of file +// download.js + +import { keysMatchRecursive } from './schema-utils.js'; + +const JsonSchema = '/static/schema/codemeta_schema.json'; +const metadataJson = document.getElementById("metadata-json"); +const downloadButton = document.getElementById("downloadButton"); +const downloadBtn = document.getElementById("downloadBtn"); + +// Function to trigger file download from JSON textarea +export function setupDownload() { + downloadButton.addEventListener("click", (event) => { + downloadFile(event); + }); + downloadBtn.addEventListener("click", (event) => { + downloadFile(event); + }); +} + +// Function to handle download with validation +export function downloadFile(event) { + event.preventDefault(); + + try { + const data = metadataJson.value; + const entered_metadata = JSON.parse(data); // Move inside try block + const metadata = getCleanedMetadata(entered_metadata); + const jsonKeys = Object.keys(metadata); // Extract keys from received JSON + + let repoName = "metadata"; // Default name + + fetch(JsonSchema) + .then(response => response.json()) + .then(schema => { + // Extract all property keys + const allowedKeys = Object.keys(schema.properties || {}); + const requiredKeys = schema.required || []; + + // Get key comparison result + const keyCheck = keysMatchRecursive(allowedKeys, requiredKeys, metadata, schema); + + if (!keyCheck.isMatch) { + let errorMessage = ""; + if (keyCheck.missingKeys.length > 0) { + errorMessage += `Not all required elements were filled. Please add content to the following elements:\n\n ${keyCheck.missingKeys.join(", ")}\n`; + } + if (keyCheck.extraKeys.length > 0) { + errorMessage += `There are elements which are not part of the standard. Please remove the following elements:\n\n: ${keyCheck.extraKeys.join(", ")}\n`; + } + if (keyCheck.nestedErrors.length > 0) { + errorMessage += `\nNested Errors:\n${keyCheck.nestedErrors.join("\n")}`; + } + alert(errorMessage); + } else { + jsonPrettier(repoName, metadata); + } + }) + .catch(error => { + console.error('Error loading schema:', error); + }); + } + catch (e) { + let errorMessage = `\n\nCurrent Metadata:\n${JSON.stringify(metadata, null, 2)}`; + alert(errorMessage); + alert("Invalid JSON. Please check your syntax:metadata"); + console.error("JSON Parsing Error:", e); + } +} + +// Provide metadata as download +export function jsonPrettier(repoName, metadata) { + let validJson; + const values = Object.values(metadata).slice(0, 2); + // Check the conditions + if (values[0] !== "https://w3id.org/codemeta/3.0" || values[1] !== "SoftwareSourceCode") { + // Update the first two keys in the object + const keys = Object.keys(metadata); + if (keys.length >= 2) { + metadata[keys[0]] = "https://w3id.org/codemeta/3.0"; // Update the first key's value + metadata[keys[1]] = "SoftwareSourceCode"; // Update the second key's value + } + } + + if (metadata.name) { + repoName = metadata.name; + validJson = JSON.stringify(metadata, null, 2); + } + const fileName = `${repoName}/codemeta.json`; + const blob = new Blob([validJson], { type: "application/json" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.innerHTML = "Download JSON"; + link.setAttribute("download", fileName); + document.body.appendChild(link); + link.click(); + setTimeout(() => { + URL.revokeObjectURL(link.href); + link.parentNode.removeChild(link); + }, 100); +} + +// Function to create a cleaned copy of an object by removing empty entries +export function getCleanedMetadata(obj) { + const cleanedObj = Array.isArray(obj) ? [] : {}; + Object.keys(obj).forEach(key => { + if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) { + // Recursively clean nested objects + const cleanedNested = getCleanedMetadata(obj[key]); + if (Object.keys(cleanedNested).length > 0) { + cleanedObj[key] = cleanedNested; + } + } else if (Array.isArray(obj[key])) { + // Remove empty elements from arrays + const cleanedArray = obj[key].filter(item => item !== null && item !== undefined && item !== ''); + if (cleanedArray.length > 0) { + cleanedObj[key] = cleanedArray; + } + } else if (obj[key] !== null && obj[key] !== undefined && obj[key] !== '') { + // Copy non-empty values + cleanedObj[key] = obj[key]; + } + }); + return cleanedObj; +} diff --git a/static/foundation/js/vendor/dropdown-utils.js b/static/foundation/js/vendor/dropdown-utils.js new file mode 100644 index 0000000..5560654 --- /dev/null +++ b/static/foundation/js/vendor/dropdown-utils.js @@ -0,0 +1,61 @@ +// dropdown-utils.js +import { getSchema } from "./schema-utils.js"; +const JsonSchema = '/static/schema/codemeta_schema.json'; +const dropdownElements = document.querySelectorAll('select[data-dropdown-schema]'); + +// Create a general dropdown class +class DynamicDropdown { + constructor(dropdownId, jsonSchemaUrl, schemaProperty) { + this.dropdownId = dropdownId; // The ID of the dropdown element + this.jsonSchemaUrl = jsonSchemaUrl; // The URL of the JSON schema + this.schemaProperty = schemaProperty; // The property in the schema to use for options + } + + populateDropdown() { + getSchema().then(schema => { + const enumValues = schema.properties?.[this.schemaProperty]?.enum || []; + const dropdown = document.getElementById(this.dropdownId); + + if (!dropdown) { + console.error(`Dropdown with ID "${this.dropdownId}" not found.`); + return; + } + + // Clear existing options + dropdown.innerHTML = ''; + + // Add default "Select" option + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.textContent = 'Select an option'; + dropdown.appendChild(defaultOption); + + // Populate dropdown with enum values + enumValues.forEach(value => { + const option = document.createElement('option'); + option.value = value; + option.textContent = value; + dropdown.appendChild(option); + }); + }) + .catch(error => { + console.error(`Failed to load schema or populate dropdown for "${this.dropdownId}":`, error); + }); + } +} + +// Automatically initialize all dropdowns with the data-dropdown-schema attribute +export function initializeDynamicDropdowns() { + dropdownElements.forEach(dropdown => { + const schemaProperty = dropdown.getAttribute('data-dropdown-schema'); + const dropdownId = dropdown.id; + + if (schemaProperty && dropdownId) { + // Create an instance of DynamicDropdown for each dropdown + const dynamicDropdown = new DynamicDropdown(dropdownId, JsonSchema, schemaProperty); + dynamicDropdown.populateDropdown(); + } else { + console.error(`Dropdown with ID "${dropdownId}" is missing required attributes.`); + } + }); +} diff --git a/static/foundation/js/vendor/dropdown.js b/static/foundation/js/vendor/dropdown.js deleted file mode 100644 index 8fa18fb..0000000 --- a/static/foundation/js/vendor/dropdown.js +++ /dev/null @@ -1,24 +0,0 @@ -// src/dropdown.js -// Dynamic dropdown population - -import { JsonSchemaUrl } from './schema.js'; - -export class DynamicDropdown { - constructor(dropdownId, schemaProperty) { - this.dropdownId = dropdownId; - this.schemaProperty = schemaProperty; - } - populateDropdown() { - fetch(JsonSchemaUrl) - .then(res=>res.json()) - .then(schema=>{ - const values = schema.properties?.[this.schemaProperty]?.enum||[]; - const dd = document.getElementById(this.dropdownId); - if (!dd) return; - dd.innerHTML = ''; - values.forEach(v=>{ - const opt = document.createElement('option'); opt.value=v; opt.textContent=v; dd.appendChild(opt); - }); - }); - } -} \ No newline at end of file diff --git a/static/foundation/js/vendor/form-utils.js b/static/foundation/js/vendor/form-utils.js new file mode 100644 index 0000000..045a48a --- /dev/null +++ b/static/foundation/js/vendor/form-utils.js @@ -0,0 +1,91 @@ +// form-utils.js + +import { fetchRequiredAndRecommendedFields , getSchema} from './schema-utils.js'; + +const metadataJson = document.getElementById("metadata-json"); +const inputs = document.querySelectorAll("#metadata-form input, #metadata-form select"); + +export function setupForm() { + inputs.forEach(input => validateInput(input)); + + inputs.forEach(input => { + input.addEventListener("input", () => handleInputChange(input)); + input.addEventListener("change", () => handleInputChange(input)); + }); +} + +function handleInputChange(input) { + validateInput(input); + + const jsonObject = JSON.parse(metadataJson.value); + const key = input.name.split("[")[0]; + const subkey = input.name.split("[")[1]?.split("]")[0]; + + if (!isInTable(input) && !isInAddRowControls(input)) { + if (subkey) { + if (!jsonObject[key]) jsonObject[key] = {}; + jsonObject[key][subkey] = input.value; + } else { + jsonObject[key] = input.value; + } + metadataJson.value = JSON.stringify(jsonObject, null, 2); + } +} + +export function getFieldKey(input) { + let key = input.name || input.id || ""; + if (key.includes("[")) { + key = key.split("[")[0]; + } + if (key.endsWith("HiddenInput")) { + key = key.replace(/HiddenInput$/, ""); + } + return key; +} + +export function validateInput(input) { + const skipValidationIds = [ + 'contributorGivenNameInput', 'contributorFamilyNameInput', 'contributorEmailInput', + 'authorGivenNameInput', 'authorFamilyNameInput', 'authorEmailInput' + ]; + if (skipValidationIds.includes(input.id)) return; + getSchema().then(schema => { + const { required, recommended } = fetchRequiredAndRecommendedFields(schema); + const allMandatory = [...required, ...recommended]; + + const key = getFieldKey(input); + + if (allMandatory.includes(key)) { + if (input.value.trim() === "") { + input.classList.add("invalid"); + } else { + input.classList.remove("invalid"); + } + } else { + input.classList.remove("invalid"); + } + }) + .catch(() => input.classList.remove("invalid")); +} + +function isInTable(element) { + return !!element.closest('table'); +} + +function isInAddRowControls(element) { + return !!element.closest('.add-row-controls'); +} + +export function updateFormFromJson(jsonObject) { + inputs.forEach(input => { + const key = input.name.split("[")[0]; + const subkey = input.name.split("[")[1]?.split("]")[0]; + + if (subkey) { + input.value = jsonObject[key]?.[subkey] || ""; + } else { + input.value = jsonObject[key] || ""; + } + validateInput(input); + }); +} diff --git a/static/foundation/js/vendor/init.js b/static/foundation/js/vendor/init.js index 537afb6..e820d44 100644 --- a/static/foundation/js/vendor/init.js +++ b/static/foundation/js/vendor/init.js @@ -1,41 +1,21 @@ -// src/init.js -// Main bootstrap +// init.js -import { setMandatoryFieldsFromSchema } from './schema.js'; -import { validateMandatoryFields } from './schema.js'; -import { initializeTaggingFields, setupSingleInputObject } from './tagging.js'; -import { initializeTableTaggingCells, updateTableHiddenInput } from './tableUtils.js'; -import { DynamicDropdown } from './dropdown.js'; -import { showPopup, checkAndShowPopup, setupTabs, setupNavButtons, toggleSection } from './popup.js'; -import { downloadFile } from './download.js'; - -// Wire up on DOMContentLoaded -export function init() { - document.addEventListener('DOMContentLoaded', () => { - setMandatoryFieldsFromSchema(); - initializeTaggingFields(); - document.querySelectorAll('.single-input-object-label[data-single-input-object]').forEach(label => { - const key = label.dataset.singleInputObject; - setupSingleInputObject({ - containerId: key+'Object', hiddenInputId:key+'HiddenInput', inputId:key+'Input', jsonKey:key - }); - }); - document.querySelectorAll('table.auto-property-table').forEach(table=>{ - const key=table.id.replace(/Table$/, ''); updateTableHiddenInput(key); - }); - initializeTableTaggingCells(); - document.querySelectorAll('select[data-dropdown-schema]').forEach(dd=>{ - new DynamicDropdown(dd.id, dd.dataset.dropdownSchema).populateDropdown(); - }); - setupTabs(document.querySelectorAll('.tab-links_ext a'), document.querySelectorAll('.tab-content_ext .tab')); - setupNavButtons(); - checkAndShowPopup(window.location.hash, document.title); - toggleSection(); - const ts=document.getElementById('toggleSwitch'); if(ts) ts.addEventListener('change', toggleSection); - document.getElementById('copy-button')?.addEventListener('click', e=>{ e.preventDefault(); document.getElementById('metadata-json').select(); document.execCommand('copy'); }); - document.getElementById('downloadButton')?.addEventListener('click', downloadFile); - document.getElementById('downloadBtn')?.addEventListener('click', downloadFile); - }); -} - -init(); \ No newline at end of file +import { setupForm } from './form-utils.js'; +import { initializeTaggingFields} from './tagging.js'; +import { setupTables, highlightEmptyAddRowControls} from './table-utils.js'; +import { setupDownload } from './download.js'; +import { setupUI } from './ui.js'; +import{ initializeDynamicDropdowns } from './dropdown-utils.js'; +import {setMandatoryFieldsFromSchema} from './schema-utils.js'; +// Entry point: called when DOM is fully loaded +document.addEventListener('DOMContentLoaded', () => { + highlightEmptyAddRowControls(); + setupForm(); + initializeTaggingFields(); + setupTables(); + setupDownload(); + setupUI(); + initializeDynamicDropdowns(); + setMandatoryFieldsFromSchema(); + +}); diff --git a/static/foundation/js/vendor/main.js b/static/foundation/js/vendor/main.js deleted file mode 100644 index 0450fe0..0000000 --- a/static/foundation/js/vendor/main.js +++ /dev/null @@ -1 +0,0 @@ -import './init.js'; \ No newline at end of file diff --git a/static/foundation/js/vendor/popup.js b/static/foundation/js/vendor/popup.js deleted file mode 100644 index 5a3a2c0..0000000 --- a/static/foundation/js/vendor/popup.js +++ /dev/null @@ -1,85 +0,0 @@ -// src/popup.js -// Popup display, tooltip handlers, and tab navigation - -/** Show the metadata popup dialog. */ -export function showPopup() { - const popup = document.getElementById('popup'); - if (popup) popup.style.display = 'block'; -} - -// Close button handler -const closeBtn = document.getElementById('closePopup'); -if (closeBtn) { - closeBtn.onclick = () => { - const popup = document.getElementById('popup'); - if (popup) popup.style.display = 'none'; - }; -} - -// Click outside popup closes it -window.onclick = event => { - const popup = document.getElementById('popup'); - if (event.target === popup) { - popup.style.display = 'none'; - } -}; - -/** Remember and show popup per-repo and tab */ -export function checkAndShowPopup(tab, repo) { - const key = `popupShown-${repo}-${tab}`; - if (!localStorage.getItem(key)) { - showPopup(); - localStorage.setItem(key, 'true'); - } -} - -// Tooltip hover for .custom-tooltip-metadata -document.querySelectorAll('.custom-tooltip-metadata').forEach(el => { - const tooltip = el.querySelector('.tooltip-text-metadata'); - const icon = el.querySelector('i'); - if (!tooltip || !icon) return; - el.addEventListener('mouseenter', () => { - Object.assign(tooltip.style, { - display: 'block', - visibility: 'visible', - opacity: '1', - position: 'fixed', - zIndex: '9999' - }); - const rect = icon.getBoundingClientRect(); - tooltip.style.left = `${rect.right}px`; - tooltip.style.top = `${rect.top + 16}px`; - }); - el.addEventListener('mouseleave', () => { - tooltip.style.display = ''; - }); -}); - -/** Toggle collapse sections by class */ -export function toggleSection() { - document.querySelectorAll('.collapsible').forEach(section => { - section.classList.toggle('collapsed'); - }); -} - -/** Setup tab navigation */ -export function setupTabs(tabs, contents) { - tabs.forEach(tab => { - tab.addEventListener('click', e => { - e.preventDefault(); - const target = document.querySelector(tab.getAttribute('href')); - contents.forEach(c => c.classList.remove('active')); - tabs.forEach(t => t.classList.remove('active')); - tab.classList.add('active'); - if (target) target.classList.add('active'); - }); - }); -} - -/** Setup next/prev nav buttons if any */ -export function setupNavButtons() { - const prev = document.getElementById('prev-btn'); - const next = document.getElementById('next-btn'); - if (prev) prev.addEventListener('click', () => window.history.back()); - if (next) next.addEventListener('click', () => window.history.forward()); -} diff --git a/static/foundation/js/vendor/schema-utils.js b/static/foundation/js/vendor/schema-utils.js new file mode 100644 index 0000000..3d65640 --- /dev/null +++ b/static/foundation/js/vendor/schema-utils.js @@ -0,0 +1,150 @@ +// schema-utils.js +const JsonSchema = '/static/schema/codemeta_schema.json'; +const inputs = document.querySelectorAll("#metadata-form input, #metadata-form select"); +let schemaCache = null; +let schemaPromise = null; + +export function getSchema() { +if (schemaCache) return Promise.resolve(schemaCache); +if (schemaPromise) return schemaPromise; + +schemaPromise = fetch(JsonSchema) + .then(response => response.json()) + .then(schema => { + schemaCache = schema; + return schemaCache; + }); + +return schemaPromise; +} + +// Helper: Fetch required and recommended fields from schema +export function fetchRequiredAndRecommendedFields(schema) { + // Required fields: standard JSON Schema + const required = schema.required || []; + // Recommended fields: codemeta uses "recommended" (array) or similar + const recommended = schema.recommended || []; + return { required, recommended }; +} + +// Helper to get nested property keys for a specific type +export function getNestedExpectedKeys(schema, typeName) { + // For JSON Schema Draft-07 and later, use $defs; for older, use definitions + const defs = schema.$defs || schema.definitions || {}; + const typeDef = defs[typeName]; + if (!typeDef || !typeDef.properties) { + return []; + } + // Exclude @type if you want + return Object.keys(typeDef.properties).filter(key => key !== "@type"); +} + +// Compare allowed/required JSON keys with actual JSON object keys +export function matchKeys(allowedKeys, requiredKeys, jsonKeys) { + // Ensure "@type" is always allowed + if (!allowedKeys.includes("@type")) { + allowedKeys = allowedKeys.concat("@type"); + } + const lowerAllowedKeys = allowedKeys.map(key => key.toLowerCase()); + const lowerRequiredKeys = requiredKeys.map(key => key.toLowerCase()); + const lowerJsonKeys = jsonKeys.map(key => key.toLowerCase()); + + const missingKeys = lowerRequiredKeys.filter(key => !lowerJsonKeys.includes(key)); + const extraKeys = lowerJsonKeys.filter(key => !lowerAllowedKeys.includes(key)); + + return { missingKeys, extraKeys }; +} + +// Recursive key comparison including nested objects and arrays +export function keysMatchRecursive(allowedKeys, requiredKeys, jsonObject, schema) { + const jsonKeys = Object.keys(jsonObject); + const { missingKeys, extraKeys } = matchKeys(allowedKeys, requiredKeys, jsonKeys); + + let nestedErrors = []; + + for (const key of jsonKeys) { + const value = jsonObject[key]; + if (Array.isArray(value)) { + value.forEach((item, idx) => { + if (item && typeof item === "object") { + const typeName = item["@type"] || key; + const expectedKeys = getNestedExpectedKeys(schema, typeName); + const requiredNested = []; // Optionally, get required keys for this type from schema + const result = keysMatchRecursive(expectedKeys, requiredNested, item, schema); + if (!result.isMatch) { + nestedErrors.push( + `In ${key}[${idx}] with ${typeName}: Missing Keys: ${result.missingKeys.join(", ")}, Extra Keys: ${result.extraKeys.join(", ")}` + ); + if (result.nestedErrors.length > 0) { + nestedErrors = nestedErrors.concat(result.nestedErrors); + } + } + } + }); + } else if (value && typeof value === "object") { + const typeName = value["@type"] || key; + const expectedKeys = getNestedExpectedKeys(schema, typeName); + const requiredNested = []; + const result = keysMatchRecursive(expectedKeys, requiredNested, value, schema); + if (!result.isMatch) { + nestedErrors.push( + `In ${key}: Missing Keys: ${result.missingKeys.join(", ")}, Extra Keys: ${result.extraKeys.join(", ")}` + ); + if (result.nestedErrors.length > 0) { + nestedErrors = nestedErrors.concat(result.nestedErrors); + } + } + } + } + + return { + isMatch: missingKeys.length === 0 && extraKeys.length === 0 && nestedErrors.length === 0, + missingKeys, + extraKeys, + nestedErrors + }; +} + +// Function to dynamically mark mandatory fields based on required key in JSON schema +export function setMandatoryFieldsFromSchema() { + getSchema().then(schema => { + const { required, recommended } = fetchRequiredAndRecommendedFields(schema); + + required.forEach(function (fieldKey) { + // Find all inputs where the name matches the required field + + inputs.forEach(function (input) { + // 1. Standard input/select fields + const standardInputs = document.querySelectorAll(`[name="${fieldKey}"]`); + standardInputs.forEach(function (input) { + input.setAttribute('required', true); + }); + + // 2. Tagging fields (hidden input for tagging/tagging_autocomplete) + const hiddenInput = document.getElementById(fieldKey + 'HiddenInput'); + if (hiddenInput) { + hiddenInput.setAttribute('required', true); + } + + // 3. Add asterisk to the correct label + // Try standard label first + let label = document.querySelector(`label[for="${fieldKey}"]`); + // If not found, try tagging label + if (!label) { + label = document.querySelector(`.tagging-label[for="${fieldKey}Input"]`); + } + if (label && !label.innerHTML.includes('*')) { + const asterisk = document.createElement('span'); + asterisk.style.color = 'red'; + asterisk.style.fontSize = '18px'; + asterisk.textContent = '*'; + label.appendChild(document.createTextNode(' ')); + label.appendChild(asterisk); + } + }); + }); + }) + .catch(error => { + console.error('Error loading the JSON schema:', error); + }); +} \ No newline at end of file diff --git a/static/foundation/js/vendor/schema.js b/static/foundation/js/vendor/schema.js deleted file mode 100644 index 888f377..0000000 --- a/static/foundation/js/vendor/schema.js +++ /dev/null @@ -1,75 +0,0 @@ -// src/schema.js -// JSON-Schema loading and mandatory field handling - -export const JsonSchemaUrl = '/static/schema/codemeta_schema.json'; - -// /** -// * Fetch required and recommended fields from JSON schema. -// * @returns {Promise<{required: string[], recommended: string[]}>} -// */ -export async function fetchRequiredAndRecommendedFields() { - const response = await fetch(JsonSchemaUrl); - const schema = await response.json(); - return { - required: schema.required || [], - recommended: schema.recommended || [] - }; -} - -/** - * Dynamically mark schema-required fields with 'required' attribute and a red asterisk. - */ -export async function setMandatoryFieldsFromSchema() { - const { required } = await fetchRequiredAndRecommendedFields(); - const inputs = document.querySelectorAll("#metadata-form input, #metadata-form select"); - - required.forEach(fieldKey => { - inputs.forEach(input => { - if (input.name === fieldKey || input.id === fieldKey) { - input.setAttribute('required', ''); - } - }); - - const hiddenInput = document.getElementById(fieldKey + 'HiddenInput'); - if (hiddenInput) { - hiddenInput.setAttribute('required', ''); - } - - let label = document.querySelector(`label[for="${fieldKey}"]`); - if (!label) { - label = document.querySelector(`.tagging-label[for="${fieldKey}Input"]`); - } - if (label && !label.innerHTML.includes('*')) { - const asterisk = document.createElement('span'); - asterisk.style.color = 'red'; - asterisk.style.fontSize = '18px'; - asterisk.textContent = '*'; - label.appendChild(document.createTextNode(' ')); - label.appendChild(asterisk); - } - }); -} - -// /** -// * Validate that all required fields in formData JSON string are populated. -// * @param {string} formData -// * @returns {Promise} -// */ -export function validateMandatoryFields(formData) { - return new Promise((resolve, reject) => { - fetch(JsonSchemaUrl) - .then(res => res.json()) - .then(schema => { - const required = schema.required || []; - let parsed; - try { - parsed = JSON.parse(formData); - } catch { - return reject('Invalid JSON'); - } - const ok = required.every(f => parsed[f] && parsed[f].toString().trim() !== ''); - resolve(ok); - }) - .catch(err => reject(err)); - }); -} \ No newline at end of file diff --git a/static/foundation/js/vendor/table-utils.js b/static/foundation/js/vendor/table-utils.js new file mode 100644 index 0000000..d14aa49 --- /dev/null +++ b/static/foundation/js/vendor/table-utils.js @@ -0,0 +1,744 @@ +// table-utils.js +import {fetchRequiredAndRecommendedFields, getSchema} from './schema-utils.js'; +import {updateSuggestionsBoxPosition, setupTableTagAutocomplete} from './tagging.js'; + +const JsonSchema = '/static/schema/codemeta_schema.json'; +const metadataJson = document.getElementById("metadata-json"); + +// Function to extract cell value based on column type +export function extractCellValue(cell, coltype) { + if (!cell) return ''; + if (coltype === 'dropdown') { + if (cell.hasAttribute('data-value')) { + return cell.getAttribute('data-value'); + } else if (cell.querySelector('select')) { + return cell.querySelector('select').value; + } else { + return cell.textContent.trim(); + } + } else if (coltype === 'tagging' || coltype === 'tagging_autocomplete') { + // Extract all tag values from data-tag or data-value attribute + return Array.from(cell.querySelectorAll('.tag')).map(tagEl => { + let val = tagEl.getAttribute('data-tag') || tagEl.getAttribute('data-value'); + if (val) return val; + // Remove the trailing " ×" or "×" from textContent + return tagEl.textContent.replace(/\s*×$/, '').trim(); + }); + } else { + return cell.textContent.trim(); + } +} + + // New table +export function updateTableHiddenInput(key) { + // Get all rows of the table + const table = document.querySelector(`#${key}Table`); + const hiddenInput = document.getElementById(`${key}TableHiddenInput`); + if (!table || !hiddenInput) return; + + const atType = table.getAttribute('data-at-type'); + const rows = Array.from(table.querySelectorAll('tbody tr')) + .filter(row => !row.classList.contains('add-row-controls')); // <-- skip add-row-controls + + // Check if this table is marked as unique + if (table.getAttribute('unique-tab') === 'True') { + // Get all headers and their data-col and data-coltype + const headerCells = Array.from(table.querySelectorAll('thead th')); + const headers = headerCells.map(th => ({ + name: th.getAttribute('data-col'), + coltype: th.getAttribute('data-coltype') + })); + + // elements: all headers with data-coltype == 'element' + const elements = headers + .filter(h => h.coltype === 'element') + .map(h => h.name); + + // subElements: all headers not 'delete' or 'element' + const subElements = headers + .filter(h => h.coltype !== 'delete' && h.coltype !== 'element') + .map(h => h.name); + + // Find the table body + const tbody = table.querySelector('tbody'); + const rows = tbody ? Array.from(tbody.querySelectorAll('tr')).filter(row => !row.classList.contains('add-row-controls')) : []; + const existingJson = JSON.parse(metadataJson.value); + + // Build elementList + const elementList = {}; + elements.forEach(element => { + elementList[element] = []; + }); + + rows.forEach(row => { + const cells = Array.from(row.cells); + // Build the element object from subElements + let elementObj = { "@type": atType }; + subElements.forEach((field) => { + const headerIdx = headers.findIndex(h => h.name === field && h.coltype !== 'element' && h.coltype !== 'delete'); + if (headerIdx >= 0 && cells[headerIdx]) { + const coltype = headers[headerIdx].coltype; + elementObj[field] = extractCellValue(cells[headerIdx], coltype); + } + }); + + // For each element, check if the checkbox is checked + elements.forEach(element => { + // Find the header index for this element + const headerIdx = headers.findIndex(h => h.name === element); + if (headerIdx >= 0 && cells[headerIdx]) { + const checkbox = cells[headerIdx].querySelector('.checkbox-element'); + if (checkbox && checkbox.checked) { + elementList[element].push({ ...elementObj }); + } + } + }); + }); + + // Update JSON + Object.keys(elementList).forEach(element => { + existingJson[element] = elementList[element]; + }); + metadataJson.value = JSON.stringify(existingJson, null, 2); + + return; + } + + if (rows.length === 0) { + hiddenInput.value = '[]'; + // Also update the main JSON + const jsonObject = JSON.parse(metadataJson.value); + jsonObject[key] = []; + metadataJson.value = JSON.stringify(jsonObject, null, 2); + return; + } + + // Get column headers (excluding the last "Delete" column) + const headers = Array.from(table.querySelectorAll('thead th')) + .map(th => th.getAttribute('data-col')) + .slice(0, -1); + + // Build array of objects + const data = rows.map(row => { + const cells = Array.from(row.querySelectorAll('td')); + let obj = {}; + if (atType) obj['@type'] = atType; + headers.forEach((header, i) => { + if (!header) return; // Skip if header is empty or undefined + const cell = cells[i]; + if (!cell) { + obj[header] = ''; + return; + } + const coltype = cell.getAttribute('data-coltype'); + obj[header] = extractCellValue(cell, coltype); + }); + return obj; + }); + + hiddenInput.value = JSON.stringify(data); + + // Also update the main JSON + const jsonObject = JSON.parse(metadataJson.value); + jsonObject[key] = data; + metadataJson.value = JSON.stringify(jsonObject, null, 2); +} + +// Set up event listeners on all auto-property-tables +export function setupTables() { + // Add function to color add items when element is required or recommended and empty + document.querySelectorAll('table.auto-property-table').forEach(function (table) { + table.addEventListener('click', function (e) { + // Delete rows + if (e.target.classList.contains('delete-row-btn')) { + const row = e.target.closest('tr'); + if (row) { + row.remove(); + // Update the hidden input + const tableId = table.id; + if (tableId && tableId.endsWith('Table')) { + const key = tableId.replace(/Table$/, ''); + updateTableHiddenInput(key); + } + } + } + + // Update other fields + // Only allow editing on that is not the last column (delete icon) + const cell = e.target.closest('td'); + if (!cell) return; + if (cell.classList.contains('table-tagging-cell')) return; + if (cell.classList.contains('table-tagging-cell')) return; + const row = cell.parentElement; + const allCells = Array.from(row.children); + // Don't edit the last cell (delete icon) + if (allCells.indexOf(cell) === allCells.length - 1) return; + // Prevent multiple inputs + if (cell.querySelector('input')) return; + + const oldValue = cell.textContent; + cell.innerHTML = ''; + const input = document.createElement('input'); + input.type = 'text'; + input.value = oldValue; + input.style.width = '100%'; + input.style.boxSizing = 'border-box'; + cell.appendChild(input); + input.focus(); + + // Save on blur or Enter + function save() { + cell.textContent = input.value; + // Update the hidden input for this table + const tableId = table.id; + if (tableId && tableId.endsWith('Table')) { + const key = tableId.replace(/Table$/, ''); + updateTableHiddenInput(key); + } + } + input.addEventListener('blur', save); + input.addEventListener('keydown', function (evt) { + if (evt.key === 'Enter') { + input.blur(); + } else if (evt.key === 'Escape') { + cell.textContent = oldValue; + } + }); + + }); + }); + // Add Row functionality for all auto-property-tables + document.querySelectorAll('.add-row-btn').forEach(function (btn) { + btn.addEventListener('click', function () { + const key = btn.getAttribute('data-table-key'); + const table = document.getElementById(key + 'Table'); + const hiddenInput = document.getElementById(key + 'TableHiddenInput'); + if (!table || !hiddenInput) return; + + // Find the add-row-controls row + const addRowControls = table.querySelector('tr.add-row-controls[data-table-key="' + key + '"]'); + if (!addRowControls) return; + + // Get input values + const inputs = addRowControls.querySelectorAll('.add-row-input, .add-row-tag-input, .add-row-dropdown-select'); + console.log({ inputs }) + const values = Array.from(inputs).map(input => input.value.trim()); + + // Prevent adding if all fields are empty + const allEmpty = values.every(val => val === ''); + if (allEmpty) return; + + // Create new row + const newRow = document.createElement('tr'); + // Get column headers + const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.getAttribute('data-col')); + // Only add data columns, skip the last header ("Delete") + for (let i = 0; i < headers.length - 1; i++) { + const header = headers[i]; + // Find the input for this column + const input = Array.from(inputs).find(inp => + inp.getAttribute('data-col') === header && !inp.classList.contains('invalid') + ); + const th = table.querySelector(`thead th[data-col="${header}"]`); + const colType = th ? th.getAttribute('data-coltype') : (input ? input.getAttribute('data-coltype') : null); + const col = input ? input.getAttribute('data-col') : null; + const dataType = table.getAttribute('data-at-type'); + const td = document.createElement('td'); + console.log({header, input, col, colType, dataType}) + if (colType === 'element') { + // Find the checkbox in the add-row-controls row + console.log('Looking for checkbox with data-role:', col); + const checkboxInput = addRowControls.querySelector(`input[type="checkbox"][data-role="${header}"]`); + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.classList.add('checkbox-element'); + checkbox.setAttribute('data-role', col); + checkbox.name = `checkbox-${col}`; + console.log('Try to check for checkbox') + console.log({ checkboxInput }) + // Set checked state based on add-row-controls checkbox + if (checkboxInput && checkboxInput.checked) { + checkbox.checked = true; + console.log('Copyied checked state') + } + td.setAttribute('data-col', col); + td.setAttribute('data-coltype', 'element'); + td.setAttribute('data-type', dataType); + td.appendChild(checkbox); + } else if (colType === 'dropdown') { + console.log("Got to dropdown") + td.className = 'table-tagging-cell'; + td.setAttribute('data-col', col); + td.setAttribute('data-coltype', 'dropdown'); + td.setAttribute('data-type', dataType); + + // Show the selected value as plain text + const value = input ? input.value : ''; + console.log('Selected value:', input.value); + td.textContent = value; + } else if (colType === 'tagging' || colType === 'tagging_autocomplete') { + td.className = 'table-tagging-cell'; + td.setAttribute('data-col', col); + td.setAttribute('data-coltype', 'tagging'); + td.setAttribute('data-type', dataType); + // Tag UI + const tagsList = document.createElement('div'); + tagsList.className = 'tags-list'; + (addRowTags[col] || []).forEach(tag => { + const span = document.createElement('span'); + span.className = 'tag'; + span.setAttribute('data-tag', tag); + span.innerHTML = tag + ' ×'; + tagsList.appendChild(span); + }); + const input = document.createElement('input'); + input.className = 'tag-input'; + input.type = 'text'; + input.style.display = 'none'; + input.placeholder = 'Add tag and press Enter'; + td.appendChild(tagsList); + td.appendChild(input); + // Reset tags for next row + addRowTags[col] = []; + // Remove tag elements from add-row-controls + const addRowContainer = document.querySelector('.add-row-tags-container[data-col="' + col + '"]'); + if (addRowContainer) { + addRowContainer.querySelectorAll('.tag').forEach(tagEl => tagEl.remove()); + } + // If tagging_autocomplete, initialize autocomplete for this cell + if (colType === 'tagging_autocomplete') { + getSchema().then(schema => { + const autocompleteSource = schema["$defs"]?.[dataType]?.properties?.[col]?.items?.enum || []; + if (autocompleteSource.length > 0) { + setupTableTagAutocomplete({ cell: td, autocompleteSource }); + } + }); + } + } else { + td.textContent = input ? input.value : ''; + } + newRow.appendChild(td); + } + const deleteTd = document.createElement('td'); + deleteTd.innerHTML = ''; + newRow.appendChild(deleteTd); + + // Insert new row above add-row-controls + addRowControls.parentNode.insertBefore(newRow, addRowControls); + + initializeTableTaggingCells(); + + // Clear input fields + inputs.forEach(input => { + if (input.tagName === 'SELECT') { + input.selectedIndex = 0; + } else { + input.value = ''; + } + }); + + // Update hidden input + updateTableHiddenInput(key); + + // Remove color + addRowControls.classList.remove('invalid'); + }); + }); + + // Store tags for each tagging column before row is added + const addRowTags = {}; + + // Initialize tagging for add-row-controls + document.querySelectorAll('.add-row-tags-container').forEach(container => { + const col = container.getAttribute('data-col'); + addRowTags[col] = []; + const input = container.querySelector('.add-row-tag-input'); + const colType = container.getAttribute('data-coltype'); + const dataType = container.getAttribute('data-type'); + // --- Autocomplete setup --- + let autocompleteSource = []; + let suggestionsBox = createSuggestionsBox(container); + + if (colType === 'tagging_autocomplete') { + getSchema().then(schema => { + autocompleteSource = schema["$defs"]?.[dataType]?.properties?.[col]?.items?.enum || []; + }); + + input.addEventListener('input', function () { + const query = input.value.trim().toLowerCase(); + suggestionsBox.innerHTML = ''; + if (!query || autocompleteSource.length === 0) { + suggestionsBox.style.display = 'none'; + return; + } + const selectedTags = addRowTags[col]; + const filtered = autocompleteSource.filter( + tag => tag.toLowerCase().startsWith(query) && !selectedTags.includes(tag) + ); + if (filtered.length === 0) { + suggestionsBox.style.display = 'none'; + return; + } + filtered.forEach(tag => { + const div = document.createElement('div'); + div.className = 'suggestion-item'; + div.textContent = tag; + div.style.cursor = 'pointer'; + div.onclick = function () { + input.value = tag; + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + suggestionsBox.style.display = 'none'; + }; + suggestionsBox.appendChild(div); + }); + // Position suggestions below the input + const inputRect = input.getBoundingClientRect(); + suggestionsBox.style.left = inputRect.left + 'px'; + suggestionsBox.style.top = inputRect.bottom + 'px'; + suggestionsBox.style.width = input.offsetWidth + 'px'; + suggestionsBox.style.display = 'block'; + }); + + input.addEventListener('focus', function () { + suggestionsBox.innerHTML = ''; + if (!autocompleteSource.length) { + suggestionsBox.style.display = 'none'; + return; + } + const query = input.value.trim().toLowerCase(); + const selectedTags = addRowTags[col]; + const filtered = autocompleteSource.filter( + tag => !selectedTags.includes(tag) && (query === "" || tag.toLowerCase().startsWith(query)) + ); + if (filtered.length === 0) { + suggestionsBox.style.display = 'none'; + return; + } + filtered.forEach(tag => { + const div = document.createElement('div'); + div.className = 'suggestion-item'; + div.textContent = tag; + div.style.cursor = 'pointer'; + div.onclick = function () { + input.value = tag; + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + suggestionsBox.style.display = 'none'; + }; + suggestionsBox.appendChild(div); + }); + // Position suggestions below the input + updateSuggestionsBoxPosition(input, suggestionsBox); + suggestionsBox.style.display = 'block'; + }); + + window.addEventListener('scroll', () => updateSuggestionsBoxPosition(input, suggestionsBox), true); + window.addEventListener('resize', () => updateSuggestionsBoxPosition(input, suggestionsBox)); + + // Hide suggestions on blur/click outside + input.addEventListener('blur', function () { + setTimeout(() => { suggestionsBox.style.display = 'none'; }, 200); + }); + } else if (colType === 'dropdown') { + getSchema().then(schema => { + const options = schema["$defs"]?.[dataType]?.properties?.[col]?.enum || []; + const select = document.createElement('select'); + select.className = 'add-row-dropdown-select'; + select.name = 'selectElement'; + select.setAttribute('data-col', col); + select.setAttribute('data-type', dataType); + select.setAttribute('data-coltype', 'dropdown'); + select.innerHTML = '' + + options.map(opt => ``).join(''); + // Replace the input with the select + input.style.display = 'none'; + container.appendChild(select); + + // On change, update addRowTags or values as needed + select.addEventListener('change', function () { + addRowTags[col] = [select.value]; + console.log('Selected value:', select.value); + }); + }); + } + + // Add tag on Enter + input.addEventListener('keydown', function (e) { + if (e.key === 'Enter' && input.value.trim() !== '') { + e.preventDefault(); + const tag = input.value.trim(); + if (colType === 'tagging_autocomplete') { + if (autocompleteSource.includes(tag)) { + if (!addRowTags[col].includes(tag)) { + addRowTags[col].push(tag); + + // Create tag element + const span = document.createElement('span'); + span.className = 'tag'; + span.setAttribute('data-tag', tag); + span.innerHTML = tag + ' ×'; + container.insertBefore(span, input); + } + input.value = ''; + if (suggestionsBox) suggestionsBox.style.display = 'none'; + } else { + showInvalidTagMessage(container, input, "Please select a value from the list."); + input.classList.add("invalid"); + setTimeout(() => input.classList.remove("invalid"), 1000); + input.value = ''; + } + } else { + // For plain tagging, just add the tag + if (!addRowTags[col].includes(tag)) { + addRowTags[col].push(tag); + const span = document.createElement('span'); + span.className = 'tag'; + span.setAttribute('data-tag', tag); + span.innerHTML = tag + ' ×'; + container.insertBefore(span, input); + } + input.value = ''; + } + } + }); + + // Remove tag on click + container.addEventListener('click', function (e) { + if (e.target.classList.contains('remove-tag')) { + const tag = e.target.getAttribute('data-tag'); + addRowTags[col] = addRowTags[col].filter(t => t !== tag); + e.target.parentElement.remove(); + } + }); + }); + + initializeTableTaggingCells(); + + // Hide all tag-inputs when clicking outside + document.addEventListener('click', function () { + document.querySelectorAll('td.table-tagging-cell .tag-input').forEach(function (input) { + input.style.display = 'none'; + }); + }); +} + // Add function to color add items when element is required or recommended and empty + export function highlightEmptyAddRowControls() { + getSchema().then(schema => { + const { required, recommended } = fetchRequiredAndRecommendedFields(schema); + const allMandatory = [...required, ...recommended]; + + document.querySelectorAll('table.auto-property-table').forEach(table => { + const tableId = table.id; + if (!tableId || !tableId.endsWith('Table')) return; + const key = tableId.replace(/Table$/, ''); + + // Find the corresponding add-row-controls + const addRowControls = document.querySelector(`.add-row-controls[data-table-key="${key}"]`); + if (!addRowControls) return; + + if (allMandatory.includes(key)) { + const tbody = table.querySelector('tbody'); + const rows = tbody ? tbody.querySelectorAll('tr') : []; + if (rows.length === 0) { + addRowControls.classList.add('invalid'); + } else { + addRowControls.classList.remove('invalid'); + } + } else { + addRowControls.classList.remove('invalid'); + } + }); + }); +} + +export function createSuggestionsBox() { + let suggestionsBox = document.querySelector('.tag-suggestions-global'); + if (!suggestionsBox) { + suggestionsBox = document.createElement('div'); + suggestionsBox.className = 'tag-suggestions tag-suggestions-global'; + suggestionsBox.style.position = 'absolute'; + suggestionsBox.style.background = '#fff'; + suggestionsBox.style.border = '1px solid #ccc'; + suggestionsBox.style.zIndex = 10000; + suggestionsBox.style.display = 'none'; + document.body.appendChild(suggestionsBox); + } + return suggestionsBox; +} + +function showInvalidTagMessage(container, input, message) { + // Remove any existing invalid message + const existing = container.querySelector('.invalid-tag-message'); + if (existing) existing.remove(); + + const msg = document.createElement("span"); + msg.classList.add("highlight-tag", "invalid-tag-message"); + msg.innerHTML = `❌ ${message} Got it!`; + container.insertBefore(msg, input); + + // Remove on click or after a timeout + msg.querySelector('.acknowledge-tag').onclick = () => msg.remove(); + setTimeout(() => { if (msg.parentNode) msg.remove(); }, 2500); +} + // Enable tag editing in table cells + export function initializeTableTaggingCells() { + document.querySelectorAll('td.table-tagging-cell').forEach(function (cell) { + // Prevent double-binding + if (cell.dataset.taggingInitialized) return; + cell.dataset.taggingInitialized = "true"; + + const tagsList = cell.querySelector('.tags-list'); + let input = cell.querySelector('.tag-input'); + if (!input) { + input = document.createElement('input'); + input.className = 'tag-input'; + input.type = 'text'; + input.style.display = 'none'; + input.placeholder = 'Add tag and press Enter'; + cell.appendChild(input); + } +// ------------------------------Contributors table ends----------------------------//// + +//--------------- --- Autocomplete logic: fetch source and setup --------------// + + // You can set data-autocomplete-source on the cell or column header, or fetch from schema + + const col = cell.getAttribute('data-col'); + const colType = cell.getAttribute('data-coltype'); + const dataType = cell.getAttribute('data-type'); + // Example: fetch from schema if available + if (colType == 'tagging_autocomplete') { + getSchema().then(schema => { + autocompleteSource = schema["$defs"]?.[dataType]?.properties?.[col]?.items?.enum || []; + if (autocompleteSource.length > 0) { + setupTableTagAutocomplete({ cell, autocompleteSource }); + } + }); + } else if (colType === 'dropdown') { + // Show value as plain text initially + const currentValue = cell.getAttribute('data-value') || cell.textContent.trim() || ''; + cell.innerHTML = ''; + cell.textContent = currentValue; + + // Only show dropdown on cell click + cell.addEventListener('click', function handleDropdownCellClick(e) { + // Prevent multiple dropdowns + if (cell.querySelector('select')) return; + getSchema().then(schema => { + const options = schema["$defs"]?.[dataType]?.properties?.[col]?.enum || []; + const select = document.createElement('select'); + select.className = 'table-dropdown-select'; + select.name = 'ChangingSelect' + select.innerHTML = '' + + options.map(opt => ``).join(''); + select.value = currentValue; + + // Replace cell content with select + cell.innerHTML = ''; + cell.appendChild(select); + select.focus(); + + // On change or blur, update cell and data + function finalizeSelection() { + const selectedValue = select.value; + cell.setAttribute('data-value', selectedValue); + cell.innerHTML = selectedValue; + + // Remove this event listener to avoid duplicate dropdowns + cell.removeEventListener('click', handleDropdownCellClick); + + // Re-attach the click event for future edits + setTimeout(() => { + cell.addEventListener('click', handleDropdownCellClick); + }, 0); + + // Update the hidden input and main JSON + const table = cell.closest('table'); + if (table && table.id.endsWith('Table')) { + const key = table.id.replace(/Table$/, ''); + updateTableHiddenInput(key); + } + } + + select.addEventListener('change', finalizeSelection); + select.addEventListener('blur', finalizeSelection); + }); + + // Remove this event listener to prevent re-entry until finished + cell.removeEventListener('click', handleDropdownCellClick); + }); + + return; // Skip further tag logic for dropdowns + } + + + // Show input when cell is clicked (not on tag or remove) + cell.addEventListener('click', function (e) { + if (e.target.classList.contains('remove-tag') || e.target.classList.contains('tag')) return; + input.style.display = 'inline-block'; + input.focus(); + e.stopPropagation(); + }); + + // Hide input when focus is lost + input.addEventListener('blur', function () { + setTimeout(function () { input.style.display = 'none'; }, 100); + }); + + // Add tag on Enter + input.addEventListener('keydown', function (e) { + if (e.key === 'Enter' && input.value.trim() !== '') { + e.preventDefault(); + e.stopPropagation(); + const tag = input.value.trim(); + if ([...tagsList.querySelectorAll('.tag')].some(t => t.textContent.trim() === tag + '×')) { + input.value = ''; + return; + } + // Get autocompleteSource for this cell/column + let autocompleteSource = []; + const col = cell.getAttribute('data-col'); + const dataType = cell.getAttribute('data-type'); + const colType = cell.getAttribute('data-coltype'); + if (colType === 'tagging_autocomplete') { + // You may want to cache this for performance + getSchema().then(schema => { + autocompleteSource = schema["$defs"]?.[dataType]?.properties?.[col]?.items?.enum || []; + if (!autocompleteSource.includes(tag)) { + showInvalidTagMessage(cell, input, "Please select a value from the list."); + input.classList.add("invalid"); + setTimeout(() => input.classList.remove("invalid"), 1000); + input.value = ''; + return; + } + }); + } + const span = document.createElement('span'); + span.className = 'tag'; + span.setAttribute('data-tag', tag); + span.innerHTML = tag + ' ×'; + tagsList.appendChild(span); + input.value = ''; + const table = cell.closest('table'); + if (table && table.id.endsWith('Table')) { + const key = table.id.replace(/Table$/, ''); + updateTableHiddenInput(key); + } + } + }); + + // Remove tag on click (cell-local) + tagsList.addEventListener('click', function (e) { + if (e.target.classList.contains('remove-tag')) { + e.target.parentElement.remove(); + input.style.display = 'inline-block'; + input.focus(); + const table = cell.closest('table'); + if (table && table.id.endsWith('Table')) { + const key = table.id.replace(/Table$/, ''); + updateTableHiddenInput(key); + } + e.stopPropagation(); + } + }); + }); +} \ No newline at end of file diff --git a/static/foundation/js/vendor/tableUtils.js b/static/foundation/js/vendor/tableUtils.js deleted file mode 100644 index 4767446..0000000 --- a/static/foundation/js/vendor/tableUtils.js +++ /dev/null @@ -1,100 +0,0 @@ -// src/tableUtils.js -// Table row add/remove and hidden-input sync - -import { createSuggestionsBox, setupTagging } from './tagging.js'; -import { JsonSchemaUrl } from './schema.js'; - -/** Extract cell value based on type */ -export function extractCellValue(cell, coltype) { - if (coltype === 'checkbox') - return cell.querySelector('input').checked; - if (coltype === 'dropdown') - return cell.querySelector('select').value; - if (coltype === 'tagging' || coltype === 'tagging_autocomplete') - return cell.querySelector('input').value; - return cell.textContent.trim(); -} - -/** Update hidden input for a table */ -export function updateTableHiddenInput(key) { - const table = document.getElementById(key + 'Table'); - const hiddenInput = document.getElementById(key + 'TableHiddenInput'); - if (!table || !hiddenInput) return; - - const atType = table.dataset.atType; - const headers = Array.from(table.querySelectorAll('thead th')) - .map(th => ({ name: th.dataset.col, coltype: th.dataset.coltype })); - - // Prepare a container for arrays of elements - const data = {}; - headers - .filter(h => h.coltype === 'element') - .forEach(h => { data[h.name] = []; }); - - // Build objects from each row - Array.from(table.querySelectorAll('tbody tr')) - .filter(row => !row.classList.contains('add-row-controls')) - .forEach(row => { - const cells = Array.from(row.cells); - const obj = { '@type': atType }; - headers - .filter(h => h.coltype !== 'element' && h.coltype !== 'delete') - .forEach((h, i) => { - obj[h.name] = extractCellValue(cells[i], h.coltype); - }); - headers - .filter(h => h.coltype === 'element') - .forEach(h => { - data[h.name].push(obj); - }); - }); - - // Merge into metadata JSON - const meta = JSON.parse(document.getElementById('metadata-json').value); - Object.keys(data).forEach(k => meta[k] = data[k]); - document.getElementById('metadata-json').value = JSON.stringify(meta, null, 2); -} - -/** Initialize add-row controls and tagging in tables */ -export function initializeTableTaggingCells() { - // --- Row addition buttons --- - document.querySelectorAll('.add-row-controls').forEach(ctrl => { - const addButton = ctrl.querySelector('button.add-row'); - if (!addButton) return; // ← guard to avoid null - addButton.addEventListener('click', () => { - const tableId = ctrl.dataset.for; - const table = document.getElementById(tableId + 'Table'); - const newRow = table.insertRow(table.rows.length - 1); - - // Clone each input/select from the controls row into the new row - Array.from(ctrl.querySelectorAll('input, select')) - .forEach((inp, i) => { - const cell = newRow.insertCell(i); - cell.appendChild(inp.cloneNode()); - }); - - updateTableHiddenInput(tableId); - }); - }); - - // --- Initialize tagging/autocomplete in table cells --- - document.querySelectorAll('table.auto-property-table td').forEach(cell => { - const coltype = cell.dataset.coltype; - if (coltype === 'tagging_autocomplete') { - fetch(JsonSchemaUrl) - .then(r => r.json()) - .then(schema => { - const key = cell.dataset.col; - const list = schema.properties?.[key]?.items?.enum || []; - setupTagging({ - containerId: cell.id + 'Tags', - hiddenInputId: cell.id + 'HiddenInput', - inputId: cell.id + 'Input', - useAutocomplete: true, - autocompleteSource: list, - jsonKey: key - }); - }); - } - }); -} diff --git a/static/foundation/js/vendor/tagging.js b/static/foundation/js/vendor/tagging.js index 99f6ab5..69eba08 100644 --- a/static/foundation/js/vendor/tagging.js +++ b/static/foundation/js/vendor/tagging.js @@ -1,169 +1,390 @@ -// src/tagging.js -// Tagging and autocomplete for multi-value fields and single-object inputs - -import { getFieldKey } from './utils.js'; - -/** Show invalid tag error */ -function showInvalidTagMessage(container, input, message) { - const msg = document.createElement('div'); - msg.className = 'invalid-tag-message'; - msg.textContent = message; - container.appendChild(msg); - setTimeout(() => msg.remove(), 2000); -} - -/** Update suggestions box position */ -function updateSuggestionsBoxPosition(input, box) { - const rect = input.getBoundingClientRect(); - Object.assign(box.style, { - position: 'fixed', - left: `${rect.left}px`, top: `${rect.bottom}px`, - width: `${rect.width}px` - }); -} +// tagging.js +import { getSchema } from "./schema-utils.js"; +const SPDX_URL = 'https://raw.githubusercontent.com/spdx/license-list-data/master/json/licenses.json'; +const metadataJson = document.getElementById("metadata-json"); -/** Create empty suggestions box */ -export function createSuggestionsBox(container) { - const box = document.createElement('div'); - box.className = 'suggestions-box'; - container.appendChild(box); - return box; -} +// show highlighted tag for keywords -/** Core tagging initializer */ -export function setupTagging({ containerId, hiddenInputId, inputId, suggestionsId = null, jsonKey, useAutocomplete = false, autocompleteSource = [] }) { +// Tagging Logic +export function setupTagging({ + containerId, + hiddenInputId, + inputId, + suggestionsId = null, + jsonKey, + useAutocomplete = false, + autocompleteSource = [] +}) { const container = document.getElementById(containerId); const hiddenInput = document.getElementById(hiddenInputId); const input = document.getElementById(inputId); - const suggestionsBox = suggestionsId ? document.getElementById(suggestionsId) : createSuggestionsBox(container); - const metadataJson = document.getElementById('metadata-json'); + const suggestionsBox = suggestionsId ? document.getElementById(suggestionsId) : null; - let selectedTags = []; + // Detect if this is an object-tagging field (e.g., referencePublication) const label = document.querySelector(`.tagging-label[for="${inputId}"]`); const taggingType = label ? label.getAttribute('data-tagging-type') : null; + // For tagging_object, get the constant type from data attribute (set in template) + const objectKey = label ? label.getAttribute('data-tagging-object-key') : null; const constantType = label ? label.getAttribute('data-constant-type') : null; - if (taggingType === 'tagging_object') { - try { selectedTags = JSON.parse(hiddenInput.value) || []; } catch { selectedTags = []; } + let selectedTags = []; + + // Parse initial value + if (taggingType === "tagging_object") { + try { + const parsed = JSON.parse(hiddenInput.value); + if (Array.isArray(parsed)) selectedTags = parsed; + } catch { + selectedTags = []; + } } else { - selectedTags = hiddenInput.value.split(',').map(v => v.trim()).filter(Boolean); + // tagging (array of strings) + selectedTags = hiddenInput.value + .split(",") + .map(v => v.trim()) + .filter(Boolean); } + // Render tags function renderTags() { - container.querySelectorAll('.tag').forEach(t => t.remove()); - selectedTags.forEach(item => { - const tag = document.createElement('span'); - tag.className = 'tag'; - const value = taggingType === 'tagging_object' ? item.identifier : item; - tag.dataset.value = value; - tag.innerHTML = `${value}×`; - container.insertBefore(tag, input); - }); - } - - function updateHidden() { - hiddenInput.value = taggingType === 'tagging_object' - ? JSON.stringify(selectedTags) - : selectedTags.join(', '); - const jsonObj = JSON.parse(metadataJson.value); - jsonObj[jsonKey] = selectedTags; - metadataJson.value = JSON.stringify(jsonObj, null, 2); + container.querySelectorAll('.tag').forEach(tag => tag.remove()); + if (taggingType === "tagging_object") { + selectedTags.forEach(item => { + const identifier = item.identifier || ''; + const type = item['@type'] || constantType || ''; + const tag = document.createElement("span"); + tag.classList.add("tag"); + tag.setAttribute("data-value", identifier); + tag.innerHTML = `${identifier}×`; + container.insertBefore(tag, input); + }); + } else { + selectedTags.forEach(item => { + const tag = document.createElement("span"); + tag.classList.add("tag"); + tag.setAttribute("data-value", item); + tag.innerHTML = `${item}×`; + container.insertBefore(tag, input); + }); + } } + // Add tag logic function addTag(tagValue) { if (!tagValue) return; - if (taggingType === 'tagging_object') { - if (!selectedTags.find(i => i.identifier === tagValue)) { - selectedTags.push({ '@type': constantType || 'ScholarlyArticle', identifier: tagValue }); - } + if (taggingType === "tagging_object") { + if (selectedTags.some(item => item.identifier === tagValue)) return; + selectedTags.push({ "@type": constantType || "ScholarlyArticle", "identifier": tagValue }); } else { - if (!selectedTags.includes(tagValue)) { - selectedTags.push(tagValue); - } + if (selectedTags.includes(tagValue)) return; + selectedTags.push(tagValue); } - renderTags(); updateHidden(); input.value = ''; - suggestionsBox.style.display = 'none'; + renderTags(); + updateHidden(); + input.value = ""; + if (suggestionsBox) suggestionsBox.style.display = "none"; + input.classList.remove("invalid"); // Remove invalid color immediately } - // Initial render - renderTags(); + // Show yellow tag once if any tag exists + if (selectedTags.length > 0) { + const highlightTag = document.createElement("span"); + highlightTag.classList.add("highlight-tag"); + highlightTag.innerHTML = `⚠️ Suggestion: Curate here Got it!`; + container.insertBefore(highlightTag, input); + } + + if (useAutocomplete && suggestionsBox) { + input.addEventListener("input", () => { + const query = input.value.trim().toLowerCase(); + suggestionsBox.innerHTML = ""; + + if (!query) return (suggestionsBox.style.display = "none"); + + const filtered = autocompleteSource.filter( + tag => tag.toLowerCase().startsWith(query) && + !(objectKey + ? selectedTags.some(item => item[objectKey] === tag) + : selectedTags.includes(tag)) + ); + + if (filtered.length === 0) { + suggestionsBox.style.display = "none"; + return; + } + + filtered.forEach(tag => { + const div = document.createElement("div"); + div.classList.add("suggestion-item"); + div.textContent = tag; + div.onclick = () => addTag(tag); + suggestionsBox.appendChild(div); + }); + + // --- Position the suggestion box using getBoundingClientRect --- + updateSuggestionsBoxPosition(input, suggestionsBox) + suggestionsBox.style.display = "block"; + }); + window.addEventListener('scroll', () => updateSuggestionsBoxPosition(input, suggestionsBox), true); + window.addEventListener('resize', () => updateSuggestionsBoxPosition(input, suggestionsBox)); - if (useAutocomplete) { - input.addEventListener('input', () => { + input.addEventListener("focus", () => { + // Show all suggestions if input is empty, or filtered if not const query = input.value.trim().toLowerCase(); - suggestionsBox.innerHTML = ''; - if (!query) return (suggestionsBox.style.display = 'none'); - const filtered = autocompleteSource.filter(tag => - tag.toLowerCase().startsWith(query) && !selectedTags.some(i => (taggingType==='tagging_object'?i.identifier:i) === tag) + suggestionsBox.innerHTML = ""; + + // Filter as in your input event + const filtered = autocompleteSource.filter( + tag => !( + objectKey + ? selectedTags.some(item => item[objectKey] === tag) + : selectedTags.includes(tag) + ) && (query === "" || tag.toLowerCase().startsWith(query)) ); - if (filtered.length === 0) return (suggestionsBox.style.display = 'none'); + + if (filtered.length === 0) { + suggestionsBox.style.display = "none"; + return; + } + filtered.forEach(tag => { - const div = document.createElement('div'); div.className='suggestion-item'; div.textContent = tag; + const div = document.createElement("div"); + div.classList.add("suggestion-item"); + div.textContent = tag; div.onclick = () => addTag(tag); suggestionsBox.appendChild(div); }); - suggestionsBox.style.display = 'block'; - updateSuggestionsBoxPosition(input, suggestionsBox); + + // Position the suggestion box + const rect = input.getBoundingClientRect(); + suggestionsBox.style.position = "fixed"; + suggestionsBox.style.left = rect.left + "px"; + suggestionsBox.style.top = rect.bottom + "px"; + suggestionsBox.style.width = rect.width + "px"; + suggestionsBox.style.display = "block"; + }); + + document.addEventListener("click", (e) => { + if (!suggestionsBox.contains(e.target) && e.target !== input) { + suggestionsBox.style.display = "none"; + } }); } - // Remove tag - container.addEventListener('click', e => { - if (e.target.classList.contains('remove-tag')) { - selectedTags = selectedTags.filter(i => (taggingType==='tagging_object'?i.identifier:i) !== e.target.dataset.value); - e.target.parentElement.remove(); updateHidden(); + // Remove tag logic + container.addEventListener("click", (e) => { + if (e.target.classList.contains("remove-tag")) { + const value = e.target.dataset.value; + if (taggingType === "tagging_object") { + selectedTags = selectedTags.filter(item => item.identifier !== value); + } else { + selectedTags = selectedTags.filter(tag => tag !== value); + } + e.target.parentElement.remove(); + updateHidden(); + } + if (e.target.classList.contains("acknowledge-tag")) { + e.target.parentElement.remove(); } }); - input.addEventListener('keydown', e => { - if (e.key === 'Enter') { - e.preventDefault(); const val = input.value.trim(); - if (useAutocomplete && !autocompleteSource.includes(val)) { - showInvalidTagMessage(container, input, 'Please select a value from the list.'); - } else addTag(val); + // Add tag on pressing Enter key + input.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + const newTag = input.value.trim(); + if (useAutocomplete) { + if (autocompleteSource.includes(newTag)) { + addTag(newTag); + } else { + showInvalidTagMessage(container, input, "Please select a value from the list."); + input.classList.add("invalid"); + setTimeout(() => input.classList.remove("invalid"), 1000); + input.value = ""; + } + } else if (newTag) { + addTag(newTag); + } } }); + + // Update hidden input and JSON + function updateHidden() { + if (taggingType === "tagging_object") { + hiddenInput.value = JSON.stringify(selectedTags); + } else { + hiddenInput.value = selectedTags.join(", "); + } + const jsonObject = JSON.parse(metadataJson.value); + jsonObject[jsonKey] = selectedTags; + metadataJson.value = JSON.stringify(jsonObject, null, 2); + } + + // Initial render + renderTags(); } -/** Initialize all tagging fields on page */ export function initializeTaggingFields() { + // Initialize all taggings and taggings_autocomplete document.querySelectorAll('.tagging-label[data-tagging]').forEach(label => { const key = label.getAttribute('data-tagging'); - const type = label.getAttribute('data-tagging-type'); - const setup = { - containerId: key + 'Tags', - hiddenInputId: key + 'HiddenInput', - inputId: key + 'Input', - suggestionsId: key + 'Suggestions', - jsonKey: key, - useAutocomplete: type === 'tagging_autocomplete', - autocompleteSource: [] // will be fetched in init - }; - setupTagging(setup); + const taggingType = label.getAttribute('data-tagging-type'); // "tagging" or "tagging_autocomplete" + const containerId = key + 'Tags'; + const hiddenInputId = key + 'HiddenInput'; + const inputId = key + 'Input'; + const suggestionsId = key + 'Suggestions'; + + if (taggingType === "tagging_autocomplete") { + if (key === "license") { + fetch(SPDX_URL) + .then(response => response.json()) + .then(data => { + const spdxLicenses = data.licenses.map(license => license.licenseId); + setupTagging({ + containerId, + hiddenInputId, + inputId, + suggestionsId, + jsonKey: key, + useAutocomplete: true, + autocompleteSource: spdxLicenses + }); + }) + .catch(error => console.error('Error fetching SPDX licenses:', error)); + } else { + + getSchema().then(schema => { + const autocompleteSource = schema.properties?.[key]?.items?.enum || []; + setupTagging({ + containerId, + hiddenInputId, + inputId, + suggestionsId, + jsonKey: key, + useAutocomplete: true, + autocompleteSource + }); + }); + } + } else { + setupTagging({ + containerId, + hiddenInputId, + inputId, + jsonKey: key, + useAutocomplete: false + }); + } }); } -/** Single-object input helper */ -export function setupSingleInputObject({ containerId, hiddenInputId, inputId, jsonKey }) { - const input = document.getElementById(inputId); - const hiddenInput = document.getElementById(hiddenInputId); - const metadataJson = document.getElementById('metadata-json'); - const label = document.querySelector(`.single-input-object-label[for="${inputId}"]`); - const constantType = label ? label.getAttribute('data-single-input-object-type') : null; - - let obj = {}; - try { obj = JSON.parse(hiddenInput.value); } catch {} - if (obj.identifier) input.value = obj.identifier; - - function updateObj() { - const identifier = input.value.trim(); - const newObj = { '@type': constantType || 'ScholarlyArticle', identifier }; - hiddenInput.value = JSON.stringify(newObj); - const jsonObj = JSON.parse(metadataJson.value); - jsonObj[jsonKey] = newObj; - metadataJson.value = JSON.stringify(jsonObj, null, 2); +export function showInvalidTagMessage(container, input, message) { + // Remove any existing invalid message + const existing = container.querySelector('.invalid-tag-message'); + if (existing) existing.remove(); + + const msg = document.createElement("span"); + msg.classList.add("highlight-tag", "invalid-tag-message"); + msg.innerHTML = `❌ ${message} Got it!`; + container.insertBefore(msg, input); + + msg.querySelector('.acknowledge-tag').onclick = () => msg.remove(); + setTimeout(() => { if (msg.parentNode) msg.remove(); }, 2500); +} + +export function updateSuggestionsBoxPosition(input, suggestionsBox) { + const rect = input.getBoundingClientRect(); + suggestionsBox.style.left = rect.left + "px"; + suggestionsBox.style.top = rect.bottom + "px"; + suggestionsBox.style.width = rect.width + "px"; +} + + // General autocomplete technique +export function setupTagAutocompleteInput({ input, selectedTagsProvider, autocompleteSource, onTagSelected, container }) { + // Create or get suggestions box + let suggestionsBox = container.querySelector('.tag-suggestions-global'); + if (!suggestionsBox) { + suggestionsBox = createSuggestionsBox(container); } - input.addEventListener('input', updateObj); - input.addEventListener('change', updateObj); + input.addEventListener('input', function () { + const query = input.value.trim().toLowerCase(); + suggestionsBox.innerHTML = ''; + if (!query) { + suggestionsBox.style.display = 'none'; + return; + } + const selectedTags = selectedTagsProvider(); + const filtered = autocompleteSource.filter( + tag => tag.toLowerCase().startsWith(query) && !selectedTags.includes(tag) + ); + if (filtered.length === 0) { + suggestionsBox.style.display = 'none'; + return; + } + filtered.forEach(tag => { + const div = document.createElement('div'); + div.className = 'suggestion-item'; + div.textContent = tag; + div.style.cursor = 'pointer'; + div.onclick = function () { + onTagSelected(tag); + suggestionsBox.style.display = 'none'; + }; + suggestionsBox.appendChild(div); + }); + // Position suggestions below the input + updateSuggestionsBoxPosition(input, suggestionsBox); + suggestionsBox.style.display = 'block'; + }); + + input.addEventListener('focus', function () { + suggestionsBox.innerHTML = ''; + const query = input.value.trim().toLowerCase(); + const selectedTags = selectedTagsProvider(); + // Show all suggestions if input is empty, or filtered if not + const filtered = autocompleteSource.filter( + tag => !selectedTags.includes(tag) && (query === "" || tag.toLowerCase().startsWith(query)) + ); + if (filtered.length === 0) { + suggestionsBox.style.display = 'none'; + return; + } + filtered.forEach(tag => { + const div = document.createElement('div'); + div.className = 'suggestion-item'; + div.textContent = tag; + div.style.cursor = 'pointer'; + div.onclick = function () { + onTagSelected(tag); + suggestionsBox.style.display = 'none'; + }; + suggestionsBox.appendChild(div); + }); + // Position suggestions below the input + updateSuggestionsBoxPosition(input, suggestionsBox); + suggestionsBox.style.display = 'block'; + }); + + // Hide suggestions on blur/click outside + input.addEventListener('blur', function () { + setTimeout(() => { suggestionsBox.style.display = 'none'; }, 200); + }); } + + // Enable tagging autocomplete + export function setupTableTagAutocomplete({ cell, autocompleteSource }) { + const input = cell.querySelector('.tag-input'); + if (!input) return; + const tagsList = cell.querySelector('.tags-list'); + setupTagAutocompleteInput({ + input, + selectedTagsProvider: () => Array.from(tagsList.querySelectorAll('.tag')).map(t => t.textContent.trim().replace('×', '').trim()), + autocompleteSource, + onTagSelected: (tag) => { + input.value = tag; + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + }, + container: cell + }); +} \ No newline at end of file diff --git a/static/foundation/js/vendor/ui.js b/static/foundation/js/vendor/ui.js new file mode 100644 index 0000000..4b457c5 --- /dev/null +++ b/static/foundation/js/vendor/ui.js @@ -0,0 +1,138 @@ +// ui.js + +// pop-up message for Contributor and Author tabs +function showPopup() { + document.getElementById('popup').style.display = 'block'; +} + +function closePopup() { + document.getElementById('popup').style.display = 'none'; +} + +export function setupUI() { + const closeBtn = document.getElementById('closePopup'); + if (closeBtn) closeBtn.onclick = closePopup; + + window.onclick = function (event) { + if (event.target === document.getElementById('popup')) { + closePopup(); + } + }; + + + // tabs_ext + const tabs_ext = document.querySelectorAll('.tab-links_ext a'); + const contents = document.querySelectorAll('.tab-content_ext .tab'); + + tabs_ext.forEach(tab => { + tab.addEventListener('click', function (event) { + event.preventDefault(); + tabs_ext.forEach(item => item.parentElement.classList.remove('active')); + contents.forEach(content => content.classList.remove('active')); + this.parentElement.classList.add('active'); + let contentId = this.getAttribute('href'); + document.querySelector(contentId).classList.add('active'); + }); + }); + + // Attach event listeners to all forward buttons + document.querySelectorAll('.forwardBtn').forEach(function (forwardBtn) { + forwardBtn.addEventListener('click', function (event) { + event.preventDefault(); + const tabLinks = Array.from(document.querySelectorAll('.tab-links_ext a')); + const activeTab = tabLinks.find(link => link.parentElement.classList.contains('active')); + if (!activeTab) return; + const currentIndex = tabLinks.indexOf(activeTab); + if (currentIndex !== -1 && currentIndex < tabLinks.length - 1) { + tabLinks[currentIndex + 1].click(); + } + }); + }); + + // Attach event listeners to all backward buttons + document.querySelectorAll('.backwardBtn').forEach(function (backwardBtn) { + backwardBtn.addEventListener('click', function (event) { + event.preventDefault(); + const tabLinks = Array.from(document.querySelectorAll('.tab-links_ext a')); + const activeTab = tabLinks.find(link => link.parentElement.classList.contains('active')); + if (!activeTab) return; + const currentIndex = tabLinks.indexOf(activeTab); + if (currentIndex > 0) { + tabLinks[currentIndex - 1].click(); + } + }); + }); + + // custom tooltips + document.querySelectorAll('.custom-tooltip-metadata').forEach(function (element) { + const tooltip = element.querySelector('.tooltip-text-metadata'); + const icon = element.querySelector('i'); + element.addEventListener('mouseenter', function () { + tooltip.style.display = 'block'; + tooltip.style.visibility = 'visible'; + tooltip.style.opacity = '1'; + tooltip.style.position = 'fixed'; + tooltip.style.zIndex = '9999'; + const rect = icon.getBoundingClientRect(); + const margin = 16; + let left = rect.right; + let top = rect.top + margin; + tooltip.style.left = left + 'px'; + tooltip.style.top = top + 'px'; + }); + element.addEventListener('mouseleave', function () { + tooltip.style.display = 'none'; + tooltip.style.visibility = 'hidden'; + tooltip.style.opacity = '0'; + }); + }); + + // toggle + const toggleSwitch = document.getElementById('toggleSwitch'); + if (toggleSwitch) { + toggleSwitch.addEventListener('change', toggleSection); + window.addEventListener('load', toggleSection); + } +} + +// toggle +function toggleSection() { + var formContainer = document.getElementById('formContainer'); + var metadataFormDisplay = document.getElementById('metadataFormDisplay'); + var toggleSwitch = document.getElementById('toggleSwitch'); + var personInfoElements = document.querySelectorAll('.person-info'); // Select all elements with the class 'person-info' + + if (toggleSwitch.checked) { + metadataFormDisplay.style.display = 'block'; + formContainer.classList.remove('full-width'); + formContainer.classList.add('half-width'); + personInfoElements.forEach(function (element) { + // element.style.width = '57%'; + }); + } else { + metadataFormDisplay.style.display = 'none'; + formContainer.classList.remove('half-width'); + formContainer.classList.add('full-width'); + personInfoElements.forEach(function (element) { + element.style.width = '70%'; + }); + } +} + +// UI feedback on copy +export function actionFeedback(value) { + var feedback = document.getElementById('actionFeedback'); + feedback.innerHTML = value; + feedback.style.display = 'inline'; + setTimeout(function () { + feedback.style.display = 'none'; + }, 2000); +} + +export function toggleCollapse() { + const content = document.getElementById('contributor-explanation'); + if (content) { + content.style.display = (content.style.display === 'none' || content.style.display === '') ? 'block' : 'none'; + } +} + window.toggleCollapse = toggleCollapse; diff --git a/static/foundation/js/vendor/utils.js b/static/foundation/js/vendor/utils.js deleted file mode 100644 index abc6bc6..0000000 --- a/static/foundation/js/vendor/utils.js +++ /dev/null @@ -1,18 +0,0 @@ -// src/utils.js -// Utility functions: key extraction - -// /** -// * Get field key for an input element, stripping array notation and hidden suffix. -// * @param {HTMLInputElement|HTMLSelectElement} input -// * @returns {string} -// */ -export function getFieldKey(input) { - let key = input.name || input.id || ""; - if (key.includes("[")) { - key = key.split("[")[0]; - } - if (key.endsWith("HiddenInput")) { - key = key.replace(/HiddenInput$/, ""); - } - return key; -} diff --git a/templates/base.html b/templates/base.html index cd946ae..108630b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -45,7 +45,7 @@ {% block content %}{% endblock %} - + + - - - - {% block scripts %}{% endblock %} From 587aa5cefbcc17c987aeca693a338407b4c44528 Mon Sep 17 00:00:00 2001 From: Sundraiz-Shah Date: Mon, 30 Jun 2025 11:58:51 +0200 Subject: [PATCH 07/26] requiments update --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7f1e4be..48b39c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,5 +11,5 @@ python-gitlab==3.13.0 GitPython>=3.1.41 # Git-based packages pinned to specific commits/branches -git+https://github.com/softwarepub/hermes-plugin-github-gitlab@2b29c2830a4a444fcf850fdc6ba5859819a203d7#egg=hermes_plugin_git +git+https://github.com/softwarepub/hermes-plugin-github-gitlab@2a28c038bd87a3050f365ed103702554786ad66f#egg=hermes_plugin_githublab git+https://github.com/Aidajafarbigloo/hermes@3918954c463cf50916e824ac63d4e4e15e3c5277#egg=hermes \ No newline at end of file From 961efd372b92dba1119fe3dfdb8287c3cf23acaa Mon Sep 17 00:00:00 2001 From: Sundraiz-Shah Date: Wed, 2 Jul 2025 15:27:51 +0200 Subject: [PATCH 08/26] Removed Script from Index.html --- .../templates/meta_creator/index.html | 11 +------- static/foundation/js/vendor/init.js | 3 ++- static/foundation/js/vendor/ui.js | 25 +++++++++++++++++++ 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/meta_creator/templates/meta_creator/index.html b/meta_creator/templates/meta_creator/index.html index 379eb2b..7888be6 100644 --- a/meta_creator/templates/meta_creator/index.html +++ b/meta_creator/templates/meta_creator/index.html @@ -60,14 +60,5 @@
- + {% endblock content %} diff --git a/static/foundation/js/vendor/init.js b/static/foundation/js/vendor/init.js index 4847f0d..b0cda77 100644 --- a/static/foundation/js/vendor/init.js +++ b/static/foundation/js/vendor/init.js @@ -4,11 +4,12 @@ import { setupForm } from './form-utils.js'; import { initializeTaggingFields} from './tagging.js'; import { setupTables} from './table-utils.js'; import { setupDownload } from './download.js'; -import { setupUI } from './ui.js'; +import { setupUI, loadpage} from './ui.js'; import{ initializeDynamicDropdowns } from './dropdown-utils.js'; import {setMandatoryFieldsFromSchema} from './schema-utils.js'; // Entry point: called when DOM is fully loaded document.addEventListener('DOMContentLoaded', () => { + loadpage(); setupForm(); initializeTaggingFields(); setupTables(); diff --git a/static/foundation/js/vendor/ui.js b/static/foundation/js/vendor/ui.js index 7d9936e..daae8ff 100644 --- a/static/foundation/js/vendor/ui.js +++ b/static/foundation/js/vendor/ui.js @@ -258,3 +258,28 @@ export function toggleCollapse() { return tagCount === 0; } +// loading spinner +function lodder(formId, overlayId, delay = 2000) { + const form = document.getElementById(formId); + const overlay = document.getElementById(overlayId); + + if (!form || !overlay) return; + + form.addEventListener('submit', function(event) { + event.preventDefault(); // Prevent default submission + overlay.classList.add('active'); // Show loading overlay + + setTimeout(function() { + form.submit(); // Submit after delay + }, delay); + }); +} + +// loadder Only runs ehen you sumbit the index html form +export function loadpage() { + const form = document.getElementById('form1'); + const overlay = document.getElementById('overlay'); + if (form && overlay) { + lodder('form1', 'overlay'); + } +} From 5748abe677e2e6f4e9e66301744ba172b4041f7e Mon Sep 17 00:00:00 2001 From: Sundraiz-Shah Date: Mon, 7 Jul 2025 10:30:05 +0200 Subject: [PATCH 09/26] Issue 195 Tagging input goes to JSON complete --- static/foundation/js/vendor/form-utils.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/static/foundation/js/vendor/form-utils.js b/static/foundation/js/vendor/form-utils.js index 5dc6af5..ae433cf 100644 --- a/static/foundation/js/vendor/form-utils.js +++ b/static/foundation/js/vendor/form-utils.js @@ -23,7 +23,6 @@ function handleInputChange(input) { const key = input.name.split("[")[0]; const subkey = input.name.split("[")[1]?.split("]")[0]; const excludedInputs = []; - // Collect all IDs of single input objects const singleInputObjectIds = Array.from(document.querySelectorAll('input[data-single-input-object]')) .map(input => input.name) // or .id, depending on what you want to exclude by @@ -35,7 +34,12 @@ function handleInputChange(input) { const addRowFields = document.querySelectorAll('[data-add-row="true"]'); const addRowFieldNames = Array.from(addRowFields).map(field => field.name).filter(Boolean); excludedInputs.push(...addRowFieldNames); - + + //Skip inputs inside [key]Tags divs + const parentDiv = input.closest('div'); + const isInsideTagsDiv = parentDiv && parentDiv.id && parentDiv.id.endsWith('Tags'); + if (isInsideTagsDiv) return; + if (!isInTable(input) && !isInAddRowControls(input)) { if (!excludedInputs.includes(input.name)) { if (subkey) { From cfdc1cb6a2a52dd7fcb011f32ecd40814a1e4aa7 Mon Sep 17 00:00:00 2001 From: Sundraiz-Shah Date: Tue, 15 Jul 2025 12:27:20 +0200 Subject: [PATCH 10/26] Home page responsive (completed) --- .../templates/meta_creator/error.html | 2 +- .../templates/meta_creator/index.html | 85 +- .../templates/meta_creator/information.html | 26 +- .../templates/meta_creator/legals.html | 95 +- .../templates/meta_creator/showdata.html | 792 +++--- meta_creator/views.py | 1 + static/foundation/css/foundation.css | 2515 +++++++++-------- static/foundation/css/newstylesheet.css | 7 + templates/base.html | 25 +- 9 files changed, 1846 insertions(+), 1702 deletions(-) create mode 100644 static/foundation/css/newstylesheet.css diff --git a/meta_creator/templates/meta_creator/error.html b/meta_creator/templates/meta_creator/error.html index 35de543..3b2c3fa 100644 --- a/meta_creator/templates/meta_creator/error.html +++ b/meta_creator/templates/meta_creator/error.html @@ -5,7 +5,7 @@ {% load static %} {% block content %} -
+

Something went wrong.

We're sorry, but there was an error while processing your request.

{{ error_message }}

diff --git a/meta_creator/templates/meta_creator/index.html b/meta_creator/templates/meta_creator/index.html index 98470e1..6ace47b 100644 --- a/meta_creator/templates/meta_creator/index.html +++ b/meta_creator/templates/meta_creator/index.html @@ -5,8 +5,10 @@ {% block content %} -
-

+
+
+ +
+

Software Metadata Extraction and Curation Software

+
+
+ + {% csrf_token %} +
+ Project URL + + The GitHub repository URL + + +
+ + + {% if error_message_url %} + {{ error_message_url }} + {% endif %} +
+
+ Token Key + + To extract metadata from GitHub repositories token is not needed, + however, using one resolves rate limitations and allows for additional metadata extraction. +
+ How to Generate a Token? +
+ Visit
Managing your personal access tokens + +
- - - {% if error_message_url %} - {{ error_message_url }} - {% endif %} -
-
- Token Key - - To extract metadata from GitHub repositories token is not needed, - however, using one resolves rate limitations and allows for additional metadata extraction. -
- How to Generate a Token? -
- Visit Managing your personal access tokens -
- -
+
+ + {% if error_message_token %} + {{ error_message_token }} + {% endif %} + +
- - {% if error_message_token %} - {{ error_message_token }} - {% endif %} - - +
+ + + +
+
@@ -61,4 +73,5 @@
+ {% endblock content %} diff --git a/meta_creator/templates/meta_creator/information.html b/meta_creator/templates/meta_creator/information.html index c9186ca..67c8ac5 100644 --- a/meta_creator/templates/meta_creator/information.html +++ b/meta_creator/templates/meta_creator/information.html @@ -5,17 +5,23 @@ {% block content %} -
-
-

What is SMECS ?

+
+
+
+

What is SMECS ?

+
+
+
+

Software Metadata Extraction and Curation Software or SMECS is a web application to extract and curate software metadata following the CodeMeta software metadata standard.

+

SMECS facilitates the extraction of software metadata from repositories on GitHub/GitLab. It offers a user-friendly graphical user interface for visualizing the retrieved metadata. This empowers Research Software Engineers (RSE) to curate the extracted metadata according to their requirements. Ultimately, SMECS delivers the curated metadata in JSON format, enhancing usability and accessibility. +

+

To get more information about this tool and to access the source code please visit SMECS GitHub

+
+
-
-

Software Metadata Extraction and Curation Software or SMECS is a web application to extract and curate software metadata following the CodeMeta software metadata standard.

-

SMECS facilitates the extraction of software metadata from repositories on GitHub/GitLab. It offers a user-friendly graphical user interface for visualizing the retrieved metadata. This empowers Research Software Engineers (RSE) to curate the extracted metadata according to their requirements. Ultimately, SMECS delivers the curated metadata in JSON format, enhancing usability and accessibility. -

-

To get more information about this tool and to access the source code please visit SMECS GitHub

+
+ +
- -
{% endblock content %} diff --git a/meta_creator/templates/meta_creator/legals.html b/meta_creator/templates/meta_creator/legals.html index b6d4b82..a1931ca 100644 --- a/meta_creator/templates/meta_creator/legals.html +++ b/meta_creator/templates/meta_creator/legals.html @@ -5,50 +5,59 @@ {% block content %} -
-
-

Legals/Impressum

+
+
+
+

Legals/Impressum

+
-
-

Address:

-
- OFFIS e. V.
- Escherweg 2
- 26121 Oldenburg
- Germany
- Telefon: +49 441 9722-0
- Fax: +49 441 9722-102
- E-Mail: institut@offis.de
- Internet: www.offis.de -
- -

Board Members

-

- Prof. Dr. Sebastian Lehnhoff (Chairman)
- Prof. Dr. techn. Susanne Boll-Westermann
- Prof. Dr.-Ing. Andreas Hein
- Prof. Dr.-Ing. Astrid Nieße -

- -

Register Court

-

- Amtsgericht Oldenburg
- Registernummer VR 1956 -

- -

VAT Identification Number

-

- DE 811582102 -

- -

Responsible in the sense of press law

-
- Dr. Jürgen Meister
- OFFIS e.V.
- Escherweg 2
- 26121 Oldenburg -
+
+
+

Address:

+
+ OFFIS e. V.
+ Escherweg 2
+ 26121 Oldenburg
+ Germany
+ Telefon: +49 441 9722-0
+ Fax: +49 441 9722-102
+ E-Mail: institut@offis.de
+ Internet: www.offis.de +
+
+
+

Board Members

+

+ Prof. Dr. Sebastian Lehnhoff (Chairman)
+ Prof. Dr. techn. Susanne Boll-Westermann
+ Prof. Dr.-Ing. Andreas Hein
+ Prof. Dr.-Ing. Astrid Nieße +

+
+
+

Register Court

+

+ Amtsgericht Oldenburg
+ Registernummer VR 1956 +

+
+
+

VAT Identification Number

+

+ DE 811582102 +

+
+
+ +

Responsible in the sense of press law

+
+ Dr. Jürgen Meister
+ OFFIS e.V.
+ Escherweg 2
+ 26121 Oldenburg +
+
-
+
{% endblock content %} diff --git a/meta_creator/templates/meta_creator/showdata.html b/meta_creator/templates/meta_creator/showdata.html index 3b3d46c..a02905c 100644 --- a/meta_creator/templates/meta_creator/showdata.html +++ b/meta_creator/templates/meta_creator/showdata.html @@ -3,413 +3,441 @@ {% load custom_filters %} {% block content %} - +
-
- -
+ -
+
-
- - +
+ +
+
{% endblock content %} diff --git a/meta_creator/views.py b/meta_creator/views.py index b85e6f4..927aad0 100644 --- a/meta_creator/views.py +++ b/meta_creator/views.py @@ -70,6 +70,7 @@ def index(request): "description_metadata":description_metadata, "extracted_metadata":extracted_metadata, "my_json_str": my_json_str, + "from_showdata": True, }, request)) except ConnectTimeout: diff --git a/static/foundation/css/foundation.css b/static/foundation/css/foundation.css index 698686e..f053ce7 100644 --- a/static/foundation/css/foundation.css +++ b/static/foundation/css/foundation.css @@ -1,7 +1,7 @@ /* Style for SMECS */ /******************** General ********************/ -body { +/* body { height: 100%; margin: 0; padding: 0; @@ -69,819 +69,863 @@ ol { ul ul, ol ul, ul ol, ol ol { margin-left: 1.25rem; margin-bottom: 0; - } - -[type='text'], [type='password'], [type='date'], [type='datetime'], [type='datetime-local'], [type='month'], [type='week'], [type='email'], [type='number'], [type='search'], [type='tel'], [type='time'], [type='url'], [type='color'], + } */ + +[type="text"], +[type="password"], +[type="date"], +[type="datetime"], +[type="datetime-local"], +[type="month"], +[type="week"], +[type="email"], +[type="number"], +[type="search"], +[type="tel"], +[type="time"], +[type="url"], +[type="color"], textarea { - display: block; - box-sizing: border-box; - width: 100%; - height: 2.4375rem; - /* margin: 0 0 1rem; */ - padding: 0.5rem; - border: 1px solid #cacaca; - border-radius: 0; - background-color: #fefefe; - box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1); - font-family: inherit; - font-size: 1rem; - font-weight: normal; - line-height: 1.5; - color: #0a0a0a; - transition: box-shadow 0.5s, border-color 0.25s ease-in-out; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; + display: block; + box-sizing: border-box; + width: 100%; + height: 2.4375rem; + /* margin: 0 0 1rem; */ + padding: 0.5rem; + border: 1px solid #cacaca; + border-radius: 0; + background-color: #fefefe; + box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1); + font-family: inherit; + font-size: 1rem; + font-weight: normal; + line-height: 1.5; + color: #0a0a0a; + transition: box-shadow 0.5s, border-color 0.25s ease-in-out; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +/********************* SMECS navigation bar **********************/ +#main-nav { + background-color: #055f82; + /* overflow: hidden; + padding: 0; + margin: 0; */ +} +/* + #main-nav .nav-list { + list-style-type: none; + font-size: 15px; + display: inline; + float: left; + margin: 0 50px; + padding: 15px; + } */ + +#main-nav .nav-item { + /* float: left; */ + padding: 5px; + /* margin-right: 20px; */ +} + +#main-nav .nav-link { + display: block; + color: white; + /* text-align: center; */ + /* padding: 5px 20px; */ + text-decoration: none; +} + +#main-nav .nav-link:hover { + background-color: #4ba0af; + border-radius: 5px; +} + +#main-nav a.logo { + /* float: left; */ + color: #fff; + font-size: 24px; + font-weight: bold; + /* text-align: center; */ + /* padding: 0px 20px; */ + /* text-decoration: none; + transition: red 0.3s ease; */ +} + +#main-nav a.logo:hover::after { + transition: all 0.35s; +} + +#main-nav .nav-link.active { + border-radius: 5px; + background-color: #4ba0af; + color: #fff; } /******************** Initial Form of the software metadata Extractor (index.html) ********************/ .InitialFormContainer { - max-width: 60%; - margin: 0 auto; - padding: 20px; - border: 1px solid #ddd; - border-radius: 5px; - margin-top: 9rem; - display: flex; + /* max-width: 60%; + margin: 40px; */ + padding: 20px; + border: 1px solid #ddd; + border-radius: 5px; + margin-top: 9rem; + /* display: flex; flex-direction: column; - align-items: center; + align-items: center; */ } .mainHeader { - text-align: center; - margin-bottom: 0; - font-size: 3rem; - font-weight: bold; + text-align: center; + margin-bottom: 0; + font-size: 3rem; + font-weight: bold; } #smecsSubtitle { - font-size: 1.3rem; + font-size: 1.3rem; } #form1 { - display: flex; - flex-direction: column; - justify-content: center; - margin: 3.5rem auto; - max-width: 600px; + display: flex; + flex-direction: column; + justify-content: center; + margin: 3.5rem auto; + max-width: 600px; } - #form1 h6 { - font-size: 18px; - font-weight: bold; - text-align: left; - margin-top: 2rem; - } +#form1 h6 { + font-size: 18px; + font-weight: bold; + text-align: left; + margin-top: 2rem; +} - #form1 input[type=url], - #form1 input[type=text] { - width: 500px; - padding: 8px; - border-radius: 4px; - border: 1px solid #ccc; - box-sizing: border-box; - font-size: 16px; - } +#form1 input[type="url"], +#form1 input[type="text"] { + /* width: 500px; */ + padding: 8px; + border-radius: 4px; + border: 1px solid #ccc; + box-sizing: border-box; + font-size: 16px; +} -button.ExData, button.TrySMECS { - background-color: #055F82; - color: white; - padding: 1% 2%; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; - margin: auto; - font-weight: bold; - margin-top: 3.5rem; +button.ExData { + background-color: #055f82; + color: white; + padding: 5px; + border: #ccc; + border-radius: 4px; + + cursor: pointer; + /* font-size: 1rem; */ + /* margin: auto; */ + font-weight: bold; + margin-top: 3.5rem; } - button.ExData:hover, button.TrySMECS:hover { - background-color: #4BA0AF; - cursor: pointer; - } +button.TrySMECS { + background-color: #055f82; + color: white; + /* padding: 1% 2%; */ + border: #ccc; + border-radius: 4px; + + cursor: pointer; + /* font-size: 1rem; */ + /* margin: auto; */ + font-weight: bold; + /* margin-top: 3.5rem; */ +} + +button.ExData:hover, +button.TrySMECS:hover { + background-color: #4ba0af; + cursor: pointer; +} button#new-url-extraction-page { - background-color: #4BA0AF; - border: 1px solid white; - color: white; - padding: 0.5% 0.8%; - border-radius: 4px; - cursor: pointer; - font-size: 72%; - margin: auto; - font-weight: bold; - margin-top: 1.5rem; - position: absolute; + background-color: #4ba0af; + border: 1px solid white; + color: white; + /* padding: 0.5% 0.8%; */ + border-radius: 4px; + cursor: pointer; + /* font-size: 72%; */ + /* margin: auto; */ + font-weight: bold; + /* margin-top: 1.5rem; */ + /* position: absolute; left: 93.8%; - bottom: 94.5%; + bottom: 94.5%; */ } - button#new-url-extraction-page:hover { - background-color: #055F82; - cursor: pointer; - border: 1px solid #4BA0AF; - } +button#new-url-extraction-page:hover { + background-color: #055f82; + cursor: pointer; + border: 1px solid #4ba0af; +} /************ Tool Tip Styles ************/ .tooltip-container { - display: flex; - align-items: center; + display: flex; + align-items: center; } .tool-tip { - display: inline-block; - position: relative; - margin-left: 0.5em; - color: white; + display: inline-block; + position: relative; + margin-left: 0.5em; + color: white; } - .tool-tip .tool-tip__icon { - background: #3b4246; - border-radius: 10px; - cursor: pointer; - display: inline-block; - font-family: times new roman; - height: 20px; - line-height: 1.3em; - text-align: center; - width: 20px; - } +.tool-tip .tool-tip__icon { + background: #3b4246; + border-radius: 10px; + cursor: pointer; + display: inline-block; + font-family: times new roman; + height: 20px; + line-height: 1.3em; + text-align: center; + width: 20px; +} - .tool-tip .tool-tip__icon a { - text-decoration: none; - color: white; - } - - .tool-tip .tool-tip__info { - display: none; - background: #262626; - opacity: 90%; - border: 1px solid #e9f16c; - border-radius: 3px; - font-size: 0.875em; - padding: 1em; - position: absolute; - left: 30px; - top: -20px; - width: 250px; - z-index: 2; - border-radius: 6px; - } +.tool-tip .tool-tip__icon a { + text-decoration: none; + color: white; +} - .tool-tip .tool-tip__info:before, - .tool-tip .tool-tip__info:after { - content: ""; - position: absolute; - left: -10px; - top: 7px; - border-style: solid; - border-width: 10px 10px 10px 0; - border-color: transparent #e9f16c; - } - - .tool-tip .tool-tip__info:after { - left: -10px; - border-right-color: #262626; - } - - .tool-tip .tool-tip__info .info { - display: block; - } - - .tool-tip:hover .tool-tip__info, - .tool-tip:focus .tool-tip__info { - display: inline-block; - } +.tool-tip .tool-tip__info { + display: none; + background: #262626; + opacity: 90%; + border: 1px solid #e9f16c; + border-radius: 3px; + font-size: 0.875em; + padding: 1em; + position: absolute; + left: 30px; + top: -20px; + width: 250px; + z-index: 2; + border-radius: 6px; +} + +.tool-tip .tool-tip__info:before, +.tool-tip .tool-tip__info:after { + content: ""; + position: absolute; + left: -10px; + top: 7px; + border-style: solid; + border-width: 10px 10px 10px 0; + border-color: transparent #e9f16c; +} + +.tool-tip .tool-tip__info:after { + left: -10px; + border-right-color: #262626; +} + +.tool-tip .tool-tip__info .info { + display: block; +} + +.tool-tip:hover .tool-tip__info, +.tool-tip:focus .tool-tip__info { + display: inline-block; +} a:focus + .tool-tip .tool-tip__info { - display: inline-block; + display: inline-block; } -#form1 input[type=url].error { - margin-bottom: 0; +#form1 input[type="url"].error { + margin-bottom: 0; } span.error_message { - color: red; - margin-top: 5px; + color: red; + margin-top: 5px; } /******************** Style for showdata.html ********************/ /* Styles for the entered data section */ +.whole-extraction-page { + margin-top: 5rem; + height: 98%; +} #more-info { - font-size: 14px; - color: #333; - text-align: center; - padding: 10px; - background-color: #f5f5f5; - border: 1px solid #ddd; - border-radius: 5px; - margin: 0 auto 20px; - max-width: 500px; + font-size: 14px; + color: #333; + text-align: center; + padding: 10px; + background-color: #f5f5f5; + border: 1px solid #ddd; + border-radius: 5px; + margin: 0 auto 20px; + max-width: 500px; } #enteredData { - margin: 1%; + margin: 1%; } - #enteredData table { - margin: 0; - border-collapse: collapse; - width: 100%; - } +#enteredData table { + margin: 0; + border-collapse: collapse; + width: 100%; +} - #enteredData td:first-child { - background-color: #c9c7c7e8; - padding: 5px; - font-weight: bold; - border-radius: 4px; - width: 20%; - text-align: center; - } +#enteredData td:first-child { + background-color: #c9c7c7e8; + padding: 5px; + font-weight: bold; + border-radius: 4px; + width: 20%; + text-align: center; +} - #enteredData td:last-child { - padding: 5px; - font-weight: normal; - border-radius: 4px; - border: 1px solid #ccc; - width: 80%; - } +#enteredData td:last-child { + padding: 5px; + font-weight: normal; + border-radius: 4px; + border: 1px solid #ccc; + width: 80%; +} /* Styles for the extracted metadata section */ .DataHeader { - margin-top: 1rem; - margin-left: 0.5rem; + margin-top: 1rem; + margin-left: 0.5rem; } -form.data-form2, form.data-form3 { - background-color: #f2f2f2; - padding: 10px; +form.data-form2, +form.data-form3 { + background-color: #f2f2f2; + padding: 10px; } form.data-form2 { - margin: 1%; - margin-top: 4%; - width: 98%; + margin: 1%; + margin-top: 4%; + width: 98%; } form.data-form3 { - overflow: auto; - height: 77.5%; - border-radius: 2px; - margin-top: 0; + overflow: auto; + height: 77.5%; + border-radius: 2px; + margin-top: 0; } .data-form3::after { - content: ""; - display: table; - clear: both; + content: ""; + display: table; + clear: both; } .tab .form-group { - width: 90%; - margin: 0px 0% 1% 9%; + width: 90%; + margin: 0px 0% 1% 9%; } .form-group label { - display: block; - font-weight: bold; + display: block; + font-weight: bold; } -.form-group input[type="text"], select { - width: 110%; - padding: 8px; - border-radius: 4px; - border: 1px solid #ccc; - transition: border-color 0.2s ease-in-out; +.form-group input[type="text"], +select { + width: 110%; + padding: 8px; + border-radius: 4px; + border: 1px solid #ccc; + transition: border-color 0.2s ease-in-out; } /* style for general single input elements */ .form-group .single_inputs { - width: 40%; - float: left; - margin-right: 10%; - margin-bottom: 0.2%; - box-sizing: border-box; + width: 40%; + float: left; + margin-right: 10%; + margin-bottom: 0.2%; + box-sizing: border-box; } -.form-group input[type="text"]:focus, select:focus { - outline: none; - border-color: #0088cc; +.form-group input[type="text"]:focus, +select:focus { + outline: none; + border-color: #0088cc; } /* style for general single input elements with longer values */ .form-group .long_field { - width: 85%; - margin-bottom: 0.2%; - margin-right: 10%; - float: left; - box-sizing: border-box; + width: 85%; + margin-bottom: 0.2%; + margin-right: 10%; + float: left; + box-sizing: border-box; } .form-group legend { - font-size: 0.8em; - font-weight: bold; + font-size: 0.8em; + font-weight: bold; } .form-group fieldset { - margin: 0; - padding: 0; - border: 1px solid #ada8a8; - border-radius: 4px; + margin: 0; + padding: 0; + border: 1px solid #ada8a8; + border-radius: 4px; } - .form-group fieldset.fieldset_content, .form-group fieldset.copyrightHolder { - width: 40%; - float: right; - margin-right: 15px; - margin-left: 5px; - margin-top: 5px; - margin-bottom: 5px; - } - +.form-group fieldset.fieldset_content, +.form-group fieldset.copyrightHolder { + width: 40%; + float: right; + margin-right: 15px; + margin-left: 5px; + margin-top: 5px; + margin-bottom: 5px; +} - .form-group fieldset label, .data-form3 label { - display: inline; - float: left; - font-size: 98%; - } +.form-group fieldset label, +.data-form3 label { + display: inline; + float: left; + font-size: 98%; +} - .data-form3 label:first-letter { - text-transform: uppercase; - } +.data-form3 label:first-letter { + text-transform: uppercase; +} .wide-input { - height: 100px; + height: 100px; } .form-group fieldset input[type="text"] { - width: 97%; - padding: 8px; - border-radius: 4px; - border: 1px solid #ccc; - transition: border-color 0.2s ease-in-out; - margin-bottom: 5px; - margin-left: 4px; + width: 97%; + padding: 8px; + border-radius: 4px; + border: 1px solid #ccc; + transition: border-color 0.2s ease-in-out; + margin-bottom: 5px; + margin-left: 4px; } .form-group fieldset label { - margin-left: 4px; + margin-left: 4px; } .form-group fieldset input[type="text"]:focus { - outline: none; - border-color: #0088cc; + outline: none; + border-color: #0088cc; } /* Styles for the metadata display section */ -.form-container { - height: 60vh; - overflow: hidden; - justify-content: space-between; - width: 70%; - float: left; - height: 500px; - width: 200px; -} +/* .form-container { + height: 60vh; + overflow: hidden; + justify-content: space-between; + width: 70%; + float: left; + height: 500px; + width: 200px; +} */ .metadata-container { - flex: 1; - margin-left: 20px; + flex: 1; + margin-left: 20px; } #metadata-json { - width: 100%; - height: 92%; - padding: 8px; - border-radius: 4px; - margin-bottom: 2px; + width: 100%; + height: 92%; + padding: 8px; + border-radius: 4px; + margin-bottom: 2px; } .invalid { - border: 1px solid rgba(226, 7, 7, 0.535); - background-color: rgba(225, 94, 94, 0.377); + border: 1px solid rgba(226, 7, 7, 0.535); + background-color: rgba(225, 94, 94, 0.377); } - button#new-url { - float: left; - margin: 0.5%; - margin-left: 1%; + float: left; + margin: 0.5%; + margin-left: 1%; } .main-container { - display: flex; - justify-content: space-between; - transition: all 0.3s ease; - margin: 0.4%; - height: 92%; + /* display: flex; */ + /* justify-content: space-between; */ + transition: all 0.3s ease; + /* margin: 0.4%; */ + height: 92%; } -.form-container { - width: 74%; - flex: 1; -} +/* .form-container { + width: 74%; + flex: 1; +} */ -.metadata-form-display { - float: right; - height: 500px; - width: 25%; -} +/* .metadata-form-display { + float: right; + height: 500px; + width: 25%; +} */ /* Error page / connection time out */ div.error-container { - background-color: #fff; - border: 1px solid #ddd; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - margin: 100px auto; - padding: 20px; - max-width: 400px; - text-align: center; + background-color: #fff; + border: 1px solid #ddd; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin: 100px auto; + padding: 20px; + max-width: 400px; + text-align: center; } - div.error-container h1 { - color: red; - font-size: 24px; - margin-bottom: 10px; - } +div.error-container h1 { + color: red; + font-size: 24px; + margin-bottom: 10px; +} - div.error-container p { - font-size: 16px; - margin-bottom: 15px; - } +div.error-container p { + font-size: 16px; + margin-bottom: 15px; +} - div.error-container a { - color: #007bff; - text-decoration: none; - } +div.error-container a { + color: #007bff; + text-decoration: none; +} - div.error-container a:hover { - text-decoration: underline; - } +div.error-container a:hover { + text-decoration: underline; +} /********************************* Style for auto table **********************************/ /* Fixed width and equal column distribution for auto-property-table */ .auto-property-table { - margin-top: 8px; - width: 110%; /* Same as .long_field */ - min-width: 500px; /* Optional: for usability */ - table-layout: auto; /* Ensures equal column width */ - margin-bottom: 1rem; - border-collapse: collapse; -} - - .auto-property-table th, - .auto-property-table td { - padding: 8px 12px; - width: auto; /* Let table-layout: fixed handle equal width */ - text-align: left; - padding: 8px; - border: 1px solid #ccc; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 80px; - max-width: 200px; - } - - .auto-property-table th:first-child, - .auto-property-table td:first-child { - max-width: 200px; - min-width: 20px; - width: 15%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } + margin-top: 8px; + width: 110%; /* Same as .long_field */ + min-width: 500px; /* Optional: for usability */ + table-layout: auto; /* Ensures equal column width */ + margin-bottom: 1rem; + border-collapse: collapse; +} + +.auto-property-table th, +.auto-property-table td { + padding: 8px 12px; + width: auto; /* Let table-layout: fixed handle equal width */ + text-align: left; + padding: 8px; + border: 1px solid #ccc; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 80px; + max-width: 200px; +} + +.auto-property-table th:first-child, +.auto-property-table td:first-child { + max-width: 200px; + min-width: 20px; + width: 15%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} tr.add-row-controls, tr.add-row-controls td { - display: table-row; - width: auto; - box-sizing: border-box; - padding: 0.5em; - vertical-align: middle; - background: #f9f9f9; /* Optional: subtle highlight */ + display: table-row; + width: auto; + box-sizing: border-box; + padding: 0.5em; + vertical-align: middle; + background: #f9f9f9; /* Optional: subtle highlight */ } - tr.add-row-controls td { - /* Remove any display:block/flex if present */ - display: table-cell; - } +tr.add-row-controls td { + /* Remove any display:block/flex if present */ + display: table-cell; +} .auto-property-table td .tags-table-container, .auto-property-table .add-row-tags-container { - width: 100% !important; - max-width: 100% !important; - box-sizing: border-box; - padding: 0; + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box; + padding: 0; } - .auto-property-table td .tags-table-container input[type="text"], - .auto-property-table .add-row-tags-container input[type="text"] { - width: 95% !important; - max-width: 95% !important; - margin-left: 8px; - margin-right: 8px; - box-sizing: border-box; - min-width: 0; - padding: 4px 0; - font-size: 1em; - } +.auto-property-table td .tags-table-container input[type="text"], +.auto-property-table .add-row-tags-container input[type="text"] { + width: 95% !important; + max-width: 95% !important; + margin-left: 8px; + margin-right: 8px; + box-sizing: border-box; + min-width: 0; + padding: 4px 0; + font-size: 1em; +} .auto-property-table td .tags-list { - width: auto !important; - display: flex; - flex-wrap: wrap; - gap: 4px; - align-items: center; - box-sizing: border-box; - padding: 0; + width: auto !important; + display: flex; + flex-wrap: wrap; + gap: 4px; + align-items: center; + box-sizing: border-box; + padding: 0; } - .add-row-controls.invalid { - background-color: #ffd6d6; /* light red */ - border: 1px solid #ff0000; - } +.add-row-controls.invalid { + background-color: #ffd6d6; /* light red */ + border: 1px solid #ff0000; +} .add-row-controls .add-row-input, .add-row-controls .add-row-tag-input, .add-row-controls .add-row-dropdown-select { - min-width: 60px; - max-width: 200px; - width: 100%; - max-width: 100% !important; - box-sizing: border-box; - height: 2.4375rem; - padding: 0.5rem; - border: 1px solid #cacaca; - background-color: #fefefe; - font-size: 1rem; + min-width: 60px; + max-width: 200px; + width: 100%; + max-width: 100% !important; + box-sizing: border-box; + height: 2.4375rem; + padding: 0.5rem; + border: 1px solid #cacaca; + background-color: #fefefe; + font-size: 1rem; } - .add-row-controls .add-row-btn { - padding: 4px 12px; - font-size: 0.95em; - height: 32px; - white-space: nowrap; - } +.add-row-controls .add-row-btn { + padding: 4px 12px; + font-size: 0.95em; + height: 32px; + white-space: nowrap; +} .add-row-tags-container { - position: relative; + position: relative; } td.table-tagging-cell { - position: relative; + position: relative; } - td.table-tagging-cell input.tag-input, - td.table-tagging-cell select.table-dropdown-select { - width: 100%; - max-width: 100%; - box-sizing: border-box; - min-width: 0; - } +td.table-tagging-cell input.tag-input, +td.table-tagging-cell select.table-dropdown-select { + width: 100%; + max-width: 100%; + box-sizing: border-box; + min-width: 0; +} /* Make the tags-list a flex container so tags wrap and only take as much space as needed */ .tags-list { - display: flex; - flex-wrap: wrap; - gap: 4px; /* Optional: space between tags */ - align-items: center; + display: flex; + flex-wrap: wrap; + gap: 4px; /* Optional: space between tags */ + align-items: center; } /* Make each tag inline-flex and auto-sized */ .tag { - display: inline-flex; - align-items: center; - background: #e0e0e0; - border-radius: 12px; - padding: 2px 8px; - font-size: 0.95em; - margin: 2px 0; - white-space: nowrap; - max-width: 100%; + display: inline-flex; + align-items: center; + background: #e0e0e0; + border-radius: 12px; + padding: 2px 8px; + font-size: 0.95em; + margin: 2px 0; + white-space: nowrap; + max-width: 100%; } .tag-suggestions { - position: absolute; - background: #fff; - border: 1px solid #ccc; - z-index: 10000; - box-shadow: 0 2px 8px rgba(0,0,0,0.15); - max-height: 200px; - overflow-y: auto; - font-size: 0.95em; - min-width: 180px; /* optional: ensures a minimum width */ - padding: 0; - margin: 0; + position: absolute; + background: #fff; + border: 1px solid #ccc; + z-index: 10000; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + max-height: 200px; + overflow-y: auto; + font-size: 0.95em; + min-width: 180px; /* optional: ensures a minimum width */ + padding: 0; + margin: 0; } .suggestion-item { - padding: 4px 8px; - cursor: pointer; + padding: 4px 8px; + cursor: pointer; } - .suggestion-item:hover { - background: #f0f0f0; - } +.suggestion-item:hover { + background: #f0f0f0; +} /********************************* Style for contributors table **********************************/ /* contributors table */ -.contributors-table, .authors-table { - margin: 0 auto; - margin-bottom: 4%; - width: 80%; - border-collapse: collapse; - border: 1px solid #ddd; -} - - /* table header cells */ - .contributors-table th, .authors-table th { - background-color: #f2f2f2; - padding: 3px; - font-size: 70%; - text-align: center; - font-weight: bold; - border: 1px solid #ddd; - } +.contributors-table, +.authors-table { + margin: 0 auto; + margin-bottom: 4%; + width: 80%; + border-collapse: collapse; + border: 1px solid #ddd; +} - /* table data cells */ - .contributors-table td, .authors-table td { - padding: 10px; - border: 1px solid #ddd; - width: fit-content; - } +/* table header cells */ +.contributors-table th, +.authors-table th { + background-color: #f2f2f2; + padding: 3px; + font-size: 70%; + text-align: center; + font-weight: bold; + border: 1px solid #ddd; +} - /* Zebra-striping for table rows */ - .contributors-table tbody tr:nth-child(even) { - background-color: #f2f2f2; - } +/* table data cells */ +.contributors-table td, +.authors-table td { + padding: 10px; + border: 1px solid #ddd; + width: fit-content; +} + +/* Zebra-striping for table rows */ +.contributors-table tbody tr:nth-child(even) { + background-color: #f2f2f2; +} button.contributorBTN, button.authorBTN { - background-color: #828983; - color: white; - padding: 10px 20px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - margin: auto; - font-weight: bold; - margin-top: 1.5rem; - width: auto; - transition: background-color 0.3s; + background-color: #828983; + color: white; + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + margin: auto; + font-weight: bold; + margin-top: 1.5rem; + width: auto; + transition: background-color 0.3s; } - button.contributorBTN:hover, button.authorBTN:hover { - background-color: #6c6f7e; - } +button.contributorBTN:hover, +button.authorBTN:hover { + background-color: #6c6f7e; +} #metadata-form input.valid { - background-color: black; + background-color: black; } /* input container */ div.input-container { - display: flex; - gap: 10px; - margin-bottom: 15px; + display: flex; + gap: 10px; + margin-bottom: 15px; } - /* each input */ - div.input-container input { - padding: 10px; - border: 1px solid #ccc; - border-radius: 5px; - width: 200px; - font-size: 14px; - } - - /* placeholder text */ - div.input-container input::placeholder { - color: #999; - } - -/********************* SMECS navigation bar **********************/ -#main-nav { - background-color: #055F82; - overflow: hidden; - padding: 0; - margin: 0; +/* each input */ +div.input-container input { + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + width: 200px; + font-size: 14px; } - #main-nav .nav-list { - list-style-type: none; - font-size: 15px; - display: inline; - float: left; - margin: 0 50px; - padding: 15px; - } - - #main-nav .nav-item { - float: left; - padding: 5px; - margin-right: 20px; - } - - #main-nav .nav-link { - display: block; - color: white; - text-align: center; - padding: 5px 20px; - text-decoration: none; - } - - #main-nav .nav-link:hover { - background-color: #4BA0AF; - ; - border-radius: 5px; - } - - #main-nav a.logo { - float: left; - color: #fff; - font-size: 24px; - font-weight: bold; - text-align: center; - padding: 14px 20px; - text-decoration: none; - transition: red 0.3s ease; - } - - #main-nav a.logo:hover::after { - transition: all 0.35s; - } - - #main-nav .nav-link.active { - border-radius: 5px; - background-color: #4BA0AF; - color: #fff; - } +/* placeholder text */ +div.input-container input::placeholder { + color: #999; +} /********************* SMECS Information page **********************/ .info-page { - max-width: 60%; - margin: 50px auto; - padding: 30px; - background-color: #fff; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - border-radius: 8px; - font-family: 'Roboto', Arial, sans-serif; - line-height: 1.6; - color: #333; -} - -.info-header h3 { + /* max-width: 60%; + margin: 50px auto; */ + padding: 30px; + background-color: #fff; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border-radius: 8px; + font-family: "Roboto", Arial, sans-serif; + line-height: 1.6; + color: #333; + display: flex; + flex-direction: column; + /* height: 100vh; */ + /* padding-bottom: 100px; */ + margin-top: 9rem; +} + +/* .info-header h3 { text-align: center; font-size: 2em; color: #333; margin-bottom: 30px; -} +} */ -.info-content p { +/* .info-content p { font-size: 1.1em; text-align: justify; margin-bottom: 20px; -} +} */ .info-content a { - color: #007BFF; - text-decoration: none; + color: #007bff; + text-decoration: none; } - .info-content a:hover { - text-decoration: underline; - } +.info-content a:hover { + text-decoration: underline; +} .info-content h4 { - font-size: 1.5em; - color: #333; - margin-top: 20px; - margin-bottom: 10px; + /* font-size: 1.5em; */ + color: #333; + /* margin-top: 20px; + margin-bottom: 10px; */ } -.info-content ul, +/* .info-content ul, .info-content ol { margin: 20px 0; padding-left: 20px; -} +} */ - .info-content ul li, +/* .info-content ul li, .info-content ol li { margin-bottom: 10px; - } + } */ -button.TrySMECS { +/* button.TrySMECS { margin: 20% auto; padding: 1% 2%; -} +} */ -@media (max-width: 992px) { +/* @media (max-width: 992px) { .info-page { max-width: 80%; } @@ -927,572 +971,584 @@ button.TrySMECS { font-size: 0.9em; padding: 8px 16px; } -} +} */ /********************* SMECS First page tool-tips **********************/ .custom-tooltip { - position: relative; - float: left; - display: inline; - cursor: pointer; + position: relative; + float: left; + display: inline; + cursor: pointer; } .custom-tooltip-metadata { - position: relative; - display: inline-block; - cursor: pointer; + position: relative; + display: inline-block; + cursor: pointer; } .fa-info-circle { - opacity: 0.5; - font-size: 12px; - z-index: 1; + opacity: 0.5; + font-size: 12px; + z-index: 1; } .tooltip-text-metadata { - visibility: hidden; - font-size: 70%; - min-width: 200px; - max-width: 260px; - width: auto; - background-color: #5e5b5b; - color: #fff; - text-align: left; - border-radius: 6px; - padding: 5px; - z-index: 1000; - /*top: 100%; + visibility: hidden; + font-size: 70%; + min-width: 200px; + max-width: 260px; + width: auto; + background-color: #5e5b5b; + color: #fff; + text-align: left; + border-radius: 6px; + padding: 5px; + z-index: 1000; + /*top: 100%; left: 50%; position: absolute; */ - position: fixed; - left: 0; /* Will be set dynamically */ - top: 0; /* Will be set dynamically */ - transition: opacity 0.3s; - pointer-events: none; - opacity: 0; - word-break: break-word; /* Allow long words to wrap */ - white-space: normal; /* Allow text to wrap */ - box-sizing: border-box; - overflow-wrap: break-word; /* Ensure wrapping for long words */ - display: none; + position: fixed; + left: 0; /* Will be set dynamically */ + top: 0; /* Will be set dynamically */ + transition: opacity 0.3s; + pointer-events: none; + opacity: 0; + word-break: break-word; /* Allow long words to wrap */ + white-space: normal; /* Allow text to wrap */ + box-sizing: border-box; + overflow-wrap: break-word; /* Ensure wrapping for long words */ + display: none; } .custom-tooltip-metadata:hover .tooltip-text-metadata { - visibility: visible; - opacity: 1; + visibility: visible; + opacity: 1; } .custom-tooltip .tooltip-text { - visibility: hidden; - font-size: 65%; - width: 25rem; - word-wrap: break-word; - background-color: #5e5b5b; - color: #fff; - text-align: left; - border-radius: 6px; - padding: 5px; - position: absolute; - z-index: 1; - left: 15%; - transform: translateX(10%); - transition: opacity 0.3s; - pointer-events: none; + visibility: hidden; + font-size: 65%; + width: 25rem; + word-wrap: break-word; + background-color: #5e5b5b; + color: #fff; + text-align: left; + border-radius: 6px; + padding: 5px; + position: absolute; + z-index: 1; + left: 15%; + transform: translateX(10%); + transition: opacity 0.3s; + pointer-events: none; } .custom-tooltip-metadata .tooltip-text-metadata { - visibility: hidden; - font-size: 70%; - width: 100%; - background-color: #5e5b5b; - color: #fff; - text-align: left; - border-radius: 6px; - padding: 3px; - position: absolute; - top: 0; - transition: opacity 0.3s; - pointer-events: none; -} - -.custom-tooltip:hover .tooltip-text, .custom-tooltip .tooltip-text:hover { - visibility: visible; - opacity: 1; - pointer-events: auto; + visibility: hidden; + font-size: 70%; + width: 100%; + background-color: #5e5b5b; + color: #fff; + text-align: left; + border-radius: 6px; + padding: 3px; + position: absolute; + top: 0; + transition: opacity 0.3s; + pointer-events: none; +} + +.custom-tooltip:hover .tooltip-text, +.custom-tooltip .tooltip-text:hover { + visibility: visible; + opacity: 1; + pointer-events: auto; } .custom-tooltip-metadata:hover .tooltip-text-metadata { - visibility: visible; - opacity: 1; + visibility: visible; + opacity: 1; } .custom-tooltip .tooltip-text a { - color: rgb(112, 155, 233); - text-decoration: none; - font-weight: bold; + color: rgb(112, 155, 233); + text-decoration: none; + font-weight: bold; } - .custom-tooltip .tooltip-text a:hover { - text-decoration: underline; - } +.custom-tooltip .tooltip-text a:hover { + text-decoration: underline; +} .custom-tooltip i { - margin-left: 5px; + margin-left: 5px; } /***********************************Icons**************************************/ .feedback { - display: none; - margin-left: 10px; - color: green; - font-size: 1em; + display: none; + margin-left: 10px; + color: green; + font-size: 1em; } -.fa-download, .fa-copy, .fa-trash-alt { - border: none; - float: right; - margin-bottom: 0.5%; - font-size: 1.2em; - color: #055F82; - transition: color 0.3s; - background-color: none; - margin-left: 2.5%; +.fa-download, +.fa-copy, +.fa-trash-alt { + border: none; + float: right; + margin-bottom: 0.5%; + font-size: 1.2em; + color: #055f82; + transition: color 0.3s; + background-color: none; + margin-left: 2.5%; } - .fa-download:hover, .fa-copy:hover, .fa-trash-alt:hover { - color: #4BA0AF; - cursor: pointer; - } +.fa-download:hover, +.fa-copy:hover, +.fa-trash-alt:hover { + color: #4ba0af; + cursor: pointer; +} /************************************navigation-buttons**************************************/ .navigation-buttons { - clear: both; - width: 100%; - margin-top: 1%; + clear: both; + width: 100%; + margin-top: 1%; } - .navigation-buttons .backwardBtn, .navigation-buttons .forwardBtn { - width: 50px; - height: 50px; - border: none; - border-radius: 50%; - background-color: #055F82; - color: white; - font-size: 18px; - justify-content: center; - align-items: center; - cursor: pointer; - transition: background-color 0.3s; - } +.navigation-buttons .backwardBtn, +.navigation-buttons .forwardBtn { + width: 50px; + height: 50px; + border: none; + border-radius: 50%; + background-color: #055f82; + color: white; + font-size: 18px; + justify-content: center; + align-items: center; + cursor: pointer; + transition: background-color 0.3s; +} - .navigation-buttons .backwardBtn { - margin-left: 9%; - display: block; - float: left; - margin-bottom: 3%; - } +.navigation-buttons .backwardBtn { + margin-left: 9%; + display: block; + float: left; + margin-bottom: 3%; +} - .navigation-buttons .forwardBtn { - margin-right: 3%; - float: right; - display: block; - margin-bottom: 3%; - } +.navigation-buttons .forwardBtn { + margin-right: 3%; + float: right; + display: block; + margin-bottom: 3%; +} - .navigation-buttons button.ExData { - float: right; - margin: 0; - margin-right: 3%; - } +.navigation-buttons button.ExData { + float: right; + margin: 0; + margin-right: 3%; +} - .navigation-buttons .backwardBtn:hover, .navigation-buttons .forwardBtn:hover { - background-color: #4BA0AF; - } +.navigation-buttons .backwardBtn:hover, +.navigation-buttons .forwardBtn:hover { + background-color: #4ba0af; +} .backwardBtn::before { - content: '←'; - font-size: 24px; + content: "←"; + font-size: 24px; } .forwardBtn::before { - content: '→'; - font-size: 24px; + content: "→"; + font-size: 24px; } .single_inputs { - position: relative; + position: relative; } #suggestions { - position: absolute; - border: 1px solid #ccc; - background-color: #fff; - z-index: 1000; - width: 98%; - max-height: 200px; - overflow-y: auto; - display: none; - top: 100%; - left: 0; - box-sizing: border-box; + position: absolute; + border: 1px solid #ccc; + background-color: #fff; + z-index: 1000; + width: 98%; + max-height: 200px; + overflow-y: auto; + display: none; + top: 100%; + left: 0; + box-sizing: border-box; } .suggestion-item { - padding: 8px; - cursor: pointer; + padding: 8px; + cursor: pointer; } - .suggestion-item:hover { - background-color: #f0f0f0; - } +.suggestion-item:hover { + background-color: #f0f0f0; +} .input-container { - position: relative; + position: relative; } .added-element { - margin-top: 10px; + margin-top: 10px; } /************************************** JSON toggle **************************************/ -body, html { - overflow: hidden; /* Prevent page scrolling */ +body, +html { + overflow: hidden; /* Prevent page scrolling */ } .toggle { - text-align: right; - height: 0.1%; + text-align: right; + padding-top: 28px; + /* height: 0.1%; */ } -.form-container, .metadata-form-display { - padding: 0.4%; - border: 1px solid #ccc; - border-radius: 5px; - transition: all 0.3s ease; - height: 96%; - overflow: hidden; - margin: 2px; +.form-container, +.metadata-form-display { + padding: 0.4%; + border: 1px solid #ccc; + border-radius: 5px; + transition: all 0.3s ease; + height: 96%; + overflow: hidden; + margin: 2px; + width: 100%; } -.full-width { - width: 100%; +/* .full-width { + width: 100%; } .half-width { - width: 48%; -} + width: 100%; +} */ .metadata-form-display { - display: none; + display: none; } .toggle-switch { - position: relative; - display: inline-block; - width: 60px; - height: 34px; + position: relative; + display: inline-block; + width: 60px; + height: 34px; } - .toggle-switch input { - opacity: 0; - width: 0; - height: 0; - } +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} .slider { - position: absolute; - cursor: pointer; - right: 31%; - top: 77%; - bottom: 10%; - background-color: #ccc; - transition: .4s; - border-radius: 34px; - display: flex; - align-items: center; - color: white; - font-weight: bold; - font-size: 75%; - padding: 0 10px; - width: 94%; - height: 50%; + position: absolute; + cursor: pointer; + right: 31%; + top: 77%; + bottom: 10%; + background-color: #ccc; + transition: 0.4s; + border-radius: 34px; + display: flex; + align-items: center; + color: white; + font-weight: bold; + font-size: 75%; + padding: 0 10px; + width: 94%; + height: 50%; +} + +.slider:before { + position: absolute; + content: ""; + height: 98%; + width: 28%; + border-radius: 50%; + background-color: white; + transition: 0.4s; + left: 2px; + bottom: 4px; + top: 0; } - .slider:before { - position: absolute; - content: ""; - height: 98%; - width: 28%; - border-radius: 50%; - background-color: white; - transition: .4s; - left: 2px; - bottom: 4px; - top: 0; - } - input:checked + .slider { - background-color: #055F82; + background-color: #055f82; } - input:checked + .slider:before { - transform: translateX(243%); - } +input:checked + .slider:before { + transform: translateX(243%); +} /************************************** tabs_ext **************************************/ .tabs_ext { - width: 100%; + width: 100%; } .tab-links_ext { - list-style-type: none; - display: flex; - justify-content: flex-start; - margin: 0 auto; - width: 80%; - justify-content: space-between; - font-size: x-large; - font-family: Arial, sans-serif; + list-style-type: none; + display: flex; + justify-content: flex-start; + margin: 0 auto; + width: 80%; + justify-content: space-between; + font-size: x-large; + font-family: Arial, sans-serif; } - .tab-links_ext ul { - list-style-type: none; - padding: 0; - display: flex; - } +.tab-links_ext ul { + list-style-type: none; + padding: 0; + display: flex; +} - .tab-links_ext li { - margin: 0; - } +.tab-links_ext li { + margin: 0; +} - .tab-links_ext a { - display: block; - padding: 10px 20px; - text-decoration: none; - font-size: 55%; - color: #555; - border-radius: 4px 4px 0 0; - transition: background-color 0.3s, color 0.3s; - } +.tab-links_ext a { + display: block; + padding: 10px 20px; + text-decoration: none; + font-size: 55%; + color: #555; + border-radius: 4px 4px 0 0; + transition: background-color 0.3s, color 0.3s; +} - .tab-links_ext a:hover { - background-color: #f0f0f0; - color: #000; - } +.tab-links_ext a:hover { + background-color: #f0f0f0; + color: #000; +} - .tab-links_ext li.active a { - background: #02103f; - color: #ccc; - border-bottom: 2px solid #02103f; - } +.tab-links_ext li.active a { + background: #02103f; + color: #ccc; + border-bottom: 2px solid #02103f; +} .tab { - display: none; + display: none; } - .tab.active { - display: block; - } +.tab.active { + display: block; +} /************************************contributors'-explanation**************************************/ .person-info { - position: absolute; - height: auto; - width: 52%; - top: 21%; - margin: 0 auto; - padding: 0.5%; - border-radius: 5px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - text-align: left; - left: 15%; - font-size: large; - background-color: #ccc; + position: absolute; + height: auto; + width: 52%; + top: 21%; + margin: 0 auto; + padding: 0.5%; + border-radius: 5px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + text-align: left; + left: 15%; + font-size: large; + background-color: #ccc; } - .person-info h5 { - font-size: 86%; - font-weight: bold; - } +.person-info h5 { + font-size: 86%; + font-weight: bold; +} - .person-info p { - margin: 0; - line-height: 1.5; - margin-bottom: 2%; - } +.person-info p { + margin: 0; + line-height: 1.5; + margin-bottom: 2%; +} /********************* Contributor table when more than 10 people **********************/ .scrollable-table { - max-height: 55%; - overflow-y: auto; - display: block; + max-height: 55%; + overflow-y: auto; + display: block; } /************************************popup**************************************/ .popup { - display: none; - position: fixed; - z-index: 1; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgb(0,0,0); - background-color: rgba(0,0,0,0.4); - background-color: rgb(0 0 0 / 58%); + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgb(0, 0, 0); + background-color: rgba(0, 0, 0, 0.4); + background-color: rgb(0 0 0 / 58%); } .popup-content { - background-color: #fefefe; - margin: 15% auto; - padding: 1%; - border: 1px solid #888; - width: 50%; - border: solid 3px; - border-radius: 19px; + background-color: #fefefe; + margin: 15% auto; + padding: 1%; + border: 1px solid #888; + width: 50%; + border: solid 3px; + border-radius: 19px; } .close-btn { - color: #aaa; - float: right; - font-size: 28px; - font-weight: bold; + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; } - .close-btn:hover, - .close-btn:focus { - color: black; - text-decoration: none; - cursor: pointer; - } +.close-btn:hover, +.close-btn:focus { + color: black; + text-decoration: none; + cursor: pointer; +} /************************************Color-code-info**************************************/ /* Create info strings to explain color and other codes */ .color-code-container { - align-items: center; - background-color: #ccc; - width: 99.9%; - background-color: #f2f2f2; - padding-top: 0.7%; - height: 14%; + background-color: #f2f2f2; + /* align-items: center; + background-color: #ccc; + width: 99.9%; + + padding-top: 0.7%; + height: 14%; */ } .color-code { - display: flex; - align-items: center; - margin-right: 20px; - width: 30%; - padding: 5px; + display: flex; + align-items: center; + margin-right: 20px; + width: 30%; + padding: 5px; } - .color-code > span:first-child { - width: 15px; - height: 15px; - border-radius: 50%; - display: inline-block; - margin-right: 8px; - } +.color-code > span:first-child { + width: 15px; + height: 15px; + border-radius: 50%; + display: inline-block; + margin-right: 8px; +} .color-code-text { - font-size: 55%; + font-size: 55%; } .red { - background-color: #e15e5e; - opacity: 0.3; - border: #e20707 solid 1px; + background-color: #e15e5e; + opacity: 0.3; + border: #e20707 solid 1px; } .yellow { - background-color: #fef6da; - border: yellow solid 1px; + background-color: #fef6da; + border: yellow solid 1px; } /* Create info string for the '*' symbol */ .color-code > span:first-child.symbol { - width: 15px; - height: 15px; - border-radius: 0; /* Remove the circle shape */ - display: flex; /* Use flexbox for centering */ - justify-content: center; /* Center horizontally */ - background-color: transparent; /* No background for the symbol */ - font-size: 18px; /* Match the size of the circle */ - color: #e15e5e; /* Match the color of the circle */ - font-weight: bold; /* Make the * bold */ - line-height: 1; /* Ensure proper vertical alignment */ -} - - .color-code > span:first-child.symbol::before { - content: "*"; /* Add the * symbol */ - } + width: 15px; + height: 15px; + border-radius: 0; /* Remove the circle shape */ + display: flex; /* Use flexbox for centering */ + justify-content: center; /* Center horizontally */ + background-color: transparent; /* No background for the symbol */ + font-size: 18px; /* Match the size of the circle */ + color: #e15e5e; /* Match the color of the circle */ + font-weight: bold; /* Make the * bold */ + line-height: 1; /* Ensure proper vertical alignment */ +} + +.color-code > span:first-child.symbol::before { + content: "*"; /* Add the * symbol */ +} /********************* Responsiveness **********************/ @media (min-width: 1024px) and (max-width: 1700px) { - .InitialFormContainer { + /* .InitialFormContainer { height: 73%; margin-top: 3rem; - } + } */ - button.ExData { + /* button.ExData { margin-top: 5%; font-size: 73%; font-weight: normal; padding: 6px 12px; - } + } */ - .custom-tooltip .tooltip-text { - width: 25rem; - left: 8%; - } + .custom-tooltip .tooltip-text { + width: 25rem; + left: 8%; + } - .info-page { + /* .info-page { max-width: 81%; font-size: 85%; - } + } */ - .tab-links_ext a { - font-size: 50%; - padding: 7px 7px; - } + .tab-links_ext a { + font-size: 50%; + padding: 7px 7px; + } - #form1 h6 { - font-size: 87%; - } + #form1 h6 { + font-size: 87%; + } - .metadata-container { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px; - } + .metadata-container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + } - .left { - flex-grow: 1; - justify-content: flex-start; - } + .left { + flex-grow: 1; + justify-content: flex-start; + } - .right { - display: flex; - justify-content: flex-end; - gap: 5px; - flex-grow: 2; - } + .right { + display: flex; + justify-content: flex-end; + gap: 5px; + flex-grow: 2; + } - button#updateFormBtn { - background-color: #055F82; - border: 1px solid white; - color: white; - padding: 5px 8px; - border-radius: 4px; - cursor: pointer; - font-size: 72%; - font-weight: bold; - } + button#updateFormBtn { + background-color: #055f82; + border: 1px solid white; + color: white; + padding: 5px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 72%; + font-weight: bold; + } - button#new-url-extraction-page { + /* button#new-url-extraction-page { background-color: #4BA0AF; border: 1px solid white; color: white; @@ -1506,358 +1562,367 @@ input:checked + .slider { position: absolute; left: 93.8%; bottom: 94.5%; - } - - .fa-copy:before, .fa-download:before { - font-size: 80%; - } + } */ - #metadata-json { - font-size: 74%; - } + .fa-copy:before, + .fa-download:before { + font-size: 80%; + } - #metadata-form-display h4 { - font-size: medium; - } + #metadata-json { + font-size: 74%; + } - .slider { - top: -16%; - right: 20%; - font-size: 71%; - width: 94%; - height: 48%; - } + #metadata-form-display h4 { + font-size: medium; + } - .metadata-container { - margin-left: 2%; - margin-top: 4%; - } + .slider { + top: -16%; + right: 20%; + font-size: 71%; + width: 94%; + height: 48%; + } - .form-group fieldset.fieldset_content, .form-group fieldset.copyrightHolder { - width: 45%; - margin-right: 4%; - margin-top: 4%; - margin-bottom: 7%; - } + .metadata-container { + margin-left: 2%; + margin-top: 4%; + } - .navigation-buttons .backwardBtn, .navigation-buttons .forwardBtn { - width: 2em; - height: 2em; - } + .form-group fieldset.fieldset_content, + .form-group fieldset.copyrightHolder { + width: 45%; + margin-right: 4%; + margin-top: 4%; + margin-bottom: 7%; + } - button.contributorBTN, button.authorBTN { - font-size: 65%; - } + .navigation-buttons .backwardBtn, + .navigation-buttons .forwardBtn { + width: 2em; + height: 2em; + } - .contributors-table, .authors-table { - font-size: 78%; - } + button.contributorBTN, + button.authorBTN { + font-size: 65%; + } - .contributors-table th, .authors-table th { - font-size: 83%; - } + .contributors-table, + .authors-table { + font-size: 78%; + } - .whole-extraction-page { - height: 98%; - } + .contributors-table th, + .authors-table th { + font-size: 83%; + } } /* ******************************************** */ @media (min-width: 2320px) AND (max-width: 2560px) { - button#new-url-extraction-page { + /* button#new-url-extraction-page { font-size: 86%; - } - - .tab-links_ext a { - font-size: 58%; - } - - .color-code-container { - height: 12%; - } - - .contributors-table, .authors-table { - font-size: 60%; - } - - .navigation-buttons .backwardBtn, .navigation-buttons .forwardBtn { - width: 55px; - height: 55px; - } - - .backwardBtn::before, .forwardBtn::before { - font-size: 18px; - } - - button.contributorBTN, button.authorBTN { - padding: 7px 9px; - font-size: 15px; - } - - .contributors-table td, .authors-table td, .contributors-table th, .authors-table th { - padding: 8px; - font-size: 1.5em; - } - - .fa-trash-alt:before, .fa-trash-can:before, .fa-copy:before, .fa-download:before { - font-size: 110%; - } - - .form-group input[type="text"], select { - padding: 6px; - height: 6%; - font-size: 1em; - } - - .form-group .single_inputs { - margin-bottom: 0.5%; - } + } */ + + .tab-links_ext a { + font-size: 58%; + } + + .color-code-container { + height: 12%; + } + + .contributors-table, + .authors-table { + font-size: 60%; + } + + .navigation-buttons .backwardBtn, + .navigation-buttons .forwardBtn { + width: 55px; + height: 55px; + } + + .backwardBtn::before, + .forwardBtn::before { + font-size: 18px; + } + + button.contributorBTN, + button.authorBTN { + padding: 7px 9px; + font-size: 15px; + } + + .contributors-table td, + .authors-table td, + .contributors-table th, + .authors-table th { + padding: 8px; + font-size: 1.5em; + } + + .fa-trash-alt:before, + .fa-trash-can:before, + .fa-copy:before, + .fa-download:before { + font-size: 110%; + } + + .form-group input[type="text"], + select { + padding: 6px; + height: 6%; + font-size: 1em; + } - #metadata-json { - font-size: 100%; - } + .form-group .single_inputs { + margin-bottom: 0.5%; + } - form.data-form2, form.data-form3 { - padding: 0px; - } + #metadata-json { + font-size: 100%; + } - .navigation-buttons { - margin-bottom: 20px; - } + form.data-form2, + form.data-form3 { + padding: 0px; + } - .navigation-buttons button.ExData { - padding: 0.5%; - font-size: 80%; - } + .navigation-buttons { + margin-bottom: 20px; + } - .fa-download, .fa-copy, .fa-trash-alt { - margin-bottom: 2.5%; - } + .navigation-buttons button.ExData { + padding: 0.5%; + font-size: 80%; + } - .form-group legend { - font-size: 0.8em; - } + .fa-download, + .fa-copy, + .fa-trash-alt { + margin-bottom: 2.5%; + } - .form-group fieldset.fieldset_content, .form-group fieldset.copyrightHolder { - margin-top: 22px; - } + .form-group legend { + font-size: 0.8em; + } - .suggestion-item { - padding: 1px; - font-size: 66%; - width: 100%; - } + .form-group fieldset.fieldset_content, + .form-group fieldset.copyrightHolder { + margin-top: 22px; + } - .scrollable-table { - max-height: 50%; - } + .suggestion-item { + padding: 1px; + font-size: 66%; + width: 100%; + } - .custom-tooltip .tooltip-text { - transform: translateY(-18%); - font-size: 77%; - width: 30em; - left: 15%; - } + .scrollable-table { + max-height: 50%; + } - .InitialFormContainer { - max-width: 45%; - max-height: 77%; - overflow: hidden; - left: 15%; - font-size: 89%; - } + .custom-tooltip .tooltip-text { + transform: translateY(-18%); + font-size: 77%; + width: 30em; + left: 15%; + } + .InitialFormContainer { + max-width: 45%; + max-height: 77%; + overflow: hidden; + left: 15%; + font-size: 89%; + } + /* button.ExData { margin-top: 3.5rem; - } + } */ - .color-code-text { - font-size: 79%; - } + .color-code-text { + font-size: 79%; + } } /* *********************Fixed width when editing cells*********************** */ .table-cell { - white-space: nowrap; - /*overflow: hidden;*/ - text-overflow: ellipsis; + white-space: nowrap; + /*overflow: hidden;*/ + text-overflow: ellipsis; } .editable-input { - width: 100%; - height: 100%; - border: none; - padding: 0; - margin: 0; - box-sizing: border-box; + width: 100%; + height: 100%; + border: none; + padding: 0; + margin: 0; + box-sizing: border-box; } /* *********************Extracting Spinner*********************** */ .overlay { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - z-index: 1000; - justify-content: center; - align-items: center; - overflow: hidden; + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; + justify-content: center; + align-items: center; + overflow: hidden; } - .overlay.active { - display: flex; - } +.overlay.active { + display: flex; +} .loading-spinner { - text-align: center; - color: white; + text-align: center; + color: white; } - .loading-spinner .spinner { - width: 40px; - height: 40px; - border: 4px solid rgba(255, 255, 255, 0.3); - border-top-color: #333; - border-radius: 50%; - animation: spin 1s linear infinite; - } +.loading-spinner .spinner { + width: 40px; + height: 40px; + border: 4px solid rgba(255, 255, 255, 0.3); + border-top-color: #333; + border-radius: 50%; + animation: spin 1s linear infinite; +} @keyframes spin { - 0% { - transform: rotate(0deg); - } + 0% { + transform: rotate(0deg); + } - 100% { - transform: rotate(360deg); - } + 100% { + transform: rotate(360deg); + } } /* *********************Tags*********************** */ .form-group .tags-container { - width: 110%; - display: flex; - flex-wrap: wrap; - gap: 5px; - align-items: center; - padding: 5px; - box-sizing: border-box; + width: 110%; + display: flex; + flex-wrap: wrap; + gap: 5px; + align-items: center; + padding: 5px; + box-sizing: border-box; } .form-group .tag { - background-color: #dddddd; - color: rgb(34, 34, 34); - padding: 4px 8px; - border-radius: 6px; - display: flex; - align-items: center; - font-size: 13px; + background-color: #dddddd; + color: rgb(34, 34, 34); + padding: 4px 8px; + border-radius: 6px; + display: flex; + align-items: center; + font-size: 13px; } .form-group .remove-tag { - margin-left: 6px; - cursor: pointer; - font-weight: bold; + margin-left: 6px; + cursor: pointer; + font-weight: bold; } #languageInput { - border: none; - outline: none; - flex-grow: 1; - min-width: 120px; + border: none; + outline: none; + flex-grow: 1; + min-width: 120px; } #suggestions { - position: absolute; - background: white; - border: 1px solid #ccc; - max-height: 150px; - overflow-y: auto; - display: none; - z-index: 1000; + position: absolute; + background: white; + border: 1px solid #ccc; + max-height: 150px; + overflow-y: auto; + display: none; + z-index: 1000; } .suggestion-item { - padding: 5px 10px; - cursor: pointer; + padding: 5px 10px; + cursor: pointer; } - .suggestion-item:hover { - background-color: #f0f0f0; - } +.suggestion-item:hover { + background-color: #f0f0f0; +} .form-group .tags-container .input[type="text"] { - width: 110%; - outline: none; - flex-grow: 1; - min-width: 0px; - font-size: 1em; - padding: 4px 0; + width: 110%; + outline: none; + flex-grow: 1; + min-width: 0px; + font-size: 1em; + padding: 4px 0; } div div p#contributor-explanation { - width: 52%; + width: 52%; } .collapsible-content { - display: none; - margin-top: 10px; - z-index: 2000; + display: none; + margin-top: 10px; + z-index: 2000; } .collapsible-button { - position: absolute; - top: 19%; - left: 14%; - width: fit-content; - cursor: pointer; - border: none; - padding: 10px; - font-size: 16px; - font-weight: bold; - text-align: left; + position: absolute; + top: 19%; + left: 14%; + width: fit-content; + cursor: pointer; + border: none; + padding: 10px; + font-size: 16px; + font-weight: bold; + text-align: left; } .highlight-tag { - background-color: #fef6da; - padding: 5px 10px; - margin: 5px; - border-radius: 4px; - display: inline-block; - font-weight: bold; + background-color: #fef6da; + padding: 5px 10px; + margin: 5px; + border-radius: 4px; + display: inline-block; + font-weight: bold; } - .highlight-tag .acknowledge-tag { - margin-left: 10px; - color: #055F82; - cursor: pointer; - text-decoration: underline; - } - -.info-page { - display: flex; - flex-direction: column; - height: 100vh; - padding-bottom: 100px; - margin-top: 0; +.highlight-tag .acknowledge-tag { + margin-left: 10px; + color: #055f82; + cursor: pointer; + text-decoration: underline; } .info-header { - flex-shrink: 0; - margin-top: 0; + flex-shrink: 0; + margin-top: 0; } .info-content { - flex-grow: 1; - overflow-y: auto; - padding: 20px; - margin-top: 0; + flex-grow: 1; + overflow-y: auto; + padding: 20px; + margin-top: 0; } .unstyled-link { text-decoration: none; color: inherit; - cursor: pointer; + cursor: pointer; } diff --git a/static/foundation/css/newstylesheet.css b/static/foundation/css/newstylesheet.css new file mode 100644 index 0000000..c3d759f --- /dev/null +++ b/static/foundation/css/newstylesheet.css @@ -0,0 +1,7 @@ +.navbar .navbar-toggler-icon{ + background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); + +} +.navbar .navbar-toggler{ + border-color:rgba(255, 255, 255, 0.5); +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index ad006ab..eb6e9b6 100644 --- a/templates/base.html +++ b/templates/base.html @@ -7,7 +7,9 @@ {% block title %}SMECS{% endblock %} + + @@ -17,13 +19,18 @@ + - + From 2dbd0d1633a38356d71b52fe12757d6b9339fe1e Mon Sep 17 00:00:00 2001 From: Sundraiz-Shah Date: Fri, 25 Jul 2025 14:24:19 +0200 Subject: [PATCH 11/26] Resposive --- .../templates/meta_creator/index.html | 127 ++--- .../templates/meta_creator/showdata.html | 52 +-- static/foundation/css/foundation.css | 62 +-- static/foundation/css/newstylesheet.css | 15 +- static/foundation/js/vendor/ui.js | 436 ++++++++++-------- 5 files changed, 373 insertions(+), 319 deletions(-) diff --git a/meta_creator/templates/meta_creator/index.html b/meta_creator/templates/meta_creator/index.html index 6ace47b..72255ee 100644 --- a/meta_creator/templates/meta_creator/index.html +++ b/meta_creator/templates/meta_creator/index.html @@ -1,77 +1,94 @@ -{% extends 'base.html' %} - -{% load static %} - -{% block content %} +{% extends 'base.html' %} {% load static %} {% block content %}

-

Software Metadata Extraction and Curation Software

+

+ Software Metadata Extraction and Curation + Software +

-
- {% csrf_token %} -
- Project URL - - The GitHub repository URL - - -
- - - {% if error_message_url %} - {{ error_message_url }} - {% endif %} -
-
- Token Key - - To extract metadata from GitHub repositories token is not needed, - however, using one resolves rate limitations and allows for additional metadata extraction. -
- How to Generate a Token? -
- Visit Managing your personal access tokens -
- -
-
- - {% if error_message_token %} - {{ error_message_token }} - {% endif %} - -
+
+ {% csrf_token %} +
+ Project URL + The GitHub repository URL + +
+ + + {% if error_message_url %} + {{ error_message_url }} + {% endif %} +
+
+ Token Key + + To extract metadata from GitHub repositories token is not needed, + however, using one resolves rate limitations and allows for + additional metadata extraction. +
+ How to Generate a Token? +
+ Visit + Managing your personal access tokens +
+ +
+
+ + {% if error_message_token %} + {{ error_message_token }} + {% endif %} + +
- - - -
-
-

Extracting...

+

Extracting...

- - {% endblock content %} diff --git a/meta_creator/templates/meta_creator/showdata.html b/meta_creator/templates/meta_creator/showdata.html index a02905c..1b41a0e 100644 --- a/meta_creator/templates/meta_creator/showdata.html +++ b/meta_creator/templates/meta_creator/showdata.html @@ -6,19 +6,8 @@
- - - -
- - -
- -
- -
-
-
+
+
-
{% endblock content %} diff --git a/static/foundation/css/foundation.css b/static/foundation/css/foundation.css index f053ce7..1b88887 100644 --- a/static/foundation/css/foundation.css +++ b/static/foundation/css/foundation.css @@ -134,7 +134,7 @@ textarea { display: block; color: white; /* text-align: center; */ - /* padding: 5px 20px; */ + padding: 5px 10px; text-decoration: none; } @@ -418,7 +418,7 @@ form.data-form2 { form.data-form3 { overflow: auto; - height: 77.5%; + height: 90%; border-radius: 2px; margin-top: 0; } @@ -546,7 +546,7 @@ select:focus { #metadata-json { width: 100%; - height: 92%; + height: 100%; padding: 8px; border-radius: 4px; margin-bottom: 2px; @@ -564,11 +564,11 @@ button#new-url { } .main-container { - /* display: flex; */ + display: flex; /* justify-content: space-between; */ transition: all 0.3s ease; /* margin: 0.4%; */ - height: 92%; + height: 100%; } /* .form-container { @@ -1209,12 +1209,12 @@ div.input-container input::placeholder { /************************************** JSON toggle **************************************/ body, html { - overflow: hidden; /* Prevent page scrolling */ + /* overflow: hidden; Prevent page scrolling */ } .toggle { text-align: right; - padding-top: 28px; + /* padding-top: 28px; */ /* height: 0.1%; */ } @@ -1224,19 +1224,19 @@ html { border: 1px solid #ccc; border-radius: 5px; transition: all 0.3s ease; - height: 96%; - overflow: hidden; + /* height: 96%; */ + overflow: overlay; margin: 2px; width: 100%; } -/* .full-width { +.full-width { width: 100%; } .half-width { - width: 100%; -} */ + width: 60%; +} .metadata-form-display { display: none; @@ -1259,8 +1259,8 @@ html { position: absolute; cursor: pointer; right: 31%; - top: 77%; - bottom: 10%; + /* top: 77%; */ + bottom: 8px; background-color: #ccc; transition: 0.4s; border-radius: 34px; @@ -1355,13 +1355,11 @@ input:checked + .slider:before { position: absolute; height: auto; width: 52%; - top: 21%; margin: 0 auto; padding: 0.5%; border-radius: 5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); text-align: left; - left: 15%; font-size: large; background-color: #ccc; } @@ -1438,8 +1436,8 @@ input:checked + .slider:before { .color-code { display: flex; align-items: center; - margin-right: 20px; - width: 30%; + /* margin-right: 20px; */ + /* width: 30%; */ padding: 5px; } @@ -1451,9 +1449,9 @@ input:checked + .slider:before { margin-right: 8px; } -.color-code-text { +/* .color-code-text { font-size: 55%; -} +} */ .red { background-color: #e15e5e; @@ -1485,6 +1483,14 @@ input:checked + .slider:before { } /********************* Responsiveness **********************/ +@media (min-width: 300px) and (max-width: 990px) { + #metadata-json { + height: 30%; + } + .main-container { + display: list-item; + } +} @media (min-width: 1024px) and (max-width: 1700px) { /* .InitialFormContainer { @@ -1509,10 +1515,10 @@ input:checked + .slider:before { font-size: 85%; } */ - .tab-links_ext a { + /* .tab-links_ext a { font-size: 50%; padding: 7px 7px; - } + } */ #form1 h6 { font-size: 87%; @@ -1577,13 +1583,13 @@ input:checked + .slider:before { font-size: medium; } - .slider { + /* .slider { top: -16%; right: 20%; font-size: 71%; width: 94%; height: 48%; - } + } */ .metadata-container { margin-left: 2%; @@ -1875,13 +1881,17 @@ div div p#contributor-explanation { } .collapsible-content { - display: none; + /* display: none; */ + position: absolute; + top: 100%; /* Below the button */ + left: 0; + width: 100%; margin-top: 10px; z-index: 2000; } .collapsible-button { - position: absolute; + /* position: absolute; */ top: 19%; left: 14%; width: fit-content; diff --git a/static/foundation/css/newstylesheet.css b/static/foundation/css/newstylesheet.css index c3d759f..aa967b5 100644 --- a/static/foundation/css/newstylesheet.css +++ b/static/foundation/css/newstylesheet.css @@ -1,7 +1,10 @@ -.navbar .navbar-toggler-icon{ - background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); - +.navbar .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} +.navbar .navbar-toggler { + border-color: rgba(255, 255, 255, 0.5); +} +.relative-wrapper { + position: relative; + display: flex; } -.navbar .navbar-toggler{ - border-color:rgba(255, 255, 255, 0.5); -} \ No newline at end of file diff --git a/static/foundation/js/vendor/ui.js b/static/foundation/js/vendor/ui.js index daae8ff..3627f84 100644 --- a/static/foundation/js/vendor/ui.js +++ b/static/foundation/js/vendor/ui.js @@ -6,280 +6,320 @@ Tooltips */ import { getSchema } from "./schema-utils.js"; const metadataJson = document.getElementById("metadata-json"); -const urlInputs = document.querySelectorAll('.url-input'); -const tabs_ext = document.querySelectorAll('.tab-links_ext a'); -const contents = document.querySelectorAll('.tab-content_ext .tab'); - +const urlInputs = document.querySelectorAll(".url-input"); +const tabs_ext = document.querySelectorAll(".tab-links_ext a"); +const contents = document.querySelectorAll(".tab-content_ext .tab"); export function setupUI() { - const closeBtn = document.getElementById('closePopup'); + const closeBtn = document.getElementById("closePopup"); if (closeBtn) closeBtn.onclick = closePopup; window.onclick = function (event) { - if (event.target === document.getElementById('popup')) { - closePopup(); - } + if (event.target === document.getElementById("popup")) { + closePopup(); + } }; - const copyBtn = document.getElementById('copy-button'); - // copy button for json - copyBtn.addEventListener('click', function (event) { + const copyBtn = document.getElementById("copy-button"); + // copy button for json + copyBtn.addEventListener("click", function (event) { event.preventDefault(); metadataJson.select(); - document.execCommand('copy'); + document.execCommand("copy"); actionFeedback("Text copied!"); -}); + }); // tabs_ext - tabs_ext.forEach(tab => { - tab.addEventListener('click', function (event) { - event.preventDefault(); - tabs_ext.forEach(item => item.parentElement.classList.remove('active')); - contents.forEach(content => content.classList.remove('active')); - this.parentElement.classList.add('active'); - let contentId = this.getAttribute('href'); - document.querySelector(contentId).classList.add('active'); - }); + tabs_ext.forEach((tab) => { + tab.addEventListener("click", function (event) { + event.preventDefault(); + tabs_ext.forEach((item) => item.parentElement.classList.remove("active")); + contents.forEach((content) => content.classList.remove("active")); + this.parentElement.classList.add("active"); + let contentId = this.getAttribute("href"); + document.querySelector(contentId).classList.add("active"); + }); }); // Attach event listeners to all forward buttons - document.querySelectorAll('.forwardBtn').forEach(function (forwardBtn) { - forwardBtn.addEventListener('click', function (event) { - event.preventDefault(); - const tabLinks = Array.from(document.querySelectorAll('.tab-links_ext a')); - const activeTab = tabLinks.find(link => link.parentElement.classList.contains('active')); - if (!activeTab) return; - const currentIndex = tabLinks.indexOf(activeTab); - if (currentIndex !== -1 && currentIndex < tabLinks.length - 1) { - tabLinks[currentIndex + 1].click(); - } - }); + document.querySelectorAll(".forwardBtn").forEach(function (forwardBtn) { + forwardBtn.addEventListener("click", function (event) { + event.preventDefault(); + const tabLinks = Array.from( + document.querySelectorAll(".tab-links_ext a") + ); + const activeTab = tabLinks.find((link) => + link.parentElement.classList.contains("active") + ); + if (!activeTab) return; + const currentIndex = tabLinks.indexOf(activeTab); + if (currentIndex !== -1 && currentIndex < tabLinks.length - 1) { + tabLinks[currentIndex + 1].click(); + } + }); }); // Attach event listeners to all backward buttons - document.querySelectorAll('.backwardBtn').forEach(function (backwardBtn) { - backwardBtn.addEventListener('click', function (event) { - event.preventDefault(); - const tabLinks = Array.from(document.querySelectorAll('.tab-links_ext a')); - const activeTab = tabLinks.find(link => link.parentElement.classList.contains('active')); - if (!activeTab) return; - const currentIndex = tabLinks.indexOf(activeTab); - if (currentIndex > 0) { - tabLinks[currentIndex - 1].click(); - } - }); + document.querySelectorAll(".backwardBtn").forEach(function (backwardBtn) { + backwardBtn.addEventListener("click", function (event) { + event.preventDefault(); + const tabLinks = Array.from( + document.querySelectorAll(".tab-links_ext a") + ); + const activeTab = tabLinks.find((link) => + link.parentElement.classList.contains("active") + ); + if (!activeTab) return; + const currentIndex = tabLinks.indexOf(activeTab); + if (currentIndex > 0) { + tabLinks[currentIndex - 1].click(); + } + }); }); // custom tooltips - document.querySelectorAll('.custom-tooltip-metadata').forEach(function (element) { - const tooltip = element.querySelector('.tooltip-text-metadata'); - const icon = element.querySelector('i'); - element.addEventListener('mouseenter', function () { - tooltip.style.display = 'block'; - tooltip.style.visibility = 'visible'; - tooltip.style.opacity = '1'; - tooltip.style.position = 'fixed'; - tooltip.style.zIndex = '9999'; - const rect = icon.getBoundingClientRect(); - const margin = 16; - let left = rect.right; - let top = rect.top + margin; - tooltip.style.left = left + 'px'; - tooltip.style.top = top + 'px'; + document + .querySelectorAll(".custom-tooltip-metadata") + .forEach(function (element) { + const tooltip = element.querySelector(".tooltip-text-metadata"); + const icon = element.querySelector("i"); + element.addEventListener("mouseenter", function () { + tooltip.style.display = "block"; + tooltip.style.visibility = "visible"; + tooltip.style.opacity = "1"; + tooltip.style.position = "fixed"; + tooltip.style.zIndex = "9999"; + const rect = icon.getBoundingClientRect(); + const margin = 16; + let left = rect.right; + let top = rect.top + margin; + tooltip.style.left = left + "px"; + tooltip.style.top = top + "px"; }); - element.addEventListener('mouseleave', function () { - tooltip.style.display = 'none'; - tooltip.style.visibility = 'hidden'; - tooltip.style.opacity = '0'; + element.addEventListener("mouseleave", function () { + tooltip.style.display = "none"; + tooltip.style.visibility = "hidden"; + tooltip.style.opacity = "0"; }); - }); - + }); + // Initialize the state on page load window.onload = function () { toggleSection(); - document.getElementById('toggleSwitch').addEventListener('change', toggleSection); -}; + document + .getElementById("toggleSwitch") + .addEventListener("change", toggleSection); + }; //highlightsURLs highlightEditableUrls(urlInputs); + initAutoCloseCollapses(); } // pop-up message for Contributor and Author tabs function showPopup() { - document.getElementById('popup').style.display = 'block'; + document.getElementById("popup").style.display = "block"; } function closePopup() { - document.getElementById('popup').style.display = 'none'; + document.getElementById("popup").style.display = "none"; } - // Function to check if the popup should be shown for a given tab and repo - function checkAndShowPopup(tab, repo) { - const key = `popupShown-${repo}-${tab}`; - if (!localStorage.getItem(key)) { - showPopup(); - localStorage.setItem(key, 'true'); - } - } +// Function to check if the popup should be shown for a given tab and repo +function checkAndShowPopup(tab, repo) { + const key = `popupShown-${repo}-${tab}`; + if (!localStorage.getItem(key)) { + showPopup(); + localStorage.setItem(key, "true"); + } +} // toggle function toggleSection() { - var formContainer = document.getElementById('formContainer'); - var metadataFormDisplay = document.getElementById('metadataFormDisplay'); - var toggleSwitch = document.getElementById('toggleSwitch'); - var personInfoElements = document.querySelectorAll('.person-info'); // Select all elements with the class 'person-info' + var formContainer = document.getElementById("formContainer"); + var metadataFormDisplay = document.getElementById("metadataFormDisplay"); + var toggleSwitch = document.getElementById("toggleSwitch"); + var personInfoElements = document.querySelectorAll(".person-info"); // Select all elements with the class 'person-info' if (toggleSwitch.checked) { - metadataFormDisplay.style.display = 'block'; - formContainer.classList.remove('full-width'); - formContainer.classList.add('half-width'); - personInfoElements.forEach(function (element) { - // element.style.width = '57%'; - }); + metadataFormDisplay.style.display = "block"; + formContainer.classList.remove("col-lg-12"); + formContainer.classList.add("col-lg-9"); + metadataFormDisplay.classList.add("col-lg-3"); + personInfoElements.forEach(function (element) { + // element.style.width = '57%'; + }); } else { - metadataFormDisplay.style.display = 'none'; - formContainer.classList.remove('half-width'); - formContainer.classList.add('full-width'); - personInfoElements.forEach(function (element) { - element.style.width = '70%'; - }); + metadataFormDisplay.style.display = "none"; + formContainer.classList.remove("col-lg-9"); + formContainer.classList.add("col-lg-12"); + metadataFormDisplay.classList.remove("col-lg-3"); + personInfoElements.forEach(function (element) { + element.style.width = "70%"; + }); } } // UI feedback on copy export function actionFeedback(value) { - var feedback = document.getElementById('actionFeedback'); + var feedback = document.getElementById("actionFeedback"); feedback.innerHTML = value; - feedback.style.display = 'inline'; + feedback.style.display = "inline"; setTimeout(function () { - feedback.style.display = 'none'; + feedback.style.display = "none"; }, 2000); } export function toggleCollapse() { - const content = document.getElementById('contributor-explanation'); + const content = document.getElementById("contributor-explanation"); if (content) { - content.style.display = (content.style.display === 'none' || content.style.display === '') ? 'block' : 'none'; + content.style.display = + content.style.display === "none" || content.style.display === "" + ? "block" + : "none"; } } - window.toggleCollapse = toggleCollapse; +window.toggleCollapse = toggleCollapse; // Applying the yellow border for suggesting the user to change or review the extracted value - export function highlightEditableUrls(urlInputs) { - urlInputs.forEach(input => { - const initialValue = input.value; - if (initialValue !== "") { - input.style.border = '2px solid yellow'; - input.style.backgroundColor = '#fef6da'; - } - input.addEventListener('input', () => { - if (input.value !== initialValue) { - input.style.border = ''; - input.style.backgroundColor = ''; - } else if (initialValue !== "") { - // Reapply the yellow border if the value is reset to the initial value - input.style.border = '2px solid yellow'; - input.style.backgroundColor = '#fef6da'; - } - }); +export function highlightEditableUrls(urlInputs) { + urlInputs.forEach((input) => { + const initialValue = input.value; + if (initialValue !== "") { + input.style.border = "2px solid yellow"; + input.style.backgroundColor = "#fef6da"; + } + input.addEventListener("input", () => { + if (input.value !== initialValue) { + input.style.border = ""; + input.style.backgroundColor = ""; + } else if (initialValue !== "") { + // Reapply the yellow border if the value is reset to the initial value + input.style.border = "2px solid yellow"; + input.style.backgroundColor = "#fef6da"; + } }); + }); } - // Pinkish inputs, when no metadata is extracted - export function validateInput(input) { - const skipValidationIds = [ - 'contributorGivenNameInput', - 'contributorFamilyNameInput', - 'contributorEmailInput', - 'authorGivenNameInput', - 'authorFamilyNameInput', - 'authorEmailInput' - ]; - if (skipValidationIds.includes(input.id)) { - return; // Skip validation for the specified inputs - } - - // Fetch schema and validate only if field is required or recommended - getSchema().then(schema => { - const { required, recommended } = fetchRequiredAndRecommendedFields(schema); - const allMandatory = [...required, ...recommended]; +// Pinkish inputs, when no metadata is extracted +export function validateInput(input) { + const skipValidationIds = [ + "contributorGivenNameInput", + "contributorFamilyNameInput", + "contributorEmailInput", + "authorGivenNameInput", + "authorFamilyNameInput", + "authorEmailInput", + ]; + if (skipValidationIds.includes(input.id)) { + return; // Skip validation for the specified inputs + } - // --- Tagging support --- - // If input is inside a tags-container, validate the hidden input instead - const tagsContainer = input.closest('.tags-container'); - if (tagsContainer) { - const taggingWrapper = tagsContainer.closest('.tagging-wrapper'); - if (taggingWrapper) { - const hiddenInput = taggingWrapper.querySelector('input[type="hidden"]'); - const label = taggingWrapper.querySelector('.tagging-label'); - const taggingType = label ? label.getAttribute('data-tagging-type') : null; - const key = getFieldKey(hiddenInput); + // Fetch schema and validate only if field is required or recommended + getSchema() + .then((schema) => { + const { required, recommended } = + fetchRequiredAndRecommendedFields(schema); + const allMandatory = [...required, ...recommended]; - if (allMandatory.includes(key)) { - if (taggingType === "tagging_object") { - // Check number of tags in the container - if (isTaggingObjectEmpty(tagsContainer)) { - input.classList.add("invalid"); - } else { - input.classList.remove("invalid"); - } - } else { - // For normal tagging, check hidden input - if (hiddenInput.value.trim() === "") { - input.classList.add("invalid"); - } else { - input.classList.remove("invalid"); - } - } - } else { - input.classList.remove("invalid"); - } - return; - } - } + // --- Tagging support --- + // If input is inside a tags-container, validate the hidden input instead + const tagsContainer = input.closest(".tags-container"); + if (tagsContainer) { + const taggingWrapper = tagsContainer.closest(".tagging-wrapper"); + if (taggingWrapper) { + const hiddenInput = taggingWrapper.querySelector( + 'input[type="hidden"]' + ); + const label = taggingWrapper.querySelector(".tagging-label"); + const taggingType = label + ? label.getAttribute("data-tagging-type") + : null; + const key = getFieldKey(hiddenInput); - // --- Standard input/select validation --- - const key = getFieldKey(input); - if (allMandatory.includes(key)) { - if (input.value.trim() === "") { - input.classList.add("invalid"); - } else { - input.classList.remove("invalid"); - } + if (allMandatory.includes(key)) { + if (taggingType === "tagging_object") { + // Check number of tags in the container + if (isTaggingObjectEmpty(tagsContainer)) { + input.classList.add("invalid"); + } else { + input.classList.remove("invalid"); + } } else { + // For normal tagging, check hidden input + if (hiddenInput.value.trim() === "") { + input.classList.add("invalid"); + } else { input.classList.remove("invalid"); + } } - }) - .catch(() => { - // On schema load error, fallback to no validation + } else { input.classList.remove("invalid"); - }); + } + return; + } + } + + // --- Standard input/select validation --- + const key = getFieldKey(input); + if (allMandatory.includes(key)) { + if (input.value.trim() === "") { + input.classList.add("invalid"); + } else { + input.classList.remove("invalid"); + } + } else { + input.classList.remove("invalid"); + } + }) + .catch(() => { + // On schema load error, fallback to no validation + input.classList.remove("invalid"); + }); } - function isTaggingObjectEmpty(tagsContainer) { - // Count the number of .tag elements inside the tagsContainer - const tagCount = tagsContainer.querySelectorAll('.tag').length; - return tagCount === 0; - } +function isTaggingObjectEmpty(tagsContainer) { + // Count the number of .tag elements inside the tagsContainer + const tagCount = tagsContainer.querySelectorAll(".tag").length; + return tagCount === 0; +} // loading spinner function lodder(formId, overlayId, delay = 2000) { - const form = document.getElementById(formId); - const overlay = document.getElementById(overlayId); + const form = document.getElementById(formId); + const overlay = document.getElementById(overlayId); - if (!form || !overlay) return; + if (!form || !overlay) return; - form.addEventListener('submit', function(event) { - event.preventDefault(); // Prevent default submission - overlay.classList.add('active'); // Show loading overlay + form.addEventListener("submit", function (event) { + event.preventDefault(); // Prevent default submission + overlay.classList.add("active"); // Show loading overlay - setTimeout(function() { - form.submit(); // Submit after delay - }, delay); - }); + setTimeout(function () { + form.submit(); // Submit after delay + }, delay); + }); } // loadder Only runs ehen you sumbit the index html form export function loadpage() { - const form = document.getElementById('form1'); - const overlay = document.getElementById('overlay'); - if (form && overlay) { - lodder('form1', 'overlay'); - } + const form = document.getElementById("form1"); + const overlay = document.getElementById("overlay"); + if (form && overlay) { + lodder("form1", "overlay"); + } +} + +function initAutoCloseCollapses(collapseSelector = ".collapsible-content") { + document.querySelectorAll('[data-bs-toggle="collapse"]').forEach((button) => { + button.addEventListener("click", function () { + const targetId = this.getAttribute("data-bs-target"); + const targetCollapse = document.querySelector(targetId); + + document + .querySelectorAll(`${collapseSelector}.show`) + .forEach((openCollapse) => { + if (openCollapse !== targetCollapse) { + new bootstrap.Collapse(openCollapse, { toggle: false }).hide(); + } + }); + }); + }); } From 5b31196f053e24a39fe5abc32ece1c842f71448c Mon Sep 17 00:00:00 2001 From: Sundraiz-Shah Date: Mon, 28 Jul 2025 13:22:08 +0200 Subject: [PATCH 12/26] Css cleaned and Responsive --- .../templates/meta_creator/showdata.html | 16 +- static/foundation/css/foundation.css | 311 +----------------- static/foundation/css/newstylesheet.css | 10 - 3 files changed, 22 insertions(+), 315 deletions(-) delete mode 100644 static/foundation/css/newstylesheet.css diff --git a/meta_creator/templates/meta_creator/showdata.html b/meta_creator/templates/meta_creator/showdata.html index 1b41a0e..86ea85f 100644 --- a/meta_creator/templates/meta_creator/showdata.html +++ b/meta_creator/templates/meta_creator/showdata.html @@ -271,12 +271,12 @@

Extra Hints!

{% elif key == "contributor" %}
- -
+ +

A "contributor" refers to anyone who aids in software development in any capacity, from coding to testing, highlighting the collaborative nature of software projects.

- -
+ +

An "author" in software authorship is someone who significantly contributes to the creation and development of software, including roles like coding, project management, and documentation.

@@ -337,7 +337,7 @@

Extra Hints!

{% endfor %} {% if unique_tab %} {% for col, value in metadata_dict.items %} - + {% if col == key %} {% elif col == "author" %} @@ -350,7 +350,7 @@

Extra Hints!

{% endfor %} {% endif %} - + @@ -386,12 +386,12 @@

Extra Hints!

{% endfor %} {% if unique_tab %} {% for col, value in metadata_dict.items %} - + {% endfor %} {% endif %} - + diff --git a/static/foundation/css/foundation.css b/static/foundation/css/foundation.css index 1b88887..d01d126 100644 --- a/static/foundation/css/foundation.css +++ b/static/foundation/css/foundation.css @@ -1,75 +1,6 @@ /* Style for SMECS */ /******************** General ********************/ -/* body { - height: 100%; - margin: 0; - padding: 0; - background: #fefefe; - font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; - font-weight: normal; - line-height: 2.5; -} - -div, -dl, -dt, -dd, -ul, -ol, -li, -h1, -h2, -h3, -h4, -h5, -h6, -pre, -form, -blockquote, -th, -td { - margin: 0; - padding: 0; -} - - -hr { - clear: both; - max-width: 75rem; - height: 0; - margin: 1.25rem auto; - border-top: 0; - border-right: 0; - border-bottom: 1px solid #cacaca; - border-left: 0; -} - -ul, -ol, -dl { - margin-bottom: 1rem; - list-style-position: outside; - line-height: 1.6; -} - -li { - font-size: inherit; -} - -ul { - margin-left: 1.25rem; - list-style-type: disc; -} - -ol { - margin-left: 1.25rem; -} - - ul ul, ol ul, ul ol, ol ol { - margin-left: 1.25rem; - margin-bottom: 0; - } */ [type="text"], [type="password"], @@ -90,7 +21,6 @@ textarea { box-sizing: border-box; width: 100%; height: 2.4375rem; - /* margin: 0 0 1rem; */ padding: 0.5rem; border: 1px solid #cacaca; border-radius: 0; @@ -108,32 +38,18 @@ textarea { } /********************* SMECS navigation bar **********************/ + #main-nav { background-color: #055f82; - /* overflow: hidden; - padding: 0; - margin: 0; */ -} -/* - #main-nav .nav-list { - list-style-type: none; - font-size: 15px; - display: inline; - float: left; - margin: 0 50px; - padding: 15px; - } */ +} #main-nav .nav-item { - /* float: left; */ padding: 5px; - /* margin-right: 20px; */ } #main-nav .nav-link { display: block; color: white; - /* text-align: center; */ padding: 5px 10px; text-decoration: none; } @@ -144,14 +60,9 @@ textarea { } #main-nav a.logo { - /* float: left; */ color: #fff; font-size: 24px; font-weight: bold; - /* text-align: center; */ - /* padding: 0px 20px; */ - /* text-decoration: none; - transition: red 0.3s ease; */ } #main-nav a.logo:hover::after { @@ -163,18 +74,19 @@ textarea { background-color: #4ba0af; color: #fff; } +.navbar .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} +.navbar .navbar-toggler { + border-color: rgba(255, 255, 255, 0.5); +} /******************** Initial Form of the software metadata Extractor (index.html) ********************/ .InitialFormContainer { - /* max-width: 60%; - margin: 40px; */ padding: 20px; border: 1px solid #ddd; border-radius: 5px; margin-top: 9rem; - /* display: flex; - flex-direction: column; - align-items: center; */ } .mainHeader { @@ -205,7 +117,6 @@ textarea { #form1 input[type="url"], #form1 input[type="text"] { - /* width: 500px; */ padding: 8px; border-radius: 4px; border: 1px solid #ccc; @@ -221,8 +132,6 @@ button.ExData { border-radius: 4px; cursor: pointer; - /* font-size: 1rem; */ - /* margin: auto; */ font-weight: bold; margin-top: 3.5rem; } @@ -230,15 +139,10 @@ button.ExData { button.TrySMECS { background-color: #055f82; color: white; - /* padding: 1% 2%; */ border: #ccc; border-radius: 4px; - cursor: pointer; - /* font-size: 1rem; */ - /* margin: auto; */ font-weight: bold; - /* margin-top: 3.5rem; */ } button.ExData:hover, @@ -251,16 +155,9 @@ button#new-url-extraction-page { background-color: #4ba0af; border: 1px solid white; color: white; - /* padding: 0.5% 0.8%; */ border-radius: 4px; cursor: pointer; - /* font-size: 72%; */ - /* margin: auto; */ font-weight: bold; - /* margin-top: 1.5rem; */ - /* position: absolute; - left: 93.8%; - bottom: 94.5%; */ } button#new-url-extraction-page:hover { @@ -529,15 +426,6 @@ select:focus { } /* Styles for the metadata display section */ -/* .form-container { - height: 60vh; - overflow: hidden; - justify-content: space-between; - width: 70%; - float: left; - height: 500px; - width: 200px; -} */ .metadata-container { flex: 1; @@ -565,23 +453,10 @@ button#new-url { .main-container { display: flex; - /* justify-content: space-between; */ transition: all 0.3s ease; - /* margin: 0.4%; */ height: 100%; } -/* .form-container { - width: 74%; - flex: 1; -} */ - -/* .metadata-form-display { - float: right; - height: 500px; - width: 25%; -} */ - /* Error page / connection time out */ div.error-container { background-color: #fff; @@ -614,10 +489,11 @@ div.error-container a:hover { } /********************************* Style for auto table **********************************/ + /* Fixed width and equal column distribution for auto-property-table */ .auto-property-table { margin-top: 8px; - width: 110%; /* Same as .long_field */ + width: 115%; /* Same as .long_field */ min-width: 500px; /* Optional: for usability */ table-layout: auto; /* Ensures equal column width */ margin-bottom: 1rem; @@ -864,8 +740,6 @@ div.input-container input::placeholder { /********************* SMECS Information page **********************/ .info-page { - /* max-width: 60%; - margin: 50px auto; */ padding: 30px; background-color: #fff; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); @@ -880,19 +754,6 @@ div.input-container input::placeholder { margin-top: 9rem; } -/* .info-header h3 { - text-align: center; - font-size: 2em; - color: #333; - margin-bottom: 30px; -} */ - -/* .info-content p { - font-size: 1.1em; - text-align: justify; - margin-bottom: 20px; -} */ - .info-content a { color: #007bff; text-decoration: none; @@ -903,76 +764,9 @@ div.input-container input::placeholder { } .info-content h4 { - /* font-size: 1.5em; */ color: #333; - /* margin-top: 20px; - margin-bottom: 10px; */ -} - -/* .info-content ul, -.info-content ol { - margin: 20px 0; - padding-left: 20px; -} */ - -/* .info-content ul li, - .info-content ol li { - margin-bottom: 10px; - } */ - -/* button.TrySMECS { - margin: 20% auto; - padding: 1% 2%; -} */ - -/* @media (max-width: 992px) { - .info-page { - max-width: 80%; - } - - .info-header h3 { - font-size: 1.6em; - } - - .info-content p { - font-size: 0.95em; - } -} - -@media (max-width: 768px) { - .info-page { - max-width: 90%; - } - - .info-header h3 { - font-size: 1.4em; - } - - .info-content p { - font-size: 0.9em; - } } -@media (max-width: 576px) { - .info-page { - max-width: 95%; - padding: 20px; - } - - .info-header h3 { - font-size: 1.2em; - } - - .info-content p { - font-size: 0.85em; - } - - button.TrySMECS { - font-size: 0.9em; - padding: 8px 16px; - } -} */ - /********************* SMECS First page tool-tips **********************/ .custom-tooltip { @@ -1006,10 +800,6 @@ div.input-container input::placeholder { border-radius: 6px; padding: 5px; z-index: 1000; - /*top: 100%; - left: 50%; - position: absolute; - */ position: fixed; left: 0; /* Will be set dynamically */ top: 0; /* Will be set dynamically */ @@ -1207,15 +997,9 @@ div.input-container input::placeholder { } /************************************** JSON toggle **************************************/ -body, -html { - /* overflow: hidden; Prevent page scrolling */ -} .toggle { text-align: right; - /* padding-top: 28px; */ - /* height: 0.1%; */ } .form-container, @@ -1224,7 +1008,6 @@ html { border: 1px solid #ccc; border-radius: 5px; transition: all 0.3s ease; - /* height: 96%; */ overflow: overlay; margin: 2px; width: 100%; @@ -1425,19 +1208,11 @@ input:checked + .slider:before { /* Create info strings to explain color and other codes */ .color-code-container { background-color: #f2f2f2; - /* align-items: center; - background-color: #ccc; - width: 99.9%; - - padding-top: 0.7%; - height: 14%; */ } .color-code { display: flex; align-items: center; - /* margin-right: 20px; */ - /* width: 30%; */ padding: 5px; } @@ -1449,10 +1224,6 @@ input:checked + .slider:before { margin-right: 8px; } -/* .color-code-text { - font-size: 55%; -} */ - .red { background-color: #e15e5e; opacity: 0.3; @@ -1493,33 +1264,11 @@ input:checked + .slider:before { } @media (min-width: 1024px) and (max-width: 1700px) { - /* .InitialFormContainer { - height: 73%; - margin-top: 3rem; - } */ - - /* button.ExData { - margin-top: 5%; - font-size: 73%; - font-weight: normal; - padding: 6px 12px; - } */ - .custom-tooltip .tooltip-text { width: 25rem; left: 8%; } - /* .info-page { - max-width: 81%; - font-size: 85%; - } */ - - /* .tab-links_ext a { - font-size: 50%; - padding: 7px 7px; - } */ - #form1 h6 { font-size: 87%; } @@ -1554,22 +1303,6 @@ input:checked + .slider:before { font-weight: bold; } - /* button#new-url-extraction-page { - background-color: #4BA0AF; - border: 1px solid white; - color: white; - padding: 0.5% 0.8%; - border-radius: 4px; - cursor: pointer; - font-size: 72%; - margin: auto; - font-weight: bold; - margin-top: 1.5rem; - position: absolute; - left: 93.8%; - bottom: 94.5%; - } */ - .fa-copy:before, .fa-download:before { font-size: 80%; @@ -1583,14 +1316,6 @@ input:checked + .slider:before { font-size: medium; } - /* .slider { - top: -16%; - right: 20%; - font-size: 71%; - width: 94%; - height: 48%; - } */ - .metadata-container { margin-left: 2%; margin-top: 4%; @@ -1629,10 +1354,6 @@ input:checked + .slider:before { /* ******************************************** */ @media (min-width: 2320px) AND (max-width: 2560px) { - /* button#new-url-extraction-page { - font-size: 86%; - } */ - .tab-links_ext a { font-size: 58%; } @@ -1746,10 +1467,6 @@ input:checked + .slider:before { left: 15%; font-size: 89%; } - /* - button.ExData { - margin-top: 3.5rem; - } */ .color-code-text { font-size: 79%; @@ -1759,7 +1476,6 @@ input:checked + .slider:before { /* *********************Fixed width when editing cells*********************** */ .table-cell { white-space: nowrap; - /*overflow: hidden;*/ text-overflow: ellipsis; } @@ -1881,7 +1597,6 @@ div div p#contributor-explanation { } .collapsible-content { - /* display: none; */ position: absolute; top: 100%; /* Below the button */ left: 0; @@ -1889,9 +1604,11 @@ div div p#contributor-explanation { margin-top: 10px; z-index: 2000; } - +.relative-wrapper { + position: relative; + display: flex; +} .collapsible-button { - /* position: absolute; */ top: 19%; left: 14%; width: fit-content; diff --git a/static/foundation/css/newstylesheet.css b/static/foundation/css/newstylesheet.css deleted file mode 100644 index aa967b5..0000000 --- a/static/foundation/css/newstylesheet.css +++ /dev/null @@ -1,10 +0,0 @@ -.navbar .navbar-toggler-icon { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); -} -.navbar .navbar-toggler { - border-color: rgba(255, 255, 255, 0.5); -} -.relative-wrapper { - position: relative; - display: flex; -} From d19bc488ebf9d9918ee4151d33911b2009f2311c Mon Sep 17 00:00:00 2001 From: Sundraiz-Shah Date: Tue, 29 Jul 2025 10:16:57 +0200 Subject: [PATCH 13/26] Enable keyboard navigation for option selection in dropdown lists #182 --- .../templates/meta_creator/showdata.html | 6 +- static/foundation/css/foundation.css | 36 + static/foundation/js/vendor/tagging.js | 920 ++++++++++-------- 3 files changed, 552 insertions(+), 410 deletions(-) diff --git a/meta_creator/templates/meta_creator/showdata.html b/meta_creator/templates/meta_creator/showdata.html index 86ea85f..9d444d7 100644 --- a/meta_creator/templates/meta_creator/showdata.html +++ b/meta_creator/templates/meta_creator/showdata.html @@ -14,13 +14,9 @@

Metdata in JSON format

diff --git a/static/foundation/css/foundation.css b/static/foundation/css/foundation.css index d01d126..e97aba2 100644 --- a/static/foundation/css/foundation.css +++ b/static/foundation/css/foundation.css @@ -1653,3 +1653,39 @@ div div p#contributor-explanation { color: inherit; cursor: pointer; } + +.suggestion-item.active { + background-color: #3d7eb3; + color: white; +} + +/* Enhanced dropdown styling for + + +
@@ -274,6 +276,10 @@

Extra Hints!

An "author" in software authorship is someone who significantly contributes to the creation and development of software, including roles like coding, project management, and documentation.

+
+ +
+

An "maintainer" in software authorship is someone who significantly maintainers to the creation and development of software, including roles like coding, project management, and documentation.

{% endif %} diff --git a/static/foundation/css/foundation.css b/static/foundation/css/foundation.css index e97aba2..c8a9983 100644 --- a/static/foundation/css/foundation.css +++ b/static/foundation/css/foundation.css @@ -1011,6 +1011,7 @@ div.input-container input::placeholder { overflow: overlay; margin: 2px; width: 100%; + height: 100%; } .full-width { @@ -1255,11 +1256,12 @@ input:checked + .slider:before { /********************* Responsiveness **********************/ @media (min-width: 300px) and (max-width: 990px) { - #metadata-json { - height: 30%; + .form-container { + height: 50%; } .main-container { - display: list-item; + /* display: list-item; */ + flex-wrap: wrap; } } @@ -1619,6 +1621,12 @@ div div p#contributor-explanation { font-weight: bold; text-align: left; } +.collapsible-button:focus { + background-color: #055f82; + color: white; + border-radius: 5px; + font-weight: normal; +} .highlight-tag { background-color: #fef6da; @@ -1689,3 +1697,16 @@ select option:checked { background-color: #055f82; color: white; } +/* dropdown searcable */ + +.searchable-dropdown { + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: right 10px center; + background-size: 16px 16px; + cursor: pointer; +} + +.searchable-dropdown:focus::placeholder { + color: transparent; +} diff --git a/static/foundation/js/vendor/table-utils.js b/static/foundation/js/vendor/table-utils.js index a54d7d4..ea89a0d 100644 --- a/static/foundation/js/vendor/table-utils.js +++ b/static/foundation/js/vendor/table-utils.js @@ -5,745 +5,846 @@ Tracks user input in tables Syncs table rows with the main JSON object Handles input blur/change events to auto-update hidden fields and JSON */ -import {fetchRequiredAndRecommendedFields, getSchema} from './schema-utils.js'; -import {updateSuggestionsBoxPosition, setupTableTagAutocomplete,createSuggestionsBox} from './tagging.js'; +import { + fetchRequiredAndRecommendedFields, + getSchema, +} from "./schema-utils.js"; +import { + updateSuggestionsBoxPosition, + setupTableTagAutocomplete, + createSuggestionsBox, + enableEditableTagsInTable, +} from "./tagging.js"; const metadataJson = document.getElementById("metadata-json"); - // New table - export function updateTableHiddenInput(key) { - // Get all rows of the table - const table = document.querySelector(`#${key}Table`); - const hiddenInput = document.getElementById(`${key}TableHiddenInput`); - if (!table || !hiddenInput) return; - - const atType = table.getAttribute('data-at-type'); - const rows = Array.from(table.querySelectorAll('tbody tr')) - .filter(row => !row.classList.contains('add-row-controls')); // <-- skip add-row-controls - - // Check if this table is marked as unique - if (table.getAttribute('unique-tab') === 'True') { - // Get all headers and their data-col and data-coltype - const headerCells = Array.from(table.querySelectorAll('thead th')); - const headers = headerCells.map(th => ({ - name: th.getAttribute('data-col'), - coltype: th.getAttribute('data-coltype') - })); - - // elements: all headers with data-coltype == 'element' - const elements = headers - .filter(h => h.coltype === 'element') - .map(h => h.name); - - // subElements: all headers not 'delete' or 'element' - const subElements = headers - .filter(h => h.coltype !== 'delete' && h.coltype !== 'element') - .map(h => h.name); - - // Find the table body - const tbody = table.querySelector('tbody'); - const rows = tbody ? Array.from(tbody.querySelectorAll('tr')).filter(row => !row.classList.contains('add-row-controls')) : []; - const existingJson = JSON.parse(metadataJson.value); - - // Build elementList - const elementList = {}; - elements.forEach(element => { - elementList[element] = []; - }); - - rows.forEach(row => { - const cells = Array.from(row.cells); - // Build the element object from subElements - let elementObj = { "@type": atType }; - subElements.forEach((field) => { - const headerIdx = headers.findIndex(h => h.name === field && h.coltype !== 'element' && h.coltype !== 'delete'); - if (headerIdx >= 0 && cells[headerIdx]) { - const coltype = headers[headerIdx].coltype; - elementObj[field] = extractCellValue(cells[headerIdx], coltype); - } - }); - - // For each element, check if the checkbox is checked - elements.forEach(element => { - // Find the header index for this element - const headerIdx = headers.findIndex(h => h.name === element); - if (headerIdx >= 0 && cells[headerIdx]) { - const checkbox = cells[headerIdx].querySelector('.checkbox-element'); - if (checkbox && checkbox.checked) { - elementList[element].push({ ...elementObj }); - } - } - }); - }); - - // Update JSON - Object.keys(elementList).forEach(element => { - existingJson[element] = elementList[element]; - }); - metadataJson.value = JSON.stringify(existingJson, null, 2); +// New table +export function updateTableHiddenInput(key) { + // Get all rows of the table + const table = document.querySelector(`#${key}Table`); + const hiddenInput = document.getElementById(`${key}TableHiddenInput`); + if (!table || !hiddenInput) return; + + const atType = table.getAttribute("data-at-type"); + const rows = Array.from(table.querySelectorAll("tbody tr")).filter( + (row) => !row.classList.contains("add-row-controls") + ); // <-- skip add-row-controls + + // Check if this table is marked as unique + if (table.getAttribute("unique-tab") === "True") { + // Get all headers and their data-col and data-coltype + const headerCells = Array.from(table.querySelectorAll("thead th")); + const headers = headerCells.map((th) => ({ + name: th.getAttribute("data-col"), + coltype: th.getAttribute("data-coltype"), + })); + + // elements: all headers with data-coltype == 'element' + const elements = headers + .filter((h) => h.coltype === "element") + .map((h) => h.name); + + // subElements: all headers not 'delete' or 'element' + const subElements = headers + .filter((h) => h.coltype !== "delete" && h.coltype !== "element") + .map((h) => h.name); + + // Find the table body + const tbody = table.querySelector("tbody"); + const rows = tbody + ? Array.from(tbody.querySelectorAll("tr")).filter( + (row) => !row.classList.contains("add-row-controls") + ) + : []; + const existingJson = JSON.parse(metadataJson.value); + + // Build elementList + const elementList = {}; + elements.forEach((element) => { + elementList[element] = []; + }); - return; + rows.forEach((row) => { + const cells = Array.from(row.cells); + // Build the element object from subElements + let elementObj = { "@type": atType }; + subElements.forEach((field) => { + const headerIdx = headers.findIndex( + (h) => + h.name === field && + h.coltype !== "element" && + h.coltype !== "delete" + ); + if (headerIdx >= 0 && cells[headerIdx]) { + const coltype = headers[headerIdx].coltype; + elementObj[field] = extractCellValue(cells[headerIdx], coltype); } - - if (rows.length === 0) { - hiddenInput.value = '[]'; - // Also update the main JSON - const jsonObject = JSON.parse(metadataJson.value); - jsonObject[key] = []; - metadataJson.value = JSON.stringify(jsonObject, null, 2); - return; + }); + + // For each element, check if the checkbox is checked + elements.forEach((element) => { + // Find the header index for this element + const headerIdx = headers.findIndex((h) => h.name === element); + if (headerIdx >= 0 && cells[headerIdx]) { + const checkbox = cells[headerIdx].querySelector(".checkbox-element"); + if (checkbox && checkbox.checked) { + elementList[element].push({ ...elementObj }); + } } + }); + }); - // Get column headers (excluding the last "Delete" column) - const headers = Array.from(table.querySelectorAll('thead th')) - .map(th => th.getAttribute('data-col')) - .slice(0, -1); - - // Build array of objects - const data = rows.map(row => { - const cells = Array.from(row.querySelectorAll('td')); - let obj = {}; - if (atType) obj['@type'] = atType; - headers.forEach((header, i) => { - if (!header) return; // Skip if header is empty or undefined - const cell = cells[i]; - if (!cell) { - obj[header] = ''; - return; - } - const coltype = cell.getAttribute('data-coltype'); - obj[header] = extractCellValue(cell, coltype); - }); - return obj; - }); + // Update JSON + Object.keys(elementList).forEach((element) => { + existingJson[element] = elementList[element]; + }); + metadataJson.value = JSON.stringify(existingJson, null, 2); + + return; + } + + if (rows.length === 0) { + hiddenInput.value = "[]"; + // Also update the main JSON + const jsonObject = JSON.parse(metadataJson.value); + jsonObject[key] = []; + metadataJson.value = JSON.stringify(jsonObject, null, 2); + return; + } + + // Get column headers (excluding the last "Delete" column) + const headers = Array.from(table.querySelectorAll("thead th")) + .map((th) => th.getAttribute("data-col")) + .slice(0, -1); + + // Build array of objects + const data = rows.map((row) => { + const cells = Array.from(row.querySelectorAll("td")); + let obj = {}; + if (atType) obj["@type"] = atType; + headers.forEach((header, i) => { + if (!header) return; // Skip if header is empty or undefined + const cell = cells[i]; + if (!cell) { + obj[header] = ""; + return; + } + const coltype = cell.getAttribute("data-coltype"); + obj[header] = extractCellValue(cell, coltype); + }); + return obj; + }); - hiddenInput.value = JSON.stringify(data); + hiddenInput.value = JSON.stringify(data); - // Also update the main JSON - const jsonObject = JSON.parse(metadataJson.value); - jsonObject[key] = data; - metadataJson.value = JSON.stringify(jsonObject, null, 2); - } + // Also update the main JSON + const jsonObject = JSON.parse(metadataJson.value); + jsonObject[key] = data; + metadataJson.value = JSON.stringify(jsonObject, null, 2); +} // Function to extract cell value based on column type export function extractCellValue(cell, coltype) { - if (!cell) return ''; - if (coltype === 'dropdown') { - if (cell.hasAttribute('data-value')) { - return cell.getAttribute('data-value'); - } else if (cell.querySelector('select')) { - return cell.querySelector('select').value; - } else { - return cell.textContent.trim(); - } - } else if (coltype === 'tagging' || coltype === 'tagging_autocomplete') { - // Extract all tag values from data-tag or data-value attribute - return Array.from(cell.querySelectorAll('.tag')).map(tagEl => { - let val = tagEl.getAttribute('data-tag') || tagEl.getAttribute('data-value'); - if (val) return val; - // Remove the trailing " ×" or "×" from textContent - return tagEl.textContent.replace(/\s*×$/, '').trim(); - }); + if (!cell) return ""; + if (coltype === "dropdown") { + if (cell.hasAttribute("data-value")) { + return cell.getAttribute("data-value"); + } else if (cell.querySelector("select")) { + return cell.querySelector("select").value; } else { - return cell.textContent.trim(); + return cell.textContent.trim(); } + } else if (coltype === "tagging" || coltype === "tagging_autocomplete") { + // Extract all tag values from data-tag or data-value attribute + return Array.from(cell.querySelectorAll(".tag")).map((tagEl) => { + let val = + tagEl.getAttribute("data-tag") || tagEl.getAttribute("data-value"); + if (val) return val; + // Remove the trailing " ×" or "×" from textContent + return tagEl.textContent.replace(/\s*×$/, "").trim(); + }); + } else { + return cell.textContent.trim(); + } } - // Set up event listeners on all auto-property-tables export function setupTables() { + // Loop over all tables with the class 'auto-property-table' + document + .querySelectorAll("table.auto-property-table") + .forEach(function (table) { + // Extract the key from the table's id (assumes id is like 'copyrightHolderTable') + const tableId = table.id; + if (!tableId || !tableId.endsWith("Table")) return; + const key = tableId.replace(/Table$/, ""); + + // Attach a listener for cell edits (blur on any input or td) + table.addEventListener( + "blur", + function (e) { + if (e.target.tagName === "TD" || e.target.tagName === "INPUT") { + updateTableHiddenInput(key); + } + }, + true + ); + + table.addEventListener("change", function (e) { + if (e.target.classList.contains("checkbox-element")) { + updateTableHiddenInput(key); + } + }); - // Loop over all tables with the class 'auto-property-table' - document.querySelectorAll('table.auto-property-table').forEach(function (table) { - // Extract the key from the table's id (assumes id is like 'copyrightHolderTable') - const tableId = table.id; - if (!tableId || !tableId.endsWith('Table')) return; - const key = tableId.replace(/Table$/, ''); - - // Attach a listener for cell edits (blur on any input or td) - table.addEventListener('blur', function (e) { - if (e.target.tagName === 'TD' || e.target.tagName === 'INPUT') { - updateTableHiddenInput(key); - } - }, true); - - table.addEventListener('change', function (e) { - if (e.target.classList.contains('checkbox-element')) { - updateTableHiddenInput(key); - } - }); - - // Optionally, update on row addition/removal or other events as needed - // For initial sync - updateTableHiddenInput(key); + // Optionally, update on row addition/removal or other events as needed + // For initial sync + updateTableHiddenInput(key); }); - // Add Row functionality for all auto-property-tables - document.querySelectorAll('.add-row-btn').forEach(function (btn) { - btn.addEventListener('click', function () { - const key = btn.getAttribute('data-table-key'); - const table = document.getElementById(key + 'Table'); - const hiddenInput = document.getElementById(key + 'TableHiddenInput'); - if (!table || !hiddenInput) return; - - // Find the add-row-controls row - const addRowControls = table.querySelector('tr.add-row-controls[data-table-key="' + key + '"]'); - if (!addRowControls) return; - - // Get input values - const inputs = addRowControls.querySelectorAll('.add-row-input, .add-row-tag-input, .add-row-dropdown-select'); - console.log({ inputs }) - const values = Array.from(inputs).map(input => input.value.trim()); - - // Prevent adding if all fields are empty - const allEmpty = values.every(val => val === ''); - if (allEmpty) return; - - // Create new row - const newRow = document.createElement('tr'); - // Get column headers - const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.getAttribute('data-col')); - // Only add data columns, skip the last header ("Delete") - for (let i = 0; i < headers.length - 1; i++) { - const header = headers[i]; - // Find the input for this column - const input = Array.from(inputs).find(inp => - inp.getAttribute('data-col') === header && !inp.classList.contains('invalid') - ); - const th = table.querySelector(`thead th[data-col="${header}"]`); - const colType = th ? th.getAttribute('data-coltype') : (input ? input.getAttribute('data-coltype') : null); - const col = input ? input.getAttribute('data-col') : null; - const dataType = table.getAttribute('data-at-type'); - const td = document.createElement('td'); - console.log({header, input, col, colType, dataType}) - if (colType === 'element') { - // Find the checkbox in the add-row-controls row - console.log('Looking for checkbox with data-role:', col); - const checkboxInput = addRowControls.querySelector(`input[type="checkbox"][data-role="${header}"]`); - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.classList.add('checkbox-element'); - checkbox.setAttribute('data-role', col); - checkbox.name = `checkbox-${col}`; - console.log('Try to check for checkbox') - console.log({ checkboxInput }) - // Set checked state based on add-row-controls checkbox - if (checkboxInput && checkboxInput.checked) { - checkbox.checked = true; - console.log('Copyied checked state') - } - td.setAttribute('data-col', col); - td.setAttribute('data-coltype', 'element'); - td.setAttribute('data-type', dataType); - td.appendChild(checkbox); - } else if (colType === 'dropdown') { - console.log("Got to dropdown") - td.className = 'table-tagging-cell'; - td.setAttribute('data-col', col); - td.setAttribute('data-coltype', 'dropdown'); - td.setAttribute('data-type', dataType); - - // Show the selected value as plain text - const value = input ? input.value : ''; - console.log('Selected value:', input.value); - td.textContent = value; - } else if (colType === 'tagging' || colType === 'tagging_autocomplete') { - td.className = 'table-tagging-cell'; - td.setAttribute('data-col', col); - td.setAttribute('data-coltype', 'tagging'); - td.setAttribute('data-type', dataType); - // Tag UI - const tagsList = document.createElement('div'); - tagsList.className = 'tags-list'; - (addRowTags[col] || []).forEach(tag => { - const span = document.createElement('span'); - span.className = 'tag'; - span.setAttribute('data-tag', tag); - span.innerHTML = tag + ' ×'; - tagsList.appendChild(span); - }); - const input = document.createElement('input'); - input.className = 'tag-input'; - input.type = 'text'; - input.style.display = 'none'; - input.placeholder = 'Add tag and press Enter'; - td.appendChild(tagsList); - td.appendChild(input); - // Reset tags for next row - addRowTags[col] = []; - // Remove tag elements from add-row-controls - const addRowContainer = document.querySelector('.add-row-tags-container[data-col="' + col + '"]'); - if (addRowContainer) { - addRowContainer.querySelectorAll('.tag').forEach(tagEl => tagEl.remove()); - } - // If tagging_autocomplete, initialize autocomplete for this cell - if (colType === 'tagging_autocomplete') { - getSchema().then(schema => { - const autocompleteSource = schema["$defs"]?.[dataType]?.properties?.[col]?.items?.enum || []; - if (autocompleteSource.length > 0) { - setupTableTagAutocomplete({ cell: td, autocompleteSource }); - } - }); - } - } else { - td.textContent = input ? input.value : ''; - } - newRow.appendChild(td); - } - const deleteTd = document.createElement('td'); - deleteTd.innerHTML = ''; - newRow.appendChild(deleteTd); - - // Insert new row above add-row-controls - addRowControls.parentNode.insertBefore(newRow, addRowControls); - - initializeTableTaggingCells(); - - // Clear input fields - inputs.forEach(input => { - if (input.tagName === 'SELECT') { - input.selectedIndex = 0; - } else { - input.value = ''; - } + // Add Row functionality for all auto-property-tables + document.querySelectorAll(".add-row-btn").forEach(function (btn) { + btn.addEventListener("click", function () { + const key = btn.getAttribute("data-table-key"); + const table = document.getElementById(key + "Table"); + const hiddenInput = document.getElementById(key + "TableHiddenInput"); + if (!table || !hiddenInput) return; + + // Find the add-row-controls row + const addRowControls = table.querySelector( + 'tr.add-row-controls[data-table-key="' + key + '"]' + ); + if (!addRowControls) return; + + // Get input values + const inputs = addRowControls.querySelectorAll( + ".add-row-input, .add-row-tag-input, .add-row-dropdown-select" + ); + const values = Array.from(inputs).map((input) => input.value.trim()); + + // Prevent adding if all fields are empty + const allEmpty = values.every((val) => val === ""); + if (allEmpty) return; + + // Create new row + const newRow = document.createElement("tr"); + // Get column headers + const headers = Array.from(table.querySelectorAll("thead th")).map((th) => + th.getAttribute("data-col") + ); + // Only add data columns, skip the last header ("Delete") + for (let i = 0; i < headers.length - 1; i++) { + const header = headers[i]; + // Find the input for this column + const input = Array.from(inputs).find( + (inp) => + inp.getAttribute("data-col") === header && + !inp.classList.contains("invalid") + ); + const th = table.querySelector(`thead th[data-col="${header}"]`); + const colType = th + ? th.getAttribute("data-coltype") + : input + ? input.getAttribute("data-coltype") + : null; + const col = input ? input.getAttribute("data-col") : null; + const dataType = table.getAttribute("data-at-type"); + const td = document.createElement("td"); + td.className = "text-center"; + console.log({ header, input, col, colType, dataType }); + if (colType === "element") { + // Find the checkbox in the add-row-controls row + console.log("Looking for checkbox with data-role:", col); + const checkboxInput = addRowControls.querySelector( + `input[type="checkbox"][data-role="${header}"]` + ); + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.classList.add("checkbox-element"); + checkbox.setAttribute("data-role", col); + checkbox.name = `checkbox-${col}`; + console.log("Try to check for checkbox"); + console.log({ checkboxInput }); + // Set checked state based on add-row-controls checkbox + if (checkboxInput && checkboxInput.checked) { + checkbox.checked = true; + console.log("Copyied checked state"); + } + td.setAttribute("data-col", col); + td.setAttribute("data-coltype", "element"); + td.setAttribute("data-type", dataType); + td.appendChild(checkbox); + } else if (colType === "dropdown") { + console.log("Got to dropdown"); + td.className = "table-tagging-cell"; + td.setAttribute("data-col", col); + td.setAttribute("data-coltype", "dropdown"); + td.setAttribute("data-type", dataType); + + // Show the selected value as plain text + const value = input ? input.value : ""; + console.log("Selected value:", input.value); + td.textContent = value; + } else if ( + colType === "tagging" || + colType === "tagging_autocomplete" + ) { + td.className = "table-tagging-cell"; + td.setAttribute("data-col", col); + td.setAttribute("data-coltype", "tagging"); + td.setAttribute("data-type", dataType); + // Tag UI + const tagsList = document.createElement("div"); + tagsList.className = "tags-list"; + (addRowTags[col] || []).forEach((tag) => { + const span = document.createElement("span"); + span.className = "tag"; + span.setAttribute("data-tag", tag); + span.innerHTML = + tag + ' ×'; + tagsList.appendChild(span); + }); + const input = document.createElement("input"); + input.className = "tag-input"; + input.type = "text"; + input.style.display = "none"; + input.placeholder = "Add tag and press Enter"; + td.appendChild(tagsList); + td.appendChild(input); + // Reset tags for next row + addRowTags[col] = []; + // Remove tag elements from add-row-controls + const addRowContainer = document.querySelector( + '.add-row-tags-container[data-col="' + col + '"]' + ); + if (addRowContainer) { + addRowContainer + .querySelectorAll(".tag") + .forEach((tagEl) => tagEl.remove()); + } + // If tagging_autocomplete, initialize autocomplete for this cell + if (colType === "tagging_autocomplete") { + getSchema().then((schema) => { + const autocompleteSource = + schema["$defs"]?.[dataType]?.properties?.[col]?.items?.enum || + []; + if (autocompleteSource.length > 0) { + setupTableTagAutocomplete({ cell: td, autocompleteSource }); + } }); + } + } else { + td.textContent = input ? input.value : ""; + } + newRow.appendChild(td); + } + const deleteTd = document.createElement("td"); + deleteTd.className = "d-flex justify-content-center align-items-center"; + deleteTd.style.height = "50px"; + deleteTd.innerHTML = + ''; + newRow.appendChild(deleteTd); + + // Insert new row above add-row-controls + addRowControls.parentNode.insertBefore(newRow, addRowControls); + + initializeTableTaggingCells(); + enableEditableTagsInTable(); + // Clear input fields + inputs.forEach((input) => { + if (input.tagName === "SELECT") { + input.selectedIndex = 0; + } else { + input.value = ""; + } + }); - // Update hidden input - updateTableHiddenInput(key); + // Update hidden input + updateTableHiddenInput(key); - // Remove color - addRowControls.classList.remove('invalid'); - }); + // Remove color + addRowControls.classList.remove("invalid"); }); + }); + + // Store tags for each tagging column before row is added + const addRowTags = {}; + + // Initialize tagging for add-row-controls + document.querySelectorAll(".add-row-tags-container").forEach((container) => { + const col = container.getAttribute("data-col"); + addRowTags[col] = []; + const input = container.querySelector(".add-row-tag-input"); + const colType = container.getAttribute("data-coltype"); + const dataType = container.getAttribute("data-type"); + // --- Autocomplete setup --- + let autocompleteSource = []; + let suggestionsBox = createSuggestionsBox(container); + + if (colType === "tagging_autocomplete") { + getSchema().then((schema) => { + autocompleteSource = + schema["$defs"]?.[dataType]?.properties?.[col]?.items?.enum || []; + }); + + input.addEventListener("input", function () { + const query = input.value.trim().toLowerCase(); + suggestionsBox.innerHTML = ""; + if (!query || autocompleteSource.length === 0) { + suggestionsBox.style.display = "none"; + return; + } + const selectedTags = addRowTags[col]; + const filtered = autocompleteSource.filter( + (tag) => + tag.toLowerCase().startsWith(query) && !selectedTags.includes(tag) + ); + if (filtered.length === 0) { + suggestionsBox.style.display = "none"; + return; + } + filtered.forEach((tag) => { + const div = document.createElement("div"); + div.className = "suggestion-item"; + div.textContent = tag; + div.style.cursor = "pointer"; + div.onclick = function () { + input.value = tag; + input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" })); + suggestionsBox.style.display = "none"; + }; + suggestionsBox.appendChild(div); + }); + // Position suggestions below the input + const inputRect = input.getBoundingClientRect(); + suggestionsBox.style.left = inputRect.left + "px"; + suggestionsBox.style.top = inputRect.bottom + "px"; + suggestionsBox.style.width = input.offsetWidth + "px"; + suggestionsBox.style.display = "block"; + }); + + input.addEventListener("focus", function () { + suggestionsBox.innerHTML = ""; + if (!autocompleteSource.length) { + suggestionsBox.style.display = "none"; + return; + } + const query = input.value.trim().toLowerCase(); + const selectedTags = addRowTags[col]; + const filtered = autocompleteSource.filter( + (tag) => + !selectedTags.includes(tag) && + (query === "" || tag.toLowerCase().startsWith(query)) + ); + if (filtered.length === 0) { + suggestionsBox.style.display = "none"; + return; + } + filtered.forEach((tag) => { + const div = document.createElement("div"); + div.className = "suggestion-item"; + div.textContent = tag; + div.style.cursor = "pointer"; + div.onclick = function () { + input.value = tag; + input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" })); + suggestionsBox.style.display = "none"; + }; + suggestionsBox.appendChild(div); + }); + // Position suggestions below the input + updateSuggestionsBoxPosition(input, suggestionsBox); + suggestionsBox.style.display = "block"; + }); + + window.addEventListener( + "scroll", + () => updateSuggestionsBoxPosition(input, suggestionsBox), + true + ); + window.addEventListener("resize", () => + updateSuggestionsBoxPosition(input, suggestionsBox) + ); + + // Hide suggestions on blur/click outside + input.addEventListener("blur", function () { + setTimeout(() => { + suggestionsBox.style.display = "none"; + }, 200); + }); + } else if (colType === "dropdown") { + getSchema().then((schema) => { + const options = + schema["$defs"]?.[dataType]?.properties?.[col]?.enum || []; + const select = document.createElement("select"); + select.className = "add-row-dropdown-select"; + select.name = "selectElement"; + select.setAttribute("data-col", col); + select.setAttribute("data-type", dataType); + select.setAttribute("data-coltype", "dropdown"); + select.innerHTML = + '' + + options + .map((opt) => ``) + .join(""); + // Replace the input with the select + input.style.display = "none"; + container.appendChild(select); + + // On change, update addRowTags or values as needed + select.addEventListener("change", function () { + addRowTags[col] = [select.value]; + console.log("Selected value:", select.value); + }); + }); + } - // Store tags for each tagging column before row is added - const addRowTags = {}; - - // Initialize tagging for add-row-controls - document.querySelectorAll('.add-row-tags-container').forEach(container => { - const col = container.getAttribute('data-col'); - addRowTags[col] = []; - const input = container.querySelector('.add-row-tag-input'); - const colType = container.getAttribute('data-coltype'); - const dataType = container.getAttribute('data-type'); - // --- Autocomplete setup --- - let autocompleteSource = []; - let suggestionsBox = createSuggestionsBox(container); - - if (colType === 'tagging_autocomplete') { - getSchema().then(schema => { - autocompleteSource = schema["$defs"]?.[dataType]?.properties?.[col]?.items?.enum || []; - }); - - input.addEventListener('input', function () { - const query = input.value.trim().toLowerCase(); - suggestionsBox.innerHTML = ''; - if (!query || autocompleteSource.length === 0) { - suggestionsBox.style.display = 'none'; - return; - } - const selectedTags = addRowTags[col]; - const filtered = autocompleteSource.filter( - tag => tag.toLowerCase().startsWith(query) && !selectedTags.includes(tag) - ); - if (filtered.length === 0) { - suggestionsBox.style.display = 'none'; - return; - } - filtered.forEach(tag => { - const div = document.createElement('div'); - div.className = 'suggestion-item'; - div.textContent = tag; - div.style.cursor = 'pointer'; - div.onclick = function () { - input.value = tag; - input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); - suggestionsBox.style.display = 'none'; - }; - suggestionsBox.appendChild(div); - }); - // Position suggestions below the input - const inputRect = input.getBoundingClientRect(); - suggestionsBox.style.left = inputRect.left + 'px'; - suggestionsBox.style.top = inputRect.bottom + 'px'; - suggestionsBox.style.width = input.offsetWidth + 'px'; - suggestionsBox.style.display = 'block'; - }); - - input.addEventListener('focus', function () { - suggestionsBox.innerHTML = ''; - if (!autocompleteSource.length) { - suggestionsBox.style.display = 'none'; - return; - } - const query = input.value.trim().toLowerCase(); - const selectedTags = addRowTags[col]; - const filtered = autocompleteSource.filter( - tag => !selectedTags.includes(tag) && (query === "" || tag.toLowerCase().startsWith(query)) - ); - if (filtered.length === 0) { - suggestionsBox.style.display = 'none'; - return; - } - filtered.forEach(tag => { - const div = document.createElement('div'); - div.className = 'suggestion-item'; - div.textContent = tag; - div.style.cursor = 'pointer'; - div.onclick = function () { - input.value = tag; - input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); - suggestionsBox.style.display = 'none'; - }; - suggestionsBox.appendChild(div); - }); - // Position suggestions below the input - updateSuggestionsBoxPosition(input, suggestionsBox); - suggestionsBox.style.display = 'block'; - }); - - window.addEventListener('scroll', () => updateSuggestionsBoxPosition(input, suggestionsBox), true); - window.addEventListener('resize', () => updateSuggestionsBoxPosition(input, suggestionsBox)); - - // Hide suggestions on blur/click outside - input.addEventListener('blur', function () { - setTimeout(() => { suggestionsBox.style.display = 'none'; }, 200); - }); - } else if (colType === 'dropdown') { - getSchema().then(schema => { - const options = schema["$defs"]?.[dataType]?.properties?.[col]?.enum || []; - const select = document.createElement('select'); - select.className = 'add-row-dropdown-select'; - select.name = 'selectElement'; - select.setAttribute('data-col', col); - select.setAttribute('data-type', dataType); - select.setAttribute('data-coltype', 'dropdown'); - select.innerHTML = '' + - options.map(opt => ``).join(''); - // Replace the input with the select - input.style.display = 'none'; - container.appendChild(select); - - // On change, update addRowTags or values as needed - select.addEventListener('change', function () { - addRowTags[col] = [select.value]; - console.log('Selected value:', select.value); - }); - }); - } - - // Add tag on Enter - input.addEventListener('keydown', function (e) { - if (e.key === 'Enter' && input.value.trim() !== '') { - e.preventDefault(); - const tag = input.value.trim(); - if (colType === 'tagging_autocomplete') { - if (autocompleteSource.includes(tag)) { - if (!addRowTags[col].includes(tag)) { - addRowTags[col].push(tag); - - // Create tag element - const span = document.createElement('span'); - span.className = 'tag'; - span.setAttribute('data-tag', tag); - span.innerHTML = tag + ' ×'; - container.insertBefore(span, input); - } - input.value = ''; - if (suggestionsBox) suggestionsBox.style.display = 'none'; - } else { - showInvalidTagMessage(container, input, "Please select a value from the list."); - input.classList.add("invalid"); - setTimeout(() => input.classList.remove("invalid"), 1000); - input.value = ''; - } - } else { - // For plain tagging, just add the tag - if (!addRowTags[col].includes(tag)) { - addRowTags[col].push(tag); - const span = document.createElement('span'); - span.className = 'tag'; - span.setAttribute('data-tag', tag); - span.innerHTML = tag + ' ×'; - container.insertBefore(span, input); - } - input.value = ''; - } - } - }); - - // Remove tag on click - container.addEventListener('click', function (e) { - if (e.target.classList.contains('remove-tag')) { - const tag = e.target.getAttribute('data-tag'); - addRowTags[col] = addRowTags[col].filter(t => t !== tag); - e.target.parentElement.remove(); - } - }); - }); - - // functionanilties within all auto-property-tables - document.querySelectorAll('table.auto-property-table').forEach(function (table) { - table.addEventListener('click', function (e) { - // Delete rows - if (e.target.classList.contains('delete-row-btn')) { - const row = e.target.closest('tr'); - if (row) { - row.remove(); - // Update the hidden input - const tableId = table.id; - if (tableId && tableId.endsWith('Table')) { - const key = tableId.replace(/Table$/, ''); - updateTableHiddenInput(key); - } - } + // Add tag on Enter + input.addEventListener("keydown", function (e) { + if (e.key === "Enter" && input.value.trim() !== "") { + e.preventDefault(); + const tag = input.value.trim(); + if (colType === "tagging_autocomplete") { + if (autocompleteSource.includes(tag)) { + if (!addRowTags[col].includes(tag)) { + addRowTags[col].push(tag); + + // Create tag element + const span = document.createElement("span"); + span.className = "tag"; + span.setAttribute("data-tag", tag); + span.innerHTML = + tag + + ' ×'; + container.insertBefore(span, input); } + input.value = ""; + if (suggestionsBox) suggestionsBox.style.display = "none"; + } else { + showInvalidTagMessage( + container, + input, + "Please select a value from the list." + ); + input.classList.add("invalid"); + setTimeout(() => input.classList.remove("invalid"), 1000); + input.value = ""; + } + } else { + // For plain tagging, just add the tag + if (!addRowTags[col].includes(tag)) { + addRowTags[col].push(tag); + const span = document.createElement("span"); + span.className = "tag"; + span.setAttribute("data-tag", tag); + span.innerHTML = + tag + ' ×'; + container.insertBefore(span, input); + } + input.value = ""; + } + } + }); - // Update other fields - // Only allow editing on that is not the last column (delete icon) - const cell = e.target.closest('td'); - if (!cell) return; - if (cell.classList.contains('table-tagging-cell')) return; - if (cell.classList.contains('table-tagging-cell')) return; - const row = cell.parentElement; - const allCells = Array.from(row.children); - // Don't edit the last cell (delete icon) - if (allCells.indexOf(cell) === allCells.length - 1) return; - // Prevent multiple inputs - if (cell.querySelector('input')) return; - - const oldValue = cell.textContent; - cell.innerHTML = ''; - const input = document.createElement('input'); - input.type = 'text'; - input.value = oldValue; - input.style.width = '100%'; - input.style.boxSizing = 'border-box'; - cell.appendChild(input); - input.focus(); - - // Save on blur or Enter - function save() { - cell.textContent = input.value; - // Update the hidden input for this table - const tableId = table.id; - if (tableId && tableId.endsWith('Table')) { - const key = tableId.replace(/Table$/, ''); - updateTableHiddenInput(key); - } + // Remove tag on click + container.addEventListener("click", function (e) { + if (e.target.classList.contains("remove-tag")) { + const tag = e.target.getAttribute("data-tag"); + addRowTags[col] = addRowTags[col].filter((t) => t !== tag); + e.target.parentElement.remove(); + } + }); + }); + + // functionanilties within all auto-property-tables + document + .querySelectorAll("table.auto-property-table") + .forEach(function (table) { + table.addEventListener("click", function (e) { + // Delete rows + if (e.target.classList.contains("delete-row-btn")) { + const row = e.target.closest("tr"); + if (row) { + row.remove(); + // Update the hidden input + const tableId = table.id; + if (tableId && tableId.endsWith("Table")) { + const key = tableId.replace(/Table$/, ""); + updateTableHiddenInput(key); } - input.addEventListener('blur', save); - input.addEventListener('keydown', function (evt) { - if (evt.key === 'Enter') { - input.blur(); - } else if (evt.key === 'Escape') { - cell.textContent = oldValue; - } - }); + } + } + // Update other fields + // Only allow editing on that is not the last column (delete icon) + const cell = e.target.closest("td"); + if (!cell) return; + if (cell.classList.contains("table-tagging-cell")) return; + if (cell.classList.contains("table-tagging-cell")) return; + const row = cell.parentElement; + const allCells = Array.from(row.children); + // Don't edit the last cell (delete icon) + if (allCells.indexOf(cell) === allCells.length - 1) return; + // Prevent multiple inputs + if (cell.querySelector("input")) return; + + const oldValue = cell.textContent; + cell.innerHTML = ""; + const input = document.createElement("input"); + input.type = "text"; + input.value = oldValue; + input.style.width = "100%"; + input.style.boxSizing = "border-box"; + cell.appendChild(input); + input.focus(); + + // Save on blur or Enter + function save() { + cell.textContent = input.value; + // Update the hidden input for this table + const tableId = table.id; + if (tableId && tableId.endsWith("Table")) { + const key = tableId.replace(/Table$/, ""); + updateTableHiddenInput(key); + } + } + input.addEventListener("blur", save); + input.addEventListener("keydown", function (evt) { + if (evt.key === "Enter") { + input.blur(); + } else if (evt.key === "Escape") { + cell.textContent = oldValue; + } }); + }); }); - initializeTableTaggingCells(); + initializeTableTaggingCells(); - // Hide all tag-inputs when clicking outside - document.addEventListener('click', function () { - document.querySelectorAll('td.table-tagging-cell .tag-input').forEach(function (input) { - input.style.display = 'none'; - }); - }); + // Hide all tag-inputs when clicking outside + document.addEventListener("click", function () { + document + .querySelectorAll("td.table-tagging-cell .tag-input") + .forEach(function (input) { + input.style.display = "none"; + }); + }); - highlightEmptyAddRowControls(); + highlightEmptyAddRowControls(); } - // Add function to color add items when element is required or recommended and empty - export function highlightEmptyAddRowControls() { - getSchema().then(schema => { - const { required, recommended } = fetchRequiredAndRecommendedFields(schema); - const allMandatory = [...required, ...recommended]; - - document.querySelectorAll('table.auto-property-table').forEach(table => { - const tableId = table.id; - if (!tableId || !tableId.endsWith('Table')) return; - const key = tableId.replace(/Table$/, ''); - - // Find the corresponding add-row-controls - const addRowControls = document.querySelector(`.add-row-controls[data-table-key="${key}"]`); - if (!addRowControls) return; - - if (allMandatory.includes(key)) { - const tbody = table.querySelector('tbody'); - const rows = tbody ? tbody.querySelectorAll('tr') : []; - if (rows.length === 0) { - addRowControls.classList.add('invalid'); - } else { - addRowControls.classList.remove('invalid'); - } - } else { - addRowControls.classList.remove('invalid'); - } - }); - }); +// Add function to color add items when element is required or recommended and empty +export function highlightEmptyAddRowControls() { + getSchema().then((schema) => { + const { required, recommended } = fetchRequiredAndRecommendedFields(schema); + const allMandatory = [...required, ...recommended]; + + document.querySelectorAll("table.auto-property-table").forEach((table) => { + const tableId = table.id; + if (!tableId || !tableId.endsWith("Table")) return; + const key = tableId.replace(/Table$/, ""); + + // Find the corresponding add-row-controls + const addRowControls = document.querySelector( + `.add-row-controls[data-table-key="${key}"]` + ); + if (!addRowControls) return; + + if (allMandatory.includes(key)) { + const tbody = table.querySelector("tbody"); + const rows = tbody ? tbody.querySelectorAll("tr") : []; + if (rows.length === 0) { + addRowControls.classList.add("invalid"); + } else { + addRowControls.classList.remove("invalid"); + } + } else { + addRowControls.classList.remove("invalid"); + } + }); + }); } - // Enable tag editing in table cells - export function initializeTableTaggingCells() { - document.querySelectorAll('td.table-tagging-cell').forEach(function (cell) { - // Prevent double-binding - if (cell.dataset.taggingInitialized) return; - cell.dataset.taggingInitialized = "true"; - - const tagsList = cell.querySelector('.tags-list'); - let input = cell.querySelector('.tag-input'); - if (!input) { - input = document.createElement('input'); - input.className = 'tag-input'; - input.type = 'text'; - input.style.display = 'none'; - input.placeholder = 'Add tag and press Enter'; - cell.appendChild(input); +// Enable tag editing in table cells +export function initializeTableTaggingCells() { + document.querySelectorAll("td.table-tagging-cell").forEach(function (cell) { + // Prevent double-binding + if (cell.dataset.taggingInitialized) return; + cell.dataset.taggingInitialized = "true"; + + const tagsList = cell.querySelector(".tags-list"); + let input = cell.querySelector(".tag-input"); + if (!input) { + input = document.createElement("input"); + input.className = "tag-input"; + input.type = "text"; + input.style.display = "none"; + input.placeholder = "Add tag and press Enter"; + cell.appendChild(input); + } + // ------------------------------Contributors table ends----------------------------//// + + //--------------- --- Autocomplete logic: fetch source and setup --------------// + + // You can set data-autocomplete-source on the cell or column header, or fetch from schema + + const col = cell.getAttribute("data-col"); + const colType = cell.getAttribute("data-coltype"); + const dataType = cell.getAttribute("data-type"); + // Example: fetch from schema if available + if (colType == "tagging_autocomplete") { + getSchema().then((schema) => { + autocompleteSource = + schema["$defs"]?.[dataType]?.properties?.[col]?.items?.enum || []; + if (autocompleteSource.length > 0) { + setupTableTagAutocomplete({ cell, autocompleteSource }); } -// ------------------------------Contributors table ends----------------------------//// - -//--------------- --- Autocomplete logic: fetch source and setup --------------// - - // You can set data-autocomplete-source on the cell or column header, or fetch from schema - - const col = cell.getAttribute('data-col'); - const colType = cell.getAttribute('data-coltype'); - const dataType = cell.getAttribute('data-type'); - // Example: fetch from schema if available - if (colType == 'tagging_autocomplete') { - getSchema().then(schema => { - autocompleteSource = schema["$defs"]?.[dataType]?.properties?.[col]?.items?.enum || []; - if (autocompleteSource.length > 0) { - setupTableTagAutocomplete({ cell, autocompleteSource }); - } - }); - } else if (colType === 'dropdown') { - // Show value as plain text initially - const currentValue = cell.getAttribute('data-value') || cell.textContent.trim() || ''; - cell.innerHTML = ''; - cell.textContent = currentValue; - - // Only show dropdown on cell click - cell.addEventListener('click', function handleDropdownCellClick(e) { - // Prevent multiple dropdowns - if (cell.querySelector('select')) return; - getSchema().then(schema => { - const options = schema["$defs"]?.[dataType]?.properties?.[col]?.enum || []; - const select = document.createElement('select'); - select.className = 'table-dropdown-select'; - select.name = 'ChangingSelect' - select.innerHTML = '' + - options.map(opt => ``).join(''); - select.value = currentValue; - - // Replace cell content with select - cell.innerHTML = ''; - cell.appendChild(select); - select.focus(); - - // On change or blur, update cell and data - function finalizeSelection() { - const selectedValue = select.value; - cell.setAttribute('data-value', selectedValue); - cell.innerHTML = selectedValue; - - // Remove this event listener to avoid duplicate dropdowns - cell.removeEventListener('click', handleDropdownCellClick); - - // Re-attach the click event for future edits - setTimeout(() => { - cell.addEventListener('click', handleDropdownCellClick); - }, 0); - - // Update the hidden input and main JSON - const table = cell.closest('table'); - if (table && table.id.endsWith('Table')) { - const key = table.id.replace(/Table$/, ''); - updateTableHiddenInput(key); - } - } - - select.addEventListener('change', finalizeSelection); - select.addEventListener('blur', finalizeSelection); - }); - - // Remove this event listener to prevent re-entry until finished - cell.removeEventListener('click', handleDropdownCellClick); - }); + }); + } else if (colType === "dropdown") { + // Show value as plain text initially + const currentValue = + cell.getAttribute("data-value") || cell.textContent.trim() || ""; + cell.innerHTML = ""; + cell.textContent = currentValue; + + // Only show dropdown on cell click + cell.addEventListener("click", function handleDropdownCellClick(e) { + // Prevent multiple dropdowns + if (cell.querySelector("select")) return; + getSchema().then((schema) => { + const options = + schema["$defs"]?.[dataType]?.properties?.[col]?.enum || []; + const select = document.createElement("select"); + select.className = "table-dropdown-select"; + select.name = "ChangingSelect"; + select.innerHTML = + '' + + options + .map((opt) => ``) + .join(""); + select.value = currentValue; + + // Replace cell content with select + cell.innerHTML = ""; + cell.appendChild(select); + select.focus(); + + // On change or blur, update cell and data + function finalizeSelection() { + const selectedValue = select.value; + cell.setAttribute("data-value", selectedValue); + cell.innerHTML = selectedValue; + + // Remove this event listener to avoid duplicate dropdowns + cell.removeEventListener("click", handleDropdownCellClick); + + // Re-attach the click event for future edits + setTimeout(() => { + cell.addEventListener("click", handleDropdownCellClick); + }, 0); + + // Update the hidden input and main JSON + const table = cell.closest("table"); + if (table && table.id.endsWith("Table")) { + const key = table.id.replace(/Table$/, ""); + updateTableHiddenInput(key); + } + } - return; // Skip further tag logic for dropdowns - } + select.addEventListener("change", finalizeSelection); + select.addEventListener("blur", finalizeSelection); + }); + // Remove this event listener to prevent re-entry until finished + cell.removeEventListener("click", handleDropdownCellClick); + }); - // Show input when cell is clicked (not on tag or remove) - cell.addEventListener('click', function (e) { - if (e.target.classList.contains('remove-tag') || e.target.classList.contains('tag')) return; - input.style.display = 'inline-block'; - input.focus(); - e.stopPropagation(); - }); + return; // Skip further tag logic for dropdowns + } - // Hide input when focus is lost - input.addEventListener('blur', function () { - setTimeout(function () { input.style.display = 'none'; }, 100); - }); + // Show input when cell is clicked (not on tag or remove) + cell.addEventListener("click", function (e) { + if ( + e.target.classList.contains("remove-tag") || + e.target.classList.contains("tag") + ) + return; + input.style.display = "inline-block"; + input.focus(); + e.stopPropagation(); + }); - // Add tag on Enter - input.addEventListener('keydown', function (e) { - if (e.key === 'Enter' && input.value.trim() !== '') { - e.preventDefault(); - e.stopPropagation(); - const tag = input.value.trim(); - if ([...tagsList.querySelectorAll('.tag')].some(t => t.textContent.trim() === tag + '×')) { - input.value = ''; - return; - } - // Get autocompleteSource for this cell/column - let autocompleteSource = []; - const col = cell.getAttribute('data-col'); - const dataType = cell.getAttribute('data-type'); - const colType = cell.getAttribute('data-coltype'); - if (colType === 'tagging_autocomplete') { - // You may want to cache this for performance - getSchema().then(schema => { - autocompleteSource = schema["$defs"]?.[dataType]?.properties?.[col]?.items?.enum || []; - if (!autocompleteSource.includes(tag)) { - showInvalidTagMessage(cell, input, "Please select a value from the list."); - input.classList.add("invalid"); - setTimeout(() => input.classList.remove("invalid"), 1000); - input.value = ''; - return; - } - }); - } - const span = document.createElement('span'); - span.className = 'tag'; - span.setAttribute('data-tag', tag); - span.innerHTML = tag + ' ×'; - tagsList.appendChild(span); - input.value = ''; - const table = cell.closest('table'); - if (table && table.id.endsWith('Table')) { - const key = table.id.replace(/Table$/, ''); - updateTableHiddenInput(key); - } - } - }); + // Hide input when focus is lost + input.addEventListener("blur", function () { + setTimeout(function () { + input.style.display = "none"; + }, 100); + }); - // Remove tag on click (cell-local) - tagsList.addEventListener('click', function (e) { - if (e.target.classList.contains('remove-tag')) { - e.target.parentElement.remove(); - input.style.display = 'inline-block'; - input.focus(); - const table = cell.closest('table'); - if (table && table.id.endsWith('Table')) { - const key = table.id.replace(/Table$/, ''); - updateTableHiddenInput(key); - } - e.stopPropagation(); + // Add tag on Enter + input.addEventListener("keydown", function (e) { + if (e.key === "Enter" && input.value.trim() !== "") { + e.preventDefault(); + e.stopPropagation(); + const tag = input.value.trim(); + if ( + [...tagsList.querySelectorAll(".tag")].some( + (t) => t.textContent.trim() === tag + "×" + ) + ) { + input.value = ""; + return; + } + // Get autocompleteSource for this cell/column + let autocompleteSource = []; + const col = cell.getAttribute("data-col"); + const dataType = cell.getAttribute("data-type"); + const colType = cell.getAttribute("data-coltype"); + if (colType === "tagging_autocomplete") { + // You may want to cache this for performance + getSchema().then((schema) => { + autocompleteSource = + schema["$defs"]?.[dataType]?.properties?.[col]?.items?.enum || []; + if (!autocompleteSource.includes(tag)) { + showInvalidTagMessage( + cell, + input, + "Please select a value from the list." + ); + input.classList.add("invalid"); + setTimeout(() => input.classList.remove("invalid"), 1000); + input.value = ""; + return; } - }); + }); + } + const span = document.createElement("span"); + span.className = "tag"; + span.setAttribute("data-tag", tag); + span.innerHTML = + tag + ' ×'; + tagsList.appendChild(span); + input.value = ""; + const table = cell.closest("table"); + if (table && table.id.endsWith("Table")) { + const key = table.id.replace(/Table$/, ""); + updateTableHiddenInput(key); + } + } }); -} \ No newline at end of file + + // Remove tag on click (cell-local) + tagsList.addEventListener("click", function (e) { + if (e.target.classList.contains("remove-tag")) { + e.target.parentElement.remove(); + input.style.display = "inline-block"; + input.focus(); + const table = cell.closest("table"); + if (table && table.id.endsWith("Table")) { + const key = table.id.replace(/Table$/, ""); + updateTableHiddenInput(key); + } + e.stopPropagation(); + } + }); + }); +} diff --git a/static/foundation/js/vendor/tagging.js b/static/foundation/js/vendor/tagging.js index 02aed66..5ff456c 100644 --- a/static/foundation/js/vendor/tagging.js +++ b/static/foundation/js/vendor/tagging.js @@ -247,7 +247,11 @@ export function setupTagging({ // Remove tag logic container.addEventListener("click", (e) => { - if (e.target.classList.contains("remove-tag")) { + const isRemove = e.target.classList.contains("remove-tag"); + const isTag = e.target.classList.contains("tag"); + + // 🔴 Remove tag logic + if (isRemove) { const value = e.target.dataset.value; if (taggingType === "tagging_object") { selectedTags = selectedTags.filter((item) => item.identifier !== value); @@ -257,8 +261,24 @@ export function setupTagging({ e.target.parentElement.remove(); updateHidden(); } - if (e.target.classList.contains("acknowledge-tag")) { - e.target.parentElement.remove(); + + // 🟡 Edit tag logic + if (isTag) { + const value = e.target.dataset.value; + + // Remove from list + if (taggingType === "tagging_object") { + selectedTags = selectedTags.filter((item) => item.identifier !== value); + } else { + selectedTags = selectedTags.filter((tag) => tag !== value); + } + + updateHidden(); + renderTags(); + + // Populate input for editing + input.value = value; + input.focus(); } }); @@ -308,6 +328,8 @@ export function setupTagging({ // Initial render renderTags(); + enableEditableTagsInTable(); + setupAcknowledgeTags(); } export function initializeTaggingFields() { @@ -574,3 +596,62 @@ export function setupTableTagAutocomplete({ cell, autocompleteSource }) { container: cell, }); } +export function enableEditableTagsInTable() { + document.querySelectorAll(".table-tagging-cell").forEach((cell) => { + const tagList = cell.querySelector(".tags-list"); + const input = cell.querySelector(".tag-input"); + + if (!tagList || !input) return; + + // ✅ Only add one listener (delegated) + if (!tagList.dataset.bound) { + tagList.addEventListener("click", (e) => { + if (e.target.closest(".highlight-tag")) return; + + const tagEl = e.target.closest(".tag"); + const removeEl = e.target.closest(".remove-tag"); + + if (!tagEl || !tagList.contains(tagEl)) return; + + const tagValue = tagEl.dataset.tag; + + if (removeEl) { + tagEl.remove(); // delete tag + return; + } + //First click both removes tag and enables input + e.preventDefault(); + e.stopPropagation(); + // Edit tag + tagEl.remove(); + input.style.display = "inline-block"; + input.value = tagValue; + input.focus(); + }); + + tagList.dataset.bound = "true"; // Prevent multiple bindings + } + + // Hide input when empty and blurred + if (!input.dataset.bound) { + input.addEventListener("blur", () => { + if (input.value.trim() === "") { + input.style.display = "none"; + } + }); + + input.dataset.bound = "true"; // ✅ Prevent rebinding blur + } + }); +} +function setupAcknowledgeTags() { + document.querySelectorAll(".acknowledge-tag").forEach((el) => { + el.addEventListener("click", (e) => { + const wrapper = el.closest(".highlight-tag"); + if (wrapper) { + wrapper.style.display = "none"; + e.stopPropagation(); // prevent tag click logic from hijacking this + } + }); + }); +} diff --git a/static/foundation/js/vendor/ui.js b/static/foundation/js/vendor/ui.js index 3627f84..1ae73b7 100644 --- a/static/foundation/js/vendor/ui.js +++ b/static/foundation/js/vendor/ui.js @@ -103,10 +103,15 @@ export function setupUI() { // Initialize the state on page load window.onload = function () { + const toggleSwitch = document.getElementById("toggleSwitch"); + if (window.screen.width <= 990) { + toggleSwitch.checked = false; + } + toggleSection(); - document - .getElementById("toggleSwitch") - .addEventListener("change", toggleSection); + + toggleSwitch.addEventListener("change", toggleSection); + window.addEventListener("resize", toggleSection); }; //highlightsURLs highlightEditableUrls(urlInputs); @@ -137,6 +142,13 @@ function toggleSection() { var toggleSwitch = document.getElementById("toggleSwitch"); var personInfoElements = document.querySelectorAll(".person-info"); // Select all elements with the class 'person-info' + if (window.screen.width <= 990 && toggleSwitch.checked == false) { + formContainer.style.height = "100%"; + } else if (window.screen.width <= 990 && toggleSwitch.checked) { + formContainer.style.height = "50%"; + } else { + formContainer.style.height = "100%"; + } if (toggleSwitch.checked) { metadataFormDisplay.style.display = "block"; formContainer.classList.remove("col-lg-12"); @@ -322,4 +334,17 @@ function initAutoCloseCollapses(collapseSelector = ".collapsible-content") { }); }); }); + // Add click listener to close collapses when clicking outside + document.addEventListener("click", function (e) { + const isInsideToggle = e.target.closest('[data-bs-toggle="collapse"]'); + const isInsideCollapse = e.target.closest(collapseSelector); + + if (!isInsideToggle && !isInsideCollapse) { + document + .querySelectorAll(`${collapseSelector}.show`) + .forEach((openCollapse) => { + new bootstrap.Collapse(openCollapse, { toggle: false }).hide(); + }); + } + }); } From d951b8271a99b03da2166474234cb9c03a4816d0 Mon Sep 17 00:00:00 2001 From: Sundraiz-Shah Date: Mon, 11 Aug 2025 11:14:58 +0200 Subject: [PATCH 15/26] Merge changes --- hermes.toml | 7 +- static/foundation/js/vendor/download.js | 209 ++++++------ static/foundation/js/vendor/dropdown-utils.js | 106 +++--- static/foundation/js/vendor/form-utils.js | 53 +-- static/foundation/js/vendor/init.js | 19 +- ...foundation.js => old_script_foundation.js} | 0 static/foundation/js/vendor/schema-utils.js | 303 ++++++++++-------- 7 files changed, 384 insertions(+), 313 deletions(-) rename static/foundation/js/vendor/{foundation.js => old_script_foundation.js} (100%) diff --git a/hermes.toml b/hermes.toml index da154b7..782642e 100644 --- a/hermes.toml +++ b/hermes.toml @@ -1,10 +1,5 @@ - -# SPDX-FileCopyrightText: 2023 German Aerospace Center (DLR) -# -# SPDX-License-Identifier: CC0-1.0 - [harvest] -sources = [ "githublab", "cff", "codemeta", ] # ordered priority (first one is most important) +sources = [ "githublab", "cff", "codemeta",] [deposit] target = "invenio_rdm" diff --git a/static/foundation/js/vendor/download.js b/static/foundation/js/vendor/download.js index 36a914d..bf89094 100644 --- a/static/foundation/js/vendor/download.js +++ b/static/foundation/js/vendor/download.js @@ -5,125 +5,144 @@ Cleans out empty fields Formats and downloads the JSON as a codemeta.json file */ -import { keysMatchRecursive } from './schema-utils.js'; +import { keysMatchRecursive } from "./schema-utils.js"; -const JsonSchema = '/static/schema/codemeta_schema.json'; +const JsonSchema = "/static/schema/codemeta_schema.json"; const metadataJson = document.getElementById("metadata-json"); const downloadButton = document.getElementById("downloadButton"); const downloadBtn = document.getElementById("downloadBtn"); // Function to trigger file download from JSON textarea export function setupDownload() { - downloadButton.addEventListener("click", (event) => { - downloadFile(event); - }); - downloadBtn.addEventListener("click", (event) => { - downloadFile(event); - }); + downloadButton.addEventListener("click", (event) => { + downloadFile(event); + }); + downloadBtn.addEventListener("click", (event) => { + downloadFile(event); + }); } // Function to handle download with validation function downloadFile(event) { - event.preventDefault(); + event.preventDefault(); - try { - const data = metadataJson.value; - const entered_metadata = JSON.parse(data); // Move inside try block - const metadata = getCleanedMetadata(entered_metadata); - const jsonKeys = Object.keys(metadata); // Extract keys from received JSON + try { + const data = metadataJson.value; + const entered_metadata = JSON.parse(data); // Move inside try block + const metadata = getCleanedMetadata(entered_metadata); + const jsonKeys = Object.keys(metadata); // Extract keys from received JSON - let repoName = "metadata"; // Default name + let repoName = "metadata"; // Default name - fetch(JsonSchema) - .then(response => response.json()) - .then(schema => { - // Extract all property keys - const allowedKeys = Object.keys(schema.properties || {}); - const requiredKeys = schema.required || []; + fetch(JsonSchema) + .then((response) => response.json()) + .then((schema) => { + // Extract all property keys + const allowedKeys = Object.keys(schema.properties || {}); + const requiredKeys = schema.required || []; - // Get key comparison result - const keyCheck = keysMatchRecursive(allowedKeys, requiredKeys, metadata, schema); + // Get key comparison result + const keyCheck = keysMatchRecursive( + allowedKeys, + requiredKeys, + metadata, + schema + ); - if (!keyCheck.isMatch) { - let errorMessage = ""; - if (keyCheck.missingKeys.length > 0) { - errorMessage += `Not all required elements were filled. Please add content to the following elements:\n\n ${keyCheck.missingKeys.join(", ")}\n`; - } - if (keyCheck.extraKeys.length > 0) { - errorMessage += `There are elements which are not part of the standard. Please remove the following elements:\n\n: ${keyCheck.extraKeys.join(", ")}\n`; - } - if (keyCheck.nestedErrors.length > 0) { - errorMessage += `\nNested Errors:\n${keyCheck.nestedErrors.join("\n")}`; - } - alert(errorMessage); - } else { - jsonPrettier(repoName, metadata); - } - }) - .catch(error => { - console.error('Error loading schema:', error); - }); - } - catch (e) { - let errorMessage = `\n\nCurrent Metadata:\n${JSON.stringify(metadata, null, 2)}`; - alert(errorMessage); - alert("Invalid JSON. Please check your syntax:metadata"); - console.error("JSON Parsing Error:", e); - } + if (!keyCheck.isMatch) { + let errorMessage = ""; + if (keyCheck.missingKeys.length > 0) { + errorMessage += `Not all required elements were filled. Please add content to the following elements:\n\n ${keyCheck.missingKeys.join( + ", " + )}\n`; + } + if (keyCheck.extraKeys.length > 0) { + errorMessage += `There are elements which are not part of the standard. Please remove the following elements:\n\n: ${keyCheck.extraKeys.join( + ", " + )}\n`; + } + if (keyCheck.nestedErrors.length > 0) { + errorMessage += `\nNested Errors:\n${keyCheck.nestedErrors.join( + "\n" + )}`; + } + alert(errorMessage); + } else { + jsonPrettier(repoName, metadata); + } + }) + .catch((error) => { + console.error("Error loading schema:", error); + }); + } catch (e) { + let errorMessage = `\n\nCurrent Metadata:\n${JSON.stringify( + metadata, + null, + 2 + )}`; + alert(errorMessage); + alert("Invalid JSON. Please check your syntax:metadata"); + console.error("JSON Parsing Error:", e); + } } // Provide metadata as download function jsonPrettier(repoName, metadata) { - let validJson; - const values = Object.values(metadata).slice(0, 2); - // Check the conditions - if (values[0] !== "https://w3id.org/codemeta/3.0" || values[1] !== "SoftwareSourceCode") { - // Update the first two keys in the object - const keys = Object.keys(metadata); - if (keys.length >= 2) { - metadata[keys[0]] = "https://w3id.org/codemeta/3.0"; // Update the first key's value - metadata[keys[1]] = "SoftwareSourceCode"; // Update the second key's value - } + let validJson; + const values = Object.values(metadata).slice(0, 2); + // Check the conditions + if ( + values[0] !== "https://w3id.org/codemeta/3.0" || + values[1] !== "SoftwareSourceCode" + ) { + // Update the first two keys in the object + const keys = Object.keys(metadata); + if (keys.length >= 2) { + metadata[keys[0]] = "https://w3id.org/codemeta/3.0"; // Update the first key's value + metadata[keys[1]] = "SoftwareSourceCode"; // Update the second key's value } + } - if (metadata.name) { - repoName = metadata.name; - validJson = JSON.stringify(metadata, null, 2); - } - const fileName = `${repoName}/codemeta.json`; - const blob = new Blob([validJson], { type: "application/json" }); - const link = document.createElement("a"); - link.href = URL.createObjectURL(blob); - link.innerHTML = "Download JSON"; - link.setAttribute("download", fileName); - document.body.appendChild(link); - link.click(); - setTimeout(() => { - URL.revokeObjectURL(link.href); - link.parentNode.removeChild(link); - }, 100); + if (metadata.name) { + repoName = metadata.name; + validJson = JSON.stringify(metadata, null, 2); + } + const fileName = `${repoName}/codemeta.json`; + const blob = new Blob([validJson], { type: "application/json" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.innerHTML = "Download JSON"; + link.setAttribute("download", fileName); + document.body.appendChild(link); + link.click(); + setTimeout(() => { + URL.revokeObjectURL(link.href); + link.parentNode.removeChild(link); + }, 100); } // Function to create a cleaned copy of an object by removing empty entries function getCleanedMetadata(obj) { - const cleanedObj = Array.isArray(obj) ? [] : {}; - Object.keys(obj).forEach(key => { - if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) { - // Recursively clean nested objects - const cleanedNested = getCleanedMetadata(obj[key]); - if (Object.keys(cleanedNested).length > 0) { - cleanedObj[key] = cleanedNested; - } - } else if (Array.isArray(obj[key])) { - // Remove empty elements from arrays - const cleanedArray = obj[key].filter(item => item !== null && item !== undefined && item !== ''); - if (cleanedArray.length > 0) { - cleanedObj[key] = cleanedArray; - } - } else if (obj[key] !== null && obj[key] !== undefined && obj[key] !== '') { - // Copy non-empty values - cleanedObj[key] = obj[key]; - } - }); - return cleanedObj; + const cleanedObj = Array.isArray(obj) ? [] : {}; + Object.keys(obj).forEach((key) => { + if (obj[key] && typeof obj[key] === "object" && !Array.isArray(obj[key])) { + // Recursively clean nested objects + const cleanedNested = getCleanedMetadata(obj[key]); + if (Object.keys(cleanedNested).length > 0) { + cleanedObj[key] = cleanedNested; + } + } else if (Array.isArray(obj[key])) { + // Remove empty elements from arrays + const cleanedArray = obj[key].filter( + (item) => item !== null && item !== undefined && item !== "" + ); + if (cleanedArray.length > 0) { + cleanedObj[key] = cleanedArray; + } + } else if (obj[key] !== null && obj[key] !== undefined && obj[key] !== "") { + // Copy non-empty values + cleanedObj[key] = obj[key]; + } + }); + return cleanedObj; } diff --git a/static/foundation/js/vendor/dropdown-utils.js b/static/foundation/js/vendor/dropdown-utils.js index 566ec96..9b161d1 100644 --- a/static/foundation/js/vendor/dropdown-utils.js +++ b/static/foundation/js/vendor/dropdown-utils.js @@ -1,65 +1,77 @@ // dropdown-utils.js /*Populates >{{ error_message_token }} {% endif %} - +
+ +
diff --git a/meta_creator/templates/meta_creator/information.html b/meta_creator/templates/meta_creator/information.html index 19db2a5..6e0a99c 100644 --- a/meta_creator/templates/meta_creator/information.html +++ b/meta_creator/templates/meta_creator/information.html @@ -1,26 +1,42 @@ -{% extends 'base.html' %} - -{% load static %} - -{% block content %} - +{% extends 'base.html' %} {% load static %} {% block content %}
-

What is SMECS ?

+

What is SMECS ?

-
-

Software Metadata Extraction and Curation Software or SMECS is a web application to extract and curate software metadata following the CodeMeta software metadata standard.

-

SMECS facilitates the extraction of software metadata from repositories on GitHub/GitLab. It offers a user-friendly graphical user interface for visualizing the retrieved metadata. This empowers Research Software Engineers (RSE) to curate the extracted metadata according to their requirements. Ultimately, SMECS delivers the curated metadata in JSON format, enhancing usability and accessibility. -

-

To get more information about this tool and to access the source code please visit SMECS GitHub

+
+

+ Software Metadata Extraction and Curation Software or SMECS is a web + application to extract and curate software metadata following the + CodeMeta software metadata standard. +

+

+ SMECS facilitates the extraction of software metadata from + repositories on GitHub/GitLab. It offers a user-friendly graphical + user interface for visualizing the retrieved metadata. This empowers + Research Software Engineers (RSE) to curate the extracted metadata + according to their requirements. Ultimately, SMECS delivers the + curated metadata in JSON format, enhancing usability and + accessibility. +

+

+ To get more information about this tool and to access the source code + please visit + SMECS GitHub +

-
- +
+
-{% endblock content %} + {% endblock content %} +
diff --git a/meta_creator/templates/meta_creator/showdata.html b/meta_creator/templates/meta_creator/showdata.html index 9868437..6df7523 100644 --- a/meta_creator/templates/meta_creator/showdata.html +++ b/meta_creator/templates/meta_creator/showdata.html @@ -50,7 +50,7 @@

Metdata in JSON format

Missing required metadata
-
+
Missing recommended metadata
@@ -58,7 +58,8 @@

Metdata in JSON format

Mandatory elements
-
+ +
-
+ {% csrf_token %}
@@ -117,7 +118,7 @@

Extra Hints!

{% endif %} {% for key, value in metadata_dict.items %} -
+
{% if type_metadata|get:key == "hidden" %}