Browse Source

multilanguage widget appearance updated

swithc from buttons to dropdown for language selection

a dedicated widget template added for address and links for hadith app
master
Mohsen Taba 1 day ago
parent
commit
8665eed84d
  1. 158
      templates/utils/widgets/links_json_widget.html
  2. 240
      templates/utils/widgets/multilang_address_widget.html
  3. 209
      templates/utils/widgets/multilang_json_widget.html

158
templates/utils/widgets/links_json_widget.html

@ -0,0 +1,158 @@
{% load i18n %}
<div class="space-y-3 max-w-2xl" data-links-json data-field-name="{{ widget.field_name }}">
<div class="space-y-2" data-links-container>
{% for link in widget.links %}
<div class="flex items-center gap-2" data-link-row>
<div class="w-2/5">
<input type="text" value="{{ link.title }}" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl link-title" placeholder="{% trans 'Link Title (e.g. Source)' %}">
</div>
<div class="w-3/5">
<input type="url" value="{{ link.link }}" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl link-url" placeholder="{% trans 'URL (e.g. https://...)' %}">
</div>
<button type="button" class="remove-link-btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-2 text-sm transition-colors duration-150" title="Remove Link">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
{% empty %}
<div class="flex items-center gap-2" data-link-row>
<div class="w-2/5">
<input type="text" value="" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl link-title" placeholder="{% trans 'Link Title (e.g. Source)' %}">
</div>
<div class="w-3/5">
<input type="url" value="" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl link-url" placeholder="{% trans 'URL (e.g. https://...)' %}">
</div>
<button type="button" class="remove-link-btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-2 text-sm transition-colors duration-150" title="Remove Link">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
{% endfor %}
</div>
<button type="button" class="add-link-btn mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary-50 dark:hover:bg-primary-950/30 rounded border border-primary-200 dark:border-primary-800 transition-colors duration-150" data-add-link>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
{% trans "Add Link" %}
</button>
<input type="hidden" name="{{ widget.field_name }}" value='{{ widget.serialized|escapejs }}'>
</div>
<script>
(function () {
function init(root) {
if (root.getAttribute("data-initialized") === "true") return;
root.setAttribute("data-initialized", "true");
var fieldName = root.getAttribute("data-field-name");
if (!fieldName) return;
var container = root.querySelector("[data-links-container]");
var hidden = root.querySelector('input[type="hidden"][name="' + fieldName + '"]');
if (!container || !hidden) return;
function serialize() {
var result = [];
container.querySelectorAll('[data-link-row]').forEach(function (row) {
var titleInput = row.querySelector('.link-title');
var urlInput = row.querySelector('.link-url');
var titleVal = titleInput ? titleInput.value.trim() : '';
var urlVal = urlInput ? urlInput.value.trim() : '';
if (titleVal !== '' || urlVal !== '') {
result.push({ title: titleVal, link: urlVal });
}
});
try {
hidden.value = JSON.stringify(result);
} catch (e) {
console.error('JSON stringify error:', e);
}
}
function bindInputEvents(input) {
input.addEventListener('input', serialize);
}
container.querySelectorAll('input').forEach(bindInputEvents);
var addBtn = root.querySelector('[data-add-link]');
if (addBtn) {
addBtn.addEventListener('click', function () {
var row = document.createElement('div');
row.className = 'flex items-center gap-2 mt-2';
row.setAttribute('data-link-row', '');
var col1 = document.createElement('div');
col1.className = 'w-2/5';
var inputTitle = document.createElement('input');
inputTitle.type = 'text';
inputTitle.className = 'border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl link-title';
inputTitle.placeholder = 'Link Title (e.g. Source)';
bindInputEvents(inputTitle);
col1.appendChild(inputTitle);
var col2 = document.createElement('div');
col2.className = 'w-3/5';
var inputUrl = document.createElement('input');
inputUrl.type = 'url';
inputUrl.className = 'border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl link-url';
inputUrl.placeholder = 'URL (e.g. https://...)';
bindInputEvents(inputUrl);
col2.appendChild(inputUrl);
var delBtn = document.createElement('button');
delBtn.type = 'button';
delBtn.className = 'remove-link-btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-2 text-sm transition-colors duration-150';
delBtn.title = 'Remove Link';
delBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" /></svg>';
delBtn.addEventListener('click', function () {
row.remove();
serialize();
});
row.appendChild(col1);
row.appendChild(col2);
row.appendChild(delBtn);
container.appendChild(row);
inputTitle.focus();
serialize();
});
}
container.querySelectorAll('.remove-link-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var row = btn.closest('[data-link-row]');
if (row) {
row.remove();
serialize();
}
});
});
}
function initializeWidgets() {
document.querySelectorAll('[data-links-json]').forEach(init);
}
if (document.readyState === 'loading') {
document.addEventListener("DOMContentLoaded", initializeWidgets);
} else {
initializeWidgets();
}
setTimeout(initializeWidgets, 100);
document.addEventListener("formset:added", function (event) {
var newFormset = event.detail.formsetRow;
if (newFormset) {
newFormset.querySelectorAll('[data-links-json]').forEach(init);
}
});
})();
</script>

