bootstrap-switch.js 18 KB

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