diff --git a/Languages/en_US/Editor.php b/Languages/en_US/Editor.php index 77beb9a6f3..f3a7ec9fb9 100644 --- a/Languages/en_US/Editor.php +++ b/Languages/en_US/Editor.php @@ -15,6 +15,21 @@ $editortxt['font_name'] = 'Font name'; $editortxt['font_size'] = 'Font size'; $editortxt['font_color'] = 'Font color'; +$editortxt['black'] = 'Black'; +$editortxt['red'] = 'Red'; +$editortxt['yellow'] = 'Yellow'; +$editortxt['pink'] = 'Pink'; +$editortxt['green'] = 'Green'; +$editortxt['orange'] = 'Orange'; +$editortxt['purple'] = 'Purple'; +$editortxt['blue'] = 'Blue'; +$editortxt['beige'] = 'Beige'; +$editortxt['brown'] = 'Brown'; +$editortxt['teal'] = 'Teal'; +$editortxt['navy'] = 'Navy'; +$editortxt['maroon'] = 'Maroon'; +$editortxt['lime_green'] = 'Lime Green'; +$editortxt['white'] = 'White'; $editortxt['remove_formatting'] = 'Remove formatting'; $editortxt['cut'] = 'Cut'; $editortxt['browser_no_cut'] = 'Your browser does not allow the cut command. Please use the keyboard shortcut Ctrl/Cmd-X'; diff --git a/Languages/en_US/General.php b/Languages/en_US/General.php index 84d0b33a0f..c1712ea84e 100644 --- a/Languages/en_US/General.php +++ b/Languages/en_US/General.php @@ -239,6 +239,7 @@ $txt['page'] = 'Page'; $txt['prev'] = 'Previous page'; $txt['next'] = 'Next page'; +$txt['breadcrumb'] = 'Breadcrumb'; $txt['page_title_number'] = '{title} - Page {pagenum, number, integer}'; @@ -307,7 +308,8 @@ $txt['reply_quote'] = 'Reply with quote'; $txt['reply'] = 'Reply'; $txt['reply_noun'] = 'Reply'; -$txt['reply_number'] = 'Reply #{0, number} - '; +$txt['reply_number'] = 'Reply #{0, number}'; +$txt['reply_number_sr'] = 'Reply #{0, number}'; $txt['approve'] = 'Approve'; $txt['unapprove'] = 'Unapprove'; $txt['approve_all'] = 'approve all'; diff --git a/Languages/en_US/Help.php b/Languages/en_US/Help.php index 4eb6b15316..20593cf9ec 100644 --- a/Languages/en_US/Help.php +++ b/Languages/en_US/Help.php @@ -265,6 +265,7 @@ $helptxt['enable_ajax_alerts'] = 'This option allows your members to receive AJAX notifications. This means that members do not need to refresh the page to get new notifications.
NOTE: This option might cause a severe load at your server with many users online.'; $helptxt['alerts_auto_purge'] = 'Once an alert has been read, it is rarely needed again. For performance reasons, it is a good idea to automatically delete them after a while.'; $helptxt['jquery_source'] = 'This will determine the source used to load the jQuery Library. Google CDN, jQuery CDN and Microsoft CDN will load the jQuery library from those respective CDN networks. Local will only use the local source. Custom allows you to specify a custom URL for the library.'; +$helptxt['fontawesome_source'] = 'This will determine the source used to load the Font Awesome Library. Cloudflare CDN, and Font Awesome CDN will load the Font Awesome library from those respective CDN networks. Local will only use the local source. Custom allows you to specify a custom URL for the library.'; $helptxt['compactTopicPagesEnable'] = 'This will just show a selection of the number of pages.
Example: "3" to display: 1 ... 4 [5] 6 ... 9
"5" to display: 1 ... 3 4 [5] 6 7 ... 9'; diff --git a/Languages/en_US/ManageMaintenance.php b/Languages/en_US/ManageMaintenance.php index fe635e5c38..d6e0299ae2 100644 --- a/Languages/en_US/ManageMaintenance.php +++ b/Languages/en_US/ManageMaintenance.php @@ -100,7 +100,7 @@ $txt['database_optimize_attempt'] = 'Attempting to optimize your database...'; $txt['database_optimizing'] = 'Optimizing {0}... {1} KB optimized.'; $txt['database_already_optimized'] = 'All of the tables were already optimized.'; -$txt['database_opimize_unneeded'] = 'It was not necessary to optimize any tables.'; +$txt['database_opimize_unneeded'] = 'No tables need to be optimized.'; $txt['database_optimized'] = ' table(s) optimized.'; $txt['database_no_id'] = 'has a non-existent member ID'; @@ -162,16 +162,14 @@ $txt['maintain_recount'] = 'Recount all forum totals and statistics'; $txt['maintain_recount_info'] = 'Should the total replies of a topic or the number of PMs in your inbox be incorrect: this function will recount all saved counts and statistics for you.'; -$txt['maintain_errors'] = 'Find and repair any errors'; -$txt['maintain_errors_info'] = 'If, for example, posts or topics are missing after a server crash, this function may help in finding them again.'; -$txt['maintain_logs'] = 'Empty out unimportant logs'; -$txt['maintain_logs_info'] = 'This function will empty out all unimportant logs. This should be avoided unless something is wrong, but it does not hurt anything.'; -$txt['maintain_cache'] = 'Empty SMF’s cache'; -$txt['maintain_cache_info'] = 'This function will empty out the cache should you need it to be cleared.'; -$txt['maintain_optimize'] = 'Optimize all tables'; -$txt['maintain_optimize_info'] = 'This task allows you to optimize all tables. This will get rid of overhead, effectively making the tables smaller in size and your forum faster.'; +$txt['maintain_repair'] = 'Find and repair any errors'; +$txt['maintain_repair_info'] = 'Try to find and fix any errors that may prevent posts or topics from showinng up or being searchable. This should be run afer a forum conversion.'; +$txt['maintain_logs'] = 'Clear logs'; +$txt['maintain_logs_info'] = 'Clear out all information-related logs, such as the error log. This should be avoided unless something is wrong, and will not adversely affect forum operations'; +$txt['maintain_cleancache'] = 'Empty SMF’s cache'; +$txt['maintain_cleancache_info'] = 'Empty out the cache should you need it to be cleared.'; $txt['maintain_version'] = 'Check all files against current versions'; -$txt['maintain_version_info'] = 'This maintenance task allows you to do a detailed version check of all forum files against the official list of latest versions.'; +$txt['maintain_version_info'] = 'Runs a detailed version check of all forum files against the official list of latest versions and displays the results.'; $txt['maintain_rebuild_settings'] = 'Rebuild Settings.php'; $txt['maintain_rebuild_settings_info'] = 'This task reconstructs your Settings.php file. It does not change the values stored in the file. Instead, it cleans up and reformats your Settings.php file to a pristine version.'; $txt['maintain_run_now'] = 'Run task now'; @@ -183,7 +181,7 @@ $txt['maintain_old_nothing_else'] = 'Any sort of topic.'; $txt['maintain_old_are_moved'] = 'Moved/merged topic notices.'; $txt['maintain_old_are_locked'] = 'Locked.'; -$txt['maintain_old_are_not_stickied'] = 'But do not count stickied topics.'; +$txt['maintain_old_are_not_stickied'] = 'Exclude sticky topics.'; $txt['maintain_old_all'] = 'All boards (click to select specific boards)'; $txt['maintain_old_choose'] = 'Specific boards (click to select all)'; $txt['maintain_old_remove'] = 'Remove now'; @@ -207,10 +205,10 @@ $txt['mediumtext_introduction'] = 'The default messages table can contain posts up to a size of 65535 characters, in order be able to store bigger texts the column must be converted to "MEDIUMTEXT". This operation is not reversible.'; $txt['body_checking_introduction'] = 'This function will convert the column of your database that contains the text of the messages into a "TEXT" format (currently is "MEDIUMTEXT"). This operation will allow to slightly reduce the amount of space occupied by each message (1 byte per message). If any message stored into the database is longer than 65535 characters it will be truncated and part of the text will be lost.'; -$txt['entity_convert_title'] = 'Convert HTML-entities to UTF-8 characters'; -$txt['entity_convert_only_utf8'] = 'The database needs to be in UTF-8 format before HTML-entities can be converted to UTF-8'; -$txt['entity_convert_introduction'] = 'This function will convert all characters that are stored in the database as HTML-entities to UTF-8 characters. This is especially useful when you have just converted your forum from a character set like ISO-8859-1 while non-latin characters were used on the forum. The browser then sends all characters as HTML-entities. For example, the HTML-entity α represents the greek letter α (alpha). Converting entities to UTF-8 will improve searching and sorting of text and reduce storage size.'; -$txt['entity_convert_proceed'] = 'Proceed'; +$txt['maintain_convertentities'] = 'Convert HTML-entities to UTF-8 characters'; +$txt['maintain_convertentities_only_utf8'] = 'The database needs to be in UTF-8 format before HTML-entities can be converted to UTF-8'; +$txt['maintain_convertentities_info'] = 'This function will convert all characters that are stored in the database as HTML-entities to UTF-8 characters. This is especially useful when you have just converted your forum from a character set like ISO-8859-1 while non-latin characters were used on the forum. The browser then sends all characters as HTML-entities. For example, the HTML-entity α represents the greek letter α (alpha). Converting entities to UTF-8 will improve searching and sorting of text and reduce storage size.'; +$txt['maintain_convertentities_proceed'] = 'Proceed'; // Move topics out. $txt['move_topics_maintenance'] = 'Move Topics'; diff --git a/Languages/en_US/ManageSettings.php b/Languages/en_US/ManageSettings.php index 613785f972..1b4e720c0e 100644 --- a/Languages/en_US/ManageSettings.php +++ b/Languages/en_US/ManageSettings.php @@ -73,12 +73,16 @@ $txt['alerts_auto_purge_0'] = 'Never'; $txt['alerts_per_page'] = 'Alerts Per Page'; $txt['jquery_source'] = 'Source for the jQuery Library'; -$txt['jquery_custom_label'] = 'Custom'; $txt['jquery_custom'] = 'Custom URL to the jQuery Library'; -$txt['jquery_local'] = 'Local'; -$txt['jquery_google_cdn'] = 'Google CDN'; -$txt['jquery_jquery_cdn'] = 'jQuery CDN'; -$txt['jquery_microsoft_cdn'] = 'Microsoft CDN'; +$txt['fontawesome_source'] = 'Source for the FontAwesome Library'; +$txt['fontawesome_custom'] = 'Custom URL to the FontAwesome Library'; +$txt['cdn_custom_label'] = 'Custom'; +$txt['local_cdn'] = 'Local'; +$txt['google_cdn'] = 'Google CDN'; +$txt['jquery_cdn'] = 'jQuery CDN'; +$txt['microsoft_cdn'] = 'Microsoft CDN'; +$txt['cloudflare_cdn'] = 'Cloudflare CDN'; +$txt['fontawesome_cdn'] = 'FontAwesome CDN'; $txt['minimize_files'] = 'Minimize CSS and JavaScript files'; $txt['queryless_urls'] = 'Use friendly URLs'; $txt['queryless_urls_note'] = 'Supported on Apache, Lighttpd, and LiteSpeed only'; diff --git a/Languages/en_US/Profile.php b/Languages/en_US/Profile.php index 5331a6ea37..11b2b67b4d 100644 --- a/Languages/en_US/Profile.php +++ b/Languages/en_US/Profile.php @@ -645,6 +645,8 @@ $txt['theme_opt_posting'] = 'Posting'; $txt['theme_opt_moderation'] = 'Moderation'; $txt['theme_opt_personal_messages'] = 'Personal Messages'; +$txt['theme_opt_colormode'] = 'Theme Color Mode'; +$txt['theme_opt_variant'] = 'Theme Variant'; $txt['export_profile_data'] = 'Download profile data'; $txt['export_profile_data_desc'] = 'This section allows you to export a copy of your forum profile data to a downloadable file, optionally including your posts and personal messages.
Please note:'; diff --git a/Languages/en_US/Themes.php b/Languages/en_US/Themes.php index a25bca89b1..28296bb44d 100644 --- a/Languages/en_US/Themes.php +++ b/Languages/en_US/Themes.php @@ -65,6 +65,7 @@ $txt['theme_url_config'] = 'Theme URLs and Configuration'; $txt['theme_variants'] = 'Theme Variants'; +$txt['theme_colormodes'] = 'Theme Color Modes'; $txt['theme_options'] = 'Theme Options and Preferences'; $txt['actual_theme_name'] = 'This theme’s name: '; $txt['actual_theme_dir'] = 'This theme’s directory: '; @@ -73,6 +74,7 @@ $txt['current_theme_style'] = 'This theme’s style: '; $txt['theme_variants_default'] = 'Default theme variant'; +$txt['variant_default'] = 'Default'; $txt['theme_variants_user_disable'] = 'Disable user variant selection'; $txt['site_slogan'] = 'Site slogan'; @@ -164,3 +166,11 @@ // Open Graph $txt['og_image'] = 'Open Graph image'; $txt['og_image_desc'] = 'Suggested size: 175x175px. Open Graph is used for social media sharing.'; + +// Theme Mode (dark, light, system, etc) +$txt['theme_pick_colormode'] = 'Select Color Mode'; +$txt['theme_colormode_default'] = 'Default color mode'; +$txt['theme_colormode_user_disable'] = 'Disable user color mode selection'; +$txt['colormode_light'] = 'Light'; +$txt['colormode_dark'] = 'Dark'; +$txt['colormode_system'] = 'System Default'; diff --git a/Sources/Actions/Admin/Features.php b/Sources/Actions/Admin/Features.php index 5df1cc9f44..94ec0d8cf5 100644 --- a/Sources/Actions/Admin/Features.php +++ b/Sources/Actions/Admin/Features.php @@ -1572,10 +1572,10 @@ public static function basicConfigVars(): array 'select', 'jquery_source', [ - 'cdn' => Lang::getTxt('jquery_google_cdn', file: 'ManageSettings'), - 'jquery_cdn' => Lang::getTxt('jquery_jquery_cdn', file: 'ManageSettings'), - 'microsoft_cdn' => Lang::getTxt('jquery_microsoft_cdn', file: 'ManageSettings'), - 'local' => Lang::getTxt('jquery_local', file: 'ManageSettings'), + 'cdn' => Lang::getTxt('google_cdn', file: 'ManageSettings'), + 'jquery_cdn' => Lang::getTxt('jquery_cdn', file: 'ManageSettings'), + 'microsoft_cdn' => Lang::getTxt('microsoft_cdn', file: 'ManageSettings'), + 'local' => Lang::getTxt('local_cdn', file: 'ManageSettings'), 'custom' => Lang::getTxt('jquery_custom', file: 'ManageSettings'), ], 'onchange' => 'if (this.value == \'custom\'){document.getElementById(\'jquery_custom\').disabled = false; } else {document.getElementById(\'jquery_custom\').disabled = true;}', @@ -1585,6 +1585,24 @@ public static function basicConfigVars(): array 'jquery_custom', 'disabled' => !isset(Config::$modSettings['jquery_source']) || (isset(Config::$modSettings['jquery_source']) && Config::$modSettings['jquery_source'] != 'custom'), 'size' => 75, ], + + // FontAwesome source + [ + 'select', + 'fontawesome_source', + [ + 'cdn' => Lang::getTxt('cloudflare_cdn', file: 'ManageSettings'), + 'fontawesome_cdn' => Lang::getTxt('fontawesome_cdn', file: 'ManageSettings'), + 'local' => Lang::getTxt('local_cdn', file: 'ManageSettings'), + 'custom' => Lang::getTxt('fontawesome_custom', file: 'ManageSettings'), + ], + 'onchange' => 'if (this.value == \'custom\'){document.getElementById(\'fontawesome_custom\').disabled = false; } else {document.getElementById(\'fontawesome_custom\').disabled = true;}', + ], + [ + 'text', + 'fontawesome_custom', + 'disabled' => !isset(Config::$modSettings['fontawesome_source']) || (isset(Config::$modSettings['fontawesome_source']) && Config::$modSettings['fontawesome_source'] != 'custom'), 'size' => 75, + ], '', // css and js minification. diff --git a/Sources/Actions/Admin/Maintenance.php b/Sources/Actions/Admin/Maintenance.php index 20fceba407..367c9fca4a 100644 --- a/Sources/Actions/Admin/Maintenance.php +++ b/Sources/Actions/Admin/Maintenance.php @@ -79,7 +79,6 @@ class Maintenance implements ActionInterface public static array $subactions = [ 'routine' => [ 'function' => 'routine', - 'template' => 'maintain_routine', 'activities' => [ 'version' => 'version', 'repair' => 'repair', @@ -91,7 +90,6 @@ class Maintenance implements ActionInterface ], 'database' => [ 'function' => 'database', - 'template' => 'maintain_database', 'activities' => [ 'optimize' => 'optimize', 'convertentities' => 'entitiesToUnicode', @@ -155,7 +153,7 @@ public function execute(): void // Set a few things. Utils::$context['page_title'] = Lang::getTxt('maintain_title', file: 'Admin'); Utils::$context['sub_action'] = $this->subaction; - Utils::$context['sub_template'] = !empty(self::$subactions[$this->subaction]['template']) ? self::$subactions[$this->subaction]['template'] : ''; + Utils::$context['sub_template'] = self::$subactions[$this->subaction]['template'] ?? 'options'; $call = \is_string(self::$subactions[$this->subaction]['function']) && method_exists($this, self::$subactions[$this->subaction]['function']) ? [$this, self::$subactions[$this->subaction]['function']] : Utils::getCallable(self::$subactions[$this->subaction]['function']); @@ -184,6 +182,12 @@ public function routine(): void if (isset($_GET['done']) && \in_array($_GET['done'], ['recount', 'rebuild_settings'])) { Utils::$context['maintenance_finished'] = Lang::getTxt('maintain_' . $_GET['done'], file: 'ManageMaintenance'); } + Utils::$context['template_layers'][] = 'maintain'; + Utils::$context['options'] = array_combine( + array_keys(self::$subactions[$this->subaction]['activities']), + array_fill(0, count(self::$subactions[$this->subaction]['activities']), []), + ); + Utils::$context['post_url'] = Config::$scripturl . '?action=admin;area=maintain'; } /** @@ -191,23 +195,30 @@ public function routine(): void */ public function database(): void { + Utils::$context['template_layers'][] = 'maintain'; + Utils::$context['options'] = array_combine( + array_keys(self::$subactions[$this->subaction]['activities']), + array_fill(0, count(self::$subactions[$this->subaction]['activities']), []), + ); + Utils::$context['post_url'] = Config::$scripturl . '?action=admin;area=maintain;sa=database'; + // Show some conversion options? Utils::$context['convert_entities'] = true; if (Config::$db_type == 'mysql') { - $colData = Db::$db->list_columns('{db_prefix}messages', true); + $body_type = array_column(Db::$db->list_columns('{db_prefix}messages', true), 'type', 'name')['body']; + Utils::$context['options']['convertmsgbody']['title'] = Lang::$txt[($body_type == 'text' ? 'mediumtext' : 'text') . '_title']; + Utils::$context['options']['convertmsgbody']['info'] = Lang::$txt['mediumtext_info']; - foreach ($colData as $column) { - if ($column['name'] == 'body') { - $body_type = $column['type']; - } + if ($body_type != 'text' && !empty(Config::$modSettings['max_messageLength']) && Config::$modSettings['max_messageLength'] < 65536) { + Utils::$context['options']['convertmsgbody']['after'] = '

' . Lang::$txt['convert_to_suggest_text'] . '

