<commit_before>( function(window) {
'use strict';
angular.module('tmh.dynamicLocale', []).provider('tmhDynamicLocale', function() {

  var defaultLocale,
    localeLocationPattern = 'angular/i18n/angular-locale_{{locale}}.js',
    storageFactory = 'tmhDynamicLocaleStorageCache',
    storage,
    storeKey = 'tmhDynamicLocale.locale',
    promiseCache = {},
    activeLocale;

  /**
   * Loads a script asynchronously
   *
   * @param {string} url The url for the script
   @ @param {function) callback A function to be called once the script is loaded
   */
  function loadScript(url, callback, errorCallback, $timeout) {
    var script = document.createElement('script'),
      body = document.getElementsByTagName('body')[0],
      removed = false;

    script.type = 'text/javascript';
    if (script.readyState) { // IE
      script.onreadystatechange = function () {
        if (script.readyState === 'complete' ||
            script.readyState === 'loaded') {
          script.onreadystatechange = null;
          $timeout(
            function () {
              if (removed) return;
              removed = true;
              body.removeChild(script);
              callback();
            }, 30, false);
        }
      };
    } else { // Others
      script.onload = function () {
        if (removed) return;
        removed = true;
        body.removeChild(script);
        callback();
      };
      script.onerror = function () {
        if (removed) return;
        removed = true;
        body.removeChild(script);
        errorCallback();
      }
    }
    script.src = url;
    script.async = false;
    body.appendChild(script);
  }

  /**
   * Loads a locale and replaces the properties from the current locale with the new locale information
   *
   * @param localeUrl The path to the new locale
   * @param $locale The locale at the curent scope
   */
  function loadLocale(localeUrl, $locale, localeId, $rootScope, $q, localeCache, $timeout) {

    function overrideValues(oldObject, newObject) {
      if (activeLocale !== localeId) {
        return;
      }
      angular.forEach(oldObject, function(value, key) {
        if (!newObject[key]) {
          delete oldObject[key];
        } else if (angular.isArray(newObject[key])) {
          oldObject[key].length = newObject[key].length;
        }
      });
      angular.forEach(newObject, function(value, key) {
        if (angular.isArray(newObject[key]) || angular.isObject(newObject[key])) {
          if (!oldObject[key]) {
            oldObject[key] = angular.isArray(newObject[key]) ? [] : {};
          }
          overrideValues(oldObject[key], newObject[key]);
        } else {
          oldObject[key] = newObject[key];
        }
      });
    }

    if (promiseCache[localeId]) return promiseCache[localeId];

    var cachedLocale,
      deferred = $q.defer();
    if (localeId === activeLocale) {
      deferred.resolve($locale);
    } else if ((cachedLocale = localeCache.get(localeId))) {
      activeLocale = localeId;
      $rootScope.$evalAsync(function() {
        overrideValues($locale, cachedLocale);
        $rootScope.$broadcast('$localeChangeSuccess', localeId, $locale);
        storage.put(storeKey, localeId);
        deferred.resolve($locale);
      });
    } else {
      activeLocale = localeId;
      promiseCache[localeId] = deferred.promise;
      loadScript(localeUrl, function () {
        // Create a new injector with the new locale
        var localInjector = angular.injector(['ngLocale']),
          externalLocale = localInjector.get('$locale');

        overrideValues($locale, externalLocale);
        localeCache.put(localeId, externalLocale);
        delete promiseCache[localeId];

        $rootScope.$apply(function () {
          $rootScope.$broadcast('$localeChangeSuccess', localeId, $locale);
          storage.put(storeKey, localeId);
          deferred.resolve($locale);
        });
      }, function () {
        delete promiseCache[localeId];

        $rootScope.$apply(function () {
          $rootScope.$broadcast('$localeChangeError', localeId);
          deferred.reject(localeId);
        });
      }, $timeout);
    }
    return deferred.promise;
  }

  this.localeLocationPattern = function(value) {
    if (value) {
      localeLocationPattern = value;
      return this;
    } else {
      return localeLocationPattern;
    }
  };

  this.useStorage = function(storageName) {
    storageFactory = storageName;
  };

  this.useCookieStorage = function() {
    this.useStorage('$cookieStore');
  };

  this.defaultLocale = function (value) {
    defaultLocale = value;
  };

  this.$get = ['$rootScope', '$injector', '$interpolate', '$locale', '$q', 'tmhDynamicLocaleCache', '$timeout', function($rootScope, $injector, interpolate, locale, $q, tmhDynamicLocaleCache, $timeout) {
    var localeLocation = interpolate(localeLocationPattern);

    storage = $injector.get(storageFactory);
    $rootScope.$evalAsync(function () {
      var initialLocale;
      if ((initialLocale = (storage.get(storeKey) || defaultLocale))) {
        loadLocale(localeLocation({locale: initialLocale}), locale, initialLocale, $rootScope, $q, tmhDynamicLocaleCache, $timeout);
      }
    });
    return {
      /**
       * @ngdoc method
       * @description
       * @param {string=} value Sets the locale to the new locale. Changing the locale will trigger
       *    a background task that will retrieve the new locale and configure the current $locale
       *    instance with the information from the new locale
       */
      set: function(value) {
        return loadLocale(localeLocation({locale: value}), locale, value, $rootScope, $q, tmhDynamicLocaleCache, $timeout);
      }
    };
  }];
}).provider('tmhDynamicLocaleCache', function() {
  this.$get = ['$cacheFactory', function($cacheFactory) {
    return $cacheFactory('tmh.dynamicLocales');
  }];
}).provider('tmhDynamicLocaleStorageCache', function() {
  this.$get = ['$cacheFactory', function($cacheFactory) {
    return $cacheFactory('tmh.dynamicLocales.store');
  }];
}).run(['tmhDynamicLocale', angular.noop]);
}(window) );
<commit_msg>chore(jshint): fix jshint checkstyle issue<commit_after>( function(window) {
'use strict';
angular.module('tmh.dynamicLocale', []).provider('tmhDynamicLocale', function() {

  var defaultLocale,
    localeLocationPattern = 'angular/i18n/angular-locale_{{locale}}.js',
    storageFactory = 'tmhDynamicLocaleStorageCache',
    storage,
    storeKey = 'tmhDynamicLocale.locale',
    promiseCache = {},
    activeLocale;

  /**
   * Loads a script asynchronously
   *
   * @param {string} url The url for the script
   @ @param {function) callback A function to be called once the script is loaded
   */
  function loadScript(url, callback, errorCallback, $timeout) {
    var script = document.createElement('script'),
      body = document.getElementsByTagName('body')[0],
      removed = false;

    script.type = 'text/javascript';
    if (script.readyState) { // IE
      script.onreadystatechange = function () {
        if (script.readyState === 'complete' ||
            script.readyState === 'loaded') {
          script.onreadystatechange = null;
          $timeout(
            function () {
              if (removed) return;
              removed = true;
              body.removeChild(script);
              callback();
            }, 30, false);
        }
      };
    } else { // Others
      script.onload = function () {
        if (removed) return;
        removed = true;
        body.removeChild(script);
        callback();
      };
      script.onerror = function () {
        if (removed) return;
        removed = true;
        body.removeChild(script);
        errorCallback();
      };
    }
    script.src = url;
    script.async = false;
    body.appendChild(script);
  }

  /**
   * Loads a locale and replaces the properties from the current locale with the new locale information
   *
   * @param localeUrl The path to the new locale
   * @param $locale The locale at the curent scope
   */
  function loadLocale(localeUrl, $locale, localeId, $rootScope, $q, localeCache, $timeout) {

    function overrideValues(oldObject, newObject) {
      if (activeLocale !== localeId) {
        return;
      }
      angular.forEach(oldObject, function(value, key) {
        if (!newObject[key]) {
          delete oldObject[key];
        } else if (angular.isArray(newObject[key])) {
          oldObject[key].length = newObject[key].length;
        }
      });
      angular.forEach(newObject, function(value, key) {
        if (angular.isArray(newObject[key]) || angular.isObject(newObject[key])) {
          if (!oldObject[key]) {
            oldObject[key] = angular.isArray(newObject[key]) ? [] : {};
          }
          overrideValues(oldObject[key], newObject[key]);
        } else {
          oldObject[key] = newObject[key];
        }
      });
    }

    if (promiseCache[localeId]) return promiseCache[localeId];

    var cachedLocale,
      deferred = $q.defer();
    if (localeId === activeLocale) {
      deferred.resolve($locale);
    } else if ((cachedLocale = localeCache.get(localeId))) {
      activeLocale = localeId;
      $rootScope.$evalAsync(function() {
        overrideValues($locale, cachedLocale);
        $rootScope.$broadcast('$localeChangeSuccess', localeId, $locale);
        storage.put(storeKey, localeId);
        deferred.resolve($locale);
      });
    } else {
      activeLocale = localeId;
      promiseCache[localeId] = deferred.promise;
      loadScript(localeUrl, function () {
        // Create a new injector with the new locale
        var localInjector = angular.injector(['ngLocale']),
          externalLocale = localInjector.get('$locale');

        overrideValues($locale, externalLocale);
        localeCache.put(localeId, externalLocale);
        delete promiseCache[localeId];

        $rootScope.$apply(function () {
          $rootScope.$broadcast('$localeChangeSuccess', localeId, $locale);
          storage.put(storeKey, localeId);
          deferred.resolve($locale);
        });
      }, function () {
        delete promiseCache[localeId];

        $rootScope.$apply(function () {
          $rootScope.$broadcast('$localeChangeError', localeId);
          deferred.reject(localeId);
        });
      }, $timeout);
    }
    return deferred.promise;
  }

  this.localeLocationPattern = function(value) {
    if (value) {
      localeLocationPattern = value;
      return this;
    } else {
      return localeLocationPattern;
    }
  };

  this.useStorage = function(storageName) {
    storageFactory = storageName;
  };

  this.useCookieStorage = function() {
    this.useStorage('$cookieStore');
  };

  this.defaultLocale = function (value) {
    defaultLocale = value;
  };

  this.$get = ['$rootScope', '$injector', '$interpolate', '$locale', '$q', 'tmhDynamicLocaleCache', '$timeout', function($rootScope, $injector, interpolate, locale, $q, tmhDynamicLocaleCache, $timeout) {
    var localeLocation = interpolate(localeLocationPattern);

    storage = $injector.get(storageFactory);
    $rootScope.$evalAsync(function () {
      var initialLocale;
      if ((initialLocale = (storage.get(storeKey) || defaultLocale))) {
        loadLocale(localeLocation({locale: initialLocale}), locale, initialLocale, $rootScope, $q, tmhDynamicLocaleCache, $timeout);
      }
    });
    return {
      /**
       * @ngdoc method
       * @description
       * @param {string=} value Sets the locale to the new locale. Changing the locale will trigger
       *    a background task that will retrieve the new locale and configure the current $locale
       *    instance with the information from the new locale
       */
      set: function(value) {
        return loadLocale(localeLocation({locale: value}), locale, value, $rootScope, $q, tmhDynamicLocaleCache, $timeout);
      }
    };
  }];
}).provider('tmhDynamicLocaleCache', function() {
  this.$get = ['$cacheFactory', function($cacheFactory) {
    return $cacheFactory('tmh.dynamicLocales');
  }];
}).provider('tmhDynamicLocaleStorageCache', function() {
  this.$get = ['$cacheFactory', function($cacheFactory) {
    return $cacheFactory('tmh.dynamicLocales.store');
  }];
}).run(['tmhDynamicLocale', angular.noop]);
}(window) );
<|endoftext|>


