29
0
mirror of https://github.com/joomla/joomla-cms.git synced 2024-06-28 08:03:40 +00:00

[4.0] Switcher Field (#24463)

This commit is contained in:
Brian Teeman 2019-05-04 17:41:42 +01:00 committed by George Wilson
parent 8d64acbd98
commit 8507bfaa64
14 changed files with 125 additions and 571 deletions

View File

@ -12,5 +12,4 @@ joomla-field-permissions.w-c.es6.js
joomla-field-send-test-mail.w-c.es6.js
joomla-field-simple-color.w-c.es6.js
joomla-field-subform.w-c.es6.js
joomla-field-switcher.w-c.es6.js
# End of TODO

View File

@ -37,6 +37,9 @@
.controls {
margin-left: 0;
.switcher__legend {
float:none;
}
}
.control-label {

View File

@ -22,4 +22,8 @@
.revert-controls .controls {
margin-left: 0;
}
.switcher__legend {
margin-left: -220px;
}
}

View File

@ -49,6 +49,12 @@
"media/system/js/showon.min.js"
]
},
"switcher": {
"name": "switcher",
"css": [
"media/system/css/fields/switcher.min.css"
]
},
"fields.validate": {
"name": "fields.validate",
"dependencies": [

View File

@ -1,261 +0,0 @@
((customElements) => {
// Keycodes
const KEYCODE = {
ENTER: 13,
SPACE: 32,
};
class JoomlaSwitcherElement extends HTMLElement {
/* Attributes to monitor */
static get observedAttributes() { return ['type', 'off-text', 'on-text']; }
get type() { return this.getAttribute('type'); }
set type(value) { return this.setAttribute('type', value); }
get offText() { return this.getAttribute('off-text') || 'Off'; }
get onText() { return this.getAttribute('on-text') || 'On'; }
// attributeChangedCallback(attr, oldValue, newValue) {}
constructor() {
super();
this.inputs = [];
this.spans = [];
this.initialized = false;
this.inputsContainer = '';
this.newActive = '';
this.inputLabel = '';
this.inputLabelText = '';
// Let's bind some functions so we always have the same context
this.createMarkup = this.createMarkup.bind(this);
this.addListeners = this.addListeners.bind(this);
this.removeListeners = this.removeListeners.bind(this);
this.switch = this.switch.bind(this);
this.toggle = this.toggle.bind(this);
this.keyEvents = this.keyEvents.bind(this);
this.onFocus = this.onFocus.bind(this);
}
/* Lifecycle, element appended to the DOM */
connectedCallback() {
// Element was moved so we need to re add the event listeners
if (this.initialized && this.inputs.length > 0) {
this.addListeners();
return;
}
this.inputs = [].slice.call(this.querySelectorAll('input'));
if (this.inputs.length !== 2 || this.inputs[0].type !== 'radio') {
throw new Error('`Joomla-switcher` requires two inputs type="radio"');
}
// this.inputLabel = document.querySelector(`[for="${this.id}"]`);
//
// if (this.inputLabel) {
// this.inputLabelText = this.inputLabel.innerText;
// }
// Create the markup
this.createMarkup();
this.inputsContainer = this.inputs[0].parentNode;
this.inputsContainer.setAttribute('role', 'switch');
if (this.inputs[1].checked) {
this.inputs[1].parentNode.classList.add('active');
this.spans[1].classList.add('active');
// Aria-label ONLY in the container span!
this.inputsContainer.setAttribute('aria-labelledby', `${this.id}-lbl`); // this.spans[1].innerHTML);
} else {
this.spans[0].classList.add('active');
// Aria-label ONLY in the container span!
this.inputsContainer.setAttribute('aria-label', this.spans[0].innerHTML);
}
this.addListeners();
}
/* Lifecycle, element removed from the DOM */
disconnectedCallback() {
this.removeListeners();
}
/* Method to dispatch events */
dispatchCustomEvent(eventName) {
const OriginalCustomEvent = new CustomEvent(eventName, { bubbles: true, cancelable: true });
OriginalCustomEvent.relatedTarget = this;
this.dispatchEvent(OriginalCustomEvent);
this.removeEventListener(eventName, this);
}
/** Method to build the switch */
createMarkup() {
let checked = 0;
// If no type has been defined, the default as "success"
if (!this.type) {
this.setAttribute('type', 'success');
}
// Create the first 'span' wrapper
const spanFirst = document.createElement('fieldset');
spanFirst.classList.add('switcher');
spanFirst.classList.add(this.type);
spanFirst.setAttribute('tabindex', '0');
// Set the id to the fieldset
spanFirst.id = this.id;
// Remove the id from the custom Element
// this.removeAttribute('id');
const switchEl = document.createElement('span');
switchEl.classList.add('switch');
switchEl.classList.add(this.type);
this.inputs.forEach((input, index) => {
// Remove the tab focus from the inputs
input.setAttribute('tabindex', '-1');
if (input.checked) {
spanFirst.setAttribute('aria-checked', true);
}
spanFirst.appendChild(input);
if (index === 1 && input.checked) {
checked = 1;
}
});
spanFirst.appendChild(switchEl);
// Create the second 'span' wrapper
const spanSecond = document.createElement('span');
spanSecond.classList.add('switcher-labels');
const labelFirst = document.createElement('span');
labelFirst.classList.add('switcher-label-0');
labelFirst.innerHTML = `${this.offText}`;
const labelSecond = document.createElement('span');
labelSecond.classList.add('switcher-label-1');
labelSecond.innerHTML = `${this.onText}`;
if (checked === 0) {
labelFirst.classList.add('active');
} else {
labelSecond.classList.add('active');
}
this.spans.push(labelFirst);
this.spans.push(labelSecond);
spanSecond.appendChild(labelFirst);
spanSecond.appendChild(labelSecond);
// Append everything back to the main element
this.appendChild(spanFirst);
this.appendChild(spanSecond);
this.initialized = true;
}
/** Method to toggle the switch */
switch() {
this.spans.forEach((span) => {
span.classList.remove('active');
});
if (this.inputsContainer.classList.contains('active')) {
this.inputsContainer.classList.remove('active');
} else {
this.inputsContainer.classList.add('active');
}
// Remove active class from all inputs
this.inputs.forEach((input) => {
input.classList.remove('active');
});
// Check if active
if (this.newActive === 1) {
this.inputs[this.newActive].classList.add('active');
this.inputs[1].setAttribute('checked', '');
this.inputs[0].removeAttribute('checked');
this.inputsContainer.setAttribute('aria-checked', true);
// Aria-label ONLY in the container span!
this.inputsContainer.setAttribute('aria-label', `${this.inputLabelText} ${this.spans[1].innerHTML}`);
// Dispatch the "joomla.switcher.on" event
this.dispatchCustomEvent('joomla.switcher.on');
} else {
this.inputs[1].removeAttribute('checked');
this.inputs[0].setAttribute('checked', '');
this.inputs[0].classList.add('active');
this.inputsContainer.setAttribute('aria-checked', false);
// Aria-label ONLY in the container span!
this.inputsContainer.setAttribute('aria-label', `${this.inputLabelText} ${this.spans[0].innerHTML}`);
// Dispatch the "joomla.switcher.off" event
this.dispatchCustomEvent('joomla.switcher.off');
}
this.spans[this.newActive].classList.add('active');
}
/** Method to toggle the switch */
toggle() {
this.newActive = this.inputs[1].classList.contains('active') ? 0 : 1;
this.switch();
}
keyEvents(event) {
if (event.keyCode === KEYCODE.ENTER || event.keyCode === KEYCODE.SPACE) {
event.preventDefault();
this.newActive = this.inputs[1].classList.contains('active') ? 0 : 1;
this.switch();
}
}
onFocus() {
this.inputsContainer.focus();
}
addListeners() {
if (this.inputLabel) {
this.inputLabel.addEventListener('click', this.onFocus);
}
this.inputs.forEach((switchEl) => {
// Add the active class on click
switchEl.addEventListener('click', this.toggle);
});
this.inputsContainer.addEventListener('keydown', this.keyEvents);
}
removeListeners() {
if (this.inputLabel) {
this.inputLabel.removeEventListener('click', this.onFocus);
}
this.inputs.forEach((switchEl) => {
// Add the active class on click
switchEl.removeEventListener('click', this.toggle);
});
this.inputsContainer.removeEventListener('keydown', this.keyEvents);
}
}
customElements.define('joomla-field-switcher', JoomlaSwitcherElement);
})(customElements);

View File

@ -1,176 +0,0 @@
// Switcher
//
// Functions
//
// Retrieve color Sass maps
@function color($key: "blue") {
@return map-get($colors, $key);
}
@function theme-color($key: "primary") {
@return map-get($theme-colors, $key);
}
@function gray($key: "100") {
@return map-get($grays, $key);
}
// Request a theme color level
@function theme-color-level($color-name: "primary", $level: 0) {
$color: theme-color($color-name);
$color-base: if($level > 0, $black, $white);
$level: abs($level);
@return mix($color-base, $color, $level * $theme-color-interval);
}
//
// Variables
//
$switcher-width: 62px;
$switcher-height: 28px;
$border-width: 1px;
$border-radius: 0;
$white: #fff;
$gray-600: #868e96;
$blue: #1e87f0;
$red: #f0506e;
$yellow: #faa05a;
$green: #2f7d32;
$theme-colors: ();
$theme-colors: map-merge((
primary: $blue,
secondary: $gray-600,
success: $green,
warning: $yellow,
danger: $red
), $theme-colors);
$theme-color-interval: 8%;
//
// Base styles
//
joomla-field-switcher {
box-sizing: border-box;
display: block;
height: $switcher-height;
.switcher {
position: relative;
box-sizing: border-box;
display: inline-block;
width: $switcher-width;
height: $switcher-height;
vertical-align: middle;
cursor: pointer;
user-select: none;
background-color: darken($white, 5%);
background-clip: content-box;
border: $border-width solid rgba(0,0,0,.18);
border-radius: $border-radius;
box-shadow: 0 0 0 0 rgb(223,223,223) inset;
transition: border .4s ease 0s, box-shadow .4s ease 0s;
&.active {
transition: border .4s ease 0s, box-shadow .4s ease 0s, background-color 1.2s ease 0s;
.switch {
left: calc((#{$switcher-width} / 2) - (#{$border-width} * 2));
}
}
}
input {
position: absolute;
top: 0;
left: 0;
z-index: 2;
width: $switcher-width;
height: $switcher-height;
padding: 0;
margin: 0;
cursor: pointer;
opacity: 0;
}
.switch {
position: absolute;
top: 0;
left: 0;
width: calc(#{$switcher-width} / 2);
height: calc(#{$switcher-height} - (#{$border-width} * 2));
background: $white;
border-radius: $border-radius;
box-shadow: 0 1px 3px rgba(0,0,0,.15);
transition: left .2s ease 0s;
}
.switcher:focus .switch {
animation: switcherPulsate 1.5s infinite;
}
input:checked {
z-index: 0;
}
.switcher-labels {
position: relative;
span {
position: absolute;
top: 0;
color: var(--dark);
visibility: hidden;
opacity: 0;
transition: all .2s ease-in-out;
&.active {
visibility: visible;
opacity: 1;
transition: all .2s ease-in-out;
}
}
}
}
[dir="rtl"] joomla-field-switcher .switcher-labels span {
right: 10px;
}
[dir="ltr"] joomla-field-switcher .switcher-labels span {
left: 10px;
}
// Alternate styles
@each $color, $value in $theme-colors {
joomla-field-switcher[type="#{$color}"] .switcher.active {
background-color: theme-color-level($color);
border-color: theme-color-level($color);
box-shadow: 0 0 0 calc(#{$switcher-height} / 2) theme-color-level($color) inset;
}
}
@keyframes switcherPulsate {
0% {
box-shadow: 0 0 0 0 rgba(66,133,244,.55);
}
70% {
box-shadow: 0 0 0 10px rgba(66,133,244,0);
}
100% {
box-shadow: 0 0 0 0 rgba(66,133,244,0);
}
}

View File

@ -0,0 +1,80 @@
.switcher {
position: relative;
width: 18rem;
height: 28px;
}
.switcher input {
position: absolute;
top: 0;
z-index: 2;
opacity: 0;
cursor: pointer;
height: 28px;
width: 62px;
margin: 0;
}
.switcher input:checked {
z-index: 1;
}
.switcher input:checked + label {
opacity: 1;
}
.switcher input:not(:checked) + label {
opacity: 0;
}
.switcher label {
line-height: 28px;
display: inline-block;
width: 6rem;
height: 100%;
margin-left: 70px;
text-align: left;
position: absolute;
transition: opacity 0.25s ease;
margin-bottom: 0;
}
.switcher .toggle-outside {
height: 100%;
padding: 0.25rem;
overflow: hidden;
transition: 0.25s ease all;
background: grey;
position: absolute;
width: 62px;
box-sizing: border-box;
}
.switcher input ~ input:checked ~ .toggle-outside{
background: green;
}
.switcher .toggle-inside {
height: 20px;
width: 20px;
background: white;
position: absolute;
transition: 0.25s ease all;
}
.switcher input:checked ~ .toggle-outside .toggle-inside {
left: 0.25rem;
}
.switcher input ~ input:checked ~ .toggle-outside .toggle-inside {
left: 38px;
}
.switcher__legend {
margin-bottom: 1rem;
font-size: 1rem;
font-weight: 400;
float: left;
width: 220px;
padding-top: 5px;
padding-right: 5px;
text-align: left;
}
.col-md-3 .switcher__legend {
margin-left: 0;
}
.col-md-9 .switcher__legend {
margin-left: -220px;
}

View File

@ -28,13 +28,14 @@ use Joomla\CMS\HTML\HTMLHelper;
<?php HTMLHelper::_('script', 'system/showon.min.js', array('version' => 'auto', 'relative' => true)); ?>
<?php $datashowon = ' data-showon=\'' . json_encode(FormHelper::parseShowOnConditions($field->showon, $field->formControl, $field->group)) . '\''; ?>
<?php endif; ?>
<div class="control-group<?php echo $groupClass; ?>"<?php echo $datashowon; ?>>
<?php if (isset($displayData->showlabel)) : ?>
<div class="control-group<?php echo $groupClass; ?>"<?php echo $datashowon; ?>>
<div class="controls"><?php echo $field->input; ?></div>
</div>
<?php else : ?>
<?php echo $field->renderField(); ?>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endforeach; ?>
</fieldset>

View File

@ -9,6 +9,7 @@
defined('JPATH_BASE') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
extract($displayData, null);
@ -48,6 +49,9 @@ if (empty($options))
return '';
}
// Load the css files
Factory::getApplication()->getDocument()->getWebAssetManager()->enableAsset('switcher');
/**
* The format of the input tag to be filled in using sprintf.
* %1 - id
@ -55,55 +59,27 @@ if (empty($options))
* %3 - value
* %4 = any other attributes
*/
$format = '<input type="radio" id="%1$s" name="%2$s" value="%3$s" %4$s>';
$alt = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $name);
HTMLHelper::_('webcomponent', 'system/fields/joomla-field-switcher.min.js', ['version' => 'auto', 'relative' => true]);
// Set the type of switcher
$type = '';
if ($pos = strpos($class, 'switcher-'))
{
$type = 'type="' . strtok(substr($class, $pos + 9), ' ') . '"';
}
// Add the attributes of the fieldset in an array
$attribs = [
'id="' . $id . '"',
$type,
'off-text="' . $options[0]->text . '"',
'on-text="' . $options[1]->text . '"',
];
if (!empty($disabled))
{
$attribs[] = 'disabled';
}
if (!empty($onclick))
{
$attribs[] = 'onclick="' . $onclick . '()"';
}
if (!empty($onchange))
{
$attribs[] = 'onchange="' . $onchange . '()"';
}
$input = '<input type="radio" id="%1$s" name="%2$s" value="%3$s" %4$s>';
?>
<joomla-field-switcher <?php echo implode(' ', $attribs); ?>>
<fieldset>
<legend class="switcher__legend">
<?php echo $label; ?>
</legend>
<div class="switcher" role="switch">
<?php foreach ($options as $i => $option) : ?>
<?php
// Initialize some option attributes.
$checked = ((string) $option->value == $value) ? 'checked="checked"' : '';
$active = ((string) $option->value == $value) ? 'class="active"' : '';
// Initialize some JavaScript option attributes.
$oid = $id . $i;
$ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8');
$attributes = array_filter(array($checked, $active, null));
$checked = ((string) $option->value == $value) ? 'checked="checked"' : '';
$active = ((string) $option->value == $value) ? 'class="active"' : '';
$oid = $id . $i;
$ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8');
$attributes = array_filter(array($checked, $active, null));
$text = $options[$i]->text;
?>
<?php echo sprintf($format, $oid, $name, $ovalue, implode(' ', $attributes)); ?>
<?php echo sprintf($input, $oid, $name, $ovalue, implode(' ', $attributes)); ?>
<?php echo '<label for="' . $oid . '">' . $text . '</label>'; ?>
<?php endforeach; ?>
</joomla-field-switcher>
<span class="toggle-outside"><span class="toggle-inside"></span></span>
</div>
</fieldset>

View File

@ -948,7 +948,7 @@ abstract class FormField
$options['rel'] = '';
if (empty($options['hiddenLabel']) && $this->getAttribute('hiddenLabel'))
if (empty($options['hiddenLabel']) && $this->getAttribute('hiddenLabel') || $this->class === 'switcher')
{
$options['hiddenLabel'] = true;
}

View File

@ -45,20 +45,21 @@ form:not(.form-no-margin) {
}
form .form-no-margin {
.form-no-margin {
.control-group {
.controls {
margin-left: 0;
.switcher__legend {
float:none;
}
}
.control-label {
float: none;
}
}
}
.spacer hr {

View File

@ -1,6 +0,0 @@
<div id="basic-switch">
<joomla-field-switcher id="jform_sef" off-text="No" on-text="Yes" type="success">
<input name="switcher" id="sw1" value="0" type="radio" checked>
<input name="switcher" id="sw2" value="1" type="radio">
</joomla-field-switcher>
</div>

View File

@ -1,68 +0,0 @@
/**
* @package Joomla.Tests
* @subpackage JavaScript Tests
*
* @copyright Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* @since 4.0.0
* @version 1.0.0
*/
describe('Joomla switcher tests', () => {
// Set up the script
beforeEach(() => {
fixture.setBase('tests/javascript/joomla-switcher')
html_fixture = fixture.load('fixtures/fixture.html');
window.fix = html_fixture[0];
});
afterEach(() => {
fixture.cleanup();
});
describe('Joomla switcher first option preselected', () => {
it('Should have a constructor name: JoomlaSwitcherElement', () => {
const switcher = fix.querySelector('joomla-field-switcher');
expect(switcher.constructor.name).toBe('JoomlaSwitcherElement');
});
it('Joomla switcher have 5 spans inside', () => {
console.log(fix)
const switcher = fix.querySelector('joomla-field-switcher');
const container = switcher.querySelector('fieldset.switcher');
const spans = [].slice.call(switcher.querySelectorAll('span'));
expect(spans.length).toBe(4) && expect(container.length).toBe(1);
});
it('Joomla switcher have 1st span with aria-checked true', () => {
const switcher = fix.querySelector('joomla-field-switcher');
const container = switcher.querySelector('fieldset.switcher');
expect(container.getAttribute('aria-checked')).toBe('true');
});
it('Joomla switcher have 1st span to have class active', () => {
const switcher = fix.querySelector('joomla-field-switcher');
const span = switcher.querySelector('span.switcher-label-0');
expect(span.classList.contains('active')).toBe(true);
});
it('Joomla switcher have 2nd span not to have class active', () => {
const switcher = fix.querySelector('joomla-field-switcher');
const span = switcher.querySelector('span.switcher-label-1');
expect(span.classList.contains('active')).toBe(false);
});
it('Joomla switcher on click change state', () => {
const clickable = fix.querySelector('input#sw1');
const switcher = fix.querySelector('joomla-field-switcher');
const span0 = switcher.querySelector('span.switcher-label-0');
const span1 = switcher.querySelector('span.switcher-label-1');
const container = switcher.querySelector('fieldset.switcher');
clickable.click();
expect(container.getAttribute('aria-checked')).toBe('true');
expect(span0.classList.contains('active')).toBe(false);
expect(span1.classList.contains('active')).toBe(true);
});
});
});

View File

@ -19,14 +19,6 @@ module.exports = function (config) {
// Load web components polyfill
{ pattern: 'media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.js', loaded: true, served: true, watch: false },
// Load the files to test against
{ pattern: 'media/system/css/fields/joomla-field-switcher.css', loaded: true, served: true, watch: false },
{ pattern: 'media/system/js/fields/joomla-field-switcher-es5.js', loaded: true, served: true, watch: false },
// Load the tests definitions files
'tests/javascript/joomla-switcher/*.spec.js',
],
// preprocess matching files before serving them to the browser
@ -69,6 +61,9 @@ module.exports = function (config) {
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
// Currently we have no tests as we've moved switcher away. Once we add tests change this back to true
failOnEmptyTestSuite: false,
// list of plugins
plugins: [
'karma-fixture',