'; } - - Utils::$context['convert_to'] = $body_type == 'text' ? 'mediumtext' : null; + } else { + unset(Utils::$context['options']['convertmsgbody']); } if (isset($_GET['done']) && $_GET['done'] == 'convertentities') { - Utils::$context['maintenance_finished'] = Lang::getTxt('entity_convert_title', file: 'ManageMaintenance'); + Utils::$context['maintenance_finished'] = Lang::getTxt('maintain_convertentities_title', file: 'ManageMaintenance'); } } diff --git a/Sources/Actions/Admin/Subscriptions.php b/Sources/Actions/Admin/Subscriptions.php index 2c89120980..1b3781721d 100644 --- a/Sources/Actions/Admin/Subscriptions.php +++ b/Sources/Actions/Admin/Subscriptions.php @@ -871,6 +871,7 @@ public function modifyUser(): void // Setup the template. Utils::$context['sub_template'] = 'modify_user_subscription'; Utils::$context['page_title'] = Lang::getTxt(Utils::$context['action_type'] . '_subscriber', file: 'ManagePaid'); + Theme::loadJavaScriptFile('paidsubs.js', ['defer' => true, 'minimize' => true], 'smf_paidsubs'); // If we haven't been passed the subscription ID get it. if (Utils::$context['log_id'] && !Utils::$context['sub_id']) { diff --git a/Sources/Actions/Admin/Themes.php b/Sources/Actions/Admin/Themes.php index 9fe376eb0b..1acdbef168 100644 --- a/Sources/Actions/Admin/Themes.php +++ b/Sources/Actions/Admin/Themes.php @@ -602,7 +602,49 @@ public function setOptions(): void Utils::$context['sub_template'] = 'set_options'; Utils::$context['page_title'] = Lang::getTxt('theme_settings', file: 'Admin'); - Utils::$context['options'] = Utils::$context['theme_options']; + // Check for variants or dark mode + if (!empty(Theme::$current->settings['theme_variants']) || !empty(Theme::$current->settings['has_dark_mode'])) { + Utils::$context['options'] = []; + + // Theme Variants + if (!empty(Theme::$current->settings['theme_variants'])) { + $available_variants = []; + foreach (Theme::$current->settings['theme_variants'] as $variant) { + $available_variants[$variant] = Lang::$txt['variant_' . $variant] ?? $variant; + } + + Utils::$context['options'][] = Lang::$txt['theme_opt_variant']; + Utils::$context['options'][] = [ + 'id' => 'theme_variant', + 'label' => Lang::$txt['theme_pick_variant'], + 'options' => $available_variants, + 'default' => isset(Theme::$current->settings['default_variant']) && !empty(Theme::$current->settings['default_variant']) ? Theme::$current->settings['default_variant'] : Theme::$current->settings['theme_variants'][0], + 'enabled' => !empty(Theme::$current->settings['theme_variants']), + ]; + } + + // Theme Color Mode + if (!empty(Theme::$current->settings['has_dark_mode'])) { + $available_modes = []; + foreach (Theme::$current->settings['theme_colormodes'] as $mode) { + $available_modes[$mode] = Lang::$txt['colormode_' . $mode] ?? $mode; + } + + Utils::$context['options'][] = Lang::$txt['theme_opt_colormode']; + Utils::$context['options'][] = [ + 'id' => 'theme_colormode', + 'label' => Lang::$txt['theme_pick_colormode'], + 'options' => $available_modes, + 'default' => isset(Theme::$current->settings['default_colormode']) && !empty(Theme::$current->settings['default_colormode']) ? Theme::$current->settings['default_colormode'] : Theme::$current->settings['theme_colormodes'][0], + 'enabled' => !empty(Theme::$current->settings['has_dark_mode']), + ]; + } + + Utils::$context['options'] = array_merge(Utils::$context['options'], Utils::$context['theme_options']); + } else { + Utils::$context['options'] = Utils::$context['theme_options']; + } + Utils::$context['theme_settings'] = Theme::$current->settings; if (empty($_REQUEST['who'])) { @@ -833,6 +875,9 @@ public function setSettings(): void if (!empty(Theme::$current->settings['theme_variants'])) { Utils::$context['theme_variants'] = []; + // Add the default variant + Theme::$current->settings['theme_variants'] = array_unique(array_merge(['default'], Theme::$current->settings['theme_variants'])); + foreach (Theme::$current->settings['theme_variants'] as $variant) { // Have any text, old chap? Utils::$context['theme_variants'][$variant] = [ diff --git a/Sources/Actions/BoardIndex.php b/Sources/Actions/BoardIndex.php index 6333705f30..8bbd5b50b2 100644 --- a/Sources/Actions/BoardIndex.php +++ b/Sources/Actions/BoardIndex.php @@ -57,8 +57,11 @@ class BoardIndex implements ActionInterface, Routable public function execute(): void { Theme::loadTemplate('BoardIndex'); - Utils::$context['template_layers'][] = 'boardindex_outer'; - + Utils::$context['sub_templates'] = [ + 'newsfader', + 'boardindex', + 'info_center', + ]; Utils::$context['page_title'] = Lang::getTxt('forum_index', ['forum_name' => Utils::$context['forum_name']], file: 'General'); // Set a canonical URL for this page. diff --git a/Sources/Actions/Calendar.php b/Sources/Actions/Calendar.php index 84bf30d12f..b6435048cb 100644 --- a/Sources/Actions/Calendar.php +++ b/Sources/Actions/Calendar.php @@ -118,7 +118,7 @@ public function show(): void // This is gonna be needed... Theme::loadTemplate('Calendar'); - Theme::loadCSSFile('calendar.css', ['force_current' => false, 'validate' => true, 'rtl' => 'calendar.rtl.css'], 'smf_calendar'); + Theme::loadCSSFile('calendar.css', ['force_current' => false, 'validate' => true], 'smf_calendar'); Theme::loadJavaScriptFile('calendar.js', ['defer' => true], 'smf_calendar'); // Did they specify an individual event ID? If so, let's splice the year/month in to what we would otherwise be doing. @@ -771,6 +771,8 @@ public function clock(): void $bcd = !isset($_REQUEST['rb']) && !isset($_REQUEST['omfg']) && !isset($_REQUEST['time']); Theme::loadTemplate('Calendar'); + Theme::loadCSSFile('calendar.css', ['force_current' => false, 'validate' => true], 'smf_calendar'); + Theme::loadJavaScriptFile('calendar.js', ['defer' => true], 'smf_calendar'); if ($bcd) { Utils::$context['sub_template'] = 'bcd'; diff --git a/Sources/Actions/Credits.php b/Sources/Actions/Credits.php index c4f8e98e67..a8f516aeb3 100644 --- a/Sources/Actions/Credits.php +++ b/Sources/Actions/Credits.php @@ -342,11 +342,13 @@ public function execute(): void 'Minify | © Matthias Mullie | Licensed under The MIT License (MIT)', 'PHP-Punycode | © True B.V. | Licensed under The MIT License (MIT)', 'Coloris | © Mohammed Bassit | Licensed under The MIT License (MIT)', + ' Font Awesome | © 2023 | Licensed under The MIT License (MIT)', ], 'fonts' => [ ' Anonymous Pro | © 2009 | This font is licensed under the SIL Open Font License, Version 1.1', ' ConsolaMono | © 2012 | This font is licensed under the SIL Open Font License, Version 1.1', ' Phennig | © 2009-2012 | This font is licensed under the SIL Open Font License, Version 1.1', + ' Font Awesome | © 2023 | This font is licensed under the SIL Open Font License, Version 1.1', ], ]; diff --git a/Sources/Actions/Display.php b/Sources/Actions/Display.php index c0b7aea94d..4c4092fa85 100644 --- a/Sources/Actions/Display.php +++ b/Sources/Actions/Display.php @@ -209,7 +209,6 @@ public function prepareDisplayContext(): array|bool 'quote' => [ 'label' => Lang::getTxt('quote_action', file: 'General'), 'href' => Config::$scripturl . '?action=post;quote=' . $output['id'] . ';topic=' . Utils::$context['current_topic'] . '.' . Utils::$context['start'] . ';last_msg=' . Topic::$info->id_last_msg, - 'javascript' => 'onclick="return oQuickReply.quote(' . $output['id'] . ');"', 'icon' => 'quote', 'show' => Utils::$context['can_quote'], ], @@ -225,7 +224,7 @@ public function prepareDisplayContext(): array|bool 'label' => Lang::getTxt('quick_edit', file: 'General'), 'class' => 'quick_edit', 'id' => 'modify_button_' . $output['id'], - 'custom' => 'onclick="oQuickModify.modifyMsg(\'' . $output['id'] . '\', \'' . !empty(Config::$modSettings['toggle_subject']) . '\')"', + 'custom' => 'hidden', 'icon' => 'quick_edit_button', 'show' => $output['can_modify'], ], @@ -1005,6 +1004,7 @@ protected function setupTemplate(): void { // Load the proper template. Theme::loadTemplate('Display'); + Theme::loadCSSFile('postbit.css', ['minimize' => true], 'smf_post'); Theme::loadCSSFile('attachments.css', ['minimize' => true, 'order_pos' => 450], 'smf_attachments'); // Set a canonical URL for this page. @@ -1078,7 +1078,7 @@ protected function setupTemplate(): void // Load the drafts js file. if (!empty(Topic::$info->permissions['drafts_autosave'])) { - Theme::loadJavaScriptFile('drafts.js', ['defer' => false, 'minimize' => true], 'smf_drafts'); + Theme::loadJavaScriptFile('sceditor.plugins.drafts.js', ['defer' => true, 'minimize' => true], 'smf_drafts'); } // Spellcheck @@ -1086,16 +1086,15 @@ protected function setupTemplate(): void Theme::loadJavaScriptFile('spellcheck.js', ['defer' => false, 'minimize' => true], 'smf_spellcheck'); } - // topic.js - Theme::loadJavaScriptFile('topic.js', ['defer' => false, 'minimize' => true], 'smf_topic'); - - // quotedText.js + Theme::loadJavaScriptFile('topic.js', ['defer' => true, 'minimize' => true], 'smf_topic'); + Theme::loadJavaScriptFile('sceditor.plugins.quote-fast.js', ['defer' => true, 'minimize' => true], 'smf_quote_fast'); Theme::loadJavaScriptFile('quotedText.js', ['defer' => true, 'minimize' => true], 'smf_quotedText'); // Mentions if (!empty(Config::$modSettings['enable_mentions']) && User::$me->allowedTo('mention')) { - Theme::loadJavaScriptFile('jquery.atwho.min.js', ['defer' => true], 'smf_atwho'); - Theme::loadJavaScriptFile('jquery.caret.min.js', ['defer' => true], 'smf_caret'); + Theme::loadJavaScriptFile('caret.js', ['defer' => true, 'minimize' => true], 'smf_caret'); + Theme::loadCSSFile('atwho.css', ['minimize' => true], 'smf_atwho'); + Theme::loadJavaScriptFile('atwho.js', ['defer' => true, 'minimize' => true], 'smf_atwho'); Theme::loadJavaScriptFile('mentions.js', ['defer' => true, 'minimize' => true], 'smf_mentions'); } @@ -1301,21 +1300,45 @@ protected function setOldTopicWarning(): void */ protected function loadEditor(): void { - // Now create the editor. - new Editor([ + $editorOptions = [ 'id' => 'quickReply', 'value' => '', 'labels' => [ 'post_button' => Lang::getTxt('post', file: 'General'), ], - // add height and width for the editor - 'height' => '150px', - 'width' => '100%', // We do HTML preview here. 'preview_type' => Editor::PREVIEW_HTML, - // This is required + // This is a required field. 'required' => true, - ]); + // SCEditor plugins. + 'plugins' => ['quoteFast'], + // SCEditor options. + 'options' => [ + 'quoteFastOptions' => [ + 'sPostContainerId' => 'forumposts', + 'sQuickButtonsSelector' => '.quickbuttons', + 'iChildNum' => 0, + 'sJumpAnchor' => 'quickreply_anchor', + ], + ], + ]; + + if (!empty(Config::$modSettings['enable_mentions']) && User::$me->allowedTo('mention')) { + $editorOptions['plugins'][] = 'mentions'; + } + + if (!empty(Topic::$info->permissions['drafts_autosave'])) { + $editorOptions['plugins'][] = 'drafts'; + $editorOptions['plugins'][] = 'messageDrafts'; + $editorOptions['options']['draftOptions'] = [ + 'sLastNote' => 'draft_lastautosave', + 'sLastID' => 'id_draft', + 'sQueryParams' => 'action=post2;board=' . Utils::$context['current_board'], + 'iFreq' => empty(Config::$modSettings['drafts_autosave_frequency']) ? 60000 : Config::$modSettings['drafts_autosave_frequency'] * 1000, + ]; + } + + new Editor($editorOptions); Utils::$context['attached'] = ''; Utils::$context['make_poll'] = isset($_REQUEST['poll']); diff --git a/Sources/Actions/Login.php b/Sources/Actions/Login.php index 21f702b778..1dce62508f 100644 --- a/Sources/Actions/Login.php +++ b/Sources/Actions/Login.php @@ -60,6 +60,7 @@ public function execute(): void parent::checkAjax(); // Get the template ready.... not really much else to do. + Theme::loadJavaScriptFile('login.js', ['minimize' => true], 'smf_login'); Utils::$context['page_title'] = Lang::getTxt('login', file: 'General'); Utils::$context['default_username'] = &$_REQUEST['u']; Utils::$context['default_password'] = ''; diff --git a/Sources/Actions/Login2.php b/Sources/Actions/Login2.php index ce81f3de0f..57e0e7076f 100644 --- a/Sources/Actions/Login2.php +++ b/Sources/Actions/Login2.php @@ -237,6 +237,7 @@ public function main(): void // Load the template stuff. Theme::loadTemplate('Login'); + Theme::loadJavaScriptFile('login.js', ['minimize' => true], 'smf_login'); Utils::$context['sub_template'] = 'login'; // Create a one time token. diff --git a/Sources/Actions/MessageIndex.php b/Sources/Actions/MessageIndex.php index d01b3c8a33..50c54370c6 100644 --- a/Sources/Actions/MessageIndex.php +++ b/Sources/Actions/MessageIndex.php @@ -258,7 +258,7 @@ public static function getBoardList(array $boardListOptions = []): array public static function buildTopicContext(array $row): void { // Reference the main color class. - $colorClass = 'windowbg'; + $colorClass = ''; // Does the theme support message previews? if (!empty(Config::$modSettings['preview_characters'])) { @@ -311,9 +311,14 @@ public static function buildTopicContext(array $row): void // Decide how many pages the topic should have. if ($row['num_replies'] + 1 > Utils::$context['messages_per_page']) { + $templateOverrides = [ + 'extra_before' => '', + 'current_page' => '', + ]; + // We can't pass start by reference. $start = -1; - $pages = new PageIndex(Config::$scripturl . '?topic=' . $row['id_topic'] . '.%1$d', $start, $row['num_replies'] + 1, (int) Utils::$context['messages_per_page'], true, false); + $pages = new PageIndex(Config::$scripturl . '?topic=' . $row['id_topic'] . '.%1$d', $start, $row['num_replies'] + 1, (int) Utils::$context['messages_per_page'], true, false, $templateOverrides); // If we can use all, show all. if (!empty(Config::$modSettings['enableAllMessages']) && $row['num_replies'] + 1 < Config::$modSettings['enableAllMessages']) { @@ -350,7 +355,7 @@ public static function buildTopicContext(array $row): void // Is this topic pending approval, or does it have any posts pending approval? if (!empty($row['unapproved_posts']) && User::$me->allowedTo('approve_posts')) { - $colorClass .= (!$row['approved'] ? ' approvetopic' : ' approvepost'); + $colorClass .= !$row['approved'] ? ' approvetopic' : ' approvepost'; } // Sticky topics should get a different color, too. @@ -933,15 +938,11 @@ protected function setUnapprovedPostsMessage(): void { // If we can view unapproved messages and there are some build up a list. if (User::$me->allowedTo('approve_posts') && (Board::$info->unapproved_topics || Board::$info->unapproved_posts)) { - $untopics = Board::$info->unapproved_topics ? '' . Board::$info->unapproved_topics . '' : 0; - - $unposts = Board::$info->unapproved_posts ? '' . (Board::$info->unapproved_posts - Board::$info->unapproved_topics) . '' : 0; - Utils::$context['unapproved_posts_message'] = Lang::getTxt( 'there_are_unapproved_topics', [ - 'topics' => $untopics, - 'posts' => $unposts, + 'topics' => Board::$info->unapproved_topics, + 'posts' => Board::$info->unapproved_posts - Board::$info->unapproved_topics, 'url' => Config::$scripturl . '?action=moderate;area=postmod;sa=' . (Board::$info->unapproved_topics ? 'topics' : 'posts') . ';brd=' . Board::$info->id, ], file: 'General', @@ -958,6 +959,7 @@ protected function setUnapprovedPostsMessage(): void */ protected function setupTemplate(): void { + Theme::loadTemplate('BoardIndex'); Theme::loadTemplate('MessageIndex'); // Javascript for inline editing. diff --git a/Sources/Actions/PersonalMessage.php b/Sources/Actions/PersonalMessage.php index f23b0c4935..fbad9e8d84 100644 --- a/Sources/Actions/PersonalMessage.php +++ b/Sources/Actions/PersonalMessage.php @@ -291,6 +291,7 @@ public function isAgreementAction(): bool public function execute(): void { Theme::loadTemplate(isset($_REQUEST['xml']) ? 'Xml' : 'PersonalMessage'); + Theme::loadCSSFile('postbit.css', ['minimize' => true], 'smf_post'); $this->buildLimitBar(); diff --git a/Sources/Actions/Post.php b/Sources/Actions/Post.php index ba1953b354..ec8aaa2c9e 100644 --- a/Sources/Actions/Post.php +++ b/Sources/Actions/Post.php @@ -348,17 +348,20 @@ public function show(): void // Mentions if (!empty(Config::$modSettings['enable_mentions']) && User::$me->allowedTo('mention')) { - Theme::loadJavaScriptFile('jquery.caret.min.js', ['defer' => true], 'smf_caret'); - Theme::loadJavaScriptFile('jquery.atwho.min.js', ['defer' => true], 'smf_atwho'); + Theme::loadCSSFile('atwho.css', ['minimize' => true], 'smf_atwho'); + Theme::loadJavaScriptFile('atwho.js', ['dmfer' => true, 'minimize' => true], 'smf_atwho'); + Theme::loadJavaScriptFile('caret.js', ['defer' => true, 'minimize' => true], 'smf_caret'); Theme::loadJavaScriptFile('mentions.js', ['defer' => true, 'minimize' => true], 'smf_mentions'); } // Load the drafts.js file if (Utils::$context['drafts_autosave']) { - Theme::loadJavaScriptFile('drafts.js', ['defer' => false, 'minimize' => true], 'smf_drafts'); + Theme::loadJavaScriptFile('sceditor.plugins.drafts.js', ['defer' => true, 'minimize' => true], 'smf_drafts'); } - // quotedText.js + Theme::loadCSSFile('attachments.css', ['minimize' => true, 'order_pos' => 450], 'smf_attachments'); + Theme::loadJavaScriptFile('post.js', ['defer' => true, 'minimize' => true], 'smf_post'); + Theme::loadJavaScriptFile('sceditor.plugins.quote-fast.js', ['defer' => true, 'minimize' => true], 'smf_quote_fast'); Theme::loadJavaScriptFile('quotedText.js', ['defer' => true, 'minimize' => true], 'smf_quotedText'); // Knowing the current board ID might be handy. @@ -1001,8 +1004,6 @@ protected function showPreview(): void 'is_last' => false, ]; } - Utils::$context['last_choice_id'] = $choice_id; - Utils::$context['choices'][\count(Utils::$context['choices']) - 1]['is_last'] = true; } // Are you... a guest? @@ -1600,50 +1601,6 @@ protected function showAttachments(): void });'); } } - - // File Upload. - if (Utils::$context['can_post_attachment']) { - $acceptedFiles = empty(Utils::$context['allowed_extensions']) ? '' : implode(',', array_map( - function ($val) { - return !empty($val) ? ('.' . Utils::htmlTrim($val)) : ''; - }, - explode(',', Utils::$context['allowed_extensions']), - )); - - Theme::loadJavaScriptFile('dropzone.min.js', ['defer' => true], 'smf_dropzone'); - Theme::loadJavaScriptFile('smf_fileUpload.js', ['defer' => true, 'minimize' => true], 'smf_fileUpload'); - Theme::addInlineJavaScript(' - $(function() { - smf_fileUpload({ - dictDefaultMessage : ' . Utils::escapeJavaScript(Lang::getTxt('attach_drop_zone', file: 'Post')) . ', - dictFallbackMessage : ' . Utils::escapeJavaScript(Lang::getTxt('attach_drop_zone_no', file: 'Post')) . ', - dictCancelUpload : ' . Utils::escapeJavaScript(Lang::getTxt('modify_cancel', file: 'General')) . ', - genericError: ' . Utils::escapeJavaScript(Lang::getTxt('attach_php_error', file: 'Post')) . ', - text_attachDropzoneLabel: ' . Utils::escapeJavaScript(Lang::getTxt('attach_drop_zone', file: 'Post')) . ', - text_attachLimitNag: ' . Utils::escapeJavaScript(Lang::getTxt('attach_limit_nag', file: 'Post')) . ', - text_attachLeft: ' . Utils::escapeJavaScript(Lang::getTxt('attachments_left', file: 'Post')) . ', - text_deleteAttach: ' . Utils::escapeJavaScript(Lang::getTxt('attached_file_delete', file: 'Post')) . ', - text_attachDeleted: ' . Utils::escapeJavaScript(Lang::getTxt('attached_file_deleted', file: 'Post')) . ', - text_insertBBC: ' . Utils::escapeJavaScript(Lang::getTxt('attached_insert_bbc', file: 'Post')) . ', - text_attachUploaded: ' . Utils::escapeJavaScript(Lang::getTxt('attached_file_uploaded', file: 'Post')) . ', - text_attach_unlimited: ' . Utils::escapeJavaScript(Lang::getTxt('attach_drop_unlimited', file: 'Post')) . ', - text_totalMaxSize: ' . Utils::escapeJavaScript(Lang::getTxt('attach_max_total_file_size_current', file: 'Post')) . ', - text_max_size_progress: ' . Utils::escapeJavaScript('{currentRemain} ' . (Config::$modSettings['attachmentPostLimit'] >= 1024 ? Lang::getTxt('megabyte', file: 'General') : Lang::getTxt('kilobyte', file: 'General')) . ' / {currentTotal} ' . (Config::$modSettings['attachmentPostLimit'] >= 1024 ? Lang::getTxt('megabyte', file: 'General') : Lang::getTxt('kilobyte', file: 'General'))) . ', - dictMaxFilesExceeded: ' . Utils::escapeJavaScript(Lang::getTxt('more_attachments_error', file: 'Post')) . ', - dictInvalidFileType: ' . Utils::escapeJavaScript(Lang::getTxt('cant_upload_type', Utils::$context, file: 'Post')) . ', - dictFileTooBig: ' . Utils::escapeJavaScript(Lang::getTxt('file_too_big', [Lang::numberFormat(Config::$modSettings['attachmentSizeLimit'], 0)], file: 'Post')) . ', - acceptedFiles: ' . Utils::escapeJavaScript($acceptedFiles) . ', - thumbnailWidth: ' . (!empty(Config::$modSettings['attachmentThumbWidth']) ? Config::$modSettings['attachmentThumbWidth'] : 'null') . ', - thumbnailHeight: ' . (!empty(Config::$modSettings['attachmentThumbHeight']) ? Config::$modSettings['attachmentThumbHeight'] : 'null') . ', - limitMultiFileUploadSize:' . round(max(Config::$modSettings['attachmentPostLimit'] - (Utils::$context['attachments']['total_size'] / 1024), 0)) * 1024 . ', - maxFileAmount: ' . (!empty(Utils::$context['num_allowed_attachments']) ? Utils::$context['num_allowed_attachments'] : 'null') . ', - maxTotalSize: ' . (!empty(Config::$modSettings['attachmentPostLimit']) ? Config::$modSettings['attachmentPostLimit'] : '0') . ', - maxFilesize: ' . (!empty(Config::$modSettings['attachmentSizeLimit']) ? Config::$modSettings['attachmentSizeLimit'] : '0') . ', - }); - });', true); - } - - Theme::loadCSSFile('attachments.css', ['minimize' => true, 'order_pos' => 450], 'smf_attachments'); } /** @@ -1764,19 +1721,92 @@ protected function loadDrafts(): void */ protected function loadEditor(): void { - new Editor([ + $editorOptions = [ 'id' => 'message', 'value' => Utils::$context['message'], 'labels' => [ 'post_button' => Utils::$context['submit_label'], ], - // add height and width for the editor - 'height' => '175px', - 'width' => '100%', // We do XML preview here. 'preview_type' => Editor::PREVIEW_XML, + // This is a required field. 'required' => true, - ]); + // SCEditor plugins. + 'plugins' => ['quoteFast', 'messagePreview'], + // SCEditor options. + 'options' => [ + 'quoteFastOptions' => [ + 'sPostContainerId' => 'recent', + 'sQuickButtonsSelector' => '.quickbuttons', + 'iChildNum' => 1, + 'sJumpAnchor' => 'postmodify', + ], + ], + ]; + + if (!empty(Config::$modSettings['enable_mentions']) && User::$me->allowedTo('mention')) { + $editorOptions['plugins'][] = 'mentions'; + } + + if (Utils::$context['drafts_autosave']) { + $editorOptions['plugins'][] = 'drafts'; + $editorOptions['plugins'][] = 'messageDrafts'; + $editorOptions['options']['draftOptions'] = [ + 'sLastNote' => 'draft_lastautosave', + 'sLastID' => 'id_draft', + 'sQueryParams' => 'action=post2;board=' . Utils::$context['current_board'], + 'iFreq' => empty(Config::$modSettings['drafts_autosave_frequency']) ? 60000 : Config::$modSettings['drafts_autosave_frequency'] * 1000, + ]; + } + + if (Utils::$context['can_post_attachment']) { + Theme::loadJavaScriptFile('dropzone.min.js', ['defer' => true], 'smf_dropzone'); + Theme::loadJavaScriptFile('smf_fileUpload.js', ['defer' => true, 'minimize' => true], 'smf_fileUpload'); + + $editorOptions['plugins'][] = 'fileUpload'; + $editorOptions['options']['fileUploadOptions'] = [ + 'dictDefaultMessage' => Lang::$txt['attach_drop_zone'], + 'dictFallbackMessage' => Lang::$txt['attach_drop_zone_no'], + 'dictCancelUpload' => Lang::$txt['modify_cancel'], + 'genericError' => Lang::$txt['attach_php_error'], + 'text_attachDropzoneLabel' => Lang::$txt['attach_drop_zone'], + 'text_attachLimitNag' => Lang::$txt['attach_limit_nag'], + 'text_attachLeft' => Lang::$txt['attachments_left'], + 'text_deleteAttach' => Lang::$txt['attached_file_delete'], + 'text_attachDeleted' => Lang::$txt['attached_file_deleted'], + 'text_insertBBC' => Lang::$txt['attached_insert_bbc'], + 'text_attachUploaded' => Lang::$txt['attached_file_uploaded'], + 'text_attach_unlimited' => Lang::$txt['attach_drop_unlimited'], + 'text_totalMaxSize' => Lang::$txt['attach_max_total_file_size_current'], + 'text_max_size_progress' => '{currentRemain} ' . (Config::$modSettings['attachmentPostLimit'] >= 1024 ? Lang::$txt['megabyte'] : Lang::$txt['kilobyte']) . ' / {currentTotal} ' . (Config::$modSettings['attachmentPostLimit'] >= 1024 ? Lang::$txt['megabyte'] : Lang::$txt['kilobyte']), + 'dictMaxFilesExceeded' => Lang::$txt['more_attachments_error'], + 'dictInvalidFileType' => Lang::getTxt('cant_upload_type', Utils::$context), + 'dictFileTooBig' => Lang::getTxt( + 'file_too_big', + [Lang::numberFormat(Config::$modSettings['attachmentSizeLimit'], 0)], + ), + 'acceptedFiles' => implode( + ',', + array_map( + fn($val) => !empty($val) ? ('.' . Utils::htmlTrim($val)) : '', + explode(',', Utils::$context['allowed_extensions']), + ), + ), + 'thumbnailWidth' => !empty(Config::$modSettings['attachmentThumbWidth']) ? Config::$modSettings['attachmentThumbWidth'] : null, + 'thumbnailHeight' => !empty(Config::$modSettings['attachmentThumbHeight']) ? Config::$modSettings['attachmentThumbHeight'] : null, + 'limitMultiFileUploadSize' => round( + max( + Config::$modSettings['attachmentPostLimit'] - (Utils::$context['attachments']['total_size'] / 1024), + 0, + ), + ) * 1024, + 'maxFileAmount' => !empty(Utils::$context['num_allowed_attachments']) ? Utils::$context['num_allowed_attachments'] : null, + 'maxTotalSize' => !empty(Config::$modSettings['attachmentPostLimit']) ? Config::$modSettings['attachmentPostLimit'] : 0, + 'maxFilesize' => !empty(Config::$modSettings['attachmentSizeLimit']) ? Config::$modSettings['attachmentSizeLimit'] : 0, + ]; + } + + new Editor($editorOptions); } /** @@ -1830,6 +1860,100 @@ protected function setMessageIcons(): void */ protected function setupPostingFields(): void { + /* + **Keys in Each Option Array**: + + **`can_show`** (bool) + A boolean flag that determines if the option should be displayed. If + set to `true`, the option will be rendered; if `false`, it will be skipped. + + **`name`** (string) + The `name` attribute for the checkbox input. This is the identifier + sent when the form is submitted. Each checkbox must have a unique name. + + **`id`** (string) + The `id` attribute for the checkbox input. This must be unique + and is used to associate the checkbox with its label (`for` attribute). + + **`checked`** (bool) + Whether the checkbox should be checked by default. If set to `true`, + the checkbox will be checked. If set to `false`, it will be unchecked. + + **`label`** (string) + The label text that will appear next to the checkbox. It describes + the option to the user, such as "Notify me of replies". + + **`value`** (string, optional) + The `value` attribute of the checkbox. This determines the value that + will be submitted when the checkbox is checked. The default value is `'1'`. + + **`hidden`** (array, optional) + An associative array of hidden input fields related to this option. + The keys represent the name of the hidden field, and the values + represent the value of the hidden field. + */ + Utils::$context['Additional_options'] = [ + [ + 'can_show' => Utils::$context['can_notify'], + 'name' => 'notify', + 'id' => 'check_notify', + 'checked' => Utils::$context['notify'] || !empty(Theme::$current->options['auto_notify']) || Utils::$context['auto_notify'], + 'label' => Lang::$txt['notify_replies'], + ], + [ + 'can_show' => Utils::$context['can_lock'], + 'name' => 'lock', + 'id' => 'check_lock', + 'checked' => Utils::$context['locked'], + 'label' => Lang::$txt['lock_topic'], + 'hidden' => ['already_locked' => Utils::$context['already_locked']], + ], + [ + 'can_show' => Utils::$context['can_sticky'], + 'name' => 'sticky', + 'id' => 'check_sticky', + 'checked' => Utils::$context['sticky'], + 'label' => Lang::$txt['sticky_after_posting'], + 'hidden' => ['already_sticky' => Utils::$context['already_sticky']], + ], + [ + 'can_show' => Utils::$context['can_move'], + 'name' => 'move', + 'id' => 'check_move', + 'checked' => !empty(Utils::$context['move']), + 'label' => Lang::$txt['move_after_posting'], + ], + [ + 'can_show' => Utils::$context['can_announce'] && Utils::$context['is_first_post'], + 'name' => 'announce_topic', + 'id' => 'check_announce', + 'checked' => !empty(Utils::$context['announce']), + 'label' => Lang::$txt['announce_topic'], + ], + [ + 'can_show' => Utils::$context['show_approval'] === 2, + 'name' => 'approve', + 'id' => 'approve', + 'checked' => Utils::$context['show_approval'] === 2, + 'label' => Lang::$txt['approve_this_post'], + ], + [ + 'can_show' => true, + 'name' => 'goback', + 'id' => 'check_back', + 'checked' => Utils::$context['back_to_topic'] || !empty(Theme::$current->options['return_to_post']), + 'label' => Lang::$txt['back_to_topic'], + ], + [ + 'can_show' => true, + 'name' => 'ns', + 'id' => 'check_smileys', + 'checked' => !Utils::$context['use_smileys'], + 'label' => Lang::$txt['dont_use_smileys'], + 'value' => 'NS', + ], + ]; + /* Each item in Utils::$context['posting_fields'] is an array similar to one of the following: diff --git a/Sources/Actions/Profile/BuddyIgnoreLists.php b/Sources/Actions/Profile/BuddyIgnoreLists.php index 4a37eba5c5..e7545b1de7 100644 --- a/Sources/Actions/Profile/BuddyIgnoreLists.php +++ b/Sources/Actions/Profile/BuddyIgnoreLists.php @@ -96,7 +96,7 @@ public function execute(): void Menu::$loaded['profile']->tab_data = [ 'title' => Lang::getTxt('editBuddyIgnoreLists', file: 'Profile'), 'description' => Lang::getTxt('buddy_ignore_desc', file: 'Profile'), - 'icon_class' => 'main_icons profile_hd', + 'icon_class' => 'main_icons profile medium_icon', 'tabs' => [ 'buddies' => [], 'ignore' => [], diff --git a/Sources/Actions/Profile/Main.php b/Sources/Actions/Profile/Main.php index 4edb72ab28..a739cd1dfc 100644 --- a/Sources/Actions/Profile/Main.php +++ b/Sources/Actions/Profile/Main.php @@ -460,7 +460,7 @@ class Main implements ActionInterface, Routable 'report' => [ 'label' => 'report_profile', 'custom_url' => '{scripturl}?action=reporttm;{session_var}={session_id}', - 'icon' => 'warning', + 'icon' => 'report', 'enabled' => true, 'permission' => [ 'own' => [], @@ -671,12 +671,12 @@ public function execute(): void } // Set the template for this area and add the profile layer. + Theme::loadTemplate('Profile'); + Theme::loadJavaScriptFile('profile.js', ['defer' => true, 'minimize' => true], 'smf_profile'); + Theme::loadCSSFile('profile.css', ['minimize' => true], 'smf_profile'); Utils::$context['sub_template'] = $menu->include_data['sub_template'] ?? $menu->include_data['function']; - Utils::$context['template_layers'][] = 'profile'; - Theme::loadJavaScriptFile('profile.js', ['defer' => false, 'minimize' => true], 'smf_profile'); - // Right - are we saving - if so let's save the old data first. if (Utils::$context['completed_save']) { // Clean up the POST variables. diff --git a/Sources/Actions/Profile/ShowPosts.php b/Sources/Actions/Profile/ShowPosts.php index 3f288a1e5d..8350276c10 100644 --- a/Sources/Actions/Profile/ShowPosts.php +++ b/Sources/Actions/Profile/ShowPosts.php @@ -90,7 +90,7 @@ public function execute(): void Menu::$loaded['profile']->tab_data = [ 'title' => Lang::getTxt('showPosts', file: 'Profile'), 'description' => Lang::getTxt('showPosts_help', file: 'Profile'), - 'icon_class' => 'main_icons profile_hd', + 'icon_class' => 'main_icons profile medium_icon', 'tabs' => [ 'messages' => [ ], diff --git a/Sources/Actions/Profile/StatPanel.php b/Sources/Actions/Profile/StatPanel.php index f1006f1bca..fab53c2a08 100644 --- a/Sources/Actions/Profile/StatPanel.php +++ b/Sources/Actions/Profile/StatPanel.php @@ -49,7 +49,7 @@ public function execute(): void // Menu tab Menu::$loaded['profile']->tab_data = [ 'title' => Lang::getTxt('statPanel_showStats', ['name' => Profile::$member->name]), - 'icon' => 'stats_info.png', + 'icon_class' => 'main_icons stats', ]; // Is the load average too high to allow searching just now? diff --git a/Sources/Actions/Profile/Summary.php b/Sources/Actions/Profile/Summary.php index 25be72a45c..8d9b1398c3 100644 --- a/Sources/Actions/Profile/Summary.php +++ b/Sources/Actions/Profile/Summary.php @@ -48,7 +48,7 @@ public function execute(): void // Menu tab Menu::$loaded['profile']->tab_data = [ 'title' => Lang::getTxt('summary', file: 'General'), - 'icon_class' => 'main_icons profile_hd', + 'icon_class' => 'main_icons profile medium_icon', ]; // Expand the warning settings. diff --git a/Sources/Actions/Profile/ThemeOptions.php b/Sources/Actions/Profile/ThemeOptions.php index ee6d593eff..4dd1012052 100644 --- a/Sources/Actions/Profile/ThemeOptions.php +++ b/Sources/Actions/Profile/ThemeOptions.php @@ -43,6 +43,49 @@ public function execute(): void Theme::loadTemplate('Settings'); Theme::loadSubTemplate('options'); + // Check for variants or dark mode + if (!empty(Theme::$current->settings['theme_variants']) || !empty(Theme::$current->settings['has_dark_mode'])) { + + Lang::load('Themes'); + Utils::$context['additional_options'] = []; + + // Theme Variants + if (!empty(Theme::$current->settings['theme_variants']) && (empty(Theme::$current->settings['disable_user_variant']) || User::$me->allowedTo('admin_forum'))) { + $available_variants = []; + foreach (Theme::$current->settings['theme_variants'] as $variant) { + $available_variants[$variant] = Lang::$txt['variant_' . $variant] ?? $variant; + } + + Utils::$context['additional_options'][] = Lang::$txt['theme_opt_variant']; + Utils::$context['additional_options'][] = [ + 'id' => 'theme_variant', + 'label' => Lang::$txt['theme_pick_variant'], + 'options' => $available_variants, + 'default' => isset(Theme::$current->settings['default_variant']) && !empty(Theme::$current->settings['default_variant']) ? Theme::$current->settings['default_variant'] : Theme::$current->settings['theme_variants'][0], + 'enabled' => !empty(Theme::$current->settings['theme_variants']), + ]; + } + + // Theme Color Mode + if (!empty(Theme::$current->settings['has_dark_mode']) && (empty(Theme::$current->settings['disable_user_mode']) || User::$me->allowedTo('admin_forum'))) { + $available_modes = []; + foreach (Theme::$current->settings['theme_colormodes'] as $mode) { + $available_modes[$mode] = Lang::$txt['colormode_' . $mode] ?? $mode; + } + + Utils::$context['additional_options'][] = Lang::$txt['theme_opt_colormode']; + Utils::$context['additional_options'][] = [ + 'id' => 'theme_colormode', + 'label' => Lang::$txt['theme_pick_colormode'], + 'options' => $available_modes, + 'default' => isset(Theme::$current->settings['default_colormode']) && !empty(Theme::$current->settings['default_colormode']) ? Theme::$current->settings['default_colormode'] : Theme::$current->settings['theme_colormodes'][0], + 'enabled' => !empty(Theme::$current->settings['has_dark_mode']), + ]; + } + + Utils::$context['theme_options'] = array_merge(Utils::$context['additional_options'], Utils::$context['theme_options']); + } + // Let mods hook into the theme options. IntegrationHook::call('integrate_theme_options'); diff --git a/Sources/Actions/Profile/Tracking.php b/Sources/Actions/Profile/Tracking.php index 95c2099ba4..630c9f3a8e 100644 --- a/Sources/Actions/Profile/Tracking.php +++ b/Sources/Actions/Profile/Tracking.php @@ -95,7 +95,7 @@ public function execute(): void Menu::$loaded['profile']->tab_data = [ 'title' => Lang::getTxt('tracking', file: 'Profile'), 'description' => Lang::getTxt('tracking_description', file: 'Profile'), - 'icon_class' => 'main_icons profile_hd', + 'icon_class' => 'main_icons profile medium_icon', 'tabs' => [], ]; diff --git a/Sources/Actions/Reminder.php b/Sources/Actions/Reminder.php index f80454786a..850cac8e6c 100644 --- a/Sources/Actions/Reminder.php +++ b/Sources/Actions/Reminder.php @@ -261,6 +261,7 @@ public function setPassword2(): void IntegrationHook::call('integrate_reset_pass', [$this->member->username, $this->member->username, $_POST['passwrd1']]); Theme::loadTemplate('Login'); + Theme::loadJavaScriptFile('login.js', ['minimize' => true], 'smf_login'); Utils::$context += [ 'page_title' => Lang::getTxt('reminder_password_set', file: 'Profile'), 'sub_template' => 'login', @@ -384,6 +385,7 @@ public function secretAnswer2(): void // Tell them it went fine. Theme::loadTemplate('Login'); + Theme::loadJavaScriptFile('login.js', ['minimize' => true], 'smf_login'); Utils::$context += [ 'page_title' => Lang::getTxt('reminder_password_set', file: 'Profile'), 'sub_template' => 'login', diff --git a/Sources/Actions/Search.php b/Sources/Actions/Search.php index 4427689e13..e835849cd8 100644 --- a/Sources/Actions/Search.php +++ b/Sources/Actions/Search.php @@ -18,6 +18,7 @@ use SMF\ActionInterface; use SMF\ActionRouter; use SMF\ActionTrait; +use SMF\Board; use SMF\Category; use SMF\Config; use SMF\Db\DatabaseApi as Db; @@ -70,6 +71,7 @@ public function execute(): void // Don't load this in XML mode. if (!isset($_REQUEST['xml'])) { Theme::loadTemplate('Search'); + Theme::loadTemplate('GenericControls'); Theme::loadJavaScriptFile('suggest.js', ['defer' => false, 'minimize' => true], 'smf_suggest'); } @@ -174,91 +176,18 @@ public function execute(): void } } - // Find all the boards this user is allowed to see. - $request = Db::$db->query( - 'SELECT b.id_cat, c.name AS cat_name, b.id_board, b.name, b.child_level - FROM {db_prefix}boards AS b - LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat) - WHERE {query_see_board} - AND redirect = {string:empty_string}', - [ - 'empty_string' => '', - ], - identifier: 'order_by_board_order', - ); - Utils::$context['num_boards'] = Db::$db->num_rows($request); - Utils::$context['boards_check_all'] = true; - Utils::$context['categories'] = []; - - while ($row = Db::$db->fetch_assoc($request)) { - // This category hasn't been set up yet.. - if (!isset(Utils::$context['categories'][$row['id_cat']])) { - Utils::$context['categories'][$row['id_cat']] = [ - 'id' => $row['id_cat'], - 'name' => $row['cat_name'], - 'boards' => [], - ]; - } - - $is_recycle_board = !empty(Config::$modSettings['recycle_enable']) && $row['id_board'] == Config::$modSettings['recycle_board']; - - // Set this board up, and let the template know when it's a child. (indent them..) - Utils::$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = [ - 'id' => $row['id_board'], - 'name' => $row['name'], - 'child_level' => $row['child_level'], - ]; - - // If user selected some particular boards, is this one of them? - if (!empty(Utils::$context['search_params']['brd'])) { - Utils::$context['categories'][$row['id_cat']]['boards'][$row['id_board']]['selected'] = \in_array($row['id_board'], Utils::$context['search_params']['brd']); - } - // User didn't select any boards, so select all except ignored and recycle boards. - else { - Utils::$context['categories'][$row['id_cat']]['boards'][$row['id_board']]['selected'] = !$is_recycle_board && !\in_array($row['id_board'], User::$me->ignoreboards); - } - - // If a board wasn't checked that probably should have been ensure the board selection is selected, yo! - if (!Utils::$context['categories'][$row['id_cat']]['boards'][$row['id_board']]['selected'] && !$is_recycle_board) { - Utils::$context['boards_check_all'] = false; - } - } - Db::$db->free_result($request); - - Category::sort(Utils::$context['categories']); - - // Now, let's sort the list of categories into the boards for templates that like that. - $temp_boards = []; - - foreach (Utils::$context['categories'] as $category) { - $temp_boards[] = [ - 'name' => $category['name'], - 'child_ids' => array_keys($category['boards']), - ]; - $temp_boards = array_merge($temp_boards, array_values($category['boards'])); - - // Include a list of boards per category for easy toggling. - Utils::$context['categories'][$category['id']]['child_ids'] = array_keys($category['boards']); + // If user selected some particular boards, is this one of them? + if (!empty(Utils::$context['search_params']['brd'])) { + $boards = Utils::$context['search_params']['brd']; } - - $max_boards = ceil(\count($temp_boards) / 2); - - if ($max_boards == 1) { - $max_boards = 2; + // User didn't select any boards, so select all except ignored and recycle boards. + elseif (!empty(Config::$modSettings['recycle_enable']) && !empty(Config::$modSettings['recycle_board'])) { + $boards = array_merge(User::$me->ignoreboards, [(int) Config::$modSettings['recycle_board']]); + } else { + $boards = User::$me->ignoreboards; } - // Now, alternate them so they can be shown left and right ;). - Utils::$context['board_columns'] = []; - - for ($i = 0; $i < $max_boards; $i++) { - Utils::$context['board_columns'][] = $temp_boards[$i]; - - if (isset($temp_boards[$i + $max_boards])) { - Utils::$context['board_columns'][] = $temp_boards[$i + $max_boards]; - } else { - Utils::$context['board_columns'][] = []; - } - } + Utils::$context['categories'] = Board::getUserVisibleBoards($boards); if (!empty($_REQUEST['topic'])) { Utils::$context['search_params']['topic'] = (int) $_REQUEST['topic']; diff --git a/Sources/BBCode/Code1.php b/Sources/BBCode/Code1.php index f5410b07ea..f3e7d42c4f 100644 --- a/Sources/BBCode/Code1.php +++ b/Sources/BBCode/Code1.php @@ -39,7 +39,7 @@ class Code1 extends BBCode /** * */ - public ?string $content = '
{txt_code} {txt_code_select}
$1'; + public ?string $content = '
{txt_code}
$1
'; /** * @@ -63,53 +63,27 @@ public function validate(BBCodeInterface &$bbc, array|string &$data, array $disa if (!isset($disabled['code'])) { $code = \is_array($data) ? $data[0] : $data; - $add_begin = ( - \is_array($data) - && isset($data[1]) - && strtoupper($data[1]) === 'PHP' - && !str_contains($code, '<?php') - ); + $parts = preg_split('~(<\?php|\?>)~', $code, -1, PREG_SPLIT_DELIM_CAPTURE); - if ($add_begin) { - $code = '<?php ' . $code . '?>'; - $data[1] = 'PHP'; - } - - $php_parts = preg_split('~(<\?php|\?>)~', $code, -1, PREG_SPLIT_DELIM_CAPTURE); - - for ($php_i = 0, $php_n = \count($php_parts); $php_i < $php_n; $php_i++) { + for ($i = 0, $n = count($parts); $i < $n; $i++) { // Do PHP code coloring? - if ($php_parts[$php_i] != '<?php') { + if ($parts[$i] != '<?php') { continue; } - $php_string = ''; - - while ($php_i + 1 < \count($php_parts) && $php_parts[$php_i] != '?>') { - $php_string .= $php_parts[$php_i]; - $php_parts[$php_i++] = ''; - } - - $php_parts[$php_i] = Parser::highlightPhpCode($php_string . $php_parts[$php_i]); + $string = ''; - if (\is_array($data) && empty($data[1])) { - $data[1] = 'PHP'; + while ($i + 1 < $n && $parts[$i] != '?>') { + $string .= $parts[$i]; + $parts[$i++] = ''; } + $parts[$i] = Parser::highlightPhpCode($string . $parts[$i]); } - // Fix the PHP code stuff... - $code = str_replace("
\t
", "\t", implode('', $php_parts)); - - $code = str_replace("\t", "\t", $code); - - if ($add_begin) { - $code = preg_replace(['/^(.+?)<\?.{0,40}?php(?: |\s)/', '/\?>((?:\s*<\/(font|span)>)*)$/m'], '$1', $code, 2); - } - - if (\is_array($data)) { - $data[0] = $code; + if (is_array($data)) { + $data[0] = implode('', $parts); } else { - $data = $code; + $data = implode('', $parts); } } } diff --git a/Sources/BBCode/Code2.php b/Sources/BBCode/Code2.php index 9ab100397c..253d086b61 100644 --- a/Sources/BBCode/Code2.php +++ b/Sources/BBCode/Code2.php @@ -39,7 +39,7 @@ class Code2 extends BBCode /** * */ - public ?string $content = '
{txt_code} ($2) {txt_code_select}
$1'; + public ?string $content = '
{txt_code} ($2)
$1
'; /** * @@ -63,53 +63,27 @@ public function validate(BBCodeInterface &$bbc, array|string &$data, array $disa if (!isset($disabled['code'])) { $code = \is_array($data) ? $data[0] : $data; - $add_begin = ( - \is_array($data) - && isset($data[1]) - && strtoupper($data[1]) === 'PHP' - && !str_contains($code, '<?php') - ); + $parts = preg_split('~(<\?php|\?>)~', $code, -1, PREG_SPLIT_DELIM_CAPTURE); - if ($add_begin) { - $code = '<?php ' . $code . '?>'; - $data[1] = 'PHP'; - } - - $php_parts = preg_split('~(<\?php|\?>)~', $code, -1, PREG_SPLIT_DELIM_CAPTURE); - - for ($php_i = 0, $php_n = \count($php_parts); $php_i < $php_n; $php_i++) { + for ($i = 0, $n = count($parts); $i < $n; $i++) { // Do PHP code coloring? - if ($php_parts[$php_i] != '<?php') { + if ($parts[$i] != '<?php') { continue; } - $php_string = ''; - - while ($php_i + 1 < \count($php_parts) && $php_parts[$php_i] != '?>') { - $php_string .= $php_parts[$php_i]; - $php_parts[$php_i++] = ''; - } - - $php_parts[$php_i] = Parser::highlightPhpCode($php_string . $php_parts[$php_i]); + $string = ''; - if (\is_array($data) && empty($data[1])) { - $data[1] = 'PHP'; + while ($i + 1 < $n && $parts[$i] != '?>') { + $string .= $parts[$i]; + $parts[$i++] = ''; } + $parts[$i] = Parser::highlightPhpCode($string . $parts[$i]); } - // Fix the PHP code stuff... - $code = str_replace("
\t
", "\t", implode('', $php_parts)); - - $code = str_replace("\t", "\t", $code); - - if ($add_begin) { - $code = preg_replace(['/^(.+?)<\?.{0,40}?php(?: |\s)/', '/\?>((?:\s*<\/(font|span)>)*)$/m'], '$1', $code, 2); - } - - if (\is_array($data)) { - $data[0] = $code; + if (is_array($data)) { + $data[0] = implode('', $parts); } else { - $data = $code; + $data = implode('', $parts); } } } diff --git a/Sources/Board.php b/Sources/Board.php index 4abafec7c5..9a70bb9ac8 100644 --- a/Sources/Board.php +++ b/Sources/Board.php @@ -1071,6 +1071,59 @@ public static function getMsgMemberID(int $messageID): int return (int) $memberID; } + /** + * Fetches the list of boards the user is allowed to see and organizes them by categories. + * + * This function queries the database for boards visible to the current user, grouped by categories. + * It returns a structured array of categories and their associated boards. + * + * @param array $boards An array of board IDs to mark as selected. + * @return array The structured array of categories and boards. + */ + public static function getUserVisibleBoards(array $boards): array + { + // Query to fetch boards visible to the user. + $request = Db::$db->query( + 'order_by_board_order', + 'SELECT id_board, b.name, child_level, c.name AS cat_name, id_cat + FROM {db_prefix}boards AS b + JOIN {db_prefix}categories AS c USING (id_cat) + WHERE {query_see_board} + AND redirect = {string:empty_string} + ORDER BY board_order', + [ + 'empty_string' => '', + ], + ); + + $categories = []; + $categoryTracker = []; + $currentCategoryIndex = -1; + + // Process the results and group boards by categories. + while ($row = Db::$db->fetch_assoc($request)) { + if (!isset($categoryTracker[$row['id_cat']])) { + $categories[++$currentCategoryIndex] = [ + 'id' => (int) $row['id_cat'], + 'name' => $row['cat_name'], + 'boards' => [], + ]; + $categoryTracker[$row['id_cat']] = true; + } + + $categories[$currentCategoryIndex]['boards'][] = [ + 'id' => (int) $row['id_board'], + 'name' => $row['name'], + 'child_level' => (int) $row['child_level'], + 'selected' => in_array($row['id_board'], $boards), + ]; + } + + Db::$db->free_result($request); + + return $categories; + } + /** * Modify the settings and position of a board. * Used by ManageBoards.php to change the settings of a board. diff --git a/Sources/Editor.php b/Sources/Editor.php index 1e7d79b617..d9d4f20fe1 100644 --- a/Sources/Editor.php +++ b/Sources/Editor.php @@ -21,7 +21,7 @@ /** * Creates the editor input box so that people can write messages to post. */ -class Editor implements \ArrayAccess +class Editor implements \ArrayAccess, \Stringable { use ArrayAccessHelper; @@ -51,9 +51,14 @@ class Editor implements \ArrayAccess public string $value; /** - * @var bool + * Determines whether the editor starts in rich text (WYSIWYG) mode. + * + * This property is initialized based on several factors: + * - If the global setting `disable_wysiwyg` is enabled; + * - If the user's theme preference or the provided option `force_rich` is true; + * - If a request explicitly sets the editor mode for the instance (e.g., `$_REQUEST[$this->id . '_mode']`); it overrides other settings. * - * Whether WYSIWYG mode is initially on or off. + * @var bool True if the editor starts in WYSIWYG mode, false otherwise. */ public bool $rich_active; @@ -169,21 +174,18 @@ class Editor implements \ArrayAccess public static array $bbc_toolbar = []; /** - * @var string + * @var array * * */ - public static string $bbc_handlers = ''; + public static array $bbc_handlers = []; /** * @var array * * */ - public static array $smileys_toolbar = [ - 'postform' => [], - 'popup' => [], - ]; + public static array $smileys_toolbar = []; /********************* * Internal properties @@ -203,9 +205,76 @@ class Editor implements \ArrayAccess ****************/ /** - * Constructor. + * Initializes a new instance of the editor class and configures its options and behavior. * - * @param array $options Various options for the editor. + * This constructor prepares the editor with default or user-specified options, including its + * dimensions, behavior, and visual features. It also sets up toolbars, smileys, and WYSIWYG + * capabilities if enabled. + * + * Behavior: + * 1. Initializes the editor with a unique ID and sets default options for its dimensions and behavior. + * 2. Configures the smiley and BBC toolbars, applying any necessary translations or replacements. + * 3. Enables WYSIWYG mode based on global settings, user preferences, or provided options. + * 4. Sets the SCEditor options using the provided `$options` array. + * 5. Adds backward compatibility support by storing the editor ID in the global context. + * + * Supported options: + * - `id` (string): The unique identifier for the editor instance. Defaults to 'message'. + * - `value` (string): The initial value of the editor, with certain replacements for compatibility. + * - `disable_smiley_box` (bool): Whether to disable the smiley selection box. Default is false. + * - `columns` (int): Number of columns for the editor text area. Default is 60. + * - `rows` (int): Number of rows for the editor text area. Default is 18. + * - `width` (string): Width of the editor. Default is '100%'. + * - `height` (string): Height of the editor. Default is '250px'. + * - `form` (string): The form name associated with the editor. Default is 'postmodify'. + * - `preview_type` (int): The type of preview for the editor. Default is `self::PREVIEW_HTML`. + * - `labels` (array): Additional labels for customization. + * - `required` (bool): Indicates whether the editor input is required. Default is false. + * - `force_rich` (bool): Force the editor to start in rich text mode. Default is false. + * This option directly influences `$this->rich_active`, which determines if WYSIWYG mode is enabled. + * - `plugins` (array): List of additional plugins to be loaded. Defaults to an empty array if not set. + * - `disable_url_autolinking` (bool): If set, disables the autolinker plugin for URLs. + * - `options` (array): Additional SCEditor configuration options to be merged with default settings. + * + * Custom SCEditor options: + * - `commandsWithDropdown`: Identifies buttons that use dropdown menus. + * - `textOnlyCommands`: Configures buttons to display text without icons. + * - `commandsWithText`: Configures buttons to show text alongside icons. + * + * Hooks: + * - Hook: `integrate_sceditor_options` + * - Parameters: + * - `array &$this->sce_options`: Reference to the array of SCEditor options. + * + * - Hook: `integrate_bbc_buttons` + * - Parameters: + * - `array &$bbc_tags`: Reference to the array of BBC tags. + * - `array &$editor_tag_map`: Reference to the mapping of BBC tags to SCEditor commands. + * - `array &$disabled_tags`: Reference to the array of disabled BBC tags. + * + * Example bbc tag array: + * ```php + * [ + * 'code' => 'b', + * 'description' => Lang::getTxt('bold', var: 'editortxt'), // Optional + * 'image' => 'bold', // Optional + * 'before' => '[b]', // Optional + * 'after' => '[/b]', // Optional + * ] + * ``` + * + * Example editor tag map: + * ```php + * [ + * 'bbcode' => 'sceditorCommand', + * ] + * ``` + * + * Notes: + * - A blank array (`[]`) in the `bbc_tags` represents a separator between groups of buttons in the toolbar. + * - The `editor_tag_map` is only used when the BBC tag and the SCEditor command differ. + * + * @param array $options An associative array of configuration options for the editor. */ public function __construct(array $options) { @@ -224,7 +293,7 @@ public function __construct(array $options) $this->disable_smiley_box = !empty($options['disable_smiley_box']); $this->columns = (int) ($options['columns'] ?? 60); $this->rows = (int) ($options['rows'] ?? 18); - $this->width = (string) ($options['width'] ?? '70%'); + $this->width = (string) ($options['width'] ?? '100%'); $this->height = (string) ($options['height'] ?? '250px'); $this->form = (string) ($options['form'] ?? 'postmodify'); $this->preview_type = (int) ($options['preview_type'] ?? self::PREVIEW_HTML); @@ -237,7 +306,7 @@ public function __construct(array $options) $this->buildBbcToolbar(); $this->buildSmileysToolbar(); - $this->setSCEditorOptions(); + $this->setSCEditorOptions($options); self::$loaded[$this->id] = $this; @@ -245,6 +314,14 @@ public function __construct(array $options) Utils::$context['post_box_name'] = $this->id; } + /** + * Allows this object to be handled like a string. + */ + public function __toString(): string + { + return json_encode($this->sce_options, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + /*********************** * Public static methods ***********************/ @@ -260,6 +337,56 @@ public static function load(array $options): object return new self($options); } + /** + * Adds a new BBC tag to the toolbar before or after a specified tag. + * + * @param array $new_tag The new tag to add. + * @param string $reference_tag The tag code to reference. + * @param bool $before True to add the new tag before the reference tag, false to add it after. + */ + public static function addBbcTag(array $new_tag, string $reference_tag, bool $before = true): void + { + if (self::$bbc_tags == []) { + self::initBbcTags(); + } + + foreach (self::$bbc_tags as &$row) { + foreach ($row as $index => $tag) { + if (isset($tag['code']) && $tag['code'] === $reference_tag) { + if ($before) { + array_splice($row, $index, 0, [$new_tag]); + } else { + array_splice($row, $index + 1, 0, [$new_tag]); + } + + return; + } + } + } + } + + /** + * Removes a BBC tag from the toolbar. + * + * @param string $tag_code The tag code to remove. + */ + public static function removeBbcTag(string $tag_code): void + { + if (self::$bbc_tags == []) { + self::initBbcTags(); + } + + foreach (self::$bbc_tags as &$row) { + foreach ($row as $index => $tag) { + if (isset($tag['code']) && $tag['code'] === $tag_code) { + array_splice($row, $index, 1); + + return; + } + } + } + } + /** * Retrieves a list of message icons. * @@ -333,8 +460,6 @@ public static function getMessageIcons(int $board_id): array foreach ($icons as $k => $dummy) { $icons[$k]['url'] = Theme::$current->settings['images_url'] . '/post/' . $dummy['value'] . '.png'; - - $icons[$k]['is_last'] = false; } } // Otherwise load the icons, and check we give the right image too... @@ -359,7 +484,6 @@ public static function getMessageIcons(int $board_id): array 'value' => $row['filename'], 'name' => $row['title'], 'url' => Theme::$current->settings[file_exists(Theme::$current->settings['theme_dir'] . '/images/post/' . $row['filename'] . '.png') ? 'images_url' : 'default_images_url'] . '/post/' . $row['filename'] . '.png', - 'is_last' => false, ]; } Db::$db->free_result($request); @@ -373,6 +497,170 @@ public static function getMessageIcons(int $board_id): array return array_values($icons); } + /************************* + * Internal static methods + *************************/ + + /** + * Initializes BBC tags for the toolbar. + */ + protected static function initBbcTags(): void + { + // The below array makes it dead easy to add images to this control. Add it to the array and everything else is done for you! + /* + array( + 'code' => 'b', // Required + 'description' => Lang::getTxt('bold', var: 'editortxt'), // Required + 'image' => 'bold', // Optional + 'before' => '[b]', // Optional + 'after' => '[/b]', // Optional + ), + */ + self::$bbc_tags = [ + [ + [ + 'code' => 'bold', + 'description' => Lang::getTxt('bold', var: 'editortxt'), + ], + [ + 'code' => 'italic', + 'description' => Lang::getTxt('italic', var: 'editortxt'), + ], + [ + 'code' => 'underline', + 'description' => Lang::getTxt('underline', var: 'editortxt'), + ], + [ + 'code' => 'strike', + 'description' => Lang::getTxt('strikethrough', var: 'editortxt'), + ], + [ + 'code' => 'superscript', + 'description' => Lang::getTxt('superscript', var: 'editortxt'), + ], + [ + 'code' => 'subscript', + 'description' => Lang::getTxt('subscript', var: 'editortxt'), + ], + [ + 'image' => 'tt', + 'code' => 'tt', + 'description' => Lang::getTxt('tt', var: 'editortxt'), + ], + [], + [ + 'code' => 'pre', + 'description' => Lang::getTxt('preformatted_text', var: 'editortxt'), + ], + [ + 'code' => 'left', + 'description' => Lang::getTxt('align_left', var: 'editortxt'), + ], + [ + 'code' => 'center', + 'description' => Lang::getTxt('center', var: 'editortxt'), + ], + [ + 'code' => 'right', + 'description' => Lang::getTxt('align_right', var: 'editortxt'), + ], + [ + 'code' => 'justify', + 'description' => Lang::getTxt('justify', var: 'editortxt'), + ], + [], + [ + 'code' => 'font', + 'description' => Lang::getTxt('font_name', var: 'editortxt'), + ], + [ + 'code' => 'size', + 'description' => Lang::getTxt('font_size', var: 'editortxt'), + ], + [ + 'code' => 'color', + 'description' => Lang::getTxt('font_color', var: 'editortxt'), + ], + [], + [ + 'code' => 'removeformat', + 'description' => Lang::getTxt('remove_formatting', var: 'editortxt'), + ], + ], + [ + [ + 'code' => 'floatleft', + 'description' => Lang::getTxt('float_left', var: 'editortxt'), + ], + [ + 'code' => 'floatright', + 'description' => Lang::getTxt('float_right', var: 'editortxt'), + ], + [], + [ + 'code' => 'youtube', + 'description' => Lang::getTxt('insert_youtube_video', var: 'editortxt'), + ], + [ + 'code' => 'image', + 'description' => Lang::getTxt('insert_image', var: 'editortxt'), + ], + [ + 'code' => 'email', + 'description' => Lang::getTxt('insert_email', var: 'editortxt'), + ], + [ + 'code' => 'link', + 'description' => Lang::getTxt('insert_link', var: 'editortxt'), + ], + [ + 'code' => 'unlink', + 'description' => Lang::getTxt('unlink', var: 'editortxt'), + ], + [], + [ + 'code' => 'table', + 'description' => Lang::getTxt('insert_table', var: 'editortxt'), + ], + [ + 'code' => 'code', + 'description' => Lang::getTxt('code', var: 'editortxt'), + ], + [ + 'code' => 'quote', + 'description' => Lang::getTxt('insert_quote', var: 'editortxt'), + ], + [ + 'image' => 'heading', + 'code' => 'heading', + 'description' => Lang::getTxt('heading', var: 'editortxt'), + ], + [], + [ + 'code' => 'bulletlist', + 'description' => Lang::getTxt('bullet_list', var: 'editortxt'), + ], + [ + 'code' => 'orderedlist', + 'description' => Lang::getTxt('numbered_list', var: 'editortxt'), + ], + [ + 'code' => 'horizontalrule', + 'description' => Lang::getTxt('insert_horizontal_rule', var: 'editortxt'), + ], + [], + [ + 'code' => 'maximize', + 'description' => Lang::getTxt('maximize', var: 'editortxt'), + ], + [ + 'code' => 'source', + 'description' => Lang::getTxt('view_source', var: 'editortxt'), + ], + ], + ]; + } + /****************** * Internal methods ******************/ @@ -408,32 +696,33 @@ protected function init(): void */ Theme::loadCSSFile('jquery.sceditor.theme.css', ['force_current' => true, 'validate' => true], 'smf_jquery_sceditor_theme'); - // JS makes the editor go round - Theme::loadJavaScriptFile('editor.js', ['minimize' => true], 'smf_editor'); Theme::loadJavaScriptFile('jquery.sceditor.bbcode.min.js', [], 'smf_sceditor_bbcode'); - Theme::loadJavaScriptFile('jquery.sceditor.smf.js', ['minimize' => true], 'smf_sceditor_smf'); - - $scExtraLangs = ' - $.sceditor.locale["' . Lang::getTxt('lang_dictionary', file: 'General') . '"] = { - "Width (optional):": "' . Lang::getTxt('width', var: 'editortxt') . '", - "Height (optional):": "' . Lang::getTxt('height', var: 'editortxt') . '", - "Insert": "' . Lang::getTxt('insert', var: 'editortxt') . '", - "Description (optional):": "' . Lang::getTxt('description', var: 'editortxt') . '", - "Rows:": "' . Lang::getTxt('rows', var: 'editortxt') . '", - "Cols:": "' . Lang::getTxt('cols', var: 'editortxt') . '", - "URL:": "' . Lang::getTxt('url', var: 'editortxt') . '", - "E-mail:": "' . Lang::getTxt('email', var: 'editortxt') . '", - "Video URL:": "' . Lang::getTxt('video_url', var: 'editortxt') . '", - "More": "' . Lang::getTxt('more', var: 'editortxt') . '", - "Close": "' . Lang::getTxt('close', var: 'editortxt') . '", - dateFormat: "' . Lang::getTxt('dateformat', var: 'editortxt') . '", - details: "' . Lang::getTxt('details', var: 'editortxt') . '", - spoiler: "' . Lang::getTxt('spoiler', var: 'editortxt') . '", - summaryPrompt: "' . Lang::getTxt('summary_prompt', var: 'editortxt') . '", - };'; - - Theme::addInlineJavaScript($scExtraLangs, true); + Theme::loadJavaScriptFile('sceditor.plugins.smf.js', ['minimize' => true], 'smf_sceditor_smf_plugin'); + + $locale_key = Lang::getTxt('lang_dictionary', file: 'General'); + + $translation_map = [ + 'Width (optional):' => Lang::getTxt('width', var: 'editortxt'), + 'Height (optional):' => Lang::getTxt('height', var: 'editortxt'), + 'Insert' => Lang::getTxt('insert', var: 'editortxt'), + 'Description (optional):' => Lang::getTxt('description', var: 'editortxt'), + 'Rows:' => Lang::getTxt('rows', var: 'editortxt'), + 'Cols:' => Lang::getTxt('cols', var: 'editortxt'), + 'URL:' => Lang::getTxt('url', var: 'editortxt'), + 'E-mail:' => Lang::getTxt('email', var: 'editortxt'), + 'Video URL:' => Lang::getTxt('video_url', var: 'editortxt'), + 'More' => Lang::getTxt('more', var: 'editortxt'), + 'Close' => Lang::getTxt('close', var: 'editortxt'), + 'dateFormat' => Lang::getTxt('dateformat', var: 'editortxt'), + 'details' => Lang::getTxt('details', var: 'editortxt'), + 'spoiler' => Lang::getTxt('spoiler', var: 'editortxt'), + 'summaryPrompt' => Lang::getTxt('summary_prompt', var: 'editortxt'), + ]; + IntegrationHook::call('integrate_sceditor_locale', [&$translation_map]); + $sc_extra_langs = 'sceditor.locale["' . $locale_key . '"] = ' . json_encode($translation_map, JSON_UNESCAPED_UNICODE) . ';'; + + Theme::addInlineJavaScript($sc_extra_langs, true); Theme::addInlineJavaScript(' var smf_smileys_url = \'' . Theme::$current->settings['smileys_url'] . '\'; var bbc_quote_from = \'' . addcslashes(Lang::getTxt('quote_from', file: 'General'), "'") . '\'; @@ -466,8 +755,8 @@ protected function buildButtons(): void Utils::$context['richedit_buttons'] = [ 'save_draft' => [ 'type' => 'submit', - 'value' => Lang::getTxt('draft_save', file: 'Drafts'), - 'onclick' => !empty(Utils::$context['drafts_save']) ? 'submitThisOnce(this);' : (!empty(Utils::$context['drafts_save']) ? 'return confirm(' . Utils::escapeJavaScript(Lang::getTxt('draft_save_note', file: 'Drafts')) . ') && submitThisOnce(this);' : ''), + 'value' => Lang::$txt['draft_save'], + 'onclick' => !empty(Utils::$context['drafts_save']) ? 'return confirm(' . Utils::escapeJavaScript(Lang::$txt['draft_save_note']) . ');' : '', 'accessKey' => 'd', 'show' => !empty(Utils::$context['drafts_save']), ], @@ -490,12 +779,24 @@ protected function buildButtons(): void } /** - * Initialize the BBC button toolbar, if not already loaded. + * Initializes and constructs the BBC (Bulletin Board Code) button toolbar for the editor. + * + * This method sets up the available BBC tags and their corresponding actions for the editor. + * It manages which tags are enabled, disabled, and how they appear in the toolbar. The method + * also allows integrations or modifications via hooks for custom functionality. + * + * Behavior: + * 1. Links key context variables (e.g., `bbc_tags`, `disabled_tags`, `bbc_toolbar`) for use in the editor. + * 2. Initializes the BBC tags with predefined options, such as the tag's code, description, and icon. + * 3. Maps specific BBC tags to SCEditor commands for seamless functionality. + * 4. Dynamically generates a list of disabled buttons based on configuration settings. + * 5. Applies integration hooks (`integrate_bbc_buttons`) to allow modifications to BBC buttons. + * 6. Assembles the toolbar structure based on the active and disabled tags. */ protected function buildBbcToolbar(): void { - if (!empty(self::$bbc_tags)) { - return; + if (self::$bbc_tags == []) { + self::initBbcTags(); } Utils::$context['bbc_tags'] = &self::$bbc_tags; @@ -503,175 +804,7 @@ protected function buildBbcToolbar(): void Utils::$context['bbc_toolbar'] = &self::$bbc_toolbar; Utils::$context['bbcodes_handlers'] = &self::$bbc_handlers; - // The below array makes it dead easy to add images to this control. Add it to the array and everything else is done for you! - // Note: 'before' and 'after' are deprecated as of SMF 2.1. Instead, use a separate JS file to configure the functionality of your toolbar buttons. - /* - array( - 'code' => 'b', // Required - 'description' => Lang::getTxt('bold', var: 'editortxt'), // Required - 'image' => 'bold', // Optional - 'before' => '[b]', // Deprecated - 'after' => '[/b]', // Deprecated - ), - */ - self::$bbc_tags[] = [ - [ - 'code' => 'bold', - 'description' => Lang::getTxt('bold', var: 'editortxt'), - ], - [ - 'code' => 'italic', - 'description' => Lang::getTxt('italic', var: 'editortxt'), - ], - [ - 'code' => 'underline', - 'description' => Lang::getTxt('underline', var: 'editortxt'), - ], - [ - 'code' => 'strike', - 'description' => Lang::getTxt('strikethrough', var: 'editortxt'), - ], - [ - 'code' => 'superscript', - 'description' => Lang::getTxt('superscript', var: 'editortxt'), - ], - [ - 'code' => 'subscript', - 'description' => Lang::getTxt('subscript', var: 'editortxt'), - ], - [ - 'image' => 'tt', - 'code' => 'tt', - 'description' => Lang::getTxt('tt', var: 'editortxt'), - ], - [ - 'image' => 'hidden', - 'code' => 'spoiler', - 'description' => Lang::getTxt('spoiler', var: 'editortxt'), - ], - [], - [ - 'code' => 'pre', - 'description' => Lang::getTxt('preformatted_text', var: 'editortxt'), - ], - [ - 'code' => 'left', - 'description' => Lang::getTxt('align_left', var: 'editortxt'), - ], - [ - 'code' => 'center', - 'description' => Lang::getTxt('center', var: 'editortxt'), - ], - [ - 'code' => 'right', - 'description' => Lang::getTxt('align_right', var: 'editortxt'), - ], - [ - 'code' => 'justify', - 'description' => Lang::getTxt('justify', var: 'editortxt'), - ], - [], - [ - 'code' => 'font', - 'description' => Lang::getTxt('font_name', var: 'editortxt'), - ], - [ - 'code' => 'size', - 'description' => Lang::getTxt('font_size', var: 'editortxt'), - ], - [ - 'code' => 'color', - 'description' => Lang::getTxt('font_color', var: 'editortxt'), - ], - ]; - - if (empty(Config::$modSettings['disable_wysiwyg'])) { - self::$bbc_tags[\count(self::$bbc_tags) - 1][] = [ - 'code' => 'removeformat', - 'description' => Lang::getTxt('remove_formatting', var: 'editortxt'), - ]; - } - - self::$bbc_tags[] = [ - [ - 'code' => 'floatleft', - 'description' => Lang::getTxt('float_left', var: 'editortxt'), - ], - [ - 'code' => 'floatright', - 'description' => Lang::getTxt('float_right', var: 'editortxt'), - ], - [], - [ - 'code' => 'youtube', - 'description' => Lang::getTxt('insert_youtube_video', var: 'editortxt'), - ], - [ - 'code' => 'image', - 'description' => Lang::getTxt('insert_image', var: 'editortxt'), - ], - [ - 'code' => 'email', - 'description' => Lang::getTxt('insert_email', var: 'editortxt'), - ], - [ - 'code' => 'link', - 'description' => Lang::getTxt('insert_link', var: 'editortxt'), - ], - [ - 'code' => 'unlink', - 'description' => Lang::getTxt('unlink', var: 'editortxt'), - ], - [], - [ - 'code' => 'table', - 'description' => Lang::getTxt('insert_table', var: 'editortxt'), - ], - [ - 'code' => 'code', - 'description' => Lang::getTxt('code', var: 'editortxt'), - ], - [ - 'code' => 'quote', - 'description' => Lang::getTxt('insert_quote', var: 'editortxt'), - ], - [ - 'image' => 'details', - 'code' => 'details', - 'description' => Lang::getTxt('details', var: 'editortxt'), - ], - [], - [ - 'image' => 'heading', - 'code' => 'heading', - 'description' => Lang::getTxt('heading', var: 'editortxt'), - ], - [ - 'code' => 'bulletlist', - 'description' => Lang::getTxt('bullet_list', var: 'editortxt'), - ], - [ - 'code' => 'orderedlist', - 'description' => Lang::getTxt('numbered_list', var: 'editortxt'), - ], - [ - 'code' => 'horizontalrule', - 'description' => Lang::getTxt('insert_horizontal_rule', var: 'editortxt'), - ], - [], - [ - 'code' => 'maximize', - 'description' => Lang::getTxt('maximize', var: 'editortxt'), - ], - ]; - - if (empty(Config::$modSettings['disable_wysiwyg'])) { - self::$bbc_tags[\count(self::$bbc_tags) - 1][] = [ - 'code' => 'source', - 'description' => Lang::getTxt('view_source', var: 'editortxt'), - ]; - } - + // Map BBC tags to SCEditor commands. $editor_tag_map = [ 'b' => 'bold', 'i' => 'italic', @@ -707,6 +840,11 @@ protected function buildBbcToolbar(): void // Generate a list of buttons that shouldn't be shown - this should be the fastest way to do this. $disabled_bbc = !empty(Config::$modSettings['disabledBBC']) ? explode(',', Config::$modSettings['disabledBBC']) : []; + if (empty(Config::$modSettings['disable_wysiwyg'])) { + self::$disabled_tags['removeformat'] = true; + self::$disabled_tags['orderedlist'] = true; + } + foreach ($disabled_bbc as $tag) { $tag = trim($tag); @@ -720,66 +858,52 @@ protected function buildBbcToolbar(): void self::$disabled_tags['floatright'] = true; } - foreach ($editor_tag_map as $tag_name => $tag_alias) { - if ($tag === $tag_name) { - self::$disabled_tags[$tag_alias] = true; - } - } - - self::$disabled_tags[$tag] = true; + self::$disabled_tags[$editor_tag_map[$tag] ?? $tag] = true; } - $bbcodes_styles = ''; + // Allow mods to modify BBC buttons. + IntegrationHook::call('integrate_bbc_buttons', [&self::$bbc_tags, &$editor_tag_map, &self::$disabled_tags]); + + $group = 0; foreach (self::$bbc_tags as $row => $tag_row) { if (!isset(self::$bbc_toolbar[$row])) { self::$bbc_toolbar[$row] = []; } - $tags_row = []; - foreach ($tag_row as $tag) { - if (empty($tag['code'])) { - self::$bbc_toolbar[$row][] = implode(',', $tags_row); - $tags_row = []; - } elseif (empty(self::$disabled_tags[$tag['code']])) { - $tags_row[] = $tag['code']; - - // If we have a custom button image, set it now. - if (isset($tag['image'])) { - $bbcodes_styles .= ' - .sceditor-button-' . $tag['code'] . ' div { - background: url(\'' . Theme::$current->settings['default_theme_url'] . '/images/bbc/' . $tag['image'] . '.png\'); - }'; - } + if (isset($tag['code']) && !isset(self::$disabled_tags[$tag['code']])) { + $this_tag = $editor_tag_map[$tag['code']] ?? $tag['code']; + self::$bbc_toolbar[$row][$group][] = $this_tag; - // Set the tooltip and possibly the command info - self::$bbc_handlers .= ' - sceditor.command.set(' . Utils::escapeJavaScript($tag['code']) . ', { - tooltip: ' . Utils::escapeJavaScript($tag['description'] ?? $tag['code']); - - // Legacy support for 2.0 BBC mods - if (isset($tag['before'])) { - self::$bbc_handlers .= ', - exec: function () { - this.insertText(' . Utils::escapeJavaScript($tag['before']) . (isset($tag['after']) ? ', ' . Utils::escapeJavaScript($tag['after']) : '') . '); - }, - txtExec: [' . Utils::escapeJavaScript($tag['before']) . (isset($tag['after']) ? ', ' . Utils::escapeJavaScript($tag['after']) : '') . ']'; + if (isset($tag['before']) || isset($tag['image'])) { + self::$bbc_handlers[$this_tag] = $tag; } - - self::$bbc_handlers .= ' - });'; + } else { + $group++; } } - - if (!empty($tags_row)) { - self::$bbc_toolbar[$row][] = implode(',', $tags_row); - } } + } - if (!empty($bbcodes_styles)) { - Theme::addInlineCss($bbcodes_styles); - } + /** + * Recursively implodes an array + * + * @param string[] $glue list of values that glue elements together + * @param array $pieces multi-dimensional array to recursively implode + * @param int $counter internal + * + * @return string imploded array + */ + protected function implodeRecursive(array $glue, array $pieces, int $counter = 0): string + { + return implode( + $glue[$counter++], + array_map( + fn($v) => is_array($v) ? $this->implodeRecursive($glue, $v, $counter) : $v, + $pieces, + ), + ); } /** @@ -787,11 +911,11 @@ protected function buildBbcToolbar(): void */ protected function buildSmileysToolbar(): void { - if ($this->disable_smiley_box || !empty(self::$smileys_toolbar['postform']) || !empty(self::$smileys_toolbar['popup'])) { + if ($this->disable_smiley_box || self::$smileys_toolbar != []) { return; } - Utils::$context['smileys'] = self::$smileys_toolbar; + Utils::$context['smileys'] = &self::$smileys_toolbar; if (User::$me->smiley_set != 'none') { // Cache for longer when customized smiley codes aren't enabled @@ -813,26 +937,9 @@ protected function buildSmileysToolbar(): void ); while ($row = Db::$db->fetch_assoc($request)) { - if (Lang::txtExists('icon_' . strtolower($row['description']), file: 'General')) { - $row['description'] = Utils::htmlspecialchars(Lang::getTxt('icon_' . strtolower($row['description']), file: 'General')); - } else { - $row['description'] = Utils::htmlspecialchars($row['description']); - } - - self::$smileys_toolbar[empty($row['hidden']) ? 'postform' : 'popup'][$row['smiley_row']]['smileys'][] = $row; + self::$smileys_toolbar[] = $row; } Db::$db->free_result($request); - - foreach (self::$smileys_toolbar as $section => $smiley_rows) { - foreach ($smiley_rows as $rowIndex => $smileys) { - self::$smileys_toolbar[$section][$rowIndex]['smileys'][\count($smileys['smileys']) - 1]['isLast'] = true; - } - - if (!empty($smiley_rows)) { - self::$smileys_toolbar[$section][\count($smiley_rows) - 1]['isLast'] = true; - } - } - CacheApi::put('posting_smileys_' . User::$me->smiley_set, self::$smileys_toolbar, $cache_time); } else { self::$smileys_toolbar = $temp; @@ -841,98 +948,110 @@ protected function buildSmileysToolbar(): void } /** - * Initialize the smiley toolbar, if enabled and not already loaded. + * Configures the options for the SCEditor instance and applies + * necessary plugins, styles, and other customizations. + * + * This method sets default options for the SCEditor, including dimensions, + * style paths, plugins, toolbar configuration, emoticons, and localization + * settings. Additionally, it allows for customization via integration hooks + * and external editor options provided as arguments. + * + * @param array $editorOptions An associative array of editor options provided externally. + * + * Behavior: + * 1. Initializes default plugins, enabling `autolinker` if URL auto-linking is enabled. + * 2. Configures SCEditor options such as dimensions, toolbar, colors, fonts, and parsing behavior. + * 3. Sets emoticons and their display behavior based on smiley toolbar configurations. + * 4. Provides integration hooks to allow further modification by mods. */ - protected function setSCEditorOptions(): void + protected function setSCEditorOptions(array $editorOptions) { - // Set up the SCEditor options + if (!isset($editorOptions['plugins'])) { + $editorOptions['plugins'] = []; + } + + if ($this->preview_type == self::PREVIEW_XML) { + $editorOptions['plugins'][] = 'xmlPreview'; + Theme::loadJavaScriptFile('sceditor.plugins.xml-preview.js', ['minimize' => true], 'smf_xml_preview'); + } + + if (!empty(Config::$modSettings['autoLinkUrls']) && empty($editorOptions['disable_url_autolinking']) && User::$me->allowedTo('bbc_url')) { + $editorOptions['plugins'][] = 'autolinker'; + Autolinker::createJavaScriptFile(); + Theme::loadJavaScriptFile('sceditor.plugins.autolinker.js', ['minimize' => true], 'smf_autolinker'); + } + $this->sce_options = [ 'width' => $this->width ?? '100%', 'height' => $this->height ?? '250px', 'style' => Theme::$current->settings[file_exists(Theme::$current->settings['theme_dir'] . '/css/jquery.sceditor.default.css') ? 'theme_url' : 'default_theme_url'] . '/css/jquery.sceditor.default.css' . Utils::$context['browser_cache'], + 'autoUpdate' => true, 'emoticonsCompat' => true, - 'colors' => 'black,maroon,brown,green,navy,grey,red,orange,teal,blue,white,hotpink,yellow,limegreen,purple', + 'emoticons' => [], + 'emoticonsEnabled' => !$this->disable_smiley_box, + 'emoticonsRoot' => Theme::$current->settings['smileys_url'] . '/', + 'colors' => [ + ['black', Lang::getTxt('black', var: 'editortxt')], + ['red', Lang::getTxt('red', var: 'editortxt')], + ['yellow', Lang::getTxt('yellow', var: 'editortxt')], + ['pink', Lang::getTxt('pink', var: 'editortxt')], + ['green', Lang::getTxt('green', var: 'editortxt')], + ['orange', Lang::getTxt('orange', var: 'editortxt')], + ['purple', Lang::getTxt('purple', var: 'editortxt')], + ['blue', Lang::getTxt('blue', var: 'editortxt')], + ['beige', Lang::getTxt('beige', var: 'editortxt')], + ['brown', Lang::getTxt('brown', var: 'editortxt')], + ['teal', Lang::getTxt('teal', var: 'editortxt')], + ['navy', Lang::getTxt('navy', var: 'editortxt')], + ['maroon', Lang::getTxt('maroon', var: 'editortxt')], + ['limegreen', Lang::getTxt('lime_green', var: 'editortxt')], + ['white', Lang::getTxt('white', var: 'editortxt')], + ], + 'fonts' => 'Arial,Arial Black,Comic Sans MS,Courier New,Georgia,Impact,Sans-serif,Serif,Times New Roman,Trebuchet MS,Verdana', + 'icons' => 'monocons', 'format' => 'bbcode', - 'plugins' => '', + 'plugins' => 'smf,' . implode(',', $editorOptions['plugins'] ?? []), + 'toolbar' => $this->implodeRecursive(['||', '|', ','], self::$bbc_toolbar), + 'customTextualCommands' => self::$bbc_handlers, + 'startInSourceMode' => !$this->rich_active, 'bbcodeTrim' => false, + 'resizeWidth' => false, + 'resizeMaxHeight' => -1, + 'locale' => $this->locale ?? 'en', + 'rtl' => !empty(Utils::$context['right_to_left']), + 'commandsWithDropdown' => [ + 'color' => true, + 'heading' => true, + 'font' => true, + 'size' => true, + ], + 'textOnlyCommands' => [], + 'commandsWithText' => [], + 'parserOptions' => [ + 'txtVars' => [ + 'code' => Lang::$txt['code'], + ], + ], ]; - if (!empty(Config::$modSettings['autoLinkUrls']) && User::$me->allowedTo('bbc_url')) { - $this->sce_options['plugins'] = 'autolinker'; - Autolinker::createJavaScriptFile(); - Theme::loadJavaScriptFile('autolinker.js', ['minimize' => true], 'smf_autolinker'); - } - - if (!empty($this->locale)) { - $this->sce_options['locale'] = $this->locale; - } - - if (!empty(Utils::$context['right_to_left'])) { - $this->sce_options['rtl'] = true; + if (isset($editorOptions['options'])) { + $this->sce_options = array_merge_recursive($this->sce_options, $editorOptions['options']); } - if ($this->id != 'quickReply') { - $this->sce_options['autofocus'] = true; - } - - $this->sce_options['emoticons'] = []; - $this->sce_options['emoticonsDescriptions'] = []; - $this->sce_options['emoticonsEnabled'] = false; - - if ((!empty(self::$smileys_toolbar['postform']) || !empty(self::$smileys_toolbar['popup'])) && !$this->disable_smiley_box) { - $this->sce_options['emoticonsEnabled'] = true; - $this->sce_options['emoticons']['dropdown'] = []; - $this->sce_options['emoticons']['popup'] = []; - - $count_locations = \count(self::$smileys_toolbar); - - foreach (self::$smileys_toolbar as $location => $smiley_rows) { - $count_locations--; - - unset($smiley_location); - - if ($location == 'postform') { - $smiley_location = &$this->sce_options['emoticons']['dropdown']; - } elseif ($location == 'popup') { - $smiley_location = &$this->sce_options['emoticons']['popup']; - } - - $num_rows = \count($smiley_rows); - - // This is needed because otherwise the editor will remove all the duplicate (empty) keys and leave only 1 additional line - $empty_placeholder = 0; - - foreach ($smiley_rows as $smiley_row) { - foreach ($smiley_row['smileys'] as $smiley) { - $smiley_location[$smiley['code']] = Theme::$current->settings['smileys_url'] . '/' . $smiley['filename']; - - $this->sce_options['emoticonsDescriptions'][$smiley['code']] = $smiley['description']; - } - - if (empty($smiley_row['isLast']) && $num_rows != 1) { - $smiley_location['-' . $empty_placeholder++] = ''; - } - } - } - } - - $this->sce_options['parserOptions']['txtVars'] = [ - 'code' => Lang::getTxt('code', file: 'General'), - ]; - - $this->sce_options['toolbar'] = ''; - - if (!empty(Config::$modSettings['enableBBC'])) { - $count_tags = \count(self::$bbc_tags); - - foreach (self::$bbc_toolbar as $i => $buttonRow) { - $this->sce_options['toolbar'] .= implode('|', $buttonRow); - - $count_tags--; - - if (!empty($count_tags)) { - $this->sce_options['toolbar'] .= '||'; - } + if ($this->sce_options['emoticonsEnabled']) { + $translations = [ + 0 => 'dropdown', + 2 => 'more', + ]; + $prevRowIndex = 0; + + foreach (self::$smileys_toolbar as $smiley) { + $this->sce_options['emoticons'][$translations[$smiley['hidden']]][$smiley['code']] = [ + 'newRow' => $smiley['smiley_row'] != $prevRowIndex, + 'url' => $smiley['filename'], + 'tooltip' => Utils::htmlspecialchars(Lang::$txt['icon_' . strtolower($smiley['description'])] ?? $smiley['description']), + ]; + $prevRowIndex = $smiley['smiley_row']; } } diff --git a/Sources/PersonalMessage/PM.php b/Sources/PersonalMessage/PM.php index 930e085071..c7b2a01c67 100644 --- a/Sources/PersonalMessage/PM.php +++ b/Sources/PersonalMessage/PM.php @@ -595,7 +595,7 @@ public static function compose(): void Theme::loadJavaScriptFile('suggest.js', ['defer' => false, 'minimize' => true], 'smf_suggest'); if (Utils::$context['drafts_autosave']) { - Theme::loadJavaScriptFile('drafts.js', ['defer' => false, 'minimize' => true], 'smf_drafts'); + Theme::loadJavaScriptFile('sceditor.plugins.drafts.js', ['defer' => true, 'minimize' => true], 'smf_drafts'); } Utils::$context['sub_template'] = 'send'; @@ -792,7 +792,7 @@ public static function compose(): void } // Now create the editor. - new Editor([ + $editorOptions = [ 'id' => 'message', 'value' => Utils::$context['message'], 'height' => '175px', @@ -800,9 +800,41 @@ public static function compose(): void 'labels' => [ 'post_button' => Lang::getTxt('send_message', file: 'General'), ], + // We do XML preview here. 'preview_type' => Editor::PREVIEW_XML, + // This is a required field. 'required' => true, - ]); + // SCEditor plugins. + 'plugins' => [], + // SCEditor options. + 'options' => [ + 'previewOptions' => [ + 'sPreviewSectionContainerID' => 'preview_section', + 'sPreviewSubjectContainerID' => 'preview_subject', + 'sPreviewBodyContainerID' => 'preview_body', + 'sErrorsContainerID' => 'errors', + 'sErrorsSeriousContainerID' => 'error_serious', + 'sErrorsListContainerID' => 'error_list', + 'sCaptionContainerID' => 'caption_%ID%', + 'sTxtPreviewTitle' => Utils::escapeJavaScript(Lang::$txt['preview_title']), + 'sTxtPreviewFetch' => Utils::escapeJavaScript(Lang::$txt['preview_fetch']), + 'sUrl' => Config::$scripturl . '?action=pm;sa=send2;preview;xml', + ], + ], + ]; + + if (Utils::$context['drafts_autosave']) { + $editorOptions['plugins'][] = 'drafts'; + $editorOptions['plugins'][] = 'pmDrafts'; + $editorOptions['options']['draftOptions'] = [ + 'sLastNote' => 'draft_lastautosave', + 'sLastID' => 'id_draft', + 'sQueryParams' => 'action=pm;sa=send2;xml', + 'iFreq' => empty(Config::$modSettings['drafts_autosave_frequency']) ? 60000 : Config::$modSettings['drafts_autosave_frequency'] * 1000, + ]; + } + + new Editor($editorOptions); Utils::$context['bcc_value'] = ''; @@ -2155,8 +2187,7 @@ public static function reportErrors(array $error_types, array $named_recipients, } } - // Create it... - new Editor([ + $editorOptions = [ 'id' => 'message', 'value' => Utils::$context['message'], 'width' => '90%', @@ -2164,8 +2195,41 @@ public static function reportErrors(array $error_types, array $named_recipients, 'labels' => [ 'post_button' => Lang::getTxt('send_message', file: 'General'), ], + // We do XML preview here. 'preview_type' => Editor::PREVIEW_XML, - ]); + // This is a required field. + 'required' => true, + // SCEditor plugins. + 'plugins' => [], + // SCEditor options. + 'options' => [ + 'previewOptions' => [ + 'sPreviewSectionContainerID' => 'preview_section', + 'sPreviewSubjectContainerID' => 'preview_subject', + 'sPreviewBodyContainerID' => 'preview_body', + 'sErrorsContainerID' => 'errors', + 'sErrorsSeriousContainerID' => 'error_serious', + 'sErrorsListContainerID' => 'error_list', + 'sCaptionContainerID' => 'caption_%ID%', + 'sTxtPreviewTitle' => Utils::escapeJavaScript(Lang::$txt['preview_title']), + 'sTxtPreviewFetch' => Utils::escapeJavaScript(Lang::$txt['preview_fetch']), + 'sUrl' => Config::$scripturl . '?action=pm;sa=send2;preview;xml', + ], + ], + ]; + + if (Utils::$context['drafts_autosave']) { + $editorOptions['plugins'][] = 'drafts'; + $editorOptions['plugins'][] = 'pmDrafts'; + $editorOptions['options']['draftOptions'] = [ + 'sLastNote' => 'draft_lastautosave', + 'sLastID' => 'id_draft', + 'sQueryParams' => 'action=pm;sa=send2;xml', + 'iFreq' => empty(Config::$modSettings['drafts_autosave_frequency']) ? 60000 : Config::$modSettings['drafts_autosave_frequency'] * 1000, + ]; + } + + new Editor($editorOptions); // Check whether we need to show the code again. Utils::$context['require_verification'] = !User::$me->is_admin && !empty(Config::$modSettings['pm_posts_verification']) && User::$me->posts < Config::$modSettings['pm_posts_verification']; diff --git a/Sources/Poll.php b/Sources/Poll.php index 9b4902251c..4f57861c18 100644 --- a/Sources/Poll.php +++ b/Sources/Poll.php @@ -1059,15 +1059,11 @@ protected function initNewPoll(): void 'guest_vote' => !empty($_POST['poll_guest_vote']), ]); - // Make all five poll choices empty. - Utils::$context['last_choice_id'] = 4; - - for ($i = 0; $i <= Utils::$context['last_choice_id']; $i++) { + for ($i = 0; $i <= 4; $i++) { $this->addChoice([ 'id' => $i, 'number' => $i + 1, 'label' => '', - 'is_last' => $i === Utils::$context['last_choice_id'], ], true); } } diff --git a/Sources/Profile.php b/Sources/Profile.php index 10478fda40..e83fa95482 100644 --- a/Sources/Profile.php +++ b/Sources/Profile.php @@ -1260,12 +1260,7 @@ public function loadAvatarData(): bool } // Get a list of all the server stored avatars. - if ($this->formatted['avatar']['allow_server_stored']) { - Utils::$context['avatar_list'] = []; - Utils::$context['avatars'] = is_dir(Config::$modSettings['avatar_directory']) ? $this->getAvatars('', 0) : []; - } else { - Utils::$context['avatars'] = []; - } + Utils::$context['avatars'] = $this->formatted['avatar']['allow_server_stored'] && is_dir(Config::$modSettings['avatar_directory']) ? $this->getAvatars('', 0) : []; // Second level selected avatar... Utils::$context['avatar_selected'] = substr((string) strrchr($this->formatted['avatar']['server_pic'], '/'), 1); @@ -1479,20 +1474,13 @@ public function setupContext(array $fields): void } } - // Some spicy JS. + Theme::addJavaScriptVar('require_password', !empty(Utils::$context['require_password']), true); + Theme::addJavaScriptVar('required_security_reasons', Lang::$txt['required_security_reasons'], true); + Theme::addJavaScriptVar('autodetect', Lang::$txt['timeoffset_autodetect'], true); + + // Backwards compatibility. Theme::addInlineJavaScript(' - var form_handle = document.forms.creator; - createEventListener(form_handle); - ' . (!empty(Utils::$context['require_password']) ? ' - form_handle.addEventListener("submit", function(event) - { - if (this.oldpasswrd.value == "") - { - event.preventDefault(); - alert(' . (Utils::escapeJavaScript(Lang::getTxt('required_security_reasons', file: 'Profile'))) . '); - return false; - } - }, false);' : ''), true); + var form_handle = document.forms.creator;', true); // Any onsubmit JavaScript? if (!empty(Utils::$context['profile_onsubmit_javascript'])) { @@ -2531,7 +2519,7 @@ protected function prepareToSaveOptions(): void * @param int $level How many levels we should go in the directory. * @return array An array of information about the files and directories found. */ - protected function getAvatars(string $directory, int $level = 0): array + protected function getAvatars(string $directory = '', int $level = 0): array { $result = []; @@ -2539,7 +2527,7 @@ protected function getAvatars(string $directory, int $level = 0): array $files = []; // Open the directory.. - $dir = dir(Config::$modSettings['avatar_directory'] . (!empty($directory) ? '/' : '') . $directory); + $dir = dir(Config::$modSettings['avatar_directory'] . '/' . $directory); if (!$dir) { return []; @@ -2571,10 +2559,12 @@ protected function getAvatars(string $directory, int $level = 0): array ]; } + $directory .= $directory != '' ? '/' : ''; + foreach ($dirs as $line) { - $tmp = $this->getAvatars($directory . (!empty($directory) ? '/' : '') . $line, $level + 1); + $tmp = $this->getAvatars($directory . $line, $level + 1); - if (!empty($tmp)) { + if ($tmp != []) { $result[] = [ 'filename' => Utils::htmlspecialchars($line), 'checked' => str_contains(Utils::$context['member']['avatar']['server_pic'], $line . '/'), @@ -2588,32 +2578,20 @@ protected function getAvatars(string $directory, int $level = 0): array } foreach ($files as $line) { - $filename = substr($line, 0, (\strlen($line) - \strlen(strrchr($line, '.')))); - $extension = substr(strrchr($line, '.'), 1); + $found = preg_match('/([^.]+)\.(jpe?g|png|gif|bmp)$/i', $line, $match); // Make sure it is an image. // @todo Change this to use MIME type. - if ( - strcasecmp($extension, 'gif') != 0 - && strcasecmp($extension, 'jpg') != 0 - && strcasecmp($extension, 'jpeg') != 0 - && strcasecmp($extension, 'png') != 0 - && strcasecmp($extension, 'bmp') != 0 - && strcasecmp($extension, 'webp') != 0 - ) { + if ($found !== 1) { continue; } $result[] = [ 'filename' => Utils::htmlspecialchars($line), - 'checked' => $line == Utils::$context['member']['avatar']['server_pic'], - 'name' => Utils::htmlspecialchars(str_replace('_', ' ', $filename)), + 'checked' => $directory . $line == Utils::$context['member']['avatar']['server_pic'], + 'name' => Utils::htmlspecialchars(str_replace('_', ' ', $match[1])), 'is_dir' => false, ]; - - if ($level == 1) { - Utils::$context['avatar_list'][] = $directory . '/' . $line; - } } return $result; @@ -2657,8 +2635,6 @@ protected function setAvatarServerStored(string $filename): void if ( // Named 'blank.png' $this->new_data['avatar'] == 'blank.png' - // Not inside the expected directory. - || !str_starts_with($avatar_path, Config::$modSettings['avatar_directory'] . '/') // Not a file. || !is_file($avatar_path) // Not a valid image file. diff --git a/Sources/Theme.php b/Sources/Theme.php index 219b5c707a..831bb33434 100644 --- a/Sources/Theme.php +++ b/Sources/Theme.php @@ -2139,6 +2139,30 @@ protected function loadTemplatesAndLangFiles(): void */ protected function loadCss(): void { + // Load FontAwesome + $FontAwesomeUrls = [ + 'cdn' => 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/' . FONTAWESOME_VERSION . '/css/all.min.css', + 'fontawesome_cdn' => 'https://use.fontawesome.com/releases/v' . FONTAWESOME_VERSION . '/css/all.css', + ]; + + if (isset(Config::$modSettings['fontawesome_source']) && array_key_exists(Config::$modSettings['fontawesome_source'], $FontAwesomeUrls)) { + self::loadCSSFile($FontAwesomeUrls[Config::$modSettings['fontawesome_source']], ['external' => true, 'order_pos' => -100], 'smf_fontawesome'); + } elseif (isset(Config::$modSettings['fontawesome_source']) && Config::$modSettings['fontawesome_source'] == 'local') { + self::loadCSSFile('fontawesome.min.css', ['default_theme' => true, 'minimize' => true, 'order_pos' => -100], 'smf_fontawesome'); + } elseif (isset(Config::$modSettings['fontawesome_source'], Config::$modSettings['fontawesome_custom']) && Config::$modSettings['fontawesome_source'] == 'custom') { + self::loadCSSFile(Config::$modSettings['fontawesome_custom'], ['external' => true, 'order_pos' => -100], 'smf_fontawesome'); + } + // Fall back to the fa forum default + else { + self::loadCSSFile('https://use.fontawesome.com/releases/v' . FONTAWESOME_VERSION . '/css/all.css', ['external' => true, 'order_pos' => -100], 'smf_fontawesome'); + } + + // Icons + self::loadCSSFile('icons.css', ['minimize' => true, 'order_pos' => -200], 'smf_icons'); + + // Variables + self::loadCSSFile('variables.css', ['minimize' => true, 'order_pos' => -2], 'smf_variables'); + // And of course, let's load the default CSS file. self::loadCSSFile('index.css', ['minimize' => true, 'order_pos' => 1], 'smf_index'); @@ -2156,6 +2180,47 @@ protected function loadCss(): void self::loadCSSFile('noscript.css', ['minimize' => true, 'order_pos' => 1, 'noscript' => true], 'smf_noscript'); } + /** + * Loads the theme mode, if applicable. + */ + protected function loadMode(): void + { + Utils::$context['theme_colormode'] = ''; + + if (!empty($this->settings['has_dark_mode'])) { + // Theme Modes + $this->settings['theme_colormodes'] = ['light', 'system', 'dark']; + + // Overriding - for previews and that ilk. + if (!empty($_REQUEST['mode'])) { + $_SESSION['theme_colormode'] = $_REQUEST['mode']; + + // If the user is logged, save this to their profile + if (User::$me->is_logged && in_array($_SESSION['theme_colormode'], $this->settings['theme_colormodes'])) { + Db::$db->insert( + 'replace', + '{db_prefix}themes', + ['id_theme' => 'int', 'id_member' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'], + [self::$current->settings['theme_id'], User::$me->id, 'theme_colormode', $_SESSION['theme_colormode']], + ['id_theme', 'id_member', 'variable'], + ); + } + } + + // User selection? + if (empty($this->settings['disable_user_mode']) || User::$me->allowedTo('admin_forum')) { + Utils::$context['theme_colormode'] = !empty($_SESSION['theme_colormode']) && in_array($_SESSION['theme_colormode'], $this->settings['theme_colormodes']) ? $_SESSION['theme_colormode'] : (!empty($this->options['theme_colormode']) && in_array($this->options['theme_colormode'], $this->settings['theme_colormodes']) ? $this->options['theme_colormode'] : ''); + } + + // If no color mode, set a default + if (empty(Utils::$context['theme_colormode']) || !in_array(Utils::$context['theme_colormode'], $this->settings['theme_colormodes'])) { + Utils::$context['theme_colormode'] = !empty($this->settings['default_colormode']) && in_array($this->settings['default_colormode'], $this->settings['theme_colormodes']) ? $this->settings['default_colormode'] : $this->settings['theme_colormodes'][0]; + } + + self::loadCSSFile('dark.css', ['order_pos' => 2, 'attributes' => (Utils::$context['theme_colormode'] == 'system' ? ['media' => '(prefers-color-scheme: dark)'] : [])], 'smf_dark'); + } + } + /** * Loads the correct theme variant, if applicable. */ @@ -2166,11 +2231,34 @@ protected function loadVariant(): void Utils::$context['theme_variant_url'] = ''; if (!empty($this->settings['theme_variants'])) { + // Add the default variant + $this->settings['theme_variants'] = array_unique(array_merge(['default'], $this->settings['theme_variants'])); + // Overriding - for previews and that ilk. if (!empty($_REQUEST['variant'])) { $_SESSION['id_variant'] = $_REQUEST['variant']; + + // If the user is logged, save this to their profile + if (User::$me->is_logged && in_array($_SESSION['id_variant'], $this->settings['theme_variants'])) { + Db::$db->insert( + 'replace', + '{db_prefix}themes', + ['id_theme' => 'int', 'id_member' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'], + [self::$current->settings['theme_id'], User::$me->id, 'theme_variant', $_SESSION['id_variant']], + ['id_theme', 'id_member', 'variable'], + ); + } } + /** + * Attempt to load a variants file for variable overriding + * using data attribute (:root[data-variant="variant"]) + * + * This is useful when you only want a single file for + * recoloring the variants. + */ + self::loadCSSFile('variants.css', ['order_pos' => 0], 'smf_variants'); + // User selection? if (empty($this->settings['disable_user_variant']) || User::$me->allowedTo('admin_forum')) { Utils::$context['theme_variant'] = !empty($_SESSION['id_variant']) && \in_array($_SESSION['id_variant'], $this->settings['theme_variants']) ? $_SESSION['id_variant'] : (!empty($this->options['theme_variant']) && \in_array($this->options['theme_variant'], $this->settings['theme_variants']) ? $this->options['theme_variant'] : ''); @@ -2181,16 +2269,8 @@ protected function loadVariant(): void Utils::$context['theme_variant'] = !empty($this->settings['default_variant']) && \in_array($this->settings['default_variant'], $this->settings['theme_variants']) ? $this->settings['default_variant'] : $this->settings['theme_variants'][0]; } - // Do this to keep things easier in the templates. - Utils::$context['theme_variant'] = '_' . Utils::$context['theme_variant']; - Utils::$context['theme_variant_url'] = Utils::$context['theme_variant'] . '/'; - - if (!empty(Utils::$context['theme_variant'])) { - self::loadCSSFile('index' . Utils::$context['theme_variant'] . '.css', ['order_pos' => 300], 'smf_index' . Utils::$context['theme_variant']); - - if (Utils::$context['right_to_left']) { - self::loadCSSFile('rtl' . Utils::$context['theme_variant'] . '.css', ['order_pos' => 4200], 'smf_rtl' . Utils::$context['theme_variant']); - } + if (!empty(Utils::$context['theme_variant']) && Utils::$context['theme_variant'] !== 'default') { + self::loadCSSFile('index_' . Utils::$context['theme_variant'] . '.css', ['order_pos' => 2], 'smf_index' . Utils::$context['theme_variant']); } } } @@ -2203,6 +2283,7 @@ protected function loadJavaScript(): void // Default JS variables for use in every theme Utils::$context['javascript_vars'] = [ 'smf_theme_url' => '"' . $this->settings['theme_url'] . '"', + 'smf_theme_id' => self::$current->settings['theme_id'], 'smf_default_theme_url' => '"' . $this->settings['default_theme_url'] . '"', 'smf_images_url' => '"' . $this->settings['images_url'] . '"', 'smf_smileys_url' => '"' . Config::$modSettings['smileys_url'] . '"', @@ -2245,16 +2326,11 @@ protected function loadJavaScript(): void } // Queue our JQuery plugins! - self::loadJavaScriptFile('smf_jquery_plugins.js', ['minimize' => true], 'smf_jquery_plugins'); - - if (!User::$me->is_guest) { - self::loadJavaScriptFile('jquery.custom-scrollbar.js', ['minimize' => true], 'smf_jquery_scrollbar'); - self::loadCSSFile('jquery.custom-scrollbar.css', ['force_current' => false, 'validate' => true], 'smf_scrollbar'); - } + self::loadJavaScriptFile('smf_jquery_plugins.js', ['defer' => true, 'minimize' => true], 'smf_jquery_plugins'); // script.js and theme.js, always required, so always add them! Makes index.template.php cleaner and all. self::loadJavaScriptFile('script.js', ['defer' => false, 'minimize' => true], 'smf_script'); - self::loadJavaScriptFile('theme.js', ['minimize' => true], 'smf_theme'); + self::loadJavaScriptFile('theme.js', ['defer' => true, 'minimize' => true], 'smf_theme'); // And we should probably trigger the cron too. if (empty(Config::$modSettings['cron_is_real_cron'])) { diff --git a/Themes/default/Admin.template.php b/Themes/default/Admin.template.php index dd76d419e2..d037ef49a4 100644 --- a/Themes/default/Admin.template.php +++ b/Themes/default/Admin.template.php @@ -73,34 +73,40 @@ function template_admin() - '; + +
'; foreach (Utils::$context[Utils::$context['admin_menu_name']]['sections'] as $area_id => $area) { echo ' -
- ', $area['title'], ''; + ', $area['title'], ' +
'; foreach ($area['areas'] as $item_id => $item) { // No point showing the 'home' page here, we're already on it! - if ($area_id == 'forum' && $item_id == 'index') + if ($area_id == 'forum' && $item_id == 'index') { continue; + } $url = isset($item['url']) ? $item['url'] : Config::$scripturl . '?action=admin;area=' . $item_id . (!empty(Utils::$context[Utils::$context['admin_menu_name']]['extra_parameters']) ? Utils::$context[Utils::$context['admin_menu_name']]['extra_parameters'] : ''); - if (!empty($item['icon_file'])) - echo ' - ', $item['label'], ''; - else - echo ' - ', $item['label'], ''; + echo ' + + + ', !empty($item['icon_file']) ? '' : '', ' + + ', $item['label'], ' + '; } echo ' -
'; +
'; } + echo ' + '; + // The below functions include all the scripts needed from the simplemachines.org site. The language and format are passed for internationalization. if (empty(Config::$modSettings['disable_smf_js'])) echo ' @@ -111,7 +117,6 @@ function template_admin() echo ' '; } @@ -767,9 +758,6 @@ function template_show_settings() if (!empty(Utils::$context['settings_insert_above'])) echo Utils::$context['settings_insert_above']; - echo ' -
'; - // Is there a custom title? if (isset(Utils::$context['settings_title'])) echo ' @@ -822,54 +810,37 @@ function ($v) } ); - // Now actually loop through all the variables. - $is_open = false; + echo ' + '; + foreach (Utils::$context['config_vars'] as $config_var) { // Is it a title or a description? if (is_array($config_var) && ($config_var['type'] == 'title' || $config_var['type'] == 'desc')) { - // Not a list yet? - if ($is_open) - { - $is_open = false; - echo ' - - '; - } - // A title? if ($config_var['type'] == 'title') { echo ' -
-