<commit_before>/*
	Declare a namespace for your app

	Using a namespace keeps your code out of the global scope
	A namespace also helps keep your organized. Large apps only get larger
*/
var W = {
	// Store your components in your namespace as IM.Components
	Components: {},

	// Models go in IM.Models
	Models: {},

	// Collections as well
	Collections: {},

	// And templates
	Templates: {},

	// It's not required, but having init() in your namespace gives you a nice place to setup your application
	init: function() {
		// Create a router
		W.router = new W.Router();

		// Create the wizard
		W.wizard = new W.Wizard({
			parent: '#wizard'
		});

		// Start watching history
		Backbone.history.start();
	},

	// Hold our router instance, in case we need to do W.router.navigate()
	router: null,

	// Hold the instance of W.wizard
	wizard: null
};

// Define your templates
W.Templates['Wizard'] = Handlebars.compile([
	'
Wizard of ƒ
'
].join(''));

W.Templates['Step1'] = Handlebars.compile([
	'<button class="next">Next</button>',
	'
Step 1: Acceptance
',
	'
First, learn to accept that your process is complicated enough that it needs a wizard.
'
].join(''));

W.Templates['Step2'] = Handlebars.compile([
	'<button class="prev">Previous</button>',
	'<button class="next">Next</button>',
	'
Step 2: Planning
',
	'
Break your process into logical steps.
',
	'Do Part 1',
	'Do Part 2',
	'

',
	'

'
].join(''));

W.Templates['Step2_Part'] = Handlebars.compile([
	'
{{name}}
',
	'
{{content}}
',
	'<button class="done">I did it</button>'
].join(''));

W.Templates['Step3'] = Handlebars.compile([
	'<button class="prev">Previous</button>',
	'
Step 3: Implement
',
	'
Just do it!
'
].join(''));
<commit_msg>Fixed part links<commit_after>/*
	Declare a namespace for your app

	Using a namespace keeps your code out of the global scope
	A namespace also helps keep your organized. Large apps only get larger
*/
var W = {
	// Store your components in your namespace as IM.Components
	Components: {},

	// Models go in IM.Models
	Models: {},

	// Collections as well
	Collections: {},

	// And templates
	Templates: {},

	// It's not required, but having init() in your namespace gives you a nice place to setup your application
	init: function() {
		// Create a router
		W.router = new W.Router();

		// Create the wizard
		W.wizard = new W.Wizard({
			parent: '#wizard'
		});

		// Start watching history
		Backbone.history.start();
	},

	// Hold our router instance, in case we need to do W.router.navigate()
	router: null,

	// Hold the instance of W.wizard
	wizard: null
};

// Define your templates
W.Templates['Wizard'] = Handlebars.compile([
	'
Wizard of ƒ
'
].join(''));

W.Templates['Step1'] = Handlebars.compile([
	'<button class="next">Next</button>',
	'
Step 1: Acceptance
',
	'
First, learn to accept that your process is complicated enough that it needs a wizard.
'
].join(''));

W.Templates['Step2'] = Handlebars.compile([
	'<button class="prev">Previous</button>',
	'<button class="next">Next</button>',
	'
Step 2: Planning
',
	'
Break your process into logical steps.
',
	'Do Part 1
',
	'Do Part 2',
	'

',
	'

'
].join(''));

W.Templates['Step2_Part'] = Handlebars.compile([
	'
{{name}}
',
	'
{{content}}
',
	'<button class="done">I did it</button>'
].join(''));

W.Templates['Step3'] = Handlebars.compile([
	'<button class="prev">Previous</button>',
	'
Step 3: Implement
',
	'
Just do it!
'
].join(''));
<|endoftext|>


<commit_before>'use strict';

var Axes = require('../../plots/cartesian/axes');
var Lib = require('../../lib');
var Fx = require('../../components/fx');
var Color = require('../../components/color');
var fillText = Lib.fillText;

function hoverPoints(pointData, xval, yval, hovermode) {
    var cd = pointData.cd;
    var trace = cd[0].trace;
    var hoveron = trace.hoveron;
    var closeBoxData = [];
    var closePtData;

    if(hoveron.indexOf('boxes') !== -1) {
        closeBoxData = closeBoxData.concat(hoverOnBoxes(pointData, xval, yval, hovermode));
    }

    if(hoveron.indexOf('points') !== -1) {
        closePtData = hoverOnPoints(pointData, xval, yval);
    }

    // If there's a point in range and hoveron has points, show the best single point only.
    // If hoveron has boxes and there's no point in range (or hoveron doesn't have points), show the box stats.
    if(hovermode === 'closest') {
        if(closePtData) return [closePtData];
        return closeBoxData;
    }

    // Otherwise in compare mode, allow a point AND the box stats to be labeled
    // If there are multiple boxes in range (ie boxmode = 'overlay') we'll see stats for all of them.
    if(closePtData) {
        closeBoxData.push(closePtData);
        return closeBoxData;
    }
    return closeBoxData;
}

function hoverOnBoxes(pointData, xval, yval, hovermode) {
    var cd = pointData.cd;
    var xa = pointData.xa;
    var ya = pointData.ya;
    var trace = cd[0].trace;
    var t = cd[0].t;
    var isViolin = trace.type === 'violin';

    var pLetter, vLetter, pAxis, vAxis, vVal, pVal, dx, dy, dPos,
        hoverPseudoDistance, spikePseudoDistance;

    var boxDelta = t.bdPos;
    var boxDeltaPos, boxDeltaNeg;
    var posAcceptance = t.wHover;
    var shiftPos = function(di) { return pAxis.c2l(di.pos) + t.bPos - pAxis.c2l(pVal); };

    if(isViolin && trace.side !== 'both') {
        if(trace.side === 'positive') {
            dPos = function(di) {
                var pos = shiftPos(di);
                return Fx.inbox(pos, pos + posAcceptance, hoverPseudoDistance);
            };
            boxDeltaPos = boxDelta;
            boxDeltaNeg = 0;
        }
        if(trace.side === 'negative') {
            dPos = function(di) {
                var pos = shiftPos(di);
                return Fx.inbox(pos - posAcceptance, pos, hoverPseudoDistance);
            };
            boxDeltaPos = 0;
            boxDeltaNeg = boxDelta;
        }
    } else {
        dPos = function(di) {
            var pos = shiftPos(di);
            return Fx.inbox(pos - posAcceptance, pos + posAcceptance, hoverPseudoDistance);
        };
        boxDeltaPos = boxDeltaNeg = boxDelta;
    }

    var dVal;

    if(isViolin) {
        dVal = function(di) {
            return Fx.inbox(di.span[0] - vVal, di.span[1] - vVal, hoverPseudoDistance);
        };
    } else {
        dVal = function(di) {
            return Fx.inbox(di.min - vVal, di.max - vVal, hoverPseudoDistance);
        };
    }

    if(trace.orientation === 'h') {
        vVal = xval;
        pVal = yval;
        dx = dVal;
        dy = dPos;
        pLetter = 'y';
        pAxis = ya;
        vLetter = 'x';
        vAxis = xa;
    } else {
        vVal = yval;
        pVal = xval;
        dx = dPos;
        dy = dVal;
        pLetter = 'x';
        pAxis = xa;
        vLetter = 'y';
        vAxis = ya;
    }

    // if two boxes are overlaying, let the narrowest one win
    var pseudoDistance = Math.min(1, boxDelta / Math.abs(pAxis.r2c(pAxis.range[1]) - pAxis.r2c(pAxis.range[0])));
    hoverPseudoDistance = pointData.maxHoverDistance - pseudoDistance;
    spikePseudoDistance = pointData.maxSpikeDistance - pseudoDistance;

    function dxy(di) { return (dx(di) + dy(di)) / 2; }
    var distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy);
    Fx.getClosest(cd, distfn, pointData);

    // skip the rest (for this trace) if we didn't find a close point
    // and create the item(s) in closedata for this point
    if(pointData.index === false) return [];

    var di = cd[pointData.index];
    var lc = trace.line.color;
    var mc = (trace.marker || {}).color;

    if(Color.opacity(lc) && trace.line.width) pointData.color = lc;
    else if(Color.opacity(mc) && trace.boxpoints) pointData.color = mc;
    else pointData.color = trace.fillcolor;

    pointData[pLetter + '0'] = pAxis.c2p(di.pos + t.bPos - boxDeltaNeg, true);
    pointData[pLetter + '1'] = pAxis.c2p(di.pos + t.bPos + boxDeltaPos, true);

    pointData[pLetter + 'LabelVal'] = di.orig_p !== undefined ? di.orig_p : di.pos;

    var spikePosAttr = pLetter + 'Spike';
    pointData.spikeDistance = dxy(di) * spikePseudoDistance / hoverPseudoDistance;
    pointData[spikePosAttr] = pAxis.c2p(di.pos, true);

    var hasMean = trace.boxmean || (trace.meanline || {}).visible;
    var hasFences = trace.boxpoints || trace.points;

    // labels with euqual values (e.g. when min === q1) should be presented in certain order
    var attrs =
        (hasFences && hasMean) ? ['max', 'uf', 'q3', 'mean', 'q1', 'lf', 'min'] :
        (hasFences && !hasMean) ? ['max', 'uf', 'q3', 'q1', 'lf', 'min'] :
        (!hasFences && hasMean) ? ['max', 'q3', 'mean', 'q1', 'min'] :
        ['max', 'q3', 'q1', 'min'];

    if(trace.orientation === 'h') attrs.reverse();

    // always put median at the start so that it get the extra label
    attrs = ['med'].concat(attrs);

    var closeBoxData = [];
    for(var i = 0; i < attrs.length; i++) {
        var attr = attrs[i];

        if(!(attr in di)) continue;

        // copy out to a new object for each value to label
        var val = di[attr];
        var valPx = vAxis.c2p(val, true);
        var pointData2 = Lib.extendFlat({}, pointData);

        pointData2.attr = attr;
        pointData2[vLetter + '0'] = pointData2[vLetter + '1'] = valPx;
        pointData2[vLetter + 'LabelVal'] = val;
        pointData2[vLetter + 'Label'] = (t.labels ? t.labels[attr] + ' ' : '') + Axes.hoverLabelText(vAxis, val, trace[vLetter + 'hoverformat']);

        // Note: introduced to be able to distinguish a
        // clicked point from a box during click-to-select
        pointData2.hoverOnBox = true;

        if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') {
            pointData2[vLetter + 'err'] = di.sd;
        }

        // only keep name and spikes on the first item (median)
        pointData.name = '';
        pointData.spikeDistance = undefined;
        pointData[spikePosAttr] = undefined;

        // no hovertemplate support yet
        pointData2.hovertemplate = false;

        closeBoxData.push(pointData2);
    }

    return closeBoxData;
}

