bootstrap-switch.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  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 (!Array.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. const changeState = this.options.onSwitchChange.apply(element, args);
  280. if (changeState === false) {
  281. if (this.$element.is(':radio')) {
  282. $(`[name="${this.$element.attr('name')}"]`).trigger('previousState.bootstrapSwitch', true);
  283. } else {
  284. this.$element.trigger('previousState.bootstrapSwitch', true);
  285. }
  286. }
  287. });
  288. this.$container = this.$element.wrap(this.$container).parent();
  289. this.$wrapper = this.$container.wrap(this.$wrapper).parent();
  290. this.$element
  291. .before(this.options.inverse ? this.$off : this.$on)
  292. .before(this.$label)
  293. .before(this.options.inverse ? this.$on : this.$off);
  294. if (this.options.indeterminate) {
  295. this.$element.prop('indeterminate', true);
  296. }
  297. this::prvinit();
  298. this::prvelementHandlers();
  299. this::prvhandleHandlers();
  300. this::prvlabelHandlers();
  301. this::prvformHandler();
  302. this::prvexternalLabelHandler();
  303. this.$element.trigger('init.bootstrapSwitch', this.options.state);
  304. }
  305. setPrevOptions() {
  306. this.prevOptions = { ...this.options };
  307. }
  308. state(value, skip) {
  309. if (typeof value === 'undefined') { return this.options.state; }
  310. if (
  311. (this.options.disabled || this.options.readonly) ||
  312. (this.options.state && !this.options.radioAllOff && this.$element.is(':radio'))
  313. ) { return this.$element; }
  314. if (this.$element.is(':radio')) {
  315. $(`[name="${this.$element.attr('name')}"]`).trigger('setPreviousOptions.bootstrapSwitch');
  316. } else {
  317. this.$element.trigger('setPreviousOptions.bootstrapSwitch');
  318. }
  319. if (this.options.indeterminate) {
  320. this.indeterminate(false);
  321. }
  322. this.$element
  323. .prop('checked', Boolean(value))
  324. .trigger('change.bootstrapSwitch', skip);
  325. return this.$element;
  326. }
  327. toggleState(skip) {
  328. if (this.options.disabled || this.options.readonly) { return this.$element; }
  329. if (this.options.indeterminate) {
  330. this.indeterminate(false);
  331. return this.state(true);
  332. }
  333. return this.$element.prop('checked', !this.options.state).trigger('change.bootstrapSwitch', skip);
  334. }
  335. size(value) {
  336. if (typeof value === 'undefined') { return this.options.size; }
  337. if (this.options.size != null) {
  338. this.$wrapper.removeClass(this::prvgetClass(this.options.size));
  339. }
  340. if (value) {
  341. this.$wrapper.addClass(this::prvgetClass(value));
  342. }
  343. this::prvwidth();
  344. this::prvcontainerPosition();
  345. this.options.size = value;
  346. return this.$element;
  347. }
  348. animate(value) {
  349. if (typeof value === 'undefined') { return this.options.animate; }
  350. if (this.options.animate === Boolean(value)) { return this.$element; }
  351. return this.toggleAnimate();
  352. }
  353. toggleAnimate() {
  354. this.options.animate = !this.options.animate;
  355. this.$wrapper.toggleClass(this::prvgetClass('animate'));
  356. return this.$element;
  357. }
  358. disabled(value) {
  359. if (typeof value === 'undefined') { return this.options.disabled; }
  360. if (this.options.disabled === Boolean(value)) { return this.$element; }
  361. return this.toggleDisabled();
  362. }
  363. toggleDisabled() {
  364. this.options.disabled = !this.options.disabled;
  365. this.$element.prop('disabled', this.options.disabled);
  366. this.$wrapper.toggleClass(this::prvgetClass('disabled'));
  367. return this.$element;
  368. }
  369. readonly(value) {
  370. if (typeof value === 'undefined') { return this.options.readonly; }
  371. if (this.options.readonly === Boolean(value)) { return this.$element; }
  372. return this.toggleReadonly();
  373. }
  374. toggleReadonly() {
  375. this.options.readonly = !this.options.readonly;
  376. this.$element.prop('readonly', this.options.readonly);
  377. this.$wrapper.toggleClass(this::prvgetClass('readonly'));
  378. return this.$element;
  379. }
  380. indeterminate(value) {
  381. if (typeof value === 'undefined') { return this.options.indeterminate; }
  382. if (this.options.indeterminate === Boolean(value)) { return this.$element; }
  383. return this.toggleIndeterminate();
  384. }
  385. toggleIndeterminate() {
  386. this.options.indeterminate = !this.options.indeterminate;
  387. this.$element.prop('indeterminate', this.options.indeterminate);
  388. this.$wrapper.toggleClass(this::prvgetClass('indeterminate'));
  389. this::prvcontainerPosition();
  390. return this.$element;
  391. }
  392. inverse(value) {
  393. if (typeof value === 'undefined') { return this.options.inverse; }
  394. if (this.options.inverse === Boolean(value)) { return this.$element; }
  395. return this.toggleInverse();
  396. }
  397. toggleInverse() {
  398. this.$wrapper.toggleClass(this::prvgetClass('inverse'));
  399. const $on = this.$on.clone(true);
  400. const $off = this.$off.clone(true);
  401. this.$on.replaceWith($off);
  402. this.$off.replaceWith($on);
  403. this.$on = $off;
  404. this.$off = $on;
  405. this.options.inverse = !this.options.inverse;
  406. return this.$element;
  407. }
  408. onColor(value) {
  409. if (typeof value === 'undefined') { return this.options.onColor; }
  410. if (this.options.onColor) {
  411. this.$on.removeClass(this::prvgetClass(this.options.onColor));
  412. }
  413. this.$on.addClass(this::prvgetClass(value));
  414. this.options.onColor = value;
  415. return this.$element;
  416. }
  417. offColor(value) {
  418. if (typeof value === 'undefined') { return this.options.offColor; }
  419. if (this.options.offColor) {
  420. this.$off.removeClass(this::prvgetClass(this.options.offColor));
  421. }
  422. this.$off.addClass(this::prvgetClass(value));
  423. this.options.offColor = value;
  424. return this.$element;
  425. }
  426. onText(value) {
  427. if (typeof value === 'undefined') { return this.options.onText; }
  428. this.$on.html(value);
  429. this::prvwidth();
  430. this::prvcontainerPosition();
  431. this.options.onText = value;
  432. return this.$element;
  433. }
  434. offText(value) {
  435. if (typeof value === 'undefined') { return this.options.offText; }
  436. this.$off.html(value);
  437. this::prvwidth();
  438. this::prvcontainerPosition();
  439. this.options.offText = value;
  440. return this.$element;
  441. }
  442. labelText(value) {
  443. if (typeof value === 'undefined') { return this.options.labelText; }
  444. this.$label.html(value);
  445. this::prvwidth();
  446. this.options.labelText = value;
  447. return this.$element;
  448. }
  449. handleWidth(value) {
  450. if (typeof value === 'undefined') { return this.options.handleWidth; }
  451. this.options.handleWidth = value;
  452. this::prvwidth();
  453. this::prvcontainerPosition();
  454. return this.$element;
  455. }
  456. labelWidth(value) {
  457. if (typeof value === 'undefined') { return this.options.labelWidth; }
  458. this.options.labelWidth = value;
  459. this::prvwidth();
  460. this::prvcontainerPosition();
  461. return this.$element;
  462. }
  463. baseClass() {
  464. return this.options.baseClass;
  465. }
  466. wrapperClass(value) {
  467. if (typeof value === 'undefined') { return this.options.wrapperClass; }
  468. const wrapperClass = value || $.fn.bootstrapSwitch.defaults.wrapperClass;
  469. this.$wrapper.removeClass(this::prvgetClasses(this.options.wrapperClass).join(' '));
  470. this.$wrapper.addClass(this::prvgetClasses(wrapperClass).join(' '));
  471. this.options.wrapperClass = wrapperClass;
  472. return this.$element;
  473. }
  474. radioAllOff(value) {
  475. if (typeof value === 'undefined') { return this.options.radioAllOff; }
  476. const val = Boolean(value);
  477. if (this.options.radioAllOff === val) { return this.$element; }
  478. this.options.radioAllOff = val;
  479. return this.$element;
  480. }
  481. onInit(value) {
  482. if (typeof value === 'undefined') { return this.options.onInit; }
  483. this.options.onInit = value || $.fn.bootstrapSwitch.defaults.onInit;
  484. return this.$element;
  485. }
  486. onSwitchChange(value) {
  487. if (typeof value === 'undefined') {
  488. return this.options.onSwitchChange;
  489. }
  490. this.options.onSwitchChange =
  491. value || $.fn.bootstrapSwitch.defaults.onSwitchChange;
  492. return this.$element;
  493. }
  494. destroy() {
  495. const $form = this.$element.closest('form');
  496. if ($form.length) {
  497. $form.off('reset.bootstrapSwitch').removeData('bootstrap-switch');
  498. }
  499. this.$container
  500. .children()
  501. .not(this.$element)
  502. .remove();
  503. this.$element
  504. .unwrap()
  505. .unwrap()
  506. .off('.bootstrapSwitch')
  507. .removeData('bootstrap-switch');
  508. return this.$element;
  509. }
  510. }
  511. function bootstrapSwitch(option, ...args) {
  512. function reducer(ret, next) {
  513. const $this = $(next);
  514. const existingData = $this.data('bootstrap-switch');
  515. const data = existingData || new BootstrapSwitch(next, option);
  516. if (!existingData) {
  517. $this.data('bootstrap-switch', data);
  518. }
  519. if (typeof option === 'string') {
  520. return data[option](...args);
  521. }
  522. return ret;
  523. }
  524. return Array.prototype.reduce.call(this, reducer, this);
  525. }
  526. $.fn.bootstrapSwitch = bootstrapSwitch;
  527. $.fn.bootstrapSwitch.Constructor = BootstrapSwitch;
  528. $.fn.bootstrapSwitch.defaults = {
  529. state: true,
  530. size: null,
  531. animate: true,
  532. disabled: false,
  533. readonly: false,
  534. indeterminate: false,
  535. inverse: false,
  536. radioAllOff: false,
  537. onColor: 'primary',
  538. offColor: 'default',
  539. onText: 'ON',
  540. offText: 'OFF',
  541. labelText: '&nbsp',
  542. handleWidth: 'auto',
  543. labelWidth: 'auto',
  544. baseClass: 'bootstrap-switch',
  545. wrapperClass: 'wrapper',
  546. onInit: () => {},
  547. onSwitchChange: () => {},
  548. };