@domainkit/react 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -925,6 +925,8 @@ function useAccounts({ readOnly: refused, returnTo } = {}) {
925
925
  };
926
926
  }
927
927
  const normalize = (value) => value.trim().toLowerCase().replace(/\.$/, "");
928
+ /** Whether `domain` is the zone or a name under it, compared on whole labels. */
929
+ const contains = (zone, domain) => domain === zone || domain.endsWith(`.${zone}`);
928
930
  /**
929
931
  * The zone a value already sits in: the longest listed zone the value is at or under. Two accounts
930
932
  * can serve the same zone, so `preferred` names the one the customer picked and wins among equals;
@@ -933,10 +935,7 @@ const normalize = (value) => value.trim().toLowerCase().replace(/\.$/, "");
933
935
  const placementOf = (value, zones, preferred) => {
934
936
  const domain = normalize(value);
935
937
  if (domain === "") return null;
936
- const matching = zones.filter((candidate) => {
937
- const zone = normalize(candidate.zone);
938
- return domain === zone || domain.endsWith(`.${zone}`);
939
- });
938
+ const matching = zones.filter((candidate) => contains(normalize(candidate.zone), domain));
940
939
  const longest = matching.reduce((best, candidate) => best === null || normalize(candidate.zone).length > normalize(best.zone).length ? candidate : best, null);
941
940
  if (longest === null) return null;
942
941
  const closest = matching.filter((candidate) => normalize(candidate.zone) === normalize(longest.zone));
@@ -950,35 +949,39 @@ const placementOf = (value, zones, preferred) => {
950
949
  };
951
950
  };
952
951
  /**
953
- * What the customer typed in front of the zone they are reaching for. "mail.ex" against
954
- * `example.com` is the subdomain "mail"; "ex" on its own is none.
952
+ * Where a partly typed zone starts: the labels before it are the subdomain, the rest is the start
953
+ * of the zone. The partial zone begins on a label boundary, and a single trailing label counts only
954
+ * behind a single label, so "mail.ex" reaches for `example.com` while "probe.unrelated.sh" is a
955
+ * finished name rather than the start of `shop.com`.
955
956
  */
956
- const subdomainOf = (value, zone) => {
957
- const typed = normalize(value);
958
- const target = normalize(zone);
959
- if (typed === "" || target.startsWith(typed)) return "";
960
- const cut = typed.lastIndexOf(".");
961
- if (cut < 0) return "";
962
- const head = typed.slice(0, cut);
963
- const tail = typed.slice(cut + 1);
964
- return target.startsWith(tail) ? head : "";
957
+ const partialSplit = (typed, zone) => {
958
+ if (zone.startsWith(typed)) return "";
959
+ for (let cut = typed.indexOf("."); cut >= 0; cut = typed.indexOf(".", cut + 1)) {
960
+ const head = typed.slice(0, cut);
961
+ const tail = typed.slice(cut + 1);
962
+ if (tail === "" || !zone.startsWith(tail)) continue;
963
+ if (tail.includes(".") || !head.includes(".")) return head;
964
+ }
965
+ return null;
965
966
  };
966
- /** Zones the typed value is reaching for: a prefix of the zone, or a subdomain of one. */
967
+ /**
968
+ * Zones the typed value is reaching for. A zone that already holds the name wins outright, the
969
+ * deepest first, so `probe.example.com` offers `example.com` and never a zone that only shares
970
+ * letters with it. Otherwise the value is still being typed: a zone it is a prefix of, or one whose
971
+ * start follows a subdomain.
972
+ */
967
973
  const suggestionsFor = (value, zones) => {
968
974
  const typed = normalize(value);
969
975
  if (typed === "") return zones;
970
- return zones.filter((candidate) => {
971
- const zone = normalize(candidate.zone);
972
- if (zone.startsWith(typed)) return true;
973
- const cut = typed.lastIndexOf(".");
974
- if (cut < 0) return false;
975
- const tail = typed.slice(cut + 1);
976
- return tail !== "" && zone.startsWith(tail);
977
- });
976
+ const holding = zones.filter((candidate) => contains(normalize(candidate.zone), typed));
977
+ if (holding.length > 0) return holding.toSorted((left, right) => normalize(right.zone).length - normalize(left.zone).length);
978
+ return zones.filter((candidate) => partialSplit(typed, normalize(candidate.zone)) !== null);
978
979
  };
979
980
  /** The value a suggestion completes to: whatever subdomain was typed, then the zone. */
980
981
  const completionOf = (value, zone) => {
981
- const subdomain = subdomainOf(value, zone.zone);
982
+ const typed = normalize(value);
983
+ const target = normalize(zone.zone);
984
+ const subdomain = contains(target, typed) ? typed.slice(0, Math.max(typed.length - target.length - 1, 0)) : partialSplit(typed, target) ?? "";
982
985
  return subdomain === "" ? zone.zone : `${subdomain}.${zone.zone}`;
983
986
  };
984
987
  const optionId = (listId, zone) => `${listId}-${zone.connectionId}-${zone.zone}`;