function hoverOnPoints(pointData, xval, yval) {
    var cd = pointData.cd;
    var xa = pointData.xa;
    var ya = pointData.ya;
    var trace = cd[0].trace;
    var xPx = xa.c2p(xval);
    var yPx = ya.c2p(yval);
    var closePtData;

    var dx = function(di) {
        var rad = Math.max(3, di.mrc || 0);
        return Math.max(Math.abs(xa.c2p(di.x) - xPx) - rad, 1 - 3 / rad);
    };
    var dy = function(di) {
        var rad = Math.max(3, di.mrc || 0);
        return Math.max(Math.abs(ya.c2p(di.y) - yPx) - rad, 1 - 3 / rad);
    };
    var distfn = Fx.quadrature(dx, dy);

    // show one point per trace
    var ijClosest = false;
    var di, pt;

    for(var i = 0; i < cd.length; i++) {
        di = cd[i];

        for(var j = 0; j < (di.pts || []).length; j++) {
            pt = di.pts[j];

            var newDistance = distfn(pt);
            if(newDistance <= pointData.distance) {
                pointData.distance = newDistance;
                ijClosest = [i, j];
            }
        }
    }

    if(!ijClosest) return false;

    di = cd[ijClosest[0]];
    pt = di.pts[ijClosest[1]];

    var xc = xa.c2p(pt.x, true);
    var yc = ya.c2p(pt.y, true);
    var rad = pt.mrc || 1;

    closePtData = Lib.extendFlat({}, pointData, {
        // corresponds to index in x/y input data array
        index: pt.i,
        color: (trace.marker || {}).color,
        name: trace.name,
        x0: xc - rad,
        x1: xc + rad,
        y0: yc - rad,
        y1: yc + rad,
        spikeDistance: pointData.distance,
        hovertemplate: trace.hovertemplate
    });

    var origPos = di.orig_p;
    var pos = origPos !== undefined ? origPos : di.pos;
    var pa;
    if(trace.orientation === 'h') {
        pa = ya;
        closePtData.xLabelVal = pt.x;
        closePtData.yLabelVal = pos;
    } else {
        pa = xa;
        closePtData.xLabelVal = pos;
        closePtData.yLabelVal = pt.y;
    }

    var pLetter = pa._id.charAt(0);
    closePtData[pLetter + 'Spike'] = pa.c2p(di.pos, true);

    fillText(pt, trace, closePtData);

    return closePtData;
}

module.exports = {
    hoverPoints: hoverPoints,
    hoverOnBoxes: hoverOnBoxes,
    hoverOnPoints: hoverOnPoints
};
<commit_msg>consider reversed axes in ordering box hover<commit_after>'use strict';

var Axes = require('../../plots/cartesian/axes');
var Lib = require('../../lib');
var Fx = require('../../components/fx');
var Color = require('../../components/color');
var fillText = Lib.fillText;

function hoverPoints(pointData, xval, yval, hovermode) {
    var cd = pointData.cd;
    var trace = cd[0].trace;
    var hoveron = trace.hoveron;
    var closeBoxData = [];
    var closePtData;

    if(hoveron.indexOf('boxes') !== -1) {
        closeBoxData = closeBoxData.concat(hoverOnBoxes(pointData, xval, yval, hovermode));
    }

    if(hoveron.indexOf('points') !== -1) {
        closePtData = hoverOnPoints(pointData, xval, yval);
    }

    // If there's a point in range and hoveron has points, show the best single point only.
    // If hoveron has boxes and there's no point in range (or hoveron doesn't have points), show the box stats.
    if(hovermode === 'closest') {
        if(closePtData) return [closePtData];
        return closeBoxData;
    }

    // Otherwise in compare mode, allow a point AND the box stats to be labeled
    // If there are multiple boxes in range (ie boxmode = 'overlay') we'll see stats for all of them.
    if(closePtData) {
        closeBoxData.push(closePtData);
        return closeBoxData;
    }
    return closeBoxData;
}

function hoverOnBoxes(pointData, xval, yval, hovermode) {
    var cd = pointData.cd;
    var xa = pointData.xa;
    var ya = pointData.ya;
    var trace = cd[0].trace;
    var t = cd[0].t;
    var isViolin = trace.type === 'violin';

    var pLetter, vLetter, pAxis, vAxis, vVal, pVal, dx, dy, dPos,
        hoverPseudoDistance, spikePseudoDistance;

    var boxDelta = t.bdPos;
    var boxDeltaPos, boxDeltaNeg;
    var posAcceptance = t.wHover;
    var shiftPos = function(di) { return pAxis.c2l(di.pos) + t.bPos - pAxis.c2l(pVal); };

    if(isViolin && trace.side !== 'both') {
        if(trace.side === 'positive') {
            dPos = function(di) {
                var pos = shiftPos(di);
                return Fx.inbox(pos, pos + posAcceptance, hoverPseudoDistance);
            };
            boxDeltaPos = boxDelta;
            boxDeltaNeg = 0;
        }
        if(trace.side === 'negative') {
            dPos = function(di) {
                var pos = shiftPos(di);
                return Fx.inbox(pos - posAcceptance, pos, hoverPseudoDistance);
            };
            boxDeltaPos = 0;
            boxDeltaNeg = boxDelta;
        }
    } else {
        dPos = function(di) {
            var pos = shiftPos(di);
            return Fx.inbox(pos - posAcceptance, pos + posAcceptance, hoverPseudoDistance);
        };
        boxDeltaPos = boxDeltaNeg = boxDelta;
    }

    var dVal;

    if(isViolin) {
        dVal = function(di) {
            return Fx.inbox(di.span[0] - vVal, di.span[1] - vVal, hoverPseudoDistance);
        };
    } else {
        dVal = function(di) {
            return Fx.inbox(di.min - vVal, di.max - vVal, hoverPseudoDistance);
        };
    }

    if(trace.orientation === 'h') {
        vVal = xval;
        pVal = yval;
        dx = dVal;
        dy = dPos;
        pLetter = 'y';
        pAxis = ya;
        vLetter = 'x';
        vAxis = xa;
    } else {
        vVal = yval;
        pVal = xval;
        dx = dPos;
        dy = dVal;
        pLetter = 'x';
        pAxis = xa;
        vLetter = 'y';
        vAxis = ya;
    }

    // if two boxes are overlaying, let the narrowest one win
    var pseudoDistance = Math.min(1, boxDelta / Math.abs(pAxis.r2c(pAxis.range[1]) - pAxis.r2c(pAxis.range[0])));
    hoverPseudoDistance = pointData.maxHoverDistance - pseudoDistance;
    spikePseudoDistance = pointData.maxSpikeDistance - pseudoDistance;

    function dxy(di) { return (dx(di) + dy(di)) / 2; }
    var distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy);
    Fx.getClosest(cd, distfn, pointData);

    // skip the rest (for this trace) if we didn't find a close point
    // and create the item(s) in closedata for this point
    if(pointData.index === false) return [];

    var di = cd[pointData.index];
    var lc = trace.line.color;
    var mc = (trace.marker || {}).color;

    if(Color.opacity(lc) && trace.line.width) pointData.color = lc;
    else if(Color.opacity(mc) && trace.boxpoints) pointData.color = mc;
    else pointData.color = trace.fillcolor;

    pointData[pLetter + '0'] = pAxis.c2p(di.pos + t.bPos - boxDeltaNeg, true);
    pointData[pLetter + '1'] = pAxis.c2p(di.pos + t.bPos + boxDeltaPos, true);

    pointData[pLetter + 'LabelVal'] = di.orig_p !== undefined ? di.orig_p : di.pos;

    var spikePosAttr = pLetter + 'Spike';
    pointData.spikeDistance = dxy(di) * spikePseudoDistance / hoverPseudoDistance;
    pointData[spikePosAttr] = pAxis.c2p(di.pos, true);

    var hasMean = trace.boxmean || (trace.meanline || {}).visible;
    var hasFences = trace.boxpoints || trace.points;

    // labels with euqual values (e.g. when min === q1) should be presented in certain order
    var attrs =
        (hasFences && hasMean) ? ['max', 'uf', 'q3', 'mean', 'q1', 'lf', 'min'] :
        (hasFences && !hasMean) ? ['max', 'uf', 'q3', 'q1', 'lf', 'min'] :
        (!hasFences && hasMean) ? ['max', 'q3', 'mean', 'q1', 'min'] :
        ['max', 'q3', 'q1', 'min'];

    var rev = vAxis.range[1] < vAxis.range[0];

    if(
        (!rev && trace.orientation === 'h') ||
        (rev && trace.orientation === 'v')
    ) {
        attrs.reverse();
    }

    // always put median at the start so that it get the extra label
    attrs = ['med'].concat(attrs);

    var closeBoxData = [];
    for(var i = 0; i < attrs.length; i++) {
        var attr = attrs[i];

        if(!(attr in di)) continue;

        // copy out to a new object for each value to label
        var val = di[attr];
        var valPx = vAxis.c2p(val, true);
        var pointData2 = Lib.extendFlat({}, pointData);

        pointData2.attr = attr;
        pointData2[vLetter + '0'] = pointData2[vLetter + '1'] = valPx;
        pointData2[vLetter + 'LabelVal'] = val;
        pointData2[vLetter + 'Label'] = (t.labels ? t.labels[attr] + ' ' : '') + Axes.hoverLabelText(vAxis, val, trace[vLetter + 'hoverformat']);

        // Note: introduced to be able to distinguish a
        // clicked point from a box during click-to-select
        pointData2.hoverOnBox = true;

        if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') {
            pointData2[vLetter + 'err'] = di.sd;
        }

        // only keep name and spikes on the first item (median)
        pointData.name = '';
        pointData.spikeDistance = undefined;
        pointData[spikePosAttr] = undefined;

        // no hovertemplate support yet
        pointData2.hovertemplate = false;

        closeBoxData.push(pointData2);
    }

    return closeBoxData;
}

function hoverOnPoints(pointData, xval, yval) {
    var cd = pointData.cd;
    var xa = pointData.xa;
    var ya = pointData.ya;
    var trace = cd[0].trace;
    var xPx = xa.c2p(xval);
    var yPx = ya.c2p(yval);
    var closePtData;

    var dx = function(di) {
        var rad = Math.max(3, di.mrc || 0);
        return Math.max(Math.abs(xa.c2p(di.x) - xPx) - rad, 1 - 3 / rad);
    };
    var dy = function(di) {
        var rad = Math.max(3, di.mrc || 0);
        return Math.max(Math.abs(ya.c2p(di.y) - yPx) - rad, 1 - 3 / rad);
    };
    var distfn = Fx.quadrature(dx, dy);

    // show one point per trace
    var ijClosest = false;
    var di, pt;

    for(var i = 0; i < cd.length; i++) {
        di = cd[i];

        for(var j = 0; j < (di.pts || []).length; j++) {
            pt = di.pts[j];

            var newDistance = distfn(pt);
            if(newDistance <= pointData.distance) {
                pointData.distance = newDistance;
                ijClosest = [i, j];
            }
        }
    }

    if(!ijClosest) return false;

    di = cd[ijClosest[0]];
    pt = di.pts[ijClosest[1]];

    var xc = xa.c2p(pt.x, true);
    var yc = ya.c2p(pt.y, true);
    var rad = pt.mrc || 1;

    closePtData = Lib.extendFlat({}, pointData, {
        // corresponds to index in x/y input data array
        index: pt.i,
        color: (trace.marker || {}).color,
        name: trace.name,
        x0: xc - rad,
        x1: xc + rad,
        y0: yc - rad,
        y1: yc + rad,
        spikeDistance: pointData.distance,
        hovertemplate: trace.hovertemplate
    });

    var origPos = di.orig_p;
    var pos = origPos !== undefined ? origPos : di.pos;
    var pa;
    if(trace.orientation === 'h') {
        pa = ya;
        closePtData.xLabelVal = pt.x;
        closePtData.yLabelVal = pos;
    } else {
        pa = xa;
        closePtData.xLabelVal = pos;
        closePtData.yLabelVal = pt.y;
    }

    var pLetter = pa._id.charAt(0);
    closePtData[pLetter + 'Spike'] = pa.c2p(di.pos, true);

    fillText(pt, trace, closePtData);

    return closePtData;
}

