diff --git a/.editorconfig b/.editorconfig index 9aa5d7c3b..467bbfea3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,7 +14,7 @@ trim_trailing_whitespace = true # TS/JS-Files [*.{ts,js}] -indent_size = 2 +indent_size = 4 # JSON-Files [*.json] diff --git a/.github/phpstan.neon b/.github/phpstan.neon index 86b7418e6..1e3f33b64 100644 --- a/.github/phpstan.neon +++ b/.github/phpstan.neon @@ -14,6 +14,8 @@ parameters: - '#Call to an undefined method Kitodo\\Dlf\\Domain\\Repository\\[a-zA-Z]+Repository::findOneByUid\(\)\.#' - '#Call to an undefined method Psr\\Http\\Message\\RequestFactoryInterface::request\(\)\.#' - '#Call to an undefined method Solarium\\Core\\Query\\DocumentInterface::setField\(\)\.#' + - '#Call to an undefined method Ubl\\Iiif\\Presentation\\Common\\Model\\Resources\\CanvasInterface::getImages\(\)\.#' + - '#Call to an undefined method Ubl\\Iiif\\Presentation\\Common\\Model\\Resources\\CanvasInterface::getOtherContent\(\)\.#' - '#Call to an undefined method Ubl\\Iiif\\Presentation\\Common\\Model\\Resources\\IiifResourceInterface::getHeight\(\)\.#' - '#Call to an undefined method Ubl\\Iiif\\Presentation\\Common\\Model\\Resources\\IiifResourceInterface::getWidth\(\)\.#' - '#Call to an undefined method Ubl\\Iiif\\Presentation\\Common\\Model\\Resources\\IiifResourceInterface::getPossibleTextAnnotationContainers\(\)\.#' diff --git a/Classes/Command/BaseCommand.php b/Classes/Command/BaseCommand.php index b9e5dc9e3..8b1548bf2 100644 --- a/Classes/Command/BaseCommand.php +++ b/Classes/Command/BaseCommand.php @@ -292,7 +292,7 @@ protected function getParentDocumentUidForSaving(Document $document, bool $softC if ($doc !== null && !empty($doc->parentHref)) { // find document object by record_id of parent - $parent = AbstractDocument::getInstance($doc->parentHref, ['storagePid' => $this->storagePid]); + $parent = AbstractDocument::getInstance($doc->parentHref, 0, ['storagePid' => $this->storagePid]); if ($parent->recordId) { $parentDocument = $this->documentRepository->findOneByRecordId($parent->recordId); diff --git a/Classes/Command/DeleteCommand.php b/Classes/Command/DeleteCommand.php index 7022b83bb..fc869f2c4 100644 --- a/Classes/Command/DeleteCommand.php +++ b/Classes/Command/DeleteCommand.php @@ -217,7 +217,7 @@ private function getDocument($input): ?Document if (MathUtility::canBeInterpretedAsInteger($input->getOption('doc'))) { $document = $this->documentRepository->findByUid($input->getOption('doc')); } elseif (GeneralUtility::isValidUrl($input->getOption('doc'))) { - $doc = AbstractDocument::getInstance($input->getOption('doc'), ['storagePid' => $this->storagePid], true); + $doc = AbstractDocument::getInstance($input->getOption('doc'), 0, ['storagePid' => $this->storagePid], true); if ($doc->recordId) { $document = $this->documentRepository->findOneByRecordId($doc->recordId); diff --git a/Classes/Command/HarvestCommand.php b/Classes/Command/HarvestCommand.php index c2681d9cb..d24f22dc7 100644 --- a/Classes/Command/HarvestCommand.php +++ b/Classes/Command/HarvestCommand.php @@ -232,7 +232,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $docLocation = $baseLocation . http_build_query($params); // ...index the document... $document = null; - $doc = AbstractDocument::getInstance($docLocation, ['storagePid' => $this->storagePid], true); + $doc = AbstractDocument::getInstance($docLocation, 0, ['storagePid' => $this->storagePid], true); if ($doc === null) { $io->warning('WARNING: Document "' . $docLocation . '" could not be loaded. Skip to next document.'); diff --git a/Classes/Command/IndexCommand.php b/Classes/Command/IndexCommand.php index 33f4d21ab..d74a9cbd9 100644 --- a/Classes/Command/IndexCommand.php +++ b/Classes/Command/IndexCommand.php @@ -168,11 +168,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->error('ERROR: Document with UID "' . $input->getOption('doc') . '" could not be found on PID ' . $this->storagePid . ' .'); return BaseCommand::FAILURE; } else { - $doc = AbstractDocument::getInstance($document->getLocation(), ['storagePid' => $this->storagePid], true); + $doc = AbstractDocument::getInstance($document->getLocation(), 0, ['storagePid' => $this->storagePid], true); } } else if (GeneralUtility::isValidUrl($input->getOption('doc'))) { - $doc = AbstractDocument::getInstance($input->getOption('doc'), ['storagePid' => $this->storagePid], true); + $doc = AbstractDocument::getInstance($input->getOption('doc'), 0, ['storagePid' => $this->storagePid], true); $document = $this->getDocumentFromUrl($doc, $input->getOption('doc')); } diff --git a/Classes/Command/ReindexCommand.php b/Classes/Command/ReindexCommand.php index ac04af65f..d43cfb5eb 100644 --- a/Classes/Command/ReindexCommand.php +++ b/Classes/Command/ReindexCommand.php @@ -204,7 +204,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } foreach ($documents as $id => $document) { - $doc = AbstractDocument::getInstance($document->getLocation(), ['storagePid' => $this->storagePid], true); + $doc = AbstractDocument::getInstance($document->getLocation(), 0, ['storagePid' => $this->storagePid], true); if ($doc === null) { $io->warning('WARNING: Document "' . $document->getLocation() . '" could not be loaded. Skip to next document.'); diff --git a/Classes/Common/AbstractDocument.php b/Classes/Common/AbstractDocument.php index 42901fc8e..4b087fb71 100644 --- a/Classes/Common/AbstractDocument.php +++ b/Classes/Common/AbstractDocument.php @@ -71,6 +71,12 @@ abstract class AbstractDocument */ protected int $cPid = 0; + /** + * @access protected + * @var int This holds the page ID for the requests + */ + protected int $pageId = 0; + /** * @access public * @static @@ -515,6 +521,27 @@ abstract protected function prepareMetadataArray(int $cPid): void; */ abstract protected function setPreloadedDocument($preloadedDocument): bool; + /** + * Get information about all files contained in the document, or null if this information is not available. + * + * Returns an associative array of the following form: + * + * ```php + * [ + * '#FILE_ID' => [ + * 'url' => '...', + * 'mimetype' => '...', + * ], + * // ... + * ] + * ``` + * + * @access public + * + * @return array + */ + abstract public function getAllFiles(): array; + /** * This is a singleton class, thus an instance must be created by this method * @@ -523,12 +550,13 @@ abstract protected function setPreloadedDocument($preloadedDocument): bool; * @static * * @param string $location The URL of XML file or the IRI of the IIIF resource + * @param int $pageId * @param array $settings * @param bool $forceReload Force reloading the document instead of returning the cached instance * * @return AbstractDocument|null Instance of this class, either MetsDocument or IiifManifest */ - public static function &getInstance(string $location, array $settings = [], bool $forceReload = false) + public static function &getInstance(string $location, int $pageId = 0, array $settings = [], bool $forceReload = false) { // Create new instance depending on format (METS or IIIF) ... $documentFormat = null; @@ -576,14 +604,14 @@ public static function &getInstance(string $location, array $settings = [], bool // Sanitize input. $pid = array_key_exists('storagePid', $settings) ? max((int) $settings['storagePid'], 0) : 0; if ($documentFormat == 'METS') { - $instance = new MetsDocument($pid, $location, $xml, $settings); + $instance = new MetsDocument($pid, $location, $pageId, $xml, $settings); } elseif ($documentFormat == 'IIIF') { // TODO: Parameter $preloadedDocument of class Kitodo\Dlf\Common\IiifManifest constructor expects SimpleXMLElement|Ubl\Iiif\Presentation\Common\Model\Resources\IiifResourceInterface, Ubl\Iiif\Presentation\Common\Model\AbstractIiifEntity|null given. // @phpstan-ignore-next-line - $instance = new IiifManifest($pid, $location, $iiif); + $instance = new IiifManifest($pid, $location, $pageId, $iiif); } - if ($instance !== null) { + if ($instance) { self::setDocumentCache($location, $instance); } @@ -1140,7 +1168,7 @@ protected function magicGetRootId(): int if ($this->parentId) { // TODO: Parameter $location of static method AbstractDocument::getInstance() expects string, int|int<1, max> given. // @phpstan-ignore-next-line - $parent = self::getInstance($this->parentId, ['storagePid' => $this->pid]); + $parent = self::getInstance($this->parentId, $this->pageId, ['storagePid' => $this->pid]); $this->rootId = $parent->rootId; } $this->rootIdLoaded = true; @@ -1188,14 +1216,16 @@ protected function _setCPid(int $value): void * * @param int $pid If > 0, then only document with this PID gets loaded * @param string $location The location URL of the XML file to parse + * @param int $pageId * @param \SimpleXMLElement|IiifResourceInterface $preloadedDocument Either null or the \SimpleXMLElement * or IiifResourceInterface that has been loaded to determine the basic document format. * * @return void */ - protected function __construct(int $pid, string $location, $preloadedDocument, array $settings = []) + protected function __construct(int $pid, string $location, int $pageId, $preloadedDocument, array $settings = []) { $this->pid = $pid; + $this->pageId = $pageId; $this->setPreloadedDocument($preloadedDocument); $this->init($location, $settings); $this->establishRecordId($pid); @@ -1301,4 +1331,114 @@ private static function setDocumentCache(string $location, AbstractDocument $cur // Save value in cache $cache->set($cacheIdentifier, $currentDocument); } + + /** + * Get IDs of logical structures that a page belongs to, indexed by depth. + * + * @param int $pageNo + * @return array + */ + public function getLogicalSectionsOnPage($pageNo) + { + $this->magicGetSmLinks(); + $this->magicGetPhysicalStructure(); + + $ids = []; + if (!empty($this->physicalStructure[$pageNo]) && !empty($this->smLinks['p2l'][$this->physicalStructure[$pageNo]])) { + foreach ($this->smLinks['p2l'][$this->physicalStructure[$pageNo]] as $logId) { + $depth = $this->getStructureDepth($logId); + $ids[$depth][] = $logId; + } + } + ksort($ids); + reset($ids); + return $ids; + } + + /** + * Get URL of download file of specified page, or the empty string if there is no such link. + * + * @param int $pageNumber + * @return string + */ + public function getPageLink($pageNumber) + { + $extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey); + $fileGrpsDownload = GeneralUtility::trimExplode(',', $extConf['files']['fileGrpDownload']); + // Get image link. + foreach ($fileGrpsDownload as $fileGrpDownload) { + if (!empty($this->physicalStructureInfo[$this->physicalStructure[$pageNumber]]['files'][$fileGrpDownload])) { + return $this->getFileLocation($this->physicalStructureInfo[$this->physicalStructure[$pageNumber]]['files'][$fileGrpDownload]); + } + } + return ''; + } + + public function toArray($uriBuilder, array $config = []) + { + $this->magicGetSmLinks(); + $this->magicGetPhysicalStructure(); + + $proxyFileGroups = $config['proxyFileGroups'] ?? []; + $forceAbsoluteUrl = $config['forceAbsoluteUrl'] ?? false; + $minPage = $config['minPage'] ?? 1; + $maxPage = $config['maxPage'] ?? $this->numPages; + + $result = [ + 'pages' => [], + 'query' => [ + 'minPage' => $minPage + ] + ]; + + $allFiles = $this->getAllFiles(); + + for ($page = $minPage; $page <= $maxPage; $page++) { + $pageEntry = [ + 'logSections' => array_merge(...$this->getLogicalSectionsOnPage($page)), + 'files' => [], + ]; + + foreach ($this->physicalStructureInfo[$this->physicalStructure[$page]]['files'] as $fileGrp => $fileId) { + if (!$allFiles) { + $file = [ + 'url' => $this->getFileLocation($fileId), + 'mimetype' => $this->getFileMimeType($fileId), + ]; + } else { + $file = $allFiles[$fileId] ?? null; + if ($file === null) { + continue; + } + } + + $extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey); + $nonProxyMimeTypes = GeneralUtility::trimExplode(',', $extConf['nonProxyMimeTypes']); + + // Only deliver static images via the internal PageViewProxy. + // (For IIP and IIIF, the viewer needs to build and access a separate metadata URL, see `getMetadataURL`.) + if (in_array($fileGrp, $proxyFileGroups) && !in_array($file['mimetype'], $nonProxyMimeTypes)) { + // Configure @action URL for form. + $file['url'] = $uriBuilder + ->reset() + ->setTargetPageUid($this->pageId) + ->setCreateAbsoluteUri($forceAbsoluteUrl) + ->setArguments( + [ + 'eID' => 'tx_dlf_pageview_proxy', + 'url' => $file['url'], + 'uHash' => GeneralUtility::hmac($file['url'], 'PageViewProxy') + ] + ) + ->build(); + } + + $pageEntry['files'][$fileGrp] = $file; + } + + $result['pages'][] = $pageEntry; + } + + return $result; + } } diff --git a/Classes/Common/IiifManifest.php b/Classes/Common/IiifManifest.php index 073bf5a53..3aa522452 100644 --- a/Classes/Common/IiifManifest.php +++ b/Classes/Common/IiifManifest.php @@ -419,6 +419,56 @@ public function getFileMimeType(string $id): string return $format; } + /** + * @see AbstractDocument::getAllFiles() + */ + public function getAllFiles(): array + { + $files = []; + $canvases = $this->iiif->getDefaultCanvases(); + foreach ($canvases as $canvas) { + $images = $canvas->getImages(); + foreach ($images as $image) { + $resource = $image->getResource(); + $fileId = $resource->getId(); + if (empty($fileId)) { + continue; + } + + $mimetype = $this->getFileMimeType($fileId); + if (empty($mimetype)) { + continue; + } + + $files[$fileId] = [ + 'url' => $fileId, + 'mimetype' => $mimetype, + ]; + } + + $otherFiles = $canvas->getOtherContent(); + foreach ($otherFiles as $otherFile) { + $fileId = $$otherFile->getId(); + if (empty($fileId)) { + continue; + } + + $mimetype = $this->getFileMimeType($fileId); + if (empty($mimetype)) { + continue; + } + + // in IIIF id is URL + $files[$fileId] = [ + 'url' => $fileId, + 'mimetype' => $mimetype, + ]; + } + + } + return $files; + } + /** * @see AbstractDocument::getLogicalStructure() */ @@ -854,8 +904,7 @@ protected function ensureHasFulltextIsSet(): void * https://digi.ub.uni-heidelberg.de/diglit/iiif/hirsch_hamburg1933_04_25/list/0001.json */ if (!$this->hasFulltextSet && $this->iiif instanceof ManifestInterface) { - $manifest = $this->iiif; - $canvases = $manifest->getDefaultCanvases(); + $canvases = $this->iiif->getDefaultCanvases(); foreach ($canvases as $canvas) { if ( !empty($canvas->getSeeAlsoUrlsForFormat("application/alto+xml")) || diff --git a/Classes/Common/Indexer.php b/Classes/Common/Indexer.php index 13d7b3d6f..e3efa73d8 100644 --- a/Classes/Common/Indexer.php +++ b/Classes/Common/Indexer.php @@ -109,7 +109,7 @@ public static function add(Document $document, DocumentRepository $documentRepos $parent = $documentRepository->findByUid($parentId); if ($parent) { // get XML document of parent - $doc = AbstractDocument::getInstance($parent->getLocation(), ['storagePid' => $parent->getPid()], true); + $doc = AbstractDocument::getInstance($parent->getLocation(), 0, ['storagePid' => $parent->getPid()], true); if ($doc !== null) { $parent->setCurrentDocument($doc); $success = self::add($parent, $documentRepository); diff --git a/Classes/Common/MetsDocument.php b/Classes/Common/MetsDocument.php index 30c4ad3be..180ed6ad3 100644 --- a/Classes/Common/MetsDocument.php +++ b/Classes/Common/MetsDocument.php @@ -317,6 +317,46 @@ public function getFileMimeType(string $id): string } /** + * @see AbstractDocument::getAllFiles() + */ + public function getAllFiles(): array + { + $files = []; + $fileNodes = $this->mets->xpath('./mets:fileSec/mets:fileGrp/mets:file'); + foreach ($fileNodes as $fileNode) { + $fileId = (string) $fileNode->attributes()->ID; + if (empty($fileId)) { + continue; + } + + $url = null; + foreach ($fileNode->children('http://www.loc.gov/METS/')->FLocat as $locator) { + if ((string) $locator->attributes()['LOCTYPE'] === 'URL') { + $url = (string) $locator->attributes('http://www.w3.org/1999/xlink')->href; + break; + } + } + + if ($url === null) { + continue; + } + + $mimetype = (string) $fileNode->attributes()['MIMETYPE']; + if (empty($mimetype)) { + continue; + } + + $files[$fileId] = [ + 'url' => $url, + 'mimetype' => $mimetype, + ]; + + } + return $files; + } + + /** + * {@inheritDoc} * @see AbstractDocument::getLogicalStructure() */ public function getLogicalStructure(string $id, bool $recursive = false): array @@ -378,8 +418,10 @@ protected function getLogicalStructureInfo(SimpleXMLElement $structure, bool $re 'pagination' => '', 'type' => isset($attributes['TYPE']) ? (string) $attributes['TYPE'] : '', 'description' => '', - 'thumbnailId' => null, + 'thumbnailId' => '', 'files' => [], + // Structure depth is determined and cached on demand + 'structureDepth' => null ]; // Set volume and year information only if no label is set and this is the toplevel structure element. @@ -1133,12 +1175,23 @@ public function getFullText(string $id): string */ public function getStructureDepth(string $logId) { + if (isset($this->logicalUnits[$logId]['structureDepth'])) { + return $this->logicalUnits[$logId]['structureDepth']; + } + $ancestors = $this->mets->xpath('./mets:structMap[@TYPE="LOGICAL"]//mets:div[@ID="' . $logId . '"]/ancestor::*'); if (!empty($ancestors)) { - return count($ancestors); + $structureDepth = count($ancestors); } else { - return 0; + $structureDepth = 0; } + + // NOTE: Don't just set $this->logicalUnits[$logId] here, because it may not yet be loaded + if (isset($this->logicalUnits[$logId])) { + $this->logicalUnits[$logId]['structureDepth'] = $structureDepth; + } + + return $structureDepth; } /** diff --git a/Classes/Controller/AbstractController.php b/Classes/Controller/AbstractController.php index edec0d81a..61288edbc 100644 --- a/Classes/Controller/AbstractController.php +++ b/Classes/Controller/AbstractController.php @@ -15,6 +15,7 @@ use Kitodo\Dlf\Common\Helper; use Kitodo\Dlf\Domain\Model\Document; use Kitodo\Dlf\Domain\Repository\DocumentRepository; +use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; @@ -140,15 +141,17 @@ protected function loadDocument(int $documentId = 0): void $documentId = $this->requestData['id']; } + $pageId = $this->getRequest()->getAttribute('routing')->getPageId(); + // Try to get document format from database if (!empty($documentId)) { $doc = null; if (MathUtility::canBeInterpretedAsInteger($documentId)) { - $doc = $this->getDocumentByUid($documentId); + $doc = $this->getDocumentByUid($documentId, $pageId); } elseif (GeneralUtility::isValidUrl($documentId)) { - $doc = $this->getDocumentByUrl($documentId); + $doc = $this->getDocumentByUrl($documentId, $pageId); } if ($this->document !== null && $doc !== null) { @@ -160,7 +163,7 @@ protected function loadDocument(int $documentId = 0): void $this->document = $this->documentRepository->findOneByRecordId($this->requestData['recordId']); if ($this->document !== null) { - $doc = AbstractDocument::getInstance($this->document->getLocation(), $this->settings, true); + $doc = AbstractDocument::getInstance($this->document->getLocation(), $pageId, $this->settings, true); if ($doc !== null) { $this->document->setCurrentDocument($doc); } else { @@ -184,8 +187,8 @@ protected function loadDocument(int $documentId = 0): void protected function configureProxyUrl(string &$url): void { $this->uriBuilder->reset() - ->setTargetPageUid($this->pageUid) - ->setCreateAbsoluteUri(!empty($this->extConf['general']['forceAbsoluteUrl'])) + ->setTargetPageUid($this->getRequest()->getAttribute('routing')->getPageId()) + ->setCreateAbsoluteUri(!empty($this->settings['general']['forceAbsoluteUrl'])) ->setArguments( [ 'eID' => 'tx_dlf_pageview_proxy', @@ -514,22 +517,28 @@ protected function buildSimplePagination(PaginationInterface $pagination, Pagina ]; } + protected function getRequest(): ServerRequestInterface + { + return $GLOBALS['TYPO3_REQUEST']; + } + /** * Get document from repository by uid. * * @access private * * @param int $documentId The document's UID + * @param int $pageId * * @return AbstractDocument */ - private function getDocumentByUid(int $documentId) + private function getDocumentByUid(int $documentId, int $pageId) { $doc = null; + // find document from repository by uid $this->document = $this->documentRepository->findOneByIdAndSettings($documentId); - if ($this->document) { - $doc = AbstractDocument::getInstance($this->document->getLocation(), $this->settings, true); + $doc = AbstractDocument::getInstance($this->document->getLocation(), $pageId, $this->settings, true); } else { $this->logger->error('Invalid UID "' . $documentId . '" or PID "' . $this->settings['storagePid'] . '" for document loading'); } @@ -543,18 +552,19 @@ private function getDocumentByUid(int $documentId) * @access protected * * @param string $documentId The document's URL + * @param int $pageId * * @return AbstractDocument */ - protected function getDocumentByUrl(string $documentId) + protected function getDocumentByUrl(string $documentId, int $pageId) { - $doc = AbstractDocument::getInstance($documentId, $this->settings, true); + $doc = AbstractDocument::getInstance($documentId, $pageId, $this->settings, true); if (isset($this->settings['multiViewType']) && $doc->tableOfContents[0]['type'] === $this->settings['multiViewType']) { $childDocuments = $doc->tableOfContents[0]['children']; $i = 0; foreach ($childDocuments as $document) { - $this->documentArray[] = AbstractDocument::getInstance($document['points'], $this->settings, true); + $this->documentArray[] = AbstractDocument::getInstance($document['points'], $pageId, $this->settings, true); if (!isset($this->requestData['docPage'][$i]) && isset(explode('#', $document['points'])[1])) { $initPage = explode('#', $document['points'])[1]; $this->requestData['docPage'][$i] = $initPage; @@ -567,7 +577,7 @@ protected function getDocumentByUrl(string $documentId) if ($this->requestData['multipleSource'] && is_array($this->requestData['multipleSource'])) { $i = 0; foreach ($this->requestData['multipleSource'] as $location) { - $document = AbstractDocument::getInstance($location, $this->settings, true); + $document = AbstractDocument::getInstance($location, $pageId, $this->settings, true); if ($document !== null) { $this->documentArray['extra_' . $i] = $document; } @@ -592,9 +602,9 @@ protected function getDocumentByUrl(string $documentId) } $this->document->setLocation($documentId); - } else { - $this->logger->error('Invalid location given "' . $documentId . '" for document loading'); - } + } else { + $this->logger->error('Invalid location given "' . $documentId . '" for document loading'); + } return $doc; } diff --git a/Classes/Controller/DocumentController.php b/Classes/Controller/DocumentController.php new file mode 100644 index 000000000..0913b0ecf --- /dev/null +++ b/Classes/Controller/DocumentController.php @@ -0,0 +1,178 @@ + + * + * This file is part of the Kitodo and TYPO3 projects. + * + * @license GNU General Public License version 3 or later. + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + */ + +namespace Kitodo\Dlf\Controller; + +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Provide document JSON for client side access + * + * @package TYPO3 + * @subpackage dlf + * @access public + */ +class DocumentController extends AbstractController +{ + /** + * The main method of the PlugIn + * + * @access public + * + * @return void + */ + public function mainAction() + { + // Load current document. + $this->loadDocument(); + if ($this->isDocMissingOrEmpty()) { + // Quit without doing anything if required variables are not set. + return; + } + + $this->setPage(); + + $this->setPage(); + + $metadataUrl = null; + + if (!empty($this->settings['targetPidMetadata'])) { + $metadataUrl = $this->uriBuilder + ->reset() + ->setTargetPageUid((int) $this->settings['targetPidMetadata']) + ->setCreateAbsoluteUri(true) + ->setArguments( + [ + 'tx_dlf' => [ + 'id' => $this->requestData['id'], + ], + ] + ) + ->build(); + } + + $filesConfiguration = $this->extConf['files']; + $imageFileGroups = array_reverse(GeneralUtility::trimExplode(',', $filesConfiguration ['fileGrpImages'])); + $fulltextFileGroups = GeneralUtility::trimExplode(',', $filesConfiguration ['fileGrpFulltext']); + + $config = [ + 'forceAbsoluteUrl' => !empty($this->settings['forceAbsoluteUrl']), + 'proxyFileGroups' => !empty($this->settings['useInternalProxy']) + ? array_merge($imageFileGroups, $fulltextFileGroups) + : [], + ]; + $loaded = [ + 'state' => [ + 'documentId' => $this->requestData['id'], + 'page' => $this->requestData['page'], + 'simultaneousPages' => (int) $this->requestData['double'] + 1, + ], + 'urlTemplate' => $this->getUrlTemplate(), + 'metadataUrl' => $metadataUrl, + 'fileGroups' => [ + 'images' => $imageFileGroups, + 'fulltext' => $fulltextFileGroups, + 'download' => GeneralUtility::trimExplode(',', $this->extConf['fileGrpDownload']), + ], + 'document' => $this->document->getCurrentDocument()->toArray($this->uriBuilder, $config), + ]; + + $docConfiguration = ' + window.addEventListener("DOMContentLoaded", function() { + const tx_dlf_loaded = ' . json_encode($loaded) . '; + window.dispatchEvent(new CustomEvent("tx-dlf-documentLoaded", { + detail: { + docController: new dlfController(tx_dlf_loaded) + } + })); + });'; + + $this->view->assign('docConfiguration', $docConfiguration); + } + + /** + * Get URL template with the following placeholders: + * + * * `PAGE_NO` (for value of `tx_dlf[page]`) + * * `DOUBLE_PAGE` (for value of `tx_dlf[double]`) + * * `PAGE_GRID` (for value of `tx_dlf[pagegrid]`) + * + * @return string + */ + protected function getUrlTemplate() + { + // Should work for route enhancers like this: + // + // routeEnhancers: + // KitodoWorkview: + // type: Plugin + // namespace: tx_dlf + // routePath: '/{page}/{double}' + // requirements: + // page: \d+ + // double: 0|1 + + $make = function ($page, $double, $pageGrid) { + $result = $this->uriBuilder->reset() + ->setTargetPageUid($this->getRequest()->getAttribute('routing')->getPageId()) + ->setCreateAbsoluteUri(!empty($this->settings['forceAbsoluteUrl']) ? true : false) + ->setArguments( + [ + 'tx_dlf' => array_merge( + $this->requestData, [ + 'page' => $page, + 'double' => $double, + 'pagegrid' => $pageGrid + ] + ), + ] + ) + ->build(); + + $cHashIdx = strpos($result, '&cHash='); + if ($cHashIdx !== false) { + $result = substr($result, 0, $cHashIdx); + } + + return $result; + }; + + // Generate two URLs that differ in tx_dlf[page], tx_dlf[double] and tx_dlf[highlight]. + // We don't know the order of these parameters, so use the values for matching. + $first = $make(2, 1, 0); + $second = $make(3, 0, 1); + + $lastIdx = 0; + $result = ''; + for ($i = 0, $len = strlen($first); $i < $len; $i++) { + if ($first[$i] === $second[$i]) { + continue; + } + + $result .= substr($first, $lastIdx, $i - $lastIdx); + $lastIdx = $i + 1; + + if ($first[$i] === '2') { + $placeholder = 'PAGE_NO'; + } elseif ($first[$i] === '1') { + $placeholder = 'DOUBLE_PAGE'; + } else { + $placeholder = 'PAGE_GRID'; + } + + $result .= $placeholder; + } + $result .= substr($first, $lastIdx); + + return $result; + } +} diff --git a/Classes/Controller/Embedded3dViewerController.php b/Classes/Controller/Embedded3dViewerController.php index e43d3b6b4..d6925fe98 100644 --- a/Classes/Controller/Embedded3dViewerController.php +++ b/Classes/Controller/Embedded3dViewerController.php @@ -39,7 +39,7 @@ public function mainAction(): void } if (!empty($this->settings['document'])) { - $this->assignModelFromDocument($this->getDocumentByUrl($this->settings['document'])); + $this->assignModelFromDocument($this->getDocumentByUrl($this->settings['document'], 0)); } else { $this->loadDocument(); if (!$this->isDocMissingOrEmpty()) { diff --git a/Classes/Controller/MetadataController.php b/Classes/Controller/MetadataController.php index b98b75903..ec91c42af 100644 --- a/Classes/Controller/MetadataController.php +++ b/Classes/Controller/MetadataController.php @@ -126,6 +126,7 @@ public function mainAction(): void $data = $this->currentDocument->getToplevelMetadata($this->settings['storagePid']); } $data['_id'] = $topLevelId; + $data['_active'] = true; array_unshift($metadata, $data); } @@ -212,7 +213,7 @@ private function buildIiifData(array $metadata): array foreach ($metadata as $row) { foreach ($row as $key => $group) { - if ($key == '_id') { + if ($key == '_id' || $key === '_active') { continue; } @@ -220,7 +221,7 @@ private function buildIiifData(array $metadata): array $iiifData[$key] = $this->buildIiifDataGroup($key, $group); } else { foreach ($group as $label => $value) { - if ($label == '_id') { + if ($label === '_id' || $label === '_active') { continue; } if (is_array($value)) { @@ -382,6 +383,26 @@ private function parseMetadata(int $i, string $name, $value, array &$metadata) : } } + /** + * Get metadata for given id array. + * + * @access private + * + * @param array $toc table of content + * @param array &$output metadata + * + * @return void + */ + private function getIds($toc, &$output) + { + foreach ($toc as $entry) { + $output[$entry['id']] = true; + if (is_array($entry['children'])) { + $this->getIds($entry['children'], $output); + } + } + } + /** * Parse title of parent document if needed. * @@ -474,18 +495,28 @@ private function getMetadata(): array $metadata = []; if ($this->settings['rootline'] < 2) { // Get current structure's @ID. - $ids = []; - $page = $this->currentDocument->physicalStructure[$this->requestData['page']]; - if (!empty($page) && !empty($this->currentDocument->smLinks['p2l'][$page])) { - foreach ($this->currentDocument->smLinks['p2l'][$page] as $logId) { - $count = $this->currentDocument->getStructureDepth($logId); - $ids[$count][] = $logId; - } - } - ksort($ids); - reset($ids); + $ids = $this->currentDocument->getLogicalSectionsOnPage((int) $this->requestData['page']); + // Check if we should display all metadata up to the root. - if ($this->settings['rootline'] == 1) { + if ($this->settings['prerenderAllSections'] ?? false) { + // Collect IDs of all logical structures. This is a flattened tree, so the + // order also works for rootline configurations. + $allIds = []; + + $this->getIds($this->currentDocument->tableOfContents, $allIds); + + $idIsActive = []; + foreach ($ids as $id) { + foreach ($id as $sid) { + $idIsActive[$sid] = true; + } + } + + $metadata = $this->getMetadataForIds(array_keys($allIds), $metadata); + foreach ($metadata as &$entry) { + $entry['_active'] = isset($idIsActive[$entry['_id']]); + } + } elseif ($this->settings['rootline'] == 1) { foreach ($ids as $id) { $metadata = $this->getMetadataForIds($id, $metadata); } @@ -520,6 +551,7 @@ private function getMetadataForIds(array $id, array $metadata): array } if (!empty($data)) { $data['_id'] = $sid; + $data['_active'] = true; $metadata[] = $data; } } diff --git a/Classes/Controller/NavigationController.php b/Classes/Controller/NavigationController.php index 698d8a479..aa75147c3 100644 --- a/Classes/Controller/NavigationController.php +++ b/Classes/Controller/NavigationController.php @@ -79,8 +79,10 @@ public function mainAction(): void } // Steps for X pages backward / forward. Double page view uses double steps. - $pageSteps = $this->settings['pageStep'] * ($this->requestData['double'] + 1); + $basePageSteps = $this->settings['pageStep'] ?: 10; + $pageSteps = $basePageSteps * ($this->requestData['double'] + 1); + $this->view->assign('basePageSteps', $basePageSteps); $this->view->assign('pageSteps', $pageSteps); $this->view->assign('numPages', $this->document->getCurrentDocument()->numPages); $this->view->assign('viewData', $this->viewData); diff --git a/Classes/Controller/PageViewController.php b/Classes/Controller/PageViewController.php index 78b830535..2ab8a96fb 100644 --- a/Classes/Controller/PageViewController.php +++ b/Classes/Controller/PageViewController.php @@ -462,6 +462,22 @@ protected function getFulltext(int $page): array */ protected function addViewerJS(): void { + // TODO(client-side): Avoid redundancy to DocumentController + $filesConfiguration = $this->extConf['files']; + $imageFileGroups = array_reverse(GeneralUtility::trimExplode(',', $filesConfiguration['fileGrpImages'])); + $fulltextFileGroups = GeneralUtility::trimExplode(',', $filesConfiguration['fileGrpFulltext']); + $config = [ + 'forceAbsoluteUrl' => !empty($this->settings['forceAbsoluteUrl']), + 'proxyFileGroups' => !empty($this->settings['useInternalProxy']) + ? array_merge($imageFileGroups, $fulltextFileGroups) + : [], + // toArray uses closed interval [minPage, maxPage] + 'minPage' => $this->requestData['page'], + 'maxPage' => $this->requestData['page'] + $this->requestData['double'] + ]; + + $initDoc = $this->document->getCurrentDocument()->toArray($this->uriBuilder, $config); + if (count($this->documentArray) > 1) { $jsViewer = 'tx_dlf_viewer = [];'; $i = 0; @@ -508,14 +524,14 @@ protected function addViewerJS(): void 'measureIdLinks' => $docMeasures['measureLinks'] ]; - $jsViewer .= 'tx_dlf_viewer[' . $i . '] = new dlfViewer(' . json_encode($viewer) . '); - '; + $jsViewer .= 'tx_dlf_viewer[' . $i . '] = new dlfViewer(' . json_encode($viewer) . ');'; $i++; } } // Viewer configuration. - $viewerConfiguration = '$(document).ready(function() { + $viewerConfiguration = ' + $(document).ready(function() { if (dlfUtils.exists(dlfViewer)) { ' . $jsViewer . ' viewerCount = ' . ($i - 1) . '; @@ -546,11 +562,26 @@ protected function addViewerJS(): void ]; // Viewer configuration. - $viewerConfiguration = '$(document).ready(function() { - if (dlfUtils.exists(dlfViewer)) { - tx_dlf_viewer = new dlfViewer(' . json_encode($viewer) . '); - } - });'; + $viewerConfiguration = ' + (function () { + let docController = null; + + window.addEventListener("tx-dlf-documentLoaded", e => { + docController = e.detail.docController; + if (typeof tx_dlf_viewer !== "undefined") { + tx_dlf_viewer.setDocController(docController); + } + }); + + $(document).ready(function() { + + if (dlfUtils.exists(dlfViewer)) { + tx_dlf_viewer = new dlfViewer(' . json_encode($viewer) . '); + } + tx_dlf_viewer.setDocController(docController); + } + }); + })();'; } $this->view->assign('viewerConfiguration', $viewerConfiguration); } diff --git a/Classes/Controller/TableOfContentsController.php b/Classes/Controller/TableOfContentsController.php index 7bbcefd43..260d59ac6 100644 --- a/Classes/Controller/TableOfContentsController.php +++ b/Classes/Controller/TableOfContentsController.php @@ -127,6 +127,7 @@ private function getMenuEntry(array $entry, bool $recursive = false): array $entryArray['orderlabel'] = $entry['orderlabel']; $entryArray['type'] = $this->getTranslatedType($entry['type']); $entryArray['pagination'] = htmlspecialchars($entry['pagination']); + $entryArray['logId'] = $entry['id']; $entryArray['_OVERRIDE_HREF'] = ''; $entryArray['doNotLinkIt'] = 1; $entryArray['ITEM_STATE'] = 'NO'; @@ -142,14 +143,24 @@ private function getMenuEntry(array $entry, bool $recursive = false): array $recursive === true && !empty($entry['children']) ) { + $entryArray['isAlwaysExpanded'] = ( + is_string($entry['points']) + || empty($this->document->getCurrentDocument()->smLinks['l2p'][$entry['id']]) + ); + + $entryArray['isCurrentlyExpanded'] = ( + $entryArray['ITEM_STATE'] == 'CUR' + || $entryArray['isAlwaysExpanded'] + ); + // Build sub-menu only if one of the following conditions apply: + // 0. Configuration says that the full menu should be rendered // 1. Current menu node is in rootline // 2. Current menu node points to another file // 3. Current menu node has no corresponding images if ( - $entryArray['ITEM_STATE'] == 'CUR' - || is_string($entry['points']) - || empty($this->document->getCurrentDocument()->smLinks['l2p'][$entry['id']]) + $this->settings['showFull'] + || $entryArray['isCurrentlyExpanded'] ) { $entryArray['_SUB_MENU'] = []; foreach ($entry['children'] as $child) { diff --git a/Classes/Controller/ToolboxController.php b/Classes/Controller/ToolboxController.php index b7eddb4c8..80fdaa916 100644 --- a/Classes/Controller/ToolboxController.php +++ b/Classes/Controller/ToolboxController.php @@ -136,19 +136,31 @@ private function renderToolByName(string $tool): void * * @return array Array of image information's. */ - public function getImage(int $page): array + private function getImage(int $page, array $fileGrps): array { - // Get @USE value of METS fileGroup. - $image = $this->getFile($page, GeneralUtility::trimExplode(',', $this->settings['fileGrpsImageDownload'])); - switch ($image['mimetype']) { - case 'image/jpeg': - $image['mimetypeLabel'] = ' (JPG)'; - break; - case 'image/tiff': - $image['mimetypeLabel'] = ' (TIFF)'; + $image = []; + foreach ($fileGrps as $fileGrp) { + // Get image link. + $physicalStructureInfo = $this->currentDocument->physicalStructureInfo[$this->currentDocument->physicalStructure[$page]]; + $fileId = $physicalStructureInfo['files'][$fileGrp]; + if (!empty($fileId)) { + $image['url'] = $this->currentDocument->getDownloadLocation($fileId); + $image['mimetype'] = $this->currentDocument->getFileMimeType($fileId); + // Also see Toolbox.js + switch ($image['mimetype']) { + case 'image/jpeg': + $image['mimetypeLabel'] = ' (JPG)'; + break; + case 'image/tiff': + $image['mimetypeLabel'] = ' (TIFF)'; + break; + default: + $image['mimetypeLabel'] = ''; + } break; - default: - $image['mimetypeLabel'] = ''; + } else { + $this->logger->warning('File not found in fileGrp "' . $fileGrp . '"'); + } } return $image; } @@ -161,6 +173,7 @@ public function getImage(int $page): array * * @return void */ + // TODO(client-side) private function renderAnnotationTool(): void { if ($this->isDocMissingOrEmpty()) { @@ -320,21 +333,25 @@ private function renderImageDownloadTool(): void $this->setPage(); + // Get @USE value of METS fileGrp. + $fileGrpsImageDownload = array_reverse(GeneralUtility::trimExplode(',', $this->settings['fileGrpsImageDownload'])); + $imageArray = []; // Get left or single page download. - $image = $this->getImage($this->requestData['page']); + $image = $this->getImage($this->requestData['page'], $fileGrpsImageDownload); if ($this->filterImageFiles($image)) { $imageArray[0] = $image; } if ($this->requestData['double'] == 1) { - $image = $this->getImage($this->requestData['page'] + 1); + $image = $this->getImage($this->requestData['page'] + 1, $fileGrpsImageDownload); if ($this->filterImageFiles($image)) { $imageArray[1] = $image; } } $this->view->assign('imageDownload', $imageArray); + $this->view->assign('fileGrpsImageDownload', $fileGrpsImageDownload); } /** @@ -361,6 +378,7 @@ private function filterImageFiles($image): bool * @access private * * @param int $page Page number + * @param string[] $fileGrps File groups to consider * * @return array Array of file information */ @@ -456,42 +474,22 @@ private function renderPdfDownloadTool(): void */ private function getPageLink(): array { - $firstPageLink = ''; - $secondPageLink = ''; - $pageLinkArray = []; $pageNumber = $this->requestData['page']; - $fileGrpsDownload = GeneralUtility::trimExplode(',', $this->extConf['files']['fileGrpDownload']); - // Get image link. - while ($fileGrpDownload = array_shift($fileGrpsDownload)) { - $firstFileGroupDownload = $this->currentDocument->physicalStructureInfo[$this->currentDocument->physicalStructure[$pageNumber]]['files'][$fileGrpDownload]; - if (!empty($firstFileGroupDownload)) { - $firstPageLink = $this->currentDocument->getFileLocation($firstFileGroupDownload); - // Get second page, too, if double page view is activated. - $secondFileGroupDownload = $this->currentDocument->physicalStructureInfo[$this->currentDocument->physicalStructure[$pageNumber + 1]]['files'][$fileGrpDownload]; - if ( - $this->requestData['double'] - && $pageNumber < $this->currentDocument->numPages - && !empty($secondFileGroupDownload) - ) { - $secondPageLink = $this->currentDocument->getFileLocation($secondFileGroupDownload); - } - break; - } + $pageLinks = [ + $this->currentDocument->getPageLink($pageNumber), + ]; + // Get second page, too, if double page view is activated. + if ($this->requestData['double'] && $pageNumber < $this->currentDocument->numPages) { + $pageLinks[1] = $this->currentDocument->getPageLink($pageNumber + 1); } if ( - empty($firstPageLink) - && empty($secondPageLink) + empty($pageLinks[0]) + && empty($pageLinks[1]) ) { $this->logger->warning('File not found in fileGrps "' . $this->extConf['files']['fileGrpDownload'] . '"'); } - if (!empty($firstPageLink)) { - $pageLinkArray[0] = $firstPageLink; - } - if (!empty($secondPageLink)) { - $pageLinkArray[1] = $secondPageLink; - } - return $pageLinkArray; + return $pageLinks; } /** diff --git a/Classes/Domain/Repository/DocumentRepository.php b/Classes/Domain/Repository/DocumentRepository.php index 25785b878..18426b8c2 100644 --- a/Classes/Domain/Repository/DocumentRepository.php +++ b/Classes/Domain/Repository/DocumentRepository.php @@ -76,7 +76,7 @@ public function findOneByParameters($parameters) } else if (isset($parameters['location']) && GeneralUtility::isValidUrl($parameters['location'])) { - $doc = AbstractDocument::getInstance($parameters['location'], [], true); + $doc = AbstractDocument::getInstance($parameters['location'], 0, [], true); if ($doc->recordId) { $document = $this->findOneByRecordId($doc->recordId); @@ -91,7 +91,7 @@ public function findOneByParameters($parameters) } if ($document !== null && $doc === null) { - $doc = AbstractDocument::getInstance($document->getLocation(), [], true); + $doc = AbstractDocument::getInstance($document->getLocation(), 0, [], true); } if ($doc !== null) { diff --git a/Classes/ExpressionLanguage/DocumentTypeFunctionProvider.php b/Classes/ExpressionLanguage/DocumentTypeFunctionProvider.php index 1c3a5f62d..7b27082ac 100644 --- a/Classes/ExpressionLanguage/DocumentTypeFunctionProvider.php +++ b/Classes/ExpressionLanguage/DocumentTypeFunctionProvider.php @@ -182,12 +182,13 @@ protected function loadDocument(array $requestData, int $pid): void // find document from repository by uid $this->document = $this->documentRepository->findOneByIdAndSettings((int) $requestData['id'], ['storagePid' => $pid]); if ($this->document) { - $doc = AbstractDocument::getInstance($this->document->getLocation(), ['storagePid' => $pid], true); + $doc = AbstractDocument::getInstance($this->document->getLocation(), 0, ['storagePid' => $pid], true); } else { $this->logger->error('Invalid UID "' . $requestData['id'] . '" or PID "' . $pid . '" for document loading'); } } elseif (GeneralUtility::isValidUrl($requestData['id'])) { - $doc = AbstractDocument::getInstance($requestData['id'], ['storagePid' => $pid], true); + $doc = AbstractDocument::getInstance($requestData['id'], 0, ['storagePid' => $pid], true); + if ($doc !== null) { if ($doc->recordId) { $this->document = $this->documentRepository->findOneByRecordId($doc->recordId); @@ -207,7 +208,7 @@ protected function loadDocument(array $requestData, int $pid): void } elseif (!empty($requestData['recordId'])) { $this->document = $this->documentRepository->findOneByRecordId($requestData['recordId']); if ($this->document !== null) { - $doc = AbstractDocument::getInstance($this->document->getLocation(), ['storagePid' => $pid], true); + $doc = AbstractDocument::getInstance($this->document->getLocation(), 0, ['storagePid' => $pid], true); if ($doc !== null) { $this->document->setCurrentDocument($doc); } else { diff --git a/Classes/Hooks/DataHandler.php b/Classes/Hooks/DataHandler.php index a809fbd56..3b87a0aa4 100644 --- a/Classes/Hooks/DataHandler.php +++ b/Classes/Hooks/DataHandler.php @@ -366,7 +366,7 @@ private function deleteDocument($core, $id): void private function reindexDocument($id):void { $document = $this->getDocumentRepository()->findByUid((int) $id); - $doc = AbstractDocument::getInstance($document->getLocation(), ['storagePid' => $document->getPid()], true); + $doc = AbstractDocument::getInstance($document->getLocation(), 0, ['storagePid' => $document->getPid()], true); if ($document !== null && $doc !== null) { $document->setCurrentDocument($doc); Indexer::add($document, $this->getDocumentRepository()); diff --git a/Configuration/FlexForms/Document.xml b/Configuration/FlexForms/Document.xml new file mode 100644 index 000000000..20a7fbaed --- /dev/null +++ b/Configuration/FlexForms/Document.xml @@ -0,0 +1,67 @@ + + + + + 1 + + + + + + LLL:EXT:dlf/Resources/Private/Language/locallang_be.xlf:flexform.sheet_general + + array + + + + 1 + + + check + 1 + + + + + + 1 + + + check + 0 + + + + + + 1 + + + group + db + pages + 1 + 1 + 1 + 1 + + + suggest + + + + + + + + + + diff --git a/Configuration/FlexForms/Metadata.xml b/Configuration/FlexForms/Metadata.xml index 34b9f1a1c..251ca6082 100644 --- a/Configuration/FlexForms/Metadata.xml +++ b/Configuration/FlexForms/Metadata.xml @@ -82,6 +82,16 @@ + + + 1 + + + check + 0 + + + 1 diff --git a/Configuration/FlexForms/Navigation.xml b/Configuration/FlexForms/Navigation.xml index 3f98c73c0..33d3cb147 100644 --- a/Configuration/FlexForms/Navigation.xml +++ b/Configuration/FlexForms/Navigation.xml @@ -81,7 +81,7 @@ measureBack - doublePage,pageFirst,pageBack,pageStepBack,pageSelect,pageForward,pageStepForward,pageLast,listView,zoom,rotation,measureForward,measureBack + doublePage,pageFirst,pageStepBack,pageBack,pageSelect,pageForward,pageStepForward,pageLast,listView,zoom,rotation,measureForward,measureBack 1 diff --git a/Configuration/FlexForms/TableOfContents.xml b/Configuration/FlexForms/TableOfContents.xml index cbb7cdceb..179b18e0b 100644 --- a/Configuration/FlexForms/TableOfContents.xml +++ b/Configuration/FlexForms/TableOfContents.xml @@ -80,6 +80,16 @@ + + + 1 + + + check + 0 + + + diff --git a/Configuration/TCA/Overrides/tt_content.php b/Configuration/TCA/Overrides/tt_content.php index ab98fc84a..ff2c5a307 100644 --- a/Configuration/TCA/Overrides/tt_content.php +++ b/Configuration/TCA/Overrides/tt_content.php @@ -29,6 +29,10 @@ $GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist']['dlf_collection'] = 'layout,select_key,pages,recursive'; $GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['dlf_collection'] = 'pi_flexform'; \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue('dlf_collection', 'FILE:EXT:' . 'dlf/Configuration/FlexForms/Collection.xml'); +// Plugin "document". +$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist']['dlf_document'] = 'layout,select_key,pages,recursive'; +$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['dlf_document'] = 'pi_flexform'; +\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue('dlf_document', 'FILE:EXT:' . 'dlf/Configuration/FlexForms/Document.xml'); // Plugin "feeds". $GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist']['dlf_feeds'] = 'layout,select_key,pages,recursive'; $GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['dlf_feeds'] = 'pi_flexform'; @@ -191,7 +195,14 @@ ); \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin( - 'Kitodo.Dlf', + 'Dlf', 'Annotation', 'LLL:EXT:dlf/Resources/Private/Language/locallang_be.xlf:plugins.annotation.title', ); + +\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin( + 'Dlf', + 'Document', + 'LLL:EXT:dlf/Resources/Private/Language/locallang_be.xlf:plugins.document.title', + 'EXT:dlf/Resources/Public/Icons/tx-dlf-document.svg' +); \ No newline at end of file diff --git a/Configuration/TsConfig/ContentElements.tsconfig b/Configuration/TsConfig/ContentElements.tsconfig index baa9ba7e7..dadbc2dbd 100644 --- a/Configuration/TsConfig/ContentElements.tsconfig +++ b/Configuration/TsConfig/ContentElements.tsconfig @@ -146,6 +146,15 @@ mod.wizards.newContentElement.wizardItems { list_type = dlf_toolbox } } + tx_dlf_document { + iconIdentifier = tx-dlf-document + title = LLL:EXT:dlf/Resources/Private/Language/locallang_be.xlf:plugins.document.title + description = LLL:EXT:dlf/Resources/Private/Language/locallang_be.xlf:plugins.document.description + tt_content_defValues { + CType = list + list_type = dlf_document + } + } } show = * } diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.typoscript index 120789456..16770f1e7 100644 --- a/Configuration/TypoScript/setup.typoscript +++ b/Configuration/TypoScript/setup.typoscript @@ -20,6 +20,7 @@ page { embedded3dviewer = EXT:dlf/Resources/Public/Stylesheets/embedded-3d-viewer.css jPlayer = EXT:dlf/Resources/Public/JavaScript/jPlayer/blue.monday/css/jplayer.blue.monday.min.css openLayers = EXT:dlf/Resources/Public/JavaScript/OpenLayers/openlayers.css + kitodo = EXT:dlf/Resources/Public/Css/Kitodo.css } includeJSFooterlibs { @@ -65,8 +66,12 @@ page { kitodo-pageView-syncControl = EXT:dlf/Resources/Public/JavaScript/PageView/SyncControl.js kitodo-pageView-searchInDocument = EXT:dlf/Resources/Public/JavaScript/PageView/SearchInDocument.js kitodo-pageView-pageView = EXT:dlf/Resources/Public/JavaScript/PageView/PageView.js + kitodo-pageView-navigation = EXT:dlf/Resources/Public/JavaScript/PageView/Navigation.js + kitodo-pageView-metadata = EXT:dlf/Resources/Public/JavaScript/PageView/Metadata.js + kitodo-pageView-toolbox = EXT:dlf/Resources/Public/JavaScript/PageView/Toolbox.js + kitodo-pageView-tableofcontents = EXT:dlf/Resources/Public/JavaScript/PageView/TableOfContents.js + kitodo-controller = EXT:dlf/Resources/Public/JavaScript/PageView/Controller.js kitodo-search-suggest = EXT:dlf/Resources/Public/JavaScript/Search/Suggester.js - } } diff --git a/Documentation/Developers/ClientSide.rst b/Documentation/Developers/ClientSide.rst new file mode 100644 index 000000000..1957b2c91 --- /dev/null +++ b/Documentation/Developers/ClientSide.rst @@ -0,0 +1,127 @@ +=========== +Client-Side +=========== + +Document Plugin +=============== + +There is a new ``Document`` plugin that provides client-side access to the loaded document. +A separate plugin is used for a couple of reasons: + +* It will allow to make the client-side features opt-in. +* It will be the natural place to put API endpoints if needed. +* There doesn't seem to be another natural place to put client-side features anyways. + (For instance, the PageView plugin may not be active in media documents.) + +Document Descriptor +=================== + +The method ``Doc::toArray()`` collects all information used by the frontend into a JSON-serializable array. +See the type ``dlf.PageObject`` for an outline of its structure. + +Control Flow and Events +======================= + +* The ``Document`` plugin creates an instance of the ``Controller`` class (see ``Controller.js``) and dispatches the event ``tx-dlf-documentLoaded``. + +* Plugins that would like to interact with the document object listen to ``tx-dlf-documentLoaded``, and get the ``Controller`` instance from the event detail. + + .. code-block:: javascript + + window.addEventListener('tx-dlf-documentLoaded', (e) => { + this.docController = e.detail.docController; + }); + +* Whenever a plugin would like to *change* the view state (currently, to change the page or to toggle doublepage mode), it should call the ``Controller::changeState`` or ``Controller::changePage`` method. + This dispatches the event ``tx-dlf-stateChanged``, which is of type ``dlf.StateChangeEvent`` (see ``types.d.ts``). + + .. code-block:: javascript + + this.docController.changePage(1); + +* If a plugin would like to react to changes of the view state, it may listen to the ``tx-dlf-stateChanged`` event. + The ``Controller::eventTarget`` tells on which element the event listener should be registered. + + .. code-block:: javascript + + this.docController.eventTarget.addEventListener('tx-dlf-stateChanged', (e) => { + if (e.detail.page !== undefined) { + console.log(`Switched to page ${e.detail.page}`); + } + }); + +Metadata +======== + +To dynamically show the metadata sections of the current page: + +* At initial load, only the active metadata sections are rendered (as before). + Then, a request is sent to fetch all rendered metadata sections, and the metadata list is replaced. + For this to work, a ``targetPidMetadata`` must be configured. + This procedure is used to reduce because rendering all metadata sections can take some while for large documents. +* The attribute ``data-dlf-section`` names the ID of the logical section. + Sections that to not belong to the current page are hidden. +* For each page, the document objects lists the sections that the page belongs to. +* On page change, this information is used to show/hide the sections depending on whether or not the page belongs to it. + +Rootline configuration is considered. + +URLs and Slugs +============== + +For dynamic link generation on the client, a URL template is generated in ``DocumentController::getUrlTemplate()``. +The template contains placeholders for the relevant parameters, which are then replaced by the current values in ``Controller::makePageUrl()``. +The generated URL does not include a ``cHash``. + +This solution is intended to avoid generating all possible URL variants on the backend while still supporting slugs. + +Various +======= + +* Events + + * ``tx-dlf-stateChanged`` + * ``tx-dlf-documentLoaded`` + +* Properties + + * ``data-page-link`` + * ``data-file-groups`` + * ``data-metadata-list`` + * ``data-dlf-section`` + * ``data-text`` + * ``data-toc-item`` + * ``data-toc-expand-always`` + * ``data-toc-link`` + * ``data-document-id`` + * ``data-page`` + +* Data CSS classes + + * ``dlf-mimetype-label`` + * ``page-step-back`` + * ``page-back`` + * ``page-first`` + * ``page-step-forward`` + * ``page-forward`` + * ``page-last`` + * ``page-select`` + +* Display CSS classes + + * ``shown-if-single`` + * ``shown-if-double`` + +Code +==== + +* See ``types.d.ts`` for JavaScript type declarations +* ``TODO(client-side)``: TODOs related to client-side features + +Migration +========= + +- Add page for prerendering metadata +- Add document plugin to page view +- Set ``showFull = 1`` in table of contents +- Template adjustments diff --git a/Documentation/Developers/Index.rst b/Documentation/Developers/Index.rst index 3639619a4..024d5a2bb 100644 --- a/Documentation/Developers/Index.rst +++ b/Documentation/Developers/Index.rst @@ -8,5 +8,6 @@ These pages are aimed at developers working on Kitodo.Presentation. Metadata Database + ClientSide Validation Embedded3DViewer diff --git a/Documentation/Plugins/Index.rst b/Documentation/Plugins/Index.rst index b2b898218..7c20cb5c3 100644 --- a/Documentation/Plugins/Index.rst +++ b/Documentation/Plugins/Index.rst @@ -292,6 +292,43 @@ The collection plugin shows one collection, all collections or selected collecti `t3tsref:data-type-page-id` :Default: + +Document +-------- + +:typoscript:`plugin.tx_dlf_document` + +.. t3-field-list-table:: + :header-rows: 1 + + - :Property: + Property + :Data Type: + Data Type + :Default: + Default + + - :Property: + excludeOther_ + :Data Type: + `t3tsref:data-type-boolean` + :Default: + 1 + + - :Property: + useInternalProxy + :Data Type: + `t3tsref:data-type-boolean` + :Default: + 0 + + - :Property: + targetPidMetadata + :Data Type: + `t3tsref:data-type-page-id` + :Default: + + Embedded 3D Viewer ----------- @@ -506,6 +543,13 @@ Metadata :Default: 1 + - :Property: + prerenderAllSections + :Data Type: + :ref:`t3tsref:data-type-boolean` + :Default: + 1 + - :Property: rootline :Data Type: @@ -906,6 +950,13 @@ Table Of Contents :Default: 0 + - :Property: + showFull + :Data Type: + :ref:`t3tsref:data-type-boolean` + :Default: + 0 + - :Property: targetBasket :Data Type: diff --git a/Resources/Private/Language/de.locallang_be.xlf b/Resources/Private/Language/de.locallang_be.xlf index 779d0d3f4..6021b5d03 100644 --- a/Resources/Private/Language/de.locallang_be.xlf +++ b/Resources/Private/Language/de.locallang_be.xlf @@ -169,6 +169,18 @@ + + + + + + + + + + + + @@ -241,6 +253,10 @@ + + + + @@ -505,6 +521,10 @@ + + + + diff --git a/Resources/Private/Language/de.locallang_labels.xlf b/Resources/Private/Language/de.locallang_labels.xlf index c837de10d..c6a5b35fd 100644 --- a/Resources/Private/Language/de.locallang_labels.xlf +++ b/Resources/Private/Language/de.locallang_labels.xlf @@ -601,6 +601,10 @@ DLF: Collection DLF: Kollektion + + DLF: Document + DLF: Dokument + DLF: List View DLF: Listenansicht @@ -661,6 +665,10 @@ Eingelesene METS Dateien / IIIF-Manifeste zwischenspeichern: Dies kann die Geschwindigkeit geringfügig verbessern, führt aber zu einer sehr großen "fe_session_data" Tabelle (Standard ist "FALSE") Cache parsed METS files / IIIF manifests: Caching improves performance a little bit but can result in a very large "fe_session_data" table (default is "FALSE") + + Non proxy MIME types: comma-separated list (default is "application/vnd.kitodo.iiif,application/vnd.netfpx,application/vnd.kitodo.zoomify") + MIME Typen ohne Proxy: Komma-getrennte Liste (Standard ist "application/vnd.kitodo.iiif,application/vnd.netfpx,application/vnd.kitodo.zoomify") + Neue Kollektionen publizieren?: Sollen neue Kollektionen automatisch in der OAI-PMH-Schnittstelle veröffentlicht werden? (Standard ist "TRUE") Publish new collections?: Should new collections automatically be published in the OAI-PMH interface? (default is "TRUE") diff --git a/Resources/Private/Language/locallang_be.xlf b/Resources/Private/Language/locallang_be.xlf index 7b215dcf1..5958e6918 100644 --- a/Resources/Private/Language/locallang_be.xlf +++ b/Resources/Private/Language/locallang_be.xlf @@ -269,6 +269,9 @@ + + + @@ -377,6 +380,15 @@ + + + + + + + + + @@ -392,6 +404,9 @@ + + + diff --git a/Resources/Private/Language/locallang_labels.xlf b/Resources/Private/Language/locallang_labels.xlf index 6f1821ef5..a05319ca9 100644 --- a/Resources/Private/Language/locallang_labels.xlf +++ b/Resources/Private/Language/locallang_labels.xlf @@ -452,6 +452,9 @@ DLF: Collection + + DLF: Document + DLF: List View @@ -497,6 +500,9 @@ Cache parsed METS files / IIIF manifests: Caching improves performance a little bit but can result in a very large "fe_session_data" table (default is "FALSE") + + Non proxy MIME types: comma-separated list (default is "application/vnd.kitodo.iiif,application/vnd.netfpx,application/vnd.kitodo.zoomify") + Publish new collections?: Should new collections automatically be published in the OAI-PMH interface? (default is "TRUE") diff --git a/Resources/Private/Partials/TableOfContents/Children.html b/Resources/Private/Partials/TableOfContents/Children.html index ba2d77a7f..4ed41c68a 100644 --- a/Resources/Private/Partials/TableOfContents/Children.html +++ b/Resources/Private/Partials/TableOfContents/Children.html @@ -15,25 +15,31 @@ -
  • + -
  • + -
  • + -
  • + -
  • + -
  • + + + + + +
  • + @@ -47,6 +53,7 @@ additionalParams="{f:if(condition:'{child.id}', then:'{\'tx_dlf[id]\':child.id, \'tx_dlf[page]\':child.page}', else: '{\'tx_dlf[page]\':child.page}')}" argumentsToBeExcludedFromQueryString="{0: 'tx_dlf[measure]'}" addQueryString="1" + additionalAttributes="{'data-document-id':child.id, 'data-page':child.page, 'data-toc-link':1}" title="{f:if(condition:'{child.title}', then: '{child.title}', else: '{child.type}')}"> diff --git a/Resources/Private/Templates/Document/Main.html b/Resources/Private/Templates/Document/Main.html new file mode 100644 index 000000000..74f245ab3 --- /dev/null +++ b/Resources/Private/Templates/Document/Main.html @@ -0,0 +1,8 @@ + + + + + diff --git a/Resources/Private/Templates/Metadata/Main.html b/Resources/Private/Templates/Metadata/Main.html index 23428ed1b..42dddcde4 100644 --- a/Resources/Private/Templates/Metadata/Main.html +++ b/Resources/Private/Templates/Metadata/Main.html @@ -19,19 +19,31 @@ NOTE: We assume unescaped values - - - - - - - - - - + + + diff --git a/Resources/Private/Templates/Navigation/Main.html b/Resources/Private/Templates/Navigation/Main.html index 3b1020388..770a2365c 100644 --- a/Resources/Private/Templates/Navigation/Main.html +++ b/Resources/Private/Templates/Navigation/Main.html @@ -70,35 +70,26 @@
    - - - - - - - - - - - - + + +
    + + Make sure that no negative number is put into tx_dlf[page], so that a route enhancer that requires matches against "\d+" won't fail - + - + - +
    @@ -108,15 +99,14 @@
    - + - + - +
    @@ -137,10 +127,9 @@ - + value="{viewData.requestData.page}">
  • @@ -148,55 +137,26 @@
    - - - - - - - - - - - - + = {numPages}\", then: \"disabled:\")}" addQueryString="1" additionalParams="{'tx_dlf[page]':'{viewData.requestData.page + 1 + viewData.requestData.double}'}" argumentsToBeExcludedFromQueryString="{0: 'tx_dlf[measure]'}"> + +
    -
    - - - - - - - - - - - - -
    +
    + + {numPages - pageSteps}\", then: \"disabled:\")}" addQueryString="1" additionalParams="{'tx_dlf[page]':'{viewData.requestData.page + pageSteps}'}" additionalAttributes="{'data-text': '{forwardTextTemplate}'}" argumentsToBeExcludedFromQueryString="{0: 'tx_dlf[measure]'}"> + + +
    - - - - - - - - - - - - + = {numPages - viewData.requestData.double}\", then: \"disabled:\")}" addQueryString="1" additionalParams="{'tx_dlf[page]':'{numPages - viewData.requestData.double}'}" argumentsToBeExcludedFromQueryString="{0: 'tx_dlf[measure]'}"> + +
    @@ -302,4 +262,14 @@ + diff --git a/Resources/Private/Templates/TableOfContents/Main.html b/Resources/Private/Templates/TableOfContents/Main.html index 226f43927..0d1547e47 100644 --- a/Resources/Private/Templates/TableOfContents/Main.html +++ b/Resources/Private/Templates/TableOfContents/Main.html @@ -18,4 +18,10 @@ + + diff --git a/Resources/Private/Templates/Toolbox/Main.html b/Resources/Private/Templates/Toolbox/Main.html index 8b8836ad8..3adbd5e6d 100644 --- a/Resources/Private/Templates/Toolbox/Main.html +++ b/Resources/Private/Templates/Toolbox/Main.html @@ -48,28 +48,21 @@
  • - - - - - {imageDownload.0.mimetypeLabel} - - - - - {imageDownload.0.mimetypeLabel} - - - - - - {imageDownload.1.mimetypeLabel} - - - - - - + {fileGrpsImageDownload -> f:format.json()} + + + {imageDownload.0.mimetypeLabel} + + + + + {imageDownload.0.mimetypeLabel} + + + + + {imageDownload.1.mimetypeLabel} +
  • @@ -161,17 +154,17 @@ - + (PDF) - + (PDF) - + (PDF) @@ -233,4 +226,14 @@ + + diff --git a/Resources/Public/Css/DlfMediaPlayerStyles.css.map b/Resources/Public/Css/DlfMediaPlayerStyles.css.map new file mode 100644 index 000000000..f5f3e1721 --- /dev/null +++ b/Resources/Public/Css/DlfMediaPlayerStyles.css.map @@ -0,0 +1 @@ +{"version":3,"file":"Css/DlfMediaPlayerStyles.css","mappings":";;;AAAA;;;;;;;;;EASE;ACTF;EACI;ADWJ;AEXI;EACI;EAEA;EACA;AFYR;AEhBI;EAOQ;EACA;EACA;AFYZ;AERI;EACI;EAEA;EACA;AFSR;AEbI;EAOQ;EACA;EACA;AFSZ;AElBI;EAYY;AFShB;AEnCA;EAgCQ;EACA;EACA;EACA;EACA;EACA;AFMR;AE3CA;EAyCQ;EACA;EACA;AFKR;AEHQ;EACI;EACA;EACA;AFKZ;AErDA;EAqDQ;AFGR;AEAI;EACI;AFER;AE3DA;EA6DQ;AFCR;AE9DA;EAiEQ;AFAR;AEIQ;;EAEI;AFFZ;AEMI;EACI;EACA;EAEA;AFLR;AECI;EAOQ;AFLZ;AEFI;EAWQ;AFNZ;AELI;EAgBQ;AFRZ;AERI;EAoBQ;AFTZ;AEXI;EAwBQ;EACA;AFVZ;AG1FA;EH4FE,4EAA4E;EIiB5E;EJfA,uEAAuE;EIkBvE;EJhBA;WACS;AACX;AIkBE;;EAEE;AJhBJ;AKjGA;EJCI;EDmGF,eAAe;EClGb;EDoGF,WAAW;ECnGT;EDqGF,mBAAmB;ECpGjB;EDsGF,4BAA4B;ECrG1B;EDuGF,2BAA2B;ECtGzB;EDwGF;wEACsE;EK3GpE;AL6GJ;AKzGI;EACI;AL2GR;AK/FA;EARQ;AL0GR;AKlGA;EAJQ;ALyGR;AKjGA;EACI;EACA;ALmGJ;AKhGA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;ALkGJ;AK/FI;EACI;ALiGR;AK7FA;EACI;AL+FJ;AK5FA;EACI;AL8FJ;AK3FA;EAGI;AL2FJ;AKxFA;EACI;IACI;IACA;EL0FN;EKvFE;IACI;ELyFN;EKtFE;IACI;IACA;ELwFN;AACF;AKrFA;EAGI;EACA;EACA;EACA;ALqFJ;AKnFI;EACI;ALqFR;AKjFA;EAEQ;ALkFR;AK/EI;;EAQQ;AL2EZ;AKnFI;EAYQ;AL0EZ;AKrEA;EJ9GI;EDsLF,eAAe;ECrLb;EDuLF,WAAW;ECtLT;EDwLF,mBAAmB;ECvLjB;EDyLF,4BAA4B;ECxL1B;ED0LF,2BAA2B;ECzLzB;ED2LF;wEACsE;EK9EpE;EACA;EAGA;EACA;EAGA;EAEA;EACA;EACA;AL2EJ;AK5FA;EAoBQ;AL2ER;AK/FA;EAwBQ;EACA;EACA;AL0ER;AKpGA;EA8BQ;EACA;EACA;EACA;EACA;EACA;EACA;EACA;ALyER;AKvEQ;EACI;ALyEZ;AKjHA;EA4CY;EACA;EACA;EACA;ALwEZ;AKvHA;EAoDQ;EAEA;EACA;EACA;EACA;ALqER;AK9HA;EA4DY;EACA;EACA;ALqEZ;AKnIA;EAkEY;ALoEZ;AKjEQ;EACI;ALmEZ;AK9DA;EAGI;AL8DJ;AKtDA;EACI;IACI;IACA;IACA;IACA;ELwDN;EMrQF;INuQI,4EAA4E;II5J9E;IJ8JE,uEAAuE;II3JzE;IJ6JE;WACO;IC9PP;IKTA;IACA;IACA;IACA;IACA;IACA;EN0QF;EIlKA;;IAEE;EJoKF;EMxRF;IAaQ;IACA;IACA;IACA;EN8QN;EM9RF;IAoBQ;IACA;IACA;IACA;IACA;IACA;IACA;EN6QN;EMvSF;IA8BQ;IACA;IACA;IACA;IACA;IACA;IACA;EN4QN;AACF;AK9FA;EACI;IACI;IACA;IACA;IACA;ELgGN;EMxTF;IN0TI,4EAA4E;II/M9E;IJiNE,uEAAuE;II9MzE;IJgNE;WACO;ICjTP;IKTA;IACA;IACA;IACA;IACA;IACA;EN6TF;EIrNA;;IAEE;EJuNF;EM3UF;IAaQ;IACA;IACA;IACA;ENiUN;EMjVF;IAoBQ;IACA;IACA;IACA;IACA;IACA;IACA;ENgUN;EM1VF;IA8BQ;IACA;IACA;IACA;IACA;IACA;IACA;EN+TN;AACF;AOtWA;EACI;APwWJ;AOzWA;EAIQ;APwWR;AO5WA;EAOY;APwWZ;AO/WA;;EAWgB;EACA;EACA;APwWhB;AOrXA;EAmBQ;EACA;APqWR;AOzXA;EAuBY;APqWZ;AOlWQ;EACI;APoWZ;AO/XA;EAgCQ;APkWR;AOlYA;EAmCY;EACA;EACA;EACA;EACA;EACA;APkWZ;AO1YA;EA8CY;EACA;AP+VZ;AO7VY;EACI;AP+VhB;AOjZA;EAuDY;AP6VZ;AQpZA;EACI;EAGA;EACA;EACA;ARoZJ;AQ1ZA;EASQ;ARoZR;AQ7ZA;EAaQ;EACA;ARmZR;AQjZQ;EACI;ARmZZ;AQpaA;EAsBQ;EACA;EACA;ARiZR;AQzaA;EA2BY;EACA;EACA;ARiZZ;AQ9aA;EAiCY;ARgZZ;AQjbA;;EAsCY;AR+YZ;AQrbA;;EA0CgB;AR+YhB;AQ1YY;EACI;AR4YhB;AQ5bA;EAoDgB;AR2YhB;AQ/bA;EAyDY;EACA;ARyYZ;AQncA;EA8DY;EACA;ARwYZ;ASvcA;EAEQ;EACA;EACA;EAEA;ATucR;AS7cA;EASY;EACA;ATucZ;ASjdA;EAagB;EACA;ATuchB;ASrdA;EAkBgB;ATschB;ASxdA;EAqBoB;EACA;EACA;ATscpB;AS7dA;EA4BgB;ATochB;ASheA;EAgCgB;ATmchB;ASneA;EAoCgB;ATkchB;ASteA;EAyCY;EACA;EAEA;EACA;EACA;AT+bZ;AS7eA;EAkDY;EACA;EAEA;EACA;EACA;AT6bZ;ASzbI;EAAA;IAEQ;ET2bV;AACF;ASxbI;EAAA;IAEQ;ET0bV;AACF;ASvbI;EAAA;IAEQ;ETybV;ES3bE;IAMQ;ETwbV;AACF;AUtgBA;EAEQ;AVugBR;AUzgBA;EAMQ;AVsgBR;AU5gBA;EAWY;EACA;AVogBZ;AUhhBA;EAgBY;AVmgBZ;AUnhBA;;EAoBY;AVmgBZ;AUvhBA;EAwBY;AVkgBZ;AU1hBA;EA4BY;AVigBZ;AU7hBA;EAgCY;AVggBZ;AUhiBA;EAmCgB;EACA;EACA;AVggBhB;AU9fgB;EACI;AVggBpB;AU7fgB;EACI;AV+fpB;AU3iBA;;EAmDY;AV4fZ;AU/iBA;EAuDY;EACA;AV2fZ;AUnjBA;EA4DY;EACA;EACA;EACA;AV0fZ;AUzjBA;EAmEY;AVyfZ;AUrfI;EAEQ;AVsfZ;AUxfI;EAMQ;AVqfZ;AU3fI;ETnDA;EACA;EACA;EACA;EACA;EACA;EACA;ADijBJ;AK9jBI;EACI;ALgkBR;AWrkBA;ENSQ;AL+jBR;AWxkBA;ENaQ;AL8jBR;AW3kBA;EAIQ;AX0kBR;AWtkBA;;;;;EAMQ;AXukBR;AWnkBA;EAEI;AXokBJ;AWtkBA;EAQQ;EACA;AXikBR;AW7jBA;EASI;EACA;EACA;EAEA;AXsjBJ;AWlkBI;EACI;AXokBR;AWrkBI;EAIQ;AXokBZ;AW1jBI;EACI;AX4jBR;AWzjBI;EACI;AX2jBR;AW/kBA;EAwBQ;EACA;EACA;EACA;AX0jBR;AWrlBA;EA+BQ;AXyjBR;AWpjBI;EAAA;IACI;EXujBN;AACF;AW1jBA;EAMQ;AXujBR;AW7jBA;EAUQ;AXsjBR;AWljBA;EACI;EACA;AXojBJ;AWjjBA;EACI;AXmjBJ;AWhjBA;;;;EAII;AXkjBJ;AW/iBA;EACI;;;;IAII;EXijBN;EW9iBE;;IAEI;EXgjBN;AACF;AW7iBA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AX+iBJ;AW7iBI;EACI;EACA;EACA;AX+iBR;AW3iBA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;AX4iBJ;AW1iBI;EAAA;IACI;IACA;IACA;EX6iBN;AACF;AWhkBA;EVrHI;EACA;EACA;EACA;EACA;EACA;EACA;EUsII;AXmjBR;AW1kBA;EA2BQ;EACA;EACA;EACA;EACA;AXkjBR;AWjlBA;EAmCQ;EACA;AXijBR;AWrlBA;EAuCY;EACA;EACA;EACA;AXijBZ;AW3lBA;EA+CQ;EACA;AX+iBR;AW/lBA;EAqDQ;AX6iBR","sources":["webpack://kitodo-presentation/../Resources/Private/Less/DlfMediaPlayer.less","webpack://kitodo-presentation/../Resources/Private/Less/DlfMediaPlayer/util.less","webpack://kitodo-presentation/../Resources/Private/Less/DlfMediaPlayer/ShakaFrontend.less","webpack://kitodo-presentation/../Resources/Private/Less/DlfMediaPlayer/WaveForm.less","webpack://kitodo-presentation/./node_modules/shaka-player/ui/less/general.less","webpack://kitodo-presentation/../Resources/Private/Less/DlfMediaPlayer/DlfMediaPlayer.less","webpack://kitodo-presentation/../Resources/Private/Less/DlfMediaPlayer/FlatSeekBar.less","webpack://kitodo-presentation/../Resources/Private/Less/SlubMediaPlayer/modals/BookmarkModal.less","webpack://kitodo-presentation/../Resources/Private/Less/SlubMediaPlayer/modals/HelpModal.less","webpack://kitodo-presentation/../Resources/Private/Less/SlubMediaPlayer/modals/ScreenshotModal.less","webpack://kitodo-presentation/../Resources/Private/Less/SlubMediaPlayer/components/MarkerTable.less","webpack://kitodo-presentation/../Resources/Private/Less/SlubMediaPlayer/SlubMediaPlayer.less"],"sourcesContent":["/*\n *\n * Variables\n * ================================================\n * Value settings for type, breakpoints and\n * base settings for calculations\n *\n * Author: Thomas Jung \n *\n */\n.dlf-visible {\n visibility: visible !important;\n}\n.dlf-shaka[data-mode=\"video\"] {\n --controls-color: white;\n --volume-base-color: rgba(255, 255, 255, 0.54);\n --volume-level-color: rgba(255, 255, 255);\n}\n.dlf-shaka[data-mode=\"video\"] .dlf-media-flat-seek-bar {\n --base-color: rgba(255, 255, 255, 0.3);\n --buffered-color: rgba(255, 255, 255, 0.54);\n --played-color: #ffffff;\n}\n.dlf-shaka[data-mode=\"audio\"] {\n --controls-color: #2a2b2c;\n --volume-base-color: rgba(0, 0, 0, 0.4);\n --volume-level-color: rgba(0, 0, 0, 0.8);\n}\n.dlf-shaka[data-mode=\"audio\"] .dlf-media-flat-seek-bar {\n --base-color: rgba(0, 0, 0, 0.3);\n --buffered-color: rgba(0, 0, 0, 0.54);\n --played-color: #2a2b2c;\n}\n.dlf-shaka[data-mode=\"audio\"] .dlf-media-flat-seek-bar .dlf-media-chapter-marker {\n background-color: #c3e5ec;\n}\n.dlf-shaka .dlf-media-shaka-box {\n background-color: black;\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n.dlf-shaka .dlf-media-error {\n display: none;\n color: white;\n z-index: 1;\n}\n.dlf-shaka .dlf-media-error.dlf-visible {\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.dlf-shaka .shaka-scrim-container {\n display: none;\n}\n.dlf-shaka[data-mode=\"\"] {\n display: none;\n}\n.dlf-shaka .dlf-media-error {\n color: var(--controls-color);\n}\n.dlf-shaka .shaka-controls-button-panel > * {\n color: var(--controls-color) !important;\n}\n.dlf-shaka .shaka-volume-bar::-webkit-slider-thumb,\n.dlf-shaka .shaka-volume-bar::-moz-range-thumb {\n background: var(--volume-level-color);\n}\n.dlf-shaka[data-mode=\"audio\"] {\n height: 3.5em;\n width: 100%;\n background-color: rgba(79, 179, 199, 0.6);\n}\n.dlf-shaka[data-mode=\"audio\"] video {\n display: none;\n}\n.dlf-shaka[data-mode=\"audio\"] .dlf-media-poster {\n display: none !important;\n}\n.dlf-shaka[data-mode=\"audio\"] .dlf-media-shaka-box {\n background-color: transparent;\n}\n.dlf-shaka[data-mode=\"audio\"] .shaka-spinner-container {\n display: none;\n}\n.dlf-shaka[data-mode=\"audio\"] .shaka-bottom-controls {\n width: 100%;\n padding-bottom: 0.4em;\n}\n.shaka-bottom-controls dlf-waveform {\n /* Transparent unless explicitly made opaque through container attributes. */\n opacity: 0;\n /* When we show/hide this, do it gradually using cubic-bezier timing. */\n transition: opacity cubic-bezier(0.4, 0, 0.6, 1) 600ms;\n /* Show controls when the container's \"shown\" or \"casting\" attributes are\n * set. */\n}\n.shaka-controls-container[shown=\"true\"] .shaka-bottom-controls dlf-waveform,\n.shaka-controls-container[casting=\"true\"] .shaka-bottom-controls dlf-waveform {\n opacity: 1;\n}\n.dlf-media-player {\n -webkit-touch-callout: none;\n /* iOS Safari */\n -webkit-user-select: none;\n /* Safari */\n -khtml-user-select: none;\n /* Konqueror HTML */\n -moz-user-select: none;\n /* Old versions of Firefox */\n -ms-user-select: none;\n /* Internet Explorer/Edge */\n user-select: none;\n /* Non-prefixed version, currently\n supported by Chrome, Edge, Opera and Firefox */\n position: relative;\n}\ndlf-media:not(:defined) {\n display: none;\n}\ndlf-media dlf-chapter {\n display: none;\n}\ndlf-media dlf-media-controls {\n display: none;\n}\n.dlf-media {\n width: 100%;\n height: 100%;\n}\n.dlf-media-poster {\n display: none;\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n object-fit: contain;\n background-color: black;\n}\n.dlf-media-poster[src].dlf-visible {\n display: block;\n}\n.shaka-video-container {\n height: 100%;\n}\n.shaka-bottom-controls {\n visibility: hidden;\n}\n.shaka-controls-button-panel {\n justify-content: flex-start !important;\n}\n@media all and (min-width: calc(768px + 1px)) {\n .shaka-controls-button-panel button {\n margin-left: 8px;\n margin-right: 8px;\n }\n .shaka-overflow-menu-button {\n margin-left: 0 !important;\n }\n .shaka-fullscreen-button {\n margin-left: 3px !important;\n margin-right: 4px !important;\n }\n}\n.shaka-current-time {\n flex-shrink: 1 !important;\n white-space: nowrap;\n overflow-x: scroll;\n position: relative;\n}\n.shaka-current-time::-webkit-scrollbar {\n display: none;\n}\nbody .shaka-video-container {\n touch-action: none !important;\n}\nbody.seek-or-scrub .shaka-play-button,\nbody.seek-or-scrub .shaka-controls-button-panel {\n pointer-events: none;\n}\nbody.seek-or-scrub * {\n cursor: grabbing !important;\n}\n.dlf-media-thumbnail-preview {\n -webkit-touch-callout: none;\n /* iOS Safari */\n -webkit-user-select: none;\n /* Safari */\n -khtml-user-select: none;\n /* Konqueror HTML */\n -moz-user-select: none;\n /* Old versions of Firefox */\n -ms-user-select: none;\n /* Internet Explorer/Edge */\n user-select: none;\n /* Non-prefixed version, currently\n supported by Chrome, Edge, Opera and Firefox */\n visibility: hidden;\n position: absolute;\n bottom: 0px;\n padding-bottom: 25px;\n padding-top: 50px;\n z-index: 1;\n cursor: pointer;\n text-align: center;\n}\n.dlf-media-thumbnail-preview .displayed {\n display: block !important;\n}\n.dlf-media-thumbnail-preview .content-box {\n background-color: rgba(23, 30, 30, 0.6);\n border-radius: 4px;\n padding: 1em;\n}\n.dlf-media-thumbnail-preview .display {\n display: none;\n position: relative;\n width: 160px;\n height: 90px;\n border: 1px solid white;\n box-sizing: content-box;\n margin-bottom: 0.75em;\n overflow: hidden;\n}\n.dlf-media-thumbnail-preview .display.is-open {\n display: block;\n}\n.dlf-media-thumbnail-preview .display img {\n visibility: hidden;\n position: absolute;\n top: 0;\n left: 0;\n}\n.dlf-media-thumbnail-preview .info {\n display: inline-block;\n color: white;\n font-size: 14px;\n line-height: 110%;\n max-width: 160px;\n}\n.dlf-media-thumbnail-preview .info .chapter-text {\n line-height: 110%;\n margin-bottom: 0.4em;\n display: none;\n}\n.dlf-media-thumbnail-preview .info .timecode-text {\n display: block;\n}\n.dlf-media-thumbnail-preview .info.on-chapter-marker .chapter-text {\n font-weight: bold;\n}\n.dlf-media-chapter-marker {\n z-index: 0;\n}\n@media all and (pointer: fine) {\n .dlf-media-chapter-marker {\n width: 4px;\n height: 4px;\n background-color: #d5dfdf;\n border-radius: 2px;\n }\n .dlf-media-flat-seek-bar {\n /* Transparent unless explicitly made opaque through container attributes. */\n opacity: 0;\n /* When we show/hide this, do it gradually using cubic-bezier timing. */\n transition: opacity cubic-bezier(0.4, 0, 0.6, 1) 600ms;\n /* Show controls when the container's \"shown\" or \"casting\" attributes are\n * set. */\n -webkit-tap-highlight-color: transparent;\n touch-action: none;\n position: relative;\n margin: 0px 6px;\n padding-top: 6px;\n height: 16px;\n cursor: pointer;\n }\n .shaka-controls-container[shown=\"true\"] .dlf-media-flat-seek-bar,\n .shaka-controls-container[casting=\"true\"] .dlf-media-flat-seek-bar {\n opacity: 1;\n }\n .dlf-media-flat-seek-bar .range {\n position: absolute;\n width: 100%;\n height: 4px;\n border-radius: 4px;\n }\n .dlf-media-flat-seek-bar .seek-marker {\n position: absolute;\n left: 45%;\n height: 4px;\n width: 1px;\n background-color: #4e6666;\n z-index: -1;\n visibility: hidden;\n }\n .dlf-media-flat-seek-bar .seek-thumb-bar {\n position: absolute;\n left: 45%;\n height: 4px;\n width: 1px;\n background-color: rgba(78, 102, 102, 0.4);\n z-index: -2;\n visibility: hidden;\n }\n}\n@media all and (pointer: coarse) {\n .dlf-media-chapter-marker {\n width: 1px;\n height: 5px;\n margin-top: 1px;\n background-color: rgba(213, 223, 223, 0.3);\n }\n .dlf-media-flat-seek-bar {\n /* Transparent unless explicitly made opaque through container attributes. */\n opacity: 0;\n /* When we show/hide this, do it gradually using cubic-bezier timing. */\n transition: opacity cubic-bezier(0.4, 0, 0.6, 1) 600ms;\n /* Show controls when the container's \"shown\" or \"casting\" attributes are\n * set. */\n -webkit-tap-highlight-color: transparent;\n touch-action: none;\n position: relative;\n margin: 0px 6px;\n padding-top: 8px;\n height: 23px;\n cursor: pointer;\n }\n .shaka-controls-container[shown=\"true\"] .dlf-media-flat-seek-bar,\n .shaka-controls-container[casting=\"true\"] .dlf-media-flat-seek-bar {\n opacity: 1;\n }\n .dlf-media-flat-seek-bar .range {\n position: absolute;\n width: 100%;\n height: 7px;\n border-radius: 7px;\n }\n .dlf-media-flat-seek-bar .seek-marker {\n position: absolute;\n left: 45%;\n height: 7px;\n width: 1px;\n background-color: #4e6666;\n z-index: -1;\n visibility: hidden;\n }\n .dlf-media-flat-seek-bar .seek-thumb-bar {\n position: absolute;\n left: 45%;\n height: 7px;\n width: 1px;\n background-color: rgba(78, 102, 102, 0.4);\n z-index: -2;\n visibility: hidden;\n }\n}\n.bookmark-modal {\n text-align: left;\n}\n.bookmark-modal .share-buttons {\n margin-bottom: 1em;\n}\n.bookmark-modal .share-buttons a {\n margin-right: 1em;\n}\n.bookmark-modal .share-buttons a img,\n.bookmark-modal .share-buttons a .material-icons-round {\n width: 40px;\n height: 40px;\n font-size: 40px;\n}\n.bookmark-modal .url-qrcode {\n padding-top: 1em;\n display: none;\n}\n.bookmark-modal .url-qrcode hr {\n margin-bottom: 1.4em;\n}\n.bookmark-modal .url-qrcode.dlf-visible {\n display: block;\n}\n.bookmark-modal .url-line {\n display: flex;\n}\n.bookmark-modal .url-line input {\n width: 90%;\n height: 2rem;\n font-size: 1rem;\n line-height: 1rem;\n color: #4e6666;\n margin-right: 4pt;\n}\n.bookmark-modal .start-at div {\n display: none;\n margin-top: 1em;\n}\n.bookmark-modal .start-at div.shown {\n display: block;\n}\n.bookmark-modal .start-at label {\n padding-left: 2pt;\n}\n.help-modal {\n width: auto;\n left: 50% !important;\n right: unset !important;\n transform: translateX(-50%) translateY(-50%) !important;\n}\n.help-modal .body-container {\n padding: 0 1em !important;\n}\n.help-modal .subheader {\n text-align: left;\n font-weight: bold;\n}\n.help-modal .subheader:not(:nth-child(1)) {\n margin-top: 2em;\n}\n.help-modal .keybindings-table {\n line-height: 1.5rem;\n font-size: 80%;\n white-space: nowrap;\n}\n.help-modal .keybindings-table th.kb-group {\n padding-left: 0.2em;\n text-align: left;\n font-weight: bold;\n}\n.help-modal .keybindings-table tbody tr[aria-disabled=\"true\"] {\n opacity: 0.4;\n}\n.help-modal .keybindings-table thead[aria-disabled=\"true\"],\n.help-modal .keybindings-table tbody[aria-disabled=\"true\"] {\n opacity: 0.4;\n}\n.help-modal .keybindings-table thead[aria-disabled=\"true\"] tr,\n.help-modal .keybindings-table tbody[aria-disabled=\"true\"] tr {\n opacity: 1;\n}\n.help-modal .keybindings-table tbody tr:first-child {\n border-top: 1px solid #d5dfdf;\n}\n.help-modal .keybindings-table tbody tr td {\n color: white;\n}\n.help-modal .keybindings-table td.key {\n width: 40%;\n text-align: right;\n}\n.help-modal .keybindings-table td.action {\n padding-left: 3em;\n text-align: left;\n}\n.screenshot-modal .body-container {\n display: grid;\n grid-template-columns: 3fr 1fr;\n column-gap: 1em;\n text-align: left;\n}\n.screenshot-modal .body-container .screenshot-config {\n grid-column: 2;\n grid-row: 1;\n}\n.screenshot-modal .body-container .screenshot-config h4 {\n margin-bottom: 0.6em;\n font-size: 120%;\n}\n.screenshot-modal .body-container .screenshot-config section {\n margin-bottom: 0.8em;\n}\n.screenshot-modal .body-container .screenshot-config section h1 {\n font-weight: bold;\n font-size: 100%;\n margin-bottom: 0.3em;\n}\n.screenshot-modal .body-container .screenshot-config .metadata-overlay label {\n padding-left: 2pt;\n}\n.screenshot-modal .body-container .screenshot-config .file-format-option {\n margin-left: 0.3em;\n}\n.screenshot-modal .body-container .screenshot-config .download-link {\n margin-top: 0.2em;\n}\n.screenshot-modal .body-container .snap-tip {\n display: flex;\n gap: 0.4em;\n margin-top: 2em;\n line-height: 1.25;\n color: #d5dfdf;\n}\n.screenshot-modal .body-container canvas {\n grid-column: 1;\n grid-row: 1;\n background-color: black;\n width: 100%;\n height: auto;\n}\n@media screen and (max-width: 1600px) {\n .screenshot-modal .body-container {\n grid-template-columns: 2fr 1fr;\n }\n}\n@media screen and (max-width: 1200px) {\n .screenshot-modal .body-container {\n grid-template-columns: 1fr 1fr;\n }\n}\n@media screen and (max-width: 480px) {\n .screenshot-modal .body-container {\n display: block;\n }\n .screenshot-modal canvas {\n display: none;\n }\n}\n.dlf-media-markers h2 {\n margin-bottom: 0.4em;\n}\n.dlf-media-markers .dlf-media-markers-empty-msg {\n display: none;\n}\n.dlf-media-markers .dlf-media-markers-list table {\n margin-top: 0.4em;\n line-height: normal;\n}\n.dlf-media-markers .dlf-media-markers-list th {\n border-bottom: 1px solid black;\n}\n.dlf-media-markers .dlf-media-markers-list td,\n.dlf-media-markers .dlf-media-markers-list th {\n padding: 0.1em 2em 0.1em 0;\n}\n.dlf-media-markers .dlf-media-markers-list tbody tr:hover {\n background-color: #eee;\n}\n.dlf-media-markers .dlf-media-markers-list tr.active-segment {\n color: darkcyan;\n}\n.dlf-media-markers .dlf-media-markers-list .marker-id-col {\n width: 50%;\n}\n.dlf-media-markers .dlf-media-markers-list .marker-id-col input {\n font: inherit;\n background: transparent;\n border: none;\n}\n.dlf-media-markers .dlf-media-markers-list .marker-id-col input:focus {\n outline: none;\n}\n.dlf-media-markers .dlf-media-markers-list .marker-id-col input::placeholder {\n font-style: italic;\n}\n.dlf-media-markers .dlf-media-markers-list .marker-start-col,\n.dlf-media-markers .dlf-media-markers-list .marker-end-col {\n cursor: pointer;\n}\n.dlf-media-markers .dlf-media-markers-list .marker-buttons-col {\n width: 0;\n white-space: nowrap;\n}\n.dlf-media-markers .dlf-media-markers-list button {\n cursor: pointer;\n background: transparent;\n border: none;\n outline: none;\n}\n.dlf-media-markers .dlf-media-markers-list .material-icons-round {\n font-size: 20px;\n}\n.dlf-media-markers.is-empty .dlf-media-markers-empty-msg {\n display: block;\n}\n.dlf-media-markers.is-empty .dlf-media-markers-list {\n display: none;\n}\n.dlf-media-markers.is-empty kbd {\n white-space: pre;\n font-family: monospace;\n font-weight: bold;\n background-color: rgba(238, 238, 238, 0.3);\n border-radius: 3px;\n border: 1px solid #b4b4b4;\n padding: 4px 7px;\n}\nslub-media:not(:defined) {\n display: none;\n}\nslub-media dlf-chapter {\n display: none;\n}\nslub-media dlf-media-controls {\n display: none;\n}\nslub-media dlf-meta {\n display: none;\n}\nbody[data-has-video] .page-control,\nbody[data-has-video] .document-functions li.doublepage,\nbody[data-has-video] .view-functions li.rotate,\nbody[data-has-video] .view-functions li.zoom .in,\nbody[data-has-video] .view-functions li.zoom .out {\n display: none;\n}\n.control-bar {\n border-right: none !important;\n}\n.control-bar .offcanvas-toggle {\n bottom: unset;\n top: 5px;\n}\n.combined-container {\n position: absolute;\n width: 100%;\n height: 100%;\n display: grid;\n}\n.combined-container:fullscreen {\n background-color: white;\n}\n.combined-container:fullscreen .media-panel {\n padding: 1em 1em;\n}\n.combined-container[data-mode=\"audio\"] {\n grid-template-rows: 1fr auto;\n}\n.combined-container[data-mode=\"video\"] {\n grid-template-rows: 0 1fr;\n}\n.combined-container .media-panel {\n grid-row: 1;\n margin: 6em 1em;\n text-align: left;\n overflow-y: scroll;\n}\n.combined-container .tx-dlf-view {\n grid-row: 2;\n}\n@media screen and (max-width: 1023px) {\n .document-view {\n top: 50px;\n }\n}\n.document-view .tx-dlf-view {\n position: relative;\n}\n.document-view .media-viewport {\n height: 100%;\n}\n.dlf-media-player {\n width: 100%;\n height: 100%;\n}\n.inline-icon {\n vertical-align: middle;\n}\n.sxnd-waveform-button,\n.sxnd-screenshot-button,\n.sxnd-bookmark-button,\n.sxnd-help-button {\n font-size: 22px !important;\n}\n@media all and (max-width: 768px) {\n .sxnd-waveform-button,\n .sxnd-screenshot-button,\n .sxnd-bookmark-button,\n .sxnd-help-button {\n display: none !important;\n }\n .shaka-volume-bar-container,\n .shaka-mute-button {\n display: none !important;\n }\n}\n.sxnd-modal-cover {\n visibility: hidden;\n position: absolute;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n z-index: 100000;\n background-color: black;\n opacity: 0;\n}\n.sxnd-modal-cover.shown {\n visibility: visible;\n opacity: 0.33;\n transition: opacity 200ms linear;\n}\n.sxnd-modal {\n position: absolute;\n display: none;\n top: 50%;\n -ms-transform: translateY(-50%);\n transform: translateY(-50%);\n background-color: rgba(78, 102, 102, 0.9);\n color: white;\n border-radius: 5px;\n z-index: 999999;\n padding: 2rem 1.5rem 2rem 0.6rem;\n left: 1rem;\n right: 1rem;\n}\n@media screen and (min-width: 1200px) {\n .sxnd-modal {\n padding: 3rem;\n left: 5rem;\n right: 5rem;\n }\n}\n.sxnd-modal kbd {\n white-space: pre;\n font-family: monospace;\n font-weight: bold;\n background-color: rgba(238, 238, 238, 0.3);\n border-radius: 3px;\n border: 1px solid #b4b4b4;\n padding: 4px 7px;\n color: white;\n}\n.sxnd-modal button {\n background-color: rgba(255, 255, 255, 0.8);\n border: 1px solid black;\n padding: 0.5em 1em;\n border-radius: 4px;\n font-size: 100%;\n}\n.sxnd-modal .headline-container {\n position: relative;\n padding-bottom: 1rem;\n}\n.sxnd-modal .headline-container .modal-close {\n position: absolute;\n top: 0;\n right: 0;\n cursor: pointer;\n}\n.sxnd-modal .body-container {\n padding: 2rem;\n overflow-y: auto;\n}\n.sxnd-modal h3 {\n font-weight: 700;\n}\n",".dlf-visible {\n visibility: visible !important;\n}\n\n// Thanks https://stackoverflow.com/a/4407335\n.noselect() {\n -webkit-touch-callout: none; /* iOS Safari */\n -webkit-user-select: none; /* Safari */\n -khtml-user-select: none; /* Konqueror HTML */\n -moz-user-select: none; /* Old versions of Firefox */\n -ms-user-select: none; /* Internet Explorer/Edge */\n user-select: none; /* Non-prefixed version, currently\n supported by Chrome, Edge, Opera and Firefox */\n}\n\n.no-tap-highlight() {\n -webkit-tap-highlight-color: transparent;\n}\n\n.kbd() {\n white-space: pre; // Allow marking space bar with a couple of spaces\n font-family: monospace;\n font-weight: bold;\n background-color: rgba(#eee, 0.3);\n border-radius: 3px;\n border: 1px solid #b4b4b4;\n padding: 4px 7px;\n}\n",".dlf-shaka {\n &[data-mode=\"video\"] {\n --controls-color: white;\n\n --volume-base-color: rgba(255, 255, 255, 0.54);\n --volume-level-color: rgba(255, 255, 255);\n\n .dlf-media-flat-seek-bar {\n --base-color: rgba(255, 255, 255, 0.3);\n --buffered-color: rgba(255, 255, 255, 0.54);\n --played-color: rgb(255, 255, 255);\n }\n }\n\n &[data-mode=\"audio\"] {\n --controls-color: #2a2b2c;\n\n --volume-base-color: rgba(0, 0, 0, 0.4);\n --volume-level-color: rgba(0, 0, 0, 0.8);\n\n .dlf-media-flat-seek-bar {\n --base-color: rgba(0, 0, 0, 0.3);\n --buffered-color: rgba(0, 0, 0, 0.54);\n --played-color: #2a2b2c;\n\n .dlf-media-chapter-marker {\n background-color: lighten(rgb(79, 179, 199), 30%);\n }\n }\n }\n\n .dlf-media-shaka-box {\n background-color: black;\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n }\n\n .dlf-media-error {\n display: none;\n color: white;\n z-index: 1;\n\n &.dlf-visible {\n display: flex;\n justify-content: center;\n align-items: center;\n }\n }\n\n .shaka-scrim-container {\n display: none;\n }\n\n &[data-mode=\"\"] {\n display: none;\n }\n\n .dlf-media-error {\n color: var(--controls-color);\n }\n\n .shaka-controls-button-panel > * {\n color: var(--controls-color) !important;\n }\n\n .shaka-volume-bar {\n &::-webkit-slider-thumb,\n &::-moz-range-thumb {\n background: var(--volume-level-color);\n }\n }\n\n &[data-mode=\"audio\"] {\n height: 3.5em;\n width: 100%;\n\n background-color: rgba(79, 179, 199, 0.6);\n\n video {\n display: none;\n }\n\n .dlf-media-poster {\n display: none !important;\n }\n\n // Use background color from parent\n .dlf-media-shaka-box {\n background-color: transparent;\n }\n\n .shaka-spinner-container {\n display: none;\n }\n\n .shaka-bottom-controls {\n width: 100%;\n padding-bottom: 0.4em;\n }\n }\n}\n",".shaka-bottom-controls dlf-waveform {\n .show-when-controls-shown();\n}\n","/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/* General utility mixins and classes with broad applicability. */\n\n/* Make a thing unselectable. There are currently no cases where we make it\n * selectable again. */\n.unselectable() {\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n}\n\n.hidden() {\n display: none;\n}\n\n.shaka-hidden {\n /* Make this override equally specific classes.\n * If it's hidden, always hide it! */\n display: none !important;\n}\n\n.fill-container() {\n width: 100%;\n height: 100%;\n}\n\n.bottom-align-children() {\n display: flex;\n justify-content: flex-end;\n flex-direction: column;\n}\n\n.bottom-panels-elements-margin() {\n margin: 1px 6px;\n}\n\n/* For containers which host elements overlaying other things. */\n.overlay-parent() {\n /* For a detailed explanation of how this achieves an overlay, please refer\n * to https://developer.mozilla.org/en-US/docs/Web/CSS/position .\n *\n * But you don't have to, because we've encapsulated these high level\n * concepts into classes.\n *\n * This makes it possible for some children of this container to overlay the\n * others using .overlay-child(). */\n position: relative;\n\n /* Make sure any top or left styles applied from outside don't move this from\n * it's original position, now that it's relative to that original position.\n * This is a defensive move that came out of intensive debugging on IE 11. */\n top: 0;\n left: 0;\n}\n\n/* For things which overlay other things. */\n.overlay-child() {\n /* For a detailed explanation of how this achieves an overlay, please refer\n * to https://developer.mozilla.org/en-US/docs/Web/CSS/position .\n *\n * But you don't have to, because we've encapsulated these high level\n * concepts into classes.\n *\n * This makes it possible for this child to overlay the other children of a\n * .overlay-parent() object. */\n position: absolute;\n\n /* Fill the container by default. */\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n margin: 0;\n padding: 0;\n\n .fill-container();\n}\n\n.absolute-position() {\n /* When setting \"position: absolute\" it uses the left,right,top,bottom\n * properties to determine the positioning. We should set all these\n * properties to ensure it is positioned properly on all platforms. */\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n}\n\n/* For things that should not shrink inside a flex container.\n * This will be used for all controls by default. */\n.unshrinkable() {\n flex-shrink: 0;\n}\n\n/* Use this to override .unshrinkable() in particular cases that *should* shrink\n * inside a flex container. */\n.shrinkable() {\n flex-shrink: 1;\n}\n\n.show-when-controls-shown() {\n /* Transparent unless explicitly made opaque through container attributes. */\n opacity: 0;\n\n /* When we show/hide this, do it gradually using cubic-bezier timing. */\n transition: opacity cubic-bezier(0.4, 0, 0.6, 1) 600ms;\n\n /* Show controls when the container's \"shown\" or \"casting\" attributes are\n * set. */\n .shaka-controls-container[shown=\"true\"] &,\n .shaka-controls-container[casting=\"true\"] & {\n opacity: 1;\n }\n}\n\n.hide-when-shaka-controls-disabled() {\n .shaka-video-container:not([shaka-controls=\"true\"]) & {\n .hidden();\n }\n}\n\n/* The width of the bottom-section controls: seek bar, ad controls, and\nthe control buttons panel. */\n@bottom-controls-width: 96%;\n","@import (reference) \"shaka-player/ui/controls.less\";\n\n@import \"ShakaFrontend.less\";\n@import \"WaveForm.less\";\n\n.dlf-media-player {\n .noselect();\n\n // For height 100%, to make waveform usable (TODO?)\n position: relative;\n}\n\n.dlf-media-base() {\n &:not(:defined) {\n display: none;\n }\n\n dlf-chapter {\n display: none;\n }\n\n dlf-media-controls {\n display: none;\n }\n}\n\ndlf-media {\n .dlf-media-base();\n}\n\n.dlf-media {\n width: 100%;\n height: 100%;\n}\n\n.dlf-media-poster {\n display: none;\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n object-fit: contain;\n background-color: black;\n\n // Show poster only if src is set\n &[src].dlf-visible {\n display: block;\n }\n}\n\n.shaka-video-container {\n height: 100%;\n}\n\n.shaka-bottom-controls {\n visibility: hidden;\n}\n\n.shaka-controls-button-panel {\n // Prefer showing play button rather than fullscreen/overflow menu buttons\n // on tiny screens\n justify-content: flex-start !important;\n}\n\n@media all and (min-width: calc(@tabletViewportWidth + 1px)) {\n .shaka-controls-button-panel button {\n margin-left: 8px;\n margin-right: 8px;\n }\n\n .shaka-overflow-menu-button {\n margin-left: 0 !important;\n }\n\n .shaka-fullscreen-button {\n margin-left: 3px !important;\n margin-right: 4px !important;\n }\n}\n\n.shaka-current-time {\n // On small screens, let the time tracker adapt\n\n flex-shrink: 1 !important;\n white-space: nowrap;\n overflow-x: scroll;\n position: relative;\n\n &::-webkit-scrollbar {\n display: none;\n }\n}\n\nbody {\n .shaka-video-container {\n touch-action: none !important;\n }\n\n &.seek-or-scrub {\n // Don't let the big play button hinder scrubbing,\n // and make sure there are no tooltips when scrubbing.\n //\n // NOTE: We cannot just use a wildcard selector as that would lead to\n // issues on mobile (pointercancel when moving mouse vertically).\n .shaka-play-button,\n .shaka-controls-button-panel {\n pointer-events: none;\n }\n\n * {\n cursor: grabbing !important;\n }\n }\n}\n\n.dlf-media-thumbnail-preview {\n .noselect();\n\n // Use `visibility` instead of `display` because we want to use\n // `offsetWidth`, which is zero for elements with `display: none`.\n visibility: hidden;\n position: absolute;\n // Position 25px above seekbar, but overlap to the bottom (so that the\n // mouse can switch between seekbar and thumbnail preview).\n bottom: 0px;\n padding-bottom: 25px;\n // Allow to move mouse a little over the thumbnail preview without\n // closing it.\n padding-top: 50px;\n // Don't be shadowed by Shaka controlbar\n z-index: 1;\n cursor: pointer;\n text-align: center;\n\n .displayed {\n display: block !important;\n }\n\n .content-box {\n background-color: rgba(darken(@base-color, 25%), 0.6);\n border-radius: 4px;\n padding: 1em;\n }\n\n .display {\n display: none;\n position: relative;\n width: 160px;\n height: 90px;\n border: 1px solid white;\n box-sizing: content-box;\n margin-bottom: 0.75em;\n overflow: hidden;\n\n &.is-open {\n display: block;\n }\n\n img {\n visibility: hidden;\n position: absolute;\n top: 0;\n left: 0;\n }\n }\n\n .info {\n display: inline-block;\n // Same color and font-size as `.shaka-current-time`\n color: white;\n font-size: 14px;\n line-height: 110%;\n max-width: 160px;\n\n .chapter-text {\n line-height: 110%;\n margin-bottom: 0.4em;\n display: none;\n }\n\n .timecode-text {\n display: block;\n }\n\n &.on-chapter-marker .chapter-text {\n font-weight: bold;\n }\n }\n}\n\n.dlf-media-chapter-marker {\n // More than .seek-marker\n // Less than Shaka's tooltips\n z-index: 0;\n}\n\n@seek-bar-height: 4px;\n@seek-bar-margin: 6px;\n\n// Thanks https://stackoverflow.com/a/41585180 (Less variables in media query)\n// TODO: Use CSS variables?\n@media all and (pointer: fine) {\n .dlf-media-chapter-marker {\n width: @seek-bar-height;\n height: @seek-bar-height;\n background-color: @light-color;\n border-radius: @seek-bar-height / 2;\n }\n\n @import (multiple) \"./FlatSeekBar.less\";\n}\n\n@media all and (pointer: coarse) {\n .dlf-media-chapter-marker {\n width: 1px;\n height: @seek-bar-height - 2px;\n margin-top: 1px;\n background-color: rgba(@light-color, 0.3);\n }\n\n @seek-bar-height: 7px;\n @seek-bar-margin: 8px;\n\n @import (multiple) \"./FlatSeekBar.less\";\n}\n","// Imported from DlfMediaPlayer.less\n\n.dlf-media-flat-seek-bar {\n .show-when-controls-shown();\n .no-tap-highlight();\n\n // This lets us use pointer events for touch\n touch-action: none;\n position: relative;\n margin: 0px 6px;\n padding-top: @seek-bar-margin;\n height: @seek-bar-height + 2 * @seek-bar-margin;\n cursor: pointer;\n\n .range {\n position: absolute;\n width: 100%;\n height: @seek-bar-height;\n border-radius: @seek-bar-height;\n }\n\n .seek-marker {\n position: absolute;\n left: 45%;\n height: @seek-bar-height;\n width: 1px;\n background-color: @base-color;\n z-index: -1; // Less than .dlf-media-chapter-marker\n visibility: hidden;\n }\n\n .seek-thumb-bar {\n position: absolute;\n left: 45%;\n height: @seek-bar-height;\n width: 1px;\n background-color: rgba(@base-color, 0.4);\n z-index: -2; // Less than .dlf-media-chapter-marker and .seek-marker\n visibility: hidden;\n }\n}\n",".bookmark-modal {\n text-align: left;\n\n .share-buttons {\n margin-bottom: 1em;\n\n a {\n margin-right: 1em;\n\n img,\n .material-icons-round {\n width: 40px;\n height: 40px;\n font-size: 40px;\n }\n }\n }\n\n .url-qrcode {\n padding-top: 1em;\n display: none;\n\n hr {\n margin-bottom: 1.4em;\n }\n\n &.dlf-visible {\n display: block;\n }\n }\n\n .url-line {\n display: flex;\n\n input {\n width: 90%;\n height: 2rem;\n font-size: 1rem;\n line-height: 1rem;\n color: @base-color;\n margin-right: 4pt;\n }\n }\n\n .start-at {\n div {\n display: none;\n margin-top: 1em;\n\n &.shown {\n display: block;\n }\n }\n\n label {\n padding-left: @label-padding;\n }\n }\n}\n",".help-modal {\n width: auto;\n\n // Center horizontally (TODO: refactor transform into sxnd-modal)\n left: 50% !important;\n right: unset !important;\n transform: translateX(-50%) translateY(-50%) !important;\n\n .body-container {\n padding: 0 1em !important;\n }\n\n .subheader {\n text-align: left;\n font-weight: bold;\n\n &:not(:nth-child(1)) {\n margin-top: 2em;\n }\n }\n\n .keybindings-table {\n line-height: 1.5rem;\n font-size: 80%;\n white-space: nowrap;\n\n th.kb-group {\n padding-left: 0.2em;\n text-align: left;\n font-weight: bold;\n }\n\n tbody tr[aria-disabled=\"true\"] {\n opacity: 0.4;\n }\n\n thead[aria-disabled=\"true\"],\n tbody[aria-disabled=\"true\"] {\n opacity: 0.4;\n\n // Don't reduce opacity twice\n tr {\n opacity: 1;\n }\n }\n\n tbody tr {\n &:first-child {\n border-top: 1px solid @light-color;\n }\n\n td {\n color: white;\n }\n }\n\n td.key {\n width: 40%;\n text-align: right;\n }\n\n td.action {\n padding-left: 3em;\n text-align: left;\n }\n }\n}\n",".screenshot-modal {\n .body-container {\n display: grid;\n grid-template-columns: 3fr 1fr;\n column-gap: 1em;\n\n text-align: left;\n\n .screenshot-config {\n grid-column: 2;\n grid-row: 1;\n\n h4 {\n margin-bottom: 0.6em;\n font-size: 120%;\n }\n\n section {\n margin-bottom: 0.8em;\n\n h1 {\n font-weight: bold;\n font-size: 100%;\n margin-bottom: 0.3em;\n }\n }\n\n .metadata-overlay label {\n padding-left: @label-padding;\n }\n\n .file-format-option {\n margin-left: 0.3em;\n }\n\n .download-link {\n margin-top: 0.2em;\n }\n }\n\n .snap-tip {\n display: flex;\n gap: 0.4em;\n\n margin-top: 2em;\n line-height: 1.25;\n color: @light-color;\n }\n\n canvas {\n grid-column: 1;\n grid-row: 1;\n\n background-color: black;\n width: 100%;\n height: auto;\n }\n }\n\n @media screen and (max-width: 1600px) {\n .body-container {\n grid-template-columns: 2fr 1fr;\n }\n }\n\n @media screen and (max-width: @desktopViewportWidth) {\n .body-container {\n grid-template-columns: 1fr 1fr;\n }\n }\n\n @media screen and (max-width: @phoneLandscapeViewportWidth) {\n .body-container {\n display: block;\n }\n\n canvas {\n display: none;\n }\n }\n}\n",".dlf-media-markers {\n h2 {\n margin-bottom: 0.4em;\n }\n\n .dlf-media-markers-empty-msg {\n display: none;\n }\n\n .dlf-media-markers-list {\n table {\n margin-top: 0.4em;\n line-height: normal;\n }\n\n th {\n border-bottom: 1px solid black;\n }\n\n td, th {\n padding: 0.1em 2em 0.1em 0;\n }\n\n tbody tr:hover {\n background-color: #eee;\n }\n\n tr.active-segment {\n color: darkcyan;\n }\n\n .marker-id-col {\n width: 50%;\n\n input {\n font: inherit;\n background: transparent;\n border: none;\n\n &:focus {\n outline: none;\n }\n\n &::placeholder {\n font-style: italic;\n }\n }\n }\n\n .marker-start-col,\n .marker-end-col {\n cursor: pointer;\n }\n\n .marker-buttons-col {\n width: 0;\n white-space: nowrap;\n }\n\n button {\n cursor: pointer;\n background: transparent;\n border: none;\n outline: none;\n }\n\n .material-icons-round {\n font-size: 20px;\n }\n }\n\n &.is-empty {\n .dlf-media-markers-empty-msg {\n display: block;\n }\n\n .dlf-media-markers-list {\n display: none;\n }\n\n kbd {\n .kbd();\n }\n }\n}\n","@import \"modals/BookmarkModal.less\";\n@import \"modals/HelpModal.less\";\n@import \"modals/ScreenshotModal.less\";\n\n@import \"components/MarkerTable.less\";\n\n@zIndexModalCover: 100000;\n@zIndexModal: 999999;\n\nslub-media {\n .dlf-media-base();\n\n dlf-meta {\n display: none;\n }\n}\n\nbody[data-has-video] {\n .page-control,\n .document-functions li.doublepage,\n .view-functions li.rotate,\n .view-functions li.zoom .in,\n .view-functions li.zoom .out {\n display: none;\n }\n}\n\n.control-bar {\n // Avoid white border/separator between TOC and video\n border-right: none !important;\n\n // These buttons are used to toggle TOC and metadata on mobile (see\n // @slub_digitalcollections). Put them to the top so they won't overlap\n // with video controls.\n .offcanvas-toggle {\n bottom: unset;\n top: 5px;\n }\n}\n\n.combined-container {\n &:fullscreen {\n background-color: white;\n\n .media-panel {\n padding: 1em 1em;\n }\n }\n\n position: absolute;\n width: 100%;\n height: 100%;\n\n display: grid;\n\n &[data-mode=\"audio\"] {\n grid-template-rows: 1fr auto;\n }\n\n &[data-mode=\"video\"] {\n grid-template-rows: 0 1fr;\n }\n\n .media-panel {\n grid-row: 1;\n margin: 6em 1em;\n text-align: left;\n overflow-y: scroll;\n }\n\n .tx-dlf-view {\n grid-row: 2;\n }\n}\n\n.document-view {\n @media screen and (max-width: (@tabletLandscapeViewportWidth - 1px)) {\n top: 50px;\n }\n\n .tx-dlf-view {\n position: relative;\n }\n\n .media-viewport {\n height: 100%;\n }\n}\n\n.dlf-media-player {\n width: 100%;\n height: 100%;\n}\n\n.inline-icon {\n vertical-align: middle;\n}\n\n.sxnd-waveform-button,\n.sxnd-screenshot-button,\n.sxnd-bookmark-button,\n.sxnd-help-button {\n font-size: 22px !important;\n}\n\n@media all and (max-width: @tabletViewportWidth) {\n .sxnd-waveform-button,\n .sxnd-screenshot-button,\n .sxnd-bookmark-button,\n .sxnd-help-button {\n display: none !important;\n }\n\n .shaka-volume-bar-container,\n .shaka-mute-button {\n display: none !important;\n }\n}\n\n.sxnd-modal-cover {\n visibility: hidden;\n position: absolute;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n z-index: @zIndexModalCover;\n background-color: black;\n opacity: 0;\n\n &.shown {\n visibility: visible;\n opacity: 0.33;\n transition: opacity 200ms linear;\n }\n}\n\n.sxnd-modal {\n position: absolute;\n display: none;\n top: 50%;\n -ms-transform: translateY(-50%);\n transform: translateY(-50%);\n background-color: rgba(@base-color, 0.9);\n color: white;\n border-radius: 5px;\n z-index: @zIndexModal;\n\n padding: 2rem 1.5rem 2rem 0.6rem;\n left: 1rem;\n right: 1rem;\n\n @media screen and (min-width: @desktopViewportWidth) {\n padding: 3rem;\n left: 5rem;\n right: 5rem;\n }\n\n kbd {\n .kbd();\n color: white;\n }\n\n button {\n background-color: rgba(white, 0.8);\n border: 1px solid black;\n padding: 0.5em 1em;\n border-radius: 4px;\n font-size: 100%;\n }\n\n .headline-container {\n position: relative;\n padding-bottom: 1rem;\n\n .modal-close {\n position: absolute;\n top: 0;\n right: 0;\n cursor: pointer;\n }\n }\n\n .body-container {\n padding: 2rem;\n overflow-y: auto;\n // max-height is set in JS\n }\n\n h3 {\n font-weight: 700;\n }\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/Resources/Public/Css/DlfMediaVendor.css.map b/Resources/Public/Css/DlfMediaVendor.css.map new file mode 100644 index 000000000..e278f218e --- /dev/null +++ b/Resources/Public/Css/DlfMediaVendor.css.map @@ -0,0 +1 @@ +{"version":3,"file":"Css/DlfMediaVendor.css","mappings":";;;AAAA;;;;EAIE;AACF;;;;EAIE;AACF,iEAAiE;AACjE;sBACsB;ACStB;EDPE;sCACoC;ECSpC;ADPF;AACA,gEAAgE;AAChE,2CAA2C;AAC3C;mDACmD;AACnD;6BAC6B;AAC7B;4BAC4B;AAC5B;;;;EAIE;AACF,4EAA4E;AAC5E;2BAC2B;AErB3B;EFuBE;;;;;;;qCAOmC;ECUnC;EDRA;;8EAE4E;ECW5E;EACA;EDTA;kCACgC;EEjChC;EFmCA,kDAAkD;EAClD,yCAAyC;AAC3C;AE1CA;EASI;EACA;AFoCJ;AE9CA;EAeI;AFkCJ;AACA;;;;;qCAKqC;AElBrC;EDrBE;EACA;ECWA;AFgCF;AEvBA;EFyBE;;oBAEkB;EE9BhB;AFgCJ;AE5BA;EDtBE;EACA;ECWA;AF2CF;AEjCA;EFmCE;;oBAEkB;EEzChB;AF2CJ;AEtCA;EDvBE;EACA;ECWA;AFsDF;AE3CA;EF6CE;;oBAEkB;EEpDhB;AFsDJ;AEhDA;EDxBE;EACA;ECWA;AFiEF;AErDA;EFuDE;;oBAEkB;EE/DhB;AFiEJ;AACA;;8CAE8C;AEzD9C;EF2DE;;uDAEqD;AACvD;AACA;;iDAEiD;AExDjD;EF0DE;;;;;;;gCAO8B;EC5D9B;ED8DA,mCAAmC;EC3DnC;EACA;EACA;EACA;EACA;EACA;EAnDA;EACA;EDiHA,wEAAwE;EEtExE;EFwEA,yEAAyE;EErEzE;EFuEA,yDAAyD;EEpEzD;EFsEA,4DAA4D;EEnE5D;EFqEA,mCAAmC;EElEnC;EFoEA;2DACyD;EACzD;qEACmE;EE/DnE;AFiEF;AClCE;EAzGA;AD8IF;AE9FA;EDgCE;ADiEF;AErEE;EFuEA,0CAA0C;AAC5C;AExEE;ED5EA;ADuJF;AACA;4CAC4C;AEnE5C;EACE;EACA;EACA;EFqEA;;;;IAIE;EElEF;AFoEF;AACA;;mBAEmB;AEjEnB;EFmEE,kEAAkE;EEjElE;EACA;EFmEA,2EAA2E;EEhE3E;EACA;EFkEA,gCAAgC;EE/DhC;EFiEA,gCAAgC;EE9DhC;EFgEA,wBAAwB;EE7DxB;EACA;EF+DA;8DAC4D;EE5D5D;EACA;EACA;EF8DA,2CAA2C;EClM3C;EACA;EACA;EACA;EDoMA,4EAA4E;ECrG5E;EDuGA,uEAAuE;ECpGvE;EDsGA;WACS;EACT;6CAC2C;AAC7C;ACtGE;;EAEE;ADwGJ;AEtEE;EFwEA,gCAAgC;EEtE9B;EFwEF,wBAAwB;EErEtB;EFuEF,qCAAqC;EEpEnC;EFsEF,2EAA2E;EC/L3E;EC8HE;EFoEF;cACY;EEjEV;EACA;EACA;AFmEJ;AACA,0EAA0E;AE/D1E;EACE;AFiEF;AACA;;2BAE2B;AE9D3B;EFgEE;kCACgC;EE9DhC;ED9JA;EACA;EA2EA;EDqJA;;uEAEqE;ECvKrE;EACA;EACA;EACA;EACA;EDyKA,0DAA0D;EErE1D;EACA;EACA;AFuEF;AEpEA;EACE;EACA;EAEA;EAEA;EACA;EAEA;EAEA;EACA;EAEA;EACA;EACA;EACA;EFiEA,uCAAuC;EACvC,4EAA4E;EC3K5E;ED6KA,uEAAuE;EC1KvE;ED4KA;WACS;AACX;AC1KE;;EAEE;AD4KJ;AE7FA;EAuBI;EACA;AFyEJ;AEjGA;EA4BI;AFwEJ;AEpEA;EACE;EAEA;EAEA;EACA;AFoEF;AE1EA;EASI;EAEA;EACA;EACA;EAEA;EACA;EACA;EACA;AFkEJ;AEhEI;EACE;AFkEN;AEvFA;EA0BI;EAEA;EAEA;EACA;AF8DJ;AE7FA;EAmCI;EAEA;EACA;AF4DJ;AExDA;EACE;EDpPA;EACA;EA2EA;EDqOA;;uEAEqE;ECvPrE;EACA;EACA;EACA;EACA;EDyPA,4EAA4E;ECxO5E;ED0OA,uEAAuE;ECvOvE;EDyOA;WACS;EACT,2EAA2E;EEpE3E;AFsEF;ACzOE;;EAEE;AD2OJ;AEvEA;EFyEE;;uEAEqE;EC7QrE;EACA;EACA;EACA;EACA;ED+QA;;qCAEmC;EE7EnC;EF+EA,mEAAmE;EE5EnE;EACA;EACA;EF8EA;;;;;mBAKiB;EE3EjB;EACA;EF6EA,iEAAiE;EE1EjE;EACA;EACA;AF4EF;AErGA;EA4BI;EACA;AF4EJ;AExEA;EF0EE;sEACoE;EExEpE;EF0EA;+CAC6C;EEvE7C;AFyEF;AACA,2BAA2B;AEtE3B;EFwEE;;uEAEqE;ECxTrE;EACA;EACA;EACA;EACA;EAhEA;EACA;EC8SA;EACA;EACA;EACA;AF8EF;ACjSE;EAzGA;AD6YF;AE5EA;EF8EE;;iCAE+B;EAC/B;sDACoD;EACpD;;;;;;;qCAOmC;ECzXnC;ED2XA;;8EAE4E;ECxX5E;EACA;ECkSA;EACA;EACA;EACA;EACA;EFyFA;0BACwB;EEtFxB;AFwFF;AACA;;;;EAIE;AACF,yCAAyC;AACzC,6EAA6E;AGhb7E;EHkbE;;;;;;8DAM4D;EGhb5D;EACA;EACA;EACA;EHkbA;+EAC6E;EG/a7E;EHibA,oCAAoC;EG9apC;EHgbA,0CAA0C;EG7a1C;EH+aA,eAAe;EG5af;EH8aA;;4BAE0B;EG3a1B;EACA;EACA;EH6aA,8CAA8C;EG1a9C;EH4aA,4EAA4E;ECpX5E;EDsXA,uEAAuE;ECnXvE;EDqXA;WACS;EACT;;;+EAG6E;AAC/E;ACvXE;;EAEE;ADyXJ;AGlbE;EACE;AHobJ;AGjbE;EACE;AHmbJ;AACA;;;EAGE;AG/aF;EACE;EACA;EACA;AHibF;AG/aE;EHibA;8BAC4B;EGpf5B;EACA;EACA;AHsfF;AACA,0DAA0D;AGjb1D;EHmbE;iBACe;AACjB;AGrbA;;EHwbE;2BACyB;EGrbvB;AHubJ;AG3bA;;EAUI;EACA;AHqbJ;AACA;;qDAEqD;AGjbrD;;EAEI;AHmbJ;AACA;;;;EAIE;AACF;;;;;;;;;;;;;;;;;;;0CAmB0C;AAC1C,4DAA4D;AAC5D;;+EAE+E;AAC/E;0DAC0D;AAC1D;kDACkD;AAClD,4CAA4C;AI3b5C;EJ6bE,sDAAsD;EACtD;;;;;;;qCAOmC;ECphBnC;EDshBA;;8EAE4E;ECnhB5E;EACA;EDqhBA,4DAA4D;EItiB5D;EJwiBA,oEAAoE;EIriBpE;EJuiBA,uCAAuC;EIpiBvC;EJsiBA,4EAA4E;EIniB5E;AJqiBF;AIhdA;EACE;AJkdF;AI/cA;EJidE,qDAAqD;EI/frD;EACA;EJigBA,wCAAwC;EACxC;;;;;;;gCAO8B;EChiB9B;EDkiBA,mCAAmC;EC/hBnC;EACA;EACA;EACA;EACA;EACA;EAnDA;EACA;EDqlBA;;gBAEc;EI9gBd;EJghBA;6EAC2E;EI7gB3E;EJ+gBA;yDACuD;EI5gBvD;EJ8gBA,8DAA8D;EAC9D,8CAA8C;AAChD;AI7gBE;EJ+gBA,6CAA6C;EIvkB7C;EJykBA;;wDAEsD;EItkBtD;EJwkBA;qBACmB;EIrkBnB;EACA;EACA;AJukBF;AIthBE;EJwhBA,oEAAoE;EIlkBpE;EJokBA;kBACgB;EIjkBhB;EJmkBA,kDAAkD;EIhkBlD;EACA;EACA;EJkkBA,+BAA+B;EI/jB/B;AJikBF;AI9hBE;EJgiBA,6CAA6C;EIjmB7C;EJmmBA;;wDAEsD;EIhmBtD;EJkmBA;qBACmB;EI/lBnB;EACA;EACA;AJimBF;AIviBE;EJyiBA,oEAAoE;EI5lBpE;EJ8lBA;kBACgB;EI3lBhB;EJ6lBA,kDAAkD;EI1lBlD;EACA;EACA;EJ4lBA,+BAA+B;EIzlB/B;AJ2lBF;AIniBA;EJqiBE,4EAA4E;ECvkB5E;EDykBA,uEAAuE;ECtkBvE;EDwkBA;WACS;AACX;ACtkBE;;EAEE;ADwkBJ;AI3iBA;EJ6iBE;;;;;;;gCAO8B;EChoB9B;EDkoBA,mCAAmC;EC/nBnC;EACA;EACA;EACA;EACA;EACA;EAnDA;EACA;ADqrBF;AACA;;;;;;;;;;;;;;;;;;;;;;;;EAwBE;AACF;iDACiD;AKjtBjD;ELmtBE;;yBAEuB;EACvB;;;;;;;gCAO8B;EClrB9B;EDorBA,mCAAmC;ECjrBnC;EACA;EACA;EACA;EDmrBA,sBAAsB;EK7tBtB;EACA;EL+tBA,uCAAuC;EK5tBvC;EACA;EACA;EACA;AL8tBF;AACA,oDAAoD;AK3tBpD;EACE;EACA;EACA;EL6tBA,8CAA8C;EK1tB9C;EL4tBA,gCAAgC;EKztBhC;AL2tBF;AACA,wBAAwB;AKxtBxB;EACE;IACE;EL0tBF;AACF;AACA,oEAAoE;AKvtBpE;EACE;IACE;IACA;ELytBF;EKttBA;IACE;IACA;ELwtBF;EKrtBA;IACE;IACA;ELutBF;AACF;AACA;;;;EAIE;AACF,2EAA2E;AAC3E;4DAC4D;AMryB5D;ENuyBE,yEAAyE;EMryBzE;ENuyBA,gEAAgE;EC3sBhE;ED6sBA,0DAA0D;EMnyB1D;ENqyBA;iBACe;EMlyBf;ANoyBF;AACA;;;;EAIE;AACF;iEACiE;AOzzBjE;;EP4zBE;2CACyC;EOzzBzC;EACA;EP2zBA,sCAAsC;EOxzBtC;EP0zBA,gCAAgC;EOvzBhC;EACA;EACA;EACA;EACA;EPyzBA,gDAAgD;EAChD,4EAA4E;ECpuB5E;EDsuBA,uEAAuE;ECnuBvE;EDquBA;WACS;EACT;uBACqB;EO1zBrB;EACA;EP4zBA,4BAA4B;EOzzB5B;EACA;EACA;EACA;EP2zBA,iCAAiC;EACjC;4CAC0C;EAC1C;4CAC0C;AAC5C;ACjvBE;;;;EAEE;ADqvBJ;AOn2BA;;EAiCI;EACA;EACA;EACA;EACA;EACA;EPs0BF,yEAAyE;EOn0BvE;EACA;EPq0BF,0DAA0D;EAC1D,oDAAoD;EO9zBlD;EPg0BF,4DAA4D;AAC9D;AOt0BI;;EACE;APy0BN;AOv3BA;;EAsDM;APq0BN;AOl0BI;;EACE;APq0BN;AO/3BA;;EPk4BE,oDAAoD;EOh0BlD;EACA;APk0BJ;AO7zBE;;EPg0BA,oDAAoD;EO9zBlD;APg0BJ;AACA;;;;;uBAKuB;AO5zBvB;EACE;AP8zBF;AACA;uBACuB;AO3zBvB;EACE;EP6zBA,8DAA8D;EO1zB9D;EACA;AP4zBF;AACA;;sDAEsD;AOzzBtD;EP2zBE,0EAA0E;EOzzB1E;AP2zBF;AACA,8DAA8D;AOxzB9D;EP0zBE,oDAAoD;EOvzBlD;APyzBJ;AACA;UACU;AOrzBV;EPuzBE,mEAAmE;EACnE,sCAAsC;AACxC;AOzzBA;EP2zBE,oDAAoD;EOvzBlD;APyzBJ;AO7zBA;EP+zBE,oDAAoD;EOrzBlD;APuzBJ;AACA,yDAAyD;AOnzBzD;EPqzBE,oDAAoD;EOnzBpD;APqzBF;AACA;;;;EAIE;AACF,iBAAiB;AQ37BjB;ER67BE,0EAA0E;EQ37B1E;ER67BA,oEAAoE;AACtE;AQh8BA;EAMI;AR67BJ;AQz7BA;;ER47BE;;uEAEqE;EC73BrE;EACA;EACA;EACA;EACA;ED+3BA;;2BAEyB;AAC3B;AQh8BI;;ERm8BF;6CAC2C;EQj8BvC;ARm8BN;AQ97BA;EPVE;EACA;EA2EA;ADi4BF;AQ/7BE;EACE;ARi8BJ;AQ77BA;EAGE;EACA;EACA;ER67BA;iBACe;EQ17Bf;AR47BF;ACz3BE;EAzGA;ADq+BF;AQx8BA;;EAYI;EACA;ARg8BJ;AQ78BA;EPRE;ADw9BF;AQ37BA;;EPnCE;EACA;EACA;EDk+BA;wBACsB;EQ5/BtB;AR8/BF;AQ/7BA;ERi8BE;8DAC4D;EQ/7B5D;ERi8BA;;;;;IAKE;EQ97BF;EACA;EACA;EACA;ARg8BF;AQ77BA;EACE;EACA;EACA;EAMA;AR07BF;AQ97BE;EACE;ARg8BJ;AQ17BA;EACE;EACA;EACA;AR47BF;AACA;;;;;;;;;;;;;;;;;;;;;;;;EAwBE;AACF;kEACkE;AS1hClE;EACE;ET4hCA,yEAAyE;EACzE;mEACiE;EACjE;oEACkE;AACpE;AS/hCE;EACE;ETiiCF,mDAAmD;AACrD;AS/hCI;;;EACE;ETmiCJ,gDAAgD;EShiC5C;EACA;EACA;EACA;ETkiCJ,YAAY;ES/hCR;EACA;EACA;EACA;ETiiCJ,gBAAgB;ES9hCZ;EACA;ETgiCJ,oEAAoE;ES7hChE;ET+hCJ,0DAA0D;ESnkC1D;EACA;EACA;EACA;EACA;ATqkCF;AS5hCI;;;EACE;ATgiCN;ASzhCI;;;EACE;EAtDJ;EACA;EACA;EACA;EACA;ATolCF;AS1hCI;;;EACE;EA/DJ;EACA;EACA;EACA;EACA;AT8lCF;AU/nCA;EACA;EACA;EACA;EACA;AACA;;ACLA;EACA;EACA;EACA;EACA;AACA;;AAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AACA","sources":["webpack://kitodo-presentation/./node_modules/shaka-player/ui/controls.less","webpack://kitodo-presentation/./node_modules/shaka-player/ui/less/general.less","webpack://kitodo-presentation/./node_modules/shaka-player/ui/less/containers.less","webpack://kitodo-presentation/./node_modules/shaka-player/ui/less/buttons.less","webpack://kitodo-presentation/./node_modules/shaka-player/ui/less/range_elements.less","webpack://kitodo-presentation/./node_modules/shaka-player/ui/less/spinner.less","webpack://kitodo-presentation/./node_modules/shaka-player/ui/less/other_elements.less","webpack://kitodo-presentation/./node_modules/shaka-player/ui/less/overflow_menu.less","webpack://kitodo-presentation/./node_modules/shaka-player/ui/less/ad_controls.less","webpack://kitodo-presentation/./node_modules/shaka-player/ui/less/tooltip.less","webpack://kitodo-presentation/https:/fonts.googleapis.com/css","webpack://kitodo-presentation/https:/fonts.googleapis.com/icon"],"sourcesContent":["/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n/* General utility mixins and classes with broad applicability. */\n/* Make a thing unselectable. There are currently no cases where we make it\n * selectable again. */\n.shaka-hidden {\n /* Make this override equally specific classes.\n * If it's hidden, always hide it! */\n display: none !important;\n}\n/* For containers which host elements overlaying other things. */\n/* For things which overlay other things. */\n/* For things that should not shrink inside a flex container.\n * This will be used for all controls by default. */\n/* Use this to override .unshrinkable() in particular cases that *should* shrink\n * inside a flex container. */\n/* The width of the bottom-section controls: seek bar, ad controls, and\nthe control buttons panel. */\n/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n/* All of the top-level containers into which various visible features go. */\n/* A container for the entire video + controls combo. This is the auto-setup\n * div which we populate. */\n.shaka-video-container {\n /* For a detailed explanation of how this achieves an overlay, please refer\n * to https://developer.mozilla.org/en-US/docs/Web/CSS/position .\n *\n * But you don't have to, because we've encapsulated these high level\n * concepts into classes.\n *\n * This makes it possible for some children of this container to overlay the\n * others using .overlay-child(). */\n position: relative;\n /* Make sure any top or left styles applied from outside don't move this from\n * it's original position, now that it's relative to that original position.\n * This is a defensive move that came out of intensive debugging on IE 11. */\n top: 0;\n left: 0;\n /* Without this, the container somehow winds up being a tad taller than it\n * should be (484px vs 480px). */\n display: flex;\n /* Set a special font for material design icons. */\n /* Set the fonts for all other content. */\n}\n.shaka-video-container .material-icons-round {\n font-family: 'Material Icons Round';\n font-size: 24px;\n}\n.shaka-video-container * {\n font-family: Roboto-Regular, Roboto, sans-serif;\n}\n/* Each browser has a different prefixed pseudo-class for fullscreened elements.\n * Define the properties of a fullscreened element in a mixin, then apply to\n * each of the browser-specific pseudo-classes.\n * NOTE: These fullscreen pseudo-classes can't be combined with commas into a\n * single delcaration. Browsers ignore the rest of the list once they hit one\n * pseudo-class they don't support. */\n.shaka-video-container:fullscreen {\n width: 100%;\n height: 100%;\n background-color: black;\n}\n.shaka-video-container:fullscreen .shaka-text-container {\n /* In fullscreen mode, the text displayer's font size should be relative to\n * the either window height or width (whichever is smaller), instead of a\n * fixed size. */\n font-size: 4.4vmin;\n}\n.shaka-video-container:-webkit-full-screen {\n width: 100%;\n height: 100%;\n background-color: black;\n}\n.shaka-video-container:-webkit-full-screen .shaka-text-container {\n /* In fullscreen mode, the text displayer's font size should be relative to\n * the either window height or width (whichever is smaller), instead of a\n * fixed size. */\n font-size: 4.4vmin;\n}\n.shaka-video-container:-moz-full-screen {\n width: 100%;\n height: 100%;\n background-color: black;\n}\n.shaka-video-container:-moz-full-screen .shaka-text-container {\n /* In fullscreen mode, the text displayer's font size should be relative to\n * the either window height or width (whichever is smaller), instead of a\n * fixed size. */\n font-size: 4.4vmin;\n}\n.shaka-video-container:-ms-fullscreen {\n width: 100%;\n height: 100%;\n background-color: black;\n}\n.shaka-video-container:-ms-fullscreen .shaka-text-container {\n /* In fullscreen mode, the text displayer's font size should be relative to\n * the either window height or width (whichever is smaller), instead of a\n * fixed size. */\n font-size: 4.4vmin;\n}\n/* The actual video element. Sits inside .shaka-video-container and gives it a\n * size in non-fullscreen mode. In fullscreen mode, the sizing relationship\n * flips. CSS is just great like that. :-( */\n.shaka-video {\n /* At the moment, nothing special is required here.\n * Note that this should NOT be an overlay-child, as its size could dictate\n * the size of the container for some applications. */\n}\n/* A container for all controls, including the giant play button, seek bar, etc.\n * Sits inside .shaka-video-container, on top of (Z axis) .shaka-video, and\n * below (Y axis) .shaka-play-button-container. */\n.shaka-controls-container {\n /* For a detailed explanation of how this achieves an overlay, please refer\n * to https://developer.mozilla.org/en-US/docs/Web/CSS/position .\n *\n * But you don't have to, because we've encapsulated these high level\n * concepts into classes.\n *\n * This makes it possible for this child to overlay the other children of a\n * .overlay-parent() object. */\n position: absolute;\n /* Fill the container by default. */\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n margin: 0;\n padding: 0;\n width: 100%;\n height: 100%;\n /* Without this, the controls container overflows the video container. */\n box-sizing: border-box;\n /* A flex container, to make layout of children easier to reason about. */\n display: flex;\n /* Defines in which direction the children should flow. */\n flex-direction: column;\n /* Pushes the children toward the bottom of the container. */\n justify-content: flex-end;\n /* Centers children horizontally. */\n align-items: center;\n /* By default, do not allow any of our controls to shrink.\n * Specific controls can use .shrinkable() to override. */\n /* Position the controls container in front of the text container, so that\n * the text container doesn't interfere with the control buttons. */\n z-index: 1;\n}\n.shaka-video-container:not([shaka-controls=\"true\"]) .shaka-controls-container {\n display: none;\n}\n.shaka-controls-container * {\n flex-shrink: 0;\n}\n.shaka-controls-container[casting=\"true\"] {\n /* Hide fullscreen button while casting. */\n}\n.shaka-controls-container[casting=\"true\"] .shaka-fullscreen-button {\n display: none;\n}\n/* Container for controls positioned at the bottom of the video container:\n * controls button panel and the seek bar. */\n.shaka-bottom-controls {\n width: 96%;\n padding: 0;\n padding-bottom: 2.5%;\n /* Position the bottom panel in front of other controls (play button and\n * spinner containers).\n * TODO: A different layout arrangement might be a better solution for this.\n * Need to experiment.\n */\n z-index: 1;\n}\n/* This is the container for the horizontal row of controls above the seek bar.\n * It sits above (Y axis) the seek bar, and below (Y axis) the giant play button\n * in the middle. */\n.shaka-controls-button-panel {\n /* Fill the space horizontally, with no extra padding or margin. */\n padding: 0;\n margin: 0;\n /* This is itself a flex container, with children layed out horizontally. */\n display: flex;\n flex-direction: row;\n /* Push children to the right. */\n justify-content: flex-end;\n /* Center children vertically. */\n align-items: center;\n /* TODO: Document why. */\n overflow: hidden;\n min-width: 48px;\n /* Make sure we don't inherit odd font sizes and styles from the environment.\n * TODO: When did this happen? What forced us to do this? */\n font-size: 12px;\n font-weight: normal;\n font-style: normal;\n /* Make sure contents cannot be selected. */\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n /* Transparent unless explicitly made opaque through container attributes. */\n opacity: 0;\n /* When we show/hide this, do it gradually using cubic-bezier timing. */\n transition: opacity cubic-bezier(0.4, 0, 0.6, 1) 600ms;\n /* Show controls when the container's \"shown\" or \"casting\" attributes are\n * set. */\n /* All buttons, divs, and other controls directly inside this panel should\n * have these characteristics by default. */\n}\n.shaka-controls-container[shown=\"true\"] .shaka-controls-button-panel,\n.shaka-controls-container[casting=\"true\"] .shaka-controls-button-panel {\n opacity: 1;\n}\n.shaka-controls-button-panel > * {\n /* White text or button icons. */\n color: white;\n /* 32px tall controls. */\n height: 32px;\n /* Consistent alignment of buttons. */\n line-height: 0.5;\n /* Consistent margins (external) and padding (internal) between controls. */\n margin: 1px 6px;\n padding: 0;\n /* Transparent backgrounds, no borders, and a pointer when you mouse over\n * them. */\n background: transparent;\n border: 0;\n cursor: pointer;\n}\n/* Buttons hide certain items if they are found inside the control panel */\n.shaka-controls-button-panel .shaka-overflow-menu-only {\n display: none;\n}\n/* The container for the giant play button. Sits above (Y axis) the\n * other video controls and seek bar, in the middle of the video frame, on top\n * of (Z axis) the video. */\n.shaka-play-button-container {\n /* Take up as much space as possible, but shrink (vertically) to accomodate\n * the controls at the bottom. */\n margin: 0;\n width: 100%;\n height: 100%;\n flex-shrink: 1;\n /* When setting \"position: absolute\" it uses the left,right,top,bottom\n * properties to determine the positioning. We should set all these\n * properties to ensure it is positioned properly on all platforms. */\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n /* Keep the play button in the middle of this container. */\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.shaka-statistics-container {\n overflow-x: hidden;\n overflow-y: auto;\n min-width: 300px;\n color: white;\n background-color: rgba(35, 35, 35, 0.9);\n font-size: 14px;\n padding: 5px 10px;\n border-radius: 2px;\n position: absolute;\n z-index: 2;\n left: 15px;\n top: 15px;\n /* Fades out with the other controls. */\n /* Transparent unless explicitly made opaque through container attributes. */\n opacity: 0;\n /* When we show/hide this, do it gradually using cubic-bezier timing. */\n transition: opacity cubic-bezier(0.4, 0, 0.6, 1) 600ms;\n /* Show controls when the container's \"shown\" or \"casting\" attributes are\n * set. */\n}\n.shaka-controls-container[shown=\"true\"] .shaka-statistics-container,\n.shaka-controls-container[casting=\"true\"] .shaka-statistics-container {\n opacity: 1;\n}\n.shaka-statistics-container div {\n display: flex;\n justify-content: space-between;\n}\n.shaka-statistics-container span {\n color: #969696;\n}\n.shaka-context-menu {\n background-color: rgba(35, 35, 35, 0.9);\n border-radius: 2px;\n position: absolute;\n z-index: 3;\n}\n.shaka-context-menu button {\n padding: 5px 10px;\n width: 100%;\n display: flex;\n align-items: center;\n color: white;\n background: transparent;\n border: 0;\n cursor: pointer;\n}\n.shaka-context-menu button:hover {\n background-color: rgba(50, 50, 50, 0.9);\n}\n.shaka-context-menu label {\n padding: 0 20px;\n align-items: flex-start;\n color: white;\n cursor: pointer;\n}\n.shaka-context-menu .shaka-current-selection-span {\n align-items: flex-start;\n color: white;\n cursor: pointer;\n}\n.shaka-scrim-container {\n margin: 0;\n width: 100%;\n height: 100%;\n flex-shrink: 1;\n /* When setting \"position: absolute\" it uses the left,right,top,bottom\n * properties to determine the positioning. We should set all these\n * properties to ensure it is positioned properly on all platforms. */\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n /* Transparent unless explicitly made opaque through container attributes. */\n opacity: 0;\n /* When we show/hide this, do it gradually using cubic-bezier timing. */\n transition: opacity cubic-bezier(0.4, 0, 0.6, 1) 600ms;\n /* Show controls when the container's \"shown\" or \"casting\" attributes are\n * set. */\n /* A black gradient at the bottom, behind the controls, but only so high. */\n background: linear-gradient(to top, #000000 0, rgba(0, 0, 0, 0) 15%);\n}\n.shaka-controls-container[shown=\"true\"] .shaka-scrim-container,\n.shaka-controls-container[casting=\"true\"] .shaka-scrim-container {\n opacity: 1;\n}\n.shaka-text-container {\n /* When setting \"position: absolute\" it uses the left,right,top,bottom\n * properties to determine the positioning. We should set all these\n * properties to ensure it is positioned properly on all platforms. */\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n /* Make sure the text container doesn't steal pointer events from another\n * layer, such as the ad container. There is nothing interactive in this\n * layer, so this should be fine. */\n pointer-events: none;\n /* Place the text container on the bottom of the video container. */\n bottom: 0%;\n width: 100%;\n min-width: 48px;\n /* When the controls fade in or out, it takes 600ms. Thus, when the text\n * container adjusts to the presence or absence of controls, we should wait\n * briefly, so the captions don't end up appearing behind the controls.\n * Instead of being a gradual animation, this is a fast animation with a\n * significant delay, since the captions moving around is a little\n * distracting. */\n transition: bottom cubic-bezier(0.4, 0, 0.6, 1) 100ms;\n transition-delay: 500ms;\n /* These are defaults which are overridden by JS or cue styles. */\n font-size: 20px;\n line-height: 1.4;\n color: #ffffff;\n}\n.shaka-text-container span.shaka-text-wrapper {\n display: inline;\n background: none;\n}\n.shaka-controls-container[shown=\"true\"] ~ .shaka-text-container {\n /* While the controls are shown, the text container should avoid the 15%\n * at the bottom of the video, to avoid overlapping with controls. */\n bottom: 15%;\n /* Disable the transition delay when moving the captions up, so that the\n * controls don't appear over the captions. */\n transition-delay: 0ms;\n}\n/* The buffering spinner. */\n.shaka-spinner-container {\n /* When setting \"position: absolute\" it uses the left,right,top,bottom\n * properties to determine the positioning. We should set all these\n * properties to ensure it is positioned properly on all platforms. */\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n flex-shrink: 1;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.shaka-video-container:not([shaka-controls=\"true\"]) .shaka-spinner-container {\n display: none;\n}\n.shaka-spinner {\n /* This uses the same trickery as the big play button define\n the spinner's width and height. See .shaka-play-button\n for the detailed explanation. */\n /* For the padding thing to work, spinner div needs to be an\n overlay-parent and spinner svg - an overlay child. */\n /* For a detailed explanation of how this achieves an overlay, please refer\n * to https://developer.mozilla.org/en-US/docs/Web/CSS/position .\n *\n * But you don't have to, because we've encapsulated these high level\n * concepts into classes.\n *\n * This makes it possible for some children of this container to overlay the\n * others using .overlay-child(). */\n position: relative;\n /* Make sure any top or left styles applied from outside don't move this from\n * it's original position, now that it's relative to that original position.\n * This is a defensive move that came out of intensive debugging on IE 11. */\n top: 0;\n left: 0;\n margin: 0;\n box-sizing: border-box;\n padding: 7.8%;\n width: 0;\n height: 0;\n /* Add a bit of a white shadow to keep our black spinner visible\n on a black background. */\n filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.5));\n}\n/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n/* The main buttons in the UI controls. */\n/* The giant play button, which sits inside .shaka-player-button-container. */\n.shaka-play-button {\n /* Set width & height in a round-about way. By using padding, we can keep\n * a 1:1 aspect ratio and size the button relative to the container width.\n *\n * Since padding is applied equally to top, bottom, left, and right, only use\n * half of the intended percentage for each.\n *\n * Based on tips from https://stackoverflow.com/a/12925343 */\n box-sizing: border-box;\n padding: 7.5%;\n width: 0;\n height: 0;\n /* To be properly positioned in the center, this should have no margin.\n * This might have been set for buttons generally by the app or user-agent. */\n margin: 0;\n /* This makes the button a circle. */\n border-radius: 50%;\n /* A small drop shadow below the button. */\n box-shadow: rgba(0, 0, 0, 0.1) 0 0 20px 0;\n /* No border. */\n border: none;\n /* The play arrow is a picture. It is treated a background image.\n * The following settings ensure it shows only once and in the\n * center of the button. */\n background-size: 50%;\n background-repeat: no-repeat;\n background-position: center center;\n /* A background color behind the play arrow. */\n background-color: rgba(255, 255, 255, 0.9);\n /* Transparent unless explicitly made opaque through container attributes. */\n opacity: 0;\n /* When we show/hide this, do it gradually using cubic-bezier timing. */\n transition: opacity cubic-bezier(0.4, 0, 0.6, 1) 600ms;\n /* Show controls when the container's \"shown\" or \"casting\" attributes are\n * set. */\n /* Actual icon images for the two states this could be in.\n * These will be inlined as data URIs when compiled, and so do not need to be\n * deployed separately from the compiled CSS.\n * Note that these URIs should relative to ui/controls.less, not this file. */\n}\n.shaka-controls-container[shown=\"true\"] .shaka-play-button,\n.shaka-controls-container[casting=\"true\"] .shaka-play-button {\n opacity: 1;\n}\n.shaka-play-button[icon=\"play\"] {\n background-image: url(\"data:image/svg+xml,%3Csvg%20fill%3D%22%23000000%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20width%3D%2224%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpath%20d%3D%22M8%205v14l11-7z%22%2F%3E%0A%20%20%20%20%3Cpath%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%0A%3C%2Fsvg%3E\");\n}\n.shaka-play-button[icon=\"pause\"] {\n background-image: url(\"data:image/svg+xml,%3Csvg%20fill%3D%22%23000000%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20width%3D%2224%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpath%20d%3D%22M6%2019h4V5H6v14zm8-14v14h4V5h-4z%22%2F%3E%0A%20%20%20%20%3Cpath%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%0A%3C%2Fsvg%3E\");\n}\n/* This button contains the current time and duration of the video.\n * It's only clickable when the content is live, and current time is behind live\n * edge. Otherwise, the button is disabled.\n */\n.shaka-current-time {\n font-size: 14px;\n color: #ffffff;\n cursor: pointer;\n}\n.shaka-current-time[disabled] {\n /* Set the background and the color, otherwise it might be overwritten by\n * the css styles in demo. */\n background-color: transparent;\n color: white;\n cursor: default;\n}\n/* Use a consistent outline focus style across browsers. */\n.shaka-controls-container {\n /* Disable this Mozilla-specific focus ring, since we have an outline defined\n * for focus. */\n}\n.shaka-controls-container button:focus,\n.shaka-controls-container input:focus {\n /* Most browsers will fall back to \"Highlight\" (system setting) color for\n * the focus outline. */\n outline: 1px solid Highlight;\n}\n.shaka-controls-container button:-moz-focus-inner,\n.shaka-controls-container input:-moz-focus-outer {\n outline: none;\n border: 0;\n}\n/* Outline on focus is important for accessibility, but\n * it doesn't look great. This removes the outline for\n * mouse users while leaving it for keyboard users. */\n.shaka-controls-container:not(.shaka-keyboard-navigation) button:focus,\n.shaka-controls-container:not(.shaka-keyboard-navigation) input:focus {\n outline: none;\n}\n/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n/* Special styles for input elements with type \"range\".\n *\n * These elements are composed of two main parts: a \"track\", which is the\n * horizontal bar, and the \"thumb\", which is the knob which slides along that\n * bar.\n *\n * In order to style the track across browsers (cough, IE 11), we need to do\n * something a bit tricky. Styling the track is a nightmare, especially if you\n * want the thumb to be larger. On IE 11, this gets clipped at the track size.\n * So a tiny track with a large thumb is not easily achieved. It can be done,\n * but the techniques for it are incompatible with the gradient background we\n * want to apply to it.\n *\n * The solution is to put the input inside a div container, and apply the\n * background gradient styles to the container. The container will act as a\n * visible, virtual track, inside which is contained a larger, invisible track,\n * in which is contained a visible thumb. This way, the thumb is not larger\n * than the actual track (for IE 11's sake), but can be larger than the virtual\n * track. And since we are still using a semantically correct input element,\n * the element is inherently accessible. */\n/* These control the color and size of the various pieces. */\n/* The range container is the div that contains a range element.\n * This div will act as a virtual track to allow us to style the track space.\n * An actual track still exists inside the range element, but is transparent. */\n/* The \"track\" is the pseudo-element inside the range element which represents\n * the horizontal bar on which the \"thumb\" (knob) moves. */\n/* The \"thumb\" is the pseudo-element inside the range element which represents\n * the knob which moves along the \"track\" (bar). */\n/* This is the actual range input element. */\n.shaka-range-container {\n /* This contains an input element which overlays it. */\n /* For a detailed explanation of how this achieves an overlay, please refer\n * to https://developer.mozilla.org/en-US/docs/Web/CSS/position .\n *\n * But you don't have to, because we've encapsulated these high level\n * concepts into classes.\n *\n * This makes it possible for some children of this container to overlay the\n * others using .overlay-child(). */\n position: relative;\n /* Make sure any top or left styles applied from outside don't move this from\n * it's original position, now that it's relative to that original position.\n * This is a defensive move that came out of intensive debugging on IE 11. */\n top: 0;\n left: 0;\n /* Vertical margins to occupy the same space as the thumb. */\n margin: 4px 6px;\n /* Smaller height to contain the background for the virtual track. */\n height: 4px;\n /* Rounded ends on the virtual track. */\n border-radius: 4px;\n /* Until we set a gradient background in JS, this will be the track color. */\n background: white;\n}\n.shaka-volume-bar-container {\n width: 100px;\n}\n.shaka-range-element {\n /* Remove any browser styling of the range element. */\n -webkit-appearance: none;\n background: transparent;\n /* Overlay and fill the container div. */\n /* For a detailed explanation of how this achieves an overlay, please refer\n * to https://developer.mozilla.org/en-US/docs/Web/CSS/position .\n *\n * But you don't have to, because we've encapsulated these high level\n * concepts into classes.\n *\n * This makes it possible for this child to overlay the other children of a\n * .overlay-parent() object. */\n position: absolute;\n /* Fill the container by default. */\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n margin: 0;\n padding: 0;\n width: 100%;\n height: 100%;\n /* The range element should be big enough to contain the thumb without\n * clipping it. It is very tricky to make the thumb show outside the track\n * on IE 11. */\n height: 12px;\n /* Position the top of the range element so that it is centered on the\n * container. Note that the container is actually smaller than the thumb. */\n top: -4px;\n /* Make sure clicking at the very top of the bar still takes effect and is not\n * confused with clicking the video to play/pause it. */\n z-index: 1;\n /* Pseudo-elements for Blink-based or WebKit-based browsers. */\n /* Pseudo-elements for Gecko-based browsers. */\n}\n.shaka-range-element::-webkit-slider-runnable-track {\n /* The track should fill the range element. */\n width: 100%;\n /* The track should be tall enough to contain the thumb without clipping it.\n * It is very tricky to make the thumb show outside the track on IE 11, and\n * it is incompatible with our background gradients. */\n height: 12px;\n /* Some browsers have default backgrounds, colors, or borders for this.\n * Hide them all. */\n background: transparent;\n color: transparent;\n border: none;\n}\n.shaka-range-element::-webkit-slider-thumb {\n /* Remove default styles on WebKit-based and Blink-based browsers. */\n -webkit-appearance: none;\n /* On some browsers (IE 11), the thumb has a border, which affects the size.\n * Disable it. */\n border: none;\n /* Make the thumb a circle and set its diameter. */\n border-radius: 12px;\n height: 12px;\n width: 12px;\n /* Give it the desired color. */\n background: white;\n}\n.shaka-range-element::-moz-range-track {\n /* The track should fill the range element. */\n width: 100%;\n /* The track should be tall enough to contain the thumb without clipping it.\n * It is very tricky to make the thumb show outside the track on IE 11, and\n * it is incompatible with our background gradients. */\n height: 12px;\n /* Some browsers have default backgrounds, colors, or borders for this.\n * Hide them all. */\n background: transparent;\n color: transparent;\n border: none;\n}\n.shaka-range-element::-moz-range-thumb {\n /* Remove default styles on WebKit-based and Blink-based browsers. */\n -webkit-appearance: none;\n /* On some browsers (IE 11), the thumb has a border, which affects the size.\n * Disable it. */\n border: none;\n /* Make the thumb a circle and set its diameter. */\n border-radius: 12px;\n height: 12px;\n width: 12px;\n /* Give it the desired color. */\n background: white;\n}\n.shaka-seek-bar-container {\n /* Transparent unless explicitly made opaque through container attributes. */\n opacity: 0;\n /* When we show/hide this, do it gradually using cubic-bezier timing. */\n transition: opacity cubic-bezier(0.4, 0, 0.6, 1) 600ms;\n /* Show controls when the container's \"shown\" or \"casting\" attributes are\n * set. */\n}\n.shaka-controls-container[shown=\"true\"] .shaka-seek-bar-container,\n.shaka-controls-container[casting=\"true\"] .shaka-seek-bar-container {\n opacity: 1;\n}\n.shaka-ad-markers {\n /* For a detailed explanation of how this achieves an overlay, please refer\n * to https://developer.mozilla.org/en-US/docs/Web/CSS/position .\n *\n * But you don't have to, because we've encapsulated these high level\n * concepts into classes.\n *\n * This makes it possible for this child to overlay the other children of a\n * .overlay-parent() object. */\n position: absolute;\n /* Fill the container by default. */\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n margin: 0;\n padding: 0;\n width: 100%;\n height: 100%;\n}\n/*!\n * @license\n * The SVG/CSS buffering spinner is based on http://codepen.io/jczimm/pen/vEBpoL\n * Some local modifications have been made.\n *\n * Copyright (c) 2016 by jczimm\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n/* This is the spinner SVG itself, which contains a circular path element.\n * It sits inside the play button and fills it. */\n.shaka-spinner-svg {\n /* Because of some sizing hacks in the play button (see comments there), this\n * spinner needs to be an overlay child to be properly sized and positioned\n * within the button. */\n /* For a detailed explanation of how this achieves an overlay, please refer\n * to https://developer.mozilla.org/en-US/docs/Web/CSS/position .\n *\n * But you don't have to, because we've encapsulated these high level\n * concepts into classes.\n *\n * This makes it possible for this child to overlay the other children of a\n * .overlay-parent() object. */\n position: absolute;\n /* Fill the container by default. */\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n /* Keep it spinning! */\n animation: rotate 2s linear infinite;\n transform-origin: center center;\n /* The SVG should fill its container. */\n width: 100%;\n height: 100%;\n margin: 0;\n padding: 0;\n}\n/* This is the path element, which draws a circle. */\n.shaka-spinner-path {\n stroke: #202124;\n stroke-dasharray: 20, 200;\n stroke-dashoffset: 0;\n /* Animate the stroke of this circular path. */\n animation: dash 1s ease-in-out infinite;\n /* Round the line on the ends. */\n stroke-linecap: round;\n}\n/* Spin the whole SVG. */\n@keyframes rotate {\n 100% {\n transform: rotate(360deg);\n }\n}\n/* Pulse the circle's outline forward and backward while it spins. */\n@keyframes dash {\n 0% {\n stroke-dasharray: 1, 200;\n stroke-dashoffset: 0;\n }\n 50% {\n stroke-dasharray: 89, 200;\n stroke-dashoffset: -35px;\n }\n 100% {\n stroke-dasharray: 89, 200;\n stroke-dashoffset: -124px;\n }\n}\n/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n/* UI elements that did not fit into the buttons/range elements category. */\n/* This is a spacer element used to separate elements within the control\n * buttons panel. It's just an empty div of certain width. */\n.shaka-spacer {\n /* This should not have a pointer-style cursor like the other controls. */\n cursor: default;\n /* Make the element shrink to accommodate things to the right. */\n flex-shrink: 1;\n /* Make the element grow to take up the remaining space. */\n flex-grow: 1;\n /* Margins don't shrink. Remove margins in order to be more flexible when\n * shrinking. */\n margin: 0;\n}\n/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n/* The overflow menu and all settings submenus. These appear on top of all\n * other controls (Z axis) when the overflow button is clicked. */\n.shaka-overflow-menu,\n.shaka-settings-menu {\n /* It's okay to add a vertical scroll if there are too many items, but\n * horizontal scrolling is not allowed. */\n overflow-x: hidden;\n overflow-y: auto;\n /* Don't wrap text to the next line. */\n white-space: nowrap;\n /* Styles for the menu itself. */\n background: white;\n box-shadow: 0 1px 9px 0 rgba(0, 0, 0, 0.4);\n border-radius: 2px;\n max-height: 250px;\n min-width: 180px;\n /* The menus fade out with the other controls. */\n /* Transparent unless explicitly made opaque through container attributes. */\n opacity: 0;\n /* When we show/hide this, do it gradually using cubic-bezier timing. */\n transition: opacity cubic-bezier(0.4, 0, 0.6, 1) 600ms;\n /* Show controls when the container's \"shown\" or \"casting\" attributes are\n * set. */\n /* When displayed as a flex container, elements inside will flow in a\n * vertical column. */\n display: flex;\n flex-direction: column;\n /* Where the menu appears. */\n position: absolute;\n z-index: 2;\n right: 15px;\n bottom: 30px;\n /* The buttons inside the menu. */\n /* These are the elements which contain the material design icons.\n * TODO: Pull MD icon details out of JS. */\n /* If the seekbar is missing, this is positioned lower.\n * TODO: Solve with flex layout instead? */\n}\n.shaka-controls-container[shown=\"true\"] .shaka-overflow-menu,\n.shaka-controls-container[shown=\"true\"] .shaka-settings-menu,\n.shaka-controls-container[casting=\"true\"] .shaka-overflow-menu,\n.shaka-controls-container[casting=\"true\"] .shaka-settings-menu {\n opacity: 1;\n}\n.shaka-overflow-menu button,\n.shaka-settings-menu button {\n font-size: 14px;\n background: transparent;\n color: black;\n border: none;\n min-height: 30px;\n padding: 3.5px 6px;\n /* The button itself is a flex container, with children center-aligned. */\n display: flex;\n align-items: center;\n /* When hovered, the button's background is highlighted. */\n /* The button is clickable, showing cursor pointer */\n cursor: pointer;\n /* The label inside button is also showing cursor pointer */\n}\n.shaka-overflow-menu button:hover,\n.shaka-settings-menu button:hover {\n background: #e0e0e0;\n}\n.shaka-overflow-menu button label,\n.shaka-settings-menu button label {\n cursor: pointer;\n}\n.shaka-keyboard-navigation .shaka-overflow-menu button:focus,\n.shaka-keyboard-navigation .shaka-settings-menu button:focus {\n background: #e0e0e0;\n}\n.shaka-overflow-menu i,\n.shaka-settings-menu i {\n /* TODO(b/116651454): eliminate hard-coded offsets */\n padding-left: 10px;\n padding-right: 10px;\n}\n.shaka-overflow-menu.shaka-low-position,\n.shaka-settings-menu.shaka-low-position {\n /* TODO(b/116651454): eliminate hard-coded offsets */\n bottom: 15px;\n}\n/* The span elements inside the top-level overflow menu contain single lines\n * of text, which are the button name and the current selection. For example,\n * a captions button might have \"Captions\" in one span (the button name), and\n * \"Farsi\" in another (the current selection).\n * These are displayed inside a .shaka-overflow-button-label grouping, to the\n * right of MD icons. */\n.shaka-overflow-menu span {\n text-align: left;\n}\n/* This contains span elements with single lines of text, and appears to the\n * right of MD icons. */\n.shaka-overflow-button-label {\n position: relative;\n /* This is a flex container, whose children flow vertically. */\n display: flex;\n flex-direction: column;\n}\n/* This is the specific span element which shows the current selection from some\n * submenu. For example, it would contain the currently-selected subtitle\n * language, the currently-selected resolution, etc. */\n.shaka-current-selection-span {\n /* This is dimmer than the other span, which is the name of the submenu. */\n color: rgba(0, 0, 0, 0.54);\n}\n/* The submenus have somewhat different margins inside them. */\n.shaka-settings-menu span {\n /* TODO(b/116651454): eliminate hard-coded offsets */\n margin-left: 54px;\n}\n/* This is a button within each submenu that takes you back to the main overflow\n * menu. */\n.shaka-back-to-overflow-button {\n /* The label inside the button, which says something like \"back\". */\n /* The MD icon for the \"back\" arrow. */\n}\n.shaka-back-to-overflow-button span {\n /* TODO(b/116651454): eliminate hard-coded offsets */\n margin-left: 0;\n}\n.shaka-back-to-overflow-button i {\n /* TODO(b/116651454): eliminate hard-coded offsets */\n padding-right: 20px;\n}\n/* The menu item for resolutions which contains \"auto\". */\n.shaka-auto-span {\n /* TODO(b/116651454): eliminate hard-coded offsets */\n left: 17px;\n}\n/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n/* Ad controls. */\n.shaka-controls-container[ad-active=\"true\"] {\n /* While showing an ad, pass pointer events through to the ad container. */\n pointer-events: none;\n /* Except in the bottom controls, which should still be clickable. */\n}\n.shaka-controls-container[ad-active=\"true\"] .shaka-bottom-controls {\n pointer-events: auto;\n}\n.shaka-client-side-ad-container,\n.shaka-server-side-ad-container {\n /* When setting \"position: absolute\" it uses the left,right,top,bottom\n * properties to determine the positioning. We should set all these\n * properties to ensure it is positioned properly on all platforms. */\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n /* IMA SDK adds their own ad UI into an iframe element.\n * Adjust its position to fit in with our UI, when\n * Shaka UI is enabled. */\n}\n.shaka-video-container[shaka-controls=\"true\"] .shaka-client-side-ad-container iframe,\n.shaka-video-container[shaka-controls=\"true\"] .shaka-server-side-ad-container iframe {\n /* This moves the iframe up a little bit, so it\n * doesn't operlap with our controls. */\n height: 90%;\n}\n.shaka-server-side-ad-container {\n width: 100%;\n height: 100%;\n flex-shrink: 1;\n}\n.shaka-server-side-ad-container:not([ad-active=\"true\"]) {\n pointer-events: none;\n}\n.shaka-ad-controls {\n display: flex;\n flex-direction: row;\n z-index: 1;\n /* Add some room between the ad controls and the controls\n button panel. */\n padding-bottom: 1%;\n}\n.shaka-video-container:not([shaka-controls=\"true\"]) .shaka-ad-controls {\n display: none;\n}\n.shaka-ad-controls button,\n.shaka-ad-controls div {\n color: white;\n font-size: initial;\n}\n.shaka-ad-controls div:not(.shaka-skip-ad-counter) {\n margin: 1px 6px;\n}\n.shaka-ad-counter,\n.shaka-ad-position {\n display: flex;\n justify-content: flex-end;\n flex-direction: column;\n /* Give white text a black shadow, so it's visible against a\n * white background. */\n text-shadow: 1px 1px 4px black;\n}\n.shaka-skip-ad-container {\n /* Skip button is positioned at the very right edge of the\n * video container unlike the rest of the bottom controls. */\n position: relative;\n /* This math is determining how far the button is from the right edge.\n * Ad panel's parent is centered and @bottom-controls-width wide, so\n * 100 - @bottom-controls-width = margins from both sides of the container.\n * That divided by 2 is margin on one side, so we take that, and move the\n * button from its normal position to the right by that percentage.\n */\n right: -2%;\n display: flex;\n flex-direction: row;\n margin: 0;\n}\n.shaka-skip-ad-button {\n padding: 5px 15px;\n background: rgba(0, 0, 0, 0.7);\n border: none;\n cursor: pointer;\n}\n.shaka-skip-ad-button:disabled {\n background: rgba(0, 0, 0, 0.3);\n}\n.shaka-skip-ad-counter {\n padding: 5px 5px;\n background: rgba(0, 0, 0, 0.7);\n margin: 0;\n}\n/*!\n * @license\n * The tooltip is based on https://github.com/felipefialho/css-components/\n * Local modifications have been performed.\n *\n * Copyright (c) 2017 Felipe Fialho\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n/* .shaka-tooltips-on enables the tooltips and is only added to the\n * control panel when the 'enableTooltips' option is set to true */\n.shaka-tooltips-on {\n overflow: visible;\n /* Adds an additional attribute for the status in .shaka-tooltip-status */\n /* The first tooltip of the panel is not centered on top of the button\n * but rather aligned with the left border of the control panel */\n /* The last tooltip of the panel is not centered on top of the button\n * but rather aligned with the right border of the control panel */\n}\n.shaka-tooltips-on > [class*=\"shaka-tooltip\"] {\n position: relative;\n /* The :after pseudo-element contains the tooltip */\n}\n.shaka-tooltips-on > [class*=\"shaka-tooltip\"]:hover:after,\n.shaka-tooltips-on > [class*=\"shaka-tooltip\"]:focus-visible:after,\n.shaka-tooltips-on > [class*=\"shaka-tooltip\"]:active:after {\n content: attr(aria-label);\n /* Override .material-icons-round text styling */\n font-family: Roboto-Regular, Roboto, sans-serif;\n line-height: 16px;\n white-space: nowrap;\n font-size: 13px;\n /* Styling */\n background: rgba(35, 35, 35, 0.9);\n color: white;\n border-radius: 3px;\n padding: 5px 10px;\n /* Positioning */\n position: absolute;\n bottom: 37px;\n /* Left attribute is set to half of the width of the parent button */\n left: 16px;\n /* The tooltip is also translated 50% to appear centered */\n -webkit-transform: translateX(-50%);\n -moz-transform: translateX(-50%);\n -ms-transform: translateX(-50%);\n -o-transform: translateX(-50%);\n transform: translateX(-50%);\n}\n.shaka-tooltips-on > .shaka-tooltip-status:hover:after,\n.shaka-tooltips-on > .shaka-tooltip-status:focus-visible:after,\n.shaka-tooltips-on > .shaka-tooltip-status:active:after {\n content: attr(aria-label) \" (\" attr(shaka-status) \")\";\n}\n.shaka-tooltips-on button:first-child:hover:after,\n.shaka-tooltips-on button:first-child:focus-visible:after,\n.shaka-tooltips-on button:first-child:active:after {\n left: 0;\n -webkit-transform: translateX(0%);\n -moz-transform: translateX(0%);\n -ms-transform: translateX(0%);\n -o-transform: translateX(0%);\n transform: translateX(0%);\n}\n.shaka-tooltips-on button:last-child:hover:after,\n.shaka-tooltips-on button:last-child:focus-visible:after,\n.shaka-tooltips-on button:last-child:active:after {\n left: 32px;\n -webkit-transform: translateX(-100%);\n -moz-transform: translateX(-100%);\n -ms-transform: translateX(-100%);\n -o-transform: translateX(-100%);\n transform: translateX(-100%);\n}\n@font-face {\n font-family: 'Roboto';\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxP.ttf) format('truetype');\n}\n\n@font-face {\n font-family: 'Material Icons Round';\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialiconsround/v106/LDItaoyNOAY6Uewc665JcIzCKsKc_M9flwmM.otf) format('opentype');\n}\n\n.material-icons-round {\n font-family: 'Material Icons Round';\n font-weight: normal;\n font-style: normal;\n font-size: 24px;\n line-height: 1;\n letter-spacing: normal;\n text-transform: none;\n display: inline-block;\n white-space: nowrap;\n word-wrap: normal;\n direction: ltr;\n}\n\n","/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/* General utility mixins and classes with broad applicability. */\n\n/* Make a thing unselectable. There are currently no cases where we make it\n * selectable again. */\n.unselectable() {\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n}\n\n.hidden() {\n display: none;\n}\n\n.shaka-hidden {\n /* Make this override equally specific classes.\n * If it's hidden, always hide it! */\n display: none !important;\n}\n\n.fill-container() {\n width: 100%;\n height: 100%;\n}\n\n.bottom-align-children() {\n display: flex;\n justify-content: flex-end;\n flex-direction: column;\n}\n\n.bottom-panels-elements-margin() {\n margin: 1px 6px;\n}\n\n/* For containers which host elements overlaying other things. */\n.overlay-parent() {\n /* For a detailed explanation of how this achieves an overlay, please refer\n * to https://developer.mozilla.org/en-US/docs/Web/CSS/position .\n *\n * But you don't have to, because we've encapsulated these high level\n * concepts into classes.\n *\n * This makes it possible for some children of this container to overlay the\n * others using .overlay-child(). */\n position: relative;\n\n /* Make sure any top or left styles applied from outside don't move this from\n * it's original position, now that it's relative to that original position.\n * This is a defensive move that came out of intensive debugging on IE 11. */\n top: 0;\n left: 0;\n}\n\n/* For things which overlay other things. */\n.overlay-child() {\n /* For a detailed explanation of how this achieves an overlay, please refer\n * to https://developer.mozilla.org/en-US/docs/Web/CSS/position .\n *\n * But you don't have to, because we've encapsulated these high level\n * concepts into classes.\n *\n * This makes it possible for this child to overlay the other children of a\n * .overlay-parent() object. */\n position: absolute;\n\n /* Fill the container by default. */\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n margin: 0;\n padding: 0;\n\n .fill-container();\n}\n\n.absolute-position() {\n /* When setting \"position: absolute\" it uses the left,right,top,bottom\n * properties to determine the positioning. We should set all these\n * properties to ensure it is positioned properly on all platforms. */\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n}\n\n/* For things that should not shrink inside a flex container.\n * This will be used for all controls by default. */\n.unshrinkable() {\n flex-shrink: 0;\n}\n\n/* Use this to override .unshrinkable() in particular cases that *should* shrink\n * inside a flex container. */\n.shrinkable() {\n flex-shrink: 1;\n}\n\n.show-when-controls-shown() {\n /* Transparent unless explicitly made opaque through container attributes. */\n opacity: 0;\n\n /* When we show/hide this, do it gradually using cubic-bezier timing. */\n transition: opacity cubic-bezier(0.4, 0, 0.6, 1) 600ms;\n\n /* Show controls when the container's \"shown\" or \"casting\" attributes are\n * set. */\n .shaka-controls-container[shown=\"true\"] &,\n .shaka-controls-container[casting=\"true\"] & {\n opacity: 1;\n }\n}\n\n.hide-when-shaka-controls-disabled() {\n .shaka-video-container:not([shaka-controls=\"true\"]) & {\n .hidden();\n }\n}\n\n/* The width of the bottom-section controls: seek bar, ad controls, and\nthe control buttons panel. */\n@bottom-controls-width: 96%;\n","/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/* All of the top-level containers into which various visible features go. */\n\n@transparent: rgba(0, 0, 0, 0);\n\n/* A container for the entire video + controls combo. This is the auto-setup\n * div which we populate. */\n.shaka-video-container {\n .overlay-parent();\n\n /* Without this, the container somehow winds up being a tad taller than it\n * should be (484px vs 480px). */\n display: flex;\n\n /* Set a special font for material design icons. */\n .material-icons-round {\n font-family: 'Material Icons Round';\n font-size: 24px;\n }\n\n /* Set the fonts for all other content. */\n * {\n font-family: Roboto-Regular, Roboto, sans-serif;\n }\n}\n\n/* Each browser has a different prefixed pseudo-class for fullscreened elements.\n * Define the properties of a fullscreened element in a mixin, then apply to\n * each of the browser-specific pseudo-classes.\n * NOTE: These fullscreen pseudo-classes can't be combined with commas into a\n * single delcaration. Browsers ignore the rest of the list once they hit one\n * pseudo-class they don't support. */\n.fullscreen-container() {\n .fill-container();\n\n background-color: black;\n\n .shaka-text-container {\n /* In fullscreen mode, the text displayer's font size should be relative to\n * the either window height or width (whichever is smaller), instead of a\n * fixed size. */\n font-size: 4.4vmin;\n }\n}\n.shaka-video-container:fullscreen { .fullscreen-container(); }\n.shaka-video-container:-webkit-full-screen { .fullscreen-container(); }\n.shaka-video-container:-moz-full-screen { .fullscreen-container(); }\n.shaka-video-container:-ms-fullscreen { .fullscreen-container(); }\n\n/* The actual video element. Sits inside .shaka-video-container and gives it a\n * size in non-fullscreen mode. In fullscreen mode, the sizing relationship\n * flips. CSS is just great like that. :-( */\n.shaka-video {\n /* At the moment, nothing special is required here.\n * Note that this should NOT be an overlay-child, as its size could dictate\n * the size of the container for some applications. */\n}\n\n/* A container for all controls, including the giant play button, seek bar, etc.\n * Sits inside .shaka-video-container, on top of (Z axis) .shaka-video, and\n * below (Y axis) .shaka-play-button-container. */\n.shaka-controls-container {\n .overlay-child();\n\n .hide-when-shaka-controls-disabled();\n\n /* Without this, the controls container overflows the video container. */\n box-sizing: border-box;\n\n /* A flex container, to make layout of children easier to reason about. */\n display: flex;\n\n /* Defines in which direction the children should flow. */\n flex-direction: column;\n\n /* Pushes the children toward the bottom of the container. */\n justify-content: flex-end;\n\n /* Centers children horizontally. */\n align-items: center;\n\n /* By default, do not allow any of our controls to shrink.\n * Specific controls can use .shrinkable() to override. */\n * { .unshrinkable(); }\n\n /* Position the controls container in front of the text container, so that\n * the text container doesn't interfere with the control buttons. */\n z-index: 1;\n\n &[casting=\"true\"] {\n /* Hide fullscreen button while casting. */\n .shaka-fullscreen-button {\n .hidden();\n }\n }\n}\n\n/* Container for controls positioned at the bottom of the video container:\n * controls button panel and the seek bar. */\n.shaka-bottom-controls {\n width: @bottom-controls-width;\n padding: 0;\n padding-bottom: 2.5%;\n\n /* Position the bottom panel in front of other controls (play button and\n * spinner containers).\n * TODO: A different layout arrangement might be a better solution for this.\n * Need to experiment.\n */\n z-index: 1;\n}\n\n/* This is the container for the horizontal row of controls above the seek bar.\n * It sits above (Y axis) the seek bar, and below (Y axis) the giant play button\n * in the middle. */\n.shaka-controls-button-panel {\n /* Fill the space horizontally, with no extra padding or margin. */\n padding: 0;\n margin: 0;\n\n /* This is itself a flex container, with children layed out horizontally. */\n display: flex;\n flex-direction: row;\n\n /* Push children to the right. */\n justify-content: flex-end;\n\n /* Center children vertically. */\n align-items: center;\n\n /* TODO: Document why. */\n overflow: hidden;\n min-width: 48px;\n\n /* Make sure we don't inherit odd font sizes and styles from the environment.\n * TODO: When did this happen? What forced us to do this? */\n font-size: 12px;\n font-weight: normal;\n font-style: normal;\n\n /* Make sure contents cannot be selected. */\n .unselectable();\n\n .show-when-controls-shown();\n\n /* All buttons, divs, and other controls directly inside this panel should\n * have these characteristics by default. */\n & > * {\n /* White text or button icons. */\n color: white;\n\n /* 32px tall controls. */\n height: 32px;\n\n /* Consistent alignment of buttons. */\n line-height: 0.5;\n\n /* Consistent margins (external) and padding (internal) between controls. */\n .bottom-panels-elements-margin();\n\n padding: 0;\n\n /* Transparent backgrounds, no borders, and a pointer when you mouse over\n * them. */\n background: transparent;\n border: 0;\n cursor: pointer;\n }\n}\n\n/* Buttons hide certain items if they are found inside the control panel */\n.shaka-controls-button-panel .shaka-overflow-menu-only {\n display: none;\n}\n\n/* The container for the giant play button. Sits above (Y axis) the\n * other video controls and seek bar, in the middle of the video frame, on top\n * of (Z axis) the video. */\n.shaka-play-button-container {\n /* Take up as much space as possible, but shrink (vertically) to accomodate\n * the controls at the bottom. */\n margin: 0;\n .fill-container();\n .shrinkable();\n .absolute-position();\n\n /* Keep the play button in the middle of this container. */\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.shaka-statistics-container {\n overflow-x: hidden;\n overflow-y: auto;\n\n min-width: 300px;\n\n color: white;\n background-color: rgba(35, 35, 35, 0.9);\n\n font-size: 14px;\n\n padding: 5px 10px;\n border-radius: 2px;\n\n position: absolute;\n z-index: 2;\n left: 15px;\n top: 15px;\n\n /* Fades out with the other controls. */\n .show-when-controls-shown();\n\n div {\n display: flex;\n justify-content: space-between;\n }\n\n span {\n color: rgb(150, 150, 150);\n }\n}\n\n.shaka-context-menu {\n background-color: rgba(35, 35, 35, 0.9);\n\n border-radius: 2px;\n\n position: absolute;\n z-index: 3;\n\n button {\n padding: 5px 10px;\n\n width: 100%;\n display: flex;\n align-items: center;\n\n color: white;\n background: transparent;\n border: 0;\n cursor: pointer;\n\n &:hover {\n background-color: rgba(50, 50, 50, 0.9);\n }\n }\n\n label {\n padding: 0 20px;\n\n align-items: flex-start;\n\n color: white;\n cursor: pointer;\n }\n\n .shaka-current-selection-span {\n align-items: flex-start;\n\n color: white;\n cursor: pointer;\n }\n}\n\n.shaka-scrim-container {\n margin: 0;\n .fill-container();\n .shrinkable();\n .absolute-position();\n .show-when-controls-shown();\n\n /* A black gradient at the bottom, behind the controls, but only so high. */\n background: linear-gradient(to top, rgba(0, 0, 0, 1) 0, @transparent 15%);\n}\n\n.shaka-text-container {\n .absolute-position();\n\n /* Make sure the text container doesn't steal pointer events from another\n * layer, such as the ad container. There is nothing interactive in this\n * layer, so this should be fine. */\n pointer-events: none;\n\n /* Place the text container on the bottom of the video container. */\n bottom: 0%;\n width: 100%;\n min-width: 48px;\n\n /* When the controls fade in or out, it takes 600ms. Thus, when the text\n * container adjusts to the presence or absence of controls, we should wait\n * briefly, so the captions don't end up appearing behind the controls.\n * Instead of being a gradual animation, this is a fast animation with a\n * significant delay, since the captions moving around is a little\n * distracting. */\n transition: bottom cubic-bezier(0.4, 0, 0.6, 1) 100ms;\n transition-delay: 500ms;\n\n /* These are defaults which are overridden by JS or cue styles. */\n font-size: 20px;\n line-height: 1.4; // relative to font size.\n color: rgb(255, 255, 255);\n\n span.shaka-text-wrapper {\n display: inline;\n background: none;\n }\n}\n\n.shaka-controls-container[shown=\"true\"] ~ .shaka-text-container {\n /* While the controls are shown, the text container should avoid the 15%\n * at the bottom of the video, to avoid overlapping with controls. */\n bottom: 15%;\n\n /* Disable the transition delay when moving the captions up, so that the\n * controls don't appear over the captions. */\n transition-delay: 0ms;\n}\n\n/* The buffering spinner. */\n.shaka-spinner-container {\n .absolute-position();\n .fill-container();\n .hide-when-shaka-controls-disabled();\n\n flex-shrink: 1;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n@spinner-size-percentage: 15.6%;\n\n.shaka-spinner {\n /* This uses the same trickery as the big play button define\n the spinner's width and height. See .shaka-play-button\n for the detailed explanation. */\n\n /* For the padding thing to work, spinner div needs to be an\n overlay-parent and spinner svg - an overlay child. */\n .overlay-parent();\n\n margin: 0;\n box-sizing: border-box;\n padding: @spinner-size-percentage / 2;\n width: 0;\n height: 0;\n\n /* Add a bit of a white shadow to keep our black spinner visible\n on a black background. */\n filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.5));\n}\n","/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/* The main buttons in the UI controls. */\n\n@play-button-size-percentage: 15%;\n\n.disabled-button() {\n /* Set the background and the color, otherwise it might be overwritten by\n * the css styles in demo. */\n background-color: transparent;\n color: white;\n cursor: default;\n}\n\n/* The giant play button, which sits inside .shaka-player-button-container. */\n.shaka-play-button {\n /* Set width & height in a round-about way. By using padding, we can keep\n * a 1:1 aspect ratio and size the button relative to the container width.\n *\n * Since padding is applied equally to top, bottom, left, and right, only use\n * half of the intended percentage for each.\n *\n * Based on tips from https://stackoverflow.com/a/12925343 */\n box-sizing: border-box;\n padding: @play-button-size-percentage / 2;\n width: 0;\n height: 0;\n\n /* To be properly positioned in the center, this should have no margin.\n * This might have been set for buttons generally by the app or user-agent. */\n margin: 0;\n\n /* This makes the button a circle. */\n border-radius: 50%;\n\n /* A small drop shadow below the button. */\n box-shadow: rgba(0, 0, 0, 0.1) 0 0 20px 0;\n\n /* No border. */\n border: none;\n\n /* The play arrow is a picture. It is treated a background image.\n * The following settings ensure it shows only once and in the\n * center of the button. */\n background-size: 50%;\n background-repeat: no-repeat;\n background-position: center center;\n\n /* A background color behind the play arrow. */\n background-color: rgba(255, 255, 255, 0.9);\n\n .show-when-controls-shown();\n\n /* Actual icon images for the two states this could be in.\n * These will be inlined as data URIs when compiled, and so do not need to be\n * deployed separately from the compiled CSS.\n * Note that these URIs should relative to ui/controls.less, not this file. */\n &[icon=\"play\"] {\n background-image: data-uri('images/play_arrow.svg');\n }\n\n &[icon=\"pause\"] {\n background-image: data-uri('images/pause.svg');\n }\n}\n\n/* This button contains the current time and duration of the video.\n * It's only clickable when the content is live, and current time is behind live\n * edge. Otherwise, the button is disabled.\n */\n.shaka-current-time {\n font-size: 14px;\n color: rgb(255, 255, 255);\n cursor: pointer;\n\n &[disabled] {\n .disabled-button();\n }\n}\n\n/* Use a consistent outline focus style across browsers. */\n.shaka-controls-container {\n button:focus, input:focus {\n /* Most browsers will fall back to \"Highlight\" (system setting) color for\n * the focus outline. */\n outline: 1px solid Highlight;\n }\n\n /* Disable this Mozilla-specific focus ring, since we have an outline defined\n * for focus. */\n button:-moz-focus-inner, input:-moz-focus-outer {\n outline: none;\n border: 0;\n }\n}\n\n/* Outline on focus is important for accessibility, but\n * it doesn't look great. This removes the outline for\n * mouse users while leaving it for keyboard users. */\n.shaka-controls-container:not(.shaka-keyboard-navigation) {\n button:focus, input:focus {\n outline: none;\n }\n}\n","/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/* Special styles for input elements with type \"range\".\n *\n * These elements are composed of two main parts: a \"track\", which is the\n * horizontal bar, and the \"thumb\", which is the knob which slides along that\n * bar.\n *\n * In order to style the track across browsers (cough, IE 11), we need to do\n * something a bit tricky. Styling the track is a nightmare, especially if you\n * want the thumb to be larger. On IE 11, this gets clipped at the track size.\n * So a tiny track with a large thumb is not easily achieved. It can be done,\n * but the techniques for it are incompatible with the gradient background we\n * want to apply to it.\n *\n * The solution is to put the input inside a div container, and apply the\n * background gradient styles to the container. The container will act as a\n * visible, virtual track, inside which is contained a larger, invisible track,\n * in which is contained a visible thumb. This way, the thumb is not larger\n * than the actual track (for IE 11's sake), but can be larger than the virtual\n * track. And since we are still using a semantically correct input element,\n * the element is inherently accessible. */\n\n/* These control the color and size of the various pieces. */\n@thumb-color: white;\n@track-default-color: white;\n@thumb-size: 12px;\n@track-height: 4px;\n\n/* The range container is the div that contains a range element.\n * This div will act as a virtual track to allow us to style the track space.\n * An actual track still exists inside the range element, but is transparent. */\n.range-container() {\n /* This contains an input element which overlays it. */\n .overlay-parent();\n\n /* Vertical margins to occupy the same space as the thumb. */\n margin: (@thumb-size - @track-height)/2 6px;\n\n /* Smaller height to contain the background for the virtual track. */\n height: @track-height;\n\n /* Rounded ends on the virtual track. */\n border-radius: @track-height;\n\n /* Until we set a gradient background in JS, this will be the track color. */\n background: @track-default-color;\n}\n\n/* The \"track\" is the pseudo-element inside the range element which represents\n * the horizontal bar on which the \"thumb\" (knob) moves. */\n.track() {\n /* The track should fill the range element. */\n width: 100%;\n\n /* The track should be tall enough to contain the thumb without clipping it.\n * It is very tricky to make the thumb show outside the track on IE 11, and\n * it is incompatible with our background gradients. */\n height: @thumb-size;\n\n /* Some browsers have default backgrounds, colors, or borders for this.\n * Hide them all. */\n background: transparent;\n color: transparent;\n border: none;\n}\n\n/* The \"thumb\" is the pseudo-element inside the range element which represents\n * the knob which moves along the \"track\" (bar). */\n.thumb() {\n /* Remove default styles on WebKit-based and Blink-based browsers. */\n -webkit-appearance: none;\n\n /* On some browsers (IE 11), the thumb has a border, which affects the size.\n * Disable it. */\n border: none;\n\n /* Make the thumb a circle and set its diameter. */\n border-radius: @thumb-size;\n height: @thumb-size;\n width: @thumb-size;\n\n /* Give it the desired color. */\n background: @thumb-color;\n}\n\n/* This is the actual range input element. */\n.range-element() {\n /* Remove any browser styling of the range element. */\n -webkit-appearance: none;\n background: transparent;\n\n /* Overlay and fill the container div. */\n .overlay-child();\n\n /* The range element should be big enough to contain the thumb without\n * clipping it. It is very tricky to make the thumb show outside the track\n * on IE 11. */\n height: @thumb-size;\n\n /* Position the top of the range element so that it is centered on the\n * container. Note that the container is actually smaller than the thumb. */\n top: (@track-height - @thumb-size) / 2;\n\n /* Make sure clicking at the very top of the bar still takes effect and is not\n * confused with clicking the video to play/pause it. */\n z-index: 1;\n\n /* Pseudo-elements for Blink-based or WebKit-based browsers. */\n &::-webkit-slider-runnable-track {\n .track();\n }\n\n &::-webkit-slider-thumb {\n .thumb();\n }\n\n /* Pseudo-elements for Gecko-based browsers. */\n &::-moz-range-track {\n .track();\n }\n\n &::-moz-range-thumb {\n .thumb();\n }\n}\n\n.shaka-range-container {\n .range-container();\n}\n\n.shaka-volume-bar-container {\n width: 100px;\n}\n\n.shaka-range-element {\n .range-element();\n}\n\n.shaka-seek-bar-container {\n .show-when-controls-shown();\n}\n\n.shaka-ad-markers {\n .overlay-child();\n}\n","/*!\n * @license\n * The SVG/CSS buffering spinner is based on http://codepen.io/jczimm/pen/vEBpoL\n * Some local modifications have been made.\n *\n * Copyright (c) 2016 by jczimm\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/* This is the spinner SVG itself, which contains a circular path element.\n * It sits inside the play button and fills it. */\n.shaka-spinner-svg {\n /* Because of some sizing hacks in the play button (see comments there), this\n * spinner needs to be an overlay child to be properly sized and positioned\n * within the button. */\n .overlay-child();\n\n /* Keep it spinning! */\n animation: rotate 2s linear infinite;\n transform-origin: center center;\n\n /* The SVG should fill its container. */\n width: 100%;\n height: 100%;\n margin: 0;\n padding: 0;\n}\n\n/* This is the path element, which draws a circle. */\n.shaka-spinner-path {\n stroke: #202124;\n stroke-dasharray: 20, 200;\n stroke-dashoffset: 0;\n\n /* Animate the stroke of this circular path. */\n animation: dash 1s ease-in-out infinite;\n\n /* Round the line on the ends. */\n stroke-linecap: round;\n}\n\n/* Spin the whole SVG. */\n@keyframes rotate {\n 100% {\n transform: rotate(360deg);\n }\n}\n\n/* Pulse the circle's outline forward and backward while it spins. */\n@keyframes dash {\n 0% {\n stroke-dasharray: 1, 200;\n stroke-dashoffset: 0;\n }\n\n 50% {\n stroke-dasharray: 89, 200;\n stroke-dashoffset: -35px;\n }\n\n 100% {\n stroke-dasharray: 89, 200;\n stroke-dashoffset: -124px;\n }\n}\n","/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/* UI elements that did not fit into the buttons/range elements category. */\n\n/* This is a spacer element used to separate elements within the control\n * buttons panel. It's just an empty div of certain width. */\n.shaka-spacer {\n /* This should not have a pointer-style cursor like the other controls. */\n cursor: default;\n\n /* Make the element shrink to accommodate things to the right. */\n .shrinkable();\n\n /* Make the element grow to take up the remaining space. */\n flex-grow: 1;\n\n /* Margins don't shrink. Remove margins in order to be more flexible when\n * shrinking. */\n margin: 0;\n}\n","/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/* The overflow menu and all settings submenus. These appear on top of all\n * other controls (Z axis) when the overflow button is clicked. */\n.shaka-overflow-menu,\n.shaka-settings-menu {\n /* It's okay to add a vertical scroll if there are too many items, but\n * horizontal scrolling is not allowed. */\n overflow-x: hidden;\n overflow-y: auto;\n\n /* Don't wrap text to the next line. */\n white-space: nowrap;\n\n /* Styles for the menu itself. */\n background: white;\n box-shadow: 0 1px 9px 0 rgba(0, 0, 0, 0.4);\n border-radius: 2px;\n max-height: 250px;\n min-width: 180px;\n\n /* The menus fade out with the other controls. */\n .show-when-controls-shown();\n\n /* When displayed as a flex container, elements inside will flow in a\n * vertical column. */\n display: flex;\n flex-direction: column;\n\n /* Where the menu appears. */\n position: absolute;\n z-index: 2;\n right: 15px;\n bottom: 30px;\n\n /* The buttons inside the menu. */\n button {\n font-size: 14px;\n background: transparent;\n color: black;\n border: none;\n min-height: 30px;\n padding: 3.5px 6px;\n\n /* The button itself is a flex container, with children center-aligned. */\n display: flex;\n align-items: center;\n\n /* When hovered, the button's background is highlighted. */\n &:hover {\n background: rgb(224, 224, 224);\n }\n\n /* The button is clickable, showing cursor pointer */\n cursor: pointer;\n\n /* The label inside button is also showing cursor pointer */\n label {\n cursor: pointer;\n }\n\n .shaka-keyboard-navigation &:focus {\n background: rgb(224, 224, 224);\n }\n }\n\n /* These are the elements which contain the material design icons.\n * TODO: Pull MD icon details out of JS. */\n i {\n /* TODO(b/116651454): eliminate hard-coded offsets */\n padding-left: 10px;\n padding-right: 10px;\n }\n\n /* If the seekbar is missing, this is positioned lower.\n * TODO: Solve with flex layout instead? */\n &.shaka-low-position {\n /* TODO(b/116651454): eliminate hard-coded offsets */\n bottom: 15px;\n }\n}\n\n/* The span elements inside the top-level overflow menu contain single lines\n * of text, which are the button name and the current selection. For example,\n * a captions button might have \"Captions\" in one span (the button name), and\n * \"Farsi\" in another (the current selection).\n * These are displayed inside a .shaka-overflow-button-label grouping, to the\n * right of MD icons. */\n.shaka-overflow-menu span {\n text-align: left;\n}\n\n/* This contains span elements with single lines of text, and appears to the\n * right of MD icons. */\n.shaka-overflow-button-label {\n position: relative;\n\n /* This is a flex container, whose children flow vertically. */\n display: flex;\n flex-direction: column;\n}\n\n/* This is the specific span element which shows the current selection from some\n * submenu. For example, it would contain the currently-selected subtitle\n * language, the currently-selected resolution, etc. */\n.shaka-current-selection-span {\n /* This is dimmer than the other span, which is the name of the submenu. */\n color: rgba(0, 0, 0, 0.54);\n}\n\n/* The submenus have somewhat different margins inside them. */\n.shaka-settings-menu {\n span {\n /* TODO(b/116651454): eliminate hard-coded offsets */\n margin-left: 54px;\n }\n}\n\n/* This is a button within each submenu that takes you back to the main overflow\n * menu. */\n.shaka-back-to-overflow-button {\n /* The label inside the button, which says something like \"back\". */\n span {\n /* TODO(b/116651454): eliminate hard-coded offsets */\n margin-left: 0;\n }\n\n /* The MD icon for the \"back\" arrow. */\n i {\n /* TODO(b/116651454): eliminate hard-coded offsets */\n padding-right: 20px;\n }\n}\n\n/* The menu item for resolutions which contains \"auto\". */\n.shaka-auto-span {\n /* TODO(b/116651454): eliminate hard-coded offsets */\n left: 17px;\n}\n","/** @license\n * Shaka Player\n * Copyright 2016 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/* Ad controls. */\n.ad-text-shadow() {\n /* Give white text a black shadow, so it's visible against a\n * white background. */\n text-shadow: 1px 1px 4px black;\n}\n\n.shaka-controls-container[ad-active=\"true\"] {\n /* While showing an ad, pass pointer events through to the ad container. */\n pointer-events: none;\n\n /* Except in the bottom controls, which should still be clickable. */\n .shaka-bottom-controls {\n pointer-events: auto;\n }\n}\n\n.shaka-client-side-ad-container, .shaka-server-side-ad-container {\n .absolute-position();\n\n /* IMA SDK adds their own ad UI into an iframe element.\n * Adjust its position to fit in with our UI, when\n * Shaka UI is enabled. */\n iframe {\n .shaka-video-container[shaka-controls=\"true\"] & {\n /* This moves the iframe up a little bit, so it\n * doesn't operlap with our controls. */\n height: 90%;\n }\n }\n}\n\n.shaka-server-side-ad-container {\n .fill-container();\n .shrinkable();\n\n &:not([ad-active=\"true\"]) {\n pointer-events: none;\n }\n}\n\n.shaka-ad-controls {\n .hide-when-shaka-controls-disabled();\n\n display: flex;\n flex-direction: row;\n z-index: 1;\n\n /* Add some room between the ad controls and the controls\n button panel. */\n padding-bottom: 1%;\n\n button, div {\n color: white;\n font-size: initial;\n }\n\n div:not(.shaka-skip-ad-counter) {\n .bottom-panels-elements-margin();\n }\n}\n\n.shaka-ad-counter, .shaka-ad-position {\n .bottom-align-children();\n .ad-text-shadow();\n}\n\n.shaka-skip-ad-container {\n /* Skip button is positioned at the very right edge of the\n * video container unlike the rest of the bottom controls. */\n position: relative;\n\n /* This math is determining how far the button is from the right edge.\n * Ad panel's parent is centered and @bottom-controls-width wide, so\n * 100 - @bottom-controls-width = margins from both sides of the container.\n * That divided by 2 is margin on one side, so we take that, and move the\n * button from its normal position to the right by that percentage.\n */\n right: (100 - @bottom-controls-width) / 2 * -1;\n display: flex;\n flex-direction: row;\n margin: 0;\n}\n\n.shaka-skip-ad-button {\n padding: 5px 15px;\n background: rgba(0, 0, 0, 0.7);\n border: none;\n\n &:disabled {\n background: rgba(0, 0, 0, 0.3);\n }\n\n cursor: pointer;\n}\n\n.shaka-skip-ad-counter {\n padding: 5px 5px;\n background: rgba(0, 0, 0, 0.7);\n margin: 0;\n}\n","/*!\n * @license\n * The tooltip is based on https://github.com/felipefialho/css-components/\n * Local modifications have been performed.\n *\n * Copyright (c) 2017 Felipe Fialho\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n@material-icons-width: 32px;\n\n.translateX(@percent) {\n -webkit-transform: translateX(percentage(@percent));\n -moz-transform: translateX(percentage(@percent));\n -ms-transform: translateX(percentage(@percent));\n -o-transform: translateX(percentage(@percent));\n transform: translateX(percentage(@percent));\n}\n\n/* .shaka-tooltips-on enables the tooltips and is only added to the\n * control panel when the 'enableTooltips' option is set to true */\n.shaka-tooltips-on {\n overflow: visible;\n\n & > [class*=\"shaka-tooltip\"] {\n position: relative;\n\n /* The :after pseudo-element contains the tooltip */\n &:hover:after, &:focus-visible:after, &:active:after {\n content: attr(aria-label);\n\n /* Override .material-icons-round text styling */\n font-family: Roboto-Regular, Roboto, sans-serif;\n line-height: @material-icons-width / 2;\n white-space: nowrap;\n font-size: 13px;\n\n /* Styling */\n background: rgba(35, 35, 35, 0.9);\n color: white;\n border-radius: 3px;\n padding: 5px 10px;\n\n /* Positioning */\n position: absolute;\n bottom: @material-icons-width + 5px;\n\n /* Left attribute is set to half of the width of the parent button */\n left: @material-icons-width / 2;\n\n /* The tooltip is also translated 50% to appear centered */\n .translateX(-0.5);\n }\n }\n\n /* Adds an additional attribute for the status in .shaka-tooltip-status */\n & > .shaka-tooltip-status {\n &:hover:after, &:focus-visible:after, &:active:after {\n content: attr(aria-label) \" (\" attr(shaka-status) \")\";\n }\n }\n\n /* The first tooltip of the panel is not centered on top of the button\n * but rather aligned with the left border of the control panel */\n button:first-child {\n &:hover:after, &:focus-visible:after, &:active:after {\n left: 0;\n .translateX(0);\n }\n }\n\n /* The last tooltip of the panel is not centered on top of the button\n * but rather aligned with the right border of the control panel */\n button:last-child {\n &:hover:after, &:focus-visible:after,&:active:after {\n left: @material-icons-width;\n .translateX(-1);\n }\n }\n}\n","@font-face {\n font-family: 'Roboto';\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxP.ttf) format('truetype');\n}\n","@font-face {\n font-family: 'Material Icons Round';\n font-style: normal;\n font-weight: 400;\n src: url(https://fonts.gstatic.com/s/materialiconsround/v106/LDItaoyNOAY6Uewc665JcIzCKsKc_M9flwmM.otf) format('opentype');\n}\n\n.material-icons-round {\n font-family: 'Material Icons Round';\n font-weight: normal;\n font-style: normal;\n font-size: 24px;\n line-height: 1;\n letter-spacing: normal;\n text-transform: none;\n display: inline-block;\n white-space: nowrap;\n word-wrap: normal;\n direction: ltr;\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/Resources/Public/Css/Kitodo.css b/Resources/Public/Css/Kitodo.css new file mode 100644 index 000000000..03268bc72 --- /dev/null +++ b/Resources/Public/Css/Kitodo.css @@ -0,0 +1,9 @@ +body.page-single .shown-if-double, +body.page-double .shown-if-single { + display: none !important; +} + +/* TODO(client-side): Better condition? */ +.dlf-toc-collapsed ul { + display: none; +} diff --git a/Resources/Public/Icons/tx-dlf-document.svg b/Resources/Public/Icons/tx-dlf-document.svg new file mode 100644 index 000000000..02641b72b --- /dev/null +++ b/Resources/Public/Icons/tx-dlf-document.svg @@ -0,0 +1,74 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/Resources/Public/JavaScript/PageView/Controller.js b/Resources/Public/JavaScript/PageView/Controller.js new file mode 100644 index 000000000..60391fefd --- /dev/null +++ b/Resources/Public/JavaScript/PageView/Controller.js @@ -0,0 +1,231 @@ +/** + * (c) Kitodo. Key to digital objects e.V. + * + * This file is part of the Kitodo and TYPO3 projects. + * + * @license GNU General Public License version 3 or later. + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + */ + +class dlfController { + /** + * + * @param {dlf.Loaded} doc + */ + constructor(doc) { + /** @private */ + this.doc = doc; + + this.eventTarget.addEventListener("tx-dlf-stateChanged", this.onStateChanged.bind(this)); + window.addEventListener("popstate", this.onPopState.bind(this)); + + // Set initial state, so that browser navigation also works initial page + history.replaceState(/** @type {dlf.PageHistoryState} */({ + type: "tx-dlf-page-state", + ...this.doc.state + }), ""); + + this.updateMultiPage(this.simultaneousPages); + + if (doc.metadataUrl !== null) { + this.metadataPromise = fetch(doc.metadataUrl) + .then((response) => response.text()) + .then((html) => ({ + htmlCode: html, + })); + } else { + this.metadataPromise = Promise.reject(); + } + } + + /** + * Get event target on which stateChanged events are dispatched. + * + * TODO(client-side): Either make this customizable, e.g. use some top-level wrapper element, or make dlfController the EventTarget + * + * @returns {EventTarget} + */ + get eventTarget() { + return document.body; + } + + get documentId() { + return this.doc.state.documentId; + } + + get currentPageNo() { + return this.doc.state.page; + } + + get simultaneousPages() { + return this.doc.state.simultaneousPages; + } + + getVisiblePages(firstPageNo = this.doc.state.page) { + const result = []; + + for (let i = 0; i < this.simultaneousPages; i++) { + const pageNo = firstPageNo + i; + const pageObj = this.doc.document.pages[pageNo - 1]; + + if (pageObj !== undefined) { + result.push({ pageNo, pageObj }); + } + } + + return result; + } + + /** + * + * @param {number} pageNo + * @returns {dlf.PageObject | undefined} + */ + getPageByNo(pageNo) { + return this.doc.document.pages[pageNo - 1]; + } + + get numPages() { + return this.doc.document.pages.length; + } + + /** + * + * @param {number} pageNo + * @param {string[]} fileGroups + * @returns {dlf.ResourceLocator | undefined} + */ + findFileByGroup(pageNo, fileGroups) { + const pageObj = this.getPageByNo(pageNo); + + if (pageObj === undefined) { + return; + } + + return dlfUtils.findFirstSet(pageObj.files, fileGroups); + } + + /** + * + * @param {number} pageNo + * @param {dlf.FileKind} fileKind + * @returns {dlf.ResourceLocator | undefined} + */ + findFileByKind(pageNo, fileKind) { + return this.findFileByGroup(pageNo, this.doc.fileGroups[fileKind]); // eslint-disable-line + } + + fetchMetadata() { + return this.metadataPromise; + } + + /** + * @param {dlf.StateChangeDetail} detail + */ + changeState(detail) { + // TODO(client-side): Consider passing full new state in stateChanged event, then reduce usage of currentPageNo and simultaneousPages properties + if (detail.page !== undefined) { + this.doc.state.page = detail.page; + } + if (detail.simultaneousPages !== undefined) { + this.doc.state.simultaneousPages = detail.simultaneousPages; + } + document.body.dispatchEvent(new CustomEvent("tx-dlf-stateChanged", { detail })); + } + + /** + * Navigate to given page. + * + * @param {number} pageNo + */ + changePage(pageNo) { + const clampedPageNo = Math.max(1, Math.min(this.numPages, pageNo)); + + if (clampedPageNo !== this.doc.state.page) { + this.changeState({ + source: "navigation", + page: clampedPageNo, + }); + } + } + + /** + * @param {number} pageNo + * @param {boolean} pageGrid + * @returns {string} + */ + makePageUrl(pageNo, pageGrid = false) { + const doublePage = this.simultaneousPages >= 2 ? 1 : 0; + + return this.doc.urlTemplate + .replace(/DOUBLE_PAGE/u, doublePage) + .replace(/PAGE_NO/u, pageNo) + .replace(/PAGE_GRID/u, pageGrid ? "1" : "0"); + } + + /** + * @param {dlf.StateChangeEvent} e + * @private + */ + onStateChanged(e) { + this.pushHistory(e); + + if (e.detail.simultaneousPages !== undefined) { + this.updateMultiPage(e.detail.simultaneousPages); + } + } + + /** + * @param {PopStateEvent} e + * @private + */ + onPopState(e) { + if (e.state == null || e.state.type !== "tx-dlf-page-state") { + return; + } + + const state = /** @type {dlf.PageHistoryState} */(e.state); + + if (state.documentId !== this.doc.state.documentId) { + return; + } + + e.preventDefault(); + this.changeState({ + "source": "history", + "page": state.page === this.currentPageNo ? undefined : state.page, + "simultaneousPages": state.simultaneousPages === this.simultaneousPages ? undefined : state.simultaneousPages + }); + } + + /** + * @param {dlf.StateChangeEvent} e + * @private + */ + pushHistory(e) { + // Avoid loop of pushState/dispatchEvent + if (e.detail.source === "history") { + return; + } + + history.pushState(/** @type {dlf.PageHistoryState} */({ + type: "tx-dlf-page-state", + ...this.doc.state + }), "", this.makePageUrl(this.doc.state.page)); + } + + /** + * @param {number} simultaneousPages + * @private + */ + updateMultiPage(simultaneousPages) { + if (simultaneousPages === 1) { + document.body.classList.add("page-single"); + document.body.classList.remove("page-double"); + } else if (simultaneousPages === 2) { + document.body.classList.remove("page-single"); + document.body.classList.add("page-double"); + } + } +} diff --git a/Resources/Public/JavaScript/PageView/FulltextControl.js b/Resources/Public/JavaScript/PageView/FulltextControl.js index 39366658a..445594640 100644 --- a/Resources/Public/JavaScript/PageView/FulltextControl.js +++ b/Resources/Public/JavaScript/PageView/FulltextControl.js @@ -251,6 +251,13 @@ var dlfViewerFullTextControl = function(map) { * @param {FullTextFeature} fulltextData */ dlfViewerFullTextControl.prototype.loadFulltextData = function (fulltextData) { + // remove previously inserted features + this.layers_.textblock.getSource().clear(); + this.layers_.textline.getSource().clear(); + this.layers_.select.getSource().clear(); + this.textblocks_ = new dlfFulltextSegments(); + this.textlines_ = new dlfFulltextSegments(); + // add features to fulltext layer this.textblockFeatures_ = fulltextData.getTextblocks(); this.layers_.textblock.getSource().addFeatures(this.textblockFeatures_); @@ -268,6 +275,7 @@ dlfViewerFullTextControl.prototype.loadFulltextData = function (fulltextData) { // If the control is *not* yet active, the fulltext is instead rendered on activation. if (this.isActive) { this.showFulltext(this.textblockFeatures_); + this.enableFulltextSelect(); } } }; diff --git a/Resources/Public/JavaScript/PageView/Metadata.js b/Resources/Public/JavaScript/PageView/Metadata.js new file mode 100644 index 000000000..5879eea5d --- /dev/null +++ b/Resources/Public/JavaScript/PageView/Metadata.js @@ -0,0 +1,116 @@ +/** + * (c) Kitodo. Key to digital objects e.V. + * + * This file is part of the Kitodo and TYPO3 projects. + * + * @license GNU General Public License version 3 or later. + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + */ + +const dlfRootline = { + /** + * Only the metadata of the current logical element is shown. + */ + None: 0, + /** + * All rootline metadata is shown. + */ + All: 1, + /** + * Only titledata (toplevel) is shown. + */ + Titledata: 2, +}; + +/** + * Handle dynamic changes to the metadata plugin. + * - Update visibility of sections depending on page + */ +class dlfMetadata { + /** + * + * @param {dlfController} docController + * @param {object} config + * @param {HTMLElement} config.container + * @param {0 | 1 | 2} config.rootline Rootline configuration, see enum {@link dlfRootline}. + */ + constructor(docController, config) { + /** @protected */ + this.docController = docController; + /** @protected */ + this.config = config; + + this.docController.eventTarget.addEventListener("tx-dlf-stateChanged", () => { + this.onStateChanged(); + }); + + this.fetchMetadata(); + } + + /** + * @private + */ + async fetchMetadata() { + try { + const metadata = await this.docController.fetchMetadata(); + const element = document.createElement("div"); + + element.innerHTML = metadata.htmlCode; // eslint-disable-line + const metadataContainer = element.querySelector(".dlf-metadata-container"); + + if (metadataContainer !== null) { + this.config.container.replaceWith(metadataContainer); + this.updateSectionVisibility(); + } + } catch (error) { + /* eslint no-console: ["error", { allow: ["warn", "error"] }] */ + console.warn("Could not fetch additional metadata:", error); + } + } + + /** + * @private + */ + onStateChanged() { + this.updateSectionVisibility(); + } + + /** + * @protected + */ + updateSectionVisibility() { + document.querySelectorAll("[data-metadata-list][data-dlf-section]").forEach((element) => { + let isShown = false; + + for (const page of this.docController.getVisiblePages()) { + if (this.shouldShowSection(page.pageObj, element.getAttribute("data-dlf-section"))) { + isShown = true; + break; + } + } + + element.hidden = !isShown; + }); + } + + /** + * @param {dlf.PageObject} pageObj + * @param {string} section + * @returns {boolean} + * @protected + */ + shouldShowSection(pageObj, section) { + switch (this.config.rootline) { + case dlfRootline.None: + default: + return section === pageObj.logSections[0]; + + case dlfRootline.All: + return pageObj.logSections.includes(section); + + case dlfRootline.Titledata: + return section === pageObj.logSections[pageObj.logSections.length - 1]; + } + } +} diff --git a/Resources/Public/JavaScript/PageView/Navigation.js b/Resources/Public/JavaScript/PageView/Navigation.js new file mode 100644 index 000000000..80468ba3d --- /dev/null +++ b/Resources/Public/JavaScript/PageView/Navigation.js @@ -0,0 +1,177 @@ +/** + * (c) Kitodo. Key to digital objects e.V. + * + * This file is part of the Kitodo and TYPO3 projects. + * + * @license GNU General Public License version 3 or later. + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + */ + +class dlfNavigation { + /** + * + * @param {dlfController} docController + * @param {object} config + * @param {Record} config.features Which navigation features should + * be handled by this instance. + * @param {number} config.basePageSteps Number of pages to skip for long step (not yet considering + * single/double page mode). + */ + constructor(docController, config) { + /** @private */ + this.docController = docController; + + /** @private */ + this.config = config; + + /** + * @private + */ + this.navigationButtons = { + pageStepBack: { + button: document.querySelector(".page-step-back"), + getPage: (prevPageNo) => prevPageNo - this.getLongStep(), + }, + pageBack: { + button: document.querySelector(".page-back"), + // When we're on second page in double-page mode, make sure the "back" button is still shown + getPage: (prevPageNo) => Math.max(1, prevPageNo - this.docController.simultaneousPages), + }, + pageFirst: { + button: document.querySelector(".page-first"), + getPage: (prevPageNo) => 1, + }, + pageStepForward: { + button: document.querySelector(".page-step-forward"), + getPage: (prevPageNo) => prevPageNo + this.getLongStep(), + }, + pageForward: { + button: document.querySelector(".page-forward"), + getPage: (prevPageNo) => prevPageNo + this.docController.simultaneousPages, + }, + pageLast: { + button: document.querySelector(".page-last"), + getPage: (prevPageNo) => this.docController.numPages - (this.docController.simultaneousPages - 1), + }, + }; + + /** @private */ + this.pageSelect = document.querySelector(".page-select"); + + this.registerEvents(); + this.updateNavigationControls(); + } + + /** + * @private + */ + registerEvents() { + for (const [key, value] of Object.entries(this.navigationButtons)) { + + if (this.config.features[key]) { // eslint-disable-line + value.button.addEventListener("click", (e) => { + e.preventDefault(); + + const pageNo = value.getPage(this.docController.currentPageNo); + + this.docController.changePage(pageNo); + }); + } + } + + this.pageSelect.addEventListener("change", (e) => { + e.preventDefault(); + + const pageNo = Number(e.target.value); + + this.docController.changePage(pageNo); + }); + + this.docController.eventTarget.addEventListener("tx-dlf-stateChanged", () => { + this.onStateChanged(); + }); + } + + /** + * @private + */ + onStateChanged() { + this.updateNavigationControls(); + } + + /** + * Number of pages to jump in long step (e.g., 10 pages in single page mode + * vs. 20 pages in double page mode). + * + * @returns {number} + * @protected + */ + getLongStep() { + return this.config.basePageSteps * this.docController.simultaneousPages; + } + + /** + * Update DOM state of navigation buttons and dropdown. (For example, + * enable/disable the buttons depending on current page.) + * + * @private + */ + updateNavigationControls() { + const currentPageNo = this.docController.currentPageNo; + + for (const value of Object.values(this.navigationButtons)) { + const btnPageNo = value.getPage(currentPageNo); + + this.toggleButtonDisabled(value.button, btnPageNo); + this.updateUrl(value.button, btnPageNo); + this.updateText(value.button); + } + + if (this.pageSelect instanceof HTMLSelectElement) { + this.pageSelect.value = currentPageNo.toString(); + } + } + + /** + * Enable/disable the button depending on current page. + * + * @param {Element} button + * @param {number} pageNo + * @private + */ + toggleButtonDisabled(button, pageNo) { + const isBtnPageVisible = this.docController.getVisiblePages(pageNo).some((page) => page.pageNo === this.docController.currentPageNo); + + if (!isBtnPageVisible && 1 <= pageNo && pageNo <= this.docController.numPages) { + button.classList.remove("disabled"); + } else { + button.classList.add("disabled"); + } + } + + /** + * Update URLs of navigation button. + * + * @param {Element} button + * @param {number} pageNo + * @private + */ + updateUrl(button, pageNo) { + button.setAttribute("href", this.docController.makePageUrl(pageNo)); + } + + /** + * Update text of navigation button. + * + * @param {Element} button + * @private + */ + updateText(button) { + const textTemplate = button.getAttribute("data-text"); + + if (textTemplate) { + button.textContent = textTemplate.replace(/PAGE_STEPS/u, this.getLongStep()); + } + } +} diff --git a/Resources/Public/JavaScript/PageView/PageView.js b/Resources/Public/JavaScript/PageView/PageView.js index 7fa1f0518..a4650f1dc 100644 --- a/Resources/Public/JavaScript/PageView/PageView.js +++ b/Resources/Public/JavaScript/PageView/PageView.js @@ -16,29 +16,21 @@ */ /** - * @typedef {{ - * url: string; - * mimetype: string; - * }} ResourceLocator - * - * @typedef {ResourceLocator} ImageDesc - * - * @typedef {ResourceLocator} FulltextDesc - * * @typedef {ResourceLocator} ScoreDesc * * @typedef {ResourceLocator} MeasureDesc * * @typedef {{ - * div: string; - * progressElementId?: string; - * images?: ImageDesc[] | []; - * fulltexts?: FulltextDesc[] | []; - * scores?: ScoreDesc[] | []; - * controls?: ('OverviewMap' | 'ZoomPanel')[]; - * measureCoords?: MeasureDesc[] | []; - * measureIdLinks?: MeasureDesc[] | []; + * div: string; + * progressElementId?: string; + * images?: dlf.ImageDesc[] | []; + * fulltexts?: dlf.FulltextDesc[] | []; + * controls?: ('OverviewMap' | 'ZoomPanel')[]; + * measureCoords?: MeasureDesc[] | []; + * measureIdLinks?: MeasureDesc[] | []; * }} DlfViewerConfig + * + * @typedef {any} DlfDocument */ /** @@ -142,7 +134,7 @@ var dlfViewer = function (settings) { * @type {JQueryStatic.Deferred[]} * @private */ - this.fulltextsLoaded_ = []; + this.fulltextsLoaded = {}; /** * IIIF annotation lists URLs for the current canvas @@ -250,6 +242,24 @@ var dlfViewer = function (settings) { */ this.useInternalProxy = dlfUtils.exists(settings.useInternalProxy) ? settings.useInternalProxy : false; + /** + * Cache of promises / jQuery Deferred returned by `initLayer()`. + * This has two benefits: + * - Switching to a page that has already been visited is basically instantaneous. + * When relying on browser cache, there still is a flicker. + * - It may allow to prefetch pages that are likely to be visited next (TODO(client-side): do that). + * + * @type {Record} + * @private + */ + this.layersCache = {}; + + /** + * @type {dlfController | null} + * @private + */ + this.docController = null; + this.init(dlfUtils.exists(settings.controls) ? settings.controls : []); }; @@ -335,16 +345,42 @@ dlfViewer.prototype.countPages = function () { return this.imageUrls.length; }; +/** + * Update Fulltext in UI + * + * @param {JQueryStatic.Deferred | undefined} currentFulltext + */ +dlfViewer.prototype.updateFulltext = function(currentFulltext) { + if (!this.fulltextControl) { + this.fulltextControl = new dlfViewerFullTextControl(this.map); + } + if (!this.fulltextDownloadControl) { + this.fulltextDownloadControl = new dlfViewerFullTextDownloadControl(this.map); + } + if (currentFulltext !== undefined && this.images.length === 1) { + $("#tx-dlf-tools-fulltext").show(); + + currentFulltext + .then((fulltextData) => { + this.fulltextControl.loadFulltextData(fulltextData); + this.fulltextDownloadControl.setFulltextData(fulltextData); + }) + .catch(() => { + this.fulltextControl.deactivate(); + }); + } else { + $("#tx-dlf-tools-fulltext").hide(); + this.fulltextControl.deactivate(); + } +}; + /** * Methods inits and binds the custom controls to the dlfViewer. Right now that are the * fulltext, score, and the image manipulation control */ dlfViewer.prototype.addCustomControls = function() { - var fulltextControl = undefined, - fulltextDownloadControl = undefined, - annotationControl = undefined, - imageManipulationControl = undefined, - images = this.images; + var annotationControl = undefined; + var imageManipulationControl = undefined; // // Annotation facsimile @@ -464,21 +500,9 @@ dlfViewer.prototype.addCustomControls = function() { // Adds fulltext behavior and download only if there is fulltext available and no double page // behavior is active - if (this.fulltextsLoaded_[0] !== undefined && this.images.length === 1) { - fulltextControl = new dlfViewerFullTextControl(this.map); - fulltextDownloadControl = new dlfViewerFullTextDownloadControl(this.map); - - this.fulltextsLoaded_[0] - .then(function (fulltextData) { - fulltextControl.loadFulltextData(fulltextData); - fulltextDownloadControl.setFulltextData(fulltextData); - }) - .catch(function () { - fulltextControl.deactivate(); - }); - } else { - $('#tx-dlf-tools-fulltext').remove(); - } + const currentFulltext = this.fulltextsLoaded[`${this.getVisiblePages()[0].pageNo}-0`]; + + this.updateFulltext(currentFulltext); if (this.scoresLoaded_ !== undefined) { var context = this; @@ -605,18 +629,15 @@ dlfViewer.prototype.addCustomControls = function() { && this.annotationContainers[0].annotationContainers.length > 0 && this.images.length === 1) { // Adds annotation behavior only if there are annotations available and view is single page annotationControl = new DlfAnnotationControl(this.map, this.images[0], this.annotationContainers[0]); - if (fulltextControl !== undefined) { - $(fulltextControl).on("activate-fulltext", $.proxy(annotationControl.deactivate, annotationControl)); - $(annotationControl).on("activate-annotations", $.proxy(fulltextControl.deactivate, fulltextControl)); + if (this.fulltextControl !== undefined) { + $(this.fulltextControl).on("activate-fulltext", $.proxy(annotationControl.deactivate, annotationControl)); + $(annotationControl).on("activate-annotations", $.proxy(this.fulltextControl.deactivate, this.fulltextControl)); } - } - else { - $('#tx-dlf-tools-annotations').remove(); + } else { + $("#tx-dlf-tools-annotations").remove(); } - // // Add image manipulation tool if container is added. - // if ($('#tx-dlf-tools-imagetools').length > 0) { // Should be called if CORS is enabled @@ -626,8 +647,8 @@ dlfViewer.prototype.addCustomControls = function() { }); // Bind behavior of both together - if (fulltextControl !== undefined) { - $(imageManipulationControl).on("activate-imagemanipulation", $.proxy(fulltextControl.deactivate, fulltextControl)); + if (this.fulltextControl !== undefined) { + $(imageManipulationControl).on("activate-imagemanipulation", $.proxy(this.fulltextControl.deactivate, this.fulltextControl)); $(fulltextControl).on("activate-fulltext", $.proxy(imageManipulationControl.deactivate, imageManipulationControl)); } if (annotationControl !== undefined) { @@ -645,6 +666,8 @@ dlfViewer.prototype.addCustomControls = function() { /** * Add highlight field * + * Used for SRU search highlighting in DFG Viewer. + * * @param {Array.} highlightField * @param {number} imageIndex * @param {number} width @@ -790,7 +813,8 @@ dlfViewer.prototype.displayHighlightWord = function(highlightWords = null) { var self = this; var values = decodeURIComponent(this.highlightWords).split(';'); - $.when.apply($, this.fulltextsLoaded_) + const currentFulltext = this.fulltextsLoaded[this.getVisiblePages()[0].pageNo]; + $.when.apply($, currentFulltext) .done(function (fulltextData, fulltextDataImageTwo) { var stringFeatures = []; @@ -827,7 +851,7 @@ dlfViewer.prototype.init = function(controlNames) { .done($.proxy(function(layers){ // Initiate loading fulltexts - this.initLoadFulltexts(); + this.initLoadFulltexts(this.getVisiblePages()); this.initLoadScores(); var controls = controlNames.length > 0 || controlNames[0] === "" @@ -914,36 +938,110 @@ dlfViewer.prototype.init = function(controlNames) { this.initCropping(); }; +dlfViewer.prototype.getVisiblePages = function () { + if (this.docController === null) { + /* eslint no-console: ["error", { allow: ["warn", "error"] }] */ + console.error("No document controller found"); + return; + } else { + return this.docController.getVisiblePages(); + } +}; + +/** + * + * @param {dlfController | null} docController + */ +dlfViewer.prototype.setDocController = function (docController) { + if (docController === this.docController) { + return; + } + + this.docController = docController; + + if (docController === null) { + return; + } + + this.docController.eventTarget.addEventListener('tx-dlf-stateChanged', () => { + this.loadPages(this.getVisiblePages()); + }); +}; + +/** + * + * @param {any} visiblePages + * @private + * @returns + */ +dlfViewer.prototype.loadPages = function (visiblePages) { + if (this.docController === null) { + return; + } + + const pages = []; + const files = []; + for (const page of visiblePages) { + const file = this.docController.findFileByKind(page.pageNo, 'images'); + if (file === undefined) { + /* eslint no-console: ["error", { allow: ["warn", "error"] }] */ + console.warn(`No image file found on page ${page.pageNo}`); + continue; + } + pages.push(page); + files.push(file); + } + + this.initLayer(files) + .done(layers => { + this.map.setLayers(layers); + this.initLoadFulltexts(pages); + + let i = 0; + for (const page of pages) { + this.updateFulltext(this.fulltextsLoaded[`${page.pageNo}-${i}`]); + i++; + } + }); +}; + dlfViewer.prototype.updateLayerSize = function() { - this.map.updateSize(); + this.map.updateSize(); }; /** * Generate the OpenLayers layer objects for given image sources. Returns a promise / jQuery deferred object. * - * @param {ImageDesc[]} imageSourceObjs + * @param {dlf.ImageDesc[]} imageSourceObjs * @returns {jQuery.Deferred.)>} * @private */ -dlfViewer.prototype.initLayer = function(imageSourceObjs) { - - // use deferred for async behavior - var deferredResponse = new $.Deferred(), - /** - * @param {Array.<{src: *, width: *, height: *}>} imageSourceData - * @param {Array.} layers - */ - resolveCallback = $.proxy(function(imageSourceData, layers) { - this.images = imageSourceData; - deferredResponse.resolve(layers); - }, this); - - dlfUtils.fetchImageData(imageSourceObjs, this.loadingIndicator) - .done(function(imageSourceData) { - resolveCallback(imageSourceData, dlfUtils.createOlLayers(imageSourceData)); - }); - - return deferredResponse; +dlfViewer.prototype.initLayer = function (imageSourceObjs) { + const layersCacheKey = imageSourceObjs.map(image => image.url) + .join('\u001c'); // 0x1c = 28 = ASCII file separator + + let deferredResponse = this.layersCache[layersCacheKey]; + if (deferredResponse === undefined) { + // use deferred for async behavior + deferredResponse = this.layersCache[layersCacheKey] + = new $.Deferred(); + + dlfUtils.fetchImageData(imageSourceObjs, this.loadingIndicator) + .done((imageSourceData) => { + deferredResponse.resolve({ + layers: dlfUtils.createOlLayers(imageSourceData), + images: imageSourceData, + }); + }); + } + + const initDeferred = new $.Deferred(); + deferredResponse.done(({ layers, images }) => { + this.images = images; + initDeferred.resolve(layers); + }); + + return initDeferred; }; /** @@ -958,19 +1056,30 @@ dlfViewer.prototype.initLoadScores = function () { }; /** - * Start loading fulltexts and store them to `fulltextsLoaded_` (as jQuery deferred objects). + * Start loading fulltexts and store them to `fulltextsLoaded` (as jQuery deferred objects). * + * @param {dlf.PageObject[]} visiblePages * @private */ -dlfViewer.prototype.initLoadFulltexts = function () { - var cnt = Math.min(this.fulltexts.length, this.images.length); +dlfViewer.prototype.initLoadFulltexts = function (visiblePages) { + if (this.docController === null) { + return; + } + + var cnt = Math.min(visiblePages.length, this.images.length); var xOffset = 0; for (var i = 0; i < cnt; i++) { - var fulltext = this.fulltexts[i]; - var image = this.images[i]; + const image = this.images[i]; + const key = `${visiblePages[i].pageNo}-${i}`; - if (dlfUtils.isFulltextDescriptor(fulltext)) { - this.fulltextsLoaded_[i] = dlfFullTextUtils.fetchFullTextDataFromServer(fulltext.url, image, xOffset); + const fulltext = this.docController.findFileByKind(visiblePages[i].pageNo, 'fulltext'); + if (fulltext !== undefined) { + if (!(key in this.fulltextsLoaded) && dlfUtils.isFulltextDescriptor(fulltext)) { + this.fulltextsLoaded[key] = dlfFullTextUtils.fetchFullTextDataFromServer(fulltext.url, image, xOffset); + } + } else { + /* eslint no-console: ["error", { allow: ["warn", "error"] }] */ + console.warn("No fulltext file found"); } xOffset += image.width; diff --git a/Resources/Public/JavaScript/PageView/TableOfContents.js b/Resources/Public/JavaScript/PageView/TableOfContents.js new file mode 100644 index 000000000..38eb6a98f --- /dev/null +++ b/Resources/Public/JavaScript/PageView/TableOfContents.js @@ -0,0 +1,113 @@ +/** + * (c) Kitodo. Key to digital objects e.V. + * + * This file is part of the Kitodo and TYPO3 projects. + * + * @license GNU General Public License version 3 or later. + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + */ + +const dlfTocState = { + Normal: 0, + Current: 1, + Active: 2, +}; + +class dlfTableOfContents { + /** + * + * @param {dlfController} docController + */ + constructor(docController) { + /** @private */ + this.docController = docController; + /** @private */ + this.tocItems = document.querySelectorAll("[data-toc-item]"); + /** @private */ + this.tocLinks = document.querySelectorAll("[data-toc-link]"); + + this.tocLinks.forEach((link) => { + const documentId = link.getAttribute("data-document-id"); + + if (documentId && documentId !== this.docController.documentId) { + return; + } + + const pageNo = Number(link.getAttribute("data-page")); + + link.addEventListener("click", (e) => { + e.preventDefault(); + this.docController.changePage(pageNo); + }); + }); + + docController.eventTarget.addEventListener("tx-dlf-stateChanged", this.onStateChanged.bind(this)); + } + + /** + * @param {dlf.StateChangeEvent} e + * @private + */ + onStateChanged(e) { + const activeLogSections = []; + + // TODO(client-side): Add toplevel sections + for (const page of this.docController.getVisiblePages()) { + activeLogSections.push(...page.pageObj.logSections); + } + + // TODO(client-side): TOC from DB + + // See TableOfContentsController::getMenuEntry() + this.tocItems.forEach((tocItem) => { + let tocItemState = dlfTocState.Normal; + let isExpanded = Boolean(tocItem.getAttribute("data-toc-expand-always")); + + const isCurrent = activeLogSections.includes(tocItem.getAttribute("data-dlf-section")); + + if (isCurrent) { + tocItemState = dlfTocState.Current; + } + + const children = Array.from(tocItem.querySelectorAll("[data-toc-item]")); + + if (children.length > 0 && isCurrent) { + // TODO(client-side): check depth? + const isActive = children.some((tocItemChild) => activeLogSections.includes(tocItemChild.getAttribute("data-dlf-section"))); + + if (isActive) { + tocItemState = dlfTocState.Active; + } + + isExpanded = true; + } + + if (tocItemState === dlfTocState.Normal) { + tocItem.classList.add("tx-dlf-toc-no"); + } else { + tocItem.classList.remove("tx-dlf-toc-no"); + } + + if (tocItemState === dlfTocState.Active) { + tocItem.classList.add("active", "tx-dlf-toc-act"); + } else { + tocItem.classList.remove("active", "tx-dlf-toc-act"); + } + + if (tocItemState === dlfTocState.Current) { + tocItem.classList.add("current", "tx-dlf-toc-cur"); + } else { + tocItem.classList.remove("current", "tx-dlf-toc-cur"); + } + + if (isExpanded) { + tocItem.classList.remove("dlf-toc-collapsed"); + } else { + tocItem.classList.add("dlf-toc-collapsed"); + } + + // "submenu" class does not change + }); + } +} diff --git a/Resources/Public/JavaScript/PageView/Toolbox.js b/Resources/Public/JavaScript/PageView/Toolbox.js new file mode 100644 index 000000000..8ad4fcfcf --- /dev/null +++ b/Resources/Public/JavaScript/PageView/Toolbox.js @@ -0,0 +1,104 @@ +/** + * (c) Kitodo. Key to digital objects e.V. + * + * This file is part of the Kitodo and TYPO3 projects. + * + * @license GNU General Public License version 3 or later. + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + */ + +class dlfToolbox { + /** + * + * @param {dlfController} docController + */ + constructor(docController) { + /** @private */ + this.docController = docController; + /** @private */ + this.pageLinks = document.querySelectorAll("[data-page-link]"); + + docController.eventTarget.addEventListener("tx-dlf-stateChanged", this.onStateChanged.bind(this)); + this.updatePageLinks(this.docController.currentPageNo); + } + + /** + * @param {dlf.StateChangeEvent} e + * @private + */ + onStateChanged(e) { + if (e.detail.page !== undefined) { + this.updatePageLinks(e.detail.page); + } + } + + /** + * @param {number} firstPageNo + * @private + */ + updatePageLinks(firstPageNo) { + this.pageLinks.forEach((element) => { + const offset = Number(element.getAttribute("data-page-link")); + const pageNo = firstPageNo + offset; + + const fileGroups = this.getFileGroups(element); + const file = fileGroups !== null ? this.docController.findFileByGroup(pageNo, fileGroups) : this.docController.findFileByKind(pageNo, "download"); + + if (file === undefined) { + $(element).hide(); + + return; + } + $(element).show(); + + if (element instanceof HTMLAnchorElement) { + element.href = file.url; + } else { + element.querySelectorAll("a").forEach((linkEl) => { + linkEl.href = file.url; + }); + } + + const mimetypeLabelEl = element.querySelector(".dlf-mimetype-label"); + + if (mimetypeLabelEl !== null) { + // Transliterated from ToolboxController + let mimetypeLabel = ""; + + switch (file.mimetype) { + case "image/jpeg": + mimetypeLabel = " (JPG)"; + break; + + case "image/tiff": + mimetypeLabel = " (TIFF)"; + break; + } + + mimetypeLabelEl.textContent = mimetypeLabel; + } + }); + } + + /** + * @param {Element} element + * @returns {string[] | null} + * @private + */ + getFileGroups(element) { + const fileGroupsJson = element.getAttribute("data-file-groups"); + + try { + const fileGroups = JSON.parse(fileGroupsJson); + + if (Array.isArray(fileGroups) && fileGroups.every((entry) => typeof entry === "string")) { + return fileGroups; + } + } catch (e) { + // + } + + return null; + } +} diff --git a/Resources/Public/JavaScript/PageView/Utility.js b/Resources/Public/JavaScript/PageView/Utility.js index 15cb4bcc9..3bce5b978 100644 --- a/Resources/Public/JavaScript/PageView/Utility.js +++ b/Resources/Public/JavaScript/PageView/Utility.js @@ -218,10 +218,26 @@ dlfUtils.exists = function (val) { return val !== undefined; }; +/** + * Get the value in {@link map} of the first key in {@link keys} that is set. + * + * @template T + * @param {string[]} keys + * @param {Record} map + * @returns {T} + */ +dlfUtils.findFirstSet = function (map, keys) { + for (const fileGrp of keys) { + if (map[fileGrp]) { + return map[fileGrp]; + } + } +}; + /** * Fetch image data for given image sources. * - * @param {ImageDesc[]} imageSourceObjs + * @param {dlf.ImageDesc[]} imageSourceObjs * @param {LoadingIndicator} loadingIndicator * @returns {JQueryStatic.Deferred} */ @@ -280,7 +296,7 @@ dlfUtils.fetchImageData = function (imageSourceObjs, loadingIndicator) { /** * Fetches the image data for static images source. * - * @param {ImageDesc} imageSourceObj + * @param {dlf.ImageDesc} imageSourceObj * @param {LoadingIndicator} loadingIndicator * @returns {JQueryStatic.Deferred} */ @@ -425,7 +441,7 @@ dlfUtils.getIIIFResource = function getIIIFResource(imageSourceObj) { /** * Fetches the image data for iip images source. * - * @param {ImageDesc} imageSourceObj + * @param {dlf.ImageDesc} imageSourceObj * @returns {JQueryStatic.Deferred} */ dlfUtils.fetchIIPData = function (imageSourceObj) { @@ -455,7 +471,7 @@ dlfUtils.fetchIIPData = function (imageSourceObj) { /** * Fetch image data for zoomify source. * - * @param {ImageDesc} imageSourceObj + * @param {dlf.ImageDesc} imageSourceObj * @returns {JQueryStatic.Deferred} */ dlfUtils.fetchZoomifyData = function (imageSourceObj) { @@ -539,7 +555,7 @@ dlfUtils.isNullEmptyUndefinedOrNoNumber = function (val) { * @see PageView::getFulltext in PageView.php * * @param {any} obj The object to test. - * @returns {obj is FulltextDesc} + * @returns {obj is dlf.FulltextDesc} */ dlfUtils.isFulltextDescriptor = function (obj) { return ( diff --git a/Resources/Public/JavaScript/PageView/types.d.ts b/Resources/Public/JavaScript/PageView/types.d.ts new file mode 100644 index 000000000..beb010f09 --- /dev/null +++ b/Resources/Public/JavaScript/PageView/types.d.ts @@ -0,0 +1,60 @@ +namespace dlf { + type ResourceLocator = { + url: string; + mimetype: string; + }; + + type ImageDesc = ResourceLocator; + type FulltextDesc = ResourceLocator; + + type PageObject = { + /** + * IDs of the logical structures that the page belongs to, ordered by depth. + */ + logSections: string[]; + files: Record; + }; + + type Document = { + pages: PageObjects[]; + query: { + minPage: number; + }; + }; + + type PageDisplayState = { + documentId: string | number; + page: number; + simultaneousPages: number; + }; + + type FileKind = "images" | "fulltext" | "download"; + + type Loaded = { + state: PageDisplayState; + urlTemplate: string; + metadataUrl: string | null; + fileGroups: Record; + document: Document; + }; + + type StateChangeDetail = { + /** + * Who triggered the event. + * * `history`: Event is triggered due to history popstate. This is used + * to avoid pushing a popped state again. + * * `navigation`: Event is triggered by user navigation. + */ + source: "history" | "navigation"; + } & Partial; + + type StateChangeEvent = CustomEvent; + + /** + * State of document stored in `window.history`. + */ + type PageHistoryState = { + type: "tx-dlf-page-state"; + documentId: string | number; + } & PageDisplayState; +} diff --git a/Tests/Functional/Common/MetsDocumentTest.php b/Tests/Functional/Common/MetsDocumentTest.php index 07df0098b..7dc060909 100644 --- a/Tests/Functional/Common/MetsDocumentTest.php +++ b/Tests/Functional/Common/MetsDocumentTest.php @@ -29,7 +29,7 @@ public function setUp(): void protected function doc(string $file) { $url = 'http://web:8001/Tests/Fixtures/MetsDocument/' . $file; - $doc = AbstractDocument::getInstance($url, ['general' => ['useExternalApisForMetadata' => 0]]); + $doc = AbstractDocument::getInstance($url, 0, ['general' => ['useExternalApisForMetadata' => 0]]); self::assertNotNull($doc); return $doc; } diff --git a/Tests/Functional/Common/SolrIndexingTest.php b/Tests/Functional/Common/SolrIndexingTest.php index 221fe6347..0b55bd849 100644 --- a/Tests/Functional/Common/SolrIndexingTest.php +++ b/Tests/Functional/Common/SolrIndexingTest.php @@ -77,7 +77,7 @@ public function canIndexAndSearchDocument() $document->setSolrcore($core->model->getUid()); $this->persistenceManager->persistAll(); - $doc = AbstractDocument::getInstance($document->getLocation(), ['useExternalApisForMetadata' => 0]); + $doc = AbstractDocument::getInstance($document->getLocation(), 0, ['useExternalApisForMetadata' => 0]); $document->setCurrentDocument($doc); $indexingSuccessful = Indexer::add($document, $this->documentRepository); diff --git a/ext_conf_template.txt b/ext_conf_template.txt index 4813f62e5..a59996da8 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -14,6 +14,8 @@ general.caching = 0 general.publishNewCollections = 1 # cat=General; type=boolean; label=LLL:EXT:dlf/Resources/Private/Language/locallang_labels.xlf:config.general.unhideOnIndex general.unhideOnIndex = 0 +# cat=General; type=string; label=LLL:EXT:dlf/Resources/Private/Language/locallang_labels.xml:config.general.nonProxyMimeType +general.nonProxyMimeType = application/vnd.kitodo.iiif,application/vnd.netfpx,application/vnd.kitodo.zoomify # cat=General; type=boolean; label=LLL:EXT:dlf/Resources/Private/Language/locallang_labels.xlf:config.general.useExternalApisForMetadata general.useExternalApisForMetadata = 0 # cat=General; type=string; label=LLL:EXT:dlf/Resources/Private/Language/locallang_labels.xlf:config.general.requiredMetadataFields diff --git a/ext_localconf.php b/ext_localconf.php index 44f1fca79..8485731e5 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -36,6 +36,7 @@ 'tx-dlf-basket' => 'EXT:dlf/Resources/Public/Icons/tx-dlf-basket.svg', 'tx-dlf-calendar' => 'EXT:dlf/Resources/Public/Icons/tx-dlf-calendar.svg', 'tx-dlf-collection' => 'EXT:dlf/Resources/Public/Icons/tx-dlf-collection.svg', + 'tx-dlf-document' => 'EXT:dlf/Resources/Public/Icons/tx-dlf-document.svg', 'tx-dlf-feeds' => 'EXT:dlf/Resources/Public/Icons/tx-dlf-feeds.svg', 'tx-dlf-listview' => 'EXT:dlf/Resources/Public/Icons/tx-dlf-listview.svg', 'tx-dlf-metadata' => 'EXT:dlf/Resources/Public/Icons/tx-dlf-metadata.svg', @@ -157,6 +158,18 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][\Kitodo\Dlf\Updates\FileLocationUpdater::class] = \Kitodo\Dlf\Updates\FileLocationUpdater::class; +\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( + 'Dlf', + 'Document', + [ + \Kitodo\Dlf\Controller\DocumentController::class => 'main' + ], + // non-cacheable actions + [ + \Kitodo\Dlf\Controller\DocumentController::class => '' + ] +); + \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( 'Dlf', 'Search',