Sleazy Fork is available in English.

Chaturbate Easy Tipping Fixed Version

Adds a new tipping popup and modifies the existing one

  1. // ==UserScript==
  2. // @name Chaturbate Easy Tipping Fixed Version
  3. // @namespace madTipper
  4. // @version 0.13.1
  5. // @author omgmikey - fixed on 1/21/2018 by tblopgreg
  6. // @match https://es.chaturbate.com/*
  7. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
  8. // @require https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @license MIT
  13. // @run-at document-idle
  14. // @description Adds a new tipping popup and modifies the existing one
  15. // ==/UserScript==
  16.  
  17. var CSS_GREY = {'color': 'rgb(88,141,61)'};
  18. var CSS_WHITE = {'color': '#FFFFFF'};
  19. var CSS_BLACK = {'color': '#000000'};
  20.  
  21. var ID_PREFIX = '#madTipper'
  22. var CLASS_PREFIX = '.madTipper'
  23. var CLASS_INPUT = CLASS_PREFIX + '_input';
  24.  
  25. var HTML_IDS = {
  26. 'BUTTON': 'button',
  27. 'POPUP': 'popup',
  28. 'AMOUNT': 'amount',
  29. 'COUNT': 'count',
  30. 'INTERVAL': 'interval',
  31. 'VARIANCE_LOWER': 'variance_lower',
  32. 'VARIANCE_UPPER': 'variance_upper',
  33.  
  34. 'START': 'start',
  35. 'STOP': 'stop',
  36. 'TOTAL': 'total',
  37. 'ETA': 'eta'
  38. }
  39.  
  40. for (var key in HTML_IDS) {
  41. HTML_IDS[key] = ID_PREFIX + '_' + HTML_IDS[key];
  42. }
  43.  
  44. var shell = $('.tip_shell');
  45. var tipsLeft = 0;
  46. var tipFunctionTimeout = null;
  47. var juration = loadJuration();
  48.  
  49. (function initialize() {
  50.  
  51. createTipperButton();
  52. createTipperPopup();
  53. injectCSS();
  54. improveDefaultTipPopup();
  55.  
  56. loadPreviousSettings();
  57. initializeButtonCallbacks();
  58. updateTipperButton();
  59. })();
  60.  
  61. function createTipperButton() {
  62.  
  63. shell.append('<div id="madTipper_button_bg"><a href="#" id="madTipper_button"></a></div>');
  64. }
  65.  
  66. function updateTipperButton() {
  67.  
  68. if (tipsLeft == 0) {
  69. $(HTML_IDS['BUTTON']).html('MAD TIPPER').css({'width': '80px'});
  70. }
  71. else {
  72. $(HTML_IDS['BUTTON']).html('MAD TIPPER (' + tipsLeft + ')').css({'width': '120px'});
  73. }
  74. }
  75.  
  76. function createTipperPopup() {
  77.  
  78. shell.append(
  79. '<div class="overlay_popup" id="madTipper_popup">' +
  80. '<table width="100%" border="0" cellspacing="0" cellpadding="0">' +
  81. '<tbody>' +
  82. '<tr>' +
  83. '<td class="formborder">' +
  84. '<div class="title">Mad Tipper</div>' +
  85. '<div class="body">' +
  86. '<form>' +
  87. '<label>Amount per tip:</label><br >' +
  88. '<input type="text" id="madTipper_amount" class="madTipper_input">' +
  89. '<br />' +
  90.  
  91. '<label>Number of tips:</label><br >' +
  92. '<input type="text" id="madTipper_count" class="madTipper_input">' +
  93. '<br /><hr />' +
  94.  
  95. '<label>Interval:</label><br >' +
  96. '<input type="text" id="madTipper_interval" class="madTipper_input">' +
  97. '<br />' +
  98.  
  99. '<label>Interval variance lower (optional):</label><br >' +
  100. '<input type="text" id="madTipper_variance_lower" class="madTipper_input">' +
  101. '<br />' +
  102.  
  103. '<label>Interval variance upper (optional):</label><br >' +
  104. '<input type="text" id="madTipper_variance_upper" class="madTipper_input">' +
  105. '<br /><hr />' +
  106.  
  107. 'Total tip: ' + '<a id="madTipper_total"></a>' +
  108. '<br />' +
  109. 'Estimated duration: ' + '<a id="madTipper_eta"></a>' +
  110. '</form>' +
  111. '<hr />' +
  112. '<button id="madTipper_start">Start</button>' +
  113. '<button id="madTipper_stop" disabled="disabled">Stop</button>' +
  114. '</div>' +
  115. '</td>' +
  116. '</tr>' +
  117. '</tbody>' +
  118. '</table>' +
  119. '</div>'
  120. );
  121. }
  122.  
  123. function injectCSS() {
  124.  
  125. var buttonBackgroundUrl =
  126. 'url("https://ssl-ccstatic.highwebmedia.com/images/btn-sprites2.gif?ac5eba7d5cf3") no-repeat right';
  127.  
  128. var buttonFontFamily =
  129. 'UbuntuMedium,Arial,Helvetica,sans-serif';
  130.  
  131. var genericButtonCSS = {
  132. 'height':'21px',
  133. 'width':'100px',
  134. 'padding-left':'10px',
  135. 'margin-right':'10px',
  136. 'font-size':'12px',
  137. 'text-shadow':'1px 1px 0 #588d3d',
  138. 'color': '#FFFFFF'
  139. };
  140.  
  141. genericButtonCSS['font-family'] = buttonFontFamily;
  142. genericButtonCSS['background'] = buttonBackgroundUrl + ' -84px';
  143.  
  144. var mainButtonCSS = {
  145. 'position': 'absolute',
  146. 'z-index': 1000,
  147. 'left': '500px',
  148. 'top': '30px',
  149. 'height': '18px',
  150. 'padding': '3px 10px 0 0',
  151. 'text-decoration': 'none',
  152. 'text-align': 'center',
  153. 'width': '80px'
  154. }
  155.  
  156. for (var key in genericButtonCSS) {
  157. if (mainButtonCSS[key] === undefined) {
  158. mainButtonCSS[key] = genericButtonCSS[key];
  159. }
  160. }
  161.  
  162. $(HTML_IDS['BUTTON']).css(mainButtonCSS);
  163.  
  164. $(CLASS_INPUT).css({
  165. 'width': 'auto',
  166. 'margin-bottom': '10px'
  167. });
  168.  
  169. $(HTML_IDS['POPUP']).css({
  170. 'position': 'absolute',
  171. 'z-index': 1000,
  172. 'width': '280px',
  173. 'top': '-456px',
  174. 'left': '452px',
  175. 'display': 'none'
  176. }).draggable();
  177.  
  178. $(HTML_IDS['POPUP'] + ' .formborder').css({
  179. 'border-bottom': '2px solid #0b5d81',
  180. 'height': '420px'
  181. });
  182.  
  183. $(HTML_IDS['START']).css(genericButtonCSS);
  184. genericButtonCSS['background'] = buttonBackgroundUrl + ' -42px';
  185.  
  186. delete genericButtonCSS['color'];
  187. $(HTML_IDS['STOP']).css(genericButtonCSS);
  188. }
  189.  
  190. function improveDefaultTipPopup() {
  191.  
  192. $('.overlay_popup.tip_popup').css({
  193. 'top': '-240px'
  194. }).draggable();
  195.  
  196. $('#tip_message').css({
  197. 'margin-bottom': '20px'
  198. })
  199. .append('<input type="checkbox" class="float_right" id="tip_keepopen"></input><br />')
  200. .append('<br /><label class="float_right" for="tip_keepopen">Keep this window open after tipping</label>');
  201.  
  202. $('.float_right').css({
  203. 'float': 'right'
  204. });
  205.  
  206. setPopupHeight('250px');
  207.  
  208. var tipPopup = $('.tip_popup');
  209. var keepOpenCheckbox = $('#tip_keepopen');
  210. var popupIsForcedOpen = false;
  211.  
  212. /* use CB jquery to ensure correct callback execution order */
  213. var tipPopupForm = defchat_settings.domroot.find('.tip_popup form');
  214. tipPopupForm.submit(onFormSubmit);
  215.  
  216. keepOpenCheckbox.css({
  217. 'margin-top': '10px'
  218. });
  219.  
  220. $('body').click(function(ev) {
  221.  
  222. if ($('.tip_button').is(ev.target)) {
  223. popupIsForcedOpen = false;
  224. return;
  225. }
  226.  
  227. if (!popupIsForcedOpen || tipPopup.has(ev.target).length) {
  228. return;
  229. }
  230.  
  231. if (tipPopup.is(':visible')) {
  232. tipPopup.hide();
  233. }
  234.  
  235. popupIsForcedOpen = false;
  236. });
  237.  
  238. function onFormSubmit() {
  239.  
  240. setPopupHeight('270px');
  241.  
  242. if (!keepOpenCheckbox.is(':checked')) {
  243. return;
  244. }
  245.  
  246. if (!tipPopup.is(':visible')) {
  247. tipPopup.show();
  248. popupIsForcedOpen = true;
  249. }
  250. }
  251.  
  252. function setPopupHeight(value) {
  253. $('.overlay_popup.tip_popup .formborder').css({
  254. 'height': value,
  255. });
  256. }
  257. }
  258.  
  259. function startTipping() {
  260.  
  261. var err = verifyTipperFields();
  262.  
  263. if (err) {
  264. alert(err);
  265. stopTipping();
  266. return;
  267. }
  268.  
  269. saveCurrentSettings();
  270.  
  271. $(HTML_IDS['START']).prop('disabled', true).css(CSS_GREY);
  272. $(HTML_IDS['STOP']).prop('disabled', false).css(CSS_WHITE);
  273. $(CLASS_INPUT).prop('disabled', true).css(CSS_GREY);
  274.  
  275. tipsLeft = getTipCount();
  276.  
  277. /* we really want to send the first one immediately */
  278. sendTip();
  279.  
  280. if (tipsLeft > 0) {
  281. chainQueueTips();
  282. }
  283. }
  284.  
  285. function verifyTipperFields() {
  286.  
  287. function isInt(value) {
  288. var regex = /^[0-9]+$/;
  289. return regex.test(String(value));
  290. }
  291.  
  292. function isDuration(value) {
  293. try {
  294. juration.parse(value);
  295. return true;
  296. }
  297. catch(ex) {
  298. return false;
  299. }
  300. }
  301.  
  302. function isDurationOrEmpty(value) {
  303. return value === '' || isDuration(value);
  304. }
  305.  
  306. if (!isInt(getTipAmountRaw()) || getTipAmount() <= 0) {
  307. return 'Tip amount field should be a positive integer.';
  308. }
  309.  
  310. if (!isInt(getTipCountRaw()) || getTipCount() <= 0) {
  311. return 'Tip count field should be a positive integer.';
  312. }
  313.  
  314. if (!isDuration(getTipInterval())) {
  315. return 'Tip interval should contain a duration. E.g.: "2.5s", "1", "2min"';
  316. }
  317.  
  318. if (!isDurationOrEmpty(getVarianceLowerRaw()) || !isDurationOrEmpty(getVarianceUpperRaw())) {
  319. return 'Variance fields should contain durations, or be left blank. E.g.: "", "2.5s"';
  320. }
  321. }
  322.  
  323. function getSleepInterval() {
  324.  
  325. var interval = juration.parse(getTipInterval());
  326. var lower_bound = interval - getVarianceLower();
  327. var upper_bound = interval + getVarianceUpper();
  328.  
  329. return getRandomNumber(lower_bound, upper_bound) * 1000;
  330. }
  331.  
  332. function getRandomNumber(min, max) {
  333.  
  334. return Math.random() * (max - min) + min;
  335. }
  336.  
  337. function chainQueueTips() {
  338.  
  339. var sleepTime = getSleepInterval();
  340.  
  341. tipFunctionTimeout = setTimeout(function() {
  342. sendTip(chainQueueTips);
  343. }, sleepTime);
  344. }
  345.  
  346. function sendTip(queueNextTipFn) {
  347.  
  348. var queryUrl = $('.tip_popup form').attr('action');
  349.  
  350. var queryParams = $.param({
  351. 'csrfmiddlewaretoken': $.cookie('csrftoken'),
  352. 'tip_amount': getTipAmount(),
  353. 'message': '',
  354. 'tip_room_type': $('#id_tip_room_type').val(),
  355. 'tip_v': defchat_settings.v_tip_vol,
  356. });
  357.  
  358. $.ajax({
  359. url: queryUrl,
  360. data: queryParams,
  361. dataType: 'json',
  362. type: 'post',
  363. success: function(response) {
  364. if (response.error) {
  365. alert(response.error);
  366. stopTipping();
  367. }
  368. }
  369. });
  370.  
  371. updateTipsLeft();
  372.  
  373. if (tipsLeft === 0) {
  374. stopTipping();
  375. }
  376. else if (queueNextTipFn) {
  377. queueNextTipFn();
  378. }
  379. }
  380.  
  381. function updateTipsLeft() {
  382.  
  383. tipsLeft--;
  384. updateTipperButton();
  385. }
  386.  
  387. function stopTipping() {
  388.  
  389. clearTimeout(tipFunctionTimeout);
  390. tipFunctionTimeout = null;
  391.  
  392. tipsLeft = 0;
  393. updateTipperButton();
  394.  
  395. $(HTML_IDS['STOP']).prop('disabled', true).css(CSS_GREY);
  396. $(HTML_IDS['START']).prop('disabled', false).css(CSS_WHITE);
  397. $(CLASS_INPUT).prop('disabled', false).css(CSS_BLACK);
  398. }
  399.  
  400. function initializeButtonCallbacks() {
  401.  
  402. var popup = $(HTML_IDS['POPUP']);
  403. var button = $(HTML_IDS['BUTTON']);
  404.  
  405. button.click(function(ev) {
  406. if (popup.is(':visible')) {
  407. popup.hide();
  408. }
  409. else {
  410. popup.show();
  411. }
  412. });
  413.  
  414. popup.click(function(ev) {
  415. ev.stopPropagation();
  416. });
  417.  
  418. $(HTML_IDS['START']).click(function() {
  419. startTipping();
  420. $(HTML_IDS['POPUP']).hide();
  421. });
  422.  
  423. $(HTML_IDS['STOP']).click(function() {
  424. stopTipping();
  425. });
  426.  
  427. $('body').click(function(ev) {
  428. if (ev.target.id != button.prop('id')) {
  429. $(HTML_IDS['POPUP']).hide();
  430. }
  431. });
  432.  
  433. $(CLASS_INPUT).change(function() {
  434. calculateAndSetTotalTip();
  435. calculateAndSetETA();
  436. });
  437. }
  438.  
  439. function calculateAndSetTotalTip() {
  440.  
  441. var value = $(HTML_IDS['AMOUNT']).val() * $(HTML_IDS['COUNT']).val();
  442. $(HTML_IDS['TOTAL']).html(value + ' tokens');
  443. }
  444.  
  445. function calculateAndSetETA() {
  446.  
  447. var interval = juration.parse($(HTML_IDS['INTERVAL']).val());
  448.  
  449. /* we're not counting the first tip */
  450. var count = getTipCount() - 1;
  451.  
  452. var variance_lower = getVarianceLower();
  453. var variance_upper = getVarianceUpper();
  454.  
  455. var eta = (interval + variance_upper - variance_lower) * count;
  456. $(HTML_IDS['ETA']).html(juration.stringify(eta, {'format': 'long', 'units': 2}));
  457. }
  458.  
  459. function getTipAmount() {
  460.  
  461. return parseInt(getTipAmountRaw());
  462. }
  463.  
  464. function getTipAmountRaw() {
  465.  
  466. return $(HTML_IDS['AMOUNT']).val();
  467. }
  468.  
  469. function getTipInterval() {
  470.  
  471. return $(HTML_IDS['INTERVAL']).val();
  472. }
  473.  
  474. function getTipCount() {
  475.  
  476. return parseInt(getTipCountRaw());
  477. }
  478.  
  479. function getTipCountRaw() {
  480.  
  481. return $(HTML_IDS['COUNT']).val();
  482. }
  483.  
  484. function getVarianceLower() {
  485.  
  486. return parseVariance(getVarianceLowerRaw());
  487. }
  488.  
  489. function getVarianceLowerRaw() {
  490.  
  491. return $(HTML_IDS['VARIANCE_LOWER']).val();
  492. }
  493.  
  494.  
  495. function getVarianceUpper() {
  496.  
  497. return parseVariance(getVarianceUpperRaw());
  498. }
  499.  
  500. function getVarianceUpperRaw() {
  501.  
  502. return $(HTML_IDS['VARIANCE_UPPER']).val();
  503. }
  504.  
  505. function parseVariance(variance) {
  506.  
  507. if (variance == '0') {
  508. variance = 0;
  509. }
  510.  
  511. variance = variance || 0;
  512.  
  513. if (variance != 0) {
  514. variance = juration.parse(variance);
  515. }
  516.  
  517. return variance;
  518. }
  519.  
  520. function saveCurrentSettings() {
  521.  
  522. GM_setValue('amount', getTipAmount());
  523. GM_setValue('interval', getTipInterval());
  524. GM_setValue('count', getTipCount());
  525. GM_setValue('variance_lower', getVarianceLower());
  526. GM_setValue('variance_upper', getVarianceUpper());
  527. }
  528.  
  529. function loadPreviousSettings() {
  530.  
  531. var amount = GM_getValue('amount', 1);
  532. $(HTML_IDS['AMOUNT']).val(amount);
  533.  
  534. var count = GM_getValue('count', 10);
  535. $(HTML_IDS['COUNT']).val(count);
  536.  
  537. var interval = GM_getValue('interval', '1s');
  538. $(HTML_IDS['INTERVAL']).val(interval);
  539.  
  540. var variance_lower = GM_getValue('variance_lower', '');
  541. $(HTML_IDS['VARIANCE_LOWER']).val(variance_lower);
  542.  
  543. var variance_upper = GM_getValue('variance_upper', '');
  544. $(HTML_IDS['VARIANCE_UPPER']).val(variance_upper);
  545.  
  546. calculateAndSetTotalTip();
  547. calculateAndSetETA();
  548. }
  549.  
  550.  
  551. // Script ends here
  552. // Libs included because they're not on a popular cdn
  553.  
  554.  
  555. /*
  556. * juration - a natural language duration parser
  557. * https://github.com/domchristie/juration
  558. *
  559. * Copyright 2011, Dom Christie
  560. * Licenced under the MIT licence
  561. *
  562. */
  563.  
  564. function loadJuration() {
  565.  
  566. var UNITS = {
  567. seconds: {
  568. patterns: ['second', 'sec', 's'],
  569. value: 1,
  570. formats: {
  571. 'chrono': '',
  572. 'micro': 's',
  573. 'short': 'sec',
  574. 'long': 'second'
  575. }
  576. },
  577. minutes: {
  578. patterns: ['minute', 'min', 'm(?!s)'],
  579. value: 60,
  580. formats: {
  581. 'chrono': ':',
  582. 'micro': 'm',
  583. 'short': 'min',
  584. 'long': 'minute'
  585. }
  586. },
  587. hours: {
  588. patterns: ['hour', 'hr', 'h'],
  589. value: 3600,
  590. formats: {
  591. 'chrono': ':',
  592. 'micro': 'h',
  593. 'short': 'hr',
  594. 'long': 'hour'
  595. }
  596. },
  597. days: {
  598. patterns: ['day', 'dy', 'd'],
  599. value: 86400,
  600. formats: {
  601. 'chrono': ':',
  602. 'micro': 'd',
  603. 'short': 'day',
  604. 'long': 'day'
  605. }
  606. },
  607. weeks: {
  608. patterns: ['week', 'wk', 'w'],
  609. value: 604800,
  610. formats: {
  611. 'chrono': ':',
  612. 'micro': 'w',
  613. 'short': 'wk',
  614. 'long': 'week'
  615. }
  616. },
  617. months: {
  618. patterns: ['month', 'mon', 'mo', 'mth'],
  619. value: 2628000,
  620. formats: {
  621. 'chrono': ':',
  622. 'micro': 'm',
  623. 'short': 'mth',
  624. 'long': 'month'
  625. }
  626. },
  627. years: {
  628. patterns: ['year', 'yr', 'y'],
  629. value: 31536000,
  630. formats: {
  631. 'chrono': ':',
  632. 'micro': 'y',
  633. 'short': 'yr',
  634. 'long': 'year'
  635. }
  636. }
  637. };
  638.  
  639. var stringify = function(seconds, options) {
  640.  
  641. if(!_isNumeric(seconds)) {
  642. throw "juration.stringify(): Unable to stringify a non-numeric value";
  643. }
  644.  
  645. if((typeof options === 'object' && options.format !== undefined) && (options.format !== 'micro' && options.format !== 'short' && options.format !== 'long' && options.format !== 'chrono')) {
  646. throw "juration.stringify(): format cannot be '" + options.format + "', and must be either 'micro', 'short', or 'long'";
  647. }
  648.  
  649. var defaults = {
  650. format: 'short',
  651. units: undefined
  652. };
  653.  
  654. var opts = _extend(defaults, options);
  655.  
  656. var units = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'], values = [];
  657. var remaining = seconds;
  658. var activeUnits = 0;
  659. for(var i = 0, len = units.length;
  660. i < len && (opts.units == undefined || activeUnits < opts.units);
  661. i++) {
  662. var unit = UNITS[units[i]];
  663. values[i] = Math.floor(remaining / unit.value);
  664. if (values[i] > 0 || activeUnits > 0)
  665. activeUnits++;
  666.  
  667. if(opts.format === 'micro' || opts.format === 'chrono') {
  668. values[i] += unit.formats[opts.format];
  669. }
  670. else {
  671. values[i] += ' ' + _pluralize(values[i], unit.formats[opts.format]);
  672. }
  673. remaining = remaining % unit.value;
  674. }
  675. var output = '';
  676. for(i = 0, len = values.length; i < len; i++) {
  677. if(values[i].charAt(0) !== "0" && opts.format != 'chrono') {
  678. output += values[i] + ' ';
  679. }
  680. else if (opts.format == 'chrono') {
  681. output += _padLeft(values[i]+'', '0', i==values.length-1 ? 2 : 3);
  682. }
  683. }
  684. return output.replace(/\s+$/, '').replace(/^(00:)+/g, '').replace(/^0/, '');
  685. };
  686.  
  687. var parse = function(string) {
  688.  
  689. // returns calculated values separated by spaces
  690. for(var unit in UNITS) {
  691. for(var i = 0, mLen = UNITS[unit].patterns.length; i < mLen; i++) {
  692. var regex = new RegExp("((?:\\d+\\.\\d+)|\\d+)\\s?(" + UNITS[unit].patterns[i] + "s?(?=\\s|\\d|\\b))", 'gi');
  693. string = string.replace(regex, function(str, p1, p2) {
  694. return " " + (p1 * UNITS[unit].value).toString() + " ";
  695. });
  696. }
  697. }
  698.  
  699. var sum = 0,
  700. numbers = string
  701. .replace(/(?!\.)\W+/g, ' ') // replaces non-word chars (excluding '.') with whitespace
  702. .replace(/^\s+|\s+$|(?:and|plus|with)\s?/g, '') // trim L/R whitespace, replace known join words with ''
  703. .split(' ');
  704.  
  705. for(var j = 0, nLen = numbers.length; j < nLen; j++) {
  706. if(numbers[j] && isFinite(numbers[j])) {
  707. sum += parseFloat(numbers[j]);
  708. } else if(!numbers[j]) {
  709. throw "juration.parse(): Unable to parse: a falsey value";
  710. } else {
  711. // throw an exception if it's not a valid word/unit
  712. throw "juration.parse(): Unable to parse: " + numbers[j].replace(/^\d+/g, '');
  713. }
  714. }
  715. return sum;
  716. };
  717.  
  718. // _padLeft('5', '0', 2); // 05
  719. var _padLeft = function(s, c, n) {
  720. if (! s || ! c || s.length >= n) {
  721. return s;
  722. }
  723.  
  724. var max = (n - s.length)/c.length;
  725. for (var i = 0; i < max; i++) {
  726. s = c + s;
  727. }
  728.  
  729. return s;
  730. };
  731.  
  732. var _pluralize = function(count, singular) {
  733. return count == 1 ? singular : singular + "s";
  734. };
  735.  
  736. var _isNumeric = function(n) {
  737. return !isNaN(parseFloat(n)) && isFinite(n);
  738. };
  739.  
  740. var _extend = function(obj, extObj) {
  741. for (var i in extObj) {
  742. if(extObj[i] !== undefined) {
  743. obj[i] = extObj[i];
  744. }
  745. }
  746. return obj;
  747. };
  748.  
  749. var juration = {
  750. parse: parse,
  751. stringify: stringify,
  752. humanize: stringify
  753. };
  754.  
  755. return juration;
  756. };