<commit_before>import { hasFormatToParts, hasIntl, padStart, roundTo, hasRelative } from "./util.js";
import * as English from "./english.js";
import Settings from "../settings.js";
import DateTime from "../datetime.js";
import Formatter from "./formatter.js";

let intlDTCache = {};
function getCachedDTF(locString, opts = {}) {
  const key = JSON.stringify([locString, opts]);
  let dtf = intlDTCache[key];
  if (!dtf) {
    dtf = new Intl.DateTimeFormat(locString, opts);
    intlDTCache[key] = dtf;
  }
  return dtf;
}

let intlNumCache = {};
function getCachendINF(locString, opts = {}) {
  const key = JSON.stringify([locString, opts]);
  let inf = intlNumCache[key];
  if (!inf) {
    inf = new Intl.NumberFormat(locString, opts);
    intlNumCache[key] = inf;
  }
  return inf;
}

let intlRelCache = {};
function getCachendRTF(locString, opts = {}) {
  const key = JSON.stringify([locString, opts]);
  let inf = intlRelCache[key];
  if (!inf) {
    inf = new Intl.RelativeTimeFormat(locString, opts);
    intlRelCache[key] = inf;
  }
  return inf;
}

let sysLocaleCache = null;
function systemLocale() {
  if (sysLocaleCache) {
    return sysLocaleCache;
  } else if (hasIntl()) {
    const computedSys = new Intl.DateTimeFormat().resolvedOptions().locale;
    // node sometimes defaults to "und". Override that because that is dumb
    sysLocaleCache = computedSys === "und" ? "en-US" : computedSys;
    return sysLocaleCache;
  } else {
    sysLocaleCache = "en-US";
    return sysLocaleCache;
  }
}

function parseLocaleString(localeStr) {
  // I really want to avoid writing a BCP 47 parser
  // see, e.g. https://github.com/wooorm/bcp-47
  // Instead, we'll do this:

  // a) if the string has no -u extensions, just leave it alone
  // b) if it does, use Intl to resolve everything
  // c) if Intl fails, try again without the -u

  const uIndex = localeStr.indexOf("-u-");
  if (uIndex === -1) {
    return [localeStr];
  } else {
    let options;
    const smaller = localeStr.substring(0, uIndex);
    try {
      options = getCachedDTF(localeStr).resolvedOptions();
    } catch (e) {
      options = getCachedDTF(smaller).resolvedOptions();
    }

    const { numberingSystem, calendar } = options;
    // return the smaller one so that we can append the calendar and numbering overrides to it
    return [smaller, numberingSystem, calendar];
  }
}

function intlConfigString(localeStr, numberingSystem, outputCalendar) {
  if (hasIntl()) {
    if (outputCalendar || numberingSystem) {
      localeStr += "-u";

      if (outputCalendar) {
        localeStr += `-ca-${outputCalendar}`;
      }

      if (numberingSystem) {
        localeStr += `-nu-${numberingSystem}`;
      }
      return localeStr;
    } else {
      return localeStr;
    }
  } else {
    return [];
  }
}

function mapMonths(f) {
  const ms = [];
  for (let i = 1; i <= 12; i++) {
    const dt = DateTime.utc(2016, i, 1);
    ms.push(f(dt));
  }
  return ms;
}

function mapWeekdays(f) {
  const ms = [];
  for (let i = 1; i <= 7; i++) {
    const dt = DateTime.utc(2016, 11, 13 + i);
    ms.push(f(dt));
  }
  return ms;
}

function listStuff(loc, length, defaultOK, englishFn, intlFn) {
  const mode = loc.listingMode(defaultOK);

  if (mode === "error") {
    return null;
  } else if (mode === "en") {
    return englishFn(length);
  } else {
    return intlFn(length);
  }
}

function supportsFastNumbers(loc) {
  if (loc.numberingSystem && loc.numberingSystem !== "latn") {
    return false;
  } else {
    return (
      loc.numberingSystem === "latn" ||
      !loc.locale ||
      loc.locale.startsWith("en") ||
      (hasIntl() && Intl.DateTimeFormat(loc.intl).resolvedOptions().numberingSystem === "latn")
    );
  }
}

/**
 * @private
 */

class PolyeNumberFormatter {
  constructor(intl, forceSimple, opts) {
    this.padTo = opts.padTo || 0;
    this.floor = opts.floor || false;

    if (!forceSimple && hasIntl()) {
      const intlOpts = { useGrouping: false };
      if (opts.padTo > 0) intlOpts.minimumIntegerDigits = opts.padTo;
      this.inf = getCachendINF(intl, intlOpts);
    }
  }

  format(i) {
    if (this.inf) {
      const fixed = this.floor ? Math.floor(i) : i;
      return this.inf.format(fixed);
    } else {
      // to match the browser's numberformatter defaults
      const fixed = this.floor ? Math.floor(i) : roundTo(i, 3);
      return padStart(fixed, this.padTo);
    }
  }
}

/**
 * @private
 */

class PolyDateFormatter {
  constructor(dt, intl, opts) {
    this.opts = opts;
    this.hasIntl = hasIntl();

    let z;
    if (dt.zone.universal && this.hasIntl) {
      // Chromium doesn't support fixed-offset zones like Etc/GMT+8 in its formatter,
      // See https://bugs.chromium.org/p/chromium/issues/detail?id=364374.
      // So we have to make do. Two cases:
      // 1. The format options tell us to show the zone. We can't do that, so the best
      // we can do is format the date in UTC.
      // 2. The format options don't tell us to show the zone. Then we can adjust them
      // the time and tell the formatter to show it to us in UTC, so that the time is right
      // and the bad zone doesn't show up.
      // We can clean all this up when Chrome fixes this.
      z = "UTC";
      if (opts.timeZoneName) {
        this.dt = dt;
      } else {
        this.dt = dt.offset === 0 ? dt : DateTime.fromMillis(dt.ts + dt.offset * 60 * 1000);
      }
    } else if (dt.zone.type === "local") {
      this.dt = dt;
    } else {
      this.dt = dt;
      z = dt.zone.name;
    }

    if (this.hasIntl) {
      const intlOpts = Object.assign({}, this.opts);
      if (z) {
        intlOpts.timeZone = z;
      }
      this.dtf = getCachedDTF(intl, intlOpts);
    }
  }

  format() {
    if (this.hasIntl) {
      return this.dtf.format(this.dt.toJSDate());
    } else {
      const tokenFormat = English.formatString(this.opts),
        loc = Locale.create("en-US");
      return Formatter.create(loc).formatDateTimeFromString(this.dt, tokenFormat);
    }
  }

  formatToParts() {
    if (this.hasIntl && hasFormatToParts()) {
      return this.dtf.formatToParts(this.dt.toJSDate());
    } else {
      // This is kind of a cop out. We actually could do this for English. However, we couldn't do it for intl strings
      // and IMO it's too weird to have an uncanny valley like that
      return [];
    }
  }

  resolvedOptions() {
    if (this.hasIntl) {
      return this.dtf.resolvedOptions();
    } else {
      return {
        locale: "en-US",
        numberingSystem: "latn",
        outputCalendar: "gregory"
      };
    }
  }
}

/**
 * @private
 */
class PolyRelFormatter {
  constructor(intl, isEnglish, opts) {
    this.opts = Object.assign({ style: "long" }, opts);
    if (!isEnglish && hasRelative()) {
      this.rtf = getCachendRTF(intl, opts);
    }
  }

  format(count, unit) {
    if (this.rtf) {
      return this.rtf.format(count, unit);
    } else {
      return English.formatRelativeTime(unit, count, this.opts.numeric, this.opts.style !== "long");
    }
  }

  formatToParts(count, unit) {
    if (this.rtf) {
      return this.rtf.formatToParts(count, unit);
    } else {
      return [];
    }
  }
}

/**
 * @private
 */

export default class Locale {
  static fromOpts(opts) {
    return Locale.create(opts.locale, opts.numberingSystem, opts.outputCalendar, opts.defaultToEN);
  }

  static create(locale, numberingSystem, outputCalendar, defaultToEN = false) {
    const specifiedLocale = locale || Settings.defaultLocale,
      // the system locale is useful for human readable strings but annoying for parsing/formatting known formats
      localeR = specifiedLocale || (defaultToEN ? "en-US" : systemLocale()),
      numberingSystemR = numberingSystem || Settings.defaultNumberingSystem,
      outputCalendarR = outputCalendar || Settings.defaultOutputCalendar;

    return new Locale(localeR, numberingSystemR, outputCalendarR, specifiedLocale);
  }

  static resetCache() {
    sysLocaleCache = null;
    intlDTCache = {};
    intlNumCache = {};
  }

  static fromObject({ locale, numberingSystem, outputCalendar } = {}) {
    return Locale.create(locale, numberingSystem, outputCalendar);
  }

  constructor(locale, numbering, outputCalendar, specifiedLocale) {
    let [parsedLocale, parsedNumberingSystem, parsedOutputCalendar] = parseLocaleString(locale);

    this.locale = parsedLocale;
    this.numberingSystem = numbering || parsedNumberingSystem || null;
    this.outputCalendar = outputCalendar || parsedOutputCalendar || null;
    this.intl = intlConfigString(this.locale, this.numberingSystem, this.outputCalendar);

    this.weekdaysCache = { format: {}, standalone: {} };
    this.monthsCache = { format: {}, standalone: {} };
    this.meridiemCache = null;
    this.eraCache = {};

    this.specifiedLocale = specifiedLocale;
    this.fastNumbersCached = null;
  }

  get fastNumbers() {
    if (this.fastNumbersCached == null) {
      this.fastNumbersCached = supportsFastNumbers(this);
    }

    return this.fastNumbersCached;
  }

  listingMode(defaultOK = true) {
    const intl = hasIntl(),
      hasFTP = intl && hasFormatToParts(),
      isActuallyEn = this.isEnglish(),
      hasNoWeirdness =
        (this.numberingSystem === null || this.numberingSystem === "latn") &&
        (this.outputCalendar === null || this.outputCalendar === "gregory");

    if (!hasFTP && !(isActuallyEn && hasNoWeirdness) && !defaultOK) {
      return "error";
    } else if (!hasFTP || (isActuallyEn && hasNoWeirdness)) {
      return "en";
    } else {
      return "intl";
    }
  }

  clone(alts) {
    if (!alts || Object.getOwnPropertyNames(alts).length === 0) {
      return this;
    } else {
      return Locale.create(
        alts.locale || this.specifiedLocale,
        alts.numberingSystem || this.numberingSystem,
        alts.outputCalendar || this.outputCalendar,
        alts.defaultToEN || false
      );
    }
  }