module.exports = {
    hoverPoints: hoverPoints,
    hoverOnBoxes: hoverOnBoxes,
    hoverOnPoints: hoverOnPoints
};
<|endoftext|>


<commit_before>const ANALYSIS_TOOLTIP_MESSAGE = [
    `This track is currently being analyzed for loudness normalization, silence removal, clipping protection and fingerprinting.`,
    `Playing this track before the analysis has been completed may require manually adjusting volume.`
];

const ERROR_HEADER = [
    `There was an error with this track:`
];

export default function TrackView(track, opts) {
    opts = Object(opts);
    this._opts = opts;
    this._track = track;
    this._isDestroyed = false;
    this._index = -1;
    this._error = null;
    this._domNode = null;
    this._isAttached = false;
    this._analysisProgress = -1;
    this._analysisProgressUpdateFrameQueued = false;
    this._dragged = false;
    this._offset = 0;
    this._renderedPlayingStatus = false;
    this._viewUpdated = this._viewUpdated.bind(this);
    this._track.on(`viewUpdate`, this._viewUpdated);
}

TrackView.prototype.selectable = function() {
    return this._opts.selectable;
};

TrackView.prototype.page = function() {
    return this._opts.page;
};

TrackView.prototype.shouldUpdateTrackIndex = function() {
    return this._opts.updateTrackIndex;
};

TrackView.prototype.playlist = function() {
    return this._opts.playlist;
};

TrackView.prototype.itemHeight = function() {
    return this._opts.itemHeight;
};

TrackView.prototype.tooltipContext = function() {
    return this._opts.tooltipContext;
};

TrackView.prototype.$ = function() {
    return this._domNode;
};

TrackView.prototype.$container = function() {
    return this.$().find(`.track`);
};

TrackView.prototype.$trackStatus = function() {
    return this.$().find(`.track-status`);
};

TrackView.prototype.$trackNumber = function() {
    return this.$().find(`.track-number`);
};

TrackView.prototype.$trackDuration = function() {
    return this.$().find(`.track-duration`);
};

TrackView.prototype.track = function() {
    return this._track;
};

TrackView.prototype._shouldUpdateDom = function() {
    return this._domNode !== null;
};

TrackView.prototype._ensureDomNode = function(recycledDomNode, target) {
    if (this._shouldUpdateDom()) return;

    this._domNode = recycledDomNode || this.page().createElement(`div`, {
        class: `track-container`
    }).setHtml(`

            </span

Offline

Sync

`);

    if (this.selectable().contains(this)) {
        this.selected();
    } else {
        this.unselected();
    }

    if (this._dragged) {
        this.$().addClass(`track-dragging`);
    } else {
        this.$().removeClass(`track-dragging`);
    }

    this.viewUpdateTagDataChange();
    this.viewUpdateOfflineStatusChange();
    this.viewUpdateSyncStatusChange();
    this.viewUpdatePlayingStatusChange(this.playlist().getCurrentTrack() === this._track);

    let unclickableTrackStatus = false;
    if (this._track.isBeingAnalyzed()) {
        this.viewUpdateShowAnalysisStatus();
        unclickableTrackStatus = true;
    } else {
        this.viewUpdateHideAnalysisStatus();
    }

    if (this._track.hasError()) {
        this.viewUpdateShowErrorStatus();
        unclickableTrackStatus = true;
    } else {
        this.viewUpdateHideErrorStatus();
    }

    if (unclickableTrackStatus) {
        this.$trackStatus().addClass(`unclickable`);
    } else {
        this.$trackStatus().removeClass(`unclickable`);
    }

    this._updateTranslate();

    if (!recycledDomNode && target) {
        this.$().appendTo(target);
    }
};

TrackView.prototype.isDestroyed = function() {
    return this._isDestroyed;
};

TrackView.prototype.destroy = function() {
    if (this._isDestroyed) return false;
    this._track.removeListener(`viewUpdate`, this._viewUpdated);
    this._index = -1;
    this._isDestroyed = true;
    return true;
};

TrackView.prototype.isDetachedFromPlaylist = function() {
    return !this._track || this._track.isDetachedFromPlaylist();
};

TrackView.prototype.isVisible = function() {
    return this._isAttached;
};

TrackView.prototype.setIndex = function(index) {
    if (this._index !== index) {
        this._index = index;
        if (this._shouldUpdateDom()) {
            this._updateTranslate();
        }
    }

    if (this.shouldUpdateTrackIndex() && this._track) {
        this._track.setIndex(index);
    }
};

TrackView.prototype.getIndex = function() {
    return this._index;
};

TrackView.prototype._viewUpdated = function(methodName, ...args) {
    if (args.length > 0) {
        this[methodName](...args);
    } else {
        this[methodName]();
    }
};

TrackView.prototype.renderTrackInfo = function() {
    const artistAndTitle = this._track.getTrackInfo();

    this.$().find(`.track-title`).setText(artistAndTitle.title);
    this.$().find(`.track-artist`).setText(artistAndTitle.artist);
};

TrackView.prototype.renderTrackNumber = function() {
    this.$trackNumber().setText(`${this._track.getIndex() + 1}.`);
};

TrackView.prototype.renderTrackDuration = function() {
    this.$trackDuration().setText(this._track.formatTime());
};

TrackView.prototype.attach = function(target, node) {
    this._ensureDomNode(node, target);
    this._isAttached = true;
};

TrackView.prototype.detach = function() {
    if (this._isAttached) {
        this._isAttached = false;
        const node = this._domNode;
        this._domNode = null;
        return node;
    }
    return null;
};

TrackView.prototype.selected = function() {
    if (!this._shouldUpdateDom()) return;
    this.$().addClass(`track-active`);
};

TrackView.prototype.unselected = function() {
    if (!this._shouldUpdateDom()) return;
    this.$().removeClass(`track-active`);
};

TrackView.prototype.viewUpdateDestroyed = function() {
    this.destroy();
};

TrackView.prototype.viewUpdateOfflineStatusChange = function() {
    if (!this._shouldUpdateDom()) return;
    if (this._track.isAvailableOffline()) {
        this.$().find(`.offline`).addClass(`active`);
    } else {
        this.$().find(`.offline`).removeClass(`active`);
    }
};

TrackView.prototype.viewUpdateSyncStatusChange = function() {
    if (!this._shouldUpdateDom()) return;
    if (this._track.isSyncedToCloud()) {
        this.$().find(`.cloud`).addClass(`active`);
    } else {
        this.$().find(`.cloud`).removeClass(`active`);
    }
};

TrackView.prototype.viewUpdatePlayingStatusChange = function(playingStatus) {
    if (!this._shouldUpdateDom()) return;
    if (this._renderedPlayingStatus === playingStatus) return;
    this._renderedPlayingStatus = playingStatus;

    if (playingStatus) {
        this.$().addClass(`track-playing`);
    } else {
        this.$().removeClass(`track-playing`);
    }
};

TrackView.prototype._updateAnalysisProgress = function() {
    if (!this._analysisProgressUpdateFrameQueued) {
        this._analysisProgressUpdateFrameQueued = true;
    }
};

TrackView.prototype.viewUpdateAnalysisProgress = function(analysisProgress) {
    if (!this._shouldUpdateDom()) return;
    this._analysisProgress = analysisProgress;
    this._updateAnalysisProgress();
};

TrackView.prototype.viewUpdateHideAnalysisStatus = function() {
    if (!this._shouldUpdateDom()) return;
    this.$trackStatus().find(`.track-analysis-status`).hide();
};

TrackView.prototype.viewUpdateShowAnalysisStatus = function() {
    if (!this._shouldUpdateDom()) return;
    this.$trackStatus().find(`.track-analysis-status`).show(`inline-block`);
};

TrackView.prototype.viewUpdateShowErrorStatus = function() {
    if (!this._shouldUpdateDom()) return;
    this.$trackStatus().find(`.track-error-status`).show(`inline-block`);
};

TrackView.prototype.viewUpdateHideErrorStatus = function() {
    if (!this._shouldUpdateDom()) return;
    this.$trackStatus().find(`.track-error-status`).hide();
};

TrackView.prototype.viewUpdatePositionChange = function() {
    if (!this._shouldUpdateDom()) return;
    this.renderTrackNumber();
};

TrackView.prototype.viewUpdateTagDataChange = function() {
    if (!this._shouldUpdateDom()) return;
    this.renderTrackNumber();
    this.renderTrackDuration();
    this.renderTrackInfo();
};

TrackView.prototype._updateTranslate = function() {
    this.$().setTransform(this._getTranslate());
};

TrackView.prototype._getTranslate = function() {
    const index = this._index;
    let y = index * this.itemHeight();
    let x = 0;
    if (this._dragged) {
        x -= 25;
        y -= 10;
    }
    y += this._offset;
    return `translate(${x}px, ${y}px)`;
};

TrackView.prototype.setOffset = function(value) {
    this._offset = value;
    if (!this._shouldUpdateDom()) return;
    this._updateTranslate();
};

TrackView.prototype.startDragging = function() {
    if (this._dragged) return;
    this._dragged = true;
    if (!this._shouldUpdateDom()) return;
    this.$().addClass(`track-dragging`).removeClass(`transition`);
    this._updateTranslate();
};

TrackView.prototype.stopDragging = function() {
    if (!this._dragged) return;
    this._dragged = false;
    this._offset = 0;
    if (!this._shouldUpdateDom()) return;
    this.$().removeClass(`track-dragging`).addClass(`transition`);
    this._updateTranslate();

    this.page().setTimeout(() => {
        if (!this._shouldUpdateDom()) return;
        this.$().removeClass(`transition`);
    }, 220);

};
<commit_msg>Fix invisible playing status icon<commit_after>const ANALYSIS_TOOLTIP_MESSAGE = [
    `This track is currently being analyzed for loudness normalization, silence removal, clipping protection and fingerprinting.`,
    `Playing this track before the analysis has been completed may require manually adjusting volume.`
];

const ERROR_HEADER = [
    `There was an error with this track:`
];

export default function TrackView(track, opts) {
    opts = Object(opts);
    this._opts = opts;
    this._track = track;
    this._isDestroyed = false;
    this._index = -1;
    this._error = null;
    this._domNode = null;
    this._isAttached = false;
    this._analysisProgress = -1;
    this._analysisProgressUpdateFrameQueued = false;
    this._dragged = false;
    this._offset = 0;
    this._viewUpdated = this._viewUpdated.bind(this);
    this._track.on(`viewUpdate`, this._viewUpdated);
}