+
+

', ($config_var['help'] ? '' : ''), ' ', $config_var['label'], ' -

+

'; } // A description? else { echo ' -
+

', $config_var['label'], ' -

'; +

'; } continue; } - // Not a list yet? - if (!$is_open) - { - $is_open = true; - echo ' -
-
'; - } - // Hang about? Are you pulling my leg - a callback?! if (is_array($config_var) && $config_var['type'] == 'callback') { @@ -885,15 +856,15 @@ function ($v) if (in_array($config_var['type'], array('message', 'warning'))) { echo ' - + ', $config_var['label'], ' - '; +
'; } // Otherwise it's an input box of some kind. else { echo ' - '; + '; // Some quick helpers... $javascript = $config_var['javascript']; @@ -911,8 +882,8 @@ function ($v) echo ' ', $config_var['label'], '', $subtext, ($config_var['type'] == 'password' ? '
' . Lang::getTxt('admin_confirm_password', file: 'Admin') . '' : ''), ' - - ', + + ', $config_var['preinput']; // Show a check box. @@ -1037,7 +1008,7 @@ function ($v) echo isset($config_var['postinput']) ? ' ' . $config_var['postinput'] : '', ' - '; + '; } } else @@ -1045,30 +1016,19 @@ function ($v) // Just show a separator. if ($config_var == '') echo ' - -
-
'; +
'; else echo ' -
+

' . $config_var . ' -

-
'; +

'; } } - if ($is_open) - echo ' -
'; - if (empty(Utils::$context['settings_save_dont_show'])) echo ' '; - if ($is_open) - echo ' - '; - // At least one token has to be used! if (isset(Utils::$context['admin-ssc_token'])) echo ' @@ -1376,12 +1336,12 @@ function template_admin_search_results() { echo '
- ', template_admin_quick_search(), '

', Lang::getTxt('admin_search_results_desc', Utils::$context, file: 'Admin'), '

+ ', template_admin_quick_search(), '
'; @@ -1561,21 +1521,7 @@ function template_repair_boards() { echo ' '; } } diff --git a/Themes/default/BoardIndex.template.php b/Themes/default/BoardIndex.template.php index 78b9d74850..d5566d1d11 100644 --- a/Themes/default/BoardIndex.template.php +++ b/Themes/default/BoardIndex.template.php @@ -17,14 +17,6 @@ use SMF\Utils; use SMF\User; -/** - * The top part of the outer layer of the boardindex - */ -function template_boardindex_outer_above() -{ - template_newsfader(); -} - /** * This shows the newsfader */ @@ -34,14 +26,17 @@ function template_newsfader() if (!empty(Theme::$current->settings['show_newsfader']) && !empty(Utils::$context['news_lines'])) { echo ' -
    '; +
    +
    +
      '; foreach (Utils::$context['news_lines'] as $news) echo ' -
    • ', $news, '
    • '; +
    • ', $news, '
    • '; echo ' -
    +
+
'; + + + '; } /** @@ -1007,94 +936,32 @@ function in_array(variable, theArray) */ function template_hms() { - $alt = false; - echo ' - - - - '; +
+

Binary Clock

+
+
+
    '; foreach (Utils::$context['clockicons'] as $t => $v) { echo ' -
- - '; - - $alt = !$alt; + '; } - echo ' - - - -
Binary Clock
'; +
    '; foreach ($v as $i) echo ' - '; +
  • '; echo ' -
- Too tough for you? -
'; echo ' - '; + + + '; } /** @@ -1102,92 +969,29 @@ function in_array(variable, theArray) */ function template_omfg() { - $alt = false; - echo ' - - - - '; +
+

OMFG Binary Clock

+
+
+
    '; foreach (Utils::$context['clockicons'] as $t => $v) { echo ' -
- - '; - - $alt = !$alt; - } - - echo ' -
OMFG Binary Clock
'; +
    '; foreach ($v as $i) echo ' - '; +
  • '; echo ' -
- '; + + '; } /** @@ -1195,31 +999,27 @@ function in_array(variable, theArray) */ function template_thetime() { - $alt = false; - echo ' - - - - '; +
+

OMFG Binary Clock

+
+
+
    '; - foreach (Utils::$context['clockicons'] as $v) + foreach (Utils::$context['clockicons'] as $t => $v) { echo ' -
- - '; - - $alt = !$alt; + '; } echo ' -
The time you requested
'; +
    '; foreach ($v as $i) echo ' - '; + '; echo ' -
