diff --git a/format_strawberryfield.install b/format_strawberryfield.install index 21fd2821..b4b17145 100644 --- a/format_strawberryfield.install +++ b/format_strawberryfield.install @@ -39,7 +39,7 @@ function format_strawberryfield_update_8001() { 'application/json' => 'JSON', 'application/ld+json' => 'JSON-LD', 'application/xml' => 'XML', - 'text/text' => 'TEXT', + 'text/plain' => 'TEXT', 'text/turtle' => 'RDF/TURTLE', 'text/csv' => 'CSV', ], @@ -361,4 +361,24 @@ function format_strawberryfield_update_9001() { else { return 'Metadata API Configuration Entity already exists'; } -} \ No newline at end of file +} + + +/** + * Implements hook_update_N(). + * + * Updates Mimetypes for text/plain + * + */ +function format_strawberryfield_update_9002() { + $definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + $field_storage_definition = $definition_update_manager->getFieldStorageDefinition('mimetype', 'metadatadisplay_entity'); + $settings = $field_storage_definition->getSettings(); + $settings['allowed_values'] = \Drupal\format_strawberryfield\Entity\MetadataDisplayEntity::ALLOWED_MIMETYPES; + $field_storage_definition->setSettings($settings); + $definition_update_manager->updateFieldStorageDefinition($field_storage_definition); +} + + + + diff --git a/format_strawberryfield.libraries.yml b/format_strawberryfield.libraries.yml index a3145ef1..9f1e1098 100644 --- a/format_strawberryfield.libraries.yml +++ b/format_strawberryfield.libraries.yml @@ -6,6 +6,7 @@ iiif_formatstrawberryfield_utils: dependencies: - core/jquery - core/drupal + - core/once - core/drupalSettings - format_strawberryfield/jmespath_strawberry - format_strawberryfield/svgpath_polyfull diff --git a/format_strawberryfield.services.yml b/format_strawberryfield.services.yml index a5d79145..d8137777 100644 --- a/format_strawberryfield.services.yml +++ b/format_strawberryfield.services.yml @@ -3,7 +3,7 @@ services: class: Drupal\format_strawberryfield\TwigExtension tags: - {name: twig.extension} - arguments: ['@renderer', '@plugin.manager.search_api.parse_mode'] + arguments: ['@renderer', '@plugin.manager.search_api.parse_mode', '@request_stack', '@kernel'] format_strawberryfield.deletetmpstorage_subscriber: class: Drupal\format_strawberryfield\EventSubscriber\formatStrawberryfieldDeleteTmpStorage tags: diff --git a/js/iiif-archipelago-interactions_utils.js b/js/iiif-archipelago-interactions_utils.js index efe09d05..5192101b 100644 --- a/js/iiif-archipelago-interactions_utils.js +++ b/js/iiif-archipelago-interactions_utils.js @@ -1,4 +1,4 @@ -(function ($, Drupal, drupalSettings, jmespath) { +(function ($, Drupal, drupalSettings, jmespath, once) { 'use strict'; var FormatStrawberryfieldIiifUtils = { @@ -44,7 +44,23 @@ nodeidsArray = nodeids } const event = new CustomEvent('sbf:ado:view:change', { bubbles: true, detail: {nodeid: nodeidsArray} }); - el.dispatchEvent(event); + // A view might/might not have yet attach itself to listen. + // We have no control on Behavior attachment order in Drupal + // And can also not make this library depend at all on sbf-views-ajax-interactions. + // But we can check if it is already attached but just using once() + // and if not, delay with a future timeout the dispatching in the hope it finds its way. + // Literally give it a second after sync code has run. + // See Drupal.behaviors.sbf_views_ajax_interactions + const viewEventListenerInit = once.filter('listen-ado-view-change', 'body') + if (!viewEventListenerInit?.length) { + setTimeout(() => { + el.dispatchEvent(event); + } + , 1000); + } + else { + el.dispatchEvent(event); + } return this; }, @@ -61,7 +77,16 @@ encodedImageAnnotationOne = encodedImageAnnotation } const event = new CustomEvent('sbf:ado:view:change', { bubbles: true, detail: {image_annotation: encodedImageAnnotationOne} }); - el.dispatchEvent(event); + const viewEventListenerInit = once.filter('listen-ado-view-change', 'body') + if (!viewEventListenerInit?.length) { + setTimeout(() => { + el.dispatchEvent(event); + } + , 1000); + } + else { + el.dispatchEvent(event); + } return this; }, @@ -262,4 +287,4 @@ /* Make it part of the Global Drupal Object */ Drupal.FormatStrawberryfieldIiifUtils = FormatStrawberryfieldIiifUtils; -})(jQuery, Drupal, drupalSettings, jmespath); +})(jQuery, Drupal, drupalSettings, jmespath, once); diff --git a/js/mirador_strawberry.js b/js/mirador_strawberry.js index eb5e7ce6..ed508992 100644 --- a/js/mirador_strawberry.js +++ b/js/mirador_strawberry.js @@ -194,7 +194,22 @@ } } - const newParams = Object.fromEntries(new URLSearchParams(location.search)) + const newParams = {}; + const urlArray = location.hash.replace('#','').split('/'); + const urlHash = {}; + // Because one action might not have the value for another action + // We will parse the params upfront from the hash. + for (let i = 0; i < urlArray.length; i += 2) { + urlHash[urlArray[i]] = urlArray[i + 1]; + } + if (urlHash['search'] != undefined) { + newParams.search = decodeURIComponent(urlHash['search'].replace(/\+/g, " ")); + } + if (urlHash['page'] != undefined) { + newParams.page = decodeURIComponent(urlHash['page'].replace(/\+/g, " ")); + } + + if ( action.type === ActionTypes.SET_CANVAS || action.type === ActionTypes.SET_WINDOW_VIEW_TYPE @@ -280,6 +295,14 @@ canvas => canvas['@id'] ); } + // Build page parameter + if (visibleCanvases?.length) { + const canvasIndices = visibleCanvases.map(c => canvasIds.indexOf(c) + 1) + if (canvasIndices.length == 1) { + newParams.page = canvasIndices[0] + } + } + // even if we have no search being triggered by interactions, we should fetch the // 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. @@ -298,33 +321,21 @@ 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}` - ); } - - - + // Set the fragment, no matter what. + 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( @@ -421,10 +432,31 @@ } }; + const readFragmentPage = 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['page'] != undefined) { + return parseInt(decodeURIComponent(urlHash['page'].replace(/\+/g, " "))); + } + else { + return 0; + } + }; + const search_string = readFragmentSearch(); + const page_string = readFragmentPage(); if (search_string.length > 0 ) { $options.windows[0].defaultSearchQuery = search_string; } + // Note. if the setting "switchCanvasOnSearch": true is selected + // And there is a search + // And there is a hit, start canvas will have no effect. You are warned. + if (parseInt(page_string) > 0 ) { + $options.windows[0].canvasIndex = parseInt(page_string) - 1 ; + } // 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 @@ -456,8 +488,7 @@ ...viewer_override, }; } - - + $options.state = {}; //@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) { diff --git a/modules/format_strawberryfield_facets/format_strawberryfield_facets.module b/modules/format_strawberryfield_facets/format_strawberryfield_facets.module new file mode 100644 index 00000000..e69de29b 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 796cd77a..487e8c1c 100644 --- a/modules/format_strawberryfield_views/js/sbf-views-ajax-interactions.js +++ b/modules/format_strawberryfield_views/js/sbf-views-ajax-interactions.js @@ -53,7 +53,7 @@ Drupal.behaviors.sbf_views_ajax_interactions = { attach: function (context, settings) { - once('listen-ado-view-change', 'div.view', context).forEach(function (value, index) { + once('listen-ado-view-change', 'body').forEach(function (value, index) { console.log("initializing 'sbf:ado:view:change' event listener on ADO changes"); // Because this is a single Listener for all views that have this enabled // the actual caller id will be passed as part of the event data diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index dfd8b07a..fd92f96e 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -702,7 +702,7 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity break; case 'application/xml': - case 'text/text': + case 'text/plain': case 'text/turtle': case 'text/html': case 'text/csv': @@ -759,7 +759,7 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity break; case 'application/xml': - case 'text/text': + case 'text/plain': case 'text/turtle': case 'text/html': case 'text/csv': diff --git a/src/Controller/MetadataExposeDisplayController.php b/src/Controller/MetadataExposeDisplayController.php index 8d2d8e49..e7ef6caf 100644 --- a/src/Controller/MetadataExposeDisplayController.php +++ b/src/Controller/MetadataExposeDisplayController.php @@ -297,7 +297,7 @@ function () use ($context, $original_context, $metadatadisplay_entity) { break; case 'application/xml': - case 'text/text': + case 'text/plain': case 'text/turtle': case 'text/html': case 'text/csv': diff --git a/src/Entity/MetadataDisplayEntity.php b/src/Entity/MetadataDisplayEntity.php index de628033..8396026d 100644 --- a/src/Entity/MetadataDisplayEntity.php +++ b/src/Entity/MetadataDisplayEntity.php @@ -164,6 +164,16 @@ class MetadataDisplayEntity extends RevisionableContentEntityBase implements Met CONST ERRORED_CACHE_TAG_ID = 'format_strawberry:metadata_display_errored:'; + CONST ALLOWED_MIMETYPES = [ + 'text/html' => 'HTML', + 'application/json' => 'JSON', + 'application/ld+json' => 'JSON-LD', + 'application/xml' => 'XML', + 'text/plain' => 'TEXT', + 'text/turtle' => 'RDF/TURTLE', + 'text/csv' => 'CSV', + ]; + /** * Calculated Twig vars used by this template. * @@ -397,15 +407,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { 'default_value' => 'text/html', 'max_length' => 64, 'cardinality' => 1, - 'allowed_values' => [ - 'text/html' => 'HTML', - 'application/json' => 'JSON', - 'application/ld+json' => 'JSON-LD', - 'application/xml' => 'XML', - 'text/text' => 'TEXT', - 'text/turtle' => 'RDF/TURTLE', - 'text/csv' => 'CSV', - ], + 'allowed_values' => static::ALLOWED_MIMETYPES, ]) ->setRequired(TRUE) ->setDisplayOptions('view', [ diff --git a/src/TwigExtension.php b/src/TwigExtension.php index c53243ac..206c90db 100644 --- a/src/TwigExtension.php +++ b/src/TwigExtension.php @@ -3,10 +3,18 @@ namespace Drupal\format_strawberryfield; use Drupal\Component\Utility\Html; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\file\FileInterface; use Drupal\search_api\Query\QueryInterface; +use Drupal\format_strawberryfield\Entity\MetadataDisplayEntity; use Drupal\search_api\SearchApiException; use Drupal\strawberryfield\Plugin\search_api\datasource\StrawberryfieldFlavorDatasource; +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Twig\Extension\AbstractExtension; +use Drupal\Core\Url; use Twig\Markup; use Twig\TwigTest; use Twig\TwigFilter; @@ -38,17 +46,33 @@ class TwigExtension extends AbstractExtension { */ protected $renderer; + /** + * The request stack. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ + protected $requestStack; + + /** + * @var HttpKernelInterface + */ + protected HttpKernelInterface $httpKernel; + /** * Constructs \Drupal\format_strawberryfield\TwigExtension * - * @param \Drupal\Core\Render\RendererInterface $renderer + * @param RendererInterface $renderer * The renderer. - * @param \Drupal\search_api\ParseMode\ParseModePluginManager $parse_mode_manager + * @param ParseModePluginManager $parse_mode_manager * The search API parse mode manager. + * @param RequestStack $request_stack + * @param HttpKernelInterface $http_kernel */ - public function __construct(RendererInterface $renderer, ParseModePluginManager $parse_mode_manager) { + public function __construct(RendererInterface $renderer, ParseModePluginManager $parse_mode_manager, RequestStack $request_stack, HttpKernelInterface $http_kernel) { $this->renderer = $renderer; $this->parseModeManager = $parse_mode_manager; + $this->requestStack = $request_stack; + $this->httpKernel = $http_kernel; } public function getTests(): array { @@ -80,6 +104,7 @@ public function getFunctions() { [$this, 'clipboardCopy']), new TwigFunction('sbf_search_api', [$this, 'searchApiQuery']), + new TwigFunction('sbf_file_content', [$this, 'sbfFileContent'], ['is_safe' => ['all']]), ]; } @@ -121,7 +146,7 @@ public function entityIdsByLabel( string $label, string $entity_type, string $bundle_identifier = NULL, - $limit = 1 + $limit = 1 ): ?array { $fields = [ 'node' => ['title', 'type'], @@ -583,4 +608,39 @@ public function searchApiQuery(string $index, string $term, array $fulltext, arr } return []; } + + public function sbfFileContent(string $node_uuid, string $file_uuid, string $format) { + try { + /** @var ContentEntityInterface[] $nodes */ + $nodes = \Drupal::entityTypeManager() + ->getStorage('node') + ->loadByProperties(['uuid' => $node_uuid]); + $node = reset($nodes); + + if ($node && $node->access('view') && $node->hasField('field_file_drop')) { + /** @var FileInterface[] $files */ + $files = \Drupal::entityTypeManager() + ->getStorage('file') + ->loadByProperties(['uuid' => $file_uuid]); + $file = reset($files); + $found = FALSE; + $files_referenced = $node->get('field_file_drop')->getValue(); + foreach ($files_referenced as $fileinfo) { + if ($fileinfo['target_id'] == $file->id()) { + /* @var $found \Drupal\file\Entity\File */ + $found = $file; + break; + } + } + if($found && isset(MetadataDisplayEntity::ALLOWED_MIMETYPES[$file->getMimeType()])) { + return file_get_contents($file->getFileUri()); + } + } + } + catch (\Exception $exception) { + return ''; + } + + return ''; + } }