bootstrap-switch.js 19 KB

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