├── assets ├── css │ ├── autosuggest.css │ ├── comments.css │ ├── dashboard.css │ ├── facets-admin.css │ ├── facets.css │ ├── highlighting.css │ ├── instant-results.css │ ├── instant-results │ │ ├── checkbox.css │ │ ├── input.css │ │ ├── modal.css │ │ ├── options-list.css │ │ ├── page.css │ │ ├── pagination.css │ │ ├── panel.css │ │ ├── range-slider.css │ │ ├── result.css │ │ ├── results.css │ │ ├── sidebar-toggle.css │ │ ├── sidebar.css │ │ ├── sort.css │ │ ├── tokens.css │ │ ├── toolbar.css │ │ └── utilities.css │ ├── ordering.css │ ├── related-posts-block.css │ ├── sync.css │ ├── sync │ │ ├── button.css │ │ ├── controls.css │ │ ├── heading.css │ │ ├── messages.css │ │ ├── panel.css │ │ ├── progress-bar.css │ │ ├── progress.css │ │ ├── status.css │ │ └── warning.css │ └── synonyms.css └── js │ ├── autosuggest.js │ ├── blocks │ ├── facets │ │ ├── block.json │ │ ├── edit.js │ │ └── index.js │ └── related-posts │ │ ├── Edit.js │ │ └── block.js │ ├── comments.js │ ├── dashboard.js │ ├── facets.js │ ├── instant-results │ ├── admin │ │ ├── components │ │ │ └── facet-selector.js │ │ ├── config.js │ │ └── index.js │ ├── components │ │ ├── common │ │ │ ├── checkbox-list.js │ │ │ ├── checkbox.js │ │ │ ├── image.js │ │ │ ├── modal.js │ │ │ ├── panel.js │ │ │ ├── range-slider.js │ │ │ ├── small-button.js │ │ │ └── star-rating.js │ │ ├── facets │ │ │ ├── facet.js │ │ │ ├── post-type-facet.js │ │ │ ├── price-range-facet.js │ │ │ ├── search-term-facet.js │ │ │ └── taxonomy-terms-facet.js │ │ ├── layout.js │ │ ├── layout │ │ │ ├── results.js │ │ │ ├── sidebar.js │ │ │ └── toolbar.js │ │ ├── results │ │ │ ├── pagination.js │ │ │ └── result.js │ │ └── tools │ │ │ ├── active-constraints.js │ │ │ ├── clear-constraints.js │ │ │ ├── sidebar-toggle.js │ │ │ └── sort.js │ ├── config.js │ ├── context.js │ ├── functions.js │ ├── hooks.js │ ├── index.js │ ├── reducer.js │ └── utilities.js │ ├── notice.js │ ├── ordering │ ├── index.js │ └── pointers.js │ ├── settings.js │ ├── sites-admin.js │ ├── stats.js │ ├── sync │ ├── components │ │ ├── common │ │ │ ├── date-time.js │ │ │ ├── message-log.js │ │ │ └── progress-bar.js │ │ ├── icons │ │ │ ├── pause.js │ │ │ ├── play.js │ │ │ ├── stop.js │ │ │ ├── sync.js │ │ │ ├── thumbs-down.js │ │ │ └── thumbs-up.js │ │ ├── sync-page.js │ │ └── sync │ │ │ ├── controls.js │ │ │ ├── log.js │ │ │ ├── progress.js │ │ │ └── status.js │ ├── config.js │ ├── hooks.js │ ├── index.js │ └── utilities.js │ ├── synonyms │ ├── components │ │ ├── SynonymsEditor.js │ │ ├── editors │ │ │ ├── AlternativeEditor.js │ │ │ ├── AlternativesEditor.js │ │ │ ├── SetsEditor.js │ │ │ └── SolrEditor.js │ │ └── shared │ │ │ └── LinkedMultiInput.js │ ├── context.js │ ├── index.js │ ├── reducers │ │ └── editorReducer.js │ └── utils.js │ ├── utils │ └── helpers.js │ └── weighting.js ├── dist ├── css │ ├── autosuggest-styles.min.asset.php │ ├── autosuggest-styles.min.css │ ├── comments-styles.min.asset.php │ ├── comments-styles.min.css │ ├── dashboard-styles.min.asset.php │ ├── dashboard-styles.min.css │ ├── facets-admin-styles.min.asset.php │ ├── facets-admin-styles.min.css │ ├── facets-styles.min.asset.php │ ├── facets-styles.min.css │ ├── highlighting-styles.min.asset.php │ ├── highlighting-styles.min.css │ ├── instant-results-styles.min.asset.php │ ├── instant-results-styles.min.css │ ├── ordering-styles.min.asset.php │ ├── ordering-styles.min.css │ ├── related-posts-block-styles.min.asset.php │ ├── related-posts-block-styles.min.css │ ├── sync-styles.min.asset.php │ ├── sync-styles.min.css │ ├── synonyms-styles.min.asset.php │ └── synonyms-styles.min.css └── js │ ├── autosuggest-script.min.asset.php │ ├── autosuggest-script.min.js │ ├── comments-script.min.asset.php │ ├── comments-script.min.js │ ├── dashboard-script.min.asset.php │ ├── dashboard-script.min.js │ ├── facets-block-script.min.asset.php │ ├── facets-block-script.min.js │ ├── facets-script.min.asset.php │ ├── facets-script.min.js │ ├── instant-results-admin-script.min.asset.php │ ├── instant-results-admin-script.min.js │ ├── instant-results-script.min.asset.php │ ├── instant-results-script.min.js │ ├── notice-script.min.asset.php │ ├── notice-script.min.js │ ├── ordering-script.min.asset.php │ ├── ordering-script.min.js │ ├── related-posts-block-script.min.asset.php │ ├── related-posts-block-script.min.js │ ├── settings-script.min.asset.php │ ├── settings-script.min.js │ ├── sites-admin-script.min.asset.php │ ├── sites-admin-script.min.js │ ├── stats-script.min.asset.php │ ├── stats-script.min.js │ ├── sync-script.min.asset.php │ ├── sync-script.min.js │ ├── synonyms-script.min.asset.php │ ├── synonyms-script.min.js │ ├── weighting-script.min.asset.php │ └── weighting-script.min.js ├── elasticpress.php ├── images ├── features-screenshot.png ├── logo-elasticpress-io.svg ├── logo-icon.svg ├── logo.svg ├── setup-screenshot.png ├── sync-in-progress.png ├── vip-logo.svg └── warning.svg ├── includes ├── classes │ ├── AdminNotices.php │ ├── Command.php │ ├── Elasticsearch.php │ ├── Feature.php │ ├── Feature │ │ ├── Comments │ │ │ ├── Comments.php │ │ │ └── Widget.php │ │ ├── Facets │ │ │ ├── Block.php │ │ │ ├── Facets.php │ │ │ ├── Renderer.php │ │ │ └── Widget.php │ │ ├── ProtectedContent │ │ │ └── ProtectedContent.php │ │ ├── RelatedPosts │ │ │ ├── RelatedPosts.php │ │ │ └── Widget.php │ │ ├── Search │ │ │ ├── Search.php │ │ │ ├── Synonyms.php │ │ │ └── Weighting.php │ │ ├── SearchOrdering │ │ │ └── SearchOrdering.php │ │ ├── Terms │ │ │ └── Terms.php │ │ ├── Users │ │ │ └── Users.php │ │ └── WooCommerce │ │ │ └── WooCommerce.php │ ├── FeatureRequirementsStatus.php │ ├── Features.php │ ├── HealthCheck.php │ ├── HealthCheck │ │ └── HealthCheckElasticsearch.php │ ├── IndexHelper.php │ ├── Indexable.php │ ├── Indexable │ │ ├── Comment │ │ │ ├── Comment.php │ │ │ ├── QueryIntegration.php │ │ │ └── SyncManager.php │ │ ├── Post │ │ │ ├── DateQuery.php │ │ │ ├── Post.php │ │ │ ├── QueryIntegration.php │ │ │ └── SyncManager.php │ │ ├── Term │ │ │ ├── QueryIntegration.php │ │ │ ├── SyncManager.php │ │ │ └── Term.php │ │ └── User │ │ │ ├── QueryIntegration.php │ │ │ ├── SyncManager.php │ │ │ └── User.php │ ├── Indexables.php │ ├── Installer.php │ ├── Screen.php │ ├── Screen │ │ └── Sync.php │ ├── Stats.php │ ├── SyncManager.php │ └── Upgrades.php ├── compat.php ├── dashboard.php ├── health-check.php ├── mappings │ ├── comment │ │ ├── 7-0.php │ │ ├── initial.php │ │ └── pre-5-0.php │ ├── post │ │ ├── 5-0.php │ │ ├── 5-2.php │ │ ├── 7-0.php │ │ └── pre-5-0.php │ ├── term │ │ ├── 7-0.php │ │ ├── initial.php │ │ └── pre-5-0.php │ └── user │ │ ├── 7-0.php │ │ ├── initial.php │ │ └── pre-5-0.php ├── partials │ ├── dashboard-page.php │ ├── header.php │ ├── install-page.php │ ├── settings-page.php │ ├── stats-page.php │ └── sync-page.php └── utils.php ├── readme.txt └── uninstall.php /assets/css/autosuggest.css: -------------------------------------------------------------------------------- 1 | .ep-autosuggest-container { 2 | position: relative; 3 | 4 | & .ep-autosuggest { 5 | background: #fff; 6 | border: 1px solid #ccc; 7 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 8 | display: none; 9 | position: absolute; 10 | 11 | width: 100%; 12 | z-index: 200; 13 | 14 | & > ul { 15 | list-style: none; 16 | margin: 0 !important; 17 | 18 | & > li { 19 | font-family: sans-serif; 20 | 21 | & > a.autosuggest-link { 22 | color: #000; 23 | cursor: pointer; 24 | display: block; 25 | padding: 2px 10px; 26 | 27 | &:hover, 28 | &:active { 29 | background-color: #eee; 30 | text-decoration: none; 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | & .selected { 38 | background-color: #eee; 39 | text-decoration: none; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /assets/css/comments.css: -------------------------------------------------------------------------------- 1 | .ep-widget-search-comments-results { 2 | list-style-type: none; 3 | margin-left: 0; 4 | } 5 | 6 | .ep-widget-search-comments-result-item, 7 | .ep-widget-search-comments-result-item-not-found { 8 | margin-left: 0; 9 | } 10 | 11 | .ep-widget-search-comments-result-item.selected a { 12 | border: 2px dotted #171923; 13 | } 14 | -------------------------------------------------------------------------------- /assets/css/facets-admin.css: -------------------------------------------------------------------------------- 1 | .widget-ep-facet label { 2 | margin-right: 5px; 3 | } 4 | -------------------------------------------------------------------------------- /assets/css/facets.css: -------------------------------------------------------------------------------- 1 | .widget_ep-facet, 2 | .wp-block-elasticpress-facet { 3 | 4 | & input[type="search"] { 5 | margin-bottom: 1rem; 6 | } 7 | 8 | & .searchable .inner { 9 | max-height: 20em; 10 | overflow: scroll; 11 | } 12 | 13 | & .term.hide { 14 | display: none; 15 | } 16 | 17 | & .empty-term { 18 | opacity: 0.5; 19 | position: relative; 20 | } 21 | 22 | & .empty-term::after { 23 | bottom: 0; 24 | content: " "; 25 | display: block; 26 | left: 0; 27 | position: absolute; 28 | right: 0; 29 | top: 0; 30 | width: 100%; 31 | z-index: 2; 32 | } 33 | 34 | & .level-1 { 35 | padding-left: 20px; 36 | } 37 | 38 | & .level-2 { 39 | padding-left: 40px; 40 | } 41 | 42 | & .level-3 { 43 | padding-left: 60px; 44 | } 45 | 46 | & .level-4 { 47 | padding-left: 80px; 48 | } 49 | 50 | & .level-5 { 51 | padding-left: 100px; 52 | } 53 | 54 | & input[disabled] { 55 | cursor: pointer; 56 | opacity: 1; 57 | } 58 | 59 | & .term a { 60 | align-items: center; 61 | display: flex; 62 | position: relative; 63 | } 64 | 65 | & .term a:hover .ep-checkbox { 66 | background-color: #ccc; 67 | } 68 | } 69 | 70 | .ep-checkbox { 71 | align-items: center; 72 | background-color: #eee; 73 | display: flex; 74 | flex-shrink: 0; 75 | height: 1em; 76 | justify-content: center; 77 | margin-right: 0.25em; 78 | width: 1em; 79 | } 80 | 81 | .ep-checkbox::after { 82 | border: solid #fff; 83 | border-width: 0 0.125em 0.125em 0; 84 | content: ""; 85 | display: none; 86 | height: 0.5em; 87 | transform: rotate(45deg); 88 | width: 0.25em; 89 | } 90 | 91 | .ep-checkbox.checked { 92 | background-color: #5e5e5e; 93 | } 94 | 95 | .ep-checkbox.checked::after { 96 | display: block; 97 | } 98 | -------------------------------------------------------------------------------- /assets/css/highlighting.css: -------------------------------------------------------------------------------- 1 | .ep-highlight { 2 | background-color: transparent; 3 | font-style: italic; 4 | font-weight: 700; 5 | } 6 | -------------------------------------------------------------------------------- /assets/css/instant-results.css: -------------------------------------------------------------------------------- 1 | @import "instant-results/utilities.css"; 2 | @import "instant-results/checkbox.css"; 3 | @import "instant-results/input.css"; 4 | @import "instant-results/modal.css"; 5 | @import "instant-results/options-list.css"; 6 | @import "instant-results/page.css"; 7 | @import "instant-results/panel.css"; 8 | @import "instant-results/pagination.css"; 9 | @import "instant-results/range-slider.css"; 10 | @import "instant-results/result.css"; 11 | @import "instant-results/results.css"; 12 | @import "instant-results/sidebar.css"; 13 | @import "instant-results/sidebar-toggle.css"; 14 | @import "instant-results/sort.css"; 15 | @import "instant-results/tokens.css"; 16 | @import "instant-results/toolbar.css"; 17 | 18 | :root { 19 | --ep-search-background-color: #fff; 20 | --ep-search-alternate-background-color: #efefef; 21 | --ep-search-border-color: #dfdfdf; 22 | --ep-search-range-thumb-size: 1.625em; 23 | --ep-search-range-track-size: 0.75em; 24 | 25 | @media ( min-width: 768px ) { 26 | --ep-search-range-thumb-size: 1.25em; 27 | --ep-search-range-track-size: 0.5em; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /assets/css/instant-results/checkbox.css: -------------------------------------------------------------------------------- 1 | .ep-search-checkbox__count::before { 2 | content: "("; 3 | } 4 | 5 | .ep-search-checkbox__count::after { 6 | content: ")"; 7 | } 8 | -------------------------------------------------------------------------------- /assets/css/instant-results/input.css: -------------------------------------------------------------------------------- 1 | .ep-search-input { 2 | font-size: 1.25em; 3 | margin: 0 !important; 4 | width: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /assets/css/instant-results/modal.css: -------------------------------------------------------------------------------- 1 | .has-ep-search-modal { 2 | overflow: hidden; 3 | } 4 | 5 | .ep-search-modal { 6 | --ep-search-modal-focus-within: 0; 7 | background-color: rgba(43, 46, 56, 0.9); 8 | bottom: 0; 9 | display: flex; 10 | left: 0; 11 | position: fixed; 12 | right: 0; 13 | top: 0; 14 | z-index: 9999; 15 | 16 | @nest .rtl & { 17 | direction: rtl; 18 | text-align: right; 19 | } 20 | 21 | @nest .admin-bar & { 22 | top: 32px; 23 | 24 | @media ( max-width: 782px ) { 25 | top: 46px; 26 | } 27 | } 28 | 29 | &[aria-hidden="true"] { 30 | display: none; 31 | } 32 | 33 | &:focus-within { 34 | --ep-search-modal-focus-within: 1; 35 | } 36 | } 37 | 38 | .ep-search-modal__content { 39 | background-color: var(--ep-search-background-color); 40 | bottom: 0; 41 | display: flex; 42 | flex-direction: column; 43 | left: 0; 44 | position: absolute; 45 | right: 0; 46 | top: 0; 47 | 48 | @media ( min-width: 768px ) { 49 | bottom: 1em; 50 | margin: 0 auto; 51 | max-width: calc(100% - 2em); 52 | top: 1em; 53 | width: 80em; 54 | } 55 | } 56 | 57 | .ep-search-modal__close { 58 | align-self: flex-end; 59 | padding: 1em !important; 60 | } 61 | -------------------------------------------------------------------------------- /assets/css/instant-results/options-list.css: -------------------------------------------------------------------------------- 1 | .ep-search-options-list { 2 | list-style: none; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .ep-search-options-list__item { 8 | margin: 0.5em 0; 9 | 10 | &::before { 11 | content: none; 12 | } 13 | } 14 | 15 | .ep-search-options-list__sub-menu { 16 | padding-left: 1em; 17 | } 18 | -------------------------------------------------------------------------------- /assets/css/instant-results/page.css: -------------------------------------------------------------------------------- 1 | .ep-search-page { 2 | display: flex; 3 | flex-direction: column; 4 | flex-grow: 2; 5 | margin: 0; 6 | overflow-y: auto; 7 | transition: opacity 300ms ease-out; 8 | width: 100%; 9 | 10 | @media ( min-width: 768px ) { 11 | overflow: hidden; 12 | } 13 | 14 | & *, 15 | & *::before, 16 | & *::after { 17 | box-sizing: border-box; 18 | } 19 | 20 | &.is-loading { 21 | opacity: 0.5; 22 | } 23 | } 24 | 25 | .ep-search-page__header, 26 | .ep-search-page__tools, 27 | .ep-search-page__body { 28 | padding: 0 1em; 29 | } 30 | 31 | .ep-search-page__body { 32 | 33 | @media ( min-width: 768px ) { 34 | align-items: flex-start; 35 | display: flex; 36 | flex-grow: 2; 37 | overflow: hidden; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /assets/css/instant-results/pagination.css: -------------------------------------------------------------------------------- 1 | .ep-search-pagination { 2 | align-items: center; 3 | display: grid; 4 | grid-template-columns: 1fr 1fr 1fr; 5 | margin-top: auto; 6 | text-align: center; 7 | 8 | @nest .rtl & { 9 | direction: rtl; 10 | } 11 | } 12 | 13 | .ep-search-pagination__next { 14 | justify-self: end; 15 | } 16 | 17 | .ep-search-pagination__previous { 18 | justify-self: start; 19 | } 20 | -------------------------------------------------------------------------------- /assets/css/instant-results/panel.css: -------------------------------------------------------------------------------- 1 | .ep-search-panel { 2 | border: 1px solid var(--ep-search-border-color); 3 | margin: 0; 4 | padding: 0; 5 | 6 | @nest .ep-search-panel + & { 7 | border-top-width: 0; 8 | } 9 | } 10 | 11 | .ep-search-panel__heading { 12 | font-size: inherit; 13 | margin: 0; 14 | } 15 | 16 | .ep-search-panel__button { 17 | padding: 1em !important; 18 | width: 100% !important; 19 | } 20 | 21 | .ep-search-panel__content { 22 | padding: 0 1em 1em 1em; 23 | 24 | &[aria-hidden="true"] { 25 | display: none; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /assets/css/instant-results/range-slider.css: -------------------------------------------------------------------------------- 1 | .ep-search-range-slider { 2 | align-items: center; 3 | display: flex; 4 | margin: 0.5em 0; 5 | min-height: var(--ep-search-range-thumb-size); 6 | } 7 | 8 | .ep-search-range-slider__track { 9 | background: var(--ep-search-alternate-background-color); 10 | border-radius: calc(var(--ep-search-range-track-size) / 2); 11 | height: var(--ep-search-range-track-size); 12 | } 13 | 14 | .ep-search-range-slider__track-1 { 15 | background-color: currentColor; 16 | } 17 | 18 | .ep-search-range-slider__thumb { 19 | background-color: currentColor; 20 | border-radius: calc(var(--ep-search-range-thumb-size) / 2); 21 | box-shadow: 22 | inset 0 0 0 calc(var(--ep-search-range-thumb-size) / 10) currentColor, 23 | inset 0 0 0 calc((var(--ep-search-range-thumb-size) - var(--ep-search-range-track-size)) / 2) var(--ep-search-background-color); 24 | height: var(--ep-search-range-thumb-size); 25 | width: var(--ep-search-range-thumb-size); 26 | } 27 | -------------------------------------------------------------------------------- /assets/css/instant-results/result.css: -------------------------------------------------------------------------------- 1 | .ep-search-result { 2 | align-items: flex-start; 3 | display: grid; 4 | grid-gap: 0.5em; 5 | grid-template-areas: 6 | "header" 7 | "footer"; 8 | grid-template-rows: auto 1fr; 9 | 10 | @media ( min-width: 768px ) { 11 | grid-gap: 1em; 12 | grid-template-areas: 13 | "header" 14 | "description" 15 | "footer"; 16 | grid-template-rows: auto auto 1fr; 17 | } 18 | } 19 | 20 | .ep-search-result--has-thumbnail { 21 | grid-template-areas: 22 | "thumbnail header" 23 | "thumbnail footer"; 24 | grid-template-columns: min(300px, 34%) auto; 25 | 26 | @media ( min-width: 768px ) { 27 | grid-template-areas: 28 | "thumbnail header" 29 | "thumbnail description" 30 | "thumbnail footer"; 31 | } 32 | } 33 | 34 | .ep-search-result__thumbnail { 35 | display: block; 36 | grid-area: thumbnail; 37 | 38 | & img { 39 | display: block; 40 | margin: 0; 41 | width: 100%; 42 | } 43 | } 44 | 45 | .ep-search-result__header { 46 | display: grid; 47 | grid-area: header; 48 | grid-gap: 0.5em; 49 | grid-template-columns: auto; 50 | justify-items: start; 51 | } 52 | 53 | .ep-search-result__title { 54 | font-size: 1em; 55 | margin: 0; 56 | 57 | @media ( min-width: 768px ) { 58 | font-size: 1.25em; 59 | } 60 | } 61 | 62 | .ep-search-result__type { 63 | background-color: var(--ep-search-alternate-background-color); 64 | border-radius: 0.25em; 65 | display: inline-block; 66 | font-size: 0.875em; 67 | line-height: 1.5; 68 | padding: 0 0.25em; 69 | vertical-align: text-bottom; 70 | } 71 | 72 | .ep-search-result__description { 73 | display: none; 74 | font-size: 0.875em; 75 | grid-area: description; 76 | margin: 0; 77 | 78 | @media ( min-width: 768px ) { 79 | display: block; 80 | font-size: 1em; 81 | } 82 | } 83 | 84 | .ep-search-result__footer { 85 | display: grid; 86 | grid-area: footer; 87 | grid-gap: 0.5em; 88 | justify-items: start; 89 | } 90 | -------------------------------------------------------------------------------- /assets/css/instant-results/results.css: -------------------------------------------------------------------------------- 1 | .ep-search-results { 2 | display: grid; 3 | grid-gap: 2em; 4 | grid-template-columns: 100%; 5 | grid-template-rows: max-content; 6 | padding: 0 0 1em 0; 7 | width: 100%; 8 | 9 | @media ( min-width: 768px ) { 10 | height: 100%; 11 | overflow-y: auto; 12 | padding: 0 1em 1em 1em; 13 | } 14 | } 15 | 16 | .ep-search-results__header { 17 | align-items: center; 18 | display: flex; 19 | gap: 1em; 20 | justify-content: space-between; 21 | } 22 | 23 | .ep-search-results__title { 24 | font-size: 1.25em; 25 | margin: 0 !important; 26 | 27 | @media ( min-width: 768px ) { 28 | font-size: 1.5em; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /assets/css/instant-results/sidebar-toggle.css: -------------------------------------------------------------------------------- 1 | .ep-search-sidebar-toggle { 2 | width: 100%; 3 | 4 | @media ( min-width: 768px ) { 5 | display: none; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /assets/css/instant-results/sidebar.css: -------------------------------------------------------------------------------- 1 | .ep-search-sidebar { 2 | display: none; 3 | margin-bottom: 2em; 4 | 5 | &.is-open { 6 | display: block; 7 | } 8 | 9 | @media ( min-width: 768px ) { 10 | display: block; 11 | max-height: calc(100% - 1em); 12 | min-width: 25%; 13 | overflow-y: auto; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /assets/css/instant-results/sort.css: -------------------------------------------------------------------------------- 1 | .ep-search-sort { 2 | flex-shrink: 0; 3 | gap: 0.5em; 4 | margin: 0; 5 | 6 | @nest .ep-search-results & { 7 | display: none; 8 | 9 | @media ( min-width: 768px ) { 10 | align-items: center; 11 | display: flex; 12 | } 13 | } 14 | 15 | @nest .ep-search-sidebar & { 16 | display: flex; 17 | flex-direction: column; 18 | margin-bottom: 1em; 19 | 20 | @media ( min-width: 768px ) { 21 | display: none; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /assets/css/instant-results/tokens.css: -------------------------------------------------------------------------------- 1 | .ep-search-tokens { 2 | 3 | @nest .ep-search-toolbar & { 4 | display: contents; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /assets/css/instant-results/toolbar.css: -------------------------------------------------------------------------------- 1 | .ep-search-toolbar { 2 | align-items: start; 3 | display: flex; 4 | flex-wrap: wrap; 5 | gap: 0.25em; 6 | margin: 1em 0; 7 | 8 | @media ( min-width: 768px ) { 9 | align-items: center; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/css/instant-results/utilities.css: -------------------------------------------------------------------------------- 1 | .ep-search-reset-button { 2 | font: inherit !important; 3 | height: auto !important; 4 | letter-spacing: inherit !important; 5 | line-height: 1 !important; 6 | margin: 0 !important; 7 | padding: 0 !important; 8 | text-align: inherit !important; 9 | text-transform: inherit !important; 10 | width: auto !important; 11 | 12 | &, 13 | &:focus, 14 | &:hover { 15 | background: transparent !important; 16 | border: none !important; 17 | box-shadow: none !important; 18 | color: inherit !important; 19 | cursor: default !important; 20 | } 21 | 22 | &:focus { 23 | outline: medium auto Highlight !important; 24 | outline: medium auto -webkit-focus-ring-color !important; 25 | outline-offset: 0 !important; 26 | } 27 | } 28 | 29 | .ep-search-small-button { 30 | font-size: 0.875em !important; 31 | height: auto !important; 32 | line-height: 1 !important; 33 | padding: 0.5em !important; 34 | } 35 | 36 | .ep-search-icon-button { 37 | align-items: center; 38 | display: flex; 39 | justify-content: space-between; 40 | 41 | & svg { 42 | fill: currentColor; 43 | flex-shrink: 0; 44 | height: 1em; 45 | width: 1em; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /assets/css/ordering.css: -------------------------------------------------------------------------------- 1 | #ep-ordering { 2 | border-left-width: 0; 3 | border-right-width: 0; 4 | 5 | & .inside { 6 | background-color: #f1f1f1; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | & .pointers, 12 | & .pointer-search, 13 | & .loading { 14 | background-color: #fff; 15 | border-left: 1px solid #eee; 16 | border-right: 1px solid #eee; 17 | } 18 | 19 | & .new-post { 20 | background: #fff; 21 | padding: 0 1em; 22 | } 23 | 24 | & .loading { 25 | padding: 1em; 26 | 27 | & .spinner { 28 | float: left; 29 | margin-left: 0; 30 | margin-top: 0; 31 | } 32 | } 33 | 34 | & .pointer-type { 35 | border: 2px solid #0073aa; 36 | border-radius: 2px; 37 | color: #0073aa; 38 | display: inline-block; 39 | font-size: 0.75em; 40 | font-weight: 700; 41 | margin-right: 8px; 42 | padding: 1px 2px; 43 | } 44 | 45 | & .pointers { 46 | 47 | & .pointer, 48 | & .post { 49 | padding: 1em; 50 | 51 | &:nth-child(odd) { 52 | background-color: #f9f9f9; 53 | } 54 | } 55 | 56 | & .title { 57 | color: #0073aa; 58 | } 59 | 60 | & .pointer-actions { 61 | float: right; 62 | 63 | & .handle { 64 | cursor: move; 65 | } 66 | 67 | & .delete-pointer { 68 | margin-left: 10px; 69 | } 70 | } 71 | 72 | & .next-page-notice { 73 | background-color: #fdeeca; 74 | padding: 1em 0; 75 | text-align: center; 76 | } 77 | } 78 | 79 | & .legend { 80 | background: #fff; 81 | border-bottom: 1px solid #eee; 82 | padding: 1em 0; 83 | text-align: center; 84 | } 85 | 86 | & .legend-item { 87 | display: inline-block; 88 | font-size: 0.875em; 89 | font-style: italic; 90 | margin: 0 0.5em; 91 | } 92 | 93 | & .pointer-search { 94 | margin-top: 2em; 95 | 96 | & .no-results { 97 | padding: 1em; 98 | } 99 | 100 | & .section-title { 101 | border-bottom: 1px solid #eee; 102 | border-top: 1px solid #eee; 103 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); 104 | font-weight: 700; 105 | } 106 | 107 | & .search-wrapper { 108 | padding: 1em 0; 109 | } 110 | 111 | & .input-wrap { 112 | padding: 0 1em; 113 | } 114 | 115 | & .search-pointers { 116 | font-size: 18px; 117 | height: 1.7em; 118 | line-height: 100%; 119 | padding: 3px 8px; 120 | } 121 | 122 | & .pointer-results { 123 | padding: 1em 0 0; 124 | } 125 | 126 | & .pointer-result { 127 | padding: 10px 2em; 128 | 129 | & .dashicons { 130 | float: right; 131 | } 132 | 133 | &:hover { 134 | background-color: #f9f9f9; 135 | } 136 | } 137 | } 138 | 139 | & .delete-pointer, 140 | & .add-pointer { 141 | cursor: pointer; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /assets/css/related-posts-block.css: -------------------------------------------------------------------------------- 1 | .editor-styles-wrapper .wp-block-elasticpress-related-posts ul, 2 | .wp-block-elasticpress-related-posts ul { 3 | list-style-type: none; 4 | padding: 0; 5 | } 6 | 7 | .editor-styles-wrapper .wp-block-elasticpress-related-posts ul li a > div { 8 | display: inline; 9 | } 10 | -------------------------------------------------------------------------------- /assets/css/sync.css: -------------------------------------------------------------------------------- 1 | @import "sync/button.css"; 2 | @import "sync/controls.css"; 3 | @import "sync/heading.css"; 4 | @import "sync/messages.css"; 5 | @import "sync/panel.css"; 6 | @import "sync/progress.css"; 7 | @import "sync/progress-bar.css"; 8 | @import "sync/status.css"; 9 | @import "sync/warning.css"; 10 | 11 | :root { 12 | --ep-sync-color-black: #1a1e24; 13 | --ep-sync-color-error: #b52727; 14 | --ep-sync-color-light-grey: #f0f0f0; 15 | --ep-sync-color-success: #46b450; 16 | --ep-sync-color-warning: #ffb359; 17 | --ep-sync-color-white: #fff; 18 | } 19 | -------------------------------------------------------------------------------- /assets/css/sync/button.css: -------------------------------------------------------------------------------- 1 | .ep-sync-button { 2 | 3 | &.components-button.has-icon.has-text { 4 | height: 4rem; 5 | justify-content: center; 6 | width: 100%; 7 | 8 | & svg { 9 | height: 2em; 10 | margin: 0; 11 | width: 2em; 12 | } 13 | } 14 | } 15 | 16 | .ep-sync-button--sync { 17 | font-size: 1.5em; 18 | font-weight: 700; 19 | } 20 | 21 | .ep-sync-button--pause, 22 | .ep-sync-button--resume, 23 | .ep-sync-button--stop { 24 | flex-direction: column; 25 | } 26 | -------------------------------------------------------------------------------- /assets/css/sync/controls.css: -------------------------------------------------------------------------------- 1 | .ep-sync-controls { 2 | display: grid; 3 | grid-gap: 1em; 4 | grid-template-columns: 1fr 1fr; 5 | margin: 0 auto; 6 | max-width: 16rem; 7 | } 8 | 9 | .ep-sync-controls__sync { 10 | grid-column: 1 / -1; 11 | } 12 | 13 | .ep-sync-controls__learn-more { 14 | grid-column: 1 / -1; 15 | text-align: center; 16 | } 17 | -------------------------------------------------------------------------------- /assets/css/sync/heading.css: -------------------------------------------------------------------------------- 1 | .ep-sync-heading { 2 | 3 | @nest .wrap & { 4 | font-weight: 400; 5 | margin: 0.5rem 0 0.75rem; 6 | padding: 0; 7 | 8 | &h2 { 9 | color: inherit; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /assets/css/sync/messages.css: -------------------------------------------------------------------------------- 1 | .ep-sync-messages { 2 | align-content: start; 3 | background: var(--ep-sync-color-black); 4 | color: var(--ep-sync-color-white); 5 | display: grid; 6 | font-family: monospace; 7 | grid-auto-flow: column; 8 | grid-template-columns: min-content auto; 9 | height: 21em; 10 | line-height: 2; 11 | overflow-y: auto; 12 | white-space: pre-wrap; 13 | } 14 | 15 | .ep-sync-messages__message { 16 | grid-column: 2; 17 | } 18 | 19 | .ep-sync-messages__line-number { 20 | box-sizing: content-box; 21 | min-width: 3ch; 22 | opacity: 0.5; 23 | padding: 0 0.5em; 24 | text-align: right; 25 | } 26 | -------------------------------------------------------------------------------- /assets/css/sync/panel.css: -------------------------------------------------------------------------------- 1 | .ep-sync-panel { 2 | margin-bottom: 2rem; 3 | max-width: 1200px; 4 | } 5 | 6 | .ep-sync-panel__body { 7 | display: grid; 8 | grid-column-gap: 2rem; 9 | grid-row-gap: 1rem; 10 | grid-template-columns: auto 16rem; 11 | 12 | &.is-opened { 13 | padding: 2rem 2rem 1rem 2rem; 14 | } 15 | 16 | & p, 17 | & .components-toggle-control, 18 | & .components-tab-panel__tab-content { 19 | margin-bottom: 1rem; 20 | margin-top: 0; 21 | } 22 | 23 | @media (max-width: 960px) { 24 | grid-template-columns: 100%; 25 | } 26 | } 27 | 28 | .ep-sync-panel__row { 29 | grid-column: 1 / -1; 30 | } 31 | 32 | .ep-sync-panel__introduction { 33 | font-size: 18px; 34 | } 35 | -------------------------------------------------------------------------------- /assets/css/sync/progress-bar.css: -------------------------------------------------------------------------------- 1 | .ep-sync-progress-bar { 2 | background: var(--ep-sync-color-light-grey); 3 | display: flex; 4 | overflow: hidden; 5 | text-align: center; 6 | } 7 | 8 | .ep-sync-progress-bar, 9 | .ep-sync-progress-bar__progress { 10 | border-radius: 0.875em; 11 | } 12 | 13 | .ep-sync-progress-bar__progress { 14 | background: var(--wp-admin-theme-color); 15 | color: var(--ep-sync-color-white); 16 | padding: 0 0.875em; 17 | transition: all 500ms ease-in-out; 18 | white-space: nowrap; 19 | 20 | @nest .ep-sync-progress-bar--complete & { 21 | background: var(--ep-sync-color-success); 22 | } 23 | 24 | @nest .ep-sync-progress-bar--paused & { 25 | opacity: 0.5; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /assets/css/sync/progress.css: -------------------------------------------------------------------------------- 1 | @keyframes epSyncRotation { 2 | 3 | from { 4 | transform: rotate(0deg); 5 | } 6 | 7 | to { 8 | transform: rotate(359deg); 9 | } 10 | } 11 | 12 | .ep-sync-progress { 13 | align-items: center; 14 | display: grid; 15 | grid-row-gap: 1rem; 16 | grid-template-columns: min-content minmax(max-content, 1fr) 3fr; 17 | margin-bottom: 1rem; 18 | 19 | @media (max-width: 960px) { 20 | grid-template-columns: min-content auto; 21 | } 22 | 23 | & svg { 24 | animation: epSyncRotation 1500ms infinite linear; 25 | animation-play-state: paused; 26 | height: 36px; 27 | margin-right: 12px; 28 | width: 36px; 29 | } 30 | } 31 | 32 | .ep-sync-progress--syncing { 33 | 34 | & svg { 35 | animation-play-state: running; 36 | } 37 | } 38 | 39 | .ep-sync-progress__details { 40 | 41 | & strong { 42 | display: block; 43 | font-size: 14px; 44 | } 45 | } 46 | 47 | .ep-sync-progress__progress-bar { 48 | 49 | @media (max-width: 960px) { 50 | grid-column: 1 / -1; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /assets/css/sync/status.css: -------------------------------------------------------------------------------- 1 | .ep-sync-status { 2 | align-items: center; 3 | display: grid; 4 | grid-gap: 0.5rem; 5 | grid-template-columns: min-content auto; 6 | 7 | & svg { 8 | fill: var(--ep-sync-color-error); 9 | } 10 | } 11 | 12 | .ep-sync-status--success { 13 | 14 | & svg { 15 | fill: var(--ep-sync-color-success); 16 | } 17 | } 18 | 19 | .ep-sync-status__time { 20 | background-color: var(--ep-sync-color-light-grey); 21 | border-radius: 2px; 22 | padding: 0.25em 0.5em; 23 | } 24 | -------------------------------------------------------------------------------- /assets/css/sync/warning.css: -------------------------------------------------------------------------------- 1 | .ep-sync-warning { 2 | display: grid; 3 | grid-gap: 0.5rem; 4 | grid-template-columns: min-content auto; 5 | 6 | & svg { 7 | fill: var(--ep-sync-color-warning); 8 | margin-top: -3px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/css/synonyms.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ep-synonyms-input-border-color: hsl(0, 0%, 80%); 3 | --ep-synonyms-color-black: #1a1e24; 4 | --ep-synonyms-color-error: #b52727; 5 | } 6 | 7 | html.wp-toolbar { 8 | background: transparent; 9 | } 10 | 11 | #synonym-root { 12 | 13 | & .page-title-action { 14 | margin-left: 10px; 15 | } 16 | 17 | & .postbox .hndle { 18 | cursor: default; 19 | } 20 | 21 | & h2 { 22 | color: var(--ep-synonyms-color-black); 23 | } 24 | } 25 | 26 | .synonym-editor { 27 | 28 | & .postbox { 29 | width: 100%; 30 | 31 | & > .hndle { 32 | display: flex; 33 | } 34 | } 35 | } 36 | 37 | .synonym-alternative-editor, 38 | .synonym-set-editor { 39 | align-items: flex-start; 40 | display: flex; 41 | 42 | & .components-form-token-field { 43 | flex: 1; 44 | margin-bottom: 0.5em; 45 | } 46 | 47 | & .components-form-token-field__label { 48 | display: none; 49 | } 50 | 51 | & .components-form-token-field__input-container { 52 | border-color: var(--ep-synonyms-input-border-color); 53 | box-sizing: border-box; 54 | } 55 | 56 | & .components-form-token-field__token-text { 57 | padding-bottom: 1px; 58 | padding-top: 1px; 59 | 60 | @media screen and (max-width: 782px) { 61 | padding-bottom: 3px; 62 | padding-top: 3px; 63 | } 64 | } 65 | 66 | & input[type="text"].components-form-token-field__input { 67 | margin-bottom: 2px; 68 | margin-top: 2px; 69 | 70 | @media screen and (max-width: 782px) { 71 | min-height: 30px; 72 | } 73 | } 74 | 75 | & .components-form-token-field__help { 76 | margin-top: 0; 77 | } 78 | } 79 | 80 | input[type="text"].ep-synonyms__input { 81 | border: 1px solid var(--ep-synonyms-input-border-color); 82 | margin-bottom: 0.5em; 83 | margin-right: 1em; 84 | min-height: 36px; 85 | width: 10em; 86 | 87 | @media screen and (max-width: 782px) { 88 | min-height: 40px; 89 | } 90 | } 91 | 92 | .synonym-alternatives__primary-heading { 93 | width: 11em; 94 | } 95 | 96 | .synonym-alternatives__input-heading { 97 | flex: 1; 98 | } 99 | 100 | button.synonym__remove { 101 | background-color: transparent; 102 | border: none; 103 | color: var(--ep-synonyms-color-error); 104 | cursor: pointer; 105 | margin: 0 0 0 10px; 106 | min-height: 36px; 107 | padding: 0; 108 | 109 | @media screen and (max-width: 782px) { 110 | min-height: 40px; 111 | } 112 | 113 | & .dashicons-dismiss { 114 | margin: -2px 2px 0 0; 115 | } 116 | } 117 | 118 | .synonym__validation::before { 119 | content: ""; 120 | flex-basis: 100%; 121 | height: 0; 122 | } 123 | 124 | .synonym__validation, 125 | .synonym-solr-editor__validation p { 126 | color: var(--ep-synonyms-color-error); 127 | font-style: italic; 128 | } 129 | 130 | .synonym__validation { 131 | margin: 0 0 0.625em 0.5em; 132 | } 133 | 134 | .synonym-btn-group button.button { 135 | margin-right: 0.625em; 136 | } 137 | -------------------------------------------------------------------------------- /assets/js/blocks/facets/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 2, 4 | "title": "Facet (ElasticPress)", 5 | "textdomain": "elasticpress", 6 | "name": "elasticpress/facet", 7 | "icon": "feedback", 8 | "category": "widgets", 9 | "attributes": { 10 | "facet": { 11 | "type": "string", 12 | "default": "" 13 | }, 14 | "orderby": { 15 | "type" : "string", 16 | "default": "count", 17 | "enum" : [ "count", "name" ] 18 | }, 19 | "order": { 20 | "type": "string", 21 | "default": "desc", 22 | "enum": [ "desc", "asc" ] 23 | } 24 | }, 25 | "supports": { 26 | "html": false 27 | }, 28 | "editorScript": "file:/../../../../dist/js/facets-block-script.min.js", 29 | "style": "file:/../../../../dist/css/facets-styles.min.css" 30 | } -------------------------------------------------------------------------------- /assets/js/blocks/facets/edit.js: -------------------------------------------------------------------------------- 1 | import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; 2 | import { 3 | PanelBody, 4 | RadioControl, 5 | SelectControl, 6 | Spinner, 7 | Placeholder, 8 | } from '@wordpress/components'; 9 | import { Fragment, useEffect, useState, useCallback } from '@wordpress/element'; 10 | import apiFetch from '@wordpress/api-fetch'; 11 | import { __ } from '@wordpress/i18n'; 12 | 13 | const FacetBlockEdit = (props) => { 14 | const { attributes, setAttributes } = props; 15 | const [taxonomies, setTaxonomies] = useState({}); 16 | const [preview, setPreview] = useState(''); 17 | const [loading, setLoading] = useState(false); 18 | const { facet, orderby, order } = attributes; 19 | 20 | const blockProps = useBlockProps(); 21 | 22 | const load = useCallback(async () => { 23 | const taxonomies = await apiFetch({ 24 | path: '/elasticpress/v1/facets/taxonomies', 25 | }); 26 | setTaxonomies(taxonomies); 27 | }, [setTaxonomies]); 28 | 29 | useEffect(load, [load]); 30 | 31 | useEffect(() => { 32 | setLoading(true); 33 | const params = new URLSearchParams({ 34 | facet, 35 | orderby, 36 | order, 37 | }); 38 | apiFetch({ 39 | path: `/elasticpress/v1/facets/block-preview?${params}`, 40 | }) 41 | .then((preview) => setPreview(preview)) 42 | .finally(() => setLoading(false)); 43 | }, [facet, orderby, order]); 44 | 45 | return ( 46 | 47 | 48 | 49 | ({ 54 | label: taxonomy.label, 55 | value: slug, 56 | })), 57 | ]} 58 | onChange={(value) => setAttributes({ facet: value })} 59 | /> 60 | setAttributes({ orderby: value })} 69 | /> 70 | setAttributes({ order: value })} 78 | /> 79 | 80 | 81 | 82 |
83 | {loading && ( 84 | 85 | 86 | 87 | )} 88 | {/* eslint-disable-next-line react/no-danger */} 89 | {!loading &&
} 90 |
91 | 92 | ); 93 | }; 94 | export default FacetBlockEdit; 95 | -------------------------------------------------------------------------------- /assets/js/blocks/facets/index.js: -------------------------------------------------------------------------------- 1 | import edit from './edit'; 2 | import block from './block.json'; 3 | 4 | const { registerBlockType } = wp.blocks; 5 | 6 | registerBlockType(block, { 7 | edit, 8 | save: () => {}, 9 | }); 10 | -------------------------------------------------------------------------------- /assets/js/blocks/related-posts/Edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { AlignmentToolbar, BlockControls, InspectorControls } from '@wordpress/block-editor'; 5 | import { PanelBody, Placeholder, Spinner, QueryControls } from '@wordpress/components'; 6 | import { Fragment, Component, RawHTML } from '@wordpress/element'; 7 | import { __ } from '@wordpress/i18n'; 8 | import { addQueryArgs } from '@wordpress/url'; 9 | 10 | /** 11 | * Edit component 12 | */ 13 | class Edit extends Component { 14 | /** 15 | * Setup class 16 | * 17 | * @param {object} props Component properties 18 | */ 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = { 23 | posts: false, 24 | }; 25 | } 26 | 27 | /** 28 | * Load preview data 29 | */ 30 | componentDidMount() { 31 | const urlArgs = { 32 | number: 100, 33 | }; 34 | 35 | // Use 0 if in the Widgets Screen 36 | const { context: { postId = 0 } = {} } = this.props; 37 | 38 | wp.apiFetch({ 39 | path: addQueryArgs(`/wp/v2/posts/${postId}/related`, urlArgs), 40 | }) 41 | .then((posts) => { 42 | this.setState({ posts }); 43 | }) 44 | .catch(() => { 45 | this.setState({ posts: false }); 46 | }); 47 | } 48 | 49 | render() { 50 | const { 51 | attributes: { alignment, number }, 52 | setAttributes, 53 | className, 54 | } = this.props; 55 | const { posts } = this.state; 56 | 57 | const displayPosts = posts.length > number ? posts.slice(0, number) : posts; 58 | 59 | return ( 60 | 61 | 62 | setAttributes({ alignment: newValue })} 65 | /> 66 | 67 | 68 | 69 | setAttributes({ number: value })} 72 | /> 73 | 74 | 75 | 76 |
77 | {displayPosts === false || displayPosts.length === 0 ? ( 78 | 79 | {posts === false ? ( 80 | 81 | ) : ( 82 | __('No related posts yet.', 'elasticpress') 83 | )} 84 | 85 | ) : ( 86 | 102 | )} 103 |
104 |
105 | ); 106 | } 107 | } 108 | 109 | export default Edit; 110 | -------------------------------------------------------------------------------- /assets/js/blocks/related-posts/block.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { registerBlockType } from '@wordpress/blocks'; 5 | import { __ } from '@wordpress/i18n'; 6 | 7 | /** 8 | * Internal dependencies. 9 | */ 10 | import Edit from './Edit'; 11 | 12 | registerBlockType('elasticpress/related-posts', { 13 | title: __('Related Posts (ElasticPress)', 'elasticpress'), 14 | supports: { 15 | align: true, 16 | }, 17 | category: 'widgets', 18 | attributes: { 19 | alignment: { 20 | type: 'string', 21 | default: 'none', 22 | }, 23 | number: { 24 | type: 'number', 25 | default: 5, 26 | }, 27 | }, 28 | usesContext: ['postId'], 29 | 30 | /** 31 | * Handle edit 32 | * 33 | * @param {object} props Component properties 34 | * @returns {object} 35 | */ 36 | edit(props) { 37 | return ; 38 | }, 39 | 40 | /** 41 | * Handle save 42 | * 43 | * @returns {void} 44 | */ 45 | save() { 46 | return null; 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /assets/js/facets.js: -------------------------------------------------------------------------------- 1 | import { debounce } from './utils/helpers'; 2 | 3 | /** 4 | * Filters the facets to match the input search term when 5 | * the number of terms exceeds the threshold determined 6 | * by the ep_facet_search_threshold filter 7 | * 8 | * @param {event} event - keyup 9 | * @param {Node} facetTerms - terms node 10 | */ 11 | const handleFacetSearch = (event, facetTerms) => { 12 | const { target } = event; 13 | const searchTerm = target.value.toLowerCase(); 14 | const terms = facetTerms.querySelectorAll('.term'); 15 | 16 | terms.forEach((term) => { 17 | const slug = term.getAttribute('data-term-slug'); 18 | const name = term.getAttribute('data-term-name'); 19 | 20 | if (name.includes(searchTerm) || slug.includes(searchTerm)) { 21 | term.classList.remove('hide'); 22 | } else { 23 | term.classList.add('hide'); 24 | } 25 | }); 26 | }; 27 | 28 | /** 29 | * Filter facet choices to match the search field term 30 | */ 31 | const facets = document.querySelectorAll('.widget_ep-facet, .wp-block-elasticpress-facet'); 32 | 33 | facets.forEach((facet) => { 34 | const facetSearchInput = facet.querySelector('.facet-search'); 35 | 36 | if (!facetSearchInput) { 37 | return; 38 | } 39 | 40 | const facetTerms = facet.querySelector('.terms'); 41 | 42 | facet.querySelector('.facet-search').addEventListener( 43 | 'keyup', 44 | debounce((event) => { 45 | if (event.keyCode === 13) { 46 | return; 47 | } 48 | 49 | handleFacetSearch(event, facetTerms); 50 | }, 200), 51 | ); 52 | }); 53 | -------------------------------------------------------------------------------- /assets/js/instant-results/admin/components/facet-selector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { FormTokenField } from '@wordpress/components'; 5 | import { useMemo, useState, WPElement } from '@wordpress/element'; 6 | import { __ } from '@wordpress/i18n'; 7 | 8 | /** 9 | * Internal dependencies. 10 | */ 11 | import { facets } from '../config'; 12 | 13 | /** 14 | * Facet selector component. 15 | * 16 | * @param {object} props Props. 17 | * @param {string} props.defaultValue Default value. 18 | * @returns {WPElement} Element. 19 | */ 20 | export default ({ defaultValue, ...props }) => { 21 | const defaultValues = defaultValue.split(','); 22 | const [selectedFacets, setSelectedFacets] = useState(defaultValues); 23 | 24 | /** 25 | * Get the label for a facet from the facet key. 26 | * 27 | * @param {string} key Facet key. 28 | * @returns {string} Facet label. 29 | */ 30 | const getLabelFromKey = (key) => { 31 | return facets[key]?.label; 32 | }; 33 | 34 | /** 35 | * Get the key for a facet from the facet label. 36 | * 37 | * @param {string} label Facet label. 38 | * @returns {string} Facet key. 39 | */ 40 | const getKeyFromLabel = (label) => { 41 | return Object.keys(facets).find((key) => { 42 | return label === facets[key].label; 43 | }); 44 | }; 45 | 46 | /** 47 | * Suggestions for the token field. 48 | */ 49 | const suggestions = useMemo(() => Object.keys(facets).map(getLabelFromKey).filter(Boolean), []); 50 | 51 | /** 52 | * Values for the token field. 53 | */ 54 | const value = useMemo( 55 | () => selectedFacets.map(getLabelFromKey).filter(Boolean), 56 | [selectedFacets], 57 | ); 58 | 59 | /** 60 | * Handle change to token field. 61 | * 62 | * @param {Array} tokens Selected tokens. 63 | */ 64 | const onChange = (tokens) => { 65 | setSelectedFacets(tokens.map(getKeyFromLabel).filter(Boolean)); 66 | }; 67 | 68 | return ( 69 | <> 70 | 78 | 79 | 80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /assets/js/instant-results/admin/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Window dependencies. 3 | */ 4 | const { facets } = window.epInstantResultsAdmin; 5 | 6 | export { facets }; 7 | -------------------------------------------------------------------------------- /assets/js/instant-results/admin/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { render } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependences. 8 | */ 9 | import FacetSelector from './components/facet-selector'; 10 | 11 | document.addEventListener('DOMContentLoaded', () => { 12 | const input = document.getElementById('feature_instant_results_facets'); 13 | 14 | const { 15 | className, 16 | dataset: { fieldName }, 17 | id, 18 | name, 19 | value, 20 | } = input; 21 | 22 | render( 23 | , 30 | input.parentElement, 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/common/checkbox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Checkbox component. 8 | * 9 | * @param {Option} props Component props. 10 | * @param {string} props.count Checkbox count. 11 | * @param {string} props.id Checkbox ID. 12 | * @param {string} props.label Checkbox label. 13 | * 14 | * @returns {WPElement} Component element. 15 | */ 16 | export default ({ count, id, label, ...props }) => { 17 | return ( 18 |
19 | {' '} 20 | 23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/common/image.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Image component. 8 | * 9 | * @param {Option} props Component props. 10 | * 11 | * @returns {WPElement} Component element. 12 | */ 13 | export default ({ alt, height, ID, src, width, ...props }) => { 14 | return {alt}; 15 | }; 16 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/common/modal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | import FocusTrap from 'focus-trap-react'; 5 | 6 | /** 7 | * WordPress dependencies. 8 | */ 9 | import { forwardRef, useCallback, useEffect, useRef, WPElement } from '@wordpress/element'; 10 | import { closeSmall, Icon } from '@wordpress/icons'; 11 | import { __ } from '@wordpress/i18n'; 12 | 13 | /** 14 | * Modal components. 15 | * 16 | * @param {object} props Component props. 17 | * @param {WPElement} props.children Component children. 18 | * @param {boolean} props.isOpen Whether the modal is open. 19 | * @param {Function} props.onClose Callback to run when modal is closed. 20 | * @param {object} ref Ref. 21 | * @returns {WPElement} React element. 22 | */ 23 | const Modal = ({ children, isOpen, onClose, ...props }, ref) => { 24 | /** 25 | * Reference to close button element. 26 | */ 27 | const closeRef = useRef(null); 28 | 29 | /** 30 | * Handle key down. 31 | * 32 | * @param {Event} event Keydown event. 33 | */ 34 | const onKeyDown = useCallback( 35 | (event) => { 36 | if (event.key === 'Escape' || event.key === 'Esc') { 37 | onClose(); 38 | } 39 | }, 40 | [onClose], 41 | ); 42 | 43 | /** 44 | * Handle binding events to outside DOM elements. 45 | * 46 | * @returns {Function} Clean up function that removes events. 47 | */ 48 | const handleEvents = () => { 49 | const { current: modalEl } = ref; 50 | 51 | modalEl.ownerDocument.body.addEventListener('keydown', onKeyDown); 52 | 53 | return () => { 54 | modalEl.ownerDocument.body.removeEventListener('keydown', onKeyDown); 55 | }; 56 | }; 57 | 58 | /** 59 | * Handle the model being opened or closed. 60 | * 61 | * Adds a class to the body element to allow controlling scrolling. 62 | */ 63 | const handleOpen = () => { 64 | const { current: modalEl } = ref; 65 | 66 | if (isOpen) { 67 | modalEl.ownerDocument.body.classList.add('has-ep-search-modal'); 68 | closeRef.current.focus(); 69 | } else { 70 | modalEl.ownerDocument.body.classList.remove('has-ep-search-modal'); 71 | } 72 | }; 73 | 74 | useEffect(handleEvents, [onKeyDown, ref]); 75 | useEffect(handleOpen, [isOpen, ref]); 76 | 77 | return ( 78 |
86 | {isOpen && ( 87 | 88 |
89 | 98 | {children} 99 |
100 |
101 | )} 102 |
103 | ); 104 | }; 105 | 106 | export default forwardRef(Modal); 107 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/common/panel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { useState, WPElement } from '@wordpress/element'; 5 | import { chevronDown, chevronUp, Icon } from '@wordpress/icons'; 6 | 7 | /** 8 | * Facet wrapper component. 9 | * 10 | * @param {object} props Component props. 11 | * @param {WPElement} props.children Component children. 12 | * @param {boolean} props.defaultIsOpen Whether the panel is open by default. 13 | * @param {string} props.label Facet label. 14 | * @returns {WPElement} Component element. 15 | */ 16 | export default ({ children, defaultIsOpen, label }) => { 17 | const [isOpen, setIsOpen] = useState(defaultIsOpen); 18 | 19 | /** 20 | * Handle click event on the header. 21 | */ 22 | const onClick = () => { 23 | setIsOpen(!isOpen); 24 | }; 25 | 26 | return ( 27 |
28 |

29 | 38 |

39 |
40 | {children(isOpen)} 41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/common/range-slider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | import ReactSlider from 'react-slider'; 5 | 6 | /** 7 | * WordPress dependencies. 8 | */ 9 | import { WPElement } from '@wordpress/element'; 10 | 11 | /** 12 | * Range slider component. 13 | * 14 | * @param {object} props Props. 15 | * @returns {WPElement} Element. 16 | */ 17 | export default ({ ...props }) => { 18 | return ( 19 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/common/small-button.js: -------------------------------------------------------------------------------- 1 | import { WPElement } from '@wordpress/element'; 2 | 3 | /** 4 | * Small button component. 5 | * 6 | * @param {object} props Props. 7 | * @param {WPElement} props.children Children. 8 | * @param {string} props.className Class attribute. 9 | * @returns {WPElement} Element. 10 | */ 11 | export default ({ children, className, ...props }) => { 12 | return ( 13 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/common/star-rating.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { __, sprintf } from '@wordpress/i18n'; 5 | import { WPElement } from '@wordpress/element'; 6 | 7 | /** 8 | * Star rating component. 9 | * 10 | * @param {Option} props Component props. 11 | * @param {string} props.rating Rating. 12 | * 13 | * @returns {WPElement} Component element. 14 | */ 15 | export default ({ rating }) => { 16 | const label = sprintf( 17 | /* translators: %1$f Rating. %2$d Max rating. */ 18 | __('Rated %1$f out of %2$d', 'elasticpress'), 19 | rating, 20 | 5, 21 | ); 22 | 23 | return ( 24 |
25 |
26 | {label} 27 |
28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/facets/facet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import PostTypeFacet from './post-type-facet'; 10 | import PriceRangeFacet from './price-range-facet'; 11 | import TaxonomyTermsFacet from './taxonomy-terms-facet'; 12 | 13 | /** 14 | * Facet component. 15 | * 16 | * @param {object} props Props. 17 | * @param {number} props.index Facet index. 18 | * @param {string} props.name Facet name. 19 | * @param {string} props.label Facet label. 20 | * @param {string} props.postTypes Facet post types. 21 | * @param {'post_type'|'price_range'|'taxonomy'} props.type Facet type. 22 | * @returns {WPElement} Component element. 23 | */ 24 | export default ({ index, label, name, postTypes, type }) => { 25 | const defaultIsOpen = index < 2; 26 | 27 | switch (type) { 28 | case 'post_type': 29 | return ; 30 | case 'price_range': 31 | return ; 32 | case 'taxonomy': 33 | return ( 34 | 40 | ); 41 | default: 42 | return null; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/facets/post-type-facet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { useCallback, useContext, useMemo, WPElement } from '@wordpress/element'; 5 | import { __ } from '@wordpress/i18n'; 6 | 7 | /** 8 | * Internal dependencies. 9 | */ 10 | import { postTypeLabels } from '../../config'; 11 | import Context from '../../context'; 12 | import Panel from '../common/panel'; 13 | import CheckboxList from '../common/checkbox-list'; 14 | import { ActiveContraint } from '../tools/active-constraints'; 15 | 16 | /** 17 | * Post type facet component. 18 | * 19 | * @param {object} props Props. 20 | * @param {boolean} props.defaultIsOpen Whether the panel is open by default. 21 | * @param {string} props.label Facet label. 22 | * @returns {WPElement} Component element. 23 | */ 24 | export default ({ defaultIsOpen, label }) => { 25 | const { 26 | state: { 27 | aggregations: { post_type: { post_type: { buckets = [] } = {} } = {} }, 28 | args: { post_type: selectedPostTypes = [] }, 29 | isLoading, 30 | }, 31 | dispatch, 32 | } = useContext(Context); 33 | 34 | /** 35 | * Create list of filter options from aggregation buckets. 36 | * 37 | * @param {Array} options List of options. 38 | * @param {object} bucket Aggregation bucket. 39 | * @param {string} bucket.key Aggregation key. 40 | * @param {number} index Bucket index. 41 | * @returns {Array} Array of options. 42 | */ 43 | const reduceOptions = useCallback( 44 | (options, { doc_count, key }, index) => { 45 | if (!Object.prototype.hasOwnProperty.call(postTypeLabels, key)) { 46 | return options; 47 | } 48 | 49 | options.push({ 50 | checked: selectedPostTypes.includes(key), 51 | count: doc_count, 52 | id: `ep-search-post-type-${key}`, 53 | label: postTypeLabels[key].singular, 54 | order: index, 55 | value: key, 56 | }); 57 | 58 | return options; 59 | }, 60 | [selectedPostTypes], 61 | ); 62 | 63 | /** 64 | * Reduce buckets to options. 65 | */ 66 | const options = useMemo(() => buckets.reduce(reduceOptions, []), [buckets, reduceOptions]); 67 | 68 | /** 69 | * Handle checkbox change event. 70 | * 71 | * @param {string[]} postTypes Selected post types. 72 | */ 73 | const onChange = (postTypes) => { 74 | dispatch({ type: 'APPLY_ARGS', payload: { post_type: postTypes } }); 75 | }; 76 | 77 | /** 78 | * Handle clearing a post type. 79 | * 80 | * @param {string} postType Post type being cleared. 81 | */ 82 | const onClear = (postType) => { 83 | const postTypes = [...selectedPostTypes]; 84 | const index = postTypes.indexOf(postType); 85 | 86 | postTypes.splice(index, 1); 87 | 88 | dispatch({ type: 'APPLY_ARGS', payload: { post_type: postTypes } }); 89 | }; 90 | 91 | return ( 92 | options.length > 0 && ( 93 | 94 | {() => ( 95 | <> 96 | 103 | 104 | {selectedPostTypes.map((value) => ( 105 | onClear(value)} 109 | /> 110 | ))} 111 | 112 | )} 113 | 114 | ) 115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/facets/search-term-facet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { useContext, useEffect, useState, WPElement } from '@wordpress/element'; 5 | import { __, sprintf } from '@wordpress/i18n'; 6 | 7 | /** 8 | * Internal dependencies. 9 | */ 10 | import Context from '../../context'; 11 | import { useDebounce } from '../../hooks'; 12 | import { ActiveContraint } from '../tools/active-constraints'; 13 | 14 | /** 15 | * Search field component. 16 | * 17 | * @returns {WPElement} Component element. 18 | */ 19 | export default () => { 20 | const { 21 | state: { 22 | args: { search }, 23 | searchedTerm, 24 | }, 25 | dispatch, 26 | } = useContext(Context); 27 | 28 | const [value, setValue] = useState(search); 29 | 30 | /** 31 | * Dispatch the change, with debouncing. 32 | */ 33 | const dispatchChange = useDebounce((value) => { 34 | dispatch({ type: 'NEW_SEARCH_TERM', payload: value }); 35 | }, 300); 36 | 37 | /** 38 | * Handle input changes. 39 | * 40 | * @param {Event} event Change event. 41 | */ 42 | const onChange = (event) => { 43 | setValue(event.target.value); 44 | dispatchChange(event.target.value); 45 | }; 46 | 47 | /** 48 | * Handle clearing. 49 | */ 50 | const onClear = () => { 51 | dispatch({ type: 'NEW_SEARCH_TERM', payload: '' }); 52 | }; 53 | 54 | /** 55 | * Handle an external change to the search value, such as from popping 56 | * state. 57 | */ 58 | const handleSearch = () => { 59 | setValue(search); 60 | }; 61 | 62 | /** 63 | * Effects. 64 | */ 65 | useEffect(handleSearch, [search]); 66 | 67 | return ( 68 | <> 69 | 76 | {searchedTerm && ( 77 | 85 | )} 86 | 87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/layout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { useContext, WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import { facets } from '../config'; 10 | import Context from '../context'; 11 | import Facet from './facets/facet'; 12 | import SearchTermFacet from './facets/search-term-facet'; 13 | import Results from './layout/results'; 14 | import Sidebar from './layout/sidebar'; 15 | import Toolbar from './layout/toolbar'; 16 | import ActiveConstraints from './tools/active-constraints'; 17 | import ClearConstraints from './tools/clear-constraints'; 18 | import SidebarToggle from './tools/sidebar-toggle'; 19 | import Sort from './tools/sort'; 20 | 21 | /** 22 | * Search dialog. 23 | * 24 | * @returns {WPElement} Component element. 25 | */ 26 | export default () => { 27 | const { 28 | state: { isLoading }, 29 | } = useContext(Context); 30 | 31 | return ( 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 |
44 | 45 | 46 | {facets.map(({ label, name, postTypes, type }, index) => ( 47 | 55 | ))} 56 | 57 | 58 | 59 |
60 |
61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/layout/results.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal depenencies. 3 | */ 4 | import { useContext, useEffect, useRef, WPElement } from '@wordpress/element'; 5 | import { _n, sprintf } from '@wordpress/i18n'; 6 | 7 | /** 8 | * Internal dependencies. 9 | */ 10 | import Context from '../../context'; 11 | import Pagination from '../results/pagination'; 12 | import Result from '../results/result'; 13 | import Sort from '../tools/sort'; 14 | 15 | /** 16 | * Search results component. 17 | * 18 | * @returns {WPElement} Component element. 19 | */ 20 | export default () => { 21 | const { 22 | state: { 23 | args: { offset, per_page }, 24 | searchResults, 25 | searchedTerm, 26 | totalResults, 27 | }, 28 | dispatch, 29 | } = useContext(Context); 30 | 31 | const headingRef = useRef(); 32 | 33 | /** 34 | * Handle clicking next. 35 | */ 36 | const onNext = () => { 37 | dispatch({ type: 'NEXT_PAGE' }); 38 | }; 39 | 40 | /** 41 | * Handle clicking previous. 42 | */ 43 | const onPrevious = () => { 44 | dispatch({ type: 'PREVIOUS_PAGE' }); 45 | }; 46 | 47 | /** 48 | * Effects. 49 | */ 50 | useEffect(() => { 51 | headingRef.current.scrollIntoView({ behavior: 'smooth' }); 52 | }, [offset]); 53 | 54 | return ( 55 |
56 |
57 |

58 | {searchedTerm 59 | ? sprintf( 60 | /* translators: %1$d: results count. %2$s: Search term. */ 61 | _n( 62 | '%1$d result for “%2$s“', 63 | '%1$d results for “%2$s“', 64 | totalResults, 65 | 'elasticpress', 66 | ), 67 | totalResults, 68 | searchedTerm, 69 | ) 70 | : sprintf( 71 | /* translators: %d: results count. */ 72 | _n('%d result', '%d results', totalResults, 'elasticpress'), 73 | totalResults, 74 | )} 75 |

76 | 77 | 78 |
79 | 80 | {searchResults.map((hit) => ( 81 | 82 | ))} 83 | 84 | 91 |
92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/layout/sidebar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { useContext, WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import Context from '../../context'; 10 | 11 | /** 12 | * Search field component. 13 | * 14 | * @param {object} props Props. 15 | * @param {WPElement} props.children Children. 16 | * @returns {WPElement} Element. 17 | */ 18 | export default ({ children }) => { 19 | const { 20 | state: { isSidebarOpen }, 21 | } = useContext(Context); 22 | 23 | return ( 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/layout/toolbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Search field component. 8 | * 9 | * @param {object} props Props. 10 | * @param {WPElement} props.children Children. 11 | * @returns {WPElement} Element. 12 | */ 13 | export default ({ children }) => { 14 | return
{children}
; 15 | }; 16 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/results/pagination.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { __, sprintf } from '@wordpress/i18n'; 5 | import { WPElement } from '@wordpress/element'; 6 | 7 | /** 8 | * Search results component. 9 | * 10 | * @param {object} props Props. 11 | * @param {number} props.offset Current items offset. 12 | * @param {Function} props.onNext Next button handler. 13 | * @param {Function} props.onPrevious Previous button handler. 14 | * @param {number} props.perPage Items per page. 15 | * @param {number} props.total Total number of items. 16 | * @returns {WPElement} Element. 17 | */ 18 | export default ({ offset, onNext, onPrevious, perPage, total }) => { 19 | /** 20 | * Current page number. 21 | */ 22 | const currentPage = (offset + perPage) / perPage; 23 | 24 | /** 25 | * Whether there are more pages. 26 | */ 27 | const nextIsAvailable = total > offset + perPage; 28 | 29 | /** 30 | * Whether the are previous pages. 31 | */ 32 | const previousIsAvailable = offset > 0; 33 | 34 | /** 35 | * Total pages. 36 | */ 37 | const totalPages = Math.ceil(total / perPage); 38 | 39 | return ( 40 | 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/results/result.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import { postTypeLabels, isWooCommerce } from '../../config'; 10 | import { formatDate } from '../../functions'; 11 | import StarRating from '../common/star-rating'; 12 | import Image from '../common/image'; 13 | 14 | /** 15 | * Search result. 16 | * 17 | * @param {object} props Component props. 18 | * @param {object} props.hit Elasticsearch hit. 19 | * @returns {WPElement} Component element. 20 | */ 21 | export default ({ hit }) => { 22 | const { 23 | highlight: { post_title: resultTitle, post_content_plain: resultContent = [] }, 24 | _source: { 25 | meta: { _wc_average_rating: [{ value: resultRating = 0 } = {}] = [] }, 26 | post_date: resultDate, 27 | permalink: resultPermalink, 28 | post_type: resultPostType, 29 | price_html: priceHtml, 30 | thumbnail: resultThumbnail = false, 31 | }, 32 | } = hit; 33 | 34 | const postTypeLabel = postTypeLabels[resultPostType]?.singular; 35 | 36 | return ( 37 | 77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/tools/active-constraints.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { createPortal, createRef, WPElement } from '@wordpress/element'; 5 | import { closeSmall, Icon } from '@wordpress/icons'; 6 | import { __, sprintf } from '@wordpress/i18n'; 7 | 8 | /** 9 | * Internal dependencies. 10 | */ 11 | import SmallButton from '../common/small-button'; 12 | 13 | /** 14 | * Create ref for portal. 15 | */ 16 | const ref = createRef(); 17 | 18 | /** 19 | * Active filter component. 20 | * 21 | * @param {object} props Props. 22 | * @param {string} props.label Constraint label. 23 | * @param {Function} props.onClick Click handler. 24 | * @returns {WPElement} Element. 25 | */ 26 | export const ActiveContraint = ({ label, onClick }) => { 27 | if (!ref.current) { 28 | return null; 29 | } 30 | 31 | return createPortal( 32 | 41 | 42 | {label} 43 | , 44 | ref.current, 45 | ); 46 | }; 47 | 48 | /** 49 | * Active constraints component. 50 | * 51 | * @returns {WPElement} Element. 52 | */ 53 | export default () => { 54 | return
; 55 | }; 56 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/tools/clear-constraints.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { useContext, useMemo, WPElement } from '@wordpress/element'; 5 | import { __ } from '@wordpress/i18n'; 6 | 7 | /** 8 | * Internal dependencies. 9 | */ 10 | import { facets } from '../../config'; 11 | import Context from '../../context'; 12 | import SmallButton from '../common/small-button'; 13 | 14 | /** 15 | * Active constraints component. 16 | * 17 | * @returns {WPElement} Element. 18 | */ 19 | export default () => { 20 | const { 21 | state: { args }, 22 | dispatch, 23 | } = useContext(Context); 24 | 25 | /** 26 | * Return whether there are active filters. 27 | * 28 | * Only filters that are available as facets are checked, as these are the 29 | * only filters that will be cleared. This is to support applying filters 30 | * that cannot be modified by the user. 31 | * 32 | * @returns {boolean} Whether there are active filters. 33 | */ 34 | const hasFilters = useMemo(() => { 35 | return facets.some(({ name, type }) => { 36 | switch (type) { 37 | case 'post_type': 38 | case 'taxonomy': 39 | return args[name]?.length > 0; 40 | case 'price_range': 41 | return args.max_price || args.min_price; 42 | default: 43 | return args[name]; 44 | } 45 | }); 46 | }, [args]); 47 | 48 | /** 49 | * Handle clicking button. 50 | * 51 | * @returns {void} 52 | */ 53 | const onClick = () => { 54 | dispatch({ type: 'CLEAR_FACETS' }); 55 | }; 56 | 57 | return ( 58 | hasFilters && ( 59 | {__('Clear filters', 'elasticpress')} 60 | ) 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/tools/sidebar-toggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress deendencies. 3 | */ 4 | import { useContext, WPElement } from '@wordpress/element'; 5 | import { chevronDown, chevronUp, Icon } from '@wordpress/icons'; 6 | import { __ } from '@wordpress/i18n'; 7 | 8 | /** 9 | * Internal deendencies. 10 | */ 11 | import Context from '../../context'; 12 | 13 | /** 14 | * Open sidebar component. 15 | * 16 | * @returns {WPElement} Element. 17 | */ 18 | export default () => { 19 | const { 20 | state: { isSidebarOpen }, 21 | dispatch, 22 | } = useContext(Context); 23 | 24 | /** 25 | * Handle click. 26 | */ 27 | const onClick = () => { 28 | dispatch({ type: 'TOGGLE_SIDEBAR' }); 29 | }; 30 | 31 | return ( 32 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /assets/js/instant-results/components/tools/sort.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress deendencies. 3 | */ 4 | import { useContext, useMemo, WPElement } from '@wordpress/element'; 5 | import { __ } from '@wordpress/i18n'; 6 | 7 | /** 8 | * Internal deendencies. 9 | */ 10 | import { sortOptions } from '../../config'; 11 | import Context from '../../context'; 12 | 13 | /** 14 | * Search results component. 15 | * 16 | * @returns {WPElement} Component element. 17 | */ 18 | export default () => { 19 | const { 20 | state: { 21 | args: { orderby, order }, 22 | }, 23 | dispatch, 24 | } = useContext(Context); 25 | 26 | /** 27 | * The key for the current sorting option. 28 | */ 29 | const currentOption = useMemo(() => { 30 | return Object.keys(sortOptions).find((key) => { 31 | return sortOptions[key].orderby === orderby && sortOptions[key].order === order; 32 | }); 33 | }, [orderby, order]); 34 | 35 | /** 36 | * Handle sorting option change. 37 | * 38 | * @param {Event} event Change event. 39 | */ 40 | const onChange = (event) => { 41 | const { orderby, order } = sortOptions[event.target.value]; 42 | 43 | dispatch({ type: 'APPLY_ARGS', payload: { orderby, order } }); 44 | }; 45 | 46 | return ( 47 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /assets/js/instant-results/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | 6 | /** 7 | * Window dependencies. 8 | */ 9 | const { 10 | apiEndpoint, 11 | apiHost, 12 | argsSchema, 13 | currencyCode, 14 | facets, 15 | isWooCommerce, 16 | locale, 17 | matchType, 18 | paramPrefix, 19 | postTypeLabels, 20 | taxonomyLabels, 21 | } = window.epInstantResults; 22 | 23 | /** 24 | * Sorting options configuration. 25 | */ 26 | const sortOptions = { 27 | relevance_desc: { 28 | name: __('Most relevant', 'elasticpress'), 29 | orderby: 'relevance', 30 | order: 'desc', 31 | currencyCode, 32 | }, 33 | date_desc: { 34 | name: __('Date, newest to oldest', 'elasticpress'), 35 | orderby: 'date', 36 | order: 'desc', 37 | }, 38 | date_asc: { 39 | name: __('Date, oldest to newest', 'elasticpress'), 40 | orderby: 'date', 41 | order: 'asc', 42 | }, 43 | }; 44 | 45 | /** 46 | * Sort by price is only available for WooCommerce. 47 | */ 48 | if (isWooCommerce) { 49 | sortOptions.price_desc = { 50 | name: __('Price, highest to lowest', 'elasticpress'), 51 | orderby: 'price', 52 | order: 'desc', 53 | }; 54 | 55 | sortOptions.price_asc = { 56 | name: __('Price, lowest to highest', 'elasticpress'), 57 | orderby: 'price', 58 | order: 'asc', 59 | }; 60 | } 61 | 62 | export { 63 | apiEndpoint, 64 | apiHost, 65 | argsSchema, 66 | currencyCode, 67 | facets, 68 | isWooCommerce, 69 | locale, 70 | matchType, 71 | paramPrefix, 72 | postTypeLabels, 73 | sortOptions, 74 | taxonomyLabels, 75 | }; 76 | -------------------------------------------------------------------------------- /assets/js/instant-results/context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies. 3 | */ 4 | import { createContext } from '@wordpress/element'; 5 | 6 | export default createContext(); 7 | -------------------------------------------------------------------------------- /assets/js/instant-results/functions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal deendencies. 3 | */ 4 | import { currencyCode, facets, locale } from './config'; 5 | import { sanitizeArg, sanitizeParam } from './utilities'; 6 | 7 | /** 8 | * Clear facet filters from a set of args. 9 | * 10 | * @param {object} args Args to clear facets from. 11 | * @returns {object} Cleared args. 12 | */ 13 | export const clearFacetsFromArgs = (args) => { 14 | const clearedArgs = { ...args }; 15 | 16 | facets.forEach(({ name, type }) => { 17 | switch (type) { 18 | case 'price_range': 19 | delete clearedArgs.max_price; 20 | delete clearedArgs.min_price; 21 | break; 22 | default: 23 | delete clearedArgs[name]; 24 | break; 25 | } 26 | }); 27 | 28 | return clearedArgs; 29 | }; 30 | 31 | /** 32 | * Format a date. 33 | * 34 | * @param {string} date Date string. 35 | * @returns {string} Formatted number. 36 | */ 37 | export const formatDate = (date) => { 38 | return new Date(date).toLocaleString(locale, { dateStyle: 'long' }); 39 | }; 40 | 41 | /** 42 | * Format a number as a price. 43 | * 44 | * @param {number} number Number to format. 45 | * @param {object} options Formatter options. 46 | * @returns {string} Formatted number. 47 | */ 48 | export const formatPrice = (number, options) => { 49 | const format = new Intl.NumberFormat(navigator.language, { 50 | style: 'currency', 51 | currency: currencyCode, 52 | currencyDisplay: 'narrowSymbol', 53 | ...options, 54 | }); 55 | 56 | return format.format(number); 57 | }; 58 | 59 | /** 60 | * Get the post types from a search form. 61 | * 62 | * @param {HTMLFormElement} form Form element. 63 | * @returns {Array} Post types. 64 | */ 65 | export const getPostTypesFromForm = (form) => { 66 | const data = new FormData(form); 67 | 68 | if (data.has('post_type')) { 69 | return data.getAll('post_type').slice(-1); 70 | } 71 | 72 | if (data.has('post_type[]')) { 73 | return data.getAll('post_type[]'); 74 | } 75 | 76 | return []; 77 | }; 78 | 79 | /** 80 | * Get permalink URL parameters from args. 81 | * 82 | * @typedef {object} ArgSchema 83 | * @property {string} type Arg type. 84 | * @property {any} [default] Default arg value. 85 | * @property {Array} [allowedValues] Array of allowed values. 86 | * 87 | * @param {object} args Args 88 | * @param {ArgSchema} schema Args schema. 89 | * @param {string} [prefix] Prefix to prepend to args. 90 | * @returns {URLSearchParams} URLSearchParams instance. 91 | */ 92 | export const getUrlParamsFromArgs = (args, schema, prefix = '') => { 93 | const urlParams = new URLSearchParams(); 94 | 95 | Object.entries(schema).forEach(([arg, options]) => { 96 | const param = prefix + arg; 97 | const value = typeof args[arg] !== 'undefined' ? sanitizeParam(args[arg], options) : null; 98 | 99 | if (value !== null) { 100 | urlParams.set(param, value); 101 | } 102 | }); 103 | 104 | return urlParams; 105 | }; 106 | 107 | /** 108 | * Build request args from URL parameters using a given schema. 109 | * 110 | * @typedef {object} ArgSchema 111 | * @property {string} type Arg type. 112 | * @property {any} [default] Default arg value. 113 | * @property {Array} [allowedValues] Array of allowed values. 114 | * 115 | * @param {URLSearchParams} urlParams URL parameters. 116 | * @param {object.} schema Schema to build args from. 117 | * @param {string} [prefix] Parameter prefix. 118 | * @param {boolean} [useDefaults] Whether to populate params with default values. 119 | * @returns {object.} Query args. 120 | */ 121 | export const getArgsFromUrlParams = (urlParams, schema, prefix = '', useDefaults = true) => { 122 | const args = Object.entries(schema).reduce((args, [arg, options]) => { 123 | const param = urlParams.get(prefix + arg); 124 | const value = 125 | typeof param !== 'undefined' ? sanitizeArg(param, options, useDefaults) : null; 126 | 127 | if (value !== null) { 128 | args[arg] = value; 129 | } 130 | 131 | return args; 132 | }, {}); 133 | 134 | return args; 135 | }; 136 | -------------------------------------------------------------------------------- /assets/js/instant-results/hooks.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from '@wordpress/element'; 2 | import { apiEndpoint, apiHost } from './config'; 3 | 4 | /** 5 | * Get debounced version of a function that only runs a given ammount of time 6 | * after the last time it was run. 7 | * 8 | * @param {Function} callback Function to debounce. 9 | * @param {number} delay Milliseconds to delay. 10 | * @returns {Function} Debounced function. 11 | */ 12 | export const useDebounce = (callback, delay) => { 13 | const timeout = useRef(null); 14 | 15 | return useCallback( 16 | (...args) => { 17 | window.clearTimeout(timeout.current); 18 | 19 | timeout.current = window.setTimeout(() => { 20 | callback(...args); 21 | }, delay); 22 | }, 23 | [callback, delay], 24 | ); 25 | }; 26 | 27 | /** 28 | * Get a callback function for retrieving search results. 29 | * 30 | * @returns {Function} Memoized callback function for retrieving search results. 31 | */ 32 | export const useGetResults = () => { 33 | const abort = useRef(new AbortController()); 34 | const request = useRef(null); 35 | 36 | /** 37 | * Get new search results from the API. 38 | * 39 | * @param {URLSearchParams} urlParams Query arguments. 40 | * @returns {Promise} Request promise. 41 | */ 42 | const getResults = async (urlParams) => { 43 | const url = `${apiHost}${apiEndpoint}?${urlParams.toString()}`; 44 | 45 | abort.current.abort(); 46 | abort.current = new AbortController(); 47 | 48 | request.current = fetch(url, { 49 | signal: abort.current.signal, 50 | headers: { 51 | Accept: 'application/json', 52 | }, 53 | }) 54 | .then((response) => { 55 | return response.json(); 56 | }) 57 | .catch((error) => { 58 | if (error?.name !== 'AbortError' && !request.current) { 59 | throw error; 60 | } 61 | }) 62 | .finally(() => { 63 | request.current = null; 64 | }); 65 | 66 | return request.current; 67 | }; 68 | 69 | return useCallback(getResults, []); 70 | }; 71 | -------------------------------------------------------------------------------- /assets/js/instant-results/reducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies. 3 | */ 4 | import { matchType } from './config'; 5 | import { clearFacetsFromArgs } from './functions'; 6 | 7 | /** 8 | * Initial state. 9 | */ 10 | export const initialState = { 11 | aggregations: {}, 12 | args: { 13 | highlight: '', 14 | offset: 0, 15 | orderby: 'relevance', 16 | order: 'desc', 17 | per_page: 6, 18 | relation: matchType === 'all' ? 'and' : 'or', 19 | search: '', 20 | }, 21 | isLoading: false, 22 | isOpen: false, 23 | isSidebarOpen: false, 24 | isPoppingState: false, 25 | searchResults: [], 26 | searchedTerm: '', 27 | totalResults: 0, 28 | }; 29 | 30 | /** 31 | * Reducer function for handling state changes. 32 | * 33 | * @param {object} state The current state. 34 | * @param {object} action Action data. 35 | * @param {string} action.type The action name. 36 | * @param {object} action.payload New state data from the action. 37 | * @returns {object} Updated state. 38 | */ 39 | export const reducer = (state, { type, payload }) => { 40 | const newState = { ...state, isPoppingState: false }; 41 | 42 | switch (type) { 43 | case 'APPLY_ARGS': { 44 | newState.args = { ...newState.args, ...payload, offset: 0 }; 45 | newState.isOpen = true; 46 | break; 47 | } 48 | case 'CLEAR_FACETS': { 49 | newState.args = clearFacetsFromArgs(newState.args); 50 | break; 51 | } 52 | case 'NEW_SEARCH_TERM': { 53 | newState.args = clearFacetsFromArgs(newState.args); 54 | newState.args.offset = 0; 55 | newState.args.search = payload; 56 | 57 | break; 58 | } 59 | case 'NEW_SEARCH_RESULTS': { 60 | const { 61 | hits: { hits, total }, 62 | aggregations, 63 | } = payload; 64 | 65 | /** 66 | * Total number of items. 67 | */ 68 | const totalNumber = typeof total === 'number' ? total : total.value; 69 | 70 | newState.aggregations = aggregations; 71 | newState.searchResults = hits; 72 | newState.searchedTerm = newState.args.search; 73 | newState.totalResults = totalNumber; 74 | 75 | break; 76 | } 77 | case 'NEXT_PAGE': { 78 | newState.args.offset += newState.args.per_page; 79 | break; 80 | } 81 | case 'PREVIOUS_PAGE': { 82 | newState.args.offset = Math.max(newState.args.offset - newState.args.per_page, 0); 83 | break; 84 | } 85 | case 'START_LOADING': { 86 | newState.isLoading = true; 87 | break; 88 | } 89 | case 'FINISH_LOADING': { 90 | newState.isLoading = false; 91 | break; 92 | } 93 | case 'TOGGLE_SIDEBAR': { 94 | newState.isSidebarOpen = !state.isSidebarOpen; 95 | break; 96 | } 97 | case 'CLOSE_MODAL': { 98 | newState.args = clearFacetsFromArgs(newState.args); 99 | newState.isOpen = false; 100 | break; 101 | } 102 | case 'POP_STATE': { 103 | const { isOpen, ...args } = payload; 104 | 105 | newState.args = args; 106 | newState.isOpen = isOpen; 107 | newState.isPoppingState = true; 108 | 109 | break; 110 | } 111 | default: 112 | break; 113 | } 114 | 115 | return newState; 116 | }; 117 | -------------------------------------------------------------------------------- /assets/js/instant-results/utilities.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sanitize an argument value based on its type. 3 | * 4 | * @param {*} value The value. 5 | * @param {object} options Sanitization options. 6 | * @param {'number'|'numbers'|'string'|'strings'} options.type (optional) Value type. 7 | * @param {Array} options.allowedValues (optional) Allowed values. 8 | * @param {*} options.default (optional) Default value. 9 | * @param {boolean} [useDefaults] Whether to return default values. 10 | * @returns {*} Sanitized value. 11 | */ 12 | export const sanitizeArg = (value, options, useDefaults = true) => { 13 | let sanitizedValue = null; 14 | 15 | switch (value && options.type) { 16 | case 'number': 17 | sanitizedValue = parseFloat(value, 10) || null; 18 | break; 19 | case 'numbers': 20 | sanitizedValue = decodeURIComponent(value) 21 | .split(',') 22 | .map((v) => parseFloat(v, 10)) 23 | .filter(Boolean); 24 | break; 25 | case 'string': 26 | sanitizedValue = value.toString(); 27 | break; 28 | case 'strings': 29 | sanitizedValue = decodeURIComponent(value) 30 | .split(',') 31 | .map((v) => v.toString().trim()); 32 | break; 33 | default: 34 | break; 35 | } 36 | 37 | /** 38 | * If there is a list of allowed values, make sure the value is 39 | * allowed. 40 | */ 41 | if (options.allowedValues) { 42 | sanitizedValue = options.allowedValues.includes(sanitizedValue) ? sanitizedValue : null; 43 | } 44 | 45 | /** 46 | * Populate a default value if one is available and we still don't 47 | * have a value. 48 | */ 49 | if (useDefaults && sanitizedValue === null && typeof options.default !== 'undefined') { 50 | sanitizedValue = options.default; 51 | } 52 | 53 | return sanitizedValue; 54 | }; 55 | 56 | /** 57 | * Sanitize a parameter value based on its type. 58 | * 59 | * @param {*} value The value. 60 | * @param {object} options Sanitization options. 61 | * @param {'number'|'numbers'|'string'|'strings'} options.type (optional) Value type. 62 | * @param {Array} options.allowedValues (optional) Allowed values. 63 | * @param {*} options.default (optional) Default value. 64 | * @param {boolean} [useDefaults] Whether to return default values. 65 | * @returns {*} Sanitized value. 66 | */ 67 | export const sanitizeParam = (value, options, useDefaults = true) => { 68 | let sanitizedValue = null; 69 | 70 | switch (value && options.type) { 71 | case 'number': 72 | case 'string': 73 | sanitizedValue = value; 74 | break; 75 | case 'numbers': 76 | case 'strings': 77 | sanitizedValue = value.join(','); 78 | break; 79 | default: 80 | break; 81 | } 82 | 83 | /** 84 | * If there is a list of allowed values, make sure the value is 85 | * allowed. 86 | */ 87 | if (options.allowedValues) { 88 | sanitizedValue = options.allowedValues.includes(sanitizedValue) ? sanitizedValue : null; 89 | } 90 | 91 | /** 92 | * Populate a default value if one is available and we still don't 93 | * have a value. 94 | */ 95 | if (useDefaults && sanitizedValue === null && typeof options.default !== 'undefined') { 96 | sanitizedValue = options.default; 97 | } 98 | 99 | return sanitizedValue; 100 | }; 101 | -------------------------------------------------------------------------------- /assets/js/notice.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import apiFetch from '@wordpress/api-fetch'; 5 | import domReady from '@wordpress/dom-ready'; 6 | 7 | /** 8 | * Window dependencies. 9 | */ 10 | const { epAdmin, ajaxurl } = window; 11 | 12 | /** 13 | * Initialize. 14 | * 15 | * @returns {void} 16 | */ 17 | const init = () => { 18 | const notices = document.querySelectorAll('.notice[data-ep-notice]'); 19 | 20 | /** 21 | * Handle clicking in an ElasticPress notice. 22 | * 23 | * If the click target is the dismiss button send an AJAX request to remember 24 | * the dismissal. 25 | * 26 | * @param {Event} event Click event. 27 | * @returns {void} 28 | */ 29 | const onClick = (event) => { 30 | /** 31 | * Only proceed if we're clicking dismiss. 32 | */ 33 | if (!event.target.classList.contains('notice-dismiss')) { 34 | return; 35 | } 36 | 37 | /** 38 | * Handler is admin-ajax.php, so the body needs to be form data. 39 | */ 40 | const formData = new FormData(); 41 | 42 | formData.append('action', 'ep_notice_dismiss'); 43 | formData.append('notice', event.currentTarget.dataset.epNotice); 44 | formData.append('nonce', epAdmin.nonce); 45 | 46 | apiFetch({ 47 | method: 'POST', 48 | url: ajaxurl, 49 | body: formData, 50 | }); 51 | }; 52 | 53 | /** 54 | * Bind click events to notices. 55 | */ 56 | for (const notice of notices) { 57 | notice.addEventListener('click', onClick); 58 | } 59 | }; 60 | 61 | /** 62 | * Initialize. 63 | */ 64 | domReady(init); 65 | -------------------------------------------------------------------------------- /assets/js/ordering/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { render } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import { Pointers } from './pointers'; 10 | 11 | render(, document.getElementById('ordering-app')); 12 | -------------------------------------------------------------------------------- /assets/js/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import domReady from '@wordpress/dom-ready'; 5 | import { __ } from '@wordpress/i18n'; 6 | 7 | /** 8 | * Initialize. 9 | * 10 | * @returns {void} 11 | */ 12 | const init = () => { 13 | const tabs = document.querySelectorAll('.ep-credentials-tab'); 14 | const host = document.getElementById('ep_host'); 15 | const hostLabel = host.labels[0]; 16 | const hostDescription = host.nextElementSibling; 17 | const additionalFields = document.getElementsByClassName('ep-additional-fields'); 18 | 19 | let activeTab = document.querySelector('.nav-tab-active'); 20 | 21 | /** 22 | * Is the current tab the ElasticPress.io tab? 23 | * 24 | * @returns {boolean} Whether the current tab is for ElasticPress.io. 25 | */ 26 | const isEpio = () => { 27 | return activeTab && 'epio' in activeTab.dataset; 28 | }; 29 | 30 | let epioHost = isEpio() ? host.value : ''; 31 | let esHost = isEpio() ? '' : host.value; 32 | 33 | /** 34 | * Handle input on the host field. 35 | * 36 | * @param {Event} event Input event. 37 | * @returns {void} 38 | */ 39 | const onInput = (event) => { 40 | if (isEpio()) { 41 | epioHost = event.currentTarget.value; 42 | } else { 43 | esHost = event.currentTarget.value; 44 | } 45 | }; 46 | 47 | /** 48 | * Handle clicking on a tab. 49 | * 50 | * @param {Event} event Click event. 51 | * @returns {void} 52 | */ 53 | const onClick = (event) => { 54 | activeTab = event.currentTarget; 55 | 56 | /** 57 | * Set active tab. 58 | */ 59 | for (const tab of tabs) { 60 | tab.classList.toggle('nav-tab-active', tab === activeTab); 61 | } 62 | 63 | /** 64 | * Hide or show additional fields. 65 | */ 66 | for (const additionalField of additionalFields) { 67 | additionalField.classList.toggle('hidden', !isEpio()); 68 | } 69 | 70 | /** 71 | * Update field label. 72 | */ 73 | hostLabel.innerText = isEpio() 74 | ? __('ElasticPress.io Host URL', 'elasticpress') 75 | : __('Elasticsearch Host URL', 'elasticpress'); 76 | 77 | /** 78 | * If the host field is disabled, we're done. 79 | */ 80 | if (host.disabled) { 81 | return; 82 | } 83 | 84 | /** 85 | * Restore field value for the current tab. 86 | */ 87 | host.value = isEpio() ? epioHost : esHost; 88 | 89 | /** 90 | * Update host field description. 91 | */ 92 | hostDescription.innerText = isEpio() 93 | ? __('Plug in your ElasticPress.io server here!', 'elasticpress') 94 | : __('Plug in your Elasticsearch server here!', 'elasticpress'); 95 | }; 96 | 97 | /** 98 | * Bind input event to host field. 99 | */ 100 | if (host) { 101 | host.addEventListener('input', onInput); 102 | } 103 | 104 | /** 105 | * Bind click events to tabs. 106 | */ 107 | for (const tab of tabs) { 108 | tab.addEventListener('click', onClick); 109 | } 110 | }; 111 | 112 | /** 113 | * Initialize. 114 | */ 115 | domReady(init); 116 | -------------------------------------------------------------------------------- /assets/js/sites-admin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import apiFetch from '@wordpress/api-fetch'; 5 | import { ToggleControl } from '@wordpress/components'; 6 | import domReady from '@wordpress/dom-ready'; 7 | import { render, useState, WPElement } from '@wordpress/element'; 8 | import { __ } from '@wordpress/i18n'; 9 | 10 | /** 11 | * Window dependencies. 12 | */ 13 | const { ajaxurl, epsa } = window; 14 | 15 | /** 16 | * Toggle component. 17 | * 18 | * @param {object} props Component props. 19 | * @param {string} props.blogId Blog ID. 20 | * @param {boolean} props.isDefaultChecked Whether checked by default. 21 | * @returns {WPElement} Toggle component. 22 | */ 23 | const ElasticPressToggleControl = ({ blogId, isDefaultChecked }) => { 24 | const [isChecked, setIsChecked] = useState(isDefaultChecked); 25 | const [isLoading, setIsLoading] = useState(false); 26 | 27 | /** 28 | * Handle toggle change. 29 | * 30 | * @param {boolean} isChecked New checked state. 31 | * @returns {void} 32 | */ 33 | const onChange = async (isChecked) => { 34 | setIsChecked(isChecked); 35 | setIsLoading(true); 36 | 37 | const formData = new FormData(); 38 | 39 | formData.append('action', 'ep_site_admin'); 40 | formData.append('blog_id', blogId); 41 | formData.append('checked', isChecked ? 'yes' : 'no'); 42 | formData.append('nonce', epsa.nonce); 43 | 44 | await apiFetch({ 45 | method: 'POST', 46 | url: ajaxurl, 47 | body: formData, 48 | }); 49 | 50 | setIsLoading(false); 51 | }; 52 | 53 | return ( 54 | 61 | ); 62 | }; 63 | 64 | /** 65 | * Initialize. 66 | * 67 | * @returns {void} 68 | */ 69 | const init = () => { 70 | const toggles = document.getElementsByClassName('index-toggle'); 71 | 72 | for (const toggle of toggles) { 73 | render( 74 | , 78 | toggle.parentElement, 79 | ); 80 | } 81 | }; 82 | 83 | domReady(init); 84 | -------------------------------------------------------------------------------- /assets/js/stats.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-new */ 2 | 3 | import Chart from 'chart.js'; 4 | 5 | const { epChartData } = window; 6 | 7 | /** 8 | * Generates a random string representing a color. 9 | * 10 | * @returns {string} Random color 11 | */ 12 | function getRandomColor() { 13 | const letters = '0123456789ABCDEF'; 14 | let color = '#'; 15 | 16 | for (let i = 0; i < 6; i += 1) { 17 | color += letters[Math.floor(Math.random() * 16)]; 18 | } 19 | 20 | return color; 21 | } 22 | 23 | const barData = Object.entries(epChartData.indices_data); 24 | const barLabels = []; 25 | const barDocs = []; 26 | const barColors = []; 27 | 28 | Chart.defaults.global.legend.labels.usePointStyle = true; 29 | 30 | barData.forEach(function (data) { 31 | barLabels.push(data[1].name); 32 | barDocs.push(data[1].docs); 33 | barColors.push(getRandomColor()); 34 | }); 35 | 36 | const documentChart = document.getElementById('documentChart'); 37 | if (documentChart) { 38 | new Chart(documentChart, { 39 | type: 'horizontalBar', 40 | data: { 41 | labels: barLabels, 42 | datasets: [ 43 | { 44 | label: 'Documents', 45 | backgroundColor: barColors, 46 | data: barDocs, 47 | }, 48 | ], 49 | }, 50 | options: { 51 | legend: { 52 | display: false, 53 | }, 54 | title: { 55 | display: true, 56 | }, 57 | }, 58 | }); 59 | } 60 | 61 | const queriesTotalChart = document.getElementById('queriesTotalChart'); 62 | if (queriesTotalChart) { 63 | new Chart(document.getElementById('queriesTotalChart'), { 64 | type: 'pie', 65 | data: { 66 | labels: ['Indexing operations', 'Total Query operations'], 67 | datasets: [ 68 | { 69 | label: '', 70 | backgroundColor: ['#5ba9a7', '#2e7875', '#a980a4'], 71 | data: [epChartData.index_total, epChartData.query_total], 72 | }, 73 | ], 74 | }, 75 | options: { 76 | responsive: false, 77 | title: { 78 | display: true, 79 | }, 80 | legend: { 81 | position: 'right', 82 | }, 83 | tooltips: { 84 | callbacks: { 85 | /** 86 | * Appends the string operations before tooltip value 87 | * 88 | * @param {object} item Chat item 89 | * @param {object} data Data 90 | * @returns {string} Operations 91 | */ 92 | label(item, data) { 93 | const dataset = data.datasets[item.datasetIndex]; 94 | const currentValue = dataset.data[item.index]; 95 | 96 | return `Operations: ${currentValue}`; 97 | }, 98 | }, 99 | }, 100 | }, 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /assets/js/sync/components/common/date-time.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { dateI18n } from '@wordpress/date'; 5 | import { WPElement } from '@wordpress/element'; 6 | 7 | /** 8 | * Log component. 9 | * 10 | * @param {object} props Component props. 11 | * @param {string} props.dateTime Date and time. 12 | * @returns {WPElement} Component. 13 | */ 14 | export default ({ dateTime, ...props }) => { 15 | return ( 16 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /assets/js/sync/components/common/message-log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Log component. 8 | * 9 | * @param {object} props Component props. 10 | * @param {object[]} props.messages Log messages. 11 | * @returns {WPElement} Component. 12 | */ 13 | export default ({ messages }) => { 14 | return ( 15 |
16 | {messages.map((m, i) => ( 17 |
22 | {i + 1} 23 |
24 | ))} 25 | {messages.map((m) => ( 26 |
30 | {m.message} 31 |
32 | ))} 33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /assets/js/sync/components/common/progress-bar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Progress bar component. 8 | * 9 | * @param {object} props Component props. 10 | * @param {number} props.current Current value. 11 | * @param {number} props.total Current total. 12 | * @param {boolean} props.isComplete If operation is complete. 13 | * @returns {WPElement} Component. 14 | */ 15 | export default ({ isComplete, current, total }) => { 16 | const now = Math.floor((current / total) * 100); 17 | 18 | return ( 19 |
26 |
{`${now}%`}
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /assets/js/sync/components/icons/pause.js: -------------------------------------------------------------------------------- 1 | import { SVG, Path } from '@wordpress/primitives'; 2 | 3 | export default () => { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /assets/js/sync/components/icons/play.js: -------------------------------------------------------------------------------- 1 | import { SVG, Path } from '@wordpress/primitives'; 2 | 3 | export default () => { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /assets/js/sync/components/icons/stop.js: -------------------------------------------------------------------------------- 1 | import { SVG, Path } from '@wordpress/primitives'; 2 | 3 | export default () => { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /assets/js/sync/components/icons/sync.js: -------------------------------------------------------------------------------- 1 | import { SVG, Path } from '@wordpress/primitives'; 2 | 3 | export default () => { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /assets/js/sync/components/icons/thumbs-down.js: -------------------------------------------------------------------------------- 1 | import { SVG, Path } from '@wordpress/primitives'; 2 | 3 | export default () => { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /assets/js/sync/components/icons/thumbs-up.js: -------------------------------------------------------------------------------- 1 | import { SVG, Path } from '@wordpress/primitives'; 2 | 3 | export default () => { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /assets/js/sync/components/sync/controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Button } from '@wordpress/components'; 5 | import { WPElement } from '@wordpress/element'; 6 | import { __ } from '@wordpress/i18n'; 7 | import { update } from '@wordpress/icons'; 8 | 9 | /** 10 | * Internal dependencies. 11 | */ 12 | import pause from '../icons/pause'; 13 | import play from '../icons/play'; 14 | import stop from '../icons/stop'; 15 | 16 | /** 17 | * Sync button component. 18 | * 19 | * @param {object} props Component props. 20 | * @param {boolean} props.disabled If controls are disabled. 21 | * @param {boolean} props.isPaused If syncing is paused. 22 | * @param {boolean} props.isSyncing If syncing is in progress. 23 | * @param {Function} props.onPause Pause button click callback. 24 | * @param {Function} props.onResume Play button click callback. 25 | * @param {Function} props.onStop Stop button click callback. 26 | * @param {Function} props.onSync Sync button click callback. 27 | * @param {boolean} props.showSync If sync button is shown. 28 | * @returns {WPElement} Component. 29 | */ 30 | export default ({ disabled, isPaused, isSyncing, onPause, onResume, onStop, onSync, showSync }) => { 31 | /** 32 | * Render. 33 | */ 34 | return ( 35 |
36 | {showSync && !isSyncing ? ( 37 |
38 | 47 |
48 | ) : null} 49 | 50 | {isSyncing ? ( 51 | <> 52 |
53 | {isPaused ? ( 54 | 63 | ) : ( 64 | 73 | )} 74 |
75 | 76 |
77 | 85 |
86 | 87 | ) : null} 88 | 89 | {showSync ? ( 90 |
91 | 98 |
99 | ) : null} 100 |
101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /assets/js/sync/components/sync/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { TabPanel, ToggleControl } from '@wordpress/components'; 5 | import { useState, WPElement } from '@wordpress/element'; 6 | import { __, sprintf } from '@wordpress/i18n'; 7 | 8 | /** 9 | * Internal dependencies. 10 | */ 11 | import MessageLog from '../common/message-log'; 12 | 13 | /** 14 | * Sync logs component. 15 | * 16 | * @param {object} props Component props. 17 | * @param {object[]} props.messages Log messages. 18 | * @returns {WPElement} Component. 19 | */ 20 | export default ({ messages }) => { 21 | const [isOpen, setIsOpen] = useState(false); 22 | 23 | /** 24 | * Messages with the error status. 25 | */ 26 | const errorMessages = messages.filter((m) => m.status === 'error' || m.status === 'warning'); 27 | 28 | /** 29 | * Log tabs. 30 | */ 31 | const tabs = [ 32 | { 33 | messages, 34 | name: 'full', 35 | title: __('Full Log', 'elasticpress'), 36 | }, 37 | { 38 | messages: errorMessages, 39 | name: 'error', 40 | title: sprintf( 41 | /* translators: %d: Error message count. */ 42 | __('Errors (%d)', 'elasticpress'), 43 | errorMessages.length, 44 | ), 45 | }, 46 | ]; 47 | 48 | /** 49 | * Handle clicking show log button. 50 | * 51 | * @param {boolean} checked If toggle is checked. 52 | * @returns {void} 53 | */ 54 | const onToggle = (checked) => { 55 | setIsOpen(checked); 56 | }; 57 | 58 | return ( 59 | <> 60 | 65 | {isOpen ? ( 66 | 67 | {({ messages }) => } 68 | 69 | ) : null} 70 | 71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /assets/js/sync/components/sync/progress.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Icon } from '@wordpress/components'; 5 | import { useMemo, WPElement } from '@wordpress/element'; 6 | import { dateI18n } from '@wordpress/date'; 7 | import { __ } from '@wordpress/i18n'; 8 | 9 | /** 10 | * Internal dependencies. 11 | */ 12 | import DateTime from '../common/date-time'; 13 | import ProgressBar from '../common/progress-bar'; 14 | import sync from '../icons/sync'; 15 | 16 | /** 17 | * Sync button component. 18 | * 19 | * @param {object} props Component props. 20 | * @param {boolean} props.isCli If progress is for a CLI sync. 21 | * @param {boolean} props.isComplete If sync is complete. 22 | * @param {boolean} props.isPaused If sync is paused. 23 | * @param {number} props.itemsProcessed Number of items processed. 24 | * @param {number} props.itemsTotal Total number of items. 25 | * @param {string} props.dateTime Start date and time. 26 | * @returns {WPElement} Component. 27 | */ 28 | export default ({ isCli, isComplete, isPaused, itemsProcessed, itemsTotal, dateTime }) => { 29 | /** 30 | * Sync progress label. 31 | */ 32 | const label = useMemo( 33 | /** 34 | * Determine appropriate sync status label. 35 | * 36 | * @returns {string} Sync progress label. 37 | */ 38 | () => { 39 | if (isComplete) { 40 | return __('Sync complete', 'elasticpress'); 41 | } 42 | 43 | if (isPaused) { 44 | return __('Sync paused', 'elasticpress'); 45 | } 46 | 47 | if (isCli) { 48 | return __('WP CLI sync in progress', 'elasticpress'); 49 | } 50 | 51 | return __('Sync in progress', 'elasticpress'); 52 | }, 53 | [isCli, isComplete, isPaused], 54 | ); 55 | 56 | return ( 57 |
62 | 63 | 64 |
65 | {label} 66 | {dateTime ? ( 67 | <> 68 | {__('Started on', 'elasticpress')}{' '} 69 | 70 | 71 | ) : null} 72 |
73 | 74 |
75 | 76 |
77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /assets/js/sync/components/sync/status.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Icon } from '@wordpress/components'; 5 | import { dateI18n } from '@wordpress/date'; 6 | import { WPElement } from '@wordpress/element'; 7 | import { __ } from '@wordpress/i18n'; 8 | 9 | /** 10 | * Internal dependencies. 11 | */ 12 | import DateTime from '../common/date-time'; 13 | import thumbsDown from '../icons/thumbs-down'; 14 | import thumbsUp from '../icons/thumbs-up'; 15 | 16 | /** 17 | * Sync button component. 18 | * 19 | * @param {object} props Component props. 20 | * @param {string} props.dateTime Sync date and time. 21 | * @param {boolean} props.isSuccess If sync was a success. 22 | * @returns {WPElement} Component. 23 | */ 24 | export default ({ dateTime, isSuccess }) => { 25 | return ( 26 |

31 | 32 | 33 | {isSuccess 34 | ? __('Sync success on', 'elasticpress') 35 | : __('Sync unsuccessful on', 'elasticpress')}{' '} 36 | 37 | 38 |

39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /assets/js/sync/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Window dependencies. 3 | */ 4 | const { 5 | auto_start_index: autoIndex, 6 | ajax_url: ajaxUrl, 7 | index_meta: indexMeta = null, 8 | is_epio: isEpio, 9 | ep_last_sync_date: lastSyncDateTime = null, 10 | ep_last_sync_failed: lastSyncFailed = false, 11 | nonce, 12 | } = window.epDash; 13 | 14 | export { autoIndex, ajaxUrl, indexMeta, isEpio, lastSyncDateTime, lastSyncFailed, nonce }; 15 | -------------------------------------------------------------------------------- /assets/js/sync/hooks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import apiFetch from '@wordpress/api-fetch'; 5 | import { useCallback, useRef } from '@wordpress/element'; 6 | 7 | /** 8 | * Internal dependencies. 9 | */ 10 | import { ajaxUrl, nonce } from './config'; 11 | 12 | /** 13 | * Indexing hook. 14 | * 15 | * Provides methods for indexing, getting indexing status, and cancelling 16 | * indexing. Methods share an abort controller so that requests can 17 | * interrupt eachother to avoid multiple sync requests causing race conditions 18 | * or duplicate output, such as by rapidly pausing and unpausing indexing. 19 | * 20 | * @returns {object} Sync, sync status, and cancel functions. 21 | */ 22 | export const useIndex = () => { 23 | const abort = useRef(new AbortController()); 24 | const request = useRef(null); 25 | 26 | const sendRequest = useCallback( 27 | /** 28 | * Send AJAX request. 29 | * 30 | * Silently catches abort errors and clears the current request on 31 | * completion. 32 | * 33 | * @param {object} options Request options. 34 | * @throws {Error} Any non-abort errors. 35 | * @returns {Promise} Current request promise. 36 | */ 37 | (options) => { 38 | request.current = apiFetch(options).finally(() => { 39 | request.current = null; 40 | }); 41 | 42 | return request.current; 43 | }, 44 | [], 45 | ); 46 | 47 | const cancelIndex = useCallback( 48 | /** 49 | * Send a request to cancel sync. 50 | * 51 | * @returns {Promise} Fetch request promise. 52 | */ 53 | async () => { 54 | abort.current.abort(); 55 | abort.current = new AbortController(); 56 | 57 | const body = new FormData(); 58 | 59 | body.append('action', 'ep_cancel_index'); 60 | body.append('nonce', nonce); 61 | 62 | const options = { 63 | url: ajaxUrl, 64 | method: 'POST', 65 | body, 66 | signal: abort.current.signal, 67 | }; 68 | 69 | return sendRequest(options); 70 | }, 71 | [sendRequest], 72 | ); 73 | 74 | const index = useCallback( 75 | /** 76 | * Send a request to sync. 77 | * 78 | * @param {boolean} putMapping Whether to put mapping. 79 | * @returns {Promise} Fetch request promise. 80 | */ 81 | async (putMapping) => { 82 | abort.current.abort(); 83 | abort.current = new AbortController(); 84 | 85 | const body = new FormData(); 86 | 87 | body.append('action', 'ep_index'); 88 | body.append('put_mapping', putMapping ? 1 : 0); 89 | body.append('nonce', nonce); 90 | 91 | const options = { 92 | url: ajaxUrl, 93 | method: 'POST', 94 | body, 95 | signal: abort.current.signal, 96 | }; 97 | 98 | return sendRequest(options); 99 | }, 100 | [sendRequest], 101 | ); 102 | 103 | const indexStatus = useCallback( 104 | /** 105 | * Send a request for CLI sync status. 106 | * 107 | * @returns {Promise} Fetch request promise. 108 | */ 109 | async () => { 110 | abort.current.abort(); 111 | abort.current = new AbortController(); 112 | 113 | const body = new FormData(); 114 | 115 | body.append('action', 'ep_index_status'); 116 | body.append('nonce', nonce); 117 | 118 | const options = { 119 | url: ajaxUrl, 120 | method: 'POST', 121 | body, 122 | signal: abort.current.signal, 123 | }; 124 | 125 | return sendRequest(options); 126 | }, 127 | [sendRequest], 128 | ); 129 | 130 | return { cancelIndex, index, indexStatus }; 131 | }; 132 | -------------------------------------------------------------------------------- /assets/js/sync/utilities.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clear sync parameter from the URL. 3 | * 4 | * @returns {void} 5 | */ 6 | export const clearSyncParam = () => { 7 | window.history.replaceState( 8 | {}, 9 | document.title, 10 | document.location.pathname + document.location.search.replace(/&do_sync/, ''), 11 | ); 12 | }; 13 | 14 | /** 15 | * Get the total number of items from index meta. 16 | * 17 | * @param {object} indexMeta Index meta. 18 | * @returns {number} Number of items. 19 | */ 20 | export const getItemsTotalFromIndexMeta = (indexMeta) => { 21 | let itemsTotal = 0; 22 | 23 | if (indexMeta.current_sync_item) { 24 | itemsTotal += indexMeta.current_sync_item.found_items; 25 | } 26 | 27 | itemsTotal = indexMeta.sync_stack.reduce( 28 | (itemsTotal, sync) => itemsTotal + sync.found_items, 29 | itemsTotal, 30 | ); 31 | 32 | itemsTotal += indexMeta.totals.failed; 33 | itemsTotal += indexMeta.totals.skipped; 34 | itemsTotal += indexMeta.totals.synced; 35 | 36 | return itemsTotal; 37 | }; 38 | 39 | /** 40 | * Get the number of processed items from index meta. 41 | * 42 | * @param {object} indexMeta Index meta. 43 | * @returns {number} Number of processed items. 44 | */ 45 | export const getItemsProcessedFromIndexMeta = (indexMeta) => { 46 | let itemsProcessed = 0; 47 | 48 | if (indexMeta.current_sync_item) { 49 | itemsProcessed += indexMeta.current_sync_item.failed; 50 | itemsProcessed += indexMeta.current_sync_item.skipped; 51 | itemsProcessed += indexMeta.current_sync_item.synced; 52 | } 53 | 54 | itemsProcessed += indexMeta.totals.failed; 55 | itemsProcessed += indexMeta.totals.skipped; 56 | itemsProcessed += indexMeta.totals.synced; 57 | 58 | return itemsProcessed; 59 | }; 60 | -------------------------------------------------------------------------------- /assets/js/synonyms/components/SynonymsEditor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { useContext, useEffect, WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import { State, Dispatch } from '../context'; 10 | import AlterativesEditor from './editors/AlternativesEditor'; 11 | import SetsEditor from './editors/SetsEditor'; 12 | import SolrEditor from './editors/SolrEditor'; 13 | 14 | /** 15 | * Synonyms editor component. 16 | * 17 | * @returns {WPElement} Synonyms component 18 | */ 19 | const SynonymsEditor = () => { 20 | const state = useContext(State); 21 | const dispatch = useContext(Dispatch); 22 | const { alternatives, sets, isSolrEditable, isSolrVisible, dirty, submit } = state; 23 | const { 24 | pageHeading, 25 | pageDescription, 26 | pageToggleAdvanceText, 27 | pageToggleSimpleText, 28 | alternativesTitle, 29 | alternativesDescription, 30 | setsTitle, 31 | setsDescription, 32 | solrTitle, 33 | solrDescription, 34 | submitText, 35 | } = window.epSynonyms.i18n; 36 | 37 | /** 38 | * Checks if the form is valid. 39 | * 40 | * @param {object} _state Current state. 41 | * @returns {boolean} If the form is valid 42 | */ 43 | const isValid = (_state) => { 44 | return [..._state.sets, ..._state.alternatives].reduce((valid, item) => { 45 | return !valid ? valid : item.valid; 46 | }, true); 47 | }; 48 | 49 | /** 50 | * Handles submitting the form. 51 | */ 52 | const handleSubmit = () => { 53 | if (isSolrEditable) { 54 | dispatch({ type: 'REDUCE_SOLR_TO_STATE' }); 55 | } 56 | 57 | dispatch({ type: 'VALIDATE_ALL' }); 58 | dispatch({ type: 'REDUCE_STATE_TO_SOLR' }); 59 | dispatch({ type: 'SUBMIT' }); 60 | }; 61 | 62 | /** 63 | * Handle toggling the editor type. 64 | */ 65 | const handleToggleAdvance = () => { 66 | if (isSolrEditable) { 67 | dispatch({ type: 'REDUCE_SOLR_TO_STATE' }); 68 | } else { 69 | dispatch({ type: 'REDUCE_STATE_TO_SOLR' }); 70 | } 71 | 72 | dispatch({ type: 'SET_SOLR_EDITABLE', data: !isSolrEditable }); 73 | }; 74 | 75 | useEffect(() => { 76 | if (submit && !dirty && isValid(state)) { 77 | document.querySelector('.wrap form').submit(); 78 | } 79 | }, [submit, dirty, state]); 80 | 81 | return ( 82 | <> 83 |

84 | {pageHeading}{' '} 85 | 88 |

89 |

{pageDescription}

90 | 91 | {!isSolrEditable && ( 92 | <> 93 |
94 |

{`${setsTitle} (${sets.length})`}

95 |

{setsDescription}

96 | 97 |
98 |
99 |

{`${alternativesTitle} (${alternatives.length})`}

100 |

{alternativesDescription}

101 | 102 |
103 | 104 | )} 105 | 106 |
107 | {isSolrVisible &&

{solrTitle}

} 108 | {isSolrVisible &&

{solrDescription}

} 109 | 110 |
111 | 112 | 117 | 118 |
119 | 122 |
123 | 124 | ); 125 | }; 126 | 127 | export default SynonymsEditor; 128 | -------------------------------------------------------------------------------- /assets/js/synonyms/components/editors/AlternativeEditor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { useContext, useEffect, useMemo, useRef, useState, WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import { Dispatch } from '../../context'; 10 | import LinkedMultiInput from '../shared/LinkedMultiInput'; 11 | 12 | /** 13 | * Alternative Editor 14 | * 15 | * @param {object} props Props. 16 | * @returns {WPElement} AlternativeEditor component 17 | */ 18 | const AlternativeEditor = (props) => { 19 | const { id, synonyms, removeAction, updateAction } = props; 20 | const primary = synonyms.find((item) => item.primary); 21 | const [primaryTerm, setPrimaryTerm] = useState(primary ? primary.value : ''); 22 | const dispatch = useContext(Dispatch); 23 | const primaryRef = useRef(null); 24 | 25 | /** 26 | * Create primary token 27 | * 28 | * @param {string} label Label. 29 | * @returns {object} Primary token 30 | */ 31 | const createPrimaryToken = (label) => { 32 | return { 33 | label, 34 | value: label, 35 | primary: true, 36 | }; 37 | }; 38 | 39 | /** 40 | * Handle key down. 41 | * 42 | * @param {Event} event Keydown event. 43 | */ 44 | const handleKeyDown = (event) => { 45 | switch (event.key) { 46 | case 'Enter': 47 | event.preventDefault(); 48 | break; 49 | default: 50 | } 51 | }; 52 | 53 | useEffect(() => { 54 | dispatch({ 55 | type: 'UPDATE_ALTERNATIVE_PRIMARY', 56 | data: { id, token: createPrimaryToken(primaryTerm) }, 57 | }); 58 | }, [primaryTerm, id, dispatch]); 59 | 60 | useEffect(() => { 61 | primaryRef.current.focus(); 62 | }, [primaryRef]); 63 | 64 | const memoizedSynonyms = useMemo(() => { 65 | return synonyms.filter((item) => !item.primary); 66 | }, [synonyms]); 67 | 68 | return ( 69 | <> 70 | setPrimaryTerm(e.target.value)} 74 | value={primaryTerm} 75 | onKeyDown={handleKeyDown} 76 | ref={primaryRef} 77 | /> 78 | 84 | 85 | ); 86 | }; 87 | 88 | export default AlternativeEditor; 89 | -------------------------------------------------------------------------------- /assets/js/synonyms/components/editors/AlternativesEditor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Fragment, useContext, WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import { Dispatch, State } from '../../context'; 10 | import AlternativeEditor from './AlternativeEditor'; 11 | 12 | /** 13 | * Synonyms editor component. 14 | * 15 | * @param {object} props Props. 16 | * @param {object[]} props.alternatives Defined alternatives (explicit mappings). 17 | * @returns {WPElement} AlternativesEditor component 18 | */ 19 | const AlternativesEditor = ({ alternatives }) => { 20 | const dispatch = useContext(Dispatch); 21 | const state = useContext(State); 22 | const { 23 | alternativesInputHeading, 24 | alternativesPrimaryHeading, 25 | alternativesAddButtonText, 26 | alternativesErrorMessage, 27 | } = window.epSynonyms.i18n; 28 | 29 | /** 30 | * Handle click. 31 | * 32 | * @param {Event} e Event. 33 | */ 34 | const handleClick = (e) => { 35 | const [lastItem] = state.alternatives.slice(-1); 36 | if (!alternatives.length || lastItem.synonyms.filter(({ value }) => value.length).length) { 37 | dispatch({ type: 'ADD_ALTERNATIVE' }); 38 | } 39 | e.preventDefault(); 40 | }; 41 | 42 | return ( 43 |
44 |
45 |

46 | 47 | {alternativesPrimaryHeading} 48 | 49 | 50 | {alternativesInputHeading} 51 | 52 |

53 |
54 | {alternatives.map((props) => ( 55 | 56 |
57 | 62 |
63 | {!props.valid && ( 64 |

{alternativesErrorMessage}

65 | )} 66 |
67 | ))} 68 | 71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default AlternativesEditor; 78 | -------------------------------------------------------------------------------- /assets/js/synonyms/components/editors/SetsEditor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Fragment, useContext, WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import { Dispatch, State } from '../../context'; 10 | import LinkedMultiInput from '../shared/LinkedMultiInput'; 11 | 12 | /** 13 | * Synonyms editor component. 14 | * 15 | * @param {object} props Props 16 | * @param {object[]} props.sets Defined sets (equivalent synonyms). 17 | * @returns {WPElement} SetsEditor component 18 | */ 19 | const SetsEditor = ({ sets }) => { 20 | const dispatch = useContext(Dispatch); 21 | const state = useContext(State); 22 | const { setsInputHeading, setsAddButtonText, setsErrorMessage } = window.epSynonyms.i18n; 23 | 24 | /** 25 | * Handle click. 26 | * 27 | * @param {Event} e Event 28 | */ 29 | const handleClick = (e) => { 30 | const [lastSet] = state.sets.slice(-1); 31 | if (!sets.length || lastSet.synonyms.length) { 32 | dispatch({ type: 'ADD_SET' }); 33 | } 34 | e.preventDefault(); 35 | }; 36 | 37 | return ( 38 |
39 |
40 |

41 | {setsInputHeading} 42 |

43 |
44 | {sets.map((props) => ( 45 | 46 |
47 | 52 |
53 | {!props.valid && ( 54 |

{setsErrorMessage}

55 | )} 56 |
57 | ))} 58 | 61 |
62 |
63 |
64 | ); 65 | }; 66 | 67 | export default SetsEditor; 68 | -------------------------------------------------------------------------------- /assets/js/synonyms/components/editors/SolrEditor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { useContext, WPElement } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import { State, Dispatch } from '../../context'; 10 | 11 | /** 12 | * Synonym Inspector 13 | * 14 | * @returns {WPElement} SolrEditor Component 15 | */ 16 | const SolrEditor = () => { 17 | const state = useContext(State); 18 | const dispatch = useContext(Dispatch); 19 | const { alternatives, isSolrEditable, isSolrVisible, sets, solr } = state; 20 | const { 21 | synonymsTextareaInputName, 22 | solrInputHeading, 23 | solrAlternativesErrorMessage, 24 | solrSetsErrorMessage, 25 | } = window.epSynonyms.i18n; 26 | 27 | return ( 28 |
29 |
30 |

31 | {solrInputHeading} 32 |

33 |
34 |