TrackView.prototype.selectable = function() {
    return this._opts.selectable;
};

TrackView.prototype.page = function() {
    return this._opts.page;
};

TrackView.prototype.shouldUpdateTrackIndex = function() {
    return this._opts.updateTrackIndex;
};

TrackView.prototype.playlist = function() {
    return this._opts.playlist;
};

TrackView.prototype.itemHeight = function() {
    return this._opts.itemHeight;
};

TrackView.prototype.tooltipContext = function() {
    return this._opts.tooltipContext;
};

TrackView.prototype.$ = function() {
    return this._domNode;
};

TrackView.prototype.$container = function() {
    return this.$().find(`.track`);
};

TrackView.prototype.$trackStatus = function() {
    return this.$().find(`.track-status`);
};

TrackView.prototype.$trackNumber = function() {
    return this.$().find(`.track-number`);
};

TrackView.prototype.$trackDuration = function() {
    return this.$().find(`.track-duration`);
};

TrackView.prototype.track = function() {
    return this._track;
};

TrackView.prototype._shouldUpdateDom = function() {
    return this._domNode !== null;
};

TrackView.prototype._ensureDomNode = function(recycledDomNode, target) {
    if (this._shouldUpdateDom()) return;

    this._domNode = recycledDomNode || this.page().createElement(`div`, {
        class: `track-container`
    }).setHtml(`

Offline

Sync

`);

    if (this.selectable().contains(this)) {
        this.selected();
    } else {
        this.unselected();
    }

    if (this._dragged) {
        this.$().addClass(`track-dragging`);
    } else {
        this.$().removeClass(`track-dragging`);
    }

    this.viewUpdateTagDataChange();
    //this.viewUpdateOfflineStatusChange();
    //this.viewUpdateSyncStatusChange();
    this.viewUpdatePlayingStatusChange(this.playlist().getCurrentTrack() === this._track);

    let unclickableTrackStatus = false;
    if (this._track.isBeingAnalyzed()) {
        this.viewUpdateShowAnalysisStatus();
        unclickableTrackStatus = true;
    } else {
        this.viewUpdateHideAnalysisStatus();
    }

    if (this._track.hasError()) {
        this.viewUpdateShowErrorStatus();
        unclickableTrackStatus = true;
    } else {
        this.viewUpdateHideErrorStatus();
    }

    if (unclickableTrackStatus) {
        this.$trackStatus().addClass(`unclickable`);
    } else {
        this.$trackStatus().removeClass(`unclickable`);
    }

    this._updateTranslate();

    if (!recycledDomNode && target) {
        this.$().appendTo(target);
    }
};

TrackView.prototype.isDestroyed = function() {
    return this._isDestroyed;
};

TrackView.prototype.destroy = function() {
    if (this._isDestroyed) return false;
    this._track.removeListener(`viewUpdate`, this._viewUpdated);
    this._index = -1;
    this._isDestroyed = true;
    return true;
};

TrackView.prototype.isDetachedFromPlaylist = function() {
    return !this._track || this._track.isDetachedFromPlaylist();
};

TrackView.prototype.isVisible = function() {
    return this._isAttached;
};

TrackView.prototype.setIndex = function(index) {
    if (this._index !== index) {
        this._index = index;
        if (this._shouldUpdateDom()) {
            this._updateTranslate();
        }
    }

    if (this.shouldUpdateTrackIndex() && this._track) {
        this._track.setIndex(index);
    }
};

TrackView.prototype.getIndex = function() {
    return this._index;
};

TrackView.prototype._viewUpdated = function(methodName, ...args) {
    if (args.length > 0) {
        this[methodName](...args);
    } else {
        this[methodName]();
    }
};

TrackView.prototype.renderTrackInfo = function() {
    const artistAndTitle = this._track.getTrackInfo();

    this.$().find(`.track-title`).setText(artistAndTitle.title);
    this.$().find(`.track-artist`).setText(artistAndTitle.artist);
};

TrackView.prototype.renderTrackNumber = function() {
    this.$trackNumber().setText(`${this._track.getIndex() + 1}.`);
};

TrackView.prototype.renderTrackDuration = function() {
    this.$trackDuration().setText(this._track.formatTime());
};

TrackView.prototype.attach = function(target, node) {
    this._ensureDomNode(node, target);
    this._isAttached = true;
};

TrackView.prototype.detach = function() {
    if (this._isAttached) {
        this._isAttached = false;
        const node = this._domNode;
        this._domNode = null;
        return node;
    }
    return null;
};

TrackView.prototype.selected = function() {
    if (!this._shouldUpdateDom()) return;
    this.$().addClass(`track-active`);
};

TrackView.prototype.unselected = function() {
    if (!this._shouldUpdateDom()) return;
    this.$().removeClass(`track-active`);
};

TrackView.prototype.viewUpdateDestroyed = function() {
    this.destroy();
};

TrackView.prototype.viewUpdateOfflineStatusChange = function() {
    if (!this._shouldUpdateDom()) return;
    if (this._track.isAvailableOffline()) {
        this.$().find(`.offline`).addClass(`active`);
    } else {
        this.$().find(`.offline`).removeClass(`active`);
    }
};

TrackView.prototype.viewUpdateSyncStatusChange = function() {
    if (!this._shouldUpdateDom()) return;
    if (this._track.isSyncedToCloud()) {
        this.$().find(`.cloud`).addClass(`active`);
    } else {
        this.$().find(`.cloud`).removeClass(`active`);
    }
};

TrackView.prototype.viewUpdatePlayingStatusChange = function(playingStatus) {
    if (!this._shouldUpdateDom()) return;
    if (playingStatus) {
        this.$().addClass(`track-playing`);
    } else {
        this.$().removeClass(`track-playing`);
    }
};

TrackView.prototype._updateAnalysisProgress = function() {
    if (!this._analysisProgressUpdateFrameQueued) {
        this._analysisProgressUpdateFrameQueued = true;
    }
};

TrackView.prototype.viewUpdateAnalysisProgress = function(analysisProgress) {
    if (!this._shouldUpdateDom()) return;
    this._analysisProgress = analysisProgress;
    this._updateAnalysisProgress();
};

TrackView.prototype.viewUpdateHideAnalysisStatus = function() {
    if (!this._shouldUpdateDom()) return;
    this.$trackStatus().find(`.track-analysis-status`).hide();
};

TrackView.prototype.viewUpdateShowAnalysisStatus = function() {
    if (!this._shouldUpdateDom()) return;
    this.$trackStatus().find(`.track-analysis-status`).show(`inline-block`);
};

TrackView.prototype.viewUpdateShowErrorStatus = function() {
    if (!this._shouldUpdateDom()) return;
    this.$trackStatus().find(`.track-error-status`).show(`inline-block`);
};

TrackView.prototype.viewUpdateHideErrorStatus = function() {
    if (!this._shouldUpdateDom()) return;
    this.$trackStatus().find(`.track-error-status`).hide();
};

TrackView.prototype.viewUpdatePositionChange = function() {
    if (!this._shouldUpdateDom()) return;
    this.renderTrackNumber();
};

TrackView.prototype.viewUpdateTagDataChange = function() {
    if (!this._shouldUpdateDom()) return;
    this.renderTrackNumber();
    this.renderTrackDuration();
    this.renderTrackInfo();
};

TrackView.prototype._updateTranslate = function() {
    this.$().setTransform(this._getTranslate());
};

TrackView.prototype._getTranslate = function() {
    const index = this._index;
    let y = index * this.itemHeight();
    let x = 0;
    if (this._dragged) {
        x -= 25;
        y -= 10;
    }
    y += this._offset;
    return `translate(${x}px, ${y}px)`;
};

TrackView.prototype.setOffset = function(value) {
    this._offset = value;
    if (!this._shouldUpdateDom()) return;
    this._updateTranslate();
};

TrackView.prototype.startDragging = function() {
    if (this._dragged) return;
    this._dragged = true;
    if (!this._shouldUpdateDom()) return;
    this.$().addClass(`track-dragging`).removeClass(`transition`);
    this._updateTranslate();
};

TrackView.prototype.stopDragging = function() {
    if (!this._dragged) return;
    this._dragged = false;
    this._offset = 0;
    if (!this._shouldUpdateDom()) return;
    this.$().removeClass(`track-dragging`).addClass(`transition`);
    this._updateTranslate();

    this.page().setTimeout(() => {
        if (!this._shouldUpdateDom()) return;
        this.$().removeClass(`transition`);
    }, 220);

};
<|endoftext|>


