diff --git a/phpBB/adm/style/acp_profile.html b/phpBB/adm/style/acp_profile.html index 7a0ecce14e..4df30408e7 100644 --- a/phpBB/adm/style/acp_profile.html +++ b/phpBB/adm/style/acp_profile.html @@ -91,6 +91,12 @@
checked="checked" />
+

{{ lang('FIELD_ICON_EXPLAIN') }}
+
{{ Icon('font', FIELD_ICON, '', true, 'acp-icon', {'style': 'margin:0 6px;' ~ (FIELD_ICON_COLOR ? (' color: #' ~ FIELD_ICON_COLOR ~ ';') : '')}) }}
+
+ + +
{% EVENT acp_profile_contact_after %} diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index 290eaffcf8..78e380da9f 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -3152,3 +3152,29 @@ span + .o-icon { .acp-icon-disabled { color: #d0d0d0; } + + +input[type="color"] { + background-color: transparent; + border: solid 1px #d3d3d3; + border-radius: 50%; + width: 24px; + height: 24px; + padding: 2px; + cursor: pointer; + -webkit-appearance: none; +} + +input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} + +input[type="color"]::-webkit-color-swatch { + border: 0; + border-radius: 50%; +} + +input[type="color"]::-moz-color-swatch { + border: 0; + border-radius: 50%; +} diff --git a/phpBB/adm/style/admin.js b/phpBB/adm/style/admin.js index 2655fa6d90..01b2e3946f 100644 --- a/phpBB/adm/style/admin.js +++ b/phpBB/adm/style/admin.js @@ -244,6 +244,67 @@ function parse_document(container) }); } +/** +* Automatically display custom profile fields FontAwesome icon +*/ +const DEFAULT_COLOR = '#000000'; +const HEX_REGEX = /^#[A-Fa-f0-9]{6}$/; +const colorPicker = document.getElementById('field_icon_color_picker'); +const colorText = colorPicker.previousElementSibling; + +const syncColors = (source, target) => { + const value = '#' + source.value.trim(); + target.value = HEX_REGEX.test(value) ? value : DEFAULT_COLOR; +}; + +const handleInput = ({ target }) => { + if (target === colorPicker) { + colorText.value = target.value.substring(1); + } else { + syncColors(colorText, colorPicker); + } + const icon = field_icon?.nextElementSibling; + if (icon && icon.tagName.toLowerCase() === 'i') { + icon.style.color = colorPicker.value; + } +}; + +colorPicker.addEventListener('input', handleInput); +colorText.addEventListener('input', handleInput); +colorText.addEventListener('blur', () => { + if (!colorText.value.trim()) { + colorPicker.value = DEFAULT_COLOR; + } +}); +syncColors(colorText, colorPicker); + +var field_icon = document.getElementById('field_icon'); +if (!field_icon.nextElementSibling) { + icon_demo = document.createElement('i'); + icon_demo.setAttribute('style', `margin:0 6px; color: ${colorPicker.value}`); + icon_demo.setAttribute('class', `o-icon o-icon-font fa-fw fas acp-icon`); + field_icon.after(icon_demo); +} + +const updateIconClass = (element, newClass) => { + + element.classList.forEach(className => { + if (className.startsWith('fa-') && className !== 'fa-fw') { + element.classList.remove(className); + } + }); + + element.classList.add(`fa-${newClass}`); +}; + +field_icon.addEventListener('keyup', function() { + updateIconClass(this.nextElementSibling, this.value); +}); + +field_icon.addEventListener('blur', function() { + updateIconClass(this.nextElementSibling, this.value); +}); + /** * Run onload functions */ diff --git a/phpBB/includes/acp/acp_profile.php b/phpBB/includes/acp/acp_profile.php index 8cf60c2e7d..edbd691c2c 100644 --- a/phpBB/includes/acp/acp_profile.php +++ b/phpBB/includes/acp/acp_profile.php @@ -360,6 +360,7 @@ class acp_profile $field_row = array_merge($profile_field->get_default_option_values(), array( 'field_ident' => str_replace(' ', '_', utf8_clean_string($request->variable('field_ident', '', true))), 'field_required' => 0, + 'field_icon' => json_encode(['name' => '', 'color' => '']), 'field_show_novalue'=> 0, 'field_hide' => 0, 'field_show_profile'=> 0, @@ -381,7 +382,7 @@ class acp_profile // $exclude contains the data we gather in each step $exclude = array( - 1 => array('field_ident', 'lang_name', 'lang_explain', 'field_option_none', 'field_show_on_reg', 'field_show_on_pm', 'field_show_on_vt', 'field_show_on_ml', 'field_required', 'field_show_novalue', 'field_hide', 'field_show_profile', 'field_no_view', 'field_is_contact', 'field_contact_desc', 'field_contact_url'), + 1 => array('field_ident', 'field_icon', 'field_icon_color', 'lang_name', 'lang_explain', 'field_option_none', 'field_show_on_reg', 'field_show_on_pm', 'field_show_on_vt', 'field_show_on_ml', 'field_required', 'field_show_novalue', 'field_hide', 'field_show_profile', 'field_no_view', 'field_is_contact', 'field_contact_desc', 'field_contact_url'), 2 => array('field_length', 'field_maxlen', 'field_minlen', 'field_validation', 'field_novalue', 'field_default_value'), 3 => array('l_lang_name', 'l_lang_explain', 'l_lang_default_value', 'l_lang_options') ); @@ -427,6 +428,13 @@ class acp_profile $options = $profile_field->prepare_options_form($exclude, $visibility_ary); + $field_icon_data = json_decode($field_row['field_icon'], true); + $field_icon_name = $request->variable('field_icon', $field_icon_data['name'] ?: ''); + $field_icon_color = $field_icon_name ? $request->variable('field_icon_color', $field_icon_data['color'] ?: '') : ''; + $cp->vars['field_icon'] = json_encode([ + 'name' => $field_icon_name, + 'color' => $field_icon_color, + ]); $cp->vars['field_ident'] = ($action == 'create' && $step == 1) ? utf8_clean_string($request->variable('field_ident', $field_row['field_ident'], true)) : $request->variable('field_ident', $field_row['field_ident']); $cp->vars['lang_name'] = $request->variable('lang_name', $field_row['lang_name'], true); $cp->vars['lang_explain'] = $request->variable('lang_explain', $field_row['lang_explain'], true); @@ -630,6 +638,7 @@ class acp_profile { // Create basic options - only small differences between field types case 1: + $field_icon_data = json_decode($cp->vars['field_icon'], true); $template_vars = array( 'S_STEP_ONE' => true, 'S_FIELD_REQUIRED' => ($cp->vars['field_required']) ? true : false, @@ -648,6 +657,8 @@ class acp_profile 'L_LANG_SPECIFIC' => sprintf($user->lang['LANG_SPECIFIC_OPTIONS'], $config['default_lang']), 'FIELD_TYPE' => $profile_field->get_name(), 'FIELD_IDENT' => $cp->vars['field_ident'], + 'FIELD_ICON' => $field_icon_data['name'], + 'FIELD_ICON_COLOR' => $field_icon_data['color'], 'LANG_NAME' => $cp->vars['lang_name'], 'LANG_EXPLAIN' => $cp->vars['lang_explain'], ); @@ -984,6 +995,7 @@ class acp_profile 'field_is_contact' => $cp->vars['field_is_contact'], 'field_contact_desc' => $cp->vars['field_contact_desc'], 'field_contact_url' => $cp->vars['field_contact_url'], + 'field_icon' => $cp->vars['field_icon'], ); $field_data = $cp->vars; diff --git a/phpBB/includes/ucp/ucp_pm_viewmessage.php b/phpBB/includes/ucp/ucp_pm_viewmessage.php index d418e49887..e4e179c82a 100644 --- a/phpBB/includes/ucp/ucp_pm_viewmessage.php +++ b/phpBB/includes/ucp/ucp_pm_viewmessage.php @@ -373,8 +373,11 @@ function view_message($id, $mode, $folder_id, $msg_id, $folder, $message_row) if ($cp_block_row['S_PROFILE_CONTACT']) { + $icon_data = json_decode($cp_block_row['PROFILE_FIELD_ICON'], true); $template->assign_block_vars('contact', array( 'ID' => $cp_block_row['PROFILE_FIELD_IDENT'], + 'ICON' => $icon_data['name'], + 'ICON_COLOR'=> $icon_data['color'], 'NAME' => $cp_block_row['PROFILE_FIELD_NAME'], 'U_CONTACT' => $cp_block_row['PROFILE_FIELD_CONTACT'], )); diff --git a/phpBB/language/en/acp/profile.php b/phpBB/language/en/acp/profile.php index 579235c912..ff97c32071 100644 --- a/phpBB/language/en/acp/profile.php +++ b/phpBB/language/en/acp/profile.php @@ -93,6 +93,9 @@ $lang = array_merge($lang, array( 'FIELD_DESCRIPTION' => 'Field description', 'FIELD_DESCRIPTION_EXPLAIN' => 'The explanation for this field presented to the user.', 'FIELD_DROPDOWN' => 'Dropdown box', + 'FIELD_ICON' => 'Field icon', + 'FIELD_ICON_COLOR' => 'Icon colour', + 'FIELD_ICON_EXPLAIN' => 'Enter a Font Awesome icon name to display with this contact field. Optionally, set its colour using a 6-digit hex code or the colour picker. Leave blank to use phpBB’s default icon and clear the colour.', 'FIELD_IDENT' => 'Field identification', 'FIELD_IDENT_ALREADY_EXIST' => 'The chosen field identification already exist. Please choose another name.', 'FIELD_IDENT_EXPLAIN' => 'The field identification is a name to identify the profile field within the database and the templates.', diff --git a/phpBB/phpbb/db/migration/data/v400/custom_profile_field_contact_icon.php b/phpBB/phpbb/db/migration/data/v400/custom_profile_field_contact_icon.php new file mode 100644 index 0000000000..9df240f495 --- /dev/null +++ b/phpBB/phpbb/db/migration/data/v400/custom_profile_field_contact_icon.php @@ -0,0 +1,51 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\db\migration\data\v400; + +class custom_profile_field_contact_icon extends \phpbb\db\migration\migration +{ + public function effectively_installed() + { + return $this->db_tools->sql_column_exists($this->table_prefix . 'profile_fields', 'field_icon'); + } + + public static function depends_on() + { + return [ + '\phpbb\db\migration\data\v400\dev', + ]; + } + + public function update_schema() + { + return array( + 'add_columns' => [ + $this->table_prefix . 'profile_fields' => [ + 'field_icon' => array('VCHAR:255', json_encode(['name' => '', 'color' => ''])), + ], + ], + ); + } + + public function revert_schema() + { + return array( + 'drop_columns' => array( + $this->table_prefix . 'profile_fields' => array( + 'field_icon', + ), + ), + ); + } +} diff --git a/phpBB/phpbb/profilefields/manager.php b/phpBB/phpbb/profilefields/manager.php index 7cc37e12ce..8ed6870576 100644 --- a/phpBB/phpbb/profilefields/manager.php +++ b/phpBB/phpbb/profilefields/manager.php @@ -332,6 +332,7 @@ class manager $tpl_fields[] = [ 'PROFILE_FIELD_IDENT' => $field_ident, + 'PROFILE_FIELD_ICON' => $field_data['field_icon'], 'PROFILE_FIELD_TYPE' => $field_data['field_type'], 'PROFILE_FIELD_NAME' => $profile_field->get_field_name($field_data['lang_name']), 'PROFILE_FIELD_EXPLAIN' => $this->language->lang($field_data['lang_explain']), @@ -484,6 +485,7 @@ class manager $tpl_fields['row'] += [ "PROFILE_{$ident_upper}_IDENT" => $ident, + "PROFILE_{$ident_upper}_ICON" => $ident_ary['data']['field_icon'], "PROFILE_{$ident_upper}_VALUE" => $value, "PROFILE_{$ident_upper}_VALUE_RAW" => $value_raw, "PROFILE_{$ident_upper}_CONTACT" => $contact_url, @@ -498,6 +500,7 @@ class manager $tpl_fields['blockrow'][] = [ 'PROFILE_FIELD_IDENT' => $ident, + 'PROFILE_FIELD_ICON' => $ident_ary['data']['field_icon'], 'PROFILE_FIELD_VALUE' => $value, 'PROFILE_FIELD_VALUE_RAW' => $value_raw, 'PROFILE_FIELD_CONTACT' => $contact_url, diff --git a/phpBB/styles/prosilver/template/ucp_pm_viewmessage.html b/phpBB/styles/prosilver/template/ucp_pm_viewmessage.html index 6572d965b8..2f5ca3d5ae 100644 --- a/phpBB/styles/prosilver/template/ucp_pm_viewmessage.html +++ b/phpBB/styles/prosilver/template/ucp_pm_viewmessage.html @@ -71,7 +71,10 @@
class="last-cell"> - {% if contact.ID == 'pm' %} + {% if contact.ICON %} + {% set color = contact.ICON_COLOR ? ({style: 'color: #' ~ contact.ICON_COLOR}) : [] %} + {{ Icon('font', contact.ICON, '', true, '', color) }} + {% elseif contact.ID == 'pm' %} {{ Icon('font', 'message', '', true, 'far contact-icon') }} {% elseif contact.ID == 'email' %} {{ Icon('font', 'at', '', true, 'fas contact-icon') }} diff --git a/phpBB/styles/prosilver/template/viewtopic_body.html b/phpBB/styles/prosilver/template/viewtopic_body.html index febc2d35d4..97ada402f7 100644 --- a/phpBB/styles/prosilver/template/viewtopic_body.html +++ b/phpBB/styles/prosilver/template/viewtopic_body.html @@ -193,7 +193,10 @@ class="last-cell"> {% EVENT viewtopic_body_contact_icon_prepend %} - {% if postrow.contact.ID == 'pm' %} + {% if postrow.contact.ICON %} + {% set color = postrow.contact.ICON_COLOR ? ({style: 'color: #' ~ contact.ICON_COLOR}) : [] %} + {{ Icon('font', contact.ICON, '', true, '', color) }} + {% elseif postrow.contact.ID == 'pm' %} {{ Icon('font', 'message', '', true, 'far contact-icon') }} {% elseif postrow.contact.ID == 'email' %} {{ Icon('font', 'at', '', true, 'fas contact-icon') }} diff --git a/phpBB/viewtopic.php b/phpBB/viewtopic.php index 3181204292..7f2ce77eb8 100644 --- a/phpBB/viewtopic.php +++ b/phpBB/viewtopic.php @@ -2193,8 +2193,11 @@ for ($i = 0, $end = count($post_list); $i < $end; ++$i) if ($field_data['S_PROFILE_CONTACT']) { + $icon_data = json_decode($field_data['PROFILE_FIELD_ICON'], true); $template->assign_block_vars('postrow.contact', array( 'ID' => $field_data['PROFILE_FIELD_IDENT'], + 'ICON' => $icon_data['name'], + 'ICON_COLOR'=> $icon_data['color'], 'NAME' => $field_data['PROFILE_FIELD_NAME'], 'U_CONTACT' => $field_data['PROFILE_FIELD_CONTACT'], )); diff --git a/tests/functional/profile_field_contact_icon_test.php b/tests/functional/profile_field_contact_icon_test.php new file mode 100644 index 0000000000..a19ac46e44 --- /dev/null +++ b/tests/functional/profile_field_contact_icon_test.php @@ -0,0 +1,95 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +/** +* @group functional +*/ +class phpbb_functional_profile_field_contact_icon_test extends phpbb_functional_test_case +{ + protected function setUp(): void + { + parent::setUp(); + + $this->login(); + $this->admin_login(); + $this->add_lang('acp/profile'); + } + + public function test_add_contact_field_icon() + { + // Custom profile fields page + $crawler = self::request('GET', 'adm/index.php?i=acp_profile&mode=profile&sid=' . $this->sid); + + // Get any contact profile field, f.e. phpbb_twitter + $twitter_field = $crawler->filter('tbody tr') + ->reduce( + function ($node, $i) { + $text = $node->text(); + return (strpos($text, 'phpbb_twitter') !== false); + }); + + $twitter_edit_url = $twitter_field->filter('.actions a')->eq(2)->attr('href'); + + $crawler = self::request('GET', 'adm/' . $twitter_edit_url . '&sid=' . $this->sid); + + $this->assertStringContainsString('phpbb_twitter', $crawler->text()); + + $form = $crawler->selectButton('Profile type specific options')->form([ + 'field_icon' => 'twitter', + 'field_icon_color' => '1da1f2', + ]); + $crawler= self::submit($form); + + $this->assertStringContainsString('Profile type specific options', $crawler->text()); + + $form = $crawler->selectButton('Save')->form(); + $crawler= self::submit($form); + $this->assertContainsLang('CHANGED_PROFILE_FIELD', $crawler->text()); + + // Ensure contact filed icon was saved correctly + $crawler = self::request('GET', 'adm/' . $twitter_edit_url . '&sid=' . $this->sid); + $this->assertEquals('twitter', $crawler->filter('#field_icon')->attr('value')); + $this->assertEquals('1da1f2', $crawler->filter('#contact_field_icon_bgcolor')->attr('value')); + $this->assertEquals(1, $crawler->filter('i.fa-twitter')->count()); + $this->assertStringContainsString('#1da1f2;', $crawler->filter('i.fa-twitter')->attr('style')); + } + + /** + * @depends test_add_contact_field_icon + */ + public function test_display_field_icon() + { + $this->add_lang('ucp'); + + // Set Twitter profile field + $crawler = self::request('GET', 'ucp.php?i=ucp_profile&mode=profile_info'); + $this->assertContainsLang('UCP_PROFILE_PROFILE_INFO', $crawler->filter('#cp-main h2')->text()); + + $form = $crawler->selectButton('Submit')->form([ + 'pf_phpbb_twitter' => 'phpbb_twitter', + ]); + $crawler = self::submit($form); + $this->assertContainsLang('PROFILE_UPDATED', $crawler->filter('#message')->text()); + + // Ensure Twitter icon displays in topic + $crawler = self::request('GET', 'viewtopic.php?t=1'); + $this->assertEquals('Twitter', $crawler->filter('#profile1 a[title="Twitter"]')->attr('title')); + $this->assertStringContainsString('#1da1f2', $crawler->filter('#profile1 a[title="Twitter"] > i.fa-twitter')->attr('style')); + + // Ensure Twitter icon displays on view private message screen + $message_id = $this->create_private_message('Self PM', 'Self PM', [2]); + $crawler = self::request('GET', 'ucp.php?i=pm&mode=view&p=' . $message_id . '&sid=' . $this->sid); + $this->assertEquals('Twitter', $crawler->filter('.profile-contact a[title="Twitter"]')->attr('title')); + $this->assertStringContainsString('#1da1f2', $crawler->filter('.profile-contact a[title="Twitter"] > i.fa-twitter')->attr('style')); + } +}