diff --git a/js/iiif-iabookreader_strawberry.js b/js/iiif-iabookreader_strawberry.js index a8c23570..075d7c8e 100644 --- a/js/iiif-iabookreader_strawberry.js +++ b/js/iiif-iabookreader_strawberry.js @@ -331,7 +331,7 @@ BookReader.prototype.buildViewpageDiv = function(jViewpageDiv) { var index = this.currentIndex(); //OLD//var tilesourceUri = this.getPageURI(index, 1, 0).replace(/full.*/, "info.json"); //var tilesourceUri = this.getPageProp(index, 'infojson'); - var tilesourceUri = this.getPageURI(index, 1, 0).replace(/full.*/, "info.json") + getURLArgument(this.getPageURI(index, 1, 0)); + var tilesourceUri = this.getIIIFInfoJsonFromURL(this.getPageURI(index, 1, 0)) ; var dosd = $(osd_common.replace(/\[ID\]/g, "osd_s").replace('[TS]', tilesourceUri)); jViewpageDiv.html(dosd); } @@ -346,7 +346,8 @@ BookReader.prototype.buildViewpageDiv = function(jViewpageDiv) { if (typeof this.getPageURI(indices[0], 1, 0) != 'undefined') { //OLD//var tilesourceUri_left = this.getPageURI(indices[0], 1, 0).replace(/full.*/, "info.json"); //var tilesourceUri_left = this.getPageProp(indices[0], 'infojson'); - var tilesourceUri_left = this.getPageURI(indices[0], 1, 0).replace(/full.*/, "info.json") + getURLArgument(this.getPageURI(indices[0], 1, 0)); + + var tilesourceUri_left = this.getIIIFInfoJsonFromURL(this.getPageURI(indices[0], 1, 0)) ; var osd_left = osd_common.replace(/\[ID\]/g, "osd_l").replace('[TS]', tilesourceUri_left); } else { @@ -358,7 +359,8 @@ BookReader.prototype.buildViewpageDiv = function(jViewpageDiv) { if (typeof this.getPageURI(indices[1], 1, 0) != 'undefined') { //OLD//var tilesourceUri_right = this.getPageURI(indices[1], 1, 0).replace(/full.*/, "info.json"); //var tilesourceUri_right = this.getPageProp(indices[1], 'infojson'); - var tilesourceUri_right = this.getPageURI(indices[1], 1, 0).replace(/full.*/, "info.json") + getURLArgument(this.getPageURI(indices[1], 1, 0)); + + var tilesourceUri_right = this.getIIIFInfoJsonFromURL(this.getPageURI(indices[1], 1, 0)) ; var osd_right = osd_common.replace(/\[ID\]/g, "osd_r").replace('[TS]', tilesourceUri_right); } else { diff --git a/js/mirador_strawberry.js b/js/mirador_strawberry.js index 6e57e791..eb5e7ce6 100644 --- a/js/mirador_strawberry.js +++ b/js/mirador_strawberry.js @@ -281,13 +281,6 @@ ); } - // Build page parameter - const canvasIndices = visibleCanvases.map(c => canvasIds.indexOf(c) + 1) - if (view === 'single' || canvasIndices.length == 1) { - newParams.page = canvasIndices[0] - } else if (view === 'book') { - newParams.page = canvasIndices.find(e => !!e).join(',') - } // Now at the end. If a VTT annotation requested a Canvas to be set. we need to check if we have in the config // A temporary stored valued of the last clicked annotation. // Use if here. @@ -305,22 +298,33 @@ const { windowId, companionWindowId } = action const query = yield effects.select(Mirador.selectors.getSearchQuery, { companionWindowId, windowId }) newParams.search = query + let $fragment = ''; + for (const [p, val] of new URLSearchParams(newParams).entries()) { + $fragment += `${p}/${val}/`; + }; + $fragment = $fragment.slice(0, -1); + history.replaceState( + { searchParams: newParams }, + '', + `${window.location.pathname}#${$fragment}` + ); } else if (action.type === ActionTypes.REMOVE_SEARCH) { delete newParams.search + let $fragment = ''; + for (const [p, val] of new URLSearchParams(newParams).entries()) { + $fragment += `${p}/${val}/`; + }; + $fragment = $fragment.slice(0, -1); + history.replaceState( + { searchParams: newParams }, + '', + `${window.location.pathname}#${$fragment}` + ); } - let $fragment = ''; - for (const [p, val] of new URLSearchParams(newParams).entries()) { - $fragment += `${p}/${val}/`; - }; - $fragment = $fragment.slice(0, -1); - history.replaceState( - { searchParams: newParams }, - '', - `${window.location.pathname}#${$fragment}` - ); + } function* rootSaga() { yield effects.takeEvery( @@ -403,6 +407,25 @@ $options.manifests = $manifests; } + const readFragmentSearch = function() { + const urlArray = window.location.hash.replace('#','').split('/'); + const urlHash = {}; + for (let i = 0; i < urlArray.length; i += 2) { + urlHash[urlArray[i]] = urlArray[i + 1]; + } + if (urlHash['search'] != undefined) { + return decodeURIComponent(urlHash['search'].replace(/\+/g, " ")); + } + else { + return ''; + } + }; + + const search_string = readFragmentSearch(); + if (search_string.length > 0 ) { + $options.windows[0].defaultSearchQuery = search_string; + } + // Allow last minute overrides. These are more complex bc we have windows as an array and window too. // Allow a last minute override, exclude main element manifest if (typeof drupalSettings.format_strawberryfield.mirador[element_id]['viewer_overrides'] == 'object' && @@ -434,6 +457,8 @@ }; } + + //@TODO add an extra Manifests key with every other one so people can select the others. if (drupalSettings.format_strawberryfield.mirador[element_id]['custom_js'] == true) { const miradorInstance = renderMirador($options); diff --git a/js/plugin.iiif-iabookreader_strawberry.js b/js/plugin.iiif-iabookreader_strawberry.js index 9457fc38..01621956 100644 --- a/js/plugin.iiif-iabookreader_strawberry.js +++ b/js/plugin.iiif-iabookreader_strawberry.js @@ -668,9 +668,41 @@ BookReader.prototype.parseSequence = function (sequenceId) { return url.search } - }; + + +BookReader.prototype.getIIIFInfoJsonFromURL = function(string){ + let url; + + try { + url = new URL(string); + } catch (_) { + return ''; + } + let path = url.pathname; + // IIIF will have id/crop/size/rotation/filename So we will split and reverse + if (path !== '/') { + let path_parts = path.split("/"); + if (path_parts.length >= 5) { + path_parts = path_parts.reverse(); + path_parts = path_parts.slice(4); + //reverse again + path_parts = path_parts.reverse(); + path = path_parts.join('/') + '/info.json'; + url.pathname = path; + return url.toString(); + } + else { + // Might be non IIIF, so we return the Image itself. + return string; + } + } + else { + return string; + } +} + /** * @param {number} index * @return {Number|undefined} diff --git a/modules/format_strawberryfield_views/format_strawberryfield_views.module b/modules/format_strawberryfield_views/format_strawberryfield_views.module index a37fa3f9..481400eb 100644 --- a/modules/format_strawberryfield_views/format_strawberryfield_views.module +++ b/modules/format_strawberryfield_views/format_strawberryfield_views.module @@ -190,6 +190,10 @@ function format_strawberryfield_views_views_data_alter(array &$data) { 'id' => 'sbf_flavors_join', ], ]; + if ($sbf_join_field != 'sbf_flavors_join') { + $table[$sbf_join_field]['real field'] = 'sbf_flavors_join'; + } + // @TODO add also an argument ID if relationships are enabled to // $table[$advanced_fulltext_field]['argument']['id'] = 'sbf_advanced_search_api_fulltext'; // Requires a special Argument Plugin. Not needed right now. @@ -208,6 +212,24 @@ function format_strawberryfield_views_views_data_alter(array &$data) { if ($ado_filter != 'sbf_ado_filter') { $table[$ado_filter]['real field'] = 'sbf_ado_filter'; } + + $sbf_ado_join_field = _search_api_views_find_field_alias('sbf_ado_join', $table); + $table[$sbf_ado_join_field] = [ + 'title' => t('ADO(node) to ADO(node) Join'), + 'group' => t('Search'), + 'help' => t('Joins ADOs to ADOs when doing a Full Text Search.'), + 'filter' => [ + 'title' => t('ADO(node) to ADO(node) Join'), + 'field' => 'id', + 'id' => 'sbf_ado_join', + ], + ]; + if ($sbf_ado_join_field != 'sbf_ado_join') { + $table[$sbf_ado_join_field]['real field'] = 'sbf_ado_join'; + } + + + } catch (\Exception $e) { $args = [ @@ -258,7 +280,7 @@ function format_strawberryfield_views_library_info_alter(&$libraries, $extension function format_strawberryfield_views_views_pre_render(ViewExecutable $view) { $current_display = $view->current_display; $view_config = $view->storage->getDisplay($current_display); - if (!empty($view_config['display_options']['display_extenders']['sbf_ajax_interactions'] ?? NULL)) { + if (!empty($view_config['display_options']['display_extenders']['sbf_ajax_interactions'] ?? NULL) && !empty($view_config['display_options']['display_extenders']['sbf_ajax_interactions']['sbf_ajax_interactions_arguments'] ?? NULL)) { if ($view_config['display_options']['display_extenders']['sbf_ajax_interactions']) { $view->element['#attached']['library'][] = 'format_strawberryfield_views/view-ajax-interactions'; $view->element['#attached']['drupalSettings']['sbf_ajax_interactions'][$view->dom_id] = $view_config['display_options']['display_extenders']['sbf_ajax_interactions']['sbf_ajax_interactions_arguments']; diff --git a/modules/format_strawberryfield_views/format_strawberryfield_views.routing.yml b/modules/format_strawberryfield_views/format_strawberryfield_views.routing.yml index 868f5138..fb40a0eb 100644 --- a/modules/format_strawberryfield_views/format_strawberryfield_views.routing.yml +++ b/modules/format_strawberryfield_views/format_strawberryfield_views.routing.yml @@ -4,9 +4,3 @@ exposed_views_form_modal.block.ajax: _controller: '\Drupal\format_strawberryfield_views\Controller\ViewsExposedFormModalBlockAjaxController::ajaxExposedFormBlockView' requirements: _access: 'TRUE' -format_strawberryfield_views.views.sajax: - path: '/sbf/views-ajax' - defaults: - _controller: '\Drupal\format_strawberryfield_views\Controller\FormatStrawberryfieldViewAjaxController::ajaxViewAdd' - requirements: - _access: 'TRUE' diff --git a/modules/format_strawberryfield_views/js/facets-views-ajax.js b/modules/format_strawberryfield_views/js/facets-views-ajax.js index 11df2688..ef8470dd 100644 --- a/modules/format_strawberryfield_views/js/facets-views-ajax.js +++ b/modules/format_strawberryfield_views/js/facets-views-ajax.js @@ -22,7 +22,7 @@ // Loop through all facets. $.each(settings.facets_views_ajax, function (facetId, facetSettings) { // Get the View for the current facet. - var view, current_dom_id, view_path; + var view, current_dom_id, view_path, all_dom_ids_need_refresh = []; if (settings.views && settings.views.ajaxViews) { $.each(settings.views.ajaxViews, function (domId, viewSettings) { // Check if we have facet for this view. @@ -31,20 +31,37 @@ current_dom_id = viewSettings.view_dom_id; view_path = facetSettings.ajax_path; } + else { + // Means we don't have facets for this view, but we still might have a pager that needs to be reloaded, so it catches up with the Facet Query arguments. + const pagers = $('.js-view-dom-id-' + viewSettings.view_dom_id).find( + '.js-pager__items a, th.views-field a, .attachment .views-summary a', + ); + if (typeof pagers !== "undefined" && pagers.length > 0) { + // means we have a pager + all_dom_ids_need_refresh.push(viewSettings.view_dom_id); + } + } }); } if (!view || view.length != 1) { return; } - + all_dom_ids_need_refresh = Array.from(new Set(all_dom_ids_need_refresh)); // Update view on summary block click. if (Drupal.AjaxFacetsView.updateFacetsSummaryBlock() && (facetId === 'facets_summary_ajax')) { const elementsToAttach = once('summaryblock_attache', '[data-drupal-facets-summary-id=' + facetSettings.facets_summary_id + ']', context); $(elementsToAttach).children('ul').children('li').click(function (e) { e.preventDefault(); var facetLink = $(this).find('a'); + // Note for myself here. Only the actual View that is targeted by the current Facet can use facetLink.attr('href') + // the other ones need to use the original URL cleaned up + the arguments of the facetLink.attr('href') + // This is needed since Facet URL generator will (for good reasons) the ?page=argument. + // And also is absolutely unaware of pagers with different names! Drupal.AjaxFacetsView.UpdateView(facetLink.attr('href'), current_dom_id, view_path); + all_dom_ids_need_refresh.forEach((other_dom_id) => { + }); + }); } // Update view on facet item click. @@ -54,7 +71,13 @@ $(facet_item).unbind('facets_filter.facets'); $(facet_item).on('facets_filter.facets', function (event, url) { $('.js-facets-widget').trigger('facets_filtering'); + // Note for myself here. Only the actual View that is targeted by the current Facet can use facetLink.attr('href') + // the other ones need to use the original URL cleaned up + the arguments of the facetLink.attr('href') + // This is needed since Facet URL generator will (for good reasons) the ?page=argument. + // And also is absolutely unaware of pagers with different names! Drupal.AjaxFacetsView.UpdateView(url, current_dom_id, view_path); + all_dom_ids_need_refresh.forEach((other_dom_id) => { + }); }); } }); @@ -68,14 +91,16 @@ Drupal.AjaxFacetsView.UpdateView = function (href, current_dom_id, view_path) { // Refresh view. - if (typeof(Drupal.views.instances['views_dom_id:' + current_dom_id]) !== 'undefined') { + var atLeastone = false; + if (typeof(Drupal.views.instances['views_dom_id:' + current_dom_id]) !== 'undefined') { + atLeastone = true; var views_parameters = Drupal.Views.parseQueryString(href); let views_path = 'search'; if (Drupal.views.instances['views_dom_id:' + current_dom_id].settings.view_base_path !== 'undefined') { views_path = Drupal.views.instances['views_dom_id:' + current_dom_id].settings.view_base_path; } - var views_arguments = Drupal.Views.parseViewArgs(href, views_path); - var views_settings = $.extend( + const views_arguments = Drupal.Views.parseViewArgs(href, views_path); + const views_settings = $.extend( {}, Drupal.views.instances['views_dom_id:' + current_dom_id].settings, views_arguments, @@ -83,14 +108,28 @@ ); // Not even needed here if we are using the original element settings ....mmmm // Update View. - var views_ajax_settings = Drupal.views.instances['views_dom_id:' + current_dom_id].element_settings; + const views_ajax_settings = Drupal.views.instances['views_dom_id:' + current_dom_id].element_settings; views_ajax_settings.submit = views_settings; // Used to be the way in Drupal 9.x to 10.0 ... views_ajax_settings.url = view_path + '?q=' + href; views_ajax_settings.url = view_path; - var viewRefreshAjaxObject = Drupal.ajax(views_ajax_settings); + const viewRefreshAjaxObject = Drupal.ajax(views_ajax_settings); + const success = viewRefreshAjaxObject.success(); + + viewRefreshAjaxObject.success = function (response, status) { + return Promise.resolve( + Drupal.Ajax.prototype.success.call(viewRefreshAjaxObject, response, status), + ).then(() => { + Drupal.AjaxFacetsView.updateFacetsBlocks(href, views_settings.view_name, views_settings.view_display_id); + if (typeof(drupalSettings.format_strawberryfield_views) !== 'undefined') { + // Refresh facets blocks. + Drupal.updateModalViewsFormBlocks(href, views_settings.view_name, views_settings.view_display_id); + } + }); + }; viewRefreshAjaxObject.execute(); - + } + if (atLeastone) { // Update url. window.historyInitiated = true; window.history.pushState(null, document.title, href); @@ -102,11 +141,6 @@ window.location.reload(); } }); - this.updateFacetsBlocks(href, views_settings.view_name, views_settings.view_display_id); - if (typeof(drupalSettings.format_strawberryfield_views) !== 'undefined') { - // Refresh facets blocks. - Drupal.updateModalViewsFormBlocks(href, views_settings.view_name, views_settings.view_display_id); - } } } Drupal.AjaxFacetsView.updateFacetsBlocks = function (href, view_id, current_display_id) { @@ -133,7 +167,6 @@ // Update facets summary block. if (this.updateFacetsSummaryBlock()) { - var $facet_summary_wrapper = $('[data-drupal-facets-summary-id=' + settings.facets_views_ajax.facets_summary_ajax.facets_summary_id + ']'); if ($facet_summary_wrapper.length > 0) { var facet_summary_wrapper_id = $facet_summary_wrapper.attr('id'); @@ -148,7 +181,9 @@ facet_settings.submit.facet_summary_wrapper_id = settings.facets_views_ajax.facets_summary_ajax.facets_summary_id; } } - Drupal.ajax(facet_settings).execute(); + if (Object.keys(facet_settings.submit.facets_blocks).length > 0) { + Drupal.ajax(facet_settings).execute(); + } }; // Helper function to determine if we should update the summary block. diff --git a/modules/format_strawberryfield_views/js/sbf-views-ajax-dynamic.js b/modules/format_strawberryfield_views/js/sbf-views-ajax-dynamic.js index afd7519c..46dfb63a 100644 --- a/modules/format_strawberryfield_views/js/sbf-views-ajax-dynamic.js +++ b/modules/format_strawberryfield_views/js/sbf-views-ajax-dynamic.js @@ -46,57 +46,95 @@ // If using the load even we can't relay on the target anymore because // it is bound to the document/window. - let base = e.target.id; - // Retrieve the path to use for views' ajax. - let ajaxPath = drupalSettings?.views?.ajax_path ?? '/views/ajax' ; - - // If there are multiple views this might've ended up showing multiple - // times. - if (ajaxPath.constructor.toString().indexOf('Array') !== -1) { - ajaxPath = ajaxPath[0]; + function delay (miliseconds) { + return new Promise((resolve) => { + window.setTimeout(() => { + resolve(); + }, miliseconds); + }); } - // Check if there are any GET parameters to send to views. - let queryString = window.location.search || ''; - if (queryString !== '') { - // Remove the question mark and Drupal path component if any. - queryString = queryString - .slice(1) - .replace(/q=[^&]+&?|&?render=[^&]+/, ''); - if (queryString !== '') { - // If there is a '?' in ajaxPath, clean URL are on and & should be - // used to add parameters. - queryString = (/\?/.test(ajaxPath) ? '&' : '?') + queryString; - } - } + (async function () { + let currenttarget = e.currentTarget; + let base = e.target.id; + + // Check if target and base exist: + if ((currenttarget?.dataset?.sbfViewRenderTarget ?? FALSE) && (base ?? FALSE)) { + try { + // NOTE for the Future. We have to store upfront e.currentTarget; bc a delay (sync or async) ends + // making it NULL. According to Mozilla, e.currentTarget is not NULL only briefly while the event is being handled. + await delay(100); + // Retrieve the path to use for views' ajax. + let ajaxPath = drupalSettings?.views?.ajax_path ?? '/views/ajax'; + + // If there are multiple views this might've ended up showing multiple + // times. + if (ajaxPath.constructor.toString().indexOf('Array') !== -1) { + ajaxPath = ajaxPath[0]; + } + + // Check if there are any GET parameters to send to views. + + let queryString = window.location.search || ''; + if (queryString == '') { + // Thanking the Red Wizards for providing this even before we have an actually bookmarkable URL + let query_object = drupalSettings?.path?.currentQuery ?? {}; + console.log(JSON.stringify(query_object)); + //TODO. So far we had no need for this but the right approach if this is needed would be + // queryString = JSON.stringify(query_object); + // And then make it part of the submit_settings object. + } - const base_views_settings = { - view_name: e.currentTarget.dataset.sbfViewId, - view_display_id: e.currentTarget.dataset.sbfViewDisplayId, - //@TODO use the argument separator settings here. - view_args: e.currentTarget.dataset.sbfViewArguments, - view_dom_id: e.currentTarget.dataset.sbfViewRenderTarget, - view_path: window.location.pathname, - }; - let submit_settings = - $.extend( - {}, - base_views_settings, - ); - - const element_settings = { - url: drupalSettings.path.baseUrl + drupalSettings.path.pathPrefix + 'sbf/views-ajax' + queryString, - event: 'loadView', - base: base, - element: e.currentTarget, - httpMethod: "GET", - progress: { - type: 'throbber' - }, - submit: submit_settings - }; - let ajaxObject = new Drupal.ajax(element_settings); - ajaxObject.execute(); + if (queryString !== '') { + // Remove the question mark and Drupal path component if any. + queryString = queryString + .slice(1) + .replace(/q=[^&]+&?|&?render=[^&]+/, ''); + if (queryString !== '') { + // If there is a '?' in ajaxPath, clean URL are on and & should be + // used to add parameters. + queryString = (/\?/.test(ajaxPath) ? '&' : '?') + queryString; + } + } + + + const base_views_settings = { + view_name: currenttarget.dataset.sbfViewId, + view_display_id: currenttarget.dataset.sbfViewDisplayId, + //@TODO use the argument separator settings here. + view_args: currenttarget.dataset.sbfViewArguments, + view_dom_id: currenttarget.dataset.sbfViewRenderTarget, + view_path: window.location.pathname, + }; + let submit_settings = + $.extend( + {}, + base_views_settings, + Drupal.Views.parseQueryString(queryString), + ); + + const element_settings = { + url: drupalSettings.path.baseUrl + drupalSettings.path.pathPrefix + 'views/ajax', + event: 'loadView', + base: base, + element: currenttarget, + httpMethod: "GET", + progress: { + type: 'throbber' + }, + submit: submit_settings + }; + let ajaxObject = new Drupal.ajax(element_settings); + ajaxObject.execute(); + } + catch(e) { + console.log(e); + } + } + else { + // Do nothing. The Target is not defined in the data attributes. + } + })(); }; Drupal.behaviors.sbf_views_ajax_dynamic = { diff --git a/modules/format_strawberryfield_views/js/sbf-views-ajax-interactions.js b/modules/format_strawberryfield_views/js/sbf-views-ajax-interactions.js index 769a6770..796cd77a 100644 --- a/modules/format_strawberryfield_views/js/sbf-views-ajax-interactions.js +++ b/modules/format_strawberryfield_views/js/sbf-views-ajax-interactions.js @@ -35,7 +35,16 @@ view_instance.settings.view_args = image_annotation; } } - view_instance.$view.trigger("RefreshView"); + //view_instance.$view.trigger("RefreshView"); + + let href = window.location.href; + if (typeof Drupal.AjaxFacetsView != "undefined") { + Drupal.AjaxFacetsView.UpdateView(href, view_instance.settings.view_dom_id, "/views/ajax"/*view_instance.settings.view_path*/); + } + else { + view_instance.$view.trigger("RefreshView"); + } + //Drupal.AjaxFacetsView.updateFacetsBlocks(href, view_instance.settings.view_name , view_instance.settings.view_display_id); } } } diff --git a/modules/format_strawberryfield_views/src/Controller/FormatStrawberryfieldViewAjaxController.php b/modules/format_strawberryfield_views/src/Controller/FormatStrawberryfieldViewAjaxController.php index e8a33f55..35171ee2 100644 --- a/modules/format_strawberryfield_views/src/Controller/FormatStrawberryfieldViewAjaxController.php +++ b/modules/format_strawberryfield_views/src/Controller/FormatStrawberryfieldViewAjaxController.php @@ -31,6 +31,25 @@ */ class FormatStrawberryfieldViewAjaxController extends ViewAjaxController { + /** + * Parameters that should be filtered and ignored inside ajax requests. + */ + public const FILTERED_QUERY_PARAMETERS = [ + 'view_name', + 'view_display_id', + 'view_args', + 'view_path', + 'view_dom_id', + 'pager_element', + 'view_base_path', + 'ajax_page_state', + 'exposed_form_display', + AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER, + FormBuilderInterface::AJAX_FORM_REQUEST, + MainContentViewSubscriber::WRAPPER_FORMAT, + ]; + + /** * The entity storage for views. * @@ -136,7 +155,7 @@ public function ajaxView(Request $request) { }, $args); $path = $request->get('view_path') ?? Html::escape($this->currentPath->getPath()); - // If a view has an invalid Path (e.g you added some % somewhere) this will be null. + // If a view has an invalid Path (e.g. you added some % somewhere) this will be null. $target_url = $this->pathValidator->getUrlIfValid($path ?? '/'); $dom_id = $request->get('view_dom_id'); $dom_id = isset($dom_id) ? preg_replace('/[^a-zA-Z0-9_-]+/', '-', $dom_id) : NULL; @@ -147,25 +166,8 @@ public function ajaxView(Request $request) { $response = new ViewAjaxResponse(); - - - // Remove all of this stuff from the query of the request so it doesn't - // end up in pagers and tablesort URLs. - // @todo Remove this parsing once these are removed from the request in - // https://www.drupal.org/node/2504709. - foreach ([ - 'view_name', - 'view_display_id', - 'view_args', - 'view_path', - 'view_dom_id', - 'pager_element', - 'view_base_path', - 'exposed_form_display', - AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER, - FormBuilderInterface::AJAX_FORM_REQUEST, - MainContentViewSubscriber::WRAPPER_FORMAT, - ] as $key) { + $existing_page_state = $request->get('ajax_page_state'); + foreach (self::FILTERED_QUERY_PARAMETERS as $key) { $request->query->remove($key); $request->request->remove($key); } @@ -176,18 +178,24 @@ public function ajaxView(Request $request) { } $view = $this->executableFactory->get($entity); if ($view && $view->access($display_id) && $view->setDisplay($display_id) && $view->display_handler->ajaxEnabled()) { - // Fix the current path for paging. if (!empty($path)) { $this->currentPath->setPath('/' . ltrim($path, '/'), $request); } - // Let's check if our view has our advanced search filter + $filters = $view->getDisplay()->display['display_options']['filters'] ?? []; + // Gather the Views POST and GET upfront. + $views_post = $view->getRequest()->request->all(); + $views_get = $view->getRequest()->query->all(); - $filters = $view->getDisplay($display_id)->display['display_options']['filters'] ?? []; foreach ($filters as $filter) { - /* @var \Drupal\views\Plugin\views\ViewsHandlerInterface $filter */ - if ($filter['plugin_id'] == 'sbf_advanced_search_api_fulltext' + // Deeal with the RESET here + if (($views_get['op'] ?? '') === "Reset" || ($views_post['op'] ?? '') === "Reset" || $views_get['reset'] ?? FALSE) { + unset($views_post[$filter['expose']['identifier']]); + unset($views_get[$filter['expose']['identifier']]); + } + /* @var \Drupal\views\Plugin\views\ViewsHandlerInterface $filter */ + elseif ($filter['plugin_id'] == 'sbf_advanced_search_api_fulltext' && $filter['exposed'] == TRUE ) { if ($filter['expose']['identifier'] ?? NULL ) { @@ -195,9 +203,7 @@ public function ajaxView(Request $request) { // the whole GET bag as its own input based on the active request // We need to alter that. Let's check if we have the id in both GET and POST // If in POST, POST wins and replaces/needs to replace GET in the view - $views_post = $view->getRequest()->request->all(); unset($views_post['ajax_page_state']); - $views_get = $view->getRequest()->query->all(); unset($views_get['ajax_page_state']); if (isset($views_post[$filter['expose']['identifier']])) { foreach ($views_get as $get_args_keys => $value) { @@ -207,31 +213,34 @@ public function ajaxView(Request $request) { } } } - $view->getRequest()->query->replace($views_post + $views_get); } } - + $view->getRequest()->query->replace($views_post + $views_get); // Create a clone of the request object to avoid mutating the request // object stored in the request stack. $request_clone = clone $request; - // Add all POST data, because AJAX is sometimes a POST and many things, // such as tablesorts, exposed filters and paging assume GET. $param_union = $request_clone->request->all() + $request_clone->query->all(); + $used_query_parameters = $param_union; unset($param_union['ajax_page_state']); - $request_clone->query->replace($param_union); - + $request_clone->query->replace($used_query_parameters); $response->setView($view); // Overwrite the destination. // @see the redirect.destination service. - $origin_destination = $path; + $origin_destination = $path; + - $used_query_parameters = $param_union; $query = UrlHelper::buildQuery($used_query_parameters); if ($query != '') { - $origin_destination .= '?' . $query; - unset($used_query_parameters['op']); + if (!isset($used_query_parameters['reset']) && ($used_query_parameters['op'] ?? '') !== "Reset") { + unset($used_query_parameters['op']); + $origin_destination .= '?' . $query; + } + else { + $used_query_parameters = []; + } if ($target_url) { //Remove views%2Fajax from the URL set to the browser. makes no sense to allow that to be bookmarked. unset($used_query_parameters['/views/ajax']); @@ -245,9 +254,19 @@ public function ajaxView(Request $request) { $response->addCommand(new ScrollTopCommand(".js-view-dom-id-$dom_id")); $view->displayHandlers->get($display_id)->setOption('pager_element', $pager_element); } - // Reuse the same DOM id so it matches that in drupalSettings. + // Reuse the same DOM id, so it matches that in drupalSettings. $view->dom_id = $dom_id; + // Populate request attributes temporarily with ajax_page_state theme + // and theme_token for theme negotiation. + $theme_keys = [ + 'theme' => TRUE, + 'theme_token' => TRUE, + ]; + if (is_array($existing_page_state) && + ($temp_attributes = array_intersect_key($existing_page_state, $theme_keys))) { + $request->attributes->set('ajax_page_state', $temp_attributes); + } $context = new RenderContext(); $preview = $this->renderer->executeInRenderContext($context, function () use ($view, $display_id, $args) { @@ -259,12 +278,22 @@ public function ajaxView(Request $request) { ->merge($bubbleable_metadata) ->applyTo($preview); } + $request->attributes->remove('ajax_page_state'); $response->addCommand(new ReplaceCommand(".js-view-dom-id-$dom_id", $preview)); $response->addCommand(new PrependCommand(".js-view-dom-id-$dom_id", ['#type' => 'status_messages'])); //@TODO revisit in Drupal 10 //@See https://www.drupal.org/project/drupal/issues/343535 if ($target_url) { - $response->addCommand(new SbfSetBrowserUrl($target_url->toString())); + $seturl = TRUE; + $extenders = $view->display_handler->getExtenders(); + foreach ($extenders as $extender) { + if (($extender->getPluginId()== "sbf_ajax_interactions") && ($extender->options['sbf_ajax_dont_seturl'] ?? FALSE)) { + $seturl = FALSE; + } + } + if ($seturl) { + $response->addCommand(new SbfSetBrowserUrl($target_url->toString())); + } } // Views with ajax enabled aren't refreshing filters placed in blocks. @@ -284,7 +313,7 @@ public function ajaxView(Request $request) { } $response->addCommand(new ReplaceCommand("#views-exposed-form-" . $view_id, $this->renderer->render($exposed_form))); } - + $request->query->set('ajax_page_state', $existing_page_state); return $response; } else { @@ -295,12 +324,4 @@ public function ajaxView(Request $request) { throw new NotFoundHttpException(); } } - public function ajaxViewAdd(Request $request) - { - //$dom_id = "blabla"; - //$request->request->set('view_dom_id', $dom_id); - $response = $this->ajaxView($request); - return $response; - } - } diff --git a/modules/format_strawberryfield_views/src/Controller/ViewsExposedFormModalBlockAjaxController.php b/modules/format_strawberryfield_views/src/Controller/ViewsExposedFormModalBlockAjaxController.php index 190741d1..42a98898 100644 --- a/modules/format_strawberryfield_views/src/Controller/ViewsExposedFormModalBlockAjaxController.php +++ b/modules/format_strawberryfield_views/src/Controller/ViewsExposedFormModalBlockAjaxController.php @@ -127,6 +127,14 @@ public function ajaxExposedFormBlockView(Request $request) { $modalviews_blocks = array_unique($modalviews_blocks); $new_request = Request::create($path); + $new_request->setSession($request->getSession()); + // Add ajax_page_state to the new request if set. + if ($request->request->has('ajax_page_state')) { + $new_request->request->set('ajax_page_state', $request->request->all('ajax_page_state')); + } + elseif ($request->query->has('ajax_page_state')) { + $new_request->query->set('ajax_page_state', $request->query->all('ajax_page_state')); + } $request_stack = new \Symfony\Component\HttpFoundation\RequestStack; $processed = $this->pathProcessor->processInbound($path, $new_request); @@ -149,10 +157,13 @@ public function ajaxExposedFormBlockView(Request $request) { $block_view = $this->entityTypeManager ->getViewBuilder('block') ->view($block_entity); - $block_view = (string) $this->renderer->renderPlain($block_view); + $block_view = (string) $this->renderer->renderInIsolation($block_view); $response->addCommand(new ReplaceCommand('[data-drupal-modalblock-selector="js-modal-form-views-block-id-' .$block_id.'"]', $block_view)); } } + + + return $response; } } diff --git a/modules/format_strawberryfield_views/src/EventSubscriber/SearchApiSolrEventSubscriber.php b/modules/format_strawberryfield_views/src/EventSubscriber/SearchApiSolrEventSubscriber.php index 44e74b02..90c4b436 100644 --- a/modules/format_strawberryfield_views/src/EventSubscriber/SearchApiSolrEventSubscriber.php +++ b/modules/format_strawberryfield_views/src/EventSubscriber/SearchApiSolrEventSubscriber.php @@ -85,5 +85,60 @@ public function convertedQuery(PostConvertedQueryEvent $event): void { ); } } + if ($query->getOption('sbf_join_ado')) { + $options = $query->getOption('sbf_join_ado'); + if (is_array($options)) { + foreach ($options as $key => $option) { + if (is_array($option) && + !empty($option['from']) && + !empty($option['to']) && + !empty($option['v'])) { + // Options might contain more data used for HL/Advanced search etc. + // see \Drupal\format_strawberryfield_views\Plugin\views\filter\StrawberryFlavorsJoin::query + $conjunction = $option['#conjunction'] ?? 'OR'; // Used to connect the join with the main one + $join_term['from'] = $option['from']; + $join_term['to'] = $option['to']; + $subquery = $option['v']; + $join_term['v'] = '$subquery_ado_'.$key; + // $options['method'] = 'topLevelDV' or 'dvWithScore' This requires docvalues to be set on the joined fields + // This is desired because it will be faster + // Also adding $options['score'] = 'none' even faster. + // @TODO enable extra UUID type Solr field to be used as joined ones + $join = $solarium_query->getHelper()->qparser( + 'join', + $join_term + ); + $new_query_string = $solarium_query->getQuery() . " {$conjunction} " . $join; + $solarium_query->setQuery($new_query_string); + $solarium_query->addParam( + 'subquery_ado_'.$key, $subquery + ); + } + } + } + } + elseif ($query->getOption('sbf_join_ado_advanced')) { + $substitutions = $query->getOption('sbf_join_ado_advanced'); + if (is_array($substitutions) && + !empty($options['from']) && + !empty($options['to']) && + !empty($options['v'])) { + $subquery = $options['v']; + $options['v'] = '$subquery_ado'; + // $options['method'] = 'topLevelDV' or 'dvWithScore' This requires docvalues to be set on the joined fields + // This is desired because it will be faster + // Also adding $options['score'] = 'none' even faster. + // @TODO enable extra UUID type Solr field to be used as joined ones + $join = $solarium_query->getHelper()->qparser( + 'join', + $options + ); + $new_query_string = $solarium_query->getQuery() . ' OR ' . $join; + $solarium_query->setQuery($new_query_string); + $solarium_query->addParam( + 'subquery_ado', $subquery + ); + } + } } } diff --git a/modules/format_strawberryfield_views/src/Plugin/Block/ViewsExposedFilterBlockModal.php b/modules/format_strawberryfield_views/src/Plugin/Block/ViewsExposedFilterBlockModal.php index 78c7a845..286f2030 100644 --- a/modules/format_strawberryfield_views/src/Plugin/Block/ViewsExposedFilterBlockModal.php +++ b/modules/format_strawberryfield_views/src/Plugin/Block/ViewsExposedFilterBlockModal.php @@ -190,6 +190,13 @@ public function build() if ($this->view->display_handler->ajaxEnabled()) { $output['#attributes']['class'][] = 'block-modalformviews-ajax'; $output['#attributes']['class'][] = 'js-modal-form-views-block'; + // We know (thanks Core) that Resetting via Ajax breaks things + // So we will mark the reset button to have [data-drupal-selector=edit-reset] + // so we can ensure no Ajax binding will happen to that button. + if (isset($output['actions']['reset'])) { + $output['actions']['reset']['#attributes']['data-drupal-selector'] = 'edit-reset'; + } + $js_settings = [ 'view_id' => $this->view->id(), diff --git a/modules/format_strawberryfield_views/src/Plugin/views/display_extender/StrawberryAjaxInteractions.php b/modules/format_strawberryfield_views/src/Plugin/views/display_extender/StrawberryAjaxInteractions.php index d2e12f77..6e253f04 100644 --- a/modules/format_strawberryfield_views/src/Plugin/views/display_extender/StrawberryAjaxInteractions.php +++ b/modules/format_strawberryfield_views/src/Plugin/views/display_extender/StrawberryAjaxInteractions.php @@ -24,6 +24,18 @@ class StrawberryAjaxInteractions extends DisplayExtenderPluginBase { */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { if ($form_state->get('section') == 'use_ajax') { + $form['sbf_ajax_dont_seturl'] = [ + '#title' => $this->t('Do not set the Browser URL with arguments on Ajax driven Views.'), + '#type' => 'checkbox', + '#description' => $this->t('Archipelago will set on every Ajax View loaded the URL.If you have multiple Views on a single Page you might want to check this box to only have one driving the Bookmarkable URL.'), + '#default_value' => $this->options['sbf_ajax_dont_seturl'] ?? 0, + '#states' => [ + 'enabled' => [ + ':input[name="use_ajax"]' => ['checked' => TRUE], + ], + ], + ]; + $form['sbf_ajax_interactions'] = [ '#title' => $this->t('Strawberryfield ADO to ADO Interactions'), '#type' => 'checkbox', @@ -63,6 +75,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { public function submitOptionsForm(&$form, FormStateInterface $form_state) { if ($form_state->get('section') == 'use_ajax') { $this->options['sbf_ajax_interactions'] = $form_state->cleanValues()->getValue('sbf_ajax_interactions'); + $this->options['sbf_ajax_dont_seturl'] = $form_state->cleanValues()->getValue('sbf_ajax_dont_seturl'); if ($this->options['sbf_ajax_interactions']) { $this->options['sbf_ajax_interactions_arguments'] = $form_state->cleanValues()->getValue( @@ -99,6 +112,7 @@ public function validateOptionsForm(&$form, FormStateInterface $form_state) { // Prevent use ajax history when ajax for view are disabled. $form_state->setValue('sbf_ajax_interactions', FALSE); $form_state->setValue('sbf_ajax_interactions_arguments', NULL); + $form_state->setValue('sbf_ajax_dont_seturl', FALSE); } } /** diff --git a/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryADOJoin.php b/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryADOJoin.php new file mode 100644 index 00000000..d325c8fd --- /dev/null +++ b/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryADOJoin.php @@ -0,0 +1,460 @@ +setParseModeManager($container->get('plugin.manager.search_api.parse_mode')); + return $plugin; + } + + /** + * {@inheritdoc} + */ + public function defineOptions() { + $options = parent::defineOptions(); + + $options['operator']['default'] = 'or'; + $options['join_fields'] = ['default' => []]; + $options['join_field_to'] = ['default' => NULL]; + $options['ado_fields'] = ['default' => []]; + $options['negation_default'] = ['default' => ['omit']]; + $options['ado_type'] = ['default' => '']; + $options['ado_type_fields'] = ['default' => NULL]; + return $options; + } + + /** + * {@inheritdoc} + */ + public function defaultExposeOptions() { + parent::defaultExposeOptions(); + $this->options['expose']['ado_type'] = ['default' => '']; + } + + /** + * Retrieves the parse mode manager. + * + * @return \Drupal\search_api\ParseMode\ParseModePluginManager + * The parse mode manager. + */ + public function getParseModeManager() { + return $this->parseModeManager ?: \Drupal::service('plugin.manager.search_api.parse_mode'); + } + + /** + * Sets the parse mode manager. + * + * @param \Drupal\search_api\ParseMode\ParseModePluginManager $parse_mode_manager + * The new parse mode manager. + * + * @return $this + */ + public function setParseModeManager(ParseModePluginManager $parse_mode_manager) { + $this->parseModeManager = $parse_mode_manager; + return $this; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + $fields = $this->getADOFulltextFields() ?? []; + $join_fields = $this->getADONidFields() ?? []; + $type_fields = $this->getADOTypeFields() ?? []; + $form['ado_fields'] = [ + '#type' => 'select', + '#title' => $this->t('ADO(node) fields that need to match.'), + '#description' => $this->t('Select the fields that will be searched inside an ADO before Joining.'), + '#options' => $fields, + '#size' => min(4, count($fields)), + '#multiple' => TRUE, + '#default_value' => $this->options['ado_fields'], + '#required' => TRUE, + ]; + $form['join_field_to'] = [ + '#type' => 'select', + '#title' => $this->t('ADO field Holding the main Node ID to join against.'), + '#description' => $this->t('Select the fields that holds the ADOs Drupal NODE ID to be used to Join the results against. This fields need to be integers'), + '#options' => $join_fields, + '#multiple' => false, + '#default_value' => $this->options['join_field_to'], + '#required' => TRUE, + ]; + $form['join_fields'] = [ + '#type' => 'select', + '#title' => $this->t('ADO fields referencing a parent Node ID to be used for Joining (Join Fields).'), + '#description' => $this->t('Select the fields that reference Parent Nodes or ADOs to be used to Join the results against the main Node ID. This fields need to be integers'), + '#options' => $join_fields, + '#size' => min(4, count($join_fields)), + '#multiple' => TRUE, + '#default_value' => $this->options['join_fields'], + '#required' => TRUE, + ]; + $form['negation_default'] = [ + '#type' => 'select', + '#title' => $this->t('How/if at all to query Other ADOs when a negation is present.'), + '#description' => $this->t('Because the nature of many to one of ADOs (e.g CWS with Children) a + negation in the query string might still bring up some ADOs where that negation does not apply ending in ADOs/Nodes (because of the join) being added to the results that -in the + strict sense of something not being present in a CWS- might be misleading. This setting allows to decide what to do on a negation'), + '#options' => [ + 'omit' => $this->t('Do not join ADOs in the presence of a negation'), + 'include' => $this->t('Join ADOs in the presence of a negation even if that brings more results back'), + ], + '#default_value' => $this->options['negation_default'], + '#required' => TRUE, + ]; + $form['ado_type'] = [ + '#type' => 'textfield', + '#title' => $this->t('Comma separated list of ADO "type" to join.'), + '#description' => $this->t('If empty all ADOs that match/have a child to parent relation (based on the Join Fields) will be searched. You can limit that by e.g adding here Photograph to restrict it to those ADOs of type Photograph'), + '#default_value' => $this->options['ado_type'], + '#required' => FALSE, + ]; + $form['ado_type_fields'] = [ + '#type' => 'select', + '#title' => $this->t('ADO field Containing the ADO Type value.'), + '#description' => $this->t('Select the field that holds the value for the "type" json key. Will have no effect if no value was entered for ADO Type in the previous form entry.'), + '#options' => $type_fields, + '#multiple' => FALSE, + '#default_value' => $this->options['ado_type_fields'], + '#required' => TRUE, + ]; + } + + + public function query() { + $query = $this->getQuery(); + $backend = $query->getIndex()->getServerInstance()->getBackend(); + $full_text = NULL; + $type = NULL; + // We only know how to join on Solr. All rest is terribly bad poetry + if ($backend instanceof \Drupal\search_api_solr\SolrBackendInterface) { + $index_fields = $query->getIndex()->getFields(TRUE); + $solr_field_names = $backend + ->getSolrFieldNames($query->getIndex()); + + + /* @var \Drupal\views\Plugin\views\display\DisplayPluginBase[] $filters */ + $filters = $this->view->getHandlers('filter', NULL); + $value = ""; + foreach ($filters as $filter_name => $filter) { + if ($filter['plugin_id'] == 'search_api_fulltext') { + // Reuse the Full Text Field's parse mode + $parsemode = $filter['parse_mode'] ?? 'terms' ; + /** @var \Drupal\search_api\ParseMode\ParseModeInterface $parse_mode */ + $parse_mode = $this->getParseModeManager() + ->createInstance($parsemode); + + if (!$filter['exposed']) { + $op = $filter['operator']; + } + $full_text = $query->getKeys(); + $type = 'fulltext'; + } + elseif ($filter['plugin_id'] == 'sbf_advanced_search_api_fulltext') { + $full_text = $query->getKeys(); + $type = 'advanced_fulltext'; + } + if ($this->options['id'] == $filter_name) { + // This is myself, break out. + break; + } + } + // If value == "" do nothing, no need to JOIN SBF for that. + if ($type == 'fulltext' && $full_text && ((is_scalar($full_text) && strlen($full_text) > 0) || (is_array($full_text) && count($full_text) > 0 ))) { + $join_structure = []; + // Never ever make it easy Solr! + // This is the Join Subquery, sadly not useful "directly" for Flavor Highlights IF the conjunction is AND + // Because the AND implies matches across the union/intersection of main query and the Join + // but OCR might only contain a few of these. the Idea is that the combination of all keys + searched against fields match + // at the end the total. + $subquery = $this->buildADOJoinSubQuery($query, $parse_mode, $this->options['ado_fields'], $full_text); + // check the conjunctions, remove the #negations, change the ANDs to ORs. + // processor_id + $negation = FALSE; + if (strlen($subquery) > 0 ) { + // only if we have a subquery + foreach ($full_text as $key => &$entry) { + if (is_array($entry)) { + if ($entry['#negation'] ?? FALSE) { + unset($full_text[$key]); + $negation = TRUE; + } + elseif (($entry['#conjunction'] ?? FALSE) == 'AND') { + // Make it OR. could move the search term up but that would add + // an extra foreach loop. I'm cheap. + $entry['#conjunction'] = 'OR'; + } + } + elseif ($key == "#conjunction" && $entry == 'AND') { + $entry = 'OR'; + } + } + if ($this->options['negation_default'] == 'omit' && $negation) { + // In case of negation AND decision to return on negation return; + return; + } + $subquery_hl = $this->buildADOJoinSubQuery($query, $parse_mode, $this->options['ado_fields'], $full_text); + foreach ($this->options['join_fields'] as $join_field) { + if (isset($solr_field_names[$join_field])) { + $join_structure[] = [ + 'from' => $solr_field_names[$join_field], + 'to' => $solr_field_names[$this->options['join_field_to'] ?? 'its_nid'], + 'v' => $subquery, + 'hl' => $subquery_hl, + ]; + } + } + // 'hl' might be used in the future at @TODO. Right HL is limited to Strawberry Flavors. + // \Drupal\strawberryfield\Plugin\search_api\processor\StrawberryFieldHighlight::highlightFlavorsFromIndex + if (!empty($join_structure)) { + $this->getQuery()->setOption('sbf_join_ado', $join_structure); + } + } + } + elseif ($type == 'advanced_fulltext') { + if ($subtitutions = $this->getQuery()->getOption('sbf_advanced_search_filter_join', NULL)) { + foreach ($subtitutions as $placeholder => $subquery) { + $join_structure[$placeholder] = [ + 'from' => 'its_parent_id', + 'to' => 'its_nid', + 'v' => $subquery + ]; + } + $this->getQuery()->setOption('sbf_join_ado_advanced', $join_structure); + } + } + } + } + + /** + * @param \Drupal\search_api\Plugin\views\query\SearchApiQuery $query + * + * @throws \Drupal\search_api\SearchApiException + */ + protected function buildADOJoinSubQuery(SearchApiQuery $query, $parse_mode, array $queryable_fields, array|string $keys) { + $solr_field_names = $query->getIndex() + ->getServerInstance() + ->getBackend() + ->getSolrFieldNames($query->getIndex()); + // Damn Solr Search API... + + $index_fields = $query->getIndex()->getFields(TRUE); + + $settings = Utility::getIndexSolrSettings($query->getIndex()); + $language_ids = $query->getLanguages(); + $flat_keys = []; + // If there are no languages set, we need to set them. As an example, a + // language might be set by a filter in a search view. + if (empty($language_ids)) { + if (!$query->getSearchApiQuery()->hasTag('views') && $settings['multilingual']['limit_to_content_language']) { + // Limit the language to the current language being used. + $language_ids[] = \Drupal::languageManager() + ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT) + ->getId(); + } + else { + // If the query is generated by views and/or the query isn't limited + // by any languages we have to search for all languages using their + // specific fields. + $language_ids = array_keys(\Drupal::languageManager()->getLanguages()); + } + } + + if ($settings['multilingual']['include_language_independent']) { + $language_ids[] = LanguageInterface::LANGCODE_NOT_SPECIFIED; + } + + $field_names = $query->getIndex() + ->getServerInstance() + ->getBackend()->getSolrFieldNamesKeyedByLanguage($language_ids, $query->getIndex()); + $all_names = []; + foreach (($queryable_fields ?? []) as $field) { + if (isset($solr_field_names[$field]) + && 'twm_suggest' !== $solr_field_names[$field] & strpos( + $solr_field_names[$field], 'spellcheck' + ) !== 0 + ) { + $index_field = $index_fields[$field]; + $boost = $index_field->getBoost() ? '^' . $index_field->getBoost() + : ''; + $names = []; + $first_name = reset($field_names[$field]); + if (strpos($first_name, 't') === 0) { + // Add all language-specific field names. This should work for + // non Drupal Solr Documents as well which contain only a single + // name. + $names = array_values($field_names[$field]); + } + else { + $names[] = $first_name; + } + $names = array_unique($names); + foreach ($names as &$name) { + $name = $name . $boost; + } + $all_names = array_merge($all_names, $names); + } + } + $all_names = array_unique($all_names); + $names = $all_names; + $type_query = NULL; + if (count($names)) { + + // If we have $names, now calculate the AND query for ADO type if any + if ($this->options['ado_type_fields']) { + $ado_type_names_solr = []; + $types = explode(",", trim($this->options['ado_type'] ?? '')); + if (count($types)) { + foreach($types as &$type) { + $type = trim($type ?? ''); + } + $types = array_filter($types); + if (count($types)) { + $ado_type_keys = ['#conjunction' => "OR" ] + $types; + + if (isset($solr_field_names[$this->options['ado_type_fields']])) { + $ado_type_names_solr[] = $solr_field_names[$this->options['ado_type_fields']]; + } + } + if (count($ado_type_names_solr)) { + // Here we force terms. WE are trying to mimic a filter query even if we can't do one via a JOIN + // (or I can't!) + $flat_keys_type[] = \Drupal\search_api_solr\Utility\Utility::flattenKeys( + $ado_type_keys, $ado_type_names_solr, + 'terms' + ); + $type_query = implode(" ", $flat_keys_type); + $type_query = 'AND (' . $type_query . ')'; + } + } + } + + $flat_keys[] = \Drupal\search_api_solr\Utility\Utility::flattenKeys( + $keys, $names, + $parse_mode->getPluginId() + ); + if ($type_query) { + $flat_keys[] = $type_query; + } + } + return implode(" ", $flat_keys); + } + /** + * Retrieves a list of all available fulltext fields. + * + * @return string[] + * An options list of fulltext field identifiers mapped to their prefixed + * labels. + */ + protected function getADOFulltextFields() { + $fields = []; + /** @var \Drupal\search_api\IndexInterface $index */ + $index = Index::load(substr($this->table, 17)); + + $fields_info = $index->getFields(); + foreach ($index->getFulltextFields() as $field_id) { + if ($fields_info[$field_id]->getDatasourceId() == 'entity:node' || $fields_info[$field_id]->getDatasourceId() == NULL) { + $fields[$field_id] = $fields_info[$field_id]->getPrefixedLabel() . '('. $fields_info[$field_id]->getFieldIdentifier() .')'; + } + } + + return $fields; + } + + + /** + * Retrieves a list of all available fulltext fields. + * + * @return string[] + * An options list of fulltext field identifiers mapped to their prefixed + * labels. + */ + protected function getADONidFields() { + $fields = []; + /** @var \Drupal\search_api\IndexInterface $index */ + $index = Index::load(substr($this->table, 17)); + + $fields_info = $index->getFields(); + + // We will store the actual Solr Field name here. Has the benefit of being faster + // Has the downside that if someone changes the field name type (same field) this will break? + + foreach ($fields_info as $field_id => $field) { + if (($field->getDatasourceId() == 'entity:node') && ($field->getType() == "integer")) { + $property_path = $field->getPropertyPath(); + $property_path_parts = explode(":", $property_path ?? ''); + // Very hardcoded too. + if (end($property_path_parts) == "nid" || str_contains(end($property_path_parts), 'sbf_entity_reference_')) { + $fields[$field_id] = $field->getPrefixedLabel() . '(' + . $field->getFieldIdentifier() . ')'; + } + } + } + return $fields; + } + + /** + * Retrieves a list of all available fulltext fields. + * + * @return string[] + * An options list of fulltext field identifiers mapped to their prefixed + * labels. + */ + protected function getADOTypeFields() { + $fields = []; + /** @var \Drupal\search_api\IndexInterface $index */ + $index = Index::load(substr($this->table, 17)); + + $fields_info = $index->getFields(); + foreach ($fields_info as $field_id => $field) { + if (($field->getDatasourceId() == 'entity:node') && ($field->getType() == "string")) { + $property_path = $field->getPropertyPath(); + $property_path_parts = explode(":", $property_path ?? ''); + // This is kinda fixed ... we might just get all strings? + if (end($property_path_parts) == "type" || end($property_path_parts) == "digital_object_type" || str_contains($field->getFieldIdentifier(), 'type')) { + $fields[$field_id] = $field->getPrefixedLabel() . '(' + . $field->getFieldIdentifier() . ')'; + } + } + } + return $fields; + } + +} diff --git a/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryFlavorsJoin.php b/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryFlavorsJoin.php index 58b0a06f..3e28579b 100644 --- a/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryFlavorsJoin.php +++ b/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryFlavorsJoin.php @@ -119,7 +119,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { '#title' => $this->t('How/if at all to query Flavors when a negation is present.'), '#description' => $this->t('Because the nature of many to one of Strawberry Flavors (e.g many pages of a book) a negation in the query string might still bring up some pages where that negation does not apply ending in ADOs/Nodes (because of the join) being added to the results that -in the - strict sense of something not being present in a book- might be missleading. This setting allows to decide what to do on a negation'), + strict sense of something not being present in a book- might be misleading. This setting allows to decide what to do on a negation'), '#options' => [ 'omit' => $this->t('Do not join Flavors in the presence of a negation'), 'include' => $this->t('Join Flavors in the presence of a negation even if that brings more results back'), diff --git a/src/Form/MetadataDisplayForm.php b/src/Form/MetadataDisplayForm.php index 84511c67..59a0089a 100755 --- a/src/Form/MetadataDisplayForm.php +++ b/src/Form/MetadataDisplayForm.php @@ -147,7 +147,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { '#template' => $form_state->getValue('twig')[0]['value'], '#context' => ['data' => []], ]; - $this->renderer->renderPlain($build); + $this->renderer->renderInIsolation($build); } } catch (\Exception $exception) {