240
templates/utils/widgets/multilang_address_widget.html

@ -0,0 +1,240 @@
{% load i18n %}
<div class="space-y-3 max-w-2xl" data-multilang-address data-field-name="{{ widget.field_name }}">
<div>
<div class="w-full max-w-2xl pr-2">
<div class="flex items-center gap-2 py-1.5">
<span class="text-xs font-semibold text-font-subtle-light dark:text-font-subtle-dark uppercase tracking-wider">
{% trans "Select Language" %}:
</span>
<select class="lang-select border border-base-200 bg-white font-semibold min-w-[120px] rounded shadow-sm text-font-default-light text-xs focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 px-3 py-1.5" data-lang-select>
{% for code in widget.languages %}
<option value="{{ code }}">
{{ code|upper }}{% if widget.has_value_codes and code in widget.has_value_codes %} ✓{% endif %}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="space-y-3" data-inputs>
{% for item in widget.rendered_languages %}
<div class="hidden" data-input-wrapper data-lang-code="{{ item.code }}">
<div class="flex items-center gap-2 mb-1">
<span class="text-xs font-medium text-font-subtle-light dark:text-font-subtle-dark">
{{ item.code|upper }}
</span>
</div>
<div class="space-y-2" data-parts-container>
{% for part in item.parts %}
<div class="flex items-center gap-2" data-part-row>
<input type="text" value="{{ part }}" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl" placeholder="Address part (e.g. Book 1, Chapter 9)">
<button type="button" class="remove-part-btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-2 text-sm transition-colors duration-150" title="Remove part">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
{% empty %}
<div class="flex items-center gap-2" data-part-row>
<input type="text" value="" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl" placeholder="Address part (e.g. Book 1, Chapter 9)">
<button type="button" class="remove-part-btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-2 text-sm transition-colors duration-150" title="Remove part">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
{% endfor %}
</div>
<button type="button" class="add-part-btn mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary-50 dark:hover:bg-primary-950/30 rounded border border-primary-200 dark:border-primary-800 transition-colors duration-150" data-add-part>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
{% trans "Add Part" %}
</button>
</div>
{% endfor %}
<input type="hidden" name="{{ widget.field_name }}" value='{{ widget.serialized|escapejs }}'>
</div>
<div class="text-xs text-font-subtle-light dark:text-font-subtle-dark">
{% trans "Click a language code to manage its address parts." %}
</div>
</div>
<style>
/* Reuse styling rules from main widget to maintain consistency */
[data-multilang-address] [data-input-wrapper].hidden {
display: none !important;
}
[data-multilang-address] [data-input-wrapper]:not(.hidden) {
display: block !important;
}
.lang-select {
cursor: pointer;
appearance: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E") !important;
background-repeat: no-repeat !important;
background-position: right 0.5rem center !important;
background-size: 1.25rem !important;
padding-right: 1.75rem !important;
}
[dir="rtl"] .lang-select {
background-position: left 0.5rem center !important;
padding-right: 0.75rem !important;
padding-left: 1.75rem !important;
}
</style>
<script>
(function () {
function init(root) {
if (root.getAttribute("data-initialized") === "true") return;
root.setAttribute("data-initialized", "true");
var fieldName = root.getAttribute("data-field-name");
if (!fieldName) return;
var select = root.querySelector("[data-lang-select]");
var inputsRoot = root.querySelector("[data-inputs]");
if (!inputsRoot || !select) return;
// First, hide all wrappers
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
w.classList.add("hidden");
});
var selectedCode = select.value;
var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + selectedCode + '"]');
if (wrapper) {
wrapper.classList.remove("hidden");
}
select.addEventListener("change", function () {
var code = select.value;
var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + code + '"]');
if (!wrapper) return;
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
w.classList.add("hidden");
});
wrapper.classList.remove("hidden");
var input = wrapper.querySelector('input[type="text"]');
if (input) {
setTimeout(function(){ input.focus(); }, 50);
}
});
var hidden = root.querySelector('input[type="hidden"][name="' + fieldName + '"]');
function serialize() {
if (!hidden) return;
var result = [];
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
var code = w.getAttribute('data-lang-code');
var parts = [];
w.querySelectorAll('input[type="text"]').forEach(function (inp) {
if (inp.value && inp.value.trim() !== '') {
parts.push(inp.value.trim());
}
});
var option = select.querySelector('option[value="' + code + '"]');
var baseText = code.toUpperCase();
if (parts.length > 0) {
result.push({ language_code: code, title: parts });
if (option) option.textContent = baseText + ' ✓';
} else {
if (option) option.textContent = baseText;
}
});
try {
hidden.value = JSON.stringify(result);
} catch (e) {
console.error('JSON stringify error:', e);
}
}
function bindInputEvents(input) {
input.addEventListener('input', serialize);
}
inputsRoot.querySelectorAll('input[type="text"]').forEach(bindInputEvents);
inputsRoot.querySelectorAll('[data-add-part]').forEach(function (addBtn) {
addBtn.addEventListener('click', function () {
var wrapper = addBtn.closest('[data-input-wrapper]');
var container = wrapper.querySelector('[data-parts-container]');
if (!container) return;
var row = document.createElement('div');
row.className = 'flex items-center gap-2 mt-2';
row.setAttribute('data-part-row', '');
var input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Address part (e.g. Book 1, Chapter 9)';
input.className = 'border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl';
bindInputEvents(input);
var delBtn = document.createElement('button');
delBtn.type = 'button';
delBtn.className = 'remove-part-btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-2 text-sm transition-colors duration-150';
delBtn.title = 'Remove part';
delBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" /></svg>';
delBtn.addEventListener('click', function () {
row.remove();
serialize();
});
row.appendChild(input);
row.appendChild(delBtn);
container.appendChild(row);
input.focus();
serialize();
});
});
inputsRoot.querySelectorAll('.remove-part-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var row = btn.closest('[data-part-row]');
if (row) {
row.remove();
serialize();
}
});
});
}
function initializeWidgets() {
document.querySelectorAll('[data-multilang-address]').forEach(init);
}
if (document.readyState === 'loading') {
document.addEventListener("DOMContentLoaded", initializeWidgets);
} else {
initializeWidgets();
}
setTimeout(initializeWidgets, 100);
document.addEventListener("formset:added", function (event) {
var newFormset = event.detail.formsetRow;
if (newFormset) {
newFormset.querySelectorAll('[data-multilang-address]').forEach(init);
}
});
})();
</script>