'; + + '; } diff --git a/Themes/default/Display.template.php b/Themes/default/Display.template.php index 3f4589c07b..d4673f7db0 100644 --- a/Themes/default/Display.template.php +++ b/Themes/default/Display.template.php @@ -38,7 +38,7 @@ function template_main() // Show new topic info here? echo ' -
+

', Utils::$context['subject'], '', (Utils::$context['is_locked']) ? ' ' : '', (Utils::$context['is_sticky']) ? ' ' : '', '

@@ -46,7 +46,7 @@ function template_main() // Next - Prev echo ' - ', Utils::$context['previous_next'], ''; + ', Utils::$context['previous_next'], ''; if (!empty(Theme::$current->settings['display_who_viewing'])) { @@ -188,12 +188,12 @@ function template_main() // Show the page index... "Pages: [1]". echo '
- ', template_button_strip(Utils::$context['normal_buttons'], 'right'), ' - ', Utils::$context['menu_separator'], ' '; +
+ ', Utils::$context['menu_separator'], ' + ', template_button_strip(Utils::$context['normal_buttons'], 'right'), ''; // Mobile action - moderation buttons (top) if (!empty(Utils::$context['normal_buttons'])) @@ -208,8 +208,8 @@ function template_main() // Show the topic information - icon, subject, etc. echo ' -
- '; + +
'; Utils::$context['ignoredMsgs'] = array(); Utils::$context['removableMessageIDs'] = array(); @@ -219,18 +219,18 @@ function template_main() template_single_post($message); echo ' - -
'; +
+ '; // Show the page index... "Pages: [1]". echo '
- ', template_button_strip(Utils::$context['normal_buttons'], 'right'), ' - ', Utils::$context['menu_separator'], ' '; +
+ ', Utils::$context['menu_separator'], ' + ', template_button_strip(Utils::$context['normal_buttons'], 'right'), ''; // Mobile action - moderation buttons (bottom) if (!empty(Utils::$context['normal_buttons'])) @@ -288,89 +288,71 @@ function template_main()
'; echo ' - '; } @@ -425,6 +407,8 @@ function template_main() function template_single_post($message) { $ignoring = false; + $show_subject = !empty(Config::$modSettings['subject_toggle']); + $is_first_post = $message['id'] == Utils::$context['first_message']; if ($message['can_remove']) Utils::$context['removableMessageIDs'][] = $message['id']; @@ -450,31 +434,75 @@ function template_single_post($message) // Show the message. echo ' -
-
'; +
+
+ ', !$is_first_post ? ' + ' . ($message['first_new'] ? '' : '') : ''; + + echo ' +
'; + + echo ' + <', $show_subject ? 'p' : 'h3', ' class="page_number" id="msg_num_', $message['id'], '">', $is_first_post ? Lang::$txt['first_post'] : (!empty($message['counter']) ? Lang::getTxt('reply_number_sr', [$message['counter']]) : ''), ''; + + // Some people don't want subject... The div is still required or quick edit breaks. + echo ' +
+ <', $show_subject ? 'h3' : 'p', '>', $message['subject'], ' +
+
'; + + echo ' + '; + + // Show "<< Last Edit: Time by Person >>" if this post was edited. But we need the div even if it wasn't modified! + // Because we insert into it through AJAX and we don't want to stop themers moving it around if they so wish so they can put it where they want it. + echo ' +