  redefaultToEN(alts = {}) {
    return this.clone(Object.assign({}, alts, { defaultToEN: true }));
  }

  redefaultToSystem(alts = {}) {
    return this.clone(Object.assign({}, alts, { defaultToEN: false }));
  }

  months(length, format = false, defaultOK = true) {
    return listStuff(this, length, defaultOK, English.months, () => {
      const intl = format ? { month: length, day: "numeric" } : { month: length },
        formatStr = format ? "format" : "standalone";
      if (!this.monthsCache[formatStr][length]) {
        this.monthsCache[formatStr][length] = mapMonths(dt => this.extract(dt, intl, "month"));
      }
      return this.monthsCache[formatStr][length];
    });
  }

  weekdays(length, format = false, defaultOK = true) {
    return listStuff(this, length, defaultOK, English.weekdays, () => {
      const intl = format
          ? { weekday: length, year: "numeric", month: "long", day: "numeric" }
          : { weekday: length },
        formatStr = format ? "format" : "standalone";
      if (!this.weekdaysCache[formatStr][length]) {
        this.weekdaysCache[formatStr][length] = mapWeekdays(dt =>
          this.extract(dt, intl, "weekday")
        );
      }
      return this.weekdaysCache[formatStr][length];
    });
  }

  meridiems(defaultOK = true) {
    return listStuff(
      this,
      undefined,
      defaultOK,
      () => English.meridiems,
      () => {
        // In theory there could be aribitrary day periods. We're gonna assume there are exactly two
        // for AM and PM. This is probably wrong, but it's makes parsing way easier.
        if (!this.meridiemCache) {
          const intl = { hour: "numeric", hour12: true };
          this.meridiemCache = [DateTime.utc(2016, 11, 13, 9), DateTime.utc(2016, 11, 13, 19)].map(
            dt => this.extract(dt, intl, "dayperiod")
          );
        }

        return this.meridiemCache;
      }
    );
  }

  eras(length, defaultOK = true) {
    return listStuff(this, length, defaultOK, English.eras, () => {
      const intl = { era: length };

      // This is utter bullshit. Different calendars are going to define eras totally differently. What I need is the minimum set of dates
      // to definitely enumerate them.
      if (!this.eraCache[length]) {
        this.eraCache[length] = [DateTime.utc(-40, 1, 1), DateTime.utc(2017, 1, 1)].map(dt =>
          this.extract(dt, intl, "era")
        );
      }

      return this.eraCache[length];
    });
  }

  extract(dt, intlOpts, field) {
    const df = this.dtFormatter(dt, intlOpts),
      results = df.formatToParts(),
      matching = results.find(m => m.type.toLowerCase() === field);

    return matching ? matching.value : null;
  }

  numberFormatter(opts = {}) {
    // this forcesimple option is never used (the only caller short-circuits on it, but it seems safer to leave)
    // (in contrast, the rest of the condition is used heavily)
    return new PolyeNumberFormatter(this.intl, opts.forceSimple || this.fastNumbers, opts);
  }

  dtFormatter(dt, intlOpts = {}) {
    return new PolyDateFormatter(dt, this.intl, intlOpts);
  }

  relFormatter(opts = {}) {
    return new PolyRelFormatter(this.intl, this.isEnglish(), opts);
  }

  isEnglish() {
    return (
      this.locale === "en" ||
      this.locale.toLowerCase() === "en-us" ||
      (hasIntl() &&
        Intl.DateTimeFormat(this.intl)
          .resolvedOptions()
          .locale.startsWith("en-us"))
    );
  }