209
templates/utils/widgets/multilang_json_widget.html

@ -1,17 +1,18 @@
{% load i18n %} {% load i18n %}
<div class="space-y-3 max-w-2xl" data-multilang-json data-field-name="{{ widget.field_name }}"> <div class="space-y-3 max-w-2xl" data-multilang-json data-field-name="{{ widget.field_name }}">
<div class="relative">
<div class="w-full max-w-2xl overflow-x-auto scrollbar-hover pr-2">
<div class="inline-flex flex-nowrap items-center gap-1 whitespace-nowrap py-1 min-w-max" data-lang-bar>
<div>
<div class="w-full max-w-2xl pr-2">
<div class="flex items-center gap-2 py-1.5">
<span class="text-xs font-semibold text-font-subtle-light dark:text-font-subtle-dark uppercase tracking-wider">
{% trans "Select Language" %}:
</span>
<select class="lang-select border border-base-200 bg-white font-semibold min-w-[120px] rounded shadow-sm text-font-default-light text-xs focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 px-3 py-1.5" data-lang-select>
{% for code in widget.languages %} {% for code in widget.languages %}
<button type="button"
class="lang-btn px-3 py-1.5 rounded-md border transition-all duration-150 text-xs font-medium
border-gray-200 text-gray-700 hover:bg-gray-50 hover:border-gray-300
dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:border-gray-600{% if widget.has_value_codes and code in widget.has_value_codes %} has-value{% endif %}"
data-lang-code="{{ code }}">
{{ code|upper }}
</button>
<option value="{{ code }}">
{{ code|upper }}{% if widget.has_value_codes and code in widget.has_value_codes %} ✓{% endif %}
</option>
{% endfor %} {% endfor %}
</select>
</div> </div>
</div> </div>
</div> </div>
@ -36,64 +37,6 @@
</div> </div>
<style> <style>
.scrollbar-hover {
--scrollbar-track: rgb(var(--color-base-100));
--scrollbar-thumb: rgb(var(--color-base-300));
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
scrollbar-width: none;
}
.scrollbar-hover:hover { scrollbar-width: thin; }
.dark .scrollbar-hover { --scrollbar-track: rgb(var(--color-base-800)); --scrollbar-thumb: rgb(var(--color-base-600)); }
.scrollbar-hover::-webkit-scrollbar { height: 0; }
.scrollbar-hover:hover::-webkit-scrollbar { height: 6px; }
.scrollbar-hover::-webkit-scrollbar-track { background: var(--scrollbar-track); border-radius: 3px; }
.scrollbar-hover::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; }
.scrollbar-hover::-webkit-scrollbar-corner { background: var(--scrollbar-track); }
.scrollbar-hover:hover::-webkit-scrollbar-thumb:hover { background: rgb(var(--color-base-400)); }
.dark .scrollbar-hover:hover::-webkit-scrollbar-thumb:hover { background: rgb(var(--color-base-500)); }
.lang-btn {
background: transparent;
cursor: pointer;
transition: all 0.15s ease;
}
.lang-btn:hover {
background: #f8f9fa;
border-color: #dee2e6;
}
.dark .lang-btn:hover {
background: #343a40;
border-color: #6c757d;
}
.lang-btn.is-active {
border-color: #3b82f6;
background: #eff6ff;
color: #1d4ed8;
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.25);
}
.dark .lang-btn.is-active {
border-color: #2563eb;
background: #1e3a8a;
color: #dbeafe;
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.5);
}
.lang-btn.has-value {
border-color: #3b82f6;
color: #1d4ed8;
}
.dark .lang-btn.has-value {
border-color: #3b82f6;
color: #dbeafe;
}
.lang-btn.is-active:hover {
background: #eff6ff;
border-color: #3b82f6;
}
.dark .lang-btn.is-active:hover {
background: #1e3a8a;
border-color: #2563eb;
}
/* Ensure hidden class works properly */ /* Ensure hidden class works properly */
[data-input-wrapper].hidden { [data-input-wrapper].hidden {
display: none !important; display: none !important;
@ -102,125 +45,64 @@
[data-input-wrapper]:not(.hidden) { [data-input-wrapper]:not(.hidden) {
display: block !important; display: block !important;
} }
.lang-select {
cursor: pointer;
appearance: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E") !important;
background-repeat: no-repeat !important;
background-position: right 0.5rem center !important;
background-size: 1.25rem !important;
padding-right: 1.75rem !important;
}
[dir="rtl"] .lang-select {
background-position: left 0.5rem center !important;
padding-right: 0.75rem !important;
padding-left: 1.75rem !important;
}
</style> </style>
<script> <script>
(function () { (function () {
function init(root) { function init(root) {
console.log('Initializing multilang widget:', root);
if (root.getAttribute("data-initialized") === "true") return;
root.setAttribute("data-initialized", "true");
var fieldName = root.getAttribute("data-field-name"); var fieldName = root.getAttribute("data-field-name");
if (!fieldName) {
console.log('No field name found');
return;
}
if (!fieldName) return;
var buttons = root.querySelectorAll(".lang-btn[data-lang-code]");
var select = root.querySelector("[data-lang-select]");
var inputsRoot = root.querySelector("[data-inputs]"); var inputsRoot = root.querySelector("[data-inputs]");
if (!inputsRoot) {
console.log('No inputs root found');
return;
}
console.log('Found', buttons.length, 'language buttons');
if (!inputsRoot || !select) return;
// First, hide all wrappers // First, hide all wrappers
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) { inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
w.classList.add("hidden"); w.classList.add("hidden");
}); });
var hasActiveLanguage = false;
var withValue = [];
var withoutValue = [];
buttons.forEach(function (btn) {
var code = btn.getAttribute("data-lang-code");
var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + code + '"]');
var hasValue = false;
var selectedCode = select.value;
var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + selectedCode + '"]');
if (wrapper) { if (wrapper) {
var input = wrapper.querySelector('input[name="' + fieldName + '__' + code + '"], textarea[name="' + fieldName + '__' + code + '"], input[id*="' + fieldName + '__' + code + '"]');
hasValue = !!(input && input.value && input.value.trim() !== "");
}
if (hasValue) {
btn.classList.add("has-value");
withValue.push(btn);
if (!hasActiveLanguage && wrapper) {
btn.classList.add("is-active");
wrapper.classList.remove("hidden"); wrapper.classList.remove("hidden");
hasActiveLanguage = true;
console.log('Initializing with active language:', code);
}
} else {
withoutValue.push(btn);
}
});
if (!hasActiveLanguage && buttons.length) {
var firstBtn = (withValue[0] || buttons[0]);
var firstCode = firstBtn.getAttribute("data-lang-code");
var firstWrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + firstCode + '"]');
if (firstWrapper) {
firstBtn.classList.add("is-active");
firstWrapper.classList.remove("hidden");
console.log('Initializing with first language:', firstCode);
}
} }
var bar = root.querySelector('[data-lang-bar]');
if (bar) {
withValue.concat(withoutValue).forEach(function (btn) {
bar.appendChild(btn);
});
}
buttons.forEach(function (btn) {
btn.addEventListener("click", function () {
console.log('Language button clicked:', btn.getAttribute("data-lang-code"));
var code = btn.getAttribute("data-lang-code");
select.addEventListener("change", function () {
var code = select.value;
var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + code + '"]'); var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + code + '"]');
if (!wrapper) {
console.log('No wrapper found for code:', code);
return;
}
var isActive = btn.classList.contains("is-active");
console.log('Button is active:', isActive);
// Remove active class from all buttons
buttons.forEach(function (b) { b.classList.remove("is-active"); });
if (!wrapper) return;
// Hide all input wrappers
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) { inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
w.classList.add("hidden"); w.classList.add("hidden");
console.log('Hiding wrapper for:', w.getAttribute('data-lang-code'));
}); });
// Always show the clicked wrapper and hide others
btn.classList.add("is-active");
wrapper.classList.remove("hidden"); wrapper.classList.remove("hidden");
console.log('Showing wrapper for:', code);
var input = wrapper.querySelector('input, textarea'); var input = wrapper.querySelector('input, textarea');
if (input) { if (input) {
setTimeout(function(){ input.focus(); }, 50); setTimeout(function(){ input.focus(); }, 50);
} }
var hidden = root.querySelector('input[type="hidden"][name="' + fieldName + '"]');
if (hidden) {
var result = [];
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
var c = w.getAttribute('data-lang-code');
var inp = w.querySelector('input, textarea');
if (inp && inp.value && inp.value.trim() !== '') {
result.push({ language_code: c, title: inp.value });
}
});
try {
hidden.value = JSON.stringify(result);
console.log('Updated hidden field value:', hidden.value);
} catch (e) {
console.error('JSON stringify error:', e);
}
}
});
}); });
var hidden = root.querySelector('input[type="hidden"][name="' + fieldName + '"]'); var hidden = root.querySelector('input[type="hidden"][name="' + fieldName + '"]');
@ -231,13 +113,14 @@
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) { inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
var c = w.getAttribute('data-lang-code'); var c = w.getAttribute('data-lang-code');
var i = w.querySelector('input, textarea'); var i = w.querySelector('input, textarea');
var option = select.querySelector('option[value="' + c + '"]');
var baseText = c.toUpperCase();
if (i && i.value && i.value.trim() !== '') { if (i && i.value && i.value.trim() !== '') {
result.push({ language_code: c, title: i.value }); result.push({ language_code: c, title: i.value });
var btn = root.querySelector('.lang-btn[data-lang-code="' + c + '"]');
if (btn) btn.classList.add('has-value');
if (option) option.textContent = baseText + ' ✓';
} else { } else {
var btn2 = root.querySelector('.lang-btn[data-lang-code="' + c + '"]');
if (btn2) btn2.classList.remove('has-value');
if (option) option.textContent = baseText;
} }
}); });
try { hidden.value = JSON.stringify(result); } catch (e) { console.error('JSON stringify error:', e); } try { hidden.value = JSON.stringify(result); } catch (e) { console.error('JSON stringify error:', e); }
@ -247,19 +130,15 @@
} }
function initializeWidgets() { function initializeWidgets() {
console.log('Initializing all multilang widgets');
document.querySelectorAll('[data-multilang-json]').forEach(init); document.querySelectorAll('[data-multilang-json]').forEach(init);
} }
// Try multiple initialization methods
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener("DOMContentLoaded", initializeWidgets); document.addEventListener("DOMContentLoaded", initializeWidgets);
} else { } else {
// DOM is already loaded
initializeWidgets(); initializeWidgets();
} }
// Also try after a short delay to ensure everything is ready
setTimeout(initializeWidgets, 100); setTimeout(initializeWidgets, 100);
document.addEventListener("formset:added", function (event) { document.addEventListener("formset:added", function (event) {

Loading…
Cancel
Save