'; + + if (!empty(Config::$modSettings['show_modify']) && !empty($message['modified']['name'])) + echo + $message['modified']['last_edit_text']; + + echo ' +

+ '; + + if (!$message['approved'] && $message['member']['id'] != 0 && $message['member']['id'] == User::$me->id) + echo ' +

+ ', Lang::$txt['post_awaiting_approval'], ' +

'; + + echo ' +
'; // Show information about the poster of this message. echo ' -
'; +
-
-
'; - - // Some people don't want subject... The div is still required or quick edit breaks. - echo ' -
- ', $message['link'], ' -
'; - - echo ' - ', !empty($message['counter']) ? '#' . $message['counter'] . '' : '', ' - - -
'; + +
'; // Ignoring this user? Hide the post. if ($ignoring) echo ' -
- ', Lang::getTxt('ignoring_user', file: 'General'), ' - -
'; + '; // Show the post itself, finally! echo '
'; - if (!$message['approved'] && $message['member']['id'] != 0 && $message['member']['id'] == User::$me->id) - echo ' -
- ', Lang::getTxt('post_awaiting_approval', file: 'General'), ' -
'; echo ' +
+
'; // Are there any custom profile fields for above the signature? @@ -911,8 +869,8 @@ function template_single_post($message) echo '
-
-
+ +
'; } @@ -969,7 +927,7 @@ function template_quickreply() ', Lang::getTxt('name', file: 'General'), '
- +
'; if (empty(Config::$modSettings['guest_post_no_email'])) @@ -979,7 +937,7 @@ function template_quickreply() ', Lang::getTxt('email', file: 'General'), '
- +
'; } @@ -987,17 +945,7 @@ function template_quickreply() '; } - echo ' - ', template_control_richedit(Utils::$context['post_box_name'], 'smileyBox_message', 'bbcBox_message'), ' - '; + template_control_richedit('quickReply', 'smileyBox_message', 'bbcBox_message'); // Is visual verification enabled? if (Utils::$context['require_verification']) @@ -1010,7 +958,7 @@ function insertQuoteFast(messageid) // Finally, the submit buttons. echo ' - ', template_control_richedit_buttons(Utils::$context['post_box_name']), ' + ', template_control_richedit_buttons('quickReply'), ' '; echo ' @@ -1019,44 +967,9 @@ function insertQuoteFast(messageid)