<commit_before>/* eslint-disable
    no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
 * decaffeinate suggestions:
 * DS101: Remove unnecessary use of Array.from
 * DS102: Remove unnecessary code created because of implicit returns
 * DS207: Consider shorter variations of null checks
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
import TrixObject from "trix/core/object" // Don't override window.Object

import { getDirection } from "trix/core/helpers"

import Piece from "trix/models/piece"
import AttachmentPiece from "trix/models/attachment_piece"
import StringPiece from "trix/models/string_piece"
import SplittableList from "trix/models/splittable_list"

import Hash from "trix/core/collections/hash"

export default class Text extends TrixObject {
  static textForAttachmentWithAttributes(attachment, attributes) {
    const piece = new AttachmentPiece(attachment, attributes)
    return new this([ piece ])
  }

  static textForStringWithAttributes(string, attributes) {
    const piece = new StringPiece(string, attributes)
    return new this([ piece ])
  }

  static fromJSON(textJSON) {
    const pieces = Array.from(textJSON).map((pieceJSON) => Piece.fromJSON(pieceJSON))
    return new this(pieces)
  }

  constructor(pieces = []) {
    super(...arguments)
    this.pieceList = new SplittableList(
      (() => {
        const result = []

        Array.from(pieces).forEach((piece) => {
          if (!piece.isEmpty()) {
            result.push(piece)
          }
        })

        return result
      })()
    )
  }

  copy() {
    return this.copyWithPieceList(this.pieceList)
  }

  copyWithPieceList(pieceList) {
    return new this.constructor(pieceList.consolidate().toArray())
  }

  copyUsingObjectMap(objectMap) {
    const pieces = Array.from(this.getPieces()).map((piece) => objectMap.find(piece) || piece)
    return new this.constructor(pieces)
  }

  appendText(text) {
    return this.insertTextAtPosition(text, this.getLength())
  }

  insertTextAtPosition(text, position) {
    return this.copyWithPieceList(this.pieceList.insertSplittableListAtPosition(text.pieceList, position))
  }

  removeTextAtRange(range) {
    return this.copyWithPieceList(this.pieceList.removeObjectsInRange(range))
  }

  replaceTextAtRange(text, range) {
    return this.removeTextAtRange(range).insertTextAtPosition(text, range[0])
  }

  moveTextFromRangeToPosition(range, position) {
    if (range[0] <= position && position <= range[1]) {
      return
    }
    const text = this.getTextAtRange(range)
    const length = text.getLength()
    if (range[0] < position) {
      position -= length
    }
    return this.removeTextAtRange(range).insertTextAtPosition(text, position)
  }

  addAttributeAtRange(attribute, value, range) {
    const attributes = {}
    attributes[attribute] = value
    return this.addAttributesAtRange(attributes, range)
  }

  addAttributesAtRange(attributes, range) {
    return this.copyWithPieceList(
      this.pieceList.transformObjectsInRange(range, (piece) => piece.copyWithAdditionalAttributes(attributes))
    )
  }

  removeAttributeAtRange(attribute, range) {
    return this.copyWithPieceList(
      this.pieceList.transformObjectsInRange(range, (piece) => piece.copyWithoutAttribute(attribute))
    )
  }

  setAttributesAtRange(attributes, range) {
    return this.copyWithPieceList(
      this.pieceList.transformObjectsInRange(range, (piece) => piece.copyWithAttributes(attributes))
    )
  }

  getAttributesAtPosition(position) {
    return this.pieceList.getObjectAtPosition(position)?.getAttributes() || {}
  }

  getCommonAttributes() {
    const objects = Array.from(this.pieceList.toArray()).map((piece) => piece.getAttributes())
    return Hash.fromCommonAttributesOfObjects(objects).toObject()
  }

  getCommonAttributesAtRange(range) {
    return this.getTextAtRange(range).getCommonAttributes() || {}
  }

  getExpandedRangeForAttributeAtOffset(attributeName, offset) {
    let right
    let left = right = offset
    const length = this.getLength()

    while (left > 0 && this.getCommonAttributesAtRange([ left - 1, right ])[attributeName]) {
      left--
    }
    while (right < length && this.getCommonAttributesAtRange([ offset, right + 1 ])[attributeName]) {
      right++
    }

    return [ left, right ]
  }

  getTextAtRange(range) {
    return this.copyWithPieceList(this.pieceList.getSplittableListInRange(range))
  }

  getStringAtRange(range) {
    return this.pieceList.getSplittableListInRange(range).toString()
  }

  getStringAtPosition(position) {
    return this.getStringAtRange([ position, position + 1 ])
  }

  startsWithString(string) {
    return this.getStringAtRange([ 0, string.length ]) === string
  }

  endsWithString(string) {
    const length = this.getLength()
    return this.getStringAtRange([ length - string.length, length ]) === string
  }

  getAttachmentPieces() {
    return this.pieceList.toArray().filter((piece) => piece.attachment != null)
  }

  getAttachments() {
    return Array.from(this.getAttachmentPieces()).map((piece) => piece.attachment)
  }

  getAttachmentAndPositionById(attachmentId) {
    let position = 0
    for (const piece of Array.from(this.pieceList.toArray())) {
      if (piece.attachment?.id === attachmentId) {
        return { attachment: piece.attachment, position }
      }
      position += piece.length
    }
    return { attachment: null, position: null }
  }

  getAttachmentById(attachmentId) {
    const { attachment, position } = this.getAttachmentAndPositionById(attachmentId)
    return attachment
  }

  getRangeOfAttachment(attachment) {
    const attachmentAndPosition = this.getAttachmentAndPositionById(attachment.id)
    const position = attachmentAndPosition.position
    attachment = attachmentAndPosition.attachment
    if (attachment != null) {
      return [ position, position + 1 ]
    }
  }

  updateAttributesForAttachment(attributes, attachment) {
    const range = this.getRangeOfAttachment(attachment)
    if (range) {
      return this.addAttributesAtRange(attributes, range)
    } else {
      return this
    }
  }

  getLength() {
    return this.pieceList.getEndPosition()
  }

  isEmpty() {
    return this.getLength() === 0
  }

  isEqualTo(text) {
    return super.isEqualTo(text) || text?.pieceList?.isEqualTo(this.pieceList)
  }

  isBlockBreak() {
    return this.getLength() === 1 && this.pieceList.getObjectAtIndex(0).isBlockBreak()
  }

  eachPiece(callback) {
    return this.pieceList.eachObject(callback)
  }

  getPieces() {
    return this.pieceList.toArray()
  }

  getPieceAtPosition(position) {
    return this.pieceList.getObjectAtPosition(position)
  }

  contentsForInspection() {
    return { pieceList: this.pieceList.inspect() }
  }

  toSerializableText() {
    const pieceList = this.pieceList.selectSplittableList((piece) => piece.isSerializable())
    return this.copyWithPieceList(pieceList)
  }

  toString() {
    return this.pieceList.toString()
  }

  toJSON() {
    return this.pieceList.toJSON()
  }

  toConsole() {
    return JSON.stringify(Array.from(this.pieceList.toArray()).map((piece) => JSON.parse(piece.toConsole())))
  }

  // BIDI

  getDirection() {
    return getDirection(this.toString())
  }

  isRTL() {
    return this.getDirection() === "rtl"
  }
}
<commit_msg>Clean up filter loop<commit_after>/* eslint-disable
    no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
 * decaffeinate suggestions:
 * DS101: Remove unnecessary use of Array.from
 * DS102: Remove unnecessary code created because of implicit returns
 * DS207: Consider shorter variations of null checks
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
import TrixObject from "trix/core/object" // Don't override window.Object

import { getDirection } from "trix/core/helpers"

import Piece from "trix/models/piece"
import AttachmentPiece from "trix/models/attachment_piece"
import StringPiece from "trix/models/string_piece"
import SplittableList from "trix/models/splittable_list"

import Hash from "trix/core/collections/hash"

export default class Text extends TrixObject {
  static textForAttachmentWithAttributes(attachment, attributes) {
    const piece = new AttachmentPiece(attachment, attributes)
    return new this([ piece ])
  }

  static textForStringWithAttributes(string, attributes) {
    const piece = new StringPiece(string, attributes)
    return new this([ piece ])
  }

  static fromJSON(textJSON) {
    const pieces = Array.from(textJSON).map((pieceJSON) => Piece.fromJSON(pieceJSON))
    return new this(pieces)
  }

  constructor(pieces = []) {
    super(...arguments)
    const notEmpty = pieces.filter((piece) => !piece.isEmpty())
    this.pieceList = new SplittableList(notEmpty)
  }

  copy() {
    return this.copyWithPieceList(this.pieceList)
  }

  copyWithPieceList(pieceList) {
    return new this.constructor(pieceList.consolidate().toArray())
  }

  copyUsingObjectMap(objectMap) {
    const pieces = Array.from(this.getPieces()).map((piece) => objectMap.find(piece) || piece)
    return new this.constructor(pieces)
  }

  appendText(text) {
    return this.insertTextAtPosition(text, this.getLength())
  }

  insertTextAtPosition(text, position) {
    return this.copyWithPieceList(this.pieceList.insertSplittableListAtPosition(text.pieceList, position))
  }

  removeTextAtRange(range) {
    return this.copyWithPieceList(this.pieceList.removeObjectsInRange(range))
  }

  replaceTextAtRange(text, range) {
    return this.removeTextAtRange(range).insertTextAtPosition(text, range[0])
  }

  moveTextFromRangeToPosition(range, position) {
    if (range[0] <= position && position <= range[1]) {
      return
    }
    const text = this.getTextAtRange(range)
    const length = text.getLength()
    if (range[0] < position) {
      position -= length
    }
    return this.removeTextAtRange(range).insertTextAtPosition(text, position)
  }

  addAttributeAtRange(attribute, value, range) {
    const attributes = {}
    attributes[attribute] = value
    return this.addAttributesAtRange(attributes, range)
  }

  addAttributesAtRange(attributes, range) {
    return this.copyWithPieceList(
      this.pieceList.transformObjectsInRange(range, (piece) => piece.copyWithAdditionalAttributes(attributes))
    )
  }

  removeAttributeAtRange(attribute, range) {
    return this.copyWithPieceList(
      this.pieceList.transformObjectsInRange(range, (piece) => piece.copyWithoutAttribute(attribute))
    )
  }

  setAttributesAtRange(attributes, range) {
    return this.copyWithPieceList(
      this.pieceList.transformObjectsInRange(range, (piece) => piece.copyWithAttributes(attributes))
    )
  }

  getAttributesAtPosition(position) {
    return this.pieceList.getObjectAtPosition(position)?.getAttributes() || {}
  }

  getCommonAttributes() {
    const objects = Array.from(this.pieceList.toArray()).map((piece) => piece.getAttributes())
    return Hash.fromCommonAttributesOfObjects(objects).toObject()
  }

  getCommonAttributesAtRange(range) {
    return this.getTextAtRange(range).getCommonAttributes() || {}
  }

  getExpandedRangeForAttributeAtOffset(attributeName, offset) {
    let right
    let left = right = offset
    const length = this.getLength()

    while (left > 0 && this.getCommonAttributesAtRange([ left - 1, right ])[attributeName]) {
      left--
    }
    while (right < length && this.getCommonAttributesAtRange([ offset, right + 1 ])[attributeName]) {
      right++
    }

    return [ left, right ]
  }

  getTextAtRange(range) {
    return this.copyWithPieceList(this.pieceList.getSplittableListInRange(range))
  }

  getStringAtRange(range) {
    return this.pieceList.getSplittableListInRange(range).toString()
  }

  getStringAtPosition(position) {
    return this.getStringAtRange([ position, position + 1 ])
  }

  startsWithString(string) {
    return this.getStringAtRange([ 0, string.length ]) === string
  }

  endsWithString(string) {
    const length = this.getLength()
    return this.getStringAtRange([ length - string.length, length ]) === string
  }

  getAttachmentPieces() {
    return this.pieceList.toArray().filter((piece) => piece.attachment != null)
  }

  getAttachments() {
    return Array.from(this.getAttachmentPieces()).map((piece) => piece.attachment)
  }

  getAttachmentAndPositionById(attachmentId) {
    let position = 0
    for (const piece of Array.from(this.pieceList.toArray())) {
      if (piece.attachment?.id === attachmentId) {
        return { attachment: piece.attachment, position }
      }
      position += piece.length
    }
    return { attachment: null, position: null }
  }

  getAttachmentById(attachmentId) {
    const { attachment, position } = this.getAttachmentAndPositionById(attachmentId)
    return attachment
  }

  getRangeOfAttachment(attachment) {
    const attachmentAndPosition = this.getAttachmentAndPositionById(attachment.id)
    const position = attachmentAndPosition.position
    attachment = attachmentAndPosition.attachment
    if (attachment != null) {
      return [ position, position + 1 ]
    }
  }

  updateAttributesForAttachment(attributes, attachment) {
    const range = this.getRangeOfAttachment(attachment)
    if (range) {
      return this.addAttributesAtRange(attributes, range)
    } else {
      return this
    }
  }

  getLength() {
    return this.pieceList.getEndPosition()
  }

  isEmpty() {
    return this.getLength() === 0
  }

  isEqualTo(text) {
    return super.isEqualTo(text) || text?.pieceList?.isEqualTo(this.pieceList)
  }

  isBlockBreak() {
    return this.getLength() === 1 && this.pieceList.getObjectAtIndex(0).isBlockBreak()
  }

  eachPiece(callback) {
    return this.pieceList.eachObject(callback)
  }

  getPieces() {
    return this.pieceList.toArray()
  }

  getPieceAtPosition(position) {
    return this.pieceList.getObjectAtPosition(position)
  }

  contentsForInspection() {
    return { pieceList: this.pieceList.inspect() }
  }

  toSerializableText() {
    const pieceList = this.pieceList.selectSplittableList((piece) => piece.isSerializable())
    return this.copyWithPieceList(pieceList)
  }

  toString() {
    return this.pieceList.toString()
  }

  toJSON() {
    return this.pieceList.toJSON()
  }

  toConsole() {
    return JSON.stringify(Array.from(this.pieceList.toArray()).map((piece) => JSON.parse(piece.toConsole())))
  }

  // BIDI

  getDirection() {
    return getDirection(this.toString())
  }

  isRTL() {
    return this.getDirection() === "rtl"
  }
}
<|endoftext|>


<commit_before>define(["FileUtils", "EvalUtils", "MemoryArchive", "LocalStorageArchive", "ace/ace", "exampleLog"], function(
    FileUtils,
    EvalUtils,
    MemoryArchive,
    LocalStorageArchive,
    ace,
    exampleLog
) {
    "use strict"

    let Archive = LocalStorageArchive

    // Used for resizing the editor's height
    let dragOriginY

    // Convenience function which examples could use to put up closeable views
    function show(viewFunction) {
        let div = document.createElement("div")

        const ClosableComponent = {
          view(controller, args) {
            return m("div.ba.ma3.pa3.bg-light-purple",
              m("button.fr", {onclick: function () { document.body.removeChild(div) } }, "X"),
              viewFunction()
            )
          }
        }

        document.body.appendChild(div)
        m.mount(div, ClosableComponent)
    }

    const WorkspaceView = {
        editor: null,
        lastLoadedContents: "",
        currentItemIndex: null,
        archiveChoice: "local storage",
        savedItemIndexes: {
            "local storage": null,
            "memory": null,
        },
        aceEditorHeight: 20,
        toastMessages: [],

        oninit() {
            if (Archive.itemCount() === 0) {
                show(function () {
                   return m("div", "To get started with some examples, click \"Show example log\", then \"Load log\", then \"Next\", and then \"Do it\".")
                }))
            }
        },

        changeArchive() {
            const oldChoice = WorkspaceView.archiveChoice
            const newChoice = (WorkspaceView.archiveChoice === "memory" ? "local storage": "memory")

            WorkspaceView.savedItemIndexes[oldChoice] = WorkspaceView.currentItemIndex
            WorkspaceView.archiveChoice = newChoice
            Archive = (newChoice === "memory" ? MemoryArchive : LocalStorageArchive)
            WorkspaceView.currentItemIndex = WorkspaceView.savedItemIndexes[newChoice]
            if (WorkspaceView.currentItemIndex === null) {
                WorkspaceView.setEditorContents("")
            } else {
                WorkspaceView.setEditorContents(Archive.getItem(WorkspaceView.currentItemIndex))
            }
        },

        setEditorContents(newContents, isNotSaved) {
            WorkspaceView.editor.setValue(newContents)
            if (!isNotSaved) {
                WorkspaceView.lastLoadedContents = newContents
            } else {
                WorkspaceView.currentItemIndex = null
            }
            WorkspaceView.editor.selection.clearSelection()
            WorkspaceView.editor.selection.moveCursorFileStart()
        },

        getEditorContents() {
            return WorkspaceView.editor.getValue()
        },

        getSelectedEditorText() {
            let selectedText = WorkspaceView.editor.session.getTextRange(WorkspaceView.editor.getSelectionRange())
            let isNoSelection = false
            if (!selectedText) {
                // assume user wants all text if nothing is selected
                selectedText = WorkspaceView.editor.getValue()
                isNoSelection = true
            }
            return {
                text: selectedText,
                isNoSelection,
            }
        },

        save() {
            const newContents = WorkspaceView.getEditorContents()
            Archive.addItem(newContents)
            WorkspaceView.lastLoadedContents = newContents
            WorkspaceView.currentItemIndex = Archive.itemCount() - 1
        },

        confirmClear(promptText) {
            if (!WorkspaceView.getEditorContents()) return true
            if (WorkspaceView.getEditorContents() === WorkspaceView.lastLoadedContents) return true
            if (!promptText) promptText = "You have unsaved editor changes. Proceed?"
            return confirm(promptText)
        },

        clear() {
            if (!WorkspaceView.confirmClear()) return
            WorkspaceView.setEditorContents("")
            WorkspaceView.currentItemIndex = null
        },

        doIt() {
            const selection = WorkspaceView.getSelectedEditorText()
            try {
                EvalUtils.eval(selection.text)
            } catch (error) {
                WorkspaceView.toast("Eval error:\n" + error)
            }
        },

        printIt() {
            const selection = WorkspaceView.getSelectedEditorText()
            const evalResult = "" + EvalUtils.evalOrError(selection.text)
            if (selection.isNoSelection) { WorkspaceView.editor.selection.moveCursorFileEnd() }
            const selectedRange = WorkspaceView.editor.selection.getRange()
            const start = selectedRange.end
            const end = WorkspaceView.editor.session.insert(start, evalResult)
            WorkspaceView.editor.selection.setRange({start, end})
            WorkspaceView.editor.focus()
        },

        inspectIt() {
            const selection = WorkspaceView.getSelectedEditorText()
            const evalResult = EvalUtils.evalOrError(selection.text)
            console.dir(evalResult)
        },

        importText() {
            if (!WorkspaceView.confirmClear()) return
            FileUtils.loadFromFile((fileName, fileContents) => {
                if (fileContents) {
                    const newContent = fileName + "\n---------------------------------------\n" + fileContents
                    WorkspaceView.setEditorContents(newContent)
                    m.redraw()
                }
            })
        },

        exportText() {
            const fileContents = WorkspaceView.getEditorContents()
            const provisionalFileName = fileContents.split("\n")[0]
            FileUtils.saveToFile(provisionalFileName, fileContents)
        },

        skip(offset) {
            if (!Archive.itemCount()) {
                WorkspaceView.toast("No log items to display. Try saving one first -- or show the example log in the editor and then load it.")
                return
            }
            if (Archive.itemCount() === 1) {
                WorkspaceView.toast("Only one log item to display. Try saving another one first.")
                return
            }
            if (!WorkspaceView.confirmClear()) return
            if (WorkspaceView.currentItemIndex === null) {
                WorkspaceView.currentItemIndex = (offset >= 0 ?  0 : Archive.itemCount() - 1)
            } else {
                WorkspaceView.currentItemIndex = (Archive.itemCount() + WorkspaceView.currentItemIndex + offset) % Archive.itemCount()
            }
            WorkspaceView.setEditorContents(Archive.getItem(WorkspaceView.currentItemIndex))
        },

        previous() { WorkspaceView.skip(-1) },

        next() { WorkspaceView.skip(1) },

        showLog() {
            if (!WorkspaceView.confirmClear()) return
            WorkspaceView.setEditorContents(Archive.textForLog())
            WorkspaceView.currentItemIndex = null
        },

        loadLog() {
            if (Archive.itemCount() && !confirm("Replace all items with entered text for a log?")) return
            try {
                Archive.loadFromLogText(WorkspaceView.getEditorContents())
            } catch (error) {
                WorkspaceView.toast("Problem loading log from editor:\n" + error)
                return
            }
            WorkspaceView.currentItemIndex = null
            // Update lastLoadedContents in case pasted in contents to avoid warning later since data was processed as intended
            WorkspaceView.lastLoadedContents = WorkspaceView.getEditorContents()
            WorkspaceView.toast("Loaded log from editor")
        },

        showExampleLog() {
            if (!WorkspaceView.confirmClear()) return
            WorkspaceView.setEditorContents(JSON.stringify(exampleLog, null, 4))
        },

        toast(message, delay) {
            function removeToastAfterDelay() {
                setTimeout(function() {
                    console.log("toast timeout", WorkspaceView.toastMessages)
                    WorkspaceView.toastMessages.shift()
                    if ( WorkspaceView.toastMessages.length ) { removeToastAfterDelay() }
                    m.redraw()
                }, WorkspaceView.toastMessages[0].delay)
            }
            if (delay === undefined) { delay = 3000 }
            WorkspaceView.toastMessages.push({message, delay})
            if ( WorkspaceView.toastMessages.length === 1) { removeToastAfterDelay() }
        },

        view() {
            return m("main.ma2", [
                m("div#toastDiv.fixed.top-2.left-2.w-40.pa2.fieldset.bg-gold.pl3.pr3.tc.o-90.z-max",
                    { hidden: WorkspaceView.toastMessages.length === 0 },
                    WorkspaceView.toastMessages.length ? WorkspaceView.toastMessages[0].message : ""
                ),
                m("button", {onclick: WorkspaceView.changeArchive}, "Archive: " + WorkspaceView.archiveChoice),
                m("h4.bw24.b--solid.b--blue.pa1",
                    "Current item ",
                    (WorkspaceView.currentItemIndex === null ? "???" : WorkspaceView.currentItemIndex + 1),
                    " of ",
                    Archive.itemCount()
                ),
                m("input#fileInput", { "type" : "file" , "hidden" : true } ),
                m("div.w-90#editor", {
                    style: {
                        height: WorkspaceView.aceEditorHeight + "rem"
                    },
                    oncreate: function() {
                        WorkspaceView.editor = ace.edit("editor")
                        WorkspaceView.editor.getSession().setMode("ace/mode/javascript")
                        WorkspaceView.editor.getSession().setUseSoftTabs(true)
                    },
                    onupdate: function() {
                        WorkspaceView.editor.resize()
                    }
                }),
                m("div.w-90.ba.pa1.bg-light-gray", {
                    // splitter for resizing the editor's height
                    style: { cursor: "grab" },
                    draggable: true,
                    ondragstart: (event) => {
                        dragOriginY = event.screenY
                        event.dataTransfer.setData("Text", event.target.id)
                    },
                    ondragend: (event) => {
                        const yDifference = event.screenY - dragOriginY
                        const lineDifference = Math.floor(yDifference / WorkspaceView.editor.renderer.lineHeight)
                        WorkspaceView.aceEditorHeight = parseInt(WorkspaceView.aceEditorHeight) + lineDifference
                        if (WorkspaceView.aceEditorHeight < 5) { WorkspaceView.aceEditorHeight = 5 }
                        if (WorkspaceView.aceEditorHeight > 100) { WorkspaceView.aceEditorHeight = 100 }
                    },
                }),
                m("button.ma1", { onclick: WorkspaceView.doIt }, "Do it"),
                m("button.ma1", { onclick: WorkspaceView.printIt }, "Print it"),
                m("button.ma1", { onclick: WorkspaceView.inspectIt }, "Inspect it"),
                m("span.pa1"),
                m("button.ma1", { onclick: WorkspaceView.save }, "Save"),
                m("button.ma1", { onclick: WorkspaceView.clear }, "Clear"),
                m("button.ma1", { onclick: WorkspaceView.importText }, "Import"),
                m("button.ma1", { onclick: WorkspaceView.exportText }, "Export"),
                m("br"),
                m("button.ma1", { onclick: WorkspaceView.showExampleLog }, "Show example log"),
                m("button.ma1", { onclick: WorkspaceView.showLog }, "Show current log"),
                m("button.ma1", { onclick: WorkspaceView.loadLog }, "Load log"),
                m("span.pa1"),
                m("button.ma1", { onclick: WorkspaceView.previous }, "Previous"),
                m("button.ma1", { onclick: WorkspaceView.next }, "Next"),
            ])
        },
    }

    return WorkspaceView
})
<commit_msg>Make show function globally accessible for examples<commit_after>define(["FileUtils", "EvalUtils", "MemoryArchive", "LocalStorageArchive", "ace/ace", "exampleLog"], function(
    FileUtils,
    EvalUtils,
    MemoryArchive,
    LocalStorageArchive,
    ace,
    exampleLog
) {
    "use strict"

    let Archive = LocalStorageArchive

    // Used for resizing the editor's height
    let dragOriginY

    // Convenience function which examples could use to put up closeable views
    window.show = function(viewFunction) {
        let div = document.createElement("div")

        const ClosableComponent = {
            view() {
                return m("div.ba.ma3.pa3.bg-light-purple",
                m("button.fr", {onclick: function () { document.body.removeChild(div) } }, "X"),
                viewFunction()
                )
            }
        }

        document.body.appendChild(div)
        m.mount(div, ClosableComponent)
    }

    const WorkspaceView = {
        editor: null,
        lastLoadedContents: "",
        currentItemIndex: null,
        archiveChoice: "local storage",
        savedItemIndexes: {
            "local storage": null,
            "memory": null,
        },
        aceEditorHeight: 20,
        toastMessages: [],

        oninit() {
            if (Archive.itemCount() === 0) {
                window.show(function () {
                    return m("div", `To get started with some example snippets,
                        click "Show example log", then "Load log", then "Next", and then "Do it".
                        Use "Previous" and "Next" to scroll through more example snippets.`)
                })
            }
        },

        changeArchive() {
            const oldChoice = WorkspaceView.archiveChoice
            const newChoice = (WorkspaceView.archiveChoice === "memory" ? "local storage": "memory")

            WorkspaceView.savedItemIndexes[oldChoice] = WorkspaceView.currentItemIndex
            WorkspaceView.archiveChoice = newChoice
            Archive = (newChoice === "memory" ? MemoryArchive : LocalStorageArchive)
            WorkspaceView.currentItemIndex = WorkspaceView.savedItemIndexes[newChoice]
            if (WorkspaceView.currentItemIndex === null) {
                WorkspaceView.setEditorContents("")
            } else {
                WorkspaceView.setEditorContents(Archive.getItem(WorkspaceView.currentItemIndex))
            }
        },

        setEditorContents(newContents, isNotSaved) {
            WorkspaceView.editor.setValue(newContents)
            if (!isNotSaved) {
                WorkspaceView.lastLoadedContents = newContents
            } else {
                WorkspaceView.currentItemIndex = null
            }
            WorkspaceView.editor.selection.clearSelection()
            WorkspaceView.editor.selection.moveCursorFileStart()
        },

        getEditorContents() {
            return WorkspaceView.editor.getValue()
        },

        getSelectedEditorText() {
            let selectedText = WorkspaceView.editor.session.getTextRange(WorkspaceView.editor.getSelectionRange())
            let isNoSelection = false
            if (!selectedText) {
                // assume user wants all text if nothing is selected
                selectedText = WorkspaceView.editor.getValue()
                isNoSelection = true
            }
            return {
                text: selectedText,
                isNoSelection,
            }
        },

        save() {
            const newContents = WorkspaceView.getEditorContents()
            Archive.addItem(newContents)
            WorkspaceView.lastLoadedContents = newContents
            WorkspaceView.currentItemIndex = Archive.itemCount() - 1
        },

        confirmClear(promptText) {
            if (!WorkspaceView.getEditorContents()) return true
            if (WorkspaceView.getEditorContents() === WorkspaceView.lastLoadedContents) return true
            if (!promptText) promptText = "You have unsaved editor changes. Proceed?"
            return confirm(promptText)
        },

        clear() {
            if (!WorkspaceView.confirmClear()) return
            WorkspaceView.setEditorContents("")
            WorkspaceView.currentItemIndex = null
        },

        doIt() {
            const selection = WorkspaceView.getSelectedEditorText()
            try {
                EvalUtils.eval(selection.text)
            } catch (error) {
                WorkspaceView.toast("Eval error:\n" + error)
            }
        },

        printIt() {
            const selection = WorkspaceView.getSelectedEditorText()
            const evalResult = "" + EvalUtils.evalOrError(selection.text)
            if (selection.isNoSelection) { WorkspaceView.editor.selection.moveCursorFileEnd() }
            const selectedRange = WorkspaceView.editor.selection.getRange()
            const start = selectedRange.end
            const end = WorkspaceView.editor.session.insert(start, evalResult)
            WorkspaceView.editor.selection.setRange({start, end})
            WorkspaceView.editor.focus()
        },

        inspectIt() {
            const selection = WorkspaceView.getSelectedEditorText()
            const evalResult = EvalUtils.evalOrError(selection.text)
            console.dir(evalResult)
        },

        importText() {
            if (!WorkspaceView.confirmClear()) return
            FileUtils.loadFromFile((fileName, fileContents) => {
                if (fileContents) {
                    const newContent = fileName + "\n---------------------------------------\n" + fileContents
                    WorkspaceView.setEditorContents(newContent)
                    m.redraw()
                }
            })
        },

        exportText() {
            const fileContents = WorkspaceView.getEditorContents()
            const provisionalFileName = fileContents.split("\n")[0]
            FileUtils.saveToFile(provisionalFileName, fileContents)
        },

        skip(offset) {
            if (!Archive.itemCount()) {
                WorkspaceView.toast("No log items to display. Try saving one first -- or show the example log in the editor and then load it.")
                return
            }
            if (Archive.itemCount() === 1) {
                WorkspaceView.toast("Only one log item to display. Try saving another one first.")
                return
            }
            if (!WorkspaceView.confirmClear()) return
            if (WorkspaceView.currentItemIndex === null) {
                WorkspaceView.currentItemIndex = (offset >= 0 ?  0 : Archive.itemCount() - 1)
            } else {
                WorkspaceView.currentItemIndex = (Archive.itemCount() + WorkspaceView.currentItemIndex + offset) % Archive.itemCount()
            }
            WorkspaceView.setEditorContents(Archive.getItem(WorkspaceView.currentItemIndex))
        },

        previous() { WorkspaceView.skip(-1) },

        next() { WorkspaceView.skip(1) },

        showLog() {
            if (!WorkspaceView.confirmClear()) return
            WorkspaceView.setEditorContents(Archive.textForLog())
            WorkspaceView.currentItemIndex = null
        },

        loadLog() {
            if (Archive.itemCount() && !confirm("Replace all items with entered text for a log?")) return
            try {
                Archive.loadFromLogText(WorkspaceView.getEditorContents())
            } catch (error) {
                WorkspaceView.toast("Problem loading log from editor:\n" + error)
                return
            }
            WorkspaceView.currentItemIndex = null
            // Update lastLoadedContents in case pasted in contents to avoid warning later since data was processed as intended
            WorkspaceView.lastLoadedContents = WorkspaceView.getEditorContents()
            WorkspaceView.toast("Loaded log from editor")
        },

        showExampleLog() {
            if (!WorkspaceView.confirmClear()) return
            WorkspaceView.setEditorContents(JSON.stringify(exampleLog, null, 4))
        },

        toast(message, delay) {
            function removeToastAfterDelay() {
                setTimeout(function() {
                    console.log("toast timeout", WorkspaceView.toastMessages)
                    WorkspaceView.toastMessages.shift()
                    if ( WorkspaceView.toastMessages.length ) { removeToastAfterDelay() }
                    m.redraw()
                }, WorkspaceView.toastMessages[0].delay)
            }
            if (delay === undefined) { delay = 3000 }
            WorkspaceView.toastMessages.push({message, delay})
            if ( WorkspaceView.toastMessages.length === 1) { removeToastAfterDelay() }
        },

        view() {
            return m("main.ma2", [
                m("div#toastDiv.fixed.top-2.left-2.w-40.pa2.fieldset.bg-gold.pl3.pr3.tc.o-90.z-max",
                    { hidden: WorkspaceView.toastMessages.length === 0 },
                    WorkspaceView.toastMessages.length ? WorkspaceView.toastMessages[0].message : ""
                ),
                m("button", {onclick: WorkspaceView.changeArchive}, "Archive: " + WorkspaceView.archiveChoice),
                m("h4.bw24.b--solid.b--blue.pa1",
                    "Current item ",
                    (WorkspaceView.currentItemIndex === null ? "???" : WorkspaceView.currentItemIndex + 1),
                    " of ",
                    Archive.itemCount()
                ),
                m("input#fileInput", { "type" : "file" , "hidden" : true } ),
                m("div.w-90#editor", {
                    style: {
                        height: WorkspaceView.aceEditorHeight + "rem"
                    },
                    oncreate: function() {
                        WorkspaceView.editor = ace.edit("editor")
                        WorkspaceView.editor.getSession().setMode("ace/mode/javascript")
                        WorkspaceView.editor.getSession().setUseSoftTabs(true)
                    },
                    onupdate: function() {
                        WorkspaceView.editor.resize()
                    }
                }),
                m("div.w-90.ba.pa1.bg-light-gray", {
                    // splitter for resizing the editor's height
                    style: { cursor: "grab" },
                    draggable: true,
                    ondragstart: (event) => {
                        dragOriginY = event.screenY
                        event.dataTransfer.setData("Text", event.target.id)
                    },
                    ondragend: (event) => {
                        const yDifference = event.screenY - dragOriginY
                        const lineDifference = Math.floor(yDifference / WorkspaceView.editor.renderer.lineHeight)
                        WorkspaceView.aceEditorHeight = parseInt(WorkspaceView.aceEditorHeight) + lineDifference
                        if (WorkspaceView.aceEditorHeight < 5) { WorkspaceView.aceEditorHeight = 5 }
                        if (WorkspaceView.aceEditorHeight > 100) { WorkspaceView.aceEditorHeight = 100 }
                    },
                }),
                m("button.ma1", { onclick: WorkspaceView.doIt }, "Do it"),
                m("button.ma1", { onclick: WorkspaceView.printIt }, "Print it"),
                m("button.ma1", { onclick: WorkspaceView.inspectIt }, "Inspect it"),
                m("span.pa1"),
                m("button.ma1", { onclick: WorkspaceView.save }, "Save"),
                m("button.ma1", { onclick: WorkspaceView.clear }, "Clear"),
                m("button.ma1", { onclick: WorkspaceView.importText }, "Import"),
                m("button.ma1", { onclick: WorkspaceView.exportText }, "Export"),
                m("br"),
                m("button.ma1", { onclick: WorkspaceView.showExampleLog }, "Show example log"),
                m("button.ma1", { onclick: WorkspaceView.showLog }, "Show current log"),
                m("button.ma1", { onclick: WorkspaceView.loadLog }, "Load log"),
                m("span.pa1"),
                m("button.ma1", { onclick: WorkspaceView.previous }, "Previous"),
                m("button.ma1", { onclick: WorkspaceView.next }, "Next"),
            ])
        },
    }

    return WorkspaceView
})
<|endoftext|>