panda-core 0.12.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +53 -0
- data/app/assets/builds/panda_core_admin.css +2 -0
- data/app/assets/tailwind/application.css +236 -36
- data/app/builders/panda/core/form_builder.rb +120 -18
- data/app/components/panda/core/UI/badge.html.erb +10 -0
- data/app/components/panda/core/UI/badge.rb +14 -30
- data/app/components/panda/core/UI/button.html.erb +5 -0
- data/app/components/panda/core/UI/button.rb +11 -15
- data/app/components/panda/core/UI/card.html.erb +3 -0
- data/app/components/panda/core/UI/card.rb +7 -6
- data/app/components/panda/core/admin/attribute_diff_component.html.erb +21 -0
- data/app/components/panda/core/admin/attribute_diff_component.rb +29 -0
- data/app/components/panda/core/admin/breadcrumb_component.html.erb +36 -0
- data/app/components/panda/core/admin/breadcrumb_component.rb +12 -64
- data/app/components/panda/core/admin/breadcrumbs_component.html.erb +16 -0
- data/app/components/panda/core/admin/breadcrumbs_component.rb +26 -0
- data/app/components/panda/core/admin/button_component.html.erb +9 -0
- data/app/components/panda/core/admin/button_component.rb +36 -31
- data/app/components/panda/core/admin/callout_component.html.erb +17 -0
- data/app/components/panda/core/admin/callout_component.rb +57 -0
- data/app/components/panda/core/admin/code_block_component.html.erb +9 -0
- data/app/components/panda/core/admin/code_block_component.rb +23 -0
- data/app/components/panda/core/admin/container_component.html.erb +27 -0
- data/app/components/panda/core/admin/container_component.rb +11 -77
- data/app/components/panda/core/admin/contributors_summary_component.html.erb +15 -0
- data/app/components/panda/core/admin/contributors_summary_component.rb +23 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.html.erb +70 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.rb +32 -0
- data/app/components/panda/core/admin/delete_button_component.html.erb +4 -0
- data/app/components/panda/core/admin/delete_button_component.rb +41 -0
- data/app/components/panda/core/admin/dropdown_button_component.html.erb +28 -0
- data/app/components/panda/core/admin/dropdown_button_component.rb +100 -0
- data/app/components/panda/core/admin/dropdown_component.html.erb +32 -0
- data/app/components/panda/core/admin/dropdown_component.rb +65 -0
- data/app/components/panda/core/admin/empty_state_component.html.erb +16 -0
- data/app/components/panda/core/admin/empty_state_component.rb +25 -0
- data/app/components/panda/core/admin/file_gallery_component.html.erb +58 -0
- data/app/components/panda/core/admin/file_gallery_component.rb +26 -98
- data/app/components/panda/core/admin/filter_pills_component.html.erb +6 -0
- data/app/components/panda/core/admin/filter_pills_component.rb +29 -0
- data/app/components/panda/core/admin/flash_message_component.html.erb +11 -0
- data/app/components/panda/core/admin/flash_message_component.rb +33 -60
- data/app/components/panda/core/admin/form_error_component.html.erb +7 -0
- data/app/components/panda/core/admin/form_error_component.rb +8 -16
- data/app/components/panda/core/admin/form_footer_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_footer_component.rb +65 -0
- data/app/components/panda/core/admin/form_input_component.html.erb +1 -0
- data/app/components/panda/core/admin/form_input_component.rb +21 -13
- data/app/components/panda/core/admin/form_section_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_section_component.rb +64 -0
- data/app/components/panda/core/admin/form_select_component.html.erb +5 -0
- data/app/components/panda/core/admin/form_select_component.rb +25 -26
- data/app/components/panda/core/admin/heading_component.html.erb +26 -0
- data/app/components/panda/core/admin/heading_component.rb +23 -60
- data/app/components/panda/core/admin/login_form_component.html.erb +38 -0
- data/app/components/panda/core/admin/login_form_component.rb +28 -0
- data/app/components/panda/core/admin/main_layout_component.html.erb +88 -0
- data/app/components/panda/core/admin/main_layout_component.rb +19 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.html.erb +41 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.rb +27 -0
- data/app/components/panda/core/admin/navigation/item_component.html.erb +34 -0
- data/app/components/panda/core/admin/navigation/item_component.rb +69 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.html.erb +9 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.rb +44 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.html.erb +30 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.rb +36 -0
- data/app/components/panda/core/admin/page_header_component.html.erb +28 -0
- data/app/components/panda/core/admin/page_header_component.rb +19 -35
- data/app/components/panda/core/admin/pagination_component.html.erb +33 -0
- data/app/components/panda/core/admin/pagination_component.rb +56 -0
- data/app/components/panda/core/admin/panel_component.html.erb +19 -0
- data/app/components/panda/core/admin/panel_component.rb +4 -29
- data/app/components/panda/core/admin/prose_panel_component.html.erb +6 -0
- data/app/components/panda/core/admin/prose_panel_component.rb +43 -0
- data/app/components/panda/core/admin/search_bar_component.html.erb +19 -0
- data/app/components/panda/core/admin/search_bar_component.rb +31 -0
- data/app/components/panda/core/admin/search_filter_bar_component.html.erb +22 -0
- data/app/components/panda/core/admin/search_filter_bar_component.rb +27 -0
- data/app/components/panda/core/admin/secret_field_component.html.erb +24 -0
- data/app/components/panda/core/admin/secret_field_component.rb +31 -0
- data/app/components/panda/core/admin/sidebar_component.html.erb +160 -0
- data/app/components/panda/core/admin/sidebar_component.rb +55 -0
- data/app/components/panda/core/admin/slideover_component.html.erb +67 -0
- data/app/components/panda/core/admin/slideover_component.rb +31 -70
- data/app/components/panda/core/admin/statistics_component.html.erb +4 -0
- data/app/components/panda/core/admin/statistics_component.rb +6 -8
- data/app/components/panda/core/admin/tab_bar_component.html.erb +48 -0
- data/app/components/panda/core/admin/tab_bar_component.rb +32 -67
- data/app/components/panda/core/admin/table_component.html.erb +82 -0
- data/app/components/panda/core/admin/table_component.rb +54 -87
- data/app/components/panda/core/admin/tag_badge_component.html.erb +7 -0
- data/app/components/panda/core/admin/tag_badge_component.rb +21 -0
- data/app/components/panda/core/admin/tag_component.html.erb +1 -0
- data/app/components/panda/core/admin/tag_component.rb +51 -20
- data/app/components/panda/core/admin/tag_input_component.html.erb +37 -0
- data/app/components/panda/core/admin/tag_input_component.rb +22 -0
- data/app/components/panda/core/admin/tags_panel_component.html.erb +8 -0
- data/app/components/panda/core/admin/tags_panel_component.rb +33 -0
- data/app/components/panda/core/admin/user_activity_component.html.erb +15 -0
- data/app/components/panda/core/admin/user_activity_component.rb +9 -22
- data/app/components/panda/core/admin/user_display_component.html.erb +8 -0
- data/app/components/panda/core/admin/user_display_component.rb +19 -22
- data/app/components/panda/core/base.rb +35 -42
- data/app/components/panda/core/shared/footer_component.html.erb +2 -0
- data/app/components/panda/core/shared/footer_component.rb +11 -0
- data/app/components/panda/core/shared/header_component.html.erb +32 -0
- data/app/components/panda/core/shared/header_component.rb +19 -0
- data/app/constraints/panda/core/admin_constraint.rb +2 -2
- data/app/controllers/concerns/panda/core/admin_authorization.rb +58 -0
- data/app/controllers/concerns/panda/core/authorizable.rb +107 -0
- data/app/controllers/panda/core/admin/base_controller.rb +62 -3
- data/app/controllers/panda/core/admin/editor/search_controller.rb +17 -0
- data/app/controllers/panda/core/admin/feature_flags_controller.rb +35 -0
- data/app/controllers/panda/core/admin/file_categories_controller.rb +84 -0
- data/app/controllers/panda/core/admin/files_controller.rb +230 -0
- data/app/controllers/panda/core/admin/import_sessions_controller.rb +120 -0
- data/app/controllers/panda/core/admin/my_profile/logins_controller.rb +24 -0
- data/app/controllers/panda/core/admin/my_profile_controller.rb +31 -0
- data/app/controllers/panda/core/admin/search_controller.rb +15 -0
- data/app/controllers/panda/core/admin/sessions_controller.rb +84 -10
- data/app/controllers/panda/core/admin/tags_controller.rb +77 -0
- data/app/controllers/panda/core/admin/test_sessions_controller.rb +5 -6
- data/app/controllers/panda/core/admin/users_controller.rb +184 -0
- data/app/controllers/panda/core/application_controller.rb +13 -1
- data/app/helpers/panda/core/active_storage_helper.rb +24 -0
- data/app/helpers/panda/core/feature_flag_helper.rb +11 -0
- data/app/helpers/panda/core/form_helper.rb +15 -0
- data/app/javascript/panda/core/controllers/avatar_upload_controller.js +73 -0
- data/app/javascript/panda/core/controllers/clipboard_controller.js +119 -0
- data/app/javascript/panda/core/controllers/collapsible_item_controller.js +45 -0
- data/app/javascript/panda/core/controllers/custom_select_controller.js +256 -0
- data/app/javascript/panda/core/controllers/datepicker_controller.js +121 -0
- data/app/javascript/panda/core/controllers/daterange_controller.js +193 -0
- data/app/javascript/panda/core/controllers/file_gallery_controller.js +134 -0
- data/app/javascript/panda/core/controllers/file_upload_controller.js +165 -0
- data/app/javascript/panda/core/controllers/global_search_controller.js +184 -0
- data/app/javascript/panda/core/controllers/index.js +36 -0
- data/app/javascript/panda/core/controllers/mobile_sidebar_controller.js +58 -0
- data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +49 -24
- data/app/javascript/panda/core/controllers/row_link_controller.js +27 -0
- data/app/javascript/panda/core/controllers/tag_input_controller.js +183 -0
- data/app/javascript/panda/core/tailwindcss-stimulus-components.js +2 -3
- data/app/javascript/panda/core/vendor/@fortawesome--fontawesome-free@7.2.0.js +8129 -0
- data/app/javascript/panda/core/vendor/@hotwired--turbo.js +7197 -100
- data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +511 -2
- data/app/javascript/panda/core/vendor/@tailwindplus--elements@1.0.22.js +65 -0
- data/app/javascript/panda/core/vendor/cropperjs@2.1.0.js +3 -0
- data/app/javascript/panda/core/vendor/vanilla-calendar-pro.js +2 -0
- data/app/models/concerns/panda/core/has_metadata.rb +164 -0
- data/app/models/concerns/panda/core/importable.rb +79 -0
- data/app/models/concerns/panda/core/searchable.rb +66 -0
- data/app/models/concerns/panda/core/taggable.rb +37 -0
- data/app/models/panda/core/feature_flag.rb +56 -0
- data/app/models/panda/core/file_categorization.rb +14 -0
- data/app/models/panda/core/file_category.rb +45 -0
- data/app/models/panda/core/import_session.rb +79 -0
- data/app/models/panda/core/presence.rb +24 -0
- data/app/models/panda/core/tag.rb +33 -0
- data/app/models/panda/core/tagging.rb +14 -0
- data/app/models/panda/core/user.rb +94 -16
- data/app/models/panda/core/user_activity.rb +42 -0
- data/app/models/panda/core/user_session.rb +35 -0
- data/app/services/panda/core/attach_avatar_service.rb +15 -3
- data/app/services/panda/core/csv_import_service.rb +65 -0
- data/app/services/panda/core/file_categorizer.rb +52 -0
- data/app/services/panda/core/file_parser.rb +76 -0
- data/app/services/panda/core/invite_user_service.rb +44 -0
- data/app/services/panda/core/presence_service.rb +49 -0
- data/app/views/layouts/panda/core/admin.html.erb +6 -76
- data/app/views/layouts/panda/core/admin_simple.html.erb +2 -2
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +1 -73
- data/app/views/panda/core/admin/dashboard/show.html.erb +12 -15
- data/app/views/panda/core/admin/feature_flags/index.html.erb +56 -0
- data/app/views/panda/core/admin/file_categories/_form.html.erb +20 -0
- data/app/views/panda/core/admin/file_categories/edit.html.erb +5 -0
- data/app/views/panda/core/admin/file_categories/index.html.erb +55 -0
- data/app/views/panda/core/admin/file_categories/new.html.erb +5 -0
- data/app/views/panda/core/admin/files/_file_details.html.erb +156 -0
- data/app/views/panda/core/admin/files/index.html.erb +61 -0
- data/app/views/panda/core/admin/import_sessions/column_map.html.erb +43 -0
- data/app/views/panda/core/admin/import_sessions/index.html.erb +48 -0
- data/app/views/panda/core/admin/import_sessions/new.html.erb +37 -0
- data/app/views/panda/core/admin/import_sessions/preview.html.erb +51 -0
- data/app/views/panda/core/admin/import_sessions/show.html.erb +87 -0
- data/app/views/panda/core/admin/my_profile/edit.html.erb +51 -39
- data/app/views/panda/core/admin/my_profile/logins/show.html.erb +72 -0
- data/app/views/panda/core/admin/sessions/new.html.erb +1 -37
- data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +1 -18
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +1 -99
- data/app/views/panda/core/admin/shared/_slideover.html.erb +3 -3
- data/app/views/panda/core/admin/tags/_form.html.erb +21 -0
- data/app/views/panda/core/admin/tags/edit.html.erb +4 -0
- data/app/views/panda/core/admin/tags/index.html.erb +41 -0
- data/app/views/panda/core/admin/tags/new.html.erb +4 -0
- data/app/views/panda/core/admin/users/activity.html.erb +90 -0
- data/app/views/panda/core/admin/users/edit.html.erb +48 -0
- data/app/views/panda/core/admin/users/index.html.erb +161 -0
- data/app/views/panda/core/admin/users/sessions.html.erb +103 -0
- data/app/views/panda/core/admin/users/show.html.erb +224 -0
- data/app/views/panda/core/shared/_footer.html.erb +1 -2
- data/app/views/panda/core/shared/_header.html.erb +4 -22
- data/config/brakeman.ignore +68 -31
- data/config/importmap.rb +13 -11
- data/config/routes.rb +49 -2
- data/db/migrate/20260202171614_create_panda_core_file_categories.rb +30 -0
- data/db/migrate/20260203000001_add_user_management_fields_to_panda_core_users.rb +21 -0
- data/db/migrate/20260203000002_create_panda_core_user_activities.rb +24 -0
- data/db/migrate/20260203000003_create_panda_core_user_sessions.rb +25 -0
- data/db/migrate/20260207100000_create_panda_core_presences.rb +21 -0
- data/db/migrate/20260213000001_fix_active_storage_attachments_record_id_type.rb +44 -0
- data/db/migrate/20260217000001_create_panda_core_feature_flags.rb +14 -0
- data/db/migrate/20260303000001_add_metadata_to_panda_core_users.rb +11 -0
- data/db/migrate/20260310000001_create_panda_core_tags.rb +27 -0
- data/db/migrate/20260310000002_create_panda_core_import_sessions.rb +29 -0
- data/lib/generators/panda/core/install_generator.rb +32 -0
- data/lib/generators/panda/core/templates/panda.rb +63 -0
- data/lib/panda/core/asset_loader.rb +5 -2
- data/lib/panda/core/authentication.rb +3 -3
- data/lib/panda/core/configuration.rb +195 -1
- data/lib/panda/core/engine/admin_controller_config.rb +3 -6
- data/lib/panda/core/engine/autoload_config.rb +21 -9
- data/lib/panda/core/engine/omniauth_config.rb +108 -38
- data/lib/panda/core/engine/pending_migrations_check.rb +46 -0
- data/lib/panda/core/engine/sqlite_schema_compatibility.rb +36 -0
- data/lib/panda/core/engine/{phlex_config.rb → view_component_config.rb} +5 -7
- data/lib/panda/core/engine.rb +90 -25
- data/lib/panda/core/middleware.rb +35 -0
- data/lib/panda/core/navigation_registry.rb +455 -0
- data/lib/panda/core/oauth_providers.rb +27 -0
- data/lib/panda/core/permission_registry.rb +89 -0
- data/lib/panda/core/search_registry.rb +69 -0
- data/lib/panda/core/seeds/file_categories.rb +30 -0
- data/lib/panda/core/shared/inflections_config.rb +1 -5
- data/lib/panda/core/testing/rails_helper.rb +87 -0
- data/lib/panda/core/testing/support/authentication_test_helpers.rb +22 -18
- data/lib/panda/core/testing/support/system/cuprite_helpers.rb +4 -4
- data/lib/panda/core/testing/view_component_test_controller.rb +19 -0
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core/widget_registry.rb +50 -0
- data/lib/panda/core.rb +17 -8
- data/lib/tasks/panda/core/file_categories.rake +41 -0
- data/public/panda-core-assets/panda-core-0.12.5.css +2 -0
- data/public/panda-core-assets/panda-core-0.13.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.4.css +2 -0
- data/public/panda-core-assets/panda-core.css +2 -2
- data/public/panda-core-assets/vanilla-calendar.css +47 -0
- metadata +203 -26
- data/lib/panda/core/engine/inflections_config.rb +0 -23
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["input", "preview", "dropzone", "fileInfo", "removeButton"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
// Setup drag and drop handlers
|
|
8
|
+
if (this.hasDropzoneTarget) {
|
|
9
|
+
this.setupDragAndDrop()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
setupDragAndDrop() {
|
|
14
|
+
const dropzone = this.dropzoneTarget
|
|
15
|
+
|
|
16
|
+
// Prevent default drag behaviors
|
|
17
|
+
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
18
|
+
dropzone.addEventListener(eventName, this.preventDefaults.bind(this), false)
|
|
19
|
+
document.body.addEventListener(eventName, this.preventDefaults.bind(this), false)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// Highlight drop zone when item is dragged over it
|
|
23
|
+
;['dragenter', 'dragover'].forEach(eventName => {
|
|
24
|
+
dropzone.addEventListener(eventName, this.highlight.bind(this), false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
;['dragleave', 'drop'].forEach(eventName => {
|
|
28
|
+
dropzone.addEventListener(eventName, this.unhighlight.bind(this), false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Handle dropped files
|
|
32
|
+
dropzone.addEventListener('drop', this.handleDrop.bind(this), false)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
preventDefaults(e) {
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
e.stopPropagation()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
highlight(e) {
|
|
41
|
+
this.dropzoneTarget.classList.add('border-primary-600', 'bg-primary-50')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
unhighlight(e) {
|
|
45
|
+
this.dropzoneTarget.classList.remove('border-primary-600', 'bg-primary-50')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
handleDrop(e) {
|
|
49
|
+
const dt = e.dataTransfer
|
|
50
|
+
const files = dt.files
|
|
51
|
+
|
|
52
|
+
if (files.length > 0) {
|
|
53
|
+
// Update the file input with the dropped file
|
|
54
|
+
this.inputTarget.files = files
|
|
55
|
+
this.handleFileSelect()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Triggered when user selects file via input or drag-drop
|
|
60
|
+
handleFileSelect() {
|
|
61
|
+
const file = this.inputTarget.files[0]
|
|
62
|
+
|
|
63
|
+
if (!file) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Show file preview if it's an image
|
|
68
|
+
if (file.type.startsWith('image/')) {
|
|
69
|
+
this.showImagePreview(file)
|
|
70
|
+
} else {
|
|
71
|
+
this.showFileInfo(file)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
showImagePreview(file) {
|
|
76
|
+
const reader = new FileReader()
|
|
77
|
+
|
|
78
|
+
reader.onload = (e) => {
|
|
79
|
+
if (this.hasPreviewTarget) {
|
|
80
|
+
// Create or update preview image
|
|
81
|
+
const existingImage = this.previewTarget.querySelector('img')
|
|
82
|
+
if (existingImage) {
|
|
83
|
+
existingImage.src = e.target.result
|
|
84
|
+
} else {
|
|
85
|
+
const img = document.createElement('img')
|
|
86
|
+
img.src = e.target.result
|
|
87
|
+
img.className = 'max-w-xs rounded border border-gray-300 dark:border-gray-600'
|
|
88
|
+
this.previewTarget.innerHTML = ''
|
|
89
|
+
this.previewTarget.appendChild(img)
|
|
90
|
+
}
|
|
91
|
+
this.previewTarget.classList.remove('hidden')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Show file info with remove button
|
|
95
|
+
this.showFileInfo(file, true)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
reader.readAsDataURL(file)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
showFileInfo(file, withPreview = false) {
|
|
102
|
+
if (!this.hasFileInfoTarget) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const fileSize = this.humanFileSize(file.size)
|
|
107
|
+
const fileName = file.name
|
|
108
|
+
|
|
109
|
+
this.fileInfoTarget.innerHTML = `
|
|
110
|
+
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
|
|
111
|
+
<div class="flex items-center gap-x-3 flex-1 min-w-0">
|
|
112
|
+
<svg class="size-8 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
113
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
114
|
+
</svg>
|
|
115
|
+
<div class="flex-1 min-w-0">
|
|
116
|
+
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">${fileName}</p>
|
|
117
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">${fileSize}</p>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
<button type="button"
|
|
121
|
+
data-action="click->file-upload#removeFile"
|
|
122
|
+
class="flex-shrink-0 ml-3 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
|
|
123
|
+
<svg class="size-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
124
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
125
|
+
</svg>
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
`
|
|
129
|
+
this.fileInfoTarget.classList.remove('hidden')
|
|
130
|
+
|
|
131
|
+
// Hide the dropzone upload area
|
|
132
|
+
if (this.hasDropzoneTarget) {
|
|
133
|
+
this.dropzoneTarget.classList.add('hidden')
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
removeFile() {
|
|
138
|
+
// Clear the file input
|
|
139
|
+
this.inputTarget.value = ''
|
|
140
|
+
|
|
141
|
+
// Hide preview and file info
|
|
142
|
+
if (this.hasPreviewTarget) {
|
|
143
|
+
this.previewTarget.classList.add('hidden')
|
|
144
|
+
this.previewTarget.innerHTML = ''
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (this.hasFileInfoTarget) {
|
|
148
|
+
this.fileInfoTarget.classList.add('hidden')
|
|
149
|
+
this.fileInfoTarget.innerHTML = ''
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Show the dropzone again
|
|
153
|
+
if (this.hasDropzoneTarget) {
|
|
154
|
+
this.dropzoneTarget.classList.remove('hidden')
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
humanFileSize(bytes) {
|
|
159
|
+
if (bytes === 0) return '0 Bytes'
|
|
160
|
+
const k = 1024
|
|
161
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
|
162
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
163
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["input", "results", "group"]
|
|
5
|
+
static values = {
|
|
6
|
+
url: String,
|
|
7
|
+
open: { type: Boolean, default: false },
|
|
8
|
+
index: { type: Number, default: -1 }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
connect() {
|
|
12
|
+
this.debounceTimer = null
|
|
13
|
+
this.handleOutsideClick = this.handleOutsideClick.bind(this)
|
|
14
|
+
this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this)
|
|
15
|
+
document.addEventListener("click", this.handleOutsideClick)
|
|
16
|
+
document.addEventListener("keydown", this.handleGlobalKeydown)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
disconnect() {
|
|
20
|
+
document.removeEventListener("click", this.handleOutsideClick)
|
|
21
|
+
document.removeEventListener("keydown", this.handleGlobalKeydown)
|
|
22
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Cmd+K / Ctrl+K to focus search
|
|
26
|
+
handleGlobalKeydown(event) {
|
|
27
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "k") {
|
|
28
|
+
event.preventDefault()
|
|
29
|
+
this.inputTarget.focus()
|
|
30
|
+
this.inputTarget.select()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onInput() {
|
|
35
|
+
const query = this.inputTarget.value.trim()
|
|
36
|
+
|
|
37
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer)
|
|
38
|
+
|
|
39
|
+
if (query.length < 2) {
|
|
40
|
+
this.close()
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.debounceTimer = setTimeout(() => this.search(query), 200)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async search(query) {
|
|
48
|
+
try {
|
|
49
|
+
const url = `${this.urlValue}?q=${encodeURIComponent(query)}`
|
|
50
|
+
const response = await fetch(url, {
|
|
51
|
+
headers: { "Accept": "application/json" }
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
if (!response.ok) return
|
|
55
|
+
|
|
56
|
+
const data = await response.json()
|
|
57
|
+
this.renderResults(data.groups)
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.warn("[panda-core] Search failed:", e)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
renderResults(groups) {
|
|
64
|
+
const container = this.resultsTarget
|
|
65
|
+
|
|
66
|
+
if (!groups || groups.length === 0) {
|
|
67
|
+
container.innerHTML = `
|
|
68
|
+
<div class="px-4 py-6 text-center text-sm text-white/80">
|
|
69
|
+
No results found
|
|
70
|
+
</div>
|
|
71
|
+
`
|
|
72
|
+
this.open()
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let html = ""
|
|
77
|
+
groups.forEach(group => {
|
|
78
|
+
const icon = group.icon ? `<i class="${this.escapeAttr(group.icon)} mr-2.5"></i>` : ""
|
|
79
|
+
html += `<div class="px-3 pt-3 pb-1"><span class="text-xs font-semibold text-white/40 uppercase tracking-wider">${icon}${this.escapeHtml(group.name)}</span></div>`
|
|
80
|
+
|
|
81
|
+
group.results.forEach(result => {
|
|
82
|
+
const desc = result.description
|
|
83
|
+
? `<span class="text-xs text-white/40 truncate">${this.escapeHtml(result.description)}</span>`
|
|
84
|
+
: ""
|
|
85
|
+
html += `
|
|
86
|
+
<a href="${this.escapeAttr(result.href)}" class="search-result flex flex-col gap-0.5 px-3 py-2 rounded-lg hover:bg-white/15 transition-colors cursor-pointer" data-action="keydown->global-search#onResultKeydown">
|
|
87
|
+
<span class="text-sm text-white/90 truncate">${this.escapeHtml(result.name)}</span>
|
|
88
|
+
${desc}
|
|
89
|
+
</a>
|
|
90
|
+
`
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
container.innerHTML = html
|
|
95
|
+
this.indexValue = -1
|
|
96
|
+
this.open()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
open() {
|
|
100
|
+
this.resultsTarget.classList.remove("hidden")
|
|
101
|
+
this.openValue = true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
close() {
|
|
105
|
+
this.resultsTarget.classList.add("hidden")
|
|
106
|
+
this.resultsTarget.innerHTML = ""
|
|
107
|
+
this.openValue = false
|
|
108
|
+
this.indexValue = -1
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
onInputKeydown(event) {
|
|
112
|
+
if (!this.openValue) return
|
|
113
|
+
|
|
114
|
+
const results = this.resultsTarget.querySelectorAll(".search-result")
|
|
115
|
+
if (results.length === 0) return
|
|
116
|
+
|
|
117
|
+
switch (event.key) {
|
|
118
|
+
case "ArrowDown":
|
|
119
|
+
event.preventDefault()
|
|
120
|
+
this.indexValue = Math.min(this.indexValue + 1, results.length - 1)
|
|
121
|
+
this.highlightResult(results)
|
|
122
|
+
break
|
|
123
|
+
case "ArrowUp":
|
|
124
|
+
event.preventDefault()
|
|
125
|
+
this.indexValue = Math.max(this.indexValue - 1, 0)
|
|
126
|
+
this.highlightResult(results)
|
|
127
|
+
break
|
|
128
|
+
case "Enter":
|
|
129
|
+
event.preventDefault()
|
|
130
|
+
if (this.indexValue >= 0 && results[this.indexValue]) {
|
|
131
|
+
results[this.indexValue].click()
|
|
132
|
+
}
|
|
133
|
+
break
|
|
134
|
+
case "Escape":
|
|
135
|
+
event.preventDefault()
|
|
136
|
+
this.close()
|
|
137
|
+
this.inputTarget.blur()
|
|
138
|
+
break
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
onResultKeydown(event) {
|
|
143
|
+
if (event.key === "Escape") {
|
|
144
|
+
this.close()
|
|
145
|
+
this.inputTarget.focus()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
highlightResult(results) {
|
|
150
|
+
results.forEach((el, i) => {
|
|
151
|
+
if (i === this.indexValue) {
|
|
152
|
+
el.classList.add("bg-white/10")
|
|
153
|
+
el.scrollIntoView({ block: "nearest" })
|
|
154
|
+
} else {
|
|
155
|
+
el.classList.remove("bg-white/10")
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
onFocus() {
|
|
161
|
+
// If there are already results rendered, show them
|
|
162
|
+
if (this.resultsTarget.innerHTML.trim() !== "") {
|
|
163
|
+
this.open()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
handleOutsideClick(event) {
|
|
168
|
+
if (this.openValue && !this.element.contains(event.target)) {
|
|
169
|
+
this.close()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
escapeHtml(text) {
|
|
174
|
+
if (!text) return ""
|
|
175
|
+
const div = document.createElement("div")
|
|
176
|
+
div.textContent = text
|
|
177
|
+
return div.innerHTML
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
escapeAttr(text) {
|
|
181
|
+
if (!text) return ""
|
|
182
|
+
return text.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">")
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -10,6 +10,42 @@ application.register("image-cropper", ImageCropperController)
|
|
|
10
10
|
import NavigationToggleController from "./navigation_toggle_controller.js"
|
|
11
11
|
application.register("navigation-toggle", NavigationToggleController)
|
|
12
12
|
|
|
13
|
+
import MobileSidebarController from "./mobile_sidebar_controller.js"
|
|
14
|
+
application.register("mobile-sidebar", MobileSidebarController)
|
|
15
|
+
|
|
16
|
+
import RowLinkController from "./row_link_controller.js"
|
|
17
|
+
application.register("row-link", RowLinkController)
|
|
18
|
+
|
|
19
|
+
import CollapsibleItemController from "./collapsible_item_controller.js"
|
|
20
|
+
application.register("collapsible-item", CollapsibleItemController)
|
|
21
|
+
|
|
22
|
+
import CustomSelectController from "./custom_select_controller.js"
|
|
23
|
+
application.register("custom-select", CustomSelectController)
|
|
24
|
+
|
|
25
|
+
import ClipboardController from "./clipboard_controller.js"
|
|
26
|
+
application.register("clipboard", ClipboardController)
|
|
27
|
+
|
|
28
|
+
import AvatarUploadController from "./avatar_upload_controller.js"
|
|
29
|
+
application.register("avatar-upload", AvatarUploadController)
|
|
30
|
+
|
|
31
|
+
import GlobalSearchController from "./global_search_controller.js"
|
|
32
|
+
application.register("global-search", GlobalSearchController)
|
|
33
|
+
|
|
34
|
+
import TagInputController from "./tag_input_controller.js"
|
|
35
|
+
application.register("tag-input", TagInputController)
|
|
36
|
+
|
|
37
|
+
import DatepickerController from "./datepicker_controller.js"
|
|
38
|
+
application.register("datepicker", DatepickerController)
|
|
39
|
+
|
|
40
|
+
import DaterangeController from "./daterange_controller.js"
|
|
41
|
+
application.register("daterange", DaterangeController)
|
|
42
|
+
|
|
43
|
+
import FileGalleryController from "./file_gallery_controller.js"
|
|
44
|
+
application.register("file-gallery", FileGalleryController)
|
|
45
|
+
|
|
46
|
+
import FileUploadController from "./file_upload_controller.js"
|
|
47
|
+
application.register("file-upload", FileUploadController)
|
|
48
|
+
|
|
13
49
|
// Import and register TailwindCSS Stimulus Components
|
|
14
50
|
// These are needed for UI components like slideover, modals, alerts, etc.
|
|
15
51
|
import { Alert, Autosave, ColorPreview, Dropdown, Modal, Tabs, Popover, Toggle, Slideover } from "../tailwindcss-stimulus-components.js"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["sidebar", "backdrop", "hamburger"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this._isOpen = false
|
|
8
|
+
this._mediaQuery = window.matchMedia("(min-width: 1024px)")
|
|
9
|
+
this._handleBreakpoint = this._handleBreakpoint.bind(this)
|
|
10
|
+
this._mediaQuery.addEventListener("change", this._handleBreakpoint)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
disconnect() {
|
|
14
|
+
this._mediaQuery.removeEventListener("change", this._handleBreakpoint)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toggle() {
|
|
18
|
+
if (this._isOpen) {
|
|
19
|
+
this.close()
|
|
20
|
+
} else {
|
|
21
|
+
this.open()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
open() {
|
|
26
|
+
this._isOpen = true
|
|
27
|
+
this._enableTransition()
|
|
28
|
+
this.sidebarTarget.classList.remove("max-h-16")
|
|
29
|
+
this.sidebarTarget.classList.add("max-h-screen")
|
|
30
|
+
this.backdropTarget.classList.remove("hidden")
|
|
31
|
+
this.hamburgerTarget.setAttribute("aria-expanded", "true")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
close() {
|
|
35
|
+
this._isOpen = false
|
|
36
|
+
this._enableTransition()
|
|
37
|
+
this.sidebarTarget.classList.remove("max-h-screen")
|
|
38
|
+
this.sidebarTarget.classList.add("max-h-16")
|
|
39
|
+
this.backdropTarget.classList.add("hidden")
|
|
40
|
+
this.hamburgerTarget.setAttribute("aria-expanded", "false")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Apply transition only during active mobile toggles to prevent
|
|
44
|
+
// Turbo page visits from animating the sidebar height on desktop.
|
|
45
|
+
_enableTransition() {
|
|
46
|
+
const el = this.sidebarTarget
|
|
47
|
+
el.classList.add("transition-all", "ease-in-out", "duration-300")
|
|
48
|
+
el.addEventListener("transitionend", () => {
|
|
49
|
+
el.classList.remove("transition-all", "ease-in-out", "duration-300")
|
|
50
|
+
}, { once: true })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_handleBreakpoint(event) {
|
|
54
|
+
if (event.matches && this._isOpen) {
|
|
55
|
+
this.close()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
|
-
// Navigation toggle controller for expandable menu items
|
|
4
|
-
// Usage:
|
|
5
|
-
// <div data-controller="navigation-toggle">
|
|
6
|
-
// <button data-navigation-toggle-target="button"
|
|
7
|
-
// data-action="click->navigation-toggle#toggle"
|
|
8
|
-
// aria-controls="sub-menu-1"
|
|
9
|
-
// aria-expanded="false">
|
|
10
|
-
// <span>Menu Item</span>
|
|
11
|
-
// <i data-navigation-toggle-target="icon" class="fa-solid fa-chevron-right"></i>
|
|
12
|
-
// </button>
|
|
13
|
-
// <div id="sub-menu-1" data-navigation-toggle-target="menu" class="hidden">
|
|
14
|
-
// <a href="#">Sub Item 1</a>
|
|
15
|
-
// <a href="#">Sub Item 2</a>
|
|
16
|
-
// </div>
|
|
17
|
-
// </div>
|
|
18
3
|
export default class extends Controller {
|
|
19
|
-
static targets = ["button", "menu", "icon"]
|
|
4
|
+
static targets = ["button", "menu", "icon", "wrapper"]
|
|
5
|
+
|
|
6
|
+
// Classes toggled on the button when expanding/collapsing
|
|
7
|
+
static expandedButtonClasses = ["rounded-t-xl", "bg-white/15", "text-white"]
|
|
8
|
+
static collapsedButtonClasses = ["rounded-xl", "hover:bg-white/5", "text-white/80"]
|
|
20
9
|
|
|
21
10
|
connect() {
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
// Suppress transitions on connect to prevent visual jump during Turbo navigation
|
|
12
|
+
const menu = this.menuTarget
|
|
13
|
+
menu.classList.remove("transition-all", "duration-200", "ease-out")
|
|
14
|
+
|
|
15
|
+
const hasActiveChild = menu.querySelector('[class*="bg-primary-500"]')
|
|
25
16
|
if (hasActiveChild) {
|
|
26
17
|
this.expand()
|
|
27
18
|
} else {
|
|
28
|
-
// Explicitly ensure the menu is collapsed if no active child
|
|
29
19
|
this.collapse()
|
|
30
20
|
}
|
|
21
|
+
|
|
22
|
+
// Re-enable transitions for user interactions; store RAF ID so disconnect() can cancel it
|
|
23
|
+
this._rafId = requestAnimationFrame(() => {
|
|
24
|
+
menu.classList.add("transition-all", "duration-200", "ease-out")
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
disconnect() {
|
|
29
|
+
if (this._rafId) {
|
|
30
|
+
cancelAnimationFrame(this._rafId)
|
|
31
|
+
}
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
toggle(event) {
|
|
@@ -45,20 +46,44 @@ export default class extends Controller {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
expand() {
|
|
48
|
-
|
|
49
|
-
this.menuTarget.
|
|
49
|
+
// Menu animation
|
|
50
|
+
this.menuTarget.classList.remove("max-h-0", "opacity-0")
|
|
51
|
+
this.menuTarget.classList.add("max-h-96", "opacity-100")
|
|
52
|
+
|
|
50
53
|
this.buttonTarget.setAttribute("aria-expanded", "true")
|
|
51
54
|
|
|
55
|
+
// Button styling: collapsed → expanded
|
|
56
|
+
this.constructor.collapsedButtonClasses.forEach(c => this.buttonTarget.classList.remove(c))
|
|
57
|
+
this.constructor.expandedButtonClasses.forEach(c => this.buttonTarget.classList.add(c))
|
|
58
|
+
|
|
59
|
+
// Wrapper background
|
|
60
|
+
if (this.hasWrapperTarget) {
|
|
61
|
+
this.wrapperTarget.classList.add("rounded-xl", "bg-white/10", "overflow-hidden")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Chevron rotation
|
|
52
65
|
if (this.hasIconTarget) {
|
|
53
66
|
this.iconTarget.classList.add("rotate-90")
|
|
54
67
|
}
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
collapse() {
|
|
58
|
-
|
|
59
|
-
this.menuTarget.
|
|
71
|
+
// Menu animation
|
|
72
|
+
this.menuTarget.classList.remove("max-h-96", "opacity-100")
|
|
73
|
+
this.menuTarget.classList.add("max-h-0", "opacity-0")
|
|
74
|
+
|
|
60
75
|
this.buttonTarget.setAttribute("aria-expanded", "false")
|
|
61
76
|
|
|
77
|
+
// Button styling: expanded → collapsed
|
|
78
|
+
this.constructor.expandedButtonClasses.forEach(c => this.buttonTarget.classList.remove(c))
|
|
79
|
+
this.constructor.collapsedButtonClasses.forEach(c => this.buttonTarget.classList.add(c))
|
|
80
|
+
|
|
81
|
+
// Wrapper background
|
|
82
|
+
if (this.hasWrapperTarget) {
|
|
83
|
+
this.wrapperTarget.classList.remove("rounded-xl", "bg-white/10", "overflow-hidden")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Chevron rotation
|
|
62
87
|
if (this.hasIconTarget) {
|
|
63
88
|
this.iconTarget.classList.remove("rotate-90")
|
|
64
89
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Handles clickable table rows that contain a block-link anchor.
|
|
4
|
+
//
|
|
5
|
+
// Uses event delegation on the table container rather than CSS ::after
|
|
6
|
+
// pseudo-elements, which don't work reliably with display: table-row
|
|
7
|
+
// in Safari (position: relative on table-row is undefined per spec).
|
|
8
|
+
export default class extends Controller {
|
|
9
|
+
click(event) {
|
|
10
|
+
// Don't interfere with actual links (except block-link) or buttons
|
|
11
|
+
if (event.target.closest("a:not(.block-link), button, input")) return
|
|
12
|
+
|
|
13
|
+
const row = event.target.closest(".table-row")
|
|
14
|
+
if (!row) return
|
|
15
|
+
|
|
16
|
+
const link = row.querySelector("a.block-link")
|
|
17
|
+
if (!link) return
|
|
18
|
+
|
|
19
|
+
event.preventDefault()
|
|
20
|
+
|
|
21
|
+
if (typeof Turbo !== "undefined") {
|
|
22
|
+
Turbo.visit(link.href)
|
|
23
|
+
} else {
|
|
24
|
+
window.location.href = link.href
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|