bootstrap-switch.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. import jquery from 'jquery';
  2. const $ = jquery || window.jQuery || window.$;
  3. class BootstrapSwitch {
  4. constructor(element, options = {}) {
  5. this.$element = $(element);
  6. this.options = $.extend(
  7. {},
  8. $.fn.bootstrapSwitch.defaults,
  9. this._getElementOptions(),
  10. options,
  11. );
  12. this.prevOptions = {};
  13. this.$wrapper = $('<div>', {
  14. class: () => {
  15. const classes = [];
  16. classes.push(this.options.state ? 'on' : 'off');
  17. if (this.options.size) {
  18. classes.push(this.options.size);
  19. }
  20. if (this.options.disabled) {
  21. classes.push('disabled');
  22. }
  23. if (this.options.readonly) {
  24. classes.push('readonly');
  25. }
  26. if (this.options.indeterminate) {
  27. classes.push('indeterminate');
  28. }
  29. if (this.options.inverse) {
  30. classes.push('inverse');
  31. }
  32. if (this.$element.attr('id')) {
  33. classes.push(`id-${this.$element.attr('id')}`);
  34. }
  35. return classes
  36. .map(this._getClass.bind(this))
  37. .concat([this.options.baseClass], this._getClasses(this.options.wrapperClass))
  38. .join(' ');
  39. },
  40. });
  41. this.$container = $('<div>', { class: this._getClass('container') });
  42. this.$on = $('<span>', {
  43. html: this.options.onText,
  44. class: `${this._getClass('handle-on')} ${this._getClass(this.options.onColor)}`,
  45. });
  46. this.$off = $('<span>', {
  47. html: this.options.offText,
  48. class: `${this._getClass('handle-off')} ${this._getClass(this.options.offColor)}`,
  49. });
  50. this.$label = $('<span>', {
  51. html: this.options.labelText,
  52. class: this._getClass('label'),
  53. });
  54. this.$element.on('init.bootstrapSwitch', this.options.onInit.bind(this, element));
  55. this.$element.on('switchChange.bootstrapSwitch', (...args) => {
  56. if (this.options.onSwitchChange.apply(element, args) === false) {
  57. if (this.$element.is(':radio')) {
  58. $(`[name="${this.$element.attr('name')}"]`).trigger('previousState.bootstrapSwitch', true);
  59. } else {
  60. this.$element.trigger('previousState.bootstrapSwitch', true);
  61. }
  62. }
  63. });
  64. this.$container = this.$element.wrap(this.$container).parent();
  65. this.$wrapper = this.$container.wrap(this.$wrapper).parent();
  66. this.$element
  67. .before(this.options.inverse ? this.$off : this.$on)
  68. .before(this.$label)
  69. .before(this.options.inverse ? this.$on : this.$off);
  70. if (this.options.indeterminate) {
  71. this.$element.prop('indeterminate', true);
  72. }
  73. this._init();
  74. this._elementHandlers();
  75. this._handleHandlers();
  76. this._labelHandlers();
  77. this._formHandler();
  78. this._externalLabelHandler();
  79. this.$element.trigger('init.bootstrapSwitch', this.options.state);
  80. }
  81. setPrevOptions() {
  82. this.prevOptions = { ...this.options };
  83. }
  84. state(value, skip) {
  85. if (typeof value === 'undefined') { return this.options.state; }
  86. if (
  87. (this.options.disabled || this.options.readonly) ||
  88. (this.options.state && !this.options.radioAllOff && this.$element.is(':radio'))
  89. ) { return this.$element; }
  90. if (this.$element.is(':radio')) {
  91. $(`[name="${this.$element.attr('name')}"]`).trigger('setPreviousOptions.bootstrapSwitch');
  92. } else {
  93. this.$element.trigger('setPreviousOptions.bootstrapSwitch');
  94. }
  95. if (this.options.indeterminate) {
  96. this.indeterminate(false);
  97. }
  98. this.$element
  99. .prop('checked', Boolean(value))
  100. .trigger('change.bootstrapSwitch', skip);
  101. return this.$element;
  102. }
  103. toggleState(skip) {
  104. if (this.options.disabled || this.options.readonly) { return this.$element; }
  105. if (this.options.indeterminate) {
  106. this.indeterminate(false);
  107. return this.state(true);
  108. }
  109. return this.$element.prop('checked', !this.options.state).trigger('change.bootstrapSwitch', skip);
  110. }
  111. size(value) {
  112. if (typeof value === 'undefined') { return this.options.size; }
  113. if (this.options.size != null) {
  114. this.$wrapper.removeClass(this._getClass(this.options.size));
  115. }
  116. if (value) {
  117. this.$wrapper.addClass(this._getClass(value));
  118. }
  119. this._width();
  120. this._containerPosition();
  121. this.options.size = value;
  122. return this.$element;
  123. }
  124. animate(value) {
  125. if (typeof value === 'undefined') { return this.options.animate; }
  126. if (this.options.animate === Boolean(value)) { return this.$element; }
  127. return this.toggleAnimate();
  128. }
  129. toggleAnimate() {
  130. this.options.animate = !this.options.animate;
  131. this.$wrapper.toggleClass(this._getClass('animate'));
  132. return this.$element;
  133. }
  134. disabled(value) {
  135. if (typeof value === 'undefined') { return this.options.disabled; }
  136. if (this.options.disabled === Boolean(value)) { return this.$element; }
  137. return this.toggleDisabled();
  138. }
  139. toggleDisabled() {
  140. this.options.disabled = !this.options.disabled;
  141. this.$element.prop('disabled', this.options.disabled);
  142. this.$wrapper.toggleClass(this._getClass('disabled'));
  143. return this.$element;
  144. }
  145. readonly(value) {
  146. if (typeof value === 'undefined') { return this.options.readonly; }
  147. if (this.options.readonly === Boolean(value)) { return this.$element; }
  148. return this.toggleReadonly();
  149. }
  150. toggleReadonly() {
  151. this.options.readonly = !this.options.readonly;
  152. this.$element.prop('readonly', this.options.readonly);
  153. this.$wrapper.toggleClass(this._getClass('readonly'));
  154. return this.$element;
  155. }
  156. indeterminate(value) {
  157. if (typeof value === 'undefined') { return this.options.indeterminate; }
  158. if (this.options.indeterminate === Boolean(value)) { return this.$element; }
  159. return this.toggleIndeterminate();
  160. }
  161. toggleIndeterminate() {
  162. this.options.indeterminate = !this.options.indeterminate;
  163. this.$element.prop('indeterminate', this.options.indeterminate);
  164. this.$wrapper.toggleClass(this._getClass('indeterminate'));
  165. this._containerPosition();
  166. return this.$element;
  167. }
  168. inverse(value) {
  169. if (typeof value === 'undefined') { return this.options.inverse; }
  170. if (this.options.inverse === Boolean(value)) { return this.$element; }
  171. return this.toggleInverse();
  172. }
  173. toggleInverse() {
  174. this.$wrapper.toggleClass(this._getClass('inverse'));
  175. const $on = this.$on.clone(true);
  176. const $off = this.$off.clone(true);
  177. this.$on.replaceWith($off);
  178. this.$off.replaceWith($on);
  179. this.$on = $off;
  180. this.$off = $on;
  181. this.options.inverse = !this.options.inverse;
  182. return this.$element;
  183. }
  184. onColor(value) {
  185. if (typeof value === 'undefined') { return this.options.onColor; }
  186. if (this.options.onColor) {
  187. this.$on.removeClass(this._getClass(this.options.onColor));
  188. }
  189. this.$on.addClass(this._getClass(value));
  190. this.options.onColor = value;
  191. return this.$element;
  192. }
  193. offColor(value) {
  194. if (typeof value === 'undefined') { return this.options.offColor; }
  195. if (this.options.offColor) {
  196. this.$off.removeClass(this._getClass(this.options.offColor));
  197. }
  198. this.$off.addClass(this._getClass(value));
  199. this.options.offColor = value;
  200. return this.$element;
  201. }
  202. onText(value) {
  203. if (typeof value === 'undefined') { return this.options.onText; }
  204. this.$on.html(value);
  205. this._width();
  206. this._containerPosition();
  207. this.options.onText = value;
  208. return this.$element;
  209. }
  210. offText(value) {
  211. if (typeof value === 'undefined') { return this.options.offText; }
  212. this.$off.html(value);
  213. this._width();
  214. this._containerPosition();
  215. this.options.offText = value;
  216. return this.$element;
  217. }
  218. labelText(value) {
  219. if (typeof value === 'undefined') { return this.options.labelText; }
  220. this.$label.html(value);
  221. this._width();
  222. this.options.labelText = value;
  223. return this.$element;
  224. }
  225. handleWidth(value) {
  226. if (typeof value === 'undefined') { return this.options.handleWidth; }
  227. this.options.handleWidth = value;
  228. this._width();
  229. this._containerPosition();
  230. return this.$element;
  231. }
  232. labelWidth(value) {
  233. if (typeof value === 'undefined') { return this.options.labelWidth; }
  234. this.options.labelWidth = value;
  235. this._width();
  236. this._containerPosition();
  237. return this.$element;
  238. }
  239. baseClass(value) {
  240. return this.options.baseClass;
  241. }
  242. wrapperClass(value) {
  243. if (typeof value === 'undefined') { return this.options.wrapperClass; }
  244. if (!value) {
  245. value = $.fn.bootstrapSwitch.defaults.wrapperClass;
  246. }
  247. this.$wrapper.removeClass(this._getClasses(this.options.wrapperClass).join(' '));
  248. this.$wrapper.addClass(this._getClasses(value).join(' '));
  249. this.options.wrapperClass = value;
  250. return this.$element;
  251. }
  252. radioAllOff(value) {
  253. if (typeof value === 'undefined') { return this.options.radioAllOff; }
  254. const val = Boolean(value);
  255. if (this.options.radioAllOff === val) { return this.$element; }
  256. this.options.radioAllOff = val;
  257. return this.$element;
  258. }
  259. onInit(value) {
  260. if (typeof value === 'undefined') { return this.options.onInit; }
  261. if (!value) {
  262. value = $.fn.bootstrapSwitch.defaults.onInit;
  263. }
  264. this.options.onInit = value;
  265. return this.$element;
  266. }
  267. onSwitchChange(value) {
  268. if (typeof value === 'undefined') {
  269. return this.options.onSwitchChange;
  270. }
  271. if (!value) {
  272. value = $.fn.bootstrapSwitch.defaults.onSwitchChange;
  273. }
  274. this.options.onSwitchChange = value;
  275. return this.$element;
  276. }
  277. destroy() {
  278. const $form = this.$element.closest('form');
  279. if ($form.length) {
  280. $form.off('reset.bootstrapSwitch').removeData('bootstrap-switch');
  281. }
  282. this.$container
  283. .children()
  284. .not(this.$element)
  285. .remove();
  286. this.$element
  287. .unwrap()
  288. .unwrap()
  289. .off('.bootstrapSwitch')
  290. .removeData('bootstrap-switch');
  291. return this.$element;
  292. }
  293. _getElementOptions() {
  294. return {
  295. state: this.$element.is(':checked'),
  296. size: this.$element.data('size'),
  297. animate: this.$element.data('animate'),
  298. disabled: this.$element.is(':disabled'),
  299. readonly: this.$element.is('[readonly]'),
  300. indeterminate: this.$element.data('indeterminate'),
  301. inverse: this.$element.data('inverse'),
  302. radioAllOff: this.$element.data('radio-all-off'),
  303. onColor: this.$element.data('on-color'),
  304. offColor: this.$element.data('off-color'),
  305. onText: this.$element.data('on-text'),
  306. offText: this.$element.data('off-text'),
  307. labelText: this.$element.data('label-text'),
  308. handleWidth: this.$element.data('handle-width'),
  309. labelWidth: this.$element.data('label-width'),
  310. baseClass: this.$element.data('base-class'),
  311. wrapperClass: this.$element.data('wrapper-class'),
  312. };
  313. }
  314. _width() {
  315. const $handles = this.$on
  316. .add(this.$off)
  317. .add(this.$label)
  318. .css('width', '');
  319. const handleWidth = this.options.handleWidth === 'auto'
  320. ? Math.round(Math.max(this.$on.width(), this.$off.width()))
  321. : this.options.handleWidth;
  322. $handles.width(handleWidth);
  323. this.$label.width((index, width) => {
  324. if (this.options.labelWidth !== 'auto') { return this.options.labelWidth; }
  325. if (width < handleWidth) { return handleWidth; }
  326. return width;
  327. });
  328. this._handleWidth = this.$on.outerWidth();
  329. this._labelWidth = this.$label.outerWidth();
  330. this.$container.width((this._handleWidth * 2) + this._labelWidth);
  331. return this.$wrapper.width(this._handleWidth + this._labelWidth);
  332. }
  333. _containerPosition(state = this.options.state, callback) {
  334. this.$container.css('margin-left', () => {
  335. const values = [0, `-${this._handleWidth}px`];
  336. if (this.options.indeterminate) {
  337. return `-${this._handleWidth / 2}px`;
  338. }
  339. if (state) {
  340. if (this.options.inverse) {
  341. return values[1];
  342. }
  343. return values[0];
  344. }
  345. if (this.options.inverse) {
  346. return values[0];
  347. }
  348. return values[1];
  349. });
  350. }
  351. _init() {
  352. const init = () => {
  353. this.setPrevOptions();
  354. this._width();
  355. this._containerPosition();
  356. setTimeout(() => {
  357. if (this.options.animate) {
  358. return this.$wrapper.addClass(this._getClass('animate'));
  359. }
  360. }, 50);
  361. };
  362. if (this.$wrapper.is(':visible')) {
  363. init();
  364. return;
  365. }
  366. const initInterval = window.setInterval(() => {
  367. if (this.$wrapper.is(':visible')) {
  368. init();
  369. return window.clearInterval(initInterval);
  370. }
  371. }, 50);
  372. }
  373. _elementHandlers() {
  374. return this.$element.on({
  375. 'setPreviousOptions.bootstrapSwitch': this.setPrevOptions.bind(this),
  376. 'previousState.bootstrapSwitch': () => {
  377. this.options = this.prevOptions;
  378. if (this.options.indeterminate) {
  379. this.$wrapper.addClass(this._getClass('indeterminate'));
  380. }
  381. this.$element
  382. .prop('checked', this.options.state)
  383. .trigger('change.bootstrapSwitch', true);
  384. },
  385. 'change.bootstrapSwitch': (event, skip) => {
  386. event.preventDefault();
  387. event.stopImmediatePropagation();
  388. const state = this.$element.is(':checked');
  389. this._containerPosition(state);
  390. if (state === this.options.state) {
  391. return;
  392. }
  393. this.options.state = state;
  394. this.$wrapper
  395. .toggleClass(this._getClass('off'))
  396. .toggleClass(this._getClass('on'));
  397. if (!skip) {
  398. if (this.$element.is(':radio')) {
  399. $(`[name="${this.$element.attr('name')}"]`)
  400. .not(this.$element)
  401. .prop('checked', false)
  402. .trigger('change.bootstrapSwitch', true);
  403. }
  404. this.$element.trigger('switchChange.bootstrapSwitch', [state]);
  405. }
  406. },
  407. 'focus.bootstrapSwitch': (event) => {
  408. event.preventDefault();
  409. this.$wrapper.addClass(this._getClass('focused'));
  410. },
  411. 'blur.bootstrapSwitch': (event) => {
  412. event.preventDefault();
  413. this.$wrapper.removeClass(this._getClass('focused'));
  414. },
  415. 'keydown.bootstrapSwitch': (event) => {
  416. if (!event.which || this.options.disabled || this.options.readonly) {
  417. return;
  418. }
  419. if (event.which === 37 || event.which === 39) {
  420. event.preventDefault();
  421. event.stopImmediatePropagation();
  422. this.state(event.which === 39);
  423. }
  424. },
  425. });
  426. }
  427. _handleHandlers() {
  428. this.$on.on('click.bootstrapSwitch', (event) => {
  429. event.preventDefault();
  430. event.stopPropagation();
  431. this.state(false);
  432. return this.$element.trigger('focus.bootstrapSwitch');
  433. });
  434. return this.$off.on('click.bootstrapSwitch', (event) => {
  435. event.preventDefault();
  436. event.stopPropagation();
  437. this.state(true);
  438. return this.$element.trigger('focus.bootstrapSwitch');
  439. });
  440. }
  441. _labelHandlers() {
  442. const handlers = {
  443. click(event) { event.stopPropagation(); },
  444. 'mousedown.bootstrapSwitch touchstart.bootstrapSwitch': (event) => {
  445. if (this._dragStart || this.options.disabled || this.options.readonly) {
  446. return;
  447. }
  448. event.preventDefault();
  449. event.stopPropagation();
  450. this._dragStart = (event.pageX || event.originalEvent.touches[0].pageX) - parseInt(this.$container.css('margin-left'), 10);
  451. if (this.options.animate) {
  452. this.$wrapper.removeClass(this._getClass('animate'));
  453. }
  454. this.$element.trigger('focus.bootstrapSwitch');
  455. },
  456. 'mousemove.bootstrapSwitch touchmove.bootstrapSwitch': (event) => {
  457. if (this._dragStart == null) { return; }
  458. const difference = (event.pageX || event.originalEvent.touches[0].pageX) - this._dragStart;
  459. event.preventDefault();
  460. if (difference < -this._handleWidth || difference > 0) { return; }
  461. this._dragEnd = difference;
  462. this.$container.css('margin-left', `${this._dragEnd}px`);
  463. },
  464. 'mouseup.bootstrapSwitch touchend.bootstrapSwitch': (event) => {
  465. if (!this._dragStart) { return; }
  466. event.preventDefault();
  467. if (this.options.animate) {
  468. this.$wrapper.addClass(this._getClass('animate'));
  469. }
  470. if (this._dragEnd) {
  471. const state = this._dragEnd > -(this._handleWidth / 2);
  472. this._dragEnd = false;
  473. this.state(this.options.inverse ? !state : state);
  474. } else {
  475. this.state(!this.options.state);
  476. }
  477. this._dragStart = false;
  478. },
  479. 'mouseleave.bootstrapSwitch': () => {
  480. this.$label.trigger('mouseup.bootstrapSwitch');
  481. },
  482. };
  483. this.$label.on(handlers);
  484. }
  485. _externalLabelHandler() {
  486. const $externalLabel = this.$element.closest('label');
  487. $externalLabel.on('click', (event) => {
  488. event.preventDefault();
  489. event.stopImmediatePropagation();
  490. if (event.target === $externalLabel[0]) {
  491. this.toggleState();
  492. }
  493. });
  494. }
  495. _formHandler() {
  496. const $form = this.$element.closest('form');
  497. if ($form.data('bootstrap-switch')) {
  498. return;
  499. }
  500. $form
  501. .on('reset.bootstrapSwitch', () => {
  502. window.setTimeout(() => {
  503. $form.find('input')
  504. .filter(function () { return $(this).data('bootstrap-switch'); })
  505. .each(function () { return $(this).bootstrapSwitch('state', this.checked); });
  506. }, 1);
  507. })
  508. .data('bootstrap-switch', true);
  509. }
  510. _getClass(name) {
  511. return `${this.options.baseClass}-${name}`;
  512. }
  513. _getClasses(classes) {
  514. if (!$.isArray(classes)) {
  515. return [this._getClass(classes)];
  516. }
  517. return classes.map(this._getClass.bind(this));
  518. }
  519. }
  520. $.fn.bootstrapSwitch = function (option, ...args) {
  521. function reducer(ret, next) {
  522. const $this = $(next);
  523. const existingData = $this.data('bootstrap-switch');
  524. const data = existingData || new BootstrapSwitch(next, option);
  525. if (!existingData) {
  526. $this.data('bootstrap-switch', data);
  527. }
  528. if (typeof option === 'string') {
  529. return data[option](...args);
  530. }
  531. return ret;
  532. }
  533. return Array.prototype.reduce.call(this, reducer, this);
  534. };
  535. $.fn.bootstrapSwitch.Constructor = BootstrapSwitch;
  536. $.fn.bootstrapSwitch.defaults = {
  537. state: true,
  538. size: null,
  539. animate: true,
  540. disabled: false,
  541. readonly: false,
  542. indeterminate: false,
  543. inverse: false,
  544. radioAllOff: false,
  545. onColor: 'primary',
  546. offColor: 'default',
  547. onText: 'ON',
  548. offText: 'OFF',
  549. labelText: '&nbsp',
  550. handleWidth: 'auto',
  551. labelWidth: 'auto',
  552. baseClass: 'bootstrap-switch',
  553. wrapperClass: 'wrapper',
  554. onInit: () => {},
  555. onSwitchChange: () => {},
  556. };