From 1acecd8639f1ebfcd40d0ff52f6a666a5b953be6 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Sat, 13 Dec 2025 22:34:36 +0000 Subject: [PATCH] DJANGO Phase 1 --- backend/igny8_core/admin/site.py | 1 + backend/igny8_core/modules/billing/admin.py | 36 +- backend/igny8_core/modules/writer/admin.py | 60 +- backend/igny8_core/settings.py | 19 + backend/requirements.txt | 6 + backend/staticfiles/admin/js/cancel.js | 43 +- backend/staticfiles/admin/js/collapse.js | 49 + .../staticfiles/admin/js/popup_response.js | 61 +- .../collapsible-inlines.js | 45 + .../css/admin-interface-fix.css | 610 ++++++ .../admin_interface/css/admin-interface.css | 497 +++++ .../admin_interface/css/ckeditor.css | 126 ++ .../admin_interface/css/form-controls.css | 95 + .../admin_interface/css/import-export.css | 7 + .../admin_interface/css/jquery.ui.tabs.css | 247 +++ .../admin_interface/css/json-widget.css | 27 + .../admin_interface/css/language-chooser.css | 72 + .../css/list-filter-dropdown.css | 27 + .../admin_interface/css/modeltranslation.css | 17 + .../admin_interface/css/rangefilter.css | 25 + .../admin_interface/css/recent-actions.css | 10 + .../admin_interface/css/related-modal.css | 82 + .../staticfiles/admin_interface/css/rtl.css | 34 + .../admin_interface/css/sorl-thumbnail.css | 67 + .../admin_interface/css/streamfield.css | 200 ++ .../admin_interface/css/tabbed-admin.css | 37 + .../admin_interface/css/tabbed-changeform.css | 67 + .../admin_interface/css/tinymce.css | 3 + .../favico/favico-0.3.10-patched.js | 913 ++++++++ .../favico/favico-0.3.10-patched.min.js | 1 + .../foldable-apps/foldable-apps.css | 70 + .../foldable-apps/foldable-apps.js | 35 + .../magnific-popup/jquery.magnific-popup.js | 1867 +++++++++++++++++ .../magnific-popup/magnific-popup.css | 351 ++++ .../related-modal/related-modal.js | 165 ++ .../tabbed-changeform/tabbed-changeform.js | 73 + .../ckeditor/ckeditor/skins/light/LICENSE | 21 + .../ckeditor/ckeditor/skins/light/README.md | 2 + .../ckeditor/ckeditor/skins/light/bower.json | 13 + .../ckeditor/ckeditor/skins/light/dialog.css | 5 + .../ckeditor/ckeditor/skins/light/editor.css | 1348 ++++++++++++ .../ckeditor/skins/light/editor_gecko.css | 6 + .../ckeditor/skins/light/editor_ie.css | 5 + .../ckeditor/skins/light/editor_ie7.css | 5 + .../ckeditor/skins/light/editor_ie8.css | 5 + .../ckeditor/ckeditor/skins/light/icons.png | Bin 0 -> 22758 bytes .../ckeditor/skins/light/icons_hidpi.png | Bin 0 -> 33069 bytes .../ckeditor/skins/light/images/arrow.png | Bin 0 -> 261 bytes .../ckeditor/skins/light/images/close.png | Bin 0 -> 824 bytes .../skins/light/images/hidpi/close.png | Bin 0 -> 1792 bytes .../skins/light/images/hidpi/lock-open.png | Bin 0 -> 1503 bytes .../skins/light/images/hidpi/lock.png | Bin 0 -> 1616 bytes .../skins/light/images/hidpi/refresh.png | Bin 0 -> 2320 bytes .../ckeditor/skins/light/images/lock-open.png | Bin 0 -> 736 bytes .../ckeditor/skins/light/images/lock.png | Bin 0 -> 728 bytes .../ckeditor/skins/light/images/refresh.png | Bin 0 -> 953 bytes .../ckeditor/ckeditor/skins/light/skin.js | 322 +++ backend/staticfiles/colorfield/colorfield.js | 11 + .../staticfiles/colorfield/coloris/README.txt | 3 + .../colorfield/coloris/coloris.css | 577 +++++ .../staticfiles/colorfield/coloris/coloris.js | 1263 +++++++++++ .../colorfield/coloris/coloris.min.css | 1 + .../colorfield/coloris/coloris.min.js | 6 + .../import_export/action_formats.js | 22 + .../staticfiles/import_export/guess_format.js | 21 + backend/staticfiles/import_export/import.css | 115 + backend/staticfiles/rangefilter/iife.js | 25 + .../streamfield/js/admin_popup_response.js | 22 + 68 files changed, 9797 insertions(+), 46 deletions(-) create mode 100644 backend/staticfiles/admin/js/collapse.js create mode 100644 backend/staticfiles/admin_interface/collapsible-inlines/collapsible-inlines.js create mode 100644 backend/staticfiles/admin_interface/css/admin-interface-fix.css create mode 100644 backend/staticfiles/admin_interface/css/admin-interface.css create mode 100644 backend/staticfiles/admin_interface/css/ckeditor.css create mode 100644 backend/staticfiles/admin_interface/css/form-controls.css create mode 100644 backend/staticfiles/admin_interface/css/import-export.css create mode 100644 backend/staticfiles/admin_interface/css/jquery.ui.tabs.css create mode 100644 backend/staticfiles/admin_interface/css/json-widget.css create mode 100644 backend/staticfiles/admin_interface/css/language-chooser.css create mode 100644 backend/staticfiles/admin_interface/css/list-filter-dropdown.css create mode 100644 backend/staticfiles/admin_interface/css/modeltranslation.css create mode 100644 backend/staticfiles/admin_interface/css/rangefilter.css create mode 100644 backend/staticfiles/admin_interface/css/recent-actions.css create mode 100644 backend/staticfiles/admin_interface/css/related-modal.css create mode 100644 backend/staticfiles/admin_interface/css/rtl.css create mode 100644 backend/staticfiles/admin_interface/css/sorl-thumbnail.css create mode 100644 backend/staticfiles/admin_interface/css/streamfield.css create mode 100644 backend/staticfiles/admin_interface/css/tabbed-admin.css create mode 100644 backend/staticfiles/admin_interface/css/tabbed-changeform.css create mode 100644 backend/staticfiles/admin_interface/css/tinymce.css create mode 100644 backend/staticfiles/admin_interface/favico/favico-0.3.10-patched.js create mode 100644 backend/staticfiles/admin_interface/favico/favico-0.3.10-patched.min.js create mode 100644 backend/staticfiles/admin_interface/foldable-apps/foldable-apps.css create mode 100644 backend/staticfiles/admin_interface/foldable-apps/foldable-apps.js create mode 100644 backend/staticfiles/admin_interface/magnific-popup/jquery.magnific-popup.js create mode 100644 backend/staticfiles/admin_interface/magnific-popup/magnific-popup.css create mode 100644 backend/staticfiles/admin_interface/related-modal/related-modal.js create mode 100644 backend/staticfiles/admin_interface/tabbed-changeform/tabbed-changeform.js create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/LICENSE create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/README.md create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/bower.json create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/dialog.css create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/editor.css create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/editor_gecko.css create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/editor_ie.css create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/editor_ie7.css create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/editor_ie8.css create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/icons.png create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/icons_hidpi.png create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/images/arrow.png create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/images/close.png create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/images/hidpi/close.png create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/images/hidpi/lock-open.png create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/images/hidpi/lock.png create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/images/hidpi/refresh.png create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/images/lock-open.png create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/images/lock.png create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/images/refresh.png create mode 100644 backend/staticfiles/ckeditor/ckeditor/skins/light/skin.js create mode 100644 backend/staticfiles/colorfield/colorfield.js create mode 100644 backend/staticfiles/colorfield/coloris/README.txt create mode 100644 backend/staticfiles/colorfield/coloris/coloris.css create mode 100644 backend/staticfiles/colorfield/coloris/coloris.js create mode 100644 backend/staticfiles/colorfield/coloris/coloris.min.css create mode 100644 backend/staticfiles/colorfield/coloris/coloris.min.js create mode 100644 backend/staticfiles/import_export/action_formats.js create mode 100644 backend/staticfiles/import_export/guess_format.js create mode 100644 backend/staticfiles/import_export/import.css create mode 100644 backend/staticfiles/rangefilter/iife.js create mode 100644 backend/staticfiles/streamfield/js/admin_popup_response.js diff --git a/backend/igny8_core/admin/site.py b/backend/igny8_core/admin/site.py index 4feb7d76..7bd162c5 100644 --- a/backend/igny8_core/admin/site.py +++ b/backend/igny8_core/admin/site.py @@ -34,6 +34,7 @@ class Igny8AdminSite(admin.AdminSite): '💰 Billing & Accounts': { 'models': [ ('igny8_core_auth', 'Plan'), + ('billing', 'PlanLimitUsage'), ('igny8_core_auth', 'Account'), ('igny8_core_auth', 'Subscription'), ('billing', 'Invoice'), diff --git a/backend/igny8_core/modules/billing/admin.py b/backend/igny8_core/modules/billing/admin.py index 14652885..9c011c74 100644 --- a/backend/igny8_core/modules/billing/admin.py +++ b/backend/igny8_core/modules/billing/admin.py @@ -3,6 +3,7 @@ Billing Module Admin """ from django.contrib import admin from django.utils.html import format_html +from django.contrib import messages from igny8_core.admin.base import AccountAdminMixin from igny8_core.business.billing.models import ( CreditCostConfig, @@ -12,12 +13,28 @@ from igny8_core.business.billing.models import ( PaymentMethodConfig, ) from .models import CreditTransaction, CreditUsageLog, AccountPaymentMethod +from import_export.admin import ExportMixin +from import_export import resources +from rangefilter.filters import DateRangeFilter + + +from rangefilter.filters import DateRangeFilter + + +class CreditTransactionResource(resources.ModelResource): + """Resource class for exporting Credit Transactions""" + class Meta: + model = CreditTransaction + fields = ('id', 'account__name', 'transaction_type', 'amount', 'balance_after', + 'description', 'reference_id', 'created_at') + export_order = fields @admin.register(CreditTransaction) -class CreditTransactionAdmin(AccountAdminMixin, admin.ModelAdmin): +class CreditTransactionAdmin(ExportMixin, AccountAdminMixin, admin.ModelAdmin): + resource_class = CreditTransactionResource list_display = ['id', 'account', 'transaction_type', 'amount', 'balance_after', 'description', 'created_at'] - list_filter = ['transaction_type', 'created_at', 'account'] + list_filter = ['transaction_type', ('created_at', DateRangeFilter), 'account'] search_fields = ['description', 'account__name'] readonly_fields = ['created_at'] date_hierarchy = 'created_at' @@ -66,8 +83,18 @@ class InvoiceAdmin(AccountAdminMixin, admin.ModelAdmin): readonly_fields = ['created_at', 'updated_at'] +class PaymentResource(resources.ModelResource): + """Resource class for exporting Payments""" + class Meta: + model = Payment + fields = ('id', 'invoice__invoice_number', 'account__name', 'payment_method', + 'status', 'amount', 'currency', 'manual_reference', 'approved_by__email', + 'processed_at', 'created_at') + export_order = fields + + @admin.register(Payment) -class PaymentAdmin(AccountAdminMixin, admin.ModelAdmin): +class PaymentAdmin(ExportMixin, AccountAdminMixin, admin.ModelAdmin): """ Main Payment Admin with approval workflow. When you change status to 'succeeded', it automatically: @@ -76,6 +103,7 @@ class PaymentAdmin(AccountAdminMixin, admin.ModelAdmin): - Activates account - Adds credits """ + resource_class = PaymentResource list_display = [ 'id', 'invoice', @@ -88,7 +116,7 @@ class PaymentAdmin(AccountAdminMixin, admin.ModelAdmin): 'approved_by', 'processed_at', ] - list_filter = ['status', 'payment_method', 'currency', 'created_at', 'processed_at'] + list_filter = ['status', 'payment_method', 'currency', ('created_at', DateRangeFilter), ('processed_at', DateRangeFilter)] search_fields = [ 'invoice__invoice_number', 'account__name', diff --git a/backend/igny8_core/modules/writer/admin.py b/backend/igny8_core/modules/writer/admin.py index ce17d241..d1228b86 100644 --- a/backend/igny8_core/modules/writer/admin.py +++ b/backend/igny8_core/modules/writer/admin.py @@ -1,7 +1,10 @@ from django.contrib import admin +from django.contrib import messages from igny8_core.admin.base import SiteSectorAdminMixin from .models import Tasks, Images, Content from igny8_core.business.content.models import ContentTaxonomy, ContentAttribute, ContentTaxonomyRelation, ContentClusterMap +from import_export.admin import ExportMixin +from import_export import resources class ContentTaxonomyInline(admin.TabularInline): @@ -13,13 +16,24 @@ class ContentTaxonomyInline(admin.TabularInline): verbose_name_plural = 'Taxonomy Terms (Tags & Categories)' +class TaskResource(resources.ModelResource): + """Resource class for exporting Tasks""" + class Meta: + model = Tasks + fields = ('id', 'title', 'description', 'status', 'content_type', 'content_structure', + 'site__name', 'sector__name', 'cluster__name', 'created_at', 'updated_at') + export_order = fields + + @admin.register(Tasks) -class TasksAdmin(SiteSectorAdminMixin, admin.ModelAdmin): +class TasksAdmin(ExportMixin, SiteSectorAdminMixin, admin.ModelAdmin): + resource_class = TaskResource list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'status', 'cluster', 'created_at'] list_filter = ['status', 'content_type', 'content_structure', 'site', 'sector', 'cluster'] search_fields = ['title', 'description'] ordering = ['-created_at'] readonly_fields = ['created_at', 'updated_at'] + actions = ['bulk_set_status_draft', 'bulk_set_status_in_progress', 'bulk_set_status_completed'] fieldsets = ( ('Basic Info', { @@ -37,6 +51,24 @@ class TasksAdmin(SiteSectorAdminMixin, admin.ModelAdmin): }), ) + def bulk_set_status_draft(self, request, queryset): + """Set selected tasks to draft status""" + updated = queryset.update(status='draft') + self.message_user(request, f'{updated} task(s) set to draft.', messages.SUCCESS) + bulk_set_status_draft.short_description = 'Set status to Draft' + + def bulk_set_status_in_progress(self, request, queryset): + """Set selected tasks to in-progress status""" + updated = queryset.update(status='in_progress') + self.message_user(request, f'{updated} task(s) set to in progress.', messages.SUCCESS) + bulk_set_status_in_progress.short_description = 'Set status to In Progress' + + def bulk_set_status_completed(self, request, queryset): + """Set selected tasks to completed status""" + updated = queryset.update(status='completed') + self.message_user(request, f'{updated} task(s) set to completed.', messages.SUCCESS) + bulk_set_status_completed.short_description = 'Set status to Completed' + def get_site_display(self, obj): """Safely get site name""" try: @@ -93,14 +125,26 @@ class ImagesAdmin(SiteSectorAdminMixin, admin.ModelAdmin): return '-' +class ContentResource(resources.ModelResource): + """Resource class for exporting Content""" + class Meta: + model = Content + fields = ('id', 'title', 'content_type', 'content_structure', 'status', 'source', + 'site__name', 'sector__name', 'cluster__name', 'word_count', + 'meta_title', 'meta_description', 'primary_keyword', 'external_url', 'created_at') + export_order = fields + + @admin.register(Content) -class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin): +class ContentAdmin(ExportMixin, SiteSectorAdminMixin, admin.ModelAdmin): + resource_class = ContentResource list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'source', 'status', 'get_taxonomy_count', 'created_at'] list_filter = ['content_type', 'content_structure', 'source', 'status', 'site', 'sector', 'created_at'] search_fields = ['title', 'content_html', 'external_url'] ordering = ['-created_at'] readonly_fields = ['created_at', 'updated_at', 'word_count', 'get_tags_display', 'get_categories_display'] inlines = [ContentTaxonomyInline] + actions = ['bulk_set_status_published', 'bulk_set_status_draft'] fieldsets = ( ('Basic Info', { @@ -152,6 +196,18 @@ class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin): return 'No categories' get_categories_display.short_description = 'Categories' + def bulk_set_status_published(self, request, queryset): + """Set selected content to published status""" + updated = queryset.update(status='published') + self.message_user(request, f'{updated} content item(s) set to published.', messages.SUCCESS) + bulk_set_status_published.short_description = 'Set status to Published' + + def bulk_set_status_draft(self, request, queryset): + """Set selected content to draft status""" + updated = queryset.update(status='draft') + self.message_user(request, f'{updated} content item(s) set to draft.', messages.SUCCESS) + bulk_set_status_draft.short_description = 'Set status to Draft' + def get_site_display(self, obj): """Safely get site name""" try: diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py index 52960a59..bfc88a88 100644 --- a/backend/igny8_core/settings.py +++ b/backend/igny8_core/settings.py @@ -36,16 +36,25 @@ ALLOWED_HOSTS = [ ] INSTALLED_APPS = [ + # Django Admin Enhancements (must be before django.contrib.admin) + 'admin_interface', + 'colorfield', + # Core Django apps 'igny8_core.admin.apps.Igny8AdminConfig', # Custom admin config 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + # Third-party apps 'rest_framework', 'django_filters', 'corsheaders', 'drf_spectacular', # OpenAPI 3.0 schema generation + 'import_export', + 'rangefilter', + 'django_celery_results', + # IGNY8 apps 'igny8_core.auth.apps.Igny8CoreAuthConfig', # Use app config with custom label 'igny8_core.ai.apps.AIConfig', # AI Framework 'igny8_core.modules.planner.apps.PlannerConfig', @@ -591,6 +600,16 @@ LOGGING = { }, } +# Admin Interface Configuration +X_FRAME_OPTIONS = 'SAMEORIGIN' # Required for django-admin-interface + +# Celery Results Backend +CELERY_RESULT_BACKEND = 'django-db' +CELERY_CACHE_BACKEND = 'django-cache' + +# Import/Export Settings +IMPORT_EXPORT_USE_TRANSACTIONS = True + # Billing / Payments configuration STRIPE_PUBLIC_KEY = os.getenv('STRIPE_PUBLIC_KEY', '') STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', '') diff --git a/backend/requirements.txt b/backend/requirements.txt index 41db8f12..bc867666 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -15,3 +15,9 @@ psutil>=5.9.0 docker>=7.0.0 drf-spectacular>=0.27.0 stripe>=7.10.0 + +# Django Admin Enhancements +django-admin-interface==0.26.0 +django-import-export==3.3.1 +django-admin-rangefilter==0.11.1 +django-celery-results==2.5.1 diff --git a/backend/staticfiles/admin/js/cancel.js b/backend/staticfiles/admin/js/cancel.js index 3069c6f2..36c963f7 100644 --- a/backend/staticfiles/admin/js/cancel.js +++ b/backend/staticfiles/admin/js/cancel.js @@ -1,29 +1,20 @@ -'use strict'; -{ - // Call function fn when the DOM is loaded and ready. If it is already - // loaded, call the function now. - // http://youmightnotneedjquery.com/#ready - function ready(fn) { - if (document.readyState !== 'loading') { - fn(); - } else { - document.addEventListener('DOMContentLoaded', fn); - } - } +/** global: django */ - ready(function() { - function handleClick(event) { - event.preventDefault(); - const params = new URLSearchParams(window.location.search); - if (params.has('_popup')) { - window.close(); // Close the popup. - } else { - window.history.back(); // Otherwise, go back. - } - } - - document.querySelectorAll('.cancel-link').forEach(function(el) { - el.addEventListener('click', handleClick); +if (typeof(django) !== 'undefined' && typeof(django.jQuery) !== 'undefined') { + (function($) { + 'use strict'; + $(document).ready(function() { + $('.cancel-link').click(function(e) { + e.preventDefault(); + var parentWindow = window.parent; + if (parentWindow && typeof(parentWindow.dismissRelatedObjectModal) === 'function' && parentWindow !== window) { + parentWindow.dismissRelatedObjectModal(); + } else { + // fallback to default behavior + window.history.back(); + } + return false; + }); }); - }); + })(django.jQuery); } diff --git a/backend/staticfiles/admin/js/collapse.js b/backend/staticfiles/admin/js/collapse.js new file mode 100644 index 00000000..efe0b1b1 --- /dev/null +++ b/backend/staticfiles/admin/js/collapse.js @@ -0,0 +1,49 @@ +/*global gettext*/ +/* copied from django 4.0.7 */ +'use strict'; +{ + window.addEventListener('load', function() { + // Add anchor tag for Show/Hide link + const fieldsets = document.querySelectorAll('fieldset.collapse'); + for (const [i, elem] of fieldsets.entries()) { + // Don't hide if fields in this fieldset have errors + if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) { + const h2 = elem.querySelector('h2'); + const link = document.createElement('a'); + link.id = 'fieldsetcollapser' + i; + link.className = 'collapse-toggle'; + link.href = '#'; + // changed: can opt into starting visible + if (elem.classList.contains('expanded')) { + link.textContent = gettext('Hide'); + } else { + link.textContent = gettext('Show'); + elem.classList.add('collapsed'); + } + h2.appendChild(document.createTextNode(' (')); + h2.appendChild(link); + h2.appendChild(document.createTextNode(')')); + } + } + // Add toggle to hide/show anchor tag + const toggleFunc = function(ev) { + if (ev.target.matches('.collapse-toggle')) { + ev.preventDefault(); + ev.stopPropagation(); + const fieldset = ev.target.closest('fieldset'); + if (fieldset.classList.contains('collapsed')) { + // Show + ev.target.textContent = gettext('Hide'); + fieldset.classList.remove('collapsed'); + } else { + // Hide + ev.target.textContent = gettext('Show'); + fieldset.classList.add('collapsed'); + } + } + }; + document.querySelectorAll('fieldset.module').forEach(function(el) { + el.addEventListener('click', toggleFunc); + }); + }); +} diff --git a/backend/staticfiles/admin/js/popup_response.js b/backend/staticfiles/admin/js/popup_response.js index fecf0f47..dc24ef2b 100644 --- a/backend/staticfiles/admin/js/popup_response.js +++ b/backend/staticfiles/admin/js/popup_response.js @@ -1,15 +1,48 @@ -'use strict'; -{ - const initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse); - switch(initData.action) { - case 'change': - opener.dismissChangeRelatedObjectPopup(window, initData.value, initData.obj, initData.new_value); - break; - case 'delete': - opener.dismissDeleteRelatedObjectPopup(window, initData.value); - break; - default: - opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj); - break; +(function() { + + 'use strict'; + + var windowRef = window; + var windowRefProxy; + var windowName, widgetName; + var openerRef = windowRef.opener; + if (!openerRef) { + // related modal is active + openerRef = windowRef.parent; + windowName = windowRef.name; + widgetName = windowName.replace(/^(change|add|delete|lookup)_/, ''); + if (typeof(openerRef.id_to_windowname) === 'function') { + // django < 3.1 compatibility + widgetName = openerRef.id_to_windowname(widgetName); + } + windowRefProxy = { + name: widgetName, + location: windowRef.location, + close: function() { + openerRef.dismissRelatedObjectModal(); + } + }; + windowRef = windowRefProxy; } -} + + // default django popup_response.js + var initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse); + switch (initData.action) { + case 'change': + if (typeof(openerRef.dismissChangeRelatedObjectPopup) === 'function') { + openerRef.dismissChangeRelatedObjectPopup(windowRef, initData.value, initData.obj, initData.new_value); + } + break; + case 'delete': + if (typeof(openerRef.dismissDeleteRelatedObjectPopup) === 'function') { + openerRef.dismissDeleteRelatedObjectPopup(windowRef, initData.value); + } + break; + default: + if (typeof(openerRef.dismissAddRelatedObjectPopup) === 'function') { + openerRef.dismissAddRelatedObjectPopup(windowRef, initData.value, initData.obj); + } + break; + } + +})(); diff --git a/backend/staticfiles/admin_interface/collapsible-inlines/collapsible-inlines.js b/backend/staticfiles/admin_interface/collapsible-inlines/collapsible-inlines.js new file mode 100644 index 00000000..1f560f46 --- /dev/null +++ b/backend/staticfiles/admin_interface/collapsible-inlines/collapsible-inlines.js @@ -0,0 +1,45 @@ +/** global: django */ + +if (typeof(django) !== 'undefined' && typeof(django.jQuery) !== 'undefined') +{ + (function($) { + + $(document).ready(function(){ + + function collapsibleInline(scope, collapsed) { + var fieldsetCollapsed = collapsed; + var fieldsetEl = $(scope).find('> fieldset.module'); + fieldsetEl.addClass('collapse'); + var fieldsetHasErrors = (fieldsetEl.children('.errors').length > 0); + if (fieldsetHasErrors === true) { + fieldsetCollapsed = false; + } + if (fieldsetCollapsed === true) { + fieldsetEl.addClass('collapsed'); + } + var collapseToggleText = (fieldsetCollapsed ? gettext('Show') : gettext('Hide')); + var collapseToggleHTML = ' (' + collapseToggleText + ')'; + var headerEl = fieldsetEl.find('> h2,> h3'); + headerEl.append(collapseToggleHTML); + } + + var stackedInlinesOptionSel = '.admin-interface.collapsible-stacked-inlines'; + var stackedInlinesSel = stackedInlinesOptionSel + ' .inline-group[data-inline-type="stacked"]'; + var stackedInlinesCollapsed = $(stackedInlinesOptionSel).hasClass('collapsible-stacked-inlines-collapsed'); + + var tabularInlinesOptionSel = '.admin-interface.collapsible-tabular-inlines'; + var tabularInlinesSel = tabularInlinesOptionSel + ' .inline-group[data-inline-type="tabular"] .inline-related.tabular'; + var tabularInlinesCollapsed = $(stackedInlinesOptionSel).hasClass('collapsible-tabular-inlines-collapsed'); + + $(stackedInlinesSel).each(function() { + collapsibleInline(this, stackedInlinesCollapsed); + }); + + $(tabularInlinesSel).each(function() { + collapsibleInline(this, tabularInlinesCollapsed); + }); + + }); + + })(django.jQuery); +} diff --git a/backend/staticfiles/admin_interface/css/admin-interface-fix.css b/backend/staticfiles/admin_interface/css/admin-interface-fix.css new file mode 100644 index 00000000..e4cb7eaa --- /dev/null +++ b/backend/staticfiles/admin_interface/css/admin-interface-fix.css @@ -0,0 +1,610 @@ +.admin-interface { + overflow-x: hidden; +} + +/* fix login */ +.admin-interface.login #container { + width: 100%; + max-width: 360px; + margin: 15px auto; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; +} + +.admin-interface.login #content { + padding: 15px 30px 30px 30px; +} + +@media (min-width:768px){ + .admin-interface.login #container { + margin: 90px auto; + } +} + +.admin-interface.login #header { + min-height: auto; + padding: 10px 30px; + line-height: 30px; + align-items: center; + justify-content: flex-start; +} + +.admin-interface.login #header #branding h1 { + margin-right:0; +} + +.admin-interface.login #header #branding h1 img.logo { + margin-right: 0; +} + +.admin-interface.login #header #branding h1 img.logo+span { + display: block; +} + +.admin-interface.login #login-form { + display: flex; + flex-direction: column; +} + +.admin-interface.login .submit-row { + float: left; + width: 100%; + margin-top: 20px; + padding-top: 0; + padding-left: 0; + text-align: right; +} + +.admin-interface.login .submit-row label { + display: none; +} + +.admin-interface.login .submit-row input[type="submit"] { + width: 100%; + text-transform: uppercase; +} + +.admin-interface.login #footer { + display: none; +} +/* end login fix*/ + +.admin-interface #header { + height: auto; + min-height: 55px; + box-sizing: border-box; + display: flex; + justify-content: space-between; + align-items: center; +} + +@media (max-width:1024px) { + .admin-interface #header { + align-items: start; + } +} + +.admin-interface #branding h1 img.logo { + margin-top:10px; + margin-bottom:10px; + margin-right:15px; + display:inline-block !important; /* override inline display:none; */ +} + +.admin-interface #branding h1 span { + display: inline-block; +} + +.admin-interface #branding h1 img.logo+span { + white-space:nowrap; +} + +.admin-interface #user-tools { + margin-top: 10px; + margin-bottom: 10px; + white-space: nowrap; + align-self: flex-start; +} + +.admin-interface #user-tools br { + display: none; +} +@media (max-width: 768px) { + .admin-interface #user-tools br { + display: block; + } +} + +.admin-interface fieldset.collapse { + border: 1px solid transparent; +} + +.admin-interface fieldset.collapse.collapsed a.collapse-toggle, +.admin-interface fieldset.collapse a.collapse-toggle, +.admin-interface .inline-group .inline-related fieldset.module a.collapse-toggle, +.admin-interface .inline-group .inline-related fieldset.module.collapsed a.collapse-toggle { + font-weight: normal; + text-transform: lowercase; + font-size: 12px; + text-decoration: underline; + padding: 0 1px; +} + +@media (min-width: 1024px) { + .admin-interface #changelist .actions .button, + .admin-interface #changelist .actions .action-counter { + margin-left: 8px; + } +} + +.admin-interface #changelist .paginator { + margin-top:-1px; /* merge 2 borders into 1 */ + line-height:42px; +} + +.admin-interface .paginator a, +.admin-interface .paginator a:link, +.admin-interface .paginator a:visited, +.admin-interface .paginator .this-page { + padding:7px 12px; +} + +.admin-interface .paginator a, +.admin-interface .paginator .this-page { + margin-left:0px; +} + +.admin-interface .paginator .this-page, +.admin-interface .paginator a.end { + margin-right:25px; +} + +.admin-interface .paginator .this-page + a:not(.showall) { + margin-left:-25px; +} + +body.admin-interface .paginator a.showall, +body.admin-interface .paginator a.showall:link, +body.admin-interface .paginator a.showall:visited { + margin-left:20px; +} + +/* fix help text icon on newline */ +.admin-interface .inline-group thead th { + white-space:nowrap; +} + +.admin-interface .inline-group thead th img { + vertical-align: -2px; + margin-left: 5px; +} + +.admin-interface .inline-group .inlinechangelink { + background-size: contain; + padding-left: 15px; + margin-left: 10px; +} + +.admin-interface .file-thumbnail > a { + display: inline-block; +} + +.admin-interface .aligned p.file-upload { + display:table; +} + +.admin-interface form .form-row p.file-upload > a { + margin-right:20px; +} + +.admin-interface form .form-row p.file-upload .clearable-file-input { + display:inline-block; +} + +.admin-interface form .form-row p.file-upload .clearable-file-input label { + padding-bottom:0px; + margin-left:2px; +} + +.admin-interface form .form-row p.file-upload > input[type="file"] { + margin-top: 0px; +} + +@media (max-width:767px){ + + .admin-interface form .form-row p.file-upload { + width: 100%; + } + + .admin-interface form .form-row p.file-upload > a { + margin-right:0px; + display: block; + white-space: pre-wrap; + word-break: break-word; + } + + .admin-interface form .form-row p.file-upload .clearable-file-input { + display: block; + margin-top: 10px; + margin-left: 0; + margin-bottom: -10px; + } + + .admin-interface form .form-row p.file-upload > input[type="file"] { + display: block; + width: auto; + padding: 0px; + } + + /* fix inline horizontal scroll caused by checkbox-row */ + .admin-interface form .form-row > div.checkbox-row { + width: 100%; + } +} + + +/* FIX WIDE FIELDSET HELPS / ERROR MESSAGES */ +.admin-interface form .wide p.help, +.admin-interface form .wide div.help { + padding-left: 50px; +} + +.admin-interface form .wide input + p.help, +.admin-interface form .wide input + div.help { + margin-left: 160px; +} + +@media (max-width:767px){ + .admin-interface form .form-row div.help { + display: block; + width: 100%; + } +} + +.admin-interface form .wide ul.errorlist { + margin-left: 200px; +} + +/* LIST FILTER */ +.admin-interface .module.filtered h2 { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.admin-interface .module.filtered #changelist-filter { + min-width: 240px; +} + +@media (max-width: 1024px) { + .admin-interface .module.filtered #changelist-filter { + min-width: 200px; + } +} + +.admin-interface .module.filtered #changelist-filter h2 { + font-size: 11px; + padding: 10px 15px; +} + +.admin-interface .module.filtered #changelist-filter h2 + h3 { + margin-top: 0px; +} + +.admin-interface .module.filtered #changelist-filter h3 { + margin-top: 12px; + margin-bottom: 12px; +} + +.admin-interface #changelist-form .results { + scrollbar-width: thin; +} + +/* begin fix issue #13 - Datetime widget broken in long inlines */ +.admin-interface p.datetime { + white-space:nowrap; +} +/* end fix */ + +/* begin fix lateral padding to align text with field labels */ +.admin-interface .module h2, +.admin-interface.dashboard .module caption, +.admin-interface.dashboard .module th, +.admin-interface .module.filtered h2, +.admin-interface .inline-group h2, +.admin-interface #nav-sidebar .module caption, +.admin-interface #nav-sidebar .module th { + padding-left: 10px; + padding-right: 10px; +} +/* end fix */ + +/* begin fix restrict tabular-inline horizontal-scroll to inline-group instead of whole page */ +.admin-interface .inline-group[data-inline-type="tabular"] { + overflow-x:auto; +} +/* end fix */ + +/* begin fix stacked-inline margin-bottom in responsive small viewport */ +.admin-interface .inline-group[data-inline-type="stacked"] .module { + margin-bottom:0px; +} +/* end fix */ + +/* begin fix tabular inlines horizontal scroll */ +.admin-interface .inline-related.tabular { + overflow-x: scroll; + overflow-y: hidden; +} +.admin-interface .inline-related.tabular fieldset.module { + display: contents; + width: 100%; + white-space: nowrap; + position: relative; +} +.admin-interface .inline-related.tabular fieldset.module h2 { + position: sticky; + left: 0; +} +.admin-interface .inline-related.tabular fieldset.module table { + scrollbar-width: thin; +} +.admin-interface .inline-related.tabular fieldset.module table tbody tr { + position: relative; +} +/* end fix */ + +.admin-interface .inline-related h3 { + padding:6px 10px; +} + +/* begin fix issue #12 - Inlines bad delete buttons alignement */ +.admin-interface .inline-group .tabular thead th:last-child:not([class]):not([style]) { + text-align:right; +} + +.admin-interface .inline-group .tabular tr td { + vertical-align: top; +} + +.admin-interface .inline-group .tabular tr td.delete { + text-align:right; + padding-right:15px; + vertical-align: top; +} + +.admin-interface .inline-group .tabular tr td input[type="checkbox"] { + margin: 7px 0px; +} + +.admin-interface .inline-group .tabular tr td.delete a.inline-deletelink { + margin-top:4px; + overflow:hidden; + text-indent:9999px; +} +/* end fix */ + +/* top-right buttons color on hover -> just a lighten grey */ +.admin-interface .object-tools a { + color:#FFFFFF; +} +.admin-interface .object-tools a:focus, +.admin-interface .object-tools a:hover, +.admin-interface .object-tools li:focus a, +.admin-interface .object-tools li:hover a { + background-color:#AAAAAA; +} + +/* improve responsive selector */ + +/* fix [stacked, not-stacked] equalize horizontal and vertical select padding for selector */ +.admin-interface .selector .selector-available select, +.admin-interface .selector .selector-chosen select { + padding: 7px 10px; + display: block; +} + +/* fix [stacked, not-stacked] select options text overflow */ +.admin-interface .selector .selector-available select option, +.admin-interface .selector .selector-chosen select option { + overflow: hidden; + text-overflow: ellipsis; +} + +/* fix [not-stacked] equalize selectors height by adding the height of the .selector-available filter-bar */ +.admin-interface .selector:not(.stacked) .selector-chosen select { + height: calc(46px + 17.2em) !important; +} + +/* fix nav-sidebar (added in django 3.1.0) */ +.admin-interface #toggle-nav-sidebar { + top: 10px; + left: 0; + z-index: 20; + flex: 0 0 30px; + width: 30px; + height: 45px; + margin-top: 10px; + margin-right: -30px; + background-color: #FFFFFF; + font-size: 16px; + border: 1px solid #eaeaea; + border-left: none; + outline: none; + -webkit-box-shadow: 4px 4px 8px -4px #DBDBDB; + -moz-box-shadow: 4px 4px 8px -4px #DBDBDB; + box-shadow: 4px 4px 8px -4px #DBDBDB; + /*transition: left .3s;*/ +} + +.admin-interface .toggle-nav-sidebar::before { + margin-top: -2px; +} + +.admin-interface .main > #nav-sidebar + .content, +.admin-interface .main.shifted > #nav-sidebar + .content { + max-width: 100%; +} + +/* hide nav-sidebar below 1280px to prevent horizontal overflow issues */ +@media (max-width:1279px) { + .admin-interface #nav-sidebar, + .admin-interface #toggle-nav-sidebar { + display: none; + } +} + +.admin-interface #nav-sidebar { + flex: 0 0 360px; + left: -360px; + margin-left: -360px; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + padding: 40px 40px 40px 40px; + border-top: none; + border-bottom: none; + border-left: none; + scrollbar-width: thin; + /*transition: left .3s, margin-left .3s;*/ +} + +.admin-interface #nav-filter { + background-color: transparent; + border-radius: 4px; + height: 30px; + margin: 0 0 30px 0; + padding: 5px 6px; + outline-width: initial; +} + +@media (min-width:1280px) { + .admin-interface #main.shifted > #toggle-nav-sidebar { + left: 359px; + } + .admin-interface #main.shifted > #nav-sidebar { + left: 0px; + margin-left: 0; + } + .admin-interface #main:not(.shifted) > .content { + max-width: 100%; + } + .admin-interface.change-list:not(.popup) #main.shifted > #nav-sidebar + .content, + .admin-interface.change-form:not(.popup) #main.shifted > #nav-sidebar + .content { + max-width: calc(100% - 360px); + } +} + +/* fixed related widget and select2 */ +/* begin fix issue #10 - Related widget broken in long tabular inline */ +.admin-interface .related-widget-wrapper { + white-space: nowrap; +} +/* end fix */ + +.admin-interface .related-widget-wrapper select + .related-widget-wrapper-link, +.admin-interface .related-widget-wrapper .select2-container + .related-widget-wrapper-link { + margin-left: 12px !important; +} + +@media (min-width: 768px) { + .admin-interface.change-form select { + min-width: 150px; + } +} + +@media (min-width: 1024px) { + .admin-interface.change-form select { + min-width: 200px; + } +} + +.admin-interface.change-form .inline-related.tabular select { + min-width: auto !important; +} + +/* fixed time widget header border radius */ +.admin-interface .clockbox.module h2 { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +/* fix searchbar overriden padding */ +.admin-interface #changelist #changelist-search #searchbar { + padding: 2px 5px 3px 5px; +} + +@media (min-width: 1024px) { + .admin-interface #changelist #changelist-search #searchbar, + .admin-interface #changelist #changelist-search input[type="submit"], + .admin-interface #changelist #changelist-search .quiet { + margin-left: 8px; + } + .admin-interface #changelist #changelist-search label img { + vertical-align: text-top; + margin-right: 0px; + } +} + +@media (max-width: 1024px) { + .admin-interface #changelist #toolbar { + border-top: 1px solid #eee; + border-bottom: 1px solid #eee; + } + /* fixed changelist search size when there are search results and .quiet is visible */ + .admin-interface #changelist-search label img { + margin-top: 2px; + } + .admin-interface #changelist-search .quiet { + margin: 0 0 0 10px; + align-self: center; + flex-basis: content; + } +} + +@media (max-width: 767px) { + /* fixed responsive widgets */ + .admin-interface .aligned.collapsed .form-row { + display: none; + } + + .admin-interface .aligned .form-row > div { + display: flex; + max-width: 100vw; + flex-direction: column; + align-items: flex-start; + } + + .admin-interface .aligned .form-row .help { + margin-left: 0; + } + + .admin-interface .aligned .form-row .checkbox-row label { + margin: 10px 0 0 0; + padding: 0; + } + + .admin-interface .aligned .form-row input[type="file"], + .admin-interface .aligned .form-row input[type="text"], + .admin-interface .aligned .form-row input[type="email"] { + width: 100%; + } + + /* fix textarea horizontal scroll on Firefox */ + .admin-interface .aligned .form-row textarea { + width: 100% !important; + flex: 0 1 auto; + } + + .admin-interface .aligned .form-row .datetime input[type="text"] { + width: 50%; + } + + .admin-interface .aligned .form-row span + .file-upload { + margin-top: 10px; + } + + .admin-interface .aligned .form-row .file-upload input[type="file"] { + margin-top: 5px; + } +} diff --git a/backend/staticfiles/admin_interface/css/admin-interface.css b/backend/staticfiles/admin_interface/css/admin-interface.css new file mode 100644 index 00000000..bfa9ba40 --- /dev/null +++ b/backend/staticfiles/admin_interface/css/admin-interface.css @@ -0,0 +1,497 @@ +@media (prefers-color-scheme: dark) { + :root .admin-interface { + --primary: #79aec8; + --secondary: #417690; + --accent: #f5dd5d; + --primary-fg: #fff; + --body-fg: #333; + --body-bg: #fff; + --body-quiet-color: #666; + --body-loud-color: #000; + --header-color: #ffc; + --header-branding-color: var(--accent); + --header-bg: var(--secondary); + --header-link-color: var(--primary-fg); + --breadcrumbs-fg: #c4dce8; + --breadcrumbs-link-fg: var(--body-bg); + --breadcrumbs-bg: var(--primary); + --link-fg: #447e9b; + --link-hover-color: #036; + --link-selected-fg: #5b80b2; + --hairline-color: #e8e8e8; + --border-color: #ccc; + --error-fg: #ba2121; + --message-success-bg: #dfd; + --message-warning-bg: #ffc; + --message-error-bg: #ffefef; + --darkened-bg: #f8f8f8; + --selected-bg: #e4e4e4; + --selected-row: #ffc; + --button-fg: #fff; + --button-bg: var(--primary); + --button-hover-bg: #609ab6; + --default-button-bg: var(--secondary); + --default-button-hover-bg: #205067; + --close-button-bg: #888; + --close-button-hover-bg: #747474; + --delete-button-bg: #ba2121; + --delete-button-hover-bg: #a41515; + --object-tools-fg: var(--button-fg); + --object-tools-bg: var(--close-button-bg); + --object-tools-hover-bg: var(--close-button-hover-bg); + } +} + +.admin-interface #header { + background: var(--admin-interface-header-background-color); + color: var(--admin-interface-header-text-color); +} + +.admin-interface #header + #main { + border-top: var(--admin-interface-main-border-top); +} + +.admin-interface .environment-label { +} + +.admin-interface .environment-label::before { + content: ""; + display: inline-block; + width: 8px; + height: 8px; + background-color: var(--admin-interface-env-color); + border-radius: 100%; + margin-right: 6px; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; +} + +.admin-interface .environment-label::after { + content: " - "; +} + +@media (max-width: 1024px) { + .admin-interface .environment-label::after { + content: ""; + } +} + +.admin-interface .language-chooser { + display: inline-block; + position: absolute; + top: 15px; + right: 15px; + z-index: 10; +} + +@media (min-width: 768px) { + .admin-interface .language-chooser { + right: 30px; + } +} + +@media (min-width: 1024px) { + .admin-interface .language-chooser { + position: static; + float: right; + margin-left: 20px; + } +} + +.admin-interface .language-chooser-hidden-form { + display: none; +} + +.admin-interface .language-chooser-select-form { + display: inline-block; +} + +.admin-interface #branding h1, +.admin-interface.login #header h1, +.admin-interface.login #header h1 a { + color: var(--admin-interface-title-color); +} + +.admin-interface #branding h1 a { + color: inherit; +} + +.admin-interface #branding h1 .logo.default { + background-color: transparent; + background-repeat: no-repeat; + background-position: center center; + background-size: 104px 36px; + background-image: var(--admin-interface-logo-default-background-image); +} + +.admin-interface #branding h1 img.logo, +.admin-interface.login #header #branding h1 img.logo { + max-width: var(--admin-interface-logo-max-width); + max-height: var(--admin-interface-logo-max-height); +} + +.admin-interface #header #user-tools a { + color: var(--admin-interface-header-link-color); +} + +.admin-interface #header #user-tools a:hover, +.admin-interface #header #user-tools a:active { + color: var(--admin-interface-header-link-hover-color); + border-bottom-color: rgba(255, 255, 255, 0.5); +} + +.admin-interface #nav-sidebar .current-app .section:link, +.admin-interface #nav-sidebar .current-app .section:visited { + color: var(--admin-interface-module-link-selected-color); + font-weight: normal; +} + +.admin-interface #nav-sidebar .current-app .section:focus, +.admin-interface #nav-sidebar .current-app .section:hover { + color: var(--admin-interface-module-link-hover-color); +} + +.admin-interface #nav-sidebar .current-model { + background: var(--admin-interface-module-background-selected-color); +} + +.admin-interface #changelist table tbody tr.selected { + background-color: var(--admin-interface-module-background-selected-color); +} + +.admin-interface .module h2, +.admin-interface .module caption, +.admin-interface .module.filtered h2 { + background: var(--admin-interface-module-background-color); + color: var(--admin-interface-module-text-color); +} + +.admin-interface .module a.section:link, +.admin-interface .module a.section:visited { + color: var(--admin-interface-module-link-color); +} + +.admin-interface .module a.section:active, +.admin-interface .module a.section:hover { + color: var(--admin-interface-module-link-hover-color); +} + +.admin-interface div.breadcrumbs { + background: var(--admin-interface-module-background-color); + color: var(--admin-interface-module-text-color); +} + +.admin-interface div.breadcrumbs a { + color: var(--admin-interface-module-link-color); +} + +.admin-interface div.breadcrumbs a:active, +.admin-interface div.breadcrumbs a:focus, +.admin-interface div.breadcrumbs a:hover { + color: var(--admin-interface-module-link-hover-color); +} + +.admin-interface fieldset.collapse a.collapse-toggle, +.admin-interface fieldset.collapse.collapsed a.collapse-toggle, +.admin-interface .inline-group .inline-related fieldset.module a.collapse-toggle, +.admin-interface .inline-group .inline-related fieldset.module.collapsed a.collapse-toggle { + color: var(--admin-interface-module-link-color); +} + +.admin-interface fieldset.collapse a.collapse-toggle:hover, +.admin-interface fieldset.collapse a.collapse-toggle:active, +.admin-interface fieldset.collapse.collapsed a.collapse-toggle:hover, +.admin-interface fieldset.collapse.collapsed a.collapse-toggle:active, +.admin-interface .inline-group .inline-related fieldset.module a.collapse-toggle:hover, +.admin-interface .inline-group .inline-related fieldset.module a.collapse-toggle:active, +.admin-interface .inline-group .inline-related fieldset.module.collapsed a.collapse-toggle:hover, +.admin-interface .inline-group .inline-related fieldset.module.collapsed a.collapse-toggle:active { + color: var(--admin-interface-module-link-hover-color); +} + +.admin-interface .inline-group h2 { + background: var(--admin-interface-module-background-color); + color: var(--admin-interface-module-text-color); +} + +.admin-interface .selector .selector-chosen h2 { + border-color: var(--admin-interface-module-background-color); + background: var(--admin-interface-module-background-color); + color: var(--admin-interface-module-text-color); +} + +.admin-interface .selector .selector-available h2, +.admin-interface .selector .selector-chosen h2 { + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; +} + +.admin-interface .selector a.selector-chooseall:focus, +.admin-interface .selector a.selector-chooseall:hover, +.admin-interface .selector a.selector-clearall:focus, +.admin-interface .selector a.selector-clearall:hover { + color: var(--admin-interface-generic-link-hover-color); +} + +.admin-interface a:link, +.admin-interface a:visited { + color: var(--admin-interface-generic-link-color); +} + +.admin-interface a:hover { + color: var(--admin-interface-generic-link-hover-color); +} + +.admin-interface thead th a, +.admin-interface thead th a:link, +.admin-interface thead th a:visited, +.admin-interface thead th a:focus, +.admin-interface thead th a:hover { + color: #666666; +} + +.admin-interface .button, +.admin-interface input[type=submit], +.admin-interface input[type=button], +.admin-interface .submit-row input, +.admin-interface a.button { + background: var(--admin-interface-save-button-background-color); + color: var(--admin-interface-save-button-text-color); +} + +.admin-interface .button:active, +.admin-interface .button:focus, +.admin-interface .button:hover, +.admin-interface input[type=submit]:active, +.admin-interface input[type=submit]:focus, +.admin-interface input[type=submit]:hover, +.admin-interface input[type=button]:active, +.admin-interface input[type=button]:focus, +.admin-interface input[type=button]:hover { + background: var(--admin-interface-save-button-background-hover-color); + color: var(--admin-interface-save-button-text-color); + outline: none; +} + +.admin-interface .button.default, +.admin-interface input[type=submit].default, +.admin-interface .submit-row input.default { + background: var(--admin-interface-save-button-background-color); + color: var(--admin-interface-save-button-text-color); + outline: none; +} + +.admin-interface .button.default:active, +.admin-interface .button.default:focus, +.admin-interface .button.default:hover, +.admin-interface input[type=submit].default:active, +.admin-interface input[type=submit].default:focus, +.admin-interface input[type=submit].default:hover, +.admin-interface.delete-confirmation form .cancel-link:hover { + background: var(--admin-interface-save-button-background-hover-color); + color: var(--admin-interface-save-button-text-color); + outline: none; +} + +.admin-interface .submit-row a.deletelink:link, +.admin-interface .submit-row a.deletelink:visited, +.admin-interface.delete-confirmation form input[type="submit"] { + background: var(--admin-interface-delete-button-background-color); + color: var(--admin-interface-delete-button-text-color); +} + +.admin-interface .submit-row a.deletelink:hover, +.admin-interface.delete-confirmation form input[type="submit"]:hover { + background: var(--admin-interface-delete-button-background-hover-color); + color: var(--admin-interface-delete-button-text-color); +} + +.admin-interface .paginator a, +.admin-interface .paginator a:link, +.admin-interface .paginator a:visited, +.admin-interface .paginator .this-page { + border-radius: var(--admin-interface-module-border-radius); +} + +.admin-interface .paginator a, +.admin-interface .paginator a:link, +.admin-interface .paginator a:visited { + background-color: #FFFFFF; + color: var(--admin-interface-generic-link-color); +} + +.admin-interface .paginator a:hover, +.admin-interface .paginator a:active { + background-color: #F8F8F8; + color: var(--admin-interface-generic-link-hover-color); +} + +.admin-interface .paginator .this-page { + background-color: var(--admin-interface-module-background-color); + color: var(--admin-interface-module-link-color); +} + +.admin-interface .paginator a.showall, +.admin-interface .paginator a.showall:link, +.admin-interface .paginator a.showall:visited { + color: var(--admin-interface-generic-link-color); +} + +.admin-interface .paginator a.showall:hover, +.admin-interface .paginator a.showall:active { + color: var(--admin-interface-generic-link-hover-color); +} + +/* list-filter sticky */ +@media (min-width: 768px) { + .admin-interface.list-filter-sticky .module.filtered #changelist-filter { + position: sticky; + top: 30px; + float: right; + z-index: 30; + display: flex; + flex-direction: column; + overflow-y: auto; + scrollbar-width: thin; + height: 100%; + max-height: calc(100vh - 60px); + } + .admin-interface.list-filter-sticky.sticky-pagination .module.filtered #changelist-filter { + max-height: calc(100vh - 125px); + } + + /* feature not available for django < 3.1.2 */ + .admin-interface.list-filter-sticky .module.filtered #toolbar + #changelist-filter { + position: absolute; + top: 0px; + z-index: 30; + max-height: calc(100vh - 105px); + } + .admin-interface.list-filter-sticky.sticky-pagination .module.filtered #toolbar + #changelist-filter { + max-height: calc(100vh - 170px); + } +} + +.admin-interface .module.filtered #changelist-filter { + border-radius: var(--admin-interface-module-border-radius); +} + +.admin-interface .module.filtered #changelist-filter h3#changelist-filter-clear { + margin-bottom: 0; +} + +.admin-interface .module.filtered #changelist-filter .changelist-filter-clear a { + font-size: 13px; + margin: .3em 0; + padding: 0 15px; +} + +.admin-interface .module.filtered #changelist-filter .changelist-filter-clear a:focus, +.admin-interface .module.filtered #changelist-filter .changelist-filter-clear a:hover, +.admin-interface .module.filtered #changelist-filter #changelist-filter-clear a:focus, +.admin-interface .module.filtered #changelist-filter #changelist-filter-clear a:hover { + color: #666; + text-decoration: none; +} + +.admin-interface .module.filtered #changelist-filter .changelist-filter-clear a span { + font-weight: bold; +} + +.admin-interface .module.filtered #changelist-filter li a:focus, +.admin-interface .module.filtered #changelist-filter li a:hover { + color: #666; + text-decoration: none; +} + +.admin-interface.list-filter-highlight .module.filtered #changelist-filter h3.active { + font-weight: bold; +} + +.admin-interface.list-filter-highlight .module.filtered #changelist-filter ul.active li.selected { + color: var(--admin-interface-module-text-color); + background: var(--admin-interface-module-background-color); + margin-left: -10px; + padding-left: 5px; + margin-right: -10px; + border-left: 5px solid var(--admin-interface-module-background-color); + border-right: 5px solid var(--admin-interface-module-background-color); + border-radius: var(--admin-interface-module-border-radius); +} + +.admin-interface.list-filter-highlight .module.filtered #changelist-filter ul.active li.selected a, +.admin-interface.list-filter-highlight .module.filtered #changelist-filter ul.active li.selected a:link, +.admin-interface.list-filter-highlight .module.filtered #changelist-filter ul.active li.selected a:visited, +.admin-interface.list-filter-highlight .module.filtered #changelist-filter ul.active li.selected a:focus, +.admin-interface.list-filter-highlight .module.filtered #changelist-filter ul.active li.selected a:hover { + background: inherit; + color: inherit; +} + +.admin-interface .module.filtered #changelist-filter li.selected a, +.admin-interface .module.filtered #changelist-filter li.selected a:link, +.admin-interface .module.filtered #changelist-filter li.selected a:visited, +.admin-interface .module.filtered #changelist-filter li.selected a:focus, +.admin-interface .module.filtered #changelist-filter li.selected a:hover { + color: var(--admin-interface-generic-link-hover-color); +} + +/* begin fix issue #11 - Inline border bottom should not be rounded */ +.admin-interface .module h2, +.admin-interface.dashboard .module caption, +.admin-interface #nav-sidebar .module th, +.admin-interface #nav-sidebar .module caption, +.admin-interface .module.filtered h2 { + border-radius: var(--admin-interface-module-border-radius); +} + +.admin-interface .inline-group h2 { + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; +} + +.admin-interface .module.collapse.collapsed h2 { + /* fix collapsed inlines rounded bottom borders */ + border-bottom-left-radius: var(--admin-interface-module-border-radius); + border-bottom-right-radius: var(--admin-interface-module-border-radius); +} + +/* end fix */ + +.admin-interface #content-related { + border-radius: var(--admin-interface-module-border-radius); +} + +.admin-interface .select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] { + background-color: var(--admin-interface-module-background-color); + color: var(--admin-interface-module-text-color); +} + +.admin-interface #toggle-nav-sidebar { + border-top-right-radius: var(--admin-interface-module-border-radius); + border-bottom-right-radius: var(--admin-interface-module-border-radius); + color: var(--admin-interface-generic-link-color); +} + +.admin-interface #toggle-nav-sidebar:focus, +.admin-interface #toggle-nav-sidebar:hover, +.admin-interface #toggle-nav-sidebar:active { + color: var(--admin-interface-generic-link-hover-color); +} + +.admin-interface .calendar td.selected a, +.admin-interface .calendar td a:active, +.admin-interface .calendar td a:focus, +.admin-interface .calendar td a:hover, +.admin-interface .timelist a:active, +.admin-interface .timelist a:focus, +.admin-interface .timelist a:hover { + background: var(--admin-interface-module-background-color); +} + +.admin-interface .calendarbox .calendarnav-previous, +.admin-interface .calendarbox .calendarnav-next { + transition: none; + filter: invert(100%); +} diff --git a/backend/staticfiles/admin_interface/css/ckeditor.css b/backend/staticfiles/admin_interface/css/ckeditor.css new file mode 100644 index 00000000..952120cb --- /dev/null +++ b/backend/staticfiles/admin_interface/css/ckeditor.css @@ -0,0 +1,126 @@ +/* +ckeditor + light theme +https://github.com/Ikimea/ckeditor-light-theme +*/ + +.admin-interface .cke { + border: none; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; +} + +.admin-interface .cke_inner, +.admin-interface .cke_wysiwyg_frame { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +.admin-interface .cke_inner { + border: 1px solid #CCCCCC; +} + +.admin-interface .cke_chrome { + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +.admin-interface .cke_top { + background: #f8f8f8; + border-top: none; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom: 1px solid #EEEEEE; + padding-left: 10px; + padding-right: 10px; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +.admin-interface .cke_toolgroup { + background: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +.admin-interface .cke_bottom { + background: #f8f8f8; + border-top: 1px solid #EEEEEE; + + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +.admin-interface .cke_source { + padding: 13px 15px; + box-sizing: border-box; +} + +.admin-interface a.cke_button, +.admin-interface a.cke_button:active, +.admin-interface a.cke_button:hover, +.admin-interface a.cke_button:focus { + box-shadow: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + background-image: none; + border-radius: 4px; + border: none; +} + +.admin-interface a.cke_button:active, +.admin-interface a.cke_button:hover, +.admin-interface a.cke_button:focus { + background-color: #E8E8E8 !important; +} + +.admin-interface a.cke_button.cke_button_on { + background-color: #CCCCCC !important; +} + +.admin-interface a.cke_button.cke_button_disabled { + background-color: transparent !important; +} + +.admin-interface .cke_resizer { + border-color: transparent #666666 transparent transparent; +} + +@media (max-width: 767px){ + + .admin-interface .django-ckeditor-widget, + .admin-interface .cke { + width: 100% !important; + } + + .admin-interface .cke_top { + padding-left: 10px; + padding-right: 10px; + } + + .admin-interface .cke_toolbar { + height: auto; + } + + .admin-interface .cke_contents { + height: auto; + } + + .admin-interface .tabular .django-ckeditor-widget, + .admin-interface .tabular .cke { + width: 400px !important; + } + + .admin-interface .tabular .cke_contents { + height: 90px !important; + } +} diff --git a/backend/staticfiles/admin_interface/css/form-controls.css b/backend/staticfiles/admin_interface/css/form-controls.css new file mode 100644 index 00000000..43073670 --- /dev/null +++ b/backend/staticfiles/admin_interface/css/form-controls.css @@ -0,0 +1,95 @@ +/* sticky pagination */ + +.admin-interface.sticky-pagination.change-list #content-main { + padding-bottom: 4.375rem; +} + +.admin-interface.sticky-pagination.change-list .paginator { + width: 100%; + position: fixed; + bottom: 0; + right: 0; + z-index: 40; + box-sizing: border-box; + padding-left: 15px; + padding-right: 15px; + white-space: nowrap; + text-overflow: ellipsis; + border-radius: 0; + border-top: 1px solid #EEEEEE !important; + border-bottom: none; + margin: 0; +} + +.admin-interface.sticky-pagination.change-list.popup .paginator { + padding-left: 20px; + padding-right: 20px; +} + +@media (min-width:768px) { + .admin-interface.sticky-pagination.change-list:not(.popup) .paginator { + padding-left: 30px; + padding-right: 30px; + } +} + +@media (min-width:1024px) { + .admin-interface.sticky-pagination.change-list:not(.popup) .paginator { + padding-left: 40px; + padding-right: 40px; + } +} + +@media (min-width:1280px) { + .admin-interface.sticky-pagination.change-list:not(.popup) #main.shifted > #nav-sidebar + .content .paginator { + width: calc(100% - 360px); + } +} + +/* sticky submit */ + +@media (min-width:768px) { + .admin-interface.sticky-submit.change-form #content-main { + padding-bottom: 4.375rem; + } + + .admin-interface.sticky-submit.change-form .submit-row:last-of-type { + width: 100%; + position: fixed; + bottom: 0; + right: 0; + z-index: 40; + box-sizing: border-box; + padding-left: 15px; + padding-right: 15px; + white-space: nowrap; + text-overflow: ellipsis; + border-radius: 0; + border-top: 1px solid #EEEEEE; + border-bottom: none !important; + margin: 0; + } + + .admin-interface.sticky-submit.change-form.popup .submit-row:last-of-type { + padding-left: 20px; + padding-right: 20px; + } + + .admin-interface.sticky-submit.change-form:not(.popup) .submit-row:last-of-type { + padding-left: 30px; + padding-right: 30px; + } +} + +@media (min-width:1024px) { + .admin-interface.sticky-submit.change-form:not(.popup) .submit-row:last-of-type { + padding-left: 40px; + padding-right: 40px; + } +} + +@media (min-width:1280px) { + .admin-interface.sticky-submit.change-form:not(.popup) #main.shifted > #nav-sidebar + .content .submit-row:last-of-type { + width: calc(100% - 359px); + } +} diff --git a/backend/staticfiles/admin_interface/css/import-export.css b/backend/staticfiles/admin_interface/css/import-export.css new file mode 100644 index 00000000..8ea0a092 --- /dev/null +++ b/backend/staticfiles/admin_interface/css/import-export.css @@ -0,0 +1,7 @@ +/* Fix left/right scrolling broken with django-import-export #165 */ +.admin-interface table.import-preview { + display: block; + width: 100%; + max-width: 100%; + overflow: auto; +} diff --git a/backend/staticfiles/admin_interface/css/jquery.ui.tabs.css b/backend/staticfiles/admin_interface/css/jquery.ui.tabs.css new file mode 100644 index 00000000..dfc2f94f --- /dev/null +++ b/backend/staticfiles/admin_interface/css/jquery.ui.tabs.css @@ -0,0 +1,247 @@ +/* +* jQuery UI CSS Framework +* Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about) +* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses. +* http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/themes/base/jquery.ui.core.css +*/ + +/* +backward compatibility: +.ui-tabs-selected: jquery ui < 1.10 +.ui-tabs-active classes jquery ui >= 1.10 +*/ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { + display: none; +} + +.ui-helper-hidden-accessible { + position: absolute; + left: -99999999px; +} + +.ui-helper-reset { + margin: 0; + padding: 0; + border: 0; + outline: 0; + line-height: 1.3; + text-decoration: none; + font-size: 100%; + list-style: none; +} + +.ui-helper-clearfix:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} + +.ui-helper-clearfix { + display: inline-block; +} + +/* required comment for clearfix to work in Opera \*/ +* html .ui-helper-clearfix { + height: 1%; +} + +.ui-helper-clearfix { + display: block; +} + +.ui-helper-zfix { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 0; + filter: Alpha(Opacity=0); +} + +.ui-state-disabled { + cursor: default !important; +} + +.ui-icon { + display: block; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; +} + +/* http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/themes/base/jquery.ui.tabs.css */ +.ui-widget-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.ui-tabs { + position: relative; + padding: .2em; + zoom: 1; +} + +/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ +.ui-tabs .ui-tabs-nav { + margin: 0; + padding: .2em .2em 0; +} + +.ui-tabs .ui-tabs-nav li { + list-style: none; + float: left; + position: relative; + top: 1px; + margin: 0 .2em 1px 0; + border-bottom: 0 !important; + padding: 0; + white-space: nowrap; +} + +.ui-tabs .ui-tabs-nav li a { + float: left; + padding: .5em 1em; + text-decoration: none; +} + +.ui-tabs .ui-tabs-nav li.ui-tabs-active, .ui-tabs .ui-tabs-nav li.ui-tabs-selected { + margin-bottom: 0; + padding-bottom: 1px; +} + +.ui-tabs .ui-tabs-nav li.ui-tabs-active a, .ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { + cursor: text; +} + +.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { + cursor: pointer; +} + +/* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ +.ui-tabs .ui-tabs-panel { + display: block; + border-width: 0; + padding: 1em 1.4em; + background: none; +} + +.ui-tabs .ui-tabs-hide { + position: absolute; + display: none; +} + +/* Custom tabs theme */ +.admin-interface .ui-tabs { + padding: 0; +} + +.admin-interface .ui-tabs, +.admin-interface .ui-tabs .ui-widget-header, +.admin-interface .ui-tabs .ui-widget-header .ui-state-default { + border: none; + background: transparent; +} + +.admin-interface .ui-tabs .ui-tabs-nav { + padding: 10px 0 0 10px; + border-bottom: none; +} + +.admin-interface .ui-tabs .ui-tabs-nav li { + margin: 0 0 0 -1px; +} + +.admin-interface .ui-tabs .ui-tabs-nav li.required { + font-weight: bold; +} + +.admin-interface .ui-tabs .ui-tabs-nav li a { + border: 1px solid #eeeeee; + background-color: #f8f8f8; + border-bottom: none; + color: #666666; + padding: 7px 14px 8px 14px; + margin-top: 1px; + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + font-size: 12px; + text-transform: uppercase; + outline: none; +} + +.admin-interface .ui-tabs .ui-tabs-nav li.ui-tabs-active a, +.admin-interface .ui-tabs .ui-tabs-nav li.ui-tabs-selected a { + padding: 8px 14px 8px 14px; + margin-top: 0px; + margin-bottom: -1px; + font-weight: bold; + background-color: #FFFFFF; + color: var(--admin-interface-module-background-color); + border-bottom: 1px solid #FFFFFF; +} + +.admin-interface .ui-tabs .ui-tabs-panel { + border: 1px solid #eeeeee; + border-radius: 4px; + padding: 10px; + margin-bottom: 30px; + overflow: hidden; +} + +.admin-interface .inline-group .tabular .ui-tabs .ui-tabs-panel { + padding: 8px; +} + +.admin-interface .inline-group .tabular .ui-tabs .ui-tabs-nav { + padding-left: 4px; +} + +.admin-interface .inline-group .tabular tr td { + vertical-align: top; +} + +.admin-interface .inline-group .tabular tr.has_original td.original, +.admin-interface .inline-group .tabular tr td.delete { + vertical-align: top; +} + +.admin-interface .inline-group .tabular .datetime > input { + margin-right: 5px; +} + +.admin-interface .inline-group .tabular .datetime br { + display: none; +} + +.admin-interface #changelist .row1:not(.selected):hover, +.admin-interface #changelist .row2:not(.selected):hover { + background: #f9f9f9; +} + +.admin-interface .row2 { + background: #fcfcfc; +} + +.admin-interface .row2 .ui-tabs .ui-tabs-nav li a { + background-color: #f5f5f5; + border: 1px solid #ebebeb; +} + +.admin-interface .row2 .ui-tabs .ui-tabs-nav li.ui-tabs-active a, +.admin-interface .row2 .ui-tabs .ui-tabs-nav li.ui-tabs-selected a { + background-color: #fcfcfc; + border-bottom: 1px solid #fcfcfc; +} diff --git a/backend/staticfiles/admin_interface/css/json-widget.css b/backend/staticfiles/admin_interface/css/json-widget.css new file mode 100644 index 00000000..e0a999d1 --- /dev/null +++ b/backend/staticfiles/admin_interface/css/json-widget.css @@ -0,0 +1,27 @@ +/* +django-json-widget support +https://github.com/jmrivas86/django-json-widget +*/ + +.admin-interface div.jsoneditor { + border: 1px solid var(--admin-interface-module-background-color); + border-radius: var(--admin-interface-jsoneditor-border-radius); + overflow: var(--admin-interface-jsoneditor-overflow); +} + +.admin-interface div.jsoneditor-menu { + background-color: var(--admin-interface-module-background-color); + border-bottom: 1px solid var(--admin-interface-module-background-color); +} + +.admin-interface div.jsoneditor-menu a.jsoneditor-poweredBy { + color: var(--admin-interface-module-link-color); +} + +.admin-interface div.jsoneditor-contextmenu ul li button.jsoneditor-selected, +.admin-interface div.jsoneditor-contextmenu ul li button.jsoneditor-selected:focus, +.admin-interface div.jsoneditor-contextmenu ul li button.jsoneditor-selected:hover { + background-color: var(--admin-interface-module-background-selected-color); + color: #000000; + font-weight: bold; +} diff --git a/backend/staticfiles/admin_interface/css/language-chooser.css b/backend/staticfiles/admin_interface/css/language-chooser.css new file mode 100644 index 00000000..8459b9c1 --- /dev/null +++ b/backend/staticfiles/admin_interface/css/language-chooser.css @@ -0,0 +1,72 @@ +.admin-interface .language-chooser { + display: inline-block; + position: absolute; + top: 15px; + right: 15px; + z-index: 10; +} + +@media (min-width: 768px) { + .admin-interface .language-chooser { + right: 30px; + } +} + +@media (min-width: 1024px) { + .admin-interface .language-chooser { + position: static; + margin-left: 20px; + } +} + +.admin-interface .language-chooser .language-chooser-hidden-form { + display: none; +} + +.admin-interface .language-chooser .language-chooser-select-form { + display: inline-block; + position: relative; + z-index: 0; +} + +.admin-interface .language-chooser select { + width: auto; + min-width: auto; +} + +.admin-interface .language-chooser.minimal .language-chooser-select-form::after { + content: ""; + position: absolute; + right: 2px; + top: 50%; + border: solid var(--admin-interface-header-text-color); + border-width: 0px 0px 1px 1px; + display: inline-block; + padding: 2px; + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + pointer-events: none; + margin-top: -4px; +} + +.admin-interface .language-chooser.minimal .language-chooser-select-form:hover select { + border-bottom: 1px solid transparent; + color: var(--admin-interface-header-link-hover-color); +} + +.admin-interface .language-chooser.minimal select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: transparent; + border: none; + border-bottom: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 0; + color: var(--admin-interface-header-link-color); + cursor: pointer; + font-weight: inherit; + font-size: inherit; + height: auto; + margin: 0; + padding: 0 15px 0 0; +} diff --git a/backend/staticfiles/admin_interface/css/list-filter-dropdown.css b/backend/staticfiles/admin_interface/css/list-filter-dropdown.css new file mode 100644 index 00000000..207b3c7a --- /dev/null +++ b/backend/staticfiles/admin_interface/css/list-filter-dropdown.css @@ -0,0 +1,27 @@ +/* +list-filter-dropdown +*/ + +.admin-interface #changelist-filter .list-filter-dropdown { + margin-top: 15px; + margin-bottom: 15px; +} + +.admin-interface #changelist-filter h2 + .list-filter-dropdown, +.admin-interface #changelist-filter .list-filter-dropdown + .list-filter-dropdown { + margin-top: 5px; +} + +.admin-interface #changelist-filter .list-filter-dropdown h3 { + margin-top: 0 !important; +} + +.admin-interface #changelist-filter .list-filter-dropdown select { + background-color: #FFFFFF; + width: calc(100% - 30px); + margin-right: 15px; +} + +.admin-interface.list-filter-highlight #changelist-filter .list-filter-dropdown h3.active + div select { + font-weight: bold; +} diff --git a/backend/staticfiles/admin_interface/css/modeltranslation.css b/backend/staticfiles/admin_interface/css/modeltranslation.css new file mode 100644 index 00000000..9e1890b3 --- /dev/null +++ b/backend/staticfiles/admin_interface/css/modeltranslation.css @@ -0,0 +1,17 @@ +/* +django-modeltranslation support +https://github.com/deschler/django-modeltranslation +*/ + +.admin-interface #content h1 select { + text-transform: uppercase; + margin-left: 15px; + min-width: 50px; +} + +.admin-interface .ui-tabs .ui-tabs-panel[id^=tab_id_] { + border: none; + border-top: 1px solid #eeeeee; + padding: 0; + margin-bottom: 0; +} diff --git a/backend/staticfiles/admin_interface/css/rangefilter.css b/backend/staticfiles/admin_interface/css/rangefilter.css new file mode 100644 index 00000000..25b4f0ba --- /dev/null +++ b/backend/staticfiles/admin_interface/css/rangefilter.css @@ -0,0 +1,25 @@ +.admin-interface #changelist-filter .admindatefilter { + border-bottom: 1px solid var(--hairline-color); +} + +.admin-interface #changelist-filter .admindatefilter .button, +.admin-interface #changelist-filter .admindatefilter .submit-row input, +.admin-interface #changelist-filter .admindatefilter a.button, +.admin-interface #changelist-filter .admindatefilter input[type="submit"], +.admin-interface #changelist-filter .admindatefilter input[type="button"], +.admin-interface #changelist-filter .admindatefilter input[type="reset"] { + background: var(--admin-interface-module-background-color); + color: var(--admin-interface-module-link-color); + padding: 6px 10px; + font-size: 12px; + margin-right: 4px; +} + +.admin-interface #changelist-filter .admindatefilter .button:hover, +.admin-interface #changelist-filter .admindatefilter .submit-row input:hover, +.admin-interface #changelist-filter .admindatefilter a.button:hover, +.admin-interface #changelist-filter .admindatefilter input[type="submit"]:hover, +.admin-interface #changelist-filter .admindatefilter input[type="button"]:hover, +.admin-interface #changelist-filter .admindatefilter input[type="reset"]:hover { + color: var(--admin-interface-module-link-hover-color); +} diff --git a/backend/staticfiles/admin_interface/css/recent-actions.css b/backend/staticfiles/admin_interface/css/recent-actions.css new file mode 100644 index 00000000..6e251648 --- /dev/null +++ b/backend/staticfiles/admin_interface/css/recent-actions.css @@ -0,0 +1,10 @@ +.admin-interface.dashboard #content { + width: auto; + max-width: 600px; + margin-right: 0; + margin-left: 0; +} + +.admin-interface.dashboard #content #recent-actions-module { + display: none; +} diff --git a/backend/staticfiles/admin_interface/css/related-modal.css b/backend/staticfiles/admin_interface/css/related-modal.css new file mode 100644 index 00000000..015752f8 --- /dev/null +++ b/backend/staticfiles/admin_interface/css/related-modal.css @@ -0,0 +1,82 @@ +/* +related modal + magnific popup customization +https://github.com/dimsemenov/Magnific-Popup +*/ +.admin-interface .related-modal.mfp-bg { + background-color: var(--admin-interface-related-modal-background-color); + opacity: var(--admin-interface-related-modal-background-opacity); +} + +.admin-interface .related-modal .mfp-content { + height: 100%; + -webkit-box-shadow: 0px 5px 30px 0px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0px 5px 30px 0px rgba(0, 0, 0, 0.2); + box-shadow: 0px 5px 30px 0px rgba(0, 0, 0, 0.2); +} + +.admin-interface .related-modal .mfp-container { + padding: 80px 80px 80px 80px; +} + +.admin-interface .related-modal__nested .mfp-container { + padding: 40px 80px 40px 80px; +} + +@media (max-width: 640px) { + .admin-interface .related-modal .mfp-container { + padding: 80px 20px 80px 20px; + } + + .admin-interface .related-modal__nested .mfp-container { + padding: 40px 40px 40px 40px; + } +} + +@media (max-height: 640px) { + .admin-interface .related-modal .mfp-container { + padding-top: 20px; + padding-bottom: 20px; + } + + .admin-interface .related-modal__nested .mfp-container { + padding: 40px 40px 40px 40px; + } +} + +.admin-interface .related-modal .related-modal-iframe-container { + display: block; + width: 100%; + height: 100%; + position: relative; + z-index: 100; + overflow: hidden; + border-radius: var(--admin-interface-related-modal-border-radius); +} + +.admin-interface .related-modal #related-modal-iframe { + width: 100%; + height: 100%; + background-color: #FFFFFF; + background-repeat: no-repeat; + background-position: center center; + background-size: 30px 30px; + background-image: url("data:image/svg+xml;utf8,"); + border: none; + margin: 0 auto; + padding: 0; + display: block; +} + +.admin-interface .related-modal .mfp-close { + width: 40px; + height: 40px; + opacity: 1.0; + color: rgba(0, 0, 0, 0.4); + display: var(--admin-interface-related-modal-close-button-display); +} + +.admin-interface .related-modal .mfp-close:hover, +.admin-interface .related-modal .mfp-close:active { + color: rgba(0, 0, 0, 0.6); + top: 0; +} diff --git a/backend/staticfiles/admin_interface/css/rtl.css b/backend/staticfiles/admin_interface/css/rtl.css new file mode 100644 index 00000000..30c0b9d0 --- /dev/null +++ b/backend/staticfiles/admin_interface/css/rtl.css @@ -0,0 +1,34 @@ +[dir="rtl"] .admin-interface, +[dir="rtl"] .admin-interface * { + font-family: 'Vazir', sans-serif !important; +} + +[dir="rtl"] .admin-interface .main .toggle-nav-sidebar.sticky { + left: auto !important; + right: 0px !important; + margin-right: 0px !important; + margin-left: 10px; + border: 1px solid #eaeaea !important; + border-right: none !important; + border-top-right-radius: 0px !important; + border-bottom-right-radius: 0px !important; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + box-shadow: -4px 2px 8px -2px #DBDBDB !important; +} + +[dir="rtl"] .admin-interface #main.shifted > #toggle-nav-sidebar { + right: 359px !important; +} + +[dir="rtl"] .admin-interface #main > #nav-sidebar { + margin-right: -360px !important; + margin-left: 0px !important; + right: -320px !important; +} + +[dir="rtl"] .admin-interface #main.shifted > #nav-sidebar { + border-left: 1px solid #eaeaea; + margin-right: 0px !important; + padding: 40px 0px 40px 40px !important; +} diff --git a/backend/staticfiles/admin_interface/css/sorl-thumbnail.css b/backend/staticfiles/admin_interface/css/sorl-thumbnail.css new file mode 100644 index 00000000..44c61909 --- /dev/null +++ b/backend/staticfiles/admin_interface/css/sorl-thumbnail.css @@ -0,0 +1,67 @@ +/* +sorl-thumbnail - improved AdminImageMixin widget layout +https://github.com/mariocesar/sorl-thumbnail +*/ + +.admin-interface a.thumbnail + a { + font-weight: normal; +} + +.admin-interface a.thumbnail + a + input[type="checkbox"] { + margin: 10px 0px 10px 18px; +} + +@media (max-width: 767px){ + .admin-interface a.thumbnail + a { + display: block; + margin-top: 3px; + white-space: pre-wrap; + word-break: break-word; + } + .admin-interface a.thumbnail + a + input[type="checkbox"] { + margin: 15px 0; + } +} + +.admin-interface a.thumbnail ~ label { + color: #333; + font-size: 11px; + display: inline; + float: none; + margin-left: 2px; +} + +.admin-interface.change-form div[style="float:left"] { + font-size: 11px; + font-weight: bold; + color: #666; + margin-bottom: 5px; +} + +@media (max-width: 767px){ + .admin-interface.change-form div[style="float:left"] { + font-size: 12px; + width: 100%; + } +} + +.admin-interface .aligned .form-row a.thumbnail ~ input[type="file"] { + margin-top: 0px; +} + +@media (max-width:767px){ + .admin-interface .aligned .form-row a.thumbnail ~ input[type="file"] { + width: auto; + padding: 0px; + display: block; + margin-top: 3px; + } + + .admin-interface div[style="float:left"] { + margin-bottom: 0px; + } + + .admin-interface div[style="float:left"] + div.help { + margin-top: 0px !important; + } +} diff --git a/backend/staticfiles/admin_interface/css/streamfield.css b/backend/staticfiles/admin_interface/css/streamfield.css new file mode 100644 index 00000000..71ce2dbe --- /dev/null +++ b/backend/staticfiles/admin_interface/css/streamfield.css @@ -0,0 +1,200 @@ +/* +django-streamfield support +https://github.com/raagin/django-streamfield/ +*/ + +.admin-interface .form-row.field-stream { + margin: 0; + padding: 0; + border-bottom: none; +} + +.admin-interface .form-row.field-stream label[for=id_stream] { + display: none; +} + +.admin-interface .streamfield_app { + clear: both; + width: 100%; +} + +.admin-interface .streamfield_app .stream-help-text { + margin-bottom: 15px; + display: flex; + flex-direction: column; + clear: both; +} + +.admin-interface .streamfield_app .stream-help-text .stream-help-text__title { + align-self: flex-end; + user-select: none; + padding: 8px; + padding-right: 0; + color: var(--admin-interface-generic-link-color); +} + +.admin-interface .streamfield_app .stream-help-text .stream-help-text__title:hover { + color: var(--admin-interface-generic-link-hover-color); +} + +.admin-interface .streamfield_app .stream-help-text .stream-help-text__content { + background: var(--admin-interface-module-background-selected-color); + border-radius: var(--admin-interface-module-border-radius); + border: 1px solid rgba(0,0,0,0.1); + padding: 15px; +} + +.admin-interface .streamfield_app .stream-help-text .stream-help-text__content > ul { + margin: 0; + padding: 0; +} + +.admin-interface .streamfield_app .collapse-handler { + user-select: none; + padding: 8px; + padding-right: 0; + margin: 0 0 5px 0; + color: var(--admin-interface-generic-link-color); + text-decoration: none; +} + +.admin-interface .streamfield_app .collapse-handler:hover { + color: var(--admin-interface-generic-link-hover-color); + text-decoration: none; +} + +.admin-interface .streamfield_app .stream-model-block { + position: relative; + box-shadow: none; + border: 1px solid rgba(0,0,0,0.1); + border-radius: var(--admin-interface-module-border-radius); + overflow: hidden; +} + +.admin-interface .streamfield_app .stream-model-block, +.admin-interface .streamfield_app .streamfield-models.collapsed .stream-model-block { + margin-bottom: 10px; +} + +.admin-interface .streamfield_app .stream-model-block .streamblock__block__title { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin: 0; + padding: 10px 10px 10px 20px; +} + +.admin-interface .streamfield_app .stream-model-block .streamblock__block__title span { + font-size: 18px; +} + +.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle { + position: static; + right: 0; + top: 0; + color: var(--admin-interface-generic-link-color); +} + +.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle:hover { + color: var(--admin-interface-generic-link-hover-color); +} + +.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle { + display: flex; + justify-content: center; + align-items: center; +} + +.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle .block-move, +.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle .block-delete { + display: flex; + justify-content: center; + align-items: center; + width: 40px; + height: 40px; + font-weight: normal; + background: none; + flex-shrink: 0; + color: inherit; + font-size: 16px; +} + +.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle .block-move { + cursor: move; /* fallback if grab cursor is unsupported */ + cursor: grab; + cursor: -moz-grab; + cursor: -webkit-grab; +} + +.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle .block-move:before { + content: "↕"; + + display: block; +} + +.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle .block-delete:before { + content: "×"; + display: block; + font-size: 18px; +} + +.admin-interface .streamfield_app .block-fields > div { + margin-bottom: 15px; +} + +.admin-interface .streamfield_app .stream-model-block .stream-model-block__content { + background-color: #f8f8f8; + padding: 20px; +} + +.admin-interface .streamfield_app .stream-model-block .stream-model-block__content.no-subblocks.abstract-block { + display: none; +} + +.admin-interface .streamfield_app .stream-insert-new-block { + margin-bottom: 20px; +} + +.admin-interface .streamfield_app .stream-insert-new-block .add-new-block-button { + color: var(--admin-interface-generic-link-color); + text-decoration: none; +} + +.admin-interface .streamfield_app .stream-insert-new-block .add-new-block-button:hover { + color: var(--admin-interface-generic-link-hover-color); + text-decoration: none; +} + +.admin-interface .streamfield_app .stream-insert-new-block ul { + display: block; + width: 100%; + margin: 10px 0 0 0; + padding: 0; + user-select: none; +} + +.admin-interface .streamfield_app .stream-insert-new-block ul li { + display: inline-block; + font-size: 12px; + margin: 0; + padding: 0; +} + +.admin-interface .streamfield_app .stream-btn { + font-weight: normal; + text-decoration: none; + background-color: var(--admin-interface-generic-link-color); + padding: 6px 12px; + border-radius: 4px; +} + +.admin-interface .streamfield_app .stream-btn:hover { + text-decoration: none; + background-color: var(--admin-interface-generic-link-hover-color); +} + +.admin-interface .streamfield_app .stream-insert-new-block ul li .stream-btn { + margin-top: 5px; + margin-left: 5px; +} diff --git a/backend/staticfiles/admin_interface/css/tabbed-admin.css b/backend/staticfiles/admin_interface/css/tabbed-admin.css new file mode 100644 index 00000000..010296c5 --- /dev/null +++ b/backend/staticfiles/admin_interface/css/tabbed-admin.css @@ -0,0 +1,37 @@ +/* +django-tabbed-admin support +https://github.com/omji/django-tabbed-admin +*/ + +/* Hide tabs until ready */ +/* +.admin-interface #tabs ul { + display: none; +} + +.admin-interface #tabs ul.ui-tabs-nav { + display: block; +} +*/ + +.admin-interface .ui-tabs .ui-tabs-panel[id^=tabs] .module.aligned:last-child { + margin-bottom: 0; +} + +.admin-interface .ui-tabs .ui-tabs-panel[id^=tabs] .module.aligned:last-child .form-row:last-child { + border-bottom: none; +} + +@media (max-width: 350px){ + .admin-interface .ui-tabs .ui-tabs-panel[id^=tabs] .vTextField, + .admin-interface .inline-related .vTextField { + width: 17em; + } +} + +@media (max-width: 767px){ + /* fix horizontal overflow - responsive.css:563 */ + .admin-interface .ui-tabs .ui-tabs-panel[id^=tabs] .aligned .form-row > div:not([class]) { + width: 100% !important; + } +} diff --git a/backend/staticfiles/admin_interface/css/tabbed-changeform.css b/backend/staticfiles/admin_interface/css/tabbed-changeform.css new file mode 100644 index 00000000..a0f6718f --- /dev/null +++ b/backend/staticfiles/admin_interface/css/tabbed-changeform.css @@ -0,0 +1,67 @@ +.admin-interface .tabbed-changeform-tabs { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; + padding-bottom: 15px; +} + +@-moz-document url-prefix() { + .admin-interface .tabbed-changeform-tabs { + padding-bottom: 13px; + } +} + +.admin-interface .tabbed-changeform-tabs .tabbed-changeform-tablink { + appearance: none; + -webkit-appearance: none; + border: 1px solid transparent; + border-bottom: 1px solid var(--border-color); + border-radius: var(--admin-interface-module-border-radius); + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + flex-shrink: 0; + flex-grow: 0; + cursor: pointer; + padding: 10px 15px; + margin: 0; + background-color: var(--admin-interface-module-header-text-color); + color: var(--admin-interface-generic-link-color); + font-size: 13px; + font-weight: bold; + outline: none !important; +} + +.admin-interface .tabbed-changeform-tabs .tabbed-changeform-tablink + .tabbed-changeform-tablink { + margin-left: -1px; +} + +.admin-interface .tabbed-changeform-tabs .tabbed-changeform-tablink:hover { + color: var(--admin-interface-generic-link-hover-color); +} + +.admin-interface .tabbed-changeform-tabs .tabbed-changeform-tablink:focus { + color: var(--admin-interface-generic-link-hover-color); +} + +.admin-interface .tabbed-changeform-tabs .tabbed-changeform-tablink.active { + border: 1px solid var(--border-color); + border-bottom: 1px solid transparent; + color: var(--admin-interface-generic-link-active-color); +} + +.admin-interface .tabbed-changeform-tabs-remaining-space { + flex: 1; + border-bottom: 1px solid var(--border-color); +} + +.admin-interface .tabbed-changeform-tabcontent { + display: none; + padding: 1em 0; +} + +.admin-interface .tabbed-changeform-tabcontent.active { + display: block; +} diff --git a/backend/staticfiles/admin_interface/css/tinymce.css b/backend/staticfiles/admin_interface/css/tinymce.css new file mode 100644 index 00000000..76ed1b41 --- /dev/null +++ b/backend/staticfiles/admin_interface/css/tinymce.css @@ -0,0 +1,3 @@ +.admin-interface textarea.tinymce ~ p.help { + margin-top:5px !important; +} diff --git a/backend/staticfiles/admin_interface/favico/favico-0.3.10-patched.js b/backend/staticfiles/admin_interface/favico/favico-0.3.10-patched.js new file mode 100644 index 00000000..bc791352 --- /dev/null +++ b/backend/staticfiles/admin_interface/favico/favico-0.3.10-patched.js @@ -0,0 +1,913 @@ +/** + * @license MIT or GPL-2.0 + * @fileOverview Favico animations + * @author Miroslav Magda, http://blog.ejci.net + * @source: https://github.com/ejci/favico.js + * @version 0.3.10 + */ + +/** + * Create new favico instance + * @param {Object} Options + * @return {Object} Favico object + * @example + * var favico = new Favico({ + * bgColor : '#d00', + * textColor : '#fff', + * fontFamily : 'sans-serif', + * fontStyle : 'bold', + * type : 'circle', + * position : 'down', + * animation : 'slide', + * elementId: false, + * element: null, + * dataUrl: function(url){}, + * win: window + * }); + */ +(function () { + + var Favico = (function (opt) { + 'use strict'; + opt = (opt) ? opt : {}; + var _def = { + bgColor: '#d00', + textColor: '#fff', + fontFamily: 'sans-serif', //Arial,Verdana,Times New Roman,serif,sans-serif,... + fontStyle: 'bold', //normal,italic,oblique,bold,bolder,lighter,100,200,300,400,500,600,700,800,900 + type: 'circle', + position: 'down', // down, up, left, leftup (upleft) + animation: 'slide', + elementId: false, + element: null, + dataUrl: false, + win: window + }; + var _opt, _orig, _h, _w, _canvas, _context, _img, _ready, _lastBadge, _running, _readyCb, _stop, _browser, _animTimeout, _drawTimeout, _doc; + + _browser = {}; + _browser.ff = typeof InstallTrigger != 'undefined'; + _browser.chrome = !!window.chrome; + _browser.opera = !!window.opera || navigator.userAgent.indexOf('Opera') >= 0; + _browser.ie = /*@cc_on!@*/false; + _browser.safari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0; + _browser.supported = (_browser.chrome || _browser.ff || _browser.opera); + + var _queue = []; + _readyCb = function () { + }; + _ready = _stop = false; + /** + * Initialize favico + */ + var init = function () { + //merge initial options + _opt = merge(_def, opt); + _opt.bgColor = hexToRgb(_opt.bgColor); + _opt.textColor = hexToRgb(_opt.textColor); + _opt.position = _opt.position.toLowerCase(); + _opt.animation = (animation.types['' + _opt.animation]) ? _opt.animation : _def.animation; + + _doc = _opt.win.document; + + var isUp = _opt.position.indexOf('up') > -1; + var isLeft = _opt.position.indexOf('left') > -1; + + //transform the animations + if (isUp || isLeft) { + for (var a in animation.types) { + for (var i = 0; i < animation.types[a].length; i++) { + var step = animation.types[a][i]; + + if (isUp) { + if (step.y < 0.6) { + step.y = step.y - 0.4; + } else { + step.y = step.y - 2 * step.y + (1 - step.w); + } + } + + if (isLeft) { + if (step.x < 0.6) { + step.x = step.x - 0.4; + } else { + step.x = step.x - 2 * step.x + (1 - step.h); + } + } + + animation.types[a][i] = step; + } + } + } + _opt.type = (type['' + _opt.type]) ? _opt.type : _def.type; + + _orig = link. getIcons(); + //create temp canvas + _canvas = document.createElement('canvas'); + //create temp image + _img = document.createElement('img'); + var lastIcon = _orig[_orig.length - 1]; + if (lastIcon.hasAttribute('href')) { + _img.setAttribute('crossOrigin', 'anonymous'); + //get width/height + _img.onload = function () { + _h = (_img.height > 0) ? _img.height : 32; + _w = (_img.width > 0) ? _img.width : 32; + _canvas.height = _h; + _canvas.width = _w; + _context = _canvas.getContext('2d'); + icon.ready(); + }; + _img.setAttribute('src', lastIcon.getAttribute('href')); + } else { + _h = 32; + _w = 32; + _img.height = _h; + _img.width = _w; + _canvas.height = _h; + _canvas.width = _w; + _context = _canvas.getContext('2d'); + icon.ready(); + } + + }; + /** + * Icon namespace + */ + var icon = {}; + /** + * Icon is ready (reset icon) and start animation (if ther is any) + */ + icon.ready = function () { + _ready = true; + icon.reset(); + _readyCb(); + }; + /** + * Reset icon to default state + */ + icon.reset = function () { + //reset + if (!_ready) { + return; + } + _queue = []; + _lastBadge = false; + _running = false; + _context.clearRect(0, 0, _w, _h); + _context.drawImage(_img, 0, 0, _w, _h); + //_stop=true; + link.setIcon(_canvas); + //webcam('stop'); + //video('stop'); + window.clearTimeout(_animTimeout); + window.clearTimeout(_drawTimeout); + }; + /** + * Start animation + */ + icon.start = function () { + if (!_ready || _running) { + return; + } + var finished = function () { + _lastBadge = _queue[0]; + _running = false; + if (_queue.length > 0) { + _queue.shift(); + icon.start(); + } else { + + } + }; + if (_queue.length > 0) { + _running = true; + var run = function () { + // apply options for this animation + ['type', 'animation', 'bgColor', 'textColor', 'fontFamily', 'fontStyle'].forEach(function (a) { + if (a in _queue[0].options) { + _opt[a] = _queue[0].options[a]; + } + }); + animation.run(_queue[0].options, function () { + finished(); + }, false); + }; + if (_lastBadge) { + animation.run(_lastBadge.options, function () { + run(); + }, true); + } else { + run(); + } + } + }; + + /** + * Badge types + */ + var type = {}; + var options = function (opt) { + opt.n = ((typeof opt.n) === 'number') ? Math.abs(opt.n | 0) : opt.n; + opt.x = _w * opt.x; + opt.y = _h * opt.y; + opt.w = _w * opt.w; + opt.h = _h * opt.h; + opt.len = ("" + opt.n).length; + return opt; + }; + + /** + * Generate circle + * @param {Object} opt Badge options + */ + type.circle = function (opt) { + opt = options(opt); + var more = false; + if (opt.len === 2) { + opt.x = opt.x - opt.w * 0.4; + opt.w = opt.w * 1.4; + more = true; + } else if (opt.len >= 3) { + opt.x = opt.x - opt.w * 0.65; + opt.w = opt.w * 1.65; + more = true; + } + + // begin patch + // draw a smaller badge without text + opt.w = 14; + opt.h = 14; + opt.x = (_w - opt.w); + opt.y = (_h - opt.h); + // end patch + + _context.clearRect(0, 0, _w, _h); + _context.drawImage(_img, 0, 0, _w, _h); + _context.beginPath(); + _context.font = _opt.fontStyle + " " + Math.floor(opt.h * (opt.n > 99 ? 0.85 : 1)) + "px " + _opt.fontFamily; + _context.textAlign = 'center'; + if (more) { + _context.moveTo(opt.x + opt.w / 2, opt.y); + _context.lineTo(opt.x + opt.w - opt.h / 2, opt.y); + _context.quadraticCurveTo(opt.x + opt.w, opt.y, opt.x + opt.w, opt.y + opt.h / 2); + _context.lineTo(opt.x + opt.w, opt.y + opt.h - opt.h / 2); + _context.quadraticCurveTo(opt.x + opt.w, opt.y + opt.h, opt.x + opt.w - opt.h / 2, opt.y + opt.h); + _context.lineTo(opt.x + opt.h / 2, opt.y + opt.h); + _context.quadraticCurveTo(opt.x, opt.y + opt.h, opt.x, opt.y + opt.h - opt.h / 2); + _context.lineTo(opt.x, opt.y + opt.h / 2); + _context.quadraticCurveTo(opt.x, opt.y, opt.x + opt.h / 2, opt.y); + } else { + _context.arc(opt.x + opt.w / 2, opt.y + opt.h / 2, opt.h / 2, 0, 2 * Math.PI); + } + _context.fillStyle = 'rgba(' + _opt.bgColor.r + ',' + _opt.bgColor.g + ',' + _opt.bgColor.b + ',' + opt.o + ')'; + _context.fill(); + _context.closePath(); + _context.beginPath(); + _context.stroke(); + _context.fillStyle = 'rgba(' + _opt.textColor.r + ',' + _opt.textColor.g + ',' + _opt.textColor.b + ',' + opt.o + ')'; + //_context.fillText((more) ? '9+' : opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15)); + if ((typeof opt.n) === 'number' && opt.n > 999) { + _context.fillText(((opt.n > 9999) ? 9 : Math.floor(opt.n / 1000)) + 'k+', Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.2)); + } else { + _context.fillText(opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15)); + } + _context.closePath(); + }; + /** + * Generate rectangle + * @param {Object} opt Badge options + */ + type.rectangle = function (opt) { + opt = options(opt); + var more = false; + if (opt.len === 2) { + opt.x = opt.x - opt.w * 0.4; + opt.w = opt.w * 1.4; + more = true; + } else if (opt.len >= 3) { + opt.x = opt.x - opt.w * 0.65; + opt.w = opt.w * 1.65; + more = true; + } + _context.clearRect(0, 0, _w, _h); + _context.drawImage(_img, 0, 0, _w, _h); + _context.beginPath(); + _context.font = _opt.fontStyle + " " + Math.floor(opt.h * (opt.n > 99 ? 0.9 : 1)) + "px " + _opt.fontFamily; + _context.textAlign = 'center'; + _context.fillStyle = 'rgba(' + _opt.bgColor.r + ',' + _opt.bgColor.g + ',' + _opt.bgColor.b + ',' + opt.o + ')'; + _context.fillRect(opt.x, opt.y, opt.w, opt.h); + _context.fillStyle = 'rgba(' + _opt.textColor.r + ',' + _opt.textColor.g + ',' + _opt.textColor.b + ',' + opt.o + ')'; + //_context.fillText((more) ? '9+' : opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15)); + if ((typeof opt.n) === 'number' && opt.n > 999) { + _context.fillText(((opt.n > 9999) ? 9 : Math.floor(opt.n / 1000)) + 'k+', Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.2)); + } else { + _context.fillText(opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15)); + } + _context.closePath(); + }; + + /** + * Set badge + */ + var badge = function (number, opts) { + opts = ((typeof opts) === 'string' ? { + animation: opts + } : opts) || {}; + _readyCb = function () { + try { + if (typeof (number) === 'number' ? (number > 0) : (number !== '')) { + var q = { + type: 'badge', + options: { + n: number + } + }; + if ('animation' in opts && animation.types['' + opts.animation]) { + q.options.animation = '' + opts.animation; + } + if ('type' in opts && type['' + opts.type]) { + q.options.type = '' + opts.type; + } + ['bgColor', 'textColor'].forEach(function (o) { + if (o in opts) { + q.options[o] = hexToRgb(opts[o]); + } + }); + ['fontStyle', 'fontFamily'].forEach(function (o) { + if (o in opts) { + q.options[o] = opts[o]; + } + }); + _queue.push(q); + if (_queue.length > 100) { + throw new Error('Too many badges requests in queue.'); + } + icon.start(); + } else { + icon.reset(); + } + } catch (e) { + throw new Error('Error setting badge. Message: ' + e.message); + } + }; + if (_ready) { + _readyCb(); + } + }; + + /** + * Set image as icon + */ + var image = function (imageElement) { + _readyCb = function () { + try { + var w = imageElement.width; + var h = imageElement.height; + var newImg = document.createElement('img'); + var ratio = (w / _w < h / _h) ? (w / _w) : (h / _h); + newImg.setAttribute('crossOrigin', 'anonymous'); + newImg.onload=function(){ + _context.clearRect(0, 0, _w, _h); + _context.drawImage(newImg, 0, 0, _w, _h); + link.setIcon(_canvas); + }; + newImg.setAttribute('src', imageElement.getAttribute('src')); + newImg.height = (h / ratio); + newImg.width = (w / ratio); + } catch (e) { + throw new Error('Error setting image. Message: ' + e.message); + } + }; + if (_ready) { + _readyCb(); + } + }; + /** + * Set the icon from a source url. Won't work with badges. + */ + var rawImageSrc = function (url) { + _readyCb = function() { + link.setIconSrc(url); + }; + if (_ready) { + _readyCb(); + } + }; + /** + * Set video as icon + */ + var video = function (videoElement) { + _readyCb = function () { + try { + if (videoElement === 'stop') { + _stop = true; + icon.reset(); + _stop = false; + return; + } + //var w = videoElement.width; + //var h = videoElement.height; + //var ratio = (w / _w < h / _h) ? (w / _w) : (h / _h); + videoElement.addEventListener('play', function () { + drawVideo(this); + }, false); + + } catch (e) { + throw new Error('Error setting video. Message: ' + e.message); + } + }; + if (_ready) { + _readyCb(); + } + }; + /** + * Set video as icon + */ + var webcam = function (action) { + //UR + if (!window.URL || !window.URL.createObjectURL) { + window.URL = window.URL || {}; + window.URL.createObjectURL = function (obj) { + return obj; + }; + } + if (_browser.supported) { + var newVideo = false; + navigator.getUserMedia = navigator.getUserMedia || navigator.oGetUserMedia || navigator.msGetUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia; + _readyCb = function () { + try { + if (action === 'stop') { + _stop = true; + icon.reset(); + _stop = false; + return; + } + newVideo = document.createElement('video'); + newVideo.width = _w; + newVideo.height = _h; + navigator.getUserMedia({ + video: true, + audio: false + }, function (stream) { + newVideo.src = URL.createObjectURL(stream); + newVideo.play(); + drawVideo(newVideo); + }, function () { + }); + } catch (e) { + throw new Error('Error setting webcam. Message: ' + e.message); + } + }; + if (_ready) { + _readyCb(); + } + } + + }; + + var setOpt = function (key, value) { + var opts = key; + if (!(value == null && Object.prototype.toString.call(key) == '[object Object]')) { + opts = {}; + opts[key] = value; + } + + var keys = Object.keys(opts); + for (var i = 0; i < keys.length; i++) { + if (keys[i] == 'bgColor' || keys[i] == 'textColor') { + _opt[keys[i]] = hexToRgb(opts[keys[i]]); + } else { + _opt[keys[i]] = opts[keys[i]]; + } + } + + _queue.push(_lastBadge); + icon.start(); + }; + + /** + * Draw video to context and repeat :) + */ + function drawVideo(video) { + if (video.paused || video.ended || _stop) { + return false; + } + //nasty hack for FF webcam (Thanks to Julian Ćwirko, kontakt@redsunmedia.pl) + try { + _context.clearRect(0, 0, _w, _h); + _context.drawImage(video, 0, 0, _w, _h); + } catch (e) { + + } + _drawTimeout = setTimeout(function () { + drawVideo(video); + }, animation.duration); + link.setIcon(_canvas); + } + + var link = {}; + /** + * Get icons from HEAD tag or create a new element + */ + link.getIcons = function () { + var elms = []; + //get link element + var getLinks = function () { + var icons = []; + var links = _doc.getElementsByTagName('head')[0].getElementsByTagName('link'); + for (var i = 0; i < links.length; i++) { + if ((/(^|\s)icon(\s|$)/i).test(links[i].getAttribute('rel'))) { + icons.push(links[i]); + } + } + return icons; + }; + if (_opt.element) { + elms = [_opt.element]; + } else if (_opt.elementId) { + //if img element identified by elementId + elms = [_doc.getElementById(_opt.elementId)]; + elms[0].setAttribute('href', elms[0].getAttribute('src')); + } else { + //if link element + elms = getLinks(); + if (elms.length === 0) { + elms = [_doc.createElement('link')]; + elms[0].setAttribute('rel', 'icon'); + _doc.getElementsByTagName('head')[0].appendChild(elms[0]); + } + } + elms.forEach(function(item) { + item.setAttribute('type', 'image/png'); + }); + return elms; + }; + link.setIcon = function (canvas) { + var url = canvas.toDataURL('image/png'); + link.setIconSrc(url); + }; + link.setIconSrc = function (url) { + if (_opt.dataUrl) { + //if using custom exporter + _opt.dataUrl(url); + } + if (_opt.element) { + _opt.element.setAttribute('href', url); + _opt.element.setAttribute('src', url); + } else if (_opt.elementId) { + //if is attached to element (image) + var elm = _doc.getElementById(_opt.elementId); + elm.setAttribute('href', url); + elm.setAttribute('src', url); + } else { + //if is attached to fav icon + if (_browser.ff || _browser.opera) { + //for FF we need to "recreate" element, atach to dom and remove old + //var originalType = _orig.getAttribute('rel'); + var old = _orig[_orig.length - 1]; + var newIcon = _doc.createElement('link'); + _orig = [newIcon]; + //_orig.setAttribute('rel', originalType); + if (_browser.opera) { + newIcon.setAttribute('rel', 'icon'); + } + newIcon.setAttribute('rel', 'icon'); + newIcon.setAttribute('type', 'image/png'); + _doc.getElementsByTagName('head')[0].appendChild(newIcon); + newIcon.setAttribute('href', url); + if (old.parentNode) { + old.parentNode.removeChild(old); + } + } else { + _orig.forEach(function(icon) { + icon.setAttribute('href', url); + }); + } + } + }; + + //http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb#answer-5624139 + //HEX to RGB convertor + function hexToRgb(hex) { + var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, function (m, r, g, b) { + return r + r + g + g + b + b; + }); + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : false; + } + + /** + * Merge options + */ + function merge(def, opt) { + var mergedOpt = {}; + var attrname; + for (attrname in def) { + mergedOpt[attrname] = def[attrname]; + } + for (attrname in opt) { + mergedOpt[attrname] = opt[attrname]; + } + return mergedOpt; + } + + /** + * Cross-browser page visibility shim + * http://stackoverflow.com/questions/12536562/detect-whether-a-window-is-visible + */ + function isPageHidden() { + return _doc.hidden || _doc.msHidden || _doc.webkitHidden || _doc.mozHidden; + } + + /** + * @namespace animation + */ + var animation = {}; + /** + * Animation "frame" duration + */ + animation.duration = 40; + /** + * Animation types (none,fade,pop,slide) + */ + animation.types = {}; + animation.types.fade = [{ + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 0.0 + }, { + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 0.1 + }, { + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 0.2 + }, { + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 0.3 + }, { + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 0.4 + }, { + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 0.5 + }, { + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 0.6 + }, { + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 0.7 + }, { + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 0.8 + }, { + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 0.9 + }, { + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 1.0 + }]; + animation.types.none = [{ + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 1 + }]; + animation.types.pop = [{ + x: 1, + y: 1, + w: 0, + h: 0, + o: 1 + }, { + x: 0.9, + y: 0.9, + w: 0.1, + h: 0.1, + o: 1 + }, { + x: 0.8, + y: 0.8, + w: 0.2, + h: 0.2, + o: 1 + }, { + x: 0.7, + y: 0.7, + w: 0.3, + h: 0.3, + o: 1 + }, { + x: 0.6, + y: 0.6, + w: 0.4, + h: 0.4, + o: 1 + }, { + x: 0.5, + y: 0.5, + w: 0.5, + h: 0.5, + o: 1 + }, { + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 1 + }]; + animation.types.popFade = [{ + x: 0.75, + y: 0.75, + w: 0, + h: 0, + o: 0 + }, { + x: 0.65, + y: 0.65, + w: 0.1, + h: 0.1, + o: 0.2 + }, { + x: 0.6, + y: 0.6, + w: 0.2, + h: 0.2, + o: 0.4 + }, { + x: 0.55, + y: 0.55, + w: 0.3, + h: 0.3, + o: 0.6 + }, { + x: 0.50, + y: 0.50, + w: 0.4, + h: 0.4, + o: 0.8 + }, { + x: 0.45, + y: 0.45, + w: 0.5, + h: 0.5, + o: 0.9 + }, { + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 1 + }]; + animation.types.slide = [{ + x: 0.4, + y: 1, + w: 0.6, + h: 0.6, + o: 1 + }, { + x: 0.4, + y: 0.9, + w: 0.6, + h: 0.6, + o: 1 + }, { + x: 0.4, + y: 0.9, + w: 0.6, + h: 0.6, + o: 1 + }, { + x: 0.4, + y: 0.8, + w: 0.6, + h: 0.6, + o: 1 + }, { + x: 0.4, + y: 0.7, + w: 0.6, + h: 0.6, + o: 1 + }, { + x: 0.4, + y: 0.6, + w: 0.6, + h: 0.6, + o: 1 + }, { + x: 0.4, + y: 0.5, + w: 0.6, + h: 0.6, + o: 1 + }, { + x: 0.4, + y: 0.4, + w: 0.6, + h: 0.6, + o: 1 + }]; + /** + * Run animation + * @param {Object} opt Animation options + * @param {Object} cb Callabak after all steps are done + * @param {Object} revert Reverse order? true|false + * @param {Object} step Optional step number (frame bumber) + */ + animation.run = function (opt, cb, revert, step) { + var animationType = animation.types[isPageHidden() ? 'none' : _opt.animation]; + if (revert === true) { + step = (typeof step !== 'undefined') ? step : animationType.length - 1; + } else { + step = (typeof step !== 'undefined') ? step : 0; + } + cb = (cb) ? cb : function () { + }; + if ((step < animationType.length) && (step >= 0)) { + type[_opt.type](merge(opt, animationType[step])); + _animTimeout = setTimeout(function () { + if (revert) { + step = step - 1; + } else { + step = step + 1; + } + animation.run(opt, cb, revert, step); + }, animation.duration); + + link.setIcon(_canvas); + } else { + cb(); + return; + } + }; + //auto init + init(); + return { + badge: badge, + video: video, + image: image, + rawImageSrc: rawImageSrc, + webcam: webcam, + setOpt: setOpt, + reset: icon.reset, + browser: { + supported: _browser.supported + } + }; + }); + + // AMD / RequireJS + if (typeof define !== 'undefined' && define.amd) { + define([], function () { + return Favico; + }); + } + // CommonJS + else if (typeof module !== 'undefined' && module.exports) { + module.exports = Favico; + } + // included directly via