  equals(other) {
    return (
      this.locale === other.locale &&
      this.numberingSystem === other.numberingSystem &&
      this.outputCalendar === other.outputCalendar
    );
  }
}
<commit_msg>Fix typo in PolyNumberFormatter (#442)<commit_after>import { hasFormatToParts, hasIntl, padStart, roundTo, hasRelative } from "./util.js";
import * as English from "./english.js";
import Settings from "../settings.js";
import DateTime from "../datetime.js";
import Formatter from "./formatter.js";

let intlDTCache = {};
function getCachedDTF(locString, opts = {}) {
  const key = JSON.stringify([locString, opts]);
  let dtf = intlDTCache[key];
  if (!dtf) {
    dtf = new Intl.DateTimeFormat(locString, opts);
    intlDTCache[key] = dtf;
  }
  return dtf;
}

let intlNumCache = {};
function getCachendINF(locString, opts = {}) {
  const key = JSON.stringify([locString, opts]);
  let inf = intlNumCache[key];
  if (!inf) {
    inf = new Intl.NumberFormat(locString, opts);
    intlNumCache[key] = inf;
  }
  return inf;
}

let intlRelCache = {};
function getCachendRTF(locString, opts = {}) {
  const key = JSON.stringify([locString, opts]);
  let inf = intlRelCache[key];
  if (!inf) {
    inf = new Intl.RelativeTimeFormat(locString, opts);
    intlRelCache[key] = inf;
  }
  return inf;
}

let sysLocaleCache = null;
function systemLocale() {
  if (sysLocaleCache) {
    return sysLocaleCache;
  } else if (hasIntl()) {
    const computedSys = new Intl.DateTimeFormat().resolvedOptions().locale;
    // node sometimes defaults to "und". Override that because that is dumb
    sysLocaleCache = computedSys === "und" ? "en-US" : computedSys;
    return sysLocaleCache;
  } else {
    sysLocaleCache = "en-US";
    return sysLocaleCache;
  }
}

function parseLocaleString(localeStr) {
  // I really want to avoid writing a BCP 47 parser
  // see, e.g. https://github.com/wooorm/bcp-47
  // Instead, we'll do this:

  // a) if the string has no -u extensions, just leave it alone
  // b) if it does, use Intl to resolve everything
  // c) if Intl fails, try again without the -u

  const uIndex = localeStr.indexOf("-u-");
  if (uIndex === -1) {
    return [localeStr];
  } else {
    let options;
    const smaller = localeStr.substring(0, uIndex);
    try {
      options = getCachedDTF(localeStr).resolvedOptions();
    } catch (e) {
      options = getCachedDTF(smaller).resolvedOptions();
    }

    const { numberingSystem, calendar } = options;
    // return the smaller one so that we can append the calendar and numbering overrides to it
    return [smaller, numberingSystem, calendar];
  }
}

function intlConfigString(localeStr, numberingSystem, outputCalendar) {
  if (hasIntl()) {
    if (outputCalendar || numberingSystem) {
      localeStr += "-u";

      if (outputCalendar) {
        localeStr += `-ca-${outputCalendar}`;
      }

      if (numberingSystem) {
        localeStr += `-nu-${numberingSystem}`;
      }
      return localeStr;
    } else {
      return localeStr;
    }
  } else {
    return [];
  }
}

function mapMonths(f) {
  const ms = [];
  for (let i = 1; i <= 12; i++) {
    const dt = DateTime.utc(2016, i, 1);
    ms.push(f(dt));
  }
  return ms;
}

function mapWeekdays(f) {
  const ms = [];
  for (let i = 1; i <= 7; i++) {
    const dt = DateTime.utc(2016, 11, 13 + i);
    ms.push(f(dt));
  }
  return ms;
}

function listStuff(loc, length, defaultOK, englishFn, intlFn) {
  const mode = loc.listingMode(defaultOK);

  if (mode === "error") {
    return null;
  } else if (mode === "en") {
    return englishFn(length);
  } else {
    return intlFn(length);
  }
}

function supportsFastNumbers(loc) {
  if (loc.numberingSystem && loc.numberingSystem !== "latn") {
    return false;
  } else {
    return (
      loc.numberingSystem === "latn" ||
      !loc.locale ||
      loc.locale.startsWith("en") ||
      (hasIntl() && Intl.DateTimeFormat(loc.intl).resolvedOptions().numberingSystem === "latn")
    );
  }
}

/**
 * @private
 */

class PolyNumberFormatter {
  constructor(intl, forceSimple, opts) {
    this.padTo = opts.padTo || 0;
    this.floor = opts.floor || false;

    if (!forceSimple && hasIntl()) {
      const intlOpts = { useGrouping: false };
      if (opts.padTo > 0) intlOpts.minimumIntegerDigits = opts.padTo;
      this.inf = getCachendINF(intl, intlOpts);
    }
  }

  format(i) {
    if (this.inf) {
      const fixed = this.floor ? Math.floor(i) : i;
      return this.inf.format(fixed);
    } else {
      // to match the browser's numberformatter defaults
      const fixed = this.floor ? Math.floor(i) : roundTo(i, 3);
      return padStart(fixed, this.padTo);
    }
  }
}

/**
 * @private
 */

class PolyDateFormatter {
  constructor(dt, intl, opts) {
    this.opts = opts;
    this.hasIntl = hasIntl();

    let z;
    if (dt.zone.universal && this.hasIntl) {
      // Chromium doesn't support fixed-offset zones like Etc/GMT+8 in its formatter,
      // See https://bugs.chromium.org/p/chromium/issues/detail?id=364374.
      // So we have to make do. Two cases:
      // 1. The format options tell us to show the zone. We can't do that, so the best
      // we can do is format the date in UTC.
      // 2. The format options don't tell us to show the zone. Then we can adjust them
      // the time and tell the formatter to show it to us in UTC, so that the time is right
      // and the bad zone doesn't show up.
      // We can clean all this up when Chrome fixes this.
      z = "UTC";
      if (opts.timeZoneName) {
        this.dt = dt;
      } else {
        this.dt = dt.offset === 0 ? dt : DateTime.fromMillis(dt.ts + dt.offset * 60 * 1000);
      }
    } else if (dt.zone.type === "local") {
      this.dt = dt;
    } else {
      this.dt = dt;
      z = dt.zone.name;
    }

    if (this.hasIntl) {
      const intlOpts = Object.assign({}, this.opts);
      if (z) {
        intlOpts.timeZone = z;
      }
      this.dtf = getCachedDTF(intl, intlOpts);
    }
  }

  format() {
    if (this.hasIntl) {
      return this.dtf.format(this.dt.toJSDate());
    } else {
      const tokenFormat = English.formatString(this.opts),
        loc = Locale.create("en-US");
      return Formatter.create(loc).formatDateTimeFromString(this.dt, tokenFormat);
    }
  }

  formatToParts() {
    if (this.hasIntl && hasFormatToParts()) {
      return this.dtf.formatToParts(this.dt.toJSDate());
    } else {
      // This is kind of a cop out. We actually could do this for English. However, we couldn't do it for intl strings
      // and IMO it's too weird to have an uncanny valley like that
      return [];
    }
  }

  resolvedOptions() {
    if (this.hasIntl) {
      return this.dtf.resolvedOptions();
    } else {
      return {
        locale: "en-US",
        numberingSystem: "latn",
        outputCalendar: "gregory"
      };
    }
  }
}

/**
 * @private
 */
class PolyRelFormatter {
  constructor(intl, isEnglish, opts) {
    this.opts = Object.assign({ style: "long" }, opts);
    if (!isEnglish && hasRelative()) {
      this.rtf = getCachendRTF(intl, opts);
    }
  }

  format(count, unit) {
    if (this.rtf) {
      return this.rtf.format(count, unit);
    } else {
      return English.formatRelativeTime(unit, count, this.opts.numeric, this.opts.style !== "long");
    }
  }

  formatToParts(count, unit) {
    if (this.rtf) {
      return this.rtf.formatToParts(count, unit);
    } else {
      return [];
    }
  }
}

/**
 * @private
 */

export default class Locale {
  static fromOpts(opts) {
    return Locale.create(opts.locale, opts.numberingSystem, opts.outputCalendar, opts.defaultToEN);
  }

  static create(locale, numberingSystem, outputCalendar, defaultToEN = false) {
    const specifiedLocale = locale || Settings.defaultLocale,
      // the system locale is useful for human readable strings but annoying for parsing/formatting known formats
      localeR = specifiedLocale || (defaultToEN ? "en-US" : systemLocale()),
      numberingSystemR = numberingSystem || Settings.defaultNumberingSystem,
      outputCalendarR = outputCalendar || Settings.defaultOutputCalendar;

    return new Locale(localeR, numberingSystemR, outputCalendarR, specifiedLocale);
  }

  static resetCache() {
    sysLocaleCache = null;
    intlDTCache = {};
    intlNumCache = {};
  }

  static fromObject({ locale, numberingSystem, outputCalendar } = {}) {
    return Locale.create(locale, numberingSystem, outputCalendar);
  }

  constructor(locale, numbering, outputCalendar, specifiedLocale) {
    let [parsedLocale, parsedNumberingSystem, parsedOutputCalendar] = parseLocaleString(locale);

    this.locale = parsedLocale;
    this.numberingSystem = numbering || parsedNumberingSystem || null;
    this.outputCalendar = outputCalendar || parsedOutputCalendar || null;
    this.intl = intlConfigString(this.locale, this.numberingSystem, this.outputCalendar);

    this.weekdaysCache = { format: {}, standalone: {} };
    this.monthsCache = { format: {}, standalone: {} };
    this.meridiemCache = null;
    this.eraCache = {};

    this.specifiedLocale = specifiedLocale;
    this.fastNumbersCached = null;
  }

  get fastNumbers() {
    if (this.fastNumbersCached == null) {
      this.fastNumbersCached = supportsFastNumbers(this);
    }

    return this.fastNumbersCached;
  }

  listingMode(defaultOK = true) {
    const intl = hasIntl(),
      hasFTP = intl && hasFormatToParts(),
      isActuallyEn = this.isEnglish(),
      hasNoWeirdness =
        (this.numberingSystem === null || this.numberingSystem === "latn") &&
        (this.outputCalendar === null || this.outputCalendar === "gregory");

    if (!hasFTP && !(isActuallyEn && hasNoWeirdness) && !defaultOK) {
      return "error";
    } else if (!hasFTP || (isActuallyEn && hasNoWeirdness)) {
      return "en";
    } else {
      return "intl";
    }
  }

  clone(alts) {
    if (!alts || Object.getOwnPropertyNames(alts).length === 0) {
      return this;
    } else {
      return Locale.create(
        alts.locale || this.specifiedLocale,
        alts.numberingSystem || this.numberingSystem,
        alts.outputCalendar || this.outputCalendar,
        alts.defaultToEN || false
      );
    }
  }

  redefaultToEN(alts = {}) {
    return this.clone(Object.assign({}, alts, { defaultToEN: true }));
  }

  redefaultToSystem(alts = {}) {
    return this.clone(Object.assign({}, alts, { defaultToEN: false }));
  }

  months(length, format = false, defaultOK = true) {
    return listStuff(this, length, defaultOK, English.months, () => {
      const intl = format ? { month: length, day: "numeric" } : { month: length },
        formatStr = format ? "format" : "standalone";
      if (!this.monthsCache[formatStr][length]) {
        this.monthsCache[formatStr][length] = mapMonths(dt => this.extract(dt, intl, "month"));
      }
      return this.monthsCache[formatStr][length];
    });
  }

  weekdays(length, format = false, defaultOK = true) {
    return listStuff(this, length, defaultOK, English.weekdays, () => {
      const intl = format
          ? { weekday: length, year: "numeric", month: "long", day: "numeric" }
          : { weekday: length },
        formatStr = format ? "format" : "standalone";
      if (!this.weekdaysCache[formatStr][length]) {
        this.weekdaysCache[formatStr][length] = mapWeekdays(dt =>
          this.extract(dt, intl, "weekday")
        );
      }
      return this.weekdaysCache[formatStr][length];
    });
  }

  meridiems(defaultOK = true) {
    return listStuff(
      this,
      undefined,
      defaultOK,
      () => English.meridiems,
      () => {
        // In theory there could be aribitrary day periods. We're gonna assume there are exactly two
        // for AM and PM. This is probably wrong, but it's makes parsing way easier.
        if (!this.meridiemCache) {
          const intl = { hour: "numeric", hour12: true };
          this.meridiemCache = [DateTime.utc(2016, 11, 13, 9), DateTime.utc(2016, 11, 13, 19)].map(
            dt => this.extract(dt, intl, "dayperiod")
          );
        }

        return this.meridiemCache;
      }
    );
  }

  eras(length, defaultOK = true) {
    return listStuff(this, length, defaultOK, English.eras, () => {
      const intl = { era: length };

      // This is utter bullshit. Different calendars are going to define eras totally differently. What I need is the minimum set of dates
      // to definitely enumerate them.
      if (!this.eraCache[length]) {
        this.eraCache[length] = [DateTime.utc(-40, 1, 1), DateTime.utc(2017, 1, 1)].map(dt =>
          this.extract(dt, intl, "era")
        );
      }

      return this.eraCache[length];
    });
  }

  extract(dt, intlOpts, field) {
    const df = this.dtFormatter(dt, intlOpts),
      results = df.formatToParts(),
      matching = results.find(m => m.type.toLowerCase() === field);

    return matching ? matching.value : null;
  }

  numberFormatter(opts = {}) {
    // this forcesimple option is never used (the only caller short-circuits on it, but it seems safer to leave)
    // (in contrast, the rest of the condition is used heavily)
    return new PolyNumberFormatter(this.intl, opts.forceSimple || this.fastNumbers, opts);
  }

  dtFormatter(dt, intlOpts = {}) {
    return new PolyDateFormatter(dt, this.intl, intlOpts);
  }

  relFormatter(opts = {}) {
    return new PolyRelFormatter(this.intl, this.isEnglish(), opts);
  }

  isEnglish() {
    return (
      this.locale === "en" ||
      this.locale.toLowerCase() === "en-us" ||
      (hasIntl() &&
        Intl.DateTimeFormat(this.intl)
          .resolvedOptions()
          .locale.startsWith("en-us"))
    );
  }

  equals(other) {
    return (
      this.locale === other.locale &&
      this.numberingSystem === other.numberingSystem &&
      this.outputCalendar === other.outputCalendar
    );
  }
}
<|endoftext|>


<commit_before>//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Creates color key
//-------------------------------------------------------------------

d3plus.info.legend = function(vars) {

  var key_display = true

  if (vars.legend.value && vars.color.key) {

    if (!vars.color_scale) {

      var color_groups = {}
      vars.data.pool.forEach(function(d){
        var color = d3plus.variable.color(vars,d[vars.id.key])
        if (!color_groups[color]) {
          color_groups[color] = []
        }
        color_groups[color].push(d)
      })

      var colors = []
      for (color in color_groups) {

        var obj = {
          "color": color
        }

        if (vars.depth.value > 0) {

          for (var i = vars.depth.value-1; i >= 0; i--) {
            var parents = d3plus.utils.uniques(color_groups[color],vars.id.nesting[i])
            if (parents.length == 1) {
              if (!obj.name) {
                var name = d3plus.variable.text(vars,parents[0],i)
                if (name) {
                  obj.name = name
                }
              }
              if (!obj.icon) {
                var icon = d3plus.variable.value(vars,parents[0],vars.icon.key,vars.id.nesting[i])
                if (icon) {
                  obj.icon = icon
                }
              }
            }
            if (obj.name && obj.icon) {
              break;
            }
          }

        }
        else {

          for (d in color_groups[color]) {
            if (!obj.name) {
              var name = d3plus.variable.text(vars,color_groups[color][d],vars.depth.value)
              if (name) {
                obj.name = name
              }
            }
            if (!obj.icon) {
              var icon = d3plus.variable.value(vars,color_groups[color][d],vars.icon.key)
              if (icon) {
                obj.icon = icon
              }
            }
            if (obj.name && obj.icon) {
              break;
            }
          }

        }

        colors.push(obj)

      }

      var available_width = vars.width.value

      var square_size = vars.style.legend.size

      var key_width = square_size*colors.length+vars.style.legend.padding*(colors.length+1)

      if (square_size instanceof Array) {
        for (var i = square_size[1]; i >= square_size[0]; i--) {
          key_width = i*colors.length+vars.style.legend.padding*(colors.length+1)
          if (available_width >= key_width) {
            square_size = i
            break;
          }
        }
      }
      else if (typeof square_size != "number" && square_size !== false) {
        square_size = 30
      }

      if (available_width < key_width || colors.length == 1) {
        key_display = false
      }
      else {

        key_width -= vars.style.legend.padding*2

        colors.sort(function(a,b){

          a_value = a.color
          b_value = b.color

          a_value = d3.rgb(a_value).hsl()
          b_value = d3.rgb(b_value).hsl()

          if (a_value.s == 0) a_value = 361
          else a_value = a_value.h
          if (b_value.s == 0) b_value = 361
          else b_value = b_value.h

          return a_value - b_value

        })

        if (vars.style.legend.align == "start") {
          var start_x = vars.style.legend.padding
        }
        else if (vars.style.legend.align == "end") {
          var start_x = available_width - vars.style.legend.padding - key_width
        }
        else {
          var start_x = available_width/2 - key_width/2
        }

        vars.g.key.selectAll("g.scale")
          .transition().duration(vars.style.timing.transitions)
          .attr("opacity",0)
          .remove()

        var keys = vars.g.key.selectAll("g.color")
          .data(colors,function(d){
            return d.url ? d.color+"_"+d.url : d.color
          })

        function position(group) {

          group
            .attr("transform",function(g,i){
              var x = start_x + (i*(vars.style.legend.padding+square_size))
              return "translate("+x+","+vars.style.legend.padding+")"
            })

        }

        var key_enter = keys.enter().append("g")
          .attr("class","color")
          .attr("opacity",0)
          .call(position)

        function style(rect) {

          rect
            .attr("width",square_size)
            .attr("height",square_size)
            .attr("fill",function(g){

              d3.select(this.parentNode).selectAll("text")
                .remove()

              if (g.icon) {

                var short_url = d3plus.utils.strip(g.icon+"_"+g.color)

                var pattern = vars.defs.selectAll("pattern#"+short_url)
                  .data([short_url])

                var pattern_enter = pattern.enter().append("pattern")
                  .attr("id",short_url)
                  .attr("width",square_size)
                  .attr("height",square_size)

                pattern_enter.append("rect")
                  .attr("fill",g.color)
                  .attr("width",square_size)
                  .attr("height",square_size)

                pattern_enter.append("image")
                  .attr("xlink:href",g.icon)
                  .attr("width",square_size)
                  .attr("height",square_size)

                return "url(#"+short_url+")"
              }
              else {

                d3.select(this.parentNode).append("text")
                  .style("font-weight",vars.style.font.weight)
                  .attr("font-family",vars.style.font.family)
                  .attr("text-anchor","start")
                  .attr("fill",d3plus.color.text(g.color))
                  .attr("x",0)
                  .attr("y",0)
                  .each(function(t){

                    if (g.name) {

                      d3plus.utils.wordwrap({
                        "text": g.name,
                        "parent": this,
                        "width": square_size-vars.style.legend.padding*2,
                        "height": square_size-vars.style.legend.padding*2,
                        "resize": true
                      })

                    }

                  })
                  .attr("y",function(t){
                    var h = this.getBBox().height,
                        diff = parseFloat(d3.select(this).style("font-size"),10)/5
                    return square_size/2 - h/2 - diff/2
                  })
                  .selectAll("tspan")
                    .attr("x",function(t){
                      var w = this.getComputedTextLength()
                      return square_size/2 - w/2
                    })

                return g.color
              }

            })

        }

        key_enter
          .append("rect")
            .attr("class","color")
            .call(style)

        keys
          .order()
          .on(d3plus.evt.over,function(d,i){

            d3.select(this).style("cursor","pointer")

            if (d.name) {

              d3.select(this).style("cursor","pointer")

              var x = start_x + (i*(vars.style.legend.padding+square_size)),
                  y = d3.transform(d3.select(this.parentNode).attr("transform")).translate[1]

              x += square_size/2
              y += vars.style.legend.padding+square_size/2

              d3plus.tooltip.create({
                "align": "top center",
                "arrow": true,
                "background": vars.style.tooltip.background,
                "fontcolor": vars.style.tooltip.font.color,
                "fontfamily": vars.style.tooltip.font.family,
                "fontweight": vars.style.tooltip.font.weight,
                // "data": tooltip_data,
                "color": d.color,
                "icon": d.icon,
                "id": "key",
                // "mouseevents": mouse,
                "offset": square_size/2-vars.style.legend.padding,
                "parent": vars.parent,
                "style": vars.style.icon,
                "title": d.name[0],
                "x": x,
                "y": y,
                "width": "auto"
              })

            }

          })
          .on(d3plus.evt.out,function(d){
            d3plus.tooltip.remove("key")
          })
          .transition().duration(vars.style.timing.transitions)
          .attr("opacity",1)
          .call(position)

        keys.selectAll("rect.color").transition().duration(vars.style.timing.transitions)
          .call(style)

        keys.exit()
          .transition().duration(vars.style.timing.transitions)
          .attr("opacity",0)
          .remove()

      }

    }
    else {

      vars.g.key.selectAll("g.color")
        .transition().duration(vars.style.timing.transitions)
        .attr("opacity",0)
        .remove()

      var values = vars.color_scale.domain(),
          colors = vars.color_scale.range()

      if (values.length <= 2) {
        values = d3plus.utils.buckets(values,6)
      }

      var scale = vars.g.key.selectAll("g.scale")
        .data(["scale"])

      scale.enter().append("g")
        .attr("class","scale")
        .attr("opacity",0)

      var heatmap = vars.defs.selectAll("linearGradient#heatmap")
        .data(["heatmap"])

      heatmap.enter().append("linearGradient")
        .attr("id", "heatmap")
        .attr("x1", "0%")
        .attr("y1", "0%")
        .attr("x2", "100%")
        .attr("y2", "0%")
        .attr("spreadMethod", "pad");

      var stops = heatmap.selectAll("stop")
        .data(d3.range(0,colors.length))

      stops.enter().append("stop")
        .attr("stop-opacity",1)

      stops
        .attr("offset",function(i){
          return Math.round((i/(colors.length-1))*100)+"%"
        })
        .attr("stop-color",function(i){
          return colors[i]
        })

      stops.exit().remove()

      var gradient = scale.selectAll("rect#gradient")
        .data(["gradient"])

      gradient.enter().append("rect")
        .attr("id","gradient")
        .attr("x",function(d){
          if (vars.style.legend.align == "middle") {
            return vars.width.value/2
          }
          else if (vars.style.legend.align == "end") {
            return vars.width.value
          }
          else {
            return 0
          }
        })
        .attr("y",vars.style.legend.padding)
        .attr("width", 0)
        .attr("height", vars.style.legend.gradient.height)
        .attr("stroke",vars.style.legend.tick.color)
        .attr("stroke-width",1)
        .style("fill", "url(#heatmap)")

      var text = scale.selectAll("text.tick")
        .data(d3.range(0,values.length))

      text.enter().append("text")
        .attr("class","tick")
        .attr("y",0)
        .attr("x",function(d){
          if (vars.style.legend.align == "middle") {
            return vars.width.value/2
          }
          else if (vars.style.legend.align == "end") {
            return vars.width.value
          }
          else {
            return 0
          }
        })

      var label_width = 0

      text
        .order()
        .style("font-weight",vars.style.legend.tick.weight)
        .attr("font-family",vars.style.legend.tick.family)
        .attr("font-size",vars.style.legend.tick.size)
        .attr("text-anchor",vars.style.legend.tick.align)
        .attr("fill",vars.style.legend.tick.color)
        .text(function(d){
          return vars.format(values[d],vars.color.key)
        })
        .attr("y",function(d){
          return this.getBBox().height+vars.style.legend.gradient.height+vars.style.legend.padding*2
        })
        .each(function(d){
          var w = this.offsetWidth
          if (w > label_width) label_width = w
        })

      label_width += vars.style.labels.padding*2

      var key_width = label_width * (values.length-1)

      if (key_width+label_width < vars.width.value) {

        if (key_width+label_width < vars.width.value/2) {
          key_width = vars.width.value/2
          label_width = key_width/values.length
          key_width -= label_width
        }

        if (vars.style.legend.align == "start") {
          var start_x = vars.style.legend.padding
        }
        else if (vars.style.legend.align == "end") {
          var start_x = vars.width.value - vars.style.legend.padding - key_width
        }
        else {
          var start_x = vars.width.value/2 - key_width/2
        }

        text.transition().duration(vars.style.timing.transitions)
          .attr("x",function(d){
            return start_x + (label_width*d)
          })

        text.exit().transition().duration(vars.style.timing.transitions)
          .attr("opacity",0)
          .remove()

        var ticks = scale.selectAll("rect.tick")
          .data(d3.range(0,values.length))

        ticks.enter().append("rect")
          .attr("class","tick")
          .attr("x",function(d){
            if (vars.style.legend.align == "middle") {
              return vars.width.value/2
            }
            else if (vars.style.legend.align == "end") {
              return vars.width.value
            }
            else {
              return 0
            }
          })
          .attr("y",vars.style.legend.padding)
          .attr("width",0)
          .attr("height",vars.style.legend.padding+vars.style.legend.gradient.height)
          .attr("fill",vars.style.legend.tick.color)

        ticks.transition().duration(vars.style.timing.transitions)
          .attr("x",function(d){
            var mod = d == 0 ? 1 : 0
            return start_x + (label_width*d) - mod
          })
          .attr("y",vars.style.legend.padding)
          .attr("width",1)
          .attr("height",vars.style.legend.padding+vars.style.legend.gradient.height)
          .attr("fill",vars.style.legend.tick.color)

        ticks.exit().transition().duration(vars.style.timing.transitions)
          .attr("width",0)
          .remove()

        gradient.transition().duration(vars.style.timing.transitions)
          .attr("x",function(d){
            if (vars.style.legend.align == "middle") {
              return vars.width.value/2 - key_width/2
            }
            else if (vars.style.legend.align == "end") {
              return vars.width.value - key_width - vars.style.legend.padding
            }
            else {
              return vars.style.legend.padding
            }
          })
          .attr("y",vars.style.legend.padding)
          .attr("width", key_width)
          .attr("height", vars.style.legend.gradient.height)

        scale.transition().duration(vars.style.timing.transitions)
          .attr("opacity",1)

      }
      else {
        key_display = false
      }

    }

  }

  if (vars.legend.value && vars.color.key && key_display) {

    var key_box = vars.g.key.node().getBBox()
    var key_height = key_box.height+key_box.y

    vars.margin.bottom += key_height+vars.style.legend.padding

    vars.g.key.transition().duration(vars.style.timing.transitions)
      .attr("transform","translate(0,"+(vars.height.value-vars.margin.bottom)+")")

  }
  else {

    vars.g.key.transition().duration(vars.style.timing.transitions)
      .attr("transform","translate(0,"+vars.height.value+")")

  }

}
<commit_msg>improved display of legend (hidden with 1 or less value)<commit_after>//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Creates color key
//-------------------------------------------------------------------

d3plus.info.legend = function(vars) {

  var key_display = true

  if (vars.legend.value && vars.color.key) {

    if (!vars.color_scale && vars.data.keys[vars.color.key] != "number") {

      var color_groups = {}
      vars.data.pool.forEach(function(d){
        var color = d3plus.variable.color(vars,d[vars.id.key])
        if (!color_groups[color]) {
          color_groups[color] = []
        }
        color_groups[color].push(d)
      })

      var colors = []
      for (color in color_groups) {

        var obj = {
          "color": color
        }

        if (vars.depth.value > 0) {

          for (var i = vars.depth.value-1; i >= 0; i--) {
            var parents = d3plus.utils.uniques(color_groups[color],vars.id.nesting[i])
            if (parents.length == 1) {
              if (!obj.name) {
                var name = d3plus.variable.text(vars,parents[0],i)
                if (name) {
                  obj.name = name
                }
              }
              if (!obj.icon) {
                var icon = d3plus.variable.value(vars,parents[0],vars.icon.key,vars.id.nesting[i])
                if (icon) {
                  obj.icon = icon
                }
              }
            }
            if (obj.name && obj.icon) {
              break;
            }
          }

        }
        else {

          for (d in color_groups[color]) {
            if (!obj.name) {
              var name = d3plus.variable.text(vars,color_groups[color][d],vars.depth.value)
              if (name) {
                obj.name = name
              }
            }
            if (!obj.icon) {
              var icon = d3plus.variable.value(vars,color_groups[color][d],vars.icon.key)
              if (icon) {
                obj.icon = icon
              }
            }
            if (obj.name && obj.icon) {
              break;
            }
          }

        }

        colors.push(obj)

      }

      var available_width = vars.width.value

      var square_size = vars.style.legend.size

      var key_width = square_size*colors.length+vars.style.legend.padding*(colors.length+1)

      if (square_size instanceof Array) {
        for (var i = square_size[1]; i >= square_size[0]; i--) {
          key_width = i*colors.length+vars.style.legend.padding*(colors.length+1)
          if (available_width >= key_width) {
            square_size = i
            break;
          }
        }
      }
      else if (typeof square_size != "number" && square_size !== false) {
        square_size = 30
      }

      if (available_width < key_width || colors.length == 1) {
        key_display = false
      }
      else {

        key_width -= vars.style.legend.padding*2

        colors.sort(function(a,b){

          a_value = a.color
          b_value = b.color

          a_value = d3.rgb(a_value).hsl()
          b_value = d3.rgb(b_value).hsl()

          if (a_value.s == 0) a_value = 361
          else a_value = a_value.h
          if (b_value.s == 0) b_value = 361
          else b_value = b_value.h

          return a_value - b_value

        })

        if (vars.style.legend.align == "start") {
          var start_x = vars.style.legend.padding
        }
        else if (vars.style.legend.align == "end") {
          var start_x = available_width - vars.style.legend.padding - key_width
        }
        else {
          var start_x = available_width/2 - key_width/2
        }

        vars.g.key.selectAll("g.scale")
          .transition().duration(vars.style.timing.transitions)
          .attr("opacity",0)
          .remove()

        var keys = vars.g.key.selectAll("g.color")
          .data(colors,function(d){
            return d.url ? d.color+"_"+d.url : d.color
          })

        function position(group) {

          group
            .attr("transform",function(g,i){
              var x = start_x + (i*(vars.style.legend.padding+square_size))
              return "translate("+x+","+vars.style.legend.padding+")"
            })

        }

        var key_enter = keys.enter().append("g")
          .attr("class","color")
          .attr("opacity",0)
          .call(position)

        function style(rect) {

          rect
            .attr("width",square_size)
            .attr("height",square_size)
            .attr("fill",function(g){

              d3.select(this.parentNode).selectAll("text")
                .remove()

              if (g.icon) {

                var short_url = d3plus.utils.strip(g.icon+"_"+g.color)

                var pattern = vars.defs.selectAll("pattern#"+short_url)
                  .data([short_url])

                var pattern_enter = pattern.enter().append("pattern")
                  .attr("id",short_url)
                  .attr("width",square_size)
                  .attr("height",square_size)

                pattern_enter.append("rect")
                  .attr("fill",g.color)
                  .attr("width",square_size)
                  .attr("height",square_size)

                pattern_enter.append("image")
                  .attr("xlink:href",g.icon)
                  .attr("width",square_size)
                  .attr("height",square_size)

                return "url(#"+short_url+")"
              }
              else {

                d3.select(this.parentNode).append("text")
                  .style("font-weight",vars.style.font.weight)
                  .attr("font-family",vars.style.font.family)
                  .attr("text-anchor","start")
                  .attr("fill",d3plus.color.text(g.color))
                  .attr("x",0)
                  .attr("y",0)
                  .each(function(t){

                    if (g.name) {

                      d3plus.utils.wordwrap({
                        "text": g.name,
                        "parent": this,
                        "width": square_size-vars.style.legend.padding*2,
                        "height": square_size-vars.style.legend.padding*2,
                        "resize": true
                      })

                    }

                  })
                  .attr("y",function(t){
                    var h = this.getBBox().height,
                        diff = parseFloat(d3.select(this).style("font-size"),10)/5
                    return square_size/2 - h/2 - diff/2
                  })
                  .selectAll("tspan")
                    .attr("x",function(t){
                      var w = this.getComputedTextLength()
                      return square_size/2 - w/2
                    })

                return g.color
              }

            })

        }

        key_enter
          .append("rect")
            .attr("class","color")
            .call(style)

        keys
          .order()
          .on(d3plus.evt.over,function(d,i){

            d3.select(this).style("cursor","pointer")

            if (d.name) {

              d3.select(this).style("cursor","pointer")

              var x = start_x + (i*(vars.style.legend.padding+square_size)),
                  y = d3.transform(d3.select(this.parentNode).attr("transform")).translate[1]

              x += square_size/2
              y += vars.style.legend.padding+square_size/2

              d3plus.tooltip.create({
                "align": "top center",
                "arrow": true,
                "background": vars.style.tooltip.background,
                "fontcolor": vars.style.tooltip.font.color,
                "fontfamily": vars.style.tooltip.font.family,
                "fontweight": vars.style.tooltip.font.weight,
                // "data": tooltip_data,
                "color": d.color,
                "icon": d.icon,
                "id": "key",
                // "mouseevents": mouse,
                "offset": square_size/2-vars.style.legend.padding,
                "parent": vars.parent,
                "style": vars.style.icon,
                "title": d.name[0],
                "x": x,
                "y": y,
                "width": "auto"
              })

            }

          })
          .on(d3plus.evt.out,function(d){
            d3plus.tooltip.remove("key")
          })
          .transition().duration(vars.style.timing.transitions)
          .attr("opacity",1)
          .call(position)

        keys.selectAll("rect.color").transition().duration(vars.style.timing.transitions)
          .call(style)

        keys.exit()
          .transition().duration(vars.style.timing.transitions)
          .attr("opacity",0)
          .remove()

      }

    }
    else if (vars.color_scale) {

      vars.g.key.selectAll("g.color")
        .transition().duration(vars.style.timing.transitions)
        .attr("opacity",0)
        .remove()

      var values = vars.color_scale.domain(),
          colors = vars.color_scale.range()

      if (values.length <= 2) {
        values = d3plus.utils.buckets(values,6)
      }

      var scale = vars.g.key.selectAll("g.scale")
        .data(["scale"])

      scale.enter().append("g")
        .attr("class","scale")
        .attr("opacity",0)

      var heatmap = vars.defs.selectAll("linearGradient#heatmap")
        .data(["heatmap"])

      heatmap.enter().append("linearGradient")
        .attr("id", "heatmap")
        .attr("x1", "0%")
        .attr("y1", "0%")
        .attr("x2", "100%")
        .attr("y2", "0%")
        .attr("spreadMethod", "pad");

      var stops = heatmap.selectAll("stop")
        .data(d3.range(0,colors.length))

      stops.enter().append("stop")
        .attr("stop-opacity",1)

      stops
        .attr("offset",function(i){
          return Math.round((i/(colors.length-1))*100)+"%"
        })
        .attr("stop-color",function(i){
          return colors[i]
        })

      stops.exit().remove()

      var gradient = scale.selectAll("rect#gradient")
        .data(["gradient"])

      gradient.enter().append("rect")
        .attr("id","gradient")
        .attr("x",function(d){
          if (vars.style.legend.align == "middle") {
            return vars.width.value/2
          }
          else if (vars.style.legend.align == "end") {
            return vars.width.value
          }
          else {
            return 0
          }
        })
        .attr("y",vars.style.legend.padding)
        .attr("width", 0)
        .attr("height", vars.style.legend.gradient.height)
        .attr("stroke",vars.style.legend.tick.color)
        .attr("stroke-width",1)
        .style("fill", "url(#heatmap)")

      var text = scale.selectAll("text.tick")
        .data(d3.range(0,values.length))

      text.enter().append("text")
        .attr("class","tick")
        .attr("y",0)
        .attr("x",function(d){
          if (vars.style.legend.align == "middle") {
            return vars.width.value/2
          }
          else if (vars.style.legend.align == "end") {
            return vars.width.value
          }
          else {
            return 0
          }
        })

      var label_width = 0

      text
        .order()
        .style("font-weight",vars.style.legend.tick.weight)
        .attr("font-family",vars.style.legend.tick.family)
        .attr("font-size",vars.style.legend.tick.size)
        .attr("text-anchor",vars.style.legend.tick.align)
        .attr("fill",vars.style.legend.tick.color)
        .text(function(d){
          return vars.format(values[d],vars.color.key)
        })
        .attr("y",function(d){
          return this.getBBox().height+vars.style.legend.gradient.height+vars.style.legend.padding*2
        })
        .each(function(d){
          var w = this.offsetWidth
          if (w > label_width) label_width = w
        })

      label_width += vars.style.labels.padding*2

      var key_width = label_width * (values.length-1)

      if (key_width+label_width < vars.width.value) {

        if (key_width+label_width < vars.width.value/2) {
          key_width = vars.width.value/2
          label_width = key_width/values.length
          key_width -= label_width
        }

        if (vars.style.legend.align == "start") {
          var start_x = vars.style.legend.padding
        }
        else if (vars.style.legend.align == "end") {
          var start_x = vars.width.value - vars.style.legend.padding - key_width
        }
        else {
          var start_x = vars.width.value/2 - key_width/2
        }

        text.transition().duration(vars.style.timing.transitions)
          .attr("x",function(d){
            return start_x + (label_width*d)
          })

        text.exit().transition().duration(vars.style.timing.transitions)
          .attr("opacity",0)
          .remove()

        var ticks = scale.selectAll("rect.tick")
          .data(d3.range(0,values.length))

        ticks.enter().append("rect")
          .attr("class","tick")
          .attr("x",function(d){
            if (vars.style.legend.align == "middle") {
              return vars.width.value/2
            }
            else if (vars.style.legend.align == "end") {
              return vars.width.value
            }
            else {
              return 0
            }
          })
          .attr("y",vars.style.legend.padding)
          .attr("width",0)
          .attr("height",vars.style.legend.padding+vars.style.legend.gradient.height)
          .attr("fill",vars.style.legend.tick.color)

        ticks.transition().duration(vars.style.timing.transitions)
          .attr("x",function(d){
            var mod = d == 0 ? 1 : 0
            return start_x + (label_width*d) - mod
          })
          .attr("y",vars.style.legend.padding)
          .attr("width",1)
          .attr("height",vars.style.legend.padding+vars.style.legend.gradient.height)
          .attr("fill",vars.style.legend.tick.color)

        ticks.exit().transition().duration(vars.style.timing.transitions)
          .attr("width",0)
          .remove()

        gradient.transition().duration(vars.style.timing.transitions)
          .attr("x",function(d){
            if (vars.style.legend.align == "middle") {
              return vars.width.value/2 - key_width/2
            }
            else if (vars.style.legend.align == "end") {
              return vars.width.value - key_width - vars.style.legend.padding
            }
            else {
              return vars.style.legend.padding
            }
          })
          .attr("y",vars.style.legend.padding)
          .attr("width", key_width)
          .attr("height", vars.style.legend.gradient.height)

        scale.transition().duration(vars.style.timing.transitions)
          .attr("opacity",1)

      }
      else {
        key_display = false
      }

    }
    else {
      key_display = false
    }
  }

  if (vars.legend.value && vars.color.key && key_display) {

    var key_box = vars.g.key.node().getBBox()
    var key_height = key_box.height+key_box.y

    vars.margin.bottom += key_height+vars.style.legend.padding

    vars.g.key.transition().duration(vars.style.timing.transitions)
      .attr("transform","translate(0,"+(vars.height.value-vars.margin.bottom)+")")

  }
  else {

    vars.g.key.transition().duration(vars.style.timing.transitions)
      .attr("transform","translate(0,"+vars.height.value+")")

  }

}
<|endoftext|>


<commit_before>/*
	InputMagic v0.1.0
	https://github.com/ideasmashup/input-magic

	By William ANGER, ISC License, July 2014
*/

(function($, window, document, undefined){

	var firstpress = -1, lastpress = +new Date(), count = 0;

	// @bobince, stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript/3561711
	function noregex(str) {
		return str ? str.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&') : '';
	}

	function trim(str, pattern) {
		return str.replace(pattern || /^\s+|\s+$/g, '');
	}

	function inputMagic(element, options) {
		this.options = $.extend({}, this.defaults, options);
		this.element = element;
		this.$ = $(element);
		this.init();
	}

	inputMagic.prototype = {
		defaults : {
			// not yet defined...
		},

		init : function() {
			var im = this;

			this.$.keydown(function(e){
				var keycode = (e.keyCode ? e.keyCode : e.which);
				var show_hint = false;
				var now = +new Date();

				// typing text
				if (count++ == 0) firstpress = now;
				lastpress = now;

				// codes ref : http://www.quirksmode.org/js/keys.html
				console.log("pressing "+ keycode);

				if (keycode == 13) {
					// enter pressed : submit value (?)
					log.console('enter pressed... submit?');

					this.data('hint', '');
				}
				else if (keycode == 8 || keycode == 46) {
					// backspace or suppr : show hint that got deleted (?)
				}
				else if (keycode < 32) {
					// non-printable character : clear hint
					this.data('hint', '');
				}
				else if (keycode >= 33 && keycode <= 40) {
					// arrows keys (home, end, pgup, pgdwn, etc) : clear hint
					this.data('hint', '');
					show_hint = true;
				}
				else {
					// printable characters : show hint
					show_hint = true;
				}

				if (show_hint) {// display hint as requested
					setTimeout(function(){im.autocomplete()}, 10);
				}
			});
		},

		range : function(start, end) {
			if (this.element.createTextRange) {
				var range = this.element.createTextRange();
				range.collapse(true);
				range.moveStart('character', start);
				range.moveEnd('character', end);
				range.select();
			}
			else if (this.element.setSelectionRange) {
				this.element.setSelectionRange(start, end);
			}
		},

		append_hint : function(hint) {
			var text = trim(this.element.value, noregex(hint));

			this.$.val(text + hint);
			this.$.data('hint', hint);
			this.range(text.length, text.length + hint.length);
		},

		seek_hint : function(str, suggest_arr, max_results, offset, suffix) {
			var results = [];
			if (max_results === undefined) max_results = 1;
			if (suffix === undefined) suffix = '';

			for (var i=0; i<suggest_arr.length; i++) {
				if (results < max_results) {
					if (suggest_arr[i].substring(0, str.length) == str) {
						results.push(offset? suggest_arr[i].substr(str.length) + suffix : suggest_arr[i] + suffix);
					}
				}
				else break;
			}

			// return array or single value
			if (results.length == 0) {
				// no match found
				return (max_results == 1) ? suffix : [suffix];
			}
			else {
				// found a match (or more)
				return (max_results == 1) ? results[0] : results;
			}
		},

		autocomplete : function() {
			var value = this.$.val(),
				text = trim(value, noregex(this.$.data('hint'))),
				length = text.length,
				hint = '',
				module = this.$.data('type-obj');

			if (module) {
				hint = module.apply(this, value, text, length);

				if (hint.length > 0) {
					// only suggest non-empty strings
					this.append_hint(hint);
				}
				else {
					this.$.data('hint', '');
				}
			}
			else {
				this.trigger('error', 'cannot find module for type '+ this.$.data('input-type'));
			}
		},

		email_hints : function() {
			var value = this.$.val();
			var text = trim(value, noregex(this.$.data('hint')));
			var len = text.length;
			var hint = '';
			var completeness = Math.min(text.length / 14, 1);

			var ADJS = ['amazing','blithesome','charismatic','decisive','excellent','fantastical','great','heroic','incredible','jolly','kickstarter','light','magical','nice','outstanding','perfect','quality','remarkable','smart','thrilling','ultimate','vibrant','wondrous','xylophone','yes_we_can','zippy'];
			var DOMS = ['hotmail','live','yahoo','gmail','orange','aol','free','numericable'];
			var EXTS = ['com','fr','eu','org','us','net','io'];

			console.log('input='+ text +' ( '+ (completeness*100) +'% )');

			if (text.length < 2) {
				// no completion yet...
				this.trigger('state','incomplete');
			}

			// complete with "best guesses" (or "motivationnal" words)
			if (text.indexOf('@') == -1) {
				if (text.length > 0) {
					hint = this.seek_hint(text, ADJS, 1, true, '@');
				}
			}
			else {
				if (text.charAt(text.length - 1) != '@') {
					var parts = text.split('@', 2);
					var bef = parts[0];
					var aft = parts[1];

					if (aft.indexOf('.') == -1) {
						hint = this.seek_hint(aft, DOMS, 1, true, '.');
						this.trigger('state', 'incomplete');
					}
					else {
						var tmp = aft.split('.', 2);
						var host = tmp[0];
						var ext = tmp[1];

						if (aft.charAt(aft.length - 1) !== '.') {
							if (ext.length > 2) {
								this.trigger('state', 'complete');
							}
							else {
								hint = this.seek_hint(ext, EXTS, 1, true);
								this.trigger('state', 'partial');
							}
						}
						else {
							// no hint
						}

						// no hint
					}
				}
				else {
					// no hint
				}
			}

			if (hint.length > 0) {
				// only suggest non-empty strings
				this.append_hint(hint);
			}
			else {
				this.$.data('hint', '');
			}
		},

		trigger : function(eventtype, data) {
			$(this.element).trigger('input'+ eventtype, data);
		},

		modules : {
			'email' : function() {

			},
		}
	};

	$.fn.inputMagic = function(options) {
		return this.each(function() {
			if (!$.data(this, 'inputMagic')) {
				$.data(this, 'inputMagic', new inputMagic(this, options));
			}
		});
	}

})(jQuery, window, document);
<commit_msg>customizable completion modules api working, email completion converted to module, modules not yet user-definable (using preset ones)<commit_after>/*
	InputMagic v0.1.0
	https://github.com/ideasmashup/input-magic

	By William ANGER, ISC License, July 2014
*/

(function($, window, document, undefined){

	var firstpress = -1, lastpress = +new Date(), count = 0;

	// @bobince, stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript/3561711
	function noregex(str) {
		return str ? str.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&') : '';
	}

	function trim(str, pattern) {
		return str.replace(pattern || /^\s+|\s+$/g, '');
	}

	function inputMagic(element, options) {
		this.options = $.extend({}, this.defaults, options);
		this.element = element;
		this.$ = $(element);
		this.completor;
		this.init();
	}

	inputMagic.prototype = {
		defaults : {
			// not yet defined...
		},

		init : function() {
			var im = this;

			// instantiate appropriate completion module
			var type = this.$.data('type'),
				module = this.modules[type] || null;

			if (module) this.completor = module;
			else this.trigger('error', 'cannot find module for type '+ type);

			this.$.keydown(function(e){
				var keycode = (e.keyCode ? e.keyCode : e.which);
				var show_hint = false;
				var now = +new Date();

				// typing text
				if (count++ == 0) firstpress = now;
				lastpress = now;

				// codes ref : http://www.quirksmode.org/js/keys.html
				console.log("pressing "+ keycode);

				if (keycode == 13) {
					// enter pressed : submit value (?)
					log.console('enter pressed... submit?');

					this.data('hint', '');
				}
				else if (keycode == 8 || keycode == 46) {
					// backspace or suppr : show hint that got deleted (?)
				}
				else if (keycode < 32) {
					// non-printable character : clear hint
					this.data('hint', '');
				}
				else if (keycode >= 33 && keycode <= 40) {
					// arrows keys (home, end, pgup, pgdwn, etc) : clear hint
					this.data('hint', '');
					show_hint = true;
				}
				else {
					// printable characters : show hint
					show_hint = true;
				}

				if (show_hint) {// display hint as requested
					setTimeout(function(){im.autocomplete()}, 10);
				}
			});
		},

		range : function(start, end) {
			if (this.element.createTextRange) {
				var range = this.element.createTextRange();
				range.collapse(true);
				range.moveStart('character', start);
				range.moveEnd('character', end);
				range.select();
			}
			else if (this.element.setSelectionRange) {
				this.element.setSelectionRange(start, end);
			}
		},

		append_hint : function(hint) {
			var text = trim(this.element.value, noregex(hint));

			this.$.val(text + hint);
			this.$.data('hint', hint);
			this.range(text.length, text.length + hint.length);
		},

		seek_hint : function(str, suggest_arr, max_results, offset, suffix) {
			var results = [];
			if (max_results === undefined) max_results = 1;
			if (suffix === undefined) suffix = '';

			for (var i=0; i<suggest_arr.length; i++) {
				if (results < max_results) {
					if (suggest_arr[i].substring(0, str.length) == str) {
						results.push(offset? suggest_arr[i].substr(str.length) + suffix : suggest_arr[i] + suffix);
					}
				}
				else break;
			}

			// return array or single value
			if (results.length == 0) {
				// no match found
				return (max_results == 1) ? suffix : [suffix];
			}
			else {
				// found a match (or more)
				return (max_results == 1) ? results[0] : results;
			}
		},

		autocomplete : function() {
			var value = this.$.val(),
				text = trim(value, noregex(this.$.data('hint'))),
				length = text.length,
				hint = '';

			if (this.completor) {
				hint = this.completor.call(this, value, text, length);

				if (hint.length > 0) {
					// only suggest non-empty strings
					this.append_hint(hint);
				}
				else {
					this.$.data('hint', '');
				}
			}
		},

		trigger : function(eventtype, data) {
			$(this.element).trigger('input'+ eventtype, data);
		},

		modules : {
			'email' : function(value, text, length) {
				var ADJS = ['amazing','blithesome','charismatic','decisive','excellent','fantastical','great','heroic','incredible','jolly','kickstarter','light','magical','nice','outstanding','perfect','quality','remarkable','smart','thrilling','ultimate','vibrant','wondrous','xylophone','yes_we_can','zippy'];
				var DOMS = ['hotmail','live','yahoo','gmail','orange','aol','free','numericable'];
				var EXTS = ['com','fr','eu','org','us','net','io'];

				if (text.length < 2) {
					// no completion yet...
					this.trigger('state','incomplete');
				}

				// complete with "best guesses" (or "motivationnal" words)
				if (text.indexOf('@') == -1) {
					if (text.length > 0) {
						return this.seek_hint(text, ADJS, 1, true, '@');
					}
				}
				else {
					if (text.charAt(text.length - 1) != '@') {
						var parts = text.split('@', 2);
						var bef = parts[0];
						var aft = parts[1];

						if (aft.indexOf('.') == -1) {
							this.trigger('state', 'incomplete');
							return this.seek_hint(aft, DOMS, 1, true, '.');
						}
						else {
							var tmp = aft.split('.', 2);
							var host = tmp[0];
							var ext = tmp[1];

							if (aft.charAt(aft.length - 1) !== '.') {
								if (ext.length > 2) {
									this.trigger('state', 'complete');
								}
								else {
									this.trigger('state', 'partial');
									return this.seek_hint(ext, EXTS, 1, true);
								}
							}
							else {
								// no hint
							}

							// no hint
						}
					}
					else {
						// no hint
					}
				}

				// no hint
				return '';
			}
		}
	};

	$.fn.inputMagic = function(options) {
		return this.each(function() {
			if (!$.data(this, 'inputMagic')) {
				$.data(this, 'inputMagic', new inputMagic(this, options));
			}
		});
	}

})(jQuery, window, document);
<|endoftext|>


<commit_before>import { bindable, customElement } from 'aurelia-templating';
import { bindingMode } from 'aurelia-binding';
import { inject } from 'aurelia-dependency-injection';
import { TaskQueue } from 'aurelia-task-queue';
import { getBooleanFromAttributeValue } from '../common/attributes';
import { MdInputUpdateService } from './input-update-service';

@customElement('md-input')
@inject(Element, TaskQueue, MdInputUpdateService)
export class MdInput {
  static id = 0;

  @bindable() mdLabel = '';
  @bindable({
    defaultBindingMode: bindingMode.oneTime
  }) mdPlaceholder = '';
  @bindable({
    defaultBindingMode: bindingMode.oneTime
  }) mdTextArea = false;
  @bindable({
    defaultBindingMode: bindingMode.oneTime
  }) mdType = 'text';
  @bindable({
    defaultBindingMode: bindingMode.oneTime
  }) mdValidate = false;
  @bindable({
    defaultBindingMode: bindingMode.twoWay
  }) mdValue = '';

  constructor(element, taskQueue, updateService) {
    this.element = element;
    this.taskQueue = taskQueue;
    this.controlId = `md-input-${MdInput.id++}`;
    this.updateService = updateService;
  }

  attached() {
    this.mdTextArea = getBooleanFromAttributeValue(this.mdTextArea);
    if (getBooleanFromAttributeValue(this.mdValidate)) {
      this.input.classList.add('validate');
    }
    if (this.mdPlaceholder) {
      this.input.setAttribute('placeholder', this.mdPlaceholder);
      this.update();
    }
  }

  mdValueChanged() {
    // this.update();
    this.updateService.update();
  }

  update() {
    this.taskQueue.queueMicroTask(() => {
      Materialize.updateTextFields();
      if (this.mdTextArea) {
        $(this.input).trigger('autoresize');
      }
    });
  }
}
<commit_msg>refactor(md-input): removed update method<commit_after>import { bindable, customElement } from 'aurelia-templating';
import { bindingMode } from 'aurelia-binding';
import { inject } from 'aurelia-dependency-injection';
import { TaskQueue } from 'aurelia-task-queue';
import { getBooleanFromAttributeValue } from '../common/attributes';
import { MdInputUpdateService } from './input-update-service';

@customElement('md-input')
@inject(Element, TaskQueue, MdInputUpdateService)
export class MdInput {
  static id = 0;

  @bindable() mdLabel = '';
  @bindable({
    defaultBindingMode: bindingMode.oneTime
  }) mdPlaceholder = '';
  @bindable({
    defaultBindingMode: bindingMode.oneTime
  }) mdTextArea = false;
  @bindable({
    defaultBindingMode: bindingMode.oneTime
  }) mdType = 'text';
  @bindable({
    defaultBindingMode: bindingMode.oneTime
  }) mdValidate = false;
  @bindable({
    defaultBindingMode: bindingMode.twoWay
  }) mdValue = '';

  constructor(element, taskQueue, updateService) {
    this.element = element;
    this.taskQueue = taskQueue;
    this.controlId = `md-input-${MdInput.id++}`;
    this.updateService = updateService;
  }

  attached() {
    this.mdTextArea = getBooleanFromAttributeValue(this.mdTextArea);
    if (getBooleanFromAttributeValue(this.mdValidate)) {
      this.input.classList.add('validate');
    }
    if (this.mdPlaceholder) {
      this.input.setAttribute('placeholder', this.mdPlaceholder);
      this.update();
    }
  }

  mdValueChanged() {
    this.updateService.update();
    if (this.mdTextArea) {
      $(this.input).trigger('autoresize');
    }
  }
}
<|endoftext|>


<commit_before>/**
List - abstract class for inputs that have source option loaded from js array or via ajax

@class list
@extends abstractinput
**/
(function ($) {
    "use strict";

    var List = function (options) {

    };

    $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput);

    $.extend(List.prototype, {
        render: function () {
            var deferred = $.Deferred();

            this.error = null;
            this.onSourceReady(function () {
                this.renderList();
                deferred.resolve();
            }, function () {
                this.error = this.options.sourceError;
                deferred.resolve();
            });

            return deferred.promise();
        },

        html2value: function (html) {
            return null; //can't set value by text
        },

        value2html: function (value, element, display, response) {
            var deferred = $.Deferred(),
                success = function () {
                    if(typeof display === 'function') {
                        //custom display method
                        display.call(element, value, this.sourceData, response);
                    } else {
                        this.value2htmlFinal(value, element);
                    }
                    deferred.resolve();
               };

            //for null value just call success without loading source
            if(value === null) {
               success.call(this);
            } else {
               this.onSourceReady(success, function () { deferred.resolve(); });
            }

            return deferred.promise();
        },

        // ------------- additional functions ------------

        onSourceReady: function (success, error) {
            //run source if it function
            var source;
            if ($.isFunction(this.options.source)) {
                source = this.options.source.call(this.options.scope);
                this.sourceData = null;
            } else {
                source = this.options.source;
            }

            //if allready loaded just call success
            if(this.options.sourceCache && $.isArray(this.sourceData)) {
                success.call(this);
                return;
            }

            //try parse json in single quotes (for double quotes jquery does automatically)
            try {
                source = $.fn.editableutils.tryParseJson(source, false);
            } catch (e) {
                error.call(this);
                return;
            }

            //loading from url
            if (typeof source === 'string') {
                //try to get sourceData from cache
                if(this.options.sourceCache) {
                    var cacheID = source,
                    cache;

                    if (!$(document).data(cacheID)) {
                        $(document).data(cacheID, {});
                    }
                    cache = $(document).data(cacheID);

                    //check for cached data
                    if (cache.loading === false && cache.sourceData) { //take source from cache
                        this.sourceData = cache.sourceData;
                        this.doPrepend();
                        success.call(this);
                        return;
                    } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later
                        cache.callbacks.push($.proxy(function () {
                            this.sourceData = cache.sourceData;
                            this.doPrepend();
                            success.call(this);
                        }, this));

                        //also collecting error callbacks
                        cache.err_callbacks.push($.proxy(error, this));
                        return;
                    } else { //no cache yet, activate it
                        cache.loading = true;
                        cache.callbacks = [];
                        cache.err_callbacks = [];
                    }
                }

                //loading sourceData from server
                $.ajax({
                    url: source,
                    type: 'get',
                    cache: false,
                    dataType: 'json',
                    success: $.proxy(function (data) {
                        if(cache) {
                            cache.loading = false;
                        }
                        this.sourceData = this.makeArray(data);
                        if($.isArray(this.sourceData)) {
                            if(cache) {
                                //store result in cache
                                cache.sourceData = this.sourceData;
                                //run success callbacks for other fields waiting for this source
                                $.each(cache.callbacks, function () { this.call(); });
                            }
                            this.doPrepend();
                            success.call(this);
                        } else {
                            error.call(this);
                            if(cache) {
                                //run error callbacks for other fields waiting for this source
                                $.each(cache.err_callbacks, function () { this.call(); });
                            }
                        }
                    }, this),
                    error: $.proxy(function () {
                        error.call(this);
                        if(cache) {
                             cache.loading = false;
                             //run error callbacks for other fields
                             $.each(cache.err_callbacks, function () { this.call(); });
                        }
                    }, this)
                });
            } else { //options as json/array
                this.sourceData = this.makeArray(source);

                if($.isArray(this.sourceData)) {
                    this.doPrepend();
                    success.call(this);
                } else {
                    error.call(this);
                }
            }
        },

        doPrepend: function () {
            if(this.options.prepend === null || this.options.prepend === undefined) {
                return;
            }

            if(!$.isArray(this.prependData)) {
                //run prepend if it is function (once)
                if ($.isFunction(this.options.prepend)) {
                    this.options.prepend = this.options.prepend.call(this.options.scope);
                }

                //try parse json in single quotes
                this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true);

                //convert prepend from string to object
                if (typeof this.options.prepend === 'string') {
                    this.options.prepend = {'': this.options.prepend};
                }

                this.prependData = this.makeArray(this.options.prepend);
            }

            if($.isArray(this.prependData) && $.isArray(this.sourceData)) {
                this.sourceData = this.prependData.concat(this.sourceData);
            }
        },

        /*
         renders input list
        */
        renderList: function() {
            // this method should be overwritten in child class
        },

         /*
         set element's html by value
        */
        value2htmlFinal: function(value, element) {
            // this method should be overwritten in child class
        },

        /**
        * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}]
        */
        makeArray: function(data) {
            var count, obj, result = [], item, iterateItem;
            if(!data || typeof data === 'string') {
                return null;
            }

            if($.isArray(data)) { //array
                /*
                   function to iterate inside item of array if item is object.
                   Caclulates count of keys in item and store in obj.
                */
                iterateItem = function (k, v) {
                    obj = {value: k, text: v};
                    if(count++ >= 2) {
                        return false;// exit from `each` if item has more than one key.
                    }
                };

                for(var i = 0; i < data.length; i++) {
                    item = data[i];
                    if(typeof item === 'object') {
                        count = 0; //count of keys inside item
                        $.each(item, iterateItem);
                        //case: [{val1: 'text1'}, {val2: 'text2} ...]
                        if(count === 1) {
                            result.push(obj);
                            //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...]
                        } else if(count > 1) {
                            //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text')
                            if(item.children) {
                                item.children = this.makeArray(item.children);
                            }
                            result.push(item);
                        }
                    } else {
                        //case: ['text1', 'text2' ...]
                        result.push({value: item, text: item});
                    }
                }
            } else {  //case: {val1: 'text1', val2: 'text2, ...}
                $.each(data, function (k, v) {
                    result.push({value: k, text: v});
                });
            }
            return result;
        },

        option: function(key, value) {
            this.options[key] = value;
            if(key === 'source') {
                this.sourceData = null;
            }
            if(key === 'prepend') {
                this.prependData = null;
            }
        }

    });

    List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
        /**
        Source data for list.
        If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`
        For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order.

        If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option.

        If **function**, it should return data in format above (since 1.4.0).

        Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only).
        `[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]`

        @property source
        @type string | array | object | function
        @default null
        **/
        source: null,
        /**
        Data automatically prepended to the beginning of dropdown list.

        @property prepend
        @type string | array | object | function
        @default false
        **/
        prepend: false,
        /**
        Error message when list cannot be loaded (e.g. ajax error)

        @property sourceError
        @type string
        @default Error when loading list
        **/
        sourceError: 'Error when loading list',
        /**
        if true and source is **string url** - results will be cached for fields with the same source.
        Usefull for editable column in grid to prevent extra requests.

        @property sourceCache
        @type boolean
        @default true
        @since 1.2.0
        **/
        sourceCache: true
    });

    $.fn.editabletypes.list = List;

}(window.jQuery));
<commit_msg>comments<commit_after>/**
List - abstract class for inputs that have source option loaded from js array or via ajax

@class list
@extends abstractinput
**/
(function ($) {
    "use strict";

    var List = function (options) {

    };

    $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput);

    $.extend(List.prototype, {
        render: function () {
            var deferred = $.Deferred();

            this.error = null;
            this.onSourceReady(function () {
                this.renderList();
                deferred.resolve();
            }, function () {
                this.error = this.options.sourceError;
                deferred.resolve();
            });

            return deferred.promise();
        },

        html2value: function (html) {
            return null; //can't set value by text
        },

        value2html: function (value, element, display, response) {
            var deferred = $.Deferred(),
                success = function () {
                    if(typeof display === 'function') {
                        //custom display method
                        display.call(element, value, this.sourceData, response);
                    } else {
                        this.value2htmlFinal(value, element);
                    }
                    deferred.resolve();
               };

            //for null value just call success without loading source
            if(value === null) {
               success.call(this);
            } else {
               this.onSourceReady(success, function () { deferred.resolve(); });
            }

            return deferred.promise();
        },

        // ------------- additional functions ------------

        onSourceReady: function (success, error) {
            //run source if it function
            var source;
            if ($.isFunction(this.options.source)) {
                source = this.options.source.call(this.options.scope);
                this.sourceData = null;
                //note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed
            } else {
                source = this.options.source;
            }

            //if allready loaded just call success
            if(this.options.sourceCache && $.isArray(this.sourceData)) {
                success.call(this);
                return;
            }

            //try parse json in single quotes (for double quotes jquery does automatically)
            try {
                source = $.fn.editableutils.tryParseJson(source, false);
            } catch (e) {
                error.call(this);
                return;
            }

            //loading from url
            if (typeof source === 'string') {
                //try to get sourceData from cache
                if(this.options.sourceCache) {
                    var cacheID = source,
                    cache;

                    if (!$(document).data(cacheID)) {
                        $(document).data(cacheID, {});
                    }
                    cache = $(document).data(cacheID);

                    //check for cached data
                    if (cache.loading === false && cache.sourceData) { //take source from cache
                        this.sourceData = cache.sourceData;
                        this.doPrepend();
                        success.call(this);
                        return;
                    } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later
                        cache.callbacks.push($.proxy(function () {
                            this.sourceData = cache.sourceData;
                            this.doPrepend();
                            success.call(this);
                        }, this));

                        //also collecting error callbacks
                        cache.err_callbacks.push($.proxy(error, this));
                        return;
                    } else { //no cache yet, activate it
                        cache.loading = true;
                        cache.callbacks = [];
                        cache.err_callbacks = [];
                    }
                }

                //loading sourceData from server
                $.ajax({
                    url: source,
                    type: 'get',
                    cache: false,
                    dataType: 'json',
                    success: $.proxy(function (data) {
                        if(cache) {
                            cache.loading = false;
                        }
                        this.sourceData = this.makeArray(data);
                        if($.isArray(this.sourceData)) {
                            if(cache) {
                                //store result in cache
                                cache.sourceData = this.sourceData;
                                //run success callbacks for other fields waiting for this source
                                $.each(cache.callbacks, function () { this.call(); });
                            }
                            this.doPrepend();
                            success.call(this);
                        } else {
                            error.call(this);
                            if(cache) {
                                //run error callbacks for other fields waiting for this source
                                $.each(cache.err_callbacks, function () { this.call(); });
                            }
                        }
                    }, this),
                    error: $.proxy(function () {
                        error.call(this);
                        if(cache) {
                             cache.loading = false;
                             //run error callbacks for other fields
                             $.each(cache.err_callbacks, function () { this.call(); });
                        }
                    }, this)
                });
            } else { //options as json/array
                this.sourceData = this.makeArray(source);

                if($.isArray(this.sourceData)) {
                    this.doPrepend();
                    success.call(this);
                } else {
                    error.call(this);
                }
            }
        },

        doPrepend: function () {
            if(this.options.prepend === null || this.options.prepend === undefined) {
                return;
            }

            if(!$.isArray(this.prependData)) {
                //run prepend if it is function (once)
                if ($.isFunction(this.options.prepend)) {
                    this.options.prepend = this.options.prepend.call(this.options.scope);
                }

                //try parse json in single quotes
                this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true);

                //convert prepend from string to object
                if (typeof this.options.prepend === 'string') {
                    this.options.prepend = {'': this.options.prepend};
                }

                this.prependData = this.makeArray(this.options.prepend);
            }

            if($.isArray(this.prependData) && $.isArray(this.sourceData)) {
                this.sourceData = this.prependData.concat(this.sourceData);
            }
        },

        /*
         renders input list
        */
        renderList: function() {
            // this method should be overwritten in child class
        },

         /*
         set element's html by value
        */
        value2htmlFinal: function(value, element) {
            // this method should be overwritten in child class
        },

        /**
        * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}]
        */
        makeArray: function(data) {
            var count, obj, result = [], item, iterateItem;
            if(!data || typeof data === 'string') {
                return null;
            }

            if($.isArray(data)) { //array
                /*
                   function to iterate inside item of array if item is object.
                   Caclulates count of keys in item and store in obj.
                */
                iterateItem = function (k, v) {
                    obj = {value: k, text: v};
                    if(count++ >= 2) {
                        return false;// exit from `each` if item has more than one key.
                    }
                };

                for(var i = 0; i < data.length; i++) {
                    item = data[i];
                    if(typeof item === 'object') {
                        count = 0; //count of keys inside item
                        $.each(item, iterateItem);
                        //case: [{val1: 'text1'}, {val2: 'text2} ...]
                        if(count === 1) {
                            result.push(obj);
                            //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...]
                        } else if(count > 1) {
                            //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text')
                            if(item.children) {
                                item.children = this.makeArray(item.children);
                            }
                            result.push(item);
                        }
                    } else {
                        //case: ['text1', 'text2' ...]
                        result.push({value: item, text: item});
                    }
                }
            } else {  //case: {val1: 'text1', val2: 'text2, ...}
                $.each(data, function (k, v) {
                    result.push({value: k, text: v});
                });
            }
            return result;
        },

        option: function(key, value) {
            this.options[key] = value;
            if(key === 'source') {
                this.sourceData = null;
            }
            if(key === 'prepend') {
                this.prependData = null;
            }
        }

    });

    List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
        /**
        Source data for list.
        If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`
        For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order.

        If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option.

        If **function**, it should return data in format above (since 1.4.0).

        Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only).
        `[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]`

        @property source
        @type string | array | object | function
        @default null
        **/
        source: null,
        /**
        Data automatically prepended to the beginning of dropdown list.

        @property prepend
        @type string | array | object | function
        @default false
        **/
        prepend: false,
        /**
        Error message when list cannot be loaded (e.g. ajax error)

        @property sourceError
        @type string
        @default Error when loading list
        **/
        sourceError: 'Error when loading list',
        /**
        if true and source is **string url** - results will be cached for fields with the same source.
        Usefull for editable column in grid to prevent extra requests.

        @property sourceCache
        @type boolean
        @default true
        @since 1.2.0
        **/
        sourceCache: true
    });

    $.fn.editabletypes.list = List;

}(window.jQuery));
<|endoftext|>