'; - // Draft autosave available and the user has it enabled? - if (!empty(Utils::$context['drafts_autosave'])) - echo ' - '; - if (Utils::$context['show_spellchecking']) echo '
'; - - echo ' - '; } diff --git a/Themes/default/Errors.template.php b/Themes/default/Errors.template.php index ccac97dfb9..88ded706e4 100644 --- a/Themes/default/Errors.template.php +++ b/Themes/default/Errors.template.php @@ -15,7 +15,6 @@ use SMF\Theme; use SMF\Utils; -// @todo /* This template file contains only the sub template fatal_error. It is shown when an error occurs, and should show at least a back button and Utils::$context['error_message']. @@ -26,12 +25,12 @@ */ function template_fatal_error() { - if (!empty(Utils::$context['simple_action'])) + if (!empty(Utils::$context['simple_action']) || !empty(Utils::$context['from_ajax'])) echo ' - +

', Utils::$context['error_title'], ' -
-
+

+
', Utils::$context['error_message'], '
'; else @@ -43,10 +42,8 @@ function template_fatal_error() ', Utils::$context['error_title'], '
-
-
- ', Utils::$context['error_message'], ' -
+
+ ', Utils::$context['error_message'], '
'; @@ -110,10 +107,12 @@ function template_error_log() -
- - -
+
    +
  • + + +
  • +
