PageSourceSearch

https://noscrubs.io/assets/controllers/query_bar_render-0a2e5fbc25…75dba13a09847b9c91e25506083def6178c377f7.js

js noscrubs.io collected 2026-10-04 17:34:07 UTC 5,927 bytes, 137 lines download raw bytes

1// Pure HTML-string builders for the query bar. No Stimulus/DOM state here —
2// everything takes plain data in and returns markup, so the controller only
3// has to orchestrate state changes and hand data to these.
4import {
5  getFieldSchema,
6  humanize,
7  isHiddenValuePredicate,
8  operationLabels,
9  valueOptionsFor,
10} from "controllers/query_bar_schema";
11
12export function escapeHtml(value) {
13  return String(value).replace(
14    /[&<>"']/g,
15    (char) =>
16      ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
17        char
18      ],
19  );
20}
21
22export function placeholderForStage(stage, hasClauses) {
23  if (stage === "operator") return "Choose operator…";
24  if (stage === "value") return "Value…";
25  return hasClauses ? "AND field…" : "Filter… e.g. status:active";
26}
27
28export function draftInputMarkup() {
29  return `<input type="text" autocomplete="off" spellcheck="false" role="combobox" aria-expanded="false"
30    class="min-w-32 flex-1 border-0 bg-transparent p-1 text-sm text-base-content outline-none placeholder:text-base-content/40"
31    data-query-bar-target="input"
32    data-action="input->query-bar#onInput keydown->query-bar#onKeydown paste->query-bar#onPaste blur->query-bar#onBlur focus->query-bar#onFocus" />`;
33}
34
35// A new clause keeps its completed draft segments visible while the next
36// segment is being selected. These spans are intentionally non-interactive:
37// Backspace is the small, reliable way to move back through a draft.
38export function draftClauseMarkup(draft) {
39  if (draft.stage === "field") return draftInputMarkup();
40
41  const fieldLabel = [...draft.path, draft.attribute].map(humanize).join(" → ");
42  const fieldSegment = `<span class="font-medium text-base-content">${escapeHtml(fieldLabel)}</span>`;
43
44  if (draft.stage === "operator") {
45    return `${fieldSegment}${segmentInputMarkup(draft.text)}`;
46  }
47
48  const operatorLabel =
49    operationLabels[draft.predicate]?.label || draft.predicate;
50  const operatorSegment = `<span class="text-base-content/60">${escapeHtml(operatorLabel)}</span>`;
51  return `${fieldSegment}${operatorSegment}${segmentInputMarkup(draft.text)}`;
52}
53
54// A small, bordered input that replaces a single clause segment in place
55// while it's being edited, sized to its content via the `size` attribute so
56// it doesn't balloon into the rest of the bar.
57export function segmentInputMarkup(text) {
58  const size = Math.max(4, text.length + 2);
59  return `<input type="text" autocomplete="off" spellcheck="false" role="combobox" aria-expanded="false" size="${size}"
60    class="w-48 max-w-full flex-none rounded border border-primary bg-transparent px-1 py-0.5 text-sm text-base-content outline-none"
61    data-query-bar-target="input"
62    data-action="input->query-bar#onInput keydown->query-bar#onKeydown paste->query-bar#onPaste blur->query-bar#onBlur focus->query-bar#onFocus" />`;
63}
64
65export function clauseValueLabel(clause, fieldSchema) {
66  if (isHiddenValuePredicate(clause.predicate)) return null;
67
68  const options = valueOptionsFor(fieldSchema, clause.predicate);
69  const values = Array.isArray(clause.value) ? clause.value : [clause.value];
70  return values.map((value) => options?.[value] ?? value).join(", ");
71}
72
73// `activeStage`, when set to "field" | "operator" | "value", swaps that one
74// segment for an inline input while the other segments stay as plain buttons
75// — so editing a clause never removes it or its siblings from view.
76export function clauseMarkup(
77  schemaTree,
78  clause,
79  activeStage = null,
80  activeText = "",
81) {
82  const fieldSchema = getFieldSchema(
83    schemaTree,
84    clause.path,
85    clause.attribute,
86  ) || { type: "string" };
87  const fieldLabel = [...clause.path, clause.attribute]
88    .map(humanize)
89    .join(" → ");
90  const operatorLabel =
91    operationLabels[clause.predicate]?.label || clause.predicate;
92  const valueLabel = clauseValueLabel(clause, fieldSchema);
93
94  const fieldSegment =
95    activeStage === "field"
96      ? segmentInputMarkup(activeText)
97      : `<button type="button" class="font-medium text-base-content hover:underline" data-stage="field" data-action="click->
97query-bar#editSegment">${escapeHtml(fieldLabel)}</button>`;
98
99  const operatorSegment =
100    activeStage === "operator"
101      ? segmentInputMarkup(activeText)
102      : `<button type="button" class="text-base-content/60 hover:underline" data-stage="operator" data-action="click->query-bar#editSegment">${escapeHtml(operatorLabel)}</button>`;
103
104  const valueSegment =
105    activeStage === "value"
106      ? segmentInputMarkup(activeText)
107      : valueLabel === null
108        ? ""
109        : `<button type="button" class="font-semibold text-primary hover:underline" data-stage="value" data-action="click->query-bar#editSegment">${escapeHtml(valueLabel)}</button>`;
110
111  return `
112    <span class="inline-flex items-center gap-1 rounded-md bg-base-200 px-2 py-1 text-sm" data-clause-id="${clause.id}">
113      ${fieldSegment}
114      ${operatorSegment}
115      ${valueSegment}
116      <button type="button" class="text-base-content/40 hover:text-error" data-action="click->query-bar#removeClause" aria-label="Remove filter">×</button>
117    </span>
118  `;
119}
120
121export function suggestionListMarkup(suggestions, highlightedIndex) {
122  return `<ul role="listbox">${suggestions
123    .map(
124      (item, index) => `
125    <li role="option" aria-selected="${index === highlightedIndex}">
126      <button type="button"
127        class="flex w-full items-center justify-between gap-2 px-3 py-1.5 text-left text-sm ${index === highlightedIndex ? "bg-base-200" : ""} ${item.kind === "association" ? "italic text-base-content/60" : "text-base-content"}"
128        data-action="pointerdown->query-bar#pickSuggestion mouseenter->query-bar#hoverSuggestion"
129        data-index="${index}"
130      >
131        <span>${escapeHtml(item.label)}</span>
132        ${item.selected ? '<span aria-hidden="true">✓</span>' : ""}
133      </button>
134    </li>`,
135    )
136    .join("")}</ul>`;
137};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.