@atlassian/aui
Version:
Atlassian User Interface library
286 lines (265 loc) • 9.72 kB
JavaScript
import './spin';
import './tooltip';
import $ from './jquery';
import { setBooleanAttribute } from './internal/attributes';
import { supportsFocusWithin } from './internal/browser';
import enforce from './internal/enforcer';
import keyCode from './key-code';
import skateTemplateHtml from 'skatejs-template-html';
import skate from './internal/skate';
import { INPUT_SUFFIX } from './internal/constants';
import CustomEvent from './polyfills/custom-event';
import SpinnerEl, { SIZE } from './spinner';
import { I18n } from './i18n';
function fireChangeEvent(element) {
if (element._canFireEventsNow) {
element.dispatchEvent(new CustomEvent('change', { bubbles: true }));
}
}
function getInput(element) {
return element._input || (element._input = element.querySelector('input'));
}
function getSpinner(element) {
return element._spinner || (element._spinner = new SpinnerEl());
}
function removedAttributeHandler(attributeName, element) {
getInput(element).removeAttribute(attributeName);
}
function fallbackAttributeHandler(attributeName, element, change) {
getInput(element).setAttribute(attributeName, change.newValue);
}
function getAttributeHandler(attributeName) {
return {
removed: removedAttributeHandler.bind(this, attributeName),
fallback: fallbackAttributeHandler.bind(this, attributeName),
};
}
const formAttributeHandler = {
removed: function (element) {
removedAttributeHandler.call(this, 'form', element);
element._formId = null;
},
fallback: function (element, change) {
fallbackAttributeHandler.call(this, 'form', element, change);
element._formId = change.newValue;
},
};
const idAttributeHandler = {
removed: removedAttributeHandler.bind(this, 'id'),
fallback: function (element, change) {
const val = `${change.newValue}${INPUT_SUFFIX}`;
getInput(element).setAttribute('id', val);
},
};
const valueAttributeHandler = {
removed: function (element) {
removedAttributeHandler.call(this, 'value', element);
// Internet Explorer 11 has a bug where it doesn't clear out the previous value
// when the attribute is removed.
getInput(element).value = 'on';
},
fallback: function (element, change) {
fallbackAttributeHandler.call(this, 'value', element, change);
},
};
const checkedAttributeHandler = {
removed: function (element) {
getInput(element).checked = false;
},
fallback: function (element) {
getInput(element).checked = true;
},
};
const labelHandler = {
removed: function (element) {
getInput(element).removeAttribute('aria-label');
},
fallback: function (element, change) {
getInput(element).setAttribute('aria-label', change.newValue);
},
};
function clickHandler(element, e) {
const input = getInput(element);
if (!element.disabled && !element.busy && e.target !== input) {
input.checked = !input.checked;
fireChangeEvent(element);
}
setBooleanAttribute(element, 'checked', input.checked);
}
function setDisabledForLabels(element, disabled) {
if (!element.id) {
return;
}
Array.prototype.forEach.call(
document.querySelectorAll(`aui-label[for="${element.id}"]`),
function (el) {
el.disabled = disabled;
}
);
}
/**
* Workaround to prevent pressing SPACE on busy state.
* Preventing click event still makes the toggle flip and revert back.
* So on CSS side, the input has "pointer-events: none" on busy state.
*/
function bindEventsToInput(element) {
getInput(element).addEventListener('keydown', function (e) {
if (element.busy && e.keyCode === keyCode.SPACE) {
e.preventDefault();
}
});
// prevent toggle can be trigger through SPACE key on Firefox
if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
getInput(element).addEventListener('click', function (e) {
if (element.busy) {
e.preventDefault();
}
});
}
// support focus-within manually when necessary
if (!supportsFocusWithin()) {
element._input.addEventListener('focus', () => element.classList.add('active'));
element._input.addEventListener('blur', () => element.classList.remove('active'));
}
}
const ToggleEl = skate('aui-toggle', {
// "assistive" class avoids direct interaction with the <input> element
// (which prevents our click handler from being called),
// while allow the element to still participate in the form.
template: skateTemplateHtml(
'<input type="checkbox" role="switch" class="aui-toggle-input assistive">',
'<span class="aui-toggle-view">',
'<span class="aui-toggle-tick aui-icon aui-icon-small aui-iconfont-success"></span>',
'<span class="aui-toggle-cross aui-icon aui-icon-small aui-iconfont-close-dialog"></span>',
'</span>'
),
created: function (element) {
getInput(element); // avoid using _input in attribute handlers
getSpinner(element).setAttribute('size', SIZE.SMALL.name);
$(getInput(element)).tooltip({
title: function () {
return this.checked
? this.getAttribute('tooltip-on')
: this.getAttribute('tooltip-off');
},
gravity: 's',
hoverable: false,
});
bindEventsToInput(element);
if (element.hasAttribute('checked')) {
getInput(element).setAttribute('checked', '');
}
element._canFireEventsNow = true;
},
attached: function (element) {
enforce(element).attributeExists('label');
},
events: {
click: clickHandler,
},
attributes: {
'id': idAttributeHandler,
'checked': checkedAttributeHandler,
'disabled': getAttributeHandler('disabled'),
'form': formAttributeHandler,
'name': getAttributeHandler('name'),
'value': valueAttributeHandler,
'tooltip-on': {
value: I18n.getText('aui.toggle.on'),
fallback: function (element, change) {
getInput(element).setAttribute(
'tooltip-on',
change.newValue || I18n.getText('aui.toggle.on')
);
},
},
'tooltip-off': {
value: I18n.getText('aui.toggle.off'),
fallback: function (element, change) {
getInput(element).setAttribute(
'tooltip-off',
change.newValue || I18n.getText('aui.toggle.off')
);
},
},
'label': labelHandler,
},
prototype: {
focus: function () {
getInput(this).focus();
return this;
},
get checked() {
return getInput(this).checked;
},
set checked(value) {
// Need to explicitly set the property on the checkbox because the
// checkbox's property doesn't change with it's attribute after it
// is clicked.
if (getInput(this).checked !== value) {
getInput(this).checked = value;
setBooleanAttribute(this, 'checked', value);
}
},
get disabled() {
// AUI-4958 - this may be accessed by a jQuery event handler in response to
// a DOMNodeInserted event being fired. In this scenario, the `template`
// function has been called by skate, but the `created` callback has not.
return getInput(this).disabled;
},
set disabled(value) {
return setBooleanAttribute(this, 'disabled', value);
},
get form() {
return document.getElementById(this._formId);
},
set form(value) {
formAttributeHandler.fallback.call(this, this, { newValue: value || null });
return this.form;
},
get name() {
return getInput(this).name;
},
set name(value) {
this.setAttribute('name', value);
return value;
},
get value() {
return getInput(this).value;
},
set value(value) {
// Setting the value of an input to null sets it to empty string.
let newVal = value === null ? '' : value;
this.setAttribute('value', newVal);
return newVal;
},
get busy() {
return getInput(this).getAttribute('aria-busy') === 'true';
},
set busy(value) {
const input = getInput(this);
const spinner = getSpinner(this);
setBooleanAttribute(this, 'busy', value);
if (value) {
input.setAttribute('aria-busy', 'true');
input.indeterminate = true;
if (this.checked) {
input.classList.add('indeterminate-checked');
$(this.querySelector('.aui-toggle-tick')).append(spinner);
} else {
$(this.querySelector('.aui-toggle-cross')).append(spinner);
}
} else {
input.classList.remove('indeterminate-checked');
input.indeterminate = false;
input.removeAttribute('aria-busy');
if (spinner.parentNode) {
spinner.parentNode.removeChild(this._spinner);
}
}
setDisabledForLabels(this, !!value);
return value;
},
},
});
export default ToggleEl;