'; // We have some errors, must be some mods installed :P @@ -121,19 +120,24 @@ function template_error_log() { echo '
-
', $error['id'], '
- - -
- ', $error['time'], ' + + + ', $error['time'], ' +
-
+
+ #', $error['id'], ' + +
-
-
- +
+
+ ', Lang::$txt['backtrace_title'], ' +
+
+ ', $error['member']['link'], ''; if (!empty($error['member']['ip'])) @@ -159,19 +163,15 @@ function template_error_log() echo '
-
- - ', Lang::getTxt('backtrace_title', file: 'ManageMaintenance'), ' - + +
+
+ ', Lang::getTxt('error_type_name', ['type' => $error['error_type']['type'] === 'critical' ? '' . $error['error_type']['name'] . '' : $error['error_type']['name']]), '
+ + ', $error['message']['html'], '
-
-
- ', Lang::getTxt('error_type_name', ['type' => $error['error_type']['type'] === 'critical' ? '' . $error['error_type']['name'] . '' : $error['error_type']['name']], file: 'ManageMaintenance'), '
- - ', $error['message']['html'], ' -
'; } diff --git a/Themes/default/EventEditor.template.php b/Themes/default/EventEditor.template.php index ed350caf6f..4c4d3fdaa0 100644 --- a/Themes/default/EventEditor.template.php +++ b/Themes/default/EventEditor.template.php @@ -226,7 +226,7 @@ function template_event_new()
- +
'; @@ -717,9 +717,9 @@ function template_linked_events() } } - if (!empty($event->location)) { + if ($event['location'] != '') { echo ' -
', $event->location; +
', nl2br($event['location']); } $rrule_description = $event->getParentEvent()->recurrence_iterator->getRRule()->getDescription($event); diff --git a/Themes/default/GenericControls.template.php b/Themes/default/GenericControls.template.php index 2cbab501d2..66d2e3ceb9 100644 --- a/Themes/default/GenericControls.template.php +++ b/Themes/default/GenericControls.template.php @@ -18,69 +18,47 @@ use SMF\Verifier; /** - * This function displays all the stuff you get with a richedit box - BBC, smileys, etc. + * Renders a rich-text editor, including BBC buttons and smileys if enabled. * - * @param string $editor_id The editor ID - * @param null|bool $smileyContainer If null, hides the smiley section regardless of settings - * @param null|bool $bbcContainer If null, hides the bbcode buttons regardless of settings + * This function sets up a textarea as a rich-text editor using SCEditor. + * The `$smiley_container` and `$bbc_container` parameters control the visibility + * and source of smiley and BBC containers, respectively. + * + * @param string $editor_id The unique ID of the editor. + * @param null|bool|string $smiley_container Controls the smiley container: + * - `null`: Hides the smiley section. + * - `true`: Generates the container dynamically using JavaScript. + * - `string`: Specifies the HTML element ID for the smiley container. + * @param null|bool|string $bbc_container Controls the BBC container: + * - `null`: Hides the BBC buttons. + * - `true`: Generates the container dynamically using JavaScript. + * - `string`: Specifies the HTML element ID for the BBC container. */ -function template_control_richedit($editor_id, $smileyContainer = null, $bbcContainer = null) +function template_control_richedit(string $editor_id, null|bool|string $smiley_container = null, null|bool|string $bbc_container = null): void { $editor_context = Editor::$loaded[$editor_id]; - if ($smileyContainer === null) - $editor_context['sce_options']['emoticonsEnabled'] = false; - - if ($bbcContainer === null) - $editor_context['sce_options']['toolbar'] = ''; - echo ' - -
- + '; } /** - * This template shows the form buttons at the bottom of the editor + * Renders the buttons section below a rich-text editor. * - * @param string $editor_id The editor ID + * Displays form buttons such as "Post" or "Preview" and integrates + * functionality like auto-saving drafts if enabled. + * + * @param string $editor_id The unique ID of the editor for which buttons are displayed. + * + * @return void */ -function template_control_richedit_buttons($editor_id) +function template_control_richedit_buttons(string $editor_id): void { $editor_context = Editor::$loaded[$editor_id]; @@ -90,80 +68,56 @@ function template_control_richedit_buttons($editor_id) '; - $tempTab = Utils::$context['tabindex']; - - if (!empty(Utils::$context['drafts_save'])) - $tempTab++; - elseif ($editor_context['preview_type']) - $tempTab++; - elseif (Utils::$context['show_spellchecking']) - $tempTab++; - - $tempTab++; - Utils::$context['tabindex'] = $tempTab; - foreach (Utils::$context['richedit_buttons'] as $name => $button) { - if ($name == 'spell_check') { - $button['onclick'] = 'oEditorHandle_' . $editor_id . '.spellCheckStart();'; - } - if ($name == 'preview') { - $button['value'] = isset($editor_context['labels']['preview_button']) ? $editor_context['labels']['preview_button'] : $button['value']; - $button['onclick'] = $editor_context['preview_type'] == Editor::PREVIEW_XML ? '' : 'return submitThisOnce(this);'; + $button['value'] = $editor_context['labels']['preview_button'] ?? $button['value']; $button['show'] = $editor_context['preview_type']; } if ($button['show']) { echo ' - '; + '; } } echo ' - + '; - // Start an instance of the auto saver if it's enabled + // Include auto-save feature if drafts are enabled. if (!empty(Utils::$context['drafts_save']) && !empty(Utils::$context['drafts_autosave'])) echo ' - - - '; + + '; } /** - * This template displays a verification form + * Displays a verification form with CAPTCHA or question-based challenges. + * + * Used to validate user input for forms, supporting various verification + * mechanisms such as CAPTCHA images, reCAPTCHA, and custom questions. + * + * @param int|string $verify_id The unique identifier for the verification control. + * @param string $display_type Determines how to display items: + * - `'single'`: Displays one item at a time (e.g., in a loop). + * - `'all'`: Displays all items together. + * @param bool $reset Whether to reset the internal tracking counter. * - * @param int|string $verify_id The verification control ID - * @param string $display_type What type to display. Can be 'single' to only show one verification option or 'all' to show all of them - * @param bool $reset Whether to reset the internal tracking counter - * @return bool False if there's nothing else to show, true if $display_type is 'single', nothing otherwise + * @return bool Returns `false` if no items are left to display, `true` if displaying a single item, and `void` otherwise. */ -function template_control_verification($verify_id, $display_type = 'all', $reset = false) +function template_control_verification(int|string $verify_id, string $display_type = 'all', bool $reset = false): bool { $verify_context = Verifier::$loaded[$verify_id]; - // Keep track of where we are. + // Reset tracking if necessary. if (empty($verify_context->tracking) || $reset) $verify_context->tracking = 0; - // How many items are there to display in total. $total_items = count($verify_context->questions) + ($verify_context->show_visual || $verify_context->can_recaptcha ? 1 : 0); - // If we've gone too far, stop. + // Stop if all items are processed. if ($verify_context->tracking > $total_items) return false; @@ -207,7 +161,7 @@ function template_control_verification($verify_id, $display_type = 'all', $reset
', Lang::getTxt('visual_verification_sound', file: 'General'), ' / ', Lang::getTxt('visual_verification_request_new', file: 'General'), '', $display_type != 'quick_reply' ? '
' : '', '
', Lang::getTxt('visual_verification_description', file: 'General'), $display_type != 'quick_reply' ? '
' : '', ' - +
'; } @@ -229,7 +183,7 @@ function template_control_verification($verify_id, $display_type = 'all', $reset echo '
', $verify_context->questions[$qIndex]['q'], ':
- questions[$qIndex]['is_error'] ? 'style="border: 1px red solid;"' : '', ' tabindex="', Utils::$context['tabindex']++, '" required> + questions[$qIndex]['is_error'] ? 'style="border: 1px red solid;"' : '', ' required>
'; } @@ -249,3 +203,98 @@ function template_control_verification($verify_id, $display_type = 'all', $reset if ($display_type == 'single') return true; } + +/** + * Renders a UI for choosing boards within categories. + * + * This function outputs a set of fieldsets representing categories, + * each containing a nested list of boards. Boards can be selected using checkboxes. + * The function also handles the display of child boards in a hierarchical structure + * and adds JavaScript functionality to enable selecting or deselecting all boards within a category. + * + * @param array $categories An array of categories, each containing: + * - 'name': The name of the category. + * - 'boards': An array of boards with: + * - 'id': The unique identifier for the board. + * - 'name': The display name of the board. + * - 'selected': Whether the board is selected (boolean). + * - 'child_level': The hierarchical level of the board (integer). + */ +function template_choose_boards(array $categories): void +{ + foreach ($categories as $category) + { + echo ' +
+ + ', $category['name'], ' + +
    '; + + for ($i = 0, $n = count($category['boards']); $i < $n; $i++) + { + echo ' +
  • + '; + + // Nest child boards inside another list. + $curr_child_level = $category['boards'][$i]['child_level']; + $next_child_level = $category['boards'][$i + 1]['child_level'] ?? 0; + + if ($next_child_level > $curr_child_level) + { + echo ' +
      '; + } + else + { + // Close child board lists until we reach a common level + // with the next board. + while ($next_child_level < $curr_child_level--) + { + echo ' + +
    '; + } + + echo '
  • '; + } + } + + echo ' +
+
'; + } + + echo ' + '; +} diff --git a/Themes/default/GenericMenu.template.php b/Themes/default/GenericMenu.template.php index 1b157e9777..f8d8a75db8 100644 --- a/Themes/default/GenericMenu.template.php +++ b/Themes/default/GenericMenu.template.php @@ -28,21 +28,21 @@ function template_generic_menu_dropdown_above() // Load the menu // Add mobile menu as well echo ' - - - ', Lang::getTxt('mobile_generic_menu', ['label' => $menu_label], file: 'General'), ' - -
+
+ - '; + '; if (!empty(Utils::$context['can_register'])) echo '
-
+

', Lang::getTxt('register_prompt', ['scripturl' => Config::$scripturl], file: 'General'), ' -

'; - - // It is a long story as to why we have this when we're clearly not going to use it. - if (!empty(Utils::$context['from_ajax'])) - echo ' -
- '; +

'; echo ' -
+ '; + + if (empty(Utils::$context['from_ajax'])) + echo '
'; } @@ -174,14 +125,17 @@ function template_login() */ function template_login_tfa() { - echo ' + if (empty(Utils::$context['from_ajax'])) + echo ' '; + + if (empty(Utils::$context['from_ajax'])) + echo '
'; } @@ -286,16 +208,17 @@ function template_kick_guest() { // This isn't that much... just like normal login but with a message at the top. echo ' -
- -
'; +

+
+

+ ', Lang::$txt['login'], ' +

+
+
+ +
+ +
+ +
+ +
+ +

+ ', Lang::$txt['forgot_your_password'], ' +

+
+ + + '; } /** @@ -349,38 +266,37 @@ function template_maintenance() { // Display the administrator's message at the top. echo ' -
-
'; } @@ -363,32 +311,6 @@ function template_maintain_topics() ', Lang::getTxt('maintain_done', ['task' => Utils::$context['maintenance_finished']], file: 'Admin'), ' '; - // Bit of javascript for showing which boards to prune in an otherwise hidden list. - echo ' - '; - echo '
@@ -411,45 +333,14 @@ function swapRot()


-

- + ', Lang::getTxt('maintain_old_all', file: 'ManageMaintenance'), ' -

- - + + @@ -564,12 +455,12 @@ function template_convert_entities() echo '
-

', Lang::getTxt('entity_convert_title', file: 'ManageMaintenance'), '

+

', Lang::$txt['maintain_convertentities_title'], '

-

', Lang::getTxt('entity_convert_introduction', file: 'ManageMaintenance'), '

+

', Lang::$txt['maintain_convertentities_introduction'], '

- +
'; diff --git a/Themes/default/ManageMembergroups.template.php b/Themes/default/ManageMembergroups.template.php index c59fe33eb8..6d7e1828f9 100644 --- a/Themes/default/ManageMembergroups.template.php +++ b/Themes/default/ManageMembergroups.template.php @@ -365,7 +365,6 @@ function template_edit_group() '; + '; } /** diff --git a/Themes/default/ManagePaid.template.php b/Themes/default/ManagePaid.template.php index ab446837a7..ffa3beb330 100644 --- a/Themes/default/ManagePaid.template.php +++ b/Themes/default/ManagePaid.template.php @@ -211,12 +211,6 @@ function template_delete_subscription() */ function template_modify_user_subscription() { - // Some quickly stolen javascript from Post, could do with being more efficient :) - echo ' - '; - echo '
@@ -258,16 +252,16 @@ function template_modify_user_subscription()
', Lang::getTxt('start_date_and_time', file: 'ManagePaid'), ' - '; // Show a list of all the years we allow... - for ($year = 2005; $year <= 2030; $year++) + for ($cur_year = idate('Y'), $year = min($cur_year - 10, Utils::$context['sub']['start']['year']); $year <= $cur_year + 10; $year++) echo ' '; echo ' - '; // There are 12 months per year - ensure that they all get listed. for ($month = 1; $month <= 12; $month++) @@ -292,16 +286,16 @@ function template_modify_user_subscription()
', Lang::getTxt('end_date_and_time', file: 'ManagePaid'), ' - '; // Show a list of all the years we allow... - for ($year = 2005; $year <= 2030; $year++) + for ($cur_year = idate('Y'), $year = min($cur_year - 10, Utils::$context['sub']['end']['year']); $year <= $cur_year + 10; $year++) echo ' '; echo ' - '; // There are 12 months per year - ensure that they all get listed. for ($month = 1; $month <= 12; $month++) @@ -330,15 +324,17 @@ function template_modify_user_subscription() '; if (!empty(Utils::$context['pending_payments'])) diff --git a/Themes/default/ManagePermissions.template.php b/Themes/default/ManagePermissions.template.php index 8eff419d04..f6b076a39c 100644 --- a/Themes/default/ManagePermissions.template.php +++ b/Themes/default/ManagePermissions.template.php @@ -116,9 +116,9 @@ function template_permission_index() echo '
diff --git a/Themes/default/ManageSearch.template.php b/Themes/default/ManageSearch.template.php index 32cc926ae0..1690169416 100644 --- a/Themes/default/ManageSearch.template.php +++ b/Themes/default/ManageSearch.template.php @@ -322,7 +322,7 @@ function template_create_index_progress()

- +
@@ -330,23 +330,8 @@ function template_create_index_progress() '; - } /** diff --git a/Themes/default/Memberlist.template.php b/Themes/default/Memberlist.template.php index 38b1191a8d..5014ee8fe1 100644 --- a/Themes/default/Memberlist.template.php +++ b/Themes/default/Memberlist.template.php @@ -23,8 +23,8 @@ function template_main() echo '
- ', template_button_strip(Utils::$context['memberlist_buttons'], 'right'), ' + ', template_button_strip(Utils::$context['memberlist_buttons'], 'right'), '

diff --git a/Themes/default/MessageIndex.template.php b/Themes/default/MessageIndex.template.php index e9287cc323..61f22812eb 100644 --- a/Themes/default/MessageIndex.template.php +++ b/Themes/default/MessageIndex.template.php @@ -21,7 +21,7 @@ */ function template_main() { - echo '
+ echo '

', Utils::$context['name'], '

'; if (isset(Utils::$context['description']) && Utils::$context['description'] != '') @@ -73,46 +73,15 @@ function template_main() if (!empty(Utils::$context['boards']) && (!empty(Theme::$current->options['show_children']) || Utils::$context['start'] == 0)) { echo ' -

', Lang::getTxt('sub_boards', file: 'General'), '

-
'; - - foreach (Utils::$context['boards'] as $board) - { - echo ' -
-
- ', function_exists('template_bi_' . $board['type'] . '_icon') ? call_user_func('template_bi_' . $board['type'] . '_icon', $board) : template_bi_board_icon($board), ' -
-
- ', function_exists('template_bi_' . $board['type'] . '_info') ? call_user_func('template_bi_' . $board['type'] . '_info', $board) : template_bi_board_info($board), ' -
'; - - // Show some basic information about the number of posts, etc. - echo ' -
- ', function_exists('template_bi_' . $board['type'] . '_stats') ? call_user_func('template_bi_' . $board['type'] . '_stats', $board) : template_bi_board_stats($board), ' -
'; - - // Show the last post if there is one. - echo ' -
- ', function_exists('template_bi_' . $board['type'] . '_lastpost') ? call_user_func('template_bi_' . $board['type'] . '_lastpost', $board) : template_bi_board_lastpost($board), ' -
'; - - // Won't somebody think of the children! - if (function_exists('template_bi_' . $board['type'] . '_children')) - call_user_func('template_bi_' . $board['type'] . '_children', $board); - else - template_bi_board_children($board); +
+
'; - echo ' -
'; - } + template_list_boards(Utils::$context['boards']); echo ' -
'; +
'; } // Let them know why their message became unapproved. @@ -155,139 +124,7 @@ function template_main() echo '
'; - echo ' -
'; - - echo ' -
'; - - // Are there actually any topics to show? - if (!empty(Utils::$context['topics'])) - { - echo ' -
-
', Utils::$context['topics_headers']['subject'], ' / ', Utils::$context['topics_headers']['starter'], '
-
', Utils::$context['topics_headers']['replies'], ' / ', Utils::$context['topics_headers']['views'], '
-
', Utils::$context['topics_headers']['last_post'], '
'; - - // Show a "select all" box for quick moderation? - if (!empty(Utils::$context['can_quick_mod']) && Theme::$current->options['display_quick_mod'] == 1) - echo ' -
- -
'; - - // If it's on in "image" mode, don't show anything but the column. - elseif (!empty(Utils::$context['can_quick_mod'])) - echo ' -
'; - } - - // No topics... just say, "sorry bub". - else - echo ' -

', Lang::getTxt('topic_alert_none', file: 'General'), '

'; - - echo ' -
'; - - // Contain the topic list - echo ' -
'; - - foreach (Utils::$context['topics'] as $topic) - { - echo ' -
-
- - ', $topic['is_posted_in'] ? '' : '', ' -
-
-
'; - - // Now we handle the icons - echo ' -
'; - - if ($topic['is_watched']) - echo ' - '; - - if ($topic['is_locked']) - echo ' - '; - - if ($topic['is_sticky']) - echo ' - '; - - if ($topic['is_redirect']) - echo ' - '; - - if ($topic['is_poll']) - echo ' - '; - - echo ' -
'; - - echo ' -
- ', $topic['new'] && User::$me->is_logged ? '' . Lang::getTxt('new', file: 'General') . '' : '', ' - - ', $topic['first_post']['link'], (!$topic['approved'] ? ' (' . Lang::getTxt('awaiting_approval', file: 'General') . ')' : ''), ' - -
-

- ', Lang::getTxt('started_by_member', ['member' => $topic['first_post']['member']['link']], file: 'General'), ' -

- ', !empty($topic['pages']) ? '' . $topic['pages'] . '' : '', ' -
-
-
-

', Lang::getTxt('number_of_replies', [$topic['replies']], file: 'General'), '
', Lang::getTxt('number_of_views', [$topic['views']], file: 'General'), '

-
-
-

', Lang::getTxt('last_post_topic', ['post_link' => '' . $topic['last_post']['time'] . '', 'member_link' => $topic['last_post']['member']['link']], file: 'General'), '

-
'; - - // Show the quick moderation options? - if (!empty(Utils::$context['can_quick_mod'])) - { - echo ' -
'; - - if (Theme::$current->options['display_quick_mod'] == 1) - echo ' - '; - else - { - // Check permissions on each and show only the ones they are allowed to use. - if ($topic['quick_mod']['remove']) - echo ''; - - if ($topic['quick_mod']['lock']) - echo ''; - - if ($topic['quick_mod']['lock'] || $topic['quick_mod']['remove']) - echo '
'; - - if ($topic['quick_mod']['sticky']) - echo ''; - - if ($topic['quick_mod']['move']) - echo ''; - } - echo ' -
'; - } - echo ' -
'; - } - echo ' -
'; + template_list_topics(Utils::$context['topics_headers'], Utils::$context['topics']); if (!empty(Utils::$context['can_quick_mod']) && Theme::$current->options['display_quick_mod'] == 1 && !empty(Utils::$context['topics'])) { @@ -314,10 +151,6 @@ function template_main()
'; } - echo ' -
'; - - // Finish off the form - again. if (!empty(Utils::$context['can_quick_mod']) && Theme::$current->options['display_quick_mod'] > 0 && !empty(Utils::$context['topics'])) echo ' @@ -325,12 +158,12 @@ function template_main() echo '
- ', template_button_strip(Utils::$context['normal_buttons'], 'right'), ' ', Utils::$context['menu_separator'], ' '; +
+ ', template_button_strip(Utils::$context['normal_buttons'], 'right'), ''; // Mobile action buttons (bottom) if (!empty(Utils::$context['normal_buttons'])) @@ -346,10 +179,12 @@ function template_main() // Show breadcrumbs at the bottom too. theme_linktree(); + echo ' + '; + });'; // Javascript for inline editing. echo ' - '; @@ -391,134 +226,229 @@ function template_main() } /** - * Outputs the board icon for a standard board. + * Get the previous or next value in an associative array based on a given key. * - * @param array $board Current board information. - */ -function template_bi_board_icon($board) -{ - echo ' - '; -} - -/** - * Outputs the board icon for a redirect. + * @param array $array The associative array. + * @param mixed $current_key The key to search for. + * @param string $direction "before" to get the previous value, "after" to get the next value. * - * @param array $board Current board information. + * @return mixed|null The found value if exists, otherwise null. */ -function template_bi_redirect_icon($board) +function get_adjacent_value(array $array, $current_key, string $direction = "before") { - echo ' - '; + reset($array); + $previous_value = null; + + while (key($array) !== null) { + $key_in_loop = key($array); + $value = current($array); + + if ($direction === "before" && $key_in_loop === $current_key) { + return $previous_value; + } + + next($array); + + if ($direction === "after" && $key_in_loop === $current_key) { + return current($array) !== false ? current($array) : null; + } + + $previous_value = $value; + } + + return null; } /** - * Outputs the board info for a standard board or redirect. - * - * @param array $board Current board information. + * This actually displays the message index */ -function template_bi_board_info($board) +function template_list_topics(array $headers, array $topics): void { - echo ' - - ', $board['name'], ' - '; + /** + * Topic header column definitions. + * + * @var array $columns + * Each column has: + * - `class`: The CSS class name. + * - `content`: The displayed content. + * - `size`: The column width in CSS grid format. + * - `rowspan`: Number of rows the column should span. + * - `colspan`: Number of columns the column should span. + * - `row_number`: The row in which the element starts (1-based). + */ + $columns = [ + 'icon' => [ + 'class' => 'topic_icon', + 'content' => ['header' => ''], + 'size' => 'max-content', + 'rowspan' => 2, + 'colspan' => 1, + 'row_number' => 1, + ], + 'info' => [ + 'class' => 'info', + 'content' => ['header' => '{subject} / {starter}'], + 'size' => '1fr', + 'rowspan' => 1, + 'colspan' => 2, + 'row_number' => 1, + ], + 'author' => [ + 'class' => 'topic_author', + 'content' => ['header' => ''], + 'size' => 'auto', + 'rowspan' => 1, + 'colspan' => 1, + 'row_number' => 2, + ], + 'stats' => [ + 'class' => 'topic_stats', + 'content' => ['header' => '{replies} / {views}'], + 'size' => '10%', + 'rowspan' => 2, + 'colspan' => 1, + 'row_number' => 1, + ], + 'lastpost' => [ + 'class' => 'lastpost', + 'content' => ['header' => '{last_post}'], + 'size' => '28%', + 'rowspan' => 2, + 'colspan' => 1, + 'row_number' => 1, + ], + 'moderation' => [ + 'class' => 'moderation', + 'content' => [ + 'header' => '' + ], + 'size' => 'auto', + 'rowspan' => 2, + 'colspan' => 1, + 'row_number' => 1, + ], + ]; + + // Remove moderation column if quick mod is disabled + if (empty(Utils::$context['can_quick_mod']) || empty(Theme::$current->options['display_quick_mod'])) { + unset($columns['moderation']); + } - // Has it outstanding posts for approval? - if ($board['can_approve_posts'] && ($board['unapproved_posts'] || $board['unapproved_topics'])) - echo ' - !'; + $grid_rows = []; + $num_rows = 1; + $grid_sizes = []; + $row_numbers = []; + $indexed_grid_areas = []; + $column_index = 0; // Tracks column position + $column_numbers = []; + $prev_name = ''; + + foreach ($columns as $name => $column) { + $row_index = $column['row_number'] - 1; // Convert to zero-based index + + if (!isset($grid_rows[$row_index])) { + $grid_rows[$row_index] = []; + } + + for ($i = 0; $i < $column['colspan']; $i++) { + for ($y = 0; $y < $column['rowspan']; $y++) { + $current_row = $row_index + $y; + $row_numbers[$current_row][$name] = ($column_numbers[$row_index] ?? 0); + + if ($row_numbers[$current_row][$name] === 0) { + $row_numbers[$current_row][$name] = (get_adjacent_value($row_numbers[$current_row], $name) ?? -1) + 1; + } + + $column_index = $row_numbers[$current_row][$name]; + $grid_rows[$current_row][$column_index + $i] = $name; + } + + // Add column sizes only if we're on the first row. + if ($column['row_number'] === 1) { + $grid_sizes[] = $column['size']; + } + } + + // Move to the next available column + //~ $column_index = $row_numbers[$row_index][$column['colspan']; + $column_numbers[$row_index] = ($column_numbers[$row_index] ?? $row_numbers[$row_index][$name]) + $column['colspan']; + $num_rows = max($num_rows, $row_index + $column['rowspan']); + $prev_name = $name; + } + + // Fill empty grid areas. + for ($y = 0; $y < $num_rows; $y++) { + for ($i = 0, $n = count($grid_sizes); $i < $n; $i++) { + $indexed_grid_areas[$y][$i] = $grid_rows[$y][$i] ?? '.'; + } + } + + // Convert rows into CSS grid template areas. + $grid_areas_str = implode('" "', array_map(fn($r) => implode(' ', $r), $indexed_grid_areas)); echo ' -
', $board['description'], '
'; + '; -/** - * Outputs the board stats for a redirect. - * - * @param array $board Current board information. - */ -function template_bi_redirect_stats($board) -{ - echo ' -

- ', Lang::getTxt('number_of_redirects', [$board->posts], file: 'General'), ' -

'; -} + if ($topics == []) + { + // No topics... just say, "sorry bub". + echo ' +
+

', Lang::$txt['topic_alert_none'], '

+
'; + } + else + { + echo ' +
+
'; + + foreach ($columns as $column) { + echo ' +
' . Lang::formatText($column['content']['header'], $headers) . '
'; + } -/** - * Outputs the board lastposts for a standard board or a redirect. - * When on a mobile device, this may be hidden if no last post exists. - * - * @param array $board Current board information. - */ -function template_bi_board_lastpost($board) -{ - if (!empty($board['last_post']['id'])) echo ' -

', $board['last_post']['last_post_message'], '

'; -} +
'; -/** - * Outputs the board children for a standard board. - * - * @param array $board Current board information. - */ -function template_bi_board_children($board) -{ - // Show the "Child Boards: ". (there's a link_children but we're going to bold the new ones...) - if (!empty($board['children'])) - { - // Sort the links into an array with new boards bold so it can be imploded. - $children = array(); - /* Each child in each board's children has: - id, name, description, new (is it new?), topics (#), posts (#), href, link, and last_post. */ - foreach ($board['children'] as $child) + foreach ($topics as $topic) { - if (!$child['is_redirect']) - $child['link'] = '' . ($child['new'] ? '' . Lang::getTxt('new', file: 'General') . ' ' : '') . '' . $child['name'] . ''; - else - $child['link'] = '' . $child['name'] . ''; + echo ' +
'; - // Has it posts awaiting approval? - if ($child['can_approve_posts'] && ($child['unapproved_posts'] || $child['unapproved_topics'])) - $child['link'] .= ' !'; + foreach ($columns as $name => $column) { + echo ' +
'; - $children[] = $child['new'] ? '' . $child['link'] . '' : '' . $child['link'] . ''; - } + $callable = $column['content']['callable'] ?? ('template_topic_' . $name); + + if (is_callable($callable)) { + call_user_func($callable, $topic); + } + echo ' +
'; + } + echo ' +
'; + } echo ' -
-

', - Lang::getTxt( - 'sub_boards_list', - [ - 'id' => 'child_list_' . $board['id'], - 'num' => count($children), - 'list' => implode(' ', $children), - ], - file: 'General', - ), - '

-
'; +
'; } } @@ -567,3 +497,141 @@ function template_topic_legend()

'; } + +/** + * Renders the topic icon column. + * + * @param array $topic The topic data. + */ +function template_topic_icon($topic) +{ + echo ' + ', $topic['is_posted_in'] ? ' + ' : ''; +} + +/** + * Renders the topic info column, including title and preview. + * + * @param array $topic The topic data. + */ +function template_topic_info($topic) +{ + // Now we handle the icons + echo ' +
'; + + if ($topic['is_watched']) + echo ' + '; + + if ($topic['is_locked']) + echo ' + '; + + if ($topic['is_sticky']) + echo ' + '; + + if ($topic['is_redirect']) + echo ' + '; + + if ($topic['is_poll']) + echo ' + '; + + echo ' +
'; + + echo ' +
', $topic['new'] && User::$me->is_logged ? ' + ' . Lang::$txt['new'] . '' : '', ' + + ', $topic['first_post']['link'], (!$topic['approved'] ? ' (' . Lang::$txt['awaiting_approval'] . ')' : ''), ' + +
'; +} + +/** + * Renders the topic author. + * + * @param array $topic The topic data. + */ +function template_topic_author($topic) +{ + echo ' +

+ ', Lang::getTxt('started_by_member', ['member' => $topic['first_post']['member']['link']]), ' +

', !empty($topic['pages']) ? ' + ' . $topic['pages'] . '' : ''; +} + +/** + * Renders the topic stats column. + * + * @param array $topic The topic data. + */ +function template_topic_stats($topic) +{ + echo ' + ' . Lang::formatText( + '{0}
{1}', + [ + Lang::getTxt('number_of_replies', [$topic['replies']]), + Lang::getTxt('number_of_views', [$topic['views']]), + ], + ); +} + +/** + * Renders the last post column. + * + * @param array $topic The topic data. + */ +function template_topic_lastpost($topic) +{ + echo ' + ' . Lang::getTxt( + 'last_post_topic', + [ + 'post_link' => '' . $topic['last_post']['time'] . '', + 'member_link' => $topic['last_post']['member']['link'], + ], + ); +} + +/** + * Renders the topic moderation column (checkboxes, remove, lock, etc.). + * + * @param array $topic The topic data. + */ +function template_topic_moderation($topic) +{ + if (Theme::$current->options['display_quick_mod'] == 1) + echo ' + '; + else + { + // Check permissions on each and show only the ones they are allowed to use. + if ($topic['quick_mod']['remove']) + echo ' + '; + + if ($topic['quick_mod']['lock']) + echo ' + '; + + if ($topic['quick_mod']['lock'] || $topic['quick_mod']['remove']) + echo ' +
'; + + if ($topic['quick_mod']['sticky']) + echo ' + '; + + if ($topic['quick_mod']['move']) + echo ' + '; + } +} diff --git a/Themes/default/ModerationCenter.template.php b/Themes/default/ModerationCenter.template.php index 4b8f200ff0..19affa5ce0 100644 --- a/Themes/default/ModerationCenter.template.php +++ b/Themes/default/ModerationCenter.template.php @@ -48,9 +48,9 @@ function template_group_requests_block() echo '
'; } @@ -818,8 +816,9 @@ function template_search()
    '; @@ -971,7 +970,7 @@ function template_send() // Show the preview of the personal message. echo ' -
'; } - echo ' -
'; - // Two columns with the most popular boards by posts and activity (activity = users posts / total posts). echo ' -
-
+

', Lang::getTxt('statPanel_topBoards', file: 'Profile'), ' @@ -1384,7 +1377,7 @@ function template_statPanel() if (empty(Utils::$context['popular_boards'])) echo ' -

', Lang::getTxt('statPanel_noPosts', file: 'Profile'), '

'; +

', Lang::$txt['statPanel_noPosts'], '

'; else { @@ -1408,8 +1401,6 @@ function template_statPanel() '; } echo ' -

-

', Lang::getTxt('statPanel_topBoardsActivity', file: 'Profile'), ' @@ -1441,11 +1432,10 @@ function template_statPanel() '; } echo ' -

-
'; +
'; echo ' -
'; +
'; } /** @@ -2121,7 +2111,7 @@ function template_groupMembership() if (Utils::$context['can_edit_primary']) echo ' - '; + '; echo ' '; @@ -2137,8 +2127,8 @@ function template_groupMembership() if (Utils::$context['can_edit_primary']) echo ' -
- +
+
'; // Any groups they can join? @@ -2171,29 +2161,6 @@ function template_groupMembership()
'; } } - - // Javascript for the selector stuff. - echo ' - '; } echo ' @@ -2223,60 +2190,9 @@ function template_ignoreboards()

', Lang::getTxt('ignoreboards_info', file: 'Profile'), '

-
-
-
    '; - - foreach (Utils::$context['categories'] as $category) - { - echo ' -
  • - ', $category['name'], ' -
      '; +
      '; - $cat_boards = array_values($category['boards']); - foreach ($cat_boards as $key => $board) - { - echo ' -
    • - '; - - // Nest child boards inside another list. - $curr_child_level = $board['child_level']; - $next_child_level = $cat_boards[$key + 1]['child_level'] ?? 0; - - if ($next_child_level > $curr_child_level) - { - echo ' -
        '; - } - else - { - // Close child board lists until we reach a common level - // with the next board. - while ($next_child_level < $curr_child_level--) - { - echo ' - -
      '; - } - - echo ' -
    • '; - } - } - - echo ' -
    -
  • '; - } - - echo ' -
-
'; + template_choose_boards(Utils::$context['categories']); // Show the standard "Save Settings" profile button. template_profile_save(); @@ -2365,51 +2281,9 @@ function template_issueWarning() echo ' '; - - echo ' + var notification_templates = ', json_encode(Utils::$context['notification_templates']), '; + var level_effects = ', json_encode(Utils::$context['level_effects']), '; +

@@ -2447,9 +2321,9 @@ function updateSlider(slideAmount) echo '
- ', Lang::formatText('{0, number, :: percent}', [0]), ' ', Lang::formatText('{0, number, :: percent}', [100]), ' + ', Lang::formatText('{0, number, :: percent}', [0]), ' ', Lang::formatText('{0, number, :: percent}', [100]), '
- ', Lang::getTxt('profile_warning_impact', file: 'Profile'), ': ', Lang::formatText('{0, number, :: percent}', [Utils::$context['member']['warning']]), ' (', Utils::$context['level_effects'][Utils::$context['current_level']], ') + ', Lang::$txt['profile_warning_impact'], ': ', Lang::formatText('{0, number, :: percent}', [Utils::$context['member']['warning']]), ' (', Utils::$context['level_effects'][Utils::$context['current_level']], ')
'; @@ -2481,7 +2355,7 @@ function updateSlider(slideAmount)
- +
@@ -2493,8 +2367,8 @@ function updateSlider(slideAmount)
- + '; foreach (Utils::$context['notification_templates'] as $id_template => $template) @@ -2503,8 +2377,6 @@ function updateSlider(slideAmount) echo ' - -
'; } @@ -2518,67 +2390,14 @@ function updateSlider(slideAmount) echo ' - - + +

'; // Previous warnings? template_show_list('view_warnings'); - - echo ' - '; } /** @@ -2814,7 +2633,7 @@ function template_profile_group_manage() ', Lang::getTxt('additional_membergroups', file: 'Profile'), '
- +
'; // For each membergroup show a checkbox so members can be assigned to more than one group. @@ -2824,12 +2643,7 @@ function template_profile_group_manage()
'; echo ' - - - +
'; } @@ -2866,7 +2680,7 @@ function template_profile_signature_modify() echo '
-
'; +
'; // If there is a limit at all! if (!empty(Utils::$context['signature_limits']['max_length'])) @@ -2875,24 +2689,13 @@ function template_profile_signature_modify() if (!empty(Utils::$context['show_preview_button'])) echo ' - '; + '; if (Utils::$context['signature_warning']) echo ' ', Utils::$context['signature_warning'], ''; - // Some javascript used to count how many characters have been used so far in the signature. echo ' -
'; } @@ -2904,45 +2707,45 @@ function template_profile_avatar_select() // Start with the upper menu echo '
- - - '; +
+ ', Lang::$txt['personal_picture'], ''; if (empty(Config::$modSettings['gravatarEnabled']) || empty(Config::$modSettings['gravatarOverride'])) echo ' - +
'; if (!empty(Utils::$context['member']['avatar']['allow_server_stored'])) echo ' - +
'; if (!empty(Utils::$context['member']['avatar']['allow_external'])) echo ' - +
'; if (!empty(Utils::$context['member']['avatar']['allow_upload'])) echo ' - +
'; if (!empty(Utils::$context['member']['avatar']['allow_gravatar'])) echo ' - - + + '; echo ' +
'; @@ -2950,66 +2753,53 @@ function template_profile_avatar_select() if (!empty(Utils::$context['member']['avatar']['allow_server_stored'])) { echo ' -
-
- '; // This lists all the file categories. foreach (Utils::$context['avatars'] as $avatar) - echo ' - '; - - echo ' - -
-
- -
-
- -
- -
'; + if ($avatar['is_dir']) + { + echo ' + '; + + foreach ($avatar['files'] as $a) + echo ' + '; + + echo ' + '; + } + else + echo ' + '; + + echo ' + + '; } // If the user can link to an off server avatar, show them a box to input the address. if (!empty(Utils::$context['member']['avatar']['allow_external'])) echo ' -
- ', Utils::$context['member']['avatar']['choice'] == 'external' ? '
' : '', ' -
', Lang::getTxt('avatar_by_url', file: 'Profile'), '
', !empty(Config::$modSettings['avatar_action_too_large']) && Config::$modSettings['avatar_action_too_large'] == 'option_download_and_resize' ? template_max_size('external') : '', ' -
-
'; +
+
', Lang::$txt['avatar_by_url'], '
', !empty(Config::$modSettings['avatar_action_too_large']) && Config::$modSettings['avatar_action_too_large'] == 'option_download_and_resize' ? template_max_size('external') : '', ' +
+
'; // If the user is able to upload avatars to the server show them an upload box. if (!empty(Utils::$context['member']['avatar']['allow_upload'])) echo ' -
- ', Utils::$context['member']['avatar']['choice'] == 'upload' ? '
' : '', ' - ', template_max_size('upload'), ' +
+ ', template_max_size('upload'), ' ', (!empty(Utils::$context['member']['avatar']['id_attach']) ? '
' : ''), ' -
'; + '; // if the user is able to use Gravatar avatars show then the image preview if (!empty(Utils::$context['member']['avatar']['allow_gravatar'])) { echo ' -
- ', Utils::$context['member']['avatar']['choice'] == 'gravatar' ? '
' : ''; +
'; if (empty(Config::$modSettings['gravatarAllowExtraEmail'])) echo ' @@ -3023,59 +2813,15 @@ function template_profile_avatar_select() $textbox_value = Utils::$context['member']['avatar']['external']; echo ' -
', Lang::getTxt('gravatar_alternateEmail', file: 'Profile'), '
- '; +
', Lang::$txt['gravatar_alternateEmail'], '
+ '; } echo ' -
'; + '; } echo ' - +
'; } @@ -3089,12 +2835,10 @@ function template_max_size($type) $w = !empty(Config::$modSettings['avatar_max_width_' . $type]) ? Lang::numberFormat(Config::$modSettings['avatar_max_width_' . $type]) : 0; $h = !empty(Config::$modSettings['avatar_max_height_' . $type]) ? Lang::numberFormat(Config::$modSettings['avatar_max_height_' . $type]) : 0; - $suffix = (!empty($w) ? 'w' : '') . (!empty($h) ? 'h' : ''); - if (empty($suffix)) - return; - - echo ' -
', Lang::getTxt('avatar_max_size_' . $suffix, ['w' => $w, 'h' => $h], file: 'Profile'), '
'; + $suffix = ($w != 0 ? 'w' : '') . ($h != 0 ? 'h' : ''); + if ($suffix != '') + echo ' +
', Lang::getTxt('avatar_max_size_' . $suffix, ['w' => $w, 'h' => $h]), '
'; } /** @@ -3409,7 +3153,7 @@ function template_export_profile_data() - + '; } @@ -3477,8 +3221,6 @@ function template_export_profile_data() } echo ' - -
', Lang::getTxt('export_format', file: 'Profile'), '
@@ -3493,26 +3235,16 @@ function template_export_profile_data()
-
'; +
+ '; // At least one active or completed export exists. if (!empty($dltoken)) { echo ' - -
- - - -
'; - } - // No existing exports. - else - { - echo ' - '; + + + '; } echo ' diff --git a/Themes/default/Recent.template.php b/Themes/default/Recent.template.php index 59bf74aa2e..4693263dba 100644 --- a/Themes/default/Recent.template.php +++ b/Themes/default/Recent.template.php @@ -22,7 +22,7 @@ function template_recent() { echo '
-
+

