4 Commits
1f4778eeb2
...
76762bebf8
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
76762bebf8 |
nginx update
|
1 month ago |
|
|
471f410970 |
language codes updated , address and links json connected to its template
|
1 month ago |
|
|
8665eed84d |
multilanguage widget appearance updated
swithc from buttons to dropdown for language selection a dedicated widget template added for address and links for hadith app |
1 month ago |
|
|
6c13b951c0 |
hadis multilanguage fields refactor
hadis data synced with projects language map with migrate_json_key script command all the views , serializers , admin methods and models updated with this data schema update . |
1 month ago |
16 changed files with 924 additions and 1714 deletions
-
139apps/hadis/admin/category.py
-
614apps/hadis/admin/hadis.py
-
349apps/hadis/admin/reference.py
-
422apps/hadis/admin/transmitter.py
-
69apps/hadis/management/commands/migrate_json_keys.py
-
22apps/hadis/models/category.py
-
46apps/hadis/models/hadis.py
-
44apps/hadis/models/reference.py
-
60apps/hadis/models/transmitter.py
-
16apps/hadis/serializers/category.py
-
6apps/hadis/views/category.py
-
18nginx/dovodi.conf
-
158templates/utils/widgets/links_json_widget.html
-
240templates/utils/widgets/multilang_address_widget.html
-
235templates/utils/widgets/multilang_json_widget.html
-
198utils/multilang_json_widget.py
@ -0,0 +1,69 @@ |
|||
from django.core.management.base import BaseCommand |
|||
from django.db import transaction |
|||
from django.apps import apps |
|||
from django.db import models as django_models |
|||
|
|||
class Command(BaseCommand): |
|||
help = "Rename 'text' key to 'title' in all multi-language JSON fields across Hadis app models." |
|||
|
|||
@transaction.atomic |
|||
def handle(self, *args, **options): |
|||
hadis_app = apps.get_app_config('hadis') |
|||
models = hadis_app.get_models() |
|||
|
|||
self.stdout.write("Starting key migration in JSON fields...") |
|||
|
|||
for model in models: |
|||
json_fields = [ |
|||
f.name for f in model._meta.get_fields() |
|||
if isinstance(f, django_models.JSONField) |
|||
] |
|||
if not json_fields: |
|||
continue |
|||
|
|||
self.stdout.write(f"Checking model {model.__name__} for fields: {json_fields}") |
|||
|
|||
queryset = model.objects.all() |
|||
for obj in queryset: |
|||
changed = False |
|||
for field in json_fields: |
|||
value = getattr(obj, field) |
|||
if not isinstance(value, list): |
|||
continue |
|||
|
|||
new_list = [] |
|||
field_changed = False |
|||
for item in value: |
|||
if isinstance(item, dict): |
|||
# Check if this item follows the multi-language pattern |
|||
# lang_key can be 'language_code', 'lang', or 'code' |
|||
lang_val = item.get("language_code") or item.get("lang") or item.get("code") |
|||
# text_val can be 'text' or 'title' |
|||
text_val = item.get("text") |
|||
|
|||
if lang_val and text_val is not None: |
|||
# We found a multi-language item with a 'text' key |
|||
new_item = { |
|||
"language_code": str(lang_val), |
|||
"title": text_val |
|||
} |
|||
# Copy any other keys |
|||
for k, v in item.items(): |
|||
if k not in ["language_code", "lang", "code", "text", "title"]: |
|||
new_item[k] = v |
|||
new_list.append(new_item) |
|||
field_changed = True |
|||
else: |
|||
new_list.append(item) |
|||
else: |
|||
new_list.append(item) |
|||
|
|||
if field_changed: |
|||
setattr(obj, field, new_list) |
|||
changed = True |
|||
|
|||
if changed: |
|||
obj.save() |
|||
self.stdout.write(f" Updated keys in {model.__name__} ID: {obj.id}") |
|||
|
|||
self.stdout.write(self.style.SUCCESS("JSON key migration completed!")) |
|||
@ -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> |
|||
@ -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> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue