UNPKG

@atlassian/aui

Version:

Atlassian User Interface library

286 lines (265 loc) 9.72 kB
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;