', Lang::getTxt('recent_posts', file: 'General'), '

@@ -83,7 +83,7 @@ function template_unread() echo '
-
+

', (!empty(Board::$info->name) ? Board::$info->name . ' - ' : '') . Utils::$context['page_title'], '

@@ -262,7 +262,7 @@ function template_replies() echo '
-
+

', (!empty(Board::$info->name) ? Board::$info->name . ' - ' : '') . Utils::$context['page_title'], '

diff --git a/Themes/default/Register.template.php b/Themes/default/Register.template.php index 24d30b5ce7..5d40240951 100644 --- a/Themes/default/Register.template.php +++ b/Themes/default/Register.template.php @@ -103,93 +103,58 @@ function verifyAgree() } echo ' -
-
-

', Lang::getTxt('registration_form', file: 'Login'), '

-
+
+

', Lang::$txt['registration_form'], '

+
+
-

', Lang::getTxt('required_info', file: 'Login'), '

+

', Lang::$txt['required_info'], '

+
+
+ +
+
+
-
-
-
-
- -
-
- - -
-
-
- -
-
-
-
-
- - -
-
-
-
- -
-
- - -
-
-
-
- -
-
- -
-
'; +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
'; // If there is any field marked as required, show it here! if (!empty(Utils::$context['custom_fields_required']) && !empty(Utils::$context['custom_fields'])) { - echo ' -
'; - foreach (Utils::$context['custom_fields'] as $field) if ($field['show_reg'] > 1) echo ' -
- ', $field['name'], ': - ', $field['desc'], ' -
-
', str_replace('name="', 'tabindex="' . Utils::$context['tabindex']++ . '" name="', $field['input_html']), '
'; - - echo ' -
'; +
+ ', $field['name'], ': + ', $field['desc'], ' +
+
', $field['input_html'], '
'; } - echo ' - -
'; - // If we have either of these, show the extra group. if (!empty(Utils::$context['profile_fields']) || !empty(Utils::$context['custom_fields'])) echo '
-

', Lang::getTxt('additional_information', file: 'Login'), '

-
-
-
-
'; +

', Lang::$txt['additional_information'], '

+
'; if (!empty(Utils::$context['profile_fields'])) { @@ -207,8 +172,8 @@ function verifyAgree() else { echo ' -
- ', $field['label'], ':'; +
+ ', $field['label'], ':'; // Does it have any subtext to show? if (!empty($field['subtext'])) @@ -216,8 +181,8 @@ function verifyAgree() ', $field['subtext'], ''; echo ' -
-
'; +
+
'; // Want to put something infront of the box? if (!empty($field['preinput'])) @@ -232,18 +197,18 @@ function verifyAgree() // Maybe it's a text box - very likely! elseif (in_array($field['type'], array('int', 'float', 'text', 'password', 'url'))) echo ' - '; + '; // You "checking" me out? ;) elseif ($field['type'] == 'check') echo ' - '; + '; // Always fun - select boxes! elseif ($field['type'] == 'select') { echo ' - '; if (isset($field['options'])) { @@ -268,7 +233,7 @@ function verifyAgree() ', $field['postinput']; echo ' - '; +
'; } } } @@ -279,46 +244,33 @@ function verifyAgree() foreach (Utils::$context['custom_fields'] as $field) if ($field['show_reg'] < 2) echo ' -
- ', $field['name'], ': - ', $field['desc'], ' -
-
', $field['input_html'], '
'; +
+ ', $field['name'], ': + ', $field['desc'], ' +
+
', $field['input_html'], '
'; } - // If we have either of these, close the list like a proper gent. - if (!empty(Utils::$context['profile_fields']) || !empty(Utils::$context['custom_fields'])) - echo ' - - -
'; - if (Utils::$context['visual_verification']) echo '
-

', Lang::getTxt('verification', file: 'General'), '

+

', Lang::$txt['verification'], '

-
-
- ', template_control_verification(Utils::$context['visual_verification_id'], 'all'), ' -
-
'; - - echo ' -
'; +
+ ', template_control_verification(Utils::$context['visual_verification_id'], 'all'), ' +
'; // Age restriction in effect? if (empty(Utils::$context['agree']) && Utils::$context['show_coppa']) echo ' -
-
- '; +
+
+ '; else echo ' - '; + '; echo ' -
@@ -443,30 +395,14 @@ function template_verification_sound() ', Lang::getTxt('visual_verification_sound', file: 'General'), ' ', Theme::template_css(), ' - -
'; - - if (BrowserDetector::isBrowser('is_ie') || BrowserDetector::isBrowser('is_ie11')) - echo ' - - - - '; - else - echo ' +
'; - - echo ' +
', Lang::getTxt('visual_verification_sound_again', file: 'Login'), '
', Lang::getTxt('visual_verification_sound_direct', file: 'Login'), '

@@ -483,51 +419,49 @@ function template_admin_register() { echo '
- -
-

', Lang::getTxt('admin_browse_register_new', file: 'Admin'), '

-
-
'; +
+

', Lang::$txt['admin_browse_register_new'], '

+
+ '; if (!empty(Utils::$context['registration_done'])) echo ' -
+

', Utils::$context['registration_done'], ' -

'; +

'; echo ' -
-
- - ', Lang::getTxt('admin_register_username_desc', file: 'Login'), ' -
-
- -
-
- - ', Lang::getTxt('admin_register_email_desc', file: 'Login'), ' -
-
- -
-
- - ', Lang::getTxt('admin_register_password_desc', file: 'Login'), ' -
-
- -
'; +
+ + ', Lang::$txt['admin_register_username_desc'], ' +
+
+ +
+
+ + ', Lang::$txt['admin_register_email_desc'], ' +
+
+ +
+
+ + ', Lang::$txt['admin_register_password_desc'], ' +
+
+ +
'; if (!empty(Utils::$context['member_groups'])) { echo ' -
- - ', Lang::getTxt('admin_register_group_desc', file: 'Login'), ' -
-
- '; foreach (Utils::$context['member_groups'] as $id => $name) echo ' @@ -535,7 +469,7 @@ function template_admin_register() echo ' -
'; +
'; } // If there is any field marked as required, show it here! @@ -543,36 +477,32 @@ function template_admin_register() foreach (Utils::$context['custom_fields'] as $field) if ($field['show_reg'] > 1) echo ' -
- ', $field['name'], ': +
+ ', $field['name'], ': ', $field['desc'], ' -
-
- ', str_replace('name="', 'tabindex="' . Utils::$context['tabindex']++ . '" name="', $field['input_html']), ' -
'; +
+
+ ', $field['input_html'], ' +
'; echo ' -
- - ', Lang::getTxt('admin_register_email_detail_desc', file: 'Login'), ' -
-
- -
-
- -
-
- -
- -
- +
+ + ', Lang::$txt['admin_register_email_detail_desc'], ' +
+
+ +
+
+ +
+
+ +
+ -
-

'; @@ -600,17 +530,21 @@ function template_edit_agreement() echo '
-

', Lang::getTxt('registration_agreement', file: 'General'), '

-
'; +

', Lang::$txt['registration_agreement'], '

+
+
'; // Is there more than one language to choose from? if (count(Utils::$context['editable_agreements']) > 1) { echo ' +
+

', Lang::$txt['language_configuration'], '

+
-
- ', Lang::getTxt('admin_agreement_select_language', file: 'Admin'), ' - '; foreach (Utils::$context['editable_agreements'] as $file => $name) echo ' @@ -622,7 +556,7 @@ function template_edit_agreement() - +
'; @@ -630,7 +564,6 @@ function template_edit_agreement() // Show the actual agreement in an oversized text box. echo ' -
'; @@ -661,16 +594,10 @@ function template_edit_agreement() echo '
- + -
@@ -687,45 +614,41 @@ function template_edit_reserved_words()
', Lang::getTxt('settings_saved', file: 'Admin'), '
'; echo ' -
-
-

', Lang::getTxt('admin_reserved_set', file: 'Admin'), '

-
-
-

', Lang::getTxt('admin_reserved_line', file: 'Admin'), '

+
+

', Lang::$txt['admin_reserved_set'], '

+
+ +

', Lang::$txt['admin_reserved_line'], '

-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- +
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ -
'; } @@ -740,17 +663,18 @@ function template_edit_privacy_policy() // Just a big box to edit the text file ;). echo '
-

', Lang::getTxt('privacy_policy', file: 'General'), '

-
'; +

', Lang::$txt['privacy_policy'], '

+
+
'; // Is there more than one language to choose from? if (count(Utils::$context['editable_policies']) > 1) { echo '
-
- ', Lang::getTxt('admin_agreement_select_language', file: 'Admin'), ' - '; foreach (Utils::$context['editable_policies'] as $lang => $name) echo ' @@ -761,14 +685,13 @@ function template_edit_privacy_policy()
- +
'; } echo ' -
'; // Show the actual policy in an oversized text box. @@ -803,7 +726,7 @@ function template_edit_privacy_policy() echo '
- + diff --git a/Themes/default/Reminder.template.php b/Themes/default/Reminder.template.php index e4d50718e4..19ee06caf2 100644 --- a/Themes/default/Reminder.template.php +++ b/Themes/default/Reminder.template.php @@ -20,21 +20,18 @@ function template_main() { echo ' -
- + '; } @@ -80,8 +73,7 @@ function template_reminder_pick() function template_sent() { echo ' -
-