diff --git a/core/githubhelper.py b/core/githubhelper.py index df80686b2..b6454ce32 100644 --- a/core/githubhelper.py +++ b/core/githubhelper.py @@ -623,6 +623,7 @@ def parse_libraries_json(self, libraries_json: dict) -> dict: "category": libraries_json.get("category", []), "maintainers": libraries_json.get("maintainers", []), "cxxstd": libraries_json.get("cxxstd"), + "cxxstd_max": libraries_json.get("cxxstd_max"), "cpp20_module_support": libraries_json.get("modules", False), } diff --git a/frontend/fuse-entry.js b/frontend/fuse-entry.js new file mode 100644 index 000000000..7730d00a6 --- /dev/null +++ b/frontend/fuse-entry.js @@ -0,0 +1,2 @@ +import Fuse from 'fuse.js'; +window.Fuse = Fuse; diff --git a/libraries/migrations/0040_libraryversion_cpp_standard_maximum.py b/libraries/migrations/0040_libraryversion_cpp_standard_maximum.py new file mode 100644 index 000000000..44a91758e --- /dev/null +++ b/libraries/migrations/0040_libraryversion_cpp_standard_maximum.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.2 on 2026-05-11 23:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("libraries", "0039_flag_known_bot_commit_authors"), + ] + + operations = [ + migrations.AddField( + model_name="libraryversion", + name="cpp_standard_maximum", + field=models.CharField(blank=True, max_length=50, null=True), + ), + ] diff --git a/libraries/models.py b/libraries/models.py index c1619f5ce..12c2799ed 100644 --- a/libraries/models.py +++ b/libraries/models.py @@ -477,6 +477,17 @@ def category_tags(self): class LibraryVersion(models.Model): + # Source: https://docs.cppalliance.org/contributor-guide/requirements/library-metadata.html + CPP_STANDARD_DISPLAY_NAMES = { + "98": "C++98", + "03": "C++03", + "11": "C++11", + "14": "C++14", + "17": "C++17", + "20": "C++20", + "23": "C++23", + } + version = models.ForeignKey( "versions.Version", related_name="library_version", @@ -512,6 +523,7 @@ class LibraryVersion(models.Model): deletions = models.IntegerField(default=0) files_changed = models.IntegerField(default=0) cpp_standard_minimum = models.CharField(max_length=50, blank=True, null=True) + cpp_standard_maximum = models.CharField(max_length=50, blank=True, null=True) cpp20_module_support = models.BooleanField(default=False) dependencies = models.ManyToManyField( "libraries.Library", @@ -554,19 +566,16 @@ def author_details(self): } def get_cpp_standard_minimum_display(self): - """Returns the display name for the C++ standard, or the value if not found. - - Source of values is - https://docs.cppalliance.org/user-guide/prev/library_metadata.html""" - display_names = { - "98": "C++98", - "03": "C++03", - "11": "C++11", - "14": "C++14", - "17": "C++17", - "20": "C++20", - } - return display_names.get(self.cpp_standard_minimum, self.cpp_standard_minimum) + """Returns the display name for the minimum C++ standard, or the value if not found.""" + return self.CPP_STANDARD_DISPLAY_NAMES.get( + self.cpp_standard_minimum, self.cpp_standard_minimum + ) + + def get_cpp_standard_maximum_display(self): + """Returns the display name for the maximum C++ standard, or the value if not found.""" + return self.CPP_STANDARD_DISPLAY_NAMES.get( + self.cpp_standard_maximum, self.cpp_standard_maximum + ) class Issue(models.Model): diff --git a/libraries/tests/test_github.py b/libraries/tests/test_github.py index f1c0e8b1f..92a1e4063 100644 --- a/libraries/tests/test_github.py +++ b/libraries/tests/test_github.py @@ -68,6 +68,7 @@ def test_get_library_list(library_updater): "github_url": "example.com", "description": "Test description", "cxxstd": "11", + "cxxstd_max": None, "category": ["Test"], "authors": ["John Doe"], "maintainers": ["Jane Doe"], diff --git a/libraries/tests/test_models.py b/libraries/tests/test_models.py index 441e65933..35229da56 100644 --- a/libraries/tests/test_models.py +++ b/libraries/tests/test_models.py @@ -16,6 +16,16 @@ def test_get_cpp_standard_minimum_display(library_version): assert library_version.get_cpp_standard_minimum_display() == "42" +def test_get_cpp_standard_maximum_display(library_version): + library_version.cpp_standard_maximum = "17" + library_version.save() + assert library_version.get_cpp_standard_maximum_display() == "C++17" + + library_version.cpp_standard_maximum = "42" + library_version.save() + assert library_version.get_cpp_standard_maximum_display() == "42" + + def test_github_properties(library): properties = library.github_properties() assert properties["owner"] == "boostorg" diff --git a/libraries/views.py b/libraries/views.py index 4f6ead4ee..866ff6a1d 100644 --- a/libraries/views.py +++ b/libraries/views.py @@ -124,17 +124,29 @@ class LibraryListBase(BoostVersionMixin, V3Mixin, VersionAlertMixin, ListView): template_name = "libraries/grid_list.html" v3_template_name = "v3/library_page.html" - def get_v3_context_data(self, **kwargs): + def get_v3_context_data(self, queryset=None, **kwargs): context = {} - cpp_options = [ - ("all", "All"), - ("cpp03", "C++03"), - ("cpp11", "C++11"), - ("cpp14", "C++14"), - ("cpp17", "C++17"), - ("cpp20", "C++20"), - ("cpp23", "C++23"), + view_str = self.kwargs.get("library_view_str") + + cpp_options = [("all", "All")] + list( + LibraryVersion.CPP_STANDARD_DISPLAY_NAMES.items() + ) + + tiers_present = sorted( + {lv.library.tier for lv in (queryset or []) if lv.library.tier is not None} + ) + + grading_options = [("all", "All")] + [ + (Tier(t).label.lower(), Tier(t).label) for t in tiers_present + ] + + category_options = [ + (c.slug, c.name) for c in self.get_categories(self._selected_version) ] + + request_get = self.request.GET + selected_categories = request_get.getlist("category") + context["library_filter_fields"] = [ { "type": "dropdown", @@ -146,83 +158,124 @@ def get_v3_context_data(self, **kwargs): ("categorized", "Category"), ("grading", "Grading"), ], - "selected": self.kwargs.get("library_view_str"), + "selected": view_str, + "default": "list", "width": "category", + "deselectable": False, + "exclude_from_clear": True, }, { "type": "dropdown", "name": "grading", "label": "Grading", - "options": [ - ("all", "All"), - ("flagship", "Flagship"), - ("core", "Core"), - ("deprecated", "Deprecated"), - ("legacy", "Legacy"), - ], - "selected": "all", + "options": grading_options, + "selected": request_get.get("grading", "all"), + "default": "all", "width": "wide", + "deselectable": True, }, { "type": "dropdown", "name": "min_cpp", "label": "Min. C++ Version", "options": cpp_options, - "selected": "all", + "selected": request_get.get("min_cpp", "all"), + "default": "all", "width": "narrow", + "deselectable": True, }, { "type": "dropdown", "name": "max_cpp", "label": "Max. C++ Version", "options": cpp_options, - "selected": "all", + "selected": request_get.get("max_cpp", "all"), + "default": "all", "width": "narrow", + "deselectable": True, }, { "type": "combo_multi", "name": "category", "label": "Category", - "options": [ - ("algorithms", "Algorithms"), - ("asynchronous", "Asynchronous"), - ("awaitables", "Awaitables"), - ("containers", "Containers"), - ("coroutines", "Coroutines"), - ("correctness", "Correctness"), - # More dummy data to show scrollbar - ("data_processing", "Data processing"), - ("debugging", "Debugging"), - ("file_systems", "File systems"), - ("formatting", "Formatting"), - ("graphics", "Graphics"), - ], + "options": category_options, + "selected_values": selected_categories, "width": "wide", "placeholder": "Search", + "deselectable": True, }, + # Sort is applied client-side via libraryFilter.sortItems(); no + # queryset.order_by() here. The default alphabetical order comes + # from the view's `ordering = "library__name"`. { "type": "dropdown", "name": "sort", "label": "Sort by", "options": [ ("alphabetical", "Alphabetical"), - ("popular", "Most Popular"), - ("updated", "Recently Updated"), - ("release", "Release Date"), + ("popularity", "Most Popular"), ], - "selected": "alphabetical", + "selected": request_get.get("sort", "alphabetical"), + "default": "alphabetical", + "deselectable": True, }, ] - context["library_view_str"] = self.kwargs.get("library_view_str") + context["library_view_str"] = view_str + context["library_filter_defaults"] = { + f["name"]: f["default"] + for f in context["library_filter_fields"] + if "default" in f + } + context["library_filter_clear_url"] = reverse( + "libraries-list", + kwargs={ + "version_slug": self.kwargs.get("version_slug"), + "library_view_str": "list", + }, + ) + + # Compact JSON payload for client-side filtering on list/grid views. + context["library_dataset"] = [ + { + "slug": lv.library.slug, + "name": lv.library.name, + "description": lv.description or lv.library.description or "", + "category_slugs": [c.slug for c in lv.library.categories.all()], + "category_names": [c.name for c in lv.library.categories.all()], + "author_names": [ + a.display_name for a in lv.authors.all() if a.display_name + ], + "cpp_min": lv.get_cpp_standard_minimum_display() or "", + "cpp_max": lv.get_cpp_standard_maximum_display() or "", + "tier": ( + Tier(lv.library.tier).label.lower() + if lv.library.tier is not None + else "" + ), + } + for lv in (queryset or []) + ] + context["library_search_query"] = self.request.GET.get("q", "") return context def render_v3_response(self): """Render the v3 template through Django's standard TemplateView pipeline.""" + queryset = self.get_queryset() + # Resolve selected_version once so get_v3_context_data can reuse it. + self._selected_version = self._resolve_selected_version() context = self.get_context_data( - **self.get_v3_context_data(), object_list=self.get_queryset() + **self.get_v3_context_data(queryset=queryset), object_list=queryset ) return self.render_to_response(context) + def _resolve_selected_version(self): + version_slug = determine_selected_boost_version( + self.kwargs.get("version_slug"), self.request + ) + if version_slug == LATEST_RELEASE_URL_PATH_STR: + return Version.objects.most_recent() + return Version.objects.filter(slug=version_slug).first() + def get_queryset(self): queryset = super().get_queryset() version_slug = determine_selected_boost_version( diff --git a/package.json b/package.json index 7d7456d6c..b50df1a35 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "build": "NODE_ENV=production tailwindcss -i frontend/styles.css -o static/css/styles.css --minify", "builddocs": "NODE_ENV=production tailwindcss -c ./docstailwind.config.js -i frontend/docsstyles.css -o static/css/docsstyles.css --minify", "builduserguide": "NODE_ENV=production tailwindcss -c ./userguidetailwind.config.js -i frontend/userguidestyles.css -o static/css/userguidestyles.css --minify", - "build:wysiwyg": "esbuild frontend/wysiwyg-editor.js --bundle --minify --format=esm --outfile=static/js/v3/wysiwyg-editor.js --external:mermaid" + "build:wysiwyg": "esbuild frontend/wysiwyg-editor.js --bundle --minify --format=esm --outfile=static/js/v3/wysiwyg-editor.js --external:mermaid", + "build:fuse": "esbuild frontend/fuse-entry.js --bundle --minify --format=iife --outfile=static/js/v3/fuse.min.js" }, "dependencies": { "@svgdotjs/svg.js": "^3.2.4", @@ -37,6 +38,7 @@ "htmx": "^0.0.2", "lowlight": "^3.0.0", "dompurify": "^3.2.2", + "fuse.js": "7.3.0", "marked": "^17.0.0", "tailwindcss": "3.2.1", "turndown": "^7.2.0", diff --git a/static/css/v3/forms.css b/static/css/v3/forms.css index e4553b95c..abe4fdbeb 100644 --- a/static/css/v3/forms.css +++ b/static/css/v3/forms.css @@ -311,10 +311,15 @@ .dropdown--combo.dropdown--open .dropdown__trigger, .dropdown__trigger--active { background-color: var(--color-surface-mid, #f7f7f8); - border-color: var(--color-stroke-strong, #05081640); + border-color: var(--color-stroke-weak); border-bottom-left-radius: 0; border-bottom-right-radius: 0; - border-bottom-color: var(--color-stroke-weak, #0508161A); + border-bottom-color: var(--color-stroke-weak); +} + +.dropdown--has-value .dropdown__trigger, +.dropdown--has-value .dropdown__trigger:hover { + border-color: var(--color-stroke-link-accent); } .dropdown__panel { @@ -388,6 +393,17 @@ text-decoration: underline; } +.dropdown__item--disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.dropdown__item--disabled:hover, +.dropdown__item--disabled:focus { + background-color: transparent; + color: var(--color-text-secondary); +} + .field--error .dropdown__trigger { background-color: var(--color-surface-error-weak, #fdf2f2); border-color: var(--color-stroke-error, #d53f3f33); @@ -430,6 +446,29 @@ outline: none; } +.dropdown__clear-btn { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + padding: 0; + margin: 0; + cursor: pointer; + flex-shrink: 0; +} + +.dropdown__clear-btn:focus-visible { + outline: none; +} + +.dropdown__clear-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + color: var(--color-icon-link-accent); +} + .combo__search-icon { width: 16px; height: 16px; diff --git a/static/css/v3/library-filter.css b/static/css/v3/library-filter.css index fd696b5cb..a19c3cb24 100644 --- a/static/css/v3/library-filter.css +++ b/static/css/v3/library-filter.css @@ -22,6 +22,7 @@ display: flex; flex-wrap: wrap; gap: var(--space-medium); + align-items: flex-end; } .library-filter__fields div:has(.btn) { @@ -40,11 +41,11 @@ } .library-filter__field-narrow { - width: 104px; + width: min(120px, fit-content); } .library-filter__field-default { - width: 160px; + width: min(160px, fit-content); } .library-filter__field-wide { diff --git a/static/css/v3/library-item.css b/static/css/v3/library-item.css index eefcedd6b..d8812e6fe 100644 --- a/static/css/v3/library-item.css +++ b/static/css/v3/library-item.css @@ -16,7 +16,7 @@ margin: 0; padding: 0; display: grid; - grid-template-columns: 160px 2fr 1fr 1fr 1fr auto; + grid-template-columns: 160px 2fr 1fr 1fr max-content auto; column-gap: var(--space-xl); max-width: 1408px; width: 100%; @@ -139,7 +139,7 @@ a.library-item__name:hover { @media (max-width: 1024px) { .library-item-list { - grid-template-columns: auto 2fr 1fr 1fr min-content auto; + grid-template-columns: auto 2fr 1fr 1fr max-content auto; /* Figma specifies --space-large here, but long descriptions cause overflow at tablet widths. Using --space-default until content constraints are defined. */ diff --git a/static/css/v3/library-page.css b/static/css/v3/library-page.css index 36dc6634a..c1f8b41e0 100644 --- a/static/css/v3/library-page.css +++ b/static/css/v3/library-page.css @@ -1,12 +1,24 @@ +/* Styles for the V3 library page: page layout, empty state, hero, + list/grid/categorized results, and responsive overrides. */ + +/* === Page Layout === */ + +.v3-container { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.v3-container > .min-vh-110 { + flex: 1 0 auto; + min-height: 0; +} + .library-page__container { padding: 0 var(--space-large); } -/* -============================================= -= Hero Styling = -============================================= -*/ +/* === Hero === */ .library-page-hero { margin-top: var(--space-xl); @@ -61,11 +73,7 @@ html.dark .library-page-hero .hero-image-dark { letter-spacing: var(--letter-spacing-tight); } -/* -====================================== -= List Styling = -====================================== -*/ +/* === List & Grid View === */ .library-item-list { border: 1px solid var(--color-stroke-weak); @@ -103,7 +111,7 @@ html.dark .library-page-hero .hero-image-dark { align-items: center; } -.library-page_list-header-justify { +.library-page__list-header-justify { justify-self: center; padding-left: calc(16px + var(--space-large)); } @@ -117,11 +125,7 @@ html.dark .library-page-hero .hero-image-dark { letter-spacing: var(--letter-spacing-tight); } -/* -================================================= -= Category Display Styling = -================================================= -*/ +/* === Categorized & Grading View === */ .library-page__category-header { grid-column: 1 / -1; @@ -152,6 +156,68 @@ html.dark .library-page-hero .hero-image-dark { } +/* === Empty State (no search/filter results) === */ + +.library-page__empty-state { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-medium); + background-color: var(--color-surface-weak); + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-stroke-weak); + padding: var(--space-large); + padding-bottom: calc(var(--space-large) + var(--space-medium)); + margin-bottom: var(--space-xxl); +} + +.library-page__empty-state[hidden] { + display: none; +} + +.library-page__empty-state-image { + width: 488px; + height: auto; + aspect-ratio: 488 / 416; + max-width: 100%; +} + +.library-page__empty-state-image--dark { + display: none; +} + +html.dark .library-page__empty-state-image--light { + display: none; +} + +html.dark .library-page__empty-state-image--dark { + display: block; +} + +.library-page__empty-state-message { + margin: 0; + padding: 0; + text-align: center; + color: var(--color-text-primary); + font-family: var(--font-sans); + font-size: var(--font-size-medium); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-default); + letter-spacing: var(--letter-spacing-tight); +} + +.library-page__empty-state--in-list { + list-style: none; + width: 100%; + grid-column: 1 / -1; + padding: var(--space-xl) 0; + border: none; + margin-bottom: 0; +} + + +/* === Mobile Overrides (< 768px) === */ + @media (max-width: 767px) { .library-page__list-header { display: none; @@ -175,4 +241,8 @@ html.dark .library-page-hero .hero-image-dark { margin-left: var(--space-medium); margin-right: var(--space-medium); } + + .library-page__empty-state-image { + width: 100%; + } } diff --git a/static/js/v3/fuse.min.js b/static/js/v3/fuse.min.js new file mode 100644 index 000000000..385599954 --- /dev/null +++ b/static/js/v3/fuse.min.js @@ -0,0 +1 @@ +(()=>{function w(s){return Array.isArray?Array.isArray(s):De(s)==="[object Array]"}function Se(s){if(typeof s=="string")return s;if(typeof s=="bigint")return s.toString();let e=s+"";return e=="0"&&1/s==-1/0?"-0":e}function Y(s){return s==null?"":Se(s)}function C(s){return typeof s=="string"}function z(s){return typeof s=="number"}function ve(s){return s===!0||s===!1||we(s)&&De(s)=="[object Boolean]"}function _e(s){return typeof s=="object"}function we(s){return _e(s)&&s!==null}function _(s){return s!=null}function j(s){return!s.trim().length}function De(s){return s==null?s===void 0?"[object Undefined]":"[object Null]":Object.prototype.toString.call(s)}var be="Incorrect 'index' type",ke=s=>`Invalid value for key ${s}`,xe=s=>`Pattern length exceeds max of ${s}.`,Le=s=>`Missing ${s} property in key`,Oe=s=>`Property 'weight' in key '${s}' must be a positive integer`,ge=Object.prototype.hasOwnProperty,U=class{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach(n=>{let i=Fe(n);this._keys.push(i),this._keyMap[i.id]=i,t+=i.weight}),this._keys.forEach(n=>{n.weight/=t})}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}};function Fe(s){let e=null,t=null,n=null,i=1,r=null;if(C(s)||w(s))n=s,e=Ae(s),t=V(s);else{if(!ge.call(s,"name"))throw new Error(Le("name"));let c=s.name;if(n=c,ge.call(s,"weight")&&(i=s.weight,i<=0))throw new Error(Oe(c));e=Ae(c),t=V(c),r=s.getFn}return{path:e,id:t,weight:i,src:n,getFn:r}}function Ae(s){return w(s)?s:s.split(".")}function V(s){return w(s)?s.join("."):s}function Re(s,e){let t=[],n=!1,i=(r,c,h,o)=>{if(_(r))if(!c[h])t.push(o!==void 0?{v:r,i:o}:r);else{let l=c[h],u=r[l];if(!_(u))return;if(h===c.length-1&&(C(u)||z(u)||ve(u)||typeof u=="bigint"))t.push(o!==void 0?{v:Y(u),i:o}:Y(u));else if(w(u)){n=!0;for(let f=0,d=u.length;fs.score===e.score?s.idx{this._keysMap[t.id]=n})}create(){this.isCreated||!this.docs.length||(this.isCreated=!0,C(this.docs[0])?this.docs.forEach((e,t)=>{this._addString(e,t)}):this.docs.forEach((e,t)=>{this._addObject(e,t)}),this.norm.clear())}add(e){let t=this.size();C(e)?this._addString(e,t):this._addObject(e,t)}removeAt(e){this.records.splice(e,1);for(let t=e,n=this.size();t=0;t-=1)this.records.splice(e[t],1);for(let t=0,n=this.records.length;t{let c=i.getFn?i.getFn(e):this.getFn(e,i.path);if(_(c)){if(w(c)){let h=[];for(let o=0,l=c.length;ot),records:this.records}}};function Me(s,e,{getFn:t=g.getFn,fieldNormWeight:n=g.fieldNormWeight}={}){let i=new O({getFn:t,fieldNormWeight:n});return i.setKeys(s.map(Fe)),i.setSources(e),i.create(),i}function We(s,{getFn:e=g.getFn,fieldNormWeight:t=g.fieldNormWeight}={}){let{keys:n,records:i}=s,r=new O({getFn:e,fieldNormWeight:t});return r.setKeys(n),r.setIndexRecords(i),r}function Ke(s=[],e=g.minMatchCharLength){let t=[],n=-1,i=-1,r=0;for(let c=s.length;r=e&&t.push([n,i]),n=-1)}return s[r-1]&&r-n>=e&&t.push([n,r-1]),t}var k=32;function He(s,e,t,{location:n=g.location,distance:i=g.distance,threshold:r=g.threshold,findAllMatches:c=g.findAllMatches,minMatchCharLength:h=g.minMatchCharLength,includeMatches:o=g.includeMatches,ignoreLocation:l=g.ignoreLocation}={}){if(e.length>k)throw new Error(xe(k));let u=e.length,f=s.length,d=Math.max(0,Math.min(n,f)),a=r,p=d,A=(E,I)=>{let D=E/u;if(l)return D;let L=Math.abs(d-I);return i?D+L/i:L?1:D},m=h>1||o,B=m?Array(f):[],S;for(;(S=s.indexOf(e,p))>-1;){let E=A(0,S);if(a=Math.min(E,a),p=S+u,m){let I=0;for(;I=L;F-=1){let T=F-1,de=t[s[T]];if(m&&(B[T]=+!!de),x[F]=(x[F+1]<<1|1)&de,E&&(x[F]|=(v[F+1]|v[F])<<1|1|v[F+1]),x[F]&ye&&(b=A(E,T),b<=a)){if(a=b,p=T,p<=d)break;L=Math.max(1,2*d-p)}}if(A(E+1,d)>a)break;v=x}let Q={isMatch:p>=0,score:Math.max(.001,b)};if(m){let E=Ke(B,h);E.length?o&&(Q.indices=E):Q.isMatch=!1}return Q}function Qe(s){let e={};for(let t=0,n=s.length;tt[0]-n[0]||t[1]-n[1]);let e=[s[0]];for(let t=1,n=s.length;ts.normalize("NFD").replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,"").replace(Ge,e=>Be[e]):s=>s,N=class{constructor(e,{location:t=g.location,threshold:n=g.threshold,distance:i=g.distance,includeMatches:r=g.includeMatches,findAllMatches:c=g.findAllMatches,minMatchCharLength:h=g.minMatchCharLength,isCaseSensitive:o=g.isCaseSensitive,ignoreDiacritics:l=g.ignoreDiacritics,ignoreLocation:u=g.ignoreLocation}={}){if(this.options={location:t,threshold:n,distance:i,includeMatches:r,findAllMatches:c,minMatchCharLength:h,isCaseSensitive:o,ignoreDiacritics:l,ignoreLocation:u},e=o?e:e.toLowerCase(),e=l?R(e):e,this.pattern=e,this.chunks=[],!this.pattern.length)return;let f=(a,p)=>{this.chunks.push({pattern:a,alphabet:Qe(a),startIndex:p})},d=this.pattern.length;if(d>k){let a=0,p=d%k,A=d-p;for(;a{let{isMatch:S,score:v,indices:b}=He(e,A,m,{location:r+B,distance:c,threshold:h,findAllMatches:o,minMatchCharLength:l,includeMatches:i,ignoreLocation:u});S&&(a=!0),d+=v,S&&b&&f.push(...b)});let p={isMatch:a,score:a?d/this.chunks.length:1};return a&&i&&(p.indices=le(f)),p}},y=class{constructor(e){this.pattern=e}static isMultiMatch(e){return pe(e,this.multiRegex)}static isSingleMatch(e){return pe(e,this.singleRegex)}search(e){return{isMatch:!1,score:1}}};function pe(s,e){let t=s.match(e);return t?t[1]:null}var J=class extends y{constructor(e){super(e)}static get type(){return"exact"}static get multiRegex(){return/^="(.*)"$/}static get singleRegex(){return/^=(.*)$/}search(e){let t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}},X=class extends y{constructor(e){super(e)}static get type(){return"inverse-exact"}static get multiRegex(){return/^!"(.*)"$/}static get singleRegex(){return/^!(.*)$/}search(e){let n=e.indexOf(this.pattern)===-1;return{isMatch:n,score:n?0:1,indices:[0,e.length-1]}}},Z=class extends y{constructor(e){super(e)}static get type(){return"prefix-exact"}static get multiRegex(){return/^\^"(.*)"$/}static get singleRegex(){return/^\^(.*)$/}search(e){let t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}},q=class extends y{constructor(e){super(e)}static get type(){return"inverse-prefix-exact"}static get multiRegex(){return/^!\^"(.*)"$/}static get singleRegex(){return/^!\^(.*)$/}search(e){let t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}},ee=class extends y{constructor(e){super(e)}static get type(){return"suffix-exact"}static get multiRegex(){return/^"(.*)"\$$/}static get singleRegex(){return/^(.*)\$$/}search(e){let t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}},te=class extends y{constructor(e){super(e)}static get type(){return"inverse-suffix-exact"}static get multiRegex(){return/^!"(.*)"\$$/}static get singleRegex(){return/^!(.*)\$$/}search(e){let t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}},P=class extends y{constructor(e,{location:t=g.location,threshold:n=g.threshold,distance:i=g.distance,includeMatches:r=g.includeMatches,findAllMatches:c=g.findAllMatches,minMatchCharLength:h=g.minMatchCharLength,isCaseSensitive:o=g.isCaseSensitive,ignoreDiacritics:l=g.ignoreDiacritics,ignoreLocation:u=g.ignoreLocation}={}){super(e),this._bitapSearch=new N(e,{location:t,threshold:n,distance:i,includeMatches:r,findAllMatches:c,minMatchCharLength:h,isCaseSensitive:o,ignoreDiacritics:l,ignoreLocation:u})}static get type(){return"fuzzy"}static get multiRegex(){return/^"(.*)"$/}static get singleRegex(){return/^(.*)$/}search(e){return this._bitapSearch.searchIn(e)}},W=class extends y{constructor(e){super(e)}static get type(){return"include"}static get multiRegex(){return/^'"(.*)"$/}static get singleRegex(){return/^'(.*)$/}search(e){let t=0,n,i=[],r=this.pattern.length;for(;(n=e.indexOf(this.pattern,t))>-1;)t=n+r,i.push([n,t-1]);let c=!!i.length;return{isMatch:c,score:c?0:1,indices:i}}},se=[J,W,Z,q,te,ee,X,P],me=se.length,Ye="\0",Ue="|";function Ve(s){let e=[],t=s.length,n=0;for(;n=t)break;let i=n;for(;i=t||s[r]===" "){i++;break}if(s[r]==="$"&&(r+1>=t||s[r+1]===" ")){i+=2;break}}i++}e.push(s.substring(n,i)),n=i}else{for(;i{let i=n.replace(/\u0000/g,"|"),r=Ve(i.trim()).filter(h=>h&&!!h.trim()),c=[];for(let h=0,o=r.length;h!!(s[H.AND]||s[H.OR]),Ze=s=>!!s[re.PATH],qe=s=>!w(s)&&_e(s)&&!ce(s),Ce=s=>({[H.AND]:Object.keys(s).map(e=>({[e]:s[e]}))});function Ie(s,e,{auto:t=!0}={}){let n=i=>{if(C(i)){let o={keyId:null,pattern:i};return t&&(o.searcher=K(i,e)),o}let r=Object.keys(i),c=Ze(i);if(!c&&r.length>1&&!ce(i))return n(Ce(i));if(qe(i)){let o=c?i[re.PATH]:r[0],l=c?i[re.PATTERN]:i[o];if(!C(l))throw new Error(ke(o));let u={keyId:V(o),pattern:l};return t&&(u.searcher=K(l,e)),u}let h={children:[],operator:r[0]};return r.forEach(o=>{let l=i[o];w(l)&&l.forEach(u=>{h.children.push(n(u))})}),h};return ce(s)||(s=Ce(s)),n(s)}function oe(s,{ignoreFieldNorm:e=g.ignoreFieldNorm}){let t=1;return s.forEach(({key:n,norm:i,score:r})=>{let c=n?n.weight:null;t*=Math.pow(r===0&&c?Number.EPSILON:r,(c||1)*(e?1:i))}),t}function et(s,{ignoreFieldNorm:e=g.ignoreFieldNorm}){s.forEach(t=>{t.score=oe(t.matches,{ignoreFieldNorm:e})})}var ue=class{constructor(e){this.limit=e,this.heap=[]}get size(){return this.heap.length}shouldInsert(e){return this.size0;){let n=e-1>>1;if(t[e].score<=t[n].score)break;let i=t[e];t[e]=t[n],t[n]=i,e=n}}_sinkDown(e){let t=this.heap,n=t.length,i=e;do{e=i;let r=2*e+1,c=2*e+2;if(rt[i].score&&(i=r),ct[i].score&&(i=c),i!==e){let h=t[e];t[e]=t[i],t[i]=h}}while(i!==e)}};function tt(s,e){let t=s.matches;e.matches=[],_(t)&&t.forEach(n=>{if(!_(n.indices)||!n.indices.length)return;let{indices:i,value:r}=n,c={indices:i,value:r};n.key&&(c.key=n.key.src),n.idx>-1&&(c.refIndex=n.idx),e.matches.push(c)})}function st(s,e){e.score=s.score}function nt(s,e,{includeMatches:t=g.includeMatches,includeScore:n=g.includeScore}={}){let i=[];return t&&i.push(tt),n&&i.push(st),s.map(r=>{let{idx:c}=r,h={item:e[c],refIndex:c};return i.length&&i.forEach(o=>{o(r,h)}),h})}var it=/\b\w+\b/g;function he({isCaseSensitive:s=!1,ignoreDiacritics:e=!1}={}){return{tokenize(t){return s||(t=t.toLowerCase()),e&&(t=R(t)),t.match(it)||[]}}}function rt(s,e,t){let n=new Map,i=new Map,r=0;function c(h,o,l,u){let f=t.tokenize(h);if(!f.length)return;r++;let d=new Map;for(let a of f)d.set(a,(d.get(a)||0)+1);for(let[a,p]of d){let A={docIdx:o,keyIdx:l,subIdx:u,tf:p},m=n.get(a);m||(m=[],n.set(a,m)),m.push(A),i.set(a,(i.get(a)||0)+1)}}for(let h of s){let{i:o,v:l,$:u}=h;if(l!==void 0){c(l,o,-1,-1);continue}if(u)for(let f=0;fc.docIdx!==e),r=n.length-i.length;r>0&&(s.fieldCount-=r,s.df.set(t,(s.df.get(t)||0)-r),i.length===0?(s.terms.delete(t),s.df.delete(t)):s.terms.set(t,i))}}var M=class{constructor(e,t,n){this.options={...g,...t},this.options.useExtendedSearch,this.options.useTokenSearch,this._keyStore=new U(this.options.keys),this._docs=e,this._myIndex=null,this._invertedIndex=null,this.setCollection(e,n),this._lastQuery=null,this._lastSearcher=null}_getSearcher(e){if(this._lastQuery===e)return this._lastSearcher;let t=this._invertedIndex?{...this.options,_invertedIndex:this._invertedIndex}:this.options,n=K(e,t);return this._lastQuery=e,this._lastSearcher=n,n}setCollection(e,t){if(this._docs=e,t&&!(t instanceof O))throw new Error(be);if(this._myIndex=t||Me(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight}),this.options.useTokenSearch){let n=he({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics});this._invertedIndex=rt(this._myIndex.records,this._myIndex.keys.length,n)}}add(e){if(_(e)&&(this._docs.push(e),this._myIndex.add(e),this._invertedIndex)){let t=this._myIndex.records[this._myIndex.records.length-1],n=he({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics});ct(this._invertedIndex,t,this._myIndex.keys.length,n)}}remove(e=()=>!1){let t=[],n=[];for(let i=0,r=this._docs.length;i=0;i-=1)this._docs.splice(n[i],1);this._myIndex.removeAll(n)}return t}removeAt(e){this._invertedIndex&&Ee(this._invertedIndex,e);let t=this._docs.splice(e,1)[0];return this._myIndex.removeAt(e),t}getIndex(){return this._myIndex}search(e,t){let{limit:n=-1}=t||{},{includeMatches:i,includeScore:r,shouldSort:c,sortFn:h,ignoreFieldNorm:o}=this.options;if(C(e)&&!e.trim()){let f=this._docs.map((d,a)=>({item:d,refIndex:a}));return z(n)&&n>-1&&(f=f.slice(0,n)),f}let l=z(n)&&n>0&&C(e),u;if(l){let f=new ue(n);C(this._docs[0])?this._searchStringList(e,{heap:f,ignoreFieldNorm:o}):this._searchObjectList(e,{heap:f,ignoreFieldNorm:o}),u=f.extractSorted(h)}else u=C(e)?C(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),et(u,{ignoreFieldNorm:o}),c&&u.sort(h),z(n)&&n>-1&&(u=u.slice(0,n));return nt(u,this._docs,{includeMatches:i,includeScore:r})}_searchStringList(e,{heap:t,ignoreFieldNorm:n}={}){let i=this._getSearcher(e),{records:r}=this._myIndex,c=t?null:[];return r.forEach(({v:h,i:o,n:l})=>{if(!_(h))return;let{isMatch:u,score:f,indices:d}=i.searchIn(h);if(u){let a={item:h,idx:o,matches:[{score:f,value:h,norm:l,indices:d}]};t?(a.score=oe(a.matches,{ignoreFieldNorm:n}),t.shouldInsert(a.score)&&t.insert(a)):c.push(a)}}),c}_searchLogical(e){let t=Ie(e,this.options),n=(h,o,l)=>{if(!("children"in h)){let{keyId:a,searcher:p}=h,A;return a===null?(A=[],this._myIndex.keys.forEach((m,B)=>{A.push(...this._findMatches({key:m,value:o[B],searcher:p}))})):A=this._findMatches({key:this._keyStore.get(a),value:this._myIndex.getValueForItemAtKeyId(o,a),searcher:p}),A&&A.length?[{idx:l,item:o,matches:A}]:[]}let{children:u,operator:f}=h,d=[];for(let a=0,p=u.length;a{if(_(h)){let l=n(t,h,o);l.length&&(r.has(o)||(r.set(o,{idx:o,item:h,matches:[]}),c.push(r.get(o))),l.forEach(({matches:u})=>{r.get(o).matches.push(...u)}))}}),c}_searchObjectList(e,{heap:t,ignoreFieldNorm:n}={}){let i=this._getSearcher(e),{keys:r,records:c}=this._myIndex,h=t?null:[];return c.forEach(({$:o,i:l})=>{if(!_(o))return;let u=[],f=!1,d=!1;if(r.forEach((a,p)=>{let A=this._findMatches({key:a,value:o[p],searcher:i});A.length?(u.push(...A),A[0].hasInverse&&(d=!0)):f=!0}),!(d&&f)&&u.length){let a={idx:l,item:o,matches:u};t?(a.score=oe(a.matches,{ignoreFieldNorm:n}),t.shouldInsert(a.score)&&t.insert(a)):h.push(a)}}),h}_findMatches({key:e,value:t,searcher:n}){if(!_(t))return[];let i=[];if(w(t))t.forEach(({v:r,i:c,n:h})=>{if(!_(r))return;let{isMatch:o,score:l,indices:u,hasInverse:f}=n.searchIn(r);o&&i.push({score:l,key:e,value:r,idx:c,norm:h,indices:u,hasInverse:f})});else{let{v:r,n:c}=t,{isMatch:h,score:o,indices:l,hasInverse:u}=n.searchIn(r);h&&i.push({score:o,key:e,value:r,norm:c,indices:l,hasInverse:u})}return i}},ae=class{static condition(e,t){return t.useTokenSearch}constructor(e,t){this.options=t,this.analyzer=he({isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics});let n=this.analyzer.tokenize(e),i=t._invertedIndex,{df:r,fieldCount:c}=i;this.termSearchers=[],this.idfWeights=[];for(let h of n){this.termSearchers.push(new N(h,{location:t.location,threshold:t.threshold,distance:t.distance,includeMatches:t.includeMatches,findAllMatches:t.findAllMatches,minMatchCharLength:t.minMatchCharLength,isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,ignoreLocation:!0}));let o=r.get(h)||0,l=Math.log(1+(c-o+.5)/(o+.5));this.idfWeights.push(l)}}searchIn(e){if(!this.termSearchers.length)return{isMatch:!1,score:1};let t=[],n=0,i=0,r=0;for(let o=0;o0?1-n/i:0,h={isMatch:!0,score:Math.max(.001,c)};return this.options.includeMatches&&t.length&&(h.indices=le(t)),h}};M.version="7.3.0";M.createIndex=Me;M.parseIndex=We;M.config=g;M.match=function(s,e,t){return K(s,{...g,...t}).searchIn(e)};M.parseQuery=Ie;fe(ne);fe(ae);M.use=function(...s){s.forEach(e=>fe(e))};window.Fuse=M;})(); diff --git a/templates/v3/includes/_field_combo_multi.html b/templates/v3/includes/_field_combo_multi.html index 726291989..a76a6f830 100644 --- a/templates/v3/includes/_field_combo_multi.html +++ b/templates/v3/includes/_field_combo_multi.html @@ -10,6 +10,7 @@ error (optional) — error message (activates error state) required (optional) — if truthy, adds required attribute extra_class (optional) — additional classes on the wrapper + deselectable (optional) — if truthy, shows a clear (X) button when any values are selected Usage: {% include "v3/includes/_field_combo_multi.html" with name="tags" label="Tags" options=tags_options placeholder="Search tags..." %} {% endcomment %} @@ -18,7 +19,8 @@ {% if selected_values %} {% endif %} -
{# djlint:on #} @@ -76,10 +82,10 @@ x-show="!jsReady" :disabled="jsReady"> {% for value, label in options %} - + {% endfor %} -