commit f9941541b796343595b90968bfdcde530597de92 Author: Sven Wappler Date: Mon Aug 10 22:31:00 2026 +0200 TYPO3 v15 dev-main snapshot () diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/Classes/Attribute/AsAvatarProvider.php b/Classes/Attribute/AsAvatarProvider.php new file mode 100644 index 0000000..b9dd40a --- /dev/null +++ b/Classes/Attribute/AsAvatarProvider.php @@ -0,0 +1,38 @@ + $before + * @param list $after + */ + public function __construct( + public string $identifier, + public array $before = [], + public array $after = [], + ) {} +} diff --git a/Classes/Attribute/AsController.php b/Classes/Attribute/AsController.php new file mode 100644 index 0000000..d7cfe99 --- /dev/null +++ b/Classes/Attribute/AsController.php @@ -0,0 +1,29 @@ + $before List of component identifiers, which should appear before + * @param list $after List of component identifiers, which should appear after + */ + public function __construct( + public string $identifier, + public array $before = [], + public array $after = [], + ) {} +} diff --git a/Classes/Authentication/BackendLocker.php b/Classes/Authentication/BackendLocker.php new file mode 100644 index 0000000..3de0290 --- /dev/null +++ b/Classes/Authentication/BackendLocker.php @@ -0,0 +1,81 @@ +getAbsolutePathToLockFile()); + } + + public function lockBackend(string $redirectUriFromFileContent): bool + { + return GeneralUtility::writeFile($this->getAbsolutePathToLockFile(), $redirectUriFromFileContent, true); + } + + public function unlock(): void + { + unlink($this->getAbsolutePathToLockFile()); + } + + public function getAbsolutePathToLockFile(): string + { + // This setting is empty by default to utilize the fallback storage location. + // If set specifically, this is the preference. + if (($GLOBALS['TYPO3_CONF_VARS']['BE']['lockBackendFile'] ?? '') !== '') { + return Environment::getProjectPath() . '/' . $GLOBALS['TYPO3_CONF_VARS']['BE']['lockBackendFile']; + } + + return $this->getLockPath() . '/LOCK_BACKEND'; + } + + public function getRedirectUriFromLockContents(): string + { + return file_get_contents($this->getAbsolutePathToLockFile()); + } + + /** + * Based on composer or legacy mode, return a fallback directory + * location where LOCK_BACKEND can be stored. + * Composer-mode: "var/lock" is preferred because it is recommended to + * be shared and persist across deployments and the location + * is writable by the webserver) + * Legacy: "config/", because usually writable by webserver and persistent. + */ + protected function getLockPath(): string + { + return Environment::isComposerMode() + ? Environment::getVarPath() . '/lock' + : Environment::getConfigPath(); + } +} diff --git a/Classes/Authentication/Event/PasswordHasBeenResetEvent.php b/Classes/Authentication/Event/PasswordHasBeenResetEvent.php new file mode 100644 index 0000000..fa44d37 --- /dev/null +++ b/Classes/Authentication/Event/PasswordHasBeenResetEvent.php @@ -0,0 +1,25 @@ +sessionId; + } + + public function getTargetUser(): array + { + return $this->targetUser; + } + + public function getCurrentUser(): array + { + return $this->currentUser; + } +} diff --git a/Classes/Authentication/PasswordReset.php b/Classes/Authentication/PasswordReset.php new file mode 100644 index 0000000..516bb8b --- /dev/null +++ b/Classes/Authentication/PasswordReset.php @@ -0,0 +1,525 @@ +getPreparedQueryBuilder(); + $statement = $queryBuilder + ->select('uid') + ->from('be_users') + ->setMaxResults(1) + ->executeQuery(); + return (int)$statement->fetchOne() > 0; + } + + /** + * Check if a specific backend user can be used to trigger an email reset for (email + password set) + */ + public function isEnabledForUser(int $userId): bool + { + $queryBuilder = $this->getPreparedQueryBuilder(); + $statement = $queryBuilder + ->select('uid') + ->from('be_users') + ->andWhere( + $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT)) + ) + ->setMaxResults(1) + ->executeQuery(); + return $statement->fetchOne() > 0; + } + + /** + * Determine the right user and send out an email. If multiple users are found with the same email address + * an alternative email is sent. + * + * If no user is found, this is logged to the system (but not to sys_log). + * + * The method intentionally does not return anything to avoid any information disclosure or exposure. + * + * @param ServerRequestInterface $request + * @param Context $context + * @param string $emailAddress + */ + public function initiateReset(ServerRequestInterface $request, Context $context, string $emailAddress): void + { + if (!GeneralUtility::validEmail($emailAddress)) { + return; + } + if ($this->hasExceededMaximumAttemptsForReset($emailAddress)) { + $this->logger->alert('Password reset requested for email {email} but was requested too many times.', ['email' => $emailAddress]); + return; + } + $queryBuilder = $this->getPreparedQueryBuilder(); + $users = $queryBuilder + ->select('*') + ->from('be_users') + ->andWhere( + $queryBuilder->expr()->eq('email', $queryBuilder->createNamedParameter($emailAddress)) + ) + ->executeQuery() + ->fetchAllAssociative(); + if ($users === []) { + // No user found, do nothing, also no log to sys_log in order avoid log flooding + $this->logger->warning('Password reset requested for email {email} but no valid users', ['email' => $emailAddress]); + } elseif (count($users) > 1) { + // More than one user with the same email address found, send out the email that one cannot send out a reset link + $this->sendAmbiguousEmail($request, $context, $emailAddress); + } else { + $user = reset($users); + unset($user['password']); + $this->sendResetEmail($request, $context, $user); + } + } + + /** + * Send out an email to a given email address and note that a reset was triggered but email was used multiple times. + * Used when the database returned multiple users. + */ + protected function sendAmbiguousEmail(ServerRequestInterface $request, Context $context, string $emailAddress): void + { + $emailObject = $this->templatedEmailFactory->create($request) + ->to(new Address($emailAddress)) + ->assign('email', $emailAddress) + ->setTemplate('PasswordReset/AmbiguousResetRequested'); + $this->mailer->send($emailObject); + $this->logger->warning('Password reset sent to email address {email} but multiple accounts found', ['email' => $emailAddress]); + $this->log( + 'Sent password reset email to email address %s but with multiple accounts attached.', + SystemLogLoginAction::PASSWORD_RESET_REQUEST, + SystemLogErrorClassification::WARNING, + 0, + [ + 'email' => $emailAddress, + ], + NormalizedParams::createFromRequest($request)->getRemoteAddress(), + $context + ); + } + + /** + * Send out an email to a user that does have an email address added to his account, containing a reset link. + */ + protected function sendResetEmail(ServerRequestInterface $request, Context $context, array $user): void + { + $resetLink = $this->generateResetLinkForUser($context, (int)$user['uid'], (string)$user['email']); + $emailObject = $this->templatedEmailFactory->create($request) + ->to(new Address((string)$user['email'], $user['realName'])) + ->assign('name', $user['realName']) + ->assign('email', $user['email']) + ->assign('language', $user['lang'] ?: 'en') + ->assign('resetLink', $resetLink) + ->assign('username', $user['username']) + ->assign('userData', $user) + ->setTemplate('PasswordReset/ResetRequested'); + + $this->mailer->send($emailObject); + + $this->logger->info('Sent password reset email to email address {email} for user {username}', [ + 'email' => $user['email'], + 'username' => $user['username'], + ]); + $this->log( + 'Sent password reset email to email address %s', + SystemLogLoginAction::PASSWORD_RESET_REQUEST, + SystemLogErrorClassification::SECURITY_NOTICE, + (int)$user['uid'], + [ + 'email' => $user['email'], + ], + NormalizedParams::createFromRequest($request)->getRemoteAddress(), + $context + ); + } + + /** + * Creates a token, stores it in the database, and then creates an absolute URL for resetting the password. + * This is all in one method so it is not exposed from the outside. + * + * This function requires: + * a) the user is allowed to do a password reset (no check is done anymore) + * b) a valid email address. + * + * @param Context $context + * @param int $userId the backend user uid + * @param string $emailAddress is part of the hash to ensure that the email address does not get reset. + */ + protected function generateResetLinkForUser(Context $context, int $userId, string $emailAddress): UriInterface + { + $token = $this->random->generateRandomHexString(96); + $currentTime = $context->getAspect('date')->getDateTime(); + $expiresOn = $currentTime->modify(self::TOKEN_VALID_UNTIL); + // Create a hash ("one time password") out of the token including the timestamp of the expiration date + $hash = $this->hashService->hmac($token . '|' . $expiresOn->getTimestamp() . '|' . $emailAddress . '|' . $userId, 'password-reset', HashAlgo::SHA3_256); + + // Set the token in the database, which is hashed + $this->connectionPool + ->getConnectionForTable('be_users') + ->update( + 'be_users', + ['password_reset_token' => $this->passwordHashFactory->getDefaultHashInstance('BE')->getHashedPassword($hash)], + ['uid' => $userId] + ); + + return $this->uriBuilder->buildUriFromRoute( + 'password_reset_validate', + [ + // "token" + 't' => $token, + // "expiration date" + 'e' => $expiresOn->getTimestamp(), + // "identity" + 'i' => hash('sha1', $emailAddress . (string)$userId), + ], + UriBuilder::ABSOLUTE_URL + ); + } + + /** + * Validates all query parameters / GET parameters of the given request against the token. + */ + public function isValidResetTokenFromRequest(ServerRequestInterface $request): bool + { + $user = $this->findValidUserForToken( + (string)($request->getQueryParams()['t'] ?? ''), + (string)($request->getQueryParams()['i'] ?? ''), + (int)($request->getQueryParams()['e'] ?? 0) + ); + return $user !== null; + } + + /** + * Fetch the user record from the database if the token is valid, and has matched all criteria + * + * @return array|null the BE User database record + */ + protected function findValidUserForToken(string $token, string $identity, int $expirationTimestamp): ?array + { + // Early return if token expired + if ($expirationTimestamp < time()) { + return null; + } + + $user = null; + // Find the token in the database + $queryBuilder = $this->getPreparedQueryBuilder(); + + $queryBuilder + ->select('uid', 'username', 'realName', 'email', 'password_reset_token', 'password') + ->from('be_users'); + + $platform = $queryBuilder->getConnection()->getDatabasePlatform(); + if ($platform instanceof DoctrineMariaDBPlatform || $platform instanceof DoctrineMySQLPlatform) { + $queryBuilder->andWhere( + $queryBuilder->expr()->comparison('SHA1(CONCAT(' . $queryBuilder->quoteIdentifier('email') . ', ' . $queryBuilder->quoteIdentifier('uid') . '))', $queryBuilder->expr()::EQ, $queryBuilder->createNamedParameter($identity)) + ); + $user = $queryBuilder->executeQuery()->fetchAssociative(); + } else { + // no native SHA1/ CONCAT functionality, has to be done in PHP + $stmt = $queryBuilder->executeQuery(); + while ($row = $stmt->fetchAssociative()) { + if (hash_equals(hash('sha1', $row['email'] . (string)$row['uid']), $identity)) { + $user = $row; + break; + } + } + } + + if (!is_array($user) || empty($user)) { + return null; + } + + // Validate hash by rebuilding the hash from the parameters and the URL and see if this matches against the stored password_reset_token + $hash = $this->hashService->hmac($token . '|' . $expirationTimestamp . '|' . $user['email'] . '|' . $user['uid'], 'password-reset', HashAlgo::SHA3_256); + if (!$this->passwordHashFactory->getDefaultHashInstance('BE')->checkPassword($hash, $user['password_reset_token'] ?? '')) { + return null; + } + return $user; + } + + /** + * Update the password in the database if the password matches and the token is valid. + * + * @return bool whether the password was reset or not + */ + public function resetPassword(ServerRequestInterface $request, Context $context): bool + { + $expirationTimestamp = (int)($request->getQueryParams()['e'] ?? ''); + $identityHash = (string)($request->getQueryParams()['i'] ?? ''); + $token = (string)($request->getQueryParams()['t'] ?? ''); + $newPassword = (string)($request->getParsedBody()['password'] ?? ''); + $newPasswordRepeat = (string)($request->getParsedBody()['passwordrepeat'] ?? ''); + + $user = $this->findValidUserForToken($token, $identityHash, $expirationTimestamp); + if ($user === null) { + $this->logger->warning('Password reset not possible. Valid user for token not found.'); + return false; + } + $userId = (int)$user['uid']; + + if ($newPassword === '') { + $this->logger->debug('Password reset not possible because an empty password was provided.'); + return false; + } + + if ($newPassword !== $newPasswordRepeat) { + $this->logger->debug('Password reset not possible because new password and new password repeat do not match.'); + return false; + } + + if (!$this->isValidPassword($newPassword, $user)) { + $this->logger->debug('The new password does not match all requirements of the password policy.'); + return false; + } + + $this->connectionPool + ->getConnectionForTable('be_users') + ->update( + 'be_users', + [ + 'password_reset_token' => '', + 'password' => $this->passwordHashFactory->getDefaultHashInstance('BE')->getHashedPassword($newPassword), + ], + ['uid' => $userId] + ); + + $this->eventDispatcher->dispatch(new PasswordHasBeenResetEvent($userId)); + + $this->invalidateUserSessions($userId); + + $this->logger->info('Password reset successful for user \'{username}\'', ['username' => $user['username'], 'user_id' => $userId]); + $this->log( + 'Password reset successful for user %s', + SystemLogLoginAction::PASSWORD_RESET_ACCOMPLISHED, + SystemLogErrorClassification::SECURITY_NOTICE, + $userId, + [ + 'email' => $user['email'], + 'user' => $userId, + ], + NormalizedParams::createFromRequest($request)->getRemoteAddress(), + $context + ); + return true; + } + + /** + * The querybuilder for finding the right user - and adds some restrictions: + * - No CLI users + * - No Admin users (with option) + * - No hidden/deleted users + * - Password must be set + * - Username must be set + * - Email address must be set + */ + protected function getPreparedQueryBuilder(): QueryBuilder + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_users'); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(RootLevelRestriction::class)) + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(StartTimeRestriction::class)) + ->add(GeneralUtility::makeInstance(EndTimeRestriction::class)) + ->add(GeneralUtility::makeInstance(HiddenRestriction::class)); + $queryBuilder->where( + $queryBuilder->expr()->neq('username', $queryBuilder->createNamedParameter('')), + $queryBuilder->expr()->neq('username', $queryBuilder->createNamedParameter('_cli_')), + $queryBuilder->expr()->neq('password', $queryBuilder->createNamedParameter('')), + $queryBuilder->expr()->neq('email', $queryBuilder->createNamedParameter('')) + ); + if (!($GLOBALS['TYPO3_CONF_VARS']['BE']['passwordResetForAdmins'] ?? false)) { + $queryBuilder->andWhere( + $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)) + ); + } + return $queryBuilder; + } + + /** + * Adds an entry to "sys_log", also used to track the maximum allowed attempts. + * + * @param string $message the information / message in english + * @param int $action see SystemLogLoginAction + * @param int $error see SystemLogErrorClassification + * @param array $data additional information, used for the message + * @param string $ipAddress + */ + protected function log(string $message, int $action, int $error, int $userId, array $data, $ipAddress, Context $context): void + { + $this->connectionPool + ->getConnectionForTable('sys_log') + ->insert( + 'sys_log', + [ + 'userid' => $userId, + 'type' => SystemLogType::LOGIN, + 'channel' => SystemLogType::toChannel(SystemLogType::LOGIN), + 'level' => SystemLogType::toLevel(SystemLogType::LOGIN), + 'action' => $action, + 'error' => $error, + 'details' => $message, + 'log_data' => json_encode($data), + 'tablename' => 'be_users', + 'recuid' => $userId, + 'IP' => (string)$ipAddress, + 'tstamp' => $context->getAspect('date')->get('timestamp'), + 'event_pid' => 0, + 'workspace' => 0, + ], + [ + Connection::PARAM_INT, + Connection::PARAM_INT, + Connection::PARAM_STR, + Connection::PARAM_STR, + Connection::PARAM_INT, + Connection::PARAM_INT, + Connection::PARAM_STR, + Connection::PARAM_STR, + Connection::PARAM_STR, + Connection::PARAM_INT, + Connection::PARAM_STR, + Connection::PARAM_INT, + Connection::PARAM_INT, + Connection::PARAM_INT, + ] + ); + } + + /** + * Checks if an email reset link has been requested more than the configured amount of times. + * Default values are 3 times in the last 30 minutes configured in Services.yaml + */ + protected function hasExceededMaximumAttemptsForReset(string $email): bool + { + $limiter = $this->rateLimiterFactory->create($email); + $limit = $limiter->consume(); + return !$limit->isAccepted(); + } + + /** + * Returns, if the given password is compliant with the global password policy for backend users + */ + protected function isValidPassword(string $password, array $user): bool + { + $passwordPolicy = $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] ?? 'default'; + $passwordPolicyValidator = GeneralUtility::makeInstance( + PasswordPolicyValidator::class, + PasswordPolicyAction::UPDATE_USER_PASSWORD, + is_string($passwordPolicy) ? $passwordPolicy : '' + ); + $contextData = new ContextData(currentPasswordHash: $user['password']); + $contextData->setData('currentUsername', $user['username']); + $contextData->setData('currentFullname', $user['realName']); + $event = $this->eventDispatcher->dispatch( + new EnrichPasswordValidationContextDataEvent( + $contextData, + $user, + self::class + ) + ); + $contextData = $event->getContextData(); + + return $passwordPolicyValidator->isValidPassword($password, $contextData); + } + + /** + * Invalidate all backend user sessions by given user id + */ + protected function invalidateUserSessions(int $userId): void + { + $this->sessionManager->invalidateAllSessionsByUserId( + $this->sessionManager->getSessionBackend('BE'), + $userId + ); + } +} diff --git a/Classes/Backend/Avatar/Avatar.php b/Classes/Backend/Avatar/Avatar.php new file mode 100644 index 0000000..8d3983e --- /dev/null +++ b/Classes/Backend/Avatar/Avatar.php @@ -0,0 +1,135 @@ + $avatarProviders + */ + public function __construct( + #[Autowire(service: 'cache.runtime')] + protected FrontendInterface $cache, + protected DependencyOrderingService $dependencyOrderingService, + protected IconFactory $iconFactory, + protected array $avatarProviders = [], + ) { + $this->validateAvatarProviders(); + } + + /** + * Renders an avatar based on a Fluid template which contains some base wrapper css classes. + * Has a simple caching functionality. Used in Avatar ViewHelper for instance. + * Renders avatar of a given backend user record, or of current logged-in backend user. + */ + public function render(?array $backendUser = null, int $size = 32, bool $showIcon = false): string + { + if (!is_array($backendUser)) { + /** @var array $backendUser */ + $backendUser = $this->getBackendUser()->user; + } + $cacheId = 'avatar_' . sha1($backendUser['uid'] . $size . $showIcon); + $avatar = $this->cache->get($cacheId); + if (!$avatar) { + $icon = $showIcon ? $this->iconFactory->getIconForRecord('be_users', $backendUser, IconSize::SMALL)->render() : ''; + $avatar + = '' + . '' . $this->getImgTag($backendUser, $size) . '' + . ($showIcon ? '' . $icon . '' : '') + . ''; + $this->cache->set($cacheId, $avatar); + } + return $avatar; + } + + /** + * Returns an HTML tag of given backend users avatar. + */ + protected function getImgTag(array $backendUser, int $size = 32): string + { + $avatarImage = $this->getImage($backendUser, $size); + return ''; + } + + /** + * Get Image from first provider that returns one. + */ + protected function getImage(array $backendUser, int $size): Image + { + foreach ($this->avatarProviders as $provider) { + $avatarImage = $provider->getImage($backendUser, $size); + if (!empty($avatarImage)) { + return $avatarImage; + } + } + return GeneralUtility::makeInstance( + Image::class, + (string)PathUtility::getSystemResourceUri('EXT:core/Resources/Public/Icons/T3Icons/svgs/avatar/avatar-default.svg'), + $size, + $size + ); + } + + /** + * Validates the registered avatar providers + * + * @throws \RuntimeException + */ + protected function validateAvatarProviders(): void + { + foreach ($this->avatarProviders as $provider) { + if (!($provider instanceof AvatarProviderInterface)) { + throw new \RuntimeException( + sprintf( + 'Avatar provider must implement interface "%s", "%s" given.', + AvatarProviderInterface::class, + get_debug_type($provider), + ), + 1439317802, + ); + } + } + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Backend/Avatar/AvatarProviderInterface.php b/Classes/Backend/Avatar/AvatarProviderInterface.php new file mode 100644 index 0000000..ef6f252 --- /dev/null +++ b/Classes/Backend/Avatar/AvatarProviderInterface.php @@ -0,0 +1,31 @@ +getAvatarFileUid($backendUser['uid']); + if ($fileUid === 0) { + // Early return if there is no valid image file UID + return null; + } + // Get file object + try { + $file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject($fileUid); + $processedImage = $file->process( + ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, + ['width' => $size . 'c', 'height' => $size . 'c'] + ); + + $publicUrl = $processedImage->getPublicUrl(); + if ($publicUrl) { + $image = GeneralUtility::makeInstance( + Image::class, + $publicUrl, + $processedImage->getProperty('width'), + $processedImage->getProperty('height') + ); + } else { + $image = null; + } + } catch (FileDoesNotExistException $e) { + // No image found + $image = null; + } + + return $image; + } + + /** + * Get the sys_file UID of the avatar of the given backend user ID + * + * @param int $backendUserId the UID of the be_users record + * @return int the sys_file UID or 0 if none found + */ + protected function getAvatarFileUid($backendUserId) + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_reference'); + $fileUid = $queryBuilder + ->select('uid_local') + ->from('sys_file_reference') + ->where( + $queryBuilder->expr()->eq( + 'tablenames', + $queryBuilder->createNamedParameter('be_users') + ), + $queryBuilder->expr()->eq( + 'fieldname', + $queryBuilder->createNamedParameter('avatar') + ), + $queryBuilder->expr()->eq( + 'uid_foreign', + $queryBuilder->createNamedParameter((int)$backendUserId, Connection::PARAM_INT) + ) + ) + ->executeQuery() + ->fetchOne(); + + return (int)$fileUid; + } +} diff --git a/Classes/Backend/Avatar/Image.php b/Classes/Backend/Avatar/Image.php new file mode 100644 index 0000000..482d29e --- /dev/null +++ b/Classes/Backend/Avatar/Image.php @@ -0,0 +1,78 @@ +url = $url; + $this->width = (int)$width; + $this->height = (int)$height; + } + + /** + * Fetches the URL to the avatar image + * + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * @return int + */ + public function getWidth() + { + return $this->width; + } + + /** + * @return int + */ + public function getHeight() + { + return $this->height; + } +} diff --git a/Classes/Backend/Bookmark/Bookmark.php b/Classes/Backend/Bookmark/Bookmark.php new file mode 100644 index 0000000..ee3a78a --- /dev/null +++ b/Classes/Backend/Bookmark/Bookmark.php @@ -0,0 +1,65 @@ + $this->id, + 'route' => $this->route, + 'arguments' => $this->arguments, + 'title' => $this->title, + 'groupId' => $this->groupId, + 'iconIdentifier' => $this->iconIdentifier, + 'iconOverlayIdentifier' => $this->iconOverlayIdentifier, + 'module' => $this->module, + 'href' => $this->href, + 'editable' => $this->editable, + 'accessible' => $this->accessible, + ]; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/Classes/Backend/Bookmark/BookmarkGroup.php b/Classes/Backend/Bookmark/BookmarkGroup.php new file mode 100644 index 0000000..9e06ec0 --- /dev/null +++ b/Classes/Backend/Bookmark/BookmarkGroup.php @@ -0,0 +1,55 @@ + $this->id, + 'label' => $this->label, + 'type' => $this->type->value, + 'priority' => $this->type->getPriority(), + 'sorting' => $this->sorting, + 'editable' => $this->editable, + 'selectable' => $this->selectable, + ]; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/Classes/Backend/Bookmark/BookmarkGroupType.php b/Classes/Backend/Bookmark/BookmarkGroupType.php new file mode 100644 index 0000000..92a15ae --- /dev/null +++ b/Classes/Backend/Bookmark/BookmarkGroupType.php @@ -0,0 +1,57 @@ + 0, + self::SYSTEM => 1, + self::GLOBAL => 2, + }; + } +} diff --git a/Classes/Backend/Bookmark/BookmarkRepository.php b/Classes/Backend/Bookmark/BookmarkRepository.php new file mode 100644 index 0000000..56ac412 --- /dev/null +++ b/Classes/Backend/Bookmark/BookmarkRepository.php @@ -0,0 +1,432 @@ +connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + $row = $queryBuilder->select('*') + ->from(self::TABLE_NAME) + ->where( + $queryBuilder->expr()->eq( + 'uid', + $queryBuilder->createNamedParameter($id, Connection::PARAM_INT) + ) + ) + ->executeQuery() + ->fetchAssociative(); + + return $row !== false ? $row : null; + } + + /** + * @param list $ids + * @return array Indexed by bookmark ID + */ + public function findByIds(array $ids): array + { + if ($ids === []) { + return []; + } + + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + $result = $queryBuilder->select('*') + ->from(self::TABLE_NAME) + ->where( + $queryBuilder->expr()->in( + 'uid', + $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY) + ) + ) + ->executeQuery(); + + $bookmarks = []; + while ($row = $result->fetchAssociative()) { + $bookmarks[(int)$row['uid']] = $row; + } + + return $bookmarks; + } + + public function findByUser(int $userId): array + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + + $constraints = []; + + // User's own bookmarks with a non-negative sc_group value. + // Bookmarks in user-created groups also match here, as their sc_group defaults to 0. + $constraints[] = $queryBuilder->expr()->and( + $queryBuilder->expr()->eq( + 'userid', + $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT) + ), + $queryBuilder->expr()->gte( + 'sc_group', + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ) + ); + + // Global bookmarks (negative sc_group) - visible to all users + $constraints[] = $queryBuilder->expr()->lt( + 'sc_group', + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ); + + $result = $queryBuilder->select('*') + ->from(self::TABLE_NAME) + ->where($queryBuilder->expr()->or(...$constraints)) + ->orderBy('sc_group') + ->addOrderBy('sorting') + ->executeQuery(); + + $bookmarks = []; + while ($row = $result->fetchAssociative()) { + $bookmarks[] = $row; + } + + return $bookmarks; + } + + public function exists(int $userId, string $routeIdentifier, string $arguments): bool + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + $queryBuilder->getRestrictions()->removeAll(); + + $uid = $queryBuilder->select('uid') + ->from(self::TABLE_NAME) + ->where( + $queryBuilder->expr()->eq( + 'userid', + $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq('route', $queryBuilder->createNamedParameter($routeIdentifier)), + $queryBuilder->expr()->eq('arguments', $queryBuilder->createNamedParameter($arguments)) + ) + ->executeQuery() + ->fetchOne(); + + return (bool)$uid; + } + + /** + * @return int|false The new bookmark ID or false on failure + */ + public function insert(int $userId, string $routeIdentifier, string $arguments, string $title): int|false + { + $connection = $this->connectionPool->getConnectionForTable(self::TABLE_NAME); + $affectedRows = $connection->insert( + self::TABLE_NAME, + [ + 'userid' => $userId, + 'route' => $routeIdentifier, + 'arguments' => $arguments, + 'description' => $title ?: 'Bookmark', + 'sorting' => $GLOBALS['EXEC_TIME'], + ] + ); + + if ($affectedRows === 1) { + return (int)$connection->lastInsertId(); + } + + return false; + } + + public function update( + int $id, + ?int $userId, + string $title, + int|string $groupId, + bool $allowGlobalGroups = true + ): int { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + $queryBuilder->update(self::TABLE_NAME) + ->where( + $queryBuilder->expr()->eq( + 'uid', + $queryBuilder->createNamedParameter($id, Connection::PARAM_INT) + ) + ) + ->set('description', $title); + + if (is_string($groupId)) { + $queryBuilder->set('group_uuid', $groupId); + $queryBuilder->set('sc_group', BookmarkService::GROUP_DEFAULT); + } else { + $effectiveGroupId = $allowGlobalGroups ? $groupId : max(BookmarkService::GROUP_DEFAULT, $groupId); + $queryBuilder->set('sc_group', $effectiveGroupId); + $queryBuilder->set('group_uuid', null); + } + + if ($userId !== null) { + $queryBuilder->andWhere( + $queryBuilder->expr()->eq( + 'userid', + $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT) + ) + ); + if (!$allowGlobalGroups) { + $queryBuilder->andWhere( + $queryBuilder->expr()->gte( + 'sc_group', + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ) + ); + } + } + + return $queryBuilder->executeStatement(); + } + + public function delete(int $id, ?int $userId = null): int + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + $queryBuilder->delete(self::TABLE_NAME) + ->where( + $queryBuilder->expr()->eq( + 'uid', + $queryBuilder->createNamedParameter($id, Connection::PARAM_INT) + ) + ); + + if ($userId !== null) { + $queryBuilder->andWhere( + $queryBuilder->expr()->eq( + 'userid', + $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT) + ) + ); + } + + return $queryBuilder->executeStatement(); + } + + /** + * @param array $ids + */ + public function deleteMultiple(array $ids, int $userId): int + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + + return $queryBuilder->delete(self::TABLE_NAME) + ->where( + $queryBuilder->expr()->in( + 'uid', + $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY) + ), + $queryBuilder->expr()->eq( + 'userid', + $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT) + ) + ) + ->executeStatement(); + } + + public function updateSorting(int $id, int $userId, int $sorting): int + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + + return $queryBuilder->update(self::TABLE_NAME) + ->where( + $queryBuilder->expr()->eq( + 'uid', + $queryBuilder->createNamedParameter($id, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'userid', + $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT) + ) + ) + ->set('sorting', $sorting) + ->executeStatement(); + } + + /** + * @param array $ids + */ + public function moveToGroup(array $ids, int $userId, int|string $groupId, bool $allowGlobalGroups = true): int + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + $queryBuilder->update(self::TABLE_NAME) + ->where( + $queryBuilder->expr()->in( + 'uid', + $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY) + ), + $queryBuilder->expr()->eq( + 'userid', + $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT) + ) + ); + + if (is_string($groupId)) { + $queryBuilder->set('group_uuid', $groupId); + $queryBuilder->set('sc_group', BookmarkService::GROUP_DEFAULT); + } else { + $effectiveGroupId = $allowGlobalGroups ? $groupId : max(BookmarkService::GROUP_DEFAULT, $groupId); + $queryBuilder->set('sc_group', $effectiveGroupId); + $queryBuilder->set('group_uuid', null); + } + + return $queryBuilder->executeStatement(); + } + + public function findGroupsByUser(int $userId): array + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME); + + $result = $queryBuilder + ->select('*') + ->from(self::GROUP_TABLE_NAME) + ->where( + $queryBuilder->expr()->eq('userid', $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT)) + ) + ->orderBy('sorting', 'ASC') + ->executeQuery(); + + $groups = []; + while ($row = $result->fetchAssociative()) { + $groups[] = $row; + } + + return $groups; + } + + public function findGroupByUuid(string $uuid): ?array + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME); + $row = $queryBuilder + ->select('*') + ->from(self::GROUP_TABLE_NAME) + ->where( + $queryBuilder->expr()->eq('uuid', $queryBuilder->createNamedParameter($uuid)) + ) + ->executeQuery() + ->fetchAssociative(); + + return $row !== false ? $row : null; + } + + public function createGroup(int $userId, string $label): ?string + { + $uuid = (string)Uuid::v4(); + + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME); + + $maxSorting = $queryBuilder + ->select('sorting') + ->from(self::GROUP_TABLE_NAME) + ->where( + $queryBuilder->expr()->eq('userid', $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT)) + ) + ->orderBy('sorting', 'DESC') + ->setMaxResults(1) + ->executeQuery() + ->fetchOne(); + $sorting = ($maxSorting !== false ? (int)$maxSorting : 0) + 1; + + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME); + $affectedRows = $queryBuilder + ->insert(self::GROUP_TABLE_NAME) + ->values([ + 'uuid' => $uuid, + 'userid' => $userId, + 'label' => $label, + 'sorting' => $sorting, + ]) + ->executeStatement(); + + return $affectedRows === 1 ? $uuid : null; + } + + public function updateGroup(string $uuid, string $label): int + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME); + + return $queryBuilder + ->update(self::GROUP_TABLE_NAME) + ->where( + $queryBuilder->expr()->eq('uuid', $queryBuilder->createNamedParameter($uuid)) + ) + ->set('label', $label) + ->executeStatement(); + } + + public function deleteGroup(string $uuid): int + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME); + + return $queryBuilder + ->delete(self::GROUP_TABLE_NAME) + ->where( + $queryBuilder->expr()->eq('uuid', $queryBuilder->createNamedParameter($uuid)) + ) + ->executeStatement(); + } + + /** + * @param list $uuids + */ + public function reorderGroups(array $uuids, int $userId): void + { + $sorting = 0; + foreach ($uuids as $uuid) { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::GROUP_TABLE_NAME); + $queryBuilder + ->update(self::GROUP_TABLE_NAME) + ->where( + $queryBuilder->expr()->eq('uuid', $queryBuilder->createNamedParameter($uuid)), + $queryBuilder->expr()->eq('userid', $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT)) + ) + ->set('sorting', $sorting++) + ->executeStatement(); + } + } + + public function moveBookmarksFromGroupToDefault(string $uuid, int $userId): int + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + + return $queryBuilder + ->update(self::TABLE_NAME) + ->where( + $queryBuilder->expr()->eq('group_uuid', $queryBuilder->createNamedParameter($uuid)), + $queryBuilder->expr()->eq('userid', $queryBuilder->createNamedParameter($userId, Connection::PARAM_INT)) + ) + ->set('group_uuid', null) + ->set('sc_group', BookmarkService::GROUP_DEFAULT) + ->executeStatement(); + } +} diff --git a/Classes/Backend/Bookmark/BookmarkService.php b/Classes/Backend/Bookmark/BookmarkService.php new file mode 100644 index 0000000..df43689 --- /dev/null +++ b/Classes/Backend/Bookmark/BookmarkService.php @@ -0,0 +1,622 @@ +router; + } + + public function isEnabled(): bool + { + return (bool)($this->getBackendUser()->getTSConfig()['options.']['enableBookmarks'] ?? false); + } + + /** + * @return Bookmark[] + */ + public function getBookmarks(): array + { + $backendUser = $this->getBackendUser(); + $userId = (int)$backendUser->user['uid']; + $rows = $this->bookmarkRepository->findByUser($userId); + + // Build list of valid group IDs from existing groups + $groups = $this->getGroups(); + $validGroupIds = array_map(static fn(BookmarkGroup $g) => $g->id, $groups); + + $bookmarks = []; + + foreach ($rows as $row) { + // Skip bookmarks the user cannot read (e.g., global bookmarks they can't navigate to) + if (!$this->bookmarkVoter->vote(BookmarkVoter::READ, $row, $backendUser)) { + continue; + } + $row = $this->migrateBookmarkGroup($row, $validGroupIds); + $bookmark = $this->createBookmarkFromRow($row, $backendUser); + if ($bookmark !== null) { + $bookmarks[] = $bookmark; + } + } + + return $bookmarks; + } + + /** + * Ensures bookmark is assigned to a valid group for output. + */ + private function migrateBookmarkGroup(array $row, array $validGroupIds): array + { + $groupUuid = $row['group_uuid'] ?? null; + $scGroup = (int)($row['sc_group'] ?? 0); + $groupId = $groupUuid !== null ? $groupUuid : $scGroup; + + if (in_array($groupId, $validGroupIds, true)) { + return $row; + } + + $row['group_uuid'] = null; + $row['sc_group'] = $scGroup < 0 ? self::GROUP_SUPERGLOBAL : self::GROUP_DEFAULT; + + return $row; + } + + /** + * @return BookmarkGroup[] + */ + public function getGroups(): array + { + $groups = []; + $backendUser = $this->getBackendUser(); + $languageService = $this->getLanguageService(); + $globalPrefix = $languageService->sL('core.bookmarks:global'); + + // User-created groups + $userId = (int)$backendUser->user['uid']; + $userGroupRows = $this->bookmarkRepository->findGroupsByUser($userId); + foreach ($userGroupRows as $index => $row) { + $groups[] = $this->createBookmarkGroup( + $row['uuid'], + $row['label'] ?? '', + BookmarkGroupType::USER, + (int)($row['sorting'] ?? $index), + $backendUser, + isset($row['userid']) ? (int)$row['userid'] : null, + ); + } + + // TSconfig groups and their global counterparts + $configGroups = $backendUser->getTSConfig()['options.']['bookmarkGroups.'] ?? []; + $tsconfigIndex = 0; + if (is_array($configGroups)) { + foreach ($configGroups as $groupId => $configLabel) { + $groupId = (int)$groupId; + if ($groupId <= 0 || $groupId === 100 || $configLabel === '' || $configLabel === null) { + continue; + } + $label = $languageService->sL((string)$configLabel); + $sorting = $tsconfigIndex++; + + $groups[] = $this->createBookmarkGroup( + $groupId, + $label, + BookmarkGroupType::SYSTEM, + $sorting, + $backendUser, + ); + + $groups[] = $this->createBookmarkGroup( + -$groupId, + $globalPrefix . ': ' . $label, + BookmarkGroupType::GLOBAL, + $sorting, + $backendUser, + ); + } + } + + // Superglobal group + $groups[] = $this->createBookmarkGroup( + self::GROUP_SUPERGLOBAL, + $globalPrefix . ': ' . $languageService->sL('core.bookmarks:all'), + BookmarkGroupType::GLOBAL, + 0, + $backendUser, + ); + + // Default group + $groups[] = $this->createBookmarkGroup( + self::GROUP_DEFAULT, + $languageService->sL('core.bookmarks:group_default'), + BookmarkGroupType::SYSTEM, + 100, + $backendUser, + ); + + // Sort by type priority, then by sorting value + $priorityKeys = array_map(static fn(BookmarkGroup $g) => $g->type->getPriority(), $groups); + $sortingKeys = array_map(static fn(BookmarkGroup $g) => $g->sorting, $groups); + array_multisort($priorityKeys, SORT_ASC, SORT_NUMERIC, $sortingKeys, SORT_ASC, SORT_NUMERIC, $groups); + + return $groups; + } + + public function getBookmark(int $id): ?Bookmark + { + $row = $this->bookmarkRepository->findById($id); + if ($row === null) { + return null; + } + $backendUser = $this->getBackendUser(); + if (!$this->bookmarkVoter->vote(BookmarkVoter::READ, $row, $backendUser)) { + return null; + } + $groups = $this->getGroups(); + $validGroupIds = array_map(static fn(BookmarkGroup $g) => $g->id, $groups); + $row = $this->migrateBookmarkGroup($row, $validGroupIds); + return $this->createBookmarkFromRow($row, $backendUser); + } + + public function hasBookmark(string $routeIdentifier, string $arguments): bool + { + $userId = (int)$this->getBackendUser()->user['uid']; + return $this->bookmarkRepository->exists($userId, $routeIdentifier, $arguments); + } + + /** + * @return int|false The ID of the newly created bookmark, or false on failure + */ + public function createBookmark(string $routeIdentifier, string $arguments = '', string $title = ''): int|false + { + $backendUser = $this->getBackendUser(); + + if (!$this->bookmarkVoter->vote(BookmarkVoter::CREATE, [], $backendUser)) { + return false; + } + + if (!$this->router->hasRoute($routeIdentifier)) { + return false; + } + + if ($arguments !== '' && !json_validate($arguments)) { + return false; + } + + $userId = (int)$backendUser->user['uid']; + $result = $this->bookmarkRepository->insert($userId, $routeIdentifier, $arguments, $title); + + return $result; + } + + /** + * @return array{success: bool, error?: string} + */ + public function updateBookmark(int $id, string $title, int|string $groupId): array + { + $backendUser = $this->getBackendUser(); + + // Check if bookmark exists + $bookmark = $this->bookmarkRepository->findById($id); + if ($bookmark === null) { + return $this->errorResponse( + 'core.bookmarks:error.notFound.message' + ); + } + + // Check edit permission + if (!$this->bookmarkVoter->vote(BookmarkVoter::EDIT, $bookmark, $backendUser)) { + return $this->errorResponse( + 'core.bookmarks:error.accessDenied.message' + ); + } + + // Check if user can use global groups + if (is_int($groupId) && $groupId < 0 && !$backendUser->isAdmin()) { + return $this->errorResponse( + 'core.bookmarks:error.globalGroupNotAllowed.message' + ); + } + + $userId = $backendUser->isAdmin() ? null : (int)$backendUser->user['uid']; + $allowGlobalGroups = $backendUser->isAdmin(); + + $this->bookmarkRepository->update( + $id, + $userId, + $title, + $groupId, + $allowGlobalGroups + ); + + return ['success' => true]; + } + + /** + * @return array{success: false, error: string} + */ + private function errorResponse(string $label): array + { + return [ + 'success' => false, + 'error' => $this->getLanguageService()->sL($label), + ]; + } + + /** + * @return array{success: bool, error?: string} + */ + public function deleteBookmark(int $id): array + { + $backendUser = $this->getBackendUser(); + $bookmark = $this->bookmarkRepository->findById($id); + if ($bookmark === null) { + return $this->errorResponse( + 'core.bookmarks:error.notFound.message' + ); + } + + if (!$this->bookmarkVoter->vote(BookmarkVoter::DELETE, $bookmark, $backendUser)) { + return $this->errorResponse( + 'core.bookmarks:error.accessDenied.message' + ); + } + + $affectedRows = $this->bookmarkRepository->delete($id); + + if ($affectedRows !== 1) { + return $this->errorResponse( + 'core.bookmarks:error.deleteFailed.message' + ); + } + + return ['success' => true]; + } + + /** + * @param int[] $bookmarkIds + */ + public function reorderBookmarks(array $bookmarkIds): bool + { + $backendUser = $this->getBackendUser(); + $userId = (int)$backendUser->user['uid']; + + // Fetch all bookmarks in one query + $bookmarks = $this->bookmarkRepository->findByIds($bookmarkIds); + + $sorting = 0; + foreach ($bookmarkIds as $bookmarkId) { + $bookmark = $bookmarks[$bookmarkId] ?? null; + if ($bookmark !== null && $this->bookmarkVoter->vote(BookmarkVoter::EDIT, $bookmark, $backendUser)) { + $this->bookmarkRepository->updateSorting($bookmarkId, $userId, $sorting++); + } + } + + return true; + } + + /** + * @param int[] $bookmarkIds + */ + public function deleteBookmarks(array $bookmarkIds): bool + { + $backendUser = $this->getBackendUser(); + $userId = (int)$backendUser->user['uid']; + + // Fetch all bookmarks in one query + $bookmarks = $this->bookmarkRepository->findByIds($bookmarkIds); + + $manageableIds = []; + foreach ($bookmarkIds as $bookmarkId) { + $bookmark = $bookmarks[$bookmarkId] ?? null; + if ($bookmark !== null && $this->bookmarkVoter->vote(BookmarkVoter::DELETE, $bookmark, $backendUser)) { + $manageableIds[] = $bookmarkId; + } + } + + if ($manageableIds !== []) { + $this->bookmarkRepository->deleteMultiple($manageableIds, $userId); + } + + return true; + } + + /** + * @param int[] $bookmarkIds + */ + public function moveBookmarks(array $bookmarkIds, int|string $groupId): bool + { + $backendUser = $this->getBackendUser(); + $userId = (int)$backendUser->user['uid']; + $allowGlobalGroups = $backendUser->isAdmin(); + + // Validate target group access for user-created groups + if (is_string($groupId)) { + $group = $this->bookmarkRepository->findGroupByUuid($groupId); + if ($group === null || !$this->bookmarkGroupVoter->vote(BookmarkGroupVoter::READ, $group, $backendUser)) { + return false; + } + } + + // Fetch all bookmarks in one query and filter to those user can edit + $bookmarks = $this->bookmarkRepository->findByIds($bookmarkIds); + + $manageableIds = []; + foreach ($bookmarkIds as $bookmarkId) { + $bookmark = $bookmarks[$bookmarkId] ?? null; + if ($bookmark !== null && $this->bookmarkVoter->vote(BookmarkVoter::EDIT, $bookmark, $backendUser)) { + $manageableIds[] = $bookmarkId; + } + } + + if ($manageableIds === []) { + return false; + } + + $this->bookmarkRepository->moveToGroup( + $manageableIds, + $userId, + $groupId, + $allowGlobalGroups + ); + + return true; + } + + public function createGroup(string $label): ?BookmarkGroup + { + $backendUser = $this->getBackendUser(); + + if (!$this->bookmarkGroupVoter->vote(BookmarkGroupVoter::CREATE, [], $backendUser)) { + return null; + } + + $userId = (int)$backendUser->user['uid']; + $uuid = $this->bookmarkRepository->createGroup($userId, $label); + + if ($uuid === null) { + return null; + } + + $row = $this->bookmarkRepository->findGroupByUuid($uuid); + if ($row === null) { + return null; + } + + return $this->createBookmarkGroup( + $row['uuid'], + $row['label'] ?? '', + BookmarkGroupType::USER, + 0, + $backendUser, + isset($row['userid']) ? (int)$row['userid'] : null, + ); + } + + public function updateGroup(string $uuid, string $label): bool + { + $backendUser = $this->getBackendUser(); + $group = $this->bookmarkRepository->findGroupByUuid($uuid); + if ($group === null || !$this->bookmarkGroupVoter->vote(BookmarkGroupVoter::EDIT, $group, $backendUser)) { + return false; + } + + $this->bookmarkRepository->updateGroup($uuid, $label); + return true; + } + + public function deleteGroup(string $uuid): bool + { + $backendUser = $this->getBackendUser(); + $group = $this->bookmarkRepository->findGroupByUuid($uuid); + if ($group === null || !$this->bookmarkGroupVoter->vote(BookmarkGroupVoter::DELETE, $group, $backendUser)) { + return false; + } + + $userId = (int)$backendUser->user['uid']; + $this->bookmarkRepository->moveBookmarksFromGroupToDefault($uuid, $userId); + $affectedRows = $this->bookmarkRepository->deleteGroup($uuid); + + return $affectedRows > 0; + } + + /** + * @param string[] $uuids + */ + public function reorderGroups(array $uuids): bool + { + $backendUser = $this->getBackendUser(); + $userId = (int)$backendUser->user['uid']; + + // Get all user's groups in one query + $userGroups = $this->bookmarkRepository->findGroupsByUser($userId); + $groupsByUuid = []; + foreach ($userGroups as $group) { + $groupsByUuid[$group['uuid']] = $group; + } + + // Verify all provided UUIDs can be edited by the user + foreach ($uuids as $uuid) { + $group = $groupsByUuid[$uuid] ?? null; + if ($group === null || !$this->bookmarkGroupVoter->vote(BookmarkGroupVoter::EDIT, $group, $backendUser)) { + return false; + } + } + + $this->bookmarkRepository->reorderGroups($uuids, $userId); + + return true; + } + + private function createBookmarkFromRow(array $row, BackendUserAuthentication $user): ?Bookmark + { + $routeIdentifier = $row['route'] ?? ''; + + try { + $arguments = json_decode($row['arguments'] ?? '', true, 64, JSON_THROW_ON_ERROR); + } catch (\JsonException) { + return null; + } + + if (!is_array($arguments)) { + return null; + } + + $moduleName = $this->getModuleNameFromRouteIdentifier($routeIdentifier); + if ($moduleName === '') { + return null; + } + + $accessible = $this->bookmarkVoter->vote(BookmarkVoter::NAVIGATE, $row, $user); + $editable = $this->bookmarkVoter->vote(BookmarkVoter::EDIT, $row, $user); + + $groupUuid = $row['group_uuid'] ?? null; + $groupId = $groupUuid !== null ? $groupUuid : (int)$row['sc_group']; + $bookmarkData = $this->parseRecordEditData($routeIdentifier, $arguments); + $iconData = $this->resolveBookmarkIcon($routeIdentifier, $moduleName, $bookmarkData); + $href = $accessible ? (string)$this->uriBuilder->buildUriFromRoute($routeIdentifier, $arguments) : ''; + + return new Bookmark( + id: (int)$row['uid'], + route: $routeIdentifier, + arguments: $row['arguments'] ?? '', + title: ($row['description'] ?? false) ?: 'Bookmark', + groupId: $groupId, + iconIdentifier: $iconData['identifier'], + iconOverlayIdentifier: $iconData['overlay'], + module: $moduleName, + href: $href, + editable: $editable, + accessible: $accessible, + ); + } + + /** + * @return array{identifier: string, overlay: string} + */ + private function resolveBookmarkIcon(string $routeIdentifier, string $moduleName, array $bookmarkData): array + { + $identifier = ''; + $overlay = ''; + + switch ($routeIdentifier) { + case 'record_edit': + $table = $bookmarkData['table'] ?? ''; + $recordid = $bookmarkData['recordid'] ?? 0; + + $action = $bookmarkData['action'] ?? ''; + if ($action === 'edit') { + $row = BackendUtility::getRecordWSOL($table, (int)$recordid) ?? []; + $icon = $this->iconFactory->getIconForRecord($table, $row, IconSize::SMALL); + } elseif ($action === 'new') { + $icon = $this->iconFactory->getIconForRecord($table, [], IconSize::SMALL); + } else { + $icon = $this->iconFactory->getIcon('empty-empty', IconSize::SMALL); + } + $identifier = $icon->getIdentifier(); + $overlay = $icon->getOverlayIcon()?->getIdentifier() ?? ''; + break; + + case 'file_edit': + $identifier = 'mimetypes-text-html'; + break; + + default: + $iconIdentifier = ''; + if ($module = $this->moduleProvider->getModule($moduleName, null, false)) { + $iconIdentifier = $module->getIconIdentifier(); + if ($iconIdentifier === '' && $module->getParentModule()) { + $iconIdentifier = $module->getParentModule()->getIconIdentifier(); + } + } + if ($iconIdentifier === '') { + $iconIdentifier = 'empty-empty'; + } + $identifier = $iconIdentifier; + } + + return [ + 'identifier' => $identifier, + 'overlay' => $overlay, + ]; + } + + private function createBookmarkGroup( + int|string $id, + string $label, + BookmarkGroupType $type, + int $sorting, + BackendUserAuthentication $user, + ?int $userid = null + ): BookmarkGroup { + $voterData = ['id' => $id, 'userid' => $userid]; + $editable = $this->bookmarkGroupVoter->vote(BookmarkGroupVoter::EDIT, $voterData, $user); + $selectable = $this->bookmarkGroupVoter->vote(BookmarkGroupVoter::SELECT, $voterData, $user); + + return new BookmarkGroup( + id: $id, + label: $label, + type: $type, + sorting: $sorting, + editable: $editable, + selectable: $selectable, + ); + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Backend/Bookmark/Security/BookmarkGroupVoter.php b/Classes/Backend/Bookmark/Security/BookmarkGroupVoter.php new file mode 100644 index 0000000..7835b49 --- /dev/null +++ b/Classes/Backend/Bookmark/Security/BookmarkGroupVoter.php @@ -0,0 +1,93 @@ + $this->canRead($group, $user), + self::CREATE => $this->canCreate($user), + self::EDIT => $this->canEdit($group, $user), + self::DELETE => $this->canDelete($group, $user), + self::SELECT => $this->canSelect($group, $user), + default => false, + }; + } + + private function canCreate(BackendUserAuthentication $user): bool + { + return isset($user->user['uid']) && (int)$user->user['uid'] > 0; + } + + private function canRead(array $group, BackendUserAuthentication $user): bool + { + $userId = (int)$user->user['uid']; + return (int)$group['userid'] === $userId; + } + + private function canEdit(array $group, BackendUserAuthentication $user): bool + { + $groupId = $group['id'] ?? null; + + // System/global groups (integer IDs) are not editable + if (is_int($groupId) || is_numeric($groupId)) { + return false; + } + + // User-created groups (UUID strings) are editable only if user owns them + if (!isset($group['userid'])) { + return false; + } + + $userId = (int)$user->user['uid']; + return (int)$group['userid'] === $userId; + } + + private function canDelete(array $group, BackendUserAuthentication $user): bool + { + // Delete permission mirrors edit permission + return $this->canEdit($group, $user); + } + + private function canSelect(array $group, BackendUserAuthentication $user): bool + { + $groupId = $group['id'] ?? 0; + + // Global groups (negative IDs) are only selectable by admins + if (is_int($groupId) && $groupId < 0) { + return $user->isAdmin(); + } + + return true; + } +} diff --git a/Classes/Backend/Bookmark/Security/BookmarkVoter.php b/Classes/Backend/Bookmark/Security/BookmarkVoter.php new file mode 100644 index 0000000..da749ca --- /dev/null +++ b/Classes/Backend/Bookmark/Security/BookmarkVoter.php @@ -0,0 +1,253 @@ +router; + } + + public function vote(string $attribute, array $bookmark, BackendUserAuthentication $user): bool + { + return match ($attribute) { + self::READ => $this->canRead($bookmark, $user), + self::CREATE => $this->canCreate($user), + self::EDIT => $this->canEdit($bookmark, $user), + self::DELETE => $this->canDelete($bookmark, $user), + self::NAVIGATE => $this->canNavigate($bookmark, $user), + default => false, + }; + } + + private function canCreate(BackendUserAuthentication $user): bool + { + return isset($user->user['uid']) && (int)$user->user['uid'] > 0; + } + + private function canRead(array $bookmark, BackendUserAuthentication $user): bool + { + $userId = (int)$user->user['uid']; + + // User's own bookmarks are always readable + if ((int)$bookmark['userid'] === $userId) { + return true; + } + + // Global bookmarks are only readable if user can navigate to them + if ((int)$bookmark['sc_group'] < 0) { + return $this->canNavigate($bookmark, $user); + } + + return false; + } + + private function canEdit(array $bookmark, BackendUserAuthentication $user): bool + { + if (!$this->canRead($bookmark, $user)) { + return false; + } + + $userId = (int)$user->user['uid']; + $isOwner = (int)$bookmark['userid'] === $userId; + $groupId = isset($bookmark['group_uuid']) && $bookmark['group_uuid'] !== '' ? $bookmark['group_uuid'] : (int)$bookmark['sc_group']; + $isGlobal = is_int($groupId) && $groupId < 0; + + if ($isOwner && !$isGlobal) { + return true; + } + + if ($user->isAdmin() && $isGlobal) { + return true; + } + + return false; + } + + private function canDelete(array $bookmark, BackendUserAuthentication $user): bool + { + $userId = (int)$user->user['uid']; + + if ((int)$bookmark['userid'] === $userId) { + return true; + } + + if ($user->isAdmin() && (int)$bookmark['sc_group'] < 0) { + return true; + } + + return false; + } + + private function canNavigate(array $bookmark, BackendUserAuthentication $user): bool + { + $routeIdentifier = $bookmark['route'] ?? ''; + + try { + $arguments = json_decode($bookmark['arguments'] ?? '', true, 64, JSON_THROW_ON_ERROR); + } catch (\JsonException) { + return false; + } + + if (!is_array($arguments)) { + return false; + } + + $moduleName = $this->getModuleNameFromRouteIdentifier($routeIdentifier); + if ($moduleName === '') { + return false; + } + + if (!$this->canAccessModule($routeIdentifier, $moduleName, $user)) { + return false; + } + + if (!$this->canAccessFile($moduleName, $arguments)) { + return false; + } + + if (!$this->canAccessRecord($moduleName, $routeIdentifier, $arguments, $user)) { + return false; + } + + return true; + } + + private function canAccessModule(string $routeIdentifier, string $moduleName, BackendUserAuthentication $user): bool + { + // record_edit has its own access checks via canAccessRecord + if ($routeIdentifier === 'record_edit') { + return true; + } + + return $this->moduleProvider->accessGranted($moduleName, $user); + } + + private function canAccessFile(string $moduleName, array $arguments): bool + { + if ($moduleName !== 'file_FilelistList' && $moduleName !== 'media_management') { + return true; + } + + $combinedIdentifier = (string)($arguments['id'] ?? ''); + if ($combinedIdentifier === '') { + return true; + } + + $storage = $this->storageRepository->findByCombinedIdentifier($combinedIdentifier); + if ($storage === null || $storage->isFallbackStorage()) { + return false; + } + + $folderIdentifier = substr($combinedIdentifier, strpos($combinedIdentifier, ':') + 1); + try { + $storage->getFolder($folderIdentifier); + } catch (InsufficientFolderAccessPermissionsException) { + return false; + } catch (FolderDoesNotExistException) { + return true; + } catch (\Throwable $e) { + $this->logger->error('Failed to resolve folder identifier "{folder}" in backend user bookmark: {message}', [ + 'folder' => $folderIdentifier, + 'message' => $e->getMessage(), + ]); + return false; + } + + return true; + } + + private function canAccessRecord( + string $moduleName, + string $routeIdentifier, + array $arguments, + BackendUserAuthentication $user + ): bool { + if ($moduleName === 'file_FilelistList' || $moduleName === 'media_management') { + return true; + } + + $bookmarkData = $this->parseRecordEditData($routeIdentifier, $arguments); + $pageId = 0; + + if ($moduleName === 'record_edit' && isset($bookmarkData['table'], $bookmarkData['recordid'])) { + if (!$user->check('tables_modify', $bookmarkData['table'])) { + return false; + } + + $action = $bookmarkData['action'] ?? ''; + $recordId = (int)$bookmarkData['recordid']; + + if ($action === 'edit' || ($action === 'new' && $recordId < 0)) { + $record = BackendUtility::getRecord($bookmarkData['table'], abs($recordId)); + if ($record === null || $record === []) { + return false; + } + $pageId = ($bookmarkData['table'] === 'pages' ? (int)($record['uid'] ?? 0) : (int)($record['pid'] ?? 0)); + } elseif ($action === 'new' && $recordId > 0) { + $pageId = $recordId; + } + } else { + $pageId = (int)($arguments['id'] ?? 0); + } + + if ($pageId > 0 && !$user->isAdmin()) { + if ($user->isInWebMount($pageId) === null) { + return false; + } + $pageRow = BackendUtility::getRecord('pages', $pageId); + if ($pageRow === null || !$user->doesUserHaveAccess($pageRow, Permission::PAGE_SHOW)) { + return false; + } + } + + return true; + } +} diff --git a/Classes/Backend/Bookmark/Traits/RouteParserTrait.php b/Classes/Backend/Bookmark/Traits/RouteParserTrait.php new file mode 100644 index 0000000..3679425 --- /dev/null +++ b/Classes/Backend/Bookmark/Traits/RouteParserTrait.php @@ -0,0 +1,67 @@ +getRouter()->getRoute($routeIdentifier)?->getOption('module')?->getIdentifier() ?? ''); + } + + /** + * @return array{table?: string, recordid?: string, action?: string} + */ + private function parseRecordEditData(string $routeIdentifier, array $arguments): array + { + if ($routeIdentifier !== 'record_edit' || !is_array($arguments['edit'] ?? null)) { + return []; + } + + $table = key($arguments['edit']); + $tableData = current($arguments['edit']); + $recordId = is_array($tableData) ? key($tableData) : null; + + if (!is_string($table) || (!is_string($recordId) && !is_int($recordId))) { + return []; + } + + $recordId = (string)$recordId; + if (str_ends_with($recordId, ',')) { + $recordId = substr($recordId, 0, -1); + } + + return [ + 'table' => $table, + 'recordid' => $recordId, + 'action' => $arguments['edit'][$table][$recordId] ?? '', + ]; + } +} diff --git a/Classes/Backend/ColorScheme.php b/Classes/Backend/ColorScheme.php new file mode 100644 index 0000000..94a313c --- /dev/null +++ b/Classes/Backend/ColorScheme.php @@ -0,0 +1,52 @@ + 'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:colorScheme.auto', + self::light => 'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:colorScheme.light', + self::dark => 'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:colorScheme.dark', + }; + } + + public function getIcon(): string + { + return match ($this) { + self::auto => 'actions-circle-half', + self::light => 'actions-brightness-high', + self::dark => 'actions-moon', + }; + } + + public static function getAvailableItemsForSelection(): array + { + return [ + ['label' => self::auto->getLabel(), 'value' => self::auto->value], + ['label' => self::light->getLabel(), 'value' => self::light->value], + ['label' => self::dark->getLabel(), 'value' => self::dark->value], + ]; + } +} diff --git a/Classes/Backend/Event/ModifyClearCacheActionsEvent.php b/Classes/Backend/Event/ModifyClearCacheActionsEvent.php new file mode 100644 index 0000000..72a5f99 --- /dev/null +++ b/Classes/Backend/Event/ModifyClearCacheActionsEvent.php @@ -0,0 +1,82 @@ + $cacheActions + * @param list $cacheActionIdentifiers + */ + public function __construct(private array $cacheActions, private array $cacheActionIdentifiers) {} + + /** + * @param CacheAction $cacheAction + */ + public function addCacheAction(array $cacheAction): void + { + $this->cacheActions[] = $cacheAction; + } + + /** + * @param list $cacheActions + */ + public function setCacheActions(array $cacheActions): void + { + $this->cacheActions = $cacheActions; + } + + /** + * @return list + */ + public function getCacheActions(): array + { + return $this->cacheActions; + } + + /** + * @param non-empty-string $cacheActionIdentifier + */ + public function addCacheActionIdentifier(string $cacheActionIdentifier): void + { + $this->cacheActionIdentifiers[] = $cacheActionIdentifier; + } + + /** + * @param list $cacheActionIdentifiers + */ + public function setCacheActionIdentifiers(array $cacheActionIdentifiers): void + { + $this->cacheActionIdentifiers = $cacheActionIdentifiers; + } + + /** + * @return list + */ + public function getCacheActionIdentifiers(): array + { + return $this->cacheActionIdentifiers; + } +} diff --git a/Classes/Backend/Event/SystemInformationToolbarCollectorEvent.php b/Classes/Backend/Event/SystemInformationToolbarCollectorEvent.php new file mode 100644 index 0000000..f305ef6 --- /dev/null +++ b/Classes/Backend/Event/SystemInformationToolbarCollectorEvent.php @@ -0,0 +1,34 @@ +toolbarItem; + } +} diff --git a/Classes/Backend/QrCodeSize.php b/Classes/Backend/QrCodeSize.php new file mode 100644 index 0000000..023cd81 --- /dev/null +++ b/Classes/Backend/QrCodeSize.php @@ -0,0 +1,39 @@ + 64, + self::MEDIUM => 128, + self::LARGE => 256, + self::MEGA => 512, + }; + } +} diff --git a/Classes/Backend/ThumbnailSize.php b/Classes/Backend/ThumbnailSize.php new file mode 100644 index 0000000..6738172 --- /dev/null +++ b/Classes/Backend/ThumbnailSize.php @@ -0,0 +1,48 @@ + $size . 'm', $this->getBaseDimensions()); + } + + public function getCroppedDimensions(): array + { + return array_map(static fn(int $size) => $size . 'c', $this->getBaseDimensions()); + } + + private function getBaseDimensions(): array + { + return match ($this) { + self::DEFAULT, self::SMALL => [32, 32], + self::MEDIUM => [64, 64], + self::LARGE => [96, 96], + }; + } +} diff --git a/Classes/Backend/ToolbarItems/BookmarkToolbarItem.php b/Classes/Backend/ToolbarItems/BookmarkToolbarItem.php new file mode 100644 index 0000000..73e450e --- /dev/null +++ b/Classes/Backend/ToolbarItems/BookmarkToolbarItem.php @@ -0,0 +1,97 @@ +request = $request; + } + + /** + * Checks whether the user has access to this toolbar item. + */ + public function checkAccess(): bool + { + return $this->bookmarkService->isEnabled(); + } + + /** + * Render bookmark icon. + */ + public function getItem(): string + { + $view = $this->backendViewFactory->create($this->request); + return $view->render('ToolbarItems/BookmarkToolbarItemItem'); + } + + /** + * This item has a drop-down. + */ + public function hasDropDown(): bool + { + return true; + } + + /** + * Render drop-down content. + * The dropdown contains a custom element that fetches data via AJAX. + */ + public function getDropDown(): string + { + $view = $this->backendViewFactory->create($this->request); + return $view->render('ToolbarItems/BookmarkToolbarItemDropDown'); + } + + /** + * This toolbar item needs no additional attributes. + */ + public function getAdditionalAttributes(): array + { + return []; + } + + /** + * Position relative to others. + */ + public function getIndex(): int + { + return 40; + } +} diff --git a/Classes/Backend/ToolbarItems/ClearCacheToolbarItem.php b/Classes/Backend/ToolbarItems/ClearCacheToolbarItem.php new file mode 100644 index 0000000..b160e6f --- /dev/null +++ b/Classes/Backend/ToolbarItems/ClearCacheToolbarItem.php @@ -0,0 +1,182 @@ + + */ + protected array $cacheActions = []; + + /** + * @var list + */ + protected array $optionValues = []; + + private ServerRequestInterface $request; + + public function __construct( + UriBuilder $uriBuilder, + EventDispatcherInterface $eventDispatcher, + private readonly BackendViewFactory $backendViewFactory, + ) { + $isAdmin = $this->getBackendUser()->isAdmin(); + $userTsConfig = $this->getBackendUser()->getTSConfig(); + + // Clear all page-related caches + if ($isAdmin || ($userTsConfig['options.']['clearCache.']['pages'] ?? false)) { + $this->cacheActions[] = [ + 'id' => 'pages', + 'title' => 'core.cache:group.pages.label', + 'description' => 'core.cache:group.pages.description', + 'endpoint' => (string)$uriBuilder->buildUriFromRoute('ajax_clearcache_group_pages'), + 'severity' => 'success', + 'iconIdentifier' => 'actions-bolt-alt', + ]; + $this->optionValues[] = 'pages'; + } + + // Clearing of all caches is only shown if explicitly enabled via TSConfig + // or if BE-User is admin and the TSconfig explicitly disables the possibility for admins. + // This is useful for big production systems where admins accidentally could slow down the system. + if (($userTsConfig['options.']['clearCache.']['all'] ?? false) + || ($isAdmin && (bool)($userTsConfig['options.']['clearCache.']['all'] ?? true)) + ) { + $this->cacheActions[] = [ + 'id' => 'all', + 'title' => 'core.cache:group.all.label', + 'description' => 'core.cache:group.all.description', + 'endpoint' => (string)$uriBuilder->buildUriFromRoute('ajax_clearcache_group_all'), + 'severity' => 'danger', + 'iconIdentifier' => 'actions-bolt-alt', + ]; + $this->optionValues[] = 'all'; + } + + $event = new ModifyClearCacheActionsEvent($this->cacheActions, $this->optionValues); + $event = $eventDispatcher->dispatch($event); + $this->cacheActions = $event->getCacheActions(); + + $this->optionValues = $event->getCacheActionIdentifiers(); + } + + public function setRequest(ServerRequestInterface $request): void + { + $this->request = $request; + } + + /** + * Checks whether the user has access to this toolbar item. + */ + public function checkAccess(): bool + { + $backendUser = $this->getBackendUser(); + if ($backendUser->isAdmin()) { + return true; + } + foreach ($this->optionValues as $value) { + if ($backendUser->getTSConfig()['options.']['clearCache.'][$value] ?? false) { + return true; + } + } + return false; + } + + /** + * Render clear cache icon, based on the option if there is more than one icon or just one. + */ + public function getItem(): string + { + $view = $this->backendViewFactory->create($this->request); + if ($this->hasDropDown()) { + return $view->render('ToolbarItems/ClearCacheToolbarItem'); + } + $cacheAction = end($this->cacheActions); + $view->assignMultiple([ + 'endpoint' => $cacheAction['endpoint'], + 'title' => $cacheAction['title'], + 'iconIdentifier' => $cacheAction['iconIdentifier'], + ]); + return $view->render('ToolbarItems/ClearCacheToolbarItemSingle'); + } + + /** + * Render drop-down. + */ + public function getDropDown(): string + { + $view = $this->backendViewFactory->create($this->request); + $view->assign('cacheActions', $this->cacheActions); + return $view->render('ToolbarItems/ClearCacheToolbarItemDropDown'); + } + + /** + * No additional attributes needed. + */ + public function getAdditionalAttributes(): array + { + return []; + } + + /** + * This item has a drop-down, if there is more than one cache action available for the current Backend user. + */ + public function hasDropDown(): bool + { + return count($this->cacheActions) > 1; + } + + /** + * Position relative to others + */ + public function getIndex(): int + { + return 20; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Backend/ToolbarItems/LiveSearchToolbarItem.php b/Classes/Backend/ToolbarItems/LiveSearchToolbarItem.php new file mode 100644 index 0000000..ad6bfef --- /dev/null +++ b/Classes/Backend/ToolbarItems/LiveSearchToolbarItem.php @@ -0,0 +1,98 @@ +request = $request; + } + + /** + * Checks whether the user has access to this toolbar item. + * Live search depends on the records module and only available when that module is allowed. + */ + public function checkAccess(): bool + { + return $this->moduleProvider->accessGranted('records', $this->getBackendUser()); + } + + /** + * Render search field. + */ + public function getItem(): string + { + $view = $this->backendViewFactory->create($this->request); + return $view->render('ToolbarItems/LiveSearchToolbarItem'); + } + + /** + * This item needs additional attributes. + */ + public function getAdditionalAttributes(): array + { + return ['class' => 't3js-toolbar-item-search']; + } + + /** + * This item has no drop-down. + */ + public function hasDropDown(): bool + { + return false; + } + + /** + * No drop-down here. + */ + public function getDropDown(): string + { + return ''; + } + + /** + * Position relative to others, live search should be very right. + */ + public function getIndex(): int + { + return 10; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Backend/ToolbarItems/SystemInformationToolbarItem.php b/Classes/Backend/ToolbarItems/SystemInformationToolbarItem.php new file mode 100644 index 0000000..782a5ec --- /dev/null +++ b/Classes/Backend/ToolbarItems/SystemInformationToolbarItem.php @@ -0,0 +1,352 @@ +highestSeverity = InformationStatus::INFO; + } + + public function setRequest(ServerRequestInterface $request): void + { + $this->request = $request; + } + + /** + * Add a system message. + * This is a callback method for signal receivers. + * + * @param string $text The text to be displayed + * @param InformationStatus $status The status of this system message + * @param int $count Will be added to the total count + * @param string $module The associated module + * @param string $params Query string with additional parameters + */ + public function addSystemMessage($text, InformationStatus $status = InformationStatus::OK, $count = 0, $module = '', $params = ''): void + { + $this->systemMessageTotalCount += $count; + + // define the severity for the badge + if ($status->isGreaterThan($this->highestSeverity)) { + $this->highestSeverity = $status; + } + + $this->systemMessages[] = [ + 'module' => $module, + 'params' => $params, + 'count' => $count, + 'status' => $status->value, + 'text' => $text, + ]; + } + + /** + * Add a system information. + * This is a callback method for signal receivers. + * + * @param string $title The title of this system information, typically a LLL:EXT:... label string + * @param string $value The associated value + * @param string $iconIdentifier The icon identifier + * @param InformationStatus $status The status of this system information + */ + public function addSystemInformation($title, $value, $iconIdentifier, InformationStatus $status = InformationStatus::NOTICE): void + { + $this->systemInformation[] = [ + 'title' => $title, + 'value' => $value, + 'iconIdentifier' => $iconIdentifier, + 'status' => $status->value, + ]; + } + + /** + * Checks whether the user has access to this toolbar item. + */ + public function checkAccess(): bool + { + return $this->getBackendUserAuthentication()->isAdmin(); + } + + /** + * Render system information dropdown. + */ + public function getItem(): string + { + $view = $this->backendViewFactory->create($this->request); + return $view->render('ToolbarItems/SystemInformationToolbarItem'); + } + + /** + * Render drop-down + */ + public function getDropDown(): string + { + if (!$this->checkAccess()) { + return ''; + } + $this->collectInformation(); + $view = $this->backendViewFactory->create($this->request); + $view->assignMultiple([ + 'messages' => $this->systemMessages, + 'count' => $this->systemMessageTotalCount > 99 ? '99+' : $this->systemMessageTotalCount, + 'severityBadgeClass' => $this->severityBadgeClass, + 'systemInformation' => $this->systemInformation, + ]); + return $view->render('ToolbarItems/SystemInformationDropDown'); + } + + /** + * No additional attributes needed. + */ + public function getAdditionalAttributes(): array + { + return []; + } + + /** + * This item has a drop-down. + */ + public function hasDropDown(): bool + { + return true; + } + + /** + * Position relative to others + */ + public function getIndex(): int + { + return 30; + } + + /** + * Collect the information for the drop-down. + */ + protected function collectInformation(): void + { + $this->addTypo3Version(); + $this->addInstallationMode(); + $this->addWebServer(); + $this->addPhpVersion(); + $this->addDebugger(); + $this->addDatabase(); + $this->addApplicationContext(); + $this->addGitRevision(); + $this->addOperatingSystem(); + $this->eventDispatcher->dispatch(new SystemInformationToolbarCollectorEvent($this)); + $this->severityBadgeClass = $this->highestSeverity !== InformationStatus::NOTICE ? 'badge-' . $this->highestSeverity->value : ''; + } + + protected function addTypo3Version(): void + { + $this->systemInformation[] = [ + 'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.typo3-version', + 'value' => $this->typo3Version->getVersion(), + 'iconIdentifier' => 'information-typo3-version', + ]; + } + + protected function addInstallationMode(): void + { + $this->systemInformation[] = [ + 'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.installationMethod', + 'value' => Environment::isComposerMode() + ? $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.installationMethod.composer') + : $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.installationMethod.classic'), + 'iconIdentifier' => 'actions-package', + ]; + } + + protected function addWebServer(): void + { + $this->systemInformation[] = [ + 'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.webserver', + 'value' => $_SERVER['SERVER_SOFTWARE'] ?? '', + 'iconIdentifier' => 'information-webserver', + ]; + } + + protected function addPhpVersion(): void + { + $this->systemInformation[] = [ + 'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.phpversion', + 'value' => PHP_VERSION, + 'iconIdentifier' => 'information-php-version', + ]; + } + + protected function addDebugger(): void + { + $knownDebuggers = ['xdebug', 'Zend Debugger']; + foreach ($knownDebuggers as $debugger) { + if (extension_loaded($debugger)) { + $debuggerVersion = phpversion($debugger) ?: ''; + $this->systemInformation[] = [ + 'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.debugger', + 'value' => sprintf('%s %s', $debugger, $debuggerVersion), + 'iconIdentifier' => 'information-debugger', + ]; + } + } + } + + protected function addDatabase(): void + { + foreach (GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionNames() as $connectionName) { + $serverVersion = '[' . $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.database.offline') . ']'; + $success = true; + try { + $serverVersion = GeneralUtility::makeInstance(ConnectionPool::class) + ->getConnectionByName($connectionName) + ->getPlatformServerVersion(); + } catch (\Exception $exception) { + $success = false; + } + $this->systemInformation[] = [ + 'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.database', + 'titleAddition' => $connectionName, + 'value' => $serverVersion, + 'status' => $success ? InformationStatus::NOTICE->value : InformationStatus::ERROR->value, + 'iconIdentifier' => 'information-database', + ]; + } + } + + protected function addApplicationContext(): void + { + $applicationContext = Environment::getContext(); + $this->systemInformation[] = [ + 'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.applicationcontext', + 'value' => (string)$applicationContext, + 'status' => $applicationContext->isProduction() ? InformationStatus::NOTICE->value : InformationStatus::WARNING->value, + 'iconIdentifier' => 'information-application-context', + ]; + } + + /** + * Gets the current GIT revision and branch + */ + protected function addGitRevision(): void + { + if (!str_ends_with($this->typo3Version->getVersion(), '-dev') || $this->isFunctionDisabled('exec')) { + return; + } + // check if git exists + $returnCode = 0; + CommandUtility::exec('git --version', $_, $returnCode); + if ($returnCode !== 0) { + // git is not available + return; + } + + $revision = CommandUtility::exec('git rev-parse --short HEAD'); + $branch = CommandUtility::exec('git rev-parse --abbrev-ref HEAD'); + if ($revision === false || $branch === false) { + return; + } + $revision = trim($revision); + $branch = trim($branch); + if ($revision !== '' && $branch !== '') { + $this->systemInformation[] = [ + 'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.gitrevision', + 'value' => sprintf('%s [%s]', $revision, $branch), + 'iconIdentifier' => 'information-git', + ]; + } + } + + /** + * Gets the system kernel and version + */ + protected function addOperatingSystem(): void + { + switch (PHP_OS_FAMILY) { + case 'Linux': + $icon = 'linux'; + break; + case 'Darwin': + $icon = 'apple'; + break; + case 'Windows': + $icon = 'windows'; + break; + default: + $icon = 'unknown'; + } + $this->systemInformation[] = [ + 'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.sysinfo.operatingsystem', + 'value' => PHP_OS . ' ' . php_uname('r'), + 'iconIdentifier' => 'information-os-' . $icon, + ]; + } + + /** + * Check if the given PHP function is disabled in the system. + */ + protected function isFunctionDisabled(string $functionName): bool + { + $disabledFunctions = GeneralUtility::trimExplode(',', (string)ini_get('disable_functions')); + if (!empty($disabledFunctions)) { + return in_array($functionName, $disabledFunctions, true); + } + return false; + } + + protected function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Backend/ToolbarItems/UserToolbarItem.php b/Classes/Backend/ToolbarItems/UserToolbarItem.php new file mode 100644 index 0000000..ab64454 --- /dev/null +++ b/Classes/Backend/ToolbarItems/UserToolbarItem.php @@ -0,0 +1,196 @@ +request = $request; + } + + /** + * Item is always enabled. + */ + public function checkAccess(): bool + { + return true; + } + + /** + * Render username and an icon. + */ + public function getItem(): string + { + $backendUser = $this->getBackendUser(); + $view = $this->backendViewFactory->create($this->request); + $view->assignMultiple([ + 'currentUser' => $backendUser->user, + 'switchUserMode' => (int)$backendUser->getOriginalUserIdWhenInSwitchUserMode(), + ]); + return $view->render('ToolbarItems/UserToolbarItem'); + } + + /** + * Render drop-down content. + */ + public function getDropDown(): string + { + $backendUser = $this->getBackendUser(); + + $mostRecentUsers = []; + if ($backendUser->isAdmin() + && $backendUser->getOriginalUserIdWhenInSwitchUserMode() === null + && isset($backendUser->uc['recentSwitchedToUsers']) + && is_array($backendUser->uc['recentSwitchedToUsers']) + ) { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users'); + $result = $queryBuilder + ->select('uid', 'username', 'realName') + ->from('be_users') + ->where( + $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($backendUser->uc['recentSwitchedToUsers'], Connection::PARAM_INT_ARRAY)) + )->executeQuery(); + + // Flip the array to have a "sorted" list of items + $mostRecentUsers = array_flip($backendUser->uc['recentSwitchedToUsers']); + + while ($row = $result->fetchAssociative()) { + $mostRecentUsers[$row['uid']] = $row; + } + + // Remove any item that is not an array (means, the stored uid is not available anymore) + $mostRecentUsers = array_filter($mostRecentUsers, is_array(...)); + + $availableUsers = array_keys($mostRecentUsers); + if (!empty(array_diff($backendUser->uc['recentSwitchedToUsers'], $availableUsers))) { + $backendUser->uc['recentSwitchedToUsers'] = $availableUsers; + $backendUser->writeUC(); + } + } + + $modules = null; + if ($userModule = $this->moduleProvider->getModuleForMenu('user', $backendUser)) { + $modules = $userModule->getSubModules(); + } + $helpModules = null; + if ($helpModule = $this->moduleProvider->getModuleForMenu('help', $this->getBackendUser())) { + $helpModules = $helpModule->getSubModules(); + } + $view = $this->backendViewFactory->create($this->request); + $view->assignMultiple([ + 'modules' => $modules, + 'helpModules' => $helpModules, + 'switchUserMode' => $this->getBackendUser()->getOriginalUserIdWhenInSwitchUserMode() !== null, + 'recentUsers' => $mostRecentUsers, + 'colorSchemeSwitchEnabled' => $this->getColorSchemeSwitchEnabled(), + 'activeColorScheme' => $backendUser->uc['colorScheme'] ?? 'auto', + 'colorSchemes' => $this->getColorSchemes(), + ]); + return $view->render('ToolbarItems/UserToolbarItemDropDown'); + } + + /** + * Returns an additional class if user is in "switch user" mode. + */ + public function getAdditionalAttributes(): array + { + $result = [ + 'class' => 'toolbar-item-user', + ]; + if ($this->getBackendUser()->getOriginalUserIdWhenInSwitchUserMode()) { + $result['class'] .= ' su-user'; + } + return $result; + } + + /** + * This item has a drop-down. + */ + public function hasDropDown(): bool + { + return true; + } + + /** + * Position relative to others. + */ + public function getIndex(): int + { + return 90; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getColorSchemeSwitchEnabled(): bool + { + $backendUser = $this->getBackendUser(); + $userTS = $backendUser->getTSConfig(); + + return !isset($userTS['setup.']['fields.']['colorScheme.']['disabled']) || $userTS['setup.']['fields.']['colorScheme.']['disabled'] !== '1'; + } + + protected function getColorSchemes(): array + { + $schemes = []; + + foreach (ColorScheme::cases() as $scheme) { + $schemeItem = [ + 'label' => $this->getLanguageService()->sL($scheme->getLabel()), + 'value' => $scheme->value, + 'icon' => $scheme->getIcon(), + ]; + + $schemes[] = $schemeItem; + } + + return $schemes; + } +} diff --git a/Classes/Breadcrumb/BreadcrumbContext.php b/Classes/Breadcrumb/BreadcrumbContext.php new file mode 100644 index 0000000..0df2bb4 --- /dev/null +++ b/Classes/Breadcrumb/BreadcrumbContext.php @@ -0,0 +1,73 @@ + Parent Page > "Create New Content") + * $suffixNode = new BreadcrumbNode(identifier: 'new', label: 'Create New Content'); + * $context = new BreadcrumbContext($parentPage, [$suffixNode]); + * + * @internal Subject to change until v15 LTS + */ +final readonly class BreadcrumbContext +{ + /** + * @param RecordInterface|ResourceInterface|null $mainContext The main entity (record or resource) + * @param BreadcrumbNode[] $suffixNodes Additional nodes to append after the main breadcrumb trail + */ + public function __construct( + public RecordInterface|ResourceInterface|null $mainContext, + public array $suffixNodes = [], + ) {} + + /** + * Checks if this context has a valid main entity. + */ + public function hasContext(): bool + { + return $this->mainContext !== null; + } + + /** + * Checks if this context has suffix nodes. + */ + public function hasSuffixNodes(): bool + { + return $this->suffixNodes !== []; + } +} diff --git a/Classes/Breadcrumb/BreadcrumbFactory.php b/Classes/Breadcrumb/BreadcrumbFactory.php new file mode 100644 index 0000000..8a75956 --- /dev/null +++ b/Classes/Breadcrumb/BreadcrumbFactory.php @@ -0,0 +1,236 @@ +logger->warning( + 'Failed to load record for breadcrumb', + ['table' => $table, 'uid' => $uid] + ); + return new BreadcrumbContext(null, []); + } + + try { + $record = $this->recordFactory->createResolvedRecordFromDatabaseRow($table, $rawRecord); + return new BreadcrumbContext($record, []); + } catch (\Exception $e) { + // @todo: Catching \Exception here is a code smell, this shouldn't be so generic and can hide away too many issues. + $this->logger->error( + 'Failed to create record instance for breadcrumb', + ['table' => $table, 'uid' => $uid, 'exception' => $e->getMessage()] + ); + return new BreadcrumbContext(null, []); + } + } + + /** + * Creates breadcrumb context for editing multiple records. + * + * Shows a generic "Edit Multiple [RecordType]" node instead of individual records. + * + * @param string $table The table name + * @param int $pid The parent page ID + * @return BreadcrumbContext Context with parent page and "edit multiple" suffix node + */ + public function forEditMultipleAction(string $table, int $pid): BreadcrumbContext + { + $parentRecord = $this->getParentPageRecord($pid); + $schema = $this->tcaSchemaFactory->has($table) ? $this->tcaSchemaFactory->get($table) : null; + + $recordTypeLabel = $schema?->getTitle($this->getLanguageService()->sL(...)) + ?? $schema?->getTitle() + ?? $table; + + $suffixNode = new BreadcrumbNode( + identifier: 'edit-multiple-' . $table, + label: sprintf( + $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editMultiple'), + $recordTypeLabel + ), + icon: $this->iconFactory->getIconForRecord($table, [])->getIdentifier(), + ); + + return new BreadcrumbContext($parentRecord, [$suffixNode]); + } + + /** + * Creates breadcrumb context for creating a new record. + * + * @param string $table The table name + * @param int $pid The parent page ID + * @param array $defaults Default values for the new record (used for icon overlay) + * @return BreadcrumbContext Context with parent page and "create new" suffix node + */ + public function forNewAction(string $table, int $pid, array $defaults = []): BreadcrumbContext + { + $parentRecord = $this->getParentPageRecord($pid); + $schema = $this->tcaSchemaFactory->has($table) ? $this->tcaSchemaFactory->get($table) : null; + + $recordTypeLabel = $schema?->getTitle($this->getLanguageService()->sL(...)) + ?? $schema?->getTitle() + ?? $table; + + try { + $icon = $this->iconFactory->getIconForRecord($table, $defaults); + $suffixNode = new BreadcrumbNode( + identifier: 'new-' . $table, + label: sprintf( + $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createNew'), + $recordTypeLabel + ), + icon: $icon->getIdentifier(), + iconOverlay: 'overlay-new', + ); + } catch (\Exception $e) { + $this->logger->warning( + 'Failed to create icon for new record breadcrumb', + ['table' => $table, 'exception' => $e->getMessage()] + ); + $suffixNode = new BreadcrumbNode( + identifier: 'new-' . $table, + label: sprintf( + $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createNew'), + $recordTypeLabel + ), + ); + } + + return new BreadcrumbContext($parentRecord, [$suffixNode]); + } + + /** + * Creates breadcrumb context from a page record array. + * + * Example: + * `$view->getDocHeaderComponent()->setBreadcrumbContext($this->breadcrumbFactory->forPageArray($pageInfo));` + * + * @param array $pageRecord The page record array (must contain 'uid') + * @return BreadcrumbContext Context with the page record or null on failure + */ + public function forPageArray(array $pageRecord): BreadcrumbContext + { + if (!isset($pageRecord['uid'])) { + $this->logger->warning('Page record array must contain uid for breadcrumb'); + return new BreadcrumbContext(null, []); + } + + try { + $record = $this->recordFactory->createResolvedRecordFromDatabaseRow('pages', $pageRecord); + return new BreadcrumbContext($record, []); + } catch (\Exception $e) { + $this->logger->error( + 'Failed to create page record instance for breadcrumb', + ['uid' => $pageRecord['uid'], 'exception' => $e->getMessage()] + ); + return new BreadcrumbContext(null, []); + } + } + + /** + * Creates breadcrumb context for any resource (file or folder). + * + * @param ResourceInterface $resource The resource (file or folder) + * @return BreadcrumbContext Context with the resource + */ + public function forResource(ResourceInterface $resource): BreadcrumbContext + { + return new BreadcrumbContext($resource, []); + } + + /** + * Gets the parent page record for a given PID. + * + * @param int $pid The page ID + * @return RecordInterface|null The page record or null if not found/accessible + */ + private function getParentPageRecord(int $pid): ?RecordInterface + { + if ($pid <= 0) { + return null; + } + + $rawRecord = BackendUtility::getRecord('pages', $pid); + if ($rawRecord === null) { + $this->logger->warning( + 'Failed to load parent page for breadcrumb', + ['pid' => $pid] + ); + return null; + } + + try { + return $this->recordFactory->createResolvedRecordFromDatabaseRow('pages', $rawRecord); + } catch (\Exception $e) { + // @todo: Catching \Exception here is a code smell, this shouldn't be so generic and can hide away too many issues. + $this->logger->error( + 'Failed to create page record instance for breadcrumb', + ['pid' => $pid, 'exception' => $e->getMessage()] + ); + return null; + } + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Breadcrumb/BreadcrumbProviderInterface.php b/Classes/Breadcrumb/BreadcrumbProviderInterface.php new file mode 100644 index 0000000..a4286ce --- /dev/null +++ b/Classes/Breadcrumb/BreadcrumbProviderInterface.php @@ -0,0 +1,61 @@ +hasContext(); + } + + public function generate(?BreadcrumbContext $context, ?ServerRequestInterface $request): array + { + $breadcrumb = []; + + $currentModule = $this->moduleResolver->resolveModule($request); + if ($currentModule !== null) { + // Add parent modules first (for third-level modules) + $breadcrumb = $this->buildModuleHierarchy($currentModule); + } + + // Handle file storage tree + if ($currentModule?->getNavigationComponent() === '@typo3/backend/tree/file-storage-tree-container') { + $id = $request?->getQueryParams()['id'] ?? null; + $label = $this->getLanguageService()->sL($currentModule->getTitle()); + $icon = 'apps-filetree-folder'; + + if ($id !== null && $storage = $this->storageRepository->findByCombinedIdentifier($id)) { + $label = $storage->getName(); + if (!$storage->isOnline() || !$storage->isBrowsable()) { + $icon = 'apps-filetree-folder-locked'; + } + } + + $breadcrumb[] = new BreadcrumbNode( + identifier: (string)$id, + label: $label, + icon: $icon, + ); + } + + // Handle page tree (default for null context or no module) + if ($currentModule === null || $currentModule->getNavigationComponent() === '@typo3/backend/tree/page-tree-element') { + $breadcrumb[] = new BreadcrumbNode( + identifier: '0', + label: (string)$GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], + icon: 'apps-pagetree-root', + ); + } + + return $breadcrumb; + } + + public function getPriority(): int + { + // Low priority - only handles null contexts as fallback + return 0; + } + + /** + * Builds the module hierarchy including parent modules. + * + * For third-level modules, this returns [parent, current]. + * For second-level modules, this returns [current]. + * For standalone modules, this returns [current]. + * + * @return BreadcrumbNode[] + */ + private function buildModuleHierarchy(ModuleInterface $currentModule): array + { + $modules = []; + $moduleChain = []; + + // Build chain from current to root + $module = $currentModule; + while ($module !== null) { + $moduleChain[] = $module; + $module = $module->getParentModule(); + } + + // Reverse to get root-to-current order and skip the top-level parent (main menu item) + $moduleChain = array_reverse($moduleChain); + + // Skip the first item if we have more than one (first is the main menu container like "web") + if (count($moduleChain) > 1) { + array_shift($moduleChain); + } + + // Build breadcrumb nodes for each module in the chain + foreach ($moduleChain as $module) { + $modules[] = new BreadcrumbNode( + identifier: $module->getIdentifier(), + label: $this->getLanguageService()->sL($module->getTitle()), + icon: $module->getIconIdentifier(), + url: (string)$this->uriBuilder->buildUriFromRoute($module->getIdentifier(), $module->getNavigationComponent() === '@typo3/backend/tree/page-tree-element' ? ['id' => '0'] : []), + forceShowIcon: true, + ); + } + + return $modules; + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Breadcrumb/RecordBreadcrumbProvider.php b/Classes/Breadcrumb/RecordBreadcrumbProvider.php new file mode 100644 index 0000000..773070e --- /dev/null +++ b/Classes/Breadcrumb/RecordBreadcrumbProvider.php @@ -0,0 +1,275 @@ +mainContext instanceof RecordInterface; + } + + public function generate(?BreadcrumbContext $context, ?ServerRequestInterface $request): array + { + if ($context === null || !$context->mainContext instanceof RecordInterface) { + return []; + } + + $record = $context->mainContext; + $breadcrumb = []; + $currentModule = $this->moduleResolver->resolveModule($request); + $showRootline = $this->shouldShowRootline($currentModule); + $targetModule = $currentModule !== null + ? $this->extractRouteIdentifier($request, $currentModule) + : $this->getTargetModule(); + + // Add module hierarchy (for third-level modules, this includes parent modules) + if ($currentModule !== null) { + $breadcrumb = array_merge($breadcrumb, $this->buildModuleHierarchy($currentModule, $request, $showRootline)); + } else { + $breadcrumb[] = new BreadcrumbNode( + identifier: '0', + label: (string)$GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], + icon: 'apps-pagetree-root', + url: (string)$this->uriBuilder->buildUriFromRoute($targetModule, $showRootline ? ['id' => '0'] : []), + ); + } + + // Add page rootline if applicable + if ($showRootline) { + $breadcrumb = array_merge($breadcrumb, $this->buildRootline($record, $targetModule)); + } + + // Add the current record + $breadcrumb[] = $this->buildRecordNode($record, $targetModule); + + return $breadcrumb; + } + + public function getPriority(): int + { + return 10; + } + + /** + * Returns the target module identifier for navigation. + */ + private function getTargetModule(): string + { + // Default to web_layout for page-based navigation + return 'web_layout'; + } + + /** + * Builds the page rootline for a record. + * + * @return BreadcrumbNode[] + */ + private function buildRootline(RecordInterface $record, string $targetModule): array + { + $breadcrumb = []; + $pid = $record->getPid(); + + try { + $rootline = BackendUtility::BEgetRootLine($pid); + if ($rootline === []) { + return []; + } + + // Remove the site root (already added as first node) + array_pop($rootline); + ksort($rootline); + + foreach ($rootline as $item) { + if (!is_array($item) || !isset($item['uid'])) { + continue; + } + + try { + $icon = $this->iconFactory->getIconForRecord('pages', $item, IconSize::SMALL); + $breadcrumb[] = new BreadcrumbNode( + identifier: (string)$item['uid'], + label: BackendUtility::cropToTitleLength($item['title'] ?? ''), + icon: $icon->getIdentifier(), + iconOverlay: $icon->getOverlayIcon()?->getIdentifier(), + url: (string)$this->uriBuilder->buildUriFromRoute($targetModule, ['id' => $item['uid']]), + ); + } catch (\Exception $e) { + $this->logger->warning( + 'Failed to create breadcrumb node for page', + ['uid' => $item['uid'], 'exception' => $e->getMessage()] + ); + } + } + } catch (\Exception $e) { + $this->logger->warning( + 'Failed to build rootline for record', + ['table' => $record->getMainType(), 'uid' => $record->getUid(), 'exception' => $e->getMessage()] + ); + } + + return $breadcrumb; + } + + /** + * Builds a breadcrumb node for the current record. + */ + private function buildRecordNode(RecordInterface $record, string $targetModule): BreadcrumbNode + { + try { + $icon = $this->iconFactory->getIconForRecord( + $record->getMainType(), + $record->getRawRecord()?->toArray(), + IconSize::SMALL + ); + + $recordTitle = BackendUtility::getRecordTitle($record->getMainType(), $record->getRawRecord()?->toArray()); + return new BreadcrumbNode( + identifier: (string)$record->getUid(), + label: BackendUtility::cropToTitleLength($recordTitle), + icon: $icon->getIdentifier(), + iconOverlay: $icon->getOverlayIcon()?->getIdentifier(), + url: $record->getMainType() === 'pages' ? (string)$this->uriBuilder->buildUriFromRoute($targetModule, ['id' => (string)$record->getUid()]) : null, + ); + } catch (\Exception $e) { + $this->logger->error( + 'Failed to create breadcrumb node for record', + ['table' => $record->getMainType(), 'uid' => $record->getUid(), 'exception' => $e->getMessage()] + ); + + // Return a minimal fallback node + return new BreadcrumbNode( + identifier: (string)$record->getUid(), + label: $record->getMainType() . ' [' . $record->getUid() . ']', + ); + } + } + + /** + * Builds the module hierarchy including parent modules. + * + * For third-level modules, this returns [parent, current]. + * For second-level modules, this returns [current]. + * For standalone modules, this returns [current]. + * + * @return BreadcrumbNode[] + */ + private function buildModuleHierarchy(ModuleInterface $currentModule, ?ServerRequestInterface $request, bool $showRootline): array + { + $modules = []; + $moduleChain = []; + + // Build chain from current to root + $module = $currentModule; + while ($module !== null) { + $moduleChain[] = $module; + $module = $module->getParentModule(); + } + + // Reverse to get root-to-current order and skip the top-level parent (main menu item) + $moduleChain = array_reverse($moduleChain); + + // Skip the first item if we have more than one (first is the main menu container like "web") + if (count($moduleChain) > 1) { + array_shift($moduleChain); + } + + // Build breadcrumb nodes for each module in the chain + foreach ($moduleChain as $index => $module) { + $isLastModule = $index === count($moduleChain) - 1; + // For the last module (current), use the full route identifier to preserve route/action + // For parent modules, use base module identifier + $routeIdentifier = $isLastModule ? $this->extractRouteIdentifier($request, $module) : $module->getIdentifier(); + $modules[] = new BreadcrumbNode( + identifier: $module->getIdentifier(), + label: $this->getLanguageService()->sL($module->getTitle()), + icon: $module->getIconIdentifier(), + url: (string)$this->uriBuilder->buildUriFromRoute($routeIdentifier, $showRootline ? ['id' => '0'] : []), + forceShowIcon: true, + ); + } + + return $modules; + } + + /** + * Determines if the rootline should be shown based on the current module. + * + * Modules using the page tree navigation component typically support page-based navigation. + */ + private function shouldShowRootline(?ModuleInterface $currentModule): bool + { + // @todo This is quite implicit, but using the page-tree-element as navigation component + // signals that the current module can handle ?id= as a page parameter. + return $currentModule === null || $currentModule->getNavigationComponent() === '@typo3/backend/tree/page-tree-element'; + } + + /** + * Extracts the route identifier from the current request. + * + * This returns the full route identifier (e.g., 'manage_search_index.Administration_externalDocuments') + * to preserve sub-routes and actions in breadcrumb navigation. + * + * @return string The route identifier or module identifier as fallback + */ + private function extractRouteIdentifier(?ServerRequestInterface $request, ModuleInterface $module): string + { + // Try to get the full route identifier from routing attribute + if ($request !== null + && ($routeResult = $request->getAttribute('routing')) !== null + && ($route = $routeResult->getRoute()) !== null + && !empty(($routeIdentifier = $route->getOption('_identifier'))) + ) { + return $routeIdentifier; + } + + // Fallback to module identifier + return $module->getIdentifier(); + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Breadcrumb/ResourceBreadcrumbProvider.php b/Classes/Breadcrumb/ResourceBreadcrumbProvider.php new file mode 100644 index 0000000..de2f20a --- /dev/null +++ b/Classes/Breadcrumb/ResourceBreadcrumbProvider.php @@ -0,0 +1,201 @@ +mainContext instanceof ResourceInterface; + } + + public function generate(?BreadcrumbContext $context, ?ServerRequestInterface $request): array + { + if ($context === null || !$context->mainContext instanceof ResourceInterface) { + return []; + } + + $resource = $context->mainContext; + $breadcrumb = []; + $currentModule = $this->moduleResolver->resolveModule($request); + + // Add module node + if ($currentModule !== null) { + $languageService = $this->getLanguageService(); + $breadcrumb[] = new BreadcrumbNode( + identifier: $currentModule->getIdentifier(), + label: $languageService->sL($currentModule->getTitle()), + icon: $currentModule->getIconIdentifier(), + iconOverlay: null, + url: (string)$this->uriBuilder->buildUriFromRoute($currentModule->getIdentifier(), ['id' => '']), + forceShowIcon: true, + ); + } + + // Build resource hierarchy + $resourceHierarchy = $this->buildResourceHierarchy($resource); + + // Add resource nodes + foreach ($resourceHierarchy as $item) { + try { + $icon = $this->iconFactory->getIconForResource($item, IconSize::SMALL); + $label = $item->getName(); + $combinedIdentifier = $this->getCombinedIdentifier($item); + + // Use storage name for root folder + if ($item->getIdentifier() === $item->getStorage()->getRootLevelFolder()->getIdentifier()) { + $label = $item->getStorage()->getName(); + } + + $breadcrumb[] = new BreadcrumbNode( + identifier: $combinedIdentifier, + label: $label, + icon: $icon->getIdentifier(), + iconOverlay: $icon->getOverlayIcon()?->getIdentifier(), + url: (string)$this->uriBuilder->buildUriFromRoute($this->getTargetModule(), ['id' => $combinedIdentifier]), + ); + } catch (\Exception $e) { + $this->logger->warning( + 'Failed to create breadcrumb node for resource', + ['identifier' => $item->getIdentifier(), 'exception' => $e->getMessage()] + ); + } + } + + return $breadcrumb; + } + + public function getPriority(): int + { + return 10; + } + + /** + * Returns the target module identifier for navigation. + */ + private function getTargetModule(): string + { + return 'media_management'; + } + + /** + * Builds the resource hierarchy from root to current resource. + * + * @return ResourceInterface[] + */ + private function buildResourceHierarchy(ResourceInterface $resource): array + { + $hierarchy = []; + $folder = null; + + // Start with the resource itself + if ($resource instanceof FileInterface) { + $hierarchy[] = $resource; + try { + $folder = $resource->getParentFolder(); + } catch (\Exception $e) { + $this->logger->warning( + 'Failed to get parent folder for file', + ['identifier' => $resource->getIdentifier(), 'exception' => $e->getMessage()] + ); + return $hierarchy; + } + } elseif ($resource instanceof FolderInterface) { + $folder = $resource; + } + + // Traverse up the folder hierarchy + if ($folder instanceof Folder) { + $currentFolder = $folder; + $hierarchy[] = $folder; + + // Walk up to the root folder + $maxDepth = 100; // Safety limit to prevent infinite loops + $depth = 0; + + while ($depth < $maxDepth) { + $depth++; + + try { + $parent = $currentFolder->getParentFolder(); + } catch (InsufficientFolderAccessPermissionsException $e) { + // User doesn't have access to parent folder, stop here + $this->logger->info( + 'Stopped breadcrumb traversal due to insufficient folder access', + ['folder' => $currentFolder->getCombinedIdentifier()] + ); + break; + } + + // Check if we've reached the root (parent points to itself) + if ($parent->getCombinedIdentifier() === $currentFolder->getCombinedIdentifier()) { + break; + } + + // Add parent to hierarchy and continue upwards + $hierarchy[] = $parent; + $currentFolder = $parent; + } + } + + // Reverse to get root-to-current order + return array_reverse($hierarchy); + } + + /** + * Gets the combined identifier for a resource. + * Constructs it from storage UID and resource identifier. + */ + private function getCombinedIdentifier(ResourceInterface $resource): string + { + return $resource->getStorage()->getUid() . ':' . $resource->getIdentifier(); + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Clipboard/Clipboard.php b/Classes/Clipboard/Clipboard.php new file mode 100644 index 0000000..ba55649 --- /dev/null +++ b/Classes/Clipboard/Clipboard.php @@ -0,0 +1,830 @@ +=1 and denotes the pad-number + * 'mode' : 'copy' means copy-mode, default = moving ('cut') + * 'el' : Array of elements: + * DB: keys = '[tablename]|[uid]' eg. 'tt_content:123' + * DB: values = 1 (basically insignificant) + * FILE: keys = '_FILE|[md5 of path]' eg. '_FILE|9ebc7e5c74' + * FILE: values = The full filepath, eg. '/www/htdocs/typo3/32/dummy/fileadmin/sem1_3_examples/alternative_index.php' + * or 'C:/www/htdocs/typo3/32/dummy/fileadmin/sem1_3_examples/alternative_index.php' + * + * 'current' pointer to current tab (among the above...) + * + * The virtual tablename '_FILE' will always indicate files/folders. When checking for elements from eg. 'all tables' + * (by using an empty string) '_FILE' entries are excluded (so in effect only DB elements are counted) + */ + public array $clipData = []; + + public bool $changed = false; + + public string $current = ''; + + public bool $lockToNormal = false; + + public int $numberOfPads = 3; + + protected ?ServerRequestInterface $request = null; + + public function __construct( + protected readonly IconFactory $iconFactory, + protected readonly UriBuilder $uriBuilder, + protected readonly ResourceFactory $resourceFactory, + protected readonly TcaSchemaFactory $tcaSchemaFactory, + ) {} + + /***************************************** + * + * Initialize + * + ****************************************/ + /** + * Initialize the clipboard from the be_user session + */ + public function initializeClipboard(?ServerRequestInterface $request = null): void + { + // Initialize the request + // @todo: Clipboard does two things: It is a repository to find out which records + // are in the clipboard, and it is a class to help with rendering the + // clipboard. $request is optional and only used in rendering. + // It would be better to split these two aspects into single classes. + $this->request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? null; + + $userTsConfig = $this->getBackendUser()->getTSConfig(); + // Get data + $clipData = $this->getBackendUser()->getModuleData('clipboard', !empty($userTsConfig['options.']['saveClipboard']) ? '' : 'ses') ?: []; + $clipData += ['normal' => []]; + $this->numberOfPads = MathUtility::forceIntegerInRange((int)($userTsConfig['options.']['clipboardNumberPads'] ?? 3), 0, 20); + // Resets/reinstates the clipboard pads + $this->clipData['normal'] = is_array($clipData['normal']) ? $clipData['normal'] : []; + for ($a = 1; $a <= $this->numberOfPads; $a++) { + $index = 'tab_' . $a; + $this->clipData[$index] = is_iterable($clipData[$index] ?? null) ? $clipData[$index] : []; + } + // Setting the current pad pointer ($this->current)) + $current = (string)($clipData['current'] ?? ''); + $this->current = isset($this->clipData[$current]) ? $current : 'normal'; + $this->clipData['current'] = $this->current; + } + + /** + * Call this method after initialization if you want to lock the clipboard to operate on the normal pad only. + * Trying to switch pad through ->setCmd will not work. + * This is used by the clickmenu since it only allows operation on single elements at a time (that is the "normal" pad) + */ + public function lockToNormal(): void + { + $this->lockToNormal = true; + $this->current = 'normal'; + } + + /** + * The array $cmd may hold various keys which notes some action to take. + * Normally perform only one action at a time. + * In scripts like db_list.php / filelist/mod1/index.php the GET-var CB is used to control the clipboard. + * + * Selecting / Deselecting elements + * Array $cmd['el'] has keys = element-ident, value = element value (see description of clipData array in header) + * Selecting elements for 'copy' should be done by simultaneously setting setCopyMode. + * + * @param array $cmd Array of actions, see function description + */ + public function setCmd(array $cmd): void + { + $cmd['el'] ??= []; + $cmd['el'] = is_iterable($cmd['el']) ? $cmd['el'] : []; + foreach ($cmd['el'] as $key => $value) { + if ($this->current === 'normal') { + unset($this->clipData['normal']); + } + if ($value) { + $this->clipData[$this->current]['el'][$key] = $value; + } else { + $this->removeElement((string)$key); + } + $this->changed = true; + } + // Change clipboard pad (if not locked to normal) + if ($cmd['setP'] ?? false) { + $this->setCurrentPad((string)$cmd['setP']); + } + // Remove element (value = item ident: DB; '[tablename]|[uid]' FILE: '_FILE|[md5 hash of path]' + if ($cmd['remove'] ?? false) { + $this->removeElement((string)$cmd['remove']); + $this->changed = true; + } + // Remove all on current pad (value = pad-ident) + if ($cmd['removeAll'] ?? false) { + $this->clipData[$cmd['removeAll']] = []; + $this->changed = true; + } + // Set copy mode of the tab + if (isset($cmd['setCopyMode'])) { + $this->clipData[$this->current]['mode'] = $cmd['setCopyMode'] ? 'copy' : ''; + $this->changed = true; + } + } + + /** + * Setting the current pad on clipboard + * + * @param string $padIdentifier Key in the array $this->clipData + */ + public function setCurrentPad(string $padIdentifier): void + { + // Change clipboard pad (if not locked to normal) + if (!$this->lockToNormal && $this->current !== $padIdentifier) { + if (isset($this->clipData[$padIdentifier])) { + $this->clipData['current'] = ($this->current = $padIdentifier); + } + if ($this->current !== 'normal' || !$this->isElements()) { + $this->clipData[$this->current]['mode'] = ''; + } + // Setting mode to default (move) if no items on it or if not 'normal' + $this->changed = true; + } + } + + /** + * Call this after initialization and setCmd in order to save the clipboard to the user session. + * The function will check if the internal flag ->changed has been set and if so, save the clipboard. Else not. + */ + public function endClipboard(): void + { + if ($this->changed) { + $this->saveClipboard(); + } + $this->changed = false; + } + + /** + * Cleans up an incoming element array $CBarr (Array selecting/deselecting elements) + * + * @param array $CBarr Element array from outside ("key" => "selected/deselected") + * @param string $table The 'table which is allowed'. Must be set. + * @param bool $removeDeselected Can be set in order to remove entries which are marked for deselection. + * @return array Processed input $CBarr + */ + public function cleanUpCBC(array $CBarr, string $table, bool $removeDeselected = false): array + { + foreach ($CBarr as $reference => $value) { + [$referenceTable] = explode('|', $reference, 2); + if ($referenceTable !== $table || ($removeDeselected && !$value)) { + unset($CBarr[$reference]); + } + } + return $CBarr; + } + + public function getClipboardData(string $table = ''): array + { + $lang = $this->getLanguageService(); + + $clipboardData = [ + 'current' => $this->current, + 'copyMode' => $this->currentMode(), + 'elementCount' => count($this->elFromTable($table)), + ]; + + // Initialize tabs by adding the "normal" tab + $tabs = [ + [ + 'identifier' => 'normal', + 'info' => $this->getTabInfo('normal', $table), + 'title' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.normal'), + 'description' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.normal-description'), + 'items' => $this->current === 'normal' ? $this->getTabItems('normal', $table) : [], + ], + ]; + // Add numeric tabs + for ($a = 1; $a <= $this->numberOfPads; $a++) { + $tabs[] = [ + 'identifier' => 'tab_' . $a, + 'info' => $this->getTabInfo('tab_' . $a, $table), + 'title' => sprintf($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cliptabs-name'), (string)$a), + 'description' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cliptabs-description'), + 'items' => $this->current === 'tab_' . $a ? $this->getTabItems('tab_' . $a, $table) : [], + ]; + } + // Add tabs to clipboard Data + $clipboardData['tabs'] = $tabs; + + return $clipboardData; + } + + /** + * Get the items for the given pad identifier + * + * @param string $padIdentifier Pad reference + * @return array The tab items + */ + protected function getTabItems(string $padIdentifier, string $currentTable): array + { + if (!is_array($this->clipData[$padIdentifier]['el'] ?? false)) { + return []; + } + + $records = []; + foreach ($this->clipData[$padIdentifier]['el'] as $reference => $value) { + if (!$value) { + // Skip element if empty value + continue; + } + [$table, $uid] = explode('|', $reference); + // Rendering files/directories on the clipboard + if ($table === '_FILE') { + $fileObject = $this->resourceFactory->retrieveFileOrFolderObject($value); + if ($fileObject) { + $thumb = ''; + $folder = $fileObject instanceof Folder; + $size = $folder ? '' : '(' . GeneralUtility::formatSize((int)$fileObject->getSize()) . 'bytes)'; + /** @var File $fileObject */ + if (!$folder && ($fileObject->isImage() || $fileObject->isMediaFile())) { + $processedFile = $fileObject->process( + ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, + [ + 'maxWidth' => 64, + 'maxHeight' => 64, + ] + ); + $thumb = ''; + } + $linkItemText = BackendUtility::cropToTitleLength($fileObject->getName()); + $combinedIdentifier = $fileObject->getParentFolder()->getCombinedIdentifier(); + $filesRequested = $currentTable === '_FILE'; + $records[] = [ + 'identifier' => '_FILE|' . md5($value), + 'icon' => $this->iconFactory + ->getIconForResource($fileObject, IconSize::SMALL) + ->setTitle($fileObject->getName() . ' ' . $size) + ->render(), + 'title' => $this->linkItemText(htmlspecialchars($linkItemText), $combinedIdentifier, $filesRequested), + 'thumb' => $thumb, + 'infoDataDispatch' => [ + 'action' => 'TYPO3.InfoWindow.showItem', + 'args' => GeneralUtility::jsonEncodeForHtmlAttribute([$table, $value], false), + ], + ]; + } else { + // If the file did not exist (or is illegal) then it is removed from the clipboard immediately: + unset($this->clipData[$padIdentifier]['el'][$reference]); + $this->changed = true; + } + } else { + // Rendering records: + $record = BackendUtility::getRecordWSOL($table, (int)$uid); + if (is_array($record)) { + $isRequestedTable = $currentTable !== '_FILE'; + $records[] = [ + 'identifier' => $table . '|' . $uid, + 'icon' => $this->iconFactory->getIconForRecord($table, $record, IconSize::SMALL)->render(), + 'title' => $this->linkItemText(htmlspecialchars(BackendUtility::cropToTitleLength(BackendUtility::getRecordTitle( + $table, + $record + ))), $record, $isRequestedTable), + 'infoDataDispatch' => [ + 'action' => 'TYPO3.InfoWindow.showItem', + 'args' => GeneralUtility::jsonEncodeForHtmlAttribute([$table, (int)$uid], false), + ], + ]; + + $localizationData = $this->getLocalizations($table, $record, $isRequestedTable); + if (!empty($localizationData)) { + $records = array_merge($records, $localizationData); + } + } else { + unset($this->clipData[$padIdentifier]['el'][$reference]); + $this->changed = true; + } + } + } + $this->endClipboard(); + return $records; + } + + /** + * Returns true if the clipboard contains elements + */ + public function hasElements(): bool + { + foreach ($this->clipData as $data) { + if (isset($data['el']) && is_array($data['el']) && !empty($data['el'])) { + return true; + } + } + return false; + } + + /** + * Gets all localizations of the current record. + * + * @param string $table The table + * @param array $parentRecord The parent record + * @param bool $isRequestedTable Whether the element is from the requested table + * @return array HTML table rows + */ + protected function getLocalizations(string $table, array $parentRecord, bool $isRequestedTable): array + { + if (!$this->tcaSchemaFactory->has($table)) { + return []; + } + $schema = $this->tcaSchemaFactory->get($table); + if (!$schema->isLanguageAware()) { + return []; + } + + $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); + + $records = []; + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)); + + $queryBuilder + ->select('*') + ->from($table) + ->where( + $queryBuilder->expr()->eq( + $languageCapability->getTranslationOriginPointerField()->getName(), + $queryBuilder->createNamedParameter((int)$parentRecord['uid'], Connection::PARAM_INT) + ), + $queryBuilder->expr()->neq( + $languageCapability->getLanguageField()->getName(), + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ), + $queryBuilder->expr()->gt( + 'pid', + $queryBuilder->createNamedParameter(-1, Connection::PARAM_INT) + ) + ) + ->orderBy($languageCapability->getLanguageField()->getName()); + + foreach ($queryBuilder->executeQuery()->fetchAllAssociative() as $record) { + $title = htmlspecialchars(BackendUtility::cropToTitleLength(BackendUtility::getRecordTitle($table, $record))); + if (!$isRequestedTable) { + // In case the current table is not the requested table, e.g. "_FILE", wrap title in "muted" style + $title = '' . $title . ''; + } + $records[] = [ + 'icon' => $this->iconFactory->getIconForRecord($table, $record, IconSize::SMALL)->render(), + 'title' => $title, + 'infoDataDispatch' => [ + 'action' => 'TYPO3.InfoWindow.showItem', + 'args' => GeneralUtility::jsonEncodeForHtmlAttribute([$table, (int)$record['uid']], false), + ], + ]; + } + + return $records; + } + + /** + * Additional information for the tab. This is either + * the current copyMode (for "normal") or the elements + * count, for numeric tabs. Latter will not be shown, + * in case no elements exist for the tab. + * + * @param string $padIdentifier Identifier for the clipboard pad + * @param string $table The table name to count for elements + */ + protected function getTabInfo(string $padIdentifier, string $table = ''): string + { + $el = count($this->elFromTable($table, $padIdentifier)); + if (!$el) { + return ''; + } + $modeLabel = ($this->clipData['normal']['mode'] ?? '') === 'copy' ? $this->clipboardLabel('cm.copy') : $this->clipboardLabel('cm.cut'); + return ' (' . ($padIdentifier === 'normal' ? $modeLabel : htmlspecialchars((string)$el)) . ')'; + } + + /** + * Wraps the title of the element in a link to the page/folder where they originate from. + * Will be wrapped into "muted" style in case the element is not from the currently requested table. + * + * @param string $itemText Title of element - must be htmlspecialchar'ed on beforehand. + * @param array|string $reference If array, a record is expected. If string, its the folders' combined identifier + * @param bool $isRequestedTable Whether the element is from the requested table + */ + protected function linkItemText(string $itemText, $reference, bool $isRequestedTable): string + { + if (is_array($reference)) { + if ($isRequestedTable) { + // Wrap in link to corresponding page in recordlist in case current requested table matches + $itemText = '' . $itemText . ''; + } else { + $itemText = '' . $itemText . ''; + } + } elseif (is_string($reference)) { + if ($isRequestedTable && ExtensionManagementUtility::isLoaded('filelist')) { + // Wrap in link to the files folder in case current requested table matches and filelist is loaded + $itemText = '' . $itemText . ''; + } else { + $itemText = '' . $itemText . ''; + } + } + return $itemText; + } + + /** + * Returns the select-url for database elements + * + * @param string $table Table name + * @param int $uid Uid of record + * @param bool $copy If set, copymode will be enabled + * @param bool $deselect If set, the link will deselect, otherwise select. + * @return string URL linking to the current script but with the CB array set to select the element with table/uid + */ + public function selUrlDB(string $table, int $uid, bool $copy = false, bool $deselect = false): string + { + return $this->buildUrl(['CB' => [ + 'el' => [ + $table . '|' . $uid => $deselect ? 0 : 1, + ], + 'setCopyMode' => (int)$copy, + ]]); + } + + /** + * Returns the select-url for files + * + * @param string $path Filepath + * @param bool $copy If set, copymode will be enabled + * @param bool $deselect If set, the link will deselect, otherwise select. + * @return string URL linking to the current script but with the CB array set to select the path + */ + public function selUrlFile(string $path, bool $copy = false, bool $deselect = false): string + { + return $this->buildUrl(['CB' => [ + 'el' => [ + '_FILE|' . md5($path) => $deselect ? '' : $path, + ], + 'setCopyMode' => (int)$copy, + ]]); + } + + /** + * pasteUrl of the element (database and file) + * For the meaning of $table and $uid, please read from ->makePasteCmdArray!!! + * The URL will point to tce_file or tce_db depending in $table + * + * @param string $table Tablename (_FILE for files) + * @param string|int $identifier "destination": can be positive or negative indicating how the paste is done + * (paste into / paste after). For files, this is the combined identifier. + * @param bool $setRedirect If set, then the redirect URL will point back to the current script, but with CB reset. + * @param array|null $update Additional key/value pairs which should get set in the moved/copied record (via DataHandler) + */ + public function pasteUrl(string $table, $identifier, bool $setRedirect = true, ?array $update = null): string + { + $urlParameters = [ + 'CB' => [ + 'paste' => $table . '|' . $identifier, + 'pad' => $this->current, + ], + ]; + if ($setRedirect) { + $urlParameters['redirect'] = $this->buildUrl(['CB' => []]); + } + if (is_array($update)) { + $urlParameters['CB']['update'] = $update; + } + return (string)$this->uriBuilder->buildUriFromRoute($table === '_FILE' ? 'tce_file' : 'tce_db', $urlParameters); + } + + /** + * Returns confirm JavaScript message + * + * @param string $table Table name + * @param array|string $reference For records its an array, for files its a string (path) + * @param string $type Type-code + * @return string the text for a confirm message + */ + public function confirmMsgText( + string $table, + $reference, + string $type, + CountMode $countMode = CountMode::CURRENT, + ): string { + if (!$this->getBackendUser()->jsConfirmation(JsConfirmation::COPY_MOVE_PASTE)) { + return ''; + } + + $selectedElements = match ($countMode) { + CountMode::CURRENT => $this->elFromTable($table), + CountMode::ALL => $this->elFromTable(), + }; + + $labelKey = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.' + . ($this->currentMode() === 'copy' ? 'copy' : 'move') + . ($this->current === 'normal' ? '' : 'cb') . '_' . $type; + $confirmationMessage = $this->getLanguageService()->sL($labelKey); + + if ($table === '_FILE' && is_string($reference)) { + $recordTitle = PathUtility::basename($reference); + if ($this->current === 'normal') { + $selectedItem = reset($selectedElements); + $selectedRecordTitle = PathUtility::basename($selectedItem); + } else { + $selectedRecordTitle = (string)count($selectedElements); + } + } else { + $recordTitle = $table === 'pages' && !is_array($reference) + ? $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] + : BackendUtility::getRecordTitle($table, $reference); + if ($this->current === 'normal') { + $selectedItem = $this->getSelectedRecord(); + $selectedRecordTitle = $selectedItem['_RECORD_TITLE']; + } else { + $selectedRecordTitle = (string)count($selectedElements); + } + } + + return sprintf( + $confirmationMessage, + GeneralUtility::fixed_lgd_cs($selectedRecordTitle, 30), + GeneralUtility::fixed_lgd_cs($recordTitle, 30) + ); + } + + /** + * Clipboard label - getting from "EXT:core/Resources/Private/Language/locallang_core.xlf:" + * + * @param string $key Label Key + * @return string htmspecialchared' label + */ + protected function clipboardLabel(string $key): string + { + return htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:' . $key)); + } + + /***************************************** + * + * Helper functions + * + ****************************************/ + /** + * Removes element on clipboard + * + * @param string $elementKey Key of element in ->clipData array + */ + public function removeElement(string $elementKey): void + { + unset($this->clipData[$this->current]['el'][$elementKey]); + $this->changed = true; + } + + /** + * Saves the clipboard, no questions asked. + * Use ->endClipboard normally (as it checks if changes has been done so saving is necessary) + */ + protected function saveClipboard(): void + { + $this->getBackendUser()->pushModuleData('clipboard', $this->clipData); + } + + /** + * Returns the current mode, 'copy' or 'cut' + * + * @return string "copy" or "cut + */ + public function currentMode(): string + { + return ($this->clipData[$this->current]['mode'] ?? '') === 'copy' ? 'copy' : 'cut'; + } + + /** + * This traverses the elements on the current clipboard pane + * and unsets elements which does not exist anymore or are disabled. + */ + public function cleanCurrent(): void + { + if (!is_array($this->clipData[$this->current]['el'] ?? false)) { + return; + } + + foreach ($this->clipData[$this->current]['el'] as $reference => $value) { + [$table, $uid] = explode('|', $reference); + $unset = false; + + if (!$value) { + $unset = true; + } elseif ($table === '_FILE') { + try { + $fileOrFolder = $this->resourceFactory->retrieveFileOrFolderObject($value); + + if (($fileOrFolder instanceof File || $fileOrFolder instanceof Folder) + && !$fileOrFolder->checkActionPermission('read') + ) { + $unset = true; + } + } catch (InsufficientFolderAccessPermissionsException|ResourceDoesNotExistException) { + // If either the file has been deleted in the meantime or the user lacks permissions + // for the folder, we just remove the clipboard entry silently + $unset = true; + } + } elseif (!$this->isRecordAccessAllowed($table, (int)$uid)) { + $unset = true; + } + + if ($unset) { + $this->removeElement($reference); + } + } + } + + protected function isRecordAccessAllowed(string $table, int $uid): bool + { + $row = BackendUtility::getRecord($table, (int)$uid, ['uid', 'pid']); + if (!is_array($row)) { + return false; + } + + if (!$this->getBackendUser()->check('tables_select', $table)) { + return false; + } + + $schema = $this->tcaSchemaFactory->get($table); + $rootLevelCapability = $schema->getCapability(TcaSchemaCapability::RestrictionRootLevel); + + $pid = (int)($table === 'pages' ? $row['uid'] : $row['pid']); + if ($pid === 0) { + return $this->getBackendUser()->isAdmin() || $rootLevelCapability->canAccessRecordsOnRootLevel(); + } + + $page = BackendUtility::getRecord('pages', $pid); + if (!is_array($page)) { + return false; + } + + if (!$this->getBackendUser()->doesUserHaveAccess($page, Permission::PAGE_SHOW)) { + return false; + } + + return true; + } + + /** + * Counts the number of elements from the table $matchTable. If $matchTable is blank, all tables (except '_FILE' of course) is counted. + * + * @param string $matchTable Table to match/count for. + * @param string $padIdentifier Can optionally be used to set another pad than the current. + * @return array Array with keys from the CB. + */ + public function elFromTable(string $matchTable = '', string $padIdentifier = ''): array + { + $padIdentifier = $padIdentifier ?: $this->current; + + if (!is_array($this->clipData[$padIdentifier]['el'] ?? false)) { + return []; + } + + $elements = []; + foreach ($this->clipData[$padIdentifier]['el'] as $reference => $value) { + if (!$value) { + continue; + } + [$table, $uid] = explode('|', $reference); + if ($table !== '_FILE') { + if ((!$matchTable || $table === $matchTable) && $this->tcaSchemaFactory->has($table)) { + $elements[$reference] = $padIdentifier === 'normal' ? $value : $uid; + } + } elseif ($table === $matchTable) { + $elements[$reference] = $value; + } + } + return $elements; + } + + /** + * Verifies if the item $table/$uid is on the current pad. + * If the pad is "normal" and the element exists, the mode value is returned. + * Thus you'll know if the item was copied or cut. + * + * @param string $table Table name, (_FILE for files...) + * @param string|int $identifier Either the records' uid or a filepath + * @return string If selected the current mode is returned, otherwise an empty string + */ + public function isSelected(string $table, $identifier): string + { + $key = $table . '|' . $identifier; + $mode = $this->current === 'normal' ? $this->currentMode() : 'any'; + return !empty($this->clipData[$this->current]['el'][$key]) ? $mode : ''; + } + + /** + * Returns the first element on the current clipboard + * Makes sense only for DB records - not files! + * + * @return array Element record with extra field _RECORD_TITLE set to the title of the record + */ + public function getSelectedRecord(): array + { + $elements = $this->elFromTable(); + reset($elements); + [$table, $uid] = explode('|', (string)key($elements)); + if (!$this->isSelected($table, (int)$uid)) { + return []; + } + $selectedRecord = BackendUtility::getRecordWSOL($table, (int)$uid); + $selectedRecord['_RECORD_TITLE'] = BackendUtility::getRecordTitle($table, $selectedRecord); + return $selectedRecord; + } + + /** + * Reports if the current pad has elements (does not check file/DB type OR if file/DBrecord exists or not. Only counting array) + * + * @return bool TRUE if elements exist. + */ + protected function isElements(): bool + { + return is_array($this->clipData[$this->current]['el'] ?? null) && !empty($this->clipData[$this->current]['el']); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + /** + * Builds a URL to the current module with the received + * parameters, merged / replaced by additional parameters. + */ + protected function buildUrl(array $parameters = []): string + { + if ($this->request === null) { + throw new \RuntimeException( + 'Request object must be set to generate clipboard URL\'s', + 1633604720 + ); + } + return (string)$this->uriBuilder->buildUriFromRequest( + $this->request, + array_replace($this->request->getQueryParams(), $parameters) + ); + } +} diff --git a/Classes/Clipboard/Type/CountMode.php b/Classes/Clipboard/Type/CountMode.php new file mode 100644 index 0000000..0d10295 --- /dev/null +++ b/Classes/Clipboard/Type/CountMode.php @@ -0,0 +1,29 @@ +identifier = $identifier; + $this->module = $module; + $this->keymap = $keymap; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getModule(): ?JavaScriptModuleInstruction + { + return $this->module; + } + + public function getKeymap(): ?JavaScriptModuleInstruction + { + return $this->keymap; + } + + public function setOptions(array $options): Addon + { + $this->options = $options; + + return $this; + } + + public function getOptions(): array + { + return $this->options; + } + + public function setCssFiles(array $cssFiles): Addon + { + $this->cssFiles = $cssFiles; + + return $this; + } + + public function getCssFiles(): array + { + return $this->cssFiles; + } +} diff --git a/Classes/CodeEditor/CodeEditor.php b/Classes/CodeEditor/CodeEditor.php new file mode 100644 index 0000000..aa2486d --- /dev/null +++ b/Classes/CodeEditor/CodeEditor.php @@ -0,0 +1,146 @@ +buildConfiguration(); + + if (isset($configuration['modes'])) { + $modeRegistry = GeneralUtility::makeInstance(ModeRegistry::class); + foreach ($configuration['modes'] as $formatCode => $mode) { + $modeInstance = GeneralUtility::makeInstance(Mode::class, $mode['module'])->setFormatCode($formatCode); + + if (!empty($mode['extensions']) && is_array($mode['extensions'])) { + $modeInstance->bindToFileExtensions($mode['extensions']); + } + + if (isset($mode['default']) && $mode['default'] === true) { + $modeInstance->setAsDefault(); + } + + $modeRegistry->register($modeInstance); + } + } + + $addonRegistry = GeneralUtility::makeInstance(AddonRegistry::class); + if (isset($configuration['addons'])) { + foreach ($configuration['addons'] as $identifier => $addon) { + $addonInstance = GeneralUtility::makeInstance(Addon::class, $identifier, $addon['module'] ?? null, $addon['keymap'] ?? null); + + if (!empty($addon['cssFiles']) && is_array($addon['cssFiles'])) { + $addonInstance->setCssFiles($addon['cssFiles']); + } + + if (!empty($addon['options']) && is_array($addon['options'])) { + $addonInstance->setOptions($addon['options']); + } + + $addonRegistry->register($addonInstance); + } + } + } + + /** + * Compiles the configuration for code editor. Configuration is stored in caching framework. + * + * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException + * @throws \InvalidArgumentException + */ + protected function buildConfiguration(): array + { + if ($this->configuration !== null) { + return $this->configuration; + } + + $this->configuration = [ + 'modes' => [], + 'addons' => [], + ]; + + $cache = $this->getCache(); + $packageManager = GeneralUtility::makeInstance(PackageManager::class); + $cacheIdentifier = $this->generateCacheIdentifier($packageManager); + $configurationFromCache = $cache->get($cacheIdentifier); + if ($configurationFromCache !== false) { + $this->configuration = $configurationFromCache; + } else { + $packages = $packageManager->getActivePackages(); + + foreach ($packages as $package) { + $configurationPath = $package->getPackagePath() . 'Configuration/Backend/T3editor'; + $modesFileNameForPackage = $configurationPath . '/Modes.php'; + if (is_file($modesFileNameForPackage)) { + $definedModes = require $modesFileNameForPackage; + if (is_array($definedModes)) { + $this->configuration['modes'] = array_merge($this->configuration['modes'], $definedModes); + } + } + + $addonsFileNameForPackage = $configurationPath . '/Addons.php'; + if (is_file($addonsFileNameForPackage)) { + $definedAddons = require $addonsFileNameForPackage; + if (is_array($definedAddons)) { + $this->configuration['addons'] = array_merge($this->configuration['addons'], $definedAddons); + } + } + } + $cache->set($cacheIdentifier, $this->configuration); + } + + return $this->configuration; + } + + protected function generateCacheIdentifier(PackageManager $packageManager): string + { + return (new PackageDependentCacheIdentifier($packageManager))->withPrefix('T3editorConfiguration')->toString(); + } + + /** + * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + * @throws \InvalidArgumentException + */ + protected function getCache(): FrontendInterface + { + return GeneralUtility::makeInstance(CacheManager::class)->getCache('assets'); + } +} diff --git a/Classes/CodeEditor/Exception/InvalidModeException.php b/Classes/CodeEditor/Exception/InvalidModeException.php new file mode 100644 index 0000000..9435a78 --- /dev/null +++ b/Classes/CodeEditor/Exception/InvalidModeException.php @@ -0,0 +1,26 @@ +module = $module; + } + + public function getModule(): JavaScriptModuleInstruction + { + return $this->module; + } + + public function getFormatCode(): string + { + return $this->formatCode; + } + + public function setFormatCode(string $formatCode): Mode + { + $this->formatCode = $formatCode; + + return $this; + } + + public function bindToFileExtensions(array $fileExtensions): Mode + { + $this->fileExtensions = $fileExtensions; + + return $this; + } + + public function getBoundFileExtensions(): array + { + return $this->fileExtensions; + } + + public function setAsDefault(): Mode + { + $this->isDefault = true; + + return $this; + } + + public function isDefault(): bool + { + return $this->isDefault; + } +} diff --git a/Classes/CodeEditor/Registry/AddonRegistry.php b/Classes/CodeEditor/Registry/AddonRegistry.php new file mode 100644 index 0000000..f4f8972 --- /dev/null +++ b/Classes/CodeEditor/Registry/AddonRegistry.php @@ -0,0 +1,61 @@ +registeredAddons[] = $addon; + + return $this; + } + + public function getAddons(): array + { + return $this->registeredAddons; + } + + /** + * @param Addon[] $addons + */ + public function compileSettings(array $addons): array + { + $settings = []; + foreach ($addons as $addon) { + $settings = array_merge($settings, $addon->getOptions()); + } + + return $settings; + } +} diff --git a/Classes/CodeEditor/Registry/ModeRegistry.php b/Classes/CodeEditor/Registry/ModeRegistry.php new file mode 100644 index 0000000..0366fea --- /dev/null +++ b/Classes/CodeEditor/Registry/ModeRegistry.php @@ -0,0 +1,99 @@ +registeredModes[$mode->getFormatCode()] = $mode; + if ($mode->isDefault()) { + $this->defaultMode = $mode; + } + + return $this; + } + + /** + * Removes registered modes + */ + public function unregister(string $formatCode): ModeRegistry + { + if (isset($this->registeredModes[$formatCode])) { + unset($this->registeredModes[$formatCode]); + } + + return $this; + } + + public function isRegistered(string $formatCode): bool + { + return isset($this->registeredModes[$formatCode]); + } + + /** + * @throws InvalidModeException + */ + public function getByFormatCode(string $formatCode): Mode + { + foreach ($this->registeredModes as $mode) { + if ($mode->getFormatCode() === $formatCode) { + return $mode; + } + } + + throw new InvalidModeException('Tried to get unregistered code editor mode by format code "' . $formatCode . '"', 1499710203); + } + + /** + * @throws InvalidModeException + */ + public function getByFileExtension(string $fileExtension): Mode + { + foreach ($this->registeredModes as $mode) { + if (in_array($fileExtension, $mode->getBoundFileExtensions(), true)) { + return $mode; + } + } + + throw new InvalidModeException('Cannot find a registered mode for requested file extension "' . $fileExtension . '"', 1500306488); + } + + public function getDefaultMode(): Mode + { + return $this->defaultMode; + } +} diff --git a/Classes/Command/CreateBackendUserCommand.php b/Classes/Command/CreateBackendUserCommand.php new file mode 100644 index 0000000..7cfac79 --- /dev/null +++ b/Classes/Command/CreateBackendUserCommand.php @@ -0,0 +1,417 @@ +addOption( + 'username', + 'u', + InputOption::VALUE_REQUIRED, + 'The username of the backend user', + )->addOption( + 'password', + 'p', + InputOption::VALUE_REQUIRED, + 'The password of the backend user. See security note below.', + )->addOption( + 'email', + 'e', + InputOption::VALUE_REQUIRED, + 'The email address of the backend user', + '', + ) + ->addOption( + 'groups', + 'g', + InputOption::VALUE_REQUIRED, + 'Assign given groups to the user' + ) + ->addOption( + 'language', + 'l', + InputOption::VALUE_REQUIRED, + 'The language for the user interface' + ) + ->addOption( + 'admin', + 'a', + InputOption::VALUE_NONE, + 'Create user with admin privileges' + )->addOption( + 'maintainer', + 'm', + InputOption::VALUE_NONE, + 'Create user with maintainer privileges', + )->setHelp( + <<Create a backend user using environment variables + +Example: +------------------------------------------------- +TYPO3_BE_USER_NAME=username \ +TYPO3_BE_USER_EMAIL=admin@example.com \ +TYPO3_BE_USER_GROUPS= \ +TYPO3_BE_USER_LANGUAGE=de \ +TYPO3_BE_USER_ADMIN=0 \ +TYPO3_BE_USER_MAINTAINER=0 \ +./bin/typo3 backend:user:create --no-interaction +------------------------------------------------- + +Variable "TYPO3_BE_USER_PASSWORD" and options "-p" or "--password" can be +used to provide a password. Using this can be a security risk since the password +may end up in shell history files. Prefer the interactive mode. Additionally, +writing a command to shell history can be suppressed by prefixing the command +with a space when using `bash` or `zsh`. + +EOT + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $input->setInteractive(!$input->getOption('no-interaction')); + + /** @var QuestionHelper $questionHelper */ + $questionHelper = $this->getHelper('question'); + $username = $this->getUsername($questionHelper, $input, $output); + $password = $this->getPassword($questionHelper, $input, $output); + $email = $this->getEmail($questionHelper, $input, $output) ?: ''; + $maintainer = $this->getMaintainer($questionHelper, $input, $output); + $language = $this->getLanguage($questionHelper, $input, $output) ?: 'en'; + + // If the user is 'maintainer' it is also required to set the 'admin' flag. + if ($maintainer) { + $admin = true; + } else { + $admin = $this->getAdmin($questionHelper, $input, $output); + } + + // If 'admin' flag was set, this prompt is skipped. + // Because this user does already have access to the entire system. + if ($admin) { + $groups = []; + } else { + $groups = $this->getGroups($questionHelper, $input, $output); + } + + $this->createUser($username, $password, $email, $admin, $maintainer, $groups, $language); + + return Command::SUCCESS; + } + + private function getUsername(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): string + { + // Taking deleted users into account as we want the username to be unique. + // So in case a user was deleted and will be restored, this could cause duplicated usernames. + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_users'); + $queryBuilder->getRestrictions()->removeAll(); + $usernames = $queryBuilder + ->select('username') + ->from('be_users') + ->executeQuery() + ->fetchFirstColumn(); + + $usernameValidator = static function ($username) use ($usernames) { + if (empty($username)) { + throw new \RuntimeException( + 'Backend username must not be empty.', + 1669822315, + ); + } + + if (in_array($username, $usernames, true)) { + throw new \RuntimeException( + 'The username "' . $username . '" is already taken. Please use another username.', + 1670797516, + ); + } + + return $username; + }; + + $usernameFromCli = $this->getFallbackValueEnvOrOption($input, 'username', 'TYPO3_BE_USER_NAME'); + if ($usernameFromCli === false && $input->isInteractive()) { + $questionUsername = new Question('Enter the backend username of the new account: '); + $questionUsername->setValidator($usernameValidator); + + return $questionHelper->ask($input, $output, $questionUsername); + } + + return $usernameValidator($usernameFromCli); + } + + private function getPassword(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): string + { + $passwordValidator = function ($password) { + $passwordValidationErrors = $this->getBackendUserPasswordValidationErrors((string)$password); + if (!empty($passwordValidationErrors)) { + throw new \RuntimeException( + 'The given password is not secure enough!' . PHP_EOL + . ' * ' . implode(PHP_EOL . ' * ', $passwordValidationErrors), + 1670267532, + ); + } + + return $password; + }; + + $passwordFromCli = $this->getFallbackValueEnvOrOption($input, 'password', 'TYPO3_BE_USER_PASSWORD'); + + // Force this question if no password set via cli. + // Thus, the user will always be prompted for a password even --no-interaction is set. + $currentlyInteractive = $input->isInteractive(); + $input->setInteractive(true); + if ($passwordFromCli === false) { + $questionPassword = new Question('Enter a password for the backend user: '); + $questionPassword->setHidden(true); + $questionPassword->setHiddenFallback(false); + $questionPassword->setValidator($passwordValidator); + + return $questionHelper->ask($input, $output, $questionPassword); + } + $input->setInteractive($currentlyInteractive); + + return $passwordValidator($passwordFromCli); + } + + private function getEmail(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): string + { + $emailValidator = static function ($email) { + if (!empty($email) && !GeneralUtility::validEmail($email)) { + throw new \RuntimeException( + 'The given email is not valid! Please try again.', + 1669813635, + ); + } + + return $email; + }; + + $emailFromCli = $this->getFallbackValueEnvOrOption($input, 'email', 'TYPO3_BE_USER_EMAIL'); + if ($emailFromCli === false && $input->isInteractive()) { + $questionEmail = new Question('Enter the email for the backend user: ', ''); + $questionEmail->setValidator($emailValidator); + + return $questionHelper->ask($input, $output, $questionEmail); + } + + return (string)$emailValidator($emailFromCli); + } + + private function getGroups(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): array + { + $queryBuilder = $this->connectionPool->getConnectionForTable('be_groups'); + $groupsList = $queryBuilder->select(['uid', 'title'], 'be_groups')->fetchAllAssociative(); + + $groupChoices = []; + foreach ($groupsList as $group) { + $groupChoices[$group['uid']] = $group['title']; + } + + $groupValidator = static function ($groupList) use ($groupChoices) { + $groups = GeneralUtility::intExplode(',', $groupList ?: ''); + foreach ($groups as $group) { + if (!empty($group) && !isset($groupChoices[$group])) { + throw new \RuntimeException( + 'The given group uid "' . $group . '" does not exist.', + 1670812929, + ); + } + } + + return $groups; + }; + + $groupsFromCli = $this->getFallbackValueEnvOrOption($input, 'groups', 'TYPO3_BE_USER_GROUPS'); + if ($groupsFromCli === false && $input->isInteractive()) { + if (empty($groupChoices)) { + return []; + } + + $questionGroups = new ChoiceQuestion('Select groups the newly created backend user should be assigned to (use comma-separated list for multiple groups): ', $groupChoices); + $questionGroups->setMultiselect(true); + $questionGroups->setValidator($groupValidator); + // Ensure keys are selected and not the values + $questionGroups->setAutocompleterValues(array_keys($groupChoices)); + return $questionHelper->ask($input, $output, $questionGroups); + } + + return $groupValidator($groupsFromCli); + } + + private function getMaintainer(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): bool + { + $maintainerFromCli = $this->getFallbackValueEnvOrOption($input, 'maintainer', 'TYPO3_BE_USER_MAINTAINER'); + if ($maintainerFromCli === false && $input->isInteractive()) { + $questionMaintainer = new ConfirmationQuestion('Create user with maintainer privileges [y/n default: n] ? ', false); + return (bool)$questionHelper->ask($input, $output, $questionMaintainer); + } + + return (bool)$maintainerFromCli; + } + + private function getAdmin(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): bool + { + $adminFromCli = $this->getFallbackValueEnvOrOption($input, 'admin', 'TYPO3_BE_USER_ADMIN'); + if ($adminFromCli === false && $input->isInteractive()) { + $questionAdmin = new ConfirmationQuestion('Create user with admin privileges [y/n default: n] ? ', false); + return (bool)$questionHelper->ask($input, $output, $questionAdmin); + } + + return (bool)$adminFromCli; + } + + private function getLanguage(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): string + { + $languagesList = $this->locales->getLanguages(); + + $languageValidator = static function ($language) use ($languagesList) { + if (!empty($language) && !isset($languagesList[$language])) { + throw new \RuntimeException( + 'The given language "' . $language . '" is not supported.', + 1769429507 + ); + } + + return $language; + }; + + $languageFromCli = $this->getFallbackValueEnvOrOption($input, 'language', 'TYPO3_BE_USER_LANGUAGE'); + if ($languageFromCli === false && $input->isInteractive()) { + $questionLanguage = new Question('Enter the language for the user interface [eg: de/fr/it/...]: ', ''); + $questionLanguage->setValidator($languageValidator); + $questionLanguage->setAutocompleterValues(array_keys($languagesList)); + + return $questionHelper->ask($input, $output, $questionLanguage); + } + + return (string)$languageValidator($languageFromCli); + } + + /** + * Get a value from + * 1. environment variable + * 2. cli option + */ + private function getFallbackValueEnvOrOption(InputInterface $input, string $option, string $envVar): string|bool + { + $optionShortcut = $this->getDefinition()->getOption($option)->getShortcut(); + $parameterOptions = ['--' . $option]; + if ($optionShortcut !== null) { + $parameterOptions[] = '-' . $optionShortcut; + } + return $input->hasParameterOption($parameterOptions) ? $input->getOption($option) : getenv($envVar); + } + + private function getBackendUserPasswordValidationErrors(string $password): array + { + $GLOBALS['LANG'] = $this->languageServiceFactory->create('en'); + $passwordPolicy = $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] ?? 'default'; + $passwordPolicyValidator = new PasswordPolicyValidator( + PasswordPolicyAction::NEW_USER_PASSWORD, + is_string($passwordPolicy) ? $passwordPolicy : '' + ); + $contextData = new ContextData(); + $passwordPolicyValidator->isValidPassword($password, $contextData); + + return $passwordPolicyValidator->getValidationErrors(); + } + + /** + * Create a backend user. + * similar to "\TYPO3\CMS\Install\Service\SetupService::createUser()", + * but accepts admin/maintainer flag and groups + */ + private function createUser(string $username, string $password, string $email = '', bool $admin = false, bool $maintainer = false, array $groups = [], string $language = 'en'): void + { + // Initialize backend user authentication to ensure the new backend user can be created with proper permissions + Bootstrap::initializeBackendAuthentication(); + + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $backendUserId = StringUtility::getUniqueId('NEW'); + $data = [ + 'be_users' => [ + $backendUserId => [ + 'pid' => 0, + 'username' => $username, + 'password' => $password, + 'email' => $email, + 'admin' => $admin ? 1 : 0, + 'usergroup' => $groups, + 'disable' => 0, + 'lang' => $language, + ], + ], + ]; + + $dataHandler->start($data, []); + $dataHandler->process_datamap(); + + $backendUserId = $dataHandler->substNEWwithIDs[$backendUserId] ?? null; + if ($maintainer && $backendUserId) { + $maintainerIds = $this->configurationManager->getConfigurationValueByPath('SYS/systemMaintainers') ?? []; + sort($maintainerIds); + $maintainerIds[] = $backendUserId; + $this->configurationManager->setLocalConfigurationValuesByPathValuePairs([ + 'SYS/systemMaintainers' => array_unique($maintainerIds), + ]); + } + } +} diff --git a/Classes/Command/DebugBackendModulesCommand.php b/Classes/Command/DebugBackendModulesCommand.php new file mode 100644 index 0000000..dcd5f8f --- /dev/null +++ b/Classes/Command/DebugBackendModulesCommand.php @@ -0,0 +1,214 @@ +languageService = $GLOBALS['LANG'] = $this->languageServiceFactory->create('en'); + // Note: We cannot directly use autowire of 'backend.modules' because that + // would only give us the final constructed registry, without access to data + // like "packageName" and "labels". + // @todo We should expose this data in our registry. + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addOption( + 'csv-export', + 'x', + InputOption::VALUE_NONE, + 'Dump data as CSV (instead of CLI table)' + ) + ->addOption( + 'core-only', + 'c', + InputOption::VALUE_NONE, + 'Only show core extensions' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $degraded = false; + try { + $container = $this->bootService->getContainer(); + } catch (\Throwable $e) { + $container = $this->failsafeContainer; + $degraded = true; + } + + $coreOnly = $input->getOption('core-only'); + + $title = 'Backend Modules'; + if ($coreOnly) { + $title .= ' - Core'; + } + if ($degraded) { + $title .= ' (failsafe)'; + } + + // We need this low-level access because 'packageName' and 'labels' cannot be retrieved by the Module Registry API (yet) + $modulesFromPackages = $container->get('backend.modules')->getArrayCopy(); + $modulesFromPackages = $this->moduleFactory->adaptAliasMappingFromModuleConfiguration($modulesFromPackages); + + $initializedModulesFromPackages = []; + foreach ($modulesFromPackages as $identifier => $configuration) { + if (!$coreOnly || str_starts_with($configuration['packageName'], 'typo3/cms-')) { + $initializedModulesFromPackages[$identifier] = $this->moduleFactory->createModule($identifier, $configuration); + } + } + + $registry = GeneralUtility::makeInstance(ModuleRegistry::class, $initializedModulesFromPackages); + $modules = $registry->getModules(); + + $linearTree = []; + $headers = [ + 'Pkg', + 'Main level', + 'Second level', + 'Third level', + 'Position', + 'Labels', + 'Path', + ]; + $this->walkTree($modules, $linearTree, $modulesFromPackages); + + if ($input->getOption('csv-export')) { + $out = fopen('php://output', 'w'); + $separator = ';'; + $enclosure = '"'; + $escape = '\\'; + $eol = PHP_EOL; + + fputcsv($out, $headers, $separator, $enclosure, $escape, $eol); + foreach ($linearTree as $data) { + if ($data instanceof TableSeparator) { + $blankOutput = []; + foreach ($headers as $ignored) { + $blankOutput[] = ''; + } + fputcsv($out, $blankOutput, $separator, $enclosure, $escape, $eol); + } else { + fputcsv($out, $data, $separator, $enclosure, $escape, $eol); + } + } + fclose($out); + } else { + $io->title($title); + $table = new Table($output); + $table->setHeaders($headers); + + foreach ($linearTree as $data) { + $table->addRow($data); + } + $table->render(); + } + + return Command::SUCCESS; + } + + /** + * @param ModuleInterface[] $modules + */ + private function walkTree(array $modules, array &$linearTree, array $modulesFromPackages, int $level = 1, array $parentStack = []): void + { + foreach ($modules as $module) { + // Main menus have no "parent". We only iterate these elements on the first level. + if ($level === 1 && $module->getParentIdentifier() !== '') { + continue; + } + + $outputStack = $parentStack; + $outputStack[] = $module->getIdentifier(); + + $linearTree[] = [ + $modulesFromPackages[$module->getIdentifier()]['packageName'], + + $outputStack[0] ?? '', + $outputStack[1] ?? '', + $outputStack[2] ?? '', + + ($module->getPosition() !== [] ? json_encode($module->getPosition()) : ''), + $this->languageService->sL($module->getTitle()) . ' [' . $this->parseLabels($modulesFromPackages[$module->getIdentifier()]['labels']) . ']', + $module->getPath(), + ]; + + // Next level + if ($module->hasSubModules()) { + $this->walkTree($module->getSubModules(), $linearTree, $modulesFromPackages, $level + 1, [...$parentStack, $module->getIdentifier()]); + } + + if ($level === 1) { + $linearTree[] = new TableSeparator(); + } + } + + if ($level === 1) { + // Remove last separator + array_pop($linearTree); + } + } + + private function parseLabels(array|string $labels): string + { + if (is_string($labels)) { + return $labels; + } + + $out = "\n"; + $out .= ' title: ' . ($labels['title'] ?? '-') . "\n"; + $out .= ' shortDescription: ' . ($labels['shortDescription'] ?? '-') . "\n"; + $out .= ' description: ' . ($labels['description'] ?? '-') . "\n"; + return $out; + } +} diff --git a/Classes/Command/DebugBackendRoutesCommand.php b/Classes/Command/DebugBackendRoutesCommand.php new file mode 100644 index 0000000..487d203 --- /dev/null +++ b/Classes/Command/DebugBackendRoutesCommand.php @@ -0,0 +1,238 @@ +addOption( + 'json', + null, + InputOption::VALUE_NONE, + 'Output routes in JSON format' + ) + ->addOption( + 'filter', + 'f', + InputOption::VALUE_REQUIRED, + 'Filter routes by name (supports partial matching)' + ) + ->addOption( + 'limit', + 'l', + InputOption::VALUE_REQUIRED, + 'Limit routes by type: ajax, module, or route' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $jsonOutput = $input->getOption('json'); + $filter = $input->getOption('filter'); + $limit = $input->getOption('limit'); + + // Validate limit option + if ($limit !== null) { + $limit = strtolower($limit); + if (!in_array($limit, ['ajax', 'module', 'route'], true)) { + $io->error('Invalid limit type. Valid options are: ajax, module, route'); + return Command::FAILURE; + } + } + + // Collect all routes + $routes = $this->collectAllRoutes($filter, $limit); + + if (empty($routes)) { + $messages = []; + if ($filter) { + $messages[] = 'filter: ' . $filter; + } + if ($limit) { + $messages[] = 'type: ' . $limit; + } + if ($messages) { + $io->warning('No routes found matching ' . implode(', ', $messages)); + } else { + $io->warning('No routes found.'); + } + return Command::SUCCESS; + } + + // Sort routes by name + ksort($routes); + + if ($jsonOutput) { + $this->outputJson($routes, $output); + } else { + $this->outputTable($routes, $io, $output); + } + + return Command::SUCCESS; + } + + /** + * Collect all routes from Router (including AJAX routes) and Module routes + * + * @return array + */ + private function collectAllRoutes(?string $filter, ?string $limitType): array + { + $routes = []; + + // Build a set of module identifiers for quick lookup + $moduleIdentifiers = []; + foreach ($this->moduleRegistry->getModules() as $module) { + if ($module->hasParentModule() || $module->isStandalone()) { + $moduleIdentifiers[$module->getIdentifier()] = true; + } + } + + // Get all routes from Router (includes regular routes, AJAX routes, and module routes) + // Note: Routes can be either TYPO3 Route or Symfony Route objects + foreach ($this->router->getRoutes() as $routeName => $route) { + if ($filter && !str_contains((string)$routeName, $filter)) { + continue; + } + + // Determine route type + $type = 'Route'; + if (str_starts_with((string)$routeName, 'ajax_')) { + $type = 'Ajax'; + } elseif (isset($moduleIdentifiers[(string)$routeName])) { + $type = 'Module'; + } + + // Apply limit filter if specified + if ($limitType !== null) { + $typeNormalized = strtolower($type); + if ($typeNormalized !== $limitType) { + continue; + } + } + + // Get methods - works for both Symfony and TYPO3 Route objects + $methods = $route->getMethods(); + $methodString = empty($methods) ? 'ANY' : implode('|', $methods); + + // Get options - works for both route types + $options = method_exists($route, 'getOptions') ? $route->getOptions() : []; + $target = $options['target'] ?? $options['_controller'] ?? '-'; + + $routes[$routeName] = [ + 'name' => (string)$routeName, + 'method' => $methodString, + 'path' => $route->getPath(), + 'target' => $target, + 'type' => $type, + 'options' => $options, + ]; + } + + return $routes; + } + + /** + * Output routes as JSON + */ + private function outputJson(array $routes, OutputInterface $output): void + { + $jsonData = []; + foreach ($routes as $route) { + $jsonData[] = [ + 'name' => $route['name'], + 'method' => $route['method'], + 'path' => $route['path'], + 'target' => $route['target'], + 'type' => $route['type'], + 'options' => $route['options'], + ]; + } + + $output->writeln((string)json_encode($jsonData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } + + /** + * Output routes as formatted table (similar to Symfony's debug:router) + */ + private function outputTable(array $routes, SymfonyStyle $io, OutputInterface $output): void + { + $table = new Table($output); + $table->setHeaders(['Name', 'Method', 'Path', 'Target', 'Type']); + $rows = []; + foreach ($routes as $route) { + $rows[] = [ + $route['name'], + $route['method'], + $route['path'], + $this->formatTarget($route['target']), + $route['type'], + ]; + } + + $table->setRows($rows); + $table->render(); + + $io->newLine(); + $io->writeln(sprintf('%d routes found', count($routes))); + } + + /** + * Format the target for display (shorten class names) + */ + private function formatTarget(string $target): string + { + // Shorten TYPO3 class names for better readability + $target = str_replace('TYPO3\\CMS\\', '', $target); + + // Limit length if too long + if (strlen($target) > 80) { + return substr($target, 0, 77) . '...'; + } + + return $target; + } +} diff --git a/Classes/Command/LockBackendCommand.php b/Classes/Command/LockBackendCommand.php new file mode 100644 index 0000000..ff713d7 --- /dev/null +++ b/Classes/Command/LockBackendCommand.php @@ -0,0 +1,77 @@ +addArgument( + 'redirect', + InputArgument::OPTIONAL, + 'If set, a locked TYPO3 Backend will redirect to URI specified with this argument. The URI is saved as a string in the lockfile that is specified in the system configuration.', + '' + ); + } + + /** + * Executes the command for adding the lock file + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->title($this->getDescription()); + if ($this->lockService->isLocked()) { + $io->note('A lock file already exists. Overwriting it.'); + } + $lockFile = $this->lockService->getAbsolutePathToLockFile(); + $redirectUriFromLockFileContent = $input->getArgument('redirect'); + if ($redirectUriFromLockFileContent) { + $redirectUriFromLockFileContent = is_string($redirectUriFromLockFileContent) ? $redirectUriFromLockFileContent : ''; + } + if (!$this->lockService->lockBackend($redirectUriFromLockFileContent)) { + $io->error('Failed to create lock file "' . $lockFile . '".'); + return Command::FAILURE; + } + $message = 'Wrote lock file to "' . $lockFile . '"'; + if ($redirectUriFromLockFileContent !== '') { + $message .= LF . 'with target URI "' . $redirectUriFromLockFileContent . '".'; + } + $io->success($message); + return Command::SUCCESS; + } +} diff --git a/Classes/Command/ProgressListener/ReferenceIndexProgressListener.php b/Classes/Command/ProgressListener/ReferenceIndexProgressListener.php new file mode 100644 index 0000000..70c6edf --- /dev/null +++ b/Classes/Command/ProgressListener/ReferenceIndexProgressListener.php @@ -0,0 +1,116 @@ +io = $io; + $this->isEnabled = $io->isQuiet() === false; + } + + public function start(int $maxSteps = 0, ?string $additionalMessage = null): void + { + if (!$this->isEnabled) { + return; + } + $tableName = $additionalMessage; + if ($maxSteps > 0) { + $this->io->section('Update index of table ' . $tableName); + $this->progressBar = $this->io->createProgressBar($maxSteps); + $this->progressBar->start($maxSteps); + } else { + $this->io->section('Nothing to update for empty table ' . $tableName); + $this->progressBar = null; + } + } + + public function advance(int $step = 1, ?string $additionalMessage = null): void + { + if (!$this->isEnabled) { + return; + } + if ($additionalMessage) { + $this->showMessageWhileInProgress(function () use ($additionalMessage) { + $this->io->writeln($additionalMessage); + }); + } + if ($this->progressBar !== null) { + $this->progressBar->advance($step); + } + } + + public function finish(?string $additionalMessage = null): void + { + if (!$this->isEnabled) { + return; + } + if ($this->progressBar !== null) { + $this->progressBar->finish(); + $this->progressBar = null; + $this->io->writeln(PHP_EOL); + } + if ($additionalMessage) { + $this->io->writeln($additionalMessage); + } + } + + public function log(string $message, string $logLevel = LogLevel::INFO): void + { + if (!$this->isEnabled) { + return; + } + $this->showMessageWhileInProgress(function () use ($message, $logLevel) { + switch ($logLevel) { + case LogLevel::ERROR: + $this->io->error($message); + break; + case LogLevel::WARNING: + $this->io->warning($message); + break; + default: + $this->io->writeln($message); + } + }); + } + + protected function showMessageWhileInProgress(callable $messageFunction): void + { + if ($this->progressBar !== null) { + $this->progressBar->clear(); + $messageFunction(); + $this->progressBar->display(); + } else { + $messageFunction(); + } + } +} diff --git a/Classes/Command/ReferenceIndexUpdateCommand.php b/Classes/Command/ReferenceIndexUpdateCommand.php new file mode 100644 index 0000000..017df72 --- /dev/null +++ b/Classes/Command/ReferenceIndexUpdateCommand.php @@ -0,0 +1,69 @@ +addOption( + 'check', + 'c', + InputOption::VALUE_NONE, + 'Only check the reference index of TYPO3' + ); + } + + /** + * Executes the command for adding or removing the lock file + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + Bootstrap::initializeBackendAuthentication(); + $io = new SymfonyStyle($input, $output); + + $isTestOnly = (bool)$input->getOption('check'); + + $progressListener = GeneralUtility::makeInstance(ReferenceIndexProgressListener::class); + $progressListener->initialize($io); + $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class); + if ($isTestOnly) { + $io->section('Reference Index being TESTED (nothing written, remove the "--check" argument)'); + } else { + $io->section('Reference Index is now being updated'); + } + $referenceIndex->updateIndex($isTestOnly, $progressListener); + return Command::SUCCESS; + } +} diff --git a/Classes/Command/ResetPasswordCommand.php b/Classes/Command/ResetPasswordCommand.php new file mode 100644 index 0000000..ef0d8a4 --- /dev/null +++ b/Classes/Command/ResetPasswordCommand.php @@ -0,0 +1,152 @@ +addArgument( + 'backendurl', + InputArgument::REQUIRED, + 'The URL of the TYPO3 Backend, e.g. https://www.example.com/typo3/' + )->addArgument( + 'email', + InputArgument::REQUIRED, + 'The email address of a valid backend user' + ); + } + /** + * Executes the command for sending out an email to reset the password. + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $email = $input->getArgument('email'); + $email = is_string($email) ? $email : ''; + if (!GeneralUtility::validEmail($email)) { + $io->error('The given email "' . $email . '" is not a valid email address.'); + return Command::FAILURE; + } + $backendUrl = $input->getArgument('backendurl'); + $backendUrl = is_string($backendUrl) ? $backendUrl : ''; + if (!GeneralUtility::isValidUrl($backendUrl)) { + $io->error('The given backend URL "' . $backendUrl . '" is not a valid URL.'); + return Command::FAILURE; + } + $request = $this->createFakeWebRequest($backendUrl); + $GLOBALS['TYPO3_REQUEST'] = $request; + $this->passwordReset->initiateReset($request, $this->context, $email); + $io->success('Password reset for email address "' . $email . '" initiated.'); + return Command::SUCCESS; + } + + /** + * This is needed to create a link to the backend properly. + */ + protected function createFakeWebRequest(string $backendUrl): ServerRequestInterface + { + $uri = new Uri($backendUrl); + $request = new ServerRequest( + $uri, + 'GET', + 'php://input', + [], + [ + 'HTTP_HOST' => $uri->getHost(), + 'SERVER_NAME' => $uri->getHost(), + 'HTTPS' => $uri->getScheme() === 'https', + 'SCRIPT_FILENAME' => __FILE__, + 'SCRIPT_NAME' => rtrim($uri->getPath(), '/') . '/', + ] + ); + $backedUpEnvironment = $this->simulateEnvironmentForBackendEntryPoint(); + $normalizedParams = NormalizedParams::createFromRequest($request); + + // Restore the environment + Environment::initialize( + Environment::getContext(), + Environment::isCli(), + Environment::isComposerMode(), + Environment::getProjectPath(), + Environment::getPublicPath(), + Environment::getVarPath(), + Environment::getConfigPath(), + $backedUpEnvironment['currentScript'], + Environment::isWindows() ? 'WINDOWS' : 'UNIX' + ); + + return $request + ->withAttribute('normalizedParams', $normalizedParams) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + } + + /** + * This is a workaround to use "PublicPath . /typo3/index.php" instead of "publicPath . /typo3/sysext/core/bin/typo3" + * so the web root is detected properly in normalizedParams. + */ + protected function simulateEnvironmentForBackendEntryPoint(): array + { + $currentEnvironment = Environment::toArray(); + Environment::initialize( + Environment::getContext(), + Environment::isCli(), + Environment::isComposerMode(), + Environment::getProjectPath(), + Environment::getPublicPath(), + Environment::getVarPath(), + Environment::getConfigPath(), + // This is ugly, as this change fakes the directory + dirname(Environment::getCurrentScript(), 4) . DIRECTORY_SEPARATOR . 'index.php', + Environment::isWindows() ? 'WINDOWS' : 'UNIX' + ); + return $currentEnvironment; + } +} diff --git a/Classes/Command/UnlockBackendCommand.php b/Classes/Command/UnlockBackendCommand.php new file mode 100644 index 0000000..badfdce --- /dev/null +++ b/Classes/Command/UnlockBackendCommand.php @@ -0,0 +1,56 @@ +title($this->getDescription()); + $lockFile = $this->lockService->getAbsolutePathToLockFile(); + if ($this->lockService->isLocked()) { + $this->lockService->unlock(); + if ($this->lockService->isLocked()) { + $io->caution('Could not remove lock file "' . $lockFile . '"!'); + return Command::FAILURE; + } + $io->success('Removed lock file "' . $lockFile . '".'); + } else { + $io->note('No lock file "' . $lockFile . '" was found.' . LF . 'Hence no lock can be removed.'); + } + return Command::SUCCESS; + } +} diff --git a/Classes/Configuration/BackendUserConfiguration.php b/Classes/Configuration/BackendUserConfiguration.php new file mode 100644 index 0000000..2f6b2e7 --- /dev/null +++ b/Classes/Configuration/BackendUserConfiguration.php @@ -0,0 +1,178 @@ +backendUser = $backendUser ?: $GLOBALS['BE_USER']; + } + + /** + * Returns a specific user setting + * + * @param string $key Identifier, allows also dotted notation for subarrays + * @return mixed Value associated + */ + public function get(string $key) + { + return (str_contains($key, '.')) ? $this->getFromDottedNotation($key) : $this->backendUser->uc[$key]; + } + + /** + * Get all user settings + * + * @return mixed all values, usually a multi-dimensional array + */ + public function getAll() + { + return $this->backendUser->uc; + } + + /** + * Sets user settings by key/value pair + * + * @param mixed $value + */ + public function set(string $key, $value): void + { + if (str_contains($key, '.')) { + $this->setFromDottedNotation($key, $value); + } else { + $this->backendUser->uc[$key] = $value; + } + + $this->backendUser->writeUC(); + } + + /** + * Adds a value to a Comma-separated list + * stored in $key of user settings + * + * @param mixed $value + */ + public function addToList(string $key, $value): void + { + $list = $this->get($key); + + if (!isset($list)) { + $list = $value; + } elseif (!GeneralUtility::inList($list, $value)) { + $list .= ',' . $value; + } + + $this->set($key, $list); + } + + /** + * Removes a value from a Comma-separated list + * stored in $key of user settings + * + * @param mixed $value + */ + public function removeFromList(string $key, $value): void + { + $list = $this->get($key); + + if (GeneralUtility::inList($list, $value)) { + $list = GeneralUtility::trimExplode(',', $list, true); + $list = ArrayUtility::removeArrayEntryByValue($list, $value); + $this->set($key, implode(',', $list)); + } + } + + /** + * Resets the user settings to the default + */ + public function clear(): void + { + $this->backendUser->resetUC(); + } + + /** + * Unsets a key in user settings + */ + public function unsetOption(string $key): void + { + if (isset($this->backendUser->uc[$key])) { + unset($this->backendUser->uc[$key]); + $this->backendUser->writeUC(); + } + } + + /** + * Computes the subarray from dotted notation + * + * @param string $key Dotted notation of subkeys like moduleData.module1.general.checked + * @return mixed value of the settings + */ + protected function getFromDottedNotation(string $key) + { + $subkeys = GeneralUtility::trimExplode('.', $key); + $configuration = $this->backendUser->uc; + + foreach ($subkeys as $subkey) { + if (isset($configuration[$subkey])) { + $configuration = &$configuration[$subkey]; + } else { + $configuration = []; + break; + } + } + + return $configuration; + } + + /** + * Sets the value of a key written in dotted notation + * + * @param mixed $value + */ + protected function setFromDottedNotation(string $key, $value): void + { + $subkeys = GeneralUtility::trimExplode('.', $key, true); + $lastKey = $subkeys[count($subkeys) - 1]; + $configuration = &$this->backendUser->uc; + + foreach ($subkeys as $subkey) { + if ($subkey === $lastKey) { + $configuration[$subkey] = $value; + } else { + $configuration = &$configuration[$subkey]; + } + } + } +} diff --git a/Classes/Configuration/SiteTcaConfiguration.php b/Classes/Configuration/SiteTcaConfiguration.php new file mode 100644 index 0000000..f7ffb10 --- /dev/null +++ b/Classes/Configuration/SiteTcaConfiguration.php @@ -0,0 +1,95 @@ +getActivePackages(); + // First load "full table" files from Configuration/SiteConfiguration + $finder = (new Finder())->files()->depth(0)->name('*.php'); + $hasDirectoryEntries = false; + foreach ($activePackages as $package) { + try { + $finder->in($package->getPackagePath() . 'Configuration/SiteConfiguration'); + } catch (\InvalidArgumentException $e) { + // No such directory in this package + continue; + } + $hasDirectoryEntries = true; + } + if ($hasDirectoryEntries) { + foreach ($finder as $fileInfo) { + $GLOBALS['SiteConfiguration'][substr($fileInfo->getBasename(), 0, -4)] = require $fileInfo->getPathname(); + } + } + // Execute override files from Configuration/SiteConfiguration/Overrides + $finder = (new Finder())->files()->depth(0)->name('*.php'); + $hasDirectoryEntries = false; + foreach ($activePackages as $package) { + try { + $finder->in($package->getPackagePath() . 'Configuration/SiteConfiguration/Overrides'); + } catch (\InvalidArgumentException $e) { + // No such directory in this package + continue; + } + $hasDirectoryEntries = true; + } + if ($hasDirectoryEntries) { + foreach ($finder as $fileInfo) { + require $fileInfo->getPathname(); + } + } + $result = $GLOBALS['SiteConfiguration']; + unset($GLOBALS['SiteConfiguration']); + $tcaMigration = GeneralUtility::makeInstance(TcaMigration::class); + $tcaProcessingResult = $tcaMigration->migrate($result); + $messages = $tcaProcessingResult->getMessages(); + if (!empty($messages)) { + $context = 'Automatic TCA migration done during bootstrap of Site TCA Configuration.' + . ' Please adapt TCA accordingly, these migrations will be removed.' + . ' Please adapt these areas:'; + array_unshift($messages, $context); + trigger_error(implode(LF, $messages), E_USER_DEPRECATED); + } + return $tcaProcessingResult->getTca(); + } +} diff --git a/Classes/Configuration/TCA/ItemsProcessorFunctions.php b/Classes/Configuration/TCA/ItemsProcessorFunctions.php new file mode 100644 index 0000000..1fb8ea1 --- /dev/null +++ b/Classes/Configuration/TCA/ItemsProcessorFunctions.php @@ -0,0 +1,164 @@ +getAllSites() as $site) { + foreach ($site->getAllLanguages() as $languageId => $language) { + if (!isset($fieldDefinition['items'][$languageId])) { + $fieldDefinition['items'][$languageId] = [ + 'label' => $language->getTitle(), + 'value' => $languageId, + 'icon' => $language->getFlagIdentifier(), + 'tempTitles' => [], + ]; + } elseif ($fieldDefinition['items'][$languageId]['label'] !== $language->getTitle()) { + // Temporarily store different titles + $fieldDefinition['items'][$languageId]['tempTitles'][] = $language->getTitle(); + } + } + } + + if (!isset($fieldDefinition['items'][0])) { + // Since TcaSiteLanguage has a special behaviour, enforcing the + // default language ("0") to be always added to the site configuration, + // we have to add it to the available items, in case it is not already + // present. This only happens for the first ever created site configuration. + $fieldDefinition['items'][] = ['label' => 'Default', 'value' => 0, 'icon' => '', 'tempTitles' => []]; + } + + ksort($fieldDefinition['items']); + + // Build the final language label + foreach ($fieldDefinition['items'] as &$language) { + $language['label'] .= ' [' . $language['value'] . ']'; + if ($language['tempTitles'] !== []) { + $language['label'] .= ' (' . implode(',', array_unique($language['tempTitles'])) . ')'; + // Unset the temporary title "storage" + unset($language['tempTitles']); + } + } + unset($language); + + // Add PHP_INT_MAX as last - placeholder - value to allow creation of new records + // with the "Create new" button, which is usually not possible in "selector" mode. + // Note: The placeholder will never be displayed in the selector. + $fieldDefinition['items'] = array_values( + array_merge($fieldDefinition['items'], [['label' => 'Placeholder', 'value' => PHP_INT_MAX]]) + ); + } + + /** + * Return language items for use in site_languages.fallbacks + */ + public function populateFallbackLanguages(array &$fieldDefinition): void + { + foreach (GeneralUtility::makeInstance(SiteFinder::class)->getAllSites() as $site) { + foreach ($site->getAllLanguages() as $languageId => $language) { + if (isset($fieldDefinition['row']['languageId'][0]) + && (int)$fieldDefinition['row']['languageId'][0] === $languageId + ) { + // Skip current language id + continue; + } + if (!isset($fieldDefinition['items'][$languageId])) { + $fieldDefinition['items'][$languageId] = [ + 'label' => $language->getTitle(), + 'value' => $languageId, + 'icon' => $language->getFlagIdentifier(), + 'tempTitles' => [], + ]; + } elseif ($fieldDefinition['items'][$languageId]['label'] !== $language->getTitle()) { + // Temporarily store different titles + $fieldDefinition['items'][$languageId]['tempTitles'][] = $language->getTitle(); + } + } + } + ksort($fieldDefinition['items']); + + // Build the final language label + foreach ($fieldDefinition['items'] as &$language) { + if ($language['tempTitles'] !== []) { + $language['label'] .= ' (' . implode(',', array_unique($language['tempTitles'])) . ')'; + // Unset the temporary title "storage" + unset($language['tempTitles']); + } + } + unset($language); + + $fieldDefinition['items'] = array_values($fieldDefinition['items']); + } + + public function populateFlags(array &$fieldConfiguration): void + { + $filter = (new CountryFilter())->setExcludeCountries(['um']); + $countries = GeneralUtility::makeInstance(CountryProvider::class)->getFiltered($filter); + /** @var Country $country */ + foreach ($countries as $country) { + $code = strtolower($country->getAlpha2IsoCode()); + $fieldConfiguration['items'][] = [ + 'label' => $country->getName(), + 'value' => $code, + 'icon' => 'flags-' . $code, + 'group' => 'countries', + ]; + } + // Additional country variants + $variants = ['ca-qc', 'es-ct', 'es-ga', 'gb-eng', 'gb-nir', 'gb-sct', 'gb-wls']; + foreach ($variants as $variant) { + $split = explode('-', $variant); + $base = $countries[strtoupper($split[0])]; + $fieldConfiguration['items'][] = [ + 'label' => sprintf('%s - %s', $base->getName(), strtoupper($split[1])), + 'value' => $variant, + 'icon' => 'flags-' . $variant, + 'group' => 'countries', + ]; + } + + $colors = ['black', 'white', 'blue', 'indigo', 'purple', 'pink', 'orange', 'yellow', 'green', 'teal', 'cyan', 'rainbow']; + foreach ($colors as $color) { + $fieldConfiguration['items'][] = [ + 'label' => $color, + 'value' => $color, + 'icon' => 'flags-' . $color, + 'group' => 'colors', + ]; + } + } +} diff --git a/Classes/Configuration/TCA/UserFunctions.php b/Classes/Configuration/TCA/UserFunctions.php new file mode 100644 index 0000000..56d5233 --- /dev/null +++ b/Classes/Configuration/TCA/UserFunctions.php @@ -0,0 +1,120 @@ +getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site.languages.new') . ']'; + return; + } + + $primaryValue = $record['primary'] ?? null; + $isPrimary = false; + if (is_array($primaryValue)) { + $isPrimary = !empty($primaryValue[0]); + } elseif ($primaryValue !== null) { + $isPrimary = (bool)$primaryValue; + } + if (!$isPrimary) { + $isPrimary = ((int)($record['languageId'][0] ?? -1) === 0); + } + $parameters['title'] = sprintf( + '%s %s [%d] (%s) Base: %s%s', + $record['enabled'] ? '' : '[' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:disabled') . ']', + $record['title'], + $languageId, + $record['locale'], + $record['base'], + $isPrimary ? ' ★' : '' + ); + } + + /** + * Used to build the IRRE title of a site route element + */ + public function getRouteTitle(array &$parameters): void + { + $record = $parameters['row']; + if (($record['type'][0] ?? false) === 'uri') { + $parameters['title'] = sprintf( + '%s %s %s', + $record['route'], + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site.routes.irreHeader.redirectsTo'), + $record['source'] ?: '[' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:undefined') . ']' + ); + } else { + $parameters['title'] = $record['route']; + } + } + + /** + * Used to build the IRRE title of a site error handling element + */ + public function getErrorHandlingTitle(array &$parameters): void + { + $record = $parameters['row']; + $format = '%s: %s'; + $arguments = [$record['errorCode']]; + switch ($record['errorHandler'][0] ?? false) { + case 'Fluid': + $arguments[] = $record['errorFluidTemplate']; + break; + case 'Page': + $arguments[] = $record['errorContentSource']; + break; + case 'PHP': + $arguments[] = $record['errorPhpClassFQCN']; + break; + default: + $arguments[] = $record['errorHandler'][0] ?? ''; + } + $parameters['title'] = sprintf($format, ...$arguments); + } + + public static function getAllSystemLocales(): array + { + $locales = []; + foreach (Locales::getAllSystemLocales() as $locale) { + $locales[] = ['label' => $locale, 'value' => $locale]; + } + return $locales; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Configuration/TranslationConfigurationProvider.php b/Classes/Configuration/TranslationConfigurationProvider.php new file mode 100644 index 0000000..02cb830 --- /dev/null +++ b/Classes/Configuration/TranslationConfigurationProvider.php @@ -0,0 +1,245 @@ +. + * + * @param int $pageId Page id (used to get TSconfig configuration setting flag and label for default language) + * @return array Array with languages + */ + public function getSystemLanguages(int $pageId = 0): array + { + $cacheKey = 'system-language-cache-page-uid-' . $pageId; + if ($this->runtimeCache->has($cacheKey)) { + return $this->runtimeCache->get($cacheKey); + } + $allSystemLanguages = []; + if ($pageId === 0) { + // Used for e.g. filelist, where there is no site selected. + // This also means that there is no "-1" (All Languages) selectable. + // Languages are consolidated across all sites with unique titles. + $sites = $this->siteFinder->getAllSites(); + foreach ($sites as $site) { + $this->addSiteLanguagesToConsolidatedList( + $allSystemLanguages, + $site->getAvailableLanguages($this->getBackendUserAuthentication()), + ); + } + $this->computeSystemLanguagesTitleAndFlag($allSystemLanguages, true); + } else { + try { + $site = $this->siteFinder->getSiteByPageId($pageId); + } catch (SiteNotFoundException) { + $site = new NullSite(); + } + $siteLanguages = $site->getAvailableLanguages($this->getBackendUserAuthentication(), true); + if (!isset($siteLanguages[0])) { + $siteLanguages[0] = $site->getDefaultLanguage(); + } + $this->addSiteLanguagesToConsolidatedList($allSystemLanguages, $siteLanguages); + $this->computeSystemLanguagesTitleAndFlag($allSystemLanguages); + } + ksort($allSystemLanguages); + $this->runtimeCache->set($cacheKey, $allSystemLanguages); + return $allSystemLanguages; + } + + protected function addSiteLanguagesToConsolidatedList(array &$allSystemLanguages, array $languagesOfSpecificSite): void + { + foreach ($languagesOfSpecificSite as $language) { + $languageId = $language->getLanguageId(); + $allSystemLanguages[$languageId] ??= [ + 'uid' => $languageId, + 'titlesMap' => [], + 'flagsMap' => [], + ]; + $allSystemLanguages[$languageId]['titlesMap'][$language->getTitle()] = true; + $allSystemLanguages[$languageId]['flagsMap'][$language->getFlagIdentifier()] = true; + } + } + + protected function computeSystemLanguagesTitleAndFlag(array &$allSystemLanguages, bool $showIdInTitle = false): void + { + foreach ($allSystemLanguages as &$language) { + $language['title'] = implode(', ', array_keys($language['titlesMap'])); + if ($language['uid'] === 0 && count($language['titlesMap']) > 1) { + // "Default" label for language 0 with multiple titles. + $language['title'] = $this->getLanguageService()->translate('LGL.defaultLanguage', 'core.general'); + } + if ($showIdInTitle) { + $language['title'] .= ' [' . $language['uid'] . ']'; + } + + $language['flagIcon'] = array_key_first($language['flagsMap']); + if (count($language['titlesMap']) > 1 || count($language['flagsMap']) > 1) { + $language['flagIcon'] = 'flags-multiple'; + } + + unset($language['titlesMap'], $language['flagsMap']); + } + } + + /** + * Information about translation for an element + * Will overlay workspace version of record too! + * + * @param string $table Table name + * @param int $uid Record uid + * @param int $languageUid Language uid. If 0, then all languages are selected. + * @param array|null $row The record to be translated + * @param array|string $selFieldList Select fields for the query which fetches the translations of the current record + * @return array|string Array with information or error message as a string. + */ + public function translationInfo($table, $uid, $languageUid = 0, ?array $row = null, $selFieldList = ''): array|string + { + if (!$this->tcaSchemaFactory->has($table) || !$uid) { + return 'No table "' . $table . '" or no UID value'; + } + $schema = $this->tcaSchemaFactory->get($table); + if (!$schema->isLanguageAware()) { + return 'Translation is not supported for this table!'; + } + if ($row === null) { + $row = BackendUtility::getRecordWSOL($table, $uid); + } + if (!is_array($row)) { + return 'Record "' . $table . '_' . $uid . '" was not found'; + } + $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); + $languageFieldName = $languageCapability->getLanguageField()->getName(); + $translationOriginPointerFieldName = $languageCapability->getTranslationOriginPointerField()->getName(); + if ($row[$languageFieldName] > 0) { + return 'Record "' . $table . '_' . $uid . '" seems to be a translation already (has a language value "' . $row[$languageFieldName] . '", relation to record "' . $row[$translationOriginPointerFieldName] . '")'; + } + if ($row[$translationOriginPointerFieldName] != 0) { + return 'Record "' . $table . '_' . $uid . '" seems to be a translation already (has a relation to record "' . $row[$translationOriginPointerFieldName] . '")'; + } + // Look for translations of this record, index by language field value: + if (!empty($selFieldList)) { + if (is_array($selFieldList)) { + $selectFields = $selFieldList; + } else { + $selectFields = GeneralUtility::trimExplode(',', $selFieldList); + } + } else { + $selectFields = ['uid', $languageFieldName]; + } + $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUserAuthentication()->workspace)); + $queryBuilder + ->select(...$selectFields) + ->from($table) + ->where( + $queryBuilder->expr()->eq( + $translationOriginPointerFieldName, + $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'pid', + $queryBuilder->createNamedParameter( + $row['pid'], + Connection::PARAM_INT + ) + ) + ); + if (!$languageUid) { + $queryBuilder->andWhere( + $queryBuilder->expr()->gt( + $languageFieldName, + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ) + ); + } else { + $queryBuilder + ->andWhere( + $queryBuilder->expr()->eq( + $languageFieldName, + $queryBuilder->createNamedParameter($languageUid, Connection::PARAM_INT) + ) + ); + } + $translationRecords = $queryBuilder->executeQuery()->fetchAllAssociative(); + + $translations = []; + $translationsErrors = []; + foreach ($translationRecords as $translationRecord) { + if (!isset($translations[$translationRecord[$languageFieldName]])) { + $translations[$translationRecord[$languageFieldName]] = $translationRecord; + } else { + $translationsErrors[$translationRecord[$languageFieldName]][] = $translationRecord; + } + } + return [ + 'table' => $table, + 'uid' => $uid, + 'CType' => $row['CType'] ?? '', + 'sys_language_uid' => $row[$languageFieldName] ?? null, + 'translations' => $translations, + 'excessive_translations' => $translationsErrors, + ]; + } + + protected function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Context/PageContext.php b/Classes/Context/PageContext.php new file mode 100644 index 0000000..6b9ee03 --- /dev/null +++ b/Classes/Context/PageContext.php @@ -0,0 +1,151 @@ +isAccessible() before processing. + * + * Usage: + * $pageContext = $request->getAttribute('pageContext'); + * if (!$pageContext->isAccessible()) { + * // Show no access page + * return $view->renderResponse('NoAccess'); + * } + * $selectedLanguages = $pageContext->selectedLanguageIds; + * $languageInfo = $pageContext->languageInformation; + * $rootLine = $pageContext->rootLine; + * $pageTsConfig = $pageContext->pageTsConfig; + * $moduleTsConfig = $pageContext->getModuleTsConfig('web_layout'); + * + * @internal + */ +final readonly class PageContext +{ + /** + * @param int $pageId Page ID (always preserved, even if no access) + * @param ?array $pageRecord Page record from readPageAccess (null if no access) + * @param int[] $selectedLanguageIds Selected language IDs (resolved and validated) + * @param PageLanguageInformation $languageInformation Complete language information for this page + * @param array $rootLine Page rootline including the page itself (empty array if no access) + * @param array $pageTsConfig PageTSconfig array (dots removed, overlaid by user permissions, falls back to page 0 if no access) + * @param Permission $pagePermissions User's permissions for this page (calculated from backendUser->calcPerms) + */ + public function __construct( + public int $pageId, + public ?array $pageRecord, + public SiteInterface $site, + public array $rootLine, + public array $pageTsConfig, + public array $selectedLanguageIds, + public PageLanguageInformation $languageInformation, + public Permission $pagePermissions, + ) {} + + /** + * Check if user has access to the page. + * + * Returns false if user has no access to the requested page. + * Controllers should check this before processing page-specific operations. + */ + public function isAccessible(): bool + { + return $this->pageRecord !== null && $this->pagePermissions->showPagePermissionIsGranted(); + } + + /** + * Get primary selected language for single-language views. + * + * Logic: + * - If exactly 1 non-default language is selected → use that translation + * - If 0 or 2+ non-default languages are selected → use default (0) + * + * This ensures that when switching from multi-language to single-language view, + * the user's focused translation is preserved (when they had one selected). + * + * @return int Primary language ID + */ + public function getPrimaryLanguageId(): int + { + $nonDefaultLanguages = array_filter($this->selectedLanguageIds, static fn(int $id): bool => $id > 0); + if (count($nonDefaultLanguages) === 1) { + return reset($nonDefaultLanguages); + } + return 0; + } + + /** + * Check if multiple languages are currently selected. + * + * This is useful for determining if comparison/multi-column view should be shown. + */ + public function hasMultipleLanguagesSelected(): bool + { + return count($this->selectedLanguageIds) > 1; + } + + public function isLanguageSelected(int $languageId): bool + { + return in_array($languageId, $this->selectedLanguageIds, true); + } + + public function isDefaultLanguageSelected(): bool + { + return $this->isLanguageSelected(0); + } + + /** + * Get page title (localized if translation exists). + * + * @param int|null $languageId Language ID (null = primary selected language) + */ + public function getPageTitle(?int $languageId = null): string + { + $languageId ??= $this->getPrimaryLanguageId(); + + if ($languageId === 0) { + return $this->pageRecord['title'] ?? ''; + } + + $translation = $this->languageInformation->getTranslationRecord($languageId); + return $translation['title'] ?? $this->pageRecord['title'] ?? ''; + } + + /** + * This is a convenience method to easily access mod.{module}.* configuration. + */ + public function getModuleTsConfig(string $module): array + { + return is_array($this->pageTsConfig['mod'][$module] ?? false) ? $this->pageTsConfig['mod'][$module] : []; + } +} diff --git a/Classes/Context/PageContextFactory.php b/Classes/Context/PageContextFactory.php new file mode 100644 index 0000000..8018267 --- /dev/null +++ b/Classes/Context/PageContextFactory.php @@ -0,0 +1,219 @@ +getAttribute('site'); + if (!$site instanceof SiteInterface) { + throw new SiteNotFoundException('No site found in request', 1731234567); + } + + // Check page access + $pageRecord = BackendUtility::readPageAccess($pageId, $backendUser->getPagePermsClause(Permission::PAGE_SHOW)) ?: null; + if ($pageId === 0 || !$pageRecord) { + // Either root page (pid=0) which has no real page record or no access. + // Return context with preserved pageId. + // pageRecord might be ['path' => '/'] for admins or NULL if no access or non-admin + // Still calculate permissions (admins have access to pid=0, editors don't). + return new PageContext( + pageId: $pageId, + pageRecord: $pageRecord, + site: $site, + rootLine: [], + pageTsConfig: GeneralUtility::removeDotsFromTS(BackendUtility::getPagesTSconfig(0)), + selectedLanguageIds: [0], + languageInformation: $this->languageService->getLanguageInformationForPage(0, $site, $backendUser), + pagePermissions: new Permission($backendUser->calcPerms($pageRecord ?: ['uid' => 0])), + ); + } + + // Get language information FIRST (needed for validation) + $languageInformation = $this->languageService->getLanguageInformationForPage($pageId, $site, $backendUser); + + // Resolve languages with fallback chain + $languagesFromRequest = $request->getQueryParams()['languages'] ?? $request->getParsedBody()['languages'] ?? null; + + // Extract ModuleData languages (with backward compat for old 'language' parameter) + $moduleData = $request->getAttribute('moduleData'); + $moduleDataLanguages = null; + if ($moduleData instanceof ModuleData) { + $moduleDataLanguages = $moduleData->get('languages'); + // Backward compatibility: convert old 'language' (single int) to 'languages' (array) + if ($moduleDataLanguages === null) { + $oldLanguage = $moduleData->get('language'); + if ($oldLanguage !== null) { + $moduleDataLanguages = [(int)$oldLanguage]; + } + } + } + + // Use SharedUserPreferences fallback chain (page-specific > ModuleData > default) + // This ensures page-specific preferences are shared across modules + $resolvedLanguages = $this->sharedPreferences->resolveLanguages( + $backendUser, + $languagesFromRequest, + $pageId, + $moduleDataLanguages + ); + + // Validate against existing translations on this page (ensures getPrimaryLanguageId() is valid) + // Preference is preserved across navigation (only stored when explicitly changed via request) + $existingLanguageIds = $languageInformation->getAllExistingLanguageIds(); + $validLanguages = array_intersect($resolvedLanguages, $existingLanguageIds); + + // Ensure at least default language if none are valid + if (empty($validLanguages)) { + $validLanguages = [0]; + } + + $validLanguages = array_values($validLanguages); + + // Store preference in SharedUserPreferences when explicitly changed via request + if ($languagesFromRequest !== null) { + $this->sharedPreferences->setPageLanguages($backendUser, $pageId, $validLanguages); + } + + // Also update ModuleData if present (for backward compatibility and UI state) + if ($moduleData instanceof ModuleData) { + $moduleData->set('languages', $validLanguages); + } + + // Create full PageContext for resolved page record + return new PageContext( + pageId: $pageId, + pageRecord: $pageRecord, + site: $site, + rootLine: BackendUtility::BEgetRootLine($pageId), + pageTsConfig: GeneralUtility::removeDotsFromTS(BackendUtility::getPagesTSconfig($pageId)), + selectedLanguageIds: $validLanguages, + languageInformation: $languageInformation, + pagePermissions: new Permission($backendUser->calcPerms($pageRecord)), + ); + } + + /** + * Create PageContext with specific languages (no fallback resolution). + * + * This is useful for testing or to explicitly set languages + * without going through the fallback chain. + * + * Access Handling: + * If the user has no access to the requested page or pid=0, a PageContext is still returned, + * while pageRecord mit be null if no access. Controllers should check isAccessible(). + */ + public function createWithLanguages( + ServerRequestInterface $request, + int $pageId, + array $languageIds, + BackendUserAuthentication $backendUser + ): PageContext { + $site = $request->getAttribute('site'); + if (!$site instanceof SiteInterface) { + throw new SiteNotFoundException('No site found in request', 1731234569); + } + + $pageRecord = BackendUtility::readPageAccess($pageId, $backendUser->getPagePermsClause(Permission::PAGE_SHOW)) ?: null; + if ($pageId === 0 || !$pageRecord) { + // Either root page (pid=0) which has no real page record or no access. + // Return context with preserved pageId. + // pageRecord might be ['path' => '/'] for admins or NULL if no access or non-admin + // Still calculate permissions (admins have access to pid=0, editors don't). + return new PageContext( + pageId: $pageId, + pageRecord: $pageRecord, + site: $site, + rootLine: [], + pageTsConfig: GeneralUtility::removeDotsFromTS(BackendUtility::getPagesTSconfig(0)), + selectedLanguageIds: array_map('intval', $languageIds), + languageInformation: $this->languageService->getLanguageInformationForPage(0, $site, $backendUser), + pagePermissions: new Permission($backendUser->calcPerms($pageRecord ?: ['uid' => 0])), + ); + } + + // Create full PageContext for resolved page record + return new PageContext( + pageId: $pageId, + pageRecord: $pageRecord, + site: $site, + rootLine: BackendUtility::BEgetRootLine($pageId), + pageTsConfig: GeneralUtility::removeDotsFromTS(BackendUtility::getPagesTSconfig($pageId)), + selectedLanguageIds: array_map('intval', $languageIds), + languageInformation: $this->languageService->getLanguageInformationForPage($pageId, $site, $backendUser), + pagePermissions: new Permission($backendUser->calcPerms($pageRecord)), + ); + } +} diff --git a/Classes/ContextMenu/ContextMenu.php b/Classes/ContextMenu/ContextMenu.php new file mode 100644 index 0000000..2b6f87d --- /dev/null +++ b/Classes/ContextMenu/ContextMenu.php @@ -0,0 +1,123 @@ +itemProvidersRegistry = $itemProvidersRegistry; + } + + public function getItems(string $table, string $identifier, string $context = ''): array + { + $items = []; + foreach ($this->getAvailableProviders($table, $identifier, $context) as $provider) { + $items = $provider->addItems($items); + } + return $this->cleanItems($items); + } + + /** + * @return ProviderInterface[] + */ + protected function getAvailableProviders(string $table, string $identifier, string $context): array + { + $providers = $this->itemProvidersRegistry->getItemProviders(); + $availableProviders = []; + foreach ($providers as $provider) { + $provider->setContext($table, $identifier, $context); + if ($provider->canHandle()) { + $priority = $provider->getPriority(); + $availableProviders[$priority] = $provider; + } + } + krsort($availableProviders); + return $availableProviders; + } + + /** + * Clean up double dividers. + * Don't render menu when there are no item or submenu. + */ + protected function cleanItems(array $items): array + { + $canRender = false; + $prevItemWasDivider = false; + + foreach ($items as $key => $item) { + // Assign the key as the identifier for each item. + // This is needed for the JavaScript to render a single node + $items[$key]['identifier'] = $key; + + if ($item['type'] === 'item') { + $canRender = true; + $prevItemWasDivider = false; + continue; + } + if ($item['type'] === 'divider') { + if ($prevItemWasDivider === true) { + unset($items[$key]); + } else { + $prevItemWasDivider = true; + } + continue; + } + if ($item['type'] === 'submenu') { + $childItems = $this->cleanItems($item['childItems']); + if (empty($childItems)) { + unset($items[$key]); + } else { + $items[$key]['childItems'] = $childItems; + $canRender = true; + $prevItemWasDivider = false; + } + continue; + } + } + + if ($canRender) { + //Remove first and last divider + $fistItem = reset($items); + if ($fistItem['type'] === 'divider') { + $key = key($items); + unset($items[$key]); + } + $lastItem = end($items); + if ($lastItem['type'] === 'divider') { + $key = key($items); + unset($items[$key]); + } + } else { + //no menu when there are no item or submenu + $items = []; + } + return $items; + } +} diff --git a/Classes/ContextMenu/ItemProviders/AbstractProvider.php b/Classes/ContextMenu/ItemProviders/AbstractProvider.php new file mode 100644 index 0000000..38c3907 --- /dev/null +++ b/Classes/ContextMenu/ItemProviders/AbstractProvider.php @@ -0,0 +1,210 @@ +languageService = $GLOBALS['LANG']; + $this->backendUser = $GLOBALS['BE_USER']; + } + + public function setContext(string $table, string $identifier, string $context = ''): void + { + $this->table = $table; + $this->identifier = $identifier; + $this->context = $context; + } + + /** + * Provider initialization, heavy stuff + */ + protected function initialize() + { + $this->initClipboard(); + $this->initDisabledItems(); + } + + /** + * Returns the provider priority which is used for determining the order in which providers are adding items + * to the result array. Highest priority means provider is evaluated first. + */ + public function getPriority(): int + { + return 100; + } + + /** + * Whether this provider can handle given request (usually a check based on table, uid and context) + */ + public function canHandle(): bool + { + return false; + } + + /** + * Initialize clipboard object - necessary for all copy/cut/paste operations + */ + protected function initClipboard() + { + $clipboard = GeneralUtility::makeInstance(Clipboard::class); + $clipboard->initializeClipboard(); + // This locks the clipboard to the Normal for this request. + $clipboard->lockToNormal(); + // This removes all no longer existing elements + $clipboard->cleanCurrent(); + // This stores the changed clipboard data + $clipboard->endClipboard(); + $this->clipboard = $clipboard; + } + + /** + * Fills $this->disabledItems with the values from TSConfig. + * Disabled items can be set separately for each context. + */ + protected function initDisabledItems() + { + if ($this->context) { + $tsConfigValue = $this->backendUser->getTSConfig()['options.']['contextMenu.']['table.'][$this->table . '.'][$this->context . '.']['disableItems'] ?? ''; + } else { + $tsConfigValue = $this->backendUser->getTSConfig()['options.']['contextMenu.']['table.'][$this->table . '.']['disableItems'] ?? ''; + } + $this->disabledItems = GeneralUtility::trimExplode(',', $tsConfigValue, true); + } + + /** + * Adds new items to the given array or modifies existing items + */ + public function addItems(array $items): array + { + $this->initialize(); + $items += $this->prepareItems($this->itemsConfiguration); + return $items; + } + + /** + * Converts item configuration (from $this->itemsConfiguration) into an array ready for returning by controller + */ + protected function prepareItems(array $itemsConfiguration): array + { + $iconFactory = GeneralUtility::makeInstance(IconFactory::class); + $items = []; + foreach ($itemsConfiguration as $name => $configuration) { + $type = !empty($configuration['type']) ? $configuration['type'] : 'item'; + if ($this->canRender($name, $type)) { + $items[$name] = [ + 'type' => $type, + 'label' => !empty($configuration['label']) ? htmlspecialchars($this->languageService->sL($configuration['label'])) : '', + 'icon' => !empty($configuration['iconIdentifier']) ? $iconFactory->getIcon($configuration['iconIdentifier'], IconSize::SMALL)->render('inline') : '', + 'additionalAttributes' => $this->getAdditionalAttributes($name), + 'callbackAction' => !empty($configuration['callbackAction']) ? $configuration['callbackAction'] : '', + ]; + if ($type === 'submenu') { + $items[$name]['childItems'] = $this->prepareItems($configuration['childItems']); + } + } + } + return $items; + } + + /** + * Returns an array of additional attributes for given item. Additional attributes are used to pass item specific data + * to the JS. E.g. message for the delete confirmation dialog + */ + protected function getAdditionalAttributes(string $itemName): array + { + return []; + } + + /** + * Checks whether certain item can be rendered (e.g. check for disabled items or permissions) + */ + protected function canRender(string $itemName, string $type): bool + { + return true; + } + + /** + * Returns a clicked record identifier + */ + protected function getIdentifier(): string + { + return ''; + } +} diff --git a/Classes/ContextMenu/ItemProviders/ItemProvidersRegistry.php b/Classes/ContextMenu/ItemProviders/ItemProvidersRegistry.php new file mode 100644 index 0000000..e384419 --- /dev/null +++ b/Classes/ContextMenu/ItemProviders/ItemProvidersRegistry.php @@ -0,0 +1,47 @@ +itemProviders[] = $itemProvider; + } + } + } + + /** + * Get all registered item providers + * + * @return ProviderInterface[] + */ + public function getItemProviders(): array + { + return $this->itemProviders; + } +} diff --git a/Classes/ContextMenu/ItemProviders/PageProvider.php b/Classes/ContextMenu/ItemProviders/PageProvider.php new file mode 100644 index 0000000..15df63c --- /dev/null +++ b/Classes/ContextMenu/ItemProviders/PageProvider.php @@ -0,0 +1,561 @@ + [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.view', + 'iconIdentifier' => 'actions-view-page', + 'callbackAction' => 'viewRecord', + ], + 'edit' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.edit', + 'iconIdentifier' => 'actions-page-open', + 'callbackAction' => 'editRecord', + ], + 'new' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.newSubpage', + 'iconIdentifier' => 'actions-page-new', + 'callbackAction' => 'newRecord', + ], + 'info' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info', + 'iconIdentifier' => 'actions-document-info', + 'callbackAction' => 'openInfoPopUp', + ], + 'qrcode' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:showPageQrCode', + 'iconIdentifier' => 'actions-qrcode', + 'callbackAction' => 'showQrCode', + ], + 'divider1' => [ + 'type' => 'divider', + ], + 'copy' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy', + 'iconIdentifier' => 'actions-edit-copy', + 'callbackAction' => 'copy', + ], + 'copyRelease' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy', + 'iconIdentifier' => 'actions-edit-copy-release', + 'callbackAction' => 'clipboardRelease', + ], + 'cut' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut', + 'iconIdentifier' => 'actions-edit-cut', + 'callbackAction' => 'cut', + ], + 'cutRelease' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cutrelease', + 'iconIdentifier' => 'actions-edit-cut-release', + 'callbackAction' => 'clipboardRelease', + ], + 'pasteAfter' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteafter', + 'iconIdentifier' => 'actions-document-paste-after', + 'callbackAction' => 'pasteAfter', + ], + 'pasteInto' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteinto', + 'iconIdentifier' => 'actions-document-paste-into', + 'callbackAction' => 'pasteInto', + ], + 'divider2' => [ + 'type' => 'divider', + ], + 'more' => [ + 'type' => 'submenu', + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.more', + 'iconIdentifier' => '', + 'callbackAction' => 'openSubmenu', + 'childItems' => [ + 'pagesSort' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_sort.xlf:title', + 'iconIdentifier' => 'actions-page-move', + 'callbackAction' => 'pagesSort', + ], + 'pagesNewMultiple' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_new.xlf:title', + 'iconIdentifier' => 'apps-pagetree-drag-move-between', + 'callbackAction' => 'pagesNewMultiple', + ], + 'mountAsTreeRoot' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.tempMountPoint', + 'iconIdentifier' => 'actions-pagetree-mountroot', + 'callbackAction' => 'mountAsTreeRoot', + ], + 'showInMenus' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_showInMenus', + 'iconIdentifier' => 'actions-view', + 'callbackAction' => 'showInMenus', + ], + 'hideInMenus' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_hideInMenus', + 'iconIdentifier' => 'actions-ban', + 'callbackAction' => 'hideInMenus', + ], + ], + ], + 'divider3' => [ + 'type' => 'divider', + ], + 'enable' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:enable', + 'iconIdentifier' => 'actions-edit-unhide', + 'callbackAction' => 'enableRecord', + ], + 'disable' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:disable', + 'iconIdentifier' => 'actions-edit-hide', + 'callbackAction' => 'disableRecord', + ], + 'delete' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete', + 'iconIdentifier' => 'actions-edit-delete', + 'callbackAction' => 'deleteRecord', + ], + 'history' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_history', + 'iconIdentifier' => 'actions-document-history-open', + 'callbackAction' => 'openHistoryPopUp', + ], + 'clearCache' => [ + 'label' => 'core.cache:page.label', + 'iconIdentifier' => 'actions-system-cache-clear', + 'callbackAction' => 'clearCache', + ], + ]; + + protected bool $languageAccess = false; + + /** + * Checks if the provider can add items to the menu + */ + public function canHandle(): bool + { + return $this->table === 'pages'; + } + + public function getPriority(): int + { + return 100; + } + + protected function canRender(string $itemName, string $type): bool + { + if (in_array($type, ['divider', 'submenu'], true)) { + return true; + } + if (in_array($itemName, $this->disabledItems, true)) { + return false; + } + $canRender = false; + switch ($itemName) { + case 'view': + case 'qrcode': + $canRender = $this->canBeViewed(); + break; + case 'edit': + $canRender = $this->canBeEdited(); + break; + case 'new': + case 'pagesNewMultiple': + $canRender = $this->canBeCreated(); + break; + case 'info': + $canRender = $this->canShowInfo(); + break; + case 'enable': + $canRender = $this->canBeEnabled(); + break; + case 'disable': + $canRender = $this->canBeDisabled(); + break; + case 'showInMenus': + $canRender = $this->canBeToggled('nav_hide', 1); + break; + case 'hideInMenus': + $canRender = $this->canBeToggled('nav_hide', 0); + break; + case 'delete': + $canRender = $this->canBeDeleted(); + break; + case 'history': + $canRender = $this->canShowHistory(); + break; + case 'pagesSort': + $canRender = $this->canBeSorted(); + break; + case 'mountAsTreeRoot': + $canRender = !$this->isRoot(); + break; + case 'copy': + $canRender = $this->canBeCopied(); + break; + case 'copyRelease': + $canRender = $this->isRecordInClipboard('copy'); + break; + case 'cut': + $canRender = $this->canBeCut() && !$this->isRecordInClipboard('cut'); + break; + case 'cutRelease': + $canRender = $this->isRecordInClipboard('cut'); + break; + case 'pasteAfter': + $canRender = $this->canBePastedAfter(); + break; + case 'pasteInto': + $canRender = $this->canBePastedInto(); + break; + case 'clearCache': + $canRender = $this->canClearCache(); + break; + } + return $canRender; + } + + /** + * Saves calculated permissions for a page to speed things up + */ + protected function initPermissions(): void + { + $this->pagePermissions = new Permission($this->backendUser->calcPerms($this->record)); + $this->languageAccess = $this->hasLanguageAccess(); + } + + /** + * Checks if the user may create pages below the given page + */ + protected function canBeCreated(): bool + { + if (!$this->backendUser->checkLanguageAccess(0)) { + return false; + } + if ($this->getLanguageField() !== '' + && !in_array($this->record[$this->getLanguageField()] ?? false, [0, -1]) + ) { + return false; + } + if (!$this->backendUser->check('tables_modify', $this->table)) { + return false; + } + return $this->hasPagePermission(Permission::PAGE_NEW); + } + + /** + * Checks if the user has editing rights + */ + protected function canBeEdited(): bool + { + if (!$this->languageAccess) { + return false; + } + if ($this->isRoot()) { + return false; + } + if ($this->getSchema()?->hasCapability(TcaSchemaCapability::AccessReadOnly)) { + return false; + } + if ($this->backendUser->isAdmin()) { + return true; + } + if ($this->getSchema()?->hasCapability(TcaSchemaCapability::AccessAdminOnly)) { + return false; + } + if (!$this->backendUser->check('tables_modify', $this->table)) { + return false; + } + return !$this->isRecordLocked() && $this->hasPagePermission(Permission::PAGE_EDIT); + } + + /** + * Check if a page is locked + */ + protected function isRecordLocked(): bool + { + return (bool)$this->record[$this->getSchema()->getCapability(TcaSchemaCapability::EditLock)->getFieldName()]; + } + + /** + * Checks if the page is allowed to can be cut + */ + protected function canBeCut(): bool + { + if (!$this->languageAccess) { + return false; + } + if ($this->getLanguageField() !== '' + && !in_array($this->record[$this->getLanguageField()] ?? false, [0, -1]) + ) { + return false; + } + if (!$this->backendUser->check('tables_modify', $this->table)) { + return false; + } + return !$this->isWebMount() + && $this->canBeEdited() + && !$this->isDeletePlaceholder(); + } + + /** + * Checks if the page is allowed to be copied + */ + protected function canBeCopied(): bool + { + if (!$this->languageAccess) { + return false; + } + if ($this->getLanguageField() !== '' + && !in_array($this->record[$this->getLanguageField()] ?? false, [0, -1]) + ) { + return false; + } + if (!$this->backendUser->check('tables_select', $this->table)) { + return false; + } + return !$this->isRoot() + && !$this->isWebMount() + && !$this->isRecordInClipboard('copy') + && $this->hasPagePermission(Permission::PAGE_SHOW) + && !$this->isDeletePlaceholder(); + } + + /** + * Checks if something can be pasted into the node + */ + protected function canBePastedInto(): bool + { + if (!$this->languageAccess) { + return false; + } + $clipboardElementCount = count($this->clipboard->elFromTable($this->table)); + + return $clipboardElementCount + && $this->canBeCreated() + && !$this->isDeletePlaceholder(); + } + + /** + * Checks if something can be pasted after the node + */ + protected function canBePastedAfter(): bool + { + if (!$this->languageAccess) { + return false; + } + $clipboardElementCount = count($this->clipboard->elFromTable($this->table)); + return $clipboardElementCount + && $this->canBeCreated() + && !$this->isDeletePlaceholder(); + } + + /** + * Check if sub pages of given page can be sorted + */ + protected function canBeSorted(): bool + { + if (!$this->languageAccess) { + return false; + } + return $this->backendUser->check('tables_modify', $this->table) + && $this->hasPagePermission(Permission::CONTENT_EDIT) + && !$this->isDeletePlaceholder() + && $this->backendUser->workspace === 0; + } + + /** + * Checks if the page is allowed to be removed + */ + protected function canBeDeleted(): bool + { + if (!$this->languageAccess) { + return false; + } + return !$this->isRoot() + && !$this->isDeletePlaceholder() + && !$this->isRecordLocked() + && !$this->isDeletionDisabledInTS() + && $this->hasPagePermission(Permission::PAGE_DELETE); + } + + /** + * Checks if the page is allowed to be viewed in frontend + */ + protected function canBeViewed(): bool + { + return !$this->isRoot() + && !$this->isDeleted() + && $this->previewLinkCanBeBuild(); + } + + /** + * Checks if the page is allowed to show info + */ + protected function canShowInfo(): bool + { + return !$this->isRoot(); + } + + /** + * Checks if the user has clear cache rights + */ + protected function canClearCache(): bool + { + return !$this->isRoot() + && ($this->backendUser->isAdmin() || ($this->backendUser->getTSConfig()['options.']['clearCache.']['pages'] ?? false)); + } + + /** + * Determines whether this node is deleted. + */ + protected function isDeleted(): bool + { + return !empty($this->record['deleted']) || $this->isDeletePlaceholder(); + } + + /** + * Returns true if current record is a root page + */ + protected function isRoot(): bool + { + return (int)$this->identifier === 0; + } + + /** + * Returns true if current record is a web mount + */ + protected function isWebMount(): bool + { + return in_array($this->identifier, $this->backendUser->getWebmounts()); + } + + protected function getAdditionalAttributes(string $itemName): array + { + $attributes = []; + if ($itemName === 'view' || $itemName === 'qrcode') { + $attributes += $this->getViewAdditionalAttributes(); + } + if ($itemName === 'enable' || $itemName === 'disable') { + $attributes += $this->getEnableDisableAdditionalAttributes(); + } + if ($itemName === 'delete') { + $attributes += $this->getDeleteAdditionalAttributes(); + } + if ($itemName === 'pasteInto') { + $attributes += $this->getPasteAdditionalAttributes('into'); + } + if ($itemName === 'pasteAfter') { + $attributes += $this->getPasteAdditionalAttributes('after'); + } + if ($itemName === 'pagesSort') { + $attributes += [ + 'data-pages-sort-url' => (string)$this->uriBuilder->buildUriFromRoute('pages_sort', ['id' => $this->record['uid'] ?? null]), + ]; + } + if ($itemName === 'pagesNewMultiple') { + $attributes += [ + 'data-pages-new-multiple-url' => (string)$this->uriBuilder->buildUriFromRoute('pages_new', ['id' => $this->record['uid'] ?? 0]), + ]; + } + + if ($itemName === 'edit') { + $attributes = [ + 'data-pages-language-uid' => $this->record[$this->getLanguageField()] ?? null, + ]; + } + return $attributes; + } + + protected function getPreviewPid(): int + { + return (int)($this->record[$this->getLanguageField()] ?? 0) === 0 ? (int)$this->record['uid'] : (int)$this->record['l10n_parent']; + } + + /** + * Returns the view link + */ + protected function getViewLink(): string + { + return (string)PreviewUriBuilder::create($this->record)->buildUri(); + } + + /** + * Checks if user has access to this column, the doktype + * is not excluded and that it contains the given value. + */ + protected function canBeToggled(string $fieldName, int $value): bool + { + if (!$this->languageAccess || $this->isRoot()) { + return false; + } + $field = $this->getSchema()->getField($fieldName); + if ($field->supportsAccessControl() + && !$this->isExcludedDoktype() + && $this->backendUser->check('non_exclude_fields', $this->table . ':' . $fieldName) + && $this->backendUser->check('tables_modify', $this->table) + ) { + return (int)$this->record[$fieldName] === $value; + } + return false; + } + + /** + * Returns true if a current user has access to the language of the record + * + * @see BackendUserAuthentication::checkLanguageAccess() + */ + protected function hasLanguageAccess(): bool + { + if ($this->backendUser->isAdmin()) { + return true; + } + if (($languageField = $this->getLanguageField()) !== '' && isset($this->record[$languageField])) { + return $this->backendUser->checkLanguageAccess((int)$this->record[$languageField]); + } + return true; + } + + /** + * Returns true if the page doktype is excluded + */ + protected function isExcludedDoktype(): bool + { + $doktypeRegistry = GeneralUtility::makeInstance(PageDoktypeRegistry::class); + return !$doktypeRegistry->isPageTypeViewable((int)($this->record['doktype'] ?? 0)); + } +} diff --git a/Classes/ContextMenu/ItemProviders/ProviderInterface.php b/Classes/ContextMenu/ItemProviders/ProviderInterface.php new file mode 100644 index 0000000..b0aae57 --- /dev/null +++ b/Classes/ContextMenu/ItemProviders/ProviderInterface.php @@ -0,0 +1,42 @@ +record is placed on + * + * @var array + */ + protected $pageRecord = []; + + /** + * Local cache for the result of BackendUserAuthentication::calcPerms() + * + * @var Permission + */ + protected $pagePermissions; + + /** + * @var array + */ + protected $itemsConfiguration = [ + 'view' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.view', + 'iconIdentifier' => 'actions-view', + 'callbackAction' => 'viewRecord', + ], + 'edit' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.edit', + 'iconIdentifier' => 'actions-open', + 'callbackAction' => 'editRecord', + ], + 'new' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.new', + 'iconIdentifier' => 'actions-plus', + 'callbackAction' => 'newRecord', + ], + 'info' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info', + 'iconIdentifier' => 'actions-document-info', + 'callbackAction' => 'openInfoPopUp', + ], + 'divider1' => [ + 'type' => 'divider', + ], + 'copy' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy', + 'iconIdentifier' => 'actions-edit-copy', + 'callbackAction' => 'copy', + ], + 'copyRelease' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy', + 'iconIdentifier' => 'actions-edit-copy-release', + 'callbackAction' => 'clipboardRelease', + ], + 'cut' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut', + 'iconIdentifier' => 'actions-edit-cut', + 'callbackAction' => 'cut', + ], + 'cutRelease' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cutrelease', + 'iconIdentifier' => 'actions-edit-cut-release', + 'callbackAction' => 'clipboardRelease', + ], + 'pasteAfter' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteafter', + 'iconIdentifier' => 'actions-document-paste-after', + 'callbackAction' => 'pasteAfter', + ], + 'divider2' => [ + 'type' => 'divider', + ], + 'more' => [ + 'type' => 'submenu', + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.more', + 'iconIdentifier' => '', + 'callbackAction' => 'openSubmenu', + 'childItems' => [ + 'newWizard' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_newWizard', + 'iconIdentifier' => 'actions-plus', + 'callbackAction' => 'newContentWizard', + ], + ], + ], + 'divider3' => [ + 'type' => 'divider', + ], + 'enable' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:enable', + 'iconIdentifier' => 'actions-edit-unhide', + 'callbackAction' => 'enableRecord', + ], + 'disable' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:disable', + 'iconIdentifier' => 'actions-edit-hide', + 'callbackAction' => 'disableRecord', + ], + 'delete' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete', + 'iconIdentifier' => 'actions-edit-delete', + 'callbackAction' => 'deleteRecord', + ], + 'history' => [ + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_history', + 'iconIdentifier' => 'actions-document-history-open', + 'callbackAction' => 'openHistoryPopUp', + ], + ]; + + public function __construct( + protected readonly TcaSchemaFactory $tcaSchemaFactory, + protected readonly UriBuilder $uriBuilder, + protected readonly LocalizationRepository $localizationRepository, + ) { + parent::__construct(); + } + + /** + * Whether this provider should kick in + */ + public function canHandle(): bool + { + if (in_array($this->table, ['sys_file', 'pages'], true)) { + return false; + } + return $this->tcaSchemaFactory->has($this->table); + } + + /** + * Initialize db record + */ + protected function initialize() + { + parent::initialize(); + $this->record = BackendUtility::getRecordWSOL($this->table, (int)$this->identifier); + $this->initPermissions(); + } + + /** + * Priority is set to lower then default value, in order to skip this provider if there is less generic provider available. + */ + public function getPriority(): int + { + return 60; + } + + /** + * This provider works as a fallback if there is no provider dedicated for certain table, thus it's only kicking in when $items are empty. + */ + public function addItems(array $items): array + { + if (!empty($items)) { + return $items; + } + $this->initialize(); + return $this->prepareItems($this->itemsConfiguration); + } + + /** + * Whether a given item can be rendered (e.g. user has enough permissions) + */ + protected function canRender(string $itemName, string $type): bool + { + if (in_array($type, ['divider', 'submenu'], true)) { + return true; + } + if (in_array($itemName, $this->disabledItems, true)) { + return false; + } + $canRender = false; + switch ($itemName) { + case 'view': + $canRender = $this->canBeViewed(); + break; + case 'edit': + $canRender = $this->canBeEdited(); + break; + case 'new': + $canRender = $this->canBeNew(); + break; + case 'newWizard': + $canRender = $this->canOpenNewCEWizard(); + break; + case 'info': + $canRender = $this->canShowInfo(); + break; + case 'enable': + $canRender = $this->canBeEnabled(); + break; + case 'disable': + $canRender = $this->canBeDisabled(); + break; + case 'delete': + $canRender = $this->canBeDeleted(); + break; + case 'history': + $canRender = $this->canShowHistory(); + break; + case 'copy': + $canRender = $this->canBeCopied(); + break; + case 'copyRelease': + $canRender = $this->isRecordInClipboard('copy'); + break; + case 'cut': + $canRender = $this->canBeCut(); + break; + case 'cutRelease': + $canRender = $this->isRecordInClipboard('cut'); + break; + case 'pasteAfter': + $canRender = $this->canBePastedAfter(); + break; + } + return $canRender; + } + + /** + * Saves calculated permissions for a page containing given record, to speed things up + */ + protected function initPermissions() + { + $this->pageRecord = BackendUtility::getRecord('pages', $this->record['pid']) ?? []; + $this->pagePermissions = new Permission($this->backendUser->calcPerms($this->pageRecord)); + } + + /** + * Returns true if a current user have access to given permission + * + * @see BackendUserAuthentication::doesUserHaveAccess() + */ + protected function hasPagePermission(int $permission): bool + { + return $this->backendUser->isAdmin() || $this->pagePermissions->isGranted($permission); + } + + /** + * Additional attributes for JS + */ + protected function getAdditionalAttributes(string $itemName): array + { + $attributes = []; + if ($itemName === 'view') { + $attributes += $this->getViewAdditionalAttributes(); + } + if ($itemName === 'enable' || $itemName === 'disable') { + $attributes += $this->getEnableDisableAdditionalAttributes(); + } + if ($itemName === 'newWizard' && $this->table === 'tt_content') { + $urlParameters = [ + 'id' => $this->record['pid'], + 'sys_language_uid' => $this->record[$this->getLanguageField()] ?? null, + 'colPos' => $this->record['colPos'], + 'uid_pid' => -$this->record['uid'], + ]; + $url = (string)$this->uriBuilder->buildUriFromRoute('new_content_element_wizard', $urlParameters); + $attributes += [ + 'data-new-wizard-url' => $url, + 'data-title' => $this->languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:newContentElement'), + ]; + } + if ($itemName === 'delete') { + $attributes += $this->getDeleteAdditionalAttributes(); + } + if ($itemName === 'pasteAfter') { + $attributes += $this->getPasteAdditionalAttributes('after'); + } + return $attributes; + } + + /** + * Additional attributes for the 'view' item + */ + protected function getViewAdditionalAttributes(): array + { + $attributes = []; + $viewLink = $this->getViewLink(); + if ($viewLink) { + $attributes += [ + 'data-preview-url' => $viewLink, + ]; + } + return $attributes; + } + + /** + * Additional attributes for the hide & unhide items + */ + protected function getEnableDisableAdditionalAttributes(): array + { + $hiddenFieldName = ''; + if (($schema = $this->getSchema())?->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) { + $hiddenFieldName = $schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName(); + } + return [ + 'data-disable-field' => $hiddenFieldName, + ]; + } + + /** + * Additional attributes for the pasteInto and pasteAfter items + * + * @param string $type "after" or "into" + */ + protected function getPasteAdditionalAttributes(string $type): array + { + $closeText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel'); + $okText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:ok'); + $attributes = []; + if ($this->backendUser->jsConfirmation(JsConfirmation::COPY_MOVE_PASTE)) { + $selItem = $this->clipboard->getSelectedRecord(); + $title = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:clip_paste'); + + $confirmMessage = sprintf( + $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.' + . ($this->clipboard->currentMode() === 'copy' ? 'copy' : 'move') . '_' . $type), + BackendUtility::cropToTitleLength($selItem['_RECORD_TITLE']), + BackendUtility::cropToTitleLength(BackendUtility::getRecordTitle($this->table, $this->record)) + ); + $attributes += [ + 'data-title' => $title, + 'data-message' => $confirmMessage, + 'data-button-close-text' => $closeText, + 'data-button-ok-text' => $okText, + ]; + } + return $attributes; + } + + /** + * Additional data for a "delete" action (confirmation modal title and message) + */ + protected function getDeleteAdditionalAttributes(): array + { + $closeText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel'); + $okText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete'); + $attributes = []; + if ($this->backendUser->jsConfirmation(JsConfirmation::DELETE)) { + $title = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.delete.title'); + $recordInfo = BackendUtility::cropToTitleLength(BackendUtility::getRecordTitle($this->table, $this->record)); + if ($this->backendUser->shallDisplayDebugInformation()) { + $recordInfo .= ' [' . $this->table . ':' . $this->record['uid'] . ']'; + } + $confirmMessage = sprintf( + $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.delete'), + trim($recordInfo) + ); + $confirmMessage .= BackendUtility::referenceCount( + $this->table, + $this->record['uid'], + LF . $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord') + ); + $translationCount = count($this->localizationRepository->getRecordTranslations($this->table, $this->record['uid'])); + if ($translationCount > 0) { + $confirmMessage .= LF . sprintf( + $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord'), + $translationCount + ); + } + + $attributes += [ + 'data-title' => $title, + 'data-message' => $confirmMessage, + 'data-button-close-text' => $closeText, + 'data-button-ok-text' => $okText, + ]; + } + return $attributes; + } + + /** + * Returns id of the Page used for preview + */ + protected function getPreviewPid(): int + { + return (int)$this->record['pid']; + } + + /** + * Returns the view link + */ + protected function getViewLink(): string + { + return (string)PreviewUriBuilder::createForRecordPreview( + $this->table, + $this->record, + $this->pageRecord['uid'] ?? 0 + )->buildUri(); + } + + /** + * Checks if the page is allowed to show info + */ + protected function canShowInfo(): bool + { + return true; + } + + /** + * Checks if the page is allowed to show info + */ + protected function canShowHistory(): bool + { + $userTsConfig = $this->backendUser->getTSConfig(); + return (bool)trim($userTsConfig['options.']['showHistory.'][$this->table] ?? $userTsConfig['options.']['showHistory'] ?? '1'); + } + + /** + * Checks if the record can be previewed in frontend + */ + protected function canBeViewed(): bool + { + return $this->previewLinkCanBeBuild(); + } + + /** + * Whether a record can be edited + */ + protected function canBeEdited(): bool + { + if ($this->getSchema()?->hasCapability(TcaSchemaCapability::AccessReadOnly)) { + return false; + } + if ($this->backendUser->isAdmin()) { + return true; + } + if ($this->getSchema()?->hasCapability(TcaSchemaCapability::AccessAdminOnly)) { + return false; + } + + $access = !$this->isRecordLocked() + && $this->backendUser->check('tables_modify', $this->table) + && $this->hasPagePermission(Permission::CONTENT_EDIT) + && $this->backendUser->checkRecordEditAccess($this->table, $this->record)->isAllowed; + return $access; + } + + /** + * Whether a record can be created + */ + protected function canBeNew(): bool + { + return $this->canBeEdited() && !$this->isRecordATranslation(); + } + + /** + * Checks if disableDelete flag is set in TSConfig for the current table + */ + protected function isDeletionDisabledInTS(): bool + { + return (bool)trim( + $this->backendUser->getTSConfig()['options.']['disableDelete.'][$this->table] + ?? $this->backendUser->getTSConfig()['options.']['disableDelete'] + ?? '' + ); + } + + /** + * Checks if the user has the right to delete the record + */ + protected function canBeDeleted(): bool + { + return !$this->isDeletionDisabledInTS() + && !$this->isRecordCurrentBackendUser() + && $this->canBeEdited(); + } + + /** + * Returns true if current record can be unhidden/enabled + */ + protected function canBeEnabled(): bool + { + return $this->hasDisableColumnWithValue(1) && $this->canBeEdited(); + } + + /** + * Returns true if current record can be hidden + */ + protected function canBeDisabled(): bool + { + return $this->hasDisableColumnWithValue(0) + && !$this->isRecordCurrentBackendUser() + && $this->canBeEdited(); + } + + /** + * Returns true new content element wizard can be shown + */ + protected function canOpenNewCEWizard(): bool + { + return $this->table === 'tt_content' && $this->canBeEdited() && !$this->isRecordATranslation(); + } + + protected function canBeCopied(): bool + { + return !$this->isRecordInClipboard('copy') + && !$this->isRecordATranslation(); + } + + protected function canBeCut(): bool + { + return !$this->isRecordInClipboard('cut') + && $this->canBeEdited() + && !$this->isRecordATranslation(); + } + + /** + * Paste after is only shown for records from the same table (comparing record in clipboard and record clicked) + */ + protected function canBePastedAfter(): bool + { + $clipboardElementCount = count($this->clipboard->elFromTable($this->table)); + + return $clipboardElementCount + && $this->backendUser->check('tables_modify', $this->table) + && $this->hasPagePermission(Permission::CONTENT_EDIT); + } + + /** + * Checks if table have "disable" column (e.g. "hidden"), if user has access to this column + * and if it contains given value + */ + protected function hasDisableColumnWithValue(int $value): bool + { + if (!$this->getSchema()?->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) { + return false; + } + $hiddenField = $this->getSchema()->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getField(); + if (!$hiddenField->supportsAccessControl() || $this->backendUser->check('non_exclude_fields', $this->table . ':' . $hiddenField->getName())) { + return (int)($this->record[$hiddenField->getName()] ?? 0) === $value; + } + return false; + } + + /** + * Record is locked if page is locked or page is not locked but record is + */ + protected function isRecordLocked(): bool + { + if (($pageSchema = $this->tcaSchemaFactory->get('pages'))->hasCapability(TcaSchemaCapability::EditLock) + && ($this->pageRecord[$pageSchema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false) + ) { + return true; + } + if (!$this->getSchema()?->hasCapability(TcaSchemaCapability::EditLock)) { + return false; + } + return (bool)$this->record[$this->getSchema()->getCapability(TcaSchemaCapability::EditLock)->getFieldName()]; + } + + /** + * Returns true is a current record is a delete placeholder + */ + protected function isDeletePlaceholder(): bool + { + return VersionState::tryFrom($this->record['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER; + } + + /** + * Checks if current record is in the "normal" pad of the clipboard + * + * @param string $mode "copy", "cut" or '' for any mode + */ + protected function isRecordInClipboard(string $mode = ''): bool + { + $isSelected = ''; + if ($this->clipboard->current === 'normal' && isset($this->record['uid'])) { + $isSelected = $this->clipboard->isSelected($this->table, $this->record['uid']); + } + return $mode === '' ? !empty($isSelected) : $isSelected === $mode; + } + + /** + * Returns true is a record ia a translation + */ + protected function isRecordATranslation(): bool + { + if (!$this->getSchema()?->isLanguageAware()) { + return false; + } + return (int)$this->record[$this->getSchema()->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName()] !== 0; + } + + /** + * Return true in case the current record is the current backend user + */ + protected function isRecordCurrentBackendUser(): bool + { + return $this->table === 'be_users' && (int)($this->record['uid'] ?? 0) === $this->backendUser->getUserId(); + } + + protected function getIdentifier(): string + { + return $this->record['uid']; + } + + /** + * Returns true if a view link can be built for the record + */ + protected function previewLinkCanBeBuild(): bool + { + return $this->getViewLink() !== ''; + } + + /** + * Returns the configured language field + */ + protected function getLanguageField(): string + { + if (!$this->getSchema()?->isLanguageAware()) { + return ''; + } + return $this->getSchema()->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName(); + } + + protected function getSchema(): ?TcaSchema + { + if ($this->tcaSchemaFactory->has($this->table)) { + return $this->tcaSchemaFactory->get($this->table); + } + return null; + } +} diff --git a/Classes/ContextMenu/ItemProviders/SiteSettingsProvider.php b/Classes/ContextMenu/ItemProviders/SiteSettingsProvider.php new file mode 100644 index 0000000..bf1ddd5 --- /dev/null +++ b/Classes/ContextMenu/ItemProviders/SiteSettingsProvider.php @@ -0,0 +1,138 @@ + [ + 'label' => 'backend.siteconfiguration:contextMenu.editSiteConfiguration', + 'iconIdentifier' => 'actions-window', + 'callbackAction' => 'openSiteConfiguration', + ], + 'editSiteSettings' => [ + 'label' => 'backend.siteconfiguration:contextMenu.editSiteSettings', + 'iconIdentifier' => 'actions-window-cog', + 'callbackAction' => 'openSiteSettings', + ], + ]; + + public function __construct( + private readonly SiteFinder $siteFinder, + private readonly UriBuilder $uriBuilder, + ) { + parent::__construct(); + } + + public function canHandle(): bool + { + // Site configuration module requires admin access + return $this->table === 'pages' && $this->backendUser->isAdmin(); + } + + public function getPriority(): int + { + return 60; + } + + public function addItems(array $items): array + { + $this->initDisabledItems(); + + // Add site items after "edit" item + $localItems = $this->prepareItems(self::ITEMS_CONFIGURATION); + $position = array_search('edit', array_keys($items), true); + if ($position !== false) { + $items = [ + ...array_slice($items, 0, $position + 1, true), + ...$localItems, + ...array_slice($items, $position + 1, null, true), + ]; + } else { + $items = [...$items, ...$localItems]; + } + + return $items; + } + + protected function canRender(string $itemName, string $type): bool + { + if (in_array($itemName, $this->disabledItems, true)) { + return false; + } + + if ($itemName === 'editSiteSettings' || $itemName === 'editSiteConfiguration') { + return $this->canOpenSiteSettings(); + } + + return true; + } + + protected function getAdditionalAttributes(string $itemName): array + { + $pageId = (int)$this->identifier; + try { + $site = $this->siteFinder->getSiteByRootPageId($pageId); + + if ($itemName === 'editSiteSettings') { + return [ + 'data-site-settings-url' => (string)$this->uriBuilder->buildUriFromRoute( + 'site_configuration.editSettings', + ['site' => $site->getIdentifier()] + ), + ]; + } + + if ($itemName === 'editSiteConfiguration') { + return [ + 'data-site-configuration-url' => (string)$this->uriBuilder->buildUriFromRoute( + 'site_configuration.edit', + ['site' => $site->getIdentifier()] + ), + ]; + } + } catch (SiteNotFoundException) { + return []; + } + + return []; + } + + private function canOpenSiteSettings(): bool + { + // Check if this page is a site root + $pageId = (int)$this->identifier; + if ($pageId <= 0) { + return false; + } + + try { + $this->siteFinder->getSiteByRootPageId($pageId); + return true; + } catch (SiteNotFoundException) { + return false; + } + } +} diff --git a/Classes/Controller/AboutController.php b/Classes/Controller/AboutController.php new file mode 100644 index 0000000..ee1ef2a --- /dev/null +++ b/Classes/Controller/AboutController.php @@ -0,0 +1,95 @@ +eventDispatcher->dispatch($event); + $view = $this->moduleTemplateFactory->create($request); + $view->setLayout(ModuleLayout::NORMAL); + $view->assignMultiple([ + 'typo3Info' => $this->typo3Information, + 'typo3Version' => $this->version, + 'donationUrl' => $this->typo3Information::URL_DONATE, + 'trademarkUrl' => $this->typo3Information::URL_TRADEMARK, + 'loadedExtensions' => $this->getLoadedExtensions(), + 'messages' => $event->getMessages(), + 'modules' => $this->moduleProvider->getModules($this->getBackendUser()), + ]); + return $view->renderResponse('About/Index'); + } + + /** + * Fetches a list of all active (loaded) extensions in the current system + */ + protected function getLoadedExtensions(): array + { + $extensions = []; + foreach ($this->packageManager->getActivePackages() as $package) { + // Skip system extensions + if ($package->getPackageMetaData()->isFrameworkType()) { + continue; + } + $extensions[] = [ + 'key' => $package->getPackageKey(), + 'title' => $package->getPackageMetaData()->getTitle(), + 'authors' => $package->getValueFromComposerManifest('authors'), + ]; + } + return $extensions; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/AbstractFormEngineAjaxController.php b/Classes/Controller/AbstractFormEngineAjaxController.php new file mode 100644 index 0000000..1287e5a --- /dev/null +++ b/Classes/Controller/AbstractFormEngineAjaxController.php @@ -0,0 +1,80 @@ +addJavaScriptModuleInstruction($module); + } + } + + /** + * Resolve a CSS file position, possibly prefixed with 'EXT:' + * + * @param string $stylesheetFile Given file, possibly prefixed with EXT: + * @return string URL to file + */ + protected function getRelativePathToStylesheetFile(string $stylesheetFile): string + { + return (string)PathUtility::getSystemResourceUri($stylesheetFile); + } + + /** + * Parse a language file and get a label/value array from it. + * + * @param string $file EXT:path/to/file + * @return array Label/value array + */ + protected function getLabelsFromLocalizationFile(string $file): array + { + $languageService = $this->getLanguageService() ?? GeneralUtility::makeInstance(LanguageServiceFactory::class)->create('en'); + return $languageService->getLabelsFromResource($file); + } + + protected function getLanguageService(): ?LanguageService + { + return $GLOBALS['LANG'] ?? null; + } +} diff --git a/Classes/Controller/AbstractLinkBrowserController.php b/Classes/Controller/AbstractLinkBrowserController.php new file mode 100644 index 0000000..cdebf58 --- /dev/null +++ b/Classes/Controller/AbstractLinkBrowserController.php @@ -0,0 +1,556 @@ + + */ + protected array $linkHandlers = []; + + /** + * All parts of the current link. + * Comprised of url information and additional link parameters. + * + * @var array + */ + protected array $currentLinkParts = []; + + /** + * Link handler responsible for the current active link + */ + protected ?LinkHandlerInterface $currentLinkHandler = null; + + /** + * The ID of the currently active link handler + */ + protected string $currentLinkHandlerId; + + /** + * Link handler to be displayed + */ + protected ?LinkHandlerInterface $displayedLinkHandler = null; + + /** + * The ID of the displayed link handler + * This is read from the 'act' GET parameter + */ + protected string $displayedLinkHandlerId = ''; + + /** + * List of available link attribute fields + * + * @var string[] + */ + protected array $linkAttributeFields = []; + + /** + * Values of the link attributes + * + * @var string[] + */ + protected array $linkAttributeValues = []; + + protected array $parameters; + + protected DependencyOrderingService $dependencyOrderingService; + protected PageRenderer $pageRenderer; + protected UriBuilder $uriBuilder; + protected ExtensionConfiguration $extensionConfiguration; + protected BackendViewFactory $backendViewFactory; + protected EventDispatcherInterface $eventDispatcher; + + public function injectDependencyOrderingService(DependencyOrderingService $dependencyOrderingService): void + { + $this->dependencyOrderingService = $dependencyOrderingService; + } + + public function injectPageRenderer(PageRenderer $pageRenderer): void + { + $this->pageRenderer = $pageRenderer; + } + + public function injectUriBuilder(UriBuilder $uriBuilder): void + { + $this->uriBuilder = $uriBuilder; + } + + public function injectExtensionConfiguration(ExtensionConfiguration $extensionConfiguration): void + { + $this->extensionConfiguration = $extensionConfiguration; + } + + public function injectBackendViewFactory(BackendViewFactory $backendViewFactory): void + { + $this->backendViewFactory = $backendViewFactory; + } + + public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->eventDispatcher = $eventDispatcher; + } + + abstract public function getConfiguration(): array; + + abstract protected function initDocumentTemplate(): void; + + abstract protected function getCurrentPageId(): int; + + /** + * Injects the request object for the current request or subrequest + * As this controller goes only through the main() method, it is rather simple for now + * + * @param ServerRequestInterface $request the current request + * @return ResponseInterface the response with the content + */ + public function mainAction(ServerRequestInterface $request): ResponseInterface + { + $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $this->getLanguageService()); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_misc.xlf'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf'); + + $this->initVariables($request); + $this->loadLinkHandlers(); + $this->initCurrentUrl(); + + $menuData = $this->buildMenuArray($request); + if ($this->displayedLinkHandler instanceof LinkHandlerViewProviderInterface) { + $view = $this->displayedLinkHandler->createView($this->backendViewFactory, $request); + } else { + $view = $this->backendViewFactory->create($request, ['typo3/cms-backend']); + } + if ($this->displayedLinkHandler instanceof LinkHandlerVariableProviderInterface) { + $this->displayedLinkHandler->initializeVariables($request); + } + $renderLinkAttributeFields = $this->renderLinkAttributeFields($view); + if (!empty($this->currentLinkParts)) { + $this->renderCurrentUrl($view); + } + if (method_exists($this->displayedLinkHandler, 'setView')) { + $this->displayedLinkHandler->setView($view); + } + $view->assignMultiple([ + 'initialNavigationWidth' => $this->getBackendUser()->uc['selector']['navigation']['width'] ?? 250, + 'menuItems' => $menuData, + 'linkAttributes' => $renderLinkAttributeFields, + 'contentOnly' => $request->getQueryParams()['contentOnly'] ?? false, + ]); + $content = $this->displayedLinkHandler->render($request); + if (empty($content)) { + // @todo: b/w compat layer for link handler that don't render full view but return empty + // string instead. This case is unfortunate and should be removed if it gives + // headaches at some point. If so, above method_exists($this->displayedLinkHandler, 'setView') + // should be removed and setView() method should be made mandatory, or the entire + // construct should be refactored a bit. + $content = $view->render(); + } + $this->initDocumentTemplate(); + $this->pageRenderer->setTitle($this->getLanguageService()->sL( + 'LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:linkBrowser' + )); + if ($request->getQueryParams()['contentOnly'] ?? false) { + return new HtmlResponse($content); + } + $this->pageRenderer->setBodyContent('getBodyTagAttributes(), true, true) . '>' . $content); + return $this->pageRenderer->renderResponse($request); + } + + /** + * @return array{act: string, P: array} Array of parameters which have to be added to URLs + */ + public function getUrlParameters(?array $overrides = null): array + { + return [ + 'act' => $overrides['act'] ?? $this->displayedLinkHandlerId, + 'P' => $overrides['P'] ?? $this->parameters, + ]; + } + + public function getParameters(): array + { + return $this->parameters; + } + + protected function initVariables(ServerRequestInterface $request): void + { + $queryParams = $request->getQueryParams(); + $this->displayedLinkHandlerId = $queryParams['act'] ?? ''; + $this->parameters = $queryParams['P'] ?? []; + $this->linkAttributeValues = $queryParams['linkAttributes'] ?? []; + + $pageTsConfig = BackendUtility::getPagesTSconfig((int)($this->parameters['pid'] ?? 0)); + $handlerId = $this->displayedLinkHandlerId ?: 'page'; + + if (empty($this->linkAttributeValues['target'])) { + $defaultTarget = $pageTsConfig['TCEMAIN.']['linkHandler.'][$handlerId . '.']['target.']['default'] + ?? $pageTsConfig['TCEMAIN.']['linkHandler.']['properties.']['target.']['default'] + ?? ''; + if (!empty($defaultTarget)) { + $this->linkAttributeValues['target'] = $defaultTarget; + } + } + + if (empty($this->linkAttributeValues['class'])) { + $defaultCssClass = $pageTsConfig['TCEMAIN.']['linkHandler.'][$handlerId . '.']['cssClass.']['default'] + ?? $pageTsConfig['TCEMAIN.']['linkHandler.']['properties.']['cssClass.']['default'] + ?? ''; + if (!empty($defaultCssClass)) { + $this->linkAttributeValues['class'] = $defaultCssClass; + } + } + } + + /** + * @throws \UnexpectedValueException + */ + protected function loadLinkHandlers(): void + { + $linkHandlers = $this->getLinkHandlers(); + if (empty($linkHandlers)) { + throw new \UnexpectedValueException('No link handlers are configured. Check page TSconfig TCEMAIN.linkHandler.', 1442787911); + } + + $lang = $this->getLanguageService(); + foreach ($linkHandlers as $identifier => $configuration) { + $identifier = rtrim($identifier, '.'); + if ($identifier === 'properties') { + continue; + } + + if (empty($configuration['handler'])) { + throw new \UnexpectedValueException(sprintf('Missing handler for link handler "%1$s", check page TSconfig TCEMAIN.linkHandler.%1$s.handler', $identifier), 1494579849); + } + + /** @var LinkHandlerInterface $handler */ + $handler = GeneralUtility::makeInstance($configuration['handler']); + $handler->initialize( + $this, + $identifier, + $configuration['configuration.'] ?? [] + ); + + $label = !empty($configuration['label']) ? $lang->sL($configuration['label']) : ''; + $label = $label ?: $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:error.linkHandlerTitleMissing'); + $this->linkHandlers[$identifier] = [ + 'handlerInstance' => $handler, + 'label' => $label, + 'displayBefore' => isset($configuration['displayBefore']) ? GeneralUtility::trimExplode(',', $configuration['displayBefore']) : [], + 'displayAfter' => isset($configuration['displayAfter']) ? GeneralUtility::trimExplode(',', $configuration['displayAfter']) : [], + 'scanBefore' => isset($configuration['scanBefore']) ? GeneralUtility::trimExplode(',', $configuration['scanBefore']) : [], + 'scanAfter' => isset($configuration['scanAfter']) ? GeneralUtility::trimExplode(',', $configuration['scanAfter']) : [], + 'addParams' => $configuration['addParams'] ?? '', + ]; + } + } + + /** + * Reads the configured link handlers from page TSconfig + * + * @return array + */ + protected function getLinkHandlers(): array + { + $linkHandlers = (array)(BackendUtility::getPagesTSconfig($this->getCurrentPageId())['TCEMAIN.']['linkHandler.'] ?? []); + return $this->eventDispatcher + ->dispatch(new ModifyLinkHandlersEvent($linkHandlers, $this->currentLinkParts)) + ->getLinkHandlers(); + } + + /** + * Initialize $this->currentLinkParts and $this->currentLinkHandler + */ + protected function initCurrentUrl(): void + { + if (empty($this->currentLinkParts)) { + return; + } + + $orderedHandlers = $this->dependencyOrderingService->orderByDependencies($this->linkHandlers, 'scanBefore', 'scanAfter'); + + // find responsible handler for current link + foreach ($orderedHandlers as $key => $configuration) { + /** @var LinkHandlerInterface $handler */ + $handler = $configuration['handlerInstance']; + if ($handler->canHandleLink($this->currentLinkParts)) { + $this->currentLinkHandler = $handler; + $this->currentLinkHandlerId = $key; + break; + } + } + // reset the link if we have no handler for it + if (!$this->currentLinkHandler) { + $this->currentLinkParts = []; + } + + // overwrite any preexisting + foreach ($this->currentLinkParts as $key => $part) { + if ($key !== 'url') { + $this->linkAttributeValues[$key] = $part; + } + } + } + + /** + * Add the currently set Link URL to the view + */ + protected function renderCurrentUrl(ViewInterface $view): void + { + $view->assign('currentLink', $this->currentLinkHandler->formatCurrentUrl()); + } + + /** + * Returns an array definition of the top menu + * + * @return array[] + */ + protected function buildMenuArray(ServerRequestInterface $request): array + { + $allowedItems = $this->getAllowedItems(); + if ($this->displayedLinkHandlerId && !in_array($this->displayedLinkHandlerId, $allowedItems, true)) { + $this->displayedLinkHandlerId = ''; + } + + $allowedHandlers = array_flip($allowedItems); + $menuDef = []; + foreach ($this->linkHandlers as $identifier => $configuration) { + if (!isset($allowedHandlers[$identifier])) { + continue; + } + + /** @var LinkHandlerInterface $handlerInstance */ + $handlerInstance = $configuration['handlerInstance']; + $isActive = $this->displayedLinkHandlerId === $identifier || (!$this->displayedLinkHandlerId && $handlerInstance === $this->currentLinkHandler); + if ($isActive) { + $this->displayedLinkHandler = $handlerInstance; + if (!$this->displayedLinkHandlerId) { + $this->displayedLinkHandlerId = $this->currentLinkHandlerId; + } + } + + $menuDef[$identifier] = [ + 'isActive' => $isActive, + 'label' => $configuration['label'], + 'url' => $this->uriBuilder->buildUriFromRequest($request, $this->getUrlParameters(['act' => $identifier])), + 'addParams' => $configuration['addParams'] ?? '', + 'before' => $configuration['displayBefore'], + 'after' => $configuration['displayAfter'], + ]; + } + + $menuDef = $this->dependencyOrderingService->orderByDependencies($menuDef); + + // if there is no active tab + if (!$this->displayedLinkHandler) { + // empty the current link + $this->currentLinkParts = []; + $this->currentLinkHandler = null; + // select first tab + $this->displayedLinkHandlerId = (string)array_key_first($menuDef); + $this->displayedLinkHandler = $this->linkHandlers[$this->displayedLinkHandlerId]['handlerInstance']; + $menuDef[$this->displayedLinkHandlerId]['isActive'] = true; + } + + return $menuDef; + } + + /** + * @return string[] + */ + protected function getAllowedItems(): array + { + $allowedItems = $this->eventDispatcher + ->dispatch(new ModifyAllowedItemsEvent(array_keys($this->linkHandlers), $this->currentLinkParts)) + ->getAllowedItems(); + + if (isset($this->parameters['params']['allowedTypes'])) { + $allowedItems = array_intersect($allowedItems, GeneralUtility::trimExplode(',', $this->parameters['params']['allowedTypes'], true)); + } elseif (isset($this->parameters['params']['blindLinkOptions'])) { + // @todo Deprecate this option + $allowedItems = array_diff($allowedItems, GeneralUtility::trimExplode(',', $this->parameters['params']['blindLinkOptions'], true)); + } + + return $allowedItems; + } + + /** + * @return string[] + */ + protected function getAllowedLinkAttributes(): array + { + $allowedLinkAttributes = $this->displayedLinkHandler->getLinkAttributes(); + + if (isset($this->parameters['params']['allowedOptions'])) { + $allowedLinkAttributes = array_intersect($allowedLinkAttributes, GeneralUtility::trimExplode(',', $this->parameters['params']['allowedOptions'], true)); + } elseif (isset($this->parameters['params']['blindLinkFields'])) { + // @todo Deprecate this option + $allowedLinkAttributes = array_diff($allowedLinkAttributes, GeneralUtility::trimExplode(',', $this->parameters['params']['blindLinkFields'], true)); + } + + return $allowedLinkAttributes; + } + + /** + * Renders the link attributes for the selected link handler + */ + protected function renderLinkAttributeFields(ViewInterface $view): string + { + $fieldRenderingDefinitions = $this->getLinkAttributeFieldDefinitions(); + $fieldRenderingDefinitions = $this->displayedLinkHandler->modifyLinkAttributes($fieldRenderingDefinitions); + $this->linkAttributeFields = $this->getAllowedLinkAttributes(); + $content = ''; + foreach ($this->linkAttributeFields as $attribute) { + $content .= $fieldRenderingDefinitions[$attribute] ?? ''; + } + $view->assign('allowedLinkAttributes', array_combine($this->linkAttributeFields, $this->linkAttributeFields)); + + // add update button if appropriate + if (!empty($this->currentLinkParts) && $this->displayedLinkHandler === $this->currentLinkHandler && $this->currentLinkHandler->isUpdateSupported()) { + $view->assign('showUpdateParametersButton', true); + } + return $content; + } + + /** + * Create an array of link attribute field rendering definitions + * + * @return string[] + */ + protected function getLinkAttributeFieldDefinitions(): array + { + $lang = $this->getLanguageService(); + + $fieldRenderingDefinitions = []; + $fieldRenderingDefinitions['target'] = ' + +
+ + + + ' . $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:top') . ' + ' . $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:newWindow') . ' + +
'; + + $fieldRenderingDefinitions['title'] = ' + +
+ + +
'; + + $fieldRenderingDefinitions['class'] = ' + +
+ + +
'; + + $fieldRenderingDefinitions['params'] = ' + +
+ + +
'; + + $fieldRenderingDefinitions['rel'] = ' + +
+ + +
'; + + $fieldRenderingDefinitions['download'] = ' + +
+ +
'; + + return $fieldRenderingDefinitions; + } + + /** + * @return string[] Array of body-tag attributes + */ + protected function getBodyTagAttributes(): array + { + $attributes = $this->displayedLinkHandler->getBodyTagAttributes(); + return array_merge( + $attributes, + [ + 'data-linkbrowser-parameters' => json_encode($this->parameters) ?: '', + 'data-linkbrowser-attribute-fields' => json_encode(array_values($this->linkAttributeFields)) ?: '', + ] + ); + } + + protected function getDisplayedLinkHandlerId(): string + { + return $this->displayedLinkHandlerId; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/AbstractMfaController.php b/Classes/Controller/AbstractMfaController.php new file mode 100644 index 0000000..727a11b --- /dev/null +++ b/Classes/Controller/AbstractMfaController.php @@ -0,0 +1,115 @@ +mfaProviderRegistry = $mfaProviderRegistry; + } + + /** + * Main action for handling the request and returning the response + */ + abstract public function handleRequest(ServerRequestInterface $request): ResponseInterface; + + protected function isActionAllowed(string $action): bool + { + return in_array($action, $this->allowedActions, true); + } + + protected function isProviderAllowed(string $identifier): bool + { + return isset($this->allowedProviders[$identifier]); + } + + protected function isValidIdentifier(string $identifier): bool + { + return $identifier !== '' + && $this->isProviderAllowed($identifier) + && $this->mfaProviderRegistry->hasProvider($identifier); + } + + /** + * Initialize MFA configuration based on TSconfig and global configuration + */ + protected function initializeMfaConfiguration(): void + { + $backendUser = $this->getBackendUser(); + $this->mfaTsConfig = $backendUser->getTSConfig()['auth.']['mfa.'] ?? []; + $this->mfaRequired = $backendUser->isMfaSetupRequired(); + + // Set up allowed providers based on user TSconfig and user groupData + $this->allowedProviders = array_filter($this->mfaProviderRegistry->getProviders(), function (string $identifier) use ($backendUser): bool { + return $backendUser->check('mfa_providers', $identifier) + && !GeneralUtility::inList(($this->mfaTsConfig['disableProviders'] ?? ''), $identifier); + }, ARRAY_FILTER_USE_KEY); + } + + /** + * Get the recommended provider + */ + protected function getRecommendedProvider(): ?MfaProviderManifestInterface + { + $recommendedProviderIdentifier = (string)($this->mfaTsConfig['recommendedProvider'] ?? ''); + // Check if valid and allowed to be default provider, which is obviously a prerequisite + if (!$this->isValidIdentifier($recommendedProviderIdentifier) + || !$this->mfaProviderRegistry->getProvider($recommendedProviderIdentifier)->isDefaultProviderAllowed() + ) { + // If the provider, defined in user TSconfig is not valid or is not set, check the globally defined + $recommendedProviderIdentifier = (string)($GLOBALS['TYPO3_CONF_VARS']['BE']['recommendedMfaProvider'] ?? ''); + if (!$this->isValidIdentifier($recommendedProviderIdentifier) + || !$this->mfaProviderRegistry->getProvider($recommendedProviderIdentifier)->isDefaultProviderAllowed() + ) { + // If also not valid or not set, return + return null; + } + } + return $this->mfaProviderRegistry->getProvider($recommendedProviderIdentifier); + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/AjaxLoginController.php b/Classes/Controller/AjaxLoginController.php new file mode 100644 index 0000000..da5d911 --- /dev/null +++ b/Classes/Controller/AjaxLoginController.php @@ -0,0 +1,158 @@ +isAuthorizedBackendSession()) { + $result = ['success' => true]; + if ($this->hasLoginBeenProcessed($request)) { + /** @var BackendFormProtection $formProtection */ + $formProtection = $this->formProtectionFactory->createFromRequest($request); + $formProtection->setSessionTokenFromRegistry(); + $formProtection->persistSessionToken(); + } + } else { + $result = ['success' => false]; + } + return new JsonResponse(['login' => $result]); + } + + /** + * Logs out the current BE user + */ + public function logoutAction(ServerRequestInterface $request): ResponseInterface + { + $backendUser = $this->getBackendUser(); + $backendUser->logoff(); + return new JsonResponse([ + 'logout' => [ + 'success' => !isset($backendUser->user['uid']), + ], + ]); + } + + public function preflightAction(ServerRequestInterface $request): ResponseInterface + { + $headers = $request->getHeaders(); + return new JsonResponse([ + 'capabilities' => [ + 'cookie' => !empty($request->getCookieParams()), + // using legacy `Referer` (sic!) header name + 'referrer' => array_filter($headers['referer'] ?? []) !== [], + ], + ]); + } + + /** + * Handles the actual session refresh, more specifically it defines the response. + * The session refresh has been performed inside the BackendUserAuthenticator middleware. + * If that was successful, we have a BE user and report that information as response. + */ + public function refreshAction(ServerRequestInterface $request): ResponseInterface + { + $backendUser = $this->getBackendUser(); + return new JsonResponse([ + 'refresh' => [ + 'success' => isset($backendUser->user['uid']), + ], + ]); + } + + /** + * Checks if the user session is expired yet + */ + public function isTimedOutAction(ServerRequestInterface $request): ResponseInterface + { + $session = [ + 'timed_out' => false, + 'will_time_out' => false, + 'locked' => false, + ]; + $backendUser = $this->getBackendUser(); + if ($this->lockService->isLocked()) { + $session['locked'] = true; + } elseif (!isset($backendUser->user['uid'])) { + $session['timed_out'] = true; + } else { + $sessionManager = UserSessionManager::create('BE'); + // If 120 seconds from now is later than the session timeout, we need to show the refresh dialog. + // 120 is somewhat arbitrary to allow for a little room during the countdown and load times, etc. + $session['will_time_out'] = $sessionManager->willExpire($backendUser->getSession(), 120); + } + return new JsonResponse(['login' => $session]); + } + + /** + * Checks if a user is logged in and the session is active. + * + * @return bool + */ + protected function isAuthorizedBackendSession() + { + $backendUser = $this->getBackendUser(); + if ($backendUser === null) { + return false; + } + return isset($backendUser->user['uid']); + } + + /** + * Check whether the user was already authorized or not + */ + protected function hasLoginBeenProcessed(ServerRequestInterface $request): bool + { + $loginFormData = $this->getBackendUser()->getLoginFormData($request); + return LoginType::tryFrom($loginFormData['status'] ?? '') === LoginType::LOGIN && !empty($loginFormData['uname']) && !empty($loginFormData['uident']); + } + + protected function getBackendUser(): ?BackendUserAuthentication + { + return $GLOBALS['BE_USER'] ?? null; + } +} diff --git a/Classes/Controller/BackendController.php b/Classes/Controller/BackendController.php new file mode 100644 index 0000000..ff88cef --- /dev/null +++ b/Classes/Controller/BackendController.php @@ -0,0 +1,389 @@ +getBackendUser(); + $pageRenderer = $this->pageRenderer; + // apply nonce hint for elements that are shown in a modal + $pageRenderer->setApplyNonceHint(true); + + $this->setUpBasicPageRendererForBackend($pageRenderer, $this->extensionConfiguration, $request, $this->getLanguageService()); + + $javaScriptRenderer = $pageRenderer->getJavaScriptRenderer(); + $javaScriptRenderer->addGlobalAssignment(['window' => [ + 'name' => 'typo3-backend', // reset window name to a standardized value + 'opener' => null, // remove any previously set opener value + ]]); + $javaScriptRenderer->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/login-refresh.js') + ->invoke('initialize', [ + 'intervalTime' => MathUtility::forceIntegerInRange((int)$GLOBALS['TYPO3_CONF_VARS']['BE']['sessionTimeout'] - 60, 60), + 'requestTokenUrl' => (string)$this->uriBuilder->buildUriFromRoute('login_request_token'), + 'loginFramesetUrl' => (string)$this->uriBuilder->buildUriFromRoute('login_frameset'), + 'logoutUrl' => (string)$this->uriBuilder->buildUriFromRoute('logout'), + ]) + ); + $javaScriptRenderer->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/broadcast-service.js')->invoke('listen') + ); + $javaScriptRenderer->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/hotkeys/negotiator.js') + ); + $javaScriptRenderer->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/hotkeys.js') + ); + $javaScriptRenderer->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/user-settings-manager.js') + ); + // load the storage API and fill the UC into the PersistentStorage, so no additional AJAX call is needed + $javaScriptRenderer->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/storage/persistent.js') + ->invoke('load', $backendUser->uc) + ); + // Initialize bookmark store with server data if bookmarks are enabled + if ($this->bookmarkService->isEnabled()) { + $javaScriptRenderer->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/bookmark/bookmark-store.js') + ->invoke('initialize', $this->bookmarkService->getBookmarks(), $this->bookmarkService->getGroups()) + ); + } + $javaScriptRenderer->addGlobalAssignment([ + 'TYPO3' => [ + 'configuration' => [ + 'username' => htmlspecialchars($backendUser->user['username']), + 'showRefreshLoginPopup' => (bool)($GLOBALS['TYPO3_CONF_VARS']['BE']['showRefreshLoginPopup'] ?? false), + ], + ], + ]); + $javaScriptRenderer->includeAllImports(); + + // @todo: This loads a ton of labels into JS. This should be reviewed what is really needed. + // This could happen when the localization API gets an overhaul. + $pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf'); + $pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_misc.xlf'); + $pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf'); + $pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_settingseditor.xlf'); + $pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf'); + + // @todo: We can not put this into the template since PageRendererViewHelper does not deal with namespace in addInlineSettings argument + $pageRenderer->addInlineSetting('ShowItem', 'moduleUrl', (string)$this->uriBuilder->buildUriFromRoute('show_item')); + $pageRenderer->addInlineSetting('Resource', 'thumbnailUrl', (string)$this->uriBuilder->buildUriFromRoute('resource_request_thumbnail')); + $pageRenderer->addInlineSetting('RecordHistory', 'moduleUrl', (string)$this->uriBuilder->buildUriFromRoute('record_history')); + $pageRenderer->addInlineSetting('NewRecord', 'moduleUrl', (string)$this->uriBuilder->buildUriFromRoute('db_new')); + $pageRenderer->addInlineSetting('FormEngine', 'moduleUrl', (string)$this->uriBuilder->buildUriFromRoute('record_edit')); + $pageRenderer->addInlineSetting('RecordCommit', 'moduleUrl', (string)$this->uriBuilder->buildUriFromRoute('tce_db')); + $pageRenderer->addInlineSetting('FileCommit', 'moduleUrl', (string)$this->uriBuilder->buildUriFromRoute('tce_file')); + $pageRenderer->addInlineSetting('Clipboard', 'moduleUrl', (string)$this->uriBuilder->buildUriFromRoute('clipboard_process')); + $pageRenderer->addInlineSetting('Wizards', 'elementBrowserUrl', (string)$this->uriBuilder->buildUriFromRoute('wizard_element_browser')); + + // Needed for FormEngine manipulation (date picker) and DateTime components + $pageRenderer->addInlineSetting(null, 'DateConfiguration', $this->dateConfigurationFactory->getConfiguration('javascript')); + + $typo3Version = 'TYPO3 CMS ' . $this->typo3Version->getVersion(); + $title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ? $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . ' [' . $typo3Version . ']' : $typo3Version; + $pageRenderer->setTitle($title); + + $sidebarContext = new SidebarComponentContext($request, $backendUser); + $sidebar = $this->sidebarFactory->create($sidebarContext); + $view = $this->viewFactory->create($request); + $this->assignTopbarDetailsToView($request, $view, $sidebar); + $startupModule = $this->getStartupModule($request); + $noModuleAccess = $startupModule[0] === null && empty($this->moduleProvider->getModulesForModuleMenu($backendUser)); + $view->assignMultiple([ + 'startupModule' => $startupModule, + 'noModuleAccess' => $noModuleAccess, + 'workspaceAccessDenied' => $noModuleAccess && $backendUser->workspace === -99, + 'entryPoint' => $this->backendEntryPointResolver->getPathFromRequest($request), + 'stateTracker' => (string)$this->uriBuilder->buildUriFromRoute('state-tracker'), + 'sitename' => $title, + 'sitenameFirstInBackendTitle' => ($backendUser->uc['backendTitleFormat'] ?? '') === 'sitenameFirst', + 'sidebar' => $sidebar->render(), + ]); + $this->eventDispatcher->dispatch(new BeforeBackendPageRenderEvent($view, $javaScriptRenderer, $pageRenderer)); + $content = $view->render('Backend/Main'); + $content = $this->eventDispatcher->dispatch(new AfterBackendPageRenderEvent($content, $view))->getContent(); + $pageRenderer->addBodyContent('' . $content); + return $pageRenderer->renderResponse($request); + } + + /** + * Returns the main module menu as json encoded HTML string. Used when + * "update signals" request a menu reload, e.g. when an extension is loaded + * that brings new main modules. + */ + public function getModuleMenu(ServerRequestInterface $request): ResponseInterface + { + $sidebarContext = new SidebarComponentContext($request, $this->getBackendUser()); + $component = $this->sidebarFactory->create($sidebarContext)->getComponentByIdentifier('module-menu'); + return new JsonResponse(['menu' => $component?->getResult($sidebarContext)->html]); + } + + /** + * Returns the toolbar as json encoded HTML string. Used when + * "update signals" request a toolbar reload, e.g. when an extension is loaded. + */ + public function getTopbar(ServerRequestInterface $request): ResponseInterface + { + $sidebar = $this->sidebarFactory->create(new SidebarComponentContext($request, $this->getBackendUser())); + $view = $this->viewFactory->create($request); + $this->assignTopbarDetailsToView($request, $view, $sidebar); + return new JsonResponse(['topbar' => $view->render('Backend/Topbar')]); + } + + /** + * Renders the topbar, containing the backend logo, sitename etc. + */ + protected function assignTopbarDetailsToView(ServerRequestInterface $request, ViewInterface $view, Sidebar $sidebar): void + { + // Extension Configuration to find the TYPO3 logo in the left corner + $extConf = $this->extensionConfiguration->get('backend'); + $logoPath = ''; + $logoUrl = ''; + $logoWidth = 22; + $logoHeight = 22; + if (!empty($extConf['backendLogo'])) { + $configuredLogo = ltrim($extConf['backendLogo'], '/'); + $customBackendLogo = GeneralUtility::getFileAbsFileName($configuredLogo); + if ($customBackendLogo !== '' && file_exists($customBackendLogo)) { + $logoPath = $customBackendLogo; + $logoUrl = (string)PathUtility::getSystemResourceUri($configuredLogo, $request); + // set width/height for custom logo + $imageInfo = GeneralUtility::makeInstance(ImageInfo::class, $logoPath); + $logoWidth = $imageInfo->getWidth() ?: $logoWidth; + $logoHeight = $imageInfo->getHeight() ?: $logoHeight; + + // High-resolution? + if (str_contains($logoPath, '@2x.')) { + $logoWidth /= 2; + $logoHeight /= 2; + } + } + } + // if no custom logo was set or the path is invalid, use the original one + if ($logoPath === '') { + $logoUrl = (string)PathUtility::getSystemResourceUri('EXT:backend/Resources/Public/Images/typo3_logo_orange.svg', $request); + } + $view->assign('sidebar', $sidebar); + $view->assign('logoUrl', $logoUrl); + $view->assign('logoWidth', $logoWidth); + $view->assign('logoHeight', $logoHeight); + $view->assign('applicationVersion', $this->typo3Version->getVersion()); + $view->assign('siteName', $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']); + $view->assign('toolbarItems', $this->getToolbarItems($request)); + $view->assign('isImpersonated', $this->getBackendUser()->getOriginalUserIdWhenInSwitchUserMode() !== null); + } + + /** + * @return ToolbarItemInterface[] + */ + protected function getToolbarItems(ServerRequestInterface $request): array + { + return array_map(static function (ToolbarItemInterface $toolbarItem) use ($request): ToolbarItemInterface { + if ($toolbarItem instanceof RequestAwareToolbarItemInterface) { + $toolbarItem->setRequest($request); + } + return $toolbarItem; + }, array_filter( + $this->toolbarItemsRegistry->getToolbarItems(), + static fn(ToolbarItemInterface $toolbarItem): bool => $toolbarItem->checkAccess() + )); + } + + /** + * Sets the startup module from either "redirect" GET parameters or user configuration. + * + * @return array{?string, ?string} + */ + protected function getStartupModule(ServerRequestInterface $request): array + { + $startModule = null; + $startModuleIdentifier = null; + $inaccessibleRedirectModule = null; + $moduleParameters = []; + try { + $redirect = RouteRedirect::createFromRequest($request); + if ($redirect !== null && $request->getMethod() === 'GET') { + // Only redirect to existing non-ajax routes with no restriction to a specific method + $router = GeneralUtility::makeInstance(Router::class); + $redirect->resolve($router); + $module = $router->getRoute($redirect->getName())?->getOption('module'); + if ($module instanceof ModuleInterface === false + || $this->moduleProvider->accessGranted($module->getIdentifier(), $this->getBackendUser()) + ) { + // Only add start module from request in case user has access or it's a no module route, + // e.g. to FormEngine where permissions are checked by the corresponding component. + // Access might temporarily be blocked. e.g. due to being in a workspace. + $startModuleIdentifier = $redirect->getName(); + $moduleParameters = $redirect->getParameters(); + } elseif ($this->moduleProvider->isModuleRegistered($module->getIdentifier())) { + // A redirect is set, however, the user is not allowed to access the module. + // Store the requested module to later inform the user about the forced redirect. + $inaccessibleRedirectModule = $this->moduleProvider->getModule($module->getIdentifier()); + } + } + } finally { + // No valid redirect, check for the start module + if (!$startModuleIdentifier) { + $backendUser = $this->getBackendUser(); + // start module on first login, will be removed once used the first time + if (isset($backendUser->uc['startModuleOnFirstLogin'])) { + $startModuleIdentifier = $backendUser->uc['startModuleOnFirstLogin']; + unset($backendUser->uc['startModuleOnFirstLogin']); + $backendUser->writeUC(); + } elseif (isset($backendUser->uc['startModule']) && $this->moduleProvider->accessGranted($backendUser->uc['startModule'], $backendUser)) { + $startModuleIdentifier = $backendUser->uc['startModule']; + } elseif ($firstAccessibleModule = $this->moduleProvider->getFirstAccessibleModule($backendUser)) { + $startModuleIdentifier = $firstAccessibleModule->getIdentifier(); + } + + // check if the start module has additional parameters, so a redirect to a specific + // action is possible + if (is_string($startModuleIdentifier) && str_contains($startModuleIdentifier, '->')) { + [$startModuleIdentifier, $startModuleParameters] = explode('->', $startModuleIdentifier, 2); + // if no GET parameters are set, check if there are parameters given from the UC + if (!$moduleParameters && $startModuleParameters) { + $moduleParameters = $startModuleParameters; + } + } + } + } + if ($startModuleIdentifier) { + if ($this->moduleProvider->isModuleRegistered($startModuleIdentifier)) { + // startModuleIdentifier may be an alias, resolve original module + $startModule = $this->moduleProvider->getModule($startModuleIdentifier, $this->getBackendUser()); + $startModuleIdentifier = $startModule?->getIdentifier(); + } + if (is_array($moduleParameters)) { + $parameters = $moduleParameters; + } else { + $parameters = []; + parse_str($moduleParameters, $parameters); + } + try { + $deepLink = $this->uriBuilder->buildUriFromRoute($startModuleIdentifier, $parameters); + if ($startModule !== null && $inaccessibleRedirectModule !== null) { + $this->enqueueRedirectMessage($inaccessibleRedirectModule, $startModule); + } + return [$startModuleIdentifier, (string)$deepLink]; + } catch (RouteNotFoundException $e) { + // It might be, that the user does not have access to the + // $startModule, e.g. for modules with workspace restrictions. + } + } + return [null, null]; + } + + protected function enqueueRedirectMessage(ModuleInterface $requestedModule, ModuleInterface $redirectedModule): void + { + $languageService = $this->getLanguageService(); + $this->flashMessageService + ->getMessageQueueByIdentifier(FlashMessageQueue::NOTIFICATION_QUEUE) + ->enqueue( + new FlashMessage( + sprintf( + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:module.noAccess.message'), + $languageService->sL($redirectedModule->getTitle()), + $languageService->sL($requestedModule->getTitle()) + ), + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:module.noAccess.title'), + ContextualFeedbackSeverity::INFO, + true + ) + ); + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/BookmarkController.php b/Classes/Controller/BookmarkController.php new file mode 100644 index 0000000..0b02293 --- /dev/null +++ b/Classes/Controller/BookmarkController.php @@ -0,0 +1,319 @@ + true, + 'bookmarks' => $this->bookmarkService->getBookmarks(), + 'groups' => $this->bookmarkService->getGroups(), + ]); + } + + public function createAction(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + $routeIdentifier = $parsedBody['routeIdentifier'] ?? ''; + $arguments = $parsedBody['arguments'] ?? ''; + + if ($routeIdentifier === '') { + return $this->errorResponse( + 'core.bookmarks:error.missingRoute.message', + 400 + ); + } + + if ($this->bookmarkService->hasBookmark($routeIdentifier, $arguments)) { + return $this->errorResponse( + 'core.bookmarks:error.createFailed.message' + ); + } + + $bookmarkName = $parsedBody['displayName'] ?? ''; + $bookmarkId = $this->bookmarkService->createBookmark($routeIdentifier, $arguments, $bookmarkName); + + if ($bookmarkId === false) { + return $this->errorResponse( + 'core.bookmarks:error.createFailed.message', + 500 + ); + } + + $bookmark = $this->bookmarkService->getBookmark($bookmarkId); + + return new JsonResponse([ + 'success' => true, + 'bookmark' => $bookmark, + ], 201); + } + + public function updateAction(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + $bookmarkId = (int)($parsedBody['bookmarkId'] ?? 0); + $bookmarkTitle = trim($parsedBody['bookmarkTitle'] ?? ''); + // Group ID can be int (system group, including negative for global) or string (user-created UUID) + $bookmarkGroupId = $parsedBody['bookmarkGroup'] ?? 0; + if (is_numeric($bookmarkGroupId)) { + $bookmarkGroupId = (int)$bookmarkGroupId; + } + + if ($bookmarkId === 0) { + return $this->errorResponse( + 'core.bookmarks:error.missingBookmarkId.message', + 400 + ); + } + + $result = $this->bookmarkService->updateBookmark($bookmarkId, $bookmarkTitle, $bookmarkGroupId); + + if ($result['success']) { + $bookmark = $this->bookmarkService->getBookmark($bookmarkId); + if ($bookmark !== null) { + $result['bookmark'] = $bookmark; + } + } + + return new JsonResponse($result); + } + + public function deleteAction(ServerRequestInterface $request): ResponseInterface + { + $bookmarkId = (int)($request->getParsedBody()['bookmarkId'] ?? 0); + + if ($bookmarkId === 0) { + return $this->errorResponse( + 'core.bookmarks:error.missingBookmarkId.message', + 400 + ); + } + + $result = $this->bookmarkService->deleteBookmark($bookmarkId); + + return new JsonResponse($result); + } + + public function reorderAction(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + $bookmarkIds = $parsedBody['bookmarkIds'] ?? []; + + if (!is_array($bookmarkIds) || $bookmarkIds === []) { + return $this->errorResponse( + 'core.bookmarks:error.missingBookmarkIds.message', + 400 + ); + } + + $success = $this->bookmarkService->reorderBookmarks(array_map('intval', $bookmarkIds)); + + return new JsonResponse([ + 'success' => $success, + 'bookmarks' => $this->bookmarkService->getBookmarks(), + ]); + } + + public function deleteMultipleAction(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + $bookmarkIds = $parsedBody['bookmarkIds'] ?? []; + + if (!is_array($bookmarkIds) || $bookmarkIds === []) { + return $this->errorResponse( + 'core.bookmarks:error.missingBookmarkIds.message', + 400 + ); + } + + $success = $this->bookmarkService->deleteBookmarks(array_map('intval', $bookmarkIds)); + + return new JsonResponse(['success' => $success]); + } + + public function moveAction(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + $bookmarkIds = $parsedBody['bookmarkIds'] ?? []; + // Group ID can be int (system group, including negative for global) or string (user-created UUID) + $groupId = $parsedBody['groupId'] ?? 0; + if (is_numeric($groupId)) { + $groupId = (int)$groupId; + } + + if (!is_array($bookmarkIds) || $bookmarkIds === []) { + return $this->errorResponse( + 'core.bookmarks:error.missingBookmarkIds.message', + 400 + ); + } + + $success = $this->bookmarkService->moveBookmarks(array_map('intval', $bookmarkIds), $groupId); + + return new JsonResponse(['success' => $success]); + } + + public function createGroupAction(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + $label = trim($parsedBody['label'] ?? ''); + + if ($label === '') { + return $this->errorResponse( + 'core.bookmarks:error.missingLabel.message', + 400 + ); + } + + $group = $this->bookmarkService->createGroup($label); + + if ($group === null) { + return $this->errorResponse( + 'core.bookmarks:error.groupCreateFailed.message', + 500 + ); + } + + return new JsonResponse([ + 'success' => true, + 'group' => $group, + ], 201); + } + + public function updateGroupAction(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + $uuid = trim($parsedBody['uuid'] ?? ''); + $label = trim($parsedBody['label'] ?? ''); + + if ($uuid === '') { + return $this->errorResponse( + 'core.bookmarks:error.missingGroupId.message', + 400 + ); + } + + if ($label === '') { + return $this->errorResponse( + 'core.bookmarks:error.missingLabel.message', + 400 + ); + } + + $success = $this->bookmarkService->updateGroup($uuid, $label); + + if (!$success) { + return $this->errorResponse( + 'core.bookmarks:error.groupUpdateFailed.message', + 500 + ); + } + + return new JsonResponse([ + 'success' => true, + 'groups' => $this->bookmarkService->getGroups(), + ]); + } + + public function deleteGroupAction(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + $uuid = trim($parsedBody['uuid'] ?? ''); + + if ($uuid === '') { + return $this->errorResponse( + 'core.bookmarks:error.missingGroupId.message', + 400 + ); + } + + $success = $this->bookmarkService->deleteGroup($uuid); + + if (!$success) { + return $this->errorResponse( + 'core.bookmarks:error.groupDeleteFailed.message', + 500 + ); + } + + return new JsonResponse([ + 'success' => true, + 'groups' => $this->bookmarkService->getGroups(), + ]); + } + + public function reorderGroupsAction(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + $uuids = $parsedBody['uuids'] ?? []; + + if (!is_array($uuids) || $uuids === []) { + return $this->errorResponse( + 'core.bookmarks:error.missingGroupIds.message', + 400 + ); + } + + $success = $this->bookmarkService->reorderGroups($uuids); + + if (!$success) { + return $this->errorResponse( + 'core.bookmarks:error.groupReorderFailed.message', + 500 + ); + } + + return new JsonResponse([ + 'success' => true, + 'groups' => $this->bookmarkService->getGroups(), + ]); + } + + private function errorResponse(string $labelKey, int $statusCode = 200): JsonResponse + { + $languageService = $this->getLanguageService(); + return new JsonResponse([ + 'success' => false, + 'error' => $languageService->sL($labelKey) ?: $labelKey, + ], $statusCode); + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/ClearCacheController.php b/Classes/Controller/ClearCacheController.php new file mode 100644 index 0000000..4545972 --- /dev/null +++ b/Classes/Controller/ClearCacheController.php @@ -0,0 +1,98 @@ +start([], []); + $dataHandler->clear_cacheCmd('pages'); + + $languageService = $this->getLanguageService(); + return new JsonResponse([ + 'success' => true, + 'title' => $languageService->sL('core.cache:notification.group.pages.success.title'), + 'message' => $languageService->sL('core.cache:notification.group.pages.success.message'), + ]); + } + + public function flushCacheGroupAllAction(ServerRequestInterface $request): ResponseInterface + { + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $dataHandler->start([], []); + $dataHandler->clear_cacheCmd('all'); + + $languageService = $this->getLanguageService(); + return new JsonResponse([ + 'success' => true, + 'title' => $languageService->sL('core.cache:notification.group.all.success.title'), + 'message' => $languageService->sL('core.cache:notification.group.all.success.message'), + ]); + } + + public function flushCachePageAction(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + $pageUid = (int)($parsedBody['id'] ?? 0); + $languageService = $this->getLanguageService(); + $permissionClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW); + $pageRow = BackendUtility::readPageAccess($pageUid, $permissionClause); + if ($pageUid !== 0 && $this->getBackendUser()->doesUserHaveAccess($pageRow, Permission::PAGE_SHOW)) { + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $dataHandler->start([], []); + $dataHandler->clear_cacheCmd($pageUid); + return new JsonResponse([ + 'success' => true, + 'title' => $languageService->sL('core.cache:notification.page.success.title'), + 'message' => sprintf($languageService->sL('core.cache:notification.page.success.message'), BackendUtility::getRecordTitle('pages', $pageRow)), + ]); + } + return new JsonResponse([ + 'success' => false, + 'title' => $languageService->sL('core.cache:notification.page.error.title'), + 'message' => $languageService->sL('core.cache:notification.page.error.message'), + ]); + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/ClipboardController.php b/Classes/Controller/ClipboardController.php new file mode 100644 index 0000000..c377d7e --- /dev/null +++ b/Classes/Controller/ClipboardController.php @@ -0,0 +1,113 @@ +responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + $this->clipboard = GeneralUtility::makeInstance(Clipboard::class); + } + + /** + * Process incoming clipboard request + */ + public function processRequest(ServerRequestInterface $request): ResponseInterface + { + $this->clipboard->initializeClipboard($request); + + $CB = (array)($request->getParsedBody()['CB'] ?? []); + if ($CB !== []) { + // Execute commands. + $this->clipboard->setCmd($CB); + } + + // Clean up pad + $this->clipboard->cleanCurrent(); + // Save the clipboard content + $this->clipboard->endClipboard(); + + $action = (string)($request->getQueryParams()['action'] ?? ''); + if (in_array($action, self::ALLOWED_ACTIONS, true)) { + return $this->{$action . 'Action'}($request); + } + + // Default response in case no dedicated action is requested. + // This is usually done if only internal clipboard state is changed. + return $this->createResponse(['success' => true, 'data' => []]); + } + + protected function getClipboardDataAction(ServerRequestInterface $request): ResponseInterface + { + $clipboardData = $this->clipboard->getClipboardData($request->getParsedBody()['table'] ?? ''); + + // Add labels for the panel + $lang = $this->getLanguageService(); + $clipboardLabels = [ + 'clipboard' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.clipboard'), + 'copyElements' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:copyElements'), + 'moveElements' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:moveElements'), + 'copy' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy'), + 'cut' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut'), + 'info' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info'), + 'removeAll' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.removeAll'), + 'removeItem' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.removeItem'), + ]; + + return $this->createResponse([ + 'success' => $clipboardData !== [], + 'data' => array_merge($clipboardData, ['labels' => $clipboardLabels]), + ]); + } + + protected function createResponse(array $data): ResponseInterface + { + return $this->responseFactory->createResponse() + ->withHeader('Content-Type', 'application/json; charset=utf-8') + ->withBody($this->streamFactory->createStream(json_encode($data))); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/CodeEditor/CodeCompletionController.php b/Classes/Controller/CodeEditor/CodeCompletionController.php new file mode 100644 index 0000000..2a6c0cc --- /dev/null +++ b/Classes/Controller/CodeEditor/CodeCompletionController.php @@ -0,0 +1,129 @@ +isAdmin()) { + return new HtmlResponse($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_codeeditor.xlf:noPermission'), 500); + } + $pageId = (int)($request->getParsedBody()['pageId'] ?? $request->getQueryParams()['pageId']); + // Check whether there is a pageId given: + if (!$pageId) { + return new HtmlResponse($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_codeeditor.xlf:pageIDInteger'), 500); + } + // Fetch the templates + return new JsonResponse($this->getMergedTemplates($pageId, $request)); + } + + /** + * Gets merged templates by walking the rootline to a given page id. + * This is loaded once via ajax when a code editor in typoscript mode is fired. + * JS then knows the object types and can auto-complete on CTRL+space. + * + * @return array Setup part of merged template records + */ + protected function getMergedTemplates(int $pageId, ServerRequestInterface $request): array + { + $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get(); + $sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootline($rootLine, $request); + /** @var SiteInterface|null $site */ + $site = $request->getAttribute('site'); + $setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->lossyTokenizer, $site); + $setupAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class); + $this->treeTraverser->traverse($setupIncludeTree, [$setupAstBuilderVisitor]); + $setupAst = $setupAstBuilderVisitor->getAst(); + return $this->treeWalkCleanup($setupAst->toArray()); + } + + /** + * Walks through a tree of TypoScript configuration and prepares it for JS. + */ + private function treeWalkCleanup(array $treeBranch): array + { + $cleanedTreeBranch = []; + foreach ($treeBranch as $key => $value) { + $key = is_int($key) ? (string)$key : $key; + //type definition or value-assignment + if (substr($key, -1) !== '.') { + if ($value != '') { + if (mb_strlen($value) > 20) { + $value = mb_substr($value, 0, 20); + } + if (!isset($cleanedTreeBranch[$key])) { + $cleanedTreeBranch[$key] = []; + } + $cleanedTreeBranch[$key]['v'] = $value; + } + } else { + // subtree (definition of properties) + $subBranch = $this->treeWalkCleanup($value); + if ($subBranch) { + if (substr($key, -1) === '.') { + $key = rtrim($key, '.'); + } + if (!isset($cleanedTreeBranch[$key])) { + $cleanedTreeBranch[$key] = []; + } + $cleanedTreeBranch[$key]['c'] = $subBranch; + } + } + } + return $cleanedTreeBranch; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/CodeEditor/TypoScriptReferenceController.php b/Classes/Controller/CodeEditor/TypoScriptReferenceController.php new file mode 100644 index 0000000..6348a79 --- /dev/null +++ b/Classes/Controller/CodeEditor/TypoScriptReferenceController.php @@ -0,0 +1,73 @@ +loadXML(file_get_contents(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/tsref.xml'))); + + return new JsonResponse($this->getTypes($xmlDoc)); + } + + /** + * Get types from XML + */ + protected function getTypes(\DOMDocument $xmlDoc): array + { + $types = $xmlDoc->getElementsByTagName('type'); + $typeArr = []; + foreach ($types as $type) { + $typeId = $type->getAttribute('id'); + $typeName = $type->getAttribute('name'); + if (!$typeName) { + $typeName = $typeId; + } + $properties = $type->getElementsByTagName('property'); + $propArr = []; + foreach ($properties as $property) { + $p = []; + $p['name'] = $property->getAttribute('name'); + $p['type'] = $property->getAttribute('type'); + $propArr[$property->getAttribute('name')] = $p; + } + $typeArr[$typeId] = []; + $typeArr[$typeId]['properties'] = $propArr; + $typeArr[$typeId]['name'] = $typeName; + if ($type->hasAttribute('extends')) { + $typeArr[$typeId]['extends'] = $type->getAttribute('extends'); + } + } + return $typeArr; + } +} diff --git a/Classes/Controller/ColorSchemeController.php b/Classes/Controller/ColorSchemeController.php new file mode 100644 index 0000000..90423d4 --- /dev/null +++ b/Classes/Controller/ColorSchemeController.php @@ -0,0 +1,50 @@ +getParsedBody()['colorScheme']; + + if ($request->getMethod() !== 'POST' || !ColorScheme::tryFrom($colorScheme)) { + return new JsonResponse(null, 400); + } + + $backendUser = $this->getBackendUser(); + $backendUser->uc['colorScheme'] = $colorScheme; + $backendUser->writeUC(); + + return new Response(null); + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/ColumnSelectorController.php b/Classes/Controller/ColumnSelectorController.php new file mode 100644 index 0000000..57f36ed --- /dev/null +++ b/Classes/Controller/ColumnSelectorController.php @@ -0,0 +1,242 @@ +getParsedBody(); + $table = (string)($parsedBody['table'] ?? ''); + $selectedColumns = $parsedBody['selectedColumns'] ?? []; + + if ($table === '' || !is_array($selectedColumns)) { + return $this->jsonResponse([ + 'success' => false, + 'message' => htmlspecialchars( + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView.nothingUpdated') + ), + ]); + } + + $backendUser = $this->getBackendUserAuthentication(); + $displayFields = $backendUser->getModuleData('list/displayFields'); + $displayFields[$table] = $selectedColumns; + $backendUser->pushModuleData('list/displayFields', $displayFields); + + return $this->jsonResponse(['success' => true]); + } + + /** + * Generate the show columns selector form + */ + public function showColumnsSelectorAction(ServerRequestInterface $request): ResponseInterface + { + $queryParams = $request->getQueryParams(); + $table = (string)($queryParams['table'] ?? ''); + + if ($table === '') { + throw new \RuntimeException('No table was given for selecting columns', 1625169125); + } + $view = $this->backendViewFactory->create($request); + $view->assignMultiple([ + 'table' => $table, + 'columns' => $this->getColumns($table, (int)($queryParams['id'] ?? 0)), + ]); + + return $this->htmlResponse($view); + } + + /** + * Retrieve all columns for the table, which can be selected + */ + protected function getColumns(string $table, int $pageId): array + { + $tsConfig = BackendUtility::getPagesTSconfig($pageId); + + // Current fields selection + $displayFields = $this->getBackendUserAuthentication()->getModuleData('list/displayFields')[$table] ?? []; + + if ($table === '_FILE') { + // Special handling for _FILE (merging sys_file and sys_file_metadata together) + $fields = $this->getFileFields(); + } else { + // Request fields from table and add pseudo fields + $fields = array_merge(BackendUtility::getAllowedFieldsForTable($table), self::PSEUDO_FIELDS); + } + + $columns = $specialColumns = $disabledColumns = []; + foreach ($fields as $fieldName) { + $concreteTableName = $table; + + // In case we deal with _FILE, the field name is prefixed with the + // concrete table name, which is either sys_file or sys_file_metadata. + if ($table === '_FILE') { + [$concreteTableName, $fieldName] = explode('|', $fieldName); + } + + // Hide field if disabled + if ($tsConfig['TCEFORM.'][$concreteTableName . '.'][$fieldName . '.']['disabled'] ?? false) { + continue; + } + $schema = $this->tcaSchemaFactory->get($concreteTableName); + $labelFieldName = false; + if ($schema->hasCapability(TcaSchemaCapability::Label)) { + $labelFieldName = $schema->getCapability(TcaSchemaCapability::Label)->getPrimaryFieldName(); + } + + // Determine if the column should be disabled (Meaning it is always selected and can not be turned off) + $isDisabled = $fieldName === $labelFieldName; + + // Determine field label + $label = ($schema->hasField($fieldName) ? $schema->getField($fieldName)->getLabel() : '') ?: null; + $label = $this->getLanguageService()->translateLabel( + $tsConfig['TCEFORM.'][$concreteTableName . '.'][$fieldName . '.']['label.'] ?? [], + $tsConfig['TCEFORM.'][$concreteTableName . '.'][$fieldName . '.']['label'] + ?? $label + ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $fieldName + ); + + // Add configuration for this column + $columnConfiguration = [ + 'name' => $fieldName, + 'selected' => $isDisabled || in_array($fieldName, $displayFields, true), + 'disabled' => $isDisabled, + 'pseudo' => in_array($fieldName, self::PSEUDO_FIELDS, true), + 'label' => $label, + ]; + + // Add column configuration to the correct group + if ($columnConfiguration['disabled']) { + $disabledColumns[] = $columnConfiguration; + } elseif (!$columnConfiguration['label']) { + $specialColumns[] = $columnConfiguration; + } else { + $columns[] = $columnConfiguration; + } + } + + // Sort standard columns by their resolved label + usort($columns, static fn($a, $b) => $a['label'] <=> $b['label']); + + // Disabled columns go first, followed by standard columns + // and special columns, which do not have a label. + return array_merge($disabledColumns, $columns, $specialColumns); + } + + /** + * Get file related fields by merging sys_file and sys_file_metadata together + * and adding the corresponding table as prefix (needed for labels processing). + */ + protected function getFileFields(): array + { + // Get all sys_file fields expect excluded ones + $fileFields = array_filter( + BackendUtility::getAllowedFieldsForTable('sys_file'), + static fn(string $field): bool => !in_array($field, self::EXCLUDE_FILE_FIELDS, true) + ); + + // Always add crdate and tstamp fields for files + $fileFields = array_unique(array_merge($fileFields, ['crdate', 'tstamp'])); + + // Update the exclude fields with the fields, already added through sys_file, since those take precedence + $excludeFields = array_merge($fileFields, self::EXCLUDE_FILE_FIELDS); + + // Get all sys_file_metadata fields expect excluded ones + $fileMetaDataFields = array_filter( + BackendUtility::getAllowedFieldsForTable('sys_file_metadata'), + static fn(string $field): bool => !in_array($field, $excludeFields, true) + ); + + // Merge sys_file and sys_file_metadata fields together, while adding the table name as prefix + return array_merge( + array_map(static fn(string $value): string => 'sys_file|' . $value, $fileFields), + array_map(static fn(string $value): string => 'sys_file_metadata|' . $value, $fileMetaDataFields), + ); + } + + protected function htmlResponse(ViewInterface $view): ResponseInterface + { + $response = $this->responseFactory + ->createResponse() + ->withHeader('Content-Type', 'text/html; charset=utf-8'); + $response->getBody()->write($view->render('ColumnSelector')); + return $response; + } + + protected function jsonResponse(array $data): ResponseInterface + { + $response = $this->responseFactory + ->createResponse() + ->withAddedHeader('Content-Type', 'application/json; charset=utf-8'); + + $response->getBody()->write(json_encode($data)); + return $response; + } + + protected function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/ContentElement/ElementHistoryController.php b/Classes/Controller/ContentElement/ElementHistoryController.php new file mode 100644 index 0000000..00c3a1f --- /dev/null +++ b/Classes/Controller/ContentElement/ElementHistoryController.php @@ -0,0 +1,554 @@ +view = $this->moduleTemplateFactory->create($request); + $backendUser = $this->getBackendUser(); + $this->view->getDocHeaderComponent()->setPageBreadcrumb([]); + + $parsedBody = $request->getParsedBody(); + $queryParams = $request->getQueryParams(); + + $this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '', $request); + + $lastHistoryEntry = (int)($parsedBody['historyEntry'] ?? $queryParams['historyEntry'] ?? 0); + $rollbackFields = $parsedBody['rollbackFields'] ?? $queryParams['rollbackFields'] ?? null; + $element = $parsedBody['element'] ?? $queryParams['element'] ?? null; + $moduleSettings = $this->processSettings($request); + $this->view->assign('isUserInWorkspace', $backendUser->workspace > 0); + + $this->showDiff = (bool)$moduleSettings['showDiff']; + + // Start history object + $this->historyObject = GeneralUtility::makeInstance(RecordHistory::class, $element); + $this->historyObject->setShowSubElements((bool)$moduleSettings['showSubElements']); + $this->historyObject->setLastHistoryEntryNumber($lastHistoryEntry); + if ($moduleSettings['maxSteps']) { + $this->historyObject->setMaxSteps((int)$moduleSettings['maxSteps']); + } + + // Do the actual logic now (rollback, show a diff for certain changes, + // or show the full history of a page or a specific record) + $changeLog = $this->historyObject->getChangeLog(); + if (!empty($changeLog)) { + if ($rollbackFields !== null) { + $diff = $this->historyObject->getDiff($changeLog); + GeneralUtility::makeInstance(RecordHistoryRollback::class)->performRollback($rollbackFields, $diff); + } elseif ($lastHistoryEntry) { + $completeDiff = $this->historyObject->getDiff($changeLog); + $this->displayMultipleDiff($completeDiff); + $button = $this->componentFactory->createLinkButton() + ->setHref($this->buildUrl(['historyEntry' => ''])) + ->setIcon($this->iconFactory->getIcon('actions-view-go-back', IconSize::SMALL)) + ->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:fullView')) + ->setShowLabelText(true); + $this->view->addButtonToButtonBar($button); + } + if ($this->historyObject->getElementString() !== '') { + $this->displayHistory($changeLog); + } + } + + $elementData = $this->historyObject->getElementInformation(); + $editLock = false; + if (!empty($elementData)) { + [$elementTable, $elementUid] = $elementData; + $elementUid = (int)$elementUid; + $this->setPagePath($elementTable, $elementUid); + $editLock = $this->getEditLockFromElement($elementTable, $elementUid); + // Get link to page history if the element history is shown + if ($elementTable !== 'pages') { + $parentPage = BackendUtility::getRecord($elementTable, $elementUid, '*', '', false); + if ($parentPage['pid'] > 0 && BackendUtility::readPageAccess($parentPage['pid'], $backendUser->getPagePermsClause(Permission::PAGE_SHOW))) { + $button = $this->componentFactory->createLinkButton() + ->setHref($this->buildUrl([ + 'element' => 'pages:' . $parentPage['pid'], + 'historyEntry' => '', + ])) + ->setIcon($this->iconFactory->getIcon('apps-pagetree-page-default', IconSize::SMALL)) + ->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:elementHistory_link')) + ->setShowLabelText(true); + $this->view->addButtonToButtonBar($button, ButtonBar::BUTTON_POSITION_LEFT, 2); + } + } + } + + if ($element !== null) { + $this->addLanguageSwitcher($request, $backendUser, $element); + } + + $this->view->assign('editLock', $editLock); + $this->view->assign('moduleSettings', $moduleSettings); + $this->view->assign('settingsFormUrl', $this->buildUrl()); + + // Setting up the buttons and markers for docheader + $this->getButtons(); + + return $this->view->renderResponse('RecordHistory/Main'); + } + + /** + * Creates the correct path to the current record + */ + protected function setPagePath(string $table, int $uid): void + { + $record = BackendUtility::getRecord($table, $uid, '*', '', false); + if ($table === 'pages') { + $pageId = $uid; + } else { + $pageId = $record['pid']; + } + + $pageAccess = BackendUtility::readPageAccess($pageId, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)); + if (is_array($pageAccess)) { + $this->view->getDocHeaderComponent()->setPageBreadcrumb($pageAccess); + } + + $schema = $this->tcaSchemaFactory->get($table); + $this->view->assignMultiple([ + 'recordTable' => $table, + 'recordTableReadable' => $schema->getTitle($this->getLanguageService()->sL(...)), + 'recordUid' => $uid, + 'recordTitle' => $this->generateTitle($table, (string)$uid), + ]); + } + + protected function getButtons(): void + { + if ($this->returnUrl) { + $backButton = $this->componentFactory->createLinkButton() + ->setHref($this->returnUrl) + ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc')) + ->setShowLabelText(true) + ->setIcon($this->iconFactory->getIcon('actions-close', IconSize::SMALL)); + $this->view->addButtonToButtonBar($backButton); + } + } + + protected function processSettings(ServerRequestInterface $request): array + { + // Get current selection from UC, merge data, write it back to UC + $currentSelection = $this->getBackendUser()->getModuleData('history'); + if (!is_array($currentSelection)) { + $currentSelection = ['maxSteps' => '', 'showDiff' => 1, 'showSubElements' => 1]; + } + $currentSelectionOverride = $request->getParsedBody()['settings'] ?? null; + if (is_array($currentSelectionOverride) && !empty($currentSelectionOverride)) { + $currentSelection = array_merge($currentSelection, $currentSelectionOverride); + $this->getBackendUser()->pushModuleData('history', $currentSelection); + } + return $currentSelection; + } + + /** + * Add a translation selection dropdown if the record is language aware. + */ + protected function addLanguageSwitcher( + ServerRequestInterface $request, + BackendUserAuthentication $backendUser, + string $element, + ): void { + $translations = $this->historyObject->getTranslations($element); + if ($translations === null) { + return; + } + + $languageDropDownButton = $this->componentFactory->createDropDownButton() + ->setLabel($this->getLanguageService()->sL('core.core:labels.language')) + ->setShowLabelText(true); + + try { + $site = $this->siteFinder->getSiteByPageId($translations['page']); + } catch (SiteNotFoundException) { + $site = $request->getAttribute('site'); + } + + $availableLanguages = $site->getAvailableLanguages($backendUser, false, $translations['page']); + + foreach ($translations['elements'] as $translation) { + $siteLanguage = $availableLanguages[$translation['language']] ?? null; + if (!$siteLanguage instanceof SiteLanguage) { + continue; + } + + $languageItem = $this->componentFactory->createDropDownRadio() + ->setActive($translation['element'] === $element) + ->setIcon($this->iconFactory->getIcon($siteLanguage->getFlagIdentifier())) + ->setHref((string)$this->uriBuilder->buildUriFromRoute('record_history', [ + 'element' => $translation['element'], + 'returnUrl' => $this->returnUrl, + ])) + ->setLabel($siteLanguage->getTitle()); + $languageDropDownButton->addItem($languageItem); + + if ($languageItem->isActive()) { + $languageDropDownButton->setLabel($siteLanguage->getTitle()); + } + } + + $this->view->getDocHeaderComponent()->setLanguageSelector($languageDropDownButton); + } + + /** + * Displays a diff over multiple fields including rollback links + * + * @param array $diff Difference array + */ + protected function displayMultipleDiff(array $diff): void + { + $languageService = $this->getLanguageService(); + + // Get all array keys needed + /** @var string[] $arrayKeys */ + $arrayKeys = array_merge(array_keys($diff['newData']), array_keys($diff['insertsDeletes']), array_keys($diff['oldData'])); + $arrayKeys = array_unique($arrayKeys); + if (!empty($arrayKeys)) { + $lines = []; + foreach ($arrayKeys as $key) { + $singleLine = []; + $elParts = explode(':', $key); + // Turn around diff because it should be a "rollback preview" + if ((int)($diff['insertsDeletes'][$key] ?? 0) === 1) { + // insert + $singleLine['insertDelete'] = 'delete'; + } elseif ((int)($diff['insertsDeletes'][$key] ?? 0) === -1) { + $singleLine['insertDelete'] = 'insert'; + } + // Build up temporary diff array + // turn around diff because it should be a "rollback preview" + if ($diff['newData'][$key] ?? false) { + $tmpArr = [ + 'newRecord' => $diff['oldData'][$key], + 'oldRecord' => $diff['newData'][$key], + ]; + + // show changes + if (!$this->showDiff) { + // Display field names instead of full diff + // Re-write field names with labels + /** @var string[] $tmpFieldList */ + $tmpFieldList = array_keys($tmpArr['newRecord']); + foreach ($tmpFieldList as $fieldKey => $value) { + $itemLabel = ''; + if ($this->tcaSchemaFactory->has($elParts[0]) && ($schema = $this->tcaSchemaFactory->get($elParts[0]))->hasField($value)) { + $itemLabel = $schema->getField($value)->getLabel(); + } + $tmp = str_replace(':', '', $languageService->sL($itemLabel)); + if ($tmp) { + $tmpFieldList[$fieldKey] = $tmp; + } else { + // remove fields if no label available + unset($tmpFieldList[$fieldKey]); + } + } + $singleLine['fieldNames'] = implode(',', $tmpFieldList); + } else { + // Display diff + $singleLine['differences'] = $this->renderDiff($tmpArr, $elParts[0], (int)$elParts[1], true); + } + } + $elParts = explode(':', $key); + $singleLine['revertRecordUrl'] = $this->buildUrl(['rollbackFields' => $key]); + $singleLine['title'] = $this->generateTitle($elParts[0], $elParts[1]); + $singleLine['recordTable'] = $elParts[0]; + $singleLine['recordUid'] = $elParts[1]; + $lines[] = $singleLine; + } + $this->view->assign('revertAllUrl', $this->buildUrl(['rollbackFields' => 'ALL'])); + $this->view->assign('multipleDiff', $lines); + } + $this->view->assign('showDifferences', true); + } + + /** + * Shows the full change log + */ + protected function displayHistory(array $historyEntries): void + { + if ($historyEntries === []) { + return; + } + $languageService = $this->getLanguageService(); + $lines = []; + $beUserArray = BackendUtility::getUserNames('username,realName,usergroup,uid'); + + // Traverse changeLog array: + foreach ($historyEntries as $entry) { + // Build up single line + $singleLine = []; + + // Get user names + $singleLine['backendUserUid'] = $entry['userid']; + $singleLine['backendUserName'] = $beUserArray[$entry['userid']]['username'] ?? ''; + $singleLine['backendUserRealName'] = $beUserArray[$entry['userid']]['realName'] ?? ''; + // Executed by switch user + if (!empty($entry['originaluserid'])) { + $singleLine['originalBackendUserUid'] = $entry['originaluserid']; + $singleLine['originalBackendUserName'] = $beUserArray[$entry['originaluserid']]['username'] ?? ''; + $singleLine['originalBackendRealName'] = $beUserArray[$entry['originaluserid']]['realName'] ?? ''; + } + + // Is a change in a workspace? + $singleLine['isChangedInWorkspace'] = (int)$entry['workspace'] > 0; + + // Diff link + $singleLine['diffUrl'] = $this->buildUrl(['historyEntry' => $entry['uid']]); + // Add time + $singleLine['day'] = BackendUtility::date($entry['tstamp']); + $singleLine['timestamp'] = DateTimeFactory::createFromTimestamp($entry['tstamp']); + + $singleLine['title'] = $this->generateTitle($entry['tablename'], (string)$entry['recuid']); + $singleLine['recordTable'] = $entry['tablename']; + $singleLine['recordUid'] = $entry['recuid']; + + $singleLine['elementUrl'] = $this->buildUrl(['element' => $entry['tablename'] . ':' . $entry['recuid']]); + $singleLine['actiontype'] = $entry['actiontype']; + if ((int)$entry['actiontype'] === RecordHistoryStore::ACTION_MODIFY || (int)$entry['actiontype'] === RecordHistoryStore::ACTION_PUBLISH) { + // show changes + if (!$this->showDiff) { + // Display field names instead of full diff + // Re-write field names with labels + /** @var string[] $tmpFieldList */ + $tmpFieldList = array_keys($entry['newRecord']); + foreach ($tmpFieldList as $key => $value) { + $itemLabel = ''; + if ($this->tcaSchemaFactory->has($entry['tablename']) && ($schema = $this->tcaSchemaFactory->get($entry['tablename']))->hasField($value)) { + $itemLabel = $schema->getField($value)->getLabel(); + } + $tmp = str_replace(':', '', $languageService->sL($itemLabel)); + if ($tmp) { + $tmpFieldList[$key] = $tmp; + } else { + // remove fields if no label available + unset($tmpFieldList[$key]); + } + } + $singleLine['fieldNames'] = implode(',', $tmpFieldList); + } else { + // Display diff + $singleLine['differences'] = $this->renderDiff($entry, $entry['tablename'], (int)$entry['recuid']); + } + } + // put line together + $lines[] = $singleLine; + } + $this->view->assign('history', $lines); + } + + /** + * Renders HTML table-rows with the comparison information of a sys_history entry record + * + * @param array $entry sys_history entry record. + * @param string $table The table name + * @param int $rollbackUid The UID of the record + * @param bool $showRollbackLink Whether a rollback link should be shown for each changed field + * @return array array of records + */ + protected function renderDiff(array $entry, string $table, int $rollbackUid, bool $showRollbackLink = false): array + { + if (!$this->tcaSchemaFactory->has($table)) { + return []; + } + $lines = []; + if (is_array($entry['newRecord'] ?? null)) { + $fieldsToDisplay = array_keys($entry['newRecord']); + $languageService = $this->getLanguageService(); + $schema = $this->tcaSchemaFactory->get($table); + foreach ($fieldsToDisplay as $fN) { + if (!$schema->hasField($fN)) { + continue; + } + $fieldInformation = $schema->getField($fN); + if (!$fieldInformation->isType(TableColumnType::PASSTHROUGH)) { + if ($fieldInformation->isType(TableColumnType::FLEX)) { + $colConfig = $fieldInformation->getConfiguration(); + $old = $this->flexFormValueFormatter->format($table, $fN, ($entry['oldRecord'][$fN] ?? ''), $rollbackUid, $colConfig); + $new = $this->flexFormValueFormatter->format($table, $fN, ($entry['newRecord'][$fN] ?? ''), $rollbackUid, $colConfig); + $diffResult = $this->diffUtility->diff(strip_tags($old), strip_tags($new), DiffGranularity::CHARACTER); + } else { + $old = (string)BackendUtility::getProcessedValue($table, $fN, ($entry['oldRecord'][$fN] ?? ''), 0, true, false, $rollbackUid); + $new = (string)BackendUtility::getProcessedValue($table, $fN, ($entry['newRecord'][$fN] ?? ''), 0, true, false, $rollbackUid); + $diffResult = $this->diffUtility->diff(strip_tags($old), strip_tags($new)); + } + $rollbackUrl = ''; + if ($rollbackUid && $showRollbackLink) { + $rollbackUrl = $this->buildUrl(['rollbackFields' => $table . ':' . $rollbackUid . ':' . $fN]); + } + $lines[] = [ + 'title' => $languageService->sL($fieldInformation->getLabel()), + 'rollbackUrl' => $rollbackUrl, + 'result' => str_replace('\n', PHP_EOL, str_replace('\r\n', '\n', $diffResult)), + ]; + } + } + } + return $lines; + } + + /** + * Generates the URL for a link to the current page + */ + protected function buildUrl(array $overrideParameters = []): string + { + $params = []; + + // Setting default values based on GET parameters: + $elementString = $this->historyObject->getElementString(); + if ($elementString !== '') { + $params['element'] = $elementString; + } + $params['historyEntry'] = $this->historyObject->getLastHistoryEntryNumber(); + + if (!empty($this->returnUrl)) { + $params['returnUrl'] = $this->returnUrl; + } + + // Merging overriding values: + $params = array_merge($params, $overrideParameters); + + // Make the link: + return (string)$this->uriBuilder->buildUriFromRoute('record_history', $params); + } + + /** + * Generates the title and puts the record title behind + */ + protected function generateTitle(string $table, string $uid): string + { + if ($this->tcaSchemaFactory->get($table)->hasCapability(TcaSchemaCapability::Label)) { + $record = $this->getRecord($table, (int)$uid) ?? []; + return BackendUtility::getRecordTitle($table, $record); + } + return ''; + } + + /** + * Gets a database record (cached). + */ + protected function getRecord(string $table, int $uid): ?array + { + if (!isset($this->recordCache[$table][$uid])) { + $this->recordCache[$table][$uid] = BackendUtility::getRecord($table, $uid, '*', '', false); + } + return $this->recordCache[$table][$uid]; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + /** + * Get the editlock value from page of a history element + */ + protected function getEditLockFromElement(string $tableName, int $elementUid): bool + { + // If the user is admin, then he may always edit the page. + if ($this->getBackendUser()->isAdmin()) { + return false; + } + + $schema = $this->tcaSchemaFactory->get($tableName); + + // Early return if $elementUid is zero + if ($elementUid === 0) { + return !$schema->getCapability(TcaSchemaCapability::RestrictionRootLevel)->shallIgnoreRootLevelRestriction(); + } + + $record = BackendUtility::getRecord($tableName, $elementUid, '*', '', false); + // we need the parent page record for the editlock info if element isn't a page + if ($tableName !== 'pages') { + $pageId = $record['pid']; + $record = BackendUtility::getRecord('pages', $pageId, '*', '', false); + } + + return $schema->hasCapability(TcaSchemaCapability::EditLock) + && ($record[$schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false); + } +} diff --git a/Classes/Controller/ContentElement/ElementInformationController.php b/Classes/Controller/ContentElement/ElementInformationController.php new file mode 100644 index 0000000..4183dc5 --- /dev/null +++ b/Classes/Controller/ContentElement/ElementInformationController.php @@ -0,0 +1,797 @@ +getBackendUser(); + $view = $this->moduleTemplateFactory->create($request); + $view->getDocHeaderComponent()->disable(); + $queryParams = $request->getQueryParams(); + $this->table = $queryParams['table'] ?? null; + $uid = $queryParams['uid'] ?? ''; + $permsClause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW); + // Determines if table/uid point to database record or file and if user has access to view information + $accessAllowed = false; + if ($this->tcaSchemaFactory->has($this->table)) { + $uid = (int)$uid; + // Check permissions and uid value: + if ($uid && $backendUser->check('tables_select', $this->table)) { + if ((string)$this->table === 'pages') { + $this->row = BackendUtility::readPageAccess($uid, $permsClause) ?: []; + $accessAllowed = $this->row !== []; + } else { + $this->row = BackendUtility::getRecordWSOL($this->table, $uid); + if ($this->row) { + if (isset($this->row['_ORIG_uid'])) { + // Make $uid the uid of the versioned record, while $this->row['uid'] is live record uid + $uid = (int)$this->row['_ORIG_uid']; + } + $pageInfo = BackendUtility::readPageAccess((int)$this->row['pid'], $permsClause) ?: []; + $accessAllowed = $pageInfo !== [] + || ((int)$this->row['pid'] === 0 && $this->tcaSchemaFactory->get($this->table)->getCapability(TcaSchemaCapability::RestrictionRootLevel)->shallIgnoreRootLevelRestriction()); + } + } + } + } elseif ($this->table === '_FILE' || $this->table === '_FOLDER' || $this->table === 'sys_file') { + $fileOrFolderObject = $this->resourceFactory->retrieveFileOrFolderObject($uid); + if ($fileOrFolderObject instanceof Folder) { + $this->folderObject = $fileOrFolderObject; + $accessAllowed = $this->folderObject->checkActionPermission('read'); + $this->type = 'folder'; + } elseif ($fileOrFolderObject instanceof File) { + $this->fileObject = $fileOrFolderObject; + $accessAllowed = $this->fileObject->checkActionPermission('read'); + $this->type = 'file'; + $this->table = 'sys_file'; + $this->row = BackendUtility::getRecordWSOL($this->table, $fileOrFolderObject->getUid()); + } + } + + // Rendering of the output via fluid + $view->assign('accessAllowed', $accessAllowed); + $view->assign('hookContent', ''); + if (!$accessAllowed) { + return $view->renderResponse('ContentElement/ElementInformation'); + } + + // render type by user func + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/show_item.php']['typeRendering'] ?? [] as $className) { + $typeRenderObj = GeneralUtility::makeInstance($className); + if (method_exists($typeRenderObj, 'isValid') && method_exists($typeRenderObj, 'render')) { + if ($typeRenderObj->isValid($this->type, $this)) { + $view->assign('hookContent', $typeRenderObj->render($this->type, $this, $view)); + return $view->renderResponse('ContentElement/ElementInformation'); + } + } + } + + $pageTitle = $this->getPageTitle(); + $view->setTitle($pageTitle['table'] . ': ' . $pageTitle['title']); + $view->assignMultiple($pageTitle); + $view->assignMultiple($this->getPreview($request)); + $view->assignMultiple($this->getPropertiesForTable()); + $view->assignMultiple($this->getReferences($request, $uid)); + $view->assign('returnUrl', GeneralUtility::sanitizeLocalUrl($request->getQueryParams()['returnUrl'] ?? '', $request)); + $view->assign('maxTitleLength', $this->getBackendUser()->uc['titleLen'] ?? 20); + + return $view->renderResponse('ContentElement/ElementInformation'); + } + + /** + * Get page title with icon, table title and record title + */ + public function getPageTitle(): array + { + $pageTitle = [ + 'title' => BackendUtility::getRecordTitle($this->table, $this->row), + ]; + if ($this->type === 'folder') { + $pageTitle['title'] = htmlspecialchars($this->folderObject->getName()); + $pageTitle['table'] = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:folder'); + $pageTitle['icon'] = $this->iconFactory->getIconForResource($this->folderObject, IconSize::SMALL)->render(); + } elseif ($this->type === 'file') { + $schema = $this->tcaSchemaFactory->get($this->table); + $pageTitle['table'] = $schema->getTitle($this->getLanguageService()->sL(...)); + $pageTitle['icon'] = $this->iconFactory->getIconForResource($this->fileObject, IconSize::SMALL)->render(); + } else { + $schema = $this->tcaSchemaFactory->get($this->table); + $pageTitle['table'] = $schema->getTitle($this->getLanguageService()->sL(...)); + $pageTitle['icon'] = $this->iconFactory->getIconForRecord($this->table, $this->row, IconSize::SMALL); + } + return $pageTitle; + } + + public function getTable(): ?string + { + return $this->table; + } + + public function getRow(): array + { + return $this->row; + } + + public function getFileObject(): ?File + { + return $this->fileObject; + } + + public function getFolderObject(): ?Folder + { + return $this->folderObject; + } + + /** + * Get preview for current record + */ + protected function getPreview(ServerRequestInterface $request): array + { + $preview = []; + // Perhaps @todo in future: Also display preview for records - without fileObject + if (!$this->fileObject) { + return $preview; + } + + // check if file is marked as missing + if ($this->fileObject->isMissing()) { + $preview['missingFile'] = $this->fileObject->getName(); + } else { + $fileRenderer = $this->rendererRegistry->getRenderer($this->fileObject); + $preview['url'] = $this->fileObject->getPublicUrl() ?? ''; + + // Add "edit metadata" button + $preview['editMetadataUrl'] = ''; + if (($metaDataUid = $this->fileObject->getProperties()['metadata_uid'] ?? false) + && $this->fileObject->isIndexed() + && $this->fileObject->checkActionPermission('editMeta') + && $this->getBackendUser()->check('tables_modify', 'sys_file_metadata') + ) { + $urlParameters = [ + 'edit' => [ + 'sys_file_metadata' => [ + $metaDataUid => 'edit', + ], + ], + 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), + ]; + $preview['editMetadataUrl'] = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters); + } + + $width = min(590, $this->fileObject->getMetaData()['width'] ?? 590) . 'm'; + $height = min(400, $this->fileObject->getMetaData()['height'] ?? 400) . 'm'; + + // Check if there is a FileRenderer + if ($fileRenderer !== null) { + $preview['fileRenderer'] = $fileRenderer->render($this->fileObject, $width, $height); + // else check if we can create an Image preview + } elseif ($this->fileObject->isImage()) { + $preview['fileObject'] = $this->fileObject; + $preview['width'] = $width; + $preview['height'] = $height; + } + } + return $preview; + } + + /** + * Get property array for html table + */ + protected function getPropertiesForTable(): array + { + $lang = $this->getLanguageService(); + $propertiesForTable = []; + $propertiesForTable['extraFields'] = $this->getExtraFields(); + + // Traverse the list of fields to display for the record: + $fieldList = $this->getFieldList($this->table, $this->row); + $schema = $this->tcaSchemaFactory->has($this->table) ? $this->tcaSchemaFactory->get($this->table) : null; + + foreach ($fieldList as $name) { + $name = trim($name); + $uid = $this->row['uid'] ?? 0; + + if (!$schema?->hasField($name)) { + continue; + } + + // @todo Add meaningful information for mfa field. For the time being we don't display anything at all. + if ($this->type === 'db' && $name === 'mfa' && in_array($this->table, ['be_users', 'fe_users'], true)) { + continue; + } + + // not a real field -> skip + if ($this->type === 'file' && $name === 'fileinfo') { + continue; + } + + // handled explicitly below with proper byte formatting -> skip + if ($this->type === 'file' && $name === 'size') { + continue; + } + + // Field does not exist (e.g. having type=none) -> skip + if (!array_key_exists($name, $this->row)) { + continue; + } + + $label = $lang->sL($schema->getField($name)->getLabel()); + $label = $label ?: $name; + + $propertiesForTable['fields'][] = [ + 'fieldValue' => BackendUtility::getProcessedValue($this->table, $name, $this->row[$name], 0, false, false, $uid, true, 0, $this->row), + 'fieldLabel' => htmlspecialchars($label), + ]; + } + + // additional information for folders and files + if ($this->folderObject instanceof Folder || $this->fileObject instanceof File) { + // storage + if ($this->folderObject instanceof Folder) { + $propertiesForTable['fields']['storage'] = [ + 'fieldValue' => $this->folderObject->getStorage()->getName(), + 'fieldLabel' => htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:sys_file.storage')), + ]; + } + + // folder + $resourceObject = $this->fileObject ?: $this->folderObject; + $parentFolder = $resourceObject->getParentFolder(); + $propertiesForTable['fields']['folder'] = [ + 'fieldValue' => $parentFolder->getReadablePath(), + 'fieldLabel' => htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:folder')), + ]; + + if ($this->fileObject instanceof File) { + // show file dimensions for images + if ($this->fileObject->isType(FileType::IMAGE)) { + $propertiesForTable['fields']['width'] = [ + 'fieldValue' => $this->fileObject->getProperty('width') . 'px', + 'fieldLabel' => htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.width')), + ]; + $propertiesForTable['fields']['height'] = [ + 'fieldValue' => $this->fileObject->getProperty('height') . 'px', + 'fieldLabel' => htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.height')), + ]; + } + + // file size + $fileSizeInBytes = (int)$this->fileObject->getProperty('size'); + $propertiesForTable['fields']['size'] = [ + 'fieldValue' => sprintf( + '%s (%s)', + GeneralUtility::formatSize($fileSizeInBytes, htmlspecialchars($this->getLanguageService()->sL('core.common:byteSizeUnits'))), + htmlspecialchars($lang->translate('size_in_bytes', 'core.core', ['numberOfBytes' => GeneralUtility::formatSize($fileSizeInBytes, ' ')])), + ), + 'fieldLabel' => $lang->sL($schema?->hasField('size') ? $schema->getField('size')->getLabel() : ''), + ]; + + // show the metadata of a file as well + $metaData = $this->metaDataRepository->findByFileUid((int)($this->row['uid'] ?? 0)); + + // If there is no metadata record, skip it + if ($metaData !== []) { + $fileMetadataSchema = $this->tcaSchemaFactory->get('sys_file_metadata'); + $allowedFields = $this->getFieldList('sys_file_metadata', $metaData); + + foreach ($metaData as $name => $value) { + if (!in_array($name, $allowedFields, true)) { + continue; + } + if ($name === 'crdate') { + // Is of type=passthrough and already part of + // meta information displayed on top of the table + continue; + } + if (!$fileMetadataSchema->hasField($name)) { + continue; + } + + $label = $lang->sL($fileMetadataSchema->getField($name)->getLabel()); + $label = $label ?: $name; + + $propertiesForTable['fields'][] = [ + 'fieldValue' => BackendUtility::getProcessedValue('sys_file_metadata', $name, $value, 0, false, false, (int)$metaData['uid'], true, 0, $metaData), + 'fieldLabel' => htmlspecialchars($label), + ]; + } + } + } + } + + return $propertiesForTable; + } + + /** + * Get the list of fields that should be shown for the given table + */ + protected function getFieldList(string $table, array $row): array + { + $fieldNamesToExclude = []; + if ($this->tcaSchemaFactory->has($table)) { + $schema = $this->tcaSchemaFactory->get($table); + if ($schema->hasCapability(TcaSchemaCapability::AncestorReferenceField)) { + $fieldNamesToExclude[] = $schema->getCapability(TcaSchemaCapability::AncestorReferenceField)->getFieldName(); + } + if ($schema->isLanguageAware()) { + $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); + $fieldNamesToExclude[] = $languageCapability->getTranslationOriginPointerField()->getName(); + if ($languageCapability->hasDiffSourceField()) { + $fieldNamesToExclude[] = $languageCapability->getDiffSourceField()?->getName(); + } + } + } + + return $this->searchableSchemaFieldsCollector->getUniqueFieldList( + $table, + $this->visibleSchemaFieldsCollector->getFieldNames($table, $row, $fieldNamesToExclude), + false + ); + } + + /** + * Get the extra fields (uid, timestamps, creator) for the table + */ + protected function getExtraFields(): array + { + $lang = $this->getLanguageService(); + $keyLabelPair = []; + if (in_array($this->type, ['folder', 'file'], true)) { + if ($this->type === 'file') { + $keyLabelPair['uid'] = [ + 'value' => (int)$this->row['uid'], + ]; + $keyLabelPair['creation_date'] = [ + 'value' => BackendUtility::datetime($this->row['creation_date']), + 'fieldLabel' => htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.creationDate')), + 'isDatetime' => true, + ]; + $keyLabelPair['modification_date'] = [ + 'value' => BackendUtility::datetime($this->row['modification_date']), + 'fieldLabel' => htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.timestamp')), + 'isDatetime' => true, + ]; + } else { + $keyLabelPair['uid'] = [ + 'value' => $this->folderObject->getCombinedIdentifier(), + ]; + } + } else { + $keyLabelPair['uid'] = [ + 'value' => BackendUtility::getProcessedValueExtra($this->table, 'uid', $this->row['uid']), + 'fieldLabel' => rtrim(htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:show_item.php.uid')), ':'), + ]; + $schema = $this->tcaSchemaFactory->get($this->table); + if ($schema->hasCapability(TcaSchemaCapability::CreatedAt)) { + $field = $schema->getCapability(TcaSchemaCapability::CreatedAt)->getFieldName(); + $keyLabelPair[$field] = [ + 'value' => BackendUtility::datetime($this->row[$field]), + 'fieldLabel' => rtrim(htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.creationDate')), ':'), + 'isDatetime' => true, + ]; + } + if ($schema->hasCapability(TcaSchemaCapability::UpdatedAt)) { + $field = $schema->getCapability(TcaSchemaCapability::UpdatedAt)->getFieldName(); + $keyLabelPair[$field] = [ + 'value' => BackendUtility::datetime($this->row[$field]), + 'fieldLabel' => rtrim(htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.timestamp')), ':'), + 'isDatetime' => true, + ]; + } + // Show the user who created the record + $recordHistory = GeneralUtility::makeInstance(RecordHistory::class); + $ownerInformation = $recordHistory->getCreationInformationForRecord($this->table, $this->row); + $ownerUid = (int)(is_array($ownerInformation) && $ownerInformation['usertype'] === 'BE' ? $ownerInformation['userid'] : 0); + if ($ownerUid) { + $creatorRecord = BackendUtility::getRecord('be_users', $ownerUid); + if ($creatorRecord) { + $keyLabelPair['creatorRecord'] = [ + 'value' => $creatorRecord, + 'fieldLabel' => rtrim(htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.creationUserId')), ':'), + ]; + } + } + } + return $keyLabelPair; + } + + /** + * Get references section (references from and references to current record) + */ + protected function getReferences(ServerRequestInterface $request, int|string $uid): array + { + $references = []; + switch ($this->type) { + case 'db': { + $references['refLines'] = $this->makeRef($this->table, $uid, $request); + $references['refFromLines'] = $this->makeRefFrom($this->table, $uid, $request); + break; + } + case 'file': { + if ($this->fileObject && $this->fileObject->isIndexed()) { + $references['refLines'] = $this->makeRef('_FILE', $this->fileObject, $request); + } + break; + } + } + return $references; + } + + /** + * Get field name for specified table/column name + * + * @param string $fieldName Column name + */ + protected function getLabelForTableColumn(TcaSchema $schema, string $fieldName): string + { + if ($schema->hasField($fieldName)) { + $field = $schema->getField($fieldName); + $field = $field->getLabel() ? $this->getLanguageService()->sL($field->getLabel()) : $fieldName; + if (trim($field) === '') { + $field = $fieldName; + } + } else { + $field = $fieldName; + } + return $field; + } + + /** + * Returns the record actions + * + * @param int $uid + * @throws RouteNotFoundException + */ + protected function getRecordActions(TcaSchema $schema, $uid, ServerRequestInterface $request): array + { + if ($uid < 0) { + return []; + } + + $actions = []; + // Edit button + $urlParameters = [ + 'edit' => [ + $schema->getName() => [ + $uid => 'edit', + ], + ], + 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), + ]; + $actions['recordEditUrl'] = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters); + + // History button + $urlParameters = [ + 'element' => $schema->getName() . ':' . $uid, + 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), + ]; + $actions['recordHistoryUrl'] = (string)$this->uriBuilder->buildUriFromRoute('record_history', $urlParameters); + + if ($schema->getName() === 'pages') { + // Recordlist button + $actions['recordsModuleUrl'] = (string)$this->uriBuilder->buildUriFromRoute('records', ['id' => $uid, 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri()]); + + // retrieve record to get page language + $record = BackendUtility::getRecord($schema->getName(), $uid); + $previewUriBuilder = PreviewUriBuilder::create($record) + ->withRootLine(BackendUtility::BEgetRootLine($uid)); + + // View page button + $actions['previewUrlAttributes'] = $previewUriBuilder->serializeDispatcherAttributes(); + } + + return $actions; + } + + /** + * Make reference display + * + * @param string $table Table name + * @param int|File $ref Filename or uid + * @throws RouteNotFoundException + */ + protected function makeRef(string $table, $ref, ServerRequestInterface $request): array + { + $refLines = []; + $lang = $this->getLanguageService(); + // Files reside in sys_file table + if ($table === '_FILE') { + $selectTable = 'sys_file'; + $selectUid = $ref->getUid(); + } else { + $selectTable = $table; + $selectUid = $ref; + } + $queryBuilder = $this->connectionPool + ->getQueryBuilderForTable('sys_refindex'); + + $predicates = [ + $queryBuilder->expr()->eq( + 'ref_table', + $queryBuilder->createNamedParameter($selectTable) + ), + $queryBuilder->expr()->eq( + 'ref_uid', + $queryBuilder->createNamedParameter($selectUid, Connection::PARAM_INT) + ), + ]; + + $backendUser = $this->getBackendUser(); + if (!$backendUser->isAdmin()) { + $allowedSelectTables = GeneralUtility::trimExplode(',', $backendUser->groupData['tables_select']); + $predicates[] = $queryBuilder->expr()->in( + 'tablename', + $queryBuilder->createNamedParameter($allowedSelectTables, Connection::PARAM_STR_ARRAY) + ); + } + + $rows = $queryBuilder + ->select('*') + ->from('sys_refindex') + ->where(...$predicates) + ->executeQuery() + ->fetchAllAssociative(); + + // Compile information for title tag: + foreach ($rows as $row) { + if ($row['tablename'] === 'sys_file_reference') { + $row = $this->transformFileReferenceToRecordReference($row); + if ($row === null) { + continue; + } + } + if (!$this->tcaSchemaFactory->has($row['tablename'])) { + continue; + } + $schema = $this->tcaSchemaFactory->get($row['tablename']); + $line = []; + + $record = BackendUtility::getRecordWSOL($row['tablename'], $row['recuid']); + if ($record) { + if (!$this->canAccessPage($schema, $record)) { + continue; + } + $parentRecord = BackendUtility::getRecord('pages', $record['pid']); + $parentRecordTitle = is_array($parentRecord) + ? BackendUtility::getRecordTitle('pages', $parentRecord) + : ''; + $urlParameters = [ + 'edit' => [ + $row['tablename'] => [ + $row['recuid'] => 'edit', + ], + ], + 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), + ]; + $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters); + $line['url'] = $url; + $line['icon'] = $this->iconFactory->getIconForRecord($row['tablename'], $record, IconSize::SMALL)->render(); + $line['row'] = $row; + $line['record'] = $record; + $line['recordTitle'] = BackendUtility::getRecordTitle($row['tablename'], $record); + $line['parentRecord'] = $parentRecord; + $line['parentRecordTitle'] = $parentRecordTitle; + $line['title'] = $schema->getTitle($lang->sL(...)) ?: $row['tablename']; + $line['labelForTableColumn'] = $this->getLabelForTableColumn($schema, $row['field']); + $line['path'] = BackendUtility::getRecordPath($record['pid'], '', 0, 0); + $line['actions'] = $this->getRecordActions($schema, $row['recuid'], $request); + } else { + $line['row'] = $row; + $line['title'] = $schema->getTitle($lang->sL(...)) ?: $row['tablename']; + $line['labelForTableColumn'] = $this->getLabelForTableColumn($schema, $row['field']); + } + $refLines[] = $line; + } + return $refLines; + } + + /** + * Make reference display (what this elements points to) + * + * @param string $table Table name + * @param int $ref Filename or uid + */ + protected function makeRefFrom($table, $ref, ServerRequestInterface $request): array + { + $refFromLines = []; + $lang = $this->getLanguageService(); + + $queryBuilder = $this->connectionPool + ->getQueryBuilderForTable('sys_refindex'); + + $predicates = [ + $queryBuilder->expr()->eq( + 'tablename', + $queryBuilder->createNamedParameter($table) + ), + $queryBuilder->expr()->eq( + 'recuid', + $queryBuilder->createNamedParameter($ref, Connection::PARAM_INT) + ), + ]; + + $backendUser = $this->getBackendUser(); + if (!$backendUser->isAdmin()) { + $allowedSelectTables = GeneralUtility::trimExplode(',', $backendUser->groupData['tables_select']); + $predicates[] = $queryBuilder->expr()->in( + 'ref_table', + $queryBuilder->createNamedParameter($allowedSelectTables, Connection::PARAM_STR_ARRAY) + ); + } + + $rows = $queryBuilder + ->select('*') + ->from('sys_refindex') + ->where(...$predicates) + ->executeQuery() + ->fetchAllAssociative(); + + // Compile information for title tag: + foreach ($rows as $row) { + $line = []; + $record = BackendUtility::getRecordWSOL($row['ref_table'], $row['ref_uid']); + if (!$this->tcaSchemaFactory->has($row['ref_table'])) { + continue; + } + $schema = $this->tcaSchemaFactory->get($row['ref_table']); + if ($record) { + if (!$this->canAccessPage($schema, $record)) { + continue; + } + $urlParameters = [ + 'edit' => [ + $row['ref_table'] => [ + $row['ref_uid'] => 'edit', + ], + ], + 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), + ]; + $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters); + $line['url'] = $url; + $line['icon'] = $this->iconFactory->getIconForRecord($row['ref_table'], $record, IconSize::SMALL)->render(); + $line['row'] = $row; + $line['record'] = $record; + $line['recordTitle'] = BackendUtility::getRecordTitle($row['ref_table'], $record); + $line['title'] = $schema->getTitle($lang->sL(...)); + $line['labelForTableColumn'] = $this->getLabelForTableColumn($schema, $row['field']); + $line['path'] = BackendUtility::getRecordPath($record['pid'], '', 0); + $line['actions'] = $this->getRecordActions($schema, $row['ref_uid'], $request); + } else { + $line['row'] = $row; + $line['title'] = $schema->getTitle($lang->sL(...)); + $line['labelForTableColumn'] = $this->getLabelForTableColumn($schema, $row['field']); + } + $refFromLines[] = $line; + } + return $refFromLines; + } + + /** + * Convert FAL file reference (sys_file_reference) to reference index (sys_refindex) table format + */ + protected function transformFileReferenceToRecordReference(array $referenceRecord): ?array + { + $queryBuilder = $this->connectionPool + ->getQueryBuilderForTable('sys_file_reference'); + $queryBuilder->getRestrictions()->removeAll(); + $fileReference = $queryBuilder + ->select('*') + ->from('sys_file_reference') + ->where( + $queryBuilder->expr()->eq( + 'uid', + $queryBuilder->createNamedParameter($referenceRecord['recuid'], Connection::PARAM_INT) + ) + ) + ->executeQuery() + ->fetchAssociative(); + + return $fileReference ? [ + 'recuid' => $fileReference['uid_foreign'], + 'tablename' => $fileReference['tablenames'], + 'field' => $fileReference['fieldname'], + 'flexpointer' => '', + 'softref_key' => '', + 'sorting' => $fileReference['sorting_foreign'], + ] : null; + } + + /** + * @param array $record Record to be checked (ensure pid is resolved for workspaces) + */ + protected function canAccessPage(TcaSchema $schema, array $record): bool + { + $recordPid = (int)($schema->getName() === 'pages' ? $record['uid'] : $record['pid']); + $isInWebMount = (bool)$this->getBackendUser()->isInWebMount($schema->getName() === 'pages' ? $record : $record['pid']); + return $isInWebMount || ($recordPid === 0 && $schema->getCapability(TcaSchemaCapability::RestrictionRootLevel)->shallIgnoreRootLevelRestriction()); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/ContentElement/MoveElementController.php b/Classes/Controller/ContentElement/MoveElementController.php new file mode 100644 index 0000000..785dac0 --- /dev/null +++ b/Classes/Controller/ContentElement/MoveElementController.php @@ -0,0 +1,127 @@ +setUpBasicPageRendererForBackend( + $this->pageRenderer, + $this->extensionConfiguration, + $request, + $this->languageServiceFactory->createFromUserPreferences($this->getBackendUser()) + ); + $view = $this->backendViewFactory->create($request); + $queryParams = $request->getQueryParams(); + $contentOnly = $queryParams['contentOnly'] ?? false; + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/global-event-handler.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/tree/page-browser.js'); + $this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/wizard/move-content-element.js', 'MoveContentElement')->instance() + ); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_misc.xlf'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/Wizards/move_content_elements.xlf'); + + $view->assignMultiple(array_merge($this->getContentVariables($request), [ + 'contentOnly' => $contentOnly, + ])); + + $content = $view->render('ContentElement/MoveElement'); + if ($contentOnly) { + return new HtmlResponse($content); + } + $this->pageRenderer->setBodyContent('' . $content); + return new HtmlResponse($this->pageRenderer->render($request)); + } + + private function getContentVariables(ServerRequestInterface $request): array + { + $queryParams = $request->getQueryParams(); + $parsedBody = $request->getParsedBody(); + + $contentElementUid = (int)($parsedBody['uid'] ?? $queryParams['uid'] ?? 0); + $pageId = (int)($parsedBody['expandPage'] ?? $queryParams['expandPage'] ?? 0); + $sysLanguage = (int)($parsedBody['sys_language'] ?? $queryParams['sys_language'] ?? 0); + $makeCopy = (bool)($parsedBody['makeCopy'] ?? $queryParams['makeCopy'] ?? 0); + $permsClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW); + + if (!$contentElementUid) { + return []; + } + + $contentElement = BackendUtility::getRecordWSOL('tt_content', $contentElementUid); + $pageInfo = BackendUtility::readPageAccess($pageId, $permsClause); + $contentElementTitle = BackendUtility::getRecordTitle('tt_content', $contentElement); + $assigns = [ + 'record' => $contentElement, + 'makeCopyChecked' => $makeCopy, + 'pageInfo' => $pageInfo, + 'recordTitle' => BackendUtility::cropToTitleLength($contentElementTitle), + ]; + if (is_array($pageInfo) && $this->getBackendUser()->isInWebMount($pageInfo['uid'], $permsClause)) { + // Initialize the content position map: + $contentPositionMap = GeneralUtility::makeInstance(ContentMovingPagePositionMap::class); + $contentPositionMap->copyMode = $makeCopy ? 'copy' : 'move'; + $contentPositionMap->moveUid = $contentElementUid; + $contentPositionMap->cur_sys_language = $sysLanguage; + + $pageTitle = BackendUtility::getRecordTitle('pages', $pageInfo); + $assigns['pageRecord']['recordTooltip'] = BackendUtility::getRecordIconAltText($pageInfo, 'pages', false); + $assigns['pageRecord']['recordTitle'] = BackendUtility::cropToTitleLength($pageTitle); + $assigns['contentElementColumns'] = $contentPositionMap->printContentElementColumns($pageId, $pageInfo, $request); + } + return $assigns; + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/ContentElement/NewContentElementController.php b/Classes/Controller/ContentElement/NewContentElementController.php new file mode 100644 index 0000000..8f206de --- /dev/null +++ b/Classes/Controller/ContentElement/NewContentElementController.php @@ -0,0 +1,710 @@ +getParsedBody(); + $queryParams = $request->getQueryParams(); + + // Setting internal vars: + $this->id = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0); + $this->sys_language = (int)($parsedBody['language_tag'] ?? $queryParams['language_tag'] ?? 0); + $this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '', $request); + $colPos = $parsedBody['colPos'] ?? $queryParams['colPos'] ?? null; + $this->colPos = $colPos === null ? null : (int)$colPos; + $this->uid_pid = (int)($parsedBody['uid_pid'] ?? $queryParams['uid_pid'] ?? 0); + + // Getting the current page and receiving access information + $this->pageInfo = BackendUtility::readPageAccess($this->id, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)) ?: []; + + $action = (string)($parsedBody['action'] ?? $queryParams['action'] ?? 'wizard'); + if ($action === 'wizard') { + return $this->wizardAction($request); + } + if ($action === 'positionMap') { + return $this->positionMapAction($request); + } + return new HtmlResponse('Action not allowed', 400); + } + + /** + * Renders the wizard + */ + protected function wizardAction(ServerRequestInterface $request): ResponseInterface + { + if (!$this->id || $this->pageInfo === []) { + // No pageId or no access. + return new HtmlResponse('No Access'); + } + // Whether position selection must be performed (no colPos was yet defined) + $positionSelection = $this->colPos === null; + + // Get processed and modified wizard items + $wizardItems = $this->eventDispatcher->dispatch( + new ModifyNewContentElementWizardItemsEvent( + $this->getWizards($request), + $this->pageInfo, + $this->colPos, + $this->sys_language, + $this->uid_pid, + $request, + ) + )->getWizardItems(); + + $key = 'common'; + $categories = []; + foreach ($wizardItems as $wizardKey => $wizardItem) { + // An item is either a header or an item rendered with title/description and icon: + if (isset($wizardItem['header'])) { + $key = $wizardKey; + $categories[$key] = [ + 'identifier' => $key, + 'label' => $wizardItem['header'] ?: '-', + 'items' => [], + ]; + } else { + // Get default values for the wizard item + $defaultValues = (array)($wizardItem['defaultValues'] ?? []); + + // Initialize the view variables for the item + $item = [ + 'identifier' => $wizardKey, + 'icon' => $wizardItem['iconIdentifier'] ?? '', + 'iconOverlay' => $wizardItem['iconOverlay'] ?? '', + 'label' => $wizardItem['title'] ?? '', + 'description' => $wizardItem['description'] ?? '', + 'defaultValues' => $defaultValues, + ]; + // If the URL was already created (e.g. via the PSR-14 event) this needs to be + // kept and not overwritten + if (isset($wizardItem['url'])) { + $item['url'] = $wizardItem['url']; + if ($positionSelection) { + $item['requestType'] = 'ajax'; + $item['saveAndClose'] = (bool)($wizardItem['saveAndClose'] ?? false); + } + } elseif ($positionSelection) { + $item['url'] = (string)$this->uriBuilder + ->buildUriFromRoute( + 'new_content_element_wizard', + [ + 'action' => 'positionMap', + 'id' => $this->id, + 'sys_language_uid' => $this->sys_language, + 'returnUrl' => $this->returnUrl, + ] + ); + $item['requestType'] = 'ajax'; + $item['saveAndClose'] = (bool)($wizardItem['saveAndClose'] ?? false); + } else { + // In case no position has to be selected, we can just add the target + if ($wizardItem['saveAndClose'] ?? false) { + // Go to DataHandler directly instead of FormEngine + $item['url'] = (string)$this->uriBuilder->buildUriFromRoute('tce_db', [ + 'data' => [ + 'tt_content' => [ + StringUtility::getUniqueId('NEW') => array_replace($defaultValues, [ + 'colPos' => $this->colPos, + 'pid' => $this->uid_pid, + 'sys_language_uid' => $this->sys_language, + ]), + ], + ], + 'redirect' => $this->returnUrl, + ]); + } else { + $item['url'] = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [ + 'edit' => [ + 'tt_content' => [ + $this->uid_pid => 'new', + ], + ], + 'module' => '_CURRENT_MODULE_', + 'returnUrl' => $this->returnUrl, + 'defVals' => [ + 'tt_content' => array_replace($defaultValues, [ + 'colPos' => $this->colPos, + 'sys_language_uid' => $this->sys_language, + ]), + ], + ]); + } + } + $categories[$key]['items'][] = $item; + } + } + + // Unset empty categories + foreach ($categories as $key => $category) { + if ($category['items'] === []) { + unset($categories[$key]); + } + } + + $view = $this->backendViewFactory->create($request); + $view->assignMultiple([ + 'positionSelection' => $positionSelection, + 'categoriesJson' => GeneralUtility::jsonEncodeForHtmlAttribute($categories, false), + ]); + return new HtmlResponse($view->render('NewContentElement/Wizard')); + } + + /** + * Renders the position map + */ + protected function positionMapAction(ServerRequestInterface $request): ResponseInterface + { + $pageInfo = BackendUtility::readPageAccess($this->id, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)); + + $posMap = GeneralUtility::makeInstance(ContentCreationPagePositionMap::class); + $posMap->cur_sys_language = $this->sys_language; + $posMap->defVals = (array)($request->getParsedBody()['defVals'] ?? []); + $posMap->saveAndClose = (bool)($request->getParsedBody()['saveAndClose'] ?? false); + $posMap->R_URI = $this->returnUrl; + $view = $this->backendViewFactory->create($request); + $view->assign('posMap', $posMap->printContentElementColumns($this->id, $pageInfo, $request)); + return new HtmlResponse($view->render('NewContentElement/PositionMap')); + } + + /** + * Returns the array of elements in the wizard display. + * For the plugin section there is support for adding elements there from a global variable. + */ + protected function getWizards(ServerRequestInterface $request): array + { + $wizards = $this->loadAvailableWizards(); + $newContentElementWizardTsConfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['wizards.']['newContentElement.'] ?? []; + $wizardsFromPageTSConfig = $this->migrateCommonGroupToDefault($newContentElementWizardTsConfig['wizardItems.'] ?? []); + $wizardsFromPageTSConfig = $this->migratePositionalCommonGroupToDefault($wizardsFromPageTSConfig); + $wizards = $this->mergeContentElementWizardsWithPageTSConfigWizards($wizards, $wizardsFromPageTSConfig); + $wizards = $this->removeWizardsByPageTs($wizards, $newContentElementWizardTsConfig); + $wizards = $this->removeWizardsByBackendLayoutColPosRestriction($wizards, $this->pageInfo, $this->colPos, $request); + if ($wizards === []) { + return []; + } + $wizardItems = []; + foreach ($wizards as $groupKey => $wizardGroup) { + $wizards[$groupKey] = $this->prepareDependencyOrdering($wizards[$groupKey], 'before'); + $wizards[$groupKey] = $this->prepareDependencyOrdering($wizards[$groupKey], 'after'); + } + $orderedWizards = $this->orderWizards($wizards); + foreach ($orderedWizards as $groupKey => $wizardGroup) { + $groupKey = rtrim($groupKey, '.'); + $groupItems = []; + $wizardElements = $wizardGroup['elements.'] ?? []; + if (is_array($wizardElements)) { + $wizardElements = $this->orderElements($wizardElements); + foreach ($wizardElements as $itemKey => $itemConf) { + $itemKey = rtrim($itemKey, '.'); + if ($itemConf !== []) { + $groupItems[$groupKey . '_' . $itemKey] = $this->prepareWizardItem($itemConf); + } + } + } + if (!empty($groupItems)) { + $wizardItems[$groupKey]['header'] = $this->getLanguageService()->sL($wizardGroup['header'] ?? ''); + $wizardItems = array_merge($wizardItems, $groupItems); + } + } + + // Remove elements where preset values are not allowed: + return $this->removeInvalidWizardItems($wizardItems); + } + + protected function loadAvailableWizards(): array + { + $schema = $this->tcaSchemaFactory->get('tt_content'); + // Foreign table support for TypeInformation is not supported in tt_content + $typeField = $schema->getSubSchemaTypeInformation()->getFieldName(); + $fieldConfig = $schema->hasField($typeField) ? $schema->getField($typeField)->getConfiguration() : []; + $items = $fieldConfig['items'] ?? []; + $itemGroups = $fieldConfig['itemGroups'] ?? []; + $groupedWizardItems = []; + foreach (array_keys($itemGroups) as $groupIdentifier) { + $groupedWizardItems[$groupIdentifier . '.']['header'] = $itemGroups[$groupIdentifier]; + } + foreach ($items as $item) { + $selectItem = SelectItem::fromTcaItemArray($item); + if ($selectItem->isDivider()) { + continue; + } + $recordType = $selectItem->getValue(); + $groupIdentifier = $selectItem->getGroup(); + $groupedWizardItems[$groupIdentifier . '.']['elements.'] ??= []; + // In case this group is not defined in itemGroups, use the group identifier as label. + $groupedWizardItems[$groupIdentifier . '.']['header'] ??= $groupIdentifier; + $itemDescription = $selectItem->getDescription(); + $wizardEntry = [ + 'iconIdentifier' => $selectItem->getIcon(), + 'iconOverlay' => $selectItem->getIconOverlay(), + 'title' => $selectItem->getLabel(), + 'description' => $itemDescription['description'] ?? ($itemDescription ?? ''), + 'defaultValues' => [ + 'CType' => $recordType, + ], + ]; + if ($schema->hasSubSchema($recordType)) { + $wizardEntry = array_replace_recursive($wizardEntry, $schema->getSubSchema($recordType)->getRawConfiguration()['creationOptions'] ?? []); + } + $groupedWizardItems[$groupIdentifier . '.']['elements.'][$recordType . '.'] = $wizardEntry; + } + return $groupedWizardItems; + } + + /** + * This method merges Content Element wizards defined by TCA with wizards defined in PageTSConfig. + * PageTS has precedence. + * It might happen that both TCA and PageTS define an entry with exactly the same default values. + * In such a case, the automatically added TCA entry is dropped. + */ + protected function mergeContentElementWizardsWithPageTSConfigWizards(array $contentElementWizards, array $pageTsConfigWizards): array + { + $uniqueDefaultValuesInPageTsWizards = []; + foreach ($pageTsConfigWizards as $wizard) { + foreach ($wizard['elements.'] ?? [] as $elementConfig) { + $defaultValues = $elementConfig['tt_content_defValues.'] ?? []; + if ($defaultValues === []) { + continue; + } + ksort($defaultValues); + $uniqueDefaultValuesInPageTsWizards[] = $defaultValues; + } + } + foreach ($contentElementWizards as $group => $wizard) { + foreach ($wizard['elements.'] ?? [] as $key => $elementConfig) { + // Remove duplicated entry. + $defaultValues = $elementConfig['defaultValues']; + ksort($defaultValues); + if (in_array($defaultValues, $uniqueDefaultValuesInPageTsWizards, true)) { + unset($contentElementWizards[$group]['elements.'][$key]); + } + } + } + $mergedWizards = array_replace_recursive($contentElementWizards, $pageTsConfigWizards); + return $mergedWizards; + } + + /** + * Orders elements within a wizard group using before/after configuration. + * Similar to orderWizards() but for individual content elements. + */ + protected function orderElements(array $elements): array + { + // Check if any element has before/after configuration + // and return early if no reordering is required. + if (!$this->hasPositionalArguments($elements)) { + return $elements; + } + + // Prepare elements for dependency ordering. + // Create implicit chain based on initial order for consecutive elements + // without explicit dependencies, preserving relative order while allowing + // explicit positioning. + $preparedElements = []; + + // First pass: prepare all elements with their explicit dependencies + foreach ($elements as $elementKey => $element) { + $preparedElement = $element; + // Prepare before/after values (they might be comma-separated strings) + $preparedElement = $this->prepareDependencyOrdering($preparedElement, 'before'); + $preparedElement = $this->prepareDependencyOrdering($preparedElement, 'after'); + $preparedElements[$elementKey] = $preparedElement; + } + + // Second pass: add implicit chain for consecutive elements without explicit dependencies + // This preserves relative order within blocks, while explicit dependencies can reorder them + $previousIndependentElementKey = null; + foreach ($elements as $elementKey => $element) { + $isIndependent = empty($element['before']) && empty($element['after']); + if ($isIndependent) { + // Element without explicit dependency: chain with previous independent element + if ($previousIndependentElementKey !== null) { + $existingAfter = $preparedElements[$elementKey]['after'] ?? []; + if (!in_array($previousIndependentElementKey, $existingAfter, true)) { + $preparedElements[$elementKey]['after'] = array_merge($existingAfter, [$previousIndependentElementKey]); + } + } + $previousIndependentElementKey = $elementKey; + } + } + // Use dependency ordering service to order elements + return $this->dependencyOrderingService->orderByDependencies($preparedElements); + } + + protected function hasPositionalArguments(array $elements): bool + { + foreach ($elements as $element) { + if (!empty($element['before']) || !empty($element['after'])) { + return true; + } + } + return false; + } + + /** + * There are two separate ordering systems for wizard groups: + * 1. TCA itemGroup sorting by associative array item order. + * 2. PageTS defined order by "before" and "after". + * + * System 1. has a well-defined order, where every item defines "after" (linked list). + * Due to this, the two system cannot be combined. + * As soon as system 2 defines at least one "before" or "after" it takes over. + */ + protected function orderWizards(array $wizards): array + { + // First round: Order by TCA defined sorting. + $hasAtLeastOnePositionalArgument = false; + foreach ($wizards as $group => $wizard) { + if (isset($wizard['before'])) { + $hasAtLeastOnePositionalArgument = true; + $wizards[$group]['pageTsBefore'] = $wizard['before']; + unset($wizards[$group]['before']); + } + if (isset($wizard['after'])) { + $hasAtLeastOnePositionalArgument = true; + $wizards[$group]['pageTsAfter'] = $wizard['after']; + unset($wizards[$group]['after']); + } + } + // No order defined by pageTS. Use TCA sorting. + if (!$hasAtLeastOnePositionalArgument) { + $schema = $this->tcaSchemaFactory->get('tt_content'); + // Foreign table support for TypeInformation is not supported in tt_content + $typeField = $schema->getSubSchemaTypeInformation()->getFieldName(); + $fieldConfig = $schema->hasField($typeField) ? $schema->getField($typeField)->getConfiguration() : []; + $itemGroups = $fieldConfig['itemGroups'] ?? []; + // Auto-set positional information based on TCA itemGroups sorting. + $lastGroup = null; + foreach (array_keys($itemGroups) as $groupIdentifier) { + if (!array_key_exists($groupIdentifier . '.', $wizards)) { + continue; + } + if ($lastGroup !== null) { + $wizards[$groupIdentifier . '.']['after'] = [$lastGroup . '.']; + } + $lastGroup = $groupIdentifier; + } + return $this->dependencyOrderingService->orderByDependencies($wizards); + } + // Override order by pageTsConfig. + foreach ($wizards as $group => $wizard) { + // Unset "after" previously set by Content Element wizards. + unset($wizards[$group]['after']); + if (isset($wizard['pageTsBefore'])) { + $wizards[$group]['before'] = $wizard['pageTsBefore']; + unset($wizards[$group]['pageTsBefore']); + } + if (isset($wizard['pageTsAfter'])) { + $wizards[$group]['after'] = $wizard['pageTsAfter']; + unset($wizards[$group]['pageTsAfter']); + } + } + return $this->dependencyOrderingService->orderByDependencies($wizards); + } + + /** + * This method returns the wizard items, defined in Page TSconfig for b/w + * compatibility. + * + * Additionally, it migrates previously defined wizard items in the + * `common` group to the new `default` group, which is defined in TCA. + * + * @param array $wizardsFromPageTs + * @return array + */ + protected function migrateCommonGroupToDefault(array $wizardsFromPageTs): array + { + if (!array_key_exists('common.', $wizardsFromPageTs)) { + // In case "common." is not defined, just return the wizards, which are still defined via Page TSconfig + return $wizardsFromPageTs; + } + + // Prepare "removeItems" to be merged + if ($wizardsFromPageTs['default.']['elements.']['removeItems'] ?? false) { + $wizardsFromPageTs['default.']['removeItems'] = GeneralUtility::trimExplode(',', $wizardsFromPageTs['default.']['elements.']['removeItems'] ?? '', true); + } elseif ($wizardsFromPageTs['default.']['removeItems'] ?? false) { + $wizardsFromPageTs['default.']['removeItems'] = GeneralUtility::trimExplode(',', $wizardsFromPageTs['default.']['removeItems'], true); + } + + if ($wizardsFromPageTs['common.']['elements.']['removeItems'] ?? false) { + $wizardsFromPageTs['common.']['removeItems'] = GeneralUtility::trimExplode(',', $wizardsFromPageTs['common.']['elements.']['removeItems'] ?? '', true); + } elseif ($wizardsFromPageTs['common.']['removeItems'] ?? false) { + $wizardsFromPageTs['common.']['removeItems'] = GeneralUtility::trimExplode(',', $wizardsFromPageTs['common.']['removeItems'], true); + } + + $defaultItems = array_merge_recursive($wizardsFromPageTs['default.'] ?? [], $wizardsFromPageTs['common.']); + unset($wizardsFromPageTs['common.']); + + if ($defaultItems !== []) { + $wizardsFromPageTs['default.'] = $defaultItems; + } + + return $wizardsFromPageTs; + } + + protected function migratePositionalCommonGroupToDefault(array $wizards): array + { + foreach ($wizards as $group => $wizard) { + if (($wizard['before'] ?? '') === 'common') { + $wizards[$group]['before'] = 'default'; + } + if (($wizard['after'] ?? '') === 'common') { + $wizards[$group]['after'] = 'default'; + } + } + return $wizards; + } + + protected function prepareWizardItem(array $itemConf): array + { + // Just replace the "known" keys of $itemConf. This way extensions are able to set custom keys, which are not + // used by the controller, but might be evaluated by listeners of the ModifyNewContentElementWizardItemsEvent. + $itemConf = array_replace_recursive( + $itemConf, + [ + 'title' => trim($this->getLanguageService()->sL($itemConf['title'] ?? '')), + 'description' => trim($this->getLanguageService()->sL($itemConf['description'] ?? '')), + 'iconIdentifier' => $itemConf['iconIdentifier'] ?? null, + 'saveAndClose' => (bool)($itemConf['saveAndClose'] ?? false), + 'defaultValues' => array_replace_recursive( + $itemConf['tt_content_defValues'] ?? [], + $itemConf['tt_content_defValues.'] ?? [], + $itemConf['defaultValues'] ?? [] + ), + ] + ); + unset($itemConf['tt_content_defValues'], $itemConf['tt_content_defValues.']); + return $itemConf; + } + + protected function removeWizardsByPageTs(array $wizards, mixed $wizardsItemsPageTs): array + { + $removeWizardItems = $wizardsItemsPageTs['wizardItems.']['removeItems'] ?? []; + if (is_string($removeWizardItems)) { + $removeWizardItems = GeneralUtility::trimExplode(',', $removeWizardItems, true); + } + + foreach ($wizards as $key => &$wizard) { + // Leave out removeItems etc. + if (is_string($wizard)) { + unset($wizards[$key]); + continue; + } + if (in_array(rtrim((string)$key, '.'), $removeWizardItems, true)) { + unset($wizards[$key]); + continue; + } + $removeWizardElements = $wizardsItemsPageTs['wizardItems.'][$key]['removeItems'] ?? []; + if (is_string($removeWizardElements)) { + $removeWizardElements = GeneralUtility::trimExplode(',', $removeWizardElements, true); + } + foreach ($wizard['elements.'] ?? [] as $identifier => $element) { + if (in_array(rtrim((string)$identifier, '.'), $removeWizardElements, true)) { + unset($wizard['elements.'][$identifier]); + } + } + } + + return $wizards; + } + + protected function removeWizardsByBackendLayoutColPosRestriction(array $wizardGroups, array $pageInfo, ?int $colPos, ServerRequestInterface $request): array + { + // Force colPos to 0 if null to apply restrictions for 0 by default. + $colPos = (int)$colPos; + // This is the page uid of a workspace overlay already so backend layouts of workspace + // changed or moved pages should be considered correctly. + $pid = (int)$pageInfo['uid']; + $backendLayout = $this->backendLayoutView->getBackendLayoutForPage($pid); + $columnConfiguration = $this->backendLayoutView->getColPosConfigurationForPage($backendLayout, $colPos, $pid, $request); + if (!empty($columnConfiguration['allowedContentTypes'])) { + $allowedContentTypes = GeneralUtility::trimExplode(',', $columnConfiguration['allowedContentTypes'], true); + foreach ($wizardGroups as $wizardGroupName => $wizards) { + foreach (($wizards['elements.'] ?? []) as $wizardKey => $wizard) { + $cType = $wizard['defaultValues']['CType'] ?? $wizard['tt_content_defValues.']['CType'] ?? ''; + if (empty($cType)) { + continue; + } + if (!in_array(trim($cType), $allowedContentTypes, true)) { + unset($wizardGroups[$wizardGroupName]['elements.'][$wizardKey]); + } + } + } + } + if (!empty($columnConfiguration['disallowedContentTypes'])) { + $disAllowedContentTypes = GeneralUtility::trimExplode(',', $columnConfiguration['disallowedContentTypes'], true); + foreach ($wizardGroups as $wizardGroupName => $wizards) { + foreach (($wizards['elements.'] ?? []) as $wizardKey => $wizard) { + $cType = $wizard['defaultValues']['CType'] ?? $wizard['tt_content_defValues.']['CType'] ?? ''; + if (empty($cType)) { + continue; + } + if (in_array(trim($cType), $disAllowedContentTypes, true)) { + unset($wizardGroups[$wizardGroupName]['elements.'][$wizardKey]); + } + } + } + } + return $wizardGroups; + } + + /** + * Checks the array for elements which might contain invalid default values and will unset them! + * Looks for the "defaultValues" key in each element and if found it will traverse that array + * as fieldname / value pairs and check. + */ + protected function removeInvalidWizardItems(array $wizardItems): array + { + $schema = $this->tcaSchemaFactory->get('tt_content'); + $removeItems = []; + $keepItems = []; + // Get TCEFORM from TSconfig of current page + $TCEFORM_TSconfig = FormEngineUtility::getTCEFORM_TSconfig('tt_content', ['pid' => $this->id]); + $backendUser = $this->getBackendUser(); + // Traverse wizard items: + foreach ($wizardItems as $key => $cfg) { + if (!is_array($cfg['defaultValues'] ?? false)) { + continue; + } + + // This is not a group; this is likely broken configuration + if ($cfg['defaultValues'] === []) { + unset($wizardItems[$key]); + } + + // If defaultValues are defined, check access by traversing all fields with default values: + foreach ($cfg['defaultValues'] as $fieldName => $value) { + if (!$schema->hasField($fieldName)) { + continue; + } + // Get information about if the field value is OK: + $config = $schema->getField($fieldName)->getConfiguration(); + $userNotAllowedToAccess = ($config['type'] ?? '') === 'select' && ($config['authMode'] ?? false) + && !$backendUser->checkAuthMode('tt_content', $fieldName, $value); + // Check removeItems + if (!isset($removeItems[$fieldName]) && ($TCEFORM_TSconfig[$fieldName]['removeItems'] ?? false)) { + $removeItems[$fieldName] = array_flip(GeneralUtility::trimExplode( + ',', + $TCEFORM_TSconfig[$fieldName]['removeItems'], + true + )); + } + // Check keepItems + if (!isset($keepItems[$fieldName]) && ($TCEFORM_TSconfig[$fieldName]['keepItems'] ?? false)) { + $keepItems[$fieldName] = array_flip(GeneralUtility::trimExplode( + ',', + $TCEFORM_TSconfig[$fieldName]['keepItems'], + true + )); + } + $isNotInKeepItems = !empty($keepItems[$fieldName]) && !isset($keepItems[$fieldName][$value]); + if ($userNotAllowedToAccess || ($fieldName === 'CType' && (isset($removeItems[$fieldName][$value]) || $isNotInKeepItems))) { + // Remove element all together: + unset($wizardItems[$key]); + break; + } + // Add the parameter: + $wizardItems[$key]['defaultValues'][$fieldName] = $this->getLanguageService()->sL($value); + } + } + return $wizardItems; + } + + /** + * Prepare a wizard tab configuration for sorting. + */ + protected function prepareDependencyOrdering(array $wizardGroup, string $key): array + { + if (is_string($wizardGroup[$key] ?? null)) { + $wizardGroup[$key] = GeneralUtility::trimExplode(',', $wizardGroup[$key], true); + } + if (is_array($wizardGroup[$key] ?? null)) { + $wizardGroup[$key] = array_map( + static fn(string $s): string => rtrim($s, '.') . '.', + $wizardGroup[$key] + ); + } + return $wizardGroup; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/ContextMenuController.php b/Classes/Controller/ContextMenuController.php new file mode 100644 index 0000000..47b945c --- /dev/null +++ b/Classes/Controller/ContextMenuController.php @@ -0,0 +1,78 @@ +getQueryParams(); + $table = $params['table'] ?? ''; + $identifier = $params['uid'] ?? ''; + $context = $params['context'] ?? ''; + + if ($table === '' || $identifier === '') { + return new JsonResponse([], 400); + } + + $items = $contextMenu->getItems($table, $identifier, $context); + return new JsonResponse($items); + } + + public function clipboardAction(ServerRequestInterface $request): ResponseInterface + { + $clipboard = GeneralUtility::makeInstance(Clipboard::class); + $clipboard->initializeClipboard($request); + $clipboard->lockToNormal(); + + $queryParams = $request->getQueryParams(); + $parsedBody = $request->getParsedBody(); + $clipboardCommand = array_replace_recursive($queryParams['CB'] ?? [], $parsedBody['CB'] ?? []); + + // URL-decoded keys are required for the clipboard to recognize file identifiers (e.g. _FILE|...) + if (isset($clipboardCommand['el']) && is_array($clipboardCommand['el'])) { + $decodedElements = []; + foreach ($clipboardCommand['el'] as $key => $value) { + $decodedElements[urldecode((string)$key)] = $value; + } + $clipboardCommand['el'] = $decodedElements; + } + + $clipboard->setCmd($clipboardCommand); + $clipboard->cleanCurrent(); + $clipboard->endClipboard(); + + return new JsonResponse([]); + } +} diff --git a/Classes/Controller/ContextualRecordEditController.php b/Classes/Controller/ContextualRecordEditController.php new file mode 100644 index 0000000..ac7beb9 --- /dev/null +++ b/Classes/Controller/ContextualRecordEditController.php @@ -0,0 +1,453 @@ +getMethod() === 'POST' ? $this->persistAction($request) : $this->renderAction($request); + } + + /** + * Handle POST: process save/close via DataHandler and redirect back + */ + private function persistAction(ServerRequestInterface $request): ResponseInterface + { + $queryParams = $request->getQueryParams(); + $editConf = $this->parseAndValidateEditConf($queryParams['edit'] ?? []); + $table = $editConf['table']; + $uid = $this->resolveOverlayUid($table, $editConf['uid']); + + $requestAction = FormAction::createFromRequest($request); + + // Handle close (without save) + if ($requestAction->shouldHandleDocumentClosing()) { + return $this->redirectToSelf($queryParams, ['closed' => '1']); + } + + $saveSucceeded = false; + if ($requestAction->shouldProcessData()) { + $saveSucceeded = $this->processData($request, $table, $uid); + if ($saveSucceeded && $requestAction->shouldCloseAfterSave()) { + return $this->redirectToSelf($queryParams, ['closed' => '1', 'justSaved' => '1']); + } + } + + // POST-redirect-GET + $flags = ['edit' => [$table => [$uid => 'edit']]]; + if ($saveSucceeded) { + $flags['justSaved'] = '1'; + } + return $this->redirectToSelf($queryParams, $flags); + } + + /** + * Handle GET: compile the FormEngine form and render the contextual edit template. + */ + private function renderAction(ServerRequestInterface $request): ResponseInterface + { + $view = $this->moduleTemplateFactory->create($request); + $view->setUiBlock(true); + + $queryParams = $request->getQueryParams(); + $editConf = $this->parseAndValidateEditConf($queryParams['edit'] ?? []); + $table = $editConf['table']; + $uid = $this->resolveOverlayUid($table, $editConf['uid']); + + $returnUrl = GeneralUtility::sanitizeLocalUrl($queryParams['returnUrl'] ?? '', $request); + $overrideVals = is_array($queryParams['overrideVals'] ?? false) ? $queryParams['overrideVals'] : []; + $columnsOnly = $this->prepareColumnsOnlyConfiguration($queryParams['columnsOnly'] ?? null, $table); + $module = $this->moduleProvider->getModule((string)($queryParams['module'] ?? ''), $this->getBackendUser()); + + if ($module !== null) { + $view->setModuleName($module->getIdentifier()); + } + + // Compile FormEngine form + $currentEditingUrl = $this->uriBuilder->buildUriFromRoute('record_edit_contextual', array_merge($queryParams, [ + 'edit' => [$table => [$uid => 'edit']], + 'returnUrl' => $returnUrl, + ])); + + $formResult = $this->compileForm($request, $view, $table, $uid, $overrideVals, $columnsOnly, $currentEditingUrl); + $firstEl = $formResult['element'] ?? null; + if ($firstEl !== null) { + $this->formResultHandler->addAssets($formResult['results']); + $body = ' +
+ ' . $formResult['results']->getHtml() . ' + + +
'; + } else { + $view->setUiBlock(false); + $body = $formResult['errorHtml'] ?? $this->getInfobox( + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:noEditForm.message'), + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:noEditForm'), + ); + } + + $recordTitle = $firstEl !== null && trim($firstEl->title) !== '' + ? $firstEl->title + : '[' . $this->getLanguageService()->sL('core.core:labels.no_title') . ']'; + $recordTitle = BackendUtility::cropToTitleLength($recordTitle); + + // Contextual JS module with options + $contextualOptions = []; + if ($queryParams['justSaved'] ?? false) { + $contextualOptions['justSaved'] = true; + $contextualOptions['savedRecordTitle'] = $recordTitle; + } + if ($queryParams['closed'] ?? false) { + $contextualOptions['closed'] = true; + } + $this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/contextual-record-edit.js')->instance($contextualOptions) + ); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/context-menu.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/localization.js'); + + // Template variables + $view->assign('bodyHtml', $body); + $view->assign('recordTitle', $recordTitle); + + // Full edit URL points to the standard EditDocumentController + $fullEditParams = [ + 'edit' => [$table => [$uid => 'edit']], + 'returnUrl' => $returnUrl, + ]; + if ($module !== null) { + $fullEditParams['module'] = $module->getIdentifier(); + } + $view->assign('fullEditUrl', (string)$this->uriBuilder->buildUriFromRoute('record_edit', $fullEditParams)); + + return $view->renderResponse('Form/ContextualRecordEdit'); + } + + /** + * Parse and validate the edit configuration. Ensures exactly one record with command "edit". + * + * @return array{table: string, uid: int} + */ + private function parseAndValidateEditConf(array|string $editConf): array + { + if (!is_array($editConf)) { + throw new \InvalidArgumentException('Invalid edit configuration', 1772580316); + } + $backendUser = $this->getBackendUser(); + foreach ($editConf as $table => $conf) { + if (!is_array($conf) || !$this->tcaSchemaFactory->has($table)) { + continue; + } + if (!$backendUser->check('tables_modify', $table)) { + continue; + } + foreach ($conf as $uidList => $command) { + if ($command !== 'edit') { + continue; + } + $uid = (int)$uidList; + if ($uid > 0) { + return ['table' => $table, 'uid' => $uid]; + } + } + } + throw new \InvalidArgumentException('ContextualRecordEditController requires exactly one existing record to edit', 1772580317); + } + + private function prepareColumnsOnlyConfiguration(mixed $columnsOnly, string $table): array + { + if (!is_array($columnsOnly) || $columnsOnly === []) { + return []; + } + $finalColumnsOnly = array_map( + static fn($fields) => is_array($fields) ? $fields : GeneralUtility::trimExplode(',', $fields, true), + $columnsOnly + ); + // Add slug generator fields as hidden fields + if (!empty($finalColumnsOnly[$table]) && $this->tcaSchemaFactory->has($table)) { + $schema = $this->tcaSchemaFactory->get($table); + foreach ($finalColumnsOnly[$table] as $fieldName) { + if (!$schema->hasField($fieldName)) { + continue; + } + $field = $schema->getField($fieldName); + $postModifiers = $field->getConfiguration()['generatorOptions']['postModifiers'] ?? []; + if ($field->isType(\TYPO3\CMS\Core\DataHandling\TableColumnType::SLUG) + && (!is_array($postModifiers) || $postModifiers === []) + ) { + $fieldGroups = $field->getConfiguration()['generatorOptions']['fields'] ?? []; + if (is_string($fieldGroups)) { + $fieldGroups = [$fieldGroups]; + } + foreach ($fieldGroups as $fields) { + $finalColumnsOnly['__hiddenGeneratorFields'][$table] = array_merge( + $finalColumnsOnly['__hiddenGeneratorFields'][$table] ?? [], + (is_array($fields) ? $fields : GeneralUtility::trimExplode(',', $fields, true)) + ); + } + } + } + if (!empty($finalColumnsOnly['__hiddenGeneratorFields'][$table])) { + $finalColumnsOnly['__hiddenGeneratorFields'][$table] = array_diff( + array_unique($finalColumnsOnly['__hiddenGeneratorFields'][$table]), + $finalColumnsOnly[$table] + ); + } + } + return $finalColumnsOnly; + } + + /** + * Process save data via DataHandler. + * + * @return bool True if at least one record was saved without errors + */ + private function processData(ServerRequestInterface $request, string $table, int $uid): bool + { + $parsedBody = $request->getParsedBody(); + $dataMap = $parsedBody['data'] ?? []; + + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $dataHandler->setControl($parsedBody['control'] ?? []); + $dataHandler->start($dataMap, $parsedBody['cmd'] ?? []); + + if (is_array($parsedBody['mirror'] ?? null)) { + $dataHandler->setMirror($parsedBody['mirror']); + } + + $dataHandler->process_datamap(); + $dataHandler->process_cmdmap(); + + // Check if save succeeded (no errors for this record) + $erroneousRecords = $dataHandler->printLogErrorMessages(); + return !in_array($table . '.' . $uid, $erroneousRecords, true) && isset($dataMap[$table][$uid]); + } + + /** + * @return array{element: FormElementData, results: FormResultCollection}|array{errorHtml: string} + */ + private function compileForm( + ServerRequestInterface $request, + ModuleTemplate $view, + string $table, + int $uid, + array $overrideVals, + array $columnsOnly, + UriInterface $currentEditingUrl, + ): array { + try { + $formDataCompilerInput = [ + 'request' => $request, + 'tableName' => $table, + 'vanillaUid' => $uid, + 'command' => 'edit', + 'returnUrl' => (string)$currentEditingUrl, + ]; + if ($overrideVals !== [] && is_array($overrideVals[$table] ?? null)) { + $formDataCompilerInput['overrideValues'] = $overrideVals[$table]; + } + + $formData = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(TcaDatabaseRecord::class)); + + // Display "is-locked" message + $lockInfo = BackendUtility::isRecordLocked($table, $formData['databaseRow']['uid']); + if ($lockInfo) { + $view->addFlashMessage($lockInfo['msg'], '', \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::WARNING); + } + + $formElementData = new FormElementData( + title: $formData['recordTitle'], + table: $table, + uid: $formData['databaseRow']['uid'], + pid: $formData['databaseRow']['pid'] ?? 0, + record: $formData['databaseRow'], + viewId: 0, + command: 'edit', + userPermissionOnPage: $formData['userPermissionOnPage'], + ); + + BackendUtility::lockRecords($table, $formElementData->uid, $table === 'tt_content' ? $formElementData->pid : 0); + + if (!empty($columnsOnly[$table])) { + $formData['fieldListToRender'] = implode(',', $columnsOnly[$table]); + if (!empty($columnsOnly['__hiddenGeneratorFields'][$table])) { + $formData['hiddenFieldListToRender'] = implode(',', $columnsOnly['__hiddenGeneratorFields'][$table]); + } + } + + $formData['renderType'] = 'formWrapContainer'; + $formResult = $this->nodeFactory->create($formData)->render(); + $formResult = $this->formResultFactory->create($formResult); + $formResults = new FormResultCollection(); + $formResults->add($formResult); + + return ['element' => $formElementData, 'results' => $formResults]; + } catch (NoFieldsToRenderException) { + return ['errorHtml' => $this->getInfobox( + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:noFieldsEditForm.message'), + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:noFieldsEditForm'), + )]; + } catch (AccessDeniedException $e) { + return ['errorHtml' => $this->getInfobox( + $e->getMessage(), + $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noEditPermission'), + )]; + } catch (DatabaseRecordException|DatabaseRecordWorkspaceDeletePlaceholderException $e) { + return ['errorHtml' => $this->getInfobox($e->getMessage())]; + } + } + + /** + * Redirect back to this controller with additional flags for the JS module. + */ + private function redirectToSelf(array $queryParams, array $additionalParams): ResponseInterface + { + $queryParams = array_merge($queryParams, $additionalParams); + $url = $this->uriBuilder->buildUriFromRoute('record_edit_contextual', $queryParams); + return new RedirectResponse($url, 302); + } + + /** + * Resolve the workspace-aware UID for a single record. + * In a workspace, the live UID is replaced with the workspace overlay UID. + */ + private function resolveOverlayUid(string $table, int $uid): int + { + $record = $this->getRecordForEdit($table, $uid); + if (is_array($record)) { + return (int)$record['uid']; + } + return $uid; + } + + /** + * Get record for editing, resolving workspace versions. + * + * @return array|false + */ + private function getRecordForEdit(string $table, int $recordId): array|bool + { + $schema = $this->tcaSchemaFactory->get($table); + $reqRecord = BackendUtility::getRecord($table, $recordId, 'uid,pid' . ($schema->isWorkspaceAware() ? ',t3ver_oid' : '')); + if (is_array($reqRecord)) { + if ($this->getBackendUser()->workspace !== 0) { + if ($schema->isWorkspaceAware()) { + if ($reqRecord['t3ver_oid'] > 0 || VersionState::tryFrom($reqRecord['t3ver_state'] ?? 0) === VersionState::NEW_PLACEHOLDER) { + return $reqRecord; + } + $versionRec = BackendUtility::getWorkspaceVersionOfRecord( + $this->getBackendUser()->workspace, + $table, + $reqRecord['uid'], + 'uid,pid,t3ver_oid' + ); + return is_array($versionRec) ? $versionRec : $reqRecord; + } + return false; + } + return $reqRecord; + } + return false; + } + + private function getInfobox(string $message, ?string $title = null): string + { + return ' +
+
+ + ' . $this->iconFactory->getIcon('actions-close', IconSize::SMALL)->render() . ' + +
+
+ ' . ($title ? '
' . htmlspecialchars($title) . '
' : '') . ' +
+ ' . htmlspecialchars($message) . ' +
+
+
'; + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/DummyController.php b/Classes/Controller/DummyController.php new file mode 100644 index 0000000..4cf3940 --- /dev/null +++ b/Classes/Controller/DummyController.php @@ -0,0 +1,44 @@ +moduleTemplateFactory->create($request); + $view->setTitle('Blank'); + $view->getDocHeaderComponent()->disable(); + return $view->renderResponse('Dummy/Index'); + } +} diff --git a/Classes/Controller/EditDocumentController.php b/Classes/Controller/EditDocumentController.php new file mode 100644 index 0000000..5d89b1b --- /dev/null +++ b/Classes/Controller/EditDocumentController.php @@ -0,0 +1,2110 @@ + + */ + protected array $editconf = []; + + /** + * Array of tables with a lists of field names to edit for those tables. If specified, only those fields + * will be rendered. Otherwise, all (available) fields in the record are shown according to the TCA type. + */ + protected array $columnsOnly = []; + + /** + * Default values for fields + * + * @var array|null [table][field] + */ + protected $defVals; + + /** + * Array of values to force being set as hidden fields in FormEngine + * + * @var array|null [table][field] + */ + protected $overrideVals; + + /** + * If set, this value will be set in $this->retUrl as "returnUrl", if not, + * $this->retUrl will link to dummy action + * + * @var string|null + */ + protected $returnUrl; + + /** + * Prepared return URL. Contains the URL that we should return to from FormEngine if + * close button is clicked. Usually passed along as 'returnUrl', but falls back to + * "dummy" action. + * + * @var string + */ + protected $retUrl; + + /** + * Boolean: If set, then the GET var "&id=" will be added to the + * retUrl string so that the NEW id of something is returned to the script calling the form. + */ + protected bool $returnNewPageId = false; + + /** + * The preview page id. + * ID for displaying the page in the frontend, "save and view" + * Is set to the pid value of the last shown record from "viewId" - thus indicating which page to + * show when clicking the SAVE/VIEW button and transferred via GET/POST parameter "popViewId" + */ + protected int $popViewId = 0; + + /** + * If true, $this->editconf array is added a redirect response, used by Wizard/AddController + */ + protected bool $returnEditConf = false; + + /** + * @var array + */ + protected $pageinfo; + + /** + * Array of the elements to create edit forms for. + * + * @var FormElementData[] + */ + protected array $elementsData = []; + + /** + * Pointer to the first element in $elementsData + */ + protected ?FormElementData $firstEl = null; + + /** + * Counter, used to count the number of errors (when users do not have edit permissions) + */ + protected int $numberOfErrors = 0; + + protected ?ModuleInterface $module = null; + + public function __construct( + private readonly ComponentFactory $componentFactory, + protected readonly EventDispatcherInterface $eventDispatcher, + protected readonly IconFactory $iconFactory, + protected readonly RecordFactory $recordFactory, + protected readonly BreadcrumbFactory $breadcrumbFactory, + protected readonly PageRenderer $pageRenderer, + protected readonly UriBuilder $uriBuilder, + protected readonly ModuleTemplateFactory $moduleTemplateFactory, + protected readonly BackendEntryPointResolver $backendEntryPointResolver, + protected readonly ModuleProvider $moduleProvider, + private readonly FormDataCompiler $formDataCompiler, + private readonly NodeFactory $nodeFactory, + private readonly FormResultFactory $formResultFactory, + private readonly FormResultHandler $formResultHandler, + protected TcaSchemaFactory $tcaSchemaFactory, + protected readonly LocalizationRepository $localizationRepository, + private readonly SchemaLabelResolver $schemaLabelResolver, + protected readonly ResourceFactory $resourceFactory, + protected readonly FlashMessageService $flashMessageService, + ) {} + + /** + * Main dispatcher entry method registered as "record_edit" end point. + */ + public function mainAction(ServerRequestInterface $request): ResponseInterface + { + $view = $this->moduleTemplateFactory->create($request); + $view->setLayout(ModuleLayout::NORMAL); + $view->setUiBlock(true); + $body = ''; + + // Unlock all locked records + BackendUtility::lockRecords(); + $queryParams = $request->getQueryParams(); + $parsedBody = $request->getParsedBody(); + $this->module = $this->moduleProvider->getModule((string)($queryParams['module'] ?? ''), $this->getBackendUser()); + + $this->editconf = $this->sanitizeEditConf($parsedBody['edit'] ?? $queryParams['edit'] ?? []); + $this->defVals = $parsedBody['defVals'] ?? $queryParams['defVals'] ?? null; + $this->overrideVals = $parsedBody['overrideVals'] ?? $queryParams['overrideVals'] ?? null; + $this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '', $request); + $this->returnEditConf = (bool)($parsedBody['returnEditConf'] ?? $queryParams['returnEditConf'] ?? false); + $this->columnsOnly = $this->prepareColumnsOnlyConfigurationFromRequest($request); + $this->popViewId = (int)($parsedBody['popViewId'] ?? $queryParams['popViewId'] ?? 0); + + // Set overrideVals as default values if defVals does not exist. + // @todo: Why? + if (!is_array($this->defVals) && is_array($this->overrideVals)) { + $this->defVals = $this->overrideVals; + } + + // Set final return URL + $this->retUrl = $this->returnUrl ?: $this->resolveDefaultReturnUrl(); + + // Close document if a request for closing the document has been sent + $requestAction = FormAction::createFromRequest($request); + if ($requestAction->shouldHandleDocumentClosing()) { + if ($response = $this->closeAndPossiblyRedirectAction($requestAction)) { + return $response; + } + } + + $event = new BeforeFormEnginePageInitializedEvent($this, $request); + $this->eventDispatcher->dispatch($event); + + // Process incoming data via DataHandler? + if ($requestAction->shouldProcessData()) { + $this->processData($view, $request); + // Redirect if element should be closed after save + if ($requestAction->shouldCloseAfterSave()) { + return $this->closeAndPossiblyRedirectAction($requestAction); + } + } + + // Prepare current request url parameters (which might have been changed already, especially "editconf") + // Contains $request query parameters. This array is the foundation for creating + // the $currentEditingUrl var which becomes the url to which forms are submitted. + $queryParamsForGeneratingCurrentUrl = $queryParams; + $queryParamsForGeneratingCurrentUrl['edit'] = $this->editconf; + $queryParamsForGeneratingCurrentUrl['returnUrl'] = $this->retUrl; + if ($requestAction->shouldProcessData()) { + // Unset default values since we don't need them anymore. But only if all + // records have been persisted. If a DataHandler hook rejected a new record, + // editconf still contains the NEW* key and defVals must be preserved so + // the form reloads with the correct default values, such as the record type. + $hasUnresolvedNewRecords = false; + foreach ($this->editconf as $tableCmds) { + foreach (array_keys($tableCmds) as $uid) { + if (str_contains((string)$uid, 'NEW')) { + $hasUnresolvedNewRecords = true; + break 2; + } + } + } + if (!$hasUnresolvedNewRecords) { + unset($queryParamsForGeneratingCurrentUrl['defVals']); + } + } + + // Preview code is implicit only generated for GET requests, having the query + // parameters "popViewId" (the preview page id) and "showPreview" set. + if ($this->popViewId && ($queryParams['showPreview'] ?? false)) { + // Generate the preview code (markup), which is added to the module body later + $body = $this->getPreviewUriBuilderForRecordPreview($this->popViewId)->buildImmediateActionElement([PreviewUriBuilder::OPTION_SWITCH_FOCUS => null]); + // After generating the preview code, those params should no longer be applied to the form + // action, as this would otherwise always refresh the preview window on saving the record. + unset($queryParamsForGeneratingCurrentUrl['showPreview'], $queryParamsForGeneratingCurrentUrl['popViewId']); + } + + $event = new AfterFormEnginePageInitializedEvent($this, $request); + $this->eventDispatcher->dispatch($event); + + if ($requestAction->isPostRequest) { + // In case save&view is requested, we have to add this information to the redirect + // URL, since the ImmediateAction will be added to the module body afterward. + if ($requestAction->savedokview()) { + $queryParamsForGeneratingCurrentUrl['showPreview'] = true; + $queryParamsForGeneratingCurrentUrl['popViewId'] = $this->popViewId; + } + $url = $this->uriBuilder->buildUriFromRoute('record_edit', $queryParamsForGeneratingCurrentUrl); + return new RedirectResponse($url, 302); + } + + // Begin to show the edit form + $this->setModuleContext($view); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf'); + $this->pageRenderer->addInlineSetting('ShowItem', 'moduleUrl', (string)$this->uriBuilder->buildUriFromRoute('show_item')); + + // Generate the URL to the current request with modified GET parameters + // This is used in various places to "return to" in the form and for buttons etc. + $currentEditingUrl = $this->uriBuilder->buildUriFromRoute('record_edit', $queryParamsForGeneratingCurrentUrl); + + // Creating the editing form, wrap it with buttons, document selector etc. + $formResults = $this->makeEditForm($request, $view, $currentEditingUrl); + if (count($formResults) > 0) { + $this->firstEl = $this->elementsData !== [] ? reset($this->elementsData) : null; + $lastEl = $this->elementsData !== [] ? end($this->elementsData) : null; + // Dispatch event for extensions to track open documents + $this->openCurrentDocuments(); + $this->formResultHandler->addAssets($formResults); + // Put together the various elements (buttons, selectors, form) into a table + $body .= ' +
+ ' . $formResults->getHtml() . ' + + + + '; + $body .= '
'; + } + + if ($this->firstEl === null) { + // In case firstEl is null, no edit form could be created. Therefore, add an + // info box and remove the spinner, since it will never be resolved by FormEngine. + $view->setUiBlock(false); + $body .= $this->getInfobox( + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:noEditForm.message'), + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:noEditForm'), + ); + } + + // Access check... + // The page will show only if there is a valid page and if this page may be viewed by the user + $perms_clause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW); + $this->pageinfo = BackendUtility::readPageAccess($this->firstEl?->viewId, $perms_clause) ?: []; + + $documentTitle = $this->resolveDocumentTitle($this->getLanguageService()); + + // Setting up the buttons, markers for doc header and navigation component state + $this->createBreadcrumb($view); + $this->getButtons($view, $request, $this->firstEl, $currentEditingUrl, $documentTitle); + + // Create language switch options if the record is already persisted, and it is a single record to edit + if ($this->isSingleRecordView() && $this->firstEl?->isSavedRecord()) { + $this->languageSwitch($view, $this->firstEl); + } + + $view->setTitle($documentTitle); + $view->setLayout(ModuleLayout::NORMAL); + $view->assign('bodyHtml', $body); + + return $view->renderResponse('Form/EditDocument'); + } + + protected function sanitizeEditConf(array $editConf): array + { + $newConfiguration = []; + $beUser = $this->getBackendUser(); + // Traverse the GPvar edit array tables + foreach ($editConf as $table => $conf) { + if (!is_array($conf) || !$this->tcaSchemaFactory->has($table)) { + // Skip for invalid config or in case no TCA exists + continue; + } + if (!$beUser->check('tables_modify', $table)) { + // Skip in case the user has insufficient permissions and increment the error counter + $this->numberOfErrors++; + continue; + } + // Traverse the keys/comments of each table (keys can be a comma list of uids) + foreach ($conf as $cKey => $command) { + if ($command !== 'edit' && $command !== 'new') { + // Skip if invalid command + continue; + } + $ids = GeneralUtility::trimExplode(',', (string)$cKey, true); + foreach ($ids as $id) { + $newConfiguration[$table][$id] = $command; + } + } + } + // Change $this->editconf if versioning applies to any of the records + return $this->fixWSversioningInEditConf($newConfiguration); + } + + protected function setModuleContext(ModuleTemplate $view): void + { + $view->assign('moduleContext', ''); + $view->assign('moduleContextId', ''); + if ($this->module === null) { + return; + } + + $view->setModuleName($this->module->getIdentifier()); + + if ($this->module->isStandalone()) { + $parent = $this->module; + } else { + $parent = $this->module->getParentModule(); + while ($parent?->getParentModule() !== null) { + $parent = $parent->getParentModule(); + } + } + if ($parent === null) { + return; + } + $moduleContext = $parent->getIdentifier(); + + if ($moduleContext === 'file') { + // Workaround for filelist using 'media' as ModuleStorage module context… :\ + $moduleContext = 'media'; + } + + $view->assign('moduleContext', $moduleContext); + } + + protected function prepareColumnsOnlyConfigurationFromRequest(ServerRequestInterface $request): array + { + $columnsOnly = $request->getParsedBody()['columnsOnly'] ?? $request->getQueryParams()['columnsOnly'] ?? null; + $usedTables = array_keys($request->getQueryParams()['edit'] ?? []); + $finalColumnsOnly = []; + if (is_array($columnsOnly) && $columnsOnly !== []) { + $finalColumnsOnly = array_map(function ($fields) { + return is_array($fields) ? $fields : GeneralUtility::trimExplode(',', $fields, true); + }, $columnsOnly); + $finalColumnsOnly = $this->addSlugFieldsToColumnsOnly($finalColumnsOnly, $usedTables); + } + return $finalColumnsOnly; + } + + /** + * Always add required fields of slug field + */ + protected function addSlugFieldsToColumnsOnly(array $finalColumnsOnly, array $tables): array + { + foreach ($tables as $table) { + if (!empty($finalColumnsOnly[$table]) && $this->tcaSchemaFactory->has($table)) { + $schema = $this->tcaSchemaFactory->get($table); + foreach ($finalColumnsOnly[$table] as $field) { + if (!$schema->hasField($field)) { + continue; + } + $field = $schema->getField($field); + $postModifiers = $field->getConfiguration()['generatorOptions']['postModifiers'] ?? []; + if ($field->isType(TableColumnType::SLUG) + && (!is_array($postModifiers) || $postModifiers === []) + ) { + $fieldGroups = $field->getConfiguration()['generatorOptions']['fields'] ?? []; + if (is_string($fieldGroups)) { + $fieldGroups = [$fieldGroups]; + } + foreach ($fieldGroups as $fields) { + $finalColumnsOnly['__hiddenGeneratorFields'][$table] = array_merge( + $finalColumnsOnly['__hiddenGeneratorFields'][$table] ?? [], + (is_array($fields) ? $fields : GeneralUtility::trimExplode(',', $fields, true)) + ); + } + } + } + if (!empty($finalColumnsOnly['__hiddenGeneratorFields'][$table])) { + $finalColumnsOnly['__hiddenGeneratorFields'][$table] = array_diff( + array_unique($finalColumnsOnly['__hiddenGeneratorFields'][$table]), + $finalColumnsOnly[$table] + ); + } + } + } + return $finalColumnsOnly; + } + + /** + * Do processing of data, submitting it to DataHandler. + * + * Also handles the "duplication" of a record. + */ + protected function processData(ModuleTemplate $view, ServerRequestInterface $request): void + { + $requestAction = FormAction::createFromRequest($request); + $parsedBody = $request->getParsedBody(); + + $beUser = $this->getBackendUser(); + + $dataMap = $parsedBody['data'] ?? []; + $dataHandlerIncomingCommandMap = $parsedBody['cmd'] ?? []; + $this->returnNewPageId = (bool)($parsedBody['returnNewPageId'] ?? false); + + /** @var DataHandler $dataHandler */ + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + // Only options related to $dataMap submission are included here + $dataHandler->setControl($parsedBody['control'] ?? []); + + // Set default values fetched previously from GET / POST vars + if (is_array($dataMap)) { + foreach ($dataMap as $tableName => $records) { + if (is_array($this->defVals[$tableName] ?? null)) { + foreach ($records as $uid => $_) { + if (str_contains((string)$uid, 'NEW')) { + $dataMap[$tableName][$uid] = array_merge($this->defVals[$tableName], $dataMap[$tableName][$uid]); + } + } + } + } + } + + // Load DataHandler with data + $dataHandler->start($dataMap, $dataHandlerIncomingCommandMap); + if (is_array($parsedBody['mirror'] ?? null)) { + $dataHandler->setMirror($parsedBody['mirror']); + } + + // Perform the saving operation with DataHandler: + if ($requestAction->shouldProcessData()) { + $dataHandler->process_datamap(); + $dataHandler->process_cmdmap(); + + // Update the module menu for the current backend user, as they updated their UI language + $currentUserId = $beUser->getUserId(); + if ($currentUserId + && (string)($dataMap['be_users'][$currentUserId]['lang'] ?? '') !== '' + && $dataMap['be_users'][$currentUserId]['lang'] !== $beUser->user['lang'] + ) { + $newLanguageKey = $dataMap['be_users'][$currentUserId]['lang']; + // Update the current backend user language as well + $beUser->user['lang'] = $newLanguageKey; + // Re-create LANG to have the current request updated the translated page as well + $this->getLanguageService()->init($newLanguageKey); + BackendUtility::setUpdateSignal('updateBackendLanguage', [ 'language' => $newLanguageKey ]); + } + + // If pages are being edited, we set an instruction about updating the page tree after this operation. + if ($dataHandler->pagetreeNeedsRefresh + && (isset($dataMap['pages']) || $beUser->workspace !== 0 && !empty($dataMap)) + ) { + BackendUtility::setUpdateSignal('updatePageTree'); + } + + // If there was saved any new items, load them: + if (!empty($dataHandler->substNEWwithIDs)) { + // Save the expanded/collapsed states for new inline records, if any + $this->updateInlineView($request->getParsedBody()['uc'] ?? $request->getQueryParams()['uc'] ?? null, $dataHandler); + $newEditConf = []; + // Traverse all new records and forge the content of $this->editconf so we can continue to edit these records! + // $this->editconf is now updated to replace NEW-Ids with the actual persisted IDs. + foreach ($this->editconf as $tableName => $tableCmds) { + $keys = array_keys($dataHandler->substNEWwithIDs_table, $tableName); + if ($keys !== []) { + foreach ($keys as $key) { + $editId = $dataHandler->substNEWwithIDs[$key]; + // Check if the $editId isn't a child record of an IRRE action + if (!(is_array($dataHandler->newRelatedIDs[$tableName] ?? null) + && in_array($editId, $dataHandler->newRelatedIDs[$tableName])) + ) { + // Translate new id to the workspace version + if ($versionRec = BackendUtility::getWorkspaceVersionOfRecord( + $beUser->workspace, + $tableName, + $editId, + 'uid' + )) { + $editId = $versionRec['uid']; + } + $newEditConf[$tableName][$editId] = 'edit'; + } + if ($tableName === 'pages' + && !$this->shouldRedirectToEmptyPage() + && $this->retUrl !== $this->getCloseUrl($request) + && $this->returnNewPageId + ) { + $this->retUrl .= '&id=' . $dataHandler->substNEWwithIDs[$key]; + } + } + } else { + $newEditConf[$tableName] = $tableCmds; + } + } + if ($newEditConf !== []) { + $this->editconf = $newEditConf; + } + } + // See if any records was auto-created as new versions? + if (!empty($dataHandler->autoVersionIdMap)) { + $this->editconf = $this->fixWSversioningInEditConf($this->editconf, $dataHandler->autoVersionIdMap); + } + } + + // If a document is saved and a new one is created right after. + if ($requestAction->savedoknew()) { + // Find the current table + reset($this->editconf); + $nTable = (string)key($this->editconf); + // Determine insertion mode: 'top' is self-explaining, + // otherwise new elements are inserted after one using a negative uid + $insertRecordOnTop = ($this->getTsConfigOption($nTable, 'saveDocNew') === 'top'); + $ids = array_keys($this->editconf[$nTable]); + // Depending on $insertRecordOnTop, retrieve either the first or last id to get the records' pid+uid + if ($insertRecordOnTop) { + $nUid = (int)reset($ids); + } else { + $nUid = (int)end($ids); + } + $nRec = BackendUtility::getRecord($nTable, $nUid); + if ($insertRecordOnTop) { + $relatedPageId = $nRec['pid']; + } else { + if ((int)($nRec['t3ver_oid'] ?? 0) === 0) { + $relatedPageId = -$nRec['uid']; + } else { + // Use uid of live version of workspace version + $relatedPageId = -$nRec['t3ver_oid']; + } + } + // Setting a blank editconf array for a new record: + $this->editconf = []; + $this->editconf[$nTable][$relatedPageId] = 'new'; + } + + // Explicitly require a save operation + if ($requestAction->shouldProcessData()) { + $erroneousRecords = $dataHandler->printLogErrorMessages(); + $messages = []; + $table = (string)key($this->editconf); + $uidList = array_keys($this->editconf[$table]); + + foreach ($uidList as $uid) { + $uid = (int)abs($uid); + if (!in_array($table . '.' . $uid, $erroneousRecords, true)) { + $realUidInPayload = ($tceSubstId = array_search($uid, $dataHandler->substNEWwithIDs, true)) !== false ? $tceSubstId : $uid; + $row = $dataMap[$table][$uid] ?? $dataMap[$table][$realUidInPayload] ?? null; + if ($row === null) { + continue; + } + // Ensure, uid is always available to make labels with foreign table lookups possible + $row['uid'] ??= $realUidInPayload; + // If the label column of the record is not available, fetch it from database. + // This is the case when EditDocumentController is booted in single field mode (e.g. + // Template module > 'info/modify' > edit 'setup' field) or in case the field is + // not in "showitem" or is set to readonly (e.g. "file" in sys_file_metadata). + $labelCapability = $this->tcaSchemaFactory->get($table)->getCapability(TcaSchemaCapability::Label); + $labelFields = $labelCapability->getAllLabelFieldNames(); + foreach ($labelFields as $labelField) { + if (!isset($row[$labelField])) { + $tmpRecord = BackendUtility::getRecord($table, $uid, $labelFields); + if ($tmpRecord !== null) { + $row = array_merge($row, $tmpRecord); + } + break; + } + } + $recordTitle = BackendUtility::cropToTitleLength(BackendUtility::getRecordTitle($table, $row)); + $messages[] = sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:notification.record_saved.message'), $recordTitle); + } + } + + // Add messages to the flash message container only if the request is a save action (excludes "duplicate") + if ($messages !== []) { + $label = $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:notification.record_saved.title.plural'); + if (count($messages) === 1) { + $label = $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:notification.record_saved.title.singular'); + } + if (count($messages) > 10) { + $messages = [sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:notification.mass_saving.message'), count($messages))]; + } + $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(FlashMessageQueue::NOTIFICATION_QUEUE); + $flashMessage = new FlashMessage( + implode(LF, $messages), + $label, + ContextualFeedbackSeverity::OK, + true + ); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + } + + // If a document should be duplicated. + if ($requestAction->duplicatedoc()) { + // Find current table + reset($this->editconf); + $nTable = (string)key($this->editconf); + // Find the first id, getting the records pid+uid + $nUid = array_keys($this->editconf[$nTable]); + $nUid = reset($nUid); + if (!MathUtility::canBeInterpretedAsInteger($nUid)) { + $nUid = $dataHandler->substNEWwithIDs[$nUid]; + } + + $nRec = BackendUtility::getRecord($nTable, $nUid); + + // Setting a blank editconf array for a new record + $this->editconf = []; + + if ((int)($nRec['t3ver_oid'] ?? 0) > 0) { + $relatedPageId = -$nRec['t3ver_oid']; + } else { + $relatedPageId = -$nRec['uid']; + } + + /** @var DataHandler $duplicateTce */ + $duplicateTce = GeneralUtility::makeInstance(DataHandler::class); + $duplicateCmd = [ + $nTable => [ + $nUid => [ + 'copy' => $relatedPageId, + ], + ], + ]; + + $duplicateTce->start([], $duplicateCmd); + $duplicateTce->process_cmdmap(); + $duplicateUid = $duplicateTce->copyMappingArray[$nTable][$nUid] ?? null; + if ($duplicateUid !== null) { + if ($nTable === 'pages') { + BackendUtility::setUpdateSignal('updatePageTree'); + } + + $this->editconf[$nTable][$duplicateUid] = 'edit'; + + // Inform the user of the duplication + $view->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.recordDuplicated')); + } else { + $this->numberOfErrors++; + // Inform the user about the failed duplication + $view->addFlashMessage( + $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.recordDuplicationFailed'), + '', + ContextualFeedbackSeverity::ERROR + ); + } + } + } + + protected function getPreviewUriBuilderForRecordPreview($pageId): PreviewUriBuilder + { + $array_keys = array_keys($this->editconf); + $table = reset($array_keys); + $recordId = 0; + if ($table) { + $uids = array_keys($this->editconf[$table]); + $recordId = (int)((reset($uids) ?: null) ?? ''); + } + return PreviewUriBuilder::createForRecordPreview($table, $recordId, $pageId); + } + + protected function createBreadcrumb(ModuleTemplate $view): void + { + // Handle file metadata records + $file = null; + if ($this->firstEl !== null && $this->firstEl->table === 'sys_file_metadata' && $this->firstEl->uid > 0) { + // Happens if it is a select/group + $fileUid = $this->firstEl->record['file'] ?? 0; + if (is_array($fileUid)) { + $fileUid = reset($fileUid); + } + try { + $file = $this->resourceFactory->getFileObject((int)$fileUid); + } catch (FileDoesNotExistException|InsufficientUserPermissionsException $e) { + // do nothing when file is not accessible + } + } + + if ($file instanceof FileInterface) { + $view->assign('moduleContextId', $file->getParentFolder()->getCombinedIdentifier()); + $view->getDocHeaderComponent()->setResourceBreadcrumb($file); + } elseif ($this->pageinfo !== [] && $this->firstEl !== null) { + $l10nParent = (int)($this->pageinfo['l10n_parent'] ?? 0); + $pageUid = $this->pageinfo['uid'] ?? ''; + $view->assign('moduleContextId', $l10nParent !== 0 ? $l10nParent : $pageUid); + + // Determine breadcrumb based on action (edit existing vs. create new) + if ($this->firstEl->isSavedRecord()) { + if ($this->isSingleRecordView()) { + // Edit single existing record + $breadcrumbContext = $this->breadcrumbFactory->forEditAction( + $this->firstEl->table, + (int)$this->firstEl->uid + ); + } else { + // Edit multiple records + $breadcrumbContext = $this->breadcrumbFactory->forEditMultipleAction( + $this->firstEl->table, + (int)($this->pageinfo['uid'] ?? 0) + ); + } + } else { + // Create new record + $breadcrumbContext = $this->breadcrumbFactory->forNewAction( + $this->firstEl->table, + (int)($this->pageinfo['uid'] ?? 0), + $this->defVals[$this->firstEl->table] ?? [] + ); + } + $view->getDocHeaderComponent()->setBreadcrumbContext($breadcrumbContext); + } + } + + /** + * Creates the editing form with FormEngine, based on the input from GPvars. + * + * @return FormResultCollection Form result objects + */ + protected function makeEditForm(ServerRequestInterface $request, ModuleTemplate $view, UriInterface $currentRequestUrl): FormResultCollection + { + // Initialize variables + $formResults = new FormResultCollection(); + // Traverse the GPvar edit array tables + foreach ($this->editconf as $table => $conf) { + // Traverse the keys/comments of each table (keys can be a comma list of uids) + foreach ($conf as $theUid => $command) { + try { + $formDataCompilerInput = [ + 'request' => $request, + 'tableName' => $table, + 'vanillaUid' => (int)$theUid, + 'command' => $command, + 'returnUrl' => (string)$currentRequestUrl, + ]; + if (is_array($this->overrideVals) && is_array($this->overrideVals[$table])) { + $formDataCompilerInput['overrideValues'] = $this->overrideVals[$table]; + } + if (is_array($this->defVals) && $this->defVals !== []) { + $formDataCompilerInput['defaultValues'] = $this->defVals; + } + + $formData = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(TcaDatabaseRecord::class)); + + $viewId = 0; + if ($table === 'pages') { + // Only set viewId in case it's not a new page - as this can not be viewed before being saved + if ($command !== 'new' && MathUtility::canBeInterpretedAsInteger($formData['databaseRow']['uid'])) { + $viewId = (int)$formData['databaseRow']['uid']; + } + } elseif (!empty($formData['parentPageRow']['uid'])) { + $viewId = $formData['parentPageRow']['uid']; + } + + // Display "is-locked" message + if ($command === 'edit') { + $lockInfo = BackendUtility::isRecordLocked($table, $formData['databaseRow']['uid']); + if ($lockInfo) { + $view->addFlashMessage($lockInfo['msg'], '', ContextualFeedbackSeverity::WARNING); + } + } + + $el = new FormElementData( + title: $formData['recordTitle'], + table: $table, + uid: $formData['databaseRow']['uid'], + pid: ($formData['databaseRow']['pid'] ?? $viewId), + record: $formData['databaseRow'], + viewId: (int)$viewId, + command: $command, + userPermissionOnPage: $formData['userPermissionOnPage'], + ); + + $this->elementsData[] = $el; + + if ($command !== 'new') { + BackendUtility::lockRecords($table, $el->uid, $table === 'tt_content' ? $el->pid : 0); + } + + // Set list if only specific fields should be rendered. This will trigger + // ListOfFieldsContainer instead of FullRecordContainer in FormWrapContainer + if (!empty($this->columnsOnly[$table])) { + $formData['fieldListToRender'] = implode(',', $this->columnsOnly[$table]); + if (!empty($this->columnsOnly['__hiddenGeneratorFields'][$table])) { + $formData['hiddenFieldListToRender'] = implode(',', $this->columnsOnly['__hiddenGeneratorFields'][$table]); + } + } + + $formData['renderType'] = 'formWrapContainer'; + $formResult = $this->nodeFactory->create($formData)->render(); + + if ($command === 'new') { + $tableTitle = htmlspecialchars($this->resolveTypeLabel($table, $formData['databaseRow'])); + $formHeading = $this->getLanguageService()->sL('core.core:labels.createNew') . ' ' . $tableTitle; + $formResult['html'] = '

' . $formHeading . '

' . $formResult['html']; + } else { + $recordTitle = trim($formData['recordTitle']) !== '' + ? $formData['recordTitle'] + : '[' . $this->getLanguageService()->sL('core.core:labels.no_title') . ']'; + $recordTitle = BackendUtility::cropToTitleLength($recordTitle); + $recordIdentity = $this->getRecordIdentityHtml($table, $formData['databaseRow']); + $formResult['html'] = '

' . htmlspecialchars($recordTitle) . '

' . $recordIdentity . $formResult['html']; + } + + // Seems the pid is set as hidden field (again) at end?! + if ($command === 'new') { + $formResult['html'] .= ''; + } + + $formResult = $this->formResultFactory->create($formResult); + $formResults->add($formResult); + } catch (NoFieldsToRenderException $e) { + $this->numberOfErrors++; + $formResults->add(new FormResult( + $this->getInfobox( + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:noFieldsEditForm.message'), + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:noFieldsEditForm'), + ) + )); + } catch (AccessDeniedException $e) { + $this->numberOfErrors++; + + $message = $e->getMessage(); + $title = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noEditPermission'); + $formResults->add(new FormResult($this->getInfobox($message, $title))); + } catch (DatabaseRecordException|DatabaseRecordWorkspaceDeletePlaceholderException $e) { + $formResults->add(new FormResult($this->getInfobox($e->getMessage()))); + } + } + } + return $formResults; + } + + /** + * Helper function for rendering an Infobox + */ + protected function getInfobox(string $message, ?string $title = null): string + { + return '
' + . '
' + . '' + . $this->iconFactory->getIcon('actions-close', IconSize::SMALL)->render() + . '' + . '
' + . '
' + . ($title ? '
' . htmlspecialchars($title) . '
' : '') + . '
' . htmlspecialchars($message) . '
' + . '
' + . '
'; + } + + /** + * Create the panel of buttons for submitting the form or otherwise perform operations. + */ + protected function getButtons(ModuleTemplate $view, ServerRequestInterface $request, ?FormElementData $mainFormElement, UriInterface $currentEditingUrl, string $documentTitle): void + { + if ($mainFormElement !== null) { + $record = $mainFormElement->record; + $schema = $this->tcaSchemaFactory->get($mainFormElement->table); + $this->registerCloseButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_LEFT, 1); + + // Show buttons (duplicate, new, view save) when table is not read-only + if (!$this->numberOfErrors && !$schema->hasCapability(TcaSchemaCapability::AccessReadOnly)) { + $view->addButtonToButtonBar($this->componentFactory->createSaveButton('EditDocumentController')->setDisabled(true), ButtonBar::BUTTON_POSITION_LEFT, 2); + $this->registerViewButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_LEFT, 3); + + if ($mainFormElement->command !== 'new') { + $languageCapability = $schema->isLanguageAware() ? $schema->getCapability(TcaSchemaCapability::Language) : null; + $languageId = 0; + if ( + $mainFormElement->isSavedRecord() + && $schema->isLanguageAware() + && isset($record[($languageField = $languageCapability->getLanguageField()->getName())]) + ) { + $languageId = (int)$record[$languageField]; + } elseif (isset($this->defVals[$mainFormElement->table]['language_tag'])) { + $languageId = (int)$this->defVals[$mainFormElement->table]['language_tag']; + } + $l10nParent = 0; + $translationOriginPointerField = $languageCapability ? $languageCapability->getTranslationOriginPointerField()->getName() : null; + if ($translationOriginPointerField && isset($record[$translationOriginPointerField])) { + $value = $record[$translationOriginPointerField]; + if (is_array($value)) { + $value = reset($value); + } + // Happens on group + if (is_array($value) && isset($value['uid'])) { + $value = $value['uid']; + } + $l10nParent = (int)$value; + } + + if ($mainFormElement->table === 'tt_content') { + $canCreateNewOrDuplicate = $this->isInconsistentLanguageHandlingAllowed() || $this->isPageContentFreeTranslationMode($mainFormElement, $languageId); + } else { + $canCreateNewOrDuplicate = $languageId === 0 || $l10nParent === 0; + } + if ($canCreateNewOrDuplicate) { + $this->registerNewButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_LEFT, 4); + $this->registerDuplicationButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_LEFT, 5); + } + } + if ($mainFormElement->isSavedRecord()) { + $this->registerHistoryButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_RIGHT, 1, $currentEditingUrl); + $this->registerDeleteButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_LEFT, 6, $request); + } + $this->registerColumnsOnlyButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_LEFT, 7, $currentEditingUrl); + } + } + + $this->registerInfoButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_RIGHT, 2); + $this->registerOpenInNewWindowButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_RIGHT, 3, $request); + $this->registerShortcutButtonToButtonBar($view, $request, $documentTitle); + } + + /** + * Return true if inconsistent language handling is allowed + */ + protected function isInconsistentLanguageHandlingAllowed(): bool + { + $allowInconsistentLanguageHandling = BackendUtility::getPagesTSconfig( + $this->pageinfo['uid'] ?? 0 + )['mod']['web_layout']['allowInconsistentLanguageHandling'] ?? ['value' => '0']; + + return $allowInconsistentLanguageHandling['value'] === '1'; + } + + /** + * Checks if the page is in free translation mode for tt_content + */ + protected function isPageContentFreeTranslationMode(FormElementData $formElementData, int $languageId): bool + { + if ($formElementData->table !== 'tt_content') { + return false; + } + if (!$formElementData->isSavedRecord()) { + return $this->getFreeTranslationMode( + (int)($this->pageinfo['uid'] ?? 0), + (int)($this->defVals[$formElementData->table]['colPos'] ?? 0), + $languageId + ); + } + return $this->getFreeTranslationMode( + (int)($this->pageinfo['uid'] ?? 0), + (int)($formElementData->record['colPos'] ?? 0), + $languageId + ); + } + + /** + * True if the page is in free translation mode. + */ + protected function getFreeTranslationMode(int $page, int $column, int $language): bool + { + $freeTranslationMode = false; + if ($this->getConnectedContentElementTranslationsCount($page, $column, $language) === 0 + && $this->getStandAloneContentElementTranslationsCount($page, $column, $language) >= 0 + ) { + $freeTranslationMode = true; + } + return $freeTranslationMode; + } + + /** + * Register the close button to the button bar + */ + protected function registerCloseButtonToButtonBar(ModuleTemplate $view, string $position, int $group): void + { + $closeButton = $this->componentFactory->createLinkButton() + ->setHref('#') + ->setClasses('t3js-editform-close') + ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc')) + ->setShowLabelText(true) + ->setIcon($this->iconFactory->getIcon('actions-close', IconSize::SMALL)) + ->setDisabled(true); + $view->addButtonToButtonBar($closeButton, $position, $group); + } + + /** + * Register the view button to the button bar + */ + protected function registerViewButtonToButtonBar(ModuleTemplate $view, string $position, int $group): void + { + if ($this->firstEl === null) { + return; + } + // Pid to show the record + if (!$this->firstEl->viewId) { + return; + } + if ($this->firstEl->table === '') { + return; + } + // @TODO: TsConfig option should change to viewDoc + if (!$this->getTsConfigOption($this->firstEl->table, 'saveDocView')) { + return; + } + + $previewUriBuilderForCurrentPage = PreviewUriBuilder::create($this->pageinfo)->isPreviewable(); + $pageId = $this->popViewId ?: $this->firstEl->viewId; + $previewUriBuilder = PreviewUriBuilder::createForRecordPreview($this->firstEl->table, $this->firstEl->record, $pageId); + if ($previewUriBuilderForCurrentPage || $previewUriBuilder->isPreviewable()) { + $previewUrl = $previewUriBuilder->buildUri(); + if ($previewUrl) { + $viewButton = $this->componentFactory->createLinkButton() + ->setHref((string)$previewUrl) + ->setIcon($this->iconFactory->getIcon('actions-view', IconSize::SMALL)) + ->setShowLabelText(true) + ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.viewDoc')) + ->setClasses('t3js-editform-view') + ->setDisabled(true); + if (!$this->firstEl->isSavedRecord() && $this->firstEl->table === 'pages') { + $viewButton->setDataAttributes(['is-new' => '']); + } + $view->addButtonToButtonBar($viewButton, $position, $group); + } + } + } + + /** + * Register the new button to the button bar + */ + protected function registerNewButtonToButtonBar(ModuleTemplate $view, string $position, int $group): void + { + if ($this->firstEl === null) { + return; + } + if ($this->firstEl->table === '') { + return; + } + if ($this->firstEl->table === 'sys_file_metadata') { + return; + } + if (!$this->getTsConfigOption($this->firstEl->table, 'saveDocNew')) { + return; + } + $newButton = $this->componentFactory->createLinkButton() + ->setHref('#') + ->setIcon($this->iconFactory->getIcon('actions-plus', IconSize::SMALL)) + ->setShowLabelText(true) + ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.newDoc')) + ->setClasses('t3js-editform-new') + ->setDisabled(true); + if (!$this->firstEl->isSavedRecord()) { + $newButton->setDataAttributes(['is-new' => '']); + } + $view->addButtonToButtonBar($newButton, $position, $group); + } + + /** + * Register the duplication button to the button bar + */ + protected function registerDuplicationButtonToButtonBar(ModuleTemplate $view, string $position, int $group): void + { + if (!$this->isSingleRecordView()) { + return; + } + if ($this->firstEl->table === '') { + return; + } + if ($this->firstEl->table === 'sys_file_metadata') { + return; + } + if (!$this->getTsConfigOption($this->firstEl->table, 'showDuplicate')) { + return; + } + $duplicateButton = $this->componentFactory->createLinkButton() + ->setHref('#') + ->setShowLabelText(true) + ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.duplicateDoc')) + ->setIcon($this->iconFactory->getIcon('actions-document-duplicates-select', IconSize::SMALL)) + ->setClasses('t3js-editform-duplicate') + ->setDisabled(true); + if (!$this->firstEl->isSavedRecord()) { + $duplicateButton->setDataAttributes(['is-new' => '']); + } + $view->addButtonToButtonBar($duplicateButton, $position, $group); + } + + /** + * Register the delete button to the button bar + */ + protected function registerDeleteButtonToButtonBar(ModuleTemplate $view, string $position, int $group, ServerRequestInterface $request): void + { + if (!$this->isSingleRecordView()) { + return; + } + if (!$this->firstEl?->isSavedRecord()) { + return; + } + if ($this->getDisableDelete()) { + return; + } + if ($this->isRecordCurrentBackendUser()) { + return; + } + if (!$this->firstEl->hasDeleteAccess()) { + return; + } + $returnUrl = $this->retUrl; + if ($this->firstEl->table === 'pages') { + // The below is a hack to replace the return url with an url to the current module on id=0. Otherwise, + // this might lead to empty views, since the current id is the page, which is about to be deleted. + $parsedUrl = parse_url($returnUrl); + // @todo consider using $this->module here + $routePath = str_replace($this->backendEntryPointResolver->getPathFromRequest($request), '', $parsedUrl['path'] ?? ''); + parse_str($parsedUrl['query'] ?? '', $queryParams); + if ($routePath + && isset($queryParams['id']) + && (string)$this->firstEl->uid === (string)$queryParams['id'] + ) { + try { + // TODO: Use the page's pid instead of 0, this requires a clean API to manipulate the page + // tree from the outside to be able to mark the pid as active + $returnUrl = (string)$this->uriBuilder->buildUriFromRoutePath($routePath, ['id' => 0]); + } catch (ResourceNotFoundException $e) { + // Resolved path can not be matched to a configured route + } + } + } + + $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class); + $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords( + $this->firstEl->table, + (int)$this->firstEl->uid + ); + $referenceCountMessage = BackendUtility::referenceCount( + $this->firstEl->table, + (int)$this->firstEl->uid, + $this->getLanguageService()->sL( + 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord' + ), + (string)$numberOfReferences + ); + + $translations = $this->localizationRepository->getRecordTranslations( + $this->firstEl->table, + (int)$this->firstEl->uid, + ); + $count = count($translations); + $translationCountMessage = ''; + if ($count > 0) { + $translationCountMessage = sprintf($this->getLanguageService()->sL( + 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord' + ), $count); + } + + $deleteUrl = (string)$this->uriBuilder->buildUriFromRoute('tce_db', [ + 'cmd' => [ + $this->firstEl->table => [ + $this->firstEl->uid => [ + 'delete' => '1', + ], + ], + ], + 'redirect' => $returnUrl, + ]); + + $recordInfo = $this->firstEl->title; + if ($this->getBackendUser()->shallDisplayDebugInformation()) { + $recordInfo .= ' [' . $this->firstEl->table . ':' . $this->firstEl->uid . ']'; + } + + $deleteButton = $this->componentFactory->createLinkButton() + ->setClasses('t3js-editform-delete-record') + ->setDataAttributes([ + 'uid' => $this->firstEl->uid, + 'table' => $this->firstEl->table, + 'record-info' => trim($recordInfo), + 'reference-count-message' => $referenceCountMessage, + 'translation-count-message' => $translationCountMessage, + ]) + ->setHref($deleteUrl) + ->setIcon($this->iconFactory->getIcon('actions-edit-delete', IconSize::SMALL)) + ->setShowLabelText(true) + ->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:deleteItem')) + ->setDisabled(true); + $view->addButtonToButtonBar($deleteButton, $position, $group); + } + + /** + * Register the info button to the button bar + */ + protected function registerInfoButtonToButtonBar(ModuleTemplate $view, string $position, int $group): void + { + if (!$this->isSingleRecordView()) { + return; + } + if (!$this->firstEl?->isSavedRecord()) { + return; + } + $button = $this->componentFactory->createGenericButton(); + $button->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:showInfo')); + $button->setAttributes([ + 'type' => 'button', + 'data-dispatch-action' => 'TYPO3.InfoWindow.showItem', + 'data-dispatch-args-list' => $this->firstEl->table . ',' . $this->firstEl->uid, + 'disabled' => 'disabled', + ]); + $button->setIcon($this->iconFactory->getIcon('actions-document-info', IconSize::SMALL)); + $view->addButtonToButtonBar($button, $position, $group); + } + + /** + * Register the history button to the button bar + */ + protected function registerHistoryButtonToButtonBar(ModuleTemplate $view, string $position, int $group, UriInterface $currentEditingUrl): void + { + if (!$this->isSingleRecordView()) { + return; + } + if ($this->firstEl === null) { + return; + } + if ($this->firstEl->table === '') { + return; + } + if (!$this->getTsConfigOption($this->firstEl->table, 'showHistory', '1')) { + return; + } + $historyUrl = (string)$this->uriBuilder->buildUriFromRoute('record_history', [ + 'element' => $this->firstEl->table . ':' . $this->firstEl->uid, + 'returnUrl' => (string)$currentEditingUrl, + ]); + $historyButton = $this->componentFactory->createLinkButton() + ->setHref($historyUrl) + ->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:recordHistory')) + ->setIcon($this->iconFactory->getIcon('actions-document-history-open', IconSize::SMALL)) + ->setDisabled(true); + $view->addButtonToButtonBar($historyButton, $position, $group); + } + + /** + * Register "Edit whole record" button to the button bar + */ + protected function registerColumnsOnlyButtonToButtonBar(ModuleTemplate $view, string $position, int $group, UriInterface $currentEditingUrl): void + { + if (!$this->isSingleRecordView()) { + return; + } + if ($this->columnsOnly === []) { + return; + } + $query = $currentEditingUrl->getQuery(); + $query .= '&columnsOnly='; + $url = $currentEditingUrl->withQuery($query); + $columnsOnlyButton = $this->componentFactory->createLinkButton() + ->setHref((string)$url) + ->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:editWholeRecord')) + ->setShowLabelText(true) + ->setIcon($this->iconFactory->getIcon('actions-open', IconSize::SMALL)) + ->setDisabled(true); + + $view->addButtonToButtonBar($columnsOnlyButton, $position, $group); + } + + /** + * Register the open in new window button to the button bar + */ + protected function registerOpenInNewWindowButtonToButtonBar(ModuleTemplate $view, string $position, int $group, ServerRequestInterface $request): void + { + $closeUrl = $this->getCloseUrl($request); + if ($this->returnUrl === $closeUrl) { + return; + } + // Generate a URL to the current edit form + $arguments = $this->getUrlQueryParamsForCurrentRequest($request); + $arguments['returnUrl'] = $closeUrl; + $requestUri = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $arguments); + $openInNewWindowButton = $this->componentFactory + ->createLinkButton() + ->setHref('#') + ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.openInNewWindow')) + ->setIcon($this->iconFactory->getIcon('actions-window-open', IconSize::SMALL)) + ->setDataAttributes([ + 'dispatch-action' => 'TYPO3.WindowManager.localOpen', + 'dispatch-args' => GeneralUtility::jsonEncodeForHtmlAttribute([ + $requestUri, + true, // switchFocus + md5($requestUri), // windowName, + 'width=670,height=500,status=0,menubar=0,scrollbars=1,resizable=1', // windowFeatures + ]), + ]) + ->setDisabled(true); + $view->addButtonToButtonBar($openInNewWindowButton, $position, $group); + } + + /** + * Register the shortcut button to the button bar + */ + protected function registerShortcutButtonToButtonBar(ModuleTemplate $view, ServerRequestInterface $request, string $documentTitle): void + { + if ($this->returnUrl === $this->getCloseUrl($request)) { + return; + } + $arguments = $this->getUrlQueryParamsForCurrentRequest($request); + $view->getDocHeaderComponent()->setShortcutContext( + 'record_edit', + $documentTitle, + $arguments + ); + } + + protected function getUrlQueryParamsForCurrentRequest(ServerRequestInterface $request): array + { + $queryParams = $request->getQueryParams(); + $potentialArguments = [ + 'edit', + 'defVals', + 'overrideVals', + 'columnsOnly', + 'returnNewPageId', + 'module', + ]; + $arguments = []; + foreach ($potentialArguments as $argument) { + if (!empty($queryParams[$argument])) { + $arguments[$argument] = $queryParams[$argument]; + } + } + return $arguments; + } + + /** + * Get the count of connected translated content elements + */ + protected function getConnectedContentElementTranslationsCount(int $page, int $column, int $language): int + { + $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language); + return (int)$queryBuilder + ->andWhere( + $queryBuilder->expr()->gt( + $this->tcaSchemaFactory->get('tt_content')->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName(), + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ) + ) + ->executeQuery() + ->fetchOne(); + } + + /** + * Get the count of standalone translated content elements + */ + protected function getStandAloneContentElementTranslationsCount(int $page, int $column, int $language): int + { + $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language); + return (int)$queryBuilder + ->andWhere( + $queryBuilder->expr()->eq( + $this->tcaSchemaFactory->get('tt_content')->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName(), + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ) + ) + ->executeQuery() + ->fetchOne(); + } + + /** + * Get the query builder for the translation mode + */ + protected function getQueryBuilderForTranslationMode(int $page, int $column, int $language): QueryBuilder + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)); + return $queryBuilder + ->count('uid') + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq( + 'pid', + $queryBuilder->createNamedParameter($page, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + $this->tcaSchemaFactory->get('tt_content')->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName(), + $queryBuilder->createNamedParameter($language, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'colPos', + $queryBuilder->createNamedParameter($column, Connection::PARAM_INT) + ) + ); + } + + /** + * Update expanded/collapsed states on new inline records if any within backendUser->uc. + * + * @param array|null $uc The uc array to be processed and saved - uc[inlineView][...] + * @param DataHandler $dataHandler Instance of DataHandler that saved data before + */ + protected function updateInlineView(?array $uc, DataHandler $dataHandler): void + { + if (!is_array($uc['inlineView'] ?? null)) { + return; + } + $backendUser = $this->getBackendUser(); + $inlineView = (array)json_decode(is_string($backendUser->uc['inlineView'] ?? false) ? $backendUser->uc['inlineView'] : '', true); + foreach ($uc['inlineView'] as $topTable => $topRecords) { + foreach ($topRecords as $topUid => $childElements) { + foreach ($childElements as $childTable => $childRecords) { + $uids = array_keys($dataHandler->substNEWwithIDs_table, $childTable); + if (!empty($uids)) { + $newExpandedChildren = []; + foreach ($childRecords as $childUid => $state) { + if ($state && in_array($childUid, $uids)) { + $newChildUid = $dataHandler->substNEWwithIDs[$childUid]; + $newExpandedChildren[] = $newChildUid; + } + } + // Add new expanded child records to UC (if any): + if (!empty($newExpandedChildren)) { + $inlineViewCurrent = &$inlineView[$topTable][$topUid][$childTable]; + if (is_array($inlineViewCurrent)) { + $inlineViewCurrent = array_unique(array_merge($inlineViewCurrent, $newExpandedChildren)); + } else { + $inlineViewCurrent = $newExpandedChildren; + } + } + } + } + } + } + $backendUser->uc['inlineView'] = json_encode($inlineView); + $backendUser->writeUC(); + } + + /** + * Returns if delete for the current table is disabled by configuration. + * For sys_file_metadata in default language delete is always disabled. + */ + protected function getDisableDelete(): bool + { + $disableDelete = false; + if ($this->firstEl?->table === 'sys_file_metadata') { + $row = $this->firstEl->record; + if ((int)($row['language_tag'] ?? 0) === 0) { + // Always disable for default language + $disableDelete = true; + } + } else { + $disableDelete = (bool)$this->getTsConfigOption($this->firstEl->table, 'disableDelete'); + } + return $disableDelete; + } + + /** + * Return true in case the current record is the current backend user + */ + protected function isRecordCurrentBackendUser(): bool + { + $backendUser = $this->getBackendUser(); + return $this->firstEl?->table === 'be_users' && (int)($this->firstEl->uid) === $backendUser->getUserId(); + } + + /** + * Returns the URL (usually for the "returnUrl") which closes the current window. + * Used when editing a record in a popup. + */ + protected function getCloseUrl(ServerRequestInterface $request): string + { + return (string)PathUtility::getSystemResourceUri('EXT:backend/Resources/Public/Html/Close.html', $request); + } + + /** + * Make selector box for creating new translation for a record or switching to edit the record + * in an existing language. Displays only languages which are available for the current page. + */ + protected function languageSwitch(ModuleTemplate $view, FormElementData $formElement): void + { + $backendUser = $this->getBackendUser(); + if (!$this->tcaSchemaFactory->has($formElement->table)) { + return; + } + $schema = $this->tcaSchemaFactory->get($formElement->table); + if (!$schema->isLanguageAware()) { + return; + } + $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); + $languageField = $languageCapability->getLanguageField()->getName(); + $transOrigPointerField = $languageCapability->getTranslationOriginPointerField()->getName(); + + $table = $formElement->table; + if (!$backendUser->check('tables_modify', $table)) { + return; + } + $uid = $formElement->uid; + + // Get all available languages for the page + // If editing a page, the translations of the current UID need to be fetched + if ($table === 'pages') { + if (is_array($formElement->record[$transOrigPointerField] ?? null)) { + $l10nParent = $formElement->record[$transOrigPointerField]; + $l10nParent = reset($l10nParent); + } else { + $l10nParent = $formElement->record[$transOrigPointerField] ?? 0; + } + // Ensure the check is always done against the default language page + $availableLanguages = $this->getLanguages( + (int)($l10nParent ?: $uid), + $table + ); + } else { + $availableLanguages = $this->getLanguages($formElement->pid, $table); + } + // Remove default language, if user does not have access. This is necessary, since + // the default language is always added when fetching the system languages (#88504). + if (isset($availableLanguages[0]) && !$this->getBackendUser()->checkLanguageAccess(0)) { + unset($availableLanguages[0]); + } + // Page available in other languages than default language? + if (count($availableLanguages) > 1) { + $rowsByLang = []; + $fetchFields = ['uid', $languageField, $transOrigPointerField]; + // Get record in current language + $rowCurrent = BackendUtility::getLiveVersionOfRecord($table, $uid, $fetchFields); + if (!is_array($rowCurrent)) { + $rowCurrent = BackendUtility::getRecord($table, $uid, $fetchFields); + } + $currentLanguage = (int)$rowCurrent[$languageField]; + // Disabled for records with [all] language! + if ($currentLanguage > -1) { + // Get record in default language if needed + if ($currentLanguage && $rowCurrent[$transOrigPointerField]) { + $rowsByLang[0] = BackendUtility::getLiveVersionOfRecord( + $table, + $rowCurrent[$transOrigPointerField], + $fetchFields + ); + if (!is_array($rowsByLang[0])) { + $rowsByLang[0] = BackendUtility::getRecord( + $table, + $rowCurrent[$transOrigPointerField], + $fetchFields + ); + } + } else { + $rowsByLang[$rowCurrent[$languageField]] = $rowCurrent; + } + // List of language id's that should not be added to the selector + $noAddOption = []; + if ($rowCurrent[$transOrigPointerField] || $currentLanguage === 0) { + // Get record in other languages to see what's already available + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $backendUser->workspace)); + $result = $queryBuilder->select(...$fetchFields) + ->from($table) + ->where( + $queryBuilder->expr()->eq( + 'pid', + $queryBuilder->createNamedParameter($formElement->pid, Connection::PARAM_INT) + ), + $queryBuilder->expr()->gt( + $languageField, + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + $transOrigPointerField, + $queryBuilder->createNamedParameter($rowsByLang[0]['uid'], Connection::PARAM_INT) + ) + ) + ->executeQuery(); + while ($row = $result->fetchAssociative()) { + if ($backendUser->workspace !== 0 && $schema->isWorkspaceAware()) { + $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($backendUser->workspace, $table, $row['uid'], 'uid,t3ver_state'); + if (!empty($workspaceVersion)) { + $versionState = VersionState::tryFrom($workspaceVersion['t3ver_state'] ?? 0); + if ($versionState === VersionState::DELETE_PLACEHOLDER) { + // If a workspace delete placeholder exists for this translation: Mark + // this language as "don't add to selector" and continue with next row, + // otherwise an edit link to a delete placeholder would be created, which + // does not make sense. + $noAddOption[] = (int)$row[$languageField]; + continue; + } + } + } + $rowsByLang[$row[$languageField]] = $row; + } + } + $languageDropDownButton = $this->componentFactory->createDropDownButton() + ->setLabel($this->getLanguageService()->sL('core.core:labels.language')) + ->setShowActiveLabelText(true) + ->setShowLabelText(true); + + $existingLanguageItems = []; + $newLanguageItems = []; + + foreach ($availableLanguages as $languageId => $language) { + $selectorOptionLabel = $language['title']; + // Create url for creating a localized record + $addOption = true; + $createNewLanguageLink = ''; + + if (!isset($rowsByLang[$languageId])) { + // Translation in this language does not exist + if ($this->columnsOnly[$table] ?? false) { + // Don't add option since we are in a view with just a subset of fields, those views + // are specific editing fields only views and are not meant for translation handling. + $addOption = false; + } elseif (!isset($rowsByLang[0]['uid'])) { + // Don't add option since no default row to localize from exists + // TODO: Actually tt_content is able to localize from another l10n_source then L=0. + // This however is currently only possible via the translation wizard. + $addOption = false; + } + } else { + $params = [ + 'edit[' . $table . '][' . $rowsByLang[$languageId]['uid'] . ']' => 'edit', + 'module' => $this->module?->getIdentifier() ?? '', + 'returnUrl' => $this->retUrl, + ]; + if ($this->columnsOnly[$table] ?? false) { + $params['columnsOnly'] = [$table => $this->columnsOnly[$table]]; + } + if ($table === 'pages') { + // Disallow manual adjustment of the language field for pages + $params['overrideVals'] = [ + 'pages' => [ + 'sys_language_uid' => $languageId, + ], + ]; + } + $createNewLanguageLink = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $params); + } + if ($addOption && !in_array($languageId, $noAddOption, true)) { + if (!$createNewLanguageLink) { + $languageItem = $this->componentFactory->createDropDownItem() + ->setTag('typo3-backend-localization-button') + ->setAttribute('record-type', $table) + ->setAttribute('record-uid', (string)$rowsByLang[0]['uid']) + ->setAttribute('target-language', (string)$languageId) + ->setLabel($selectorOptionLabel); + if (!empty($language['flagIcon'])) { + $languageItem->setIcon($this->iconFactory->getIcon($language['flagIcon'])); + } + $newLanguageItems[] = $languageItem; + } else { + $isActive = $languageId === $currentLanguage; + $languageItem = $this->componentFactory->createDropDownRadio() + ->setLabel($selectorOptionLabel) + ->setHref($createNewLanguageLink) + ->setActive($isActive); + if (!empty($language['flagIcon'])) { + $languageItem->setIcon($this->iconFactory->getIcon($language['flagIcon'])); + } + $existingLanguageItems[] = $languageItem; + } + } + } + + // Add existing languages first + foreach ($existingLanguageItems as $item) { + $languageDropDownButton->addItem($item); + } + + // Add separator and new languages if any + if (!empty($newLanguageItems)) { + $languageDropDownButton->addItem($this->componentFactory->createDropDownDivider()); + $languageDropDownButton->addItem( + $this->componentFactory->createDropDownHeader() + ->setLabel($this->getLanguageService()->sL('core.core:labels.new_page_translation')) + ); + foreach ($newLanguageItems as $item) { + $languageDropDownButton->addItem($item); + } + } + + $view->getDocHeaderComponent()->setLanguageSelector($languageDropDownButton); + } + } + } + + /** + * Returns languages available for record translations on given page. + * + * @param int $id Page id: If zero, all available system languages will be returned. If set to + * another value, only languages, a page translation exists for, will be returned. + * @param string $table For pages we want all languages, for other records the languages of the page translations + * @return array Array with languages (uid, title, ISOcode, flagIcon) + */ + protected function getLanguages(int $id, string $table): array + { + // This usually happens when a non-pages record is added after another, so we are fetching the proper page ID + if ($id < 0 && $table !== 'pages') { + $pageId = $this->pageinfo['uid'] ?? null; + if ($pageId !== null) { + $pageId = (int)$pageId; + } else { + $fullRecord = BackendUtility::getRecord($table, abs($id)); + $pageId = (int)$fullRecord['pid']; + } + } else { + if ($table === 'pages' && $id > 0) { + $fullRecord = BackendUtility::getRecordWSOL('pages', $id); + $id = (int)($fullRecord['t3ver_oid'] ?: $fullRecord['uid']); + } + $pageId = $id; + } + // Fetch the current translations of this page, to only show the ones where there is a page translation + $allLanguages = array_filter( + GeneralUtility::makeInstance(TranslationConfigurationProvider::class)->getSystemLanguages($pageId), + static fn(array $language): bool => (int)$language['uid'] !== -1 + ); + if ($table !== 'pages' && $id > 0) { + $translatedPages = $this->localizationRepository->getPageTranslations($pageId, [], $this->getBackendUser()->workspace); + $availableLanguages = []; + if ($allLanguages[0] ?? false) { + $availableLanguages = [ + 0 => $allLanguages[0], + ]; + } + foreach ($translatedPages as $translatedPage) { + $languageId = $translatedPage->get('sys_language_uid'); + if (isset($allLanguages[$languageId])) { + $availableLanguages[$languageId] = $allLanguages[$languageId]; + } + } + return $availableLanguages; + } + return $allLanguages; + } + + /** + * Fix $this->editconf if versioning applies to any of the records + * + * @param array|null $mapArray Mapping between old and new ids if auto-versioning has been performed. + */ + protected function fixWSversioningInEditConf(array $editConf, ?array $mapArray = null): array + { + $finalConfiguration = []; + foreach ($editConf as $table => $conf) { + // Traverse the keys/comments of each table (keys can be a comma list of uids) + $newConf = []; + foreach ($conf as $theUid => $cmd) { + if ($cmd === 'edit') { + if (is_array($mapArray)) { + if ($mapArray[$table][$theUid] ?? false) { + $theUid = $mapArray[$table][$theUid]; + } + } else { + // Default, look for versions in workspace for record: + $calcPRec = $this->getRecordForEdit($table, (int)$theUid); + if (is_array($calcPRec)) { + // Setting UID again if it had changed, due to workspace versioning. + $theUid = (int)$calcPRec['uid']; + } + } + // Add the possibly manipulated IDs to the new-build newConf array: + $newConf[$theUid] = $cmd; + } else { + $newConf[$theUid] = $cmd; + } + } + $finalConfiguration[$table] = $newConf; + } + return $finalConfiguration; + } + + /** + * Get record for editing. + * + * @return array|false Returns record to edit, false if none + */ + protected function getRecordForEdit(string $table, int $recordId): array|bool + { + $schema = $this->tcaSchemaFactory->get($table); + // Fetch requested record: + $reqRecord = BackendUtility::getRecord($table, $recordId, 'uid,pid' . ($schema->isWorkspaceAware() ? ',t3ver_oid' : '')); + if (is_array($reqRecord)) { + // If workspace is OFFLINE: + if ($this->getBackendUser()->workspace !== 0) { + // Check for versioning support of the table: + if ($schema->isWorkspaceAware()) { + // If the record is already a version of "something" pass it by. + if ($reqRecord['t3ver_oid'] > 0 || VersionState::tryFrom($reqRecord['t3ver_state'] ?? 0) === VersionState::NEW_PLACEHOLDER) { + // (If it turns out not to be a version of the current workspace there will be trouble, but + // that is handled inside DataHandler then and in the interface it would clearly be an error of + // links if the user accesses such a scenario) + return $reqRecord; + } + // The input record was online and an offline version must be found or made: + // Look for version of this workspace: + $versionRec = BackendUtility::getWorkspaceVersionOfRecord( + $this->getBackendUser()->workspace, + $table, + $reqRecord['uid'], + 'uid,pid,t3ver_oid' + ); + return is_array($versionRec) ? $versionRec : $reqRecord; + } + // This means that editing cannot occur on this record because it was not supporting versioning + // which is required inside an offline workspace. + return false; + } + // In ONLINE workspace, just return the originally requested record: + return $reqRecord; + } + // Return FALSE because the table/uid was not found anyway. + return false; + } + + /** + * The return value is used for the variable $this->storeArray to prepare 'open documents' urls + */ + protected function compileStoreData(ServerRequestInterface $request, array $overriddenValues): array + { + $queryParams = $request->getQueryParams(); + $parsedBody = $request->getParsedBody(); + + $storeArray = []; + foreach (['edit', 'defVals', 'overrideVals' , 'columnsOnly'] as $key) { + $value = $overriddenValues[$key] ?? $parsedBody[$key] ?? $queryParams[$key] ?? null; + if ($value !== null) { + $storeArray[$key] = $value; + } + } + return $storeArray; + } + + /** + * Get a TSConfig 'option.' array, possibly for a specific table. + */ + protected function getTsConfigOption(string $table, string $key, string $defaultValue = ''): string + { + return trim((string)( + $this->getBackendUser()->getTSConfig()['options.'][$key . '.'][$table] + ?? $this->getBackendUser()->getTSConfig()['options.'][$key] + ?? $defaultValue + )); + } + + /** + * Called when someone is done finishing editing - either by just hitting "close" or "save + close", + * but also for New/AddController when using returnEditConf. + * + * At this time, "closing" open documents in the session and unlocking should be done already. + * + * @return ResponseInterface|null Redirect response if needed + */ + protected function closeAndPossiblyRedirectAction(FormAction $requestAction): ?ResponseInterface + { + if ($requestAction->shouldCloseWithARedirect()) { + // If ->returnEditConf is set, then add the current content of editconf to the ->retUrl variable: used by + // other scripts, like wizard_add, to know which records was created or so... + if ($this->returnEditConf && !$this->shouldRedirectToEmptyPage()) { + $this->retUrl .= '&returnEditConf=' . rawurlencode((string)json_encode($this->editconf)); + } + return new RedirectResponse($this->retUrl, 303); + } + if ($this->retUrl === '') { + return null; + } + return new RedirectResponse((string)$this->returnUrl, 303); + } + + protected function shouldRedirectToEmptyPage(): bool + { + return $this->retUrl === (string)$this->uriBuilder->buildUriFromRoute('dummy'); + } + + /** + * Close the currently open document(s) by dispatching the appropriate event. + * This is called when the user explicitly closes, or when transitioning to a new/duplicated record. + */ + /** + * Notify extensions that document(s) have been opened for editing. + * Dispatches one event per record being opened. + */ + protected function openCurrentDocuments(): void + { + // Dispatch one event per record + foreach ($this->elementsData as $element) { + $this->eventDispatcher->dispatch( + new AfterRecordOpenedEvent( + table: $element->table, + uid: $element->uid, + record: $element->record, + ) + ); + } + } + + /** + * Resolves the document title used for the browser tab and shortcut. + * + * Examples: + * - No form: "Edit form could not be loaded" + * - New record: "Create new {table}" + * - Single record: "{record title} · {table} · #{uid}" + * - Record with subtype: "{record title} · {subtype} · #{uid}" + * - Multiple records: "Edit multiple · {table} · N records" + */ + protected function resolveDocumentTitle(LanguageService $languageService): string + { + $firstEl = $this->elementsData[0] ?? null; + if ($firstEl === null) { + return $languageService->sL('backend.alt_doc:noEditForm'); + } + + $typeLabel = htmlspecialchars($this->resolveTypeLabel($firstEl->table, $firstEl->record)); + + if ($firstEl->command === 'new') { + return $languageService->sL('core.core:labels.createNew') . ' ' . $typeLabel; + } + + $recordCount = count($this->elementsData); + if ($recordCount > 1) { + return implode(' · ', [ + $languageService->sL('core.core:labels.editMultiple'), + $typeLabel, + $languageService->translate('labels.records', 'core.core', ['count' => $recordCount]), + ]); + } + + $recordTitle = trim($firstEl->title) !== '' + ? $firstEl->title + : '[' . $languageService->sL('core.core:labels.no_title') . ']'; + $recordTitle = BackendUtility::cropToTitleLength($recordTitle); + $recordTitle = htmlspecialchars($recordTitle); + + return implode(' · ', array_filter([$recordTitle, $typeLabel, '#' . $firstEl->uid])); + } + + /** + * Returns an HTML snippet showing the record type icon, table title and uid. + */ + protected function getRecordIdentityHtml(string $table, array $row): string + { + $icon = $this->iconFactory->getIconForRecord($table, $row, IconSize::SMALL)->render(); + $tableTitle = $this->resolveTypeLabel($table, $row); + $uid = (string)($row['uid'] ?? ''); + + $recordType = '' . $icon . htmlspecialchars($tableTitle) . ''; + + $debugInfo = $this->getBackendUser()->shallDisplayDebugInformation() ? $table . ':' : ''; + $recordIdentity = sprintf( + '[%s%s]', + htmlspecialchars($this->getLanguageService()->sL('core.core:labels.uid') . ' ' . $uid), + htmlspecialchars($debugInfo), + htmlspecialchars($uid), + ); + + return '
' . $recordType . $recordIdentity . '
'; + } + + /** + * Resolves a human-readable type label for a given table and record. + */ + protected function resolveTypeLabel(string $table, array $record): string + { + $languageService = $this->getLanguageService(); + $schema = $this->tcaSchemaFactory->has($table) + ? $this->tcaSchemaFactory->get($table) + : null; + $typeLabel = $schema !== null + ? $schema->getTitle($languageService->sL(...)) + : $table; + + if ($schema !== null && $schema->supportsSubSchema()) { + $fieldName = $schema->getSubSchemaTypeInformation()->getFieldName(); + $rawTypeValue = $record[$fieldName] ?? ''; + $typeValue = is_array($rawTypeValue) ? (string)($rawTypeValue[0] ?? '') : (string)$rawTypeValue; + if ($typeValue !== '') { + $label = $languageService->sL($this->schemaLabelResolver->getLabelForFieldValue($table, $fieldName, $typeValue, $record)); + if ($label === '' && $schema->hasSubSchema($typeValue)) { + $label = $schema->getSubSchema($typeValue)->getTitle($languageService->sL(...)); + } + if ($label !== '') { + $typeLabel = $label; + } + } + } + + return $typeLabel; + } + + protected function resolveDefaultReturnUrl(): string + { + $module = $this->moduleProvider->getFirstAccessibleModule($this->getBackendUser()); + $routeName = $module ? $module->getIdentifier() : 'dummy'; + return (string)$this->uriBuilder->buildUriFromRoute($routeName); + } + + /** + * Whether a single record view is requested. This + * means, only one element exists in $elementsData. + */ + protected function isSingleRecordView(): bool + { + return count($this->elementsData) === 1; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/ElementBrowserController.php b/Classes/Controller/ElementBrowserController.php new file mode 100644 index 0000000..80c19ec --- /dev/null +++ b/Classes/Controller/ElementBrowserController.php @@ -0,0 +1,84 @@ +mode = $request->getQueryParams()['mode'] ?? $request->getQueryParams()['mode'] ?? ''; + return new HtmlResponse($this->main($request)); + } + + /** + * Main function, detecting the current mode of the element browser and branching out to internal methods. + * + * @return string HTML content + */ + protected function main(ServerRequestInterface $request) + { + $browser = $this->elementBrowserRegistry->getElementBrowser($this->mode); + if (is_callable([$browser, 'setRequest'])) { + $browser->setRequest($request); + } + + $backendUser = $this->getBackendUser(); + $modData = $backendUser->getModuleData('browse_links.php', 'ses'); + [$modData] = $browser->processSessionData($modData); + $backendUser->pushModuleData('browse_links.php', $modData); + + return $browser->render(); + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/Event/AfterBackendPageRenderEvent.php b/Classes/Controller/Event/AfterBackendPageRenderEvent.php new file mode 100644 index 0000000..142f0c9 --- /dev/null +++ b/Classes/Controller/Event/AfterBackendPageRenderEvent.php @@ -0,0 +1,46 @@ +content; + } + + public function setContent(string $content): void + { + $this->content = $content; + } + + public function getView(): ViewInterface + { + return $this->view; + } +} diff --git a/Classes/Controller/Event/AfterFileStorageTreeItemsPreparedEvent.php b/Classes/Controller/Event/AfterFileStorageTreeItemsPreparedEvent.php new file mode 100644 index 0000000..80abc50 --- /dev/null +++ b/Classes/Controller/Event/AfterFileStorageTreeItemsPreparedEvent.php @@ -0,0 +1,49 @@ +> $items + */ + public function __construct( + private readonly ServerRequestInterface $request, + private array $items + ) {} + + public function getRequest(): ServerRequestInterface + { + return $this->request; + } + + public function getItems(): array + { + return $this->items; + } + + public function setItems(array $items): void + { + $this->items = $items; + } +} diff --git a/Classes/Controller/Event/AfterFormEnginePageInitializedEvent.php b/Classes/Controller/Event/AfterFormEnginePageInitializedEvent.php new file mode 100644 index 0000000..61de551 --- /dev/null +++ b/Classes/Controller/Event/AfterFormEnginePageInitializedEvent.php @@ -0,0 +1,42 @@ +controller; + } + + public function getRequest(): ServerRequestInterface + { + return $this->request; + } +} diff --git a/Classes/Controller/Event/AfterPageColumnsSelectedForLocalizationEvent.php b/Classes/Controller/Event/AfterPageColumnsSelectedForLocalizationEvent.php new file mode 100644 index 0000000..12892f3 --- /dev/null +++ b/Classes/Controller/Event/AfterPageColumnsSelectedForLocalizationEvent.php @@ -0,0 +1,86 @@ +columns; + } + + public function setColumns(array $columns): void + { + $this->columns = $columns; + } + + /** + * Returns a list of integer column position numbers used in the BackendLayout. + */ + public function getColumnList(): array + { + return $this->columnList; + } + + public function setColumnList(array $columnList): void + { + $this->columnList = $columnList; + } + + public function getBackendLayout(): BackendLayout + { + return $this->backendLayout; + } + + /** + * Returns an array of records which were used when building the original column + * manifest and column position numbers list. + */ + public function getRecords(): array + { + return $this->records; + } + + /** + * Returns request parameters passed to LocalizationController. + */ + public function getParameters(): array + { + return $this->parameters; + } +} diff --git a/Classes/Controller/Event/AfterPageTreeItemsPreparedEvent.php b/Classes/Controller/Event/AfterPageTreeItemsPreparedEvent.php new file mode 100644 index 0000000..e433b82 --- /dev/null +++ b/Classes/Controller/Event/AfterPageTreeItemsPreparedEvent.php @@ -0,0 +1,49 @@ +> $items + */ + public function __construct( + private readonly ServerRequestInterface $request, + private array $items + ) {} + + public function getRequest(): ServerRequestInterface + { + return $this->request; + } + + public function getItems(): array + { + return $this->items; + } + + public function setItems(array $items): void + { + $this->items = $items; + } +} diff --git a/Classes/Controller/Event/AfterRecordOpenedEvent.php b/Classes/Controller/Event/AfterRecordOpenedEvent.php new file mode 100644 index 0000000..74a96fb --- /dev/null +++ b/Classes/Controller/Event/AfterRecordOpenedEvent.php @@ -0,0 +1,42 @@ + $record The full database record + */ + public function __construct( + public string $table, + public int|string $uid, + public array $record, + ) {} +} diff --git a/Classes/Controller/Event/AfterRecordSummaryForLocalizationEvent.php b/Classes/Controller/Event/AfterRecordSummaryForLocalizationEvent.php new file mode 100644 index 0000000..4d35ac9 --- /dev/null +++ b/Classes/Controller/Event/AfterRecordSummaryForLocalizationEvent.php @@ -0,0 +1,46 @@ +columns; + } + + public function setColumns(array $columns): void + { + $this->columns = $columns; + } + + public function getRecords(): array + { + return $this->records; + } + + public function setRecords(array $records): void + { + $this->records = $records; + } +} diff --git a/Classes/Controller/Event/BeforeBackendPageRenderEvent.php b/Classes/Controller/Event/BeforeBackendPageRenderEvent.php new file mode 100644 index 0000000..97f7adb --- /dev/null +++ b/Classes/Controller/Event/BeforeBackendPageRenderEvent.php @@ -0,0 +1,35 @@ +controller; + } + + public function getRequest(): ServerRequestInterface + { + return $this->request; + } +} diff --git a/Classes/Controller/Event/ModifyAllowedItemsEvent.php b/Classes/Controller/Event/ModifyAllowedItemsEvent.php new file mode 100644 index 0000000..2b87682 --- /dev/null +++ b/Classes/Controller/Event/ModifyAllowedItemsEvent.php @@ -0,0 +1,61 @@ + $currentLinkParts + */ + public function __construct( + private array $allowedItems, + private array $currentLinkParts, + ) {} + + /** + * @return string[] + */ + public function getAllowedItems(): array + { + return $this->allowedItems; + } + + public function addAllowedItem(string $item): self + { + $this->allowedItems[] = $item; + return $this; + } + + public function removeAllowedItem(string $new): self + { + $this->allowedItems = array_filter($this->allowedItems, static fn(string $item): bool => $item !== $new); + return $this; + } + + /** + * @return array + */ + public function getCurrentLinkParts(): array + { + return $this->currentLinkParts; + } +} diff --git a/Classes/Controller/Event/ModifyGenericBackendMessagesEvent.php b/Classes/Controller/Event/ModifyGenericBackendMessagesEvent.php new file mode 100644 index 0000000..d5af338 --- /dev/null +++ b/Classes/Controller/Event/ModifyGenericBackendMessagesEvent.php @@ -0,0 +1,43 @@ + About" module. + */ +final class ModifyGenericBackendMessagesEvent +{ + private array $messages = []; + + public function getMessages(): array + { + return $this->messages; + } + + public function addMessage(AbstractMessage $message): void + { + $this->messages[] = $message; + } + + public function setMessages(array $messages): void + { + $this->messages = $messages; + } +} diff --git a/Classes/Controller/Event/ModifyLinkHandlersEvent.php b/Classes/Controller/Event/ModifyLinkHandlersEvent.php new file mode 100644 index 0000000..2a8f2b2 --- /dev/null +++ b/Classes/Controller/Event/ModifyLinkHandlersEvent.php @@ -0,0 +1,73 @@ + $linkHandlers + * @param array $currentLinkParts + */ + public function __construct( + private array $linkHandlers, + private array $currentLinkParts, + ) {} + + /** + * @return array + */ + public function getLinkHandlers(): array + { + return $this->linkHandlers; + } + + /** + * Gets an individual handler by name. + * + * @param string $name The handler name, including trailing period. + * @return array|null The handler definition, or null if not defined. + */ + public function getLinkHandler(string $name): ?array + { + return $this->linkHandlers[$name] ?? null; + } + + /** + * Sets a handler by name, overwriting it if it already exists. + * + * @param string $name The handler name, including trailing period. + * @param array $handler + * @return $this + */ + public function setLinkHandler(string $name, array $handler): self + { + $this->linkHandlers[$name] = $handler; + return $this; + } + + /** + * @return array + */ + public function getCurrentLinkParts(): array + { + return $this->currentLinkParts; + } +} diff --git a/Classes/Controller/Event/ModifyNewContentElementWizardItemsEvent.php b/Classes/Controller/Event/ModifyNewContentElementWizardItemsEvent.php new file mode 100644 index 0000000..ff5987a --- /dev/null +++ b/Classes/Controller/Event/ModifyNewContentElementWizardItemsEvent.php @@ -0,0 +1,142 @@ +wizardItems; + } + + public function setWizardItems(array $wizardItems): void + { + $this->wizardItems = $wizardItems; + } + + public function hasWizardItem(string $identifier): bool + { + return isset($this->wizardItems[$identifier]); + } + + public function getWizardItem(string $identifier): ?array + { + return $this->wizardItems[$identifier] ?? null; + } + + /** + * Add a new wizard item with configuration at a defined position. + * Can also be used to relocate existing items and to modify their configuration. + */ + public function setWizardItem(string $identifier, array $configuration, array $position = []): void + { + if (isset($this->wizardItems[$position['before'] ?? '']) + || isset($this->wizardItems[$position['after'] ?? '']) + ) { + // Always unset an existing item if valid positioning is requested + unset($this->wizardItems[$identifier]); + } + + // Add item before another item + if (($position['before'] ?? false) + && ($insertPosition = array_search((string)$position['before'], array_keys($this->wizardItems), true)) !== false + ) { + $this->wizardItems = array_slice($this->wizardItems, 0, $insertPosition) + + [$identifier => $configuration] + + array_slice($this->wizardItems, $insertPosition); + return; + } + + // Add item after another item + if (($position['after'] ?? false) + && ($insertPosition = array_search((string)$position['after'], array_keys($this->wizardItems), true)) !== false + ) { + $this->wizardItems = array_slice($this->wizardItems, 0, $insertPosition + 1) + + [$identifier => $configuration] + + array_slice($this->wizardItems, $insertPosition + 1); + return; + } + + // By default, add the item at the bottom or might just overwrite configuration of an existing item + $this->wizardItems[$identifier] = $configuration; + } + + public function removeWizardItem(string $identifier): bool + { + if (!$this->hasWizardItem($identifier)) { + return false; + } + + unset($this->wizardItems[$identifier]); + return true; + } + + /** + * Provides information about the current page making use of the wizard. + */ + public function getPageInfo(): array + { + return $this->pageInfo; + } + + /** + * Provides information about the column position of the button that triggered the wizard. + */ + public function getColPos(): ?int + { + return $this->colPos; + } + + /** + * Provides information about the language used while triggering the wizard. + */ + public function getSysLanguage(): int + { + return $this->sys_language; + } + + /** + * Provides information about the element to position the new element after (uid) or into (pid). + */ + public function getUidPid(): int + { + return $this->uid_pid; + } + + /** + * Provides the request in the state it was provided to the NewContentElementController::wizardAction() method. + */ + public function getRequest(): ServerRequestInterface + { + return $this->request; + } +} diff --git a/Classes/Controller/Event/ModifyNewRecordCreationLinksEvent.php b/Classes/Controller/Event/ModifyNewRecordCreationLinksEvent.php new file mode 100644 index 0000000..5e51df6 --- /dev/null +++ b/Classes/Controller/Event/ModifyNewRecordCreationLinksEvent.php @@ -0,0 +1,88 @@ + [ + * "title" => "Content", + * "icon" => "" + * "items" => [ + * "sys_note" => [ + * [ + * "url" => "...", + * "icon" => "...", + * "label" => "...", + * ], + * ], + * "sys_file_collection" => [ + * [ + * "icon" => "...", + * "label" => "...", + * "types" => [ + * "static" => [ + * 'url' => "...", + * 'icon' => "...", + * 'label' => "...", + * ], + * "folder" => [ + * 'url' => "...", + * 'icon' => "...", + * 'label' => "...", + * ], + * ], + * ], + * ], + * ], + * ], + * "system" => [ + * "title" => "System Records", + * "icon" => "" + * "items" => [ + * "sys_template" => [ + * [ + * "url" => "...", + * "icon" => "...", + * "label" => "...", + * ], + * ], + * "backend_layout" => [ + * [ + * "url" => "...", + * "icon" => "...", + * "label" => "...", + * ], + * ], + * ], + * ], + */ +final class ModifyNewRecordCreationLinksEvent +{ + public function __construct( + public array $groupedCreationLinks, + public readonly array $pageTS, + public readonly int $pageId, + public readonly ServerRequestInterface $request + ) {} +} diff --git a/Classes/Controller/Event/ModifyPageLayoutContentEvent.php b/Classes/Controller/Event/ModifyPageLayoutContentEvent.php new file mode 100644 index 0000000..9a5e6c0 --- /dev/null +++ b/Classes/Controller/Event/ModifyPageLayoutContentEvent.php @@ -0,0 +1,89 @@ +request; + } + + public function getModuleTemplate(): ModuleTemplate + { + return $this->moduleTemplate; + } + + /** + * Set content for the header. Can also be used to e.g. reorder existing content. + * IMPORTANT: This overwrites existing content from previous listeners! + */ + public function setHeaderContent(string $content): void + { + $this->headerContent = $content; + } + + /** + * Add additional content to the header + */ + public function addHeaderContent(string $content): void + { + $this->headerContent .= $content; + } + + public function getHeaderContent(): string + { + return $this->headerContent; + } + + /** + * Set content for the footer. Can also be used to e.g. reorder existing content. + * IMPORTANT: This overwrites existing content from previous listeners! + */ + public function setFooterContent(string $content): void + { + $this->footerContent = $content; + } + + /** + * Add additional content to the footer + */ + public function addFooterContent(string $content): void + { + $this->footerContent .= $content; + } + + public function getFooterContent(): string + { + return $this->footerContent; + } +} diff --git a/Classes/Controller/Event/RenderAdditionalContentToRecordListEvent.php b/Classes/Controller/Event/RenderAdditionalContentToRecordListEvent.php new file mode 100644 index 0000000..2ed1e55 --- /dev/null +++ b/Classes/Controller/Event/RenderAdditionalContentToRecordListEvent.php @@ -0,0 +1,56 @@ +request; + } + + public function addContentAbove(string $contentAbove): void + { + $this->contentAbove .= $contentAbove; + } + + public function addContentBelow(string $contentBelow): void + { + $this->contentBelow .= $contentBelow; + } + + public function getAdditionalContentAbove(): string + { + return $this->contentAbove; + } + + public function getAdditionalContentBelow(): string + { + return $this->contentBelow; + } +} diff --git a/Classes/Controller/File/FileController.php b/Classes/Controller/File/FileController.php new file mode 100644 index 0000000..34a8135 --- /dev/null +++ b/Classes/Controller/File/FileController.php @@ -0,0 +1,340 @@ +init($request); + $this->main($request); + + BackendUtility::setUpdateSignal('updateFolderTree'); + + // go and edit the new created file + if ($request->getParsedBody()['edit'] ?? '') { + $file = $this->fileData['newfile'][0]; + if ($file !== null) { + $this->redirect = $this->getFileEditRedirect($file) ?? $this->redirect; + } + } + if ($this->redirect) { + return new RedirectResponse( + GeneralUtility::locationHeaderUrl($this->redirect, $request), + 303 + ); + } + // empty response + return new HtmlResponse(''); + } + + /** + * Handles the actual process from within the ajaxExec function + * therefore, it does exactly the same as the real typo3/tce_file.php. + */ + public function processAjaxRequest(ServerRequestInterface $request): ResponseInterface + { + $this->init($request); + $this->main($request); + $flatResult = [ + 'hasErrors' => false, + ]; + foreach ($this->fileData as $action => $results) { + foreach ($results as $result) { + if (is_array($result)) { + foreach ($result as $subResult) { + $flatResult[$action][] = $this->flattenResultDataValue($subResult); + } + } else { + $flatResult[$action][] = $this->flattenResultDataValue($result); + } + } + } + + // Used in the FileStorageTree when moving / copying folders, or in the DragUploader + $messages = $this->flashMessageService->getMessageQueueByIdentifier()->getAllMessagesAndFlush(); + if (!empty($messages)) { + foreach ($messages as $message) { + $flatResult['messages'][] = [ + 'title' => $message->getTitle(), + 'message' => $message->getMessage(), + 'severity' => $message->getSeverity(), + ]; + if ($message->getSeverity() === ContextualFeedbackSeverity::ERROR) { + $flatResult['hasErrors'] = true; + } + } + } + return new JsonResponse($flatResult, $flatResult['hasErrors'] ? 500 : 200); + } + + /** + * Ajax entry point to check if a file exists in a folder + */ + public function fileExistsInFolderAction(ServerRequestInterface $request): ResponseInterface + { + $this->init($request); + $fileName = $request->getParsedBody()['fileName'] ?? $request->getQueryParams()['fileName'] ?? null; + $fileTarget = $request->getParsedBody()['fileTarget'] ?? $request->getQueryParams()['fileTarget'] ?? null; + + $fileTargetObject = $this->fileFactory->retrieveFileOrFolderObject($fileTarget); + $processedFileName = $fileTargetObject->getStorage()->sanitizeFileName($fileName, $fileTargetObject); + + $result = []; + if ($fileTargetObject->hasFile($processedFileName)) { + $fileInFolder = $fileTargetObject->getStorage()->getFileInFolder($processedFileName, $fileTargetObject); + if ($fileInFolder instanceof File) { + $result = $this->flattenFileResultDataValue($fileInFolder); + } + } + return new JsonResponse($result); + } + + /** + * Registering incoming data + */ + protected function init(ServerRequestInterface $request): void + { + // Set the GPvars from outside + $parsedBody = $request->getParsedBody(); + $queryParams = $request->getQueryParams(); + $this->file = (array)($parsedBody['data'] ?? $queryParams['data'] ?? []); + $redirectUrl = (string)($parsedBody['redirect'] ?? $queryParams['redirect'] ?? ''); + if ($this->file === [] || $redirectUrl !== '') { + // This in clipboard mode or when a new folder is created + $this->redirect = GeneralUtility::sanitizeLocalUrl($redirectUrl, $request); + } else { + $mode = key($this->file); + $elementKey = key($this->file[$mode]); + $this->redirect = GeneralUtility::sanitizeLocalUrl($this->file[$mode][$elementKey]['redirect'] ?? '', $request); + } + $this->CB = (array)($parsedBody['CB'] ?? $queryParams['CB'] ?? []); + + if (isset($this->file['rename'][0]['conflictMode'])) { + $conflictMode = $this->file['rename'][0]['conflictMode']; + unset($this->file['rename'][0]['conflictMode']); + $this->overwriteExistingFiles = DuplicationBehavior::tryFrom($conflictMode) ?? DuplicationBehavior::getDefaultDuplicationBehaviour(); + } else { + $duplicationBehaviorFromRequest = $parsedBody['overwriteExistingFiles'] ?? $queryParams['overwriteExistingFiles'] ?? ''; + $this->overwriteExistingFiles = DuplicationBehavior::tryFrom($duplicationBehaviorFromRequest) ?? DuplicationBehavior::getDefaultDuplicationBehaviour(); + } + $this->initClipboard($request); + } + + /** + * Initialize the Clipboard. This will fetch the data about files to paste/delete if such an action has been sent. + */ + protected function initClipboard(ServerRequestInterface $request): void + { + if ($this->CB !== []) { + $clipObj = GeneralUtility::makeInstance(Clipboard::class); + $clipObj->initializeClipboard($request); + if ($this->CB['paste'] ?? false) { + $clipObj->setCurrentPad((string)($this->CB['pad'] ?? '')); + $this->setPasteCmd($clipObj); + } + if ($this->CB['delete'] ?? false) { + $clipObj->setCurrentPad((string)($this->CB['pad'] ?? '')); + $this->setDeleteCmd($clipObj); + } + } + } + + /** + * Performing the file admin action: + * Initializes the objects, setting permissions, sending data to object. + */ + protected function main(ServerRequestInterface $request): void + { + $this->fileProcessor->setActionPermissions(); + $this->fileProcessor->setExistingFilesConflictMode($this->overwriteExistingFiles); + $this->fileProcessor->start($this->file, $request->getUploadedFiles()); + $this->fileData = $this->fileProcessor->processData(); + } + + /** + * Gets URI to be used for editing given file (if file extension is defined in textfile_ext) + * + * @param File $file to be edited + * @return string|null URI to be redirected to + * @throws RouteNotFoundException + */ + protected function getFileEditRedirect(File $file): ?string + { + if (!$file->isTextFile()) { + return null; + } + $properties = $file->getProperties(); + $urlParameters = [ + 'target' => $properties['storage'] . ':' . $properties['identifier'], + ]; + if ($this->redirect) { + $urlParameters['returnUrl'] = $this->redirect; + } + try { + return (string)$this->uriBuilder->buildUriFromRoute('file_edit', $urlParameters); + } catch (RouteNotFoundException $exception) { + // no route for editing files available + return ''; + } + } + + protected function flattenFileResultDataValue(File $result): array + { + $thumbUrl = $result->isImage() + ? ($result->process(ProcessedFile::CONTEXT_IMAGEPREVIEW, [])->getPublicUrl() ?? '') + : ''; + return array_merge( + $result->toArray(), + [ + 'date' => BackendUtility::date($result->getModificationTime()), + 'icon' => $this->iconFactory->getIconForFileExtension($result->getExtension(), IconSize::SMALL)->render(), + 'thumbUrl' => $thumbUrl, + 'path' => $result->getParentFolder()->getReadablePath(), + ] + ); + } + + /** + * Flatten result value from FileProcessor + * + * The value can be a File, Folder or boolean + * + * @param bool|File|Folder|ProcessedFile $result + * + * @return bool|string|array + */ + protected function flattenResultDataValue($result) + { + if ($result instanceof File) { + $result = $this->flattenFileResultDataValue($result); + } elseif ($result instanceof Folder) { + $result = $result->getIdentifier(); + } + + return $result; + } + + /** + * Applies the proper paste configuration to $this->file + */ + protected function setPasteCmd(Clipboard $clipboard): void + { + $target = explode('|', (string)$this->CB['paste'])[1] ?? ''; + $mode = $clipboard->currentMode() === 'copy' ? 'copy' : 'move'; + // Traverse elements and make CMD array + foreach ($clipboard->elFromTable('_FILE') as $key => $path) { + $this->file[$mode][] = ['data' => $path, 'target' => $target]; + if ($mode === 'move') { + $clipboard->removeElement($key); + } + } + $clipboard->endClipboard(); + } + + /** + * Applies the proper delete configuration to $this->file + */ + protected function setDeleteCmd(Clipboard $clipObj): void + { + // Traverse elements and make CMD array + foreach ($clipObj->elFromTable('_FILE') as $key => $path) { + $this->file['delete'][] = ['data' => $path]; + $clipObj->removeElement($key); + } + $clipObj->endClipboard(); + } +} diff --git a/Classes/Controller/File/ImageProcessController.php b/Classes/Controller/File/ImageProcessController.php new file mode 100644 index 0000000..8d9ba86 --- /dev/null +++ b/Classes/Controller/File/ImageProcessController.php @@ -0,0 +1,59 @@ +getQueryParams()['id'] ?? 0); + try { + $processedFile = $this->imageProcessingService->process($processedFileId); + if (!$processedFile->getOriginalFile()->checkActionPermission('read')) { + return new HtmlResponse('', 403); + } + + return new RedirectResponse( + GeneralUtility::locationHeaderUrl($processedFile->getPublicUrl() ?? '', $request) + ); + } catch (\Throwable $e) { + // Fatal error occurred, which will be responded as 404 + $this->logger->error('Processing of file with id {processed_file} failed', ['processed_file' => $processedFileId, 'exception' => $e]); + } + + return new HtmlResponse('', 404); + } +} diff --git a/Classes/Controller/FileStorage/TreeController.php b/Classes/Controller/FileStorage/TreeController.php new file mode 100644 index 0000000..1061506 --- /dev/null +++ b/Classes/Controller/FileStorage/TreeController.php @@ -0,0 +1,235 @@ +getQueryParams()['parent'] ?? null; + if ($parentIdentifier) { + $currentDepth = (int)($request->getQueryParams()['depth'] ?? 1); + $parentIdentifier = rawurldecode($parentIdentifier); + $folder = $this->resourceFactory->getFolderObjectFromCombinedIdentifier($parentIdentifier); + $items = $this->treeProvider->getSubfolders($folder, $currentDepth + 1); + } else { + $items = $this->treeProvider->getRootNodes($this->getBackendUser()); + } + return new JsonResponse($this->getPreparedItemsForOutput($request, $items)); + } + + /** + * Returns JSON representing page rootline + */ + public function fetchRootlineAction(ServerRequestInterface $request): ResponseInterface + { + $identifier = (string)($request->getQueryParams()['identifier'] ?? ''); + if ($identifier === '') { + return new JsonResponse(null, 400); + } + + try { + $folder = $this->resourceFactory->getFolderObjectFromCombinedIdentifier($identifier); + } catch (InsufficientFolderAccessPermissionsException) { + return new JsonResponse(null, 403); + } catch (FolderDoesNotExistException) { + return new JsonResponse(null, 404); + } + + $rootline = []; + while (true) { + $identifier = $folder->getCombinedIdentifier(); + $rootline[] = $identifier; + try { + $parent = $folder->getParentFolder(); + } catch (InsufficientFolderAccessPermissionsException) { + break; + } + if ($parent->getCombinedIdentifier() === $identifier) { + // parent folder of root folder is the root folder => break + break; + } + $folder = $parent; + } + + return new JsonResponse([ + 'rootline' => array_reverse($rootline), + ]); + } + + /** + * Used when the search / filter is used. + * + * @throws \Exception + */ + public function filterDataAction(ServerRequestInterface $request): ResponseInterface + { + $search = $request->getQueryParams()['q'] ?? ''; + $foundFolders = $this->treeProvider->getFilteredTree($this->getBackendUser(), $search); + + $items = []; + foreach ($foundFolders as $folder) { + if (!$folder instanceof Folder) { + continue; + } + $storage = $folder->getStorage(); + $itemsInRootLine = []; + + // Go back the root folder structure until the root folder + $nextFolder = $folder; + $isParent = false; + do { + $itemsInRootLine[$nextFolder->getCombinedIdentifier()] = array_merge( + $this->treeProvider->prepareFolderInformation($nextFolder), + [ + 'expanded' => $isParent, + 'loaded' => true, + ] + ); + $isParent = true; + try { + $nextFolder = $nextFolder->getParentFolder(); + } catch (InsufficientFolderAccessPermissionsException) { + $nextFolder = null; + } + } while ($nextFolder instanceof FolderInterface && $nextFolder->getIdentifier() !== '/'); + // Add the storage / sys_filemount itself + $storageData = $this->treeProvider->prepareFolderInformation( + $storage->getRootLevelFolder(true), + $storage->getName() + ); + $storageData = array_merge($storageData, [ + 'depth' => 0, + 'expanded' => true, + ]); + $itemsInRootLine[$storage->getUid() . ':/'] = $storageData; + + $itemsInRootLine = array_reverse($itemsInRootLine); + $depth = 0; + foreach ($itemsInRootLine as $k => $itm) { + $itm['depth'] = $depth++; + $items[$k] = $itm; + } + } + + ksort($items); + $items = array_values($items); + return new JsonResponse($this->getPreparedItemsForOutput($request, $items)); + } + + /** + * Adds information for the JSON result to be rendered. Additionally, dispatches event for modification. + */ + protected function getPreparedItemsForOutput(ServerRequestInterface $request, array $items): array + { + foreach ($items as &$item) { + $folder = $item['resource']; + $isStorage = $item['recordType'] !== 'sys_file'; + $item['resourceType'] = $isStorage ? 'storage' : 'folder'; + if ($isStorage && !$folder->getStorage()->isOnline()) { + $item['name'] .= ' (' . $this->getLanguageService()->translate('is_offline', 'core.db.sys_file_storage') . ')'; + } + $icon = $this->iconFactory->getIconForResource($folder, IconSize::SMALL, null, $isStorage ? ['mount-root' => true] : []); + $item['icon'] = $icon->getIdentifier(); + $item['overlayIcon'] = $icon->getOverlayIcon() ? $icon->getOverlayIcon()->getIdentifier() : ''; + + $tsConfigLabels = $this->getBackendUser()->getTSConfig()['options.']['folderTree.']['label.'] ?? []; + if (trim($tsConfigLabels[$folder->getCombinedIdentifier() . '.']['label'] ?? '') !== '') { + $item['labels'][] = new Label( + label: $this->getLanguageService()->sL($tsConfigLabels[$folder->getCombinedIdentifier() . '.']['label']), + color: (string)($tsConfigLabels[$folder->getCombinedIdentifier() . '.']['color'] ?? '#ff8722'), + ); + } + } + + return array_map( + static function (array $item): FileTreeItem { + return new FileTreeItem( + item: new TreeItem( + identifier: $item['identifier'], + parentIdentifier: (string)($item['parentIdentifier'] ?? ''), + recordType: (string)($item['recordType'] ?? ''), + name: (string)($item['name'] ?? ''), + prefix: (string)($item['prefix'] ?? ''), + suffix: (string)($item['suffix'] ?? ''), + tooltip: (string)($item['tooltip'] ?? ''), + depth: (int)($item['depth'] ?? 0), + hasChildren: (bool)($item['hasChildren'] ?? false), + loaded: (bool)($item['loaded'] ?? false), + icon: $item['icon'], + overlayIcon: $item['overlayIcon'], + statusInformation: (array)($item['statusInformation'] ?? []), + labels: (array)($item['labels'] ?? []), + ), + pathIdentifier: (string)($item['pathIdentifier'] ?? ''), + storage: (int)($item['storage'] ?? 0), + resourceType: $item['resourceType'], + ); + }, + $this->eventDispatcher->dispatch( + new AfterFileStorageTreeItemsPreparedEvent($request, $items) + )->getItems() + ); + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/FormFilesAjaxController.php b/Classes/Controller/FormFilesAjaxController.php new file mode 100644 index 0000000..cc31c46 --- /dev/null +++ b/Classes/Controller/FormFilesAjaxController.php @@ -0,0 +1,513 @@ +getParsedBody()['ajax']; + $parentConfig = $this->extractSignedParentConfigFromRequest((string)($arguments['context'] ?? '')); + + $domObjectId = (string)($arguments[0] ?? ''); + $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); + if (!MathUtility::canBeInterpretedAsInteger($inlineFirstPid) && !str_starts_with((string)$inlineFirstPid, 'NEW')) { + throw new \RuntimeException( + 'inlineFirstPid should either be an integer or a "NEW..." string', + 1664440476 + ); + } + $fileId = null; + if (isset($arguments[1]) && MathUtility::canBeInterpretedAsInteger($arguments[1])) { + $fileId = (int)$arguments[1]; + } + + $inlineStructure = $this->inlineStackProcessor->getStructureFromString($domObjectId, $this->tcaSchemaFactory->all()); + $inlineStructure = $this->inlineStackProcessor->addAjaxConfigurationToStructure($inlineStructure, $parentConfig); + $inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0); + $inlineParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, -1); + $fileReference = $this->inlineStackProcessor->getUnstableStructureFromStructure($inlineStructure); + + if (isset($fileReference['uid']) && MathUtility::canBeInterpretedAsInteger($fileReference['uid'])) { + // If uid comes in, it is the id of the record neighbor record "create after" + $fileReferenceVanillaUid = -1 * abs((int)$fileReference['uid']); + } else { + // Else inline first Pid is the storage pid of new inline records + $fileReferenceVanillaUid = $inlineFirstPid; + } + + $formDataCompilerInput = [ + 'request' => $request, + 'command' => 'new', + 'tableName' => self::FILE_REFERENCE_TABLE, + 'vanillaUid' => $fileReferenceVanillaUid, + 'isInlineChild' => true, + 'inlineStructure' => $inlineStructure, + 'inlineFirstPid' => $inlineFirstPid, + 'inlineParentUid' => $inlineParent['uid'], + 'inlineParentTableName' => $inlineParent['table'], + 'inlineParentFieldName' => $inlineParent['field'], + 'inlineParentConfig' => $parentConfig, + 'inlineTopMostParentUid' => $inlineTopMostParent['uid'], + 'inlineTopMostParentTableName' => $inlineTopMostParent['table'], + 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'], + ]; + if ($fileId) { + $formDataCompilerInput['inlineChildChildUid'] = $fileId; + } + + $fileReferenceData = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(TcaDatabaseRecord::class)); + + $fileReferenceData['inlineParentUid'] = $inlineParent['uid']; + $fileReferenceData['renderType'] = 'fileReferenceContainer'; + + return $this->jsonResponse( + $this->mergeFileReferenceResultIntoJsonResult( + [ + 'data' => '', + 'stylesheetFiles' => [], + 'scriptItems' => new JavaScriptItems(), + 'compilerInput' => [ + 'uid' => $fileReferenceData['databaseRow']['uid'], + 'childChildUid' => $fileId, + ], + ], + $this->nodeFactory->create($fileReferenceData)->render() + ) + ); + } + + /** + * Show the details of a file reference + */ + public function detailsAction(ServerRequestInterface $request): ResponseInterface + { + $arguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax']; + + $domObjectId = (string)($arguments[0] ?? ''); + $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); + $parentConfig = $this->extractSignedParentConfigFromRequest((string)($arguments['context'] ?? '')); + + $inlineStructure = $this->inlineStackProcessor->getStructureFromString($domObjectId, $this->tcaSchemaFactory->all()); + $inlineStructure = $this->inlineStackProcessor->addAjaxConfigurationToStructure($inlineStructure, $parentConfig); + $inlineParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, -1); + $fileReference = $this->inlineStackProcessor->getUnstableStructureFromStructure($inlineStructure); + + $parentFieldName = $inlineParent['field']; + + // Set flag in config so that only the fields are rendered + // @todo: Solve differently / rename / whatever + $parentConfig['renderFieldsOnly'] = true; + + $parentData = [ + 'processedTca' => [ + 'columns' => [ + $parentFieldName => [ + 'config' => $parentConfig, + ], + ], + ], + 'uid' => $inlineParent['uid'], + 'tableName' => $inlineParent['table'], + 'inlineFirstPid' => $inlineFirstPid, + 'returnUrl' => $parentConfig['originalReturnUrl'], + ]; + + $fileReferenceData = $this->compileFileReference($request, $parentData, $parentFieldName, (int)$fileReference['uid'], $inlineStructure); + $fileReferenceData['inlineParentUid'] = (int)$inlineParent['uid']; + $fileReferenceData['renderType'] = 'fileReferenceContainer'; + + return $this->jsonResponse( + $this->mergeFileReferenceResultIntoJsonResult( + [ + 'data' => '', + 'stylesheetFiles' => [], + 'scriptItems' => new JavaScriptItems(), + ], + $this->nodeFactory->create($fileReferenceData)->render() + ) + ); + } + + /** + * Adds localizations or synchronizes the locations of all file references. + */ + public function synchronizeLocalizeAction(ServerRequestInterface $request): ResponseInterface + { + $arguments = $request->getParsedBody()['ajax']; + + $domObjectId = (string)($arguments[0] ?? ''); + $type = $arguments[1] ?? null; + $parentConfig = $this->extractSignedParentConfigFromRequest((string)($arguments['context'] ?? '')); + + $inlineStructure = $this->inlineStackProcessor->getStructureFromString($domObjectId, $this->tcaSchemaFactory->all()); + $inlineStructure = $this->inlineStackProcessor->addAjaxConfigurationToStructure($inlineStructure, $parentConfig); + $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); + + $jsonArray = [ + 'data' => '', + 'stylesheetFiles' => [], + 'scriptItems' => new JavaScriptItems(), + 'compilerInput' => [ + 'localize' => [], + ], + ]; + if ($type === 'localize' || $type === 'synchronize' || MathUtility::canBeInterpretedAsInteger($type)) { + // Parent, this table embeds the sys_file_reference table + $inlineParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, -1); + $parentFieldName = $inlineParent['field']; + + $processedTca = $GLOBALS['TCA'][$inlineParent['table']]; + $processedTca['columns'][$parentFieldName]['config'] = $parentConfig; + + $formDataCompilerInputForParent = [ + 'request' => $request, + 'vanillaUid' => (int)$inlineParent['uid'], + 'command' => 'edit', + 'tableName' => $inlineParent['table'], + 'processedTca' => $processedTca, + 'inlineFirstPid' => $inlineFirstPid, + 'columnsToProcess' => [ + $parentFieldName, + ], + // @todo: still needed? NO! + 'inlineStructure' => $inlineStructure, + // Do not compile existing file references, we don't need them now + 'inlineCompileExistingChildren' => false, + ]; + // Full TcaDatabaseRecord is required here to have the list of connected uids $oldItemList + $parentData = $this->formDataCompiler->compile($formDataCompilerInputForParent, GeneralUtility::makeInstance(TcaDatabaseRecord::class)); + $parentLanguageField = $parentData['processedTca']['ctrl']['languageField']; + $parentLanguage = $parentData['databaseRow'][$parentLanguageField]; + $oldItemList = $parentData['databaseRow'][$parentFieldName]; + + // DataHandler cannot handle arrays as field value + if (is_array($parentLanguage)) { + $parentLanguage = implode(',', $parentLanguage); + } + + $cmd = []; + // Localize a single file reference from default language of the inlineParent element + if (MathUtility::canBeInterpretedAsInteger($type)) { + $cmd[$inlineParent['table']][$inlineParent['uid']]['inlineLocalizeSynchronize'] = [ + 'field' => $inlineParent['field'], + 'language' => $parentLanguage, + 'ids' => [$type], + ]; + } else { + // Either localize or synchronize all file references from default language of the inlineParent element + $cmd[$inlineParent['table']][$inlineParent['uid']]['inlineLocalizeSynchronize'] = [ + 'field' => $inlineParent['field'], + 'language' => $parentLanguage, + 'action' => $type, + ]; + } + + $tce = GeneralUtility::makeInstance(DataHandler::class); + $tce->start([], $cmd); + $tce->process_cmdmap(); + + $oldItems = $this->getFileReferenceUids((string)$oldItemList); + + $newItemList = (string)($tce->registerDBList[$inlineParent['table']][$inlineParent['uid']][$parentFieldName] ?? ''); + $newItems = $this->getFileReferenceUids($newItemList); + + // Render error messages from DataHandler + $tce->printLogErrorMessages(); + $messages = $this->flashMessageService->getMessageQueueByIdentifier()->getAllMessagesAndFlush(); + if (!empty($messages)) { + foreach ($messages as $message) { + $jsonArray['messages'][] = [ + 'title' => $message->getTitle(), + 'message' => $message->getMessage(), + 'severity' => $message->getSeverity(), + ]; + if ($message->getSeverity() === ContextualFeedbackSeverity::ERROR) { + $jsonArray['hasErrors'] = true; + } + } + } + + // Set the items that should be removed in the forms view: + $removedItems = array_diff($oldItems, $newItems); + $jsonArray['compilerInput']['delete'] = $removedItems; + + $localizedItems = array_diff($newItems, $oldItems); + foreach ($localizedItems as $i => $localizedFileReferenceUid) { + $fileReferenceData = $this->compileFileReference($request, $parentData, $parentFieldName, (int)$localizedFileReferenceUid, $inlineStructure); + $fileReferenceData['inlineParentUid'] = (int)$inlineParent['uid']; + $fileReferenceData['renderType'] = 'fileReferenceContainer'; + + $jsonArray = $this->mergeFileReferenceResultIntoJsonResult( + $jsonArray, + $this->nodeFactory->create($fileReferenceData)->render() + ); + + // Get the name of the field used as foreign selector (if any): + $selectedValue = $fileReferenceData['databaseRow']['uid_local']; + if (is_array($selectedValue)) { + $selectedValue = $selectedValue[0]; + } + + $jsonArray['compilerInput']['localize'][$i] = [ + 'uid' => $localizedFileReferenceUid, + 'selectedValue' => $selectedValue, + ]; + + // Remove possible virtual records in the form which showed that a file reference could be + // localized: + $transOrigPointerFieldName = $fileReferenceData['processedTca']['ctrl']['transOrigPointerField']; + if (isset($fileReferenceData['databaseRow'][$transOrigPointerFieldName]) && $fileReferenceData['databaseRow'][$transOrigPointerFieldName]) { + $transOrigPointerFieldValue = $fileReferenceData['databaseRow'][$transOrigPointerFieldName]; + if (is_array($transOrigPointerFieldValue)) { + $transOrigPointerFieldValue = $transOrigPointerFieldValue[0]; + if (is_array($transOrigPointerFieldValue) && ($transOrigPointerFieldValue['uid'] ?? false)) { + $transOrigPointerFieldValue = $transOrigPointerFieldValue['uid']; + } + } + $jsonArray['compilerInput']['localize'][$i]['remove'] = $transOrigPointerFieldValue; + } + } + } + return $this->jsonResponse($jsonArray); + } + + /** + * Store status of file references' expand / collapse state in backend user UC. + */ + public function expandOrCollapseAction(ServerRequestInterface $request): ResponseInterface + { + [$domObjectId, $expand, $collapse] = $request->getParsedBody()['ajax']; + + $inlineStructure = $this->inlineStackProcessor->getStructureFromString($domObjectId, $this->tcaSchemaFactory->all()); + $currentTable = $this->inlineStackProcessor->getUnstableStructureFromStructure($inlineStructure)['table']; + $top = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0); + $stateArray = $this->getReferenceExpandCollapseStateArray(); + // Only do some action if the top record and the current record were saved before + if (MathUtility::canBeInterpretedAsInteger($top['uid'])) { + // Set records to be expanded + foreach (GeneralUtility::trimExplode(',', $expand) as $uid) { + $stateArray[$top['table']][$top['uid']][$currentTable][] = $uid; + } + // Set records to be collapsed + foreach (GeneralUtility::trimExplode(',', $collapse) as $uid) { + $stateArray[$top['table']][$top['uid']][$currentTable] = $this->removeFromArray( + $uid, + $stateArray[$top['table']][$top['uid']][$currentTable] + ); + } + // Save states back to database + if (is_array($stateArray[$top['table']][$top['uid']][$currentTable] ?? false)) { + $stateArray[$top['table']][$top['uid']][$currentTable] = array_unique($stateArray[$top['table']][$top['uid']][$currentTable]); + $backendUser = $this->getBackendUserAuthentication(); + $backendUser->uc['inlineView'] = json_encode($stateArray); + $backendUser->writeUC(); + } + } + return $this->jsonResponse(); + } + + protected function compileFileReference(ServerRequestInterface $request, array $parentData, $parentFieldName, $fileReferenceUid, array $inlineStructure): array + { + $inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0); + return $this->formDataCompiler + ->compile( + [ + 'request' => $request, + 'command' => 'edit', + 'tableName' => self::FILE_REFERENCE_TABLE, + 'vanillaUid' => (int)$fileReferenceUid, + 'returnUrl' => $parentData['returnUrl'], + 'isInlineChild' => true, + 'inlineStructure' => $inlineStructure, + 'inlineFirstPid' => $parentData['inlineFirstPid'], + 'inlineParentConfig' => $parentData['processedTca']['columns'][$parentFieldName]['config'], + 'isInlineAjaxOpeningContext' => true, + 'inlineParentUid' => $parentData['databaseRow']['uid'] ?? $parentData['uid'], + 'inlineParentTableName' => $parentData['tableName'], + 'inlineParentFieldName' => $parentFieldName, + 'inlineTopMostParentUid' => $inlineTopMostParent['uid'], + 'inlineTopMostParentTableName' => $inlineTopMostParent['table'], + 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'], + ], + GeneralUtility::makeInstance(TcaDatabaseRecord::class) + ); + } + + /** + * Merge compiled file reference data into the json result array. + */ + protected function mergeFileReferenceResultIntoJsonResult(array $jsonResult, array $fileReferenceData): array + { + /** @var JavaScriptItems $scriptItems */ + $scriptItems = $jsonResult['scriptItems']; + + $jsonResult['data'] .= $fileReferenceData['html']; + $jsonResult['stylesheetFiles'] = []; + foreach ($fileReferenceData['stylesheetFiles'] as $stylesheetFile) { + $jsonResult['stylesheetFiles'][] = $this->getRelativePathToStylesheetFile($stylesheetFile); + } + if (!empty($fileReferenceData['inlineData'])) { + $jsonResult['inlineData'] = $fileReferenceData['inlineData']; + } + if (!empty($fileReferenceData['additionalInlineLanguageLabelFiles'])) { + $labels = []; + foreach ($fileReferenceData['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) { + ArrayUtility::mergeRecursiveWithOverrule( + $labels, + $this->getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile) + ); + } + $scriptItems->addGlobalAssignment(['TYPO3' => ['lang' => $labels]]); + } + $this->addJavaScriptModulesToJavaScriptItems($fileReferenceData['javaScriptModules'] ?? [], $scriptItems); + + return $jsonResult; + } + + /** + * Gets an array with the uids of file references out of a list of items. + */ + protected function getFileReferenceUids(string $itemList): array + { + $itemArray = GeneralUtility::trimExplode(',', $itemList, true); + // Perform modification of the selected items array: + foreach ($itemArray as &$value) { + $parts = explode('|', $value, 2); + $value = $parts[0]; + } + unset($value); + return $itemArray; + } + + /** + * Get expand / collapse state of inline items + */ + protected function getReferenceExpandCollapseStateArray(): array + { + $backendUser = $this->getBackendUserAuthentication(); + if (empty($backendUser->uc['inlineView'])) { + return []; + } + + $state = json_decode($backendUser->uc['inlineView'], true); + if (!is_array($state)) { + $state = []; + } + + return $state; + } + + /** + * Remove an element from an array. + */ + protected function removeFromArray(mixed $needle, array $haystack, bool $strict = false): array + { + $pos = array_search($needle, $haystack, $strict); + if ($pos !== false) { + unset($haystack[$pos]); + } + return $haystack; + } + + /** + * Get inlineFirstPid from a given objectId string + */ + protected function getInlineFirstPidFromDomObjectId(string $domObjectId): int|string|null + { + // Substitute FlexForm addition and make parsing a bit easier + $domObjectId = str_replace('---', ':', $domObjectId); + // The starting pattern of an object identifier (e.g. "data--) + $pattern = '/^data-(.+?)-(.+)$/'; + if (preg_match($pattern, $domObjectId, $match)) { + return $match[1]; + } + return null; + } + + /** + * Validates the config that is transferred over the wire to provide the + * correct TCA config for the parent table + */ + protected function extractSignedParentConfigFromRequest(string $contextString): array + { + if ($contextString === '') { + throw new \RuntimeException('Empty context string given', 1664486783); + } + $context = json_decode($contextString, true); + if (empty($context['config'])) { + throw new \RuntimeException('Empty context config section given', 1664486790); + } + if (!hash_equals($this->hashService->hmac((string)$context['config'], 'FilesContext'), (string)$context['hmac'])) { + throw new \RuntimeException('Hash does not validate', 1664486791); + } + return json_decode($context['config'], true); + } + + protected function jsonResponse(array $json = []): ResponseInterface + { + return $this->responseFactory->createResponse() + ->withHeader('Content-Type', 'application/json; charset=utf-8') + ->withBody($this->streamFactory->createStream((string)json_encode($json))); + } + + protected function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/FormFlexAjaxController.php b/Classes/Controller/FormFlexAjaxController.php new file mode 100644 index 0000000..14f7e5f --- /dev/null +++ b/Classes/Controller/FormFlexAjaxController.php @@ -0,0 +1,165 @@ +getParsedBody(); + + $vanillaUid = (int)$queryParameters['vanillaUid']; + $databaseRowUid = $queryParameters['databaseRowUid']; + $command = $queryParameters['command']; + $tableName = $queryParameters['tableName']; + $fieldName = $queryParameters['fieldName']; + $recordTypeValue = $queryParameters['recordTypeValue']; + $flexFormSheetName = $queryParameters['flexFormSheetName']; + $flexFormFieldName = $queryParameters['flexFormFieldName']; + $flexFormContainerName = $queryParameters['flexFormContainerName']; + + // Prepare TCA and data values for a new section container using data providers + // @todo Replace with a mutable schema + $processedTca = $GLOBALS['TCA'][$tableName]; + // Get a new unique id for this container. + $flexFormContainerIdentifier = StringUtility::getUniqueId(); + $flexSectionContainerPreparation = [ + 'flexFormSheetName' => $flexFormSheetName, + 'flexFormFieldName' => $flexFormFieldName, + 'flexFormContainerName' => $flexFormContainerName, + 'flexFormContainerIdentifier' => $flexFormContainerIdentifier, + ]; + + $formDataCompilerInput = [ + 'request' => $request, + 'tableName' => $tableName, + 'vanillaUid' => (int)$vanillaUid, + 'command' => $command, + 'recordTypeValue' => $recordTypeValue, + 'processedTca' => $processedTca, + 'flexSectionContainerPreparation' => $flexSectionContainerPreparation, + ]; + // A new container on a new record needs the 'NEW123' uid here, see comment + // in DatabaseUniqueUidNewRow for more information on that. + // @todo: Resolve, maybe with a redefinition of vanillaUid to transport the information more clean through this var? + // @see issue #80100 for a series of changes in this area + if ($command === 'new') { + $formDataCompilerInput['databaseRow']['uid'] = $databaseRowUid; + } + $formData = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(TcaDatabaseRecord::class)); + + $dataStructure = $formData['processedTca']['columns'][$fieldName]['config']['ds']; + $dataStructureIdentifier = $formData['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier']; + $formData['fieldName'] = $fieldName; + $formData['flexFormDataStructureArray'] = $dataStructure['sheets'][$flexFormSheetName]['ROOT']['el'][$flexFormFieldName]['children'][$flexFormContainerIdentifier]; + $formData['flexFormDataStructureIdentifier'] = $dataStructureIdentifier; + $formData['flexFormFieldName'] = $flexFormFieldName; + $formData['flexFormSheetName'] = $flexFormSheetName; + $formData['flexFormContainerName'] = $flexFormContainerName; + $formData['flexFormContainerIdentifier'] = $flexFormContainerIdentifier; + + $formData['flexFormFormPrefix'] = '[data][' . $flexFormSheetName . '][lDEF][' . $flexFormFieldName . '][el]'; + + // Set initialized data of that section container from compiler to the array part used + // by flexFormElementContainer which prepares parameterArray. Important for initialized + // values of group element. + if (isset($formData['databaseRow'][$fieldName] + ['data'][$flexFormSheetName] + ['lDEF'][$flexFormFieldName] + ['el'][$flexFormContainerIdentifier][$flexFormContainerName]['el'] + ) + && is_array( + $formData['databaseRow'][$fieldName] + ['data'][$flexFormSheetName] + ['lDEF'][$flexFormFieldName] + ['el'][$flexFormContainerIdentifier][$flexFormContainerName]['el'] + ) + ) { + $formData['flexFormRowData'] = $formData['databaseRow'][$fieldName] + ['data'][$flexFormSheetName] + ['lDEF'][$flexFormFieldName] + ['el'][$flexFormContainerIdentifier][$flexFormContainerName]['el']; + } + + $formData['parameterArray']['itemFormElName'] = 'data[' . $tableName . '][' . $formData['databaseRow']['uid'] . '][' . $fieldName . ']'; + + // Client-side behavior for event handlers: + $formData['parameterArray']['fieldChangeFunc'] = []; + $formData['parameterArray']['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = new UpdateValueOnFieldChange( + $tableName, + (string)$formData['databaseRow']['uid'], + $fieldName, + $formData['parameterArray']['itemFormElName'] + ); + + // @todo: check GroupElement for usage of elementBaseName ... maybe kick that thing? + + // Feed resulting form data to container structure to render HTML and other result data + $formData['renderType'] = 'flexFormContainerContainer'; + $newContainerResult = $this->nodeFactory->create($formData)->render(); + $scriptItems = new JavaScriptItems(); + + $jsonResult = [ + 'html' => $newContainerResult['html'], + 'stylesheetFiles' => [], + 'scriptItems' => $scriptItems, + ]; + + foreach ($newContainerResult['stylesheetFiles'] as $stylesheetFile) { + $jsonResult['stylesheetFiles'][] = $this->getRelativePathToStylesheetFile($stylesheetFile); + } + if (!empty($newContainerResult['additionalInlineLanguageLabelFiles'])) { + $labels = []; + foreach ($newContainerResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) { + ArrayUtility::mergeRecursiveWithOverrule( + $labels, + $this->getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile) + ); + } + $scriptItems->addGlobalAssignment(['TYPO3' => ['lang' => $labels]]); + } + $this->addJavaScriptModulesToJavaScriptItems($newContainerResult['javaScriptModules'] ?? [], $scriptItems); + + return new JsonResponse($jsonResult); + } +} diff --git a/Classes/Controller/FormInlineAjaxController.php b/Classes/Controller/FormInlineAjaxController.php new file mode 100644 index 0000000..e68788f --- /dev/null +++ b/Classes/Controller/FormInlineAjaxController.php @@ -0,0 +1,671 @@ +getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax']; + $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']); + + $domObjectId = $ajaxArguments[0] ?? ''; + $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); + if (!MathUtility::canBeInterpretedAsInteger($inlineFirstPid) + && !str_starts_with((string)$inlineFirstPid, 'NEW') + ) { + throw new \RuntimeException( + 'inlineFirstPid should either be an integer or a "NEW..." string', + 1521220491 + ); + } + $childChildUid = null; + if (isset($ajaxArguments[1]) && MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) { + $childChildUid = (int)$ajaxArguments[1]; + } + + // Parse the DOM identifier, add the levels to the structure stack + $inlineStructure = $this->inlineStackProcessor->getStructureFromString($domObjectId, $this->tcaSchemaFactory->all()); + $inlineStructure = $this->inlineStackProcessor->addAjaxConfigurationToStructure($inlineStructure, $parentConfig); + $inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0); + // Parent, this table embeds the child table + $inlineParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, -1); + // Child, a record from this table should be rendered + $child = $this->inlineStackProcessor->getUnstableStructureFromStructure($inlineStructure); + + if (isset($child['uid']) && MathUtility::canBeInterpretedAsInteger($child['uid'])) { + // If uid comes in, it is the id of the record neighbor record "create after" + $childVanillaUid = -1 * abs((int)$child['uid']); + } else { + // Else inline first Pid is the storage pid of new inline records + $childVanillaUid = $inlineFirstPid; + } + + $childTableName = $parentConfig['foreign_table']; + + $formDataCompilerInput = [ + 'request' => $request, + 'command' => 'new', + 'tableName' => $childTableName, + 'vanillaUid' => $childVanillaUid, + 'isInlineChild' => true, + 'inlineStructure' => $inlineStructure, + 'inlineFirstPid' => $inlineFirstPid, + 'inlineParentUid' => $inlineParent['uid'], + 'inlineParentTableName' => $inlineParent['table'], + 'inlineParentFieldName' => $inlineParent['field'], + 'inlineParentConfig' => $parentConfig, + 'inlineTopMostParentUid' => $inlineTopMostParent['uid'], + 'inlineTopMostParentTableName' => $inlineTopMostParent['table'], + 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'], + ]; + if ($childChildUid) { + $formDataCompilerInput['inlineChildChildUid'] = $childChildUid; + } + $childData = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(TcaDatabaseRecord::class)); + + if (($parentConfig['foreign_selector'] ?? false) && ($parentConfig['appearance']['useCombination'] ?? false)) { + // We have a foreign_selector. So, we just created a new record on an intermediate table in $childData. + // Now, if a valid id is given as second ajax parameter, the intermediate row should be connected to an + // existing record of the child-child table specified by the given uid. If there is no such id, user + // clicked on "created new" and a new child-child should be created, too. + if ($childChildUid) { + // Fetch existing child child + $childData['databaseRow'][$parentConfig['foreign_selector']] = [ + $childChildUid, + ]; + $childData['combinationChild'] = $this->compileChildChild($request, $childData, $parentConfig, $inlineStructure); + } else { + $formDataCompilerInput = [ + 'request' => $request, + 'command' => 'new', + 'tableName' => $this->getChildChildTableName($parentConfig['foreign_selector'], $childData), + 'vanillaUid' => $inlineFirstPid, + 'isInlineChild' => true, + 'isInlineAjaxOpeningContext' => true, + 'inlineStructure' => $inlineStructure, + 'inlineFirstPid' => $inlineFirstPid, + ]; + $childData['combinationChild'] = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(TcaDatabaseRecord::class)); + } + } + + $childData['inlineParentUid'] = $inlineParent['uid']; + $childData['renderType'] = 'inlineRecordContainer'; + $childResult = $this->nodeFactory->create($childData)->render(); + + $jsonArray = [ + 'data' => '', + 'stylesheetFiles' => [], + 'scriptItems' => new JavaScriptItems(), + 'compilerInput' => [ + 'uid' => $childData['databaseRow']['uid'], + 'childChildUid' => $childChildUid, + ], + ]; + + $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult); + + return new JsonResponse($jsonArray); + } + + /** + * Show the details of a child record. + */ + public function detailsAction(ServerRequestInterface $request): ResponseInterface + { + $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax']; + + $domObjectId = $ajaxArguments[0] ?? ''; + $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); + $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']); + + // Parse the DOM identifier, add the levels to the structure stack + $inlineStructure = $this->inlineStackProcessor->getStructureFromString($domObjectId, $this->tcaSchemaFactory->all()); + $inlineStructure = $this->inlineStackProcessor->addAjaxConfigurationToStructure($inlineStructure, $parentConfig); + // Parent, this table embeds the child table + $inlineParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, -1); + $parentFieldName = $inlineParent['field']; + + // Set flag in config so that only the fields are rendered + // @todo: Solve differently / rename / whatever + $parentConfig['renderFieldsOnly'] = true; + + $parentData = [ + 'processedTca' => [ + 'columns' => [ + $parentFieldName => [ + 'config' => $parentConfig, + ], + ], + ], + 'uid' => $inlineParent['uid'], + 'tableName' => $inlineParent['table'], + 'inlineFirstPid' => $inlineFirstPid, + // Hand over given original return url to compile stack. Needed if inline children compile links to + // another view (eg. edit metadata in a nested inline situation like news with inline content element image), + // so the back link is still the link from the original request. See issue #82525. This is additionally + // given down in TcaInline data provider to compiled children data. + 'returnUrl' => $parentConfig['originalReturnUrl'], + ]; + + // Child, a record from this table should be rendered + $child = $this->inlineStackProcessor->getUnstableStructureFromStructure($inlineStructure); + + $childData = $this->compileChild($request, $parentData, $parentFieldName, (int)$child['uid'], $inlineStructure); + + $childData['inlineParentUid'] = (int)$inlineParent['uid']; + $childData['renderType'] = 'inlineRecordContainer'; + $childResult = $this->nodeFactory->create($childData)->render(); + + $jsonArray = [ + 'data' => '', + 'stylesheetFiles' => [], + 'scriptItems' => new JavaScriptItems(), + ]; + + $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult); + + return new JsonResponse($jsonArray); + } + + /** + * Adds localizations or synchronizes the locations of all child records. + * Handle AJAX calls to localize all records of a parent, localize a single record or to synchronize with the original language parent. + * + * @param ServerRequestInterface $request the incoming request + * @return ResponseInterface the filled response + */ + public function synchronizeLocalizeAction(ServerRequestInterface $request): ResponseInterface + { + $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax']; + $domObjectId = $ajaxArguments[0] ?? ''; + $type = $ajaxArguments[1] ?? null; + $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']); + + // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config: + $inlineStructure = $this->inlineStackProcessor->getStructureFromString($domObjectId, $this->tcaSchemaFactory->all()); + $inlineStructure = $this->inlineStackProcessor->addAjaxConfigurationToStructure($inlineStructure, $parentConfig); + $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); + + $jsonArray = [ + 'data' => '', + 'stylesheetFiles' => [], + 'scriptItems' => new JavaScriptItems(), + 'compilerInput' => [ + 'localize' => [], + ], + ]; + if ($type === 'localize' || $type === 'synchronize' || MathUtility::canBeInterpretedAsInteger($type)) { + // Parent, this table embeds the child table + $parent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, -1); + $parentFieldName = $parent['field']; + + $processedTca = $GLOBALS['TCA'][$parent['table']]; + $processedTca['columns'][$parentFieldName]['config'] = $parentConfig; + + $formDataCompilerInputForParent = [ + 'request' => $request, + 'vanillaUid' => (int)$parent['uid'], + 'command' => 'edit', + 'tableName' => $parent['table'], + 'processedTca' => $processedTca, + 'inlineFirstPid' => $inlineFirstPid, + 'columnsToProcess' => [ + $parentFieldName, + ], + // @todo: still needed? NO! + 'inlineStructure' => $inlineStructure, + // Do not compile existing children, we don't need them now + 'inlineCompileExistingChildren' => false, + ]; + // Full TcaDatabaseRecord is required here to have the list of connected uids $oldItemList + $parentData = $this->formDataCompiler->compile($formDataCompilerInputForParent, GeneralUtility::makeInstance(TcaDatabaseRecord::class)); + $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config']; + $parentLanguageField = $parentData['processedTca']['ctrl']['languageField']; + $parentLanguage = $parentData['databaseRow'][$parentLanguageField]; + $oldItemList = $parentData['databaseRow'][$parentFieldName]; + + // DataHandler cannot handle arrays as field value + if (is_array($parentLanguage)) { + $parentLanguage = implode(',', $parentLanguage); + } + + $cmd = []; + // Localize a single child element from default language of the parent element + if (MathUtility::canBeInterpretedAsInteger($type)) { + $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [ + 'field' => $parent['field'], + 'language' => $parentLanguage, + 'ids' => [$type], + ]; + } else { + // Either localize or synchronize all child elements from default language of the parent element + $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [ + 'field' => $parent['field'], + 'language' => $parentLanguage, + 'action' => $type, + ]; + } + + $tce = GeneralUtility::makeInstance(DataHandler::class); + $tce->start([], $cmd); + $tce->process_cmdmap(); + + $newItemList = $tce->registerDBList[$parent['table']][$parent['uid']][$parentFieldName]; + + $oldItems = $this->getInlineRelatedRecordsUidArray($oldItemList); + $newItems = $this->getInlineRelatedRecordsUidArray($newItemList); + + // Render error messages from DataHandler + $tce->printLogErrorMessages(); + $messages = $this->flashMessageService->getMessageQueueByIdentifier()->getAllMessagesAndFlush(); + if (!empty($messages)) { + foreach ($messages as $message) { + $jsonArray['messages'][] = [ + 'title' => $message->getTitle(), + 'message' => $message->getMessage(), + 'severity' => $message->getSeverity(), + ]; + if ($message->getSeverity() === ContextualFeedbackSeverity::ERROR) { + $jsonArray['hasErrors'] = true; + } + } + } + + // Set the items that should be removed in the forms view: + $removedItems = array_diff($oldItems, $newItems); + $jsonArray['compilerInput']['delete'] = $removedItems; + + $localizedItems = array_diff($newItems, $oldItems); + foreach ($localizedItems as $i => $childUid) { + $childData = $this->compileChild($request, $parentData, $parentFieldName, (int)$childUid, $inlineStructure); + + $childData['inlineParentUid'] = (int)$parent['uid']; + $childData['renderType'] = 'inlineRecordContainer'; + $childResult = $this->nodeFactory->create($childData)->render(); + + $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult); + + // Get the name of the field used as foreign selector (if any): + $foreignSelector = isset($parentConfig['foreign_selector']) && $parentConfig['foreign_selector'] ? $parentConfig['foreign_selector'] : false; + $selectedValue = $foreignSelector ? $childData['databaseRow'][$foreignSelector] : null; + if (is_array($selectedValue)) { + $selectedValue = $selectedValue[0]; + } + + $jsonArray['compilerInput']['localize'][$i] = [ + 'uid' => $childUid, + 'selectedValue' => $selectedValue, + ]; + + // Remove possible virtual records in the form which showed that a child records could be localized: + $transOrigPointerFieldName = $childData['processedTca']['ctrl']['transOrigPointerField']; + if (isset($childData['databaseRow'][$transOrigPointerFieldName]) && $childData['databaseRow'][$transOrigPointerFieldName]) { + $transOrigPointerFieldValue = $childData['databaseRow'][$transOrigPointerFieldName]; + if (is_array($transOrigPointerFieldValue)) { + $transOrigPointerFieldValue = $transOrigPointerFieldValue[0]; + if (is_array($transOrigPointerFieldValue) && ($transOrigPointerFieldValue['uid'] ?? false)) { + // With nested inline containers (eg. fal sys_file_reference), row[l10n_parent][0] is sometimes + // a table / row combination again. See tx_styleguide_file file_5. If this happens we + // pick the uid field from the array ... Basically, we need the uid of the 'default language' record, + // since this is used in JS to locate and remove the 'shadowed' container. + // @todo: Find out if this is really necessary that sometimes ['databaseRow']['l10n_parent'][0] + // is resolved to a direct uid, and sometimes it's an array with items. Could this be harmonized? + $transOrigPointerFieldValue = $transOrigPointerFieldValue['uid']; + } + } + $jsonArray['compilerInput']['localize'][$i]['remove'] = $transOrigPointerFieldValue; + } + } + } + return new JsonResponse($jsonArray); + } + + /** + * Store status of inline children expand / collapse state in backend user uC. + * + * @param ServerRequestInterface $request the incoming request + * @return ResponseInterface the filled response + */ + public function expandOrCollapseAction(ServerRequestInterface $request): ResponseInterface + { + $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax']; + [$domObjectId, $expand, $collapse] = $ajaxArguments; + + // Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config + $inlineStructure = $this->inlineStackProcessor->getStructureFromString($domObjectId, $this->tcaSchemaFactory->all()); + + $backendUser = $this->getBackendUserAuthentication(); + // The current table - for this table we should add/import records + $currentTable = $this->inlineStackProcessor->getUnstableStructureFromStructure($inlineStructure); + $currentTable = $currentTable['table']; + // The top parent table - this table embeds the current table + $top = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0); + $topTable = $top['table']; + $topUid = $top['uid']; + $inlineView = $this->getInlineExpandCollapseStateArray(); + // Only do some action if the top record and the current record were saved before + if (MathUtility::canBeInterpretedAsInteger($topUid)) { + $expandUids = GeneralUtility::trimExplode(',', $expand); + $collapseUids = GeneralUtility::trimExplode(',', $collapse); + // Set records to be expanded + foreach ($expandUids as $uid) { + $inlineView[$topTable][$topUid][$currentTable][] = $uid; + } + // Set records to be collapsed + foreach ($collapseUids as $uid) { + $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]); + } + // Save states back to database + if (is_array($inlineView[$topTable][$topUid][$currentTable])) { + $inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]); + $backendUser->uc['inlineView'] = json_encode($inlineView); + $backendUser->writeUC(); + } + } + return new JsonResponse([]); + } + + /** + * Compile a full child record + * + * @param array $parentData Result array of parent + * @param string $parentFieldName Name of parent field + * @param int $childUid Uid of child to compile + * @param array $inlineStructure Current inline structure + * @return array Full result array + * + * @todo: This clones methods compileChild from TcaInline Provider. Find a better abstraction + * @todo: to also encapsulate the more complex scenarios with combination child and friends. + */ + protected function compileChild(ServerRequestInterface $request, array $parentData, $parentFieldName, $childUid, array $inlineStructure) + { + $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config']; + + $inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0); + + $childTableName = $inlineStructure['unstable']['table'] ?? null; + if (!$childTableName) { + throw new \RuntimeException('No unstable inline structure found', 1733754245); + } + + $formDataCompilerInput = [ + 'request' => $request, + 'command' => 'edit', + 'tableName' => $childTableName, + 'vanillaUid' => (int)$childUid, + 'returnUrl' => $parentData['returnUrl'], + 'isInlineChild' => true, + 'inlineStructure' => $inlineStructure, + 'inlineFirstPid' => $parentData['inlineFirstPid'], + 'inlineParentConfig' => $parentConfig, + 'isInlineAjaxOpeningContext' => true, + + // values of the current parent element + // it is always a string either an id or new... + 'inlineParentUid' => $parentData['databaseRow']['uid'] ?? $parentData['uid'], + 'inlineParentTableName' => $parentData['tableName'], + 'inlineParentFieldName' => $parentFieldName, + + // values of the top most parent element set on first level and not overridden on following levels + 'inlineTopMostParentUid' => $inlineTopMostParent['uid'], + 'inlineTopMostParentTableName' => $inlineTopMostParent['table'], + 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'], + ]; + // For foreign_selector with useCombination $mainChild is the mm record + // and $combinationChild is the child-child. For "normal" relations, $mainChild + // is just the normal child record and $combinationChild is empty. + $mainChild = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(TcaDatabaseRecord::class)); + if (($parentConfig['foreign_selector'] ?? false) && ($parentConfig['appearance']['useCombination'] ?? false)) { + // This kicks in if opening an existing mainChild that has a child-child set + $mainChild['combinationChild'] = $this->compileChildChild($request, $mainChild, $parentConfig, $inlineStructure); + } + return $mainChild; + } + + /** + * With useCombination set, not only content of the intermediate table, but also + * the connected child should be rendered in one go. Prepare this here. + * + * @param array $child Full data array of "mm" record + * @param array $parentConfig TCA configuration of "parent" + * @param array $inlineStructure Current inline structure + * @return array Full data array of child + */ + protected function compileChildChild(ServerRequestInterface $request, array $child, array $parentConfig, array $inlineStructure) + { + // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already + $childChildUid = $child['databaseRow'][$parentConfig['foreign_selector']][0]; + $formDataCompilerInput = [ + 'request' => $request, + 'command' => 'edit', + 'tableName' => $this->getChildChildTableName($parentConfig['foreign_selector'] ?? '', $child), + 'vanillaUid' => (int)$childChildUid, + 'isInlineChild' => true, + 'isInlineAjaxOpeningContext' => true, + // @todo: this is the wrong inline structure, isn't it? Shouldn't contain it the part from child child, too? + 'inlineStructure' => $inlineStructure, + 'inlineFirstPid' => $child['inlineFirstPid'], + // values of the top most parent element set on first level and not overridden on following levels + 'inlineTopMostParentUid' => $child['inlineTopMostParentUid'], + 'inlineTopMostParentTableName' => $child['inlineTopMostParentTableName'], + 'inlineTopMostParentFieldName' => $child['inlineTopMostParentFieldName'], + ]; + return $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(TcaDatabaseRecord::class)); + } + + /** + * Merge stuff from child array into json array. + * This method is needed since ajax handling methods currently need to put scriptCalls before and after child code. + * + * @param array $jsonResult Given json result + * @param array $childResult Given child result + * @return array Merged json array + */ + protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult) + { + /** @var JavaScriptItems $scriptItems */ + $scriptItems = $jsonResult['scriptItems']; + + $jsonResult['data'] .= $childResult['html']; + $jsonResult['stylesheetFiles'] = []; + foreach ($childResult['stylesheetFiles'] as $stylesheetFile) { + $jsonResult['stylesheetFiles'][] = $this->getRelativePathToStylesheetFile($stylesheetFile); + } + if (!empty($childResult['inlineData'])) { + $jsonResult['inlineData'] = $childResult['inlineData']; + } + if (!empty($childResult['additionalInlineLanguageLabelFiles'])) { + $labels = []; + foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) { + ArrayUtility::mergeRecursiveWithOverrule( + $labels, + $this->getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile) + ); + } + $scriptItems->addGlobalAssignment(['TYPO3' => ['lang' => $labels]]); + } + $this->addJavaScriptModulesToJavaScriptItems($childResult['javaScriptModules'] ?? [], $scriptItems); + + return $jsonResult; + } + + /** + * Gets an array with the uids of related records out of a list of items. + * This list could contain more information than required. This methods just + * extracts the uids. + * + * @param string $itemList The list of related child records + * @return array An array with uids + */ + protected function getInlineRelatedRecordsUidArray($itemList) + { + $itemArray = GeneralUtility::trimExplode(',', $itemList, true); + // Perform modification of the selected items array: + foreach ($itemArray as &$value) { + $parts = explode('|', $value, 2); + $value = $parts[0]; + } + unset($value); + return $itemArray; + } + + /** + * Get expand / collapse state of inline items + * + * @return array + */ + protected function getInlineExpandCollapseStateArray() + { + $backendUser = $this->getBackendUserAuthentication(); + if (!$this->backendUserHasUcInlineView($backendUser)) { + return []; + } + + $inlineView = json_decode($backendUser->uc['inlineView'], true); + if (!is_array($inlineView)) { + $inlineView = []; + } + + return $inlineView; + } + + /** + * Method to check whether the backend user has the property inline view for the current IRRE item. + * In existing or old IRRE items the attribute may not exist, then the json_decode will fail. + * + * @return bool + */ + protected function backendUserHasUcInlineView(BackendUserAuthentication $backendUser) + { + return !empty($backendUser->uc['inlineView']); + } + + /** + * Remove an element from an array. + * + * @param mixed $needle The element to be removed. + * @param array $haystack The array the element should be removed from. + * @param bool $strict Search elements strictly. + * @return array The array $haystack without the $needle + */ + protected function removeFromArray($needle, $haystack, $strict = false) + { + $pos = array_search($needle, $haystack, $strict); + if ($pos !== false) { + unset($haystack[$pos]); + } + return $haystack; + } + + /** + * Get inlineFirstPid from a given objectId string + * + * @param string $domObjectId The id attribute of an element + * @return int|string|null Pid or null + */ + protected function getInlineFirstPidFromDomObjectId(string $domObjectId) + { + // Substitute FlexForm addition and make parsing a bit easier + $domObjectId = str_replace('---', ':', $domObjectId); + // The starting pattern of an object identifier (e.g. "data--) + $pattern = '/^data-(.+?)-(.+)$/'; + if (preg_match($pattern, $domObjectId, $match)) { + return $match[1]; + } + return null; + } + + /** + * Validates the config that is transferred over the wire to provide the + * correct TCA config for the parent table + * + * @throws \RuntimeException + */ + protected function extractSignedParentConfigFromRequest(string $contextString): array + { + if ($contextString === '') { + throw new \RuntimeException('Empty context string given', 1489751361); + } + $context = json_decode($contextString, true); + if (empty($context['config'])) { + throw new \RuntimeException('Empty context config section given', 1489751362); + } + if (!hash_equals($this->hashService->hmac((string)$context['config'], 'InlineContext'), (string)$context['hmac'])) { + throw new \RuntimeException('Hash does not validate', 1489751363); + } + return json_decode($context['config'], true); + } + + /** + * The child-child table name is set in the child TCA "the selector field" and is depending on + * the TCA type (select or group) either the "foreign_table" or the (first) "allowed" table. + */ + protected function getChildChildTableName(string $foreignSelector, array $childConfiguration): string + { + $config = $childConfiguration['processedTca']['columns'][$foreignSelector]['config'] ?? []; + $type = $config['type'] ?? ''; + + return match ($type) { + 'select' => $config['foreign_table'] ?? '', + 'group' => GeneralUtility::trimExplode(',', $config['allowed'] ?? '', true)[0] ?? '', + default => '', + }; + } + + protected function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/FormSelectTreeAjaxController.php b/Classes/Controller/FormSelectTreeAjaxController.php new file mode 100644 index 0000000..9dcfc96 --- /dev/null +++ b/Classes/Controller/FormSelectTreeAjaxController.php @@ -0,0 +1,228 @@ +getQueryParams()['tableName'] ?? ''; + $fieldName = $request->getQueryParams()['fieldName'] ?? ''; + + // Prepare processedTca: Remove all column definitions except the one that contains + // our tree definition. This way only this field is calculated, everything else is ignored. + if (!$this->schemaFactory->has($tableName)) { + throw new \RuntimeException( + 'TCA for table ' . $tableName . ' not found', + 1479386729 + ); + } + $schema = $this->schemaFactory->get($tableName); + if (!$schema->hasField($fieldName)) { + throw new \RuntimeException( + 'TCA for table ' . $tableName . ' and field ' . $fieldName . ' not found', + 1479386990 + ); + } + + // @todo Replace with a mutable schema + $processedTca = $GLOBALS['TCA'][$tableName]; + + // Force given record type and set showitem to our field only + $recordTypeValue = $request->getQueryParams()['recordTypeValue']; + $processedTca['types'][$recordTypeValue]['showitem'] = $fieldName; + // Unset all columns except our field + $processedTca['columns'] = [ + $fieldName => $processedTca['columns'][$fieldName], + ]; + + $dataStructureIdentifier = ''; + $flexFormSheetName = ''; + $flexFormFieldName = ''; + $flexFormContainerIdentifier = ''; + $flexFormContainerFieldName = ''; + $flexSectionContainerPreparation = []; + if ($processedTca['columns'][$fieldName]['config']['type'] === 'flex') { + if (!empty($request->getQueryParams()['dataStructureIdentifier'])) { + $dataStructureIdentifier = $request->getQueryParams()['dataStructureIdentifier']; + } + $flexFormSheetName = $request->getQueryParams()['flexFormSheetName']; + $flexFormFieldName = $request->getQueryParams()['flexFormFieldName']; + $flexFormContainerName = $request->getQueryParams()['flexFormContainerName']; + $flexFormContainerIdentifier = $request->getQueryParams()['flexFormContainerIdentifier']; + $flexFormContainerFieldName = $request->getQueryParams()['flexFormContainerFieldName']; + $flexFormSectionContainerIsNew = (bool)$request->getQueryParams()['flexFormSectionContainerIsNew']; + + $dataStructure = $this->flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier, $schema); + + // Reduce given data structure down to the relevant element only + if (empty($flexFormContainerFieldName)) { + if (isset($dataStructure['sheets'][$flexFormSheetName]['ROOT'] + ['el'][$flexFormFieldName]) + ) { + $dataStructure = [ + 'sheets' => [ + $flexFormSheetName => [ + 'ROOT' => [ + 'type' => 'array', + 'el' => [ + $flexFormFieldName => $dataStructure['sheets'][$flexFormSheetName]['ROOT'] + ['el'][$flexFormFieldName], + ], + ], + ], + ], + ]; + } + } elseif (isset($dataStructure['sheets'][$flexFormSheetName]['ROOT'] + ['el'][$flexFormFieldName] + ['el'][$flexFormContainerName] + ['el'][$flexFormContainerFieldName]) + ) { + // If this is a tree in a section container that has just been added by the FlexFormAjaxController + // "new container" action, then this container is not yet persisted, so we need to trigger the + // TcaFlexProcess data provider again to prepare the DS and databaseRow of that container. + if ($flexFormSectionContainerIsNew) { + $flexSectionContainerPreparation = [ + 'flexFormSheetName' => $flexFormSheetName, + 'flexFormFieldName' => $flexFormFieldName, + 'flexFormContainerName' => $flexFormContainerName, + 'flexFormContainerIdentifier' => $flexFormContainerIdentifier, + ]; + } + // Now restrict the data structure to our tree element only + $dataStructure = [ + 'sheets' => [ + $flexFormSheetName => [ + 'ROOT' => [ + 'type' => 'array', + 'el' => [ + $flexFormFieldName => [ + 'section' => 1, + 'type' => 'array', + 'el' => [ + $flexFormContainerName => [ + 'type' => 'array', + 'el' => [ + $flexFormContainerFieldName => $dataStructure['sheets'][$flexFormSheetName]['ROOT'] + ['el'][$flexFormFieldName] + ['el'][$flexFormContainerName] + ['el'][$flexFormContainerFieldName], + ], + ], + ], + ], + ], + ], + ], + ], + ]; + } + $processedTca['columns'][$fieldName]['config']['ds'] = $dataStructure; + $processedTca['columns'][$fieldName]['config']['dataStructureIdentifier'] = $dataStructureIdentifier; + } + + $formDataCompilerInput = [ + 'request' => $request, + 'tableName' => $tableName, + 'vanillaUid' => (int)$request->getQueryParams()['uid'], + 'command' => $request->getQueryParams()['command'], + 'processedTca' => $processedTca, + 'recordTypeValue' => $recordTypeValue, + 'selectTreeCompileItems' => true, + 'flexSectionContainerPreparation' => $flexSectionContainerPreparation, + ]; + if (!empty($request->getQueryParams()['overrideValues'])) { + $formDataCompilerInput['overrideValues'] = json_decode($request->getQueryParams()['overrideValues'], true); + } + if (!empty($request->getQueryParams()['defaultValues'])) { + $formDataCompilerInput['defaultValues'] = json_decode($request->getQueryParams()['defaultValues'], true); + } + $formData = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(TcaSelectTreeAjaxFieldData::class)); + + if ($formData['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') { + if (empty($flexFormContainerFieldName)) { + $treeData = $formData['processedTca']['columns'][$fieldName]['config']['ds'] + ['sheets'][$flexFormSheetName]['ROOT'] + ['el'][$flexFormFieldName]['config']['items']; + } else { + $treeData = $formData['processedTca']['columns'][$fieldName]['config']['ds'] + ['sheets'][$flexFormSheetName]['ROOT'] + ['el'][$flexFormFieldName] + ['children'][$flexFormContainerIdentifier] + ['el'][$flexFormContainerFieldName]['config']['items']; + } + } else { + $treeData = $formData['processedTca']['columns'][$fieldName]['config']['items']; + } + + $data = []; + foreach ($treeData ?? [] as $item) { + $treeItem = new SelectTreeItem( + item: new TreeItem( + identifier: (string)$item['identifier'], + parentIdentifier: (string)($item['parentIdentifier'] ?? ''), + recordType: (string)($item['recordType'] ?? ''), + name: (string)($item['name'] ?? ''), + prefix: (string)($item['prefix'] ?? ''), + suffix: (string)($item['suffix'] ?? ''), + tooltip: (string)($item['tooltip'] ?? ''), + depth: (int)($item['depth'] ?? 0), + hasChildren: (bool)($item['hasChildren'] ?? false), + loaded: true, + icon: (string)($item['icon'] ?? ''), + overlayIcon: (string)($item['overlayIcon'] ?? ''), + statusInformation: (array)($item['statusInformation'] ?? []), + labels: (array)($item['labels'] ?? []), + ), + checked: (bool)($item['checked'] ?? false), + selectable: (bool)($item['selectable'] ?? false), + ); + $data[] = $treeItem; + } + + return new JsonResponse($data); + } +} diff --git a/Classes/Controller/FormSlugAjaxController.php b/Classes/Controller/FormSlugAjaxController.php new file mode 100644 index 0000000..c8e115a --- /dev/null +++ b/Classes/Controller/FormSlugAjaxController.php @@ -0,0 +1,185 @@ +checkRequest($request); + + $queryParameters = $request->getParsedBody() ?? []; + $values = $queryParameters['values']; + $mode = $queryParameters['mode']; + $tableName = (string)($queryParameters['tableName'] ?? ''); + $pid = (int)$queryParameters['pageId']; + $parentPageId = (int)$queryParameters['parentPageId']; + $recordId = (int)$queryParameters['recordId']; + $languageId = (int)$queryParameters['language']; + $fieldName = $queryParameters['fieldName']; + + $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'] ?? []; + $row = (array)BackendUtility::getRecord($tableName, $recordId); + $recordType = BackendUtility::getTCAtypeValue($tableName, $row, true); + if ($recordType !== null) { + $columnsOverridesConfigOfField = $GLOBALS['TCA'][$tableName]['types'][$recordType]['columnsOverrides'][$fieldName]['config'] ?? null; + if ($columnsOverridesConfigOfField) { + ArrayUtility::mergeRecursiveWithOverrule($fieldConfig, $columnsOverridesConfigOfField); + } + } + if (empty($fieldConfig)) { + throw new \RuntimeException( + 'No valid field configuration for table ' . $tableName . ' field name ' . $fieldName . ' found.', + 1535379534 + ); + } + + $evalInfo = !empty($fieldConfig['eval']) ? GeneralUtility::trimExplode(',', $fieldConfig['eval'], true) : []; + $hasToBeUniqueInDb = in_array('unique', $evalInfo, true); + $hasToBeUniqueInSite = in_array('uniqueInSite', $evalInfo, true); + $hasToBeUniqueInPid = in_array('uniqueInPid', $evalInfo, true); + + $hasConflict = false; + + $recordData = $values; + if (!isset($recordData['uid'])) { + $recordData['uid'] = $recordId; + } + $recordData['pid'] = $pid; + if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) { + $recordData[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] = $languageId; + } + if ($tableName === 'pages' && empty($recordData['is_siteroot'])) { + $recordData['is_siteroot'] = $row['is_siteroot'] ?? false; + } + + $workspaceId = $this->context->getPropertyFromAspect('workspace', 'id'); + $slug = GeneralUtility::makeInstance(SlugHelper::class, $tableName, $fieldName, $fieldConfig, $workspaceId); + if ($mode === 'auto') { + // New page - Feed incoming values to generator + $proposal = $slug->generate($recordData, $pid); + } elseif ($mode === 'recreate') { + $proposal = $slug->generate($recordData, $parentPageId); + } elseif ($mode === 'manual') { + // Existing record - Fetch full record and only validate against the new "slug" field. + $proposal = $slug->sanitize($values['manual']); + } else { + throw new \RuntimeException('mode must be either "auto", "recreate" or "manual"', 1535835666); + } + + $state = RecordStateFactory::forName($tableName) + ->fromArray($recordData, $pid, $recordId); + if ($hasToBeUniqueInDb && !$slug->isUniqueInTable($proposal, $state)) { + $hasConflict = true; + $proposal = $slug->buildSlugForUniqueInTable($proposal, $state); + } + if ($hasToBeUniqueInSite && !$slug->isUniqueInSite($proposal, $state)) { + $hasConflict = true; + $proposal = $slug->buildSlugForUniqueInSite($proposal, $state); + } + if ($hasToBeUniqueInPid && !$slug->isUniqueInPid($proposal, $state)) { + $hasConflict = true; + $proposal = $slug->buildSlugForUniqueInPid($proposal, $state); + } + + return new JsonResponse([ + 'hasConflicts' => $hasConflict, + 'manual' => $values['manual'] ?? '', + 'proposal' => $proposal, + ]); + } + + /** + * @throws \InvalidArgumentException + */ + protected function checkRequest(ServerRequestInterface $request): bool + { + $queryParameters = $request->getParsedBody() ?? []; + $expectedHash = $this->hashService->hmac( + implode( + '', + [ + $queryParameters['tableName'], + $queryParameters['pageId'], + $queryParameters['recordId'], + $queryParameters['language'], + $queryParameters['fieldName'], + $queryParameters['command'], + $queryParameters['parentPageId'], + ] + ), + __CLASS__ + ); + if (!hash_equals($expectedHash, $queryParameters['signature'])) { + throw new \InvalidArgumentException( + 'HMAC could not be verified', + 1535137045 + ); + } + return true; + } +} diff --git a/Classes/Controller/JavaScriptLanguageDomainController.php b/Classes/Controller/JavaScriptLanguageDomainController.php new file mode 100644 index 0000000..f27b6c8 --- /dev/null +++ b/Classes/Controller/JavaScriptLanguageDomainController.php @@ -0,0 +1,44 @@ +getAttribute('routing'); + $domain = $routing['domain']; + $locale = $routing['locale']; + return $this->javaScriptLanguageDomainProvider->createLanguageDomainResponse($domain, $locale); + } +} diff --git a/Classes/Controller/LinkBrowserController.php b/Classes/Controller/LinkBrowserController.php new file mode 100644 index 0000000..81e0fd6 --- /dev/null +++ b/Classes/Controller/LinkBrowserController.php @@ -0,0 +1,165 @@ +getCurrentPageId()); + return $tsConfig['TCEMAIN.']['linkHandler.']['page.']['configuration.'] ?? []; + } + + /** + * Encode a typolink via ajax. + * This avoids implementing the encoding functionality again in JS for the browser. + */ + public function encodeTypoLink(ServerRequestInterface $request): ResponseInterface + { + $typoLinkParts = $request->getQueryParams(); + if (isset($typoLinkParts['params'])) { + $typoLinkParts['additionalParams'] = $typoLinkParts['params']; + unset($typoLinkParts['params']); + } + $typoLink = $this->typoLinkCodecService->encode($typoLinkParts); + return new JsonResponse(['typoLink' => $typoLink]); + } + + protected function initDocumentTemplate(): void + { + if (!$this->areFieldChangeFunctionsValid() && !$this->areFieldChangeFunctionsValid(true)) { + $this->parameters['fieldChangeFunc'] = []; + } + $this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/form-engine-link-browser-adapter.js') + // @todo use a proper constructor when migrating to TypeScript + ->invoke('setOnFieldChangeItems', $this->parameters['fieldChangeFunc']) + ); + } + + protected function getCurrentPageId(): int + { + $pageId = 0; + $browserParameters = $this->parameters; + if (isset($browserParameters['pid'])) { + $pageId = $browserParameters['pid']; + } elseif (isset($browserParameters['itemName'])) { + // parse data[][] + if (preg_match('~data\[([^]]*)\]\[([^]]*)\]~', $browserParameters['itemName'], $matches)) { + $recordArray = BackendUtility::getRecord($matches['1'], $matches['2']); + if (is_array($recordArray)) { + $pageId = $recordArray['pid']; + } + } + } + return (int)BackendUtility::getRealPageId((string)$browserParameters['table'], (int)$browserParameters['uid'], (int)$pageId); + } + + protected function initCurrentUrl(): void + { + $currentLink = isset($this->parameters['currentValue']) ? trim($this->parameters['currentValue']) : ''; + /** @var array $currentLinkParts */ + $currentLinkParts = $this->typoLinkCodecService->decode($currentLink); + $currentLinkParts['params'] = $currentLinkParts['additionalParams']; + unset($currentLinkParts['additionalParams']); + + if (!empty($currentLinkParts['url'])) { + try { + $data = $this->linkService->resolve($currentLinkParts['url']); + $currentLinkParts['type'] = $data['type']; + unset($data['type']); + $currentLinkParts['url'] = $data; + } catch (UnknownLinkHandlerException $e) { + $this->flashMessageService->getMessageQueueByIdentifier()->enqueue( + new FlashMessage(message: $e->getMessage(), severity: ContextualFeedbackSeverity::ERROR) + ); + } + } + + $this->currentLinkParts = $currentLinkParts; + + parent::initCurrentUrl(); + } + + /** + * Determines whether submitted field change functions are valid + * and are coming from the system and not from an external abuse. + * + * @param bool $handleFlexformSections Whether to handle flexform sections differently + * @return bool Whether the submitted field change functions are valid + */ + protected function areFieldChangeFunctionsValid(bool $handleFlexformSections = false): bool + { + $result = false; + if (isset($this->parameters['fieldChangeFunc']) && is_array($this->parameters['fieldChangeFunc']) && isset($this->parameters['fieldChangeFuncHash'])) { + $matches = []; + $pattern = '#\\[el\\]\\[(([^]-]+-[^]-]+-)(idx\\d+-)([^]]+))\\]#i'; + $fieldChangeFunctions = $this->parameters['fieldChangeFunc']; + // Special handling of flexform sections: + // Field change functions are modified in JavaScript, thus the hash is always invalid + if ($handleFlexformSections && preg_match($pattern, $this->parameters['itemName'], $matches)) { + $originalName = $matches[1]; + $cleanedName = $matches[2] . $matches[4]; + $fieldChangeFunctions = $this->strReplaceRecursively( + $originalName, + $cleanedName, + $fieldChangeFunctions + ); + } + $result = hash_equals($this->hashService->hmac(serialize($fieldChangeFunctions), 'backend-link-browser'), $this->parameters['fieldChangeFuncHash']); + } + return $result; + } + + protected function strReplaceRecursively(string $search, string $replace, array $array): array + { + foreach ($array as &$item) { + if (is_array($item)) { + $item = $this->strReplaceRecursively($search, $replace, $item); + } else { + $item = str_replace($search, $replace, $item); + } + } + return $array; + } +} diff --git a/Classes/Controller/LinkController.php b/Classes/Controller/LinkController.php new file mode 100644 index 0000000..9f37c69 --- /dev/null +++ b/Classes/Controller/LinkController.php @@ -0,0 +1,125 @@ +getParsedBody()['identifier'] ?? null; + $resource = null; + + try { + if ($identifier) { + $resource = $this->resourceFactory->retrieveFileOrFolderObject($identifier); + } + if (!$resource instanceof File && !$resource instanceof Folder) { + throw new \InvalidArgumentException('Resource must be a file or a folder', 1679039649); + } + if ($resource->getStorage()->isFallbackStorage()) { + throw new InsufficientFileAccessPermissionsException('You are not allowed to access files outside your storages', 1679039650); + } + if ($resource instanceof File) { + if (!$resource->checkActionPermission('read')) { + throw new InsufficientFileAccessPermissionsException('You are not allowed to access this file', 1779001351); + } + $parameters = [ + 'type' => LinkService::TYPE_FILE, + 'file' => $resource, + ]; + } + if ($resource instanceof Folder) { + // Note: No explicit `$resource->checkActionPermission('read')` check here, as that would + // be a no-op since `ResourceStorage::getFolder()` calls `assureFolderReadPermission()` + // and throws `InsufficientFolderAccessPermissionsException` + $parameters = [ + 'type' => LinkService::TYPE_FOLDER, + 'folder' => $resource, + ]; + } + $link = $this->linkService->asString($parameters); + } catch (InsufficientFileAccessPermissionsException|InsufficientFolderAccessPermissionsException $exception) { + $message = match ($exception->getCode()) { + 1679039650 => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.error.message.resourceOutsideOfStorages'), + default => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.error.message.resourceNoPermissionRead'), + }; + + return new JsonResponse($this->getResponseData(false, $message)); + } catch (\Exception $exception) { + $message = match ($exception->getCode()) { + 1679039649 => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.error.message.resourceNotFileOrFolder'), + default => $exception->getMessage(), + }; + + return new JsonResponse($this->getResponseData(false, $message)); + } + + return new JsonResponse($this->getResponseData(true, null, $link)); + } + + /** + * Prepare response data for a JSON response + */ + private function getResponseData(bool $success, ?string $message = null, ?string $link = null): array + { + $flashMessageQueue = new FlashMessageQueue('backend'); + if ($message) { + $flashMessageQueue->enqueue( + new FlashMessage( + $message, + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.' . ($success ? 'success' : 'error')), + $success ? ContextualFeedbackSeverity::OK : ContextualFeedbackSeverity::ERROR + ) + ); + } + return [ + 'success' => $success, + 'status' => $flashMessageQueue, + 'link' => $link, + ]; + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/LiveSearchController.php b/Classes/Controller/LiveSearchController.php new file mode 100644 index 0000000..3b52f5d --- /dev/null +++ b/Classes/Controller/LiveSearchController.php @@ -0,0 +1,119 @@ +getQuery() === '') { + return new Response('', 400, [], 'Argument "query" is missing or empty.'); + } + + $results = $this->searchService->find($mutableSearchDemand); + $pagination = new SlidingWindowPagination($results, 15); + $response = [ + 'pagination' => [ + 'itemsPerPage' => SearchDemand::DEFAULT_LIMIT, + 'currentPage' => $pagination->getPaginator()->getCurrentPageNumber(), + 'firstPage' => $pagination->getFirstPageNumber(), + 'lastPage' => $pagination->getLastPageNumber(), + 'allPageNumbers' => $pagination->getAllPageNumbers(), + 'previousPageNumber' => $pagination->getPreviousPageNumber(), + 'nextPageNumber' => $pagination->getNextPageNumber(), + 'hasMorePages' => $pagination->getHasMorePages(), + 'hasLessPages' => $pagination->getHasLessPages(), + ], + 'results' => $results->getPaginatedItems(), + ]; + + return new JsonResponse($response); + } + + public function formAction(ServerRequestInterface $request): ResponseInterface + { + $hints = [ + 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:liveSearch_helpDescriptionPages', + 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:liveSearch_helpDescriptionContent', + 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:liveSearch_help.shortcutOpen', + ]; + + $event = $this->eventDispatcher->dispatch( + new BeforeLiveSearchFormIsBuiltEvent($hints, $request) + ); + $hints = $event->getHints(); + $searchDemand = $event->getSearchDemand(); + $randomHintKey = array_rand($hints); + $additionalViewData = $event->getAdditionalViewData(); + + $searchProviders = $this->searchService->getSearchProviderState($searchDemand); + + $activeOptions = 0; + // `isActive` is the result of `in_array()`, which returns a `bool`. + $activeOptions += count(array_filter($searchProviders, fn(array $searchProviderOption): bool => $searchProviderOption['isActive'])); + + $view = $this->backendViewFactory->create($request, ['typo3/cms-backend']); + if ($additionalViewData !== []) { + $view->assignMultiple($additionalViewData); + } + $view->assignMultiple([ + 'searchDemand' => $searchDemand, + 'hint' => $this->getLanguageService()->sL($hints[$randomHintKey]), + 'searchProviders' => $searchProviders, + 'activeOptions' => $activeOptions, + ]); + + $response = new Response(); + $response->getBody()->write($view->render('LiveSearch/Form')); + + return $response; + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php new file mode 100644 index 0000000..9cc0950 --- /dev/null +++ b/Classes/Controller/LoginController.php @@ -0,0 +1,298 @@ +createLoginLogout($request, (bool)($request->getParsedBody()['loginRefresh'] ?? $request->getQueryParams()['loginRefresh'] ?? false)); + } + + /** + * Calls the main function but with loginRefresh enabled at any time + */ + public function refreshAction(ServerRequestInterface $request): ResponseInterface + { + return $this->createLoginLogout($request, true); + } + + /** + * @param bool $loginRefresh The backend triggers this with this value set when the login is + * close to being expired and the form needs to be redrawn. + * @throws PropagateResponseException + * @throws RouteNotFoundException + */ + protected function createLoginLogout(ServerRequestInterface $request, bool $loginRefresh): ResponseInterface + { + $backendUser = $this->getBackendUserAuthentication(); + if (!empty($backendUser->user['uid'])) { + // If BE user is logged in, redirect to backend. Also handles "refresh" foo. + $this->checkRedirect($request, $loginRefresh); + } + + $languageService = $this->getLanguageService(); + if (empty($backendUser->user['uid'])) { + // If no user is logged in, initialize LanguageService with preferred browser language and set the + // language to the backend user object, so labels in fluid views are translated. + $httpAcceptLanguage = $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'] ?? ''; + $preferredBrowserLanguage = $this->locales->getPreferredClientLanguage($httpAcceptLanguage); + $languageService->init($this->locales->createLocale($preferredBrowserLanguage)); + $backendUser->user['lang'] = $preferredBrowserLanguage; + } + + if (($backgroundImageStyles = $this->authenticationStyleInformation->getBackgroundImageStyles($request)) !== '') { + $this->pageRenderer->addCssInlineBlock('loginBackgroundImage', $backgroundImageStyles, null, false, true); + } + if (($highlightColorStyles = $this->authenticationStyleInformation->getHighlightColorStyles()) !== '') { + $this->pageRenderer->addCssInlineBlock('loginHighlightColor', $highlightColorStyles, null, false, true); + } + $loginProviderIdentifier = $this->loginProviderResolver->resolveLoginProviderIdentifierFromRequest($request, 'be_lastLoginProvider'); + if (empty($backendUser->user['uid'])) { + // Show login form + $action = 'login'; + $formActionUrl = $this->uriBuilder->buildUriWithRedirect('login', ['loginProvider' => $loginProviderIdentifier], RouteRedirect::createFromRequest($request)); + } else { + // Show logout form + $action = 'logout'; + $formActionUrl = $this->uriBuilder->buildUriFromRoute('logout'); + } + $forgotPasswordUrl = $this->uriBuilder->buildUriWithRedirect('password_forget', ['loginProvider' => $loginProviderIdentifier], RouteRedirect::createFromRequest($request)); + $viewVariables = [ + 'copyright' => $this->typo3Information->getCopyrightNotice(), + 'loginFootnote' => $this->authenticationStyleInformation->getFooterNote(), + 'referrerCheckEnabled' => $this->features->isFeatureEnabled('security.backend.enforceReferrer'), + 'loginUrl' => (string)$request->getUri(), + 'loginProviderIdentifier' => $loginProviderIdentifier, + 'backendUser' => $backendUser->user, + 'hasLoginError' => $this->isLoginInProgress($request), + 'action' => $action, + 'formActionUrl' => $formActionUrl, + 'requestTokenName' => RequestToken::PARAM_NAME, + 'requestTokenValue' => $this->provideRequestTokenJwt(), + 'forgetPasswordUrl' => $forgotPasswordUrl, + 'loginRefresh' => $loginRefresh, + 'loginProviders' => $this->loginProviderResolver->getLoginProviders(), + 'loginNewsItems' => $this->getSystemNews(), + ]; + + $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $languageService); + $this->pageRenderer->setTitle('TYPO3 CMS Login: ' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '')); + + $loginProviderConfiguration = $this->loginProviderResolver->getLoginProviderConfigurationByIdentifier($loginProviderIdentifier); + $loginProvider = GeneralUtility::makeInstance($loginProviderConfiguration['provider']); + if (!$loginProvider instanceof LoginProviderInterface) { + throw new \RuntimeException($loginProviderConfiguration['provider'] . ' must implement LoginProviderInterface', 1724772171); + } + $viewFactoryData = new ViewFactoryData( + templateRootPaths: ['EXT:backend/Resources/Private/Templates'], + partialRootPaths: ['EXT:backend/Resources/Private/Partials'], + layoutRootPaths: ['EXT:backend/Resources/Private/Layouts'], + request: $request, + ); + $view = $this->viewFactory->create($viewFactoryData); + $view->assignMultiple($viewVariables); + $this->eventDispatcher->dispatch(new ModifyPageLayoutOnLoginProviderSelectionEvent($view, $request)); + $templateFile = $loginProvider->modifyView($request, $view); + $content = $view->render($templateFile); + $this->pageRenderer->setBodyContent('' . $content); + $response = $this->pageRenderer->renderResponse($request); + return $this->appendLoginProviderCookie($request, $response); + } + + /** + * Returns a new request-token value, which is signed by a new nonce value (the nonce is sent + * as cookie automatically in `RequestTokenMiddleware` since it is created via the `NoncePool`). + */ + public function requestTokenAction(ServerRequestInterface $request): ResponseInterface + { + return new JsonResponse([ + 'headerName' => RequestToken::HEADER_NAME, + 'requestToken' => $this->provideRequestTokenJwt(), + ]); + } + + /** + * @throws PropagateResponseException + */ + protected function checkRedirect(ServerRequestInterface $request, bool $loginRefresh): void + { + $formProtection = $this->formProtectionFactory->createFromRequest($request); + if (!$formProtection instanceof BackendFormProtection) { + throw new \RuntimeException('The Form Protection retrieved does not match the expected one.', 1432080411); + } + if ($loginRefresh) { + // Triggering `TYPO3/CMS/Backend/LoginRefresh` module happens in JS `TYPO3/CMS/Backend/Login` + $formProtection->setSessionTokenFromRegistry(); + $formProtection->persistSessionToken(); + } else { + $formProtection->storeSessionTokenInRegistry(); + // @todo: Consolidate RouteDispatcher::evaluateReferrer() when changing 'main' to something different + $redirectToURL = (string)$this->uriBuilder->buildUriWithRedirect('main', [], RouteRedirect::createFromRequest($request)); + throw new PropagateResponseException(new RedirectResponse($redirectToURL, 303), 1724705833); + } + } + + /** + * If a login provider was chosen in the previous request, which is not the default provider, + * it is stored in a Cookie and appended to the HTTP Response. + */ + protected function appendLoginProviderCookie(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $normalizedParams = $request->getAttribute('normalizedParams'); + $loginProviderIdentifier = $this->loginProviderResolver->resolveLoginProviderIdentifierFromRequest($request, 'be_lastLoginProvider'); + if ($loginProviderIdentifier === $this->loginProviderResolver->getPrimaryLoginProviderIdentifier()) { + return $response; + } + $cookie = new Cookie( + 'be_lastLoginProvider', + $loginProviderIdentifier, + $GLOBALS['EXEC_TIME'] + 7776000, // 90 days + $this->backendEntryPointResolver->getPathFromRequest($request), + '', + // Use the secure option when the current request is served by a secure connection + $normalizedParams->isHttps(), + true, + false, + Cookie::SAMESITE_STRICT + ); + return $response->withAddedHeader('Set-Cookie', $cookie->__toString()); + } + + /** + * Gets news as array from sys_news and converts them into a + * format suitable for showing them at the login screen. + */ + protected function getSystemNews(): array + { + $systemNews = []; + $queryResult = $this->connectionPool + ->getQueryBuilderForTable('sys_news') + ->select('uid', 'title', 'content', 'crdate') + ->from('sys_news') + ->orderBy('crdate', 'DESC') + ->executeQuery(); + while ($row = $queryResult->fetchAssociative()) { + $systemNews[] = [ + 'uid' => $row['uid'], + 'date' => $row['crdate'] ? date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], (int)$row['crdate']) : '', + 'header' => $row['title'], + 'content' => $row['content'], + ]; + } + return $systemNews; + } + + /** + * Checks if login credentials have been submitted + */ + protected function isLoginInProgress(ServerRequestInterface $request): bool + { + // @todo: Restrict to POST?! + // Value of forms submit button for login. If set, the login button was pressed. + $submitValue = $request->getParsedBody()['commandLI'] ?? $request->getQueryParams()['commandLI'] ?? ''; + $username = $request->getParsedBody()['username'] ?? $request->getQueryParams()['username'] ?? null; + return !empty($username) || !empty($submitValue); + } + + protected function provideRequestTokenJwt(): string + { + $nonce = SecurityAspect::provideIn($this->context)->provideNonce(); + return RequestToken::create('core/user-auth/be')->toHashSignedJwt($nonce); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/LogoutController.php b/Classes/Controller/LogoutController.php new file mode 100644 index 0000000..1fecd4a --- /dev/null +++ b/Classes/Controller/LogoutController.php @@ -0,0 +1,86 @@ +processLogout($request); + + $redirectUrl = $request->getParsedBody()['redirect'] ?? $request->getQueryParams()['redirect'] ?? ''; + $redirectUrl = GeneralUtility::sanitizeLocalUrl($redirectUrl, $request); + if (empty($redirectUrl)) { + $redirectUrl = (string)$this->uriBuilder->buildUriFromRoute('login', [], UriBuilder::ABSOLUTE_URL); + } + return new RedirectResponse(GeneralUtility::locationHeaderUrl($redirectUrl, $request), 303); + } + + /** + * Performs the logout processing + */ + protected function processLogout(ServerRequestInterface $request): void + { + if (empty($this->getBackendUser()->user['username'])) { + return; + } + // Logout written to log + $this->getBackendUser()->writelog(SystemLogType::LOGIN, SystemLogLoginAction::LOGOUT, SystemLogErrorClassification::MESSAGE, null, 'User %s logged out from TYPO3 Backend', [$this->getBackendUser()->user['username']]); + /** @var BackendFormProtection $backendFormProtection */ + $backendFormProtection = $this->formProtectionFactory->createFromRequest($request); + $backendFormProtection->removeSessionTokenFromRegistry(); + $this->getBackendUser()->logoff(); + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/MfaAjaxController.php b/Classes/Controller/MfaAjaxController.php new file mode 100644 index 0000000..c109953 --- /dev/null +++ b/Classes/Controller/MfaAjaxController.php @@ -0,0 +1,216 @@ +mfaProviderRegistry = $mfaProviderRegistry; + } + + /** + * Main entry point, checking prerequisite and dispatching to the requested action + */ + public function handleRequest(ServerRequestInterface $request): ResponseInterface + { + $action = (string)($request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? ''); + + if (!in_array($action, self::ALLOWED_ACTIONS, true)) { + return new JsonResponse($this->getResponseData(false, $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:ajax.invalidRequest'))); + } + + $userId = (int)($request->getParsedBody()['userId'] ?? 0); + $tableName = (string)($request->getParsedBody()['tableName'] ?? ''); + + if (!$userId || !in_array($tableName, ['be_users', 'fe_users'], true)) { + return new JsonResponse($this->getResponseData(false, $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:ajax.invalidRequest'))); + } + + $user = $this->initializeUser($userId, $tableName); + + if (!$this->isAllowedToPerformAction($action, $user)) { + return new JsonResponse($this->getResponseData(false, $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:ajax.insufficientPermissions'))); + } + + return new JsonResponse($this->{$action . 'Action'}($request, $user)); + } + + /** + * Deactivate MFA providers + * If the request contains a provider, it will be deactivated. + * Otherwise all active providers are deactivated. + */ + protected function deactivateAction(ServerRequestInterface $request, AbstractUserAuthentication $user): array + { + $lang = $this->getLanguageService(); + $userName = $user->getUserName() ?? ''; + $providerToDeactivate = (string)($request->getParsedBody()['provider'] ?? ''); + + if ($providerToDeactivate === '') { + // In case no provider is given, try to deactivate all active providers + $providersToDeactivate = $this->mfaProviderRegistry->getActiveProviders($user); + if ($providersToDeactivate === []) { + return $this->getResponseData( + false, + $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:ajax.deactivate.providersNotDeactivated'), + $user + ); + } + foreach ($providersToDeactivate as $provider) { + $propertyManager = MfaProviderPropertyManager::create($provider, $user); + if (!$provider->deactivate($request, $propertyManager)) { + return $this->getResponseData( + false, + sprintf($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:ajax.deactivate.providerNotDeactivated'), $lang->sL($provider->getTitle())), + $user + ); + } + } + return $this->getResponseData( + true, + sprintf($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:ajax.deactivate.providersDeactivated'), $userName), + $user + ); + } + + if (!$this->mfaProviderRegistry->hasProvider($providerToDeactivate)) { + return $this->getResponseData( + false, + sprintf($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:ajax.deactivate.providerNotFound'), $providerToDeactivate), + $user + ); + } + + $provider = $this->mfaProviderRegistry->getProvider($providerToDeactivate); + $propertyManager = MfaProviderPropertyManager::create($provider, $user); + + if (!$provider->isActive($propertyManager) || !$provider->deactivate($request, $propertyManager)) { + return $this->getResponseData( + false, + sprintf($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:ajax.deactivate.providerNotDeactivated'), $lang->sL($provider->getTitle())), + $user + ); + } + + return $this->getResponseData( + true, + sprintf($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:ajax.deactivate.providerDeactivated'), $lang->sL($provider->getTitle()), $userName), + $user + ); + } + + /** + * Initialize a user based on the table name + */ + protected function initializeUser(int $userId, string $tableName): AbstractUserAuthentication + { + $user = $tableName === 'be_users' + ? GeneralUtility::makeInstance(BackendUserAuthentication::class) + : GeneralUtility::makeInstance(FrontendUserAuthentication::class); + + $user->enablecolumns = ['deleted' => true]; + $user->setBeUserByUid($userId); + + return $user; + } + + /** + * Prepare response data for a JSON response + */ + protected function getResponseData(bool $success, string $message, ?AbstractUserAuthentication $user = null): array + { + $flashMessageQueue = new FlashMessageQueue('backend'); + $flashMessageQueue->enqueue( + new FlashMessage( + $message, + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:ajax.' . ($success ? 'success' : 'error')) + ) + ); + $payload = [ + 'success' => $success, + 'status' => $flashMessageQueue, + ]; + + if ($user !== null) { + $payload['remaining'] = count($this->mfaProviderRegistry->getActiveProviders($user)); + } + + return $payload; + } + + /** + * Check if the current logged in user is allowed to perform + * the requested action on the selected user. + */ + protected function isAllowedToPerformAction(string $action, AbstractUserAuthentication $user): bool + { + if ($action === 'deactivate') { + $currentBackendUser = $this->getBackendUser(); + // Only admins are allowed to deactivate providers + if (!$currentBackendUser->isAdmin()) { + return false; + } + // Providers from system maintainers can only be deactivated by system maintainers. + // However, this check is only necessary if the target is a backend user. + if (($user instanceof BackendUserAuthentication) + && $user->isSystemMaintainer(true) + && !$this->getBackendUser()->isSystemMaintainer() + ) { + return false; + } + return true; + } + + return false; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/MfaConfigurationController.php b/Classes/Controller/MfaConfigurationController.php new file mode 100644 index 0000000..4ad79b4 --- /dev/null +++ b/Classes/Controller/MfaConfigurationController.php @@ -0,0 +1,384 @@ +initializeMfaConfiguration(); + $this->view = $this->moduleTemplateFactory->create($request); + $action = (string)($request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'overview'); + + if (!$this->isActionAllowed($action)) { + return new HtmlResponse('Action not allowed', 400); + } + + $mfaProvider = null; + $identifier = (string)($request->getQueryParams()['identifier'] ?? $request->getParsedBody()['identifier'] ?? ''); + // Check if given identifier is valid + if ($this->isValidIdentifier($identifier)) { + $mfaProvider = $this->mfaProviderRegistry->getProvider($identifier); + } + // All actions expect "overview" require a provider to deal with. + // If non is found at this point, initiate a redirect to the overview. + if ($mfaProvider === null && $action !== 'overview') { + $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerNotFound'), '', ContextualFeedbackSeverity::ERROR); + return new RedirectResponse($this->getActionUri('overview')); + } + // If a valid provider is given, check if the requested action can be performed on this provider + if ($mfaProvider !== null) { + $isProviderActive = $mfaProvider->isActive( + MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()) + ); + // Some actions require the provider to be inactive + if ($isProviderActive && in_array($action, $this->providerActionsWhenInactive, true)) { + $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerActive'), '', ContextualFeedbackSeverity::ERROR); + return new RedirectResponse($this->getActionUri('overview')); + } + // Some actions require the provider to be active + if (!$isProviderActive && in_array($action, $this->providerActionsWhenActive, true)) { + $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerNotActive'), '', ContextualFeedbackSeverity::ERROR); + return new RedirectResponse($this->getActionUri('overview')); + } + } + + switch ($action) { + case 'overview': + return $this->overviewAction($request); + case 'setup': + case 'edit': + case 'activate': + case 'deactivate': + case 'unlock': + case 'save': + return $this->{$action . 'Action'}($request, $mfaProvider); + default: + return new HtmlResponse('Action not allowed', 400); + } + } + + /** + * Setup the overview with all available MFA providers + */ + protected function overviewAction(ServerRequestInterface $request): ResponseInterface + { + $this->addOverviewButtons($request); + $this->view->assignMultiple([ + 'providers' => $this->allowedProviders, + 'defaultProvider' => $this->getDefaultProviderIdentifier(), + 'recommendedProvider' => $this->getRecommendedProviderIdentifier(), + 'setupRequired' => $this->mfaRequired && !$this->mfaProviderRegistry->hasActiveProviders($this->getBackendUser()), + ]); + return $this->view->renderResponse('Mfa/Overview'); + } + + /** + * Render form to setup a provider by using provider specific content + */ + protected function setupAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface + { + $this->addFormButtons(); + $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()); + $providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::SETUP); + $this->view->assignMultiple([ + 'provider' => $mfaProvider, + 'providerContent' => $providerResponse->getBody(), + ]); + return $this->view->renderResponse('Mfa/Setup'); + } + + /** + * Handle activate request, receiving from the setup view + * by forwarding the request to the appropriate provider. + * Furthermore, add the provider as default provider in case + * it is the recommended provider for this user, or no default + * provider is yet defined the newly activated provider is allowed + * to be a default provider and there are no other providers which + * would suite as default provider. + */ + protected function activateAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface + { + $backendUser = $this->getBackendUser(); + $isRecommendedProvider = $this->getRecommendedProviderIdentifier() === $mfaProvider->getIdentifier(); + $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $backendUser); + $languageService = $this->getLanguageService(); + // Check whether activation operation was successful and the provider is now active. + if (!$mfaProvider->activate($request, $propertyManager) || !$mfaProvider->isActive($propertyManager)) { + $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:activate.failure'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::ERROR); + return new RedirectResponse($this->getActionUri('setup', ['identifier' => $mfaProvider->getIdentifier()])); + } + if ($isRecommendedProvider + || ( + $this->getDefaultProviderIdentifier() === '' + && $mfaProvider->isDefaultProviderAllowed() + && !$this->hasSuitableDefaultProviders([$mfaProvider->getIdentifier()]) + ) + ) { + $this->setDefaultProvider($mfaProvider); + } + // If this is the first activated provider, the user has logged in without being required + // to pass the MFA challenge. Therefore, no session entry exists. To prevent the challenge + // from showing up after the activation we need to set the session data here. + if (!(bool)($backendUser->getSessionData('mfa') ?? false)) { + $backendUser->setSessionData('mfa', true); + } + $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:activate.success'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::OK); + return new RedirectResponse($this->getActionUri('overview')); + } + + /** + * Handle deactivate request by forwarding the request to the + * appropriate provider. Also remove the provider as default + * provider from user UC, if set. + */ + protected function deactivateAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface + { + $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()); + $languageService = $this->getLanguageService(); + if (!$mfaProvider->deactivate($request, $propertyManager)) { + $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:deactivate.failure'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::ERROR); + } else { + if ($this->isDefaultProvider($mfaProvider)) { + $this->removeDefaultProvider(); + } + $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:deactivate.success'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::OK); + } + return new RedirectResponse($this->getActionUri('overview')); + } + + /** + * Handle unlock request by forwarding the request to the appropriate provider + */ + protected function unlockAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface + { + $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()); + $languageService = $this->getLanguageService(); + if (!$mfaProvider->unlock($request, $propertyManager)) { + $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:unlock.failure'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::ERROR); + } else { + $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:unlock.success'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::OK); + } + return new RedirectResponse($this->getActionUri('overview')); + } + + /** + * Render form to edit a provider by using provider specific content + */ + protected function editAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface + { + $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()); + if ($mfaProvider->isLocked($propertyManager)) { + // Do not show edit view for locked providers + $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerIsLocked'), '', ContextualFeedbackSeverity::ERROR); + return new RedirectResponse($this->getActionUri('overview')); + } + $this->addFormButtons(); + $providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::EDIT); + $this->view->assignMultiple([ + 'provider' => $mfaProvider, + 'providerContent' => $providerResponse->getBody(), + 'isDefaultProvider' => $this->isDefaultProvider($mfaProvider), + ]); + return $this->view->renderResponse('Mfa/Edit'); + } + + /** + * Handle save request, receiving from the edit view by + * forwarding the request to the appropriate provider. + */ + protected function saveAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface + { + $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()); + $languageService = $this->getLanguageService(); + if (!$mfaProvider->update($request, $propertyManager)) { + $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:save.failure'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::ERROR); + } else { + if ($request->getParsedBody()['defaultProvider'] ?? false) { + $this->setDefaultProvider($mfaProvider); + } elseif ($this->isDefaultProvider($mfaProvider)) { + $this->removeDefaultProvider(); + } + $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:save.success'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::OK); + } + if (!$mfaProvider->isActive($propertyManager)) { + return new RedirectResponse($this->getActionUri('overview')); + } + return new RedirectResponse($this->getActionUri('edit', ['identifier' => $mfaProvider->getIdentifier()])); + } + + /** + * Build a uri for the current controller based on the + * given action, respecting additional parameters. + */ + protected function getActionUri(string $action, array $additionalParameters = []): UriInterface + { + if (!$this->isActionAllowed($action)) { + $action = 'overview'; + } + return $this->uriBuilder->buildUriFromRoute('mfa', array_merge(['action' => $action], $additionalParameters)); + } + + /** + * Check if there are more suitable default providers for the current user + */ + protected function hasSuitableDefaultProviders(array $excludedProviders = []): bool + { + foreach ($this->allowedProviders as $identifier => $provider) { + if (!in_array($identifier, $excludedProviders, true) + && $provider->isDefaultProviderAllowed() + && $provider->isActive(MfaProviderPropertyManager::create($provider, $this->getBackendUser())) + ) { + return true; + } + } + return false; + } + + /** + * Get the default provider + */ + protected function getDefaultProviderIdentifier(): string + { + $defaultProviderIdentifier = (string)($this->getBackendUser()->uc['mfa']['defaultProvider'] ?? ''); + // The default provider value is only valid, if the corresponding provider exist and is allowed + if ($this->isValidIdentifier($defaultProviderIdentifier)) { + $defaultProvider = $this->mfaProviderRegistry->getProvider($defaultProviderIdentifier); + $propertyManager = MfaProviderPropertyManager::create($defaultProvider, $this->getBackendUser()); + // Also check if the provider is activated for the user + if ($defaultProvider->isActive($propertyManager)) { + return $defaultProviderIdentifier; + } + } + + // If the stored provider is not valid, clean up the UC + $this->removeDefaultProvider(); + return ''; + } + + /** + * Get the recommended provider + */ + protected function getRecommendedProviderIdentifier(): string + { + $recommendedProvider = $this->getRecommendedProvider(); + if ($recommendedProvider === null) { + return ''; + } + + $propertyManager = MfaProviderPropertyManager::create($recommendedProvider, $this->getBackendUser()); + // If the defined recommended provider is valid, check if it is not yet activated + return !$recommendedProvider->isActive($propertyManager) ? $recommendedProvider->getIdentifier() : ''; + } + + protected function isDefaultProvider(MfaProviderManifestInterface $mfaProvider): bool + { + return $this->getDefaultProviderIdentifier() === $mfaProvider->getIdentifier(); + } + + protected function setDefaultProvider(MfaProviderManifestInterface $mfaProvider): void + { + $this->getBackendUser()->uc['mfa']['defaultProvider'] = $mfaProvider->getIdentifier(); + $this->getBackendUser()->writeUC(); + } + + protected function removeDefaultProvider(): void + { + $this->getBackendUser()->uc['mfa']['defaultProvider'] = ''; + $this->getBackendUser()->writeUC(); + } + + protected function addFlashMessage(string $message, string $title = '', ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::INFO): void + { + $flashMessage = new FlashMessage($message, $title, $severity, true); + $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + + protected function addOverviewButtons(ServerRequestInterface $request): void + { + if (($returnUrl = $this->getReturnUrl($request)) !== '') { + $this->view->addButtonToButtonBar($this->componentFactory->createBackButton($returnUrl)); + } + } + + protected function addFormButtons(): void + { + $closeButton = $this->componentFactory->createCloseButton((string)$this->uriBuilder->buildUriFromRoute('mfa', ['action' => 'overview'])) + ->setClasses('t3js-editform-close'); + $this->view->addButtonToButtonBar($closeButton); + $this->view->addButtonToButtonBar($this->componentFactory->createSaveButton('mfaConfigurationController')->setName('save'), ButtonBar::BUTTON_POSITION_LEFT, 2); + } + + protected function getReturnUrl(ServerRequestInterface $request): string + { + $returnUrl = GeneralUtility::sanitizeLocalUrl( + $request->getQueryParams()['returnUrl'] ?? $request->getParsedBody()['returnUrl'] ?? '', + $request + ); + + if ($returnUrl === '') { + $returnUrl = (string)$this->uriBuilder->buildUriFromRoute('user_setup'); + } + + return $returnUrl; + } +} diff --git a/Classes/Controller/MfaController.php b/Classes/Controller/MfaController.php new file mode 100644 index 0000000..0a261e1 --- /dev/null +++ b/Classes/Controller/MfaController.php @@ -0,0 +1,244 @@ +initializeMfaConfiguration(); + $action = (string)($request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'auth'); + + switch ($action) { + case 'auth': + case 'verify': + $mfaProvider = $this->getMfaProviderFromRequest($request); + // All actions except "cancel" require a provider to deal with. + // If non is found at this point, throw an exception since this should never happen. + if ($mfaProvider === null) { + throw new \InvalidArgumentException('No active MFA provider was found!', 1611879242); + } + return $this->{$action . 'Action'}($request, $mfaProvider); + case 'cancel': + return $this->cancelAction($request); + default: + throw new \InvalidArgumentException('Action not allowed', 1611879244); + } + } + + /** + * Set up the authentication view for the provider by using provider specific content. + */ + protected function authAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface + { + $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $this->getLanguageService()); + $this->pageRenderer->setTitle('TYPO3 CMS Login: ' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '')); + $this->pageRenderer->loadJavaScriptModule('bootstrap'); + $view = $this->backendViewFactory->create($request); + $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()); + $providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::AUTH); + $view->assignMultiple([ + 'provider' => $mfaProvider, + 'alternativeProviders' => $this->getAlternativeProviders($mfaProvider), + 'isLocked' => $mfaProvider->isLocked($propertyManager), + 'providerContent' => $providerResponse->getBody(), + 'footerNote' => $this->authenticationStyleInformation->getFooterNote(), + 'formUrl' => $this->uriBuilder->buildUriWithRedirect('auth_mfa', ['action' => 'verify'], RouteRedirect::createFromRequest($request)), + 'redirectRoute' => $request->getQueryParams()['redirect'] ?? '', + 'redirectParams' => $request->getQueryParams()['redirectParams'] ?? '', + 'hasAuthError' => (bool)($request->getQueryParams()['failure'] ?? false), + ]); + $this->addCustomAuthenticationFormStyles($request); + $this->pageRenderer->setBodyContent('' . $view->render('Mfa/Auth')); + return $this->pageRenderer->renderResponse($request); + } + + /** + * Handle verification request, receiving from the auth view + * by forwarding the request to the appropriate provider. + */ + protected function verifyAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface + { + $backendUser = $this->getBackendUser(); + $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $backendUser); + + // Check if the provider can process the request and is not temporarily blocked + if (!$mfaProvider->canProcess($request) || $mfaProvider->isLocked($propertyManager)) { + // If this fails, cancel the authentication + return $this->cancelAction($request); + } + // Call the provider to verify the request + if (!$mfaProvider->verify($request, $propertyManager)) { + $this->log( + 'Multi-factor authentication failed for user \'###USERNAME###\' with provider \'' . $mfaProvider->getIdentifier() . '\'!', + [], + null, + Login::ATTEMPT, + SystemLogErrorClassification::SECURITY_NOTICE + ); + $this->eventDispatcher->dispatch( + new MfaVerificationFailedEvent($request, $propertyManager, $mfaProvider) + ); + // If failed, initiate a redirect back to the auth view + return new RedirectResponse($this->uriBuilder->buildUriWithRedirect( + 'auth_mfa', + [ + 'identifier' => $mfaProvider->getIdentifier(), + 'failure' => true, + ], + RouteRedirect::createFromRequest($request) + )); + } + $this->log('Multi-factor authentication successful for user ###USERNAME###'); + // If verified, store this information in the session + // and initiate a redirect back to the login view. + $backendUser->setAndSaveSessionData('mfa', true); + $backendUser->handleUserLoggedIn($request); + return new RedirectResponse( + $this->uriBuilder->buildUriWithRedirect('login', [], RouteRedirect::createFromRequest($request)) + ); + } + + /** + * Allow the user to cancel the multi-factor authentication by + * calling logoff on the user object, to destroy the session and + * other already gathered information and finally initiate a + * redirect back to the login. + */ + protected function cancelAction(ServerRequestInterface $request): ResponseInterface + { + $this->log('Multi-factor authentication canceled for user ###USERNAME###'); + $this->getBackendUser()->logoff(); + return new RedirectResponse($this->uriBuilder->buildUriWithRedirect('login', [], RouteRedirect::createFromRequest($request))); + } + + /** + * Fetch alternative (activated and allowed) providers for the user to chose from + * + * @return ProviderInterface[] + */ + protected function getAlternativeProviders(MfaProviderManifestInterface $mfaProvider): array + { + return array_filter($this->allowedProviders, function (MfaProviderManifestInterface $provider) use ($mfaProvider): bool { + return $provider !== $mfaProvider + && $provider->isActive(MfaProviderPropertyManager::create($provider, $this->getBackendUser())); + }); + } + + /** + * Log debug information for MFA events + */ + protected function log( + string $message, + array $additionalData = [], + ?MfaProviderManifestInterface $mfaProvider = null, + int $action = Login::LOGIN, + int $error = SystemLogErrorClassification::MESSAGE + ): void { + $user = $this->getBackendUser(); + $username = $user->getUserName(); + $context = [ + 'user' => [ + 'uid' => $user->getUserId(), + 'username' => $username, + ], + ]; + if ($mfaProvider !== null) { + $context['provider'] = $mfaProvider->getIdentifier(); + $context['isProviderLocked'] = $mfaProvider->isLocked( + MfaProviderPropertyManager::create($mfaProvider, $user) + ); + } + $message = str_replace('###USERNAME###', $username, $message); + $data = array_replace_recursive($context, $additionalData); + $this->logger->debug($message, $data); + if ($user->writeStdLog) { + // Write to sys_log if enabled + $user->writelog(SystemLogType::LOGIN, $action, $error, null, $message, $data); + } + } + + protected function getMfaProviderFromRequest(ServerRequestInterface $request): ?MfaProviderManifestInterface + { + $identifier = (string)($request->getQueryParams()['identifier'] ?? $request->getParsedBody()['identifier'] ?? ''); + // Check if given identifier is valid + if ($this->isValidIdentifier($identifier)) { + $provider = $this->mfaProviderRegistry->getProvider($identifier); + // Only add provider if it was activated by the current user + if ($provider->isActive(MfaProviderPropertyManager::create($provider, $this->getBackendUser()))) { + return $provider; + } + } + return null; + } + + protected function addCustomAuthenticationFormStyles(ServerRequestInterface $request): void + { + if (($backgroundImageStyles = $this->authenticationStyleInformation->getBackgroundImageStyles($request)) !== '') { + $this->pageRenderer->addCssInlineBlock('loginBackgroundImage', $backgroundImageStyles, null, false, true); + } + if (($highlightColorStyles = $this->authenticationStyleInformation->getHighlightColorStyles()) !== '') { + $this->pageRenderer->addCssInlineBlock('loginHighlightColor', $highlightColorStyles, null, false, true); + } + } +} diff --git a/Classes/Controller/MfaSetupController.php b/Classes/Controller/MfaSetupController.php new file mode 100644 index 0000000..7c42b20 --- /dev/null +++ b/Classes/Controller/MfaSetupController.php @@ -0,0 +1,275 @@ + 'GET', + 'activate' => 'POST', + 'cancel' => 'GET', + ]; + + public function __construct( + protected readonly UriBuilder $uriBuilder, + protected readonly AuthenticationStyleInformation $authenticationStyleInformation, + protected readonly PageRenderer $pageRenderer, + protected readonly ExtensionConfiguration $extensionConfiguration, + protected readonly LoggerInterface $logger, + protected readonly BackendViewFactory $backendViewFactory, + protected readonly FlashMessageService $flashMessageService, + ) {} + + public function handleRequest(ServerRequestInterface $request): ResponseInterface + { + $this->initializeMfaConfiguration(); + $action = (string)($request->getQueryParams()['action'] ?? 'setup'); + + $backendUser = $this->getBackendUser(); + if (($backendUser->getSessionData('mfa') ?? false) + || $backendUser->getOriginalUserIdWhenInSwitchUserMode() !== null + || !$backendUser->isMfaSetupRequired() + || $this->mfaProviderRegistry->hasActiveProviders($backendUser) + ) { + // Since the current user either did already pass MFA, is in "switch-user" mode, + // is not required to set up MFA or has already activated a provider, throw an + // exception to prevent the endpoint from being called unintentionally by custom code. + throw new \InvalidArgumentException('MFA setup is not necessary. Do not call this endpoint on your own.', 1632154036); + } + + $actionMethod = self::ACTION_METHOD_MAP[$action] ?? null; + if ($actionMethod !== null && $request->getMethod() === $actionMethod) { + return $this->{$action . 'Action'}($request); + } + return new HtmlResponse('', 404); + } + + /** + * Render form to setup a provider by using provider specific content. Fall + * back to provider selection view, in case no valid provider was yet selected. + */ + protected function setupAction(ServerRequestInterface $request): ResponseInterface + { + $identifier = (string)($request->getQueryParams()['identifier'] ?? ''); + if ($identifier === '' || !$this->isValidIdentifier($identifier)) { + return $this->renderSelectionView($request); + } + $mfaProvider = $this->mfaProviderRegistry->getProvider($identifier); + $this->log('Required MFA setup initiated', $mfaProvider); + return $this->renderSetupView($request, $mfaProvider); + } + + /** + * Handle activate request, receiving from the setup view + * by forwarding the request to the appropriate provider. + */ + protected function activateAction(ServerRequestInterface $request): ResponseInterface + { + $identifier = (string)($request->getParsedBody()['identifier'] ?? ''); + if ($identifier === '' || !$this->isValidIdentifier($identifier)) { + // Return to selection view in case no valid identifier is given + return new RedirectResponse($this->uriBuilder->buildUriWithRedirect('setup_mfa', [], RouteRedirect::createFromRequest($request))); + } + $mfaProvider = $this->mfaProviderRegistry->getProvider($identifier); + $backendUser = $this->getBackendUser(); + $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $backendUser); + // Check whether activation operation was successful and the provider is now active. + if (!$mfaProvider->activate($request, $propertyManager) || !$mfaProvider->isActive($propertyManager)) { + $this->log('Required MFA setup failed', $mfaProvider); + return new RedirectResponse( + $this->uriBuilder->buildUriWithRedirect( + 'setup_mfa', + [ + 'identifier' => $mfaProvider->getIdentifier(), + 'hasErrors' => true, + ], + RouteRedirect::createFromRequest($request) + ) + ); + } + $this->log('Required MFA setup successful', $mfaProvider); + // Set the activated provider as the default provider, store the "mfa" key in the session data, + // add a flash message to the session and finally initiate a redirect to the login, on which + // possible redirect parameters are evaluated again. + $backendUser->uc['mfa']['defaultProvider'] = $mfaProvider->getIdentifier(); + $backendUser->writeUC(); + $backendUser->setAndSaveSessionData('mfa', true); + $this->addSuccessMessage($mfaProvider->getTitle()); + return new RedirectResponse($this->uriBuilder->buildUriWithRedirect('login', [], RouteRedirect::createFromRequest($request))); + } + + /** + * Allow the user to cancel the multi-factor authentication setup process + * by calling logoff on the user object, to destroy the session and other + * already gathered information and finally initiate a redirect back to the login. + */ + protected function cancelAction(ServerRequestInterface $request): ResponseInterface + { + $this->log('Required MFA setup canceled'); + $this->getBackendUser()->logoff(); + return new RedirectResponse($this->uriBuilder->buildUriWithRedirect('login', [], RouteRedirect::createFromRequest($request))); + } + + /** + * Allow the user - required to set up MFA - to select between all available providers + */ + protected function renderSelectionView(ServerRequestInterface $request): ResponseInterface + { + $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $this->getLanguageService()); + $this->pageRenderer->setTitle('TYPO3 CMS Login: ' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '')); + $this->pageRenderer->loadJavaScriptModule('bootstrap'); + + $recommendedProvider = $this->getRecommendedProvider(); + $providers = array_filter($this->allowedProviders, static function (MfaProviderManifestInterface $provider) use ($recommendedProvider): bool { + // Remove the recommended provider and providers, which can not be used as default, e.g. recovery codes + return $provider->isDefaultProviderAllowed() + && ($recommendedProvider === null || $provider->getIdentifier() !== $recommendedProvider->getIdentifier()); + }); + $view = $this->initializeView($request); + $view->assignMultiple([ + 'recommendedProvider' => $recommendedProvider, + 'providers' => $providers, + ]); + $this->pageRenderer->setBodyContent('' . $view->render('Mfa/Standalone/Selection')); + return $this->pageRenderer->renderResponse($request); + } + + /** + * Render form to setup a provider by using provider specific content + */ + protected function renderSetupView( + ServerRequestInterface $request, + MfaProviderManifestInterface $mfaProvider + ): ResponseInterface { + $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $this->getLanguageService()); + $this->pageRenderer->setTitle('TYPO3 CMS Login: ' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '')); + $this->pageRenderer->loadJavaScriptModule('bootstrap'); + + $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()); + $providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::SETUP); + $view = $this->initializeView($request); + $view->assignMultiple([ + 'provider' => $mfaProvider, + 'providerContent' => $providerResponse->getBody(), + 'hasErrors' => (bool)($request->getQueryParams()['hasErrors'] ?? false), + ]); + $this->pageRenderer->setBodyContent('' . $view->render('Mfa/Standalone/Setup')); + return $this->pageRenderer->renderResponse($request); + } + + /** + * Initialize the standalone view by setting the paths and assigning view variables + */ + protected function initializeView(ServerRequestInterface $request): ViewInterface + { + $view = $this->backendViewFactory->create($request); + $view->assignMultiple([ + 'redirect' => $request->getQueryParams()['redirect'] ?? '', + 'redirectParams' => $request->getQueryParams()['redirectParams'] ?? '', + 'siteName' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], + 'footerNote' => $this->authenticationStyleInformation->getFooterNote(), + ]); + $this->addCustomAuthenticationFormStyles($request); + return $view; + } + + protected function addCustomAuthenticationFormStyles(ServerRequestInterface $request): void + { + if (($backgroundImageStyles = $this->authenticationStyleInformation->getBackgroundImageStyles($request)) !== '') { + $this->pageRenderer->addCssInlineBlock('loginBackgroundImage', $backgroundImageStyles, null, false, true); + } + if (($highlightColorStyles = $this->authenticationStyleInformation->getHighlightColorStyles()) !== '') { + $this->pageRenderer->addCssInlineBlock('loginHighlightColor', $highlightColorStyles, null, false, true); + } + } + + /** + * Extend base identifier check to further evaluate whether + * the provider is allowed to be a default provider. + */ + protected function isValidIdentifier(string $identifier): bool + { + return parent::isValidIdentifier($identifier) + && $this->mfaProviderRegistry->getProvider($identifier)->isDefaultProviderAllowed(); + } + + /** + * Add a flash message to inform the user about the successful activation of MFA and + * store this in the session, so it will be shown in the backend after the redirect. + */ + protected function addSuccessMessage(string $mfaProviderTitle): void + { + $lang = $this->getLanguageService(); + $this->flashMessageService->getMessageQueueByIdentifier()->enqueue( + new FlashMessage( + sprintf($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:standalone.setup.success.message'), $lang->sL($mfaProviderTitle)), + $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:standalone.setup.success.title'), + ContextualFeedbackSeverity::OK, + true + ) + ); + } + + /** + * Log debug information for MFA setup events + */ + protected function log(string $message, ?MfaProviderManifestInterface $mfaProvider = null): void + { + $user = $this->getBackendUser(); + $context = [ + 'user' => [ + 'uid' => $user->getUserId(), + 'username' => $user->getUserName(), + ], + ]; + if ($mfaProvider !== null) { + $context['provider'] = $mfaProvider->getIdentifier(); + } + $this->logger->debug($message, $context); + } +} diff --git a/Classes/Controller/NewRecordController.php b/Classes/Controller/NewRecordController.php new file mode 100644 index 0000000..0da3269 --- /dev/null +++ b/Classes/Controller/NewRecordController.php @@ -0,0 +1,513 @@ +moduleTemplateFactory->create($request); + $pageinfo = []; + $pidInfo = []; + $newPagesInto = false; + $newContentInto = false; + $newPagesAfter = false; + $tRows = []; + $beUser = $this->getBackendUserAuthentication(); + // Page-selection permission clause (reading) + $permsClause = $beUser->getPagePermsClause(Permission::PAGE_SHOW); + // This will hide records from display - it has nothing to do with user rights!! + $pidList = (string)($beUser->getTSConfig()['options.']['hideRecords.']['pages'] ?? ''); + if (!empty($pidList)) { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); + $permsClause .= ' AND ' . $queryBuilder->expr()->notIn( + 'pages.uid', + GeneralUtility::intExplode(',', $pidList) + ); + } + // Setting GPvars: + $parsedBody = $request->getParsedBody(); + $queryParams = $request->getQueryParams(); + // The page id to operate from + $pageUid = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0); + $returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '', $request); + // Setting up the context sensitive menu: + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/context-menu.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/new-content-element-wizard-button.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/page-wizard/new-page-wizard-button.js'); + // If a positive id is supplied, ask for the page record with permission information contained: + if ($pageUid > 0) { + $pageinfo = BackendUtility::readPageAccess($pageUid, $permsClause) ?: []; + } + // If a page-record was returned, the user had read-access to the page. + if ($pageinfo['uid'] ?? false) { + // Get record of parent page + $pidInfo = BackendUtility::getRecord('pages', ($pageinfo['pid'] ?? 0)) ?? []; + // Checking the permissions for the user with regard to the parent page: Can he create new pages, new + // content record, new page after? + if ($beUser->doesUserHaveAccess($pageinfo, Permission::PAGE_NEW)) { + $newPagesInto = true; + } + if ($beUser->doesUserHaveAccess($pageinfo, Permission::CONTENT_EDIT)) { + $newContentInto = true; + } + if (($beUser->isAdmin() || !empty($pidInfo)) && $beUser->doesUserHaveAccess($pidInfo, Permission::PAGE_NEW)) { + $newPagesAfter = true; + } + $breadcrumbContext = new BreadcrumbContext( + $this->recordFactory->createResolvedRecordFromDatabaseRow('pages', $pageinfo), + [] + ); + $view->getDocHeaderComponent()->setBreadcrumbContext($breadcrumbContext); + } elseif ($beUser->isAdmin()) { + // Admins can do it all + $newPagesInto = true; + $newContentInto = true; + $newPagesAfter = false; + } else { + // People with no permission can do nothing + $newPagesInto = false; + $newContentInto = false; + $newPagesAfter = false; + } + $title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']; + if ($pageinfo['uid'] ?? false) { + $title = strip_tags(BackendUtility::getRecordTitle('pages', $pageinfo)); + } + $view->setTitle($title); + // Acquiring TSconfig for this module/current page: + $web_list_modTSconfig = BackendUtility::getPagesTSconfig($pageinfo['uid'] ?? 0)['mod.']['web_list.'] ?? []; + $allowedNewTables = GeneralUtility::trimExplode(',', $web_list_modTSconfig['allowedNewTables'] ?? '', true); + $deniedNewTables = GeneralUtility::trimExplode(',', $web_list_modTSconfig['deniedNewTables'] ?? '', true); + // Acquiring TSconfig for this module/parent page + $web_list_modTSconfig_pid = BackendUtility::getPagesTSconfig($pageinfo['pid'] ?? 0)['mod.']['web_list.'] ?? []; + $allowedNewTables_pid = GeneralUtility::trimExplode(',', $web_list_modTSconfig_pid['allowedNewTables'] ?? '', true); + $deniedNewTables_pid = GeneralUtility::trimExplode(',', $web_list_modTSconfig_pid['deniedNewTables'] ?? '', true); + if (!$this->isRecordCreationAllowedForTable('pages', $allowedNewTables, $deniedNewTables)) { + $newPagesInto = false; + } + if (!$this->isRecordCreationAllowedForTable('pages', $allowedNewTables_pid, $deniedNewTables_pid)) { + $newPagesAfter = false; + } + + // If there was a page - or if the user is admin (admins has access to the root) we proceed, otherwise just output the header + if (empty($pageinfo['uid']) && !$this->getBackendUserAuthentication()->isAdmin()) { + return $view->renderResponse('NewRecord/NewRecord'); + } + + $lang = $this->getLanguageService(); + // Get TSconfig for current page + $pageTS = BackendUtility::getPagesTSconfig($pageUid); + // Finish initializing new pages options with TSconfig + // Each new page option may be hidden by TSconfig + $displayNewPagesIntoLink = $newPagesInto && !empty($pageTS['mod.']['wizards.']['newRecord.']['pages.']['show.']['pageInside']); + $displayNewPagesAfterLink = $newPagesAfter && !empty($pageTS['mod.']['wizards.']['newRecord.']['pages.']['show.']['pageAfter']); + $iconFile = [ + 'backendaccess' => $this->iconFactory->getIcon('status-user-group-backend', IconSize::SMALL), + 'content' => $this->iconFactory->getIcon('content-panel', IconSize::SMALL)->render(), + 'frontendaccess' => $this->iconFactory->getIcon('status-user-group-frontend', IconSize::SMALL), + 'system' => $this->iconFactory->getIcon('apps-pagetree-root', IconSize::SMALL), + ]; + $groupTitles = [ + 'backendaccess' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:recordgroup.backendaccess'), + 'content' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:recordgroup.content'), + 'frontendaccess' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:recordgroup.frontendaccess'), + 'system' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:system_records'), + ]; + $allowedTables = []; + foreach ($this->tcaSchemaFactory->all() as $table => $schema) { + $isTablesAllowed = match ($table) { + 'pages' => $this->isRecordCreationAllowedForTable('pages', $allowedNewTables, $deniedNewTables), + 'tt_content' => false, // Skip, as inserting content elements is part of the page module + default => $newContentInto && $this->isRecordCreationAllowedForTable($table, $allowedNewTables, $deniedNewTables) && $this->isTableAllowedOnPage($schema, $pageinfo, $pageUid) + }; + + if ($isTablesAllowed) { + $allowedTables[] = $table; + } + } + $groupedLinksOnTop = []; + foreach ($allowedTables as $table) { + $schema = $this->tcaSchemaFactory->get($table); + $ctrlTitle = $schema->getTitle(); + + if ($table === 'pages') { + // New pages INSIDE this pages + $newPageLinks = []; + if ($displayNewPagesIntoLink && $this->isTableAllowedOnPage($schema, $pageinfo, $pageUid)) { + // Create link to new page inside + $newPageLinks['inside'] = [ + 'icon' => $this->iconFactory->getIconForRecord($table, [], IconSize::SMALL), + 'label' => $lang->sL($ctrlTitle) . ' (' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:db_new.php.inside') . ')', + 'wizardConfiguration' => ['positionData' => ['pageUid' => $pageUid, 'insertPosition' => 'inside']], + ]; + } + // New pages AFTER this pages + if ($displayNewPagesAfterLink && $this->isTableAllowedOnPage($schema, $pidInfo, $pageUid)) { + $newPageLinks['after'] = [ + 'icon' => $this->iconFactory->getIconForRecord($table, [], IconSize::SMALL), + 'label' => $lang->sL($ctrlTitle) . ' (' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:db_new.php.after') . ')', + 'wizardConfiguration' => ['positionData' => ['pageUid' => $pageUid, 'insertPosition' => 'after']], + ]; + } + + if (!empty($newPageLinks)) { + $groupedLinksOnTop['pages'] = [ + 'title' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:createNewPage'), + 'icon' => $this->iconFactory->getIcon('actions-page-new', IconSize::SMALL), + 'items' => $newPageLinks, + ]; + } + } else { + $nameParts = explode('_', $table); + $groupName = $schema->getRawConfiguration()['groupName'] ?? ''; + if (!isset($iconFile[$groupName]) || $nameParts[0] === 'tx' || $nameParts[0] === 'tt') { + $groupName = $groupName ?: ($nameParts[1] ?? null); + // Try to extract extension name + if ($groupName) { + $_EXTKEY = ''; + $titleIsTranslatableLabel = str_starts_with($ctrlTitle, 'LLL:EXT:'); + if ($titleIsTranslatableLabel) { + // In case the title is a locallang reference, we can simply + // extract the extension name from the given extension path. + $_EXTKEY = substr($ctrlTitle, 8); + $_EXTKEY = substr($_EXTKEY, 0, (int)strpos($_EXTKEY, '/')); + } elseif (ExtensionManagementUtility::isLoaded($groupName)) { + // In case $title is not a locallang reference, we check the groupName to + // be a valid extension key. This most probably work since by convention the + // first part after tx_ / tt_ is the extension key. + $_EXTKEY = $groupName; + } + // Fetch the group title from the extension name + if ($_EXTKEY !== '') { + // Try to get the extension title + $package = $this->packageManager->getPackage($_EXTKEY); + $groupTitle = $lang->sL('LLL:EXT:' . $_EXTKEY . '/Resources/Private/Language/locallang_db.xlf:extension.title'); + // If no localisation available, read title from the Package MetaData + if (!$groupTitle) { + $groupTitle = $package->getPackageMetaData()->getTitle(); + } + $extensionIcon = $package->getResources()->getPackageIcon(); + if ($extensionIcon !== null) { + $iconResource = $this->resourceFactory->createPublicResource($extensionIcon); + $iconFile[$groupName] = '' . $groupTitle . ''; + } + if (!empty($groupTitle)) { + $groupTitles[$groupName] = $groupTitle; + } else { + $groupTitles[$groupName] = ucwords($_EXTKEY); + } + } + } else { + // Fall back to "system" in case no $groupName could be found + $groupName = 'system'; + } + } + $tRows[$groupName]['title'] = $tRows[$groupName]['title'] ?? $groupTitles[$groupName] ?? $nameParts[1] ?? $ctrlTitle; + $tRows[$groupName]['icon'] = $tRows[$groupName]['icon'] ?? $iconFile[$groupName] ?? $iconFile['system'] ?? ''; + if ($schema->supportsSubSchema() + && !$schema->getSubSchemaTypeInformation()->isPointerToForeignFieldInForeignSchema() + && $this->hasRecordTypesForDirectCreation($schema) + ) { + $tRows[$groupName]['items'][$table]['label'] = $lang->sL($ctrlTitle); + $tRows[$groupName]['items'][$table]['icon'] = $this->iconFactory->getIconForRecord($table, [], IconSize::SMALL); + $tRows[$groupName]['items'][$table]['types'] = $this->getRecordTypesForDirectCreation($request, $schema, $pageUid, $returnUrl); + } else { + $tRows[$groupName]['items'][$table] = [ + 'url' => $this->renderLink($request, $table, $pageUid, [], $returnUrl), + 'icon' => $this->iconFactory->getIconForRecord($table, [], IconSize::SMALL)->render(), + 'label' => $lang->sL($ctrlTitle), + ]; + } + } + } + // User sort + $newRecordSortList = isset($pageTS['mod.']['wizards.']['newRecord.']['order']) + ? GeneralUtility::trimExplode(',', $pageTS['mod.']['wizards.']['newRecord.']['order'], true) + : []; + uksort($tRows, static function (string $a, string $b) use ($newRecordSortList, $tRows): int { + if ($newRecordSortList !== []) { + if (in_array($a, $newRecordSortList) && in_array($b, $newRecordSortList)) { + // Both are in the list, return relative to position in array + $sub = array_search($a, $newRecordSortList) - array_search($b, $newRecordSortList); + $ret = ($sub < 0 ? -1 : $sub == 0) ? 0 : 1; + } elseif (in_array($a, $newRecordSortList)) { + // First element is in array, put to top + $ret = -1; + } elseif (in_array($b, $newRecordSortList)) { + // Second element is in array, put first to bottom + $ret = 1; + } else { + // No element is in array, return alphabetic order + $ret = strnatcasecmp($tRows[$a]['title'] ?? '', $tRows[$b]['title'] ?? ''); + } + return $ret; + } + // Return alphabetic order + return strnatcasecmp($tRows[$a]['title'] ?? '', $tRows[$b]['title'] ?? ''); + }); + $tRows = array_merge($groupedLinksOnTop, $tRows); + + $tRows = $this->eventDispatcher->dispatch( + new ModifyNewRecordCreationLinksEvent($tRows, $pageTS, $pageUid, $request) + )->groupedCreationLinks; + + $recordControls = $tRows; + + if (count($recordControls) === 1) { + $items = current($recordControls)['items'] ?? []; + if (count($items) === 1) { + $item = current($items); + // Items for tables with sub-types carry a 'types' sub-array instead of a 'url' + // and must fall through to render the selection wizard. + if (isset($item['url'])) { + return new RedirectResponse($item['url'], 301); + } + } + } + + $view->assign('recordTypeGroups', $recordControls); + + // Setting up the buttons and markers for docheader (done after permissions are checked) + // Back + if ($returnUrl) { + $view->addButtonToButtonBar($this->componentFactory->createBackButton($returnUrl), ButtonBar::BUTTON_POSITION_LEFT, 10); + } + if ($pageinfo['uid'] ?? false) { + // View + $previewUriBuilder = PreviewUriBuilder::create($pageinfo); + if ($previewUriBuilder->isPreviewable()) { + $view->addButtonToButtonBar( + $this->componentFactory->createViewButton($previewUriBuilder + ->withRootLine(BackendUtility::BEgetRootLine($pageinfo['uid'])) + ->buildDispatcherDataAttributes() ?? []), + ButtonBar::BUTTON_POSITION_LEFT, + 30 + ); + } + } + return $view->renderResponse('NewRecord/NewRecord'); + } + + /** + * Links the string $code to a create-new form for a record in $table created on page $pid + * + * @param string $table Table name (in which to create new record) + * @param int $pid PID value for the "&edit['.$table.']['.$pid.']=new" command (positive/negative) + * @param array $additionalParams Additional params, such as "defVals" tp be added to the link + * @param string $returnUrl Return URL, falls back to the current request URI when empty + * @return string The link. + */ + private function renderLink(ServerRequestInterface $request, string $table, int $pid, array $additionalParams, string $returnUrl): string + { + $params = [ + 'edit' => [ + $table => [ + $pid => 'new', + ], + ], + 'returnUrl' => $returnUrl ?: $request->getAttribute('normalizedParams')->getRequestUri(), + ]; + + if ($additionalParams) { + $params = array_replace_recursive($params, $additionalParams); + } + + return (string)$this->uriBuilder->buildUriFromRoute('record_edit', $params); + } + + /** + * Returns TRUE if the tablename $checkTable is allowed to be created on the page with record $pid_row + * + * @param TcaSchema $schema Table schema + * @param array $page Potential parent page + * @param int $pageUid Current page id + * @return bool Returns TRUE if the tablename $table is allowed to be created on the $page + */ + private function isTableAllowedOnPage(TcaSchema $schema, array $page, int $pageUid): bool + { + $rootLevelCapability = $schema->getCapability(TcaSchemaCapability::RestrictionRootLevel); + + $rootLevelConstraintMatches = ($rootLevelCapability->canExistOnRootLevel() && $pageUid === 0) || ($pageUid && $rootLevelCapability->canExistOnPages()); + if (empty($page)) { + return $rootLevelConstraintMatches && $this->getBackendUserAuthentication()->isAdmin(); + } + if (!$this->getBackendUserAuthentication()->workspaceCanCreateNewRecord($schema->getName())) { + return false; + } + // Checking doktype + $isAllowed = $this->pageDoktypeRegistry->isRecordTypeAllowedForDoktype($schema->getName(), $page['doktype']); + return $rootLevelConstraintMatches && $isAllowed; + } + + /** + * Returns whether the record link should be shown for a table + * + * Returns TRUE if: + * - $allowedNewTables and $deniedNewTables are empty + * - the table is not found in $deniedNewTables and $allowedNewTables is not set or the $table tablename is found in + * $allowedNewTables + * + * If $table tablename is found in $allowedNewTables and $deniedNewTables, + * $deniedNewTables has priority over $allowedNewTables. + * + * @param string $table Table name to test if in allowedTables + * @param array $allowedNewTables Array of new tables that are allowed. + * @param array $deniedNewTables Array of new tables that are not allowed. + * @return bool Returns TRUE if a link for creating new records should be displayed for $table + */ + private function isRecordCreationAllowedForTable(string $table, array $allowedNewTables, array $deniedNewTables): bool + { + if (!$this->getBackendUserAuthentication()->check('tables_modify', $table)) { + return false; + } + + $schema = $this->tcaSchemaFactory->get($table); + + if ($schema->hasCapability(TcaSchemaCapability::AccessReadOnly) + || $schema->hasCapability(TcaSchemaCapability::HideInUi) + || ($schema->hasCapability(TcaSchemaCapability::AccessAdminOnly) && !$this->getBackendUserAuthentication()->isAdmin()) + ) { + return false; + } + + // No deny/allow tables are set: + if (empty($allowedNewTables) && empty($deniedNewTables)) { + return true; + } + + return !in_array($table, $deniedNewTables) && (empty($allowedNewTables) || in_array($table, $allowedNewTables)); + } + + private function hasRecordTypesForDirectCreation(TcaSchema $schema): bool + { + if (count($schema->getSubSchemata()) <= 1) { + return false; + } + foreach ($schema->getSubSchemata() as $subSchema) { + if ((bool)($subSchema->getRawConfiguration()['creationOptions']['enableDirectRecordTypeCreation'] ?? true) === false) { + continue; + } + return true; + } + return false; + } + + private function getRecordTypesForDirectCreation(ServerRequestInterface $request, TcaSchema $schema, int $pageUid, string $returnUrl): array + { + $recordTypes = []; + $lang = $this->getLanguageService(); + $recordTypeField = $schema->getSubSchemaTypeInformation()->getFieldName(); + foreach ($schema->getSubSchemata() as $subSchema) { + $creationOptions = $subSchema->getRawConfiguration()['creationOptions'] ?? []; + if ((bool)($creationOptions['enableDirectRecordTypeCreation'] ?? true) === false) { + continue; + } + $recordTypeName = array_map(trim(...), explode('.', $subSchema->getName(), 2))[1] ?? ''; + $recordTypes[$recordTypeName] = [ + 'url' => $this->renderLink($request, $schema->getName(), $pageUid, [ + 'defVals' => [ + $schema->getName() => [ + $recordTypeField => $recordTypeName, + ], + ], + ], $returnUrl), + 'icon' => $this->iconFactory->getIconForRecord($schema->getName(), [$recordTypeField => $recordTypeName], IconSize::SMALL), + 'label' => $lang->sL($this->schemaLabelResolver->getLabelForFieldValue( + $schema->getName(), + $recordTypeField, + $recordTypeName, + [], + BackendUtility::getPagesTSconfig($pageUid)['TCEFORM.'][$schema->getName() . '.'][$recordTypeField . '.'] ?? [], + )) ?: $lang->sL($subSchema->getTitle()) + ?: $recordTypeName, + ]; + } + return $recordTypes; + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + private function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/OnlineMediaController.php b/Classes/Controller/OnlineMediaController.php new file mode 100644 index 0000000..19c89a5 --- /dev/null +++ b/Classes/Controller/OnlineMediaController.php @@ -0,0 +1,179 @@ +getParsedBody()['url']; + $targetFolderIdentifier = $request->getParsedBody()['targetFolder']; + $allowedExtensions = GeneralUtility::trimExplode(',', $request->getParsedBody()['allowed'] ?: ''); + + if (!empty($url)) { + $data = []; + try { + $file = $this->addMediaFromUrl($url, $targetFolderIdentifier, $allowedExtensions); + } catch (OnlineMediaAlreadyExistsException $e) { + // Ignore this exception since the endpoint is called e.g. in inline context, where the + // folder is not relevant and the same asset can be attached to a record multiple times. + $file = $e->getOnlineMedia(); + } + if ($file !== null) { + $data['file'] = $file->getUid(); + } else { + $data['error'] = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.error.invalid_url'); + } + return new JsonResponse($data); + } + return new JsonResponse(); + } + + /** + * Process add media request, and redirects to the previous page + * + * @throws \RuntimeException + */ + public function mainAction(ServerRequestInterface $request): ResponseInterface + { + $files = $request->getParsedBody()['data']; + $redirect = $request->getParsedBody()['redirect']; + $newMedia = []; + if (isset($files['newMedia'])) { + $newMedia = (array)$files['newMedia']; + } + + foreach ($newMedia as $media) { + if (!empty($media['url']) && !empty($media['target'])) { + $allowed = !empty($media['allowed']) ? GeneralUtility::trimExplode(',', $media['allowed']) : []; + try { + $file = $this->addMediaFromUrl($media['url'], $media['target'], $allowed); + if ($file !== null) { + $flashMessage = new FlashMessage( + $file->getName(), + $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.added'), + ContextualFeedbackSeverity::OK, + true + ); + } else { + $flashMessage = new FlashMessage( + $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.error.invalid_url'), + $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.error.new_media.failed'), + ContextualFeedbackSeverity::ERROR, + true + ); + } + } catch (OnlineMediaAlreadyExistsException $e) { + $flashMessage = new FlashMessage( + sprintf( + $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.error.already_exists'), + $e->getOnlineMedia()->getName() + ), + $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.error.new_media.failed'), + ContextualFeedbackSeverity::WARNING, + true + ); + } + $this->addFlashMessage($flashMessage); + if (empty($redirect) && $media['redirect']) { + $redirect = $media['redirect']; + } + } + } + + $redirect = GeneralUtility::sanitizeLocalUrl($redirect, $request); + if ($redirect) { + return new RedirectResponse($redirect, 303); + } + + throw new \RuntimeException('No redirect after uploading a media found, probably a mis-use of the template not sending the proper Return URL.', 1511945040); + } + + /** + * @param string $url + * @param string $targetFolderIdentifier + * @param string[] $allowedExtensions + * @return File|null + */ + protected function addMediaFromUrl($url, $targetFolderIdentifier, array $allowedExtensions = []) + { + $targetFolder = null; + if ($targetFolderIdentifier) { + try { + $targetFolder = $this->resourceFactory->getFolderObjectFromCombinedIdentifier($targetFolderIdentifier); + } catch (\Exception $e) { + $targetFolder = null; + } + } + if ($targetFolder === null) { + $targetFolder = $this->uploadFolderResolver->resolve($this->getBackendUser()); + } + return $this->onlineMediaHelperRegistry->transformUrlToFile($url, $targetFolder, $allowedExtensions); + } + + /** + * Add flash message to message queue + */ + protected function addFlashMessage(FlashMessage $flashMessage): void + { + $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/Page/MovePageController.php b/Classes/Controller/Page/MovePageController.php new file mode 100644 index 0000000..a9d86a5 --- /dev/null +++ b/Classes/Controller/Page/MovePageController.php @@ -0,0 +1,218 @@ +setUpBasicPageRendererForBackend( + $this->pageRenderer, + $this->extensionConfiguration, + $request, + $this->languageServiceFactory->createFromUserPreferences($this->getBackendUser()) + ); + $view = $this->backendViewFactory->create($request); + $queryParams = $request->getQueryParams(); + $contentOnly = $queryParams['contentOnly'] ?? false; + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/tree/page-browser.js'); + $this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/wizard/move-page.js', 'MovePage')->instance() + ); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_misc.xlf'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/Wizards/move_page.xlf'); + + $targetPid = (int)($queryParams['expandPage'] ?? 0); + $pageIdToMove = (int)($queryParams['uid'] ?? 0); + $makeCopy = (bool)($queryParams['makeCopy'] ?? 0); + + if ($targetPid) { + $view->assignMultiple($this->getContentVariables($pageIdToMove, $targetPid)); + } + $view->assignMultiple([ + 'activePage' => $targetPid, + 'contentOnly' => $contentOnly, + // Make-copy checkbox (clicking this will reload the page with the GET var makeCopy set differently): + 'makeCopyChecked' => $makeCopy, + 'makeCopyUrl' => $this->uriBuilder->buildUriFromRoute( + 'move_page', + [ + 'uid' => $pageIdToMove, + 'makeCopy' => !$makeCopy, + ] + ), + ]); + + $content = $view->render('Page/MovePage'); + if ($contentOnly) { + return new HtmlResponse($content); + } + $this->pageRenderer->setBodyContent('' . $content); + return new HtmlResponse($this->pageRenderer->render($request)); + } + + private function getContentVariables(int $pageIdToMove, int $targetPid): array + { + $elementRow = BackendUtility::getRecordWSOL('pages', $pageIdToMove); + $targetRow = BackendUtility::getRecordWSOL('pages', $targetPid); + if (!$this->getBackendUser()->doesUserHaveAccess($targetRow, Permission::PAGE_EDIT)) { + return []; + } + return [ + 'targetHasSubpages' => $this->pageHasSubpages($targetPid), + 'element' => [ + 'record' => $elementRow, + 'recordTooltip' => BackendUtility::getRecordIconAltText($elementRow, 'pages', false), + 'recordTitle' => BackendUtility::getRecordTitle('pages', $elementRow), + 'recordPath' => BackendUtility::getRecordPath($pageIdToMove, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW), 0), + ], + 'target' => [ + 'record' => $targetRow, + 'recordTooltip' => BackendUtility::getRecordIconAltText($targetRow, 'pages', false), + 'recordTitle' => BackendUtility::getRecordTitle('pages', $targetRow), + 'recordPath' => BackendUtility::getRecordPath($targetPid, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW), 0), + ], + 'positions' => [ + 'above' => $this->getTargetForAboveInsert($targetRow), + 'inside' => $targetRow['uid'], + 'below' => $targetRow['uid'] * -1, + ], + 'hasEditPermissions' => $this->getBackendUser()->doesUserHaveAccess($elementRow, Permission::PAGE_EDIT), + 'isDifferentPage' => $pageIdToMove !== $targetRow['uid'], + ]; + } + + private function getTargetForAboveInsert(array $targetRow): int + { + $targetPageId = (int)$targetRow['uid']; + $subpages = $this->getSubpagesForPageId($targetRow['pid']); + if (in_array($targetPageId, $subpages, true)) { + // Set pointer in array to $targetPid + while (current($subpages) !== $targetPageId) { + if (next($subpages) === false) { + // We reached the end of the array and couldn't find the target pid (how?). Fall back to pid + return (int)$targetRow['pid']; + } + } + $previousItem = prev($subpages); + if ($previousItem !== false) { + return $previousItem * -1; + } + } + + return (int)$targetRow['pid']; + } + + private function getSubpagesForPageId(int $pageId): array + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); + $queryBuilder + ->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)); + + return $queryBuilder + ->select('uid') + ->from('pages') + ->where( + $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId)), + $queryBuilder->expr()->or( + $queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter(\Local\Multilanguage\Service\DefaultLanguageTagService::getTag())), + $queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter('')), + ), + QueryHelper::stripLogicalOperatorPrefix( + $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW) + ) + ) + ->orderBy('sorting') + ->executeQuery() + ->fetchFirstColumn(); + } + + private function pageHasSubpages(int $pageId): bool + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); + $queryBuilder + ->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)); + + $count = (int)$queryBuilder + ->count('uid') + ->from('pages') + ->where( + $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId)), + $queryBuilder->expr()->or( + $queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter(\Local\Multilanguage\Service\DefaultLanguageTagService::getTag())), + $queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter('')), + ), + QueryHelper::stripLogicalOperatorPrefix( + $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW) + ) + ) + ->executeQuery() + ->fetchOne(); + + return $count > 0; + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/Page/NewMultiplePagesController.php b/Classes/Controller/Page/NewMultiplePagesController.php new file mode 100644 index 0000000..11d1d90 --- /dev/null +++ b/Classes/Controller/Page/NewMultiplePagesController.php @@ -0,0 +1,292 @@ +moduleTemplateFactory->create($request); + $backendUser = $this->getBackendUser(); + $pageUid = (int)$request->getQueryParams()['id']; + + // Show only if there is a valid page and if this page may be viewed by the user + $pageRecord = BackendUtility::readPageAccess($pageUid, $backendUser->getPagePermsClause(Permission::PAGE_SHOW)); + if (!is_array($pageRecord)) { + // User has no permission on parent page, should not happen, just render an empty page + return $view->renderResponse('Dummy/Index'); + } + + // Doc header handling + $view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord); + $view->addButtonToButtonBar( + $this->componentFactory->createViewButton( + PreviewUriBuilder::create($pageRecord) + ->withRootLine(BackendUtility::BEgetRootLine($pageUid)) + ->buildDispatcherDataAttributes() ?? [] + ) + ); + + $calculatedPermissions = new Permission($backendUser->calcPerms($pageRecord)); + $canCreateNew = $backendUser->isAdmin() || $calculatedPermissions->createPagePermissionIsGranted(); + + $view->assignMultiple([ + 'canCreateNew' => $canCreateNew, + 'maxTitleLength' => $backendUser->uc['titleLen'] ?? 20, + 'pageUid' => $pageUid, + ]); + + if ($canCreateNew) { + $newPagesData = (array)($request->getParsedBody()['pages'] ?? []); + if (!empty($newPagesData)) { + $hasNewPagesData = true; + $afterExisting = isset($request->getParsedBody()['createInListEnd']); + $hidePages = isset($request->getParsedBody()['hidePages']); + $hidePagesInMenu = isset($request->getParsedBody()['hidePagesInMenus']); + $pagesCreated = $this->createPages($newPagesData, $pageUid, $afterExisting, $hidePages, $hidePagesInMenu); + $view->assign('pagesCreated', $pagesCreated); + $subPages = $this->getSubPagesOfPage($pageUid); + $visiblePages = []; + foreach ($subPages as $page) { + $calculatedPermissions = new Permission($backendUser->calcPerms($page)); + if ($backendUser->isAdmin() || $calculatedPermissions->showPagePermissionIsGranted()) { + $visiblePages[] = $page; + } + } + $view->assign('visiblePages', $visiblePages); + } else { + $hasNewPagesData = false; + $types = $this->getTypeSelectData($pageUid, $request); + $filteredTypes = []; + $types = $this->filterTypesThatOnlyRequireTitle($types, $filteredTypes); + $view->assign('pageTypes', $types); + $view->assign('filteredTypes', $filteredTypes); + $view->assign('wizardConfiguration', ['positionData' => ['pageUid' => $pageUid, 'insertPosition' => 'inside']]); + } + $view->assign('hasNewPagesData', $hasNewPagesData); + } + + return $view->renderResponse('Page/NewPages'); + } + + /** + * Persist new pages in DB + * + * @param array $newPagesData Data array with title and page type + * @param int $pageUid Uid of page new pages should be added in + * @param bool $afterExisting True if new pages should be created after existing pages + * @param bool $hidePages True if new pages should be set to hidden + * @param bool $hidePagesInMenu True if new pages should be set to hidden in menu + * @return bool TRUE if at least on pages has been added + */ + protected function createPages(array $newPagesData, int $pageUid, bool $afterExisting, bool $hidePages, bool $hidePagesInMenu): bool + { + $pagesCreated = false; + + // Set first pid to "-1 * uid of last existing sub page" if pages should be created at end + $firstPid = $pageUid; + if ($afterExisting) { + $subPages = $this->getSubPagesOfPage($pageUid); + $lastPage = end($subPages); + if (isset($lastPage['uid']) && MathUtility::canBeInterpretedAsInteger($lastPage['uid'])) { + $firstPid = -(int)$lastPage['uid']; + } + } + + $dataMap = []; + $firstRecord = true; + $previousIdentifier = ''; + foreach ($newPagesData as $identifier => $data) { + if (!trim($data['title'])) { + continue; + } + $dataMap['pages'][$identifier]['hidden'] = (int)$hidePages; + $dataMap['pages'][$identifier]['nav_hide'] = (int)$hidePagesInMenu; + $dataMap['pages'][$identifier]['title'] = $data['title']; + $dataMap['pages'][$identifier]['doktype'] = $data['doktype']; + if ($firstRecord) { + $firstRecord = false; + $dataMap['pages'][$identifier]['pid'] = $firstPid; + } else { + $dataMap['pages'][$identifier]['pid'] = '-' . $previousIdentifier; + } + $previousIdentifier = $identifier; + } + + if (!empty($dataMap)) { + $pagesCreated = true; + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $dataHandler->start($dataMap, []); + $dataHandler->process_datamap(); + BackendUtility::setUpdateSignal('updatePageTree'); + } + + return $pagesCreated; + } + + protected function filterTypesThatOnlyRequireTitle(array $selectData, array &$filteredTypes): array + { + $pageSchema = $this->tcaSchemaFactory->get('pages'); + + foreach ($selectData as $group => $types) { + foreach ($types as $index => $type) { + $typeValue = (string)$type['value']; + $schema = $pageSchema->getSubSchema($typeValue); + foreach ($schema->getFields() as $field) { + if ($field->isRequired() && $field->getName() !== 'title') { + unset($selectData[$group][$index]); + $filteredTypes[$typeValue] = $type['label']; + continue 2; + } + } + } + } + + // Remove empty categories + $selectData = array_filter($selectData, static fn(array $types) => count($types) > 0); + + return $selectData; + } + + /** + * Page selector type data + */ + protected function getTypeSelectData(int $pageUid, ServerRequestInterface $request): array + { + $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class); + $formDataGroup->setProviderList([ + InitializeProcessedTca::class, + DatabaseParentPageRow::class, + DatabaseUserPermissionCheck::class, + DatabaseEffectivePid::class, + UserTsConfig::class, + PageTsConfig::class, + DatabaseRowInitializeNew::class, + DatabaseUniqueUidNewRow::class, + TcaSelectItems::class, + ]); + $selectItems = $this->formDataCompiler->compile( + [ + 'command' => 'new', + 'request' => $request, + 'tableName' => 'pages', + 'vanillaUid' => $pageUid, + ], + $formDataGroup + )['processedTca']['columns']['doktype']['config']['items'] ?? []; + + $groupedData = []; + $groupLabel = ''; + + foreach ($selectItems as $selectItem) { + // If it is a group, save the group label for the children underneath. + if ($selectItem['value'] === '--div--') { + // Dividers defined inside the items array are not translated + // by the GroupAndSortService. + $groupLabel = $this->getLanguageService()->sL($selectItem['label']); + } else { + $groupedData[$groupLabel][] = $selectItem; + } + } + + return $groupedData; + } + + /** + * Get a list of sub pages with some all fields from given page. + * Fetch all data fields for full page icon display + * + * @param int $pageUid Get sub pages from this pages + */ + protected function getSubPagesOfPage(int $pageUid): array + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); + $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + return $queryBuilder->select('*') + ->from('pages') + ->where( + $queryBuilder->expr()->eq( + 'pid', + $queryBuilder->createNamedParameter($pageUid, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + $this->tcaSchemaFactory->get('pages')->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName(), + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ) + ) + ->orderBy('sorting') + ->executeQuery() + ->fetchAllAssociative(); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/Page/SortSubPagesController.php b/Classes/Controller/Page/SortSubPagesController.php new file mode 100644 index 0000000..a175433 --- /dev/null +++ b/Classes/Controller/Page/SortSubPagesController.php @@ -0,0 +1,208 @@ +moduleTemplateFactory->create($request); + $backendUser = $this->getBackendUser(); + $parentPageUid = (int)($request->getQueryParams()['id'] ?? 0); + + // Show only if there is a valid page and if this page may be viewed by the user + $pageInformation = BackendUtility::readPageAccess($parentPageUid, $backendUser->getPagePermsClause(Permission::PAGE_SHOW)); + if (!is_array($pageInformation)) { + // User has no permission on parent page, should not happen, just render an empty page + return $view->renderResponse('Dummy/Index'); + } + + // Doc header handling + $view->getDocHeaderComponent()->setPageBreadcrumb($pageInformation); + $view->addButtonToButtonBar( + $this->componentFactory->createViewButton( + PreviewUriBuilder::create($pageInformation) + ->withRootLine(BackendUtility::BEgetRootLine($parentPageUid)) + ->buildDispatcherDataAttributes() ?? [] + ) + ); + + $isInWorkspace = $backendUser->workspace !== 0; + $view->assignMultiple([ + 'isInWorkspace' => $isInWorkspace, + 'maxTitleLength' => $backendUser->uc['titleLen'] ?? 20, + 'parentPageUid' => $parentPageUid, + 'dateFormat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], + 'timeFormat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], + ]); + + if (!$isInWorkspace) { + // Apply new sorting if given + $newSortBy = $request->getQueryParams()['newSortBy'] ?? null; + if ($newSortBy && in_array($newSortBy, ['title', 'subtitle', 'nav_title', 'crdate', 'tstamp'], true)) { + $this->sortSubPagesByField($parentPageUid, (string)$newSortBy); + } elseif ($newSortBy && $newSortBy === 'reverseCurrentSorting') { + $this->reverseSortingOfPages($parentPageUid); + } + + // Get sub pages, loop through them and add page/user specific permission details + $pageRecords = $this->getSubPagesOfPage($parentPageUid); + $hasInvisiblePage = false; + $subPages = []; + foreach ($pageRecords as $page) { + $pageWithPermissions = []; + $pageWithPermissions['record'] = $page; + $calculatedPermissions = new Permission($backendUser->calcPerms($page)); + $pageWithPermissions['canEdit'] = $backendUser->isAdmin() || $calculatedPermissions->editPagePermissionIsGranted(); + $canSeePage = $backendUser->isAdmin() || $calculatedPermissions->showPagePermissionIsGranted(); + if ($canSeePage) { + $subPages[] = $pageWithPermissions; + } else { + $hasInvisiblePage = true; + } + } + $view->assign('subPages', $subPages); + $view->assign('hasInvisiblePage', $hasInvisiblePage); + } + + return $view->renderResponse('Page/SortSubPages'); + } + + /** + * Sort sub pages of given uid by field name alphabetically + * + * @param int $parentPageUid Parent page uid + * @param string $newSortBy Field name to sort by + * @throws \RuntimeException If $newSortBy does not validate + */ + protected function sortSubPagesByField(int $parentPageUid, string $newSortBy) + { + if (!in_array($newSortBy, ['title', 'subtitle', 'nav_title', 'crdate', 'tstamp'], true)) { + throw new \RuntimeException( + 'New sort by must be one of "title", "subtitle", "nav_title", "crdate" or tstamp', + 1498924810 + ); + } + $subPages = $this->getSubPagesOfPage($parentPageUid, $newSortBy); + if (!empty($subPages)) { + $subPages = array_reverse($subPages); + $this->persistNewSubPageOrder($parentPageUid, $subPages); + } + } + + /** + * Reverse current sorting of sub pages + * + * @param int $parentPageUid Parent page uid + */ + protected function reverseSortingOfPages(int $parentPageUid) + { + $subPages = $this->getSubPagesOfPage($parentPageUid); + if (!empty($subPages)) { + $this->persistNewSubPageOrder($parentPageUid, $subPages); + } + } + + /** + * Store new sub page order + * + * @param int $parentPageUid Parent page uid + * @param array $subPages List of sub pages in new order + */ + protected function persistNewSubPageOrder(int $parentPageUid, array $subPages) + { + $commandArray = []; + foreach ($subPages as $subPage) { + $commandArray['pages'][$subPage['uid']]['move'] = $parentPageUid; + } + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $dataHandler->start([], $commandArray); + $dataHandler->process_cmdmap(); + BackendUtility::setUpdateSignal('updatePageTree'); + } + + /** + * Get a list of sub pages with some all fields from given page. + * Fetch all data fields for full page icon display + * + * @param int $parentPageUid Get sub pages from this pages + * @param string $orderBy Order pages by this field + */ + protected function getSubPagesOfPage(int $parentPageUid, string $orderBy = 'sorting'): array + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); + $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + return $queryBuilder->select('*') + ->from('pages') + ->where( + $queryBuilder->expr()->or( + $queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter(\Local\Multilanguage\Service\DefaultLanguageTagService::getTag())), + $queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter('')), + ), + $queryBuilder->expr()->eq( + 'pid', + $queryBuilder->createNamedParameter($parentPageUid, Connection::PARAM_INT) + ) + ) + ->orderBy($orderBy) + ->executeQuery() + ->fetchAllAssociative(); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/Page/TreeController.php b/Classes/Controller/Page/TreeController.php new file mode 100644 index 0000000..e4120d0 --- /dev/null +++ b/Classes/Controller/Page/TreeController.php @@ -0,0 +1,800 @@ +getQueryParams()['readOnly'] ?? false) { + $this->getBackendUser()->initializeWebmountsForElementBrowser(); + } + if ($request->getQueryParams()['alternativeEntryPoints'] ?? false) { + $this->alternativeEntryPoints = $request->getQueryParams()['alternativeEntryPoints']; + $this->alternativeEntryPoints = array_filter($this->alternativeEntryPoints, function (int $pageId): bool { + return $this->getBackendUser()->isInWebMount($pageId) !== null; + }); + $this->alternativeEntryPoints = array_map(intval(...), $this->alternativeEntryPoints); + $this->alternativeEntryPoints = array_unique($this->alternativeEntryPoints); + } + $userTsConfig = $this->getBackendUser()->getTSConfig(); + $this->hiddenRecords = GeneralUtility::intExplode( + ',', + (string)($userTsConfig['options.']['hideRecords.']['pages'] ?? ''), + true + ); + $this->labels = $userTsConfig['options.']['pageTree.']['label.'] ?? []; + $this->addIdAsPrefix = (bool)($userTsConfig['options.']['pageTree.']['showPageIdWithTitle'] ?? false); + $this->addDomainName = (bool)($userTsConfig['options.']['pageTree.']['showDomainNameWithTitle'] ?? false); + $this->useNavTitle = (bool)($userTsConfig['options.']['pageTree.']['showNavTitle'] ?? false); + $this->showMountPathAboveMounts = (bool)($userTsConfig['options.']['pageTree.']['showPathAboveMounts'] ?? false); + $this->userHasAccessToModifyPagesAndToDefaultLanguage = $this->getBackendUser()->check('tables_modify', 'pages') && $this->getBackendUser()->checkLanguageAccess(0); + } + + /** + * Returns page tree configuration in JSON + */ + public function fetchConfigurationAction(ServerRequestInterface $request): ResponseInterface + { + $backendUser = $this->getBackendUser(); + $userTsConfig = $backendUser->getTSConfig(); + + // Check if translation search feature is generally available (TSconfig setting) + $translationSearchAvailable = (bool)($userTsConfig['options.']['pageTree.']['searchInTranslatedPages'] ?? true); + + // Determine if translation search is enabled by the user preference - otherwise TSconfig setting applies + $translationSearchEnabled = $translationSearchAvailable + && ( + !isset($backendUser->uc['pageTree_searchInTranslatedPages']) + || $backendUser->uc['pageTree_searchInTranslatedPages'] + ); + + // Check if frontend URI search feature is generally available (TSconfig setting) + $frontendUriSearchAvailable = (bool)($userTsConfig['options.']['pageTree.']['searchByFrontendUri'] ?? true); + + // Determine if frontend URI search is enabled by the user preference - otherwise TSconfig setting applies + $frontendUriSearchEnabled = $frontendUriSearchAvailable + && ( + !isset($backendUser->uc['pageTree_searchByFrontendUri']) + || $backendUser->uc['pageTree_searchByFrontendUri'] + ); + + // Build language list from site configuration + $languages = []; + $currentLanguageTag = $_COOKIE['pageTreeLang'] ?? ''; + try { + $siteFinder = GeneralUtility::makeInstance(SiteFinder::class); + $sites = $siteFinder->getAllSites(); + foreach ($sites as $site) { + foreach ($site->getAllLanguages() as $lang) { + $tag = $lang->getLanguageTag(); + $languages[] = [ + 'languageTag' => $tag, + 'title' => $lang->getTitle(), + 'flag' => $lang->getFlagIdentifier(), + ]; + } + break; + } + } catch (\Throwable) {} + + $dataUrlParams = []; + if ($currentLanguageTag !== '') { + $dataUrlParams['language'] = $currentLanguageTag; + } + + $configuration = [ + 'allowDragMove' => $this->isDragMoveAllowed(), + 'doktypes' => $this->getDokTypes($request), + 'displayDeleteConfirmation' => $backendUser->jsConfirmation(JsConfirmation::DELETE), + 'temporaryMountPoint' => $this->getMountPointPath((int)($backendUser->uc['pageTree_temporaryMountPoint'] ?? 0)), + 'showIcons' => true, + 'dataUrl' => (string)$this->uriBuilder->buildUriFromRoute('ajax_page_tree_data', $dataUrlParams), + 'rootlineUrl' => (string)$this->uriBuilder->buildUriFromRoute('ajax_page_tree_rootline'), + 'filterUrl' => (string)$this->uriBuilder->buildUriFromRoute('ajax_page_tree_filter'), + 'setTemporaryMountPointUrl' => (string)$this->uriBuilder->buildUriFromRoute('ajax_page_tree_set_temporary_mount_point'), + 'searchInTranslatedPagesEnabled' => $translationSearchEnabled, + 'searchInTranslatedPagesAvailable' => $translationSearchAvailable, + 'searchByFrontendUriEnabled' => $frontendUriSearchEnabled, + 'searchByFrontendUriAvailable' => $frontendUriSearchAvailable, + 'languages' => $languages, + 'currentLanguage' => $currentLanguageTag, + ]; + + return new JsonResponse($configuration); + } + + public function fetchReadOnlyConfigurationAction(ServerRequestInterface $request): ResponseInterface + { + $entryPoints = (string)($request->getQueryParams()['alternativeEntryPoints'] ?? ''); + $entryPoints = GeneralUtility::intExplode(',', $entryPoints, true); + $additionalArguments = [ + 'readOnly' => 1, + ]; + if (!empty($entryPoints)) { + $additionalArguments['alternativeEntryPoints'] = $entryPoints; + } + $configuration = [ + 'displayDeleteConfirmation' => $this->getBackendUser()->jsConfirmation(JsConfirmation::DELETE), + 'temporaryMountPoint' => $this->getMountPointPath((int)($this->getBackendUser()->uc['pageTree_temporaryMountPoint'] ?? 0)), + 'showIcons' => true, + 'dataUrl' => (string)$this->uriBuilder->buildUriFromRoute('ajax_page_tree_data', $additionalArguments), + 'filterUrl' => (string)$this->uriBuilder->buildUriFromRoute('ajax_page_tree_filter', $additionalArguments), + 'setTemporaryMountPointUrl' => (string)$this->uriBuilder->buildUriFromRoute('ajax_page_tree_set_temporary_mount_point'), + 'nonViewableDoktypes' => $this->pageDoktypeRegistry->getNonViewableDoktypes(), + ]; + return new JsonResponse($configuration); + } + + /** + * Returns the list of doktypes to display in page tree toolbar drag area, + * automatically determined based on the user's group permissions. + */ + protected function getDokTypes(ServerRequestInterface $request): array + { + $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class); + // Skip DatabaseUserPermissionCheck::class to return doktypes even if the user cannot create pages at root level + $formDataGroup->setProviderList([ + InitializeProcessedTca::class, + DatabaseParentPageRow::class, + DatabaseEffectivePid::class, + UserTsConfig::class, + PageTsConfig::class, + DatabaseRowInitializeNew::class, + DatabaseUniqueUidNewRow::class, + TcaSelectItems::class, + ]); + + try { + $doktypes = $this->formDataCompiler + ->compile( + [ + 'command' => 'new', + 'request' => $request, + 'tableName' => 'pages', + 'vanillaUid' => 0, + ], + $formDataGroup + )['processedTca']['columns']['doktype']['config']['items'] ?? []; + } catch (\Exception) { + return []; + } + + return array_values( + array_map( + static fn(array $doktype) => [ + 'nodeType' => $doktype['value'], + 'icon' => $doktype['icon'] ?? '', + 'title' => $doktype['label'] ?? '', + ], + array_filter( + $doktypes, + static fn(array $doktype) => ($doktype['value'] ?? '') !== '--div--' && ($doktype['value'] ?? '') !== '' + ) + ) + ); + } + + /** + * Returns JSON representing page tree + */ + public function fetchDataAction(ServerRequestInterface $request): ResponseInterface + { + $this->initializeConfiguration($request); + + $languageParam = $request->getQueryParams()['language'] ?? ''; + $languageTag = ($languageParam !== '' && $languageParam !== '0') ? $languageParam : null; + + $items = []; + $parentIdentifier = $request->getQueryParams()['parent'] ?? null; + if ($parentIdentifier) { + $parentDepth = (int)($request->getQueryParams()['depth'] ?? 0); + // Fetching a part of a page tree + $entryPoints = $this->getAllEntryPointPageTrees((int)$parentIdentifier); + $mountPid = (int)($request->getQueryParams()['mount'] ?? 0); + $this->levelsToFetch = $parentDepth + $this->levelsToFetch; + foreach ($entryPoints as $page) { + $items[] = $this->pagesToFlatArray($page, $mountPid, $parentDepth); + } + } else { + $entryPoints = $this->getAllEntryPointPageTrees(); + foreach ($entryPoints as $page) { + $items[] = $this->pagesToFlatArray($page, (int)$page['uid']); + } + } + $items = array_merge(...$items); + + if ($languageTag !== null) { + $this->applyTranslationOverlay($items, $languageTag); + } + + return new JsonResponse($this->getPostProcessedPageItems($request, $items)); + } + + private function applyTranslationOverlay(array &$items, string $languageTag): void + { + $pageIds = []; + foreach ($items as $item) { + $uid = (int)($item['identifier'] ?? 0); + if ($uid > 0) { + $pageIds[] = $uid; + } + } + if (empty($pageIds)) { + return; + } + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('pages'); + $translations = $queryBuilder + ->select('l10n_parent', 'title', 'nav_title', 'language_tag') + ->from('pages') + ->where( + $queryBuilder->expr()->in('l10n_parent', $pageIds), + $queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter($languageTag)) + ) + ->executeQuery() + ->fetchAllAssociative(); + + $translationMap = []; + foreach ($translations as $trans) { + $translationMap[(int)$trans['l10n_parent']] = $trans; + } + + foreach ($items as &$item) { + $page = $item['_page'] ?? []; + $uid = (int)($page['uid'] ?? (int)($item['identifier'] ?? 0)); + $trans = $translationMap[$uid] ?? null; + if ($trans) { + $item['name'] = $trans['title']; + if (!empty($trans['nav_title'])) { + $item['name'] = $trans['nav_title']; + $item['nameSourceField'] = 'nav_title'; + } + $item['_page']['title'] = $trans['title']; + $item['_page']['nav_title'] = $trans['nav_title'] ?? ''; + $item['_page']['_translatedTitle'] = $trans['title']; + } + } + } + + /** + * Returns JSON representing page rootline + */ + public function fetchRootlineAction(ServerRequestInterface $request): ResponseInterface + { + $identifier = (string)($request->getQueryParams()['identifier'] ?? ''); + if (!MathUtility::canBeInterpretedAsInteger($identifier)) { + return new JsonResponse(null, 400); + } + $pageId = (int)$identifier; + + if ($pageId === 0) { + return new JsonResponse(['rootline' => ['0']]); + } + + $rootline = BackendUtility::BEgetRootLine((int)$identifier); + if ($rootline === []) { + return new JsonResponse(null, 404); + } + + return new JsonResponse([ + 'rootline' => array_map(strval(...), array_column(array_reverse($rootline), 'uid')), + ]); + } + + /** + * Returns JSON representing page tree filtered by keyword + */ + public function filterDataAction(ServerRequestInterface $request): ResponseInterface + { + $searchQuery = $request->getQueryParams()['q'] ?? ''; + if (trim($searchQuery) === '') { + return new JsonResponse([]); + } + + $this->initializeConfiguration($request); + $this->expandAllNodes = true; + + $items = []; + $entryPoints = $this->getAllEntryPointPageTrees(0, $searchQuery); + + foreach ($entryPoints as $page) { + if (!empty($page)) { + $items[] = $this->pagesToFlatArray($page, (int)$page['uid']); + } + } + $items = array_merge(...$items); + + return new JsonResponse($this->getPostProcessedPageItems($request, $items)); + } + + /** + * Sets a temporary mount point + * + * @throws \RuntimeException + */ + public function setTemporaryMountPointAction(ServerRequestInterface $request): ResponseInterface + { + if (empty($request->getParsedBody()['pid'])) { + throw new \RuntimeException( + 'Required "pid" parameter is missing.', + 1511792197 + ); + } + $pid = (int)$request->getParsedBody()['pid']; + + $this->getBackendUser()->uc['pageTree_temporaryMountPoint'] = $pid; + $this->getBackendUser()->writeUC(); + $response = [ + 'mountPointPath' => $this->getMountPointPath($pid), + ]; + return new JsonResponse($response); + } + + /** + * Converts nested tree structure produced by PageTreeRepository to a flat, one level array + * and also adds visual representation information to the data. + * + * The result is intended to be used as JSON result - dumping data directly to HTML might lead to XSS! + * + * @param array $page + * @param int $entryPoint + * @param int $depth + */ + protected function pagesToFlatArray(array $page, int $entryPoint, int $depth = 0): array + { + $backendUser = $this->getBackendUser(); + $pageId = (int)$page['uid']; + if (in_array($pageId, $this->hiddenRecords, true)) { + return []; + } + + $stopPageTree = !empty($page['php_tree_stop']) && $depth > 0; + $identifier = $entryPoint . '_' . $pageId; + + $suffix = ''; + $prefix = ''; + $nameSourceField = 'title'; + $visibleText = $page['title']; + $tooltip = BackendUtility::titleAttribForPages($page, '', false, $this->useNavTitle); + if ($pageId !== 0) { + $icon = $this->iconFactory->getIconForRecord('pages', $page, IconSize::SMALL); + } else { + $icon = $this->iconFactory->getIcon('apps-pagetree-root', IconSize::SMALL); + } + + if ($this->useNavTitle && trim($page['nav_title'] ?? '') !== '') { + $nameSourceField = 'nav_title'; + $visibleText = $page['nav_title']; + } + if (trim($visibleText) === '') { + $visibleText = htmlspecialchars('[' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title') . ']'); + } + + if ($this->addDomainName && ($page['is_siteroot'] ?? false)) { + $domain = $this->getDomainNameForPage($pageId); + $suffix = $domain !== '' ? ' [' . $domain . ']' : ''; + } + + $lockInfo = BackendUtility::isRecordLocked('pages', $pageId); + if (is_array($lockInfo)) { + $tooltip .= ' - ' . $lockInfo['msg']; + } + if ($this->addIdAsPrefix) { + $prefix = '[' . $pageId . '] '; + } + + $labels = []; + if (!empty($this->labels[$pageId . '.']) && isset($this->labels[$pageId . '.']['label']) && trim($this->labels[$pageId . '.']['label']) !== '') { + $labels[] = new Label( + label: $this->getLanguageService()->sL($this->labels[$pageId . '.']['label']), + color: (string)($this->labels[$pageId . '.']['color'] ?? '#ff8700'), + ); + } + + $editable = false; + if ($pageId !== 0) { + $editable = $this->userHasAccessToModifyPagesAndToDefaultLanguage && $backendUser->doesUserHaveAccess($page, Permission::PAGE_EDIT); + } + + $items = []; + $item = [ + // identifier is not only used for pages, therefore it's a string + 'identifier' => (string)$pageId, + 'parentIdentifier' => (string)($page['pid'] ?? ''), + 'recordType' => 'pages', + 'name' => $visibleText, + 'prefix' => !empty($prefix) ? htmlspecialchars($prefix) : '', + 'suffix' => !empty($suffix) ? htmlspecialchars($suffix) : '', + 'tooltip' => $tooltip, + 'depth' => $depth, + 'icon' => $icon->getIdentifier(), + 'overlayIcon' => $icon->getOverlayIcon() ? $icon->getOverlayIcon()->getIdentifier() : '', + 'editable' => $editable, + 'deletable' => $backendUser->doesUserHaveAccess($page, Permission::PAGE_DELETE), + 'labels' => $labels, + + // _page is only for use in events so they do not need to fetch those + // records again. The property will be removed from the final payload. + '_page' => $page, + // _translationLanguageUids contains the language UIDs for translations that matched (only populated during search) + '_translationLanguageUids' => $this->pageTreeRepository->getTranslationMatches($pageId), + 'doktype' => (int)($page['doktype'] ?? 0), + 'nameSourceField' => $nameSourceField, + 'mountPoint' => $entryPoint, + 'workspaceId' => !empty($page['t3ver_oid']) ? $page['t3ver_oid'] : $pageId, + ]; + + if (!empty($page['_children']) || $this->pageTreeRepository->hasChildren($pageId)) { + $item['hasChildren'] = true; + if ($depth >= $this->levelsToFetch) { + $page = $this->pageTreeRepository->getTreeLevels($page, 1); + } + } + if (is_array($lockInfo)) { + $item['locked'] = true; + } + if ($stopPageTree) { + $item['stopPageTree'] = true; + } + if ($depth === 0) { + if ($this->showMountPathAboveMounts) { + $item['note'] = $this->getMountPointPath($pageId); + } + } + + $items[] = $item; + if (!$stopPageTree && is_array($page['_children']) && !empty($page['_children']) && ($depth < $this->levelsToFetch || $this->expandAllNodes)) { + $items[key($items)]['loaded'] = true; + foreach ($page['_children'] as $child) { + $items = array_merge($items, $this->pagesToFlatArray($child, $entryPoint, $depth + 1)); + } + } + + return $items; + } + + protected function initializePageTreeRepository(): PageTreeRepository + { + $backendUser = $this->getBackendUser(); + $userTsConfig = $backendUser->getTSConfig(); + $excludedDocumentTypes = GeneralUtility::intExplode(',', (string)($userTsConfig['options.']['pageTree.']['excludeDoktypes'] ?? ''), true); + + $additionalQueryRestrictions = []; + if ($excludedDocumentTypes !== []) { + $additionalQueryRestrictions[] = GeneralUtility::makeInstance(DocumentTypeExclusionRestriction::class, $excludedDocumentTypes); + } + + $pageTreeRepository = GeneralUtility::makeInstance( + PageTreeRepository::class, + $backendUser->workspace, + [], + $additionalQueryRestrictions + ); + $pageTreeRepository->setAdditionalWhereClause($backendUser->getPagePermsClause(Permission::PAGE_SHOW)); + return $pageTreeRepository; + } + + /** + * Fetches all pages for all tree entry points the user is allowed to see + * + * @param string $query The search query can either be a string to be found in the title or the nav_title of a page or the uid of a page. + */ + protected function getAllEntryPointPageTrees(int $startPid = 0, string $query = ''): array + { + $this->pageTreeRepository ??= $this->initializePageTreeRepository(); + $backendUser = $this->getBackendUser(); + if ($startPid === 0) { + $startPid = (int)($backendUser->uc['pageTree_temporaryMountPoint'] ?? 0); + } + + $entryPointIds = null; + if ($startPid > 0) { + $entryPointIds = [$startPid]; + } elseif (!empty($this->alternativeEntryPoints)) { + $entryPointIds = $this->alternativeEntryPoints; + } + + $permClause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW); + if ($query !== '') { + $this->levelsToFetch = 999; + $this->pageTreeRepository->fetchFilteredTree( + $query, + $this->getAllowedMountPoints(), + $permClause + ); + } + $rootRecord = [ + 'uid' => 0, + 'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?: 'TYPO3', + ]; + $entryPointRecords = []; + $mountPoints = []; + if ($entryPointIds === null) { + //watch out for deleted pages returned as webmount + $mountPoints = $backendUser->getWebmounts(); + $mountPoints = array_filter($mountPoints, fn(int $id): bool => !in_array($id, $this->hiddenRecords, true)); + + // Switch to multiple-entryPoint-mode if the rootPage is to be mounted. + // (other mounts would appear duplicated in the pid = 0 tree otherwise) + if (in_array(0, $mountPoints, true)) { + $entryPointIds = $mountPoints; + } + } + + if ($entryPointIds === null) { + if ($query !== '') { + $rootRecord = $this->pageTreeRepository->getTree(0, null, $mountPoints); + } else { + $rootRecord = $this->pageTreeRepository->getTreeLevels($rootRecord, $this->levelsToFetch, $mountPoints); + } + + $mountPointOrdering = array_flip($mountPoints); + if (isset($rootRecord['_children'])) { + usort($rootRecord['_children'], static function ($a, $b) use ($mountPointOrdering) { + return ($mountPointOrdering[$a['uid']] ?? 0) <=> ($mountPointOrdering[$b['uid']] ?? 0); + }); + } + + $entryPointRecords[] = $rootRecord; + } else { + $entryPointIds = array_filter($entryPointIds, fn(int $id): bool => !in_array($id, $this->hiddenRecords, true)); + foreach ($entryPointIds as $k => $entryPointId) { + if ($entryPointId === 0) { + $entryPointRecord = $rootRecord; + } else { + $entryPointRecord = BackendUtility::getRecordWSOL('pages', $entryPointId, '*', $permClause); + + if ($entryPointRecord !== null && !$backendUser->isInWebMount($entryPointId)) { + $entryPointRecord = null; + } + if ($entryPointRecord === null) { + continue; + } + } + + $entryPointRecord['uid'] = (int)$entryPointRecord['uid']; + if ($query === '') { + $entryPointRecord = $this->pageTreeRepository->getTreeLevels($entryPointRecord, $this->levelsToFetch); + } else { + $entryPointRecord = $this->pageTreeRepository->getTree($entryPointRecord['uid'], null, $entryPointIds); + } + + if ($entryPointRecord !== []) { + $entryPointRecords[$k] = $entryPointRecord; + } + } + } + + return $entryPointRecords; + } + + /** + * Returns the first configured domain name for a page + */ + protected function getDomainNameForPage(int $pageId): string + { + try { + $site = $this->siteFinder->getSiteByRootPageId($pageId); + return (string)$site->getBase(); + } catch (SiteNotFoundException) { + // No site found + } + return ''; + } + + /** + * Returns the mount point path for a temporary mount or the given id + */ + protected function getMountPointPath(int $uid): string + { + if ($uid <= 0) { + return ''; + } + $rootline = array_reverse(BackendUtility::BEgetRootLine($uid)); + array_shift($rootline); + $path = []; + foreach ($rootline as $rootlineElement) { + $record = BackendUtility::getRecordWSOL('pages', $rootlineElement['uid'], 'title, nav_title', '', true, true); + $text = $record['title']; + if ($this->useNavTitle && trim($record['nav_title'] ?? '') !== '') { + $text = $record['nav_title']; + } + $path[] = htmlspecialchars($text); + } + return '/' . implode('/', $path); + } + + /** + * Check if drag-move in the svg tree is allowed for the user + */ + protected function isDragMoveAllowed(): bool + { + $backendUser = $this->getBackendUser(); + return $backendUser->isAdmin() + || ($backendUser->check('tables_modify', 'pages') && $backendUser->checkLanguageAccess(0)); + } + + /** + * Get allowed mountpoints. Returns temporary mountpoint when temporary mountpoint is used. + * + * @return int[] + */ + protected function getAllowedMountPoints(): array + { + $mountPoints = (int)($this->getBackendUser()->uc['pageTree_temporaryMountPoint'] ?? 0); + if (!$mountPoints) { + if (!empty($this->alternativeEntryPoints)) { + return $this->alternativeEntryPoints; + } + return $this->getBackendUser()->getWebmounts(); + } + return [$mountPoints]; + } + + protected function getPostProcessedPageItems(ServerRequestInterface $request, array $items): array + { + return array_map( + static function (array $item): PageTreeItem { + return new PageTreeItem( + // TreeItem + new TreeItem( + identifier: $item['identifier'], + parentIdentifier: (string)($item['parentIdentifier'] ?? ''), + recordType: (string)($item['recordType'] ?? ''), + name: (string)($item['name'] ?? ''), + note: (string)($item['note'] ?? ''), + prefix: (string)($item['prefix'] ?? ''), + suffix: (string)($item['suffix'] ?? ''), + tooltip: (string)($item['tooltip'] ?? ''), + depth: (int)($item['depth'] ?? 0), + hasChildren: (bool)($item['hasChildren'] ?? false), + loaded: (bool)($item['loaded'] ?? false), + editable: (bool)($item['editable'] ?? false), + deletable: (bool)($item['deletable'] ?? false), + icon: (string)($item['icon'] ?? ''), + overlayIcon: (string)($item['overlayIcon'] ?? ''), + statusInformation: (array)($item['statusInformation'] ?? []), + labels: (array)($item['labels'] ?? []), + ), + // PageTreeItem + doktype: (int)($item['doktype'] ?? ''), + nameSourceField: (string)($item['nameSourceField'] ?? ''), + workspaceId: (int)($item['workspaceId'] ?? 0), + locked: (bool)($item['locked'] ?? false), + stopPageTree: (bool)($item['stopPageTree'] ?? false), + mountPoint: (int)($item['mountPoint'] ?? 0), + ); + }, + $this->eventDispatcher->dispatch( + new AfterPageTreeItemsPreparedEvent($request, $items) + )->getItems() + ); + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): ?LanguageService + { + return $GLOBALS['LANG'] ?? null; + } +} diff --git a/Classes/Controller/PageLayoutController.php b/Classes/Controller/PageLayoutController.php new file mode 100644 index 0000000..1ef277e --- /dev/null +++ b/Classes/Controller/PageLayoutController.php @@ -0,0 +1,756 @@ + Layout module. + * + * @internal This class is not part of the TYPO3 API. + */ +#[AsController] +class PageLayoutController +{ + protected PageContext $pageContext; + protected ?TcaSchema $schema = null; + protected ?ModuleData $moduleData = null; + + public function __construct( + protected readonly ComponentFactory $componentFactory, + protected readonly IconFactory $iconFactory, + protected readonly PageRenderer $pageRenderer, + protected readonly UriBuilder $uriBuilder, + protected readonly PageRepository $pageRepository, + protected readonly ModuleTemplateFactory $moduleTemplateFactory, + protected readonly EventDispatcherInterface $eventDispatcher, + protected readonly ModuleProvider $moduleProvider, + protected readonly BackendLayoutRenderer $backendLayoutRenderer, + protected readonly BackendLayoutView $backendLayoutView, + protected readonly TcaSchemaFactory $tcaSchemaFactory, + protected readonly ConnectionPool $connectionPool, + protected readonly LanguageSelectorBuilder $languageSelectorBuilder, + protected readonly PageLinkMessageProvider $pageLinkMessageProvider, + private readonly SchemaLabelResolver $schemaLabelResolver, + ) {} + + public function mainAction(ServerRequestInterface $request): ResponseInterface + { + $pageContext = $request->getAttribute('pageContext'); + if (!$pageContext instanceof PageContext) { + throw new \RuntimeException('Required PageContext not available', 1731415237); + } + $this->pageContext = $pageContext; + + $languageService = $this->getLanguageService(); + $view = $this->moduleTemplateFactory->create($request); + if (!$this->pageContext->isAccessible() || $this->pageContext->pageId === 0) { + // In case page could not be resolved or we are on pid=0, show info to select a valid page in the tree + $view->setTitle($languageService->translate('title', 'backend.modules.layout')); + $view->assignMultiple([ + 'pageId' => $this->pageContext->pageId, + 'siteName' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '', + ]); + $view->getDocHeaderComponent()->disableAutomaticReloadButton(); + return $view->renderResponse('PageLayout/PageModuleNoAccess'); + } + + $this->moduleData = $request->getAttribute('moduleData'); + $this->schema = $this->tcaSchemaFactory->get('pages'); + + $pageLayoutContext = $this->createPageLayoutContext($request); + + $this->updateModuleData(); + $this->createViewModeSelection($view); + $this->addButtonsToButtonBar($view, $request); + $this->initializeClipboard($request); + $event = $this->eventDispatcher->dispatch(new ModifyPageLayoutContentEvent($request, $view)); + + $mainLayoutHtml = $this->backendLayoutRenderer->drawContent($request, $pageLayoutContext); + $primaryLanguageId = $this->pageContext->getPrimaryLanguageId(); + $pageLocalizationRecord = $this->pageContext->languageInformation->getTranslationRecord($primaryLanguageId); + + $this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf'); + + $view->setTitle($languageService->translate('title', 'backend.modules.layout'), $this->pageContext->getPageTitle()); + $view->getDocHeaderComponent()->setPageBreadcrumb($this->pageContext->pageRecord); + $view->assignMultiple([ + 'pageId' => $this->pageContext->pageId, + 'localizedPageId' => $pageLocalizationRecord['uid'] ?? 0, + 'pageLayoutContext' => $pageLayoutContext, + 'infoBoxes' => $this->generateMessagesForCurrentPage($request), + 'isPageEditable' => $this->isPageEditable($primaryLanguageId), + 'localizedPageTitle' => $this->pageContext->getPageTitle($primaryLanguageId), + 'eventContentHtmlTop' => $event->getHeaderContent(), + 'mainContentHtml' => $mainLayoutHtml, + 'eventContentHtmlBottom' => $event->getFooterContent(), + ]); + return $view->renderResponse('PageLayout/PageModule'); + } + + protected function updateModuleData(): void + { + $backendUser = $this->getBackendUser(); + if (PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) === null) { + // Invalid function, reset to default + $this->moduleData->set('viewMode', PageViewMode::LayoutView->value); + } + if ($backendUser->workspace !== 0) { + // In draft workspaces, always show all elements (including hidden) + $this->moduleData->set('showHidden', true); + } + + // Store selected languages in module data for persistence + $this->moduleData->set('languages', $this->pageContext->selectedLanguageIds); + + // Write module data + $backendUser->pushModuleData($this->moduleData->getModuleIdentifier(), $this->moduleData->toArray()); + } + + /** + * Creates the menu dropdown for switching between view modes. + * + * Available actions: + * - LayoutView: Single-language content editing (always available) + * - LanguageComparisonView: Multi-language comparison (only if translations exist) + * + * Smart Language Selection: + * When building URLs for mode switching, this method implements smart language selection: + * - Switching to layout mode with 1 translation selected → keep that translation + * - Switching to layout mode with 2+ translations selected → show default + */ + protected function createViewModeSelection(ModuleTemplate $view): void + { + $languageService = $this->getLanguageService(); + $modes = [ + PageViewMode::LayoutView->value => $languageService->sL(PageViewMode::LayoutView->getLabel()), + ]; + + // Only show comparison mode if page has translations + if (!empty($this->pageContext->languageInformation->existingTranslations)) { + $modes[PageViewMode::LanguageComparisonView->value] = $languageService->sL(PageViewMode::LanguageComparisonView->getLabel()); + } + + // Apply TSconfig blinding + $moduleTsConfig = $this->pageContext->getModuleTsConfig('web_layout'); + $blindActions = $moduleTsConfig['menu']['functions'] ?? []; + foreach ($blindActions as $key => $value) { + if (!$value && array_key_exists($key, $modes)) { + unset($modes[$key]); + } + } + + // Only create menu if there are multiple actions to choose from + if (count($modes) <= 1) { + if (count($modes) === 1) { + $this->moduleData->set('viewMode', array_key_first($modes)); + } + return; + } + + $selectedMode = (int)$this->moduleData->get('viewMode'); + if (!array_key_exists($selectedMode, $modes)) { + // Current function is not in available modes - reset to first available mode + $this->moduleData->set('viewMode', array_key_first($modes)); + $selectedMode = (int)array_key_first($modes); + } + + $actionMenu = $this->componentFactory->createMenu(); + $actionMenu->setIdentifier('actionMenu'); + $actionMenu->setLabel( + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:pagelayout.moduleMenu.dropdown.label') + ); + + foreach ($modes as $modeValue => $label) { + $urlParams = $this->buildViewModeSwitchParams($modeValue); + $menuItem = $this->componentFactory->createMenuItem() + ->setTitle($label) + ->setHref((string)$this->uriBuilder->buildUriFromRoute('web_layout', $urlParams)); + + if ($selectedMode === $modeValue) { + $menuItem->setActive(true); + } + $actionMenu->addMenuItem($menuItem); + } + $view->getDocHeaderComponent()->getMenuRegistry()->addMenu($actionMenu); + } + + /** + * Build URL parameters for switching to a specific view mode. + * + * Implements smart language selection when switching modes: + * + * FROM comparison TO layout: + * - 1 translation selected → keep that translation (user focused on it) + * - 2+ translations selected → show default language + * + * FROM layout TO comparison: + * - Keep the current language selection to preserve context + * + * @return array{id: int, viewMode: int, languages?: int[]} + */ + private function buildViewModeSwitchParams(int $targetModeValue): array + { + $params = ['id' => $this->pageContext->pageId, 'viewMode' => $targetModeValue]; + $targetMode = PageViewMode::tryFrom($targetModeValue); + + // Smart language selection when switching to layout mode FROM comparison mode + if ($targetMode === PageViewMode::LayoutView && $this->pageContext->hasMultipleLanguagesSelected()) { + $nonDefaultLanguages = array_filter($this->pageContext->selectedLanguageIds, static fn($id) => $id > 0); + $params['languages'] = [ + count($nonDefaultLanguages) === 1 + ? reset($nonDefaultLanguages) // Single translation: keep it + : 0, // Multiple: show default + ]; + } + + // When switching to comparison mode, preserve current language selection + // The comparison mode will auto-add default language if needed + if ($targetMode === PageViewMode::LanguageComparisonView) { + $params['languages'] = $this->pageContext->selectedLanguageIds; + } + + return $params; + } + + protected function createPageLayoutContext( + ServerRequestInterface $request + ): PageLayoutContext { + $backendLayout = $this->backendLayoutView->getBackendLayoutForPage($this->pageContext->pageId); + $viewMode = count($this->pageContext->languageInformation->availableLanguages) > 1 + ? PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) ?? PageViewMode::LayoutView + : PageViewMode::LayoutView; + $configuration = DrawingConfiguration::create($backendLayout, $this->pageContext->pageTsConfig, $viewMode); + $configuration->setShowHidden((bool)$this->moduleData->get('showHidden')); + + // Build language columns map from available languages + $languageColumns = []; + foreach ($this->pageContext->languageInformation->availableLanguages as $language) { + $languageColumns[$language->getLanguageId()] = $language->getTitle(); + } + // @todo Check if this is still used at all. + $configuration->setLanguageColumns($languageColumns); + + $configuration->setSelectedLanguageIds($this->pageContext->selectedLanguageIds); + return GeneralUtility::makeInstance(PageLayoutContext::class, $this->pageContext, $backendLayout, $configuration, $request); + } + + /** + * Return an array of various messages for the current page record, + * such as if the page has a special doktype, that can be rendered as info boxes. + */ + protected function generateMessagesForCurrentPage(ServerRequestInterface $request): array + { + $languageService = $this->getLanguageService(); + $backendUser = $this->getBackendUser(); + + $infoBoxes = []; + $currentDocumentType = (int)($this->pageContext->pageRecord['doktype'] ?? 0); + if ($currentDocumentType === PageRepository::DOKTYPE_SYSFOLDER && $this->moduleProvider->accessGranted('records', $backendUser)) { + $infoBoxes[] = [ + 'title' => $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:goToRecordsModule'), + 'message' => '

' . $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:goToListModuleMessage') . '

' + . '', + 'state' => ContextualFeedbackSeverity::INFO, + ]; + } + if ($currentDocumentType === PageRepository::DOKTYPE_SHORTCUT) { + $shortcutMode = (int)($this->pageContext->pageRecord['shortcut_mode'] ?? 0); + $targetPage = []; + $state = ContextualFeedbackSeverity::ERROR; + if ($shortcutMode || ($this->pageContext->pageRecord['shortcut'] ?? false)) { + switch ($shortcutMode) { + case PageRepository::SHORTCUT_MODE_NONE: + $targetPage = $this->getTargetPageIfVisible($this->pageRepository->getPage((int)($this->pageContext->pageRecord['shortcut'] ?? 0), true)); + $message = $targetPage === [] ? $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredOrNotAccessibleInternalLinkMessage') : ''; + break; + case PageRepository::SHORTCUT_MODE_FIRST_SUBPAGE: + $menuOfPages = $this->pageRepository->getMenu((int)($this->pageContext->pageRecord['uid'] ?? 0), '*', 'sorting', 'AND hidden = 0', true, true); + $targetPage = reset($menuOfPages) ?: []; + $message = $targetPage === [] ? $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredFirstSubpageMessage') : ''; + break; + case PageRepository::SHORTCUT_MODE_PARENT_PAGE: + $targetPage = $this->getTargetPageIfVisible($this->pageRepository->getPage((int)($this->pageContext->pageRecord['pid'] ?? 0), true)); + $message = $targetPage === [] ? $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredParentPageMessage') : ''; + break; + default: + $message = htmlspecialchars($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredInternalLinkMessage')); + break; + } + $message = htmlspecialchars($message); + if ($targetPage !== []) { + $linkToPid = $this->uriBuilder->buildUriFromRoute('web_layout', ['id' => $targetPage['uid']]); + $path = BackendUtility::getRecordPath($targetPage['uid'], $backendUser->getPagePermsClause(Permission::PAGE_SHOW), 1000); + $linkedPath = '' . htmlspecialchars($path) . ''; + $message .= sprintf(htmlspecialchars($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsInternalLinkMessage')), $linkedPath); + $message .= ' (' . htmlspecialchars($languageService->sL($this->schemaLabelResolver->getLabelForFieldValue('pages', 'shortcut_mode', (string)$shortcutMode, $this->pageContext->pageRecord))) . ')'; + $state = ContextualFeedbackSeverity::INFO; + } + } else { + $message = htmlspecialchars($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredInternalLinkMessage')); + } + $infoBoxes[] = [ + 'message' => $message, + 'state' => $state, + ]; + } + if ($currentDocumentType === PageRepository::DOKTYPE_LINK) { + $primaryLanguageId = $this->pageContext->getPrimaryLanguageId(); + $pageRecord = ($primaryLanguageId > 0 && ($overlayRecord = $this->pageContext->languageInformation->existingTranslations[$primaryLanguageId] ?? []) !== []) + ? $overlayRecord + : $this->pageContext->pageRecord; + $infoBoxes[] = $this->pageLinkMessageProvider->generateMessagesForPageTypeLink($pageRecord, $request); + } + if ($this->pageContext->pageRecord['content_from_pid'] ?? false) { + // If content from different pid is displayed + $contentPage = BackendUtility::getRecord('pages', (int)$this->pageContext->pageRecord['content_from_pid']); + if ($contentPage === null) { + $infoBoxes[] = [ + 'message' => sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:content_from_pid_invalid_title'), $this->pageContext->pageRecord['content_from_pid']), + 'state' => ContextualFeedbackSeverity::ERROR, + ]; + } else { + $linkToPid = $this->uriBuilder->buildUriFromRoute('web_layout', ['id' => $this->pageContext->pageRecord['content_from_pid']]); + $title = BackendUtility::getRecordTitle('pages', $contentPage); + $title = BackendUtility::cropToTitleLength($title); + $link = '' . htmlspecialchars($title) . ' (PID ' . (int)$this->pageContext->pageRecord['content_from_pid'] . ')'; + $infoBoxes[] = [ + 'message' => sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:content_from_pid_title'), $link), + 'state' => ContextualFeedbackSeverity::INFO, + ]; + } + } elseif ($this->pageContext->pageId > 0) { + $links = $this->getPageLinksWhereContentIsAlsoShownOn($this->pageContext->pageId); + if (!empty($links)) { + $infoBoxes[] = [ + 'message' => sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:content_on_pid_title'), $links), + 'state' => ContextualFeedbackSeverity::INFO, + ]; + } + } + return $infoBoxes; + } + + /** + * Get all pages with links where the content of a page $pageId is also shown on. + */ + protected function getPageLinksWhereContentIsAlsoShownOn(int $pageId): string + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); + $queryBuilder->getRestrictions()->removeAll(); + $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $queryBuilder->select('*') + ->from('pages') + ->where($queryBuilder->expr()->eq('content_from_pid', $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT))); + $links = []; + $rows = $queryBuilder->executeQuery()->fetchAllAssociative(); + if (!empty($rows)) { + foreach ($rows as $row) { + $linkToPid = $this->uriBuilder->buildUriFromRoute('web_layout', ['id' => $row['uid']]); + $title = BackendUtility::getRecordTitle('pages', $row); + $title = BackendUtility::cropToTitleLength($title); + $link = '' . htmlspecialchars($title) . ' (PID ' . (int)$row['uid'] . ')'; + $links[] = $link; + } + } + return implode(', ', $links); + } + + protected function addButtonsToButtonBar(ModuleTemplate $view, ServerRequestInterface $request): void + { + $languageService = $this->getLanguageService(); + + // Close button (show only if returnUrl is set) + $returnUrl = GeneralUtility::sanitizeLocalUrl($request->getQueryParams()['returnUrl'] ?? '', $request); + if ($returnUrl) { + // use button group -1 so that close button is to the left of other buttons + $view->addButtonToButtonBar($this->componentFactory->createCloseButton($returnUrl), ButtonBar::BUTTON_POSITION_LEFT, -1); + } + + // Language selector + $this->createLanguageSelector($view); + + // View + if ($viewButton = $this->makeViewButton()) { + $view->addButtonToButtonBar($viewButton); + } + + // Edit + if ($editButton = $this->makeEditButton($request)) { + $view->addButtonToButtonBar($editButton, ButtonBar::BUTTON_POSITION_LEFT, 3); + } + + // Cache + $clearCacheButton = $this->componentFactory->createGenericButton() + ->setTag('button') + ->setLabel($languageService->sL('core.cache:page.label')) + ->setClasses('t3js-clear-page-cache') + ->setAttributes([ + 'type' => 'button', + 'data-id' => (string)($this->pageContext->pageRecord['uid'] ?? 0), + ]) + ->setIcon($this->iconFactory->getIcon('actions-system-cache-clear', IconSize::SMALL)); + $view->addButtonToButtonBar($clearCacheButton, ButtonBar::BUTTON_POSITION_RIGHT, 1); + + // View settings + if ($this->getBackendUser()->check('tables_select', 'tt_content')) { + $viewSettingsButton = $this->componentFactory->createDropDownButton() + ->setLabel($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view')) + ->setIcon($this->iconFactory->getIcon('actions-cog')) + ->setShowLabelText(true); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/layout-module/toggle-hidden-element.js'); + $toggleHidden = $this->componentFactory->createDropDownGeneric(); + $toggleHidden->setTag('typo3-backend-page-layout-toggle-hidden'); + $toggleHiddenAttributes = ['count' => (string)$this->getNumberOfHiddenElements()]; + if ((bool)$this->moduleData->get('showHidden')) { + $toggleHiddenAttributes['active'] = 'active'; + } + $toggleHidden->setAttributes($toggleHiddenAttributes); + $viewSettingsButton->addItem($toggleHidden); + $view->addButtonToButtonBar($viewSettingsButton, ButtonBar::BUTTON_POSITION_RIGHT, 0); + } + + // Shortcut + $view->getDocHeaderComponent()->setShortcutContext( + 'web_layout', + sprintf( + '%s: %s [%d]', + $this->getLanguageService()->translate('short_description', 'backend.modules.layout'), + $this->pageContext->getPageTitle(), + $this->pageContext->pageId + ), + [ + 'id' => $this->pageContext->pageId, + 'showHidden' => (bool)$this->moduleData->get('showHidden'), + 'viewMode' => (int)$this->moduleData->get('viewMode'), + 'languages' => $this->pageContext->selectedLanguageIds, + ] + ); + } + + protected function initializeClipboard(ServerRequestInterface $request): void + { + $clipboard = GeneralUtility::makeInstance(Clipboard::class); + $clipboard->initializeClipboard($request); + $clipboard->lockToNormal(); + $clipboard->cleanCurrent(); + $clipboard->endClipboard(); + $elFromTable = $clipboard->elFromTable('tt_content'); + if (!empty($elFromTable) && $this->isContentEditable($this->pageContext->getPrimaryLanguageId())) { + $pasteItem = (int)substr((string)key($elFromTable), 11); + $pasteRecord = BackendUtility::getRecordWSOL('tt_content', $pasteItem); + $pasteTitle = BackendUtility::getRecordTitle('tt_content', $pasteRecord); + $this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/layout-module/paste.js') + ->instance([ + 'itemOnClipboardUid' => $pasteItem, + 'itemOnClipboardTitle' => $pasteTitle, + 'copyMode' => $clipboard->clipData['normal']['mode'] ?? '', + ]) + ); + } + } + + /** + * View Button + */ + protected function makeViewButton(): ?ButtonInterface + { + // Do not create a "View webpage" button if + // * Multiple languages are selected + // * record is a placeholder + // * not in "Columns" view, + if ( + $this->pageContext->hasMultipleLanguagesSelected() + || VersionState::tryFrom($this->pageContext->pageRecord['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER + || PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) !== PageViewMode::LayoutView + ) { + return null; + } + + $previewUriBuilder = PreviewUriBuilder::create($this->pageContext->pageRecord); + if (!$previewUriBuilder->isPreviewable()) { + return null; + } + + return $this->componentFactory->createViewButton($previewUriBuilder + ->withRootLine($this->pageContext->rootLine) + ->withLanguage($this->pageContext->getPrimaryLanguageId()) + ->buildDispatcherDataAttributes() ?? []); + } + + /** + * Edit Button + */ + protected function makeEditButton(ServerRequestInterface $request): ?ButtonInterface + { + $primaryLanguageId = $this->pageContext->getPrimaryLanguageId(); + if (!$this->isPageEditable($primaryLanguageId) + || PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) !== PageViewMode::LayoutView + ) { + return null; + } + + $pageUid = $this->pageContext->pageId; + if ($primaryLanguageId > 0 && ($overlayRecord = $this->pageContext->languageInformation->getTranslationRecord($primaryLanguageId)) !== null) { + $pageUid = $overlayRecord['uid']; + } + + $editParams = [ + 'edit' => ['pages' => [$pageUid => 'edit']], + 'module' => 'web_layout', + 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), + ]; + return $this->componentFactory->createGenericButton() + ->setTag('typo3-backend-contextual-record-edit-trigger') + ->setAttributes([ + 'url' => (string)$this->uriBuilder->buildUriFromRoute('record_edit_contextual', $editParams), + 'edit-url' => (string)$this->uriBuilder->buildUriFromRoute('record_edit', $editParams), + ]) + ->setLabel($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:editPageProperties')) + ->setShowLabelText(true) + ->setIcon($this->iconFactory->getIcon('actions-page-open', IconSize::SMALL)); + } + + /** + * Creates the language selector dropdown in the module toolbar. + */ + protected function createLanguageSelector(ModuleTemplate $view): void + { + if (count($this->pageContext->languageInformation->availableLanguages) <= 1) { + return; + } + + $viewMode = PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) ?? PageViewMode::LayoutView; + $isComparisonMode = $viewMode === PageViewMode::LanguageComparisonView; + $mode = $isComparisonMode ? LanguageSelectorMode::MULTI_SELECT : LanguageSelectorMode::SINGLE_SELECT; + + $languageSelector = $this->languageSelectorBuilder->build( + $this->pageContext, + $mode, + fn(array $languageIds): string => (string)$this->uriBuilder->buildUriFromRoute('web_layout', [ + 'id' => $this->pageContext->pageId, + 'viewMode' => $viewMode->value, + 'languages' => $languageIds, + ]), + $isComparisonMode && !empty($this->pageContext->languageInformation->existingTranslations) + ); + + $view->getDocHeaderComponent()->setLanguageSelector($languageSelector); + } + + /** + * Returns the number of hidden elements (including those hidden by start/end times) + * on the current page (for the current site language) + */ + protected function getNumberOfHiddenElements(): int + { + $isComparisonView = (PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) ?? PageViewMode::LayoutView) === PageViewMode::LanguageComparisonView; + $andWhere = []; + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content'); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)); + $queryBuilder + ->count('uid') + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq( + 'pid', + $queryBuilder->createNamedParameter($this->pageContext->pageId, Connection::PARAM_INT) + ) + ); + + $languageField = $this->schema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName(); + + // Build list of language IDs to include: always include -1 (all languages) and all selected languages + $languageIds = [-1]; + foreach ($this->pageContext->selectedLanguageIds as $languageId) { + $languageIds[] = $languageId; + // In comparison mode, also include default language (0) if not already selected + if ($isComparisonView && $languageId > 0 && !$this->pageContext->isDefaultLanguageSelected()) { + $languageIds[] = 0; + } + } + $languageIds = array_unique($languageIds); + + $queryBuilder->andWhere( + $queryBuilder->expr()->in( + $languageField, + $queryBuilder->createNamedParameter($languageIds, Connection::PARAM_INT_ARRAY) + ) + ); + + if ($this->schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) { + $andWhere[] = $queryBuilder->expr()->neq( + $this->schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName(), + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ); + } + if ($this->schema->hasCapability(TcaSchemaCapability::RestrictionStartTime)) { + $starttimeField = $this->schema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName(); + $andWhere[] = $queryBuilder->expr()->and( + $queryBuilder->expr()->neq( + $starttimeField, + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ), + $queryBuilder->expr()->gt( + $starttimeField, + $queryBuilder->createNamedParameter($GLOBALS['SIM_ACCESS_TIME'], Connection::PARAM_INT) + ) + ); + } + if ($this->schema->hasCapability(TcaSchemaCapability::RestrictionEndTime)) { + $endtimeField = $this->schema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName(); + $andWhere[] = $queryBuilder->expr()->and( + $queryBuilder->expr()->neq( + $endtimeField, + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ), + $queryBuilder->expr()->lte( + $endtimeField, + $queryBuilder->createNamedParameter($GLOBALS['SIM_ACCESS_TIME'], Connection::PARAM_INT) + ) + ); + } + if ($andWhere !== []) { + $queryBuilder->andWhere( + $queryBuilder->expr()->or(...$andWhere) + ); + } + $count = $queryBuilder + ->executeQuery() + ->fetchOne(); + return (int)$count; + } + + /** + * Check if page can be edited by current user. + */ + protected function isPageEditable(int $languageId): bool + { + if (empty($this->pageContext->pageRecord)) { + return false; + } + if ($this->schema->hasCapability(TcaSchemaCapability::AccessReadOnly)) { + return false; + } + $backendUser = $this->getBackendUser(); + if ($backendUser->isAdmin()) { + return true; + } + if ($this->schema->hasCapability(TcaSchemaCapability::AccessAdminOnly)) { + return false; + } + $isEditLocked = false; + if ($this->schema->hasCapability(TcaSchemaCapability::EditLock)) { + $isEditLocked = $this->pageContext->pageRecord[$this->schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false; + } + if ($isEditLocked) { + return false; + } + return $backendUser->doesUserHaveAccess($this->pageContext->pageRecord, Permission::PAGE_EDIT) + && $backendUser->checkLanguageAccess($languageId) + && $backendUser->check('tables_modify', 'pages'); + } + + /** + * Check if content can be edited by current user + */ + protected function isContentEditable(int $languageId): bool + { + if ($this->getBackendUser()->isAdmin()) { + return true; + } + $isEditLocked = false; + if ($this->schema->hasCapability(TcaSchemaCapability::EditLock)) { + $isEditLocked = $this->pageContext->pageRecord[$this->schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false; + } + if ($isEditLocked) { + return false; + } + return $this->getBackendUser()->doesUserHaveAccess($this->pageContext->pageRecord, Permission::CONTENT_EDIT) + && $this->getBackendUser()->check('tables_modify', 'tt_content') + && $this->getBackendUser()->checkLanguageAccess($languageId); + } + + /** + * Returns the target page if visible + */ + protected function getTargetPageIfVisible(array $targetPage): array + { + $fieldName = $this->schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName(); + return !($targetPage[$fieldName] ?? false) ? $targetPage : []; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/PageTsConfig/PageTsConfigActiveController.php b/Classes/Controller/PageTsConfig/PageTsConfigActiveController.php new file mode 100644 index 0000000..b0a1bf7 --- /dev/null +++ b/Classes/Controller/PageTsConfig/PageTsConfigActiveController.php @@ -0,0 +1,257 @@ + Active page TSconfig + * + * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API. + */ +#[AsController] +final readonly class PageTsConfigActiveController +{ + public function __construct( + private ContainerInterface $container, + private UriBuilder $uriBuilder, + private ModuleTemplateFactory $moduleTemplateFactory, + private TsConfigTreeBuilder $tsConfigTreeBuilder, + ) {} + + public function handleRequest(ServerRequestInterface $request): ResponseInterface + { + $backendUser = $this->getBackendUser(); + $languageService = $this->getLanguageService(); + + $queryParams = $request->getQueryParams(); + $parsedBody = $request->getParsedBody(); + + $currentModule = $request->getAttribute('module'); + $currentModuleIdentifier = $currentModule->getIdentifier(); + $moduleData = $request->getAttribute('moduleData'); + + $pageUid = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0); + $pageRecord = BackendUtility::readPageAccess($pageUid, '1=1') ?: []; + if (empty($pageRecord)) { + // Redirect to overview if page could not be determined. + // Edge case if page has been removed meanwhile. + BackendUtility::setUpdateSignal('updatePageTree'); + return new RedirectResponse($this->uriBuilder->buildUriFromRoute('pagetsconfig_pages')); + } + + // Force boolean toggles to bool and init further get/post vars + if ($moduleData->clean('displayConstantSubstitutions', [true, false])) { + $backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray()); + } + $displayConstantSubstitutions = $moduleData->get('displayConstantSubstitutions'); + if ($moduleData->clean('displayComments', [true, false])) { + $backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray()); + } + $displayComments = $moduleData->get('displayComments'); + if ($moduleData->clean('sortAlphabetically', [true, false])) { + $backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray()); + } + $sortAlphabetically = $moduleData->get('sortAlphabetically'); + + // Prepare site constants if any + $site = $request->getAttribute('site'); + $siteSettingsAst = null; + $siteSettingsFlat = []; + if ($site instanceof Site && !$site->getSettings()->isEmpty()) { + $siteSettings = $site->getSettings()->getAllFlat(); + $siteConstants = ''; + foreach ($siteSettings as $nodeIdentifier => $value) { + $siteConstants .= $nodeIdentifier . ' = ' . $value . LF; + } + $siteSettingsNode = new SiteInclude(); + $siteSettingsNode->setName('Site constants settings of site "' . $site->getIdentifier() . '"'); + $siteSettingsNode->setLineStream((new LosslessTokenizer())->tokenize($siteConstants)); + $siteSettingsTreeRoot = new RootInclude(); + $siteSettingsTreeRoot->addChild($siteSettingsNode); + $astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class); + $includeTreeTraverser = new IncludeTreeTraverser(); + $includeTreeTraverser->traverse($siteSettingsTreeRoot, [$astBuilderVisitor]); + $siteSettingsAst = $astBuilderVisitor->getAst(); + // Trigger unique identifier creation for entire tree + $siteSettingsAst->setIdentifier('pageTsConfig-siteSettingsAst'); + $siteSettingsFlat = $siteSettingsAst->flatten(); + if ($sortAlphabetically) { + // Traverse AST to sort if needed + $astTraverser = new AstTraverser(); + $astTraverser->traverse($siteSettingsAst, [new AstSortChildrenVisitor()]); + } + } + + // Base page TSconfig tree + $rootLine = BackendUtility::BEgetRootLine($pageUid, '', true); + ksort($rootLine); + $pagesTsConfigTree = $this->tsConfigTreeBuilder->getPagesTsConfigTree($rootLine, new LosslessTokenizer()); + + // Overload tree with user TSconfig if any + $userTsConfig = $backendUser->getUserTsConfig(); + if ($userTsConfig === null) { + throw new \RuntimeException('User TSconfig not initialized', 1674609098); + } + $userTsConfigAst = $userTsConfig->getUserTsConfigTree(); + $userTsConfigPageOverrides = ''; + // @todo: Ugly, similar in PageTsConfigFactory. + $userTsConfigFlat = $userTsConfigAst->flatten(); + foreach ($userTsConfigFlat as $userTsConfigIdentifier => $userTsConfigValue) { + if (str_starts_with($userTsConfigIdentifier, 'page.')) { + $userTsConfigPageOverrides .= substr($userTsConfigIdentifier, 5) . ' = ' . $userTsConfigValue . chr(10); + } + } + if (!empty($userTsConfigPageOverrides)) { + $includeNode = new TsConfigInclude(); + $includeNode->setName('pageTsConfig-overrides-by-userTsConfig'); + $includeNode->setLineStream((new LosslessTokenizer())->tokenize($userTsConfigPageOverrides)); + $pagesTsConfigTree->addChild($includeNode); + } + + // Set enabled conditions in page TSconfig include tree and let it handle constant substitutions in page TSconfig conditions. + $pageTsConfigConditions = $this->handleToggledPageTsConfigConditions($pagesTsConfigTree, $moduleData, $parsedBody, $siteSettingsFlat); + $conditionEnforcerVisitor = new IncludeTreeConditionEnforcerVisitor(); + $conditionEnforcerVisitor->setEnabledConditions(array_column(array_filter($pageTsConfigConditions, static fn(array $condition): bool => (bool)$condition['active']), 'value')); + $treeTraverser = new IncludeTreeTraverser(); + $treeTraverser->traverse($pagesTsConfigTree, [$conditionEnforcerVisitor]); + + // Create AST with constants from site and conditions + $includeTreeTraverser = new ConditionVerdictAwareIncludeTreeTraverser(); + $astBuilderVisitor = $this->container->get(IncludeTreeCommentAwareAstBuilderVisitor::class); + $astBuilderVisitor->setFlatConstants($siteSettingsFlat); + $includeTreeTraverser->traverse($pagesTsConfigTree, [$astBuilderVisitor]); + $pageTsConfigAst = $astBuilderVisitor->getAst(); + // Trigger unique identifier creation for entire tree + $pageTsConfigAst->setIdentifier('pageTsConfig'); + if ($sortAlphabetically) { + // Traverse AST to sort if needed + $astTraverser = new AstTraverser(); + $astTraverser->traverse($pageTsConfigAst, [new AstSortChildrenVisitor()]); + } + + $view = $this->moduleTemplateFactory->create($request); + $view->setTitle($languageService->sL($currentModule->getTitle()), $pageRecord['title'] ?? $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? ''); + $view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord); + $shortcutTitle = sprintf( + '%s: %s [%d]', + $languageService->translate('title', 'backend.modules.pagetsconfig_active'), + BackendUtility::getRecordTitle('pages', $pageRecord), + $pageUid + ); + $view->getDocHeaderComponent()->setShortcutContext( + $currentModuleIdentifier, + $shortcutTitle, + ['id' => $pageUid] + ); + $view->makeDocHeaderModuleMenu(['id' => $pageUid]); + $view->assignMultiple([ + 'pageUid' => $pageUid, + 'pageTitle' => $pageRecord['title'] ?? $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '', + 'displayConstantSubstitutions' => $displayConstantSubstitutions, + 'displayComments' => $displayComments, + 'sortAlphabetically' => $sortAlphabetically, + 'siteSettingsAst' => $siteSettingsAst, + 'pageTsConfigAst' => $pageTsConfigAst, + 'pageTsConfigConditions' => $pageTsConfigConditions, + 'pageTsConfigConditionsActiveCount' => count(array_filter($pageTsConfigConditions, static fn(array $condition): bool => (bool)$condition['active'])), + ]); + return $view->renderResponse('PageTsConfig/Active'); + } + + /** + * Align module data active page TSconfig conditions with toggled conditions from POST, + * write updated active conditions to user's module data if needed and + * prepare a list of active conditions for view. + */ + private function handleToggledPageTsConfigConditions(RootInclude $pageTsConfigTree, ModuleData $moduleData, ?array $parsedBody, array $flattenedConstants): array + { + $treeTraverser = new IncludeTreeTraverser(); + $treeTraverserVisitors = []; + $setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor(); + $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flattenedConstants); + $treeTraverserVisitors[] = $setupConditionConstantSubstitutionVisitor; + $conditionAggregatorVisitor = new IncludeTreeConditionAggregatorVisitor(); + $treeTraverserVisitors[] = $conditionAggregatorVisitor; + $treeTraverser->traverse($pageTsConfigTree, $treeTraverserVisitors); + $pageTsConfigConditions = $conditionAggregatorVisitor->getConditions(); + $conditionsFromPost = $parsedBody['pageTsConfigConditions'] ?? []; + $conditionsFromModuleData = array_flip((array)$moduleData->get('pageTsConfigConditions')); + $conditions = []; + foreach ($pageTsConfigConditions as $condition) { + $conditionHash = hash('xxh3', $condition['value']); + $conditionActive = array_key_exists($conditionHash, $conditionsFromModuleData); + // Note we're not feeding the post values directly to module data, but filter + // them through available conditions to prevent polluting module data with + // manipulated post values. + if (($conditionsFromPost[$conditionHash] ?? null) === '0') { + unset($conditionsFromModuleData[$conditionHash]); + $conditionActive = false; + } elseif (($conditionsFromPost[$conditionHash] ?? null) === '1') { + $conditionsFromModuleData[$conditionHash] = true; + $conditionActive = true; + } + $conditions[] = [ + 'value' => $condition['value'], + 'originalValue' => $condition['originalValue'], + 'hash' => $conditionHash, + 'active' => $conditionActive, + ]; + } + if ($conditionsFromPost) { + $moduleData->set('pageTsConfigConditions', array_keys($conditionsFromModuleData)); + $this->getBackendUser()->pushModuleData($moduleData->getModuleIdentifier(), $moduleData->toArray()); + } + return $conditions; + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/PageTsConfig/PageTsConfigIncludesController.php b/Classes/Controller/PageTsConfig/PageTsConfigIncludesController.php new file mode 100644 index 0000000..7ff16df --- /dev/null +++ b/Classes/Controller/PageTsConfig/PageTsConfigIncludesController.php @@ -0,0 +1,300 @@ + Included page TSconfig + * + * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API. + */ +#[AsController] +final readonly class PageTsConfigIncludesController +{ + public function __construct( + private UriBuilder $uriBuilder, + private ModuleTemplateFactory $moduleTemplateFactory, + private TsConfigTreeBuilder $tsConfigTreeBuilder, + private ResponseFactoryInterface $responseFactory, + private StreamFactoryInterface $streamFactory, + ) {} + + public function indexAction(ServerRequestInterface $request): ResponseInterface + { + $backendUser = $this->getBackendUser(); + $languageService = $this->getLanguageService(); + + $queryParams = $request->getQueryParams(); + + $currentModule = $request->getAttribute('module'); + $currentModuleIdentifier = $currentModule->getIdentifier(); + + $pageUid = (int)($queryParams['id'] ?? 0); + $pageRecord = BackendUtility::readPageAccess($pageUid, '1=1') ?: []; + if (empty($pageRecord)) { + // Redirect to records overview if page could not be determined. + // Edge case if page has been removed meanwhile. + BackendUtility::setUpdateSignal('updatePageTree'); + return new RedirectResponse($this->uriBuilder->buildUriFromRoute('pagetsconfig_pages')); + } + + // Prepare site constants if any + $site = $request->getAttribute('site'); + $siteSettingsTree = new RootInclude(); + if ($site instanceof Site && !$site->getSettings()->isEmpty()) { + $siteSettings = $site->getSettings()->getAllFlat(); + $siteConstants = ''; + foreach ($siteSettings as $nodeIdentifier => $value) { + $siteConstants .= $nodeIdentifier . ' = ' . $value . LF; + } + $siteSettingsNode = new SiteInclude(); + $siteSettingsNode->setName('Site constants settings of site "' . $site->getIdentifier() . '"'); + $siteSettingsNode->setLineStream((new LosslessTokenizer())->tokenize($siteConstants)); + $siteSettingsTree->addChild($siteSettingsNode); + $siteSettingsTree->setIdentifier('pageTsConfig-siteSettingsTree'); + } + + // Base page TSconfig tree + $rootLine = BackendUtility::BEgetRootLine($pageUid, '', true); + ksort($rootLine); + $pageTsConfigTree = $this->tsConfigTreeBuilder->getPagesTsConfigTree($rootLine, new LosslessTokenizer()); + + // Overload tree with user TSconfig if any + $userTsConfig = $backendUser->getUserTsConfig(); + if ($userTsConfig === null) { + throw new \RuntimeException('User TSconfig not initialized', 1675535278); + } + $userTsConfigAst = $userTsConfig->getUserTsConfigTree(); + $userTsConfigPageOverrides = ''; + // @todo: Ugly, similar in PageTsConfigFactory. + $userTsConfigFlat = $userTsConfigAst->flatten(); + foreach ($userTsConfigFlat as $userTsConfigIdentifier => $userTsConfigValue) { + if (str_starts_with($userTsConfigIdentifier, 'page.')) { + $userTsConfigPageOverrides .= substr($userTsConfigIdentifier, 5) . ' = ' . $userTsConfigValue . chr(10); + } + } + if (!empty($userTsConfigPageOverrides)) { + $includeNode = new TsConfigInclude(); + $includeNode->setName('pageTsConfig-overrides-by-userTsConfig'); + $includeNode->setLineStream((new LosslessTokenizer())->tokenize($userTsConfigPageOverrides)); + $pageTsConfigTree->addChild($includeNode); + } + $pageTsConfigTree->setIdentifier('pageTsConfig-pageTsConfigTree'); + + $treeTraverser = new IncludeTreeTraverser(); + $treeTraverserVisitors = []; + $syntaxScannerVisitor = new IncludeTreeSyntaxScannerVisitor(); + $treeTraverserVisitors[] = $syntaxScannerVisitor; + $treeTraverser->traverse($pageTsConfigTree, $treeTraverserVisitors); + + $view = $this->moduleTemplateFactory->create($request); + $view->setTitle($languageService->sL($currentModule->getTitle()), $pageRecord['title'] ?? $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? ''); + $view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord); + $shortcutTitle = sprintf( + '%s: %s [%d]', + $languageService->translate('title', 'backend.modules.pagetsconfig_includes'), + BackendUtility::getRecordTitle('pages', $pageRecord), + $pageUid + ); + $view->getDocHeaderComponent()->setShortcutContext( + $currentModuleIdentifier, + $shortcutTitle, + ['id' => $pageUid] + ); + $view->makeDocHeaderModuleMenu(['id' => $pageUid]); + $view->assignMultiple([ + 'pageUid' => $pageUid, + 'pageTitle' => $pageRecord['title'] ?? $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '', + 'siteSettingsTree' => $siteSettingsTree, + 'pageTsConfigTree' => $pageTsConfigTree, + 'syntaxErrors' => $syntaxScannerVisitor->getErrors(), + 'syntaxErrorCount' => count($syntaxScannerVisitor->getErrors()), + ]); + return $view->renderResponse('PageTsConfig/Includes'); + } + + public function sourceAction(ServerRequestInterface $request): ResponseInterface + { + $backendUser = $this->getBackendUser(); + $queryParams = $request->getQueryParams(); + $pageUid = (int)($queryParams['id'] ?? 0); + $type = $queryParams['includeType'] ?? null; + $includeIdentifier = $queryParams['identifier'] ?? null; + if ($pageUid === 0 || $includeIdentifier === null || !in_array($type, ['constants', 'setup'], true)) { + return $this->responseFactory->createResponse(400); + } + + if ($type === 'constants') { + // Prepare site constants if any + $site = $request->getAttribute('site'); + $includeTree = new RootInclude(); + if ($site instanceof Site && !$site->getSettings()->isEmpty()) { + $siteSettings = $site->getSettings()->getAllFlat(); + $siteConstants = ''; + foreach ($siteSettings as $nodeIdentifier => $value) { + $siteConstants .= $nodeIdentifier . ' = ' . $value . LF; + } + $siteSettingsNode = new SiteInclude(); + $siteSettingsNode->setName('Site constants settings of site "' . $site->getIdentifier() . '"'); + $siteSettingsNode->setLineStream((new LosslessTokenizer())->tokenize($siteConstants)); + $includeTree->addChild($siteSettingsNode); + $includeTree->setIdentifier('pageTsConfig-siteSettingsTree'); + } + } else { + // Base page TSconfig tree + $rootLine = BackendUtility::BEgetRootLine($pageUid, '', true); + ksort($rootLine); + $includeTree = $this->tsConfigTreeBuilder->getPagesTsConfigTree($rootLine, new LosslessTokenizer()); + + // Overload tree with user TSconfig if any + $userTsConfig = $backendUser->getUserTsConfig(); + if ($userTsConfig === null) { + throw new \RuntimeException('UserTsConfig not initialized', 1675535279); + } + $userTsConfigAst = $userTsConfig->getUserTsConfigTree(); + $userTsConfigPageOverrides = ''; + // @todo: Ugly, similar in PageTsConfigFactory. + $userTsConfigFlat = $userTsConfigAst->flatten(); + foreach ($userTsConfigFlat as $userTsConfigIdentifier => $userTsConfigValue) { + if (str_starts_with($userTsConfigIdentifier, 'page.')) { + $userTsConfigPageOverrides .= substr($userTsConfigIdentifier, 5) . ' = ' . $userTsConfigValue . chr(10); + } + } + if (!empty($userTsConfigPageOverrides)) { + $includeNode = new TsConfigInclude(); + $includeNode->setName('pageTsConfig-overrides-by-userTsConfig'); + $includeNode->setLineStream((new LosslessTokenizer())->tokenize($userTsConfigPageOverrides)); + $includeTree->addChild($includeNode); + } + $includeTree->setIdentifier('pageTsConfig-pageTsConfigTree'); + } + + $nodeFinderVisitor = new IncludeTreeNodeFinderVisitor(); + $nodeFinderVisitor->setNodeIdentifier($includeIdentifier); + $treeTraverser = new IncludeTreeTraverser(); + $treeTraverser->traverse($includeTree, [$nodeFinderVisitor]); + $lineStream = $nodeFinderVisitor->getFoundNode()?->getLineStream(); + if ($lineStream === null) { + return $this->responseFactory->createResponse(400); + } + + return $this->responseFactory + ->createResponse() + ->withHeader('Content-Type', 'text/plain') + ->withBody($this->streamFactory->createStream((string)$lineStream)); + } + + public function sourceWithIncludesAction(ServerRequestInterface $request): ResponseInterface + { + $backendUser = $this->getBackendUser(); + $queryParams = $request->getQueryParams(); + $pageUid = (int)($queryParams['id'] ?? 0); + $type = $queryParams['includeType'] ?? null; + $includeIdentifier = $queryParams['identifier'] ?? null; + if ($pageUid === 0 || $includeIdentifier === null || !in_array($type, ['constants', 'setup'], true)) { + return $this->responseFactory->createResponse(400); + } + + if ($type === 'constants') { + // Prepare site constants if any + $site = $request->getAttribute('site'); + $includeTree = new RootInclude(); + if ($site instanceof Site && !$site->getSettings()->isEmpty()) { + $siteSettings = $site->getSettings()->getAllFlat(); + $siteConstants = ''; + foreach ($siteSettings as $nodeIdentifier => $value) { + $siteConstants .= $nodeIdentifier . ' = ' . $value . LF; + } + $siteSettingsNode = new SiteInclude(); + $siteSettingsNode->setName('Site constants settings of site "' . $site->getIdentifier() . '"'); + $siteSettingsNode->setLineStream((new LosslessTokenizer())->tokenize($siteConstants)); + $includeTree->addChild($siteSettingsNode); + $includeTree->setIdentifier('pageTsConfig-siteSettingsTree'); + } + } else { + // Base page TSconfig tree + $rootLine = BackendUtility::BEgetRootLine($pageUid, '', true); + ksort($rootLine); + $includeTree = $this->tsConfigTreeBuilder->getPagesTsConfigTree($rootLine, new LosslessTokenizer()); + + // Overload tree with user TSconfig if any + $userTsConfig = $backendUser->getUserTsConfig(); + if ($userTsConfig === null) { + throw new \RuntimeException('UserTsConfig not initialized', 1675535280); + } + $userTsConfigAst = $userTsConfig->getUserTsConfigTree(); + $userTsConfigPageOverrides = ''; + // @todo: Ugly, similar in PageTsConfigFactory. + $userTsConfigFlat = $userTsConfigAst->flatten(); + foreach ($userTsConfigFlat as $userTsConfigIdentifier => $userTsConfigValue) { + if (str_starts_with($userTsConfigIdentifier, 'page.')) { + $userTsConfigPageOverrides .= substr($userTsConfigIdentifier, 5) . ' = ' . $userTsConfigValue . chr(10); + } + } + if (!empty($userTsConfigPageOverrides)) { + $includeNode = new TsConfigInclude(); + $includeNode->setName('pageTsConfig-overrides-by-userTsConfig'); + $includeNode->setLineStream((new LosslessTokenizer())->tokenize($userTsConfigPageOverrides)); + $includeTree->addChild($includeNode); + } + $includeTree->setIdentifier('pageTsConfig-pageTsConfigTree'); + } + + $sourceAggregatorVisitor = new IncludeTreeSourceAggregatorVisitor(); + $sourceAggregatorVisitor->setStartNodeIdentifier($includeIdentifier); + $treeTraverser = new IncludeTreeTraverser(); + $treeTraverser->traverse($includeTree, [$sourceAggregatorVisitor]); + $source = $sourceAggregatorVisitor->getSource(); + + return $this->responseFactory + ->createResponse() + ->withHeader('Content-Type', 'text/plain') + ->withBody($this->streamFactory->createStream($source)); + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/PageTsConfig/PageTsConfigRecordsOverviewController.php b/Classes/Controller/PageTsConfig/PageTsConfigRecordsOverviewController.php new file mode 100644 index 0000000..bcb337a --- /dev/null +++ b/Classes/Controller/PageTsConfig/PageTsConfigRecordsOverviewController.php @@ -0,0 +1,212 @@ + Page TSconfig Configuration + * + * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API. + */ +#[AsController] +final readonly class PageTsConfigRecordsOverviewController +{ + public function __construct( + private IconFactory $iconFactory, + private UriBuilder $uriBuilder, + private ModuleTemplateFactory $moduleTemplateFactory, + ) {} + + public function handleRequest(ServerRequestInterface $request): ResponseInterface + { + $backendUser = $this->getBackendUser(); + $currentModule = $request->getAttribute('module'); + $currentModuleIdentifier = $currentModule->getIdentifier(); + $pageId = (int)($request->getQueryParams()['id'] ?? 0); + $pageRecord = BackendUtility::readPageAccess($pageId, $backendUser->getPagePermsClause(Permission::PAGE_SHOW)) ?: []; + + $moduleData = $request->getAttribute('moduleData'); + if ($moduleData->cleanUp([])) { + $backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray()); + } + + $view = $this->moduleTemplateFactory->create($request); + + $view->setTitle( + $this->getLanguageService()->sL($currentModule->getTitle()), + $pageId !== 0 && isset($pageRecord['title']) ? $pageRecord['title'] : '' + ); + + // The page will show only if there is a valid page and if this page may be viewed by the user. + if ($pageRecord !== []) { + $view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord); + } + + $accessContent = false; + if (($pageId && $pageRecord !== []) || ($backendUser->isAdmin() && !$pageId)) { + $accessContent = true; + if (!$pageId && $backendUser->isAdmin()) { + $pageRecord = ['title' => '[root-level]', 'uid' => 0, 'pid' => 0]; + } + $view->assign('id', $pageId); + // Setting up the buttons and the module menu for the doc header + $view->getDocHeaderComponent()->setShortcutContext( + $currentModule->getIdentifier(), + $this->getLanguageService()->sL($currentModule->getTitle()), + ['id' => $pageId] + ); + } + $view->assign('accessContent', $accessContent); + $pagesUsingTSConfig = $this->getOverviewOfPagesUsingTSConfig($currentModule); + if (count($pagesUsingTSConfig) > 0) { + $view->assign('overviewOfPagesUsingTSConfig', $pagesUsingTSConfig); + } + + $view->makeDocHeaderModuleMenu(['id' => $pageId]); + return $view->renderResponse('PageTsConfig/RecordsOverview'); + } + + /** + * Renders table rows of all pages containing TSConfig together with its rootline + */ + private function getOverviewOfPagesUsingTSConfig(ModuleInterface $currentModule): array + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, 0)); + + $res = $queryBuilder + ->select('uid', 'TSconfig') + ->from('pages') + ->where( + $queryBuilder->expr()->neq( + 'TSconfig', + $queryBuilder->createNamedParameter('') + ), + $queryBuilder->expr()->eq( + 'sys_language_uid', + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ) + ) + ->executeQuery(); + + $pageArray = []; + + while ($row = $res->fetchAssociative()) { + $this->setInPageArray($pageArray, BackendUtility::BEgetRootLine($row['uid'], 'AND 1=1'), $row); + } + return $this->getList($currentModule, $pageArray); + } + + /** + * Builds a multidimensional array that reflects the page hierarchy. + */ + private function setInPageArray(array &$hierarchicArray, array $rootlineArray, array $row): void + { + ksort($rootlineArray); + if (!($rootlineArray[0]['uid'] ?? false)) { + array_shift($rootlineArray); + } + $currentElement = current($rootlineArray); + $hierarchicArray[$currentElement['uid']] = htmlspecialchars($currentElement['title']); + array_shift($rootlineArray); + if (!empty($rootlineArray)) { + if (!is_array($hierarchicArray[$currentElement['uid'] . '.'] ?? null)) { + $hierarchicArray[$currentElement['uid'] . '.'] = []; + } + $this->setInPageArray($hierarchicArray[$currentElement['uid'] . '.'], $rootlineArray, $row); + } else { + $hierarchicArray[$currentElement['uid'] . '_'] = $this->extractLinesFromTSConfig($row); + } + } + + /** + * Extract the lines of TSConfig from a given pages row. + */ + private function extractLinesFromTSConfig(array $row): array + { + $out = []; + $out['uid'] = $row['uid']; + $lines = GeneralUtility::trimExplode("\r\n", $row['TSconfig']); + $out['writtenLines'] = count($lines); + return $out; + } + + /** + * Recursive method to get the list of pages to show. + */ + private function getList(ModuleInterface $currentModule, array $pageArray, array $lines = [], int $pageDepth = 0): array + { + if ($pageArray === []) { + return $lines; + } + + foreach ($pageArray as $identifier => $title) { + if (!MathUtility::canBeInterpretedAsInteger($identifier)) { + continue; + } + $line = []; + $line['padding'] = ($pageDepth * 20); + $line['title'] = $identifier; + if (isset($pageArray[$identifier . '_'])) { + $line['link'] = $this->uriBuilder->buildUriFromRoute($currentModule->getIdentifier(), ['id' => $identifier]); + $line['icon'] = $this->iconFactory->getIconForRecord('pages', BackendUtility::getRecordWSOL('pages', $identifier), IconSize::SMALL)->render(); + $line['pageTitle'] = GeneralUtility::fixed_lgd_cs($title, 30); + $line['lines'] = ($pageArray[$identifier . '_']['writtenLines'] === 0 ? '' : $pageArray[$identifier . '_']['writtenLines']); + } else { + $line['link'] = ''; + $line['icon'] = $this->iconFactory->getIconForRecord('pages', BackendUtility::getRecordWSOL('pages', $identifier), IconSize::SMALL)->render(); + $line['pageTitle'] = GeneralUtility::fixed_lgd_cs($title, 30); + $line['lines'] = ''; + } + $lines[] = $line; + $lines = $this->getList($currentModule, $pageArray[$identifier . '.'] ?? [], $lines, $pageDepth + 1); + } + return $lines; + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/QrCodeController.php b/Classes/Controller/QrCodeController.php new file mode 100644 index 0000000..7ddd382 --- /dev/null +++ b/Classes/Controller/QrCodeController.php @@ -0,0 +1,158 @@ +getQueryParams()['content'] ?? ''; + if ($content === '') { + throw new \InvalidArgumentException('Content of the QR Code cannot be empty', 1762811499); + } + + $size = QrCodeSize::tryFrom((string)($request->getQueryParams()['size'] ?? '')) ?? QrCodeSize::MEDIUM; + $svg = $this->getQrCodeSvg($content, $size); + $response = $this->responseFactory->createResponse(); + + return $response + ->withHeader('Content-Type', 'image/svg+xml') + ->withBody($this->streamFactory->createStream($svg)); + } + + public function downloadAction(ServerRequestInterface $request): ResponseInterface + { + $body = $request->getParsedBody(); + $content = (string)($body['content'] ?? ''); + if ($content === '') { + throw new \InvalidArgumentException('Content of the QR Code cannot be empty', 1762811481); + } + + $size = QrCodeSize::tryFrom((string)($body['size'] ?? '')) ?? QrCodeSize::SMALL; + $format = (string)($body['format'] ?? 'svg'); + $svgContent = $this->getQrCodeSvg($content, $size); + $path = Environment::getVarPath() . '/transient/'; + GeneralUtility::mkdir_deep($path); + + $fileName = $path . 'qrcode-' . $size->getSize() . 'px-' . sha1($svgContent); + $svgFilePath = $fileName . '.svg'; + + if ($format === 'png') { + $pixelGraphicFilePath = $fileName . '.png'; + $this->createSvg($svgFilePath, $svgContent); + $filePath = $this->convertTo($svgFilePath, $pixelGraphicFilePath, 'png', $size); + } elseif ($format === 'svg') { + $filePath = $this->createSvg($svgFilePath, $svgContent); + } else { + throw new \InvalidArgumentException('The suffix "' . $format . '" is not supported.', 1762718268); + } + + return $this->sendFile($filePath, $format); + } + + /** + * Send file to the browser to download + */ + private function sendFile(string $filePath, string $format = 'svg'): ResponseInterface + { + $mimeType = $this->mimeTypeDetector->getMimeTypesForFileExtension($format); + $response = $this->responseFactory->createResponse(); + $fileContent = file_get_contents($filePath); + $response->getBody()->write($fileContent); + + return $response + ->withHeader('Content-Type', $mimeType[0] ?? 'image/svg+xml') + ->withHeader('Content-Disposition', 'attachment; filename="' . basename($filePath) . '"') + ->withHeader('Content-Length', (string)strlen($fileContent)); + } + + private function getQrCodeSvg(string $content, QrCodeSize $size = QrCodeSize::MEDIUM): string + { + $qrCodeRenderer = new ImageRenderer(new RendererStyle($size->getSize(), 2), new SvgImageBackEnd()); + + return (new Writer($qrCodeRenderer))->writeString($content); + } + + private function createSvg(string $file, string $content): string + { + if (file_exists($file)) { + return $file; + } + + if (!GeneralUtility::writeFile($file, $content, true)) { + throw new \RuntimeException('Unable to write file ' . $file, 1762718307); + } + + return $file; + } + + /** + * Create a pixel based image file from a given SVG file + */ + private function convertTo(string $sourcePath, string $targetPath, string $format, QrCodeSize $size = QrCodeSize::SMALL): string + { + if (file_exists($targetPath)) { + return $targetPath; + } + + $result = $this->graphicalFunctions->resize( + $sourcePath, + $format, + (string)$size->getSize(), + (string)$size->getSize(), + '', + [], + true + ); + + if ($result) { + $pngFile = $result->getRealPath(); + if ($pngFile && is_file($pngFile) && rename($pngFile, $targetPath)) { + return $targetPath; + } + } + + throw new \RuntimeException('Failed create ' . strtoupper($format) . ' ' . $targetPath . ' from SVG ' . $sourcePath . ' file.', 1762718351); + } +} diff --git a/Classes/Controller/RecordListController.php b/Classes/Controller/RecordListController.php new file mode 100644 index 0000000..6fce3d4 --- /dev/null +++ b/Classes/Controller/RecordListController.php @@ -0,0 +1,780 @@ + Records module: Rendering the listing of records on a page. + * + * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API. + */ +#[AsController] +class RecordListController +{ + protected PageContext $pageContext; + + protected string $table = ''; + protected string $searchTerm = ''; + protected string $returnUrl = ''; + protected array $modTSconfig = []; + protected ?ModuleData $moduleData = null; + protected bool $allowClipboard = true; + protected bool $allowSearch = true; + + public function __construct( + private readonly ComponentFactory $componentFactory, + protected readonly IconFactory $iconFactory, + protected readonly PageRenderer $pageRenderer, + protected readonly EventDispatcherInterface $eventDispatcher, + protected readonly UriBuilder $uriBuilder, + protected readonly ModuleTemplateFactory $moduleTemplateFactory, + protected readonly TcaSchemaFactory $tcaSchemaFactory, + protected readonly FlashMessageService $flashMessageService, + protected readonly PageContextFactory $pageContextFactory, + protected readonly LanguageSelectorBuilder $languageSelectorBuilder, + ) {} + + public function mainAction(ServerRequestInterface $request): ResponseInterface + { + + $pageContext = $request->getAttribute('pageContext'); + if (!$pageContext instanceof PageContext) { + throw new \RuntimeException( + 'PageContext not initialized by middleware.', + 1731415238 + ); + } + $this->pageContext = $pageContext; + $this->moduleData = $request->getAttribute('moduleData'); + + $languageService = $this->getLanguageService(); + $backendUser = $this->getBackendUserAuthentication(); + $parsedBody = $request->getParsedBody(); + $queryParams = $request->getQueryParams(); + + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/element/dispatch-modal-button.js'); + + BackendUtility::lockRecords(); + $pointer = max(0, (int)($parsedBody['pointer'] ?? $queryParams['pointer'] ?? 0)); + $this->table = (string)($parsedBody['table'] ?? $queryParams['table'] ?? ''); + $this->searchTerm = trim((string)($parsedBody['searchTerm'] ?? $queryParams['searchTerm'] ?? '')); + $this->returnUrl = GeneralUtility::sanitizeLocalUrl((string)($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? ''), $request); + $cmd = (string)($parsedBody['cmd'] ?? $queryParams['cmd'] ?? ''); + + // RecordList always requires default language (0) for proper record display + // Similar to PageLayoutController's comparison mode behavior + $languagesToDisplay = $this->pageContext->selectedLanguageIds; + if (!in_array(0, $languagesToDisplay, true)) { + $languagesToDisplay = array_merge([0], $languagesToDisplay); + // Create updated PageContext with modified languages and update request + $this->pageContext = $this->pageContextFactory->createWithLanguages( + $request, + $this->pageContext->pageId, + $languagesToDisplay, + $backendUser + ); + $request = $request->withAttribute('pageContext', $this->pageContext); + } + $this->moduleData->set('languages', $languagesToDisplay); + + $siteLanguages = $this->pageContext->site->getAvailableLanguages($backendUser, false, $this->pageContext->pageId); + $backendUser->pushModuleData($this->moduleData->getModuleIdentifier(), $this->moduleData->toArray()); + + // Loading module configuration, clean up settings, current page and page access + $this->modTSconfig = $this->pageContext->getModuleTsConfig('web_list'); + + // Check if Clipboard is allowed to be shown: + if (($this->modTSconfig['enableClipBoard'] ?? '') === 'activated') { + $this->moduleData->set('clipBoard', true); + $this->allowClipboard = false; + } elseif (($this->modTSconfig['enableClipBoard'] ?? '') === 'selectable') { + $this->allowClipboard = true; + } elseif (($this->modTSconfig['enableClipBoard'] ?? '') === 'deactivated') { + $this->moduleData->set('clipBoard', false); + $this->allowClipboard = false; + } + + // Check if SearchBox is allowed to be shown: + $this->allowSearch = !($this->modTSconfig['disableSearchBox'] ?? false); + + // Overwrite to show search on search request + if (!empty($this->searchTerm)) { + $this->allowSearch = true; + $this->moduleData->set('searchBox', true); + } + + // Get search levels from request or fall back to default, set in TSconifg + $search_levels = (int)($parsedBody['search_levels'] ?? $queryParams['search_levels'] ?? $this->modTSconfig['searchLevel']['default'] ?? 0); + + $dbList = GeneralUtility::makeInstance(DatabaseRecordList::class); + $dbList->setRequest($request); + $dbList->setModuleData($this->moduleData); + $dbList->calcPerms = $this->pageContext->pagePermissions; + $dbList->returnUrl = $this->returnUrl; + $dbList->showClipboardActions = true; + $dbList->disableSingleTableView = $this->modTSconfig['disableSingleTableView'] ?? false; + $dbList->listOnlyInSingleTableMode = $this->modTSconfig['listOnlyInSingleTableView'] ?? false; + $dbList->hideTables = $this->modTSconfig['hideTables'] ?? ''; + $dbList->hideTranslations = (string)($this->modTSconfig['hideTranslations'] ?? ''); + $dbList->tableTSconfigOverTCA = $this->modTSconfig['table'] ?? []; + $dbList->allowedNewTables = GeneralUtility::trimExplode(',', $this->modTSconfig['allowedNewTables'] ?? '', true); + $dbList->deniedNewTables = GeneralUtility::trimExplode(',', $this->modTSconfig['deniedNewTables'] ?? '', true); + $dbList->pageRow = $this->pageContext->pageRecord ?? []; + $dbList->modTSconfig = $this->modTSconfig; + $dbList->setLanguagesAllowedForUser($siteLanguages); + $clickTitleMode = trim($this->modTSconfig['clickTitleMode'] ?? ''); + $dbList->clickTitleMode = $clickTitleMode === '' ? 'edit' : $clickTitleMode; + if (isset($this->modTSconfig['tableDisplayOrder'])) { + $dbList->setTableDisplayOrder($this->modTSconfig['tableDisplayOrder']); + } + $clipboard = $this->initializeClipboard($request, (bool)$this->moduleData->get('clipBoard')); + $dbList->clipObj = $clipboard; + $additionalRecordListEvent = $this->eventDispatcher->dispatch(new RenderAdditionalContentToRecordListEvent($request)); + + $view = $this->moduleTemplateFactory->create($request); + + $tableListHtml = ''; + if ($this->pageContext->isAccessible() || ($this->pageContext->pageId === 0 && $search_levels !== 0 && $this->searchTerm !== '')) { + // If there is access to the page or root page is used for searching, then perform actions and render table list. + if ($cmd === 'delete' && $request->getMethod() === 'POST') { + $this->deleteRecords($request, $clipboard); + } + $dbList->start($this->pageContext->pageId, $this->table, $pointer, $this->searchTerm, $search_levels); + $tableListHtml = $dbList->generateList(); + } + + if (!$this->pageContext->pageId) { + $title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']; + } else { + $title = $this->pageContext->getPageTitle(); + } + $pageTranslationsHtml = ''; + if ($this->pageContext->pageId && !$this->searchTerm && !$cmd && !$this->table && $this->showPageTranslations()) { + // Show page translation table if there are any and display is allowed. + $pageTranslationsHtml = $this->renderPageTranslations($dbList, $siteLanguages); + } + $searchBoxHtml = ''; + if ($this->allowSearch && $this->moduleData->get('searchBox')) { + $searchBoxHtml = $this->renderSearchBox($request, $dbList, $this->searchTerm, $search_levels); + } + $clipboardHtml = ''; + if ($this->moduleData->get('clipBoard') && ($tableListHtml || $clipboard->hasElements())) { + $clipboardHtml = '
'; + } + + $view->setTitle($languageService->translate('title', 'backend.modules.list'), $title); + if (empty($tableListHtml)) { + $this->addNoRecordsFlashMessage($view, $this->table); + } + if ($this->pageContext->pageRecord) { + $view->getDocHeaderComponent()->setPageBreadcrumb($this->pageContext->pageRecord); + } + $this->getDocHeaderButtons($view, $clipboard, $request, $dbList); + $view->assignMultiple([ + 'pageId' => $this->pageContext->pageId, + 'pageTitle' => $title, + 'isPageEditable' => $this->isPageEditable(), + 'additionalContentTop' => $additionalRecordListEvent->getAdditionalContentAbove(), + 'pageTranslationsHtml' => $pageTranslationsHtml, + 'searchBoxHtml' => $searchBoxHtml, + 'tableListHtml' => $tableListHtml, + 'clipboardHtml' => $clipboardHtml, + 'additionalContentBottom' => $additionalRecordListEvent->getAdditionalContentBelow(), + ]); + return $view->renderResponse('RecordList'); + } + + public function toggleRecordVisibilityAction(ServerRequestInterface $request): ResponseInterface + { + $table = $request->getParsedBody()['table'] ?? null; + $uid = $request->getParsedBody()['uid'] ?? null; + $action = $request->getParsedBody()['action'] ?? null; + + try { + if (!isset($action, $table, $uid)) { + throw new BadRequestException('Any of the mandatory argument "table", "uid", "action" is missing', 1729161415); + } + + if ($action !== 'show' && $action !== 'hide') { + throw new BadRequestException(sprintf('Passed "action" value must be either "show" or "hide", "%s" given', $action), 1729161479); + } + + if (!$this->tcaSchemaFactory->has($table)) { + throw new BadRequestException(sprintf('Cannot execute action for non-existent table "%s"', $table), 1738593519); + } + + $schema = $this->tcaSchemaFactory->get($table); + if (!$schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) { + throw new \InvalidArgumentException(sprintf('TCA table "%s" does not support record visibility', $table), 1729166628); + } + + if (!$this->getBackendUserAuthentication()->check('tables_modify', $table)) { + throw new BadRequestException(sprintf('User has no modify access to table "%s"', $table), 1739376254); + } + + $record = BackendUtility::getRecord($table, $uid, 'uid,pid'); + if ($record === null) { + throw new BadRequestException(sprintf('A record with uid %d was not found', $uid), 1739376253); + } + + $pid = $table === 'pages' ? (int)$record['uid'] : (int)$record['pid']; + $rootLevelCapability = $schema->hasCapability(TcaSchemaCapability::RestrictionRootLevel) ? $schema->getCapability(TcaSchemaCapability::RestrictionRootLevel) : null; + if ($pid !== 0 || !$rootLevelCapability || !$rootLevelCapability->shallIgnoreRootLevelRestriction()) { + if (!BackendUtility::readPageAccess($pid, $this->getBackendUserAuthentication()->getPagePermsClause(Permission::PAGE_SHOW))) { + throw new BadRequestException(sprintf('User has no access to record with uid %d', $uid), 1739376255); + } + } + + $hiddenField = $schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName(); + + $dataHandlerDataMap = [ + $table => [ + $uid => [ + $hiddenField => $action === 'show' ? 0 : 1, + ], + ], + ]; + + /** @var DataHandler $dataHandler */ + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $dataHandler->start($dataHandlerDataMap, []); + $dataHandler->process_datamap(); + + // Prints errors (= write them to the message queue) + $dataHandler->printLogErrorMessages(); + + $response = [ + 'messages' => [], + 'hasErrors' => false, + ]; + + // Basically the same as in \TYPO3\CMS\Backend\RecordList\DatabaseRecordList->getFieldsToSelect() + $selectFields = []; + $selectFields[] = 'uid'; + $selectFields[] = 'pid'; + $selectFields[] = $schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName(); + + if ($table === 'pages') { + $selectFields[] = 'module'; + $selectFields[] = 'extendToSubpages'; + $selectFields[] = 'nav_hide'; + $selectFields[] = 'doktype'; + $selectFields[] = 'shortcut'; + $selectFields[] = 'shortcut_mode'; + $selectFields[] = 'mount_pid'; + $selectFields[] = 'is_siteroot'; + } + + $row = BackendUtility::getRecord($table, $uid, $selectFields); + if ($row !== null) { + // Get new record icon + $recordIcon = $this->iconFactory->getIconForRecord($table, $row, IconSize::SMALL); + + $response['icon'] = $recordIcon->render(); + $response['isVisible'] = (int)$row[$hiddenField] === 0; + } + + $messages = $this->flashMessageService->getMessageQueueByIdentifier()->getAllMessagesAndFlush(); + foreach ($messages as $message) { + $response['messages'][] = [ + 'title' => $message->getTitle(), + 'message' => $message->getMessage(), + 'severity' => $message->getSeverity(), + ]; + if ($message->getSeverity() === ContextualFeedbackSeverity::ERROR) { + $response['hasErrors'] = true; + } + } + } catch (VerificationRequiredException $e) { + // Handled by Middleware/SudoModeInterceptor + throw $e; + } catch (\Throwable $e) { + // @todo: having this explicit handling here sucks + $response = [ + 'messages' => [ + [ + 'title' => 'An exception occurred', + 'message' => $e->getMessage(), + 'severity' => ContextualFeedbackSeverity::ERROR, + ], + ], + 'hasErrors' => true, + ]; + } + + return new JsonResponse($response, $response['hasErrors'] ? 400 : 200); + } + + /** + * Process incoming data and configure the clipboard. + */ + protected function initializeClipboard(ServerRequestInterface $request, bool $isClipboardShown): Clipboard + { + $clipboard = GeneralUtility::makeInstance(Clipboard::class); + $cmd = (string)($request->getParsedBody()['cmd'] ?? $request->getQueryParams()['cmd'] ?? ''); + // Initialize - reads the clipboard content from the user session + $clipboard->initializeClipboard($request); + // Clipboard actions are handled: + $clipboardCommandArray = array_replace_recursive($request->getQueryParams()['CB'] ?? [], $request->getParsedBody()['CB'] ?? []); + if ($cmd === 'copyMarked' || $cmd === 'removeMarked') { + // Get CBC from request, and map the element values (true => copy, false => remove) + $CBC = array_map(static fn(): bool => ($cmd === 'copyMarked'), (array)($request->getParsedBody()['CBC'] ?? [])); + $cmd_table = (string)($request->getParsedBody()['cmd_table'] ?? $request->getQueryParams()['cmd_table'] ?? ''); + // Cleanup CBC + $clipboardCommandArray['el'] = $clipboard->cleanUpCBC($CBC, $cmd_table); + } + if (!$isClipboardShown) { + // If the clipboard is NOT shown, set the pad to 'normal'. + $clipboardCommandArray['setP'] = 'normal'; + } + // Execute commands. + $clipboard->setCmd($clipboardCommandArray); + // Clean up pad + $clipboard->cleanCurrent(); + // Save the clipboard content + $clipboard->endClipboard(); + return $clipboard; + } + + protected function deleteRecords(ServerRequestInterface $request, Clipboard $clipboard): void + { + // This is the 'delete' button in table header with multi record selection. + // The clipboard object is used to clean up the submitted entries to only the selected table. + $parsedBody = $request->getParsedBody(); + $items = $clipboard->cleanUpCBC((array)($parsedBody['CBC'] ?? []), (string)($parsedBody['cmd_table'] ?? ''), true); + if (!empty($items)) { + // Create data handler command array + $dataHandlerCmd = []; + foreach ($items as $iK => $value) { + $iKParts = explode('|', (string)$iK); + $dataHandlerCmd[$iKParts[0]][$iKParts[1]]['delete'] = 1; + } + $tce = GeneralUtility::makeInstance(DataHandler::class); + $tce->start([], $dataHandlerCmd); + $tce->process_cmdmap(); + if (isset($dataHandlerCmd['pages'])) { + BackendUtility::setUpdateSignal('updatePageTree'); + } + $tce->printLogErrorMessages(); + } + } + + protected function renderSearchBox(ServerRequestInterface $request, DatabaseRecordList $dbList, string $searchWord, int $searchLevels): string + { + $searchBox = GeneralUtility::makeInstance(RecordSearchBoxComponent::class) + ->setAllowedSearchLevels((array)($this->modTSconfig['searchLevel']['items'] ?? [])) + ->setSearchWord($searchWord) + ->setSearchLevel($searchLevels) + ->render($request, $dbList->listURL('', null, 'pointer,searchTerm')); + return $searchBox; + } + + /** + * Create the panel of buttons for submitting the form or otherwise perform operations. + */ + protected function getDocHeaderButtons(ModuleTemplate $view, Clipboard $clipboard, ServerRequestInterface $request, DatabaseRecordList $dbList): void + { + $queryParams = $request->getQueryParams(); + $lang = $this->getLanguageService(); + + // Language selector (top right area) + $this->createLanguageSelector($view, $request); + + if (!($this->modTSconfig['noCreateRecordsLink'] ?? false) && $this->editLockPermissions()) { + if ($this->table === '') { + // "General" new record button if: not in single table view, not disabled via TSconfig and page is not 'edit locked' + $newRecordButton = $this->componentFactory->createLinkButton() + ->setHref((string)$this->uriBuilder->buildUriFromRoute('db_new', ['id' => $this->pageContext->pageId, 'returnUrl' => $dbList->listURL()])) + ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:newRecordGeneral')) + ->setShowLabelText(true) + ->setIcon($this->iconFactory->getIcon('actions-plus', IconSize::SMALL)); + $view->addButtonToButtonBar($newRecordButton, ButtonBar::BUTTON_POSITION_LEFT, 10); + } elseif (($createNewRecordButton = $dbList->createActionButtonNewRecord($this->table)) !== null) { + // In single table view, render the specific create new button + $view->addButtonToButtonBar($createNewRecordButton); + } + } + + if ($this->pageContext->isAccessible() && $this->pageContext->pageId > 0) { + $uriBuilder = PreviewUriBuilder::create($this->pageContext->pageRecord); + if ($uriBuilder->isPreviewable()) { + $view->addButtonToButtonBar( + $this->componentFactory->createViewButton(PreviewUriBuilder::create($this->pageContext->pageRecord) + ->withRootLine($this->pageContext->rootLine) + ->buildDispatcherDataAttributes() ?? []), + ButtonBar::BUTTON_POSITION_LEFT, + 15 + ); + + // QR Code button + $fallbackUri = $uriBuilder + ->withRootLine($this->pageContext->rootLine) + ->buildUri(); + $previewUri = $this->componentFactory->getPreviewUrlForQrCode( + $this->pageContext->pageId, + $this->pageContext->getPrimaryLanguageId(), + $fallbackUri + ); + if ($previewUri !== null) { + $view->addButtonToButtonBar( + $this->componentFactory->createQrCodeButton($previewUri), + ButtonBar::BUTTON_POSITION_LEFT, + 15 + ); + } + } + // If edit permissions are set, see BackendUserAuthentication + if ($this->isPageEditable()) { + // Edit + $editLink = $this->uriBuilder->buildUriFromRoute('record_edit', [ + 'edit' => [ + 'pages' => [ + $this->pageContext->pageId => 'edit', + ], + ], + 'module' => 'records', + 'returnUrl' => $dbList->listURL(), + ]); + $editButton = $this->componentFactory->createLinkButton() + ->setHref((string)$editLink) + ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:editPage')) + ->setShowLabelText(true) + ->setIcon($this->iconFactory->getIcon('actions-page-open', IconSize::SMALL)); + $view->addButtonToButtonBar($editButton, ButtonBar::BUTTON_POSITION_LEFT, 20); + } + } + + // Paste + if (($this->pageContext->pagePermissions->createPagePermissionIsGranted() || $this->pageContext->pagePermissions->editContentPermissionIsGranted()) && $this->editLockPermissions()) { + $elFromTable = $clipboard->elFromTable(); + if (!empty($elFromTable)) { + $confirmMessage = $clipboard->confirmMsgText('pages', $this->pageContext->pageRecord, 'into', CountMode::ALL); + $pasteButton = $this->componentFactory->createLinkButton() + ->setHref($clipboard->pasteUrl('', $this->pageContext->pageId)) + ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:clip_paste')) + ->setClasses('t3js-modal-trigger') + ->setDataAttributes([ + 'severity' => 'warning', + 'content' => $confirmMessage, + 'title' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:clip_paste'), + ]) + ->setIcon($this->iconFactory->getIcon('actions-document-paste-into', IconSize::SMALL)) + ->setShowLabelText(true); + $view->addButtonToButtonBar($pasteButton, ButtonBar::BUTTON_POSITION_LEFT, 40); + } + } + // Cache + if ($this->pageContext->pageId) { + $clearCacheButton = $this->componentFactory->createGenericButton() + ->setTag('button') + ->setLabel($lang->sL('core.cache:page.label')) + ->setClasses('t3js-clear-page-cache') + ->setAttributes(['type' => 'button', 'data-id' => (string)$this->pageContext->pageId]) + ->setIcon($this->iconFactory->getIcon('actions-system-cache-clear', IconSize::SMALL)); + $view->addButtonToButtonBar($clearCacheButton, ButtonBar::BUTTON_POSITION_RIGHT); + } + if ($this->table + && !($this->modTSconfig['noExportRecordsLinks'] ?? false) + && $this->getBackendUserAuthentication()->isExportEnabled() + ) { + // Export + if (ExtensionManagementUtility::isLoaded('impexp')) { + $url = (string)$this->uriBuilder->buildUriFromRoute('tx_impexp_export', ['tx_impexp' => ['list' => [$this->table . ':' . $this->pageContext->pageId]]]); + $exportButton = $this->componentFactory->createLinkButton() + ->setHref($url) + ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.export')) + ->setIcon($this->iconFactory->getIcon('actions-document-export-t3d', IconSize::SMALL)) + ->setShowLabelText(true); + $view->addButtonToButtonBar($exportButton, ButtonBar::BUTTON_POSITION_LEFT, 50); + } + } + // ViewMode + $viewModeItems = []; + if ($this->allowSearch) { + $viewModeItems[] = $this->componentFactory->createDropDownToggle() + ->setActive((bool)$this->moduleData->get('searchBox')) + ->setHref($this->createModuleUri($request, ['searchBox' => $this->moduleData->get('searchBox') ? 0 : 1, 'searchTerm' => ''])) + ->setLabel($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.showSearch')) + ->setIcon($this->iconFactory->getIcon('actions-search')); + } + if ($this->allowClipboard) { + $viewModeItems[] = $this->componentFactory->createDropDownToggle() + ->setActive((bool)$this->moduleData->get('clipBoard')) + ->setHref($this->createModuleUri($request, ['clipBoard' => $this->moduleData->get('clipBoard') ? 0 : 1])) + ->setLabel($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.showClipboard')) + ->setIcon($this->iconFactory->getIcon('actions-clipboard')); + } + if (!empty($viewModeItems)) { + $viewModeButton = $this->componentFactory->createDropDownButton() + ->setLabel($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view')) + ->setIcon($this->iconFactory->getIcon('actions-cog')) + ->setShowLabelText(true); + foreach ($viewModeItems as $viewModeItem) { + $viewModeButton->addItem($viewModeItem); + } + $view->addButtonToButtonBar($viewModeButton, ButtonBar::BUTTON_POSITION_RIGHT, 0); + } + + // Shortcut + $arguments = [ + 'id' => $this->pageContext->pageId, + ]; + $potentialArguments = [ + 'pointer', + 'table', + 'searchTerm', + 'search_levels', + 'sortField', + 'sortRev', + ]; + foreach ($potentialArguments as $argument) { + if (!empty($queryParams[$argument])) { + $arguments[$argument] = $queryParams[$argument]; + } + } + $view->getDocHeaderComponent()->setShortcutContext('records', $this->getShortcutTitle($arguments), $arguments); + + // Back + if ($this->returnUrl) { + $view->addButtonToButtonBar($this->componentFactory->createBackButton($this->returnUrl)); + } + } + + protected function addNoRecordsFlashMessage(ModuleTemplate $view, string $table) + { + $languageService = $this->getLanguageService(); + if ($table && $this->tcaSchemaFactory->has($table) && $this->tcaSchemaFactory->get($table)->getTitle() !== '') { + $message = sprintf( + $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:noRecordsOfTypeOnThisPage'), + $this->tcaSchemaFactory->get($table)->getTitle($languageService->sL(...)) + ); + } else { + $message = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:noRecordsOnThisPage'); + } + $view->addFlashMessage($message, '', ContextualFeedbackSeverity::INFO); + } + + /** + * Check whether the current backend user is an admin or the current page is locked by edit lock. + */ + protected function editLockPermissions(): bool + { + return $this->getBackendUserAuthentication()->isAdmin() + || !($schema = $this->tcaSchemaFactory->get('pages'))->hasCapability(TcaSchemaCapability::EditLock) + || !($this->pageContext->pageRecord[$schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false); + } + + /** + * Returns the shortcut title for the current page. + */ + protected function getShortcutTitle(array $arguments): string + { + $tableTitle = ''; + $languageService = $this->getLanguageService(); + if (isset($arguments['table'])) { + $tableName = $arguments['table']; + if ($this->tcaSchemaFactory->has($tableName)) { + $schema = $this->tcaSchemaFactory->get($tableName); + $tableTitle = $schema->getTitle($languageService->sL(...)); + } + $tableTitle = ': ' . ($tableTitle ?: $tableName); + } + return trim(sprintf( + $languageService->translate('shortcut.title', 'backend.messages'), + $languageService->translate('title', 'backend.modules.list'), + $tableTitle, + $this->pageContext->getPageTitle(), + $this->pageContext->pageId + )); + } + + protected function showPageTranslations(): bool + { + if (!$this->getBackendUserAuthentication()->check('tables_select', 'pages')) { + return false; + } + if (isset($this->modTSconfig['table']['pages']['hideTable'])) { + return !$this->modTSconfig['table']['pages']['hideTable']; + } + $schema = $this->tcaSchemaFactory->get('pages'); + $hideTables = $this->modTSconfig['hideTables'] ?? ''; + return !$schema->hasCapability(TcaSchemaCapability::HideInUi) + && $hideTables !== '*' + && !in_array('pages', GeneralUtility::trimExplode(',', $hideTables), true); + } + + protected function renderPageTranslations(DatabaseRecordList $dbList, array $siteLanguages): string + { + $pageTranslationsDatabaseRecordList = clone $dbList; + $pageTranslationsDatabaseRecordList->id = $this->pageContext->pageId; + $pageTranslationsDatabaseRecordList->listOnlyInSingleTableMode = false; + $pageTranslationsDatabaseRecordList->disableSingleTableView = true; + $pageTranslationsDatabaseRecordList->deniedNewTables = ['pages']; + $pageTranslationsDatabaseRecordList->hideTranslations = ''; + $pageTranslationsDatabaseRecordList->setLanguagesAllowedForUser($siteLanguages); + $pageTranslationsDatabaseRecordList->showOnlyTranslatedRecords(true); + return $pageTranslationsDatabaseRecordList->getTable('pages'); + } + + protected function createModuleUri(ServerRequestInterface $request, array $params = []): string + { + $params = array_replace_recursive([ + 'id' => $this->pageContext->pageId, + 'table' => $this->table, + 'searchTerm' => $this->searchTerm, + ], $params); + + $params = array_filter($params, static function (mixed $value): bool { + return $value !== null && trim((string)$value) !== ''; + }); + + return (string)$this->uriBuilder->buildUriFromRequest($request, $params); + } + + /** + * Creates the language selector dropdown in the module toolbar. + */ + protected function createLanguageSelector(ModuleTemplate $view, ServerRequestInterface $request): void + { + if (count($this->pageContext->languageInformation->availableLanguages) <= 1) { + return; + } + + $languageSelector = $this->languageSelectorBuilder->build( + $this->pageContext, + LanguageSelectorMode::MULTI_SELECT, + fn(array $languageIds): string => $this->buildListUrl($request, ['languages' => $languageIds]), + !empty($this->pageContext->languageInformation->existingTranslations) + ); + + $view->getDocHeaderComponent()->setLanguageSelector($languageSelector); + } + + /** + * Check if page can be edited by current user + */ + protected function isPageEditable(): bool + { + $schema = $this->tcaSchemaFactory->get('pages'); + + if ($schema->hasCapability(TcaSchemaCapability::AccessReadOnly)) { + return false; + } + $backendUser = $this->getBackendUserAuthentication(); + if ($backendUser->isAdmin()) { + return true; + } + if ($schema->hasCapability(TcaSchemaCapability::AccessAdminOnly)) { + return false; + } + + return !empty($this->pageContext->pageRecord) + && $this->editLockPermissions() + && $this->pageContext->pagePermissions->editPagePermissionIsGranted() + && $backendUser->checkLanguageAccess(0) + && $backendUser->check('tables_modify', 'pages'); + } + + /** + * Build list URL preserving relevant parameters (search, table, sort). + * Does NOT preserve pagination (pointer) to allow resetting to first page. + * + * @param array $additionalParams Additional parameters to add/override (e.g., ['languages' => [0, 1]]) + */ + protected function buildListUrl(ServerRequestInterface $request, array $additionalParams = []): string + { + $queryParams = $request->getQueryParams(); + $parsedBody = $request->getParsedBody(); + + $urlParams = [ + 'id' => $this->pageContext->pageId, + ]; + + if ($this->table !== '') { + $urlParams['table'] = $this->table; + } + if ($this->searchTerm !== '') { + $urlParams['searchTerm'] = $this->searchTerm; + } + $searchLevels = (int)($parsedBody['search_levels'] ?? $queryParams['search_levels'] ?? 0); + if ($searchLevels > 0) { + $urlParams['search_levels'] = $searchLevels; + } + $sortField = (string)($parsedBody['sortField'] ?? $queryParams['sortField'] ?? ''); + if ($sortField !== '') { + $urlParams['sortField'] = $sortField; + } + $sortRev = $parsedBody['sortRev'] ?? $queryParams['sortRev'] ?? null; + if ($sortRev !== null) { + $urlParams['sortRev'] = $sortRev; + } + + // Merge with additional parameters (which can override preserved ones) + $urlParams = array_merge($urlParams, $additionalParams); + + return (string)$this->uriBuilder->buildUriFromRoute('records', $urlParams); + } + + protected function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/RecordListDownloadController.php b/Classes/Controller/RecordListDownloadController.php new file mode 100644 index 0000000..8ac644a --- /dev/null +++ b/Classes/Controller/RecordListDownloadController.php @@ -0,0 +1,412 @@ + [ + 'options' => [ + 'delimiter' => [ + 'comma' => ',', + 'semicolon' => ';', + 'pipe' => '|', + ], + 'quote' => [ + 'doublequote' => '"', + 'singlequote' => '\'', + 'space' => ' ', + ], + ], + 'defaults' => [ + 'delimiter' => ',', + 'quote' => '"', + ], + ], + 'json' => [ + 'options' => [ + 'meta' => [ + 'full' => 'full', + 'prefix' => 'prefix', + 'none' => 'none', + ], + ], + 'defaults' => [ + 'meta' => 'prefix', + ], + ], + ]; + + protected int $id = 0; + protected string $table = ''; + protected string $format = ''; + protected string $filename = ''; + protected array $modTSconfig = []; + + public function __construct( + protected readonly ResponseFactoryInterface $responseFactory, + protected readonly BackendViewFactory $backendViewFactory, + protected readonly EventDispatcherInterface $eventDispatcher, + protected readonly TcaSchemaFactory $tcaSchemaFactory, + ) {} + + /** + * Handle record download request by evaluating the provided arguments, + * checking access, initializing the record list, fetching records and + * finally calling the requested download format action (e.g. csv). + */ + public function handleDownloadRequest(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + + $this->table = (string)($parsedBody['table'] ?? ''); + if ($this->table === '') { + throw new \RuntimeException('No table was given for downloading records', 1623941276); + } + + $backendUser = $this->getBackendUserAuthentication(); + if (!$backendUser->check('tables_select', $this->table)) { + throw new AccessDeniedException('Insufficient permissions for accessing this download', 1756895674); + } + + // @todo we might want to throw an exception in case no schema exists for the table + $schema = $this->tcaSchemaFactory->has($this->table) ? $this->tcaSchemaFactory->get($this->table) : null; + $this->format = (string)($parsedBody['format'] ?? ''); + if ($this->format === '' || !isset(self::DOWNLOAD_FORMATS[$this->format])) { + throw new \RuntimeException('No or an invalid download format given', 1624562166); + } + + $this->filename = $this->generateFilename((string)($parsedBody['filename'] ?? '')); + $this->id = (int)($parsedBody['id'] ?? 0); + + // Loading module configuration + $this->modTSconfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_list.'] ?? []; + + // Loading TCEFORM for the table + $tsConfig = BackendUtility::getPagesTSconfig($this->id)['TCEFORM.'][$this->table . '.'] ?? null; + $tsConfig = is_array($tsConfig) ? $tsConfig : null; + + // Loading current page record and checking access + $perms_clause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW); + $pageinfo = BackendUtility::readPageAccess($this->id, $perms_clause); + $searchString = (string)($parsedBody['searchString'] ?? ''); + $searchLevels = (int)($parsedBody['searchLevels'] ?? $this->modTSconfig['searchLevel.']['default'] ?? 0); + if (!is_array($pageinfo) && !($this->id === 0 && $searchString !== '' && $searchLevels !== 0)) { + throw new AccessDeniedException('Insufficient permissions for accessing this download', 1623941361); + } + $rawValues = (bool)($parsedBody['rawValues'] ?? false); + + // Initialize database record list + $recordList = GeneralUtility::makeInstance(DatabaseRecordList::class); + $recordList->setRequest($request); + $recordList->modTSconfig = $this->modTSconfig; + $recordList->setLanguagesAllowedForUser($this->getSiteLanguages($request)); + $recordList->start($this->id, $this->table, 0, $searchString, $searchLevels); + $selectedPreset = (string)($parsedBody['preset'] ?? ''); + if (($parsedBody['allColumns'] ?? false) || $selectedPreset !== '') { + // Overwrite setFields in case all allowed columns should be included, + // or a preset is selected (that is only allowed to pick from the maximum + // allowed set of columns). + $recordList->setFields[$this->table] = BackendUtility::getAllowedFieldsForTable($this->table); + } + $columnsToRender = $recordList->getColumnsToRender($this->table, false, $selectedPreset); + + $hideTranslations = ($this->modTSconfig['hideTranslations'] ?? '') === '*' + || GeneralUtility::inList($this->modTSconfig['hideTranslations'] ?? '', $this->table); + + // Initialize the downloader + $downloader = GeneralUtility::makeInstance( + DownloadRecordList::class, + $recordList, + GeneralUtility::makeInstance(TranslationConfigurationProvider::class), + $this->tcaSchemaFactory, + ); + + // Fetch and process the header row and the records + $headerRow = $downloader->getHeaderRow($columnsToRender); + if (!$rawValues) { + foreach ($headerRow as &$headerField) { + $label = $schema?->hasField($headerField) ? $schema->getField($headerField)->getLabel() : null; + if ($label !== null) { + $headerField = rtrim(trim($this->getLanguageService()->translateLabel($tsConfig[$headerField . '.']['label.'] ?? [], $tsConfig[$headerField . '.']['label'] ?? $label)), ':'); + } elseif ($specialLabel = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $headerField)) { + // Special label exists for this field (Probably a management field, e.g. sorting) + $headerField = $specialLabel; + } + } + unset($headerField); + } + $records = $downloader->getRecords( + $this->table, + $columnsToRender, + $this->getBackendUserAuthentication(), + $hideTranslations, + $rawValues + ); + + $event = $this->eventDispatcher->dispatch( + new BeforeRecordDownloadIsExecutedEvent( + $headerRow, + $records, + $request, + $this->table, + $this->format, + $this->filename, + $this->id, + $this->modTSconfig, + $columnsToRender, + $hideTranslations, + ) + ); + + $downloadAction = $this->format . 'DownloadAction'; + return $this->{$downloadAction}($request, $event->getHeaderRow(), $event->getRecords()); + } + + /** + * Generate settings form for the download request + */ + public function downloadSettingsAction(ServerRequestInterface $request): ResponseInterface + { + $downloadArguments = $request->getQueryParams(); + + $this->table = (string)($downloadArguments['table'] ?? ''); + if ($this->table === '') { + throw new \RuntimeException('No table was given for downloading records', 1624551586); + } + + $this->id = (int)($downloadArguments['id'] ?? 0); + $this->modTSconfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_list.'] ?? []; + + $presets = $this->eventDispatcher->dispatch( + new BeforeRecordDownloadPresetsAreDisplayedEvent( + $this->table, + $this->modTSconfig['downloadPresets.'][$this->table . '.'] ?? [], + $request, + $this->id, + ) + )->getPresets(); + + $view = $this->backendViewFactory->create($request); + $view->assignMultiple([ + 'table' => $this->table, + 'downloadArguments' => $downloadArguments, + 'formats' => array_keys(self::DOWNLOAD_FORMATS), + 'formatOptions' => $this->getFormatOptionsWithResolvedDefaults(), + 'presets' => $presets, + ]); + + $response = $this->responseFactory->createResponse() + ->withHeader('Content-Type', 'text/html; charset=utf-8'); + + $response->getBody()->write($view->render('RecordDownloadSettings')); + return $response; + } + + /** + * Generating an download in CSV format + */ + protected function csvDownloadAction( + ServerRequestInterface $request, + array $headerRow, + array $records + ): ResponseInterface { + // Fetch csv related format options + $csvDelimiter = (string)$this->getFormatOption($request, 'delimiter'); + $csvQuote = (string)$this->getFormatOption($request, 'quote'); + + // Create result + $result[] = CsvUtility::csvValues($headerRow, $csvDelimiter, $csvQuote); + foreach ($records as $record) { + $result[] = CsvUtility::csvValues($record, $csvDelimiter, $csvQuote); + } + + return $this->generateDownloadResponse(implode(CRLF, $result)); + } + + /** + * Generating an download in JSON format + */ + protected function jsonDownloadAction( + ServerRequestInterface $request, + array $headerRow, + array $records + ): ResponseInterface { + // Fetch and evaluate json related format option + switch ($this->getFormatOption($request, 'meta')) { + case 'prefix': + $result = [$this->table . ':' . $this->id => $records]; + break; + case 'full': + $user = $this->getBackendUserAuthentication(); + $parsedBody = $request->getParsedBody(); + $result = [ + 'meta' => [ + 'table' => $this->table, + 'page' => $this->id, + 'timestamp' => GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp'), + 'user' => $user->getUserName() ?? '', + 'site' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '', + 'options' => [ + 'columns' => array_values($headerRow), + 'values' => ($parsedBody['rawvalues'] ?? false) ? 'raw' : 'processed', + ], + ], + 'records' => $records, + ]; + $searchString = (string)($parsedBody['searchString'] ?? ''); + $searchLevels = (int)($parsedBody['searchLevels'] ?? 0); + if ($searchString !== '' || $searchLevels !== 0) { + $result['meta']['search'] = [ + 'searchTerm' => $searchString, + 'searchLevels' => $searchLevels, + ]; + } + break; + case 'none': + default: + $result = $records; + break; + } + + return $this->generateDownloadResponse(json_encode($result) ?: ''); + } + + /** + * Get site languages, available for the current backend user + */ + protected function getSiteLanguages(ServerRequestInterface $request): array + { + $site = $request->getAttribute('site'); + return $site->getAvailableLanguages($this->getBackendUserAuthentication(), false, $this->id); + } + + /** + * Return an evaluated and processed custom filename or a + * default, if non or an invalid custom filename was provided. + */ + protected function generateFilename(string $filename): string + { + $defaultFilename = $this->table . '_' . date('dmy-Hi') . '.' . $this->format; + + // Return default filename if given filename is empty or not valid + if ($filename === '' || !preg_match('/^[0-9a-z._\-]+$/i', $filename)) { + return $defaultFilename; + } + + $extension = pathinfo($filename, PATHINFO_EXTENSION); + if ($extension === '') { + // Add original extension in case alternative filename did not contain any + $filename = rtrim($filename, '.') . '.' . $this->format; + } + + // Check if given or resolved extension matches the original one + return pathinfo($filename, PATHINFO_EXTENSION) === $this->format ? $filename : $defaultFilename; + } + + /** + * Return the format options with resolved default values from TSconfig + */ + protected function getFormatOptionsWithResolvedDefaults(): array + { + $formatOptions = self::DOWNLOAD_FORMATS; + + if ($this->modTSconfig === []) { + return $formatOptions; + } + + if ($this->modTSconfig['csvDelimiter'] ?? false) { + $default = (string)$this->modTSconfig['csvDelimiter']; + if (!in_array($default, $formatOptions['csv']['options']['delimiter'], true)) { + // In case the user defined option is not yet available as format options, add it + $formatOptions['csv']['options']['delimiter']['custom'] = $default; + } + $formatOptions['csv']['defaults']['delimiter'] = $default; + } + + if ($this->modTSconfig['csvQuote'] ?? false) { + $default = (string)$this->modTSconfig['csvQuote']; + if (!in_array($default, $formatOptions['csv']['options']['quote'], true)) { + // In case the user defined option is not yet available as format options, add it + $formatOptions['csv']['options']['quote']['custom'] = $default; + } + $formatOptions['csv']['defaults']['quote'] = $default; + } + + return $formatOptions; + } + + protected function getFormatOptions(ServerRequestInterface $request): array + { + return $request->getParsedBody()[$this->format] ?? []; + } + + protected function getFormatOption(ServerRequestInterface $request, string $option, $default = null) + { + return $this->getFormatOptions($request)[$option] + ?? $this->getFormatOptionsWithResolvedDefaults()[$this->format]['defaults'][$option] + ?? $default; + } + + protected function generateDownloadResponse(string $result): ResponseInterface + { + $response = $this->responseFactory->createResponse() + ->withHeader('Content-Type', 'application/octet-stream') + ->withHeader('Content-Disposition', 'attachment; filename=' . $this->filename); + $response->getBody()->write($result); + + return $response; + } + + protected function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/ResetPasswordController.php b/Classes/Controller/ResetPasswordController.php new file mode 100644 index 0000000..cfa6de6 --- /dev/null +++ b/Classes/Controller/ResetPasswordController.php @@ -0,0 +1,278 @@ +passwordReset->isEnabled()) { + return $this->redirectToLoginForm($request); + } + $this->initialize($request); + $this->initializeForgetPasswordView($request); + $this->pageRenderer->setBodyContent('' . $this->view->render('Login/ForgetPasswordForm')); + return $this->pageRenderer->renderResponse($request); + } + + /** + * Validate the email address. + * + * Restricted to POST method in Configuration/Backend/Routes.php + */ + public function initiatePasswordResetAction(ServerRequestInterface $request): ResponseInterface + { + if (!$this->passwordReset->isEnabled()) { + return $this->redirectToLoginForm($request); + } + $this->initialize($request); + $this->initializeForgetPasswordView($request); + $emailAddress = $request->getParsedBody()['email'] ?? ''; + if (!is_string($emailAddress)) { + $emailAddress = ''; + } + $this->view->assign('email', $emailAddress); + if (!GeneralUtility::validEmail($emailAddress)) { + $this->view->assign('invalidEmail', true); + } else { + $this->passwordReset->initiateReset($request, $this->context, $emailAddress); + $this->view->assign('resetInitiated', true); + } + $this->pageRenderer->setBodyContent('' . $this->view->render('Login/ForgetPasswordForm')); + // Prevent time based information disclosure by waiting a random time + // before sending a response. This prevents that the response time + // can be an indicator if the used email exists or not. Wait a random + // time between 200 milliseconds and 3 seconds. + usleep(random_int(200000, 3000000)); + return $this->pageRenderer->renderResponse($request); + } + + /** + * Validates the link and show a form to enter the new password. + */ + public function passwordResetAction(ServerRequestInterface $request): ResponseInterface + { + if (!$this->passwordReset->isEnabled()) { + return $this->redirectToLoginForm($request); + } + $this->initialize($request); + $this->initializeResetPasswordView($request); + if (!$this->passwordReset->isValidResetTokenFromRequest($request)) { + $this->view->assign('invalidToken', true); + } + $this->pageRenderer->setBodyContent('' . $this->view->render('Login/ResetPasswordForm')); + return $this->pageRenderer->renderResponse($request); + } + + /** + * Updates the password in the database. + * + * Restricted to POST method in Configuration/Backend/Routes.php + */ + public function passwordResetFinishAction(ServerRequestInterface $request): ResponseInterface + { + if (!$this->passwordReset->isEnabled()) { + return $this->redirectToLoginForm($request); + } + // Token is invalid + if (!$this->passwordReset->isValidResetTokenFromRequest($request)) { + return $this->passwordResetAction($request); + } + $this->initialize($request); + $this->initializeResetPasswordView($request); + if ($this->passwordReset->resetPassword($request, $this->context)) { + $this->view->assign('resetExecuted', true); + } else { + $this->view->assign('error', true); + } + $this->pageRenderer->setBodyContent('' . $this->view->render('Login/ResetPasswordForm')); + return $this->pageRenderer->renderResponse($request); + } + + private function redirectToLoginForm(ServerRequestInterface $request): ResponseInterface + { + return new RedirectResponse( + $this->uriBuilder->buildUriWithRedirect('login', [], RouteRedirect::createFromRequest($request)), + 303 + ); + } + + protected function initializeForgetPasswordView(ServerRequestInterface $request): void + { + $parameters = array_filter(['loginProvider' => $this->loginProvider]); + $this->view->assignMultiple([ + 'formUrl' => $this->uriBuilder->buildUriWithRedirect('password_forget_initiate_reset', $parameters, RouteRedirect::createFromRequest($request)), + 'returnUrl' => $this->uriBuilder->buildUriWithRedirect('login', $parameters, RouteRedirect::createFromRequest($request)), + ]); + } + + protected function initializeResetPasswordView(ServerRequestInterface $request): void + { + $token = $request->getQueryParams()['t'] ?? ''; + $identity = $request->getQueryParams()['i'] ?? ''; + $expirationDate = $request->getQueryParams()['e'] ?? ''; + $parameters = array_filter(['loginProvider' => $this->loginProvider]); + $formUrl = $this->uriBuilder->buildUriWithRedirect( + 'password_reset_finish', + array_filter(array_merge($parameters, [ + 't' => $token, + 'i' => $identity, + 'e' => $expirationDate, + ])), + RouteRedirect::createFromRequest($request) + ); + $this->view->assignMultiple([ + 'token' => $token, + 'identity' => $identity, + 'expirationDate' => $expirationDate, + 'formUrl' => $formUrl, + 'restartUrl' => $this->uriBuilder->buildUriWithRedirect('password_forget', $parameters, RouteRedirect::createFromRequest($request)), + 'passwordRequirements' => $this->getPasswordRequirements(), + ]); + } + + protected function initialize(ServerRequestInterface $request): void + { + $languageService = $this->getLanguageService(); + + // Only allow to execute this if not logged in as a user right now + if ($this->context->getAspect('backend.user')->isLoggedIn()) { + throw new PropagateResponseException( + new RedirectResponse($this->uriBuilder->buildUriFromRoute('login'), 303), + 1618342858 + ); + } + + // Fetch login provider from the request + $this->loginProvider = $request->getQueryParams()['loginProvider'] ?? ''; + + // Try to get the preferred browser language + $httpAcceptLanguage = $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'] ?? ''; + $preferredBrowserLanguage = $this->locales->getPreferredClientLanguage($httpAcceptLanguage); + + // If we found a $preferredBrowserLanguage, which is not the default language + // initialize $this->getLanguageService() again with $preferredBrowserLanguage. + // Additionally, set the language to the backend user object, so labels in fluid views are translated + if ($preferredBrowserLanguage !== 'default') { + $languageService->init($preferredBrowserLanguage); + $this->getBackendUserAuthentication()->user['lang'] = $preferredBrowserLanguage; + } + + $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $languageService); + $this->pageRenderer->setTitle('TYPO3 CMS Login: ' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '')); + $this->pageRenderer->loadJavaScriptModule('bootstrap'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/login.js'); + + $this->view = $this->backendViewFactory->create($request); + $this->view->assignMultiple([ + 'enablePasswordReset' => $this->passwordReset->isEnabled(), + 'referrerCheckEnabled' => $this->features->isFeatureEnabled('security.backend.enforceReferrer'), + 'loginUrl' => (string)$request->getUri(), + ]); + + $this->provideCustomLoginStyling($request); + } + + protected function provideCustomLoginStyling(ServerRequestInterface $request): void + { + if (($backgroundImageStyles = $this->authenticationStyleInformation->getBackgroundImageStyles($request)) !== '') { + $this->pageRenderer->addCssInlineBlock('loginBackgroundImage', $backgroundImageStyles, null, false, true); + } + if (($footerNote = $this->authenticationStyleInformation->getFooterNote()) !== '') { + $this->view->assign('loginFootnote', $footerNote); + } + if (($highlightColorStyles = $this->authenticationStyleInformation->getHighlightColorStyles()) !== '') { + $this->pageRenderer->addCssInlineBlock('loginHighlightColor', $highlightColorStyles, null, false, true); + } + $this->view->assignMultiple([ + 'copyright' => $this->typo3Information->getCopyrightNotice(), + ]); + } + + protected function getPasswordRequirements(): array + { + $passwordPolicy = $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] ?? 'default'; + $passwordPolicyValidator = GeneralUtility::makeInstance( + PasswordPolicyValidator::class, + PasswordPolicyAction::UPDATE_USER_PASSWORD, + is_string($passwordPolicy) ? $passwordPolicy : '' + ); + return $passwordPolicyValidator->getRequirements(); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/Resource/ResourceController.php b/Classes/Controller/Resource/ResourceController.php new file mode 100644 index 0000000..ce2cd18 --- /dev/null +++ b/Classes/Controller/Resource/ResourceController.php @@ -0,0 +1,267 @@ +getQueryParams()['identifier'] ?? null; + $resource = $this->resourceFactory->retrieveFileOrFolderObject($identifier); + if ($resource === null) { + return new JsonResponse(null, 404); + } + if (!$resource->checkActionPermission('read')) { + return new JsonResponse(null, 403); + } + + return new JsonResponse($this->getResourceResponseData($resource)); + } + + public function requestThumbnailAction(ServerRequestInterface $request): ResponseInterface + { + $identifier = $request->getQueryParams()['identifier'] ?? null; + $thumbnailSizeIdentifier = $request->getQueryParams()['size'] ?? 'default'; + $keepAspectRatio = (bool)($request->getQueryParams()['keepAspectRatio'] ?? false); + $resource = null; + + if ($identifier) { + $resource = $this->resourceFactory->retrieveFileOrFolderObject($identifier); + } + if ($resource === null || !($resource instanceof File && ($resource->isImage() || $resource->isMediaFile()))) { + return new Response(null, 404); + } + if (!$resource->checkActionPermission('read')) { + return new Response(null, 403); + } + + $thumbnailSize = ThumbnailSize::tryFrom($thumbnailSizeIdentifier) ?? ThumbnailSize::DEFAULT; + [$width, $height] = $keepAspectRatio ? $thumbnailSize->getDimensions() : $thumbnailSize->getCroppedDimensions(); + $thumbnail = $resource + ->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, ['width' => $width, 'height' => $height]); + + return new RedirectResponse( + GeneralUtility::locationHeaderUrl($thumbnail->getPublicUrl() ?? '', $request) + ); + } + + public function renameResourceAction(ServerRequestInterface $request): ResponseInterface + { + $identifier = $request->getParsedBody()['identifier'] ?? null; + $origin = null; + + if ($identifier) { + $origin = $this->resourceFactory->retrieveFileOrFolderObject($identifier); + } + + try { + if (!$origin instanceof File && !$origin instanceof Folder) { + throw new \InvalidArgumentException('Resource must be a file or a folder', 1676979120); + } + if ($origin->getStorage()->isFallbackStorage()) { + throw new InsufficientFileAccessPermissionsException('You are not allowed to access files outside your storages', 1676299579); + } + if (!$origin->checkActionPermission('rename')) { + throw new InsufficientFileAccessPermissionsException('You are not allowed to rename the resource', 1676979130); + } + $resourceName = $request->getParsedBody()['resourceName'] ?? null; + if (!$resourceName || trim((string)$resourceName) === '') { + throw new \InvalidArgumentException('The resource name cannot be empty', 1676978732); + } + $oldName = $origin->getName(); + if ($oldName === $resourceName) { + $message = sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.error.message.resourceNameNotDifferent'), $oldName); + return new JsonResponse($this->getResponseData(true, $message, $origin)); + } + + $resource = $origin->rename($resourceName); + if ($resource->getName() === $oldName) { + $message = sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.error.message.resourceNotRenamed'), $oldName); + return new JsonResponse($this->getResponseData(false, $message, $origin)); + } + } catch (ResultException $exception) { + // Possible Exception thrown within the `->rename(...)` chain via ResourceConsistencyService + return new JsonResponse($this->getResponseData(false, $this->renderResultException($exception, $this->getLanguageService()))); + } catch (\Exception $exception) { + $message = match ($exception->getCode()) { + 1676979120 => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.error.message.resourceNotFileOrFolder'), + 1676299579 => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.error.message.resourceOutsideOfStorages'), + 1676979130 => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.error.message.resourceNoPermissionRename'), + 1676978732 => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.error.message.resourceNameCannotBeEmpty'), + default => $exception->getMessage(), + }; + return new JsonResponse($this->getResponseData(false, $message)); + } + + return new JsonResponse($this->getResponseData( + true, + sprintf( + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.success.message.renamed'), + $oldName, + $resource->getName() + ), + $origin, + $resource, + )); + } + + public function replaceResourceAction(ServerRequestInterface $request): ResponseInterface + { + $uploadedFiles = $request->getUploadedFiles(); + if ($uploadedFiles === []) { + return new JsonResponse($this->getResponseData( + false, + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.error.message.resourceNotAvailableToUpload'), + )); + } + + $uid = $request->getParsedBody()['uid']; + $keepFilename = (bool)($request->getParsedBody()['keepFilename'] ?? false); + $origin = $this->resourceFactory->retrieveFileOrFolderObject($uid); + if ($origin === null) { + return new JsonResponse($this->getResponseData( + false, + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.error.message.resourceNotFound'), + )); + } + + $this->fileProcessor->setActionPermissions(); + $this->fileProcessor->setExistingFilesConflictMode(DuplicationBehavior::REPLACE); + $this->fileProcessor->start([ + 'replace' => [ + 1 => [ + 'data' => 1, + 'uid' => $uid, + 'keepFilename' => $keepFilename, + ], + ], + ], $uploadedFiles); + $result = $this->fileProcessor->processData(); + $flashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(); + $messages = implode("\n", array_map(static fn(FlashMessage $message) => $message->getMessage(), $flashMessageQueue->getAllMessagesAndFlush())); + + /** @var File|null $fileReplacement */ + $fileReplacement = $result['replace'][0][0] ?? null; + if ($fileReplacement === null) { + return new JsonResponse($this->getResponseData( + false, + $messages, + $origin + )); + } + + return new JsonResponse($this->getResponseData( + true, + $messages, + $origin, + $fileReplacement + )); + } + + /** + * Prepare response data for a JSON response + */ + private function getResponseData(bool $success, string $message, ?ResourceInterface $origin = null, ?ResourceInterface $resource = null): array + { + $flashMessageQueue = new FlashMessageQueue('backend'); + $flashMessageQueue->enqueue( + new FlashMessage( + $message, + $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_resource.xlf:ajax.' . ($success ? 'success' : 'error')) + ) + ); + // Next to the flash message, also log the action to be consistent with the use in ExtendedFileUtiltiy + $this->getBackendUser()->writelog(SystemLogType::FILE, SystemLogFileAction::RENAME, $success ? SystemLogErrorClassification::MESSAGE : SystemLogErrorClassification::USER_ERROR, null, $message, []); + return [ + 'success' => $success, + 'status' => $flashMessageQueue, + 'origin' => $this->getResourceResponseData($origin), + 'resource' => $this->getResourceResponseData($resource), + ]; + } + + /** + * Prepare resource data for a JSON response + */ + private function getResourceResponseData(?ResourceInterface $resource): ?array + { + if (!$resource) { + return null; + } + + return [ + 'type' => $resource instanceof File ? 'file' : 'folder', + 'identifier' => $resource instanceof File || $resource instanceof Folder ? $resource->getCombinedIdentifier() : null, + 'name' => $resource->getName(), + 'hasPreview' => $resource instanceof File && ($resource->isImage() || $resource->isMediaFile()), + 'uid' => $resource instanceof File ? $resource->getUid() : null, + 'metaUid' => $resource instanceof File ? $resource->getMetaData()->offsetGet('uid') : null, + 'createdAt' => $resource instanceof File ? $resource->getCreationTime() : null, + 'size' => $resource instanceof File ? $resource->getSize() : null, + ]; + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/Security/SudoModeController.php b/Classes/Controller/Security/SudoModeController.php new file mode 100644 index 0000000..d36d5b2 --- /dev/null +++ b/Classes/Controller/Security/SudoModeController.php @@ -0,0 +1,250 @@ +uriBuilder->buildUriFromRoutePath( + self::ROUTE_PATH_MODULE, + $this->buildUriParametersForClaim($claim, 'module') + ); + } + + public function buildVerifyActionUriForClaim(AccessClaim $claim): UriInterface + { + return $this->uriBuilder->buildUriFromRoutePath( + self::ROUTE_PATH_VERIFY, + $this->buildUriParametersForClaim($claim, 'verify') + ); + } + + /** + * Renders the module backend markup, including the `` element. + */ + public function moduleAction(ServerRequestInterface $request): ResponseInterface + { + $this->pageRenderer->getJavaScriptRenderer()->addGlobalAssignment([ + 'TYPO3' => [ + 'configuration' => [ + 'username' => htmlspecialchars($this->getBackendUser()->user['username']), + ], + ], + ]); + + $claim = $this->resolveClaimFromRequest($request, 'module'); + if ($claim === null) { + return $this->redirectToErrorAction(); + } + + $view = $this->moduleTemplateFactory->create($request); + $view->assignMultiple([ + 'verifyActionUri' => $this->buildVerifyActionUriForClaim($claim), + 'allowInstallToolPassword' => $this->getBackendUser()->isSystemMaintainer(), + 'labels' => $this->getLanguageService()->getLabelsFromResource('EXT:backend/Resources/Private/Language/SudoMode.xlf'), + ]); + return $view->renderResponse('SudoMode/Module'); + } + + /** + * Called from JavaScript web-component, throwing an exception that is handled by `SudoModeInterceptor` middleware. + */ + public function applyAction(ServerRequestInterface $request): ResponseInterface + { + // @todo security: action is not signed and can be by-passed easily + $claim = $this->resolveClaimFromRequest($request, 'apply'); + if ($claim === null) { + return $this->redirectToErrorAction(); + } + + $this->storage->removeClaim($claim); + throw (new RequestGrantedException('Replay request', 1605873757)) + ->withInstruction($claim->instruction); + } + + /** + * Renders markup with error messages in case `AccessClaim` could not be resolved (e.g. when expired). + */ + public function errorAction(ServerRequestInterface $request): ResponseInterface + { + $view = $this->moduleTemplateFactory->create($request); + $view->assignMultiple([ + 'cancelUri' => $this->backendEntryPointResolver->getPathFromRequest($request), + 'cancelTarget' => '_top', + 'labels' => $this->getLanguageService()->getLabelsFromResource('EXT:backend/Resources/Private/Language/SudoMode.xlf'), + ]); + return $view->renderResponse('SudoMode/Error'); + } + + /** + * Verifies the provided password, called via AJAX from JavaScript web-component. + */ + public function verifyAction(ServerRequestInterface $request): ResponseInterface + { + $claim = $this->resolveClaimFromRequest($request, 'verify'); + if ($claim === null) { + return new JsonResponse(['message' => 'bad-request'], 400); + } + + $password = (string)($request->getParsedBody()['password'] ?? ''); + $useInstallToolPassword = (bool)($request->getParsedBody()['useInstallToolPassword'] ?? false); + // Only system maintainers are allowed to use the installtool password for sudo mode operations + if (!$this->getBackendUser()->isSystemMaintainer()) { + $useInstallToolPassword = false; + } + $loggerContext = $this->buildLoggerContext($claim); + + $redirect = [ + 'uri' => (string)$this->uriBuilder->buildUriFromRoutePath( + self::ROUTE_PATH_APPLY, + $this->buildUriParametersForClaim($claim, 'apply') + ), + ]; + $event = $this->eventDispatcher->dispatch(new SudoModeVerifyEvent($claim, $password, $useInstallToolPassword)); + if ($event->isVerified()) { + $this->logger->info('Passed by PSR-14 SudoModeVerifyEvent', $loggerContext); + $this->grantClaim($claim); + return new JsonResponse(['message' => 'accessGranted', 'redirect' => $redirect]); + } + if ($useInstallToolPassword && $this->passwordVerification->verifyInstallToolPassword($password)) { + $this->logger->info('Verified with install tool password', $loggerContext); + $this->grantClaim($claim); + return new JsonResponse(['message' => 'accessGranted', 'redirect' => $redirect]); + } + if (!$useInstallToolPassword && $this->passwordVerification->verifyBackendUserPassword($password, $this->getBackendUser())) { + $this->logger->info('Verified with user password', $loggerContext); + $this->grantClaim($claim); + return new JsonResponse(['message' => 'accessGranted', 'redirect' => $redirect]); + } + return new JsonResponse(['message' => 'invalidPassword'], 403); + } + + private function redirectToErrorAction(): ResponseInterface + { + $uri = $this->uriBuilder->buildUriFromRoutePath(self::ROUTE_PATH_ERROR); + return new RedirectResponse($uri); + } + + /** + * @param string $additionalPepper used to create a specific signature (e.g. on the action) + */ + private function buildUriParametersForClaim(AccessClaim $claim, string $additionalPepper): array + { + $additionalPeppers = [self::class, $additionalPepper]; + return [ + 'claim' => $claim->id, + 'hash' => $this->hashService->hmac($claim->id, json_encode($additionalPeppers), HashAlgo::SHA3_256), + ]; + } + + /** + * @param string $additionalPepper used to create a specific signature (e.g. on the action) + */ + private function resolveClaimFromRequest(ServerRequestInterface $request, string $additionalPepper): ?AccessClaim + { + $claimId = (string)($request->getQueryParams()['claim'] ?? ''); + $claimHash = (string)($request->getQueryParams()['hash'] ?? ''); + $additionalPeppers = [self::class, $additionalPepper]; + $expectedHash = $this->hashService->hmac($claimId, json_encode($additionalPeppers), HashAlgo::SHA3_256); + if ($claimId === '' || $claimHash === '' || !hash_equals($expectedHash, $claimHash)) { + return null; + } + return $this->storage->findClaimById($claimId); + } + + private function grantClaim(AccessClaim $claim): void + { + foreach ($claim->subjects as $subject) { + $grant = $this->factory->buildGrantForSubject($subject); + $this->storage->addGrant($grant); + } + } + + /** + * @return array + */ + private function buildLoggerContext(AccessClaim $claim): array + { + $backendUserAspect = GeneralUtility::makeInstance(Context::class) + ->getAspect('backend.user'); + return [ + 'claim' => $claim->id, + 'user' => $backendUserAspect->get('id'), + ]; + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/SetupModuleController.php b/Classes/Controller/SetupModuleController.php new file mode 100644 index 0000000..cba6ec7 --- /dev/null +++ b/Classes/Controller/SetupModuleController.php @@ -0,0 +1,704 @@ +getBackendUser()->getOriginalUserIdWhenInSwitchUserMode()) { + $action = PasswordPolicyAction::UPDATE_USER_PASSWORD_SWITCH_USER_MODE; + } + + $this->passwordPolicyValidator = GeneralUtility::makeInstance( + PasswordPolicyValidator::class, + $action, + is_string($passwordPolicy) ? $passwordPolicy : '' + ); + } + + /** + * Injects the request object, checks if data should be saved, and prepares a HTML page + */ + public function mainAction(ServerRequestInterface $request): ResponseInterface + { + $view = $this->initialize($request); + $this->storeIncomingData($request); + if ($this->pagetreeNeedsRefresh || $this->settingsAreResetToDefault) { + BackendUtility::setUpdateSignal('updatePageTree'); + } + if ($this->colorSchemeChanged || $this->settingsAreResetToDefault) { + BackendUtility::setUpdateSignal('updateColorScheme', $this->getBackendUser()->uc['colorScheme'] ?? 'auto'); + } + if ($this->themeChanged || $this->settingsAreResetToDefault) { + BackendUtility::setUpdateSignal('updateTheme', $this->getBackendUser()->uc['theme'] ?? 'modern'); + } + if ($this->backendTitleFormatChanged || $this->settingsAreResetToDefault) { + BackendUtility::setUpdateSignal('updateTitleFormat', $this->getBackendUser()->uc['backendTitleFormat'] ?? 'titleFirst'); + } + if ($this->dateTimeFirstDayOfWeekChanged || $this->settingsAreResetToDefault) { + BackendUtility::setUpdateSignal('updateDateTimeFirstDayOfWeek', $this->getBackendUser()->uc['dateTimeFirstDayOfWeek'] ?? ''); + } + if ($this->languageUpdate) { + $this->getLanguageService()->init($this->getBackendUser()->user['lang'] ?? 'en'); + $locale = $this->getLanguageService()->getLocale(); + if ($locale !== null) { + $parameters = [ + 'language' => $locale->getLanguageCode(), + ]; + BackendUtility::setUpdateSignal('updateBackendLanguage', $parameters); + } + } + if ($this->persistentUpdate !== []) { + foreach ($this->persistentUpdate as $params) { + BackendUtility::setUpdateSignal('updatePersistent', $params); + } + } + + // Use FormEngine to render the user settings form + $formData = $this->compileFormData($request, $this->userSettingsSchema->getTca()); + $formData['renderType'] = 'fullRecordContainer'; + $formResultArray = $this->nodeFactory->create($formData)->render(); + $formResult = $this->formResultFactory->create($formResultArray); + $this->formResultHandler->addAssets($formResult); + + $formProtection = $this->formProtectionFactory->createFromRequest($request); + $this->addFlashMessages($view); + $view->addButtonToButtonBar($this->componentFactory->createSaveButton('SetupModuleController')->setName('data[save]')); + $this->registerResetButtonToButtonBar($view); + // Set shortcut context - reload button is added automatically + $view->getDocHeaderComponent()->setShortcutContext( + 'user_setup', + $this->getLanguageService()->translate('short_description', 'backend.modules.user_settings') + ); + $view->assignMultiple([ + 'typo3Info' => $this->typo3Information, + 'isLanguageUpdate' => $this->languageUpdate, + 'formEngineHtml' => $formResult->html, + 'formToken' => $formProtection->generateToken('BE user setup', 'edit'), + ]); + return $view->renderResponse('Setup/Main'); + } + + /** + * Compile form data for FormEngine rendering. + */ + protected function compileFormData(ServerRequestInterface $request, array $userSettingsTca): array + { + $backendUser = $this->getBackendUser(); + $formDataCompilerInput = [ + 'request' => $request, + 'tableName' => 'be_users_settings', + 'vanillaUid' => (int)$backendUser->user['uid'], + 'command' => 'edit', + 'returnUrl' => '', + 'tcaSchemata' => $this->tcaSchemaBuilder->buildFromStructure($userSettingsTca), + 'fullTca' => $userSettingsTca, + ]; + return $this->formDataCompiler->compile( + $formDataCompilerInput, + GeneralUtility::makeInstance(UserSettingsDataGroup::class) + ); + } + + /** + * Initializes the module for display of the settings form. + */ + protected function initialize(ServerRequestInterface $request): ModuleTemplate + { + $languageService = $this->getLanguageService(); + $backendUser = $this->getBackendUser(); + $view = $this->moduleTemplateFactory->create($request); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/modal.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/form-engine.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/setup-module.js'); + $this->processAdditionalJavaScriptModules($request); + $this->pageRenderer->addInlineSetting('FormEngine', 'formName', 'editform'); + $this->pageRenderer->addInlineLanguageLabelArray([ + 'FormEngine.remainingCharacters' => $languageService->translate('labels.remainingCharacters', 'core.core'), + ]); + $view->setTitle($languageService->translate('user_settings', 'backend.user_profile')); + $view->setLayout(ModuleLayout::NORMAL); + // Getting the 'override' values as set might be set in user TSconfig + $this->overrideConf = $backendUser->getTSConfig()['setup.']['override.'] ?? []; + // Getting the disabled fields might be set in user TSconfig (eg setup.fields.password.disabled=1) + $this->tsFieldConf = $backendUser->getTSConfig()['setup.']['fields.'] ?? []; + // if password is disabled, disable repeat of password too (password2) + if ($this->tsFieldConf['password.']['disabled'] ?? false) { + $this->tsFieldConf['password2.']['disabled'] = 1; + } + return $view; + } + + protected function processAdditionalJavaScriptModules(ServerRequestInterface $request): void + { + $event = new AddUserSettingsJavaScriptModulesEvent($request); + $event = $this->eventDispatcher->dispatch($event); + foreach ($event->getJavaScriptModules() as $specifier) { + $this->pageRenderer->loadJavaScriptModule($specifier); + } + } + + /** + * If settings are submitted via POST, store them + */ + protected function storeIncomingData(ServerRequestInterface $request): void + { + $postData = $request->getParsedBody(); + if (!is_array($postData) || empty($postData)) { + return; + } + + $formProtection = $this->formProtectionFactory->createFromRequest($request); + // Separate submitted data into corresponding partitions + $backendUserId = (int)$this->getBackendUser()->user['uid']; + $beUsersSubmission = $this->extractPartitionData($postData['data']['be_users_settings'][$backendUserId] ?? [], 'be_users'); + $userSettingsSubmission = $this->extractPartitionData($postData['data']['be_users_settings'][$backendUserId] ?? [], 'user_settings'); + $columns = $this->userSettingsSchema->getColumns(); + $backendUser = $this->getBackendUser(); + $beUserId = (int)$backendUser->user['uid']; + $storeRec = []; + $doSaveData = false; + $fieldList = $this->getFieldsFromShowItem(); + + if ($beUsersSubmission !== [] + && $userSettingsSubmission !== [] + && $formProtection->validateToken((string)($postData['formToken'] ?? ''), 'BE user setup', 'edit') + ) { + // UC hashed before applying changes + $save_before = md5(serialize($backendUser->uc)); + // PUT SETTINGS into the ->uc array: + // Reload left frame when switching BE language + if (isset($beUsersSubmission['lang']) && $beUsersSubmission['lang'] !== $backendUser->user['lang']) { + $this->languageUpdate = true; + } + // Reload pagetree if the title length is changed + if (isset($userSettingsSubmission['titleLen']) && $userSettingsSubmission['titleLen'] !== $backendUser->uc['titleLen']) { + $this->pagetreeNeedsRefresh = true; + } + if (isset($userSettingsSubmission['colorScheme']) && $userSettingsSubmission['colorScheme'] !== ($backendUser->uc['colorScheme'] ?? null)) { + $this->colorSchemeChanged = true; + } + if (isset($userSettingsSubmission['theme']) && $userSettingsSubmission['theme'] !== ($backendUser->uc['theme'] ?? null)) { + $this->themeChanged = true; + } + if (isset($userSettingsSubmission['backendTitleFormat']) && $userSettingsSubmission['backendTitleFormat'] !== ($backendUser->uc['backendTitleFormat'] ?? null)) { + $this->backendTitleFormatChanged = true; + } + if (isset($userSettingsSubmission['dateTimeFirstDayOfWeek']) && $userSettingsSubmission['dateTimeFirstDayOfWeek'] !== ($backendUser->uc['dateTimeFirstDayOfWeek'] ?? null)) { + $this->dateTimeFirstDayOfWeekChanged = true; + $this->persistentUpdate[] = [ + 'fieldName' => 'dateTimeFirstDayOfWeek', + 'value' => $userSettingsSubmission['dateTimeFirstDayOfWeek'], + ]; + } + // Options which should trigger direct JS persistent update, because + // their new state needs to be available in JS components right away. + foreach ($this->userSettingsSchema->getPersistentUpdateFieldNames() as $fieldName) { + $fieldValue = ((int)($userSettingsSubmission[$fieldName] ?? 0)) ? 1 : 0; + if ($fieldValue !== ($backendUser->uc[$fieldName] ?? null)) { + $this->persistentUpdate[] = [ + 'fieldName' => $fieldName, + 'value' => $fieldValue ? '1' : '0', + ]; + } + } + + if ($postData['data']['setValuesToDefault'] ?? false) { + // If every value should be default + $backendUser->resetUC(); + $this->settingsAreResetToDefault = true; + } elseif ($postData['data']['save'] ?? false) { + foreach ($columns as $field => $config) { + if (!in_array($field, $fieldList, true)) { + continue; + } + // Skip any disallowed field name, not matter if it's in be_users or user_settings partition + if (in_array($field, self::DISALLOWED_FIELD_NAMES, true)) { + continue; + } + $isBeUsersField = ($config['table'] ?? '') === 'be_users'; + $fieldType = $config['type'] ?? 'text'; + if ($isBeUsersField) { + $submittedValue = $beUsersSubmission[$field] ?? null; + if (!isset($config['access']) || ($this->checkAccess($config) && ($backendUser->user[$field] !== $submittedValue))) { + if ($fieldType === 'check') { + $fieldValue = (int)($submittedValue ?? 0); + } else { + $fieldValue = $submittedValue; + } + $storeRec['be_users'][$beUserId][$field] = $fieldValue; + $backendUser->user[$field] = $fieldValue; + } + } else { + if ($fieldType === 'check') { + $backendUser->uc[$field] = (int)($userSettingsSubmission[$field] ?? 0); + } else { + $backendUser->uc[$field] = htmlspecialchars($userSettingsSubmission[$field] ?? ''); + } + } + } + // Personal data for the users be_user-record (email, name, password...) + // If email and name is changed, set it in the users record: + $be_user_data = $beUsersSubmission; + // Temporarily hold `password2` for the hook to be able to adjust the password + $be_user_data['password2'] = $userSettingsSubmission['password2'] ?? ''; + // Possibility to modify the transmitted values. Useful to do transformations, like RSA password decryption + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/setup/mod/index.php']['modifyUserDataBeforeSave'] ?? [] as $function) { + $params = ['be_user_data' => &$be_user_data]; + GeneralUtility::callUserFunction($function, $params, $this); + } + $this->passwordIsSubmitted = (string)($be_user_data['password'] ?? '') !== ''; + $passwordIsConfirmed = $this->passwordIsSubmitted && $be_user_data['password'] === $be_user_data['password2']; + unset($be_user_data['password2']); + + // Validate password against password policy + $contextData = new ContextData( + loginMode: 'BE', + currentPasswordHash: $this->getBackendUser()->user['password'], + newUserFullName: $be_user_data['realName'] ?? $this->getBackendUser()->user['realName'] + ); + $contextData->setData('currentUsername', $this->getBackendUser()->user['username']); + $event = $this->eventDispatcher->dispatch( + new EnrichPasswordValidationContextDataEvent( + $contextData, + $be_user_data, + self::class + ) + ); + $contextData = $event->getContextData(); + + $passwordValid = true; + if ($passwordIsConfirmed + && !$this->passwordPolicyValidator->isValidPassword($be_user_data['password'], $contextData) + ) { + $passwordValid = false; + $this->passwordIsUpdated = self::PASSWORD_POLICY_FAILED; + } + + // Update the real name: + if (isset($be_user_data['realName']) && $be_user_data['realName'] !== $backendUser->user['realName']) { + $backendUser->user['realName'] = ($storeRec['be_users'][$beUserId]['realName'] = substr($be_user_data['realName'], 0, 80)); + } + // Update the email address: + if (isset($be_user_data['email']) && $be_user_data['email'] !== $backendUser->user['email']) { + $backendUser->user['email'] = ($storeRec['be_users'][$beUserId]['email'] = substr($be_user_data['email'], 0, 255)); + } + // Update the password: + if ($this->passwordIsSubmitted) { + if ($passwordIsConfirmed && $passwordValid) { + $this->passwordIsUpdated = self::PASSWORD_UPDATED; + $storeRec['be_users'][$beUserId]['password'] = $be_user_data['password']; + } elseif ($passwordIsConfirmed) { + $this->passwordIsUpdated = self::PASSWORD_POLICY_FAILED; + } else { + $this->passwordIsUpdated = self::PASSWORD_NOT_THE_SAME; + } + } + + $this->setAvatarFileUid($beUserId, $be_user_data['avatar'] ?? null, $storeRec); + + $doSaveData = true; + } + // Explicitly unset disallowed field names + foreach (self::DISALLOWED_FIELD_NAMES as $disallowedFieldName) { + unset($backendUser->uc[$disallowedFieldName]); + } + // Inserts the overriding values. + $backendUser->overrideUC(); + $save_after = md5(serialize($backendUser->uc)); + // If something in the uc-array of the user has changed, we save the array... + if ($save_before != $save_after) { + $backendUser->writeUC(); + $backendUser->writelog(SystemLogType::SETTING, SystemLogSettingAction::CHANGE, SystemLogErrorClassification::MESSAGE, null, 'Personal settings changed', []); + $this->setupIsUpdated = true; + } + // Persist data if something has changed: + if (!empty($storeRec) && $doSaveData) { + // Set user to admin to circumvent DataHandler restrictions. + // Not using isAdmin() to fetch the original value, just in case it has been boolean casted. + $savedUserAdminState = $backendUser->user['admin']; + $backendUser->user['admin'] = true; + // Make dedicated instance of TCE for storing the changes. + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $dataHandler->start($storeRec, [], $backendUser); + $dataHandler->process_datamap(); + $dataHandler->printLogErrorMessages(); + // reset the user record admin flag to previous value, just in case it gets used any further. + $backendUser->user['admin'] = $savedUserAdminState; + if ($this->passwordIsUpdated === self::PASSWORD_NOT_UPDATED || count($storeRec['be_users'][$beUserId]) > 1) { + $this->setupIsUpdated = true; + } + BackendUtility::setUpdateSignal('updateTopbar'); + } + } + } + + /** + * Returns access check (currently only "admin" is supported) + * + * @param array $config Configuration of the field, access mode is defined in key 'access' + * @return bool Whether it is allowed to modify the given field + */ + protected function checkAccess(array $config) + { + $access = $config['access']; + if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['setup']['accessLevelCheck'][$access])) { + if (class_exists($access)) { + $accessObject = GeneralUtility::makeInstance($access); + if (method_exists($accessObject, 'accessLevelCheck')) { + // Initialize vars. If method fails, $set will be set to FALSE + return $accessObject->accessLevelCheck($config); + } + } + } elseif ($access === 'admin') { + return $this->getBackendUser()->isAdmin(); + } + + return false; + } + + /** + * Returns array with fields defined in TCA user settings showitem. + * Remove fields which are disabled by user TSconfig + * + * @return list Array with field names visible in form + */ + protected function getFieldsFromShowItem(): array + { + // just keep field names, filter out control sequences + $tcaFieldNames = array_keys($this->userSettingsSchema->getColumns()); + $allowedFields = GeneralUtility::trimExplode(',', $this->userSettingsSchema->getRawShowitem(), true); + $allowedFields = array_map($this->extractShowitemFieldName(...), $allowedFields); + $allowedFields = array_filter($allowedFields, static fn(string $field): bool => in_array($field, $tcaFieldNames, true)); + $backendUser = $this->getBackendUser(); + if ($backendUser->getOriginalUserIdWhenInSwitchUserMode() && $backendUser->isSystemMaintainer(true)) { + // DataHandler denies changing the password of system maintainer users in switch user mode. + // Do not show the password fields is this case. + $key = array_search('password', $allowedFields); + if ($key !== false) { + unset($allowedFields[$key]); + } + $key = array_search('password2', $allowedFields); + if ($key !== false) { + unset($allowedFields[$key]); + } + } + + foreach ($this->tsFieldConf as $fieldName => $userTsFieldConfig) { + if (!empty($userTsFieldConfig['disabled'])) { + $fieldName = rtrim($fieldName, '.'); + $key = array_search($fieldName, $allowedFields); + if ($key !== false) { + unset($allowedFields[$key]); + } + } + } + return $allowedFields; + } + + /** + * Extracts `field_name` from a showitem field `field_name;field_label`. + */ + protected function extractShowitemFieldName(string $fieldName): string + { + $offset = strpos($fieldName, ';'); + return $offset !== false ? substr($fieldName, 0, $offset) : $fieldName; + } + + /** + * Get Avatar fileUid + */ + protected function getAvatarFileUid(int $beUserId): int + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_reference'); + $file = $queryBuilder + ->select('uid_local') + ->from('sys_file_reference') + ->where( + $queryBuilder->expr()->eq( + 'tablenames', + $queryBuilder->createNamedParameter('be_users') + ), + $queryBuilder->expr()->eq( + 'fieldname', + $queryBuilder->createNamedParameter('avatar') + ), + $queryBuilder->expr()->eq( + 'uid_foreign', + $queryBuilder->createNamedParameter($beUserId, Connection::PARAM_INT) + ) + ) + ->executeQuery() + ->fetchOne(); + return (int)$file; + } + + /** + * Set avatar fileUid for backend user + * + * @param numeric-string|''|'delete'|null $fileUid either null, a file UID, an empty string, or `delete` + */ + protected function setAvatarFileUid(int $beUserId, ?string $fileUid, array &$storeRec): void + { + // Update is only needed when new fileUid is set + if ((int)$fileUid === $this->getAvatarFileUid($beUserId)) { + return; + } + + // If user is not allowed to modify avatar $fileUid is empty - so don't overwrite existing avatar + if (empty($fileUid)) { + return; + } + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_reference'); + $queryBuilder->getRestrictions()->removeAll(); + $queryBuilder + ->delete('sys_file_reference') + ->where( + $queryBuilder->expr()->eq( + 'tablenames', + $queryBuilder->createNamedParameter('be_users') + ), + $queryBuilder->expr()->eq( + 'fieldname', + $queryBuilder->createNamedParameter('avatar') + ), + $queryBuilder->expr()->eq( + 'uid_foreign', + $queryBuilder->createNamedParameter($beUserId, Connection::PARAM_INT) + ) + ) + ->executeStatement(); + + // If Avatar is marked for delete => set it to empty string so it will be updated properly + if ($fileUid === 'delete') { + $fileUid = ''; + } + + // Create new reference + if ((int)$fileUid > 0) { + // Get file object + try { + $file = $this->resourceFactory->getFileObject((int)$fileUid); + } catch (FileDoesNotExistException $e) { + $file = false; + } + + // Check if user is allowed to use the image (only when not in simulation mode) + if ($file && !$file->getStorage()->checkFileActionPermission('read', $file)) { + $file = false; + } + + // Check if extension is allowed + if ($file && $file->isImage()) { + // Create new file reference + $storeRec['sys_file_reference']['NEW1234'] = [ + 'uid_local' => (int)$fileUid, + 'uid_foreign' => (int)$beUserId, + 'tablenames' => 'be_users', + 'fieldname' => 'avatar', + 'pid' => 0, + ]; + $storeRec['be_users'][(int)$beUserId]['avatar'] = 'NEW1234'; + } + } + } + + /** + * Register the reset configuration button to the button bar. + */ + protected function registerResetButtonToButtonBar(ModuleTemplate $view): void + { + $languageService = $this->getLanguageService(); + $resetButton = $this->componentFactory->createGenericButton() + ->setTag('button') + ->setLabel($languageService->translate('reset_configuration_button', 'backend.user_profile')) + ->setTitle($languageService->translate('reset_configuration', 'backend.user_profile')) + ->setIcon($this->iconFactory->getIcon('actions-undo', IconSize::SMALL)) + ->setShowLabelText(true) + ->setClasses('t3js-modal-trigger') + ->setAttributes([ + 'type' => 'button', + 'data-severity' => 'warning', + 'data-title' => $languageService->translate('reset_configuration', 'backend.user_profile'), + 'data-content' => $languageService->translate('set_to_standard_question', 'backend.user_profile'), + 'data-event' => 'confirm', + 'data-event-name' => 'setup:confirmation:response', + 'data-event-payload' => 'resetConfiguration', + ]); + $view->addButtonToButtonBar($resetButton, ButtonBar::BUTTON_POSITION_RIGHT); + } + + /** + * Add FlashMessages for various actions + */ + protected function addFlashMessages(ModuleTemplate $view): void + { + $languageService = $this->getLanguageService(); + if ($this->setupIsUpdated && !$this->settingsAreResetToDefault) { + $view->addFlashMessage($languageService->translate('setup_was_updated', 'backend.user_profile'), $languageService->translate('user_settings', 'backend.user_profile')); + } + if ($this->settingsAreResetToDefault) { + $view->addFlashMessage($languageService->translate('settings_are_reset', 'backend.user_profile'), $languageService->translate('reset_configuration', 'backend.user_profile')); + } + if ($this->passwordIsSubmitted) { + switch ($this->passwordIsUpdated) { + case self::PASSWORD_NOT_THE_SAME: + $view->addFlashMessage($languageService->translate('new_password_failed', 'backend.user_profile'), $languageService->translate('new_password', 'backend.user_profile'), ContextualFeedbackSeverity::ERROR); + break; + case self::PASSWORD_UPDATED: + $view->addFlashMessage($languageService->translate('new_password_ok', 'backend.user_profile'), $languageService->translate('new_password', 'backend.user_profile')); + break; + case self::PASSWORD_POLICY_FAILED: + $view->addFlashMessage($languageService->translate('password_policy_failed', 'backend.user_profile'), $languageService->translate('new_password', 'backend.user_profile'), ContextualFeedbackSeverity::ERROR); + break; + } + } + } + + /** + * @param array $data + * @return array + */ + protected function extractPartitionData(array $data, string $partition): array + { + $partitionData = []; + $prefix = $partition . '__'; + $length = strlen($prefix); + foreach ($data as $key => $value) { + if (!str_starts_with($key, $prefix)) { + continue; + } + $normalizedKey = substr($key, $length); + $partitionData[$normalizedKey] = $value; + } + return $partitionData; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/SimpleDataHandlerController.php b/Classes/Controller/SimpleDataHandlerController.php new file mode 100644 index 0000000..84d5e72 --- /dev/null +++ b/Classes/Controller/SimpleDataHandlerController.php @@ -0,0 +1,290 @@ +setMirror. + * + * @var array + */ + protected $mirror; + + /** + * Cache command sent to ->clear_cacheCmd + * + * @var string + */ + protected $cacheCmd; + + /** + * Redirect URL. Script will redirect to this location after performing operations (unless errors has occurred) + * + * @var string + */ + protected $redirect; + + /** + * Clipboard command array. May trigger changes in "cmd" + * + * @var array + */ + protected $CB; + + /** + * TYPO3 Core Engine + * + * @var \TYPO3\CMS\Core\DataHandling\DataHandler + */ + protected $tce; + + public function __construct( + protected readonly FlashMessageService $flashMessageService, + ) {} + + /** + * Injects the request object for the current request or subrequest + * As this controller goes only through the processRequest() method, it just redirects to the given URL afterwards. + * + * @param ServerRequestInterface $request the current request + * @return ResponseInterface the response with the content + */ + public function mainAction(ServerRequestInterface $request): ResponseInterface + { + $this->init($request); + + $this->initializeClipboard($request); + $this->processRequest(); + + // Write errors to flash message queue + $this->tce->printLogErrorMessages(); + if ($this->redirect) { + return new RedirectResponse(GeneralUtility::locationHeaderUrl($this->redirect, $request), 303); + } + return new HtmlResponse(''); + } + + /** + * Processes all AJAX calls and returns a JSON formatted string + */ + public function processAjaxRequest(ServerRequestInterface $request): ResponseInterface + { + $this->init($request); + + // do the regular / main logic + $this->initializeClipboard($request); + $this->processRequest(); + + $content = [ + 'redirect' => $this->redirect, + 'messages' => [], + 'hasErrors' => false, + ]; + + // Prints errors (= write them to the message queue) + $this->tce->printLogErrorMessages(); + + $messages = $this->flashMessageService->getMessageQueueByIdentifier()->getAllMessagesAndFlush(); + if (!empty($messages)) { + foreach ($messages as $message) { + $content['messages'][] = [ + 'title' => $message->getTitle(), + 'message' => $message->getMessage(), + 'severity' => $message->getSeverity(), + ]; + if ($message->getSeverity() === ContextualFeedbackSeverity::ERROR) { + $content['hasErrors'] = true; + } + } + } + return new JsonResponse($content); + } + + /** + * Initialization of the class + */ + protected function init(ServerRequestInterface $request): void + { + $parsedBody = $request->getParsedBody(); + $queryParams = $request->getQueryParams(); + + // GPvars: + $this->flags = (array)($parsedBody['flags'] ?? $queryParams['flags'] ?? []); + $this->data = (array)($parsedBody['data'] ?? $queryParams['data'] ?? []); + $this->cmd = (array)($parsedBody['cmd'] ?? $queryParams['cmd'] ?? []); + $this->mirror = (array)($parsedBody['mirror'] ?? $queryParams['mirror'] ?? []); + $this->cacheCmd = (string)($parsedBody['cacheCmd'] ?? $queryParams['cacheCmd'] ?? ''); + $this->CB = (array)($parsedBody['CB'] ?? $queryParams['CB'] ?? []); + $this->redirect = GeneralUtility::sanitizeLocalUrl((string)($parsedBody['redirect'] ?? $queryParams['redirect'] ?? ''), $request); + // Creating DataHandler object + $this->tce = GeneralUtility::makeInstance(DataHandler::class); + // Reverse order. + if ($this->flags['reverseOrder'] ?? false) { + $this->tce->reverseOrder = true; + } + } + + /** + * Clipboard pasting and deleting. + */ + protected function initializeClipboard(ServerRequestInterface $request): void + { + if ($this->CB !== []) { + $clipObj = GeneralUtility::makeInstance(Clipboard::class); + $clipObj->initializeClipboard($request); + if ($this->CB['paste'] ?? false) { + $clipObj->setCurrentPad((string)($this->CB['pad'] ?? '')); + $this->setPasteCmd($clipObj); + } + if ($this->CB['delete'] ?? false) { + $clipObj->setCurrentPad((string)($this->CB['pad'] ?? '')); + $this->setDeleteCmd($clipObj); + } + } + } + + /** + * Executing the posted actions ... + */ + protected function processRequest(): void + { + // LOAD DataHandler with data and cmd arrays: + $this->tce->start($this->data, $this->cmd); + if ($this->mirror !== []) { + $this->tce->setMirror($this->mirror); + } + // Execute actions: + $this->tce->process_datamap(); + $this->tce->process_cmdmap(); + // Clearing cache: + if (!empty($this->cacheCmd)) { + $this->tce->clear_cacheCmd($this->cacheCmd); + } + // Update page tree? + if (isset($this->data['pages']) || isset($this->cmd['pages'])) { + BackendUtility::setUpdateSignal('updatePageTree'); + } + } + + /** + * Applies the proper paste configuration to $this->cmd + * + * The reference ($this->CB['paste']) has following format: [tablename]:[paste-uid]. + * Tablename is the name of the table from which elements *on the current clipboard* is pasted with the 'pid' paste-uid. + * No tablename means that all items on the clipboard (non-files) are pasted. This requires paste-uid to be positive though. + * so 'tt_content:-3' means 'paste tt_content elements on the clipboard to AFTER tt_content:3 record + * 'tt_content:30' means 'paste tt_content elements on the clipboard into page with id 30 + * ':30' means 'paste ALL database elements on the clipboard into page with id 30 + * ':-30' not valid. + */ + protected function setPasteCmd(Clipboard $clipboard): void + { + [$pasteTable, $pasteUid] = explode('|', (string)$this->CB['paste']); + $pasteUid = (int)$pasteUid; + // pUid must be set and if pTable is not set (that means paste ALL elements) + // the uid MUST be positive/zero (pointing to page id) + if (!$pasteTable && $pasteUid < 0) { + return; + } + $elements = $clipboard->elFromTable($pasteTable); + // So the order is preserved. + $elements = array_reverse($elements); + $mode = $clipboard->currentMode() === 'copy' ? 'copy' : 'move'; + // Traverse elements and make CMD array + foreach ($elements as $key => $value) { + [$table, $uid] = explode('|', $key); + if (!is_array($this->cmd[$table] ?? null)) { + $this->cmd[$table] = []; + } + if (is_array($this->CB['update'] ?? false)) { + $this->cmd[$table][$uid][$mode] = [ + 'action' => 'paste', + 'target' => $pasteUid, + 'update' => $this->CB['update'], + ]; + } else { + $this->cmd[$table][$uid][$mode] = $pasteUid; + } + if ($mode === 'move') { + $clipboard->removeElement($key); + } + } + $clipboard->endClipboard(); + } + + /** + * Applies the proper delete configuration to $this->cmd + */ + protected function setDeleteCmd(Clipboard $clipboard): void + { + foreach ($clipboard->elFromTable() as $key => $value) { + [$table, $uid] = explode('|', $key); + if (!is_array($this->cmd[$table])) { + $this->cmd[$table] = []; + } + $this->cmd[$table][$uid]['delete'] = 1; + $clipboard->removeElement($key); + } + $clipboard->endClipboard(); + } +} diff --git a/Classes/Controller/SiteConfigurationController.php b/Classes/Controller/SiteConfigurationController.php new file mode 100644 index 0000000..fa4fbd7 --- /dev/null +++ b/Classes/Controller/SiteConfigurationController.php @@ -0,0 +1,1087 @@ +getAttribute('moduleData'); + $viewMode = SetupModuleViewMode::tryFrom($moduleData->get('viewMode') ?? '') ?? SetupModuleViewMode::TILES; + $moduleData->set('viewMode', $viewMode->value); + + // forcing uncached sites will re-initialize `SiteFinder` + // which is used later by FormEngine (implicit behavior) + $allSites = $this->siteFinder->getAllSites(false); + $pages = $this->getAllSitePages(); + $unassignedSites = []; + $duplicatedRootPages = []; + foreach ($allSites as $identifier => $site) { + $rootPageId = $site->getRootPageId(); + if (isset($pages[$rootPageId]['siteConfiguration'])) { + // rootPage is already used in a site configuration + $duplicatedRootPages[$rootPageId][] = $pages[$rootPageId]['siteConfiguration']->getIdentifier(); + $duplicatedRootPages[$rootPageId][] = $site->getIdentifier(); + $duplicatedRootPages[$rootPageId] = array_unique($duplicatedRootPages[$rootPageId]); + } + if (isset($pages[$rootPageId])) { + $pages[$rootPageId]['siteIdentifier'] = $identifier; + $pages[$rootPageId]['siteConfiguration'] = $site; + } else { + $unassignedSites[] = $site; + } + } + + $rootPagesWithSiteConfiguration = []; + $rootPagesWithoutSiteConfiguration = []; + foreach ($pages as $page) { + if (!isset($page['siteConfiguration'])) { + $rootPagesWithoutSiteConfiguration[] = $page; + } else { + $rootPagesWithSiteConfiguration[] = $page; + } + } + + $view = $this->moduleTemplateFactory->create($request); + $view->getDocHeaderComponent()->setShortcutContext( + 'site_configuration', + $this->getLanguageService()->translate('short_description', 'backend.modules.site_configuration') + ); + $this->addDocHeaderViewModeButton($view, $viewMode); + $view->setTitle($this->getLanguageService()->translate('title', 'backend.modules.site_configuration')); + $view->setLayout(ModuleLayout::NORMAL); + $view->assignMultiple([ + 'pages' => $pages, + 'viewMode' => $viewMode, + 'unassignedSites' => $unassignedSites, + 'duplicatedRootPages' => $duplicatedRootPages, + 'duplicatedEntryPoints' => $this->getDuplicatedEntryPoints($allSites, $pages), + 'invalidSets' => $this->setRegistry->getInvalidSets(), + 'rootPagesWithSiteConfiguration' => $rootPagesWithSiteConfiguration, + 'rootPagesWithoutSiteConfiguration' => $rootPagesWithoutSiteConfiguration, + ]); + + return $view->renderResponse('SiteConfiguration/Overview'); + } + + /** + * This lists all information about a site: + * - URLs + * - Languages + Translation Strategy + */ + public function detailAction(ServerRequestInterface $request): ResponseInterface + { + $siteIdentifier = $request->getQueryParams()['site'] ?? null; + if (empty($siteIdentifier)) { + throw new \RuntimeException('Site identifier to show details must be set', 1763919655); + } + $site = $this->siteFinder->getSiteByIdentifier($siteIdentifier); + $pageRecord = BackendUtility::getRecord('pages', $site->getRootPageId()) ?? []; + + $settings = $this->siteSettingsService->getUncachedSettings($site); + $setSettings = $this->siteSettingsService->getSetSettings($site); + + $categoryEnhancer = function (Category $category) use (&$categoryEnhancer, $settings, $setSettings): Category { + return new Category(...[ + ...get_object_vars($category), + 'label' => $this->getLanguageService()->sL($category->label), + 'description' => $category->description !== null ? $this->getLanguageService()->sL($category->description) : $category->description, + 'categories' => array_map($categoryEnhancer, $category->categories), + 'settings' => array_map( + fn(SettingDefinition $definition): EditableSetting => new EditableSetting( + definition: $this->resolveSettingLabels($definition), + value: $settings->get($definition->key), + systemDefault: $setSettings->get($definition->key), + typeImplementation: $this->settingsTypeRegistry->get($definition->type)->getJavaScriptModule(), + ), + $category->settings + ), + ]); + }; + + $categories = array_map($categoryEnhancer, $this->categoryRegistry->getCategories(...$site->getSets())); + + $view = $this->moduleTemplateFactory->create($request); + $this->configureDetailViewDocHeader($view, $siteIdentifier, $request); + $view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord); + $view->setTitle( + $this->getLanguageService()->translate('title', 'backend.modules.site_settings') + ); + $view->setLayout(ModuleLayout::NORMAL); + $view->assignMultiple([ + 'site' => $site, + 'page' => $pageRecord, + 'categories' => $categories, + 'localSettings' => $this->siteSettingsService->getLocalSettings($site), + ]); + // @todo: Find CSP information (if active etc) + return $view->renderResponse('SiteConfiguration/Detail'); + } + + /** + * Shows a form to create a new site configuration, or edit an existing one. + * + * @throws \RuntimeException + */ + public function editAction(ServerRequestInterface $request): ResponseInterface + { + // forcing uncached sites will re-initialize `SiteFinder` + // which is used later by FormEngine (implicit behavior) + $allSites = $this->siteFinder->getAllSites(false); + + $fullTca = array_merge($GLOBALS['TCA'], $this->siteTcaConfiguration->getTca()); + $pageUid = (int)($request->getQueryParams()['pageUid'] ?? 0); + $siteIdentifier = $request->getQueryParams()['site'] ?? null; + + if (empty($siteIdentifier) && empty($pageUid)) { + throw new \RuntimeException('Either site identifier to edit a config or page uid to add new config must be set', 1521561148); + } + $isNewConfig = empty($siteIdentifier); + + $defaultValues = []; + if ($isNewConfig) { + $defaultValues['site']['rootPageId'] = $pageUid; + $pageRecord = BackendUtility::getRecord('pages', $pageUid) ?? []; + } else { + $site = $this->siteFinder->getSiteByIdentifier($siteIdentifier); + $pageRecord = BackendUtility::getRecord('pages', $site->getRootPageId()) ?? []; + } + + if (!$isNewConfig && !isset($allSites[$siteIdentifier])) { + throw new \RuntimeException('Existing config for site ' . $siteIdentifier . ' not found', 1521561226); + } + + $returnUrl = $this->resolveReturnUrl($request); + + $formDataCompilerInput = [ + 'request' => $request, + 'tableName' => 'site', + 'vanillaUid' => $isNewConfig ? $pageUid : $allSites[$siteIdentifier]->getRootPageId(), + 'command' => $isNewConfig ? 'new' : 'edit', + 'returnUrl' => $returnUrl, + 'customData' => [ + 'siteIdentifier' => $isNewConfig ? '' : $siteIdentifier, + ], + 'defaultValues' => $defaultValues, + 'tcaSchemata' => $this->tcaSchemaBuilder->buildFromStructure($fullTca), + 'fullTca' => $fullTca, + ]; + $formData = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(SiteConfigurationDataGroup::class)); + $formData['renderType'] = 'formWrapContainer'; + $formResult = $this->nodeFactory->create($formData)->render(); + $languageService = $this->getLanguageService(); + $documentTitle = $this->resolveDocumentTitle($languageService, $isNewConfig, $siteIdentifier); + $formResult['html'] = '

' . htmlspecialchars($documentTitle) . '

' . $formResult['html']; + $formResult = $this->formResultFactory->create($formResult); + $this->formResultHandler->addAssets($formResult); + + $view = $this->moduleTemplateFactory->create($request); + $view->assignMultiple([ + // Always add rootPageId as additional field to have a reference for new records + 'rootPageId' => $isNewConfig ? $pageUid : $allSites[$siteIdentifier]->getRootPageId(), + 'returnUrl' => $returnUrl, + 'formEngineHtml' => $formResult->html, + ]); + $this->configureEditViewDocHeader($view, $siteIdentifier, $documentTitle); + $view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord); + $view->setTitle($documentTitle); + $view->setLayout(ModuleLayout::NORMAL); + return $view->renderResponse('SiteConfiguration/Edit'); + } + + /** + * Save incoming data from editAction and redirect to overview or edit + * + * @throws \RuntimeException + */ + public function saveAction(ServerRequestInterface $request): ResponseInterface + { + // loading uncached site configurations without settings.yaml + /** @var array $mappingRootPageToSite */ + $mappingRootPageToSite = []; + $allSites = $this->siteConfiguration->resolveAllExistingSitesRaw(); + foreach ($allSites as $site) { + $mappingRootPageToSite[$site->getRootPageId()] = $site; + } + + $parsedBody = $request->getParsedBody(); + $returnUrl = $this->resolveReturnUrl($request); + + if (isset($parsedBody['closeDoc']) && (int)$parsedBody['closeDoc'] === 1) { + // Closing means no save, just redirect to overview + return new RedirectResponse($returnUrl); + } + $isSave = $parsedBody['_savedok'] ?? false; + $isSaveClose = $parsedBody['_saveandclosedok'] ?? false; + if (!$isSave && !$isSaveClose) { + throw new \RuntimeException('Either save or save and close', 1520370364); + } + + if (!isset($parsedBody['data']['site']) || !is_array($parsedBody['data']['site'])) { + throw new \RuntimeException('No site data or site identifier given', 1521030950); + } + + $data = $parsedBody['data']; + // This can be NEW123 for new records + $unprocessedPageId = key($data['site']); + $isRootPageIdPlaceholder = $this->envPlaceholderProcessor->canProcess((string)$unprocessedPageId); + $pageId = $isRootPageIdPlaceholder + ? (int)$this->envPlaceholderProcessor->process($unprocessedPageId) + : (int)$unprocessedPageId; + + $sysSiteRow = current($data['site']); + $siteIdentifier = $sysSiteRow['identifier'] ?? ''; + + $isNewConfiguration = false; + $currentIdentifier = ''; + if (isset($mappingRootPageToSite[$pageId])) { + $currentSite = $mappingRootPageToSite[$pageId]; + $currentSiteConfiguration = $currentSite->getConfiguration(); + $currentIdentifier = $currentSite->getIdentifier(); + } else { + $currentSiteConfiguration = []; + $isNewConfiguration = true; + $pageId = (int)$parsedBody['rootPageId']; + if ($pageId <= 0) { + // Early validation of rootPageId - it must always be given and greater than 0 + throw new \RuntimeException('No root page id found', 1521719709); + } + } + + $siteTca = $this->siteTcaConfiguration->getTca(); + // Validate site identifier and do not store or further process it + $siteIdentifier = $this->validateAndProcessIdentifier($isNewConfiguration, $siteIdentifier, $pageId, $allSites, $mappingRootPageToSite, $siteTca); + unset($sysSiteRow['identifier']); + + try { + $newSysSiteData = []; + // Hard set rootPageId: This is TCA readOnly and not transmitted by FormEngine, but is also the "uid" of the site record + $newSysSiteData['rootPageId'] = $isRootPageIdPlaceholder ? $unprocessedPageId : $pageId; + foreach ($sysSiteRow as $fieldName => $fieldValue) { + $type = $siteTca['site']['columns'][$fieldName]['config']['type']; + $renderType = $siteTca['site']['columns'][$fieldName]['config']['renderType'] ?? ''; + switch ($type) { + case 'input': + case 'number': + case 'email': + case 'link': + case 'datetime': + case 'color': + case 'text': + $fieldValue = $this->validateAndProcessValue('site', $fieldName, $fieldValue, $siteTca); + $newSysSiteData[$fieldName] = $fieldValue; + break; + + case 'inline': + $newSysSiteData[$fieldName] = []; + $childRowIds = GeneralUtility::trimExplode(',', $fieldValue, true); + if (!isset($siteTca['site']['columns'][$fieldName]['config']['foreign_table'])) { + throw new \RuntimeException('No foreign_table found for inline type', 1521555037); + } + $foreignTable = $siteTca['site']['columns'][$fieldName]['config']['foreign_table']; + foreach ($childRowIds as $childRowId) { + $childRowData = []; + if (!isset($data[$foreignTable][$childRowId])) { + if (!empty($currentSiteConfiguration[$fieldName][$childRowId])) { + // A collapsed inline record: Fetch data from existing config + $newSysSiteData[$fieldName][] = $currentSiteConfiguration[$fieldName][$childRowId]; + continue; + } + throw new \RuntimeException('No data found for table ' . $foreignTable . ' with id ' . $childRowId, 1521555177); + } + $childRow = $data[$foreignTable][$childRowId]; + foreach ($childRow as $childFieldName => $childFieldValue) { + if ($childFieldName === 'pid') { + // pid is added by inline by default, but not relevant for yml storage + continue; + } + $type = $siteTca[$foreignTable]['columns'][$childFieldName]['config']['type']; + switch ($type) { + case 'input': + case 'number': + case 'email': + case 'link': + case 'datetime': + case 'color': + case 'select': + case 'text': + $childRowData[$childFieldName] = $childFieldValue; + break; + case 'check': + $childRowData[$childFieldName] = (bool)$childFieldValue; + break; + default: + throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1521555340); + } + } + $newSysSiteData[$fieldName][] = $childRowData; + } + break; + + case 'siteLanguage': + if (!isset($siteTca['site_language'])) { + throw new \RuntimeException('Required foreign table site_language does not exist', 1624286811); + } + if (!isset($siteTca['site_language']['columns']['languageId']) + || ($siteTca['site_language']['columns']['languageId']['config']['type'] ?? '') !== 'select' + ) { + throw new \RuntimeException( + 'Required foreign field languageId does not exist or is not of type select', + 1624286812 + ); + } + $newSysSiteData[$fieldName] = []; + $lastLanguageId = $this->getLastLanguageId(); + foreach (GeneralUtility::trimExplode(',', $fieldValue, true) as $childRowId) { + if (!isset($data['site_language'][$childRowId])) { + if (!empty($currentSiteConfiguration[$fieldName][$childRowId])) { + $newSysSiteData[$fieldName][] = $currentSiteConfiguration[$fieldName][$childRowId]; + continue; + } + throw new \RuntimeException('No data found for table site_language with id ' . $childRowId, 1624286813); + } + $childRowData = []; + foreach ($data['site_language'][$childRowId] ?? [] as $childFieldName => $childFieldValue) { + if ($childFieldName === 'pid') { + // pid is added by default, but not relevant for yml storage + continue; + } + if ($childFieldName === 'languageId' + && (int)$childFieldValue === PHP_INT_MAX + && str_starts_with($childRowId, 'NEW') + ) { + // In case we deal with a new site language, whose "languageID" field is + // set to the PHP_INT_MAX placeholder, the next available language ID has + // to be used (auto-increment). + $childRowData[$childFieldName] = ++$lastLanguageId; + continue; + } + $type = $siteTca['site_language']['columns'][$childFieldName]['config']['type']; + switch ($type) { + case 'input': + case 'number': + case 'email': + case 'link': + case 'datetime': + case 'color': + case 'select': + case 'text': + $childRowData[$childFieldName] = $childFieldValue; + break; + case 'check': + $childRowData[$childFieldName] = (bool)$childFieldValue; + break; + default: + throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1624286814); + } + } + $newSysSiteData[$fieldName][] = $childRowData; + } + break; + + case 'select': + if ($renderType === 'selectMultipleSideBySide') { + $fieldValues = is_array($fieldValue) ? $fieldValue : GeneralUtility::trimExplode(',', $fieldValue, true); + $newSysSiteData[$fieldName] = $fieldValues; + } else { + if (MathUtility::canBeInterpretedAsInteger($fieldValue)) { + $fieldValue = (int)$fieldValue; + } elseif (is_array($fieldValue)) { + $fieldValue = implode(',', $fieldValue); + } + $newSysSiteData[$fieldName] = $fieldValue; + } + + break; + + case 'check': + $newSysSiteData[$fieldName] = (bool)$fieldValue; + break; + + default: + throw new \RuntimeException('TCA type "' . $type . '" is not implemented in site handling', 1521032781); + } + } + + $newSiteConfiguration = $this->validateFullStructure( + $this->getMergeSiteData($currentSiteConfiguration, $newSysSiteData), + $isNewConfiguration + ); + + // Persist the configuration + try { + if (!$isNewConfiguration && $currentIdentifier !== $siteIdentifier) { + $this->siteWriter->rename($currentIdentifier, $siteIdentifier); + $this->getBackendUser()->writelog(Type::SITE, SiteAction::RENAME, SystemLogErrorClassification::MESSAGE, null, 'Site configuration \'%s\' was renamed to \'%s\'.', [$currentIdentifier, $siteIdentifier], 'site'); + } + $this->siteWriter->write($siteIdentifier, $newSiteConfiguration, true); + if ($isNewConfiguration) { + $this->getBackendUser()->writelog(Type::SITE, SiteAction::CREATE, SystemLogErrorClassification::MESSAGE, null, 'Site configuration \'%s\' was created.', [$siteIdentifier], 'site'); + } else { + $this->getBackendUser()->writelog(Type::SITE, SiteAction::UPDATE, SystemLogErrorClassification::MESSAGE, null, 'Site configuration \'%s\' was updated.', [$siteIdentifier], 'site'); + } + } catch (SiteConfigurationWriteException $e) { + $flashMessage = new FlashMessage($e->getMessage(), '', ContextualFeedbackSeverity::WARNING, true); + $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + } catch (SiteValidationErrorException $e) { + // Do not store new config if a validation error is thrown, but redirect only to show a generated flash message + } + + $saveRoute = $this->uriBuilder->buildUriFromRoute('site_configuration.edit', [ + 'site' => $siteIdentifier, + 'returnUrl' => $returnUrl, + ]); + if ($isSaveClose) { + return new RedirectResponse($returnUrl); + } + return new RedirectResponse($saveRoute); + } + + /** + * Validation and processing of site identifier + * + * @param bool $isNew If true, we're dealing with a new record + * @param string $identifier Given identifier to validate and process + * @param int $rootPageId Page uid this identifier is bound to + * @param array $allSites All sites loaded without `settings.yaml`. + * @param array $mappingRootPageToSite Identifier site mapping as lookup. Not loaded `settings.yaml`. + * @param array $siteTca TCA for site + * @return mixed Verified / modified value + */ + protected function validateAndProcessIdentifier(bool $isNew, string $identifier, int $rootPageId, array $allSites, array $mappingRootPageToSite, array $siteTca) + { + $languageService = $this->getLanguageService(); + // Normal "eval" processing of field first + $identifier = $this->validateAndProcessValue('site', 'identifier', $identifier, $siteTca); + if ($isNew) { + // Verify no other site with this identifier exists. If so, find a new unique name as + // identifier and show a flash message the identifier has been adapted + if (($allSites[$identifier] ?? null) instanceof Site) { + // Force this identifier to be unique + $originalIdentifier = $identifier; + $identifier = StringUtility::getUniqueId($identifier . '-'); + $message = sprintf( + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.message'), + $originalIdentifier, + $identifier + ); + $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.title'); + $flashMessage = new FlashMessage($message, $messageTitle, ContextualFeedbackSeverity::WARNING, true); + $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + } else { + // If this is an existing config, the site for this identifier must have the same rootPageId, otherwise + // a user tried to rename a site identifier to a different site that already exists. If so, we do not rename + // the site and show a flash message + $site = ($allSites[$identifier] ?? null); + if ($site instanceof Site + && $site->getRootPageId() !== $rootPageId + && ($mappingRootPageToSite[$rootPageId] ?? null) instanceof Site + ) { + // Find original value and keep this + $origSite = $mappingRootPageToSite[$rootPageId]; + $originalIdentifier = $identifier; + $identifier = $origSite->getIdentifier(); + $message = sprintf( + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.message'), + $originalIdentifier, + $identifier + ); + $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.title'); + $flashMessage = new FlashMessage($message, $messageTitle, ContextualFeedbackSeverity::WARNING, true); + $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + } + return $identifier; + } + + /** + * Simple validation and processing method for incoming form field values. + * + * Note this does not support all TCA "eval" options but only what we really need. + * + * @param string $tableName Table name + * @param string $fieldName Field name + * @param mixed $fieldValue Incoming value from FormEngine + * @param array $siteTca TCA for site + * @return mixed Verified / modified value + * @throws SiteValidationErrorException + * @throws \RuntimeException + */ + protected function validateAndProcessValue(string $tableName, string $fieldName, $fieldValue, array $siteTca) + { + $languageService = $this->getLanguageService(); + $fieldConfig = $siteTca[$tableName]['columns'][$fieldName]['config']; + $handledEvals = []; + + if (!$this->validateValueForRequired($fieldConfig, $fieldValue)) { + // Validation throws - these should be handled client side already, + // eg. 'required' being set and receiving empty, shouldn't happen server side + $message = sprintf( + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.message'), + $fieldName + ); + $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.title'); + $flashMessage = new FlashMessage($message, $messageTitle, ContextualFeedbackSeverity::WARNING, true); + $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + throw new SiteValidationErrorException( + 'Field ' . $fieldName . ' is set to required, but received empty.', + 1521726421 + ); + } + + if (!empty($fieldConfig['eval'])) { + $evalArray = GeneralUtility::trimExplode(',', $fieldConfig['eval'], true); + // Processing + if (in_array('alphanum_x', $evalArray, true)) { + $handledEvals[] = 'alphanum_x'; + $fieldValue = preg_replace('/[^a-zA-Z0-9_-]/', '', $fieldValue); + } + if (in_array('lower', $evalArray, true)) { + $handledEvals[] = 'lower'; + $fieldValue = mb_strtolower($fieldValue, 'utf-8'); + } + if (in_array('trim', $evalArray, true)) { + $handledEvals[] = 'trim'; + $fieldValue = trim($fieldValue); + } + if (in_array('int', $evalArray, true)) { + $handledEvals[] = 'int'; + $fieldValue = (int)$fieldValue; + } + if (!empty(array_diff($evalArray, $handledEvals))) { + throw new \RuntimeException('At least one not implemented \'eval\' in list ' . $fieldConfig['eval'], 1522491734); + } + } + if (isset($fieldConfig['range']['lower'])) { + $fieldValue = (int)$fieldValue < (int)$fieldConfig['range']['lower'] ? (int)$fieldConfig['range']['lower'] : (int)$fieldValue; + } + if (isset($fieldConfig['range']['upper'])) { + $fieldValue = (int)$fieldValue > (int)$fieldConfig['range']['upper'] ? (int)$fieldConfig['range']['upper'] : (int)$fieldValue; + } + return $fieldValue; + } + + /** + * Last sanitation method after all data has been gathered. Check integrity + * of full record, manipulate if possible, or throw exception if unfixable broken. + * + * @param array $newSysSiteData Incoming data + * @param bool $isNewConfiguration Flag whether site configuration is new + * @return array Updated data if needed + * @throws \RuntimeException + */ + protected function validateFullStructure(array $newSysSiteData, bool $isNewConfiguration): array + { + $languageService = $this->getLanguageService(); + // Verify there are not two error handlers with the same error code + if (isset($newSysSiteData['errorHandling']) && is_array($newSysSiteData['errorHandling'])) { + $uniqueCriteria = []; + $validChildren = []; + foreach ($newSysSiteData['errorHandling'] as $child) { + if (!isset($child['errorCode'])) { + throw new \RuntimeException('No errorCode found', 1521788518); + } + if (!in_array((int)$child['errorCode'], $uniqueCriteria, true)) { + $uniqueCriteria[] = (int)$child['errorCode']; + $child['errorCode'] = (int)$child['errorCode']; + $validChildren[] = $child; + } else { + $message = sprintf( + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.message'), + $child['errorCode'] + ); + $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.title'); + $flashMessage = new FlashMessage($message, $messageTitle, ContextualFeedbackSeverity::WARNING, true); + $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + } + $newSysSiteData['errorHandling'] = $validChildren; + } + + // Verify there is at least one site_language element configured. + if (!isset($newSysSiteData['languages']) || !is_array($newSysSiteData['languages']) || count($newSysSiteData['languages']) < 1) { + throw new \RuntimeException( + 'No default language definition found. The interface does not allow this. Aborting', + 1521789306 + ); + } + $uniqueCriteria = []; + $validChildren = []; + foreach ($newSysSiteData['languages'] as $child) { + if (!isset($child['languageId'])) { + throw new \RuntimeException('languageId not found', 1521789455); + } + if (!in_array((int)$child['languageId'], $uniqueCriteria, true)) { + $uniqueCriteria[] = (int)$child['languageId']; + $child['languageId'] = (int)$child['languageId']; + $validChildren[] = $child; + } else { + $message = sprintf( + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.message'), + $child['languageId'] + ); + $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.title'); + $flashMessage = new FlashMessage($message, $messageTitle, ContextualFeedbackSeverity::WARNING, true); + $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + } + // On new site configurations, ensure that the only existing language has the languageId set to 0 + // @todo: this shouldn't be done here, but rather properly handled in saveAction() where 'siteLanguage' is handled + if ($isNewConfiguration && count($validChildren) === 1) { + $validChildren[0]['languageId'] = 0; + } + $newSysSiteData['languages'] = $validChildren; + + // cleanup configuration + foreach ($newSysSiteData as $identifier => $value) { + if (is_array($value) && empty($value)) { + unset($newSysSiteData[$identifier]); + } + } + + return $newSysSiteData; + } + + /** + * Delete an existing configuration + */ + public function deleteAction(ServerRequestInterface $request): ResponseInterface + { + $siteIdentifier = $request->getParsedBody()['site'] ?? ''; + if (empty($siteIdentifier)) { + throw new \RuntimeException('Not site identifier given', 1521565182); + } + try { + // Verify site does exist, method throws if not + $this->siteWriter->delete($siteIdentifier); + $this->getBackendUser()->writelog(Type::SITE, SiteAction::DELETE, SystemLogErrorClassification::MESSAGE, null, 'Site configuration \'%s\' was deleted.', [$siteIdentifier], 'site'); + } catch (SiteConfigurationWriteException $e) { + $flashMessage = new FlashMessage($e->getMessage(), '', ContextualFeedbackSeverity::WARNING, true); + $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + $overviewRoute = $this->uriBuilder->buildUriFromRoute('site_configuration'); + return new RedirectResponse($overviewRoute); + } + + /** + * Create document header buttons of "detail" action + */ + protected function configureDetailViewDocHeader(ModuleTemplate $view, ?string $siteIdentifier, ServerRequestInterface $request): void + { + // Back button + if ($returnUrl = $this->resolveReturnUrl($request)) { + $view->addButtonToButtonBar($this->componentFactory->createBackButton($returnUrl)); + } + + if ($siteIdentifier) { + // 'Edit site configuration' button + $editSiteConfigurationButton = $this->componentFactory->createLinkButton() + ->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:edit.site_configuration')) + ->setIcon($this->iconFactory->getIcon('actions-open', IconSize::SMALL)) + ->setShowLabelText(true) + ->setHref((string)$this->uriBuilder->buildUriFromRoute('site_configuration.edit', [ + 'site' => $siteIdentifier, + 'returnUrl' => $this->uriBuilder->buildUriFromRoute('site_configuration.detail', [ + 'site' => $siteIdentifier, + ]), + ])); + $view->addButtonToButtonBar($editSiteConfigurationButton, ButtonBar::BUTTON_POSITION_LEFT, 3); + + // 'Edit site settings' button + $editSiteSettingsButton = $this->componentFactory->createLinkButton() + ->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:edit.editSiteSettings')) + ->setIcon($this->iconFactory->getIcon('actions-cog', IconSize::SMALL)) + ->setShowLabelText(true) + ->setHref((string)$this->uriBuilder->buildUriFromRoute('site_configuration.editSettings', [ + 'site' => $siteIdentifier, + 'returnUrl' => $this->uriBuilder->buildUriFromRoute('site_configuration.detail', [ + 'site' => $siteIdentifier, + ]), + ])); + $view->addButtonToButtonBar($editSiteSettingsButton, ButtonBar::BUTTON_POSITION_LEFT, 5); + } + + // Set shortcut context - reload button is added automatically + $view->getDocHeaderComponent()->setShortcutContext( + 'site_configuration.detail', + sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:labels.detail'), $siteIdentifier), + ['site' => $siteIdentifier] + ); + } + + /** + * Create document header buttons of "edit" action + */ + protected function configureEditViewDocHeader(ModuleTemplate $view, ?string $siteIdentifier, string $documentTitle = ''): void + { + $lang = $this->getLanguageService(); + $closeButton = $this->componentFactory->createCloseButton('#') + ->setClasses('t3js-editform-close'); + $saveButton = $this->componentFactory->createSaveButton('siteConfigurationController'); + $view->addButtonToButtonBar($closeButton); + $view->addButtonToButtonBar($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2); + if ($siteIdentifier) { + $editSiteSettingsButton = $this->componentFactory->createLinkButton() + ->setTitle($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:edit.editSiteSettings')) + ->setIcon($this->iconFactory->getIcon('actions-open', IconSize::SMALL)) + ->setShowLabelText(true) + ->setHref((string)$this->uriBuilder->buildUriFromRoute('site_configuration.editSettings', [ + 'site' => $siteIdentifier, + 'returnUrl' => $this->uriBuilder->buildUriFromRoute('site_configuration.edit', [ + 'site' => $siteIdentifier, + ]), + ])); + $view->addButtonToButtonBar($editSiteSettingsButton, ButtonBar::BUTTON_POSITION_LEFT, 3); + } + // Set shortcut context - reload button is added automatically + $view->getDocHeaderComponent()->setShortcutContext( + 'site_configuration.edit', + $documentTitle, + ['site' => $siteIdentifier], + ); + } + + /** + * Resolves the document title used for the browser tab and shortcut. + */ + protected function resolveDocumentTitle(LanguageService $languageService, bool $isNewConfig, ?string $siteIdentifier): string + { + $typeLabel = $languageService->sL('backend.siteconfiguration:edit.typeLabel'); + if ($isNewConfig) { + return $languageService->sL('backend.siteconfiguration:edit.createNewSite'); + } + return implode(' · ', array_filter([$siteIdentifier, $typeLabel])); + } + + /** + * View mode + */ + protected function addDocHeaderViewModeButton(ModuleTemplate $moduleTemplate, SetupModuleViewMode $viewMode): void + { + $languageService = $this->getLanguageService(); + $viewModeButton = $this->componentFactory->createDropDownButton() + ->setLabel($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view')) + ->setIcon($this->iconFactory->getIcon('actions-cog')) + ->setShowLabelText(true); + + $viewModeButton->addItem( + $this->componentFactory->createDropDownRadio() + ->setActive(($viewMode === SetupModuleViewMode::TILES)) + ->setHref( + (string)$this->uriBuilder->buildUriFromRoute( + 'site_configuration', + [ + 'viewMode' => SetupModuleViewMode::TILES->value, + ] + ) + ) + ->setLabel($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.tiles')) + ->setIcon($this->iconFactory->getIcon('actions-viewmode-tiles', IconSize::SMALL)) + ); + + $viewModeButton->addItem( + $this->componentFactory->createDropDownRadio() + ->setActive(($viewMode === SetupModuleViewMode::LIST)) + ->setHref( + (string)$this->uriBuilder->buildUriFromRoute( + 'site_configuration', + [ + 'viewMode' => SetupModuleViewMode::LIST->value, + ] + ) + ) + ->setLabel($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.list')) + ->setIcon($this->iconFactory->getIcon('actions-viewmode-list', IconSize::SMALL)) + ); + + $moduleTemplate->addButtonToButtonBar($viewModeButton, ButtonBar::BUTTON_POSITION_RIGHT, 2); + } + + /** + * Returns a list of pages that have 'is_siteroot' set + * or are on pid 0 and not in list of excluded doktypes + */ + protected function getAllSitePages(): array + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); + $queryBuilder->getRestrictions()->removeAll(); + $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, 0)); + $statement = $queryBuilder + ->select('*') + ->from('pages') + ->where( + $queryBuilder->expr()->or( + $queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter(\Local\Multilanguage\Service\DefaultLanguageTagService::getTag())), + $queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter('')), + ), + $queryBuilder->expr()->or( + $queryBuilder->expr()->and( + $queryBuilder->expr()->eq('pid', 0), + $queryBuilder->expr()->notIn('doktype', [ + PageRepository::DOKTYPE_SYSFOLDER, + PageRepository::DOKTYPE_SPACER, + PageRepository::DOKTYPE_LINK, + ]) + ), + $queryBuilder->expr()->eq('is_siteroot', 1) + ) + ) + ->orderBy('pid') + ->addOrderBy('sorting') + ->executeQuery(); + + $pages = []; + while ($row = $statement->fetchAssociative()) { + $row['rootline'] = BackendUtility::BEgetRootLine((int)$row['uid']); + array_pop($row['rootline']); + $row['rootline'] = array_reverse($row['rootline']); + $pages[(int)$row['uid']] = $row; + } + return $pages; + } + + /** + * Get all entry duplicates which are used multiple times + * + * @param Site[] $allSites + */ + protected function getDuplicatedEntryPoints(array $allSites, array $pages): array + { + $duplicatedEntryPoints = []; + + foreach ($allSites as $site) { + if (!isset($pages[$site->getRootPageId()])) { + continue; + } + foreach ($site->getAllLanguages() as $language) { + $base = $language->getBase(); + $entryPoint = rtrim((string)$language->getBase(), '/'); + $scheme = $base->getScheme() ? $base->getScheme() . '://' : '//'; + $entryPointWithoutScheme = str_replace($scheme, '', $entryPoint); + if (!isset($duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint])) { + $duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint] = 1; + } else { + $duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint]++; + } + } + } + return array_filter($duplicatedEntryPoints, static function (array $variants): bool { + return count($variants) > 1 || reset($variants) > 1; + }, ARRAY_FILTER_USE_BOTH); + } + + /** + * Returns the last (highest) language id from all sites + */ + protected function getLastLanguageId(): int + { + $lastLanguageId = 0; + foreach ($this->siteFinder->getAllSites() as $site) { + foreach ($site->getAllLanguages() as $language) { + if ($language->getLanguageId() > $lastLanguageId) { + $lastLanguageId = $language->getLanguageId(); + } + } + } + return $lastLanguageId; + } + + /** + * Checks if required=TRUE is set. + * If set: checks if the value is not empty (or not "0"). + * If not set or set to FALSE: Returns TRUE. + */ + protected function validateValueForRequired(array $tcaFieldConfig, mixed $value): bool + { + if (!($tcaFieldConfig['required'] ?? false)) { + return true; + } + + return !empty($value) || $value === '0'; + } + + /** + * Method keeps root config objects, which are not given via GUI. This way, + * extension authors are able to use their own objects on root level that are + * not configurable via GUI. However: We overwrite the full subset of any GUI + * object to make sure we have a clean state. + * + * Additionally, we also keep the baseVariants of languages, since they + * can't be modified via the GUI, but are part of the public API. + */ + protected function getMergeSiteData(array $currentSiteConfiguration, array $newSysSiteData): array + { + $newSysSiteData = array_merge($currentSiteConfiguration, $newSysSiteData); + + // @todo: this should go away, once base variants for languages are managable via the GUI. + $existingLanguageConfigurationsWithBaseVariants = []; + $existingLanguagesWithLegacyProperties = []; + foreach ($currentSiteConfiguration['languages'] ?? [] as $languageConfiguration) { + if (isset($languageConfiguration['baseVariants'])) { + $existingLanguageConfigurationsWithBaseVariants[$languageConfiguration['languageId']] = $languageConfiguration['baseVariants']; + } + if (isset($languageConfiguration['typo3Language'])) { + $existingLanguagesWithLegacyProperties[$languageConfiguration['languageId']]['typo3Language'] = $languageConfiguration['typo3Language']; + } + if (isset($languageConfiguration['iso-639-1'])) { + $existingLanguagesWithLegacyProperties[$languageConfiguration['languageId']]['iso-639-1'] = $languageConfiguration['iso-639-1']; + } + if (isset($languageConfiguration['direction'])) { + $existingLanguagesWithLegacyProperties[$languageConfiguration['languageId']]['direction'] = $languageConfiguration['direction']; + } + } + foreach ($newSysSiteData['languages'] ?? [] as $key => $languageConfiguration) { + $languageId = $languageConfiguration['languageId']; + if (isset($existingLanguageConfigurationsWithBaseVariants[$languageId])) { + $newSysSiteData['languages'][$key]['baseVariants'] = $existingLanguageConfigurationsWithBaseVariants[$languageId]; + } + foreach ($existingLanguagesWithLegacyProperties[$languageId] ?? [] as $propertyName => $propertyValue) { + $newSysSiteData['languages'][$key][$propertyName] = $propertyValue; + } + } + + return $newSysSiteData; + } + + private function resolveSettingLabels(SettingDefinition $definition): SettingDefinition + { + $languageService = $this->getLanguageService(); + return new SettingDefinition(...[ + ...get_object_vars($definition), + 'label' => $languageService->sL($definition->label), + 'description' => $definition->description !== null ? $languageService->sL($definition->description) : null, + 'enum' => array_map( + static fn(string|int|float|bool $label): string => $languageService->sL((string)$label), + $definition->enum + ), + ]); + } + + protected function resolveReturnUrl(ServerRequestInterface $request): string + { + return GeneralUtility::sanitizeLocalUrl( + (string)($request->getParsedBody()['returnUrl'] ?? $request->getQueryParams()['returnUrl'] ?? ''), + $request + ) ?: (string)$this->uriBuilder->buildUriFromRoute('site_configuration'); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/SiteInlineAjaxController.php b/Classes/Controller/SiteInlineAjaxController.php new file mode 100644 index 0000000..cfd6a9b --- /dev/null +++ b/Classes/Controller/SiteInlineAjaxController.php @@ -0,0 +1,403 @@ +getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax']; + $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']); + $domObjectId = $ajaxArguments[0]; + $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); + $childChildUid = null; + if (isset($ajaxArguments[1]) && MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) { + $childChildUid = (int)$ajaxArguments[1]; + } + $siteTca = $this->siteTcaConfiguration->getTca(); + $fullTca = array_merge($GLOBALS['TCA'], $siteTca); + $tcaSchemata = $this->tcaSchemaBuilder->buildFromStructure($fullTca); + + // Parse the DOM identifier, add the levels to the structure stack + $inlineStructure = $this->inlineStackProcessor->getStructureFromString($domObjectId, $tcaSchemata); + $inlineStructure = $this->inlineStackProcessor->addAjaxConfigurationToStructure($inlineStructure, $parentConfig); + $inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0); + // Parent, this table embeds the child table + $inlineParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, -1); + // Child, a record from this table should be rendered + $child = $this->inlineStackProcessor->getUnstableStructureFromStructure($inlineStructure); + if (MathUtility::canBeInterpretedAsInteger($child['uid'] ?? false)) { + // If uid comes in, it is the id of the record neighbor record "create after" + $childVanillaUid = -1 * abs((int)$child['uid']); + } else { + // Else inline first Pid is the storage pid of new inline records + $childVanillaUid = (int)$inlineFirstPid; + } + $childTableName = $parentConfig['foreign_table']; + $defaultDatabaseRow = []; + + if ($childTableName === 'site_language') { + if ($childChildUid !== null) { + $language = $this->getLanguageById($childChildUid); + if ($language !== null) { + $defaultDatabaseRow['languageId'] = $language->getLanguageId(); + $defaultDatabaseRow['locale'] = $language->getLocale()->posixFormatted(); + if ($language->getTitle() !== '') { + $defaultDatabaseRow['title'] = $language->getTitle(); + } + if ($language->getBase()->getPath() !== '/') { + $defaultDatabaseRow['base'] = '/' . strtolower($language->getLocale()->getName()) . '/'; + } + if ($language->getHreflang(true) !== '') { + $defaultDatabaseRow['hreflang'] = $language->getHreflang(); + } + if ($language->getNavigationTitle() !== '') { + $defaultDatabaseRow['navigationTitle'] = $language->getNavigationTitle(); + } + if (str_starts_with($language->getFlagIdentifier(), 'flags-')) { + $flagIdentifier = str_replace('flags-', '', $language->getFlagIdentifier()); + $defaultDatabaseRow['flag'] = ($flagIdentifier === 'multiple') ? 'global' : $flagIdentifier; + } + } elseif ($childChildUid !== 0) { + // In case no language could be found for $childChildUid and + // its value is not "0", which is a special case as the default + // language is added automatically, throw a custom exception. + throw new \RuntimeException('Referenced language not found', 1521783937); + } + } else { + // Set new childs' UID to PHP_INT_MAX, as this is the placeholder UID for + // new records, created with the "Create new" button. This is necessary + // as we use the "inline selector" mode which usually does not allow + // to create new records besides the ones, defined in the selector. + // The correct UID will then be calculated by the controller. + $childChildUid = PHP_INT_MAX; + + if (!empty($ajaxArguments[2])) { + $defaultDatabaseRow = $this->siteLanguagePresets->getPresetDetailsForLanguage($ajaxArguments[2]) ?? []; + } + } + } + + $formDataCompilerInput = [ + 'request' => $request, + 'command' => 'new', + 'tableName' => $childTableName, + 'vanillaUid' => $childVanillaUid, + 'databaseRow' => $defaultDatabaseRow, + 'isInlineChild' => true, + 'inlineStructure' => $inlineStructure, + 'inlineFirstPid' => $inlineFirstPid, + 'inlineParentUid' => $inlineParent['uid'], + 'inlineParentTableName' => $inlineParent['table'], + 'inlineParentFieldName' => $inlineParent['field'], + 'inlineParentConfig' => $parentConfig, + 'inlineTopMostParentUid' => $inlineTopMostParent['uid'], + 'inlineTopMostParentTableName' => $inlineTopMostParent['table'], + 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'], + 'tcaSchemata' => $tcaSchemata, + 'fullTca' => $fullTca, + ]; + if ($childChildUid) { + $formDataCompilerInput['inlineChildChildUid'] = $childChildUid; + } + $childData = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(SiteConfigurationDataGroup::class)); + + if (($parentConfig['foreign_selector'] ?? false) && ($parentConfig['appearance']['useCombination'] ?? false)) { + throw new \RuntimeException('useCombination not implemented in sites module', 1522493094); + } + + $childData['inlineParentUid'] = (int)$inlineParent['uid']; + $childData['renderType'] = 'inlineRecordContainer'; + $childResult = $this->nodeFactory->create($childData)->render(); + + $jsonArray = [ + 'data' => '', + 'stylesheetFiles' => [], + 'scriptItems' => new JavaScriptItems(), + 'compilerInput' => [ + 'uid' => $childData['databaseRow']['uid'], + 'childChildUid' => $childChildUid, + ], + ]; + + $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult); + + return new JsonResponse($jsonArray); + } + + /** + * Show the details of site configuration child records. + * + * @throws \RuntimeException + */ + public function openInlineChildAction(ServerRequestInterface $request): ResponseInterface + { + $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax']; + + $domObjectId = $ajaxArguments[0]; + $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); + $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']); + + $siteTca = $this->siteTcaConfiguration->getTca(); + $fullTca = array_merge($GLOBALS['TCA'], $siteTca); + + // Parse the DOM identifier, add the levels to the structure stack + $inlineStructure = $this->inlineStackProcessor->getStructureFromString($domObjectId, $this->tcaSchemaBuilder->buildFromStructure($fullTca)); + $inlineStructure = $this->inlineStackProcessor->addAjaxConfigurationToStructure($inlineStructure, $parentConfig); + // Parent, this table embeds the child table + $inlineParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, -1); + $parentFieldName = $inlineParent['field']; + + // Set flag in config so that only the fields are rendered + // @todo: Solve differently / rename / whatever + $parentConfig['renderFieldsOnly'] = true; + + $parentData = [ + 'processedTca' => [ + 'columns' => [ + $parentFieldName => [ + 'config' => $parentConfig, + ], + ], + ], + 'uid' => $inlineParent['uid'], + 'tableName' => $inlineParent['table'], + 'inlineFirstPid' => $inlineFirstPid, + // Hand over given original return url to compile stack. Needed if inline children compile links to + // another view (eg. edit metadata in a nested inline situation like news with inline content element image), + // so the back link is still the link from the original request. See issue #82525. This is additionally + // given down in TcaInline data provider to compiled children data. + 'returnUrl' => $parentConfig['originalReturnUrl'], + ]; + + // Child, a record from this table should be rendered + $child = $this->inlineStackProcessor->getUnstableStructureFromStructure($inlineStructure); + $childData = $this->compileChild($request, $parentData, $parentFieldName, (int)$child['uid'], $inlineStructure, $siteTca); + + $childData['inlineParentUid'] = (int)$inlineParent['uid']; + $childData['renderType'] = 'inlineRecordContainer'; + $childResult = $this->nodeFactory->create($childData)->render(); + + $jsonArray = [ + 'data' => '', + 'stylesheetFiles' => [], + 'scriptItems' => new JavaScriptItems(), + ]; + + $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult); + + return new JsonResponse($jsonArray); + } + + /** + * Compile a full child record + * + * @param array $parentData Result array of parent + * @param string $parentFieldName Name of parent field + * @param int $childUid Uid of child to compile + * @param array $inlineStructure Current inline structure + * @return array Full result array + * @throws \RuntimeException + * + * @todo: This clones methods compileChild from TcaInline Provider. Find a better abstraction + * @todo: to also encapsulate the more complex scenarios with combination child and friends. + */ + protected function compileChild(ServerRequestInterface $request, array $parentData, string $parentFieldName, int $childUid, array $inlineStructure, array $siteTca): array + { + $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config']; + + $inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0); + + $childTableName = $inlineStructure['unstable']['table'] ?? null; + if (!$childTableName) { + throw new \RuntimeException('No unstable inline structure found', 1733754246); + } + + $fullTca = array_merge($GLOBALS['TCA'], $siteTca); + $formDataCompilerInput = [ + 'request' => $request, + 'command' => 'edit', + 'tableName' => $childTableName, + 'vanillaUid' => (int)$childUid, + 'returnUrl' => $parentData['returnUrl'], + 'isInlineChild' => true, + 'inlineStructure' => $inlineStructure, + 'inlineFirstPid' => $parentData['inlineFirstPid'], + 'inlineParentConfig' => $parentConfig, + 'isInlineAjaxOpeningContext' => true, + 'tcaSchemata' => $this->tcaSchemaBuilder->buildFromStructure($fullTca), + 'fullTca' => $fullTca, + + // values of the current parent element + // it is always a string either an id or new... + 'inlineParentUid' => $parentData['uid'], + 'inlineParentTableName' => $parentData['tableName'], + 'inlineParentFieldName' => $parentFieldName, + + // values of the top most parent element set on first level and not overridden on following levels + 'inlineTopMostParentUid' => $inlineTopMostParent['uid'], + 'inlineTopMostParentTableName' => $inlineTopMostParent['table'], + 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'], + ]; + if (($parentConfig['foreign_selector'] ?? false) && ($parentConfig['appearance']['useCombination'] ?? false)) { + throw new \RuntimeException('useCombination not implemented in sites module', 1522493095); + } + return $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(SiteConfigurationDataGroup::class)); + } + + /** + * Merge stuff from child array into json array. + * This method is needed since ajax handling methods currently need to put scriptCalls before and after child code. + * + * @param array $jsonResult Given json result + * @param array $childResult Given child result + * @return array Merged json array + */ + protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult): array + { + /** @var JavaScriptItems $scriptItems */ + $scriptItems = $jsonResult['scriptItems']; + + $jsonResult['data'] .= $childResult['html']; + $jsonResult['stylesheetFiles'] = []; + foreach ($childResult['stylesheetFiles'] as $stylesheetFile) { + $jsonResult['stylesheetFiles'][] = $this->getRelativePathToStylesheetFile($stylesheetFile); + } + if (!empty($childResult['inlineData'])) { + $jsonResult['inlineData'] = $childResult['inlineData']; + } + if (!empty($childResult['additionalInlineLanguageLabelFiles'])) { + $labels = []; + foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) { + ArrayUtility::mergeRecursiveWithOverrule( + $labels, + $this->getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile) + ); + } + $scriptItems->addGlobalAssignment(['TYPO3' => ['lang' => $labels]]); + } + $this->addJavaScriptModulesToJavaScriptItems($childResult['javaScriptModules'] ?? [], $scriptItems); + + return $jsonResult; + } + + /** + * Inline ajax helper method. + * + * Validates the config that is transferred over the wire to provide the + * correct TCA config for the parent table + * + * @param string $contextString + * @throws \RuntimeException + */ + protected function extractSignedParentConfigFromRequest(string $contextString): array + { + if ($contextString === '') { + throw new \RuntimeException('Empty context string given', 1522771624); + } + $context = json_decode($contextString, true); + if (empty($context['config'])) { + throw new \RuntimeException('Empty context config section given', 1522771632); + } + $config = json_decode($context['config'], true); + // encode JSON again to ensure same `json_encode()` settings as used when generating original hash + // (side-note: JSON encoded literals differ for target scenarios, e.g. HTML attr, JS string, ...) + $encodedConfig = (string)json_encode($config); + if (!hash_equals($this->hashService->hmac($encodedConfig, 'InlineContext'), (string)$context['hmac'])) { + throw new \RuntimeException('Hash does not validate', 1522771640); + } + return $config; + } + + /** + * Get inlineFirstPid from a given objectId string + * + * @param string $domObjectId The id attribute of an element + * @return int|null Pid or null + */ + protected function getInlineFirstPidFromDomObjectId(string $domObjectId): ?int + { + // Substitute FlexForm addition and make parsing a bit easier + $domObjectId = str_replace('---', ':', $domObjectId); + // The starting pattern of an object identifier (e.g. "data--) + $pattern = '/^data-(.+?)-(.+)$/'; + if (preg_match($pattern, $domObjectId, $match)) { + return (int)$match[1]; + } + return null; + } + + /** + * Find a site language by id. This will return the first occurrence of a + * language, even if the same language is used in other site configurations. + */ + protected function getLanguageById(int $languageId): ?SiteLanguage + { + foreach ($this->siteFinder->getAllSites() as $site) { + foreach ($site->getAllLanguages() as $language) { + if ($languageId === $language->getLanguageId()) { + return $language; + } + } + } + + return null; + } +} diff --git a/Classes/Controller/SiteSettingsController.php b/Classes/Controller/SiteSettingsController.php new file mode 100644 index 0000000..c5b7385 --- /dev/null +++ b/Classes/Controller/SiteSettingsController.php @@ -0,0 +1,380 @@ +getAttribute('moduleData'); + $mode = SetupSettingsViewMode::tryFrom($moduleData->get('settingsMode') ?? '') ?? SetupSettingsViewMode::BASIC; + $moduleData->set('settingsMode', $mode->value); + + $identifier = $request->getQueryParams()['site'] ?? null; + if ($identifier === null) { + throw new \RuntimeException('Site identifier to edit must be set', 1713394528); + } + + $returnUrl = GeneralUtility::sanitizeLocalUrl( + (string)($request->getQueryParams()['returnUrl'] ?? ''), + $request + ) ?: null; + $overviewUrl = (string)$this->uriBuilder->buildUriFromRoute('site_configuration'); + + $site = $this->siteFinder->getSiteByIdentifier($identifier); + $view = $this->moduleTemplateFactory->create($request); + + $settings = $this->siteSettingsService->getUncachedSettings($site); + $setSettings = $this->siteSettingsService->getSetSettings($site); + + $categoryEnhancer = function (Category $category) use (&$categoryEnhancer, $settings, $setSettings): Category { + return new Category(...[ + ...get_object_vars($category), + 'label' => $this->getLanguageService()->sL($category->label), + 'description' => $category->description !== null ? $this->getLanguageService()->sL($category->description) : $category->description, + 'categories' => array_map($categoryEnhancer, $category->categories), + 'settings' => array_map( + fn(SettingDefinition $definition): EditableSetting => new EditableSetting( + definition: $this->resolveSettingLabels($definition), + value: $settings->get($definition->key), + systemDefault: $setSettings->get($definition->key), + typeImplementation: $this->settingsTypeRegistry->get($definition->type)->getJavaScriptModule(), + ), + $category->settings + ), + ]); + }; + + $categories = array_map( + $categoryEnhancer, + $this->categoryRegistry->getCategories(...$site->getSets()) + ); + $hasSettings = count($categories) > 0; + + $this->addDocHeaderBreadcrumb($view, $site); + $this->addDocHeaderCloseAndSaveButtons($view, $returnUrl ?? $overviewUrl, $hasSettings); + $this->addDocHeaderViewModeButton($view, $site, $mode); + $this->addDocHeaderSiteConfigurationButton($view, $site); + if ($hasSettings) { + $this->addDocHeaderExportButton($view, $mode); + } + // Set shortcut context - reload button is added automatically + $view->getDocHeaderComponent()->setShortcutContext( + 'site_configuration.editSettings', + sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:labels.edit'), $site->getIdentifier()), + ['site' => $site->getIdentifier()] + ); + + $view->setLayout(ModuleLayout::NORMAL); + $view->assign('site', $site); + $view->assign('siteTitle', $this->getSiteTitle($site)); + $view->assign('rootPageId', $site->getRootPageId()); + + $view->assign('actionUrl', (string)$this->uriBuilder->buildUriFromRoute('site_configuration.saveSettings', array_filter([ + 'site' => $site->getIdentifier(), + 'returnUrl' => $returnUrl, + ]))); + $view->assign('returnUrl', $returnUrl); + $view->assign('dumpUrl', (string)$this->uriBuilder->buildUriFromRoute('site_configuration.dumpSettings', ['site' => $site->getIdentifier()])); + $view->assign('categories', $categories); + $view->assign('mode', $mode); + + $formProtection = $this->formProtectionFactory->createFromRequest($request); + $view->assign('formToken', $formProtection->generateToken('site_configuration', 'saveSettings')); + + return $view->renderResponse('SiteSettings/Edit'); + } + + private function resolveSettingLabels(SettingDefinition $definition): SettingDefinition + { + $languageService = $this->getLanguageService(); + return new SettingDefinition(...[ + ...get_object_vars($definition), + 'label' => $languageService->sL($definition->label), + 'description' => $definition->description !== null ? $languageService->sL($definition->description) : null, + 'enum' => array_map( + static fn(string|int|float|bool $label): string => $languageService->sL((string)$label), + $definition->enum + ), + ]); + } + + public function saveAction(ServerRequestInterface $request): ResponseInterface + { + $identifier = $request->getQueryParams()['site'] ?? null; + if ($identifier === null) { + throw new \RuntimeException('Site identifier to edit must be set', 1713394529); + } + + $site = $this->siteFinder->getSiteByIdentifier($identifier); + + $parsedBody = $request->getParsedBody(); + $formProtection = $this->formProtectionFactory->createFromRequest($request); + if (!$formProtection->validateToken((string)($parsedBody['formToken'] ?? ''), 'site_configuration', 'saveSettings')) { + return $this->responseFactory + ->createResponse(400, 'Invalid request token given') + ->withHeader('Location', (string)$this->uriBuilder->buildUriFromRoute('site_configuration.editSettings', [ + 'site' => $site->getIdentifier(), + ])); + } + + $returnUrl = GeneralUtility::sanitizeLocalUrl( + (string)($parsedBody['returnUrl'] ?? ''), + $request + ) ?: null; + $overviewUrl = $this->uriBuilder->buildUriFromRoute('site_configuration'); + $CMD = $parsedBody['CMD'] ?? ''; + $isSave = $CMD === 'save' || $CMD === 'saveclose'; + $isSaveClose = $parsedBody['CMD'] === 'saveclose'; + if (!$isSave) { + return new RedirectResponse($returnUrl ?? $overviewUrl); + } + + $newSettings = $this->siteSettingsService->createSettingsFromFormData($site, $parsedBody['settings'] ?? []); + $settingsDiff = $this->siteSettingsService->computeSettingsDiff($site, $newSettings); + $this->siteSettingsService->writeSettings($site, $settingsDiff->asArray()); + + if ($settingsDiff->changes !== [] || $settingsDiff->deletions !== []) { + $this->getBackendUser()->writelog( + Type::SITE, + SettingAction::CHANGE, + SystemLogErrorClassification::MESSAGE, + null, + 'Site settings changed for \'%s\': %s', + [$site->getIdentifier(), json_encode($settingsDiff)], + 'site' + ); + + $languageService = $this->getLanguageService(); + $message = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:save.message.updated'); + $flashMessage = new FlashMessage($message, '', ContextualFeedbackSeverity::OK, true); + $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + + if ($isSaveClose) { + return new RedirectResponse($returnUrl ?? $overviewUrl); + } + $editRoute = $this->uriBuilder->buildUriFromRoute('site_configuration.editSettings', array_filter([ + 'site' => $site->getIdentifier(), + 'returnUrl' => $returnUrl, + ], static fn(?string $v): bool => $v !== null)); + return new RedirectResponse($editRoute); + } + + public function dumpAction(ServerRequestInterface $request): ResponseInterface + { + $identifier = $request->getQueryParams()['site'] ?? null; + if ($identifier === null) { + throw new \RuntimeException('Site identifier to edit must be set', 1724772561); + } + + $site = $this->siteFinder->getSiteByIdentifier($identifier); + $parsedBody = $request->getParsedBody(); + $specificSetting = (string)($parsedBody['specificSetting'] ?? ''); + + $minify = $specificSetting !== '' ? false : true; + + $newSettings = $this->siteSettingsService->createSettingsFromFormData($site, $parsedBody['settings'] ?? []); + $settingsDiff = $this->siteSettingsService->computeSettingsDiff($site, $newSettings, $minify); + $settings = $settingsDiff->asArray(); + if ($specificSetting !== '' && isset($settings[$specificSetting])) { + $settings = [ + $specificSetting => $settings[$specificSetting], + ]; + } + + $yamlContents = Yaml::dump($settings, 99, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP); + + return new JsonResponse([ + 'yaml' => $yamlContents, + ]); + } + + protected function addDocHeaderBreadcrumb(ModuleTemplate $moduleTemplate, Site $site): void + { + $record = BackendUtility::getRecord('pages', $site->getRootPageId()); + $moduleTemplate->getDocHeaderComponent()->setPageBreadcrumb($record ?? []); + } + + protected function addDocHeaderCloseAndSaveButtons(ModuleTemplate $moduleTemplate, string $closeUrl, bool $saveEnabled): void + { + $moduleTemplate->addButtonToButtonBar($this->componentFactory->createCloseButton($closeUrl)); + $saveButton = $this->componentFactory->createSaveButton('sitesettings_form') + ->setName('CMD') + ->setValue('save') + ->setDisabled(!$saveEnabled); + $moduleTemplate->addButtonToButtonBar($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2); + } + + protected function addDocHeaderViewModeButton(ModuleTemplate $moduleTemplate, Site $site, SetupSettingsViewMode $mode): void + { + $languageService = $this->getLanguageService(); + $viewModeButton = $this->componentFactory->createDropDownButton() + ->setLabel($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view')) + ->setShowLabelText(true); + + $viewModeButton->addItem( + $this->componentFactory->createDropDownRadio() + ->setActive(($mode === SetupSettingsViewMode::BASIC)) + ->setHref( + (string)$this->uriBuilder->buildUriFromRoute( + 'site_configuration.editSettings', + array_filter([ + 'site' => $site->getIdentifier(), + 'settingsMode' => SetupSettingsViewMode::BASIC->value, + ]) + ) + ) + ->setLabel($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_settingseditor.xlf:settingseditor.mode.basic')) + ->setIcon($this->iconFactory->getIcon('actions-window', IconSize::SMALL)) + ); + + $viewModeButton->addItem( + $this->componentFactory->createDropDownRadio() + ->setActive(($mode === SetupSettingsViewMode::ADVANCED)) + ->setHref( + (string)$this->uriBuilder->buildUriFromRoute( + 'site_configuration.editSettings', + array_filter([ + 'site' => $site->getIdentifier(), + 'settingsMode' => SetupSettingsViewMode::ADVANCED->value, + ]) + ) + ) + ->setLabel($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_settingseditor.xlf:settingseditor.mode.advanced')) + ->setIcon($this->iconFactory->getIcon('actions-window-cog', IconSize::SMALL)) + ); + + $moduleTemplate->addButtonToButtonBar($viewModeButton, ButtonBar::BUTTON_POSITION_RIGHT, 2); + } + + protected function addDocHeaderExportButton(ModuleTemplate $moduleTemplate, SetupSettingsViewMode $mode): void + { + if ($mode === SetupSettingsViewMode::ADVANCED) { + $languageService = $this->getLanguageService(); + $exportButton = $this->componentFactory->createInputButton() + ->setTitle($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:edit.yamlExport')) + ->setIcon($this->iconFactory->getIcon('actions-database-export', IconSize::SMALL)) + ->setShowLabelText(true) + ->setName('CMD') + ->setValue('export') + ->setForm('sitesettings_form'); + $moduleTemplate->addButtonToButtonBar($exportButton, ButtonBar::BUTTON_POSITION_RIGHT); + } + } + + protected function addDocHeaderSiteConfigurationButton(ModuleTemplate $moduleTemplate, Site $site): void + { + $languageService = $this->getLanguageService(); + $exportButton = $this->componentFactory->createLinkButton() + ->setTitle($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:edit.editSiteConfiguration')) + ->setIcon($this->iconFactory->getIcon('actions-open', IconSize::SMALL)) + ->setShowLabelText(true) + ->setHref((string)$this->uriBuilder->buildUriFromRoute('site_configuration.edit', [ + 'site' => $site->getIdentifier(), + 'returnUrl' => $this->uriBuilder->buildUriFromRoute('site_configuration.editSettings', [ + 'site' => $site->getIdentifier(), + ]), + ])); + $moduleTemplate->addButtonToButtonBar($exportButton, ButtonBar::BUTTON_POSITION_LEFT, 3); + } + + protected function getSiteTitle(Site $site): string + { + $websiteTitle = $site->getConfiguration()['websiteTitle'] ?? ''; + if ($websiteTitle !== '') { + return $websiteTitle; + } + $rootPage = BackendUtility::getRecord('pages', $site->getRootPageId()); + $title = $rootPage['title'] ?? ''; + if ($title !== '') { + return $title; + } + + return '(unknown)'; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/StateTrackerController.php b/Classes/Controller/StateTrackerController.php new file mode 100644 index 0000000..2d07486 --- /dev/null +++ b/Classes/Controller/StateTrackerController.php @@ -0,0 +1,33 @@ +moduleTemplateFactory->create($request); + $currentModule = $request->getAttribute('module'); + if (!($currentModule instanceof ModuleInterface) || !$this->moduleProvider->accessGranted($currentModule->getIdentifier(), $this->getBackendUser())) { + return $view->renderResponse('SubmoduleOverview/Cards'); + } + + $id = (int)($request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? 0); + $pageinfo = BackendUtility::readPageAccess($id, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)) ?: []; + $view->setTitle( + $this->getLanguageService()->sL($currentModule->getTitle()), + $id !== 0 && isset($pageinfo['title']) ? $pageinfo['title'] : '' + ); + if ($pageinfo !== []) { + $view->getDocHeaderComponent()->setPageBreadcrumb($pageinfo); + } + $view->makeDocHeaderModuleMenu(['id' => $id]); + $view->getDocHeaderComponent()->setShortcutContext( + $currentModule->getIdentifier(), + $this->getLanguageService()->sL($currentModule->getTitle()) + ); + + $view->setTitle($this->getLanguageService()->sL($currentModule->getTitle())); + $view->setLayout(ModuleLayout::NORMAL); + $view->assign('currentModule', $currentModule); + $view->assignMultiple([ + 'additionalParameters' => array_filter(['id' => $id]), + 'submodules' => $this->getAccessibleSubmodules($currentModule), + 'moduleTitle' => $this->getLanguageService()->sL($currentModule->getTitle()), + ]); + return $view->renderResponse('SubmoduleOverview/Cards'); + } + + /** + * Get all submodules the current user has access to + * + * @return ModuleInterface[] + */ + private function getAccessibleSubmodules(ModuleInterface $module): array + { + $accessibleSubmodules = []; + foreach ($module->getSubModules() as $submodule) { + // Check if the user has access to this submodule + if ($this->moduleProvider->accessGranted($submodule->getIdentifier(), $this->getBackendUser())) { + $accessibleSubmodules[] = $submodule; + } + } + return $accessibleSubmodules; + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/SwitchUserController.php b/Classes/Controller/SwitchUserController.php new file mode 100644 index 0000000..da1c70c --- /dev/null +++ b/Classes/Controller/SwitchUserController.php @@ -0,0 +1,179 @@ +eventDispatcher = $eventDispatcher; + $this->uriBuilder = $uriBuilder; + $this->responseFactory = $responseFactory; + $this->sessionBackend = $sessionManager->getSessionBackend('BE'); + } + + /** + * Handle switching current user to the requested target user + */ + public function switchUserAction(ServerRequestInterface $request): ResponseInterface + { + $currentUser = $this->getBackendUserAuthentication(); + $targetUserId = (int)($request->getParsedBody()['targetUser'] ?? 0); + + if (!$targetUserId + || !$currentUser->isAdmin() + || $targetUserId === $currentUser->getUserId() + || $currentUser->getOriginalUserIdWhenInSwitchUserMode() !== null + ) { + return $this->jsonResponse(['success' => false]); + } + + $targetUser = BackendUtility::getRecord('be_users', $targetUserId, '*', BackendUtility::BEenableFields('be_users')); + if ($targetUser === null) { + return $this->jsonResponse(['success' => false]); + } + + if (ExtensionManagementUtility::isLoaded('beuser')) { + // Set backend user listing module as starting module if installed + $currentUser->uc['startModuleOnFirstLogin'] = 'backend_user_management'; + } + $currentUser->uc['recentSwitchedToUsers'] = $this->generateListOfMostRecentSwitchedUsers($targetUserId); + $currentUser->writeUC(); + + // Write user switch to log + $currentUser->writelog(Type::LOGIN, 2, 0, null, 'User %s switched to user %s (be_users:%s)', [ + $currentUser->getUserName() ?? '', + $targetUser['username'] ?? '', + $targetUserId, + ]); + + $sessionObject = $currentUser->getSession(); + $sessionObject->set('backuserid', $currentUser->getUserId() ?? 0); + $sessionRecord = $sessionObject->toArray(); + $sessionRecord['ses_userid'] = $targetUserId; + $this->sessionBackend->update($sessionObject->getIdentifier(), $sessionRecord); + // We must regenerate the internal session so the new ses_userid is present in the userObject + $currentUser->enforceNewSessionId(); + + $event = new SwitchUserEvent( + $currentUser->getSession()->getIdentifier(), + $targetUser, + (array)$currentUser->user + ); + $this->eventDispatcher->dispatch($event); + + return $this->jsonResponse([ + 'success' => true, + 'url' => (string)$this->uriBuilder->buildUriFromRoute('main'), + ]); + } + + /** + * Handle exiting the switch user mode + */ + public function exitSwitchUserAction(ServerRequestInterface $request): ResponseInterface + { + $currentUser = $this->getBackendUserAuthentication(); + + if ($currentUser->getOriginalUserIdWhenInSwitchUserMode() === null) { + return $this->jsonResponse(['success' => false]); + } + + $sessionObject = $currentUser->getSession(); + $originalUser = (int)$sessionObject->get('backuserid'); + $sessionObject->set('backuserid', null); + $sessionRecord = $sessionObject->toArray(); + $sessionRecord['ses_userid'] = $originalUser; + $this->sessionBackend->update($sessionObject->getIdentifier(), $sessionRecord); + // We must regenerate the internal session so the new ses_userid is present in the userObject + $currentUser->enforceNewSessionId(); + + return $this->jsonResponse([ + 'success' => true, + 'url' => (string)$this->uriBuilder->buildUriFromRoute('main'), + ]); + } + + /** + * Generates a list of users to whom where switched in the past. This is limited by RECENT_USERS_LIMIT. + * + * @return int[] + */ + protected function generateListOfMostRecentSwitchedUsers(int $targetUserUid): array + { + $latestUserUids = []; + $backendUser = $this->getBackendUserAuthentication(); + + if (isset($backendUser->uc['recentSwitchedToUsers']) && is_array($backendUser->uc['recentSwitchedToUsers'])) { + $latestUserUids = $backendUser->uc['recentSwitchedToUsers']; + } + + // Remove potentially existing user in that list + $index = array_search($targetUserUid, $latestUserUids, true); + if ($index !== false) { + unset($latestUserUids[$index]); + } + array_unshift($latestUserUids, $targetUserUid); + + return array_slice($latestUserUids, 0, static::RECENT_USERS_LIMIT); + } + + protected function jsonResponse(array $data): ResponseInterface + { + $response = $this->responseFactory + ->createResponse() + ->withAddedHeader('Content-Type', 'application/json; charset=utf-8'); + + $response->getBody()->write(json_encode($data)); + return $response; + } + + protected function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/SystemInformationController.php b/Classes/Controller/SystemInformationController.php new file mode 100644 index 0000000..159dba6 --- /dev/null +++ b/Classes/Controller/SystemInformationController.php @@ -0,0 +1,46 @@ +systemInformationToolbarItem->setRequest($request); + return new HtmlResponse($this->systemInformationToolbarItem->getDropDown()); + } +} diff --git a/Classes/Controller/UserSettingsController.php b/Classes/Controller/UserSettingsController.php new file mode 100644 index 0000000..fa66e9b --- /dev/null +++ b/Classes/Controller/UserSettingsController.php @@ -0,0 +1,87 @@ +uc + * used for AJAX and Storage/Persistent JS object + * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API. + */ +class UserSettingsController +{ + private const array ALLOWED_ACTIONS = [ + 'GET' => ['get', 'getAll'], + 'POST' => ['set', 'addToList', 'removeFromList', 'unset', 'clear'], + ]; + + /** + * Processes all AJAX calls and returns a JSON for the data + */ + public function processAjaxRequest(ServerRequestInterface $request): ResponseInterface + { + // do the regular / main logic, depending on the action parameter + $action = $this->getValidActionFromRequest($request); + + $key = $request->getParsedBody()['key'] ?? $request->getQueryParams()['key'] ?? ''; + $value = $request->getParsedBody()['value'] ?? $request->getQueryParams()['value'] ?? ''; + $backendUserConfiguration = GeneralUtility::makeInstance(BackendUserConfiguration::class); + switch ($action) { + case 'get': + $content = $backendUserConfiguration->get($key); + break; + case 'getAll': + $content = $backendUserConfiguration->getAll(); + break; + case 'set': + $backendUserConfiguration->set($key, $value); + $content = $backendUserConfiguration->getAll(); + break; + case 'addToList': + $backendUserConfiguration->addToList($key, $value); + $content = $backendUserConfiguration->getAll(); + break; + case 'removeFromList': + $backendUserConfiguration->removeFromList($key, $value); + $content = $backendUserConfiguration->getAll(); + break; + case 'unset': + $backendUserConfiguration->unsetOption($key); + $content = $backendUserConfiguration->getAll(); + break; + case 'clear': + $backendUserConfiguration->clear(); + $content = ['result' => true]; + break; + default: + $content = ['result' => false]; + } + return new JsonResponse($content); + } + + protected function getValidActionFromRequest(ServerRequestInterface $request): string + { + $action = $request->getParsedBody()['action'] ?? $request->getQueryParams()['action'] ?? ''; + return in_array($action, (self::ALLOWED_ACTIONS[$request->getMethod()] ?? []), true) ? $action : ''; + } +} diff --git a/Classes/Controller/Wizard/AddController.php b/Classes/Controller/Wizard/AddController.php new file mode 100644 index 0000000..7a669ae --- /dev/null +++ b/Classes/Controller/Wizard/AddController.php @@ -0,0 +1,249 @@ +init($request); + + if ($this->returnEditConf) { + if ($this->processDataFlag) { + // Because OnTheFly can't handle MM relations with intermediate tables we use TcaDatabaseRecord here + // Otherwise already stored relations are overwritten with the new entry + $input = [ + 'request' => $request, + 'tableName' => $this->P['table'], + 'vanillaUid' => (int)$this->P['uid'], + 'command' => 'edit', + ]; + $result = $this->formDataCompiler->compile($input, GeneralUtility::makeInstance(TcaDatabaseRecord::class)); + $currentParentRow = $result['databaseRow']; + + // If that record was found (should absolutely be...), then init DataHandler and set, prepend or append + // the record + if (is_array($currentParentRow)) { + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $data = []; + $recordId = $this->table . '_' . $this->id; + // Setting the new field data: + // If the field is a flexForm field, work with the XML structure instead: + if ($this->P['flexFormPath']) { + // Current value of flexForm path: + $currentFlexFormData = $currentParentRow[$this->P['field']]; + $currentFlexFormValueByPath = ArrayUtility::getValueByPath($currentFlexFormData, $this->P['flexFormPath']); + + // Compile currentFlexFormData to functional string + $currentFlexFormValues = []; + foreach ($currentFlexFormValueByPath as $value) { + if (is_array($value)) { + // group fields are always resolved to array + $currentFlexFormValues[] = $value['table'] . '_' . $value['uid']; + } else { + // but select fields may be uids only + $currentFlexFormValues[] = $value; + } + } + $currentFlexFormValue = implode(',', $currentFlexFormValues); + + $insertValue = ''; + switch ((string)$this->P['params']['setValue']) { + case 'set': + $insertValue = $recordId; + break; + case 'append': + $insertValue = $currentFlexFormValue . ',' . $recordId; + break; + case 'prepend': + $insertValue = $recordId . ',' . $currentFlexFormValue; + break; + } + $insertValue = implode(',', GeneralUtility::trimExplode(',', $insertValue, true)); + $data[$this->P['table']][$this->P['uid']][$this->P['field']] = ArrayUtility::setValueByPath([], $this->P['flexFormPath'], $insertValue); + } else { + $currentValue = $currentParentRow[$this->P['field']]; + + // Normalize CSV values + if (!is_array($currentValue)) { + $currentValue = GeneralUtility::trimExplode(',', $currentValue, true); + } + + // Normalize all items to "
_" format + $currentValue = array_map(function (array|int|string $item): string { + // Handle per-item table for "group" elements + if (is_array($item)) { + $item = $item['table'] . '_' . $item['uid']; + } else { + $item = $this->table . '_' . $item; + } + + return $item; + }, $currentValue); + + switch ((string)$this->P['params']['setValue']) { + case 'set': + $currentValue = [$recordId]; + break; + case 'append': + $currentValue[] = $recordId; + break; + case 'prepend': + array_unshift($currentValue, $recordId); + break; + } + + $data[$this->P['table']][$this->P['uid']][$this->P['field']] = implode(',', $currentValue); + } + // Submit the data: + $dataHandler->start($data, []); + $dataHandler->process_datamap(); + } + } + // Return to the parent FormEngine record editing session: + return new RedirectResponse(GeneralUtility::sanitizeLocalUrl($this->P['returnUrl'], $request)); + } + + // Redirecting to FormEngine with instructions to create a new record + // AND when closing to return back with information about that records ID etc. + $normalizedParams = $request->getAttribute('normalizedParams'); + $redirectUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [ + 'returnEditConf' => 1, + 'edit[' . $this->P['params']['table'] . '][' . $this->pid . ']' => 'new', + // @todo add module context to wizard/add routes and set module context here + 'returnUrl' => $normalizedParams->getRequestUri(), + ]); + + return new RedirectResponse($redirectUrl); + } + + /** + * Initialization of the class. + */ + protected function init(ServerRequestInterface $request): void + { + $parsedBody = $request->getParsedBody(); + $queryParams = $request->getQueryParams(); + // Init GPvars: + $this->P = $parsedBody['P'] ?? $queryParams['P'] ?? []; + $this->returnEditConf = $parsedBody['returnEditConf'] ?? $queryParams['returnEditConf'] ?? ''; + // Get this record + $record = BackendUtility::getRecord($this->P['table'], $this->P['uid']); + // Set table: + $this->table = $this->P['params']['table']; + // Get TSconfig for it. + $TSconfig = FormEngineUtility::getTCEFORM_TSconfig( + $this->P['table'], + is_array($record) ? $record : ['pid' => (int)$this->P['params']['pid']] + ); + // Set [params][pid] + if (str_starts_with($this->P['params']['pid'], '###') && str_ends_with($this->P['params']['pid'], '###')) { + $keyword = substr($this->P['params']['pid'], 3, -3); + $this->pid = str_starts_with($keyword, 'PAGE_TSCONFIG_') + ? (int)$TSconfig[$this->P['field']][$keyword] + : (int)$TSconfig['_' . $keyword]; + } else { + $this->pid = (int)$this->P['params']['pid']; + } + + // If a new id has returned from a newly created record... + if ($this->returnEditConf) { + $editConfiguration = json_decode($this->returnEditConf, true); + if (is_array($editConfiguration[$this->table]) && MathUtility::canBeInterpretedAsInteger($this->P['uid'])) { + // Getting id and cmd from returning editConf array. + reset($editConfiguration[$this->table]); + $this->id = (int)key($editConfiguration[$this->table]); + $cmd = current($editConfiguration[$this->table]); + // ... and if everything seems OK we will register some classes for inclusion and instruct the object + // to perform processing later. + if ($this->P['params']['setValue'] + && $cmd === 'edit' + && $this->id + && $this->P['table'] + && $this->P['field'] && $this->P['uid'] + ) { + $liveRecord = BackendUtility::getLiveVersionOfRecord($this->table, $this->id, 'uid'); + if ($liveRecord) { + $this->id = $liveRecord['uid']; + } + $this->processDataFlag = 1; + } + } + } + } +} diff --git a/Classes/Controller/Wizard/EditController.php b/Classes/Controller/Wizard/EditController.php new file mode 100644 index 0000000..5bf7d1a --- /dev/null +++ b/Classes/Controller/Wizard/EditController.php @@ -0,0 +1,179 @@ +closeWindow = sprintf( + '', + GeneralUtility::implodeAttributes([ + 'src' => (string)PathUtility::getSystemResourceUri(self::JAVASCRIPT_HELPER, $request), + 'data-action' => 'window.close', + ], true) + ); + $parsedBody = $request->getParsedBody(); + $queryParams = $request->getQueryParams(); + + $this->P = $parsedBody['P'] ?? $queryParams['P'] ?? []; + // Used for the return URL to FormEngine so that we can close the window. + $this->doClose = $parsedBody['doClose'] ?? $queryParams['doClose'] ?? 0; + + return $this->processRequest(); + } + + /** + * Process request function + * Makes a header-location redirect to an edit form IF POSSIBLE from the passed data - otherwise the window will + * just close. + */ + protected function processRequest(): ResponseInterface + { + if ($this->doClose) { + return new HtmlResponse($this->closeWindow); + } + // Initialize: + $table = $this->P['table']; + $field = $this->P['field']; + $schema = $this->tcaSchemaFactory->get($table); + + if (empty($this->P['flexFormDataStructureIdentifier'])) { + // If there is not flex data structure identifier, field config is found in globals + $config = $schema->getField($field)->getConfiguration(); + } else { + // If there is a flex data structure identifier, parse that data structure and + // fetch config defined by given flex path + $dataStructure = $this->flexFormTools->parseDataStructureByIdentifier($this->P['flexFormDataStructureIdentifier'], $schema); + $config = ArrayUtility::getValueByPath($dataStructure, $this->P['flexFormDataStructurePath']); + if (!is_array($config)) { + throw new \RuntimeException( + 'Something went wrong finding flex path ' . $this->P['flexFormDataStructurePath'] + . ' in data structure identified by ' . $this->P['flexFormDataStructureIdentifier'], + 1537356346 + ); + } + } + + $urlParameters = [ + 'returnUrl' => (string)$this->uriBuilder->buildUriFromRoute('wizard_edit', ['doClose' => 1]), + ]; + + // Detecting the various allowed field type setups and acting accordingly. + if ($config['type'] === 'select' + && !($config['MM'] ?? false) + && (int)($config['maxitems'] ?? 0) <= 1 + && MathUtility::canBeInterpretedAsInteger($this->P['currentValue']) + && $this->P['currentValue'] + && $config['foreign_table'] + ) { + // SINGLE value + $urlParameters['edit[' . $config['foreign_table'] . '][' . $this->P['currentValue'] . ']'] = 'edit'; + // Redirect to FormEngine + // Note: no 'module' context here, since we're opening in a popup + $url = $this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters); + return new RedirectResponse($url); + } + + if (!empty($config['type']) + && !empty($this->P['currentSelectedValues']) + && ( + $config['type'] === 'select' && !empty($config['foreign_table']) + || $config['type'] === 'group' && !empty($config['allowed']) + ) + ) { + // MULTIPLE VALUES: + // Init settings: + $allowedTables = $config['type'] === 'group' ? $config['allowed'] : $config['foreign_table']; + // Selecting selected values into an array: + $relationHandler = GeneralUtility::makeInstance(RelationHandler::class); + $relationHandler->start($this->P['currentSelectedValues'], $allowedTables); + $value = $relationHandler->getValueArray(true); + // Traverse that array and make parameters for FormEngine + foreach ($value as $rec) { + $recTableUidParts = GeneralUtility::revExplode('_', $rec, 2); + $urlParameters['edit[' . $recTableUidParts[0] . '][' . $recTableUidParts[1] . ']'] = 'edit'; + } + // Redirect to FormEngine + $url = $this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters); + return new RedirectResponse($url); + } + return new HtmlResponse($this->closeWindow); + } +} diff --git a/Classes/Controller/Wizard/ImageManipulationController.php b/Classes/Controller/Wizard/ImageManipulationController.php new file mode 100644 index 0000000..c23efb0 --- /dev/null +++ b/Classes/Controller/Wizard/ImageManipulationController.php @@ -0,0 +1,78 @@ +isSignatureValid($request)) { + $parsedBody = json_decode($request->getParsedBody()['arguments'], true); + $fileUid = $parsedBody['image']; + $image = null; + if (MathUtility::canBeInterpretedAsInteger($fileUid)) { + try { + $image = $this->resourceFactory->getFileObject($fileUid); + } catch (FileDoesNotExistException $e) { + } + } + $view = $this->backendViewFactory->create($request); + $view->assignMultiple([ + 'image' => $image, + 'cropVariants' => $parsedBody['cropVariants'], + ]); + return new HtmlResponse($view->render('Form/ImageManipulationWizard')); + } + return new HtmlResponse('', 403); + } + + /** + * Check if hmac signature is correct + * + * @param ServerRequestInterface $request the request with the POST parameters + */ + protected function isSignatureValid(ServerRequestInterface $request): bool + { + $token = $this->hashService->hmac($request->getParsedBody()['arguments'], 'ajax_wizard_image_manipulation'); + return hash_equals($token, $request->getParsedBody()['signature']); + } +} diff --git a/Classes/Controller/Wizard/ListController.php b/Classes/Controller/Wizard/ListController.php new file mode 100644 index 0000000..d45b960 --- /dev/null +++ b/Classes/Controller/Wizard/ListController.php @@ -0,0 +1,80 @@ + Records module if a wizard-link has been clicked in FormEngine. + * + * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API. + */ +class ListController +{ + /** + * Injects the request object for the current request or sub request + */ + public function mainAction(ServerRequestInterface $request): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + $queryParams = $request->getQueryParams(); + + // Wizard parameters, coming from FormEngine linking to this wizard. + $parameters = $parsedBody['P'] ?? $queryParams['P'] ?? null; + $id = $parsedBody['id'] ?? $queryParams['id'] ?? null; + $table = $parameters['table'] ?? ''; + $origRow = BackendUtility::getRecord($table, $parameters['uid']); + $tsConfig = FormEngineUtility::getTCEFORM_TSconfig($table, $origRow ?? ['pid' => $parameters['pid'] ?? 0]); + + if (str_starts_with($parameters['params']['pid'], '###') && substr($parameters['params']['pid'], -3) === '###') { + $keyword = substr($parameters['params']['pid'], 3, -3); + if (str_starts_with($keyword, 'PAGE_TSCONFIG_')) { + $pid = (int)$tsConfig[$parameters['field']][$keyword]; + } else { + $pid = (int)$tsConfig['_' . $keyword]; + } + } else { + $pid = (int)$parameters['params']['pid']; + } + + if ((string)$id !== '') { + // If pid is blank + $redirectUrl = GeneralUtility::sanitizeLocalUrl($parameters['returnUrl'], $request); + } else { + // Otherwise, show the list + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $normalizedParams = $request->getAttribute('normalizedParams'); + $requestUri = $normalizedParams->getRequestUri(); + $urlParameters = []; + $urlParameters['id'] = $pid; + $urlParameters['table'] = $parameters['params']['table']; + $urlParameters['returnUrl'] = !empty($parameters['returnUrl']) + ? GeneralUtility::sanitizeLocalUrl($parameters['returnUrl'], $request) + : $requestUri; + $redirectUrl = (string)$uriBuilder->buildUriFromRoute('records', $urlParameters); + } + + return new RedirectResponse($redirectUrl); + } +} diff --git a/Classes/Controller/Wizard/LocalizationController.php b/Classes/Controller/Wizard/LocalizationController.php new file mode 100644 index 0000000..e1f31e2 --- /dev/null +++ b/Classes/Controller/Wizard/LocalizationController.php @@ -0,0 +1,779 @@ +getQueryParams(); + if (!isset($params['recordType'], $params['recordUid'])) { + return new JsonResponse(null, 400); + } + + $recordType = $params['recordType']; + $recordUid = (int)$params['recordUid']; + + $record = BackendUtility::getRecord($recordType, $recordUid); + if (!$record) { + return new JsonResponse(null, 404); + } + + $schema = $this->schemaFactory->get($recordType); + $recordTitle = BackendUtility::getRecordTitle($recordType, $record); + $recordInfo = [ + 'uid' => $record['uid'], + 'title' => BackendUtility::cropToTitleLength($recordTitle), + 'icon' => $this->iconFactory->getIconForRecord($recordType, $record, IconSize::SMALL)->getIdentifier(), + 'type' => $recordType, + 'typeName' => $schema->getTitle($this->getLanguageService()->sL(...)), + ]; + + return new JsonResponse($recordInfo); + } + + /** + * Get available localization handlers + * + * Returns handlers filtered by the localization context + */ + public function getHandlers(ServerRequestInterface $request): ResponseInterface + { + $params = $request->getQueryParams(); + + try { + $localizationInstructions = LocalizationInstructions::create($params); + } catch (\ValueError) { + return new JsonResponse(['error' => 'Invalid localization mode'], 400); + } catch (\InvalidArgumentException) { + // Validate required parameters + return new JsonResponse(null, 400); + } + + // Get available handlers from registry + $handlers = $this->localizationHandlerRegistry->getAvailableHandlers($localizationInstructions); + + // Prepare handlers for JSON response with translated labels + $result = []; + foreach ($handlers as $handler) { + $result[] = [ + 'identifier' => $handler->getIdentifier(), + 'label' => $this->getLanguageService()->sL($handler->getLabel()), + 'description' => $this->getLanguageService()->sL($handler->getDescription()), + 'iconIdentifier' => $handler->getIconIdentifier(), + ]; + } + + return new JsonResponse($result); + } + + /** + * Get available localization modes + */ + public function getModes(ServerRequestInterface $request): ResponseInterface + { + $params = $request->getQueryParams(); + if (!isset($params['recordType'], $params['recordUid'], $params['targetLanguage'])) { + return new JsonResponse(null, 400); + } + + $recordType = $params['recordType']; + $recordUid = (int)$params['recordUid']; + $targetLanguage = (int)$params['targetLanguage']; + + // For pages, use the recordUid directly as the page + // For other record types, find the parent page + if ($recordType === 'pages') { + $page = $recordUid; + } else { + // Get the record to find its parent page + $record = BackendUtility::getRecord($recordType, $recordUid); + if (!$record) { + return new JsonResponse(null, 404); + } + $page = (int)$record['pid']; + } + + // Get page record for permission checks + $pageRecord = BackendUtility::readPageAccess($page, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)); + if (!$pageRecord) { + return new JsonResponse(null, 403); + } + + // Get available modes based on PageTSconfig + $pageTsConfig = BackendUtility::getPagesTSconfig($page); + $schema = $this->schemaFactory->get($recordType); + if (!$schema->hasCapability(TcaSchemaCapability::Language)) { + // Table is not language-aware + return new JsonResponse(null, 400); + } + $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); + + $availableModes = array_filter( + LocalizationMode::cases(), + static function (LocalizationMode $mode) use ($pageTsConfig, $languageCapability): bool { + return match ($mode) { + LocalizationMode::COPY => ($pageTsConfig['mod.']['web_layout.']['localization.']['enableCopy'] ?? true) && $languageCapability->hasTranslationSourceField(), + LocalizationMode::TRANSLATE => (bool)($pageTsConfig['mod.']['web_layout.']['localization.']['enableTranslate'] ?? true), + }; + } + ); + + // Check if there are existing translations in the target language + // If so, we need to ensure we don't mix localization modes + // This check is only relevant for pages and tt_content records + if ($recordType === 'pages' || $recordType === 'tt_content') { + $existingMode = $this->detectExistingLocalizationMode($page, $targetLanguage); + if ($existingMode !== null) { + // Filter to only allow the existing mode + $availableModes = array_filter( + $availableModes, + static fn(LocalizationMode $mode): bool => $mode === $existingMode + ); + } + } + + // Sort by priority (highest first) + usort($availableModes, static fn(LocalizationMode $a, LocalizationMode $b): int => $b->getPriority() <=> $a->getPriority()); + + $modes = array_map( + fn(LocalizationMode $mode): array => [ + 'key' => $mode->value, + 'label' => $this->getLanguageService()->sL($mode->getLabel()), + 'description' => $this->getLanguageService()->sL($mode->getDescription()), + 'iconIdentifier' => $mode->getIconIdentifier(), + ], + $availableModes + ); + + return new JsonResponse($modes); + } + + /** + * Get all target languages available for translation (excluding default language) + */ + public function getTargets(ServerRequestInterface $request): ResponseInterface + { + $params = $request->getQueryParams(); + if (!isset($params['recordType'], $params['recordUid'])) { + return new JsonResponse(null, 400); + } + + $recordType = $params['recordType']; + $recordUid = (int)$params['recordUid']; + + // For pages, use the recordUid directly as the page + // For other record types, find the parent page + if ($recordType === 'pages') { + $page = $recordUid; + } else { + // Get the record to find its parent page + $record = BackendUtility::getRecord($recordType, $recordUid); + if (!$record) { + return new JsonResponse(null, 404); + } + $page = (int)$record['pid']; + } + + // Get page record for permission checks + $pageRecord = BackendUtility::readPageAccess($page, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)); + if (!$pageRecord) { + return new JsonResponse(null, 403); + } + + $systemLanguages = $this->translationConfigurationProvider->getSystemLanguages($page); + $availableLanguages = []; + foreach ($systemLanguages as $languageUid => $language) { + // Exclude "All languages" (-1) and default language (0) for target language selection + if ($languageUid !== -1 && $languageUid !== 0) { + $availableLanguages[] = $language; + } + } + + return new JsonResponse($availableLanguages); + } + + /** + * Get source languages that have content and can be used as translation base + */ + public function getSources(ServerRequestInterface $request): ResponseInterface + { + $params = $request->getQueryParams(); + if (!isset($params['recordType'], $params['recordUid'], $params['targetLanguage'])) { + return new JsonResponse(null, 400); + } + + $recordType = (string)$params['recordType']; + $recordUid = (int)$params['recordUid']; + $targetLanguage = (int)$params['targetLanguage']; + + // For pages, use the recordUid directly as the page + // For other record types, find the parent page + if ($recordType === 'pages') { + $page = $recordUid; + } else { + // Get the record to find its parent page + $record = BackendUtility::getRecord($recordType, $recordUid); + if (!$record) { + return new JsonResponse(null, 404); + } + $page = (int)$record['pid']; + } + + // Get page record for permission checks + $pageRecord = BackendUtility::readPageAccess($page, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)); + if (!$pageRecord) { + return new JsonResponse(null, 403); + } + + $systemLanguages = $this->translationConfigurationProvider->getSystemLanguages($page); + $availableLanguages = []; + + // Find all existing translations of the record + $record = BackendUtility::getRecord($recordType, $recordUid); + if ($record) { + $existingLanguageUids = [0]; // Always include default language + + // Check each system language to see if a translation exists + foreach (array_keys($systemLanguages) as $languageUid) { + if ($languageUid > 0) { // Skip default language (0) and "All languages" (-1) + $translation = $this->localizationRepository->getRecordTranslation($recordType, $record, (int)$languageUid); + if ($translation !== null) { + $existingLanguageUids[] = $languageUid; + } + } + } + + foreach ($existingLanguageUids as $languageUid) { + if ($languageUid !== $targetLanguage && isset($systemLanguages[$languageUid])) { + $availableLanguages[] = $systemLanguages[$languageUid]; + } + } + + // For pages with existing translations in the target language, we need to restrict source languages + // to prevent mixed translation origins (e.g., some content from language A, some from language B) + if ($recordType === 'pages') { + $availableLanguages = $this->filterSourceLanguagesForPage($recordUid, $targetLanguage, $availableLanguages); + } + } + + // Language "All" should not appear as a source of translations (see bug 92757) and keys should be sequential + $availableLanguages = array_values( + array_filter($availableLanguages, static function (array $languageRecord): bool { + return (int)$languageRecord['uid'] !== -1; + }) + ); + + return new JsonResponse($availableLanguages); + } + + /** + * Get page layout and records for localization + */ + public function getContent(ServerRequestInterface $request): ResponseInterface + { + $params = $request->getQueryParams(); + if (!isset($params['pageUid'], $params['targetLanguage'], $params['sourceLanguage'])) { + return new JsonResponse(null, 400); + } + + $pageUid = (int)$params['pageUid']; + $targetLanguage = (int)$params['targetLanguage']; + $sourceLanguage = (int)$params['sourceLanguage']; + + $records = []; + $result = $this->localizationRepository->getRecordsToCopyDatabaseResult( + $pageUid, + $targetLanguage, + $sourceLanguage, + $this->getBackendUser()->workspace + ); + + $flatRecords = []; + while ($row = $result->fetchAssociative()) { + BackendUtility::workspaceOL('tt_content', $row, $this->getBackendUser()->workspace, true); + if (!$row || VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER) { + continue; + } + $colPos = $row['colPos']; + + if (!$this->backendLayoutView->isCTypeAllowedInColPosByPage($row['CType'], $colPos, $pageUid)) { + continue; + } + + if (!isset($records[$colPos])) { + $records[$colPos] = []; + } + $recordTitle = BackendUtility::getRecordTitle('tt_content', $row); + $records[$colPos][] = [ + 'icon' => $this->iconFactory->getIconForRecord('tt_content', $row, IconSize::SMALL)->getIdentifier(), + 'title' => BackendUtility::cropToTitleLength($recordTitle), + 'uid' => $row['uid'], + ]; + $flatRecords[] = $row; + } + + $columns = $this->getPageColumns($pageUid, $flatRecords, $params); + $event = new AfterRecordSummaryForLocalizationEvent($records, $columns); + $this->eventDispatcher->dispatch($event); + + // Get the backend layout structure for visual representation + $backendLayout = $this->backendLayoutView->getBackendLayoutForPage($pageUid); + $layoutStructure = $this->buildLayoutStructure($backendLayout, $event->getColumns(), $event->getRecords()); + + return new JsonResponse([ + 'layout' => $layoutStructure, + ]); + } + + public function localize(ServerRequestInterface $request): ResponseInterface + { + $params = $request->getParsedBody(); + + if (!isset( + $params['recordType'], + $params['recordUid'], + $params['data']['sourceLanguage'], + $params['data']['targetLanguage'], + $params['data']['localizationMode'] + )) { + return new JsonResponse(null, 400); + } + + $recordType = $params['recordType']; + $recordUid = (int)$params['recordUid']; + $sourceLanguage = (int)$params['data']['sourceLanguage']; + $targetLanguage = (int)$params['data']['targetLanguage']; + $modeIdentifier = $params['data']['localizationMode']; + $handlerIdentifier = $params['data']['localizationHandler'] ?? 'manual'; + + // Prepare Additional Data + $additionalData = $params['data']; + unset($additionalData['sourceLanguage']); + unset($additionalData['targetLanguage']); + unset($additionalData['localizationMode']); + unset($additionalData['localizationHandler']); + + // Validate that the mode exists + $mode = LocalizationMode::tryFrom($modeIdentifier); + if ($mode === null) { + $response = new Response('php://temp', 400, ['Content-Type' => 'application/json; charset=utf-8']); + $response->getBody()->write('Invalid localization mode "' . $modeIdentifier . '" called.'); + return $response; + } + + try { + $localizationInstructions = new LocalizationInstructions( + $recordType, + $recordUid, + $sourceLanguage, + $targetLanguage, + $mode, + $additionalData + ); + } catch (\ValueError) { + return new JsonResponse(['error' => 'Invalid localization mode'], 400); + } catch (\InvalidArgumentException) { + // Validate required parameters + return new JsonResponse(null, 400); + } + + // Process the localization using the handler + try { + // Get the handler from the registry or fall back to the default handler + if ($this->localizationHandlerRegistry->hasHandler($handlerIdentifier)) { + $handler = $this->localizationHandlerRegistry->getHandler($handlerIdentifier); + } else { + $handler = $this->localizationHandler; + } + + // Use the handler to process the localization with the selected mode + $result = $handler->processLocalization($localizationInstructions); + } catch (\Exception $e) { + $result = LocalizationResult::error([$e->getMessage()]); + } + return new JsonResponse($result->jsonSerialize()); + } + + private function getPageColumns(int $page, array $flatRecords, array $params): array + { + $columns = []; + $backendLayout = $this->backendLayoutView->getBackendLayoutForPage($page); + + foreach ($backendLayout->getUsedColumns() as $columnPos => $columnLabel) { + $columns[$columnPos] = $this->getLanguageService()->sL($columnLabel); + } + + $event = new AfterPageColumnsSelectedForLocalizationEvent($columns, [], $backendLayout, $flatRecords, $params); + $this->eventDispatcher->dispatch($event); + + return $event->getColumns(); + } + + private function buildLayoutStructure($backendLayout, array $columns, array $records): array + { + // Calculate total elements across all columns + $elementCount = 0; + foreach ($records as $colPos => $columnRecords) { + if (is_array($columnRecords)) { + $elementCount += count($columnRecords); + } + } + + if (!$backendLayout) { + // Create a simple single-row layout when no backend layout is available + $layoutColumns = []; + foreach ($columns as $colPos => $columnLabel) { + $layoutColumns[] = [ + 'position' => (int)$colPos, + 'label' => $columnLabel, + 'records' => $records[$colPos] ?? [], + 'colspan' => 1, + 'rowspan' => 1, + 'identifier' => null, + ]; + } + + return [ + 'type' => 'layout', + 'title' => 'Default Layout', + 'identifier' => 'default', + 'colCount' => count($columns), + 'rowCount' => 1, + 'elementCount' => $elementCount, + 'rows' => [ + [ + 'columns' => $layoutColumns, + ], + ], + ]; + } + + $structure = $backendLayout->getStructure(); + $layoutRows = []; + + if (!empty($structure['__config']['backend_layout.']['rows.'])) { + $rows = $structure['__config']['backend_layout.']['rows.']; + ksort($rows); + + foreach ($rows as $row) { + $layoutColumns = []; + + if (!empty($row['columns.'])) { + foreach ($row['columns.'] as $column) { + if (!isset($column['colPos'])) { + continue; + } + + $colPos = (int)$column['colPos']; + $layoutColumns[] = [ + 'position' => $colPos, + 'label' => $columns[$colPos] ?? $column['name'], + 'records' => $records[$colPos] ?? [], + 'colspan' => (int)($column['colspan'] ?? 1), + 'rowspan' => (int)($column['rowspan'] ?? 1), + 'identifier' => $column['identifier'] ?? null, + ]; + } + } + + $layoutRows[] = [ + 'columns' => $layoutColumns, + ]; + } + } + + return [ + 'type' => 'layout', + 'title' => $backendLayout->getTitle(), + 'identifier' => $backendLayout->getIdentifier(), + 'colCount' => $backendLayout->getColCount(), + 'rowCount' => $backendLayout->getRowCount(), + 'elementCount' => $elementCount, + 'rows' => $layoutRows, + ]; + } + + /** + * Filter available source languages for page translations based on existing content + * + * For pages, when translations already exist in the target language with content assigned, + * we need to ensure that new content is only translated from the same source language(s) + * as the existing content to avoid creating mixed translations in terms of language origin. + * + * @param int $pageUid The page UID being translated + * @param int $targetLanguage The target language ID + * @param array $availableLanguages All available source languages (to be filtered) + * @return array Filtered available language configurations + */ + private function filterSourceLanguagesForPage(int $pageUid, int $targetLanguage, array $availableLanguages): array + { + // Check if a page translation exists in the target language + $pageTranslation = $this->localizationRepository->getPageTranslations($pageUid, [$targetLanguage], $this->getBackendUser()->workspace); + if ($pageTranslation === []) { + return $availableLanguages; + } + + // Get source languages used by existing content in the target language + // Note: Content elements are stored on the original page with sys_language_uid set to the target language + $usedSourceLanguages = $this->getUsedSourceLanguagesForPage($pageUid, $targetLanguage); + + if (empty($usedSourceLanguages)) { + return $availableLanguages; + } + + // Filter to only allow source languages already in use + return array_filter( + $availableLanguages, + static fn(array $language): bool => isset($usedSourceLanguages[(int)$language['uid']]) + ); + } + + /** + * Get the source languages used by existing content on a page + * + * @param int $pageUid The page UID + * @param int $targetLanguage The target language ID + * @return array Map of source language UIDs that are in use + */ + private function getUsedSourceLanguagesForPage(int $pageUid, int $targetLanguage): array + { + $schema = $this->schemaFactory->get('tt_content'); + $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); + + if (!$languageCapability->hasTranslationSourceField()) { + return []; + } + + $languageField = $languageCapability->getLanguageField()->getName(); + $translationSourceField = $languageCapability->getTranslationSourceField()->getName(); + + // Get all l10n_source UIDs from translated content + $queryBuilder = $this->connectionPool + ->getQueryBuilderForTable('tt_content'); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)); + + $result = $queryBuilder + ->select($translationSourceField) + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq( + 'pid', + $queryBuilder->createNamedParameter($pageUid, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + $languageField, + $queryBuilder->createNamedParameter($targetLanguage, Connection::PARAM_INT) + ), + $queryBuilder->expr()->gt( + $translationSourceField, + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ) + ) + ->executeQuery(); + + // Collect unique source UIDs + $sourceUids = []; + while ($row = $result->fetchAssociative()) { + $sourceUid = (int)$row[$translationSourceField]; + $sourceUids[$sourceUid] = $sourceUid; + } + + if (empty($sourceUids)) { + return []; + } + + // Get the language of all source records + $sourceQueryBuilder = $this->connectionPool + ->getQueryBuilderForTable('tt_content'); + $sourceQueryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + + $sourceResult = $sourceQueryBuilder + ->select($languageField) + ->from('tt_content') + ->where( + $sourceQueryBuilder->expr()->in( + 'uid', + $sourceQueryBuilder->createNamedParameter($sourceUids, Connection::PARAM_INT_ARRAY) + ) + ) + ->groupBy($languageField) + ->executeQuery(); + + $usedSourceLanguages = []; + while ($row = $sourceResult->fetchAssociative()) { + $sourceLanguageUid = (int)$row[$languageField]; + $usedSourceLanguages[$sourceLanguageUid] = true; + } + + return $usedSourceLanguages; + } + + /** + * Detect the localization mode used by existing translations on a page + * + * This method checks if there are existing content elements in the target language + * and determines whether they were created using COPY (free) or TRANSLATE (connected) mode. + * This prevents mixing different localization modes on the same page, which would lead to + * inconsistent translation workflows. + * + * Note: Pages themselves are always created in connected mode (using 'localize' command), + * so we check the content elements on the page to determine the actual localization mode. + * + * The distinction is made by checking the translation origin pointer field obtained from the + * schema's language capability: + * - TRANSLATE mode (connected): Records have translation origin pointer > 0 (linked to source language) + * - COPY mode (free): Records have translation origin pointer = 0 (independent copies) + * + * @param int $pageId The page ID to check + * @param int $targetLanguage The target language ID + * @return LocalizationMode|null The detected mode, or null if no translations exist + */ + private function detectExistingLocalizationMode(int $pageId, int $targetLanguage): ?LocalizationMode + { + // Get the TCA schema to determine the correct field names + $schema = $this->schemaFactory->get('tt_content'); + + if (!$schema->hasCapability(TcaSchemaCapability::Language)) { + // Table is not language-aware + return null; + } + + $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); + $transOrigPointerField = $languageCapability->getTranslationOriginPointerField()->getName(); + $languageField = $languageCapability->getLanguageField()->getName(); + + // Check content elements on the page, as pages themselves are always connected + // but their content determines the actual localization mode being used + $queryBuilder = $this->connectionPool + ->getQueryBuilderForTable('tt_content'); + + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)); + + // Select only the translation pointer field to determine the mode + $result = $queryBuilder + ->select($transOrigPointerField) + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq( + 'pid', + $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + $languageField, + $queryBuilder->createNamedParameter($targetLanguage, Connection::PARAM_INT) + ) + ) + ->executeQuery(); + + $hasRecords = false; + $hasConnected = false; + + // Iterate through results to detect the mode + while ($row = $result->fetchAssociative()) { + $hasRecords = true; + $transOrigPointer = (int)($row[$transOrigPointerField] ?? 0); + + // If we find any connected record (transOrigPointer > 0), return TRANSLATE immediately + // This prevents mixing modes even if there are also free mode records + if ($transOrigPointer > 0) { + $hasConnected = true; + break; + } + } + + // No records found + if (!$hasRecords) { + return null; + } + + // If any connected record exists, return TRANSLATE mode + // Otherwise, all records are free mode, return COPY + return $hasConnected ? LocalizationMode::TRANSLATE : LocalizationMode::COPY; + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Controller/Wizard/PageWizardController.php b/Classes/Controller/Wizard/PageWizardController.php new file mode 100644 index 0000000..5b5edb0 --- /dev/null +++ b/Classes/Controller/Wizard/PageWizardController.php @@ -0,0 +1,164 @@ +getQueryParams()['data']['position'] ?? []; + $pageUid = (int)($position['pageUid'] ?? 0); + $insertPosition = $position['insertPosition'] ?? 'inside'; + + $parentPageUid = $insertPosition === 'inside' + ? $pageUid + : (BackendUtility::getRecord('pages', $pageUid, 'pid')['pid'] ?? null); + + $backendUser = $this->getBackendUser(); + $parentPage = BackendUtility::readPageAccess((int)$parentPageUid, $backendUser->getPagePermsClause(Permission::PAGE_NEW)); + if (!$parentPage) { + return new JsonResponse(null, 403); + } + + $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class); + $formDataGroup->setProviderList([ + InitializeProcessedTca::class, + DatabaseParentPageRow::class, + DatabaseUserPermissionCheck::class, + DatabaseEffectivePid::class, + UserTsConfig::class, + PageTsConfig::class, + DatabaseRowInitializeNew::class, + DatabaseUniqueUidNewRow::class, + TcaSelectItems::class, + ]); + + $doktypes = $this->formDataCompiler + ->compile( + [ + 'command' => 'new', + 'request' => $request, + 'tableName' => 'pages', + 'vanillaUid' => $parentPageUid, + ], + $formDataGroup + )['processedTca']['columns']['doktype']['config']['items'] ?? []; + + $result = []; + foreach ($doktypes as $doktype) { + $result[] = [ + 'value' => $doktype['value'] ?? '', + 'label' => $doktype['label'] ?? '', + 'icon' => $doktype['icon'] ?? '', + 'description' => $doktype['description'] ?? '', + ]; + } + + return new JsonResponse($result, 200); + } + + public function getPageDetailAction(ServerRequestInterface $request): ResponseInterface + { + $params = $request->getQueryParams(); + $pageUid = $params['pageUid'] ?? null; + + if ($pageUid === null) { + return new JsonResponse(['error' => 'Missing required query parameter: pageUid'], 400); + } + + if ((int)$pageUid === 0) { + return new JsonResponse([ + 'uid' => 0, + 'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? 'TYPO3', + 'icon' => 'apps-pagetree-root', + ]); + } + + $page = BackendUtility::readPageAccess((int)$pageUid, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)); + + if (!$page) { + return new JsonResponse(null, 403); + } + + $recordInfo = [ + 'uid' => $page['uid'], + 'title' => $page['title'], + 'icon' => $this->iconFactory->getIconForRecord('pages', $page, IconSize::SMALL)->getIdentifier(), + ]; + + return new JsonResponse($recordInfo); + } + + public function getProcessedValueAction(ServerRequestInterface $request): ResponseInterface + { + $params = $request->getQueryParams(); + $fields = $params['fields'] ?? []; + $pageUid = (int)($params['pageUid'] ?? 0); + + $page = BackendUtility::readPageAccess($pageUid, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)); + if (!$page) { + return new JsonResponse(null, 403); + } + + $result = []; + foreach ($fields as $fieldName => $value) { + $result[$fieldName] = BackendUtility::getProcessedValue('pages', $fieldName, $value, 0, false, false, 0, true, $pageUid); + } + + return new JsonResponse($result); + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/Wizard/SuggestWizardController.php b/Classes/Controller/Wizard/SuggestWizardController.php new file mode 100644 index 0000000..32ef820 --- /dev/null +++ b/Classes/Controller/Wizard/SuggestWizardController.php @@ -0,0 +1,327 @@ +getParsedBody(); + + $search = $parsedBody['value'] ?? null; + $tableName = $parsedBody['tableName'] ?? null; + $fieldName = $parsedBody['fieldName'] ?? null; + $uid = $parsedBody['uid'] ?? null; + $pid = isset($parsedBody['pid']) ? (int)$parsedBody['pid'] : 0; + $dataStructureIdentifier = $parsedBody['dataStructureIdentifier'] ?? ''; + $flexFormSheetName = $parsedBody['flexFormSheetName'] ?? null; + $flexFormFieldName = $parsedBody['flexFormFieldName'] ?? null; + $flexFormContainerName = $parsedBody['flexFormContainerName'] ?? null; + $flexFormContainerFieldName = $parsedBody['flexFormContainerFieldName'] ?? null; + $recordType = (string)($parsedBody['recordTypeValue'] ?? '') ?: null; + $schema = $this->tcaSchemaFactory->get($tableName); + + // Determine TCA config of field + if (empty($dataStructureIdentifier)) { + // Normal columns field + $fieldInformation = $schema->getField($fieldName); + $fieldConfig = $fieldInformation->getConfiguration(); + $fieldNameInPageTsConfig = $fieldName; + + // With possible columnsOverrides + // @todo Validate if we can move this fallback recordType determination, should be do-able in v13?! + if ($recordType === null) { + $recordType = BackendUtility::getTCAtypeValue( + $tableName, + BackendUtility::getRecord($tableName, $uid) ?? [], + true + ); + } + if ($recordType !== null && $schema->hasSubSchema($recordType)) { + $fieldConfig = $schema->getSubSchema($recordType)->getField($fieldName)->getConfiguration(); + } + } else { + // A flex-form field + $dataStructure = $this->flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier, $schema); + if (empty($flexFormContainerFieldName)) { + // @todo: See if a path in pageTsConfig like "TCEForm.tableName.theContainerFieldName =" is useful and works with other pageTs, too. + $fieldNameInPageTsConfig = $flexFormFieldName; + if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT'] + ['el'][$flexFormFieldName]['config']) + ) { + throw new \RuntimeException( + 'Specified path ' . $flexFormFieldName . ' not found in flex form data structure', + 1480609491 + ); + } + $fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT'] + ['el'][$flexFormFieldName]['config']; + } else { + $fieldNameInPageTsConfig = $flexFormContainerFieldName; + if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT'] + ['el'][$flexFormFieldName] + ['el'][$flexFormContainerName] + ['el'][$flexFormContainerFieldName]['config']) + ) { + throw new \RuntimeException( + 'Specified path ' . $flexFormContainerName . ' not found in flex form section container data structure', + 1480611208 + ); + } + $fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT'] + ['el'][$flexFormFieldName] + ['el'][$flexFormContainerName] + ['el'][$flexFormContainerFieldName]['config']; + } + } + + $pageTsConfig = BackendUtility::getPagesTSconfig($pid); + + $wizardConfig = $fieldConfig['suggestOptions'] ?? []; + + $queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig); + $whereClause = $this->getWhereClause($fieldConfig); + + $resultRows = []; + + // fetch the records for each query table. A query table is a table from which records are allowed to + // be added to the TCEForm selector, originally fetched from the "allowed" config option in the TCA + foreach ($queryTables as $queryTable) { + // if the table does not exist, skip it + if (!$this->tcaSchemaFactory->has($queryTable)) { + continue; + } + + $config = $this->getConfigurationForTable($queryTable, $wizardConfig, $pageTsConfig, $tableName, $fieldNameInPageTsConfig); + + // process addWhere + if (!isset($config['addWhere']) && $whereClause) { + $config['addWhere'] = $whereClause; + } + if (isset($config['addWhere'])) { + $replacement = [ + '###THIS_UID###' => (int)$uid, + '###CURRENT_PID###' => (int)$pid, + ]; + if (isset($pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'])) { + $fieldTSconfig = $pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.']; + if (isset($fieldTSconfig['PAGE_TSCONFIG_ID'])) { + $replacement['###PAGE_TSCONFIG_ID###'] = (int)$fieldTSconfig['PAGE_TSCONFIG_ID']; + } + if (isset($fieldTSconfig['PAGE_TSCONFIG_IDLIST'])) { + $replacement['###PAGE_TSCONFIG_IDLIST###'] = implode(',', GeneralUtility::intExplode(',', (string)$fieldTSconfig['PAGE_TSCONFIG_IDLIST'])); + } + if (isset($fieldTSconfig['PAGE_TSCONFIG_STR'])) { + $connection = $this->connectionPool->getConnectionForTable($fieldConfig['foreign_table']); + // nasty hack, but it's currently not possible to just quote anything "inside" the value but not escaping + // the whole field as it is not known where it is used in the WHERE clause + $replacement['###PAGE_TSCONFIG_STR###'] = trim($connection->quote($fieldTSconfig['PAGE_TSCONFIG_STR']), '\''); + } + } + $config['addWhere'] = QueryHelper::quoteDatabaseIdentifiers($this->connectionPool->getConnectionForTable($queryTable), strtr(' ' . $config['addWhere'], $replacement)); + } + + // instantiate the class that should fetch the records for this $queryTable + $receiverClassName = $config['receiverClass'] ?? ''; + if (!class_exists($receiverClassName)) { + $receiverClassName = SuggestWizardDefaultReceiver::class; + } + $receiverObj = GeneralUtility::makeInstance($receiverClassName, $queryTable, $config); + $params = [ + 'value' => $search, + 'uid' => $uid, + ]; + $rows = $receiverObj->queryTable($params); + if (empty($rows)) { + continue; + } + $resultRows = $rows + $resultRows; + unset($rows); + } + + // Limit the number of items in the result list + $maxItems = (int)($config['maxItemsInResultList'] ?? 10); + $maxItems = min(count($resultRows), $maxItems); + + array_splice($resultRows, $maxItems); + return new JsonResponse(array_values($resultRows)); + } + + /** + * Checks if the current backend user is allowed to access the given table, based on the schema capabilities. + */ + protected function currentBackendUserMayAccessTable(TcaSchema $schema): bool + { + if ($this->getBackendUser()->isAdmin()) { + return true; + } + + // If the user is no admin, they may not access admin-only tables + if ($schema->hasCapability(TcaSchemaCapability::AccessAdminOnly)) { + return false; + } + + /** @var RootLevelCapability $rootLevelCapability */ + $rootLevelCapability = $schema->getCapability(TcaSchemaCapability::RestrictionRootLevel); + + // allow access to root level pages if security restrictions should be bypassed + return $rootLevelCapability->canAccessRecordsOnRootLevel(); + } + + /** + * Returns the configuration for the suggest wizard for the given table. This does multiple overlays from the + * TSconfig. + * + * @param string $queryTable The table to query + * @param array $wizardConfig The configuration for the wizard as configured in the data structure + * @param array $TSconfig The TSconfig array of the current page + * @param string $table The table where the wizard is used + * @param string $field The field where the wizard is used + */ + protected function getConfigurationForTable(string $queryTable, array $wizardConfig, array $TSconfig, string $table, string $field): array + { + $config = (array)($wizardConfig['default'] ?? []); + + if (is_array($wizardConfig[$queryTable] ?? null)) { + ArrayUtility::mergeRecursiveWithOverrule($config, $wizardConfig[$queryTable]); + } + + $globalSuggestTsConfig = $TSconfig['TCEFORM.']['suggest.'] ?? []; + $currentFieldSuggestTsConfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'] ?? []; + + // merge the configurations of different "levels" to get the working configuration for this table and + // field (i.e., go from the most general to the most special configuration) + if (is_array($globalSuggestTsConfig['default.'] ?? null)) { + ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($globalSuggestTsConfig['default.'])); + } + + if (is_array($globalSuggestTsConfig[$queryTable . '.'] ?? null)) { + ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($globalSuggestTsConfig[$queryTable . '.'])); + } + + // use $table instead of $queryTable here because we overlay a config + // for the input-field here, not for the queried table + if (is_array($currentFieldSuggestTsConfig['default.'] ?? null)) { + ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($currentFieldSuggestTsConfig['default.'])); + } + + if (is_array($currentFieldSuggestTsConfig[$queryTable . '.'] ?? null)) { + ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($currentFieldSuggestTsConfig[$queryTable . '.'])); + } + + return $config; + } + + /** + * Checks the given field configuration for the tables that should be used for querying and returns them as an + * array. + */ + protected function getTablesToQueryFromFieldConfiguration(array $fieldConfig): array + { + $queryTables = []; + + if (isset($fieldConfig['allowed'])) { + if ($fieldConfig['allowed'] !== '*') { + // list of allowed tables + $queryTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed']); + } else { + // all tables are allowed, if the user can access them + /** @var TcaSchema $schema */ + foreach ($this->tcaSchemaFactory->all() as $tableName => $schema) { + if ($schema->hasCapability(TcaSchemaCapability::HideInUi)) { + continue; + } + if ($this->currentBackendUserMayAccessTable($schema)) { + $queryTables[] = $tableName; + } + } + } + } elseif (isset($fieldConfig['foreign_table'])) { + // use the foreign table + $queryTables = [$fieldConfig['foreign_table']]; + } + + return $queryTables; + } + + /** + * Wraps user functions in the configuration array as a `RawValue` object, + * to be asserted later when actually calling `GeneralUtility::callUserFunction`. + */ + protected function substituteRawValues(array $config): array + { + if (!empty($config['renderFunc'])) { + $config['renderFunc'] = new RawValue($config['renderFunc']); + } + return $config; + } + + /** + * Returns the SQL WHERE clause to use for querying records. This is currently only relevant if a foreign_table + * is configured and should be used; it could e.g. be used to limit to a certain subset of records from the + * foreign table + */ + protected function getWhereClause(array $fieldConfig): string + { + if (!isset($fieldConfig['foreign_table'], $fieldConfig['foreign_table_where'])) { + return ''; + } + + // strip ORDER BY clause + return trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $fieldConfig['foreign_table_where'])); + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/Wizard/WizardController.php b/Classes/Controller/Wizard/WizardController.php new file mode 100644 index 0000000..e01f68a --- /dev/null +++ b/Classes/Controller/Wizard/WizardController.php @@ -0,0 +1,59 @@ +getProviderByRequest($request) + ->getConfiguration($request) + ->jsonSerialize() + ); + } + + public function submitDataAction(ServerRequestInterface $request): ResponseInterface + { + return new JsonResponse( + $this->getProviderByRequest($request) + ->handleSubmit($request) + ->jsonSerialize() + ); + } + + private function getProviderByRequest(ServerRequestInterface $request): WizardProviderInterface + { + return $this->wizardProviderRegistry->getProvider($request->getQueryParams()['mode'] ?? ''); + } +} diff --git a/Classes/Date/DateConfiguration.php b/Classes/Date/DateConfiguration.php new file mode 100644 index 0000000..6f186a5 --- /dev/null +++ b/Classes/Date/DateConfiguration.php @@ -0,0 +1,40 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'timezone' => $this->timezone, + 'formats' => $this->formats, + ]; + } +} diff --git a/Classes/Date/DateConfigurationFactory.php b/Classes/Date/DateConfigurationFactory.php new file mode 100644 index 0000000..8e42f53 --- /dev/null +++ b/Classes/Date/DateConfigurationFactory.php @@ -0,0 +1,60 @@ +convertPhpFormatToLuxon($phpDateFormat); + $timeFormat = $formatter->convertPhpFormatToLuxon($phpTimeFormat); + } else { + $dateFormat = $phpDateFormat; + $timeFormat = $phpTimeFormat; + } + + return new DateConfiguration( + timezone: date_default_timezone_get(), + formats: new DateFormats( + date: $dateFormat, + time: $timeFormat, + datetime: $dateFormat . ' ' . $timeFormat, + ), + ); + } +} diff --git a/Classes/Date/DateFormats.php b/Classes/Date/DateFormats.php new file mode 100644 index 0000000..544e6ec --- /dev/null +++ b/Classes/Date/DateFormats.php @@ -0,0 +1,42 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'date' => $this->date, + 'time' => $this->time, + 'datetime' => $this->datetime, + ]; + } +} diff --git a/Classes/DependencyInjection/AvatarProviderPass.php b/Classes/DependencyInjection/AvatarProviderPass.php new file mode 100644 index 0000000..85c2ac4 --- /dev/null +++ b/Classes/DependencyInjection/AvatarProviderPass.php @@ -0,0 +1,65 @@ +findDefinition(Avatar::class); + $orderedProviders = []; + $providers = []; + + foreach ($container->findTaggedServiceIds($this->tagName) as $id => $tags) { + $definition = $container->findDefinition($id); + + if (!$definition->isAutoconfigured() || $definition->isAbstract()) { + continue; + } + + foreach ($tags as $attributes) { + $identifier = $attributes['identifier']; + $providers[$identifier] = [ + 'before' => GeneralUtility::trimExplode(',', $attributes['before'] ?? '', true), + 'after' => GeneralUtility::trimExplode(',', $attributes['after'] ?? '', true), + 'serviceName' => $id, + ]; + } + } + + foreach ((new DependencyOrderingService())->orderByDependencies($providers) as ['serviceName' => $serviceName]) { + $orderedProviders[] = new Reference($serviceName); + } + + $avatarDefinition->setArgument('$avatarProviders', $orderedProviders); + } +} diff --git a/Classes/DependencyInjection/ModuleAccessGatePass.php b/Classes/DependencyInjection/ModuleAccessGatePass.php new file mode 100644 index 0000000..980b045 --- /dev/null +++ b/Classes/DependencyInjection/ModuleAccessGatePass.php @@ -0,0 +1,83 @@ +findDefinition(ModuleAccessGateRegistry::class); + $gates = []; + + foreach ($container->findTaggedServiceIds($this->tagName) as $id => $tags) { + $definition = $container->findDefinition($id); + + if (!$definition->isAutoconfigured() || $definition->isAbstract()) { + continue; + } + + foreach ($tags as $attributes) { + $identifier = $attributes['identifier'] ?? null; + if (empty($identifier)) { + throw new \RuntimeException( + sprintf('Module access gate %s must have an identifier', $id), + 1774436662 + ); + } + + if (!is_subclass_of($container->getParameterBag()->resolveValue($definition->getClass()), ModuleAccessGateInterface::class)) { + throw new \InvalidArgumentException( + sprintf('Module access gate "%s" must implement ModuleAccessGateInterface', $identifier), + 1774436669 + ); + } + + $gates[$identifier] = [ + 'before' => GeneralUtility::trimExplode(',', $attributes['before'] ?? '', true), + 'after' => GeneralUtility::trimExplode(',', $attributes['after'] ?? '', true), + 'serviceName' => $id, + ]; + } + } + + $orderedGates = (new DependencyOrderingService())->orderByDependencies($gates); + + $registryDefinition->setArgument( + '$gates', + array_map( + static fn(array $config) => new Reference($config['serviceName']), + $orderedGates + ) + ); + } +} diff --git a/Classes/DependencyInjection/SidebarComponentsPass.php b/Classes/DependencyInjection/SidebarComponentsPass.php new file mode 100644 index 0000000..8e59f9a --- /dev/null +++ b/Classes/DependencyInjection/SidebarComponentsPass.php @@ -0,0 +1,86 @@ +findDefinition(SidebarComponentsRegistry::class); + + $components = []; + + foreach ($container->findTaggedServiceIds($this->tagName) as $id => $tags) { + $definition = $container->findDefinition($id); + + if (!$definition->isAutoconfigured() || $definition->isAbstract()) { + continue; + } + + foreach ($tags as $attributes) { + $identifier = $attributes['identifier'] ?? null; + if (empty($identifier)) { + throw new \RuntimeException( + sprintf('Sidebar component %s must have an identifier', $id), + 1765923036 + ); + } + + if (!is_subclass_of($container->getParameterBag()->resolveValue($definition->getClass()), SidebarComponentInterface::class)) { + throw new \InvalidArgumentException( + sprintf('Sidebar component "%s" must implement SidebarComponentInterface', $identifier), + 1734373001 + ); + } + + $before = GeneralUtility::trimExplode(',', $attributes['before'] ?? '', true); + $after = GeneralUtility::trimExplode(',', $attributes['after'] ?? '', true); + + $components[$identifier] = [ + 'before' => $before, + 'after' => $after, + 'serviceName' => $id, + ]; + } + } + + // Order components by dependencies + $dependencyOrderingService = new DependencyOrderingService(); + $orderedComponents = $dependencyOrderingService->orderByDependencies($components); + + // Build ordered references array + $registryDefinition->setArgument('$sidebarComponents', array_map(static fn($config) => new Reference($config['serviceName']), $orderedComponents)); + } +} diff --git a/Classes/Domain/Model/Element/ImmediateActionElement.php b/Classes/Domain/Model/Element/ImmediateActionElement.php new file mode 100644 index 0000000..83303b0 --- /dev/null +++ b/Classes/Domain/Model/Element/ImmediateActionElement.php @@ -0,0 +1,78 @@ +action = $action; + $this->args = $args; + } + + public function __toString(): string + { + $attributes = ['action' => $this->action]; + if ($this->args !== null) { + $attributes['args'] = GeneralUtility::jsonEncodeForHtmlAttribute($this->args); + } + return sprintf( + '', + GeneralUtility::implodeAttributes($attributes, true) + ); + } +} diff --git a/Classes/Domain/Model/Language/LanguageItem.php b/Classes/Domain/Model/Language/LanguageItem.php new file mode 100644 index 0000000..31cff1d --- /dev/null +++ b/Classes/Domain/Model/Language/LanguageItem.php @@ -0,0 +1,64 @@ +status === LanguageStatus::Existing; + } + + public function isCreatable(): bool + { + return $this->status === LanguageStatus::Creatable; + } + + public function isAvailable(): bool + { + return $this->status !== LanguageStatus::Unavailable; + } + + public function getTitle(): string + { + return $this->siteLanguage->getTitle(); + } + + public function getFlagIdentifier(): string + { + return $this->siteLanguage->getFlagIdentifier(); + } + + public function getLanguageId(): int + { + return $this->siteLanguage->getLanguageId(); + } +} diff --git a/Classes/Domain/Model/Language/LanguageStatus.php b/Classes/Domain/Model/Language/LanguageStatus.php new file mode 100644 index 0000000..6adee47 --- /dev/null +++ b/Classes/Domain/Model/Language/LanguageStatus.php @@ -0,0 +1,41 @@ + $languageStatuses Status for each language (existing/creatable/unavailable) + * @param array $existingTranslations Raw page translation records, keyed by language ID + * @param int[] $creatableLanguageIds IDs of languages that can be created + * @param bool $canUserCreateTranslations Whether user has permission to create translations + * @param LanguageItem[] $languageItems UI-ready language items + */ + public function __construct( + public int $pageId, + public array $availableLanguages, + public array $languageStatuses, + public array $existingTranslations, + public array $creatableLanguageIds, + public bool $canUserCreateTranslations, + public array $languageItems, + ) {} + + /** + * Check if a translation exists for a specific language. + */ + public function hasTranslation(int $languageId): bool + { + return array_key_exists($languageId, $this->existingTranslations); + } + + /** + * Check if a translation can be created for a specific language. + */ + public function canCreateTranslation(int $languageId): bool + { + return in_array($languageId, $this->creatableLanguageIds, true); + } + + /** + * Get the translation record for a specific language. + * + * @return array|null Translation record or null if not found + */ + public function getTranslationRecord(int $languageId): ?array + { + return $this->existingTranslations[$languageId] ?? null; + } + + /** + * Get the status of a specific language. + */ + public function getLanguageStatus(int $languageId): LanguageStatus + { + return $this->languageStatuses[$languageId] ?? LanguageStatus::Unavailable; + } + + /** + * Get all language IDs including default (0) and all translations. + * + * @return int[] + */ + public function getAllExistingLanguageIds(): array + { + return array_merge([0], array_keys($this->existingTranslations)); + } +} diff --git a/Classes/Domain/Repository/Localization/LocalizationRepository.php b/Classes/Domain/Repository/Localization/LocalizationRepository.php new file mode 100644 index 0000000..bfb5755 --- /dev/null +++ b/Classes/Domain/Repository/Localization/LocalizationRepository.php @@ -0,0 +1,396 @@ + 0) + * - It only deals with RawRecord (not other Records) as we are usually interested in the raw values + * - It has no dependency on $GLOBALS['BE_USER'] + */ +#[Autoconfigure(public: true)] +readonly class LocalizationRepository +{ + public function __construct( + protected TcaSchemaFactory $tcaSchemaFactory, + protected RecordFactory $recordFactory, + ) {} + + /** + * Get records for copy process + */ + public function getRecordsToCopyDatabaseResult(int $pageId, int $destLanguageId, int $languageId, int $workspaceId = 0): Result + { + $originalUids = []; + + // Get original uid of existing elements triggered language + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(new DeletedRestriction()) + ->add(new WorkspaceRestriction($workspaceId)); + + $originalUidsStatement = $queryBuilder + ->select('l10n_source') + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq( + 'sys_language_uid', + $queryBuilder->createNamedParameter($destLanguageId, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'pid', + $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT) + ) + ) + ->executeQuery(); + + while ($origUid = $originalUidsStatement->fetchOne()) { + $originalUids[] = (int)$origUid; + } + + $queryBuilder + ->select('*') + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq( + 'sys_language_uid', + $queryBuilder->createNamedParameter($languageId, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'pid', + $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT) + ) + ) + ->orderBy('sorting'); + + if ($originalUids !== []) { + $queryBuilder + ->andWhere( + $queryBuilder->expr()->notIn( + 'uid', + $queryBuilder->createNamedParameter($originalUids, Connection::PARAM_INT_ARRAY) + ) + ); + } + + return $queryBuilder->executeQuery(); + } + + /** + * Fetches the translated version of a record. + * It automatically applies workspace overlay and filters out DELETE_PLACEHOLDER records. + * + * @param string|TcaSchema $tableOrSchema The table name or TCA schema + * @param int|array|RecordInterface $recordOrUid The record UID, or the full record (array or RecordInterface). + * When passing the full record, the pid is automatically used for filtering. + * @param int|LanguageAspect $language The target language ID or LanguageAspect + * @return RawRecord|null The translated record or null if not found + */ + public function getRecordTranslation( + string|TcaSchema $tableOrSchema, + int|array|RecordInterface $recordOrUid, + int|LanguageAspect $language, + int $workspaceId = 0, + bool $includeDeletedRecords = false, + ): ?RawRecord { + // Resolve table name and schema + if ($tableOrSchema instanceof TcaSchema) { + $table = $tableOrSchema->getName(); + $schema = $tableOrSchema; + } else { + $table = $tableOrSchema; + if (!$this->tcaSchemaFactory->has($table)) { + return null; + } + $schema = $this->tcaSchemaFactory->get($table); + } + + if (!$schema->isLanguageAware()) { + return null; + } + + // Resolve uid and optional pid from record + if ($recordOrUid instanceof RecordInterface) { + $uid = $recordOrUid->getUid(); + $pid = $recordOrUid->getPid(); + } elseif (is_array($recordOrUid)) { + $uid = (int)($recordOrUid['uid'] ?? 0); + $pid = isset($recordOrUid['pid']) ? (int)$recordOrUid['pid'] : null; + } else { + $uid = $recordOrUid; + $pid = null; + } + + if ($uid === 0) { + return null; + } + + // Resolve language ID + $languageId = $language instanceof LanguageAspect ? $language->getId() : $language; + + $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); + $queryBuilder->getRestrictions()->removeAll(); + + if (!$includeDeletedRecords) { + $queryBuilder->getRestrictions()->add(new DeletedRestriction()); + } + $queryBuilder->getRestrictions()->add(new WorkspaceRestriction($workspaceId)); + + // Prefer translationSourceField (l10n_source) over transOrigPointerField (l10n_parent) + $parentPointerField = $languageCapability->hasTranslationSourceField() + ? $languageCapability->getTranslationSourceField()->getName() + : $languageCapability->getTranslationOriginPointerField()->getName(); + + $queryBuilder + ->select('*') + ->from($table) + ->where( + $queryBuilder->expr()->eq( + $parentPointerField, + $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + $languageCapability->getLanguageField()->getName(), + $queryBuilder->createNamedParameter($languageId, Connection::PARAM_INT) + ) + ) + ->setMaxResults(1); + + // When a full record is provided, automatically filter by pid + if ($pid !== null) { + $queryBuilder->andWhere( + $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)) + ); + } + + $row = $queryBuilder->executeQuery()->fetchAssociative(); + + if ($row === false) { + return null; + } + + // Apply workspace overlay + BackendUtility::workspaceOL($table, $row, $workspaceId); + if (!is_array($row) || VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER) { + return null; + } + + return $this->recordFactory->createRawRecord($table, $row); + } + + /** + * Fetches all translations of a record. + * It automatically applies workspace overlay and filters out DELETE_PLACEHOLDER records. + * + * @param string|TcaSchema $tableOrSchema The table name or TCA schema + * @param int|array|RecordInterface $recordOrUid The record UID, or the full record (array or RecordInterface). + * When passing the full record, the pid is automatically used for filtering. + * @param array $limitToLanguageIds Optional list of language IDs to filter by + * @return RawRecord[] Array of translated records indexed by language ID + */ + public function getRecordTranslations( + string|TcaSchema $tableOrSchema, + int|array|RecordInterface $recordOrUid, + array $limitToLanguageIds = [], + int $workspaceId = 0, + bool $includeDeletedRecords = false, + ): array { + // Resolve table name and schema + if ($tableOrSchema instanceof TcaSchema) { + $table = $tableOrSchema->getName(); + $schema = $tableOrSchema; + } else { + $table = $tableOrSchema; + if (!$this->tcaSchemaFactory->has($table)) { + return []; + } + $schema = $this->tcaSchemaFactory->get($table); + } + + if (!$schema->isLanguageAware()) { + return []; + } + + // Resolve uid and optional pid from record + if ($recordOrUid instanceof RecordInterface) { + $uid = $recordOrUid->getUid(); + $pid = $recordOrUid->getPid(); + } elseif (is_array($recordOrUid)) { + $uid = (int)($recordOrUid['uid'] ?? 0); + $pid = isset($recordOrUid['pid']) ? (int)$recordOrUid['pid'] : null; + } else { + $uid = $recordOrUid; + $pid = null; + } + + if ($uid === 0) { + return []; + } + + $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); + $languageFieldName = $languageCapability->getLanguageField()->getName(); + + // Prefer translationSourceField (l10n_source) over transOrigPointerField (l10n_parent) + $parentPointerField = $languageCapability->hasTranslationSourceField() + ? $languageCapability->getTranslationSourceField()->getName() + : $languageCapability->getTranslationOriginPointerField()->getName(); + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); + $queryBuilder->getRestrictions()->removeAll(); + + if (!$includeDeletedRecords) { + $queryBuilder->getRestrictions()->add(new DeletedRestriction()); + } + $queryBuilder->getRestrictions()->add(new WorkspaceRestriction($workspaceId)); + + $queryBuilder + ->select('*') + ->from($table) + ->where( + $queryBuilder->expr()->eq( + $parentPointerField, + $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT) + ), + $queryBuilder->expr()->gt( + $languageFieldName, + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ) + ); + + // When a full record is provided, automatically filter by pid + if ($pid !== null) { + $queryBuilder->andWhere( + $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)) + ); + } + + if ($limitToLanguageIds !== []) { + $queryBuilder->andWhere( + $queryBuilder->expr()->in( + $languageFieldName, + $queryBuilder->createNamedParameter($limitToLanguageIds, Connection::PARAM_INT_ARRAY) + ) + ); + } + + $result = $queryBuilder->executeQuery(); + + $records = []; + while ($row = $result->fetchAssociative()) { + BackendUtility::workspaceOL($table, $row, $workspaceId); + if (is_array($row) && VersionState::tryFrom($row['t3ver_state'] ?? 0) !== VersionState::DELETE_PLACEHOLDER) { + $records[(int)$row[$languageFieldName]] = $this->recordFactory->createRawRecord($table, $row); + } + } + + return $records; + } + + /** + * Fetches all existing page translations for a given page. + * It automatically applies workspace overlay and filters out DELETE_PLACEHOLDER records. + * + * @param int $pageUid The UID of the default language page + * @param array $limitToLanguageIds Optional list of language IDs to filter by + * @return RawRecord[] Array of page translation records indexed by language ID + */ + public function getPageTranslations( + int $pageUid, + array $limitToLanguageIds = [], + int $workspaceId = 0, + bool $includeDeletedRecords = false, + ): array { + if ($pageUid === 0) { + return []; + } + + if (!$this->tcaSchemaFactory->has('pages')) { + return []; + } + + $schema = $this->tcaSchemaFactory->get('pages'); + if (!$schema->isLanguageAware()) { + return []; + } + + $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); + $languageFieldName = $languageCapability->getLanguageField()->getName(); + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); + $queryBuilder->getRestrictions()->removeAll(); + + if (!$includeDeletedRecords) { + $queryBuilder->getRestrictions()->add(new DeletedRestriction()); + } + $queryBuilder->getRestrictions()->add(new WorkspaceRestriction($workspaceId)); + + $queryBuilder + ->select('*') + ->from('pages') + ->where( + $queryBuilder->expr()->eq( + $languageCapability->getTranslationOriginPointerField()->getName(), + $queryBuilder->createNamedParameter($pageUid, Connection::PARAM_INT) + ) + ); + + if ($limitToLanguageIds !== []) { + $queryBuilder->andWhere( + $queryBuilder->expr()->in( + $languageFieldName, + $queryBuilder->createNamedParameter($limitToLanguageIds, Connection::PARAM_INT_ARRAY) + ) + ); + } + + $result = $queryBuilder->executeQuery(); + + $records = []; + while ($row = $result->fetchAssociative()) { + BackendUtility::workspaceOL('pages', $row, $workspaceId); + if (is_array($row) && VersionState::tryFrom($row['t3ver_state'] ?? 0) !== VersionState::DELETE_PLACEHOLDER) { + $records[(int)$row[$languageFieldName]] = $this->recordFactory->createRawRecord('pages', $row); + } + } + + return $records; + } +} diff --git a/Classes/Dto/Breadcrumb/BreadcrumbNode.php b/Classes/Dto/Breadcrumb/BreadcrumbNode.php new file mode 100644 index 0000000..5db2237 --- /dev/null +++ b/Classes/Dto/Breadcrumb/BreadcrumbNode.php @@ -0,0 +1,52 @@ +userPermissionOnPage); + return $permission->get($this->table ? Permission::PAGE_DELETE : Permission::CONTENT_EDIT); + } + + /** + * True if a record has been saved + */ + public function isSavedRecord(): bool + { + return $this->command !== 'new' && $this->table !== '' && MathUtility::canBeInterpretedAsInteger($this->uid); + } +} diff --git a/Classes/Dto/Settings/EditableSetting.php b/Classes/Dto/Settings/EditableSetting.php new file mode 100644 index 0000000..9cde78b --- /dev/null +++ b/Classes/Dto/Settings/EditableSetting.php @@ -0,0 +1,49 @@ +value)) { + return implode(', ', $this->value); + } + if (is_scalar($this->value)) { + return (string)$this->value; + } + return ''; + } +} diff --git a/Classes/Dto/Tree/FileTreeItem.php b/Classes/Dto/Tree/FileTreeItem.php new file mode 100644 index 0000000..8429f7e --- /dev/null +++ b/Classes/Dto/Tree/FileTreeItem.php @@ -0,0 +1,42 @@ + 'FileTreeItem', + ...$this->item->jsonSerialize(), + 'pathIdentifier' => $this->pathIdentifier, + 'storage' => $this->storage, + 'resourceType' => $this->resourceType, + ]; + } +} diff --git a/Classes/Dto/Tree/Label/Label.php b/Classes/Dto/Tree/Label/Label.php new file mode 100644 index 0000000..d518765 --- /dev/null +++ b/Classes/Dto/Tree/Label/Label.php @@ -0,0 +1,34 @@ + 'PageTreeItem', + ...$this->item->jsonSerialize(), + 'doktype' => $this->doktype, + 'nameSourceField' => $this->nameSourceField, + 'workspaceId' => $this->workspaceId, + 'locked' => $this->locked, + 'stopPageTree' => $this->stopPageTree, + 'mountPoint' => $this->mountPoint, + ]; + } +} diff --git a/Classes/Dto/Tree/SelectTreeItem.php b/Classes/Dto/Tree/SelectTreeItem.php new file mode 100644 index 0000000..1ffd270 --- /dev/null +++ b/Classes/Dto/Tree/SelectTreeItem.php @@ -0,0 +1,40 @@ + 'SelectTreeItem', + ...$this->item->jsonSerialize(), + 'checked' => $this->checked, + 'selectable' => $this->selectable, + ]; + } +} diff --git a/Classes/Dto/Tree/Status/StatusInformation.php b/Classes/Dto/Tree/Status/StatusInformation.php new file mode 100644 index 0000000..6b8eca6 --- /dev/null +++ b/Classes/Dto/Tree/Status/StatusInformation.php @@ -0,0 +1,36 @@ +setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $this->getRequest(), $this->getLanguageService()); + $view = $this->backendViewFactory->create($request); + $this->view = $view; + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/element-browser.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/hotkeys.js'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_misc.xlf'); + $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf'); + $this->initVariables($request); + } + + /** + * Returns the identifier for the browser + */ + public function getIdentifier(): string + { + return $this->identifier; + } + + protected function initVariables(ServerRequestInterface $request) + { + $this->browserParameters = ElementBrowserParameters::fromRequest($request); + } + + protected function getBodyTagParameters(): string + { + $bodyDataAttributes = array_merge( + $this->getBParamDataAttributes(), + $this->getBodyTagAttributes() + ); + return GeneralUtility::implodeAttributes($bodyDataAttributes, true, true); + } + + /** + * @return array Array of body-tag attributes + */ + protected function getBodyTagAttributes() + { + return []; + } + + /** + * Returns data attributes for the body tag, used by the Javascript. + * + * @return array Data attributes for Javascript + */ + protected function getBParamDataAttributes() + { + return $this->browserParameters->toDataAttributes(); + } + + public function setRequest(ServerRequestInterface $request): void + { + $this->request = $request; + // initialize here, this is a dirty hack as long as the interface does not support setting a request object properly + // see ElementBrowserController.php for the process on how the program code flow is used + $this->initialize($request); + } + + protected function getRequest(): ServerRequestInterface + { + return $this->request ?? $GLOBALS['TYPO3_REQUEST']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/ElementBrowser/DatabaseBrowser.php b/Classes/ElementBrowser/DatabaseBrowser.php new file mode 100644 index 0000000..edd8252 --- /dev/null +++ b/Classes/ElementBrowser/DatabaseBrowser.php @@ -0,0 +1,248 @@ +pageRenderer->loadJavaScriptModule('@typo3/backend/browse-database.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/tree/page-browser.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/column-selector-button.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/recordlist.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/record-search.js'); + } + + protected function initVariables(ServerRequestInterface $request) + { + parent::initVariables($request); + $this->expandPage = $request->getParsedBody()['expandPage'] ?? $request->getQueryParams()['expandPage'] ?? null; + } + + /** + * Session data for this class can be set from outside with this method. + * + * @param mixed[] $data Session data array + * @return array Session data and boolean which indicates that data needs to be stored in session because it's changed + */ + public function processSessionData($data) + { + if ($this->expandPage !== null) { + $data['expandPage'] = $this->expandPage; + $store = true; + } else { + $this->expandPage = (int)($data['expandPage'] ?? 0); + $store = false; + } + return [$data, $store]; + } + + /** + * @return string HTML content + */ + public function render() + { + $this->getBackendUser()->initializeWebmountsForElementBrowser(); + $this->modTSconfig = BackendUtility::getPagesTSconfig((int)$this->expandPage)['mod.']['web_list.'] ?? []; + $allowedTables = $this->browserParameters->allowedTypes; + + $withTree = true; + if ($allowedTables !== '' && $allowedTables !== '*') { + $tablesArr = GeneralUtility::trimExplode(',', $allowedTables, true); + $onlyRootLevel = true; + foreach ($tablesArr as $currentTable) { + if ($this->tcaSchemaFactory->has($currentTable)) { + $schema = $this->tcaSchemaFactory->get($currentTable); + if ($schema->getCapability(TcaSchemaCapability::RestrictionRootLevel)->canExistOnPages()) { + $onlyRootLevel = false; + break; + } + } + } + if ($onlyRootLevel) { + $withTree = false; + // page to work on is root + $this->expandPage = 0; + } + } + + $contentOnly = (bool)($this->getRequest()->getQueryParams()['contentOnly'] ?? false); + $renderedRecordList = $this->renderTableRecords($allowedTables); + + $this->pageRenderer->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:recordSelector')); + $view = $this->view; + $view->assignMultiple([ + 'treeEnabled' => $withTree, + 'treeActions' => $allowedTables === 'pages' ? ['select'] : [], + 'activePage' => $this->expandPage, + 'initialNavigationWidth' => $this->getBackendUser()->uc['selector']['navigation']['width'] ?? 250, + 'content' => $renderedRecordList, + 'contentOnly' => $contentOnly, + ]); + $content = $this->view->render('ElementBrowser/Page'); + if ($contentOnly) { + return $content; + } + $this->pageRenderer->setBodyContent('getBodyTagParameters() . '>' . $content); + return $this->pageRenderer->render($this->getRequest()); + } + + /** + * This lists all content elements for the given list of tables + * + * @param string $tables Comma separated list of tables. Set to "*" if you want all tables. + * @return string HTML code + */ + protected function renderTableRecords($tables) + { + $request = $this->getRequest(); + $backendUser = $this->getBackendUser(); + if ($this->expandPage === null || $this->expandPage < 0 || !$backendUser->isInWebMount($this->expandPage)) { + return ''; + } + // Set array with table names to list: + if (trim($tables) === '*') { + $tablesArr = $this->tcaSchemaFactory->all()->getNames(); + } else { + $tablesArr = GeneralUtility::trimExplode(',', $tables, true); + } + + $out = ''; + // Create the header, showing the current page for which the listing is. + // Includes link to the page itself, if pages are amount allowed tables. + $mainPageRecord = BackendUtility::getRecordWSOL('pages', $this->expandPage); + if (is_array($mainPageRecord)) { + $pText = htmlspecialchars(BackendUtility::cropToTitleLength($mainPageRecord['title'])); + + $out .= '

' . $this->iconFactory->getIconForRecord('pages', $mainPageRecord, IconSize::SMALL)->render() . ' '; + if (in_array('pages', $tablesArr, true)) { + $out .= ''; + $out .= '' + . $this->iconFactory->getIcon('actions-plus', IconSize::SMALL)->render() + . '' + . '' + . $pText + . ''; + $out .= ''; + } else { + $out .= $pText; + } + $out .= '

'; + } + + $permsClause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW); + $pageInfo = BackendUtility::readPageAccess($this->expandPage, $permsClause); + $existingModuleData = $backendUser->getModuleData('records'); + $moduleData = new ModuleData('records', is_array($existingModuleData) ? $existingModuleData : []); + + $dbList = GeneralUtility::makeInstance(ElementBrowserRecordList::class); + $dbList->setRequest($request); + $dbList->setModuleData($moduleData); + $dbList->setOverrideUrlParameters($this->getUrlParameters([]), $request); + $dbList->setIsEditable(false); + $dbList->calcPerms = new Permission($backendUser->calcPerms($pageInfo)); + $dbList->noControlPanels = true; + $dbList->clickMenuEnabled = false; + $dbList->displayRecordDownload = false; + $dbList->tableList = implode(',', $tablesArr); + + // Extract relating table and field from field reference (e.g., "data[pages][79][storage_pid]") + $fieldReferenceParts = $this->browserParameters->getFieldReferenceParts(); + if ($fieldReferenceParts['tableName'] !== '' && $fieldReferenceParts['fieldName'] !== '') { + $dbList->setRelatingTableAndField($fieldReferenceParts['tableName'], $fieldReferenceParts['fieldName']); + } + + $selectedTable = (string)($request->getParsedBody()['table'] ?? $request->getQueryParams()['table'] ?? ''); + $searchWord = (string)($request->getParsedBody()['searchTerm'] ?? $request->getQueryParams()['searchTerm'] ?? ''); + $searchLevels = (int)($request->getParsedBody()['search_levels'] ?? $request->getQueryParams()['search_levels'] ?? $this->modTSconfig['searchLevel.']['default'] ?? 0); + $pointer = (int)($request->getParsedBody()['pointer'] ?? $request->getQueryParams()['pointer'] ?? 0); + + $dbList->start( + (int)$this->expandPage, + $selectedTable, + MathUtility::forceIntegerInRange($pointer, 0, 100000), + $searchWord, + $searchLevels + ); + + $tableList = $dbList->generateList(); + + $out .= $this->renderSearchBox($request, $dbList, $searchWord, $searchLevels); + + // Add the HTML for the record list to output variable: + $out .= $tableList; + + return $out; + } + + protected function renderSearchBox(ServerRequestInterface $request, ElementBrowserRecordList $dblist, string $searchWord, int $searchLevels): string + { + return GeneralUtility::makeInstance(RecordSearchBoxComponent::class) + ->setAllowedSearchLevels((array)($this->modTSconfig['searchLevel.']['items.'] ?? [])) + ->setSearchWord($searchWord) + ->setSearchLevel($searchLevels) + ->render($request, $dblist->listURL('', null, 'pointer,searchTerm')); + } + + /** + * @param array $values Array of values to include into the parameters + * @return array Array of parameters which have to be added to URLs + */ + public function getUrlParameters(array $values): array + { + $pid = $values['pid'] ?? $this->expandPage; + return array_merge( + [ + 'mode' => 'db', + 'expandPage' => $pid, + ], + $this->browserParameters->toQueryParameters() + ); + } +} diff --git a/Classes/ElementBrowser/ElementBrowserInterface.php b/Classes/ElementBrowser/ElementBrowserInterface.php new file mode 100644 index 0000000..b0035be --- /dev/null +++ b/Classes/ElementBrowser/ElementBrowserInterface.php @@ -0,0 +1,46 @@ +getQueryParams(); + $parsedBody = $request->getParsedBody() ?? []; + + return new self( + fieldReference: (string)($parsedBody['fieldReference'] ?? $queryParams['fieldReference'] ?? ''), + allowedTypes: (string)($parsedBody['allowedTypes'] ?? $queryParams['allowedTypes'] ?? ''), + disallowedFileExtensions: (string)($parsedBody['disallowedFileExtensions'] ?? $queryParams['disallowedFileExtensions'] ?? ''), + irreObjectId: (string)($parsedBody['irreObjectId'] ?? $queryParams['irreObjectId'] ?? ''), + useEvents: (bool)(int)($parsedBody['useEvents'] ?? $queryParams['useEvents'] ?? 0), + ); + } + + /** + * Returns the allowed file extensions as an array. + * + * @return string[] List of allowed file extensions + */ + public function getAllowedFileExtensions(): array + { + if ($this->allowedTypes === '' || $this->allowedTypes === '*') { + return []; + } + + // Skip if it looks like a table name (contains underscore typical for TYPO3 tables) + if (str_contains($this->allowedTypes, 'sys_file')) { + return []; + } + + return GeneralUtility::trimExplode(',', $this->allowedTypes, true); + } + + /** + * Returns the disallowed file extensions as an array. + * + * @return string[] List of disallowed file extensions + */ + public function getDisallowedFileExtensions(): array + { + if ($this->disallowedFileExtensions === '') { + return []; + } + + return GeneralUtility::trimExplode(',', $this->disallowedFileExtensions, true); + } + + /** + * Parses the allowed file extensions from the allowedTypes field. + * + * @return array{allowed: string[], disallowed: string[]} + */ + public function getFileExtensions(): array + { + return [ + 'allowed' => $this->getAllowedFileExtensions(), + 'disallowed' => $this->getDisallowedFileExtensions(), + ]; + } + + /** + * Parses the allowed tables from the allowedTypes field. + * + * @return string[] List of allowed table names + */ + public function getAllowedTables(): array + { + if ($this->allowedTypes === '' || $this->allowedTypes === '*') { + return []; + } + + return GeneralUtility::trimExplode(',', $this->allowedTypes, true); + } + + /** + * Returns the field reference parsed into table name and field name. + * + * Parses format like "data[tt_content][123][image]" to extract + * table name ("tt_content") and field name ("image"). + * + * @return array{tableName: string, fieldName: string} + */ + public function getFieldReferenceParts(): array + { + $result = [ + 'tableName' => '', + 'fieldName' => '', + ]; + + if ($this->fieldReference === '') { + return $result; + } + + // Parse "data[table][uid][field]" format + $parts = explode('[', $this->fieldReference); + if (count($parts) >= 4) { + // parts[1] = "table]", parts[3] = "field]" + $result['tableName'] = rtrim($parts[1], ']'); + $result['fieldName'] = rtrim($parts[3], ']'); + } + + return $result; + } + + /** + * Returns data attributes for use in HTML elements (body tag). + * + * @return array + */ + public function toDataAttributes(): array + { + return [ + 'data-field-reference' => $this->fieldReference, + 'data-irre-object-id' => $this->irreObjectId ?: null, + 'data-use-events' => $this->useEvents ? 'true' : null, + ]; + } + + /** + * Returns array representation of the parameters. + * + * @return array{ + * fieldReference: string, + * allowedTypes: string, + * disallowedFileExtensions: string, + * irreObjectId: string, + * useEvents: bool + * } + */ + public function toArray(): array + { + return [ + 'fieldReference' => $this->fieldReference, + 'allowedTypes' => $this->allowedTypes, + 'disallowedFileExtensions' => $this->disallowedFileExtensions, + 'irreObjectId' => $this->irreObjectId, + 'useEvents' => $this->useEvents, + ]; + } + + /** + * Returns URL query parameters array (new format). + * + * @return array + */ + public function toQueryParameters(): array + { + $params = []; + if ($this->fieldReference !== '') { + $params['fieldReference'] = $this->fieldReference; + } + if ($this->allowedTypes !== '') { + $params['allowedTypes'] = $this->allowedTypes; + } + if ($this->disallowedFileExtensions !== '') { + $params['disallowedFileExtensions'] = $this->disallowedFileExtensions; + } + if ($this->irreObjectId !== '') { + $params['irreObjectId'] = $this->irreObjectId; + } + if ($this->useEvents) { + $params['useEvents'] = '1'; + } + return $params; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/Classes/ElementBrowser/ElementBrowserRegistry.php b/Classes/ElementBrowser/ElementBrowserRegistry.php new file mode 100644 index 0000000..482670b --- /dev/null +++ b/Classes/ElementBrowser/ElementBrowserRegistry.php @@ -0,0 +1,79 @@ +getIdentifier(); + if ($identifier === '') { + throw new \InvalidArgumentException('Identifier for element browser ' . get_class($elementBrowser) . ' is empty.', 1647241084); + } + if (isset($this->elementBrowsers[$identifier])) { + throw new \InvalidArgumentException('Element browser with identifier ' . $identifier . ' is already registered.', 1647241085); + } + $this->elementBrowsers[$identifier] = $elementBrowser; + } + } + + /** + * Whether a registered element browser exists for the identifier + */ + public function hasElementBrowser(string $identifier): bool + { + return isset($this->elementBrowsers[$identifier]); + } + + /** + * Get registered element browser by identifier + */ + public function getElementBrowser(string $identifier): ElementBrowserInterface + { + if (!$this->hasElementBrowser($identifier)) { + throw new \UnexpectedValueException('Element browser with identifier ' . $identifier . ' is not registered.', 1647241086); + } + return $this->elementBrowsers[$identifier]; + } + + /** + * Get all registered element browsers + * + * @return ElementBrowserInterface[] + */ + public function getElementBrowsers(): array + { + return $this->elementBrowsers; + } +} diff --git a/Classes/ElementBrowser/Event/IsFileSelectableEvent.php b/Classes/ElementBrowser/Event/IsFileSelectableEvent.php new file mode 100644 index 0000000..e96d435 --- /dev/null +++ b/Classes/ElementBrowser/Event/IsFileSelectableEvent.php @@ -0,0 +1,52 @@ +file; + } + + public function isFileSelectable(): bool + { + return $this->isFileSelectable; + } + + public function allowFileSelection(): void + { + $this->isFileSelectable = true; + } + + public function denyFileSelection(): void + { + $this->isFileSelectable = false; + } +} diff --git a/Classes/Event/AddUserSettingsJavaScriptModulesEvent.php b/Classes/Event/AddUserSettingsJavaScriptModulesEvent.php new file mode 100644 index 0000000..046c43e --- /dev/null +++ b/Classes/Event/AddUserSettingsJavaScriptModulesEvent.php @@ -0,0 +1,60 @@ +request; + } + + /** + * @param string $specifier Bare module identifier like @my/package/filename.js + */ + public function addJavaScriptModule(string $specifier): void + { + if (in_array($specifier, $this->javaScriptModules, true)) { + return; + } + $this->javaScriptModules[] = $specifier; + } + + /** + * @return string[] + */ + public function getJavaScriptModules(): array + { + return $this->javaScriptModules; + } +} diff --git a/Classes/EventListener/AfterBackendPageRenderEventListener.php b/Classes/EventListener/AfterBackendPageRenderEventListener.php new file mode 100644 index 0000000..0cd3cf6 --- /dev/null +++ b/Classes/EventListener/AfterBackendPageRenderEventListener.php @@ -0,0 +1,52 @@ +pageRenderer->getJavaScriptRenderer(); + $javaScriptRenderer->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/live-search/result-types/default-result-type.js', 'registerType') + ->invoke(null, DatabaseRecordProvider::class) + ); + $javaScriptRenderer->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/live-search/result-types/page-result-type.js', 'registerRenderer') + ->invoke(null, PageRecordProvider::class) + ); + $javaScriptRenderer->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/backend/live-search/result-types/backend-module-result-type.js', 'registerType') + ->invoke(null, BackendModuleProvider::class) + ); + } +} diff --git a/Classes/EventListener/FailedLoginAttemptNotification.php b/Classes/EventListener/FailedLoginAttemptNotification.php new file mode 100644 index 0000000..a3df440 --- /dev/null +++ b/Classes/EventListener/FailedLoginAttemptNotification.php @@ -0,0 +1,186 @@ +notificationRecipientEmailAddress = $notificationRecipientEmailAddress ?? (string)$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr']; + } + + /** + * Sends a warning email if there has been a certain amount of failed logins during a period. + * If a login fails, this function is called. It will look up the sys_log to see if there + * have been more than $failedLoginAttemptsThreshold failed logins the last X seconds + * (default 3600, see $warningPeriod). If so, an email with a warning is sent. This also + * includes failed multi-factor authentication failures. + */ + #[AsEventListener(identifier: 'typo3/cms-backend/failed-login-attempt-notification', event: LoginAttemptFailedEvent::class)] + #[AsEventListener(identifier: 'typo3/cms-backend/failed-mfa-verification-notification', event: MfaVerificationFailedEvent::class)] + public function __invoke(LoginAttemptFailedEvent|MfaVerificationFailedEvent $event): void + { + if (!$event->isBackendAttempt()) { + // This notification only works for backend users + return; + } + if (!GeneralUtility::validEmail($this->notificationRecipientEmailAddress)) { + return; + } + + /** @var BackendUserAuthentication $user */ + $user = $event->getUser(); + $earliestTimeToCheckForFailures = $GLOBALS['EXEC_TIME'] - $this->warningPeriod; + $loginFailures = $this->getLoginFailures($earliestTimeToCheckForFailures); + // Check for more than a maximum number of login failures with the last period + if (count($loginFailures) > $this->failedLoginAttemptsThreshold) { + // OK, so there were more than the max allowed number of login failures - so we will send an email then. + $this->sendLoginAttemptEmail($loginFailures, $event->getRequest()); + // Login failure attempt written to log, which will be picked up later-on again + $user->writelog( + SystemLogType::LOGIN, + SystemLogLoginAction::SEND_FAILURE_WARNING_EMAIL, + SystemLogErrorClassification::MESSAGE, + null, + 'Failure warning (%s failures within %s seconds) sent by email to %s', + [count($loginFailures), $this->warningPeriod, $this->notificationRecipientEmailAddress] + ); + } + } + + /** + * Retrieves all failed logins within a given timeframe until now. + * + * @param int $earliestTimeToCheckForFailures A UNIX timestamp that acts as the "earliest" date to check within the logs + * @return array a list of sys_log entries since the earliest, or empty if no entries have been logged + */ + private function getLoginFailures(int $earliestTimeToCheckForFailures): array + { + // Get last flag set in the log for sending an email + // If a notification was e.g. sent 20mins ago, only check the entries of the last 20 minutes + $queryBuilder = $this->createPreparedQueryBuilder($earliestTimeToCheckForFailures, SystemLogLoginAction::SEND_FAILURE_WARNING_EMAIL); + $statement = $queryBuilder + ->select('tstamp') + ->orderBy('tstamp', 'DESC') + ->setMaxResults(1) + ->executeQuery(); + if ($lastTimeANotificationWasSent = $statement->fetchOne()) { + $earliestTimeToCheckForFailures = (int)$lastTimeANotificationWasSent; + } + $queryBuilder = $this->createPreparedQueryBuilder($earliestTimeToCheckForFailures, SystemLogLoginAction::ATTEMPT); + return $queryBuilder + ->select('*') + ->orderBy('tstamp') + ->executeQuery() + ->fetchAllAssociative(); + } + + /** + * Sends out an email if the number of attempts have exceeded a limit. + * + * @param array $previousFailures sys_log entries that have been logged since the last time a notification was sent + */ + private function sendLoginAttemptEmail(array $previousFailures, ServerRequestInterface $request): void + { + $emailData = []; + foreach ($previousFailures as $row) { + $text = $this->formatLogDetails($row['details'] ?? '', $row['log_data'] ?? ''); + if ((int)$row['type'] === SystemLogType::LOGIN) { + $text = str_replace('###IP###', $row['IP'], $text); + } + $emailData[] = [ + 'row' => $row, + 'text' => $text, + ]; + } + $email = $this->templatedEmailFactory->create($request) + ->to($this->notificationRecipientEmailAddress) + ->setTemplate('Security/LoginAttemptFailedWarning') + ->assign('lines', $emailData); + + try { + $this->mailer->send($email); + } catch (TransportExceptionInterface $e) { + // Sending mail failed. Probably broken smtp setup. + // @todo Maybe log that sending mail failed. + } + } + + private function createPreparedQueryBuilder(int $earliestLogDate, int $loginAction): QueryBuilder + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_log'); + $queryBuilder + ->from('sys_log') + ->where( + $queryBuilder->expr()->eq( + 'type', + $queryBuilder->createNamedParameter(SystemLogType::LOGIN, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'action', + $queryBuilder->createNamedParameter($loginAction, Connection::PARAM_INT) + ), + $queryBuilder->expr()->gt( + 'tstamp', + $queryBuilder->createNamedParameter($earliestLogDate, Connection::PARAM_INT) + ) + ); + return $queryBuilder; + } +} diff --git a/Classes/EventListener/FailedMfaAttemptNotification.php b/Classes/EventListener/FailedMfaAttemptNotification.php new file mode 100644 index 0000000..5e2c153 --- /dev/null +++ b/Classes/EventListener/FailedMfaAttemptNotification.php @@ -0,0 +1,62 @@ +isBackendAttempt()) { + // This notification only works for backend users + return; + } + + $backendUser = $event->getUser(); + $emailAddress = $backendUser->user['email']; + if (!GeneralUtility::validEmail($emailAddress)) { + return; + } + + $emailObject = $this->templatedEmailFactory->create($event->getRequest()) + ->to(new Address($emailAddress, $backendUser->user['realName'])) + ->assign('provider', $event->getProvider()) + ->setTemplate('Mfa/FailedMfaNotification'); + $this->mailer->send($emailObject); + } +} diff --git a/Classes/EventListener/InitializeCodeEditorInEditFileForm.php b/Classes/EventListener/InitializeCodeEditorInEditFileForm.php new file mode 100644 index 0000000..8d8f563 --- /dev/null +++ b/Classes/EventListener/InitializeCodeEditorInEditFileForm.php @@ -0,0 +1,54 @@ +registerConfiguration(); + + $fileExtension = $event->getFile()->getExtension(); + + try { + $mode = $this->modeRegistry->getByFileExtension($fileExtension); + } catch (InvalidModeException $e) { + $mode = $this->modeRegistry->getDefaultMode(); + } + + $formData = $event->getFormData(); + $formData['processedTca']['columns']['data']['config']['renderType'] = 'codeEditor'; + $formData['processedTca']['columns']['data']['config']['format'] = $mode->getFormatCode(); + $event->setFormData($formData); + } +} diff --git a/Classes/Exception.php b/Classes/Exception.php new file mode 100644 index 0000000..c609c2f --- /dev/null +++ b/Classes/Exception.php @@ -0,0 +1,23 @@ +data = $data; + } + + /** + * Handler for single nodes + * + * @return array As defined in initializeResultArray() of AbstractNode + */ + abstract public function render(): array; + + /** + * Initialize the array that is returned to parent after calling. This structure + * is identical for *all* nodes. Parent will merge the return of a child with its + * own stuff and in itself return an array of the same structure. + * + * @return array{ + * additionalInlineLanguageLabelFiles: list, + * stylesheetFiles: list, + * javaScriptModules: list, + * inlineData: array, + * html: string, + * } + */ + protected function initializeResultArray(): array + { + return [ + 'additionalInlineLanguageLabelFiles' => [], + 'stylesheetFiles' => [], + 'javaScriptModules' => [], + 'inlineData' => [], + 'html' => '', + ]; + } + + /** + * Merge existing data with a child return array. + * The incoming $childReturn array should be initialized + * using initializeResultArray() beforehand. + * + * @param array $existing Currently merged array + * @param array $childReturn Array returned by child + * @param bool $mergeHtml If false, the ['html'] section of $childReturn will NOT be added to $existing + * @return array Result array + */ + protected function mergeChildReturnIntoExistingResult(array $existing, array $childReturn, bool $mergeHtml = true): array + { + if ($mergeHtml && !empty($childReturn['html'])) { + $existing['html'] .= LF . $childReturn['html']; + } + foreach ($childReturn['stylesheetFiles'] ?? [] as $value) { + $existing['stylesheetFiles'][] = $value; + } + foreach ($childReturn['javaScriptModules'] ?? [] as $module) { + $existing['javaScriptModules'][] = $module; + } + foreach ($childReturn['additionalInlineLanguageLabelFiles'] ?? [] as $inlineLanguageLabelFile) { + $existing['additionalInlineLanguageLabelFiles'][] = $inlineLanguageLabelFile; + } + if (!empty($childReturn['inlineData'])) { + $existingInlineData = $existing['inlineData']; + $childInlineData = $childReturn['inlineData']; + ArrayUtility::mergeRecursiveWithOverrule($existingInlineData, $childInlineData); + $existing['inlineData'] = $existingInlineData; + } + return $existing; + } + + /** + * Build JSON string for validations rules. + */ + protected function getValidationDataAsJsonString(array $config): string + { + $validationRules = []; + if (!empty($config['eval'])) { + $evalList = GeneralUtility::trimExplode(',', $config['eval'], true); + foreach ($evalList as $evalType) { + $validationRules[] = [ + 'type' => $evalType, + ]; + } + } + if (!empty($config['range'])) { + $newValidationRule = [ + 'type' => 'range', + ]; + + $isDateTime = ($config['type'] ?? '') === 'datetime'; + if (!empty($config['range']['lower'])) { + $lower = (int)$config['range']['lower']; + if ($isDateTime) { + $lower = date(DateTimeFormat::ISO8601_LOCALTIME, $lower); + } + $newValidationRule['lower'] = $lower; + } + if (!empty($config['range']['upper'])) { + $upper = (int)$config['range']['upper']; + if ($isDateTime) { + $upper = date(DateTimeFormat::ISO8601_LOCALTIME, $upper); + } + $newValidationRule['upper'] = $upper; + } + $validationRules[] = $newValidationRule; + } + if (!empty($config['maxitems']) || !empty($config['minitems'])) { + $minItems = isset($config['minitems']) ? (int)$config['minitems'] : 0; + $maxItems = isset($config['maxitems']) ? (int)$config['maxitems'] : 99999; + $type = $config['type'] ?: 'range'; + $validationRules[] = [ + 'type' => $type, + 'minItems' => $minItems, + 'maxItems' => $maxItems, + ]; + } + if (!empty($config['required'])) { + $validationRules[] = ['type' => 'required']; + } + if (!empty($config['min'])) { + $validationRules[] = ['type' => 'min']; + } + return json_encode($validationRules); + } +} diff --git a/Classes/Form/Behavior/OnFieldChangeInterface.php b/Classes/Form/Behavior/OnFieldChangeInterface.php new file mode 100644 index 0000000..f5fd357 --- /dev/null +++ b/Classes/Form/Behavior/OnFieldChangeInterface.php @@ -0,0 +1,26 @@ +} + */ + public function toArray(): array; +} diff --git a/Classes/Form/Behavior/OnFieldChangeTrait.php b/Classes/Form/Behavior/OnFieldChangeTrait.php new file mode 100644 index 0000000..090b206 --- /dev/null +++ b/Classes/Form/Behavior/OnFieldChangeTrait.php @@ -0,0 +1,73 @@ + + */ + protected function getOnFieldChangeItems(array $items): array + { + if ($items === []) { + return []; + } + return array_map( + static function (OnFieldChangeInterface $item): array { + return $item->toArray(); + }, + array_values($items) + ); + } + + /** + * @param string $event target client event, either `change` or `click` + * @param list $items `fieldChangeFunc` items + * @return array HTML attrs, not encoded - consumers MUST encode with `htmlspecialchars` + */ + protected function getOnFieldChangeAttrs(string $event, array $items): array + { + if ($items === []) { + return []; + } + $onFieldChangeItems = $this->getOnFieldChangeItems($items); + return [ + 'data-formengine-field-change-event' => $event, + 'data-formengine-field-change-items' => GeneralUtility::jsonEncodeForHtmlAttribute($onFieldChangeItems, false), + ]; + } + + /** + * Forwards URL query params for `LinkBrowserController` + * @param list $items `fieldChangeFunc` items + * @return array{fieldChangeFunc: array, fieldChangeFuncHash: string} relevant URL query params for `LinkBrowserController` + */ + protected function forwardOnFieldChangeQueryParams(array $items): array + { + $func = $this->getOnFieldChangeItems($items); + $hashService = GeneralUtility::makeInstance(HashService::class); + return [ + 'fieldChangeFunc' => $func, + 'fieldChangeFuncHash' => $hashService->hmac(serialize($func), 'backend-link-browser'), + ]; + } +} diff --git a/Classes/Form/Behavior/ReloadOnFieldChange.php b/Classes/Form/Behavior/ReloadOnFieldChange.php new file mode 100644 index 0000000..a2196a8 --- /dev/null +++ b/Classes/Form/Behavior/ReloadOnFieldChange.php @@ -0,0 +1,42 @@ +confirmation = $confirmation; + } + + public function toArray(): array + { + return [ + 'name' => 'typo3-backend-form-reload', + 'data' => [ + 'confirmation' => $this->confirmation, + ], + ]; + } +} diff --git a/Classes/Form/Behavior/UpdateBitmaskOnFieldChange.php b/Classes/Form/Behavior/UpdateBitmaskOnFieldChange.php new file mode 100644 index 0000000..cb420e1 --- /dev/null +++ b/Classes/Form/Behavior/UpdateBitmaskOnFieldChange.php @@ -0,0 +1,50 @@ +position = $position; + $this->total = $total; + $this->invert = $invert; + $this->elementName = $elementName; + } + + public function toArray(): array + { + return [ + 'name' => 'typo3-backend-form-update-bitmask', + 'data' => [ + 'position' => $this->position, + 'total' => $this->total, + 'invert' => $this->invert, + 'elementName' => $this->elementName, + ], + ]; + } +} diff --git a/Classes/Form/Behavior/UpdateValueOnFieldChange.php b/Classes/Form/Behavior/UpdateValueOnFieldChange.php new file mode 100644 index 0000000..dcb995c --- /dev/null +++ b/Classes/Form/Behavior/UpdateValueOnFieldChange.php @@ -0,0 +1,61 @@ +tableName = $tableName; + $this->identifier = $identifier; + $this->fieldName = $fieldName; + $this->elementName = $elementName; + } + + public function withElementName(string $elementName): self + { + if ($this->elementName === $elementName) { + return $this; + } + $target = clone $this; + $target->elementName = $elementName; + return $target; + } + + public function toArray(): array + { + return [ + 'name' => 'typo3-backend-form-update-value', + 'data' => [ + 'tableName' => $this->tableName, + 'identifier' => $this->identifier, + 'fieldName' => $this->fieldName, + 'elementName' => $this->elementName, + ], + ]; + } +} diff --git a/Classes/Form/Container/AbstractContainer.php b/Classes/Form/Container/AbstractContainer.php new file mode 100644 index 0000000..4aeee46 --- /dev/null +++ b/Classes/Form/Container/AbstractContainer.php @@ -0,0 +1,174 @@ +nodeFactory = $nodeFactory; + } + + public function injectBackendViewFactory(BackendViewFactory $backendViewFactory) + { + $this->backendViewFactory = $backendViewFactory; + } + + /** + * Merge field information configuration with default and render them. + * + * @return array Result array + */ + protected function renderFieldInformation(): array + { + $options = $this->data; + $fieldInformation = $this->defaultFieldInformation; + $currentRenderType = $this->data['renderType']; + $fieldInformationFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['fieldInformation'] ?? []; + ArrayUtility::mergeRecursiveWithOverrule($fieldInformation, $fieldInformationFromTca); + $options['renderType'] = 'fieldInformation'; + $options['renderData']['fieldInformation'] = $fieldInformation; + return $this->nodeFactory->create($options)->render(); + } + + /** + * Merge field control configuration with default controls and render them. + * + * @return array Result array + */ + protected function renderFieldControl(): array + { + $options = $this->data; + $fieldControl = $this->defaultFieldControl; + $currentRenderType = $this->data['renderType']; + $fieldControlFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['fieldControl'] ?? []; + ArrayUtility::mergeRecursiveWithOverrule($fieldControl, $fieldControlFromTca); + $options['renderType'] = 'fieldControl'; + $options['renderData']['fieldControl'] = $fieldControl; + return $this->nodeFactory->create($options)->render(); + } + + /** + * Merge field wizard configuration with default wizards and render them. + * + * @return array Result array + */ + protected function renderFieldWizard(): array + { + $options = $this->data; + $fieldWizard = $this->defaultFieldWizard; + $currentRenderType = $this->data['renderType']; + $fieldWizardFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['fieldWizard'] ?? []; + ArrayUtility::mergeRecursiveWithOverrule($fieldWizard, $fieldWizardFromTca); + $options['renderType'] = 'fieldWizard'; + $options['renderData']['fieldWizard'] = $fieldWizard; + return $this->nodeFactory->create($options)->render(); + } + + /** + * A single field of TCA 'types' 'showitem' can have three semicolon separated configuration options: + * fieldName: Name of the field to be found in TCA 'columns' section + * fieldLabel: An alternative field label + * paletteName: Name of a palette to be found in TCA 'palettes' section that is rendered after this field + * + * @param string $field Semicolon separated field configuration + * @throws \RuntimeException + */ + protected function explodeSingleFieldShowItemConfiguration(string $field): array + { + $fieldArray = GeneralUtility::trimExplode(';', $field); + if (empty($fieldArray[0])) { + throw new \RuntimeException('Field must not be empty', 1426448465); + } + return [ + 'fieldName' => $fieldArray[0], + 'fieldLabel' => !empty($fieldArray[1]) ? $fieldArray[1] : null, + 'paletteName' => !empty($fieldArray[2]) ? $fieldArray[2] : null, + ]; + } + + /** + * Render tabs with label and content. Used by TabsContainer and FlexFormTabsContainer. + * Re-uses the template Tabs.fluid.html which is also used by ModuleTemplate.php. + * + * @param array $menuItems Tab elements, each element is an array with "label" and "content" + * @param string $domId DOM id attribute, will be appended with an iteration number per tab. + */ + protected function renderTabMenu(array $menuItems, string $domId): string + { + $view = $this->backendViewFactory->create($this->data['request']); + $view->assignMultiple([ + 'id' => $domId, + 'items' => $menuItems, + 'defaultTabIndex' => 1, + 'wrapContent' => false, + 'storeLastActiveTab' => true, + ]); + return $view->render('Form/Tabs'); + } + + protected function wrapWithFieldsetAndLegend(string $fieldContent): string + { + $legend = htmlspecialchars($this->data['parameterArray']['fieldConf']['label']); + if ($this->getBackendUserAuthentication()->shallDisplayDebugInformation()) { + $fieldName = $this->data['flexFormContainerFieldName'] ?? $this->data['flexFormFieldName'] ?? $this->data['fieldName']; + $legend .= ' [' . htmlspecialchars($fieldName) . ']'; + } + $description = $this->renderDescription(); + return '
' . $legend . '' . $description . $fieldContent . '
'; + } + + protected function renderDescription(): string + { + $description = (string)($this->data['parameterArray']['fieldConf']['description'] ?? ''); + if ($description === '') { + return ''; + } + $description = $this->getLanguageService()->sL($description); + if ($description === '') { + return ''; + } + return '
' . nl2br(htmlspecialchars($description)) . '
'; + } + + protected function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Form/Container/FileReferenceContainer.php b/Classes/Form/Container/FileReferenceContainer.php new file mode 100644 index 0000000..0e8336a --- /dev/null +++ b/Classes/Form/Container/FileReferenceContainer.php @@ -0,0 +1,515 @@ +][][] => data------- + $formPrefix = $this->inlineStackProcessor->getFormPrefixFromStructure($this->data['inlineStructure']); + $domObjectId = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $this->data['inlineFirstPid']); + + $this->fileReferenceData = $this->data['inlineData']; + $this->fileReferenceData['map'][$formPrefix] = $domObjectId; + + $resultArray = $this->initializeResultArray(); + $resultArray['inlineData'] = $this->fileReferenceData; + + $html = ''; + $classes = []; + $combinationHtml = ''; + $record = $this->data['databaseRow']; + $uid = $record['uid'] ?? 0; + $appendFormFieldNames = '[' . self::FILE_REFERENCE_TABLE . '][' . $uid . ']'; + $objectId = $domObjectId . '-' . self::FILE_REFERENCE_TABLE . '-' . $uid; + $isNewRecord = $this->data['command'] === 'new'; + $hiddenFieldName = (string)($this->data['processedTca']['ctrl']['enablecolumns']['disabled'] ?? ''); + if (!$this->data['isInlineDefaultLanguageRecordInLocalizedParentContext']) { + if ($isNewRecord || $this->data['isInlineChildExpanded']) { + $fileReferenceData = $this->renderFileReference($this->data); + $html = $fileReferenceData['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fileReferenceData, false); + } else { + // This class is the marker for the JS-function to check if the full content has already been loaded + $classes[] = 't3js-not-loaded'; + } + if ($isNewRecord) { + // Add pid of file reference as hidden field + $html .= ''; + // Tell DataHandler this file reference is expanded + $ucFieldName = 'uc[inlineView]' + . '[' . $this->data['inlineTopMostParentTableName'] . ']' + . '[' . $this->data['inlineTopMostParentUid'] . ']' + . htmlspecialchars($appendFormFieldNames); + $html .= ''; + } else { + // Set additional field for processing for saving + $html .= ''; + if ($hiddenFieldName !== '' + && (!$this->data['isInlineChildExpanded'] + || !in_array($hiddenFieldName, $this->data['columnsToProcess'], true)) + ) { + $isHidden = (bool)($record[$hiddenFieldName] ?? false); + $html .= ''; + $html .= ''; + } + } + } + if ($this->data['inlineParentConfig']['renderFieldsOnly'] ?? false) { + // Render "body" part only + $resultArray['html'] = $html . $combinationHtml; + return $resultArray; + } + + // Render header row and content (if expanded) + if ($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext']) { + $classes[] = 'panel-placeholder'; + } + if ($record[$hiddenFieldName] ?? false) { + $classes[] = 'panel-hidden'; + } + if ($isNewRecord) { + $classes[] = 'inlineIsNewRecord'; + } + + // The hashed object id needs a non-numeric prefix, the value is used as ID selector in JavaScript + $hashedObjectId = 'hash-' . md5($objectId); + $containerAttributes = [ + 'id' => $objectId . '_div', + 'class' => 'form-irre-object panel panel-default ' . trim(implode(' ', $classes)), + 'data-object-uid' => $record['uid'] ?? 0, + 'data-object-id' => $objectId, + 'data-object-id-hash' => $hashedObjectId, + 'data-object-parent-group' => $domObjectId . '-' . self::FILE_REFERENCE_TABLE, + 'data-field-name' => $appendFormFieldNames, + 'data-topmost-parent-table' => $this->data['inlineTopMostParentTableName'], + 'data-topmost-parent-uid' => $this->data['inlineTopMostParentUid'], + 'data-placeholder-record' => $this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ? '1' : '0', + ]; + + $isExpanded = $this->data['isInlineChildExpanded'] ?? false; + $ariaControls = htmlspecialchars($objectId . '_fields', ENT_QUOTES | ENT_HTML5); + $resultArray['html'] = ' +
+
+
+ ' . $this->renderFileHeader($isExpanded, $ariaControls) . ' +
+
+
' . $html . $combinationHtml . '
+
'; + + return $resultArray; + } + + protected function renderFileReference(array $data): array + { + $data['tabAndInlineStack'][] = [ + 'inline', + $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $data['inlineFirstPid']) + . '-' + . $data['tableName'] + . '-' + . $data['databaseRow']['uid'], + ]; + + return $this->nodeFactory->create(array_replace_recursive($data, [ + 'inlineData' => $this->fileReferenceData, + 'renderType' => 'fullRecordContainer', + ]))->render(); + } + + /** + * Renders the HTML header for the file, such as the title, toggle-function, drag'n'drop, etc. + * Later on the command-icons are inserted here, too. + */ + protected function renderFileHeader(bool $isExpanded, string $ariaControls): string + { + $languageService = $this->getLanguageService(); + + $databaseRow = $this->data['databaseRow']; + $recordTitle = $this->getRecordTitle(); + + if (empty($recordTitle)) { + $recordTitle = '[' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']'; + } + + $objectId = htmlspecialchars($this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $this->data['inlineFirstPid']) + . '-' . self::FILE_REFERENCE_TABLE + . '-' . ($databaseRow['uid'] ?? 0)); + + $altText = BackendUtility::getRecordIconAltText($databaseRow, self::FILE_REFERENCE_TABLE, false); + + // Renders the header image (thumbnail, icon, or missing file indicator) + $headerImage = ''; + $headerBadge = ''; + $isMissing = false; + if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] ?? false) { + $fileUid = $databaseRow[self::FOREIGN_SELECTOR][0]['uid'] ?? null; + if (!empty($fileUid)) { + try { + $fileObject = $this->resourceFactory->getFileObject($fileUid); + if ($fileObject->isMissing()) { + $isMissing = true; + $recordTitle = htmlspecialchars($fileObject->getName()); + $headerImage = ' +
+ ' . $this->iconFactory->getIcon('default-not-found', IconSize::SMALL)->render() . ' +
'; + $headerBadge = ' +
+ ' + . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing')) . ' + +
'; + } elseif ($fileObject->isImage() || $fileObject->isMediaFile()) { + $imageSetup = $this->data['inlineParentConfig']['appearance']['headerThumbnail'] ?? []; + $cropVariantCollection = CropVariantCollection::create($databaseRow['crop'] ?? ''); + if (!$cropVariantCollection->getCropArea()->isEmpty()) { + $imageSetup['crop'] = $cropVariantCollection->getCropArea()->makeAbsoluteBasedOnFile($fileObject); + } + $processedImage = $fileObject->process( + ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, + array_merge(['maxWidth' => 60, 'maxHeight' => 45], $imageSetup) + ); + // Only use a thumbnail if the processing process was successful by checking if image width is set + if ($processedImage->getProperty('width')) { + $imageUrl = $processedImage->getPublicUrl() ?? ''; + $headerImage = ' +
+ +
'; + } + } + } catch (\InvalidArgumentException $e) { + $fileObject = null; + } + } + } + + if ($headerImage === '' && !$isMissing) { + $headerImage = ' +
+ ' . $this->iconFactory + ->getIconForRecord(self::FILE_REFERENCE_TABLE, $databaseRow, IconSize::SMALL) + ->setTitle($altText) + ->render() . ' +
'; + } + + return ' + +
+ ' . $this->renderFileReferenceHeaderControl() . ' +
'; + } + + /** + * Render the control-icons for a file reference (e.g. create new, sorting, delete, disable/enable). + */ + protected function renderFileReferenceHeaderControl(): string + { + $controls = []; + $databaseRow = $this->data['databaseRow']; + $databaseRow += [ + 'uid' => 0, + ]; + $parentConfig = $this->data['inlineParentConfig']; + $languageService = $this->getLanguageService(); + $backendUser = $this->getBackendUserAuthentication(); + $isNewItem = str_starts_with((string)$databaseRow['uid'], 'NEW'); + $fileReferenceTableTca = $this->data['tcaSchemata']->get(self::FILE_REFERENCE_TABLE); + $calcPerms = new Permission( + $backendUser->calcPerms(BackendUtility::readPageAccess( + (int)($this->data['parentPageRow']['uid'] ?? 0), + $backendUser->getPagePermsClause(Permission::PAGE_SHOW) + )) + ); + $event = $this->eventDispatcher->dispatch( + new ModifyFileReferenceEnabledControlsEvent($this->data, $databaseRow) + ); + if ($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext']) { + $controls['localize'] = $this->iconFactory + ->getIcon('actions-edit-localize-status-low', IconSize::SMALL) + ->setTitle($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localize.isLocalizable')) + ->render(); + } + if ($event->isControlEnabled('info')) { + if ($isNewItem) { + $controls['info'] = ' + + ' . $this->iconFactory->getIcon('empty-empty', IconSize::SMALL)->render() . ' + '; + } else { + $controls['info'] = ' + '; + } + } + // If the table is NOT a read-only table, then show these links: + if (!($parentConfig['readOnly'] ?? false) + && !($fileReferenceTableTca->getCapability(TcaSchemaCapability::AccessReadOnly)->getValue() ?? false) + && !($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false) + ) { + if ($event->isControlEnabled('sort')) { + $icon = 'actions-move-up'; + $class = ''; + if ((int)$parentConfig['inline']['first'] === (int)$databaseRow['uid']) { + $class = ' disabled'; + $icon = 'empty-empty'; + } + $controls['sort.up'] = ' + '; + + $icon = 'actions-move-down'; + $class = ''; + if ((int)$parentConfig['inline']['last'] === (int)$databaseRow['uid']) { + $class = ' disabled'; + $icon = 'empty-empty'; + } + $controls['sort.down'] = ' + '; + } + $sysFileMetadataTableTca = $this->data['tcaSchemata']->has('sys_file_metadata') ? $this->data['tcaSchemata']->get('sys_file_metadata') : null; + if (!$isNewItem + && ($languageField = ($sysFileMetadataTableTca?->getRawConfiguration()['languageField'] ?? false)) + && $backendUser->check('tables_modify', 'sys_file_metadata') + && $event->isControlEnabled('edit') + ) { + $languageId = (int)(is_array($databaseRow[$languageField] ?? null) + ? ($databaseRow[$languageField][0] ?? 0) + : ($databaseRow[$languageField] ?? 0)); + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_file_metadata'); + $metadataRecord = $queryBuilder + ->select('uid') + ->from('sys_file_metadata') + ->where( + $queryBuilder->expr()->eq( + 'file', + $queryBuilder->createNamedParameter((int)$databaseRow['uid_local'][0]['uid'], Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + $languageField, + $queryBuilder->createNamedParameter($languageId, Connection::PARAM_INT) + ) + ) + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + if (!empty($metadataRecord)) { + $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [ + 'edit[sys_file_metadata][' . (int)$metadataRecord['uid'] . ']' => 'edit', + 'module' => (string)($this->data['request']->getQueryParams()['module'] ?? ''), + 'returnUrl' => $this->data['returnUrl'], + ]); + $controls['edit'] = ' + + ' . $this->iconFactory->getIcon('actions-open', IconSize::SMALL)->render() . ' + '; + } + } + if ($event->isControlEnabled('delete') && $calcPerms->editContentPermissionIsGranted()) { + $recordInfo = $this->data['databaseRow']['uid_local'][0]['title'] ?? $this->data['recordTitle'] ?? ''; + if ($this->getBackendUserAuthentication()->shallDisplayDebugInformation()) { + $recordInfo .= ' [' . $this->data['tableName'] . ':' . $this->data['vanillaUid'] . ']'; + } + $controls['delete'] = ' + '; + } + if (($hiddenField = ($fileReferenceTableTca->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName())) !== '' + && ($fileReferenceTableTca->hasField($hiddenField)) + && $event->isControlEnabled('hide') + && ( + !($fileReferenceTableTca->getField($hiddenField)->getConfiguration()['exclude'] ?? false) + || $backendUser->check('non_exclude_fields', self::FILE_REFERENCE_TABLE . ':' . $hiddenField) + ) + ) { + if ($databaseRow[$hiddenField] ?? false) { + $controls['hide'] = ' + '; + } else { + $controls['hide'] = ' + '; + } + } + if (($parentConfig['appearance']['useSortable'] ?? false) && $event->isControlEnabled('dragdrop')) { + $controls['dragdrop'] = ' + + ' . $this->iconFactory->getIcon('actions-move-move', IconSize::SMALL)->render() . ' + '; + } + } elseif (($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false) + && MathUtility::canBeInterpretedAsInteger($this->data['inlineParentUid']) + && $event->isControlEnabled('localize') + ) { + $controls['localize'] = ' + '; + } + if ($lockInfo = BackendUtility::isRecordLocked(self::FILE_REFERENCE_TABLE, $databaseRow['uid'])) { + $controls['locked'] = ' + '; + } + + // Get modified controls. This means their markup was modified, new controls were added or controls got removed. + $controls = $this->eventDispatcher->dispatch( + new ModifyFileReferenceControlsEvent($controls, $this->data, $databaseRow) + )->getControls(); + + $out = ''; + if (($controls['edit'] ?? false) || ($controls['hide'] ?? false) || ($controls['delete'] ?? false)) { + $out .= ' +
+ ' . ($controls['edit'] ?? '') . ($controls['hide'] ?? '') . ($controls['delete'] ?? '') . ' +
'; + unset($controls['edit'], $controls['hide'], $controls['delete']); + } + if (($controls['info'] ?? false) || ($controls['new'] ?? false) || ($controls['sort.up'] ?? false) || ($controls['sort.down'] ?? false) || ($controls['dragdrop'] ?? false)) { + $out .= ' +
+ ' . ($controls['info'] ?? '') . ($controls['new'] ?? '') . ($controls['sort.up'] ?? '') . ($controls['sort.down'] ?? '') . ($controls['dragdrop'] ?? '') . ' +
'; + unset($controls['info'], $controls['new'], $controls['sort.up'], $controls['sort.down'], $controls['dragdrop']); + } + if ($controls['localize'] ?? false) { + $out .= '
' . $controls['localize'] . '
'; + unset($controls['localize']); + } + if ($controls !== [] && ($remainingControls = trim(implode('', $controls))) !== '') { + $out .= '
' . $remainingControls . '
'; + } + return $out; + } + + protected function getRecordTitle(): string + { + $databaseRow = $this->data['databaseRow']; + $fileRecord = $databaseRow['uid_local'][0]['row'] ?? null; + + if ($fileRecord === null) { + return $this->data['recordTitle'] ?: (string)$databaseRow['uid']; + } + + $title = '' . $this->getLabelFieldForRecord($databaseRow, $fileRecord, 'name') . ''; + + // In debug mode, add the table name to the record title + if ($this->getBackendUserAuthentication()->shallDisplayDebugInformation()) { + $title .= ' [' . self::FILE_REFERENCE_TABLE . ']'; + } + + return $title; + } + + protected function getLabelFieldForRecord(array $databaseRow, array $fileRecord, string $field): string + { + $value = ''; + + if (isset($databaseRow[$field])) { + $value = htmlspecialchars((string)$databaseRow[$field]); + } elseif (isset($fileRecord[$field])) { + $value = htmlspecialchars((string)$fileRecord[$field]); + } + + return $value; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Form/Container/FilesControlContainer.php b/Classes/Form/Container/FilesControlContainer.php new file mode 100644 index 0000000..d594d67 --- /dev/null +++ b/Classes/Form/Container/FilesControlContainer.php @@ -0,0 +1,448 @@ +> + */ + protected array $javaScriptModules = []; + + protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], + ]; + + public function __construct( + private readonly IconFactory $iconFactory, + private readonly InlineStackProcessor $inlineStackProcessor, + private readonly EventDispatcherInterface $eventDispatcher, + private readonly OnlineMediaHelperRegistry $onlineMediaHelperRegistry, + private readonly DefaultUploadFolderResolver $defaultUploadFolderResolver, + private readonly HashService $hashService, + ) {} + + /** + * Entry method + * + * @return array As defined in initializeResultArray() of AbstractNode + */ + public function render(): array + { + $languageService = $this->getLanguageService(); + + $this->fileReferenceData = $this->data['inlineData']; + + $inlineStructure = $this->data['inlineStructure']; + + $table = $this->data['tableName']; + $row = $this->data['databaseRow']; + $field = $this->data['fieldName']; + $parameterArray = $this->data['parameterArray']; + + $resultArray = $this->initializeResultArray(); + + $config = $parameterArray['fieldConf']['config']; + $isReadOnly = (bool)($config['readOnly'] ?? false); + $language = 0; + if ($this->data['tcaSchemata']->has($table) && $this->data['tcaSchemata']->get($table)->hasCapability(TcaSchemaCapability::Language)) { + $languageFieldName = $this->data['tcaSchemata']->get($table)->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName(); + $language = isset($row[$languageFieldName][0]) ? (int)$row[$languageFieldName][0] : (int)$row[$languageFieldName]; + } + + // Add the current inline job to the structure stack + $newStructureItem = [ + 'table' => $table, + 'uid' => $row['uid'], + 'field' => $field, + 'config' => $config, + ]; + + // Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF') + $itemName = (string)$parameterArray['itemFormElName']; + if ($itemName !== '') { + $flexFormParts = $this->extractFlexFormParts($itemName); + if ($flexFormParts !== null) { + $newStructureItem['flexform'] = $flexFormParts; + if ($flexFormParts !== [] + && isset($this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier']) + ) { + // Transport the flexform DS identifier fields to the FormFilesAjaxController + $config['dataStructureIdentifier'] = $this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier']; + } + } + } + + $inlineStructure['stable'][] = $newStructureItem; + + // Hand over original returnUrl to FormFilesAjaxController. Needed if opening for instance a + // nested element in a new view to then go back to the original returnUrl and not the url of + // the inline ajax controller + $config['originalReturnUrl'] = $this->data['returnUrl']; + + // e.g. data[
][][] + $formFieldName = $this->inlineStackProcessor->getFormPrefixFromStructure($inlineStructure); + // e.g. data------- + $formFieldIdentifier = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($inlineStructure, $this->data['inlineFirstPid']); + + $inlineChildren = $parameterArray['fieldConf']['children'] ?? []; + + $config['inline']['first'] = $config['inline']['last'] = false; + if (is_array($inlineChildren) && $inlineChildren !== []) { + $firstChild = array_first($inlineChildren); + if (isset($firstChild['databaseRow']['uid'])) { + $config['inline']['first'] = $firstChild['databaseRow']['uid']; + } + $lastChild = array_last($inlineChildren); + if (isset($lastChild['databaseRow']['uid'])) { + $config['inline']['last'] = $lastChild['databaseRow']['uid']; + } + } + + $top = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0); + + $this->fileReferenceData['config'][$formFieldIdentifier] = [ + 'table' => self::FILE_REFERENCE_TABLE, + ]; + $configJson = (string)json_encode($config); + $this->fileReferenceData['config'][$formFieldIdentifier . '-' . self::FILE_REFERENCE_TABLE] = [ + 'min' => $config['minitems'] ?? null, + 'max' => $config['maxitems'] ?? null, + 'sortable' => $config['appearance']['useSortable'] ?? false, + 'top' => [ + 'table' => $top['table'], + 'uid' => $top['uid'], + ], + 'context' => [ + 'config' => $configJson, + 'hmac' => $this->hashService->hmac($configJson, 'FilesContext'), + ], + ]; + $this->fileReferenceData['nested'][$formFieldIdentifier] = $this->data['tabAndInlineStack']; + + $resultArray['inlineData'] = $this->fileReferenceData; + + // @todo: It might be a good idea to have something like "isLocalizedRecord" or similar set by a data provider + $uidOfDefaultRecord = 0; + if ($this->data['tcaSchemata']->has($table) && $this->data['tcaSchemata']->get($table)->hasCapability(TcaSchemaCapability::Language)) { + $originPointerField = $this->data['tcaSchemata']->get($table)->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName(); + $uidOfDefaultRecord = $row[$originPointerField] ?? 0; + } + $isLocalizedParent = $language > 0 + && ($uidOfDefaultRecord[0] ?? $uidOfDefaultRecord) > 0 + && MathUtility::canBeInterpretedAsInteger($row['uid']); + $numberOfFullLocalizedChildren = 0; + $numberOfNotYetLocalizedChildren = 0; + foreach ($inlineChildren as $child) { + if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) { + $numberOfFullLocalizedChildren++; + } + if ($isLocalizedParent && $child['isInlineDefaultLanguageRecordInLocalizedParentContext']) { + $numberOfNotYetLocalizedChildren++; + } + } + + if ($isReadOnly || $numberOfFullLocalizedChildren >= ($config['maxitems'] ?? 0)) { + $config['inline']['showNewFileReferenceButton'] = false; + $config['inline']['showCreateNewRelationButton'] = false; + $config['inline']['showOnlineMediaAddButtonStyle'] = false; + } + + $fieldInformationResult = $this->renderFieldInformation(); + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); + + $fieldWizardResult = $this->renderFieldWizard(); + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false); + + $sortableRecordUids = $fileReferencesHtml = []; + foreach ($inlineChildren as $options) { + $options['inlineParentUid'] = $row['uid']; + $options['inlineFirstPid'] = $this->data['inlineFirstPid']; + $options['inlineParentConfig'] = $config; + $options['inlineData'] = $this->fileReferenceData; + $options['inlineStructure'] = $inlineStructure; + $options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray']; + $options['renderType'] = 'fileReferenceContainer'; + $fileReference = $this->nodeFactory->create($options)->render(); + $fileReferencesHtml[] = $fileReference['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fileReference, false); + if (!$options['isInlineDefaultLanguageRecordInLocalizedParentContext'] && isset($options['databaseRow']['uid'])) { + // Don't add record to list of "valid" uids if it is only the default + // language record of a not yet localized child + $sortableRecordUids[] = $options['databaseRow']['uid']; + } + } + + $view = $this->backendViewFactory->create($this->data['request']); + $view->assignMultiple([ + 'formFieldIdentifier' => $formFieldIdentifier, + 'formFieldName' => $formFieldName, + 'webComponentAttributes' => GeneralUtility::implodeAttributes([ + 'id' => $formFieldIdentifier, + 'data-type' => 'file', + 'data-object-group' => $formFieldIdentifier . '-' . self::FILE_REFERENCE_TABLE, + 'data-form-field' => $formFieldName, + 'data-expand-single' => (bool)($config['appearance']['expandSingle'] ?? false) ? 'true' : 'false', + 'data-sortable' => (bool)($config['appearance']['useSortable'] ?? false) ? 'true' : 'false', + 'data-min' => (int)($config['minitems'] ?? 0), + 'data-max' => (int)($config['maxitems'] ?? 0), + ], true), + 'fieldInformation' => $fieldInformationResult['html'], + 'fieldWizard' => $fieldWizardResult['html'], + 'fileReferences' => [ + 'id' => $formFieldIdentifier . '_records', + 'title' => $languageService->sL(trim($parameterArray['fieldConf']['label'] ?? '')), + 'records' => implode(LF, $fileReferencesHtml), + ], + 'sortableRecordUids' => implode(',', $sortableRecordUids), + 'validationRules' => $this->getValidationDataAsJsonString([ + 'type' => 'inline', + 'minitems' => $config['minitems'] ?? null, + 'maxitems' => $config['maxitems'] ?? null, + ]), + ]); + + if (!$isReadOnly && ($config['appearance']['showFileSelectors'] ?? true) !== false) { + /** @var FileExtensionFilter $fileExtensionFilter */ + $fileExtensionFilter = GeneralUtility::makeInstance(FileExtensionFilter::class); + $fileExtensionFilter->setAllowedFileExtensions($config['allowed'] ?? null); + $fileExtensionFilter->setDisallowedFileExtensions($config['disallowed'] ?? null); + $view->assign('fileSelectors', $this->getFileSelectors($inlineStructure, $config, $fileExtensionFilter)); + $filteredFileExtensions = $fileExtensionFilter->getFilteredFileExtensions(); + // Do not display "allowed file extensions" if all extensions are allowed (indicated by ['*']) + if (($filteredFileExtensions['allowedFileExtensions'] ?? null) === ['*']) { + $filteredFileExtensions = []; + } + $view->assignMultiple($filteredFileExtensions); + // Render the localization buttons if needed + if ($numberOfNotYetLocalizedChildren) { + $view->assignMultiple([ + 'showAllLocalizationLink' => !empty($config['appearance']['showAllLocalizationLink']), + 'showSynchronizationLink' => !empty($config['appearance']['showSynchronizationLink']), + ]); + } + } + + $event = $this->eventDispatcher->dispatch( + new CustomFileControlsEvent($resultArray, $table, $field, $row, $config, $formFieldIdentifier, $formFieldName) + ); + $resultArray = $event->getResultArray(); + $controls = $event->getControls(); + + if ($controls !== []) { + $view->assign('customControls', [ + 'id' => $formFieldIdentifier . '_customControls', + 'controls' => implode("\n", $controls), + ]); + } + + $resultArray['javaScriptModules'] = array_merge( + $resultArray['javaScriptModules'], + $this->javaScriptModules, + [JavaScriptModuleInstruction::create('@typo3/backend/form-engine/container/inline-control-container.js')] + ); + + $resultArray['html'] = $this->wrapWithFieldsetAndLegend($view->render('Form/FilesControlContainer')); + return $resultArray; + } + + /** + * Generate buttons to select, reference and upload files. + */ + protected function getFileSelectors(array $inlineStructure, array $inlineConfiguration, FileExtensionFilter $fileExtensionFilter): array + { + $languageService = $this->getLanguageService(); + $backendUser = $this->getBackendUserAuthentication(); + + $currentStructureDomObjectIdPrefix = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($inlineStructure, $this->data['inlineFirstPid']); + $objectPrefix = $currentStructureDomObjectIdPrefix . '-' . self::FILE_REFERENCE_TABLE; + + $controls = []; + if ($inlineConfiguration['appearance']['elementBrowserEnabled'] ?? true) { + if ($inlineConfiguration['appearance']['createNewRelationLinkTitle'] ?? false) { + $buttonText = $inlineConfiguration['appearance']['createNewRelationLinkTitle']; + } else { + $buttonText = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createNewRelation'; + } + $buttonText = $languageService->sL($buttonText); + $attributes = [ + 'type' => 'button', + 'class' => 'btn btn-default t3js-element-browser', + 'hidden' => !($inlineConfiguration['inline']['showCreateNewRelationButton'] ?? true) ? 'hidden' : null, + 'title' => $buttonText, + 'data-mode' => 'file', + 'data-allowed-types' => implode(',', $fileExtensionFilter->getAllowedFileExtensions() ?? []), + 'data-disallowed-types' => implode(',', $fileExtensionFilter->getDisallowedFileExtensions() ?? []), + 'data-irre-object-id' => $objectPrefix, + ]; + $controls[] = ' + '; + } + + $onlineMediaAllowed = []; + foreach ($this->onlineMediaHelperRegistry->getSupportedFileExtensions() as $supportedFileExtension) { + if ($fileExtensionFilter->isAllowed($supportedFileExtension)) { + $onlineMediaAllowed[] = $supportedFileExtension; + } + } + + $showUpload = (bool)($inlineConfiguration['appearance']['fileUploadAllowed'] ?? true); + $showByUrl = ($inlineConfiguration['appearance']['fileByUrlAllowed'] ?? true) && $onlineMediaAllowed !== []; + + if (($showUpload || $showByUrl) && $backendUser->getUserSettings()->isUploadFieldsInTopOfEBEnabled()) { + $folder = $this->defaultUploadFolderResolver->resolve( + $backendUser, + $this->data['tableName'] === 'pages' ? $this->data['vanillaUid'] : ($this->data['parentPageRow']['uid'] ?? 0), + $this->data['tableName'], + $this->data['fieldName'] + ); + if ( + $folder instanceof Folder + && $folder->getStorage()->checkUserActionPermission('add', 'File') + ) { + if ($showUpload) { + if ($inlineConfiguration['appearance']['uploadFilesLinkTitle'] ?? false) { + $buttonText = $inlineConfiguration['appearance']['uploadFilesLinkTitle']; + } else { + $buttonText = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_upload.select-and-submit'; + } + $buttonText = $languageService->sL($buttonText); + + $attributes = [ + 'type' => 'button', + 'class' => 'btn btn-default t3js-drag-uploader', + 'title' => $buttonText, + 'hidden' => !($inlineConfiguration['inline']['showCreateNewRelationButton'] ?? true) ? 'hidden' : null, + 'data-dropzone-target' => '#' . StringUtility::escapeCssSelector($currentStructureDomObjectIdPrefix), + 'data-insert-dropzone-before' => '1', + 'data-file-irre-object' => $objectPrefix, + 'data-file-allowed' => implode(',', $fileExtensionFilter->getAllowedFileExtensions() ?? []), + 'data-file-disallowed' => implode(',', $fileExtensionFilter->getDisallowedFileExtensions() ?? []), + 'data-target-folder' => $folder->getCombinedIdentifier(), + 'data-max-file-size' => (string)(GeneralUtility::getMaxUploadFileSize() * 1024), + ]; + $controls[] = ' + '; + + $this->javaScriptModules[] = JavaScriptModuleInstruction::create('@typo3/backend/drag-uploader.js'); + } + if ($showByUrl) { + if ($inlineConfiguration['appearance']['addMediaLinkTitle'] ?? false) { + $buttonText = $inlineConfiguration['appearance']['addMediaLinkTitle']; + } else { + $buttonText = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.button'; + } + $buttonText = $languageService->sL($buttonText); + $attributes = [ + 'type' => 'button', + 'class' => 'btn btn-default t3js-online-media-add-btn', + 'title' => $buttonText, + 'hidden' => !($inlineConfiguration['inline']['showOnlineMediaAddButtonStyle'] ?? true) ? 'hidden' : null, + 'data-target-folder' => $folder->getCombinedIdentifier(), + 'data-file-irre-object' => $objectPrefix, + 'data-online-media-allowed' => implode(',', $onlineMediaAllowed), + 'data-btn-submit' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.placeholder'), + 'data-placeholder' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.placeholder'), + 'data-online-media-allowed-help-text' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.allowEmbedSources'), + ]; + + // @todo Should be implemented as web component + $controls[] = ' + '; + + $this->javaScriptModules[] = JavaScriptModuleInstruction::create('@typo3/backend/online-media.js'); + } + } + } + + $event = $this->eventDispatcher->dispatch( + new CustomFileSelectorsEvent($controls, $this->javaScriptModules, $this->data['tableName'], $this->data['fieldName'], $this->data['databaseRow'], $inlineConfiguration, $fileExtensionFilter, $objectPrefix) + ); + $this->javaScriptModules = $event->getJavaScriptModules(); + return $event->getSelectors(); + } + + /** + * Extracts FlexForm parts of a form element name like + * data[table][uid][field][sDEF][lDEF][FlexForm][vDEF] + */ + protected function extractFlexFormParts(string $formElementName): ?array + { + $flexFormParts = null; + $matches = []; + if (preg_match('#^data(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) { + $flexFormParts = GeneralUtility::trimExplode( + '][', + trim($matches[1], '[]') + ); + } + return $flexFormParts; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Form/Container/FlexFormContainerContainer.php b/Classes/Form/Container/FlexFormContainerContainer.php new file mode 100644 index 0000000..ba26535 --- /dev/null +++ b/Classes/Form/Container/FlexFormContainerContainer.php @@ -0,0 +1,137 @@ +getLanguageService(); + + $table = $this->data['tableName']; + $row = $this->data['databaseRow']; + $fieldName = $this->data['fieldName']; + $flexFormFormPrefix = $this->data['flexFormFormPrefix']; + $flexFormDataStructureArray = $this->data['flexFormDataStructureArray']; + + $flexFormContainerIdentifier = $this->data['flexFormContainerIdentifier']; + $actionFieldName = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']' + . $flexFormFormPrefix + . '[' . $flexFormContainerIdentifier . ']' + . '[_ACTION]'; + + $moveAndDeleteContent = []; + $userHasAccessToDefaultLanguage = $this->getBackendUserAuthentication()->checkLanguageAccess(0); + if ($userHasAccessToDefaultLanguage) { + $moveAndDeleteContent[] = '' + . ''; + $moveAndDeleteContent[] = '' + . ''; + } + + $options = $this->data; + // Append container specific stuff to field prefix + $options['flexFormFormPrefix'] = $flexFormFormPrefix . '[' . $flexFormContainerIdentifier . '][' . $this->data['flexFormContainerName'] . '][el]'; + $options['flexFormDataStructureArray'] = $flexFormDataStructureArray['el']; + $options['renderType'] = 'flexFormElementContainer'; + $containerContentResult = $this->nodeFactory->create($options)->render(); + + $containerTitle = ''; + if (!empty(trim($flexFormDataStructureArray['title']))) { + $containerTitle = $languageService->sL(trim($flexFormDataStructureArray['title'])); + } + + $resultArray = $this->initializeResultArray(); + + $parentSectionContainer = sprintf('flexform-section-container-%s-%s-%s-%s', $this->data['flexFormSheetName'], $this->data['fieldName'], md5($this->data['flexFormFieldName']), md5($this->data['elementBaseName'])); + $flexFormDomContainerId = sprintf('%s-%s', $parentSectionContainer, $flexFormContainerIdentifier); + $containerAttributes = [ + 'class' => 'panel panel-default t3js-flex-section', + 'data-parent' => $parentSectionContainer, + 'data-flexform-container-id' => $flexFormContainerIdentifier, + ]; + + $panelHeaderAttributes = [ + 'class' => 'panel-heading', + ]; + + $toggleAttributes = [ + 'class' => 'panel-button collapsed', + 'type' => 'button', + 'data-bs-toggle' => 'collapse', + 'data-bs-target' => '#' . $flexFormDomContainerId, + 'aria-controls' => $flexFormDomContainerId, + 'aria-expanded' => 'false', + ]; + + $html = []; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = ''; + $html[] = '
'; + $html[] = '
'; + $html[] = implode(LF, $moveAndDeleteContent); + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = $containerContentResult['html']; + $html[] = '
'; + $html[] = ''; + $html[] = '
'; + + $resultArray['html'] = implode(LF, $html); + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $containerContentResult, false); + + return $resultArray; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Form/Container/FlexFormElementContainer.php b/Classes/Form/Container/FlexFormElementContainer.php new file mode 100644 index 0000000..daa9d56 --- /dev/null +++ b/Classes/Form/Container/FlexFormElementContainer.php @@ -0,0 +1,153 @@ +data['flexFormDataStructureArray']; + $flexFormRowData = $this->data['flexFormRowData']; + $flexFormFormPrefix = $this->data['flexFormFormPrefix']; + $parameterArray = $this->data['parameterArray']; + + $languageService = $this->getLanguageService(); + $resultArray = $this->initializeResultArray(); + + foreach ($flexFormDataStructureArray as $flexFormFieldName => $flexFormFieldArray) { + if ( + // No item array found at all + !is_array($flexFormFieldArray) + // Not a section or container and not a list of single items + || (!isset($flexFormFieldArray['type']) && !is_array($flexFormFieldArray['config'])) + ) { + continue; + } + + if (($flexFormFieldArray['type'] ?? null) === 'array') { + // Section + if (empty($flexFormFieldArray['section'])) { + $resultArray['html'] = LF . 'Section expected at ' . $flexFormFieldName . ' but not found'; + continue; + } + + $options = $this->data; + $options['flexFormDataStructureArray'] = $flexFormFieldArray; + $options['flexFormRowData'] = $flexFormRowData[$flexFormFieldName]['el'] ?? []; + $options['flexFormFieldName'] = $flexFormFieldName; + $options['renderType'] = 'flexFormSectionContainer'; + $sectionContainerResult = $this->nodeFactory->create($options)->render(); + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $sectionContainerResult); + } else { + // Set up options for single element + $fakeParameterArray = [ + 'fieldConf' => [ + 'label' => $languageService->sL(trim($flexFormFieldArray['label'] ?? '')), + 'config' => $flexFormFieldArray['config'] ?? [], + 'children' => $flexFormFieldArray['children'] ?? [], + // https://docs.typo3.org/m/typo3/reference-tca/main/en-us/Columns/Properties/OnChange.html + 'onChange' => $flexFormFieldArray['onChange'] ?? '', + ], + 'fieldChangeFunc' => $parameterArray['fieldChangeFunc'], + 'label' => $parameterArray['label'] ?? '', + ]; + + if (isset($flexFormFieldArray['description']) && !empty($flexFormFieldArray['description'])) { + $fakeParameterArray['fieldConf']['description'] = $flexFormFieldArray['description']; + } + + if ($fakeParameterArray['fieldConf']['onChange'] === 'reload') { + $confirmation = $this->getBackendUserAuthentication()->jsConfirmation(JsConfirmation::TYPE_CHANGE); + $fakeParameterArray['fieldChangeFunc']['alert'] = new ReloadOnFieldChange($confirmation); + } + + $originalFieldName = $parameterArray['itemFormElName']; + $fakeParameterArray['itemFormElName'] = $parameterArray['itemFormElName'] . $flexFormFormPrefix . '[' . $flexFormFieldName . '][vDEF]'; + if ($fakeParameterArray['itemFormElName'] !== $originalFieldName) { + // If calculated itemFormElName is different from originalFieldName + // change the originalFieldName in TBE_EDITOR_fieldChanged. This is + // especially relevant for wizards writing their content back to hidden fields + $onFieldChange = $fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] ?? null; + if ($onFieldChange instanceof UpdateValueOnFieldChange) { + $fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = $onFieldChange->withElementName($fakeParameterArray['itemFormElName']); + } + } + if (array_key_exists('vDEF', $flexFormRowData[$flexFormFieldName] ?? [])) { + $fakeParameterArray['itemFormElValue'] = $flexFormRowData[$flexFormFieldName]['vDEF']; + } else { + $fakeParameterArray['itemFormElValue'] = $fakeParameterArray['fieldConf']['config']['default'] ?? ''; + } + + $options = $this->data; + // Set either flexFormFieldName or flexFormContainerFieldName, depending on if we are a "regular" field or a flex container section field + if (empty($options['flexFormFieldName'])) { + $options['flexFormFieldName'] = $flexFormFieldName; + } else { + $options['flexFormContainerFieldName'] = $flexFormFieldName; + } + $options['parameterArray'] = $fakeParameterArray; + $options['elementBaseName'] = $this->data['elementBaseName'] . $flexFormFormPrefix . '[' . $flexFormFieldName . '][vDEF]'; + + if (!empty($flexFormFieldArray['config']['renderType'])) { + $options['renderType'] = $flexFormFieldArray['config']['renderType']; + } else { + // Fallback to type if no renderType is given + $options['renderType'] = $flexFormFieldArray['config']['type']; + } + $childResult = $this->nodeFactory->create($options)->render(); + + if (!empty($childResult['html'])) { + $html = []; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = $childResult['html']; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $resultArray['html'] .= implode(LF, $html); + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false); + } + } + } + + return $resultArray; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + +} diff --git a/Classes/Form/Container/FlexFormEntryContainer.php b/Classes/Form/Container/FlexFormEntryContainer.php new file mode 100644 index 0000000..9f55204 --- /dev/null +++ b/Classes/Form/Container/FlexFormEntryContainer.php @@ -0,0 +1,52 @@ +data['parameterArray']['fieldConf']['config']['dataStructureIdentifier']; + $flexFormDataStructureArray = $this->data['parameterArray']['fieldConf']['config']['ds']; + + $options = $this->data; + $options['flexFormDataStructureIdentifier'] = $flexFormDataStructureIdentifier; + $options['flexFormDataStructureArray'] = $flexFormDataStructureArray; + $options['flexFormRowData'] = $this->data['parameterArray']['itemFormElValue']; + $options['renderType'] = 'flexFormNoTabsContainer'; + + // Enable tabs if there is more than one sheet + if (count($flexFormDataStructureArray['sheets']) > 1) { + $options['renderType'] = 'flexFormTabsContainer'; + } + + $resultArray = $this->nodeFactory->create($options)->render(); + $resultArray['html'] = '
' . $resultArray['html'] . '
'; + $resultArray['html'] = $this->wrapWithFieldsetAndLegend($resultArray['html']); + return $resultArray; + } +} diff --git a/Classes/Form/Container/FlexFormNoTabsContainer.php b/Classes/Form/Container/FlexFormNoTabsContainer.php new file mode 100644 index 0000000..b3087b3 --- /dev/null +++ b/Classes/Form/Container/FlexFormNoTabsContainer.php @@ -0,0 +1,68 @@ +data['parameterArray']; + $flexFormDataStructureArray = $this->data['flexFormDataStructureArray']; + $flexFormRowData = $this->data['flexFormRowData']; + $resultArray = $this->initializeResultArray(); + + // Determine this single sheet name, most often it ends up with sDEF, except if only one sheet was defined + $flexFormSheetNames = array_keys($flexFormDataStructureArray['sheets']); + $sheetName = array_pop($flexFormSheetNames); + $flexFormRowDataSubPart = $flexFormRowData['data'][$sheetName]['lDEF'] ?? []; + + unset($flexFormDataStructureArray['meta']); + + if (!is_array($flexFormDataStructureArray['sheets'][$sheetName]['ROOT']['el'])) { + $resultArray['html'] = 'Data Structure ERROR: No [\'ROOT\'][\'el\'] element found in flex form definition.'; + return $resultArray; + } + + $options = $this->data; + $options['flexFormDataStructureArray'] = $flexFormDataStructureArray['sheets'][$sheetName]['ROOT']['el']; + $options['flexFormRowData'] = $flexFormRowDataSubPart; + $options['flexFormSheetName'] = $sheetName; + $options['flexFormFormPrefix'] = '[data][' . $sheetName . '][lDEF]'; + $options['parameterArray'] = $parameterArray; + + $resultArray = $this->initializeResultArray(); + + $fieldInformationResult = $this->renderFieldInformation(); + $resultArray['html'] = $fieldInformationResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); + + $options['renderType'] = 'flexFormElementContainer'; + $childResult = $this->nodeFactory->create($options)->render(); + return $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, true); + } +} diff --git a/Classes/Form/Container/FlexFormSectionContainer.php b/Classes/Form/Container/FlexFormSectionContainer.php new file mode 100644 index 0000000..2b65493 --- /dev/null +++ b/Classes/Form/Container/FlexFormSectionContainer.php @@ -0,0 +1,158 @@ +getLanguageService(); + + $flexFormDataStructureArray = $this->data['flexFormDataStructureArray']; + $flexFormRowData = $this->data['flexFormRowData']; + $flexFormFieldName = $this->data['flexFormFieldName']; + $flexFormSheetName = $this->data['flexFormSheetName']; + + $userHasAccessToDefaultLanguage = $this->getBackendUserAuthentication()->checkLanguageAccess(0); + + $resultArray = $this->initializeResultArray(); + + // Render each existing container + foreach ($flexFormDataStructureArray['children'] as $flexFormContainerIdentifier => $containerDataStructure) { + $existingContainerData = $flexFormRowData[$flexFormContainerIdentifier]; + $existingSectionContainerDataStructureType = key($existingContainerData); + $existingContainerData = $existingContainerData[$existingSectionContainerDataStructureType]; + $options = $this->data; + $options['flexFormRowData'] = $existingContainerData['el']; + $options['flexFormDataStructureArray'] = $containerDataStructure; + $options['flexFormFormPrefix'] = $this->data['flexFormFormPrefix'] . '[' . $flexFormFieldName . '][el]'; + $options['flexFormContainerName'] = $existingSectionContainerDataStructureType; + $options['flexFormContainerIdentifier'] = $flexFormContainerIdentifier; + $options['renderType'] = 'flexFormContainerContainer'; + $flexFormContainerContainerResult = $this->nodeFactory->create($options)->render(); + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $flexFormContainerContainerResult); + } + + $containerId = sprintf('flexform-section-container-%s-%s-%s-%s', $flexFormSheetName, $this->data['fieldName'], md5($flexFormFieldName), md5($this->data['elementBaseName'])); + $sectionContainerId = sprintf('flexform-section-%s-%s-%s-%s', $flexFormSheetName, $this->data['fieldName'], md5($flexFormFieldName), md5($this->data['elementBaseName'])); + $hashedSectionContainerId = 'section-' . md5($sectionContainerId); + + // "New container" handling: Creates buttons for each possible container with all relevant information for the ajax call. + $containerTemplatesHtml = []; + foreach ($flexFormDataStructureArray['el'] as $flexFormContainerName => $flexFormFieldDefinition) { + $containerTitle = ''; + if (!empty(trim($flexFormFieldDefinition['title']))) { + $containerTitle = $languageService->sL(trim($flexFormFieldDefinition['title'])); + } + $containerTemplateHtml = []; + $containerTemplateHtml[] = 'data['vanillaUid'] . '"'; + // no int cast for databaseRow uid, this can be "NEW1234..." + $containerTemplateHtml[] = 'data-databaserowuid="' . htmlspecialchars($this->data['databaseRow']['uid']) . '"'; + $containerTemplateHtml[] = 'data-command="' . htmlspecialchars($this->data['command']) . '"'; + $containerTemplateHtml[] = 'data-tablename="' . htmlspecialchars($this->data['tableName']) . '"'; + $containerTemplateHtml[] = 'data-fieldname="' . htmlspecialchars($this->data['fieldName']) . '"'; + $containerTemplateHtml[] = 'data-recordtypevalue="' . $this->data['recordTypeValue'] . '"'; + $containerTemplateHtml[] = 'data-flexformsheetname="' . htmlspecialchars($flexFormSheetName) . '"'; + $containerTemplateHtml[] = 'data-flexformfieldname="' . htmlspecialchars($flexFormFieldName) . '"'; + $containerTemplateHtml[] = 'data-flexformcontainername="' . htmlspecialchars($flexFormContainerName) . '"'; + $containerTemplateHtml[] = 'data-target="#' . htmlspecialchars($hashedSectionContainerId) . '"'; + $containerTemplateHtml[] = '>'; + $containerTemplateHtml[] = $this->iconFactory->getIcon('actions-document-new', IconSize::SMALL)->render(); + $containerTemplateHtml[] = htmlspecialchars(GeneralUtility::fixed_lgd_cs($containerTitle, 30)); + $containerTemplateHtml[] = ''; + $containerTemplatesHtml[] = implode(LF, $containerTemplateHtml); + } + // Create new elements links + $createElementsHtml = []; + if ($userHasAccessToDefaultLanguage) { + $createElementsHtml[] = '
'; + $createElementsHtml[] = '
'; + $createElementsHtml[] = implode('', $containerTemplatesHtml); + $createElementsHtml[] = '
'; + $createElementsHtml[] = '
'; + } + + $sectionTitle = ''; + if (!empty(trim($flexFormDataStructureArray['title'] ?? ''))) { + $sectionTitle = $languageService->sL(trim($flexFormDataStructureArray['title'])); + } + + // Wrap child stuff + $toggleAll = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleall')); + $html = []; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = ''; + $html[] = htmlspecialchars($sectionTitle); + $html[] = ''; + $html[] = '
'; + $html[] = ''; + $html[] = '
'; + $html[] = 'instance($containerId); + + return $resultArray; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Form/Container/FlexFormTabsContainer.php b/Classes/Form/Container/FlexFormTabsContainer.php new file mode 100644 index 0000000..09ab1b0 --- /dev/null +++ b/Classes/Form/Container/FlexFormTabsContainer.php @@ -0,0 +1,96 @@ +getLanguageService(); + + $parameterArray = $this->data['parameterArray']; + $flexFormDataStructureArray = $this->data['flexFormDataStructureArray']; + $flexFormRowData = $this->data['flexFormRowData']; + + $resultArray = $this->initializeResultArray(); + $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/tab.js'); + + $domIdPrefix = 'DTM-' . md5($this->data['parameterArray']['itemFormElName']); + $tabCounter = 0; + $tabElements = []; + foreach ($flexFormDataStructureArray['sheets'] as $sheetName => $sheetDataStructure) { + $flexFormRowSheetDataSubPart = $flexFormRowData['data'][$sheetName]['lDEF'] ?? []; + + if (!is_array($sheetDataStructure['ROOT']['el'])) { + $resultArray['html'] .= LF . 'No Data Structure ERROR: No [\'ROOT\'][\'el\'] found for sheet "' . $sheetName . '".'; + continue; + } + + $tabCounter++; + + $options = $this->data; + $options['flexFormDataStructureArray'] = $sheetDataStructure['ROOT']['el']; + $options['flexFormRowData'] = $flexFormRowSheetDataSubPart; + $options['flexFormSheetName'] = $sheetName; + $options['flexFormFormPrefix'] = '[data][' . $sheetName . '][lDEF]'; + $options['parameterArray'] = $parameterArray; + // Merge elements of this tab into a single list again and hand over to + // palette and single field container to render this group + $options['tabAndInlineStack'][] = [ + 'tab', + $domIdPrefix . '-' . $tabCounter, + ]; + $options['renderType'] = 'flexFormElementContainer'; + $childReturn = $this->nodeFactory->create($options)->render(); + + if ($childReturn['html'] !== '') { + $tabElements[] = [ + 'label' => !empty(trim($sheetDataStructure['ROOT']['sheetTitle'] ?? '')) ? $languageService->sL(trim($sheetDataStructure['ROOT']['sheetTitle'])) : $sheetName, + 'content' => $childReturn['html'], + 'description' => trim($sheetDataStructure['ROOT']['sheetDescription'] ?? '') ? $languageService->sL(trim($sheetDataStructure['ROOT']['sheetDescription'])) : '', + 'linkTitle' => trim($sheetDataStructure['ROOT']['sheetShortDescr'] ?? '') ? $languageService->sL(trim($sheetDataStructure['ROOT']['sheetShortDescr'])) : '', + ]; + } + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childReturn, false); + } + + $fieldInformationResult = $this->renderFieldInformation(); + $resultArray['html'] = $fieldInformationResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); + + $resultArray['html'] .= $this->renderTabMenu($tabElements, $domIdPrefix); + return $resultArray; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Form/Container/FormWrapContainer.php b/Classes/Form/Container/FormWrapContainer.php new file mode 100644 index 0000000..df02ae3 --- /dev/null +++ b/Classes/Form/Container/FormWrapContainer.php @@ -0,0 +1,71 @@ +data; + if (empty($this->data['fieldListToRender'])) { + $options['renderType'] = 'fullRecordContainer'; + } else { + $options['renderType'] = 'listOfFieldsContainer'; + } + $result = $this->nodeFactory->create($options)->render(); + + $childHtml = $result['html']; + + $view = $this->backendViewFactory->create($this->data['request']); + + $descriptionColumn = !empty($this->data['processedTca']['ctrl']['descriptionColumn']) + ? $this->data['processedTca']['ctrl']['descriptionColumn'] : null; + if ($descriptionColumn !== null && isset($this->data['databaseRow'][$descriptionColumn])) { + $view->assign('recordDescription', $this->data['databaseRow'][$descriptionColumn]); + } + $readOnlyRecord = !empty($this->data['processedTca']['ctrl']['readOnly']) + ? (bool)$this->data['processedTca']['ctrl']['readOnly'] : null; + if ($readOnlyRecord === true) { + $view->assign('recordReadonly', true); + } + $fieldInformationResult = $this->renderFieldInformation(); + $fieldInformationHtml = $fieldInformationResult['html']; + $result = $this->mergeChildReturnIntoExistingResult($result, $fieldInformationResult, false); + + $fieldWizardResult = $this->renderFieldWizard(); + $fieldWizardHtml = $fieldWizardResult['html']; + $result = $this->mergeChildReturnIntoExistingResult($result, $fieldWizardResult, false); + + $view->assignMultiple([ + 'fieldInformationHtml' => $fieldInformationHtml, + 'fieldWizardHtml' => $fieldWizardHtml, + 'childHtml' => $childHtml, + 'isNewRecord' => $this->data['command'] === 'new', + ]); + $result['html'] = $view->render('Form/FormWrapContainer'); + return $result; + } +} diff --git a/Classes/Form/Container/FullRecordContainer.php b/Classes/Form/Container/FullRecordContainer.php new file mode 100644 index 0000000..4dbea4f --- /dev/null +++ b/Classes/Form/Container/FullRecordContainer.php @@ -0,0 +1,89 @@ +data['recordTypeValue']; + + // List of items to be rendered + $itemList = $this->data['processedTca']['types'][$recordTypeValue]['showitem']; + + $fieldsArray = GeneralUtility::trimExplode(',', $itemList, true); + + if ($fieldsArray === []) { + throw new NoFieldsToRenderException('No fields defined for record type "' . $recordTypeValue . '" of table "' . $this->data['tableName'] . '"', 1730106227); + } + + // Streamline the fields array + // First, make sure there is always a --div-- definition for the first element + if (!str_starts_with($fieldsArray[0], '--div--')) { + array_unshift($fieldsArray, '--div--;core.form.tabs:general'); + } + // If first tab has no label definition, add "general" label + $firstTabHasLabel = count(GeneralUtility::trimExplode(';', $fieldsArray[0])) > 1; + if (!$firstTabHasLabel) { + $fieldsArray[0] = '--div--;core.form.tabs:general'; + } + // If there are at least two --div-- definitions, inner container will be a TabContainer, else a NoTabContainer + $tabCount = 0; + foreach ($fieldsArray as $field) { + if (str_starts_with($field, '--div--')) { + $tabCount++; + } + } + $hasTabs = true; + if ($tabCount < 2) { + // Remove first tab definition again if there is only one tab defined + array_shift($fieldsArray); + $hasTabs = false; + } + + $data = $this->data; + $data['fieldsArray'] = $fieldsArray; + if ($hasTabs) { + $data['renderType'] = 'tabsContainer'; + } else { + $data['renderType'] = 'noTabsContainer'; + } + + return $this->nodeFactory->create($data)->render(); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Form/Container/InlineControlContainer.php b/Classes/Form/Container/InlineControlContainer.php new file mode 100644 index 0000000..46acafa --- /dev/null +++ b/Classes/Form/Container/InlineControlContainer.php @@ -0,0 +1,553 @@ + + */ + protected $javaScriptModules = []; + + /** + * @var array Default wizards + */ + protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], + ]; + + public function __construct( + private readonly IconFactory $iconFactory, + private readonly InlineStackProcessor $inlineStackProcessor, + private readonly HashService $hashService, + ) {} + + /** + * Entry method + * + * @return array As defined in initializeResultArray() of AbstractNode + */ + public function render(): array + { + $languageService = $this->getLanguageService(); + + $this->inlineData = $this->data['inlineData']; + + $inlineStructure = $this->data['inlineStructure']; + + $table = $this->data['tableName']; + $row = $this->data['databaseRow']; + $field = $this->data['fieldName']; + $parameterArray = $this->data['parameterArray']; + + $resultArray = $this->initializeResultArray(); + + $config = $parameterArray['fieldConf']['config']; + $foreign_table = $config['foreign_table']; + $isReadOnly = isset($config['readOnly']) && $config['readOnly']; + $language = 0; + if ($this->data['tcaSchemata']->has($table) && $this->data['tcaSchemata']->get($table)->hasCapability(TcaSchemaCapability::Language)) { + $languageFieldName = $this->data['tcaSchemata']->get($table)->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName(); + $language = isset($row[$languageFieldName][0]) ? (int)$row[$languageFieldName][0] : (int)($row[$languageFieldName] ?? 0); + } + + // Add the current inline job to the structure stack + $newStructureItem = [ + 'table' => $table, + 'uid' => $row['uid'], + 'field' => $field, + 'config' => $config, + ]; + // Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF') + if (!empty($parameterArray['itemFormElName'])) { + $flexFormParts = $this->extractFlexFormParts($parameterArray['itemFormElName']); + if ($flexFormParts !== null) { + $newStructureItem['flexform'] = $flexFormParts; + } + } + $inlineStructure['stable'][] = $newStructureItem; + + // Transport the flexform DS identifier fields to the FormInlineAjaxController + if (!empty($newStructureItem['flexform']) + && isset($this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier']) + ) { + $config['dataStructureIdentifier'] = $this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier']; + } + + // Hand over original returnUrl to FormInlineAjaxController. Needed if opening for instance a + // nested element in a new view to then go back to the original returnUrl and not the url of + // the inline ajax controller + $config['originalReturnUrl'] = $this->data['returnUrl']; + + // e.g. data[
][][] + $nameForm = $this->inlineStackProcessor->getFormPrefixFromStructure($inlineStructure); + // e.g. data------- + $nameObject = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($inlineStructure, $this->data['inlineFirstPid']); + + $inlineChildren = $parameterArray['fieldConf']['children'] ?? []; + + $config['inline']['first'] = $config['inline']['last'] = false; + if (is_array($inlineChildren) && $inlineChildren !== []) { + $firstChild = array_first($inlineChildren); + if (isset($firstChild['databaseRow']['uid'])) { + $config['inline']['first'] = $firstChild['databaseRow']['uid']; + } + $lastChild = array_last($inlineChildren); + if (isset($lastChild['databaseRow']['uid'])) { + $config['inline']['last'] = $lastChild['databaseRow']['uid']; + } + } + + $top = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0); + + $this->inlineData['config'][$nameObject] = [ + 'table' => $foreign_table, + ]; + $configJson = (string)json_encode($config); + $this->inlineData['config'][$nameObject . '-' . $foreign_table] = [ + 'top' => [ + 'table' => $top['table'], + 'uid' => $top['uid'], + ], + 'context' => [ + 'config' => $configJson, + 'hmac' => $this->hashService->hmac($configJson, 'InlineContext'), + ], + ]; + $this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack']; + + $uniqueMax = 0; + $uniqueIds = []; + + if ($config['foreign_unique'] ?? false) { + // Add inlineData['unique'] with JS unique configuration + // @todo: Improve validation and throw an exception if type is neither select nor group here + $type = ($config['selectorOrUniqueConfiguration']['config']['type'] ?? '') === 'select' ? 'select' : 'groupdb'; + foreach ($inlineChildren as $child) { + // Determine used unique ids, skip not localized records + if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) { + $value = $child['databaseRow'][$config['foreign_unique']]; + // We're assuming there is only one connected value here for both select and group + if ($type === 'select') { + // A select field is an array of uids. See TcaSelectItems data provider for details. + // Pick first entry, ends up as eg. $value = 42. + $value = $value['0'] ?? []; + } else { + // A group field is an array of arrays containing uid + table + title + row. + // See TcaGroup data provider for details. + // Pick the first one (always on 0), and use uid + table only. Exclude title + row + // since the entire inlineData['unique'] array ends up in JavaScript in the end + // and we don't need and want the title and the entire row data in the frontend. + // Ends up as $value = [ 'uid' => '42', 'table' => 'tx_my_table' ] + $value = [ + 'uid' => $value[0]['uid'], + 'table' => $value[0]['table'], + ]; + } + // Note structure of $value is different in select vs. group: It's a uid for select, but an + // array with uid + table for group. + if (isset($child['databaseRow']['uid'])) { + $uniqueIds[$child['databaseRow']['uid']] = $value; + } + } + } + $possibleRecords = $config['selectorOrUniquePossibleRecords'] ?? []; + $possibleRecordsUidToTitle = []; + foreach ($possibleRecords as $possibleRecord) { + $possibleRecordsUidToTitle[$possibleRecord['value']] = $possibleRecord['label']; + } + $uniqueMax = ($config['appearance']['useCombination'] ?? false) || empty($possibleRecords) ? -1 : count($possibleRecords); + $this->inlineData['unique'][$nameObject . '-' . $foreign_table] = [ + 'max' => $uniqueMax, + 'used' => $uniqueIds, + 'type' => $type, + 'table' => $foreign_table, + 'elTable' => $config['selectorOrUniqueConfiguration']['foreignTable'] ?? '', + 'field' => $config['foreign_unique'] ?? '', + 'selector' => ($config['selectorOrUniqueConfiguration']['isSelector'] ?? false) ? $type : false, + 'possible' => $possibleRecordsUidToTitle, + ]; + } + + $resultArray['inlineData'] = $this->inlineData; + + // @todo: It might be a good idea to have something like "isLocalizedRecord" or similar set by a data provider + $uidOfDefaultRecord = 0; + if ($this->data['tcaSchemata']->has($table) && $this->data['tcaSchemata']->get($table)->hasCapability(TcaSchemaCapability::Language)) { + $originPointerField = $this->data['tcaSchemata']->get($table)->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName(); + $uidOfDefaultRecord = $row[$originPointerField] ?? 0; + } + $isLocalizedParent = $language > 0 + && ($uidOfDefaultRecord[0] ?? $uidOfDefaultRecord) > 0 + && MathUtility::canBeInterpretedAsInteger($row['uid']); + $numberOfFullLocalizedChildren = 0; + $numberOfNotYetLocalizedChildren = 0; + foreach ($inlineChildren as $child) { + if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) { + $numberOfFullLocalizedChildren++; + } + if ($isLocalizedParent && $child['isInlineDefaultLanguageRecordInLocalizedParentContext']) { + $numberOfNotYetLocalizedChildren++; + } + } + + // Render the localization buttons if needed + $localizationButtons = ''; + if ($numberOfNotYetLocalizedChildren) { + // Add the "Localize all records" button before all child records: + if (!empty($config['appearance']['showAllLocalizationLink'])) { + $localizationButtons = ' ' . $this->getLevelInteractionButton('localize', $config); + } + // Add the "Synchronize with default language" button before all child records: + if (!empty($config['appearance']['showSynchronizationLink'])) { + $localizationButtons .= ' ' . $this->getLevelInteractionButton('synchronize', $config); + } + } + + // Hide the "Create new record" button if there are more than maxitems or the field is read-only + if ($isReadOnly || $numberOfFullLocalizedChildren >= ($config['maxitems'] ?? 0) || ($uniqueMax > 0 && $numberOfFullLocalizedChildren >= $uniqueMax)) { + $config['inline']['hideNewButton'] = true; + } + + // Render the "new record" level button: + $newRecordButton = ''; + // For b/w compatibility, "showNewRecordLink" - in contrast to the other show* options - defaults to TRUE + if (!isset($config['appearance']['showNewRecordLink']) || $config['appearance']['showNewRecordLink']) { + $newRecordButton = $this->getLevelInteractionButton('newRecord', $config); + } + + $fieldInformationResult = $this->renderFieldInformation(); + $html = $fieldInformationResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); + + // Wrap all inline fields of a record with a custom element (container) + $formGroupAttributes = [ + 'id' => $nameObject, + 'data-type' => 'record', + 'data-object-group' => $nameObject . '-' . $foreign_table, + 'data-form-field' => $nameForm, + 'data-expand-single' => (bool)($config['appearance']['expandSingle'] ?? false) ? 'true' : 'false', + 'data-sortable' => (bool)($config['appearance']['useSortable'] ?? false) ? 'true' : 'false', + 'data-min' => (int)($config['minitems'] ?? 0), + 'data-max' => (int)($config['maxitems'] ?? 0), + ]; + $html .= ''; + + // Add the level buttons before all child records: + if (in_array($config['appearance']['levelLinksPosition'], ['both', 'top'], true)) { + $html .= '
' . $newRecordButton . $localizationButtons . '
'; + } + + // If it's required to select from possible child records (reusable children), add a selector box + if (!$isReadOnly && ($config['foreign_selector'] ?? false) && ($config['appearance']['showPossibleRecordsSelector'] ?? true) !== false) { + if (($config['selectorOrUniqueConfiguration']['config']['type'] ?? false) === 'select') { + $selectorBox = $this->renderPossibleRecordsSelectorTypeSelect($inlineStructure, $config, $uniqueIds); + } else { + $selectorBox = $this->renderPossibleRecordsSelectorTypeGroupDB($inlineStructure, $config); + } + $html .= $selectorBox . $localizationButtons; + } + + $title = $languageService->sL(trim($parameterArray['fieldConf']['label'] ?? '')); + $html .= '
'; + + $sortableRecordUids = []; + foreach ($inlineChildren as $options) { + $options['inlineParentUid'] = $row['uid']; + $options['inlineFirstPid'] = $this->data['inlineFirstPid']; + // @todo: this can be removed if this container no longer sets additional info to $config + $options['inlineParentConfig'] = $config; + $options['inlineData'] = $this->inlineData; + $options['inlineStructure'] = $inlineStructure; + $options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray']; + $options['renderType'] = 'inlineRecordContainer'; + $childResult = $this->nodeFactory->create($options)->render(); + $html .= $childResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false); + if (!$options['isInlineDefaultLanguageRecordInLocalizedParentContext'] && isset($options['databaseRow']['uid'])) { + // Don't add record to list of "valid" uids if it is only the default + // language record of a not yet localized child + $sortableRecordUids[] = $options['databaseRow']['uid']; + } + } + + $html .= '
'; + + $fieldWizardResult = $this->renderFieldWizard(); + $fieldWizardHtml = $fieldWizardResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false); + $html .= $fieldWizardHtml; + + // Add the level buttons after all child records: + if (in_array($config['appearance']['levelLinksPosition'], ['both', 'bottom'], true)) { + $html .= '
' . $newRecordButton . $localizationButtons . '
'; + } + if (is_array($config['customControls'] ?? false)) { + $html .= '
'; + foreach ($config['customControls'] as $customControlConfig) { + if (!isset($customControlConfig['userFunc'])) { + throw new \RuntimeException('Support for customControl without a userFunc key in TCA type inline is not supported.', 1548052629); + } + $parameters = [ + 'table' => $table, + 'field' => $field, + 'row' => $row, + 'nameObject' => $nameObject, + 'nameForm' => $nameForm, + 'config' => $config, + 'customControlConfig' => $customControlConfig, + // Warning: By reference should be used with care here and exists mostly to allow additional $resultArray['javaScriptModules'] + 'resultArray' => &$resultArray, + ]; + $html .= GeneralUtility::callUserFunction($customControlConfig['userFunc'], $parameters, $this); + } + $html .= '
'; + } + $resultArray['javaScriptModules'] = array_merge($resultArray['javaScriptModules'], $this->javaScriptModules); + $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create( + '@typo3/backend/form-engine/container/inline-control-container.js' + ); + + // Publish the uids of the child records in the given order to the browser + $html .= ''; + // Close the wrap for all inline fields (container) + $html .= '
'; + + $resultArray['html'] = $this->wrapWithFieldsetAndLegend($html); + return $resultArray; + } + + /** + * Creates the HTML code of a general button to be used on a level of inline children. + * The possible keys for the parameter $type are 'newRecord', 'localize' and 'synchronize'. + * + * @param string $type The button type, values are 'newRecord', 'localize' and 'synchronize'. + * @param array $conf TCA configuration of the parent(!) field + * @return string The HTML code of the new button, wrapped in a div + */ + protected function getLevelInteractionButton(string $type, array $conf = []): string + { + $languageService = $this->getLanguageService(); + $attributes = []; + switch ($type) { + case 'newRecord': + $title = htmlspecialchars($languageService->sL('core.core:cm.createnew')); + $icon = 'actions-plus'; + $attributes['class'] = 'btn btn-default t3js-create-new-button'; + $attributes['data-type'] = 'newRecord'; + if (!empty($conf['inline']['hideNewButton'])) { + $attributes['hidden'] = 'hidden'; + } + if (!empty($conf['appearance']['newRecordLinkAddTitle'])) { + $title = htmlspecialchars(sprintf( + $languageService->sL('core.core:cm.createnew.link'), + $languageService->sL($this->data['tcaSchemata']->get($conf['foreign_table'])->getTitle()), + )); + } elseif (isset($conf['appearance']['newRecordLinkTitle']) && $conf['appearance']['newRecordLinkTitle'] !== '') { + $title = htmlspecialchars($languageService->sL($conf['appearance']['newRecordLinkTitle'])); + } + break; + case 'localize': + $title = htmlspecialchars($languageService->sL('core.misc:localizeAllRecords')); + $icon = 'actions-document-localize'; + $attributes['class'] = 'btn btn-default t3js-synchronizelocalize-button'; + $attributes['data-type'] = 'localize'; + break; + case 'synchronize': + $title = htmlspecialchars($languageService->sL('core.misc:synchronizeWithOriginalLanguage')); + $icon = 'actions-document-synchronize'; + $attributes['class'] = 'btn btn-default t3js-synchronizelocalize-button'; + $attributes['data-type'] = 'synchronize'; + break; + default: + $title = ''; + $icon = ''; + } + // Create the button: + $icon = $icon ? $this->iconFactory->getIcon($icon, IconSize::SMALL)->render() : ''; + $attributes['title'] = $title; + return ' + + '; + } + + /** + * Generate a button that opens an element browser in a new window. + * For group/db there is no way to use a "selector" like a -box. + * + * @param array $inlineConfiguration TCA inline configuration of the parent(!) field + * @return string A HTML button that opens an element browser in a new window + */ + protected function renderPossibleRecordsSelectorTypeGroupDB(array $inlineStructure, array $inlineConfiguration): string + { + $languageService = $this->getLanguageService(); + $groupFieldConfiguration = $inlineConfiguration['selectorOrUniqueConfiguration']['config']; + $objectPrefix = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($inlineStructure, $this->data['inlineFirstPid']) . '-' . $inlineConfiguration['foreign_table']; + $elementBrowserEnabled = (bool)($inlineConfiguration['appearance']['elementBrowserEnabled'] ?? true); + // Remove any white-spaces from the allowed extension lists + $allowed = GeneralUtility::trimExplode(',', (string)($groupFieldConfiguration['allowed'] ?? ''), true); + $item = ''; + if ($elementBrowserEnabled) { + if (!empty($inlineConfiguration['appearance']['createNewRelationLinkTitle'])) { + $createNewRelationText = htmlspecialchars($languageService->sL($inlineConfiguration['appearance']['createNewRelationLinkTitle'])); + } else { + $createNewRelationText = htmlspecialchars($languageService->sL('core.core:cm.createNewRelation')); + } + $item .= ' + '; + } + $item = '
' . $item . '
'; + if (!empty($allowed)) { + $item .= ' +
+ ' . htmlspecialchars($languageService->sL('core.core:cm.allowedRelations')) . ' +
    + ' . implode(' ', array_map(static fn(string $item): string => '
  • ' . strtoupper($item) . '
  • ', $allowed)) . ' +
+
'; + } + return '
' . $item . '
'; + } + + /** + * Get a selector as used for the select type, to select from all available + * records and to create a relation to the embedding record (e.g. like MM). + * + * @param array $config TCA inline configuration of the parent(!) field + * @param array $uniqueIds The uids that have already been used and should be unique + * @return string A HTML + ' . implode('', $opt) . ' + '; + + if ($size <= 1) { + // Add a "Create new relation" button for adding new relations + // This is necessary, if the size of the selector is "1" or if + // there is only one record item in the select-box, that is selected by default + // The selector-box creates a new relation on using an onChange event (see some line above) + if (!empty($config['appearance']['createNewRelationLinkTitle'])) { + $createNewRelationText = htmlspecialchars($this->getLanguageService()->sL($config['appearance']['createNewRelationLinkTitle'])); + } else { + $createNewRelationText = htmlspecialchars($this->getLanguageService()->sL('core.core:cm.createNewRelation')); + } + $item .= ' + '; + } + + // Wrap the selector and add a spacer to the bottom + $item = '
' . $item . '
'; + return $item; + } + + /** + * Extracts FlexForm parts of a form element name like + * data[table][uid][field][sDEF][lDEF][FlexForm][vDEF] + * Helper method used in inline + * + * @param string $formElementName The form element name + * @return array|null + */ + protected function extractFlexFormParts($formElementName) + { + $flexFormParts = null; + $matches = []; + if (preg_match('#^data(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) { + $flexFormParts = GeneralUtility::trimExplode( + '][', + trim($matches[1], '[]') + ); + } + return $flexFormParts; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Form/Container/InlineRecordContainer.php b/Classes/Form/Container/InlineRecordContainer.php new file mode 100644 index 0000000..5691495 --- /dev/null +++ b/Classes/Form/Container/InlineRecordContainer.php @@ -0,0 +1,523 @@ +data; + $this->inlineData = $data['inlineData']; + + $record = $data['databaseRow']; + $inlineConfig = $data['inlineParentConfig']; + $foreignTable = $inlineConfig['foreign_table']; + + $resultArray = $this->initializeResultArray(); + + // Send a mapping information to the browser via JSON: + // e.g. data[][][] => data------- + $formPrefix = $this->inlineStackProcessor->getFormPrefixFromStructure($data['inlineStructure']); + $domObjectId = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($data['inlineStructure'], $data['inlineFirstPid']); + $this->inlineData['map'][$formPrefix] = $domObjectId; + + $resultArray['inlineData'] = $this->inlineData; + + // Get the current naming scheme for DOM name/id attributes: + $appendFormFieldNames = '[' . $foreignTable . '][' . ($record['uid'] ?? 0) . ']'; + $objectId = $domObjectId . '-' . $foreignTable . '-' . ($record['uid'] ?? 0); + $classes = []; + $html = ''; + $combinationHtml = ''; + $isNewRecord = $data['command'] === 'new'; + $hiddenField = ''; + if (isset($data['processedTca']['ctrl']['enablecolumns']['disabled'])) { + $hiddenField = $data['processedTca']['ctrl']['enablecolumns']['disabled']; + } + if (!$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) { + if ($isNewRecord || $data['isInlineChildExpanded']) { + // Render full content ONLY IF this is an AJAX request, a new record, or the record is not collapsed + if (isset($data['combinationChild'])) { + $combinationChild = $this->renderCombinationChild($data, $appendFormFieldNames); + $combinationHtml = $combinationChild['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $combinationChild, false); + } + $childArray = $this->renderChild($data); + $html = $childArray['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray, false); + } else { + // This class is the marker for the JS-function to check if the full content has already been loaded + $classes[] = 't3js-not-loaded'; + } + if ($isNewRecord) { + // Add pid of record as hidden field + $html .= ''; + // Tell DataHandler this record is expanded + $ucFieldName = 'uc[inlineView]' + . '[' . $data['inlineTopMostParentTableName'] . ']' + . '[' . $data['inlineTopMostParentUid'] . ']' + . $appendFormFieldNames; + $html .= ''; + } else { + // Set additional field for processing for saving + $html .= ''; + if (!empty($hiddenField) && (!$data['isInlineChildExpanded'] || !in_array($hiddenField, $data['columnsToProcess'], true))) { + $checked = !empty($record[$hiddenField]) ? ' checked="checked"' : ''; + $html .= ''; + $html .= ''; + } + } + } + if ($inlineConfig['renderFieldsOnly'] ?? false) { + // Render "body" part only + $html .= $combinationHtml; + } else { + // Render header row and content (if expanded) + if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) { + $classes[] = 'panel-placeholder'; + } + if (!empty($hiddenField) && isset($record[$hiddenField]) && (int)$record[$hiddenField]) { + $classes[] = 'panel-hidden'; + } + if ($isNewRecord) { + $classes[] = 'inlineIsNewRecord'; + } + + $originalUniqueValue = ''; + if (isset($record['uid'], $data['inlineData']['unique'][$domObjectId . '-' . $foreignTable]['used'][$record['uid']])) { + $uniqueValueValues = $data['inlineData']['unique'][$domObjectId . '-' . $foreignTable]['used'][$record['uid']]; + // in case of site_language we don't have the full form engine options, so fallbacks need to be taken into account + $originalUniqueValue = ($uniqueValueValues['table'] ?? $foreignTable) . '_'; + // @todo In what circumstance would $uniqueValueValues be an array that lacks a 'uid' key? Unclear, but + // it breaks the string concatenation. This is a hacky workaround for type safety only. + $uVV = ($uniqueValueValues['uid'] ?? $uniqueValueValues); + if (is_array($uVV)) { + $uVV = implode(',', $uVV); + } + $originalUniqueValue .= $uVV; + } + + // The hashed object id needs a non-numeric prefix, the value is used as ID selector in JavaScript + $hashedObjectId = 'hash-' . md5($objectId); + $containerAttributes = [ + 'id' => $objectId . '_div', + 'class' => 'form-irre-object panel panel-default ' . trim(implode(' ', $classes)), + 'data-object-uid' => $record['uid'] ?? 0, + 'data-object-id' => $objectId, + 'data-object-id-hash' => $hashedObjectId, + 'data-object-parent-group' => $domObjectId . '-' . $foreignTable, + 'data-field-name' => $appendFormFieldNames, + 'data-topmost-parent-table' => $data['inlineTopMostParentTableName'], + 'data-topmost-parent-uid' => $data['inlineTopMostParentUid'], + 'data-table-unique-original-value' => $originalUniqueValue, + 'data-placeholder-record' => $data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ? '1' : '0', + ]; + + $isExpanded = $data['isInlineChildExpanded'] ?? false; + $ariaControls = htmlspecialchars($objectId . '_fields', ENT_QUOTES | ENT_HTML5); + $html = ' +
+
+
+ ' . $this->renderForeignRecordHeader($data, $isExpanded, $ariaControls) . ' +
+
+
' . $html . $combinationHtml . '
+
'; + } + + $resultArray['html'] = $html; + return $resultArray; + } + + /** + * Render inner child + * + * @return array Result array + */ + protected function renderChild(array $data) + { + $domObjectId = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $data['inlineFirstPid']); + $data['tabAndInlineStack'][] = [ + 'inline', + $domObjectId . '-' . $data['tableName'] . '-' . $data['databaseRow']['uid'], + ]; + // @todo: ugly construct ... + $data['inlineData'] = $this->inlineData; + $data['renderType'] = 'fullRecordContainer'; + return $this->nodeFactory->create($data)->render(); + } + + /** + * Render child child + * + * Render a table with FormEngine, that occurs on an intermediate table but should be editable directly, + * so two tables are combined (the intermediate table with attributes and the sub-embedded table). + * -> This is a direct embedding over two levels! + * + * @param array $data + * @param string $appendFormFieldNames The [
][] of the parent record (the intermediate table) + * @return array Result array + */ + protected function renderCombinationChild(array $data, $appendFormFieldNames) + { + $childData = $data['combinationChild']; + $parentConfig = $data['inlineParentConfig']; + + // If field is set to readOnly, set all fields of the relation to readOnly as well + if (isset($parentConfig['readOnly']) && $parentConfig['readOnly']) { + foreach ($childData['processedTca']['columns'] as $columnName => $columnConfiguration) { + $childData['processedTca']['columns'][$columnName]['config']['readOnly'] = true; + } + } + + $resultArray = $this->initializeResultArray(); + + // Display Warning FlashMessage if it is not suppressed + if (!isset($parentConfig['appearance']['suppressCombinationWarning']) || empty($parentConfig['appearance']['suppressCombinationWarning'])) { + $combinationWarningMessage = 'core.core:warning.inline_use_combination'; + if (!empty($parentConfig['appearance']['overwriteCombinationWarningMessage'])) { + $combinationWarningMessage = $parentConfig['appearance']['overwriteCombinationWarningMessage']; + } + $message = $this->getLanguageService()->sL($combinationWarningMessage); + $markup = []; + // @TODO: This is not a FlashMessage! The markup must be changed and special CSS + // @TODO: should be created, in order to prevent confusion. + $markup[] = '
'; + $markup[] = '
'; + $markup[] = '
'; + $markup[] = ' '; + $markup[] = ' ' . $this->iconFactory->getIcon('actions-exclamation', IconSize::SMALL)->render(); + $markup[] = ' '; + $markup[] = '
'; + $markup[] = '
'; + $markup[] = '
' . htmlspecialchars($message) . '
'; + $markup[] = '
'; + $markup[] = '
'; + $markup[] = '
'; + $resultArray['html'] = implode(LF, $markup); + } + + $childArray = $this->renderChild($childData); + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray); + + // If this is a new record, add a pid value to store this record and the pointer value for the intermediate table + if ($childData['command'] === 'new') { + $comboFormFieldName = 'data[' . $childData['tableName'] . '][' . $childData['databaseRow']['uid'] . '][pid]'; + $resultArray['html'] .= ''; + } + // If the foreign_selector field is also responsible for uniqueness, tell the browser the uid of the "other" side of the relation + if ($childData['command'] === 'new' || $parentConfig['foreign_unique'] === $parentConfig['foreign_selector']) { + $parentFormFieldName = 'data' . $appendFormFieldNames . '[' . $parentConfig['foreign_selector'] . ']'; + $resultArray['html'] .= ''; + } + + return $resultArray; + } + + /** + * Renders the HTML header for a foreign record, such as the title, toggle-function, drag'n'drop, etc. + * Later on the command-icons are inserted here. + * + * @param array $data Current data + * @param bool $isExpanded Whether the record is currently expanded + * @param string $ariaControls The ID of the collapse target element + * @return string The HTML code of the header + */ + protected function renderForeignRecordHeader(array $data, bool $isExpanded, string $ariaControls): string + { + $record = $data['databaseRow']; + $recordTitle = $data['recordTitle']; + $foreignTable = $data['inlineParentConfig']['foreign_table']; + $domObjectId = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $data['inlineFirstPid']); + + if (!empty($recordTitle)) { + // The user function may return HTML, therefore we can't escape it + if (empty($data['processedTca']['ctrl']['formattedLabel_userFunc'])) { + $recordTitle = htmlspecialchars($recordTitle); + } + } else { + $recordTitle = '[' . htmlspecialchars($this->getLanguageService()->sL('core.core:labels.no_title')) . ']'; + } + + // In case the record title is not generated by a formattedLabel_userFunc, which already + // contains custom markup, and we are in debug mode, add the inline record table name. + if (empty($data['processedTca']['ctrl']['formattedLabel_userFunc']) + && $this->getBackendUserAuthentication()->shallDisplayDebugInformation() + ) { + $recordTitle .= ' [' . htmlspecialchars($foreignTable) . ']'; + } + + $objectId = htmlspecialchars($domObjectId . '-' . $foreignTable . '-' . ($record['uid'] ?? 0)); + return ' + +
+ ' . $this->renderForeignRecordHeaderControl($data) . ' +
'; + } + + /** + * Render the control-icons for a record header (create new, sorting, delete, disable/enable). + * Most of the parts are copy&paste from TYPO3\CMS\Backend\RecordList\DatabaseRecordList and + * modified for the JavaScript calls here + * + * @param array $data Current data + * @return string The HTML code with the control-icons + */ + protected function renderForeignRecordHeaderControl(array $data) + { + $rec = $data['databaseRow']; + $rec += [ + 'uid' => 0, + ]; + $inlineConfig = $data['inlineParentConfig']; + $foreignTable = $inlineConfig['foreign_table']; + $languageService = $this->getLanguageService(); + $backendUser = $this->getBackendUserAuthentication(); + // Initialize: + $cells = [ + 'hide' => '', + 'delete' => '', + 'info' => '', + 'new' => '', + 'sort.up' => '', + 'sort.down' => '', + 'dragdrop' => '', + 'localize' => '', + 'locked' => '', + ]; + $isNewItem = str_starts_with($rec['uid'], 'NEW'); + $isParentReadOnly = isset($inlineConfig['readOnly']) && $inlineConfig['readOnly']; + $isParentExisting = MathUtility::canBeInterpretedAsInteger($data['inlineParentUid']); + $tableSchema = $this->data['tcaSchemata']->get($foreignTable); + $isPagesTable = $foreignTable === 'pages'; + $enableManualSorting = ($tableSchema->hasCapability(TcaSchemaCapability::SortByField)) + || ($inlineConfig['MM'] ?? false) + || (!($data['isOnSymmetricSide'] ?? false) && ($inlineConfig['foreign_sortby'] ?? false)) + || (($data['isOnSymmetricSide'] ?? false) && ($inlineConfig['symmetric_sortby'] ?? false)); + $calcPerms = new Permission($backendUser->calcPerms(BackendUtility::readPageAccess((int)($data['parentPageRow']['uid'] ?? 0), $backendUser->getPagePermsClause(Permission::PAGE_SHOW)))); + // If the listed table is 'pages' we have to request the permission settings for each page: + $localCalcPerms = new Permission(Permission::NOTHING); + if ($isPagesTable) { + $localCalcPerms = new Permission($backendUser->calcPerms(BackendUtility::getRecord('pages', $rec['uid']))); + } + // This expresses the edit permissions for this particular element: + $permsEdit = ($isPagesTable && $localCalcPerms->editPagePermissionIsGranted()) || (!$isPagesTable && $calcPerms->editContentPermissionIsGranted()); + // The event contains all controls and their state (enabled / disabled), which might got modified by listeners + $event = $this->eventDispatcher->dispatch(new ModifyInlineElementEnabledControlsEvent($data, $rec)); + if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) { + $cells['localize'] = $this->iconFactory + ->getIcon('actions-edit-localize-status-low', IconSize::SMALL) + ->setTitle($languageService->sL('core.misc:localize.isLocalizable')) + ->render(); + } + // "Info": (All records) + if ($event->isControlEnabled('info')) { + if ($isNewItem) { + $cells['info'] = '' . $this->iconFactory->getIcon('empty-empty', IconSize::SMALL)->render() . ''; + } else { + $cells['info'] = ' + '; + } + } + // If the table is NOT a read-only table, then show these links: + if (!$isParentReadOnly && !($tableSchema->hasCapability(TcaSchemaCapability::AccessReadOnly)) && !($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false)) { + // "New record after" link (ONLY if the records in the table are sorted by a "sortby"-row or if default values can depend on previous record): + if ($event->isControlEnabled('new') && ($enableManualSorting || (($tableSchema->getRawConfiguration()['useColumnsForDefaultValues'] ?? false)))) { + if ((!$isPagesTable && $calcPerms->editContentPermissionIsGranted()) || ($isPagesTable && $calcPerms->createPagePermissionIsGranted())) { + $cells['new'] = ' + '; + } + } + // "Up/Down" links + if ($event->isControlEnabled('sort') && $permsEdit && $enableManualSorting) { + // Up + $icon = 'actions-move-up'; + $class = ''; + if ($inlineConfig['inline']['first'] == $rec['uid']) { + $class = ' disabled'; + $icon = 'empty-empty'; + } + $cells['sort.up'] = ' + '; + // Down + $icon = 'actions-move-down'; + $class = ''; + if ($inlineConfig['inline']['last'] == $rec['uid']) { + $class = ' disabled'; + $icon = 'empty-empty'; + } + + $cells['sort.down'] = ' + '; + } + // "Delete" link: + if ($event->isControlEnabled('delete') + && ( + ($isPagesTable && $localCalcPerms->deletePagePermissionIsGranted()) + || (!$isPagesTable && $calcPerms->editContentPermissionIsGranted()) + ) + ) { + $title = htmlspecialchars($languageService->sL('core.mod_web_list:delete')); + $icon = $this->iconFactory->getIcon('actions-edit-delete', IconSize::SMALL)->render(); + + $recordInfo = $data['databaseRow']['uid_local'][0]['title'] ?? $data['recordTitle'] ?? ''; + if ($this->getBackendUserAuthentication()->shallDisplayDebugInformation()) { + $recordInfo .= ' [' . $data['tableName'] . ':' . $data['vanillaUid'] . ']'; + } + + $cells['delete'] = ' + '; + } + + // "Hide/Unhide" links: + $hiddenField = $tableSchema->hasCapability(TcaSchemaCapability::RestrictionDisabledField) ? $tableSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName() : ''; + if ($event->isControlEnabled('hide') + && $permsEdit + && $hiddenField + && ($tableSchema->hasField($hiddenField) ?? false) + && (!($tableSchema->getField($hiddenField)->getConfiguration()['exclude'] ?? false) || $backendUser->check('non_exclude_fields', $foreignTable . ':' . $hiddenField)) + ) { + if ($rec[$hiddenField]) { + $title = htmlspecialchars($languageService->sL('core.mod_web_list:unHide' . ($isPagesTable ? 'Page' : ''))); + $cells['hide'] = ' + '; + } else { + $title = htmlspecialchars($languageService->sL('core.mod_web_list:hide' . ($isPagesTable ? 'Page' : ''))); + $cells['hide'] = ' + '; + } + } + // Drag&Drop Sorting: Sortable handle + if ($event->isControlEnabled('dragdrop') && $permsEdit && $enableManualSorting && ($inlineConfig['appearance']['useSortable'] ?? false)) { + $cells['dragdrop'] = ' + + ' . $this->iconFactory->getIcon('actions-move-move', IconSize::SMALL)->render() . ' + '; + } + } elseif (($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false) && $isParentExisting) { + if ($event->isControlEnabled('localize') && $data['isInlineDefaultLanguageRecordInLocalizedParentContext']) { + $cells['localize'] = ' + '; + } + } + // If the record is edit-locked by another user, we will show a little warning sign: + if ($lockInfo = BackendUtility::isRecordLocked($foreignTable, $rec['uid'])) { + $cells['locked'] = ' + '; + } + + // Get modified controls. This means their markup was modified, new controls were added or controls got removed. + $cells = $this->eventDispatcher->dispatch(new ModifyInlineElementControlsEvent($cells, $data, $rec))->getControls(); + + $out = ''; + if (!empty($cells['hide']) || !empty($cells['delete'])) { + $out .= '
' . $cells['hide'] . $cells['delete'] . '
'; + unset($cells['hide'], $cells['delete']); + } + if (!empty($cells['info']) || !empty($cells['new']) || !empty($cells['sort.up']) || !empty($cells['sort.down']) || !empty($cells['dragdrop'])) { + $out .= '
' . $cells['info'] . $cells['new'] . $cells['sort.up'] . $cells['sort.down'] . $cells['dragdrop'] . '
'; + unset($cells['info'], $cells['new'], $cells['sort.up'], $cells['sort.down'], $cells['dragdrop']); + } + if (!empty($cells['localize'])) { + $out .= '
' . $cells['localize'] . '
'; + unset($cells['localize']); + } + if (!empty($cells)) { + $cellContent = trim(implode('', $cells)); + $out .= $cellContent !== '' ? '
' . $cellContent . '
' : ''; + } + return $out; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Form/Container/ListOfFieldsContainer.php b/Classes/Form/Container/ListOfFieldsContainer.php new file mode 100644 index 0000000..afa2165 --- /dev/null +++ b/Classes/Form/Container/ListOfFieldsContainer.php @@ -0,0 +1,100 @@ +data; + $options['fieldsArray'] = $this->sanitizeFieldList($this->data['fieldListToRender']); + + if ($this->data['hiddenFieldListToRender'] ?? false) { + $hiddenFieldList = array_diff( + $this->sanitizeFieldList($this->data['hiddenFieldListToRender']), + $options['fieldsArray'] + ); + if ($hiddenFieldList !== []) { + $hiddenFieldList = implode(',', $hiddenFieldList); + $hiddenPaletteName = 'hiddenFieldsPalette' . md5($hiddenFieldList); + $options['processedTca']['palettes'][$hiddenPaletteName] = [ + 'isHiddenPalette' => true, + 'showitem' => $hiddenFieldList, + ]; + $options['fieldsArray'][] = '--palette--;;' . $hiddenPaletteName; + } + } + + $options['renderType'] = 'paletteAndSingleContainer'; + return $this->nodeFactory->create($options)->render(); + } + + protected function sanitizeFieldList(string $fieldList): array + { + $fields = array_unique(GeneralUtility::trimExplode(',', $fieldList, true)); + $fieldsByShowitem = $this->data['processedTca']['types'][$this->data['recordTypeValue']]['showitem']; + $fieldsByShowitem = GeneralUtility::trimExplode(',', $fieldsByShowitem, true); + + $allowedFields = []; + foreach ($fields as $fieldName) { + foreach ($fieldsByShowitem as $fieldByShowitem) { + $fieldByShowitemArray = $this->explodeSingleFieldShowItemConfiguration($fieldByShowitem); + if ($fieldByShowitemArray['fieldName'] === $fieldName) { + $allowedFields[] = implode(';', $fieldByShowitemArray); + break; + } + if ($fieldByShowitemArray['fieldName'] === '--palette--' + && isset($this->data['processedTca']['palettes'][$fieldByShowitemArray['paletteName']]['showitem']) + && is_string($this->data['processedTca']['palettes'][$fieldByShowitemArray['paletteName']]['showitem']) + ) { + $paletteName = $fieldByShowitemArray['paletteName']; + $paletteFields = GeneralUtility::trimExplode(',', $this->data['processedTca']['palettes'][$paletteName]['showitem'], true); + foreach ($paletteFields as $paletteField) { + $paletteFieldArray = $this->explodeSingleFieldShowItemConfiguration($paletteField); + if ($paletteFieldArray['fieldName'] === $fieldName) { + $allowedFields[] = implode(';', $paletteFieldArray); + break; + } + } + } + } + } + return $allowedFields; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Form/Container/NoTabsContainer.php b/Classes/Form/Container/NoTabsContainer.php new file mode 100644 index 0000000..3d68e77 --- /dev/null +++ b/Classes/Form/Container/NoTabsContainer.php @@ -0,0 +1,36 @@ +data; + $options['renderType'] = 'paletteAndSingleContainer'; + return $this->nodeFactory->create($options)->render(); + } +} diff --git a/Classes/Form/Container/PaletteAndSingleContainer.php b/Classes/Form/Container/PaletteAndSingleContainer.php new file mode 100644 index 0000000..5726617 --- /dev/null +++ b/Classes/Form/Container/PaletteAndSingleContainer.php @@ -0,0 +1,295 @@ +getLanguageService(); + + /* + * The first code block creates a target structure array to later create the final + * HTML string. The single fields and sub containers are rendered here already and + * other parts of the return array from children except html are accumulated in + * $this->resultArray + * + $targetStructure = [ + 0 => [ + 'type' => 'palette', + 'fieldName' => 'palette1', + 'paletteLegend' => 'palette1', + 'paletteDescription' => 'palette1Description', + 'elements' => [ + 0 => [ + 'type' => 'single', + 'fieldName' => 'paletteName', + 'fieldHtml' => 'element1', + ), + 1 => [ + 'type' => 'linebreak', + ), + 2 => [ + 'type' => 'single', + 'fieldName' => 'paletteName', + 'fieldHtml' => 'element2', + ], + ], + ], + 1 => [ + 'type' => 'single', + 'fieldName' => 'element3', + 'fieldHtml' => 'element3', + ], + 2 => [ + 'type' => 'palette', + 'fieldName' => 'palette2', + 'paletteLegend' => '', // Palette label is optional + 'paletteDescription' => '', // Palette description is optional + 'elements' => [ + 0 => [ + 'type' => 'single', + 'fieldName' => 'element4', + 'fieldHtml' => 'element4', + ], + 1 => [ + 'type' => 'linebreak', + ], + 2 => [ + 'type' => 'single', + 'fieldName' => 'element5', + 'fieldHtml' => 'element5', + ], + ], + ], + ); + */ + + // Create an intermediate structure of rendered sub elements and elements nested in palettes + $targetStructure = []; + $mainStructureCounter = -1; + $fieldsArray = $this->data['fieldsArray']; + $this->resultArray = $this->initializeResultArray(); + foreach ($fieldsArray as $fieldString) { + $fieldConfiguration = $this->explodeSingleFieldShowItemConfiguration($fieldString); + $fieldName = $fieldConfiguration['fieldName']; + if ($fieldName === '--palette--') { + $paletteElementArray = $this->createPaletteContentArray($fieldConfiguration['paletteName'] ?? ''); + if (!empty($paletteElementArray)) { + $mainStructureCounter++; + // If there is no label in ['types']['aType']['showitem'] for this palette: "--palette--;;aPalette", + // then use ['palettes']['aPalette']['label'] if given. + $paletteLegend = $fieldConfiguration['fieldLabel']; + if ($paletteLegend === null && !empty($this->data['processedTca']['palettes'][$fieldConfiguration['paletteName']]['label'])) { + $paletteLegend = $this->data['processedTca']['palettes'][$fieldConfiguration['paletteName']]['label']; + } + // Get description of palette. + $paletteDescription = $this->data['processedTca']['palettes'][$fieldConfiguration['paletteName']]['description'] ?? ''; + $targetStructure[$mainStructureCounter] = [ + 'type' => 'palette', + 'fieldName' => $fieldConfiguration['paletteName'], + 'paletteLegend' => $languageService->sL($paletteLegend), + 'paletteDescription' => $languageService->sL($paletteDescription), + 'elements' => $paletteElementArray, + ]; + } + } else { + if (!is_array($this->data['processedTca']['columns'][$fieldName] ?? null)) { + continue; + } + $options = $this->data; + $options['fieldName'] = $fieldName; + $options['renderType'] = 'singleFieldContainer'; + $childResultArray = $this->nodeFactory->create($options)->render(); + if (!empty($childResultArray['html'])) { + $mainStructureCounter++; + $targetStructure[$mainStructureCounter] = [ + 'type' => 'single', + 'fieldName' => $fieldConfiguration['fieldName'], + 'fieldHtml' => $childResultArray['html'], + ]; + } + $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $childResultArray, false); + } + } + + // Compile final content + $content = []; + foreach ($targetStructure as $element) { + if ($element['type'] === 'palette') { + $paletteName = $element['fieldName']; + $isHiddenPalette = !empty($this->data['processedTca']['palettes'][$paletteName]['isHiddenPalette']); + $html = []; + $html[] = '
'; + if (!empty($element['paletteLegend'])) { + $html[] = '

' . htmlspecialchars($element['paletteLegend']) . '

'; + } + if (!empty($element['paletteDescription'])) { + $html[] = '

' . nl2br(htmlspecialchars($element['paletteDescription'])) . '

'; + } + $html[] = $this->renderInnerPaletteContent($element); + $html[] = '
'; + $content[] = implode(LF, $html); + } else { + $html = []; + $html[] = '
'; + $html[] = '
'; + $html[] = $element['fieldHtml']; + $html[] = '
'; + $html[] = '
'; + $content[] = implode(LF, $html); + } + } + + $finalResultArray = $this->resultArray; + $finalResultArray['html'] = implode(LF, $content); + return $finalResultArray; + } + + /** + * Render single fields of a given palette + * + * @param string $paletteName The palette to render + */ + protected function createPaletteContentArray(string $paletteName): array + { + // palette needs a palette name reference, otherwise it does not make sense to try rendering of it + if (empty($paletteName) || empty($this->data['processedTca']['palettes'][$paletteName]['showitem'])) { + return []; + } + $resultStructure = []; + $foundRealElement = false; // Set to true if not only line breaks were rendered + $fieldsArray = GeneralUtility::trimExplode(',', $this->data['processedTca']['palettes'][$paletteName]['showitem'], true); + foreach ($fieldsArray as $fieldString) { + $fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString); + $fieldName = $fieldArray['fieldName']; + if ($fieldName === '--linebreak--') { + $resultStructure[] = [ + 'type' => 'linebreak', + ]; + } else { + if (!is_array($this->data['processedTca']['columns'][$fieldName] ?? null)) { + continue; + } + $options = $this->data; + $options['fieldName'] = $fieldName; + $options['renderType'] = 'singleFieldContainer'; + $singleFieldContentArray = $this->nodeFactory->create($options)->render(); + if (!empty($singleFieldContentArray['html'])) { + $foundRealElement = true; + $resultStructure[] = [ + 'type' => 'single', + 'fieldName' => $fieldName, + 'fieldHtml' => $singleFieldContentArray['html'], + ]; + } + $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $singleFieldContentArray, false); + } + } + if ($foundRealElement) { + return $resultStructure; + } + return []; + } + + /** + * Renders inner content of single elements of a palette and wrap it as needed + * + * @param array $elementArray Array of elements + * @return string Wrapped content + */ + protected function renderInnerPaletteContent(array $elementArray): string + { + $result = []; + $currentGroup = []; + + foreach ($elementArray['elements'] as $element) { + if ($element['type'] === 'linebreak') { + // Render current group before linebreak + if (!empty($currentGroup)) { + $result[] = $this->renderFieldGroup($currentGroup); + $currentGroup = []; + } + } else { + $currentGroup[] = $element; + } + } + + // Render remaining group + if (!empty($currentGroup)) { + $result[] = $this->renderFieldGroup($currentGroup); + } + + return implode(LF, $result); + } + + /** + * Renders a group of fields within a form-grid container + * + * @param array $fields Array of field elements + * @return string Rendered HTML + */ + protected function renderFieldGroup(array $fields): string + { + $numberOfItems = count($fields); + $result = []; + + if ($numberOfItems > 1) { + $result[] = '
'; + } + + foreach ($fields as $element) { + $result[] = '
'; + $result[] = $element['fieldHtml']; + $result[] = '
'; + } + + if ($numberOfItems > 1) { + $result[] = '
'; + } + + return implode(LF, $result); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Form/Container/SingleFieldContainer.php b/Classes/Form/Container/SingleFieldContainer.php new file mode 100644 index 0000000..c47a8a5 --- /dev/null +++ b/Classes/Form/Container/SingleFieldContainer.php @@ -0,0 +1,295 @@ +getBackendUserAuthentication(); + $resultArray = $this->initializeResultArray(); + + $table = $this->data['tableName']; + $row = $this->data['databaseRow']; + $fieldName = $this->data['fieldName']; + + $parameterArray = []; + $parameterArray['fieldConf'] = $this->data['processedTca']['columns'][$fieldName]; + + $isOverlay = false; + + // This field decides whether the current record is an overlay (as opposed to being a standalone record) + // Based on this decision we need to trigger field exclusion or special rendering (like readOnly) + if (isset($this->data['processedTca']['ctrl']['transOrigPointerField']) + && is_array($this->data['processedTca']['columns'][$this->data['processedTca']['ctrl']['transOrigPointerField']] ?? null) + ) { + $parentValue = $row[$this->data['processedTca']['ctrl']['transOrigPointerField']]; + if (MathUtility::canBeInterpretedAsInteger($parentValue)) { + $isOverlay = (bool)$parentValue; + } elseif (is_array($parentValue)) { + // This case may apply if the value has been converted to an array by the select or group data provider + $isOverlay = !empty($parentValue) ? (bool)$parentValue[0] : false; + } else { + throw new \InvalidArgumentException( + 'The given value "' . $parentValue . '" for the original language field ' . $this->data['processedTca']['ctrl']['transOrigPointerField'] + . ' of table ' . $table . ' is invalid.', + 1470742770 + ); + } + } + + // A couple of early returns in case the field should not be rendered + $fieldIsExcluded = $parameterArray['fieldConf']['exclude'] ?? false; + $fieldNotExcludable = $backendUser->check('non_exclude_fields', $table . ':' . $fieldName); + $fieldExcludedFromTranslatedRecords = empty($parameterArray['fieldConf']['l10n_display']) && ($parameterArray['fieldConf']['l10n_mode'] ?? '') === 'exclude'; + // Return if BE-user has no access rights to this field, @todo: another user access rights check! + if (($fieldIsExcluded && !$fieldNotExcludable) || ($isOverlay && $fieldExcludedFromTranslatedRecords) || $this->inlineFieldShouldBeSkipped()) { + return $resultArray; + } + + $tsConfig = $this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'] ?? []; + $parameterArray['fieldTSConfig'] = is_array($tsConfig) ? $tsConfig : []; + + if ($parameterArray['fieldTSConfig']['disabled'] ?? false) { + return $resultArray; + } + + // Override fieldConf by fieldTSconfig: + $parameterArray['fieldConf']['config'] = FormEngineUtility::overrideFieldConf($parameterArray['fieldConf']['config'], $parameterArray['fieldTSConfig']); + $parameterArray['itemFormElName'] = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']'; + $newElementBaseName = isset($this->data['elementBaseName']) ? $this->data['elementBaseName'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']' : ''; + + // The value to show in the form field. + $parameterArray['itemFormElValue'] = $row[$fieldName]; + // Set field to read-only if configured for translated records to show default language content as readonly + // Note: In such case, the database value of this field was already overridden by DatabaseRowDefaultAsReadonly. + if (($parameterArray['fieldConf']['l10n_display'] ?? false) + && GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly') + && $isOverlay + ) { + $parameterArray['fieldConf']['config']['readOnly'] = true; + } + + $processedTcaType = $this->data['processedTca']['ctrl']['type'] ?? ''; + $typeField = !str_contains($processedTcaType, ':') + ? $processedTcaType + : substr($processedTcaType, 0, (int)strpos($processedTcaType, ':')); + + // JavaScript code for event handlers: + $parameterArray['fieldChangeFunc'] = []; + $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = new UpdateValueOnFieldChange( + $table, + (string)$row['uid'], + $fieldName, + $parameterArray['itemFormElName'] + ); + + $requestFormEngineUpdate + = (!empty($this->data['processedTca']['ctrl']['type']) && $fieldName === $typeField) + || (isset($parameterArray['fieldConf']['onChange']) && $parameterArray['fieldConf']['onChange'] === 'reload'); + if ($requestFormEngineUpdate) { + $askForUpdate = $backendUser->jsConfirmation(JsConfirmation::TYPE_CHANGE); + $parameterArray['fieldChangeFunc']['record_type_changed'] = new ReloadOnFieldChange($askForUpdate); + } + + // Based on the type of the item, call a render function on a child element + $options = $this->data; + $options['parameterArray'] = $parameterArray; + $options['elementBaseName'] = $newElementBaseName; + if (!empty($parameterArray['fieldConf']['config']['renderType'])) { + $options['renderType'] = $parameterArray['fieldConf']['config']['renderType']; + } else { + // Fallback to type if no renderType is given + $options['renderType'] = $parameterArray['fieldConf']['config']['type']; + } + + return $this->nodeFactory->create($options)->render(); + } + + /** + * Rendering of inline fields should be skipped under certain circumstances + */ + protected function inlineFieldShouldBeSkipped(): bool + { + $table = $this->data['tableName']; + $fieldName = $this->data['fieldName']; + $fieldConfig = $this->data['processedTca']['columns'][$fieldName]['config']; + $fieldConfig += [ + 'MM' => '', + 'foreign_table' => '', + 'foreign_selector' => '', + 'foreign_field' => '', + ]; + if (($this->data['inlineStructure']['stable'] ?? []) !== []) { + $searchArray = [ + '%OR' => [ + 'config' => [ + 0 => [ + '%AND' => [ + 'foreign_table' => $table, + '%OR' => [ + '%AND' => [ + 'appearance' => ['useCombination' => true], + 'foreign_selector' => $fieldName, + ], + 'MM' => $fieldConfig['MM'], + ], + ], + ], + 1 => [ + '%AND' => [ + 'foreign_table' => $fieldConfig['foreign_table'], + 'foreign_selector' => $fieldConfig['foreign_field'], + ], + ], + ], + ], + ]; + // If we have symmetric fields, check on which side we are and hide fields, that are set automatically: + if ($this->data['isOnSymmetricSide']) { + $searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_field'] = $fieldName; + $searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_sortby'] = $fieldName; + } else { + $searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_field'] = $fieldName; + $searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_sortby'] = $fieldName; + } + // Parent record from structure stack + $parent = $this->inlineStackProcessor->getStructureLevelFromStructure($this->data['inlineStructure'], -1) ?? []; + return $this->arrayCompareComplex($parent, $searchArray); + } + return false; + } + + /** + * Handles complex comparison requests on an array. + * A request could look like the following: + * + * $searchArray = array( + * '%AND' => array( + * 'key1' => 'value1', + * 'key2' => 'value2', + * '%OR' => array( + * 'subarray' => array( + * 'subkey' => 'subvalue' + * ), + * 'key3' => 'value3', + * 'key4' => 'value4' + * ) + * ) + * ); + * + * It is possible to use the array keys '%AND.1', '%AND.2', etc. to prevent + * overwriting the sub-array. It could be necessary, if you use complex comparisons. + * + * The example above means, key1 *AND* key2 (and their values) have to match with + * the $subjectArray and additional one *OR* key3 or key4 have to meet the same + * condition. + * It is also possible to compare parts of a sub-array (e.g. "subarray"), so this + * function recurses down one level in that sub-array. + * + * @param array $subjectArray The array to search in + * @param array $searchArray The array with keys and values to search for + * @param string $type Use '%AND' or '%OR' for comparison + * @return bool The result of the comparison + */ + protected function arrayCompareComplex(array $subjectArray, array $searchArray, string $type = ''): bool + { + $localMatches = 0; + $localEntries = 0; + if ($searchArray !== []) { + // If no type was passed, try to determine + if (!$type) { + reset($searchArray); + $type = (string)key($searchArray); + $searchArray = current($searchArray); + } + // We use '%AND' and '%OR' in uppercase + $type = strtoupper($type); + // Split regular elements from sub elements + foreach ($searchArray as $key => $value) { + $localEntries++; + // Process a sub-group of OR-conditions + if ($key === '%OR') { + $localMatches += $this->arrayCompareComplex($subjectArray, $value, '%OR') ? 1 : 0; + } elseif ($key === '%AND') { + $localMatches += $this->arrayCompareComplex($subjectArray, $value, '%AND') ? 1 : 0; + } elseif (is_array($value) && $this->isAssociativeArray($searchArray)) { + $localMatches += $this->arrayCompareComplex($subjectArray[$key], $value, $type) ? 1 : 0; + } elseif (is_array($value)) { + $localMatches += $this->arrayCompareComplex($subjectArray, $value, $type) ? 1 : 0; + } else { + if (isset($subjectArray[$key]) && isset($value)) { + // Boolean match: + if (is_bool($value)) { + $localMatches += !($subjectArray[$key] xor $value) ? 1 : 0; + } elseif (is_numeric($subjectArray[$key]) && is_numeric($value)) { + $localMatches += $subjectArray[$key] == $value ? 1 : 0; + } else { + $localMatches += $subjectArray[$key] === $value ? 1 : 0; + } + } + } + // If one or more matches are required ('OR'), return TRUE after the first successful match + if ($type === '%OR' && $localMatches > 0) { + return true; + } + // If all matches are required ('AND') and we have no result after the first run, return FALSE + if ($type === '%AND' && $localMatches == 0) { + return false; + } + } + } + // Return the result for '%AND' (if nothing was checked, TRUE is returned) + return $localEntries === $localMatches; + } + + /** + * Checks whether an object is an associative array. + * + * @param mixed $object The object to be checked + * @return bool Returns TRUE, if the object is an associative array + */ + protected function isAssociativeArray($object) + { + return is_array($object) && !empty($object) && array_keys($object) !== range(0, count($object) - 1); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Form/Container/SiteLanguageContainer.php b/Classes/Form/Container/SiteLanguageContainer.php new file mode 100644 index 0000000..3a83345 --- /dev/null +++ b/Classes/Form/Container/SiteLanguageContainer.php @@ -0,0 +1,190 @@ +inlineData = $this->data['inlineData']; + + $inlineStructure = $this->data['inlineStructure']; + + $row = $this->data['databaseRow']; + $parameterArray = $this->data['parameterArray']; + $config = $parameterArray['fieldConf']['config']; + $resultArray = $this->initializeResultArray(); + + // Add the current inline job to the structure stack + $inlineStructure['stable'][] = [ + 'table' => $this->data['tableName'], + 'uid' => $row['uid'], + 'field' => $this->data['fieldName'], + 'config' => $config, + ]; + + // Hand over original returnUrl to SiteInlineAjaxController. Needed if opening for instance a + // nested element in a new view to then go back to the original returnUrl and not the url of + // the site inline ajax controller. + $config['originalReturnUrl'] = $this->data['returnUrl']; + + // e.g. data[site][1][languages] + $nameForm = $this->inlineStackProcessor->getFormPrefixFromStructure($inlineStructure); + // e.g. data-0-site-1-languages + $nameObject = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($inlineStructure, $this->data['inlineFirstPid']); + // e.g. array('table' => 'site', 'uid' => '1', 'field' => 'languages', 'config' => array()) + $top = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0); + + $this->inlineData['config'][$nameObject] = [ + 'table' => self::FOREIGN_TABLE, + ]; + + $configJson = (string)json_encode($config); + $this->inlineData['config'][$nameObject . '-' . self::FOREIGN_TABLE] = [ + 'min' => $config['minitems'], + 'max' => $config['maxitems'], + 'sortable' => false, + 'top' => [ + 'table' => $top['table'], + 'uid' => $top['uid'], + ], + 'context' => [ + 'config' => $configJson, + 'hmac' => $this->hashService->hmac($configJson, 'InlineContext'), + ], + ]; + $this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack']; + + $uniqueIds = []; + foreach ($parameterArray['fieldConf']['children'] as $children) { + $value = (int)($children['databaseRow'][self::FOREIGN_FIELD]['0'] ?? 0); + if (isset($children['databaseRow']['uid'])) { + $uniqueIds[$children['databaseRow']['uid']] = $value; + } + } + + $uniquePossibleRecords = $config['uniquePossibleRecords'] ?? []; + $possibleRecordsUidToTitle = []; + foreach ($uniquePossibleRecords as $possibleRecord) { + $possibleRecordsUidToTitle[$possibleRecord['value']] = $possibleRecord['label']; + } + $this->inlineData['unique'][$nameObject . '-' . self::FOREIGN_TABLE] = [ + // Usually "max" would be the number of possible records. However, since + // we also allow new languages to be created, we just use the maxitems value. + 'max' => $config['maxitems'], + // "used" must be a string array + 'used' => array_map(strval(...), $uniqueIds), + 'table' => self::FOREIGN_TABLE, + 'elTable' => self::FOREIGN_TABLE, + 'field' => self::FOREIGN_FIELD, + 'possible' => $possibleRecordsUidToTitle, + ]; + + $resultArray['inlineData'] = $this->inlineData; + + $fieldInformationResult = $this->renderFieldInformation(); + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); + $selectorOptions = $childRecordUids = $childHtml = []; + + foreach ($config['uniquePossibleRecords'] ?? [] as $record) { + // Do not add the PHP_INT_MAX placeholder or already configured languages + if ($record['value'] !== PHP_INT_MAX && !in_array($record['value'], $uniqueIds, true)) { + $selectorOptions[] = ['value' => (string)$record['value'], 'label' => (string)$record['label']]; + } + } + + foreach ($this->data['parameterArray']['fieldConf']['children'] as $children) { + $children['inlineParentUid'] = $row['uid']; + $children['inlineFirstPid'] = $this->data['inlineFirstPid']; + $children['inlineParentConfig'] = $config; + $children['inlineData'] = $this->inlineData; + $children['inlineStructure'] = $inlineStructure; + $children['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray']; + $children['renderType'] = 'inlineRecordContainer'; + $childResult = $this->nodeFactory->create($children)->render(); + $childHtml[] = $childResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false); + if (isset($children['databaseRow']['uid'])) { + $childRecordUids[] = $children['databaseRow']['uid']; + } + } + + $view = $this->backendViewFactory->create($this->data['request']); + $view->assignMultiple([ + 'nameObject' => $nameObject, + 'nameForm' => $nameForm, + 'webComponentAttributes' => GeneralUtility::implodeAttributes([ + 'id' => $nameObject, + 'data-type' => 'language', + 'data-object-group' => $nameObject . '-' . self::FOREIGN_TABLE, + 'data-form-field' => $nameForm, + 'data-expand-single' => (bool)($config['appearance']['expandSingle'] ?? false) ? 'true' : 'false', + 'data-sortable' => 'false', + 'data-min' => (int)($config['minitems'] ?? 0), + 'data-max' => (int)($config['maxitems'] ?? 0), + ], true), + 'fieldInformation' => $fieldInformationResult['html'], + 'selectorConfiguration' => [ + 'identifier' => $nameObject . '-' . self::FOREIGN_TABLE . '_selector', + 'options' => $selectorOptions, + ], + 'inlineRecords' => [ + 'identifier' => $nameObject . '_records', + 'title' => trim($parameterArray['fieldConf']['label'] ?? ''), + 'records' => implode(PHP_EOL, $childHtml), + ], + 'childRecordUids' => implode(',', $childRecordUids), + 'validationRules' => $this->getValidationDataAsJsonString([ + 'type' => 'inline', + 'minitems' => $config['minitems'] ?? null, + 'maxitems' => $config['maxitems'] ?? null, + ]), + 'presetOptions' => [ + 'identifier' => $nameObject . '_preset', + 'options' => $this->siteLanguagePresets->getAllForSelector(), + ], + ]); + + $resultArray['html'] = $this->wrapWithFieldsetAndLegend($view->render('Form/SiteLanguageContainer')); + $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/container/inline-control-container.js'); + + return $resultArray; + } +} diff --git a/Classes/Form/Container/TabsContainer.php b/Classes/Form/Container/TabsContainer.php new file mode 100644 index 0000000..f38f818 --- /dev/null +++ b/Classes/Form/Container/TabsContainer.php @@ -0,0 +1,106 @@ +getLanguageService(); + + // All the fields to handle in a flat list + $fieldsArray = $this->data['fieldsArray']; + + // Create a nested array from flat fieldArray list + $tabsArray = []; + // First element will be a --div--, so it is safe to start -1 here to trigger 0 as first array index + $currentTabIndex = -1; + foreach ($fieldsArray as $fieldString) { + $fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString); + if ($fieldArray['fieldName'] === '--div--') { + $currentTabIndex++; + if (empty($fieldArray['fieldLabel'])) { + throw new \RuntimeException( + 'A --div-- has no label (--div--;fieldLabel) in showitem of ' . implode(',', $fieldsArray), + 1426454001 + ); + } + $tabsArray[$currentTabIndex] = [ + 'label' => $languageService->sL($fieldArray['fieldLabel']), + 'elements' => [], + ]; + } else { + $tabsArray[$currentTabIndex]['elements'][] = $fieldArray; + } + } + + $resultArray = $this->initializeResultArray(); + $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/tab.js'); + + $domIdPrefix = 'DTM-' . md5($this->data['tableName'] . $this->data['databaseRow']['uid']); + $tabCounter = 0; + $tabElements = []; + foreach ($tabsArray as $tabWithLabelAndElements) { + $tabCounter++; + $elements = $tabWithLabelAndElements['elements']; + + // Merge elements of this tab into a single list again and hand over to + // palette and single field container to render this group + $options = $this->data; + $options['tabAndInlineStack'][] = [ + 'tab', + $domIdPrefix . '-' . $tabCounter, + ]; + $options['fieldsArray'] = []; + foreach ($elements as $element) { + $options['fieldsArray'][] = implode(';', $element); + } + $options['renderType'] = 'paletteAndSingleContainer'; + $childArray = $this->nodeFactory->create($options)->render(); + + if ($childArray['html'] !== '') { + $tabElements[] = [ + 'label' => $tabWithLabelAndElements['label'], + 'content' => $childArray['html'], + ]; + } + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray, false); + } + + $resultArray['html'] = $this->renderTabMenu($tabElements, $domIdPrefix); + return $resultArray; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Form/Element/AbstractFormElement.php b/Classes/Form/Element/AbstractFormElement.php new file mode 100644 index 0000000..ba6ae6b --- /dev/null +++ b/Classes/Form/Element/AbstractFormElement.php @@ -0,0 +1,451 @@ +nodeFactory = $nodeFactory; + } + + /** + * Merge field information configuration with default and render them. + * + * @return array Result array + */ + protected function renderFieldInformation(): array + { + $options = $this->data; + $fieldInformation = $this->defaultFieldInformation; + $fieldInformationFromTca = $options['parameterArray']['fieldConf']['config']['fieldInformation'] ?? []; + ArrayUtility::mergeRecursiveWithOverrule($fieldInformation, $fieldInformationFromTca); + $options['renderType'] = 'fieldInformation'; + $options['renderData']['fieldInformation'] = $fieldInformation; + return $this->nodeFactory->create($options)->render(); + } + + /** + * Merge field control configuration with default controls and render them. + * + * @return array Result array + */ + protected function renderFieldControl(): array + { + $options = $this->data; + $fieldControl = $this->defaultFieldControl; + $fieldControlFromTca = $options['parameterArray']['fieldConf']['config']['fieldControl'] ?? []; + ArrayUtility::mergeRecursiveWithOverrule($fieldControl, $fieldControlFromTca); + $options['renderType'] = 'fieldControl'; + $options['renderData']['fieldControl'] = $fieldControl; + return $this->nodeFactory->create($options)->render(); + } + + /** + * Merge field wizard configuration with default wizards and render them. + * + * @return array Result array + */ + protected function renderFieldWizard(): array + { + $options = $this->data; + $fieldWizard = $this->defaultFieldWizard; + $fieldWizardFromTca = $options['parameterArray']['fieldConf']['config']['fieldWizard'] ?? []; + ArrayUtility::mergeRecursiveWithOverrule($fieldWizard, $fieldWizardFromTca); + $options['renderType'] = 'fieldWizard'; + $options['renderData']['fieldWizard'] = $fieldWizard; + return $this->nodeFactory->create($options)->render(); + } + + /** + * Render a label element for the current field by given id. + */ + protected function renderLabel(string $for): string + { + $label = htmlspecialchars($this->data['parameterArray']['fieldConf']['label'] ?? ''); + if ($this->getBackendUser()->shallDisplayDebugInformation()) { + $fieldName = $this->data['flexFormContainerFieldName'] ?? $this->data['flexFormFieldName'] ?? $this->data['containerFieldName'] ?? $this->data['fieldName']; + $label .= ' [' . htmlspecialchars($fieldName) . ']'; + } + $html = ''; + $html .= $this->renderDescription(); + return $html; + } + + /** + * Elements that don't render a simple input field can't have a '
'; + $tableRows[] = ''; + $tableRows[] = ''; + $tableRows[] = ''; + $tableRows[] = ''; + } + + if ($hasGroupHeader) { + $expandAll = ($config['appearance']['expandAll'] ?? false) ? 'show' : ''; + $html[] = '
'; + } + + $html[] = '
'; + $html[] = '
'; + $tableRows[] = ''; + $tableRows[] = ''; + $tableRows[] = ''; + $tableRows[] = ''; + $tableRows[] = ''; + $tableRows[] = '' . $item['help'] . '
'; + if (!$readOnly) { + // Add table header with actions, in case the element is not readOnly + $html[] = ''; + $html[] = ''; + $html[] = ''; + $html[] = ''; + $html[] = ''; + $html[] = ''; + + // Add JavaScript module. This is only needed, in case the element + // is not readOnly, since otherwise no checkbox changes take place. + $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/multi-record-selection.js'); + } + $html[] = '' . implode(LF, $tableRows) . ''; + $html[] = '
' . $this->getRecordSelectionCheckActions() . '' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.th.name')) . '
'; + $html[] = ''; + if ($hasGroupHeader) { + $html[] = ''; + } + } + $html[] = ''; + } + + $html[] = ''; + if (!$readOnly && !empty($fieldWizardHtml)) { + $html[] = '
'; + $html[] = $fieldWizardHtml; + $html[] = '
'; + } + $html[] = ''; + $html[] = ''; + + $resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html)); + return $resultArray; + } + + /** + * A function that creates an icon with a help text. If a user clicks on + * the icon, the help text will show up as tooltip + * + * @param array $overloadHelpText Array with text to overload help text + * @return string the HTML code ready to render + */ + protected function wrapInHelp(array $overloadHelpText = []): string + { + // If there's a help text or some overload information, proceed with preparing an output + if (empty($overloadHelpText)) { + return ''; + } + $text = $this->iconFactory->getIcon('actions-system-help-open', IconSize::SMALL)->render(); + $abbrClassAdd = ' help-teaser-icon'; + $text = '' . $text . ''; + $wrappedText = ''; + return $wrappedText; + } + + protected function getRecordSelectionCheckActions(): string + { + $lang = $this->getLanguageService(); + return ' + '; + } +} diff --git a/Classes/Form/Element/SelectCountryElement.php b/Classes/Form/Element/SelectCountryElement.php new file mode 100644 index 0000000..837ca47 --- /dev/null +++ b/Classes/Form/Element/SelectCountryElement.php @@ -0,0 +1,220 @@ + [ + 'renderType' => 'selectIcons', + 'disabled' => true, + ], + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + 'after' => [ + 'selectIcons', + ], + ], + 'otherLanguageContent' => [ + 'renderType' => 'otherLanguageContent', + 'after' => [ 'localizationStateSelector' ], + ], + 'defaultLanguageDifferences' => [ + 'renderType' => 'defaultLanguageDifferences', + 'after' => [ 'otherLanguageContent' ], + ], + ]; + + /** + * Render single element + * + * @return array As defined in initializeResultArray() of AbstractNode + */ + public function render(): array + { + $resultArray = $this->initializeResultArray(); + + $parameterArray = $this->data['parameterArray']; + $config = $parameterArray['fieldConf']['config']; + + $selectItems = $parameterArray['fieldConf']['config']['items'] ?? []; + $classList = ['form-select', 'form-control-adapt']; + + // Initialization: + $selectId = StringUtility::getUniqueId('tceforms-select-'); + $selectedIcon = ''; + $size = (int)($config['size'] ?? 0); + + // Style set on '; + $html[] = $options; + $html[] = ''; + if ($hasIcons) { + $html[] = ''; + } + $html[] = ''; + if (!$disabled && !empty($fieldControlHtml)) { + $html[] = '
'; + $html[] = '
'; + $html[] = $fieldControlHtml; + $html[] = '
'; + $html[] = '
'; + } + if (!$disabled && !empty($fieldWizardHtml)) { + $html[] = '
'; + $html[] = $fieldWizardHtml; + $html[] = '
'; + } + $html[] = ''; + $html[] = ''; + $html[] = ''; + + $onFieldChangeItems = $this->getOnFieldChangeItems($parameterArray['fieldChangeFunc'] ?? []); + $resultArray['javaScriptModules']['selectSingleElement'] = JavaScriptModuleInstruction::create( + '@typo3/backend/form-engine/element/select-country-element.js' + )->invoke('initializeOnReady', '#' . $selectId, ['onChange' => $onFieldChangeItems]); + + $resultArray['html'] = implode(LF, $html); + return $resultArray; + } +} diff --git a/Classes/Form/Element/SelectMultipleSideBySideElement.php b/Classes/Form/Element/SelectMultipleSideBySideElement.php new file mode 100644 index 0000000..04124d2 --- /dev/null +++ b/Classes/Form/Element/SelectMultipleSideBySideElement.php @@ -0,0 +1,482 @@ + [ + 'renderType' => 'editPopup', + 'disabled' => true, + ], + 'addRecord' => [ + 'renderType' => 'addRecord', + 'disabled' => true, + ], + 'listModule' => [ + 'renderType' => 'listModule', + 'disabled' => true, + 'after' => [ 'addRecord' ], + ], + ]; + + /** + * Default field wizards enabled for this element. + * + * @var array + */ + protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], + 'otherLanguageContent' => [ + 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector', + ], + ], + 'defaultLanguageDifferences' => [ + 'renderType' => 'defaultLanguageDifferences', + 'after' => [ + 'otherLanguageContent', + ], + ], + ]; + + public function __construct( + private readonly IconFactory $iconFactory, + ) {} + + /** + * Merge field control configuration with default controls and render them. + * + * @return array Result array + */ + protected function renderFieldControl(): array + { + $alternativeResult = [ + 'additionalInlineLanguageLabelFiles' => [], + 'stylesheetFiles' => [], + 'javaScriptModules' => [], + 'inlineData' => [], + 'html' => '', + ]; + $options = $this->data; + $fieldControl = $this->defaultFieldControl; + $fieldControlFromTca = $options['parameterArray']['fieldConf']['config']['fieldControl'] ?? []; + ArrayUtility::mergeRecursiveWithOverrule($fieldControl, $fieldControlFromTca); + $options['renderType'] = 'fieldControl'; + if (isset($fieldControl['editPopup'])) { + $editPopupControl = $fieldControl['editPopup']; + unset($fieldControl['editPopup']); + $alternativeOptions = $options; + $alternativeOptions['renderData']['fieldControl'] = ['editPopup' => $editPopupControl]; + $alternativeResult = $this->nodeFactory->create($alternativeOptions)->render(); + } + $options['renderData']['fieldControl'] = $fieldControl; + return [$this->nodeFactory->create($options)->render(), $alternativeResult]; + } + + /** + * Render side by side element. + * + * @return array As defined in initializeResultArray() of AbstractNode + */ + public function render(): array + { + $parameterArray = $this->data['parameterArray']; + $config = $parameterArray['fieldConf']['config']; + + if ($config['readOnly'] ?? false) { + // Early return for the relatively simple read only case + return $this->renderReadOnly(); + } + + $filterTextfield = []; + $languageService = $this->getLanguageService(); + $resultArray = $this->initializeResultArray(); + $elementName = $parameterArray['itemFormElName']; + + $possibleItems = $config['items']; + $selectedItems = $parameterArray['itemFormElValue'] ?: []; + $maxItems = $config['maxitems']; + + $size = (int)($config['size'] ?? 2); + $autoSizeMax = (int)($config['autoSizeMax'] ?? 0); + if ($autoSizeMax > 0) { + $size = MathUtility::forceIntegerInRange($size, 1); + $size = MathUtility::forceIntegerInRange(count($selectedItems) + 1, $size, $autoSizeMax); + } + + $itemCanBeSelectedMoreThanOnce = !empty($config['multiple']); + + $listOfSelectedValues = []; + $selectedItemsHtml = []; + foreach ($selectedItems as $itemValue) { + foreach ($possibleItems as $possibleItem) { + if ($possibleItem['value'] == $itemValue) { + $title = $possibleItem['label']; + $listOfSelectedValues[] = $itemValue; + $selectedItemsHtml[] = ''; + break; + } + } + } + + $selectableItemCounter = 0; + $selectableItemGroupCounter = 0; + $selectableItemGroups = []; + $selectableItemsHtml = []; + + // Initialize groups + foreach ($possibleItems as $possibleItem) { + $disableAttributes = []; + if (!$itemCanBeSelectedMoreThanOnce && in_array((string)$possibleItem['value'], $selectedItems, true)) { + $disableAttributes = [ + 'disabled' => 'disabled', + 'class' => 'hidden', + ]; + } + if ($possibleItem['value'] === '--div--') { + if ($selectableItemCounter !== 0) { + $selectableItemGroupCounter++; + } + $selectableItemGroups[$selectableItemGroupCounter]['header']['title'] = $possibleItem['label']; + } else { + $selectableItemGroups[$selectableItemGroupCounter]['items'][] = [ + 'label' => $this->appendValueToLabelInDebugMode($possibleItem['label'], $possibleItem['value']), + 'attributes' => array_merge(['title' => $possibleItem['label'], 'value' => $possibleItem['value']], $disableAttributes), + ]; + // In case the item is not disabled, enable the group (if any) + if ($disableAttributes === [] && isset($selectableItemGroups[$selectableItemGroupCounter]['header'])) { + $selectableItemGroups[$selectableItemGroupCounter]['header']['disabled'] = false; + } + $selectableItemCounter++; + } + } + + // Process groups + foreach ($selectableItemGroups as $selectableItemGroup) { + if (!is_array($selectableItemGroup['items'] ?? false)) { + continue; + } + + $optionGroup = isset($selectableItemGroup['header']); + if ($optionGroup) { + $selectableItemsHtml[] = ''; + } + + foreach ($selectableItemGroup['items'] as $item) { + $selectableItemsHtml[] = ' + '; + } + + if ($optionGroup) { + $selectableItemsHtml[] = ''; + } + } + + // Html stuff for filter and select filter on top of right side of multi select boxes + $filterTextfieldId = StringUtility::getUniqueId('tceforms-multiselect-filter-'); + $filterTextfield[] = ''; + + $filterDropDownOptions = []; + if (isset($config['multiSelectFilterItems']) && is_array($config['multiSelectFilterItems']) && count($config['multiSelectFilterItems']) > 1) { + foreach ($config['multiSelectFilterItems'] as $optionElement) { + $value = $languageService->sL($optionElement[0]); + $label = $value; + if (isset($optionElement[1]) && trim($optionElement[1]) !== '') { + $label = $languageService->sL($optionElement[1]); + } + $filterDropDownOptions[] = ''; + } + } + $filterHtml = []; + $filterHtml[] = '
'; + if (!empty($filterDropDownOptions)) { + $filterHtml[] = '
'; + $filterHtml[] = '
'; + $filterHtml[] = ''; + $filterHtml[] = '
'; + $filterHtml[] = '
'; + $filterHtml[] = implode(LF, $filterTextfield); + $filterHtml[] = '
'; + $filterHtml[] = '
'; + } else { + $filterHtml[] = implode(LF, $filterTextfield); + } + $filterHtml[] = '
'; + + $multipleAttribute = ''; + if ($maxItems !== 1 && $size !== 1) { + $multipleAttribute = ' multiple="multiple"'; + } + + $fieldInformationResult = $this->renderFieldInformation(); + $fieldInformationHtml = $fieldInformationResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); + + [$fieldControlResult, $alternativeControlResult] = $this->renderFieldControl(); + $fieldControlHtml = $fieldControlResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false); + $alternativeFieldControlHtml = $alternativeControlResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $alternativeControlResult, false); + + $fieldWizardResult = $this->renderFieldWizard(); + $fieldWizardHtml = $fieldWizardResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false); + + $selectedOptionsFieldId = StringUtility::getUniqueId('tceforms-multiselect-'); + $availableOptionsFieldId = StringUtility::getUniqueId('tceforms-multiselect-'); + + $html = []; + $html[] = $this->renderLabel($selectedOptionsFieldId); + $html[] = '
'; + $html[] = $fieldInformationHtml; + $html[] = '
'; + $html[] = '
'; + $html[] = ''; + $html[] = '
'; + $html[] = '
'; + $html[] = ''; + $html[] = '
'; + $html[] = '
'; + $html[] = ' 1 && $size >= 2) { + $html[] = ''; + } + if ($maxItems > 1) { + $html[] = ''; + $html[] = ''; + } + if ($maxItems > 1 && $size >= 2) { + $html[] = ''; + } + $html[] = $alternativeFieldControlHtml; + $html[] = ''; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = ''; + $html[] = '
'; + $html[] = implode(LF, $filterHtml); + $html[] = '
'; + $selectElementAttrs = array_merge( + [ + 'size' => $size, + 'id' => $availableOptionsFieldId, + 'class' => 'form-select t3js-formengine-select-itemstoselect', + 'data-relatedfieldname' => $elementName, + 'data-exclusivevalues' => $config['exclusiveKeys'] ?? '', + 'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config), + ], + $this->getOnFieldChangeAttrs('change', $parameterArray['fieldChangeFunc'] ?? []) + ); + $html[] = ''; + $html[] = '
'; + if (!empty($fieldControlHtml)) { + $html[] = '
'; + $html[] = '
'; + $html[] = $fieldControlHtml; + $html[] = '
'; + $html[] = '
'; + } + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = ''; + $html[] = '
'; + if (!empty($fieldWizardHtml)) { + $html[] = '
'; + $html[] = $fieldWizardHtml; + $html[] = '
'; + } + $html[] = '
'; + $html[] = ''; + + $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create( + '@typo3/backend/form-engine/element/select-multiple-side-by-side-element.js' + )->instance($selectedOptionsFieldId, $availableOptionsFieldId); + + $resultArray['html'] = implode(LF, $html); + return $resultArray; + } + + /** + * Create HTML of a read only multi select. Right side is not + * rendered, but just the left side with the selected items. + * + * @return array + */ + protected function renderReadOnly() + { + $languageService = $this->getLanguageService(); + $resultArray = $this->initializeResultArray(); + + $parameterArray = $this->data['parameterArray']; + $config = $parameterArray['fieldConf']['config']; + $fieldName = $parameterArray['itemFormElName']; + + $possibleItems = $config['items']; + $selectedItems = $parameterArray['itemFormElValue'] ?: []; + if (!is_array($selectedItems)) { + $selectedItems = GeneralUtility::trimExplode(',', $selectedItems, true); + } + $size = (int)($config['size'] ?? 2); + $autoSizeMax = (int)($config['autoSizeMax'] ?? 0); + if ($autoSizeMax > 0) { + $size = MathUtility::forceIntegerInRange($size, 1); + $size = MathUtility::forceIntegerInRange(count($selectedItems) + 1, $size, $autoSizeMax); + } + + $multiple = ''; + if ($size !== 1) { + $multiple = ' multiple="multiple"'; + } + + $listOfSelectedValues = []; + $optionsHtml = []; + foreach ($selectedItems as $itemValue) { + foreach ($possibleItems as $possibleItem) { + if ($possibleItem['value'] == $itemValue) { + $title = $possibleItem['label']; + $listOfSelectedValues[] = $itemValue; + $optionsHtml[] = ''; + break; + } + } + } + + $fieldInformationResult = $this->renderFieldInformation(); + $fieldInformationHtml = $fieldInformationResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); + + $selectId = StringUtility::getUniqueId('tceforms-multiselect-'); + + $html = []; + $html[] = $this->renderLabel($selectId); + $html[] = '
'; + $html[] = $fieldInformationHtml; + $html[] = '
'; + $html[] = '
'; + $html[] = ''; + $html[] = '
'; + $html[] = '
'; + $html[] = ''; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + + $resultArray['html'] = implode(LF, $html); + return $resultArray; + } + + protected function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Form/Element/SelectSingleBoxElement.php b/Classes/Form/Element/SelectSingleBoxElement.php new file mode 100644 index 0000000..2b44408 --- /dev/null +++ b/Classes/Form/Element/SelectSingleBoxElement.php @@ -0,0 +1,210 @@ + [ + 'renderType' => 'resetSelection', + ], + ]; + + /** + * Default field wizards enabled for this element. + * + * @var array + */ + protected $defaultFieldWizard = [ + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + ], + 'otherLanguageContent' => [ + 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector', + ], + ], + 'defaultLanguageDifferences' => [ + 'renderType' => 'defaultLanguageDifferences', + 'after' => [ + 'otherLanguageContent', + ], + ], + ]; + + /** + * This will render a selector box element, or possibly a special construction with two selector boxes. + * + * @return array As defined in initializeResultArray() of AbstractNode + */ + public function render(): array + { + $languageService = $this->getLanguageService(); + $resultArray = $this->initializeResultArray(); + + $parameterArray = $this->data['parameterArray']; + // Field configuration from TCA: + $config = $parameterArray['fieldConf']['config']; + $selectItems = $parameterArray['fieldConf']['config']['items']; + $disabled = !empty($config['readOnly']); + + // Get item value as array and make unique, which is fine because there can be no duplicates anyway. + $itemArray = array_flip($parameterArray['itemFormElValue']); + $width = $this->formMaxWidth($this->defaultInputWidth); + + $optionElements = []; + foreach ($selectItems as $item) { + $value = $item['value']; + $attributes = []; + // Selected or not by default + if (isset($itemArray[$value])) { + $attributes['selected'] = 'selected'; + unset($itemArray[$value]); + } + // Non-selectable element + if ((string)$value === '--div--') { + $attributes['disabled'] = 'disabled'; + $attributes['class'] = 'formcontrol-select-divider'; + } + $optionElements[] = $this->renderOptionElement($value, $item['label'], $attributes); + } + + $selectItems = $parameterArray['fieldConf']['config']['items']; + $size = (int)($config['size'] ?? 0); + $autoSizeMax = (int)($config['autoSizeMax'] ?? 0); + if ($autoSizeMax > 0) { + $size = MathUtility::forceIntegerInRange($size, 1); + $size = MathUtility::forceIntegerInRange(count($selectItems) + 1, $size, $autoSizeMax); + } + $selectId = StringUtility::getUniqueId($size === 1 ? 'tceforms-select' : 'tceforms-multiselect'); + $selectElement = $this->renderSelectElement($optionElements, $parameterArray, $config, $selectId, $size); + + $fieldInformationResult = $this->renderFieldInformation(); + $fieldInformationHtml = $fieldInformationResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); + + $fieldControlResult = $this->renderFieldControl(); + $fieldControlHtml = $fieldControlResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false); + + $fieldWizardResult = $this->renderFieldWizard(); + $fieldWizardHtml = $fieldWizardResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false); + + $html = []; + $html[] = $this->renderLabel($selectId); + $html[] = '
'; + $html[] = $fieldInformationHtml; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + if (!$disabled) { + // Add an empty hidden field which will send a blank value if all items are unselected. + $html[] = ''; + } + $html[] = $selectElement; + $html[] = '
'; + if (!$disabled) { + if (!empty($fieldControlHtml)) { + $html[] = '
'; + $html[] = $fieldControlHtml; + $html[] = '
'; + } + $html[] = '
'; + $html[] = '
'; + $html[] = '' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.holdDownCTRL')) . ''; + $html[] = '
'; + if (!empty($fieldWizardHtml)) { + $html[] = '
'; + $html[] = $fieldWizardHtml; + $html[] = '
'; + } + } else { + $html[] = '
'; + } + $html[] = '
'; + $html[] = '
'; + + $resultArray['html'] = implode(LF, $html); + return $resultArray; + } + + /** + * Renders a '; + $html[] = implode(LF, $optionElements); + $html[] = ''; + + return implode(LF, $html); + } + + /** + * Renders a single ', + + ]; + + return implode('', $html); + } +} diff --git a/Classes/Form/Element/SelectSingleElement.php b/Classes/Form/Element/SelectSingleElement.php new file mode 100644 index 0000000..42afb88 --- /dev/null +++ b/Classes/Form/Element/SelectSingleElement.php @@ -0,0 +1,261 @@ + [ + 'renderType' => 'selectIcons', + 'disabled' => true, + ], + 'localizationStateSelector' => [ + 'renderType' => 'localizationStateSelector', + 'after' => [ + 'selectIcons', + ], + ], + 'otherLanguageContent' => [ + 'renderType' => 'otherLanguageContent', + 'after' => [ 'localizationStateSelector' ], + ], + 'defaultLanguageDifferences' => [ + 'renderType' => 'defaultLanguageDifferences', + 'after' => [ 'otherLanguageContent' ], + ], + ]; + + public function __construct( + private readonly InlineStackProcessor $inlineStackProcessor, + ) {} + + /** + * Render single element + * + * @return array As defined in initializeResultArray() of AbstractNode + */ + public function render(): array + { + $resultArray = $this->initializeResultArray(); + + $table = $this->data['tableName']; + $field = $this->data['fieldName']; + $parameterArray = $this->data['parameterArray']; + $config = $parameterArray['fieldConf']['config']; + + $selectItems = $parameterArray['fieldConf']['config']['items']; + $classList = ['form-select', 'form-control-adapt']; + + // Check against inline uniqueness + $uniqueIds = []; + if (($this->data['isInlineChild'] ?? false) && ($this->data['inlineParentUid'] ?? false)) { + // If config[foreign_unique] is set for the parent inline field, all + // already used unique ids must be excluded from the select items. + $inlineObjectName = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $this->data['inlineFirstPid']); + if (($this->data['inlineParentConfig']['foreign_table'] ?? false) === $table + && ($this->data['inlineParentConfig']['foreign_unique'] ?? false) === $field + ) { + $classList[] = 't3js-inline-unique'; + $uniqueIds = $this->data['inlineData']['unique'][$inlineObjectName . '-' . $table]['used'] ?? []; + } + // hide uid of parent record for symmetric relations + if (($this->data['inlineParentConfig']['foreign_table'] ?? false) === $table + && ( + ($this->data['inlineParentConfig']['foreign_field'] ?? false) === $field + || ($this->data['inlineParentConfig']['symmetric_field'] ?? false) === $field + ) + ) { + $uniqueIds[] = $this->data['inlineParentUid']; + } + $uniqueIds = array_map(intval(...), $uniqueIds); + } + + // Initialization: + $selectId = StringUtility::getUniqueId('tceforms-select-'); + $selectedItem = null; + $size = (int)($config['size'] ?? 0); + + // Style set on '; + $html[] = $options; + $html[] = ''; + if ($hasIcons) { + $html[] = '
'; + } + $html[] = ''; + if (!$disabled && !empty($fieldControlHtml)) { + $html[] = '
'; + $html[] = '
'; + $html[] = $fieldControlHtml; + $html[] = '
'; + $html[] = '
'; + } + if (!$disabled && !empty($fieldWizardHtml)) { + $html[] = '
'; + $html[] = $fieldWizardHtml; + $html[] = '
'; + } + $html[] = ''; + $html[] = ''; + $html[] = ''; + + $onFieldChangeItems = $this->getOnFieldChangeItems($parameterArray['fieldChangeFunc'] ?? []); + $resultArray['javaScriptModules']['selectSingleElement'] = JavaScriptModuleInstruction::create( + '@typo3/backend/form-engine/element/select-single-element.js' + )->invoke('initializeOnReady', '#' . $selectId, ['onChange' => $onFieldChangeItems]); + + $resultArray['html'] = implode(LF, $html); + return $resultArray; + } +} diff --git a/Classes/Form/Element/SelectTreeElement.php b/Classes/Form/Element/SelectTreeElement.php new file mode 100644 index 0000000..be7dc06 --- /dev/null +++ b/Classes/Form/Element/SelectTreeElement.php @@ -0,0 +1,201 @@ + [ + 'renderType' => 'localizationStateSelector', + ], + ]; + + /** + * Default number of tree nodes to show (determines tree height) + * when no ['config']['size'] is set + * + * @var int + */ + protected $itemsToShow = 15; + + /** + * Number of items to show at last + * e.g. when you have only 2 items in a tree + * + * @var int + */ + protected $minItemsToShow = 5; + + /** + * Pixel height of a single tree node + * + * @var int + */ + protected $itemHeight = 20; + + /** + * Render tree widget + * + * @return array As defined in initializeResultArray() of AbstractNode + * @see AbstractNode::initializeResultArray() + */ + public function render(): array + { + $resultArray = $this->initializeResultArray(); + $parameterArray = $this->data['parameterArray']; + $formElementId = md5($parameterArray['itemFormElName']); + + // Field configuration from TCA: + $config = $parameterArray['fieldConf']['config']; + $readOnly = !empty($config['readOnly']); + $exclusiveKeys = !empty($config['exclusiveKeys']) ? $config['exclusiveKeys'] : ''; + $exclusiveKeys = $exclusiveKeys . ','; + $appearance = !empty($config['treeConfig']['appearance']) ? $config['treeConfig']['appearance'] : []; + $expanded = !empty($appearance['expandAll']); + $showHeader = !empty($appearance['showHeader']); + if (isset($config['size']) && (int)$config['size'] > 0) { + $height = max($this->minItemsToShow, (int)$config['size']); + } else { + $height = $this->itemsToShow; + } + $heightInPx = $height * $this->itemHeight; + $treeWrapperId = 'tree_' . $formElementId; + $fieldId = 'tree_record_' . $formElementId; + + $fieldName = $this->data['fieldName']; + + $dataStructureIdentifier = ''; + $flexFormSheetName = ''; + $flexFormFieldName = ''; + $flexFormContainerName = ''; + $flexFormContainerIdentifier = ''; + $flexFormContainerFieldName = ''; + $flexFormSectionContainerIsNew = false; + if ($this->data['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') { + $dataStructureIdentifier = $this->data['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier']; + if (isset($this->data['flexFormSheetName'])) { + $flexFormSheetName = $this->data['flexFormSheetName']; + } + if (isset($this->data['flexFormFieldName'])) { + $flexFormFieldName = $this->data['flexFormFieldName']; + } + if (isset($this->data['flexFormContainerName'])) { + $flexFormContainerName = $this->data['flexFormContainerName']; + } + if (isset($this->data['flexFormContainerFieldName'])) { + $flexFormContainerFieldName = $this->data['flexFormContainerFieldName']; + } + if (isset($this->data['flexFormContainerIdentifier'])) { + $flexFormContainerIdentifier = $this->data['flexFormContainerIdentifier']; + } + // Add a flag this is a tree in a new flex section container element. This is needed to initialize + // the databaseRow with this container again so the tree data provider is able to calculate tree items. + if (!empty($this->data['flexSectionContainerPreparation'])) { + $flexFormSectionContainerIsNew = true; + } + } + + $fieldInformationResult = $this->renderFieldInformation(); + $fieldInformationHtml = $fieldInformationResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); + + $fieldWizardResult = $this->renderFieldWizard(); + $fieldWizardHtml = $fieldWizardResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false); + + $html = []; + $html[] = '
'; + $html[] = $fieldInformationHtml; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = 'getValidationDataAsJsonString($config)) . '"'; + $html[] = ' data-relatedfieldname="' . htmlspecialchars($parameterArray['itemFormElName']) . '"'; + $html[] = ' data-tablename="' . htmlspecialchars($this->data['tableName']) . '"'; + $html[] = ' data-fieldname="' . htmlspecialchars($this->data['fieldName']) . '"'; + $html[] = ' data-uid="' . (int)$this->data['vanillaUid'] . '"'; + $html[] = ' data-recordtypevalue="' . htmlspecialchars($this->data['recordTypeValue']) . '"'; + $html[] = ' data-datastructureidentifier="' . htmlspecialchars($dataStructureIdentifier) . '"'; + $html[] = ' data-flexformsheetname="' . htmlspecialchars($flexFormSheetName) . '"'; + $html[] = ' data-flexformfieldname="' . htmlspecialchars($flexFormFieldName) . '"'; + $html[] = ' data-flexformcontainername="' . htmlspecialchars($flexFormContainerName) . '"'; + $html[] = ' data-flexformcontaineridentifier="' . htmlspecialchars($flexFormContainerIdentifier) . '"'; + $html[] = ' data-flexformcontainerfieldname="' . htmlspecialchars($flexFormContainerFieldName) . '"'; + $html[] = ' data-flexformsectioncontainerisnew="' . htmlspecialchars((string)$flexFormSectionContainerIsNew) . '"'; + $html[] = ' data-command="' . htmlspecialchars($this->data['command']) . '"'; + $html[] = ' data-read-only="' . ($readOnly ? '1' : '0') . '"'; + $html[] = ' data-tree-exclusive-keys="' . htmlspecialchars($exclusiveKeys) . '"'; + $html[] = ' data-tree-expand-up-to-level="' . ($expanded ? '999' : '1') . '"'; + $html[] = ' data-tree-show-toolbar="' . $showHeader . '"'; + $html[] = ' name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"'; + $html[] = ' id="treeinput' . $formElementId . '"'; + $html[] = ' value="' . htmlspecialchars(implode(',', $parameterArray['itemFormElValue'])) . '"'; + $html[] = ' data-overridevalues="' . GeneralUtility::jsonEncodeForHtmlAttribute($this->data['overrideValues']) . '"'; + $html[] = ' data-defaultvalues="' . GeneralUtility::jsonEncodeForHtmlAttribute($this->data['defaultValues']) . '"'; + $html[] = '/>'; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + if (!$readOnly && !empty($fieldWizardHtml)) { + $html[] = '
'; + $html[] = $fieldWizardHtml; + $html[] = '
'; + } + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + + $resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html)); + + $onFieldChangeItems = $this->getOnFieldChangeItems($this->getFieldChangeFuncs()); + $resultArray['javaScriptModules']['selectTreeElement'] = JavaScriptModuleInstruction::create( + '@typo3/backend/form-engine/element/select-tree-element.js', + 'SelectTreeElement' + )->instance($treeWrapperId, $fieldId, null, $onFieldChangeItems); + + return $resultArray; + } + + /** + * @return list + */ + protected function getFieldChangeFuncs(): array + { + $items = []; + $parameterArray = $this->data['parameterArray']; + if (!empty($parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'])) { + $items[] = $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged']; + } + if (!empty($parameterArray['fieldChangeFunc']['alert'])) { + $items[] = $parameterArray['fieldChangeFunc']['alert']; + } + return $items; + } +} diff --git a/Classes/Form/Element/TablePermissionElement.php b/Classes/Form/Element/TablePermissionElement.php new file mode 100644 index 0000000..17fb548 --- /dev/null +++ b/Classes/Form/Element/TablePermissionElement.php @@ -0,0 +1,258 @@ + 'none', + 'select' => 'select', + 'modify' => 'modify', + ]; + + public function __construct( + private readonly IconFactory $iconFactory, + ) {} + + public function render(): array + { + $resultArray = $this->initializeResultArray(); + + $parameterArray = $this->data['parameterArray']; + $config = $parameterArray['fieldConf']['config']; + $elementFieldName = $parameterArray['itemFormElName']; + $currentValue = ['modify' => [], 'select' => []]; + if (is_array($parameterArray['itemFormElValue']['modify'] ?? false) + && is_array($parameterArray['itemFormElValue']['select'] ?? false) + ) { + $currentValue = $parameterArray['itemFormElValue']; + } + $readOnly = (bool)($config['readOnly'] ?? false); + + $availableTables = $config['items'] ?? []; + if (empty($availableTables)) { + // Early return in case the field does not contain any items + return $resultArray; + } + + $tablesConfiguration = []; + $lang = $this->getLanguageService(); + $itemArrayModify = array_flip($currentValue['modify']); + $itemArraySelect = array_flip($currentValue['select']); + $elementId = StringUtility::getUniqueId('formengine-table-permission-'); + + foreach ($availableTables as $table) { + $permissions = []; + foreach (self::Permissions as $permission) { + $permissions[$permission] = [ + 'label' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_groups.tables_modify.permissions.' . $permission), + 'attributes' => [ + 'type' => 'radio', + 'class' => 'form-check-input t3js-table-permissions-item t3js-multi-record-selection-check', + 'value' => $permission, + 'name' => $elementId . '[' . $table['value'] . ']', + 'id' => $elementId . '[' . $table['value'] . '][' . $permission . ']', + 'data-table' => $table['value'], + ], + ]; + } + + if (isset($itemArrayModify[$table['value']])) { + $permissions[self::Permissions['modify']]['attributes']['checked'] = 'checked'; + } elseif (isset($itemArraySelect[$table['value']])) { + $permissions[self::Permissions['select']]['attributes']['checked'] = 'checked'; + } else { + $permissions[self::Permissions['none']]['attributes']['checked'] = 'checked'; + } + + if ($readOnly) { + foreach (self::Permissions as $permission) { + $permissions[$permission]['attributes']['disabled'] = 'disabled'; + } + } + + $tablesConfiguration[] = [ + 'permissions' => $permissions, + 'label' => [ + 'id' => $elementId . '-' . $table['value'] . '-label', + 'icon' => $this->getIconForTable(!empty($table['icon']) ? $table['icon'] : 'empty-empty'), + 'title' => $lang->sL($table['label']), + 'value' => $table['value'], + ], + ]; + } + + $modifyStateFieldName = htmlspecialchars($elementFieldName); + $selectStateFieldName = htmlspecialchars(str_replace($this->data['fieldName'], $config['selectFieldName'], $elementFieldName)); + + $fieldInformationResult = $this->renderFieldInformation(); + $fieldInformationHtml = $fieldInformationResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); + + $html[] = ''; + $html[] = '
'; + $html[] = $fieldInformationHtml; + + if (!$readOnly) { + $html[] = ''; + $html[] = ''; + } + + $tableRows = []; + foreach ($tablesConfiguration as $tableConfiguration) { + $tableRows[] = ''; + foreach ($tableConfiguration['permissions'] as $key => $permission) { + $tableRows[] = ''; + $tableRows[] = '
'; + $tableRows[] = ''; + $tableRows[] = ''; + $tableRows[] = '
'; + $tableRows[] = ''; + } + $tableRows[] = ''; + $tableRows[] = ''; + $tableRows[] = ''; + $tableRows[] = ''; + } + + $html[] = '
'; + $html[] = '
'; + $html[] = ''; + $html[] = ''; + $html[] = ''; + foreach (self::Permissions as $permission) { + $html[] = ''; + } + $html[] = ''; + $html[] = ''; + $html[] = ''; + + $html[] = '' . implode(LF, $tableRows) . ''; + $html[] = '
'; + $html[] = $this->getRecordSelectionCheckActions($permission === self::Permissions['none'] ? ['all'] : ['all', 'none', 'toggle'], $readOnly); + $html[] = '' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.th.name')) . '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + + if (!$readOnly) { + $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/element/table-permission-element.js'); + $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/multi-record-selection.js'); + } + + $html[] = '
'; + + $resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html)); + return $resultArray; + } + + protected function wrapWithFieldsetAndLegend(string $innerHTML): string + { + $legend = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_groups.tables_modify')); + if ($this->getBackendUser()->shallDisplayDebugInformation()) { + $legend .= ' [' . ($this->data['parameterArray']['fieldConf']['config']['selectFieldName'] ?? '') . ', ' . $this->data['fieldName'] . ']'; + } + $html = []; + $html[] = '
'; + $html[] = '' . $legend . ''; + $html[] = $innerHTML; + $html[] = '
'; + return implode(LF, $html); + } + + private function getIconForTable(string $icon): string + { + return FormEngineUtility::getIconHtml($icon); + } + + private function getRecordSelectionCheckActions(array $optionsToShow, bool $readOnly): string + { + $checkboxOptions = [ + 'all' => ' +
  • + +
  • ', + 'none' => ' +
  • + +
  • ', + 'toggle' => ' +
  • + +
  • ', + ]; + + $checkboxOptions = array_filter($checkboxOptions, static fn($checkboxOption) => in_array($checkboxOption, $optionsToShow, true), ARRAY_FILTER_USE_KEY); + if ($checkboxOptions === []) { + return ''; + } + + return ' + '; + } +} diff --git a/Classes/Form/Element/TextElement.php b/Classes/Form/Element/TextElement.php new file mode 100644 index 0000000..0689155 --- /dev/null +++ b/Classes/Form/Element/TextElement.php @@ -0,0 +1,310 @@ + [ + 'renderType' => 'localizationStateSelector', + ], + 'otherLanguageContent' => [ + 'renderType' => 'otherLanguageContent', + 'after' => [ + 'localizationStateSelector', + ], + ], + 'defaultLanguageDifferences' => [ + 'renderType' => 'defaultLanguageDifferences', + 'after' => [ + 'otherLanguageContent', + ], + ], + ]; + + /** + * The number of chars expected per row when the height of a text area field is + * automatically calculated based on the number of characters found in the field content. + * + * @var int + */ + protected $charactersPerRow = 40; + + /** + * This will render a + + +
    + +
    + + + + diff --git a/Resources/Private/Templates/LinkBrowser/Page.fluid.html b/Resources/Private/Templates/LinkBrowser/Page.fluid.html new file mode 100644 index 0000000..5224d28 --- /dev/null +++ b/Resources/Private/Templates/LinkBrowser/Page.fluid.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + {activePageIcon} {activePageTitle} + + + + +
    {activePageIcon -> f:format.raw()} {activePageTitle -> f:format.raw()}
    +
    +
    + + +

    + +
    +
    +
    + + diff --git a/Resources/Private/Templates/LinkBrowser/Record.fluid.html b/Resources/Private/Templates/LinkBrowser/Record.fluid.html new file mode 100644 index 0000000..812f4f5 --- /dev/null +++ b/Resources/Private/Templates/LinkBrowser/Record.fluid.html @@ -0,0 +1,21 @@ + + + + + + + + + + + {recordList -> f:format.raw()} + + + diff --git a/Resources/Private/Templates/LinkBrowser/Telephone.fluid.html b/Resources/Private/Templates/LinkBrowser/Telephone.fluid.html new file mode 100644 index 0000000..a6ede50 --- /dev/null +++ b/Resources/Private/Templates/LinkBrowser/Telephone.fluid.html @@ -0,0 +1,29 @@ + + + + + +
    +
    + +
    + + +
    +
    +
    +
    + + diff --git a/Resources/Private/Templates/LinkBrowser/Url.fluid.html b/Resources/Private/Templates/LinkBrowser/Url.fluid.html new file mode 100644 index 0000000..ff355ba --- /dev/null +++ b/Resources/Private/Templates/LinkBrowser/Url.fluid.html @@ -0,0 +1,30 @@ + + + + + +
    +
    + +
    + + +
    +
    +
    +
    + + diff --git a/Resources/Private/Templates/ListNavigation.fluid.html b/Resources/Private/Templates/ListNavigation.fluid.html new file mode 100644 index 0000000..649a30a --- /dev/null +++ b/Resources/Private/Templates/ListNavigation.fluid.html @@ -0,0 +1,56 @@ + + + + + + + + + diff --git a/Resources/Private/Templates/LiveSearch/Form.fluid.html b/Resources/Private/Templates/LiveSearch/Form.fluid.html new file mode 100644 index 0000000..6975b71 --- /dev/null +++ b/Resources/Private/Templates/LiveSearch/Form.fluid.html @@ -0,0 +1,50 @@ + + + +
    +
    + + + + + +
    +
    + +
    + + +
    + + +
    + diff --git a/Resources/Private/Templates/Login/ForgetPasswordForm.fluid.html b/Resources/Private/Templates/Login/ForgetPasswordForm.fluid.html new file mode 100644 index 0000000..20ff575 --- /dev/null +++ b/Resources/Private/Templates/Login/ForgetPasswordForm.fluid.html @@ -0,0 +1,64 @@ + + + + + + + + + diff --git a/Resources/Private/Templates/Login/ResetPasswordForm.fluid.html b/Resources/Private/Templates/Login/ResetPasswordForm.fluid.html new file mode 100644 index 0000000..e327ffe --- /dev/null +++ b/Resources/Private/Templates/Login/ResetPasswordForm.fluid.html @@ -0,0 +1,93 @@ + + + + + + + + + diff --git a/Resources/Private/Templates/Login/UserPassLoginForm.fluid.html b/Resources/Private/Templates/Login/UserPassLoginForm.fluid.html new file mode 100644 index 0000000..15c0133 --- /dev/null +++ b/Resources/Private/Templates/Login/UserPassLoginForm.fluid.html @@ -0,0 +1,68 @@ + + + + + + + + + + + + +
    +
    + +
    +
    +
    + + diff --git a/Resources/Private/Templates/Mfa/Auth.fluid.html b/Resources/Private/Templates/Mfa/Auth.fluid.html new file mode 100644 index 0000000..da852d0 --- /dev/null +++ b/Resources/Private/Templates/Mfa/Auth.fluid.html @@ -0,0 +1,83 @@ + + + + + diff --git a/Resources/Private/Templates/Mfa/Edit.fluid.html b/Resources/Private/Templates/Mfa/Edit.fluid.html new file mode 100644 index 0000000..d4af912 --- /dev/null +++ b/Resources/Private/Templates/Mfa/Edit.fluid.html @@ -0,0 +1,56 @@ + + + + + + + + +

    +

    {provider.description -> f:translate(key: provider.description, default: provider.description)}

    + +
    + +
    + {providerContent -> f:format.raw()} +
    +
    + +
    +
    +

    +

    +
    + + +
    +
    +
    +
    +
    +
    +

    +

    + + + + +
    +
    +
    +
    + +
    + + diff --git a/Resources/Private/Templates/Mfa/Overview.fluid.html b/Resources/Private/Templates/Mfa/Overview.fluid.html new file mode 100644 index 0000000..352f623 --- /dev/null +++ b/Resources/Private/Templates/Mfa/Overview.fluid.html @@ -0,0 +1,105 @@ + + + + + +

    + + + + + + +

    +
    +
    + + +

    +
    +
    +
    +
    + + +

    +
    +
    +
    + + + +
    + + + +
    +
    +
    +
    + + + +
    +
    +
    + +
    +
    +

    + {providerTitle} + + + + + + + + + + +

    +
    +
    +
    +

    {provider.description -> f:translate(key: provider.description, default: provider.description)}

    +
    + +
    +
    + + diff --git a/Resources/Private/Templates/Mfa/Setup.fluid.html b/Resources/Private/Templates/Mfa/Setup.fluid.html new file mode 100644 index 0000000..66e47fa --- /dev/null +++ b/Resources/Private/Templates/Mfa/Setup.fluid.html @@ -0,0 +1,33 @@ + + + + + + +

    + +
    +
    +
    + + {providerContent -> f:format.raw()} +
    +
    + +
    + +
    +
    +
    + +
    + + diff --git a/Resources/Private/Templates/Mfa/Standalone/Selection.fluid.html b/Resources/Private/Templates/Mfa/Standalone/Selection.fluid.html new file mode 100644 index 0000000..39db9a6 --- /dev/null +++ b/Resources/Private/Templates/Mfa/Standalone/Selection.fluid.html @@ -0,0 +1,79 @@ + + + + + + +
    +
    + +
    +
    + +
    +
    +
    +
    + + diff --git a/Resources/Private/Templates/Mfa/Standalone/Setup.fluid.html b/Resources/Private/Templates/Mfa/Standalone/Setup.fluid.html new file mode 100644 index 0000000..dbb39cc --- /dev/null +++ b/Resources/Private/Templates/Mfa/Standalone/Setup.fluid.html @@ -0,0 +1,68 @@ + + + + + diff --git a/Resources/Private/Templates/NewContentElement/PositionMap.fluid.html b/Resources/Private/Templates/NewContentElement/PositionMap.fluid.html new file mode 100644 index 0000000..f484d42 --- /dev/null +++ b/Resources/Private/Templates/NewContentElement/PositionMap.fluid.html @@ -0,0 +1,10 @@ + + +
    {f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:2_selectPosition') -> f:format.htmlspecialchars()}
    +
    + {posMap -> f:format.raw()} +
    + diff --git a/Resources/Private/Templates/NewContentElement/Wizard.fluid.html b/Resources/Private/Templates/NewContentElement/Wizard.fluid.html new file mode 100644 index 0000000..01fe9a7 --- /dev/null +++ b/Resources/Private/Templates/NewContentElement/Wizard.fluid.html @@ -0,0 +1,19 @@ + + + +
    {f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:1_selectType')}
    +
    + + + + + diff --git a/Resources/Private/Templates/NewRecord/NewRecord.fluid.html b/Resources/Private/Templates/NewRecord/NewRecord.fluid.html new file mode 100644 index 0000000..086c783 --- /dev/null +++ b/Resources/Private/Templates/NewRecord/NewRecord.fluid.html @@ -0,0 +1,62 @@ + + + + + + +

    + +
    +
    + {recordTypeGroup.icon -> f:format.raw()} {recordTypeGroup.title} +
    + + + +
    + + +
    +
    + + + {recordTypeItem.icon -> f:format.raw()} + {recordTypeItem.label} + + + + + {recordTypeItem.icon -> f:format.raw()} + {recordTypeItem.label} + + +
    +
    +
    +
    +
    +
    + +
    + + diff --git a/Resources/Private/Templates/Page/MovePage.fluid.html b/Resources/Private/Templates/Page/MovePage.fluid.html new file mode 100644 index 0000000..3e50a32 --- /dev/null +++ b/Resources/Private/Templates/Page/MovePage.fluid.html @@ -0,0 +1,74 @@ + + + + + +

    + +

    +

    {element.recordPath}

    + + +
    +
    + + +
    +
    + +
    + +
    + +
    +
    + + + +
    +
    + + {target.recordTitle} [{target.record.uid}] +
    +
    + : {target.recordPath} +
    +
    +
    + + + + + +
    + + + + +
    + +
    +
    + +
    + +
    +
    +
    + +
    + + diff --git a/Resources/Private/Templates/Page/NewPages.fluid.html b/Resources/Private/Templates/Page/NewPages.fluid.html new file mode 100644 index 0000000..f0d78db --- /dev/null +++ b/Resources/Private/Templates/Page/NewPages.fluid.html @@ -0,0 +1,282 @@ + + + + + + + + + +

    + +

    + + + + + + + + + + + + + + + + + + +

    + +

    +
    + + + + + + + + + + + + + + + +
    + + + + {page.title -> f:format.crop(maxCharacters: maxTitleLength)} + + +
    + + + +
    +
    +
    +
    +
    + + + + + +

    + +

    + + + +
    +
    + + + + +

    + + , {label} +

    + + + +
    +
    + + +
    +
    + + + + + +
    + +
    + +
    + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/Resources/Private/Templates/Page/SortSubPages.fluid.html b/Resources/Private/Templates/Page/SortSubPages.fluid.html new file mode 100644 index 0000000..bb81a4e --- /dev/null +++ b/Resources/Private/Templates/Page/SortSubPages.fluid.html @@ -0,0 +1,163 @@ + + + + + + + + + +

    + +

    + + + + + + + + + + + + +

    + +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + +
    + + + + + + + {page.record.title -> f:format.crop(maxCharacters: maxTitleLength)} + + {page.record.subtitle -> f:format.crop(maxCharacters: maxTitleLength)} + + {page.record.nav_title -> f:format.crop(maxCharacters: maxTitleLength)} + + {page.record.tstamp} + + {page.record.crdate} +
    +
    + +

    + +

    + + + + + + + + + + + + + + + + + + +
    + + + +
    +
    +
    + +
    + + diff --git a/Resources/Private/Templates/PageLayout/FluidBasedContentPreviewRenderingException.fluid.html b/Resources/Private/Templates/PageLayout/FluidBasedContentPreviewRenderingException.fluid.html new file mode 100644 index 0000000..1253ba2 --- /dev/null +++ b/Resources/Private/Templates/PageLayout/FluidBasedContentPreviewRenderingException.fluid.html @@ -0,0 +1,11 @@ + + + {error.message} + + diff --git a/Resources/Private/Templates/PageLayout/PageLayout.fluid.html b/Resources/Private/Templates/PageLayout/PageLayout.fluid.html new file mode 100644 index 0000000..a26e102 --- /dev/null +++ b/Resources/Private/Templates/PageLayout/PageLayout.fluid.html @@ -0,0 +1,9 @@ +{namespace be=TYPO3\CMS\Backend\ViewHelpers} + + + + + + + + diff --git a/Resources/Private/Templates/PageLayout/PageModule.fluid.html b/Resources/Private/Templates/PageLayout/PageModule.fluid.html new file mode 100644 index 0000000..ebcfd3f --- /dev/null +++ b/Resources/Private/Templates/PageLayout/PageModule.fluid.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + {localizedPageTitle} + + + + + + {infoBox.message} + + + + {eventContentHtmlTop} + +
    + {mainContentHtml} +
    + + {eventContentHtmlBottom} + +
    + + diff --git a/Resources/Private/Templates/PageLayout/PageModuleNoAccess.fluid.html b/Resources/Private/Templates/PageLayout/PageModuleNoAccess.fluid.html new file mode 100644 index 0000000..3d94005 --- /dev/null +++ b/Resources/Private/Templates/PageLayout/PageModuleNoAccess.fluid.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + +

    {siteName}

    + + +
    + + diff --git a/Resources/Private/Templates/PageLayout/UnusedRecords.fluid.html b/Resources/Private/Templates/PageLayout/UnusedRecords.fluid.html new file mode 100644 index 0000000..2831ac9 --- /dev/null +++ b/Resources/Private/Templates/PageLayout/UnusedRecords.fluid.html @@ -0,0 +1 @@ + diff --git a/Resources/Private/Templates/PageTsConfig/Active.fluid.html b/Resources/Private/Templates/PageTsConfig/Active.fluid.html new file mode 100644 index 0000000..6b8bb3a --- /dev/null +++ b/Resources/Private/Templates/PageTsConfig/Active.fluid.html @@ -0,0 +1,384 @@ + + + + + + + + + + + + + + + +

    + +

    +

    + + + + + +

    +
    + +
    +
    + +

    + + + + + +
    + + + + +
    +
    +
    +
    + + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    + +
    + + +
    +
    +
    +
    +
    +
    + +
    + + +
    +
    +
    +
    +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    + + +
    + + +
    +
    +
    +
    +
    +
    +
    + + +
    + +
    +
    +
      + +
    +
    +
    +
    +
    + + + + +
  • +
    + +
    {comment}
    +
    +
    +
  • +
    + +
  • + + + {child.originalValueTokenStream} + + + +
  • +
    +
  • + + + + + {child.name} + + = + {child.value} + + + =< + {child.referenceSourceStream} + + + +
    +
      + +
    +
    +
    +
  • +
    +
    + + diff --git a/Resources/Private/Templates/PageTsConfig/Includes.fluid.html b/Resources/Private/Templates/PageTsConfig/Includes.fluid.html new file mode 100644 index 0000000..2811a5b --- /dev/null +++ b/Resources/Private/Templates/PageTsConfig/Includes.fluid.html @@ -0,0 +1,264 @@ + + + + + + + + + + + + + + + +

    + +

    +

    + + +

    +
    + +
    +
    + + +

    +
    + + +
    +
    +
    + + +
    + +
    +
    +
      + +
    +
    +
    +
    +
    + + + + +
  • + + + +
    +
    +
    +
    + + + + + + + + {child.lineStream} + + + {child.lineStream} + + + {child.lineStream} + + + {child.name} + + + + +
    +
    + + + + + + + + +
    +
    +
    +
    +
    + + + + + + +
    +
    +
    + + +
      + +
    +
    +
  • +
    +
    +
    + + + +
    + +
    +
    + +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/Resources/Private/Templates/PageTsConfig/RecordsOverview.fluid.html b/Resources/Private/Templates/PageTsConfig/RecordsOverview.fluid.html new file mode 100644 index 0000000..78f81d7 --- /dev/null +++ b/Resources/Private/Templates/PageTsConfig/RecordsOverview.fluid.html @@ -0,0 +1,76 @@ + + + + + + + + + + + + + +

    + +

    +

    + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + + + {line.pageTitle} + + {line.lines}
    +
    +
    +
    +
    +
    + +
    + + diff --git a/Resources/Private/Templates/RecordDownloadSettings.fluid.html b/Resources/Private/Templates/RecordDownloadSettings.fluid.html new file mode 100644 index 0000000..9866818 --- /dev/null +++ b/Resources/Private/Templates/RecordDownloadSettings.fluid.html @@ -0,0 +1,105 @@ + + +
    + +

    +
    +
    + +
    + +
    +
    + + +
    +
    +
    + +
    + +
    +
    + + +
    +
    + +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + + +
    +

    +
    + +
    + + +
    +
    +
    +
    +
    +
    + + + + + + + +
    + + diff --git a/Resources/Private/Templates/RecordHistory/Main.fluid.html b/Resources/Private/Templates/RecordHistory/Main.fluid.html new file mode 100644 index 0000000..41c449d --- /dev/null +++ b/Resources/Private/Templates/RecordHistory/Main.fluid.html @@ -0,0 +1,25 @@ + + + + + + +

    + {f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:title')} + + + {recordTableReadable} "{recordTitle}" + +

    + + + + + + +
    + + diff --git a/Resources/Private/Templates/RecordList.fluid.html b/Resources/Private/Templates/RecordList.fluid.html new file mode 100644 index 0000000..b53bdad --- /dev/null +++ b/Resources/Private/Templates/RecordList.fluid.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + {pageTitle} + + {additionalContentTop} + {searchBoxHtml} + {pageTranslationsHtml} + {tableListHtml} + {clipboardHtml} + {additionalContentBottom} + + + diff --git a/Resources/Private/Templates/RecordSearchBox.fluid.html b/Resources/Private/Templates/RecordSearchBox.fluid.html new file mode 100644 index 0000000..fc2ba21 --- /dev/null +++ b/Resources/Private/Templates/RecordSearchBox.fluid.html @@ -0,0 +1,41 @@ +
    +
    +
    + + + + + + + +
    +
    +
    diff --git a/Resources/Private/Templates/Security/CspModule.fluid.html b/Resources/Private/Templates/Security/CspModule.fluid.html new file mode 100644 index 0000000..22537e1 --- /dev/null +++ b/Resources/Private/Templates/Security/CspModule.fluid.html @@ -0,0 +1,76 @@ + + + + + + + + + +

    +

    + + + + + + + + + + + + + +

    + + + +

    + +

    +
      + + +
    • security.{scope}.{disposition}ContentSecurityPolicy
    • +
      +
      +
    +
    +
    + + + +

    + +

    +
      + +
    • $GLOBALS['TYPO3_CONF_VARS']['{key}']['contentSecurityPolicyReportingUrl'] = '{value}'
    • +
      +
    +
    +
    + + + + +
    + + diff --git a/Resources/Private/Templates/Setup/Main.fluid.html b/Resources/Private/Templates/Setup/Main.fluid.html new file mode 100644 index 0000000..b53edbe --- /dev/null +++ b/Resources/Private/Templates/Setup/Main.fluid.html @@ -0,0 +1,32 @@ + + + + + + +

    + +

    + +
    + {formEngineHtml} + +
    + + + +
    +
    +
    + + diff --git a/Resources/Private/Templates/SiteConfiguration/Detail.fluid.html b/Resources/Private/Templates/SiteConfiguration/Detail.fluid.html new file mode 100644 index 0000000..812e40e --- /dev/null +++ b/Resources/Private/Templates/SiteConfiguration/Detail.fluid.html @@ -0,0 +1,577 @@ + + + + + + + + + + + + + + + +

    + + + + + + + + +

    + + + + + +

    + +

    + + +
    + +
    +
    + + + + + + + + + + + + + + + + +
    + + + +
    + + + + + + + + + + + + {language.title} [{language.languageId}] + + + + {language.title} (disabled) + [{language.languageId}] + + + + + {language.base} +
    +
    +
    +
    + + + + +
    + +
    +
    +
      + +
    • + + + + + +
        + + + + + + + + + + + + + + + + + + + + + +
      +
    • +
      +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + + + + +
    + +
    +
    +
      + +
    • + + + {route.route} + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
    • +
      +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + + + +
    + +
    +
    +
      + +
    • + + + + {enhancerName} + + +
      + +
      +
    • +
      +
    +
    +
    +
    +
    + +

    + +

    + + + + + +

    + +

    +

    + +

    +
      + +
    • {set}
    • +
      +
    +
    + +

    + +

    + + + +
    + +
    +
    +
      + +
    • + + + {key} + + = + + {setting -> f:format.json()} + + +
    • +
      +
    +
    +
    +
    +
    + + + + +
    + +
    +
    + + + +
      + +
    • + + {section.label} + + +
    • +
      +
    +
    + + + + +
    +
    +
    +
    +
    +
    +
    + + + + + +
    +
    +
    + + +
      + + +
    • +
      +
      + # {setting.definition.label} +
      +
      +
    • +
      +
    • + + + {setting.definition.key} + + = + + {setting.renderedValue} + + +
    • +
      +
    +
    + + +
  • + + + {label} + + = + + {value} + + +
  • +
    + + +
      + + + +
    +
    + + + + +
  • + + + {label} + +
    +
      + + + +
    +
    +
  • +
    + + + +
    +
    diff --git a/Resources/Private/Templates/SiteConfiguration/Edit.fluid.html b/Resources/Private/Templates/SiteConfiguration/Edit.fluid.html new file mode 100644 index 0000000..0c7bfae --- /dev/null +++ b/Resources/Private/Templates/SiteConfiguration/Edit.fluid.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + +
    + {formEngineHtml -> f:format.raw()} + + + + +
    + +
    + + diff --git a/Resources/Private/Templates/SiteConfiguration/Overview.fluid.html b/Resources/Private/Templates/SiteConfiguration/Overview.fluid.html new file mode 100644 index 0000000..f415890 --- /dev/null +++ b/Resources/Private/Templates/SiteConfiguration/Overview.fluid.html @@ -0,0 +1,633 @@ + + + + + + + + + + +

    + + + +
      + +
    • + + + +
    • +
      + +
    • + + + +
    • +
      + +
    • + + + +
    • +
      +
    +
    +
    + +

    + + +

    + +

    +

    + + + +

    +
    +
    + + + + + +
      + +
    • + + + + +
        + +
      • {duplicateSite}
      • +
        +
      +
      +
    • +
      +
    +
    +
    + + + + +
      + +
    • + {item} + +
        + +
      • {schema} ({count})
      • +
        +
      +
      +
    • +
      +
    +
    +
    + + +
    + + +

    + +

    +
    +
    +
    + + +

    {f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:overview.rootPagesWithoutSiteConfiguration.title')}

    +

    + +

    + +
    + + + + + + + + + + + + + + + + +
     
    + + + {page.title} + +
    + + + + +
    +
    +
    +
    + + +

    {f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:overview.unassignedSites.title')}

    +

    + +

    + +
    + + + + + + + + + + + + + + + +
     
    + {unassignedSite.identifier} + +
    + +
    +
    +
    +
    + + + +

    {f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:overview.invalidSets.description')}

    + +
    + + + + + + + + + + + + + + + +
    {invalidSet.name} + +
    +
    +
    +
    + + +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
     
    + + + + + + {rootPage.title} + + + + {rootPage.title} + + + + + + {page.siteIdentifier} + + +
    + + + +
    +
    +
    + + + +
    + + +
    + + + {siteLanguage.title} [{siteLanguage.languageId}] + + + + {siteLanguage.title} (disabled) [{siteLanguage.languageId}] + + +
    +
    +
    +
    + + + + + + +
    + + + + + + + + + + {f:be.uri(route: 'site_configuration')} + + + + + + + + + + +
    +
    +
    +
    + + +
    + + + + + + + +
    +
    +
    + +
    +
    +

    + {rootPage.title} +

    + + + {page.siteIdentifier} + + +
    +
    + +
      +
    • + +
    • +
    • +
      + + : + +
      + {page.siteConfiguration.allLanguages.0.title} + + + + +
      +
      +
    • +
    • +
      + + : + +
      + + +
      + + + + +
      +
      + +
      +
      + + + +
      +
      +
      + + + +
    • +
    • +
      + + : + +
      + + + + {errorHandling.errorCode}{f:if(condition: '{errorHandlingIterator.isLast}', then: '', else: ', ')} + + + + + + +
      +
      +
    • +
    • +
      + + : + +
      + + + + {route.route}{f:if(condition: '{routeIterator.isLast}', then: '', else: ', ')} + + + + + + +
      +
      +
    • +
    +
    + +
    +
    +
    +
    + + +
    + + + + +
    +
    + +
    +
    + + diff --git a/Resources/Private/Templates/SiteSettings/Edit.fluid.html b/Resources/Private/Templates/SiteSettings/Edit.fluid.html new file mode 100644 index 0000000..c67bf7d --- /dev/null +++ b/Resources/Private/Templates/SiteSettings/Edit.fluid.html @@ -0,0 +1,45 @@ + + + + + + + + + + + +

    + +

    + + + + + + + + + + + + + + +
    diff --git a/Resources/Private/Templates/SubmoduleOverview/Cards.fluid.html b/Resources/Private/Templates/SubmoduleOverview/Cards.fluid.html new file mode 100644 index 0000000..3421d22 --- /dev/null +++ b/Resources/Private/Templates/SubmoduleOverview/Cards.fluid.html @@ -0,0 +1,75 @@ + + + + + + + +

    {f:translate(key: currentModule.title, default: currentModule.title)}

    + + +

    {f:translate(key: currentModule.description, default: currentModule.description) -> f:transform.html()}

    +
    + +

    {f:translate(key: currentModule.shortDescription, default: currentModule.shortDescription)}

    +
    +
    + + +
    + +
    +
    +
    + +
    +
    +

    {f:translate(key: subModule.title, default: subModule.title)}

    + + {f:translate(key: subModule.shortDescription, default: subModule.shortDescription)} + +
    +
    +
    + +

    {f:translate(key: subModule.description, default: subModule.description)}

    +
    +
    + +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + diff --git a/Resources/Private/Templates/SudoMode/Error.fluid.html b/Resources/Private/Templates/SudoMode/Error.fluid.html new file mode 100644 index 0000000..f94cce5 --- /dev/null +++ b/Resources/Private/Templates/SudoMode/Error.fluid.html @@ -0,0 +1,12 @@ + + + + diff --git a/Resources/Private/Templates/SudoMode/Module.fluid.html b/Resources/Private/Templates/SudoMode/Module.fluid.html new file mode 100644 index 0000000..f59b875 --- /dev/null +++ b/Resources/Private/Templates/SudoMode/Module.fluid.html @@ -0,0 +1,11 @@ + + + + diff --git a/Resources/Private/Templates/ToolbarItems/BookmarkToolbarItemDropDown.fluid.html b/Resources/Private/Templates/ToolbarItems/BookmarkToolbarItemDropDown.fluid.html new file mode 100644 index 0000000..4d85d88 --- /dev/null +++ b/Resources/Private/Templates/ToolbarItems/BookmarkToolbarItemDropDown.fluid.html @@ -0,0 +1 @@ + diff --git a/Resources/Private/Templates/ToolbarItems/BookmarkToolbarItemItem.fluid.html b/Resources/Private/Templates/ToolbarItems/BookmarkToolbarItemItem.fluid.html new file mode 100644 index 0000000..c0c4bb2 --- /dev/null +++ b/Resources/Private/Templates/ToolbarItems/BookmarkToolbarItemItem.fluid.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/Resources/Private/Templates/ToolbarItems/ClearCacheToolbarItem.fluid.html b/Resources/Private/Templates/ToolbarItems/ClearCacheToolbarItem.fluid.html new file mode 100644 index 0000000..3fc6655 --- /dev/null +++ b/Resources/Private/Templates/ToolbarItems/ClearCacheToolbarItem.fluid.html @@ -0,0 +1,7 @@ + + + + diff --git a/Resources/Private/Templates/ToolbarItems/ClearCacheToolbarItemDropDown.fluid.html b/Resources/Private/Templates/ToolbarItems/ClearCacheToolbarItemDropDown.fluid.html new file mode 100644 index 0000000..efc3c98 --- /dev/null +++ b/Resources/Private/Templates/ToolbarItems/ClearCacheToolbarItemDropDown.fluid.html @@ -0,0 +1,21 @@ + + + + diff --git a/Resources/Private/Templates/ToolbarItems/ClearCacheToolbarItemSingle.fluid.html b/Resources/Private/Templates/ToolbarItems/ClearCacheToolbarItemSingle.fluid.html new file mode 100644 index 0000000..a0a3474 --- /dev/null +++ b/Resources/Private/Templates/ToolbarItems/ClearCacheToolbarItemSingle.fluid.html @@ -0,0 +1,6 @@ + + + + diff --git a/Resources/Private/Templates/ToolbarItems/LiveSearchToolbarItem.fluid.html b/Resources/Private/Templates/ToolbarItems/LiveSearchToolbarItem.fluid.html new file mode 100644 index 0000000..3df11e1 --- /dev/null +++ b/Resources/Private/Templates/ToolbarItems/LiveSearchToolbarItem.fluid.html @@ -0,0 +1,14 @@ + + + + + + diff --git a/Resources/Private/Templates/ToolbarItems/SystemInformationDropDown.fluid.html b/Resources/Private/Templates/ToolbarItems/SystemInformationDropDown.fluid.html new file mode 100644 index 0000000..dcc2d97 --- /dev/null +++ b/Resources/Private/Templates/ToolbarItems/SystemInformationDropDown.fluid.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/Private/Templates/ToolbarItems/SystemInformationToolbarItem.fluid.html b/Resources/Private/Templates/ToolbarItems/SystemInformationToolbarItem.fluid.html new file mode 100644 index 0000000..6bc6c7d --- /dev/null +++ b/Resources/Private/Templates/ToolbarItems/SystemInformationToolbarItem.fluid.html @@ -0,0 +1,8 @@ + + + + + diff --git a/Resources/Private/Templates/ToolbarItems/UserToolbarItem.fluid.html b/Resources/Private/Templates/ToolbarItems/UserToolbarItem.fluid.html new file mode 100644 index 0000000..ed544da --- /dev/null +++ b/Resources/Private/Templates/ToolbarItems/UserToolbarItem.fluid.html @@ -0,0 +1,19 @@ + + + + + + {f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:toolbarItems.user.account.switchToUserMode.account.prefix')} + {f:if(condition: '{currentUser.realName}', then: '{currentUser.realName} ({currentUser.username})', else: '{currentUser.username}')} + + + + + {f:if(condition: '{currentUser.realName}', then: '{currentUser.realName}', else: '{currentUser.username}')} + + + + diff --git a/Resources/Private/Templates/ToolbarItems/UserToolbarItemDropDown.fluid.html b/Resources/Private/Templates/ToolbarItems/UserToolbarItemDropDown.fluid.html new file mode 100644 index 0000000..cb1770f --- /dev/null +++ b/Resources/Private/Templates/ToolbarItems/UserToolbarItemDropDown.fluid.html @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + +
      + +
    • + + + + + {f:if(condition: user.realName, then: user.realName, else: user.username)} + + + +
    • +
      +
    +
    + + + + + + + + + + + + + + diff --git a/Resources/Private/tsref.xml b/Resources/Private/tsref.xml new file mode 100644 index 0000000..ee29734 --- /dev/null +++ b/Resources/Private/tsref.xml @@ -0,0 +1,5786 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -params + NOTE: This applies ONLY if "CARRAY +TDParams" is set to be data type +This property is used only in some cases where CARRAY is used. Please look out for a note about that in the various cases. +]]> + + + + + + + + + + + + + + + + key = helloWorld +helloWorld = TEXT + +helloWorld.value = this item will be returned + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -params + Additional parameters to all links in TYPO3 (excluding menu-links) +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + tag in the header of the document. Set this to the value that is expected to be the URL, and append a "/" to the end of the string. + +Example: +config.baseURL = https://typo3.org/sub_dir/]]> + + + + on page into the cache lifetime calculation of page , add the following TypoScript: +config.cache. = : + +Thus, if you want to include the fe_users records on page 2 in the cache lifetime calculation for page 10, add the following TypoScript: +config.cache.10 = fe_users:2 + +Multiple record sources can be added as comma-separated list, e.g. +config.cache.10 = fe_users:2,tt_news:11 + +In order to consider records for the cache lifetime of all pages, use the *all* keyword: +config.cache.all = fe_users:2]]> + + + + + + + + + + + + + + + + + + + + .... to the tag !! +Use this feature in templates supplying other content-types than HTML. That could be an image or a WAP-page!]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + section of the page. Use this to insert a note like that "Programmed by My-Agency" ...]]> + + + + tag on the page. If you set "config.doctype" to a keyword enabling XHTML then some attributes are already set. This property allows you to override any preset attributes with you own content if needed. + +Special: If you set it to "none" then no attributes will be set at any event. + +Example: +config.htmlTag_setParams =  xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US"]]> + + + + + + + + + + + + + + + + + + + + Install>Update wizard): +compatibility mode < 4.0:   0 +compatibility mode >= 4.0:   1 + +Example: +config.inlineStyle2TempFile = 1]]> + + + + + + + + + + + + + + + + Displaying workspace named "%s" (number %s)! +config.message_preview_workspace =
    Displaying workspace number %2$s named "%1$s"!
    ]]>
    + +
    + + + + + + + +This is especially useful if you want to add RDFa or microformats to your html. +]]> + + + + tag, set this to 1. If the value is 2 then the tag is not printed at all. +Please take note that this tag is required for XHTML compliant output, so you should only disable this tag if you generate it manually already.]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="showWebsiteTitle" type="int"> + <description><![CDATA[If you want to omit the website title (from the site configuration) in your <title> tag, set this to 0.]]></description> + <default><![CDATA[1]]></default> + </property> + <property name="no_cache" type="boolean"> + <description><![CDATA[If this is set to true, the page will not be cached. If set to false, it's ignored. Other parameters may have set it to true of other reasons.]]></description> + <default><![CDATA[-]]></default> + </property> + <property name="pageRendererTemplateFile" type="string"> + <description><![CDATA[ + Sets the template for page renderer class (\TYPO3\CMS\Core\Page\PageRenderer). + +Example: + +pageRendererTemplateFile = fileadmin/test_pagerender.html + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="pageTitleFirst" type="boolean"> + <description><![CDATA[If set (and the page title is printed) then the page-title will be printed BEFORE the template title.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="pageTitleSeparator" type="string"> + <description><![CDATA[The signs which should be printed in the title tag between the website name and the page title.]]></description> + <default><![CDATA[:]]></default> + </property> + <property name="removeDefaultCss" type="boolean"> + <description><![CDATA[Remove CSS generated by _CSS_DEFAULT_STYLE configuration of extensions.]]></description> + <default><![CDATA[false]]></default> + </property> + <property name="removeDefaultJS" type="string"> + <description><![CDATA[If set, the default JavaScript in the header will be removed. +The default JavaScript is the decryption function for email addresses. + +Special case: if the value is "external" then the default JavaScript is written to a temporary file and included from that file. See "inlineStyle2TempFile" below. + +Depends on the compatibility mode (see Tools>Install>Update wizard): +compatibility mode < 4.0:   0 +compatibility mode >= 4.0:   1 + +Example: +config.removeDefaultJS = external +config.removeDefaultJS = 1]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="sendCacheHeaders" type="boolean"> + <description><![CDATA[If set, TYPO3 will output cache-control headers to the client based mainly on  whether the page was cached internally. This feature allows client browsers and/or reverse proxies to take load of TYPO3 websites. + +The conditions for allowing client caching are: +page was cachedNo *_INT or *_EXT objects were on the page (eg. USER_INT)No frontend user is logged inNo backend user is logged in + +If these conditions are met, the headers sent are: + +Expires: [expire time of page cache] +ETag: [md5 of content] +Cache-Control: max-age=[seconds til expiretime] +Pragma: public + +In case caching is not allowed, these headers are sent to avoid client caching: +Cache-Control: private, no-store + +Notice that enabling the browser caches means you have to consider how log files are written. Because when a page is cached on the client it will not invoke a request to the webserver, thus not writing the request to the log. There should be ways to circumvent these problems but they are outside the domain of TYPO3 in any case. + +Tip: Enabling cache-control headers might confuse editors seeing old content served from the browser cache. "Shift-Reload" will bypass both browser- and reverse-proxy caches and even make TYPO3 regenerate the page. Teach them that trick! + +Thanks to Ole Tange, www.forbrug.dk for co-authoring this feature.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="sendCacheHeadersForSharedCaches" type="string"> + <description><![CDATA[If enabled, TYPO3 will output cache-control headers to a possible proxy / shared cache based mainly on whether the page was cached internally. This feature allows shared caches such as CDNs / reverse proxies to take load of TYPO3 websites while delivering fresh content to the client. + +The value "auto" will send s-maxage if TYPO3 is served behind a trusted reverse proxy, config.sendCacheHeaders might be respected if the request was not performed by a trusted reverse proxy. + +The value "force" will send s-maxage in any case, config.sendCacheHeaders will be ignored. + +The conditions for allowing proxy caching are: +page was cachedNo *_INT or *_EXT objects were on the page (eg. USER_INT)No frontend user is logged inNo backend user is logged in + +If these conditions are met, the headers sent are: + +Expires: [expire time of page cache] +ETag: [md5 of content] +Cache-Control: max-age=0, s-maxage=[seconds til expiretime] +Pragma: public + +In case caching is not allowed, these headers are sent to avoid client caching: +Cache-Control: private, no-store + +Notice that enabling the shared caches means you have to consider how log files are written. Because when a page is cached on the client it will not invoke a request to the webserver, thus not writing the request to the log. There should be ways to circumvent these problems but they are outside the domain of TYPO3 in any case. + +Tip: Enabling cache-control headers might confuse editors seeing old content served from the shared cache. You should configure you shared cache to bypass cache if the cookie "be_typo_user" is set.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="spamProtectEmailAddresses" type="int"> + <description><![CDATA[-10 to 10 + If set, then all email addresses in typolinks will be encrypted so spam bots cannot detect them. + +If you set this value, then the encryption is simply an +offset of character values. If you set this value to "-2" then all +characters will have their ASCII value offset by "-2". To make this +possible, a little JavaScript code is added to every generated web page! +(It is recommended to set the value in the range from -5 to 1 since setting it to >= 2 means a "z" is converted to "|" which is a special character in TYPO3 tables syntax – and that might confuse columns in tables. Now hardcoded range) +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="spamProtectEmailAddresses_atSubst" type="string"> + <description><![CDATA[Substitute label for the at-sign (@).]]></description> + <default><![CDATA[(at)]]></default> + </property> + <property name="spamProtectEmailAddresses_lastDotSubst" type="string"> + <description><![CDATA[Substitute label for the last dot in the email address. +Example: (dot)]]></description> + <default><![CDATA[Default: . ( <= just a simple dot)]]></default> + </property> + <property name="typolinkLinkAccessRestrictedPages" type="string"> + <description><![CDATA[integer (page id) / keyword "NONE" + If set, typolinks pointing to access restricted pages will still link to the page even though the page cannot be accessed. If the value of this setting is an integer it will be interpreted as a page id to which the link will be directed. +If the value is "NONE" the original link to the page will be kept although it will generate a page-not-found situation (which can of course be picked up properly by the page-not-found handler and present a nice login form). + +See "showAccessRestrictedPages" for menu objects as well (similar feature for menus) + +Example: +config.typolinkLinkAccessRestrictedPages = 29 +config.typolinkLinkAccessRestrictedPages_addParams = &return_url=###RETURN_URL###&pageId=###PAGE_ID### + +Will create a link to page with id 29 and add GET parameters where the return URL and original page id is a part of it.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="typolinkLinkAccessRestrictedPages_addParams" type="string"> + <description><![CDATA[See "typolinkLinkAccessRestrictedPages" above]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="xmlprologue" type="string"> + <description><![CDATA[If empty (not set) then the default XML 1.0 prologue is set, when the doctype is set to a known keyword (eg xhtml_11): + +<?xml version="1.0" encoding="utf-8"> + +If set to one of the know keywords then a standard prologue will be set: +"xml_10" XML 1.0 prologue (see above) +"xml_11" XML 1.1 prologue + +If "none" then the default XML prologue is not set. +Any other string is used as the XML prologue itself.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="CONTENT"> + <property name="renderObj" type="cObj"> + <description><![CDATA[ +]]></description> + <default><![CDATA[< [tablename]]]></default> + </property> + <property name="cache" type="cache"> + <description><![CDATA[ + Stores the rendered content into the caching framework and reads it from there. + This allows you to reuse this content without prior rendering. The presence of cache.key will trigger this feature. +]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="select" type="select"> + <description><![CDATA[The SQL-statement is set here!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="slide" type="slide"> + <description><![CDATA[int/stdWrap + If set and no content element is found by the select command, then the rootLine will be traversed back until some content is found. + +Possible values are "-1" (slide back up to the siteroot), "1" (only the current level) and "2" (up from one level back). + +Use -1 in combination with collect. + +.collect (integer/stdWrap): If set, all content elements found on current and parent pages will be collected. Otherwise, the sliding would stop after the first hit. Set this value to the amount of levels to collect on, or use "-1" to collect up to the siteroot. +.collectFuzzy (boolean/stdWrap): Only useful in collect mode. If no content elements have been found for the specified depth in collect mode, traverse further until at least one match has occurred. +.collectReverse (boolean/stdWrap): Change order of elements in collect mode. If set, elements of the current page will be on the bottom.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="table" type="stdWrap"> + <description><![CDATA[TableName/stdWrap + The table, the content should come from. +In standard-configurations this will be "tt_content" +NOTE: Only tables allowed are "pages" or tables prefixed with one of these: "tt_", "tx_", "ttx_", "fe_", "user_" or "static_"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap" type="stdWrap"> + <description><![CDATA[wrap/stdWrap + Wrap the whole content-story...]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="cache"> + <property name="key" type="stdWrap"> + <description><![CDATA[ string / stdwrap + The cache identifier that is used to store the rendered content into the cache and to read it from there. +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="lifetime" type="stdWrap"> + <description><![CDATA["unlimited"/"default"/integer/stdWrap + Lifetime of the content within the cache. Allows you to determine the lifetime of the cached object. This does not depend on the lifetime of the cached version of the page on which it is used. + Possible values are any positive integer and the keywords "unlimited" and "default". +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="tags" type="stdWrap"> + <description><![CDATA[boolean/stdWrap + Can hold a comma-separated list of tags. These tags will be attached to the entry within cache_hash + cache and can be used to purge the cached content. +]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="slide" extends="stdWrap"> + <property name="collect" type="stdWrap"> + <description><![CDATA[int/stdWrap + If set, all content elements found on current and parent pages will be collected. Otherwise, the sliding would stop after the first hit. Set this value to the amount of levels to collect on, or use “-1” to collect up to the siteroot. +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="collectFuzzy" type="stdWrap"> + <description><![CDATA[boolean/stdWrap + Only useful in collect mode. If no content elements have been found for the specified depth in collect mode, traverse further until at least one match has occurred. +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="collectReverse" type="stdWrap"> + <description><![CDATA[boolean/stdWrap + Change order of elements in collect mode. If set, elements of the current page will be at the bottom. +]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="extbase" name="extbase"> + <property name="pluginName" type="stdWrap"> + <description><![CDATA[string/stdWrap + Sets variables for initializing extbase.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="controllerExtensionName" type="stdWrap"> + <description><![CDATA[string/stdWrap + Sets the extension name of the controller. + Important: This is for example essential if you have translations at the usual paths in your extension and want to use them right away in your template via <f:translate/>.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="controllerName" type="stdWrap"> + <description><![CDATA[string/stdWrap + Sets the name of the controller.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="controllerActionName" type="stdWrap"> + <description><![CDATA[string/stdWrap + Sets the name of the action.]]></description> + <default><![CDATA[]]></default> + </property> + </type> + <type id="FILES"> + <property name="references" type="stdWrap"> + <description><![CDATA[string/stdWrap or array + Provides a way to load files from a file field (of type IRRE with sys_file_reference as child table). + You can either provide a UID or a comma-separated list of UIDs from the database table sys_file_reference + or you have to specify a table, uid and field name in the according sub-properties of "references". + See further documentation of these sub-properties in the table below.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="cache" type="cache"> + <description><![CDATA[ + Stores the rendered content into the caching framework and reads it from there. + This allows you to reuse this content without prior rendering. The presence of cache.key will trigger this feature. +]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="files" type="stdWrap"> + <description><![CDATA[string/stdWrap + Comma-separated list of sys_file UIDs, which are loaded into the FILES object.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="collections" type="stdWrap"> + <description><![CDATA[string/stdWrap + Comma-separated list of sys_file_collection UIDs, which are loaded into the FILES object.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="folders" type="stdWrap"> + <description><![CDATA[string/stdWrap + Comma-separated list of combined folder identifiers which are loaded into the FILES object. + A combined folder identifier looks like this: [storageUid]:[folderIdentifier]. + The first part is the UID of the storage and the second part the identifier of the folder. + The identifier of the folder is often equivalent to the relative path of the folder.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="sorting" type="stdWrap"> + <description><![CDATA[string/stdWrap + You can sort in revers order with: sorting.direction = DESC]]></description> + <default><![CDATA[]]></default> + </property> + <property name="begin" type="integer"> + <description><![CDATA[integer]]></description> + <default><![CDATA[]]></default> + </property> + <property name="maxItems" type="integer"> + <description><![CDATA[integer]]></description> + <default><![CDATA[]]></default> + </property> + <property name="renderObj" type="cObj"> + <description><![CDATA[cObject + optionSplit + The cObject used for rendering the files. It is executed once for every file. + Note that during each execution you can find information about the current file using + the getText property "file" with the "current" keyword.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[stdWrap]]></description> + <default><![CDATA[]]></default> + </property> + </type> + <type id="FLUIDTEMPLATE"> + <property name="templateName" type="stdWrap"> + <description><![CDATA[string/stdwrap +This name is used together with the set format to find the template in the given templateRootPaths. Use this property to define a content object, which should be used as template file. It is an alternative to ".file". If ".templateName" is set, it takes precedence.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="template" type="cObject"> + <description><![CDATA[cObject +Use this property to define a content object, which should be used as template file. It is an alternative to ".file"; if ".template" is set, it takes precedence. While any content object can be used here, the cObject FILE might be the usual choice.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="file" type="stdWrap"> + <description><![CDATA[string/stdWrap +The fluid template file. It is an alternative to ".template" and is used only, if ".template" is not set.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="cache" type="cache"> + <description><![CDATA[ + Stores the rendered content into the caching framework and reads it from there. + This allows you to reuse this content without prior rendering. The presence of cache.key will trigger this feature. +]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="templateRootPaths" type="array"> + <description><![CDATA[array of file paths with stdWrap +Used to define several paths for templates, which will be tried in reversed order (the paths are searched from bottom to top). The first folder where the desired layout is found, is used. If the array keys are numeric, they are first sorted and then tried in reversed order. +Useful in combination with the templateName property.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="layoutRootPath" type="stdWrap"> + <description><![CDATA[file path/stdWrap +Sets a specific layout path; usually it is Layouts/ underneath the template file.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="layoutRootPaths" type="array"> + <description><![CDATA[array of file paths with stdWrap +Used to define several paths for layouts, which will be tried in reversed order (the paths are searched from bottom to top). The first folder where the desired layout is found, is used. If the array keys are numeric, they are first sorted and then tried in reversed order. +If property layoutRootPath (singular) is also used, it will be placed as the first option in the list of fall back paths.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="partialRootPath" type="stdWrap"> + <description><![CDATA[file path/stdWrap +Sets a specific partials path; usually it is Partials/ underneath the template file.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="layoutRootPaths" type="array"> + <description><![CDATA[array of file paths with stdWrap +Sets the format of the current request.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="format" type="stdWrap"> + <description><![CDATA[keyword / stdWrap + Used to define several paths for partials, which will be tried in reversed order. The first folder where the desired partial is found, is used. The keys of the array define the order. +See layoutRootPaths for more details.]]></description> + <default><![CDATA[html]]></default> + </property> + <property name="extbase" type="extbase"> + <description><![CDATA[Additional Extbase configuration]]></description> + <default><![CDATA[]]></default> + </property> + <property name="variables" type="array"> + <description><![CDATA[array + Sets variables that should be available in the fluid template. The keys are the variable names in Fluid. +Reserved variables are "data" and "current", which are filled automatically with the current data set.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="settings" type="array"> + <description><![CDATA[array + Sets the given settings array in the fluid template. In the view, the value can then be used. + +Example: + +page = PAGE +page { + 10 = FLUIDTEMPLATE + 10 { + file = fileadmin/templates/MyTemplate.html + settings { + copyrightYear = 2013 + } + } +} + +To access copyrightYear in the template file use this: + +{settings.copyrightYear} + +Apart from just setting a key-value pair as done in the example, you can also reference objects or access constants as well.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="dataProcessing" type="array"> + <description><![CDATA[array + Add one or multiple processors to manipulate the $data variable of the currently rendered content object, like tt_content or page. The sub-property options can be used to pass parameters to the processor class.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[stdWrap]]></description> + <default><![CDATA[]]></default> + </property> + </type> + <type id="FORM_dataArray"> + <property name="10" type="FORM_dataArray_element"> + <description><![CDATA[alternative way to define form Elements]]></description> + <default><![CDATA[]]></default> + </property> + <property name="20" type="FORM_dataArray_element"> + <description><![CDATA[alternative way to define form Elements]]></description> + <default><![CDATA[]]></default> + </property> + <property name="30" type="FORM_dataArray_element"> + <description><![CDATA[alternative way to define form Elements]]></description> + <default><![CDATA[]]></default> + </property> + <property name="40" type="FORM_dataArray_element"> + <description><![CDATA[alternative way to define form Elements]]></description> + <default><![CDATA[]]></default> + </property> + </type> + <type id="FORM_dataArray_element"> + <property name="label" type="string"> + <description><![CDATA[]]></description> + <default><![CDATA[]]></default> + </property> + <property name="type" type="string"> + <description><![CDATA[]]></description> + <default><![CDATA[]]></default> + </property> + <property name="name" type="string"> + <description><![CDATA[]]></description> + <default><![CDATA[]]></default> + </property> + <property name="value" type="string"> + <description><![CDATA[]]></description> + <default><![CDATA[]]></default> + </property> + <property name="required" type="boolean"> + <description><![CDATA[]]></description> + <default><![CDATA[]]></default> + </property> + </type> + <type id="FORM"> + <property name="CHECK.layout" type="string"> + <description><![CDATA[Alternative layout for checkboxes]]></description> + <default><![CDATA[the "layout"-property]]></default> + </property> + <property name="COMMENT.layout" type="string"> + <description><![CDATA[Alternative layout for comments.]]></description> + <default><![CDATA[the "layout"-property]]></default> + </property> + <property name="LABEL.layout" type="string"> + <description><![CDATA[Alternative layout for label types]]></description> + <default><![CDATA[the "layout"-property]]></default> + </property> + <property name="RADIO.layout" type="string"> + <description><![CDATA[Alternative layout for radiobuttons]]></description> + <default><![CDATA[the "layout"-property]]></default> + </property> + <property name="REQ" type="REQ"> + <description><![CDATA[boolean + Defines if required-fields should be checked and marked up]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="accessibility" type="boolean"> + <description><![CDATA[If set, then the form will be compliant with accessibility guidelines (XHTML compliant). This includes: + +label string will be wrapped in <label for="formname[fieldname-hash]"> ... </label>All form elements will have an id-attribute carrying the formname with the md5-hashed fieldname appended + +Notice: In TYPO3 4.0 and later, CSS Styled Content is configured to produce accessible forms by default.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="commentWrap" type="stdWrap"> + <description><![CDATA[Comments: Wrap for comments IF you use ###COMMENT###]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="data" type="stdWrap"> + <description><![CDATA[This is the data that sets up the form. See above. +"||" can be used instead of linebreaks]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="dataArray" type="FORM_dataArray"> + <description><![CDATA[This is an alternative way to define the form-fields. Instead of using the syntax with vertical separator bars suggested by the .data property, you can define the elements in regular TypoScript style arrays. +.dataArray is added to the input in .data if any. +Every entry in the dataArray is numeric and has three main properties, label, type, value and required. 'label' and 'value' has stdWrap properties. +There is an alternative property to .value, which is .valueArray. This is also an array in the same style with numeric entries which has properties label, value and selected. 'label' has stdWrap properties. + +Example: +  dataArray { +    10.label = Name: +    10.type = name=input +    10.value = [Enter name] +    10.required = 1 +    20.label = Eyecolor +    20.type = eyecolor=select +    20.valueArray { +      10.label = Blue +      10.value = 1 +      20.label = Red +      20.value = 2 +      20.selected = 1 +    } +    40.type = submit=submit +    40.value = Submit +  } + + +This is the same as this line in the .data property: + +Name: | *name=input | [Enter name] +Eyecolor: | eyecolor=select | Blue=1, *Red=2 +| submit=submit | Submit + +Why do it this way?  Good question, but doing it this way has a tremendous advantage because labels are all separated from the codes. In addition it's much easier to pull out or insert new elements in the form. +Inserting an email-field after the name field would be like this: +  dataArray { +    15.label = Email: +    15.type = input +    15.value = your@email.com +    15.specialEval = EMAIL +  } + +Or translating the form to danish (setting config.language to 'da'): + +  dataArray { +    10.label.lang.da = Navn: +    10.value.lang.da = [Indtast dit navn] +    20.label.lang.da = Øjenfarve +    20.valueArray { +      10.label.lang.da = Blå +      20.label.lang.da = Rød +    } +    40.value.lang.da = Send +  } +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="dontMd5FieldNames" type="boolean"> + <description><![CDATA[The IDs generated for all elements in a form are md5 hashes from the fieldname. Setting this to true will disable this behaviour and use a cleaned fieldname, prefixed with the form name as the ID, instead. +This can be useful to style specifically named fields with CSS.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="emailMess" type="string"> + <description><![CDATA[Message if a field evaluated to be an email adresse did not validate. + +NOTE: May be overridden by the property override feature of the formdata (see above)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="fieldPrefix" type="string"> + <description><![CDATA[Alternative prefix for the name of the fields in this form. Otherwise, all fields are prefixed with the form name (either a unique hash or the name set in the "formName" property). If set to "0", there will be no prefix at all.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="fieldWrap" type="stdWrap"> + <description><![CDATA[Field: Wraps the fields]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="formName" type="string"> + <description><![CDATA[An alternative name for this form. Default will be a unique (random) hash. + +<form name="...">]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="hiddenFields" type="cObjArray"> + <description><![CDATA[Used to set hiddenFields from TS. + +Example: +hiddenFields.pid = TEXT +hiddenFields.pid.value = 2 + +This makes a hidden-field with the name "pid" and value "2".]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="image" type="IMAGE"> + <description><![CDATA[If this is a valid image the submitbutton is rendered as this image!! + +NOTE: CurrentValue is set to the caption-label before generating the image.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="labelWrap" type="stdWrap"> + <description><![CDATA[Labels: Wraps the label]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="layout" type="string"> + <description><![CDATA[This defines how the label and the field are placed towards each other. + +Example: +This substitutes the "###FIELD###" with the field data and the "###LABEL###' with labeldata. + +<tr><td>###FIELD###</td><td> ###LABEL###</td></tr> + +You can also use the marker ###COMMENT### which is ALSO the label value inserted, but wrapped in .commentWrap stdWrap-properties (see below)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="locationData" type="string"> + <description><![CDATA[boolean / string + If this value is true, then a hidden-field called "locationData" is added to the form. This field wil be loaded with a value like this: +[page id]:[current record table]:[current record id] +For example, if a formfield is inserted on page with uid = "100", as a page-content item from the table "tt_content" with id "120", then the value would be "100:tt_content:120". +The value is use by eg. the cObject SEARCHRESULT. If the value $GLOBALS["HTTP_POST_VARS"]["locationData"] is detected here, the search is done as if it was performed on this page! This is very useful if you want a search functionality implemented on a page with the "stype" field set to "L1" which means that the search is carried out from the first level in the rootline. +Suppose you want the search to submit to a dedicated searchpage where ever. This page will then know - because of locationData - that the search was submitted from another place on the website. +If "locationData" is not only true but also set to "HTTP_POST_VARS" then the value will insert the content of $GLOBALS["HTTP_POST_VARS"]["locationData"] instead of the true location data of the page. This should be done with search-fields as this will carry the initial searching start point with. +NOTE: May be overridden by the property override feature of the formdata (see above)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="method" type="string"> + <description><![CDATA[form-method + + Example: +GET]]></description> + <default><![CDATA[POST]]></default> + </property> + <property name="noValueInsert" type="boolean"> + <description><![CDATA[By default values that are submitted to the same page (and thereby same form, eg. at searchforms) are re-inserted in the form instead of any default-data that might be set up. +This, however, applies ONLY if the "no_cache=1" is set! (a page being cached may not include user-specific defaults in the fields of course...) +If you set this flag, "noValueInsert", the content will always be the default content.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="noWrapAttr" type="boolean"> + <description><![CDATA[If this value is true then all wrap attributes of textarea elements are suppressed. This is needed for XHTML-compliancy. + +The wrap attributes can also be disabled on a per-field basis by using the special keyword "disabled" as the value of the wrap attribute.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="no_cache" type="string"> + <description><![CDATA[Default no_cache-option]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="params" type="string"> + <description><![CDATA[form-element tag parameters + Extra parameters to form elements + +Example: +params = style="width:200px;" +params.textarea = style="width:300px;" +params.check = + +This sets the default to 200 px width, but excludes check-boxes and sets textareas to 300.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="radioWrap" type="stdWrap"> + <description><![CDATA[Wraps the labels for radiobuttons]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="radioWrap.accessibilityWrap" type="wrap"> + <description><![CDATA[Defines how radio buttons are wrapped when accessibility mode is turned on (see below “accessibility” property) + default: + + <fieldset###RADIO_FIELD_ID###><legend>###RADIO_GROUP_LABEL###</legend>|</fieldset> + ]]></description> + <default><![CDATA[<fieldset###RADIO_FIELD_ID###><legend>###RADIO_GROUP_LABEL###</legend>|</fieldset> +]]></default> + </property> + <property name="radioInputWrap" type="stdWrap"> + <description><![CDATA[Wraps the input element and label of a radio button.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="recipient" type="stdWrap"> + <description><![CDATA[(list of) string /stdWrap + Email recipient of the formmail content (generates the hiddenfield "recipient")]]></description> + <default><![CDATA[No email]]></default> + </property> + <property name="redirect" type="stdWrap"> + <description><![CDATA[URL to redirect to (generates the hidden field "redirect") + +Integer: this is regarded to be a page in TYPO3 +String: this is regarded to be a normal url +Empty; the current page is chosen. + +NOTE: If this value is set the target of this overriddes the target of the "type".]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[Wraps the hole form (before formtags is added)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="target" type="string"> + <description><![CDATA[target + Default target of the form. ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="type" type="int"> + <description><![CDATA[Type (action="" of the form): + +Integer: this is regarded to be a page in TYPO3 +String: this is regarded to be a normal URL (eg. "formmail.php") +Empty: the current page is chosen. + +NOTE: If type is integer/empty the form will be submitted to a page in TYPO3 and if this page has a value for target/no_cache, then this will be used instead of the default target/no_cache below. + +NOTE: If the redirect-value is set, the redirect-target overrides the target set by the action-url + +NOTE: May be overridden by the property override feature of the formdata (see above)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrapFieldName" type="wrap"> + <description><![CDATA[This wraps  the fieldnames before they are applied to the form-field tags. + +Example: +If value is tx_myextension[input][  |  ]  then the fieldname "email" would be wrapped to this value: tx_myextension[input][email]]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="REQ"> + <property name="fieldWrap" type="stdWrap"> + <description><![CDATA[Field: Wraps the fields, but for required fields]]></description> + <default><![CDATA[the "fieldWrap"-property]]></default> + </property> + <property name="labelWrap" type="stdWrap"> + <description><![CDATA[Labels: Wraps the label, but for required fields]]></description> + <default><![CDATA[the "labelWrap"-property]]></default> + </property> + <property name="layout" type="string"> + <description><![CDATA[The same as "layout" above, but for required fields]]></description> + <default><![CDATA[the "layout"-property]]></default> + </property> + </type> + <type id="GifBuilderObj"> + <property name="if" type="if"> + <description><![CDATA[.if (->if) is a property of all gifbuilder-objects. If the property is present and NOT set, the object is NOT rendered! This corresponds to the functionality of ".if" of the stdWrap-function.]]></description> + </property> + </type> + <type id="GIFBUILDER"> + <property name="1" type="GifBuilderObj"> + <description><![CDATA[.if (->if) is a property of all gifbuilder-objects. If the property is present and NOT set, the object is NOT rendered! This corresponds to the functionality of ".if" of the stdWrap-function.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="2" type="GifBuilderObj"> + <description><![CDATA[.if (->if) is a property of all gifbuilder-objects. If the property is present and NOT set, the object is NOT rendered! This corresponds to the functionality of ".if" of the stdWrap-function.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="3" type="GifBuilderObj"> + <description><![CDATA[.if (->if) is a property of all gifbuilder-objects. If the property is present and NOT set, the object is NOT rendered! This corresponds to the functionality of ".if" of the stdWrap-function.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="4" type="GifBuilderObj"> + <description><![CDATA[.if (->if) is a property of all gifbuilder-objects. If the property is present and NOT set, the object is NOT rendered! This corresponds to the functionality of ".if" of the stdWrap-function.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="XY" type="string"> + <description><![CDATA[x,y +calc + Size of the gif-file. ]]></description> + <default><![CDATA[100,20]]></default> + </property> + <property name="backColor" type="string"> + <description><![CDATA[GraphicColor + Background color for the gif]]></description> + <default><![CDATA[white]]></default> + </property> + <property name="format" type="string"> + <description><![CDATA["gif" / "jpg" + Output type. +"jpg"/"jpeg" = jpg-image]]></description> + <default><![CDATA[gif]]></default> + </property> + <property name="maxHeight" type="int"> + <description><![CDATA[pixels + Maximal height of gif-file]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="maxWidth" type="int"> + <description><![CDATA[pixels + Maximal width of gif-file]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="offset" type="string"> + <description><![CDATA[x,y +calc + Offset all objects on the gif.]]></description> + <default><![CDATA[0,0]]></default> + </property> + <property name="quality" type="int"> + <description><![CDATA[posint (10-100) + JPG-quality (if ".format" = jpg/jpeg)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="speed" type="int"> + <description><![CDATA[posint (0-10) + Speed parameter (if ".format" = avif)]]></description> + <default><![CDATA[-1]]></default> + </property> + <property name="transparentBackground" type="boolean"> + <description><![CDATA[Set this flag to render the background transparent. TYPO3 makes the color found at position 0,0 of the image (upper left corner) transparent. +If you render text you should leave the niceText option OFF as the result with probably be more precise without the niceText antialiasing hack]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="transparentColor" type="stdWrap"> + <description><![CDATA[HTMLColor /stdWrap + Specify a color that should be transparent + +Example-values: +#ffffcc +red +255,255,127 + +Option: +transparentColor.closest = 1 +This will allow for the closest color to be matched instead. You may need this if your image is not guaranteed "clean".]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="workArea" type="string"> + <description><![CDATA[x,y,w,h + calc + Define the workarea on the giffile. All the GifBuilderObj's will see this as the dimensions of the gif-file regarding alignment, overlaying of images an so on. Only will TEXT-objects exceeding the boundaries of the workarea print outside this area.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="ADJUST" extends="GifBuilderObj"> + <property name="value" type="string"> + <description><![CDATA[This lets you adjust the input-levels like in Photoshop's "levels"-dialog. If you need to adjust gamma, look at the EFFECT-object. +Example: + +20 = ADJUST +20.value = inputLevels = 13,230 + +properties: + +inputLevels: low,high +outputLevels: low, high +autoLevels: - +]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="BOX" extends="GifBuilderObj"> + <property name="align" type="string"> + <description><![CDATA[VHalign +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="color" type="string"> + <description><![CDATA[GraphicColor + fill-color]]></description> + <default><![CDATA[black]]></default> + </property> + <property name="dimensions" type="string"> + <description><![CDATA[x,y,w,h +calc + Dimensions of a filled box. +x,y    is the offset. +w,h    is the dimensions. Dimensions of 1 will result in 1-pixel wide lines!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="opacity" type="int"> + <description><![CDATA[pos-int (1-100) + Dimensions of a filled box. +Opacity (i.e. inverse of transparency, e.g. 100% opacity = 0% transparency)]]></description> + <default><![CDATA[100 +]]></default> + </property> + </type> + <type id="CROP" extends="GifBuilderObj"> + <property name="align" type="string"> + <description><![CDATA[VHalign +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="backColor" type="string"> + <description><![CDATA[GraphicColor +]]></description> + <default><![CDATA[The original backColor]]></default> + </property> + <property name="crop" type="string"> + <description><![CDATA[x,y,v,h + calc + x,y is offset of the crop-frame, +v,h  is the dimensions]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="ELLIPSE" extends="GifBuilderObj"> + <property name="dimensions" type="string"> + <description><![CDATA[x,y,w,h +calc +Dimensions of a filled ellipse. +x,y is the offset. +w,h is the dimensions. Dimensions of 1 will result in 1-pixel wide lines! + +Example: +file = GIFBUILDER +file { + XY = 200,200 + format = jpg + quality = 100 + 10 = ELLIPSE + 10.dimensions = 100,100,50,50 + 10.color = red + +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="color" type="string"> + <description><![CDATA[GraphicColor +fill-color + +Example: +file = GIFBUILDER +file { + XY = 200,200 + format = jpg + quality = 100 + 10 = ELLIPSE + 10.dimensions = 100,100,50,50 + 10.color = red + +]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="EFFECT" extends="GifBuilderObj"> + <property name="value" type="string"> + <description><![CDATA[.value = [Varnavn] = [value] | [Varnavn] = [value] + +Example: +20 = EFFECT +20.value = gamme=1.3 | flip | rotate=180 + + +gamma: 0.5 - 3.0 +blur: 1-99 +sharpen: 1-99 +solarize: 0-99 +swirl: 0-100 +wave: ampli , length +charcoal: 0-100 +gray: - +edge: 0-99 +emboss: - +flip: - (Vertical flipping) +flop: - (Horizontal flipping) +rotate: 0-360 (Rotation) +colors: 2-255 +shear: -90 - 90 (Horizontal shearing) +invert: - (invert the colors) +]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="EMBOSS" extends="GifBuilderObj"> + <property name="blur" type="int"> + <description><![CDATA[posint (1-99) + Blurring of the shadow. Above 40 only values of 40,50,60,70,80,90 means something.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="highColor" type="string"> + <description><![CDATA[GraphicColor + Upper border-color]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="intensity" type="int"> + <description><![CDATA[posint(0-100) + How "massive" the emboss is. This value can - if it has a high value combined with a blurred shadow - create a kind of soft-edged outline.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="lowColor" type="string"> + <description><![CDATA[GraphicColor + lower border-color]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="offset" type="string"> + <description><![CDATA[x,y + Offset of the emboss]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="opacity" type="int"> + <description><![CDATA[posint (1-100) + Opacity (transparency^-1) +100% opacity = 0% transparency). Only active with a value for blur.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="textObjNum" type="int"> + <description><![CDATA[pos-int + Must point to the TEXT-object if these shadow-properties are not properties to a TEXT-object directly ("stand-alone-shadow"). Then the shadow needs to know which TEXT-object it should be a shadow of! +If - on the other hand - the shadow is a property to a text-object, this property is not needed.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="GB_IMAGE" extends="GifBuilderObj"> + <property name="align" type="string"> + <description><![CDATA[VHalign +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="file" type="imgResource"> + <description><![CDATA[The imagefile]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="mask" type="imgResource"> + <description><![CDATA[Optional mask-image for the imagefile.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="offset" type="string"> + <description><![CDATA[x,y +calc + Offset ]]></description> + <default><![CDATA[0,0]]></default> + </property> + <property name="tile" type="string"> + <description><![CDATA[x,y + tile x,y times. +Maximum times is 20 each direction. If you need more, use a larger image.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="OUTLINE" extends="GifBuilderObj"> + <property name="color" type="string"> + <description><![CDATA[GraphicColor + Outline color]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="textObjNum" type="int"> + <description><![CDATA[pos-int + Must point to the TEXT-object if these shadow-properties are not properties to a TEXT-object directly ("stand-alone-shadow"). Then the shadow needs to know which TEXT-object it should be a shadow of! +If - on the other hand - the shadow is a property to a text-object, this property is not needed.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="thickness" type="string"> + <description><![CDATA[x,y + Thickness in each direction, range 1-2]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="SCALE" extends="GifBuilderObj"> + <property name="height" type="string"> + <description><![CDATA[pixels + calc +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="params" type="string"> + <description><![CDATA[ImageMagickParams +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="width" type="string"> + <description><![CDATA[pixels + calc +]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="SHADOW" extends="GifBuilderObj"> + <property name="blur" type="int"> + <description><![CDATA[posint (1-99) + Blurring of the shadow. Above 40 only values of 40,50,60,70,80,90 means something. + +NOTE: Unfortunately the blurring capabilities of ImageMagick is not very mature in the version 4.2.9. This is addressed in the later version 5.2.0 where a gaussian blur-function is added. BUT as we do cannot use the latest ImageMagick development yet, this is not utilized so far.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="color" type="string"> + <description><![CDATA[GraphicColor + Shadow color]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="intensity" type="int"> + <description><![CDATA[posint(0-100) + How "massive" the shadow is. This value can - if it has a high value combined with a blurred shadow - create a kind of soft-edged outline.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="offset" type="string"> + <description><![CDATA[x,y + Shadow offset]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="opacity" type="int"> + <description><![CDATA[posint (1-100) + Opacity (transparency^-1) +100% opacity = 0% transparency). Only active with a value for blur.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="textObjNum" type="int"> + <description><![CDATA[pos-int + Must point to the TEXT-object if these shadow-properties are not properties to a TEXT-object directly ("stand-alone-shadow"). Then the shadow needs to know which TEXT-object it should be a shadow of! +If - on the other hand - the shadow is a property to a text-object, this property is not needed.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="GB_TEXT" extends="GifBuilderObj"> + <property name="align" type="string"> + <description><![CDATA[align + Alignment of the text]]></description> + <default><![CDATA[left]]></default> + </property> + <property name="angle" type="string"> + <description><![CDATA[degree + Rotation degrees of the text. + +NOTE: Angle is not available if spacing/wordSpacing is set.]]></description> + <default><![CDATA[0 +Range: -90 til 90]]></default> + </property> + <property name="antiAlias" type="boolean"> + <description><![CDATA[FreeType antialiasing. Notice, the default mode is "on"! + +Note: This option is not available if .niceText is enabled]]></description> + <default><![CDATA[1]]></default> + </property> + <property name="breakWidth" type="int"> + <description><![CDATA[Defines the maximum width for an object, overlapping elements will force an automatic line break. + ]]></description> + <default><![CDATA[ + ]]></default> + </property> + <property name="breakSpace" type="float"> + <description><![CDATA[Defines a value that is multiplied by the line height of the current element. + ]]></description> + <default><![CDATA[1.0]]></default> + </property> + <property name="doNotStripHTML" type="boolean"> + <description><![CDATA[If set, HTML-tags in the string inserted are NOT removed. Any other way HTML-code is removed by default!]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="emboss" type="EMBOSS"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="fontColor" type="stdWrap"> + <description><![CDATA[GraphicColor /stdWrap + Font color]]></description> + <default><![CDATA[black]]></default> + </property> + <property name="fontFile" type="string"> + <description><![CDATA[Font face (truetype font you can upload!!)]]></description> + <default><![CDATA[Nimbus (Arial-clone)]]></default> + </property> + <property name="fontSize" type="int"> + <description><![CDATA[posint + Font size]]></description> + <default><![CDATA[12]]></default> + </property> + <property name="hide" type="boolean"> + <description><![CDATA[If this is true, the text is NOT printed. +This feature may be used if you need a shadow-object to base a shadow on the text, but do not want the text to print.]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="iterations" type="int"> + <description><![CDATA[posint + How many times the text should be "printed" onto it self. This will add the effect of bold text. + +Note: This option is not available if .niceText is enabled]]></description> + <default><![CDATA[1]]></default> + </property> + <property name="maxWidth" type="int"> + <description><![CDATA[pixels + Sets the maximum width in pixels, the text must be. Reduces the fontSize if the text does not fit within this width. + +Does not support setting alternative fontSizes in splitRendering options. + +(By Rene Fritz <r.fritz@colorcube.de>)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="niceText" type="boolean"> + <description><![CDATA[This is a very popular feature that helps to render small letters much nicer than the freetype library can normally do. But it also loads the system very much! +The principle of this function is to create a black/white giffile in twice or more times the size of the actual gif-file and then print the text onto this is a scaled dimension. Afterwards ImageMagick (IM) scales down the mask and masks the font color down on the original gif-file through the temporary mask. +The fact that the font  is  actually rendered in the double size and scaled down adds a more homogeneous shape to the letters. Some fonts are more critical than others though.  If you do not need the quality, then don't use the function. + +Some properties: +.before = IM-params before scale +.after = IM-params after scale +.sharpen = sharpen-value for the mask (after scaling), integer 0-99 (this enables you to make the text crisper if it's too blurred!) +.scaleFactor = scaling-factor, int 2-5]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="offset" type="string"> + <description><![CDATA[x,y +calc + Offset of the text]]></description> + <default><![CDATA[0,0]]></default> + </property> + <property name="outline" type="OUTLINE"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="shadow" type="SHADOW"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="spacing" type="int"> + <description><![CDATA[posint + Pixel-distance between letters. This may render ugly!]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="splitRendering.compX" type="string"> + <description><![CDATA[Split the rendering of a string into separate processes with individual configurations. By this method a certain range of characters can be rendered with another font face or size. This is very useful if you want to use separate fonts for strings where you have latin characters combined with eg. Japanese and there is a separate font file for each. +You can also render keywords in another font/size/color. + +Properties: +splitRendering.compX = Additional pixelspace between parts, x direction +splitRendering.compY = Additional pixelspace between parts, y direction +splitRendering.[array] = keyword  [charRange, highlightWord] +splitRendering.[array] { +  fontFile = Alternative font file for this rendering +  fontSize = Alternative font size for this rendering +  color = Alternative color for this rendering, works ONLY without "niceText" +  xSpaceBefore = x-Space before this part +  xSpaceAfter = x-Space after this part +  ySpaceBefore = y-Space before this part +  ySpaceAfter =  y-Space after this part +} + +Keyword: charRange +splitRendering.[array].value = Commaseparated list of character ranges (eg. "100-200") given as Unicode character numbers. The list accepts optional starting and ending points, eg. " - 200" or " 200 -" and single values, eg. "65, 66, 67" + +Keyword: highlightWord +splitRendering.[array].value = Word to highlight, makes a case sensitive search for this. + +Limitations: +The pixelcompensation values are not corrected for scale factor used with niceText. Basically this means that when niceText is used, these values will have only the half effect.When word spacing is used the "highlightWord" mode doesn't work.The color override works only without "niceText". + +Example: +  10.splitRendering.compX = 2 +  10.splitRendering.compY = -2 +  10.splitRendering.10 = charRange +  10.splitRendering.10 { +    value = 200-380 , 65, 66 +    fontSize = 50 +    fontFile =  typo3/sysext/core/Resources/Private/Font/nimbus.ttf +    xSpaceBefore = 30 +  } +  10.splitRendering.20 = highlightWord +  10.splitRendering.20 { +    value = TheWord +    color = red +  }]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="splitRendering.compY" type="string"> + <description><![CDATA[Split the rendering of a string into separate processes with individual configurations. By this method a certain range of characters can be rendered with another font face or size. This is very useful if you want to use separate fonts for strings where you have latin characters combined with eg. Japanese and there is a separate font file for each. +You can also render keywords in another font/size/color. + +Properties: +splitRendering.compX = Additional pixelspace between parts, x direction +splitRendering.compY = Additional pixelspace between parts, y direction +splitRendering.[array] = keyword  [charRange, highlightWord] +splitRendering.[array] { +  fontFile = Alternative font file for this rendering +  fontSize = Alternative font size for this rendering +  color = Alternative color for this rendering, works ONLY without "niceText" +  xSpaceBefore = x-Space before this part +  xSpaceAfter = x-Space after this part +  ySpaceBefore = y-Space before this part +  ySpaceAfter =  y-Space after this part +} + +Keyword: charRange +splitRendering.[array].value = Commaseparated list of character ranges (eg. "100-200") given as Unicode character numbers. The list accepts optional starting and ending points, eg. " - 200" or " 200 -" and single values, eg. "65, 66, 67" + +Keyword: highlightWord +splitRendering.[array].value = Word to highlight, makes a case sensitive search for this. + +Limitations: +The pixelcompensation values are not corrected for scale factor used with niceText. Basically this means that when niceText is used, these values will have only the half effect.When word spacing is used the "highlightWord" mode doesn't work.The color override works only without "niceText". + +Example: +  10.splitRendering.compX = 2 +  10.splitRendering.compY = -2 +  10.splitRendering.10 = charRange +  10.splitRendering.10 { +    value = 200-380 , 65, 66 +    fontSize = 50 +    fontFile =  typo3/sysext/core/Resources/Private/Font/nimbus.ttf +    xSpaceBefore = 30 +  } +  10.splitRendering.20 = highlightWord +  10.splitRendering.20 { +    value = TheWord +    color = red +  }]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="text" type="stdWrap"> + <description><![CDATA[This is text text-string on the gif-file. The item is rendered only if this string is not empty. +The cObj->data-array is loaded with the page-record, if for the GIFBUILDER-object is used]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="textMaxLength" type="int"> + <description><![CDATA[The maximum length of the text.  This is just a natural break that prevents incidental rendering of very long texts!]]></description> + <default><![CDATA[100]]></default> + </property> + <property name="wordSpacing" type="int"> + <description><![CDATA[posint + Pixel-distance between words.]]></description> + <default><![CDATA[= ".spacing"*2]]></default> + </property> + </type> + <type id="WORKAREA" extends="GifBuilderObj"> + <property name="clear" type="string">(isset) + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="set" type="string"> + <description><![CDATA[x,y,w,h + calc + Sets another workarea +]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="HMENU"> + <property name="1" type="mObj"> + <description><![CDATA[Required! +Defines which menuObj that should render the menuitems on the various levels. +1 is the first level, 2 is the second level, 3 is the third level, 4 is .... + +Example: +temp.sidemenu = HMENU +temp.sidemenu.1 = TMENU  ]]></description> + <default><![CDATA[ (no menu)]]></default> + </property> + <property name="2" type="mObj"> + <description><![CDATA[Defines which menuObj that should render the menuitems on the various levels. +1 is the first level, 2 is the second level, 3 is the third level, 4 is .... + +Example: +temp.sidemenu = HMENU +temp.sidemenu.1 = TMENU  ]]></description> + <default><![CDATA[ (no menu)]]></default> + </property> + <property name="3" type="mObj"> + <description><![CDATA[Defines which menuObj that should render the menuitems on the various levels. +1 is the first level, 2 is the second level, 3 is the third level, 4 is .... + +Example: +temp.sidemenu = HMENU +temp.sidemenu.1 = TMENU  ]]></description> + <default><![CDATA[ (no menu)]]></default> + </property> + <property name="4" type="mObj"> + <description><![CDATA[Defines which menuObj that should render the menuitems on the various levels. +1 is the first level, 2 is the second level, 3 is the third level, 4 is .... + +Example: +temp.sidemenu = HMENU +temp.sidemenu.1 = TMENU  ]]></description> + <default><![CDATA[ (no menu)]]></default> + </property> + <property name="addQueryString" type="string"> + <description><![CDATA[see typolink.addQueryString + +Notice: This works only for special=language.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="alwaysActivePIDlist" type="stdWrap"> + <description><![CDATA[List of Integers /stdWrap + This is a list of page UID numbers that will always be regarded as active menu items and thereby automatically opened regardless of the rootline.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="begin" type="int"> + <description><![CDATA[int +calc + The first item in the menu. + +Example: +This results in a menu, where the first two items are skipped starting with item number 3: +  begin = 3 + +Notice: Affects all sub menus as well. (See "minItems" for notice)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="cache" type="cache"> + <description><![CDATA[ + Stores the rendered content into the caching framework and reads it from there. + This allows you to reuse this content without prior rendering. The presence of cache.key will trigger this feature. +]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="entryLevel" type="int"> + <description><![CDATA[Defines at which level in the rootLine, the menu should start. +Default is "0" which gives us a menu of the very first pages on the site. +If the value is < 0, entryLevel is chosen from "behind" in the rootLine. Thus "-1" is a menu with items from the outermost level, "-2" is the level before the outermost...]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="excludeDoktypes" type="intList"> + <description><![CDATA[list of integers + Enter the list of page document types (doktype) to exclude from menus. By default pages that are marked for backend user access only (6) are excluded. ]]></description> + <default><![CDATA[5,6]]></default> + </property> + <property name="excludeUidList" type="int"> + <description><![CDATA[list of integers + This is a list of page uid's to exclude when the select statement is done. Comma-separated. You may add "current" to the list to exclude the current page. + +Example: +The pages with these uid-number will NOT be within the menu!! Additionally the current page is always excluded too. +  excludeUidList = 34,2,current]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="if" type="if"> + <description><![CDATA[If "if" returns false, the menu is not generated]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="includeNotInMenu" type="boolean"> + <description><![CDATA[If set, pages with "Not in menu" will be included in menus. +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="maxItems" type="int"> + <description><![CDATA[The maximum items in the menu. More items will be ignored. + +Notice: Affects all sub menus as well. (See "minItems" for notice)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="minItems" type="int"> + <description><![CDATA[The minimum items in the menu. If the number of pages does not reach this level, a dummy-page with the title "..." and uid=[currentpage_id] is inserted. + +Notice: Affects all sub menus as well. To set the value for each menu level individually, set the properties in the menu objects (see "Common properties" table).]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="protectLvar" type="string"> + <description><![CDATA[boolean / keyword + If set, then for each page in the menu it will be checked if an Alternative Page Language record for the language defined in "config.sys_language_uid" (typically defined via &L) exists for the page. If that is not the case and the pages "Localization settings" have the "Hide page if no translation for current language exists" flag set, then the menu item will link to a non accessible page that will yield an error page to the user. Setting this option will prevent that situation by simply adding "&L=0" for such pages, meaning that they will switch to the default language rather than keeping the current language. +The check is only carried out if a translation is requested ("config.sys_language_uid" is not zero). + +Keyword: "all" +When set to "all" the same check is carried out but it will not look if "Hide page if no translation for current language exists" is set - it always reverts to default language if no translation is found.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="special" type="HMENU_SPECIAL"> + <description><![CDATA["directory" / "list" / "updated" / "browse" / "rootline" / "keywords" / "language" + (See TSref for details: + <a href="https://docs.typo3.org/typo3cms/TyposcriptReference/ContentObjects/Hmenu/#hmenu-special-property"> + https://docs.typo3.org/typo3cms/TyposcriptReference/ContentObjects/Hmenu/#hmenu-special-property</a> )]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap" type="stdWrap"> + <description>wrap/stdWrap<![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="HMENU_SPECIAL"> + <property name="value" type="stdWrap"> + <description><![CDATA[list of page-uid's /stdWrap]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="directory" extends="HMENU_SPECIAL"/> + <type id="list" extends="HMENU_SPECIAL"/> + <type id="updated" extends="HMENU_SPECIAL"> + <property name="mode" type="string"> + <description><![CDATA[Which field in the pages-table to use. Default is "SYS_LASTCHANGED" (which is updated when a page is generated to the youngest tstamp of the records on the page), "manual" or "lastUpdated" will use the field "lastUpdated" (set manually in the page-record) and "tstamp" will use the "tstamp"-field of the pagerecord, which is set automatically when the record is changed. "crdate" will use "crdate"-field of the pagerecord. "starttime" will use the starttime field. + +Fields with zero value is not selected anyway.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="depth" type="string"> + <description><![CDATA[By default (if the value is not an integer) the depth is 20 levels. The range is 1-20. A depth of 1 means only the start id, depth of 2 means start-id + first level. NOTE: depth is relative to beginAtLevel. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="beginAtLevel" type="int"> + <description><![CDATA[Determines starting level for the pagetrees generated based on .value and .depth. Zero is default and includes the start id. 1=starts with the first row of subpages, 2=starts with the second row of subpages. Depth is relative to this starting point. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="maxAge" type="string"> + <description><![CDATA[Seconds+calc. + Pages with update-dates older than currenttime minus this number of seconds will not be shown in the menu no matter what. Default is "not used". You may use +-*/ for calculations. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="limit" type="int"> + <description><![CDATA[Max number of items in the menu. Default is 10, max is 100. + ]]></description> + <default><![CDATA[10 +]]></default> + </property> + <property name="excludeNoSearchPages" type="boolean"> + <description><![CDATA[If set, pages marked "No search" is not included into special-menus. +Support for Mount Pages: Yes. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="rootline" extends="HMENU_SPECIAL"> + <property name="range" type="string"> + <description><![CDATA[rootline creates a menu with pages from the "rootline" (see earlier in this reference) + +.range = [begin-level] | [end-level] (same way as you reference the .entryLevel for HMENU) + +This... + +page.2 = HMENU +page.2.special = rootline +page.2.special.range = 1|-2 +page.2.special.targets.3 = page +page.2.1 = TMENU +page.2.1.target = _top +page.2.1.wrap = <HR> | <HR> +page.2.1.NO { + linkWrap = | > +} +... creates a menu like this: + +Page level 1 > Page level 2 > Page level 3 > Page level 4 > + +(The menu starts at level 1 and does NOT link to the current page (-2 is the level before). Further all pages on level 3 will have "page" as target and all other "_top") + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="reverseOrder" type="boolean"> + <description><![CDATA[If set to true, the order of the rootline menu elements will be reversed. + ]]></description> + <default><![CDATA[false]]></default> + </property> + <property name="targets" type="string"> + <description><![CDATA[.targets.[0-x] targets + + This... + +page.2 = HMENU +page.2.special = rootline +page.2.special.range = 1|-2 +page.2.special.targets.3 = page +page.2.1 = TMENU +page.2.1.target = _top +page.2.1.wrap = <HR> | <HR> +page.2.1.NO { + linkWrap = | > +} + +... creates a menu like this: + +Page level 1 > Page level 2 > Page level 3 > Page level 4 > + +(The menu starts at level 1 and does NOT link to the current page (-2 is the level before). Further all pages on level 3 will have "page" as target and all other "_top") + + ]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="browse" extends="HMENU_SPECIAL"> + <property name="items" type="string"> + <description><![CDATA[.items ( "|" separated list of "itemnames") + This kind of menu is built of items given by a list from the property ".item". Each element in the list (sep. by "|") is either a reserved itemname (see list) with a predefined function or a userdefined name which you can assign a link to any page. Note that the current page cannot be the root-page of a site. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="items.prevnextToSection" type="boolean"> + <description><![CDATA[items.prevnextToSection (boolean) - if set, the "prev" and "next" navigation will jump to the next section when it reaches the end of pages in the current section. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="next" type="HMENU_SPECIAL_browseItem"> + <description><![CDATA[next / prev : links to next page / previous page. Next and previous pages are from the same "pid" as the current page id (or "value") - that is the next item in a menu with the current page. Also referred to as current level. + +If ".prevnextToSection" is set then next/prev will link to the first page of next section / last page of previous section. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="prev" type="HMENU_SPECIAL_browseItem"> + <description><![CDATA[next / prev : links to next page / previous page. Next and previous pages are from the same "pid" as the current page id (or "value") - that is the next item in a menu with the current page. Also referred to as current level. + +If ".prevnextToSection" is set then next/prev will link to the first page of next section / last page of previous section. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="nextsection" type="HMENU_SPECIAL_browseItem"> + <description><![CDATA[nextsection / prevsection : links to next section / previous section. A section is defined as the subpages of a page on the same level as the parent (pid) page of the current page. Will not work if parent page of current page is the root page of the site. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="prevsection" type="HMENU_SPECIAL_browseItem"> + <description><![CDATA[nextsection / prevsection : links to next section / previous section. A section is defined as the subpages of a page on the same level as the parent (pid) page of the current page. Will not work if parent page of current page is the root page of the site. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="nextsection_last" type="HMENU_SPECIAL_browseItem"> + <description><![CDATA[Where nextsection/prevsection links to the first page in a section, these links to the last pages. If there is only one page in the section that will be both first and last. Will not work if parent page of current page is the root page of the site. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="prevsection_last" type="HMENU_SPECIAL_browseItem"> + <description><![CDATA[Where nextsection/prevsection links to the first page in a section, these links to the last pages. If there is only one page in the section that will be both first and last. Will not work if parent page of current page is the root page of the site. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="first" type="HMENU_SPECIAL_browseItem"> + <description><![CDATA[First / Last page on current level. If there is only one page on the current level that page will be both first and last. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="last" type="HMENU_SPECIAL_browseItem"> + <description><![CDATA[First / Last page on current level. If there is only one page on the current level that page will be both first and last. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="up" type="HMENU_SPECIAL_browseItem"> + <description><![CDATA[Links to the parent (pid) page of the current page. (up 1 level) Will always be available + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="index" type="HMENU_SPECIAL_browseItem"> + <description><![CDATA[Links to the parent of the parent page of the current page (up 2 levels). May not be available if that page is out of the rootline. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="HMENU_SPECIAL_browseItem"> + <property name="target" type="string"> + <description><![CDATA[optional/alternative target of the item]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="uid" type="int"> + <description><![CDATA[. (uid of page) - optional/alternative page-uid to link to +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="fields" type="string"> + <description><![CDATA[.[itemnames].fields.[fieldname] (string) + override field "fieldname" in pagerecord.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="keywords" extends="HMENU_SPECIAL"> + <property name="mode" type="string"> + <description><![CDATA[Which field in the pages-table to use for sorting. Default is "SYS_LASTCHANGED" (which is updated when a page is generated to the youngest tstamp of the records on the page), "manual" or "lastUpdated" will use the field "lastUpdated" (set manually in the page-record) and "tstamp" will use the "tstamp"-field of the pagerecord, which is set automatically when the record is changed. "crdate" will use "crdate"-field of the pagerecord. "starttime" will use the starttime field. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="depth" type="string"> + <description><![CDATA[By default (if the value is not an integer) the depth is 20 levels. The range is 1-20. A depth of 1 means only the start id, depth of 2 means start-id + first level. NOTE: depth is relative to beginAtLevel. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="beginAtLevel" type="int"> + <description><![CDATA[Determines starting level for the pagetrees generated based on .value and .depth. Zero is default and includes the start id. 1=starts with the first row of subpages, 2=starts with the second row of subpages. Depth is relative to this starting point. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="limit" type="int"> + <description><![CDATA[Max number of items in the menu. Default is 10, max is 100. + ]]></description> + <default><![CDATA[10 +]]></default> + </property> + <property name="excludeNoSearchPages" type="boolean"> + <description><![CDATA[If set, pages marked "No search" is not included into special-menus. +Support for Mount Pages: Yes. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="entryLevel" type="string"> + <description><![CDATA[.entryLevel = where in the rootline the search begins. Standard rootline syntax (-x to x)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="setKeywords" type="stdWrap"> + <description><![CDATA[.setKeywords (/stdWrap) = lets you define the keywords manually by defining them as a commaseparated list. If this property is defined, it overrides the default, which is the keywords of the current page. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="keywordsField" type="string"> + <description><![CDATA[.keywordsField = defines the field in the pages-table in which to search for the keywords. Default is the fieldname "keyword". No check is done to see if the field you enter here exists, so enter an existing field, OK?!]]></description> + <default><![CDATA["keyword" +]]></default> + </property> + <property name="keywordsField.sourceField" type="string"> + <description><![CDATA[.keywordsField.sourceField = defines the field from the current page from which to take the keywords being matched. The default is "keyword". (Notice that ".keywordsField" is only setting the page-record field to search in !)]]></description> + <default><![CDATA["keyword" +]]></default> + </property> + </type> + <type id="language" extends="HMENU_SPECIAL"/> + <type id="userdefined" extends="HMENU_SPECIAL"> + <property name="file" type="string"> + <description><![CDATA[.file [resource] = filename of the php-file to include. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="userfunction" extends="HMENU_SPECIAL"> + <property name="userFunc" type="string"> + <description><![CDATA[.userFunc = function-name + Calls a user function/method in class which should (as with "userdefined" above) return an array with page records for the menu. +]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="mObj"> + <property name="alternativeSortingField" type="string"> + <description><![CDATA[Normally the menuitems are sorted by the fields "sorting" in the pages- and tt_content-table. Here you can enter a list of fields that is used in the SQL- "ORDER BY" statement instead. + +Examples (for "pages" table): +alternativeSortingField = title desc +(This will render the menu in reversed alphabetical order) + +LIMITATIONS: +This property works with normal menus, sectionsIndex menus and special-menus of type "directory".]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="begin" type="int"> + <description><![CDATA[int +calc + The first item in the menu. + +Example: +This results in a menu, where the first two items are skipped starting with item number 3: +  begin = 3 + +Takes precedence over HMENU.begin]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="imgNameNotRandom" type="boolean"> + <description><![CDATA[If set, the image names of menuitems is not randomly assigned. Useful switch if you're manipulating these images with some external JavaScript + +NOTE: Don't set this if you're working with a menu with sectionIndex! In that case you need special unique names of items based on something else than the uid of the parent page of course!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="imgNamePrefix" type="string"> + <description><![CDATA[prefix for the imagenames. This prefix is appended with the uid of the page.]]></description> + <default><![CDATA["img"]]></default> + </property> + <property name="itemArrayProcFunc" type="string"> + <description><![CDATA[function-name + The first variable passed to this function is the "menuArr" array with the menuitems as they are collected based on the type of menu. +You're free to manipulate or add to this array as you like. Just remember to return the array again! + +Note: +.parentObj property is hardcoded to be a reference to the calling AbstractMenuContentObject object. Here you'll find eg. ->id to be the uid of the menu item generating a submenu and such. + +Presetting element state +You can override element states like SPC, IFSUB, ACT, CUR or USR by setting the key ITEM_STATE in the page records. See cObject HMENU/special=userdefined for more information.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="maxItems" type="int"> + <description><![CDATA[The maximum items in the menu. More items will be ignored. + +Takes precedence over HMENU.maxItems]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="minItems" type="int"> + <description><![CDATA[The minimum items in the menu. If the number of pages does not reach this level, a dummy-page with the title "..." and uid=[currentpage_id] is inserted. + +Takes precedence over HMENU.minItems]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="sectionIndex" type="string"> + <description><![CDATA[This is a property that all menuObj's share. If it's set, then the menu will not consist of links to pages on the "next level" but rather links to the parent page to the menu, but in addition "#"-links to the cObjects rendered on the page. In other words, the menuitems will be links to the content elements (with colPos=0!) on the page. A section index. + +.sectionIndex = [boolean] + +If you set this, all content elements (from tt_content table) of "Column" = "Normal" and the "Index"-check box clicked are selected. This corresponds to the "Menu/Sitemap" content element when "Section index" is selected as type. + +.sectionIndex.type = "all" / "header" + +If you set this additional property to "all", then the "Index"-checkbox is not considered and all content elements with colPos=0 is selected. + +If this property is "header" then only content elements with a visible header-layout (and a non-empty 'header'-field!) is selected. In other words, if the header layout of an element is set to "Hidden" then the page will not appear in the menu. + +The data-record /Behind the scene: + +When the menu-records are selected it works like this: The parent page record is used as the "base" for the menu-record. That means that any "no_cache" or "target"-properties of the parent page is used for the whole menu. + +But of course some fields from the tt_content records are transferred: This is how it mapped: + +$temp[$row[uid]]=$basePageRow; + +$temp[$row[uid]]["title"]=$row["header"]; + +$temp[$row[uid]]["subtitle"]=$row["subheader"]; + +$temp[$row[uid]]["starttime"]=$row["starttime"]; + +$temp[$row[uid]]["endtime"]=$row["endtime"]; + +$temp[$row[uid]]["fe_group"]=$row["fe_group"]; + +$temp[$row[uid]]["media"]=$row["media"]; + +$temp[$row[uid]]["header_layout"]=$row["header_layout"]; + +$temp[$row[uid]]["bodytext"]=$row["bodytext"]; + +$temp[$row[uid]]["image"]=$row["image"]; + +$temp[$row[uid]]["sectionIndex_uid"]=$row["uid"]; + +Basically this shows that + +- the field "header" and "subheader" from tt_content are mapped to "title" and "subtitle" in the pages-record. Thus you shouldn't need to change your standard menu-objects to fit this thing... + +- the fields "starttime", "endtime", "fe_group", "media" from tt_content are mapped to the same fields in a pages-record. + +- the fields "header_layout", "bodytext" and "image" are mapped to non-existing fields in the page-record + +- a new field, "sectionIndex_uid" is introduced in the page-record. If this field is present in a page record, the linkData()-function will prepend a hash-mark and the number of the field. + +NOTE: + +You cannot create submenus to sectionIndex-menus. That doesn't make any sense as these elements are not pages and thereby have no "childs". +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="showAccessRestrictedPages" type="string"> + <description><![CDATA[integer (page id) / keyword "NONE" + If set, pages in the menu will include pages with frontend user group access enabled.  However the page is of course not accessible and therefore the URL in the menu will be linked to the page with the ID of this value. On that page you could put a login form or other message. +If the value is "NONE" the link will not be changed and the site will perform page-not-found handling when clicked (which can be used to capture the event and act accordingly of course). + +Properties: +.addParam = Additional parameter for the URL, which can hold two markers; ###RETURN_URL### which will be substituted with the link the page would have had if it had been accessible and ###PAGE_ID### holding the page id of the page coming from (could be used to look up which fe_groups was required for access. + +Example: +showAccessRestrictedPages = 22 +showAccessRestrictedPages.addParams = &return_url=###RETURN_URL###&pageId=###PAGE_ID### + +The example will link access restricted menu items to page id 22 with the return URL in the GET var "return_url" and the page id in the GET var "pageId".]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="submenuObjSuffixes" type="string"> + <description><![CDATA[Defines a suffix for alternative sub-level menu objects. Useful to create special submenus depending on their parent menu element. See example below. + +Example: +This example will generate a menu where the menu objects for the second level will differ depending on the number of the first level item for which the submenu is rendered. The second level objects used are "2" (the default), "2a" and "2b" (the alternatives). Which of them is used is defined by "1.submenuObjSuffixes" which has the configuration "a |*| |*| b". This configuration means that the first menu element will use configuration "2a" and the last will use "2b" while anything in between will use "2" (no suffix applied) + +page.200 = HMENU +page.200 { +  1 = TMENU +  1.wrap = <div style="width:200px; border: 1px solid;">|</div> +  1.expAll = 1 +  1.submenuObjSuffixes = a |*|  |*| b +  1.NO.allWrap = <b>|</b><br/> + +  2 = TMENU +  2.NO.allWrap = <div style="background:red;">|</div> + +  2a = TMENU +  2a.NO.allWrap = <div style="background:yellow;">|</div> + +  2b = TMENU +  2b.NO.allWrap = <div style="background:green;">|</div> +} + +The result can be seen in the image below (applied on the testsite package): + + + +Applies to TMENU on >= 2nd level in a menu.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="TMENU" extends="mObj"> + <property name="ACT" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Enable/Configuration for menu items which are found in the rootLine]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="ACTRO" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Enable/Configuration for menu items which are found in the rootLine]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="ACTIFSUB" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Enable/Configuration for menu items which are found in the rootLine and has subpages]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="ACTIFSUBRO" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Enable/Configuration for menu items which are found in the rootLine and has subpages]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="CUR" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Enable/Configuration for a menu item if the item is the current page.]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="CURRO" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Enable/Configuration for a menu item if the item is the current page.]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="CURIFSUB" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Enable/Configuration for a menu item if the item is the current page and has subpages.]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="CURIFSUBRO" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Enable/Configuration for a menu item if the item is the current page and has subpages.]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="IFSUB" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Enable/Configuration for menu items which has subpages]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="IFSUBRO" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Enable/Configuration for menu items which has subpages]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="NO" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + The default "Normal" state rendering of Item. This is required for all menus. +If you specify properties for the "NO" property you do not have to set it "1". Otherwise with no properties setting "NO=1" will render the menu anyways (for TMENU this may make sense). + +The simplest menu TYPO3 can generate is then: + +page.20 = HMENU +page.20.1 = TMENU +page.20.1.NO = 1 + +That will be pure <a> tags wrapped around page titles.]]></description> + <default><![CDATA[1]]></default> + </property> + <property name="SPC" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Enable/Configuration for 'Spacer' pages. +Spacers are pages of the doktype "Spacer". These are not viewable pages but "placeholders" which can be used to divide menuitems.]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="USERDEF1" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Userdefined, see .itemArrayProcFunc for details on how to use this. +You can set the ITEM_STATE values USERDEF1 and USERDEF2 (+...RO) from a script/userfunction processing the menu item array. See HMENU/special=userdefined or the property .itemArrayProcFunc of the menu objects.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="USERDEF1RO" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Userdefined, see .itemArrayProcFunc for details on how to use this. +You can set the ITEM_STATE values USERDEF1 and USERDEF2 (+...RO) from a script/userfunction processing the menu item array. See HMENU/special=userdefined or the property .itemArrayProcFunc of the menu objects.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="USERDEF2" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + (See above)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="USERDEF2RO" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + (See above)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="USR" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Enable/Configuration for menu items which are access restricted pages that a user has access to.]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="USRRO" type="TMENUITEM"> + <description><![CDATA[Boolean / (config) + Enable/Configuration for menu items which are access restricted pages that a user has access to.]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="IProcFunc" type="string"> + <description><![CDATA[function-name + The internal array "I" is passed to this function and expected returned as well. Subsequent to this function call the menu item is compiled by implode()'ing the array $I[parts] in the passed array. Thus you may modify this if you need to. +See example on the testsite and in media/scripts/example_itemArrayProcFunc.php]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="debugItemConf" type="string"> + <description><![CDATA[Outputs (by the debug()-function) the configuration arrays for each menuitem. Useful to debug optionSplit things and such... +Applies to TMENU]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="expAll" type="stdWrap">boolean/stdWrap + <description><![CDATA[If this is true, the menu will always show the menu on the level underneath the menuitem. This corresponds to a situation where a user has clicked a menuitem and the menu folds out the next level. This can enable that to happen on all items as default.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="forceTypeValue" type="int"> + <description><![CDATA[If set, the &type parameter of the link is forced to this value regardless of target.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="JSWindow" type="boolean"> + <description><![CDATA[If set, the links of the menu-items will open by JavaScript in a pop-up window. + +.newWindow boolean, that lets every menuitem open in its own window opposite to opening in the same window for each click. + +.params is the list of parameters sent to the JavaScript open-window function, eg: +width=200,height=300,status=0,menubar=0 +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="overrideId" type="int"> + <description><![CDATA[integer (page-id) + If set, then all links in the menu will point to this pageid. Instead the real uid of the page is sent by the parameter "&real_uid=[uid]". +This feature is smart, if you have inserted a menu from somewhere else, perhaps a shared menu, but wants the menuitems to call the same page, which then generates a proper output based on the real_uid. +Applies to TMENU]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[Wraps the whole item using stdWrap +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="submenuObjSuffixes" type="string"> + <description><![CDATA[Defines a suffix for alternative sub-level menu objects. Useful to create special submenus depending on their parent menu element. See example below. + +Example: +This example will generate a menu where the menu objects for the second level will differ depending on the number of the first level item for which the submenu is rendered. The second level objects used are "2" (the default), "2a" and "2b" (the alternatives). Which of them is used is defined by "1.submenuObjSuffixes" which has the configuration "a |*| |*| b". This configuration means that the first menu element will use configuration "2a" and the last will use "2b" while anything in between will use "2" (no suffix applied) + +page.200 = HMENU +page.200 { +  1 = TMENU +  1.wrap = <div style="width:200px; border: 1px solid;">|</div> +  1.expAll = 1 +  1.submenuObjSuffixes = a |*|  |*| b +  1.NO.allWrap = <b>|</b><br/> + +  2 = TMENU +  2.NO.allWrap = <div style="background:red;">|</div> + +  2a = TMENU +  2a.NO.allWrap = <div style="background:yellow;">|</div> + +  2b = TMENU +  2b.NO.allWrap = <div style="background:green;">|</div> +} + +The result can be seen in the image below (applied on the testsite package): + + + +Applies to TMENU on >= 2nd level in a menu.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="target" type="string"> + <description><![CDATA[target + Target of the menulinks]]></description> + <default><![CDATA[self]]></default> + </property> + <property name="wrap" type="wrap"> + <description><![CDATA[Wraps only if there were items in the menu!]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="TMENUITEM"> + <property name="ATagBeforeWrap" type="boolean"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="ATagParams" type="stdWrap"> + <description><![CDATA[<A>-params /stdWrap + Additional parameters + +Example: +class="board"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="ATagTitle" type="stdWrap"> + <description><![CDATA[Allows you to specify the "title" attribute of the <a> tag around the menu item. + +Example: +ATagTitle.field = abstract // description + +This would use the abstract or description field for the <a title=""> attribute.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="RO" type="boolean"> + <description><![CDATA[if set, rollOver is enabled for this link]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="additionalParams" type="stdWrap"> + <description><![CDATA[Define parameters that are added to the end of the URL. This must be code ready to insert after the last parameter. + +For details, see typolink->additionalParams]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="after" type="stdWrap"> + <description><![CDATA[HTML /stdWrap + The series of "before..." properties are duplicated to "after..." properties as well. The only difference is that the output generated by the .after.... properties are placed after the link and not before. +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="afterImg" type="imgResource">The series of "before..." properties are duplicated to "after..." properties as well. The only difference is that the output generated by the .after.... properties are placed after the link and not before. + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="afterImgLink" type="boolean"> + <description><![CDATA[If set, this image is linked with the same <A> tag as the text + The series of "before..." properties are duplicated to "after..." properties as well. The only difference is that the output generated by the .after.... properties are placed after the link and not before.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="afterImgTagParams" type="string"> + <description><![CDATA[<img>-params + The series of "before..." properties are duplicated to "after..." properties as well. The only difference is that the output generated by the .after.... properties are placed after the link and not before. +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="afterROImg" type="imgResource"> + <description><![CDATA[If set, ".afterImg" and ".afterROImg" is expected to create a rollOver-pair. + The series of "before..." properties are duplicated to "after..." properties as well. The only difference is that the output generated by the .after.... properties are placed after the link and not before. ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="afterWrap" type="wrap"> + <description><![CDATA[wrap around the ".after"-code + The series of "before..." properties are duplicated to "after..." properties as well. The only difference is that the output generated by the .after.... properties are placed after the link and not before.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="allStdWrap" type="stdWrap"> + <description><![CDATA[stdWrap of the whole item]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="allWrap" type="stdWrap"> + <description><![CDATA[wrap /stdWrap + Wraps the whole item]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="altTarget" type="string"> + <description><![CDATA[target + Alternative target overriding the target property of the TMENU if set.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="before" type="stdWrap"> + <description><![CDATA[HTML /stdWrap +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="beforeImg" type="imgResource"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="beforeImgLink" type="boolean"> + <description><![CDATA[If set, this image is linked with the same <A> tag as the text]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="beforeImgTagParams" type="string"> + <description><![CDATA[<img>-params +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="beforeROImg" type="imgResource"> + <description><![CDATA[If set, ".beforeImg" and ".beforeROImg" is expected to create a rollOver-pair. ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="beforeWrap" type="wrap"> + <description><![CDATA[wrap around the ".before"-code]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="doNotLinkIt" type="boolean"> + <description><![CDATA[if set, the linktext are not linked at all!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="doNotShowLink" type="boolean"> + <description><![CDATA[if set, the text will not be shown at all (smart with spacers)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="linkWrap" type="wrap"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[stdWrap to the link-text! ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap2" type="stdWrap"> + <description><![CDATA[stdWrap to the total link-text and ATag.  (Notice that the plain default value passed to stdWrap function is "|".)]]></description> + <default><![CDATA[ | ]]></default> + </property> + <property name="subst_elementUid" type="boolean"> + <description><![CDATA[If set, all appearances of the string '{elementUid}' in the total element html-code (after wrapped in .allWrap} is substituted with the uid number of the menu item. +This is useful if you want to insert an identification code in the HTML in order to manipulate properties with JavaScript.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrapItemAndSub " type="wrap"> + <description><![CDATA[Wraps the whole item and any submenu concatenated to it.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="stringList"/> + <type id="charRangeMap" extends="array"/> + <type id="wrap"/> + <type id="wrapSplitChar" extends="wrap"> + <property name="splitChar" type="string"> + <description><![CDATA[defines an alternative splitting character (default is "|" - the vertical line)]]></description> + <default><![CDATA[|]]></default> + </property> + </type> + <type id="HTMLparser"> + <property name="allowTags" type="string"> + <description><![CDATA[Default allowed tags]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="globalNesting" type="string"> + <description><![CDATA[List of tags (among the already set tags), which will be forced to have the nesting-flag set to "global"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="htmlSpecialChars" type="int"> + <description><![CDATA[values: -1 / 0 / 1 / 2 + This regards all content which is NOT tags: +"0" means "disabled" - nothing is done +"1" means the content outside tags is htmlspecialchar()'ed (PHP-function which converts &"<> to &...;) +"2" is the same as "1" but entities like "&" or "ê" are untouched. +"-1" does the opposite of "1" - converts < to <, > to >, " to " etc.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="keepNonMatchedTags" type="string"> + <description><![CDATA[If set (true=1), then all tags are kept regardless of tags present as keys in $tags-array. +If "protect", then the preserved tags have their <> converted to < and > +Default is to REMOVE all tags, which are not specifically assigned to be allowed! So you might probably want to set this value!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="localNesting" type="string"> + <description><![CDATA[List of tags (among the already set tags), which will be forced to have the nesting-flag set to true]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="noAttrib" type="string"> + <description><![CDATA[List of tags (among the already set tags), which will be forced to have the allowedAttribs value set to zero (which means, all attributes will be removed.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="removeTags" type="string"> + <description><![CDATA[List of tags (among the already set tags), which will be configured so they are surely removed.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="rmTagIfNoAttrib" type="string"> + <description><![CDATA[List of tags (among the already set tags), which will be forced to have the rmTagIfNoAttrib set to true]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="tags.[tagname]" type="HTMLparser_tags"> + <description><![CDATA[Either set this property to 0 or 1 to allow or deny the tag. If you enter ->HTMLparser_tags properties, those will automatically overrule this option, thus it's not needed then. +[tagname] in lowercase.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="HTMLparser_tags"> + <property name="allowedAttribs" type="string"> + <description><![CDATA['0' (zero) = no attributes allowed, '[commalist of attributes]' = only allowed attributes. If blank/not set, all attributes are allowed.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="fixAttrib" type="array"> + <description><![CDATA[fixAttrib.[attribute].always = true / false +If set, the attribute is always processed. Normally an attribute is processed only if it exists + +fixAttrib.[attribute].casesensitiveComp = true / false +If set, the comparison in .removeIfEquals and .list will be case-sensitive. At this point, it's insensitive.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="fixAttrib.[attribute]" type="HTMLparser_tags_fixAttrib"> + <description><![CDATA[If no attribute exists by this name, this value is set as default value (if this value is not blank)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="nesting" type=""> + <description><![CDATA[If set true, then this tag must have starting and ending tags in the correct order. Any tags not in this order will be discarded. Thus '</B><B><I></B></I></B>' will be converted to '<B><I></B></I>'. +Is the value "global" then true nesting in relation to other tags marked for "global" nesting control is preserved. This means that if <B> and <I> are set for global nesting then this string '</B><B><I></B></I></B>' is converted to '<B></B>']]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="overrideAttribs" type="string"> + <description><![CDATA[If set, this string is preset as the attributes of the tag. ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="protect" type="boolean"> + <description><![CDATA[If set, the tag <> is converted to < and >]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="remap" type="string"> + <description><![CDATA[If set, the tagname is remapped to this tagname]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="rmTagIfNoAttrib" type="boolean"> + <description><![CDATA[If set, then the tag is removed if no attributes happened to be there.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="HTMLparser_tags_fixAttrib"> + <property name="default" type="string"> + <description><![CDATA[If no attribute exists by this name, this value is set as default value (if this value is not blank)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="list" type="stringList"> + <description><![CDATA[Attribute value must be in this list. If not, the value is set to the first element.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="prefixRelPathWith" type="string"> + <description><![CDATA[If the value of the attribute seems to be a relative URL (no scheme like "http" and no "/" as first char) then that value of this property will be prefixed the attribute. + +Example: + +...fixAttrib.src.prefixRelPathWith = http://192.168.230.3/typo3/32/dummy/]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="range" type="intList"> + <description><![CDATA[Setting integer range. ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="removeIfEquals" type="string"> + <description><![CDATA[If the attribute value matches the value set here, then it is removed.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="removeIfFalse" type="stingList"> + <description><![CDATA[boolean/"blank" string +If set, then the attribute is removed if it is "false". If this value is set to "blank" then the value must be a blank string (that means a "zero" value will not be removed)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="set" type="string"> + <description><![CDATA[Force the attribute value to this value.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="trim" type="boolean"> + <description><![CDATA[If any of these keys are set, the value is passed through the respective PHP-functions.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="unset" type="boolean"> + <description><![CDATA[ If set, the attribute is unset.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="userFunc" type="string"> + <description><![CDATA[User function for processing of the attribute. + +Example: + +...fixAttrib.href.userFunc = tx_realurl->test_urlProc]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="IMAGE"> + <property name="altText" type="stdWrap"> + <description><![CDATA[If no alttext is specified, it will use an empty alttext + +("alttext" is the old spelling of this attribute. It will be used only if "altText" does not specify a value or properties)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="emptyTitleHandling" type="string"> + <description><![CDATA[Value can be "keepEmpty" to preserve an empty title attribute, or "useAlt" to use the alt attribute instead. +]]></description> + <default><![CDATA[useAlt +]]></default> + </property> + <property name="cache" type="cache"> + <description><![CDATA[ + Stores the rendered content into the caching framework and reads it from there. + This allows you to reuse this content without prior rendering. The presence of cache.key will trigger this feature. +]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="file" type="imgResource"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="if" type="if"> + <description><![CDATA[if "if" returns false the image is not shown!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="imageLinkWrap" type="imageLinkWrap"> + <description><![CDATA[boolean / imageLinkWrap + +CAUTION: only active if set to 1: +imageLinkWrap = 1 + +Additional Note: ONLY active if linkWrap is NOT set and file is NOT GIFBUILDER (as it works with the original imagefile)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="titleText" type="stdWrap"> + <description><![CDATA[If no titletext is specified, it will use the alttext instead. If no alttext is specified, it will use an empty alttext]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="linkWrap" type="wrap"> + <description><![CDATA[(before ".wrap")]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="params" type="stdWrap"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap" type="wrap"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="IMG_RESOURCE"> + <property name="cache" type="cache"> + <description><![CDATA[ + Stores the rendered content into the caching framework and reads it from there. + This allows you to reuse this content without prior rendering. The presence of cache.key will trigger this feature. +]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="file" type="imgResource"> + <description><![CDATA[ ]]></description> + <default><![CDATA[]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[ ]]></description> + <default><![CDATA[]]></default> + </property> + </type> + <type id="IMGTEXT" extends="cObjArray"> + <property name="1" type="IMAGE"> + <description><![CDATA[Rendering of the images +The register "IMAGE_NUM" is set with the number of image being rendered for each rendering of an image-object. Starting with zero. +The image-object should not be of type GIFBUILDER! +Important: +"file.import.current = 1" fetches the name of the images! +</description>]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="2" type="IMAGE"> + <description><![CDATA[Rendering of the images +The register "IMAGE_NUM" is set with the number of image being rendered for each rendering of an image-object. Starting with zero. +The image-object should not be of type GIFBUILDER! +Important: +"file.import.current = 1" fetches the name of the images! +</description>]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="3" type="IMAGE"> + <description><![CDATA[Rendering of the images +The register "IMAGE_NUM" is set with the number of image being rendered for each rendering of an image-object. Starting with zero. +The image-object should not be of type GIFBUILDER! +Important: +"file.import.current = 1" fetches the name of the images! +</description>]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="altText" type="stdWrap"> + <description><![CDATA[Default altText/titleText if no alternatives are provided by the ->IMAGE cObjects + +If no alttext is specified, it will use an empty alttext]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="borderCol" type="stdWrap"> + <description><![CDATA[Color of the border, if ".border" is set]]></description> + <default><![CDATA[black]]></default> + </property> + <property name="borderThick" type="stdWrap"> + <description><![CDATA[Width of the border around the pictures]]></description> + <default><![CDATA[1]]></default> + </property> + <property name="caption" type="stdWrap"> + <description><![CDATA[Caption]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="captionSplit" type="boolean"> + <description><![CDATA[If this is set, the caption text is split by the character (or string) from ".token" , and every item is displayed under an image each in the image block. +.token = (string /stdWrap) Character to split the caption elements (default is chr(10)) +.cObject = cObject, used to fetch the caption for the split +.stdWrap = stdWrap properties used to render the caption.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="colRelations" type="stdWrap"> + <description><![CDATA[This value defines the width-relations of the images in the columns of IMGTEXT. The syntax is "[int] : [int] : [int] : ..." for each column. If there are more imagecolumns than figures in this value, it's ignored. If the relation between two of these figures exceeds 10, this function is ignore. +It works only fully if all images are downscaled by their maxW-definition. + +Example: +If 6 images are placed in three columns and their width's are high enough to be forcibly scaled, this value will scale the images in the to be eg. 100, 200 and 300 pixels from left to right +1 : 2 : 3]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="colSpace" type="stdWrap"> + <description><![CDATA[space between columns]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="cols" type="stdWrap "> + <description><![CDATA[Columns]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="emptyTitleHandling" type="string"> + <description><![CDATA[Value can be "keepEmpty" to preserve an empty title attribute, or "useAlt" to use the alt attribute instead. +]]></description> + <default><![CDATA[useAlt +]]></default> + </property> + <property name="equalH" type="stdWrap"> + <description><![CDATA[If this value is greater than zero, it will secure that images in a row has the same height. The width will be calculated. +If the total width of the images raise above the "maxW"-value of the table the height for each image will be scaled down equally so that the images still have the same height but is within the limits of the totalWidth. +Please note that this value will override the properties "width", "maxH", "maxW", "minW", "minH" of the IMAGE-objects generating the images. It will generate a table with no columns!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="image_frames" type="array "> + <description><![CDATA[Frames: +.key points to the frame used. + +".image_frames.x" is imgResource-mask (".m")properties which will override to the [imgResource].m properties of the imageObjects. This is used to mask the images into a frame. + +Example: +1 { + mask = media/uploads/darkroom1_mask.jpg + bgImg = GIFBUILDER + bgImg { + XY = 100,100 + backColor = {$bgCol} + } + bottomImg = GIFBUILDER + bottomImg { + XY = 100,100 + backColor = black + } + bottomImg_mask = media/uploads/darkroom1_bottom.jpg +} + +NOTE: This cancels the jpg-quality settings sent as ordinary ".params" to the imgResource. In addition the output of this operation will always be jpg or gif! +NOTE: Works ONLY if IMAGE-obj is NOT GIFBUILDER]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="imgList" type="stdWrap"> + <description><![CDATA[list of images from ".imgPath" + +Example: +This imports the list of images from tt_content's image-field +"imgList.field = image"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="imgMax" type="stdWrap"> + <description><![CDATA[max number of images]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="imgObjNum" type="int"> + <description><![CDATA[ +optionSplit +Here you define, which IMAGE-cObjects from the array "1,2,3,4..." in this object that should render the images. +"current" is set to the image-filename. + +Example: +"imgObjNum = 1 |*||*| 2": +This would render the first two images with "1. ..." and the last image with "2. ...", provided that the ".imgList" contains 3 images.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="imgPath" type="stdWrap"> + <description><![CDATA[Path to the images + +Example: +"uploads/pics/"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="imgStart" type="stdWrap"> + <description><![CDATA[start with image-number ".imgStart"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="maxW" type="stdWrap"> + <description><![CDATA[max width of the image-table. +This will scale images not in the right size! Takes the number of columns into account! + +NOTE: Works ONLY if IMAGE-obj is NOT GIFBUILDER]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="maxWInText" type="stdWrap"> + <description><![CDATA[max width of the image-table, if the text is wrapped around the image-table (on the left or right side). +This will scale images not in the right size! Takes the number of columns into account! + +NOTE: Works ONLY if IMAGE-obj is NOT GIFBUILDER]]></description> + <default><![CDATA[50% of maxW]]></default> + </property> + <property name="rowSpace" type="stdWrap"> + <description><![CDATA[space between rows]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="rows" type="stdWrap"> + <description><![CDATA[Rows (higher priority than "cols")]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="spaceBelowAbove" type="stdWrap"> + <description><![CDATA[Pixel space between content and images when position of image is above or below text (but not in text)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="tableStdWrap" type="stdWrap"> + <description><![CDATA[This passes the final <table> code for the image block to the stdWrap function.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="text" type="stdWrap"> + <description><![CDATA[Use this to import / generate the content, that should flow around the imageblock.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="textMargin" type="stdWrap"> + <description><![CDATA[margin between the image and the content]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="textMargin_outOfText" type="boolean"> + <description><![CDATA[If set, the textMargin space will still be inserted even if the image is placed above or below the text. +This flag is only for a kind of backwards compatibility because this "feature" was recently considered a bug and thus corrected. So if anyone has depended on this way things are done, you can compensate with this flag.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="textPos" type="stdWrap"> + <description><![CDATA[Textposition: +bit[0-2]: 000 = centre, 001 = right, 010 = left +bit[3-5]: 000 = over, 001 = under, 010 text + +0 - Above: Centre +1 - Above: Right +2 - Above: Left +8 - Below: Centre +9 - Below: Right +10 - Below: Left +17 - In Text: Right +18 - In Text: Left +25 - In Text: Right (no wrap) +26 - In Text: Left (no wrap)]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="LOAD_REGISTER" extends="array"> + <property name="[myRegisterVar1]" type="stdWrap"> + <description><![CDATA[Example: +(This sets "contentWidth", "label" and "head") + +page.27 = LOAD_REGISTER +page.27 { + contentWidth = 500 + + label.field = header + + head = some text + head.wrap = <B> | </B> +}]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="[myRegisterVar2]" type="stdWrap"> + <default><![CDATA[ +]]></default> + </property> + <property name="[myRegisterVar3]" type="stdWrap"> + <default><![CDATA[ +]]></default> + </property> + <property name="cache" type="cache"> + <description><![CDATA[ + Stores the rendered content into the caching framework and reads it from there. + This allows you to reuse this content without prior rendering. The presence of cache.key will trigger this feature. +]]></description> + <default><![CDATA[0]]></default> + </property> + </type> + <type id="META" extends="array"> + <property name="REFRESH" type="stdWrap"> + <description><![CDATA[Meta tag +If value is empty (after trimming) the meta tag is not generated. +[seconds]; [url, leave blank for same page] +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="DESCRIPTION" type="stdWrap"> + <description><![CDATA[Meta tag +If value is empty (after trimming) the meta tag is not generated. +If the "key" (eg. "REFRESH" or "DESCRIPTION") is "REFRESH" (caseinsensitive), then the "http-equiv"-attribute is used in the meta tag instead of "name". + +Examples: +.REFRESH = [seconds]; [url, leave blank for same page] +.DESCRIPTION = This is the description of the content in this document +.KEYWORDS = This is the keywords...]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="KEYWORDS" type="stdWrap"> + <description><![CDATA[Meta tag +If value is empty (after trimming) the meta tag is not generated. +Examples: +.KEYWORDS = This, is, the, list, of, keywords]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="[myMetaTag]" type="stdWrap"> + <description><![CDATA[Meta tag +If value is empty (after trimming) the meta tag is not generated. +Of course you can specify your own meta tags too...]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="MULTIMEDIA"> + <property name="file" type="stdWrap"> + <description><![CDATA[The multimedia file. Types are: +txt, html, htm:Inserted directly +class:Java-applet +swf:Flash animation +swa, dcr:ShockWave Animation +wav,au,ogg,opus,flac:Sound +avi,mov,asf,mpg,wmv:Movies (AVI, QuickTime, MPEG4)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="params" type="stdWrap"> + <description><![CDATA[This is parameters for the multimedia-objects. Use this to enter stuff like with and height: + +Example: +width=200 +height=300 + +... will generate a tag like '<embed .... width="200" height="300">' +height= + +An empty string will remove the parameter from the embed-tag]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="RECORDS"> + <property name="cache" type="cache"> + <description><![CDATA[ + Stores the rendered content into the caching framework and reads it from there. + This allows you to reuse this content without prior rendering. The presence of cache.key will trigger this feature. +]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="conf" type="array"> + <description><![CDATA[Config-array which renders records from table tablename]]></description> + <default><![CDATA[If this is NOT defined, the rendering of the records is done with the toplevel-object [tablename] - just like the cObject, CONTENT!]]></default> + </property> + <property name="dontCheckPid" type="boolean"> + <description><![CDATA[Normally a record cannot be selected, if it's parent page (pid) is not accessible for the website user. This option disables that check.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="source" type="stdWrap"> + <description><![CDATA[List of record-id's, optionally with appended table-names. + +Example: +tt_content_34, 45, tt_links_56]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="tables" type="stringList"> + <description><![CDATA[List of accepted tables. If any items in the ".source"-list is not prepended with a tablename, the first table in this list is assumed to be the table for such records. +Also tablenames configured in .conf is allowed. + +Example: +tables = tt_content, tt_address, tt_links +conf.tx_myexttable = TEXT +conf.tx_myexttable.value = Hello world + +This adds the tables tt_content, tt_address, tt_links, tx_myexttable]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap" type="wrap"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="SEARCHRESULT"> + <property name="addExtUrlsAndShortCuts" type="boolean"> + <description><![CDATA[If set, then the doktypes 3 (Link) and 4 (Shortcut) are added to the doktypes being searched. +However at this point in time, no pages will be select if they do not have at least one tt_content record on them! That is because the pages and tt_content (or other) table is joined. So there must at least one occurrence of a tt_content element on an Link / Shortcut page for them to show up.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="allowedCols" type="string"> + <description><![CDATA[List (separated by ":") of allowed table-cols. + +Example: +pages.title:tt_content.bodytext]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="languageField.[2nd table]" type="string"> + <description><![CDATA[Setting a field name to filter language on. This works like the "languageField" setting in ->select + +Example: + +languageField.tt_content = sys_language_uid]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="layout" type="string"> + <description><![CDATA[This defines how the search content is shown. + +Example: +This substitutes the following fields: +###RANGELOW###:The low result range, eg. "1" +###RANGEHIGH###:The high result range, eg. "10" +###TOTAL###:The total results +###RESULT###:The result itself +###NEXT###:The next-button +###PREV###:The prev-button]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="next" type="cObj"> + <description><![CDATA[This cObject will be wrapped by a link to the next searchresult. This is the code substituting the "###NEXT###"-mark]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="noOrderBy" type="boolean"> + <description><![CDATA[If this is set, the result is NOT sorted after lastUpdated, tstamp for the pages-table.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="noResultObj" type="cObj"> + <description><![CDATA[the cObject used if the search results in no rows.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="prev" type="cObj"> + <description><![CDATA[This cObject will be wrapped by a link to the prev searchresult. This is the code substituting the "###PREV###"-mark]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="range" type="int"> + <description><![CDATA[The number of results at a time!]]></description> + <default><![CDATA[20]]></default> + </property> + <property name="renderObj" type="cObj"> + <description><![CDATA[the cObject to render the searchresults +$cObj->data array is set to the resulting record from the search. +Please note, that in all fields are named [tablename]_[fieldname]. Thus the page title is in the field "pages_title". +Apart from this, these fields from the pages-table are also present: +uid]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="renderWrap" type="wrap"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="resultObj" type="cObj"> + <description><![CDATA[the cObject prepended in the search results returns rows]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[Wrap the whole content...]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="target" type="target"> + <description><![CDATA[target til next/prev links!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap" type="wrap"> + <description><![CDATA[Wrap the whole content...]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="TEXT" extends="stdWrap"> + <property name="cache" type="cache"> + <description><![CDATA[ + Stores the rendered content into the caching framework and reads it from there. + This allows you to reuse this content without prior rendering. The presence of cache.key will trigger this feature. +]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="value" type="stdWrap"> + <description><![CDATA[text, wrap with stdWrap properties]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="USER"> + <property name="cache" type="cache"> + <description><![CDATA[ + Stores the rendered content into the caching framework and reads it from there. + This allows you to reuse this content without prior rendering. The presence of cache.key will trigger this feature. +]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="userFunc" type="string"> + <description><![CDATA[The name of the function. If you specify the name with a '->' in, it's interpreted as a call to a method in a class. +Two parameters are sent: A content variable (which is empty in this case, but not when used from stdWrap function .postUserFunc and .preUserFunc) and the second parameter is an array with the properties of this cObject if any. + +Example: +This TypoScript will display all content element headers of a page in reversed order. Please take a look in media/scripts/example_callfunction.php!! +(Also demonstrated on the testsite, page + +page = PAGE +page.typeNum=0 + +page.30 = USER +page.30 { + userFunc = user_various->listContentRecordsOnPage + reverseOrder = 1 +}]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="USER_INT"> + <property name="cache" type="cache"> + <description><![CDATA[ + Stores the rendered content into the caching framework and reads it from there. + This allows you to reuse this content without prior rendering. The presence of cache.key will trigger this feature. +]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="userFunc" type="string"> + <description><![CDATA[The name of the function. If you specify the name with a '->' in, it's interpreted as a call to a method in a class. +Two parameters are sent: A content variable (which is empty in this case, but not when used from stdWrap function .postUserFunc and .preUserFunc) and the second parameter is an array with the properties of this cObject if any. + +Example: +This TypoScript will display all content element headers of a page in reversed order. Please take a look in media/scripts/example_callfunction.php!! +(Also demonstrated on the testsite, page + +page = PAGE +page.typeNum=0 + +page.30 = USER +page.30 { + userFunc = user_various->listContentRecordsOnPage + reverseOrder = 1 +}]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="TLO"> + <property name="config" type="CONFIG"> + <description><![CDATA[Global configuration. +These values are stored with cached pages which means they are also accessible when retrieving a cached page.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="constants" type="CONSTANTS"> + <description><![CDATA[Site-specific constants, eg. a general email-adresse. These constants may be substituted in the text throughout the pages. The substitution is done by parseFunc. (Option: constants=1)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="resources" type="array"> + <description><![CDATA[Resources in list (internal)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="types" type="array"> + <description><![CDATA[Types (internal) +type=99 reserved for plaintext display ]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="PAGE" extends="cObjArray"> + <property name="10" type="cObj"> + <description><![CDATA[the object which should be rendered in the page. You have to set this to a contenttype (IMAGE, TEXT, HTML...) +You can specify as much elements in this array as you want. + +Example: + +page.10 = TEXT +page.10.value = Here goes the header +page.20 = TEXT +page.20.value = Hello World! +page.30 = TEXT +page.30.value = Here goes the footer]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="20" type="cObj"> + <description><![CDATA[the object which should be rendered in the page. You have to set this to a contenttype (IMAGE, TEXT, HTML...) +You can specify as much elements in this array as you want. + +Example: + +page.10 = TEXT +page.10.value = Here goes the header +page.20 = TEXT +page.20.value = Hello World! +page.30 = TEXT +page.30.value = Here goes the footer]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="30" type="cObj"> + <description><![CDATA[the object which should be rendered in the page. You have to set this to a contenttype (IMAGE, TEXT, HTML...) +You can specify as much elements in this array as you want. + +Example: + +page.10 = TEXT +page.10.value = Here goes the header +page.20 = TEXT +page.20.value = Hello World! +page.30 = TEXT +page.30.value = Here goes the footer]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="bgImg" type="imgResource"> + <description><![CDATA[Background image on the page. This is automatically added to the body-tag.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="bodyTag" type="string"> + <description><![CDATA[Bodytag on the page + +Example: +page.bodyTag = <body bgcolor="{$bgCol}">]]></description> + <default><![CDATA[<body bgcolor="#FFFFFF">]]></default> + </property> + <property name="bodyTagAdd" type="string"> + <description><![CDATA[This content is added to the end of the bodyTag.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="bodyTagCObject" type="cObj"> + <description><![CDATA[This is default bodytag overridden by ".bodyTag" if that is set.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="cssInline" type="CARRAY"> + <description><![CDATA[Use cObjects for creating inline CSS + +Example: + +cssInline { + 10 = TEXT + 10.value = h1 {margin:15px;} + + 20 = TEXT + 20.value = h1 span {color: blue;} +} + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="config" type="CONFIG"> + <description><![CDATA[configuration for the page. Any entries override the same entries in the toplevel-object "config".]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="disableBodyTag" type="boolean"> + <description><![CDATA[This option disables <body> tag generation by the TYPO3 core. It is useful for extensions like TemplaVoila, which can produce its own <body> tag with additional attributes.]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="footerData" type="CARRAY"> + <description><![CDATA[Same as headerData above, except that this block gets included at the bottom of the page (just before the closing body tag). + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="headTag" type="string"> + <description><![CDATA[Head-tag if alternatives are wanted]]></description> + <default><![CDATA[<head>]]></default> + </property> + <property name="headerData" type="cObjArray"> + <description><![CDATA[Inserts content in the header-section. Could be JavaScripts, meta-tags, other stylesheet references. +Is inserted after all the style-definitions.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="includeCSS" type="array"> + <description><![CDATA[Inserts a stylesheet (just like the .stylesheet property) by allows to setting up more than a single stylesheet, because you can enter files in an array. + +The file definition must be a valid "resource" datatype, otherwise nothing is inserted. + +Each file has optional properties: + +.media - setting the media attribute of the <style> tag. + +.title - setting the title of the <style> tag. + +.alternate - If set (boolean) then the rel-attribute will be "alternate stylesheet" + +.import - If set (boolean) then the @import way of including a stylesheet is used instead of <link> + +.allWrap - wraps the complete tag, useful for conditional comments. + +.external - If set, there is no file existence check. Useful for inclusion of external files. + +.inline - Inline the contents of the CSS file using a <style> tag. + +Example: + +includeCSS { + file1 = fileadmin/mystylesheet1.css + + file2 = stylesheet_uploaded_to_template*.css + file2.title = High contrast + file2.media = print + + ie6Style = fileadmin/css/style3.css + ie6Style.allWrap = <!--[if lte IE 7]>|<![endif]--> + + bootstrap = https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css + bootstrap.external = 1 +} +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="includeJS" type="array"> + <description><![CDATA[Inserts one or more (Java)Scripts in <script> tags. + +The file definition must be a valid "resource" datatype, otherwise nothing is inserted. This means that remote files cannot be referenced (i.e. using "http://..."), except by using the ".external" property. + +Each file has optional properties: + +.type - setting the MIME type of the script (default: empty, set it to "text/javascript" for backwards-compatibility) + +.forceOnTop - boolean flag. If set, this file will be added on top of all other files. + +.allWrap - wraps the complete tag, useful for conditional comments. + +.external - If set, there is no file existence check. Useful for inclusion of external files. + +Example: + +includeJS { + file1 = fileadmin/helloworld.js + file1.type = application/x-javascript + file2 = javascript_uploaded_to_template*.js +}]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="includeJSLibs" type="array"> + <description><![CDATA[Adds JS library files to head of page. + +The file definition must be a valid "resource" datatype, otherwise nothing is inserted. This means that remote files cannot be referenced (i.e. using "http://..."), except by using the ".external" property. + +Each file has optional properties: + +.allWrap - wraps the complete tag, useful for conditional comments. + +.external - If set, there is no file existence check. Useful for inclusion of external files. + +Example: + +includeJSLibs.twitter = https://twitter.com/javascripts/blogger.js + +includeJSLibs.twitter.external = 1 +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="includeJSFooter" type="array"> + <description><![CDATA[resource + Same as includeJS above, except that this block gets included at the bottom of the page (just before the closing body tag). +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="includeJSFooterlibs" type="array"> + <description><![CDATA[ Same as includeJSLibs above, except that this block gets included at the bottom of the page (just before the closing body tag). +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="inlineSettings" type="array"> + <description><![CDATA[array of strings +Adds settings to the page. + +Example: + +page.inlineSettings { + setting1 = Hello + setting2 = GoOnTop +} + +will produce following source: + +TYPO3.settings = {"TS":{"setting1":"Hello","setting2":"GoOnTop"}}; +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="jsInline" type="CARRAY"> + <description><![CDATA[Use cObjects for creating inline JavaScript + +Example: + +page.jsInline { +10 = TEXT +10.dataWrap = var pageId = {TSFE:id}; +} + +Note: + +with config.removeDefaultJS = external, the inlineJS is moved to external file. +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="jsFooterInline" type="CARRAY"> + <description><![CDATA[Same jsInline above, except that the JavaScript gets inserted at the bottom of the page (just before the closing body tag). +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="meta" type="META"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="shortcutIcon" type="string"> + <description><![CDATA[Favicon of the page. Create a reference to an icon here! +Browsers that support favicons display them in the browser's address bar, next to the site's name in lists of bookmarks, and next to the page's title in a Tabbed Document Interface. + +Note: +This must be a valid ".ico"-file (iconfile)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[Wraps the content of the cObject array with stdWrap options]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="typeNum" type="int"> + <description><![CDATA[This decides the typeId of the page. The value defaults to 0 for the first +found PAGE object, but it MUST be set and be unique as soon you use more than one such object (watch this if you use frames on your page)!]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="wrap" type="wrap"> + <description><![CDATA[Wraps the content of the cObject array]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="CONSTANTS"> + <property name="[myConstant]" type="string"> + <description><![CDATA[Constants. + +Examples: +.EMAIL = email@email.com +Now if parseFunc anywhere is configured with constants=1 then all cases of the string ###EMAIL### will be substituted in the text. +see ->parseFunc]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="encapsLines"> + <property name="addAttributes" type="array"> + <description><![CDATA[Attributes to set in the encapsulation tag. + +Example: +addAttributes.P { + style=padding-bottom:0px; margin-top:1px; margin-bottom:1px; + align=center +} + +([tagname] is in uppercase.) + +.setOnly = +exists : This will set the value ONLY if the property does not already exist +blank : This will set the value ONLY if the property does not already exist OR is blank ("") + +Default is to always override/set the attributes value.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="defaultAlign" type="stdWrap"> + <description><![CDATA[If set, this value is set as the default "align" value of the wrapping tags, both from .encapsTagList, .bypassEncapsTagList and .nonWrappedTag]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="encapsLinesStdWrap.[tagname]" type="stdWrap"> + <description><![CDATA[Wraps the content inside all encapsulated lines. +([tagname] is in uppercase.)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="encapsTagList" type="stringList"> + <description><![CDATA[List of tags which qualify as encapsulating tags. Must be lowercase. + +Example: +encapsTagList = div, p + +This setting will recognize the red line below as encapsulated lines: + +First line of text +Some <div>text</div> +<p>Some text</p> +<div>Some text</div> +<B>Some text</B>]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="innerStdWrap_all" type="stdWrap"> + <description><![CDATA[Wraps the content inside all lines, whether they are encapsulated or not.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="nonWrappedTag" type="string"> + <description><![CDATA[For all non-wrapped lines, you can set here which tag it should be wrapped in. Example would be "P". This is an alternative to .wrapNonWrappedLines and has the advantage that it's attributes are set by .addAttributes as well as defaultAlign. Thus you can easier match the wrapping tags used for nonwrapped and wrapped lines.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="remapTag.[tagname]" type="string"> + <description><![CDATA[Enter a new tag name here if you wish the tagname of any encapsulation to be unified to a single tag name. + +For instance, setting this value to "remapTags.P=DIV" would convert: + +<p>Some text</p> +<div>Some text</div> + +to + +<div>Some text</div> +<div>Some text</div> + +([tagname] is in uppercase.)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="removeWrapping" type="boolean"> + <description><![CDATA[If set, then all existing wrapping will be removed. + +This: + +First line of text +Some <div>text</div> +<p>Some text</p> +<div>Some text</div> +<B>Some text</B> + +becomes this: + +First line of text +Some <div>text</div> +Some text +Some text +<B>Some text</B>]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrapNonWrappedLines" type="wrap"> + <description><![CDATA[Wrapping for non-encapsulated lines + +Example: +.wrapNonWrappedLines = <P>|</P> + +This: + +First line of text +<p>Some text</p> + +becomes this: + +<P>First line of text</P> +<p>Some text</p>]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="filelink"> + <property name="ATagBeforeWrap" type="boolean"> + <description><![CDATA[If set, the link is first wrapped with ".wrap" and then the <A>-tag.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="ATagParams" type="stdWrap"> + <description><![CDATA[Additional parameters + +Example: +class="board"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="altText" type="stdWrap"> + <description><![CDATA[For icons (image made with "iconCObject" must have their own properties) + +If no alttext is specified, it will use an empty alttext]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="emptyTitleHandling" type="string"> + <description><![CDATA[Value can be "keepEmpty" to preserve an empty title attribute, or "useAlt" to use the alt attribute instead. +]]></description> + <default><![CDATA[useAlt +]]></default> + </property> + <property name="file" type="stdWrap"> + <description><![CDATA[stdWrap of the label (by default the label is the filename) after having been wrapped with A-tag!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="icon" type="stdWrap"> + <description><![CDATA[Set if icon should be shown]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="iconCObject" type="cObj"> + <description><![CDATA[Enter a cObject to use alternatively for the icons, eg. IMAGE type. +If this is set, it'll substitute the use of the thumbs-script for display of thumbnails.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="icon_image_ext_list" type="stringList"> + <description><![CDATA[These are the extensions that should render as thumbnails instead of icons.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="icon_link" type="boolean"> + <description><![CDATA[If the icon should be linked also]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="labelStdWrap" type="stdWrap"> + <description><![CDATA[stdWrap options for the label (by default the label is the filename) before being wrapped with the A-tags. +Use this to eg. import another label from a database field or such.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="path" type="stdWrap"> + <description><![CDATA[Example: +"uploads/media/"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="removePrependedNumbers" type="boolean"> + <description><![CDATA[if set, any 2-digit prepended numbers ("eg _23") in the filename is removed.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="size" type="stdWrap"> + <description><![CDATA[Set if size should be shown]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="target" type="string"> + <description><![CDATA[_self / _top / _blank / ... +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap" type="wrap"> + <description><![CDATA[Wraps the links.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="if"> + <property name="directReturn" type="boolean"> + <description><![CDATA[If this property exists the true/false of this value is returned. Could be used to set true/false by TypoScript constant]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="equals" type="stdWrap"> + <description><![CDATA[returns false if content does not equal ".value"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="isFalse" type="stdWrap"> + <description><![CDATA[If the content is "false"... (empty or zero)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="isGreaterThan" type="stdWrap"> + <description><![CDATA[returns false if content is not greater than ".value"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="isInList" type="stdWrap"> + <description><![CDATA[returns false if content is not in the comma-separated list ".value". +The list in ".value" may not have spaces between elements!!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="contains" type="stdWrap"> + <description><![CDATA[returns true if content string is in the haystack of ".value".]]></description> + <default><![CDATA[]]></default> + </property> + <property name="startsWith" type="stdWrap"> + <description><![CDATA[returns true if content string is the start of ".value".]]></description> + <default><![CDATA[]]></default> + </property> + <property name="endsWith" type="stdWrap"> + <description><![CDATA[returns true if content string is the end of ".value".]]></description> + <default><![CDATA[]]></default> + </property> + <property name="isLessThan" type="stdWrap"> + <description><![CDATA[returns false if content is not less than ".value"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="isPositive" type="stdWrap"> + <description><![CDATA[returns false if content is not positive]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="isTrue" type="stdWrap"> + <description><![CDATA[If the content is "true".... (not empty string and not zero)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="negate" type="boolean"> + <description><![CDATA[This negates the result just before it exits. So if anything above returns true the overall returns ends up returning false!!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="value" type="stdWrap"> + <description><![CDATA["value" (the comparison value mentioned above)]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="imageLinkWrap"> + <property name="JSwindow" type="stdWrap"> + <description><![CDATA[boolean/stdWrap + The image will be opened in a new window which is fitted to the dimensions of the image!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="JSwindow.altUrl" type="stdWrap"> + <description><![CDATA[If this returns anything, the URL shown in the JS-window is NOT showpic.php but the url given here!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="JSwindow.altUrl_noDefaultParams" type="boolean"> + <description><![CDATA[If this is set, the image parameters are not appended to the altUrl +automatically. This is useful if you want to create them with a userfunction +instead.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="JSwindow.expand" type="intList"> + <description><![CDATA[x and y is added to the window dimensions.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="JSwindow.newWindow" type="boolean"> + <description><![CDATA[Each picture will open in a new window!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="bodyTag" type="string"> + <description><![CDATA[Body tag of the new window]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="directImageLink" type="boolean"> + <description><![CDATA[If true, a link to the generated image file will be returned directly (showpic.php is not used)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="linkParams" type="typolink"> + <description><![CDATA[Allows the manipulation of the generated typolink if JSwindow is not used. + + Example: + linkParams.ATagParams.dataWrap = class="{$styles.content.imgtext.linkWrap.lightboxCssClass}" rel="{$styles.content.imgtext.linkWrap.lightboxRelAttribute}" +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="effects" type="string"> + <description><![CDATA[Example: +gamma=1,3 | sharpen=80 | solarize=70]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="enable" type="stdWrap"> + <description><![CDATA[The image is linked ONLY if this is true!!]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="file" type="stdWrap"> + <description><![CDATA[Override the path of the image which is displayed]]></description> + <default><![CDATA[ + ]]></default> + </property> + <property name="height" type="int"> + <description><![CDATA[Range: 1-1000 +If you add "m" to either the width or height, the image will be held in proportions and width/height works as max-dimensions]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="sample" type="boolean"> + <description><![CDATA[If set, -sample is used to scale images instead of -geometry. Sample does not use antialiasing and is therefore much faster.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[->stdWrap + Enable stdWrap for the image.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="target" type="string"> + <description><![CDATA[NOTE: Only if ".JSwindow" is set]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="title" type="string"> + <description><![CDATA[page title of the new window (HTML)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="typolink" type="typolink"> + <description><![CDATA[NOTE: This overrides the imageLinkWrap if it returns anything!!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="width" type="int"> + <description><![CDATA[Range: 1-1000 +If you add "m" to either the width or height, the image will be held in proportions and width/height works as max-dimensions]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap" type="wrap"> + <description><![CDATA[Wrap of the image, which is output between the body-tags]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="imgResource"> + <property name="ext" type="stdWrap"> + <description><![CDATA[ +]]></description> + <default><![CDATA[web]]></default> + </property> + <property name="frame" type="int"> + <description><![CDATA[Chooses which frame in an gif-animation or pdf-file. +"" = first frame (zero)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="height" type="stdWrap"> + <description><![CDATA[see ".width"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="import" type="stdWrap"> + <description><![CDATA[value should be set to the path of the file +with stdWrap you get the filename from the data-array + +Example: +This returns the first image in the field "image" from the data-array: +.import = uploads/pics/ +.import.field = image +.import.listNum = 0]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="m" type="mask"> + <description><![CDATA[NOTE: Mask for the image.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="maxH" type="stdWrap"> + <description><![CDATA[Max height]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="maxW" type="stdWrap"> + <description><![CDATA[Max width]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="minH" type="int"> + <description><![CDATA[Min height (overrules maxW/maxH)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="minW" type="int"> + <description><![CDATA[Min width (overrules maxW/maxH)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="params" type="string"> + <description><![CDATA[ImageMagick command-line: +fx. "-rotate 90" or "-negate"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="sample" type="boolean"> + <description><![CDATA[If set, -sample is used to scale images instead of -geometry. Sample does not use antialiasing and is therefore much faster.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stripProfile" type="boolean"> + <description><![CDATA[If set, IM-command will use a stripProfile-command which shrinks the generated thumbnails. See Install Tool for options and details. + +If processor_stripColorProfileByDefault is set in the install tool, you can deactivate it by setting stripProfile=0. + +Example: + +10 = IMAGE +10.file = fileadmin/images/image1.jpg +10.file.stripProfile = 1 + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="width" type="stdWrap"> + <description><![CDATA[If both the width and the height are set and one of the numbers is appended by an "m", the proportions will be preserved and thus width/height are treated as maximum dimensions for the image. The image will be scaled to fit into width/height rectangle. + +If both the width and the height are set and at least one of the numbers is appended by a "c", cropscaling will be enabled. This means that the proportions will be preserved and the image will be scaled to fit around a rectangle with width/height dimensions. Then, a centered portion from inside of the image (size defined by width/height) will be cut out. +The "c" can have a percentage value (-100 ... +100) after it, which defines how much the cropping will be moved off the center to the border. + +Notice that you can only use "m" or "c" at the same time! + +Examples: +This crops 120x80px from the center of the scaled image: +.width = 120c.height = 80c + +This crops 100x100px; from landscape-images at the left and portrait-images centered: +.width = 100c-100.height = 100c + +This crops 100x100px; from landscape-images a bit right of the center and portrait-images a bit upper than centered: +.width = 100c+30 +.height = 100c-25]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="noScale" type="boolean"> + <description><![CDATA[If set, image will never be scaled. Only width and height are calculated according to the other properties, so that image is _displayed_ resized, but original files is used. Example: + + file = test.jpg // has 1600 x 1200 pixels) + file.width = 240m + file.height = 240m + file.noScale = 1 + +results in + + <img src="test.jpg" width="240" height="180" /> + // note src="test.jpg" is the _original_ file. + +Usage: + + For creating PDFs or printing of pages the original file could provide much better quality as a rescaled one + ]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="mask"> + <property name="bgImg" type="imgResource"> + <description><![CDATA[NOTE: Both "m.mask" and "m.bgImg" must be valid images.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="bottomImg" type="imgResource"> + <description><![CDATA[An image masked by "m.bottomImg_mask" onto "m.bgImg" before the imgResources is masked by "m.mask". +Both "m.bottomImg" and "m.bottomImg_mask" is scaled to fit the size of the imgResource image! +This is most often used to create an underlay for the imgResource. +NOTE: Both "m.bottomImg" and "m.bottomImg_mask" must be valid images.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="bottomImg_mask" type="imgResource"> + <description><![CDATA[(optional) +NOTE: Both "m.bottomImg" and "m.bottomImg_mask" must be valid images.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="mask" type="imgResource"> + <description><![CDATA[The mask by which the image is masked onto "m.bgImg". Both "m.mask" and "m.bgImg" is scaled to fit the size of the imgResource image! +NOTE: Both "m.mask" and "m.bgImg" must be valid images.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="makelinks"> + <property name="http" type="makelinksHttp"> + <description><![CDATA[]]></description> + <default><![CDATA[]]></default> + </property> + <property name="mailto" type="makelinksMailto"> + <description><![CDATA[]]></description> + <default><![CDATA[]]></default> + </property> + </type> + <type id="makelinksHttp"> + <property name="ATagBeforeWrap" type="boolean"> + <description><![CDATA[If set, the link is first wrapped with http.wrap and then the <A>-tag.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="ATagParams" type="stdWrap"> + <description><![CDATA[Additional parameters + +Example: +class="board"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="extTarget" type="string"> + <description><![CDATA[The target of the link]]></description> + <default><![CDATA[_top]]></default> + </property> + <property name="keep" type="stringList"> + <description><![CDATA[list: "scheme","path","query" +As default the link-text will be the full domain-name of the link. + +Examples: +http://www.webaddress.rld/test/doc.php?id=3 +"": www.webaddress.rld +"scheme": http://www.webaddress.rld +"scheme,path": http://www.webaddress.rld/test/doc.php +"scheme,path,query": http://www.webaddress.rld/test/doc.php?id=3]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap" type="wrap"> + <description><![CDATA[wrap around the link]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="makelinksMailto"> + <property name="ATagBeforeWrap" type="boolean"> + <description><![CDATA[If set, the link is first wrapped with mailto.wrap and then the <A>-tag.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="ATagParams" type="stdWrap"> + <description><![CDATA[Additional parameters + +Example: +class="board"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap" type="wrap"> + <description><![CDATA[wrap around the link]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="numRows"> + <property name="select" type="select"> + <description><![CDATA[Select query for the operation. + +The property "selectFields" is overridden internally with "count(*)".]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="table" type="string"> + <description><![CDATA[The name of the database table +]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="numberFormat"> + <property name="decimals" type="stdWrap"> + <description><![CDATA[integer / stdWrap +Number of decimals the formatted number will have. Defaults to 0, so that your input will be rounded off in that case. +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="dec_point" type="stdWrap"> + <description><![CDATA[string / stdWrap +Character that divides the decimals from the rest. Defaults to "." +]]></description> + <default><![CDATA[.]]></default> + </property> + <property name="thousands_sep" type="stdWrap"> + <description><![CDATA[string / stdWrap +Character that divides the thousands of the number. Defaults to ",", set an empty value to have no thousands separator. +]]></description> + <default><![CDATA[,]]></default> + </property> + </type> + <type id="parseFunc"> + <property name="allowTags" type="stringList"> + <description><![CDATA[List of tags, which are allowed to exist in code! +Highest priority: If a tag is found in allowTags, denyTags is ignored!!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="constants" type="boolean"> + <description><![CDATA[The toplevel-defined constants will be substituted in the text. The constant-name is wrapped in "###". + +Example: +constants.EMAIL = email@email.com +(NOTE: This is toplevel TypoScript!) +All cases of the string ###EMAIL### will be substituted in the text. The constants are defined as a toplevel object. ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="denyTags" type="stringList"> + <description><![CDATA[List of tags, which may NOT exist in code! (use "*" for all.) +Lowest priority: If a tag is NOT found in allowTags, denyTags is checked. If denyTags is not "*" and the tag is not found in the list, the tag may exist! + +Example: +This allows <B>, <I>, <A> and <IMG> -tags to exist +.allowTags = b,i,a,img +.denyTags = *]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="externalBlocks " type="stringList"> + <description><![CDATA[This allows you to pre-split the content passed to parseFunc so that only content outside the blocks with the given tags is parsed. +Extra properties: +.[tagname] { + callRecursive = [boolean]; If set, the content of the block is directed into parseFunc again. Otherwise the content is just passed through with no other processing than stdWrap (see below) + callRecursive.dontWrapSelf = [boolean]; If set, the tags of the block is not wrapped around the content returned from parseFunc. + callRecursive.alternativeWrap = Alternative wrapping instead of the original tags. + callRecursive.tagStdWrap = ->stdWrap processing of the block-tags. + stdWrap = ->stdWrap processing of the whole block (regardless of whether callRecursive was set.) + stripNLprev = [boolean]; Strips off last linebreak of the previous outside block + stripNLnext = [boolean]; Strips off first linebreak of the next outside block + stripNL = [boolean]: Does both of the above. + + HTMLtableCells = [boolean]; If set, then the content is expected to be a table and every table-cell is traversed. + # Below, default is all cells and 1,2,3... overrides for specific cols. + HTMLtableCells.[default/1/2/3/...] { + callRecursive = [boolean]; The content is parsed through current parseFunc + stdWrap = ->stdWrap processing of the content in the cell + tagStdWrap = -> The <TD> tag is processed by ->stdWrap + } + HTMLtableCells.addChr10BetweenParagraphs = [boolean]; If set, then all </P><P> appearances will have a chr(10) inserted between them +} + +Example: +This example is used to split regular bodytext content so that tables and blockquotes in the bodytext are processed correctly. The blockquotes are passed into parseFunc again (recursively) and further their top/bottom margins are set to 0 (so no apparent linebreaks are seen) +The tables are also displayed with a number of properties of the cells overridden. +tt_content.text.20.parseFunc.externalBlocks { + blockquote.callRecursive=1 + blockquote.callRecursive.tagStdWrap.HTMLparser = 1 + blockquote.callRecursive.tagStdWrap.HTMLparser { + tags.blockquote.fixAttrib.style.list = margin-bottom:0;margin-top:0; + tags.blockquote.fixAttrib.style.always=1 + } + blockquote.stripNLprev=1 + blockquote.stripNLnext=1 + + table.stripNL=1 + table.stdWrap.HTMLparser = 1 + table.stdWrap.HTMLparser { + tags.table.overrideAttribs = border=0 cellpadding=2 cellspacing=1 style="margin-top:10px; margin-bottom:10px;" + tags.tr.allowedAttribs=0 + tags.td.overrideAttribs = valign=top bgcolor="#eeeeee" style="font-family : Verdana, Geneva, Arial, Helvetica, sans-serif;font-size : 10px;" + } +}]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="if" type="if"> + <description><![CDATA[if "if" returns false the input value is not parsed, but returned directly.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="makelinks" type="makelinks"> + <description><![CDATA[Set it to 1 for conversion +Convert web-addresses prefixed with "http://" and mail-addresses prefixed with "mailto:" +to links. +Example: +makelinks = 1 +makelinks.http.keep = path +makelinks.http.extTarget = _blank +makelinks.mailto.keep = path]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="nonTypoTagStdWrap" type="stdWrap"> + <description><![CDATA[Like .plainTextStdWrap. Difference: +.plainTextStdWrap works an ALL non-tag pieces in the text. .nonTypoTagStdWrap is post processing of all text (including tags) between special TypoTags (unless .breakoutTypoTagContent is not set for the TypoTag)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="nonTypoTagUserFunc" type="string"> + <description><![CDATA[Like .userFunc. Differences is (like nonTypoTagStdWrap) that this is post processing of all content pieces around TypoTags while .userFunc processes all non-tag content. (Notice: .breakoutTypoTagContent must be set for the TypoTag if it's excluded from nonTypoTagContent)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="plainTextStdWrap" type="stdWrap"> + <description><![CDATA[This is stdWrap properties for all non-tag content. ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="short" type="array"> + <description><![CDATA[Like constants above, but local. + +Example: +This substitutes all occurrences of "T3" with "TYPO3 CMS" and "T3web" with a link to typo3.org. +short { + T3 = TYPO3 CMS + T3web = <a href="https://typo3.org">typo3</a> +}]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="tags" type="tags"> + <description><![CDATA[Here you can define custom tags that will parse the content to something.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="userFunc" type="string"> + <description><![CDATA[This passes the non-tag content to a function of your own choice. Similar to eg. .postUserFunc in stdWrap. +Remember the function name must possibly be prepended "user_"]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="plugin"> + <property name="_CSS_DEFAULT_STYLE" type="string"> + <description><![CDATA[Use this to have some default CSS styles inserted in the header section of the document. Most likely this will provide a default acceptable display from the plugin, but should ideally be cleared and moved to an external stylesheet. +This value is for all plugins read by the pagegen script when making the header of the document.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="_LOCAL_LANG.[lang-key].[label-key]" type="string"> + <description><![CDATA[Can be used to override the default locallang labels for an Extbase plugin.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="userFunc" type="string"> + <description><![CDATA[Property setting up the USER / USER_INT object of the plugin]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="select"> + <property name="andWhere" type="stdWrap"> + <description><![CDATA[SQL-where without "AND"!, +Example: +andWhere = doktype = 1]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="begin" type="int "> + <description><![CDATA[begin with record number value + +Special keyword: "total" is substituted with count(*)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="groupBy" type="string"> + <description><![CDATA[SQL-groupBy without "group by"! Eg. "CType"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="join" type="string"> + <description><![CDATA[Enter tablename for JOIN , LEFT OUTER JOIN and RIGHT OUTER JOIN respectively.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="languageField" type="string"> + <description><![CDATA[If set, this points to the field in the record which holds a reference to a site language. And if set, the records returned by the select-function will be selected only if the value of this field matches the frontend language which is set by the config.sys_language_uid option]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="max" type="int "> + <description><![CDATA[max records + +Special keyword: "total" is substituted with count(*)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="markers" type="array"> + <description><![CDATA[array of markers + The markers defined in this section can be used, wrapped in the usual ###markername### way, in any other property of select. +Each value is properly escaped and quoted to prevent SQL injection problems. This provides a way to safely use external data +(e.g. database fields, GET/POST parameters) in a query. +<markername>.value (value) sets the value directly +<markername>.commaSeparatedList (bool) If set the value is interpreted as a comma separated list of values. Each value in +the list is individually escaped and quoted. +(stdWrap properties ...) All stdWrap properties can be used for each markername + +Example: + +page.60 = CONTENT +page.60 { + table = tt_content + select { + pidInList = 73 + where = header != ###whatever### + orderBy = ###sortfield### + markers { + whatever.data = GP:first + sortfield.value = sor + sortfield.wrap = |ting + } + } +} +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="orderBy" type="string"> + <description><![CDATA[without "order by"! Eg. "sorting, title"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="pidInList" type="stdWrap"> + <description><![CDATA[ +list of page_id]]></description> + <default><![CDATA[this]]></default> + </property> + <property name="selectFields" type="string"> + <description><![CDATA[List of fields to select, or "count(*)".]]></description> + <default><![CDATA[*]]></default> + </property> + <property name="uidInList" type="intList"> + <description><![CDATA[list of page_id +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="where" type="string"> + <description><![CDATA[SQL-where without "where"!, Eg. " (title LIKE '%SOMETHING%' AND NOT doktype) "]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="split" extends="cObjArray"> + <property name="1" type="stdWrap"> + <description><![CDATA[The object that should treat the value. +NOTE: The "current"-value is set to the value of current item, when the objects are called. See "stdWrap" / current. + +Example (stdWrap used): +1.current = 1 +1.wrap = <B> | </B> + +Example (CARRAY used): +1 { + 10 = TEXT + 10.current = 1 + 10.wrap = <B> | </B> + 20 = TEXT + 20.value = Mytext +}]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="2" type="stdWrap"> + <description/> + <default><![CDATA[ +]]></default> + </property> + <property name="3" type="stdWrap"> + <description/> + <default><![CDATA[ +]]></default> + </property> + <property name="cObjNum" type="int"> + <description><![CDATA[+optionSplit +This is a pointer the array of this object ("1,2,3,4"), that should treat the items, resulting from the split.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="max" type="stdWrap"> + <description><![CDATA[max number of splits]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="min" type="stdWrap"> + <description><![CDATA[min number of splits.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="returnKey" type="stdWrap"> + <description><![CDATA[Instead of parsing the split result, just return this element of the index immediately.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="token" type="stdWrap"> + <description><![CDATA[string or character (token) used to split the value]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap" type="wrap "> + <description><![CDATA[Defines a wrap for each item.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="stdWrap"> + <property name="HTMLparser" type="HTMLparser"> + <description><![CDATA[boolean / HTMLparser +This object allows you to parse the HTML-content and make all kinds of advanced filterings on the content. +Value must be set and properties are those of ->HTMLparser. +(See adminguide for ->HTMLparser options)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="age" type="string"> + <description><![CDATA[If enabled with a "1" (number, integer) the content is seen as a date (UNIX-time) and the difference from present time and the content-time is returned as one of these four variations: +"xx min" or "xx hrs" or "xx days" or "xx yrs" +The limits between which layout is used are 60 minutes, 24 hours, 365 days, + +NOTE: +If you set this property with a non-integer, it's used to format the four units. Use eight values to format both singular and plural values, where the first four values are the plural values and the last four are singular. This is the default string: + +" min| hrs| days| yrs| min| hour| day| year" + +Set another string if you want to change the units. You may include the "-signs. They are removed anyway.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="append" type="cObj"> + <description><![CDATA[cObject appended to content (after)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="br" type="boolean"> + <description><![CDATA[PHP function nl2br(); Converts linebreaks to <br />-tags]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="brTag" type="string"> + <description><![CDATA[All ASCII-codes of "10" (CR) is substituted with value]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="bytes" type="boolean"> + <description><![CDATA[Will format the input (an integer) as bytes: bytes, kb, mb + +If you add a value for the property "labels" you can alter the default suffixes. Labels for bytes, kilo, mega and giga are separated by vertical bar (|) and possibly encapsulated in "". Eg: " | K| M| G" (which is the default value) +Thus: + +bytes.labels = " | K| M| G"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="cObject" type="cObj"> + <description><![CDATA[Loads content from a content-object]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="cache" type="cache"> + <description><![CDATA[ + Stores the rendered content into the caching framework and reads it from there. + This allows you to reuse this content without prior rendering. The presence of cache.key will trigger this feature. +]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="case" type="case"> + <description><![CDATA[Converts case + +- upper: Converts to upper case (default) +- lower: Converts to lower case +- capitalize: Capitalize words + +Uses utf-8 for the operation.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="char" type="int"> + <description><![CDATA[Content is set to the chr(value). +PHP: $content=chr((int)$conf["char"]);]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="crop" type="string"> + <description><![CDATA[Crops the content to a certain length +Syntax: +/- (chars) = from left / from right | [string] | [boolean: keep whole words] + +Examples: +20 | ... => max 20 characters. If more, the value will be truncated to first 20 chars and prepended with "..." +-20 | ... => max 20 characters. If more, the value will be truncated to last 20 chars and appended with "..." +20 | ... | 1 => max 20 characters. If more, the value will be truncated to last 20 chars and appended with "...". If the division is in the middle of a word, the remains of that word is removed. + +Uses utf-8 for the operation.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="cropHTML" type="string"> + <description><![CDATA[Crops the content to a certain length. In contrast to stdWrap.crop it respects HTML tags. It does not crop inside tags and closes open tags. Entities (like ">") are counted as one char. See stdWrap.crop below for a syntax description and examples. + +Note that stdWrap.crop should not be used if stdWrap.cropHTML is already used.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="csConv" type="string"> + <description><![CDATA[Convert the charset of the string from the charset given as value to utf-8.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="current" type="boolean"> + <description><![CDATA[Sets the content to the "current"-value (see ->split)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="data" type="getText"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="dataWrap" type="string"> + <description><![CDATA[The content is parsed for sections of {...} and the content of {...} is of the type getText and substituted with the result of getText. + +Example: +This should result in a font-tag where the fontsize is decided by the global variable "size": +<font size="{global : size}"> | </font>]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="date" type="string"> + <description><![CDATA[The content should be data-type "UNIX-time". Returns the content formatted as a date. +$content=Date($conf["date"], $content); + +Example where a timestamp is imported: +.value.field = tstamp +.value.date = ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="debug" type="boolean"> + <description><![CDATA[Prints content with HTMLSpecialChars() and <PRE></PRE>: Useful for debugging which value stdWrap actually ends up with, if you're constructing a website with TypoScript. +Should be used under construction only.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="debugData" type="boolean"> + <description><![CDATA[Prints the current data-array, $cObj->data, directly to browser. This is where ".field" gets data from. +Should be used under construction only. ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="debugFunc" type="boolean"> + <description><![CDATA[Prints the content directly to browser with the debug() function. +Should be used under construction only. +Set to value "2" the content will be printed in a table - looks nicer.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="doubleBrTag" type="string"> + <description><![CDATA[All double-line-breaks are substituted with this value.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="encapsLines" type="encapsLines"> + <description><![CDATA[Lets you split the content by chr(10) and process each line independently. Used to format content made with the RTE.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="field" type="string"> + <description><![CDATA[Sets the content to the value $cObj->data[field] + +Example: Set content to the value of field "title": ".field = title" +$cObj->data changes. See the description for the data type "getText"/field! + +Note: You can also divide fieldnames by "//". Say, you set "nav_title // title" as the value, then the content from the field nav_title will be returned unless it is a blank string, in which case the title-field's value is returned.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="fieldRequired" type="string"> + <description><![CDATA[value in this field MUST be set]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="hash" type="stdWrap"> + <description><![CDATA[Returns a hashed value of the current content. Use one of the algorithms which are available in PHP. For a list of supported algorithms see https://www.php.net/manual/en/function.hash-algos.php + +Example: + +page.10 = TEXT +page.10 { + value = test@example.com + hash = md5 + wrap = <img src="http://www.gravatar.com/avatar/|" /> +}]]></description> + <default><![CDATA[]]></default> + </property> + <property name="htmlSpecialChars" type="boolean"> + <description><![CDATA[Passes the content through htmlspecialchars()-PHP-function +Additional property ".preserveEntities" will preserve entities so only non-entity chars are affected.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="if" type="if"> + <description><![CDATA[If the if-object returns false, stdWrap returns "" immediately]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="ifBlank" type="stdWrap"> + <description><![CDATA[Same as "ifEmpty" but the check is done using strlen().]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="ifEmpty" type="stdWrap"> + <description><![CDATA[if the content is empty (trimmed) at this point, the content is loaded with "ifEmpty". Zeros are treated as empty values!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="innerWrap" type="stdWrap"> + <description><![CDATA[Wraps the content]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="innerWrap2" type="stdWrap"> + <description><![CDATA[same as .innerWrap (but watch the order in which they are executed)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="insertData" type="boolean"> + <description><![CDATA[If set, then the content string is parsed like .dataWrap above. + +Example: +Displays the page title: +10 = TEXT +10.value = This is the page title: {page:title} +10.insertData = 1]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="intval" type="boolean"> + <description><![CDATA[PHP function intval(); Returns an integer. +PHP: $content=intval($content);]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="keywords" type="boolean"> + <description><![CDATA[splits the content by characters "," ";" and chr(10) (return), trims each value and returns a comma-separated list of the values.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="lang" type="ARR_languages_strings"> + <description><![CDATA[This is used to define optional language specific values. +If the global language key set by the ->config property .language is found in this array, then this value is used instead of the default input value to stdWrap. + +Example: +config.language = de +page.10 = TEXT +page.10.value = I am a Berliner! +page.10.lang.de = Ich bin ein Berliner! + +Output will be "Ich bin..." instead of "I am..."]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="listNum" type="listNum"> + <description><![CDATA[Explodes the content with "," (comma) and the content is set to the item[value]. + +Special keyword: "last" is set to the last element of the array! + +.splitChar (string): +Defines the string used to explode the value. If splitChar is an integer, the character with that number is used (eg. "10" to split lines...). +Default: "," (comma) + +.listNum = rand: +Returns a random item out of the list + +.stdWrap (stdWrap properties): +stdWrap properties of the listNum... + +Examples: + +We have a value of "item 1, item 2, item 3, item 4": +This would return "item 3": +.listNum = last - 1 + + +This way, the subtitle field changes on every reload: +page.5 = COA_INT +page.5 { + 10 = TEXT + 10 { + field = subtitle + stdWrap.listNum = rand + } +} +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="noTrimWrap" type="wrap"> + <description><![CDATA[This wraps the content with the values val1 and val2 in the example below - including surrounding whitespace! - without trimming the values. Note that this kind of wrap requires a "|" character to begin and end the wrap. + +Example: +| val1 | val2 |]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="numRows" type="numRows"> + <description><![CDATA[Returns the number of rows resulting from the select]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="numberFormat" type="numberFormat"> + <description><![CDATA[Formats a float value to any number format you need (e.g. prices) + +With this property you can format a float value and display it like you want, for example as a price. It's a wrapper for PHP's number_format() function. +You can define how many decimals you want and which separators you want for decimals and thousands. + +Examples: + +lib.myPrice = TEXT +lib.myPrice { + value = 0.8 + numberFormat { + decimals = 2 + dec_point = , + } + noTrimWrap = || €| +} +# Will result in "0,80 €" + + +lib.carViews = CONTENT +lib.carViews { + table = tx_mycarext_car + select.pidInList = 42 + renderObj = TEXT + renderObj { + field = views + numberFormat.thousands_sep = . + } +} +# Will result in something like "2.055" +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="outerWrap" type="stdWrap"> + <description><![CDATA[Wraps the complete content]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="override" type="stdWrap"> + <description><![CDATA[if "override" returns something else than "" or zero (trimmed), the content is loaded with this! ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="parseFunc" type="parseFunc"> + <description><![CDATA[object path reference / parseFunc +Processing instructions for the content. +Notice: If you enter a string as value this will be taken as a reference to an object path globally in the TypoScript object tree. This will be the basis configuration for parseFunc merged with any properties you add here. It works exactly like references does for content elements. + +Example: +parseFunc = < lib.parseFunc_RTE +parseFunc.tags.myTag = TEXT +parseFunc.tags.myTag.value = This will be inserted when <myTag> is found!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="postCObject" type="cObj"> + <description><![CDATA[cObject appended the content]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="postUserFunc" type="string"> + <description><![CDATA[Calling a PHP-function or method in a class, passing the current content to the function as first parameter and any properties as second parameter. Please see the description of the cObject USER for in-depth information. + +Example: +You can paste this example directly into a new template record. + +page = PAGE +page.typeNum=0 + +page.10 = TEXT +page.10 { + value = Hello World + postUserFunc = user_reverseString + postUserFunc.uppercase = 1 +} + +page.20 = TEXT +page.20 { + value = Hello World + postUserFunc = user_various->reverseString + postUserFunc.uppercase = 1 + postUserFunc.typolink = 11 +}]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="postUserFuncInt" type="string"> + <description><![CDATA[Calling a PHP-function or method in a class, passing the current content to the function as first parameter and any properties as second parameter. The result will be rendered non-cached, outside the main page-rendering. Please see the description of the cObject USER_INT for in-depth information. +Supplied by Jens Ellerbrock]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="preCObject" type="cObj"> + <description><![CDATA[cObject prepended the content ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="preIfEmptyListNum" type="listNum"> + <description><![CDATA[(as "listNum" below)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="preUserFunc" type="string"> + <description><![CDATA[Calling a PHP-function or method in a class, passing the current content to the function as first parameter and any properties as second parameter. +See .postUserFunc]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="prefixComment" type="string"> + <description><![CDATA[Prefixes content with a HTML comment with the second part of input string (divided by "|") where first part is an integer telling how many trailing tabs to put before the comment on a new line. +The content is parsed through insertData. + +Example: +prefixComment = 2 | CONTENT ELEMENT, uid:{field:uid}/{field:CType} + +Will indent the comment with 1 tab (and the next line with 2+1 tabs) +(Added in TYPO3 >3.6.0RC1)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="prepend" type="cObj"> + <description><![CDATA[cObject prepended to content (before)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="prioriCalc" type="boolean"> + <description><![CDATA[Calculation of the value using operators -+*/%^ plus respects priority to + and - operators and parenthesis levels (). +. (period) is decimal delimiter. +Returns a doublevalue. +If .prioriCalc is set to "intval" an integer is returned. +There is no errorchecking and division by zero or other invalid values may generate strange results. Also you use a proper syntax because future modifications to the function used may allow for more operators and features. + +Examples: +100%7 = 2 +-5*-4 = 20 ++6^2 = 36 +6 ^(1+1) = 36 +-5*-4+6^2-100%7 = 54 +-5 * (-4+6) ^ 2 - 100%7 = 98 +-5 * ((-4+6) ^ 2) - 100%7 = -22]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="rawUrlEncode" type="boolean"> + <description><![CDATA[Passes the content through rawurlencode()-PHP-function]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="replacement" type="boolean"> + <description><![CDATA[Allows to execute search/replace-functionality. Optionally PCRE-regex are supported (see: http://www.php.net/manual/en/function.preg-replace.php) A numeric index allows multiple replacements at once. + +Example: + +20 = TEXT +20 { + value = There_is_a_cat,_a_dog_and_a_tiger_in_da_hood!_Yeah! + stdWrap.replacement { + 10 { + search = _ + replace.char = 32 + } + 20 { + search = in da hood + replace = around the block + } + 30 { + search = #a (Cat|Dog|Tiger)#i + replace = an animal + useRegExp = 1 + } + } +} +]]></description> + <default><![CDATA[]]></default> + </property> + <property name="required" type="boolean"> + <description><![CDATA[This flag requires the content to be set to some value after any content-import and treatment that might have happened now (data, field, current, listNum, trim). Zero's is NOT regarded as empty! Use "if" instead! +If the content i empty, "" is returned immediately.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="round" type="array"> + <description><![CDATA[Round a number. + +Additional properties: + +.roundType = round (default) | ceil | floor +.decimals = 0 (default) or number of digits after decimal separator (only with roundType = round) + + +Example: +temp.number = TEXT +temp.number { + value = 3.14159 + round.roundType = round +} + +or + +temp.number = TEXT +temp.number { + value = 3.14159 + round.decimals = 2 +}]]></description> + <default><![CDATA[]]></default> + </property> + <property name="setContentToCurrent" type="boolean"> + <description><![CDATA[Sets the current value to the incoming content of the function.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="setCurrent" type="stdWrap"> + <description><![CDATA[Sets the "current"-value. This is normally set from some outside routine, so be careful with this. But it might be handy to do this]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="split" type="split"> + <description><![CDATA[ +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[Recursive call to stdWrap function]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="strftime" type="string"> + <description><![CDATA[Exactly like "date" above. See the PHP-manual (strftime) for the codes, or datatype "strftime-conf". +This formatting is useful if the locale is set in advance in the CONFIG-object. See this. + +Properties: +.charset : Can be set to the charset of the output string if you need to convert it to utf-8. Default is to take the intelligently guessed charset from \TYPO3\CMS\Core\Charset\CharsetConverter.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stripHtml" type="boolean"> + <description><![CDATA[Strips all html-tags.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="substring" type="stringList"> + <description><![CDATA[Returns the substring with [p1] and [p2] send as the 2nd and 3rd parameter to the PHP substring function. + +Uses utf-8 as character set for the operation.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="trim" type="boolean"> + <description><![CDATA[PHP-function trim(); Removes whitespace around value]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="typolink" type="typolink"> + <description><![CDATA[Wraps the content with a link-tag]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap" type="wrapSplitChar"> + <description><![CDATA[]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap2" type="wrapSplitChar"> + <description><![CDATA[same as .wrap (but watch the order in which they are executed)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap3" type="wrapSplitChar"> + <description><![CDATA[same as .wrap (but watch the order in which they are executed)]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrapAlign" type="stdWrap"> + <description><![CDATA[Wraps content with <div style=text-align:[value];"> | </div> if align is set]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="SVG"> + <property name="cache" type="cache"> + <description><![CDATA[ + Stores the rendered content into the caching framework and reads it from there. + This allows you to reuse this content without prior rendering. The presence of cache.key will trigger this feature. +]]></description> + <default><![CDATA[0]]></default> + </property> + <property name="width" type="stdWrap"> + <description><![CDATA[int/stdWrap + width of SVG +]]></description> + <default><![CDATA[600]]></default> + </property> + <property name="height" type="stdWrap"> + <description><![CDATA[int/stdWrap + height of SVG +]]></description> + <default><![CDATA[400]]></default> + </property> + <property name="src" type="stdWrap"> + <description><![CDATA[file resource/stdWrap + SVG file resource +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="value" type="stdWrap"> + <description><![CDATA[XML/stdWrap + SVG raw XML. When src is defined the file will be loaded and value is ignored. + +Example: +10 = SVG +10 { + width = 600 + height = 600 + value ( + <rect x="100" y="100" width="500" height="200" fill="white" stroke="black" stroke-width="5px"/> + <line x1="0" y1="200" x2="700" y2="200" stroke="red" stroke-width="20px"/> + <polygon points="185 0 125 25 185 100" transform="rotate(135 125 25)" /> + <circle cx="190" cy="150" r="40" stroke="black" stroke-width="2" fill="yellow"/> + ) + noscript.cObject = TEXT + noscript.cObject.value = no svg rendering possible, use a browser +} +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="noscript" type="stdWrap"> + <description><![CDATA[text/stdWrap + Output if SVG output is not possible +]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="renderMode" type="string"> + <description><![CDATA[The setting .renderMode can be set to "inline" to render an inline version of the SVG file. + ]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[stdwrap properties, applied on the returned object. +]]></description> + </property> + </type> + <type id="tags" extends="array"> + <property name="[myHtmlTag]" type="cObject"> + <description><![CDATA[Every entry in the Array... corresponds to a tag, that will be parsed. The elements MUST be in lowercase. +Every entry must be set to a content-object. +"current" is set to the content of the tag, eg <TAG>content</TAG>: here "current" is set to "content". +Parameters: +Parameters of the tag is set in $cObj->parameters (key is lowercased): +<TAG COLOR="red">content</TAG> +=> $cObj->parameters[color] = red +Special added properties to the content-object: +$cObj->parameters[allParams]: this is automatically set to the whole parameter-string of the tag, eg ' color="red"' +[cObject].stripNL: is a boolean option, which tells parseFunc that NewLines before and after content of the tag should be stripped. +[cObject].breakoutTypoTagContent: is a boolean option, which tells parseFunc that this block of content is breaking up the nonTypoTag content and that the content after this must be re-wrapped. + +Examples: +tags.bold = TEXT +tags.bold { + current = 1 + wrap = <B> | </B> +} +tags.bold.stripNL = 1]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="typolink"> + <property name="ATagBeforeWrap" type="boolean"> + <description><![CDATA[If set, the link is first wrapped with ".wrap" and then the <A>-tag.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="ATagParams" type="stdWrap"> + <description><![CDATA[Additional parameters + +Example: +class="board"]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="JSwindow_params" type="string"> + <description><![CDATA[Preset values for opening the window. This example lists almost all possible attributes: +status=1,menubar=1,scrollbars=1,resizable=1,location=1,directories=1,toolbar=1]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="addQueryString" type="boolean"> + <description><![CDATA[Add the QUERY_STRING to the start of the link. Notice that this does not check for any duplicate parameters! This is not a problem (only the last parameter of the same name will be applied). + +.method: If set to GET then then the parsed query arguments will be used. This settings are useful if you use URL processing extensions like Real URL, which translate part of the path into query arguments. + +.exclude: List of query arguments to exclude from the link (eg L or cHash).]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="additionalParams" type="stdWrap"> + <description><![CDATA[This is parameters that are added to the end of the url. This must be code ready to insert after the last parameter. + +Example: +'&print=1' + +NOTE: This is only active for internal links!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="extTarget" type="stdWrap"> + <description><![CDATA[target used for external links]]></description> + <default><![CDATA[_top]]></default> + </property> + <property name="fileTarget" type="string"> + <description><![CDATA[Default file link target. Used by typolink if no fileTarget is set. ]]></description> + <default><![CDATA[ + ]]></default> + </property> + <property name="no_cache" type="stdWrap"> + <description><![CDATA[Adds a "&no_cache=1"-parameter to the link]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="parameter" type="stdWrap"> + <description><![CDATA[This is the data, that ->typoLink uses to create the link. The value is trimmed and if it's empty, ->typoLink returns the input value untouched. + +NOTE: If used from parseFunc, this value should be imported by: +typolink.parameter.data = parameters : allParams + +Examples: +Internal links: +integers (51): creates a link to page with uid = 51 +filerefs (fileadmin/somedir/thedoc.html): creates a link to the file on the local server. +strings (some_alias): creates a link to the page with alias = "some_alias" + +External links: +email-addresses (name@email.com): creates a link to the email-addr. +domains (www.domain.com): creates link to http://-page + +The input is parsed like this: +First the parameter is splitted by character-space. This provides a way to pass more parameters. See "target" below here. +If a "@" is in the string, it's an email +If a period (.) is in the string AND if the period (.) is found before a slash (/) is found OR if a doubleslash is found, then it's a URL +If a slash (/) is found, it's a filereference. If the file/directory does not exist on the server, the link is NOT made! + +Now the input can be an alias or page-id. If the input is an integer it's a page-id, if it's two comma separated integers, it's an id/type pair, else it's an alias. For page-id's or aliases you can prepend a "#" mark with a number indication tt_content record number on the page to jump to! (if .section-property is present, it overrides this). +If you insert only "#234" where "234" is the tt_content record number, it links to the current page-id +Notice: The parameter can contain a keyword that hands over link generation to an external function. See example below this table! + +Target +Target is normally defined by the "extTarget" and "target" properties of typolink. But you may override this target by adding the new target after the parameter separated by a whitespace. Thus the target becomes the second parameter. +If the "Target" parameter is set to the "-" character, then it's the same as no target passed to the function. This feature enables you to still pass a class as third parameter and title as fourth parameter without setting the target also. + +Open in windows with fixed dimensions (JavaScript) +It is possible to open the link in a window opened by JavaScript (with "window.open"). For this, just set the target value to "123x456" where 123 is the window width and 456 is the window height. You can also specify additional parameters to the function by entering them separated from the width and height with a colon ":". For instance "230x450:resizable=0,location=1" will disable resizing of the window and enable the location bar. +Also see property "JSwindow". + +Class +If you specify a third parameter separated by whitespace in the parameter value this becomes the class-parameter of the link. This class parameter is inserted in the link-tag before any values from .ATagParams which means this class value will override any class value set in ATagParams (at least for MSIE). If set to "-", then it's the same as no class passed to the function. This feature enables you to still pass a title as fourth parameter without setting the class also. + +Title +The title attribute is normally specified via .ATagParams or directly via the .title property. But you may override this value by adding the desired title as the fourth parameter (parameters separated by whitespace) to typolink. + +Examples of multiparameters: +Consider this .parameter value passed to this function: + +51 _blank blueLink + +This would result in a link approx like this: + +<A href="?id=51" target="_blank" class="blueLink">]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="returnLast" type="string"> + <description><![CDATA[If set to "url" then it will return the URL of the link ($this->lastTypoLinkUrl) +If set to "target" it will return the target of the link. +So, in these two cases you will not get the value wrapped but the url or target value returned!]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="section" type="stdWrap"> + <description><![CDATA[If this value is present, it's prepended with a "#" and placed after any internal url to another page in TYPO3. +This is used create a link, which jumps from one page directly the section on another page.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="target" type="stdWrap"> + <description><![CDATA[target used for internal links]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="title" type="stdWrap"> + <description><![CDATA[Sets the title parameter of the A-tag.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="userFunc" type="string"> + <description><![CDATA[This passes the link-data compiled by the typolink function to a user-defined function for final manipulation. +The $content variable passed to the user-function (first parameter) is an array with the keys "TYPE", "TAG", "url", "targetParams" and "aTagParams". +TYPE is an indication of link-kind: mailto, url, file, page +TAG is the full <A>-tag as generated and ready from the typolink function. +The latter three is combined into the 'TAG' value after this formula: + +<a href="'.$finalTagParts['url'].'"'. + $finalTagParts['targetParams']. + $finalTagParts['aTagParams'].'> + +The userfunction must return an <A>-tag.]]></description> + <default><![CDATA[ +]]></default> + </property> + <property name="wrap" type="wrap"> + <description><![CDATA[Wraps the links.]]></description> + <default><![CDATA[ +]]></default> + </property> + </type> + <type id="listNum"> + <property name="splitChar" type="string"> + <description><![CDATA[Defines the string used to explode the value. If splitChar is an integer, the character with that number is used (e.g. "10" to split lines...). + + Default: "," (comma)]]></description> + <default><![CDATA[,]]></default> + </property> + <property name="stdWrap" type="stdWrap"> + <description><![CDATA[stdwrap properties of the listNum... +]]></description> + </property> + </type> + <type id="additionalHeadersArray" extends="array"> + <property name="10" type="additionalHeadersItem"> + <description><![CDATA[Defines a header. + +Must at least contain the header property.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="20" type="additionalHeadersItem"> + <description><![CDATA[Defines a header. + +Must at least contain the header property.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="30" type="additionalHeadersItem"> + <description><![CDATA[Defines a header. + +Must at least contain the header property.]]></description> + <default><![CDATA[]]></default> + </property> + <property name="40" type="additionalHeadersItem"> + <description><![CDATA[Defines a header. + +Must at least contain the header property.]]></description> + <default><![CDATA[]]></default> + </property> + </type> + <type id="additionalHeadersItem"> + <property name="header" type="string"> + <description><![CDATA[The header string]]></description> + <default><![CDATA[]]></default> + </property> + <property name="replace" type="boolean"> + <description><![CDATA[Optional. + +If set, previous headers with the same name are replaced with the current one. + +Default is "1".]]></description> + <default><![CDATA[1]]></default> + </property> + <property name="httpResponseCode" type="int"> + <description><![CDATA[Optional. HTTP status code as an integer.]]></description> + <default><![CDATA[]]></default> + </property> + </type> +</tsRef> diff --git a/Resources/Public/Css/backend.css b/Resources/Public/Css/backend.css new file mode 100644 index 0000000..8d0fa66 --- /dev/null +++ b/Resources/Public/Css/backend.css @@ -0,0 +1,3819 @@ + +@charset "UTF-8"; +/*! + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +/*! + * Bootstrap v5.1.3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +:root,[data-bs-theme=light]{--bs-blue:#3085d6;--bs-indigo:#6610f2;--bs-purple:#7c5ac4;--bs-pink:#d4458c;--bs-red:#d64545;--bs-orange:#f28522;--bs-yellow:#e0a810;--bs-green:#2a9960;--bs-teal:#2aa89c;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#737373;--bs-gray-dark:#333;--bs-gray-100:#f5f5f5;--bs-gray-200:#eee;--bs-gray-300:#d7d7d7;--bs-gray-400:#ccc;--bs-gray-500:#bbb;--bs-gray-600:#737373;--bs-gray-700:#5a5a5a;--bs-gray-800:#333;--bs-gray-900:#1e1e1e;--bs-primary:#3085d6;--bs-secondary:#737373;--bs-success:#2a9960;--bs-info:#2aa89c;--bs-warning:#e0a810;--bs-danger:#d64545;--bs-light:#f5f5f5;--bs-default:#f5f5f5;--bs-notice:#333;--bs-dark:#1e1e1e;--bs-primary-rgb:48,133,214;--bs-secondary-rgb:115,115,115;--bs-success-rgb:42,153,96;--bs-info-rgb:42,168,156;--bs-warning-rgb:224,168,16;--bs-danger-rgb:214,69,69;--bs-light-rgb:245,245,245;--bs-default-rgb:245,245,245;--bs-notice-rgb:51,51,51;--bs-dark-rgb:30,30,30;--bs-primary-text-emphasis:#133556;--bs-secondary-text-emphasis:#2e2e2e;--bs-success-text-emphasis:#113d26;--bs-info-text-emphasis:#11433e;--bs-warning-text-emphasis:#5a4306;--bs-danger-text-emphasis:#561c1c;--bs-light-text-emphasis:#5a5a5a;--bs-dark-text-emphasis:#5a5a5a;--bs-primary-bg-subtle:#d6e7f7;--bs-secondary-bg-subtle:#e3e3e3;--bs-success-bg-subtle:#d4ebdf;--bs-info-bg-subtle:#d4eeeb;--bs-warning-bg-subtle:#f9eecf;--bs-danger-bg-subtle:#f7dada;--bs-light-bg-subtle:#fafafa;--bs-dark-bg-subtle:#ccc;--bs-primary-border-subtle:#acceef;--bs-secondary-border-subtle:#c7c7c7;--bs-success-border-subtle:#aad6bf;--bs-info-border-subtle:#aadcd7;--bs-warning-border-subtle:#f3dc9f;--bs-danger-border-subtle:#efb5b5;--bs-light-border-subtle:#eee;--bs-dark-border-subtle:#bbb;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:Verdana,Arial,Helvetica,sans-serif;--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg,hsla(0,0%,100%,.15),hsla(0,0%,100%,0));--bs-root-font-size:1rem;--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:0.75rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#000;--bs-body-color-rgb:0,0,0;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(0,0,0,.75);--bs-secondary-color-rgb:0,0,0;--bs-secondary-bg:#eee;--bs-secondary-bg-rgb:238,238,238;--bs-tertiary-color:rgba(0,0,0,.5);--bs-tertiary-color-rgb:0,0,0;--bs-tertiary-bg:#f5f5f5;--bs-tertiary-bg-rgb:245,245,245;--bs-heading-color:inherit;--bs-link-color:#212424;--bs-link-color-rgb:33,36,36;--bs-link-decoration:none;--bs-link-hover-color:#1a1d1d;--bs-link-hover-color-rgb:26,29,29;--bs-link-hover-decoration:underline;--bs-code-color:#d4458c;--bs-highlight-color:#000;--bs-highlight-bg:#f9eecf;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#d7d7d7;--bs-border-color-translucent:rgba(0,0,0,.175);--bs-border-radius:0.125rem;--bs-border-radius-sm:0.125rem;--bs-border-radius-lg:0.125rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0,0,0,.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0,0,0,.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0,0,0,.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0,0,0,.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(48,133,214,.25);--bs-form-valid-color:#2a9960;--bs-form-valid-border-color:#2a9960;--bs-form-invalid-color:#d64545;--bs-form-invalid-border-color:#d64545} +[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#d7d7d7;--bs-body-color-rgb:215,215,215;--bs-body-bg:#1e1e1e;--bs-body-bg-rgb:30,30,30;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:hsla(0,0%,84%,.75);--bs-secondary-color-rgb:215,215,215;--bs-secondary-bg:#333;--bs-secondary-bg-rgb:51,51,51;--bs-tertiary-color:hsla(0,0%,84%,.5);--bs-tertiary-color-rgb:215,215,215;--bs-tertiary-bg:#292929;--bs-tertiary-bg-rgb:41,41,41;--bs-primary-text-emphasis:#83b6e6;--bs-secondary-text-emphasis:#ababab;--bs-success-text-emphasis:#7fc2a0;--bs-info-text-emphasis:#7fcbc4;--bs-warning-text-emphasis:#eccb70;--bs-danger-text-emphasis:#e68f8f;--bs-light-text-emphasis:#f5f5f5;--bs-dark-text-emphasis:#d7d7d7;--bs-primary-bg-subtle:#0a1b2b;--bs-secondary-bg-subtle:#171717;--bs-success-bg-subtle:#081f13;--bs-info-bg-subtle:#08221f;--bs-warning-bg-subtle:#2d2203;--bs-danger-bg-subtle:#2b0e0e;--bs-light-bg-subtle:#333;--bs-dark-bg-subtle:#1a1a1a;--bs-primary-border-subtle:#1d5080;--bs-secondary-border-subtle:#454545;--bs-success-border-subtle:#195c3a;--bs-info-border-subtle:#19655e;--bs-warning-border-subtle:#86650a;--bs-danger-border-subtle:#802929;--bs-light-border-subtle:#5a5a5a;--bs-dark-border-subtle:#333;--bs-heading-color:inherit;--bs-link-color:#83b6e6;--bs-link-hover-color:#9cc5eb;--bs-link-color-rgb:131,182,230;--bs-link-hover-color-rgb:156,197,235;--bs-code-color:#e58fba;--bs-highlight-color:#d7d7d7;--bs-highlight-bg:#5a4306;--bs-border-color:#5a5a5a;--bs-border-color-translucent:hsla(0,0%,100%,.15);--bs-form-valid-color:#7fc2a0;--bs-form-valid-border-color:#7fc2a0;--bs-form-invalid-color:#e68f8f;--bs-form-invalid-border-color:#e68f8f} +*,:after,:before{box-sizing:border-box} +:root{font-size:var(--bs-root-font-size)} +@media (prefers-reduced-motion:no-preference){ +:root{scroll-behavior:smooth}} +body{background-color:var(--bs-body-bg);color:var(--bs-body-color);font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);margin:0;text-align:var(--bs-body-text-align);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)} +hr{border:0;border-top:var(--bs-border-width) solid;color:inherit;margin:1rem 0;opacity:1} +.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6,typo3-backend-editable-page-title{color:var(--bs-heading-color);font-weight:500;line-height:1.2;margin-bottom:calc(var(--typo3-spacing)/2);margin-top:0} +.h1,h1,typo3-backend-editable-page-title{font-size:calc(1.275rem + .3vw)} +@media (min-width:1200px){ +.h1,h1,typo3-backend-editable-page-title{font-size:1.5rem}} +.h2,h2{font-size:1.25rem} +.h3,h3{font-size:1rem} +.h4,h4{font-size:.875rem} +.h5,.h6,h5,h6{font-size:.75rem} +p{margin-bottom:var(--typo3-spacing);margin-top:0} +abbr[title]{cursor:help;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none} +address{font-style:normal;line-height:inherit;margin-bottom:1rem} +ol,ul{padding-left:2rem} +dl,ol,ul{margin-bottom:1rem;margin-top:0} +ol ol,ol ul,ul ol,ul ul{margin-bottom:0} +dt{font-weight:700} +dd{margin-bottom:.5rem;margin-left:0} +blockquote{margin:0 0 1rem} +b,strong{font-weight:bolder} +.small,small{font-size:.875em} +.mark,mark{background-color:var(--bs-highlight-bg);color:var(--bs-highlight-color);padding:.1875em} +sub,sup{font-size:.75em;line-height:0;position:relative;vertical-align:baseline} +sub{bottom:-.25em} +sup{top:-.5em} +a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1))} +a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)} +code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em} +pre{display:block;font-size:.875em;margin-bottom:1rem;margin-top:0;overflow:auto} +pre code{color:inherit;font-size:inherit;word-break:normal} +code{color:var(--bs-code-color);font-size:.875em;word-wrap:break-word} +a>code{color:inherit} +kbd{background-color:var(--bs-body-color);border-radius:.125rem;color:var(--bs-body-bg);font-size:.875em;padding:.1875rem .375rem} +kbd kbd{padding:0} +figure{margin:0 0 1rem} +img,svg{vertical-align:middle} +table{border-collapse:collapse;caption-side:bottom} +caption{color:var(--bs-secondary-color);padding-bottom:.5rem;padding-top:.5rem;text-align:left} +th{text-align:inherit;text-align:-webkit-match-parent} +tbody,td,tfoot,th,thead,tr{border:0 solid;border-color:inherit} +label{display:inline-block} +button{border-radius:0} +button:focus:not(:focus-visible){outline:0} +button,input,optgroup,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit;margin:0} +button,select{text-transform:none} +[role=button]{cursor:pointer} +select{word-wrap:normal} +select:disabled{opacity:1} +[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important} +[type=button],[type=reset],[type=submit],button{-webkit-appearance:button} +[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer} +::-moz-focus-inner{border-style:none;padding:0} +textarea{resize:vertical} +fieldset{border:0;margin:0;min-width:0;padding:0} +legend{float:left;font-size:calc(1.275rem + .3vw);line-height:inherit;margin-bottom:.5rem;padding:0;width:100%} +@media (min-width:1200px){ +legend{font-size:1.5rem}} +legend+*{clear:left} +::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0} +::-webkit-inner-spin-button{height:auto} +[type=search]{-webkit-appearance:textfield;outline-offset:-2px} +::-webkit-search-decoration{-webkit-appearance:none} +::-webkit-color-swatch-wrapper{padding:0} +::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} +::file-selector-button{-webkit-appearance:button;font:inherit} +output{display:inline-block} +iframe{border:0} +summary{cursor:pointer;display:list-item} +progress{vertical-align:baseline} +[hidden]{display:none!important} +.lead{font-size:.9375rem;font-weight:300} +.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2} +@media (min-width:1200px){ +.display-1{font-size:5rem}} +.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2} +@media (min-width:1200px){ +.display-2{font-size:4.5rem}} +.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2} +@media (min-width:1200px){ +.display-3{font-size:4rem}} +.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2} +@media (min-width:1200px){ +.display-4{font-size:3.5rem}} +.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2} +@media (min-width:1200px){ +.display-5{font-size:3rem}} +.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2} +@media (min-width:1200px){ +.display-6{font-size:2.5rem}} +.list-inline,.list-unstyled{list-style:none;padding-left:0} +.list-inline-item{display:inline-block} +.list-inline-item:not(:last-child){margin-right:.5rem} +.initialism{font-size:.875em;text-transform:uppercase} +.blockquote{font-size:.9375rem;margin-bottom:1rem} +.blockquote>:last-child{margin-bottom:0} +.blockquote-footer{color:#737373;font-size:.875em;margin-bottom:1rem;margin-top:-1rem} +.blockquote-footer:before{content:"— "} +.img-fluid,.img-thumbnail{height:auto;max-width:100%} +.img-thumbnail{background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);box-shadow:var(--bs-box-shadow-sm);padding:.25rem} +.figure{display:inline-block} +.figure-img{line-height:1;margin-bottom:.5rem} +.figure-caption{color:var(--bs-secondary-color);font-size:.875em} +.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;margin-left:auto;margin-right:auto;padding-left:calc(var(--bs-gutter-x)*.5);padding-right:calc(var(--bs-gutter-x)*.5);width:100%} +@media (min-width:576px){ +.container,.container-sm{max-width:540px}} +@media (min-width:768px){ +.container,.container-md,.container-sm{max-width:720px}} +@media (min-width:992px){ +.container,.container-lg,.container-md,.container-sm{max-width:960px}} +@media (min-width:1200px){ +.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}} +@media (min-width:1400px){ +.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}} +:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px} +.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-left:calc(var(--bs-gutter-x)*-.5);margin-right:calc(var(--bs-gutter-x)*-.5);margin-top:calc(var(--bs-gutter-y)*-1)} +.row>*{flex-shrink:0;margin-top:var(--bs-gutter-y);max-width:100%;padding-left:calc(var(--bs-gutter-x)*.5);padding-right:calc(var(--bs-gutter-x)*.5);width:100%} +.col{flex:1 0 0%} +.row-cols-auto>*{flex:0 0 auto;width:auto} +.row-cols-1>*{flex:0 0 auto;width:100%} +.row-cols-2>*{flex:0 0 auto;width:50%} +.row-cols-3>*{flex:0 0 auto;width:33.33333333%} +.row-cols-4>*{flex:0 0 auto;width:25%} +.row-cols-5>*{flex:0 0 auto;width:20%} +.row-cols-6>*{flex:0 0 auto;width:16.66666667%} +.col-auto{flex:0 0 auto;width:auto} +.col-1{flex:0 0 auto;width:8.33333333%} +.col-2{flex:0 0 auto;width:16.66666667%} +.col-3{flex:0 0 auto;width:25%} +.col-4{flex:0 0 auto;width:33.33333333%} +.col-5{flex:0 0 auto;width:41.66666667%} +.col-6{flex:0 0 auto;width:50%} +.col-7{flex:0 0 auto;width:58.33333333%} +.col-8{flex:0 0 auto;width:66.66666667%} +.col-9{flex:0 0 auto;width:75%} +.col-10{flex:0 0 auto;width:83.33333333%} +.col-11{flex:0 0 auto;width:91.66666667%} +.col-12{flex:0 0 auto;width:100%} +.offset-1{margin-left:8.33333333%} +.offset-2{margin-left:16.66666667%} +.offset-3{margin-left:25%} +.offset-4{margin-left:33.33333333%} +.offset-5{margin-left:41.66666667%} +.offset-6{margin-left:50%} +.offset-7{margin-left:58.33333333%} +.offset-8{margin-left:66.66666667%} +.offset-9{margin-left:75%} +.offset-10{margin-left:83.33333333%} +.offset-11{margin-left:91.66666667%} +.g-0,.gx-0{--bs-gutter-x:0} +.g-0,.gy-0{--bs-gutter-y:0} +.g-1,.gx-1{--bs-gutter-x:0.25rem} +.g-1,.gy-1{--bs-gutter-y:0.25rem} +.g-2,.gx-2{--bs-gutter-x:0.5rem} +.g-2,.gy-2{--bs-gutter-y:0.5rem} +.g-3,.gx-3{--bs-gutter-x:1rem} +.g-3,.gy-3{--bs-gutter-y:1rem} +.g-4,.gx-4{--bs-gutter-x:1.5rem} +.g-4,.gy-4{--bs-gutter-y:1.5rem} +.g-5,.gx-5{--bs-gutter-x:3rem} +.g-5,.gy-5{--bs-gutter-y:3rem} +@media (min-width:576px){ +.col-sm{flex:1 0 0%} +.row-cols-sm-auto>*{flex:0 0 auto;width:auto} +.row-cols-sm-1>*{flex:0 0 auto;width:100%} +.row-cols-sm-2>*{flex:0 0 auto;width:50%} +.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%} +.row-cols-sm-4>*{flex:0 0 auto;width:25%} +.row-cols-sm-5>*{flex:0 0 auto;width:20%} +.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%} +.col-sm-auto{flex:0 0 auto;width:auto} +.col-sm-1{flex:0 0 auto;width:8.33333333%} +.col-sm-2{flex:0 0 auto;width:16.66666667%} +.col-sm-3{flex:0 0 auto;width:25%} +.col-sm-4{flex:0 0 auto;width:33.33333333%} +.col-sm-5{flex:0 0 auto;width:41.66666667%} +.col-sm-6{flex:0 0 auto;width:50%} +.col-sm-7{flex:0 0 auto;width:58.33333333%} +.col-sm-8{flex:0 0 auto;width:66.66666667%} +.col-sm-9{flex:0 0 auto;width:75%} +.col-sm-10{flex:0 0 auto;width:83.33333333%} +.col-sm-11{flex:0 0 auto;width:91.66666667%} +.col-sm-12{flex:0 0 auto;width:100%} +.offset-sm-0{margin-left:0} +.offset-sm-1{margin-left:8.33333333%} +.offset-sm-2{margin-left:16.66666667%} +.offset-sm-3{margin-left:25%} +.offset-sm-4{margin-left:33.33333333%} +.offset-sm-5{margin-left:41.66666667%} +.offset-sm-6{margin-left:50%} +.offset-sm-7{margin-left:58.33333333%} +.offset-sm-8{margin-left:66.66666667%} +.offset-sm-9{margin-left:75%} +.offset-sm-10{margin-left:83.33333333%} +.offset-sm-11{margin-left:91.66666667%} +.g-sm-0,.gx-sm-0{--bs-gutter-x:0} +.g-sm-0,.gy-sm-0{--bs-gutter-y:0} +.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem} +.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem} +.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem} +.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem} +.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem} +.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem} +.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem} +.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem} +.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem} +.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}} +@media (min-width:768px){ +.col-md{flex:1 0 0%} +.row-cols-md-auto>*{flex:0 0 auto;width:auto} +.row-cols-md-1>*{flex:0 0 auto;width:100%} +.row-cols-md-2>*{flex:0 0 auto;width:50%} +.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%} +.row-cols-md-4>*{flex:0 0 auto;width:25%} +.row-cols-md-5>*{flex:0 0 auto;width:20%} +.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%} +.col-md-auto{flex:0 0 auto;width:auto} +.col-md-1{flex:0 0 auto;width:8.33333333%} +.col-md-2{flex:0 0 auto;width:16.66666667%} +.col-md-3{flex:0 0 auto;width:25%} +.col-md-4{flex:0 0 auto;width:33.33333333%} +.col-md-5{flex:0 0 auto;width:41.66666667%} +.col-md-6{flex:0 0 auto;width:50%} +.col-md-7{flex:0 0 auto;width:58.33333333%} +.col-md-8{flex:0 0 auto;width:66.66666667%} +.col-md-9{flex:0 0 auto;width:75%} +.col-md-10{flex:0 0 auto;width:83.33333333%} +.col-md-11{flex:0 0 auto;width:91.66666667%} +.col-md-12{flex:0 0 auto;width:100%} +.offset-md-0{margin-left:0} +.offset-md-1{margin-left:8.33333333%} +.offset-md-2{margin-left:16.66666667%} +.offset-md-3{margin-left:25%} +.offset-md-4{margin-left:33.33333333%} +.offset-md-5{margin-left:41.66666667%} +.offset-md-6{margin-left:50%} +.offset-md-7{margin-left:58.33333333%} +.offset-md-8{margin-left:66.66666667%} +.offset-md-9{margin-left:75%} +.offset-md-10{margin-left:83.33333333%} +.offset-md-11{margin-left:91.66666667%} +.g-md-0,.gx-md-0{--bs-gutter-x:0} +.g-md-0,.gy-md-0{--bs-gutter-y:0} +.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem} +.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem} +.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem} +.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem} +.g-md-3,.gx-md-3{--bs-gutter-x:1rem} +.g-md-3,.gy-md-3{--bs-gutter-y:1rem} +.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem} +.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem} +.g-md-5,.gx-md-5{--bs-gutter-x:3rem} +.g-md-5,.gy-md-5{--bs-gutter-y:3rem}} +@media (min-width:992px){ +.col-lg{flex:1 0 0%} +.row-cols-lg-auto>*{flex:0 0 auto;width:auto} +.row-cols-lg-1>*{flex:0 0 auto;width:100%} +.row-cols-lg-2>*{flex:0 0 auto;width:50%} +.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%} +.row-cols-lg-4>*{flex:0 0 auto;width:25%} +.row-cols-lg-5>*{flex:0 0 auto;width:20%} +.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%} +.col-lg-auto{flex:0 0 auto;width:auto} +.col-lg-1{flex:0 0 auto;width:8.33333333%} +.col-lg-2{flex:0 0 auto;width:16.66666667%} +.col-lg-3{flex:0 0 auto;width:25%} +.col-lg-4{flex:0 0 auto;width:33.33333333%} +.col-lg-5{flex:0 0 auto;width:41.66666667%} +.col-lg-6{flex:0 0 auto;width:50%} +.col-lg-7{flex:0 0 auto;width:58.33333333%} +.col-lg-8{flex:0 0 auto;width:66.66666667%} +.col-lg-9{flex:0 0 auto;width:75%} +.col-lg-10{flex:0 0 auto;width:83.33333333%} +.col-lg-11{flex:0 0 auto;width:91.66666667%} +.col-lg-12{flex:0 0 auto;width:100%} +.offset-lg-0{margin-left:0} +.offset-lg-1{margin-left:8.33333333%} +.offset-lg-2{margin-left:16.66666667%} +.offset-lg-3{margin-left:25%} +.offset-lg-4{margin-left:33.33333333%} +.offset-lg-5{margin-left:41.66666667%} +.offset-lg-6{margin-left:50%} +.offset-lg-7{margin-left:58.33333333%} +.offset-lg-8{margin-left:66.66666667%} +.offset-lg-9{margin-left:75%} +.offset-lg-10{margin-left:83.33333333%} +.offset-lg-11{margin-left:91.66666667%} +.g-lg-0,.gx-lg-0{--bs-gutter-x:0} +.g-lg-0,.gy-lg-0{--bs-gutter-y:0} +.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem} +.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem} +.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem} +.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem} +.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem} +.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem} +.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem} +.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem} +.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem} +.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}} +@media (min-width:1200px){ +.col-xl{flex:1 0 0%} +.row-cols-xl-auto>*{flex:0 0 auto;width:auto} +.row-cols-xl-1>*{flex:0 0 auto;width:100%} +.row-cols-xl-2>*{flex:0 0 auto;width:50%} +.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%} +.row-cols-xl-4>*{flex:0 0 auto;width:25%} +.row-cols-xl-5>*{flex:0 0 auto;width:20%} +.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%} +.col-xl-auto{flex:0 0 auto;width:auto} +.col-xl-1{flex:0 0 auto;width:8.33333333%} +.col-xl-2{flex:0 0 auto;width:16.66666667%} +.col-xl-3{flex:0 0 auto;width:25%} +.col-xl-4{flex:0 0 auto;width:33.33333333%} +.col-xl-5{flex:0 0 auto;width:41.66666667%} +.col-xl-6{flex:0 0 auto;width:50%} +.col-xl-7{flex:0 0 auto;width:58.33333333%} +.col-xl-8{flex:0 0 auto;width:66.66666667%} +.col-xl-9{flex:0 0 auto;width:75%} +.col-xl-10{flex:0 0 auto;width:83.33333333%} +.col-xl-11{flex:0 0 auto;width:91.66666667%} +.col-xl-12{flex:0 0 auto;width:100%} +.offset-xl-0{margin-left:0} +.offset-xl-1{margin-left:8.33333333%} +.offset-xl-2{margin-left:16.66666667%} +.offset-xl-3{margin-left:25%} +.offset-xl-4{margin-left:33.33333333%} +.offset-xl-5{margin-left:41.66666667%} +.offset-xl-6{margin-left:50%} +.offset-xl-7{margin-left:58.33333333%} +.offset-xl-8{margin-left:66.66666667%} +.offset-xl-9{margin-left:75%} +.offset-xl-10{margin-left:83.33333333%} +.offset-xl-11{margin-left:91.66666667%} +.g-xl-0,.gx-xl-0{--bs-gutter-x:0} +.g-xl-0,.gy-xl-0{--bs-gutter-y:0} +.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem} +.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem} +.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem} +.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem} +.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem} +.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem} +.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem} +.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem} +.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem} +.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}} +@media (min-width:1400px){ +.col-xxl{flex:1 0 0%} +.row-cols-xxl-auto>*{flex:0 0 auto;width:auto} +.row-cols-xxl-1>*{flex:0 0 auto;width:100%} +.row-cols-xxl-2>*{flex:0 0 auto;width:50%} +.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%} +.row-cols-xxl-4>*{flex:0 0 auto;width:25%} +.row-cols-xxl-5>*{flex:0 0 auto;width:20%} +.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%} +.col-xxl-auto{flex:0 0 auto;width:auto} +.col-xxl-1{flex:0 0 auto;width:8.33333333%} +.col-xxl-2{flex:0 0 auto;width:16.66666667%} +.col-xxl-3{flex:0 0 auto;width:25%} +.col-xxl-4{flex:0 0 auto;width:33.33333333%} +.col-xxl-5{flex:0 0 auto;width:41.66666667%} +.col-xxl-6{flex:0 0 auto;width:50%} +.col-xxl-7{flex:0 0 auto;width:58.33333333%} +.col-xxl-8{flex:0 0 auto;width:66.66666667%} +.col-xxl-9{flex:0 0 auto;width:75%} +.col-xxl-10{flex:0 0 auto;width:83.33333333%} +.col-xxl-11{flex:0 0 auto;width:91.66666667%} +.col-xxl-12{flex:0 0 auto;width:100%} +.offset-xxl-0{margin-left:0} +.offset-xxl-1{margin-left:8.33333333%} +.offset-xxl-2{margin-left:16.66666667%} +.offset-xxl-3{margin-left:25%} +.offset-xxl-4{margin-left:33.33333333%} +.offset-xxl-5{margin-left:41.66666667%} +.offset-xxl-6{margin-left:50%} +.offset-xxl-7{margin-left:58.33333333%} +.offset-xxl-8{margin-left:66.66666667%} +.offset-xxl-9{margin-left:75%} +.offset-xxl-10{margin-left:83.33333333%} +.offset-xxl-11{margin-left:91.66666667%} +.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0} +.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0} +.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem} +.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem} +.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem} +.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem} +.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem} +.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem} +.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem} +.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem} +.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem} +.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}} +.fade{transition:opacity .15s linear} +@media (prefers-reduced-motion:reduce){ +.fade{transition:none}} +.fade:not(.show){opacity:0} +.collapse:not(.show){display:none} +.collapsing{height:0;overflow:hidden;transition:height .35s ease} +@media (prefers-reduced-motion:reduce){ +.collapsing{transition:none}} +.collapsing.collapse-horizontal{height:auto;transition:width .35s ease;width:0} +@media (prefers-reduced-motion:reduce){ +.collapsing.collapse-horizontal{transition:none}} +.carousel{position:relative} +.carousel.pointer-event{touch-action:pan-y} +.carousel-inner{overflow:hidden;position:relative;width:100%} +.carousel-inner:after{clear:both;content:"";display:block} +.carousel-item{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:none;float:left;margin-right:-100%;position:relative;transition:transform .6s ease-in-out;width:100%} +@media (prefers-reduced-motion:reduce){ +.carousel-item{transition:none}} +.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block} +.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)} +.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)} +.carousel-fade .carousel-item{opacity:0;transform:none;transition-property:opacity} +.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{opacity:1;z-index:1} +.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{opacity:0;transition:opacity 0s .6s;z-index:0} +@media (prefers-reduced-motion:reduce){ +.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}} +.carousel-control-next,.carousel-control-prev{align-items:center;background:none;border:0;bottom:0;color:#fff;display:flex;justify-content:center;opacity:.5;padding:0;position:absolute;text-align:center;top:0;transition:opacity .15s ease;width:15%;z-index:1} +@media (prefers-reduced-motion:reduce){ +.carousel-control-next,.carousel-control-prev{transition:none}} +.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;opacity:.9;outline:0;text-decoration:none} +.carousel-control-prev{left:0} +.carousel-control-next{right:0} +.carousel-control-next-icon,.carousel-control-prev-icon{background-position:50%;background-repeat:no-repeat;background-size:100% 100%;display:inline-block;height:2rem;width:2rem} +.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3E%3C/svg%3E")} +.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E")} +.carousel-indicators{bottom:0;display:flex;justify-content:center;left:0;margin-bottom:1rem;margin-left:15%;margin-right:15%;padding:0;position:absolute;right:0;z-index:2} +.carousel-indicators [data-bs-target]{background-clip:padding-box;background-color:#fff;border:0;border-bottom:10px solid transparent;border-top:10px solid transparent;box-sizing:content-box;cursor:pointer;flex:0 1 auto;height:3px;margin-left:3px;margin-right:3px;opacity:.5;padding:0;text-indent:-999px;transition:opacity .6s ease;width:30px} +@media (prefers-reduced-motion:reduce){ +.carousel-indicators [data-bs-target]{transition:none}} +.carousel-indicators .active{opacity:1} +.carousel-caption{bottom:1.25rem;color:#fff;left:15%;padding-bottom:1.25rem;padding-top:1.25rem;position:absolute;right:15%;text-align:center} +.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)} +.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000} +.carousel-dark .carousel-caption{color:#000} +[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon{filter:invert(1) grayscale(100)} +[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000} +[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000} +.text-bg-primary{background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important;color:#fff!important} +.text-bg-secondary{background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important;color:#fff!important} +.text-bg-success{background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important;color:#fff!important} +.text-bg-info{background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important;color:#000!important} +.text-bg-warning{background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important;color:#000!important} +.text-bg-danger{background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important;color:#fff!important} +.text-bg-light{background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important;color:#000!important} +.text-bg-default{background-color:RGBA(var(--bs-default-rgb),var(--bs-bg-opacity,1))!important;color:#000!important} +.text-bg-notice{background-color:RGBA(var(--bs-notice-rgb),var(--bs-bg-opacity,1))!important;color:#fff!important} +.text-bg-dark{background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important;color:#fff!important} +.float-start{float:var(--typo3-position-start)!important} +.float-end{float:var(--typo3-position-end)!important} +.float-none{float:none!important} +.d-inline{display:inline!important} +.d-inline-block{display:inline-block!important} +.d-block{display:block!important} +.d-grid{display:grid!important} +.d-inline-grid{display:inline-grid!important} +.d-table{display:table!important} +.d-table-row{display:table-row!important} +.d-table-cell{display:table-cell!important} +.d-flex{display:flex!important} +.d-inline-flex{display:inline-flex!important} +.d-none{display:none!important} +.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important} +.border-0{border:0!important} +.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important} +.border-top-0{border-top:0!important} +.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important} +.border-end-0{border-right:0!important} +.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important} +.border-bottom-0{border-bottom:0!important} +.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important} +.border-start-0{border-left:0!important} +.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important} +.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important} +.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important} +.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important} +.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important} +.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important} +.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important} +.border-default{--bs-border-opacity:1;border-color:rgba(var(--bs-default-rgb),var(--bs-border-opacity))!important} +.border-notice{--bs-border-opacity:1;border-color:rgba(var(--bs-notice-rgb),var(--bs-border-opacity))!important} +.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important} +.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important} +.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important} +.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important} +.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important} +.border-success-subtle{border-color:var(--bs-success-border-subtle)!important} +.border-info-subtle{border-color:var(--bs-info-border-subtle)!important} +.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important} +.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important} +.border-light-subtle{border-color:var(--bs-light-border-subtle)!important} +.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important} +.border-1{border-width:1px!important} +.border-2{border-width:2px!important} +.border-3{border-width:3px!important} +.border-4{border-width:4px!important} +.border-5{border-width:5px!important} +.w-25{width:25%!important} +.w-50{width:50%!important} +.w-75{width:75%!important} +.w-100{width:100%!important} +.w-auto{width:auto!important} +.mw-100{max-width:100%!important} +.vw-100{width:100vw!important} +.min-vw-100{min-width:100vw!important} +.h-25{height:25%!important} +.h-50{height:50%!important} +.h-75{height:75%!important} +.h-100{height:100%!important} +.h-auto{height:auto!important} +.mh-100{max-height:100%!important} +.vh-100{height:100vh!important} +.min-vh-100{min-height:100vh!important} +.flex-fill{flex:1 1 auto!important} +.flex-row{flex-direction:row!important} +.flex-column{flex-direction:column!important} +.flex-row-reverse{flex-direction:row-reverse!important} +.flex-column-reverse{flex-direction:column-reverse!important} +.flex-grow-0{flex-grow:0!important} +.flex-grow-1{flex-grow:1!important} +.flex-shrink-0{flex-shrink:0!important} +.flex-shrink-1{flex-shrink:1!important} +.flex-wrap{flex-wrap:wrap!important} +.flex-nowrap{flex-wrap:nowrap!important} +.flex-wrap-reverse{flex-wrap:wrap-reverse!important} +.justify-content-start{justify-content:flex-start!important} +.justify-content-end{justify-content:flex-end!important} +.justify-content-center{justify-content:center!important} +.justify-content-between{justify-content:space-between!important} +.justify-content-around{justify-content:space-around!important} +.justify-content-evenly{justify-content:space-evenly!important} +.align-items-start{align-items:flex-start!important} +.align-items-end{align-items:flex-end!important} +.align-items-center{align-items:center!important} +.align-items-baseline{align-items:baseline!important} +.align-items-stretch{align-items:stretch!important} +.align-content-start{align-content:flex-start!important} +.align-content-end{align-content:flex-end!important} +.align-content-center{align-content:center!important} +.align-content-between{align-content:space-between!important} +.align-content-around{align-content:space-around!important} +.align-content-stretch{align-content:stretch!important} +.align-self-auto{align-self:auto!important} +.align-self-start{align-self:flex-start!important} +.align-self-end{align-self:flex-end!important} +.align-self-center{align-self:center!important} +.align-self-baseline{align-self:baseline!important} +.align-self-stretch{align-self:stretch!important} +.m-0{margin:0!important} +.m-1{margin:.25rem!important} +.m-2{margin:.5rem!important} +.m-3{margin:1rem!important} +.m-4{margin:1.5rem!important} +.m-5{margin:3rem!important} +.m-auto{margin:auto!important} +.mx-0{margin-left:0!important;margin-right:0!important} +.mx-1{margin-left:.25rem!important;margin-right:.25rem!important} +.mx-2{margin-left:.5rem!important;margin-right:.5rem!important} +.mx-3{margin-left:1rem!important;margin-right:1rem!important} +.mx-4{margin-left:1.5rem!important;margin-right:1.5rem!important} +.mx-5{margin-left:3rem!important;margin-right:3rem!important} +.mx-auto{margin-left:auto!important;margin-right:auto!important} +.my-0{margin-bottom:0!important;margin-top:0!important} +.my-1{margin-bottom:.25rem!important;margin-top:.25rem!important} +.my-2{margin-bottom:.5rem!important;margin-top:.5rem!important} +.my-3{margin-bottom:1rem!important;margin-top:1rem!important} +.my-4{margin-bottom:1.5rem!important;margin-top:1.5rem!important} +.my-5{margin-bottom:3rem!important;margin-top:3rem!important} +.my-auto{margin-bottom:auto!important;margin-top:auto!important} +.mt-0{margin-top:0!important} +.mt-1{margin-top:.25rem!important} +.mt-2{margin-top:.5rem!important} +.mt-3{margin-top:1rem!important} +.mt-4{margin-top:1.5rem!important} +.mt-5{margin-top:3rem!important} +.mt-auto{margin-top:auto!important} +.me-0{margin-right:0!important} +.me-1{margin-right:.25rem!important} +.me-2{margin-right:.5rem!important} +.me-3{margin-right:1rem!important} +.me-4{margin-right:1.5rem!important} +.me-5{margin-right:3rem!important} +.me-auto{margin-right:auto!important} +.mb-0{margin-bottom:0!important} +.mb-1{margin-bottom:.25rem!important} +.mb-2{margin-bottom:.5rem!important} +.mb-3{margin-bottom:1rem!important} +.mb-4{margin-bottom:1.5rem!important} +.mb-5{margin-bottom:3rem!important} +.mb-auto{margin-bottom:auto!important} +.ms-0{margin-left:0!important} +.ms-1{margin-left:.25rem!important} +.ms-2{margin-left:.5rem!important} +.ms-3{margin-left:1rem!important} +.ms-4{margin-left:1.5rem!important} +.ms-5{margin-left:3rem!important} +.ms-auto{margin-left:auto!important} +.p-0{padding:0!important} +.p-1{padding:.25rem!important} +.p-2{padding:.5rem!important} +.p-3{padding:1rem!important} +.p-4{padding:1.5rem!important} +.p-5{padding:3rem!important} +.px-0{padding-left:0!important;padding-right:0!important} +.px-1{padding-left:.25rem!important;padding-right:.25rem!important} +.px-2{padding-left:.5rem!important;padding-right:.5rem!important} +.px-3{padding-left:1rem!important;padding-right:1rem!important} +.px-4{padding-left:1.5rem!important;padding-right:1.5rem!important} +.px-5{padding-left:3rem!important;padding-right:3rem!important} +.py-0{padding-bottom:0!important;padding-top:0!important} +.py-1{padding-bottom:.25rem!important;padding-top:.25rem!important} +.py-2{padding-bottom:.5rem!important;padding-top:.5rem!important} +.py-3{padding-bottom:1rem!important;padding-top:1rem!important} +.py-4{padding-bottom:1.5rem!important;padding-top:1.5rem!important} +.py-5{padding-bottom:3rem!important;padding-top:3rem!important} +.pt-0{padding-top:0!important} +.pt-1{padding-top:.25rem!important} +.pt-2{padding-top:.5rem!important} +.pt-3{padding-top:1rem!important} +.pt-4{padding-top:1.5rem!important} +.pt-5{padding-top:3rem!important} +.pe-0{padding-right:0!important} +.pe-1{padding-right:.25rem!important} +.pe-2{padding-right:.5rem!important} +.pe-3{padding-right:1rem!important} +.pe-4{padding-right:1.5rem!important} +.pe-5{padding-right:3rem!important} +.pb-0{padding-bottom:0!important} +.pb-1{padding-bottom:.25rem!important} +.pb-2{padding-bottom:.5rem!important} +.pb-3{padding-bottom:1rem!important} +.pb-4{padding-bottom:1.5rem!important} +.pb-5{padding-bottom:3rem!important} +.ps-0{padding-left:0!important} +.ps-1{padding-left:.25rem!important} +.ps-2{padding-left:.5rem!important} +.ps-3{padding-left:1rem!important} +.ps-4{padding-left:1.5rem!important} +.ps-5{padding-left:3rem!important} +.gap-0{gap:0!important} +.gap-1{gap:.25rem!important} +.gap-2{gap:.5rem!important} +.gap-3{gap:1rem!important} +.gap-4{gap:1.5rem!important} +.gap-5{gap:3rem!important} +.row-gap-0{row-gap:0!important} +.row-gap-1{row-gap:.25rem!important} +.row-gap-2{row-gap:.5rem!important} +.row-gap-3{row-gap:1rem!important} +.row-gap-4{row-gap:1.5rem!important} +.row-gap-5{row-gap:3rem!important} +.font-monospace{font-family:var(--bs-font-monospace)!important} +.fs-1{font-size:calc(1.275rem + .3vw)!important} +.fs-2{font-size:1.25rem!important} +.fs-3{font-size:1rem!important} +.fs-4{font-size:.875rem!important} +.fs-5,.fs-6{font-size:.75rem!important} +.fst-italic{font-style:italic!important} +.fst-normal{font-style:normal!important} +.fw-lighter{font-weight:lighter!important} +.fw-light{font-weight:300!important} +.fw-normal{font-weight:400!important} +.fw-medium{font-weight:500!important} +.fw-semibold{font-weight:600!important} +.fw-bold{font-weight:700!important} +.fw-bolder{font-weight:bolder!important} +.lh-1{line-height:1!important} +.lh-sm{line-height:1.25!important} +.lh-base{line-height:1.5!important} +.lh-lg{line-height:2!important} +.text-start{text-align:start!important} +.text-end{text-align:end!important} +.text-center{text-align:center!important} +.text-decoration-none{text-decoration:none!important} +.text-decoration-underline{text-decoration:underline!important} +.text-decoration-line-through{text-decoration:line-through!important} +.text-lowercase{text-transform:lowercase!important} +.text-uppercase{text-transform:uppercase!important} +.text-capitalize{text-transform:capitalize!important} +.text-wrap{white-space:normal!important} +.text-nowrap{white-space:nowrap!important} +.text-break{word-wrap:break-word!important;word-break:break-word!important} +.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important} +.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important} +.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important} +.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important} +.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important} +.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important} +.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important} +.link-underline-default{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-default-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-default-rgb),var(--bs-link-underline-opacity))!important} +.link-underline-notice{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-notice-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-notice-rgb),var(--bs-link-underline-opacity))!important} +.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important} +.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important} +.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important} +.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important} +.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important} +.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important} +.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important} +.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important} +.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important} +.bg-default{--bs-bg-opacity:1;background-color:rgba(var(--bs-default-rgb),var(--bs-bg-opacity))!important} +.bg-notice{--bs-bg-opacity:1;background-color:rgba(var(--bs-notice-rgb),var(--bs-bg-opacity))!important} +.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important} +.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important} +.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important} +.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important} +.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important} +.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important} +.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important} +.bg-opacity-10{--bs-bg-opacity:0.1} +.bg-opacity-25{--bs-bg-opacity:0.25} +.bg-opacity-50{--bs-bg-opacity:0.5} +.bg-opacity-75{--bs-bg-opacity:0.75} +.bg-opacity-100{--bs-bg-opacity:1} +.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important} +.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important} +.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important} +.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important} +.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important} +.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important} +.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important} +.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important} +.bg-gradient{background-image:var(--bs-gradient)!important} +.visible{visibility:visible!important} +.invisible{visibility:hidden!important} +@media (min-width:576px){ +.d-sm-inline{display:inline!important} +.d-sm-inline-block{display:inline-block!important} +.d-sm-block{display:block!important} +.d-sm-grid{display:grid!important} +.d-sm-inline-grid{display:inline-grid!important} +.d-sm-table{display:table!important} +.d-sm-table-row{display:table-row!important} +.d-sm-table-cell{display:table-cell!important} +.d-sm-flex{display:flex!important} +.d-sm-inline-flex{display:inline-flex!important} +.d-sm-none{display:none!important} +.flex-sm-fill{flex:1 1 auto!important} +.flex-sm-row{flex-direction:row!important} +.flex-sm-column{flex-direction:column!important} +.flex-sm-row-reverse{flex-direction:row-reverse!important} +.flex-sm-column-reverse{flex-direction:column-reverse!important} +.flex-sm-grow-0{flex-grow:0!important} +.flex-sm-grow-1{flex-grow:1!important} +.flex-sm-shrink-0{flex-shrink:0!important} +.flex-sm-shrink-1{flex-shrink:1!important} +.flex-sm-wrap{flex-wrap:wrap!important} +.flex-sm-nowrap{flex-wrap:nowrap!important} +.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important} +.justify-content-sm-start{justify-content:flex-start!important} +.justify-content-sm-end{justify-content:flex-end!important} +.justify-content-sm-center{justify-content:center!important} +.justify-content-sm-between{justify-content:space-between!important} +.justify-content-sm-around{justify-content:space-around!important} +.justify-content-sm-evenly{justify-content:space-evenly!important} +.align-items-sm-start{align-items:flex-start!important} +.align-items-sm-end{align-items:flex-end!important} +.align-items-sm-center{align-items:center!important} +.align-items-sm-baseline{align-items:baseline!important} +.align-items-sm-stretch{align-items:stretch!important} +.align-content-sm-start{align-content:flex-start!important} +.align-content-sm-end{align-content:flex-end!important} +.align-content-sm-center{align-content:center!important} +.align-content-sm-between{align-content:space-between!important} +.align-content-sm-around{align-content:space-around!important} +.align-content-sm-stretch{align-content:stretch!important} +.align-self-sm-auto{align-self:auto!important} +.align-self-sm-start{align-self:flex-start!important} +.align-self-sm-end{align-self:flex-end!important} +.align-self-sm-center{align-self:center!important} +.align-self-sm-baseline{align-self:baseline!important} +.align-self-sm-stretch{align-self:stretch!important} +.m-sm-0{margin:0!important} +.m-sm-1{margin:.25rem!important} +.m-sm-2{margin:.5rem!important} +.m-sm-3{margin:1rem!important} +.m-sm-4{margin:1.5rem!important} +.m-sm-5{margin:3rem!important} +.m-sm-auto{margin:auto!important} +.mx-sm-0{margin-left:0!important;margin-right:0!important} +.mx-sm-1{margin-left:.25rem!important;margin-right:.25rem!important} +.mx-sm-2{margin-left:.5rem!important;margin-right:.5rem!important} +.mx-sm-3{margin-left:1rem!important;margin-right:1rem!important} +.mx-sm-4{margin-left:1.5rem!important;margin-right:1.5rem!important} +.mx-sm-5{margin-left:3rem!important;margin-right:3rem!important} +.mx-sm-auto{margin-left:auto!important;margin-right:auto!important} +.my-sm-0{margin-bottom:0!important;margin-top:0!important} +.my-sm-1{margin-bottom:.25rem!important;margin-top:.25rem!important} +.my-sm-2{margin-bottom:.5rem!important;margin-top:.5rem!important} +.my-sm-3{margin-bottom:1rem!important;margin-top:1rem!important} +.my-sm-4{margin-bottom:1.5rem!important;margin-top:1.5rem!important} +.my-sm-5{margin-bottom:3rem!important;margin-top:3rem!important} +.my-sm-auto{margin-bottom:auto!important;margin-top:auto!important} +.mt-sm-0{margin-top:0!important} +.mt-sm-1{margin-top:.25rem!important} +.mt-sm-2{margin-top:.5rem!important} +.mt-sm-3{margin-top:1rem!important} +.mt-sm-4{margin-top:1.5rem!important} +.mt-sm-5{margin-top:3rem!important} +.mt-sm-auto{margin-top:auto!important} +.me-sm-0{margin-right:0!important} +.me-sm-1{margin-right:.25rem!important} +.me-sm-2{margin-right:.5rem!important} +.me-sm-3{margin-right:1rem!important} +.me-sm-4{margin-right:1.5rem!important} +.me-sm-5{margin-right:3rem!important} +.me-sm-auto{margin-right:auto!important} +.mb-sm-0{margin-bottom:0!important} +.mb-sm-1{margin-bottom:.25rem!important} +.mb-sm-2{margin-bottom:.5rem!important} +.mb-sm-3{margin-bottom:1rem!important} +.mb-sm-4{margin-bottom:1.5rem!important} +.mb-sm-5{margin-bottom:3rem!important} +.mb-sm-auto{margin-bottom:auto!important} +.ms-sm-0{margin-left:0!important} +.ms-sm-1{margin-left:.25rem!important} +.ms-sm-2{margin-left:.5rem!important} +.ms-sm-3{margin-left:1rem!important} +.ms-sm-4{margin-left:1.5rem!important} +.ms-sm-5{margin-left:3rem!important} +.ms-sm-auto{margin-left:auto!important} +.p-sm-0{padding:0!important} +.p-sm-1{padding:.25rem!important} +.p-sm-2{padding:.5rem!important} +.p-sm-3{padding:1rem!important} +.p-sm-4{padding:1.5rem!important} +.p-sm-5{padding:3rem!important} +.px-sm-0{padding-left:0!important;padding-right:0!important} +.px-sm-1{padding-left:.25rem!important;padding-right:.25rem!important} +.px-sm-2{padding-left:.5rem!important;padding-right:.5rem!important} +.px-sm-3{padding-left:1rem!important;padding-right:1rem!important} +.px-sm-4{padding-left:1.5rem!important;padding-right:1.5rem!important} +.px-sm-5{padding-left:3rem!important;padding-right:3rem!important} +.py-sm-0{padding-bottom:0!important;padding-top:0!important} +.py-sm-1{padding-bottom:.25rem!important;padding-top:.25rem!important} +.py-sm-2{padding-bottom:.5rem!important;padding-top:.5rem!important} +.py-sm-3{padding-bottom:1rem!important;padding-top:1rem!important} +.py-sm-4{padding-bottom:1.5rem!important;padding-top:1.5rem!important} +.py-sm-5{padding-bottom:3rem!important;padding-top:3rem!important} +.pt-sm-0{padding-top:0!important} +.pt-sm-1{padding-top:.25rem!important} +.pt-sm-2{padding-top:.5rem!important} +.pt-sm-3{padding-top:1rem!important} +.pt-sm-4{padding-top:1.5rem!important} +.pt-sm-5{padding-top:3rem!important} +.pe-sm-0{padding-right:0!important} +.pe-sm-1{padding-right:.25rem!important} +.pe-sm-2{padding-right:.5rem!important} +.pe-sm-3{padding-right:1rem!important} +.pe-sm-4{padding-right:1.5rem!important} +.pe-sm-5{padding-right:3rem!important} +.pb-sm-0{padding-bottom:0!important} +.pb-sm-1{padding-bottom:.25rem!important} +.pb-sm-2{padding-bottom:.5rem!important} +.pb-sm-3{padding-bottom:1rem!important} +.pb-sm-4{padding-bottom:1.5rem!important} +.pb-sm-5{padding-bottom:3rem!important} +.ps-sm-0{padding-left:0!important} +.ps-sm-1{padding-left:.25rem!important} +.ps-sm-2{padding-left:.5rem!important} +.ps-sm-3{padding-left:1rem!important} +.ps-sm-4{padding-left:1.5rem!important} +.ps-sm-5{padding-left:3rem!important} +.gap-sm-0{gap:0!important} +.gap-sm-1{gap:.25rem!important} +.gap-sm-2{gap:.5rem!important} +.gap-sm-3{gap:1rem!important} +.gap-sm-4{gap:1.5rem!important} +.gap-sm-5{gap:3rem!important} +.row-gap-sm-0{row-gap:0!important} +.row-gap-sm-1{row-gap:.25rem!important} +.row-gap-sm-2{row-gap:.5rem!important} +.row-gap-sm-3{row-gap:1rem!important} +.row-gap-sm-4{row-gap:1.5rem!important} +.row-gap-sm-5{row-gap:3rem!important} +.text-sm-start{text-align:start!important} +.text-sm-end{text-align:end!important} +.text-sm-center{text-align:center!important}} +@media (min-width:768px){ +.d-md-inline{display:inline!important} +.d-md-inline-block{display:inline-block!important} +.d-md-block{display:block!important} +.d-md-grid{display:grid!important} +.d-md-inline-grid{display:inline-grid!important} +.d-md-table{display:table!important} +.d-md-table-row{display:table-row!important} +.d-md-table-cell{display:table-cell!important} +.d-md-flex{display:flex!important} +.d-md-inline-flex{display:inline-flex!important} +.d-md-none{display:none!important} +.flex-md-fill{flex:1 1 auto!important} +.flex-md-row{flex-direction:row!important} +.flex-md-column{flex-direction:column!important} +.flex-md-row-reverse{flex-direction:row-reverse!important} +.flex-md-column-reverse{flex-direction:column-reverse!important} +.flex-md-grow-0{flex-grow:0!important} +.flex-md-grow-1{flex-grow:1!important} +.flex-md-shrink-0{flex-shrink:0!important} +.flex-md-shrink-1{flex-shrink:1!important} +.flex-md-wrap{flex-wrap:wrap!important} +.flex-md-nowrap{flex-wrap:nowrap!important} +.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important} +.justify-content-md-start{justify-content:flex-start!important} +.justify-content-md-end{justify-content:flex-end!important} +.justify-content-md-center{justify-content:center!important} +.justify-content-md-between{justify-content:space-between!important} +.justify-content-md-around{justify-content:space-around!important} +.justify-content-md-evenly{justify-content:space-evenly!important} +.align-items-md-start{align-items:flex-start!important} +.align-items-md-end{align-items:flex-end!important} +.align-items-md-center{align-items:center!important} +.align-items-md-baseline{align-items:baseline!important} +.align-items-md-stretch{align-items:stretch!important} +.align-content-md-start{align-content:flex-start!important} +.align-content-md-end{align-content:flex-end!important} +.align-content-md-center{align-content:center!important} +.align-content-md-between{align-content:space-between!important} +.align-content-md-around{align-content:space-around!important} +.align-content-md-stretch{align-content:stretch!important} +.align-self-md-auto{align-self:auto!important} +.align-self-md-start{align-self:flex-start!important} +.align-self-md-end{align-self:flex-end!important} +.align-self-md-center{align-self:center!important} +.align-self-md-baseline{align-self:baseline!important} +.align-self-md-stretch{align-self:stretch!important} +.m-md-0{margin:0!important} +.m-md-1{margin:.25rem!important} +.m-md-2{margin:.5rem!important} +.m-md-3{margin:1rem!important} +.m-md-4{margin:1.5rem!important} +.m-md-5{margin:3rem!important} +.m-md-auto{margin:auto!important} +.mx-md-0{margin-left:0!important;margin-right:0!important} +.mx-md-1{margin-left:.25rem!important;margin-right:.25rem!important} +.mx-md-2{margin-left:.5rem!important;margin-right:.5rem!important} +.mx-md-3{margin-left:1rem!important;margin-right:1rem!important} +.mx-md-4{margin-left:1.5rem!important;margin-right:1.5rem!important} +.mx-md-5{margin-left:3rem!important;margin-right:3rem!important} +.mx-md-auto{margin-left:auto!important;margin-right:auto!important} +.my-md-0{margin-bottom:0!important;margin-top:0!important} +.my-md-1{margin-bottom:.25rem!important;margin-top:.25rem!important} +.my-md-2{margin-bottom:.5rem!important;margin-top:.5rem!important} +.my-md-3{margin-bottom:1rem!important;margin-top:1rem!important} +.my-md-4{margin-bottom:1.5rem!important;margin-top:1.5rem!important} +.my-md-5{margin-bottom:3rem!important;margin-top:3rem!important} +.my-md-auto{margin-bottom:auto!important;margin-top:auto!important} +.mt-md-0{margin-top:0!important} +.mt-md-1{margin-top:.25rem!important} +.mt-md-2{margin-top:.5rem!important} +.mt-md-3{margin-top:1rem!important} +.mt-md-4{margin-top:1.5rem!important} +.mt-md-5{margin-top:3rem!important} +.mt-md-auto{margin-top:auto!important} +.me-md-0{margin-right:0!important} +.me-md-1{margin-right:.25rem!important} +.me-md-2{margin-right:.5rem!important} +.me-md-3{margin-right:1rem!important} +.me-md-4{margin-right:1.5rem!important} +.me-md-5{margin-right:3rem!important} +.me-md-auto{margin-right:auto!important} +.mb-md-0{margin-bottom:0!important} +.mb-md-1{margin-bottom:.25rem!important} +.mb-md-2{margin-bottom:.5rem!important} +.mb-md-3{margin-bottom:1rem!important} +.mb-md-4{margin-bottom:1.5rem!important} +.mb-md-5{margin-bottom:3rem!important} +.mb-md-auto{margin-bottom:auto!important} +.ms-md-0{margin-left:0!important} +.ms-md-1{margin-left:.25rem!important} +.ms-md-2{margin-left:.5rem!important} +.ms-md-3{margin-left:1rem!important} +.ms-md-4{margin-left:1.5rem!important} +.ms-md-5{margin-left:3rem!important} +.ms-md-auto{margin-left:auto!important} +.p-md-0{padding:0!important} +.p-md-1{padding:.25rem!important} +.p-md-2{padding:.5rem!important} +.p-md-3{padding:1rem!important} +.p-md-4{padding:1.5rem!important} +.p-md-5{padding:3rem!important} +.px-md-0{padding-left:0!important;padding-right:0!important} +.px-md-1{padding-left:.25rem!important;padding-right:.25rem!important} +.px-md-2{padding-left:.5rem!important;padding-right:.5rem!important} +.px-md-3{padding-left:1rem!important;padding-right:1rem!important} +.px-md-4{padding-left:1.5rem!important;padding-right:1.5rem!important} +.px-md-5{padding-left:3rem!important;padding-right:3rem!important} +.py-md-0{padding-bottom:0!important;padding-top:0!important} +.py-md-1{padding-bottom:.25rem!important;padding-top:.25rem!important} +.py-md-2{padding-bottom:.5rem!important;padding-top:.5rem!important} +.py-md-3{padding-bottom:1rem!important;padding-top:1rem!important} +.py-md-4{padding-bottom:1.5rem!important;padding-top:1.5rem!important} +.py-md-5{padding-bottom:3rem!important;padding-top:3rem!important} +.pt-md-0{padding-top:0!important} +.pt-md-1{padding-top:.25rem!important} +.pt-md-2{padding-top:.5rem!important} +.pt-md-3{padding-top:1rem!important} +.pt-md-4{padding-top:1.5rem!important} +.pt-md-5{padding-top:3rem!important} +.pe-md-0{padding-right:0!important} +.pe-md-1{padding-right:.25rem!important} +.pe-md-2{padding-right:.5rem!important} +.pe-md-3{padding-right:1rem!important} +.pe-md-4{padding-right:1.5rem!important} +.pe-md-5{padding-right:3rem!important} +.pb-md-0{padding-bottom:0!important} +.pb-md-1{padding-bottom:.25rem!important} +.pb-md-2{padding-bottom:.5rem!important} +.pb-md-3{padding-bottom:1rem!important} +.pb-md-4{padding-bottom:1.5rem!important} +.pb-md-5{padding-bottom:3rem!important} +.ps-md-0{padding-left:0!important} +.ps-md-1{padding-left:.25rem!important} +.ps-md-2{padding-left:.5rem!important} +.ps-md-3{padding-left:1rem!important} +.ps-md-4{padding-left:1.5rem!important} +.ps-md-5{padding-left:3rem!important} +.gap-md-0{gap:0!important} +.gap-md-1{gap:.25rem!important} +.gap-md-2{gap:.5rem!important} +.gap-md-3{gap:1rem!important} +.gap-md-4{gap:1.5rem!important} +.gap-md-5{gap:3rem!important} +.row-gap-md-0{row-gap:0!important} +.row-gap-md-1{row-gap:.25rem!important} +.row-gap-md-2{row-gap:.5rem!important} +.row-gap-md-3{row-gap:1rem!important} +.row-gap-md-4{row-gap:1.5rem!important} +.row-gap-md-5{row-gap:3rem!important} +.text-md-start{text-align:start!important} +.text-md-end{text-align:end!important} +.text-md-center{text-align:center!important}} +@media (min-width:992px){ +.d-lg-inline{display:inline!important} +.d-lg-inline-block{display:inline-block!important} +.d-lg-block{display:block!important} +.d-lg-grid{display:grid!important} +.d-lg-inline-grid{display:inline-grid!important} +.d-lg-table{display:table!important} +.d-lg-table-row{display:table-row!important} +.d-lg-table-cell{display:table-cell!important} +.d-lg-flex{display:flex!important} +.d-lg-inline-flex{display:inline-flex!important} +.d-lg-none{display:none!important} +.flex-lg-fill{flex:1 1 auto!important} +.flex-lg-row{flex-direction:row!important} +.flex-lg-column{flex-direction:column!important} +.flex-lg-row-reverse{flex-direction:row-reverse!important} +.flex-lg-column-reverse{flex-direction:column-reverse!important} +.flex-lg-grow-0{flex-grow:0!important} +.flex-lg-grow-1{flex-grow:1!important} +.flex-lg-shrink-0{flex-shrink:0!important} +.flex-lg-shrink-1{flex-shrink:1!important} +.flex-lg-wrap{flex-wrap:wrap!important} +.flex-lg-nowrap{flex-wrap:nowrap!important} +.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important} +.justify-content-lg-start{justify-content:flex-start!important} +.justify-content-lg-end{justify-content:flex-end!important} +.justify-content-lg-center{justify-content:center!important} +.justify-content-lg-between{justify-content:space-between!important} +.justify-content-lg-around{justify-content:space-around!important} +.justify-content-lg-evenly{justify-content:space-evenly!important} +.align-items-lg-start{align-items:flex-start!important} +.align-items-lg-end{align-items:flex-end!important} +.align-items-lg-center{align-items:center!important} +.align-items-lg-baseline{align-items:baseline!important} +.align-items-lg-stretch{align-items:stretch!important} +.align-content-lg-start{align-content:flex-start!important} +.align-content-lg-end{align-content:flex-end!important} +.align-content-lg-center{align-content:center!important} +.align-content-lg-between{align-content:space-between!important} +.align-content-lg-around{align-content:space-around!important} +.align-content-lg-stretch{align-content:stretch!important} +.align-self-lg-auto{align-self:auto!important} +.align-self-lg-start{align-self:flex-start!important} +.align-self-lg-end{align-self:flex-end!important} +.align-self-lg-center{align-self:center!important} +.align-self-lg-baseline{align-self:baseline!important} +.align-self-lg-stretch{align-self:stretch!important} +.m-lg-0{margin:0!important} +.m-lg-1{margin:.25rem!important} +.m-lg-2{margin:.5rem!important} +.m-lg-3{margin:1rem!important} +.m-lg-4{margin:1.5rem!important} +.m-lg-5{margin:3rem!important} +.m-lg-auto{margin:auto!important} +.mx-lg-0{margin-left:0!important;margin-right:0!important} +.mx-lg-1{margin-left:.25rem!important;margin-right:.25rem!important} +.mx-lg-2{margin-left:.5rem!important;margin-right:.5rem!important} +.mx-lg-3{margin-left:1rem!important;margin-right:1rem!important} +.mx-lg-4{margin-left:1.5rem!important;margin-right:1.5rem!important} +.mx-lg-5{margin-left:3rem!important;margin-right:3rem!important} +.mx-lg-auto{margin-left:auto!important;margin-right:auto!important} +.my-lg-0{margin-bottom:0!important;margin-top:0!important} +.my-lg-1{margin-bottom:.25rem!important;margin-top:.25rem!important} +.my-lg-2{margin-bottom:.5rem!important;margin-top:.5rem!important} +.my-lg-3{margin-bottom:1rem!important;margin-top:1rem!important} +.my-lg-4{margin-bottom:1.5rem!important;margin-top:1.5rem!important} +.my-lg-5{margin-bottom:3rem!important;margin-top:3rem!important} +.my-lg-auto{margin-bottom:auto!important;margin-top:auto!important} +.mt-lg-0{margin-top:0!important} +.mt-lg-1{margin-top:.25rem!important} +.mt-lg-2{margin-top:.5rem!important} +.mt-lg-3{margin-top:1rem!important} +.mt-lg-4{margin-top:1.5rem!important} +.mt-lg-5{margin-top:3rem!important} +.mt-lg-auto{margin-top:auto!important} +.me-lg-0{margin-right:0!important} +.me-lg-1{margin-right:.25rem!important} +.me-lg-2{margin-right:.5rem!important} +.me-lg-3{margin-right:1rem!important} +.me-lg-4{margin-right:1.5rem!important} +.me-lg-5{margin-right:3rem!important} +.me-lg-auto{margin-right:auto!important} +.mb-lg-0{margin-bottom:0!important} +.mb-lg-1{margin-bottom:.25rem!important} +.mb-lg-2{margin-bottom:.5rem!important} +.mb-lg-3{margin-bottom:1rem!important} +.mb-lg-4{margin-bottom:1.5rem!important} +.mb-lg-5{margin-bottom:3rem!important} +.mb-lg-auto{margin-bottom:auto!important} +.ms-lg-0{margin-left:0!important} +.ms-lg-1{margin-left:.25rem!important} +.ms-lg-2{margin-left:.5rem!important} +.ms-lg-3{margin-left:1rem!important} +.ms-lg-4{margin-left:1.5rem!important} +.ms-lg-5{margin-left:3rem!important} +.ms-lg-auto{margin-left:auto!important} +.p-lg-0{padding:0!important} +.p-lg-1{padding:.25rem!important} +.p-lg-2{padding:.5rem!important} +.p-lg-3{padding:1rem!important} +.p-lg-4{padding:1.5rem!important} +.p-lg-5{padding:3rem!important} +.px-lg-0{padding-left:0!important;padding-right:0!important} +.px-lg-1{padding-left:.25rem!important;padding-right:.25rem!important} +.px-lg-2{padding-left:.5rem!important;padding-right:.5rem!important} +.px-lg-3{padding-left:1rem!important;padding-right:1rem!important} +.px-lg-4{padding-left:1.5rem!important;padding-right:1.5rem!important} +.px-lg-5{padding-left:3rem!important;padding-right:3rem!important} +.py-lg-0{padding-bottom:0!important;padding-top:0!important} +.py-lg-1{padding-bottom:.25rem!important;padding-top:.25rem!important} +.py-lg-2{padding-bottom:.5rem!important;padding-top:.5rem!important} +.py-lg-3{padding-bottom:1rem!important;padding-top:1rem!important} +.py-lg-4{padding-bottom:1.5rem!important;padding-top:1.5rem!important} +.py-lg-5{padding-bottom:3rem!important;padding-top:3rem!important} +.pt-lg-0{padding-top:0!important} +.pt-lg-1{padding-top:.25rem!important} +.pt-lg-2{padding-top:.5rem!important} +.pt-lg-3{padding-top:1rem!important} +.pt-lg-4{padding-top:1.5rem!important} +.pt-lg-5{padding-top:3rem!important} +.pe-lg-0{padding-right:0!important} +.pe-lg-1{padding-right:.25rem!important} +.pe-lg-2{padding-right:.5rem!important} +.pe-lg-3{padding-right:1rem!important} +.pe-lg-4{padding-right:1.5rem!important} +.pe-lg-5{padding-right:3rem!important} +.pb-lg-0{padding-bottom:0!important} +.pb-lg-1{padding-bottom:.25rem!important} +.pb-lg-2{padding-bottom:.5rem!important} +.pb-lg-3{padding-bottom:1rem!important} +.pb-lg-4{padding-bottom:1.5rem!important} +.pb-lg-5{padding-bottom:3rem!important} +.ps-lg-0{padding-left:0!important} +.ps-lg-1{padding-left:.25rem!important} +.ps-lg-2{padding-left:.5rem!important} +.ps-lg-3{padding-left:1rem!important} +.ps-lg-4{padding-left:1.5rem!important} +.ps-lg-5{padding-left:3rem!important} +.gap-lg-0{gap:0!important} +.gap-lg-1{gap:.25rem!important} +.gap-lg-2{gap:.5rem!important} +.gap-lg-3{gap:1rem!important} +.gap-lg-4{gap:1.5rem!important} +.gap-lg-5{gap:3rem!important} +.row-gap-lg-0{row-gap:0!important} +.row-gap-lg-1{row-gap:.25rem!important} +.row-gap-lg-2{row-gap:.5rem!important} +.row-gap-lg-3{row-gap:1rem!important} +.row-gap-lg-4{row-gap:1.5rem!important} +.row-gap-lg-5{row-gap:3rem!important} +.text-lg-start{text-align:start!important} +.text-lg-end{text-align:end!important} +.text-lg-center{text-align:center!important}} +@media (min-width:1200px){ +.d-xl-inline{display:inline!important} +.d-xl-inline-block{display:inline-block!important} +.d-xl-block{display:block!important} +.d-xl-grid{display:grid!important} +.d-xl-inline-grid{display:inline-grid!important} +.d-xl-table{display:table!important} +.d-xl-table-row{display:table-row!important} +.d-xl-table-cell{display:table-cell!important} +.d-xl-flex{display:flex!important} +.d-xl-inline-flex{display:inline-flex!important} +.d-xl-none{display:none!important} +.flex-xl-fill{flex:1 1 auto!important} +.flex-xl-row{flex-direction:row!important} +.flex-xl-column{flex-direction:column!important} +.flex-xl-row-reverse{flex-direction:row-reverse!important} +.flex-xl-column-reverse{flex-direction:column-reverse!important} +.flex-xl-grow-0{flex-grow:0!important} +.flex-xl-grow-1{flex-grow:1!important} +.flex-xl-shrink-0{flex-shrink:0!important} +.flex-xl-shrink-1{flex-shrink:1!important} +.flex-xl-wrap{flex-wrap:wrap!important} +.flex-xl-nowrap{flex-wrap:nowrap!important} +.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important} +.justify-content-xl-start{justify-content:flex-start!important} +.justify-content-xl-end{justify-content:flex-end!important} +.justify-content-xl-center{justify-content:center!important} +.justify-content-xl-between{justify-content:space-between!important} +.justify-content-xl-around{justify-content:space-around!important} +.justify-content-xl-evenly{justify-content:space-evenly!important} +.align-items-xl-start{align-items:flex-start!important} +.align-items-xl-end{align-items:flex-end!important} +.align-items-xl-center{align-items:center!important} +.align-items-xl-baseline{align-items:baseline!important} +.align-items-xl-stretch{align-items:stretch!important} +.align-content-xl-start{align-content:flex-start!important} +.align-content-xl-end{align-content:flex-end!important} +.align-content-xl-center{align-content:center!important} +.align-content-xl-between{align-content:space-between!important} +.align-content-xl-around{align-content:space-around!important} +.align-content-xl-stretch{align-content:stretch!important} +.align-self-xl-auto{align-self:auto!important} +.align-self-xl-start{align-self:flex-start!important} +.align-self-xl-end{align-self:flex-end!important} +.align-self-xl-center{align-self:center!important} +.align-self-xl-baseline{align-self:baseline!important} +.align-self-xl-stretch{align-self:stretch!important} +.m-xl-0{margin:0!important} +.m-xl-1{margin:.25rem!important} +.m-xl-2{margin:.5rem!important} +.m-xl-3{margin:1rem!important} +.m-xl-4{margin:1.5rem!important} +.m-xl-5{margin:3rem!important} +.m-xl-auto{margin:auto!important} +.mx-xl-0{margin-left:0!important;margin-right:0!important} +.mx-xl-1{margin-left:.25rem!important;margin-right:.25rem!important} +.mx-xl-2{margin-left:.5rem!important;margin-right:.5rem!important} +.mx-xl-3{margin-left:1rem!important;margin-right:1rem!important} +.mx-xl-4{margin-left:1.5rem!important;margin-right:1.5rem!important} +.mx-xl-5{margin-left:3rem!important;margin-right:3rem!important} +.mx-xl-auto{margin-left:auto!important;margin-right:auto!important} +.my-xl-0{margin-bottom:0!important;margin-top:0!important} +.my-xl-1{margin-bottom:.25rem!important;margin-top:.25rem!important} +.my-xl-2{margin-bottom:.5rem!important;margin-top:.5rem!important} +.my-xl-3{margin-bottom:1rem!important;margin-top:1rem!important} +.my-xl-4{margin-bottom:1.5rem!important;margin-top:1.5rem!important} +.my-xl-5{margin-bottom:3rem!important;margin-top:3rem!important} +.my-xl-auto{margin-bottom:auto!important;margin-top:auto!important} +.mt-xl-0{margin-top:0!important} +.mt-xl-1{margin-top:.25rem!important} +.mt-xl-2{margin-top:.5rem!important} +.mt-xl-3{margin-top:1rem!important} +.mt-xl-4{margin-top:1.5rem!important} +.mt-xl-5{margin-top:3rem!important} +.mt-xl-auto{margin-top:auto!important} +.me-xl-0{margin-right:0!important} +.me-xl-1{margin-right:.25rem!important} +.me-xl-2{margin-right:.5rem!important} +.me-xl-3{margin-right:1rem!important} +.me-xl-4{margin-right:1.5rem!important} +.me-xl-5{margin-right:3rem!important} +.me-xl-auto{margin-right:auto!important} +.mb-xl-0{margin-bottom:0!important} +.mb-xl-1{margin-bottom:.25rem!important} +.mb-xl-2{margin-bottom:.5rem!important} +.mb-xl-3{margin-bottom:1rem!important} +.mb-xl-4{margin-bottom:1.5rem!important} +.mb-xl-5{margin-bottom:3rem!important} +.mb-xl-auto{margin-bottom:auto!important} +.ms-xl-0{margin-left:0!important} +.ms-xl-1{margin-left:.25rem!important} +.ms-xl-2{margin-left:.5rem!important} +.ms-xl-3{margin-left:1rem!important} +.ms-xl-4{margin-left:1.5rem!important} +.ms-xl-5{margin-left:3rem!important} +.ms-xl-auto{margin-left:auto!important} +.p-xl-0{padding:0!important} +.p-xl-1{padding:.25rem!important} +.p-xl-2{padding:.5rem!important} +.p-xl-3{padding:1rem!important} +.p-xl-4{padding:1.5rem!important} +.p-xl-5{padding:3rem!important} +.px-xl-0{padding-left:0!important;padding-right:0!important} +.px-xl-1{padding-left:.25rem!important;padding-right:.25rem!important} +.px-xl-2{padding-left:.5rem!important;padding-right:.5rem!important} +.px-xl-3{padding-left:1rem!important;padding-right:1rem!important} +.px-xl-4{padding-left:1.5rem!important;padding-right:1.5rem!important} +.px-xl-5{padding-left:3rem!important;padding-right:3rem!important} +.py-xl-0{padding-bottom:0!important;padding-top:0!important} +.py-xl-1{padding-bottom:.25rem!important;padding-top:.25rem!important} +.py-xl-2{padding-bottom:.5rem!important;padding-top:.5rem!important} +.py-xl-3{padding-bottom:1rem!important;padding-top:1rem!important} +.py-xl-4{padding-bottom:1.5rem!important;padding-top:1.5rem!important} +.py-xl-5{padding-bottom:3rem!important;padding-top:3rem!important} +.pt-xl-0{padding-top:0!important} +.pt-xl-1{padding-top:.25rem!important} +.pt-xl-2{padding-top:.5rem!important} +.pt-xl-3{padding-top:1rem!important} +.pt-xl-4{padding-top:1.5rem!important} +.pt-xl-5{padding-top:3rem!important} +.pe-xl-0{padding-right:0!important} +.pe-xl-1{padding-right:.25rem!important} +.pe-xl-2{padding-right:.5rem!important} +.pe-xl-3{padding-right:1rem!important} +.pe-xl-4{padding-right:1.5rem!important} +.pe-xl-5{padding-right:3rem!important} +.pb-xl-0{padding-bottom:0!important} +.pb-xl-1{padding-bottom:.25rem!important} +.pb-xl-2{padding-bottom:.5rem!important} +.pb-xl-3{padding-bottom:1rem!important} +.pb-xl-4{padding-bottom:1.5rem!important} +.pb-xl-5{padding-bottom:3rem!important} +.ps-xl-0{padding-left:0!important} +.ps-xl-1{padding-left:.25rem!important} +.ps-xl-2{padding-left:.5rem!important} +.ps-xl-3{padding-left:1rem!important} +.ps-xl-4{padding-left:1.5rem!important} +.ps-xl-5{padding-left:3rem!important} +.gap-xl-0{gap:0!important} +.gap-xl-1{gap:.25rem!important} +.gap-xl-2{gap:.5rem!important} +.gap-xl-3{gap:1rem!important} +.gap-xl-4{gap:1.5rem!important} +.gap-xl-5{gap:3rem!important} +.row-gap-xl-0{row-gap:0!important} +.row-gap-xl-1{row-gap:.25rem!important} +.row-gap-xl-2{row-gap:.5rem!important} +.row-gap-xl-3{row-gap:1rem!important} +.row-gap-xl-4{row-gap:1.5rem!important} +.row-gap-xl-5{row-gap:3rem!important} +.text-xl-start{text-align:start!important} +.text-xl-end{text-align:end!important} +.text-xl-center{text-align:center!important}} +@media (min-width:1400px){ +.d-xxl-inline{display:inline!important} +.d-xxl-inline-block{display:inline-block!important} +.d-xxl-block{display:block!important} +.d-xxl-grid{display:grid!important} +.d-xxl-inline-grid{display:inline-grid!important} +.d-xxl-table{display:table!important} +.d-xxl-table-row{display:table-row!important} +.d-xxl-table-cell{display:table-cell!important} +.d-xxl-flex{display:flex!important} +.d-xxl-inline-flex{display:inline-flex!important} +.d-xxl-none{display:none!important} +.flex-xxl-fill{flex:1 1 auto!important} +.flex-xxl-row{flex-direction:row!important} +.flex-xxl-column{flex-direction:column!important} +.flex-xxl-row-reverse{flex-direction:row-reverse!important} +.flex-xxl-column-reverse{flex-direction:column-reverse!important} +.flex-xxl-grow-0{flex-grow:0!important} +.flex-xxl-grow-1{flex-grow:1!important} +.flex-xxl-shrink-0{flex-shrink:0!important} +.flex-xxl-shrink-1{flex-shrink:1!important} +.flex-xxl-wrap{flex-wrap:wrap!important} +.flex-xxl-nowrap{flex-wrap:nowrap!important} +.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important} +.justify-content-xxl-start{justify-content:flex-start!important} +.justify-content-xxl-end{justify-content:flex-end!important} +.justify-content-xxl-center{justify-content:center!important} +.justify-content-xxl-between{justify-content:space-between!important} +.justify-content-xxl-around{justify-content:space-around!important} +.justify-content-xxl-evenly{justify-content:space-evenly!important} +.align-items-xxl-start{align-items:flex-start!important} +.align-items-xxl-end{align-items:flex-end!important} +.align-items-xxl-center{align-items:center!important} +.align-items-xxl-baseline{align-items:baseline!important} +.align-items-xxl-stretch{align-items:stretch!important} +.align-content-xxl-start{align-content:flex-start!important} +.align-content-xxl-end{align-content:flex-end!important} +.align-content-xxl-center{align-content:center!important} +.align-content-xxl-between{align-content:space-between!important} +.align-content-xxl-around{align-content:space-around!important} +.align-content-xxl-stretch{align-content:stretch!important} +.align-self-xxl-auto{align-self:auto!important} +.align-self-xxl-start{align-self:flex-start!important} +.align-self-xxl-end{align-self:flex-end!important} +.align-self-xxl-center{align-self:center!important} +.align-self-xxl-baseline{align-self:baseline!important} +.align-self-xxl-stretch{align-self:stretch!important} +.m-xxl-0{margin:0!important} +.m-xxl-1{margin:.25rem!important} +.m-xxl-2{margin:.5rem!important} +.m-xxl-3{margin:1rem!important} +.m-xxl-4{margin:1.5rem!important} +.m-xxl-5{margin:3rem!important} +.m-xxl-auto{margin:auto!important} +.mx-xxl-0{margin-left:0!important;margin-right:0!important} +.mx-xxl-1{margin-left:.25rem!important;margin-right:.25rem!important} +.mx-xxl-2{margin-left:.5rem!important;margin-right:.5rem!important} +.mx-xxl-3{margin-left:1rem!important;margin-right:1rem!important} +.mx-xxl-4{margin-left:1.5rem!important;margin-right:1.5rem!important} +.mx-xxl-5{margin-left:3rem!important;margin-right:3rem!important} +.mx-xxl-auto{margin-left:auto!important;margin-right:auto!important} +.my-xxl-0{margin-bottom:0!important;margin-top:0!important} +.my-xxl-1{margin-bottom:.25rem!important;margin-top:.25rem!important} +.my-xxl-2{margin-bottom:.5rem!important;margin-top:.5rem!important} +.my-xxl-3{margin-bottom:1rem!important;margin-top:1rem!important} +.my-xxl-4{margin-bottom:1.5rem!important;margin-top:1.5rem!important} +.my-xxl-5{margin-bottom:3rem!important;margin-top:3rem!important} +.my-xxl-auto{margin-bottom:auto!important;margin-top:auto!important} +.mt-xxl-0{margin-top:0!important} +.mt-xxl-1{margin-top:.25rem!important} +.mt-xxl-2{margin-top:.5rem!important} +.mt-xxl-3{margin-top:1rem!important} +.mt-xxl-4{margin-top:1.5rem!important} +.mt-xxl-5{margin-top:3rem!important} +.mt-xxl-auto{margin-top:auto!important} +.me-xxl-0{margin-right:0!important} +.me-xxl-1{margin-right:.25rem!important} +.me-xxl-2{margin-right:.5rem!important} +.me-xxl-3{margin-right:1rem!important} +.me-xxl-4{margin-right:1.5rem!important} +.me-xxl-5{margin-right:3rem!important} +.me-xxl-auto{margin-right:auto!important} +.mb-xxl-0{margin-bottom:0!important} +.mb-xxl-1{margin-bottom:.25rem!important} +.mb-xxl-2{margin-bottom:.5rem!important} +.mb-xxl-3{margin-bottom:1rem!important} +.mb-xxl-4{margin-bottom:1.5rem!important} +.mb-xxl-5{margin-bottom:3rem!important} +.mb-xxl-auto{margin-bottom:auto!important} +.ms-xxl-0{margin-left:0!important} +.ms-xxl-1{margin-left:.25rem!important} +.ms-xxl-2{margin-left:.5rem!important} +.ms-xxl-3{margin-left:1rem!important} +.ms-xxl-4{margin-left:1.5rem!important} +.ms-xxl-5{margin-left:3rem!important} +.ms-xxl-auto{margin-left:auto!important} +.p-xxl-0{padding:0!important} +.p-xxl-1{padding:.25rem!important} +.p-xxl-2{padding:.5rem!important} +.p-xxl-3{padding:1rem!important} +.p-xxl-4{padding:1.5rem!important} +.p-xxl-5{padding:3rem!important} +.px-xxl-0{padding-left:0!important;padding-right:0!important} +.px-xxl-1{padding-left:.25rem!important;padding-right:.25rem!important} +.px-xxl-2{padding-left:.5rem!important;padding-right:.5rem!important} +.px-xxl-3{padding-left:1rem!important;padding-right:1rem!important} +.px-xxl-4{padding-left:1.5rem!important;padding-right:1.5rem!important} +.px-xxl-5{padding-left:3rem!important;padding-right:3rem!important} +.py-xxl-0{padding-bottom:0!important;padding-top:0!important} +.py-xxl-1{padding-bottom:.25rem!important;padding-top:.25rem!important} +.py-xxl-2{padding-bottom:.5rem!important;padding-top:.5rem!important} +.py-xxl-3{padding-bottom:1rem!important;padding-top:1rem!important} +.py-xxl-4{padding-bottom:1.5rem!important;padding-top:1.5rem!important} +.py-xxl-5{padding-bottom:3rem!important;padding-top:3rem!important} +.pt-xxl-0{padding-top:0!important} +.pt-xxl-1{padding-top:.25rem!important} +.pt-xxl-2{padding-top:.5rem!important} +.pt-xxl-3{padding-top:1rem!important} +.pt-xxl-4{padding-top:1.5rem!important} +.pt-xxl-5{padding-top:3rem!important} +.pe-xxl-0{padding-right:0!important} +.pe-xxl-1{padding-right:.25rem!important} +.pe-xxl-2{padding-right:.5rem!important} +.pe-xxl-3{padding-right:1rem!important} +.pe-xxl-4{padding-right:1.5rem!important} +.pe-xxl-5{padding-right:3rem!important} +.pb-xxl-0{padding-bottom:0!important} +.pb-xxl-1{padding-bottom:.25rem!important} +.pb-xxl-2{padding-bottom:.5rem!important} +.pb-xxl-3{padding-bottom:1rem!important} +.pb-xxl-4{padding-bottom:1.5rem!important} +.pb-xxl-5{padding-bottom:3rem!important} +.ps-xxl-0{padding-left:0!important} +.ps-xxl-1{padding-left:.25rem!important} +.ps-xxl-2{padding-left:.5rem!important} +.ps-xxl-3{padding-left:1rem!important} +.ps-xxl-4{padding-left:1.5rem!important} +.ps-xxl-5{padding-left:3rem!important} +.gap-xxl-0{gap:0!important} +.gap-xxl-1{gap:.25rem!important} +.gap-xxl-2{gap:.5rem!important} +.gap-xxl-3{gap:1rem!important} +.gap-xxl-4{gap:1.5rem!important} +.gap-xxl-5{gap:3rem!important} +.row-gap-xxl-0{row-gap:0!important} +.row-gap-xxl-1{row-gap:.25rem!important} +.row-gap-xxl-2{row-gap:.5rem!important} +.row-gap-xxl-3{row-gap:1rem!important} +.row-gap-xxl-4{row-gap:1.5rem!important} +.row-gap-xxl-5{row-gap:3rem!important} +.text-xxl-start{text-align:start!important} +.text-xxl-end{text-align:end!important} +.text-xxl-center{text-align:center!important}} +@media (min-width:1200px){ +.fs-1{font-size:1.5rem!important}} +@media print{ +.d-print-inline{display:inline!important} +.d-print-inline-block{display:inline-block!important} +.d-print-block{display:block!important} +.d-print-grid{display:grid!important} +.d-print-inline-grid{display:inline-grid!important} +.d-print-table{display:table!important} +.d-print-table-row{display:table-row!important} +.d-print-table-cell{display:table-cell!important} +.d-print-flex{display:flex!important} +.d-print-inline-flex{display:inline-flex!important} +.d-print-none{display:none!important}} +@property --token-color-primary-base{syntax:"<color>";inherits:true;initial-value:#205eb5} +@layer color{ +:root{--typo3-color-neutral-origin:hsl(from #000 h s 50%);--typo3-color-neutral-tint:hsl(from var(--token-color-primary-base,#000) h s 50%);--typo3-color-neutral-mix:8%;--token-color-neutral-base:color-mix(in srgb,var(--typo3-color-neutral-origin),var(--typo3-color-neutral-tint) var(--typo3-color-neutral-mix,0%));--token-color-red-base:#d64545;--token-color-orange-base:#f28522;--token-color-yellow-base:#e0a810;--token-color-lime-base:#7cb518;--token-color-green-base:#2a9960;--token-color-teal-base:#2aa89c;--token-color-blue-base:#3085d6;--token-color-indigo-base:#5563c4;--token-color-purple-base:#7c5ac4;--token-color-magenta-base:#d4458c;--typo3-color-state-harmonize:5%;--token-color-primary-base:#205eb5;--token-color-secondary-base:color-mix(in srgb,#737373,var(--token-color-primary-base) var(--typo3-color-state-harmonize));--token-color-info-base:color-mix(in srgb,#abdced,var(--token-color-primary-base) var(--typo3-color-state-harmonize));--token-color-success-base:color-mix(in srgb,#2a9960,var(--token-color-primary-base) var(--typo3-color-state-harmonize));--token-color-warning-base:color-mix(in srgb,#e0a810,var(--token-color-primary-base) var(--typo3-color-state-harmonize));--token-color-danger-base:color-mix(in srgb,#d64545,var(--token-color-primary-base) var(--typo3-color-state-harmonize));--token-color-notice-base:color-mix(in srgb,#737373,var(--token-color-primary-base) var(--typo3-color-state-harmonize))} +@layer neutral{ +:root{--token-color-neutral-0:hsl(from var(--token-color-neutral-base) h s 100%);--token-color-neutral-1:hsl(from var(--token-color-neutral-base) h s 99%);--token-color-neutral-2:hsl(from var(--token-color-neutral-base) h s 98%);--token-color-neutral-3:hsl(from var(--token-color-neutral-base) h s 97%);--token-color-neutral-4:hsl(from var(--token-color-neutral-base) h s 96%);--token-color-neutral-5:hsl(from var(--token-color-neutral-base) h s 95%);--token-color-neutral-6:hsl(from var(--token-color-neutral-base) h s 94%);--token-color-neutral-7:hsl(from var(--token-color-neutral-base) h s 93%);--token-color-neutral-8:hsl(from var(--token-color-neutral-base) h s 92%);--token-color-neutral-9:hsl(from var(--token-color-neutral-base) h s 91%);--token-color-neutral-10:hsl(from var(--token-color-neutral-base) h s 90%);--token-color-neutral-12:hsl(from var(--token-color-neutral-base) h s 88%);--token-color-neutral-13:hsl(from var(--token-color-neutral-base) h s 87%);--token-color-neutral-15:hsl(from var(--token-color-neutral-base) h s 85%);--token-color-neutral-16:hsl(from var(--token-color-neutral-base) h s 84%);--token-color-neutral-18:hsl(from var(--token-color-neutral-base) h s 82%);--token-color-neutral-20:hsl(from var(--token-color-neutral-base) h s 80%);--token-color-neutral-22:hsl(from var(--token-color-neutral-base) h s 78%);--token-color-neutral-25:hsl(from var(--token-color-neutral-base) h s 75%);--token-color-neutral-28:hsl(from var(--token-color-neutral-base) h s 72%);--token-color-neutral-30:hsl(from var(--token-color-neutral-base) h s 70%);--token-color-neutral-35:hsl(from var(--token-color-neutral-base) h s 65%);--token-color-neutral-40:hsl(from var(--token-color-neutral-base) h s 60%);--token-color-neutral-45:hsl(from var(--token-color-neutral-base) h s 55%);--token-color-neutral-50:hsl(from var(--token-color-neutral-base) h s 50%);--token-color-neutral-55:hsl(from var(--token-color-neutral-base) h s 45%);--token-color-neutral-60:hsl(from var(--token-color-neutral-base) h s 40%);--token-color-neutral-65:hsl(from var(--token-color-neutral-base) h s 35%);--token-color-neutral-70:hsl(from var(--token-color-neutral-base) h s 30%);--token-color-neutral-72:hsl(from var(--token-color-neutral-base) h s 28%);--token-color-neutral-74:hsl(from var(--token-color-neutral-base) h s 26%);--token-color-neutral-75:hsl(from var(--token-color-neutral-base) h s 25%);--token-color-neutral-76:hsl(from var(--token-color-neutral-base) h s 24%);--token-color-neutral-78:hsl(from var(--token-color-neutral-base) h s 22%);--token-color-neutral-80:hsl(from var(--token-color-neutral-base) h s 20%);--token-color-neutral-82:hsl(from var(--token-color-neutral-base) h s 18%);--token-color-neutral-84:hsl(from var(--token-color-neutral-base) h s 16%);--token-color-neutral-85:hsl(from var(--token-color-neutral-base) h s 15%);--token-color-neutral-86:hsl(from var(--token-color-neutral-base) h s 14%);--token-color-neutral-88:hsl(from var(--token-color-neutral-base) h s 12%);--token-color-neutral-90:hsl(from var(--token-color-neutral-base) h s 10%);--token-color-neutral-91:hsl(from var(--token-color-neutral-base) h s 9%);--token-color-neutral-92:hsl(from var(--token-color-neutral-base) h s 8%);--token-color-neutral-93:hsl(from var(--token-color-neutral-base) h s 7%);--token-color-neutral-94:hsl(from var(--token-color-neutral-base) h s 6%);--token-color-neutral-95:hsl(from var(--token-color-neutral-base) h s 5%);--token-color-neutral-96:hsl(from var(--token-color-neutral-base) h s 4%);--token-color-neutral-97:hsl(from var(--token-color-neutral-base) h s 3%);--token-color-neutral-98:hsl(from var(--token-color-neutral-base) h s 2%);--token-color-neutral-99:hsl(from var(--token-color-neutral-base) h s 1%);--token-color-neutral-100:hsl(from var(--token-color-neutral-base) h s 0%)}} +@layer state{ +@layer primary{ +:root{--token-color-primary-1:hsl(from var(--token-color-primary-base) h s 99%);--token-color-primary-2:hsl(from var(--token-color-primary-base) h s 98%);--token-color-primary-3:hsl(from var(--token-color-primary-base) h s 97%);--token-color-primary-4:hsl(from var(--token-color-primary-base) h s 96%);--token-color-primary-5:hsl(from var(--token-color-primary-base) h s 95%);--token-color-primary-10:hsl(from var(--token-color-primary-base) h s 90%);--token-color-primary-15:hsl(from var(--token-color-primary-base) h s 85%);--token-color-primary-20:hsl(from var(--token-color-primary-base) h s 80%);--token-color-primary-25:hsl(from var(--token-color-primary-base) h s 75%);--token-color-primary-30:hsl(from var(--token-color-primary-base) h s 70%);--token-color-primary-35:hsl(from var(--token-color-primary-base) h s 65%);--token-color-primary-40:hsl(from var(--token-color-primary-base) h s 60%);--token-color-primary-45:hsl(from var(--token-color-primary-base) h s 55%);--token-color-primary-50:hsl(from var(--token-color-primary-base) h s 50%);--token-color-primary-55:hsl(from var(--token-color-primary-base) h s 45%);--token-color-primary-60:hsl(from var(--token-color-primary-base) h s 40%);--token-color-primary-65:hsl(from var(--token-color-primary-base) h s 35%);--token-color-primary-70:hsl(from var(--token-color-primary-base) h s 30%);--token-color-primary-75:hsl(from var(--token-color-primary-base) h s 25%);--token-color-primary-80:hsl(from var(--token-color-primary-base) h s 20%);--token-color-primary-85:hsl(from var(--token-color-primary-base) h s 15%);--token-color-primary-90:hsl(from var(--token-color-primary-base) h s 10%);--token-color-primary-95:hsl(from var(--token-color-primary-base) h s 5%);--token-color-primary-96:hsl(from var(--token-color-primary-base) h s 4%);--token-color-primary-97:hsl(from var(--token-color-primary-base) h s 3%);--token-color-primary-98:hsl(from var(--token-color-primary-base) h s 2%);--token-color-primary-99:hsl(from var(--token-color-primary-base) h s 1%)}} +@layer secondary{ +:root{--token-color-secondary-1:hsl(from var(--token-color-secondary-base) h s 99%);--token-color-secondary-2:hsl(from var(--token-color-secondary-base) h s 98%);--token-color-secondary-3:hsl(from var(--token-color-secondary-base) h s 97%);--token-color-secondary-4:hsl(from var(--token-color-secondary-base) h s 96%);--token-color-secondary-5:hsl(from var(--token-color-secondary-base) h s 95%);--token-color-secondary-10:hsl(from var(--token-color-secondary-base) h s 90%);--token-color-secondary-15:hsl(from var(--token-color-secondary-base) h s 85%);--token-color-secondary-20:hsl(from var(--token-color-secondary-base) h s 80%);--token-color-secondary-25:hsl(from var(--token-color-secondary-base) h s 75%);--token-color-secondary-30:hsl(from var(--token-color-secondary-base) h s 70%);--token-color-secondary-35:hsl(from var(--token-color-secondary-base) h s 65%);--token-color-secondary-40:hsl(from var(--token-color-secondary-base) h s 60%);--token-color-secondary-45:hsl(from var(--token-color-secondary-base) h s 55%);--token-color-secondary-50:hsl(from var(--token-color-secondary-base) h s 50%);--token-color-secondary-55:hsl(from var(--token-color-secondary-base) h s 45%);--token-color-secondary-60:hsl(from var(--token-color-secondary-base) h s 40%);--token-color-secondary-65:hsl(from var(--token-color-secondary-base) h s 35%);--token-color-secondary-70:hsl(from var(--token-color-secondary-base) h s 30%);--token-color-secondary-75:hsl(from var(--token-color-secondary-base) h s 25%);--token-color-secondary-80:hsl(from var(--token-color-secondary-base) h s 20%);--token-color-secondary-85:hsl(from var(--token-color-secondary-base) h s 15%);--token-color-secondary-90:hsl(from var(--token-color-secondary-base) h s 10%);--token-color-secondary-95:hsl(from var(--token-color-secondary-base) h s 5%);--token-color-secondary-96:hsl(from var(--token-color-secondary-base) h s 4%);--token-color-secondary-97:hsl(from var(--token-color-secondary-base) h s 3%);--token-color-secondary-98:hsl(from var(--token-color-secondary-base) h s 2%);--token-color-secondary-99:hsl(from var(--token-color-secondary-base) h s 1%)}} +@layer info{ +:root{--token-color-info-1:hsl(from var(--token-color-info-base) h s 99%);--token-color-info-2:hsl(from var(--token-color-info-base) h s 98%);--token-color-info-3:hsl(from var(--token-color-info-base) h s 97%);--token-color-info-4:hsl(from var(--token-color-info-base) h s 96%);--token-color-info-5:hsl(from var(--token-color-info-base) h s 95%);--token-color-info-10:hsl(from var(--token-color-info-base) h s 90%);--token-color-info-15:hsl(from var(--token-color-info-base) h s 85%);--token-color-info-20:hsl(from var(--token-color-info-base) h s 80%);--token-color-info-25:hsl(from var(--token-color-info-base) h s 75%);--token-color-info-30:hsl(from var(--token-color-info-base) h s 70%);--token-color-info-35:hsl(from var(--token-color-info-base) h s 65%);--token-color-info-40:hsl(from var(--token-color-info-base) h s 60%);--token-color-info-45:hsl(from var(--token-color-info-base) h s 55%);--token-color-info-50:hsl(from var(--token-color-info-base) h s 50%);--token-color-info-55:hsl(from var(--token-color-info-base) h s 45%);--token-color-info-60:hsl(from var(--token-color-info-base) h s 40%);--token-color-info-65:hsl(from var(--token-color-info-base) h s 35%);--token-color-info-70:hsl(from var(--token-color-info-base) h s 30%);--token-color-info-75:hsl(from var(--token-color-info-base) h s 25%);--token-color-info-80:hsl(from var(--token-color-info-base) h s 20%);--token-color-info-85:hsl(from var(--token-color-info-base) h s 15%);--token-color-info-90:hsl(from var(--token-color-info-base) h s 10%);--token-color-info-95:hsl(from var(--token-color-info-base) h s 5%);--token-color-info-96:hsl(from var(--token-color-info-base) h s 4%);--token-color-info-97:hsl(from var(--token-color-info-base) h s 3%);--token-color-info-98:hsl(from var(--token-color-info-base) h s 2%);--token-color-info-99:hsl(from var(--token-color-info-base) h s 1%)}} +@layer success{ +:root{--token-color-success-1:hsl(from var(--token-color-success-base) h s 99%);--token-color-success-2:hsl(from var(--token-color-success-base) h s 98%);--token-color-success-3:hsl(from var(--token-color-success-base) h s 97%);--token-color-success-4:hsl(from var(--token-color-success-base) h s 96%);--token-color-success-5:hsl(from var(--token-color-success-base) h s 95%);--token-color-success-10:hsl(from var(--token-color-success-base) h s 90%);--token-color-success-15:hsl(from var(--token-color-success-base) h s 85%);--token-color-success-20:hsl(from var(--token-color-success-base) h s 80%);--token-color-success-25:hsl(from var(--token-color-success-base) h s 75%);--token-color-success-30:hsl(from var(--token-color-success-base) h s 70%);--token-color-success-35:hsl(from var(--token-color-success-base) h s 65%);--token-color-success-40:hsl(from var(--token-color-success-base) h s 60%);--token-color-success-45:hsl(from var(--token-color-success-base) h s 55%);--token-color-success-50:hsl(from var(--token-color-success-base) h s 50%);--token-color-success-55:hsl(from var(--token-color-success-base) h s 45%);--token-color-success-60:hsl(from var(--token-color-success-base) h s 40%);--token-color-success-65:hsl(from var(--token-color-success-base) h s 35%);--token-color-success-70:hsl(from var(--token-color-success-base) h s 30%);--token-color-success-75:hsl(from var(--token-color-success-base) h s 25%);--token-color-success-80:hsl(from var(--token-color-success-base) h s 20%);--token-color-success-85:hsl(from var(--token-color-success-base) h s 15%);--token-color-success-90:hsl(from var(--token-color-success-base) h s 10%);--token-color-success-95:hsl(from var(--token-color-success-base) h s 5%);--token-color-success-96:hsl(from var(--token-color-success-base) h s 4%);--token-color-success-97:hsl(from var(--token-color-success-base) h s 3%);--token-color-success-98:hsl(from var(--token-color-success-base) h s 2%);--token-color-success-99:hsl(from var(--token-color-success-base) h s 1%)}} +@layer warning{ +:root{--token-color-warning-1:hsl(from var(--token-color-warning-base) h s 99%);--token-color-warning-2:hsl(from var(--token-color-warning-base) h s 98%);--token-color-warning-3:hsl(from var(--token-color-warning-base) h s 97%);--token-color-warning-4:hsl(from var(--token-color-warning-base) h s 96%);--token-color-warning-5:hsl(from var(--token-color-warning-base) h s 95%);--token-color-warning-10:hsl(from var(--token-color-warning-base) h s 90%);--token-color-warning-15:hsl(from var(--token-color-warning-base) h s 85%);--token-color-warning-20:hsl(from var(--token-color-warning-base) h s 80%);--token-color-warning-25:hsl(from var(--token-color-warning-base) h s 75%);--token-color-warning-30:hsl(from var(--token-color-warning-base) h s 70%);--token-color-warning-35:hsl(from var(--token-color-warning-base) h s 65%);--token-color-warning-40:hsl(from var(--token-color-warning-base) h s 60%);--token-color-warning-45:hsl(from var(--token-color-warning-base) h s 55%);--token-color-warning-50:hsl(from var(--token-color-warning-base) h s 50%);--token-color-warning-55:hsl(from var(--token-color-warning-base) h s 45%);--token-color-warning-60:hsl(from var(--token-color-warning-base) h s 40%);--token-color-warning-65:hsl(from var(--token-color-warning-base) h s 35%);--token-color-warning-70:hsl(from var(--token-color-warning-base) h s 30%);--token-color-warning-75:hsl(from var(--token-color-warning-base) h s 25%);--token-color-warning-80:hsl(from var(--token-color-warning-base) h s 20%);--token-color-warning-85:hsl(from var(--token-color-warning-base) h s 15%);--token-color-warning-90:hsl(from var(--token-color-warning-base) h s 10%);--token-color-warning-95:hsl(from var(--token-color-warning-base) h s 5%);--token-color-warning-96:hsl(from var(--token-color-warning-base) h s 4%);--token-color-warning-97:hsl(from var(--token-color-warning-base) h s 3%);--token-color-warning-98:hsl(from var(--token-color-warning-base) h s 2%);--token-color-warning-99:hsl(from var(--token-color-warning-base) h s 1%)}} +@layer danger{ +:root{--token-color-danger-1:hsl(from var(--token-color-danger-base) h s 99%);--token-color-danger-2:hsl(from var(--token-color-danger-base) h s 98%);--token-color-danger-3:hsl(from var(--token-color-danger-base) h s 97%);--token-color-danger-4:hsl(from var(--token-color-danger-base) h s 96%);--token-color-danger-5:hsl(from var(--token-color-danger-base) h s 95%);--token-color-danger-10:hsl(from var(--token-color-danger-base) h s 90%);--token-color-danger-15:hsl(from var(--token-color-danger-base) h s 85%);--token-color-danger-20:hsl(from var(--token-color-danger-base) h s 80%);--token-color-danger-25:hsl(from var(--token-color-danger-base) h s 75%);--token-color-danger-30:hsl(from var(--token-color-danger-base) h s 70%);--token-color-danger-35:hsl(from var(--token-color-danger-base) h s 65%);--token-color-danger-40:hsl(from var(--token-color-danger-base) h s 60%);--token-color-danger-45:hsl(from var(--token-color-danger-base) h s 55%);--token-color-danger-50:hsl(from var(--token-color-danger-base) h s 50%);--token-color-danger-55:hsl(from var(--token-color-danger-base) h s 45%);--token-color-danger-60:hsl(from var(--token-color-danger-base) h s 40%);--token-color-danger-65:hsl(from var(--token-color-danger-base) h s 35%);--token-color-danger-70:hsl(from var(--token-color-danger-base) h s 30%);--token-color-danger-75:hsl(from var(--token-color-danger-base) h s 25%);--token-color-danger-80:hsl(from var(--token-color-danger-base) h s 20%);--token-color-danger-85:hsl(from var(--token-color-danger-base) h s 15%);--token-color-danger-90:hsl(from var(--token-color-danger-base) h s 10%);--token-color-danger-95:hsl(from var(--token-color-danger-base) h s 5%);--token-color-danger-96:hsl(from var(--token-color-danger-base) h s 4%);--token-color-danger-97:hsl(from var(--token-color-danger-base) h s 3%);--token-color-danger-98:hsl(from var(--token-color-danger-base) h s 2%);--token-color-danger-99:hsl(from var(--token-color-danger-base) h s 1%)}} +@layer notice{ +:root{--token-color-notice-1:hsl(from var(--token-color-notice-base) h s 99%);--token-color-notice-2:hsl(from var(--token-color-notice-base) h s 98%);--token-color-notice-3:hsl(from var(--token-color-notice-base) h s 97%);--token-color-notice-4:hsl(from var(--token-color-notice-base) h s 96%);--token-color-notice-5:hsl(from var(--token-color-notice-base) h s 95%);--token-color-notice-10:hsl(from var(--token-color-notice-base) h s 90%);--token-color-notice-15:hsl(from var(--token-color-notice-base) h s 85%);--token-color-notice-20:hsl(from var(--token-color-notice-base) h s 80%);--token-color-notice-25:hsl(from var(--token-color-notice-base) h s 75%);--token-color-notice-30:hsl(from var(--token-color-notice-base) h s 70%);--token-color-notice-35:hsl(from var(--token-color-notice-base) h s 65%);--token-color-notice-40:hsl(from var(--token-color-notice-base) h s 60%);--token-color-notice-45:hsl(from var(--token-color-notice-base) h s 55%);--token-color-notice-50:hsl(from var(--token-color-notice-base) h s 50%);--token-color-notice-55:hsl(from var(--token-color-notice-base) h s 45%);--token-color-notice-60:hsl(from var(--token-color-notice-base) h s 40%);--token-color-notice-65:hsl(from var(--token-color-notice-base) h s 35%);--token-color-notice-70:hsl(from var(--token-color-notice-base) h s 30%);--token-color-notice-75:hsl(from var(--token-color-notice-base) h s 25%);--token-color-notice-80:hsl(from var(--token-color-notice-base) h s 20%);--token-color-notice-85:hsl(from var(--token-color-notice-base) h s 15%);--token-color-notice-90:hsl(from var(--token-color-notice-base) h s 10%);--token-color-notice-95:hsl(from var(--token-color-notice-base) h s 5%);--token-color-notice-96:hsl(from var(--token-color-notice-base) h s 4%);--token-color-notice-97:hsl(from var(--token-color-notice-base) h s 3%);--token-color-notice-98:hsl(from var(--token-color-notice-base) h s 2%);--token-color-notice-99:hsl(from var(--token-color-notice-base) h s 1%)}}} +@layer red{ +:root{--token-color-red-1:hsl(from var(--token-color-red-base) h s 99%);--token-color-red-2:hsl(from var(--token-color-red-base) h s 98%);--token-color-red-3:hsl(from var(--token-color-red-base) h s 97%);--token-color-red-4:hsl(from var(--token-color-red-base) h s 96%);--token-color-red-5:hsl(from var(--token-color-red-base) h s 95%);--token-color-red-10:hsl(from var(--token-color-red-base) h s 90%);--token-color-red-15:hsl(from var(--token-color-red-base) h s 85%);--token-color-red-20:hsl(from var(--token-color-red-base) h s 80%);--token-color-red-25:hsl(from var(--token-color-red-base) h s 75%);--token-color-red-30:hsl(from var(--token-color-red-base) h s 70%);--token-color-red-35:hsl(from var(--token-color-red-base) h s 65%);--token-color-red-40:hsl(from var(--token-color-red-base) h s 60%);--token-color-red-45:hsl(from var(--token-color-red-base) h s 55%);--token-color-red-50:hsl(from var(--token-color-red-base) h s 50%);--token-color-red-55:hsl(from var(--token-color-red-base) h s 45%);--token-color-red-60:hsl(from var(--token-color-red-base) h s 40%);--token-color-red-65:hsl(from var(--token-color-red-base) h s 35%);--token-color-red-70:hsl(from var(--token-color-red-base) h s 30%);--token-color-red-75:hsl(from var(--token-color-red-base) h s 25%);--token-color-red-80:hsl(from var(--token-color-red-base) h s 20%);--token-color-red-85:hsl(from var(--token-color-red-base) h s 15%);--token-color-red-90:hsl(from var(--token-color-red-base) h s 10%);--token-color-red-95:hsl(from var(--token-color-red-base) h s 5%);--token-color-red-96:hsl(from var(--token-color-red-base) h s 4%);--token-color-red-97:hsl(from var(--token-color-red-base) h s 3%);--token-color-red-98:hsl(from var(--token-color-red-base) h s 2%);--token-color-red-99:hsl(from var(--token-color-red-base) h s 1%)}} +@layer orange{ +:root{--token-color-orange-1:hsl(from var(--token-color-orange-base) h s 99%);--token-color-orange-2:hsl(from var(--token-color-orange-base) h s 98%);--token-color-orange-3:hsl(from var(--token-color-orange-base) h s 97%);--token-color-orange-4:hsl(from var(--token-color-orange-base) h s 96%);--token-color-orange-5:hsl(from var(--token-color-orange-base) h s 95%);--token-color-orange-10:hsl(from var(--token-color-orange-base) h s 90%);--token-color-orange-15:hsl(from var(--token-color-orange-base) h s 85%);--token-color-orange-20:hsl(from var(--token-color-orange-base) h s 80%);--token-color-orange-25:hsl(from var(--token-color-orange-base) h s 75%);--token-color-orange-30:hsl(from var(--token-color-orange-base) h s 70%);--token-color-orange-35:hsl(from var(--token-color-orange-base) h s 65%);--token-color-orange-40:hsl(from var(--token-color-orange-base) h s 60%);--token-color-orange-45:hsl(from var(--token-color-orange-base) h s 55%);--token-color-orange-50:hsl(from var(--token-color-orange-base) h s 50%);--token-color-orange-55:hsl(from var(--token-color-orange-base) h s 45%);--token-color-orange-60:hsl(from var(--token-color-orange-base) h s 40%);--token-color-orange-65:hsl(from var(--token-color-orange-base) h s 35%);--token-color-orange-70:hsl(from var(--token-color-orange-base) h s 30%);--token-color-orange-75:hsl(from var(--token-color-orange-base) h s 25%);--token-color-orange-80:hsl(from var(--token-color-orange-base) h s 20%);--token-color-orange-85:hsl(from var(--token-color-orange-base) h s 15%);--token-color-orange-90:hsl(from var(--token-color-orange-base) h s 10%);--token-color-orange-95:hsl(from var(--token-color-orange-base) h s 5%);--token-color-orange-96:hsl(from var(--token-color-orange-base) h s 4%);--token-color-orange-97:hsl(from var(--token-color-orange-base) h s 3%);--token-color-orange-98:hsl(from var(--token-color-orange-base) h s 2%);--token-color-orange-99:hsl(from var(--token-color-orange-base) h s 1%)}} +@layer yellow{ +:root{--token-color-yellow-1:hsl(from var(--token-color-yellow-base) h s 99%);--token-color-yellow-2:hsl(from var(--token-color-yellow-base) h s 98%);--token-color-yellow-3:hsl(from var(--token-color-yellow-base) h s 97%);--token-color-yellow-4:hsl(from var(--token-color-yellow-base) h s 96%);--token-color-yellow-5:hsl(from var(--token-color-yellow-base) h s 95%);--token-color-yellow-10:hsl(from var(--token-color-yellow-base) h s 90%);--token-color-yellow-15:hsl(from var(--token-color-yellow-base) h s 85%);--token-color-yellow-20:hsl(from var(--token-color-yellow-base) h s 80%);--token-color-yellow-25:hsl(from var(--token-color-yellow-base) h s 75%);--token-color-yellow-30:hsl(from var(--token-color-yellow-base) h s 70%);--token-color-yellow-35:hsl(from var(--token-color-yellow-base) h s 65%);--token-color-yellow-40:hsl(from var(--token-color-yellow-base) h s 60%);--token-color-yellow-45:hsl(from var(--token-color-yellow-base) h s 55%);--token-color-yellow-50:hsl(from var(--token-color-yellow-base) h s 50%);--token-color-yellow-55:hsl(from var(--token-color-yellow-base) h s 45%);--token-color-yellow-60:hsl(from var(--token-color-yellow-base) h s 40%);--token-color-yellow-65:hsl(from var(--token-color-yellow-base) h s 35%);--token-color-yellow-70:hsl(from var(--token-color-yellow-base) h s 30%);--token-color-yellow-75:hsl(from var(--token-color-yellow-base) h s 25%);--token-color-yellow-80:hsl(from var(--token-color-yellow-base) h s 20%);--token-color-yellow-85:hsl(from var(--token-color-yellow-base) h s 15%);--token-color-yellow-90:hsl(from var(--token-color-yellow-base) h s 10%);--token-color-yellow-95:hsl(from var(--token-color-yellow-base) h s 5%);--token-color-yellow-96:hsl(from var(--token-color-yellow-base) h s 4%);--token-color-yellow-97:hsl(from var(--token-color-yellow-base) h s 3%);--token-color-yellow-98:hsl(from var(--token-color-yellow-base) h s 2%);--token-color-yellow-99:hsl(from var(--token-color-yellow-base) h s 1%)}} +@layer lime{ +:root{--token-color-lime-1:hsl(from var(--token-color-lime-base) h s 99%);--token-color-lime-2:hsl(from var(--token-color-lime-base) h s 98%);--token-color-lime-3:hsl(from var(--token-color-lime-base) h s 97%);--token-color-lime-4:hsl(from var(--token-color-lime-base) h s 96%);--token-color-lime-5:hsl(from var(--token-color-lime-base) h s 95%);--token-color-lime-10:hsl(from var(--token-color-lime-base) h s 90%);--token-color-lime-15:hsl(from var(--token-color-lime-base) h s 85%);--token-color-lime-20:hsl(from var(--token-color-lime-base) h s 80%);--token-color-lime-25:hsl(from var(--token-color-lime-base) h s 75%);--token-color-lime-30:hsl(from var(--token-color-lime-base) h s 70%);--token-color-lime-35:hsl(from var(--token-color-lime-base) h s 65%);--token-color-lime-40:hsl(from var(--token-color-lime-base) h s 60%);--token-color-lime-45:hsl(from var(--token-color-lime-base) h s 55%);--token-color-lime-50:hsl(from var(--token-color-lime-base) h s 50%);--token-color-lime-55:hsl(from var(--token-color-lime-base) h s 45%);--token-color-lime-60:hsl(from var(--token-color-lime-base) h s 40%);--token-color-lime-65:hsl(from var(--token-color-lime-base) h s 35%);--token-color-lime-70:hsl(from var(--token-color-lime-base) h s 30%);--token-color-lime-75:hsl(from var(--token-color-lime-base) h s 25%);--token-color-lime-80:hsl(from var(--token-color-lime-base) h s 20%);--token-color-lime-85:hsl(from var(--token-color-lime-base) h s 15%);--token-color-lime-90:hsl(from var(--token-color-lime-base) h s 10%);--token-color-lime-95:hsl(from var(--token-color-lime-base) h s 5%);--token-color-lime-96:hsl(from var(--token-color-lime-base) h s 4%);--token-color-lime-97:hsl(from var(--token-color-lime-base) h s 3%);--token-color-lime-98:hsl(from var(--token-color-lime-base) h s 2%);--token-color-lime-99:hsl(from var(--token-color-lime-base) h s 1%)}} +@layer green{ +:root{--token-color-green-1:hsl(from var(--token-color-green-base) h s 99%);--token-color-green-2:hsl(from var(--token-color-green-base) h s 98%);--token-color-green-3:hsl(from var(--token-color-green-base) h s 97%);--token-color-green-4:hsl(from var(--token-color-green-base) h s 96%);--token-color-green-5:hsl(from var(--token-color-green-base) h s 95%);--token-color-green-10:hsl(from var(--token-color-green-base) h s 90%);--token-color-green-15:hsl(from var(--token-color-green-base) h s 85%);--token-color-green-20:hsl(from var(--token-color-green-base) h s 80%);--token-color-green-25:hsl(from var(--token-color-green-base) h s 75%);--token-color-green-30:hsl(from var(--token-color-green-base) h s 70%);--token-color-green-35:hsl(from var(--token-color-green-base) h s 65%);--token-color-green-40:hsl(from var(--token-color-green-base) h s 60%);--token-color-green-45:hsl(from var(--token-color-green-base) h s 55%);--token-color-green-50:hsl(from var(--token-color-green-base) h s 50%);--token-color-green-55:hsl(from var(--token-color-green-base) h s 45%);--token-color-green-60:hsl(from var(--token-color-green-base) h s 40%);--token-color-green-65:hsl(from var(--token-color-green-base) h s 35%);--token-color-green-70:hsl(from var(--token-color-green-base) h s 30%);--token-color-green-75:hsl(from var(--token-color-green-base) h s 25%);--token-color-green-80:hsl(from var(--token-color-green-base) h s 20%);--token-color-green-85:hsl(from var(--token-color-green-base) h s 15%);--token-color-green-90:hsl(from var(--token-color-green-base) h s 10%);--token-color-green-95:hsl(from var(--token-color-green-base) h s 5%);--token-color-green-96:hsl(from var(--token-color-green-base) h s 4%);--token-color-green-97:hsl(from var(--token-color-green-base) h s 3%);--token-color-green-98:hsl(from var(--token-color-green-base) h s 2%);--token-color-green-99:hsl(from var(--token-color-green-base) h s 1%)}} +@layer teal{ +:root{--token-color-teal-1:hsl(from var(--token-color-teal-base) h s 99%);--token-color-teal-2:hsl(from var(--token-color-teal-base) h s 98%);--token-color-teal-3:hsl(from var(--token-color-teal-base) h s 97%);--token-color-teal-4:hsl(from var(--token-color-teal-base) h s 96%);--token-color-teal-5:hsl(from var(--token-color-teal-base) h s 95%);--token-color-teal-10:hsl(from var(--token-color-teal-base) h s 90%);--token-color-teal-15:hsl(from var(--token-color-teal-base) h s 85%);--token-color-teal-20:hsl(from var(--token-color-teal-base) h s 80%);--token-color-teal-25:hsl(from var(--token-color-teal-base) h s 75%);--token-color-teal-30:hsl(from var(--token-color-teal-base) h s 70%);--token-color-teal-35:hsl(from var(--token-color-teal-base) h s 65%);--token-color-teal-40:hsl(from var(--token-color-teal-base) h s 60%);--token-color-teal-45:hsl(from var(--token-color-teal-base) h s 55%);--token-color-teal-50:hsl(from var(--token-color-teal-base) h s 50%);--token-color-teal-55:hsl(from var(--token-color-teal-base) h s 45%);--token-color-teal-60:hsl(from var(--token-color-teal-base) h s 40%);--token-color-teal-65:hsl(from var(--token-color-teal-base) h s 35%);--token-color-teal-70:hsl(from var(--token-color-teal-base) h s 30%);--token-color-teal-75:hsl(from var(--token-color-teal-base) h s 25%);--token-color-teal-80:hsl(from var(--token-color-teal-base) h s 20%);--token-color-teal-85:hsl(from var(--token-color-teal-base) h s 15%);--token-color-teal-90:hsl(from var(--token-color-teal-base) h s 10%);--token-color-teal-95:hsl(from var(--token-color-teal-base) h s 5%);--token-color-teal-96:hsl(from var(--token-color-teal-base) h s 4%);--token-color-teal-97:hsl(from var(--token-color-teal-base) h s 3%);--token-color-teal-98:hsl(from var(--token-color-teal-base) h s 2%);--token-color-teal-99:hsl(from var(--token-color-teal-base) h s 1%)}} +@layer blue{ +:root{--token-color-blue-1:hsl(from var(--token-color-blue-base) h s 99%);--token-color-blue-2:hsl(from var(--token-color-blue-base) h s 98%);--token-color-blue-3:hsl(from var(--token-color-blue-base) h s 97%);--token-color-blue-4:hsl(from var(--token-color-blue-base) h s 96%);--token-color-blue-5:hsl(from var(--token-color-blue-base) h s 95%);--token-color-blue-10:hsl(from var(--token-color-blue-base) h s 90%);--token-color-blue-15:hsl(from var(--token-color-blue-base) h s 85%);--token-color-blue-20:hsl(from var(--token-color-blue-base) h s 80%);--token-color-blue-25:hsl(from var(--token-color-blue-base) h s 75%);--token-color-blue-30:hsl(from var(--token-color-blue-base) h s 70%);--token-color-blue-35:hsl(from var(--token-color-blue-base) h s 65%);--token-color-blue-40:hsl(from var(--token-color-blue-base) h s 60%);--token-color-blue-45:hsl(from var(--token-color-blue-base) h s 55%);--token-color-blue-50:hsl(from var(--token-color-blue-base) h s 50%);--token-color-blue-55:hsl(from var(--token-color-blue-base) h s 45%);--token-color-blue-60:hsl(from var(--token-color-blue-base) h s 40%);--token-color-blue-65:hsl(from var(--token-color-blue-base) h s 35%);--token-color-blue-70:hsl(from var(--token-color-blue-base) h s 30%);--token-color-blue-75:hsl(from var(--token-color-blue-base) h s 25%);--token-color-blue-80:hsl(from var(--token-color-blue-base) h s 20%);--token-color-blue-85:hsl(from var(--token-color-blue-base) h s 15%);--token-color-blue-90:hsl(from var(--token-color-blue-base) h s 10%);--token-color-blue-95:hsl(from var(--token-color-blue-base) h s 5%);--token-color-blue-96:hsl(from var(--token-color-blue-base) h s 4%);--token-color-blue-97:hsl(from var(--token-color-blue-base) h s 3%);--token-color-blue-98:hsl(from var(--token-color-blue-base) h s 2%);--token-color-blue-99:hsl(from var(--token-color-blue-base) h s 1%)}} +@layer indigo{ +:root{--token-color-indigo-1:hsl(from var(--token-color-indigo-base) h s 99%);--token-color-indigo-2:hsl(from var(--token-color-indigo-base) h s 98%);--token-color-indigo-3:hsl(from var(--token-color-indigo-base) h s 97%);--token-color-indigo-4:hsl(from var(--token-color-indigo-base) h s 96%);--token-color-indigo-5:hsl(from var(--token-color-indigo-base) h s 95%);--token-color-indigo-10:hsl(from var(--token-color-indigo-base) h s 90%);--token-color-indigo-15:hsl(from var(--token-color-indigo-base) h s 85%);--token-color-indigo-20:hsl(from var(--token-color-indigo-base) h s 80%);--token-color-indigo-25:hsl(from var(--token-color-indigo-base) h s 75%);--token-color-indigo-30:hsl(from var(--token-color-indigo-base) h s 70%);--token-color-indigo-35:hsl(from var(--token-color-indigo-base) h s 65%);--token-color-indigo-40:hsl(from var(--token-color-indigo-base) h s 60%);--token-color-indigo-45:hsl(from var(--token-color-indigo-base) h s 55%);--token-color-indigo-50:hsl(from var(--token-color-indigo-base) h s 50%);--token-color-indigo-55:hsl(from var(--token-color-indigo-base) h s 45%);--token-color-indigo-60:hsl(from var(--token-color-indigo-base) h s 40%);--token-color-indigo-65:hsl(from var(--token-color-indigo-base) h s 35%);--token-color-indigo-70:hsl(from var(--token-color-indigo-base) h s 30%);--token-color-indigo-75:hsl(from var(--token-color-indigo-base) h s 25%);--token-color-indigo-80:hsl(from var(--token-color-indigo-base) h s 20%);--token-color-indigo-85:hsl(from var(--token-color-indigo-base) h s 15%);--token-color-indigo-90:hsl(from var(--token-color-indigo-base) h s 10%);--token-color-indigo-95:hsl(from var(--token-color-indigo-base) h s 5%);--token-color-indigo-96:hsl(from var(--token-color-indigo-base) h s 4%);--token-color-indigo-97:hsl(from var(--token-color-indigo-base) h s 3%);--token-color-indigo-98:hsl(from var(--token-color-indigo-base) h s 2%);--token-color-indigo-99:hsl(from var(--token-color-indigo-base) h s 1%)}} +@layer purple{ +:root{--token-color-purple-1:hsl(from var(--token-color-purple-base) h s 99%);--token-color-purple-2:hsl(from var(--token-color-purple-base) h s 98%);--token-color-purple-3:hsl(from var(--token-color-purple-base) h s 97%);--token-color-purple-4:hsl(from var(--token-color-purple-base) h s 96%);--token-color-purple-5:hsl(from var(--token-color-purple-base) h s 95%);--token-color-purple-10:hsl(from var(--token-color-purple-base) h s 90%);--token-color-purple-15:hsl(from var(--token-color-purple-base) h s 85%);--token-color-purple-20:hsl(from var(--token-color-purple-base) h s 80%);--token-color-purple-25:hsl(from var(--token-color-purple-base) h s 75%);--token-color-purple-30:hsl(from var(--token-color-purple-base) h s 70%);--token-color-purple-35:hsl(from var(--token-color-purple-base) h s 65%);--token-color-purple-40:hsl(from var(--token-color-purple-base) h s 60%);--token-color-purple-45:hsl(from var(--token-color-purple-base) h s 55%);--token-color-purple-50:hsl(from var(--token-color-purple-base) h s 50%);--token-color-purple-55:hsl(from var(--token-color-purple-base) h s 45%);--token-color-purple-60:hsl(from var(--token-color-purple-base) h s 40%);--token-color-purple-65:hsl(from var(--token-color-purple-base) h s 35%);--token-color-purple-70:hsl(from var(--token-color-purple-base) h s 30%);--token-color-purple-75:hsl(from var(--token-color-purple-base) h s 25%);--token-color-purple-80:hsl(from var(--token-color-purple-base) h s 20%);--token-color-purple-85:hsl(from var(--token-color-purple-base) h s 15%);--token-color-purple-90:hsl(from var(--token-color-purple-base) h s 10%);--token-color-purple-95:hsl(from var(--token-color-purple-base) h s 5%);--token-color-purple-96:hsl(from var(--token-color-purple-base) h s 4%);--token-color-purple-97:hsl(from var(--token-color-purple-base) h s 3%);--token-color-purple-98:hsl(from var(--token-color-purple-base) h s 2%);--token-color-purple-99:hsl(from var(--token-color-purple-base) h s 1%)}} +@layer magenta{ +:root{--token-color-magenta-1:hsl(from var(--token-color-magenta-base) h s 99%);--token-color-magenta-2:hsl(from var(--token-color-magenta-base) h s 98%);--token-color-magenta-3:hsl(from var(--token-color-magenta-base) h s 97%);--token-color-magenta-4:hsl(from var(--token-color-magenta-base) h s 96%);--token-color-magenta-5:hsl(from var(--token-color-magenta-base) h s 95%);--token-color-magenta-10:hsl(from var(--token-color-magenta-base) h s 90%);--token-color-magenta-15:hsl(from var(--token-color-magenta-base) h s 85%);--token-color-magenta-20:hsl(from var(--token-color-magenta-base) h s 80%);--token-color-magenta-25:hsl(from var(--token-color-magenta-base) h s 75%);--token-color-magenta-30:hsl(from var(--token-color-magenta-base) h s 70%);--token-color-magenta-35:hsl(from var(--token-color-magenta-base) h s 65%);--token-color-magenta-40:hsl(from var(--token-color-magenta-base) h s 60%);--token-color-magenta-45:hsl(from var(--token-color-magenta-base) h s 55%);--token-color-magenta-50:hsl(from var(--token-color-magenta-base) h s 50%);--token-color-magenta-55:hsl(from var(--token-color-magenta-base) h s 45%);--token-color-magenta-60:hsl(from var(--token-color-magenta-base) h s 40%);--token-color-magenta-65:hsl(from var(--token-color-magenta-base) h s 35%);--token-color-magenta-70:hsl(from var(--token-color-magenta-base) h s 30%);--token-color-magenta-75:hsl(from var(--token-color-magenta-base) h s 25%);--token-color-magenta-80:hsl(from var(--token-color-magenta-base) h s 20%);--token-color-magenta-85:hsl(from var(--token-color-magenta-base) h s 15%);--token-color-magenta-90:hsl(from var(--token-color-magenta-base) h s 10%);--token-color-magenta-95:hsl(from var(--token-color-magenta-base) h s 5%);--token-color-magenta-96:hsl(from var(--token-color-magenta-base) h s 4%);--token-color-magenta-97:hsl(from var(--token-color-magenta-base) h s 3%);--token-color-magenta-98:hsl(from var(--token-color-magenta-base) h s 2%);--token-color-magenta-99:hsl(from var(--token-color-magenta-base) h s 1%)}}} +:root{color-scheme:light dark;container-type:inline-size;font-size:max(1em,14px);min-height:100dvh;overscroll-behavior:none;scroll-behavior:smooth;--typo3-font-size:.75rem;--typo3-font-size-small:.6875rem;--typo3-font-family-sans-serif:Verdana,Arial,Helvetica,sans-serif;--typo3-font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;--typo3-font-family:var(--typo3-font-family-sans-serif);--typo3-font-family-code:var(--typo3-font-family-monospace);--typo3-line-height:1.5;--typo3-spacing:1rem;--typo3-header-font-family:"Open Sans Variable",sans-serif;--typo3-zindex-dropdown:1000;--typo3-zindex-modal-backdrop:1050;--typo3-zindex-modal:1055;--typo3-zindex-header:990;--typo3-text-color-base:light-dark(var(--token-color-neutral-90),var(--token-color-neutral-10));--typo3-text-color-link:var(--typo3-text-color-base);--typo3-text-color-variant:color-mix(in srgb,currentColor,transparent 35%);--typo3-text-color-primary:light-dark(var(--token-color-primary-60),var(--token-color-primary-30));--typo3-text-color-secondary:light-dark(var(--token-color-secondary-70),var(--token-color-secondary-35));--typo3-text-color-info:light-dark(var(--token-color-info-70),var(--token-color-info-40));--typo3-text-color-success:light-dark(var(--token-color-success-70),var(--token-color-success-50));--typo3-text-color-warning:light-dark(var(--token-color-warning-80),var(--token-color-warning-40));--typo3-text-color-danger:light-dark(var(--token-color-danger-60),var(--token-color-danger-40));--typo3-text-color-code:light-dark(var(--token-color-magenta-60),var(--token-color-magenta-35));--typo3-text-color-notice:light-dark(var(--token-color-notice-75),var(--token-color-notice-40));--typo3-text-color-default:var(--typo3-text-color-base);--typo3-status-indicator-primary-color:light-dark(var(--token-color-primary-60),var(--token-color-primary-30));--typo3-status-indicator-secondary-color:light-dark(var(--token-color-secondary-70),var(--token-color-secondary-35));--typo3-status-indicator-info-color:light-dark(var(--token-color-info-60),var(--token-color-info-40));--typo3-status-indicator-success-color:light-dark(var(--token-color-success-60),var(--token-color-success-50));--typo3-status-indicator-warning-color:light-dark(var(--token-color-warning-50),var(--token-color-warning-40));--typo3-status-indicator-danger-color:light-dark(var(--token-color-danger-60),var(--token-color-danger-40));--typo3-status-indicator-notice-color:light-dark(var(--token-color-notice-65),var(--token-color-notice-40));--typo3-status-indicator-default-color:currentColor;--typo3-surface-dim:light-dark(var(--token-color-neutral-13),var(--token-color-neutral-92));--typo3-surface-base:light-dark(var(--token-color-neutral-7),var(--token-color-neutral-84));--typo3-surface-bright:light-dark(var(--token-color-neutral-1),var(--token-color-neutral-76));--typo3-surface-container-lowest:light-dark(var(--token-color-neutral-1),var(--token-color-neutral-92));--typo3-surface-container-low:light-dark(var(--token-color-neutral-4),var(--token-color-neutral-88));--typo3-surface-container-base:light-dark(var(--token-color-neutral-7),var(--token-color-neutral-84));--typo3-surface-container-high:light-dark(var(--token-color-neutral-10),var(--token-color-neutral-80));--typo3-surface-container-highest:light-dark(var(--token-color-neutral-13),var(--token-color-neutral-76));--typo3-surface-primary:light-dark(var(--token-color-primary-60),var(--token-color-primary-70));--typo3-surface-primary-text:light-dark(var(--token-color-primary-1),var(--token-color-primary-1));--typo3-surface-container-primary:light-dark(var(--token-color-primary-10),var(--token-color-primary-90));--typo3-surface-container-primary-text:light-dark(var(--token-color-primary-90),var(--token-color-primary-10));--typo3-surface-secondary:light-dark(var(--token-color-secondary-70),var(--token-color-secondary-70));--typo3-surface-secondary-text:light-dark(var(--token-color-secondary-1),var(--token-color-secondary-1));--typo3-surface-container-secondary:light-dark(var(--token-color-secondary-10),var(--token-color-secondary-90));--typo3-surface-container-secondary-text:light-dark(var(--token-color-secondary-90),var(--token-color-secondary-10));--typo3-surface-info:light-dark(var(--token-color-info-20),var(--token-color-info-70));--typo3-surface-info-text:light-dark(var(--token-color-info-90),var(--token-color-info-1));--typo3-surface-container-info:light-dark(var(--token-color-info-10),var(--token-color-info-90));--typo3-surface-container-info-text:light-dark(var(--token-color-info-90),var(--token-color-info-10));--typo3-surface-success:light-dark(var(--token-color-success-70),var(--token-color-success-80));--typo3-surface-success-text:light-dark(var(--token-color-success-1),var(--token-color-success-1));--typo3-surface-container-success:light-dark(var(--token-color-success-10),var(--token-color-success-90));--typo3-surface-container-success-text:light-dark(var(--token-color-success-90),var(--token-color-success-10));--typo3-surface-warning:light-dark(var(--token-color-warning-40),var(--token-color-warning-80));--typo3-surface-warning-text:light-dark(var(--token-color-warning-90),var(--token-color-warning-1));--typo3-surface-container-warning:light-dark(var(--token-color-warning-10),var(--token-color-warning-90));--typo3-surface-container-warning-text:light-dark(var(--token-color-warning-90),var(--token-color-warning-10));--typo3-surface-danger:light-dark(var(--token-color-danger-50),var(--token-color-danger-70));--typo3-surface-danger-text:light-dark(var(--token-color-danger-1),var(--token-color-danger-1));--typo3-surface-container-danger:light-dark(var(--token-color-danger-10),var(--token-color-danger-90));--typo3-surface-container-danger-text:light-dark(var(--token-color-danger-90),var(--token-color-danger-10));--typo3-surface-notice:light-dark(var(--token-color-notice-75),var(--token-color-notice-85));--typo3-surface-notice-text:light-dark(var(--token-color-notice-1),var(--token-color-notice-1));--typo3-surface-container-notice:light-dark(var(--token-color-notice-10),var(--token-color-notice-90));--typo3-surface-container-notice-text:light-dark(var(--token-color-notice-95),var(--token-color-notice-1));--typo3-surface-default:light-dark(var(--token-color-neutral-6),var(--token-color-neutral-93));--typo3-surface-default-text:light-dark(var(--token-color-neutral-90),var(--token-color-neutral-15));--typo3-surface-container-default:light-dark(var(--token-color-neutral-6),var(--token-color-neutral-93));--typo3-surface-container-default-text:light-dark(var(--token-color-neutral-90),var(--token-color-neutral-15));--typo3-border-mix:17.5%;--typo3-outline-width:.25rem;--typo3-outline-style:solid;--typo3-outline-transparent-mix:25%;--typo3-overlay-bg:#000;--typo3-overlay-opacity:.75;--typo3-transition-color:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,opacity .15s ease-in-out;--typo3-state-default-color:light-dark(var(--token-color-neutral-90),var(--token-color-neutral-15));--typo3-state-default-bg:light-dark(var(--token-color-neutral-3),var(--token-color-neutral-85));--typo3-state-default-border-color:light-dark(var(--token-color-neutral-30),var(--token-color-neutral-70));--typo3-state-default-hover-color:var(--typo3-state-default-color);--typo3-state-default-hover-bg:light-dark(var(--token-color-neutral-10),var(--token-color-neutral-80));--typo3-state-default-hover-border-color:light-dark(var(--token-color-neutral-35),var(--token-color-neutral-70));--typo3-state-default-focus-color:var(--typo3-state-default-color);--typo3-state-default-focus-bg:light-dark(var(--token-color-neutral-15),var(--token-color-neutral-75));--typo3-state-default-focus-border-color:light-dark(var(--token-color-neutral-40),var(--token-color-neutral-65));--typo3-state-default-disabled-color:var(--typo3-state-default-color);--typo3-state-default-disabled-bg:var(--typo3-state-default-bg);--typo3-state-default-disabled-border-color:var(--typo3-state-default-border-color);--typo3-state-primary-color:light-dark(var(--token-color-primary-1),var(--token-color-primary-1));--typo3-state-primary-bg:light-dark(var(--token-color-primary-60),var(--token-color-primary-70));--typo3-state-primary-border-color:light-dark(var(--token-color-primary-65),var(--token-color-primary-65));--typo3-state-primary-hover-color:var(--typo3-state-primary-color);--typo3-state-primary-hover-bg:light-dark(var(--token-color-primary-65),var(--token-color-primary-65));--typo3-state-primary-hover-border-color:light-dark(var(--token-color-primary-70),var(--token-color-primary-60));--typo3-state-primary-focus-color:var(--typo3-state-primary-color);--typo3-state-primary-focus-bg:light-dark(var(--token-color-primary-70),var(--token-color-primary-60));--typo3-state-primary-focus-border-color:light-dark(var(--token-color-primary-75),var(--token-color-primary-55));--typo3-state-primary-disabled-color:var(--typo3-state-primary-color);--typo3-state-primary-disabled-bg:var(--typo3-state-primary-bg);--typo3-state-primary-disabled-border-color:var(--typo3-state-primary-border-color);--typo3-state-secondary-color:light-dark(var(--token-color-secondary-1),var(--token-color-secondary-1));--typo3-state-secondary-bg:light-dark(var(--token-color-secondary-70),var(--token-color-secondary-70));--typo3-state-secondary-border-color:light-dark(var(--token-color-secondary-75),var(--token-color-secondary-65));--typo3-state-secondary-hover-color:var(--typo3-state-secondary-color);--typo3-state-secondary-hover-bg:light-dark(var(--token-color-secondary-75),var(--token-color-secondary-65));--typo3-state-secondary-hover-border-color:light-dark(var(--token-color-secondary-80),var(--token-color-secondary-60));--typo3-state-secondary-focus-color:var(--typo3-state-secondary-color);--typo3-state-secondary-focus-bg:light-dark(var(--token-color-secondary-80),var(--token-color-secondary-60));--typo3-state-secondary-focus-border-color:light-dark(var(--token-color-secondary-85),var(--token-color-secondary-55));--typo3-state-secondary-disabled-color:var(--typo3-state-secondary-color);--typo3-state-secondary-disabled-bg:var(--typo3-state-secondary-bg);--typo3-state-secondary-disabled-border-color:var(--typo3-state-secondary-border-color);--typo3-state-success-color:light-dark(var(--token-color-success-1),var(--token-color-success-1));--typo3-state-success-bg:light-dark(var(--token-color-success-70),var(--token-color-success-80));--typo3-state-success-border-color:light-dark(var(--token-color-success-80),var(--token-color-success-70));--typo3-state-success-hover-color:var(--typo3-state-success-color);--typo3-state-success-hover-bg:light-dark(var(--token-color-success-75),var(--token-color-success-75));--typo3-state-success-hover-border-color:light-dark(var(--token-color-success-85),var(--token-color-success-65));--typo3-state-success-focus-color:var(--typo3-state-success-color);--typo3-state-success-focus-bg:light-dark(var(--token-color-success-80),var(--token-color-success-70));--typo3-state-success-focus-border-color:light-dark(var(--token-color-success-90),var(--token-color-success-60));--typo3-state-success-disabled-color:var(--typo3-state-success-color);--typo3-state-success-disabled-bg:var(--typo3-state-success-bg);--typo3-state-success-disabled-border-color:var(--typo3-state-success-border-color);--typo3-state-warning-color:light-dark(var(--token-color-warning-90),var(--token-color-warning-1));--typo3-state-warning-bg:light-dark(var(--token-color-warning-35),var(--token-color-warning-80));--typo3-state-warning-border-color:light-dark(var(--token-color-warning-45),var(--token-color-warning-70));--typo3-state-warning-hover-color:var(--typo3-state-warning-color);--typo3-state-warning-hover-bg:light-dark(var(--token-color-warning-40),var(--token-color-warning-75));--typo3-state-warning-hover-border-color:light-dark(var(--token-color-warning-50),var(--token-color-warning-65));--typo3-state-warning-focus-color:var(--typo3-state-warning-color);--typo3-state-warning-focus-bg:light-dark(var(--token-color-warning-45),var(--token-color-warning-70));--typo3-state-warning-focus-border-color:light-dark(var(--token-color-warning-55),var(--token-color-warning-60));--typo3-state-warning-disabled-color:var(--typo3-state-warning-color);--typo3-state-warning-disabled-bg:var(--typo3-state-warning-bg);--typo3-state-warning-disabled-border-color:var(--typo3-state-warning-border-color);--typo3-state-danger-color:light-dark(var(--token-color-danger-1),var(--token-color-danger-1));--typo3-state-danger-bg:light-dark(var(--token-color-danger-55),var(--token-color-danger-70));--typo3-state-danger-border-color:light-dark(var(--token-color-danger-65),var(--token-color-danger-60));--typo3-state-danger-hover-color:var(--typo3-state-danger-color);--typo3-state-danger-hover-bg:light-dark(var(--token-color-danger-60),var(--token-color-danger-65));--typo3-state-danger-hover-border-color:light-dark(var(--token-color-danger-70),var(--token-color-danger-55));--typo3-state-danger-focus-color:var(--typo3-state-danger-color);--typo3-state-danger-focus-bg:light-dark(var(--token-color-danger-65),var(--token-color-danger-60));--typo3-state-danger-focus-border-color:light-dark(var(--token-color-danger-75),var(--token-color-danger-50));--typo3-state-danger-disabled-color:var(--typo3-state-danger-color);--typo3-state-danger-disabled-bg:var(--typo3-state-danger-bg);--typo3-state-danger-disabled-border-color:var(--typo3-state-danger-border-color);--typo3-state-info-color:light-dark(var(--token-color-info-90),var(--token-color-info-1));--typo3-state-info-bg:light-dark(var(--token-color-info-20),var(--token-color-info-85));--typo3-state-info-border-color:light-dark(var(--token-color-info-30),var(--token-color-info-75));--typo3-state-info-hover-color:var(--typo3-state-info-color);--typo3-state-info-hover-bg:light-dark(var(--token-color-info-25),var(--token-color-info-80));--typo3-state-info-hover-border-color:light-dark(var(--token-color-info-35),var(--token-color-info-70));--typo3-state-info-focus-color:var(--typo3-state-info-color);--typo3-state-info-focus-bg:light-dark(var(--token-color-info-30),var(--token-color-info-75));--typo3-state-info-focus-border-color:light-dark(var(--token-color-info-40),var(--token-color-info-65));--typo3-state-info-disabled-color:var(--typo3-state-info-color);--typo3-state-info-disabled-bg:var(--typo3-state-info-bg);--typo3-state-info-disabled-border-color:var(--typo3-state-info-border-color);--typo3-state-notice-color:light-dark(var(--token-color-notice-1),var(--token-color-notice-15));--typo3-state-notice-bg:light-dark(var(--token-color-notice-75),var(--token-color-notice-75));--typo3-state-notice-border-color:light-dark(var(--token-color-notice-85),var(--token-color-notice-65));--typo3-state-notice-hover-color:var(--typo3-state-notice-color);--typo3-state-notice-hover-bg:light-dark(var(--token-color-notice-80),var(--token-color-notice-70));--typo3-state-notice-hover-border-color:light-dark(var(--token-color-notice-90),var(--token-color-notice-60));--typo3-state-notice-focus-color:var(--typo3-state-notice-color);--typo3-state-notice-focus-bg:light-dark(var(--token-color-notice-85),var(--token-color-notice-65));--typo3-state-notice-focus-border-color:light-dark(var(--token-color-notice-95),var(--token-color-notice-55));--typo3-state-notice-disabled-color:var(--typo3-state-notice-color);--typo3-state-notice-disabled-bg:var(--typo3-state-notice-bg);--typo3-state-notice-disabled-border-color:var(--typo3-state-notice-border-color);--typo3-state-red-color:light-dark(var(--token-color-red-1),var(--token-color-red-1));--typo3-state-red-bg:light-dark(var(--token-color-red-55),var(--token-color-red-70));--typo3-state-red-border-color:light-dark(var(--token-color-red-65),var(--token-color-red-60));--typo3-state-red-hover-color:var(--typo3-state-red-color);--typo3-state-red-hover-bg:light-dark(var(--token-color-red-60),var(--token-color-red-65));--typo3-state-red-hover-border-color:light-dark(var(--token-color-red-70),var(--token-color-red-55));--typo3-state-red-focus-color:var(--typo3-state-red-color);--typo3-state-red-focus-bg:light-dark(var(--token-color-red-65),var(--token-color-red-60));--typo3-state-red-focus-border-color:light-dark(var(--token-color-red-75),var(--token-color-red-50));--typo3-state-red-disabled-color:var(--typo3-state-red-color);--typo3-state-red-disabled-bg:var(--typo3-state-red-bg);--typo3-state-red-disabled-border-color:var(--typo3-state-red-border-color);--typo3-state-orange-color:light-dark(var(--token-color-orange-90),var(--token-color-orange-1));--typo3-state-orange-bg:light-dark(var(--token-color-orange-40),var(--token-color-orange-75));--typo3-state-orange-border-color:light-dark(var(--token-color-orange-50),var(--token-color-orange-65));--typo3-state-orange-hover-color:var(--typo3-state-orange-color);--typo3-state-orange-hover-bg:light-dark(var(--token-color-orange-45),var(--token-color-orange-70));--typo3-state-orange-hover-border-color:light-dark(var(--token-color-orange-55),var(--token-color-orange-60));--typo3-state-orange-focus-color:var(--typo3-state-orange-color);--typo3-state-orange-focus-bg:light-dark(var(--token-color-orange-50),var(--token-color-orange-65));--typo3-state-orange-focus-border-color:light-dark(var(--token-color-orange-60),var(--token-color-orange-55));--typo3-state-orange-disabled-color:var(--typo3-state-orange-color);--typo3-state-orange-disabled-bg:var(--typo3-state-orange-bg);--typo3-state-orange-disabled-border-color:var(--typo3-state-orange-border-color);--typo3-state-yellow-color:light-dark(var(--token-color-yellow-90),var(--token-color-yellow-1));--typo3-state-yellow-bg:light-dark(var(--token-color-yellow-35),var(--token-color-yellow-80));--typo3-state-yellow-border-color:light-dark(var(--token-color-yellow-45),var(--token-color-yellow-70));--typo3-state-yellow-hover-color:var(--typo3-state-yellow-color);--typo3-state-yellow-hover-bg:light-dark(var(--token-color-yellow-40),var(--token-color-yellow-75));--typo3-state-yellow-hover-border-color:light-dark(var(--token-color-yellow-50),var(--token-color-yellow-65));--typo3-state-yellow-focus-color:var(--typo3-state-yellow-color);--typo3-state-yellow-focus-bg:light-dark(var(--token-color-yellow-45),var(--token-color-yellow-70));--typo3-state-yellow-focus-border-color:light-dark(var(--token-color-yellow-55),var(--token-color-yellow-60));--typo3-state-yellow-disabled-color:var(--typo3-state-yellow-color);--typo3-state-yellow-disabled-bg:var(--typo3-state-yellow-bg);--typo3-state-yellow-disabled-border-color:var(--typo3-state-yellow-border-color);--typo3-state-lime-color:light-dark(var(--token-color-lime-90),var(--token-color-lime-1));--typo3-state-lime-bg:light-dark(var(--token-color-lime-35),var(--token-color-lime-85));--typo3-state-lime-border-color:light-dark(var(--token-color-lime-45),var(--token-color-lime-75));--typo3-state-lime-hover-color:var(--typo3-state-lime-color);--typo3-state-lime-hover-bg:light-dark(var(--token-color-lime-40),var(--token-color-lime-80));--typo3-state-lime-hover-border-color:light-dark(var(--token-color-lime-50),var(--token-color-lime-70));--typo3-state-lime-focus-color:var(--typo3-state-lime-color);--typo3-state-lime-focus-bg:light-dark(var(--token-color-lime-45),var(--token-color-lime-75));--typo3-state-lime-focus-border-color:light-dark(var(--token-color-lime-55),var(--token-color-lime-65));--typo3-state-lime-disabled-color:var(--typo3-state-lime-color);--typo3-state-lime-disabled-bg:var(--typo3-state-lime-bg);--typo3-state-lime-disabled-border-color:var(--typo3-state-lime-border-color);--typo3-state-green-color:light-dark(var(--token-color-green-1),var(--token-color-green-1));--typo3-state-green-bg:light-dark(var(--token-color-green-70),var(--token-color-green-80));--typo3-state-green-border-color:light-dark(var(--token-color-green-80),var(--token-color-green-70));--typo3-state-green-hover-color:var(--typo3-state-green-color);--typo3-state-green-hover-bg:light-dark(var(--token-color-green-75),var(--token-color-green-75));--typo3-state-green-hover-border-color:light-dark(var(--token-color-green-85),var(--token-color-green-65));--typo3-state-green-focus-color:var(--typo3-state-green-color);--typo3-state-green-focus-bg:light-dark(var(--token-color-green-80),var(--token-color-green-70));--typo3-state-green-focus-border-color:light-dark(var(--token-color-green-90),var(--token-color-green-60));--typo3-state-green-disabled-color:var(--typo3-state-green-color);--typo3-state-green-disabled-bg:var(--typo3-state-green-bg);--typo3-state-green-disabled-border-color:var(--typo3-state-green-border-color);--typo3-state-teal-color:light-dark(var(--token-color-teal-90),var(--token-color-teal-1));--typo3-state-teal-bg:light-dark(var(--token-color-teal-35),var(--token-color-teal-80));--typo3-state-teal-border-color:light-dark(var(--token-color-teal-45),var(--token-color-teal-70));--typo3-state-teal-hover-color:var(--typo3-state-teal-color);--typo3-state-teal-hover-bg:light-dark(var(--token-color-teal-40),var(--token-color-teal-75));--typo3-state-teal-hover-border-color:light-dark(var(--token-color-teal-50),var(--token-color-teal-65));--typo3-state-teal-focus-color:var(--typo3-state-teal-color);--typo3-state-teal-focus-bg:light-dark(var(--token-color-teal-45),var(--token-color-teal-70));--typo3-state-teal-focus-border-color:light-dark(var(--token-color-teal-55),var(--token-color-teal-60));--typo3-state-teal-disabled-color:var(--typo3-state-teal-color);--typo3-state-teal-disabled-bg:var(--typo3-state-teal-bg);--typo3-state-teal-disabled-border-color:var(--typo3-state-teal-border-color);--typo3-state-blue-color:light-dark(var(--token-color-blue-1),var(--token-color-blue-1));--typo3-state-blue-bg:light-dark(var(--token-color-blue-55),var(--token-color-blue-70));--typo3-state-blue-border-color:light-dark(var(--token-color-blue-65),var(--token-color-blue-60));--typo3-state-blue-hover-color:var(--typo3-state-blue-color);--typo3-state-blue-hover-bg:light-dark(var(--token-color-blue-60),var(--token-color-blue-65));--typo3-state-blue-hover-border-color:light-dark(var(--token-color-blue-70),var(--token-color-blue-55));--typo3-state-blue-focus-color:var(--typo3-state-blue-color);--typo3-state-blue-focus-bg:light-dark(var(--token-color-blue-65),var(--token-color-blue-60));--typo3-state-blue-focus-border-color:light-dark(var(--token-color-blue-75),var(--token-color-blue-50));--typo3-state-blue-disabled-color:var(--typo3-state-blue-color);--typo3-state-blue-disabled-bg:var(--typo3-state-blue-bg);--typo3-state-blue-disabled-border-color:var(--typo3-state-blue-border-color);--typo3-state-indigo-color:light-dark(var(--token-color-indigo-1),var(--token-color-indigo-1));--typo3-state-indigo-bg:light-dark(var(--token-color-indigo-55),var(--token-color-indigo-70));--typo3-state-indigo-border-color:light-dark(var(--token-color-indigo-65),var(--token-color-indigo-60));--typo3-state-indigo-hover-color:var(--typo3-state-indigo-color);--typo3-state-indigo-hover-bg:light-dark(var(--token-color-indigo-60),var(--token-color-indigo-65));--typo3-state-indigo-hover-border-color:light-dark(var(--token-color-indigo-70),var(--token-color-indigo-55));--typo3-state-indigo-focus-color:var(--typo3-state-indigo-color);--typo3-state-indigo-focus-bg:light-dark(var(--token-color-indigo-65),var(--token-color-indigo-60));--typo3-state-indigo-focus-border-color:light-dark(var(--token-color-indigo-75),var(--token-color-indigo-50));--typo3-state-indigo-disabled-color:var(--typo3-state-indigo-color);--typo3-state-indigo-disabled-bg:var(--typo3-state-indigo-bg);--typo3-state-indigo-disabled-border-color:var(--typo3-state-indigo-border-color);--typo3-state-purple-color:light-dark(var(--token-color-purple-1),var(--token-color-purple-1));--typo3-state-purple-bg:light-dark(var(--token-color-purple-55),var(--token-color-purple-70));--typo3-state-purple-border-color:light-dark(var(--token-color-purple-65),var(--token-color-purple-60));--typo3-state-purple-hover-color:var(--typo3-state-purple-color);--typo3-state-purple-hover-bg:light-dark(var(--token-color-purple-60),var(--token-color-purple-65));--typo3-state-purple-hover-border-color:light-dark(var(--token-color-purple-70),var(--token-color-purple-55));--typo3-state-purple-focus-color:var(--typo3-state-purple-color);--typo3-state-purple-focus-bg:light-dark(var(--token-color-purple-65),var(--token-color-purple-60));--typo3-state-purple-focus-border-color:light-dark(var(--token-color-purple-75),var(--token-color-purple-50));--typo3-state-purple-disabled-color:var(--typo3-state-purple-color);--typo3-state-purple-disabled-bg:var(--typo3-state-purple-bg);--typo3-state-purple-disabled-border-color:var(--typo3-state-purple-border-color);--typo3-state-magenta-color:light-dark(var(--token-color-magenta-1),var(--token-color-magenta-1));--typo3-state-magenta-bg:light-dark(var(--token-color-magenta-55),var(--token-color-magenta-70));--typo3-state-magenta-border-color:light-dark(var(--token-color-magenta-65),var(--token-color-magenta-60));--typo3-state-magenta-hover-color:var(--typo3-state-magenta-color);--typo3-state-magenta-hover-bg:light-dark(var(--token-color-magenta-60),var(--token-color-magenta-65));--typo3-state-magenta-hover-border-color:light-dark(var(--token-color-magenta-70),var(--token-color-magenta-55));--typo3-state-magenta-focus-color:var(--typo3-state-magenta-color);--typo3-state-magenta-focus-bg:light-dark(var(--token-color-magenta-65),var(--token-color-magenta-60));--typo3-state-magenta-focus-border-color:light-dark(var(--token-color-magenta-75),var(--token-color-magenta-50));--typo3-state-magenta-disabled-color:var(--typo3-state-magenta-color);--typo3-state-magenta-disabled-bg:var(--typo3-state-magenta-bg);--typo3-state-magenta-disabled-border-color:var(--typo3-state-magenta-border-color);--typo3-shadow-2:0 1px 2px light-dark(rgba(0,0,0,.04),rgba(0,0,0,.25)),0 1px 2px light-dark(rgba(0,0,0,.08),rgba(0,0,0,.35));--typo3-shadow-4:0 2px 4px light-dark(rgba(0,0,0,.04),rgba(0,0,0,.25)),0 2px 4px light-dark(rgba(0,0,0,.08),rgba(0,0,0,.35));--typo3-shadow-8:0 4px 8px light-dark(rgba(0,0,0,.04),rgba(0,0,0,.25)),0 4px 8px light-dark(rgba(0,0,0,.08),rgba(0,0,0,.35));--typo3-shadow-16:0 8px 16px light-dark(rgba(0,0,0,.04),rgba(0,0,0,.25)),0 8px 16px light-dark(rgba(0,0,0,.08),rgba(0,0,0,.35));--typo3-shadow-28:0 14px 28px light-dark(rgba(0,0,0,.04),rgba(0,0,0,.25)),0 14px 28px light-dark(rgba(0,0,0,.08),rgba(0,0,0,.35));--typo3-shadow-64:0 0 4px light-dark(rgba(0,0,0,.04),rgba(0,0,0,.25)),0 32px 64px light-dark(rgba(0,0,0,.08),rgba(0,0,0,.35));--typo3-shadow-type-basic:var(--typo3-shadow-2);--typo3-shadow-type-strong:var(--typo3-shadow-4);--typo3-shadow-type-tooltip:var(--typo3-shadow-8);--typo3-shadow-type-flyout:var(--typo3-shadow-16);--typo3-shadow-type-dialog:var(--typo3-shadow-28);--typo3-shadow-type-window:var(--typo3-shadow-64);--typo3-component-color:var(--typo3-text-color-base);--typo3-component-variant-color:var(--typo3-text-color-variant);--typo3-component-primary-color:var(--typo3-text-color-primary);--typo3-component-secondary-color:var(--typo3-text-color-secondary);--typo3-component-match-highlight-color:inherit;--typo3-component-match-highlight-bg:color-mix(in srgb,light-dark(var(--token-color-orange-30),var(--token-color-orange-70)),transparent 50%);--typo3-component-bg:var(--typo3-surface-container-low);--typo3-component-link-color:var(--typo3-text-color-primary);--typo3-component-link-hover-color:color-mix(in srgb,var(--typo3-component-link-color),var(--typo3-component-color) 15%);--typo3-component-font-size:var(--typo3-font-size);--typo3-component-line-height:var(--typo3-line-height);--typo3-component-border-radius:.75em;--typo3-component-border-width:1px;--typo3-component-border-color:color-mix(in srgb,var(--typo3-component-bg),var(--typo3-component-color) var(--typo3-border-mix));--typo3-component-padding-y:.75rem;--typo3-component-padding-x:1rem;--typo3-component-box-shadow:var(--typo3-shadow-2);--typo3-component-box-shadow-strong:var(--typo3-shadow-type-strong);--typo3-component-box-shadow-tooltip:var(--typo3-shadow-type-tooltip);--typo3-component-box-shadow-flyout:var(--typo3-shadow-type-flyout);--typo3-component-box-shadow-dialog:var(--typo3-shadow-type-dialog);--typo3-component-box-shadow-window:var(--typo3-shadow-type-window);--typo3-component-hover-color:var(--typo3-state-default-hover-color);--typo3-component-hover-bg:var(--typo3-state-default-hover-bg);--typo3-component-hover-border-color:var(--typo3-state-default-hover-border-color);--typo3-component-focus-color:var(--typo3-state-default-focus-color);--typo3-component-focus-bg:var(--typo3-state-default-focus-bg);--typo3-component-focus-border-color:var(--typo3-state-default-focus-border-color);--typo3-component-active-color:var(--typo3-state-primary-color);--typo3-component-active-bg:var(--typo3-state-primary-bg);--typo3-component-active-border-color:var(--typo3-state-primary-border-color);--typo3-component-disabled-color:var(--typo3-text-color-variant);--typo3-component-disabled-bg:var(--typo3-surface-container-base);--typo3-component-disabled-border-color:var(--typo3-component-border-color);--typo3-component-spacing:2rem;--typo3-list-item-padding-y:.5rem;--typo3-list-item-padding-x:.75rem;--typo3-list-item-hover-color:var(--typo3-component-hover-color);--typo3-list-item-hover-bg:var(--typo3-component-hover-bg);--typo3-list-item-hover-border-color:var(--typo3-component-hover-border-color);--typo3-list-item-focus-color:var(--typo3-component-focus-color);--typo3-list-item-focus-bg:var(--typo3-component-focus-bg);--typo3-list-item-focus-border-color:var(--typo3-component-focus-border-color);--typo3-list-item-active-color:var(--typo3-component-active-color);--typo3-list-item-active-bg:var(--typo3-component-active-bg);--typo3-list-item-active-border-color:var(--typo3-component-active-border-color);--typo3-list-item-disabled-color:var(--typo3-component-disabled-color);--typo3-list-item-disabled-bg:var(--typo3-component-disabled-bg);--typo3-list-item-disabled-border-color:var(--typo3-component-disabled-border-color);--typo3-input-font-size:.75rem;--typo3-input-line-height:1.5;--typo3-input-padding-y:.5rem;--typo3-input-padding-x:.75rem;--typo3-input-sm-padding-y:.3125rem;--typo3-input-sm-padding-x:.5rem;--typo3-input-sm-font-size:.6875rem;--typo3-input-border-width:1px;--typo3-input-border-radius:.75em;--typo3-input-color:var(--typo3-text-color-base);--typo3-input-bg:var(--typo3-surface-container-lowest);--typo3-input-group-addon-bg:color-mix(in srgb,var(--typo3-input-bg),var(--typo3-input-color) 10%);--typo3-input-border-color:var(--typo3-state-default-border-color);--typo3-input-hover-color:var(--typo3-input-color);--typo3-input-hover-bg:var(--typo3-input-bg);--typo3-input-hover-border-color:var(--typo3-state-default-hover-border-color);--typo3-input-focus-color:var(--typo3-input-color);--typo3-input-focus-bg:var(--typo3-input-bg);--typo3-input-focus-border-color:var(--typo3-state-primary-focus-border-color);--typo3-input-active-color:var(--typo3-state-primary-color);--typo3-input-active-bg:var(--typo3-state-primary-bg);--typo3-input-active-border-color:var(--typo3-state-primary-focus-border-color);--typo3-input-disabled-color:var(--typo3-state-default-disabled-color);--typo3-input-disabled-bg:var(--typo3-state-default-disabled-bg);--typo3-input-disabled-border-color:var(--typo3-state-default-disabled-border-color);--typo3-input-disabled-opacity:.65;--typo3-icons-close:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%23fff' d='M11.9 5.5 9.4 8l2.5 2.5c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L8 9.4l-2.5 2.5c-.2.2-.5.2-.7 0l-.7-.7c-.2-.2-.2-.5 0-.7L6.6 8 4.1 5.5c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0L8 6.6l2.5-2.5c.2-.2.5-.2.7 0l.7.7c.2.2.2.5 0 .7'/%3E%3C/svg%3E");--typo3-icons-check:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%23fff' d='m13.3 4.8-.7-.7c-.2-.2-.5-.2-.7 0L6.5 9.5 4 6.9c-.2-.2-.5-.2-.7 0l-.6.7c-.2.2-.2.5 0 .7l3.6 3.6c.2.2.5.2.7 0l6.4-6.4c.1-.2.1-.5-.1-.7'/%3E%3C/svg%3E");--typo3-icons-chevron-down:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%23fff' d='m4.464 6.05-.707.707L8 11l4.243-4.243-.707-.707L8 9.586z'/%3E%3C/svg%3E");--typo3-icons-accent:light-dark(var(--token-color-primary-45),var(--token-color-primary-35));--typo3-scaffold-color:var(--typo3-text-color-base);--typo3-scaffold-bg:var(--typo3-surface-base);--typo3-scaffold-border-color:color-mix(in srgb,var(--typo3-surface-base),var(--typo3-scaffold-color) var(--typo3-scaffold-border-mix));--typo3-scaffold-border-mix:10%;--typo3-scaffold-gap:.5rem;--typo3-scaffold-header-height:3.75rem;--typo3-scaffold-header-color:var(--typo3-text-color-base);--typo3-scaffold-header-bg:var(--typo3-surface-bright);--typo3-scaffold-header-box-shadow:var(--typo3-shadow-2);--typo3-scaffold-header-item-height:2.5rem;--typo3-scaffold-header-item-width:2.5rem;--typo3-scaffold-header-item-border-radius:.75rem;--typo3-scaffold-header-padding-y:.5rem;--typo3-scaffold-header-padding-x:.5rem;--typo3-scaffold-header-foldout-bg:var(--typo3-surface-container-high);--typo3-scaffold-header-zindex:var(--typo3-zindex-header);--typo3-scaffold-sidebar-color:var(--typo3-text-color-base);--typo3-scaffold-sidebar-bg:var(--typo3-surface-container-high);--typo3-scaffold-sidebar-boder-color:color-mix(in srgb,var(--typo3-scaffold-sidebar-bg),var(--typo3-scaffold-sidebar-color) var(--typo3-scaffold-border-mix));--typo3-scaffold-sidebar-border-width:1px;--typo3-scaffold-sidebar-collapsed-width:minmax(0,min-content);--typo3-scaffold-sidebar-expanded-width:240px;--typo3-scaffold-sidebar-box-shadow:var(--typo3-shadow-type-flyout);--typo3-scaffold-sidebar-width:var(--typo3-scaffold-sidebar-collapsed-width)} +:root.t3js-disable-transitions{--typo3-transition-color:none} +[data-color-scheme=dark]{color-scheme:only dark} +[data-color-scheme=light]{color-scheme:only light} +[data-theme=classic]{--typo3-color-neutral-mix:0%;--typo3-color-state-harmonize:0%;--typo3-icons-accent:light-dark(#ff8700,#ff8700)} +[data-theme=classic] .scaffold-header,[data-theme=classic] .scaffold-sidebar{color-scheme:only dark} +[data-theme=fresh]{--typo3-color-neutral-mix:12%;--token-color-primary-base:#5033c7;--token-color-secondary-base:hsl(from var(--token-color-primary-base) h 10% l);--typo3-scaffold-header-color:var(--typo3-surface-primary-text);--typo3-scaffold-header-bg:light-dark(hsl(from var(--token-color-primary-base) h calc(s * .67) 20%),hsl(from var(--token-color-primary-base) h calc(s * .42) 20%));--typo3-scaffold-header-box-shadow:none;--typo3-scaffold-sidebar-color:var(--typo3-surface-primary-text);--typo3-scaffold-sidebar-bg:light-dark(hsl(from var(--token-color-primary-base) h calc(s * .67) 20%),hsl(from var(--token-color-primary-base) h calc(s * .42) 20%));--typo3-scaffold-sidebar-border-width:0} +[data-theme=fresh] .scaffold-sidebar{--typo3-icons-accent:light-dark(hsl(from var(--token-color-primary-base) h s 75%),hsl(from var(--token-color-primary-base) h s 70%))} +@media (min-width:992px){ +[data-theme=fresh] .scaffold-content:before{--size:1rem;background-color:var(--typo3-scaffold-sidebar-bg);content:"";height:calc(var(--size)*2);inset-inline-start:calc(var(--size)*-1);-webkit-mask:radial-gradient(circle at var(--typo3-position-end-percent) 100%,transparent calc(var(--size) - .5px),#000 calc(var(--size) + .5px));mask:radial-gradient(circle at var(--typo3-position-end-percent) 100%,transparent calc(var(--size) - .5px),#000 calc(var(--size) + .5px));position:absolute;top:calc(var(--size)*-1);-webkit-user-select:none;-moz-user-select:none;user-select:none;width:calc(var(--size)*2);z-index:calc(var(--typo3-zindex-header) + 2)} +[data-theme=fresh] .modal-position-sheet{--typo3-modal-border-radius:1rem}} +:root.t3js-disable-transitions,:root.t3js-disable-transitions *,:root.t3js-disable-transitions :after,:root.t3js-disable-transitions :before{transition:none!important} +:root{--typo3-position-modifier:1;--typo3-position-start-percent:0%;--typo3-position-end-percent:100%;--typo3-position-start:left;--typo3-position-end:right} +[dir=rtl]{--typo3-position-modifier:-1;--typo3-position-start-percent:100%;--typo3-position-end-percent:0%;--typo3-position-start:right;--typo3-position-end:left} +*{scrollbar-color:color-mix(in srgb,currentColor,transparent 75%) transparent} +:root,body{background-color:var(--typo3-scaffold-bg);color:var(--typo3-scaffold-color)} +body{font-size:var(--typo3-font-size);min-height:100%} +.scaffold,iframe{background-color:var(--typo3-scaffold-bg);color:var(--typo3-scaffold-color)} +.scaffold{contain:strict;display:grid;grid-template-columns:clamp(0%,var(--typo3-scaffold-sidebar-expanded-width),75%) 1fr;grid-template-rows:auto auto 1fr;height:100dvh;position:relative;width:100%} +.scaffold-state{grid-column:1/3;grid-row:1} +.scaffold-header{grid-column:1/3;grid-row:2} +.scaffold-sidebar{background-color:var(--typo3-scaffold-sidebar-bg);color:var(--typo3-scaffold-sidebar-color);display:none;overflow:hidden;z-index:calc(var(--typo3-scaffold-header-zindex) - 1);-webkit-border-end:var(--typo3-scaffold-sidebar-border-width) solid var(--typo3-scaffold-sidebar-boder-color);border-inline-end:var(--typo3-scaffold-sidebar-border-width) solid var(--typo3-scaffold-sidebar-boder-color);grid-column:1/3;grid-row:3;transform:translateX(-100%);transition:transform .25s ease-in-out,display allow-discrete .25s ease-in-out} +@container (min-width: 320px){ +.scaffold-sidebar{grid-column:1/2}} +.scaffold-content{grid-column:1/3;grid-row:3} +.scaffold-sidebar-flyout .scaffold-sidebar{box-shadow:var(--typo3-scaffold-sidebar-box-shadow);display:block;transform:translateX(0)} +@starting-style{ +.scaffold-sidebar-flyout .scaffold-sidebar{transform:translateX(-100%)}} +@container (min-width: 992px){ +.scaffold{grid-template-columns:var(--typo3-scaffold-sidebar-width) 1fr} +.scaffold-sidebar-expanded{--typo3-scaffold-sidebar-width:var(--typo3-scaffold-sidebar-expanded-width)} +.scaffold-sidebar{display:flex;flex-direction:column;grid-column:1/2;transform:none;transition:none} +.scaffold-sidebar.scaffold-sidebar-disabled{display:none} +.scaffold-sidebar.scaffold-sidebar-disabled+.scaffold-content{grid-column:1/3} +.scaffold-sidebar.scaffold-sidebar-disabled+.scaffold-content:before{display:none} +.scaffold-content{grid-column:2/3}} +.scaffold-header{align-items:center;background-color:var(--typo3-scaffold-header-bg);box-shadow:var(--typo3-scaffold-header-box-shadow);color:var(--typo3-scaffold-header-color);display:flex;gap:var(--typo3-scaffold-gap);height:var(--typo3-scaffold-header-height);padding:var(--typo3-scaffold-header-padding-y) var(--typo3-scaffold-header-padding-x);z-index:var(--typo3-scaffold-header-zindex)} +.scaffold-topbar{align-items:center;display:flex;flex-grow:1} +.scaffold-toolbar{display:none;z-index:var(--typo3-zindex-dropdown)} +@media (min-width:992px){ +.scaffold-toolbar{background-color:transparent;bottom:auto;display:block;inset-inline-start:auto;overflow:visible}} +@media (max-width:991.98px){ +.scaffold-toolbar-expanded .scaffold-toolbar{background-color:var(--typo3-scaffold-header-foldout-bg);display:block;height:calc(100dvh - var(--typo3-scaffold-header-height));position:absolute;top:var(--typo3-scaffold-header-height);inset-inline:0;overflow:auto;padding:1rem .5rem} +.scaffold-toolbar-expanded .scaffold-toolbar:before{box-shadow:var(--typo3-scaffold-header-box-shadow);content:"";height:1px;position:fixed;top:calc(var(--typo3-scaffold-header-height) - 1px);inset-inline:0}} +.scaffold-content{background:var(--typo3-scaffold-bg);overflow:auto;position:relative!important} +.scaffold-content-module{height:100%} +.scaffold-content-module-iframe{border:none;display:block;height:100%;width:100%} +.scaffold-content typo3-backend-content-navigation [slot=navigation]{display:flex;flex-direction:column;height:100%} +.scaffold-content typo3-backend-content-navigation [slot=content]{display:flex;flex:1 0 0;flex-direction:row;height:100%} +.scaffold-overlay{background-color:var(--typo3-overlay-bg);display:none;grid-column:1/3;grid-row:3;opacity:0;transition:opacity .25s ease-in-out,display allow-discrete .25s ease-in-out;z-index:calc(var(--typo3-scaffold-header-zindex) - 2)} +.scaffold-sidebar-flyout .scaffold-overlay{display:block;opacity:var(--typo3-overlay-opacity)} +@starting-style{ +.scaffold-sidebar-flyout .scaffold-overlay{opacity:0}} +.topbar{--topbar-site-bg:transparent;--topbar-item-color:var(--typo3-scaffold-header-color);--topbar-item-bg:var(--typo3-scaffold-header-bg);--topbar-item-border-color:transparent;--topbar-item-height:var(--typo3-scaffold-header-item-height);--topbar-item-width:var(--typo3-scaffold-header-item-width);--topbar-item-border-radius:var(--typo3-scaffold-header-item-border-radius);--topbar-item-color-state:initial;--topbar-item-bg-state:initial;--topbar-item-border-color-state:initial;--topbar-workspace-bg:light-dark(var(--token-color-primary-20),var(--token-color-primary-80));--topbar-item-hover-color:var(--typo3-scaffold-header-color);--topbar-item-hover-bg:color-mix(in srgb,var(--topbar-item-bg),var(--topbar-item-color) 5%);--topbar-item-hover-border-color:color-mix(in srgb,var(--topbar-item-bg),var(--topbar-item-color) 20%);--topbar-item-focus-color:var(--typo3-scaffold-header-color);--topbar-item-focus-bg:color-mix(in srgb,var(--topbar-item-bg),var(--topbar-item-color) 10%);--topbar-item-focus-border-color:color-mix(in srgb,var(--topbar-item-bg),var(--topbar-item-color) 25%);align-items:center;contain:inline-size;display:flex;flex-grow:1;gap:.5rem;position:relative} +.topbar-button{align-items:center;background-color:var(--topbar-item-bg-state,var(--topbar-item-bg));border:1px solid var(--topbar-item-border-color-state,var(--topbar-item-border-color));border-radius:var(--topbar-item-border-radius);color:var(--topbar-item-color-state,var(--topbar-item-color));display:inline-flex;height:var(--topbar-item-height);isolation:isolate;justify-content:center;outline-offset:0;transition:var(--typo3-transition-color);width:var(--topbar-item-width)} +@media (prefers-reduced-motion){ +.topbar-button{transition:none}} +.topbar-button:hover{--topbar-item-color-state:var(--topbar-item-hover-color);--topbar-item-bg-state:var(--topbar-item-hover-bg);--topbar-item-border-color-state:var(--topbar-item-hover-border-color)} +.topbar-button:focus{--topbar-item-color-state:var(--topbar-item-focus-color);--topbar-item-bg-state:var(--topbar-item-focus-bg);--topbar-item-border-color-state:var(--topbar-item-focus-border-color)} +.topbar-button:focus,.topbar-button:hover{z-index:1} +.topbar-button:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--topbar-item-border-color-state),transparent var(--typo3-outline-transparent-mix));z-index:2} +.topbar-button[disabled],.topbar-button[disabled]:focus,.topbar-button[disabled]:hover{cursor:not-allowed;opacity:.5} +.topbar-button.topbar-button-modulemenu{inset-inline-start:0} +.topbar-button.topbar-button-toolbar{inset-inline-end:var(--topbar-item-width)} +.topbar-button.topbar-button-search{inset-inline-end:0} +@media (min-width:992px){ +.topbar-button-search,.topbar-button-toolbar{display:none}} +.topbar-site-container{contain:inline-size;flex-grow:1} +.topbar-site{align-items:center;border-radius:var(--topbar-item-border-radius);display:inline-flex;gap:.5rem;height:var(--topbar-item-height);line-height:1.2;max-width:100%;white-space:nowrap} +.topbar-site,.topbar-site:focus,.topbar-site:hover{color:inherit;text-decoration:none} +.topbar-site:focus .topbar-site-title,.topbar-site:hover .topbar-site-title{text-decoration:underline;-webkit-text-decoration-color:color-mix(in srgb,currentColor,transparent 50%);text-decoration-color:color-mix(in srgb,currentColor,transparent 50%);text-underline-offset:.1em} +.topbar-site:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--topbar-item-focus-border-color),transparent var(--typo3-outline-transparent-mix))} +.topbar-site-logo{display:none} +.topbar-site-logo img{max-height:35px;max-width:180px;-o-object-fit:contain;object-fit:contain;width:auto} +@media (min-width:320px){ +.topbar-site-logo{display:block}} +.topbar-site-title{overflow:hidden} +.topbar-site-name,.topbar-site-version{display:block;overflow:hidden;text-overflow:ellipsis} +.topbar-site-version{opacity:.5} +.toolbar,.toolbar-list{display:flex;padding:0} +.toolbar-list{flex-wrap:wrap;gap:.5rem;list-style:none;margin:0;max-width:100%} +@media (min-width:992px){ +.toolbar-list{gap:1px}} +.toolbar-item{display:block;position:relative;width:100%} +@media (min-width:600px){ +.toolbar-item{width:calc(50% - .25rem)}} +@media (min-width:750px){ +.toolbar-item{width:calc(33.33% - .33333rem)}} +@media (min-width:992px){ +.toolbar-item{width:auto}} +.toolbar-item .dropdown-menu{width:350px} +@media (max-width:991.98px){ +.toolbar-item .dropdown-menu{inset-inline-start:50%!important;max-height:calc(100dvh - 2rem)!important;position:fixed!important;top:50%!important;transform:translate(calc(-50%*var(--typo3-position-modifier)),-50%)!important;width:calc(100dvw - 2rem)!important;position-area:unset!important} +.toolbar-item .dropdown-menu::backdrop{background-color:var(--typo3-overlay-bg);opacity:var(--typo3-overlay-opacity)}} +.toolbar-item-avatar{display:inline-block;vertical-align:middle} +.toolbar-item-avatar .avatar{margin-left:-3px;margin-right:-3px} +.toolbar-item-icon{display:inline-flex;flex-shrink:0} +.toolbar-item-name,.toolbar-item-title{flex-grow:1;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap} +.toolbar-item-title:has(+.toolbar-item-name){flex-grow:0} +.toolbar-item-badge{flex-shrink:0} +.toolbar-item-link{--toolbar-item-link-height:var(--typo3-scaffold-header-item-height);--toolbar-item-link-width:var(--typo3-scaffold-header-item-width);--toolbar-item-link-border-radius:var(--typo3-scaffold-header-item-border-radius);--toolbar-item-link-color:var(--typo3-scaffold-header-color);--toolbar-item-link-bg:var(--typo3-scaffold-header-bg);--toolbar-item-link-border-color:color-mix(in srgb,var(--typo3-scaffold-header-bg),var(--typo3-scaffold-header-color) var(--typo3-border-mix));--toolbar-item-link-color-state:initial;--toolbar-item-link-bg-state:initial;--toolbar-item-link-border-color-state:initial;--toolbar-item-link-hover-color:var(--toolbar-item-link-color);--toolbar-item-link-hover-bg:color-mix(in srgb,var(--toolbar-item-link-bg),var(--toolbar-item-link-color) 10%);--toolbar-item-link-hover-border-color:color-mix(in srgb,var(--toolbar-item-link-bg),var(--toolbar-item-link-color) 20%);--toolbar-item-link-focus-color:var(--toolbar-item-link-color);--toolbar-item-link-focus-bg:color-mix(in srgb,var(--toolbar-item-link-bg),var(--toolbar-item-link-color) 15%);--toolbar-item-link-focus-border-color:color-mix(in srgb,var(--toolbar-item-link-bg),var(--toolbar-item-link-color) 25%);align-items:center;background-color:var(--toolbar-item-link-bg-state,var(--toolbar-item-link-bg));border:1px solid var(--toolbar-item-link-border-color-state,var(--toolbar-item-link-border-color));border-radius:var(--toolbar-item-link-border-radius);color:var(--toolbar-item-link-color-state,var(--toolbar-item-link-color));display:flex;gap:.75em;height:var(--toolbar-item-link-height);min-width:var(--toolbar-item-link-width);outline-offset:0;padding:0 calc((var(--typo3-scaffold-header-item-width) - 18px)/2);position:relative;text-decoration:none;text-overflow:ellipsis;transition:var(--typo3-transition-color);white-space:nowrap;width:100%} +@media (min-width:992px){ +.toolbar-item-link{--toolbar-item-link-color-state:inherit;--toolbar-item-link-bg-state:transparent;--toolbar-item-link-border-color-state:transparent}} +@media (prefers-reduced-motion){ +.toolbar-item-link{transition:none}} +@media (min-width:992px){ +.toolbar-item-link{justify-content:center}} +.toolbar-item-link:focus,.toolbar-item-link:hover{text-decoration:none} +.toolbar-item-link:hover{--toolbar-item-link-color-state:var(--toolbar-item-link-hover-color);--toolbar-item-link-bg-state:var(--toolbar-item-link-hover-bg);--toolbar-item-link-border-color-state:var(--toolbar-item-link-hover-border-color)} +.open .toolbar-item-link,.toolbar-item-link.show,.toolbar-item-link:focus{--toolbar-item-link-color-state:var(--toolbar-item-link-focus-color);--toolbar-item-link-bg-state:var(--toolbar-item-link-focus-bg);--toolbar-item-link-border-color-state:var(--toolbar-item-link-focus-border-color)} +.toolbar-item-link:focus,.toolbar-item-link:hover{z-index:1} +.open .toolbar-item-link,.toolbar-item-link.show{z-index:2} +.toolbar-item-link:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--toolbar-item-link-border-color-state),transparent var(--typo3-outline-transparent-mix));z-index:3} +.scaffold-in-impersonation #typo3-cms-backend-backend-toolbaritems-usertoolbaritem .toolbar-item-link{--toolbar-item-link-color-state:var(--typo3-state-primary-color);--toolbar-item-link-bg-state:var(--typo3-state-primary-bg);--toolbar-item-link-border-color-state:var(--typo3-state-primary-border-color)} +.scaffold-in-impersonation #typo3-cms-backend-backend-toolbaritems-usertoolbaritem .toolbar-item-link:hover{--toolbar-item-link-color-state:var(--typo3-state-primary-hover-color);--toolbar-item-link-bg-state:var(--typo3-state-primary-hover-bg);--toolbar-item-link-border-color-state:var(--typo3-state-primary-hover-border-color)} +.open .scaffold-in-impersonation #typo3-cms-backend-backend-toolbaritems-usertoolbaritem .toolbar-item-link,.scaffold-in-impersonation #typo3-cms-backend-backend-toolbaritems-usertoolbaritem .toolbar-item-link.show,.scaffold-in-impersonation #typo3-cms-backend-backend-toolbaritems-usertoolbaritem .toolbar-item-link:focus{--toolbar-item-link-color-state:var(--typo3-state-primary-focus-color);--toolbar-item-link-state:var(--typo3-state-primary-focus-bg);--toolbar-item-link-border-color-state:var(--typo3-state-primary-focus-border-color)} +.scaffold-in-impersonation #typo3-cms-backend-backend-toolbaritems-usertoolbaritem .toolbar-item-link:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-state-primary-bg),transparent var(--typo3-outline-transparent-mix));z-index:3} +@media (min-width:992px){ +.toolbar-item-title{height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;clip:rect(0,0,0,0)!important;border:0!important;white-space:nowrap!important} +.toolbar-item-badge{inset-inline-end:-.5rem;position:absolute;top:-.5rem;z-index:4}} +.modulemenu{--modulemenu-color:var(--typo3-scaffold-sidebar-color);--modulemenu-bg:var(--typo3-scaffold-sidebar-bg);--modulemenu-spacer:2px;--modulemenu-group-spacer:.75rem;--modulemenu-group-indent:calc(var(--modulemenu-group-line-indent) + 0.5rem);--modulemenu-group-line-indent:calc(var(--modulemenu-action-padding) + var(--modulemenu-icon-size)/2);--modulemenu-icon-size:32px;--modulemenu-icon-border-radius:calc(var(--modulemenu-action-border-radius) - var(--modulemenu-action-padding));--modulemenu-action-bg:transparent;--modulemenu-action-color:var(--modulemenu-color);--modulemenu-action-border-width:1px;--modulemenu-action-border-color:transparent;--modulemenu-action-min-width:42px;--modulemenu-action-padding:4px;--modulemenu-action-border-radius:.75em;--modulemenu-action-bg-state:initial;--modulemenu-action-color-state:initial;--modulemenu-action-border-color-state:initial;--modulemenu-action-hover-color:var(--modulemenu-color);--modulemenu-action-hover-bg:color-mix(in srgb,var(--modulemenu-bg),var(--modulemenu-color) 5%);--modulemenu-action-hover-border-color:color-mix(in srgb,var(--modulemenu-bg),var(--modulemenu-color) 20%);--modulemenu-action-focus-color:var(--modulemenu-color);--modulemenu-action-focus-bg:color-mix(in srgb,var(--modulemenu-bg),var(--modulemenu-color) 10%);--modulemenu-action-focus-border-color:color-mix(in srgb,var(--modulemenu-bg),var(--modulemenu-color) 25%);--modulemenu-action-active-color:var(--modulemenu-color);--modulemenu-action-active-bg:color-mix(in srgb,var(--modulemenu-bg),var(--modulemenu-color) 10%);--modulemenu-action-active-border-color:color-mix(in srgb,var(--modulemenu-bg),var(--modulemenu-color) 25%);--modulemenu-action-collapsed-indicator-color:var(--typo3-surface-secondary-text);--modulemenu-action-collapsed-indicator-bg:var(--typo3-surface-secondary);background-color:var(--modulemenu-bg);color:var(--modulemenu-color);list-style:none;margin:0} +.modulemenu>ul{container-type:inline-size;display:grid;gap:var(--modulemenu-group-spacer);min-width:var(--modulemenu-action-min-width)} +@container (max-width: 59px){ +[data-modulemenu-level="1"]+[data-modulemenu-level="1"]:before{background-color:color-mix(in srgb,currentColor,transparent 85%);content:"";display:block;height:1px;margin-bottom:var(--modulemenu-group-spacer);width:100%}} +.modulemenu-action{align-items:center;background-color:var(--modulemenu-action-bg-state,var(--modulemenu-action-bg));border:var(--modulemenu-action-border-width) solid var(--modulemenu-action-border-color-state,var(--modulemenu-action-border-color));border-radius:var(--modulemenu-action-border-radius);color:var(--modulemenu-action-color-state,var(--modulemenu-action-color));container-type:inline-size;display:flex;min-width:var(--modulemenu-action-min-width);outline-offset:0;overflow:hidden;padding:var(--modulemenu-action-padding);position:relative;text-align:start;transition:var(--typo3-transition-color)} +@media (prefers-reduced-motion){ +.modulemenu-action{transition:none}} +.modulemenu-action:focus,.modulemenu-action:hover{color:inherit;outline:none;text-decoration:none} +.modulemenu-action:focus-visible,.modulemenu-action:hover{--modulemenu-action-color-state:var(--modulemenu-action-hover-color);--modulemenu-action-bg-state:var(--modulemenu-action-hover-bg);--modulemenu-action-border-color-state:var(--modulemenu-action-hover-border-color)} +.modulemenu-action:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--modulemenu-action-border-color-state,var(--modulemenu-action-border-color)),transparent var(--typo3-outline-transparent-mix))} +.modulemenu-action.modulemenu-action-active:not(:has(~ul.collapse.show)){--modulemenu-action-color-state:var(--modulemenu-action-active-color);--modulemenu-action-bg-state:var(--modulemenu-action-active-bg);--modulemenu-action-border-color-state:var(--modulemenu-action-active-border-color)} +.modulemenu-icon{align-items:center;border-radius:var(--modulemenu-icon-border-radius);display:flex;flex-shrink:0;height:var(--modulemenu-icon-size);justify-content:center;overflow:hidden;position:relative;width:var(--modulemenu-icon-size)} +.modulemenu-name{flex-grow:1;-webkit-margin-start:.5rem;height:1px;margin-inline-start:.5rem;overflow:hidden;padding:0;position:absolute;text-overflow:ellipsis;width:1px;clip:rect(0,0,0,0);white-space:nowrap} +@container (min-width: 60px){ +.modulemenu-name{height:auto;position:static;width:auto}} +.modulemenu-group>button:not([disabled]) .modulemenu-indicator{color:inherit;display:none;flex-grow:0;flex-shrink:0;height:16px;margin:8px;position:relative;width:16px} +.modulemenu-group>button:not([disabled]) .modulemenu-indicator:after,.modulemenu-group>button:not([disabled]) .modulemenu-indicator:before{border-top:1px solid;content:"";height:0;position:absolute;top:50%;transition:transform .25s ease-in-out;width:6px} +.modulemenu-group>button:not([disabled]) .modulemenu-indicator:before{inset-inline-start:3px;transform:rotate(45deg)} +.modulemenu-group>button:not([disabled]) .modulemenu-indicator:after{inset-inline-end:3px;transform:rotate(-45deg)} +@container (max-width: 59px){ +.modulemenu-indicator{background-color:var(--modulemenu-action-collapsed-indicator-bg)!important;border-radius:50%;bottom:3px;color:var(--modulemenu-action-collapsed-indicator-color)!important;display:block!important;inset-inline-end:3px;margin:0!important;outline:2px solid var(--modulemenu-action-bg-state,var(--modulemenu-bg));position:absolute!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}} +@container (min-width: 60px){ +.modulemenu-indicator{display:block!important}} +.modulemenu-group-container{container-type:inline-size;display:grid;gap:var(--modulemenu-spacer);list-style:none;margin:0;padding:0;position:relative} +@container (min-width: 80px){ +.modulemenu-group-container .modulemenu-group-container{-webkit-padding-start:var(--modulemenu-group-indent);padding-inline-start:var(--modulemenu-group-indent)} +.modulemenu-group-container .modulemenu-group-container:before{background-color:color-mix(in srgb,currentColor,transparent 80%);bottom:0;content:"";display:block;inset-inline-start:var(--modulemenu-group-line-indent);position:absolute;top:0;width:1px}} +.modulemenu-group-spacer{border-top:1px dashed color-mix(in srgb,currentColor,transparent 75%);margin:0} +.modulemenu-group{display:grid;gap:var(--modulemenu-spacer)} +button[aria-expanded=true]:not([disabled]) .modulemenu-indicator:before{transform:rotate(-45deg)} +button[aria-expanded=true]:not([disabled]) .modulemenu-indicator:after{transform:rotate(45deg)} +.sidebar-container{display:flex;flex-direction:column;height:100%;overflow-x:hidden;overflow-y:auto} +.sidebar-component{--sidebar-component-padding-x:.5rem;--sidebar-component-padding-y:.75rem;display:block;flex:0 0 auto;padding:var(--sidebar-component-padding-y) var(--sidebar-component-padding-x);position:relative} +.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6,typo3-backend-editable-page-title{font-family:var(--typo3-header-font-family);font-variant:normal;font-weight:400} +.h1:not(:first-child):not([class]),.h2:not(:first-child):not([class]),.h3:not(:first-child):not([class]),.headline-spaced.h1,.headline-spaced.h2,.headline-spaced.h3,h1.headline-spaced,h1:not(:first-child):not([class]),h2.headline-spaced,h2:not(:first-child):not([class]),h3.headline-spaced,h3:not(:first-child):not([class]),typo3-backend-editable-page-title.headline-spaced,typo3-backend-editable-page-title:not(:first-child):not([class]){margin-top:calc(var(--typo3-spacing)*2)} +.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit} +a{color:var(--typo3-text-color-link);text-decoration:none} +a:hover{text-decoration:underline} +a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none} +.text-base{color:var(--typo3-text-color-base)!important} +.text-muted,.text-variant{color:var(--typo3-text-color-variant)!important} +.text-primary{color:var(--typo3-text-color-primary)!important} +.text-secondary{color:var(--typo3-text-color-secondary)!important} +.text-info{color:var(--typo3-text-color-info)!important} +.text-success{color:var(--typo3-text-color-success)!important} +.text-warning{color:var(--typo3-text-color-warning)!important} +.text-danger{color:var(--typo3-text-color-danger)!important} +.text-notice{color:var(--typo3-text-color-notice)!important} +.text-default{color:var(--typo3-text-color-default)!important} +.text-highlight{background-color:var(--typo3-component-match-highlight-bg)!important;color:inherit!important} +.text-code,code{color:var(--typo3-text-color-code)!important} +.text-reset{color:inherit!important} +.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.hidden,.hide{display:none!important} +.visually-hidden{height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;width:1px!important;clip:rect(0,0,0,0)!important;border:0!important;white-space:nowrap!important} +.visually-hidden:not(caption){inset-block-start:0;inset-inline-start:0;position:absolute!important} +.order-0{order:0} +.order-1{order:1} +.order-2{order:2} +.order-3{order:3} +.order-4{order:4} +.order-5{order:5} +.order-6{order:6} +.order-7{order:7} +.order-8{order:8} +.order-9{order:9} +.dropdown{position:relative;anchor-scope:--dropdown} +:root{--typo3-dropdown-anchor-offset:.125rem} +.dropdown-toggle{anchor-name:--dropdown;text-decoration:none;white-space:nowrap} +.dropdown-toggle:after{background-color:currentColor;content:"";display:inline-block;height:1rem;margin:0;-webkit-mask-image:var(--typo3-icons-chevron-down);mask-image:var(--typo3-icons-chevron-down);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;opacity:.75;vertical-align:middle;width:1rem} +.dropdown-toggle-no-chevron:after{display:none!important} +.dropdown-toggle-link{align-items:center;background:none;border:0;border-radius:1px;display:inline-flex;font-weight:inherit;gap:.25rem;outline-offset:1px;padding:0} +.dropdown-toggle-link:hover{text-decoration:underline} +.dropdown-toggle-link:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,transparent,currentColor var(--typo3-outline-transparent-mix))} +.dropdown-toggle>div{align-items:center;display:flex} +.dropdown-toggle>div:has(>span[data-identifier=empty-empty]),.dropdown-toggle>div:has(>typo3-backend-icon[identifier=empty-empty]){order:1} +.dropdown-menu{--typo3-dropdown-min-width:10rem;--typo3-dropdown-padding-x:2px;--typo3-dropdown-padding-y:2px;--typo3-dropdown-font-size:var(--typo3-component-font-size);--typo3-dropdown-line-height:var(--typo3-component-line-height);--typo3-dropdown-color:var(--typo3-component-color);--typo3-dropdown-bg:var(--typo3-component-bg);--typo3-dropdown-border-color:var(--typo3-component-border-color);--typo3-dropdown-border-radius:var(--typo3-component-border-radius);--typo3-dropdown-border-width:var(--typo3-component-border-width);--typo3-dropdown-inner-border-radius:calc(var(--typo3-component-border-radius) - var(--typo3-component-border-width));--typo3-dropdown-divider-bg:var(--typo3-component-border-color);--typo3-dropdown-divider-margin-y:var(--typo3-list-item-padding-y);--typo3-dropdown-box-shadow:var(--typo3-component-box-shadow);--typo3-dropdown-item-color:var(--typo3-component-color);--typo3-dropdown-item-hover-color:var(--typo3-list-item-hover-color);--typo3-dropdown-item-hover-bg:var(--typo3-list-item-hover-bg);--typo3-dropdown-item-hover-border-color:var(--typo3-list-item-hover-border-color);--typo3-dropdown-item-focus-color:var(--typo3-list-item-focus-color);--typo3-dropdown-item-focus-bg:var(--typo3-list-item-focus-bg);--typo3-dropdown-item-focus-border-color:var(--typo3-list-item-focus-border-color);--typo3-dropdown-item-active-color:var(--typo3-list-item-active-color);--typo3-dropdown-item-active-bg:var(--typo3-list-item-active-bg);--typo3-dropdown-item-active-border-color:var(--typo3-list-item-active-border-color);--typo3-dropdown-item-disabled-color:var(--typo3-list-item-disabled-color);--typo3-dropdown-item-disabled-bg:transparent;--typo3-dropdown-item-disabled-border-color:transparent;--typo3-dropdown-item-padding-x:var(--typo3-list-item-padding-x);--typo3-dropdown-item-padding-y:var(--typo3-list-item-padding-y);--typo3-dropdown-headline-font-size:.875rem;--typo3-dropdown-headline-font-family:var(--typo3-header-font-family);--typo3-dropdown-header-font-size:.75rem;--typo3-dropdown-header-padding-x:var(--typo3-list-item-padding-x);--typo3-dropdown-header-padding-y:var(--typo3-list-item-padding-y);background-color:var(--typo3-dropdown-bg);border:var(--typo3-dropdown-border-width) solid var(--typo3-dropdown-border-color);border-radius:var(--typo3-dropdown-border-radius);box-shadow:var(--typo3-dropdown-box-shadow);color:var(--typo3-dropdown-color);display:none;font-size:var(--typo3-dropdown-font-size);line-height:var(--typo3-dropdown-line-height);list-style:none;margin:0;max-height:calc(100dvh - 1.25rem);max-width:calc(100dvw - 1.25rem);min-width:var(--typo3-dropdown-min-width);padding:var(--typo3-dropdown-padding-y) var(--typo3-dropdown-padding-x);text-align:start;position-anchor:--dropdown;position-try-fallbacks:flip-block,flip-inline,flip-block flip-inline;overflow-y:auto} +.dropdown-menu:popover-open{display:block} +.dropdown-menu[popover]{position-area:block-end span-inline-end;margin-block:var(--typo3-dropdown-anchor-offset)} +.dropdown-menu a:not([class]){color:var(--typo3-component-link-color)} +.dropdown-menu a:not([class]):hover{color:var(--typo3-component-link-hover-color)} +.dropdown-menu>li+li{margin-top:1px} +.dropdown-menu>.dropdown,.dropdown-menu>:has(>.dropdown-toggle){anchor-scope:--dropdown} +.dropdown-menu>.dropdown>.dropdown-toggle,.dropdown-menu>:has(>.dropdown-toggle)>.dropdown-toggle{align-items:center;display:flex} +.dropdown-menu>.dropdown>.dropdown-toggle:after,.dropdown-menu>:has(>.dropdown-toggle)>.dropdown-toggle:after{rotate:calc(-90deg*var(--typo3-position-modifier));-webkit-margin-start:auto;margin-inline-start:auto} +.dropdown-menu>.dropdown>.dropdown-menu[popover],.dropdown-menu>:has(>.dropdown-toggle)>.dropdown-menu[popover]{position-area:inline-end span-block-start;margin-block:0;margin-inline:calc(var(--typo3-dropdown-item-padding-x)/2*-1)} +.dropdown-menu:has(.dropdown-menu:popover-open)>:not(:has(>.dropdown-menu:popover-open)){opacity:.5;transition:opacity .15s ease} +.dropdown-divider{border-top:1px solid var(--typo3-dropdown-divider-bg);height:0;margin:var(--typo3-dropdown-divider-margin-y) 0;overflow:hidden} +.dropdown-item{background-color:transparent;border:0;border-radius:var(--typo3-dropdown-inner-border-radius);clear:both;color:var(--typo3-dropdown-item-color);display:block;font-weight:400;outline-offset:-1px;overflow:hidden;padding:var(--typo3-dropdown-item-padding-y) var(--typo3-dropdown-item-padding-x);text-align:inherit;text-decoration:none;white-space:unset;width:100%} +.dropdown-item[role=button]:hover,a.dropdown-item:hover,button.dropdown-item:hover{background-color:var(--typo3-dropdown-item-hover-bg);color:var(--typo3-dropdown-item-hover-color);outline:1px solid var(--typo3-dropdown-item-hover-border-color);text-decoration:none} +.dropdown-item[role=button].focus,.dropdown-item[role=button]:focus,a.dropdown-item.focus,a.dropdown-item:focus,button.dropdown-item.focus,button.dropdown-item:focus{background-color:var(--typo3-dropdown-item-focus-bg);color:var(--typo3-dropdown-item-focus-color);outline:1px solid var(--typo3-dropdown-item-focus-border-color)} +.dropdown-item[role=button].active,.dropdown-item[role=button]:active,a.dropdown-item.active,a.dropdown-item:active,button.dropdown-item.active,button.dropdown-item:active{--typo3-icons-accent:currentColor;background-color:var(--typo3-dropdown-item-active-bg);color:var(--typo3-dropdown-item-active-color);outline:1px solid var(--typo3-dropdown-item-active-border-color)} +.dropdown-item[role=button].disabled,.dropdown-item[role=button][disabled],a.dropdown-item.disabled,a.dropdown-item[disabled],button.dropdown-item.disabled,button.dropdown-item[disabled]{background-color:var(--typo3-dropdown-item-disabled-bg);color:var(--typo3-dropdown-item-disabled-color);outline:1px solid var(--typo3-dropdown-item-disabled-border-color);pointer-events:none} +.dropdown-item-spaced{align-items:center;display:flex;gap:.5em;text-overflow:ellipsis;white-space:nowrap} +.dropdown-item-action{flex-shrink:0;width:auto} +.dropdown-item-columns{display:flex;gap:.5em} +.dropdown-item-column-icon{width:var(--icon-size-small)} +.dropdown-item-column-title{flex-grow:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.dropdown-item-column-text{flex-grow:1} +.dropdown-item-column-value{-webkit-padding-start:1em;padding-inline-start:1em} +.dropdown-item-status{align-items:center;display:flex;justify-content:center} +.dropdown-item-status,.dropdown-item-status:after{height:var(--icon-size-small);width:var(--icon-size-small)} +.dropdown-item-status:after{content:"";display:block;-webkit-mask-position:center center;mask-position:center center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain} +[data-dropdowntoggle-status=active] .dropdown-item-status:after{background-color:var(--typo3-text-color-primary);-webkit-mask-image:var(--typo3-icons-check);mask-image:var(--typo3-icons-check)} +.dropdown-item-text{color:var(--typo3-dropdown-item-color);display:block;margin-bottom:0;padding:var(--typo3-dropdown-item-padding-y) var(--typo3-dropdown-item-padding-x)} +.dropdown-header,.dropdown-headline{display:block;flex-grow:1;font-family:var(--typo3-dropdown-headline-font-family);font-size:var(--typo3-dropdown-headline-font-size);font-weight:700;line-height:1.2;margin:0;padding:var(--typo3-dropdown-header-padding-y) var(--typo3-dropdown-header-padding-x)} +.dropdown-header+.dropdown-divider,.dropdown-header+.dropdown-item-text,.dropdown-headline+.dropdown-divider,.dropdown-headline+.dropdown-item-text{margin-top:0;padding-top:0} +.dropdown-headline{font-size:var(--typo3-dropdown-headline-font-size)} +.dropdown-header{font-size:var(--typo3-dropdown-header-font-size)} +.dropdown-table{margin-bottom:calc(var(--typo3-dropdown-item-padding-y)/2);margin-top:calc(var(--typo3-dropdown-item-padding-y)/2)} +.dropdown-table td,.dropdown-table th{padding:calc(var(--typo3-dropdown-item-padding-y)/2) calc(var(--typo3-dropdown-item-padding-x)/2);vertical-align:top} +.dropdown-table td:first-child,.dropdown-table th:first-child{-webkit-padding-start:var(--typo3-dropdown-item-padding-x);padding-inline-start:var(--typo3-dropdown-item-padding-x)} +.dropdown-table td:last-child,.dropdown-table th:last-child{-webkit-padding-end:var(--typo3-dropdown-item-padding-x);padding-inline-end:var(--typo3-dropdown-item-padding-x)} +.dropdown-table td[data-type=title],.dropdown-table th[data-type=title]{max-width:10rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.dropdown-table td[data-type=icon]:first-child,.dropdown-table th[data-type=icon]:first-child{-webkit-padding-end:0;padding-inline-end:0} +.dropdown-table td[data-type=icon] .icon,.dropdown-table th[data-type=icon] .icon{vertical-align:unset} +.dropdown-table td[data-type=status],.dropdown-table th[data-type=status]{width:0;-webkit-padding-end:0;padding-inline-end:0} +.dropdown-table td[data-type=value],.dropdown-table th[data-type=value]{word-break:break-word} +.dropdown-list{list-style:none;margin:0;padding:0} +.dropdown-list li{display:flex;gap:1px} +.dropdown-list li+li{margin-top:1px} +.dropdown-row{align-items:start;display:flex;gap:calc(var(--typo3-dropdown-item-padding-x)/2);padding:calc(var(--typo3-dropdown-item-padding-y)/2) var(--typo3-dropdown-item-padding-x) var(--typo3-dropdown-item-padding-y)} +typo3-breadcrumb{display:block;width:100%} +.breadcrumb,.breadcrumb-measurement{--typo3-breadcrumb-divider-content:"/";--typo3-breadcrumb-divider-opacity:.25;--typo3-breadcrumb-divider-space:.35rem;--typo3-breadcrumb-element-color:inherit;--typo3-breadcrumb-element-spacing:.25rem;--typo3-breadcrumb-element-padding-x:.5rem;--typo3-breadcrumb-element-padding-y:.25rem} +.breadcrumb-condensed{--typo3-breadcrumb-divider-space:.2rem;--typo3-breadcrumb-element-spacing:.15rem} +.breadcrumb{align-items:center;display:flex;flex-wrap:wrap;margin:0;padding:0;white-space:nowrap;width:100%} +.breadcrumb-collapsible{flex-wrap:nowrap} +.breadcrumb-right{justify-content:end} +.breadcrumb-item{display:flex;position:relative} +.breadcrumb-item:after{content:var(--typo3-breadcrumb-divider-content)/"";margin:0 var(--typo3-breadcrumb-divider-space);opacity:var(--typo3-breadcrumb-divider-opacity)} +.breadcrumb-item-last:after{display:none} +.breadcrumb-item typo3-backend-icon{width:var(--icon-size-small)} +.breadcrumb:has(:not(.breadcrumb-item)) .breadcrumb-item:last-child:after{display:none} +.breadcrumb-collapsible>.breadcrumb-item .breadcrumb-element,.breadcrumb-collapsible>.breadcrumb-item:last-child{overflow:hidden} +.breadcrumb-collapsible>.breadcrumb-item .breadcrumb-element-label{overflow:hidden;text-overflow:ellipsis} +.breadcrumb-element{align-items:center;background-color:transparent;border:none;color:var(--typo3-breadcrumb-element-color);display:flex;gap:var(--typo3-breadcrumb-element-spacing);line-height:inherit;margin:0;padding:0} +.breadcrumb-item>a,a.breadcrumb-element,button.breadcrumb-element{border-radius:1px;outline-offset:1px} +.breadcrumb-item>a:hover,a.breadcrumb-element:hover,button.breadcrumb-element:hover{text-decoration:underline} +.breadcrumb-item>a:focus-visible,a.breadcrumb-element:focus-visible,button.breadcrumb-element:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,currentColor,transparent 25%)} +.nav,typo3-backend-tab-scroller{--typo3-nav-color:var(--typo3-component-color);--typo3-nav-bg:transparent;--typo3-nav-border-color:var(--typo3-component-border-color);--typo3-nav-border-width:var(--typo3-component-border-width);--typo3-nav-border-radius:var(--typo3-component-border-radius);--typo3-nav-font-size:var(--typo3-font-size);--typo3-nav-link-border-radius:var(--typo3-nav-border-radius);--typo3-nav-link-padding-x:1rem;--typo3-nav-link-padding-y:.5rem;--typo3-nav-link-color:var(--typo3-text-color-link);--typo3-nav-link-bg:transparent;--typo3-nav-link-border-color:transparent;--typo3-nav-link-hover-color:var(--typo3-text-color-link);--typo3-nav-link-hover-bg:transparent;--typo3-nav-link-hover-border-color:transparent;--typo3-nav-link-focus-color:var(--typo3-text-color-link);--typo3-nav-link-focus-bg:transparent;--typo3-nav-link-focus-border-color:transparent;--typo3-nav-link-danger-color:var(--typo3-text-color-danger);--typo3-nav-link-danger-bg:transparent;--typo3-nav-link-danger-border-color:transparent;--typo3-nav-link-active-color:var(--typo3-text-color-primary);--typo3-nav-link-active-bg:transparent;--typo3-nav-link-active-border-color:var(--typo3-state-primary-border-color);--typo3-nav-link-disabled-color:var(--typo3-text-color-variant);--typo3-nav-link-disabled-bg:transparent;--typo3-nav-link-disabled-border-color:transparent} +.nav{background-color:var(--typo3-nav-bg);display:flex;flex-wrap:wrap;font-size:var(--typo3-nav-font-size);gap:1px;list-style:none;margin-bottom:0;padding-inline:0} +.nav-link{--typo3-nav-link-state-color:var(--typo3-nav-link-color);--typo3-nav-link-state-bg:var(--typo3-nav-link-bg);--typo3-nav-link-state-border-color:var(--typo3-nav-link-border-color);background:var(--typo3-nav-link-state-bg);border:0;border-radius:var(--typo3-nav-link-border-radius);color:var(--typo3-nav-link-state-color);display:block;outline-offset:0;padding:var(--typo3-nav-link-padding-y) var(--typo3-nav-link-padding-x);position:relative;transition:color .2s ease-in-out,background-color .2s ease-in-out,border-color .2s ease-in-out;white-space:nowrap} +.nav-link:hover{--typo3-nav-link-state-color:var(--typo3-nav-link-hover-color);--typo3-nav-link-state-bg:var(--typo3-nav-link-hover-bg);--typo3-nav-link-state-border-color:var(--typo3-nav-link-hover-border-color);text-decoration:underline} +.nav-link:focus{--typo3-nav-link-state-color:var(--typo3-nav-link-focus-color);--typo3-nav-link-state-bg:var(--typo3-nav-link-focus-bg);--typo3-nav-link-state-border-color:var(--typo3-nav-link-focus-border-color);text-decoration:underline} +.nav-link:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-nav-link-state-color),var(--typo3-nav-link-state-bg))} +.nav-item.has-validation-error .nav-link{--typo3-nav-link-state-color:var(--typo3-nav-link-danger-color);--typo3-nav-link-state-bg:var(--typo3-nav-link-danger-bg);--typo3-nav-link-state-border-color:var(--typo3-nav-link-danger-border-color);-webkit-padding-start:calc(var(--typo3-nav-link-padding-x) + 16px + .25rem);padding-inline-start:calc(var(--typo3-nav-link-padding-x) + 16px + .25rem)} +.nav-item.has-validation-error .nav-link:before{background:var(--typo3-state-danger-bg) url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='11' r='1' fill='%23fff'/%3E%3Cpath fill='%23fff' d='M8.5 9h-1l-.445-4.45A.5.5 0 0 1 7.552 4h.896a.5.5 0 0 1 .497.55z'/%3E%3C/svg%3E") center/contain no-repeat;border-radius:50%;content:"";height:16px;inset-inline-start:var(--typo3-nav-link-padding-x);position:absolute;top:50%;transform:translateY(-50%);width:16px} +.nav-item.show .nav-link,.nav-link.active{text-decoration:underline;--typo3-nav-link-state-color:var(--typo3-nav-link-active-color)!important;--typo3-nav-link-state-bg:var(--typo3-nav-link-active-bg)!important;--typo3-nav-link-state-border-color:var(--typo3-nav-link-active-border-color)!important} +.nav-link.disabled,.nav-link[disabled]{--typo3-nav-link-state-color:var(--typo3-nav-link-disabled-color)!important;--typo3-nav-link-state-bg:var(--typo3-nav-link-disabled-bg)!important;--typo3-nav-link-state-border-color:var(--typo3-nav-link-disabled-border-color)!important;cursor:default;pointer-events:none} +.nav-pills{--typo3-nav-link-danger-color:var(--typo3-state-danger-color);--typo3-nav-link-danger-bg:var(--typo3-state-danger-bg);--typo3-nav-link-active-color:var(--typo3-state-primary-color);--typo3-nav-link-active-bg:var(--typo3-state-primary-bg);gap:var(--typo3-outline-width)} +.nav-pills .nav-item.show .nav-link,.nav-pills .nav-link.active{text-decoration:none} +.nav-tabs,typo3-backend-tab-scroller{--typo3-nav-bg:var(--typo3-component-bg);--typo3-outline-width:1px;--typo3-nav-border-width:2px;--typo3-nav-link-bg:var(--typo3-nav-bg);--typo3-nav-link-hover-bg:color-mix(in srgb,var(--typo3-nav-bg),currentColor 5%);--typo3-nav-link-focus-bg:color-mix(in srgb,var(--typo3-nav-bg),currentColor 7.5%);--typo3-nav-link-active-bg:color-mix(in srgb,var(--typo3-nav-bg),currentColor 7.5%)} +.nav-tabs{background:linear-gradient(var(--typo3-nav-border-color),var(--typo3-nav-border-color)) bottom/100% var(--typo3-nav-border-width) no-repeat;flex-grow:1;flex-wrap:nowrap;gap:1px;position:relative;z-index:1} +.nav-tabs .nav-link{border-end-end-radius:0;border-end-start-radius:0;margin-bottom:var(--typo3-nav-border-width);outline-offset:-4px} +.nav-tabs .nav-link:after{background-color:var(--typo3-nav-link-state-border-color);bottom:calc(var(--typo3-nav-border-width)*-1);content:"";display:block;height:var(--typo3-nav-border-width);left:0;position:absolute;right:0} +.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{text-decoration:none} +typo3-backend-tab-scroller{align-items:stretch;display:flex;position:relative} +.nav-tabs-scroll{align-items:center;background-color:var(--typo3-nav-link-bg);border:0;border-bottom:var(--typo3-nav-border-width) solid var(--typo3-nav-border-color);bottom:0;color:var(--typo3-nav-link-color);cursor:pointer;display:flex;justify-content:center;padding:0;position:absolute;top:0;transition:opacity .15s ease-in-out;width:2rem;z-index:2} +.nav-tabs-scroll:hover{background-color:var(--typo3-nav-link-hover-bg);color:var(--typo3-nav-link-hover-color)} +.nav-tabs-scroll-start{border-start-start-radius:var(--typo3-nav-link-border-radius);inset-inline-start:0;-webkit-border-end:1px solid var(--typo3-nav-border-color);border-inline-end:1px solid var(--typo3-nav-border-color)} +.nav-tabs-scroll-end{border-start-end-radius:var(--typo3-nav-link-border-radius);inset-inline-end:0;-webkit-border-start:1px solid var(--typo3-nav-border-color);border-inline-start:1px solid var(--typo3-nav-border-color)} +typo3-backend-tab-scroller>.nav-tabs{overflow-x:auto;scroll-padding-inline:2rem;scrollbar-width:none} +typo3-backend-tab-scroller>.nav-tabs::-webkit-scrollbar{display:none} +.tab-content>.tab-pane{display:none} +.tab-content>.active{display:block} +.pagination{--typo3-pagination-padding-y:var(--typo3-input-padding-y);--typo3-pagination-padding-x:var(--typo3-input-padding-x);--typo3-pagination-font-size:var(--typo3-font-size);--typo3-pagination-line-height:var(--typo3-component-line-height);--typo3-pagination-border-radius:var(--typo3-component-border-radius);--typo3-pagination-border-width:var(--typo3-component-border-width);--typo3-pagination-color:var(--typo3-component-color);--typo3-pagination-bg:var(--typo3-component-bg);--typo3-pagination-border-color:var(--typo3-component-border-color);--typo3-pagination-hover-color:var(--typo3-list-item-hover-color);--typo3-pagination-hover-bg:var(--typo3-list-item-hover-bg);--typo3-pagination-hover-border-color:var(--typo3-list-item-hover-border-color);--typo3-pagination-active-color:var(--typo3-list-item-active-color);--typo3-pagination-active-bg:var(--typo3-list-item-active-bg);--typo3-pagination-active-border-color:var(--typo3-list-item-active-border-color);--typo3-pagination-disabled-color:var(--typo3-list-item-disabled-color);--typo3-pagination-disabled-bg:var(--typo3-list-item-disabled-bg);--typo3-pagination-disabled-border-color:var(--typo3-list-item-disabled-border-color);--typo3-pagination-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out;display:flex;flex-wrap:wrap;list-style:none;padding:0;-webkit-padding-start:var(--typo3-pagination-border-width);margin-bottom:var(--typo3-spacing);padding-inline-start:var(--typo3-pagination-border-width);row-gap:2px} +.page-item{text-align:center;-webkit-margin-start:calc(var(--typo3-pagination-border-width)*-1);margin-inline-start:calc(var(--typo3-pagination-border-width)*-1)} +.page-item:first-child .page-link{border-end-start-radius:var(--typo3-pagination-border-radius);border-start-start-radius:var(--typo3-pagination-border-radius)} +.page-item:last-child .page-link{border-end-end-radius:var(--typo3-pagination-border-radius);border-start-end-radius:var(--typo3-pagination-border-radius)} +.page-link{align-items:center;background-color:var(--typo3-pagination-bg);border:var(--typo3-pagination-border-width) solid var(--typo3-pagination-border-color);color:var(--typo3-pagination-color);display:flex;font-size:var(--typo3-pagination-font-size);gap:.25rem;height:100%;justify-content:center;line-height:var(--typo3-pagination-line-height);outline-offset:0;padding:var(--typo3-pagination-padding-y) var(--typo3-pagination-padding-x);position:relative;text-decoration:none;transition:var(--typo3-pagination-transition);-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap} +@media (prefers-reduced-motion){ +.page-link{transition:none}} +.page-link:not(span):focus,.page-link:not(span):hover{--typo3-pagination-color:var(--typo3-pagination-hover-color);--typo3-pagination-bg:var(--typo3-pagination-hover-bg);--typo3-pagination-border-color:var(--typo3-pagination-hover-border-color);text-decoration:none;z-index:1} +.page-link:not(span):focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-pagination-border-color),transparent 25%);z-index:3!important} +.active>.page-link,.page-link.active{--typo3-pagination-color:var(--typo3-pagination-active-color)!important;--typo3-pagination-bg:var(--typo3-pagination-active-bg)!important;--typo3-pagination-border-color:var(--typo3-pagination-active-border-color)!important;z-index:2} +.disabled>.page-link,.page-link.disabled{--typo3-pagination-color:var(--typo3-pagination-disabled-color)!important;--typo3-pagination-bg:var(--typo3-pagination-disabled-bg)!important;--typo3-pagination-border-color:var(--typo3-pagination-disabled-border-color)!important;pointer-events:none} +.paginator-input{display:inline-block;margin:-7px 0} +.paginator-input.form-control{min-width:auto;width:auto} +:root{--icon-color-primary:currentColor;--icon-color-accent:#ff8700;--icon-size-small:16px;--icon-size-medium:32px;--icon-size-large:48px;--icon-size-mega:64px;--icon-unify-modifier:0.86;--icon-opacity-disabled:0.5} +.icon{color:var(--icon-color-primary,currentColor);display:inline-flex;flex-shrink:0;height:var(--icon-size,1em);line-height:var(--icon-size,1em);overflow:hidden;position:relative;white-space:nowrap;width:var(--icon-size,1em)} +.icon img,.icon svg{display:block;height:100%;width:100%} +.icon *{display:block;line-height:inherit} +.icon-markup{display:block;left:0;top:0} +.icon-markup,.icon-overlay{bottom:0;position:absolute;right:0;text-align:center} +.icon-overlay{height:68.75%;width:68.75%} +.icon-spin .icon-markup{animation:icon-spin 2s linear infinite} +@keyframes icon-spin{ +0%{transform:rotate(0deg)} +to{transform:rotate(1turn)}} +.icon-bidi:dir(rtl) .icon-markup{transform:scaleX(-1)} +.icon-state-disabled .icon-markup{opacity:var(--icon-opacity-disabled)} +.icon-size-small{--icon-size:var(--icon-size-small)} +.icon-size-small .icon-unify{font-size:calc(var(--icon-size)*var(--icon-unify-modifier));line-height:var(--icon-size)} +.icon-size-small .icon-overlay .icon-unify{font-size:calc(var(--icon-size)/1.6*var(--icon-unify-modifier));line-height:calc(var(--icon-size)/1.6)} +.icon-size-medium{--icon-size:var(--icon-size-medium)} +.icon-size-medium .icon-unify{font-size:calc(var(--icon-size)*var(--icon-unify-modifier));line-height:var(--icon-size)} +.icon-size-medium .icon-overlay .icon-unify{font-size:calc(var(--icon-size)/1.6*var(--icon-unify-modifier));line-height:calc(var(--icon-size)/1.6)} +.icon-size-large{--icon-size:var(--icon-size-large)} +.icon-size-large .icon-unify{font-size:calc(var(--icon-size)*var(--icon-unify-modifier));line-height:var(--icon-size)} +.icon-size-large .icon-overlay .icon-unify{font-size:calc(var(--icon-size)/1.6*var(--icon-unify-modifier));line-height:calc(var(--icon-size)/1.6)} +.icon-size-mega{--icon-size:var(--icon-size-mega)} +.icon-size-mega .icon-unify{font-size:calc(var(--icon-size)*var(--icon-unify-modifier));line-height:var(--icon-size)} +.icon-size-mega .icon-overlay .icon-unify{font-size:calc(var(--icon-size)/1.6*var(--icon-unify-modifier));line-height:calc(var(--icon-size)/1.6)} +.icon,:root,typo3-backend-icon{--icon-color-accent:var(--typo3-icons-accent)} +.icon{vertical-align:-22%} +.icon-actions-edit-copy-release,.icon-actions-edit-cut-release,.icon-status-dialog-error,.icon-status-status-current,.icon-status-status-permission-denied{--icon-color-primary:var(--typo3-text-color-danger)} +.icon-status-status-sorting-asc,.icon-status-status-sorting-desc{--icon-color-primary:var(--typo3-text-color-variant)} +.icon-status-dialog-information{--icon-color-primary:var(--typo3-text-color-info)} +.icon-status-dialog-ok,.icon-status-status-permission-granted{--icon-color-primary:var(--typo3-text-color-success)} +.icon-status-dialog-notification{--icon-color-primary:var(--typo3-text-color-notice)} +.icon-status-dialog-warning{--icon-color-primary:var(--typo3-text-color-warning)} +.icon-emphasized{--icon-emphasized-color:var(--typo3-state-default-color);--icon-emphasized-bg:var(--typo3-state-default-bg);align-items:center;background-color:var(--icon-emphasized-bg);border-radius:100%;color:var(--icon-emphasized-color);display:flex;height:2rem;justify-content:center;width:2rem} +.alert .icon-emphasized{--icon-emphasized-color:var(--typo3-alert-icon-color);--icon-emphasized-bg:var(--typo3-alert-icon-bg)} +.callout .icon-emphasized{--icon-emphasized-color:var(--typo3-callout-icon-color);--icon-emphasized-bg:var(--typo3-callout-icon-bg)} +:root{--typo3-caret-color:var(--typo3-text-color-base);--typo3-caret-rotation:0deg;--typo3-caret-size:16px;--typo3-caret-icon:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%23fff' d='m4.464 6.05-.707.707L8 11l4.243-4.243-.707-.707L8 9.586z'/%3E%3C/svg%3E")} +.caret{background-color:var(--typo3-caret-color);display:inline-block;height:var(--typo3-caret-size);line-height:1;-webkit-mask-image:var(--typo3-caret-icon);mask-image:var(--typo3-caret-icon);-webkit-mask-position:center center;mask-position:center center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:relative;rotate:var(--typo3-caret-rotation);transition:all .25s ease-in-out;vertical-align:-22%;width:var(--typo3-caret-size)} +@media (prefers-reduced-motion:reduce){ +.caret{transition:none}} +hr.spacer{border-top:none;margin-bottom:var(--typo3-spacing);margin-top:var(--typo3-spacing)} +.alert{--typo3-alert-color:inherit;--typo3-alert-bg:transparent;--typo3-alert-icon-color:inherit;--typo3-alert-icon-bg:transparent;--typo3-alert-padding-x:1rem;--typo3-alert-padding-y:1rem;--typo3-alert-padding-dismissable-end:3rem;--typo3-alert-margin-bottom:var(--typo3-spacing);--typo3-alert-border-color:color-mix(in srgb,var(--typo3-alert-bg),var(--typo3-alert-color) var(--typo3-border-mix));--typo3-alert-border-width:1px;--typo3-alert-border-radius:var(--typo3-component-border-radius);--typo3-alert-link-color:inherit;background-color:var(--typo3-alert-bg);border:var(--typo3-alert-border-width) solid var(--typo3-alert-border-color);border-radius:var(--typo3-alert-border-radius);color:var(--typo3-alert-color);margin-bottom:var(--typo3-alert-margin-bottom);padding:var(--typo3-alert-padding-y) var(--typo3-alert-padding-x);position:relative} +.alert a{color:inherit;text-decoration:underline} +.alert-inner{display:flex;gap:calc(var(--typo3-alert-padding-x)*.75)} +.alert-content{align-self:center;contain:inline-size;flex-grow:1} +.alert-title{font-size:1.12em;font-weight:700;line-height:1.2;margin-bottom:.25em} +.alert-message{word-wrap:break-word} +.alert-body,.alert-message{font-size:.9em;margin:0} +.alert-body>:last-child,.alert-message>:last-child{margin-bottom:0} +.alert-body>ul,.alert-message>ul{padding-left:0;-webkit-padding-start:1.5em;padding-inline-start:1.5em} +.alert-dismissible{-webkit-padding-start:var(--typo3-alert-padding-x);padding-inline-start:var(--typo3-alert-padding-x);-webkit-padding-end:var(--typo3-alert-padding-dismissable-end);padding-inline-end:var(--typo3-alert-padding-dismissable-end)} +.alert-dismissible .close{background:none;border:none;border-radius:.25rem;color:inherit;font-size:1.125rem;inset-block-start:.625rem;inset-inline-end:.625rem;line-height:1;opacity:.5;outline-offset:0;padding:0;position:absolute} +.alert-dismissible .close:focus,.alert-dismissible .close:hover{color:inherit;opacity:1} +.alert-dismissible .close:focus-visible{outline:2px solid var(--typo3-alert-color)} +.alert-actions{background-color:var(--typo3-alert-border-color);display:flex;flex-flow:row wrap;gap:1px;margin:var(--typo3-alert-padding-y) calc(var(--typo3-alert-padding-x)*-1) calc(var(--typo3-alert-padding-y)*-1);padding-top:1px} +.alert-dismissible .alert-actions{-webkit-margin-end:calc(var(--typo3-alert-padding-dismissable-end)*-1);margin-inline-end:calc(var(--typo3-alert-padding-dismissable-end)*-1)} +.alert-actions a{background-color:var(--typo3-alert-bg);border-end-start-radius:calc(var(--typo3-alert-border-radius) - 2px);flex-basis:25%;flex-grow:1;font-weight:700;outline-offset:-2px;padding:.5rem var(--typo3-alert-padding-x);text-align:center;text-decoration:none} +.alert-actions a:last-child{border-end-end-radius:calc(var(--typo3-alert-border-radius) - 2px);border-end-start-radius:0} +.alert-actions a:hover{background-color:color-mix(in srgb,var(--typo3-alert-bg),var(--typo3-alert-color) 5%)} +.alert-actions a:focus{background-color:color-mix(in srgb,var(--typo3-alert-bg),var(--typo3-alert-color) 10%)} +.alert-actions a:focus-visible{outline:2px solid var(--typo3-alert-color)} +.alert-actions a.executing{pointer-events:none} +.alert-actions a.disabled{opacity:.4;pointer-events:none} +.alert-primary{--typo3-alert-color:var(--typo3-surface-container-primary-text);--typo3-alert-bg:var(--typo3-surface-container-primary);--typo3-alert-icon-color:var(--typo3-state-primary-color);--typo3-alert-icon-bg:var(--typo3-state-primary-bg)} +.alert-secondary{--typo3-alert-color:var(--typo3-surface-container-secondary-text);--typo3-alert-bg:var(--typo3-surface-container-secondary);--typo3-alert-icon-color:var(--typo3-state-secondary-color);--typo3-alert-icon-bg:var(--typo3-state-secondary-bg)} +.alert-info{--typo3-alert-color:var(--typo3-surface-container-info-text);--typo3-alert-bg:var(--typo3-surface-container-info);--typo3-alert-icon-color:var(--typo3-state-info-color);--typo3-alert-icon-bg:var(--typo3-state-info-bg)} +.alert-success{--typo3-alert-color:var(--typo3-surface-container-success-text);--typo3-alert-bg:var(--typo3-surface-container-success);--typo3-alert-icon-color:var(--typo3-state-success-color);--typo3-alert-icon-bg:var(--typo3-state-success-bg)} +.alert-warning{--typo3-alert-color:var(--typo3-surface-container-warning-text);--typo3-alert-bg:var(--typo3-surface-container-warning);--typo3-alert-icon-color:var(--typo3-state-warning-color);--typo3-alert-icon-bg:var(--typo3-state-warning-bg)} +.alert-danger{--typo3-alert-color:var(--typo3-surface-container-danger-text);--typo3-alert-bg:var(--typo3-surface-container-danger);--typo3-alert-icon-color:var(--typo3-state-danger-color);--typo3-alert-icon-bg:var(--typo3-state-danger-bg)} +.alert-notice{--typo3-alert-color:var(--typo3-surface-container-notice-text);--typo3-alert-bg:var(--typo3-surface-container-notice);--typo3-alert-icon-color:var(--typo3-state-notice-color);--typo3-alert-icon-bg:var(--typo3-state-notice-bg)} +.alert-default{--typo3-alert-color:var(--typo3-surface-container-default-text);--typo3-alert-bg:var(--typo3-surface-container-default);--typo3-alert-icon-color:var(--typo3-state-default-color);--typo3-alert-icon-bg:var(--typo3-state-default-bg)} +typo3-backend-formengine-suggest-result-container{inset-inline-start:0;max-width:calc(100dvw - 20px);position:absolute;z-index:var(--typo3-zindex-dropdown)} +typo3-backend-formengine-suggest-result-list{border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:var(--typo3-component-border-radius);box-shadow:var(--typo3-component-box-shadow);flex-direction:column;gap:1px;padding:1px} +typo3-backend-formengine-suggest-result-item,typo3-backend-formengine-suggest-result-list{background-color:var(--typo3-component-bg);display:flex;font-size:var(--typo3-component-font-size);line-height:var(--typo3-component-line-height)} +typo3-backend-formengine-suggest-result-item{border-radius:calc(var(--typo3-component-border-radius) - var(--typo3-component-border-width));color:var(--typo3-component-color);cursor:pointer;gap:.5em;padding:var(--typo3-list-item-padding-y) var(--typo3-list-item-padding-x)} +typo3-backend-formengine-suggest-result-item:focus,typo3-backend-formengine-suggest-result-item:hover{outline-offset:-1px;z-index:1} +typo3-backend-formengine-suggest-result-item:hover{background-color:var(--typo3-list-item-hover-bg);color:var(--typo3-list-item-hover-color);outline:1px solid var(--typo3-list-item-hover-border-color)} +typo3-backend-formengine-suggest-result-item:focus{background-color:var(--typo3-list-item-focus-bg);color:var(--typo3-list-item-focus-color);outline:1px solid var(--typo3-list-item-focus-border-color)} +typo3-backend-formengine-suggest-result-item .formengine-suggest-result-item-icon{flex-grow:0;flex-shrink:0} +typo3-backend-formengine-suggest-result-item .formengine-suggest-result-item-label{flex-grow:1} +typo3-backend-formengine-suggest-result-item .formengine-suggest-result-item-label .small,typo3-backend-formengine-suggest-result-item .formengine-suggest-result-item-label small{opacity:.5} +:root{--typo3-badge-primary-color:var(--typo3-state-primary-color);--typo3-badge-primary-bg:var(--typo3-state-primary-bg);--typo3-badge-primary-border-color:var(--typo3-state-primary-border-color);--typo3-badge-primary-link-hover-color:var(--typo3-state-primary-hover-color);--typo3-badge-primary-link-hover-bg:var(--typo3-state-primary-hover-bg);--typo3-badge-primary-link-hover-border-color:var(--typo3-state-primary-hover-border-color);--typo3-badge-primary-link-focus-color:var(--typo3-state-primary-focus-color);--typo3-badge-primary-link-focus-bg:var(--typo3-state-primary-focus-bg);--typo3-badge-primary-link-focus-border-color:var(--typo3-state-primary-focus-border-color);--typo3-badge-secondary-color:var(--typo3-state-secondary-color);--typo3-badge-secondary-bg:var(--typo3-state-secondary-bg);--typo3-badge-secondary-border-color:var(--typo3-state-secondary-border-color);--typo3-badge-secondary-link-hover-color:var(--typo3-state-secondary-hover-color);--typo3-badge-secondary-link-hover-bg:var(--typo3-state-secondary-hover-bg);--typo3-badge-secondary-link-hover-border-color:var(--typo3-state-secondary-hover-border-color);--typo3-badge-secondary-link-focus-color:var(--typo3-state-secondary-focus-color);--typo3-badge-secondary-link-focus-bg:var(--typo3-state-secondary-focus-bg);--typo3-badge-secondary-link-focus-border-color:var(--typo3-state-secondary-focus-border-color);--typo3-badge-info-color:var(--typo3-state-info-color);--typo3-badge-info-bg:var(--typo3-state-info-bg);--typo3-badge-info-border-color:var(--typo3-state-info-border-color);--typo3-badge-info-link-hover-color:var(--typo3-state-info-hover-color);--typo3-badge-info-link-hover-bg:var(--typo3-state-info-hover-bg);--typo3-badge-info-link-hover-border-color:var(--typo3-state-info-hover-border-color);--typo3-badge-info-link-focus-color:var(--typo3-state-info-focus-color);--typo3-badge-info-link-focus-bg:var(--typo3-state-info-focus-bg);--typo3-badge-info-link-focus-border-color:var(--typo3-state-info-focus-border-color);--typo3-badge-success-color:var(--typo3-state-success-color);--typo3-badge-success-bg:var(--typo3-state-success-bg);--typo3-badge-success-border-color:var(--typo3-state-success-border-color);--typo3-badge-success-link-hover-color:var(--typo3-state-success-hover-color);--typo3-badge-success-link-hover-bg:var(--typo3-state-success-hover-bg);--typo3-badge-success-link-hover-border-color:var(--typo3-state-success-hover-border-color);--typo3-badge-success-link-focus-color:var(--typo3-state-success-focus-color);--typo3-badge-success-link-focus-bg:var(--typo3-state-success-focus-bg);--typo3-badge-success-link-focus-border-color:var(--typo3-state-success-focus-border-color);--typo3-badge-warning-color:var(--typo3-state-warning-color);--typo3-badge-warning-bg:var(--typo3-state-warning-bg);--typo3-badge-warning-border-color:var(--typo3-state-warning-border-color);--typo3-badge-warning-link-hover-color:var(--typo3-state-warning-hover-color);--typo3-badge-warning-link-hover-bg:var(--typo3-state-warning-hover-bg);--typo3-badge-warning-link-hover-border-color:var(--typo3-state-warning-hover-border-color);--typo3-badge-warning-link-focus-color:var(--typo3-state-warning-focus-color);--typo3-badge-warning-link-focus-bg:var(--typo3-state-warning-focus-bg);--typo3-badge-warning-link-focus-border-color:var(--typo3-state-warning-focus-border-color);--typo3-badge-danger-color:var(--typo3-state-danger-color);--typo3-badge-danger-bg:var(--typo3-state-danger-bg);--typo3-badge-danger-border-color:var(--typo3-state-danger-border-color);--typo3-badge-danger-link-hover-color:var(--typo3-state-danger-hover-color);--typo3-badge-danger-link-hover-bg:var(--typo3-state-danger-hover-bg);--typo3-badge-danger-link-hover-border-color:var(--typo3-state-danger-hover-border-color);--typo3-badge-danger-link-focus-color:var(--typo3-state-danger-focus-color);--typo3-badge-danger-link-focus-bg:var(--typo3-state-danger-focus-bg);--typo3-badge-danger-link-focus-border-color:var(--typo3-state-danger-focus-border-color);--typo3-badge-notice-color:var(--typo3-state-notice-color);--typo3-badge-notice-bg:var(--typo3-state-notice-bg);--typo3-badge-notice-border-color:var(--typo3-state-notice-border-color);--typo3-badge-notice-link-hover-color:var(--typo3-state-notice-hover-color);--typo3-badge-notice-link-hover-bg:var(--typo3-state-notice-hover-bg);--typo3-badge-notice-link-hover-border-color:var(--typo3-state-notice-hover-border-color);--typo3-badge-notice-link-focus-color:var(--typo3-state-notice-focus-color);--typo3-badge-notice-link-focus-bg:var(--typo3-state-notice-focus-bg);--typo3-badge-notice-link-focus-border-color:var(--typo3-state-notice-focus-border-color);--typo3-badge-default-color:var(--typo3-state-default-color);--typo3-badge-default-bg:var(--typo3-state-default-bg);--typo3-badge-default-border-color:var(--typo3-state-default-border-color);--typo3-badge-default-link-hover-color:var(--typo3-state-default-hover-color);--typo3-badge-default-link-hover-bg:var(--typo3-state-default-hover-bg);--typo3-badge-default-link-hover-border-color:var(--typo3-state-default-hover-border-color);--typo3-badge-default-link-focus-color:var(--typo3-state-default-focus-color);--typo3-badge-default-link-focus-bg:var(--typo3-state-default-focus-bg);--typo3-badge-default-link-focus-border-color:var(--typo3-state-default-focus-border-color)} +.badge{--typo3-badge-color:var(--typo3-badge-default-color);--typo3-badge-bg:var(--typo3-badge-default-bg);--typo3-badge-border-color:var(--typo3-badge-default-border-color);--typo3-badge-link-hover-color:var(--typo3-badge-default-link-hover-color);--typo3-badge-link-hover-bg:var(--typo3-badge-default-link-hover-bg);--typo3-badge-link-hover-border-color:var(--typo3-badge-default-link-hover-border-color);--typo3-badge-link-focus-color:var(--typo3-badge-default-link-focus-color);--typo3-badge-link-focus-bg:var(--typo3-badge-default-link-focus-bg);--typo3-badge-link-focus-border-color:var(--typo3-badge-default-link-focus-border-color);--typo3-badge-padding-y:calc(0.34375em - 1px);--typo3-badge-padding-x:.65em;--typo3-badge-border-radius:.75em;--typo3-badge-font-size:0.91667em;align-items:center;background-color:var(--typo3-badge-bg);border:1px solid var(--typo3-badge-border-color);border-radius:var(--typo3-badge-border-radius);color:var(--typo3-badge-color);display:inline-flex;font-size:var(--typo3-badge-font-size);font-weight:600;gap:.35em;line-height:1;outline-offset:0;padding:var(--typo3-badge-padding-y) var(--typo3-badge-padding-x);text-align:center;vertical-align:middle;white-space:nowrap} +.badge[href]{text-decoration:none} +.badge[href]:hover{--typo3-badge-color:var(--typo3-badge-link-hover-color);--typo3-badge-bg:var(--typo3-badge-link-hover-bg);--typo3-badge-border-color:var(--typo3-badge-link-hover-border-color)} +.badge[href]:focus{--typo3-badge-color:var(--typo3-badge-link-focus-color);--typo3-badge-bg:var(--typo3-badge-link-focus-bg);--typo3-badge-border-color:var(--typo3-badge-link-focus-border-color)} +.badge[href]:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-badge-link-focus-bg),transparent 25%)} +.badge-primary{--typo3-badge-color:var(--typo3-badge-primary-color);--typo3-badge-bg:var(--typo3-badge-primary-bg);--typo3-badge-border-color:var(--typo3-badge-primary-border-color);--typo3-badge-link-hover-color:var(--typo3-badge-primary-link-hover-color);--typo3-badge-link-hover-bg:var(--typo3-badge-primary-link-hover-bg);--typo3-badge-link-hover-border-color:var(--typo3-badge-primary-link-hover-border-color);--typo3-badge-link-focus-color:var(--typo3-badge-primary-link-focus-color);--typo3-badge-link-focus-bg:var(--typo3-badge-primary-link-focus-bg);--typo3-badge-link-focus-border-color:var(--typo3-badge-primary-link-focus-border-color)} +.badge-secondary{--typo3-badge-color:var(--typo3-badge-secondary-color);--typo3-badge-bg:var(--typo3-badge-secondary-bg);--typo3-badge-border-color:var(--typo3-badge-secondary-border-color);--typo3-badge-link-hover-color:var(--typo3-badge-secondary-link-hover-color);--typo3-badge-link-hover-bg:var(--typo3-badge-secondary-link-hover-bg);--typo3-badge-link-hover-border-color:var(--typo3-badge-secondary-link-hover-border-color);--typo3-badge-link-focus-color:var(--typo3-badge-secondary-link-focus-color);--typo3-badge-link-focus-bg:var(--typo3-badge-secondary-link-focus-bg);--typo3-badge-link-focus-border-color:var(--typo3-badge-secondary-link-focus-border-color)} +.badge-info{--typo3-badge-color:var(--typo3-badge-info-color);--typo3-badge-bg:var(--typo3-badge-info-bg);--typo3-badge-border-color:var(--typo3-badge-info-border-color);--typo3-badge-link-hover-color:var(--typo3-badge-info-link-hover-color);--typo3-badge-link-hover-bg:var(--typo3-badge-info-link-hover-bg);--typo3-badge-link-hover-border-color:var(--typo3-badge-info-link-hover-border-color);--typo3-badge-link-focus-color:var(--typo3-badge-info-link-focus-color);--typo3-badge-link-focus-bg:var(--typo3-badge-info-link-focus-bg);--typo3-badge-link-focus-border-color:var(--typo3-badge-info-link-focus-border-color)} +.badge-success{--typo3-badge-color:var(--typo3-badge-success-color);--typo3-badge-bg:var(--typo3-badge-success-bg);--typo3-badge-border-color:var(--typo3-badge-success-border-color);--typo3-badge-link-hover-color:var(--typo3-badge-success-link-hover-color);--typo3-badge-link-hover-bg:var(--typo3-badge-success-link-hover-bg);--typo3-badge-link-hover-border-color:var(--typo3-badge-success-link-hover-border-color);--typo3-badge-link-focus-color:var(--typo3-badge-success-link-focus-color);--typo3-badge-link-focus-bg:var(--typo3-badge-success-link-focus-bg);--typo3-badge-link-focus-border-color:var(--typo3-badge-success-link-focus-border-color)} +.badge-warning{--typo3-badge-color:var(--typo3-badge-warning-color);--typo3-badge-bg:var(--typo3-badge-warning-bg);--typo3-badge-border-color:var(--typo3-badge-warning-border-color);--typo3-badge-link-hover-color:var(--typo3-badge-warning-link-hover-color);--typo3-badge-link-hover-bg:var(--typo3-badge-warning-link-hover-bg);--typo3-badge-link-hover-border-color:var(--typo3-badge-warning-link-hover-border-color);--typo3-badge-link-focus-color:var(--typo3-badge-warning-link-focus-color);--typo3-badge-link-focus-bg:var(--typo3-badge-warning-link-focus-bg);--typo3-badge-link-focus-border-color:var(--typo3-badge-warning-link-focus-border-color)} +.badge-danger{--typo3-badge-color:var(--typo3-badge-danger-color);--typo3-badge-bg:var(--typo3-badge-danger-bg);--typo3-badge-border-color:var(--typo3-badge-danger-border-color);--typo3-badge-link-hover-color:var(--typo3-badge-danger-link-hover-color);--typo3-badge-link-hover-bg:var(--typo3-badge-danger-link-hover-bg);--typo3-badge-link-hover-border-color:var(--typo3-badge-danger-link-hover-border-color);--typo3-badge-link-focus-color:var(--typo3-badge-danger-link-focus-color);--typo3-badge-link-focus-bg:var(--typo3-badge-danger-link-focus-bg);--typo3-badge-link-focus-border-color:var(--typo3-badge-danger-link-focus-border-color)} +.badge-notice{--typo3-badge-color:var(--typo3-badge-notice-color);--typo3-badge-bg:var(--typo3-badge-notice-bg);--typo3-badge-border-color:var(--typo3-badge-notice-border-color);--typo3-badge-link-hover-color:var(--typo3-badge-notice-link-hover-color);--typo3-badge-link-hover-bg:var(--typo3-badge-notice-link-hover-bg);--typo3-badge-link-hover-border-color:var(--typo3-badge-notice-link-hover-border-color);--typo3-badge-link-focus-color:var(--typo3-badge-notice-link-focus-color);--typo3-badge-link-focus-bg:var(--typo3-badge-notice-link-focus-bg);--typo3-badge-link-focus-border-color:var(--typo3-badge-notice-link-focus-border-color)} +.badge-default{--typo3-badge-color:var(--typo3-badge-default-color);--typo3-badge-bg:var(--typo3-badge-default-bg);--typo3-badge-border-color:var(--typo3-badge-default-border-color);--typo3-badge-link-hover-color:var(--typo3-badge-default-link-hover-color);--typo3-badge-link-hover-bg:var(--typo3-badge-default-link-hover-bg);--typo3-badge-link-hover-border-color:var(--typo3-badge-default-link-hover-border-color);--typo3-badge-link-focus-color:var(--typo3-badge-default-link-focus-color);--typo3-badge-link-focus-bg:var(--typo3-badge-default-link-focus-bg);--typo3-badge-link-focus-border-color:var(--typo3-badge-default-link-focus-border-color)} +.badge-stable{--typo3-badge-color:var(--typo3-badge-success-color);--typo3-badge-bg:var(--typo3-badge-success-bg);--typo3-badge-border-color:var(--typo3-badge-success-border-color);--typo3-badge-link-hover-color:var(--typo3-badge-success-link-hover-color);--typo3-badge-link-hover-bg:var(--typo3-badge-success-link-hover-bg);--typo3-badge-link-hover-border-color:var(--typo3-badge-success-link-hover-border-color);--typo3-badge-link-focus-color:var(--typo3-badge-success-link-focus-color);--typo3-badge-link-focus-bg:var(--typo3-badge-success-link-focus-bg);--typo3-badge-link-focus-border-color:var(--typo3-badge-success-link-focus-border-color)} +.badge-experimental{--typo3-badge-color:var(--typo3-badge-info-color);--typo3-badge-bg:var(--typo3-badge-info-bg);--typo3-badge-border-color:var(--typo3-badge-info-border-color);--typo3-badge-link-hover-color:var(--typo3-badge-info-link-hover-color);--typo3-badge-link-hover-bg:var(--typo3-badge-info-link-hover-bg);--typo3-badge-link-hover-border-color:var(--typo3-badge-info-link-hover-border-color);--typo3-badge-link-focus-color:var(--typo3-badge-info-link-focus-color);--typo3-badge-link-focus-bg:var(--typo3-badge-info-link-focus-bg);--typo3-badge-link-focus-border-color:var(--typo3-badge-info-link-focus-border-color)} +.badge-beta{--typo3-badge-color:var(--typo3-badge-warning-color);--typo3-badge-bg:var(--typo3-badge-warning-bg);--typo3-badge-border-color:var(--typo3-badge-warning-border-color);--typo3-badge-link-hover-color:var(--typo3-badge-warning-link-hover-color);--typo3-badge-link-hover-bg:var(--typo3-badge-warning-link-hover-bg);--typo3-badge-link-hover-border-color:var(--typo3-badge-warning-link-hover-border-color);--typo3-badge-link-focus-color:var(--typo3-badge-warning-link-focus-color);--typo3-badge-link-focus-bg:var(--typo3-badge-warning-link-focus-bg);--typo3-badge-link-focus-border-color:var(--typo3-badge-warning-link-focus-border-color)} +.badge-alpha,.badge-deprecated{--typo3-badge-color:var(--typo3-badge-danger-color);--typo3-badge-bg:var(--typo3-badge-danger-bg);--typo3-badge-border-color:var(--typo3-badge-danger-border-color);--typo3-badge-link-hover-color:var(--typo3-badge-danger-link-hover-color);--typo3-badge-link-hover-bg:var(--typo3-badge-danger-link-hover-bg);--typo3-badge-link-hover-border-color:var(--typo3-badge-danger-link-hover-border-color);--typo3-badge-link-focus-color:var(--typo3-badge-danger-link-focus-color);--typo3-badge-link-focus-bg:var(--typo3-badge-danger-link-focus-bg);--typo3-badge-link-focus-border-color:var(--typo3-badge-danger-link-focus-border-color)} +.badge-space-start{-webkit-margin-start:1em;margin-inline-start:1em} +.badge-space-end{-webkit-margin-end:1em;margin-inline-end:1em} +.badge-pill{--typo3-badge-border-radius:1em} +.badge-list{display:flex;flex-wrap:wrap;gap:.25rem;list-style:none;margin:0;padding:0} +typo3-backend-status-indicator{--typo3-status-indicator-color:var(--typo3-status-indicator-default-color);--typo3-status-indicator-dot-size:8px;--typo3-status-indicator-ring-offset:1px;--typo3-status-indicator-ring-width:2px;align-items:center;display:inline-flex;flex-shrink:0;height:var(--typo3-status-indicator-dot-size);justify-content:center;width:var(--typo3-status-indicator-dot-size);-webkit-margin-end:var(--typo3-status-indicator-ring-width);margin-inline-end:var(--typo3-status-indicator-ring-width)} +.badge typo3-backend-status-indicator{--typo3-status-indicator-dot-size:6px;-webkit-margin-end:0;margin-inline-end:0} +typo3-backend-status-indicator .status-indicator{align-items:center;background-color:var(--typo3-status-indicator-color);border-radius:50%;display:inline-flex;flex-shrink:0;height:var(--typo3-status-indicator-dot-size);justify-content:center;position:relative;width:var(--typo3-status-indicator-dot-size)} +typo3-backend-status-indicator .status-indicator-info{--typo3-status-indicator-color:var(--typo3-status-indicator-info-color)} +typo3-backend-status-indicator .status-indicator-success{--typo3-status-indicator-color:var(--typo3-status-indicator-success-color)} +typo3-backend-status-indicator .status-indicator-warning{--typo3-status-indicator-color:var(--typo3-status-indicator-warning-color)} +typo3-backend-status-indicator .status-indicator-danger{--typo3-status-indicator-color:var(--typo3-status-indicator-danger-color)} +typo3-backend-status-indicator .status-indicator-notice{--typo3-status-indicator-color:var(--typo3-status-indicator-notice-color)} +typo3-backend-status-indicator .status-indicator-primary{--typo3-status-indicator-color:var(--typo3-status-indicator-primary-color)} +typo3-backend-status-indicator .status-indicator-secondary{--typo3-status-indicator-color:var(--typo3-status-indicator-secondary-color)} +typo3-backend-status-indicator .status-indicator-default{--typo3-status-indicator-color:var(--typo3-status-indicator-default-color)} +typo3-backend-status-indicator .status-indicator-active,typo3-backend-status-indicator .status-indicator-online{--typo3-status-indicator-color:var(--typo3-status-indicator-success-color)} +typo3-backend-status-indicator .status-indicator-running{--typo3-status-indicator-color:var(--typo3-status-indicator-info-color)} +typo3-backend-status-indicator .status-indicator-disabled{--typo3-status-indicator-color:var(--typo3-status-indicator-danger-color)} +typo3-backend-status-indicator .status-indicator-live:after{animation:status-indicator-live 1.8s ease-in-out infinite;border-radius:50%;content:"";inset:0;opacity:.5;outline:var(--typo3-status-indicator-ring-width) solid var(--typo3-status-indicator-color);outline-offset:var(--typo3-status-indicator-ring-offset);position:absolute} +@media (prefers-reduced-motion:reduce){ +typo3-backend-status-indicator .status-indicator-live:after{animation:none}} +typo3-backend-status-indicator .status-indicator-loading:after{animation:status-indicator-loading .9s linear infinite;background:conic-gradient(from 90deg,transparent,var(--typo3-status-indicator-color));border-radius:50%;content:"";inset:calc((var(--typo3-status-indicator-ring-width) + var(--typo3-status-indicator-ring-offset))*-1);-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - var(--typo3-status-indicator-ring-width)),#000 calc(100% - var(--typo3-status-indicator-ring-width)));mask:radial-gradient(farthest-side,transparent calc(100% - var(--typo3-status-indicator-ring-width)),#000 calc(100% - var(--typo3-status-indicator-ring-width)));position:absolute} +@media (prefers-reduced-motion:reduce){ +typo3-backend-status-indicator .status-indicator-loading:after{animation:none}} +@keyframes status-indicator-live{ +0%{opacity:.1} +50%{opacity:.5} +to{opacity:.1}} +@keyframes status-indicator-loading{ +to{transform:rotate(1turn)}} +.form-section{--typo3-form-section-border-width:1px;--typo3-form-section-border-color:color-mix(in srgb,var(--typo3-surface-container-low),var(--typo3-text-color-base) var(--typo3-border-mix));--typo3-form-section-padding-y:1.5rem;--typo3-form-section-padding-x:0;--typo3-form-section-headline-font-size:1rem;--treelist-bg:var(--typo3-surface-container-low);container-type:inline-size;padding:var(--typo3-form-section-padding-y) var(--typo3-form-section-padding-x)} +.form-section+.form-section{border-top:var(--typo3-form-section-border-width) solid var(--typo3-form-section-border-color)} +.form-section>:last-child{margin-bottom:0} +.tab-pane>.form-section:last-child{padding-bottom:0} +.form-section-headline{font-size:var(--typo3-form-section-headline-font-size);margin-bottom:var(--typo3-spacing);margin-top:0} +.form-section-headline+.form-section-description{margin-bottom:var(--typo3-spacing);margin-top:calc(var(--typo3-spacing)/2*-1)} +.form-section-description{color:var(--typo3-text-color-variant)} +.form-group{margin-bottom:var(--typo3-spacing)} +.form-group>:first-child{margin-top:0} +.form-group>:last-child{margin-bottom:0} +.form-group-dashed+.form-group-dashed{border-top:var(--typo3-component-border-width) dashed var(--typo3-component-border-color);padding-top:var(--typo3-spacing)} +.form-multigroup-wrap{display:grid;gap:var(--typo3-spacing);grid-template-columns:1fr 1fr;width:100%} +.form-grid{--typo3-form-grid-columns:1;--typo3-form-grid-min-col-width:250px;--typo3-form-grid-spacing:var(--typo3-spacing);--typo3-form-grid-spacing-y:var(--typo3-form-grid-spacing);--typo3-form-grid-total-gap:calc((var(--typo3-form-grid-columns) - 1)*var(--typo3-form-grid-spacing));--typo3-form-grid-ideal-col-width:calc((100% - var(--typo3-form-grid-total-gap))/var(--typo3-form-grid-columns));--typo3-form-grid-col-width:clamp(var(--typo3-form-grid-min-col-width),var(--typo3-form-grid-ideal-col-width),100%);display:grid;gap:var(--typo3-form-grid-spacing-y) var(--typo3-form-grid-spacing);margin-bottom:var(--typo3-form-grid-spacing)} +.form-grid>*{margin-bottom:0;margin-top:0} +@container (min-width: 600px){ +.form-grid{grid-template-columns:repeat(auto-fit,minmax(var(--typo3-form-grid-col-width),1fr))}} +.form-grid:has(>.form-group>.form-check:only-child){--typo3-form-grid-spacing-y:calc(var(--typo3-form-grid-spacing)/2)} +.form-row{align-items:flex-end;display:flex;flex-wrap:wrap;gap:var(--typo3-spacing);margin-bottom:var(--typo3-spacing)} +.form-row>.form-group,.form-row>.form-group>[class*=form-row]{margin-bottom:0} +@media (min-width:576px){ +.form-row-sm{align-items:flex-end;display:flex;flex-wrap:wrap;gap:var(--typo3-spacing);margin-bottom:var(--typo3-spacing)} +.form-row-sm>.form-group,.form-row-sm>.form-group>[class*=form-row]{margin-bottom:0}} +@media (min-width:768px){ +.form-row-md{align-items:flex-end;display:flex;flex-wrap:wrap;gap:var(--typo3-spacing);margin-bottom:var(--typo3-spacing)} +.form-row-md>.form-group,.form-row-md>.form-group>[class*=form-row]{margin-bottom:0}} +@media (min-width:992px){ +.form-row-lg{align-items:flex-end;display:flex;flex-wrap:wrap;gap:var(--typo3-spacing);margin-bottom:var(--typo3-spacing)} +.form-row-lg>.form-group,.form-row-lg>.form-group>[class*=form-row]{margin-bottom:0}} +@media (min-width:1200px){ +.form-row-xl{align-items:flex-end;display:flex;flex-wrap:wrap;gap:var(--typo3-spacing);margin-bottom:var(--typo3-spacing)} +.form-row-xl>.form-group,.form-row-xl>.form-group>[class*=form-row]{margin-bottom:0}} +@media (min-width:1400px){ +.form-row-xxl{align-items:flex-end;display:flex;flex-wrap:wrap;gap:var(--typo3-spacing);margin-bottom:var(--typo3-spacing)} +.form-row-xxl>.form-group,.form-row-xxl>.form-group>[class*=form-row]{margin-bottom:0}} +.form-label{align-items:center;color:inherit;display:inline-flex;float:none;font-size:var(--typo3-font-size);font-style:normal;font-weight:700;gap:.25rem;margin-bottom:calc(var(--typo3-spacing)*.25);word-break:break-word} +.form-label code{display:contents} +.form-labellabel{cursor:pointer} +.form-control{--typo3-form-control-font-size:var(--typo3-input-font-size);--typo3-form-control-line-height:var(--typo3-input-line-height);--typo3-form-control-padding-x:var(--typo3-input-padding-x);--typo3-form-control-padding-y:var(--typo3-input-padding-y);--typo3-form-control-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;--typo3-form-control-color:var(--typo3-input-color);--typo3-form-control-placeholder-color:color-mix(in srgb,var(--typo3-input-color),transparent 35%);--typo3-form-control-bg:var(--typo3-input-bg);--typo3-form-control-border-radius:var(--typo3-input-border-radius);--typo3-form-control-border-width:var(--typo3-input-border-width);--typo3-form-control-border-color:var(--typo3-input-border-color);--typo3-form-control-hover-color:var(--typo3-input-hover-color);--typo3-form-control-hover-bg:var(--typo3-input-hover-bg);--typo3-form-control-hover-border-color:var(--typo3-input-hover-border-color);--typo3-form-control-focus-color:var(--typo3-input-focus-color);--typo3-form-control-focus-bg:var(--typo3-input-focus-bg);--typo3-form-control-focus-border-color:var(--typo3-input-focus-border-color);--typo3-form-control-disabled-color:var(--typo3-input-disabled-color);--typo3-form-control-disabled-bg:var(--typo3-input-disabled-bg);--typo3-form-control-disabled-border-color:var(--typo3-input-disabled-border-color);--typo3-form-control-disabled-opacity:var(--typo3-input-disabled-opacity);--typo3-form-control-min-height:calc(var(--typo3-form-control-padding-y)*2 + var(--typo3-form-control-font-size)*var(--typo3-form-control-line-height) + var(--typo3-form-control-border-width)*2);--typo3-form-control-icon-size:16px;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-clip:padding-box;background-color:var(--typo3-form-control-bg);border:var(--typo3-form-control-border-width) solid var(--typo3-form-control-border-color);border-radius:var(--typo3-form-control-border-radius);color:var(--typo3-form-control-color);display:block;font-size:var(--typo3-form-control-font-size);font-weight:400;line-height:var(--typo3-form-control-line-height);min-height:var(--typo3-form-control-min-height);min-width:120px;outline-offset:0;padding:var(--typo3-form-control-padding-y) var(--typo3-form-control-padding-x);transition:var(--typo3-form-control-transition);width:100%} +.form-control[type=search]{-webkit-padding-start:calc(var(--typo3-form-control-padding-x)*1.5 + var(--typo3-form-control-icon-size));background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='gray' d='M13.92 15c-.29 0-.56-.12-.76-.32l-2.89-2.88c-.98.68-2.16 1.04-3.36 1.04C3.65 12.85 1 10.2 1 6.92 1 3.65 3.65 1 6.92 1s5.92 2.65 5.92 5.92c0 1.19-.36 2.37-1.04 3.36l2.89 2.89c.19.19.31.47.31.76 0 .58-.49 1.07-1.08 1.07m-7-12.58c-2.48 0-4.5 2.02-4.5 4.5s2.02 4.5 4.5 4.5 4.5-2.02 4.5-4.5-2.02-4.5-4.5-4.5'/%3E%3C/svg%3E");background-position:var(--typo3-position-start) var(--typo3-form-control-padding-x) top 50%;background-repeat:no-repeat;background-size:var(--typo3-form-control-icon-size);padding-inline-start:calc(var(--typo3-form-control-padding-x)*1.5 + var(--typo3-form-control-icon-size))} +.form-control[type=search]::-webkit-search-cancel-button{align-self:center;-webkit-appearance:none;background-color:currentColor;height:var(--typo3-form-control-icon-size);margin:0;-webkit-mask-image:var(--typo3-icons-close);mask-image:var(--typo3-icons-close);opacity:.3;width:var(--typo3-form-control-icon-size)} +.form-control[type=search]::-webkit-search-cancel-button:hover{opacity:.5} +.form-control[type=file]{overflow:hidden} +.form-control[type=file]:not([disabled]):not([readonly]){cursor:pointer} +.form-control:hover{--typo3-form-control-color:var(--typo3-form-control-hover-color);--typo3-form-control-bg:var(--typo3-form-control-hover-bg);--typo3-form-control-border-color:var(--typo3-form-control-hover-border-color);z-index:2!important} +.form-control:focus{--typo3-form-control-color:var(--typo3-form-control-focus-color);--typo3-form-control-bg:var(--typo3-form-control-focus-bg);--typo3-form-control-border-color:var(--typo3-form-control-focus-border-color);outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-form-control-focus-border-color),transparent 25%);z-index:3!important} +.form-control::-moz-placeholder{color:var(--typo3-form-control-placeholder-color);opacity:1} +.form-control::placeholder{color:var(--typo3-form-control-placeholder-color);opacity:1} +.form-control[disabled],.form-control[readonly]{--typo3-form-control-color:color-mix(in srgb,var(--typo3-form-control-disabled-color),transparent calc((1 - var(--typo3-input-disabled-opacity))*100%));--typo3-form-control-bg:var(--typo3-form-control-disabled-bg);--typo3-form-control-border-color:var(--typo3-form-control-disabled-border-color)} +.form-control[disabled]{cursor:not-allowed} +.form-control ::-ms-clear,.form-control::-ms-reveal{display:none} +.form-control::-webkit-file-upload-button{margin:calc(var(--typo3-form-control-padding-y)*-1) calc(var(--typo3-form-control-padding-x)*-1);padding:var(--typo3-form-control-padding-y) var(--typo3-form-control-padding-x);-webkit-margin-end:var(--typo3-form-control-padding-x);background-color:color-mix(in srgb,var(--typo3-form-control-bg),var(--typo3-form-control-color) 7.5%);border:0 solid;border-color:inherit;border-inline-end-width:var(--typo3-form-control-border-width);border-radius:0;color:var(--typo3-form-control-color);margin-inline-end:var(--typo3-form-control-padding-x);pointer-events:none;-webkit-transition:var(--typo3-form-control-transition);transition:var(--typo3-form-control-transition)} +.form-control::file-selector-button{margin:calc(var(--typo3-form-control-padding-y)*-1) calc(var(--typo3-form-control-padding-x)*-1);padding:var(--typo3-form-control-padding-y) var(--typo3-form-control-padding-x);-webkit-margin-end:var(--typo3-form-control-padding-x);background-color:color-mix(in srgb,var(--typo3-form-control-bg),var(--typo3-form-control-color) 7.5%);border:0 solid;border-color:inherit;border-inline-end-width:var(--typo3-form-control-border-width);border-radius:0;color:var(--typo3-form-control-color);margin-inline-end:var(--typo3-form-control-padding-x);pointer-events:none;transition:var(--typo3-form-control-transition)} +.form-control:hover:not([disabled]):not([readonly])::-webkit-file-upload-button{background-color:color-mix(in srgb,var(--typo3-form-control-bg),var(--typo3-form-control-color) 15%)} +.form-control:hover:not([disabled]):not([readonly])::file-selector-button{background-color:color-mix(in srgb,var(--typo3-form-control-bg),var(--typo3-form-control-color) 15%)} +.form-control-sm{--typo3-form-control-padding-y:var(--typo3-input-sm-padding-y);--typo3-form-control-padding-x:var(--typo3-input-sm-padding-x);--typo3-form-control-font-size:var(--typo3-input-sm-font-size)} +.form-control-adapt{max-width:100%;width:auto} +.form-select{--typo3-form-select-border-radius:var(--typo3-input-border-radius);--typo3-form-select-font-size:var(--typo3-input-font-size);--typo3-form-select-line-height:var(--typo3-input-line-height);--typo3-form-select-padding-x:var(--typo3-input-padding-x);--typo3-form-select-padding-y:var(--typo3-input-padding-y);--typo3-form-select-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;--typo3-form-select-color:var(--typo3-input-color);--typo3-form-select-bg:var(--typo3-input-bg);--typo3-form-select-icon-size:16px;--typo3-form-select-border-width:var(--typo3-input-border-width);--typo3-form-select-border-color:var(--typo3-input-border-color);--typo3-form-select-hover-color:var(--typo3-input-hover-color);--typo3-form-select-hover-bg:var(--typo3-input-hover-bg);--typo3-form-select-hover-border-color:var(--typo3-input-hover-border-color);--typo3-form-select-focus-color:var(--typo3-input-focus-color);--typo3-form-select-focus-bg:var(--typo3-input-focus-bg);--typo3-form-select-focus-border-color:var(--typo3-input-focus-border-color);--typo3-form-select-disabled-color:var(--typo3-input-disabled-color);--typo3-form-select-disabled-bg:var(--typo3-input-disabled-bg);--typo3-form-select-disabled-border-color:var(--typo3-input-disabled-border-color);--typo3-form-select-disabled-opacity:var(--typo3-input-disabled-opacity);--typo3-form-select-min-height:calc(var(--typo3-form-select-padding-y)*2 + var(--typo3-form-select-font-size)*var(--typo3-form-select-line-height) + var(--typo3-form-select-border-width)*2);-webkit-appearance:none;-moz-appearance:none;appearance:none;display:block;padding:var(--typo3-form-select-padding-y) var(--typo3-form-select-padding-x);position:relative;width:100%;-webkit-padding-end:calc(var(--typo3-form-select-padding-x)*2 + var(--typo3-form-select-icon-size));background-clip:border-box;background-color:var(--typo3-form-select-bg);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xml:space='preserve' viewBox='0 0 16 16'%3E%3Cpath fill='gray' d='m4.464 6.05-.707.707L8 11l4.243-4.243-.707-.707L8 9.586z'/%3E%3C/svg%3E");background-position:var(--typo3-position-end) var(--typo3-form-select-padding-x) top 50%;background-repeat:no-repeat;background-size:var(--typo3-form-select-icon-size);border:var(--typo3-form-select-border-width) solid var(--typo3-form-select-border-color);border-radius:var(--typo3-form-select-border-radius);color:var(--typo3-form-select-color);font-size:var(--typo3-form-select-font-size);font-weight:400;line-height:var(--typo3-form-select-line-height);min-height:var(--typo3-form-select-min-height);min-width:120px;outline-offset:0;overflow-x:hidden;padding-inline-end:calc(var(--typo3-form-select-padding-x)*2 + var(--typo3-form-select-icon-size));text-overflow:ellipsis;transition:var(--typo3-form-select-transition)} +.form-select:hover{--typo3-form-select-color:var(--typo3-form-select-hover-color);--typo3-form-select-bg:var(--typo3-form-select-hover-bg);--typo3-form-select-border-color:var(--typo3-form-select-hover-border-color);z-index:2!important} +.form-select:focus{--typo3-form-select-color:var(--typo3-form-select-focus-color);--typo3-form-select-bg:var(--typo3-form-select-focus-bg);--typo3-form-select-border-color:var(--typo3-form-select-focus-border-color);outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-form-select-focus-border-color),transparent 25%);z-index:3!important} +.form-select[disabled]{--typo3-form-select-color:color-mix(in srgb,var(--typo3-form-select-disabled-color),transparent calc((1 - var(--typo3-form-select-disabled-opacity))*100%));--typo3-form-select-bg:var(--typo3-form-select-disabled-bg);--typo3-form-select-border-color:var(--typo3-form-select-disabled-border-color);cursor:not-allowed} +.form-select[multiple],.form-select[size]:not([size="1"]){background-image:none;min-height:10rem;padding:0;scrollbar-color:var(--typo3-text-color-secondary) transparent} +.form-select optgroup{line-height:1.5;-webkit-padding-start:var(--typo3-form-select-padding-y);padding-inline-start:var(--typo3-form-select-padding-y)} +.form-select optgroup,.form-select optgroup option:first-child{margin-top:var(--typo3-form-select-padding-y)} +.form-select option{border-radius:calc(var(--typo3-form-select-border-radius) - 1px);margin:1px;overflow:hidden;padding:var(--typo3-form-select-padding-y) var(--typo3-form-select-padding-x);text-overflow:ellipsis} +.form-select-sm{--typo3-form-select-padding-y:var(--typo3-input-sm-padding-y);--typo3-form-select-padding-x:var(--typo3-input-sm-padding-x);--typo3-form-select-font-size:var(--typo3-input-sm-font-size)} +.form-range{--typo3-form-range-contrast:var(--typo3-component-color);--typo3-form-range-border-radius:var(--typo3-input-border-radius);--typo3-form-range-border-width:var(--typo3-input-border-width);--typo3-form-range-border-color:var(--typo3-input-border-color);--typo3-form-range-padding-y:var(--typo3-input-padding-y);--typo3-form-range-font-size:var(--typo3-input-font-size);--typo3-form-range-height:calc(var(--typo3-form-range-padding-y)*2 + var(--typo3-form-range-font-size)*var(--typo3-input-line-height) + var(--typo3-form-range-border-width)*2);--typo3-form-range-bg-state:initial;--typo3-form-range-border-color-state:initial;--typo3-form-range-bg:var(--typo3-input-bg);--typo3-form-range-hover-bg:var(--typo3-input-hover-bg);--typo3-form-range-hover-border-color:var(--typo3-input-hover-border-color);--typo3-form-range-focus-bg:var(--typo3-input-focus-bg);--typo3-form-range-focus-border-color:var(--typo3-input-focus-border-color);--typo3-form-range-disabled-bg:var(--typo3-input-disabled-bg);--typo3-form-range-disabled-border-color:var(--typo3-input-disabled-border-color);--typo3-form-range-shadow:var(--typo3-component-box-shadow-strong);--typo3-form-range-thumb-height:calc(var(--typo3-form-range-font-size)*var(--typo3-input-line-height) + var(--typo3-form-range-border-width)*2);--typo3-form-range-thumb-width:var(--typo3-form-range-font-size);--typo3-form-range-track-height:var(--typo3-form-range-font-size);--typo3-form-range-track-width:100%;width:100%} +.form-range-sm{--typo3-form-range-padding-y:var(--typo3-input-sm-padding-y);--typo3-form-range-font-size:var(--typo3-input-sm-font-size)} +.form-range-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;display:block;height:var(--typo3-form-range-height);outline:none;width:100%} +.form-range-input::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;background-color:var(--typo3-form-range-bg-state,var(--typo3-form-range-bg));border:var(--typo3-form-range-border-width) solid var(--typo3-form-range-border-color-state,var(--typo3-form-range-border-color));border-radius:var(--typo3-form-range-border-radius);box-shadow:var(--typo3-form-range-thumb-shadow);box-sizing:border-box;cursor:grab;height:var(--typo3-form-range-thumb-height);margin-top:calc((var(--typo3-form-range-thumb-height) - var(--typo3-form-range-track-height))/2*-1 - var(--typo3-form-range-border-width));width:var(--typo3-form-range-thumb-width)} +.form-range-input::-webkit-slider-thumb:active{cursor:grabbing} +.form-range-input::-webkit-slider-runnable-track{background:var(--typo3-form-range-bg-state,var(--typo3-form-range-bg));border:var(--typo3-form-range-border-width) solid var(--typo3-form-range-border-color-state,var(--typo3-form-range-border-color));border-radius:var(--typo3-form-range-border-radius);box-sizing:border-box;height:var(--typo3-form-range-track-height)} +.form-range-input::-moz-range-thumb{-moz-appearance:none;appearance:none;background-color:var(--typo3-form-range-bg-state,var(--typo3-form-range-bg));border:var(--typo3-form-range-border-width) solid var(--typo3-form-range-border-color-state,var(--typo3-form-range-border-color));border-radius:var(--typo3-form-range-border-radius);box-shadow:var(--typo3-form-range-thumb-shadow);box-sizing:border-box;cursor:grab;height:var(--typo3-form-range-thumb-height);width:var(--typo3-form-range-thumb-width)} +.form-range-input::-moz-range-thumb:active{cursor:grabbing} +.form-range-input::-moz-range-track{background:var(--typo3-form-range-bg-state,var(--typo3-form-range-bg));width:100%} +.form-range-input::-moz-range-progress,.form-range-input::-moz-range-track{border:var(--typo3-form-range-border-width) solid var(--typo3-form-range-border-color-state,var(--typo3-form-range-border-color));border-radius:var(--typo3-form-range-border-radius);box-sizing:border-box;height:var(--typo3-form-range-track-height)} +.form-range-input::-moz-range-progress{background:color-mix(in srgb,var(--typo3-form-range-bg-state,var(--typo3-form-range-bg)),var(--typo3-form-range-contrast) 10%)} +.form-range-input:hover{--typo3-form-range-border-color-state:var(--typo3-form-range-hover-border-color);--typo3-form-range-bg-state:var(--typo3-form-range-hover-bg)} +.form-range-input:active,.form-range-input:focus{--typo3-form-range-border-color-state:var(--typo3-form-range-focus-border-color);--typo3-form-range-bg-state:var(--typo3-form-range-focus-bg)} +.form-range-input:focus-visible::-webkit-slider-thumb{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-form-range-border-color-state,var(--typo3-form-range-border-color)),transparent 25%);outline-offset:0} +.form-range-input:focus-visible::-moz-range-thumb{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-form-range-border-color-state,var(--typo3-form-range-border-color)),transparent 25%);outline-offset:0} +.form-range-input[disabled]{--typo3-form-range-border-color-state:var(--typo3-form-range-disabled-border-color);--typo3-form-range-bg-state:var(--typo3-form-range-disabled-bg);cursor:not-allowed} +.form-check{--typo3-form-check-mask-position:center center;--typo3-form-check-mask-image-none:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0"/>');--typo3-form-check-mask-image-indeterminate:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M12.5 9h-9c-.3 0-.5-.2-.5-.5v-1c0-.3.2-.5.5-.5h9c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5"/></svg>');--typo3-form-check-mask-image-switch:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="5"/></svg>');--typo3-form-check-mask-image-check-checked:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m13.3 4.8-.7-.7c-.2-.2-.5-.2-.7 0L6.5 9.5 4 6.9c-.2-.2-.5-.2-.7 0l-.6.7c-.2.2-.2.5 0 .7l3.6 3.6c.2.2.5.2.7 0l6.4-6.4c.1-.2.1-.5-.1-.7"/></svg>');--typo3-form-check-mask-image-radio-checked:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="5"/></svg>');--typo3-form-check-mask-image:var(--typo3-form-check-mask-image-none);--typo3-form-check-top-correction:calc((1.5em - var(--typo3-form-check-height))/2);--typo3-form-check-margin-bottom:calc(var(--typo3-spacing)/2);--typo3-form-check-space:.5em;--typo3-form-check-width:1em;--typo3-form-check-height:1em;--typo3-form-check-padding-inline-start:var(--typo3-form-check-width);--typo3-form-check-border-radius:.25em;--typo3-form-check-color:var(--typo3-input-color);--typo3-form-check-bg:var(--typo3-input-bg);--typo3-form-check-border-width:var(--typo3-input-border-width);--typo3-form-check-border-color:var(--typo3-input-border-color);--typo3-form-check-hover-color:var(--typo3-input-hover-color);--typo3-form-check-hover-bg:var(--typo3-input-hover-bg);--typo3-form-check-hover-border-color:var(--typo3-input-hover-border-color);--typo3-form-check-focus-border-color:var(--typo3-input-focus-border-color);--typo3-form-check-checked-color:var(--typo3-input-active-color);--typo3-form-check-checked-bg:var(--typo3-input-active-bg);--typo3-form-check-checked-border-color:var(--typo3-input-active-border-color);--typo3-form-check-disabled-opacity:var(--typo3-input-disabled-opacity);--typo3-form-check-transition-time:.2s;--typo3-form-check-transition:color var(--typo3-form-check-transition-time) ease-in-out,background-color var(--typo3-form-check-transition-time) ease-in-out,border-color var(--typo3-form-check-transition-time) ease-in-out,box-shadow var(--typo3-form-check-transition-time) ease-in-out,mask-position var(--typo3-form-check-transition-time) ease-in-out;display:flex;gap:var(--typo3-form-check-space);margin-bottom:var(--typo3-form-check-margin-bottom);position:relative} +@media (prefers-reduced-motion){ +.form-check{--typo3-form-check-transition:none}} +.form-check-input{align-items:center;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--typo3-form-check-bg);border:var(--typo3-form-check-border-width) solid var(--typo3-form-check-border-color);color:var(--typo3-form-check-color);cursor:pointer;display:inline-flex;flex-grow:0;flex-shrink:0;justify-content:center;margin-top:var(--typo3-form-check-top-correction)} +.form-check-input,.form-check-input:before{height:var(--typo3-form-check-height);transition:var(--typo3-form-check-transition);width:var(--typo3-form-check-width)} +.form-check-input:before{background-color:var(--typo3-form-check-color);content:"";display:block;-webkit-mask-image:var(--typo3-form-check-mask-image);mask-image:var(--typo3-form-check-mask-image);-webkit-mask-position:var(--typo3-form-check-mask-position);mask-position:var(--typo3-form-check-mask-position);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;pointer-events:none} +.form-check-input[type=checkbox]{border-radius:var(--typo3-form-check-border-radius)} +.form-check-input[type=radio]{border-radius:50%} +.form-check-input:hover{--typo3-form-check-color:var(--typo3-form-check-hover-color);--typo3-form-check-bg:var(--typo3-form-check-hover-bg);--typo3-form-check-border-color:var(--typo3-form-check-hover-border-color)} +.form-check-input:hover~.form-check-label{--typo3-form-check-color:var(--typo3-form-check-hover-color)} +.form-check-input:focus{--typo3-form-check-border-color:var(--typo3-form-check-focus-border-color);outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-form-check-focus-border-color),transparent 25%)} +.form-check-input:indeterminate[type=checkbox]{--typo3-form-check-mask-image:var(--typo3-form-check-mask-image-indeterminate)} +.form-check-input:checked{--typo3-form-check-color:var(--typo3-form-check-checked-color);--typo3-form-check-bg:var(--typo3-form-check-checked-bg);--typo3-form-check-border-color:var(--typo3-form-check-checked-border-color)} +.form-check-input:checked~.form-check-label{--typo3-form-check-color:var(--typo3-form-check-checked-color)} +.form-check-input:checked[type=checkbox]{--typo3-form-check-mask-image:var(--typo3-form-check-mask-image-check-checked)} +.form-check-input:checked[type=radio]{--typo3-form-check-mask-image:var(--typo3-form-check-mask-image-radio-checked)} +.form-check-input[disabled]{cursor:not-allowed;opacity:var(--typo3-form-check-disabled-opacity)} +.form-check-input[disabled]~.form-check-label{cursor:default;opacity:var(--typo3-form-check-disabled-opacity)} +.form-check-label{cursor:pointer} +.form-switch{--typo3-form-check-mask-position:left center;--typo3-form-check-width:2em;--typo3-form-check-border-radius:.5em} +.form-switch .form-check-input{--typo3-form-check-mask-image:var(--typo3-form-check-mask-image-switch)!important} +.form-switch .form-check-input:indeterminate{--typo3-form-check-mask-position:center center} +.form-switch .form-check-input:checked{--typo3-form-check-mask-position:right center} +.form-check-inline{display:inline-flex;margin-right:var(--typo3-spacing)} +.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none} +.btn-check[disabled]+.btn{filter:none;opacity:var(--typo3-form-check-disabled-opacity);pointer-events:none} +.form-check.form-check-type-labeled-toggle{align-items:center;display:flex;-webkit-padding-start:0;gap:var(--typo3-form-check-space);padding-inline-start:0} +.form-check.form-check-type-labeled-toggle.form-check-inline{display:inline-flex} +.form-check.form-check-type-labeled-toggle .form-check-input{align-items:center;background-image:none!important;border-radius:var(--typo3-form-check-border-radius);color:var(--typo3-form-check-color);display:inline-flex;flex-grow:0;flex-shrink:0;float:none;font-size:1em;height:auto;line-height:1;margin:0;padding:.5em .75em;width:auto} +.form-check.form-check-type-labeled-toggle .form-check-input:before{background-color:transparent;content:attr(data-form-check-label-unchecked);height:auto;-webkit-mask-image:none;mask-image:none;width:auto} +.form-check.form-check-type-labeled-toggle .form-check-input:checked:before{content:attr(data-form-check-label-checked)} +.form-check.form-check-type-icon-toggle{--typo3-form-check-width:1.5em;--typo3-form-check-height:1.5em} +.form-check.form-check-type-icon-toggle .form-check-input:before{display:none} +.form-check.form-check-type-icon-toggle .form-check-label-icon{align-items:center;color:var(--typo3-form-check-color);display:flex;height:var(--typo3-form-check-height);inset-inline-start:0;justify-content:center;position:absolute;top:var(--typo3-form-check-top-correction);-webkit-user-select:none;-moz-user-select:none;user-select:none;width:var(--typo3-form-check-width)} +.form-check.form-check-type-icon-toggle .form-check-label-icon>*{align-items:center;display:flex;justify-content:center} +.form-check.form-check-type-icon-toggle .form-check-label-icon-checked,.form-check.form-check-type-icon-toggle .form-check-label-icon-indeterminate{display:none!important} +.form-check.form-check-type-icon-toggle .form-check-input:checked~.form-check-label .form-check-label-icon-checked,.form-check.form-check-type-icon-toggle .form-check-label-icon-unchecked{display:flex!important} +.form-check.form-check-type-icon-toggle .form-check-input:checked~.form-check-label .form-check-label-icon-indeterminate,.form-check.form-check-type-icon-toggle .form-check-input:checked~.form-check-label .form-check-label-icon-unchecked{display:none!important} +.form-check.form-check-type-icon-toggle .form-check-input:indeterminate~.form-check-label .form-check-label-icon-checked{display:none!important} +.form-check.form-check-type-icon-toggle .form-check-input:indeterminate~.form-check-label .form-check-label-icon-unchecked{display:none!important} +.form-check.form-check-type-icon-toggle .form-check-input:indeterminate~.form-check-label .form-check-label-icon-indeterminate{display:flex!important} +.form-check.form-check-type-toggle{--typo3-form-check-width:1.33333em;--typo3-form-check-height:1.33333em} +.form-check.form-check-type-card{--typo3-form-check-width:1.5em;--typo3-form-check-height:1.5em;--typo3-form-check-card-color:var(--typo3-text-color-base);--typo3-form-check-card-bg:var(--typo3-surface-container-low);--typo3-form-check-card-border-radius:var(--typo3-component-border-radius);--typo3-form-check-card-border-color:color-mix(in srgb,var(--typo3-form-check-card-bg),var(--typo3-form-check-card-color) var(--typo3-border-mix));--typo3-form-check-card-padding:1rem;background:var(--typo3-form-check-card-bg);border:1px solid var(--typo3-form-check-card-border-color);border-radius:var(--typo3-form-check-card-border-radius);box-shadow:var(--typo3-component-box-shadow);color:var(--typo3-form-check-card-color);margin-bottom:var(--typo3-spacing);padding:var(--typo3-form-check-card-padding);transition:var(--typo3-form-check-transition)} +.form-check.form-check-type-card:focus,.form-check.form-check-type-card:hover{box-shadow:var(--typo3-component-box-shadow-strong)} +.form-check.form-check-type-card:has(>.form-check-input:focus){--typo3-form-check-card-border-color:var(--typo3-form-check-focus-border-color);outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-form-check-focus-border-color),transparent 25%)} +.form-check.form-check-type-card:has(>.form-check-input:checked){--typo3-form-check-card-border-color:var(--typo3-form-check-checked-border-color)} +.form-check.form-check-type-card .form-check-input{order:1} +.form-check.form-check-type-card .form-check-input:focus{outline:none} +.form-check.form-check-type-card .form-check-input:checked[type=radio]{--typo3-form-check-mask-image:var(--typo3-form-check-mask-image-check-checked)} +.form-check.form-check-type-card .form-check-label{align-items:self-start;display:grid;flex-grow:1;gap:.5rem;-webkit-user-select:none;-moz-user-select:none;user-select:none} +.form-check.form-check-type-card .form-check-label:has(>.form-check-label-body){grid-template-rows:-webkit-min-content auto;grid-template-rows:min-content auto} +.form-check.form-check-type-card .form-check-label:after{border-radius:var(--typo3-form-check-card-border-radius);bottom:0;content:"";display:block;left:0;position:absolute;right:0;top:0} +.form-check.form-check-type-card .form-check-label-header{align-items:center;display:flex;font-family:inherit;font-size:1rem;gap:.5rem;line-height:1.2} +.form-check.form-check-type-card .form-check-label-header-inherit{font-size:inherit;line-height:inherit} +.form-check.form-check-type-card .form-check-label-label-body{display:block} +.form-check-card-container{--typo3-form-check-card-grid-gap:1rem;--typo3-form-check-card-min-size:200px;--typo3-form-check-card-max-size:1fr;display:grid;gap:var(--typo3-form-check-card-grid-gap);grid-template-columns:repeat(auto-fit,minmax(var(--typo3-form-check-card-min-size),var(--typo3-form-check-card-max-size)));margin-bottom:var(--typo3-spacing)} +.form-check-card-container-small{--typo3-form-check-card-max-size:300px} +.form-check-card-container .form-check-type-card{margin-bottom:0} +.form-check-card-container-headline{font-size:.8125rem;font-weight:700;grid-column:1/-1;margin-bottom:-3px} +*+.form-check-card-container-headline{margin-top:calc(var(--typo3-form-check-card-grid-gap)*.5)} +.form-check.form-check-size-input{margin-bottom:calc(var(--typo3-spacing)/2);margin-top:calc(var(--typo3-spacing)/2)} +.form-text{margin-top:calc(var(--typo3-spacing)*.25)} +.form-description{color:var(--typo3-text-color-variant);margin-bottom:calc(var(--typo3-spacing)*.25)} +.has-change{--typo3-input-color:var(--typo3-surface-container-info-text);--typo3-input-bg:var(--typo3-surface-container-info);--typo3-input-group-addon-bg:color-mix(in srgb,var(--typo3-input-bg),var(--typo3-input-color) 10%);--typo3-input-border-color:var(--typo3-state-info-border-color);--typo3-input-hover-color:var(--typo3-input-color);--typo3-input-hover-bg:var(--typo3-input-bg);--typo3-input-hover-border-color:var(--typo3-state-info-hover-border-color);--typo3-input-focus-color:var(--typo3-input-color);--typo3-input-focus-bg:var(--typo3-input-bg);--typo3-input-focus-border-color:var(--typo3-state-info-focus-border-color)} +.has-change .form-label:before,.has-change.form-label:before{background-color:var(--typo3-text-color-info);background-size:contain;content:"";display:inline-block;flex-shrink:0;height:1.3333333333em;-webkit-mask:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='currentColor'%3E%3Cpath d='M8 2c3.3 0 6 2.7 6 6s-2.7 6-6 6-6-2.7-6-6 2.7-6 6-6m0-1C4.1 1 1 4.1 1 8s3.1 7 7 7 7-3.1 7-7-3.1-7-7-7'/%3E%3Ccircle cx='8' cy='11' r='1'/%3E%3Cpath d='M8.5 9h-1l-.445-4.45A.5.5 0 0 1 7.552 4h.896a.5.5 0 0 1 .497.55z'/%3E%3C/g%3E%3C/svg%3E");mask:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='currentColor'%3E%3Cpath d='M8 2c3.3 0 6 2.7 6 6s-2.7 6-6 6-6-2.7-6-6 2.7-6 6-6m0-1C4.1 1 1 4.1 1 8s3.1 7 7 7 7-3.1 7-7-3.1-7-7-7'/%3E%3Ccircle cx='8' cy='11' r='1'/%3E%3Cpath d='M8.5 9h-1l-.445-4.45A.5.5 0 0 1 7.552 4h.896a.5.5 0 0 1 .497.55z'/%3E%3C/g%3E%3C/svg%3E");vertical-align:middle;width:1.3333333333em} +.has-success,.is-valid{--typo3-input-color:var(--typo3-surface-container-success-text);--typo3-input-bg:var(--typo3-surface-container-success);--typo3-input-group-addon-bg:color-mix(in srgb,var(--typo3-input-bg),var(--typo3-input-color) 10%);--typo3-input-border-color:var(--typo3-state-success-border-color);--typo3-input-hover-color:var(--typo3-input-color);--typo3-input-hover-bg:var(--typo3-input-bg);--typo3-input-hover-border-color:var(--typo3-state-success-hover-border-color);--typo3-input-focus-color:var(--typo3-input-color);--typo3-input-focus-bg:var(--typo3-input-bg);--typo3-input-focus-border-color:var(--typo3-state-success-focus-border-color)} +.has-success .form-label:before,.has-success.form-label:before,.is-valid .form-label:before,.is-valid.form-label:before{background-color:var(--typo3-text-color-success);background-size:contain;content:"";display:inline-block;flex-shrink:0;height:1.3333333333em;-webkit-mask:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='currentColor'%3E%3Cpath d='m12.1 5.3-.4-.3c-.1-.1-.3-.1-.4 0L6.6 9.8l-2-2c-.1-.1-.3-.1-.4 0l-.3.4c-.1.1-.1.3 0 .4L6 10.7l.4.3c.1.1.3.1.4 0l.4-.4 4.9-4.9c.1-.1.1-.3 0-.4'/%3E%3Cpath d='M8 2c3.3 0 6 2.7 6 6s-2.7 6-6 6-6-2.7-6-6 2.7-6 6-6m0-1C4.1 1 1 4.1 1 8s3.1 7 7 7 7-3.1 7-7-3.1-7-7-7'/%3E%3C/g%3E%3C/svg%3E");mask:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='currentColor'%3E%3Cpath d='m12.1 5.3-.4-.3c-.1-.1-.3-.1-.4 0L6.6 9.8l-2-2c-.1-.1-.3-.1-.4 0l-.3.4c-.1.1-.1.3 0 .4L6 10.7l.4.3c.1.1.3.1.4 0l.4-.4 4.9-4.9c.1-.1.1-.3 0-.4'/%3E%3Cpath d='M8 2c3.3 0 6 2.7 6 6s-2.7 6-6 6-6-2.7-6-6 2.7-6 6-6m0-1C4.1 1 1 4.1 1 8s3.1 7 7 7 7-3.1 7-7-3.1-7-7-7'/%3E%3C/g%3E%3C/svg%3E");vertical-align:middle;width:1.3333333333em} +.has-warning{--typo3-input-color:var(--typo3-surface-container-warning-text);--typo3-input-bg:var(--typo3-surface-container-warning);--typo3-input-group-addon-bg:color-mix(in srgb,var(--typo3-input-bg),var(--typo3-input-color) 10%);--typo3-input-border-color:var(--typo3-state-warning-border-color);--typo3-input-hover-color:var(--typo3-input-color);--typo3-input-hover-bg:var(--typo3-input-bg);--typo3-input-hover-border-color:var(--typo3-state-warning-hover-border-color);--typo3-input-focus-color:var(--typo3-input-color);--typo3-input-focus-bg:var(--typo3-input-bg);--typo3-input-focus-border-color:var(--typo3-state-warning-focus-border-color)} +.has-warning .form-label:before,.has-warning.form-label:before{background-color:var(--typo3-text-color-warning);background-size:contain;content:"";display:inline-block;flex-shrink:0;height:1.3333333333em;-webkit-mask:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='currentColor'%3E%3Ccircle cx='8' cy='12' r='1'/%3E%3Cpath d='M8.5 10h-1l-.445-4.45A.5.5 0 0 1 7.552 5h.896a.5.5 0 0 1 .497.55z'/%3E%3Cpath d='M8 2.008a.98.98 0 0 1 .875.515l5.536 9.992a.98.98 0 0 1-.013.993.98.98 0 0 1-.862.492H2.464a.98.98 0 0 1-.862-.492.98.98 0 0 1-.013-.993l5.536-9.992A.98.98 0 0 1 8 2.008m0-1a1.98 1.98 0 0 0-1.75 1.03L.715 12.032C-.024 13.364.94 15 2.464 15h11.072c1.524 0 2.488-1.636 1.75-2.97L9.749 2.04A1.98 1.98 0 0 0 8 1.009z'/%3E%3C/g%3E%3C/svg%3E");mask:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='currentColor'%3E%3Ccircle cx='8' cy='12' r='1'/%3E%3Cpath d='M8.5 10h-1l-.445-4.45A.5.5 0 0 1 7.552 5h.896a.5.5 0 0 1 .497.55z'/%3E%3Cpath d='M8 2.008a.98.98 0 0 1 .875.515l5.536 9.992a.98.98 0 0 1-.013.993.98.98 0 0 1-.862.492H2.464a.98.98 0 0 1-.862-.492.98.98 0 0 1-.013-.993l5.536-9.992A.98.98 0 0 1 8 2.008m0-1a1.98 1.98 0 0 0-1.75 1.03L.715 12.032C-.024 13.364.94 15 2.464 15h11.072c1.524 0 2.488-1.636 1.75-2.97L9.749 2.04A1.98 1.98 0 0 0 8 1.009z'/%3E%3C/g%3E%3C/svg%3E");vertical-align:middle;width:1.3333333333em} +.has-error,.is-invalid{--typo3-input-color:var(--typo3-surface-container-danger-text);--typo3-input-bg:var(--typo3-surface-container-danger);--typo3-input-group-addon-bg:color-mix(in srgb,var(--typo3-input-bg),var(--typo3-input-color) 10%);--typo3-input-border-color:var(--typo3-state-danger-border-color);--typo3-input-hover-color:var(--typo3-input-color);--typo3-input-hover-bg:var(--typo3-input-bg);--typo3-input-hover-border-color:var(--typo3-state-danger-hover-border-color);--typo3-input-focus-color:var(--typo3-input-color);--typo3-input-focus-bg:var(--typo3-input-bg);--typo3-input-focus-border-color:var(--typo3-state-danger-focus-border-color)} +.has-error .form-label:before,.has-error.form-label:before,.is-invalid .form-label:before,.is-invalid.form-label:before{background-color:var(--typo3-text-color-danger);background-size:contain;content:"";display:inline-block;flex-shrink:0;height:1.3333333333em;-webkit-mask:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='currentColor'%3E%3Cpath d='M8 2c3.3 0 6 2.7 6 6s-2.7 6-6 6-6-2.7-6-6 2.7-6 6-6m0-1C4.1 1 1 4.1 1 8s3.1 7 7 7 7-3.1 7-7-3.1-7-7-7'/%3E%3Ccircle cx='8' cy='11' r='1'/%3E%3Cpath d='M8.5 9h-1l-.445-4.45A.5.5 0 0 1 7.552 4h.896a.5.5 0 0 1 .497.55z'/%3E%3C/g%3E%3C/svg%3E");mask:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='currentColor'%3E%3Cpath d='M8 2c3.3 0 6 2.7 6 6s-2.7 6-6 6-6-2.7-6-6 2.7-6 6-6m0-1C4.1 1 1 4.1 1 8s3.1 7 7 7 7-3.1 7-7-3.1-7-7-7'/%3E%3Ccircle cx='8' cy='11' r='1'/%3E%3Cpath d='M8.5 9h-1l-.445-4.45A.5.5 0 0 1 7.552 4h.896a.5.5 0 0 1 .497.55z'/%3E%3C/g%3E%3C/svg%3E");vertical-align:middle;width:1.3333333333em} +typo3-backend-editable-setting :user-invalid{--typo3-input-color:var(--typo3-surface-container-danger-text);--typo3-input-bg:var(--typo3-surface-container-danger);--typo3-input-group-addon-bg:color-mix(in srgb,var(--typo3-input-bg),var(--typo3-input-color) 10%);--typo3-input-border-color:var(--typo3-state-danger-border-color);--typo3-input-hover-color:var(--typo3-input-color);--typo3-input-hover-bg:var(--typo3-input-bg);--typo3-input-hover-border-color:var(--typo3-state-danger-hover-border-color);--typo3-input-focus-color:var(--typo3-input-color);--typo3-input-focus-bg:var(--typo3-input-bg);--typo3-input-focus-border-color:var(--typo3-state-danger-focus-border-color)} +.form-hint{--typo3-formhint-color:inherit;--typo3-formhint-bg:inherit;--typo3-formhint-border-color:transparent;--typo3-formhint-border-width:var(--typo3-input-border-width);--typo3-formhint-border-radius:var(--typo3-input-border-radius);--typo3-formhint-box-shadow:none;background-color:var(--typo3-formhint-bg);border:var(--typo3-formhint-border-width) solid var(--typo3-formhint-border-color);border-radius:var(--typo3-formhint-border-radius);box-shadow:var(--typo3-formhint-box-shadow);color:var(--typo3-formhint-color);font-size:.625rem;padding:calc(var(--typo3-input-padding-y)/2) var(--typo3-input-padding-x)} +.form-hint--primary{--typo3-formhint-color:var(--typo3-state-primary-color);--typo3-formhint-bg:var(--typo3-state-primary-bg);--typo3-formhint-border-color:var(--typo3-state-primary-border-color)} +.form-hint--secondary{--typo3-formhint-color:var(--typo3-state-secondary-color);--typo3-formhint-bg:var(--typo3-state-secondary-bg);--typo3-formhint-border-color:var(--typo3-state-secondary-border-color)} +.form-hint--info{--typo3-formhint-color:var(--typo3-state-info-color);--typo3-formhint-bg:var(--typo3-state-info-bg);--typo3-formhint-border-color:var(--typo3-state-info-border-color)} +.form-hint--success{--typo3-formhint-color:var(--typo3-state-success-color);--typo3-formhint-bg:var(--typo3-state-success-bg);--typo3-formhint-border-color:var(--typo3-state-success-border-color)} +.form-hint--warning{--typo3-formhint-color:var(--typo3-state-warning-color);--typo3-formhint-bg:var(--typo3-state-warning-bg);--typo3-formhint-border-color:var(--typo3-state-warning-border-color)} +.form-hint--danger{--typo3-formhint-color:var(--typo3-state-danger-color);--typo3-formhint-bg:var(--typo3-state-danger-bg);--typo3-formhint-border-color:var(--typo3-state-danger-border-color)} +.form-hint--notice{--typo3-formhint-color:var(--typo3-state-notice-color);--typo3-formhint-bg:var(--typo3-state-notice-bg);--typo3-formhint-border-color:var(--typo3-state-notice-border-color)} +.form-hint--default{--typo3-formhint-color:var(--typo3-state-default-color);--typo3-formhint-bg:var(--typo3-state-default-bg);--typo3-formhint-border-color:var(--typo3-state-default-border-color)} +.form-wizards-wrap{display:grid;gap:.25rem;grid-template-columns:1fr;width:100%} +.form-wizards-wrap>*{grid-column:1/2} +.form-wizards-wrap>*>:first-child{margin-top:0} +.form-wizards-wrap>*>:last-child{margin-bottom:0} +.form-wizards-wrap>.form-wizards-item-element{min-width:120px} +.form-wizards-wrap>.form-wizards-item-element .form-select[multiple],.form-wizards-wrap>.form-wizards-item-element .form-select[size]:not([size="1"]){height:100%} +.form-wizards-wrap>.form-wizards-item-aside{align-self:flex-start;grid-column:2/3;white-space:nowrap} +.form-wizards-wrap>.form-wizards-item-aside+.form-wizards-item-aside{grid-column:3/4} +.form-wizards-wrap>.form-wizards-item-bottom .btn{text-align:left;white-space:wrap} +.form-wizard-icon-list{background:var(--typo3-component-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:var(--typo3-input-border-radius);color:var(--typo3-component-color);display:flex;flex-wrap:wrap;gap:2px;margin-top:.25rem;padding:calc(var(--typo3-spacing)/4)} +.form-wizard-icon-list-item a,.form-wizard-icon-list-item button{align-items:center;background-color:var(--typo3-component-bg);border:none;border-radius:calc(var(--typo3-component-border-radius)/2);color:var(--typo3-component-color);display:flex;height:100%;justify-content:center;line-height:1;outline-offset:-1px;padding:calc(var(--typo3-spacing)/2)} +.form-wizard-icon-list-item a>span[title],.form-wizard-icon-list-item button>span[title]{display:block} +.form-wizard-icon-list-item a:hover,.form-wizard-icon-list-item button:hover{background-color:var(--typo3-list-item-hover-bg);color:var(--typo3-list-item-hover-color);outline:1px solid var(--typo3-list-item-hover-border-color)} +.form-wizard-icon-list-item a:focus,.form-wizard-icon-list-item button:focus{background-color:var(--typo3-list-item-focus-bg);color:var(--typo3-list-item-focus-color);outline:1px solid var(--typo3-list-item-focus-border-color)} +.form-wizard-icon-list-item a.active,.form-wizard-icon-list-item a:active,.form-wizard-icon-list-item button.active,.form-wizard-icon-list-item button:active{background-color:var(--typo3-list-item-active-bg);color:var(--typo3-list-item-active-color);outline:1px solid var(--typo3-list-item-active-border-color)} +.form-wizard-icon-list-item img{display:block;max-height:128px;max-width:128px;min-width:16px} +.form-control-clearable{-webkit-padding-end:2.3em;padding-inline-end:2.3em} +.form-control-clearable-wrapper{border:0;flex-grow:1;padding:0;position:relative} +.form-control-clearable-wrapper input[type=search]::-webkit-search-cancel-button{display:none} +.form-control-clearable-wrapper .form-control{-webkit-padding-end:2.3em;padding-inline-end:2.3em} +.form-control-clearable-wrapper .close{align-items:center;background-color:transparent;border:none;color:var(--typo3-input-color);display:flex;height:16px;inset-inline-end:.75em;opacity:.3;padding:0;position:absolute;top:50%;transform:translateY(-50%);z-index:3} +.form-control-clearable-wrapper .close:hover{opacity:.5} +.form-control-clearable-wrapper .close .icon{vertical-align:0} +.form-control-clearable-wrapper:focus{border-color:inherit;box-shadow:none} +.form-control-clearable-wrapper:focus-within{z-index:3} +.input-group,.input-grouped{align-items:stretch;border-radius:var(--typo3-input-border-radius);display:flex;flex-wrap:wrap;position:relative;width:100%} +.input-group>.form-control,.input-group>.form-control-clearable-wrapper,.input-group>.form-select,.input-group>typo3-backend-color-picker,.input-grouped>.form-control,.input-grouped>.form-control-clearable-wrapper,.input-grouped>.form-select,.input-grouped>typo3-backend-color-picker{flex:1 1 auto;min-width:0;position:relative;width:1%} +.input-group>.form-control:focus,.input-group>.form-select:focus,.input-grouped>.form-control:focus,.input-grouped>.form-select:focus{z-index:5} +.input-group>.input-group-text>.form-check-type-toggle,.input-grouped>.input-group-text>.form-check-type-toggle{margin-bottom:0} +.input-group .btn,.input-grouped .btn{position:relative;z-index:2} +.input-group .btn:focus,.input-grouped .btn:focus{z-index:5} +.input-grouped{gap:calc(var(--typo3-spacing)/2)} +.input-group-text{align-items:center;background-color:var(--typo3-input-group-addon-bg);border:var(--typo3-input-border-width) solid var(--typo3-input-border-color);border-radius:var(--typo3-input-border-radius);color:var(--typo3-input-color);display:flex;font-size:var(--typo3-input-font-size);font-weight:400;line-height:var(--typo3-input-line-height);min-width:2.5rem;padding:var(--typo3-input-padding-y) var(--typo3-input-padding-x);text-align:center;white-space:nowrap} +.input-group-icon{vertical-align:middle} +.input-group-icon img{max-height:16px} +.input-group-sm{--typo3-input-font-size:var(--typo3-input-sm-font-size);--typo3-input-padding-y:var(--typo3-input-sm-padding-y);--typo3-input-padding-x:var(--typo3-input-sm-padding-x)} +.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu),.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-end-end-radius:0;border-start-end-radius:0} +.input-group>*+.form-control-clearable-wrapper>.form-control,.input-group>*+.form-control-clearable-wrapper>.form-select{border-end-start-radius:0;border-start-start-radius:0} +.input-group>.form-control-clearable-wrapper:not(:last-child)>.form-control,.input-group>.form-control-clearable-wrapper:not(:last-child)>.form-select{border-end-end-radius:0;border-start-end-radius:0} +.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback),.input-group>typo3-backend-color-picker:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback)>.form-control{-webkit-margin-start:calc(var(--typo3-input-border-width)*-1);border-end-start-radius:0;border-start-start-radius:0;margin-inline-start:calc(var(--typo3-input-border-width)*-1)} +.input-group>label.visually-hidden:first-child+.form-control,.input-group>label.visually-hidden:first-child+.form-select{border-end-start-radius:inherit!important;border-start-start-radius:inherit!important} +.form{margin-bottom:var(--typo3-spacing)} +.form-inline{display:inline} +.form-slim{align-items:center;display:flex;flex-wrap:wrap;gap:calc(var(--typo3-spacing)/2)} +.form-slim .form-group{margin-bottom:0} +.form-group-search-result{padding-bottom:7px} +.btn{--typo3-btn-padding-y:var(--typo3-input-padding-y);--typo3-btn-padding-x:var(--typo3-input-padding-x);--typo3-btn-font-size:var(--typo3-input-font-size);--typo3-btn-line-height:var(--typo3-input-line-height);--typo3-btn-border-width:var(--typo3-input-border-width);--typo3-btn-border-radius:var(--typo3-input-border-radius);--typo3-btn-disabled-opacity:.65;--typo3-btn-transition:var(--typo3-transition-color);--typo3-btn-color:inherit;--typo3-btn-bg:transparent;--typo3-btn-border-color:transparent;--typo3-btn-hover-color:inherit;--typo3-btn-hover-bg:transparent;--typo3-btn-hover-border-color:transparent;--typo3-btn-focus-color:inherit;--typo3-btn-focus-bg:transparent;--typo3-btn-focus-border-color:transparent;--typo3-btn-disabled-color:inherit;--typo3-btn-disabled-bg:transparent;--typo3-btn-disabled-border-color:transparent;--typo3-btn-min-height:calc(var(--typo3-btn-padding-y)*2 + var(--typo3-btn-font-size)*var(--typo3-btn-line-height) + var(--typo3-btn-border-width)*2);align-items:center;background-color:var(--typo3-btn-bg);border:var(--typo3-btn-border-width) solid var(--typo3-btn-border-color);border-radius:var(--typo3-btn-border-radius);color:var(--typo3-btn-color);cursor:pointer;display:inline-flex;font-size:var(--typo3-btn-font-size);font-weight:400;gap:.35em;justify-content:center;line-height:var(--typo3-btn-line-height);min-height:var(--typo3-btn-min-height);outline-offset:0;padding:var(--typo3-btn-padding-y) var(--typo3-btn-padding-x);text-decoration:none;transition:var(--typo3-btn-transition);-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;white-space:nowrap} +.btn:hover{--typo3-btn-color:var(--typo3-btn-hover-color);--typo3-btn-bg:var(--typo3-btn-hover-bg);--typo3-btn-border-color:var(--typo3-btn-hover-border-color);text-decoration:inherit;z-index:2!important} +.btn:focus{--typo3-btn-color:var(--typo3-btn-focus-color);--typo3-btn-bg:var(--typo3-btn-focus-bg);--typo3-btn-border-color:var(--typo3-btn-focus-border-color);z-index:3!important} +.btn-check:focus-visible+.btn,.btn:focus-visible,.btn:has(:focus-visible){outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-btn-focus-border-color),transparent 25%)} +.btn-check:checked+.btn,.btn.active,.btn[aria-pressed=true],:not(.btn-check)+.btn:active{box-shadow:inset 0 0 1000px 0 color-mix(in srgb,var(--typo3-btn-color),transparent 90%)} +.btn.disabled,.btn[disabled],fieldset[disabled] .btn{--typo3-btn-color:var(--typo3-btn-disabled-color);--typo3-btn-bg:var(--typo3-btn-disabled-bg);--typo3-btn-border-color:var(--typo3-btn-disabled-border-color);cursor:not-allowed;opacity:var(--typo3-btn-disabled-opacity);pointer-events:none} +.btn-sm{--typo3-btn-padding-y:var(--typo3-input-sm-padding-y);--typo3-btn-padding-x:var(--typo3-input-sm-padding-x);--typo3-btn-font-size:var(--typo3-input-sm-font-size)} +.btn-icon{--typo3-btn-padding-y:var(--typo3-input-sm-padding-y);--typo3-btn-padding-x:var(--typo3-input-sm-padding-y)} +.btn-block{display:flex;width:100%} +.btn-block+.btn-block{margin-top:calc(var(--typo3-spacing)*.5)} +.btn-block-vertical{flex-direction:column;text-align:center;white-space:normal;width:100%} +.btn-align-start{justify-content:start} +.btn-align-end{justify-content:end} +.btn-link{--typo3-btn-padding-y:0;--typo3-btn-padding-x:0;--typo3-btn-focus-border-color:var(--typo3-state-default-focus-border-color)} +.btn-link:focus,.btn-link:hover{border:1px solid transparent;text-decoration:underline} +.btn-primary{--typo3-btn-color:var(--typo3-state-primary-color);--typo3-btn-bg:var(--typo3-state-primary-bg);--typo3-btn-border-color:var(--typo3-state-primary-border-color);--typo3-btn-hover-color:var(--typo3-state-primary-hover-color);--typo3-btn-hover-bg:var(--typo3-state-primary-hover-bg);--typo3-btn-hover-border-color:var(--typo3-state-primary-hover-border-color);--typo3-btn-focus-color:var(--typo3-state-primary-focus-color);--typo3-btn-focus-bg:var(--typo3-state-primary-focus-bg);--typo3-btn-focus-border-color:var(--typo3-state-primary-focus-border-color);--typo3-btn-disabled-color:var(--typo3-state-primary-disabled-color);--typo3-btn-disabled-bg:var(--typo3-state-primary-disabled-bg);--typo3-btn-disabled-border-color:var(--typo3-state-primary-disabled-border-color)} +.btn-secondary{--typo3-btn-color:var(--typo3-state-secondary-color);--typo3-btn-bg:var(--typo3-state-secondary-bg);--typo3-btn-border-color:var(--typo3-state-secondary-border-color);--typo3-btn-hover-color:var(--typo3-state-secondary-hover-color);--typo3-btn-hover-bg:var(--typo3-state-secondary-hover-bg);--typo3-btn-hover-border-color:var(--typo3-state-secondary-hover-border-color);--typo3-btn-focus-color:var(--typo3-state-secondary-focus-color);--typo3-btn-focus-bg:var(--typo3-state-secondary-focus-bg);--typo3-btn-focus-border-color:var(--typo3-state-secondary-focus-border-color);--typo3-btn-disabled-color:var(--typo3-state-secondary-disabled-color);--typo3-btn-disabled-bg:var(--typo3-state-secondary-disabled-bg);--typo3-btn-disabled-border-color:var(--typo3-state-secondary-disabled-border-color)} +.btn-info{--typo3-btn-color:var(--typo3-state-info-color);--typo3-btn-bg:var(--typo3-state-info-bg);--typo3-btn-border-color:var(--typo3-state-info-border-color);--typo3-btn-hover-color:var(--typo3-state-info-hover-color);--typo3-btn-hover-bg:var(--typo3-state-info-hover-bg);--typo3-btn-hover-border-color:var(--typo3-state-info-hover-border-color);--typo3-btn-focus-color:var(--typo3-state-info-focus-color);--typo3-btn-focus-bg:var(--typo3-state-info-focus-bg);--typo3-btn-focus-border-color:var(--typo3-state-info-focus-border-color);--typo3-btn-disabled-color:var(--typo3-state-info-disabled-color);--typo3-btn-disabled-bg:var(--typo3-state-info-disabled-bg);--typo3-btn-disabled-border-color:var(--typo3-state-info-disabled-border-color)} +.btn-success{--typo3-btn-color:var(--typo3-state-success-color);--typo3-btn-bg:var(--typo3-state-success-bg);--typo3-btn-border-color:var(--typo3-state-success-border-color);--typo3-btn-hover-color:var(--typo3-state-success-hover-color);--typo3-btn-hover-bg:var(--typo3-state-success-hover-bg);--typo3-btn-hover-border-color:var(--typo3-state-success-hover-border-color);--typo3-btn-focus-color:var(--typo3-state-success-focus-color);--typo3-btn-focus-bg:var(--typo3-state-success-focus-bg);--typo3-btn-focus-border-color:var(--typo3-state-success-focus-border-color);--typo3-btn-disabled-color:var(--typo3-state-success-disabled-color);--typo3-btn-disabled-bg:var(--typo3-state-success-disabled-bg);--typo3-btn-disabled-border-color:var(--typo3-state-success-disabled-border-color)} +.btn-warning{--typo3-btn-color:var(--typo3-state-warning-color);--typo3-btn-bg:var(--typo3-state-warning-bg);--typo3-btn-border-color:var(--typo3-state-warning-border-color);--typo3-btn-hover-color:var(--typo3-state-warning-hover-color);--typo3-btn-hover-bg:var(--typo3-state-warning-hover-bg);--typo3-btn-hover-border-color:var(--typo3-state-warning-hover-border-color);--typo3-btn-focus-color:var(--typo3-state-warning-focus-color);--typo3-btn-focus-bg:var(--typo3-state-warning-focus-bg);--typo3-btn-focus-border-color:var(--typo3-state-warning-focus-border-color);--typo3-btn-disabled-color:var(--typo3-state-warning-disabled-color);--typo3-btn-disabled-bg:var(--typo3-state-warning-disabled-bg);--typo3-btn-disabled-border-color:var(--typo3-state-warning-disabled-border-color)} +.btn-danger{--typo3-btn-color:var(--typo3-state-danger-color);--typo3-btn-bg:var(--typo3-state-danger-bg);--typo3-btn-border-color:var(--typo3-state-danger-border-color);--typo3-btn-hover-color:var(--typo3-state-danger-hover-color);--typo3-btn-hover-bg:var(--typo3-state-danger-hover-bg);--typo3-btn-hover-border-color:var(--typo3-state-danger-hover-border-color);--typo3-btn-focus-color:var(--typo3-state-danger-focus-color);--typo3-btn-focus-bg:var(--typo3-state-danger-focus-bg);--typo3-btn-focus-border-color:var(--typo3-state-danger-focus-border-color);--typo3-btn-disabled-color:var(--typo3-state-danger-disabled-color);--typo3-btn-disabled-bg:var(--typo3-state-danger-disabled-bg);--typo3-btn-disabled-border-color:var(--typo3-state-danger-disabled-border-color)} +.btn-notice{--typo3-btn-color:var(--typo3-state-notice-color);--typo3-btn-bg:var(--typo3-state-notice-bg);--typo3-btn-border-color:var(--typo3-state-notice-border-color);--typo3-btn-hover-color:var(--typo3-state-notice-hover-color);--typo3-btn-hover-bg:var(--typo3-state-notice-hover-bg);--typo3-btn-hover-border-color:var(--typo3-state-notice-hover-border-color);--typo3-btn-focus-color:var(--typo3-state-notice-focus-color);--typo3-btn-focus-bg:var(--typo3-state-notice-focus-bg);--typo3-btn-focus-border-color:var(--typo3-state-notice-focus-border-color);--typo3-btn-disabled-color:var(--typo3-state-notice-disabled-color);--typo3-btn-disabled-bg:var(--typo3-state-notice-disabled-bg);--typo3-btn-disabled-border-color:var(--typo3-state-notice-disabled-border-color)} +.btn-default{--typo3-btn-color:var(--typo3-state-default-color);--typo3-btn-bg:var(--typo3-state-default-bg);--typo3-btn-border-color:var(--typo3-state-default-border-color);--typo3-btn-hover-color:var(--typo3-state-default-hover-color);--typo3-btn-hover-bg:var(--typo3-state-default-hover-bg);--typo3-btn-hover-border-color:var(--typo3-state-default-hover-border-color);--typo3-btn-focus-color:var(--typo3-state-default-focus-color);--typo3-btn-focus-bg:var(--typo3-state-default-focus-bg);--typo3-btn-focus-border-color:var(--typo3-state-default-focus-border-color);--typo3-btn-disabled-color:var(--typo3-state-default-disabled-color);--typo3-btn-disabled-bg:var(--typo3-state-default-disabled-bg);--typo3-btn-disabled-border-color:var(--typo3-state-default-disabled-border-color)} +.btn-borderless{--typo3-btn-color:inherit;--typo3-btn-bg:transparent;--typo3-btn-border-color:transparent} +.btn-group,.btn-group-vertical{align-items:center;display:inline-flex;vertical-align:middle} +.btn-group-vertical>.dropdown,.btn-group>.dropdown{display:inline-flex} +.btn-group-vertical>.btn,.btn-group-vertical>.dropdown>.btn,.btn-group>.btn,.btn-group>.dropdown>.btn{flex:1 1 auto;position:relative} +.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group-vertical>.dropdown>.btn.active,.btn-group-vertical>.dropdown>.btn:active,.btn-group-vertical>.dropdown>.btn:focus,.btn-group-vertical>.dropdown>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover,.btn-group>.dropdown>.btn.active,.btn-group>.dropdown>.btn:active,.btn-group>.dropdown>.btn:focus,.btn-group>.dropdown>.btn:hover{z-index:1} +.btn-group>:is(.btn-group,.dropdown):not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{-webkit-margin-start:calc(var(--typo3-input-border-width)*-1);margin-inline-start:calc(var(--typo3-input-border-width)*-1)} +.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>:is(.btn-group,.dropdown):not(:last-child)>.btn{border-end-end-radius:0;border-start-end-radius:0} +.btn-group>.btn:nth-child(n+3),.btn-group>:is(.btn-group,.dropdown):not(:first-child)>.btn,.btn-group>:not(.btn-check)+.btn{border-end-start-radius:0;border-start-start-radius:0} +.btn-group-vertical{align-items:flex-start;flex-direction:column;justify-content:center} +.btn-group-vertical>.btn,.btn-group-vertical>:is(.btn-group,.dropdown){width:100%} +.btn-group-vertical>:is(.btn,.btn-group,.dropdown):not(:first-child){-webkit-margin-before:calc(var(--typo3-input-border-width)*-1);margin-block-start:calc(var(--typo3-input-border-width)*-1)} +.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>:is(.btn-group,.dropdown):not(:last-child)>.btn{border-end-end-radius:0;border-end-start-radius:0} +.btn-group-vertical>:is(.btn,.dropdown)~.btn,.btn-group-vertical>:is(.btn-group,.dropdown):not(:first-child)>.btn{border-start-end-radius:0;border-start-start-radius:0} +.btn-toolbar{display:flex;flex-wrap:wrap;gap:calc(var(--typo3-spacing)/2);justify-content:flex-start} +.btn-toolbar-nowrap{flex-wrap:nowrap} +.btn-group-sm>.btn{--typo3-btn-padding-y:var(--typo3-input-sm-padding-y);--typo3-btn-padding-x:var(--typo3-input-sm-padding-x);--typo3-btn-font-size:var(--typo3-input-sm-font-size)} +code,pre{-webkit-hyphens:none;hyphens:none} +code[class*=language-],pre[class*=language-]{background:none;color:inherit;font-size:1em;text-align:start;white-space:pre;word-break:normal;word-spacing:normal;word-wrap:normal;-webkit-hyphens:none;hyphens:none;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4} +pre[class*=language-]{border-radius:4px;margin-bottom:var(--typo3-spacing);overflow:auto;padding:1em} +:not(pre)>code[class*=language-],pre[class*=language-]{background-color:rgba(0,0,0,.05)} +kbd{--typo3-kbd-font-family:var(--typo3-font-family-monospace);--typo3-kbd-color:var(--typo3-text-color-base);--typo3-kbd-bg:var(--typo3-surface-bright);--typo3-kbd-border-color:color-mix(in srgb,var(--typo3-kbd-bg),var(--typo3-kbd-color) var(--typo3-border-mix));--typo3-kbd-border-width:var(--typo3-component-border-width);--typo3-kbd-border-radius:.5em;--typo3-kbd-padding-x:.5em;--typo3-kbd-font-size:.875em;--typo3-kbd-font-weight:bold;--typo3-kbd-shadow:0 1px 0 var(--typo3-kbd-border-color),var(--typo3-shadow-2);align-items:center;background-color:var(--typo3-kbd-bg);border-radius:var(--typo3-kbd-border-radius);box-shadow:var(--typo3-kbd-shadow);color:var(--typo3-kbd-color);display:inline-flex;font-family:var(--typo3-kbd-font-family);font-size:var(--typo3-kbd-font-size);font-weight:var(--typo3-kbd-font-weight);gap:.15em;height:calc(var(--typo3-line-height)*1em);justify-content:center;min-width:1.5em;outline:var(--typo3-kbd-border-width) solid var(--typo3-kbd-border-color);outline-offset:-1px;padding:0 var(--typo3-kbd-padding-x)!important;vertical-align:text-top} +kbd kbd{display:inline-grid;font-size:1em} +kbd:has(kbd){background-color:unset!important;box-shadow:unset!important;color:unset!important;outline:unset!important;padding:unset!important} +:root{--avatar-size-small:16px;--avatar-size-medium:32px;--avatar-size-large:48px;--avatar-size-mega:64px} +.avatar{display:block;height:var(--avatar-size,1em);line-height:var(--avatar-size,1em);position:relative;width:var(--avatar-size,1em)} +.avatar-image{overflow:hidden} +.avatar-image,.avatar-image:after{border-radius:50%;display:block;height:100%;width:100%} +.avatar-image:after{border:1px solid hsla(0,0%,100%,.1);content:"";left:0;position:absolute;top:0} +.avatar-image>img{display:block;height:auto!important;width:100%!important} +.avatar-size-small{--avatar-size:var(--avatar-size-small)} +.avatar-size-medium{--avatar-size:var(--avatar-size-medium)} +.avatar-size-large{--avatar-size:var(--avatar-size-large)} +.avatar-size-mega{--avatar-size:var(--avatar-size-mega)} +.avatar-icon{bottom:0;height:calc(var(--avatar-size, 1em)*.5);inset-inline-end:0;position:absolute;width:calc(var(--avatar-size, 1em)*.5)} +.avatar-icon .icon{--icon-size:calc(var(--avatar-size, 1em)*0.5);display:block} +.callout{--typo3-callout-color:var(--typo3-surface-container-default-text);--typo3-callout-bg:var(--typo3-surface-container-default-bg);--typo3-callout-border-color:var(--typo3-state-default-bg);--typo3-callout-icon-color:var(--typo3-state-default-color);--typo3-callout-icon-bg:var(--typo3-state-default-bg);--typo3-callout-border-radius:var(--typo3-component-border-radius);--typo3-callout-padding-y:1rem;--typo3-callout-padding-x:1rem;background-color:var(--typo3-callout-bg);color:var(--typo3-callout-color);display:flex;gap:calc(var(--typo3-callout-padding-x)*.75);-webkit-border-start:.5rem solid var(--typo3-callout-border-color);border-inline-start:.5rem solid var(--typo3-callout-border-color);border-radius:var(--typo3-callout-border-radius);margin-bottom:var(--typo3-spacing);overflow-wrap:break-word;padding:var(--typo3-callout-padding-y) var(--typo3-callout-padding-x);word-wrap:break-word;-webkit-hyphens:auto;hyphens:auto;word-break:break-word} +.callout-icon{margin-top:-2px} +.callout-content{display:flex;flex-direction:column;flex-grow:1;justify-content:center} +.callout-content>*{width:100%} +.callout-title{font-size:1.2em;line-height:1.2;margin-bottom:.5em} +.callout-body>:last-child{margin-bottom:0} +.callout-sm{--typo3-callout-padding-y:.5rem;--typo3-callout-padding-x:.5rem} +.callout-sm .callout-title{font-size:1em;margin:0} +.callout-primary{--typo3-callout-color:var(--typo3-surface-container-primary-text);--typo3-callout-bg:var(--typo3-surface-container-primary);--typo3-callout-border-color:var(--typo3-state-primary-bg);--typo3-callout-icon-color:var(--typo3-state-primary-color);--typo3-callout-icon-bg:var(--typo3-state-primary-bg)} +.callout-secondary{--typo3-callout-color:var(--typo3-surface-container-secondary-text);--typo3-callout-bg:var(--typo3-surface-container-secondary);--typo3-callout-border-color:var(--typo3-state-secondary-bg);--typo3-callout-icon-color:var(--typo3-state-secondary-color);--typo3-callout-icon-bg:var(--typo3-state-secondary-bg)} +.callout-info{--typo3-callout-color:var(--typo3-surface-container-info-text);--typo3-callout-bg:var(--typo3-surface-container-info);--typo3-callout-border-color:var(--typo3-state-info-bg);--typo3-callout-icon-color:var(--typo3-state-info-color);--typo3-callout-icon-bg:var(--typo3-state-info-bg)} +.callout-success{--typo3-callout-color:var(--typo3-surface-container-success-text);--typo3-callout-bg:var(--typo3-surface-container-success);--typo3-callout-border-color:var(--typo3-state-success-bg);--typo3-callout-icon-color:var(--typo3-state-success-color);--typo3-callout-icon-bg:var(--typo3-state-success-bg)} +.callout-warning{--typo3-callout-color:var(--typo3-surface-container-warning-text);--typo3-callout-bg:var(--typo3-surface-container-warning);--typo3-callout-border-color:var(--typo3-state-warning-bg);--typo3-callout-icon-color:var(--typo3-state-warning-color);--typo3-callout-icon-bg:var(--typo3-state-warning-bg)} +.callout-danger{--typo3-callout-color:var(--typo3-surface-container-danger-text);--typo3-callout-bg:var(--typo3-surface-container-danger);--typo3-callout-border-color:var(--typo3-state-danger-bg);--typo3-callout-icon-color:var(--typo3-state-danger-color);--typo3-callout-icon-bg:var(--typo3-state-danger-bg)} +.callout-notice{--typo3-callout-color:var(--typo3-surface-container-notice-text);--typo3-callout-bg:var(--typo3-surface-container-notice);--typo3-callout-border-color:var(--typo3-state-notice-bg);--typo3-callout-icon-color:var(--typo3-state-notice-color);--typo3-callout-icon-bg:var(--typo3-state-notice-bg)} +.callout-default{--typo3-callout-color:var(--typo3-surface-container-default-text);--typo3-callout-bg:var(--typo3-surface-container-default);--typo3-callout-border-color:var(--typo3-state-default-bg);--typo3-callout-icon-color:var(--typo3-state-default-color);--typo3-callout-icon-bg:var(--typo3-state-default-bg)} +.statusreport{--typo3-statusreport-color:var(--typo3-component-color);--typo3-statusreport-bg:var(--typo3-component-bg);--typo3-statusreport-border-color:color-mix(in srgb,var(--typo3-statusreport-bg),var(--typo3-statusreport-color) var(--typo3-border-mix));--typo3-statusreport-border-radius:var(--typo3-component-border-radius);--typo3-statusreport-box-shadow:var(--typo3-component-box-shadow);--typo3-statusreport-spacing:.5rem;--typo3-statusreport-icon-size:16px;--typo3-statusreport-icon-color:var(--typo3-state-default-color);--typo3-statusreport-icon-bg:var(--typo3-state-default-bg);--typo3-statusreport-icon-primary-color:var(--typo3-state-primary-color);--typo3-statusreport-icon-primary-bg:var(--typo3-state-primary-bg);--typo3-statusreport-icon-secondary-color:var(--typo3-state-secondary-color);--typo3-statusreport-icon-secondary-bg:var(--typo3-state-secondary-bg);--typo3-statusreport-icon-info-color:var(--typo3-state-info-color);--typo3-statusreport-icon-info-bg:var(--typo3-state-info-bg);--typo3-statusreport-icon-success-color:var(--typo3-state-success-color);--typo3-statusreport-icon-success-bg:var(--typo3-state-success-bg);--typo3-statusreport-icon-warning-color:var(--typo3-state-warning-color);--typo3-statusreport-icon-warning-bg:var(--typo3-state-warning-bg);--typo3-statusreport-icon-danger-color:var(--typo3-state-danger-color);--typo3-statusreport-icon-danger-bg:var(--typo3-state-danger-bg);--typo3-statusreport-icon-notice-color:var(--typo3-state-notice-color);--typo3-statusreport-icon-notice-bg:var(--typo3-state-notice-bg);--typo3-statusreport-icon-default-color:var(--typo3-state-default-color);--typo3-statusreport-icon-default-bg:var(--typo3-state-default-bg);background-color:var(--typo3-statusreport-bg);border:1px solid var(--typo3-statusreport-border-color);color:var(--typo3-statusreport-color);display:grid;gap:var(--typo3-statusreport-spacing);grid-template:"statusreport-indicator statusreport-title statusreport-body"/calc(var(--typo3-statusreport-icon-size)*1.5) 400px auto;padding:calc(var(--typo3-statusreport-spacing)*1.5);width:100%} +.statusreport-indicator{display:flex;grid-area:statusreport-indicator;justify-content:center} +.statusreport-title{grid-area:statusreport-title} +.statusreport-body{grid-area:statusreport-body} +.statusreport .statusreport-indicator-icon{align-items:center;color:var(--typo3-statusreport-icon-color);display:inline-flex;font-size:var(--typo3-statusreport-icon-size);height:var(--typo3-statusreport-icon-size);justify-content:center;position:relative;width:var(--typo3-statusreport-icon-size)} +.statusreport .statusreport-indicator-icon:before{background-color:var(--typo3-statusreport-icon-bg);border-radius:50%;content:" ";height:calc(var(--typo3-statusreport-icon-size)*1.5);left:50%;position:absolute;top:50%;transform:translate(calc(-50%*var(--typo3-position-modifier)),-50%);width:calc(var(--typo3-statusreport-icon-size)*1.5)} +.statusreport-wrapper{border-radius:var(--typo3-statusreport-border-radius);box-shadow:var(--typo3-statusreport-box-shadow);margin-bottom:var(--typo3-spacing)} +.statusreport-wrapper .statusreport:first-child{border-top-left-radius:var(--typo3-statusreport-border-radius);border-top-right-radius:var(--typo3-statusreport-border-radius)} +.statusreport-wrapper .statusreport:last-child{border-bottom-left-radius:var(--typo3-statusreport-border-radius);border-bottom-right-radius:var(--typo3-statusreport-border-radius)} +.statusreport-wrapper .statusreport+.statusreport{margin-top:-1px} +.statusreport[data-severity=primary]{--typo3-statusreport-icon-bg:var(--typo3-statusreport-icon-primary-bg);--typo3-statusreport-icon-color:var(--typo3-statusreport-icon-primary-color)} +.statusreport[data-severity=secondary]{--typo3-statusreport-icon-bg:var(--typo3-statusreport-icon-secondary-bg);--typo3-statusreport-icon-color:var(--typo3-statusreport-icon-secondary-color)} +.statusreport[data-severity=success]{--typo3-statusreport-icon-bg:var(--typo3-statusreport-icon-success-bg);--typo3-statusreport-icon-color:var(--typo3-statusreport-icon-success-color)} +.statusreport[data-severity=info]{--typo3-statusreport-icon-bg:var(--typo3-statusreport-icon-info-bg);--typo3-statusreport-icon-color:var(--typo3-statusreport-icon-info-color)} +.statusreport[data-severity=warning]{--typo3-statusreport-icon-bg:var(--typo3-statusreport-icon-warning-bg);--typo3-statusreport-icon-color:var(--typo3-statusreport-icon-warning-color)} +.statusreport[data-severity=danger]{--typo3-statusreport-icon-bg:var(--typo3-statusreport-icon-danger-bg);--typo3-statusreport-icon-color:var(--typo3-statusreport-icon-danger-color)} +.statusreport[data-severity=light]{--typo3-statusreport-icon-bg:var(--typo3-statusreport-icon-light-bg);--typo3-statusreport-icon-color:var(--typo3-statusreport-icon-light-color)} +.statusreport[data-severity=default]{--typo3-statusreport-icon-bg:var(--typo3-statusreport-icon-default-bg);--typo3-statusreport-icon-color:var(--typo3-statusreport-icon-default-color)} +.statusreport[data-severity=notice]{--typo3-statusreport-icon-bg:var(--typo3-statusreport-icon-notice-bg);--typo3-statusreport-icon-color:var(--typo3-statusreport-icon-notice-color)} +.statusreport[data-severity=dark]{--typo3-statusreport-icon-bg:var(--typo3-statusreport-icon-dark-bg);--typo3-statusreport-icon-color:var(--typo3-statusreport-icon-dark-color)} +.statusreport-wrapper{container-type:inline-size} +@container (max-width: 800px){ +.statusreport{grid-template:"statusreport-indicator statusreport-title statusreport-body"/calc(var(--typo3-statusreport-icon-size)*1.5) 200px auto}} +@container (max-width: 500px){ +.statusreport{grid-template:"statusreport-indicator statusreport-title" "statusreport-indicator statusreport-body"/calc(var(--typo3-statusreport-icon-size)*1.5) auto}} +.diff,.diff-inline{--typo3-diff-padding-y:var(--typo3-component-padding-y);--typo3-diff-padding-x:var(--typo3-component-padding-x);--typo3-diff-bg:var(--typo3-component-bg);--typo3-diff-color:var(--typo3-component-color);--typo3-diff-border-color:var(--typo3-component-border-color);--typo3-diff-border-width:var(--typo3-component-border-width);--typo3-diff-title-bg:color-mix(in srgb,var(--typo3-diff-bg),var(--typo3-diff-color) 15%);--typo3-diff-del-bg:var(--typo3-surface-container-danger);--typo3-diff-del-color:var(--typo3-surface-container-danger-text);--typo3-diff-ins-bg:var(--typo3-surface-container-success);--typo3-diff-ins-color:var(--typo3-surface-container-success-text)} +.diff{background-color:var(--typo3-component-bg);border:var(--typo3-diff-border-width) solid var(--typo3-diff-border-color);color:var(--typo3-diff-color);display:table} +.diff .diff-item-button,.diff .diff-item-result,.diff .diff-item-text,.diff .diff-item-title{display:table-cell;padding:var(--typo3-diff-padding-y) var(--typo3-component-padding-x)} +.diff .diff-item{display:table-row} +.diff .diff-item+.diff-item .diff-item-button,.diff .diff-item+.diff-item .diff-item-result,.diff .diff-item+.diff-item .diff-item-text,.diff .diff-item+.diff-item .diff-item-title{border-top:var(--typo3-diff-border-width) solid var(--typo3-diff-border-color)} +.diff-item-title{background-color:var(--typo3-diff-title-bg);-webkit-padding-end:10px;font-style:italic;padding-inline-end:10px;white-space:nowrap} +.diff-item-result{font-family:var(--typo3-font-family-monospace);white-space:pre-wrap;width:100%;word-break:break-word;word-wrap:break-word} +.diff-item-result del{background-color:var(--typo3-diff-del-bg);color:var(--typo3-diff-del-color);text-decoration:none} +.diff-item-result ins{background-color:var(--typo3-diff-ins-bg);color:var(--typo3-diff-ins-color);text-decoration:none} +.diff-item-result.diff-item-result-inline{white-space:normal} +.diff-group{-webkit-border-start:5px solid var(--typo3-component-border-color);border-bottom:1px dotted var(--typo3-component-border-color);border-inline-start:5px solid var(--typo3-component-border-color);margin-bottom:var(--typo3-spacing);-webkit-padding-start:var(--typo3-component-padding-x);padding-bottom:var(--typo3-component-padding-y);padding-inline-start:var(--typo3-component-padding-x)} +.diff-group>:last-child{margin-bottom:0} +.diff-inline{font-family:var(--typo3-font-family-monospace)} +.diff-inline del{background-color:var(--typo3-diff-del-bg);color:var(--typo3-diff-del-color);text-decoration:none} +.diff-inline ins{background-color:var(--typo3-diff-ins-bg);color:var(--typo3-diff-ins-color);text-decoration:none} +:root{--module-color:var(--typo3-text-color-base);--module-bg:var(--typo3-surface-container-lowest);--module-width:auto;--module-layout-wide-width:auto;--module-layout-normal-width:1320px;--module-docheader-zindex:var(--typo3-zindex-header);--module-docheader-padding-y:.75rem;--module-docheader-padding-x:1.5rem;--module-docheader-padding:var(--module-docheader-padding-y) var(--module-docheader-padding-x);--module-docheader-spacing-y:.5rem;--module-docheader-spacing-x:.75rem;--module-docheader-spacing:var(--module-docheader-spacing-y) var(--module-docheader-spacing-x);--module-docheader-bg:var(--module-bg);--module-docheader-border-color:color-mix(in srgb,var(--module-docheader-bg),var(--module-color) 10%);--module-docheader-border-width:1px;--module-docheader-column-height:1.78125rem;--module-docheader-bar-height:calc(var(--module-docheader-padding-y)*2 + var(--module-docheader-column-height));--module-docheader-scroll-offset:calc((var(--module-docheader-bar-height) + var(--module-docheader-padding-y))*-1);--module-docheader-height:calc(var(--module-docheader-bar-height)*2 + var(--module-docheader-border-width));--module-docheader-box-shadow:var(--typo3-shadow-2);--module-body-padding-y:1.5rem;--module-body-padding-x:1.5rem;--module-body-padding:var(--module-body-padding-y) var(--module-body-padding-x)} +body:has(.module) :target{scroll-margin-top:var(--module-docheader-height)} +.module{background-color:var(--module-bg);color:var(--module-color);display:grid;grid-template-areas:"moduleDocHeaderNavigation" "moduleDocHeaderButtons" "moduleBody";grid-template-columns:100%;grid-template-rows:auto auto 1fr;max-width:100%;min-height:100%;position:relative;width:100%} +body>.module{min-height:100dvh} +.module-layout-wide{--module-width:var(--module-layout-wide-width)} +.module-layout-normal{--module-width:var(--module-layout-normal-width)} +.module-loading-indicator{min-height:5px;position:fixed;width:100%;z-index:calc(var(--typo3-zindex-header) + 1)} +.module-docheader{background-color:var(--module-docheader-bg);display:flex;flex-wrap:wrap;gap:var(--module-docheader-spacing);justify-content:space-between;min-height:var(--module-docheader-bar-height);padding:var(--module-docheader-padding);position:relative;z-index:var(--typo3-zindex-header)} +.module-docheader-column{align-items:center;display:flex;max-width:100%;min-height:var(--module-docheader-column-height)} +.module-docheader-column:not(:has(>*)){display:none} +.module-docheader-column:has(typo3-backend-content-navigation-toggle[hidden]){display:none} +.module-docheader-column:last-child:not(:first-child){-webkit-margin-start:auto;margin-inline-start:auto} +.module-docheader-column-grow{flex-grow:1} +.module-docheader-column-breadcrumb{flex-basis:120px;flex-grow:1;min-width:0} +.module-docheader-container{display:flex;flex-wrap:wrap;gap:var(--module-docheader-spacing);margin:0 auto;max-width:var(--module-width);width:100%} +.module-docheader .dropdown-menu{max-height:calc(100dvh - var(--module-docheader-height))} +.module-docheader-navigation{border-bottom:var(--module-docheader-border-width) solid var(--module-docheader-border-color);grid-area:moduleDocHeaderNavigation;min-height:calc(var(--module-docheader-bar-height) + var(--module-docheader-border-width))} +.module-docheader-buttons:has(.module-docheader-column:empty){padding:0} +.module-docheader-buttons{animation:module-docheader-buttons-shadow linear forwards;grid-area:moduleDocHeaderButtons;position:-webkit-sticky;position:sticky;top:0;z-index:calc(var(--typo3-zindex-header) - 1);animation-timeline:scroll(root);animation-range:0 calc(var(--module-docheader-bar-height) + 1px)} +@keyframes module-docheader-buttons-shadow{ +0%{box-shadow:0 0 0 transparent} +to{box-shadow:var(--module-docheader-box-shadow)}} +.module-body{background-color:var(--module-bg);contain:inline-size;grid-area:moduleBody;padding:var(--module-body-padding);position:relative} +.module-body>.container{padding-left:0;padding-right:0} +.module-body .container-small{margin:0 auto;max-width:768px} +.module-body>:last-child{margin-bottom:0} +.module-body-container{margin:0 auto;max-width:var(--module-width);width:100%} +.panel{--typo3-panel-color:var(--typo3-component-color);--typo3-panel-bg:var(--typo3-component-bg);--typo3-panel-border-color:color-mix(in srgb,var(--typo3-panel-bg),var(--typo3-panel-color) var(--typo3-border-mix));--typo3-panel-border-width:var(--typo3-component-border-width);--typo3-panel-border-radius:var(--typo3-component-border-radius);--typo3-panel-border-radius-top:var(--typo3-panel-border-radius);--typo3-panel-border-radius-bottom:var(--typo3-panel-border-radius);--typo3-panel-border-radius-inner-top:max(0px,calc(var(--typo3-panel-border-radius-top) - var(--typo3-panel-border-width)));--typo3-panel-border-radius-inner-bottom:max(0px,calc(var(--typo3-panel-border-radius-bottom) - var(--typo3-panel-border-width)));--typo3-panel-padding-y:1rem;--typo3-panel-padding-x:1rem;--typo3-panel-sm-padding-y:.75rem;--typo3-panel-sm-padding-x:.75rem;--typo3-panel-header-padding-y:calc(var(--typo3-panel-padding-y)*0.75);--typo3-panel-header-padding-x:var(--typo3-panel-padding-x);--typo3-panel-header-bg:var(--typo3-surface-container-low);--typo3-panel-header-color:var(--typo3-text-color-base);--typo3-panel-box-shadow:var(--typo3-component-box-shadow);--typo3-panel-progress-bg:var(--typo3-state-primary-bg);--typo3-panel-progress-height:3px;--typo3-panel-primary-header-color:var(--typo3-surface-container-primary-text);--typo3-panel-primary-header-bg:var(--typo3-surface-container-primary);--typo3-panel-primary-border-color:color-mix(in srgb,var(--typo3-panel-primary-header-bg),var(--typo3-panel-primary-header-color) var(--typo3-border-mix));--typo3-panel-secondary-header-color:var(--typo3-surface-container-secondary-text);--typo3-panel-secondary-header-bg:var(--typo3-surface-container-secondary);--typo3-panel-secondary-border-color:color-mix(in srgb,var(--typo3-panel-secondary-header-bg),var(--typo3-panel-secondary-header-color) var(--typo3-border-mix));--typo3-panel-info-header-color:var(--typo3-surface-container-info-text);--typo3-panel-info-header-bg:var(--typo3-surface-container-info);--typo3-panel-info-border-color:color-mix(in srgb,var(--typo3-panel-info-header-bg),var(--typo3-panel-info-header-color) var(--typo3-border-mix));--typo3-panel-success-header-color:var(--typo3-surface-container-success-text);--typo3-panel-success-header-bg:var(--typo3-surface-container-success);--typo3-panel-success-border-color:color-mix(in srgb,var(--typo3-panel-success-header-bg),var(--typo3-panel-success-header-color) var(--typo3-border-mix));--typo3-panel-warning-header-color:var(--typo3-surface-container-warning-text);--typo3-panel-warning-header-bg:var(--typo3-surface-container-warning);--typo3-panel-warning-border-color:color-mix(in srgb,var(--typo3-panel-warning-header-bg),var(--typo3-panel-warning-header-color) var(--typo3-border-mix));--typo3-panel-danger-header-color:var(--typo3-surface-container-danger-text);--typo3-panel-danger-header-bg:var(--typo3-surface-container-danger);--typo3-panel-danger-border-color:color-mix(in srgb,var(--typo3-panel-danger-header-bg),var(--typo3-panel-danger-header-color) var(--typo3-border-mix));--typo3-panel-notice-header-color:var(--typo3-surface-container-notice-text);--typo3-panel-notice-header-bg:var(--typo3-surface-container-notice);--typo3-panel-notice-border-color:color-mix(in srgb,var(--typo3-panel-notice-header-bg),var(--typo3-panel-notice-header-color) var(--typo3-border-mix));--typo3-panel-default-header-color:var(--typo3-surface-container-default-text);--typo3-panel-default-header-bg:var(--typo3-surface-container-default);--typo3-panel-default-border-color:color-mix(in srgb,var(--typo3-panel-default-header-bg),var(--typo3-panel-default-header-color) var(--typo3-border-mix));display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-clip:border-box;background-color:var(--typo3-panel-bg);border:var(--typo3-panel-border-width) solid var(--typo3-panel-border-color);border-radius:var(--typo3-panel-border-radius-top) var(--typo3-panel-border-radius-top) var(--typo3-panel-border-radius-bottom) var(--typo3-panel-border-radius-bottom);box-shadow:var(--typo3-panel-box-shadow);color:var(--typo3-panel-color);-webkit-margin-after:var(--typo3-spacing);margin-block-end:var(--typo3-spacing);position:relative} +.panel .panel-collapse>.table-fit,.panel .panel-collapse>form>.table-fit,.panel>.table-fit{border:none;border-radius:0;box-shadow:none;margin:0} +.panel .panel-collapse>.alert,.panel>.alert{--typo3-alert-margin-bottom:0;--typo3-alert-border-width:0;--typo3-alert-border-radius:0} +.panel .panel-collapse>.form-section,.panel>.form-section{--typo3-form-section-padding-y:var(--typo3-panel-padding-y);--typo3-form-section-padding-x:var(--typo3-panel-padding-x)} +.panel .panel-collapse>.tab-wrapper,.panel>.tab-wrapper{padding:var(--typo3-panel-padding-y) var(--typo3-panel-padding-x)} +.panel>.alert:first-child,.panel>.table-fit:first-child{border-start-end-radius:var(--typo3-panel-border-radius-inner-bottom);border-start-start-radius:var(--typo3-panel-border-radius-inner-bottom);border-top:0} +.panel .panel-collapse>form>.table-fit:last-child,.panel:last-child .panel-collapse>.alert:last-child,.panel:last-child .panel-collapse>.table-fit:last-child,.panel>.alert:last-child,.panel>.table-fit:last-child,:not(.panel-group)>.panel>.panel-collapse>.alert:last-child,:not(.panel-group)>.panel>.panel-collapse>.table-fit:last-child{border-end-end-radius:var(--typo3-panel-border-radius-inner-bottom);border-end-start-radius:var(--typo3-panel-border-radius-inner-bottom)} +.panel-loader{padding:var(--typo3-panel-padding-y) var(--typo3-panel-padding-x)} +.panel:has(.panel-progress){min-height:5px} +.panel-progress{background-color:transparent;border-radius:var(--typo3-panel-border-radius-inner-top) var(--typo3-panel-border-radius-inner-top) 0 0;display:none;height:max(2 * var(--typo3-panel-border-radius-inner-top),var(--typo3-panel-progress-height));inset-inline-start:0;-webkit-mask-image:linear-gradient(to bottom,#000 0,#000 var(--typo3-panel-progress-height),transparent var(--typo3-panel-progress-height));mask-image:linear-gradient(to bottom,#000 0,#000 var(--typo3-panel-progress-height),transparent var(--typo3-panel-progress-height));-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;overflow:hidden;position:absolute;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%;z-index:1} +.panel-progress .panel-progress-bar{background-color:var(--typo3-panel-progress-bg);display:block;height:100%} +.panel-has-progress>.panel-progress{display:block} +.panel>typo3-backend-progress-bar{--progress-border-radius:var(--typo3-panel-border-radius-inner-top) var(--typo3-panel-border-radius-inner-top) 0 0;--progress-bar-height:max(calc(var(--typo3-panel-border-radius-inner-top)*2),var(--typo3-panel-progress-height));inset-inline-start:0;-webkit-mask-image:linear-gradient(to bottom,#000 0,#000 var(--typo3-panel-progress-height),transparent var(--typo3-panel-progress-height));mask-image:linear-gradient(to bottom,#000 0,#000 var(--typo3-panel-progress-height),transparent var(--typo3-panel-progress-height));-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;overflow:hidden;position:absolute;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%;z-index:2} +.panel-button{align-items:center;background:transparent;border:none;display:flex;flex-grow:1;gap:.25rem;min-width:0;padding:0;position:relative;text-align:start;width:100%} +.panel-button:after{bottom:calc(var(--typo3-panel-padding-y)*-1);content:"";left:0;position:absolute;right:0;top:calc(var(--typo3-panel-padding-y)*-1)} +.panel-heading{background-color:var(--typo3-panel-header-bg);border-radius:var(--typo3-panel-border-radius-inner-top) var(--typo3-panel-border-radius-inner-top) var(--typo3-panel-border-radius-inner-bottom) var(--typo3-panel-border-radius-inner-bottom);color:var(--typo3-panel-header-color);container-type:inline-size;font-size:var(--typo3-font-size);font-weight:400;margin:0;outline-offset:0;padding:var(--typo3-panel-header-padding-y) var(--typo3-panel-header-padding-x);position:relative} +.panel-heading:not(:last-child,.collapsed,:has(.collapsed),:has(+.collapse:not(.show)),:has(+.collapsing),:has(.panel-button[aria-expanded=false])){--typo3-panel-border-radius-inner-bottom:0} +.panel-heading [data-bs-toggle=collapse]{outline:none} +.panel-heading:has([data-bs-toggle=collapse]:focus-visible){outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-panel-border-color),transparent 25%);z-index:1} +.panel-heading .caret{flex-shrink:0;width:var(--icon-size-small);--typo3-caret-color:var(--typo3-panel-header-color)} +.panel-heading.collapsed .caret,.panel-heading:has(.collapsed) .caret,.panel-heading:has(.panel-button[aria-expanded=false]) .caret{--typo3-caret-rotation:calc(-90deg*var(--typo3-position-modifier))} +.panel-heading-row{align-items:center;display:flex;flex-grow:1;gap:.5rem;max-width:100%} +.panel-heading-row .panel-button{width:auto} +.panel-heading-row-spread{flex-wrap:wrap;justify-content:space-between} +@container (max-width: 500px){ +.panel-heading-row{flex-wrap:wrap} +.panel-heading-row .panel-button{flex-basis:100%} +.panel-heading-row .panel-actions{-webkit-padding-start:calc(var(--icon-size-small) + .25rem);padding-inline-start:calc(var(--icon-size-small) + .25rem)}} +.panel-heading-column{align-items:center;display:flex;flex-wrap:wrap;gap:.75rem} +.panel-title{flex-grow:1;font-size:var(--typo3-font-size);line-height:1.2;margin-bottom:0;margin-top:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.panel-meta{color:var(--typo3-text-color-variant);display:block;font-size:var(--typo3-font-size-small);font-weight:400;line-height:1.2} +.panel-heading:has([data-bs-toggle=collapse]:hover) .panel-title{text-decoration:underline} +.panel-badge,.panel-icon,.panel-thumbnail{align-items:center;display:flex;flex-shrink:0;justify-content:center} +.panel-thumbnail{height:45px;overflow:hidden;width:60px;-webkit-margin-end:.25rem;margin-inline-end:.25rem} +.panel-thumbnail>img{border-radius:4px;height:auto;max-height:100%;max-width:100%;width:auto} +.panel-actions{align-items:center;display:flex;flex-shrink:0;flex-wrap:wrap;gap:.25rem;justify-content:end;position:relative;z-index:1} +.panel-body{padding:var(--typo3-panel-padding-y) var(--typo3-panel-padding-x)} +.panel-body:last-child{border-end-end-radius:var(--typo3-panel-border-radius-inner-bottom);border-end-start-radius:var(--typo3-panel-border-radius-inner-bottom)} +.panel-body>:first-child{margin-top:0} +.panel-body>:last-child{margin-bottom:0} +.panel-body-overflow{max-width:100%;overflow:auto} +.panel-collapse .form-section:not(.hide):not(:has(~.form-section:not(.hide))),.panel-collapse:last-child{border-end-end-radius:var(--typo3-panel-border-radius-inner-bottom);border-end-start-radius:var(--typo3-panel-border-radius-inner-bottom)} +.panel-footer{padding:var(--typo3-panel-padding-y) var(--typo3-panel-padding-x)} +.panel-list{padding-left:var(--typo3-panel-padding-x)} +.panel-list>li+li{margin-top:.2rem} +.panel-condensed{--typo3-panel-padding-y:var(--typo3-panel-sm-padding-y);--typo3-panel-padding-x:var(--typo3-panel-sm-padding-x)} +.panel-active,.panel-primary{--typo3-panel-header-color:var(--typo3-panel-primary-header-color);--typo3-panel-header-bg:var(--typo3-panel-primary-header-bg);--typo3-panel-border-color:var(--typo3-panel-primary-border-color)} +.panel-secondary{--typo3-panel-header-color:var(--typo3-panel-secondary-header-color);--typo3-panel-header-bg:var(--typo3-panel-secondary-header-bg);--typo3-panel-border-color:var(--typo3-panel-secondary-border-color)} +.panel-important,.panel-info{--typo3-panel-header-color:var(--typo3-panel-info-header-color);--typo3-panel-header-bg:var(--typo3-panel-info-header-bg);--typo3-panel-border-color:var(--typo3-panel-info-border-color)} +.panel-feature,.panel-success{--typo3-panel-header-color:var(--typo3-panel-success-header-color);--typo3-panel-header-bg:var(--typo3-panel-success-header-bg);--typo3-panel-border-color:var(--typo3-panel-success-border-color)} +.panel-deprecation,.panel-warning{--typo3-panel-header-color:var(--typo3-panel-warning-header-color);--typo3-panel-header-bg:var(--typo3-panel-warning-header-bg);--typo3-panel-border-color:var(--typo3-panel-warning-border-color)} +.panel-breaking,.panel-danger{--typo3-panel-header-color:var(--typo3-panel-danger-header-color);--typo3-panel-header-bg:var(--typo3-panel-danger-header-bg);--typo3-panel-border-color:var(--typo3-panel-danger-border-color)} +.panel-notice{--typo3-panel-header-color:var(--typo3-panel-notice-header-color);--typo3-panel-header-bg:var(--typo3-panel-notice-header-bg);--typo3-panel-border-color:var(--typo3-panel-notice-border-color)} +.panel-default{--typo3-panel-header-color:var(--typo3-panel-default-header-color);--typo3-panel-header-bg:var(--typo3-panel-default-header-bg);--typo3-panel-border-color:var(--typo3-panel-default-border-color)} +.panel-hidden,.panel-placeholder{border-style:dashed;opacity:.5} +.panel-hidden>.panel-heading,.panel-placeholder>.panel-heading{background-color:transparent} +.panel-hidden:focus-within,.panel-hidden:hover,.panel-placeholder:focus-within,.panel-placeholder:hover{opacity:1} +.panel-group{--typo3-panel-group-border-radius:var(--typo3-component-border-radius);--typo3-panel-group-border-radius-top:var(--typo3-panel-group-border-radius);--typo3-panel-group-border-radius-bottom:var(--typo3-panel-group-border-radius);--typo3-panel-group-box-shadow:var(--typo3-component-box-shadow);border-radius:var(--typo3-panel-group-border-radius-top) var(--typo3-panel-group-border-radius-top) var(--typo3-panel-group-border-radius-bottom) var(--typo3-panel-group-border-radius-bottom);box-shadow:var(--typo3-panel-group-box-shadow);display:flex;flex-flow:column;margin-bottom:var(--typo3-spacing)} +.panel-group:empty{display:none} +.panel-group>.panel{--typo3-panel-border-radius-top:0px;--typo3-panel-border-radius-bottom:0px;box-shadow:none;margin-bottom:0;margin-top:calc(var(--typo3-panel-border-width)*-1)} +.panel-group>.panel:first-child{margin-top:0;--typo3-panel-border-radius-top:var(--typo3-panel-group-border-radius)} +.panel-group>.panel:last-child{--typo3-panel-border-radius-bottom:var(--typo3-panel-group-border-radius)} +.panel,.panel-heading{transition:all .2s ease-in-out;transition-property:box-shadow,border,border-radius,transform} +.table{--typo3-table-font-size:var(--typo3-font-size);--typo3-table-color:var(--typo3-component-color);--typo3-table-bg:var(--typo3-component-bg);--typo3-table-bg-type:initial;--typo3-table-bg-state:initial;--typo3-table-border-style:none;--typo3-table-border-width:var(--typo3-component-border-width);--typo3-table-border-color:var(--typo3-component-border-color);--typo3-table-padding-y:.75rem;--typo3-table-padding-x:1rem;--typo3-table-sm-font-size:var(--typo3-font-size-small);--typo3-table-sm-padding-y:.5rem;--typo3-table-sm-padding-x:.75rem;--typo3-table-primary-color:var(--typo3-surface-container-primary-text);--typo3-table-primary-bg:var(--typo3-surface-container-primary);--typo3-table-primary-border-color:var(--typo3-state-primary-border-color);--typo3-table-secondary-color:var(--typo3-surface-container-secondary-text);--typo3-table-secondary-bg:var(--typo3-surface-container-secondary);--typo3-table-secondary-border-color:var(--typo3-state-secondary-border-color);--typo3-table-info-color:var(--typo3-surface-container-info-text);--typo3-table-info-bg:var(--typo3-surface-container-info);--typo3-table-info-border-color:var(--typo3-state-info-border-color);--typo3-table-success-color:var(--typo3-surface-container-success-text);--typo3-table-success-bg:var(--typo3-surface-container-success);--typo3-table-success-border-color:var(--typo3-state-success-border-color);--typo3-table-warning-color:var(--typo3-surface-container-warning-text);--typo3-table-warning-bg:var(--typo3-surface-container-warning);--typo3-table-warning-border-color:var(--typo3-state-warning-border-color);--typo3-table-danger-color:var(--typo3-surface-container-danger-text);--typo3-table-danger-bg:var(--typo3-surface-container-danger);--typo3-table-danger-border-color:var(--typo3-state-danger-border-color);--typo3-table-notice-color:var(--typo3-surface-container-notice-text);--typo3-table-notice-bg:var(--typo3-surface-container-notice);--typo3-table-notice-border-color:var(--typo3-state-notice-border-color);--typo3-table-default-color:var(--typo3-surface-container-default-text);--typo3-table-default-bg:var(--typo3-surface-container-default);--typo3-table-default-border-color:var(--typo3-state-default-border-color);font-size:var(--typo3-table-font-size);margin-bottom:var(--typo3-spacing);width:100%} +.table,.table>:not(caption)>*>*{border-color:var(--typo3-table-border-color)} +.table>:not(caption)>*>*{background-color:var(--typo3-table-bg-state,var(--typo3-table-bg-type,var(--typo3-table-bg)));border-bottom-width:var(--typo3-table-border-width);color:var(--typo3-table-color);padding:var(--typo3-table-padding-y) calc(var(--typo3-table-padding-x)/2);vertical-align:middle} +.table>:not(caption)>*>*>:last-child{margin-bottom:0} +.table>:not(caption)>*>:is(th){white-space:nowrap} +.table>:not(caption)>*>:first-child{-webkit-padding-start:var(--typo3-table-padding-x);padding-inline-start:var(--typo3-table-padding-x)} +.table>:not(caption)>*>:last-child{-webkit-padding-end:var(--typo3-table-padding-x);padding-inline-end:var(--typo3-table-padding-x)} +.table>:not(caption)>.inactive>*{color:color-mix(in srgb,var(--typo3-table-color),transparent 50%)} +.table caption{color:var(--typo3-text-color-variant);padding-bottom:var(--typo3-table-padding-y);padding-top:var(--typo3-table-padding-y)} +.table .col-avatar,.table .col-checkbox,.table .col-icon{-webkit-padding-end:0;padding-inline-end:0} +.table .col-50{width:50%} +.table .col-task,.table .col-title{width:99%} +.table .col-title-flexible{max-width:40ch;min-width:200px} +.table .col-checkbox .form-check{--typo3-form-check-top-correction:0;--typo3-form-check-margin-bottom:0} +.table .col-checkbox,.table .col-icon{box-sizing:content-box;min-width:16px;white-space:nowrap;width:16px} +.table .col-indicator{width:10px;-webkit-padding-end:0;padding-inline-end:0;white-space:nowrap} +.table .col-time{width:8ch} +.table .col-datetime,.table .col-time{box-sizing:content-box;white-space:nowrap} +.table .col-datetime{width:14ch} +.table .col-avatar{box-sizing:content-box;width:32px} +.table .col-action,.table .col-username{width:15ch} +.table .col-fieldname{min-width:200px;width:200px} +@media (min-width:768px){ +.table .col-fieldname{width:250px}} +.table .col-language{width:200px} +.table .col-recordtitle{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:250px} +.table .col-path{max-width:40ch;min-width:200px;white-space:wrap;word-wrap:break-word;word-break:break-all} +.table .col-action,.table .col-language,.table .col-recordtitle,.table .col-state{min-width:120px} +.table .col-differences,.table .col-task{min-width:400px} +.table .col-clipboard,.table .col-control,.table .col-nowrap,.table .col-radiogroup{white-space:nowrap!important} +.table .col-clipboard,.table .col-control{text-align:end} +.table .col-border-left{-webkit-border-start:var(--typo3-table-border-width) solid var(--typo3-table-border-color);border-inline-start:var(--typo3-table-border-width) solid var(--typo3-table-border-color)} +.table .col-min{min-width:150px} +.table .col-responsive{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +@media (min-width:768px){ +.table .col-word-break{word-wrap:break-word;word-break:break-all}} +.table .col-white-space-normal{white-space:normal} +.table .col-progress{padding-bottom:calc(var(--typo3-table-padding-y)*.75);padding-top:calc(var(--typo3-table-padding-y)*.75)} +.table-sm{--typo3-table-font-size:var(--typo3-table-sm-font-size);--typo3-table-padding-x:var(--typo3-table-sm-padding-x)} +.caption-top{caption-side:top} +.table-striped-columns>:not(caption)>tr>:nth-child(2n),.table-striped>tbody>tr:nth-of-type(odd)>*{--typo3-table-bg-type:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 3%)} +.table-hover>tbody>tr:hover>*{--typo3-table-bg-state:color-mix(in srgb,var(--typo3-table-bg-type,var(--typo3-table-bg)),var(--typo3-table-color) 6%)} +.table-bordered>:not(caption)>*{border-width:var(--typo3-table-border-width) 0} +.table-bordered>:not(caption)>*>*{border-width:0 var(--typo3-table-border-width)} +.table-transparent{--typo3-table-bg:transparent} +.table-vertical-top>:not(caption)>*>*{vertical-align:top} +.table-center>:not(caption)>*>*{text-align:center} +.table-fit{--typo3-table-color:var(--typo3-component-color);--typo3-table-bg:var(--typo3-component-bg);--typo3-table-border-width:var(--typo3-component-border-width);--typo3-table-border-color:var(--typo3-component-border-color);--typo3-table-border-radius:var(--typo3-component-border-radius);--typo3-table-progress-height:3px;border-radius:var(--typo3-table-border-radius);box-shadow:var(--typo3-component-box-shadow);color:var(--typo3-table-color);margin-bottom:var(--typo3-spacing);overflow-x:auto;overflow-y:hidden;width:100%;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar;background-color:var(--typo3-table-bg);border:1px solid var(--typo3-table-border-color)} +td .table-fit{margin-bottom:0} +.table-fit caption{border-top:var(--typo3-table-border-width) solid var(--typo3-table-border-color);-webkit-padding-start:var(--typo3-component-padding-x);padding-inline-start:var(--typo3-component-padding-x);-webkit-padding-end:var(--typo3-component-padding-x);padding-inline-end:var(--typo3-component-padding-x)} +.table-fit .caption-top caption{border-bottom:var(--typo3-table-border-width) solid var(--typo3-table-border-color);border-top:0} +.table-fit>typo3-backend-progress-bar{--progress-border-radius:0;--progress-bar-height:var(--typo3-table-progress-height);height:0;-webkit-user-select:none;-moz-user-select:none;user-select:none} +.table-fit>.table{margin-bottom:0} +.table-fit>.table>:first-child>*,.table-fit>.table>colgroup:first-child+*>*{border-top-width:0} +.table-fit>.table>*>*>:first-child{-webkit-border-start:0;border-inline-start:0} +.table-fit>.table>*>*>:last-child{-webkit-border-end:0;border-inline-end:0} +.table-fit>.table>:last-child>:last-child,.table-fit>.table>:last-child>:last-child>*{border-bottom-width:0} +.table-fit-wrap td,.table-fit-wrap th{white-space:normal} +.table-fit-inline-block{display:inline-block;margin:0;max-width:100%;width:auto} +.table-fit-inline-block>.table{width:auto} +.table .primary,.table-primary{--typo3-table-color:var(--typo3-table-primary-color);--typo3-table-bg:var(--typo3-table-primary-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .secondary,.table-secondary{--typo3-table-color:var(--typo3-table-secondary-color);--typo3-table-bg:var(--typo3-table-secondary-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .info,.table-info{--typo3-table-color:var(--typo3-table-info-color);--typo3-table-bg:var(--typo3-table-info-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .success,.table-success{--typo3-table-color:var(--typo3-table-success-color);--typo3-table-bg:var(--typo3-table-success-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .warning,.table-warning{--typo3-table-color:var(--typo3-table-warning-color);--typo3-table-bg:var(--typo3-table-warning-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .danger,.table-danger{--typo3-table-color:var(--typo3-table-danger-color);--typo3-table-bg:var(--typo3-table-danger-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .notice,.table-notice{--typo3-table-color:var(--typo3-table-notice-color);--typo3-table-bg:var(--typo3-table-notice-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .default,.table-default{--typo3-table-color:var(--typo3-table-default-color);--typo3-table-bg:var(--typo3-table-default-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .active,.table-active{--typo3-table-color:var(--typo3-table-primary-color);--typo3-table-bg:var(--typo3-table-primary-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .selected,.table-selected{--typo3-table-color:var(--typo3-table-info-color);--typo3-table-bg:var(--typo3-table-info-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .installed,.table-installed{--typo3-table-color:var(--typo3-table-success-color);--typo3-table-bg:var(--typo3-table-success-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .insecure,.table-insecure{--typo3-table-color:var(--typo3-table-danger-color);--typo3-table-bg:var(--typo3-table-danger-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .outdated,.table-outdated{--typo3-table-color:var(--typo3-table-warning-color);--typo3-table-bg:var(--typo3-table-warning-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .available,.table-available{--typo3-table-color:var(--typo3-table-info-color);--typo3-table-bg:var(--typo3-table-info-bg);--typo3-table-border-color:color-mix(in srgb,var(--typo3-table-bg),var(--typo3-table-color) 10%)} +.table .row-drop-after:not(:last-child)>*,.table tr:has(+.row-drop-before)>*{border-bottom-color:var(--typo3-state-primary-border-color);box-shadow:inset 0 -1px 0 0 var(--typo3-state-primary-border-color)} +.table .row-drop-before:first-child>*{box-shadow:inset 0 2px 0 0 var(--typo3-state-primary-border-color)} +.table .row-drop-after:last-child>*{box-shadow:inset 0 -2px 0 0 var(--typo3-state-primary-border-color)} +.simpletable{margin-bottom:1.5em;padding:0} +.simpletable td,.simpletable th{padding:.25em 1em} +.simpletable td:first-child,.simpletable th:first-child{-webkit-padding-start:0;padding-inline-start:0} +.simpletable td:last-child,.simpletable th:last-child{-webkit-padding-end:0;padding-inline-end:0} +.modal{--typo3-modal-width:var(--typo3-modal-width-default);--typo3-modal-height:var(--typo3-modal-height-default);--typo3-modal-border-radius:var(--typo3-component-border-radius);--typo3-modal-shadow:var(--typo3-component-box-shadow-dialog);--typo3-modal-padding:1rem;--typo3-modal-spacing:.5rem;--typo3-modal-offset:1.5rem;--typo3-modal-offset-multiplier-y:2;--typo3-modal-offset-multiplier-x:2;--typo3-modal-slide:-50px;--typo3-modal-color:var(--typo3-component-color);--typo3-modal-bg:var(--typo3-component-bg);--typo3-modal-border-width:var(--typo3-component-border-width);--typo3-modal-border-color:var(--typo3-component-border-color);--typo3-modal-header-color:var(--typo3-component-active-color);--typo3-modal-header-bg:var(--typo3-component-active-bg);--typo3-modal-backdrop-bg:var(--typo3-overlay-bg);--typo3-modal-backdrop-opacity:var(--typo3-overlay-opacity);background:transparent;border:var(--typo3-modal-border-width) solid var(--typo3-modal-border-color);border-radius:var(--typo3-modal-border-radius);box-shadow:var(--typo3-modal-shadow);display:flex;flex-direction:column;height:var(--typo3-modal-height);inset:0;max-height:calc(100dvh - var(--typo3-modal-offset)*var(--typo3-modal-offset-multiplier-y));max-width:calc(100dvw - var(--typo3-modal-offset)*var(--typo3-modal-offset-multiplier-x));opacity:1;outline:var(--typo3-modal-border-width) solid var(--typo3-modal-border-color);outline-offset:calc(var(--typo3-modal-border-width)*-1);padding:0;transition:opacity .3s ease-out,inset .3s ease-out,overlay allow-discrete .3s ease-out,display allow-discrete .3s ease-out;width:var(--typo3-modal-width)} +.modal-header{align-items:center;background:var(--typo3-modal-header-bg);border-bottom:var(--typo3-modal-border-width) solid var(--typo3-modal-border-color);color:var(--typo3-modal-header-color);display:flex;flex-shrink:0;flex-wrap:wrap;font-weight:700;gap:var(--typo3-modal-spacing);justify-content:space-between;padding:var(--typo3-modal-padding)} +.modal-header-close{background:transparent;border:none;border-radius:var(--typo3-input-border-radius);color:inherit;margin:calc(var(--typo3-modal-header-padding-y)*-.5) calc(var(--typo3-modal-header-padding-x)*-.5) calc(var(--typo3-modal-header-padding-y)*-.5) auto;opacity:.5;padding:calc(var(--typo3-modal-header-padding-y)*.5) calc(var(--typo3-modal-header-padding-x)*.5);-webkit-user-select:none;-moz-user-select:none;user-select:none;z-index:1} +.modal-header-close:active,.modal-header-close:focus,.modal-header-close:hover{background:none;box-shadow:none;opacity:1;outline:none} +.modal-header-close:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,currentColor,transparent 50%)} +.modal-header:has(+.modal-progress){border-bottom:0;padding-bottom:0} +.modal-progress{background-color:var(--typo3-modal-header-bg);border-bottom:var(--typo3-modal-border-width) solid var(--typo3-modal-border-color);color:var(--typo3-modal-header-color);padding:var(--typo3-modal-padding)} +.modal-progress>:first-child{margin-top:0} +.modal-progress>:last-child{margin-bottom:0} +.modal-body{background:var(--typo3-modal-bg);color:var(--typo3-modal-color);display:flex;flex-direction:column;flex-grow:1;min-height:0;overflow-y:auto;padding:var(--typo3-modal-padding);position:relative} +.modal-body>:first-child{margin-top:0} +.modal-body>:last-child{margin-bottom:0} +.modal-loading{flex:1 0 auto;height:100%;justify-content:center} +.modal-footer,.modal-loading{align-items:center;display:flex} +.modal-footer{background:var(--typo3-modal-bg);border-top:1px solid var(--typo3-modal-border-color);color:var(--typo3-modal-color);flex-shrink:0;flex-wrap:wrap;gap:calc(var(--typo3-modal-spacing)/2);justify-content:flex-end;padding:calc(var(--typo3-modal-padding)/2) var(--typo3-modal-padding)} +.modal::backdrop{background-color:var(--typo3-modal-backdrop-bg);opacity:var(--typo3-modal-backdrop-opacity)} +@starting-style{ +.modal{opacity:0}} +.modal.modal-closing{opacity:0;transition:opacity .3s ease-out,inset .3s ease-out} +.modal::backdrop{transition:opacity .2s ease-out,overlay allow-discrete .2s ease-out,display allow-discrete .2s ease-out} +@starting-style{ +.modal::backdrop{opacity:0}} +.modal.modal-closing::backdrop{opacity:0} +@media (prefers-reduced-motion:reduce){ +.modal{transition:none} +@starting-style{ +.modal{inset:0;opacity:1}} +.modal.modal-closing{inset:0;opacity:1} +.modal::backdrop{transition:none} +@starting-style{ +.modal::backdrop{opacity:var(--typo3-modal-backdrop-opacity)}} +.modal.modal-closing::backdrop{opacity:var(--typo3-modal-backdrop-opacity)}} +.modal-style-light{color-scheme:only light} +.modal-style-dark{color-scheme:only dark} +.modal-severity-primary{--typo3-modal-header-color:var(--typo3-surface-container-primary-text);--typo3-modal-header-bg:var(--typo3-surface-container-primary)} +.modal-severity-secondary{--typo3-modal-header-color:var(--typo3-surface-container-secondary-text);--typo3-modal-header-bg:var(--typo3-surface-container-secondary)} +.modal-severity-info{--typo3-modal-header-color:var(--typo3-surface-container-info-text);--typo3-modal-header-bg:var(--typo3-surface-container-info)} +.modal-severity-success{--typo3-modal-header-color:var(--typo3-surface-container-success-text);--typo3-modal-header-bg:var(--typo3-surface-container-success)} +.modal-severity-warning{--typo3-modal-header-color:var(--typo3-surface-container-warning-text);--typo3-modal-header-bg:var(--typo3-surface-container-warning)} +.modal-severity-danger{--typo3-modal-header-color:var(--typo3-surface-container-danger-text);--typo3-modal-header-bg:var(--typo3-surface-container-danger)} +.modal-severity-notice{--typo3-modal-header-color:var(--typo3-surface-container-notice-text);--typo3-modal-header-bg:var(--typo3-surface-container-notice)} +.modal-severity-default{--typo3-modal-header-color:var(--typo3-surface-container-default-text);--typo3-modal-header-bg:var(--typo3-surface-container-default)} +:root{--typo3-modal-width-small:440px;--typo3-modal-width-default:600px;--typo3-modal-width-medium:800px;--typo3-modal-width-large:1000px;--typo3-modal-width-full:100%;--typo3-modal-height-small:440px;--typo3-modal-height-default:fit-content;--typo3-modal-height-medium:520px;--typo3-modal-height-large:800px;--typo3-modal-height-full:100%} +.modal-width-small{--typo3-modal-width:var(--typo3-modal-width-small)} +.modal-width-default{--typo3-modal-width:var(--typo3-modal-width-default)} +.modal-width-medium{--typo3-modal-width:var(--typo3-modal-width-medium)} +.modal-width-large{--typo3-modal-width:var(--typo3-modal-width-large)} +.modal-width-full{--typo3-modal-width:var(--typo3-modal-width-full)} +.modal-height-small{--typo3-modal-height:var(--typo3-modal-height-small)} +.modal-height-default{--typo3-modal-height:var(--typo3-modal-height-default)} +.modal-height-medium{--typo3-modal-height:var(--typo3-modal-height-medium)} +.modal-height-large{--typo3-modal-height:var(--typo3-modal-height-large)} +.modal-height-full{--typo3-modal-height:var(--typo3-modal-height-full)} +.modal-size-small{--typo3-modal-width:var(--typo3-modal-width-small)} +.modal-size-small.modal-type-iframe{--typo3-modal-height:var(--typo3-modal-height-small)} +.modal-size-medium{--typo3-modal-width:var(--typo3-modal-width-medium);--typo3-modal-height:var(--typo3-modal-height-medium)} +.modal-size-large{--typo3-modal-width:var(--typo3-modal-width-large);--typo3-modal-height:var(--typo3-modal-height-large)} +.modal-size-full{--typo3-modal-width:var(--typo3-modal-width-full);--typo3-modal-height:var(--typo3-modal-height-full)} +.modal-size-expand{--typo3-modal-width:var(--typo3-modal-width-medium);--typo3-modal-height:var(--typo3-modal-height-full)} +@media (max-width:767px){ +.modal-size-expand{--typo3-modal-height:calc(100% - 100px)}} +.modal-position-center{--typo3-modal-slide:-100px} +@starting-style{ +.modal-position-center{inset-block-start:var(--typo3-modal-slide)}} +.modal-position-center.modal-closing{inset-block-start:var(--typo3-modal-slide)} +.modal-position-top{--typo3-modal-offset-multiplier-y:1;-webkit-margin-before:0;margin-block-start:0;-webkit-margin-after:auto;border-start-end-radius:0;border-start-start-radius:0;margin-block-end:auto;-webkit-border-before:0;border-block-start:0} +@starting-style{ +.modal-position-top{inset-block-start:var(--typo3-modal-slide)}} +.modal-position-top.modal-closing{inset-block-start:var(--typo3-modal-slide)} +.modal-position-end{--typo3-modal-offset-multiplier-x:1;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:0;border-end-end-radius:0;border-start-end-radius:0;margin-inline-end:0;-webkit-border-end:0;border-inline-end:0} +@starting-style{ +.modal-position-end{inset-inline-end:var(--typo3-modal-slide)}} +.modal-position-end.modal-closing{inset-inline-end:var(--typo3-modal-slide)} +.modal-position-bottom{--typo3-modal-offset-multiplier-y:1;-webkit-margin-before:auto;margin-block-start:auto;-webkit-margin-after:0;border-end-end-radius:0;border-end-start-radius:0;margin-block-end:0;-webkit-border-after:0;border-block-end:0} +@starting-style{ +.modal-position-bottom{inset-block-end:var(--typo3-modal-slide)}} +.modal-position-bottom.modal-closing{inset-block-end:var(--typo3-modal-slide)} +.modal-position-start{--typo3-modal-offset-multiplier-x:1;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:auto;border-end-start-radius:0;border-start-start-radius:0;margin-inline-end:auto;-webkit-border-start:0;border-inline-start:0} +@starting-style{ +.modal-position-start{inset-inline-start:var(--typo3-modal-slide)}} +.modal-position-start.modal-closing{inset-inline-start:var(--typo3-modal-slide)} +.modal-position-sheet{--typo3-modal-offset-multiplier-y:0;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:0;border-end-end-radius:0;border-start-end-radius:0;margin-inline-end:0;-webkit-border-end:0;border-inline-end:0} +@starting-style{ +.modal-position-sheet{inset-inline-end:var(--typo3-modal-slide)}} +.modal-position-sheet.modal-closing{inset-inline-end:var(--typo3-modal-slide)} +@media (max-width:767px){ +.modal-position-sheet{--typo3-modal-offset-multiplier-x:0;--typo3-modal-offset-multiplier-y:2;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;-webkit-margin-before:auto;margin-block-start:auto;-webkit-margin-after:0;border-end-end-radius:0;border-end-start-radius:0;border-start-end-radius:var(--typo3-modal-border-radius);margin-block-end:0;-webkit-border-end:var(--typo3-modal-border-width) solid var(--typo3-modal-border-color);border-inline-end:var(--typo3-modal-border-width) solid var(--typo3-modal-border-color);-webkit-border-after:0;border-block-end:0} +@starting-style{ +.modal-position-sheet{inset-block-end:var(--typo3-modal-slide);inset-inline-end:0}} +.modal-position-sheet.modal-closing{inset-block-end:var(--typo3-modal-slide);inset-inline-end:0}} +.modal-type-iframe,.modal-type-iframe .modal-body{padding:0} +.modal-type-iframe .modal-iframe{border:0;display:block;height:100%;left:0;position:absolute;top:0;width:100%} +.modal-image-manipulation{--typo3-modal-sidebar-width-md:250px;--typo3-modal-sidebar-width-lg:300px} +.modal-image-manipulation .modal-body{padding:0} +@media (min-width:768px){ +.modal-image-manipulation .modal-body{display:flex;flex-direction:row}} +.modal-image-manipulation .modal-panel-main{--typo3-bg-checkerboard-pattern-size:20px;--typo3-bg-checkerboard-background-color:light-dark(var(--token-color-neutral-10),var(--token-color-neutral-85));--typo3-bg-checkerboard-background-image-color:light-dark(var(--token-color-neutral-0),var(--token-color-neutral-90));align-items:center;background:var(--typo3-bg-checkerboard-background-color);background-clip:padding-box;background-image:linear-gradient(45deg,var(--typo3-bg-checkerboard-background-image-color) 25%,transparent 25%),linear-gradient(135deg,var(--typo3-bg-checkerboard-background-image-color) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,var(--typo3-bg-checkerboard-background-image-color) 75%),linear-gradient(135deg,transparent 75%,var(--typo3-bg-checkerboard-background-image-color) 75%);background-position:0 0,calc(var(--typo3-bg-checkerboard-pattern-size)/2) 0,calc(var(--typo3-bg-checkerboard-pattern-size)/2) calc(var(--typo3-bg-checkerboard-pattern-size)/2*-1),0 calc(var(--typo3-bg-checkerboard-pattern-size)/2);background-size:var(--typo3-bg-checkerboard-pattern-size) var(--typo3-bg-checkerboard-pattern-size);display:flex;justify-content:center;overflow:visible;padding:var(--typo3-modal-padding);width:100%} +@media (min-width:768px){ +.modal-image-manipulation .modal-panel-main{width:calc(100% - var(--typo3-modal-sidebar-width-md))}} +@media (min-width:992px){ +.modal-image-manipulation .modal-panel-main{width:calc(100% - var(--typo3-modal-sidebar-width-lg))}} +.modal-image-manipulation .modal-panel-main img{height:auto;max-height:100%;max-width:100%} +.modal-image-manipulation .modal-panel-sidebar{flex-shrink:0;padding:var(--typo3-modal-padding);-webkit-border-start:1px solid rgba(0,0,0,.25);border-inline-start:1px solid rgba(0,0,0,.25);overflow:auto;position:relative;-webkit-overflow-scrolling:touch;width:100%} +@media (min-width:768px){ +.modal-image-manipulation .modal-panel-sidebar{width:var(--typo3-modal-sidebar-width-md)}} +@media (min-width:992px){ +.modal-image-manipulation .modal-panel-sidebar{width:var(--typo3-modal-sidebar-width-lg)}} +.popover{--typo3-popover-zindex:1070;--typo3-popover-max-width:276px;--typo3-popover-bg:var(--typo3-surface-container-low);--typo3-popover-color:var(--typo3-component-color);--typo3-popover-border-width:var(--typo3-component-border-width);--typo3-popover-border-color:color-mix(in srgb,var(--typo3-popover-bg),var(--typo3-popover-color) 5%);--typo3-popover-border-radius:var(--typo3-component-border-radius);--typo3-popover-inner-border-radius:var(--typo3-component-border-radius);--typo3-popover-box-shadow:var(--typo3-component-box-shadow-flyout);--typo3-popover-header-padding-x:.75rem;--typo3-popover-header-padding-y:1rem;--typo3-popover-header-color:var(--typo3-text-color-base);--typo3-popover-header-bg:var(--typo3-surface-container-base);--typo3-popover-body-padding-x:.75rem;--typo3-popover-body-padding-y:1rem;--typo3-popover-body-color:var(--typo3-component-color);--typo3-popover-arrow-width:1rem;--typo3-popover-arrow-height:.5rem;--typo3-popover-arrow-border:color-mix(in srgb,var(--typo3-popover-bg),var(--typo3-popover-color) 5%);display:block;left:0;max-width:var(--typo3-popover-max-width);position:absolute;top:0;z-index:var(--typo3-popover-zindex);word-wrap:break-word;background-clip:padding-box;background-color:var(--typo3-popover-bg);border:var(--typo3-popover-border-width) solid var(--typo3-popover-border-color);box-shadow:var(--typo3-popover-box-shadow)} +.popover:after,.popover:before{border:0 solid transparent;content:"";display:block;position:absolute;transform:translateX(-50%)} +.popover .popover-arrow{display:block;height:var(--typo3-popover-arrow-height);width:var(--typo3-popover-arrow-width)} +.popover .popover-arrow:after,.popover .popover-arrow:before{border:0 solid transparent;content:"";display:block;position:absolute} +.popover .popover-header{background-color:var(--typo3-popover-header-bg);border-bottom:var(--typo3-popover-border-width) solid var(--typo3-popover-border-color);color:var(--typo3-popover-header-color);margin-bottom:0;padding:var(--typo3-popover-header-padding-y) var(--typo3-popover-header-padding-x)} +.popover .popover-header:empty{display:none} +.popover .popover-body{color:var(--typo3-popover-body-color);padding:var(--typo3-popover-body-padding-y) var(--typo3-popover-body-padding-x)} +.popover[data-popper-placement^=top]>.popover-arrow{bottom:calc((var(--typo3-popover-arrow-height))*-1 - var(--typo3-popover-border-width))} +.popover[data-popper-placement^=top]>.popover-arrow:after,.popover[data-popper-placement^=top]>.popover-arrow:before{border-width:var(--typo3-popover-arrow-height) calc(var(--typo3-popover-arrow-width)*.5) 0} +.popover[data-popper-placement^=top]>.popover-arrow:before{border-top-color:var(--typo3-popover-arrow-border);bottom:0} +.popover[data-popper-placement^=top]>.popover-arrow:after{border-top-color:var(--typo3-popover-bg);bottom:var(--typo3-popover-border-width)} +.popover[data-popper-placement^=right]>.popover-arrow{height:var(--typo3-popover-arrow-width);left:calc((var(--typo3-popover-arrow-height))*-1 - var(--typo3-popover-border-width));width:var(--typo3-popover-arrow-height)} +.popover[data-popper-placement^=right]>.popover-arrow:after,.popover[data-popper-placement^=right]>.popover-arrow:before{border-width:calc(var(--typo3-popover-arrow-width)*.5) var(--typo3-popover-arrow-height) calc(var(--typo3-popover-arrow-width)*.5) 0} +.popover[data-popper-placement^=right]>.popover-arrow:before{border-right-color:var(--typo3-popover-arrow-border);left:0} +.popover[data-popper-placement^=right]>.popover-arrow:after{border-right-color:var(--typo3-popover-header-bg);left:var(--typo3-popover-border-width)} +.popover[data-popper-placement^=bottom]>.popover-arrow{top:calc((var(--typo3-popover-arrow-height))*-1 - var(--typo3-popover-border-width))} +.popover[data-popper-placement^=bottom]>.popover-arrow:after,.popover[data-popper-placement^=bottom]>.popover-arrow:before{border-width:0 calc(var(--typo3-popover-arrow-width)*.5) var(--typo3-popover-arrow-height)} +.popover[data-popper-placement^=bottom]>.popover-arrow:before{border-bottom-color:var(--typo3-popover-arrow-border);top:0} +.popover[data-popper-placement^=bottom]>.popover-arrow:after{border-bottom-color:var(--typo3-popover-header-bg);top:var(--typo3-popover-border-width)} +.popover[data-popper-placement^=bottom] .popover-header:before{border-bottom:var(--typo3-popover-border-width) solid var(--typo3-popover-header-bg);content:"";display:block;left:50%;margin-left:calc(var(--typo3-popover-arrow-width)*-.5);position:absolute;top:0;width:var(--typo3-popover-arrow-width)} +.popover[data-popper-placement^=left]>.popover-arrow{height:var(--typo3-popover-arrow-width);right:calc((var(--typo3-popover-arrow-height))*-1 - var(--typo3-popover-border-width));width:var(--typo3-popover-arrow-height)} +.popover[data-popper-placement^=left]>.popover-arrow:after,.popover[data-popper-placement^=left]>.popover-arrow:before{border-width:calc(var(--typo3-popover-arrow-width)*.5) 0 calc(var(--typo3-popover-arrow-width)*.5) var(--typo3-popover-arrow-height)} +.popover[data-popper-placement^=left]>.popover-arrow:before{border-left-color:var(--typo3-popover-arrow-border);right:0} +.popover[data-popper-placement^=left]>.popover-arrow:after{border-left-color:var(--typo3-popover-header-bg);right:var(--typo3-popover-border-width)} +.list-group{--typo3-list-group-border-width:var(--typo3-component-border-width);--typo3-list-group-border-radius:var(--typo3-component-border-radius);--typo3-list-group-margin-y:1rem;--typo3-list-group-padding-x:1rem;--typo3-list-group-padding-y:.5rem;--typo3-list-group-color:var(--typo3-component-color);--typo3-list-group-bg:var(--typo3-component-bg);--typo3-list-group-border-color:var(--typo3-component-border-color);--typo3-list-group-hover-color:var(--typo3-list-item-hover-color);--typo3-list-group-hover-bg:var(--typo3-list-item-hover-bg);--typo3-list-group-hover-border-color:var(--typo3-list-item-hover-border-color);--typo3-list-group-active-color:var(--typo3-list-item-active-color);--typo3-list-group-active-bg:var(--typo3-list-item-active-bg);--typo3-list-group-active-border-color:var(--typo3-list-item-active-border-color);--typo3-list-group-disabled-color:var(--typo3-list-item-disabled-color);--typo3-list-group-disabled-bg:var(--typo3-list-item-disabled-bg);--typo3-list-group-disabled-border-color:var(--typo3-list-item-disabled-border-color);border-radius:var(--typo3-list-group-border-radius);display:flex;flex-direction:column;margin-bottom:0;-webkit-padding-start:0;padding-inline-start:0} +.list-group,.list-group-item-action{background-color:var(--typo3-list-group-bg);color:var(--typo3-list-group-color)} +.list-group-item-action{position:relative;text-align:inherit;width:100%} +.list-group-item-action:focus,.list-group-item-action:hover{--typo3-list-group-color:var(--typo3-list-group-hover-color);--typo3-list-group-bg:var(--typo3-list-group-hover-bg);--typo3-list-group-border-color:var(--typo3-list-group-hover-border-color);text-decoration:none;z-index:1} +.list-group-item-action:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-list-group-border-color),transparent 25%);z-index:3!important} +.list-group-item{background-color:var(--typo3-list-group-bg);border:var(--typo3-list-group-border-width) solid var(--typo3-list-group-border-color);color:var(--typo3-list-group-color);display:block;outline-offset:0;padding:var(--typo3-list-group-padding-y) var(--typo3-list-group-padding-x);position:relative;text-decoration:none} +.list-group-item:first-child{border-start-end-radius:var(--typo3-list-group-border-radius);border-start-start-radius:var(--typo3-list-group-border-radius)} +.list-group-item:last-child{border-end-end-radius:var(--typo3-list-group-border-radius);border-end-start-radius:var(--typo3-list-group-border-radius)} +.list-group-item.active{--typo3-list-group-color:var(--typo3-list-group-active-color);--typo3-list-group-bg:var(--typo3-list-group-active-bg);--typo3-list-group-border-color:var(--typo3-list-group-active-border-color);z-index:2} +.list-group-item.disabled,.list-group-item[disabled]{--typo3-list-group-color:var(--typo3-list-group-disabled-color);--typo3-list-group-bg:var(--typo3-list-group-disabled-bg);--typo3-list-group-border-color:var(--typo3-list-group-disabled-border-color);pointer-events:none} +.list-group-item+.list-group-item{margin-top:calc(var(--typo3-list-group-border-width)*-1)} +.list-group-button{align-items:center;background:transparent;border:none;display:flex;flex-grow:1;gap:.25rem;padding:var(--typo3-list-group-padding-y) var(--typo3-list-group-padding-x);position:relative;text-align:start;width:100%} +.list-group-button[aria-expanded=false] .caret{--typo3-caret-rotation:calc(-90deg*var(--typo3-position-modifier))} +.card-header+.list-group{margin-top:var(--typo3-list-group-margin-y)} +.list-group-flush{--typo3-list-group-border-radius:0} +.list-group-flush .list-group-item{border-left:0;border-right:0} +.list-group-flush .list-group-item:first-child{border-top:0} +.list-group-flush .list-group-item:last-child{border-bottom:0} +.upload-file-picker{bottom:0;height:1px;inset-inline-end:0;position:fixed;visibility:hidden;width:1px} +.activity{display:grid;gap:0 .5rem;grid-template-areas:"icon title time";grid-template-columns:[icon] -webkit-max-content [content-start] minmax(0,1fr) [content-end time] -webkit-max-content;grid-template-columns:[icon] max-content [content-start] minmax(0,1fr) [content-end time] max-content;max-width:100%;min-width:0;width:100%} +.activity>*{grid-column:content-start/content-end} +.activity-icon{grid-area:icon;grid-column:unset!important} +.activity-title{grid-area:title;grid-column:unset!important} +.activity-time{grid-area:time;grid-column:unset!important;opacity:.75} +.activity-source,.activity-time{font-size:var(--typo3-font-size-small)} + +/*! + * Cropper v$VERSION + * https://github.com/fengyuanchen/cropper + * + * Copyright (c) 2014-$YEAR Fengyuan Chen and contributors + * Released under the MIT license + * + * Date: $DATE + */ +.cropper-container{overflow:hidden;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none} +.cropper-container img{display:block;height:100%;image-orientation:0deg!important;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%} +.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal{bottom:0;left:0;position:absolute;right:0;top:0} +.cropper-drag-box{background-color:#fff;opacity:0} +.cropper-modal{background-color:#000;opacity:.5} +.cropper-view-box{display:block;height:100%;outline:1px solid #69f;outline-color:rgba(102,153,255,.75);overflow:hidden;width:100%} +.cropper-dashed{border:0 dashed #fff;display:block;position:absolute} +.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.3333333333%;left:0;top:33.3333333333%;width:100%} +.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.3333333333%;top:0;width:33.3333333333%} +.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;position:absolute;width:100%} +.cropper-face{background-color:hsla(0,0%,100%,.1);cursor:move;left:0;top:0} +.cropper-line{background-color:#69f} +.cropper-line.line-e{cursor:e-resize;right:-3px;top:0;width:5px} +.cropper-line.line-n{cursor:n-resize;height:5px;left:0;top:-3px} +.cropper-line.line-w{cursor:w-resize;left:-3px;top:0;width:5px} +.cropper-line.line-s{bottom:-3px;cursor:s-resize;height:5px;left:0} +.cropper-point{background-color:#69f;height:5px;opacity:.75;width:5px} +.cropper-point.point-e{cursor:e-resize;margin-top:-3px;right:-3px;top:50%} +.cropper-point.point-n{cursor:n-resize;left:50%;margin-left:-3px;top:-3px} +.cropper-point.point-w{cursor:w-resize;left:-3px;margin-top:-3px;top:50%} +.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px} +.cropper-point.point-ne{cursor:ne-resize;right:-3px;top:-3px} +.cropper-point.point-nw{cursor:nw-resize;left:-3px;top:-3px} +.cropper-point.point-sw{bottom:-3px;cursor:sw-resize;left:-3px} +.cropper-point.point-se{bottom:-3px;cursor:se-resize;height:20px;opacity:1;right:-3px;width:20px} +.cropper-point.point-se:before{background-color:#69f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%} +@media (min-width:768px){ +.cropper-point.point-se{height:15px;width:15px}} +@media (min-width:992px){ +.cropper-point.point-se{height:10px;width:10px}} +@media (min-width:1200px){ +.cropper-point.point-se{height:5px;opacity:.75;width:5px}} +.cropper-invisible{opacity:0} +.cropper-hide{height:auto!important;left:0;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;opacity:0;position:fixed;top:0;width:auto!important;z-index:-1} +.cropper-hidden{display:none!important} +.cropper-move{cursor:move} +.cropper-crop{cursor:crosshair} +.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed} +select-pure{--typo3-form-selectpure-border-radius:var(--typo3-input-border-radius);--typo3-form-selectpure-border-width:var(--typo3-input-border-width);--typo3-form-selectpure-border-color:var(--typo3-input-border-color);--typo3-form-selectpure-padding-x:var(--typo3-input-padding-x);--typo3-form-selectpure-padding-y:var(--typo3-input-padding-y);--typo3-form-selectpure-font-size:var(--typo3-input-font-size);--typo3-form-selectpure-line-height:var(--typo3-input-line-height);--typo3-form-selectpure-color:var(--typo3-input-color);--typo3-form-selectpure-bg:var(--typo3-input-bg);--typo3-form-selectpure-hover-color:var(--typo3-input-hover-color);--typo3-form-selectpure-hover-bg:var(--typo3-input-hover-bg);--typo3-form-selectpure-focus-border-color:var(--typo3-input-focus-border-color);--typo3-form-selectpure-disabled-color:var(--typo3-input-disabled-color);--typo3-form-selectpure-disabled-bg:var(--typo3-input-disabled-bg);--font-size:var(--typo3-form-selectpure-font-size);--font-family:inherit;--font-weight:400;--border-radius:var(--typo3-form-selectpure-border-radius);--border-width:var(--typo3-form-selectpure-border-width);--border-color:var(--typo3-form-selectpure-border-color);--padding:calc(var(--typo3-form-selectpure-padding-y) - 4px) var(--typo3-form-selectpure-padding-x);--select-height:calc(var(--typo3-form-selectpure-padding-y)*2 + var(--typo3-form-selectpure-font-size)*var(--typo3-form-selectpure-line-height) + var(--typo3-form-selectpure-border-width)*2);--select-width:100%;--color:var(--typo3-form-selectpure-color);--background-color:var(--typo3-form-selectpure-bg);--hover-color:var(--typo3-form-selectpure-hover-color);--hover-background-color:var(--typo3-form-selectpure-hover-bg);--disabled-color:var(--typo3-form-selectpure-disabled-color);--disabled-background-color:var(--typo3-form-selectpure-disabled-bg);--select-outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-form-selectpure-focus-border-color),transparent 25%);--selected-background-color:var(--typo3-component-active-bg);--selected-color:var(--typo3-component-active-color);--dropdown-gap:calc(var(--typo3-spacing)*0.5);--dropdown-items:5;--dropdown-z-index:2} +.input-group>select-pure{flex:1 1 auto;position:relative} +.sortable-ghost{opacity:.4!important} +:root{--alwan-pattern:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 20 20'%3E%3Cpath fill='%23D0D0D0' d='M0 0h10v10H0Z'/%3E%3Cpath fill='%23fff' d='M0 10h10v10H0Z'/%3E%3Cpath fill='%23D0D0D0' d='M10 10h10v10H10Z'/%3E%3Cpath fill='%23fff' d='M10 0h10v10H10Z'/%3E%3C/svg%3E")} +.alwan{--bg:#fff;--fg:#333;--outer-bc:#ccc;--bc:#ccc;--btn-bg-hover:#f0f0f0;--thumb-bg:#fff;--thumb-bg-hover:#f0f0f0;--label-color:#555;--input-bg:#fafafa;--input-bg-hover:#f0f0f0;--input-bc-hover:#a8a8a8;--swatches-bg:#f5f5f5;background:var(--bg);border:1px solid var(--outer-bc);max-width:260px;overflow:hidden;width:260px} +.alwan *{box-sizing:border-box} +.alwan>div{width:260px} +.alwan__popover-container{height:0;transform:translate(0);width:0} +.alwan__popover-container>.alwan{border:0;box-shadow:0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12);height:auto;left:0;overflow:auto;position:fixed;top:0;z-index:999999} +.alwan:not(.alwan--open){height:0;overflow:hidden;visibility:hidden} +.alwan.alwan--dark{--bg:#111;--fg:#ccc;--outer-bc:#aaa;--bc:#34373a;--btn-bg-hover:#444;--thumb-bg:#151515;--thumb-bg-hover:#242424;--label-color:#d8d8d8;--input-bg:#181818;--input-bg-hover:#272727;--input-bc-hover:#484b4d;--swatches-bg:#151515} +.alwan__container{align-items:center;border-top:1px solid var(--bc);display:flex;padding:10px 15px;position:relative} +.alwan__container>*{width:100%} +.alwan__selector{background:linear-gradient(to bottom,transparent,#000),linear-gradient(to right,#fff,hsl(var(--h,0),100%,50%));height:136px;outline:0;overflow:hidden;touch-action:none;-webkit-user-select:none;-moz-user-select:none;user-select:none} +.alwan__selector:focus .alwan__cursor{box-shadow:0 0 1px 2px #117ce0} +.alwan__selector[tabindex]{cursor:crosshair} +.alwan__selector[tabindex]:active,.alwan__selector[tabindex]:active>.alwan__cursor{cursor:grabbing} +.alwan__selector[tabindex] .alwan__cursor{cursor:grab;cursor:-webkit-grab} +.alwan__cursor{background:transparent;border:2px solid #fff;border-radius:100%;box-shadow:0 0 1px 1px rgba(0,0,0,.3);height:16px;left:-8px;position:relative;top:-8px;width:16px} +.alwan__preview{--color:rgb(var(--rgb),var(--a));border:1px solid var(--bc);border-radius:5px;flex-shrink:0;height:42px;margin-right:15px;width:42px} +.alwan__preview .alwan__cp{align-items:center;border-radius:0;color:#fff;display:flex;height:100%;justify-content:center;margin:0;opacity:0;position:relative;width:100%} +.alwan__preview .alwan__cp:focus,.alwan__preview .alwan__cp:hover:not(:disabled){background-color:rgba(0,0,0,.6);opacity:1;z-index:100} +.alwan__preview .alwan__cp:focus-visible{border:0} +.alwan__slider{--hue-track:linear-gradient(90deg,red,#f0f,#00f,#0ff,#0f0,#ff0,red);--alpha-track:linear-gradient(90deg,transparent,rgb(var(--rgb))),var(--alwan-pattern);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;cursor:pointer;display:block;height:15px;margin:0;outline:0;padding:0;touch-action:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%} +.alwan__slider:active{cursor:grabbing;cursor:-webkit-grabbing} +.alwan__slider::-webkit-slider-runnable-track{-webkit-appearance:none;appearance:none;border:0;border-radius:8px;height:15px;width:100%} +.alwan__slider::-moz-range-track{border:0;border-radius:8px;height:15px;width:100%} +.alwan__slider:focus-visible::-webkit-slider-runnable-track{outline:2px solid rgba(17,124,224,.5);outline-offset:2px} +.alwan__slider:focus-visible::-moz-range-track{outline:2px solid rgba(17,124,224,.5);outline-offset:2px} +.alwan__slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;background-color:var(--thumb-bg);border:1px solid #999;border-radius:100%;box-shadow:0 0 1px 1px rgba(0,0,0,.3);height:20px;margin-top:-2.5px;width:20px} +.alwan__slider::-moz-range-thumb{background-color:var(--thumb-bg);border:1px solid #999;border-radius:100%;box-shadow:0 0 1px 1px rgba(0,0,0,.3);box-sizing:border-box;height:20px;padding:0;width:20px} +.alwan__slider:not(:disabled)::-webkit-slider-thumb:hover{background:var(--thumb-bg-hover);border-color:#555;cursor:grab;cursor:-webkit-grab} +.alwan__slider:not(:disabled)::-moz-range-thumb:hover{background:var(--thumb-bg-hover);border-color:#555;cursor:grab;cursor:-webkit-grab} +.alwan__slider:active:not(:disabled)::-webkit-slider-thumb{cursor:grabbing;cursor:-webkit-grabbing;outline:5px solid rgba(17,124,224,.3)} +.alwan__slider:active:not(:disabled)::-moz-range-thumb{cursor:grabbing;cursor:-webkit-grabbing;outline:5px solid rgba(17,124,224,.3)} +.alwan__slider:focus::-webkit-slider-thumb{border-color:#117ce0} +.alwan__slider:focus::-moz-range-thumb{border-color:#117ce0} +.alwan__slider:disabled{cursor:default;opacity:.5} +.alwan__hue{direction:rtl} +.alwan__hue::-webkit-slider-runnable-track{background:var(--hue-track)} +.alwan__hue::-moz-range-track{background:var(--hue-track)} +.alwan__alpha{margin-top:14px} +.alwan__alpha::-webkit-slider-runnable-track{background:var(--alpha-track)} +.alwan__alpha::-moz-range-track{background:var(--alpha-track)} +.alwan__inputs{display:flex} +.alwan__inputs>label{color:var(--label-color);cursor:pointer;font-family:system-ui;font-size:13px;margin-right:4px;text-transform:uppercase;-webkit-user-select:none;-moz-user-select:none;user-select:none} +.alwan__input,.alwan__inputs>label{line-height:1;text-align:center;width:100%} +.alwan__input{background-color:var(--input-bg);border:1px solid var(--bc);border-radius:2px;color:var(--fg);display:block;font-family:monospace;font-size:14px;margin-bottom:3px;padding:8px 0;transition:border-color .2s,background-color .2s} +.alwan__input::-moz-selection{background-color:#117ce0;color:#fff} +.alwan__input::selection{background-color:#117ce0;color:#fff} +.alwan__input:focus{background-color:var(--bg);border-color:#117ce0;outline:0} +.alwan__input:focus+span{color:#117ce0} +.alwan__input:hover:not(:focus,:disabled){background-color:var(--input-bg-hover);border-color:var(--input-bc-hover)} +.alwan__input:disabled,.alwan__input:disabled+span{opacity:.5} +.alwan__swatches{background-color:var(--swatches-bg);border-top:1px solid var(--bc);display:flex;flex-wrap:wrap;justify-content:center;max-height:100px;overflow-y:auto;padding:10px 15px 0} +.alwan__button{background:transparent;border:1px solid transparent;color:var(--fg);cursor:pointer;display:inline-block;outline:0;padding:8px 4px;transition:background-color .2s ease-in-out;width:auto} +.alwan__button:disabled{cursor:not-allowed;opacity:.5} +.alwan__button:hover:not(:disabled){background-color:var(--btn-bg-hover)} +.alwan__button:focus-visible:not(.alwan__ref){border-color:#117ce0} +.alwan__button svg{fill:currentColor;pointer-events:none;vertical-align:middle} +.alwan__swatch{border-radius:3px;height:22px;margin:0 5px 10px;width:22px} +.alwan__swatch:hover:not(:disabled){transform:scale(1.1)} +.alwan__swatch:focus{outline:1px solid var(--color);outline-offset:1px} +.alwan__ref{border:3px solid #f5f5f5;border-radius:3px;flex-shrink:0;height:26px;outline:1px solid #333;width:26px} +.alwan__ref:focus,.alwan__ref:focus-visible{box-shadow:0 0 2px 2px #117ce0;outline-color:#117ce0} +.alwan__cp{margin-right:15px} +.alwan__preview,.alwan__ref,.alwan__swatch{background:var(--alwan-pattern);background-clip:padding-box;overflow:hidden;padding:0;position:relative} +.alwan__preview:before,.alwan__ref:before,.alwan__swatch:before{background:var(--color);content:"";height:100%;left:0;position:absolute;top:0;width:100%} +.alwan__toggle-button{align-items:center;border-top:1px solid var(--bc);display:flex;height:10px;justify-content:center;overflow:hidden;padding:0;width:100%} +.alwan--collapse.alwan__swatches{display:none} +.alwan--collapse+.alwan__toggle-button svg{transform:rotate(180deg)} +.alwan{--bg:var(--typo3-component-bg);--fg:var(--typo3-component-color);--outer-bc:var(--typo3-component-border-color);--bc:var(--typo3-component-border-color);--btn-bg-hover:var(--typo3-component-hover-bg);--thumb-bg:var(--typo3-state-default-bg);--thumb-bg-hover:var(--typo3-state-default-hover-bg);--label-color:var(--typo3-component-color);--input-bg:var(--typo3-input-bg);--input-bg-hover:var(--typo3-input-hover-bg);--input-bc-hover:var(--typo3-input-hover-border-color);--swatches-bg:var(--typo3-component-bg)} +:root{--flatpickr-bar-size:40px;--flatpickr-grid-size:32px;--flatpickr-grid-spacing:2px;--flatpickr-width:calc(var(--flatpickr-grid-size)*7 + var(--flatpickr-grid-spacing)*10 + 2px)} +.flatpickr-calendar{animation:none;background:var(--typo3-component-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:var(--typo3-component-border-radius);box-shadow:var(--typo3-component-box-shadow-tooltip);color:var(--typo3-component-color);direction:ltr;display:none;font-size:var(--typo3-component-font-size);opacity:0;padding:0;position:absolute;text-align:center;touch-action:manipulation;visibility:hidden;width:var(--flatpickr-width)} +.flatpickr-calendar:before{border-width:5px;margin:0 -5px} +.flatpickr-calendar:after{border-width:4px;margin:0 -4px} +.flatpickr-calendar:after,.flatpickr-calendar:before{border:6px solid transparent;content:"";display:block;height:0;left:22px;pointer-events:none;position:absolute;width:0} +.flatpickr-calendar.inline,.flatpickr-calendar.open{max-height:640px;opacity:1;visibility:visible} +.flatpickr-calendar.open{display:inline-block;z-index:200} +.flatpickr-calendar.open.animate{animation:fpFadeInDown .3s cubic-bezier(.23,1,.32,1)} +.flatpickr-calendar.inline{display:block;position:relative;top:2px} +.flatpickr-calendar.static{position:absolute;top:calc(100% + 2px)} +.flatpickr-calendar.static.open{display:block;z-index:199} +.flatpickr-calendar.rightMost:after,.flatpickr-calendar.rightMost:before{left:auto;right:22px} +.flatpickr-calendar.arrowTop:after,.flatpickr-calendar.arrowTop:before{bottom:100%} +.flatpickr-calendar.arrowTop:after,.flatpickr-calendar.arrowTop:before{border-bottom-color:var(--typo3-component-bg)} +.flatpickr-calendar.arrowBottom:after,.flatpickr-calendar.arrowBottom:before{top:100%} +.flatpickr-calendar.arrowBottom:after,.flatpickr-calendar.arrowBottom:before{border-top-color:var(--typo3-component-bg)} +.flatpickr-calendar.hasTime .flatpickr-innerContainer{border-bottom:var(--typo3-component-border-width) solid var(--typo3-component-border-color);margin-bottom:1px} +.flatpickr-calendar .flatpickr-am-pm:hover,.flatpickr-calendar .flatpickr-next-month:hover,.flatpickr-calendar .flatpickr-prev-month:hover,.flatpickr-calendar input:hover,.flatpickr-calendar select:hover{background:var(--typo3-component-hover-bg);color:var(--typo3-component-hover-color);outline:1px solid var(--typo3-component-hover-border-color);outline-offset:-1px} +.flatpickr-calendar .flatpickr-am-pm:focus,.flatpickr-calendar .flatpickr-next-month:focus,.flatpickr-calendar .flatpickr-prev-month:focus,.flatpickr-calendar input:focus,.flatpickr-calendar select:focus{background:var(--typo3-component-focus-bg);color:var(--typo3-component-focus-color);outline:1px solid var(--typo3-component-focus-border-color);outline-offset:-1px} +.flatpickr-calendar .flatpickr-am-pm[disabled],.flatpickr-calendar .flatpickr-next-month[disabled],.flatpickr-calendar .flatpickr-prev-month[disabled],.flatpickr-calendar input[disabled],.flatpickr-calendar select[disabled]{background:var(--typo3-component-disabled-bg);color:var(--typo3-component-disabled-color);outline:1px solid var(--typo3-component-disabled-border-color);outline-offset:-1px;pointer-events:none} +.flatpickr-calendar .flatpickr-am-pm::-ms-clear,.flatpickr-calendar .flatpickr-next-month::-ms-clear,.flatpickr-calendar .flatpickr-prev-month::-ms-clear,.flatpickr-calendar input::-ms-clear,.flatpickr-calendar select::-ms-clear{display:none} +.flatpickr-calendar .flatpickr-am-pm::-webkit-inner-spin-button,.flatpickr-calendar .flatpickr-am-pm::-webkit-outer-spin-button,.flatpickr-calendar .flatpickr-next-month::-webkit-inner-spin-button,.flatpickr-calendar .flatpickr-next-month::-webkit-outer-spin-button,.flatpickr-calendar .flatpickr-prev-month::-webkit-inner-spin-button,.flatpickr-calendar .flatpickr-prev-month::-webkit-outer-spin-button,.flatpickr-calendar input::-webkit-inner-spin-button,.flatpickr-calendar input::-webkit-outer-spin-button,.flatpickr-calendar select::-webkit-inner-spin-button,.flatpickr-calendar select::-webkit-outer-spin-button{-webkit-appearance:none;appearance:none;margin:0} +.flatpickr-wrapper{display:inline-block;position:relative} +.flatpickr-months{border-bottom:1px solid var(--typo3-component-border-color);display:flex;padding:1px} +.flatpickr-month{color:var(--typo3-component-color);position:relative;fill:var(--typo3-component-color);flex-grow:1;line-height:1;overflow:hidden} +.flatpickr-month,.flatpickr-next-month,.flatpickr-prev-month{align-items:center;height:var(--flatpickr-bar-size);justify-content:center;-webkit-user-select:none;-moz-user-select:none;user-select:none} +.flatpickr-next-month,.flatpickr-prev-month{color:inherit;cursor:pointer;display:flex;text-decoration:none;width:var(--flatpickr-bar-size);z-index:3;fill:currentColor} +.flatpickr-next-month svg,.flatpickr-prev-month svg{display:block;height:14px;width:14px} +.flatpickr-next-month svg path,.flatpickr-prev-month svg path{transition:fill .1s} +.flatpickr-prev-month{border-start-start-radius:calc(var(--typo3-input-border-radius) - 1px)} +.flatpickr-next-month{border-start-end-radius:calc(var(--typo3-input-border-radius) - 1px)} +.numInputWrapper{height:auto;position:relative} +.numInputWrapper .numInput{-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield} +.numInputWrapper input,.numInputWrapper span{display:inline-block} +.numInputWrapper input{padding:0 .5em;width:100%} +.numInputWrapper span{color:inherit;cursor:pointer;height:50%;line-height:50%;opacity:0;padding:0 4px 0 2px;position:absolute;right:2px;width:12px} +.numInputWrapper span svg path{color:currentColor} +.numInputWrapper span:active,.numInputWrapper span:hover{background:var(--typo3-component-hover-bg);color:var(--typo3-component-hover-color)} +.numInputWrapper span:focus{background:var(--typo3-component-focus-bg);color:var(--typo3-component-focus-color)} +.numInputWrapper span:after{content:"";display:block;position:absolute;top:calc(50% - 2px)} +.numInputWrapper span.arrowUp{top:2px} +.numInputWrapper span.arrowUp:after{border-bottom:4px solid;border-left:4px solid transparent;border-right:4px solid transparent} +.numInputWrapper span.arrowDown{bottom:2px} +.numInputWrapper span.arrowDown:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid} +.numInputWrapper:hover span{opacity:1} +.flatpickr-current-month{color:inherit;display:flex;font-weight:300;gap:1em;height:var(--flatpickr-bar-size);justify-content:center;line-height:1;padding:0} +.flatpickr-current-month .numInputWrapper{width:5em} +.flatpickr-current-month .cur-year{border:1px solid transparent;border-radius:0;display:block} +.flatpickr-current-month .cur-year,.flatpickr-current-month .flatpickr-monthDropdown-months{background:transparent;color:var(--typo3-component-color);font-family:inherit;font-size:inherit;font-weight:300;height:100%;line-height:inherit} +.flatpickr-current-month .flatpickr-monthDropdown-months{-webkit-appearance:menulist;-moz-appearance:menulist;appearance:menulist;border:none;border-radius:0;cursor:pointer;padding:0 .5em;position:relative;vertical-align:initial;width:auto} +.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month{background-color:var(--typo3-component-bg);padding:0} +.flatpickr-weekdays{height:var(--flatpickr-grid-size)} +.flatpickr-weekdaycontainer{display:flex;flex-wrap:wrap;gap:var(--flatpickr-grid-spacing);padding:0 calc(var(--flatpickr-grid-spacing)*2);width:var(--flatpickr-width)} +.flatpickr-weekday{background:transparent;color:var(--typo3-component-color);cursor:default;display:block;font-size:90%;font-weight:bolder;height:var(--flatpickr-grid-size);line-height:var(--flatpickr-grid-size);text-align:center;width:var(--flatpickr-grid-size)} +.flatpickr-weekwrapper{box-shadow:1px 0 0 var(--typo3-component-border-color)} +.flatpickr-weeks{display:flex;flex-wrap:wrap;gap:var(--flatpickr-grid-spacing);padding:calc(var(--flatpickr-grid-spacing)*2) 0} +.flatpickr-weeks .flatpickr-day{--flatpickr-day-bg:transparent!important;--flatpickr-day-color:var(--typo3-component-disabled-color)!important;--flatpickr-day-border-color:transparent!important;cursor:default!important} +.flatpickr-days{align-items:flex-start;overflow:hidden;position:relative} +.dayContainer,.flatpickr-days{display:flex;width:var(--flatpickr-width)} +.dayContainer{flex-wrap:wrap;gap:var(--flatpickr-grid-spacing);opacity:1;padding:calc(var(--flatpickr-grid-spacing)*2);transform:translateZ(0)} +.dayContainer+.dayContainer{box-shadow:-1px 0 0 var(--typo3-component-border-color)} +.flatpickr-day{background:var(--flatpickr-day-bg,transparent);border:1px solid var(--flatpickr-day-border-color,transparent);border-radius:var(--typo3-component-border-radius);color:var(--flatpickr-day-color,inherit);display:block;font-weight:400;height:var(--flatpickr-grid-size);line-height:var(--flatpickr-grid-size);position:relative;text-align:center;width:var(--flatpickr-grid-size)} +.flatpickr-day.inRange,.flatpickr-day.nextMonthDay.inRange,.flatpickr-day.nextMonthDay.today.inRange,.flatpickr-day.nextMonthDay:hover,.flatpickr-day.prevMonthDay.inRange,.flatpickr-day.prevMonthDay.today.inRange,.flatpickr-day.prevMonthDay:hover,.flatpickr-day.today.inRange,.flatpickr-day:hover{cursor:pointer;--flatpickr-day-color:var(--typo3-component-hover-color);--flatpickr-day-bg:var(--typo3-component-hover-bg);--flatpickr-day-border-color:var(--typo3-component-hover-border-color)} +.flatpickr-day.nextMonthDay:focus,.flatpickr-day.prevMonthDay:focus,.flatpickr-day:focus{cursor:pointer;--flatpickr-day-color:var(--typo3-component-focus-color);--flatpickr-day-bg:var(--typo3-component-focus-bg);--flatpickr-day-border-color:var(--typo3-component-focus-border-color)} +.flatpickr-day.today{--flatpickr-day-bg:var(--typo3-component-disabled-bg);--flatpickr-day-border-color:var(--typo3-component-active-border-color)} +.flatpickr-day.today:hover{--flatpickr-day-color:var(--typo3-component-hover-color);--flatpickr-day-bg:var(--typo3-component-hover-bg)} +.flatpickr-day.today:focus{--flatpickr-day-color:var(--typo3-component-focus-color);--flatpickr-day-bg:var(--typo3-component-focus-bg)} +.flatpickr-day.endRange,.flatpickr-day.endRange.inRange,.flatpickr-day.endRange.nextMonthDay,.flatpickr-day.endRange.prevMonthDay,.flatpickr-day.endRange:focus,.flatpickr-day.endRange:hover,.flatpickr-day.selected,.flatpickr-day.selected.inRange,.flatpickr-day.selected.nextMonthDay,.flatpickr-day.selected.prevMonthDay,.flatpickr-day.selected:focus,.flatpickr-day.selected:hover,.flatpickr-day.startRange,.flatpickr-day.startRange.inRange,.flatpickr-day.startRange.nextMonthDay,.flatpickr-day.startRange.prevMonthDay,.flatpickr-day.startRange:focus,.flatpickr-day.startRange:hover{--flatpickr-day-color:var(--typo3-component-active-color);--flatpickr-day-bg:var(--typo3-component-active-bg);--flatpickr-day-border-color:var(--typo3-component-active-border-color)} +.flatpickr-day.endRange.startRange,.flatpickr-day.selected.startRange,.flatpickr-day.startRange.startRange{border-radius:var(--typo3-component-border-radius) 0 0 var(--typo3-component-border-radius)} +.flatpickr-day.endRange.endRange,.flatpickr-day.selected.endRange,.flatpickr-day.startRange.endRange{border-radius:0 var(--typo3-component-border-radius) var(--typo3-component-border-radius) 0} +.flatpickr-day.endRange.startRange+.endRange:not(:nth-child(7n+1)),.flatpickr-day.selected.startRange+.endRange:not(:nth-child(7n+1)),.flatpickr-day.startRange.startRange+.endRange:not(:nth-child(7n+1)){box-shadow:-10px 0 0 var(--typo3-component-active-bg)} +.flatpickr-day.endRange.startRange.endRange,.flatpickr-day.selected.startRange.endRange,.flatpickr-day.startRange.startRange.endRange{border-radius:var(--typo3-component-border-radius)} +.flatpickr-day.inRange{border-radius:0} +.flatpickr-day.nextMonthDay,.flatpickr-day.notAllowed,.flatpickr-day.notAllowed.nextMonthDay,.flatpickr-day.notAllowed.prevMonthDay,.flatpickr-day.prevMonthDay{cursor:default} +.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover{--flatpickr-day-color:var(--typo3-component-disabled-color);--flatpickr-day-bg:var(--typo3-component-disabled-bg);--flatpickr-day-border-color:var(--typo3-component-disabled-border-color);cursor:not-allowed} +.flatpickr-day.week.selected{border-radius:0;box-shadow:-5px 0 0 var(--typo3-component-active-bg),5px 0 0 var(--typo3-component-active-bg)} +.flatpickr-day.hidden{visibility:hidden} +.rangeMode .flatpickr-day{margin-top:1px} +.flatpickr-innerContainer{display:flex} +.flatpickr-time{display:flex;gap:1px;padding:1px;text-align:center} +.flatpickr-time .numInputWrapper{flex:1;height:var(--flatpickr-bar-size);width:40%} +.flatpickr-time.hasSeconds .numInputWrapper{width:26%} +.flatpickr-time.time24hr .numInputWrapper{width:49%} +.flatpickr-time input{background:transparent;border:0;border-radius:0;box-shadow:none;color:var(--typo3-component-color);font-size:var(--typo3-component-font-size);height:inherit;line-height:inherit;margin:0;padding:0;position:relative;text-align:center} +.flatpickr-time .flatpickr-am-pm,.flatpickr-time .flatpickr-time-separator{align-self:center;color:var(--typo3-component-color);font-weight:700;height:var(--flatpickr-bar-size);line-height:var(--flatpickr-bar-size);-webkit-user-select:none;-moz-user-select:none;user-select:none;width:2%} +.flatpickr-time .flatpickr-am-pm{cursor:pointer;font-weight:400;text-align:center;width:18%} +.flatpickr-input[readonly]{cursor:pointer} +@keyframes fpFadeInDown{ +0%{opacity:0;transform:translate3d(0,-20px,0)} +to{opacity:1;transform:translateZ(0)}} +.shortcut-buttons-flatpickr-wrapper{background-color:var(--typo3-component-bg);border-end-end-radius:calc(var(--typo3-input-border-radius) - 1px);border-end-start-radius:calc(var(--typo3-input-border-radius) - 1px);border-top:var(--typo3-component-border-width) solid var(--typo3-component-border-color);color:var(--typo3-component-color);display:flex;justify-content:center;padding:calc(var(--typo3-component-padding-y)/2) var(--typo3-component-padding-x)} +.shortcut-buttons-flatpickr-label{align-content:center;display:flex;flex-direction:column;padding:var(--typo3-component-padding-y) var(--typo3-component-padding-x)} +.shortcut-buttons-flatpickr-buttons{display:flex;flex-flow:row wrap} +.shortcut-buttons-flatpickr-button{background-color:transparent;border:none;color:inherit;padding:.5em .75em} +.shortcut-buttons-flatpickr-button:hover{background:var(--typo3-component-hover-bg);color:var(--typo3-component-hover-color);outline:1px solid var(--typo3-component-hover-border-color);outline-offset:-1px} +.shortcut-buttons-flatpickr-button:focus{background:var(--typo3-component-focus-bg);color:var(--typo3-component-focus-color);outline:1px solid var(--typo3-component-focus-border-color);outline-offset:-1px} +:root{--tree-color:var(--typo3-text-color-base);--tree-bg:var(--typo3-surface-container-low);--tree-node-color:var(--tree-color);--tree-node-bg:var(--tree-bg);--tree-node-border-color:transparent;--tree-node-focus-color:var(--typo3-component-focus-color);--tree-node-focus-bg:var(--typo3-component-focus-bg);--tree-node-focus-border-color:var(--typo3-component-focus-border-color);--tree-node-hover-color:var(--typo3-component-hover-color);--tree-node-hover-bg:var(--typo3-component-hover-bg);--tree-node-hover-border-color:var(--typo3-component-hover-border-color);--tree-node-selected-color:var(--typo3-component-active-color);--tree-node-selected-bg:var(--typo3-component-active-bg);--tree-node-selected-border-color:var(--typo3-component-active-border-color);--tree-node-information:var(--tree-node-color);--tree-node-information-success:var(--typo3-status-indicator-success-color);--tree-node-information-warning:var(--typo3-status-indicator-warning-color);--tree-node-information-danger:var(--typo3-status-indicator-danger-color);--tree-node-information-info:var(--typo3-status-indicator-info-color);--tree-info-bg:var(--typo3-surface-info);--tree-info-color:var(--typo3-surface-info-text);--tree-drop-position-bg:var(--typo3-component-primary-color);--tree-drag-dropzone-delete-color:var(--typo3-surface-danger-text);--tree-drag-dropzone-delete-bg:var(--typo3-surface-danger);--tree-toolbar-padding-y:.75rem;--tree-toolbar-padding-x:.75rem;--tree-toolbar-padding:var(--tree-toolbar-padding-y) var(--tree-toolbar-padding-x);--tree-toolbar-spacing-y:1rem;--tree-toolbar-spacing-x:.5rem;--tree-toolbar-spacing:var(--tree-toolbar-spacing-y) var(--tree-toolbar-spacing-x);--tree-toolbar-bg:var(--tree-bg);--tree-toolbar-border-color:color-mix(in srgb,var(--tree-toolbar-bg),var(--tree-color) 10%);--tree-toolbar-border-width:1px;--tree-toolbar-element-height:1.78125rem;--tree-toolbar-bar-height:calc(var(--tree-toolbar-padding-y)*2 + var(--tree-toolbar-element-height));--tree-toolbar-box-shadow:var(--typo3-shadow-2)} +typo3-backend-component-filestorage-browser,typo3-backend-component-page-browser,typo3-backend-component-page-position-select,typo3-backend-navigation-component-filestoragetree,typo3-backend-navigation-component-formeditortree,typo3-backend-navigation-component-pagetree{display:flex;flex-direction:column;height:100%} +typo3-backend-component-filestorage-browser>:last-child,typo3-backend-component-page-browser>:last-child,typo3-backend-component-page-position-select>:last-child,typo3-backend-navigation-component-filestoragetree>:last-child,typo3-backend-navigation-component-formeditortree>:last-child,typo3-backend-navigation-component-pagetree>:last-child{flex:1} +.nodes-container{position:relative} +.nodes-container,.nodes-loader{background:var(--tree-bg);color:var(--tree-color);height:100%;width:100%} +.nodes-loader{inset-inline-start:0;position:absolute;top:0;z-index:3000} +.nodes-loader-inner{align-items:center;display:flex;height:100%;justify-content:center;width:100%} +.nodes-root{background-color:inherit;display:block;height:100%;inset-inline-start:0;overflow-y:auto;padding:2px;position:absolute;top:0;width:100%} +.nodes-list{contain:strict;transform:translateZ(0)} +.node{align-items:center;background-color:var(--tree-node-bg);border-radius:4px;color:var(--tree-node-color);cursor:pointer;display:flex;inset-inline-start:0;outline-color:var(--tree-node-border-color)!important;outline-offset:-1px;padding:0 12px;position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%} +.node-hover,.node:hover{--tree-node-color:var(--tree-node-hover-color);--tree-node-bg:var(--tree-node-hover-bg);--tree-node-border-color:var(--tree-node-hover-border-color);outline:1px solid} +.node-focus,.node:focus{--tree-node-color:var(--tree-node-focus-color);--tree-node-bg:var(--tree-node-focus-bg);--tree-node-border-color:var(--tree-node-focus-border-color);outline:1px solid} +.node-active,.node-selected,.node:active{--tree-node-color:var(--tree-node-selected-color)!important;--tree-node-bg:var(--tree-node-selected-bg)!important;--tree-node-border-color:var(--tree-node-selected-border-color)!important;outline:1px solid} +.node-active .node-information,.node-selected .node-information,.node:active .node-information{--tree-node-information:var(--tree-node-selected-color)!important} +.node-disabled .node-content,.node-dragging .node-content,.node-dragging .node-icon,.node-dragging .node-stop,.node-dragging .node-toggle,.node[disabled] .node-content{opacity:.5} +.node-dragging-after .node-content:after,.node-dragging-before .node-content:after{background-color:var(--tree-drop-position-bg);content:"";height:2px;left:0;pointer-events:none;position:absolute;width:100%} +.node-dragging-before .node-content:after{top:0} +.node-dragging-after .node-content:after{bottom:0} +.node-label{border-radius:.25rem;bottom:4px;inset-inline-start:4px;pointer-events:none;position:absolute;top:4px;width:.25rem} +.node-label,.node-treelines{-webkit-user-select:none;-moz-user-select:none;user-select:none} +.node-treelines{display:flex;flex-shrink:0;height:100%} +.node-treeline{color:inherit;flex-shrink:0;height:100%;opacity:.15;position:relative;width:20px} +.node-treeline:after,.node-treeline:before{background-color:currentColor;content:"";position:absolute} +.node-treeline--line:before{height:100%;inset-inline-start:50%;top:0;width:1px} +.node-treeline--line:after{display:none} +.node-treeline--last:before{height:50%;inset-inline-start:50%;top:0;width:1px} +.node-treeline--last:after{height:1px;inset-inline-start:50%;top:calc(50% - .5px);width:50%} +.node-treeline--connect:before{height:100%;inset-inline-start:50%;top:0;width:1px} +.node-treeline--connect:after{height:1px;inset-inline-start:50%;top:calc(50% - .5px);width:50%} +.node-action,.node-icon,.node-loading,.node-stop,.node-toggle{align-items:center;display:flex;flex-shrink:0;height:100%;justify-content:center;width:20px} +.node-treelines+.node-loading,.node-treelines+.node-stop,.node-treelines+.node-toggle{-webkit-margin-start:-20px;margin-inline-start:-20px} +.node-treelines+.node-loading typo3-backend-icon,.node-treelines+.node-stop typo3-backend-icon,.node-treelines+.node-toggle typo3-backend-icon{background-color:var(--tree-node-bg);position:relative} +.node-content{height:100%;position:relative} +.node-content,.node-contentlabel{display:flex;flex-grow:1;overflow:hidden} +.node-contentlabel{align-items:center;flex-wrap:wrap;-webkit-padding-start:.25rem;padding-inline-start:.25rem} +.node-name,.node-note{min-width:0;overflow:hidden;pointer-events:none;text-overflow:ellipsis;white-space:nowrap;width:100%} +.node-note{font-size:10px;margin-top:-.65em;opacity:.65} +.node-edit{display:flex;flex-grow:1;padding:0;width:100%;-webkit-padding-start:calc(.25rem - 1px);background:var(--typo3-component-bg);border:1px solid var(--tree-node-border-color);color:var(--typo3-component-color);outline:none;padding-inline-start:calc(.25rem - 1px)} +.node-highlight-text{background-color:var(--typo3-component-match-highlight-bg);color:var(--typo3-component-match-highlight-color)} +.node-information{display:flex;gap:.15rem;-webkit-padding-start:.25rem;color:var(--tree-node-information);opacity:.75;padding-inline-start:.25rem} +.node-information-success{--tree-node-information:var(--tree-node-information-success)} +.node-information-warning{--tree-node-information:var(--tree-node-information-warning)} +.node-information-danger{--tree-node-information:var(--tree-node-information-danger)} +.node-information-info{--tree-node-information:var(--tree-node-information-info)} +.node-action{cursor:pointer;display:none} +.node:hover .node-action{display:flex} +.node-dropzone-delete{align-items:center;background-color:var(--tree-drag-dropzone-delete-bg);border-end-end-radius:4px;border-start-end-radius:4px;color:var(--tree-drag-dropzone-delete-color);display:flex;gap:.25rem;height:100%;inset-block-start:0;inset-inline-end:0;justify-content:center;padding:0 .5rem;position:absolute;z-index:1} +.node-dropzone-delete *{pointer-events:none} +.node-dropzone-delete:hover{background-color:color-mix(in srgb,var(--tree-drag-dropzone-delete-bg),#fff 50%)} +.dragging-tooltip{align-items:center;background-color:var(--typo3-component-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:var(--typo3-component-border-radius);box-shadow:var(--typo3-component-box-shadow-tooltip);color:var(--typo3-component-color);display:grid;gap:.5rem;grid-template-columns:auto 1fr;left:0;margin:0;max-width:250px;padding:var(--typo3-component-padding-y) var(--typo3-component-padding-x);pointer-events:none;position:fixed;top:0;z-index:999999} +.dragging-tooltip-content{align-items:center;display:flex;flex-grow:1;gap:.5rem;overflow:hidden} +.dragging-tooltip-content-label{display:flex;flex-direction:column;gap:.15rem} +.dragging-tooltip-content-description{opacity:.75} +.dragging-tooltip-thumbnails{display:flex;gap:.15rem} +.dragging-tooltip-thumbnails typo3-backend-drag-tooltip-thumbnail{display:block} +.dragging-tooltip-thumbnails typo3-backend-drag-tooltip-thumbnail img,.dragging-tooltip-thumbnails typo3-backend-drag-tooltip-thumbnail svg{border-radius:.15rem;display:block} +.node-mount-point{align-items:center;background-color:var(--tree-info-bg);border:0;border-bottom:1px solid rgba(0,0,0,.25);color:var(--tree-info-color);display:flex;gap:.5em;padding:.75em 1.167em} +.node-mount-point__icon{flex:0 auto} +.node-mount-point__icon.mountpoint-close{cursor:pointer} +.node-mount-point__text{flex:1 0 0;overflow:hidden;padding:0 .5em} +typo3-backend-form-selecttree{display:flex;flex-direction:column;height:100%} +typo3-backend-form-selecttree>:last-child{flex:1} +typo3-backend-form-selecttree-toolbar{display:block} +.wizard-content:has(typo3-backend-component-page-position-select){padding:0!important;--tree-toolbar-padding:calc(var(--wizard-padding)*0.75) var(--wizard-padding)} +typo3-backend-component-page-position-select>typo3-breadcrumb{padding:var(--wizard-padding);padding-bottom:0} +.tree-element{border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:var(--typo3-input-border-radius);overflow:hidden} +.tree-toolbar{background-color:var(--tree-bg);color:var(--tree-color)} +.tree-toolbar__menu{border-bottom:var(--tree-toolbar-border-width) solid var(--tree-toolbar-border-color);gap:var(--tree-toolbar-spacing-x);min-height:calc(var(--tree-toolbar-bar-height) + var(--tree-toolbar-border-width))} +.tree-toolbar__menu,.tree-toolbar__submenu{display:flex;padding:var(--tree-toolbar-padding);position:relative} +.tree-toolbar__submenu{align-items:center;box-shadow:var(--tree-toolbar-box-shadow);flex-wrap:wrap;min-height:var(--tree-toolbar-bar-height);z-index:1} +.tree-toolbar__submenu-toggle{align-self:flex-start;flex-shrink:0;margin-left:auto} +.tree-toolbar__submenu-items{display:flex;flex:1;flex-wrap:wrap;max-height:var(--tree-toolbar-element-height);overflow:hidden;row-gap:calc(var(--tree-toolbar-padding-y)/2)} +.tree-toolbar__submenu-items--expanded{max-height:none} +.tree-toolbar__submenu-items button{margin-right:var(--tree-toolbar-padding-x)} +.tree-toolbar__buttons{align-items:center;box-shadow:var(--tree-toolbar-box-shadow);display:flex;gap:var(--tree-toolbar-spacing-x);min-height:var(--tree-toolbar-bar-height);padding:var(--tree-toolbar-padding);position:relative;z-index:1} +.tree-toolbar__search{flex-grow:1} +.tree-toolbar__menuitem{align-items:center;background:0 0;border:1px solid transparent;border-radius:var(--typo3-input-border-radius);color:inherit;display:inline-flex;font-size:var(--typo3-font-size-small);justify-content:center;min-height:var(--tree-toolbar-element-height);outline-offset:0;padding:.25rem} +.tree-toolbar__drag-node{cursor:move} +.element-browser{contain:strict;display:flex;flex-flow:column nowrap;height:100dvh;width:100%} +.element-browser .h3,.element-browser h3{font-size:1.2em} +.element-browser-header{align-items:center;background-color:var(--module-docheader-bg);display:flex;gap:var(--typo3-spacing);min-height:42px;padding:calc(var(--typo3-spacing)/2) var(--typo3-spacing)} +.element-browser-header-title{flex-grow:1} +.element-browser-nav{background-color:var(--module-docheader-bg);border-bottom:1px solid var(--module-docheader-border-color);padding:calc(var(--typo3-spacing)/2) var(--typo3-spacing)} +.element-browser-header+.element-browser-nav{padding-top:0} +.element-browser-body{overflow:unset;padding:var(--typo3-spacing)} +.element-browser-body>:first-child{margin-top:0} +.element-browser-body>:last-child{margin-bottom:0} +.element-browser-attributes{background-color:var(--module-docheader-bg);border-bottom:1px solid var(--module-docheader-border-color);padding:var(--typo3-spacing)} +.element-browser-attributes>:first-child{margin-top:0} +.element-browser-attributes>:last-child{margin-bottom:0} +.element-browser-main{background-color:var(--typo3-surface-base);color:var(--typo3-text-color-base);display:flex;flex-grow:1;flex-wrap:nowrap;overflow:hidden;position:relative} +.element-browser-main-sidebar{background-color:var(--module-docheader-bg);flex-shrink:0;height:100%} +.element-browser-main-content{container-type:inline-size;flex-grow:1;height:100%;overflow:auto} +.element-browser-main-content:has(typo3-backend-content-navigation-toggle:not([hidden])){display:grid;grid-template-columns:auto 1fr;grid-template-rows:auto 1fr} +.element-browser-main-content:has(typo3-backend-content-navigation-toggle:not([hidden]))>:not(.element-browser-navigation-toggle){grid-column:2} +.element-browser-main-content:has(typo3-backend-content-navigation-toggle:not([hidden])):before{background-color:var(--module-docheader-bg);content:"";grid-column:1;grid-row:1/-1;-webkit-border-end:1px solid var(--module-docheader-border-color);border-inline-end:1px solid var(--module-docheader-border-color)} +.element-browser-navigation-toggle{background-color:var(--module-docheader-bg);display:none;padding:.75rem;-webkit-border-end:1px solid var(--module-docheader-border-color);align-self:start;border-inline-end:1px solid var(--module-docheader-border-color);grid-column:1;grid-row:1/-1;position:-webkit-sticky;position:sticky;top:0} +.element-browser-navigation-toggle:has(typo3-backend-content-navigation-toggle:not([hidden])){align-items:start;display:flex} +.element-browser-form-group{margin-bottom:calc(var(--typo3-spacing)/2)} +.element-browser-form-group:last-child{margin-bottom:0} +.element-browser-form-group .form-label{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +@container (min-width: 500px){ +.element-browser-form-group{align-items:center;display:flex;gap:calc(var(--typo3-spacing)/2)} +.element-browser-form-group .form-label{flex-shrink:0;margin-bottom:0!important;overflow:hidden;text-overflow:ellipsis;width:200px}} +.media{display:grid;grid-template-columns:2rem auto;grid-gap:1rem} +.media .media-body{grid-column:1/3;overflow:hidden} +.media .media-left+.media-body{align-self:center;grid-column:2/3} +.note-list{display:grid;gap:var(--typo3-spacing);margin-bottom:var(--typo3-spacing)} +.note-list .note{margin-bottom:0} +.note{--typo3-note-color:var(--typo3-component-color);--typo3-note-bg:var(--typo3-component-bg);--typo3-note-border-color:color-mix(in srgb,var(--typo3-note-bg),var(--typo3-note-color) var(--typo3-border-mix));--typo3-note-border-width:var(--typo3-component-border-width);--typo3-note-border-radius:var(--typo3-component-border-radius);--typo3-note-padding-y:1rem;--typo3-note-padding-x:1rem;--typo3-note-header-bg:var(--typo3-surface-container-low);--typo3-note-header-color:var(--typo3-text-color-base);--typo3-note-header-padding-y:.5rem;--typo3-note-header-padding-x:1rem;--typo3-note-box-shadow:var(--typo3-component-box-shadow);--typo3-note-primary-header-color:var(--typo3-surface-container-primary-text);--typo3-note-primary-header-bg:var(--typo3-surface-container-primary);--typo3-note-primary-border-color:color-mix(in srgb,var(--typo3-note-primary-header-bg),var(--typo3-note-primary-header-color) var(--typo3-border-mix));--typo3-note-secondary-header-color:var(--typo3-surface-container-secondary-text);--typo3-note-secondary-header-bg:var(--typo3-surface-container-secondary);--typo3-note-secondary-border-color:color-mix(in srgb,var(--typo3-note-secondary-header-bg),var(--typo3-note-secondary-header-color) var(--typo3-border-mix));--typo3-note-info-header-color:var(--typo3-surface-container-info-text);--typo3-note-info-header-bg:var(--typo3-surface-container-info);--typo3-note-info-border-color:color-mix(in srgb,var(--typo3-note-info-header-bg),var(--typo3-note-info-header-color) var(--typo3-border-mix));--typo3-note-success-header-color:var(--typo3-surface-container-success-text);--typo3-note-success-header-bg:var(--typo3-surface-container-success);--typo3-note-success-border-color:color-mix(in srgb,var(--typo3-note-success-header-bg),var(--typo3-note-success-header-color) var(--typo3-border-mix));--typo3-note-warning-header-color:var(--typo3-surface-container-warning-text);--typo3-note-warning-header-bg:var(--typo3-surface-container-warning);--typo3-note-warning-border-color:color-mix(in srgb,var(--typo3-note-warning-header-bg),var(--typo3-note-warning-header-color) var(--typo3-border-mix));--typo3-note-danger-header-color:var(--typo3-surface-container-danger-text);--typo3-note-danger-header-bg:var(--typo3-surface-container-danger);--typo3-note-danger-border-color:color-mix(in srgb,var(--typo3-note-danger-header-bg),var(--typo3-note-danger-header-color) var(--typo3-border-mix));--typo3-note-notice-header-color:var(--typo3-surface-container-notice-text);--typo3-note-notice-header-bg:var(--typo3-surface-container-notice);--typo3-note-notice-border-color:color-mix(in srgb,var(--typo3-note-notice-header-bg),var(--typo3-note-notice-header-color) var(--typo3-border-mix));--typo3-note-default-header-color:var(--typo3-surface-container-default-text);--typo3-note-default-header-bg:var(--typo3-surface-container-default);--typo3-note-default-border-color:color-mix(in srgb,var(--typo3-note-default-header-bg),var(--typo3-note-default-header-color) var(--typo3-border-mix));background-color:var(--typo3-note-bg);border:var(--typo3-note-border-width) solid var(--typo3-note-border-color);border-radius:var(--typo3-note-border-radius);box-shadow:var(--typo3-note-box-shadow);color:var(--typo3-note-color);margin-bottom:var(--typo3-spacing);overflow:hidden;position:relative;z-index:1} +.note-header{background-color:var(--typo3-note-header-bg);color:var(--typo3-note-header-color);padding:var(--typo3-note-header-padding-y) var(--typo3-note-header-padding-x)} +.note-header-bar{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem} +.note-actions{-webkit-margin-start:auto;margin-inline-start:auto} +.note-body{padding:var(--typo3-note-padding-y) var(--typo3-note-padding-x)} +.note-body>:first-child{margin-top:0} +.note-body>:last-child{margin-bottom:0} +.note-primary{--typo3-note-header-color:var(--typo3-note-primary-header-color);--typo3-note-header-bg:var(--typo3-note-primary-header-bg);--typo3-note-border-color:var(--typo3-note-primary-border-color)} +.note-secondary{--typo3-note-header-color:var(--typo3-note-secondary-header-color);--typo3-note-header-bg:var(--typo3-note-secondary-header-bg);--typo3-note-border-color:var(--typo3-note-secondary-border-color)} +.note-category-1,.note-info{--typo3-note-header-color:var(--typo3-note-info-header-color);--typo3-note-header-bg:var(--typo3-note-info-header-bg);--typo3-note-border-color:var(--typo3-note-info-border-color)} +.note-category-4,.note-success{--typo3-note-header-color:var(--typo3-note-success-header-color);--typo3-note-header-bg:var(--typo3-note-success-header-bg);--typo3-note-border-color:var(--typo3-note-success-border-color)} +.note-category-2,.note-warning{--typo3-note-header-color:var(--typo3-note-warning-header-color);--typo3-note-header-bg:var(--typo3-note-warning-header-bg);--typo3-note-border-color:var(--typo3-note-warning-border-color)} +.note-danger{--typo3-note-header-color:var(--typo3-note-danger-header-color);--typo3-note-header-bg:var(--typo3-note-danger-header-bg);--typo3-note-border-color:var(--typo3-note-danger-border-color)} +.note-category-3,.note-notice{--typo3-note-header-color:var(--typo3-note-notice-header-color);--typo3-note-header-bg:var(--typo3-note-notice-header-bg);--typo3-note-border-color:var(--typo3-note-notice-border-color)} +.note-category-0,.note-default{--typo3-note-header-color:var(--typo3-note-default-header-color);--typo3-note-header-bg:var(--typo3-note-default-header-bg);--typo3-note-border-color:var(--typo3-note-default-border-color)} +:root{--typo3-card-grid-gap:1rem;--typo3-card-padding:1rem} +.card{--typo3-card-color:var(--typo3-text-color-base);--typo3-card-color-subtle:var(--typo3-text-color-variant);--typo3-card-bg:var(--typo3-surface-container-low);--typo3-card-border-radius:var(--typo3-component-border-radius);--typo3-card-border-color:color-mix(in srgb,var(--typo3-card-bg),var(--typo3-card-color) var(--typo3-border-mix));background:var(--typo3-card-bg);border:1px solid var(--typo3-card-border-color);border-radius:var(--typo3-card-border-radius);box-shadow:var(--typo3-component-box-shadow);color:var(--typo3-card-color);display:flex;flex-direction:column;margin-bottom:var(--typo3-spacing);overflow:hidden;position:relative;transition:all .2s ease-in-out;transition-property:box-shadow,border,transform} +.card .table-fit{border-bottom:0;border-left:0;border-radius:0;border-right:0;box-shadow:none;margin-bottom:0} +.card .table-fit:not(:first-child){margin-top:var(--typo3-card-padding)} +.card-container{container-type:inline-size;display:grid;gap:var(--typo3-card-grid-gap);grid-template-columns:1fr;margin-bottom:var(--typo3-spacing)} +.card-container .card{margin-bottom:0} +@container (min-width: 768px){ +.card-container{grid-template-columns:repeat(2,1fr)}} +@container (min-width: 1200px){ +.card-container{grid-template-columns:repeat(4,1fr)}} +@container (min-width: 992px){ +.card-size-medium{grid-column:span 2}} +.card-size-large{grid-column:1/-1} +.card-body,.card-footer,.card-header,.card-image{padding:var(--typo3-card-padding) var(--typo3-card-padding) 0 var(--typo3-card-padding)} +.card-body:last-child,.card-footer:last-child,.card-header:last-child,.card-image:last-child{padding-bottom:var(--typo3-card-padding)} +.card-body>:first-child:not(.row),.card-footer>:first-child:not(.row),.card-header>:first-child:not(.row),.card-image>:first-child:not(.row){margin-top:0} +.card-body>:last-child,.card-footer>:last-child,.card-header>:last-child,.card-image>:last-child{margin-bottom:0} +.card-image{padding-left:0;padding-right:0;position:relative} +.card-image:first-child{padding-top:0} +.card-image:first-child .card-image-badge{top:calc(var(--typo3-card-padding)*.5)} +.card-image:last-child{padding-bottom:0} +.card-image .card-image-badge{inset-inline-end:calc(var(--typo3-card-padding)*.5);position:absolute;top:var(--typo3-card-padding)} +.card-image img{display:block;height:auto;margin:0 auto;width:100%} +.card-header{border-bottom:none} +.card-header .card-icon{float:var(--typo3-position-start);-webkit-margin-end:calc(var(--typo3-card-padding)*.5);margin-inline-end:calc(var(--typo3-card-padding)*.5)} +.card-header .card-header-body{display:block;overflow:hidden} +.card-header .card-title{display:block;font-family:inherit;font-size:1.35em;font-weight:500;line-height:1.2em;margin:0} +.card-header .card-subtitle{color:var(--typo3-card-color-subtle);display:block;font-size:1em;line-height:1.2em;margin-top:.5em} +.card-header .card-longdesc{margin-top:1em} +.card-body{flex:1 1 auto} +.card-footer{border-top:none;margin-top:auto} +.card-disabled{opacity:.4} +.card-primary{--typo3-card-color:var(--typo3-surface-container-primary-text);--typo3-card-bg:var(--typo3-surface-container-primary)} +.card-secondary{--typo3-card-color:var(--typo3-surface-container-secondary-text);--typo3-card-bg:var(--typo3-surface-container-secondary)} +.card-info{--typo3-card-color:var(--typo3-surface-container-info-text);--typo3-card-bg:var(--typo3-surface-container-info)} +.card-success{--typo3-card-color:var(--typo3-surface-container-success-text);--typo3-card-bg:var(--typo3-surface-container-success)} +.card-warning{--typo3-card-color:var(--typo3-surface-container-warning-text);--typo3-card-bg:var(--typo3-surface-container-warning)} +.card-danger{--typo3-card-color:var(--typo3-surface-container-danger-text);--typo3-card-bg:var(--typo3-surface-container-danger)} +.card-notice{--typo3-card-color:var(--typo3-surface-container-notice-text);--typo3-card-bg:var(--typo3-surface-container-notice)} +.card-default{--typo3-card-color:var(--typo3-surface-container-default-text);--typo3-card-bg:var(--typo3-surface-container-default)} +.context-menu{background-color:var(--typo3-component-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:var(--typo3-component-border-radius);box-shadow:var(--typo3-component-box-shadow-flyout);color:var(--typo3-component-color);font-size:var(--typo3-component-font-size);inset-inline-start:0;line-height:var(--typo3-component-line-height);margin:0;max-height:calc(100% - 20px);max-width:calc(100% - 20px);overflow-y:auto;padding:2px;position:absolute;top:0;z-index:1000} +.context-menu-overlay{background-color:transparent;inset:0;opacity:.1;position:fixed;z-index:1000} +.context-menu-group{display:flex;flex-direction:column;gap:1px;list-style:none;margin:0;min-width:150px;padding:0;position:relative} +.context-menu-divider{border-top:var(--typo3-component-border-width) solid var(--typo3-component-border-color)!important;height:0;margin-bottom:var(--typo3-list-item-padding-y);margin-top:var(--typo3-list-item-padding-y);outline:none;padding:0} +.context-menu-item{background-color:transparent;border:none;border-radius:calc(var(--typo3-component-border-radius) - var(--typo3-component-border-width));cursor:pointer;display:flex;gap:.5em;padding:var(--typo3-list-item-padding-y) var(--typo3-list-item-padding-x);position:relative;text-align:start;text-decoration:none;width:100%} +.context-menu-item:focus,.context-menu-item:hover{outline-offset:-1px;z-index:1} +.context-menu-item:hover{background-color:var(--typo3-component-hover-bg);color:var(--typo3-component-hover-color);outline:1px solid var(--typo3-component-hover-border-color)} +.context-menu-item:focus{background-color:var(--typo3-component-focus-bg);color:var(--typo3-component-focus-color);outline:1px solid var(--typo3-component-focus-border-color)} +.context-menu-item-icon{flex-grow:0;flex-shrink:0;width:var(--icon-size-small)} +.context-menu-item-icon,.context-menu-item-label{-webkit-user-select:none;-moz-user-select:none;user-select:none} +.context-menu-item-label{flex-grow:1} +.context-menu-item-indicator{flex-grow:0;flex-shrink:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:var(--icon-size-small)} +typo3-backend-live-search{--typo3-livesearch-padding:var(--typo3-modal-padding);--typo3-livesearch-border-width:var(--typo3-component-border-width);--typo3-livesearch-border-color:var(--typo3-component-border-color);display:flex;flex-direction:column;inset:0;position:absolute} +typo3-backend-live-search .search-option-badge{inset-inline-start:100%;isolation:isolate;position:absolute;top:0!important;transform:translate(calc(-50%*var(--typo3-position-modifier)),-50%)} +typo3-backend-live-search form{border-bottom:var(--typo3-livesearch-border-width) solid var(--typo3-livesearch-border-color);padding:var(--typo3-livesearch-padding)} +typo3-backend-live-search-hint{display:flex;gap:1ch} +typo3-backend-live-search-hint p{margin-bottom:0} +typo3-backend-live-search-result-pagination nav{border-top:var(--typo3-livesearch-border-width) solid var(--typo3-livesearch-border-color);padding:var(--typo3-livesearch-padding)} +typo3-backend-live-search-result-pagination nav>:first-child{margin-top:0} +typo3-backend-live-search-result-pagination nav>:last-child{margin-bottom:0} +typo3-backend-live-search-result-page{cursor:pointer} +typo3-backend-live-search-result-container{contain:size;display:flex;flex-direction:row;flex-grow:1} +typo3-backend-live-search-result-container>.alert{align-self:flex-start;flex:1 1 auto;margin:var(--typo3-livesearch-padding)} +typo3-backend-live-search-result-item-container,typo3-backend-live-search-result-item-detail-container{flex-basis:50%;flex-grow:1;overflow:auto;padding:var(--typo3-livesearch-padding);position:relative} +typo3-backend-live-search-result-item-container{padding-top:0} +typo3-backend-live-search-result-action-list,typo3-backend-live-search-result-list{display:flex;flex-direction:column;gap:1px} +typo3-backend-live-search-result-list .livesearch-result-item-group-label{background-color:var(--typo3-surface-container-low);border-bottom:1px solid var(--typo3-component-border-color);color:var(--typo3-text-color-base);font-weight:700;line-height:inherit;margin-bottom:var(--typo3-list-item-padding-y);padding-bottom:var(--typo3-list-item-padding-y);padding-top:var(--typo3-livesearch-padding);z-index:1} +typo3-backend-live-search-result-list .livesearch-result-item-group-label.sticky{position:-webkit-sticky;position:sticky;top:0;z-index:2} +typo3-backend-live-search-result-item-detail-container .livesearch-detail-preamble{border-bottom:var(--typo3-livesearch-border-width) solid var(--typo3-livesearch-border-color);margin-bottom:var(--typo3-livesearch-padding);padding:var(--typo3-livesearch-padding) 0;text-align:center} +typo3-backend-live-search-result-item-detail-container .livesearch-detail-preamble .h3,typo3-backend-live-search-result-item-detail-container .livesearch-detail-preamble h3{margin-top:var(--typo3-spacing)!important} +typo3-backend-live-search-result-item-detail-container .livesearch-detail-preamble p{margin-bottom:0} +typo3-backend-live-search-result-item-detail-container .livesearch-detail-preamble-thumbnail{align-items:center;display:flex;height:115px;justify-content:center} +typo3-backend-live-search-result-item-detail-container .livesearch-detail-preamble-thumbnail img{border-radius:2px;box-shadow:var(--typo3-component-box-shadow-strong);height:auto;max-height:100%;max-width:100%;outline:2px solid var(--typo3-resource-tile-border-color);outline-offset:0;width:auto} +typo3-backend-live-search-result-item-detail-container .livesearch-detail-properties{border-bottom:var(--typo3-livesearch-border-width) solid var(--typo3-livesearch-border-color);-moz-column-gap:var(--typo3-spacing);column-gap:var(--typo3-spacing);display:grid;font-size:var(--typo3-font-size-small);grid-template-columns:minmax(0,auto) minmax(0,1fr);margin:0 0 var(--typo3-livesearch-padding);padding:0 0 var(--typo3-livesearch-padding);row-gap:calc(var(--typo3-spacing)*.5)} +typo3-backend-live-search-result-item-detail-container .livesearch-detail-properties dt{font-weight:400;opacity:.6} +typo3-backend-live-search-result-item-detail-container .livesearch-detail-properties dd{margin:0;word-break:break-word} +typo3-backend-live-search-result-item-detail-container .livesearch-detail-preamble-type{opacity:.5} +typo3-backend-live-search-result-item,typo3-backend-live-search-result-item-action{--typo3-livesearch-result-item-padding-y:var(--typo3-list-item-padding-y);--typo3-livesearch-result-item-padding-x:var(--typo3-list-item-padding-x);--typo3-livesearch-result-item-color:var(--typo3-component-color);--typo3-livesearch-result-item-bg:var(--typo3-component-bg);--typo3-livesearch-result-item-border-color:var(--typo3-component-border-color);--typo3-livesearch-result-iten-border-width:var(--typo3-component-border-width);--typo3-livesearch-result-item-border-radius:var(--typo3-component-border-radius);background-color:var(--typo3-livesearch-result-item-bg);border:var(--typo3-livesearch-result-iten-border-width) solid var(--typo3-livesearch-result-item-border-color);border-radius:var(--typo3-livesearch-result-item-border-radius);color:var(--typo3-livesearch-result-item-color);cursor:pointer;display:flex;font-size:var(--typo3-component-font-size);gap:var(--typo3-spacing);line-height:var(--typo3-component-line-height);outline-offset:0;padding:var(--typo3-livesearch-result-item-padding-y) var(--typo3-livesearch-result-item-padding-x)} +typo3-backend-live-search-result-item-action.active,typo3-backend-live-search-result-item-action:focus,typo3-backend-live-search-result-item-action:hover,typo3-backend-live-search-result-item.active,typo3-backend-live-search-result-item:focus,typo3-backend-live-search-result-item:hover{z-index:1} +typo3-backend-live-search-result-item-action:hover,typo3-backend-live-search-result-item:hover{--typo3-livesearch-result-item-color:var(--typo3-component-hover-color);--typo3-livesearch-result-item-bg:var(--typo3-component-hover-bg);--typo3-livesearch-result-item-border-color:var(--typo3-component-hover-border-color)} +typo3-backend-live-search-result-item-action.active,typo3-backend-live-search-result-item-action:focus,typo3-backend-live-search-result-item.active,typo3-backend-live-search-result-item:focus{--typo3-livesearch-result-item-color:var(--typo3-component-focus-color);--typo3-livesearch-result-item-bg:var(--typo3-component-focus-bg);--typo3-livesearch-result-item-border-color:var(--typo3-component-focus-border-color)} +typo3-backend-live-search-result-item-action:focus-within,typo3-backend-live-search-result-item:focus-within{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-livesearch-result-item-border-color),transparent 25%)} +typo3-backend-live-search-result-item .livesearch-expand-action,typo3-backend-live-search-result-item-action .livesearch-expand-action{align-items:center;display:flex;flex:0;margin:calc(var(--typo3-livesearch-result-item-padding-y)*-1) calc(var(--typo3-livesearch-result-item-padding-x)*-1);padding:var(--typo3-livesearch-result-item-padding-y) var(--typo3-livesearch-result-item-padding-x);-webkit-border-start:var(--typo3-component-border-width) solid transparent;border-inline-start:var(--typo3-component-border-width) solid transparent} +typo3-backend-live-search-result-item .livesearch-expand-action:hover,typo3-backend-live-search-result-item-action .livesearch-expand-action:hover{border-inline-start-color:var(--typo3-livesearch-result-item-border-color)} +typo3-backend-live-search-result-item,typo3-backend-live-search-result-item-action{overflow:hidden;width:100%} +typo3-backend-live-search-result-item-action>:first-child,typo3-backend-live-search-result-item>:first-child{display:flex;flex-grow:1;gap:.5em;overflow:hidden} +typo3-backend-live-search-result-item-action>:first-child .livesearch-result-item-icon,typo3-backend-live-search-result-item-action>:first-child .livesearch-result-item-indicator,typo3-backend-live-search-result-item>:first-child .livesearch-result-item-icon,typo3-backend-live-search-result-item>:first-child .livesearch-result-item-indicator{align-items:center;display:flex;flex-grow:0;flex-shrink:0;gap:.5em} +typo3-backend-live-search-result-item-action>:first-child .livesearch-result-item-summary,typo3-backend-live-search-result-item>:first-child .livesearch-result-item-summary{display:flex;flex-direction:column;gap:.25em;overflow:hidden} +typo3-backend-live-search-result-item-action>:first-child .livesearch-result-item-summary .small,typo3-backend-live-search-result-item-action>:first-child .livesearch-result-item-summary small,typo3-backend-live-search-result-item>:first-child .livesearch-result-item-summary .small,typo3-backend-live-search-result-item>:first-child .livesearch-result-item-summary small{opacity:.5} +typo3-backend-live-search-result-item-action>:first-child .livesearch-result-item-title,typo3-backend-live-search-result-item>:first-child .livesearch-result-item-title{align-items:center;display:flex;flex-grow:1;gap:.5em} +typo3-backend-live-search-result-item-action>:first-child .livesearch-result-item-title-contentlabel,typo3-backend-live-search-result-item>:first-child .livesearch-result-item-title-contentlabel{overflow:hidden;text-overflow:ellipsis;text-wrap:nowrap} +typo3-backend-bookmark-manager-content{--bookmark-manager-color:var(--typo3-component-color);--bookmark-manager-bg:var(--typo3-surface-container-low);--bookmark-manager-border-radius:var(--typo3-component-border-radius);--bookmark-manager-border-color:color-mix(in srgb,var(--bookmark-manager-bg),var(--bookmark-manager-color) 10%);--bookmark-manager-padding:1rem;--bookmark-manager-box-shadow:var(--typo3-component-box-shadow);--bookmark-manager-border-width:var(--typo3-component-border-width);--bookmark-manager-toolbar-color:var(--bookmark-manager-color);--bookmark-manager-toolbar-bg:transparent} +typo3-backend-bookmark-manager-content .bookmark-manager{border:var(--bookmark-manager-border-width) solid var(--bookmark-manager-border-color);border-radius:var(--bookmark-manager-border-radius);box-shadow:var(--bookmark-manager-box-shadow);display:grid;grid-template-areas:"toolbar" "content";grid-template-rows:auto 1fr} +typo3-backend-bookmark-manager-content .bookmark-manager-toolbar{align-items:center;background-color:var(--bookmark-manager-toolbar-bg);border-bottom:var(--bookmark-manager-border-width) solid var(--bookmark-manager-border-color);color:var(--bookmark-manager-toolbar-color);display:flex;gap:.5rem;grid-area:toolbar;padding:var(--bookmark-manager-padding)} +typo3-backend-bookmark-manager-content .bookmark-manager-toolbar-start{align-items:center;display:flex;gap:.5rem} +typo3-backend-bookmark-manager-content .bookmark-manager-toolbar-end{align-items:center;display:flex;gap:.5rem;margin-left:auto} +typo3-backend-bookmark-manager-content .bookmark-manager-content{grid-area:content;overflow-y:auto;padding:var(--bookmark-manager-padding)} +.modal-header:has(+.modal-body typo3-backend-bookmark-manager-content){border-bottom:0} +.modal-body typo3-backend-bookmark-manager-content{--bookmark-manager-padding:var(--typo3-modal-padding);--bookmark-manager-border-radius:0;--bookmark-manager-box-shadow:none;bottom:0;left:0;position:absolute;right:0;top:0} +.modal-body typo3-backend-bookmark-manager-content .bookmark-manager{border:none;bottom:0;left:0;position:absolute;right:0;top:0} +typo3-backend-wizard{--wizard-color:var(--typo3-component-color);--wizard-bg:var(--typo3-surface-container-low);--wizard-border-radius:var(--typo3-component-border-radius);--wizard-border-color:color-mix(in srgb,var(--wizard-bg),var(--wizard-color) 5%);--wizard-padding:1rem;--wizard-box-shadow:var(--typo3-component-box-shadow);--wizard-border-width:var(--typo3-component-border-width);--wizard-progress-bg:var(--typo3-surface-container-base);--wizard-progress-color:var(--typo3-text-color-base);--wizard-progress-border-color:color-mix(in srgb,var(--wizard-progress-bg),var(--wizard-progress-color) 10%)} +typo3-backend-wizard .wizard{border:var(--wizard-border-width) solid var(--wizard-border-color);border-radius:var(--wizard-border-radius);box-shadow:var(--wizard-box-shadow);display:grid;grid-template-rows:1fr auto} +typo3-backend-wizard .wizard:has(.wizard-progress){grid-template-rows:auto 1fr auto} +typo3-backend-wizard .wizard-progress{background-color:var(--wizard-progress-bg);border-bottom:var(--wizard-border-width) solid var(--wizard-progress-border-color);color:var(--wizard-progress-color);padding:calc(var(--wizard-padding)*.75) var(--wizard-padding)} +typo3-backend-wizard .wizard-progress typo3-backend-progress-tracker{margin:0} +typo3-backend-wizard .wizard-loader{align-items:center;display:flex;flex-direction:column;gap:.5rem;height:100%;justify-content:center} +typo3-backend-wizard .wizard-content{overflow-y:auto;padding:var(--wizard-padding)} +typo3-backend-wizard .wizard-actions{border-top:var(--wizard-border-width) solid var(--wizard-border-color);display:flex;justify-content:space-between;padding:calc(var(--wizard-padding)/2) var(--wizard-padding);-webkit-user-select:none;-moz-user-select:none;user-select:none} +.modal-header:has(+.modal-body typo3-backend-wizard){border-bottom:0} +.modal-body typo3-backend-wizard{--wizard-padding:var(--typo3-modal-padding);--wizard-progress-bg:var(--typo3-modal-header-bg);--wizard-progress-color:var(--typo3-modal-header-color);--wizard-border-radius:0;--wizard-box-shadow:none;bottom:0;left:0;position:absolute;right:0;top:0} +.modal-body typo3-backend-wizard .wizard{border:none;bottom:0;left:0;position:absolute;right:0;top:0} +.modal-body typo3-backend-wizard .wizard-progress{padding-top:0} +.recordidentity,.recordidentity-type{align-items:center;display:inline-flex;gap:.25rem} +@keyframes record-pulse{ +0%,to{opacity:1} +50%{opacity:.4}} +.recordlist{--typo3-recordlist-color:var(--typo3-component-color);--typo3-recordlist-bg:var(--typo3-component-bg);--typo3-recordlist-border-color:color-mix(in srgb,var(--typo3-recordlist-bg),var(--typo3-recordlist-color) var(--typo3-border-mix));--typo3-recordlist-border-width:var(--typo3-component-border-width);--typo3-recordlist-border-radius:var(--typo3-component-border-radius);--typo3-recordlist-border-radius-top:var(--typo3-recordlist-border-radius);--typo3-recordlist-border-radius-bottom:var(--typo3-recordlist-border-radius);--typo3-recordlist-border-radius-inner-top:max(0px,calc(var(--typo3-recordlist-border-radius-top) - var(--typo3-recordlist-border-width)));--typo3-recordlist-border-radius-inner-bottom:max(0px,calc(var(--typo3-recordlist-border-radius-bottom) - var(--typo3-recordlist-border-width)));--typo3-recordlist-padding-y:.75rem;--typo3-recordlist-padding-x:1rem;--typo3-recordlist-header-bg:var(--typo3-surface-container-default);--typo3-recordlist-header-color:var(--typo3-surface-container-default-text);--typo3-recordlist-spacing:var(--typo3-component-spacing);--typo3-recordlist-box-shadow:var(--typo3-component-box-shadow);--typo3-recordlist-progress-bg:var(--typo3-state-primary-bg);background:var(--typo3-recordlist-bg);border:var(--typo3-recordlist-border-width) solid var(--typo3-recordlist-border-color);border-radius:var(--typo3-recordlist-border-radius);box-shadow:var(--typo3-recordlist-box-shadow);margin-bottom:var(--typo3-spacing);overflow:hidden} +.recordlist table tr td.deletePlaceholder{text-decoration:line-through} +.recordlist .alert,.recordlist .table-fit{border-bottom:0;border-left:0;border-radius:0;border-right:0;box-shadow:none;margin-bottom:0} +.recordlist .alert{padding:var(--typo3-recordlist-padding-y) var(--typo3-recordlist-padding-x)} +.recordlist+.recordlist{margin-top:calc(var(--typo3-spacing)*1.5)} +.recordlist tr{opacity:1;transition:opacity .5s} +.recordlist tr.record-pulse{animation:record-pulse 1s ease-out 0s 1 normal none} +.recordlist-heading{background:var(--typo3-recordlist-header-bg);color:var(--typo3-recordlist-header-color);padding:var(--typo3-recordlist-padding-y) var(--typo3-recordlist-padding-x)} +.recordlist-heading,.recordlist-heading-row{align-items:center;display:flex;flex-wrap:wrap;gap:var(--typo3-recordlist-padding-y) var(--typo3-recordlist-padding-x)} +.recordlist-heading-row,.recordlist-heading-title{flex-grow:1;max-width:100%} +.recordlist-heading-title{font-weight:700;width:250px} +.recordlist-heading-actions,.recordlist-heading-selection{align-items:center;display:flex;flex-wrap:wrap;gap:.25rem} +.resource-tiles{--typo3-resource-tiles-grid-spacing:.5rem;--typo3-resource-tiles-grid-width:150px;display:grid;gap:var(--typo3-resource-tiles-grid-spacing);grid-template-columns:repeat(auto-fill,var(--typo3-resource-tiles-grid-width));-webkit-user-select:none;-moz-user-select:none;user-select:none} +.resource-tiles-container{container-type:inline-size;margin-bottom:var(--typo3-spacing)} +@container (min-width: 480px){ +.resource-tiles{--typo3-resource-tiles-grid-width:169px}} +@container (min-width: 768px){ +.resource-tiles{--typo3-resource-tiles-grid-width:200px}} +.resource-tile{--typo3-resource-tile-spacing:1rem;--typo3-resource-tile-border-radius:var(--typo3-component-border-radius);--typo3-resource-tile-nameplate-size:12px;--typo3-resource-tile-nameplate-activity-size:10px;--typo3-resource-tile-checkbox-size:16px;--typo3-resource-tile-color-state:initial;--typo3-resource-tile-bg-state:initial;--typo3-resource-tile-color:var(--typo3-component-color);--typo3-resource-tile-bg:var(--typo3-component-bg);--typo3-resource-tile-border-color:color-mix(in srgb,var(--typo3-resource-tile-bg-state,var(--typo3-resource-tile-bg)),var(--typo3-resource-tile-color-state,var(--typo3-resource-tile-color)) var(--typo3-border-mix));--typo3-resource-tile-primary-color:var(--typo3-surface-container-primary-text);--typo3-resource-tile-primary-bg:var(--typo3-surface-container-primary);--typo3-resource-tile-secondary-color:var(--typo3-surface-container-secondary-text);--typo3-resource-tile-secondary-bg:var(--typo3-surface-container-secondary);--typo3-resource-tile-info-color:var(--typo3-surface-container-info-text);--typo3-resource-tile-info-bg:var(--typo3-surface-container-info);--typo3-resource-tile-success-color:var(--typo3-surface-container-success-text);--typo3-resource-tile-success-bg:var(--typo3-surface-container-success);--typo3-resource-tile-warning-color:var(--typo3-surface-container-warning-text);--typo3-resource-tile-warning-bg:var(--typo3-surface-container-warning);--typo3-resource-tile-danger-color:var(--typo3-surface-container-danger-text);--typo3-resource-tile-danger-bg:var(--typo3-surface-container-danger);--typo3-resource-tile-notice-color:var(--typo3-surface-container-notice-text);--typo3-resource-tile-notice-bg:var(--typo3-surface-container-notice);--typo3-resource-tile-default-color:var(--typo3-surface-container-default-text);--typo3-resource-tile-default-bg:var(--typo3-surface-container-default);background-color:var(--typo3-resource-tile-bg-state,var(--typo3-resource-tile-bg));border:1px solid var(--typo3-resource-tile-border-color);border-radius:var(--typo3-resource-tile-border-radius);color:var(--typo3-resource-tile-color-state,var(--typo3-resource-tile-color));padding-top:98%;position:relative} +.resource-tile:hover{--typo3-resource-tile-bg-state:color-mix(in srgb,var(--typo3-resource-tile-bg),var(--typo3-resource-tile-color) 6%);--typo3-resource-tile-color-state:var(--typo3-resource-tile-color);text-decoration:none} +.resource-tile:focus-within{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-resource-tile-border-color),transparent 25%)} +.resource-tile.active{--typo3-resource-tile-color:var(--typo3-resource-tile-primary-color);--typo3-resource-tile-bg:var(--typo3-resource-tile-primary-bg)} +.resource-tile.info,.resource-tile.selected{--typo3-resource-tile-color:var(--typo3-resource-tile-info-color);--typo3-resource-tile-bg:var(--typo3-resource-tile-info-bg)} +.resource-tile.success{--typo3-resource-tile-color:var(--typo3-resource-tile-success-color);--typo3-resource-tile-bg:var(--typo3-resource-tile-success-bg)} +.resource-tile.danger{--typo3-resource-tile-color:var(--typo3-resource-tile-danger-color);--typo3-resource-tile-bg:var(--typo3-resource-tile-danger-bg)} +.resource-tile.warning{--typo3-resource-tile-color:var(--typo3-resource-tile-warning-color);--typo3-resource-tile-bg:var(--typo3-resource-tile-warning-bg)} +.resource-tile.active .resource-tile-checkbox,.resource-tile.selected .resource-tile-checkbox,.resource-tile:focus-within .resource-tile-checkbox,.resource-tile:hover .resource-tile-checkbox{display:block} +.resource-tile>a,.resource-tile>button{align-items:unset;background:none;border:none;bottom:0;color:inherit;display:flex;flex-direction:column;left:0;outline:none;padding:0;position:absolute;right:0;text-decoration:none;top:0;width:100%} +.resource-tile-label{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0} +.resource-tile-preview{flex:1;margin:var(--typo3-resource-tile-spacing);margin-bottom:0;position:relative} +.resource-tile-preview-content{height:100%;left:0;position:absolute;top:0;width:100%} +.resource-tile-icon,.resource-tile-image{align-items:center;display:flex;height:100%;justify-content:center;width:100%} +.resource-tile-image img{border-radius:2px;box-shadow:var(--typo3-component-box-shadow-strong);height:auto;max-height:100%;max-width:100%;-o-object-fit:contain;object-fit:contain;outline:2px solid var(--typo3-resource-tile-border-color);outline-offset:0;width:auto} +.resource-tile-image-icon{inset-inline-start:calc(var(--typo3-resource-tile-spacing)*-.5);position:absolute;top:calc(var(--typo3-resource-tile-spacing)*-.5)} +.resource-tile-nameplate{display:flex;flex-direction:column;font-size:var(--typo3-resource-tile-nameplate-size);padding:var(--typo3-resource-tile-spacing);text-align:center;width:100%} +.resource-tile-nameplate-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.resource-tile-nameplate-activity{font-size:var(--typo3-resource-tile-nameplate-activity-size);opacity:.75} +.resource-tile-checkbox{display:none;font-size:var(--typo3-resource-tile-checkbox-size);inset-inline-end:calc(var(--typo3-resource-tile-spacing)/2);position:absolute;top:calc(var(--typo3-resource-tile-spacing)/2)} +.resource-tile-checkbox .form-check-input{margin-top:0} +.recordsearchbox-container{margin-bottom:var(--typo3-spacing)} +.recordsearchbox-container [data-recordsearchbox-levels]{max-width:140px} +.treeline-container,.treelist{--treelist-color:var(--typo3-text-color-base);--treelist-comment-color:var(--typo3-text-color-variant);--treelist-bg:var(--typo3-surface-container-lowest);--treelist-border-width:1px;--treelist-border-color:color-mix(in srgb,var(--treelist-bg),var(--treelist-color) 20%);--treelist-indentation:1rem;--treelist-indentation-spacer:.5rem;--treelist-item-spacer:2px;--treelist-item-line-height:24px;--treelist-control-size:16px} +.treelist,.treelist ul{background:var(--treelist-bg);color:var(--treelist-color);padding:0;-webkit-padding-start:calc(var(--treelist-indentation)/2);list-style:none;padding-inline-start:calc(var(--treelist-indentation)/2);position:relative} +.treelist ul:before,.treelist:before{bottom:calc(var(--treelist-item-line-height)/2);content:"";display:block;inset-inline-start:calc(var(--treelist-indentation)/2 - var(--treelist-border-width)/2);margin-bottom:-2px;position:absolute;top:0;width:0;-webkit-border-start:1px solid var(--treelist-border-color);border-inline-start:1px solid var(--treelist-border-color)} +.treelist ul:after,.treelist:after{content:"";display:table} +.treelist li{margin:var(--treelist-item-spacer) 0;-webkit-padding-start:var(--treelist-indentation);line-height:var(--treelist-item-line-height);padding-inline-start:var(--treelist-indentation);position:relative} +.treelist li:before{border-top:1px solid var(--treelist-border-color);content:"";display:block;height:0;inset-inline-start:0;margin-top:-2px;position:absolute;top:calc(var(--treelist-item-line-height)/2 + 1px);width:calc(var(--treelist-indentation) - var(--treelist-indentation-spacer) - 1px)} +.treelist li:last-child:before{background:var(--treelist-bg);bottom:0;height:auto;inset-inline-start:0;width:calc(var(--treelist-indentation) - var(--treelist-indentation-spacer))} +.treelist li.active>.treelist-group:before{background-color:hsla(0,0%,100%,.75);border:1px solid rgba(0,0,0,.25);border-radius:2px;bottom:-1px;content:"";display:block;left:calc(var(--treelist-indentation-space)*-1);position:absolute;right:calc(var(--treelist-indentation-space)*-1);top:-1px} +.treelist li.loose:before{display:none!important} +.treelist li .treelist-control{align-items:center;display:flex;justify-content:center} +.treelist-comment{color:var(--treelist-comment-color);font-family:var(--typo3-font-family-monospace)} +.treelist-group{display:block} +.treelist-group,.treelist-group>span{position:relative} +.treelist-group-monospace{font-family:var(--typo3-font-family-monospace)} +.treelist-operator{opacity:.5} +.treelist-value{font-weight:700} +.treelist-icon{top:-1px} +.treelist-show{display:inline-block;position:relative;text-align:center;width:var(--treelist-control-size);-webkit-margin-start:-2px;margin-inline-start:-2px} +.treelist-control{display:block;inset-inline-start:0;line-height:var(--treelist-control-size);text-align:center;top:calc(var(--treelist-item-line-height)/2);-webkit-user-select:none;-moz-user-select:none;user-select:none} +.treelist-control,.treelist-control:before,.treelist-control:target:before{height:var(--treelist-control-size);position:absolute;transform:translate(calc(-50%*var(--typo3-position-modifier)),-50%);width:var(--treelist-control-size)} +.treelist-control:before,.treelist-control:target:before{background-color:var(--treelist-bg);content:"";inset-inline-start:50%;top:50%} +.treelist-control:active,.treelist-control:focus,.treelist-control:hover{cursor:pointer;outline:none;text-decoration:none} +.treelist-control typo3-backend-icon{--icon-color-primary:var(--treelist-color)} +.treelist-root{-webkit-padding-start:0;padding-inline-start:0} +.treelist-root:before{display:none} +.treelist-root>li{-webkit-padding-start:var(--treelist-control-size);padding-inline-start:var(--treelist-control-size)} +.treelist-root>li:before{display:none} +.treelist-root>li>.treelist-group>.treelist-control{-webkit-margin-start:calc(var(--treelist-control-size)*-1);margin-inline-start:calc(var(--treelist-control-size)*-1)} +.treelist-root>li>.treelist-group>.treelist-control:before{background-color:transparent} +.treelist-root-clean>li{-webkit-padding-start:0;padding-inline-start:0} +.example .treeline-container,.example .treelist{--treelist-color:var(--typo3-example-color);--treelist-bg:var(--typo3-example-bg)} +.panel .treeline-container,.panel .treelist{--treelist-color:var(--typo3-panel-color);--treelist-bg:var(--typo3-panel-bg)} +.indent{--indent-base:16px;--indent-level:0;-webkit-margin-start:calc(var(--indent-base)*var(--indent-level));margin-inline-start:calc(var(--indent-base)*var(--indent-level))} +.indent-inline-block{display:inline-block} +:root{--settings-color:var(--typo3-component-color);--settings-padding:calc(var(--typo3-spacing)*2);--settings-bg:var(--typo3-component-bg);--settings-border-width:var(--typo3-component-border-width);--settings-border-color:var(--typo3-component-border-color);--settings-border-radius:var(--typo3-component-border-radius);--settings-box-shadow:var(--typo3-component-box-shadow);--settings-highlight:var(--typo3-component-primary-color);--settings-indicator-bg:transparent;--settings-item-color:var(--settings-color);--settings-item-bg:var(--settings-bg);--settings-search-height:80px;--settings-nav-height-correction:100px;--settings-nav-item-padding-x:var(--typo3-list-item-padding-x);--settings-nav-item-padding-y:var(--typo3-list-item-padding-y);--settings-parent-offset:0} +.module-body typo3-backend-settings-editor{--settings-parent-offset:var(--module-docheader-bar-height)} +.modal-body typo3-backend-settings-editor{--settings-parent-offset:var(--typo3-modal-padding)} +.settings-container{container-type:inline-size} +.settings{background-color:var(--settings-bg);border:var(--settings-border-width) solid var(--settings-border-color);border-radius:var(--settings-border-radius);box-shadow:var(--settings-box-shadow);color:var(--settings-color);display:grid;gap:var(--settings-border-width);grid-template-columns:1fr} +.settings-search{align-items:center;background-color:color-mix(in srgb,var(--settings-bg),currentColor 1%);border-start-end-radius:calc(var(--settings-border-radius) - var(--settings-border-width));border-start-start-radius:calc(var(--settings-border-radius) - var(--settings-border-width));display:flex;height:var(--settings-search-height);padding-inline:calc(var(--settings-padding) - .3rem);position:-webkit-sticky;position:sticky;top:var(--settings-parent-offset);z-index:5} +.settings-search:has(+:not([hidden])){border-bottom:var(--settings-border-width) solid var(--settings-border-color)} +.settings-navigation{display:none;position:relative} +.settings-navigation-inner{padding:calc(var(--settings-padding) - .3rem)} +.settings-body-inner{padding:var(--settings-padding)} +@container (min-width: 780px){ +.settings:has(.settings-navigation){grid-template-columns:300px 1fr} +.settings:has(.settings-navigation) .settings-search{grid-column:span 2} +.settings:has(.settings-navigation) .settings-navigation{display:flex;flex-direction:column} +.settings:has(.settings-navigation) .settings-navigation-inner{flex-grow:1;overflow-y:auto;position:-webkit-sticky;position:sticky;scrollbar-gutter:stable;top:calc(var(--settings-parent-offset)*-1)} +.settings:has(.settings-navigation):has(.settings-search) .settings-navigation-inner{top:calc(var(--settings-search-height) + var(--settings-parent-offset))}} +.settings-navigation ul{list-style:none;margin:0;padding:0} +.settings-navigation ul li{margin-top:1px} +.settings-navigation ul ul{-webkit-padding-start:1rem;padding-inline-start:1rem} +.settings-navigation>ul:first-child>li:first-child{margin-top:0} +.settings-navigation [identifier=actions-chevron-end]{opacity:.5} +.settings-navigation-item{background-color:transparent;border:none;border-radius:calc(var(--typo3-component-border-radius) - var(--typo3-component-border-width));color:var(--typo3-component-color);cursor:pointer;display:flex;gap:.5em;padding:var(--settings-nav-item-padding-y) var(--settings-nav-item-padding-x);position:relative;text-align:start;text-decoration:none;width:100%} +.settings-navigation-item.active,.settings-navigation-item:focus,.settings-navigation-item:hover{outline-offset:-1px;text-decoration:none;z-index:1} +.settings-navigation-item:hover{background-color:var(--typo3-component-hover-bg);color:var(--typo3-component-hover-color);outline:1px solid var(--typo3-component-hover-border-color)} +.settings-navigation-item:focus-visible{outline:1px solid var(--typo3-component-focus-border-color)} +.settings-navigation-item.active{background-color:var(--typo3-component-focus-bg);color:var(--typo3-component-focus-color);outline:1px solid var(--typo3-component-focus-border-color)} +.settings-navigation-item-icon{flex-grow:0;flex-shrink:0;width:var(--icon-size-small)} +.settings-navigation-item-icon,.settings-navigation-item-label{-webkit-user-select:none;-moz-user-select:none;user-select:none} +.settings-navigation-item-label{flex-grow:1} +.settings-category{text-wrap:balance;z-index:10} +.settings-category-headline{align-items:center;display:flex;gap:.5em} +.settings-category-headline>typo3-backend-icon{color:var(--typo3-text-color-primary)} +.settings-category-description,.settings-category-headline{max-width:600px} +.settings-category-list+.settings-category-list{margin-top:calc(var(--typo3-spacing)*2)} +.settings-item{background:var(--settings-item-bg);border-radius:var(--settings-border-radius);color:var(--settings-item-color);contain:inline-size;padding-block:var(--typo3-component-padding-y);position:relative;-webkit-padding-start:calc(var(--typo3-component-padding-x) + 4px);padding-inline-start:calc(var(--typo3-component-padding-x) + 4px);-webkit-padding-end:calc(var(--typo3-component-padding-x) + 3rem);padding-inline-end:calc(var(--typo3-component-padding-x) + 3rem);-webkit-margin-start:calc(var(--typo3-component-padding-x)*-1);margin-inline-start:calc(var(--typo3-component-padding-x)*-1);-webkit-margin-end:calc(var(--typo3-component-padding-x)*-1);margin-inline-end:calc(var(--typo3-component-padding-x)*-1)} +.settings-item:focus-within,.settings-item:focus-within *{--settings-item-bg:var(--typo3-component-focus-bg);--settings-item-color:var(--typo3-component-focus-color)} +.settings-item:focus-within{outline:1px solid var(--typo3-component-focus-border-color);outline-offset:-1px} +.settings-item:focus .settings-item-actions,.settings-item:focus-within .settings-item-actions,.settings-item:hover .settings-item-actions{opacity:1} +.settings-item-indicator{background:var(--settings-indicator-bg);border-end-start-radius:calc(var(--settings-border-radius) - var(--settings-border-width));border-start-start-radius:calc(var(--settings-border-radius) - var(--settings-border-width));inset-block-end:var(--settings-border-width);inset-block-start:var(--settings-border-width);inset-inline-start:var(--settings-border-width);position:absolute;width:.3rem} +.settings-item[data-status=modified],.settings-item[data-status=modified] *{--settings-indicator-bg:var(--typo3-state-info-bg)} +.settings-item[data-status=error],.settings-item[data-status=error] *{--settings-indicator-bg:var(--typo3-state-danger-bg)} +.settings-item-actions{display:flex;inset-block-end:0;inset-block-start:0;inset-inline-end:0;justify-content:center;opacity:0;padding-block:var(--typo3-component-padding-y);position:absolute;transition:opacity .3s ease-in-out;width:3rem} +.settings-item-actions>.dropdown>button{align-items:center;background-color:transparent;border:none;border-radius:50%;color:inherit;display:flex;height:32px;justify-content:center;margin-top:-4px;outline:none;padding:0;width:32px} +.settings-item-actions>.dropdown>button:hover{background:color-mix(in srgb,var(--settings-item-bg),var(--settings-item-color) 10%)} +.settings-item-actions>.dropdown>button:focus{background:color-mix(in srgb,var(--typo3-component-focus-bg),var(--typo3-component-focus-border-color) 20%);color:var(--typo3-component-focus-color)} +.settings-item-actions>.dropdown>button:after{display:none} +.settings-item-title{margin-bottom:calc(var(--typo3-spacing)/2)} +.settings-item-label{font-weight:700;margin-bottom:calc(var(--typo3-spacing)*.25)} +.settings-item-description{color:color-mix(in srgb,var(--settings-color),var(--settings-bg) 25%)} +.settings-item-key{color:var(--settings-highlight);font-family:var(--typo3-font-family-code);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.settings-item-message{margin-top:calc(var(--typo3-spacing)/2)} +.settings-item-message:empty{display:none} +:is(.settings-item-description,.settings-category-description) :is(p,dl,ol,ul,blockquote,pre){margin-bottom:calc(var(--typo3-spacing)*.25)} +:is(.settings-item-description,.settings-category-description) :is(ol,ul){padding-left:0;padding-right:0;-webkit-padding-start:calc(var(--typo3-spacing)*.25 + 1rem);padding-inline-start:calc(var(--typo3-spacing)*.25 + 1rem)} +:is(.settings-item-description,.settings-category-description) blockquote{padding:calc(var(--typo3-spacing)*.25);-webkit-padding-start:calc(var(--typo3-spacing)*.5);padding-inline-start:calc(var(--typo3-spacing)*.5);-webkit-border-start:.25rem solid color-mix(in srgb,var(--typo3-text-color-base),transparent 75%);border-inline-start:.25rem solid color-mix(in srgb,var(--typo3-text-color-base),transparent 75%);margin-bottom:calc(var(--typo3-spacing)*.25)} +:is(.settings-item-description,.settings-category-description) code{background:light-dark(var(--token-color-neutral-7),var(--token-color-neutral-90));border-radius:var(--typo3-component-border-radius);padding:.2em .4em;white-space:break-spaces} +.example{--typo3-example-color:var(--typo3-component-color);--typo3-example-bg:var(--typo3-surface-base);--typo3-example-border-width:var(--typo3-component-border-width);--typo3-example-border-radius:var(--typo3-component-border-radius);--typo3-example-border-color:var(--typo3-component-border-color);background-color:var(--typo3-example-bg);border:var(--typo3-example-border-width) solid var(--typo3-example-border-color);border-radius:var(--typo3-example-border-radius);color:var(--typo3-example-color);margin-bottom:var(--typo3-spacing);padding:var(--typo3-spacing)} +.example+:not(.example){margin-top:var(--typo3-component-spacing)} +.example>:last-child{margin-bottom:0} +.example:before{color:var(--typo3-component-variant-color);content:"EXAMPLE";display:block;font-size:.9em;font-weight:700;margin-bottom:var(--typo3-spacing)} +.example--code:before{content:"EXAMPLE CODE"} +.example--checkered{--typo3-bg-checkerboard-pattern-size:20px;--typo3-bg-checkerboard-background-color:light-dark(var(--token-color-neutral-10),var(--token-color-neutral-85));--typo3-bg-checkerboard-background-image-color:light-dark(var(--token-color-neutral-0),var(--token-color-neutral-90));background:var(--typo3-bg-checkerboard-background-color);background-clip:padding-box;background-image:linear-gradient(45deg,var(--typo3-bg-checkerboard-background-image-color) 25%,transparent 25%),linear-gradient(135deg,var(--typo3-bg-checkerboard-background-image-color) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,var(--typo3-bg-checkerboard-background-image-color) 75%),linear-gradient(135deg,transparent 75%,var(--typo3-bg-checkerboard-background-image-color) 75%);background-position:0 0,calc(var(--typo3-bg-checkerboard-pattern-size)/2) 0,calc(var(--typo3-bg-checkerboard-pattern-size)/2) calc(var(--typo3-bg-checkerboard-pattern-size)/2*-1),0 calc(var(--typo3-bg-checkerboard-pattern-size)/2);background-size:var(--typo3-bg-checkerboard-pattern-size) var(--typo3-bg-checkerboard-pattern-size)} +.example pre code{display:block;overflow:auto;width:100%} +.dropzone{--dropzone-font-size:var(--typo3-component-font-size);--dropzone-color:var(--typo3-component-color);--dropzone-bg:var(--typo3-component-bg);--dropzone-border-color:var(--typo3-component-border-color);--dropzone-hover-color:var(--typo3-component-hover-color);--dropzone-hover-bg:var(--typo3-component-hover-bg);--dropzone-hover-border-color:var(--typo3-component-hover-border-color);--dropzone-focus-color:var(--typo3-component-focus-color);--dropzone-focus-bg:var(--typo3-component-focus-bg);--dropzone-focus-border-color:var(--typo3-component-focus-border-color);--dropzone-border-width:var(--typo3-component-border-width);--dropzone-border-radius:var(--typo3-component-border-radius);--dropzone-spacing:.25rem;--dropzone-padding-y:var(--typo3-component-padding-y);--dropzone-padding-x:var(--typo3-component-padding-x);--dropzone-icons-arrow-down:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%23fff' d='M7 2v7.3H5c-.4 0-.6.5-.4.8l3 3.7c.2.2.6.2.8 0l3-3.7c.2-.3 0-.8-.4-.8H9V2z'/%3E%3C/svg%3E");--dropzone-icons-close:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%23fff' d='M11.9 5.5 9.4 8l2.5 2.5c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L8 9.4l-2.5 2.5c-.2.2-.5.2-.7 0l-.7-.7c-.2-.2-.2-.5 0-.7L6.6 8 4.1 5.5c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0L8 6.6l2.5-2.5c.2-.2.5-.2.7 0l.7.7c.2.2.2.5 0 .7'/%3E%3C/svg%3E");--dropzone-icons-upload:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='%23fff'%3E%3Cpath d='M10 11h1v1h-1zm2 0h1v1h-1zm-.73-5H4.73a.25.25 0 0 1-.188-.414l3.27-3.743a.244.244 0 0 1 .377 0l3.27 3.743A.25.25 0 0 1 11.27 6'/%3E%3Cpath d='M14.5 9H10v1h4v3H2v-3h4V9H1.5a.5.5 0 0 0-.5.5v4a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 0-.5-.5'/%3E%3Cpath d='M7 6h2v4H7z'/%3E%3C/g%3E%3C/svg%3E");--dropdown-close-border-radius:3px;--dropdown-close-color:var(--typo3-state-default-color);--dropdown-close-bg:var(--typo3-state-default-bg);--dropdown-close-border-color:var(--typo3-state-default-border-color);--dropdown-close-hover-color:var(--typo3-state-default-hover-color);--dropdown-close-hover-bg:var(--typo3-state-default-hover-bg);--dropdown-close-hover-border-color:var(--typo3-state-default-hover-border-color);--dropdown-close-border-width:1px;--dropzone-close-width:2rem;--dropzone-close-height:2rem;--dropzone-close-icon-size:1rem;--dropzone-close-icon-image:var(--dropzone-icons-close);--dropzone-icon-size:1rem;--dropzone-icon-height:2.5rem;--dropzone-icon-width:2.5rem;--dropzone-icon-radius:50%;--dropzone-icon-color:var(--dropzone-color);--dropzone-icon-bg:var(--dropzone-border-color);--dropzone-icon-success-color:var(--typo3-state-success-color);--dropzone-icon-success-bg:var(--typo3-state-success-bg);--dropzone-icon-info-color:var(--typo3-state-info-color);--dropzone-icon-info-bg:var(--typo3-state-info-bg);--dropzone-icon-icon:var(--dropzone-icons-upload);background-color:color-mix(in srgb,var(--dropzone-bg),transparent 65%);border-radius:var(--typo3-component-border-radius);cursor:pointer;padding:var(--dropzone-padding-y) var(--dropzone-padding-x);position:relative;-webkit-padding-end:calc(var(--dropzone-close-width) + var(--dropzone-padding-x) + var(--dropzone-spacing));border:var(--dropzone-border-width) dashed var(--dropzone-border-color);color:var(--dropzone-color);outline-offset:0;padding-inline-end:calc(var(--dropzone-close-width) + var(--dropzone-padding-x) + var(--dropzone-spacing));transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out} +@media (prefers-reduced-motion:reduce){ +.dropzone{transition:none}} +.dropzone:hover{--dropzone-color:var(--dropzone-hover-color);--dropzone-bg:var(--dropzone-hover-bg);--dropzone-border-color:var(--dropzone-hover-border-color)} +.dropzone:has(.dropzone-hint:focus){--dropzone-color:var(--dropzone-focus-color);--dropzone-bg:var(--dropzone-focus-bg);--dropzone-border-color:var(--dropzone-focus-border-color)} +.dropzone:has(.dropzone-hint:focus-visible){border-style:solid;outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--dropzone-border-color),transparent 25%)} +.dropzone-mask{inset:0;position:absolute} +.dropzone-close{align-items:center;background-color:var(--dropdown-close-bg);border:var(--dropdown-close-border-width) solid var(--dropdown-close-border-color);border-radius:var(--dropdown-close-border-radius);color:var(--dropdown-close-color);display:flex;height:var(--dropzone-close-height);inset-inline-end:calc(var(--dropzone-padding-x)/2);justify-content:center;outline:0;position:absolute;top:calc(var(--dropzone-padding-y)/2);width:var(--dropzone-close-width)} +.dropzone-close:focus,.dropzone-close:hover{--dropdown-close-color:var(--dropdown-close-hover-color);--dropdown-close-bg:var(--dropdown-close-hover-bg);--dropdown-close-border-color:var(--dropdown-close-hover-border-color)} +.dropzone-close:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--dropdown-close-border-color),transparent 25%)} +.dropzone-close:before{background-color:var(--dropdown-close-color);background-size:contain;content:"";display:block;height:var(--dropzone-close-icon-size);-webkit-mask:var(--dropzone-close-icon-image);mask:var(--dropzone-close-icon-image);width:var(--dropzone-close-icon-size)} +.dropzone-hint{align-items:center;background:transparent;border:0;display:flex;gap:var(--dropzone-padding-x);outline:0;padding:0;text-align:start} +.dropzone-hint-body :first-child{margin-top:0} +.dropzone-hint-body :last-child{margin-bottom:0} +.dropzone-hint-icon{align-items:center;background-color:var(--dropzone-icon-bg);border-radius:var(--dropzone-icon-radius);display:flex;height:var(--dropzone-icon-height);justify-content:center;text-rendering:auto;width:var(--dropzone-icon-width);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transform:translate(0)} +.dropzone-hint-icon:before{background-color:var(--dropzone-icon-color);background-size:contain;content:"";display:block;height:var(--dropzone-icon-size);-webkit-mask:var(--dropzone-icon-icon);mask:var(--dropzone-icon-icon);width:var(--dropzone-icon-size)} +.dropzone-hint-title{font-size:var(--dropzone-font-size);font-weight:700;margin-bottom:.25em} +.drop-status-ok{--dropzone-icon-color:var(--dropzone-icon-success-color);--dropzone-icon-bg:var(--dropzone-icon-success-bg)} +.drop-in-progress{--dropzone-icon-color:var(--dropzone-icon-info-color);--dropzone-icon-bg:var(--dropzone-icon-info-bg);--dropzone-icon-icon:var(--dropzone-icons-arrow-down)} +.filelist-main .dropzone{--dropzone-bg:#000;border-width:2px;height:calc(100% - var(--module-docheader-height));left:0;margin:0;padding:0;position:absolute;top:var(--module-docheader-height);width:100%;z-index:10} +.filelist-main .dropzone .dropzone-hint{background:var(--typo3-component-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:var(--typo3-component-border-radius);box-shadow:var(--typo3-component-box-shadow-tooltip);color:var(--typo3-component-color);left:50%;padding:var(--dropzone-padding-y) var(--dropzone-padding-x);position:absolute;top:50%;transform:translate(-50%,-50%)} +.grideditor{--grideditor-grid-spacing:1rem;--grideditor-cell-spacing:1rem;--grideditor-cell-color:var(--typo3-component-color);--grideditor-cell-bg:var(--typo3-component-bg);--grideditor-cell-border-radius:.75em;--grideditor-cell-border-color:var(--typo3-component-border-color);--grideditor-cell-shadow:var(--typo3-component-box-shadow-strong);--grideditor-row-height:180px;--grideditor-action-size:32px;--grideditor-action-spacing:2px;--grideditor-action-color:var(--typo3-state-default-color);--grideditor-action-bg:var(--typo3-state-default-bg);--grideditor-action-border-color:var(--typo3-state-default-border-color);--grideditor-action-border-radius:var(--typo3-input-border-radius);--grideditor-action-hover-color:var(--typo3-state-default-hover-color);--grideditor-action-hover-bg:var(--typo3-state-default-hover-bg);--grideditor-action-hover-border-color:var(--typo3-state-default-hover-border-color);--grideditor-action-focus-color:var(--typo3-state-default-focus-color);--grideditor-action-focus-bg:var(--typo3-state-default-focus-bg);--grideditor-action-focus-border-color:var(--typo3-state-default-focus-border-color);display:grid;gap:var(--grideditor-grid-spacing);grid-template:"grideditor-control-top ." "grideditor-editor grideditor-control-right" "grideditor-control-bottom ." "grideditor-preview grideditor-preview" auto/minmax(auto,1fr)} +.grideditor-control{align-items:center;display:flex;justify-content:center} +.grideditor-control-top{grid-area:grideditor-control-top} +.grideditor-control-right{grid-area:grideditor-control-right} +.grideditor-control-bottom{grid-area:grideditor-control-bottom} +.grideditor-editor{grid-area:grideditor-editor} +.grideditor-editor-grid{display:grid;gap:1em;grid-auto-columns:1fr;grid-auto-rows:var(--grideditor-row-height);width:100%} +.grideditor-preview{grid-area:grideditor-preview} +.grideditor-cell{--grideditor-cell-col-start:var(--grideditor-cell-col,1);--grideditor-cell-col-end:calc(var(--grideditor-cell-col, 1) + var(--grideditor-cell-colspan, 1));--grideditor-cell-row-start:var(--grideditor-cell-row,1);--grideditor-cell-row-end:calc(var(--grideditor-cell-row, 1) + var(--grideditor-cell-rowspan, 1));background-color:var(--grideditor-cell-bg);border:1px solid var(--grideditor-cell-border-color);border-radius:var(--grideditor-cell-border-radius);box-shadow:var(--grideditor-cell-shadow);color:var(--grideditor-cell-color);display:flex;flex-direction:column;grid-column:var(--grideditor-cell-col-start) /var(--grideditor-cell-col-end);grid-row:var(--grideditor-cell-row-start) /var(--grideditor-cell-row-end);height:100%;min-height:100px;width:100%} +.grideditor-cell>*{width:100%} +.grideditor-cell-actions{flex-grow:1;min-height:calc(var(--grideditor-cell-spacing) + (var(--grideditor-action-size) + var(--grideditor-action-spacing))*3);min-width:calc(var(--grideditor-cell-spacing) + (var(--grideditor-action-size) + var(--grideditor-action-spacing))*3);position:relative} +.grideditor-cell-info{background-color:color-mix(in srgb,var(--grideditor-cell-bg),currentColor 5%);border-end-end-radius:var(--grideditor-cell-border-radius);border-end-start-radius:var(--grideditor-cell-border-radius);font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:.75rem;line-height:1.2em;padding:calc(var(--grideditor-cell-spacing)/2) var(--grideditor-cell-spacing)} +.grideditor-action{align-items:center;background-color:var(--grideditor-action-bg);border:1px solid var(--grideditor-action-border-color);border-radius:var(--grideditor-action-border-radius);color:var(--grideditor-action-color);display:flex;height:var(--grideditor-action-size);inset-inline-start:50%;justify-content:center;position:absolute;top:50%;transform:translate(calc(-50%*var(--typo3-position-modifier)),-50%);width:var(--grideditor-action-size)} +.grideditor-action:hover{background-color:var(--grideditor-action-hover-bg);border-color:var(--grideditor-action-hover-border-color);color:var(--grideditor-action-hover-color);outline:none} +.grideditor-action:focus{background-color:var(--grideditor-action-focus-bg);border-color:var(--grideditor-action-focus-border-color);color:var(--grideditor-action-focus-color);z-index:1} +.grideditor-action:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--grideditor-action-focus-border-color),transparent 25%);outline-offset:0} +.grideditor-action-expand-down{top:calc(50% + var(--grideditor-action-size) + var(--grideditor-action-spacing))} +.grideditor-action-expand-right{inset-inline-start:calc(50% + var(--grideditor-action-size) + var(--grideditor-action-spacing))} +.grideditor-action-shrink-up{top:calc(50% - var(--grideditor-action-size) - var(--grideditor-action-spacing))} +.grideditor-action-shrink-left{inset-inline-start:calc(50% - var(--grideditor-action-size) - var(--grideditor-action-spacing))} +.pagelayout{--pagelayout-grid-spacing:1rem;--pagelayout-cell-spacing:1rem;--pagelayout-cell-color:var(--typo3-component-color);--pagelayout-cell-bg:var(--typo3-component-bg);--pagelayout-cell-border-radius:.75em;--pagelayout-cell-border-color:var(--typo3-component-border-color);--pagelayout-cell-shadow:var(--typo3-component-box-shadow-strong);display:grid;gap:var(--pagelayout-grid-spacing);grid-auto-columns:1fr;width:100%} +.pagelayout-cell{--pagelayout-cell-col-start:var(--pagelayout-cell-col,1);--pagelayout-cell-col-end:calc(var(--pagelayout-cell-col, 1) + var(--pagelayout-cell-colspan, 1));--pagelayout-cell-row-start:var(--pagelayout-cell-row,1);--pagelayout-cell-row-end:calc(var(--pagelayout-cell-row, 1) + var(--pagelayout-cell-rowspan, 1));display:grid;grid-column:var(--pagelayout-cell-col-start) /var(--pagelayout-cell-col-end);grid-row:var(--pagelayout-cell-row-start) /var(--pagelayout-cell-row-end);grid-template-rows:auto 1fr;height:100%;width:100%} +.pagelayout-cell-content{background-color:var(--pagelayout-cell-bg);border:1px solid var(--pagelayout-cell-border-color);border-radius:var(--pagelayout-cell-border-radius);box-shadow:var(--pagelayout-cell-shadow);color:var(--pagelayout-cell-color);padding:var(--pagelayout-cell-spacing)} +.pagelayout-cell-content>:last-child{margin-bottom:0} +:root{--typo3-debugger-color:var(--typo3-component-color);--typo3-debugger-bg:var(--typo3-component-bg);--typo3-debugger-bg-variant:color-mix(in srgb,var(--typo3-debugger-bg),var(--typo3-debugger-color) 3%);--typo3-debugger-outline:#101010;--typo3-debugger-border-color:var(--typo3-component-border-color);--typo3-debugger-border-radius:var(--typo3-component-border-radius);--typo3-debugger-box-shadow:var(--typo3-component-box-shadow);--typo3-debugger-top-bg:var(--typo3-surface-container-default);--typo3-debugger-top-color:var(--typo3-surface-container-default-text);--typo3-debugger-color-variant:var(--typo3-text-color-variant);--typo3-debugger-unregistered-bg:light-dark(var(--token-color-warning-60),var(--token-color-blue-60));--typo3-debugger-scope-bg:light-dark(var(--token-color-teal-60),var(--token-color-teal-80));--typo3-debugger-ptype-bg:light-dark(var(--token-color-green-60),var(--token-color-green-70));--typo3-debugger-visibility-bg:light-dark(var(--token-color-magenta-55),var(--token-color-magenta-75));--typo3-debugger-unitialized-bg:light-dark(var(--token-color-orange-60),var(--token-color-orange-75));--typo3-debugger-dirty-bg:light-dark(var(--token-color-yellow-70),var(--token-color-yellow-80));--typo3-debugger-filtered-bg:light-dark(var(--token-color-yellow-70),var(--token-color-yellow-80));--typo3-debugger-string-color:light-dark(var(--token-color-orange-60),var(--token-color-orange-40));--typo3-debugger-type-color:light-dark(var(--token-color-blue-60),var(--token-color-blue-25));--typo3-debugger-closure-color:light-dark(var(--token-color-yellow-70),var(--token-color-yellow-50));--typo3-debugger-property-color:var(--typo3-component-color)} +typo3-backend-color-scheme-switch{display:block;-webkit-margin-start:var(--typo3-list-item-padding-x);margin-inline-start:var(--typo3-list-item-padding-x);-webkit-margin-end:var(--typo3-list-item-padding-x);margin-inline-end:var(--typo3-list-item-padding-x);padding-top:var(--typo3-list-item-padding-y)} +typo3-backend-color-scheme-switch .btn-group,typo3-backend-color-scheme-switch .dropdown-list{padding-bottom:var(--typo3-list-item-padding-y);width:100%} +typo3-backend-color-scheme-switch .btn-group .btn{-webkit-padding-start:.75rem;padding-inline-start:.75rem;-webkit-padding-end:.75rem;padding-inline-end:.75rem} +typo3-backend-color-scheme-switch .btn-group .btn:first-child{flex:1 1 auto;gap:calc(var(--typo3-spacing)*2/3);justify-content:flex-start} +typo3-backend-color-scheme-switch .btn-group .btn:first-child+.btn{flex:0 0 auto} +typo3-backend-color-scheme-switch .dropdown-item{-webkit-padding-start:calc(.75rem + 1px);padding-inline-start:calc(.75rem + 1px);-webkit-padding-end:calc(.75rem + 1px);padding-inline-end:calc(.75rem + 1px)} +typo3-backend-color-scheme-switch .dropdown-item-column-title-info{color:var(--typo3-text-color-variant);font-style:italic;-webkit-margin-start:.25rem;margin-inline-start:.25rem} +typo3-backend-color-scheme-switch .dropdown-item-columns{align-items:center;gap:calc(var(--typo3-spacing)*2/3);justify-content:flex-start} +typo3-backend-color-scheme-switch .btn[disabled]{pointer-events:unset} + +/*! + * CSS copied from jQuery UI Resizable 1.11.4 (http://jqueryui.com) + * used in element `<typo3-backend-draggable-resizable>` + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ +.ui-resizable{position:relative} +.ui-resizable-handle{display:block;font-size:.1px;position:absolute;touch-action:none} +.ui-resizable-autohide .ui-resizable-handle,.ui-resizable-disabled .ui-resizable-handle{display:none} +.ui-resizable-n{cursor:n-resize;height:7px;left:0;top:-5px;width:100%} +.ui-resizable-s{bottom:-5px;cursor:s-resize;height:7px;left:0;width:100%} +.ui-resizable-e{cursor:e-resize;height:100%;right:-5px;top:0;width:7px} +.ui-resizable-w{cursor:w-resize;height:100%;left:-5px;top:0;width:7px} +.ui-resizable-se{bottom:1px;cursor:se-resize;height:12px;right:1px;width:12px} +.ui-resizable-sw{bottom:-5px;cursor:sw-resize;height:9px;left:-5px;width:9px} +.ui-resizable-nw{cursor:nw-resize;height:9px;left:-5px;top:-5px;width:9px} +.ui-resizable-ne{cursor:ne-resize;height:9px;right:-5px;top:-5px;width:9px} +typo3-backend-draggable-resizable{display:inline-block;position:absolute;transition:none} +typo3-backend-draggable-resizable[reverting]{transition-duration:.25s;transition-property:left,top,width,height} +.cropper typo3-backend-draggable-resizable .cropper-focus-area{height:100%;width:100%} +typo3-backend-draggable-resizable .ui-resizable-handle{z-index:90} +typo3-backend-editable-page-title{display:block;text-overflow:ellipsis;white-space:nowrap;--input-border-color:var(--typo3-state-default-border-color);--input-hover-border-color:var(--typo3-state-default-hover-border-color);--input-focus-border-color:var(--typo3-state-default-focus-border-color);--button-hover-color:var(--typo3-state-default-focus-color);--button-hover-bg:var(--typo3-state-default-hover-bg);--button-hover-border-color:var(--typo3-state-default-focus-border-color);--button-focus-color:var(--typo3-state-default-focus-color);--button-focus-bg:var(--typo3-state-default-focus-bg);--button-focus-border-color:var(--typo3-state-default-focus-border-color)} +typo3-backend-formengine-char-counter{display:flex;inset-inline-start:0;position:absolute;top:100%;z-index:5} +typo3-backend-formengine-char-counter .form-hint{--typo3-formhint-box-shadow:var(--typo3-component-box-shadow-tooltip);margin-top:.5rem} +typo3-backend-icon{height:var(--icon-size,1em);width:var(--icon-size,1em)} +typo3-backend-icon[size=small]{height:var(--icon-size-small,16px);width:var(--icon-size-small,16px)} +typo3-backend-icon[size=default]{height:var(--icon-size,1em);width:var(--icon-size,1em)} +typo3-backend-icon[size=medium]{height:var(--icon-size-medium,32px);width:var(--icon-size-medium,32px)} +typo3-backend-icon[size=large]{height:var(--icon-size-large,48px);width:var(--icon-size-large,48px)} +typo3-backend-icon[size=mega]{height:var(--icon-size-mega,64px);width:var(--icon-size-mega,64px)} +typo3-backend-security-csp-reports .infolist-container{container-type:inline-size} +typo3-backend-security-csp-reports .infolist{display:flex;flex-direction:column;gap:var(--typo3-spacing)} +typo3-backend-security-csp-reports .infolist-info{display:none} +@container (max-width: 899px){ +typo3-backend-security-csp-reports .infolist-info-showrecord{background:rgba(0,0,0,.5);display:block;height:100%;left:0;padding:1.5rem;position:absolute;top:0;width:100%}} +@container (min-width: 900px){ +typo3-backend-security-csp-reports .infolist{display:grid;grid-template:"header header" "content info";grid-template-columns:auto 400px} +typo3-backend-security-csp-reports .infolist-header{grid-area:header} +typo3-backend-security-csp-reports .infolist-content{grid-area:content} +typo3-backend-security-csp-reports .infolist-info{display:block;grid-area:info} +typo3-backend-security-csp-reports .infolist-info-norecord,typo3-backend-security-csp-reports .infolist-info-record{position:-webkit-sticky;position:sticky;top:calc(var(--module-docheader-height) + var(--typo3-spacing))}} +typo3-backend-security-sudo-mode,typo3-backend-security-sudo-mode #sudo-mode-verification{display:block} +typo3-backend-table-wizard{display:inline-block} +typo3-formengine-container-inline>:has(+input[type=hidden]:last-child),typo3-formengine-container-inline>:last-child{margin-bottom:0} +typo3-formengine-element-datetime{display:block;anchor-name:--typo3-formengine-element-datetime;anchor-scope:--typo3-formengine-element-datetime} +typo3-formengine-element-datetime .flatpickr-calendar:not([popover]:popover-open){display:none} +typo3-formengine-element-datetime .flatpickr-calendar[popover]:popover-open{display:block;inset:auto;margin:var(--typo3-dropdown-anchor-offset) 0;min-width:var(--flatpickr-width);position:fixed;width:auto!important;position-anchor:--typo3-formengine-element-datetime;position-area:block-end span-inline-end;position-try-fallbacks:flip-block,flip-inline,flip-block flip-inline;overflow:unset} +typo3-formengine-element-datetime .flatpickr-calendar[popover]:popover-open .flatpickr-weeks{flex-direction:column} +typo3-formengine-element-datetime .flatpickr-calendar[popover]:popover-open .flatpickr-days{width:auto!important} +typo3-formengine-element-link .form-control-explanation:not([hidden])~.form-control-clearable-wrapper{display:none} +#alert-container{--typo3-alert-container-color:var(--typo3-text-color-base);--typo3-alert-container-bg:var(--typo3-surface-container-high);--typo3-alert-contanier-border-radius:var(--typo3-component-border-radius);--typo3-alert-container-padding:.5rem;--typo3-alert-container-border-color:color-mix(in srgb,var(--typo3-alert-container-bg),var(--typo3-alert-container-color) var(--typo3-border-mix));--typo3-alert-container-shadow:var(--typo3-component-box-shadow-dialog);--typo3-alert-container-offset:calc(var(--typo3-spacing)*1.5);--typo3-alert-container-bar-shadow:var(--typo3-component-box-shadow-strong);background-color:var(--typo3-alert-container-bg);border:1px solid var(--typo3-alert-container-border-color);border-radius:var(--typo3-alert-contanier-border-radius);bottom:var(--typo3-alert-container-offset);box-shadow:var(--typo3-alert-container-shadow);color:var(--typo3-alert-container-color);max-width:calc(100% - var(--typo3-alert-container-offset)*2);overflow:hidden;position:fixed;width:400px;z-index:10000} +@media (max-width:767px){ +#alert-container{inset-inline-start:50%;transform:translateX(-50%)}} +@media (min-width:768px){ +#alert-container{inset-inline-end:calc(var(--typo3-spacing)*1.5)}} +#alert-container .alert-list{display:grid;gap:var(--typo3-alert-container-padding);grid-template-columns:1fr;max-height:50dvh;outline-offset:-2px;overflow-y:auto;padding:var(--typo3-alert-container-padding)} +#alert-container .alert-list:focus-visible{outline:2px solid color-mix(in srgb,var(--typo3-alert-container-border-color),currentColor 25%)} +#alert-container typo3-notification-clear-all{box-shadow:var(--typo3-alert-container-bar-shadow);display:block;padding:var(--typo3-alert-container-padding);position:relative;text-align:right;z-index:1} +typo3-notification-message{display:block} +typo3-notification-message .alert{--typo3-alert-margin-bottom:0} +.ck{--typo3-rte-base-fg:var(--typo3-surface-container-high);--typo3-rte-base-bg:var(--typo3-surface-container-lowest);--typo3-rte-base-text:var(--typo3-component-color);--typo3-rte-base-border-color:var(--typo3-component-border-color);--typo3-rte-accessibility-help-dialog-border-color:light-dark(var(--token-color-neutral-10),var(--token-color-neutral-85));--typo3-rte-accessibility-help-dialog-code-background-color:light-dark(var(--token-color-neutral-7),var(--token-color-neutral-90));--typo3-rte-accessibility-help-dialog-kbd-shadow-color:light-dark(var(--token-color-neutral-5),var(--token-color-neutral-95));--typo3-rte-default-button-hover-bg:var(--typo3-component-hover-bg);--typo3-rte-default-button-focus-bg:var(--typo3-component-focus-bg);--typo3-rte-default-button-active-bg:var(--typo3-component-active-bg);--typo3-rte-split-button-hover-bg:color-mix(in srgb,var(--typo3-rte-default-button-hover-bg),var(--typo3-rte-base-bg));--typo3-rte-split-button-hover-border-color:var(--typo3-component-hover-border-color);--typo3-rte-on-button-color:var(--typo3-state-default-color);--typo3-rte-on-button-bg:var(--typo3-state-default-bg);--typo3-rte-on-button-hover-bg:var(--typo3-state-default-hover-bg);--typo3-rte-on-button-focus-bg:var(--typo3-state-default-focus-bg);--typo3-rte-on-button-active-bg:var(--typo3-state-default-focus-bg);--typo3-rte-on-button-disabled-bg:var(--typo3-state-default-disabled-bg);--typo3-rte-fullscreen-padding-x:var(--ck-spacing-small);--typo3-rte-fullscreen-padding-y:1.5rem;--typo3-rte-powered-by-text-color:var(--typo3-rte-base-text);--typo3-rte-powered-by-background:transparent;--typo3-rte-powered-by-border-color:transparent;--ck-accessibility-help-dialog-border-color:var(--typo3-rte-accessibility-help-dialog-border-color);--ck-accessibility-help-dialog-code-background-color:var(--typo3-rte-accessibility-help-dialog-code-background-color);--ck-accessibility-help-dialog-kbd-shadow-color:var(--typo3-rte-accessibility-help-dialog-kbd-shadow-color);--ck-color-base-foreground:var(--typo3-rte-base-fg);--ck-color-base-background:var(--typo3-rte-base-bg);--ck-color-base-border:var(--typo3-rte-base-border-color);--ck-color-toolbar-background:var(--typo3-rte-base-bg);--ck-color-toolbar-border:var(--typo3-rte-base-border-color);--ck-color-toolbar-text:var(--typo3-rte-base-text);--ck-color-button-default-hover-background:var(--typo3-rte-default-button-hover-bg);--ck-color-button-default-focus-background:var(--typo3-rte-default-button-focus-bg);--ck-color-button-default-active-background:var(--typo3-rte-default-button-focus-bg);--ck-color-split-button-hover-background:var(--typo3-rte-split-button-hover-bg);--ck-color-split-button-hover-border:var(--typo3-rte-split-button-hover-border-color);--ck-color-button-on-color:var(--typo3-rte-on-button-color);--ck-color-button-on-background:var(--typo3-rte-on-button-bg);--ck-color-button-on-disabled-background:var(--typo3-rte-on-button-disabled-bg);--ck-color-button-on-hover-background:var(--typo3-rte-on-button-hover-bg);--ck-color-button-on-focus-background:var(--typo3-rte-on-button-focus-bg);--ck-color-button-on-active-background:var(--typo3-rte-on-button-active-bg);--ck-color-dropdown-panel-background:var(--typo3-rte-base-bg);--ck-color-dropdown-panel-border:var(--typo3-rte-base-border-color);--ck-color-panel-background:var(--typo3-rte-base-bg);--ck-color-panel-border:var(--typo3-rte-base-border-color);--ck-color-dialog-background:var(--typo3-rte-base-bg);--ck-color-dialog-form-header-border:var(--typo3-rte-base-border-color);--ck-color-labeled-field-label-background:var(--typo3-rte-base-bg);--ck-color-list-background:var(--typo3-rte-base-bg);--ck-color-list-button-hover-background:var(--typo3-rte-default-button-hover-bg);--ck-color-list-button-focus-background:var(--typo3-rte-default-button-focus-bg);--ck-color-list-button-active-background:var(--typo3-rte-default-button-active-bg);--ck-color-input-background:var(--typo3-rte-base-bg);--ck-color-input-border:var(--typo3-rte-base-border-color);--ck-color-text:var(--typo3-rte-base-text);--ck-focus-outer-shadow:var(--ck-focus-outer-shadow-geometry) color-mix(in srgb,var(--typo3-input-focus-border-color),transparent 25%);--ck-powered-by-text-color:var(--typo3-rte-powered-by-text-color);--ck-powered-by-background:var(--typo3-rte-powered-by-background);--ck-powered-by-border-color:var(--typo3-rte-powered-by-border-color);--ck-style-panel-button-label-background:var(--typo3-rte-on-button-bg);--ck-style-panel-button-hover-label-background:var(--typo3-rte-default-button-hover-bg);--ck-style-panel-button-hover-border-color:color-mix(in srgb,var(--ck-style-panel-button-hover-label-background),currentColor 25%)} +.ck.ck-toolbar>.ck-toolbar__items>.ck-link-toolbar__preview{max-width:80vw;overflow:hidden;padding:0 var(--ck-spacing-medium);text-overflow:ellipsis;white-space:nowrap} +.ck.ck-fullscreen__main-wrapper .ck-fullscreen__menu-bar .ck-menu-bar,.ck.ck-fullscreen__main-wrapper .ck-fullscreen__toolbar .ck-toolbar{padding:var(--typo3-rte-fullscreen-padding-x) var(--typo3-rte-fullscreen-padding-y)} +.ck.ck-fullscreen__main-wrapper .ck-fullscreen__editable .ck.ck-editor__editable:not(.ck-editor__nested-editable){background:var(--typo3-rte-base-bg);box-shadow:var(--typo3-component-box-shadow)} +.ck.ck-powered-by .ck-icon path{fill:var(--ck-powered-by-text-color)} +typo3-rte-ckeditor-ckeditor5>textarea[slot=textarea]{display:none!important} +typo3-rte-ckeditor-ckeditor5 .ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-disabled{display:none} +.t3js-formengine-placeholder-placeholder typo3-rte-ckeditor-ckeditor5{opacity:.5} +.t3js-formengine-placeholder-placeholder typo3-rte-ckeditor-ckeditor5 .ck.ck-editor__top{display:none} +typo3-backend-color-picker .form-control{-webkit-padding-start:calc(1.25rem + var(--typo3-input-sm-padding-x)*2);padding-inline-start:calc(1.25rem + var(--typo3-input-sm-padding-x)*2)} +typo3-backend-combobox{display:block;width:100%;--typo3-form-combobox-padding-x:var(--typo3-input-padding-x);--typo3-form-combobox-icon-size:16px;--typo3-form-combobox-listbox-zindex:var(--typo3-zindex-dropdown);--typo3-form-combobox-listbox-min-width:10rem;--typo3-form-combobox-listbox-padding:2px;--typo3-form-combobox-listbox-font-size:var(--typo3-component-font-size);--typo3-form-combobox-listbox-line-height:var(--typo3-component-line-height);--typo3-form-combobox-listbox-color:var(--typo3-component-color);--typo3-form-combobox-listbox-bg:var(--typo3-component-bg);--typo3-form-combobox-listbox-border-color:var(--typo3-component-border-color);--typo3-form-combobox-listbox-border-radius:var(--typo3-component-border-radius);--typo3-form-combobox-listbox-border-width:var(--typo3-component-border-width);--typo3-form-combobox-listbox-box-shadow:var(--typo3-component-box-shadow);--typo3-form-combobox-option-indicator:var(--typo3-text-color-primary);--typo3-form-combobox-option-color:var(--typo3-component-color);--typo3-form-combobox-option-hover-color:var(--typo3-list-item-hover-color);--typo3-form-combobox-option-hover-bg:var(--typo3-list-item-hover-bg);--typo3-form-combobox-option-focus-color:var(--typo3-list-item-focus-color);--typo3-form-combobox-option-focus-bg:var(--typo3-list-item-focus-bg);--typo3-form-combobox-option-disabled-color:var(--typo3-list-item-disabled-color);--typo3-form-combobox-option-disabled-bg:transparent;--typo3-form-combobox-option-padding-x:var(--typo3-list-item-padding-x);--typo3-form-combobox-option-padding-y:var(--typo3-list-item-padding-y)} +typo3-backend-combobox typo3-backend-combobox-choice:not([slot=choices]){display:none} +typo3-backend-combobox[data-has-value]{--typo3-form-combobox-controls-width:calc(var(--typo3-form-combobox-icon-size)*2 + var(--typo3-form-combobox-padding-x)*2 + 0.5rem)} +typo3-backend-combobox:not([data-has-value]){--typo3-form-combobox-controls-width:calc(var(--typo3-form-combobox-icon-size) + var(--typo3-form-combobox-padding-x)*2)} +typo3-backend-combobox .form-control{-webkit-padding-end:var(--typo3-form-combobox-controls-width,calc(var(--typo3-form-combobox-icon-size) + var(--typo3-form-combobox-padding-x)*2));padding-inline-end:var(--typo3-form-combobox-controls-width,calc(var(--typo3-form-combobox-icon-size) + var(--typo3-form-combobox-padding-x)*2))} +typo3-backend-combobox input[type=number]{-moz-appearance:textfield;-webkit-appearance:textfield;appearance:textfield} +typo3-backend-combobox input[type=number]::-webkit-inner-spin-button,typo3-backend-combobox input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0} +typo3-backend-combobox::part(indicator){opacity:.6} +typo3-backend-combobox::part(listbox){background-color:var(--typo3-form-combobox-listbox-bg);border:var(--typo3-form-combobox-listbox-border-width) solid var(--typo3-form-combobox-listbox-border-color);border-radius:var(--typo3-form-combobox-listbox-border-radius);box-shadow:var(--typo3-form-combobox-listbox-box-shadow);color:var(--typo3-form-combobox-listbox-color);font-size:var(--typo3-form-combobox-listbox-font-size);line-height:var(--typo3-form-combobox-listbox-line-height);margin:0;max-height:200px;min-width:var(--typo3-form-combobox-listbox-min-width);overflow-y:auto;padding:var(--typo3-form-combobox-listbox-padding);z-index:var(--typo3-form-combobox-listbox-zindex)} +typo3-backend-combobox-choice{background-color:transparent;border-radius:calc(var(--typo3-form-combobox-listbox-border-radius) - var(--typo3-form-combobox-listbox-padding));color:var(--typo3-form-combobox-option-color);cursor:pointer;padding:var(--typo3-form-combobox-option-padding-y) var(--typo3-form-combobox-option-padding-x)} +typo3-backend-combobox-choice+typo3-backend-combobox-choice{margin-top:1px} +typo3-backend-combobox-choice:hover{background-color:var(--typo3-form-combobox-option-hover-bg);color:var(--typo3-form-combobox-option-hover-color)} +typo3-backend-combobox-choice[disabled]{background-color:var(--typo3-form-combobox-option-disabled-bg);color:var(--typo3-form-combobox-option-disabled-color);cursor:not-allowed;pointer-events:none} +typo3-backend-combobox-choice[aria-selected=true]{background-color:var(--typo3-form-combobox-option-focus-bg);color:var(--typo3-form-combobox-option-focus-color)} +typo3-backend-combobox-choice::part(icon){-webkit-margin-end:.5rem;flex-shrink:0;margin-inline-end:.5rem} +typo3-backend-combobox-choice::part(content){flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +typo3-backend-combobox-choice::part(indicator){color:var(--typo3-form-combobox-option-indicator);-webkit-margin-end:.25rem;margin-inline-end:.25rem;min-width:var(--typo3-form-combobox-icon-size)} +typo3-backend-content-navigation{--content-navigation-divider-color:color-mix(in srgb,var(--typo3-surface-container-lowest),var(--typo3-text-color-base) 10%);--content-navigation-divider-color-active:var(--typo3-state-primary-border-color);--content-navigation-flyout-box-shadow:var(--typo3-component-box-shadow-flyout)} +typo3-backend-content-navigation [slot=content] typo3-backend-progress-bar{--progress-border-radius:0;left:0;position:absolute;right:0;top:0;z-index:10} +typo3-qrcode{--typo3-qrcode-url-success-bg:var(--token-color-success-15);--typo3-qrcode-url-success-box-shadow:var(--token-color-success-30);--typo3-qrcode-url-error-bg:var(--token-color-danger-15);--typo3-qrcode-url-error-box-shadow:var(--token-color-danger-30);display:inline-block} +typo3-qrcode .preview{margin-bottom:var(--typo3-spacing);text-align:center} +typo3-qrcode .url-info-section.copy-success input{background:var(--typo3-qrcode-url-success-bg)} +typo3-qrcode .url-info-section.copy-success .btn,typo3-qrcode .url-info-section.copy-success input{box-shadow:0 0 0 .1rem var(--typo3-qrcode-url-success-box-shadow)} +typo3-qrcode .url-info-section.copy-error input{background:var(--typo3-qrcode-url-error-bg)} +typo3-qrcode .url-info-section.copy-error .btn,typo3-qrcode .url-info-section.copy-error input{box-shadow:0 0 0 .1rem var(--typo3-qrcode-url-error-box-shadow)} +typo3-backend-workspace-selector{--workspace-selector-min-width:42px;--workspace-selector-min-height:42px;--workspace-selector-padding:4px;--workspace-selector-icon-size:32px;--workspace-selector-icon-border-radius:calc(var(--workspace-selector-border-radius) - var(--workspace-selector-padding));--workspace-selector-bg:var(--typo3-scaffold-sidebar-bg);--workspace-selector-color:var(--typo3-scaffold-sidebar-color);--workspace-selector-transition-color:var(--typo3-transition-color);--workspace-selector-border-radius:var(--typo3-component-border-radius);--workspace-selector-border-color:color-mix(in srgb,var(--workspace-selector-bg),var(--workspace-selector-color) 20%);--workspace-selector-hover-color:var(--workspace-selector-color);--workspace-selector-hover-bg:color-mix(in srgb,var(--workspace-selector-bg),var(--workspace-selector-color) 10%);--workspace-selector-hover-border-color:color-mix(in srgb,var(--workspace-selector-bg),var(--workspace-selector-color) 30%);--workspace-selector-focus-color:var(--workspace-selector-color);--workspace-selector-focus-bg:color-mix(in srgb,var(--workspace-selector-bg),var(--workspace-selector-color) 15%);--workspace-selector-focus-border-color:color-mix(in srgb,var(--workspace-selector-bg),var(--workspace-selector-color) 35%);container-type:inline-size;display:block;min-height:var(--workspace-selector-min-width);min-width:var(--workspace-selector-min-width);position:relative} +typo3-backend-workspace-selector>.dropdown{anchor-scope:--workspace-selector} +typo3-backend-workspace-selector .workspace-selector{--typo3-icons-accent:currentColor;align-items:center;background-color:var(--workspace-selector-active-bg,var(--workspace-selector-bg));border:1px solid var(--workspace-selector-active-border-color,var(--workspace-selector-border-color));border-radius:var(--workspace-selector-border-radius);color:var(--workspace-selector-active-color,var(--workspace-selector-color));display:flex;justify-content:center;min-height:var(--workspace-selector-min-width);min-width:var(--workspace-selector-min-width);outline:none;overflow:hidden;padding:var(--workspace-selector-padding);pointer-events:none;text-align:start;transition:background-color .2s ease-in-out,color .2s ease-in-out,border-color .2s ease-in-out;width:100%;anchor-name:--workspace-selector} +typo3-backend-workspace-selector .workspace-selector:after{display:none} +@container (min-width: 60px){ +typo3-backend-workspace-selector .workspace-selector{justify-content:flex-start}} +typo3-backend-workspace-selector .workspace-selector:not([disabled]){cursor:pointer;pointer-events:auto} +typo3-backend-workspace-selector .workspace-selector:not([disabled]):focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) var(--workspace-selector-active-focus-border-color,var(--workspace-selector-focus-border-color))} +typo3-backend-workspace-selector .workspace-selector:not([disabled]):hover{background-color:var(--workspace-selector-active-hover-bg,var(--workspace-selector-hover-bg));border-color:var(--workspace-selector-active-hover-border-color,var(--workspace-selector-hover-border-color))} +typo3-backend-workspace-selector .workspace-selector-icon{align-items:center;border-radius:var(--workspace-selector-icon-border-radius);display:flex;flex-shrink:0;height:var(--workspace-selector-icon-size);justify-content:center;overflow:hidden;position:relative;width:var(--workspace-selector-icon-size)} +typo3-backend-workspace-selector .workspace-selector-name{flex-grow:1;-webkit-margin-start:.5rem;height:1px;margin-inline-start:.5rem;overflow:hidden;padding:0;position:absolute;text-overflow:ellipsis;width:1px;clip:rect(0,0,0,0);white-space:nowrap} +@container (min-width: 60px){ +typo3-backend-workspace-selector .workspace-selector-name{height:auto;position:static;width:auto}} +typo3-backend-workspace-selector .workspace-selector-indicator{align-items:center;display:none;height:16px;justify-content:center;width:16px;-webkit-margin-start:auto;flex-shrink:0;margin-inline-start:auto;margin:8px;transition:transform .2s ease-in-out} +@container (min-width: 60px){ +typo3-backend-workspace-selector .workspace-selector-indicator{display:flex}} +typo3-backend-workspace-selector .dropdown-menu{--typo3-dropdown-bg:var(--workspace-selector-bg);--typo3-dropdown-color:var(--workspace-selector-color);--typo3-dropdown-border-color:var(--workspace-selector-border-color);--typo3-dropdown-item-color:var(--workspace-selector-color);--typo3-dropdown-item-hover-color:var(--workspace-selector-hover-color);--typo3-dropdown-item-hover-bg:var(--workspace-selector-hover-bg);--typo3-dropdown-item-hover-border-color:var(--workspace-selector-hover-border-color);--typo3-dropdown-item-focus-color:var(--workspace-selector-focus-color);--typo3-dropdown-item-focus-bg:var(--workspace-selector-focus-bg);--typo3-dropdown-item-focus-border-color:var(--workspace-selector-focus-border-color);--typo3-dropdown-item-active-color:var(--workspace-selector-focus-color);--typo3-dropdown-item-active-bg:var(--workspace-selector-focus-bg);--typo3-dropdown-item-active-border-color:var(--workspace-selector-focus-border-color);position-anchor:--workspace-selector;max-width:400px;min-width:205px;width:anchor-size(width)} +typo3-backend-workspace-selector .dropdown-list>li:is(:first-child) .dropdown-item{border-end-end-radius:0;border-end-start-radius:0} +typo3-backend-workspace-selector .dropdown-list>li:is(:last-child) .dropdown-item{border-start-end-radius:0;border-start-start-radius:0} +typo3-backend-workspace-selector .dropdown-list>li:not(:first-child,:last-child) .dropdown-item{border-radius:0} +typo3-backend-workspace-selector .dropdown-item{-webkit-border-start:.5rem solid var(--workspace-selector-border-color);border-inline-start:.5rem solid var(--workspace-selector-border-color);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +typo3-backend-workspace-selector .dropdown-item.active{pointer-events:none} +typo3-backend-workspace-selector .workspace-selector-loading{align-items:center;display:flex;justify-content:center;min-height:36px} +.short-url-input-group{max-width:-webkit-fit-content;max-width:-moz-fit-content;max-width:fit-content} +.short-url-input-group typo3-backend-combobox{flex:0 0 320px} +.short-url-input-group typo3-backend-combobox input{border-bottom-right-radius:0;border-top-right-radius:0} +.short-url-input-group>input[type=text]{border-left:0;border-radius:0;flex:0 0 150px} +.short-url-input-group>input[type=text]:focus{z-index:2} +.short-url-input-group .form-wizards-items-aside,.short-url-input-group .form-wizards-items-top{flex:0 0 auto} +.short-url-input-group .form-wizards-items-aside .btn,.short-url-input-group .form-wizards-items-aside .btn-group,.short-url-input-group .form-wizards-items-top .btn,.short-url-input-group .form-wizards-items-top .btn-group{border-bottom-left-radius:0;border-top-left-radius:0} +.collapse-horizontal{height:auto;overflow:hidden;vertical-align:middle;width:0} +.collapse-horizontal.show{display:inline-block;width:auto} +.collapse-horizontal.collapsing{display:inline-block;transition-property:width,visibility;width:0} +.cropper .cropper-line{background-color:transparent} +.cropper .cropper-face{border:1px dashed var(--typo3-state-primary-bg)} +.cropper .cropper-dashed{border-color:var(--typo3-state-primary-bg)} +.cropper .cropper-point{background-color:var(--typo3-state-primary-bg)} +.cropper .cropper-point.point-nw{left:0;top:0} +.cropper .cropper-point.point-w{left:0} +.cropper .cropper-point.point-sw{bottom:0;left:0} +.cropper .cropper-point.point-ne{right:0;top:0} +.cropper .cropper-point.point-e{right:0} +.cropper .cropper-point.point-se{bottom:0;right:0} +.cropper .cropper-point.point-se:before{background-color:#fff} +.cropper .cropper-point.point-n{top:0} +.cropper .cropper-point.point-s{bottom:0} +.cropper .cropper-view-box{outline:1px dashed var(--typo3-state-primary-bg)} +.cropper .cropper-image-container{direction:ltr;display:block;max-height:calc(100dvh - 250px);max-width:1000px;width:100%} +@media (min-width:992px){ +.cropper .cropper-image-container{max-height:100%}} +.cropper .ratio-buttons{margin-bottom:10px} +.cropper .ratio-buttons .btn:not(.active) .icon{display:none} +.cropper .panel-group{--typo3-panel-border-radius:0;margin:-15px;position:relative} +.cropper .panel-group .panel{border:0;-webkit-border-start:3px solid var(--typo3-panel-border-color);border-inline-start:3px solid var(--typo3-panel-border-color);margin-bottom:0} +.cropper .panel-group .panel:has(.panel-heading):not(:has(.collapsed)){border-color:var(--typo3-state-primary-bg)} +.cropper .panel-group .panel+.panel{margin-top:1px} +.cropper .cropper-container.cropper-bg{overflow:visible} +.cropper .cropper-crop-box{overflow:hidden} +.cropper .cropper-crop-box:after{background-color:var(--typo3-state-primary-bg);color:var(--typo3-state-primary-color);content:"Cropped area";font-size:var(--typo3-font-size-small);inset-inline-start:0;overflow:hidden;padding:.5em .75em;pointer-events:none;position:absolute;text-overflow:ellipsis;top:0;white-space:nowrap} +.cropper .cropper-line.line-w{left:0} +.cropper .cropper-line.line-e{right:0} +.cropper .cropper-line.line-n{top:0} +.cropper .cropper-line.line-s{bottom:0} +.cropper .ui-resizable-handle.ui-resizable-e,.cropper .ui-resizable-handle.ui-resizable-n,.cropper .ui-resizable-handle.ui-resizable-s,.cropper .ui-resizable-handle.ui-resizable-w{border-color:transparent;transform:none} +.cropper .ui-resizable-handle.ui-resizable-e,.cropper .ui-resizable-handle.ui-resizable-w{width:6px} +.cropper .ui-resizable-handle.ui-resizable-n,.cropper .ui-resizable-handle.ui-resizable-s{height:6px} +.cropper .ui-resizable-handle.ui-resizable-e{right:0} +.cropper .ui-resizable-handle.ui-resizable-w{left:0} +.cropper .ui-resizable-handle.ui-resizable-n{top:0} +.cropper .ui-resizable-handle.ui-resizable-s{bottom:0} +.cropper .ui-resizable-handle.ui-resizable-ne,.cropper .ui-resizable-handle.ui-resizable-nw,.cropper .ui-resizable-handle.ui-resizable-se,.cropper .ui-resizable-handle.ui-resizable-sw{background-color:var(--typo3-state-primary-bg);height:6px;transform:none;width:6px} +.cropper .ui-resizable-handle.ui-resizable-nw{left:0;top:0} +.cropper .ui-resizable-handle.ui-resizable-ne{right:0;top:0} +.cropper .ui-resizable-handle.ui-resizable-se{bottom:0;right:0} +.cropper .ui-resizable-handle.ui-resizable-sw{bottom:0;left:0} +.cropper .cropper-focus-area{background-color:rgba(215,187,0,.5);cursor:move;height:200px;opacity:1;overflow:hidden;position:absolute;transition:background-color .3s;width:200px;z-index:999999} +.cropper .cropper-focus-area.has-nodrop,.cropper .cropper-focus-area.has-nodrop:hover{background-color:rgba(211,35,46,.6)!important;transition:background-color .3s} +.cropper .cropper-focus-area:focus,.cropper .cropper-focus-area:hover{background-color:rgba(215,187,0,.7)} +.cropper .cropper-focus-area:after{background-color:hsla(0,0%,100%,.95);color:#000;content:"Focus";font-size:10px;height:16px;inset-inline-start:0;max-width:44px;overflow:hidden;padding:0 4px 0 8px;pointer-events:none;position:absolute;text-overflow:ellipsis;top:0;white-space:nowrap;width:100%} +.cropper .cropper-cover-area{background:url(../Images/cropper-background-cover-area.svg);cursor:not-allowed;opacity:1;pointer-events:none;position:absolute;z-index:99999} +.cropper .cropper-cover-area:after{background-color:hsla(0,0%,100%,.95);color:#000;content:"Cover area";font-size:10px;height:16px;inset-inline-start:0;max-width:80px;overflow:hidden;padding:0 4px;pointer-events:none;position:absolute;text-overflow:ellipsis;top:0;white-space:nowrap;width:100%} +.cropper .cropper-preview-thumbnail{direction:ltr;max-height:100px;max-width:100px;overflow:hidden;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none} +.cropper .cropper-preview-thumbnail:after{background-color:rgba(0,0,0,.5);bottom:0;content:" ";left:0;position:absolute;right:0;top:0;z-index:9} +.cropper .cropper-preview-thumbnail.wide{height:auto;width:100px} +.cropper .cropper-preview-thumbnail.tall{height:80px;width:auto} +.cropper .cropper-preview-thumbnail-image{inset-inline-start:0;top:0} +.cropper .wide .cropper-preview-thumbnail-image{height:auto;width:100%} +.cropper .tall .cropper-preview-thumbnail-image{height:100%;width:auto} +.cropper .cropper-preview-thumbnail-crop-area{border:1px solid var(--typo3-state-primary-bg);overflow:hidden;position:absolute;z-index:10} +.cropper .cropper-preview-thumbnail-focus-area{background-color:rgba(215,187,0,.7);position:absolute;z-index:11} +:root .cropper-preview-thumbnail-crop-image{display:block;height:100%;image-orientation:0deg;max-height:none;max-width:none;min-height:0;min-width:0;width:100%} +.cropper-preview-container{overflow:hidden;position:relative} +.cropper-preview-container img{display:block;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;position:absolute;width:100%} +:root{--page-position-grid-spacing:1rem;--page-position-cell-spacing:1rem;--page-position-target-bg:color-mix(in srgb,var(--module-bg),currentColor 3%);--page-position-meta-bg:color-mix(in srgb,var(--module-bg),transparent 30%);--page-position-target-border-radius:4px} +.page-position{display:grid;gap:var(--page-position-grid-spacing);grid-template:"page-position-control-top ." "page-position-target page-position-control-right" "page-position-control-bottom ." auto/minmax(auto,1fr)} +.page-position-control{align-items:center;display:flex;justify-content:center} +.page-position-control-top{grid-area:page-position-control-top} +.page-position-control-right{grid-area:page-position-control-right} +.page-position-control-bottom{grid-area:page-position-control-bottom} +.page-position .page-position-target{background:var(--page-position-target-bg);border:1px solid var(--page-position-target-bg);border-radius:var(--page-position-target-border-radius);box-shadow:var(--typo3-component-box-shadow-strong);display:flex;flex-direction:column;grid-area:page-position-target;height:100%;min-height:100px;width:100%} +.page-position .page-position-target-abstract{flex-grow:1;padding:calc(var(--page-position-cell-spacing)/2) var(--page-position-cell-spacing);position:relative} +.page-position .page-position-target-meta{background-color:var(--page-position-meta-bg);border-end-end-radius:var(--page-position-target-border-radius);border-end-start-radius:var(--page-position-target-border-radius);font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:.75rem;line-height:1.2em;padding:calc(var(--page-position-cell-spacing)/2) var(--page-position-cell-spacing)} +.page-position-map{--page-position-grid-border-radius:5px;--page-position-grid-border-color:color-mix(in srgb,var(--module-bg),currentColor 30%);--page-position-grid-spacing:1rem;--page-position-grid-inner-spacing:1rem;--page-position-grid-cell-border-radius:4px;--page-position-grid-cell-bg:color-mix(in srgb,var(--module-bg),currentColor 3%);--page-position-element-spacing:1rem;--page-position-element-bg:var(--typo3-surface-bright);--page-position-element-border-radius:2px;--page-position-element-warning-bg:var(--typo3-surface-container-warning);--page-position-element-hidden-bg:var(--page-position-grid-cell-bg);background-color:var(--module-bg);border:1px dotted var(--page-position-grid-border-color);border-radius:var(--page-position-grid-border-radius);margin-bottom:var(--typo3-spacing);overflow-x:auto;overflow-y:hidden} +.page-position-map .page-position-headline,.page-position-map .page-position-info{margin:var(--page-position-grid-spacing) var(--page-position-grid-spacing) 0} +.page-position-map .page-position-grid{border-collapse:separate;border-spacing:var(--page-position-grid-spacing);min-width:100%;table-layout:fixed} +.page-position-map .page-position-grid td{background-color:var(--page-position-grid-cell-bg);border-radius:var(--page-position-grid-cell-border-radius);padding:calc(var(--page-position-grid-inner-spacing) - var(--page-position-grid-spacing)) 0;vertical-align:top} +.page-position-map .page-position-grid .column-title{margin:var(--page-position-element-spacing) var(--page-position-grid-inner-spacing)} +.page-position-map .page-position-grid ul{list-style:none;padding:0} +.page-position-map .page-position-grid .page-position-action{text-align:center} +.page-position-map .page-position-grid .page-position-record{background-color:var(--page-position-element-bg);border:1px solid var(--page-position-element-bg);border-radius:var(--page-position-element-border-radius);box-shadow:var(--typo3-component-box-shadow-strong);margin:var(--page-position-grid-spacing) var(--page-position-grid-inner-spacing);overflow:hidden;padding:var(--page-position-element-spacing)} +.typo3-TCEforms{width:100%} +.sortableHandle{cursor:move!important} +img.t3-tceforms-sysfile-imagepreview{float:var(--typo3-position-start);-webkit-margin-end:10px;margin-inline-end:10px} +.typo3-TCEforms span.typo3-TCEforms-newToken{color:#900;font-weight:700} +.t3-form-original-language{background-color:var(--typo3-state-default-bg);border:var(--typo3-input-border-width) solid var(--typo3-state-default-border-color);border-radius:var(--typo3-input-border-radius);color:var(--typo3-state-default-color);font-size:.625rem;padding:calc(var(--typo3-input-padding-y)/2) var(--typo3-input-padding-x);word-break:break-all} +.t3-form-original-language-diff{border:1px solid var(--typo3-state-default-border-color);font-size:.625rem;margin-top:4px} +.t3-form-original-language-diffheader{font-weight:700;padding:2px} +.t3-form-original-language-diffcontent{padding:2px} +.t3-form-original-language .icon{-webkit-margin-end:5px;margin-inline-end:5px} +div.t3-form-field-container:first-child .t3-form-field-label-flex{border-top:0} +.form-irre-object,.t3-flex-section{opacity:1;transition:opacity .5s} +.form-irre-object--deleted,.t3-flex-section--deleted{opacity:0!important} +.t3-form-field-disable{display:none} +.formengine-field-item.disabled .t3-form-field-disable{background:var(--typo3-form-section-bg);display:block;height:100%;left:0;opacity:.5;position:absolute;top:0;width:100%;z-index:10} +.formengine-field-item.disabled .form-description,.formengine-field-item.disabled .t3-form-field-eval-null-checkbox{position:relative;z-index:20} +.treeline-icon{display:inline-block;margin-bottom:-.75rem;margin-top:-.75rem;position:relative;-webkit-margin-end:2px;flex-shrink:0;height:54px;margin-inline-end:2px;overflow:hidden;padding:0;vertical-align:middle;white-space:nowrap;width:16px} +.treeline-icon:after,.treeline-icon:before{content:"";inset-inline-start:50%;position:absolute} +.treeline-icon:before{height:100%;top:0;width:2px;-webkit-margin-start:-1px;margin-inline-start:-1px;-webkit-border-start:1px solid var(--treelist-border-color);border-inline-start:1px solid var(--treelist-border-color)} +.treeline-icon:after{border-top:1px solid var(--treelist-border-color);height:2px;margin-top:-1px;top:50%;width:100%} +.treeline-icon-jointop:before{top:50%} +.treeline-icon-joinbottom:before{top:-50%} +.treeline-icon-blank,.treeline-icon-clear:after,.treeline-icon-clear:before,.treeline-icon-line:after{display:none} +.treeline-container{align-items:center;display:flex;gap:calc(var(--typo3-spacing)*.25)} +.treeline-label{flex-grow:1;overflow:hidden;text-overflow:ellipsis;text-wrap:nowrap} +span.dragIcon{display:inline-block;height:16px} +#dragIcon{opacity:.5;position:absolute;visibility:hidden;white-space:nowrap;z-index:20} +.ui-block{background:rgba(0,0,0,.3);height:100%;left:0;padding-top:200px;position:absolute;text-align:center;top:0;width:100%;z-index:3000} +.typo3-install-container{margin:0 auto;max-width:620px;padding:4rem 1rem} +.typo3-install-content{background-color:var(--typo3-component-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:1rem;box-shadow:var(--typo3-component-box-shadow-strong);color:var(--typo3-component-color);overflow:hidden} +.typo3-install-content>div>:first-child{margin-top:0} +.typo3-install-content>div>:last-child{margin-bottom:0} +.typo3-install-content-header{padding:2rem} +.typo3-install-content-header-logo{margin:0 auto 1rem} +.typo3-install-content-progress{background-color:color-mix(in srgb,var(--typo3-component-bg),var(--typo3-component-color) 3%);padding:1rem 2rem} +.typo3-install-content-body,.typo3-install-content-progress{border-top:var(--typo3-component-border-width) solid var(--typo3-component-border-color)} +.typo3-install-content-body{padding:2rem} +.typo3-install-content-body>:first-child{margin-top:0} +.typo3-install-content-body>:last-child{margin-bottom:0} +.typo3-install-content-spacer{margin-top:2rem} +.typo3-install-content-spacer-small{margin-top:1rem} +.extensionConfiguration-form .tab-content{margin-bottom:1rem} +.t3-install-form-password-toggle[data-toggle-state=invisible] .icon-actions-eye,.t3-install-form-password-toggle[data-toggle-state=visible] .icon-actions-lock{display:none} +body[data-typo3-login-ready]{overflow-y:auto} +.typo3-login{--typo3-login-highlight:#f80;--typo3-login-btn-color:#fff;--typo3-login-btn-bg:var(--typo3-login-highlight);--typo3-login-width:320px;--typo3-login-width-large:960px;--typo3-login-color:var(--typo3-text-color-base);--typo3-login-bg:var(--typo3-surface-base);--typo3-login-padding-x:2.5em;--typo3-login-copyright-color:var(--typo3-text-color-variant);background-color:var(--typo3-login-bg);background-position:50%;background-size:cover;color:var(--typo3-login-color);display:flex;flex-direction:row;height:auto!important;width:100%} +.typo3-login .highlight{color:var(--typo3-login-highlight);fill:var(--typo3-login-highlight)} +.typo3-login-inner{display:flex;flex-direction:column;margin:0 auto;min-height:100dvh;width:99.999%} +.typo3-login-container{align-items:center;display:flex;flex:1 1 auto;flex-direction:column;justify-content:center;padding:1.5em;z-index:100} +.typo3-login-footnote{color:#666;display:block;font-size:.95em;margin-left:auto;margin-right:auto;padding:1em 1.5em;text-align:center} +@media (min-width:768px){ +.typo3-login-footnote{bottom:1.5em;flex:none;inset-inline-end:0;position:absolute}} +.typo3-login-footnote p{margin:0} +.typo3-login-wrap{margin:0 auto;max-width:var(--typo3-login-width);width:100%} +.typo3-login-wrap.typo3-login-wrap-large{max-width:var(--typo3-login-large)} +.typo3-login-links{margin-bottom:20px;margin-top:20px;padding-top:inherit} +.typo3-login-links a{display:flex;gap:.5rem} +#t3js-login-url{border:0;height:0;overflow:hidden;padding:0;width:0;clip:rect(0,0,0,0);cursor:default} +.typo3-login-logo img,.typo3-login-logo svg{display:block;height:auto;margin:0 auto;max-width:220px;width:auto} +.typo3-login-news-heading{font-size:.875rem;line-height:1.3em;margin-top:0} +.card-login{overflow:visible;--typo3-card-bg:var(--typo3-surface-container-lowest)} +.card-login .card-heading{padding:2.5em var(--typo3-login-padding-x) 0} +.card-login .card-body{padding:1.75em var(--typo3-login-padding-x) 2.5em} +.card-login .card-footer{border-top:3px solid var(--typo3-login-highlight);padding:1.5em var(--typo3-login-padding-x)} +.card-login a{font-weight:700} +@media (max-width:767.98px){ +.card-login{margin-bottom:0}} +.card-login.card-mfa .card-heading .h2,.card-login.card-mfa .card-heading h2{margin:.75rem 0} +.input-login{--typo3-input-padding-x:12px;--typo3-input-padding-y:12px;--typo3-input-line-height:1.3em} +.btn-login{--typo3-btn-padding-y:12px;--typo3-btn-padding-x:12px;--typo3-btn-line-height:1.3em;--typo3-btn-color:var(--typo3-login-btn-color);--typo3-btn-bg:var(--typo3-login-btn-bg);--typo3-btn-border-color:hsl(from var(--typo3-login-btn-bg) h s calc(l - 5));--typo3-btn-hover-color:var(--typo3-login-btn-color);--typo3-btn-hover-bg:hsl(from var(--typo3-login-btn-bg) h s calc(l - 3));--typo3-btn-hover-border-color:hsl(from var(--typo3-login-btn-bg) h s calc(l - 8));--typo3-btn-focus-color:var(--typo3-login-btn-color);--typo3-btn-focus-bg:hsl(from var(--typo3-login-btn-bg) h s calc(l - 6));--typo3-btn-focus-border-color:hsl(from var(--typo3-login-btn-bg) h s calc(l - 11));--typo3-btn-disabled-color:var(--typo3-login-btn-color);--typo3-btn-disabled-bg:var(--typo3-login-btn-bg);--typo3-btn-disabled-border-color:hsl(from var(--typo3-login-btn-bg) h s calc(l - 5))} +.typo3-login-carousel{padding:var(--typo3-login-padding-x)} +.typo3-login-carousel-control{background-color:var(--typo3-card-bg);height:60px;line-height:60px;margin-top:-30px;opacity:.75;padding:0;position:absolute;text-align:center;top:50%;transition:opacity .2s ease-in-out;width:20px} +@media (prefers-reduced-motion:reduce){ +.typo3-login-carousel-control{transition:none}} +.typo3-login-carousel-control:hover{opacity:1} +.typo3-login-carousel-control.left{border:1px solid var(--typo3-card-border-color);inset-inline-start:-20px;-webkit-border-end:0;border-inline-end:0;border-radius:var(--typo3-card-border-radius) 0 0 var(--typo3-card-border-radius)} +.typo3-login-carousel-control.right{border:1px solid var(--typo3-card-border-color);inset-inline-end:-20px;-webkit-border-start:0;border-inline-start:0;border-radius:0 var(--typo3-card-border-radius) var(--typo3-card-border-radius) 0} +.typo3-login-copyright-link{background-color:transparent;border:none;display:flex;font-weight:400!important;justify-content:space-between;padding:0;width:100%} +.typo3-login-copyright-link:hover{text-decoration:underline} +.typo3-login-copyright-link>img{float:var(--typo3-position-end);margin-top:-4px} +.typo3-login-copyright-text{color:var(--typo3-login-copyright-color);font-size:.95em;padding-top:1em} +.typo3-login-copyright-text .list-unstyled{line-height:2.25} +.typo3-login-copyright-text>:first-child{margin-top:0} +.typo3-login-copyright-text>:last-child{margin-bottom:0} +video{background-color:#000} +.nowrap{white-space:nowrap} +.nowrap-disabled{white-space:normal!important} +.section{margin-bottom:15px} +.media-gallery__list .media-gallery__item{display:inline-block;float:none;margin-bottom:15px;-webkit-margin-end:-4px;margin-inline-end:-4px;vertical-align:top} +.media-gallery__list .media-gallery__item .media-object{max-height:158px;overflow:hidden} +.media-gallery__list .media-gallery__item img.thumbnail{height:auto;margin-bottom:0;max-height:150px;max-width:100%} +.media-gallery__list .media-gallery__item span.thumbnail{display:inline-block;margin-bottom:0} +.form-control-holder{position:relative} +.formengine-field-item{display:block;position:relative} +textarea.formengine-textarea{resize:none} +.sticky-form-actions{--typo3-stickyform-bg:var(--typo3-surface-base);--typo3-stickyform-border-width:var(--typo3-component-border-width);--typo3-stickyform-border-color:var(--typo3-component-border-color);--typo3-stickyform-padding-y:1rem;--typo3-stickyform-padding-x:1rem;--typo3-stickyform-spacing:1rem;background:var(--typo3-stickyform-bg);border-bottom:var(--typo3-stickyform-border-width) solid var(--typo3-stickyform-border-color);display:flex;gap:var(--typo3-stickyform-spacing);margin-inline:calc(var(--typo3-stickyform-padding-x)*-1);margin-bottom:var(--typo3-stickyform-spacing);padding:var(--typo3-stickyform-padding-y) var(--typo3-stickyform-padding-x);position:-webkit-sticky;position:sticky;top:0;z-index:2} +.modal-body:has(.sticky-form-actions){padding-top:0} +.multi-record-selection-actions-wrapper{margin:.5rem 0;min-height:calc(var(--typo3-input-sm-padding-y)*2 + var(--typo3-input-border-width)*2 + var(--typo3-input-font-size)*var(--typo3-input-line-height))} +.contextual-record-edit{--contextual-record-edit-bg:var(--typo3-surface-base);--contextual-record-edit-border-color:color-mix(in srgb,var(--contextual-record-edit-bg),var(--typo3-text-color-base) 15%);--contextual-record-edit-success-color:var(--typo3-state-success-color);--contextual-record-edit-success-bg:var(--typo3-state-success-bg)} +.contextual-record-edit-header{align-items:center;background:var(--contextual-record-edit-bg);border-bottom:1px solid var(--contextual-record-edit-border-color);display:flex;gap:.75rem;overflow:hidden;padding:.5rem 1rem;position:-webkit-sticky;position:sticky;top:0;z-index:10} +.contextual-record-edit-title-group{align-items:center;display:flex;flex:1;gap:.25rem;min-width:0} +.contextual-record-edit-title{font-size:.875rem;font-weight:700;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.contextual-record-edit-actions{align-items:center;display:flex;flex-shrink:0;gap:.5rem} +@media (max-width:500px){ +.contextual-record-edit-button-label{display:none}} +.contextual-record-edit-saved-indicator{background-color:var(--contextual-record-edit-success-bg);border-radius:.25rem;color:var(--contextual-record-edit-success-color);font-size:.75rem;opacity:1;padding:.125rem .5rem;transition:opacity .5s ease} +.contextual-record-edit-body{padding:1rem} +.localization-wizard .option{margin-bottom:.25rem} +.localization-fieldset{display:grid;gap:calc(var(--typo3-spacing)/4)} +.localization-fieldset+.localization-fieldset{margin-top:var(--typo3-spacing)} +pre.ts-hl{font-family:Lucida Console,Lucida Sans Typewriter,Bitsream Vera Sans Mono,monospace} +pre.ts-hl .ts-operator{color:#000;font-weight:700} +pre.ts-hl .ts-value{color:#c00} +pre.ts-hl .ts-objstr{color:#00c} +pre.ts-hl .ts-value_copy{color:#060} +pre.ts-hl .ts-default,pre.ts-hl .ts-ignored,pre.ts-hl .ts-value_unset{background-color:#6c6} +pre.ts-hl .ts-comment{color:#666;font-style:italic} +pre.ts-hl .ts-condition{background-color:maroon;color:#fff;font-weight:700} +pre.ts-hl .ts-error{background-color:#ff0;border:1px dashed red;color:#000;font-weight:700} +pre.ts-hl .ts-linenum{background-color:#eee;color:#212424} +.about-module a:not([class]){color:var(--typo3-component-link-color)} +.about-module a:not([class]):hover{color:var(--typo3-component-link-hover-color)} +td.permission-column-list{white-space:nowrap;width:106px} +td.permission-column-group{-webkit-padding-start:0;padding-inline-start:0;white-space:nowrap;width:200px} +td.permission-column-group .input-group{flex-wrap:nowrap} +td.permission-column-group .input-group select{min-width:100px} +.access-legend{--typo3-access-legend-guide-color:var(--typo3-text-color-base);--typo3-access-legend-number-border-color:var(--typo3-text-color-base);margin-bottom:18px} +.access-legend .number{border:1px solid var(--typo3-access-legend-number-border-color);border-radius:2px;display:inline-block;font-weight:700;height:16px;line-height:14px;margin-inline:10px 5px;text-align:center;width:16px} +.access-legend .edge span{background:var(--typo3-access-legend-guide-color);display:block;height:10px;margin-top:8px;width:1px;-webkit-margin-start:6px;margin-inline-start:6px} +.access-legend .edge span span{height:1px;width:10px;-webkit-margin-start:1px;margin-inline-start:1px} +.access-legend .hr span{height:1px;margin-top:-1px;width:20px;-webkit-margin-start:-4px;margin-inline-start:-4px} +.access-legend .hr span,.access-legend .t3-vr span{background:var(--typo3-access-legend-guide-color);display:block} +.access-legend .t3-vr span{height:20px;margin-top:-2px;width:1px;-webkit-margin-start:6px;margin-inline-start:6px} +@media (min-width:992px){ +.beuser-comparison-table{table-layout:fixed} +.beuser-comparison-table td,.beuser-comparison-table th{-moz-column-width:auto;column-width:auto}} +.beuser-comparison-table th[scope=row]{white-space:wrap} +.beuser-comparison-element{align-items:flex-start;display:flex;gap:var(--typo3-spacing)} +.beuser-comparison-element__title{flex-grow:1;padding-top:calc(var(--typo3-input-border-width) + var(--typo3-input-sm-padding-y));white-space:wrap;word-break:break-word} +.beuser-online-table tbody:nth-of-type(odd)>tr>*{--bs-table-bg-type:var(--bs-table-striped-bg);--bs-table-color-type:var(--bs-table-striped-color)} +.beuser-online-table tbody tr:hover td,.beuser-online-table tbody:hover td[rowspan]{--bs-table-bg-type:var(--bs-table-hover-bg);--bs-table-color-type:var(--bs-table-hover-color)} +.beuser-online-table .beuser-online-table_row td:first-child{-webkit-padding-start:.5rem;padding-inline-start:.5rem} +.beuser-online-table td.col-datetime span{margin-left:.5rem} +.extensionmanager-is-loading{opacity:0!important} +.extensionmanager-is-hidden{display:none!important} +.extensionmanager-is-shown{display:block!important} +.extension-list-last-updated{font-weight:700;-webkit-padding-end:.4em;cursor:help;padding-inline-end:.4em} +.extension-list th:first-child,.extension-list th:nth-child(2){width:4%} +.extension-list th:nth-child(3),.extension-list th:nth-child(4){width:30%} +.extension-list th:nth-child(5),.extension-list th:nth-child(6),.extension-list th:nth-child(7){width:7%} +.extension-list th:nth-child(8){width:11%} +.extension-list-terTable th:first-child,.extension-list-terTable th:nth-child(2){width:4%} +.extension-list-terTable th:nth-child(3),.extension-list-terTable th:nth-child(6){width:30%} +.extension-list-terTable th:nth-child(4),.extension-list-terTable th:nth-child(5),.extension-list-terTable th:nth-child(7){width:7%} +.extension-list-terTable th:nth-child(8){width:11%} +.extension-icon{height:32px;width:32px;-webkit-margin-end:1em;border-radius:4px;margin-inline-end:1em} +#terTableWrapper{margin-top:1em;position:relative} +#terTableWrapper .splash-receivedata{display:none;left:0;position:absolute;right:0;text-align:center;top:50px} +.distribution-official-badge{bottom:0;inset-inline-end:.75em;position:absolute;transform:translateY(25%)} +.distribution-official-badge img{display:block;width:50px} +@media (min-width:768px){ +.distribution-official-badge img{width:64px}} +.distribution-image{display:block;max-height:225px;overflow:hidden} +.distribution-detail-previewpane{margin-bottom:2em;max-width:100%} +@media (min-width:768px){ +.distribution-detail-previewpane{float:var(--typo3-position-start);-webkit-margin-end:3em;margin-inline-end:3em}} +.distribution-detail-body{overflow:hidden;zoom:1} +.distribution-detail-header{margin-bottom:2.5em} +.distribution-detail-header .h1,.distribution-detail-header h1,.distribution-detail-header typo3-backend-editable-page-title{margin-bottom:.5em} +.distribution-detail-header p{margin-bottom:1.25em;max-width:500px;padding:0} +.distribution-detail-actions{list-style:none;padding:0} +.distribution-detail-actions li{margin:.25em 0} +.t3-filelist-info-container{align-items:center;display:flex;flex-direction:column;justify-content:center;margin-top:-2rem;min-height:300px} +.file-replace-dialog{flex-direction:column} +.file-replace-dialog,.file-replace-dialog-summary{display:flex;gap:var(--typo3-spacing)} +.file-replace-dialog-summary-thumbnail{flex-shrink:1} +.file-replace-dialog-summary-info{flex-grow:1} +.help-view img{margin:1em 0} +.help-copyright{border-top:1px solid rgba(0,0,0,.15);margin-top:1em;padding-top:1em} +.help-teaser{cursor:help!important} +.help-has-link{cursor:pointer!important} +#PageInformationControllerTable a[data-contextmenu-trigger]{-webkit-margin-end:4px;margin-inline-end:4px} +.install-tool-modal .panel-rst .panel-collapse,.install-tool-modal .panel-version .panel-collapse{overflow:auto;position:relative} +.install-tool-modal .panel-rst .panel-heading,.install-tool-modal .panel-version .panel-heading{position:relative} +.install-tool-modal .panel-rst .panel-heading strong,.install-tool-modal .panel-version .panel-heading strong{line-height:1.5em} +.install-tool-modal .panel-rst .rst-tags,.install-tool-modal .panel-version .rst-tags{display:flex;gap:.5rem;inset-inline-end:var(--typo3-panel-padding-x);position:absolute;top:calc(var(--typo3-panel-padding-y)/2)} +.install-tool-modal .panel-rst .rst-tags~.panel-body,.install-tool-modal .panel-version .rst-tags~.panel-body{padding-top:calc(var(--typo3-panel-padding-y)*2)} +.install-tool-modal .panel-rst .rst-links,.install-tool-modal .panel-version .rst-links{bottom:calc(var(--typo3-panel-padding-y)/2);display:flex;gap:.5rem;inset-inline-end:var(--typo3-panel-padding-x);position:absolute} +.install-tool-modal .panel-rst .rst-links~.panel-body,.install-tool-modal .panel-version .rst-links~.panel-body{padding-bottom:calc(var(--typo3-panel-padding-y)*2)} +.install-tool-modal .list-group-item a{display:block} +.install-tool-modal .list-group-item.active a{color:#fff} +.install-tool-modal .table .t3-languagePacks-inactive,.install-tool-modal .table .t3-languagePacks-inactive td{color:#aaa} +.install-tool-modal .t3-install-displaytwinimageimages{border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:var(--typo3-component-border-radius);margin-bottom:var(--typo3-spacing);padding:var(--typo3-component-padding-y) var(--typo3-component-padding-x)} +.install-tool-modal .t3-install-displaytwinimagetextarea pre{border-top:0} +.install-tool-modal .bg-transparent-emulation{background:url(../Images/bg_transparent_emulation.png);padding:10px;text-align:center} +.install-tool-modal .bg-transparent-emulation img{max-width:300px} +.install-tool-modal #phpinfo table{table-layout:fixed;width:100%;word-wrap:break-word;margin-bottom:var(--typo3-spacing)} +.install-tool-modal .upgrade_analysis_item_to_filter pre a{text-decoration:underline} +.install-tool-modal .upgradeWizards-wizards-output .row-explanation{white-space:pre-wrap} +.install-tool-modal ul{word-wrap:anywhere} +.module-action-list{display:grid;gap:.25rem;margin-bottom:var(--typo3-spacing)} +.module-action-item{align-items:center;background-color:var(--typo3-component-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:var(--typo3-component-border-radius);color:var(--typo3-component-color);display:flex;gap:var(--typo3-spacing);padding:var(--typo3-component-padding-y) var(--typo3-component-padding-x)} +.module-action-item .module-action-item-body{flex-grow:1} +.module-action-item .module-action-item-title{font-weight:700;margin-bottom:calc(var(--typo3-spacing)/4)} +.localconf-list{display:flex;flex-direction:column;gap:1rem} +.localconf-item{border:1px solid var(--typo3-panel-border-color);width:100%} +.localconf-item.searchhit~.alert{display:none} +.localconf-item-heading{background-color:var(--typo3-panel-header-bg);padding:.5rem 1rem} +.localconf-item-body{padding:1rem} +.localconf-item-body>:first-child{margin-top:0} +.localconf-item-body>:last-child{margin-bottom:0} +.configuration-map-container{display:flex;flex-direction:column;gap:calc(var(--typo3-spacing)*.5);margin-bottom:var(--typo3-spacing);margin-top:var(--typo3-spacing)} +.configuration-map-container .configuration-map-item{align-items:stretch;display:flex;position:relative;width:100%} +.configuration-map-container .configuration-map-item-collection{display:flex;flex-direction:column;gap:calc(var(--typo3-spacing)*.5)} +.configuration-map-container .configuration-map-item-header{flex:1 1 0;font-weight:700;margin-bottom:calc(var(--typo3-spacing)*.25)} +.configuration-map-container .configuration-map-item-header:last-child{flex-basis:calc(var(--typo3-input-padding-x)*2 + var(--icon-size-small))} +.configuration-map-container .btn-configuration-map-add{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content} +:root{--pagemodule-grid-border-radius:.75em;--pagemodule-grid-border-color:color-mix(in srgb,var(--module-bg),currentColor 30%);--pagemodule-grid-spacing:1rem;--pagemodule-grid-inner-spacing:1rem;--pagemodule-grid-cell-header-size:.875rem;--pagemodule-grid-cell-border-radius:calc(var(--pagemodule-grid-border-radius) - 3px);--pagemodule-grid-cell-bg:var(--typo3-surface-container-base);--pagemodule-grid-cell-restricted-bg:var(--typo3-surface-container-warning);--pagemodule-grid-column-unused-bg:var(--typo3-surface-container-warning);--pagemodule-element-spacing:1rem;--pagemodule-element-bg:var(--typo3-surface-bright);--pagemodule-element-border-radius:.75em;--pagemodule-element-border-color:color-mix(in srgb,var(--pagemodule-element-bg),currentColor 15%);--pagemodule-element-warning-bg:var(--typo3-surface-container-warning);--pagemodule-element-hidden-bg:var(--pagemodule-grid-cell-bg);--pagemodule-action-height:28.5px;--pagemodule-dropzone-bg:var(--typo3-surface-container-warning);--pagemodule-dropzone-possible-bg:var(--typo3-surface-container-success)} +.t3-grid-table{border-collapse:separate;border-spacing:var(--pagemodule-grid-spacing);min-width:100%;table-layout:fixed} +.t3-grid-container{border:1px dotted var(--pagemodule-grid-border-color);border-radius:var(--pagemodule-grid-border-radius);margin-bottom:var(--typo3-spacing);overflow-x:auto;overflow-y:hidden} +.t3-grid-cell{background-color:var(--pagemodule-grid-cell-bg);border-radius:var(--pagemodule-grid-cell-border-radius);padding:calc(var(--pagemodule-grid-inner-spacing) - var(--pagemodule-grid-spacing)) 0} +.t3-grid-cell-restricted{background-color:var(--pagemodule-grid-cell-restricted-bg)} +.t3-page-column-unused{background-color:var(--pagemodule-grid-column-unused-bg)} +.t3-grid-cell-hidden{display:none} +.t3-grid-cell-unassigned{background-image:repeating-linear-gradient(-45deg,color-mix(in srgb,currentColor,transparent 95%),color-mix(in srgb,currentColor,transparent 95%) 5px,transparent 0,transparent 10px)} +.t3-page-columns{border:0;min-width:100%} +.t3-page-column{max-width:300px;min-width:150px} +.t3-page-column>.callout{margin:var(--pagemodule-grid-inner-spacing)} +.t3-page-column-header{margin:var(--pagemodule-element-spacing) var(--pagemodule-grid-inner-spacing);-webkit-margin-end:calc(var(--pagemodule-grid-inner-spacing)*2);margin-inline-end:calc(var(--pagemodule-grid-inner-spacing)*2);position:relative;text-align:start} +.t3-page-column-header,.t3-page-column-title{font-size:var(--pagemodule-grid-cell-header-size);font-weight:700} +.t3-page-column-header-icons{bottom:0;color:var(--typo3-text-color-primary);inset-inline-end:calc(var(--pagemodule-grid-inner-spacing)*-1);opacity:.65;position:absolute;transition:var(--typo3-transition-color)} +.t3-page-column-header-icons:focus-within,.t3-page-column-header-icons:hover{opacity:1} +.t3-page-lang-copyce{margin:var(--pagemodule-grid-spacing)} +.t3-page-lang-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.t3-page-ce{display:block;margin:var(--pagemodule-grid-spacing) var(--pagemodule-grid-inner-spacing);position:relative;transition:height .4s ease-out} +.t3-page-ce.active-drag{z-index:4500} +.t3-page-ce[data-dragdrop-clone] .t3-page-ce-element{opacity:.25;-webkit-user-select:none;-moz-user-select:none;user-select:none} +.t3-page-ce-element{background-color:var(--pagemodule-element-bg);border:1px solid var(--pagemodule-element-border-color);border-radius:var(--pagemodule-element-border-radius);box-shadow:var(--typo3-component-box-shadow-strong);margin-bottom:var(--pagemodule-grid-spacing);overflow:hidden} +.t3-page-ce-hidden .t3-page-ce-element{background-color:var(--pagemodule-element-hidden-bg);border:1px dashed color-mix(in srgb,var(--pagemodule-element-hidden-bg),currentColor 40%);box-shadow:none;opacity:.5;transition:opacity .3s ease-in-out} +.t3-page-ce-hidden .t3-page-ce-element:focus-within,.t3-page-ce-hidden .t3-page-ce-element:hover{opacity:1} +.t3-page-ce-warning .t3-page-ce-element{background-color:var(--pagemodule-element-warning-bg);border:1px solid color-mix(in srgb,var(--pagemodule-element-warning-bg),currentColor 10%);box-shadow:none} +.t3-page-ce-header{align-items:center;display:flex;gap:.5rem;padding:var(--pagemodule-element-spacing)} +.t3-page-ce-header[draggable=true]{cursor:grab} +.t3-page-ce-header-left{-moz-column-gap:.25rem;column-gap:.25rem;display:flex} +.t3-page-ce-header-left,.t3-page-ce-header-right{flex-shrink:0} +.t3-page-ce-header-title{flex-grow:1;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.t3-page-ce-header-icon-flag{align-items:center;display:inline-flex} +.t3-page-ce-body{padding:var(--pagemodule-grid-spacing);word-wrap:break-word;margin-top:calc(var(--pagemodule-grid-spacing)*-1)} +.t3-page-ce-body:empty{display:none} +.t3-page-ce-body img{height:auto;max-width:100%} +.t3-page-ce-footer{background-color:rgba(0,0,0,.05);border-bottom-left-radius:var(--pagemodule-element-border-radius);border-bottom-right-radius:var(--pagemodule-element-border-radius);font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:.75rem;padding:calc(var(--pagemodule-grid-spacing)/2) var(--pagemodule-grid-spacing)} +.t3-page-ce-actions{height:var(--pagemodule-action-height);text-align:center;z-index:2} +.t3-page-ce-dropzone{background-color:var(--pagemodule-dropzone-bg);border:1px dashed color-mix(in srgb,var(--pagemodule-dropzone-bg),currentColor 10%);border-radius:var(--pagemodule-element-border-radius);height:var(--pagemodule-action-height);position:relative;top:0} +.t3-page-ce-dropzone.active{width:100%} +.t3-page-ce-dropzone.active.t3-page-ce-dropzone-possible{--pagemodule-dropzone-bg:var(--pagemodule-dropzone-possible-bg)} +.element-preview a{color:inherit} +.element-preview-header:empty{display:none} +.element-preview-header-status{font-size:.65625rem;opacity:.5} +.element-preview-header-date{font-size:.65625rem} +.element-preview-header-header,.element-preview-header-subheader{font-weight:700} +.element-preview-header+.element-preview-content{margin-top:.5rem} +.element-preview-content:empty{display:none} +.element-preview-content>:first-child{margin-top:0} +.element-preview-content>:last-child{margin-bottom:0} +.element-preview-content .preview-thumbnails{margin-top:.5rem} +.preview-thumbnails{--preview-thumbnails-element-bg:var(--typo3-surface-bright);--preview-thumbnails-element-border-color:color-mix(in srgb,var(--preview-thumbnails-element-bg),currentColor 15%);--preview-thumbnails-element-border-radius:var(--typo3-component-border-radius);--preview-thumbnails-element-spacing:.5rem;--preview-thumbnails-size:64px;display:flex;flex-wrap:wrap;gap:.5rem} +.preview-thumbnails-element{background-color:var(--preview-thumbnails-element-bg);border:1px solid var(--preview-thumbnails-element-border-color);border-radius:var(--preview-thumbnails-element-border-radius);display:block;padding:var(--preview-thumbnails-element-spacing)} +.preview-thumbnails-element-image{align-items:center;display:flex;height:var(--preview-thumbnails-size);justify-content:center;overflow:hidden;width:var(--preview-thumbnails-size)} +.preview-thumbnails-element-image .icon{height:calc(var(--preview-thumbnails-size)/2);width:calc(var(--preview-thumbnails-size)/2)} +.tx_recycler_recycler tr.collapse{display:none} +.tx_recycler_recycler tr.collapse.show{display:table-row} +.scheduler-sortable-handle{cursor:move!important} +.scheduler-panel+.scheduler-panel{margin-top:calc(var(--typo3-spacing)*1.5)} +@media (min-width:1200px){ +.scheduler-info-text{width:90%}} +.module-styleguide .module-body-container{container-type:inline-size;display:flex;flex-wrap:wrap;gap:2.5rem} +.styleguide-navigation{align-self:flex-start;flex:0 1 200px} +.styleguide-content{contain:inline-size;flex:1 1 500px} +.styleguide-content .icon-container-wrapper{display:grid;gap:.25rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))} +.styleguide-content .icon-container{--typo3-icon-container-bg:var(--typo3-state-default-bg);--typo3-icon-container-border-radius:var(--typo3-component-border-radius);--typo3-icon-container-transition-color:var(--typo3-transition-color);background-color:var(--typo3-icon-container-bg);border-radius:var(--typo3-icon-container-border-radius);display:flex;flex-direction:column;gap:var(--typo3-spacing);justify-content:space-between;padding:var(--typo3-spacing);transition:var(--typo3-icon-container-transition-color)} +.styleguide-content .icon-container:hover{--typo3-icon-container-bg:var(--typo3-state-default-hover-bg)} +.styleguide-content .icon-container span.icon-container-icon,.styleguide-content .icon-container span.icon-container-label{display:block;text-align:center} +.styleguide-content .icon-container:not(:hover):not(:focus-visible) span.icon-container-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.styleguide-content .example-container{align-items:center;display:flex;flex-wrap:wrap;gap:var(--typo3-spacing)} +.styleguide-content .example-container.example-container--large-spacing{gap:calc(var(--typo3-spacing)*2)} +.styleguide-example{--typo3-example-border-width:var(--typo3-component-border-width);--typo3-example-border-color:var(--typo3-component-border-color);--typo3-example-border-radius:var(--typo3-component-border-radius);--typo3-example-box-shadow:var(--typo3-component-box-shadow);--typo3-example-margin-bottom:var(--typo3-spacing);--typo3-example-content-bg:var(--typo3-surface-base);--typo3-example-content-padding:var(--typo3-spacing);--typo3-example-code-bg:var(--typo3-surface-container-base);--typo3-example-code-border-top-width:var(--typo3-component-border-width);--typo3-example-code-border-top-color:var(--typo3-component-border-color);--typo3-example-code-padding:var(--typo3-spacing);--typo3-example-label-color:var(--typo3-component-variant-color);--typo3-example-label-margin-bottom:var(--typo3-spacing);border:var(--typo3-example-border-width) solid var(--typo3-example-border-color);border-radius:var(--typo3-example-border-radius);box-shadow:var(--typo3-example-box-shadow);margin-bottom:calc(var(--typo3-example-margin-bottom)*1.5)} +.styleguide-example>:first-child{border-start-end-radius:calc(var(--typo3-example-border-radius) - var(--typo3-example-border-width));border-start-start-radius:calc(var(--typo3-example-border-radius) - var(--typo3-example-border-width))} +.styleguide-example>:last-child{border-end-end-radius:calc(var(--typo3-example-border-radius) - var(--typo3-example-border-width));border-end-start-radius:calc(var(--typo3-example-border-radius) - var(--typo3-example-border-width))} +.styleguide-example .styleguide-example-code{background-color:var(--typo3-example-code-bg);border-top:var(--typo3-example-code-border-top-width) solid var(--typo3-example-code-border-top-color);margin:0;padding:var(--typo3-example-code-padding)} +.styleguide-example .styleguide-example-code .example{padding:0} +.styleguide-example .example{border:0;border-radius:0;margin-bottom:0} +.styleguide-example .example:first-child{border-top-left-radius:var(--typo3-component-border-radius);border-top-right-radius:var(--typo3-component-border-radius)} +.styleguide-example .example:last-child{border-bottom-left-radius:var(--typo3-component-border-radius);border-bottom-right-radius:var(--typo3-component-border-radius)} +.styleguide-example .example:before{display:none} +.styleguide-example .example .styleguide-example-label{color:var(--typo3-example-label-color);display:block;font-size:.9em;font-weight:700;margin-bottom:var(--typo3-example-label-margin-bottom)} +.colorscheme-switch{align-items:center;display:flex;gap:.5em;padding-bottom:var(--typo3-spacing)} +@container (min-width: 740px){ +.styleguide-navigation{max-height:calc(100dvh - var(--module-docheader-height) - var(--module-body-padding-y)*2);overflow:auto;position:-webkit-sticky;position:sticky;top:calc(var(--module-docheader-bar-height) + var(--module-body-padding-y))}} +#SetupModuleController .form-section .form-control-wrap{max-width:804px} +[data-module-id=typo3-module-viewpage]{--viewpage-topbar-height:40px;--viewpage-resizable-size:5px;--viewpage-item-radius:5px} +[data-module-id=typo3-module-viewpage] .module-body{text-align:center} +[data-module-id=typo3-module-viewpage] .module-body .callout{text-align:start} +[data-module-id=typo3-module-viewpage] .resizable-e,[data-module-id=typo3-module-viewpage] .resizable-s,[data-module-id=typo3-module-viewpage] .resizable-w{position:absolute;z-index:90} +[data-module-id=typo3-module-viewpage] .resizable-w{height:100%;left:calc(var(--viewpage-resizable-size)*-1);top:0;width:var(--viewpage-resizable-size)} +[data-module-id=typo3-module-viewpage] .resizable-s{bottom:calc(var(--viewpage-resizable-size)*-1);height:var(--viewpage-resizable-size);width:100%} +[data-module-id=typo3-module-viewpage] .resizable-e{height:100%;right:calc(var(--viewpage-resizable-size)*-1);top:0;width:var(--viewpage-resizable-size)} +.viewpage-item{background-color:var(--typo3-surface-container-highest);border-radius:var(--viewpage-item-radius);box-shadow:var(--typo3-component-box-shadow-tooltip);color:var(--typo3-text-color-base);display:inline-block;position:relative} +.viewpage-item iframe{border-radius:0 0 var(--viewpage-item-radius) var(--viewpage-item-radius);display:block} +.viewpage-topbar{align-items:center;display:flex;flex-direction:row;flex-shrink:0;height:var(--viewpage-topbar-height);justify-content:space-between;padding:.75em 1em} +.viewpage-topbar-orientation a{color:inherit;opacity:.5;transition:all .2s ease-in-out} +.viewpage-topbar-orientation a:hover{opacity:1} +.viewpage-topbar-size{direction:ltr} +.viewpage-topbar-size input{background-color:transparent;border:0;border-bottom:1px solid var(--typo3-state-primary-border-color);color:inherit;padding-left:0;padding-right:0;transition:all .5s ease-in-out} +.viewpage-topbar-size input:focus,.viewpage-topbar-size input:hover{border-bottom-color:var(--typo3-state-primary-focus-border-color);outline:0} +.viewpage-resizeable{background-color:var(--typo3-surface-container-low);border-radius:0 var(--viewpage-item-radius) var(--viewpage-item-radius)} +.workspace-panel tr.collapsing{transition:none} +.workspace-panel tr.collapse{display:none} +.workspace-panel tr.collapse.show{display:table-row} +.workspace-panel .page-link{height:100%} \ No newline at end of file diff --git a/Resources/Public/Css/webfonts.css b/Resources/Public/Css/webfonts.css new file mode 100644 index 0000000..2dcf758 --- /dev/null +++ b/Resources/Public/Css/webfonts.css @@ -0,0 +1,14 @@ +/*! + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +@font-face{font-display:swap;font-family:Open Sans Variable;font-stretch:75% 100%;font-style:normal;font-weight:300 800;src:url(../Fonts/OpenSans/open-sans-latin-wdth-normal.woff2) format("woff2-variations");unicode-range:u+00??,u+0131,u+0152-0153,u+02bb-02bc,u+02c6,u+02da,u+02dc,u+0304,u+0308,u+0329,u+2000-206f,u+20ac,u+2122,u+2191,u+2193,u+2212,u+2215,u+feff,u+fffd} +@font-face{font-display:swap;font-family:Open Sans Variable;font-stretch:75% 100%;font-style:italic;font-weight:300 800;src:url(../Fonts/OpenSans/open-sans-latin-wdth-italic.woff2) format("woff2-variations");unicode-range:u+00??,u+0131,u+0152-0153,u+02bb-02bc,u+02c6,u+02da,u+02dc,u+0304,u+0308,u+0329,u+2000-206f,u+20ac,u+2122,u+2191,u+2193,u+2212,u+2215,u+feff,u+fffd} \ No newline at end of file diff --git a/Resources/Public/Fonts/OpenSans/open-sans-latin-wdth-italic.woff2 b/Resources/Public/Fonts/OpenSans/open-sans-latin-wdth-italic.woff2 new file mode 100644 index 0000000..ac861a5 Binary files /dev/null and b/Resources/Public/Fonts/OpenSans/open-sans-latin-wdth-italic.woff2 differ diff --git a/Resources/Public/Fonts/OpenSans/open-sans-latin-wdth-normal.woff2 b/Resources/Public/Fonts/OpenSans/open-sans-latin-wdth-normal.woff2 new file mode 100644 index 0000000..a963d43 Binary files /dev/null and b/Resources/Public/Fonts/OpenSans/open-sans-latin-wdth-normal.woff2 differ diff --git a/Resources/Public/Html/Close.html b/Resources/Public/Html/Close.html new file mode 100644 index 0000000..e5b39a0 --- /dev/null +++ b/Resources/Public/Html/Close.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> + <head> + <!-- Close script, used in particular by FormEngine to close the current edit window --> + <!-- TYPO3 Script ID: typo3/sysext/backend/Resources/Public/Html/Close.html --> + <meta charset="utf-8" /> + <title>Close + + + + + diff --git a/Resources/Public/Icons/Extension.svg b/Resources/Public/Icons/Extension.svg new file mode 100644 index 0000000..bca7a17 --- /dev/null +++ b/Resources/Public/Icons/Extension.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/Public/Icons/favicon.ico b/Resources/Public/Icons/favicon.ico new file mode 100644 index 0000000..90fdfe1 Binary files /dev/null and b/Resources/Public/Icons/favicon.ico differ diff --git a/Resources/Public/Images/bg_transparent_emulation.png b/Resources/Public/Images/bg_transparent_emulation.png new file mode 100644 index 0000000..df1b1dd Binary files /dev/null and b/Resources/Public/Images/bg_transparent_emulation.png differ diff --git a/Resources/Public/Images/cropper-background-cover-area.svg b/Resources/Public/Images/cropper-background-cover-area.svg new file mode 100644 index 0000000..4f1006f --- /dev/null +++ b/Resources/Public/Images/cropper-background-cover-area.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Resources/Public/Images/default.gif b/Resources/Public/Images/default.gif new file mode 100644 index 0000000..92f96cc Binary files /dev/null and b/Resources/Public/Images/default.gif differ diff --git a/Resources/Public/Images/typo3_logo_orange.svg b/Resources/Public/Images/typo3_logo_orange.svg new file mode 100644 index 0000000..d853637 --- /dev/null +++ b/Resources/Public/Images/typo3_logo_orange.svg @@ -0,0 +1,3 @@ + + + diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/autocomplete.js b/Resources/Public/JavaScript/Contrib/@codemirror/autocomplete.js new file mode 100644 index 0000000..96b55a7 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/autocomplete.js @@ -0,0 +1 @@ +import{Annotation as Qe,StateEffect as I,EditorSelection as v,codePointAt as C,codePointSize as O,fromCodePoint as ce,Facet as fe,combineConfig as Xe,StateField as X,Prec as Y,Text as Ye,Transaction as Ge,MapMode as G,RangeValue as Je,RangeSet as Ze,CharCategory as J}from"@codemirror/state";import{Direction as _e,logException as Z,showTooltip as et,EditorView as R,ViewPlugin as tt,getTooltip as he,Decoration as U,WidgetType as it,keymap as pe}from"@codemirror/view";import{syntaxTree as B,indentUnit as nt}from"@codemirror/language";class _{constructor(e,t,n,o){this.state=e,this.pos=t,this.explicit=n,this.view=o,this.abortListeners=[],this.abortOnDocChange=!1}tokenBefore(e){let t=B(this.state).resolveInner(this.pos,-1);for(;t&&e.indexOf(t.name)<0;)t=t.parent;return t?{from:t.from,to:this.pos,text:this.state.sliceDoc(t.from,this.pos),type:t.type}:null}matchBefore(e){let t=this.state.doc.lineAt(this.pos),n=Math.max(t.from,this.pos-250),o=t.text.slice(n-t.from,this.pos-t.from),s=o.search(ge(e,!1));return s<0?null:{from:n+s,to:this.pos,text:o.slice(s)}}get aborted(){return this.abortListeners==null}addEventListener(e,t,n){e=="abort"&&this.abortListeners&&(this.abortListeners.push(t),n&&n.onDocChange&&(this.abortOnDocChange=!0))}}function ue(i){let e=Object.keys(i).join(""),t=/\w/.test(e);return t&&(e=e.replace(/\w/g,"")),`[${t?"\\w":""}${e.replace(/[^\w\s]/g,"\\$&")}]`}function ot(i){let e=Object.create(null),t=Object.create(null);for(let{label:o}of i){e[o[0]]=!0;for(let s=1;stypeof o=="string"?{label:o}:o),[t,n]=e.every(o=>/^\w+$/.test(o.label))?[/\w*$/,/\w+$/]:ot(e);return o=>{let s=o.matchBefore(n);return s||o.explicit?{from:s?s.from:o.pos,options:e,validFor:t}:null}}function st(i,e){return t=>{for(let n=B(t.state).resolveInner(t.pos,-1);n;n=n.parent){if(i.indexOf(n.name)>-1)return e(t);if(n.type.isTop)break}return null}}function lt(i,e){return t=>{for(let n=B(t.state).resolveInner(t.pos,-1);n;n=n.parent){if(i.indexOf(n.name)>-1)return null;if(n.type.isTop)break}return e(t)}}class me{constructor(e,t,n,o){this.completion=e,this.source=t,this.match=n,this.score=o}}function T(i){return i.selection.main.from}function ge(i,e){var t;let{source:n}=i,o=e&&n[0]!="^",s=n[n.length-1]!="$";return!o&&!s?i:new RegExp(`${o?"^":""}(?:${n})${s?"$":""}`,(t=i.flags)!==null&&t!==void 0?t:i.ignoreCase?"i":"")}const V=Qe.define();function ye(i,e,t,n){let{main:o}=i.selection,s=t-o.from,l=n-o.from;return Object.assign(Object.assign({},i.changeByRange(r=>r!=o&&t!=n&&i.sliceDoc(r.from+s,r.from+l)!=i.sliceDoc(t,n)?{range:r}:{changes:{from:r.from+s,to:n==o.from?r.to:r.from+l,insert:e},range:v.cursor(r.from+s+e.length)})),{scrollIntoView:!0,userEvent:"input.complete"})}const be=new WeakMap;function rt(i){if(!Array.isArray(i))return i;let e=be.get(i);return e||be.set(i,e=de(i)),e}const H=I.define(),F=I.define();class at{constructor(e){this.pattern=e,this.chars=[],this.folded=[],this.any=[],this.precise=[],this.byWord=[],this.score=0,this.matched=[];for(let t=0;t=48&&b<=57||b>=97&&b<=122?2:b>=65&&b<=90?1:0:(N=ce(b))!=N.toLowerCase()?1:N!=N.toUpperCase()?2:0;(!d||Q==1&&K||S==0&&Q!=0)&&(t[h]==b||n[h]==b&&(p=!0)?l[h++]=d:l.length&&(k=!1)),S=Q,d+=O(b)}return h==a&&l[0]==0&&k?this.result(-100+(p?-200:0),l,e):u==a&&g==0?this.ret(-200-e.length+(x==e.length?0:-100),[0,x]):r>-1?this.ret(-700-e.length,[r,r+this.pattern.length]):u==a?this.ret(-900-e.length,[g,x]):h==a?this.result(-100+(p?-200:0)+-700+(k?0:-1100),l,e):t.length==2?null:this.result((o[0]?-700:0)+-200+-1100,o,e)}result(e,t,n){let o=[],s=0;for(let l of t){let r=l+(this.astral?O(C(n,l)):1);s&&o[s-1]==l?o[s-1]=r:(o[s++]=l,o[s++]=r)}return this.ret(e-n.length,o)}}class ct{constructor(e){this.pattern=e,this.matched=[],this.score=0,this.folded=e.toLowerCase()}match(e){if(e.length!1,activateOnTypingDelay:100,selectOnOpen:!0,override:null,closeOnBlur:!0,maxRenderedOptions:100,defaultKeymap:!0,tooltipClass:()=>"",optionClass:()=>"",aboveCursor:!1,icons:!0,addToOptions:[],positionInfo:ft,filterStrict:!1,compareCompletions:(e,t)=>e.label.localeCompare(t.label),interactionDelay:75,updateSyncTime:100},{defaultKeymap:(e,t)=>e&&t,closeOnBlur:(e,t)=>e&&t,icons:(e,t)=>e&&t,tooltipClass:(e,t)=>n=>xe(e(n),t(n)),optionClass:(e,t)=>n=>xe(e(n),t(n)),addToOptions:(e,t)=>e.concat(t),filterStrict:(e,t)=>e||t})}});function xe(i,e){return i?e?i+" "+e:i:e}function ft(i,e,t,n,o,s){let l=i.textDirection==_e.RTL,r=l,a=!1,c="top",f,h,p=e.left-o.left,u=o.right-e.right,g=n.right-n.left,x=n.bottom-n.top;if(r&&p=x||d>e.top?f=t.bottom-e.top:(c="bottom",f=e.bottom-t.top)}let K=(e.bottom-e.top)/s.offsetHeight,k=(e.right-e.left)/s.offsetWidth;return{style:`${c}: ${f/K}px; max-width: ${h/k}px`,class:"cm-completionInfo-"+(a?l?"left-narrow":"right-narrow":r?"left":"right")}}function ht(i){let e=i.addToOptions.slice();return i.icons&&e.push({render(t){let n=document.createElement("div");return n.classList.add("cm-completionIcon"),t.type&&n.classList.add(...t.type.split(/\s+/g).map(o=>"cm-completionIcon-"+o)),n.setAttribute("aria-hidden","true"),n},position:20}),e.push({render(t,n,o,s){let l=document.createElement("span");l.className="cm-completionLabel";let r=t.displayLabel||t.label,a=0;for(let c=0;ca&&l.appendChild(document.createTextNode(r.slice(a,f)));let p=l.appendChild(document.createElement("span"));p.appendChild(document.createTextNode(r.slice(f,h))),p.className="cm-completionMatchedText",a=h}return at.position-n.position).map(t=>t.render)}function ee(i,e,t){if(i<=t)return{from:0,to:i};if(e<0&&(e=0),e<=i>>1){let o=Math.floor(e/t);return{from:o*t,to:(o+1)*t}}let n=Math.floor((i-e)/t);return{from:i-(n+1)*t,to:i-n*t}}class pt{constructor(e,t,n){this.view=e,this.stateField=t,this.applyCompletion=n,this.info=null,this.infoDestroy=null,this.placeInfoReq={read:()=>this.measureInfo(),write:a=>this.placeInfo(a),key:this},this.space=null,this.currentClass="";let o=e.state.field(t),{options:s,selected:l}=o.open,r=e.state.facet(y);this.optionContent=ht(r),this.optionClass=r.optionClass,this.tooltipClass=r.tooltipClass,this.range=ee(s.length,l,r.maxRenderedOptions),this.dom=document.createElement("div"),this.dom.className="cm-tooltip-autocomplete",this.updateTooltipClass(e.state),this.dom.addEventListener("mousedown",a=>{let{options:c}=e.state.field(t).open;for(let f=a.target,h;f&&f!=this.dom;f=f.parentNode)if(f.nodeName=="LI"&&(h=/-(\d+)$/.exec(f.id))&&+h[1]{let c=e.state.field(this.stateField,!1);c&&c.tooltip&&e.state.facet(y).closeOnBlur&&a.relatedTarget!=e.contentDOM&&e.dispatch({effects:F.of(null)})}),this.showOptions(s,o.id)}mount(){this.updateSel()}showOptions(e,t){this.list&&this.list.remove(),this.list=this.dom.appendChild(this.createListBox(e,t,this.range)),this.list.addEventListener("scroll",()=>{this.info&&this.view.requestMeasure(this.placeInfoReq)})}update(e){var t;let n=e.state.field(this.stateField),o=e.startState.field(this.stateField);if(this.updateTooltipClass(e.state),n!=o){let{options:s,selected:l,disabled:r}=n.open;(!o.open||o.open.options!=s)&&(this.range=ee(s.length,l,e.state.facet(y).maxRenderedOptions),this.showOptions(s,n.id)),this.updateSel(),r!=((t=o.open)===null||t===void 0?void 0:t.disabled)&&this.dom.classList.toggle("cm-tooltip-autocomplete-disabled",!!r)}}updateTooltipClass(e){let t=this.tooltipClass(e);if(t!=this.currentClass){for(let n of this.currentClass.split(" "))n&&this.dom.classList.remove(n);for(let n of t.split(" "))n&&this.dom.classList.add(n);this.currentClass=t}}positioned(e){this.space=e,this.info&&this.view.requestMeasure(this.placeInfoReq)}updateSel(){let e=this.view.state.field(this.stateField),t=e.open;if((t.selected>-1&&t.selected=this.range.to)&&(this.range=ee(t.options.length,t.selected,this.view.state.facet(y).maxRenderedOptions),this.showOptions(t.options,e.id)),this.updateSelectedOption(t.selected)){this.destroyInfo();let{completion:n}=t.options[t.selected],{info:o}=n;if(!o)return;let s=typeof o=="string"?document.createTextNode(o):o(n);if(!s)return;"then"in s?s.then(l=>{l&&this.view.state.field(this.stateField,!1)==e&&this.addInfoPane(l,n)}).catch(l=>Z(this.view.state,l,"completion info")):this.addInfoPane(s,n)}}addInfoPane(e,t){this.destroyInfo();let n=this.info=document.createElement("div");if(n.className="cm-tooltip cm-completionInfo",e.nodeType!=null)n.appendChild(e),this.infoDestroy=null;else{let{dom:o,destroy:s}=e;n.appendChild(o),this.infoDestroy=s||null}this.dom.appendChild(n),this.view.requestMeasure(this.placeInfoReq)}updateSelectedOption(e){let t=null;for(let n=this.list.firstChild,o=this.range.from;n;n=n.nextSibling,o++)n.nodeName!="LI"||!n.id?o--:o==e?n.hasAttribute("aria-selected")||(n.setAttribute("aria-selected","true"),t=n):n.hasAttribute("aria-selected")&&n.removeAttribute("aria-selected");return t&&dt(this.list,t),t}measureInfo(){let e=this.dom.querySelector("[aria-selected]");if(!e||!this.info)return null;let t=this.dom.getBoundingClientRect(),n=this.info.getBoundingClientRect(),o=e.getBoundingClientRect(),s=this.space;if(!s){let l=this.dom.ownerDocument.defaultView||window;s={left:0,top:0,right:l.innerWidth,bottom:l.innerHeight}}return o.top>Math.min(s.bottom,t.bottom)-10||o.bottomn.from||n.from==0))if(s=p,typeof c!="string"&&c.header)o.appendChild(c.header(c));else{let u=o.appendChild(document.createElement("completion-section"));u.textContent=p}}const f=o.appendChild(document.createElement("li"));f.id=t+"-"+l,f.setAttribute("role","option");let h=this.optionClass(r);h&&(f.className=h);for(let p of this.optionContent){let u=p(r,this.view.state,this.view,a);u&&f.appendChild(u)}}return n.from&&o.classList.add("cm-completionListIncompleteTop"),n.tonew pt(t,i,e)}function dt(i,e){let t=i.getBoundingClientRect(),n=e.getBoundingClientRect(),o=t.height/i.offsetHeight;n.topt.bottom&&(i.scrollTop+=(n.bottom-t.bottom)/o)}function we(i){return(i.boost||0)*100+(i.apply?10:0)+(i.info?5:0)+(i.type?1:0)}function mt(i,e){let t=[],n=null,o=c=>{t.push(c);let{section:f}=c.completion;if(f){n||(n=[]);let h=typeof f=="string"?f:f.name;n.some(p=>p.name==h)||n.push(typeof f=="string"?{name:h}:f)}},s=e.facet(y);for(let c of i)if(c.hasResult()){let f=c.result.getMatch;if(c.result.filter===!1)for(let h of c.result.options)o(new me(h,c.source,f?f(h):[],1e9-t.length));else{let h=e.sliceDoc(c.from,c.to),p,u=s.filterStrict?new ct(h):new at(h);for(let g of c.result.options)if(p=u.match(g.label)){let x=g.displayLabel?f?f(g,p.matched):[]:p.matched;o(new me(g,c.source,x,p.score+(g.boost||0)))}}}if(n){let c=Object.create(null),f=0,h=(p,u)=>{var g,x;return((g=p.rank)!==null&&g!==void 0?g:1e9)-((x=u.rank)!==null&&x!==void 0?x:1e9)||(p.nameh.score-f.score||a(f.completion,h.completion))){let f=c.completion;!r||r.label!=f.label||r.detail!=f.detail||r.type!=null&&f.type!=null&&r.type!=f.type||r.apply!=f.apply||r.boost!=f.boost?l.push(c):we(c.completion)>we(r)&&(l[l.length-1]=c),r=c.completion}return l}class D{constructor(e,t,n,o,s,l){this.options=e,this.attrs=t,this.tooltip=n,this.timestamp=o,this.selected=s,this.disabled=l}setSelected(e,t){return e==this.selected||e>=this.options.length?this:new D(this.options,ve(t,e),this.tooltip,this.timestamp,e,this.disabled)}static build(e,t,n,o,s){let l=mt(e,t);if(!l.length)return o&&e.some(a=>a.state==1)?new D(o.options,o.attrs,o.tooltip,o.timestamp,o.selected,!0):null;let r=t.facet(y).selectOnOpen?0:-1;if(o&&o.selected!=r&&o.selected!=-1){let a=o.options[o.selected].completion;for(let c=0;cc.hasResult()?Math.min(a,c.from):a,1e8),create:vt,above:s.aboveCursor},o?o.timestamp:Date.now(),r,!1)}map(e){return new D(this.options,this.attrs,Object.assign(Object.assign({},this.tooltip),{pos:e.mapPos(this.tooltip.pos)}),this.timestamp,this.selected,this.disabled)}}class z{constructor(e,t,n){this.active=e,this.id=t,this.open=n}static start(){return new z(xt,"cm-ac-"+Math.floor(Math.random()*2e6).toString(36),null)}update(e){let{state:t}=e,n=t.facet(y),s=(n.override||t.languageDataAt("autocomplete",T(t)).map(rt)).map(r=>(this.active.find(c=>c.source==r)||new w(r,this.active.some(c=>c.state!=0)?1:0)).update(e,n));s.length==this.active.length&&s.every((r,a)=>r==this.active[a])&&(s=this.active);let l=this.open;l&&e.docChanged&&(l=l.map(e.changes)),e.selection||s.some(r=>r.hasResult()&&e.changes.touchesRange(r.from,r.to))||!gt(s,this.active)?l=D.build(s,t,this.id,l,n):l&&l.disabled&&!s.some(r=>r.state==1)&&(l=null),!l&&s.every(r=>r.state!=1)&&s.some(r=>r.hasResult())&&(s=s.map(r=>r.hasResult()?new w(r.source,0):r));for(let r of e.effects)r.is(te)&&(l=l&&l.setSelected(r.value,this.id));return s==this.active&&l==this.open?this:new z(s,this.id,l)}get tooltip(){return this.open?this.open.tooltip:null}get attrs(){return this.open?this.open.attrs:this.active.length?yt:bt}}function gt(i,e){if(i==e)return!0;for(let t=0,n=0;;){for(;t-1&&(t["aria-activedescendant"]=i+"-"+e),t}const xt=[];function Ce(i,e){if(i.isUserEvent("input.complete")){let n=i.annotation(V);if(n&&e.activateOnCompletion(n))return 12}let t=i.isUserEvent("input.type");return t&&e.activateOnTyping?5:t?1:i.isUserEvent("delete.backward")?2:i.selection?8:i.docChanged?16:0}class w{constructor(e,t,n=-1){this.source=e,this.state=t,this.explicitPos=n}hasResult(){return!1}update(e,t){let n=Ce(e,t),o=this;(n&8||n&16&&this.touches(e))&&(o=new w(o.source,0)),n&4&&o.state==0&&(o=new w(this.source,1)),o=o.updateFor(e,n);for(let s of e.effects)if(s.is(H))o=new w(o.source,1,s.value?T(e.state):-1);else if(s.is(F))o=new w(o.source,0);else if(s.is(Se))for(let l of s.value)l.source==o.source&&(o=l);return o}updateFor(e,t){return this.map(e.changes)}map(e){return e.empty||this.explicitPos<0?this:new w(this.source,this.state,e.mapPos(this.explicitPos))}touches(e){return e.changes.touchesRange(T(e.state))}}class M extends w{constructor(e,t,n,o,s){super(e,2,t),this.result=n,this.from=o,this.to=s}hasResult(){return!0}updateFor(e,t){var n;if(!(t&3))return this.map(e.changes);let o=this.result;o.map&&!e.changes.empty&&(o=o.map(o,e.changes));let s=e.changes.mapPos(this.from),l=e.changes.mapPos(this.to,1),r=T(e.state);if((this.explicitPos<0?r<=s:rl||!o||t&2&&T(e.startState)==this.from)return new w(this.source,t&4?1:0);let a=this.explicitPos<0?-1:e.changes.mapPos(this.explicitPos);return wt(o.validFor,e.state,s,l)?new M(this.source,a,o,s,l):o.update&&(o=o.update(o,s,l,new _(e.state,r,a>=0)))?new M(this.source,a,o,o.from,(n=o.to)!==null&&n!==void 0?n:T(e.state)):new w(this.source,1,a)}map(e){return e.empty?this:(this.result.map?this.result.map(this.result,e):this.result)?new M(this.source,this.explicitPos<0?-1:e.mapPos(this.explicitPos),this.result,e.mapPos(this.from),e.mapPos(this.to,1)):new w(this.source,0)}touches(e){return e.changes.touchesRange(this.from,this.to)}}function wt(i,e,t,n){if(!i)return!1;let o=e.sliceDoc(t,n);return typeof i=="function"?i(o,t,n,e):ge(i,!0).test(o)}const Se=I.define({map(i,e){return i.map(t=>t.map(e))}}),te=I.define(),m=X.define({create(){return z.start()},update(i,e){return i.update(e)},provide:i=>[et.from(i,e=>e.tooltip),R.contentAttributes.from(i,e=>e.attrs)]});function ie(i,e){const t=e.completion.apply||e.completion.label;let n=i.state.field(m).active.find(o=>o.source==e.source);return n instanceof M?(typeof t=="string"?i.dispatch(Object.assign(Object.assign({},ye(i.state,t,n.from,n.to)),{annotations:V.of(e.completion)})):t(i,e.completion,n.from,n.to),!0):!1}const vt=ut(m,ie);function j(i,e="option"){return t=>{let n=t.state.field(m,!1);if(!n||!n.open||n.open.disabled||Date.now()-n.open.timestamp-1?n.open.selected+o*(i?1:-1):i?0:l-1;return r<0?r=e=="page"?0:l-1:r>=l&&(r=e=="page"?l-1:0),t.dispatch({effects:te.of(r)}),!0}}const Ie=i=>{let e=i.state.field(m,!1);return i.state.readOnly||!e||!e.open||e.open.selected<0||e.open.disabled||Date.now()-e.open.timestampi.state.field(m,!1)?(i.dispatch({effects:H.of(!0)}),!0):!1,Te=i=>{let e=i.state.field(m,!1);return!e||!e.active.some(t=>t.state!=0)?!1:(i.dispatch({effects:F.of(null)}),!0)};class Ct{constructor(e,t){this.active=e,this.context=t,this.time=Date.now(),this.updates=[],this.done=void 0}}const St=50,It=1e3,Ot=tt.fromClass(class{constructor(i){this.view=i,this.debounceUpdate=-1,this.running=[],this.debounceAccept=-1,this.pendingStart=!1,this.composing=0;for(let e of i.state.field(m).active)e.state==1&&this.startQuery(e)}update(i){let e=i.state.field(m),t=i.state.facet(y);if(!i.selectionSet&&!i.docChanged&&i.startState.field(m)==e)return;let n=i.transactions.some(s=>{let l=Ce(s,t);return l&8||(s.selection||s.docChanged)&&!(l&3)});for(let s=0;sSt&&Date.now()-l.time>It){for(let r of l.context.abortListeners)try{r()}catch(a){Z(this.view.state,a)}l.context.abortListeners=null,this.running.splice(s--,1)}else l.updates.push(...i.transactions)}this.debounceUpdate>-1&&clearTimeout(this.debounceUpdate),i.transactions.some(s=>s.effects.some(l=>l.is(H)))&&(this.pendingStart=!0);let o=this.pendingStart?50:t.activateOnTypingDelay;if(this.debounceUpdate=e.active.some(s=>s.state==1&&!this.running.some(l=>l.active.source==s.source))?setTimeout(()=>this.startUpdate(),o):-1,this.composing!=0)for(let s of i.transactions)s.isUserEvent("input.type")?this.composing=2:this.composing==2&&s.selection&&(this.composing=3)}startUpdate(){this.debounceUpdate=-1,this.pendingStart=!1;let{state:i}=this.view,e=i.field(m);for(let t of e.active)t.state==1&&!this.running.some(n=>n.active.source==t.source)&&this.startQuery(t)}startQuery(i){let{state:e}=this.view,t=T(e),n=new _(e,t,i.explicitPos==t,this.view),o=new Ct(i,n);this.running.push(o),Promise.resolve(i.source(n)).then(s=>{o.context.aborted||(o.done=s||null,this.scheduleAccept())},s=>{this.view.dispatch({effects:F.of(null)}),Z(this.view.state,s)})}scheduleAccept(){this.running.every(i=>i.done!==void 0)?this.accept():this.debounceAccept<0&&(this.debounceAccept=setTimeout(()=>this.accept(),this.view.state.facet(y).updateSyncTime))}accept(){var i;this.debounceAccept>-1&&clearTimeout(this.debounceAccept),this.debounceAccept=-1;let e=[],t=this.view.state.facet(y);for(let n=0;nl.source==o.active.source);if(s&&s.state==1)if(o.done==null){let l=new w(o.active.source,0);for(let r of o.updates)l=l.update(r,t);l.state!=1&&e.push(l)}else this.startQuery(s)}e.length&&this.view.dispatch({effects:Se.of(e)})}},{eventHandlers:{blur(i){let e=this.view.state.field(m,!1);if(e&&e.tooltip&&this.view.state.facet(y).closeOnBlur){let t=e.open&&he(this.view,e.open.tooltip);(!t||!t.dom.contains(i.relatedTarget))&&setTimeout(()=>this.view.dispatch({effects:F.of(null)}),10)}},compositionstart(){this.composing=1},compositionend(){this.composing==3&&setTimeout(()=>this.view.dispatch({effects:H.of(!1)}),20),this.composing=0}}}),Tt=typeof navigator=="object"&&/Win/.test(navigator.platform),Et=Y.highest(R.domEventHandlers({keydown(i,e){let t=e.state.field(m,!1);if(!t||!t.open||t.open.disabled||t.open.selected<0||i.key.length>1||i.ctrlKey&&!(Tt&&i.altKey)||i.metaKey)return!1;let n=t.open.options[t.open.selected],o=t.active.find(l=>l.source==n.source),s=n.completion.commitCharacters||o.result.commitCharacters;return s&&s.indexOf(i.key)>-1&&ie(e,n),!1}})),Ee=R.baseTheme({".cm-tooltip.cm-tooltip-autocomplete":{"& > ul":{fontFamily:"monospace",whiteSpace:"nowrap",overflow:"hidden auto",maxWidth_fallback:"700px",maxWidth:"min(700px, 95vw)",minWidth:"250px",maxHeight:"10em",height:"100%",listStyle:"none",margin:0,padding:0,"& > li, & > completion-section":{padding:"1px 3px",lineHeight:1.2},"& > li":{overflowX:"hidden",textOverflow:"ellipsis",cursor:"pointer"},"& > completion-section":{display:"list-item",borderBottom:"1px solid silver",paddingLeft:"0.5em",opacity:.7}}},"&light .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#17c",color:"white"},"&light .cm-tooltip-autocomplete-disabled ul li[aria-selected]":{background:"#777"},"&dark .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#347",color:"white"},"&dark .cm-tooltip-autocomplete-disabled ul li[aria-selected]":{background:"#444"},".cm-completionListIncompleteTop:before, .cm-completionListIncompleteBottom:after":{content:'"\xB7\xB7\xB7"',opacity:.5,display:"block",textAlign:"center"},".cm-tooltip.cm-completionInfo":{position:"absolute",padding:"3px 9px",width:"max-content",maxWidth:"400px",boxSizing:"border-box",whiteSpace:"pre-line"},".cm-completionInfo.cm-completionInfo-left":{right:"100%"},".cm-completionInfo.cm-completionInfo-right":{left:"100%"},".cm-completionInfo.cm-completionInfo-left-narrow":{right:"30px"},".cm-completionInfo.cm-completionInfo-right-narrow":{left:"30px"},"&light .cm-snippetField":{backgroundColor:"#00000022"},"&dark .cm-snippetField":{backgroundColor:"#ffffff22"},".cm-snippetFieldPosition":{verticalAlign:"text-top",width:0,height:"1.15em",display:"inline-block",margin:"0 -0.7px -.7em",borderLeft:"1.4px dotted #888"},".cm-completionMatchedText":{textDecoration:"underline"},".cm-completionDetail":{marginLeft:"0.5em",fontStyle:"italic"},".cm-completionIcon":{fontSize:"90%",width:".8em",display:"inline-block",textAlign:"center",paddingRight:".6em",opacity:"0.6",boxSizing:"content-box"},".cm-completionIcon-function, .cm-completionIcon-method":{"&:after":{content:"'\u0192'"}},".cm-completionIcon-class":{"&:after":{content:"'\u25CB'"}},".cm-completionIcon-interface":{"&:after":{content:"'\u25CC'"}},".cm-completionIcon-variable":{"&:after":{content:"'\u{1D465}'"}},".cm-completionIcon-constant":{"&:after":{content:"'\u{1D436}'"}},".cm-completionIcon-type":{"&:after":{content:"'\u{1D461}'"}},".cm-completionIcon-enum":{"&:after":{content:"'\u222A'"}},".cm-completionIcon-property":{"&:after":{content:"'\u25A1'"}},".cm-completionIcon-keyword":{"&:after":{content:"'\u{1F511}\uFE0E'"}},".cm-completionIcon-namespace":{"&:after":{content:"'\u25A2'"}},".cm-completionIcon-text":{"&:after":{content:"'abc'",fontSize:"50%",verticalAlign:"middle"}}});class Pt{constructor(e,t,n,o){this.field=e,this.line=t,this.from=n,this.to=o}}class re{constructor(e,t,n){this.field=e,this.from=t,this.to=n}map(e){let t=e.mapPos(this.from,-1,G.TrackDel),n=e.mapPos(this.to,1,G.TrackDel);return t==null||n==null?null:new re(this.field,t,n)}}class ae{constructor(e,t){this.lines=e,this.fieldPositions=t}instantiate(e,t){let n=[],o=[t],s=e.doc.lineAt(t),l=/^\s*/.exec(s.text)[0];for(let a of this.lines){if(n.length){let c=l,f=/^\t*/.exec(a)[0].length;for(let h=0;hnew re(a.field,o[a.line]+a.from,o[a.line]+a.to));return{text:n,ranges:r}}static parse(e){let t=[],n=[],o=[],s;for(let l of e.split(/\r\n?|\n/)){for(;s=/[#$]\{(?:(\d+)(?::([^}]*))?|((?:\\[{}]|[^}])*))\}/.exec(l);){let r=s[1]?+s[1]:null,a=s[2]||s[3]||"",c=-1,f=a.replace(/\\[{}]/g,h=>h[1]);for(let h=0;h=c&&p.field++}o.push(new Pt(c,n.length,s.index,s.index+f.length)),l=l.slice(0,s.index)+a+l.slice(s.index+s[0].length)}l=l.replace(/\\([{}])/g,(r,a,c)=>{for(let f of o)f.line==n.length&&f.from>c&&(f.from--,f.to--);return a}),n.push(l)}return new ae(n,o)}}let At=U.widget({widget:new class extends it{toDOM(){let i=document.createElement("span");return i.className="cm-snippetFieldPosition",i}ignoreEvent(){return!1}}}),Rt=U.mark({class:"cm-snippetField"});class L{constructor(e,t){this.ranges=e,this.active=t,this.deco=U.set(e.map(n=>(n.from==n.to?At:Rt).range(n.from,n.to)))}map(e){let t=[];for(let n of this.ranges){let o=n.map(e);if(!o)return null;t.push(o)}return new L(t,this.active)}selectionInsideField(e){return e.ranges.every(t=>this.ranges.some(n=>n.field==this.active&&n.from<=t.from&&n.to>=t.to))}}const $=I.define({map(i,e){return i&&i.map(e)}}),Dt=I.define(),E=X.define({create(){return null},update(i,e){for(let t of e.effects){if(t.is($))return t.value;if(t.is(Dt)&&i)return new L(i.ranges,t.value)}return i&&e.docChanged&&(i=i.map(e.changes)),i&&e.selection&&!i.selectionInsideField(e.selection)&&(i=null),i},provide:i=>R.decorations.from(i,e=>e?e.deco:U.none)});function ne(i,e){return v.create(i.filter(t=>t.field==e).map(t=>v.range(t.from,t.to)))}function Pe(i){let e=ae.parse(i);return(t,n,o,s)=>{let{text:l,ranges:r}=e.instantiate(t.state,o),a={changes:{from:o,to:s,insert:Ye.of(l)},scrollIntoView:!0,annotations:n?[V.of(n),Ge.userEvent.of("input.complete")]:void 0};if(r.length&&(a.selection=ne(r,0)),r.some(c=>c.field>0)){let c=new L(r,0),f=a.effects=[$.of(c)];t.state.field(E,!1)===void 0&&f.push(I.appendConfig.of([E,Bt,jt,Ee]))}t.dispatch(t.state.update(a))}}function Ae(i){return({state:e,dispatch:t})=>{let n=e.field(E,!1);if(!n||i<0&&n.active==0)return!1;let o=n.active+i,s=i>0&&!n.ranges.some(l=>l.field==o+i);return t(e.update({selection:ne(n.ranges,o),effects:$.of(s?null:new L(n.ranges,o)),scrollIntoView:!0})),!0}}const Re=({state:i,dispatch:e})=>i.field(E,!1)?(e(i.update({effects:$.of(null)})),!0):!1,De=Ae(1),Le=Ae(-1);function Lt(i){let e=i.field(E,!1);return!!(e&&e.ranges.some(t=>t.field==e.active+1))}function Mt(i){let e=i.field(E,!1);return!!(e&&e.active>0)}const kt=[{key:"Tab",run:De,shift:Le},{key:"Escape",run:Re}],oe=fe.define({combine(i){return i.length?i[0]:kt}}),Bt=Y.highest(pe.compute([oe],i=>i.facet(oe)));function Ft(i,e){return Object.assign(Object.assign({},e),{apply:Pe(i)})}const jt=R.domEventHandlers({mousedown(i,e){let t=e.state.field(E,!1),n;if(!t||(n=e.posAtCoords({x:i.clientX,y:i.clientY}))==null)return!1;let o=t.ranges.find(s=>s.from<=n&&s.to>=n);return!o||o.field==t.active?!1:(e.dispatch({selection:ne(t.ranges,o.field),effects:$.of(t.ranges.some(s=>s.field>o.field)?new L(t.ranges,o.field):null),scrollIntoView:!0}),!0)}});function $t(i){let e=i.replace(/[\]\-\\]/g,"\\$&");try{return new RegExp(`[\\p{Alphabetic}\\p{Number}_${e}]+`,"ug")}catch{return new RegExp(`[w${e}]`,"g")}}function Me(i,e){return new RegExp(e(i.source),i.unicode?"u":"")}const ke=Object.create(null);function Wt(i){return ke[i]||(ke[i]=new WeakMap)}function Be(i,e,t,n,o){for(let s=i.iterLines(),l=0;!s.next().done;){let{value:r}=s,a;for(e.lastIndex=0;a=e.exec(r);)if(!n[a[0]]&&l+a.index!=o&&(t.push({type:"text",label:a[0]}),n[a[0]]=!0,t.length>=2e3))return;l+=r.length+1}}function Fe(i,e,t,n,o){let s=i.length>=1e3,l=s&&e.get(i);if(l)return l;let r=[],a=Object.create(null);if(i.children){let c=0;for(let f of i.children){if(f.length>=1e3)for(let h of Fe(f,e,t,n-c,o-c))a[h.label]||(a[h.label]=!0,r.push(h));else Be(f,t,r,a,o-c);c+=f.length+1}}else Be(i,t,r,a,o);return s&&r.length<2e3&&e.set(i,r),r}const Nt=i=>{let e=i.state.languageDataAt("wordChars",i.pos).join(""),t=$t(e),n=i.matchBefore(Me(t,l=>l+"$"));if(!n&&!i.explicit)return null;let o=n?n.from:i.pos,s=Fe(i.state.doc,Wt(e),t,5e4,o);return{from:o,options:s,validFor:Me(t,l=>"^"+l)}},W={brackets:["(","[","{","'",'"'],before:")]}:;>",stringPrefixes:[]},P=I.define({map(i,e){let t=e.mapPos(i,-1,G.TrackAfter);return t??void 0}}),se=new class extends Je{};se.startSide=1,se.endSide=-1;const je=X.define({create(){return Ze.empty},update(i,e){if(i=i.map(e.changes),e.selection){let t=e.state.doc.lineAt(e.selection.main.head);i=i.update({filter:n=>n>=t.from&&n<=t.to})}for(let t of e.effects)t.is(P)&&(i=i.update({add:[se.range(t.value,t.value+1)]}));return i}});function Ut(){return[Ht,je]}const le="()[]{}<>";function $e(i){for(let e=0;e{if((Vt?i.composing:i.compositionStarted)||i.state.readOnly)return!1;let o=i.state.selection.main;if(n.length>2||n.length==2&&O(C(n,0))==1||e!=o.from||t!=o.to)return!1;let s=Ue(i.state,n);return s?(i.dispatch(s),!0):!1}),Ne=({state:i,dispatch:e})=>{if(i.readOnly)return!1;let n=We(i,i.selection.main.head).brackets||W.brackets,o=null,s=i.changeByRange(l=>{if(l.empty){let r=zt(i.doc,l.head);for(let a of n)if(a==r&&q(i.doc,l.head)==$e(C(a,0)))return{changes:{from:l.head-a.length,to:l.head+a.length},range:v.cursor(l.head-a.length)}}return{range:o=l}});return o||e(i.update(s,{scrollIntoView:!0,userEvent:"delete.backward"})),!o},qt=[{key:"Backspace",run:Ne}];function Ue(i,e){let t=We(i,i.selection.main.head),n=t.brackets||W.brackets;for(let o of n){let s=$e(C(o,0));if(e==o)return s==o?Xt(i,o,n.indexOf(o+o+o)>-1,t):Kt(i,o,s,t.before||W.before);if(e==s&&Ve(i,i.selection.main.from))return Qt(i,o,s)}return null}function Ve(i,e){let t=!1;return i.field(je).between(0,i.doc.length,n=>{n==e&&(t=!0)}),t}function q(i,e){let t=i.sliceString(e,e+2);return t.slice(0,O(C(t,0)))}function zt(i,e){let t=i.sliceString(e-2,e);return O(C(t,0))==t.length?t:t.slice(1)}function Kt(i,e,t,n){let o=null,s=i.changeByRange(l=>{if(!l.empty)return{changes:[{insert:e,from:l.from},{insert:t,from:l.to}],effects:P.of(l.to+e.length),range:v.range(l.anchor+e.length,l.head+e.length)};let r=q(i.doc,l.head);return!r||/\s/.test(r)||n.indexOf(r)>-1?{changes:{insert:e+t,from:l.head},effects:P.of(l.head+e.length),range:v.cursor(l.head+e.length)}:{range:o=l}});return o?null:i.update(s,{scrollIntoView:!0,userEvent:"input.type"})}function Qt(i,e,t){let n=null,o=i.changeByRange(s=>s.empty&&q(i.doc,s.head)==t?{changes:{from:s.head,to:s.head+t.length,insert:t},range:v.cursor(s.head+t.length)}:n={range:s});return n?null:i.update(o,{scrollIntoView:!0,userEvent:"input.type"})}function Xt(i,e,t,n){let o=n.stringPrefixes||W.stringPrefixes,s=null,l=i.changeByRange(r=>{if(!r.empty)return{changes:[{insert:e,from:r.from},{insert:e,from:r.to}],effects:P.of(r.to+e.length),range:v.range(r.anchor+e.length,r.head+e.length)};let a=r.head,c=q(i.doc,a),f;if(c==e){if(He(i,a))return{changes:{insert:e+e,from:a},effects:P.of(a+e.length),range:v.cursor(a+e.length)};if(Ve(i,a)){let p=t&&i.sliceDoc(a,a+e.length*3)==e+e+e?e+e+e:e;return{changes:{from:a,to:a+p.length,insert:p},range:v.cursor(a+p.length)}}}else{if(t&&i.sliceDoc(a-2*e.length,a)==e+e&&(f=qe(i,a-2*e.length,o))>-1&&He(i,f))return{changes:{insert:e+e+e+e,from:a},effects:P.of(a+e.length),range:v.cursor(a+e.length)};if(i.charCategorizer(a)(c)!=J.Word&&qe(i,a,o)>-1&&!Yt(i,a,e,o))return{changes:{insert:e+e,from:a},effects:P.of(a+e.length),range:v.cursor(a+e.length)}}return{range:s=r}});return s?null:i.update(l,{scrollIntoView:!0,userEvent:"input.type"})}function He(i,e){let t=B(i).resolveInner(e+1);return t.parent&&t.from==e}function Yt(i,e,t,n){let o=B(i).resolveInner(e,-1),s=n.reduce((l,r)=>Math.max(l,r.length),0);for(let l=0;l<5;l++){let r=i.sliceDoc(o.from,Math.min(o.to,o.from+t.length+s)),a=r.indexOf(t);if(!a||a>-1&&n.indexOf(r.slice(0,a))>-1){let f=o.firstChild;for(;f&&f.from==o.from&&f.to-f.from>t.length+a;){if(i.sliceDoc(f.to-t.length,f.to)==t)return!1;f=f.firstChild}return!0}let c=o.to==e&&o.parent;if(!c)break;o=c}return!1}function qe(i,e,t){let n=i.charCategorizer(e);if(n(i.sliceDoc(e-1,e))!=J.Word)return e;for(let o of t){let s=e-o.length;if(i.sliceDoc(s,e)==o&&n(i.sliceDoc(s-1,s))!=J.Word)return s}return-1}function Gt(i={}){return[Et,m,y.of(i),Ot,Jt,Ee]}const ze=[{key:"Ctrl-Space",run:Oe},{key:"Escape",run:Te},{key:"ArrowDown",run:j(!0)},{key:"ArrowUp",run:j(!1)},{key:"PageDown",run:j(!0,"page")},{key:"PageUp",run:j(!1,"page")},{key:"Enter",run:Ie}],Jt=Y.highest(pe.computeN([y],i=>i.facet(y).defaultKeymap?[ze]:[]));function Zt(i){let e=i.field(m,!1);return e&&e.active.some(t=>t.state==1)?"pending":e&&e.active.some(t=>t.state!=0)?"active":null}const Ke=new WeakMap;function _t(i){var e;let t=(e=i.field(m,!1))===null||e===void 0?void 0:e.open;if(!t||t.disabled)return[];let n=Ke.get(t.options);return n||Ke.set(t.options,n=t.options.map(o=>o.completion)),n}function ei(i){var e;let t=(e=i.field(m,!1))===null||e===void 0?void 0:e.open;return t&&!t.disabled&&t.selected>=0?t.options[t.selected].completion:null}function ti(i){var e;let t=(e=i.field(m,!1))===null||e===void 0?void 0:e.open;return t&&!t.disabled&&t.selected>=0?t.selected:null}function ii(i){return te.of(i)}export{_ as CompletionContext,Ie as acceptCompletion,Gt as autocompletion,Re as clearSnippet,Ut as closeBrackets,qt as closeBracketsKeymap,Te as closeCompletion,Nt as completeAnyWord,de as completeFromList,ze as completionKeymap,Zt as completionStatus,_t as currentCompletions,Ne as deleteBracketPair,Lt as hasNextSnippetField,Mt as hasPrevSnippetField,st as ifIn,lt as ifNotIn,Ue as insertBracket,ye as insertCompletionText,j as moveCompletionSelection,De as nextSnippetField,V as pickedCompletion,Le as prevSnippetField,ei as selectedCompletion,ti as selectedCompletionIndex,ii as setSelectedCompletion,Pe as snippet,Ft as snippetCompletion,oe as snippetKeymap,Oe as startCompletion}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/commands.js b/Resources/Public/JavaScript/Contrib/@codemirror/commands.js new file mode 100644 index 0000000..e46827e --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/commands.js @@ -0,0 +1 @@ +import{Annotation as Me,Facet as be,combineConfig as qt,StateField as Kt,Transaction as X,ChangeSet as $t,ChangeDesc as _t,EditorSelection as h,StateEffect as Qt,Text as Ee,findClusterBreak as O,countColumn as Y,CharCategory as L}from"@codemirror/state";import{EditorView as M,Direction as Xt}from"@codemirror/view";import{IndentContext as Te,getIndentation as Oe,indentString as U,matchBrackets as b,syntaxTree as Z,getIndentUnit as j,indentUnit as Yt}from"@codemirror/language";import{NodeProp as ee}from"@lezer/common";const Ie=e=>{let{state:t}=e,r=t.doc.lineAt(t.selection.main.from),n=te(e.state,r.from);return n.line?Re(e):n.block?Ue(e):!1};function D(e,t){return({state:r,dispatch:n})=>{if(r.readOnly)return!1;let l=e(t,r);return l?(n(r.update(l)),!0):!1}}const Re=D(ne,0),Zt=D(ne,1),jt=D(ne,2),ve=D(N,0),en=D(N,1),tn=D(N,2),Ue=D((e,t)=>N(e,t,rn(t)),0);function te(e,t){let r=e.languageDataAt("commentTokens",t);return r.length?r[0]:{}}const I=50;function nn(e,{open:t,close:r},n,l){let o=e.sliceDoc(n-I,n),c=e.sliceDoc(l,l+I),s=/\s*$/.exec(o)[0].length,i=/^\s*/.exec(c)[0].length,f=o.length-s;if(o.slice(f-t.length,f)==t&&c.slice(i,i+r.length)==r)return{open:{pos:n-s,margin:s&&1},close:{pos:l+i,margin:i&&1}};let u,a;l-n<=2*I?u=a=e.sliceDoc(n,l):(u=e.sliceDoc(n,n+I),a=e.sliceDoc(l-I,l));let d=/^\s*/.exec(u)[0].length,B=/\s*$/.exec(a)[0].length,g=a.length-B-r.length;return u.slice(d,d+t.length)==t&&a.slice(g,g+r.length)==r?{open:{pos:n+d+t.length,margin:/\s/.test(u.charAt(d+t.length))?1:0},close:{pos:l-B-r.length,margin:/\s/.test(a.charAt(g-1))?1:0}}:null}function rn(e){let t=[];for(let r of e.selection.ranges){let n=e.doc.lineAt(r.from),l=r.to<=n.to?n:e.doc.lineAt(r.to),o=t.length-1;o>=0&&t[o].to>n.from?t[o].to=l.to:t.push({from:n.from+/^\s*/.exec(n.text)[0].length,to:l.to})}return t}function N(e,t,r=t.selection.ranges){let n=r.map(o=>te(t,o.from).block);if(!n.every(o=>o))return null;let l=r.map((o,c)=>nn(t,n[c],o.from,o.to));if(e!=2&&!l.every(o=>o))return{changes:t.changes(r.map((o,c)=>l[c]?[]:[{from:o.from,insert:n[c].open+" "},{from:o.to,insert:" "+n[c].close}]))};if(e!=1&&l.some(o=>o)){let o=[];for(let c=0,s;cl&&(o==c||c>a.from)){l=a.from;let d=/^\s*/.exec(a.text)[0].length,B=d==a.length,g=a.text.slice(d,d+f.length)==f?d:-1;do.comment<0&&(!o.empty||o.single))){let o=[];for(let{line:s,token:i,indent:f,empty:u,single:a}of n)(a||!u)&&o.push({from:s.from+f,insert:i+" "});let c=t.changes(o);return{changes:c,selection:t.selection.map(c,1)}}else if(e!=1&&n.some(o=>o.comment>=0)){let o=[];for(let{line:c,comment:s,token:i}of n)if(s>=0){let f=c.from+s,u=f+i.length;c.text[u-c.from]==" "&&u++,o.push({from:f,to:u})}return{changes:o}}return null}const re=Me.define(),Ne=Me.define(),Ve=be.define(),Fe=be.define({combine(e){return qt(e,{minDepth:100,newGroupDelay:500,joinToEvent:(t,r)=>r},{minDepth:Math.max,newGroupDelay:Math.min,joinToEvent:(t,r)=>(n,l)=>t(n,l)||r(n,l)})}}),V=Kt.define({create(){return C.empty},update(e,t){let r=t.state.facet(Fe),n=t.annotation(re);if(n){let i=p.fromTransaction(t,n.selection),f=n.side,u=f==0?e.undone:e.done;return i?u=P(u,u.length,r.minDepth,i):u=ze(u,t.startState.selection),new C(f==0?n.rest:u,f==0?u:n.rest)}let l=t.annotation(Ne);if((l=="full"||l=="before")&&(e=e.isolate()),t.annotation(X.addToHistory)===!1)return t.changes.empty?e:e.addMapping(t.changes.desc);let o=p.fromTransaction(t),c=t.annotation(X.time),s=t.annotation(X.userEvent);return o?e=e.addChanges(o,c,s,r,t):t.selection&&(e=e.addSelection(t.startState.selection,c,s,r.newGroupDelay)),(l=="full"||l=="after")&&(e=e.isolate()),e},toJSON(e){return{done:e.done.map(t=>t.toJSON()),undone:e.undone.map(t=>t.toJSON())}},fromJSON(e){return new C(e.done.map(p.fromJSON),e.undone.map(p.fromJSON))}});function on(e={}){return[V,Fe.of(e),M.domEventHandlers({beforeinput(t,r){let n=t.inputType=="historyUndo"?oe:t.inputType=="historyRedo"?G:null;return n?(t.preventDefault(),n(r)):!1}})]}const ln=V;function F(e,t){return function({state:r,dispatch:n}){if(!t&&r.readOnly)return!1;let l=r.field(V,!1);if(!l)return!1;let o=l.pop(e,r,t);return o?(n(o),!0):!1}}const oe=F(0,!1),G=F(1,!1),Ge=F(0,!0),Pe=F(1,!0);function Je(e){return function(t){let r=t.field(V,!1);if(!r)return 0;let n=e==0?r.done:r.undone;return n.length-(n.length&&!n[0].changes?1:0)}}const cn=Je(0),sn=Je(1);class p{constructor(t,r,n,l,o){this.changes=t,this.effects=r,this.mapped=n,this.startSelection=l,this.selectionsAfter=o}setSelAfter(t){return new p(this.changes,this.effects,this.mapped,this.startSelection,t)}toJSON(){var t,r,n;return{changes:(t=this.changes)===null||t===void 0?void 0:t.toJSON(),mapped:(r=this.mapped)===null||r===void 0?void 0:r.toJSON(),startSelection:(n=this.startSelection)===null||n===void 0?void 0:n.toJSON(),selectionsAfter:this.selectionsAfter.map(l=>l.toJSON())}}static fromJSON(t){return new p(t.changes&&$t.fromJSON(t.changes),[],t.mapped&&_t.fromJSON(t.mapped),t.startSelection&&h.fromJSON(t.startSelection),t.selectionsAfter.map(h.fromJSON))}static fromTransaction(t,r){let n=k;for(let l of t.startState.facet(Ve)){let o=l(t);o.length&&(n=n.concat(o))}return!n.length&&t.changes.empty?null:new p(t.changes.invert(t.startState.doc),n,void 0,r||t.startState.selection,k)}static selection(t){return new p(void 0,k,void 0,void 0,t)}}function P(e,t,r,n){let l=t+1>r+20?t-r-1:0,o=e.slice(l,t);return o.push(n),o}function un(e,t){let r=[],n=!1;return e.iterChangedRanges((l,o)=>r.push(l,o)),t.iterChangedRanges((l,o,c,s)=>{for(let i=0;i=f&&c<=u&&(n=!0)}}),n}function fn(e,t){return e.ranges.length==t.ranges.length&&e.ranges.filter((r,n)=>r.empty!=t.ranges[n].empty).length===0}function we(e,t){return e.length?t.length?e.concat(t):e:t}const k=[],an=200;function ze(e,t){if(e.length){let r=e[e.length-1],n=r.selectionsAfter.slice(Math.max(0,r.selectionsAfter.length-an));return n.length&&n[n.length-1].eq(t)?e:(n.push(t),P(e,e.length-1,1e9,r.setSelAfter(n)))}else return[p.selection([t])]}function hn(e){let t=e[e.length-1],r=e.slice();return r[e.length-1]=t.setSelAfter(t.selectionsAfter.slice(0,t.selectionsAfter.length-1)),r}function le(e,t){if(!e.length)return e;let r=e.length,n=k;for(;r;){let l=dn(e[r-1],t,n);if(l.changes&&!l.changes.empty||l.effects.length){let o=e.slice(0,r);return o[r-1]=l,o}else t=l.mapped,r--,n=l.selectionsAfter}return n.length?[p.selection(n)]:k}function dn(e,t,r){let n=we(e.selectionsAfter.length?e.selectionsAfter.map(s=>s.map(t)):k,r);if(!e.changes)return p.selection(n);let l=e.changes.map(t),o=t.mapDesc(e.changes,!0),c=e.mapped?e.mapped.composeDesc(o):o;return new p(l,Qt.mapEffects(e.effects,t),c,e.startSelection.map(o),n)}const mn=/^(input\.type|delete)($|\.)/;class C{constructor(t,r,n=0,l=void 0){this.done=t,this.undone=r,this.prevTime=n,this.prevUserEvent=l}isolate(){return this.prevTime?new C(this.done,this.undone):this}addChanges(t,r,n,l,o){let c=this.done,s=c[c.length-1];return s&&s.changes&&!s.changes.empty&&t.changes&&(!n||mn.test(n))&&(!s.selectionsAfter.length&&r-this.prevTime0&&r-this.prevTimer.empty?e.moveByChar(r,t):R(r,t))}function m(e){return e.textDirectionAt(e.state.selection.main.head)==Xt.LTR}const ce=e=>J(e,!m(e)),se=e=>J(e,m(e)),gn=e=>J(e,!0),yn=e=>J(e,!1);function w(e,t){return A(e,r=>r.empty?e.moveByGroup(r,t):R(r,t))}const We=e=>w(e,!m(e)),He=e=>w(e,m(e)),kn=e=>w(e,!0),An=e=>w(e,!1),qe=typeof Intl<"u"&&Intl.Segmenter?new Intl.Segmenter(void 0,{granularity:"word"}):null;function Ke(e,t,r){let n=e.state.charCategorizer(t.from),l=L.Space,o=t.from,c=0,s=!1,i=!1,f=!1,u=d=>{if(s)return!1;o+=r?d.length:-d.length;let B=n(d),g;if(B==L.Word&&d.charCodeAt(0)<128&&/[\W_]/.test(d)&&(B=-1),l==L.Space&&(l=B),l!=B)return!1;if(l==L.Word)if(d.toLowerCase()==d){if(!r&&i)return!1;f=!0}else if(f){if(r)return!1;s=!0}else{if(i&&r&&n(g=e.state.sliceDoc(o,o+1))==L.Word&&g.toLowerCase()==g)return!1;i=!0}return c++,!0},a=e.moveByChar(t,r,d=>(u(d),u));if(qe&&l==L.Word&&a.from==t.from+c*(r?1:-1)){let d=Math.min(t.head,a.head),B=Math.max(t.head,a.head),g=e.state.sliceDoc(d,B);if(g.length>1&&/[\u4E00-\uffff]/.test(g)){let v=Array.from(qe.segment(g));if(v.length>1)return r?h.cursor(t.head+v[1].index,-1):h.cursor(a.head+v[v.length-1].index,1)}}return a}function $e(e,t){return A(e,r=>r.empty?Ke(e,r,t):R(r,t))}const Bn=e=>$e(e,!0),Sn=e=>$e(e,!1);function Cn(e,t,r){if(t.type.prop(r))return!0;let n=t.to-t.from;return n&&(n>2||/[^\s,.;:]/.test(e.sliceDoc(t.from,t.to)))||t.firstChild}function z(e,t,r){let n=Z(e).resolveInner(t.head),l=r?ee.closedBy:ee.openedBy;for(let i=t.head;;){let f=r?n.childAfter(i):n.childBefore(i);if(!f)break;Cn(e,f,l)?n=f:i=r?f.to:f.from}let o=n.type.prop(l),c,s;return o&&(c=r?b(e,n.from,1):b(e,n.to,-1))&&c.matched?s=r?c.end.to:c.end.from:s=r?n.to:n.from,h.cursor(s,r?-1:1)}const _e=e=>A(e,t=>z(e.state,t,!m(e))),Qe=e=>A(e,t=>z(e.state,t,m(e)));function Xe(e,t){return A(e,r=>{if(!r.empty)return R(r,t);let n=e.moveVertically(r,t);return n.head!=r.head?n:e.moveToLineBoundary(r,t)})}const ie=e=>Xe(e,!1),ue=e=>Xe(e,!0);function Ye(e){let t=e.scrollDOM.clientHeightc.empty?e.moveVertically(c,t,r.height):R(c,t));if(l.eq(n.selection))return!1;let o;if(r.selfScroll){let c=e.coordsAtPos(n.selection.main.head),s=e.scrollDOM.getBoundingClientRect(),i=s.top+r.marginTop,f=s.bottom-r.marginBottom;c&&c.top>i&&c.bottomZe(e,!1),W=e=>Ze(e,!0);function x(e,t,r){let n=e.lineBlockAt(t.head),l=e.moveToLineBoundary(t,r);if(l.head==t.head&&l.head!=(r?n.to:n.from)&&(l=e.moveToLineBoundary(t,r,!1)),!r&&l.head==n.from&&n.length){let o=/^\s*/.exec(e.state.sliceDoc(n.from,Math.min(n.from+100,n.to)))[0].length;o&&t.head!=n.from+o&&(l=h.cursor(n.from+o))}return l}const je=e=>A(e,t=>x(e,t,!0)),et=e=>A(e,t=>x(e,t,!1)),tt=e=>A(e,t=>x(e,t,!m(e))),nt=e=>A(e,t=>x(e,t,m(e))),rt=e=>A(e,t=>h.cursor(e.lineBlockAt(t.head).from,1)),ot=e=>A(e,t=>h.cursor(e.lineBlockAt(t.head).to,-1));function lt(e,t,r){let n=!1,l=E(e.selection,o=>{let c=b(e,o.head,-1)||b(e,o.head,1)||o.head>0&&b(e,o.head-1,1)||o.headlt(e,t,!1),xn=({state:e,dispatch:t})=>lt(e,t,!0);function y(e,t){let r=E(e.state.selection,n=>{let l=t(n);return h.range(n.anchor,l.head,l.goalColumn,l.bidiLevel||void 0)});return r.eq(e.state.selection)?!1:(e.dispatch(S(e.state,r)),!0)}function H(e,t){return y(e,r=>e.moveByChar(r,t))}const ae=e=>H(e,!m(e)),he=e=>H(e,m(e)),Dn=e=>H(e,!0),Ln=e=>H(e,!1);function q(e,t){return y(e,r=>e.moveByGroup(r,t))}const st=e=>q(e,!m(e)),it=e=>q(e,m(e)),Mn=e=>q(e,!0),bn=e=>q(e,!1);function ut(e,t){return y(e,r=>Ke(e,r,t))}const En=e=>ut(e,!0),Tn=e=>ut(e,!1),ft=e=>y(e,t=>z(e.state,t,!m(e))),at=e=>y(e,t=>z(e.state,t,m(e)));function ht(e,t){return y(e,r=>e.moveVertically(r,t))}const de=e=>ht(e,!1),me=e=>ht(e,!0);function dt(e,t){return y(e,r=>e.moveVertically(r,t,Ye(e).height))}const pe=e=>dt(e,!1),ge=e=>dt(e,!0),mt=e=>y(e,t=>x(e,t,!0)),pt=e=>y(e,t=>x(e,t,!1)),gt=e=>y(e,t=>x(e,t,!m(e))),yt=e=>y(e,t=>x(e,t,m(e))),kt=e=>y(e,t=>h.cursor(e.lineBlockAt(t.head).from)),At=e=>y(e,t=>h.cursor(e.lineBlockAt(t.head).to)),ye=({state:e,dispatch:t})=>(t(S(e,{anchor:0})),!0),ke=({state:e,dispatch:t})=>(t(S(e,{anchor:e.doc.length})),!0),Ae=({state:e,dispatch:t})=>(t(S(e,{anchor:e.selection.main.anchor,head:0})),!0),Be=({state:e,dispatch:t})=>(t(S(e,{anchor:e.selection.main.anchor,head:e.doc.length})),!0),Bt=({state:e,dispatch:t})=>(t(e.update({selection:{anchor:0,head:e.doc.length},userEvent:"select"})),!0),St=({state:e,dispatch:t})=>{let r=_(e).map(({from:n,to:l})=>h.range(n,Math.min(l+1,e.doc.length)));return t(e.update({selection:h.create(r),userEvent:"select"})),!0},Ct=({state:e,dispatch:t})=>{let r=E(e.selection,n=>{var l;let o=Z(e).resolveStack(n.from,1);for(let c=o;c;c=c.next){let{node:s}=c;if((s.from=n.to||s.to>n.to&&s.from<=n.from)&&(!((l=s.parent)===null||l===void 0)&&l.parent))return h.range(s.to,s.from)}return n});return t(S(e,r)),!0},xt=({state:e,dispatch:t})=>{let r=e.selection,n=null;return r.ranges.length>1?n=h.create([r.main]):r.main.empty||(n=h.create([h.cursor(r.main.head)])),n?(t(S(e,n)),!0):!1};function T(e,t){if(e.state.readOnly)return!1;let r="delete.selection",{state:n}=e,l=n.changeByRange(o=>{let{from:c,to:s}=o;if(c==s){let i=t(o);ic&&(r="delete.forward",i=K(e,i,!0)),c=Math.min(c,i),s=Math.max(s,i)}else c=K(e,c,!1),s=K(e,s,!0);return c==s?{range:o}:{changes:{from:c,to:s},range:h.cursor(c,cl(e)))n.between(t,t,(l,o)=>{lt&&(t=r?o:l)});return t}const Se=(e,t,r)=>T(e,n=>{let l=n.from,{state:o}=e,c=o.doc.lineAt(l),s,i;if(r&&!t&&l>c.from&&lSe(e,!1,!0),On=e=>Se(e,!1,!1),Ce=e=>Se(e,!0,!1),Dt=(e,t)=>T(e,r=>{let n=r.head,{state:l}=e,o=l.doc.lineAt(n),c=l.charCategorizer(n);for(let s=null;;){if(n==(t?o.to:o.from)){n==r.head&&o.number!=(t?l.doc.lines:1)&&(n+=t?1:-1);break}let i=O(o.text,n-o.from,t)+o.from,f=o.text.slice(Math.min(n,i)-o.from,Math.max(n,i)-o.from),u=c(f);if(s!=null&&u!=s)break;(f!=" "||n!=r.head)&&(s=u),n=i}return n}),xe=e=>Dt(e,!1),Lt=e=>Dt(e,!0),Mt=e=>T(e,t=>{let r=e.lineBlockAt(t.head).to;return t.headT(e,t=>{let r=e.lineBlockAt(t.head).from;return t.head>r?r:Math.max(0,t.head-1)}),bt=e=>T(e,t=>{let r=e.moveToLineBoundary(t,!1).head;return t.head>r?r:Math.max(0,t.head-1)}),Et=e=>T(e,t=>{let r=e.moveToLineBoundary(t,!0).head;return t.head{if(e.readOnly)return!1;let r=[];for(let n=0,l="",o=e.doc.iter();;){if(o.next(),o.lineBreak||o.done){let c=l.search(/\s+$/);if(c>-1&&r.push({from:n-(l.length-c),to:n}),o.done)break;l=""}else l=o.value;n+=o.value.length}return r.length?(t(e.update({changes:r,userEvent:"delete"})),!0):!1},Tt=({state:e,dispatch:t})=>{if(e.readOnly)return!1;let r=e.changeByRange(n=>({changes:{from:n.from,to:n.to,insert:Ee.of(["",""])},range:h.cursor(n.from)}));return t(e.update(r,{scrollIntoView:!0,userEvent:"input"})),!0},Ot=({state:e,dispatch:t})=>{if(e.readOnly)return!1;let r=e.changeByRange(n=>{if(!n.empty||n.from==0||n.from==e.doc.length)return{range:n};let l=n.from,o=e.doc.lineAt(l),c=l==o.from?l-1:O(o.text,l-o.from,!1)+o.from,s=l==o.to?l+1:O(o.text,l-o.from,!0)+o.from;return{changes:{from:c,to:s,insert:e.doc.slice(l,s).append(e.doc.slice(c,l))},range:h.cursor(s)}});return r.changes.empty?!1:(t(e.update(r,{scrollIntoView:!0,userEvent:"move.character"})),!0)};function _(e){let t=[],r=-1;for(let n of e.selection.ranges){let l=e.doc.lineAt(n.from),o=e.doc.lineAt(n.to);if(!n.empty&&n.to==o.from&&(o=e.doc.lineAt(n.to-1)),r>=l.number){let c=t[t.length-1];c.to=o.to,c.ranges.push(n)}else t.push({from:l.from,to:o.to,ranges:[n]});r=o.number+1}return t}function It(e,t,r){if(e.readOnly)return!1;let n=[],l=[];for(let o of _(e)){if(r?o.to==e.doc.length:o.from==0)continue;let c=e.doc.lineAt(r?o.to+1:o.from-1),s=c.length+1;if(r){n.push({from:o.to,to:c.to},{from:o.from,insert:c.text+e.lineBreak});for(let i of o.ranges)l.push(h.range(Math.min(e.doc.length,i.anchor+s),Math.min(e.doc.length,i.head+s)))}else{n.push({from:c.from,to:o.from},{from:o.to,insert:e.lineBreak+c.text});for(let i of o.ranges)l.push(h.range(i.anchor-s,i.head-s))}}return n.length?(t(e.update({changes:n,scrollIntoView:!0,selection:h.create(l,e.selection.mainIndex),userEvent:"move.line"})),!0):!1}const Rt=({state:e,dispatch:t})=>It(e,t,!1),vt=({state:e,dispatch:t})=>It(e,t,!0);function Ut(e,t,r){if(e.readOnly)return!1;let n=[];for(let l of _(e))r?n.push({from:l.from,insert:e.doc.slice(l.from,l.to)+e.lineBreak}):n.push({from:l.to,insert:e.lineBreak+e.doc.slice(l.from,l.to)});return t(e.update({changes:n,scrollIntoView:!0,userEvent:"input.copyline"})),!0}const Nt=({state:e,dispatch:t})=>Ut(e,t,!1),Vt=({state:e,dispatch:t})=>Ut(e,t,!0),Ft=e=>{if(e.state.readOnly)return!1;let{state:t}=e,r=t.changes(_(t).map(({from:l,to:o})=>(l>0?l--:o{let o;if(e.lineWrapping){let c=e.lineBlockAt(l.head),s=e.coordsAtPos(l.head,l.assoc||1);s&&(o=c.bottom+e.documentTop-s.bottom+e.defaultLineHeight/2)}return e.moveVertically(l,!0,o)}).map(r);return e.dispatch({changes:r,selection:n,scrollIntoView:!0,userEvent:"delete.line"}),!0},vn=({state:e,dispatch:t})=>(t(e.update(e.replaceSelection(e.lineBreak),{scrollIntoView:!0,userEvent:"input"})),!0),Un=({state:e,dispatch:t})=>(t(e.update(e.changeByRange(r=>{let n=/^\s*/.exec(e.doc.lineAt(r.from).text)[0];return{changes:{from:r.from,to:r.to,insert:e.lineBreak+n},range:h.cursor(r.from+n.length+1)}}),{scrollIntoView:!0,userEvent:"input"})),!0);function Nn(e,t){if(/\(\)|\[\]|\{\}/.test(e.sliceDoc(t-1,t+1)))return{from:t,to:t};let r=Z(e).resolveInner(t),n=r.childBefore(t),l=r.childAfter(t),o;return n&&l&&n.to<=t&&l.from>=t&&(o=n.type.prop(ee.closedBy))&&o.indexOf(l.name)>-1&&e.doc.lineAt(n.to).from==e.doc.lineAt(l.from).from&&!/\S/.test(e.sliceDoc(n.to,l.from))?{from:n.to,to:l.from}:null}const Gt=Jt(!1),Pt=Jt(!0);function Jt(e){return({state:t,dispatch:r})=>{if(t.readOnly)return!1;let n=t.changeByRange(l=>{let{from:o,to:c}=l,s=t.doc.lineAt(o),i=!e&&o==c&&Nn(t,o);e&&(o=c=(c<=s.to?s:t.doc.lineAt(c)).to);let f=new Te(t,{simulateBreak:o,simulateDoubleBreak:!!i}),u=Oe(f,o);for(u==null&&(u=Y(/^\s*/.exec(t.doc.lineAt(o).text)[0],t.tabSize));cs.from&&o{let l=[];for(let c=n.from;c<=n.to;){let s=e.doc.lineAt(c);s.number>r&&(n.empty||n.to>s.from)&&(t(s,l,n),r=s.number),c=s.to+1}let o=e.changes(l);return{changes:l,range:h.range(o.mapPos(n.anchor,1),o.mapPos(n.head,1))}})}const wt=({state:e,dispatch:t})=>{if(e.readOnly)return!1;let r=Object.create(null),n=new Te(e,{overrideIndentation:o=>{let c=r[o];return c??-1}}),l=De(e,(o,c,s)=>{let i=Oe(n,o.from);if(i==null)return;/\S/.test(o.text)||(i=0);let f=/^\s*/.exec(o.text)[0],u=U(e,i);(f!=u||s.frome.readOnly?!1:(t(e.update(De(e,(r,n)=>{n.push({from:r.from,insert:e.facet(Yt)})}),{userEvent:"input.indent"})),!0),Le=({state:e,dispatch:t})=>e.readOnly?!1:(t(e.update(De(e,(r,n)=>{let l=/^\s*/.exec(r.text)[0];if(!l)return;let o=Y(l,e.tabSize),c=0,s=U(e,Math.max(0,o-j(e)));for(;c(e.setTabFocusMode(),!0),Vn=e=>(e.setTabFocusMode(2e3),!0),Fn=({state:e,dispatch:t})=>e.selection.ranges.some(r=>!r.empty)?Q({state:e,dispatch:t}):(t(e.update(e.replaceSelection(" "),{scrollIntoView:!0,userEvent:"input"})),!0),Wt=[{key:"Ctrl-b",run:ce,shift:ae,preventDefault:!0},{key:"Ctrl-f",run:se,shift:he},{key:"Ctrl-p",run:ie,shift:de},{key:"Ctrl-n",run:ue,shift:me},{key:"Ctrl-a",run:rt,shift:kt},{key:"Ctrl-e",run:ot,shift:At},{key:"Ctrl-d",run:Ce},{key:"Ctrl-h",run:$},{key:"Ctrl-k",run:Mt},{key:"Ctrl-Alt-h",run:xe},{key:"Ctrl-o",run:Tt},{key:"Ctrl-t",run:Ot},{key:"Ctrl-v",run:W}],Ht=[{key:"ArrowLeft",run:ce,shift:ae,preventDefault:!0},{key:"Mod-ArrowLeft",mac:"Alt-ArrowLeft",run:We,shift:st,preventDefault:!0},{mac:"Cmd-ArrowLeft",run:tt,shift:gt,preventDefault:!0},{key:"ArrowRight",run:se,shift:he,preventDefault:!0},{key:"Mod-ArrowRight",mac:"Alt-ArrowRight",run:He,shift:it,preventDefault:!0},{mac:"Cmd-ArrowRight",run:nt,shift:yt,preventDefault:!0},{key:"ArrowUp",run:ie,shift:de,preventDefault:!0},{mac:"Cmd-ArrowUp",run:ye,shift:Ae},{mac:"Ctrl-ArrowUp",run:fe,shift:pe},{key:"ArrowDown",run:ue,shift:me,preventDefault:!0},{mac:"Cmd-ArrowDown",run:ke,shift:Be},{mac:"Ctrl-ArrowDown",run:W,shift:ge},{key:"PageUp",run:fe,shift:pe},{key:"PageDown",run:W,shift:ge},{key:"Home",run:et,shift:pt,preventDefault:!0},{key:"Mod-Home",run:ye,shift:Ae},{key:"End",run:je,shift:mt,preventDefault:!0},{key:"Mod-End",run:ke,shift:Be},{key:"Enter",run:Gt},{key:"Mod-a",run:Bt},{key:"Backspace",run:$,shift:$},{key:"Delete",run:Ce},{key:"Mod-Backspace",mac:"Alt-Backspace",run:xe},{key:"Mod-Delete",mac:"Alt-Delete",run:Lt},{mac:"Mod-Backspace",run:bt},{mac:"Mod-Delete",run:Et}].concat(Wt.map(e=>({mac:e.key,run:e.run,shift:e.shift}))),Gn=[{key:"Alt-ArrowLeft",mac:"Ctrl-ArrowLeft",run:_e,shift:ft},{key:"Alt-ArrowRight",mac:"Ctrl-ArrowRight",run:Qe,shift:at},{key:"Alt-ArrowUp",run:Rt},{key:"Shift-Alt-ArrowUp",run:Nt},{key:"Alt-ArrowDown",run:vt},{key:"Shift-Alt-ArrowDown",run:Vt},{key:"Escape",run:xt},{key:"Mod-Enter",run:Pt},{key:"Alt-l",mac:"Ctrl-l",run:St},{key:"Mod-i",run:Ct,preventDefault:!0},{key:"Mod-[",run:Le},{key:"Mod-]",run:Q},{key:"Mod-Alt-\\",run:wt},{key:"Shift-Mod-k",run:Ft},{key:"Shift-Mod-\\",run:ct},{key:"Mod-/",run:Ie},{key:"Alt-A",run:ve},{key:"Ctrl-m",mac:"Shift-Alt-m",run:zt}].concat(Ht),Pn={key:"Tab",run:Q,shift:Le};export{en as blockComment,tn as blockUncomment,Vt as copyLineDown,Nt as copyLineUp,yn as cursorCharBackward,gn as cursorCharForward,ce as cursorCharLeft,se as cursorCharRight,ke as cursorDocEnd,ye as cursorDocStart,An as cursorGroupBackward,kn as cursorGroupForward,We as cursorGroupLeft,He as cursorGroupRight,et as cursorLineBoundaryBackward,je as cursorLineBoundaryForward,tt as cursorLineBoundaryLeft,nt as cursorLineBoundaryRight,ue as cursorLineDown,ot as cursorLineEnd,rt as cursorLineStart,ie as cursorLineUp,ct as cursorMatchingBracket,W as cursorPageDown,fe as cursorPageUp,Sn as cursorSubwordBackward,Bn as cursorSubwordForward,_e as cursorSyntaxLeft,Qe as cursorSyntaxRight,Gn as defaultKeymap,$ as deleteCharBackward,On as deleteCharBackwardStrict,Ce as deleteCharForward,xe as deleteGroupBackward,Lt as deleteGroupForward,Ft as deleteLine,bt as deleteLineBoundaryBackward,Et as deleteLineBoundaryForward,Mt as deleteToLineEnd,In as deleteToLineStart,Rn as deleteTrailingWhitespace,Wt as emacsStyleKeymap,on as history,ln as historyField,pn as historyKeymap,Le as indentLess,Q as indentMore,wt as indentSelection,Pn as indentWithTab,Pt as insertBlankLine,vn as insertNewline,Gt as insertNewlineAndIndent,Un as insertNewlineKeepIndent,Fn as insertTab,Ve as invertedEffects,Ne as isolateHistory,Zt as lineComment,jt as lineUncomment,vt as moveLineDown,Rt as moveLineUp,G as redo,sn as redoDepth,Pe as redoSelection,Bt as selectAll,Ln as selectCharBackward,Dn as selectCharForward,ae as selectCharLeft,he as selectCharRight,Be as selectDocEnd,Ae as selectDocStart,bn as selectGroupBackward,Mn as selectGroupForward,st as selectGroupLeft,it as selectGroupRight,St as selectLine,pt as selectLineBoundaryBackward,mt as selectLineBoundaryForward,gt as selectLineBoundaryLeft,yt as selectLineBoundaryRight,me as selectLineDown,At as selectLineEnd,kt as selectLineStart,de as selectLineUp,xn as selectMatchingBracket,ge as selectPageDown,pe as selectPageUp,Ct as selectParentSyntax,Tn as selectSubwordBackward,En as selectSubwordForward,ft as selectSyntaxLeft,at as selectSyntaxRight,xt as simplifySelection,Tt as splitLine,Ht as standardKeymap,Vn as temporarilySetTabFocusMode,ve as toggleBlockComment,Ue as toggleBlockCommentByLine,Ie as toggleComment,Re as toggleLineComment,zt as toggleTabFocusMode,Ot as transposeChars,oe as undo,cn as undoDepth,Ge as undoSelection}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/lang-css.js b/Resources/Public/JavaScript/Contrib/@codemirror/lang-css.js new file mode 100644 index 0000000..03d816f --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/lang-css.js @@ -0,0 +1 @@ +import{parser as v}from"@lezer/css";import{syntaxTree as y,LRLanguage as k,indentNodeProp as x,continuedIndent as z,foldNodeProp as S,foldInside as q,LanguageSupport as C}from"@codemirror/language";import{NodeWeakMap as F,IterMode as N}from"@lezer/common";let c=null;function u(){if(!c&&typeof document=="object"&&document.body){let{style:r}=document.body,a=[],o=new Set;for(let t in r)t!="cssText"&&t!="cssFloat"&&typeof r[t]=="string"&&(/[A-Z]/.test(t)&&(t=t.replace(/[A-Z]/g,e=>"-"+e.toLowerCase())),o.has(t)||(a.push(t),o.add(t)));c=a.sort().map(t=>({type:"property",label:t}))}return c||[]}const m=["active","after","any-link","autofill","backdrop","before","checked","cue","default","defined","disabled","empty","enabled","file-selector-button","first","first-child","first-letter","first-line","first-of-type","focus","focus-visible","focus-within","fullscreen","has","host","host-context","hover","in-range","indeterminate","invalid","is","lang","last-child","last-of-type","left","link","marker","modal","not","nth-child","nth-last-child","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","part","placeholder","placeholder-shown","read-only","read-write","required","right","root","scope","selection","slotted","target","target-text","valid","visited","where"].map(r=>({type:"class",label:r})),h=["above","absolute","activeborder","additive","activecaption","after-white-space","ahead","alias","all","all-scroll","alphabetic","alternate","always","antialiased","appworkspace","asterisks","attr","auto","auto-flow","avoid","avoid-column","avoid-page","avoid-region","axis-pan","background","backwards","baseline","below","bidi-override","blink","block","block-axis","bold","bolder","border","border-box","both","bottom","break","break-all","break-word","bullets","button","button-bevel","buttonface","buttonhighlight","buttonshadow","buttontext","calc","capitalize","caps-lock-indicator","caption","captiontext","caret","cell","center","checkbox","circle","cjk-decimal","clear","clip","close-quote","col-resize","collapse","color","color-burn","color-dodge","column","column-reverse","compact","condensed","contain","content","contents","content-box","context-menu","continuous","copy","counter","counters","cover","crop","cross","crosshair","currentcolor","cursive","cyclic","darken","dashed","decimal","decimal-leading-zero","default","default-button","dense","destination-atop","destination-in","destination-out","destination-over","difference","disc","discard","disclosure-closed","disclosure-open","document","dot-dash","dot-dot-dash","dotted","double","down","e-resize","ease","ease-in","ease-in-out","ease-out","element","ellipse","ellipsis","embed","end","ethiopic-abegede-gez","ethiopic-halehame-aa-er","ethiopic-halehame-gez","ew-resize","exclusion","expanded","extends","extra-condensed","extra-expanded","fantasy","fast","fill","fill-box","fixed","flat","flex","flex-end","flex-start","footnotes","forwards","from","geometricPrecision","graytext","grid","groove","hand","hard-light","help","hidden","hide","higher","highlight","highlighttext","horizontal","hsl","hsla","hue","icon","ignore","inactiveborder","inactivecaption","inactivecaptiontext","infinite","infobackground","infotext","inherit","initial","inline","inline-axis","inline-block","inline-flex","inline-grid","inline-table","inset","inside","intrinsic","invert","italic","justify","keep-all","landscape","large","larger","left","level","lighter","lighten","line-through","linear","linear-gradient","lines","list-item","listbox","listitem","local","logical","loud","lower","lower-hexadecimal","lower-latin","lower-norwegian","lowercase","ltr","luminosity","manipulation","match","matrix","matrix3d","medium","menu","menutext","message-box","middle","min-intrinsic","mix","monospace","move","multiple","multiple_mask_images","multiply","n-resize","narrower","ne-resize","nesw-resize","no-close-quote","no-drop","no-open-quote","no-repeat","none","normal","not-allowed","nowrap","ns-resize","numbers","numeric","nw-resize","nwse-resize","oblique","opacity","open-quote","optimizeLegibility","optimizeSpeed","outset","outside","outside-shape","overlay","overline","padding","padding-box","painted","page","paused","perspective","pinch-zoom","plus-darker","plus-lighter","pointer","polygon","portrait","pre","pre-line","pre-wrap","preserve-3d","progress","push-button","radial-gradient","radio","read-only","read-write","read-write-plaintext-only","rectangle","region","relative","repeat","repeating-linear-gradient","repeating-radial-gradient","repeat-x","repeat-y","reset","reverse","rgb","rgba","ridge","right","rotate","rotate3d","rotateX","rotateY","rotateZ","round","row","row-resize","row-reverse","rtl","run-in","running","s-resize","sans-serif","saturation","scale","scale3d","scaleX","scaleY","scaleZ","screen","scroll","scrollbar","scroll-position","se-resize","self-start","self-end","semi-condensed","semi-expanded","separate","serif","show","single","skew","skewX","skewY","skip-white-space","slide","slider-horizontal","slider-vertical","sliderthumb-horizontal","sliderthumb-vertical","slow","small","small-caps","small-caption","smaller","soft-light","solid","source-atop","source-in","source-out","source-over","space","space-around","space-between","space-evenly","spell-out","square","start","static","status-bar","stretch","stroke","stroke-box","sub","subpixel-antialiased","svg_masks","super","sw-resize","symbolic","symbols","system-ui","table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row","table-row-group","text","text-bottom","text-top","textarea","textfield","thick","thin","threeddarkshadow","threedface","threedhighlight","threedlightshadow","threedshadow","to","top","transform","translate","translate3d","translateX","translateY","translateZ","transparent","ultra-condensed","ultra-expanded","underline","unidirectional-pan","unset","up","upper-latin","uppercase","url","var","vertical","vertical-text","view-box","visible","visibleFill","visiblePainted","visibleStroke","visual","w-resize","wait","wave","wider","window","windowframe","windowtext","words","wrap","wrap-reverse","x-large","x-small","xor","xx-large","xx-small"].map(r=>({type:"keyword",label:r})).concat(["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"].map(r=>({type:"constant",label:r}))),L=["a","abbr","address","article","aside","b","bdi","bdo","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","dd","del","details","dfn","dialog","div","dl","dt","em","figcaption","figure","footer","form","header","hgroup","h1","h2","h3","h4","h5","h6","hr","html","i","iframe","img","input","ins","kbd","label","legend","li","main","meter","nav","ol","output","p","pre","ruby","section","select","small","source","span","strong","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","tr","u","ul"].map(r=>({type:"type",label:r})),s=/^(\w[\w-]*|-\w[\w-]*|)$/,P=/^-(-[\w-]*)?$/;function T(r,a){var o;if((r.name=="("||r.type.isError)&&(r=r.parent||r),r.name!="ArgList")return!1;let t=(o=r.parent)===null||o===void 0?void 0:o.firstChild;return t?.name!="Callee"?!1:a.sliceString(t.from,t.to)=="var"}const g=new F,A=["Declaration"];function B(r){for(let a=r;;){if(a.type.isTop)return a;if(!(a=a.parent))return r}}function b(r,a,o){if(a.to-a.from>4096){let t=g.get(a);if(t)return t;let e=[],l=new Set,i=a.cursor(N.IncludeAnonymous);if(i.firstChild())do for(let n of b(r,i.node,o))l.has(n.label)||(l.add(n.label),e.push(n));while(i.nextSibling());return g.set(a,e),e}else{let t=[],e=new Set;return a.cursor().iterate(l=>{var i;if(o(l)&&l.matchContext(A)&&((i=l.node.nextSibling)===null||i===void 0?void 0:i.name)==":"){let n=r.sliceString(l.from,l.to);e.has(n)||(e.add(n),t.push({label:n,type:"variable"}))}}),t}}const f=r=>a=>{let{state:o,pos:t}=a,e=y(o).resolveInner(t,-1),l=e.type.isError&&e.from==e.to-1&&o.doc.sliceString(e.from,e.to)=="-";if(e.name=="PropertyName"||(l||e.name=="TagName")&&/^(Block|Styles)$/.test(e.resolve(e.to).name))return{from:e.from,options:u(),validFor:s};if(e.name=="ValueName")return{from:e.from,options:h,validFor:s};if(e.name=="PseudoClassName")return{from:e.from,options:m,validFor:s};if(r(e)||(a.explicit||l)&&T(e,o.doc))return{from:r(e)||l?e.from:t,options:b(o.doc,B(e),r),validFor:P};if(e.name=="TagName"){for(let{parent:d}=e;d;d=d.parent)if(d.name=="Block")return{from:e.from,options:u(),validFor:s};return{from:e.from,options:L,validFor:s}}if(!a.explicit)return null;let i=e.resolve(t),n=i.childBefore(t);return n&&n.name==":"&&i.name=="PseudoClassSelector"?{from:t,options:m,validFor:s}:n&&n.name==":"&&i.name=="Declaration"||i.name=="ArgList"?{from:t,options:h,validFor:s}:i.name=="Block"||i.name=="Styles"?{from:t,options:u(),validFor:s}:null},w=f(r=>r.name=="VariableName"),p=k.define({name:"css",parser:v.configure({props:[x.add({Declaration:z()}),S.add({"Block KeyframeList":q})]}),languageData:{commentTokens:{block:{open:"/*",close:"*/"}},indentOnInput:/^\s*\}$/,wordChars:"-"}});function I(){return new C(p,p.data.of({autocomplete:w}))}export{I as css,w as cssCompletionSource,p as cssLanguage,f as defineCSSCompletionSource}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/lang-html.js b/Resources/Public/JavaScript/Contrib/@codemirror/lang-html.js new file mode 100644 index 0000000..67181a1 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/lang-html.js @@ -0,0 +1 @@ +import{parser as W,configureNesting as L}from"@lezer/html";import{cssLanguage as I,css as J}from"@codemirror/lang-css";import{javascriptLanguage as S,typescriptLanguage as K,jsxLanguage as Q,tsxLanguage as X,javascript as Y}from"@codemirror/lang-javascript";import{EditorView as Z}from"@codemirror/view";import{EditorSelection as ee}from"@codemirror/state";import{syntaxTree as q,LRLanguage as te,indentNodeProp as le,foldNodeProp as ae,bracketMatchingHandle as ne,LanguageSupport as re}from"@codemirror/language";const T=["_blank","_self","_top","_parent"],N=["ascii","utf-8","utf-16","latin1","latin1"],j=["get","post","put","delete"],E=["application/x-www-form-urlencoded","multipart/form-data","text/plain"],p=["true","false"],l={},se={a:{attrs:{href:null,ping:null,type:null,media:null,target:T,hreflang:null}},abbr:l,address:l,area:{attrs:{alt:null,coords:null,href:null,target:null,ping:null,media:null,hreflang:null,type:null,shape:["default","rect","circle","poly"]}},article:l,aside:l,audio:{attrs:{src:null,mediagroup:null,crossorigin:["anonymous","use-credentials"],preload:["none","metadata","auto"],autoplay:["autoplay"],loop:["loop"],controls:["controls"]}},b:l,base:{attrs:{href:null,target:T}},bdi:l,bdo:l,blockquote:{attrs:{cite:null}},body:l,br:l,button:{attrs:{form:null,formaction:null,name:null,value:null,autofocus:["autofocus"],disabled:["autofocus"],formenctype:E,formmethod:j,formnovalidate:["novalidate"],formtarget:T,type:["submit","reset","button"]}},canvas:{attrs:{width:null,height:null}},caption:l,center:l,cite:l,code:l,col:{attrs:{span:null}},colgroup:{attrs:{span:null}},command:{attrs:{type:["command","checkbox","radio"],label:null,icon:null,radiogroup:null,command:null,title:null,disabled:["disabled"],checked:["checked"]}},data:{attrs:{value:null}},datagrid:{attrs:{disabled:["disabled"],multiple:["multiple"]}},datalist:{attrs:{data:null}},dd:l,del:{attrs:{cite:null,datetime:null}},details:{attrs:{open:["open"]}},dfn:l,div:l,dl:l,dt:l,em:l,embed:{attrs:{src:null,type:null,width:null,height:null}},eventsource:{attrs:{src:null}},fieldset:{attrs:{disabled:["disabled"],form:null,name:null}},figcaption:l,figure:l,footer:l,form:{attrs:{action:null,name:null,"accept-charset":N,autocomplete:["on","off"],enctype:E,method:j,novalidate:["novalidate"],target:T}},h1:l,h2:l,h3:l,h4:l,h5:l,h6:l,head:{children:["title","base","link","style","meta","script","noscript","command"]},header:l,hgroup:l,hr:l,html:{attrs:{manifest:null}},i:l,iframe:{attrs:{src:null,srcdoc:null,name:null,width:null,height:null,sandbox:["allow-top-navigation","allow-same-origin","allow-forms","allow-scripts"],seamless:["seamless"]}},img:{attrs:{alt:null,src:null,ismap:null,usemap:null,width:null,height:null,crossorigin:["anonymous","use-credentials"]}},input:{attrs:{alt:null,dirname:null,form:null,formaction:null,height:null,list:null,max:null,maxlength:null,min:null,name:null,pattern:null,placeholder:null,size:null,src:null,step:null,value:null,width:null,accept:["audio/*","video/*","image/*"],autocomplete:["on","off"],autofocus:["autofocus"],checked:["checked"],disabled:["disabled"],formenctype:E,formmethod:j,formnovalidate:["novalidate"],formtarget:T,multiple:["multiple"],readonly:["readonly"],required:["required"],type:["hidden","text","search","tel","url","email","password","datetime","date","month","week","time","datetime-local","number","range","color","checkbox","radio","file","submit","image","reset","button"]}},ins:{attrs:{cite:null,datetime:null}},kbd:l,keygen:{attrs:{challenge:null,form:null,name:null,autofocus:["autofocus"],disabled:["disabled"],keytype:["RSA"]}},label:{attrs:{for:null,form:null}},legend:l,li:{attrs:{value:null}},link:{attrs:{href:null,type:null,hreflang:null,media:null,sizes:["all","16x16","16x16 32x32","16x16 32x32 64x64"]}},map:{attrs:{name:null}},mark:l,menu:{attrs:{label:null,type:["list","context","toolbar"]}},meta:{attrs:{content:null,charset:N,name:["viewport","application-name","author","description","generator","keywords"],"http-equiv":["content-language","content-type","default-style","refresh"]}},meter:{attrs:{value:null,min:null,low:null,high:null,max:null,optimum:null}},nav:l,noscript:l,object:{attrs:{data:null,type:null,name:null,usemap:null,form:null,width:null,height:null,typemustmatch:["typemustmatch"]}},ol:{attrs:{reversed:["reversed"],start:null,type:["1","a","A","i","I"]},children:["li","script","template","ul","ol"]},optgroup:{attrs:{disabled:["disabled"],label:null}},option:{attrs:{disabled:["disabled"],label:null,selected:["selected"],value:null}},output:{attrs:{for:null,form:null,name:null}},p:l,param:{attrs:{name:null,value:null}},pre:l,progress:{attrs:{value:null,max:null}},q:{attrs:{cite:null}},rp:l,rt:l,ruby:l,samp:l,script:{attrs:{type:["text/javascript"],src:null,async:["async"],defer:["defer"],charset:N}},section:l,select:{attrs:{form:null,name:null,size:null,autofocus:["autofocus"],disabled:["disabled"],multiple:["multiple"]}},slot:{attrs:{name:null}},small:l,source:{attrs:{src:null,type:null,media:null}},span:l,strong:l,style:{attrs:{type:["text/css"],media:null,scoped:null}},sub:l,summary:l,sup:l,table:l,tbody:l,td:{attrs:{colspan:null,rowspan:null,headers:null}},template:l,textarea:{attrs:{dirname:null,form:null,maxlength:null,name:null,placeholder:null,rows:null,cols:null,autofocus:["autofocus"],disabled:["disabled"],readonly:["readonly"],required:["required"],wrap:["soft","hard"]}},tfoot:l,th:{attrs:{colspan:null,rowspan:null,headers:null,scope:["row","col","rowgroup","colgroup"]}},thead:l,time:{attrs:{datetime:null}},title:l,tr:l,track:{attrs:{src:null,label:null,default:null,kind:["subtitles","captions","descriptions","chapters","metadata"],srclang:null}},ul:{children:["li","script","template","ul","ol"]},var:l,video:{attrs:{src:null,poster:null,width:null,height:null,crossorigin:["anonymous","use-credentials"],preload:["auto","metadata","none"],autoplay:["autoplay"],mediagroup:["movie"],muted:["muted"],controls:["controls"]}},wbr:l},_={accesskey:null,class:null,contenteditable:p,contextmenu:null,dir:["ltr","rtl","auto"],draggable:["true","false","auto"],dropzone:["copy","move","link","string:","file:"],hidden:["hidden"],id:null,inert:["inert"],itemid:null,itemprop:null,itemref:null,itemscope:["itemscope"],itemtype:null,lang:["ar","bn","de","en-GB","en-US","es","fr","hi","id","ja","pa","pt","ru","tr","zh"],spellcheck:p,autocorrect:p,autocapitalize:p,style:null,tabindex:null,title:null,translate:["yes","no"],rel:["stylesheet","alternate","author","bookmark","help","license","next","nofollow","noreferrer","prefetch","prev","search","tag"],role:"alert application article banner button cell checkbox complementary contentinfo dialog document feed figure form grid gridcell heading img list listbox listitem main navigation region row rowgroup search switch tab table tabpanel textbox timer".split(" "),"aria-activedescendant":null,"aria-atomic":p,"aria-autocomplete":["inline","list","both","none"],"aria-busy":p,"aria-checked":["true","false","mixed","undefined"],"aria-controls":null,"aria-describedby":null,"aria-disabled":p,"aria-dropeffect":null,"aria-expanded":["true","false","undefined"],"aria-flowto":null,"aria-grabbed":["true","false","undefined"],"aria-haspopup":p,"aria-hidden":p,"aria-invalid":["true","false","grammar","spelling"],"aria-label":null,"aria-labelledby":null,"aria-level":null,"aria-live":["off","polite","assertive"],"aria-multiline":p,"aria-multiselectable":p,"aria-owns":null,"aria-posinset":null,"aria-pressed":["true","false","mixed","undefined"],"aria-readonly":p,"aria-relevant":null,"aria-required":p,"aria-selected":["true","false","undefined"],"aria-setsize":null,"aria-sort":["ascending","descending","none","other"],"aria-valuemax":null,"aria-valuemin":null,"aria-valuenow":null,"aria-valuetext":null},z="beforeunload copy cut dragstart dragover dragleave dragenter dragend drag paste focus blur change click load mousedown mouseenter mouseleave mouseup keydown keyup resize scroll unload".split(" ").map(e=>"on"+e);for(let e of z)_[e]=null;class C{constructor(n,r){this.tags=Object.assign(Object.assign({},se),n),this.globalAttrs=Object.assign(Object.assign({},_),r),this.allTags=Object.keys(this.tags),this.globalAttrNames=Object.keys(this.globalAttrs)}}C.default=new C;function y(e,n,r=e.length){if(!n)return"";let a=n.firstChild,t=a&&a.getChild("TagName");return t?e.sliceString(t.from,Math.min(t.to,r)):""}function v(e,n=!1){for(;e;e=e.parent)if(e.name=="Element")if(n)n=!1;else return e;return null}function D(e,n,r){let a=r.tags[y(e,v(n))];return a?.children||r.allTags}function $(e,n){let r=[];for(let a=v(n);a&&!a.type.isTop;a=v(a.parent)){let t=y(e,a);if(t&&a.lastChild.name=="CloseTag")break;t&&r.indexOf(t)<0&&(n.name=="EndTag"||n.from>=a.firstChild.to)&&r.push(t)}return r}const F=/^[:\-\.\w\u00b7-\uffff]*$/;function P(e,n,r,a,t){let o=/\s*>/.test(e.sliceDoc(t,t+5))?"":">",s=v(r,!0);return{from:a,to:t,options:D(e.doc,s,n).map(u=>({label:u,type:"type"})).concat($(e.doc,r).map((u,i)=>({label:"/"+u,apply:"/"+u+o,type:"type",boost:99-i}))),validFor:/^\/?[:\-\.\w\u00b7-\uffff]*$/}}function V(e,n,r,a){let t=/\s*>/.test(e.sliceDoc(a,a+5))?"":">";return{from:r,to:a,options:$(e.doc,n).map((o,s)=>({label:o,apply:o+t,type:"type",boost:99-s})),validFor:F}}function oe(e,n,r,a){let t=[],o=0;for(let s of D(e.doc,r,n))t.push({label:"<"+s,type:"type"});for(let s of $(e.doc,r))t.push({label:"",type:"type",boost:99-o++});return{from:a,to:a,options:t,validFor:/^<\/?[:\-\.\w\u00b7-\uffff]*$/}}function ie(e,n,r,a,t){let o=v(r),s=o?n.tags[y(e.doc,o)]:null,u=s&&s.attrs?Object.keys(s.attrs):[],i=s&&s.globalAttrs===!1?u:u.length?u.concat(n.globalAttrNames):n.globalAttrNames;return{from:a,to:t,options:i.map(f=>({label:f,type:"property"})),validFor:F}}function ue(e,n,r,a,t){var o;let s=(o=r.parent)===null||o===void 0?void 0:o.getChild("AttributeName"),u=[],i;if(s){let f=e.sliceDoc(s.from,s.to),g=n.globalAttrs[f];if(!g){let c=v(r),m=c?n.tags[y(e.doc,c)]:null;g=m?.attrs&&m.attrs[f]}if(g){let c=e.sliceDoc(a,t).toLowerCase(),m='"',d='"';/^['"]/.test(c)?(i=c[0]=='"'?/^[^"]*$/:/^[^']*$/,m="",d=e.sliceDoc(t,t+1)==c[0]?"":c[0],c=c.slice(1),a++):i=/^[^\s<>='"]*$/;for(let h of g)u.push({label:h,apply:m+h+d,type:"constant"})}}return{from:a,to:t,options:u,validFor:i}}function B(e,n){let{state:r,pos:a}=n,t=q(r).resolveInner(a,-1),o=t.resolve(a);for(let s=a,u;o==t&&(u=t.childBefore(s));){let i=u.lastChild;if(!i||!i.type.isError||i.fromB(a,t)}const ce=S.parser.configure({top:"SingleExpression"}),G=[{tag:"script",attrs:e=>e.type=="text/typescript"||e.lang=="ts",parser:K.parser},{tag:"script",attrs:e=>e.type=="text/babel"||e.type=="text/jsx",parser:Q.parser},{tag:"script",attrs:e=>e.type=="text/typescript-jsx",parser:X.parser},{tag:"script",attrs(e){return/^(importmap|speculationrules|application\/(.+\+)?json)$/i.test(e.type)},parser:ce},{tag:"script",attrs(e){return!e.type||/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i.test(e.type)},parser:S.parser},{tag:"style",attrs(e){return(!e.lang||e.lang=="css")&&(!e.type||/^(text\/)?(x-)?(stylesheet|css)$/i.test(e.type))},parser:I.parser}],R=[{name:"style",parser:I.parser.configure({top:"Styles"})}].concat(z.map(e=>({name:e,parser:S.parser}))),O=te.define({name:"html",parser:W.configure({props:[le.add({Element(e){let n=/^(\s*)(<\/)?/.exec(e.textAfter);return e.node.to<=e.pos+n[0].length?e.continue():e.lineIndent(e.node.from)+(n[2]?0:e.unit)},"OpenTag CloseTag SelfClosingTag"(e){return e.column(e.node.from)+e.unit},Document(e){if(e.pos+/\s*/.exec(e.textAfter)[0].lengthe.getChild("TagName")})]}),languageData:{commentTokens:{block:{open:""}},indentOnInput:/^\s*<\/\w+\W$/,wordChars:"-._"}}),k=O.configure({wrap:L(G,R)});function pe(e={}){let n="",r;e.matchClosingTags===!1&&(n="noMatch"),e.selfClosingTags===!0&&(n=(n?n+" ":"")+"selfClosing"),(e.nestedLanguages&&e.nestedLanguages.length||e.nestedAttributes&&e.nestedAttributes.length)&&(r=L((e.nestedLanguages||[]).concat(G),(e.nestedAttributes||[]).concat(R)));let a=r?O.configure({wrap:r,dialect:n}):n?k.configure({dialect:n}):k;return new re(a,[k.data.of({autocomplete:M(e)}),e.autoCloseTags!==!1?U:[],Y().support,J().support])}const H=new Set("area base br col command embed frame hr img input keygen link meta param source track wbr menuitem".split(" ")),U=Z.inputHandler.of((e,n,r,a,t)=>{if(e.composing||e.state.readOnly||n!=r||a!=">"&&a!="/"||!k.isActiveAt(e.state,n,-1))return!1;let o=t(),{state:s}=o,u=s.changeByRange(i=>{var f,g,c;let m=s.doc.sliceString(i.from-1,i.to)==a,{head:d}=i,h=q(s).resolveInner(d,-1),b;if(m&&a==">"&&h.name=="EndTag"){let w=h.parent;if(((g=(f=w.parent)===null||f===void 0?void 0:f.lastChild)===null||g===void 0?void 0:g.name)!="CloseTag"&&(b=y(s.doc,w.parent,d))&&!H.has(b)){let A=d+(s.doc.sliceString(d,d+1)===">"?1:0),x=``;return{range:i,changes:{from:d,to:A,insert:x}}}}else if(m&&a=="/"&&h.name=="IncompleteCloseTag"){let w=h.parent;if(h.from==d-2&&((c=w.lastChild)===null||c===void 0?void 0:c.name)!="CloseTag"&&(b=y(s.doc,w,d))&&!H.has(b)){let A=d+(s.doc.sliceString(d,d+1)===">"?1:0),x=`${b}>`;return{range:ee.cursor(d+x.length,-1),changes:{from:d,to:A,insert:x}}}}return{range:i}});return u.changes.empty?!1:(e.dispatch([o,s.update(u,{userEvent:"input.complete",scrollIntoView:!0})]),!0)});export{U as autoCloseTags,pe as html,de as htmlCompletionSource,M as htmlCompletionSourceWith,k as htmlLanguage,O as htmlPlain}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/lang-javascript.js b/Resources/Public/JavaScript/Contrib/@codemirror/lang-javascript.js new file mode 100644 index 0000000..5ea8b66 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/lang-javascript.js @@ -0,0 +1,13 @@ +import{parser as M}from"@lezer/javascript";import{syntaxTree as S,LRLanguage as _,indentNodeProp as R,continuedIndent as $,flatIndent as W,delimitedIndent as Z,foldNodeProp as z,foldInside as H,defineLanguageFacet as K,sublanguageProp as T,LanguageSupport as q}from"@codemirror/language";import{EditorSelection as G}from"@codemirror/state";import{EditorView as Q}from"@codemirror/view";import{snippetCompletion as p,ifNotIn as U,completeFromList as Y}from"@codemirror/autocomplete";import{NodeWeakMap as ee,IterMode as te}from"@lezer/common";const w=[p("function ${name}(${params}) {\n ${}\n}",{label:"function",detail:"definition",type:"keyword"}),p("for (let ${index} = 0; ${index} < ${bound}; ${index}++) {\n ${}\n}",{label:"for",detail:"loop",type:"keyword"}),p("for (let ${name} of ${collection}) {\n ${}\n}",{label:"for",detail:"of loop",type:"keyword"}),p("do {\n ${}\n} while (${})",{label:"do",detail:"loop",type:"keyword"}),p("while (${}) {\n ${}\n}",{label:"while",detail:"loop",type:"keyword"}),p(`try { + \${} +} catch (\${error}) { + \${} +}`,{label:"try",detail:"/ catch block",type:"keyword"}),p("if (${}) {\n ${}\n}",{label:"if",detail:"block",type:"keyword"}),p(`if (\${}) { + \${} +} else { + \${} +}`,{label:"if",detail:"/ else block",type:"keyword"}),p(`class \${name} { + constructor(\${params}) { + \${} + } +}`,{label:"class",detail:"definition",type:"keyword"}),p('import {${names}} from "${module}"\n${}',{label:"import",detail:"named",type:"keyword"}),p('import ${name} from "${module}"\n${}',{label:"import",detail:"default",type:"keyword"})],I=w.concat([p("interface ${name} {\n ${}\n}",{label:"interface",detail:"definition",type:"keyword"}),p("type ${name} = ${type}",{label:"type",detail:"definition",type:"keyword"}),p("enum ${name} {\n ${}\n}",{label:"enum",detail:"definition",type:"keyword"})]),x=new ee,D=new Set(["Script","Block","FunctionExpression","FunctionDeclaration","ArrowFunction","MethodDeclaration","ForStatement"]);function y(e){return(t,r)=>{let n=t.node.getChild("VariableDefinition");return n&&r(n,e),!0}}const ne=["FunctionDeclaration"],re={FunctionDeclaration:y("function"),ClassDeclaration:y("class"),ClassExpression:()=>!0,EnumDeclaration:y("constant"),TypeAliasDeclaration:y("type"),NamespaceDeclaration:y("namespace"),VariableDefinition(e,t){e.matchContext(ne)||t(e,"variable")},TypeDefinition(e,t){t(e,"type")},__proto__:null};function J(e,t){let r=x.get(t);if(r)return r;let n=[],a=!0;function i(o,l){let c=e.sliceString(o.from,o.to);n.push({label:c,type:l})}return t.cursor(te.IncludeAnonymous).iterate(o=>{if(a)a=!1;else if(o.name){let l=re[o.name];if(l&&l(o,i)||D.has(o.name))return!1}else if(o.to-o.from>8192){for(let l of J(e,o.node))n.push(l);return!1}}),x.set(t,n),n}const h=/^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/,k=["TemplateString","String","RegExp","LineComment","BlockComment","VariableDefinition","TypeDefinition","Label","PropertyDefinition","PropertyName","PrivatePropertyDefinition","PrivatePropertyName",".","?."];function X(e){let t=S(e.state).resolveInner(e.pos,-1);if(k.indexOf(t.name)>-1)return null;let r=t.name=="VariableName"||t.to-t.from<20&&h.test(e.state.sliceDoc(t.from,t.to));if(!r&&!e.explicit)return null;let n=[];for(let a=t;a;a=a.parent)D.has(a.name)&&(n=n.concat(J(e.state.doc,a)));return{options:n,from:r?t.from:e.pos,validFor:h}}function v(e,t,r){var n;let a=[];for(;;){let i=t.firstChild,o;if(i?.name=="VariableName")return a.push(e(i)),{path:a.reverse(),name:r};if(i?.name=="MemberExpression"&&((n=o=i.lastChild)===null||n===void 0?void 0:n.name)=="PropertyName")a.push(e(o)),t=i;else return null}}function E(e){let t=n=>e.state.doc.sliceString(n.from,n.to),r=S(e.state).resolveInner(e.pos,-1);return r.name=="PropertyName"?v(t,r.parent,t(r)):(r.name=="."||r.name=="?.")&&r.parent.name=="MemberExpression"?v(t,r.parent,""):k.indexOf(r.name)>-1?null:r.name=="VariableName"||r.to-r.from<20&&h.test(t(r))?{path:[],name:t(r)}:r.name=="MemberExpression"?v(t,r,""):e.explicit?{path:[],name:""}:null}function oe(e,t){let r=[],n=new Set;for(let a=0;;a++){for(let o of(Object.getOwnPropertyNames||Object.keys)(e)){if(!/^[a-zA-Z_$\xaa-\uffdc][\w$\xaa-\uffdc]*$/.test(o)||n.has(o))continue;n.add(o);let l;try{l=e[o]}catch{continue}r.push({label:o,type:typeof l=="function"?/^[A-Z]/.test(o)?"class":t?"function":"method":t?"variable":"property",boost:-a})}let i=Object.getPrototypeOf(e);if(!i)return r;e=i}}function ae(e){let t=new Map;return r=>{let n=E(r);if(!n)return null;let a=e;for(let o of n.path)if(a=a[o],!a)return null;let i=t.get(a);return i||t.set(a,i=oe(a,!n.path.length)),{from:r.pos-n.name.length,options:i,validFor:h}}}const m=_.define({name:"javascript",parser:M.configure({props:[R.add({IfStatement:$({except:/^\s*({|else\b)/}),TryStatement:$({except:/^\s*({|catch\b|finally\b)/}),LabeledStatement:W,SwitchBody:e=>{let t=e.textAfter,r=/^\s*\}/.test(t),n=/^\s*(case|default)\b/.test(t);return e.baseIndent+(r?0:n?1:2)*e.unit},Block:Z({closing:"}"}),ArrowFunction:e=>e.baseIndent+e.unit,"TemplateString BlockComment":()=>null,"Statement Property":$({except:/^{/}),JSXElement(e){let t=/^\s*<\//.test(e.textAfter);return e.lineIndent(e.node.from)+(t?0:e.unit)},JSXEscape(e){let t=/\s*\}/.test(e.textAfter);return e.lineIndent(e.node.from)+(t?0:e.unit)},"JSXOpenTag JSXSelfClosingTag"(e){return e.column(e.node.from)+e.unit}}),z.add({"Block ClassBody SwitchBody EnumBody ObjectExpression ArrayExpression ObjectType":H,BlockComment(e){return{from:e.from+2,to:e.to-2}}})]}),languageData:{closeBrackets:{brackets:["(","[","{","'",'"',"`"]},commentTokens:{line:"//",block:{open:"/*",close:"*/"}},indentOnInput:/^\s*(?:case |default:|\{|\}|<\/)$/,wordChars:"$"}}),N={test:e=>/^JSX/.test(e.name),facet:K({commentTokens:{block:{open:"{/*",close:"*/}"}}})},P=m.configure({dialect:"ts"},"typescript"),A=m.configure({dialect:"jsx",props:[T.add(e=>e.isTop?[N]:void 0)]}),L=m.configure({dialect:"jsx ts",props:[T.add(e=>e.isTop?[N]:void 0)]},"typescript");let O=e=>({label:e,type:"keyword"});const F="break case const continue default delete export extends false finally in instanceof let new return static super switch this throw true typeof var yield".split(" ").map(O),ie=F.concat(["declare","implements","private","protected","public"].map(O));function le(e={}){let t=e.jsx?e.typescript?L:A:e.typescript?P:m,r=e.typescript?I.concat(ie):w.concat(F);return new q(t,[m.data.of({autocomplete:U(k,Y(r))}),m.data.of({autocomplete:X}),e.jsx?B:[]])}function se(e){for(;;){if(e.name=="JSXOpenTag"||e.name=="JSXSelfClosingTag"||e.name=="JSXFragmentTag")return e;if(e.name=="JSXEscape"||!e.parent)return null;e=e.parent}}function j(e,t,r=e.length){for(let n=t?.firstChild;n;n=n.nextSibling)if(n.name=="JSXIdentifier"||n.name=="JSXBuiltin"||n.name=="JSXNamespacedName"||n.name=="JSXMemberExpression")return e.sliceString(n.from,Math.min(n.to,r));return""}const pe=typeof navigator=="object"&&/Android\b/.test(navigator.userAgent),B=Q.inputHandler.of((e,t,r,n,a)=>{if((pe?e.composing:e.compositionStarted)||e.state.readOnly||t!=r||n!=">"&&n!="/"||!m.isActiveAt(e.state,t,-1))return!1;let i=a(),{state:o}=i,l=o.changeByRange(c=>{var u;let{head:s}=c,f=S(o).resolveInner(s-1,-1),g;if(f.name=="JSXStartTag"&&(f=f.parent),!(o.doc.sliceString(s-1,s)!=n||f.name=="JSXAttributeValue"&&f.to>s)){if(n==">"&&f.name=="JSXFragmentTag")return{range:c,changes:{from:s,insert:""}};if(n=="/"&&f.name=="JSXStartCloseTag"){let d=f.parent,b=d.parent;if(b&&d.from==s-2&&((g=j(o.doc,b.firstChild,s))||((u=b.firstChild)===null||u===void 0?void 0:u.name)=="JSXFragmentTag")){let C=`${g}>`;return{range:G.cursor(s+C.length,-1),changes:{from:s,insert:C}}}}else if(n==">"){let d=se(f);if(d&&d.name=="JSXOpenTag"&&!/^\/?>|^<\//.test(o.doc.sliceString(s,s+2))&&(g=j(o.doc,d,s)))return{range:c,changes:{from:s,insert:``}}}}return{range:c}});return l.changes.empty?!1:(e.dispatch([i,o.update(l,{userEvent:"input.complete",scrollIntoView:!0})]),!0)});function ce(e,t){return t||(t={parserOptions:{ecmaVersion:2019,sourceType:"module"},env:{browser:!0,node:!0,es6:!0,es2015:!0,es2017:!0,es2020:!0},rules:{}},e.getRules().forEach((r,n)=>{r.meta.docs.recommended&&(t.rules[n]=2)})),r=>{let{state:n}=r,a=[];for(let{from:i,to:o}of m.findRegions(n)){let l=n.doc.lineAt(i),c={line:l.number-1,col:i-l.from,pos:i};for(let u of e.verify(n.sliceDoc(i,o),t))a.push(fe(u,n.doc,c))}return a}}function V(e,t,r,n){return r.line(e+n.line).from+t+(e==1?n.col-1:-1)}function fe(e,t,r){let n=V(e.line,e.column,t,r),a={from:n,to:e.endLine!=null&&e.endColumn!=1?V(e.endLine,e.endColumn,t,r):n,message:e.message,source:e.ruleId?"eslint:"+e.ruleId:"eslint",severity:e.severity==1?"warning":"error"};if(e.fix){let{range:i,text:o}=e.fix,l=i[0]+r.pos-n,c=i[1]+r.pos-n;a.actions=[{name:"fix",apply(u,s){u.dispatch({changes:{from:s+l,to:s+c,insert:o},scrollIntoView:!0})}}]}return a}export{B as autoCloseTags,E as completionPath,ce as esLint,le as javascript,m as javascriptLanguage,A as jsxLanguage,X as localCompletionSource,ae as scopeCompletionSource,w as snippets,L as tsxLanguage,P as typescriptLanguage,I as typescriptSnippets}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/lang-json.js b/Resources/Public/JavaScript/Contrib/@codemirror/lang-json.js new file mode 100644 index 0000000..1844bf8 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/lang-json.js @@ -0,0 +1 @@ +import{parser as a}from"@lezer/json";import{LRLanguage as s,indentNodeProp as i,continuedIndent as r,foldNodeProp as c,foldInside as d,LanguageSupport as g}from"@codemirror/language";const m=()=>n=>{try{JSON.parse(n.state.doc.toString())}catch(e){if(!(e instanceof SyntaxError))throw e;const t=p(e,n.state.doc);return[{from:t,message:e.message,severity:"error",to:t}]}return[]};function p(n,e){let t;return(t=n.message.match(/at position (\d+)/))?Math.min(+t[1],e.length):(t=n.message.match(/at line (\d+) column (\d+)/))?Math.min(e.line(+t[1]).from+ +t[2]-1,e.length):0}const o=s.define({name:"json",parser:a.configure({props:[i.add({Object:r({except:/^\s*\}/}),Array:r({except:/^\s*\]/})}),c.add({"Object Array":d})]}),languageData:{closeBrackets:{brackets:["[","{",'"']},indentOnInput:/^\s*[\}\]]$/}});function u(){return new g(o)}export{u as json,o as jsonLanguage,m as jsonParseLinter}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/lang-php.js b/Resources/Public/JavaScript/Contrib/@codemirror/lang-php.js new file mode 100644 index 0000000..2e1a606 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/lang-php.js @@ -0,0 +1 @@ +import{parser as l}from"@lezer/php";import{parseMixed as i}from"@lezer/common";import{html as p}from"@codemirror/lang-html";import{LRLanguage as u,indentNodeProp as d,continuedIndent as r,delimitedIndent as m,foldNodeProp as f,foldInside as c,LanguageSupport as g}from"@codemirror/language";const a=u.define({name:"php",parser:l.configure({props:[d.add({IfStatement:r({except:/^\s*({|else\b|elseif\b|endif\b)/}),TryStatement:r({except:/^\s*({|catch\b|finally\b)/}),SwitchBody:e=>{let o=e.textAfter,t=/^\s*\}/.test(o),n=/^\s*(case|default)\b/.test(o);return e.baseIndent+(t?0:n?1:2)*e.unit},ColonBlock:e=>e.baseIndent+e.unit,"Block EnumBody DeclarationList":m({closing:"}"}),ArrowFunction:e=>e.baseIndent+e.unit,"String BlockComment":()=>null,Statement:r({except:/^({|end(for|foreach|switch|while)\b)/})}),f.add({"Block EnumBody DeclarationList SwitchBody ArrayExpression ValueList":c,ColonBlock(e){return{from:e.from+1,to:e.to}},BlockComment(e){return{from:e.from+2,to:e.to-2}}})]}),languageData:{commentTokens:{block:{open:"/*",close:"*/"},line:"//"},indentOnInput:/^\s*(?:case |default:|end(?:if|for(?:each)?|switch|while)|else(?:if)?|\{|\})$/,wordChars:"$",closeBrackets:{stringPrefixes:["b","B"]}}});function h(e={}){let o=[],t;if(e.baseLanguage!==null)if(e.baseLanguage)t=e.baseLanguage;else{let n=p({matchClosingTags:!1});o.push(n.support),t=n.language}return new g(a.configure({wrap:t&&i(n=>n.type.isTop?{parser:t.parser,overlay:s=>s.name=="Text"}:null),top:e.plain?"Program":"Template"}),o)}export{h as php,a as phpLanguage}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/lang-sql.js b/Resources/Public/JavaScript/Contrib/@codemirror/lang-sql.js new file mode 100644 index 0000000..91aafb3 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/lang-sql.js @@ -0,0 +1,2 @@ +import{syntaxTree as ie,indentNodeProp as se,continuedIndent as oe,foldNodeProp as le,LRLanguage as ce,LanguageSupport as de}from"@codemirror/language";import{styleTags as me,tags as i}from"@lezer/highlight";import{ExternalTokenizer as ue,LRParser as fe}from"@lezer/lr";import{ifNotIn as pe,completeFromList as ge}from"@codemirror/autocomplete";const he=36,X=1,_e=2,b=3,S=4,be=5,ve=6,ye=7,ke=8,xe=9,Oe=10,we=11,Qe=12,Ce=13,Se=14,Pe=15,qe=16,Te=17,I=18,Ue=19,R=20,j=21,D=22,ze=23,Be=24;function P(t){return t>=65&&t<=90||t>=97&&t<=122||t>=48&&t<=57}function Le(t){return t>=48&&t<=57||t>=97&&t<=102||t>=65&&t<=70}function p(t,e,r){for(let a=!1;;){if(t.next<0)return;if(t.next==e&&!a){t.advance();return}a=r&&!a&&t.next==92,t.advance()}}function Xe(t,e){e:for(;;){if(t.next<0)return console.log("exit at end",t.pos);if(t.next==36){t.advance();for(let r=0;r)".charCodeAt(r);for(;;){if(t.next<0)return;if(t.next==a&&t.peek(1)==39){t.advance(2);return}t.advance()}}function q(t,e){for(;!(t.next!=95&&!P(t.next));)e!=null&&(e+=String.fromCharCode(t.next)),t.advance();return e}function Re(t){if(t.next==39||t.next==34||t.next==96){let e=t.next;t.advance(),p(t,e,!1)}else q(t)}function Z(t,e){for(;t.next==48||t.next==49;)t.advance();e&&t.next==e&&t.advance()}function N(t,e){for(;;){if(t.next==46){if(e)break;e=!0}else if(t.next<48||t.next>57)break;t.advance()}if(t.next==69||t.next==101)for(t.advance(),(t.next==43||t.next==45)&&t.advance();t.next>=48&&t.next<=57;)t.advance()}function V(t){for(;!(t.next<0||t.next==10);)t.advance()}function g(t,e){for(let r=0;r!=&|~^/",specialVar:"?",identifierQuotes:'"',caseInsensitiveIdentifiers:!1,words:$(_,h)};function je(t,e,r,a){let n={};for(let s in U)n[s]=(t.hasOwnProperty(s)?t:U)[s];return e&&(n.words=$(e,r||"",a)),n}function A(t){return new ue(e=>{var r;let{next:a}=e;if(e.advance(),g(a,T)){for(;g(e.next,T);)e.advance();e.acceptToken(he)}else if(a==36&&t.doubleDollarQuotedStrings){let n=q(e,"");e.next==36&&(e.advance(),Xe(e,n),e.acceptToken(b))}else if(a==39||a==34&&t.doubleQuotedStrings)p(e,a,t.backslashEscapes),e.acceptToken(b);else if(a==35&&t.hashComments||a==47&&e.next==47&&t.slashComments)V(e),e.acceptToken(X);else if(a==45&&e.next==45&&(!t.spaceAfterDashes||e.peek(1)==32))V(e),e.acceptToken(X);else if(a==47&&e.next==42){e.advance();for(let n=1;;){let s=e.next;if(e.next<0)break;if(e.advance(),s==42&&e.next==47){if(n--,e.advance(),!n)break}else s==47&&e.next==42&&(n++,e.advance())}e.acceptToken(_e)}else if((a==101||a==69)&&e.next==39)e.advance(),p(e,39,!0),e.acceptToken(b);else if((a==110||a==78)&&e.next==39&&t.charSetCasts)e.advance(),p(e,39,t.backslashEscapes),e.acceptToken(b);else if(a==95&&t.charSetCasts)for(let n=0;;n++){if(e.next==39&&n>1){e.advance(),p(e,39,t.backslashEscapes),e.acceptToken(b);break}if(!P(e.next))break;e.advance()}else if(t.plsqlQuotingMechanism&&(a==113||a==81)&&e.next==39&&e.peek(1)>0&&!g(e.peek(1),T)){let n=e.peek(1);e.advance(2),Ie(e,n),e.acceptToken(b)}else if(a==40)e.acceptToken(ye);else if(a==41)e.acceptToken(ke);else if(a==123)e.acceptToken(xe);else if(a==125)e.acceptToken(Oe);else if(a==91)e.acceptToken(we);else if(a==93)e.acceptToken(Qe);else if(a==59)e.acceptToken(Ce);else if(t.unquotedBitLiterals&&a==48&&e.next==98)e.advance(),Z(e),e.acceptToken(D);else if((a==98||a==66)&&(e.next==39||e.next==34)){const n=e.next;e.advance(),t.treatBitsAsBytes?(p(e,n,t.backslashEscapes),e.acceptToken(ze)):(Z(e,n),e.acceptToken(D))}else if(a==48&&(e.next==120||e.next==88)||(a==120||a==88)&&e.next==39){let n=e.next==39;for(e.advance();Le(e.next);)e.advance();n&&e.next==39&&e.advance(),e.acceptToken(S)}else if(a==46&&e.next>=48&&e.next<=57)N(e,!0),e.acceptToken(S);else if(a==46)e.acceptToken(Se);else if(a>=48&&a<=57)N(e,!1),e.acceptToken(S);else if(g(a,t.operatorChars)){for(;g(e.next,t.operatorChars);)e.advance();e.acceptToken(Pe)}else if(g(a,t.specialVar))e.next==a&&e.advance(),Re(e),e.acceptToken(Te);else if(g(a,t.identifierQuotes))p(e,a,!1),e.acceptToken(Ue);else if(a==58||a==44)e.acceptToken(qe);else if(P(a)){let n=q(e,String.fromCharCode(a));e.acceptToken(e.next==46||e.peek(-n.length-1)==46?I:(r=t.words[n.toLowerCase()])!==null&&r!==void 0?r:I)}})}const E=A(U),De=fe.deserialize({version:14,states:"%vQ]QQOOO#wQRO'#DSO$OQQO'#CwO%eQQO'#CxO%lQQO'#CyO%sQQO'#CzOOQQ'#DS'#DSOOQQ'#C}'#C}O'UQRO'#C{OOQQ'#Cv'#CvOOQQ'#C|'#C|Q]QQOOQOQQOOO'`QQO'#DOO(xQRO,59cO)PQQO,59cO)UQQO'#DSOOQQ,59d,59dO)cQQO,59dOOQQ,59e,59eO)jQQO,59eOOQQ,59f,59fO)qQQO,59fOOQQ-E6{-E6{OOQQ,59b,59bOOQQ-E6z-E6zOOQQ,59j,59jOOQQ-E6|-E6|O+VQRO1G.}O+^QQO,59cOOQQ1G/O1G/OOOQQ1G/P1G/POOQQ1G/Q1G/QP+kQQO'#C}O+rQQO1G.}O)PQQO,59cO,PQQO'#Cw",stateData:",[~OtOSPOSQOS~ORUOSUOTUOUUOVROXSOZTO]XO^QO_UO`UOaPObPOcPOdUOeUOfUOgUOhUO~O^]ORvXSvXTvXUvXVvXXvXZvX]vX_vX`vXavXbvXcvXdvXevXfvXgvXhvX~OsvX~P!jOa_Ob_Oc_O~ORUOSUOTUOUUOVROXSOZTO^tO_UO`UOa`Ob`Oc`OdUOeUOfUOgUOhUO~OWaO~P$ZOYcO~P$ZO[eO~P$ZORUOSUOTUOUUOVROXSOZTO^QO_UO`UOaPObPOcPOdUOeUOfUOgUOhUO~O]hOsoX~P%zOajObjOcjO~O^]ORkaSkaTkaUkaVkaXkaZka]ka_ka`kaakabkackadkaekafkagkahka~Oska~P'kO^]O~OWvXYvX[vX~P!jOWnO~P$ZOYoO~P$ZO[pO~P$ZO^]ORkiSkiTkiUkiVkiXkiZki]ki_ki`kiakibkickidkiekifkigkihki~Oski~P)xOWkaYka[ka~P'kO]hO~P$ZOWkiYki[ki~P)xOasObsOcsO~O",goto:"#hwPPPPPPPPPPPPPPPPPPPPPPPPPPx||||!Y!^!d!xPPP#[TYOZeUORSTWZbdfqT[OZQZORiZSWOZQbRQdSQfTZgWbdfqQ^PWk^lmrQl_Qm`RrseVORSTWZbdfq",nodeNames:"\u26A0 LineComment BlockComment String Number Bool Null ( ) { } [ ] ; . Operator Punctuation SpecialVar Identifier QuotedIdentifier Keyword Type Bits Bytes Builtin Script Statement CompositeIdentifier Parens Braces Brackets Statement",maxTerm:38,nodeProps:[["isolate",-4,1,2,3,19,""]],skippedNodes:[0,1,2],repeatNodeCount:3,tokenData:"RORO",tokenizers:[0,E],topRules:{Script:[0,25]},tokenPrec:0});function z(t){let e=t.cursor().moveTo(t.from,-1);for(;/Comment/.test(e.name);)e.moveTo(e.from,-1);return e.node}function y(t,e){let r=t.sliceString(e.from,e.to),a=/^([`'"])(.*)\1$/.exec(r);return a?a[2]:r}function w(t){return t&&(t.name=="Identifier"||t.name=="QuotedIdentifier")}function Ze(t,e){if(e.name=="CompositeIdentifier"){let r=[];for(let a=e.firstChild;a;a=a.nextSibling)w(a)&&r.push(y(t,a));return r}return[y(t,e)]}function W(t,e){for(let r=[];;){if(!e||e.name!=".")return r;let a=z(e);if(!w(a))return r;r.unshift(y(t,a)),e=z(a)}}function Ne(t,e){let r=ie(t).resolveInner(e,-1),a=$e(t.doc,r);return r.name=="Identifier"||r.name=="QuotedIdentifier"||r.name=="Keyword"?{from:r.from,quoted:r.name=="QuotedIdentifier"?t.doc.sliceString(r.from,r.from+1):null,parents:W(t.doc,z(r)),aliases:a}:r.name=="."?{from:e,quoted:null,parents:W(t.doc,r),aliases:a}:{from:e,quoted:null,parents:[],empty:!0,aliases:a}}const Ve=new Set("where group having order union intersect except all distinct limit offset fetch for".split(" "));function $e(t,e){let r;for(let n=e;!r;n=n.parent){if(!n)return null;n.name=="Statement"&&(r=n)}let a=null;for(let n=r.firstChild,s=!1,c=null;n;n=n.nextSibling){let l=n.name=="Keyword"?t.sliceString(n.from,n.to).toLowerCase():null,o=null;if(!s)s=l=="from";else if(l=="as"&&c&&w(n.nextSibling))o=y(t,n.nextSibling);else{if(l&&Ve.has(l))break;c&&w(n)&&(o=y(t,n))}o&&(a||(a=Object.create(null)),a[o]=Ze(t,c)),c=/Identifier$/.test(n.name)?n:null}return a}function Ae(t,e){return t?e.map(r=>Object.assign(Object.assign({},r),{label:r.label[0]==t?r.label:t+r.label+t,apply:void 0})):e}const Ee=/^\w*$/,We=/^[`'"]?\w*[`'"]?$/;function M(t){return t.self&&typeof t.self.label=="string"}class B{constructor(e,r){this.idQuote=e,this.idCaseInsensitive=r,this.list=[],this.children=void 0}child(e){let r=this.children||(this.children=Object.create(null)),a=r[e];return a||(e&&!this.list.some(n=>n.label==e)&&this.list.push(K(e,"type",this.idQuote,this.idCaseInsensitive)),r[e]=new B(this.idQuote,this.idCaseInsensitive))}maybeChild(e){return this.children?this.children[e]:null}addCompletion(e){let r=this.list.findIndex(a=>a.label==e.label);r>-1?this.list[r]=e:this.list.push(e)}addCompletions(e){for(let r of e)this.addCompletion(typeof r=="string"?K(r,"property",this.idQuote,this.idCaseInsensitive):r)}addNamespace(e){Array.isArray(e)?this.addCompletions(e):M(e)?this.addNamespace(e.children):this.addNamespaceObject(e)}addNamespaceObject(e){for(let r of Object.keys(e)){let a=e[r],n=null,s=r.replace(/\\?\./g,l=>l=="."?"\0":l).split("\0"),c=this;M(a)&&(n=a.self,a=a.children);for(let l=0;l{let{parents:v,from:ae,quoted:k,empty:re,aliases:x}=Ne(f.state,f.pos);if(re&&!f.explicit)return null;x&&v.length==1&&(v=x[v[0]]||v);let d=o;for(let O of v){for(;!d.children||!d.children[O];)if(d==o&&u)d=u;else if(d==u&&a)d=d.child(a);else return null;let L=d.maybeChild(O);if(!L)return null;d=L}let ne=k&&f.state.sliceDoc(f.pos,f.pos+1)==k,C=d.list;return d==o&&x&&(C=C.concat(Object.keys(x).map(O=>({label:O,type:"constant"})))),{from:ae,to:ne?f.pos+1:void 0,options:Ae(k,C),validFor:k?We:Ee}}}function Ke(t,e){let r=Object.keys(t).map(a=>({label:e?a.toUpperCase():a,type:t[a]==j?"type":t[a]==R?"keyword":"variable",boost:-1}));return pe(["QuotedIdentifier","SpecialVar","String","LineComment","BlockComment","."],ge(r))}let Fe=De.configure({props:[se.add({Statement:oe()}),le.add({Statement(t,e){return{from:Math.min(t.from+100,e.doc.lineAt(t.from).to),to:t.to}},BlockComment(t){return{from:t.from+2,to:t.to-2}}}),me({Keyword:i.keyword,Type:i.typeName,Builtin:i.standard(i.name),Bits:i.number,Bytes:i.string,Bool:i.bool,Null:i.null,Number:i.number,String:i.string,Identifier:i.name,QuotedIdentifier:i.special(i.string),SpecialVar:i.special(i.name),LineComment:i.lineComment,BlockComment:i.blockComment,Operator:i.operator,"Semi Punctuation":i.punctuation,"( )":i.paren,"{ }":i.brace,"[ ]":i.squareBracket})]});class m{constructor(e,r,a){this.dialect=e,this.language=r,this.spec=a}get extension(){return this.language.extension}static define(e){let r=je(e,e.keywords,e.types,e.builtin),a=ce.define({name:"sql",parser:Fe.configure({tokenizers:[{from:E,to:A(r)}]}),languageData:{commentTokens:{line:"--",block:{open:"/*",close:"*/"}},closeBrackets:{brackets:["(","[","{","'",'"',"`"]}}});return new m(r,a,e)}}function F(t,e=!1){return Ke(t.dialect.words,e)}function G(t,e=!1){return t.language.data.of({autocomplete:F(t,e)})}function Y(t){return t.schema?Me(t.schema,t.tables,t.schemas,t.defaultTable,t.defaultSchema,t.dialect||Q):()=>null}function H(t){return t.schema?(t.dialect||Q).language.data.of({autocomplete:Y(t)}):[]}function Ge(t={}){let e=t.dialect||Q;return new de(e.language,[H(t),G(e,!!t.upperCaseKeywords)])}const Q=m.define({}),Ye=m.define({charSetCasts:!0,doubleDollarQuotedStrings:!0,operatorChars:"+-*/<>=~!@#%^&|`?",specialVar:"",keywords:_+"a abort abs absent access according ada admin aggregate alias also always analyse analyze array_agg array_max_cardinality asensitive assert assignment asymmetric atomic attach attribute attributes avg backward base64 begin_frame begin_partition bernoulli bit_length blocked bom c cache called cardinality catalog_name ceil ceiling chain char_length character_length character_set_catalog character_set_name character_set_schema characteristics characters checkpoint class class_origin cluster coalesce cobol collation_catalog collation_name collation_schema collect column_name columns command_function command_function_code comment comments committed concurrently condition_number configuration conflict connection_name constant constraint_catalog constraint_name constraint_schema contains content control conversion convert copy corr cost covar_pop covar_samp csv cume_dist current_catalog current_row current_schema cursor_name database datalink datatype datetime_interval_code datetime_interval_precision db debug defaults defined definer degree delimiter delimiters dense_rank depends derived detach detail dictionary disable discard dispatch dlnewcopy dlpreviouscopy dlurlcomplete dlurlcompleteonly dlurlcompletewrite dlurlpath dlurlpathonly dlurlpathwrite dlurlscheme dlurlserver dlvalue document dump dynamic_function dynamic_function_code element elsif empty enable encoding encrypted end_frame end_partition endexec enforced enum errcode error event every exclude excluding exclusive exp explain expression extension extract family file filter final first_value flag floor following force foreach fortran forward frame_row freeze fs functions fusion g generated granted greatest groups handler header hex hierarchy hint id ignore ilike immediately immutable implementation implicit import include including increment indent index indexes info inherit inherits inline insensitive instance instantiable instead integrity intersection invoker isnull k key_member key_type label lag last_value lead leakproof least length library like_regex link listen ln load location lock locked log logged lower m mapping matched materialized max max_cardinality maxvalue member merge message message_length message_octet_length message_text min minvalue mod mode more move multiset mumps name namespace nfc nfd nfkc nfkd nil normalize normalized nothing notice notify notnull nowait nth_value ntile nullable nullif nulls number occurrences_regex octet_length octets off offset oids operator options ordering others over overlay overriding owned owner p parallel parameter_mode parameter_name parameter_ordinal_position parameter_specific_catalog parameter_specific_name parameter_specific_schema parser partition pascal passing passthrough password percent percent_rank percentile_cont percentile_disc perform period permission pg_context pg_datatype_name pg_exception_context pg_exception_detail pg_exception_hint placing plans pli policy portion position position_regex power precedes preceding prepared print_strict_params procedural procedures program publication query quote raise range rank reassign recheck recovery refresh regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy regr_syy reindex rename repeatable replace replica requiring reset respect restart restore result_oid returned_cardinality returned_length returned_octet_length returned_sqlstate returning reverse routine_catalog routine_name routine_schema routines row_count row_number rowtype rule scale schema_name schemas scope scope_catalog scope_name scope_schema security selective self sensitive sequence sequences serializable server server_name setof share show simple skip slice snapshot source specific_name sqlcode sqlerror sqrt stable stacked standalone statement statistics stddev_pop stddev_samp stdin stdout storage strict strip structure style subclass_origin submultiset subscription substring substring_regex succeeds sum symmetric sysid system system_time t table_name tables tablesample tablespace temp template ties token top_level_count transaction_active transactions_committed transactions_rolled_back transform transforms translate translate_regex trigger_catalog trigger_name trigger_schema trim trim_array truncate trusted type types uescape unbounded uncommitted unencrypted unlink unlisten unlogged unnamed untyped upper uri use_column use_variable user_defined_type_catalog user_defined_type_code user_defined_type_name user_defined_type_schema vacuum valid validate validator value_of var_pop var_samp varbinary variable_conflict variadic verbose version versioning views volatile warning whitespace width_bucket window within wrapper xmlagg xmlattributes xmlbinary xmlcast xmlcomment xmlconcat xmldeclaration xmldocument xmlelement xmlexists xmlforest xmliterate xmlnamespaces xmlparse xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltext xmlvalidate yes",types:h+"bigint int8 bigserial serial8 varbit bool box bytea cidr circle precision float8 inet int4 json jsonb line lseg macaddr macaddr8 money numeric pg_lsn point polygon float4 int2 smallserial serial2 serial serial4 text timetz timestamptz tsquery tsvector txid_snapshot uuid xml"}),J="accessible algorithm analyze asensitive authors auto_increment autocommit avg avg_row_length binlog btree cache catalog_name chain change changed checkpoint checksum class_origin client_statistics coalesce code collations columns comment committed completion concurrent consistent contains contributors convert database databases day_hour day_microsecond day_minute day_second delay_key_write delayed delimiter des_key_file dev_pop dev_samp deviance directory disable discard distinctrow div dual dumpfile enable enclosed ends engine engines enum errors escaped even event events every explain extended fast field fields flush force found_rows fulltext grants handler hash high_priority hosts hour_microsecond hour_minute hour_second ignore ignore_server_ids import index index_statistics infile innodb insensitive insert_method install invoker iterate keys kill linear lines list load lock logs low_priority master master_heartbeat_period master_ssl_verify_server_cert masters max max_rows maxvalue message_text middleint migrate min min_rows minute_microsecond minute_second mod mode modify mutex mysql_errno no_write_to_binlog offline offset one online optimize optionally outfile pack_keys parser partition partitions password phase plugin plugins prev processlist profile profiles purge query quick range read_write rebuild recover regexp relaylog remove rename reorganize repair repeatable replace require resume rlike row_format rtree schedule schema_name schemas second_microsecond security sensitive separator serializable server share show slave slow snapshot soname spatial sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_no_cache sql_small_result ssl starting starts std stddev stddev_pop stddev_samp storage straight_join subclass_origin sum suspend table_name table_statistics tables tablespace terminated triggers truncate uncommitted uninstall unlock upgrade use use_frm user_resources user_statistics utc_date utc_time utc_timestamp variables views warnings xa xor year_month zerofill",ee=h+"bool blob long longblob longtext medium mediumblob mediumint mediumtext tinyblob tinyint tinytext text bigint int1 int2 int3 int4 int8 float4 float8 varbinary varcharacter precision datetime unsigned signed",te="charset clear edit ego help nopager notee nowarning pager print prompt quit rehash source status system tee",He=m.define({operatorChars:"*+-%<>!=&|^",charSetCasts:!0,doubleQuotedStrings:!0,unquotedBitLiterals:!0,hashComments:!0,spaceAfterDashes:!0,specialVar:"@?",identifierQuotes:"`",keywords:_+"group_concat "+J,types:ee,builtin:te}),Je=m.define({operatorChars:"*+-%<>!=&|^",charSetCasts:!0,doubleQuotedStrings:!0,unquotedBitLiterals:!0,hashComments:!0,spaceAfterDashes:!0,specialVar:"@?",identifierQuotes:"`",keywords:_+"always generated groupby_concat hard persistent shutdown soft virtual "+J,types:ee,builtin:te}),et=m.define({keywords:_+"trigger proc view index for add constraint key primary foreign collate clustered nonclustered declare exec go if use index holdlock nolock nowait paglock pivot readcommitted readcommittedlock readpast readuncommitted repeatableread rowlock serializable snapshot tablock tablockx unpivot updlock with",types:h+"bigint smallint smallmoney tinyint money real text nvarchar ntext varbinary image hierarchyid uniqueidentifier sql_variant xml",builtin:"binary_checksum checksum connectionproperty context_info current_request_id error_line error_message error_number error_procedure error_severity error_state formatmessage get_filestream_transaction_context getansinull host_id host_name isnull isnumeric min_active_rowversion newid newsequentialid rowcount_big xact_state object_id",operatorChars:"*+-%<>!=^&|/",specialVar:"@"}),tt=m.define({keywords:_+"abort analyze attach autoincrement conflict database detach exclusive fail glob ignore index indexed instead isnull notnull offset plan pragma query raise regexp reindex rename replace temp vacuum virtual",types:h+"bool blob long longblob longtext medium mediumblob mediumint mediumtext tinyblob tinyint tinytext text bigint int2 int8 unsigned signed real",builtin:"auth backup bail changes clone databases dbinfo dump echo eqp explain fullschema headers help import imposter indexes iotrace lint load log mode nullvalue once print prompt quit restore save scanstats separator shell show stats system tables testcase timeout timer trace vfsinfo vfslist vfsname width",operatorChars:"*+-%<>!=&|/~",identifierQuotes:'`"',specialVar:"@:?$"}),at=m.define({keywords:"add all allow alter and any apply as asc authorize batch begin by clustering columnfamily compact consistency count create custom delete desc distinct drop each_quorum exists filtering from grant if in index insert into key keyspace keyspaces level limit local_one local_quorum modify nan norecursive nosuperuser not of on one order password permission permissions primary quorum rename revoke schema select set storage superuser table three to token truncate ttl two type unlogged update use user users using values where with writetime infinity NaN",types:h+"ascii bigint blob counter frozen inet list map static text timeuuid tuple uuid varint",slashComments:!0}),rt=m.define({keywords:_+"abort accept access add all alter and any arraylen as asc assert assign at attributes audit authorization avg base_table begin between binary_integer body by case cast char_base check close cluster clusters colauth column comment commit compress connected constant constraint crash create current currval cursor data_base database dba deallocate debugoff debugon declare default definition delay delete desc digits dispose distinct do drop else elseif elsif enable end entry exception exception_init exchange exclusive exists external fast fetch file for force form from function generic goto grant group having identified if immediate in increment index indexes indicator initial initrans insert interface intersect into is key level library like limited local lock log logging loop master maxextents maxtrans member minextents minus mislabel mode modify multiset new next no noaudit nocompress nologging noparallel not nowait number_base of off offline on online only option or order out package parallel partition pctfree pctincrease pctused pls_integer positive positiven pragma primary prior private privileges procedure public raise range raw rebuild record ref references refresh rename replace resource restrict return returning returns reverse revoke rollback row rowid rowlabel rownum rows run savepoint schema segment select separate set share snapshot some space split sql start statement storage subtype successful synonym tabauth table tables tablespace task terminate then to trigger truncate type union unique unlimited unrecoverable unusable update use using validate value values variable view views when whenever where while with work",builtin:"appinfo arraysize autocommit autoprint autorecovery autotrace blockterminator break btitle cmdsep colsep compatibility compute concat copycommit copytypecheck define echo editfile embedded feedback flagger flush heading headsep instance linesize lno loboffset logsource longchunksize markup native newpage numformat numwidth pagesize pause pno recsep recsepchar repfooter repheader serveroutput shiftinout show showmode spool sqlblanklines sqlcase sqlcode sqlcontinue sqlnumber sqlpluscompatibility sqlprefix sqlprompt sqlterminator suffix tab term termout timing trimout trimspool ttitle underline verify version wrap",types:h+"ascii bfile bfilename bigserial bit blob dec long number nvarchar nvarchar2 serial smallint string text uid varchar2 xml",operatorChars:"*/+-%<>!=~",doubleQuotedStrings:!0,charSetCasts:!0,plsqlQuotingMechanism:!0});export{at as Cassandra,et as MSSQL,Je as MariaSQL,He as MySQL,rt as PLSQL,Ye as PostgreSQL,m as SQLDialect,tt as SQLite,Q as StandardSQL,G as keywordCompletion,F as keywordCompletionSource,H as schemaCompletion,Y as schemaCompletionSource,Ge as sql}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/lang-xml.js b/Resources/Public/JavaScript/Contrib/@codemirror/lang-xml.js new file mode 100644 index 0000000..097cd9d --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/lang-xml.js @@ -0,0 +1 @@ +import{parser as L}from"@lezer/xml";import{syntaxTree as E,LRLanguage as _,indentNodeProp as w,foldNodeProp as x,bracketMatchingHandle as M,LanguageSupport as k}from"@codemirror/language";import{EditorSelection as D}from"@codemirror/state";import{EditorView as P}from"@codemirror/view";function v(e,t){let i=t&&t.getChild("TagName");return i?e.sliceString(i.from,i.to):""}function N(e,t){let i=t&&t.firstChild;return!i||i.name!="OpenTag"?"":v(e,i)}function B(e,t,i){let n=t&&t.getChildren("Attribute").find(o=>o.from<=i&&o.to>=i),a=n&&n.getChild("AttributeName");return a?e.sliceString(a.from,a.to):""}function S(e){for(let t=e&&e.parent;t;t=t.parent)if(t.name=="Element")return t;return null}function H(e,t){var i;let n=E(e).resolveInner(t,-1),a=null;for(let o=n;!a&&o.parent;o=o.parent)(o.name=="OpenTag"||o.name=="CloseTag"||o.name=="SelfClosingTag"||o.name=="MismatchedCloseTag")&&(a=o);if(a&&(a.to>t||a.lastChild.type.isError)){let o=a.parent;if(n.name=="TagName")return a.name=="CloseTag"||a.name=="MismatchedCloseTag"?{type:"closeTag",from:n.from,context:o}:{type:"openTag",from:n.from,context:S(o)};if(n.name=="AttributeName")return{type:"attrName",from:n.from,context:a};if(n.name=="AttributeValue")return{type:"attrValue",from:n.from,context:a};let s=n==a||n.name=="Attribute"?n.childBefore(t):n;return s?.name=="StartTag"?{type:"openTag",from:t,context:S(o)}:s?.name=="StartCloseTag"&&s.to<=t?{type:"closeTag",from:t,context:o}:s?.name=="Is"?{type:"attrValue",from:t,context:a}:s?{type:"attrName",from:t,context:a}:null}else if(n.name=="StartCloseTag")return{type:"closeTag",from:t,context:n.parent};for(;n.parent&&n.to==t&&!(!((i=n.lastChild)===null||i===void 0)&&i.type.isError);)n=n.parent;return n.name=="Element"||n.name=="Text"||n.name=="Document"?{type:"tag",from:t,context:n.name=="Element"?n:S(n)}:null}class R{constructor(t,i,n){this.attrs=i,this.attrValues=n,this.children=[],this.name=t.name,this.completion=Object.assign(Object.assign({type:"type"},t.completion||{}),{label:this.name}),this.openCompletion=Object.assign(Object.assign({},this.completion),{label:"<"+this.name}),this.closeCompletion=Object.assign(Object.assign({},this.completion),{label:"",boost:2}),this.closeNameCompletion=Object.assign(Object.assign({},this.completion),{label:this.name+">"}),this.text=t.textContent?t.textContent.map(a=>({label:a,type:"text"})):[]}}const j=/^[:\-\.\w\u00b7-\uffff]*$/;function A(e){return Object.assign(Object.assign({type:"property"},e.completion||{}),{label:e.name})}function V(e){return typeof e=="string"?{label:`"${e}"`,type:"constant"}:/^"/.test(e.label)?e:Object.assign(Object.assign({},e),{label:`"${e.label}"`})}function $(e,t){let i=[],n=[],a=Object.create(null);for(let l of t){let p=A(l);i.push(p),l.global&&n.push(p),l.values&&(a[l.name]=l.values.map(V))}let o=[],s=[],h=Object.create(null);for(let l of e){let p=n,u=a;l.attributes&&(p=p.concat(l.attributes.map(r=>typeof r=="string"?i.find(b=>b.label==r)||{label:r,type:"property"}:(r.values&&(u==a&&(u=Object.create(u)),u[r.name]=r.values.map(V)),A(r)))));let d=new R(l,p,u);h[d.name]=d,o.push(d),l.top&&s.push(d)}s.length||(s=o);for(let l=0;l{var p;let{doc:u}=l.state,d=H(l.state,l.pos);if(!d||d.type=="tag"&&!l.explicit)return null;let{type:r,from:b,context:c}=d;if(r=="openTag"){let m=s,f=N(u,c);if(f){let g=h[f];m=g?.children||o}return{from:b,options:m.map(g=>g.completion),validFor:j}}else if(r=="closeTag"){let m=N(u,c);return m?{from:b,to:l.pos+(u.sliceString(l.pos,l.pos+1)==">"?1:0),options:[((p=h[m])===null||p===void 0?void 0:p.closeNameCompletion)||{label:m+">",type:"type"}],validFor:j}:null}else if(r=="attrName"){let m=h[v(u,c)];return{from:b,options:m?.attrs||n,validFor:j}}else if(r=="attrValue"){let m=B(u,c,b);if(!m)return null;let f=h[v(u,c)],g=(f?.attrValues||a)[m];return!g||!g.length?null:{from:b,to:l.pos+(u.sliceString(l.pos,l.pos+1)=='"'?1:0),options:g,validFor:/^"[^"]*"?$/}}else if(r=="tag"){let m=N(u,c),f=h[m],g=[],C=c&&c.lastChild;m&&(!C||C.name!="CloseTag"||v(u,C)!=m)&&g.push(f?f.closeCompletion:{label:"",type:"type",boost:2});let O=g.concat((f?.children||(c?o:s)).map(T=>T.openCompletion));if(c&&f?.text.length){let T=c.firstChild;T.to>l.pos-20&&!/\S/.test(l.state.sliceDoc(T.to,l.pos))&&(O=O.concat(f.text))}return{from:b,options:O,validFor:/^<\/?[:\-\.\w\u00b7-\uffff]*$/}}else return null}}const y=_.define({name:"xml",parser:L.configure({props:[w.add({Element(e){let t=/^\s*<\//.test(e.textAfter);return e.lineIndent(e.node.from)+(t?0:e.unit)},"OpenTag CloseTag SelfClosingTag"(e){return e.column(e.node.from)+e.unit}}),x.add({Element(e){let t=e.firstChild,i=e.lastChild;return!t||t.name!="OpenTag"?null:{from:t.to,to:i.name=="CloseTag"?i.from:e.to}}}),M.add({"OpenTag CloseTag":e=>e.getChild("TagName")})]}),languageData:{commentTokens:{block:{open:""}},indentOnInput:/^\s*<\/$/}});function q(e={}){let t=[y.data.of({autocomplete:$(e.elements||[],e.attributes||[])})];return e.autoCloseTags!==!1&&t.push(F),new k(y,t)}function I(e,t,i=e.length){if(!t)return"";let n=t.firstChild,a=n&&n.getChild("TagName");return a?e.sliceString(a.from,Math.min(a.to,i)):""}const F=P.inputHandler.of((e,t,i,n,a)=>{if(e.composing||e.state.readOnly||t!=i||n!=">"&&n!="/"||!y.isActiveAt(e.state,t,-1))return!1;let o=a(),{state:s}=o,h=s.changeByRange(l=>{var p,u,d;let{head:r}=l,b=s.doc.sliceString(r-1,r)==n,c=E(s).resolveInner(r,-1),m;if(b&&n==">"&&c.name=="EndTag"){let f=c.parent;if(((u=(p=f.parent)===null||p===void 0?void 0:p.lastChild)===null||u===void 0?void 0:u.name)!="CloseTag"&&(m=I(s.doc,f.parent,r))){let g=r+(s.doc.sliceString(r,r+1)===">"?1:0),C=``;return{range:l,changes:{from:r,to:g,insert:C}}}}else if(b&&n=="/"&&c.name=="StartCloseTag"){let f=c.parent;if(c.from==r-2&&((d=f.lastChild)===null||d===void 0?void 0:d.name)!="CloseTag"&&(m=I(s.doc,f,r))){let g=r+(s.doc.sliceString(r,r+1)===">"?1:0),C=`${m}>`;return{range:D.cursor(r+C.length,-1),changes:{from:r,to:g,insert:C}}}}return{range:l}});return h.changes.empty?!1:(e.dispatch([o,s.update(h,{userEvent:"input.complete",scrollIntoView:!0})]),!0)});export{F as autoCloseTags,$ as completeFromSchema,q as xml,y as xmlLanguage}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/language.js b/Resources/Public/JavaScript/Contrib/@codemirror/language.js new file mode 100644 index 0000000..b6fac24 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/language.js @@ -0,0 +1,3 @@ +import{NodeProp as y,IterMode as he,Tree as b,TreeFragment as H,Parser as yt,NodeType as U,NodeSet as fe}from"@lezer/common";import{StateEffect as j,StateField as Q,Facet as v,EditorState as L,countColumn as ue,combineConfig as vt,RangeSet as xt,RangeSetBuilder as X,Prec as St}from"@codemirror/state";import{ViewPlugin as V,logException as ce,EditorView as w,Decoration as p,WidgetType as Tt,gutter as de,GutterMarker as pe,Direction as $}from"@codemirror/view";import{tags as u,tagHighlighter as ge,highlightTree as me,styleTags as ke}from"@lezer/highlight";import{StyleModule as Pt}from"style-mod";var Y;const P=new y;function Z(n){return v.define({combine:n?t=>t.concat(n):void 0})}const At=new y;class g{constructor(t,e,r=[],i=""){this.data=t,this.name=i,L.prototype.hasOwnProperty("tree")||Object.defineProperty(L.prototype,"tree",{get(){return m(this)}}),this.parser=e,this.extension=[S.of(this),L.languageData.of((s,o,l)=>{let a=Ct(s,o,l),h=a.type.prop(P);if(!h)return[];let f=s.facet(h),c=a.type.prop(At);if(c){let k=a.resolve(o-a.from,l);for(let d of c)if(d.test(k,s)){let x=s.facet(d.facet);return d.type=="replace"?x:x.concat(f)}}return f})].concat(r)}isActiveAt(t,e,r=-1){return Ct(t,e,r).type.prop(P)==this.data}findRegions(t){let e=t.facet(S);if(e?.data==this.data)return[{from:0,to:t.doc.length}];if(!e||!e.allowsNesting)return[];let r=[],i=(s,o)=>{if(s.prop(P)==this.data){r.push({from:o,to:o+s.length});return}let l=s.prop(y.mounted);if(l){if(l.tree.prop(P)==this.data){if(l.overlay)for(let a of l.overlay)r.push({from:a.from+o,to:a.to+o});else r.push({from:o,to:o+s.length});return}else if(l.overlay){let a=r.length;if(i(l.tree,l.overlay[0].from+o),r.length>a)return}}for(let a=0;ar.isTop?e:void 0)]}),t.name)}configure(t,e){return new J(this.data,this.parser.configure(t),e||this.name)}get allowsNesting(){return this.parser.hasWrappers()}}function m(n){let t=n.field(g.state,!1);return t?t.tree:b.empty}function It(n,t,e=50){var r;let i=(r=n.field(g.state,!1))===null||r===void 0?void 0:r.context;if(!i)return null;let s=i.viewport;i.updateViewport({from:0,to:t});let o=i.isDone(t)||i.work(e,t)?i.tree:null;return i.updateViewport(s),o}function be(n,t=n.doc.length){var e;return((e=n.field(g.state,!1))===null||e===void 0?void 0:e.context.isDone(t))||!1}function we(n,t=n.viewport.to,e=100){let r=It(n.state,t,e);return r!=m(n.state)&&n.dispatch({}),!!r}function ye(n){var t;return((t=n.plugin(Lt))===null||t===void 0?void 0:t.isWorking())||!1}class Dt{constructor(t){this.doc=t,this.cursorPos=0,this.string="",this.cursor=t.iter()}get length(){return this.doc.length}syncTo(t){return this.string=this.cursor.next(t-this.cursorPos).value,this.cursorPos=t+this.string.length,this.cursorPos-this.string.length}chunk(t){return this.syncTo(t),this.string}get lineChunks(){return!0}read(t,e){let r=this.cursorPos-this.string.length;return t=this.cursorPos?this.doc.sliceString(t,e):this.string.slice(t-r,e-r)}}let N=null;class C{constructor(t,e,r=[],i,s,o,l,a){this.parser=t,this.state=e,this.fragments=r,this.tree=i,this.treeLen=s,this.viewport=o,this.skipped=l,this.scheduleOn=a,this.parse=null,this.tempSkipped=[]}static create(t,e,r){return new C(t,e,[],b.empty,0,r,[],null)}startParse(){return this.parser.startParse(new Dt(this.state.doc),this.fragments)}work(t,e){return e!=null&&e>=this.state.doc.length&&(e=void 0),this.tree!=b.empty&&this.isDone(e??this.state.doc.length)?(this.takeTree(),!0):this.withContext(()=>{var r;if(typeof t=="number"){let i=Date.now()+t;t=()=>Date.now()>i}for(this.parse||(this.parse=this.startParse()),e!=null&&(this.parse.stoppedAt==null||this.parse.stoppedAt>e)&&e=this.treeLen&&((this.parse.stoppedAt==null||this.parse.stoppedAt>t)&&this.parse.stopAt(t),this.withContext(()=>{for(;!(e=this.parse.advance()););}),this.treeLen=t,this.tree=e,this.fragments=this.withoutTempSkipped(H.addTree(this.tree,this.fragments,!0)),this.parse=null)}withContext(t){let e=N;N=this;try{return t()}finally{N=e}}withoutTempSkipped(t){for(let e;e=this.tempSkipped.pop();)t=Ot(t,e.from,e.to);return t}changes(t,e){let{fragments:r,tree:i,treeLen:s,viewport:o,skipped:l}=this;if(this.takeTree(),!t.empty){let a=[];if(t.iterChangedRanges((h,f,c,k)=>a.push({fromA:h,toA:f,fromB:c,toB:k})),r=H.applyChanges(r,a),i=b.empty,s=0,o={from:t.mapPos(o.from,-1),to:t.mapPos(o.to,1)},this.skipped.length){l=[];for(let h of this.skipped){let f=t.mapPos(h.from,1),c=t.mapPos(h.to,-1);ft.from&&(this.fragments=Ot(this.fragments,i,s),this.skipped.splice(r--,1))}return this.skipped.length>=e?!1:(this.reset(),!0)}reset(){this.parse&&(this.takeTree(),this.parse=null)}skipUntilInView(t,e){this.skipped.push({from:t,to:e})}static getSkippingParser(t){return new class extends yt{createParse(e,r,i){let s=i[0].from,o=i[i.length-1].to;return{parsedPos:s,advance(){let a=N;if(a){for(let h of i)a.tempSkipped.push(h);t&&(a.scheduleOn=a.scheduleOn?Promise.all([a.scheduleOn,t]):t)}return this.parsedPos=o,new b(U.none,[],[],o-s)},stoppedAt:null,stopAt(){}}}}}isDone(t){t=Math.min(t,this.state.doc.length);let e=this.fragments;return this.treeLen>=t&&e.length&&e[0].from==0&&e[0].to>=t}static get(){return N}}function Ot(n,t,e){return H.applyChanges(n,[{fromA:t,toA:e,fromB:t,toB:e}])}class M{constructor(t){this.context=t,this.tree=t.tree}apply(t){if(!t.docChanged&&this.tree==this.context.tree)return this;let e=this.context.changes(t.changes,t.state),r=this.context.treeLen==t.startState.doc.length?void 0:Math.max(t.changes.mapPos(this.context.treeLen),e.viewport.to);return e.work(20,r)||e.takeTree(),new M(e)}static init(t){let e=Math.min(3e3,t.doc.length),r=C.create(t.facet(S).parser,t,{from:0,to:e});return r.work(20,e)||r.takeTree(),new M(r)}}g.state=Q.define({create:M.init,update(n,t){for(let e of t.effects)if(e.is(g.setState))return e.value;return t.startState.facet(S)!=t.state.facet(S)?M.init(t.state):n.apply(t)}});let Mt=n=>{let t=setTimeout(()=>n(),500);return()=>clearTimeout(t)};typeof requestIdleCallback<"u"&&(Mt=n=>{let t=-1,e=setTimeout(()=>{t=requestIdleCallback(n,{timeout:400})},100);return()=>t<0?clearTimeout(e):cancelIdleCallback(t)});const tt=typeof navigator<"u"&&(!((Y=navigator.scheduling)===null||Y===void 0)&&Y.isInputPending)?()=>navigator.scheduling.isInputPending():null,Lt=V.fromClass(class{constructor(t){this.view=t,this.working=null,this.workScheduled=0,this.chunkEnd=-1,this.chunkBudget=-1,this.work=this.work.bind(this),this.scheduleWork()}update(t){let e=this.view.state.field(g.state).context;(e.updateViewport(t.view.viewport)||this.view.viewport.to>e.treeLen)&&this.scheduleWork(),(t.docChanged||t.selectionSet)&&(this.view.hasFocus&&(this.chunkBudget+=50),this.scheduleWork()),this.checkAsyncSchedule(e)}scheduleWork(){if(this.working)return;let{state:t}=this.view,e=t.field(g.state);(e.tree!=e.context.tree||!e.context.isDone(t.doc.length))&&(this.working=Mt(this.work))}work(t){this.working=null;let e=Date.now();if(this.chunkEndi+1e3,a=s.context.work(()=>tt&&tt()||Date.now()>o,i+(l?0:1e5));this.chunkBudget-=Date.now()-e,(a||this.chunkBudget<=0)&&(s.context.takeTree(),this.view.dispatch({effects:g.setState.of(new M(s.context))})),this.chunkBudget>0&&!(a&&!l)&&this.scheduleWork(),this.checkAsyncSchedule(s.context)}checkAsyncSchedule(t){t.scheduleOn&&(this.workScheduled++,t.scheduleOn.then(()=>this.scheduleWork()).catch(e=>ce(this.view.state,e)).then(()=>this.workScheduled--),t.scheduleOn=null)}destroy(){this.working&&this.working()}isWorking(){return!!(this.working||this.workScheduled>0)}},{eventHandlers:{focus(){this.scheduleWork()}}}),S=v.define({combine(n){return n.length?n[0]:null},enables:n=>[g.state,Lt,w.contentAttributes.compute([n],t=>{let e=t.facet(n);return e&&e.name?{"data-language":e.name}:{}})]});class ve{constructor(t,e=[]){this.language=t,this.support=e,this.extension=[t,e]}}class bt{constructor(t,e,r,i,s,o=void 0){this.name=t,this.alias=e,this.extensions=r,this.filename=i,this.loadFunc=s,this.support=o,this.loading=null}load(){return this.loading||(this.loading=this.loadFunc().then(t=>this.support=t,t=>{throw this.loading=null,t}))}static of(t){let{load:e,support:r}=t;if(!e){if(!r)throw new RangeError("Must pass either 'load' or 'support' to LanguageDescription.of");e=()=>Promise.resolve(r)}return new bt(t.name,(t.alias||[]).concat(t.name).map(i=>i.toLowerCase()),t.extensions||[],t.filename,e,r)}static matchFilename(t,e){for(let i of t)if(i.filename&&i.filename.test(e))return i;let r=/\.([^.]+)$/.exec(e);if(r){for(let i of t)if(i.extensions.indexOf(r[1])>-1)return i}return null}static matchLanguageName(t,e,r=!0){e=e.toLowerCase();for(let i of t)if(i.alias.some(s=>s==e))return i;if(r)for(let i of t)for(let s of i.alias){let o=e.indexOf(s);if(o>-1&&(s.length>2||!/\w/.test(e[o-1])&&!/\w/.test(e[o+s.length])))return i}return null}}const et=v.define(),nt=v.define({combine:n=>{if(!n.length)return" ";let t=n[0];if(!t||/\S/.test(t)||Array.from(t).some(e=>e!=t[0]))throw new Error("Invalid indent unit: "+JSON.stringify(n[0]));return t}});function B(n){let t=n.facet(nt);return t.charCodeAt(0)==9?n.tabSize*t.length:t.length}function rt(n,t){let e="",r=n.tabSize,i=n.facet(nt)[0];if(i==" "){for(;t>=r;)e+=" ",t-=r;i=" "}for(let s=0;s=t?Se(n,e,t):null}function xe(n,t,e){let r=Object.create(null),i=new q(n,{overrideIndentation:o=>{var l;return(l=r[o])!==null&&l!==void 0?l:-1}}),s=[];for(let o=t;o<=e;){let l=n.doc.lineAt(o);o=l.to+1;let a=it(i,l.from);if(a==null)continue;/\S/.test(l.text)||(a=0);let h=/^\s*/.exec(l.text)[0],f=rt(n,a);h!=f&&(r[l.from]=a,s.push({from:l.from,to:l.from+h.length,insert:f}))}return n.changes(s)}class q{constructor(t,e={}){this.state=t,this.options=e,this.unit=B(t)}lineAt(t,e=1){let r=this.state.doc.lineAt(t),{simulateBreak:i,simulateDoubleBreak:s}=this.options;return i!=null&&i>=r.from&&i<=r.to?s&&i==t?{text:"",from:t}:(e<0?i-1&&(s+=o-this.countColumn(r,r.search(/\S|$/))),s}countColumn(t,e=t.length){return ue(t,this.state.tabSize,e)}lineIndent(t,e=1){let{text:r,from:i}=this.lineAt(t,e),s=this.options.overrideIndentation;if(s){let o=s(i);if(o>-1)return o}return this.countColumn(r,r.search(/\S|$/))}get simulatedBreak(){return this.options.simulateBreak||null}}const Nt=new y;function Se(n,t,e){let r=t.resolveStack(e),i=r.node.enterUnfinishedNodesBefore(e);if(i!=r.node){let s=[];for(let o=i;o!=r.node;o=o.parent)s.push(o);for(let o=s.length-1;o>=0;o--)r={node:s[o],next:r}}return Bt(r,n,e)}function Bt(n,t,e){for(let r=n;r;r=r.next){let i=Pe(r.node);if(i)return i(_.create(t,e,r))}return 0}function Te(n){return n.pos==n.options.simulateBreak&&n.options.simulateDoubleBreak}function Pe(n){let t=n.type.prop(Nt);if(t)return t;let e=n.firstChild,r;if(e&&(r=e.type.prop(y.closedBy))){let i=n.lastChild,s=i&&r.indexOf(i.name)>-1;return o=>Rt(o,!0,1,void 0,s&&!Te(o)?i.from:void 0)}return n.parent==null?Ae:null}function Ae(){return 0}class _ extends q{constructor(t,e,r){super(t.state,t.options),this.base=t,this.pos=e,this.context=r}get node(){return this.context.node}static create(t,e,r){return new _(t,e,r)}get textAfter(){return this.textAfterPos(this.pos)}get baseIndent(){return this.baseIndentFor(this.node)}baseIndentFor(t){let e=this.state.doc.lineAt(t.from);for(;;){let r=t.resolve(e.from);for(;r.parent&&r.parent.from==r.from;)r=r.parent;if(Ce(r,t))break;e=this.state.doc.lineAt(r.from)}return this.lineIndent(e.from)}continue(){return Bt(this.context.next,this.base,this.pos)}}function Ce(n,t){for(let e=t;e;e=e.parent)if(n==e)return!0;return!1}function Ie(n){let t=n.node,e=t.childAfter(t.from),r=t.lastChild;if(!e)return null;let i=n.options.simulateBreak,s=n.state.doc.lineAt(e.from),o=i==null||i<=s.from?s.to:Math.min(s.to,i);for(let l=e.to;;){let a=t.childAfter(l);if(!a||a==r)return null;if(!a.type.isSkipped)return a.fromRt(r,t,e,n)}function Rt(n,t,e,r,i){let s=n.textAfter,o=s.match(/^\s*/)[0].length,l=r&&s.slice(o,o+r.length)==r||i==n.pos+o,a=t?Ie(n):null;return a?l?n.column(a.from):n.column(a.to):n.baseIndent+(l?0:n.unit*e)}const Oe=n=>n.baseIndent;function Me({except:n,units:t=1}={}){return e=>{let r=n&&n.test(e.textAfter);return e.baseIndent+(r?0:t*e.unit)}}const Le=200;function Ne(){return L.transactionFilter.of(n=>{if(!n.docChanged||!n.isUserEvent("input.type")&&!n.isUserEvent("input.complete"))return n;let t=n.startState.languageDataAt("indentOnInput",n.startState.selection.main.head);if(!t.length)return n;let e=n.newDoc,{head:r}=n.newSelection.main,i=e.lineAt(r);if(r>i.from+Le)return n;let s=e.sliceString(i.from,r);if(!t.some(h=>h.test(s)))return n;let{state:o}=n,l=-1,a=[];for(let{head:h}of o.selection.ranges){let f=o.doc.lineAt(h);if(f.from==l)continue;l=f.from;let c=it(o,f.from);if(c==null)continue;let k=/^\s*/.exec(f.text)[0],d=rt(o,c);k!=d&&a.push({from:f.from,to:f.from+k.length,insert:d})}return a.length?[n,{changes:a,sequential:!0}]:n})}const Ft=v.define(),Et=new y;function Be(n){let t=n.firstChild,e=n.lastChild;return t&&t.toe)continue;if(s&&l.from=t&&h.to>e&&(s=h)}}return s}function Fe(n){let t=n.lastChild;return t&&t.to==n.to&&t.type.isError}function D(n,t,e){for(let r of n.facet(Ft)){let i=r(n,t,e);if(i)return i}return Re(n,t,e)}function Wt(n,t){let e=t.mapPos(n.from,1),r=t.mapPos(n.to,-1);return e>=r?void 0:{from:e,to:r}}const O=j.define({map:Wt}),A=j.define({map:Wt});function st(n){let t=[];for(let{head:e}of n.state.selection.ranges)t.some(r=>r.from<=e&&r.to>=e)||t.push(n.lineBlockAt(e));return t}const T=Q.define({create(){return p.none},update(n,t){n=n.map(t.changes);for(let e of t.effects)if(e.is(O)&&!We(n,e.value.from,e.value.to)){let{preparePlaceholder:r}=t.state.facet(lt),i=r?p.replace({widget:new $e(r(t.state,e.value))}):qt;n=n.update({add:[i.range(e.value.from,e.value.to)]})}else e.is(A)&&(n=n.update({filter:(r,i)=>e.value.from!=r||e.value.to!=i,filterFrom:e.value.from,filterTo:e.value.to}));if(t.selection){let e=!1,{head:r}=t.selection.main;n.between(r,r,(i,s)=>{ir&&(e=!0)}),e&&(n=n.update({filterFrom:r,filterTo:r,filter:(i,s)=>s<=r||i>=r}))}return n},provide:n=>w.decorations.from(n),toJSON(n,t){let e=[];return n.between(0,t.doc.length,(r,i)=>{e.push(r,i)}),e},fromJSON(n){if(!Array.isArray(n)||n.length%2)throw new RangeError("Invalid JSON for fold state");let t=[];for(let e=0;e{(!i||i.from>s)&&(i={from:s,to:o})}),i}function We(n,t,e){let r=!1;return n.between(t,t,(i,s)=>{i==t&&s==e&&(r=!0)}),r}function ot(n,t){return n.field(T,!1)?t:t.concat(j.appendConfig.of(at()))}const Ht=n=>{for(let t of st(n)){let e=D(n.state,t.from,t.to);if(e)return n.dispatch({effects:ot(n.state,[O.of(e),z(n,e)])}),!0}return!1},Ut=n=>{if(!n.state.field(T,!1))return!1;let t=[];for(let e of st(n)){let r=R(n.state,e.from,e.to);r&&t.push(A.of(r),z(n,r,!1))}return t.length&&n.dispatch({effects:t}),t.length>0};function z(n,t,e=!0){let r=n.state.doc.lineAt(t.from).number,i=n.state.doc.lineAt(t.to).number;return w.announce.of(`${n.state.phrase(e?"Folded lines":"Unfolded lines")} ${r} ${n.state.phrase("to")} ${i}.`)}const jt=n=>{let{state:t}=n,e=[];for(let r=0;r{let t=n.state.field(T,!1);if(!t||!t.size)return!1;let e=[];return t.between(0,n.state.doc.length,(r,i)=>{e.push(A.of({from:r,to:i}))}),n.dispatch({effects:e}),!0};function He(n,t){for(let e=t;;){let r=D(n.state,e.from,e.to);if(r&&r.to>t.from)return r;if(!e.from)return null;e=n.lineBlockAt(e.from-1)}}const Ue=n=>{let t=[];for(let e of st(n)){let r=R(n.state,e.from,e.to);if(r)t.push(A.of(r),z(n,r,!1));else{let i=He(n,e);i&&t.push(O.of(i),z(n,i))}}return t.length>0&&n.dispatch({effects:ot(n.state,t)}),!!t.length},je=[{key:"Ctrl-Shift-[",mac:"Cmd-Alt-[",run:Ht},{key:"Ctrl-Shift-]",mac:"Cmd-Alt-]",run:Ut},{key:"Ctrl-Alt-[",run:jt},{key:"Ctrl-Alt-]",run:Vt}],Ve={placeholderDOM:null,preparePlaceholder:null,placeholderText:"\u2026"},lt=v.define({combine(n){return vt(n,Ve)}});function at(n){let t=[T,Ge];return n&&t.push(lt.of(n)),t}function $t(n,t){let{state:e}=n,r=e.facet(lt),i=o=>{let l=n.lineBlockAt(n.posAtDOM(o.target)),a=R(n.state,l.from,l.to);a&&n.dispatch({effects:A.of(a)}),o.preventDefault()};if(r.placeholderDOM)return r.placeholderDOM(n,i,t);let s=document.createElement("span");return s.textContent=r.placeholderText,s.setAttribute("aria-label",e.phrase("folded code")),s.title=e.phrase("unfold"),s.className="cm-foldPlaceholder",s.onclick=i,s}const qt=p.replace({widget:new class extends Tt{toDOM(n){return $t(n,null)}}});class $e extends Tt{constructor(t){super(),this.value=t}eq(t){return this.value==t.value}toDOM(t){return $t(t,this.value)}}const qe={openText:"\u2304",closedText:"\u203A",markerDOM:null,domEventHandlers:{},foldingChanged:()=>!1};class ht extends pe{constructor(t,e){super(),this.config=t,this.open=e}eq(t){return this.config==t.config&&this.open==t.open}toDOM(t){if(this.config.markerDOM)return this.config.markerDOM(this.open);let e=document.createElement("span");return e.textContent=this.open?this.config.openText:this.config.closedText,e.title=t.state.phrase(this.open?"Fold line":"Unfold line"),e}}function ze(n={}){let t=Object.assign(Object.assign({},qe),n),e=new ht(t,!0),r=new ht(t,!1),i=V.fromClass(class{constructor(o){this.from=o.viewport.from,this.markers=this.buildMarkers(o)}update(o){(o.docChanged||o.viewportChanged||o.startState.facet(S)!=o.state.facet(S)||o.startState.field(T,!1)!=o.state.field(T,!1)||m(o.startState)!=m(o.state)||t.foldingChanged(o))&&(this.markers=this.buildMarkers(o.view))}buildMarkers(o){let l=new X;for(let a of o.viewportLineBlocks){let h=R(o.state,a.from,a.to)?r:D(o.state,a.from,a.to)?e:null;h&&l.add(a.from,a.from,h)}return l.finish()}}),{domEventHandlers:s}=t;return[i,de({class:"cm-foldGutter",markers(o){var l;return((l=o.plugin(i))===null||l===void 0?void 0:l.markers)||xt.empty},initialSpacer(){return new ht(t,!1)},domEventHandlers:Object.assign(Object.assign({},s),{click:(o,l,a)=>{if(s.click&&s.click(o,l,a))return!0;let h=R(o.state,l.from,l.to);if(h)return o.dispatch({effects:A.of(h)}),!0;let f=D(o.state,l.from,l.to);return f?(o.dispatch({effects:O.of(f)}),!0):!1}})}),at()]}const Ge=w.baseTheme({".cm-foldPlaceholder":{backgroundColor:"#eee",border:"1px solid #ddd",color:"#888",borderRadius:".2em",margin:"0 1px",padding:"0 1px",cursor:"pointer"},".cm-foldGutter span":{padding:"0 1px",cursor:"pointer"}});class W{constructor(t,e){this.specs=t;let r;function i(l){let a=Pt.newName();return(r||(r=Object.create(null)))["."+a]=l,a}const s=typeof e.all=="string"?e.all:e.all?i(e.all):void 0,o=e.scope;this.scope=o instanceof g?l=>l.prop(P)==o.data:o?l=>l==o:void 0,this.style=ge(t.map(l=>({tag:l.tag,class:l.class||i(Object.assign({},l,{tag:null}))})),{all:s}).style,this.module=r?new Pt(r):null,this.themeType=e.themeType}static define(t,e){return new W(t,e||{})}}const ft=v.define(),zt=v.define({combine(n){return n.length?[n[0]]:null}});function G(n){let t=n.facet(ft);return t.length?t:n.facet(zt)}function Je(n,t){let e=[Qe],r;return n instanceof W&&(n.module&&e.push(w.styleModule.of(n.module)),r=n.themeType),t?.fallback?e.push(zt.of(n)):r?e.push(ft.computeN([w.darkTheme],i=>i.facet(w.darkTheme)==(r=="dark")?[n]:[])):e.push(ft.of(n)),e}function _e(n,t,e){let r=G(n),i=null;if(r){for(let s of r)if(!s.scope||e&&s.scope(e)){let o=s.style(t);o&&(i=i?i+" "+o:o)}}return i}class Ke{constructor(t){this.markCache=Object.create(null),this.tree=m(t.state),this.decorations=this.buildDeco(t,G(t.state)),this.decoratedTo=t.viewport.to}update(t){let e=m(t.state),r=G(t.state),i=r!=G(t.startState),{viewport:s}=t.view,o=t.changes.mapPos(this.decoratedTo,1);e.length=s.to?(this.decorations=this.decorations.map(t.changes),this.decoratedTo=o):(e!=this.tree||t.viewportChanged||i)&&(this.tree=e,this.decorations=this.buildDeco(t.view,r),this.decoratedTo=s.to)}buildDeco(t,e){if(!e||!this.tree.length)return p.none;let r=new X;for(let{from:i,to:s}of t.visibleRanges)me(this.tree,e,(o,l,a)=>{r.add(o,l,this.markCache[a]||(this.markCache[a]=p.mark({class:a})))},i,s);return r.finish()}}const Qe=St.high(V.fromClass(Ke,{decorations:n=>n.decorations})),Xe=W.define([{tag:u.meta,color:"#404740"},{tag:u.link,textDecoration:"underline"},{tag:u.heading,textDecoration:"underline",fontWeight:"bold"},{tag:u.emphasis,fontStyle:"italic"},{tag:u.strong,fontWeight:"bold"},{tag:u.strikethrough,textDecoration:"line-through"},{tag:u.keyword,color:"#708"},{tag:[u.atom,u.bool,u.url,u.contentSeparator,u.labelName],color:"#219"},{tag:[u.literal,u.inserted],color:"#164"},{tag:[u.string,u.deleted],color:"#a11"},{tag:[u.regexp,u.escape,u.special(u.string)],color:"#e40"},{tag:u.definition(u.variableName),color:"#00f"},{tag:u.local(u.variableName),color:"#30a"},{tag:[u.typeName,u.namespace],color:"#085"},{tag:u.className,color:"#167"},{tag:[u.special(u.variableName),u.macroName],color:"#256"},{tag:u.definition(u.propertyName),color:"#00c"},{tag:u.comment,color:"#940"},{tag:u.invalid,color:"#f00"}]),Ye=w.baseTheme({"&.cm-focused .cm-matchingBracket":{backgroundColor:"#328c8252"},"&.cm-focused .cm-nonmatchingBracket":{backgroundColor:"#bb555544"}}),Gt=1e4,Jt="()[]{}",_t=v.define({combine(n){return vt(n,{afterCursor:!0,brackets:Jt,maxScanDistance:Gt,renderMatch:en})}}),Ze=p.mark({class:"cm-matchingBracket"}),tn=p.mark({class:"cm-nonmatchingBracket"});function en(n){let t=[],e=n.matched?Ze:tn;return t.push(e.range(n.start.from,n.start.to)),n.end&&t.push(e.range(n.end.from,n.end.to)),t}const nn=Q.define({create(){return p.none},update(n,t){if(!t.docChanged&&!t.selection)return n;let e=[],r=t.state.facet(_t);for(let i of t.state.selection.ranges){if(!i.empty)continue;let s=F(t.state,i.head,-1,r)||i.head>0&&F(t.state,i.head-1,1,r)||r.afterCursor&&(F(t.state,i.head,1,r)||i.headw.decorations.from(n)}),rn=[nn,Ye];function sn(n={}){return[_t.of(n),rn]}const Kt=new y;function ut(n,t,e){let r=n.prop(t<0?y.openedBy:y.closedBy);if(r)return r;if(n.name.length==1){let i=e.indexOf(n.name);if(i>-1&&i%2==(t<0?1:0))return[e[i+t]]}return null}function ct(n){let t=n.type.prop(Kt);return t?t(n.node):n}function F(n,t,e,r={}){let i=r.maxScanDistance||Gt,s=r.brackets||Jt,o=m(n),l=o.resolveInner(t,e);for(let a=l;a;a=a.parent){let h=ut(a.type,e,s);if(h&&a.from0?t>=f.from&&tf.from&&t<=f.to))return on(n,t,e,a,f,h,s)}}return ln(n,t,e,o,l.type,i,s)}function on(n,t,e,r,i,s,o){let l=r.parent,a={from:i.from,to:i.to},h=0,f=l?.cursor();if(f&&(e<0?f.childBefore(r.from):f.childAfter(r.to)))do if(e<0?f.to<=r.from:f.from>=r.to){if(h==0&&s.indexOf(f.type.name)>-1&&f.from0)return null;let h={from:e<0?t-1:t,to:e>0?t+1:t},f=n.doc.iterRange(t,e>0?n.doc.length:0),c=0;for(let k=0;!f.next().done&&k<=s;){let d=f.value;e<0&&(k+=d.length);let x=t+k*e;for(let I=e>0?0:d.length-1,ae=e>0?d.length:-1;I!=ae;I+=e){let K=o.indexOf(d[I]);if(!(K<0||r.resolveInner(x+I,1).type!=i))if(K%2==0==e>0)c++;else{if(c==1)return{start:h,end:{from:x+I,to:x+I+1},matched:K>>1==a>>1};c--}}e>0&&(k+=d.length)}return f.done?{start:h,matched:!1}:null}function Qt(n,t,e,r=0,i=0){t==null&&(t=n.search(/[^\s\u00a0]/),t==-1&&(t=n.length));let s=i;for(let o=r;o=this.string.length}sol(){return this.pos==0}peek(){return this.string.charAt(this.pos)||void 0}next(){if(this.pose}eatSpace(){let t=this.pos;for(;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>t}skipToEnd(){this.pos=this.string.length}skipTo(t){let e=this.string.indexOf(t,this.pos);if(e>-1)return this.pos=e,!0}backUp(t){this.pos-=t}column(){return this.lastColumnPosr?o.toLowerCase():o,s=this.string.substr(this.pos,t.length);return i(s)==i(t)?(e!==!1&&(this.pos+=t.length),!0):null}else{let i=this.string.slice(this.pos).match(t);return i&&i.index>0?null:(i&&e!==!1&&(this.pos+=i[0].length),i)}}current(){return this.string.slice(this.start,this.pos)}}function an(n){return{name:n.name||"",token:n.token,blankLine:n.blankLine||(()=>{}),startState:n.startState||(()=>!0),copyState:n.copyState||hn,indent:n.indent||(()=>null),languageData:n.languageData||{},tokenTable:n.tokenTable||gt}}function hn(n){if(typeof n!="object")return n;let t={};for(let e in n){let r=n[e];t[e]=r instanceof Array?r.slice():r}return t}const Xt=new WeakMap;class wt extends g{constructor(t){let e=Z(t.languageData),r=an(t),i,s=new class extends yt{createParse(o,l,a){return new un(i,o,l,a)}};super(e,s,[et.of((o,l)=>this.getIndent(o,l))],t.name),this.topNode=pn(e),i=this,this.streamParser=r,this.stateAfter=new y({perNode:!0}),this.tokenTable=t.tokenTable?new re(r.tokenTable):dn}static define(t){return new wt(t)}getIndent(t,e){let r=m(t.state),i=r.resolve(e);for(;i&&i.type!=this.topNode;)i=i.parent;if(!i)return null;let s,{overrideIndentation:o}=t.options;o&&(s=Xt.get(t.state),s!=null&&s1e4)return null;for(;a=r&&e+t.length<=i&&t.prop(n.stateAfter);if(s)return{state:n.streamParser.copyState(s),pos:e+t.length};for(let o=t.children.length-1;o>=0;o--){let l=t.children[o],a=e+t.positions[o],h=l instanceof b&&a=t.length)return t;!i&&t.type==n.topNode&&(i=!0);for(let s=t.children.length-1;s>=0;s--){let o=t.positions[s],l=t.children[s],a;if(oe&&pt(n,i.tree,0-i.offset,e,o),a;if(l&&(a=Yt(n,i.tree,e+i.offset,l.pos+i.offset,!1)))return{state:l.state,tree:a}}return{state:n.streamParser.startState(r?B(r):4),tree:b.empty}}class un{constructor(t,e,r,i){this.lang=t,this.input=e,this.fragments=r,this.ranges=i,this.stoppedAt=null,this.chunks=[],this.chunkPos=[],this.chunk=[],this.chunkReused=void 0,this.rangeIndex=0,this.to=i[i.length-1].to;let s=C.get(),o=i[0].from,{state:l,tree:a}=fn(t,r,o,s?.state);this.state=l,this.parsedPos=this.chunkStart=o+a.length;for(let h=0;h=e?this.finish():t&&this.parsedPos>=t.viewport.to?(t.skipUntilInView(this.parsedPos,e),this.finish()):null}stopAt(t){this.stoppedAt=t}lineAfter(t){let e=this.input.chunk(t);if(this.input.lineChunks)e==` +`&&(e="");else{let r=e.indexOf(` +`);r>-1&&(e=e.slice(0,r))}return t+e.length<=this.to?e:e.slice(0,this.to-t)}nextLine(){let t=this.parsedPos,e=this.lineAfter(t),r=t+e.length;for(let i=this.rangeIndex;;){let s=this.ranges[i].to;if(s>=r||(e=e.slice(0,s-(r-e.length)),i++,i==this.ranges.length))break;let o=this.ranges[i].from,l=this.lineAfter(o);e+=l,r=o+l.length}return{line:e,end:r}}skipGapsTo(t,e,r){for(;;){let i=this.ranges[this.rangeIndex].to,s=t+e;if(r>0?i>s:i>=s)break;let o=this.ranges[++this.rangeIndex].from;e+=o-i}return e}moveRangeIndex(){for(;this.ranges[this.rangeIndex].to1){s=this.skipGapsTo(e,s,1),e+=s;let o=this.chunk.length;s=this.skipGapsTo(r,s,-1),r+=s,i+=this.chunk.length-o}return this.chunk.push(t,e,r,i),s}parseLine(t){let{line:e,end:r}=this.nextLine(),i=0,{streamParser:s}=this.lang,o=new dt(e,t?t.state.tabSize:4,t?B(t.state):2);if(o.eol())s.blankLine(this.state,o.indentUnit);else for(;!o.eol();){let l=Zt(s.token,o,this.state);if(l&&(i=this.emitToken(this.lang.tokenTable.resolve(l),this.parsedPos+o.start,this.parsedPos+o.pos,4,i)),o.start>1e4)break}this.parsedPos=r,this.moveRangeIndex(),this.parsedPost.start)return i}throw new Error("Stream parser failed to advance stream.")}const gt=Object.create(null),E=[U.none],cn=new fe(E),te=[],ee=Object.create(null),ne=Object.create(null);for(let[n,t]of[["variable","variableName"],["variable-2","variableName.special"],["string-2","string.special"],["def","variableName.definition"],["tag","tagName"],["attribute","attributeName"],["type","typeName"],["builtin","variableName.standard"],["qualifier","modifier"],["error","invalid"],["header","heading"],["property","propertyName"]])ne[n]=ie(gt,t);class re{constructor(t){this.extra=t,this.table=Object.assign(Object.create(null),ne)}resolve(t){return t?this.table[t]||(this.table[t]=ie(this.extra,t)):0}}const dn=new re(gt);function mt(n,t){te.indexOf(n)>-1||(te.push(n),console.warn(t))}function ie(n,t){let e=[];for(let l of t.split(" ")){let a=[];for(let h of l.split(".")){let f=n[h]||u[h];f?typeof f=="function"?a.length?a=a.map(f):mt(h,`Modifier ${h} used at start of tag`):a.length?mt(h,`Tag ${h} used as modifier`):a=Array.isArray(f)?f:[f]:mt(h,`Unknown highlighting tag ${h}`)}for(let h of a)e.push(h)}if(!e.length)return 0;let r=t.replace(/ /g,"_"),i=r+" "+e.map(l=>l.id),s=ee[i];if(s)return s.id;let o=ee[i]=U.define({id:E.length,name:r,props:[ke({[r]:e})]});return E.push(o),o.id}function pn(n){let t=U.define({id:E.length,name:"Document",props:[P.add(()=>n)],top:!0});return E.push(t),t}function se(n){return n.length<=4096&&/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac\ufb50-\ufdff]/.test(n)}function oe(n){for(let t=n.iter();!t.next().done;)if(se(t.value))return!0;return!1}function gn(n){let t=!1;return n.iterChanges((e,r,i,s,o)=>{!t&&oe(o)&&(t=!0)}),t}const kt=v.define({combine:n=>n.some(t=>t)});function mn(n={}){let t=[kn];return n.alwaysIsolate&&t.push(kt.of(!0)),t}const kn=V.fromClass(class{constructor(n){this.always=n.state.facet(kt)||n.textDirection!=$.LTR||n.state.facet(w.perLineTextDirection),this.hasRTL=!this.always&&oe(n.state.doc),this.tree=m(n.state),this.decorations=this.always||this.hasRTL?le(n,this.tree,this.always):p.none}update(n){let t=n.state.facet(kt)||n.view.textDirection!=$.LTR||n.state.facet(w.perLineTextDirection);if(!t&&!this.hasRTL&&gn(n.changes)&&(this.hasRTL=!0),!t&&!this.hasRTL)return;let e=m(n.state);(t!=this.always||e!=this.tree||n.docChanged||n.viewportChanged)&&(this.tree=e,this.always=t,this.decorations=le(n.view,e,t))}},{provide:n=>{function t(e){var r,i;return(i=(r=e.plugin(n))===null||r===void 0?void 0:r.decorations)!==null&&i!==void 0?i:p.none}return[w.outerDecorations.of(t),St.lowest(w.bidiIsolatedRanges.of(t))]}});function le(n,t,e){let r=new X,i=n.visibleRanges;e||(i=bn(i,n.state.doc));for(let{from:s,to:o}of i)t.iterate({enter:l=>{let a=l.type.prop(y.isolate);a&&r.add(l.from,l.to,wn[a])},from:s,to:o});return r.finish()}function bn(n,t){let e=t.iter(),r=0,i=[],s=null;for(let{from:o,to:l}of n)if(!(s&&s.to>o&&(o=s.to,o>=l)))for(r+e.value.lengtha-10?s.to=Math.min(l,h):i.push(s={from:a,to:Math.min(l,h)})),h>=l)break;r=h,e.next()}return i}const wn={rtl:p.mark({class:"cm-iso",inclusive:!0,attributes:{dir:"rtl"},bidiIsolate:$.RTL}),ltr:p.mark({class:"cm-iso",inclusive:!0,attributes:{dir:"ltr"},bidiIsolate:$.LTR}),auto:p.mark({class:"cm-iso",inclusive:!0,attributes:{dir:"auto"},bidiIsolate:null})};export{Dt as DocInput,W as HighlightStyle,q as IndentContext,J as LRLanguage,g as Language,bt as LanguageDescription,ve as LanguageSupport,C as ParseContext,wt as StreamLanguage,dt as StringStream,_ as TreeIndentContext,mn as bidiIsolates,sn as bracketMatching,Kt as bracketMatchingHandle,at as codeFolding,Me as continuedIndent,Xe as defaultHighlightStyle,Z as defineLanguageFacet,De as delimitedIndent,It as ensureSyntaxTree,Oe as flatIndent,jt as foldAll,Ht as foldCode,O as foldEffect,ze as foldGutter,Be as foldInside,je as foldKeymap,Et as foldNodeProp,Ft as foldService,T as foldState,D as foldable,Ee as foldedRanges,we as forceParsing,B as getIndentUnit,it as getIndentation,_e as highlightingFor,Nt as indentNodeProp,Ne as indentOnInput,xe as indentRange,et as indentService,rt as indentString,nt as indentUnit,S as language,P as languageDataProp,F as matchBrackets,At as sublanguageProp,Je as syntaxHighlighting,ye as syntaxParserRunning,m as syntaxTree,be as syntaxTreeAvailable,Ue as toggleFold,Vt as unfoldAll,Ut as unfoldCode,A as unfoldEffect}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/lint.js b/Resources/Public/JavaScript/Contrib/@codemirror/lint.js new file mode 100644 index 0000000..5c111ef --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/lint.js @@ -0,0 +1 @@ +import{Decoration as h,showPanel as K,EditorView as y,ViewPlugin as U,logException as X,gutter as J,showTooltip as Q,hoverTooltip as ee,getPanel as te,WidgetType as ie,GutterMarker as ne}from"@codemirror/view";import{StateEffect as k,StateField as S,Facet as M,combineConfig as A,RangeSet as R}from"@codemirror/state";import u from"crelt";class se{constructor(e,n,i){this.from=e,this.to=n,this.diagnostic=i}}class g{constructor(e,n,i){this.diagnostics=e,this.panel=n,this.selected=i}static init(e,n,i){let s=e,o=i.facet(m).markerFilter;o&&(s=o(s,i));let l=h.set(s.map(r=>r.from==r.to||r.from==r.to-1&&i.doc.lineAt(r.from).to==r.from?h.widget({widget:new ue(r),diagnostic:r}).range(r.from):h.mark({attributes:{class:"cm-lintRange cm-lintRange-"+r.severity+(r.markClass?" "+r.markClass:"")},diagnostic:r}).range(r.from,r.to)),!0);return new g(l,n,b(l))}}function b(t,e=null,n=0){let i=null;return t.between(n,1e9,(s,o,{spec:l})=>{if(!(e&&l.diagnostic!=e))return i=new se(s,o,l.diagnostic),!1}),i}function F(t,e){let n=e.pos,i=e.end||n,s=t.state.facet(m).hideOn(t,n,i);if(s!=null)return s;let o=t.startState.doc.lineAt(e.pos);return!!(t.effects.some(l=>l.is(w))||t.changes.touchesRange(o.from,Math.max(o.to,i)))}function E(t,e){return t.field(f,!1)?e:e.concat(k.appendConfig.of(W))}function B(t,e){return{effects:E(t,[w.of(e)])}}const w=k.define(),D=k.define(),O=k.define(),f=S.define({create(){return new g(h.none,null,null)},update(t,e){if(e.docChanged&&t.diagnostics.size){let n=t.diagnostics.map(e.changes),i=null,s=t.panel;if(t.selected){let o=e.changes.mapPos(t.selected.from,1);i=b(n,t.selected.diagnostic,o)||b(n,null,o)}!n.size&&s&&e.state.facet(m).autoPanel&&(s=null),t=new g(n,s,i)}for(let n of e.effects)if(n.is(w)){let i=e.state.facet(m).autoPanel?n.value.length?x.open:null:t.panel;t=g.init(n.value,i,e.state)}else n.is(D)?t=new g(t.diagnostics,n.value?x.open:null,t.selected):n.is(O)&&(t=new g(t.diagnostics,t.panel,n.value));return t},provide:t=>[K.from(t,e=>e.panel),y.decorations.from(t,e=>e.diagnostics)]});function oe(t){let e=t.field(f,!1);return e?e.diagnostics.size:0}const le=h.mark({class:"cm-lintRange cm-lintRange-active"});function re(t,e,n){let{diagnostics:i}=t.state.field(f),s=[],o=2e8,l=0;i.between(e-(n<0?1:0),e+(n>0?1:0),(a,c,{spec:d})=>{e>=a&&e<=c&&(a==c||(e>a||n>0)&&(eV(t,n,!1)))}const j=t=>{let e=t.state.field(f,!1);(!e||!e.panel)&&t.dispatch({effects:E(t.state,[D.of(!0)])});let n=te(t,x.open);return n&&n.dom.querySelector(".cm-panel-lint ul").focus(),!0},L=t=>{let e=t.state.field(f,!1);return!e||!e.panel?!1:(t.dispatch({effects:D.of(!1)}),!0)},z=t=>{let e=t.state.field(f,!1);if(!e)return!1;let n=t.state.selection.main,i=e.diagnostics.iter(n.to+1);return!i.value&&(i=e.diagnostics.iter(0),!i.value||i.from==n.from&&i.to==n.to)?!1:(t.dispatch({selection:{anchor:i.from,head:i.to},scrollIntoView:!0}),!0)},ae=t=>{let{state:e}=t,n=e.field(f,!1);if(!n)return!1;let i=e.selection.main,s,o,l,r;return n.diagnostics.between(0,e.doc.length,(a,c)=>{cl)&&(l=a,r=c)}),l==null||s==null&&l==i.from?!1:(t.dispatch({selection:{anchor:s??l,head:o??r},scrollIntoView:!0}),!0)},ce=[{key:"Mod-Shift-m",run:j,preventDefault:!0},{key:"F8",run:z}],H=U.fromClass(class{constructor(t){this.view=t,this.timeout=-1,this.set=!0;let{delay:e}=t.state.facet(m);this.lintTime=Date.now()+e,this.run=this.run.bind(this),this.timeout=setTimeout(this.run,e)}run(){clearTimeout(this.timeout);let t=Date.now();if(tPromise.resolve(i(this.view)))).then(i=>{let s=i.reduce((o,l)=>o.concat(l));this.view.state.doc==e.doc&&this.view.dispatch(B(this.view.state,s))},i=>{X(this.view.state,i)})}}update(t){let e=t.state.facet(m);(t.docChanged||e!=t.startState.facet(m)||e.needsRefresh&&e.needsRefresh(t))&&(this.lintTime=Date.now()+e.delay,this.set||(this.set=!0,this.timeout=setTimeout(this.run,e.delay)))}force(){this.set&&(this.lintTime=Date.now(),this.run())}destroy(){clearTimeout(this.timeout)}}),m=M.define({combine(t){return Object.assign({sources:t.map(e=>e.source).filter(e=>e!=null)},A(t.map(e=>e.config),{delay:750,markerFilter:null,tooltipFilter:null,needsRefresh:null,hideOn:()=>null},{needsRefresh:(e,n)=>e?n?i=>e(i)||n(i):e:n}))}});function de(t,e={}){return[m.of({source:t,config:e}),H,W]}function fe(t){let e=t.plugin(H);e&&e.force()}function $(t){let e=[];if(t)e:for(let{name:n}of t){for(let i=0;io.toLowerCase()==s.toLowerCase())){e.push(s);continue e}}e.push("")}return e}function V(t,e,n){var i;let s=n?$(e.actions):[];return u("li",{class:"cm-diagnostic cm-diagnostic-"+e.severity},u("span",{class:"cm-diagnosticText"},e.renderMessage?e.renderMessage(t):e.message),(i=e.actions)===null||i===void 0?void 0:i.map((o,l)=>{let r=!1,a=Z=>{if(Z.preventDefault(),r)return;r=!0;let P=b(t.state.field(f).diagnostics,e);P&&o.apply(t,P.from,P.to)},{name:c}=o,d=s[l]?c.indexOf(s[l]):-1,p=d<0?c:[c.slice(0,d),u("u",c.slice(d,d+1)),c.slice(d+1)];return u("button",{type:"button",class:"cm-diagnosticAction",onclick:a,onmousedown:a,"aria-label":` Action: ${c}${d<0?"":` (access key "${s[l]})"`}.`},p)}),e.source&&u("div",{class:"cm-diagnosticSource"},e.source))}class ue extends ie{constructor(e){super(),this.diagnostic=e}eq(e){return e.diagnostic==this.diagnostic}toDOM(){return u("span",{class:"cm-lintPoint cm-lintPoint-"+this.diagnostic.severity})}}class N{constructor(e,n){this.diagnostic=n,this.id="item_"+Math.floor(Math.random()*4294967295).toString(16),this.dom=V(e,n,!0),this.dom.id=this.id,this.dom.setAttribute("role","option")}}class x{constructor(e){this.view=e,this.items=[];let n=s=>{if(s.keyCode==27)L(this.view),this.view.focus();else if(s.keyCode==38||s.keyCode==33)this.moveSelection((this.selectedIndex-1+this.items.length)%this.items.length);else if(s.keyCode==40||s.keyCode==34)this.moveSelection((this.selectedIndex+1)%this.items.length);else if(s.keyCode==36)this.moveSelection(0);else if(s.keyCode==35)this.moveSelection(this.items.length-1);else if(s.keyCode==13)this.view.focus();else if(s.keyCode>=65&&s.keyCode<=90&&this.selectedIndex>=0){let{diagnostic:o}=this.items[this.selectedIndex],l=$(o.actions);for(let r=0;r{for(let o=0;oL(this.view)},"\xD7")),this.update()}get selectedIndex(){let e=this.view.state.field(f).selected;if(!e)return-1;for(let n=0;n{let c=-1,d;for(let p=i;pi&&(this.items.splice(i,c-i),s=!0)),n&&d.diagnostic==n.diagnostic?d.dom.hasAttribute("aria-selected")||(d.dom.setAttribute("aria-selected","true"),o=d):d.dom.hasAttribute("aria-selected")&&d.dom.removeAttribute("aria-selected"),i++});i({sel:o.dom.getBoundingClientRect(),panel:this.list.getBoundingClientRect()}),write:({sel:l,panel:r})=>{let a=r.height/this.list.offsetHeight;l.topr.bottom&&(this.list.scrollTop+=(l.bottom-r.bottom)/a)}})):this.selectedIndex<0&&this.list.removeAttribute("aria-activedescendant"),s&&this.sync()}sync(){let e=this.list.firstChild;function n(){let i=e;e=i.nextSibling,i.remove()}for(let i of this.items)if(i.dom.parentNode==this.list){for(;e!=i.dom;)n();e=i.dom.nextSibling}else this.list.insertBefore(i.dom,e);for(;e;)n()}moveSelection(e){if(this.selectedIndex<0)return;let n=this.view.state.field(f),i=b(n.diagnostics,this.items[e].diagnostic);i&&this.view.dispatch({selection:{anchor:i.from,head:i.to},scrollIntoView:!0,effects:O.of(i)})}static open(e){return new x(e)}}function v(t,e='viewBox="0 0 40 40"'){return`url('data:image/svg+xml,${encodeURIComponent(t)}')`}function C(t){return v(``,'width="6" height="3"')}const me=y.baseTheme({".cm-diagnostic":{padding:"3px 6px 3px 8px",marginLeft:"-1px",display:"block",whiteSpace:"pre-wrap"},".cm-diagnostic-error":{borderLeft:"5px solid #d11"},".cm-diagnostic-warning":{borderLeft:"5px solid orange"},".cm-diagnostic-info":{borderLeft:"5px solid #999"},".cm-diagnostic-hint":{borderLeft:"5px solid #66d"},".cm-diagnosticAction":{font:"inherit",border:"none",padding:"2px 4px",backgroundColor:"#444",color:"white",borderRadius:"3px",marginLeft:"8px",cursor:"pointer"},".cm-diagnosticSource":{fontSize:"70%",opacity:.7},".cm-lintRange":{backgroundPosition:"left bottom",backgroundRepeat:"repeat-x",paddingBottom:"0.7px"},".cm-lintRange-error":{backgroundImage:C("#d11")},".cm-lintRange-warning":{backgroundImage:C("orange")},".cm-lintRange-info":{backgroundImage:C("#999")},".cm-lintRange-hint":{backgroundImage:C("#66d")},".cm-lintRange-active":{backgroundColor:"#ffdd9980"},".cm-tooltip-lint":{padding:0,margin:0},".cm-lintPoint":{position:"relative","&:after":{content:'""',position:"absolute",bottom:0,left:"-2px",borderLeft:"3px solid transparent",borderRight:"3px solid transparent",borderBottom:"4px solid #d11"}},".cm-lintPoint-warning":{"&:after":{borderBottomColor:"orange"}},".cm-lintPoint-info":{"&:after":{borderBottomColor:"#999"}},".cm-lintPoint-hint":{"&:after":{borderBottomColor:"#66d"}},".cm-panel.cm-panel-lint":{position:"relative","& ul":{maxHeight:"100px",overflowY:"auto","& [aria-selected]":{backgroundColor:"#ddd","& u":{textDecoration:"underline"}},"&:focus [aria-selected]":{background_fallback:"#bdf",backgroundColor:"Highlight",color_fallback:"white",color:"HighlightText"},"& u":{textDecoration:"none"},padding:0,margin:0},"& [name=close]":{position:"absolute",top:"0",right:"2px",background:"inherit",border:"none",font:"inherit",padding:0,margin:0}}});function Y(t){return t=="error"?4:t=="warning"?3:t=="info"?2:1}class he extends ne{constructor(e){super(),this.diagnostics=e,this.severity=e.reduce((n,i)=>Y(n)pe(e,n,i)),n}}function ge(t,e){let n=i=>{let s=e.getBoundingClientRect();if(!(i.clientX>s.left-10&&i.clientXs.top-10&&i.clientYe.getBoundingClientRect()}}})}),e.onmouseout=e.onmousemove=null,ge(t,e)}let{hoverTime:s}=t.state.facet(T),o=setTimeout(i,s);e.onmouseout=()=>{clearTimeout(o),e.onmouseout=e.onmousemove=null},e.onmousemove=()=>{clearTimeout(o),o=setTimeout(i,s)}}function be(t,e){let n=Object.create(null);for(let s of e){let o=t.lineAt(s.from);(n[o.from]||(n[o.from]=[])).push(s)}let i=[];for(let s in n)i.push(new he(n[s]).range(+s));return R.of(i,!0)}const ke=J({class:"cm-gutter-lint",markers:t=>t.state.field(_)}),_=S.define({create(){return R.empty},update(t,e){t=t.map(e.changes);let n=e.state.facet(T).markerFilter;for(let i of e.effects)if(i.is(w)){let s=i.value;n&&(s=n(s||[],e.state)),t=be(e.state.doc,s.slice(0))}return t}}),I=k.define(),q=S.define({create(){return null},update(t,e){return t&&e.docChanged&&(t=F(e,t)?null:Object.assign(Object.assign({},t),{pos:e.changes.mapPos(t.pos)})),e.effects.reduce((n,i)=>i.is(I)?i.value:n,t)},provide:t=>Q.from(t)}),we=y.baseTheme({".cm-gutter-lint":{width:"1.4em","& .cm-gutterElement":{padding:".2em"}},".cm-lint-marker":{width:"1em",height:"1em"},".cm-lint-marker-info":{content:v('')},".cm-lint-marker-warning":{content:v('')},".cm-lint-marker-error":{content:v('')}}),W=[f,y.decorations.compute([f],t=>{let{selected:e,panel:n}=t.field(f);return!e||!n||e.from==e.to?h.none:h.set([le.range(e.from,e.to)])}),ee(re,{hideOn:F}),me],T=M.define({combine(t){return A(t,{hoverTime:300,markerFilter:null,tooltipFilter:null})}});function xe(t={}){return[T.of(t),_,ke,we,q]}function ye(t,e){let n=t.field(f,!1);if(n&&n.diagnostics.size)for(let i=R.iter([n.diagnostics]);i.value;i.next())e(i.value.spec.diagnostic,i.from,i.to)}export{L as closeLintPanel,oe as diagnosticCount,ye as forEachDiagnostic,fe as forceLinting,xe as lintGutter,ce as lintKeymap,de as linter,z as nextDiagnostic,j as openLintPanel,ae as previousDiagnostic,B as setDiagnostics,w as setDiagnosticsEffect}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/search.js b/Resources/Public/JavaScript/Contrib/@codemirror/search.js new file mode 100644 index 0000000..8e67175 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/search.js @@ -0,0 +1,2 @@ +import{showPanel as te,EditorView as S,getPanel as R,Decoration as f,ViewPlugin as re,runScopeHandlers as ve}from"@codemirror/view";import{codePointAt as be,fromCodePoint as Ce,codePointSize as ke,StateEffect as W,StateField as ne,EditorSelection as p,Facet as ie,combineConfig as se,CharCategory as m,RangeSetBuilder as Le,Prec as We,EditorState as Fe,findClusterBreak as le}from"@codemirror/state";import h from"crelt";const oe=typeof String.prototype.normalize=="function"?t=>t.normalize("NFKD"):t=>t;class y{constructor(e,r,n=0,i=e.length,s,o){this.test=o,this.value={from:0,to:0},this.done=!1,this.matches=[],this.buffer="",this.bufferPos=0,this.iter=e.iterRange(n,i),this.bufferStart=n,this.normalize=s?l=>s(oe(l)):oe,this.query=this.normalize(r)}peek(){if(this.bufferPos==this.buffer.length){if(this.bufferStart+=this.buffer.length,this.iter.next(),this.iter.done)return-1;this.bufferPos=0,this.buffer=this.iter.value}return be(this.buffer,this.bufferPos)}next(){for(;this.matches.length;)this.matches.pop();return this.nextOverlapping()}nextOverlapping(){for(;;){let e=this.peek();if(e<0)return this.done=!0,this;let r=Ce(e),n=this.bufferStart+this.bufferPos;this.bufferPos+=ke(e);let i=this.normalize(r);for(let s=0,o=n;;s++){let l=i.charCodeAt(s),c=this.match(l,o,this.bufferPos+this.bufferStart);if(s==i.length-1){if(c)return this.value=c,this;break}o==n&&sthis.to&&(this.curLine=this.curLine.slice(0,this.to-this.curLineStart)),this.iter.next())}nextLine(){this.curLineStart=this.curLineStart+this.curLine.length+1,this.curLineStart>this.to?this.curLine="":this.getLine(0)}next(){for(let e=this.matchPos-this.curLineStart;;){this.re.lastIndex=e;let r=this.matchPos<=this.to&&this.re.exec(this.curLine);if(r){let n=this.curLineStart+r.index,i=n+r[0].length;if(this.matchPos=q(this.text,i+(n==i?1:0)),n==this.curLineStart+this.curLine.length&&this.nextLine(),(nthis.value.to)&&(!this.test||this.test(n,i,r)))return this.value={from:n,to:i,match:r},this;e=this.matchPos-this.curLineStart}else if(this.curLineStart+this.curLine.length=n||i.to<=r){let l=new k(r,e.sliceString(r,n));return B.set(e,l),l}if(i.from==r&&i.to==n)return i;let{text:s,from:o}=i;return o>r&&(s=e.sliceString(r,o)+s,o=r),i.to=this.to?this.to:this.text.lineAt(e).to}next(){for(;;){let e=this.re.lastIndex=this.matchPos-this.flat.from,r=this.re.exec(this.flat.text);if(r&&!r[0]&&r.index==e&&(this.re.lastIndex=e+1,r=this.re.exec(this.flat.text)),r){let n=this.flat.from+r.index,i=n+r[0].length;if((this.flat.to>=this.to||r.index+r[0].length<=this.flat.text.length-10)&&(!this.test||this.test(n,i,r)))return this.value={from:n,to:i,match:r},this.matchPos=q(this.text,i+(n==i?1:0)),this}if(this.flat.to==this.to)return this.done=!0,this;this.flat=k.get(this.text,this.flat.from,this.chunkEnd(this.flat.from+this.flat.text.length*2))}}}typeof Symbol<"u"&&(z.prototype[Symbol.iterator]=ae.prototype[Symbol.iterator]=function(){return this});function Ae(t){try{return new RegExp(t,_),!0}catch{return!1}}function q(t,e){if(e>=t.length)return e;let r=t.lineAt(e),n;for(;e=56320&&n<57344;)e++;return e}function N(t){let e=String(t.state.doc.lineAt(t.state.selection.main.head).number),r=h("input",{class:"cm-textfield",name:"line",value:e}),n=h("form",{class:"cm-gotoLine",onkeydown:s=>{s.keyCode==27?(s.preventDefault(),t.dispatch({effects:P.of(!1)}),t.focus()):s.keyCode==13&&(s.preventDefault(),i())},onsubmit:s=>{s.preventDefault(),i()}},h("label",t.state.phrase("Go to line"),": ",r)," ",h("button",{class:"cm-button",type:"submit"},t.state.phrase("go")));function i(){let s=/^([+-])?(\d+)?(:\d+)?(%)?$/.exec(r.value);if(!s)return;let{state:o}=t,l=o.doc.lineAt(o.selection.main.head),[,c,a,u,x]=s,E=u?+u.slice(1):0,L=a?+a:l.number;if(a&&x){let Q=L/100;c&&(Q=Q*(c=="-"?-1:1)+l.number/o.doc.lines),L=Math.round(o.doc.lines*Q)}else a&&c&&(L=L*(c=="-"?-1:1)+l.number);let Z=o.doc.line(Math.max(1,Math.min(o.doc.lines,L))),ee=p.cursor(Z.from+Math.max(0,Math.min(E,Z.length)));t.dispatch({effects:[P.of(!1),S.scrollIntoView(ee.from,{y:"center"})],selection:ee}),t.focus()}return{dom:n}}const P=W.define(),he=ne.define({create(){return!0},update(t,e){for(let r of e.effects)r.is(P)&&(t=r.value);return t},provide:t=>te.from(t,e=>e?N:null)}),ue=t=>{let e=R(t,N);if(!e){let r=[P.of(!0)];t.state.field(he,!1)==null&&r.push(W.appendConfig.of([he,De])),t.dispatch({effects:r}),e=R(t,N)}return e&&e.dom.querySelector("input").select(),!0},De=S.baseTheme({".cm-panel.cm-gotoLine":{padding:"2px 6px 4px","& label":{fontSize:"80%"}}}),Ee={highlightWordAroundCursor:!1,minSelectionLength:1,maxMatches:100,wholeWords:!1},fe=ie.define({combine(t){return se(t,Ee,{highlightWordAroundCursor:(e,r)=>e||r,minSelectionLength:Math.min,maxMatches:Math.min})}});function Re(t){let e=[we,Ie];return t&&e.push(fe.of(t)),e}const qe=f.mark({class:"cm-selectionMatch"}),Pe=f.mark({class:"cm-selectionMatch cm-selectionMatch-main"});function de(t,e,r,n){return(r==0||t(e.sliceDoc(r-1,r))!=m.Word)&&(n==e.doc.length||t(e.sliceDoc(n,n+1))!=m.Word)}function Te(t,e,r,n){return t(e.sliceDoc(r,r+1))==m.Word&&t(e.sliceDoc(n-1,n))==m.Word}const Ie=re.fromClass(class{constructor(t){this.decorations=this.getDeco(t)}update(t){(t.selectionSet||t.docChanged||t.viewportChanged)&&(this.decorations=this.getDeco(t.view))}getDeco(t){let e=t.state.facet(fe),{state:r}=t,n=r.selection;if(n.ranges.length>1)return f.none;let i=n.main,s,o=null;if(i.empty){if(!e.highlightWordAroundCursor)return f.none;let c=r.wordAt(i.head);if(!c)return f.none;o=r.charCategorizer(i.head),s=r.sliceDoc(c.from,c.to)}else{let c=i.to-i.from;if(c200)return f.none;if(e.wholeWords){if(s=r.sliceDoc(i.from,i.to),o=r.charCategorizer(i.head),!(de(o,r,i.from,i.to)&&Te(o,r,i.from,i.to)))return f.none}else if(s=r.sliceDoc(i.from,i.to),!s)return f.none}let l=[];for(let c of t.visibleRanges){let a=new y(r.doc,s,c.from,c.to);for(;!a.next().done;){let{from:u,to:x}=a.value;if((!o||de(o,r,u,x))&&(i.empty&&u<=i.from&&x>=i.to?l.push(Pe.range(u,x)):(u>=i.to||x<=i.from)&&l.push(qe.range(u,x)),l.length>e.maxMatches))return f.none}}return f.set(l)}},{decorations:t=>t.decorations}),we=S.baseTheme({".cm-selectionMatch":{backgroundColor:"#99ff7780"},".cm-searchMatch .cm-selectionMatch":{backgroundColor:"transparent"}}),Oe=({state:t,dispatch:e})=>{let{selection:r}=t,n=p.create(r.ranges.map(i=>t.wordAt(i.head)||p.cursor(i.head)),r.mainIndex);return n.eq(r)?!1:(e(t.update({selection:n})),!0)};function $e(t,e){let{main:r,ranges:n}=t.selection,i=t.wordAt(r.head),s=i&&i.from==r.from&&i.to==r.to;for(let o=!1,l=new y(t.doc,e,n[n.length-1].to);;)if(l.next(),l.done){if(o)return null;l=new y(t.doc,e,0,Math.max(0,n[n.length-1].from-1)),o=!0}else{if(o&&n.some(c=>c.from==l.value.from))continue;if(s){let c=t.wordAt(l.value.from);if(!c||c.from!=l.value.from||c.to!=l.value.to)continue}return l.value}}const me=({state:t,dispatch:e})=>{let{ranges:r}=t.selection;if(r.some(s=>s.from===s.to))return Oe({state:t,dispatch:e});let n=t.sliceDoc(r[0].from,r[0].to);if(t.selection.ranges.some(s=>t.sliceDoc(s.from,s.to)!=n))return!1;let i=$e(t,n);return i?(e(t.update({selection:t.selection.addRange(p.range(i.from,i.to),!1),effects:S.scrollIntoView(i.to)})),!0):!1},M=ie.define({combine(t){return se(t,{top:!1,caseSensitive:!1,literal:!1,regexp:!1,wholeWord:!1,createPanel:e=>new Ue(e),scrollToMatch:e=>S.scrollIntoView(e)})}});function Qe(t){return t?[M.of(t),Y]:Y}class V{constructor(e){this.search=e.search,this.caseSensitive=!!e.caseSensitive,this.literal=!!e.literal,this.regexp=!!e.regexp,this.replace=e.replace||"",this.valid=!!this.search&&(!this.regexp||Ae(this.search)),this.unquoted=this.unquote(this.search),this.wholeWord=!!e.wholeWord}unquote(e){return this.literal?e:e.replace(/\\([nrt\\])/g,(r,n)=>n=="n"?` +`:n=="r"?"\r":n=="t"?" ":"\\")}eq(e){return this.search==e.search&&this.replace==e.replace&&this.caseSensitive==e.caseSensitive&&this.regexp==e.regexp&&this.wholeWord==e.wholeWord}create(){return this.regexp?new Ne(this):new ze(this)}getCursor(e,r=0,n){let i=e.doc?e:Fe.create({doc:e});return n==null&&(n=i.doc.length),this.regexp?b(this,i,r,n):v(this,i,r,n)}}class pe{constructor(e){this.spec=e}}function v(t,e,r,n){return new y(e.doc,t.unquoted,r,n,t.caseSensitive?void 0:i=>i.toLowerCase(),t.wholeWord?_e(e.doc,e.charCategorizer(e.selection.main.head)):void 0)}function _e(t,e){return(r,n,i,s)=>((s>r||s+i.length=r)return null;i.push(n.value)}return i}highlight(e,r,n,i){let s=v(this.spec,e,Math.max(0,r-this.spec.unquoted.length),Math.min(n+this.spec.unquoted.length,e.doc.length));for(;!s.next().done;)i(s.value.from,s.value.to)}}function b(t,e,r,n){return new z(e.doc,t.search,{ignoreCase:!t.caseSensitive,test:t.wholeWord?Be(e.charCategorizer(e.selection.main.head)):void 0},r,n)}function T(t,e){return t.slice(le(t,e,!1),e)}function I(t,e){return t.slice(e,le(t,e))}function Be(t){return(e,r,n)=>!n[0].length||(t(T(n.input,n.index))!=m.Word||t(I(n.input,n.index))!=m.Word)&&(t(I(n.input,n.index+n[0].length))!=m.Word||t(T(n.input,n.index+n[0].length))!=m.Word)}class Ne extends pe{nextMatch(e,r,n){let i=b(this.spec,e,n,e.doc.length).next();return i.done&&(i=b(this.spec,e,0,r).next()),i.done?null:i.value}prevMatchInRange(e,r,n){for(let i=1;;i++){let s=Math.max(r,n-i*1e4),o=b(this.spec,e,s,n),l=null;for(;!o.next().done;)l=o.value;if(l&&(s==r||l.from>s+10))return l;if(s==r)return null}}prevMatch(e,r,n){return this.prevMatchInRange(e,0,r)||this.prevMatchInRange(e,n,e.doc.length)}getReplacement(e){return this.spec.unquote(this.spec.replace).replace(/\$([$&\d+])/g,(r,n)=>n=="$"?"$":n=="&"?e.match[0]:n!="0"&&+n=r)return null;i.push(n.value)}return i}highlight(e,r,n,i){let s=b(this.spec,e,Math.max(0,r-250),Math.min(n+250,e.doc.length));for(;!s.next().done;)i(s.value.from,s.value.to)}}const C=W.define(),H=W.define(),g=ne.define({create(t){return new K(w(t).create(),null)},update(t,e){for(let r of e.effects)r.is(C)?t=new K(r.value.create(),t.panel):r.is(H)&&(t=new K(t.query,r.value?j:null));return t},provide:t=>te.from(t,e=>e.panel)});function Ve(t){let e=t.field(g,!1);return e?e.query.spec:w(t)}function He(t){var e;return((e=t.field(g,!1))===null||e===void 0?void 0:e.panel)!=null}class K{constructor(e,r){this.query=e,this.panel=r}}const Ke=f.mark({class:"cm-searchMatch"}),Ge=f.mark({class:"cm-searchMatch cm-searchMatch-selected"}),je=re.fromClass(class{constructor(t){this.view=t,this.decorations=this.highlight(t.state.field(g))}update(t){let e=t.state.field(g);(e!=t.startState.field(g)||t.docChanged||t.selectionSet||t.viewportChanged)&&(this.decorations=this.highlight(e))}highlight({query:t,panel:e}){if(!e||!t.spec.valid)return f.none;let{view:r}=this,n=new Le;for(let i=0,s=r.visibleRanges,o=s.length;is[i+1].from-2*250;)c=s[++i].to;t.highlight(r.state,l,c,(a,u)=>{let x=r.state.selection.ranges.some(E=>E.from==a&&E.to==u);n.add(a,u,x?Ge:Ke)})}return n.finish()}},{decorations:t=>t.decorations});function F(t){return e=>{let r=e.state.field(g,!1);return r&&r.query.spec.valid?t(e,r):J(e)}}const A=F((t,{query:e})=>{let{to:r}=t.state.selection.main,n=e.nextMatch(t.state,r,r);if(!n)return!1;let i=p.single(n.from,n.to),s=t.state.facet(M);return t.dispatch({selection:i,effects:[X(t,n),s.scrollToMatch(i.main,t)],userEvent:"select.search"}),Me(t),!0}),D=F((t,{query:e})=>{let{state:r}=t,{from:n}=r.selection.main,i=e.prevMatch(r,n,n);if(!i)return!1;let s=p.single(i.from,i.to),o=t.state.facet(M);return t.dispatch({selection:s,effects:[X(t,i),o.scrollToMatch(s.main,t)],userEvent:"select.search"}),Me(t),!0}),ge=F((t,{query:e})=>{let r=e.matchAll(t.state,1e3);return!r||!r.length?!1:(t.dispatch({selection:p.create(r.map(n=>p.range(n.from,n.to))),userEvent:"select.search.matches"}),!0)}),xe=({state:t,dispatch:e})=>{let r=t.selection;if(r.ranges.length>1||r.main.empty)return!1;let{from:n,to:i}=r.main,s=[],o=0;for(let l=new y(t.doc,t.sliceDoc(n,i));!l.next().done;){if(s.length>1e3)return!1;l.value.from==n&&(o=s.length),s.push(p.range(l.value.from,l.value.to))}return e(t.update({selection:p.create(s,o),userEvent:"select.search.matches"})),!0},G=F((t,{query:e})=>{let{state:r}=t,{from:n,to:i}=r.selection.main;if(r.readOnly)return!1;let s=e.nextMatch(r,n,n);if(!s)return!1;let o=[],l,c,a=[];if(s.from==n&&s.to==i&&(c=r.toText(e.getReplacement(s)),o.push({from:s.from,to:s.to,insert:c}),s=e.nextMatch(r,s.from,s.to),a.push(S.announce.of(r.phrase("replaced match on line $",r.doc.lineAt(n).number)+"."))),s){let u=o.length==0||o[0].from>=s.to?0:s.to-s.from-c.length;l=p.single(s.from-u,s.to-u),a.push(X(t,s)),a.push(r.facet(M).scrollToMatch(l.main,t))}return t.dispatch({changes:o,selection:l,effects:a,userEvent:"input.replace"}),!0}),Se=F((t,{query:e})=>{if(t.state.readOnly)return!1;let r=e.matchAll(t.state,1e9).map(i=>{let{from:s,to:o}=i;return{from:s,to:o,insert:e.getReplacement(i)}});if(!r.length)return!1;let n=t.state.phrase("replaced $ matches",r.length)+".";return t.dispatch({changes:r,effects:S.announce.of(n),userEvent:"input.replace.all"}),!0});function j(t){return t.state.facet(M).createPanel(t)}function w(t,e){var r,n,i,s,o;let l=t.selection.main,c=l.empty||l.to>l.from+100?"":t.sliceDoc(l.from,l.to);if(e&&!c)return e;let a=t.facet(M);return new V({search:((r=e?.literal)!==null&&r!==void 0?r:a.literal)?c:c.replace(/\n/g,"\\n"),caseSensitive:(n=e?.caseSensitive)!==null&&n!==void 0?n:a.caseSensitive,literal:(i=e?.literal)!==null&&i!==void 0?i:a.literal,regexp:(s=e?.regexp)!==null&&s!==void 0?s:a.regexp,wholeWord:(o=e?.wholeWord)!==null&&o!==void 0?o:a.wholeWord})}function ye(t){let e=R(t,j);return e&&e.dom.querySelector("[main-field]")}function Me(t){let e=ye(t);e&&e==t.root.activeElement&&e.select()}const J=t=>{let e=t.state.field(g,!1);if(e&&e.panel){let r=ye(t);if(r&&r!=t.root.activeElement){let n=w(t.state,e.query.spec);n.valid&&t.dispatch({effects:C.of(n)}),r.focus(),r.select()}}else t.dispatch({effects:[H.of(!0),e?C.of(w(t.state,e.query.spec)):W.appendConfig.of(Y)]});return!0},U=t=>{let e=t.state.field(g,!1);if(!e||!e.panel)return!1;let r=R(t,j);return r&&r.dom.contains(t.root.activeElement)&&t.focus(),t.dispatch({effects:H.of(!1)}),!0},Je=[{key:"Mod-f",run:J,scope:"editor search-panel"},{key:"F3",run:A,shift:D,scope:"editor search-panel",preventDefault:!0},{key:"Mod-g",run:A,shift:D,scope:"editor search-panel",preventDefault:!0},{key:"Escape",run:U,scope:"editor search-panel"},{key:"Mod-Shift-l",run:xe},{key:"Mod-Alt-g",run:ue},{key:"Mod-d",run:me,preventDefault:!0}];class Ue{constructor(e){this.view=e;let r=this.query=e.state.field(g).query.spec;this.commit=this.commit.bind(this),this.searchField=h("input",{value:r.search,placeholder:d(e,"Find"),"aria-label":d(e,"Find"),class:"cm-textfield",name:"search",form:"","main-field":"true",onchange:this.commit,onkeyup:this.commit}),this.replaceField=h("input",{value:r.replace,placeholder:d(e,"Replace"),"aria-label":d(e,"Replace"),class:"cm-textfield",name:"replace",form:"",onchange:this.commit,onkeyup:this.commit}),this.caseField=h("input",{type:"checkbox",name:"case",form:"",checked:r.caseSensitive,onchange:this.commit}),this.reField=h("input",{type:"checkbox",name:"re",form:"",checked:r.regexp,onchange:this.commit}),this.wordField=h("input",{type:"checkbox",name:"word",form:"",checked:r.wholeWord,onchange:this.commit});function n(i,s,o){return h("button",{class:"cm-button",name:i,onclick:s,type:"button"},o)}this.dom=h("div",{onkeydown:i=>this.keydown(i),class:"cm-search"},[this.searchField,n("next",()=>A(e),[d(e,"next")]),n("prev",()=>D(e),[d(e,"previous")]),n("select",()=>ge(e),[d(e,"all")]),h("label",null,[this.caseField,d(e,"match case")]),h("label",null,[this.reField,d(e,"regexp")]),h("label",null,[this.wordField,d(e,"by word")]),...e.state.readOnly?[]:[h("br"),this.replaceField,n("replace",()=>G(e),[d(e,"replace")]),n("replaceAll",()=>Se(e),[d(e,"replace all")])],h("button",{name:"close",onclick:()=>U(e),"aria-label":d(e,"close"),type:"button"},["\xD7"])])}commit(){let e=new V({search:this.searchField.value,caseSensitive:this.caseField.checked,regexp:this.reField.checked,wholeWord:this.wordField.checked,replace:this.replaceField.value});e.eq(this.query)||(this.query=e,this.view.dispatch({effects:C.of(e)}))}keydown(e){ve(this.view,e,"search-panel")?e.preventDefault():e.keyCode==13&&e.target==this.searchField?(e.preventDefault(),(e.shiftKey?D:A)(this.view)):e.keyCode==13&&e.target==this.replaceField&&(e.preventDefault(),G(this.view))}update(e){for(let r of e.transactions)for(let n of r.effects)n.is(C)&&!n.value.eq(this.query)&&this.setQuery(n.value)}setQuery(e){this.query=e,this.searchField.value=e.search,this.replaceField.value=e.replace,this.caseField.checked=e.caseSensitive,this.reField.checked=e.regexp,this.wordField.checked=e.wholeWord}mount(){this.searchField.select()}get pos(){return 80}get top(){return this.view.state.facet(M).top}}function d(t,e){return t.state.phrase(e)}const O=30,$=/[\s\.,:;?!]/;function X(t,{from:e,to:r}){let n=t.state.doc.lineAt(e),i=t.state.doc.lineAt(r).to,s=Math.max(n.from,e-O),o=Math.min(i,r+O),l=t.state.sliceDoc(s,o);if(s!=n.from){for(let c=0;cl.length-O;c--)if(!$.test(l[c-1])&&$.test(l[c])){l=l.slice(0,c);break}}return S.announce.of(`${t.state.phrase("current match")}. ${l} ${t.state.phrase("on line")} ${n.number}.`)}const Xe=S.baseTheme({".cm-panel.cm-search":{padding:"2px 6px 4px",position:"relative","& [name=close]":{position:"absolute",top:"0",right:"4px",backgroundColor:"inherit",border:"none",font:"inherit",padding:0,margin:0},"& input, & button, & label":{margin:".2em .6em .2em 0"},"& input[type=checkbox]":{marginRight:".2em"},"& label":{fontSize:"80%",whiteSpace:"pre"}},"&light .cm-searchMatch":{backgroundColor:"#ffff0054"},"&dark .cm-searchMatch":{backgroundColor:"#00ffff8a"},"&light .cm-searchMatch-selected":{backgroundColor:"#ff6a0054"},"&dark .cm-searchMatch-selected":{backgroundColor:"#ff00ff8a"}}),Y=[g,We.low(je),Xe];export{z as RegExpCursor,y as SearchCursor,V as SearchQuery,U as closeSearchPanel,A as findNext,D as findPrevious,Ve as getSearchQuery,ue as gotoLine,Re as highlightSelectionMatches,J as openSearchPanel,Se as replaceAll,G as replaceNext,Qe as search,Je as searchKeymap,He as searchPanelOpen,ge as selectMatches,me as selectNextOccurrence,xe as selectSelectionMatches,C as setSearchQuery}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/state.js b/Resources/Public/JavaScript/Contrib/@codemirror/state.js new file mode 100644 index 0000000..14bbf67 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/state.js @@ -0,0 +1,5 @@ +class v{lineAt(e){if(e<0||e>this.length)throw new RangeError(`Invalid position ${e} in document of length ${this.length}`);return this.lineInner(e,!1,1,0)}line(e){if(e<1||e>this.lines)throw new RangeError(`Invalid line number ${e} in ${this.lines}-line document`);return this.lineInner(e,!0,1,0)}replace(e,t,n){[e,t]=J(this,e,t);let i=[];return this.decompose(0,e,i,2),n.length&&n.decompose(0,n.length,i,3),this.decompose(t,this.length,i,1),E.from(i,this.length-(t-e)+n.length)}append(e){return this.replace(this.length,this.length,e)}slice(e,t=this.length){[e,t]=J(this,e,t);let n=[];return this.decompose(e,t,n,0),E.from(n,t-e)}eq(e){if(e==this)return!0;if(e.length!=this.length||e.lines!=this.lines)return!1;let t=this.scanIdentical(e,1),n=this.length-this.scanIdentical(e,-1),i=new q(this),s=new q(e);for(let r=t,h=t;;){if(i.next(r),s.next(r),r=0,i.lineBreak!=s.lineBreak||i.done!=s.done||i.value!=s.value)return!1;if(h+=i.value.length,i.done||h>=n)return!0}}iter(e=1){return new q(this,e)}iterRange(e,t=this.length){return new ke(this,e,t)}iterLines(e,t){let n;if(e==null)n=this.iter();else{t==null&&(t=this.lines+1);let i=this.line(e).from;n=this.iterRange(i,Math.max(i,t==this.lines+1?this.length:t<=1?0:this.line(t-1).to))}return new ye(n)}toString(){return this.sliceString(0)}toJSON(){let e=[];return this.flatten(e),e}constructor(){}static of(e){if(e.length==0)throw new RangeError("A document must have at least one line");return e.length==1&&!e[0]?v.empty:e.length<=32?new m(e):E.from(m.split(e,[]))}}class m extends v{constructor(e,t=He(e)){super(),this.text=e,this.length=t}get lines(){return this.text.length}get children(){return null}lineInner(e,t,n,i){for(let s=0;;s++){let r=this.text[s],h=i+r.length;if((t?n:h)>=e)return new Se(i,h,n,r);i=h+1,n++}}decompose(e,t,n,i){let s=e<=0&&t>=this.length?this:new m(xe(this.text,e,t),Math.min(t,this.length)-Math.max(0,e));if(i&1){let r=n.pop(),h=Q(s.text,r.text.slice(),0,s.length);if(h.length<=32)n.push(new m(h,r.length+s.length));else{let a=h.length>>1;n.push(new m(h.slice(0,a)),new m(h.slice(a)))}}else n.push(s)}replace(e,t,n){if(!(n instanceof m))return super.replace(e,t,n);[e,t]=J(this,e,t);let i=Q(this.text,Q(n.text,xe(this.text,0,e)),t),s=this.length+n.length-(t-e);return i.length<=32?new m(i,s):E.from(m.split(i,[]),s)}sliceString(e,t=this.length,n=` +`){[e,t]=J(this,e,t);let i="";for(let s=0,r=0;s<=t&&re&&r&&(i+=n),es&&(i+=h.slice(Math.max(0,e-s),t-s)),s=a+1}return i}flatten(e){for(let t of this.text)e.push(t)}scanIdentical(){return 0}static split(e,t){let n=[],i=-1;for(let s of e)n.push(s),i+=s.length+1,n.length==32&&(t.push(new m(n,i)),n=[],i=-1);return i>-1&&t.push(new m(n,i)),t}}class E extends v{constructor(e,t){super(),this.children=e,this.length=t,this.lines=0;for(let n of e)this.lines+=n.lines}lineInner(e,t,n,i){for(let s=0;;s++){let r=this.children[s],h=i+r.length,a=n+r.lines-1;if((t?a:h)>=e)return r.lineInner(e,t,n,i);i=h+1,n=a+1}}decompose(e,t,n,i){for(let s=0,r=0;r<=t&&s=r){let o=i&((r<=e?1:0)|(a>=t?2:0));r>=e&&a<=t&&!o?n.push(h):h.decompose(e-r,t-r,n,o)}r=a+1}}replace(e,t,n){if([e,t]=J(this,e,t),n.lines=s&&t<=h){let a=r.replace(e-s,t-s,n),o=this.lines-r.lines+a.lines;if(a.lines>4&&a.lines>o>>6){let f=this.children.slice();return f[i]=a,new E(f,this.length-(t-e)+n.length)}return super.replace(s,h,a)}s=h+1}return super.replace(e,t,n)}sliceString(e,t=this.length,n=` +`){[e,t]=J(this,e,t);let i="";for(let s=0,r=0;se&&s&&(i+=n),er&&(i+=h.sliceString(e-r,t-r,n)),r=a+1}return i}flatten(e){for(let t of this.children)t.flatten(e)}scanIdentical(e,t){if(!(e instanceof E))return 0;let n=0,[i,s,r,h]=t>0?[0,0,this.children.length,e.children.length]:[this.children.length-1,e.children.length-1,-1,-1];for(;;i+=t,s+=t){if(i==r||s==h)return n;let a=this.children[i],o=e.children[s];if(a!=o)return n+a.scanIdentical(o,t);n+=a.length+1}}static from(e,t=e.reduce((n,i)=>n+i.length+1,-1)){let n=0;for(let c of e)n+=c.lines;if(n<32){let c=[];for(let p of e)p.flatten(c);return new m(c,t)}let i=Math.max(32,n>>5),s=i<<1,r=i>>1,h=[],a=0,o=-1,f=[];function u(c){let p;if(c.lines>s&&c instanceof E)for(let b of c.children)u(b);else c.lines>r&&(a>r||!a)?(d(),h.push(c)):c instanceof m&&a&&(p=f[f.length-1])instanceof m&&c.lines+p.lines<=32?(a+=c.lines,o+=c.length+1,f[f.length-1]=new m(p.text.concat(c.text),p.length+1+c.length)):(a+c.lines>i&&d(),a+=c.lines,o+=c.length+1,f.push(c))}function d(){a!=0&&(h.push(f.length==1?f[0]:E.from(f,o)),o=-1,a=f.length=0)}for(let c of e)u(c);return d(),h.length==1?h[0]:new E(h,t)}}v.empty=new m([""],0);function He(l){let e=-1;for(let t of l)e+=t.length+1;return e}function Q(l,e,t=0,n=1e9){for(let i=0,s=0,r=!0;s=t&&(a>n&&(h=h.slice(0,n-i)),i0?1:(e instanceof m?e.text.length:e.children.length)<<1]}nextInner(e,t){for(this.done=this.lineBreak=!1;;){let n=this.nodes.length-1,i=this.nodes[n],s=this.offsets[n],r=s>>1,h=i instanceof m?i.text.length:i.children.length;if(r==(t>0?h:0)){if(n==0)return this.done=!0,this.value="",this;t>0&&this.offsets[n-1]++,this.nodes.pop(),this.offsets.pop()}else if((s&1)==(t>0?0:1)){if(this.offsets[n]+=t,e==0)return this.lineBreak=!0,this.value=` +`,this;e--}else if(i instanceof m){let a=i.text[r+(t<0?-1:0)];if(this.offsets[n]+=t,a.length>Math.max(0,e))return this.value=e==0?a:t>0?a.slice(e):a.slice(0,a.length-e),this;e-=a.length}else{let a=i.children[r+(t<0?-1:0)];e>a.length?(e-=a.length,this.offsets[n]+=t):(t<0&&this.offsets[n]--,this.nodes.push(a),this.offsets.push(t>0?1:(a instanceof m?a.text.length:a.children.length)<<1))}}}next(e=0){return e<0&&(this.nextInner(-e,-this.dir),e=this.value.length),this.nextInner(e,this.dir)}}class ke{constructor(e,t,n){this.value="",this.done=!1,this.cursor=new q(e,t>n?-1:1),this.pos=t>n?e.length:0,this.from=Math.min(t,n),this.to=Math.max(t,n)}nextInner(e,t){if(t<0?this.pos<=this.from:this.pos>=this.to)return this.value="",this.done=!0,this;e+=Math.max(0,t<0?this.pos-this.to:this.from-this.pos);let n=t<0?this.pos-this.from:this.to-this.pos;e>n&&(e=n),n-=e;let{value:i}=this.cursor.next(e);return this.pos+=(i.length+e)*t,this.value=i.length<=n?i:t<0?i.slice(i.length-n):i.slice(0,n),this.done=!this.value,this}next(e=0){return e<0?e=Math.max(e,this.from-this.pos):e>0&&(e=Math.min(e,this.to-this.pos)),this.nextInner(e,this.cursor.dir)}get lineBreak(){return this.cursor.lineBreak&&this.value!=""}}class ye{constructor(e){this.inner=e,this.afterBreak=!0,this.value="",this.done=!1}next(e=0){let{done:t,lineBreak:n,value:i}=this.inner.next(e);return t&&this.afterBreak?(this.value="",this.afterBreak=!1):t?(this.done=!0,this.value=""):n?this.afterBreak?this.value="":(this.afterBreak=!0,this.next()):(this.value=i,this.afterBreak=!1),this}get lineBreak(){return!1}}typeof Symbol<"u"&&(v.prototype[Symbol.iterator]=function(){return this.iter()},q.prototype[Symbol.iterator]=ke.prototype[Symbol.iterator]=ye.prototype[Symbol.iterator]=function(){return this});class Se{constructor(e,t,n,i){this.from=e,this.to=t,this.number=n,this.text=i}get length(){return this.to-this.from}}function J(l,e,t){return e=Math.max(0,Math.min(l.length,e)),[e,Math.max(e,Math.min(l.length,t))]}let L="lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,3b,f,,w,1j,,,,4,8,4,,3,7,a,2,t,,1m,,,,2,4,8,,9,,a,2,q,,2,2,1l,,4,2,4,2,2,3,3,,u,2,3,,b,2,1l,,4,5,,2,4,,k,2,m,6,,,1m,,,2,,4,8,,7,3,a,2,u,,1n,,,,c,,9,,14,,3,,1l,3,5,3,,4,7,2,b,2,t,,1m,,2,,2,,3,,5,2,7,2,b,2,s,2,1l,2,,,2,4,8,,9,,a,2,t,,20,,4,,2,3,,,8,,29,,2,7,c,8,2q,,2,9,b,6,22,2,r,,,,,,1j,e,,5,,2,5,b,,10,9,,2u,4,,6,,2,2,2,p,2,4,3,g,4,d,,2,2,6,,f,,jj,3,qa,3,t,3,t,2,u,2,1s,2,,7,8,,2,b,9,,19,3,3b,2,y,,3a,3,4,2,9,,6,3,63,2,2,,1m,,,7,,,,,2,8,6,a,2,,1c,h,1r,4,1c,7,,,5,,14,9,c,2,w,4,2,2,,3,1k,,,2,3,,,3,1m,8,2,2,48,3,,d,,7,4,,6,,3,2,5i,1m,,5,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,p,2,5,,47,2,q,i,d,,12,8,p,b,1a,3,1c,,2,4,2,2,13,,1v,6,2,2,2,2,c,,8,,1b,,1f,,,3,2,2,5,2,,,16,2,8,,6m,,2,,4,,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,ar,2,49,b,4w,,1i,f,1k,3,1d,4,2,2,1x,3,10,5,,8,1q,,c,2,1g,9,a,4,2,,2n,3,2,,,2,6,,4g,,3,8,l,2,1l,2,,,,,m,,e,7,3,5,5f,8,2,3,,,n,,29,,2,6,,,2,,,2,,2,6j,,2,4,6,2,,2,r,2,2d,8,2,,,2,2y,,,,2,6,,,2t,3,2,4,,5,77,9,,2,6t,,a,2,,,4,,40,4,2,2,4,,w,a,14,6,2,4,8,,9,6,2,3,1a,d,,2,ba,7,,6,,,2a,m,2,7,,2,,2,3e,6,3,,,2,,7,,,20,2,3,,,,9n,2,f0b,5,1n,7,t4,,1r,4,29,,f5k,2,43q,,,3,4,5,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,79,7,c5,4,15s,7,31,7,240,5,gx7k,2o,3k,6o".split(",").map(l=>l?parseInt(l,36):1);for(let l=1;ll)return L[e-1]<=l;return!1}function Ie(l){return l>=127462&&l<=127487}const Pe=8205;function $(l,e,t=!0,n=!0){return(t?Ae:Ke)(l,e,n)}function Ae(l,e,t){if(e==l.length)return e;e&&be(l.charCodeAt(e))&&Oe(l.charCodeAt(e-1))&&e--;let n=X(l,e);for(e+=se(n);e=0&&Ie(X(l,r));)s++,r-=2;if(s%2==0)break;e+=2}else break}return e}function Ke(l,e,t){for(;e>0;){let n=Ae(l,e-2,t);if(n=56320&&l<57344}function Oe(l){return l>=55296&&l<56320}function X(l,e){let t=l.charCodeAt(e);if(!Oe(t)||e+1==l.length)return t;let n=l.charCodeAt(e+1);return be(n)?(t-55296<<10)+(n-56320)+65536:t}function Qe(l){return l<=65535?String.fromCharCode(l):(l-=65536,String.fromCharCode((l>>10)+55296,(l&1023)+56320))}function se(l){return l<65536?1:2}const re=/\r\n?|\n/;var C=function(l){return l[l.Simple=0]="Simple",l[l.TrackDel=1]="TrackDel",l[l.TrackBefore=2]="TrackBefore",l[l.TrackAfter=3]="TrackAfter",l}(C||(C={}));class O{constructor(e){this.sections=e}get length(){let e=0;for(let t=0;te)return s+(e-i);s+=h}else{if(n!=C.Simple&&o>=e&&(n==C.TrackDel&&ie||n==C.TrackBefore&&ie))return null;if(o>e||o==e&&t<0&&!h)return e==i||t<0?s:s+a;s+=a}i=o}if(e>i)throw new RangeError(`Position ${e} is out of range for changeset of length ${i}`);return s}touchesRange(e,t=e){for(let n=0,i=0;n=0&&i<=t&&h>=e)return it?"cover":!0;i=h}return!1}toString(){let e="";for(let t=0;t=0?":"+i:"")}return e}toJSON(){return this.sections}static fromJSON(e){if(!Array.isArray(e)||e.length%2||e.some(t=>typeof t!="number"))throw new RangeError("Invalid JSON representation of ChangeDesc");return new O(e)}static create(e){return new O(e)}}class k extends O{constructor(e,t){super(e),this.inserted=t}apply(e){if(this.length!=e.length)throw new RangeError("Applying change set to a document with the wrong length");return le(this,(t,n,i,s,r)=>e=e.replace(i,i+(n-t),r),!1),e}mapDesc(e,t=!1){return he(this,e,t,!0)}invert(e){let t=this.sections.slice(),n=[];for(let i=0,s=0;i=0){t[i]=h,t[i+1]=r;let a=i>>1;for(;n.length0&&R(n,t,s.text),s.forward(f),h+=f}let o=e[r++];for(;h>1].toJSON()))}return e}static of(e,t,n){let i=[],s=[],r=0,h=null;function a(f=!1){if(!f&&!i.length)return;rd||u<0||d>t)throw new RangeError(`Invalid change range ${u} to ${d} (in doc of length ${t})`);let p=c?typeof c=="string"?v.of(c.split(n||re)):c:v.empty,b=p.length;if(u==d&&b==0)return;ur&&y(i,u-r,-1),y(i,d-u,b),R(s,i,p),r=d}}return o(e),a(!h),h}static empty(e){return new k(e?[e,-1]:[],[])}static fromJSON(e){if(!Array.isArray(e))throw new RangeError("Invalid JSON representation of ChangeSet");let t=[],n=[];for(let i=0;ih&&typeof r!="string"))throw new RangeError("Invalid JSON representation of ChangeSet");if(s.length==1)t.push(s[0],0);else{for(;n.length=0&&t<=0&&t==l[i+1]?l[i]+=e:e==0&&l[i]==0?l[i+1]+=t:n?(l[i]+=e,l[i+1]+=t):l.push(e,t)}function R(l,e,t){if(t.length==0)return;let n=e.length-2>>1;if(n>1])),!(t||r==l.sections.length||l.sections[r+1]<0);)h=l.sections[r++],a=l.sections[r++];e(i,o,s,f,u),i=o,s=f}}}function he(l,e,t,n=!1){let i=[],s=n?[]:null,r=new z(l),h=new z(e);for(let a=-1;;)if(r.ins==-1&&h.ins==-1){let o=Math.min(r.len,h.len);y(i,o,-1),r.forward(o),h.forward(o)}else if(h.ins>=0&&(r.ins<0||a==r.i||r.off==0&&(h.len=0&&a=0){let o=0,f=r.len;for(;f;)if(h.ins==-1){let u=Math.min(f,h.len);o+=u,f-=u,h.forward(u)}else if(h.ins==0&&h.lena||r.ins>=0&&r.len>a)&&(h||n.length>o),s.forward2(a),r.forward(a)}}}}class z{constructor(e){this.set=e,this.i=0,this.next()}next(){let{sections:e}=this.set;this.i>1;return t>=e.length?v.empty:e[t]}textBit(e){let{inserted:t}=this.set,n=this.i-2>>1;return n>=t.length&&!e?v.empty:t[n].slice(this.off,e==null?void 0:this.off+e)}forward(e){e==this.len?this.next():(this.len-=e,this.off+=e)}forward2(e){this.ins==-1?this.forward(e):e==this.ins?this.next():(this.ins-=e,this.off+=e)}}class T{constructor(e,t,n){this.from=e,this.to=t,this.flags=n}get anchor(){return this.flags&32?this.to:this.from}get head(){return this.flags&32?this.from:this.to}get empty(){return this.from==this.to}get assoc(){return this.flags&8?-1:this.flags&16?1:0}get bidiLevel(){let e=this.flags&7;return e==7?null:e}get goalColumn(){let e=this.flags>>6;return e==16777215?void 0:e}map(e,t=-1){let n,i;return this.empty?n=i=e.mapPos(this.from,t):(n=e.mapPos(this.from,1),i=e.mapPos(this.to,-1)),n==this.from&&i==this.to?this:new T(n,i,this.flags)}extend(e,t=e){if(e<=this.anchor&&t>=this.anchor)return g.range(e,t);let n=Math.abs(e-this.anchor)>Math.abs(t-this.anchor)?e:t;return g.range(this.anchor,n)}eq(e,t=!1){return this.anchor==e.anchor&&this.head==e.head&&(!t||!this.empty||this.assoc==e.assoc)}toJSON(){return{anchor:this.anchor,head:this.head}}static fromJSON(e){if(!e||typeof e.anchor!="number"||typeof e.head!="number")throw new RangeError("Invalid JSON representation for SelectionRange");return g.range(e.anchor,e.head)}static create(e,t,n){return new T(e,t,n)}}class g{constructor(e,t){this.ranges=e,this.mainIndex=t}map(e,t=-1){return e.empty?this:g.create(this.ranges.map(n=>n.map(e,t)),this.mainIndex)}eq(e,t=!1){if(this.ranges.length!=e.ranges.length||this.mainIndex!=e.mainIndex)return!1;for(let n=0;ne.toJSON()),main:this.mainIndex}}static fromJSON(e){if(!e||!Array.isArray(e.ranges)||typeof e.main!="number"||e.main>=e.ranges.length)throw new RangeError("Invalid JSON representation for EditorSelection");return new g(e.ranges.map(t=>T.fromJSON(t)),e.main)}static single(e,t=e){return new g([g.range(e,t)],0)}static create(e,t=0){if(e.length==0)throw new RangeError("A selection needs at least one range");for(let n=0,i=0;ie?8:0)|s)}static normalized(e,t=0){let n=e[t];e.sort((i,s)=>i.from-s.from),t=e.indexOf(n);for(let i=1;is.head?g.range(a,h):g.range(h,a))}}return new g(e,t)}}function Ce(l,e){for(let t of l.ranges)if(t.to>e)throw new RangeError("Selection points outside of document")}let ae=0;class A{constructor(e,t,n,i,s){this.combine=e,this.compareInput=t,this.compare=n,this.isStatic=i,this.id=ae++,this.default=e([]),this.extensions=typeof s=="function"?s(this):s}get reader(){return this}static define(e={}){return new A(e.combine||(t=>t),e.compareInput||((t,n)=>t===n),e.compare||(e.combine?(t,n)=>t===n:oe),!!e.static,e.enables)}of(e){return new Y([],this,0,e)}compute(e,t){if(this.isStatic)throw new Error("Can't compute a static facet");return new Y(e,this,1,t)}computeN(e,t){if(this.isStatic)throw new Error("Can't compute a static facet");return new Y(e,this,2,t)}from(e,t){return t||(t=n=>n),this.compute([e],n=>t(n.field(e)))}}function oe(l,e){return l==e||l.length==e.length&&l.every((t,n)=>t===e[n])}class Y{constructor(e,t,n,i){this.dependencies=e,this.facet=t,this.type=n,this.value=i,this.id=ae++}dynamicSlot(e){var t;let n=this.value,i=this.facet.compareInput,s=this.id,r=e[s]>>1,h=this.type==2,a=!1,o=!1,f=[];for(let u of this.dependencies)u=="doc"?a=!0:u=="selection"?o=!0:(((t=e[u.id])!==null&&t!==void 0?t:1)&1)==0&&f.push(e[u.id]);return{create(u){return u.values[r]=n(u),1},update(u,d){if(a&&d.docChanged||o&&(d.docChanged||d.selection)||fe(u,f)){let c=n(u);if(h?!Me(c,u.values[r],i):!i(c,u.values[r]))return u.values[r]=c,1}return 0},reconfigure:(u,d)=>{let c,p=d.config.address[s];if(p!=null){let b=_(d,p);if(this.dependencies.every(x=>x instanceof A?d.facet(x)===u.facet(x):x instanceof F?d.field(x,!1)==u.field(x,!1):!0)||(h?Me(c=n(u),b,i):i(c=n(u),b)))return u.values[r]=b,0}else c=n(u);return u.values[r]=c,1}}}}function Me(l,e,t){if(l.length!=e.length)return!1;for(let n=0;nl[a.id]),i=t.map(a=>a.type),s=n.filter(a=>!(a&1)),r=l[e.id]>>1;function h(a){let o=[];for(let f=0;fn===i),e);return e.provide&&(t.provides=e.provide(t)),t}create(e){let t=e.facet(Re).find(n=>n.field==this);return(t?.create||this.createF)(e)}slot(e){let t=e[this.id]>>1;return{create:n=>(n.values[t]=this.create(n),1),update:(n,i)=>{let s=n.values[t],r=this.updateF(s,i);return this.compareF(s,r)?0:(n.values[t]=r,1)},reconfigure:(n,i)=>i.config.address[this.id]!=null?(n.values[t]=i.field(this),0):(n.values[t]=this.create(n),1)}}init(e){return[this,Re.of({field:this,create:e})]}get extension(){return this}}const B={lowest:4,low:3,default:2,high:1,highest:0};function W(l){return e=>new Te(e,l)}const Ye={highest:W(B.highest),high:W(B.high),default:W(B.default),low:W(B.low),lowest:W(B.lowest)};class Te{constructor(e,t){this.inner=e,this.prec=t}}class H{of(e){return new ue(this,e)}reconfigure(e){return H.reconfigure.of({compartment:this,extension:e})}get(e){return e.config.compartments.get(this)}}class ue{constructor(e,t){this.compartment=e,this.inner=t}}class ne{constructor(e,t,n,i,s,r){for(this.base=e,this.compartments=t,this.dynamicSlots=n,this.address=i,this.staticValues=s,this.facets=r,this.statusTemplate=[];this.statusTemplate.length>1]}static resolve(e,t,n){let i=[],s=Object.create(null),r=new Map;for(let d of _e(e,t,r))d instanceof F?i.push(d):(s[d.facet.id]||(s[d.facet.id]=[])).push(d);let h=Object.create(null),a=[],o=[];for(let d of i)h[d.id]=o.length<<1,o.push(c=>d.slot(c));let f=n?.config.facets;for(let d in s){let c=s[d],p=c[0].facet,b=f&&f[d]||[];if(c.every(x=>x.type==0))if(h[p.id]=a.length<<1|1,oe(b,c))a.push(n.facet(p));else{let x=p.combine(c.map(ie=>ie.value));a.push(n&&p.compare(x,n.facet(p))?n.facet(p):x)}else{for(let x of c)x.type==0?(h[x.id]=a.length<<1|1,a.push(x.value)):(h[x.id]=o.length<<1,o.push(ie=>x.dynamicSlot(ie)));h[p.id]=o.length<<1,o.push(x=>Xe(x,p,c))}}let u=o.map(d=>d(h));return new ne(e,r,u,h,a,s)}}function _e(l,e,t){let n=[[],[],[],[],[]],i=new Map;function s(r,h){let a=i.get(r);if(a!=null){if(a<=h)return;let o=n[a].indexOf(r);o>-1&&n[a].splice(o,1),r instanceof ue&&t.delete(r.compartment)}if(i.set(r,h),Array.isArray(r))for(let o of r)s(o,h);else if(r instanceof ue){if(t.has(r.compartment))throw new RangeError("Duplicate use of compartment in extensions");let o=e.get(r.compartment)||r.inner;t.set(r.compartment,o),s(o,h)}else if(r instanceof Te)s(r.inner,r.prec);else if(r instanceof F)n[h].push(r),r.provides&&s(r.provides,h);else if(r instanceof Y)n[h].push(r),r.facet.extensions&&s(r.facet.extensions,B.default);else{let o=r.extension;if(!o)throw new Error(`Unrecognized extension value in extension set (${r}). This sometimes happens because multiple instances of @codemirror/state are loaded, breaking instanceof checks.`);s(o,h)}}return s(l,B.default),n.reduce((r,h)=>r.concat(h))}function U(l,e){if(e&1)return 2;let t=e>>1,n=l.status[t];if(n==4)throw new Error("Cyclic dependency between fields and/or facets");if(n&2)return n;l.status[t]=4;let i=l.computeSlot(l,l.config.dynamicSlots[t]);return l.status[t]=2|i}function _(l,e){return e&1?l.config.staticValues[e>>1]:l.values[e>>1]}const Be=A.define(),ce=A.define({combine:l=>l.some(e=>e),static:!0}),Fe=A.define({combine:l=>l.length?l[0]:void 0,static:!0}),Je=A.define(),Le=A.define(),Ve=A.define(),Ne=A.define({combine:l=>l.length?l[0]:!1});class V{constructor(e,t){this.type=e,this.value=t}static define(){return new De}}class De{of(e){return new V(this,e)}}class qe{constructor(e){this.map=e}of(e){return new S(this,e)}}class S{constructor(e,t){this.type=e,this.value=t}map(e){let t=this.type.map(this.value,e);return t===void 0?void 0:t==this.value?this:new S(this.type,t)}is(e){return this.type==e}static define(e={}){return new qe(e.map||(t=>t))}static mapEffects(e,t){if(!e.length)return e;let n=[];for(let i of e){let s=i.map(t);s&&n.push(s)}return n}}S.reconfigure=S.define(),S.appendConfig=S.define();class I{constructor(e,t,n,i,s,r){this.startState=e,this.changes=t,this.selection=n,this.effects=i,this.annotations=s,this.scrollIntoView=r,this._doc=null,this._state=null,n&&Ce(n,t.newLength),s.some(h=>h.type==I.time)||(this.annotations=s.concat(I.time.of(Date.now())))}static create(e,t,n,i,s,r){return new I(e,t,n,i,s,r)}get newDoc(){return this._doc||(this._doc=this.changes.apply(this.startState.doc))}get newSelection(){return this.selection||this.startState.selection.map(this.changes)}get state(){return this._state||this.startState.applyTransaction(this),this._state}annotation(e){for(let t of this.annotations)if(t.type==e)return t.value}get docChanged(){return!this.changes.empty}get reconfigured(){return this.startState.config!=this.state.config}isUserEvent(e){let t=this.annotation(I.userEvent);return!!(t&&(t==e||t.length>e.length&&t.slice(0,e.length)==e&&t[e.length]=="."))}}I.time=V.define(),I.userEvent=V.define(),I.addToHistory=V.define(),I.remote=V.define();function et(l,e){let t=[];for(let n=0,i=0;;){let s,r;if(n=l[n]))s=l[n++],r=l[n++];else if(i=0;i--){let s=n[i](l);s instanceof I?l=s:Array.isArray(s)&&s.length==1&&s[0]instanceof I?l=s[0]:l=ze(e,N(s),!1)}return l}function nt(l){let e=l.startState,t=e.facet(Ve),n=l;for(let i=t.length-1;i>=0;i--){let s=t[i](l);s&&Object.keys(s).length&&(n=$e(n,de(e,s,l.changes.newLength),!0))}return n==l?l:I.create(e,l.changes,l.selection,n.effects,n.annotations,n.scrollIntoView)}const it=[];function N(l){return l==null?it:Array.isArray(l)?l:[l]}var M=function(l){return l[l.Word=0]="Word",l[l.Space=1]="Space",l[l.Other=2]="Other",l}(M||(M={}));const st=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;let ge;try{ge=new RegExp("[\\p{Alphabetic}\\p{Number}_]","u")}catch{}function rt(l){if(ge)return ge.test(l);for(let e=0;e"\x80"&&(t.toUpperCase()!=t.toLowerCase()||st.test(t)))return!0}return!1}function lt(l){return e=>{if(!/\S/.test(e))return M.Space;if(rt(e))return M.Word;for(let t=0;t-1)return M.Word;return M.Other}}class w{constructor(e,t,n,i,s,r){this.config=e,this.doc=t,this.selection=n,this.values=i,this.status=e.statusTemplate.slice(),this.computeSlot=s,r&&(r._state=this);for(let h=0;hi.set(o,a)),t=null),i.set(h.value.compartment,h.value.extension)):h.is(S.reconfigure)?(t=null,n=h.value):h.is(S.appendConfig)&&(t=null,n=N(n).concat(h.value));let s;t?s=e.startState.values.slice():(t=ne.resolve(n,i,this),s=new w(t,this.doc,this.selection,t.dynamicSlots.map(()=>null),(a,o)=>o.reconfigure(a,this),null).values);let r=e.startState.facet(ce)?e.newSelection:e.newSelection.asSingle();new w(t,e.newDoc,r,s,(h,a)=>a.update(h,e),e)}replaceSelection(e){return typeof e=="string"&&(e=this.toText(e)),this.changeByRange(t=>({changes:{from:t.from,to:t.to,insert:e},range:g.cursor(t.from+e.length)}))}changeByRange(e){let t=this.selection,n=e(t.ranges[0]),i=this.changes(n.changes),s=[n.range],r=N(n.effects);for(let h=1;hr.spec.fromJSON(h,a)))}}return w.create({doc:e.doc,selection:g.fromJSON(e.selection),extensions:t.extensions?i.concat([t.extensions]):i})}static create(e={}){let t=ne.resolve(e.extensions||[],new Map),n=e.doc instanceof v?e.doc:v.of((e.doc||"").split(t.staticFacet(w.lineSeparator)||re)),i=e.selection?e.selection instanceof g?e.selection:g.single(e.selection.anchor,e.selection.head):g.single(0);return Ce(i,n.length),t.staticFacet(ce)||(i=i.asSingle()),new w(t,n,i,t.dynamicSlots.map(()=>null),(s,r)=>r.create(s),null)}get tabSize(){return this.facet(w.tabSize)}get lineBreak(){return this.facet(w.lineSeparator)||` +`}get readOnly(){return this.facet(Ne)}phrase(e,...t){for(let n of this.facet(w.phrases))if(Object.prototype.hasOwnProperty.call(n,e)){e=n[e];break}return t.length&&(e=e.replace(/\$(\$|\d*)/g,(n,i)=>{if(i=="$")return"$";let s=+(i||1);return!s||s>t.length?n:t[s-1]})),e}languageDataAt(e,t,n=-1){let i=[];for(let s of this.facet(Be))for(let r of s(this,t,n))Object.prototype.hasOwnProperty.call(r,e)&&i.push(r[e]);return i}charCategorizer(e){return lt(this.languageDataAt("wordChars",e).join(""))}wordAt(e){let{text:t,from:n,length:i}=this.doc.lineAt(e),s=this.charCategorizer(e),r=e-n,h=e-n;for(;r>0;){let a=$(t,r,!1);if(s(t.slice(a,r))!=M.Word)break;r=a}for(;hl.length?l[0]:4}),w.lineSeparator=Fe,w.readOnly=Ne,w.phrases=A.define({compare(l,e){let t=Object.keys(l),n=Object.keys(e);return t.length==n.length&&t.every(i=>l[i]==e[i])}}),w.languageData=Be,w.changeFilter=Je,w.transactionFilter=Le,w.transactionExtender=Ve,H.reconfigure=S.define();function ht(l,e,t={}){let n={};for(let i of l)for(let s of Object.keys(i)){let r=i[s],h=n[s];if(h===void 0)n[s]=r;else if(!(h===r||r===void 0))if(Object.hasOwnProperty.call(t,s))n[s]=t[s](h,r);else throw new Error("Config merge conflict for field "+s)}for(let i in e)n[i]===void 0&&(n[i]=e[i]);return n}class j{eq(e){return this==e}range(e,t=e){return D.create(e,t,this)}}j.prototype.startSide=j.prototype.endSide=0,j.prototype.point=!1,j.prototype.mapMode=C.TrackDel;class D{constructor(e,t,n){this.from=e,this.to=t,this.value=n}static create(e,t,n){return new D(e,t,n)}}function pe(l,e){return l.from-e.from||l.value.startSide-e.value.startSide}class ve{constructor(e,t,n,i){this.from=e,this.to=t,this.value=n,this.maxPoint=i}get length(){return this.to[this.to.length-1]}findIndex(e,t,n,i=0){let s=n?this.to:this.from;for(let r=i,h=s.length;;){if(r==h)return r;let a=r+h>>1,o=s[a]-e||(n?this.value[a].endSide:this.value[a].startSide)-t;if(a==r)return o>=0?r:h;o>=0?h=a:r=a+1}}between(e,t,n,i){for(let s=this.findIndex(t,-1e9,!0),r=this.findIndex(n,1e9,!1,s);sc||d==c&&o.startSide>0&&o.endSide<=0)continue;(c-d||o.endSide-o.startSide)<0||(r<0&&(r=d),o.point&&(h=Math.max(h,c-d)),n.push(o),i.push(d-r),s.push(c-r))}return{mapped:n.length?new ve(i,s,n,h):null,pos:r}}}class P{constructor(e,t,n,i){this.chunkPos=e,this.chunk=t,this.nextLayer=n,this.maxPoint=i}static create(e,t,n,i){return new P(e,t,n,i)}get length(){let e=this.chunk.length-1;return e<0?0:Math.max(this.chunkEnd(e),this.nextLayer.length)}get size(){if(this.isEmpty)return 0;let e=this.nextLayer.size;for(let t of this.chunk)e+=t.value.length;return e}chunkEnd(e){return this.chunkPos[e]+this.chunk[e].length}update(e){let{add:t=[],sort:n=!1,filterFrom:i=0,filterTo:s=this.length}=e,r=e.filter;if(t.length==0&&!r)return this;if(n&&(t=t.slice().sort(pe)),this.isEmpty)return t.length?P.of(t):this;let h=new Ue(this,null,-1).goto(0),a=0,o=[],f=new Z;for(;h.value||a=0){let u=t[a++];f.addInner(u.from,u.to,u.value)||o.push(u)}else h.rangeIndex==1&&h.chunkIndexthis.chunkEnd(h.chunkIndex)||sh.to||s=s&&e<=s+r.length&&r.between(s,e-s,t-s,n)===!1)return}this.nextLayer.between(e,t,n)}}iter(e=0){return K.from([this]).goto(e)}get isEmpty(){return this.nextLayer==this}static iter(e,t=0){return K.from(e).goto(t)}static compare(e,t,n,i,s=-1){let r=e.filter(u=>u.maxPoint>0||!u.isEmpty&&u.maxPoint>=s),h=t.filter(u=>u.maxPoint>0||!u.isEmpty&&u.maxPoint>=s),a=We(r,h,n),o=new G(r,a,s),f=new G(h,a,s);n.iterGaps((u,d,c)=>je(o,u,f,d,c,i)),n.empty&&n.length==0&&je(o,0,f,0,0,i)}static eq(e,t,n=0,i){i==null&&(i=999999999);let s=e.filter(f=>!f.isEmpty&&t.indexOf(f)<0),r=t.filter(f=>!f.isEmpty&&e.indexOf(f)<0);if(s.length!=r.length)return!1;if(!s.length)return!0;let h=We(s,r),a=new G(s,h,0).goto(n),o=new G(r,h,0).goto(n);for(;;){if(a.to!=o.to||!we(a.active,o.active)||a.point&&(!o.point||!a.point.eq(o.point)))return!1;if(a.to>i)return!0;a.next(),o.next()}}static spans(e,t,n,i,s=-1){let r=new G(e,null,s).goto(t),h=t,a=r.openStart;for(;;){let o=Math.min(r.to,n);if(r.point){let f=r.activeForPoint(r.to),u=r.pointFromh&&(i.span(h,o,r.active,a),a=r.openEnd(o));if(r.to>n)return a+(r.point&&r.to>n?1:0);h=r.to,r.next()}}static of(e,t=!1){let n=new Z;for(let i of e instanceof D?[e]:t?at(e):e)n.add(i.from,i.to,i.value);return n.finish()}static join(e){if(!e.length)return P.empty;let t=e[e.length-1];for(let n=e.length-2;n>=0;n--)for(let i=e[n];i!=P.empty;i=i.nextLayer)t=new P(i.chunkPos,i.chunk,t,Math.max(i.maxPoint,t.maxPoint));return t}}P.empty=new P([],[],null,-1);function at(l){if(l.length>1)for(let e=l[0],t=1;t0)return l.slice().sort(pe);e=n}return l}P.empty.nextLayer=P.empty;class Z{finishChunk(e){this.chunks.push(new ve(this.from,this.to,this.value,this.maxPoint)),this.chunkPos.push(this.chunkStart),this.chunkStart=-1,this.setMaxPoint=Math.max(this.setMaxPoint,this.maxPoint),this.maxPoint=-1,e&&(this.from=[],this.to=[],this.value=[])}constructor(){this.chunks=[],this.chunkPos=[],this.chunkStart=-1,this.last=null,this.lastFrom=-1e9,this.lastTo=-1e9,this.from=[],this.to=[],this.value=[],this.maxPoint=-1,this.setMaxPoint=-1,this.nextLayer=null}add(e,t,n){this.addInner(e,t,n)||(this.nextLayer||(this.nextLayer=new Z)).add(e,t,n)}addInner(e,t,n){let i=e-this.lastTo||n.startSide-this.last.endSide;if(i<=0&&(e-this.lastFrom||n.startSide-this.last.startSide)<0)throw new Error("Ranges must be added sorted by `from` position and `startSide`");return i<0?!1:(this.from.length==250&&this.finishChunk(!0),this.chunkStart<0&&(this.chunkStart=e),this.from.push(e-this.chunkStart),this.to.push(t-this.chunkStart),this.last=n,this.lastFrom=e,this.lastTo=t,this.value.push(n),n.point&&(this.maxPoint=Math.max(this.maxPoint,t-e)),!0)}addChunk(e,t){if((e-this.lastTo||t.value[0].startSide-this.last.endSide)<0)return!1;this.from.length&&this.finishChunk(!0),this.setMaxPoint=Math.max(this.setMaxPoint,t.maxPoint),this.chunks.push(t),this.chunkPos.push(e);let n=t.value.length-1;return this.last=t.value[n],this.lastFrom=t.from[n]+e,this.lastTo=t.to[n]+e,!0}finish(){return this.finishInner(P.empty)}finishInner(e){if(this.from.length&&this.finishChunk(!1),this.chunks.length==0)return e;let t=P.create(this.chunkPos,this.chunks,this.nextLayer?this.nextLayer.finishInner(e):e,this.setMaxPoint);return this.from=null,t}}function We(l,e,t){let n=new Map;for(let s of l)for(let r=0;r=this.minPoint)break}}setRangeIndex(e){if(e==this.layer.chunk[this.chunkIndex].value.length){if(this.chunkIndex++,this.skip)for(;this.chunkIndex=n&&i.push(new Ue(r,t,n,s));return i.length==1?i[0]:new K(i)}get startSide(){return this.value?this.value.startSide:0}goto(e,t=-1e9){for(let n of this.heap)n.goto(e,t);for(let n=this.heap.length>>1;n>=0;n--)me(this.heap,n);return this.next(),this}forward(e,t){for(let n of this.heap)n.forward(e,t);for(let n=this.heap.length>>1;n>=0;n--)me(this.heap,n);(this.to-e||this.value.endSide-t)<0&&this.next()}next(){if(this.heap.length==0)this.from=this.to=1e9,this.value=null,this.rank=-1;else{let e=this.heap[0];this.from=e.from,this.to=e.to,this.value=e.value,this.rank=e.rank,e.value&&e.next(),me(this.heap,0)}}}function me(l,e){for(let t=l[e];;){let n=(e<<1)+1;if(n>=l.length)break;let i=l[n];if(n+1=0&&(i=l[n+1],n++),t.compare(i)<0)break;l[n]=t,l[e]=i,e=n}}class G{constructor(e,t,n){this.minPoint=n,this.active=[],this.activeTo=[],this.activeRank=[],this.minActive=-1,this.point=null,this.pointFrom=0,this.pointRank=0,this.to=-1e9,this.endSide=0,this.openStart=-1,this.cursor=K.from(e,t,n)}goto(e,t=-1e9){return this.cursor.goto(e,t),this.active.length=this.activeTo.length=this.activeRank.length=0,this.minActive=-1,this.to=e,this.endSide=t,this.openStart=-1,this.next(),this}forward(e,t){for(;this.minActive>-1&&(this.activeTo[this.minActive]-e||this.active[this.minActive].endSide-t)<0;)this.removeActive(this.minActive);this.cursor.forward(e,t)}removeActive(e){ee(this.active,e),ee(this.activeTo,e),ee(this.activeRank,e),this.minActive=Ge(this.active,this.activeTo)}addActive(e){let t=0,{value:n,to:i,rank:s}=this.cursor;for(;t0;)t++;te(this.active,t,n),te(this.activeTo,t,i),te(this.activeRank,t,s),e&&te(e,t,this.cursor.from),this.minActive=Ge(this.active,this.activeTo)}next(){let e=this.to,t=this.point;this.point=null;let n=this.openStart<0?[]:null;for(;;){let i=this.minActive;if(i>-1&&(this.activeTo[i]-this.cursor.from||this.active[i].endSide-this.cursor.startSide)<0){if(this.activeTo[i]>e){this.to=this.activeTo[i],this.endSide=this.active[i].endSide;break}this.removeActive(i),n&&ee(n,i)}else if(this.cursor.value)if(this.cursor.from>e){this.to=this.cursor.from,this.endSide=this.cursor.startSide;break}else{let s=this.cursor.value;if(!s.point)this.addActive(n),this.cursor.next();else if(t&&this.cursor.to==this.to&&this.cursor.from=0&&n[i]=0&&!(this.activeRank[n]e||this.activeTo[n]==e&&this.active[n].endSide>=this.point.endSide)&&t.push(this.active[n]);return t.reverse()}openEnd(e){let t=0;for(let n=this.activeTo.length-1;n>=0&&this.activeTo[n]>e;n--)t++;return t}}function je(l,e,t,n,i,s){l.goto(e),t.goto(n);let r=n+i,h=n,a=n-e;for(;;){let o=l.to+a-t.to||l.endSide-t.endSide,f=o<0?l.to+a:t.to,u=Math.min(f,r);if(l.point||t.point?l.point&&t.point&&(l.point==t.point||l.point.eq(t.point))&&we(l.activeForPoint(l.to),t.activeForPoint(t.to))||s.comparePoint(h,u,l.point,t.point):u>h&&!we(l.active,t.active)&&s.compareRange(h,u,l.active,t.active),f>r)break;h=f,o<=0&&l.next(),o>=0&&t.next()}}function we(l,e){if(l.length!=e.length)return!1;for(let t=0;t=e;n--)l[n+1]=l[n];l[e]=t}function Ge(l,e){let t=-1,n=1e9;for(let i=0;i=e)return i;if(i==l.length)break;s+=l.charCodeAt(i)==9?t-s%t:1,i=$(l,i)}return n===!0?-1:l.length}export{V as Annotation,De as AnnotationType,O as ChangeDesc,k as ChangeSet,M as CharCategory,H as Compartment,g as EditorSelection,w as EditorState,A as Facet,Se as Line,C as MapMode,Ye as Prec,D as Range,P as RangeSet,Z as RangeSetBuilder,j as RangeValue,T as SelectionRange,S as StateEffect,qe as StateEffectType,F as StateField,v as Text,I as Transaction,X as codePointAt,se as codePointSize,ht as combineConfig,ot as countColumn,$ as findClusterBreak,ft as findColumn,Qe as fromCodePoint}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/theme-one-dark.js b/Resources/Public/JavaScript/Contrib/@codemirror/theme-one-dark.js new file mode 100644 index 0000000..99fa652 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/theme-one-dark.js @@ -0,0 +1 @@ +import{EditorView as C}from"@codemirror/view";import{HighlightStyle as y,syntaxHighlighting as B}from"@codemirror/language";import{tags as o}from"@lezer/highlight";const d="#e5c07b",c="#e06c75",m="#56b6c2",g="#ffffff",r="#abb2bf",e="#7d8799",s="#61afef",b="#98c379",a="#d19a66",p="#c678dd",f="#21252b",l="#2c313a",n="#282c34",t="#353a42",u="#3E4451",i="#528bff",v={chalky:d,coral:c,cyan:m,invalid:g,ivory:r,stone:e,malibu:s,sage:b,whiskey:a,violet:p,darkBackground:f,highlightBackground:l,background:n,tooltipBackground:t,selection:u,cursor:i},k=C.theme({"&":{color:r,backgroundColor:n},".cm-content":{caretColor:i},".cm-cursor, .cm-dropCursor":{borderLeftColor:i},"&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":{backgroundColor:u},".cm-panels":{backgroundColor:f,color:r},".cm-panels.cm-panels-top":{borderBottom:"2px solid black"},".cm-panels.cm-panels-bottom":{borderTop:"2px solid black"},".cm-searchMatch":{backgroundColor:"#72a1ff59",outline:"1px solid #457dff"},".cm-searchMatch.cm-searchMatch-selected":{backgroundColor:"#6199ff2f"},".cm-activeLine":{backgroundColor:"#6699ff0b"},".cm-selectionMatch":{backgroundColor:"#aafe661a"},"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket":{backgroundColor:"#bad0f847"},".cm-gutters":{backgroundColor:n,color:e,border:"none"},".cm-activeLineGutter":{backgroundColor:l},".cm-foldPlaceholder":{backgroundColor:"transparent",border:"none",color:"#ddd"},".cm-tooltip":{border:"none",backgroundColor:t},".cm-tooltip .cm-tooltip-arrow:before":{borderTopColor:"transparent",borderBottomColor:"transparent"},".cm-tooltip .cm-tooltip-arrow:after":{borderTopColor:t,borderBottomColor:t},".cm-tooltip-autocomplete":{"& > ul > li[aria-selected]":{backgroundColor:l,color:r}}},{dark:!0}),h=y.define([{tag:o.keyword,color:p},{tag:[o.name,o.deleted,o.character,o.propertyName,o.macroName],color:c},{tag:[o.function(o.variableName),o.labelName],color:s},{tag:[o.color,o.constant(o.name),o.standard(o.name)],color:a},{tag:[o.definition(o.name),o.separator],color:r},{tag:[o.typeName,o.className,o.number,o.changed,o.annotation,o.modifier,o.self,o.namespace],color:d},{tag:[o.operator,o.operatorKeyword,o.url,o.escape,o.regexp,o.link,o.special(o.string)],color:m},{tag:[o.meta,o.comment],color:e},{tag:o.strong,fontWeight:"bold"},{tag:o.emphasis,fontStyle:"italic"},{tag:o.strikethrough,textDecoration:"line-through"},{tag:o.link,color:e,textDecoration:"underline"},{tag:o.heading,fontWeight:"bold",color:c},{tag:[o.atom,o.bool,o.special(o.variableName)],color:a},{tag:[o.processingInstruction,o.string,o.inserted],color:b},{tag:o.invalid,color:g}]),x=[k,B(h)];export{v as color,x as oneDark,h as oneDarkHighlightStyle,k as oneDarkTheme}; diff --git a/Resources/Public/JavaScript/Contrib/@codemirror/view.js b/Resources/Public/JavaScript/Contrib/@codemirror/view.js new file mode 100644 index 0000000..5d52d94 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@codemirror/view.js @@ -0,0 +1,5 @@ +import{Text as Y,RangeSet as V,MapMode as yt,RangeValue as xi,findClusterBreak as wt,EditorSelection as M,Facet as k,StateEffect as xt,ChangeSet as vi,findColumn as ke,CharCategory as Ln,EditorState as Lt,Annotation as Bn,Transaction as Pn,Prec as Ae,codePointAt as Si,codePointSize as Hn,combineConfig as De,StateField as Ci,RangeSetBuilder as Nn,countColumn as Mi}from"@codemirror/state";import{StyleModule as vt}from"style-mod";import{keyName as Vn,base as Fn,shift as Wn}from"w3c-keyname";function Bt(n){let t;return n.nodeType==11?t=n.getSelection?n:n.ownerDocument:t=n,t.getSelection()}function Oe(n,t){return t?n==t||n.contains(t.nodeType!=1?t.parentNode:t):!1}function zn(n){let t=n.activeElement;for(;t&&t.shadowRoot;)t=t.shadowRoot.activeElement;return t}function Zt(n,t){if(!t.anchorNode)return!1;try{return Oe(n,t.anchorNode)}catch{return!1}}function St(n){return n.nodeType==3?ut(n,0,n.nodeValue.length).getClientRects():n.nodeType==1?n.getClientRects():[]}function Pt(n,t,e,i){return e?ki(n,t,e,i,-1)||ki(n,t,e,i,1):!1}function dt(n){for(var t=0;;t++)if(n=n.previousSibling,!n)return t}function te(n){return n.nodeType==1&&/^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(n.nodeName)}function ki(n,t,e,i,s){for(;;){if(n==e&&t==i)return!0;if(t==(s<0?0:Z(n))){if(n.nodeName=="DIV")return!1;let o=n.parentNode;if(!o||o.nodeType!=1)return!1;t=dt(n)+(s<0?0:1),n=o}else if(n.nodeType==1){if(n=n.childNodes[t+(s<0?-1:0)],n.nodeType==1&&n.contentEditable=="false")return!1;t=s<0?Z(n):0}else return!1}}function Z(n){return n.nodeType==3?n.nodeValue.length:n.childNodes.length}function Ht(n,t){let e=t?n.left:n.right;return{left:e,right:e,top:n.top,bottom:n.bottom}}function In(n){let t=n.visualViewport;return t?{left:0,right:t.width,top:0,bottom:t.height}:{left:0,right:n.innerWidth,top:0,bottom:n.innerHeight}}function Ai(n,t){let e=t.width/n.offsetWidth,i=t.height/n.offsetHeight;return(e>.995&&e<1.005||!isFinite(e)||Math.abs(t.width-n.offsetWidth)<1)&&(e=1),(i>.995&&i<1.005||!isFinite(i)||Math.abs(t.height-n.offsetHeight)<1)&&(i=1),{scaleX:e,scaleY:i}}function Kn(n,t,e,i,s,o,r,l){let h=n.ownerDocument,c=h.defaultView||window;for(let a=n,f=!1;a&&!f;)if(a.nodeType==1){let d,u=a==h.body,g=1,p=1;if(u)d=In(c);else{if(/^(fixed|sticky)$/.test(getComputedStyle(a).position)&&(f=!0),a.scrollHeight<=a.clientHeight&&a.scrollWidth<=a.clientWidth){a=a.assignedSlot||a.parentNode;continue}let w=a.getBoundingClientRect();({scaleX:g,scaleY:p}=Ai(a,w)),d={left:w.left,right:w.left+a.clientWidth*g,top:w.top,bottom:w.top+a.clientHeight*p}}let b=0,m=0;if(s=="nearest")t.top0&&t.bottom>d.bottom+m&&(m=t.bottom-d.bottom+m+r)):t.bottom>d.bottom&&(m=t.bottom-d.bottom+r,e<0&&t.top-m0&&t.right>d.right+b&&(b=t.right-d.right+b+o)):t.right>d.right&&(b=t.right-d.right+o,e<0&&t.lefts.clientHeight&&(i=s),!e&&s.scrollWidth>s.clientWidth&&(e=s),s=s.assignedSlot||s.parentNode;else if(s.nodeType==11)s=s.host;else break;return{x:e,y:i}}class jn{constructor(){this.anchorNode=null,this.anchorOffset=0,this.focusNode=null,this.focusOffset=0}eq(t){return this.anchorNode==t.anchorNode&&this.anchorOffset==t.anchorOffset&&this.focusNode==t.focusNode&&this.focusOffset==t.focusOffset}setRange(t){let{anchorNode:e,focusNode:i}=t;this.set(e,Math.min(t.anchorOffset,e?Z(e):0),i,Math.min(t.focusOffset,i?Z(i):0))}set(t,e,i,s){this.anchorNode=t,this.anchorOffset=e,this.focusNode=i,this.focusOffset=s}}let Ct=null;function Di(n){if(n.setActive)return n.setActive();if(Ct)return n.focus(Ct);let t=[];for(let e=n;e&&(t.push(e,e.scrollTop,e.scrollLeft),e!=e.ownerDocument);e=e.parentNode);if(n.focus(Ct==null?{get preventScroll(){return Ct={preventScroll:!0},!0}}:void 0),!Ct){Ct=!1;for(let e=0;eMath.max(1,n.scrollHeight-n.clientHeight-4)}function Ei(n,t){for(let e=n,i=t;;){if(e.nodeType==3&&i>0)return{node:e,offset:i};if(e.nodeType==1&&i>0){if(e.contentEditable=="false")return null;e=e.childNodes[i-1],i=Z(e)}else if(e.parentNode&&!te(e))i=dt(e),e=e.parentNode;else return null}}function Li(n,t){for(let e=n,i=t;;){if(e.nodeType==3&&ie)return f.domBoundsAround(t,e,c);if(d>=t&&s==-1&&(s=h,o=c),c>e&&f.dom.parentNode==this.dom){r=h,l=a;break}a=d,c=d+f.breakAfter}return{from:o,to:l<0?i+this.length:l,startDOM:(s?this.children[s-1].dom.nextSibling:null)||this.dom.firstChild,endDOM:r=0?this.children[r].dom:null}}markDirty(t=!1){this.flags|=2,this.markParentsDirty(t)}markParentsDirty(t){for(let e=this.parent;e;e=e.parent){if(t&&(e.flags|=2),e.flags&1)return;e.flags|=1,t=!1}}setParent(t){this.parent!=t&&(this.parent=t,this.flags&7&&this.markParentsDirty(!0))}setDOM(t){this.dom!=t&&(this.dom&&(this.dom.cmView=null),this.dom=t,t.cmView=this)}get rootView(){for(let t=this;;){let e=t.parent;if(!e)return t;t=e}}replaceChildren(t,e,i=Te){this.markDirty();for(let s=t;sthis.pos||t==this.pos&&(e>0||this.i==0||this.children[this.i-1].breakAfter))return this.off=t-this.pos,this;let i=this.children[--this.i];this.pos-=i.length+i.breakAfter}}}function Hi(n,t,e,i,s,o,r,l,h){let{children:c}=n,a=c.length?c[t]:null,f=o.length?o[o.length-1]:null,d=f?f.breakAfter:r;if(!(t==i&&a&&!r&&!d&&o.length<2&&a.merge(e,s,o.length?f:null,e==0,l,h))){if(i0&&(!r&&o.length&&a.merge(e,a.length,o[0],!1,l,0)?a.breakAfter=o.shift().breakAfter:(e2);var y={mac:Ii||/Mac/.test(I.platform),windows:/Win/.test(I.platform),linux:/Linux|X11/.test(I.platform),ie:ee,ie_version:Vi?Re.documentMode||6:Le?+Le[1]:Ee?+Ee[1]:0,gecko:Fi,gecko_version:Fi?+(/Firefox\/(\d+)/.exec(I.userAgent)||[0,0])[1]:0,chrome:!!Be,chrome_version:Be?+Be[1]:0,ios:Ii,android:/Android\b/.test(I.userAgent),webkit:Wi,safari:zi,webkit_version:Wi?+(/\bAppleWebKit\/(\d+)/.exec(I.userAgent)||[0,0])[1]:0,tabSize:Re.documentElement.style.tabSize!=null?"tab-size":"-moz-tab-size"};const Xn=256;class $ extends O{constructor(t){super(),this.text=t}get length(){return this.text.length}createDOM(t){this.setDOM(t||document.createTextNode(this.text))}sync(t,e){this.dom||this.createDOM(),this.dom.nodeValue!=this.text&&(e&&e.node==this.dom&&(e.written=!0),this.dom.nodeValue=this.text)}reuseDOM(t){t.nodeType==3&&this.createDOM(t)}merge(t,e,i){return this.flags&8||i&&(!(i instanceof $)||this.length-(e-t)+i.length>Xn||i.flags&8)?!1:(this.text=this.text.slice(0,t)+(i?i.text:"")+this.text.slice(e),this.markDirty(),!0)}split(t){let e=new $(this.text.slice(t));return this.text=this.text.slice(0,t),this.markDirty(),e.flags|=this.flags&8,e}localPosFromDOM(t,e){return t==this.dom?e:e?this.text.length:0}domAtPos(t){return new W(this.dom,t)}domBoundsAround(t,e,i){return{from:i,to:i+this.length,startDOM:this.dom,endDOM:this.dom.nextSibling}}coordsAt(t,e){return Gn(this.dom,t,e)}}class it extends O{constructor(t,e=[],i=0){super(),this.mark=t,this.children=e,this.length=i;for(let s of e)s.setParent(this)}setAttrs(t){if(Ti(t),this.mark.class&&(t.className=this.mark.class),this.mark.attrs)for(let e in this.mark.attrs)t.setAttribute(e,this.mark.attrs[e]);return t}canReuseDOM(t){return super.canReuseDOM(t)&&!((this.flags|t.flags)&8)}reuseDOM(t){t.nodeName==this.mark.tagName.toUpperCase()&&(this.setDOM(t),this.flags|=6)}sync(t,e){this.dom?this.flags&4&&this.setAttrs(this.dom):this.setDOM(this.setAttrs(document.createElement(this.mark.tagName))),super.sync(t,e)}merge(t,e,i,s,o,r){return i&&(!(i instanceof it&&i.mark.eq(this.mark))||t&&o<=0||et&&e.push(i=t&&(s=o),i=h,o++}let r=this.length-t;return this.length=t,s>-1&&(this.children.length=s,this.markDirty()),new it(this.mark,e,r)}domAtPos(t){return Ki(this,t)}coordsAt(t,e){return ji(this,t,e)}}function Gn(n,t,e){let i=n.nodeValue.length;t>i&&(t=i);let s=t,o=t,r=0;t==0&&e<0||t==i&&e>=0?y.chrome||y.gecko||(t?(s--,r=1):o=0)?0:l.length-1];return y.safari&&!r&&h.width==0&&(h=Array.prototype.find.call(l,c=>c.width)||h),r?Ht(h,r<0):h||null}class at extends O{static create(t,e,i){return new at(t,e,i)}constructor(t,e,i){super(),this.widget=t,this.length=e,this.side=i,this.prevWidget=null}split(t){let e=at.create(this.widget,this.length-t,this.side);return this.length-=t,e}sync(t){(!this.dom||!this.widget.updateDOM(this.dom,t))&&(this.dom&&this.prevWidget&&this.prevWidget.destroy(this.dom),this.prevWidget=null,this.setDOM(this.widget.toDOM(t)),this.widget.editable||(this.dom.contentEditable="false"))}getSide(){return this.side}merge(t,e,i,s,o,r){return i&&(!(i instanceof at)||!this.widget.compare(i.widget)||t>0&&o<=0||e0)?W.before(this.dom):W.after(this.dom,t==this.length)}domBoundsAround(){return null}coordsAt(t,e){let i=this.widget.coordsAt(this.dom,t,e);if(i)return i;let s=this.dom.getClientRects(),o=null;if(!s.length)return null;let r=this.side?this.side<0:t>0;for(let l=r?s.length-1:0;o=s[l],!(t>0?l==0:l==s.length-1||o.top0?W.before(this.dom):W.after(this.dom)}localPosFromDOM(){return 0}domBoundsAround(){return null}coordsAt(t){return this.dom.getBoundingClientRect()}get overrideDOMText(){return Y.empty}get isHidden(){return!0}}$.prototype.children=at.prototype.children=Tt.prototype.children=Te;function Ki(n,t){let e=n.dom,{children:i}=n,s=0;for(let o=0;so&&t0;o--){let r=i[o-1];if(r.dom.parentNode==e)return r.domAtPos(r.length)}for(let o=s;o0&&t instanceof it&&s.length&&(i=s[s.length-1])instanceof it&&i.mark.eq(t.mark)?qi(i,t.children[0],e-1):(s.push(t),t.setParent(n)),n.length+=t.length}function ji(n,t,e){let i=null,s=-1,o=null,r=-1;function l(c,a){for(let f=0,d=0;f=a&&(u.children.length?l(u,a-d):(!o||o.isHidden&&e>0)&&(g>a||d==g&&u.getSide()>0)?(o=u,r=a-d):(d-1?1:0)!=s.length-(e&&s.indexOf(e)>-1?1:0))return!1;for(let o of i)if(o!=e&&(s.indexOf(o)==-1||n[o]!==t[o]))return!1;return!0}function He(n,t,e){let i=!1;if(t)for(let s in t)e&&s in e||(i=!0,s=="style"?n.style.cssText="":n.removeAttribute(s));if(e)for(let s in e)t&&t[s]==e[s]||(i=!0,s=="style"?n.style.cssText=e[s]:n.setAttribute(s,e[s]));return i}function Un(n){let t=Object.create(null);for(let e=0;e0?3e8:-4e8:e>0?1e8:-1e8,new ht(t,e,e,i,t.widget||null,!1)}static replace(t){let e=!!t.block,i,s;if(t.isBlockGap)i=-5e8,s=4e8;else{let{start:o,end:r}=_i(t,e);i=(o?e?-3e8:-1:5e8)-1,s=(r?e?2e8:1:-6e8)+1}return new ht(t,i,s,e,t.widget||null,!0)}static line(t){return new Xt(t)}static set(t,e=!1){return V.of(t,e)}hasHeight(){return this.widget?this.widget.estimatedHeight>-1:!1}}R.none=V.empty;class _t extends R{constructor(t){let{start:e,end:i}=_i(t);super(e?-1:5e8,i?1:-6e8,null,t),this.tagName=t.tagName||"span",this.class=t.class||"",this.attrs=t.attributes||null}eq(t){var e,i;return this==t||t instanceof _t&&this.tagName==t.tagName&&(this.class||((e=this.attrs)===null||e===void 0?void 0:e.class))==(t.class||((i=t.attrs)===null||i===void 0?void 0:i.class))&&ie(this.attrs,t.attrs,"class")}range(t,e=t){if(t>=e)throw new RangeError("Mark decorations may not be empty");return super.range(t,e)}}_t.prototype.point=!1;class Xt extends R{constructor(t){super(-2e8,-2e8,null,t)}eq(t){return t instanceof Xt&&this.spec.class==t.spec.class&&ie(this.spec.attributes,t.spec.attributes)}range(t,e=t){if(e!=t)throw new RangeError("Line decoration ranges must be zero-length");return super.range(t,e)}}Xt.prototype.mapMode=yt.TrackBefore,Xt.prototype.point=!0;class ht extends R{constructor(t,e,i,s,o,r){super(e,i,o,t),this.block=s,this.isReplace=r,this.mapMode=s?e<=0?yt.TrackBefore:yt.TrackAfter:yt.TrackDel}get type(){return this.startSide!=this.endSide?F.WidgetRange:this.startSide<=0?F.WidgetBefore:F.WidgetAfter}get heightRelevant(){return this.block||!!this.widget&&(this.widget.estimatedHeight>=5||this.widget.lineBreaks>0)}eq(t){return t instanceof ht&&Qn(this.widget,t.widget)&&this.block==t.block&&this.startSide==t.startSide&&this.endSide==t.endSide}range(t,e=t){if(this.isReplace&&(t>e||t==e&&this.startSide>0&&this.endSide<=0))throw new RangeError("Invalid range for replacement decoration");if(!this.isReplace&&e!=t)throw new RangeError("Widget decorations can only have zero-length ranges");return super.range(t,e)}}ht.prototype.point=!0;function _i(n,t=!1){let{inclusiveStart:e,inclusiveEnd:i}=n;return e==null&&(e=n.inclusive),i==null&&(i=n.inclusive),{start:e??t,end:i??t}}function Qn(n,t){return n==t||!!(n&&t&&n.compare(t))}function Ne(n,t,e,i=0){let s=e.length-1;s>=0&&e[s]+i>=n?e[s]=Math.max(e[s],t):e.push(n,t)}class B extends O{constructor(){super(...arguments),this.children=[],this.length=0,this.prevAttrs=void 0,this.attrs=null,this.breakAfter=0}merge(t,e,i,s,o,r){if(i){if(!(i instanceof B))return!1;this.dom||i.transferDOM(this)}return s&&this.setDeco(i?i.attrs:null),Ni(this,t,e,i?i.children.slice():[],o,r),!0}split(t){let e=new B;if(e.breakAfter=this.breakAfter,this.length==0)return e;let{i,off:s}=this.childPos(t);s&&(e.append(this.children[i].split(s),0),this.children[i].merge(s,this.children[i].length,null,!1,0,0),i++);for(let o=i;o0&&this.children[i-1].length==0;)this.children[--i].destroy();return this.children.length=i,this.markDirty(),this.length=t,e}transferDOM(t){this.dom&&(this.markDirty(),t.setDOM(this.dom),t.prevAttrs=this.prevAttrs===void 0?this.attrs:this.prevAttrs,this.prevAttrs=void 0,this.dom=null)}setDeco(t){ie(this.attrs,t)||(this.dom&&(this.prevAttrs=this.attrs,this.markDirty()),this.attrs=t)}append(t,e){qi(this,t,e)}addLineDeco(t){let e=t.spec.attributes,i=t.spec.class;e&&(this.attrs=Pe(e,this.attrs||{})),i&&(this.attrs=Pe({class:i},this.attrs||{}))}domAtPos(t){return Ki(this,t)}reuseDOM(t){t.nodeName=="DIV"&&(this.setDOM(t),this.flags|=6)}sync(t,e){var i;this.dom?this.flags&4&&(Ti(this.dom),this.dom.className="cm-line",this.prevAttrs=this.attrs?null:void 0):(this.setDOM(document.createElement("div")),this.dom.className="cm-line",this.prevAttrs=this.attrs?null:void 0),this.prevAttrs!==void 0&&(He(this.dom,this.prevAttrs,this.attrs),this.dom.classList.add("cm-line"),this.prevAttrs=void 0),super.sync(t,e);let s=this.dom.lastChild;for(;s&&O.get(s)instanceof it;)s=s.lastChild;if(!s||!this.length||s.nodeName!="BR"&&((i=O.get(s))===null||i===void 0?void 0:i.isEditable)==!1&&(!y.ios||!this.children.some(o=>o instanceof $))){let o=document.createElement("BR");o.cmIgnore=!0,this.dom.appendChild(o)}}measureTextSize(){if(this.children.length==0||this.length>20)return null;let t=0,e;for(let i of this.children){if(!(i instanceof $)||/[^ -~]/.test(i.text))return null;let s=St(i.dom);if(s.length!=1)return null;t+=s[0].width,e=s[0].height}return t?{lineHeight:this.dom.getBoundingClientRect().height,charWidth:t/this.length,textHeight:e}:null}coordsAt(t,e){let i=ji(this,t,e);if(!this.children.length&&i&&this.parent){let{heightOracle:s}=this.parent.view.viewState,o=i.bottom-i.top;if(Math.abs(o-s.lineHeight)<2&&s.textHeight=e){if(o instanceof B)return o;if(r>e)break}s=r+o.breakAfter}return null}}class st extends O{constructor(t,e,i){super(),this.widget=t,this.length=e,this.deco=i,this.breakAfter=0,this.prevWidget=null}merge(t,e,i,s,o,r){return i&&(!(i instanceof st)||!this.widget.compare(i.widget)||t>0&&o<=0||e0}}class Ve extends gt{constructor(t){super(),this.height=t}toDOM(){let t=document.createElement("div");return t.className="cm-gap",this.updateDOM(t),t}eq(t){return t.height==this.height}updateDOM(t){return t.style.height=this.height+"px",!0}get editable(){return!0}get estimatedHeight(){return this.height}ignoreEvent(){return!1}}class Gt{constructor(t,e,i,s){this.doc=t,this.pos=e,this.end=i,this.disallowBlockEffectsFor=s,this.content=[],this.curLine=null,this.breakAtStart=0,this.pendingBuffer=0,this.bufferMarks=[],this.atCursorPos=!0,this.openStart=-1,this.openEnd=-1,this.text="",this.textOff=0,this.cursor=t.iter(),this.skip=e}posCovered(){if(this.content.length==0)return!this.breakAtStart&&this.doc.lineAt(this.pos).from!=this.pos;let t=this.content[this.content.length-1];return!(t.breakAfter||t instanceof st&&t.deco.endSide<0)}getLine(){return this.curLine||(this.content.push(this.curLine=new B),this.atCursorPos=!0),this.curLine}flushBuffer(t=this.bufferMarks){this.pendingBuffer&&(this.curLine.append(se(new Tt(-1),t),t.length),this.pendingBuffer=0)}addBlockWidget(t){this.flushBuffer(),this.curLine=null,this.content.push(t)}finish(t){this.pendingBuffer&&t<=this.bufferMarks.length?this.flushBuffer():this.pendingBuffer=0,!this.posCovered()&&!(t&&this.content.length&&this.content[this.content.length-1]instanceof st)&&this.getLine()}buildText(t,e,i){for(;t>0;){if(this.textOff==this.text.length){let{value:o,lineBreak:r,done:l}=this.cursor.next(this.skip);if(this.skip=0,l)throw new Error("Ran out of text content when drawing inline views");if(r){this.posCovered()||this.getLine(),this.content.length?this.content[this.content.length-1].breakAfter=1:this.breakAtStart=1,this.flushBuffer(),this.curLine=null,this.atCursorPos=!0,t--;continue}else this.text=o,this.textOff=0}let s=Math.min(this.text.length-this.textOff,t,512);this.flushBuffer(e.slice(e.length-i)),this.getLine().append(se(new $(this.text.slice(this.textOff,this.textOff+s)),e),i),this.atCursorPos=!0,this.textOff+=s,t-=s,i=0}}span(t,e,i,s){this.buildText(e-t,i,s),this.pos=e,this.openStart<0&&(this.openStart=s)}point(t,e,i,s,o,r){if(this.disallowBlockEffectsFor[r]&&i instanceof ht){if(i.block)throw new RangeError("Block decorations may not be specified via plugins");if(e>this.doc.lineAt(this.pos).to)throw new RangeError("Decorations that replace line breaks may not be specified via plugins")}let l=e-t;if(i instanceof ht)if(i.block)i.startSide>0&&!this.posCovered()&&this.getLine(),this.addBlockWidget(new st(i.widget||kt.block,l,i));else{let h=at.create(i.widget||kt.inline,l,l?0:i.startSide),c=this.atCursorPos&&!h.isEditable&&o<=s.length&&(t0),a=!h.isEditable&&(ts.length||i.startSide<=0),f=this.getLine();this.pendingBuffer==2&&!c&&!h.isEditable&&(this.pendingBuffer=0),this.flushBuffer(s),c&&(f.append(se(new Tt(1),s),o),o=s.length+Math.max(0,o-s.length)),f.append(se(h,s),o),this.atCursorPos=a,this.pendingBuffer=a?ts.length?1:2:0,this.pendingBuffer&&(this.bufferMarks=s.slice())}else this.doc.lineAt(this.pos).from==this.pos&&this.getLine().addLineDeco(i);l&&(this.textOff+l<=this.text.length?this.textOff+=l:(this.skip+=l-(this.text.length-this.textOff),this.text="",this.textOff=0),this.pos=e),this.openStart<0&&(this.openStart=o)}static build(t,e,i,s,o){let r=new Gt(t,e,i,o);return r.openEnd=V.spans(s,e,i,r),r.openStart<0&&(r.openStart=r.openEnd),r.finish(r.openEnd),r}}function se(n,t){for(let e of t)n=new it(e,[n],n.length);return n}class kt extends gt{constructor(t){super(),this.tag=t}eq(t){return t.tag==this.tag}toDOM(){return document.createElement(this.tag)}updateDOM(t){return t.nodeName.toLowerCase()==this.tag}get isHidden(){return!0}}kt.inline=new kt("span"),kt.block=new kt("div");var E=function(n){return n[n.LTR=0]="LTR",n[n.RTL=1]="RTL",n}(E||(E={}));const pt=E.LTR,Fe=E.RTL;function Xi(n){let t=[];for(let e=0;e=e){if(l.level==i)return r;(o<0||(s!=0?s<0?l.frome:t[o].level>l.level))&&(o=r)}}if(o<0)throw new RangeError("Index out of range");return o}}function $i(n,t){if(n.length!=t.length)return!1;for(let e=0;e=0;p-=3)if(U[p+1]==-u){let b=U[p+2],m=b&2?s:b&4?b&1?o:s:0;m&&(D[f]=D[U[p]]=m),l=p;break}}else{if(U.length==189)break;U[l++]=f,U[l++]=d,U[l++]=h}else if((g=D[f])==2||g==1){let p=g==s;h=p?0:1;for(let b=l-3;b>=0;b-=3){let m=U[b+2];if(m&2)break;if(p)U[b+2]|=2;else{if(m&4)break;U[b+2]|=4}}}}}function so(n,t,e,i){for(let s=0,o=i;s<=e.length;s++){let r=s?e[s-1].to:n,l=sh;)g==b&&(g=e[--p].from,b=p?e[p-1].to:n),D[--g]=u;h=a}else o=c,h++}}}function ze(n,t,e,i,s,o,r){let l=i%2?2:1;if(i%2==s%2)for(let h=t,c=0;hh&&r.push(new tt(h,p.from,u));let b=p.direction==pt!=!(u%2);Ie(n,b?i+1:i,s,p.inner,p.from,p.to,r),h=p.to}g=p.to}else{if(g==e||(a?D[g]!=l:D[g]==l))break;g++}d?ze(n,h,g,i+1,s,d,r):ht;){let a=!0,f=!1;if(!c||h>o[c-1].to){let p=D[h-1];p!=l&&(a=!1,f=p==16)}let d=!a&&l==1?[]:null,u=a?i:i+1,g=h;t:for(;;)if(c&&g==o[c-1].to){if(f)break t;let p=o[--c];if(!a)for(let b=p.from,m=c;;){if(b==t)break t;if(m&&o[m-1].to==b)b=o[--m].from;else{if(D[b-1]==l)break t;break}}if(d)d.push(p);else{p.toD.length;)D[D.length]=256;let i=[],s=t==pt?0:1;return Ie(n,s,s,e,0,n.length,i),i}function Qi(n){return[new tt(0,n,0)]}let Ji="";function Zi(n,t,e,i,s){var o;let r=i.head-n.from,l=tt.find(t,r,(o=i.bidiLevel)!==null&&o!==void 0?o:-1,i.assoc),h=t[l],c=h.side(s,e);if(r==c){let d=l+=s?1:-1;if(d<0||d>=t.length)return null;h=t[l=d],r=h.side(!s,e),c=h.side(s,e)}let a=wt(n.text,r,h.forward(s,e));(ah.to)&&(a=c),Ji=n.text.slice(Math.min(r,a),Math.max(r,a));let f=l==(s?t.length-1:0)?null:t[l+(s?1:-1)];return f&&a==c&&f.level+(s?0:1)n.some(t=>t)}),ls=k.define({combine:n=>n.some(t=>t)}),hs=k.define();class Rt{constructor(t,e="nearest",i="nearest",s=5,o=5,r=!1){this.range=t,this.y=e,this.x=i,this.yMargin=s,this.xMargin=o,this.isSnapshot=r}map(t){return t.empty?this:new Rt(this.range.map(t),this.y,this.x,this.yMargin,this.xMargin,this.isSnapshot)}clip(t){return this.range.to<=t.doc.length?this:new Rt(M.cursor(t.doc.length),this.y,this.x,this.yMargin,this.xMargin,this.isSnapshot)}}const ne=xt.define({map:(n,t)=>n.map(t)}),as=xt.define();function _(n,t,e){let i=n.facet(ss);i.length?i[0](t):window.onerror?window.onerror(String(t),e,void 0,void 0,t):e?console.error(e+":",t):console.error(t)}const rt=k.define({combine:n=>n.length?n[0]:!0});let oo=0;const Nt=k.define();class P{constructor(t,e,i,s,o){this.id=t,this.create=e,this.domEventHandlers=i,this.domEventObservers=s,this.extension=o(this)}static define(t,e){const{eventHandlers:i,eventObservers:s,provide:o,decorations:r}=e||{};return new P(oo++,t,i,s,l=>{let h=[Nt.of(l)];return r&&h.push(Vt.of(c=>{let a=c.plugin(l);return a?r(a):R.none})),o&&h.push(o(l)),h})}static fromClass(t,e){return P.define(i=>new t(i),e)}}class qe{constructor(t){this.spec=t,this.mustUpdate=null,this.value=null}update(t){if(this.value){if(this.mustUpdate){let e=this.mustUpdate;if(this.mustUpdate=null,this.value.update)try{this.value.update(e)}catch(i){if(_(e.state,i,"CodeMirror plugin crashed"),this.value.destroy)try{this.value.destroy()}catch{}this.deactivate()}}}else if(this.spec)try{this.value=this.spec.create(t)}catch(e){_(t.state,e,"CodeMirror plugin crashed"),this.deactivate()}return this}destroy(t){var e;if(!((e=this.value)===null||e===void 0)&&e.destroy)try{this.value.destroy()}catch(i){_(t.state,i,"CodeMirror plugin crashed")}}deactivate(){this.spec=this.value=null}}const cs=k.define(),oe=k.define(),Vt=k.define(),fs=k.define(),je=k.define(),ds=k.define();function us(n,t){let e=n.state.facet(ds);if(!e.length)return e;let i=e.map(o=>o instanceof Function?o(n):o),s=[];return V.spans(i,t.from,t.to,{point(){},span(o,r,l,h){let c=o-t.from,a=r-t.from,f=s;for(let d=l.length-1;d>=0;d--,h--){let u=l[d].spec.bidiIsolate,g;if(u==null&&(u=no(t.text,c,a)),h>0&&f.length&&(g=f[f.length-1]).to==c&&g.direction==u)g.to=a,f=g.inner;else{let p={from:c,to:a,direction:u,inner:[]};f.push(p),f=p.inner}}}}),s}const gs=k.define();function ps(n){let t=0,e=0,i=0,s=0;for(let o of n.state.facet(gs)){let r=o(n);r&&(r.left!=null&&(t=Math.max(t,r.left)),r.right!=null&&(e=Math.max(e,r.right)),r.top!=null&&(i=Math.max(i,r.top)),r.bottom!=null&&(s=Math.max(s,r.bottom)))}return{left:t,right:e,top:i,bottom:s}}const Ft=k.define();class q{constructor(t,e,i,s){this.fromA=t,this.toA=e,this.fromB=i,this.toB=s}join(t){return new q(Math.min(this.fromA,t.fromA),Math.max(this.toA,t.toA),Math.min(this.fromB,t.fromB),Math.max(this.toB,t.toB))}addToSet(t){let e=t.length,i=this;for(;e>0;e--){let s=t[e-1];if(!(s.fromA>i.toA)){if(s.toAa)break;o+=2}if(!h)return i;new q(h.fromA,h.toA,h.fromB,h.toB).addToSet(i),r=h.toA,l=h.toB}}}class $t{constructor(t,e,i){this.view=t,this.state=e,this.transactions=i,this.flags=0,this.startState=t.state,this.changes=vi.empty(this.startState.doc.length);for(let o of i)this.changes=this.changes.compose(o.changes);let s=[];this.changes.iterChangedRanges((o,r,l,h)=>s.push(new q(o,r,l,h))),this.changedRanges=s}static create(t,e,i){return new $t(t,e,i)}get viewportChanged(){return(this.flags&4)>0}get heightChanged(){return(this.flags&2)>0}get geometryChanged(){return this.docChanged||(this.flags&10)>0}get focusChanged(){return(this.flags&1)>0}get docChanged(){return!this.changes.empty}get selectionSet(){return this.transactions.some(t=>t.selection)}get empty(){return this.flags==0&&this.transactions.length==0}}class ms extends O{get length(){return this.view.state.doc.length}constructor(t){super(),this.view=t,this.decorations=[],this.dynamicDecorationMap=[!1],this.domChanged=null,this.hasComposition=null,this.markedForComposition=new Set,this.editContextFormatting=R.none,this.lastCompositionAfterCursor=!1,this.minWidth=0,this.minWidthFrom=0,this.minWidthTo=0,this.impreciseAnchor=null,this.impreciseHead=null,this.forceSelection=!1,this.lastUpdate=Date.now(),this.setDOM(t.contentDOM),this.children=[new B],this.children[0].setParent(this),this.updateDeco(),this.updateInner([new q(0,0,0,t.state.doc.length)],0,null)}update(t){var e;let i=t.changedRanges;this.minWidth>0&&i.length&&(i.every(({fromA:c,toA:a})=>athis.minWidthTo)?(this.minWidthFrom=t.changes.mapPos(this.minWidthFrom,1),this.minWidthTo=t.changes.mapPos(this.minWidthTo,1)):this.minWidth=this.minWidthFrom=this.minWidthTo=0),this.updateEditContextFormatting(t);let s=-1;this.view.inputState.composing>=0&&!this.view.observer.editContext&&(!((e=this.domChanged)===null||e===void 0)&&e.newSel?s=this.domChanged.newSel.head:!uo(t.changes,this.hasComposition)&&!t.selectionSet&&(s=t.state.selection.main.head));let o=s>-1?lo(this.view,t.changes,s):null;if(this.domChanged=null,this.hasComposition){this.markedForComposition.clear();let{from:c,to:a}=this.hasComposition;i=new q(c,a,t.changes.mapPos(c,-1),t.changes.mapPos(a,1)).addToSet(i.slice())}this.hasComposition=o?{from:o.range.fromB,to:o.range.toB}:null,(y.ie||y.chrome)&&!o&&t&&t.state.doc.lines!=t.startState.doc.lines&&(this.forceSelection=!0);let r=this.decorations,l=this.updateDeco(),h=co(r,l,t.changes);return i=q.extendWithRanges(i,h),!(this.flags&7)&&i.length==0?!1:(this.updateInner(i,t.startState.doc.length,o),t.transactions.length&&(this.lastUpdate=Date.now()),!0)}updateInner(t,e,i){this.view.viewState.mustMeasureContent=!0,this.updateChildren(t,e,i);let{observer:s}=this.view;s.ignore(()=>{this.dom.style.height=this.view.viewState.contentHeight/this.view.scaleY+"px",this.dom.style.flexBasis=this.minWidth?this.minWidth+"px":"";let r=y.chrome||y.ios?{node:s.selectionRange.focusNode,written:!1}:void 0;this.sync(this.view,r),this.flags&=-8,r&&(r.written||s.selectionRange.focusNode!=r.node)&&(this.forceSelection=!0),this.dom.style.height=""}),this.markedForComposition.forEach(r=>r.flags&=-9);let o=[];if(this.view.viewport.from||this.view.viewport.to=0?s[r]:null;if(!l)break;let{fromA:h,toA:c,fromB:a,toB:f}=l,d,u,g,p;if(i&&i.range.fromBa){let x=Gt.build(this.view.state.doc,a,i.range.fromB,this.decorations,this.dynamicDecorationMap),S=Gt.build(this.view.state.doc,i.range.toB,f,this.decorations,this.dynamicDecorationMap);u=x.breakAtStart,g=x.openStart,p=S.openEnd;let C=this.compositionView(i);S.breakAtStart?C.breakAfter=1:S.content.length&&C.merge(C.length,C.length,S.content[0],!1,S.openStart,0)&&(C.breakAfter=S.content[0].breakAfter,S.content.shift()),x.content.length&&C.merge(0,0,x.content[x.content.length-1],!0,0,x.openEnd)&&x.content.pop(),d=x.content.concat(C).concat(S.content)}else({content:d,breakAtStart:u,openStart:g,openEnd:p}=Gt.build(this.view.state.doc,a,f,this.decorations,this.dynamicDecorationMap));let{i:b,off:m}=o.findPos(c,1),{i:w,off:v}=o.findPos(h,-1);Hi(this,w,v,b,m,d,u,g,p)}i&&this.fixCompositionDOM(i)}updateEditContextFormatting(t){this.editContextFormatting=this.editContextFormatting.map(t.changes);for(let e of t.transactions)for(let i of e.effects)i.is(as)&&(this.editContextFormatting=i.value)}compositionView(t){let e=new $(t.text.nodeValue);e.flags|=8;for(let{deco:s}of t.marks)e=new it(s,[e],e.length);let i=new B;return i.append(e,0),i}fixCompositionDOM(t){let e=(o,r)=>{r.flags|=8|(r.children.some(h=>h.flags&7)?1:0),this.markedForComposition.add(r);let l=O.get(o);l&&l!=r&&(l.dom=null),r.setDOM(o)},i=this.childPos(t.range.fromB,1),s=this.children[i.i];e(t.line,s);for(let o=t.marks.length-1;o>=-1;o--)i=s.childPos(i.off,1),s=s.children[i.i],e(o>=0?t.marks[o].node:t.text,s)}updateSelection(t=!1,e=!1){(t||!this.view.observer.selectionRange.focusNode)&&this.view.observer.readSelectionRange();let i=this.view.root.activeElement,s=i==this.dom,o=!s&&Zt(this.dom,this.view.observer.selectionRange)&&!(i&&this.dom.contains(i));if(!(s||e||o))return;let r=this.forceSelection;this.forceSelection=!1;let l=this.view.state.selection.main,h=this.moveToLine(this.domAtPos(l.anchor)),c=l.empty?h:this.moveToLine(this.domAtPos(l.head));if(y.gecko&&l.empty&&!this.hasComposition&&ro(h)){let f=document.createTextNode("");this.view.observer.ignore(()=>h.node.insertBefore(f,h.node.childNodes[h.offset]||null)),h=c=new W(f,0),r=!0}let a=this.view.observer.selectionRange;(r||!a.focusNode||(!Pt(h.node,h.offset,a.anchorNode,a.anchorOffset)||!Pt(c.node,c.offset,a.focusNode,a.focusOffset))&&!this.suppressWidgetCursorChange(a,l))&&(this.view.observer.ignore(()=>{y.android&&y.chrome&&this.dom.contains(a.focusNode)&&fo(a.focusNode,this.dom)&&(this.dom.blur(),this.dom.focus({preventScroll:!0}));let f=Bt(this.view.root);if(f)if(l.empty){if(y.gecko){let d=ho(h.node,h.offset);if(d&&d!=3){let u=(d==1?Ei:Li)(h.node,h.offset);u&&(h=new W(u.node,u.offset))}}f.collapse(h.node,h.offset),l.bidiLevel!=null&&f.caretBidiLevel!==void 0&&(f.caretBidiLevel=l.bidiLevel)}else if(f.extend){f.collapse(h.node,h.offset);try{f.extend(c.node,c.offset)}catch{}}else{let d=document.createRange();l.anchor>l.head&&([h,c]=[c,h]),d.setEnd(c.node,c.offset),d.setStart(h.node,h.offset),f.removeAllRanges(),f.addRange(d)}o&&this.view.root.activeElement==this.dom&&(this.dom.blur(),i&&i.focus())}),this.view.observer.setSelectionRange(h,c)),this.impreciseAnchor=h.precise?null:new W(a.anchorNode,a.anchorOffset),this.impreciseHead=c.precise?null:new W(a.focusNode,a.focusOffset)}suppressWidgetCursorChange(t,e){return this.hasComposition&&e.empty&&Pt(t.focusNode,t.focusOffset,t.anchorNode,t.anchorOffset)&&this.posFromDOM(t.focusNode,t.focusOffset)==e.head}enforceCursorAssoc(){if(this.hasComposition)return;let{view:t}=this,e=t.state.selection.main,i=Bt(t.root),{anchorNode:s,anchorOffset:o}=t.observer.selectionRange;if(!i||!e.empty||!e.assoc||!i.modify)return;let r=B.find(this,e.head);if(!r)return;let l=r.posAtStart;if(e.head==l||e.head==l+r.length)return;let h=this.coordsAt(e.head,-1),c=this.coordsAt(e.head,1);if(!h||!c||h.bottom>c.top)return;let a=this.domAtPos(e.head+e.assoc);i.collapse(a.node,a.offset),i.modify("move",e.assoc<0?"forward":"backward","lineboundary"),t.observer.readSelectionRange();let f=t.observer.selectionRange;t.docView.posFromDOM(f.anchorNode,f.anchorOffset)!=e.from&&i.collapse(s,o)}moveToLine(t){let e=this.dom,i;if(t.node!=e)return t;for(let s=t.offset;!i&&s=0;s--){let o=O.get(e.childNodes[s]);o instanceof B&&(i=o.domAtPos(o.length))}return i?new W(i.node,i.offset,!0):t}nearest(t){for(let e=t;e;){let i=O.get(e);if(i&&i.rootView==this)return i;e=e.parentNode}return null}posFromDOM(t,e){let i=this.nearest(t);if(!i)throw new RangeError("Trying to find position for a DOM position outside of the document");return i.localPosFromDOM(t,e)+i.posAtStart}domAtPos(t){let{i:e,off:i}=this.childCursor().findPos(t,-1);for(;e=0;r--){let l=this.children[r],h=o-l.breakAfter,c=h-l.length;if(ht||l.covers(1))&&(!i||l instanceof B&&!(i instanceof B&&e>=0)))i=l,s=c;else if(i&&c==t&&h==t&&l instanceof st&&Math.abs(e)<2){if(l.deco.startSide<0)break;r&&(i=null)}o=c}return i?i.coordsAt(t-s,e):null}coordsForChar(t){let{i:e,off:i}=this.childPos(t,1),s=this.children[e];if(!(s instanceof B))return null;for(;s.children.length;){let{i:l,off:h}=s.childPos(i,1);for(;;l++){if(l==s.children.length)return null;if((s=s.children[l]).length)break}i=h}if(!(s instanceof $))return null;let o=wt(s.text,i);if(o==i)return null;let r=ut(s.dom,i,o).getClientRects();for(let l=0;lMath.max(this.view.scrollDOM.clientWidth,this.minWidth)+1,l=-1,h=this.view.textDirection==E.LTR;for(let c=0,a=0;as)break;if(c>=i){let u=f.dom.getBoundingClientRect();if(e.push(u.height),r){let g=f.dom.lastChild,p=g?St(g):[];if(p.length){let b=p[p.length-1],m=h?b.right-u.left:u.right-b.left;m>l&&(l=m,this.minWidth=o,this.minWidthFrom=c,this.minWidthTo=d)}}}c=d+f.breakAfter}return e}textDirectionAt(t){let{i:e}=this.childPos(t,1);return getComputedStyle(this.children[e].dom).direction=="rtl"?E.RTL:E.LTR}measureTextSize(){for(let o of this.children)if(o instanceof B){let r=o.measureTextSize();if(r)return r}let t=document.createElement("div"),e,i,s;return t.className="cm-line",t.style.width="99999px",t.style.position="absolute",t.textContent="abc def ghi jkl mno pqr stu",this.view.observer.ignore(()=>{this.dom.appendChild(t);let o=St(t.firstChild)[0];e=t.getBoundingClientRect().height,i=o?o.width/27:7,s=o?o.height:e,t.remove()}),{lineHeight:e,charWidth:i,textHeight:s}}childCursor(t=this.length){let e=this.children.length;return e&&(t-=this.children[--e].length),new Pi(this.children,t,e)}computeBlockGapDeco(){let t=[],e=this.view.viewState;for(let i=0,s=0;;s++){let o=s==e.viewports.length?null:e.viewports[s],r=o?o.from-1:this.length;if(r>i){let l=(e.lineBlockAt(r).bottom-e.lineBlockAt(i).top)/this.view.scaleY;t.push(R.replace({widget:new Ve(l),block:!0,inclusive:!0,isBlockGap:!0}).range(i,r))}if(!o)break;i=o.to+1}return R.set(t)}updateDeco(){let t=1,e=this.view.state.facet(Vt).map(o=>(this.dynamicDecorationMap[t++]=typeof o=="function")?o(this.view):o),i=!1,s=this.view.state.facet(fs).map((o,r)=>{let l=typeof o=="function";return l&&(i=!0),l?o(this.view):o});for(s.length&&(this.dynamicDecorationMap[t++]=i,e.push(V.join(s))),this.decorations=[this.editContextFormatting,...e,this.computeBlockGapDeco(),this.view.viewState.lineGapDeco];te.anchor?-1:1),s;if(!i)return;!e.empty&&(s=this.coordsAt(e.anchor,e.anchor>e.head?-1:1))&&(i={left:Math.min(i.left,s.left),top:Math.min(i.top,s.top),right:Math.max(i.right,s.right),bottom:Math.max(i.bottom,s.bottom)});let o=ps(this.view),r={left:i.left-o.left,top:i.top-o.top,right:i.right+o.right,bottom:i.bottom+o.bottom},{offsetWidth:l,offsetHeight:h}=this.view.scrollDOM;Kn(this.view.scrollDOM,r,e.head{it.from&&(e=!0)}),e}function go(n,t,e=1){let i=n.charCategorizer(t),s=n.doc.lineAt(t),o=t-s.from;if(s.length==0)return M.cursor(t);o==0?e=1:o==s.length&&(e=-1);let r=o,l=o;e<0?r=wt(s.text,o,!1):l=wt(s.text,o);let h=i(s.text.slice(r,l));for(;r>0;){let c=wt(s.text,r,!1);if(i(s.text.slice(c,r))!=h)break;r=c}for(;ln?t.left-n:Math.max(0,n-t.right)}function mo(n,t){return t.top>n?t.top-n:Math.max(0,n-t.bottom)}function Ye(n,t){return n.topt.top+1}function ys(n,t){return tn.bottom?{top:n.top,left:n.left,right:n.right,bottom:t}:n}function _e(n,t,e){let i,s,o,r,l=!1,h,c,a,f;for(let g=n.firstChild;g;g=g.nextSibling){let p=St(g);for(let b=0;bv||r==v&&o>w){i=g,s=m,o=w,r=v;let x=v?e0?b0)}w==0?e>m.bottom&&(!a||a.bottomm.top)&&(c=g,f=m):a&&Ye(a,m)?a=ws(a,m.bottom):f&&Ye(f,m)&&(f=ys(f,m.top))}}if(a&&a.bottom>=e?(i=h,s=a):f&&f.top<=e&&(i=c,s=f),!i)return{node:n,offset:0};let d=Math.max(s.left,Math.min(s.right,t));if(i.nodeType==3)return xs(i,d,e);if(l&&i.contentEditable!="false")return _e(i,d,e);let u=Array.prototype.indexOf.call(n.childNodes,i)+(t>=(s.left+s.right)/2?1:0);return{node:n,offset:u}}function xs(n,t,e){let i=n.nodeValue.length,s=-1,o=1e9,r=0;for(let l=0;le?a.top-e:e-a.bottom)-1;if(a.left-1<=t&&a.right+1>=t&&f=(a.left+a.right)/2,u=d;if((y.chrome||y.gecko)&&ut(n,l).getBoundingClientRect().left==a.right&&(u=!d),f<=0)return{node:n,offset:l+(u?1:0)};s=l+(u?1:0),o=f}}}return{node:n,offset:s>-1?s:r>0?n.nodeValue.length:0}}function vs(n,t,e,i=-1){var s,o;let r=n.contentDOM.getBoundingClientRect(),l=r.top+n.viewState.paddingTop,h,{docHeight:c}=n.viewState,{x:a,y:f}=t,d=f-l;if(d<0)return 0;if(d>c)return n.state.doc.length;for(let x=n.viewState.heightOracle.textHeight/2,S=!1;h=n.elementAtHeight(d),h.type!=F.Text;)for(;d=i>0?h.bottom+x:h.top-x,!(d>=0&&d<=c);){if(S)return e?null:0;S=!0,i=-i}f=l+d;let u=h.from;if(un.viewport.to)return n.viewport.to==n.state.doc.length?n.state.doc.length:e?null:Ss(n,r,h,a,f);let g=n.dom.ownerDocument,p=n.root.elementFromPoint?n.root:g,b=p.elementFromPoint(a,f);b&&!n.contentDOM.contains(b)&&(b=null),b||(a=Math.max(r.left+1,Math.min(r.right-1,a)),b=p.elementFromPoint(a,f),b&&!n.contentDOM.contains(b)&&(b=null));let m,w=-1;if(b&&((s=n.docView.nearest(b))===null||s===void 0?void 0:s.isEditable)!=!1){if(g.caretPositionFromPoint){let x=g.caretPositionFromPoint(a,f);x&&({offsetNode:m,offset:w}=x)}else if(g.caretRangeFromPoint){let x=g.caretRangeFromPoint(a,f);x&&({startContainer:m,startOffset:w}=x,(!n.contentDOM.contains(m)||y.safari&&bo(m,w,a)||y.chrome&&yo(m,w,a))&&(m=void 0))}}if(!m||!n.docView.dom.contains(m)){let x=B.find(n.docView,u);if(!x)return d>h.top+h.height/2?h.to:h.from;({node:m,offset:w}=_e(x.dom,a,f))}let v=n.docView.nearest(m);if(!v)return null;if(v.isWidget&&((o=v.dom)===null||o===void 0?void 0:o.nodeType)==1){let x=v.dom.getBoundingClientRect();return t.yn.defaultLineHeight*1.5){let l=n.viewState.heightOracle.textHeight,h=Math.floor((s-e.top-(n.defaultLineHeight-l)*.5)/l);o+=h*n.viewState.heightOracle.lineLength}let r=n.state.sliceDoc(e.from,e.to);return e.from+ke(r,o,n.state.tabSize)}function bo(n,t,e){let i;if(n.nodeType!=3||t!=(i=n.nodeValue.length))return!1;for(let s=n.nextSibling;s;s=s.nextSibling)if(s.nodeType!=1||s.nodeName!="BR")return!1;return ut(n,i-1,i).getBoundingClientRect().left>e}function yo(n,t,e){if(t!=0)return!1;for(let s=n;;){let o=s.parentNode;if(!o||o.nodeType!=1||o.firstChild!=s)return!1;if(o.classList.contains("cm-line"))break;s=o}let i=n.nodeType==1?n.getBoundingClientRect():ut(n,0,Math.max(n.nodeValue.length,1)).getBoundingClientRect();return e-i.left>5}function Xe(n,t){let e=n.lineBlockAt(t);if(Array.isArray(e.type)){for(let i of e.type)if(i.to>t||i.to==t&&(i.to==e.to||i.type==F.Text))return i}return e}function wo(n,t,e,i){let s=Xe(n,t.head),o=!i||s.type!=F.Text||!(n.lineWrapping||s.widgetLineBreaks)?null:n.coordsAtPos(t.assoc<0&&t.head>s.from?t.head-1:t.head);if(o){let r=n.dom.getBoundingClientRect(),l=n.textDirectionAt(s.from),h=n.posAtCoords({x:e==(l==E.LTR)?r.right-1:r.left+1,y:(o.top+o.bottom)/2});if(h!=null)return M.cursor(h,e?-1:1)}return M.cursor(e?s.to:s.from,e?-1:1)}function Cs(n,t,e,i){let s=n.state.doc.lineAt(t.head),o=n.bidiSpans(s),r=n.textDirectionAt(s.from);for(let l=t,h=null;;){let c=Zi(s,o,r,l,e),a=Ji;if(!c){if(s.number==(e?n.state.doc.lines:1))return l;a=` +`,s=n.state.doc.line(s.number+(e?1:-1)),o=n.bidiSpans(s),c=n.visualLineSide(s,!e)}if(h){if(!h(a))return l}else{if(!i)return c;h=i(a)}l=c}}function xo(n,t,e){let i=n.state.charCategorizer(t),s=i(e);return o=>{let r=i(o);return s==Ln.Space&&(s=r),s==r}}function vo(n,t,e,i){let s=t.head,o=e?1:-1;if(s==(e?n.state.doc.length:0))return M.cursor(s,t.assoc);let r=t.goalColumn,l,h=n.contentDOM.getBoundingClientRect(),c=n.coordsAtPos(s,t.assoc||-1),a=n.documentTop;if(c)r==null&&(r=c.left-h.left),l=o<0?c.top:c.bottom;else{let u=n.viewState.lineBlockAt(s);r==null&&(r=Math.min(h.right-h.left,n.defaultCharacterWidth*(s-u.from))),l=(o<0?u.top:u.bottom)+a}let f=h.left+r,d=i??n.viewState.heightOracle.textHeight>>1;for(let u=0;;u+=10){let g=l+(d+u)*o,p=vs(n,{x:f,y:g},!1,o);if(gh.bottom||(o<0?ps)){let b=n.docView.coordsForChar(p),m=!b||g{if(t>o&&ts(n)),e.from,t.head>e.from?-1:1);return i==e.from?e:M.cursor(i,io)&&this.lineBreak(),s=r}return this.findPointBefore(i,e),this}readTextNode(t){let e=t.nodeValue;for(let i of this.points)i.node==t&&(i.pos=this.text.length+Math.min(i.offset,e.length));for(let i=0,s=this.lineSeparator?null:/\r\n?|\n/g;;){let o=-1,r=1,l;if(this.lineSeparator?(o=e.indexOf(this.lineSeparator,i),r=this.lineSeparator.length):(l=s.exec(e))&&(o=l.index,r=l[0].length),this.append(e.slice(i,o<0?e.length:o)),o<0)break;if(this.lineBreak(),r>1)for(let h of this.points)h.node==t&&h.pos>this.text.length&&(h.pos-=r-1);i=o+r}}readNode(t){if(t.cmIgnore)return;let e=O.get(t),i=e&&e.overrideDOMText;if(i!=null){this.findPointInside(t,i.length);for(let s=i.iter();!s.next().done;)s.lineBreak?this.lineBreak():this.append(s.value)}else t.nodeType==3?this.readTextNode(t):t.nodeName=="BR"?t.nextSibling&&this.lineBreak():t.nodeType==1&&this.readRange(t.firstChild,null)}findPointBefore(t,e){for(let i of this.points)i.node==t&&t.childNodes[i.offset]==e&&(i.pos=this.text.length)}findPointInside(t,e){for(let i of this.points)(t.nodeType==3?i.node==t:t.contains(i.node))&&(i.pos=this.text.length+(Co(t,i.node,i.offset)?e:0))}}function Co(n,t,e){for(;;){if(!t||e-1;let{impreciseHead:o,impreciseAnchor:r}=t.docView;if(t.state.readOnly&&e>-1)this.newSel=null;else if(e>-1&&(this.bounds=t.docView.domBoundsAround(e,i,0))){let l=o||r?[]:Do(t),h=new So(l,t.state);h.readRange(this.bounds.startDOM,this.bounds.endDOM),this.text=h.text,this.newSel=Oo(l,this.bounds.from)}else{let l=t.observer.selectionRange,h=o&&o.node==l.focusNode&&o.offset==l.focusOffset||!Oe(t.contentDOM,l.focusNode)?t.state.selection.main.head:t.docView.posFromDOM(l.focusNode,l.focusOffset),c=r&&r.node==l.anchorNode&&r.offset==l.anchorOffset||!Oe(t.contentDOM,l.anchorNode)?t.state.selection.main.anchor:t.docView.posFromDOM(l.anchorNode,l.anchorOffset),a=t.viewport;if((y.ios||y.chrome)&&t.state.selection.main.empty&&h!=c&&(a.from>0||a.toDate.now()-100?n.inputState.lastKeyCode:-1;if(t.bounds){let{from:r,to:l}=t.bounds,h=s.from,c=null;(o===8||y.android&&t.text.length=s.from&&e.to<=s.to&&(e.from!=s.from||e.to!=s.to)&&s.to-s.from-(e.to-e.from)<=4?e={from:s.from,to:s.to,insert:n.state.doc.slice(s.from,e.from).append(e.insert).append(n.state.doc.slice(e.to,s.to))}:(y.mac||y.android)&&e&&e.from==e.to&&e.from==s.head-1&&/^\. ?$/.test(e.insert.toString())&&n.contentDOM.getAttribute("autocorrect")=="off"?(i&&e.insert.length==2&&(i=M.single(i.main.anchor-1,i.main.head-1)),e={from:s.from,to:s.to,insert:Y.of([" "])}):y.chrome&&e&&e.from==e.to&&e.from==s.head&&e.insert.toString()==` + `&&n.lineWrapping&&(i&&(i=M.single(i.main.anchor-1,i.main.head-1)),e={from:s.from,to:s.to,insert:Y.of([" "])}),e)return $e(n,e,i,o);if(i&&!i.main.eq(s)){let r=!1,l="select";return n.inputState.lastSelectionTime>Date.now()-50&&(n.inputState.lastSelectionOrigin=="select"&&(r=!0),l=n.inputState.lastSelectionOrigin),n.dispatch({selection:i,scrollIntoView:r,userEvent:l}),!0}else return!1}function $e(n,t,e,i=-1){if(y.ios&&n.inputState.flushIOSKey(t))return!0;let s=n.state.selection.main;if(y.android&&(t.to==s.to&&(t.from==s.from||t.from==s.from-1&&n.state.sliceDoc(t.from,s.from)==" ")&&t.insert.length==1&&t.insert.lines==2&&Mt(n.contentDOM,"Enter",13)||(t.from==s.from-1&&t.to==s.to&&t.insert.length==0||i==8&&t.insert.lengths.head)&&Mt(n.contentDOM,"Backspace",8)||t.from==s.from&&t.to==s.to+1&&t.insert.length==0&&Mt(n.contentDOM,"Delete",46)))return!0;let o=t.insert.toString();n.inputState.composing>=0&&n.inputState.composing++;let r,l=()=>r||(r=ko(n,t,e));return n.state.facet(ns).some(h=>h(n,t.from,t.to,o,l))||n.dispatch(l()),!0}function ko(n,t,e){let i,s=n.state,o=s.selection.main;if(t.from>=o.from&&t.to<=o.to&&t.to-t.from>=(o.to-o.from)/3&&(!e||e.main.empty&&e.main.from==t.from+t.insert.length)&&n.inputState.composing<0){let l=o.fromt.to?s.sliceDoc(t.to,o.to):"";i=s.replaceSelection(n.state.toText(l+t.insert.sliceString(0,void 0,n.state.lineBreak)+h))}else{let l=s.changes(t),h=e&&e.main.to<=l.newLength?e.main:void 0;if(s.selection.ranges.length>1&&n.inputState.composing>=0&&t.to<=o.to&&t.to>=o.to-10){let c=n.state.sliceDoc(t.from,t.to),a,f=e&&bs(n,e.main.head);if(f){let g=t.insert.length-(t.to-t.from);a={from:f.from,to:f.to-g}}else a=n.state.doc.lineAt(o.head);let d=o.to-t.to,u=o.to-o.from;i=s.changeByRange(g=>{if(g.from==o.from&&g.to==o.to)return{changes:l,range:h||g.map(l)};let p=g.to-d,b=p-c.length;if(g.to-g.from!=u||n.state.sliceDoc(b,p)!=c||g.to>=a.from&&g.from<=a.to)return{range:g};let m=s.changes({from:b,to:p,insert:t.insert}),w=g.to-o.to;return{changes:m,range:h?M.range(Math.max(0,h.anchor+w),Math.max(0,h.head+w)):g.map(m)}})}else i={changes:l,selection:h&&s.selection.replaceRange(h)}}let r="input.type";return(n.composing||n.inputState.compositionPendingChange&&n.inputState.compositionEndedAt>Date.now()-50)&&(n.inputState.compositionPendingChange=!1,r+=".compose",n.inputState.compositionFirstChange&&(r+=".start",n.inputState.compositionFirstChange=!1)),s.update(i,{userEvent:r,scrollIntoView:!0})}function Ao(n,t,e,i){let s=Math.min(n.length,t.length),o=0;for(;o0&&l>0&&n.charCodeAt(r-1)==t.charCodeAt(l-1);)r--,l--;if(i=="end"){let h=Math.max(0,o-Math.min(r,l));e-=r+h-o}if(r=r?o-e:0;o-=h,l=o+(l-r),r=o}else if(l=l?o-e:0;o-=h,r=o+(r-l),l=o}return{from:o,toA:r,toB:l}}function Do(n){let t=[];if(n.root.activeElement!=n.contentDOM)return t;let{anchorNode:e,anchorOffset:i,focusNode:s,focusOffset:o}=n.observer.selectionRange;return e&&(t.push(new Ms(e,i)),(s!=e||o!=i)&&t.push(new Ms(s,o))),t}function Oo(n,t){if(n.length==0)return null;let e=n[0].pos,i=n.length==2?n[1].pos:e;return e>-1&&i>-1?M.single(e+t,i+t):null}class To{setSelectionOrigin(t){this.lastSelectionOrigin=t,this.lastSelectionTime=Date.now()}constructor(t){this.view=t,this.lastKeyCode=0,this.lastKeyTime=0,this.lastTouchTime=0,this.lastFocusTime=0,this.lastScrollTop=0,this.lastScrollLeft=0,this.pendingIOSKey=void 0,this.tabFocusMode=-1,this.lastSelectionOrigin=null,this.lastSelectionTime=0,this.lastContextMenu=0,this.scrollHandlers=[],this.handlers=Object.create(null),this.composing=-1,this.compositionFirstChange=null,this.compositionEndedAt=0,this.compositionPendingKey=!1,this.compositionPendingChange=!1,this.mouseSelection=null,this.draggedContent=null,this.handleEvent=this.handleEvent.bind(this),this.notifiedFocused=t.hasFocus,y.safari&&t.contentDOM.addEventListener("input",()=>null),y.gecko&&Yo(t.contentDOM.ownerDocument)}handleEvent(t){!Vo(this.view,t)||this.ignoreDuringComposition(t)||t.type=="keydown"&&this.keydown(t)||this.runHandlers(t.type,t)}runHandlers(t,e){let i=this.handlers[t];if(i){for(let s of i.observers)s(this.view,e);for(let s of i.handlers){if(e.defaultPrevented)break;if(s(this.view,e)){e.preventDefault();break}}}}ensureHandlers(t){let e=Ro(t),i=this.handlers,s=this.view.contentDOM;for(let o in e)if(o!="scroll"){let r=!e[o].handlers.length,l=i[o];l&&r!=!l.handlers.length&&(s.removeEventListener(o,this.handleEvent),l=null),l||s.addEventListener(o,this.handleEvent,{passive:r})}for(let o in i)o!="scroll"&&!e[o]&&s.removeEventListener(o,this.handleEvent);this.handlers=e}keydown(t){if(this.lastKeyCode=t.keyCode,this.lastKeyTime=Date.now(),t.keyCode==9&&this.tabFocusMode>-1&&(!this.tabFocusMode||Date.now()<=this.tabFocusMode))return!0;if(this.tabFocusMode>0&&t.keyCode!=27&&Os.indexOf(t.keyCode)<0&&(this.tabFocusMode=-1),y.android&&y.chrome&&!t.synthetic&&(t.keyCode==13||t.keyCode==8))return this.view.observer.delayAndroidKey(t.key,t.keyCode),!0;let e;return y.ios&&!t.synthetic&&!t.altKey&&!t.metaKey&&((e=Ds.find(i=>i.keyCode==t.keyCode))&&!t.ctrlKey||Eo.indexOf(t.key)>-1&&t.ctrlKey&&!t.shiftKey)?(this.pendingIOSKey=e||t,setTimeout(()=>this.flushIOSKey(),250),!0):(t.keyCode!=229&&this.view.observer.forceFlush(),!1)}flushIOSKey(t){let e=this.pendingIOSKey;return!e||e.key=="Enter"&&t&&t.from0?!0:y.safari&&!y.ios&&this.compositionPendingKey&&Date.now()-this.compositionEndedAt<100?(this.compositionPendingKey=!1,!0):!1:!1}startMouseSelection(t){this.mouseSelection&&this.mouseSelection.destroy(),this.mouseSelection=t}update(t){this.view.observer.update(t),this.mouseSelection&&this.mouseSelection.update(t),this.draggedContent&&t.docChanged&&(this.draggedContent=this.draggedContent.map(t.changes)),t.transactions.length&&(this.lastKeyCode=this.lastSelectionTime=0)}destroy(){this.mouseSelection&&this.mouseSelection.destroy()}}function As(n,t){return(e,i)=>{try{return t.call(n,i,e)}catch(s){_(e.state,s)}}}function Ro(n){let t=Object.create(null);function e(i){return t[i]||(t[i]={observers:[],handlers:[]})}for(let i of n){let s=i.spec;if(s&&s.domEventHandlers)for(let o in s.domEventHandlers){let r=s.domEventHandlers[o];r&&e(o).handlers.push(As(i.value,r))}if(s&&s.domEventObservers)for(let o in s.domEventObservers){let r=s.domEventObservers[o];r&&e(o).observers.push(As(i.value,r))}}for(let i in X)e(i).handlers.push(X[i]);for(let i in j)e(i).observers.push(j[i]);return t}const Ds=[{key:"Backspace",keyCode:8,inputType:"deleteContentBackward"},{key:"Enter",keyCode:13,inputType:"insertParagraph"},{key:"Enter",keyCode:13,inputType:"insertLineBreak"},{key:"Delete",keyCode:46,inputType:"deleteContentForward"}],Eo="dthko",Os=[16,17,18,20,91,92,224,225],le=6;function he(n){return Math.max(0,n)*.7+8}function Lo(n,t){return Math.max(Math.abs(n.clientX-t.clientX),Math.abs(n.clientY-t.clientY))}class Bo{constructor(t,e,i,s){this.view=t,this.startEvent=e,this.style=i,this.mustSelect=s,this.scrollSpeed={x:0,y:0},this.scrolling=-1,this.lastEvent=e,this.scrollParents=qn(t.contentDOM),this.atoms=t.state.facet(je).map(r=>r(t));let o=t.contentDOM.ownerDocument;o.addEventListener("mousemove",this.move=this.move.bind(this)),o.addEventListener("mouseup",this.up=this.up.bind(this)),this.extend=e.shiftKey,this.multiple=t.state.facet(Lt.allowMultipleSelections)&&Po(t,e),this.dragging=No(t,e)&&Vs(e)==1?null:!1}start(t){this.dragging===!1&&this.select(t)}move(t){if(t.buttons==0)return this.destroy();if(this.dragging||this.dragging==null&&Lo(this.startEvent,t)<10)return;this.select(this.lastEvent=t);let e=0,i=0,s=0,o=0,r=this.view.win.innerWidth,l=this.view.win.innerHeight;this.scrollParents.x&&({left:s,right:r}=this.scrollParents.x.getBoundingClientRect()),this.scrollParents.y&&({top:o,bottom:l}=this.scrollParents.y.getBoundingClientRect());let h=ps(this.view);t.clientX-h.left<=s+le?e=-he(s-t.clientX):t.clientX+h.right>=r-le&&(e=he(t.clientX-r)),t.clientY-h.top<=o+le?i=-he(o-t.clientY):t.clientY+h.bottom>=l-le&&(i=he(t.clientY-l)),this.setScrollSpeed(e,i)}up(t){this.dragging==null&&this.select(this.lastEvent),this.dragging||t.preventDefault(),this.destroy()}destroy(){this.setScrollSpeed(0,0);let t=this.view.contentDOM.ownerDocument;t.removeEventListener("mousemove",this.move),t.removeEventListener("mouseup",this.up),this.view.inputState.mouseSelection=this.view.inputState.draggedContent=null}setScrollSpeed(t,e){this.scrollSpeed={x:t,y:e},t||e?this.scrolling<0&&(this.scrolling=setInterval(()=>this.scroll(),50)):this.scrolling>-1&&(clearInterval(this.scrolling),this.scrolling=-1)}scroll(){let{x:t,y:e}=this.scrollSpeed;t&&this.scrollParents.x&&(this.scrollParents.x.scrollLeft+=t,t=0),e&&this.scrollParents.y&&(this.scrollParents.y.scrollTop+=e,e=0),(t||e)&&this.view.win.scrollBy(t,e),this.dragging===!1&&this.select(this.lastEvent)}skipAtoms(t){let e=null;for(let i=0;ie.isUserEvent("input.type"))?this.destroy():this.style.update(t)&&setTimeout(()=>this.select(this.lastEvent),20)}}function Po(n,t){let e=n.state.facet(ts);return e.length?e[0](t):y.mac?t.metaKey:t.ctrlKey}function Ho(n,t){let e=n.state.facet(es);return e.length?e[0](t):y.mac?!t.altKey:!t.ctrlKey}function No(n,t){let{main:e}=n.state.selection;if(e.empty)return!1;let i=Bt(n.root);if(!i||i.rangeCount==0)return!0;let s=i.getRangeAt(0).getClientRects();for(let o=0;o=t.clientX&&r.top<=t.clientY&&r.bottom>=t.clientY)return!0}return!1}function Vo(n,t){if(!t.bubbles)return!0;if(t.defaultPrevented)return!1;for(let e=t.target,i;e!=n.contentDOM;e=e.parentNode)if(!e||e.nodeType==11||(i=O.get(e))&&i.ignoreEvent(t))return!1;return!0}const X=Object.create(null),j=Object.create(null),Ts=y.ie&&y.ie_version<15||y.ios&&y.webkit_version<604;function Fo(n){let t=n.dom.parentNode;if(!t)return;let e=t.appendChild(document.createElement("textarea"));e.style.cssText="position: fixed; left: -10000px; top: 10px",e.focus(),setTimeout(()=>{n.focus(),e.remove(),Rs(n,e.value)},50)}function Rs(n,t){let{state:e}=n,i,s=1,o=e.toText(t),r=o.lines==e.selection.ranges.length;if(Ue!=null&&e.selection.ranges.every(h=>h.empty)&&Ue==o.toString()){let h=-1;i=e.changeByRange(c=>{let a=e.doc.lineAt(c.from);if(a.from==h)return{range:c};h=a.from;let f=e.toText((r?o.line(s++).text:t)+e.lineBreak);return{changes:{from:a.from,insert:f},range:M.cursor(c.from+f.length)}})}else r?i=e.changeByRange(h=>{let c=o.line(s++);return{changes:{from:h.from,to:h.to,insert:c.text},range:M.cursor(h.from+c.length)}}):i=e.replaceSelection(o);n.dispatch(i,{userEvent:"input.paste",scrollIntoView:!0})}j.scroll=n=>{n.inputState.lastScrollTop=n.scrollDOM.scrollTop,n.inputState.lastScrollLeft=n.scrollDOM.scrollLeft},X.keydown=(n,t)=>(n.inputState.setSelectionOrigin("select"),t.keyCode==27&&n.inputState.tabFocusMode!=0&&(n.inputState.tabFocusMode=Date.now()+2e3),!1),j.touchstart=(n,t)=>{n.inputState.lastTouchTime=Date.now(),n.inputState.setSelectionOrigin("select.pointer")},j.touchmove=n=>{n.inputState.setSelectionOrigin("select.pointer")},X.mousedown=(n,t)=>{if(n.observer.flush(),n.inputState.lastTouchTime>Date.now()-2e3)return!1;let e=null;for(let i of n.state.facet(is))if(e=i(n,t),e)break;if(!e&&t.button==0&&(e=Io(n,t)),e){let i=!n.hasFocus;n.inputState.startMouseSelection(new Bo(n,t,e,i)),i&&n.observer.ignore(()=>{Di(n.contentDOM);let o=n.root.activeElement;o&&!o.contains(n.contentDOM)&&o.blur()});let s=n.inputState.mouseSelection;if(s)return s.start(t),s.dragging===!1}return!1};function Es(n,t,e,i){if(i==1)return M.cursor(t,e);if(i==2)return go(n.state,t,e);{let s=B.find(n.docView,t),o=n.state.doc.lineAt(s?s.posAtEnd:t),r=s?s.posAtStart:o.from,l=s?s.posAtEnd:o.to;return lt>=e.top&&t<=e.bottom&&n>=e.left&&n<=e.right;function Wo(n,t,e,i){let s=B.find(n.docView,t);if(!s)return 1;let o=t-s.posAtStart;if(o==0)return 1;if(o==s.length)return-1;let r=s.coordsAt(o,-1);if(r&&Ls(e,i,r))return-1;let l=s.coordsAt(o,1);return l&&Ls(e,i,l)?1:r&&r.bottom>=i?-1:1}function Bs(n,t){let e=n.posAtCoords({x:t.clientX,y:t.clientY},!1);return{pos:e,bias:Wo(n,e,t.clientX,t.clientY)}}const zo=y.ie&&y.ie_version<=11;let Ps=null,Hs=0,Ns=0;function Vs(n){if(!zo)return n.detail;let t=Ps,e=Ns;return Ps=n,Ns=Date.now(),Hs=!t||e>Date.now()-400&&Math.abs(t.clientX-n.clientX)<2&&Math.abs(t.clientY-n.clientY)<2?(Hs+1)%3:1}function Io(n,t){let e=Bs(n,t),i=Vs(t),s=n.state.selection;return{update(o){o.docChanged&&(e.pos=o.changes.mapPos(e.pos),s=s.map(o.changes))},get(o,r,l){let h=Bs(n,o),c,a=Es(n,h.pos,h.bias,i);if(e.pos!=h.pos&&!r){let f=Es(n,e.pos,e.bias,i),d=Math.min(f.from,a.from),u=Math.max(f.to,a.to);a=d1&&(c=Ko(s,h.pos))?c:l?s.addRange(a):M.create([a])}}}function Ko(n,t){for(let e=0;e=t)return M.create(n.ranges.slice(0,e).concat(n.ranges.slice(e+1)),n.mainIndex==e?0:n.mainIndex-(n.mainIndex>e?1:0))}return null}X.dragstart=(n,t)=>{let{selection:{main:e}}=n.state;if(t.target.draggable){let s=n.docView.nearest(t.target);if(s&&s.isWidget){let o=s.posAtStart,r=o+s.length;(o>=e.to||r<=e.from)&&(e=M.range(o,r))}}let{inputState:i}=n;return i.mouseSelection&&(i.mouseSelection.dragging=!0),i.draggedContent=e,t.dataTransfer&&(t.dataTransfer.setData("Text",n.state.sliceDoc(e.from,e.to)),t.dataTransfer.effectAllowed="copyMove"),!1},X.dragend=n=>(n.inputState.draggedContent=null,!1);function Fs(n,t,e,i){if(!e)return;let s=n.posAtCoords({x:t.clientX,y:t.clientY},!1),{draggedContent:o}=n.inputState,r=i&&o&&Ho(n,t)?{from:o.from,to:o.to}:null,l={from:s,insert:e},h=n.state.changes(r?[r,l]:l);n.focus(),n.dispatch({changes:h,selection:{anchor:h.mapPos(s,-1),head:h.mapPos(s,1)},userEvent:r?"move.drop":"input.drop"}),n.inputState.draggedContent=null}X.drop=(n,t)=>{if(!t.dataTransfer)return!1;if(n.state.readOnly)return!0;let e=t.dataTransfer.files;if(e&&e.length){let i=Array(e.length),s=0,o=()=>{++s==e.length&&Fs(n,t,i.filter(r=>r!=null).join(n.state.lineBreak),!1)};for(let r=0;r{/[\x00-\x08\x0e-\x1f]{2}/.test(l.result)||(i[r]=l.result),o()},l.readAsText(e[r])}return!0}else{let i=t.dataTransfer.getData("Text");if(i)return Fs(n,t,i,!0),!0}return!1},X.paste=(n,t)=>{if(n.state.readOnly)return!0;n.observer.flush();let e=Ts?null:t.clipboardData;return e?(Rs(n,e.getData("text/plain")||e.getData("text/uri-list")),!0):(Fo(n),!1)};function qo(n,t){let e=n.dom.parentNode;if(!e)return;let i=e.appendChild(document.createElement("textarea"));i.style.cssText="position: fixed; left: -10000px; top: 10px",i.value=t,i.focus(),i.selectionEnd=t.length,i.selectionStart=0,setTimeout(()=>{i.remove(),n.focus()},50)}function jo(n){let t=[],e=[],i=!1;for(let s of n.selection.ranges)s.empty||(t.push(n.sliceDoc(s.from,s.to)),e.push(s));if(!t.length){let s=-1;for(let{from:o}of n.selection.ranges){let r=n.doc.lineAt(o);r.number>s&&(t.push(r.text),e.push({from:r.from,to:Math.min(n.doc.length,r.to+1)})),s=r.number}i=!0}return{text:t.join(n.lineBreak),ranges:e,linewise:i}}let Ue=null;X.copy=X.cut=(n,t)=>{let{text:e,ranges:i,linewise:s}=jo(n.state);if(!e&&!s)return!1;Ue=s?e:null,t.type=="cut"&&!n.state.readOnly&&n.dispatch({changes:i,scrollIntoView:!0,userEvent:"delete.cut"});let o=Ts?null:t.clipboardData;return o?(o.clearData(),o.setData("text/plain",e),!0):(qo(n,e),!1)};const Ws=Bn.define();function zs(n,t){let e=[];for(let i of n.facet(os)){let s=i(n,t);s&&e.push(s)}return e?n.update({effects:e,annotations:Ws.of(!0)}):null}function Is(n){setTimeout(()=>{let t=n.hasFocus;if(t!=n.inputState.notifiedFocused){let e=zs(n.state,t);e?n.dispatch(e):n.update([])}},10)}j.focus=n=>{n.inputState.lastFocusTime=Date.now(),!n.scrollDOM.scrollTop&&(n.inputState.lastScrollTop||n.inputState.lastScrollLeft)&&(n.scrollDOM.scrollTop=n.inputState.lastScrollTop,n.scrollDOM.scrollLeft=n.inputState.lastScrollLeft),Is(n)},j.blur=n=>{n.observer.clearSelectionRange(),Is(n)},j.compositionstart=j.compositionupdate=n=>{n.observer.editContext||(n.inputState.compositionFirstChange==null&&(n.inputState.compositionFirstChange=!0),n.inputState.composing<0&&(n.inputState.composing=0))},j.compositionend=n=>{n.observer.editContext||(n.inputState.composing=-1,n.inputState.compositionEndedAt=Date.now(),n.inputState.compositionPendingKey=!0,n.inputState.compositionPendingChange=n.observer.pendingRecords().length>0,n.inputState.compositionFirstChange=null,y.chrome&&y.android?n.observer.flushSoon():n.inputState.compositionPendingChange?Promise.resolve().then(()=>n.observer.flush()):setTimeout(()=>{n.inputState.composing<0&&n.docView.hasComposition&&n.update([])},50))},j.contextmenu=n=>{n.inputState.lastContextMenu=Date.now()},X.beforeinput=(n,t)=>{var e,i;if(t.inputType=="insertReplacementText"&&n.observer.editContext){let o=(e=t.dataTransfer)===null||e===void 0?void 0:e.getData("text/plain"),r=t.getTargetRanges();if(o&&r.length){let l=r[0],h=n.posAtDOM(l.startContainer,l.startOffset),c=n.posAtDOM(l.endContainer,l.endOffset);return $e(n,{from:h,to:c,insert:n.state.toText(o)},null),!0}}let s;if(y.chrome&&y.android&&(s=Ds.find(o=>o.inputType==t.inputType))&&(n.observer.delayAndroidKey(s.key,s.keyCode),s.key=="Backspace"||s.key=="Delete")){let o=((i=window.visualViewport)===null||i===void 0?void 0:i.height)||0;setTimeout(()=>{var r;(((r=window.visualViewport)===null||r===void 0?void 0:r.height)||0)>o+10&&n.hasFocus&&(n.contentDOM.blur(),n.focus())},100)}return y.ios&&t.inputType=="deleteContentForward"&&n.observer.flushSoon(),y.safari&&t.inputType=="insertText"&&n.inputState.composing>=0&&setTimeout(()=>j.compositionend(n,t),20),!1};const Ks=new Set;function Yo(n){Ks.has(n)||(Ks.add(n),n.addEventListener("copy",()=>{}),n.addEventListener("cut",()=>{}))}const qs=["pre-wrap","normal","pre-line","break-spaces"];let mt=!1;function Qe(){mt=!1}class js{constructor(t){this.lineWrapping=t,this.doc=Y.empty,this.heightSamples={},this.lineHeight=14,this.charWidth=7,this.textHeight=14,this.lineLength=30}heightForGap(t,e){let i=this.doc.lineAt(e).number-this.doc.lineAt(t).number+1;return this.lineWrapping&&(i+=Math.max(0,Math.ceil((e-t-i*this.lineLength*.5)/this.lineLength))),this.lineHeight*i}heightForLine(t){return this.lineWrapping?(1+Math.max(0,Math.ceil((t-this.lineLength)/(this.lineLength-5))))*this.lineHeight:this.lineHeight}setDoc(t){return this.doc=t,this}mustRefreshForWrapping(t){return qs.indexOf(t)>-1!=this.lineWrapping}mustRefreshForHeights(t){let e=!1;for(let i=0;i-1,h=Math.round(e)!=Math.round(this.lineHeight)||this.lineWrapping!=l;if(this.lineWrapping=l,this.lineHeight=e,this.charWidth=i,this.textHeight=s,this.lineLength=o,h){this.heightSamples={};for(let c=0;c0}set outdated(t){this.flags=(t?2:0)|this.flags&-3}setHeight(t){this.height!=t&&(Math.abs(this.height-t)>ae&&(mt=!0),this.height=t)}replace(t,e,i){return z.of(i)}decomposeLeft(t,e){e.push(this)}decomposeRight(t,e){e.push(this)}applyChanges(t,e,i,s){let o=this,r=i.doc;for(let l=s.length-1;l>=0;l--){let{fromA:h,toA:c,fromB:a,toB:f}=s[l],d=o.lineAt(h,T.ByPosNoHeight,i.setDoc(e),0,0),u=d.to>=c?d:o.lineAt(c,T.ByPosNoHeight,i,0,0);for(f+=u.to-c,c=u.to;l>0&&d.from<=s[l-1].toA;)h=s[l-1].fromA,a=s[l-1].fromB,l--,ho*2){let l=t[e-1];l.break?t.splice(--e,1,l.left,null,l.right):t.splice(--e,1,l.left,l.right),i+=1+l.break,s-=l.size}else if(o>s*2){let l=t[i];l.break?t.splice(i,1,l.left,null,l.right):t.splice(i,1,l.left,l.right),i+=2+l.break,o-=l.size}else break;else if(s=o&&r(this.blockAt(0,i,s,o))}updateHeight(t,e=0,i=!1,s){return s&&s.from<=e&&s.more&&this.setHeight(s.heights[s.index++]),this.outdated=!1,this}toString(){return`block(${this.length})`}}class K extends _s{constructor(t,e){super(t,e,null),this.collapsed=0,this.widgetHeight=0,this.breaks=0}blockAt(t,e,i,s){return new G(s,this.length,i,this.height,this.breaks)}replace(t,e,i){let s=i[0];return i.length==1&&(s instanceof K||s instanceof N&&s.flags&4)&&Math.abs(this.length-s.length)<10?(s instanceof N?s=new K(s.length,this.height):s.height=this.height,this.outdated||(s.outdated=!1),s):z.of(i)}updateHeight(t,e=0,i=!1,s){return s&&s.from<=e&&s.more?this.setHeight(s.heights[s.index++]):(i||this.outdated)&&this.setHeight(Math.max(this.widgetHeight,t.heightForLine(this.length-this.collapsed))+this.breaks*t.lineHeight),this.outdated=!1,this}toString(){return`line(${this.length}${this.collapsed?-this.collapsed:""}${this.widgetHeight?":"+this.widgetHeight:""})`}}class N extends z{constructor(t){super(t,0)}heightMetrics(t,e){let i=t.doc.lineAt(e).number,s=t.doc.lineAt(e+this.length).number,o=s-i+1,r,l=0;if(t.lineWrapping){let h=Math.min(this.height,t.lineHeight*o);r=h/o,this.length>o+1&&(l=(this.height-h)/(this.length-o-1))}else r=this.height/o;return{firstLine:i,lastLine:s,perLine:r,perChar:l}}blockAt(t,e,i,s){let{firstLine:o,lastLine:r,perLine:l,perChar:h}=this.heightMetrics(e,s);if(e.lineWrapping){let c=s+(t0){let o=i[i.length-1];o instanceof N?i[i.length-1]=new N(o.length+s):i.push(null,new N(s-1))}if(t>0){let o=i[0];o instanceof N?i[0]=new N(t+o.length):i.unshift(new N(t-1),null)}return z.of(i)}decomposeLeft(t,e){e.push(new N(t-1),null)}decomposeRight(t,e){e.push(null,new N(this.length-t-1))}updateHeight(t,e=0,i=!1,s){let o=e+this.length;if(s&&s.from<=e+this.length&&s.more){let r=[],l=Math.max(e,s.from),h=-1;for(s.from>e&&r.push(new N(s.from-e-1).updateHeight(t,e));l<=o&&s.more;){let a=t.doc.lineAt(l).length;r.length&&r.push(null);let f=s.heights[s.index++];h==-1?h=f:Math.abs(f-h)>=ae&&(h=-2);let d=new K(a,f);d.outdated=!1,r.push(d),l+=a+1}l<=o&&r.push(null,new N(o-l).updateHeight(t,l));let c=z.of(r);return(h<0||Math.abs(c.height-this.height)>=ae||Math.abs(h-this.heightMetrics(t,e).perLine)>=ae)&&(mt=!0),ce(this,c)}else(i||this.outdated)&&(this.setHeight(t.heightForGap(e,e+this.length)),this.outdated=!1);return this}toString(){return`gap(${this.length})`}}class _o extends z{constructor(t,e,i){super(t.length+e+i.length,t.height+i.height,e|(t.outdated||i.outdated?2:0)),this.left=t,this.right=i,this.size=t.size+i.size}get break(){return this.flags&1}blockAt(t,e,i,s){let o=i+this.left.height;return tl))return c;let a=e==T.ByPosNoHeight?T.ByPosNoHeight:T.ByPos;return h?c.join(this.right.lineAt(l,a,i,r,l)):this.left.lineAt(l,a,i,s,o).join(c)}forEachLine(t,e,i,s,o,r){let l=s+this.left.height,h=o+this.left.length+this.break;if(this.break)t=h&&this.right.forEachLine(t,e,i,l,h,r);else{let c=this.lineAt(h,T.ByPos,i,s,o);t=t&&c.from<=e&&r(c),e>c.to&&this.right.forEachLine(c.to+1,e,i,l,h,r)}}replace(t,e,i){let s=this.left.length+this.break;if(ethis.left.length)return this.balanced(this.left,this.right.replace(t-s,e-s,i));let o=[];t>0&&this.decomposeLeft(t,o);let r=o.length;for(let l of i)o.push(l);if(t>0&&Xs(o,r-1),e=i&&e.push(null)),t>i&&this.right.decomposeLeft(t-i,e)}decomposeRight(t,e){let i=this.left.length,s=i+this.break;if(t>=s)return this.right.decomposeRight(t-s,e);t2*e.size||e.size>2*t.size?z.of(this.break?[t,null,e]:[t,e]):(this.left=ce(this.left,t),this.right=ce(this.right,e),this.setHeight(t.height+e.height),this.outdated=t.outdated||e.outdated,this.size=t.size+e.size,this.length=t.length+this.break+e.length,this)}updateHeight(t,e=0,i=!1,s){let{left:o,right:r}=this,l=e+o.length+this.break,h=null;return s&&s.from<=e+o.length&&s.more?h=o=o.updateHeight(t,e,i,s):o.updateHeight(t,e,i),s&&s.from<=l+r.length&&s.more?h=r=r.updateHeight(t,l,i,s):r.updateHeight(t,l,i),h?this.balanced(o,r):(this.height=this.left.height+this.right.height,this.outdated=!1,this)}toString(){return this.left+(this.break?" ":"-")+this.right}}function Xs(n,t){let e,i;n[t]==null&&(e=n[t-1])instanceof N&&(i=n[t+1])instanceof N&&n.splice(t-1,3,new N(e.length+1+i.length))}const Xo=5;class bi{constructor(t,e){this.pos=t,this.oracle=e,this.nodes=[],this.lineStart=-1,this.lineEnd=-1,this.covering=null,this.writtenTo=t}get isCovered(){return this.covering&&this.nodes[this.nodes.length-1]==this.covering}span(t,e){if(this.lineStart>-1){let i=Math.min(e,this.lineEnd),s=this.nodes[this.nodes.length-1];s instanceof K?s.length+=i-this.pos:(i>this.pos||!this.isCovered)&&this.nodes.push(new K(i-this.pos,-1)),this.writtenTo=i,e>i&&(this.nodes.push(null),this.writtenTo++,this.lineStart=-1)}this.pos=e}point(t,e,i){if(t=Xo)&&this.addLineDeco(s,o,r)}else e>t&&this.span(t,e);this.lineEnd>-1&&this.lineEnd-1)return;let{from:t,to:e}=this.oracle.doc.lineAt(this.pos);this.lineStart=t,this.lineEnd=e,this.writtenTot&&this.nodes.push(new K(this.pos-t,-1)),this.writtenTo=this.pos}blankContent(t,e){let i=new N(e-t);return this.oracle.doc.lineAt(t).to==e&&(i.flags|=4),i}ensureLine(){this.enterLine();let t=this.nodes.length?this.nodes[this.nodes.length-1]:null;if(t instanceof K)return t;let e=new K(0,-1);return this.nodes.push(e),e}addBlock(t){this.enterLine();let e=t.deco;e&&e.startSide>0&&!this.isCovered&&this.ensureLine(),this.nodes.push(t),this.writtenTo=this.pos=this.pos+t.length,e&&e.endSide>0&&(this.covering=t)}addLineDeco(t,e,i){let s=this.ensureLine();s.length+=i,s.collapsed+=i,s.widgetHeight=Math.max(s.widgetHeight,t),s.breaks+=e,this.writtenTo=this.pos=this.pos+i}finish(t){let e=this.nodes.length==0?null:this.nodes[this.nodes.length-1];this.lineStart>-1&&!(e instanceof K)&&!this.isCovered?this.nodes.push(new K(0,-1)):(this.writtenToa.clientHeight||a.scrollWidth>a.clientWidth)&&f.overflow!="visible"){let d=a.getBoundingClientRect();o=Math.max(o,d.left),r=Math.min(r,d.right),l=Math.max(l,d.top),h=Math.min(c==n.parentNode?s.innerHeight:h,d.bottom)}c=f.position=="absolute"||f.position=="fixed"?a.offsetParent:a.parentNode}else if(c.nodeType==11)c=c.host;else break;return{left:o-e.left,right:Math.max(o,r)-e.left,top:l-(e.top+t),bottom:Math.max(l,h)-(e.top+t)}}function Qo(n,t){let e=n.getBoundingClientRect();return{left:0,right:e.right-e.left,top:t,bottom:e.bottom-(e.top+t)}}class Je{constructor(t,e,i){this.from=t,this.to=e,this.size=i}static same(t,e){if(t.length!=e.length)return!1;for(let i=0;itypeof i!="function"&&i.class=="cm-lineWrapping");this.heightOracle=new js(e),this.stateDeco=t.facet(Vt).filter(i=>typeof i!="function"),this.heightMap=z.empty().applyChanges(this.stateDeco,Y.empty,this.heightOracle.setDoc(t.doc),[new q(0,0,0,t.doc.length)]);for(let i=0;i<2&&(this.viewport=this.getViewport(0,null),!!this.updateForViewport());i++);this.updateViewportLines(),this.lineGaps=this.ensureLineGaps([]),this.lineGapDeco=R.set(this.lineGaps.map(i=>i.draw(this,!1))),this.computeVisibleRanges()}updateForViewport(){let t=[this.viewport],{main:e}=this.state.selection;for(let i=0;i<=1;i++){let s=i?e.head:e.anchor;if(!t.some(({from:o,to:r})=>s>=o&&s<=r)){let{from:o,to:r}=this.lineBlockAt(s);t.push(new fe(o,r))}}return this.viewports=t.sort((i,s)=>i.from-s.from),this.updateScaler()}updateScaler(){let t=this.scaler;return this.scaler=this.heightMap.height<=7e6?$s:new yi(this.heightOracle,this.heightMap,this.viewports),t.eq(this.scaler)?0:2}updateViewportLines(){this.viewportLines=[],this.heightMap.forEachLine(this.viewport.from,this.viewport.to,this.heightOracle.setDoc(this.state.doc),0,0,t=>{this.viewportLines.push(zt(t,this.scaler))})}update(t,e=null){this.state=t.state;let i=this.stateDeco;this.stateDeco=this.state.facet(Vt).filter(a=>typeof a!="function");let s=t.changedRanges,o=q.extendWithRanges(s,Go(i,this.stateDeco,t?t.changes:vi.empty(this.state.doc.length))),r=this.heightMap.height,l=this.scrolledToBottom?null:this.scrollAnchorAt(this.scrollTop);Qe(),this.heightMap=this.heightMap.applyChanges(this.stateDeco,t.startState.doc,this.heightOracle.setDoc(this.state.doc),o),(this.heightMap.height!=r||mt)&&(t.flags|=2),l?(this.scrollAnchorPos=t.changes.mapPos(l.from,-1),this.scrollAnchorHeight=l.top):(this.scrollAnchorPos=-1,this.scrollAnchorHeight=this.heightMap.height);let h=o.length?this.mapViewport(this.viewport,t.changes):this.viewport;(e&&(e.range.headh.to)||!this.viewportIsAppropriate(h))&&(h=this.getViewport(0,e));let c=h.from!=this.viewport.from||h.to!=this.viewport.to;this.viewport=h,t.flags|=this.updateForViewport(),(c||!t.changes.empty||t.flags&2)&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(this.mapLineGaps(this.lineGaps,t.changes))),t.flags|=this.computeVisibleRanges(),e&&(this.scrollTarget=e),!this.mustEnforceCursorAssoc&&t.selectionSet&&t.view.lineWrapping&&t.state.selection.main.empty&&t.state.selection.main.assoc&&!t.state.facet(ls)&&(this.mustEnforceCursorAssoc=!0)}measure(t){let e=t.contentDOM,i=window.getComputedStyle(e),s=this.heightOracle,o=i.whiteSpace;this.defaultTextDirection=i.direction=="rtl"?E.RTL:E.LTR;let r=this.heightOracle.mustRefreshForWrapping(o),l=e.getBoundingClientRect(),h=r||this.mustMeasureContent||this.contentDOMHeight!=l.height;this.contentDOMHeight=l.height,this.mustMeasureContent=!1;let c=0,a=0;if(l.width&&l.height){let{scaleX:x,scaleY:S}=Ai(e,l);(x>.005&&Math.abs(this.scaleX-x)>.005||S>.005&&Math.abs(this.scaleY-S)>.005)&&(this.scaleX=x,this.scaleY=S,c|=8,r=h=!0)}let f=(parseInt(i.paddingTop)||0)*this.scaleY,d=(parseInt(i.paddingBottom)||0)*this.scaleY;(this.paddingTop!=f||this.paddingBottom!=d)&&(this.paddingTop=f,this.paddingBottom=d,c|=10),this.editorWidth!=t.scrollDOM.clientWidth&&(s.lineWrapping&&(h=!0),this.editorWidth=t.scrollDOM.clientWidth,c|=8);let u=t.scrollDOM.scrollTop*this.scaleY;this.scrollTop!=u&&(this.scrollAnchorHeight=-1,this.scrollTop=u),this.scrolledToBottom=Ri(t.scrollDOM);let g=(this.printing?Qo:Uo)(e,this.paddingTop),p=g.top-this.pixelViewport.top,b=g.bottom-this.pixelViewport.bottom;this.pixelViewport=g;let m=this.pixelViewport.bottom>this.pixelViewport.top&&this.pixelViewport.right>this.pixelViewport.left;if(m!=this.inView&&(this.inView=m,m&&(h=!0)),!this.inView&&!this.scrollTarget)return 0;let w=l.width;if((this.contentDOMWidth!=w||this.editorHeight!=t.scrollDOM.clientHeight)&&(this.contentDOMWidth=l.width,this.editorHeight=t.scrollDOM.clientHeight,c|=8),h){let x=t.docView.measureVisibleLineHeights(this.viewport);if(s.mustRefreshForHeights(x)&&(r=!0),r||s.lineWrapping&&Math.abs(w-this.contentDOMWidth)>s.charWidth){let{lineHeight:S,charWidth:C,textHeight:L}=t.docView.measureTextSize();r=S>0&&s.refresh(o,S,C,L,w/C,x),r&&(t.docView.minWidth=0,c|=8)}p>0&&b>0?a=Math.max(p,b):p<0&&b<0&&(a=Math.min(p,b)),Qe();for(let S of this.viewports){let C=S.from==this.viewport.from?x:t.docView.measureVisibleLineHeights(S);this.heightMap=(r?z.empty().applyChanges(this.stateDeco,Y.empty,this.heightOracle,[new q(0,0,0,t.state.doc.length)]):this.heightMap).updateHeight(s,0,r,new Ys(S.from,C))}mt&&(c|=2)}let v=!this.viewportIsAppropriate(this.viewport,a)||this.scrollTarget&&(this.scrollTarget.range.headthis.viewport.to);return v&&(c&2&&(c|=this.updateScaler()),this.viewport=this.getViewport(a,this.scrollTarget),c|=this.updateForViewport()),(c&2||v)&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(r?[]:this.lineGaps,t)),c|=this.computeVisibleRanges(),this.mustEnforceCursorAssoc&&(this.mustEnforceCursorAssoc=!1,t.docView.enforceCursorAssoc()),c}get visibleTop(){return this.scaler.fromDOM(this.pixelViewport.top)}get visibleBottom(){return this.scaler.fromDOM(this.pixelViewport.bottom)}getViewport(t,e){let i=.5-Math.max(-.5,Math.min(.5,t/1e3/2)),s=this.heightMap,o=this.heightOracle,{visibleTop:r,visibleBottom:l}=this,h=new fe(s.lineAt(r-i*1e3,T.ByHeight,o,0,0).from,s.lineAt(l+(1-i)*1e3,T.ByHeight,o,0,0).to);if(e){let{head:c}=e.range;if(ch.to){let a=Math.min(this.editorHeight,this.pixelViewport.bottom-this.pixelViewport.top),f=s.lineAt(c,T.ByPos,o,0,0),d;e.y=="center"?d=(f.top+f.bottom)/2-a/2:e.y=="start"||e.y=="nearest"&&c=l+Math.max(10,Math.min(i,250)))&&s>r-2*1e3&&o>1,r=s<<1;if(this.defaultTextDirection!=E.LTR&&!i)return[];let l=[],h=(a,f,d,u)=>{if(f-aa&&mm.from>=d.from&&m.to<=d.to&&Math.abs(m.from-a)m.fromw));if(!b){if(fm.from<=f&&m.to>=f)){let m=e.moveToLineBoundary(M.cursor(f),!1,!0).head;m>a&&(f=m)}b=new Je(a,f,this.gapSize(d,a,f,u))}l.push(b)},c=a=>{if(a.lengtha.from&&h(a.from,u,a,f),ge.draw(this,this.heightOracle.lineWrapping))))}computeVisibleRanges(){let t=this.stateDeco;this.lineGaps.length&&(t=t.concat(this.lineGapDeco));let e=[];V.spans(t,this.viewport.from,this.viewport.to,{span(s,o){e.push({from:s,to:o})},point(){}},20);let i=e.length!=this.visibleRanges.length||this.visibleRanges.some((s,o)=>s.from!=e[o].from||s.to!=e[o].to);return this.visibleRanges=e,i?4:0}lineBlockAt(t){return t>=this.viewport.from&&t<=this.viewport.to&&this.viewportLines.find(e=>e.from<=t&&e.to>=t)||zt(this.heightMap.lineAt(t,T.ByPos,this.heightOracle,0,0),this.scaler)}lineBlockAtHeight(t){return t>=this.viewportLines[0].top&&t<=this.viewportLines[this.viewportLines.length-1].bottom&&this.viewportLines.find(e=>e.top<=t&&e.bottom>=t)||zt(this.heightMap.lineAt(this.scaler.fromDOM(t),T.ByHeight,this.heightOracle,0,0),this.scaler)}scrollAnchorAt(t){let e=this.lineBlockAtHeight(t+8);return e.from>=this.viewport.from||this.viewportLines[0].top-t>200?e:this.viewportLines[0]}elementAtHeight(t){return zt(this.heightMap.blockAt(this.scaler.fromDOM(t),this.heightOracle,0,0),this.scaler)}get docHeight(){return this.scaler.toDOM(this.heightMap.height)}get contentHeight(){return this.docHeight+this.paddingTop+this.paddingBottom}}class fe{constructor(t,e){this.from=t,this.to=e}}function Zo(n,t,e){let i=[],s=n,o=0;return V.spans(e,n,t,{span(){},point(r,l){r>s&&(i.push({from:s,to:r}),o+=r-s),s=l}},20),s=1)return t[t.length-1].to;let i=Math.floor(n*e);for(let s=0;;s++){let{from:o,to:r}=t[s],l=r-o;if(i<=l)return o+i;i-=l}}function ue(n,t){let e=0;for(let{from:i,to:s}of n.ranges){if(t<=s){e+=t-i;break}e+=s-i}return e/n.total}function tr(n,t){for(let e of n)if(t(e))return e}const $s={toDOM(n){return n},fromDOM(n){return n},scale:1,eq(n){return n==this}};class yi{constructor(t,e,i){let s=0,o=0,r=0;this.viewports=i.map(({from:l,to:h})=>{let c=e.lineAt(l,T.ByPos,t,0,0).top,a=e.lineAt(h,T.ByPos,t,0,0).bottom;return s+=a-c,{from:l,to:h,top:c,bottom:a,domTop:0,domBottom:0}}),this.scale=(7e6-s)/(e.height-s);for(let l of this.viewports)l.domTop=r+(l.top-o)*this.scale,r=l.domBottom=l.domTop+(l.bottom-l.top),o=l.bottom}toDOM(t){for(let e=0,i=0,s=0;;e++){let o=ee.from==t.viewports[i].from&&e.to==t.viewports[i].to):!1}}function zt(n,t){if(t.scale==1)return n;let e=t.toDOM(n.top),i=t.toDOM(n.bottom);return new G(n.from,n.length,e,i-e,Array.isArray(n._content)?n._content.map(s=>zt(s,t)):n._content)}const ge=k.define({combine:n=>n.join(" ")}),Ze=k.define({combine:n=>n.indexOf(!0)>-1}),ti=vt.newName(),Us=vt.newName(),Qs=vt.newName(),Js={"&light":"."+Us,"&dark":"."+Qs};function ei(n,t,e){return new vt(t,{finish(i){return/&/.test(i)?i.replace(/&\w*/,s=>{if(s=="&")return n;if(!e||!e[s])throw new RangeError(`Unsupported selector: ${s}`);return e[s]}):n+" "+i}})}const er=ei("."+ti,{"&":{position:"relative !important",boxSizing:"border-box","&.cm-focused":{outline:"1px dotted #212121"},display:"flex !important",flexDirection:"column"},".cm-scroller":{display:"flex !important",alignItems:"flex-start !important",fontFamily:"monospace",lineHeight:1.4,height:"100%",overflowX:"auto",position:"relative",zIndex:0,overflowAnchor:"none"},".cm-content":{margin:0,flexGrow:2,flexShrink:0,display:"block",whiteSpace:"pre",wordWrap:"normal",boxSizing:"border-box",minHeight:"100%",padding:"4px 0",outline:"none","&[contenteditable=true]":{WebkitUserModify:"read-write-plaintext-only"}},".cm-lineWrapping":{whiteSpace_fallback:"pre-wrap",whiteSpace:"break-spaces",wordBreak:"break-word",overflowWrap:"anywhere",flexShrink:1},"&light .cm-content":{caretColor:"black"},"&dark .cm-content":{caretColor:"white"},".cm-line":{display:"block",padding:"0 2px 0 6px"},".cm-layer":{position:"absolute",left:0,top:0,contain:"size style","& > *":{position:"absolute"}},"&light .cm-selectionBackground":{background:"#d9d9d9"},"&dark .cm-selectionBackground":{background:"#222"},"&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground":{background:"#d7d4f0"},"&dark.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground":{background:"#233"},".cm-cursorLayer":{pointerEvents:"none"},"&.cm-focused > .cm-scroller > .cm-cursorLayer":{animation:"steps(1) cm-blink 1.2s infinite"},"@keyframes cm-blink":{"0%":{},"50%":{opacity:0},"100%":{}},"@keyframes cm-blink2":{"0%":{},"50%":{opacity:0},"100%":{}},".cm-cursor, .cm-dropCursor":{borderLeft:"1.2px solid black",marginLeft:"-0.6px",pointerEvents:"none"},".cm-cursor":{display:"none"},"&dark .cm-cursor":{borderLeftColor:"#444"},".cm-dropCursor":{position:"absolute"},"&.cm-focused > .cm-scroller > .cm-cursorLayer .cm-cursor":{display:"block"},".cm-iso":{unicodeBidi:"isolate"},".cm-announced":{position:"fixed",top:"-10000px"},"@media print":{".cm-announced":{display:"none"}},"&light .cm-activeLine":{backgroundColor:"#cceeff44"},"&dark .cm-activeLine":{backgroundColor:"#99eeff33"},"&light .cm-specialChar":{color:"red"},"&dark .cm-specialChar":{color:"#f78"},".cm-gutters":{flexShrink:0,display:"flex",height:"100%",boxSizing:"border-box",insetInlineStart:0,zIndex:200},"&light .cm-gutters":{backgroundColor:"#f5f5f5",color:"#6c6c6c",borderRight:"1px solid #ddd"},"&dark .cm-gutters":{backgroundColor:"#333338",color:"#ccc"},".cm-gutter":{display:"flex !important",flexDirection:"column",flexShrink:0,boxSizing:"border-box",minHeight:"100%",overflow:"hidden"},".cm-gutterElement":{boxSizing:"border-box"},".cm-lineNumbers .cm-gutterElement":{padding:"0 3px 0 5px",minWidth:"20px",textAlign:"right",whiteSpace:"nowrap"},"&light .cm-activeLineGutter":{backgroundColor:"#e2f2ff"},"&dark .cm-activeLineGutter":{backgroundColor:"#222227"},".cm-panels":{boxSizing:"border-box",position:"sticky",left:0,right:0,zIndex:300},"&light .cm-panels":{backgroundColor:"#f5f5f5",color:"black"},"&light .cm-panels-top":{borderBottom:"1px solid #ddd"},"&light .cm-panels-bottom":{borderTop:"1px solid #ddd"},"&dark .cm-panels":{backgroundColor:"#333338",color:"white"},".cm-tab":{display:"inline-block",overflow:"hidden",verticalAlign:"bottom"},".cm-widgetBuffer":{verticalAlign:"text-top",height:"1em",width:0,display:"inline"},".cm-placeholder":{color:"#888",display:"inline-block",verticalAlign:"top"},".cm-highlightSpace:before":{content:"attr(data-display)",position:"absolute",pointerEvents:"none",color:"#888"},".cm-highlightTab":{backgroundImage:`url('data:image/svg+xml,')`,backgroundSize:"auto 100%",backgroundPosition:"right 90%",backgroundRepeat:"no-repeat"},".cm-trailingSpace":{backgroundColor:"#ff332255"},".cm-button":{verticalAlign:"middle",color:"inherit",fontSize:"70%",padding:".2em 1em",borderRadius:"1px"},"&light .cm-button":{backgroundImage:"linear-gradient(#eff1f5, #d9d9df)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#b4b4b4, #d0d3d6)"}},"&dark .cm-button":{backgroundImage:"linear-gradient(#393939, #111)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#111, #333)"}},".cm-textfield":{verticalAlign:"middle",color:"inherit",fontSize:"70%",border:"1px solid silver",padding:".2em .5em"},"&light .cm-textfield":{backgroundColor:"white"},"&dark .cm-textfield":{border:"1px solid #555",backgroundColor:"inherit"}},Js),ir={childList:!0,characterData:!0,subtree:!0,attributes:!0,characterDataOldValue:!0},ii=y.ie&&y.ie_version<=11;class sr{constructor(t){this.view=t,this.active=!1,this.editContext=null,this.selectionRange=new jn,this.selectionChanged=!1,this.delayedFlush=-1,this.resizeTimeout=-1,this.queue=[],this.delayedAndroidKey=null,this.flushingAndroidKey=-1,this.lastChange=0,this.scrollTargets=[],this.intersection=null,this.resizeScroll=null,this.intersecting=!1,this.gapIntersection=null,this.gaps=[],this.printQuery=null,this.parentCheck=-1,this.dom=t.contentDOM,this.observer=new MutationObserver(e=>{for(let i of e)this.queue.push(i);(y.ie&&y.ie_version<=11||y.ios&&t.composing)&&e.some(i=>i.type=="childList"&&i.removedNodes.length||i.type=="characterData"&&i.oldValue.length>i.target.nodeValue.length)?this.flushSoon():this.flush()}),window.EditContext&&t.constructor.EDIT_CONTEXT!==!1&&!(y.chrome&&y.chrome_version<126)&&(this.editContext=new or(t),t.state.facet(rt)&&(t.contentDOM.editContext=this.editContext.editContext)),ii&&(this.onCharData=e=>{this.queue.push({target:e.target,type:"characterData",oldValue:e.prevValue}),this.flushSoon()}),this.onSelectionChange=this.onSelectionChange.bind(this),this.onResize=this.onResize.bind(this),this.onPrint=this.onPrint.bind(this),this.onScroll=this.onScroll.bind(this),window.matchMedia&&(this.printQuery=window.matchMedia("print")),typeof ResizeObserver=="function"&&(this.resizeScroll=new ResizeObserver(()=>{var e;((e=this.view.docView)===null||e===void 0?void 0:e.lastUpdate){this.parentCheck<0&&(this.parentCheck=setTimeout(this.listenForScroll.bind(this),1e3)),e.length>0&&e[e.length-1].intersectionRatio>0!=this.intersecting&&(this.intersecting=!this.intersecting,this.intersecting!=this.view.inView&&this.onScrollChanged(document.createEvent("Event")))},{threshold:[0,.001]}),this.intersection.observe(this.dom),this.gapIntersection=new IntersectionObserver(e=>{e.length>0&&e[e.length-1].intersectionRatio>0&&this.onScrollChanged(document.createEvent("Event"))},{})),this.listenForScroll(),this.readSelectionRange()}onScrollChanged(t){this.view.inputState.runHandlers("scroll",t),this.intersecting&&this.view.measure()}onScroll(t){this.intersecting&&this.flush(!1),this.editContext&&this.view.requestMeasure(this.editContext.measureReq),this.onScrollChanged(t)}onResize(){this.resizeTimeout<0&&(this.resizeTimeout=setTimeout(()=>{this.resizeTimeout=-1,this.view.requestMeasure()},50))}onPrint(t){(t.type=="change"||!t.type)&&!t.matches||(this.view.viewState.printing=!0,this.view.measure(),setTimeout(()=>{this.view.viewState.printing=!1,this.view.requestMeasure()},500))}updateGaps(t){if(this.gapIntersection&&(t.length!=this.gaps.length||this.gaps.some((e,i)=>e!=t[i]))){this.gapIntersection.disconnect();for(let e of t)this.gapIntersection.observe(e);this.gaps=t}}onSelectionChange(t){let e=this.selectionChanged;if(!this.readSelectionRange()||this.delayedAndroidKey)return;let{view:i}=this,s=this.selectionRange;if(i.state.facet(rt)?i.root.activeElement!=this.dom:!Zt(i.dom,s))return;let o=s.anchorNode&&i.docView.nearest(s.anchorNode);if(o&&o.ignoreEvent(t)){e||(this.selectionChanged=!1);return}(y.ie&&y.ie_version<=11||y.android&&y.chrome)&&!i.state.selection.main.empty&&s.focusNode&&Pt(s.focusNode,s.focusOffset,s.anchorNode,s.anchorOffset)?this.flushSoon():this.flush(!1)}readSelectionRange(){let{view:t}=this,e=Bt(t.root);if(!e)return!1;let i=y.safari&&t.root.nodeType==11&&zn(this.dom.ownerDocument)==this.dom&&nr(this.view,e)||e;if(!i||this.selectionRange.eq(i))return!1;let s=Zt(this.dom,i);return s&&!this.selectionChanged&&t.inputState.lastFocusTime>Date.now()-200&&t.inputState.lastTouchTime{let o=this.delayedAndroidKey;o&&(this.clearDelayedAndroidKey(),this.view.inputState.lastKeyCode=o.keyCode,this.view.inputState.lastKeyTime=Date.now(),!this.flush()&&o.force&&Mt(this.dom,o.key,o.keyCode))};this.flushingAndroidKey=this.view.win.requestAnimationFrame(s)}(!this.delayedAndroidKey||t=="Enter")&&(this.delayedAndroidKey={key:t,keyCode:e,force:this.lastChange{this.delayedFlush=-1,this.flush()}))}forceFlush(){this.delayedFlush>=0&&(this.view.win.cancelAnimationFrame(this.delayedFlush),this.delayedFlush=-1),this.flush()}pendingRecords(){for(let t of this.observer.takeRecords())this.queue.push(t);return this.queue}processRecords(){let t=this.pendingRecords();t.length&&(this.queue=[]);let e=-1,i=-1,s=!1;for(let o of t){let r=this.readMutation(o);r&&(r.typeOver&&(s=!0),e==-1?{from:e,to:i}=r:(e=Math.min(r.from,e),i=Math.max(r.to,i)))}return{from:e,to:i,typeOver:s}}readChange(){let{from:t,to:e,typeOver:i}=this.processRecords(),s=this.selectionChanged&&Zt(this.dom,this.selectionRange);if(t<0&&!s)return null;t>-1&&(this.lastChange=Date.now()),this.view.inputState.lastFocusTime=0,this.selectionChanged=!1;let o=new Mo(this.view,t,e,i);return this.view.docView.domChanged={newSel:o.newSel?o.newSel.main:null},o}flush(t=!0){if(this.delayedFlush>=0||this.delayedAndroidKey)return!1;t&&this.readSelectionRange();let e=this.readChange();if(!e)return this.view.requestMeasure(),!1;let i=this.view.state,s=ks(this.view,e);return this.view.state==i&&(e.domChanged||e.newSel&&!e.newSel.main.eq(this.view.state.selection.main))&&this.view.update([]),s}readMutation(t){let e=this.view.docView.nearest(t.target);if(!e||e.ignoreMutation(t))return null;if(e.markDirty(t.type=="attributes"),t.type=="attributes"&&(e.flags|=4),t.type=="childList"){let i=Zs(e,t.previousSibling||t.target.previousSibling,-1),s=Zs(e,t.nextSibling||t.target.nextSibling,1);return{from:i?e.posAfter(i):e.posAtStart,to:s?e.posBefore(s):e.posAtEnd,typeOver:!1}}else return t.type=="characterData"?{from:e.posAtStart,to:e.posAtEnd,typeOver:t.target.nodeValue==t.oldValue}:null}setWindow(t){t!=this.win&&(this.removeWindowListeners(this.win),this.win=t,this.addWindowListeners(this.win))}addWindowListeners(t){t.addEventListener("resize",this.onResize),this.printQuery?this.printQuery.addEventListener?this.printQuery.addEventListener("change",this.onPrint):this.printQuery.addListener(this.onPrint):t.addEventListener("beforeprint",this.onPrint),t.addEventListener("scroll",this.onScroll),t.document.addEventListener("selectionchange",this.onSelectionChange)}removeWindowListeners(t){t.removeEventListener("scroll",this.onScroll),t.removeEventListener("resize",this.onResize),this.printQuery?this.printQuery.removeEventListener?this.printQuery.removeEventListener("change",this.onPrint):this.printQuery.removeListener(this.onPrint):t.removeEventListener("beforeprint",this.onPrint),t.document.removeEventListener("selectionchange",this.onSelectionChange)}update(t){this.editContext&&(this.editContext.update(t),t.startState.facet(rt)!=t.state.facet(rt)&&(t.view.contentDOM.editContext=t.state.facet(rt)?this.editContext.editContext:null))}destroy(){var t,e,i;this.stop(),(t=this.intersection)===null||t===void 0||t.disconnect(),(e=this.gapIntersection)===null||e===void 0||e.disconnect(),(i=this.resizeScroll)===null||i===void 0||i.disconnect();for(let s of this.scrollTargets)s.removeEventListener("scroll",this.onScroll);this.removeWindowListeners(this.win),clearTimeout(this.parentCheck),clearTimeout(this.resizeTimeout),this.win.cancelAnimationFrame(this.delayedFlush),this.win.cancelAnimationFrame(this.flushingAndroidKey),this.editContext&&(this.view.contentDOM.editContext=null,this.editContext.destroy())}}function Zs(n,t,e){for(;t;){let i=O.get(t);if(i&&i.parent==n)return i;let s=t.parentNode;t=s!=n.dom?s:e>0?t.nextSibling:t.previousSibling}return null}function tn(n,t){let e=t.startContainer,i=t.startOffset,s=t.endContainer,o=t.endOffset,r=n.docView.domAtPos(n.state.selection.main.anchor);return Pt(r.node,r.offset,s,o)&&([e,i,s,o]=[s,o,e,i]),{anchorNode:e,anchorOffset:i,focusNode:s,focusOffset:o}}function nr(n,t){if(t.getComposedRanges){let s=t.getComposedRanges(n.root)[0];if(s)return tn(n,s)}let e=null;function i(s){s.preventDefault(),s.stopImmediatePropagation(),e=s.getTargetRanges()[0]}return n.contentDOM.addEventListener("beforeinput",i,!0),n.dom.ownerDocument.execCommand("indent"),n.contentDOM.removeEventListener("beforeinput",i,!0),e?tn(n,e):null}class or{constructor(t){this.from=0,this.to=0,this.pendingContextChange=null,this.handlers=Object.create(null),this.resetRange(t.state);let e=this.editContext=new window.EditContext({text:t.state.doc.sliceString(this.from,this.to),selectionStart:this.toContextPos(Math.max(this.from,Math.min(this.to,t.state.selection.main.anchor))),selectionEnd:this.toContextPos(t.state.selection.main.head)});this.handlers.textupdate=i=>{let{anchor:s}=t.state.selection.main,o={from:this.toEditorPos(i.updateRangeStart),to:this.toEditorPos(i.updateRangeEnd),insert:Y.of(i.text.split(` +`))};o.from==this.from&&sthis.to&&(o.to=s),!(o.from==o.to&&!o.insert.length)&&(this.pendingContextChange=o,t.state.readOnly||$e(t,o,M.single(this.toEditorPos(i.selectionStart),this.toEditorPos(i.selectionEnd))),this.pendingContextChange&&(this.revertPending(t.state),this.setSelection(t.state)))},this.handlers.characterboundsupdate=i=>{let s=[],o=null;for(let r=this.toEditorPos(i.rangeStart),l=this.toEditorPos(i.rangeEnd);r{let s=[];for(let o of i.getTextFormats()){let r=o.underlineStyle,l=o.underlineThickness;if(r!="None"&&l!="None"){let h=`text-decoration: underline ${r=="Dashed"?"dashed ":r=="Squiggle"?"wavy ":""}${l=="Thin"?1:2}px`;s.push(R.mark({attributes:{style:h}}).range(this.toEditorPos(o.rangeStart),this.toEditorPos(o.rangeEnd)))}}t.dispatch({effects:as.of(R.set(s))})},this.handlers.compositionstart=()=>{t.inputState.composing<0&&(t.inputState.composing=0,t.inputState.compositionFirstChange=!0)},this.handlers.compositionend=()=>{t.inputState.composing=-1,t.inputState.compositionFirstChange=null};for(let i in this.handlers)e.addEventListener(i,this.handlers[i]);this.measureReq={read:i=>{this.editContext.updateControlBounds(i.contentDOM.getBoundingClientRect());let s=Bt(i.root);s&&s.rangeCount&&this.editContext.updateSelectionBounds(s.getRangeAt(0).getBoundingClientRect())}}}applyEdits(t){let e=0,i=!1,s=this.pendingContextChange;return t.changes.iterChanges((o,r,l,h,c)=>{if(i)return;let a=c.length-(r-o);if(s&&r>=s.to)if(s.from==o&&s.to==r&&s.insert.eq(c)){s=this.pendingContextChange=null,e+=a,this.to+=a;return}else s=null,this.revertPending(t.state);if(o+=e,r+=e,r<=this.from)this.from+=a,this.to+=a;else if(othis.to||this.to-this.from+c.length>3e4){i=!0;return}this.editContext.updateText(this.toContextPos(o),this.toContextPos(r),c.toString()),this.to+=a}e+=a}),s&&!i&&this.revertPending(t.state),!i}update(t){let e=this.pendingContextChange;!this.applyEdits(t)||!this.rangeIsValid(t.state)?(this.pendingContextChange=null,this.resetRange(t.state),this.editContext.updateText(0,this.editContext.text.length,t.state.doc.sliceString(this.from,this.to)),this.setSelection(t.state)):(t.docChanged||t.selectionSet||e)&&this.setSelection(t.state),(t.geometryChanged||t.docChanged||t.selectionSet)&&t.view.requestMeasure(this.measureReq)}resetRange(t){let{head:e}=t.selection.main;this.from=Math.max(0,e-1e4),this.to=Math.min(t.doc.length,e+1e4)}revertPending(t){let e=this.pendingContextChange;this.pendingContextChange=null,this.editContext.updateText(this.toContextPos(e.from),this.toContextPos(e.from+e.insert.length),t.doc.sliceString(e.from,e.to))}setSelection(t){let{main:e}=t.selection,i=this.toContextPos(Math.max(this.from,Math.min(this.to,e.anchor))),s=this.toContextPos(e.head);(this.editContext.selectionStart!=i||this.editContext.selectionEnd!=s)&&this.editContext.updateSelection(i,s)}rangeIsValid(t){let{head:e}=t.selection.main;return!(this.from>0&&e-this.from<500||this.to1e4*3)}toEditorPos(t){return t+this.from}toContextPos(t){return t-this.from}destroy(){for(let t in this.handlers)this.editContext.removeEventListener(t,this.handlers[t])}}class A{get state(){return this.viewState.state}get viewport(){return this.viewState.viewport}get visibleRanges(){return this.viewState.visibleRanges}get inView(){return this.viewState.inView}get composing(){return this.inputState.composing>0}get compositionStarted(){return this.inputState.composing>=0}get root(){return this._root}get win(){return this.dom.ownerDocument.defaultView||window}constructor(t={}){var e;this.plugins=[],this.pluginMap=new Map,this.editorAttrs={},this.contentAttrs={},this.bidiCache=[],this.destroyed=!1,this.updateState=2,this.measureScheduled=-1,this.measureRequests=[],this.contentDOM=document.createElement("div"),this.scrollDOM=document.createElement("div"),this.scrollDOM.tabIndex=-1,this.scrollDOM.className="cm-scroller",this.scrollDOM.appendChild(this.contentDOM),this.announceDOM=document.createElement("div"),this.announceDOM.className="cm-announced",this.announceDOM.setAttribute("aria-live","polite"),this.dom=document.createElement("div"),this.dom.appendChild(this.announceDOM),this.dom.appendChild(this.scrollDOM),t.parent&&t.parent.appendChild(this.dom);let{dispatch:i}=t;this.dispatchTransactions=t.dispatchTransactions||i&&(s=>s.forEach(o=>i(o,this)))||(s=>this.update(s)),this.dispatch=this.dispatch.bind(this),this._root=t.root||Yn(t.parent)||document,this.viewState=new Gs(t.state||Lt.create(t)),t.scrollTo&&t.scrollTo.is(ne)&&(this.viewState.scrollTarget=t.scrollTo.value.clip(this.viewState.state)),this.plugins=this.state.facet(Nt).map(s=>new qe(s));for(let s of this.plugins)s.update(this);this.observer=new sr(this),this.inputState=new To(this),this.inputState.ensureHandlers(this.plugins),this.docView=new ms(this),this.mountStyles(),this.updateAttrs(),this.updateState=0,this.requestMeasure(),!((e=document.fonts)===null||e===void 0)&&e.ready&&document.fonts.ready.then(()=>this.requestMeasure())}dispatch(...t){let e=t.length==1&&t[0]instanceof Pn?t:t.length==1&&Array.isArray(t[0])?t[0]:[this.state.update(...t)];this.dispatchTransactions(e,this)}update(t){if(this.updateState!=0)throw new Error("Calls to EditorView.update are not allowed while an update is in progress");let e=!1,i=!1,s,o=this.state;for(let d of t){if(d.startState!=o)throw new RangeError("Trying to update state with a transaction that doesn't start from the previous state.");o=d.state}if(this.destroyed){this.viewState.state=o;return}let r=this.hasFocus,l=0,h=null;t.some(d=>d.annotation(Ws))?(this.inputState.notifiedFocused=r,l=1):r!=this.inputState.notifiedFocused&&(this.inputState.notifiedFocused=r,h=zs(o,r),h||(l=1));let c=this.observer.delayedAndroidKey,a=null;if(c?(this.observer.clearDelayedAndroidKey(),a=this.observer.readChange(),(a&&!this.state.doc.eq(o.doc)||!this.state.selection.eq(o.selection))&&(a=null)):this.observer.clear(),o.facet(Lt.phrases)!=this.state.facet(Lt.phrases))return this.setState(o);s=$t.create(this,o,t),s.flags|=l;let f=this.viewState.scrollTarget;try{this.updateState=2;for(let d of t){if(f&&(f=f.map(d.changes)),d.scrollIntoView){let{main:u}=d.state.selection;f=new Rt(u.empty?u:M.cursor(u.head,u.head>u.anchor?-1:1))}for(let u of d.effects)u.is(ne)&&(f=u.value.clip(this.state))}this.viewState.update(s,f),this.bidiCache=Ce.update(this.bidiCache,s.changes),s.empty||(this.updatePlugins(s),this.inputState.update(s)),e=this.docView.update(s),this.state.facet(Ft)!=this.styleModules&&this.mountStyles(),i=this.updateAttrs(),this.showAnnouncements(t),this.docView.updateSelection(e,t.some(d=>d.isUserEvent("select.pointer")))}finally{this.updateState=0}if(s.startState.facet(ge)!=s.state.facet(ge)&&(this.viewState.mustMeasureContent=!0),(e||i||f||this.viewState.mustEnforceCursorAssoc||this.viewState.mustMeasureContent)&&this.requestMeasure(),e&&this.docViewUpdate(),!s.empty)for(let d of this.state.facet(Ke))try{d(s)}catch(u){_(this.state,u,"update listener")}(h||a)&&Promise.resolve().then(()=>{h&&this.state==h.startState&&this.dispatch(h),a&&!ks(this,a)&&c.force&&Mt(this.contentDOM,c.key,c.keyCode)})}setState(t){if(this.updateState!=0)throw new Error("Calls to EditorView.setState are not allowed while an update is in progress");if(this.destroyed){this.viewState.state=t;return}this.updateState=2;let e=this.hasFocus;try{for(let i of this.plugins)i.destroy(this);this.viewState=new Gs(t),this.plugins=t.facet(Nt).map(i=>new qe(i)),this.pluginMap.clear();for(let i of this.plugins)i.update(this);this.docView.destroy(),this.docView=new ms(this),this.inputState.ensureHandlers(this.plugins),this.mountStyles(),this.updateAttrs(),this.bidiCache=[]}finally{this.updateState=0}e&&this.focus(),this.requestMeasure()}updatePlugins(t){let e=t.startState.facet(Nt),i=t.state.facet(Nt);if(e!=i){let s=[];for(let o of i){let r=e.indexOf(o);if(r<0)s.push(new qe(o));else{let l=this.plugins[r];l.mustUpdate=t,s.push(l)}}for(let o of this.plugins)o.mustUpdate!=t&&o.destroy(this);this.plugins=s,this.pluginMap.clear()}else for(let s of this.plugins)s.mustUpdate=t;for(let s=0;s-1&&this.win.cancelAnimationFrame(this.measureScheduled),this.observer.delayedAndroidKey){this.measureScheduled=-1,this.requestMeasure();return}this.measureScheduled=0,t&&this.observer.forceFlush();let e=null,i=this.scrollDOM,s=i.scrollTop*this.scaleY,{scrollAnchorPos:o,scrollAnchorHeight:r}=this.viewState;Math.abs(s-this.viewState.scrollTop)>1&&(r=-1),this.viewState.scrollAnchorHeight=-1;try{for(let l=0;;l++){if(r<0)if(Ri(i))o=-1,r=this.viewState.heightMap.height;else{let u=this.viewState.scrollAnchorAt(s);o=u.from,r=u.top}this.updateState=1;let h=this.viewState.measure(this);if(!h&&!this.measureRequests.length&&this.viewState.scrollTarget==null)break;if(l>5){console.warn(this.measureRequests.length?"Measure loop restarted more than 5 times":"Viewport failed to stabilize");break}let c=[];h&4||([this.measureRequests,c]=[c,this.measureRequests]);let a=c.map(u=>{try{return u.read(this)}catch(g){return _(this.state,g),en}}),f=$t.create(this,this.state,[]),d=!1;f.flags|=h,e?e.flags|=h:e=f,this.updateState=2,f.empty||(this.updatePlugins(f),this.inputState.update(f),this.updateAttrs(),d=this.docView.update(f),d&&this.docViewUpdate());for(let u=0;u1||g<-1){s=s+g,i.scrollTop=s/this.scaleY,r=-1;continue}}break}}}finally{this.updateState=0,this.measureScheduled=-1}if(e&&!e.empty)for(let l of this.state.facet(Ke))l(e)}get themeClasses(){return ti+" "+(this.state.facet(Ze)?Qs:Us)+" "+this.state.facet(ge)}updateAttrs(){let t=sn(this,cs,{class:"cm-editor"+(this.hasFocus?" cm-focused ":" ")+this.themeClasses}),e={spellcheck:"false",autocorrect:"off",autocapitalize:"off",translate:"no",contenteditable:this.state.facet(rt)?"true":"false",class:"cm-content",style:`${y.tabSize}: ${this.state.tabSize}`,role:"textbox","aria-multiline":"true"};this.state.readOnly&&(e["aria-readonly"]="true"),sn(this,oe,e);let i=this.observer.ignore(()=>{let s=He(this.contentDOM,this.contentAttrs,e),o=He(this.dom,this.editorAttrs,t);return s||o});return this.editorAttrs=t,this.contentAttrs=e,i}showAnnouncements(t){let e=!0;for(let i of t)for(let s of i.effects)if(s.is(A.announce)){e&&(this.announceDOM.textContent=""),e=!1;let o=this.announceDOM.appendChild(document.createElement("div"));o.textContent=s.value}}mountStyles(){this.styleModules=this.state.facet(Ft);let t=this.state.facet(A.cspNonce);vt.mount(this.root,this.styleModules.concat(er).reverse(),t?{nonce:t}:void 0)}readMeasured(){if(this.updateState==2)throw new Error("Reading the editor layout isn't allowed during an update");this.updateState==0&&this.measureScheduled>-1&&this.measure(!1)}requestMeasure(t){if(this.measureScheduled<0&&(this.measureScheduled=this.win.requestAnimationFrame(()=>this.measure())),t){if(this.measureRequests.indexOf(t)>-1)return;if(t.key!=null){for(let e=0;ei.spec==t)||null),e&&e.update(this).value}get documentTop(){return this.contentDOM.getBoundingClientRect().top+this.viewState.paddingTop}get documentPadding(){return{top:this.viewState.paddingTop,bottom:this.viewState.paddingBottom}}get scaleX(){return this.viewState.scaleX}get scaleY(){return this.viewState.scaleY}elementAtHeight(t){return this.readMeasured(),this.viewState.elementAtHeight(t)}lineBlockAtHeight(t){return this.readMeasured(),this.viewState.lineBlockAtHeight(t)}get viewportLineBlocks(){return this.viewState.viewportLines}lineBlockAt(t){return this.viewState.lineBlockAt(t)}get contentHeight(){return this.viewState.contentHeight}moveByChar(t,e,i){return Ge(this,t,Cs(this,t,e,i))}moveByGroup(t,e){return Ge(this,t,Cs(this,t,e,i=>xo(this,t.head,i)))}visualLineSide(t,e){let i=this.bidiSpans(t),s=this.textDirectionAt(t.from),o=i[e?i.length-1:0];return M.cursor(o.side(e,s)+t.from,o.forward(!e,s)?1:-1)}moveToLineBoundary(t,e,i=!0){return wo(this,t,e,i)}moveVertically(t,e,i){return Ge(this,t,vo(this,t,e,i))}domAtPos(t){return this.docView.domAtPos(t)}posAtDOM(t,e=0){return this.docView.posFromDOM(t,e)}posAtCoords(t,e=!0){return this.readMeasured(),vs(this,t,e)}coordsAtPos(t,e=1){this.readMeasured();let i=this.docView.coordsAt(t,e);if(!i||i.left==i.right)return i;let s=this.state.doc.lineAt(t),o=this.bidiSpans(s),r=o[tt.find(o,t-s.from,-1,e)];return Ht(i,r.dir==E.LTR==e>0)}coordsForChar(t){return this.readMeasured(),this.docView.coordsForChar(t)}get defaultCharacterWidth(){return this.viewState.heightOracle.charWidth}get defaultLineHeight(){return this.viewState.heightOracle.lineHeight}get textDirection(){return this.viewState.defaultTextDirection}textDirectionAt(t){return!this.state.facet(rs)||tthis.viewport.to?this.textDirection:(this.readMeasured(),this.docView.textDirectionAt(t))}get lineWrapping(){return this.viewState.heightOracle.lineWrapping}bidiSpans(t){if(t.length>rr)return Qi(t.length);let e=this.textDirectionAt(t.from),i;for(let o of this.bidiCache)if(o.from==t.from&&o.dir==e&&(o.fresh||$i(o.isolates,i=us(this,t))))return o.order;i||(i=us(this,t));let s=Ui(t.text,e,i);return this.bidiCache.push(new Ce(t.from,t.to,e,i,!0,s)),s}get hasFocus(){var t;return(this.dom.ownerDocument.hasFocus()||y.safari&&((t=this.inputState)===null||t===void 0?void 0:t.lastContextMenu)>Date.now()-3e4)&&this.root.activeElement==this.contentDOM}focus(){this.observer.ignore(()=>{Di(this.contentDOM),this.docView.updateSelection()})}setRoot(t){this._root!=t&&(this._root=t,this.observer.setWindow((t.nodeType==9?t:t.ownerDocument).defaultView||window),this.mountStyles())}destroy(){this.root.activeElement==this.contentDOM&&this.contentDOM.blur();for(let t of this.plugins)t.destroy(this);this.plugins=[],this.inputState.destroy(),this.docView.destroy(),this.dom.remove(),this.observer.destroy(),this.measureScheduled>-1&&this.win.cancelAnimationFrame(this.measureScheduled),this.destroyed=!0}static scrollIntoView(t,e={}){return ne.of(new Rt(typeof t=="number"?M.cursor(t):t,e.y,e.x,e.yMargin,e.xMargin))}scrollSnapshot(){let{scrollTop:t,scrollLeft:e}=this.scrollDOM,i=this.viewState.scrollAnchorAt(t);return ne.of(new Rt(M.cursor(i.from),"start","start",i.top-t,e,!0))}setTabFocusMode(t){t==null?this.inputState.tabFocusMode=this.inputState.tabFocusMode<0?0:-1:typeof t=="boolean"?this.inputState.tabFocusMode=t?0:-1:this.inputState.tabFocusMode!=0&&(this.inputState.tabFocusMode=Date.now()+t)}static domEventHandlers(t){return P.define(()=>({}),{eventHandlers:t})}static domEventObservers(t){return P.define(()=>({}),{eventObservers:t})}static theme(t,e){let i=vt.newName(),s=[ge.of(i),Ft.of(ei(`.${i}`,t))];return e&&e.dark&&s.push(Ze.of(!0)),s}static baseTheme(t){return Ae.lowest(Ft.of(ei("."+ti,t,Js)))}static findFromDOM(t){var e;let i=t.querySelector(".cm-content"),s=i&&O.get(i)||O.get(t);return((e=s?.rootView)===null||e===void 0?void 0:e.view)||null}}A.styleModule=Ft,A.inputHandler=ns,A.scrollHandler=hs,A.focusChangeEffect=os,A.perLineTextDirection=rs,A.exceptionSink=ss,A.updateListener=Ke,A.editable=rt,A.mouseSelectionStyle=is,A.dragMovesSelection=es,A.clickAddsSelectionRange=ts,A.decorations=Vt,A.outerDecorations=fs,A.atomicRanges=je,A.bidiIsolatedRanges=ds,A.scrollMargins=gs,A.darkTheme=Ze,A.cspNonce=k.define({combine:n=>n.length?n[0]:""}),A.contentAttributes=oe,A.editorAttributes=cs,A.lineWrapping=A.contentAttributes.of({class:"cm-lineWrapping"}),A.announce=xt.define();const rr=4096,en={};class Ce{constructor(t,e,i,s,o,r){this.from=t,this.to=e,this.dir=i,this.isolates=s,this.fresh=o,this.order=r}static update(t,e){if(e.empty&&!t.some(o=>o.fresh))return t;let i=[],s=t.length?t[t.length-1].dir:E.LTR;for(let o=Math.max(0,t.length-10);o=0;s--){let o=i[s],r=typeof o=="function"?o(n):o;r&&Pe(r,e)}return e}const lr=y.mac?"mac":y.windows?"win":y.linux?"linux":"key";function hr(n,t){const e=n.split(/-(?!$)/);let i=e[e.length-1];i=="Space"&&(i=" ");let s,o,r,l;for(let h=0;hi.concat(s),[]))),e}function cr(n,t,e){return ln(rn(n.state),t,n,e)}let lt=null;const fr=4e3;function dr(n,t=lr){let e=Object.create(null),i=Object.create(null),s=(r,l)=>{let h=i[r];if(h==null)i[r]=l;else if(h!=l)throw new Error("Key binding "+r+" is used both as a regular binding and as a multi-stroke prefix")},o=(r,l,h,c,a)=>{var f,d;let u=e[r]||(e[r]=Object.create(null)),g=l.split(/ (?!$)/).map(m=>hr(m,t));for(let m=1;m{let x=lt={view:v,prefix:w,scope:r};return setTimeout(()=>{lt==x&&(lt=null)},fr),!0}]})}let p=g.join(" ");s(p,!1);let b=u[p]||(u[p]={preventDefault:!1,stopPropagation:!1,run:((d=(f=u._any)===null||f===void 0?void 0:f.run)===null||d===void 0?void 0:d.slice())||[]});h&&b.run.push(h),c&&(b.preventDefault=!0),a&&(b.stopPropagation=!0)};for(let r of n){let l=r.scope?r.scope.split(" "):["editor"];if(r.any)for(let c of l){let a=e[c]||(e[c]=Object.create(null));a._any||(a._any={preventDefault:!1,stopPropagation:!1,run:[]});let{any:f}=r;for(let d in a)a[d].run.push(u=>f(u,si))}let h=r[t]||r.key;if(h)for(let c of l)o(c,h,r.run,r.preventDefault,r.stopPropagation),r.shift&&o(c,"Shift-"+h,r.shift,r.preventDefault,r.stopPropagation)}return e}let si=null;function ln(n,t,e,i){si=t;let s=Vn(t),o=Si(s,0),r=Hn(o)==s.length&&s!=" ",l="",h=!1,c=!1,a=!1;lt&<.view==e&<.scope==i&&(l=lt.prefix+" ",Os.indexOf(t.keyCode)<0&&(c=!0,lt=null));let f=new Set,d=b=>{if(b){for(let m of b.run)if(!f.has(m)&&(f.add(m),m(e)))return b.stopPropagation&&(a=!0),!0;b.preventDefault&&(b.stopPropagation&&(a=!0),c=!0)}return!1},u=n[i],g,p;return u&&(d(u[l+pe(s,t,!r)])?h=!0:r&&(t.altKey||t.metaKey||t.ctrlKey)&&!(y.windows&&t.ctrlKey&&t.altKey)&&(g=Fn[t.keyCode])&&g!=s?(d(u[l+pe(g,t,!0)])||t.shiftKey&&(p=Wn[t.keyCode])!=s&&p!=g&&d(u[l+pe(p,t,!1)]))&&(h=!0):r&&t.shiftKey&&d(u[l+pe(s,t,!0)])&&(h=!0),!h&&d(u._any)&&(h=!0)),c&&(h=!0),h&&a&&t.stopPropagation(),si=null,h}class Ot{constructor(t,e,i,s,o){this.className=t,this.left=e,this.top=i,this.width=s,this.height=o}draw(){let t=document.createElement("div");return t.className=this.className,this.adjust(t),t}update(t,e){return e.className!=this.className?!1:(this.adjust(t),!0)}adjust(t){t.style.left=this.left+"px",t.style.top=this.top+"px",this.width!=null&&(t.style.width=this.width+"px"),t.style.height=this.height+"px"}eq(t){return this.left==t.left&&this.top==t.top&&this.width==t.width&&this.height==t.height&&this.className==t.className}static forRange(t,e,i){if(i.empty){let s=t.coordsAtPos(i.head,i.assoc||1);if(!s)return[];let o=hn(t);return[new Ot(e,s.left-o.left,s.top-o.top,null,s.bottom-s.top)]}else return ur(t,e,i)}}function hn(n){let t=n.scrollDOM.getBoundingClientRect();return{left:(n.textDirection==E.LTR?t.left:t.right-n.scrollDOM.clientWidth*n.scaleX)-n.scrollDOM.scrollLeft*n.scaleX,top:t.top-n.scrollDOM.scrollTop*n.scaleY}}function an(n,t,e,i){let s=n.coordsAtPos(t,e*2);if(!s)return i;let o=n.dom.getBoundingClientRect(),r=(s.top+s.bottom)/2,l=n.posAtCoords({x:o.left+1,y:r}),h=n.posAtCoords({x:o.right-1,y:r});return l==null||h==null?i:{from:Math.max(i.from,Math.min(l,h)),to:Math.min(i.to,Math.max(l,h))}}function ur(n,t,e){if(e.to<=n.viewport.from||e.from>=n.viewport.to)return[];let i=Math.max(e.from,n.viewport.from),s=Math.min(e.to,n.viewport.to),o=n.textDirection==E.LTR,r=n.contentDOM,l=r.getBoundingClientRect(),h=hn(n),c=r.querySelector(".cm-line"),a=c&&window.getComputedStyle(c),f=l.left+(a?parseInt(a.paddingLeft)+Math.min(0,parseInt(a.textIndent)):0),d=l.right-(a?parseInt(a.paddingRight):0),u=Xe(n,i),g=Xe(n,s),p=u.type==F.Text?u:null,b=g.type==F.Text?g:null;if(p&&(n.lineWrapping||u.widgetLineBreaks)&&(p=an(n,i,1,p)),b&&(n.lineWrapping||g.widgetLineBreaks)&&(b=an(n,s,-1,b)),p&&b&&p.from==b.from&&p.to==b.to)return w(v(e.from,e.to,p));{let S=p?v(e.from,null,p):x(u,!1),C=b?v(null,e.to,b):x(g,!0),L=[];return(p||u).to<(b||g).from-(p&&b?1:0)||u.widgetLineBreaks>1&&S.bottom+n.defaultLineHeight/2Qt&&ct.from=bt)break;J>ot&&wi(Math.max(Q,ot),S==null&&Q<=Qt,Math.min(J,bt),C==null&&J>=Jt,Et.dir)}if(ot=ft.to+1,ot>=bt)break}return Ut.length==0&&wi(Qt,S==null,Jt,C==null,n.textDirection),{top:H,bottom:nt,horizontal:Ut}}function x(S,C){let L=l.top+(C?S.top:S.bottom);return{top:L,bottom:L,horizontal:[]}}}function gr(n,t){return n.constructor==t.constructor&&n.eq(t)}class pr{constructor(t,e){this.view=t,this.layer=e,this.drawn=[],this.scaleX=1,this.scaleY=1,this.measureReq={read:this.measure.bind(this),write:this.draw.bind(this)},this.dom=t.scrollDOM.appendChild(document.createElement("div")),this.dom.classList.add("cm-layer"),e.above&&this.dom.classList.add("cm-layer-above"),e.class&&this.dom.classList.add(e.class),this.scale(),this.dom.setAttribute("aria-hidden","true"),this.setOrder(t.state),t.requestMeasure(this.measureReq),e.mount&&e.mount(this.dom,t)}update(t){t.startState.facet(me)!=t.state.facet(me)&&this.setOrder(t.state),(this.layer.update(t,this.dom)||t.geometryChanged)&&(this.scale(),t.view.requestMeasure(this.measureReq))}docViewUpdate(t){this.layer.updateOnDocViewUpdate!==!1&&t.requestMeasure(this.measureReq)}setOrder(t){let e=0,i=t.facet(me);for(;e!gr(e,this.drawn[i]))){let e=this.dom.firstChild,i=0;for(let s of t)s.update&&e&&s.constructor&&this.drawn[i].constructor&&s.update(e,this.drawn[i])?(e=e.nextSibling,i++):this.dom.insertBefore(s.draw(),e);for(;e;){let s=e.nextSibling;e.remove(),e=s}this.drawn=t}}destroy(){this.layer.destroy&&this.layer.destroy(this.dom,this.view),this.dom.remove()}}const me=k.define();function ni(n){return[P.define(t=>new pr(t,n)),me.of(n)]}const cn=!y.ios,At=k.define({combine(n){return De(n,{cursorBlinkRate:1200,drawRangeCursor:!0},{cursorBlinkRate:(t,e)=>Math.min(t,e),drawRangeCursor:(t,e)=>t||e})}});function mr(n={}){return[At.of(n),yr,wr,xr,ls.of(!0)]}function br(n){return n.facet(At)}function fn(n){return n.startState.facet(At)!=n.state.facet(At)}const yr=ni({above:!0,markers(n){let{state:t}=n,e=t.facet(At),i=[];for(let s of t.selection.ranges){let o=s==t.selection.main;if(s.empty?!o||cn:e.drawRangeCursor){let r=o?"cm-cursor cm-cursor-primary":"cm-cursor cm-cursor-secondary",l=s.empty?s:M.cursor(s.head,s.head>s.anchor?-1:1);for(let h of Ot.forRange(n,r,l))i.push(h)}}return i},update(n,t){n.transactions.some(i=>i.selection)&&(t.style.animationName=t.style.animationName=="cm-blink"?"cm-blink2":"cm-blink");let e=fn(n);return e&&dn(n.state,t),n.docChanged||n.selectionSet||e},mount(n,t){dn(t.state,n)},class:"cm-cursorLayer"});function dn(n,t){t.style.animationDuration=n.facet(At).cursorBlinkRate+"ms"}const wr=ni({above:!1,markers(n){return n.state.selection.ranges.map(t=>t.empty?[]:Ot.forRange(n,"cm-selectionBackground",t)).reduce((t,e)=>t.concat(e))},update(n,t){return n.docChanged||n.selectionSet||n.viewportChanged||fn(n)},class:"cm-selectionLayer"}),oi={".cm-line":{"& ::selection, &::selection":{backgroundColor:"transparent !important"}},".cm-content":{"& :focus":{caretColor:"initial !important","&::selection, & ::selection":{backgroundColor:"Highlight !important"}}}};cn&&(oi[".cm-line"].caretColor=oi[".cm-content"].caretColor="transparent !important");const xr=Ae.highest(A.theme(oi)),un=xt.define({map(n,t){return n==null?null:t.mapPos(n)}}),It=Ci.define({create(){return null},update(n,t){return n!=null&&(n=t.changes.mapPos(n)),t.effects.reduce((e,i)=>i.is(un)?i.value:e,n)}}),vr=P.fromClass(class{constructor(n){this.view=n,this.cursor=null,this.measureReq={read:this.readPos.bind(this),write:this.drawCursor.bind(this)}}update(n){var t;let e=n.state.field(It);e==null?this.cursor!=null&&((t=this.cursor)===null||t===void 0||t.remove(),this.cursor=null):(this.cursor||(this.cursor=this.view.scrollDOM.appendChild(document.createElement("div")),this.cursor.className="cm-dropCursor"),(n.startState.field(It)!=e||n.docChanged||n.geometryChanged)&&this.view.requestMeasure(this.measureReq))}readPos(){let{view:n}=this,t=n.state.field(It),e=t!=null&&n.coordsAtPos(t);if(!e)return null;let i=n.scrollDOM.getBoundingClientRect();return{left:e.left-i.left+n.scrollDOM.scrollLeft*n.scaleX,top:e.top-i.top+n.scrollDOM.scrollTop*n.scaleY,height:e.bottom-e.top}}drawCursor(n){if(this.cursor){let{scaleX:t,scaleY:e}=this.view;n?(this.cursor.style.left=n.left/t+"px",this.cursor.style.top=n.top/e+"px",this.cursor.style.height=n.height/e+"px"):this.cursor.style.left="-100000px"}}destroy(){this.cursor&&this.cursor.remove()}setDropPos(n){this.view.state.field(It)!=n&&this.view.dispatch({effects:un.of(n)})}},{eventObservers:{dragover(n){this.setDropPos(this.view.posAtCoords({x:n.clientX,y:n.clientY}))},dragleave(n){(n.target==this.view.contentDOM||!this.view.contentDOM.contains(n.relatedTarget))&&this.setDropPos(null)},dragend(){this.setDropPos(null)},drop(){this.setDropPos(null)}}});function Sr(){return[It,vr]}function gn(n,t,e,i,s){t.lastIndex=0;for(let o=n.iterRange(e,i),r=e,l;!o.next().done;r+=o.value.length)if(!o.lineBreak)for(;l=t.exec(o.value);)s(r+l.index,l)}function Cr(n,t){let e=n.visibleRanges;if(e.length==1&&e[0].from==n.viewport.from&&e[0].to==n.viewport.to)return e;let i=[];for(let{from:s,to:o}of e)s=Math.max(n.state.doc.lineAt(s).from,s-t),o=Math.min(n.state.doc.lineAt(o).to,o+t),i.length&&i[i.length-1].to>=s?i[i.length-1].to=o:i.push({from:s,to:o});return i}class be{constructor(t){const{regexp:e,decoration:i,decorate:s,boundary:o,maxLength:r=1e3}=t;if(!e.global)throw new RangeError("The regular expression given to MatchDecorator should have its 'g' flag set");if(this.regexp=e,s)this.addMatch=(l,h,c,a)=>s(a,c,c+l[0].length,l,h);else if(typeof i=="function")this.addMatch=(l,h,c,a)=>{let f=i(l,h,c);f&&a(c,c+l[0].length,f)};else if(i)this.addMatch=(l,h,c,a)=>a(c,c+l[0].length,i);else throw new RangeError("Either 'decorate' or 'decoration' should be provided to MatchDecorator");this.boundary=o,this.maxLength=r}createDeco(t){let e=new Nn,i=e.add.bind(e);for(let{from:s,to:o}of Cr(t,this.maxLength))gn(t.state.doc,this.regexp,s,o,(r,l)=>this.addMatch(l,t,r,i));return e.finish()}updateDeco(t,e){let i=1e9,s=-1;return t.docChanged&&t.changes.iterChanges((o,r,l,h)=>{h>t.view.viewport.from&&l1e3?this.createDeco(t.view):s>-1?this.updateRange(t.view,e.map(t.changes),i,s):e}updateRange(t,e,i,s){for(let o of t.visibleRanges){let r=Math.max(o.from,i),l=Math.min(o.to,s);if(l>r){let h=t.state.doc.lineAt(r),c=h.toh.from;r--)if(this.boundary.test(h.text[r-1-h.from])){a=r;break}for(;ld.push(m.range(p,b));if(h==c)for(this.regexp.lastIndex=a-h.from;(u=this.regexp.exec(h.text))&&u.indexthis.addMatch(b,t,p,g));e=e.update({filterFrom:a,filterTo:f,filter:(p,b)=>pf,add:d})}}return e}}const ri=/x/.unicode!=null?"gu":"g",Mr=new RegExp(`[\0-\b +-\x7F-\x9F\xAD\u061C\u200B\u200E\u200F\u2028\u2029\u202D\u202E\u2066\u2067\u2069\uFEFF\uFFF9-\uFFFC]`,ri),kr={0:"null",7:"bell",8:"backspace",10:"newline",11:"vertical tab",13:"carriage return",27:"escape",8203:"zero width space",8204:"zero width non-joiner",8205:"zero width joiner",8206:"left-to-right mark",8207:"right-to-left mark",8232:"line separator",8237:"left-to-right override",8238:"right-to-left override",8294:"left-to-right isolate",8295:"right-to-left isolate",8297:"pop directional isolate",8233:"paragraph separator",65279:"zero width no-break space",65532:"object replacement"};let li=null;function Ar(){var n;if(li==null&&typeof document<"u"&&document.body){let t=document.body.style;li=((n=t.tabSize)!==null&&n!==void 0?n:t.MozTabSize)!=null}return li||!1}const ye=k.define({combine(n){let t=De(n,{render:null,specialChars:Mr,addSpecialChars:null});return(t.replaceTabs=!Ar())&&(t.specialChars=new RegExp(" |"+t.specialChars.source,ri)),t.addSpecialChars&&(t.specialChars=new RegExp(t.specialChars.source+"|"+t.addSpecialChars.source,ri)),t}});function Dr(n={}){return[ye.of(n),Or()]}let pn=null;function Or(){return pn||(pn=P.fromClass(class{constructor(n){this.view=n,this.decorations=R.none,this.decorationCache=Object.create(null),this.decorator=this.makeDecorator(n.state.facet(ye)),this.decorations=this.decorator.createDeco(n)}makeDecorator(n){return new be({regexp:n.specialChars,decoration:(t,e,i)=>{let{doc:s}=e.state,o=Si(t[0],0);if(o==9){let r=s.lineAt(i),l=e.state.tabSize,h=Mi(r.text,l,i-r.from);return R.replace({widget:new Lr((l-h%l)*this.view.defaultCharacterWidth/this.view.scaleX)})}return this.decorationCache[o]||(this.decorationCache[o]=R.replace({widget:new Er(n,o)}))},boundary:n.replaceTabs?void 0:/[^]/})}update(n){let t=n.state.facet(ye);n.startState.facet(ye)!=t?(this.decorator=this.makeDecorator(t),this.decorations=this.decorator.createDeco(n.view)):this.decorations=this.decorator.updateDeco(n,this.decorations)}},{decorations:n=>n.decorations}))}const Tr="\u2022";function Rr(n){return n>=32?Tr:n==10?"\u2424":String.fromCharCode(9216+n)}class Er extends gt{constructor(t,e){super(),this.options=t,this.code=e}eq(t){return t.code==this.code}toDOM(t){let e=Rr(this.code),i=t.state.phrase("Control character")+" "+(kr[this.code]||"0x"+this.code.toString(16)),s=this.options.render&&this.options.render(this.code,i,e);if(s)return s;let o=document.createElement("span");return o.textContent=e,o.title=i,o.setAttribute("aria-label",i),o.className="cm-specialChar",o}ignoreEvent(){return!1}}class Lr extends gt{constructor(t){super(),this.width=t}eq(t){return t.width==this.width}toDOM(){let t=document.createElement("span");return t.textContent=" ",t.className="cm-tab",t.style.width=this.width+"px",t}ignoreEvent(){return!1}}const mn=P.fromClass(class{constructor(){this.height=1e3,this.attrs={style:"padding-bottom: 1000px"}}update(n){let{view:t}=n,e=t.viewState.editorHeight-t.defaultLineHeight-t.documentPadding.top-.5;e>=0&&e!=this.height&&(this.height=e,this.attrs={style:`padding-bottom: ${e}px`})}});function Br(){return[mn,oe.of(n=>{var t;return((t=n.plugin(mn))===null||t===void 0?void 0:t.attrs)||null})]}function Pr(){return Nr}const Hr=R.line({class:"cm-activeLine"}),Nr=P.fromClass(class{constructor(n){this.decorations=this.getDeco(n)}update(n){(n.docChanged||n.selectionSet)&&(this.decorations=this.getDeco(n.view))}getDeco(n){let t=-1,e=[];for(let i of n.state.selection.ranges){let s=n.lineBlockAt(i.head);s.from>t&&(e.push(Hr.range(s.from)),t=s.from)}return R.set(e)}},{decorations:n=>n.decorations});class Vr extends gt{constructor(t){super(),this.content=t}toDOM(){let t=document.createElement("span");return t.className="cm-placeholder",t.style.pointerEvents="none",t.appendChild(typeof this.content=="string"?document.createTextNode(this.content):this.content),typeof this.content=="string"?t.setAttribute("aria-label","placeholder "+this.content):t.setAttribute("aria-hidden","true"),t}coordsAt(t){let e=t.firstChild?St(t.firstChild):[];if(!e.length)return null;let i=window.getComputedStyle(t.parentNode),s=Ht(e[0],i.direction!="rtl"),o=parseInt(i.lineHeight);return s.bottom-s.top>o*1.5?{left:s.left,right:s.right,top:s.top,bottom:s.top+o}:s}ignoreEvent(){return!1}}function Fr(n){return P.fromClass(class{constructor(t){this.view=t,this.placeholder=n?R.set([R.widget({widget:new Vr(n),side:1}).range(0)]):R.none}get decorations(){return this.view.state.doc.length?R.none:this.placeholder}},{decorations:t=>t.decorations})}const hi=2e3;function Wr(n,t,e){let i=Math.min(t.line,e.line),s=Math.max(t.line,e.line),o=[];if(t.off>hi||e.off>hi||t.col<0||e.col<0){let r=Math.min(t.off,e.off),l=Math.max(t.off,e.off);for(let h=i;h<=s;h++){let c=n.doc.line(h);c.length<=l&&o.push(M.range(c.from+r,c.to+l))}}else{let r=Math.min(t.col,e.col),l=Math.max(t.col,e.col);for(let h=i;h<=s;h++){let c=n.doc.line(h),a=ke(c.text,r,n.tabSize,!0);if(a<0)o.push(M.cursor(c.to));else{let f=ke(c.text,l,n.tabSize);o.push(M.range(c.from+a,c.from+f))}}}return o}function zr(n,t){let e=n.coordsAtPos(n.viewport.from);return e?Math.round(Math.abs((e.left-t)/n.defaultCharacterWidth)):-1}function bn(n,t){let e=n.posAtCoords({x:t.clientX,y:t.clientY},!1),i=n.state.doc.lineAt(e),s=e-i.from,o=s>hi?-1:s==i.length?zr(n,t.clientX):Mi(i.text,n.state.tabSize,e-i.from);return{line:i.number,col:o,off:s}}function Ir(n,t){let e=bn(n,t),i=n.state.selection;return e?{update(s){if(s.docChanged){let o=s.changes.mapPos(s.startState.doc.line(e.line).from),r=s.state.doc.lineAt(o);e={line:r.number,col:e.col,off:Math.min(e.off,r.length)},i=i.map(s.changes)}},get(s,o,r){let l=bn(n,s);if(!l)return i;let h=Wr(n.state,e,l);return h.length?r?M.create(h.concat(i.ranges)):M.create(h):i}}:null}function Kr(n){let t=n?.eventFilter||(e=>e.altKey&&e.button==0);return A.mouseSelectionStyle.of((e,i)=>t(i)?Ir(e,i):null)}const qr={Alt:[18,n=>!!n.altKey],Control:[17,n=>!!n.ctrlKey],Shift:[16,n=>!!n.shiftKey],Meta:[91,n=>!!n.metaKey]},jr={style:"cursor: crosshair"};function Yr(n={}){let[t,e]=qr[n.key||"Alt"],i=P.fromClass(class{constructor(s){this.view=s,this.isDown=!1}set(s){this.isDown!=s&&(this.isDown=s,this.view.update([]))}},{eventObservers:{keydown(s){this.set(s.keyCode==t||e(s))},keyup(s){(s.keyCode==t||!e(s))&&this.set(!1)},mousemove(s){this.set(e(s))}}});return[i,A.contentAttributes.of(s=>{var o;return!((o=s.plugin(i))===null||o===void 0)&&o.isDown?jr:null})]}const Kt="-10000px";class yn{constructor(t,e,i,s){this.facet=e,this.createTooltipView=i,this.removeTooltipView=s,this.input=t.state.facet(e),this.tooltips=this.input.filter(r=>r);let o=null;this.tooltipViews=this.tooltips.map(r=>o=i(r,o))}update(t,e){var i;let s=t.state.facet(this.facet),o=s.filter(h=>h);if(s===this.input){for(let h of this.tooltipViews)h.update&&h.update(t);return!1}let r=[],l=e?[]:null;for(let h=0;he[c]=h),e.length=l.length),this.input=s,this.tooltips=o,this.tooltipViews=r,!0}}function _r(n={}){return we.of(n)}function Xr(n){let{win:t}=n;return{top:0,left:0,bottom:t.innerHeight,right:t.innerWidth}}const we=k.define({combine:n=>{var t,e,i;return{position:y.ios?"absolute":((t=n.find(s=>s.position))===null||t===void 0?void 0:t.position)||"fixed",parent:((e=n.find(s=>s.parent))===null||e===void 0?void 0:e.parent)||null,tooltipSpace:((i=n.find(s=>s.tooltipSpace))===null||i===void 0?void 0:i.tooltipSpace)||Xr}}}),wn=new WeakMap,xe=P.fromClass(class{constructor(n){this.view=n,this.above=[],this.inView=!0,this.madeAbsolute=!1,this.lastTransaction=0,this.measureTimeout=-1;let t=n.state.facet(we);this.position=t.position,this.parent=t.parent,this.classes=n.themeClasses,this.createContainer(),this.measureReq={read:this.readMeasure.bind(this),write:this.writeMeasure.bind(this),key:this},this.resizeObserver=typeof ResizeObserver=="function"?new ResizeObserver(()=>this.measureSoon()):null,this.manager=new yn(n,ai,(e,i)=>this.createTooltip(e,i),e=>{this.resizeObserver&&this.resizeObserver.unobserve(e.dom),e.dom.remove()}),this.above=this.manager.tooltips.map(e=>!!e.above),this.intersectionObserver=typeof IntersectionObserver=="function"?new IntersectionObserver(e=>{Date.now()>this.lastTransaction-50&&e.length>0&&e[e.length-1].intersectionRatio<1&&this.measureSoon()},{threshold:[1]}):null,this.observeIntersection(),n.win.addEventListener("resize",this.measureSoon=this.measureSoon.bind(this)),this.maybeMeasure()}createContainer(){this.parent?(this.container=document.createElement("div"),this.container.style.position="relative",this.container.className=this.view.themeClasses,this.parent.appendChild(this.container)):this.container=this.view.dom}observeIntersection(){if(this.intersectionObserver){this.intersectionObserver.disconnect();for(let n of this.manager.tooltipViews)this.intersectionObserver.observe(n.dom)}}measureSoon(){this.measureTimeout<0&&(this.measureTimeout=setTimeout(()=>{this.measureTimeout=-1,this.maybeMeasure()},50))}update(n){n.transactions.length&&(this.lastTransaction=Date.now());let t=this.manager.update(n,this.above);t&&this.observeIntersection();let e=t||n.geometryChanged,i=n.state.facet(we);if(i.position!=this.position&&!this.madeAbsolute){this.position=i.position;for(let s of this.manager.tooltipViews)s.dom.style.position=this.position;e=!0}if(i.parent!=this.parent){this.parent&&this.container.remove(),this.parent=i.parent,this.createContainer();for(let s of this.manager.tooltipViews)this.container.appendChild(s.dom);e=!0}else this.parent&&this.view.themeClasses!=this.classes&&(this.classes=this.container.className=this.view.themeClasses);e&&this.maybeMeasure()}createTooltip(n,t){let e=n.create(this.view),i=t?t.dom:null;if(e.dom.classList.add("cm-tooltip"),n.arrow&&!e.dom.querySelector(".cm-tooltip > .cm-tooltip-arrow")){let s=document.createElement("div");s.className="cm-tooltip-arrow",e.dom.appendChild(s)}return e.dom.style.position=this.position,e.dom.style.top=Kt,e.dom.style.left="0px",this.container.insertBefore(e.dom,i),e.mount&&e.mount(this.view),this.resizeObserver&&this.resizeObserver.observe(e.dom),e}destroy(){var n,t,e;this.view.win.removeEventListener("resize",this.measureSoon);for(let i of this.manager.tooltipViews)i.dom.remove(),(n=i.destroy)===null||n===void 0||n.call(i);this.parent&&this.container.remove(),(t=this.resizeObserver)===null||t===void 0||t.disconnect(),(e=this.intersectionObserver)===null||e===void 0||e.disconnect(),clearTimeout(this.measureTimeout)}readMeasure(){let n=this.view.dom.getBoundingClientRect(),t=1,e=1,i=!1;if(this.position=="fixed"&&this.manager.tooltipViews.length){let{dom:s}=this.manager.tooltipViews[0];if(y.gecko)i=s.offsetParent!=this.container.ownerDocument.body;else if(s.style.top==Kt&&s.style.left=="0px"){let o=s.getBoundingClientRect();i=Math.abs(o.top+1e4)>1||Math.abs(o.left)>1}}if(i||this.position=="absolute")if(this.parent){let s=this.parent.getBoundingClientRect();s.width&&s.height&&(t=s.width/this.parent.offsetWidth,e=s.height/this.parent.offsetHeight)}else({scaleX:t,scaleY:e}=this.view.viewState);return{editor:n,parent:this.parent?this.container.getBoundingClientRect():n,pos:this.manager.tooltips.map((s,o)=>{let r=this.manager.tooltipViews[o];return r.getCoords?r.getCoords(s.pos):this.view.coordsAtPos(s.pos)}),size:this.manager.tooltipViews.map(({dom:s})=>s.getBoundingClientRect()),space:this.view.state.facet(we).tooltipSpace(this.view),scaleX:t,scaleY:e,makeAbsolute:i}}writeMeasure(n){var t;if(n.makeAbsolute){this.madeAbsolute=!0,this.position="absolute";for(let l of this.manager.tooltipViews)l.dom.style.position="absolute"}let{editor:e,space:i,scaleX:s,scaleY:o}=n,r=[];for(let l=0;l=Math.min(e.bottom,i.bottom)||f.rightMath.min(e.right,i.right)+.1){a.style.top=Kt;continue}let u=h.arrow?c.dom.querySelector(".cm-tooltip-arrow"):null,g=u?7:0,p=d.right-d.left,b=(t=wn.get(c))!==null&&t!==void 0?t:d.bottom-d.top,m=c.offset||$r,w=this.view.textDirection==E.LTR,v=d.width>i.right-i.left?w?i.left:i.right-d.width:w?Math.max(i.left,Math.min(f.left-(u?14:0)+m.x,i.right-p)):Math.min(Math.max(i.left,f.left-p+(u?14:0)-m.x),i.right-p),x=this.above[l];!h.strictSide&&(x?f.top-(d.bottom-d.top)-m.yi.bottom)&&x==i.bottom-f.bottom>f.top-i.top&&(x=this.above[l]=!x);let S=(x?f.top-i.top:i.bottom-f.bottom)-g;if(Sv&&H.topC&&(C=x?H.top-b-2-g:H.bottom+g+2);if(this.position=="absolute"?(a.style.top=(C-n.parent.top)/o+"px",a.style.left=(v-n.parent.left)/s+"px"):(a.style.top=C/o+"px",a.style.left=v/s+"px"),u){let H=f.left+(w?m.x:-m.x)-(v+14-7);u.style.left=H/s+"px"}c.overlap!==!0&&r.push({left:v,top:C,right:L,bottom:C+b}),a.classList.toggle("cm-tooltip-above",x),a.classList.toggle("cm-tooltip-below",!x),c.positioned&&c.positioned(n.space)}}maybeMeasure(){if(this.manager.tooltips.length&&(this.view.inView&&this.view.requestMeasure(this.measureReq),this.inView!=this.view.inView&&(this.inView=this.view.inView,!this.inView)))for(let n of this.manager.tooltipViews)n.dom.style.top=Kt}},{eventObservers:{scroll(){this.maybeMeasure()}}}),Gr=A.baseTheme({".cm-tooltip":{zIndex:100,boxSizing:"border-box"},"&light .cm-tooltip":{border:"1px solid #bbb",backgroundColor:"#f5f5f5"},"&light .cm-tooltip-section:not(:first-child)":{borderTop:"1px solid #bbb"},"&dark .cm-tooltip":{backgroundColor:"#333338",color:"white"},".cm-tooltip-arrow":{height:"7px",width:`${7*2}px`,position:"absolute",zIndex:-1,overflow:"hidden","&:before, &:after":{content:"''",position:"absolute",width:0,height:0,borderLeft:"7px solid transparent",borderRight:"7px solid transparent"},".cm-tooltip-above &":{bottom:"-7px","&:before":{borderTop:"7px solid #bbb"},"&:after":{borderTop:"7px solid #f5f5f5",bottom:"1px"}},".cm-tooltip-below &":{top:"-7px","&:before":{borderBottom:"7px solid #bbb"},"&:after":{borderBottom:"7px solid #f5f5f5",top:"1px"}}},"&dark .cm-tooltip .cm-tooltip-arrow":{"&:before":{borderTopColor:"#333338",borderBottomColor:"#333338"},"&:after":{borderTopColor:"transparent",borderBottomColor:"transparent"}}}),$r={x:0,y:0},ai=k.define({enables:[xe,Gr]}),qt=k.define({combine:n=>n.reduce((t,e)=>t.concat(e),[])});class Me{static create(t){return new Me(t)}constructor(t){this.view=t,this.mounted=!1,this.dom=document.createElement("div"),this.dom.classList.add("cm-tooltip-hover"),this.manager=new yn(t,qt,(e,i)=>this.createHostedView(e,i),e=>e.dom.remove())}createHostedView(t,e){let i=t.create(this.view);return i.dom.classList.add("cm-tooltip-section"),this.dom.insertBefore(i.dom,e?e.dom.nextSibling:this.dom.firstChild),this.mounted&&i.mount&&i.mount(this.view),i}mount(t){for(let e of this.manager.tooltipViews)e.mount&&e.mount(t);this.mounted=!0}positioned(t){for(let e of this.manager.tooltipViews)e.positioned&&e.positioned(t)}update(t){this.manager.update(t)}destroy(){var t;for(let e of this.manager.tooltipViews)(t=e.destroy)===null||t===void 0||t.call(e)}passProp(t){let e;for(let i of this.manager.tooltipViews){let s=i[t];if(s!==void 0){if(e===void 0)e=s;else if(e!==s)return}}return e}get offset(){return this.passProp("offset")}get getCoords(){return this.passProp("getCoords")}get overlap(){return this.passProp("overlap")}get resize(){return this.passProp("resize")}}const Ur=ai.compute([qt],n=>{let t=n.facet(qt);return t.length===0?null:{pos:Math.min(...t.map(e=>e.pos)),end:Math.max(...t.map(e=>{var i;return(i=e.end)!==null&&i!==void 0?i:e.pos})),create:Me.create,above:t[0].above,arrow:t.some(e=>e.arrow)}});class Qr{constructor(t,e,i,s,o){this.view=t,this.source=e,this.field=i,this.setHover=s,this.hoverTime=o,this.hoverTimeout=-1,this.restartTimeout=-1,this.pending=null,this.lastMove={x:0,y:0,target:t.dom,time:0},this.checkHover=this.checkHover.bind(this),t.dom.addEventListener("mouseleave",this.mouseleave=this.mouseleave.bind(this)),t.dom.addEventListener("mousemove",this.mousemove=this.mousemove.bind(this))}update(){this.pending&&(this.pending=null,clearTimeout(this.restartTimeout),this.restartTimeout=setTimeout(()=>this.startHover(),20))}get active(){return this.view.state.field(this.field)}checkHover(){if(this.hoverTimeout=-1,this.active.length)return;let t=Date.now()-this.lastMove.time;tl.bottom||e.xl.right+t.defaultCharacterWidth)return;let h=t.bidiSpans(t.state.doc.lineAt(s)).find(a=>a.from<=s&&a.to>=s),c=h&&h.dir==E.RTL?-1:1;o=e.x{this.pending==l&&(this.pending=null,h&&!(Array.isArray(h)&&!h.length)&&t.dispatch({effects:this.setHover.of(Array.isArray(h)?h:[h])}))},h=>_(t.state,h,"hover tooltip"))}else r&&!(Array.isArray(r)&&!r.length)&&t.dispatch({effects:this.setHover.of(Array.isArray(r)?r:[r])})}get tooltip(){let t=this.view.plugin(xe),e=t?t.manager.tooltips.findIndex(i=>i.create==Me.create):-1;return e>-1?t.manager.tooltipViews[e]:null}mousemove(t){var e,i;this.lastMove={x:t.clientX,y:t.clientY,target:t.target,time:Date.now()},this.hoverTimeout<0&&(this.hoverTimeout=setTimeout(this.checkHover,this.hoverTime));let{active:s,tooltip:o}=this;if(s.length&&o&&!Jr(o.dom,t)||this.pending){let{pos:r}=s[0]||this.pending,l=(i=(e=s[0])===null||e===void 0?void 0:e.end)!==null&&i!==void 0?i:r;(r==l?this.view.posAtCoords(this.lastMove)!=r:!Zr(this.view,r,l,t.clientX,t.clientY))&&(this.view.dispatch({effects:this.setHover.of([])}),this.pending=null)}}mouseleave(t){clearTimeout(this.hoverTimeout),this.hoverTimeout=-1;let{active:e}=this;if(e.length){let{tooltip:i}=this;i&&i.dom.contains(t.relatedTarget)?this.watchTooltipLeave(i.dom):this.view.dispatch({effects:this.setHover.of([])})}}watchTooltipLeave(t){let e=i=>{t.removeEventListener("mouseleave",e),this.active.length&&!this.view.dom.contains(i.relatedTarget)&&this.view.dispatch({effects:this.setHover.of([])})};t.addEventListener("mouseleave",e)}destroy(){clearTimeout(this.hoverTimeout),this.view.dom.removeEventListener("mouseleave",this.mouseleave),this.view.dom.removeEventListener("mousemove",this.mousemove)}}const ve=4;function Jr(n,t){let e=n.getBoundingClientRect();return t.clientX>=e.left-ve&&t.clientX<=e.right+ve&&t.clientY>=e.top-ve&&t.clientY<=e.bottom+ve}function Zr(n,t,e,i,s,o){let r=n.scrollDOM.getBoundingClientRect(),l=n.documentTop+n.documentPadding.top+n.contentHeight;if(r.left>i||r.rights||Math.min(r.bottom,l)=t&&h<=e}function tl(n,t={}){let e=xt.define(),i=Ci.define({create(){return[]},update(s,o){if(s.length&&(t.hideOnChange&&(o.docChanged||o.selection)?s=[]:t.hideOn&&(s=s.filter(r=>!t.hideOn(o,r))),o.docChanged)){let r=[];for(let l of s){let h=o.changes.mapPos(l.pos,-1,yt.TrackDel);if(h!=null){let c=Object.assign(Object.create(null),l);c.pos=h,c.end!=null&&(c.end=o.changes.mapPos(c.end)),r.push(c)}}s=r}for(let r of o.effects)r.is(e)&&(s=r.value),r.is(xn)&&(s=[]);return s},provide:s=>qt.from(s)});return{active:i,extension:[i,P.define(s=>new Qr(s,n,i,e,t.hoverTime||300)),Ur]}}function el(n,t){let e=n.plugin(xe);if(!e)return null;let i=e.manager.tooltips.indexOf(t);return i<0?null:e.manager.tooltipViews[i]}function il(n){return n.facet(qt).some(t=>t)}const xn=xt.define(),sl=xn.of(null);function nl(n){let t=n.plugin(xe);t&&t.maybeMeasure()}const ci=k.define({combine(n){let t,e;for(let i of n)t=t||i.topContainer,e=e||i.bottomContainer;return{topContainer:t,bottomContainer:e}}});function ol(n){return n?[ci.of(n)]:[]}function rl(n,t){let e=n.plugin(vn),i=e?e.specs.indexOf(t):-1;return i>-1?e.panels[i]:null}const vn=P.fromClass(class{constructor(n){this.input=n.state.facet(fi),this.specs=this.input.filter(e=>e),this.panels=this.specs.map(e=>e(n));let t=n.state.facet(ci);this.top=new Se(n,!0,t.topContainer),this.bottom=new Se(n,!1,t.bottomContainer),this.top.sync(this.panels.filter(e=>e.top)),this.bottom.sync(this.panels.filter(e=>!e.top));for(let e of this.panels)e.dom.classList.add("cm-panel"),e.mount&&e.mount()}update(n){let t=n.state.facet(ci);this.top.container!=t.topContainer&&(this.top.sync([]),this.top=new Se(n.view,!0,t.topContainer)),this.bottom.container!=t.bottomContainer&&(this.bottom.sync([]),this.bottom=new Se(n.view,!1,t.bottomContainer)),this.top.syncClasses(),this.bottom.syncClasses();let e=n.state.facet(fi);if(e!=this.input){let i=e.filter(h=>h),s=[],o=[],r=[],l=[];for(let h of i){let c=this.specs.indexOf(h),a;c<0?(a=h(n.view),l.push(a)):(a=this.panels[c],a.update&&a.update(n)),s.push(a),(a.top?o:r).push(a)}this.specs=i,this.panels=s,this.top.sync(o),this.bottom.sync(r);for(let h of l)h.dom.classList.add("cm-panel"),h.mount&&h.mount()}else for(let i of this.panels)i.update&&i.update(n)}destroy(){this.top.sync([]),this.bottom.sync([])}},{provide:n=>A.scrollMargins.of(t=>{let e=t.plugin(n);return e&&{top:e.top.scrollMargin(),bottom:e.bottom.scrollMargin()}})});class Se{constructor(t,e,i){this.view=t,this.top=e,this.container=i,this.dom=void 0,this.classes="",this.panels=[],this.syncClasses()}sync(t){for(let e of this.panels)e.destroy&&t.indexOf(e)<0&&e.destroy();this.panels=t,this.syncDOM()}syncDOM(){if(this.panels.length==0){this.dom&&(this.dom.remove(),this.dom=void 0);return}if(!this.dom){this.dom=document.createElement("div"),this.dom.className=this.top?"cm-panels cm-panels-top":"cm-panels cm-panels-bottom",this.dom.style[this.top?"top":"bottom"]="0";let e=this.container||this.view.dom;e.insertBefore(this.dom,this.top?e.firstChild:null)}let t=this.dom.firstChild;for(let e of this.panels)if(e.dom.parentNode==this.dom){for(;t!=e.dom;)t=Sn(t);t=t.nextSibling}else this.dom.insertBefore(e.dom,t);for(;t;)t=Sn(t)}scrollMargin(){return!this.dom||this.container?0:Math.max(0,this.top?this.dom.getBoundingClientRect().bottom-Math.max(0,this.view.scrollDOM.getBoundingClientRect().top):Math.min(innerHeight,this.view.scrollDOM.getBoundingClientRect().bottom)-this.dom.getBoundingClientRect().top)}syncClasses(){if(!(!this.container||this.classes==this.view.themeClasses)){for(let t of this.classes.split(" "))t&&this.container.classList.remove(t);for(let t of(this.classes=this.view.themeClasses).split(" "))t&&this.container.classList.add(t)}}}function Sn(n){let t=n.nextSibling;return n.remove(),t}const fi=k.define({enables:vn});class et extends xi{compare(t){return this==t||this.constructor==t.constructor&&this.eq(t)}eq(t){return!1}destroy(t){}}et.prototype.elementClass="",et.prototype.toDOM=void 0,et.prototype.mapMode=yt.TrackBefore,et.prototype.startSide=et.prototype.endSide=-1,et.prototype.point=!0;const jt=k.define(),Cn=k.define(),ll={class:"",renderEmptyElements:!1,elementStyle:"",markers:()=>V.empty,lineMarker:()=>null,widgetMarker:()=>null,lineMarkerChange:null,initialSpacer:null,updateSpacer:null,domEventHandlers:{}},Yt=k.define();function hl(n){return[ui(),Yt.of(Object.assign(Object.assign({},ll),n))]}const di=k.define({combine:n=>n.some(t=>t)});function ui(n){let t=[al];return n&&n.fixed===!1&&t.push(di.of(!0)),t}const al=P.fromClass(class{constructor(n){this.view=n,this.prevViewport=n.viewport,this.dom=document.createElement("div"),this.dom.className="cm-gutters",this.dom.setAttribute("aria-hidden","true"),this.dom.style.minHeight=this.view.contentHeight/this.view.scaleY+"px",this.gutters=n.state.facet(Yt).map(t=>new kn(n,t));for(let t of this.gutters)this.dom.appendChild(t.dom);this.fixed=!n.state.facet(di),this.fixed&&(this.dom.style.position="sticky"),this.syncGutters(!1),n.scrollDOM.insertBefore(this.dom,n.contentDOM)}update(n){if(this.updateGutters(n)){let t=this.prevViewport,e=n.view.viewport,i=Math.min(t.to,e.to)-Math.max(t.from,e.from);this.syncGutters(i<(e.to-e.from)*.8)}n.geometryChanged&&(this.dom.style.minHeight=this.view.contentHeight/this.view.scaleY+"px"),this.view.state.facet(di)!=!this.fixed&&(this.fixed=!this.fixed,this.dom.style.position=this.fixed?"sticky":""),this.prevViewport=n.view.viewport}syncGutters(n){let t=this.dom.nextSibling;n&&this.dom.remove();let e=V.iter(this.view.state.facet(jt),this.view.viewport.from),i=[],s=this.gutters.map(o=>new cl(o,this.view.viewport,-this.view.documentPadding.top));for(let o of this.view.viewportLineBlocks)if(i.length&&(i=[]),Array.isArray(o.type)){let r=!0;for(let l of o.type)if(l.type==F.Text&&r){gi(e,i,l.from);for(let h of s)h.line(this.view,l,i);r=!1}else if(l.widget)for(let h of s)h.widget(this.view,l)}else if(o.type==F.Text){gi(e,i,o.from);for(let r of s)r.line(this.view,o,i)}else if(o.widget)for(let r of s)r.widget(this.view,o);for(let o of s)o.finish();n&&this.view.scrollDOM.insertBefore(this.dom,t)}updateGutters(n){let t=n.startState.facet(Yt),e=n.state.facet(Yt),i=n.docChanged||n.heightChanged||n.viewportChanged||!V.eq(n.startState.facet(jt),n.state.facet(jt),n.view.viewport.from,n.view.viewport.to);if(t==e)for(let s of this.gutters)s.update(n)&&(i=!0);else{i=!0;let s=[];for(let o of e){let r=t.indexOf(o);r<0?s.push(new kn(this.view,o)):(this.gutters[r].update(n),s.push(this.gutters[r]))}for(let o of this.gutters)o.dom.remove(),s.indexOf(o)<0&&o.destroy();for(let o of s)this.dom.appendChild(o.dom);this.gutters=s}return i}destroy(){for(let n of this.gutters)n.destroy();this.dom.remove()}},{provide:n=>A.scrollMargins.of(t=>{let e=t.plugin(n);return!e||e.gutters.length==0||!e.fixed?null:t.textDirection==E.LTR?{left:e.dom.offsetWidth*t.scaleX}:{right:e.dom.offsetWidth*t.scaleX}})});function Mn(n){return Array.isArray(n)?n:[n]}function gi(n,t,e){for(;n.value&&n.from<=e;)n.from==e&&t.push(n.value),n.next()}class cl{constructor(t,e,i){this.gutter=t,this.height=i,this.i=0,this.cursor=V.iter(t.markers,e.from)}addElement(t,e,i){let{gutter:s}=this,o=(e.top-this.height)/t.scaleY,r=e.height/t.scaleY;if(this.i==s.elements.length){let l=new An(t,r,o,i);s.elements.push(l),s.dom.appendChild(l.dom)}else s.elements[this.i].update(t,r,o,i);this.height=e.bottom,this.i++}line(t,e,i){let s=[];gi(this.cursor,s,e.from),i.length&&(s=s.concat(i));let o=this.gutter.config.lineMarker(t,e,s);o&&s.unshift(o);let r=this.gutter;s.length==0&&!r.config.renderEmptyElements||this.addElement(t,e,s)}widget(t,e){let i=this.gutter.config.widgetMarker(t,e.widget,e),s=i?[i]:null;for(let o of t.state.facet(Cn)){let r=o(t,e.widget,e);r&&(s||(s=[])).push(r)}s&&this.addElement(t,e,s)}finish(){let t=this.gutter;for(;t.elements.length>this.i;){let e=t.elements.pop();t.dom.removeChild(e.dom),e.destroy()}}}class kn{constructor(t,e){this.view=t,this.config=e,this.elements=[],this.spacer=null,this.dom=document.createElement("div"),this.dom.className="cm-gutter"+(this.config.class?" "+this.config.class:"");for(let i in e.domEventHandlers)this.dom.addEventListener(i,s=>{let o=s.target,r;if(o!=this.dom&&this.dom.contains(o)){for(;o.parentNode!=this.dom;)o=o.parentNode;let h=o.getBoundingClientRect();r=(h.top+h.bottom)/2}else r=s.clientY;let l=t.lineBlockAtHeight(r-t.documentTop);e.domEventHandlers[i](t,l,s)&&s.preventDefault()});this.markers=Mn(e.markers(t)),e.initialSpacer&&(this.spacer=new An(t,0,0,[e.initialSpacer(t)]),this.dom.appendChild(this.spacer.dom),this.spacer.dom.style.cssText+="visibility: hidden; pointer-events: none")}update(t){let e=this.markers;if(this.markers=Mn(this.config.markers(t.view)),this.spacer&&this.config.updateSpacer){let s=this.config.updateSpacer(this.spacer.markers[0],t);s!=this.spacer.markers[0]&&this.spacer.update(t.view,0,0,[s])}let i=t.view.viewport;return!V.eq(this.markers,e,i.from,i.to)||(this.config.lineMarkerChange?this.config.lineMarkerChange(t):!1)}destroy(){for(let t of this.elements)t.destroy()}}class An{constructor(t,e,i,s){this.height=-1,this.above=0,this.markers=[],this.dom=document.createElement("div"),this.dom.className="cm-gutterElement",this.update(t,e,i,s)}update(t,e,i,s){this.height!=e&&(this.height=e,this.dom.style.height=e+"px"),this.above!=i&&(this.dom.style.marginTop=(this.above=i)?i+"px":""),fl(this.markers,s)||this.setMarkers(t,s)}setMarkers(t,e){let i="cm-gutterElement",s=this.dom.firstChild;for(let o=0,r=0;;){let l=r,h=oo(l,h,c)||r(l,h,c):r}return i}})}});class pi extends et{constructor(t){super(),this.number=t}eq(t){return this.number==t.number}toDOM(){return document.createTextNode(this.number)}}function mi(n,t){return n.state.facet(Dt).formatNumber(t,n.state)}const dl=Yt.compute([Dt],n=>({class:"cm-lineNumbers",renderEmptyElements:!1,markers(t){return t.state.facet(Dn)},lineMarker(t,e,i){return i.some(s=>s.toDOM)?null:new pi(mi(t,t.state.doc.lineAt(e.from).number))},widgetMarker:(t,e,i)=>{for(let s of t.state.facet(On)){let o=s(t,e,i);if(o)return o}return null},lineMarkerChange:t=>t.startState.facet(Dt)!=t.state.facet(Dt),initialSpacer(t){return new pi(mi(t,Tn(t.state.doc.lines)))},updateSpacer(t,e){let i=mi(e.view,Tn(e.view.state.doc.lines));return i==t.number?t:new pi(i)},domEventHandlers:n.facet(Dt).domEventHandlers}));function ul(n={}){return[Dt.of(n),ui(),dl]}function Tn(n){let t=9;for(;t{let t=[],e=-1;for(let i of n.selection.ranges){let s=n.doc.lineAt(i.head).from;s>e&&(e=s,t.push(gl.range(s)))}return V.of(t)});function ml(){return pl}const Rn=new Map;function bl(n){let t=Rn.get(n);return t||Rn.set(n,t=R.mark({attributes:n===" "?{class:"cm-highlightTab"}:{class:"cm-highlightSpace","data-display":n.replace(/ /g,"\xB7")}})),t}function En(n){return P.define(t=>({decorations:n.createDeco(t),update(e){this.decorations=n.updateDeco(e,this.decorations)}}),{decorations:t=>t.decorations})}const yl=En(new be({regexp:/\t| +/g,decoration:n=>bl(n[0]),boundary:/\S/}));function wl(){return yl}const xl=En(new be({regexp:/\s+$/g,decoration:R.mark({class:"cm-trailingSpace"}),boundary:/\S/}));function vl(){return xl}const Sl={HeightMap:z,HeightOracle:js,MeasuredHeights:Ys,QueryType:T,ChangedRange:q,computeOrder:Ui,moveVisually:Zi,clearHeightChangeFlag:Qe,getHeightChangeFlag:()=>mt};export{tt as BidiSpan,G as BlockInfo,F as BlockType,R as Decoration,E as Direction,A as EditorView,et as GutterMarker,be as MatchDecorator,Ot as RectangleMarker,P as ViewPlugin,$t as ViewUpdate,gt as WidgetType,Sl as __test,sl as closeHoverTooltips,Yr as crosshairCursor,mr as drawSelection,Sr as dropCursor,br as getDrawSelectionConfig,rl as getPanel,el as getTooltip,hl as gutter,jt as gutterLineClass,Cn as gutterWidgetClass,ui as gutters,il as hasHoverTooltips,Pr as highlightActiveLine,ml as highlightActiveLineGutter,Dr as highlightSpecialChars,vl as highlightTrailingWhitespace,wl as highlightWhitespace,tl as hoverTooltip,nn as keymap,ni as layer,Dn as lineNumberMarkers,On as lineNumberWidgetMarker,ul as lineNumbers,_ as logException,ol as panels,Fr as placeholder,Kr as rectangularSelection,nl as repositionTooltips,cr as runScopeHandlers,Br as scrollPastEnd,fi as showPanel,ai as showTooltip,_r as tooltips}; diff --git a/Resources/Public/JavaScript/Contrib/@lezer/common.js b/Resources/Public/JavaScript/Contrib/@lezer/common.js new file mode 100644 index 0000000..640bf46 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@lezer/common.js @@ -0,0 +1 @@ +const ve=1024;let ke=0;class T{constructor(e,t){this.from=e,this.to=t}}class v{constructor(e={}){this.id=ke++,this.perNode=!!e.perNode,this.deserialize=e.deserialize||(()=>{throw new Error("This node type doesn't define a deserialize function")})}add(e){if(this.perNode)throw new RangeError("Can't add per-node props to node types");return typeof e!="function"&&(e=O.match(e)),t=>{let r=e(t);return r===void 0?null:[this,r]}}}v.closedBy=new v({deserialize:l=>l.split(" ")}),v.openedBy=new v({deserialize:l=>l.split(" ")}),v.group=new v({deserialize:l=>l.split(" ")}),v.isolate=new v({deserialize:l=>{if(l&&l!="rtl"&&l!="ltr"&&l!="auto")throw new RangeError("Invalid value for isolate: "+l);return l||"auto"}}),v.contextHash=new v({perNode:!0}),v.lookAhead=new v({perNode:!0}),v.mounted=new v({perNode:!0});class q{constructor(e,t,r){this.tree=e,this.overlay=t,this.parser=r}static get(e){return e&&e.props&&e.props[v.mounted.id]}}const Ce=Object.create(null);class O{constructor(e,t,r,n=0){this.name=e,this.props=t,this.id=r,this.flags=n}static define(e){let t=e.props&&e.props.length?Object.create(null):Ce,r=(e.top?1:0)|(e.skipped?2:0)|(e.error?4:0)|(e.name==null?8:0),n=new O(e.name||"",t,e.id,r);if(e.props){for(let i of e.props)if(Array.isArray(i)||(i=i(n)),i){if(i[0].perNode)throw new RangeError("Can't store a per-node prop on a node type");t[i[0].id]=i[1]}}return n}prop(e){return this.props[e.id]}get isTop(){return(this.flags&1)>0}get isSkipped(){return(this.flags&2)>0}get isError(){return(this.flags&4)>0}get isAnonymous(){return(this.flags&8)>0}is(e){if(typeof e=="string"){if(this.name==e)return!0;let t=this.prop(v.group);return t?t.indexOf(e)>-1:!1}return this.id==e}static match(e){let t=Object.create(null);for(let r in e)for(let n of r.split(" "))t[n]=e[r];return r=>{for(let n=r.prop(v.group),i=-1;i<(n?n.length:0);i++){let s=t[i<0?r.name:n[i]];if(s)return s}}}}O.none=new O("",Object.create(null),0,8);class se{constructor(e){this.types=e;for(let t=0;t0;for(let h=this.cursor(s|C.IncludeAnonymous);;){let a=!1;if(h.from<=i&&h.to>=n&&(!f&&h.type.isAnonymous||t(h)!==!1)){if(h.firstChild())continue;a=!0}for(;a&&r&&(f||!h.type.isAnonymous)&&r(h),!h.nextSibling();){if(!h.parent())return;a=!0}}}prop(e){return e.perNode?this.props?this.props[e.id]:void 0:this.type.prop(e)}get propValues(){let e=[];if(this.props)for(let t in this.props)e.push([+t,this.props[t]]);return e}balance(e={}){return this.children.length<=8?this:ne(O.none,this.children,this.positions,0,this.children.length,0,this.length,(t,r,n)=>new z(this.type,t,r,n,this.propValues),e.makeTree||((t,r,n)=>new z(O.none,t,r,n)))}static build(e){return Ie(e)}}z.empty=new z(O.none,[],[],0);class le{constructor(e,t){this.buffer=e,this.index=t}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}get pos(){return this.index}next(){this.index-=4}fork(){return new le(this.buffer,this.index)}}class U{constructor(e,t,r){this.buffer=e,this.length=t,this.set=r}get type(){return O.none}toString(){let e=[];for(let t=0;t0));h=s[h+3]);return f}slice(e,t,r){let n=this.buffer,i=new Uint16Array(t-e),s=0;for(let f=e,h=0;f=e&&te;case 1:return t<=e&&r>e;case 2:return r>e;case 4:return!0}}function Q(l,e,t,r){for(var n;l.from==l.to||(t<1?l.from>=e:l.from>e)||(t>-1?l.to<=e:l.to0?f.length:-1;e!=a;e+=t){let o=f[e],d=h[e]+s.from;if(ue(n,r,d,d+o.length)){if(o instanceof U){if(i&C.ExcludeBuffers)continue;let p=o.findChild(0,o.buffer.length,t,r-d,n);if(p>-1)return new D(new Ae(s,o,e,d),null,p)}else if(i&C.IncludeAnonymous||!o.type.isAnonymous||re(o)){let p;if(!(i&C.IgnoreMounts)&&(p=q.get(o))&&!p.overlay)return new P(p.tree,d,e,s);let y=new P(o,d,e,s);return i&C.IncludeAnonymous||!y.type.isAnonymous?y:y.nextChild(t<0?o.children.length-1:0,t,r,n)}}}if(i&C.IncludeAnonymous||!s.type.isAnonymous||(s.index>=0?e=s.index+t:e=t<0?-1:s._parent._tree.children.length,s=s._parent,!s))return null}}get firstChild(){return this.nextChild(0,1,0,4)}get lastChild(){return this.nextChild(this._tree.children.length-1,-1,0,4)}childAfter(e){return this.nextChild(0,1,e,2)}childBefore(e){return this.nextChild(this._tree.children.length-1,-1,e,-2)}enter(e,t,r=0){let n;if(!(r&C.IgnoreOverlays)&&(n=q.get(this._tree))&&n.overlay){let i=e-this.from;for(let{from:s,to:f}of n.overlay)if((t>0?s<=i:s=i:f>i))return new P(n.tree,n.overlay[0].from+this.from,-1,this)}return this.nextChild(0,1,e,t,r)}nextSignificantParent(){let e=this;for(;e.type.isAnonymous&&e._parent;)e=e._parent;return e}get parent(){return this._parent?this._parent.nextSignificantParent():null}get nextSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index+1,1,0,4):null}get prevSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index-1,-1,0,4):null}get tree(){return this._tree}toTree(){return this._tree}toString(){return this._tree.toString()}}function pe(l,e,t,r){let n=l.cursor(),i=[];if(!n.firstChild())return i;if(t!=null){for(let s=!1;!s;)if(s=n.type.is(t),!n.nextSibling())return i}for(;;){if(r!=null&&n.type.is(r))return i;if(n.type.is(e)&&i.push(n.node),!n.nextSibling())return r==null?i:[]}}function te(l,e,t=e.length-1){for(let r=l.parent;t>=0;r=r.parent){if(!r)return!1;if(!r.type.isAnonymous){if(e[t]&&e[t]!=r.name)return!1;t--}}return!0}class Ae{constructor(e,t,r,n){this.parent=e,this.buffer=t,this.index=r,this.start=n}}class D extends ae{get name(){return this.type.name}get from(){return this.context.start+this.context.buffer.buffer[this.index+1]}get to(){return this.context.start+this.context.buffer.buffer[this.index+2]}constructor(e,t,r){super(),this.context=e,this._parent=t,this.index=r,this.type=e.buffer.set.types[e.buffer.buffer[r]]}child(e,t,r){let{buffer:n}=this.context,i=n.findChild(this.index+4,n.buffer[this.index+3],e,t-this.context.start,r);return i<0?null:new D(this.context,this,i)}get firstChild(){return this.child(1,0,4)}get lastChild(){return this.child(-1,0,4)}childAfter(e){return this.child(1,e,2)}childBefore(e){return this.child(-1,e,-2)}enter(e,t,r=0){if(r&C.ExcludeBuffers)return null;let{buffer:n}=this.context,i=n.findChild(this.index+4,n.buffer[this.index+3],t>0?1:-1,e-this.context.start,t);return i<0?null:new D(this.context,this,i)}get parent(){return this._parent||this.context.parent.nextSignificantParent()}externalSibling(e){return this._parent?null:this.context.parent.nextChild(this.context.index+e,e,0,4)}get nextSibling(){let{buffer:e}=this.context,t=e.buffer[this.index+3];return t<(this._parent?e.buffer[this._parent.index+3]:e.buffer.length)?new D(this.context,this._parent,t):this.externalSibling(1)}get prevSibling(){let{buffer:e}=this.context,t=this._parent?this._parent.index+4:0;return this.index==t?this.externalSibling(-1):new D(this.context,this._parent,e.findChild(t,this.index,-1,0,4))}get tree(){return null}toTree(){let e=[],t=[],{buffer:r}=this.context,n=this.index+4,i=r.buffer[this.index+3];if(i>n){let s=r.buffer[this.index+1];e.push(r.slice(n,i,s)),t.push(0)}return new z(this.type,e,t,this.to-this.from)}toString(){return this.context.buffer.childString(this.index)}}function de(l){if(!l.length)return null;let e=0,t=l[0];for(let i=1;it.from||s.to=e){let f=new P(s.tree,s.overlay[0].from+i.from,-1,i);(n||(n=[r])).push(Q(f,e,t,!1))}}return n?de(n):r}class X{get name(){return this.type.name}constructor(e,t=0){if(this.mode=t,this.buffer=null,this.stack=[],this.index=0,this.bufferNode=null,e instanceof P)this.yieldNode(e);else{this._tree=e.context.parent,this.buffer=e.context;for(let r=e._parent;r;r=r._parent)this.stack.unshift(r.index);this.bufferNode=e,this.yieldBuf(e.index)}}yieldNode(e){return e?(this._tree=e,this.type=e.type,this.from=e.from,this.to=e.to,!0):!1}yieldBuf(e,t){this.index=e;let{start:r,buffer:n}=this.buffer;return this.type=t||n.set.types[n.buffer[e]],this.from=r+n.buffer[e+1],this.to=r+n.buffer[e+2],!0}yield(e){return e?e instanceof P?(this.buffer=null,this.yieldNode(e)):(this.buffer=e.context,this.yieldBuf(e.index,e.type)):!1}toString(){return this.buffer?this.buffer.buffer.childString(this.index):this._tree.toString()}enterChild(e,t,r){if(!this.buffer)return this.yield(this._tree.nextChild(e<0?this._tree._tree.children.length-1:0,e,t,r,this.mode));let{buffer:n}=this.buffer,i=n.findChild(this.index+4,n.buffer[this.index+3],e,t-this.buffer.start,r);return i<0?!1:(this.stack.push(this.index),this.yieldBuf(i))}firstChild(){return this.enterChild(1,0,4)}lastChild(){return this.enterChild(-1,0,4)}childAfter(e){return this.enterChild(1,e,2)}childBefore(e){return this.enterChild(-1,e,-2)}enter(e,t,r=this.mode){return this.buffer?r&C.ExcludeBuffers?!1:this.enterChild(1,e,t):this.yield(this._tree.enter(e,t,r))}parent(){if(!this.buffer)return this.yieldNode(this.mode&C.IncludeAnonymous?this._tree._parent:this._tree.parent);if(this.stack.length)return this.yieldBuf(this.stack.pop());let e=this.mode&C.IncludeAnonymous?this.buffer.parent:this.buffer.parent.nextSignificantParent();return this.buffer=null,this.yieldNode(e)}sibling(e){if(!this.buffer)return this._tree._parent?this.yield(this._tree.index<0?null:this._tree._parent.nextChild(this._tree.index+e,e,0,4,this.mode)):!1;let{buffer:t}=this.buffer,r=this.stack.length-1;if(e<0){let n=r<0?0:this.stack[r]+4;if(this.index!=n)return this.yieldBuf(t.findChild(n,this.index,-1,0,4))}else{let n=t.buffer[this.index+3];if(n<(r<0?t.buffer.length:t.buffer[this.stack[r]+3]))return this.yieldBuf(n)}return r<0?this.yield(this.buffer.parent.nextChild(this.buffer.index+e,e,0,4,this.mode)):!1}nextSibling(){return this.sibling(1)}prevSibling(){return this.sibling(-1)}atLastNode(e){let t,r,{buffer:n}=this;if(n){if(e>0){if(this.index-1)for(let i=t+e,s=e<0?-1:r._tree.children.length;i!=s;i+=e){let f=r._tree.children[i];if(this.mode&C.IncludeAnonymous||f instanceof U||!f.type.isAnonymous||re(f))return!1}return!0}move(e,t){if(t&&this.enterChild(e,0,4))return!0;for(;;){if(this.sibling(e))return!0;if(this.atLastNode(e)||!this.parent())return!1}}next(e=!0){return this.move(1,e)}prev(e=!0){return this.move(-1,e)}moveTo(e,t=0){for(;(this.from==this.to||(t<1?this.from>=e:this.from>e)||(t>-1?this.to<=e:this.to=0;){for(let s=e;s;s=s._parent)if(s.index==n){if(n==this.index)return s;t=s,r=i+1;break e}n=this.stack[--i]}for(let n=r;n=0;i--){if(i<0)return te(this.node,e,n);let s=r[t.buffer[this.stack[i]]];if(!s.isAnonymous){if(e[n]&&e[n]!=s.name)return!1;n--}}return!0}}function re(l){return l.children.some(e=>e instanceof U||!e.type.isAnonymous||re(e))}function Ie(l){var e;let{buffer:t,nodeSet:r,maxBufferLength:n=1024,reused:i=[],minRepeatType:s=r.types.length}=l,f=Array.isArray(t)?new le(t,t.length):t,h=r.types,a=0,o=0;function d(x,w,u,A,b,m){let{id:c,start:g,end:S,size:k}=f,B=o;for(;k<0;)if(f.next(),k==-1){let W=i[c];u.push(W),A.push(g-x);return}else if(k==-3){a=c;return}else if(k==-4){o=c;return}else throw new RangeError(`Unrecognized record size: ${k}`);let H=h[c],G,J,fe=g-x;if(S-g<=n&&(J=F(f.pos-w,b))){let W=new Uint16Array(J.size-J.skip),M=f.pos-J.size,R=W.length;for(;f.pos>M;)R=j(J.start,W,R);G=new U(W,S-J.start,r),fe=J.start-x}else{let W=f.pos-k;f.next();let M=[],R=[],V=c>=s?c:-1,$=0,Y=S;for(;f.pos>W;)V>=0&&f.id==V&&f.size>=0?(f.end<=Y-n&&(_(M,R,g,$,f.end,Y,V,B),$=M.length,Y=f.end),f.next()):m>2500?p(g,W,M,R):d(g,W,M,R,V,m+1);if(V>=0&&$>0&&$-1&&$>0){let he=y(H);G=ne(H,M,R,0,M.length,0,S-g,he,he)}else G=N(H,M,R,S-g,B-S)}u.push(G),A.push(fe)}function p(x,w,u,A){let b=[],m=0,c=-1;for(;f.pos>w;){let{id:g,start:S,end:k,size:B}=f;if(B>4)f.next();else{if(c>-1&&S=0;k-=3)g[B++]=b[k],g[B++]=b[k+1]-S,g[B++]=b[k+2]-S,g[B++]=B;u.push(new U(g,b[2]-S,r)),A.push(S-x)}}function y(x){return(w,u,A)=>{let b=0,m=w.length-1,c,g;if(m>=0&&(c=w[m])instanceof z){if(!m&&c.type==x&&c.length==A)return c;(g=c.prop(v.lookAhead))&&(b=u[m]+c.length+g)}return N(x,w,u,A,b)}}function _(x,w,u,A,b,m,c,g){let S=[],k=[];for(;x.length>A;)S.push(x.pop()),k.push(w.pop()+u-b);x.push(N(r.types[c],S,k,m-b,g-m)),w.push(b-u)}function N(x,w,u,A,b=0,m){if(a){let c=[v.contextHash,a];m=m?[c].concat(m):[c]}if(b>25){let c=[v.lookAhead,b];m=m?[c].concat(m):[c]}return new z(x,w,u,A,m)}function F(x,w){let u=f.fork(),A=0,b=0,m=0,c=u.end-n,g={size:0,start:0,skip:0};e:for(let S=u.pos-x;u.pos>S;){let k=u.size;if(u.id==w&&k>=0){g.size=A,g.start=b,g.skip=m,m+=4,A+=4,u.next();continue}let B=u.pos-k;if(k<0||B=s?4:0,G=u.start;for(u.next();u.pos>B;){if(u.size<0)if(u.size==-3)H+=4;else break e;else u.id>=s&&(H+=4);u.next()}b=G,A+=k,m+=H}return(w<0||A==x)&&(g.size=A,g.start=b,g.skip=m),g.size>4?g:void 0}function j(x,w,u){let{id:A,start:b,end:m,size:c}=f;if(f.next(),c>=0&&A4){let S=f.pos-(c-4);for(;f.pos>S;)u=j(x,w,u)}w[--u]=g,w[--u]=m-x,w[--u]=b-x,w[--u]=A}else c==-3?a=A:c==-4&&(o=A);return u}let E=[],I=[];for(;f.pos>0;)d(l.start||0,l.bufferStart||0,E,I,-1,0);let L=(e=l.length)!==null&&e!==void 0?e:E.length?I[0]+E[0].length:0;return new z(h[l.topID],E.reverse(),I.reverse(),L)}const ce=new WeakMap;function ee(l,e){if(!l.isAnonymous||e instanceof U||e.type!=l)return 1;let t=ce.get(e);if(t==null){t=1;for(let r of e.children){if(r.type!=l||!(r instanceof z)){t=1;break}t+=ee(l,r)}ce.set(e,t)}return t}function ne(l,e,t,r,n,i,s,f,h){let a=0;for(let _=r;_=o)break;w+=u}if(I==L+1){if(w>o){let u=_[L];y(u.children,u.positions,0,u.children.length,N[L]+E);continue}d.push(_[L])}else{let u=N[I-1]+_[I-1].length-x;d.push(ne(l,_,N,L,I,x,u,null,h))}p.push(x+E-i)}}return y(e,t,r,n,0),(f||h)(d,p,s)}class Ne{constructor(){this.map=new WeakMap}setBuffer(e,t,r){let n=this.map.get(e);n||this.map.set(e,n=new Map),n.set(t,r)}getBuffer(e,t){let r=this.map.get(e);return r&&r.get(t)}set(e,t){e instanceof D?this.setBuffer(e.context.buffer,e.index,t):e instanceof P&&this.map.set(e.tree,t)}get(e){return e instanceof D?this.getBuffer(e.context.buffer,e.index):e instanceof P?this.map.get(e.tree):void 0}cursorSet(e,t){e.buffer?this.setBuffer(e.buffer.buffer,e.index,t):this.map.set(e.tree,t)}cursorGet(e){return e.buffer?this.getBuffer(e.buffer.buffer,e.index):this.map.get(e.tree)}}class K{constructor(e,t,r,n,i=!1,s=!1){this.from=e,this.to=t,this.tree=r,this.offset=n,this.open=(i?1:0)|(s?2:0)}get openStart(){return(this.open&1)>0}get openEnd(){return(this.open&2)>0}static addTree(e,t=[],r=!1){let n=[new K(0,e.length,e,0,!1,r)];for(let i of t)i.to>e.length&&n.push(i);return n}static applyChanges(e,t,r=128){if(!t.length)return e;let n=[],i=1,s=e.length?e[0]:null;for(let f=0,h=0,a=0;;f++){let o=f=r)for(;s&&s.from=p.from||d<=p.to||a){let y=Math.max(p.from,h)-a,_=Math.min(p.to,d)-a;p=y>=_?null:new K(y,_,p.tree,p.offset+a,f>0,!!o)}if(p&&n.push(p),s.to>d)break;s=inew T(n.from,n.to)):[new T(0,0)]:[new T(0,e.length)],this.createParse(e,t||[],r)}parse(e,t,r){let n=this.startParse(e,t,r);for(;;){let i=n.advance();if(i)return i}}}class Be{constructor(e){this.string=e}get length(){return this.string.length}chunk(e){return this.string.slice(e)}get lineChunks(){return!1}read(e,t){return this.string.slice(e,t)}}function ze(l){return(e,t,r,n)=>new Me(e,l,t,r,n)}class ge{constructor(e,t,r,n,i){this.parser=e,this.parse=t,this.overlay=r,this.target=n,this.from=i}}function me(l){if(!l.length||l.some(e=>e.from>=e.to))throw new RangeError("Invalid inner parse ranges given: "+JSON.stringify(l))}class Ee{constructor(e,t,r,n,i,s,f){this.parser=e,this.predicate=t,this.mounts=r,this.index=n,this.start=i,this.target=s,this.prev=f,this.depth=0,this.ranges=[]}}const ie=new v({perNode:!0});class Me{constructor(e,t,r,n,i){this.nest=t,this.input=r,this.fragments=n,this.ranges=i,this.inner=[],this.innerDone=0,this.baseTree=null,this.stoppedAt=null,this.baseParse=e}advance(){if(this.baseParse){let r=this.baseParse.advance();if(!r)return null;if(this.baseParse=null,this.baseTree=r,this.startInner(),this.stoppedAt!=null)for(let n of this.inner)n.parse.stopAt(this.stoppedAt)}if(this.innerDone==this.inner.length){let r=this.baseTree;return this.stoppedAt!=null&&(r=new z(r.type,r.children,r.positions,r.length,r.propValues.concat([[ie,this.stoppedAt]]))),r}let e=this.inner[this.innerDone],t=e.parse.advance();if(t){this.innerDone++;let r=Object.assign(Object.create(null),e.target.props);r[v.mounted.id]=new q(t,e.overlay,e.parser),e.target.props=r}return null}get parsedPos(){if(this.baseParse)return 0;let e=this.input.length;for(let t=this.innerDone;t=this.stoppedAt)f=!1;else if(e.hasNode(n)){if(t){let a=t.mounts.find(o=>o.frag.from<=n.from&&o.frag.to>=n.to&&o.mount.overlay);if(a)for(let o of a.mount.overlay){let d=o.from+a.pos,p=o.to+a.pos;d>=n.from&&p<=n.to&&!t.ranges.some(y=>y.fromd)&&t.ranges.push({from:d,to:p})}}f=!1}else if(r&&(s=Te(r.ranges,n.from,n.to)))f=s!=2;else if(!n.type.isAnonymous&&(i=this.nest(n,this.input))&&(n.fromnew T(d.from-n.from,d.to-n.from)):null,n.tree,o.length?o[0].from:n.from)),i.overlay?o.length&&(r={ranges:o,depth:0,prev:r}):f=!1}}else t&&(h=t.predicate(n))&&(h===!0&&(h=new T(n.from,n.to)),h.fromnew T(o.from-t.start,o.to-t.start)),t.target,a[0].from))),t=t.prev}r&&!--r.depth&&(r=r.prev)}}}}function Te(l,e,t){for(let r of l){if(r.from>=t)break;if(r.to>e)return r.from<=e&&r.to>=t?2:1}return 0}function xe(l,e,t,r,n,i){if(e=e&&t.enter(r,1,C.IgnoreOverlays|C.ExcludeBuffers)||t.next(!1)||(this.done=!0)}hasNode(e){if(this.moveTo(e.from),!this.done&&this.cursor.from+this.offset==e.from&&this.cursor.tree)for(let t=this.cursor.tree;;){if(t==e.tree)return!0;if(t.children.length&&t.positions[0]==0&&t.children[0]instanceof z)t=t.children[0];else break}return!1}}class Fe{constructor(e){var t;if(this.fragments=e,this.curTo=0,this.fragI=0,e.length){let r=this.curFrag=e[0];this.curTo=(t=r.tree.prop(ie))!==null&&t!==void 0?t:r.to,this.inner=new ye(r.tree,-r.offset)}else this.curFrag=this.inner=null}hasNode(e){for(;this.curFrag&&e.from>=this.curTo;)this.nextFrag();return this.curFrag&&this.curFrag.from<=e.from&&this.curTo>=e.to&&this.inner.hasNode(e)}nextFrag(){var e;if(this.fragI++,this.fragI==this.fragments.length)this.curFrag=this.inner=null;else{let t=this.curFrag=this.fragments[this.fragI];this.curTo=(e=t.tree.prop(ie))!==null&&e!==void 0?e:t.to,this.inner=new ye(t.tree,-t.offset)}}findMounts(e,t){var r;let n=[];if(this.inner){this.inner.cursor.moveTo(e,1);for(let i=this.inner.cursor.node;i;i=i.parent){let s=(r=i.tree)===null||r===void 0?void 0:r.prop(v.mounted);if(s&&s.parser==t)for(let f=this.fragI;f=i.to)break;h.tree==this.curFrag.tree&&n.push({frag:h,pos:i.from-h.offset,mount:s})}}}return n}}function be(l,e){let t=null,r=e;for(let n=1,i=0;n=f)break;h.to<=s||(t||(r=t=e.slice()),h.fromf&&t.splice(i+1,0,new T(f,h.to))):h.to>f?t[i--]=new T(f,h.to):t.splice(i--,1))}}return r}function je(l,e,t,r){let n=0,i=0,s=!1,f=!1,h=-1e9,a=[];for(;;){let o=n==l.length?1e9:s?l[n].to:l[n].from,d=i==e.length?1e9:f?e[i].to:e[i].from;if(s!=f){let p=Math.max(h,t),y=Math.min(o,d,r);pnew T(p.from+r,p.to+r)),d=je(e,o,h,a);for(let p=0,y=h;;p++){let _=p==d.length,N=_?a:d[p].from;if(N>y&&t.push(new K(y,N,n.tree,-s,i.from>=y||i.openStart,i.to<=N||i.openEnd)),_)break;y=d[p].to}}else t.push(new K(h,a,n.tree,-s,i.from>=s||i.openStart,i.to<=f||i.openEnd))}return t}export{ve as DefaultBufferLength,C as IterMode,q as MountedTree,v as NodeProp,se as NodeSet,O as NodeType,Ne as NodeWeakMap,Pe as Parser,z as Tree,U as TreeBuffer,X as TreeCursor,K as TreeFragment,ze as parseMixed}; diff --git a/Resources/Public/JavaScript/Contrib/@lezer/css.js b/Resources/Public/JavaScript/Contrib/@lezer/css.js new file mode 100644 index 0000000..416c4e9 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@lezer/css.js @@ -0,0 +1 @@ +import{ExternalTokenizer as P,LRParser as c}from"@lezer/lr";import{styleTags as W,tags as O}from"@lezer/highlight";const X=94,$=1,y=95,p=96,i=2,n=[9,10,11,12,13,32,133,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288],m=58,d=40,s=95,f=91,a=45,z=46,R=35,g=37;function t(e){return e>=65&&e<=90||e>=97&&e<=122||e>=161}function U(e){return e>=48&&e<=57}const u=new P((e,Q)=>{for(let o=!1,r=0,l=0;;l++){let{next:S}=e;if(t(S)||S==a||S==s||o&&U(S))!o&&(S!=a||l>0)&&(o=!0),r===l&&S==a&&r++,e.advance();else{o&&e.acceptToken(S==d?y:r==2&&Q.canShift(i)?i:p);break}}}),T=new P(e=>{if(n.includes(e.peek(-1))){let{next:Q}=e;(t(Q)||Q==s||Q==R||Q==z||Q==f||Q==m||Q==a)&&e.acceptToken(X)}}),h=new P(e=>{if(!n.includes(e.peek(-1))){let{next:Q}=e;if(Q==g&&(e.advance(),e.acceptToken($)),t(Q)){do e.advance();while(t(e.next));e.acceptToken($)}}}),b=W({"AtKeyword import charset namespace keyframes media supports":O.definitionKeyword,"from to selector":O.keyword,NamespaceName:O.namespace,KeyframeName:O.labelName,TagName:O.tagName,ClassName:O.className,PseudoClassName:O.constant(O.className),IdName:O.labelName,"FeatureName PropertyName":O.propertyName,AttributeName:O.attributeName,NumberLiteral:O.number,KeywordQuery:O.keyword,UnaryQueryOp:O.operatorKeyword,"CallTag ValueName":O.atom,VariableName:O.variableName,Callee:O.operatorKeyword,Unit:O.unit,"UniversalSelector NestingSelector":O.definitionOperator,MatchOp:O.compareOperator,"ChildOp SiblingOp, LogicOp":O.logicOperator,BinOp:O.arithmeticOperator,Important:O.modifier,Comment:O.blockComment,ParenthesizedContent:O.special(O.name),ColorLiteral:O.color,StringLiteral:O.string,":":O.punctuation,"PseudoOp #":O.derefOperator,"; ,":O.separator,"( )":O.paren,"[ ]":O.squareBracket,"{ }":O.brace}),_={__proto__:null,lang:32,"nth-child":32,"nth-last-child":32,"nth-of-type":32,"nth-last-of-type":32,dir:32,"host-context":32,url:60,"url-prefix":60,domain:60,regexp:60,selector:134},V={__proto__:null,"@import":114,"@media":138,"@charset":142,"@namespace":146,"@keyframes":152,"@supports":164},Y={__proto__:null,not:128,only:128,from:158,to:160},G=c.deserialize({version:14,states:"7WQYQ[OOO#_Q[OOOOQP'#Cd'#CdOOQP'#Cc'#CcO#fQ[O'#CfO$YQXO'#CaO$aQ[O'#ChO$lQ[O'#DPO$qQ[O'#DTOOQP'#Ed'#EdO$vQdO'#DeO%bQ[O'#DrO$vQdO'#DtO%sQ[O'#DvO&OQ[O'#DyO&TQ[O'#EPO&cQ[O'#EROOQS'#Ec'#EcOOQS'#ET'#ETQYQ[OOO&jQXO'#CdO'_QWO'#DaO'dQWO'#EjO'oQ[O'#EjQOQWOOOOQP'#Cg'#CgOOQP,59Q,59QO#fQ[O,59QO'yQ[O'#EWO(eQWO,58{O(mQ[O,59SO$lQ[O,59kO$qQ[O,59oO'yQ[O,59sO'yQ[O,59uO'yQ[O,59vO(xQ[O'#D`OOQS,58{,58{OOQP'#Ck'#CkOOQO'#C}'#C}OOQP,59S,59SO)PQWO,59SO)UQWO,59SOOQP'#DR'#DROOQP,59k,59kOOQO'#DV'#DVO)ZQ`O,59oOOQS'#Cp'#CpO$vQdO'#CqO)cQvO'#CsO*pQtO,5:POOQO'#Cx'#CxO)UQWO'#CwO+UQWO'#CyOOQS'#Eg'#EgOOQO'#Dh'#DhO+ZQ[O'#DoO+iQWO'#EkO&TQ[O'#DmO+wQWO'#DpOOQO'#El'#ElO(hQWO,5:^O+|QpO,5:`OOQS'#Dx'#DxO,UQWO,5:bO,ZQ[O,5:bOOQO'#D{'#D{O,cQWO,5:eO,hQWO,5:kO,pQWO,5:mOOQS-E8R-E8RO$vQdO,59{O,xQ[O'#EYO-VQWO,5;UO-VQWO,5;UOOQP1G.l1G.lO-|QXO,5:rOOQO-E8U-E8UOOQS1G.g1G.gOOQP1G.n1G.nO)PQWO1G.nO)UQWO1G.nOOQP1G/V1G/VO.ZQ`O1G/ZO.tQXO1G/_O/[QXO1G/aO/rQXO1G/bO0YQWO,59zO0_Q[O'#DOO0fQdO'#CoOOQP1G/Z1G/ZO$vQdO1G/ZO0mQpO,59]OOQS,59_,59_O$vQdO,59aO0uQWO1G/kOOQS,59c,59cO0zQ!bO,59eO1SQWO'#DhO1_QWO,5:TO1dQWO,5:ZO&TQ[O,5:VO&TQ[O'#EZO1lQWO,5;VO1wQWO,5:XO'yQ[O,5:[OOQS1G/x1G/xOOQS1G/z1G/zOOQS1G/|1G/|O2YQWO1G/|O2_QdO'#D|OOQS1G0P1G0POOQS1G0V1G0VOOQS1G0X1G0XO2mQtO1G/gOOQO,5:t,5:tO3TQ[O,5:tOOQO-E8W-E8WO3bQWO1G0pOOQP7+$Y7+$YOOQP7+$u7+$uO$vQdO7+$uOOQS1G/f1G/fO3mQXO'#EiO3tQWO,59jO3yQtO'#EUO4nQdO'#EfO4xQWO,59ZO4}QpO7+$uOOQS1G.w1G.wOOQS1G.{1G.{OOQS7+%V7+%VO5VQWO1G/PO$vQdO1G/oOOQO1G/u1G/uOOQO1G/q1G/qO5[QWO,5:uOOQO-E8X-E8XO5jQXO1G/vOOQS7+%h7+%hO5qQYO'#CsO(hQWO'#E[O5yQdO,5:hOOQS,5:h,5:hO6XQtO'#EXO$vQdO'#EXO7VQdO7+%ROOQO7+%R7+%ROOQO1G0`1G0`O7jQpO<T![;'S%^;'S;=`%o<%lO%^^;TUoWOy%^z!Q%^!Q![;g![;'S%^;'S;=`%o<%lO%^^;nYoW#[UOy%^z!Q%^!Q![;g![!g%^!g!h<^!h#X%^#X#Y<^#Y;'S%^;'S;=`%o<%lO%^^[[oW#[UOy%^z!O%^!O!P;g!P!Q%^!Q![>T![!g%^!g!h<^!h#X%^#X#Y<^#Y;'S%^;'S;=`%o<%lO%^_?VSpVOy%^z;'S%^;'S;=`%o<%lO%^^?hWjSOy%^z!O%^!O!P;O!P!Q%^!Q![>T![;'S%^;'S;=`%o<%lO%^_@VU#XPOy%^z!Q%^!Q![;g![;'S%^;'S;=`%o<%lO%^~@nTjSOy%^z{@}{;'S%^;'S;=`%o<%lO%^~ASUoWOy@}yzAfz{Bm{;'S@};'S;=`Co<%lO@}~AiTOzAfz{Ax{;'SAf;'S;=`Bg<%lOAf~A{VOzAfz{Ax{!PAf!P!QBb!Q;'SAf;'S;=`Bg<%lOAf~BgOR~~BjP;=`<%lAf~BrWoWOy@}yzAfz{Bm{!P@}!P!QC[!Q;'S@};'S;=`Co<%lO@}~CcSoWR~Oy%^z;'S%^;'S;=`%o<%lO%^~CrP;=`<%l@}^Cz[#[UOy%^z!O%^!O!P;g!P!Q%^!Q![>T![!g%^!g!h<^!h#X%^#X#Y<^#Y;'S%^;'S;=`%o<%lO%^XDuU]POy%^z![%^![!]EX!];'S%^;'S;=`%o<%lO%^XE`S^PoWOy%^z;'S%^;'S;=`%o<%lO%^_EqS!WVOy%^z;'S%^;'S;=`%o<%lO%^YFSSzQOy%^z;'S%^;'S;=`%o<%lO%^XFeU|POy%^z!`%^!`!aFw!a;'S%^;'S;=`%o<%lO%^XGOS|PoWOy%^z;'S%^;'S;=`%o<%lO%^XG_WOy%^z!c%^!c!}Gw!}#T%^#T#oGw#o;'S%^;'S;=`%o<%lO%^XHO[!YPoWOy%^z}%^}!OGw!O!Q%^!Q![Gw![!c%^!c!}Gw!}#T%^#T#oGw#o;'S%^;'S;=`%o<%lO%^XHySxPOy%^z;'S%^;'S;=`%o<%lO%^^I[SvUOy%^z;'S%^;'S;=`%o<%lO%^XIkUOy%^z#b%^#b#cI}#c;'S%^;'S;=`%o<%lO%^XJSUoWOy%^z#W%^#W#XJf#X;'S%^;'S;=`%o<%lO%^XJmS!`PoWOy%^z;'S%^;'S;=`%o<%lO%^XJ|UOy%^z#f%^#f#gJf#g;'S%^;'S;=`%o<%lO%^XKeS!RPOy%^z;'S%^;'S;=`%o<%lO%^_KvS!QVOy%^z;'S%^;'S;=`%o<%lO%^ZLXU!PPOy%^z!_%^!_!`6y!`;'S%^;'S;=`%o<%lO%^WLnP;=`<%l$}",tokenizers:[T,h,u,0,1,2,3],topRules:{StyleSheet:[0,4],Styles:[1,84]},specialized:[{term:95,get:e=>_[e]||-1},{term:56,get:e=>V[e]||-1},{term:96,get:e=>Y[e]||-1}],tokenPrec:1123});export{G as parser}; diff --git a/Resources/Public/JavaScript/Contrib/@lezer/highlight.js b/Resources/Public/JavaScript/Contrib/@lezer/highlight.js new file mode 100644 index 0000000..b13a474 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@lezer/highlight.js @@ -0,0 +1,2 @@ +import{NodeProp as D}from"@lezer/common";let Q=0;class m{constructor(t,a,i){this.set=t,this.base=a,this.modified=i,this.id=Q++}static define(t){if(t?.base)throw new Error("Can not derive from a modified tag");let a=new m([],null,[]);if(a.set.push(a),t)for(let i of t.set)a.set.push(i);return a}static defineModifier(){let t=new B;return a=>a.modified.indexOf(t)>-1?a:B.get(a.base||a,a.modified.concat(t).sort((i,l)=>i.id-l.id))}}let U=0;class B{constructor(){this.instances=[],this.id=U++}static get(t,a){if(!a.length)return t;let i=a[0].instances.find(r=>r.base==t&&W(a,r.modified));if(i)return i;let l=[],s=new m(l,t,a);for(let r of a)r.instances.push(s);let c=X(a);for(let r of t.set)if(!r.modified.length)for(let d of c)l.push(B.get(r,d));return s}}function W(o,t){return o.length==t.length&&o.every((a,i)=>a==t[i])}function X(o){let t=[[]];for(let a=0;ai.length-a.length)}function Y(o){let t=Object.create(null);for(let a in o){let i=o[a];Array.isArray(i)||(i=[i]);for(let l of a.split(" "))if(l){let s=[],c=2,r=l;for(let f=0;;){if(r=="..."&&f>0&&f+3==l.length){c=1;break}let h=/^"(?:[^"\\]|\\.)*?"|[^\/!]+/.exec(r);if(!h)throw new RangeError("Invalid path: "+l);if(s.push(h[0]=="*"?"":h[0][0]=='"'?JSON.parse(h[0]):h[0]),f+=h[0].length,f==l.length)break;let p=l[f++];if(f==l.length&&p=="!"){c=0;break}if(p!="/")throw new RangeError("Invalid path: "+l);r=l.slice(f)}let d=s.length-1,g=s[d];if(!g)throw new RangeError("Invalid path: "+l);let b=new A(i,c,d>0?s.slice(0,d):null);t[g]=b.sort(t[g])}}return F.add(t)}const F=new D;class A{constructor(t,a,i,l){this.tags=t,this.mode=a,this.context=i,this.next=l}get opaque(){return this.mode==0}get inherit(){return this.mode==1}sort(t){return!t||t.depth{let c=l;for(let r of s)for(let d of r.set){let g=a[d.id];if(g){c=c?c+" "+g:g;break}}return c},scope:i}}function Z(o,t){let a=null;for(let i of o){let l=i.style(t);l&&(a=a?a+" "+l:l)}return a}function V(o,t,a,i=0,l=o.length){let s=new _(i,Array.isArray(t)?t:[t],a);s.highlightRange(o.cursor(),i,l,"",s.highlighters),s.flush(l)}function $(o,t,a,i,l,s=0,c=o.length){let r=s;function d(g,b){if(!(g<=r)){for(let f=o.slice(r,g),h=0;;){let p=f.indexOf(` +`,h),R=p<0?f.length:p;if(R>h&&i(f.slice(h,R),b),p<0)break;l(),h=p+1}r=g}}V(t,a,(g,b,f)=>{d(g,""),d(b,f)},s,c),d(c,"")}class _{constructor(t,a,i){this.at=t,this.highlighters=a,this.span=i,this.class=""}startSpan(t,a){a!=this.class&&(this.flush(t),t>this.at&&(this.at=t),this.class=a)}flush(t){t>this.at&&this.class&&this.span(this.at,t,this.class)}highlightRange(t,a,i,l,s){let{type:c,from:r,to:d}=t;if(r>=i||d<=a)return;c.isTop&&(s=this.highlighters.filter(p=>!p.scope||p.scope(c)));let g=l,b=z(t)||A.empty,f=Z(s,b.tags);if(f&&(g&&(g+=" "),g+=f,b.mode==1&&(l+=(l?" ":"")+f)),this.startSpan(Math.max(a,r),g),b.opaque)return;let h=t.tree&&t.tree.prop(D.mounted);if(h&&h.overlay){let p=t.node.enter(h.overlay[0].from+r,1),R=this.highlighters.filter(v=>!v.scope||v.scope(h.tree.type)),j=t.firstChild();for(let v=0,M=r;;v++){let O=v=E||!t.nextSibling())););if(!O||E>i)break;M=O.to+r,M>a&&(this.highlightRange(p.cursor(),Math.max(a,O.from+r),Math.min(i,M),"",R),this.startSpan(Math.min(i,M),g))}j&&t.parent()}else if(t.firstChild()){h&&(l="");do if(!(t.to<=a)){if(t.from>=i)break;this.highlightRange(t,a,i,l,s),this.startSpan(Math.min(i,t.to),g)}while(t.nextSibling());t.parent()}}}function z(o){let t=o.type.prop(F);for(;t&&t.context&&!o.matchContext(t.context);)t=t.next;return t||null}const e=m.define,C=e(),N=e(),G=e(N),L=e(N),x=e(),I=e(x),H=e(x),u=e(),w=e(u),k=e(),y=e(),K=e(),S=e(K),q=e(),n={comment:C,lineComment:e(C),blockComment:e(C),docComment:e(C),name:N,variableName:e(N),typeName:G,tagName:e(G),propertyName:L,attributeName:e(L),className:e(N),labelName:e(N),namespace:e(N),macroName:e(N),literal:x,string:I,docString:e(I),character:e(I),attributeValue:e(I),number:H,integer:e(H),float:e(H),bool:e(x),regexp:e(x),escape:e(x),color:e(x),url:e(x),keyword:k,self:e(k),null:e(k),atom:e(k),unit:e(k),modifier:e(k),operatorKeyword:e(k),controlKeyword:e(k),definitionKeyword:e(k),moduleKeyword:e(k),operator:y,derefOperator:e(y),arithmeticOperator:e(y),logicOperator:e(y),bitwiseOperator:e(y),compareOperator:e(y),updateOperator:e(y),definitionOperator:e(y),typeOperator:e(y),controlOperator:e(y),punctuation:K,separator:e(K),bracket:S,angleBracket:e(S),squareBracket:e(S),paren:e(S),brace:e(S),content:u,heading:w,heading1:e(w),heading2:e(w),heading3:e(w),heading4:e(w),heading5:e(w),heading6:e(w),contentSeparator:e(u),list:e(u),quote:e(u),emphasis:e(u),strong:e(u),link:e(u),monospace:e(u),strikethrough:e(u),inserted:e(),deleted:e(),changed:e(),invalid:e(),meta:q,documentMeta:e(q),annotation:e(q),processingInstruction:e(q),definition:m.defineModifier(),constant:m.defineModifier(),function:m.defineModifier(),standard:m.defineModifier(),local:m.defineModifier(),special:m.defineModifier()},tt=J([{tag:n.link,class:"tok-link"},{tag:n.heading,class:"tok-heading"},{tag:n.emphasis,class:"tok-emphasis"},{tag:n.strong,class:"tok-strong"},{tag:n.keyword,class:"tok-keyword"},{tag:n.atom,class:"tok-atom"},{tag:n.bool,class:"tok-bool"},{tag:n.url,class:"tok-url"},{tag:n.labelName,class:"tok-labelName"},{tag:n.inserted,class:"tok-inserted"},{tag:n.deleted,class:"tok-deleted"},{tag:n.literal,class:"tok-literal"},{tag:n.string,class:"tok-string"},{tag:n.number,class:"tok-number"},{tag:[n.regexp,n.escape,n.special(n.string)],class:"tok-string2"},{tag:n.variableName,class:"tok-variableName"},{tag:n.local(n.variableName),class:"tok-variableName tok-local"},{tag:n.definition(n.variableName),class:"tok-variableName tok-definition"},{tag:n.special(n.variableName),class:"tok-variableName2"},{tag:n.definition(n.propertyName),class:"tok-propertyName tok-definition"},{tag:n.typeName,class:"tok-typeName"},{tag:n.namespace,class:"tok-namespace"},{tag:n.className,class:"tok-className"},{tag:n.macroName,class:"tok-macroName"},{tag:n.propertyName,class:"tok-propertyName"},{tag:n.operator,class:"tok-operator"},{tag:n.comment,class:"tok-comment"},{tag:n.meta,class:"tok-meta"},{tag:n.invalid,class:"tok-invalid"},{tag:n.punctuation,class:"tok-punctuation"}]);export{m as Tag,tt as classHighlighter,z as getStyleTags,$ as highlightCode,V as highlightTree,Y as styleTags,J as tagHighlighter,n as tags}; diff --git a/Resources/Public/JavaScript/Contrib/@lezer/html.js b/Resources/Public/JavaScript/Contrib/@lezer/html.js new file mode 100644 index 0000000..c3b6ee0 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@lezer/html.js @@ -0,0 +1 @@ +import{ContextTracker as U,ExternalTokenizer as T,LRParser as I}from"@lezer/lr";import{styleTags as J,tags as S}from"@lezer/highlight";import{parseMixed as j}from"@lezer/common";const z=54,L=1,F=55,K=2,ee=56,Oe=3,w=4,te=5,q=6,A=7,C=8,y=9,Y=10,re=11,ae=12,ne=13,d=57,se=14,b=58,m=20,le=22,M=23,Se=24,$=26,k=27,Pe=28,pe=31,oe=34,ce=36,he=37,fe=0,Ve=1,_e={area:!0,base:!0,br:!0,col:!0,command:!0,embed:!0,frame:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0,menuitem:!0},Te={dd:!0,li:!0,optgroup:!0,option:!0,p:!0,rp:!0,rt:!0,tbody:!0,td:!0,tfoot:!0,th:!0,tr:!0},B={dd:{dd:!0,dt:!0},dt:{dd:!0,dt:!0},li:{li:!0},option:{option:!0,optgroup:!0},optgroup:{optgroup:!0},p:{address:!0,article:!0,aside:!0,blockquote:!0,dir:!0,div:!0,dl:!0,fieldset:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,menu:!0,nav:!0,ol:!0,p:!0,pre:!0,section:!0,table:!0,ul:!0},rp:{rp:!0,rt:!0},rt:{rp:!0,rt:!0},tbody:{tbody:!0,tfoot:!0},td:{td:!0,th:!0},tfoot:{tbody:!0},th:{td:!0,th:!0},thead:{tbody:!0,tfoot:!0},tr:{tr:!0}};function qe(e){return e==45||e==46||e==58||e>=65&&e<=90||e==95||e>=97&&e<=122||e>=161}function G(e){return e==9||e==10||e==13||e==32}let Z=null,R=null,E=0;function i(e,O){let a=e.pos+O;if(E==a&&R==e)return Z;let r=e.peek(O);for(;G(r);)r=e.peek(++O);let t="";for(;qe(r);)t+=String.fromCharCode(r),r=e.peek(++O);return R=e,E=a,Z=t?t.toLowerCase():r==xe||r==de?void 0:null}const D=60,x=62,Q=47,xe=63,de=33,$e=45;function W(e,O){this.name=e,this.parent=O,this.hash=O?O.hash:0;for(let a=0;a-1?new W(i(r,1)||"",e):e},reduce(e,O){return O==m&&e?e.parent:e},reuse(e,O,a,r){let t=O.type.id;return t==q||t==ce?new W(i(r,1)||"",e):e},hash(e){return e?e.hash:0},strict:!1}),ue=new T((e,O)=>{if(e.next!=D){e.next<0&&O.context&&e.acceptToken(d);return}e.advance();let a=e.next==Q;a&&e.advance();let r=i(e,0);if(r===void 0)return;if(!r)return e.acceptToken(a?se:q);let t=O.context?O.context.name:null;if(a){if(r==t)return e.acceptToken(re);if(t&&Te[t])return e.acceptToken(d,-2);if(O.dialectEnabled(fe))return e.acceptToken(ae);for(let n=O.context;n;n=n.parent)if(n.name==r)return;e.acceptToken(ne)}else{if(r=="script")return e.acceptToken(A);if(r=="style")return e.acceptToken(C);if(r=="textarea")return e.acceptToken(y);if(_e.hasOwnProperty(r))return e.acceptToken(Y);t&&B[t]&&B[t][r]?e.acceptToken(d,-1):e.acceptToken(q)}},{contextual:!0}),Xe=new T(e=>{for(let O=0,a=0;;a++){if(e.next<0){a&&e.acceptToken(b);break}if(e.next==$e)O++;else if(e.next==x&&O>=2){a>3&&e.acceptToken(b,-2);break}else O=0;e.advance()}});function ge(e){for(;e;e=e.parent)if(e.name=="svg"||e.name=="math")return!0;return!1}const ve=new T((e,O)=>{if(e.next==Q&&e.peek(1)==x){let a=O.dialectEnabled(Ve)||ge(O.context);e.acceptToken(a?te:w,2)}else e.next==x&&e.acceptToken(w,1)});function u(e,O,a){let r=2+e.length;return new T(t=>{for(let n=0,P=0,s=0;;s++){if(t.next<0){s&&t.acceptToken(O);break}if(n==0&&t.next==D||n==1&&t.next==Q||n>=2&&nP?t.acceptToken(O,-P):t.acceptToken(a,-(P-2));break}else if((t.next==10||t.next==13)&&s){t.acceptToken(O,1);break}else n=P=0;t.advance()}})}const we=u("script",z,L),Ae=u("style",F,K),Ce=u("textarea",ee,Oe),ye=J({"Text RawText":S.content,"StartTag StartCloseTag SelfClosingEndTag EndTag":S.angleBracket,TagName:S.tagName,"MismatchedCloseTag/TagName":[S.tagName,S.invalid],AttributeName:S.attributeName,"AttributeValue UnquotedAttributeValue":S.attributeValue,Is:S.definitionOperator,"EntityReference CharacterReference":S.character,Comment:S.blockComment,ProcessingInst:S.processingInstruction,DoctypeDecl:S.documentMeta}),Ye=I.deserialize({version:14,states:",xOVO!rOOO!WQ#tO'#CqO!]Q#tO'#CzO!bQ#tO'#C}O!gQ#tO'#DQO!lQ#tO'#DSO!qOaO'#CpO!|ObO'#CpO#XOdO'#CpO$eO!rO'#CpOOO`'#Cp'#CpO$lO$fO'#DTO$tQ#tO'#DVO$yQ#tO'#DWOOO`'#Dk'#DkOOO`'#DY'#DYQVO!rOOO%OQ&rO,59]O%WQ&rO,59fO%`Q&rO,59iO%hQ&rO,59lO%sQ&rO,59nOOOa'#D^'#D^O%{OaO'#CxO&WOaO,59[OOOb'#D_'#D_O&`ObO'#C{O&kObO,59[OOOd'#D`'#D`O&sOdO'#DOO'OOdO,59[OOO`'#Da'#DaO'WO!rO,59[O'_Q#tO'#DROOO`,59[,59[OOOp'#Db'#DbO'dO$fO,59oOOO`,59o,59oO'lQ#|O,59qO'qQ#|O,59rOOO`-E7W-E7WO'vQ&rO'#CsOOQW'#DZ'#DZO(UQ&rO1G.wOOOa1G.w1G.wO(^Q&rO1G/QOOOb1G/Q1G/QO(fQ&rO1G/TOOOd1G/T1G/TO(nQ&rO1G/WOOO`1G/W1G/WOOO`1G/Y1G/YO(yQ&rO1G/YOOOa-E7[-E7[O)RQ#tO'#CyOOO`1G.v1G.vOOOb-E7]-E7]O)WQ#tO'#C|OOOd-E7^-E7^O)]Q#tO'#DPOOO`-E7_-E7_O)bQ#|O,59mOOOp-E7`-E7`OOO`1G/Z1G/ZOOO`1G/]1G/]OOO`1G/^1G/^O)gQ,UO,59_OOQW-E7X-E7XOOOa7+$c7+$cOOOb7+$l7+$lOOOd7+$o7+$oOOO`7+$r7+$rOOO`7+$t7+$tO)rQ#|O,59eO)wQ#|O,59hO)|Q#|O,59kOOO`1G/X1G/XO*RO7[O'#CvO*dOMhO'#CvOOQW1G.y1G.yOOO`1G/P1G/POOO`1G/S1G/SOOO`1G/V1G/VOOOO'#D['#D[O*uO7[O,59bOOQW,59b,59bOOOO'#D]'#D]O+WOMhO,59bOOOO-E7Y-E7YOOQW1G.|1G.|OOOO-E7Z-E7Z",stateData:"+s~O!^OS~OUSOVPOWQOXROYTO[]O][O^^O`^Oa^Ob^Oc^Ox^O{_O!dZO~OfaO~OfbO~OfcO~OfdO~OfeO~O!WfOPlP!ZlP~O!XiOQoP!ZoP~O!YlORrP!ZrP~OUSOVPOWQOXROYTOZqO[]O][O^^O`^Oa^Ob^Oc^Ox^O!dZO~O!ZrO~P#dO![sO!euO~OfvO~OfwO~OS|OhyO~OS!OOhyO~OS!QOhyO~OS!SOT!TOhyO~OS!TOhyO~O!WfOPlX!ZlX~OP!WO!Z!XO~O!XiOQoX!ZoX~OQ!ZO!Z!XO~O!YlORrX!ZrX~OR!]O!Z!XO~O!Z!XO~P#dOf!_O~O![sO!e!aO~OS!bO~OS!cO~Oi!dOSgXhgXTgX~OS!fOhyO~OS!gOhyO~OS!hOhyO~OS!iOT!jOhyO~OS!jOhyO~Of!kO~Of!lO~Of!mO~OS!nO~Ok!qO!`!oO!b!pO~OS!rO~OS!sO~OS!tO~Oa!uOb!uOc!uO!`!wO!a!uO~Oa!xOb!xOc!xO!b!wO!c!xO~Oa!uOb!uOc!uO!`!{O!a!uO~Oa!xOb!xOc!xO!b!{O!c!xO~OT~bac!dx{!d~",goto:"%p!`PPPPPPPPPPPPPPPPPPPP!a!gP!mPP!yP!|#P#S#Y#]#`#f#i#l#r#x!aP!a!aP$O$U$l$r$x%O%U%[%bPPPPPPPP%hX^OX`pXUOX`pezabcde{}!P!R!UR!q!dRhUR!XhXVOX`pRkVR!XkXWOX`pRnWR!XnXXOX`pQrXR!XpXYOX`pQ`ORx`Q{aQ}bQ!PcQ!RdQ!UeZ!e{}!P!R!UQ!v!oR!z!vQ!y!pR!|!yQgUR!VgQjVR!YjQmWR![mQpXR!^pQtZR!`tS_O`ToXp",nodeNames:"\u26A0 StartCloseTag StartCloseTag StartCloseTag EndTag SelfClosingEndTag StartTag StartTag StartTag StartTag StartTag StartCloseTag StartCloseTag StartCloseTag IncompleteCloseTag Document Text EntityReference CharacterReference InvalidEntity Element OpenTag TagName Attribute AttributeName Is AttributeValue UnquotedAttributeValue ScriptText CloseTag OpenTag StyleText CloseTag OpenTag TextareaText CloseTag OpenTag CloseTag SelfClosingTag Comment ProcessingInst MismatchedCloseTag CloseTag DoctypeDecl",maxTerm:67,context:Qe,nodeProps:[["closedBy",-10,1,2,3,7,8,9,10,11,12,13,"EndTag",6,"EndTag SelfClosingEndTag",-4,21,30,33,36,"CloseTag"],["openedBy",4,"StartTag StartCloseTag",5,"StartTag",-4,29,32,35,37,"OpenTag"],["group",-9,14,17,18,19,20,39,40,41,42,"Entity",16,"Entity TextContent",-3,28,31,34,"TextContent Entity"]],propSources:[ye],skippedNodes:[0],repeatNodeCount:9,tokenData:"#%g!aR!YOX$qXY,QYZ,QZ[$q[]&X]^,Q^p$qpq,Qqr-_rs4ysv-_vw5iwxJ^x}-_}!OKP!O!P-_!P!Q$q!Q![-_![!]!!O!]!^-_!^!_!&W!_!`#$o!`!a&X!a!c-_!c!}!!O!}#R-_#R#S!!O#S#T3V#T#o!!O#o#s-_#s$f$q$f%W-_%W%o!!O%o%p-_%p&a!!O&a&b-_&b1p!!O1p4U-_4U4d!!O4d4e-_4e$IS!!O$IS$I`-_$I`$Ib!!O$Ib$Kh-_$Kh%#t!!O%#t&/x-_&/x&Et!!O&Et&FV-_&FV;'S!!O;'S;:j!&Q;:j;=`4s<%l?&r-_?&r?Ah!!O?Ah?BY$q?BY?Mn!!O?MnO$q!Z$|c`PkW!a`!cpOX$qXZ&XZ[$q[^&X^p$qpq&Xqr$qrs&}sv$qvw+Pwx(tx!^$q!^!_*V!_!a&X!a#S$q#S#T&X#T;'S$q;'S;=`+z<%lO$q!R&bX`P!a`!cpOr&Xrs&}sv&Xwx(tx!^&X!^!_*V!_;'S&X;'S;=`*y<%lO&Xq'UV`P!cpOv&}wx'kx!^&}!^!_(V!_;'S&};'S;=`(n<%lO&}P'pT`POv'kw!^'k!_;'S'k;'S;=`(P<%lO'kP(SP;=`<%l'kp([S!cpOv(Vx;'S(V;'S;=`(h<%lO(Vp(kP;=`<%l(Vq(qP;=`<%l&}a({W`P!a`Or(trs'ksv(tw!^(t!^!_)e!_;'S(t;'S;=`*P<%lO(t`)jT!a`Or)esv)ew;'S)e;'S;=`)y<%lO)e`)|P;=`<%l)ea*SP;=`<%l(t!Q*^V!a`!cpOr*Vrs(Vsv*Vwx)ex;'S*V;'S;=`*s<%lO*V!Q*vP;=`<%l*V!R*|P;=`<%l&XW+UYkWOX+PZ[+P^p+Pqr+Psw+Px!^+P!a#S+P#T;'S+P;'S;=`+t<%lO+PW+wP;=`<%l+P!Z+}P;=`<%l$q!a,]``P!a`!cp!^^OX&XXY,QYZ,QZ]&X]^,Q^p&Xpq,Qqr&Xrs&}sv&Xwx(tx!^&X!^!_*V!_;'S&X;'S;=`*y<%lO&X!_-ljhS`PkW!a`!cpOX$qXZ&XZ[$q[^&X^p$qpq&Xqr-_rs&}sv-_vw/^wx(tx!P-_!P!Q$q!Q!^-_!^!_1n!_!a&X!a#S-_#S#T3V#T#s-_#s$f$q$f;'S-_;'S;=`4s<%l?Ah-_?Ah?BY$q?BY?Mn-_?MnO$q[/echSkWOX+PZ[+P^p+Pqr/^sw/^x!P/^!P!Q+P!Q!^/^!^!_0p!a#S/^#S#T0p#T#s/^#s$f+P$f;'S/^;'S;=`1h<%l?Ah/^?Ah?BY+P?BY?Mn/^?MnO+PS0uXhSqr0psw0px!P0p!Q!_0p!a#s0p$f;'S0p;'S;=`1b<%l?Ah0p?BY?Mn0pS1eP;=`<%l0p[1kP;=`<%l/^!U1wbhS!a`!cpOq*Vqr1nrs(Vsv1nvw0pwx)ex!P1n!P!Q*V!Q!_1n!_!a*V!a#s1n#s$f*V$f;'S1n;'S;=`3P<%l?Ah1n?Ah?BY*V?BY?Mn1n?MnO*V!U3SP;=`<%l1n!V3bchS`P!a`!cpOq&Xqr3Vrs&}sv3Vvw0pwx(tx!P3V!P!Q&X!Q!^3V!^!_1n!_!a&X!a#s3V#s$f&X$f;'S3V;'S;=`4m<%l?Ah3V?Ah?BY&X?BY?Mn3V?MnO&X!V4pP;=`<%l3V!_4vP;=`<%l-_!Z5SV!`h`P!cpOv&}wx'kx!^&}!^!_(V!_;'S&};'S;=`(n<%lO&}!_5rjhSkWc!ROX7dXZ8qZ[7d[^8q^p7dqr:crs8qst@Ttw:cwx8qx!P:c!P!Q7d!Q!]:c!]!^/^!^!_=p!_!a8q!a#S:c#S#T=p#T#s:c#s$f7d$f;'S:c;'S;=`?}<%l?Ah:c?Ah?BY7d?BY?Mn:c?MnO7d!Z7ibkWOX7dXZ8qZ[7d[^8q^p7dqr7drs8qst+Ptw7dwx8qx!]7d!]!^9f!^!a8q!a#S7d#S#T8q#T;'S7d;'S;=`:]<%lO7d!R8tVOp8qqs8qt!]8q!]!^9Z!^;'S8q;'S;=`9`<%lO8q!R9`Oa!R!R9cP;=`<%l8q!Z9mYkWa!ROX+PZ[+P^p+Pqr+Psw+Px!^+P!a#S+P#T;'S+P;'S;=`+t<%lO+P!Z:`P;=`<%l7d!_:jjhSkWOX7dXZ8qZ[7d[^8q^p7dqr:crs8qst/^tw:cwx8qx!P:c!P!Q7d!Q!]:c!]!^<[!^!_=p!_!a8q!a#S:c#S#T=p#T#s:c#s$f7d$f;'S:c;'S;=`?}<%l?Ah:c?Ah?BY7d?BY?Mn:c?MnO7d!_b#d#s1n#s$f*V$f;'S1n;'S;=`3P<%l?Ah1n?Ah?BY*V?BY?Mn1n?MnO*V!V!>kdhS!a`!cpOq*Vqr1nrs(Vsv1nvw0pwx)ex!P1n!P!Q*V!Q!_1n!_!a*V!a#V1n#V#W!?y#W#s1n#s$f*V$f;'S1n;'S;=`3P<%l?Ah1n?Ah?BY*V?BY?Mn1n?MnO*V!V!@SdhS!a`!cpOq*Vqr1nrs(Vsv1nvw0pwx)ex!P1n!P!Q*V!Q!_1n!_!a*V!a#h1n#h#i!Ab#i#s1n#s$f*V$f;'S1n;'S;=`3P<%l?Ah1n?Ah?BY*V?BY?Mn1n?MnO*V!V!AkdhS!a`!cpOq*Vqr1nrs(Vsv1nvw0pwx)ex!P1n!P!Q*V!Q!_1n!_!a*V!a#m1n#m#n!By#n#s1n#s$f*V$f;'S1n;'S;=`3P<%l?Ah1n?Ah?BY*V?BY?Mn1n?MnO*V!V!CSdhS!a`!cpOq*Vqr1nrs(Vsv1nvw0pwx)ex!P1n!P!Q*V!Q!_1n!_!a*V!a#d1n#d#e!Db#e#s1n#s$f*V$f;'S1n;'S;=`3P<%l?Ah1n?Ah?BY*V?BY?Mn1n?MnO*V!V!DkdhS!a`!cpOq*Vqr1nrs(Vsv1nvw0pwx)ex!P1n!P!Q*V!Q!_1n!_!a*V!a#X1n#X#Y!5]#Y#s1n#s$f*V$f;'S1n;'S;=`3P<%l?Ah1n?Ah?BY*V?BY?Mn1n?MnO*V!V!FSchS!a`!cpOq!G_qr!Eyrs!HUsv!Eyvw!Ncwx!Jvx!P!Ey!P!Q!G_!Q!_!Ey!_!a!G_!a!b##T!b#s!Ey#s$f!G_$f;'S!Ey;'S;=`#$i<%l?Ah!Ey?Ah?BY!G_?BY?Mn!Ey?MnO!G_!R!GfY!a`!cpOr!G_rs!HUsv!G_vw!Hpwx!Jvx!a!G_!a!b!Lv!b;'S!G_;'S;=`!N]<%lO!G_q!HZV!cpOv!HUvx!Hpx!a!HU!a!b!Iq!b;'S!HU;'S;=`!Jp<%lO!HUP!HsTO!a!Hp!a!b!IS!b;'S!Hp;'S;=`!Ik<%lO!HpP!IVTO!`!Hp!`!a!If!a;'S!Hp;'S;=`!Ik<%lO!HpP!IkOxPP!InP;=`<%l!Hpq!IvV!cpOv!HUvx!Hpx!`!HU!`!a!J]!a;'S!HU;'S;=`!Jp<%lO!HUq!JdS!cpxPOv(Vx;'S(V;'S;=`(h<%lO(Vq!JsP;=`<%l!HUa!J{X!a`Or!Jvrs!Hpsv!Jvvw!Hpw!a!Jv!a!b!Kh!b;'S!Jv;'S;=`!Lp<%lO!Jva!KmX!a`Or!Jvrs!Hpsv!Jvvw!Hpw!`!Jv!`!a!LY!a;'S!Jv;'S;=`!Lp<%lO!Jva!LaT!a`xPOr)esv)ew;'S)e;'S;=`)y<%lO)ea!LsP;=`<%l!Jv!R!L}Y!a`!cpOr!G_rs!HUsv!G_vw!Hpwx!Jvx!`!G_!`!a!Mm!a;'S!G_;'S;=`!N]<%lO!G_!R!MvV!a`!cpxPOr*Vrs(Vsv*Vwx)ex;'S*V;'S;=`*s<%lO*V!R!N`P;=`<%l!G_T!NhbhSOq!Hpqr!Ncrs!Hpsw!Ncwx!Hpx!P!Nc!P!Q!Hp!Q!_!Nc!_!a!Hp!a!b# p!b#s!Nc#s$f!Hp$f;'S!Nc;'S;=`#!}<%l?Ah!Nc?Ah?BY!Hp?BY?Mn!Nc?MnO!HpT# ubhSOq!Hpqr!Ncrs!Hpsw!Ncwx!Hpx!P!Nc!P!Q!Hp!Q!_!Nc!_!`!Hp!`!a!If!a#s!Nc#s$f!Hp$f;'S!Nc;'S;=`#!}<%l?Ah!Nc?Ah?BY!Hp?BY?Mn!Nc?MnO!HpT##QP;=`<%l!Nc!V##^chS!a`!cpOq!G_qr!Eyrs!HUsv!Eyvw!Ncwx!Jvx!P!Ey!P!Q!G_!Q!_!Ey!_!`!G_!`!a!Mm!a#s!Ey#s$f!G_$f;'S!Ey;'S;=`#$i<%l?Ah!Ey?Ah?BY!G_?BY?Mn!Ey?MnO!G_!V#$lP;=`<%l!Ey!V#$zXiS`P!a`!cpOr&Xrs&}sv&Xwx(tx!^&X!^!_*V!_;'S&X;'S;=`*y<%lO&X",tokenizers:[we,Ae,Ce,ve,ue,Xe,0,1,2,3,4,5],topRules:{Document:[0,15]},dialects:{noMatch:0,selfClosing:485},tokenPrec:487});function H(e,O){let a=Object.create(null);for(let r of e.getChildren(M)){let t=r.getChild(Se),n=r.getChild($)||r.getChild(k);t&&(a[O.read(t.from,t.to)]=n?n.type.id==$?O.read(n.from+1,n.to-1):O.read(n.from,n.to):"")}return a}function N(e,O){let a=e.getChild(le);return a?O.read(a.from,a.to):" "}function X(e,O,a){let r;for(let t of a)if(!t.attrs||t.attrs(r||(r=H(e.node.parent.firstChild,O))))return{parser:t.parser};return null}function be(e=[],O=[]){let a=[],r=[],t=[],n=[];for(let s of e)(s.tag=="script"?a:s.tag=="style"?r:s.tag=="textarea"?t:n).push(s);let P=O.length?Object.create(null):null;for(let s of O)(P[s.name]||(P[s.name]=[])).push(s);return j((s,p)=>{let f=s.type.id;if(f==Pe)return X(s,p,a);if(f==pe)return X(s,p,r);if(f==oe)return X(s,p,t);if(f==m&&n.length){let o=s.node,c=o.firstChild,V=c&&N(c,p),h;if(V){for(let l of n)if(l.tag==V&&(!l.attrs||l.attrs(h||(h=H(o,p))))){let _=o.lastChild;return{parser:l.parser,overlay:[{from:c.to,to:_.type.id==he?_.from:o.to}]}}}}if(P&&f==M){let o=s.node,c;if(c=o.firstChild){let V=P[p.read(c.from,c.to)];if(V)for(let h of V){if(h.tagName&&h.tagName!=N(o.parent,p))continue;let l=o.lastChild;if(l.type.id==$){let _=l.from+1,g=l.lastChild,v=l.to-(g&&g.isError?0:1);if(v>_)return{parser:h.parser,overlay:[{from:_,to:v}]}}else if(l.type.id==k)return{parser:h.parser,overlay:[{from:l.from,to:l.to}]}}}}return null})}export{be as configureNesting,Ye as parser}; diff --git a/Resources/Public/JavaScript/Contrib/@lezer/javascript.js b/Resources/Public/JavaScript/Contrib/@lezer/javascript.js new file mode 100644 index 0000000..f8b612e --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@lezer/javascript.js @@ -0,0 +1 @@ +import{ContextTracker as n,ExternalTokenizer as $,LRParser as t,LocalTokenGroup as e}from"@lezer/lr";import{styleTags as Y,tags as O}from"@lezer/highlight";const c=309,o=1,X=2,l=3,s=310,W=312,w=313,d=4,j=5,g=0,r=[9,10,11,12,13,32,133,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288],S=125,f=59,P=47,k=42,b=43,y=45,m=60,x=44,q=new n({start:!1,shift(Q,i){return i==d||i==j||i==W?Q:i==w},strict:!1}),_=new $((Q,i)=>{let{next:a}=Q;(a==S||a==-1||i.context)&&Q.acceptToken(s)},{contextual:!0,fallback:!0}),h=new $((Q,i)=>{let{next:a}=Q,Z;r.indexOf(a)>-1||a==P&&((Z=Q.peek(1))==P||Z==k)||a!=S&&a!=f&&a!=-1&&!i.context&&Q.acceptToken(c)},{contextual:!0}),G=new $((Q,i)=>{let{next:a}=Q;if((a==b||a==y)&&(Q.advance(),a==Q.next)){Q.advance();let Z=!i.context&&i.canShift(o);Q.acceptToken(Z?o:X)}},{contextual:!0});function p(Q,i){return Q>=65&&Q<=90||Q>=97&&Q<=122||Q==95||Q>=192||!i&&Q>=48&&Q<=57}const R=new $((Q,i)=>{if(Q.next!=m||!i.dialectEnabled(g)||(Q.advance(),Q.next==P))return;let a=0;for(;r.indexOf(Q.next)>-1;)Q.advance(),a++;if(p(Q.next,!0)){for(Q.advance(),a++;p(Q.next,!1);)Q.advance(),a++;for(;r.indexOf(Q.next)>-1;)Q.advance(),a++;if(Q.next==x)return;for(let Z=0;;Z++){if(Z==7){if(!p(Q.next,!0))return;break}if(Q.next!="extends".charCodeAt(Z))break;Q.advance(),a++}}Q.acceptToken(l,-a)}),T=Y({"get set async static":O.modifier,"for while do if else switch try catch finally return throw break continue default case":O.controlKeyword,"in of await yield void typeof delete instanceof":O.operatorKeyword,"let var const using function class extends":O.definitionKeyword,"import export from":O.moduleKeyword,"with debugger as new":O.keyword,TemplateString:O.special(O.string),super:O.atom,BooleanLiteral:O.bool,this:O.self,null:O.null,Star:O.modifier,VariableName:O.variableName,"CallExpression/VariableName TaggedTemplateExpression/VariableName":O.function(O.variableName),VariableDefinition:O.definition(O.variableName),Label:O.labelName,PropertyName:O.propertyName,PrivatePropertyName:O.special(O.propertyName),"CallExpression/MemberExpression/PropertyName":O.function(O.propertyName),"FunctionDeclaration/VariableDefinition":O.function(O.definition(O.variableName)),"ClassDeclaration/VariableDefinition":O.definition(O.className),PropertyDefinition:O.definition(O.propertyName),PrivatePropertyDefinition:O.definition(O.special(O.propertyName)),UpdateOp:O.updateOperator,"LineComment Hashbang":O.lineComment,BlockComment:O.blockComment,Number:O.number,String:O.string,Escape:O.escape,ArithOp:O.arithmeticOperator,LogicOp:O.logicOperator,BitOp:O.bitwiseOperator,CompareOp:O.compareOperator,RegExp:O.regexp,Equals:O.definitionOperator,Arrow:O.function(O.punctuation),": Spread":O.punctuation,"( )":O.paren,"[ ]":O.squareBracket,"{ }":O.brace,"InterpolationStart InterpolationEnd":O.special(O.brace),".":O.derefOperator,", ;":O.separator,"@":O.meta,TypeName:O.typeName,TypeDefinition:O.definition(O.typeName),"type enum interface implements namespace module declare":O.definitionKeyword,"abstract global Privacy readonly override":O.modifier,"is keyof unique infer":O.operatorKeyword,JSXAttributeValue:O.attributeValue,JSXText:O.content,"JSXStartTag JSXStartCloseTag JSXSelfCloseEndTag JSXEndTag":O.angleBracket,"JSXIdentifier JSXNameSpacedName":O.tagName,"JSXAttribute/JSXIdentifier JSXAttribute/JSXNameSpacedName":O.attributeName,"JSXBuiltin/JSXIdentifier":O.standard(O.tagName)}),V={__proto__:null,export:18,as:23,from:31,default:34,async:39,function:40,extends:52,this:56,true:64,false:64,null:76,void:80,typeof:84,super:102,new:136,delete:152,yield:161,await:165,class:170,public:227,private:227,protected:227,readonly:229,instanceof:248,satisfies:251,in:252,const:254,import:286,keyof:339,unique:343,infer:349,is:385,abstract:405,implements:407,type:409,let:412,var:414,using:417,interface:423,enum:427,namespace:433,module:435,declare:439,global:443,for:462,of:471,while:474,with:478,do:482,if:486,else:488,switch:492,case:498,try:504,catch:508,finally:512,return:516,throw:520,break:524,continue:528,debugger:532},U={__proto__:null,async:123,get:125,set:127,declare:187,public:189,private:189,protected:189,static:191,abstract:193,override:195,readonly:201,accessor:203,new:389},u={__proto__:null,"<":143},z=t.deserialize({version:14,states:"$RQWO'#CdO>cQWO'#H[O>kQWO'#HbO>kQWO'#HdO`Q^O'#HfO>kQWO'#HhO>kQWO'#HkO>pQWO'#HqO>uQ07iO'#HwO%[Q^O'#HyO?QQ07iO'#H{O?]Q07iO'#H}O9kQ07hO'#IPO?hQ08SO'#ChO@jQ`O'#DiQOQWOOO%[Q^O'#EPOAQQWO'#ESO:RQ7[O'#EjOA]QWO'#EjOAhQpO'#FbOOQU'#Cf'#CfOOQ07`'#Dn'#DnOOQ07`'#Jm'#JmO%[Q^O'#JmOOQO'#Jq'#JqOOQO'#Ib'#IbOBhQ`O'#EcOOQ07`'#Eb'#EbOCdQ07pO'#EcOCnQ`O'#EVOOQO'#Jp'#JpODSQ`O'#JqOEaQ`O'#EVOCnQ`O'#EcPEnO!0LbO'#CaPOOO)CDu)CDuOOOO'#IX'#IXOEyO!bO,59TOOQ07b,59T,59TOOOO'#IY'#IYOFXO#tO,59TO%[Q^O'#D`OOOO'#I['#I[OFgO?MpO,59xOOQ07b,59x,59xOFuQ^O'#I]OGYQWO'#JkOI[QrO'#JkO+}Q^O'#JkOIcQWO,5:OOIyQWO'#ElOJWQWO'#JyOJcQWO'#JxOJcQWO'#JxOJkQWO,5;YOJpQWO'#JwOOQ07f,5:Z,5:ZOJwQ^O,5:ZOLxQ08SO,5:eOMiQWO,5:mONSQ07hO'#JvONZQWO'#JuO9ZQWO'#JuONoQWO'#JuONwQWO,5;XON|QWO'#JuO!#UQrO'#JjOOQ07b'#Ch'#ChO%[Q^O'#ERO!#tQpO,5:rOOQO'#Jr'#JrOOQO-EmOOQU'#J`'#J`OOQU,5>n,5>nOOQU-EpQWO'#HQO9aQWO'#HSO!CgQWO'#HSO:RQ7[O'#HUO!ClQWO'#HUOOQU,5=j,5=jO!CqQWO'#HVO!DSQWO'#CnO!DXQWO,59OO!DcQWO,59OO!FhQ^O,59OOOQU,59O,59OO!FxQ07hO,59OO%[Q^O,59OO!ITQ^O'#H^OOQU'#H_'#H_OOQU'#H`'#H`O`Q^O,5=vO!IkQWO,5=vO`Q^O,5=|O`Q^O,5>OO!IpQWO,5>QO`Q^O,5>SO!IuQWO,5>VO!IzQ^O,5>]OOQU,5>c,5>cO%[Q^O,5>cO9kQ07hO,5>eOOQU,5>g,5>gO!NUQWO,5>gOOQU,5>i,5>iO!NUQWO,5>iOOQU,5>k,5>kO!NZQ`O'#D[O%[Q^O'#JmO!NxQ`O'#JmO# gQ`O'#DjO# xQ`O'#DjO#$ZQ^O'#DjO#$bQWO'#JlO#$jQWO,5:TO#$oQWO'#EpO#$}QWO'#JzO#%VQWO,5;ZO#%[Q`O'#DjO#%iQ`O'#EUOOQ07b,5:n,5:nO%[Q^O,5:nO#%pQWO,5:nO>pQWO,5;UO!@}Q`O,5;UO!AVQ7[O,5;UO:RQ7[O,5;UO#%xQWO,5@XO#%}Q$ISO,5:rOOQO-E<`-E<`O#'TQ07pO,5:}OCnQ`O,5:qO#'_Q`O,5:qOCnQ`O,5:}O!@rQ07hO,5:qOOQ07`'#Ef'#EfOOQO,5:},5:}O%[Q^O,5:}O#'lQ07hO,5:}O#'wQ07hO,5:}O!@}Q`O,5:qOOQO,5;T,5;TO#(VQ07hO,5:}POOO'#IV'#IVP#(kO!0LbO,58{POOO,58{,58{OOOO-EwO+}Q^O,5>wOOQO,5>},5>}O#)VQ^O'#I]OOQO-EpQ08SO1G0{O#>wQ08SO1G0{O#@oQ08SO1G0{O#CoQ(CYO'#ChO#EmQ(CYO1G1^O#EtQ(CYO'#JjO!,lQWO1G1dO#FUQ08SO,5?TOOQ07`-EkQWO1G3lO$2^Q^O1G3nO$6bQ^O'#HmOOQU1G3q1G3qO$6oQWO'#HsO>pQWO'#HuOOQU1G3w1G3wO$6wQ^O1G3wO9kQ07hO1G3}OOQU1G4P1G4POOQ07`'#GY'#GYO9kQ07hO1G4RO9kQ07hO1G4TO$;OQWO,5@XO!*fQ^O,5;[O9ZQWO,5;[O>pQWO,5:UO!*fQ^O,5:UO!@}Q`O,5:UO$;TQ(CYO,5:UOOQO,5;[,5;[O$;_Q`O'#I^O$;uQWO,5@WOOQ07b1G/o1G/oO$;}Q`O'#IdO$pQWO1G0pO!@}Q`O1G0pO!AVQ7[O1G0pOOQ07`1G5s1G5sO!@rQ07hO1G0]OOQO1G0i1G0iO%[Q^O1G0iO$wO$>TQWO1G5qO$>]QWO1G6OO$>eQrO1G6PO9ZQWO,5>}O$>oQ08SO1G5|O%[Q^O1G5|O$?PQ07hO1G5|O$?bQWO1G5{O$?bQWO1G5{O9ZQWO1G5{O$?jQWO,5?QO9ZQWO,5?QOOQO,5?Q,5?QO$@OQWO,5?QO$'TQWO,5?QOOQO-EXOOQU,5>X,5>XO%[Q^O'#HnO%7^QWO'#HpOOQU,5>_,5>_O9ZQWO,5>_OOQU,5>a,5>aOOQU7+)c7+)cOOQU7+)i7+)iOOQU7+)m7+)mOOQU7+)o7+)oO%7cQ`O1G5sO%7wQ(CYO1G0vO%8RQWO1G0vOOQO1G/p1G/pO%8^Q(CYO1G/pO>pQWO1G/pO!*fQ^O'#DjOOQO,5>x,5>xOOQO-E<[-E<[OOQO,5?O,5?OOOQO-EpQWO7+&[O!@}Q`O7+&[OOQO7+%w7+%wO$=gQ08SO7+&TOOQO7+&T7+&TO%[Q^O7+&TO%8hQ07hO7+&TO!@rQ07hO7+%wO!@}Q`O7+%wO%8sQ07hO7+&TO%9RQ08SO7++hO%[Q^O7++hO%9cQWO7++gO%9cQWO7++gOOQO1G4l1G4lO9ZQWO1G4lO%9kQWO1G4lOOQO7+%|7+%|O#%sQWO<tQ08SO1G2ZO%AVQ08SO1G2mO%CbQ08SO1G2oO%EmQ7[O,5>yOOQO-E<]-E<]O%EwQrO,5>zO%[Q^O,5>zOOQO-E<^-E<^O%FRQWO1G5uOOQ07b<YOOQU,5>[,5>[O&5cQWO1G3yO9ZQWO7+&bO!*fQ^O7+&bOOQO7+%[7+%[O&5hQ(CYO1G6PO>pQWO7+%[OOQ07b<pQWO<pQWO7+)eO'&gQWO<}AN>}O%[Q^OAN?ZOOQO<eQ(CYOG26}O!*fQ^O'#DyO1PQWO'#EWO'@ZQrO'#JiO!*fQ^O'#DqO'@bQ^O'#D}O'@iQrO'#ChO'CPQrO'#ChO!*fQ^O'#EPO'CaQ^O,5;VO!*fQ^O,5;aO!*fQ^O,5;aO!*fQ^O,5;aO!*fQ^O,5;aO!*fQ^O,5;aO!*fQ^O,5;aO!*fQ^O,5;aO!*fQ^O,5;aO!*fQ^O,5;aO!*fQ^O,5;aO!*fQ^O,5;aO!*fQ^O'#IiO'EdQWO,5a#@O#@^#@d#Ax#BW#Cr#DQ#DW#D^#Dd#Dn#Dt#Dz#EU#Eh#EnPPPPPPPPPP#EtPPPPPPP#Fi#Ip#KP#KW#K`PPPP$!d$%Z$+r$+u$+x$,q$,t$,w$-O$-WPP$-^$-b$.Y$/X$/]$/qPP$/u$/{$0PP$0S$0W$0Z$1P$1h$2P$2T$2W$2Z$2a$2d$2h$2lR!{RoqOXst!Z#c%j&m&o&p&r,h,m1w1zY!uQ'Z-Y1[5]Q%pvQ%xyQ&P|Q&e!VS'R!e-QQ'a!iS'g!r!xS*c$|*hQ+f%yQ+s&RQ,X&_Q-W'YQ-b'bQ-j'hQ/|*jQ1f,YR;Y:g%OdOPWXYZstuvw!Z!`!g!o#R#V#Y#c#n#t#x#{$O$P$Q$R$S$T$U$V$W$X$Y$a$e%j%p%}&f&i&m&o&p&r&v'O']'m'}(P(V(^(r(v(z)y+O+S,e,h,m-^-f-t-z.l.s0[0a0q1_1o1p1r1t1w1z1|2m2s3Z5Y5d5t5u5x6]7w7|8]8gS#p]:d!r)[$[$m'S)n,y,|.{2]3p5W6S9W9i:c:f:g:j:k:l:m:n:o:p:q:r:s:t:u:v:w:{;Y;Z;[;^;e;f;o;p<]Q*u%ZQ+k%{Q,Z&bQ,b&jQ.c;QQ0h+^Q0l+`Q0w+lQ1n,`Q2{.[Q4v0rQ5k1gQ6i3PQ6u;RQ7h4wR8m6j&|kOPWXYZstuvw!Z!`!g!o#R#V#Y#c#n#t#x#{$O$P$Q$R$S$T$U$V$W$X$Y$[$a$e$m%j%p%}&f&i&j&m&o&p&r&v'O'S']'m'}(P(V(^(r(v(z)n)y+O+S+^,e,h,m,y,|-^-f-t-z.[.l.s.{0[0a0q1_1o1p1r1t1w1z1|2]2m2s3P3Z3p5W5Y5d5t5u5x6S6]6j7w7|8]8g9W9i:c:f:g:j:k:l:m:n:o:p:q:r:s:t:u:v:w:{;Y;Z;[;^;e;f;o;p<]t!nQ!r!u!x!y'R'Y'Z'g'h'i-Q-W-Y-j1[5]5_$v$si#u#w$c$d$x${%O%Q%[%]%a)u){)}*P*R*Y*`*p*q+]+`+w+z.Z.i/Z/j/k/m0Q0S0^1R1U1^3O3x4S4[4f4n4p5c6g7T7^7y8j8w9[9n:O:W:y:z:|:};O;P;S;T;U;V;W;X;_;`;a;b;c;d;g;h;i;j;k;l;m;n;q;r < TypeParamList TypeDefinition extends ThisType this LiteralType ArithOp Number BooleanLiteral TemplateType InterpolationEnd Interpolation InterpolationStart NullType null VoidType void TypeofType typeof MemberExpression . ?. PropertyName [ TemplateString Escape Interpolation super RegExp ] ArrayExpression Spread , } { ObjectExpression Property async get set PropertyDefinition Block : NewExpression new TypeArgList CompareOp < ) ( ArgList UnaryExpression delete LogicOp BitOp YieldExpression yield AwaitExpression await ParenthesizedExpression ClassExpression class ClassBody MethodDeclaration Decorator @ MemberExpression PrivatePropertyName CallExpression declare Privacy static abstract override PrivatePropertyDefinition PropertyDeclaration readonly accessor Optional TypeAnnotation Equals StaticBlock FunctionExpression ArrowFunction ParamList ParamList ArrayPattern ObjectPattern PatternProperty Privacy readonly Arrow MemberExpression BinaryExpression ArithOp ArithOp ArithOp ArithOp BitOp CompareOp instanceof satisfies in const CompareOp BitOp BitOp BitOp LogicOp LogicOp ConditionalExpression LogicOp LogicOp AssignmentExpression UpdateOp PostfixExpression CallExpression TaggedTemplateExpression DynamicImport import ImportMeta JSXElement JSXSelfCloseEndTag JSXSelfClosingTag JSXIdentifier JSXBuiltin JSXIdentifier JSXNamespacedName JSXMemberExpression JSXSpreadAttribute JSXAttribute JSXAttributeValue JSXEscape JSXEndTag JSXOpenTag JSXFragmentTag JSXText JSXEscape JSXStartCloseTag JSXCloseTag PrefixCast ArrowFunction TypeParamList SequenceExpression KeyofType keyof UniqueType unique ImportType InferredType infer TypeName ParenthesizedType FunctionSignature ParamList NewSignature IndexedType TupleType Label ArrayType ReadonlyType ObjectType MethodType PropertyType IndexSignature PropertyDefinition CallSignature TypePredicate is NewSignature new UnionType LogicOp IntersectionType LogicOp ConditionalType ParameterizedType ClassDeclaration abstract implements type VariableDeclaration let var using TypeAliasDeclaration InterfaceDeclaration interface EnumDeclaration enum EnumBody NamespaceDeclaration namespace module AmbientDeclaration declare GlobalDeclaration global ClassDeclaration ClassBody AmbientFunctionDeclaration ExportGroup VariableName VariableName ImportDeclaration ImportGroup ForStatement for ForSpec ForInSpec ForOfSpec of WhileStatement while WithStatement with DoStatement do IfStatement if else SwitchStatement switch SwitchBody CaseLabel case DefaultLabel TryStatement try CatchClause catch FinallyClause finally ReturnStatement return ThrowStatement throw BreakStatement break ContinueStatement continue DebuggerStatement debugger LabeledStatement ExpressionStatement SingleExpression SingleClassItem",maxTerm:371,context:q,nodeProps:[["group",-26,8,16,18,65,201,205,209,210,212,215,218,228,230,236,238,240,242,245,251,257,259,261,263,265,267,268,"Statement",-32,12,13,28,31,32,38,48,51,52,54,59,67,75,79,81,83,84,106,107,116,117,134,137,139,140,141,142,144,145,164,165,167,"Expression",-23,27,29,33,37,39,41,168,170,172,173,175,176,177,179,180,181,183,184,185,195,197,199,200,"Type",-3,87,99,105,"ClassItem"],["openedBy",22,"<",34,"InterpolationStart",53,"[",57,"{",72,"(",157,"JSXStartCloseTag"],["closedBy",23,">",36,"InterpolationEnd",47,"]",58,"}",73,")",162,"JSXEndTag"]],propSources:[T],skippedNodes:[0,4,5,271],repeatNodeCount:37,tokenData:"$Fj(CSR!bOX%ZXY+gYZ-yZ[+g[]%Z]^.c^p%Zpq+gqr/mrs3cst:_tuEruvJSvwLkwx! Yxy!'iyz!(sz{!)}{|!,q|}!.O}!O!,q!O!P!/Y!P!Q!9j!Q!R#8g!R![#:v![!]#Gv!]!^#IS!^!_#J^!_!`#Ns!`!a$#_!a!b$(l!b!c$,k!c!}Er!}#O$-u#O#P$/P#P#Q$4h#Q#R$5r#R#SEr#S#T$7P#T#o$8Z#o#p$q#r#s$?}#s$f%Z$f$g+g$g#BYEr#BY#BZ$AX#BZ$ISEr$IS$I_$AX$I_$I|Er$I|$I}$Dd$I}$JO$Dd$JO$JTEr$JT$JU$AX$JU$KVEr$KV$KW$AX$KW&FUEr&FU&FV$AX&FV;'SEr;'S;=`I|<%l?HTEr?HT?HU$AX?HUOEr(n%d_$f&j(Op(R!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z&j&hT$f&jO!^&c!_#o&c#p;'S&c;'S;=`&w<%lO&c&j&zP;=`<%l&c'|'U]$f&j(R!bOY&}YZ&cZw&}wx&cx!^&}!^!_'}!_#O&}#O#P&c#P#o&}#o#p'}#p;'S&};'S;=`(l<%lO&}!b(SU(R!bOY'}Zw'}x#O'}#P;'S'};'S;=`(f<%lO'}!b(iP;=`<%l'}'|(oP;=`<%l&}'[(y]$f&j(OpOY(rYZ&cZr(rrs&cs!^(r!^!_)r!_#O(r#O#P&c#P#o(r#o#p)r#p;'S(r;'S;=`*a<%lO(rp)wU(OpOY)rZr)rs#O)r#P;'S)r;'S;=`*Z<%lO)rp*^P;=`<%l)r'[*dP;=`<%l(r#S*nX(Op(R!bOY*gZr*grs'}sw*gwx)rx#O*g#P;'S*g;'S;=`+Z<%lO*g#S+^P;=`<%l*g(n+dP;=`<%l%Z(CS+rq$f&j(Op(R!b't(;dOX%ZXY+gYZ&cZ[+g[p%Zpq+gqr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p$f%Z$f$g+g$g#BY%Z#BY#BZ+g#BZ$IS%Z$IS$I_+g$I_$JT%Z$JT$JU+g$JU$KV%Z$KV$KW+g$KW&FU%Z&FU&FV+g&FV;'S%Z;'S;=`+a<%l?HT%Z?HT?HU+g?HUO%Z(CS.ST(P#S$f&j'u(;dO!^&c!_#o&c#p;'S&c;'S;=`&w<%lO&c(CS.n_$f&j(Op(R!b'u(;dOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z%#`/x`$f&j!o$Ip(Op(R!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_!`0z!`#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z%#S1V`#t$Id$f&j(Op(R!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_!`2X!`#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z%#S2d_#t$Id$f&j(Op(R!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z$/|3l_'}$(n$f&j(R!bOY4kYZ5qZr4krs7nsw4kwx5qx!^4k!^!_8p!_#O4k#O#P5q#P#o4k#o#p8p#p;'S4k;'S;=`:X<%lO4k(^4r_$f&j(R!bOY4kYZ5qZr4krs7nsw4kwx5qx!^4k!^!_8p!_#O4k#O#P5q#P#o4k#o#p8p#p;'S4k;'S;=`:X<%lO4k&z5vX$f&jOr5qrs6cs!^5q!^!_6y!_#o5q#o#p6y#p;'S5q;'S;=`7h<%lO5q&z6jT$a`$f&jO!^&c!_#o&c#p;'S&c;'S;=`&w<%lO&c`6|TOr6yrs7]s;'S6y;'S;=`7b<%lO6y`7bO$a``7eP;=`<%l6y&z7kP;=`<%l5q(^7w]$a`$f&j(R!bOY&}YZ&cZw&}wx&cx!^&}!^!_'}!_#O&}#O#P&c#P#o&}#o#p'}#p;'S&};'S;=`(l<%lO&}!r8uZ(R!bOY8pYZ6yZr8prs9hsw8pwx6yx#O8p#O#P6y#P;'S8p;'S;=`:R<%lO8p!r9oU$a`(R!bOY'}Zw'}x#O'}#P;'S'};'S;=`(f<%lO'}!r:UP;=`<%l8p(^:[P;=`<%l4k#%|:hh$f&j(Op(R!bOY%ZYZ&cZq%Zqr`#P#o`x!^=^!^!_?q!_#O=^#O#P>`#P#o=^#o#p?q#p;'S=^;'S;=`@h<%lO=^&n>gXVS$f&jOY>`YZ&cZ!^>`!^!_?S!_#o>`#o#p?S#p;'S>`;'S;=`?k<%lO>`S?XSVSOY?SZ;'S?S;'S;=`?e<%lO?SS?hP;=`<%l?S&n?nP;=`<%l>`!f?xWVS(R!bOY?qZw?qwx?Sx#O?q#O#P?S#P;'S?q;'S;=`@b<%lO?q!f@eP;=`<%l?q(Q@kP;=`<%l=^'`@w]VS$f&j(OpOY@nYZ&cZr@nrs>`s!^@n!^!_Ap!_#O@n#O#P>`#P#o@n#o#pAp#p;'S@n;'S;=`Bg<%lO@ntAwWVS(OpOYApZrAprs?Ss#OAp#O#P?S#P;'SAp;'S;=`Ba<%lOAptBdP;=`<%lAp'`BjP;=`<%l@n#WBvYVS(Op(R!bOYBmZrBmrs?qswBmwxApx#OBm#O#P?S#P;'SBm;'S;=`Cf<%lOBm#WCiP;=`<%lBm(rCoP;=`<%lQ^$f&j!USOY!=yYZ&cZ!P!=y!P!Q!>|!Q!^!=y!^!_!@Y!_!}!=y!}#O!Bw#O#P!Dj#P#o!=y#o#p!@Y#p;'S!=y;'S;=`!E[<%lO!=y&n!?Ta$f&j!USO!^&c!_#Z&c#Z#[!>|#[#]&c#]#^!>|#^#a&c#a#b!>|#b#g&c#g#h!>|#h#i&c#i#j!>|#j#m&c#m#n!>|#n#o&c#p;'S&c;'S;=`&w<%lO&cS!@_X!USOY!@YZ!P!@Y!P!Q!@z!Q!}!@Y!}#O!Ac#O#P!Bb#P;'S!@Y;'S;=`!Bq<%lO!@YS!APU!US#Z#[!@z#]#^!@z#a#b!@z#g#h!@z#i#j!@z#m#n!@zS!AfVOY!AcZ#O!Ac#O#P!A{#P#Q!@Y#Q;'S!Ac;'S;=`!B[<%lO!AcS!BOSOY!AcZ;'S!Ac;'S;=`!B[<%lO!AcS!B_P;=`<%l!AcS!BeSOY!@YZ;'S!@Y;'S;=`!Bq<%lO!@YS!BtP;=`<%l!@Y&n!B|[$f&jOY!BwYZ&cZ!^!Bw!^!_!Ac!_#O!Bw#O#P!Cr#P#Q!=y#Q#o!Bw#o#p!Ac#p;'S!Bw;'S;=`!Dd<%lO!Bw&n!CwX$f&jOY!BwYZ&cZ!^!Bw!^!_!Ac!_#o!Bw#o#p!Ac#p;'S!Bw;'S;=`!Dd<%lO!Bw&n!DgP;=`<%l!Bw&n!DoX$f&jOY!=yYZ&cZ!^!=y!^!_!@Y!_#o!=y#o#p!@Y#p;'S!=y;'S;=`!E[<%lO!=y&n!E_P;=`<%l!=y(Q!Eki$f&j(R!b!USOY&}YZ&cZw&}wx&cx!^&}!^!_'}!_#O&}#O#P&c#P#Z&}#Z#[!Eb#[#]&}#]#^!Eb#^#a&}#a#b!Eb#b#g&}#g#h!Eb#h#i&}#i#j!Eb#j#m&}#m#n!Eb#n#o&}#o#p'}#p;'S&};'S;=`(l<%lO&}!f!GaZ(R!b!USOY!GYZw!GYwx!@Yx!P!GY!P!Q!HS!Q!}!GY!}#O!Ic#O#P!Bb#P;'S!GY;'S;=`!JZ<%lO!GY!f!HZb(R!b!USOY'}Zw'}x#O'}#P#Z'}#Z#[!HS#[#]'}#]#^!HS#^#a'}#a#b!HS#b#g'}#g#h!HS#h#i'}#i#j!HS#j#m'}#m#n!HS#n;'S'};'S;=`(f<%lO'}!f!IhX(R!bOY!IcZw!Icwx!Acx#O!Ic#O#P!A{#P#Q!GY#Q;'S!Ic;'S;=`!JT<%lO!Ic!f!JWP;=`<%l!Ic!f!J^P;=`<%l!GY(Q!Jh^$f&j(R!bOY!JaYZ&cZw!Jawx!Bwx!^!Ja!^!_!Ic!_#O!Ja#O#P!Cr#P#Q!Q#V#X%Z#X#Y!4|#Y#b%Z#b#c#Zd$f&j(Op(R!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q!R#?i!R!S#?i!S!^%Z!^!_*g!_#O%Z#O#P&c#P#R%Z#R#S#?i#S#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z$/l#?tf$f&j(Op(R!bo$'|OY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q!R#?i!R!S#?i!S!^%Z!^!_*g!_#O%Z#O#P&c#P#R%Z#R#S#?i#S#b%Z#b#c#V[Q]||-1},{term:334,get:Q=>U[Q]||-1},{term:70,get:Q=>u[Q]||-1}],tokenPrec:14626});export{z as parser}; diff --git a/Resources/Public/JavaScript/Contrib/@lezer/json.js b/Resources/Public/JavaScript/Contrib/@lezer/json.js new file mode 100644 index 0000000..99b2115 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@lezer/json.js @@ -0,0 +1 @@ +import{LRParser as P}from"@lezer/lr";import{styleTags as Q,tags as O}from"@lezer/highlight";const e=Q({String:O.string,Number:O.number,"True False":O.bool,PropertyName:O.propertyName,Null:O.null,",":O.separator,"[ ]":O.squareBracket,"{ }":O.brace}),r=P.deserialize({version:14,states:"$bOVQPOOOOQO'#Cb'#CbOnQPO'#CeOvQPO'#CjOOQO'#Cp'#CpQOQPOOOOQO'#Cg'#CgO}QPO'#CfO!SQPO'#CrOOQO,59P,59PO![QPO,59PO!aQPO'#CuOOQO,59U,59UO!iQPO,59UOVQPO,59QOqQPO'#CkO!nQPO,59^OOQO1G.k1G.kOVQPO'#ClO!vQPO,59aOOQO1G.p1G.pOOQO1G.l1G.lOOQO,59V,59VOOQO-E6i-E6iOOQO,59W,59WOOQO-E6j-E6j",stateData:"#O~OcOS~OQSORSOSSOTSOWQO]ROePO~OVXOeUO~O[[O~PVOg^O~Oh_OVfX~OVaO~OhbO[iX~O[dO~Oh_OVfa~OhbO[ia~O",goto:"!kjPPPPPPkPPkqwPPk{!RPPP!XP!ePP!hXSOR^bQWQRf_TVQ_Q`WRg`QcZRicQTOQZRQe^RhbRYQR]R",nodeNames:"\u26A0 JsonText True False Null Number String } { Object Property PropertyName ] [ Array",maxTerm:25,nodeProps:[["openedBy",7,"{",12,"["],["closedBy",8,"}",13,"]"]],propSources:[e],skippedNodes:[0],repeatNodeCount:2,tokenData:"(p~RaXY!WYZ!W]^!Wpq!Wrs!]|}$i}!O$n!Q!R$w!R![&V![!]&h!}#O&m#P#Q&r#Y#Z&w#b#c'f#h#i'}#o#p(f#q#r(k~!]Oc~~!`Upq!]qr!]rs!rs#O!]#O#P!w#P~!]~!wOe~~!zXrs!]!P!Q!]#O#P!]#U#V!]#Y#Z!]#b#c!]#f#g!]#h#i!]#i#j#g~#jR!Q![#s!c!i#s#T#Z#s~#vR!Q![$P!c!i$P#T#Z$P~$SR!Q![$]!c!i$]#T#Z$]~$`R!Q![!]!c!i!]#T#Z!]~$nOh~~$qQ!Q!R$w!R![&V~$|RT~!O!P%V!g!h%k#X#Y%k~%YP!Q![%]~%bRT~!Q![%]!g!h%k#X#Y%k~%nR{|%w}!O%w!Q![%}~%zP!Q![%}~&SPT~!Q![%}~&[ST~!O!P%V!Q![&V!g!h%k#X#Y%k~&mOg~~&rO]~~&wO[~~&zP#T#U&}~'QP#`#a'T~'WP#g#h'Z~'^P#X#Y'a~'fOR~~'iP#i#j'l~'oP#`#a'r~'uP#`#a'x~'}OS~~(QP#f#g(T~(WP#i#j(Z~(^P#X#Y(a~(fOQ~~(kOW~~(pOV~",tokenizers:[0],topRules:{JsonText:[0,1]},tokenPrec:0});export{r as parser}; diff --git a/Resources/Public/JavaScript/Contrib/@lezer/lr.js b/Resources/Public/JavaScript/Contrib/@lezer/lr.js new file mode 100644 index 0000000..e8eff95 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@lezer/lr.js @@ -0,0 +1 @@ +import{Parser as $,NodeProp as C,NodeSet as E,NodeType as j,DefaultBufferLength as G,Tree as P,IterMode as U}from"@lezer/common";class b{constructor(t,e,s,i,h,r,n,o,a,u=0,f){this.p=t,this.stack=e,this.state=s,this.reducePos=i,this.pos=h,this.score=r,this.buffer=n,this.bufferBase=o,this.curContext=a,this.lookAhead=u,this.parent=f}toString(){return`[${this.stack.filter((t,e)=>e%3==0).concat(this.state)}]@${this.pos}${this.score?"!"+this.score:""}`}static start(t,e,s=0){let i=t.parser.context;return new b(t,[],e,s,s,0,[],0,i?new N(i,i.start):null,0,null)}get context(){return this.curContext?this.curContext.context:null}pushState(t,e){this.stack.push(this.state,e,this.bufferBase+this.buffer.length),this.state=t}reduce(t){var e;let s=t>>19,i=t&65535,{parser:h}=this.p,r=this.reducePos=2e3&&!(!((e=this.p.parser.nodeSet.types[i])===null||e===void 0)&&e.isAnonymous)&&(a==this.p.lastBigReductionStart?(this.p.bigReductionCount++,this.p.lastBigReductionSize=u):this.p.lastBigReductionSizeo;)this.stack.pop();this.reduceContext(i,a)}storeNode(t,e,s,i=4,h=!1){if(t==0&&(!this.stack.length||this.stack[this.stack.length-1]0&&r.buffer[n-4]==0&&r.buffer[n-1]>-1){if(e==s)return;if(r.buffer[n-2]>=e){r.buffer[n-2]=s;return}}}if(!h||this.pos==s)this.buffer.push(t,e,s,i);else{let r=this.buffer.length;if(r>0&&this.buffer[r-4]!=0){let n=!1;for(let o=r;o>0&&this.buffer[o-2]>s;o-=4)if(this.buffer[o-1]>=0){n=!0;break}if(n)for(;r>0&&this.buffer[r-2]>s;)this.buffer[r]=this.buffer[r-4],this.buffer[r+1]=this.buffer[r-3],this.buffer[r+2]=this.buffer[r-2],this.buffer[r+3]=this.buffer[r-1],r-=4,i>4&&(i-=4)}this.buffer[r]=t,this.buffer[r+1]=e,this.buffer[r+2]=s,this.buffer[r+3]=i}}shift(t,e,s,i){if(t&131072)this.pushState(t&65535,this.pos);else if((t&262144)==0){let h=t,{parser:r}=this.p;(i>this.pos||e<=r.maxNode)&&(this.pos=i,r.stateFlag(h,1)||(this.reducePos=i)),this.pushState(h,s),this.shiftContext(e,s),e<=r.maxNode&&this.buffer.push(e,s,i,4)}else this.pos=i,this.shiftContext(e,s),e<=this.p.parser.maxNode&&this.buffer.push(e,s,i,4)}apply(t,e,s,i){t&65536?this.reduce(t):this.shift(t,e,s,i)}useNode(t,e){let s=this.p.reused.length-1;(s<0||this.p.reused[s]!=t)&&(this.p.reused.push(t),s++);let i=this.pos;this.reducePos=this.pos=i+t.length,this.pushState(e,i),this.buffer.push(s,i,this.reducePos,-1),this.curContext&&this.updateContext(this.curContext.tracker.reuse(this.curContext.context,t,this,this.p.stream.reset(this.pos-t.length)))}split(){let t=this,e=t.buffer.length;for(;e>0&&t.buffer[e-2]>t.reducePos;)e-=4;let s=t.buffer.slice(e),i=t.bufferBase+e;for(;t&&i==t.bufferBase;)t=t.parent;return new b(this.p,this.stack.slice(),this.state,this.reducePos,this.pos,this.score,s,i,this.curContext,this.lookAhead,t)}recoverByDelete(t,e){let s=t<=this.p.parser.maxNode;s&&this.storeNode(t,this.pos,e,4),this.storeNode(0,this.pos,e,s?8:4),this.pos=this.reducePos=e,this.score-=190}canShift(t){for(let e=new H(this);;){let s=this.p.parser.stateSlot(e.state,4)||this.p.parser.hasAction(e.state,t);if(s==0)return!1;if((s&65536)==0)return!0;e.reduce(s)}}recoverByInsert(t){if(this.stack.length>=300)return[];let e=this.p.parser.nextStates(this.state);if(e.length>8||this.stack.length>=120){let i=[];for(let h=0,r;ho&1&&n==r)||i.push(e[h],r)}e=i}let s=[];for(let i=0;i>19,i=e&65535,h=this.stack.length-s*3;if(h<0||t.getGoto(this.stack[h],i,!1)<0){let r=this.findForcedReduction();if(r==null)return!1;e=r}this.storeNode(0,this.pos,this.pos,4,!0),this.score-=100}return this.reducePos=this.pos,this.reduce(e),!0}findForcedReduction(){let{parser:t}=this.p,e=[],s=(i,h)=>{if(!e.includes(i))return e.push(i),t.allActions(i,r=>{if(!(r&393216))if(r&65536){let n=(r>>19)-h;if(n>1){let o=r&65535,a=this.stack.length-n*3;if(a>=0&&t.getGoto(this.stack[a],o,!1)>=0)return n<<19|65536|o}}else{let n=s(r,h+1);if(n!=null)return n}})};return s(this.state,0)}forceAll(){for(;!this.p.parser.stateFlag(this.state,2);)if(!this.forceReduce()){this.storeNode(0,this.pos,this.pos,4,!0);break}return this}get deadEnd(){if(this.stack.length!=3)return!1;let{parser:t}=this.p;return t.data[t.stateSlot(this.state,1)]==65535&&!t.stateSlot(this.state,4)}restart(){this.storeNode(0,this.pos,this.pos,4,!0),this.state=this.stack[0],this.stack.length=0}sameState(t){if(this.state!=t.state||this.stack.length!=t.stack.length)return!1;for(let e=0;ethis.lookAhead&&(this.emitLookAhead(),this.lookAhead=t)}close(){this.curContext&&this.curContext.tracker.strict&&this.emitContext(),this.lookAhead>0&&this.emitLookAhead()}}class N{constructor(t,e){this.tracker=t,this.context=e,this.hash=t.strict?t.hash(e):0}}class H{constructor(t){this.start=t,this.state=t.state,this.stack=t.stack,this.base=this.stack.length}reduce(t){let e=t&65535,s=t>>19;s==0?(this.stack==this.start.stack&&(this.stack=this.stack.slice()),this.stack.push(this.state,0,0),this.base+=3):this.base-=(s-1)*3;let i=this.start.p.parser.getGoto(this.stack[this.base-3],e,!0);this.state=i}}class A{constructor(t,e,s){this.stack=t,this.pos=e,this.index=s,this.buffer=t.buffer,this.index==0&&this.maybeNext()}static create(t,e=t.bufferBase+t.buffer.length){return new A(t,e,e-t.bufferBase)}maybeNext(){let t=this.stack.parent;t!=null&&(this.index=this.stack.bufferBase-t.bufferBase,this.stack=t,this.buffer=t.buffer)}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}next(){this.index-=4,this.pos-=4,this.index==0&&this.maybeNext()}fork(){return new A(this.stack,this.pos,this.index)}}function x(l,t=Uint16Array){if(typeof l!="string")return l;let e=null;for(let s=0,i=0;s=92&&r--,r>=34&&r--;let o=r-32;if(o>=46&&(o-=46,n=!0),h+=o,n)break;h*=46}e?e[i++]=h:e=new t(h)}return e}class v{constructor(){this.start=-1,this.value=-1,this.end=-1,this.extended=-1,this.lookAhead=0,this.mask=0,this.context=0}}const R=new v;class z{constructor(t,e){this.input=t,this.ranges=e,this.chunk="",this.chunkOff=0,this.chunk2="",this.chunk2Pos=0,this.next=-1,this.token=R,this.rangeIndex=0,this.pos=this.chunkPos=e[0].from,this.range=e[0],this.end=e[e.length-1].to,this.readNext()}resolveOffset(t,e){let s=this.range,i=this.rangeIndex,h=this.pos+t;for(;hs.to:h>=s.to;){if(i==this.ranges.length-1)return null;let r=this.ranges[++i];h+=r.from-s.to,s=r}return h}clipPos(t){if(t>=this.range.from&&tt)return Math.max(t,e.from);return this.end}peek(t){let e=this.chunkOff+t,s,i;if(e>=0&&e=this.chunk2Pos&&sn.to&&(this.chunk2=this.chunk2.slice(0,n.to-s)),i=this.chunk2.charCodeAt(0)}}return s>=this.token.lookAhead&&(this.token.lookAhead=s+1),i}acceptToken(t,e=0){let s=e?this.resolveOffset(e,-1):this.pos;if(s==null||s=this.chunk2Pos&&this.posthis.range.to?t.slice(0,this.range.to-this.pos):t,this.chunkPos=this.pos,this.chunkOff=0}}readNext(){return this.chunkOff>=this.chunk.length&&(this.getChunk(),this.chunkOff==this.chunk.length)?this.next=-1:this.next=this.chunk.charCodeAt(this.chunkOff)}advance(t=1){for(this.chunkOff+=t;this.pos+t>=this.range.to;){if(this.rangeIndex==this.ranges.length-1)return this.setDone();t-=this.range.to-this.pos,this.range=this.ranges[++this.rangeIndex],this.pos=this.range.from}return this.pos+=t,this.pos>=this.token.lookAhead&&(this.token.lookAhead=this.pos+1),this.readNext()}setDone(){return this.pos=this.chunkPos=this.end,this.range=this.ranges[this.rangeIndex=this.ranges.length-1],this.chunk="",this.next=-1}reset(t,e){if(e?(this.token=e,e.start=t,e.lookAhead=t+1,e.value=e.extended=-1):this.token=R,this.pos!=t){if(this.pos=t,t==this.end)return this.setDone(),this;for(;t=this.range.to;)this.range=this.ranges[++this.rangeIndex];t>=this.chunkPos&&t=this.chunkPos&&e<=this.chunkPos+this.chunk.length)return this.chunk.slice(t-this.chunkPos,e-this.chunkPos);if(t>=this.chunk2Pos&&e<=this.chunk2Pos+this.chunk2.length)return this.chunk2.slice(t-this.chunk2Pos,e-this.chunk2Pos);if(t>=this.range.from&&e<=this.range.to)return this.input.read(t,e);let s="";for(let i of this.ranges){if(i.from>=e)break;i.to>t&&(s+=this.input.read(Math.max(i.from,t),Math.min(i.to,e)))}return s}}class m{constructor(t,e){this.data=t,this.id=e}token(t,e){let{parser:s}=e.p;D(this.data,t,e,this.id,s.data,s.tokenPrecTable)}}m.prototype.contextual=m.prototype.fallback=m.prototype.extend=!1;class I{constructor(t,e,s){this.precTable=e,this.elseToken=s,this.data=typeof t=="string"?x(t):t}token(t,e){let s=t.pos,i=0;for(;;){let h=t.next<0,r=t.resolveOffset(1,1);if(D(this.data,t,e,0,this.data,this.precTable),t.token.value>-1)break;if(this.elseToken==null)return;if(h||i++,r==null)break;t.reset(r,t.token)}i&&(t.reset(s,t.token),t.acceptToken(this.elseToken,i))}}I.prototype.contextual=m.prototype.fallback=m.prototype.extend=!1;class W{constructor(t,e={}){this.token=t,this.contextual=!!e.contextual,this.fallback=!!e.fallback,this.extend=!!e.extend}}function D(l,t,e,s,i,h){let r=0,n=1<0){let d=l[c];if(o.allows(d)&&(t.token.value==-1||t.token.value==d||q(d,t.token.value,i,h))){t.acceptToken(d);break}}let u=t.next,f=0,p=l[r+2];if(t.next<0&&p>f&&l[a+p*3-3]==65535){r=l[a+p*3-1];continue t}for(;f>1,d=a+c+(c<<1),S=l[d],L=l[d+1]||65536;if(u=L)f=c+1;else{r=l[d+2],t.advance();continue t}}break}}function B(l,t,e){for(let s=t,i;(i=l[s])!=65535;s++)if(i==e)return s-t;return-1}function q(l,t,e,s){let i=B(e,s,t);return i<0||B(e,s,l)t)&&!s.type.isError)return e<0?Math.max(0,Math.min(s.to-1,t-25)):Math.min(l.length,Math.max(s.from+1,t+25));if(e<0?s.prevSibling():s.nextSibling())break;if(!s.parent())return e<0?0:l.length}}class J{constructor(t,e){this.fragments=t,this.nodeSet=e,this.i=0,this.fragment=null,this.safeFrom=-1,this.safeTo=-1,this.trees=[],this.start=[],this.index=[],this.nextFragment()}nextFragment(){let t=this.fragment=this.i==this.fragments.length?null:this.fragments[this.i++];if(t){for(this.safeFrom=t.openStart?F(t.tree,t.from+t.offset,1)-t.offset:t.from,this.safeTo=t.openEnd?F(t.tree,t.to+t.offset,-1)-t.offset:t.to;this.trees.length;)this.trees.pop(),this.start.pop(),this.index.pop();this.trees.push(t.tree),this.start.push(-t.offset),this.index.push(0),this.nextStart=this.safeFrom}else this.nextStart=1e9}nodeAt(t){if(tt)return this.nextStart=r,null;if(h instanceof P){if(r==t){if(r=Math.max(this.safeFrom,t)&&(this.trees.push(h),this.start.push(r),this.index.push(0))}else this.index[e]++,this.nextStart=r+h.length}}}class K{constructor(t,e){this.stream=e,this.tokens=[],this.mainToken=null,this.actions=[],this.tokens=t.tokenizers.map(s=>new v)}getActions(t){let e=0,s=null,{parser:i}=t.p,{tokenizers:h}=i,r=i.stateSlot(t.state,3),n=t.curContext?t.curContext.hash:0,o=0;for(let a=0;af.end+25&&(o=Math.max(f.lookAhead,o)),f.value!=0)){let p=e;if(f.extended>-1&&(e=this.addActions(t,f.extended,f.end,e)),e=this.addActions(t,f.value,f.end,e),!u.extend&&(s=f,e>p))break}}for(;this.actions.length>e;)this.actions.pop();return o&&t.setLookAhead(o),!s&&t.pos==this.stream.end&&(s=new v,s.value=t.p.parser.eofTerm,s.start=s.end=t.pos,e=this.addActions(t,s.value,s.end,e)),this.mainToken=s,this.actions}getMainToken(t){if(this.mainToken)return this.mainToken;let e=new v,{pos:s,p:i}=t;return e.start=s,e.end=Math.min(s+1,i.stream.end),e.value=s==i.stream.end?i.parser.eofTerm:0,e}updateCachedToken(t,e,s){let i=this.stream.clipPos(s.pos);if(e.token(this.stream.reset(i,t),s),t.value>-1){let{parser:h}=s.p;for(let r=0;r=0&&s.p.parser.dialect.allows(n>>1)){(n&1)==0?t.value=n>>1:t.extended=n>>1;break}}}else t.value=0,t.end=this.stream.clipPos(i+1)}putAction(t,e,s,i){for(let h=0;ht.bufferLength*4?new J(s,t.nodeSet):null}get parsedPos(){return this.minStackPos}advance(){let t=this.stacks,e=this.minStackPos,s=this.stacks=[],i,h;if(this.bigReductionCount>300&&t.length==1){let[r]=t;for(;r.forceReduce()&&r.stack.length&&r.stack[r.stack.length-2]>=this.lastBigReductionStart;);this.bigReductionCount=this.lastBigReductionSize=0}for(let r=0;re)s.push(n);else{if(this.advanceStack(n,s,t))continue;{i||(i=[],h=[]),i.push(n);let o=this.tokens.getMainToken(n);h.push(o.value,o.end)}}break}}if(!s.length){let r=i&&Y(i);if(r)return g&&console.log("Finish with "+this.stackID(r)),this.stackToTree(r);if(this.parser.strict)throw g&&i&&console.log("Stuck with token "+(this.tokens.mainToken?this.parser.getName(this.tokens.mainToken.value):"none")),new SyntaxError("No parse at "+e);this.recovering||(this.recovering=5)}if(this.recovering&&i){let r=this.stoppedAt!=null&&i[0].pos>this.stoppedAt?i[0]:this.runRecovery(i,h,s);if(r)return g&&console.log("Force-finish "+this.stackID(r)),this.stackToTree(r.forceAll())}if(this.recovering){let r=this.recovering==1?1:this.recovering*3;if(s.length>r)for(s.sort((n,o)=>o.score-n.score);s.length>r;)s.pop();s.some(n=>n.reducePos>e)&&this.recovering--}else if(s.length>1){t:for(let r=0;r500&&a.buffer.length>500)if((n.score-a.score||n.buffer.length-a.buffer.length)>0)s.splice(o--,1);else{s.splice(r--,1);continue t}}}s.length>12&&s.splice(12,s.length-12)}this.minStackPos=s[0].pos;for(let r=1;r ":"";if(this.stoppedAt!=null&&i>this.stoppedAt)return t.forceReduce()?t:null;if(this.fragments){let a=t.curContext&&t.curContext.tracker.strict,u=a?t.curContext.hash:0;for(let f=this.fragments.nodeAt(i);f;){let p=this.parser.nodeSet.types[f.type.id]==f.type?h.getGoto(t.state,f.type.id):-1;if(p>-1&&f.length&&(!a||(f.prop(C.contextHash)||0)==u))return t.useNode(f,p),g&&console.log(r+this.stackID(t)+` (via reuse of ${h.getName(f.type.id)})`),!0;if(!(f instanceof P)||f.children.length==0||f.positions[0]>0)break;let c=f.children[0];if(c instanceof P&&f.positions[0]==0)f=c;else break}}let n=h.stateSlot(t.state,4);if(n>0)return t.reduce(n),g&&console.log(r+this.stackID(t)+` (via always-reduce ${h.getName(n&65535)})`),!0;if(t.stack.length>=8400)for(;t.stack.length>6e3&&t.forceReduce(););let o=this.tokens.getActions(t);for(let a=0;ai?e.push(d):s.push(d)}return!1}advanceFully(t,e){let s=t.pos;for(;;){if(!this.advanceStack(t,null,null))return!1;if(t.pos>s)return O(t,e),!0}}runRecovery(t,e,s){let i=null,h=!1;for(let r=0;r ":"";if(n.deadEnd&&(h||(h=!0,n.restart(),g&&console.log(u+this.stackID(n)+" (restarted)"),this.advanceFully(n,s))))continue;let f=n.split(),p=u;for(let c=0;f.forceReduce()&&c<10&&(g&&console.log(p+this.stackID(f)+" (via force-reduce)"),!this.advanceFully(f,s));c++)g&&(p=this.stackID(f)+" -> ");for(let c of n.recoverByInsert(o))g&&console.log(u+this.stackID(c)+" (via recover-insert)"),this.advanceFully(c,s);this.stream.end>n.pos?(a==n.pos&&(a++,o=0),n.recoverByDelete(o,a),g&&console.log(u+this.stackID(n)+` (via recover-delete ${this.parser.getName(o)})`),O(n,s)):(!i||i.scorel;class X{constructor(t){this.start=t.start,this.shift=t.shift||y,this.reduce=t.reduce||y,this.reuse=t.reuse||y,this.hash=t.hash||(()=>0),this.strict=t.strict!==!1}}class T extends ${constructor(t){if(super(),this.wrappers=[],t.version!=14)throw new RangeError(`Parser version (${t.version}) doesn't match runtime version (14)`);let e=t.nodeNames.split(" ");this.minRepeatTerm=e.length;for(let n=0;nt.topRules[n][1]),i=[];for(let n=0;n=0)h(u,o,n[a++]);else{let f=n[a+-u];for(let p=-u;p>0;p--)h(n[a++],o,f);a++}}}this.nodeSet=new E(e.map((n,o)=>j.define({name:o>=this.minRepeatTerm?void 0:n,id:o,props:i[o],top:s.indexOf(o)>-1,error:o==0,skipped:t.skippedNodes&&t.skippedNodes.indexOf(o)>-1}))),t.propSources&&(this.nodeSet=this.nodeSet.extend(...t.propSources)),this.strict=!1,this.bufferLength=G;let r=x(t.tokenData);this.context=t.context,this.specializerSpecs=t.specialized||[],this.specialized=new Uint16Array(this.specializerSpecs.length);for(let n=0;ntypeof n=="number"?new m(r,n):n),this.topRules=t.topRules,this.dialects=t.dialects||{},this.dynamicPrecedences=t.dynamicPrecedences||null,this.tokenPrecTable=t.tokenPrec,this.termNames=t.termNames||null,this.maxNode=this.nodeSet.types.length-1,this.dialect=this.parseDialect(),this.top=this.topRules[Object.keys(this.topRules)[0]]}createParse(t,e,s){let i=new Q(this,t,e,s);for(let h of this.wrappers)i=h(i,t,e,s);return i}getGoto(t,e,s=!1){let i=this.goto;if(e>=i[0])return-1;for(let h=i[e+1];;){let r=i[h++],n=r&1,o=i[h++];if(n&&s)return o;for(let a=h+(r>>1);h0}validAction(t,e){return!!this.allActions(t,s=>s==e?!0:null)}allActions(t,e){let s=this.stateSlot(t,4),i=s?e(s):void 0;for(let h=this.stateSlot(t,1);i==null;h+=3){if(this.data[h]==65535)if(this.data[h+1]==1)h=k(this.data,h+2);else break;i=e(k(this.data,h+1))}return i}nextStates(t){let e=[];for(let s=this.stateSlot(t,1);;s+=3){if(this.data[s]==65535)if(this.data[s+1]==1)s=k(this.data,s+2);else break;if((this.data[s+2]&1)==0){let i=this.data[s+1];e.some((h,r)=>r&1&&h==i)||e.push(this.data[s],i)}}return e}configure(t){let e=Object.assign(Object.create(T.prototype),this);if(t.props&&(e.nodeSet=this.nodeSet.extend(...t.props)),t.top){let s=this.topRules[t.top];if(!s)throw new RangeError(`Invalid top rule name ${t.top}`);e.top=s}return t.tokenizers&&(e.tokenizers=this.tokenizers.map(s=>{let i=t.tokenizers.find(h=>h.from==s);return i?i.to:s})),t.specializers&&(e.specializers=this.specializers.slice(),e.specializerSpecs=this.specializerSpecs.map((s,i)=>{let h=t.specializers.find(n=>n.from==s.external);if(!h)return s;let r=Object.assign(Object.assign({},s),{external:h.to});return e.specializers[i]=M(r),r})),t.contextTracker&&(e.context=t.contextTracker),t.dialect&&(e.dialect=this.parseDialect(t.dialect)),t.strict!=null&&(e.strict=t.strict),t.wrap&&(e.wrappers=e.wrappers.concat(t.wrap)),t.bufferLength!=null&&(e.bufferLength=t.bufferLength),e}hasWrappers(){return this.wrappers.length>0}getName(t){return this.termNames?this.termNames[t]:String(t<=this.maxNode&&this.nodeSet.types[t].name||t)}get eofTerm(){return this.maxNode+1}get topNode(){return this.nodeSet.types[this.top[1]]}dynamicPrecedence(t){let e=this.dynamicPrecedences;return e==null?0:e[t]||0}parseDialect(t){let e=Object.keys(this.dialects),s=e.map(()=>!1);if(t)for(let h of t.split(" ")){let r=e.indexOf(h);r>=0&&(s[r]=!0)}let i=null;for(let h=0;hs)&&e.p.parser.stateFlag(e.state,2)&&(!t||t.scorel.external(e,s)<<1|t}return l.get}export{X as ContextTracker,W as ExternalTokenizer,z as InputStream,T as LRParser,I as LocalTokenGroup,b as Stack}; diff --git a/Resources/Public/JavaScript/Contrib/@lezer/php.js b/Resources/Public/JavaScript/Contrib/@lezer/php.js new file mode 100644 index 0000000..b4e865f --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@lezer/php.js @@ -0,0 +1 @@ +import{ExternalTokenizer as S,LRParser as q}from"@lezer/lr";import{styleTags as n,tags as $}from"@lezer/highlight";const d=1,o=2,x=263,R=3,V=264,W=265,U=266,v=4,r=5,b=6,l=7,e=8,u=9,m=10,Y=11,p=12,_=13,Z=14,c=15,k=16,j=17,w=18,h=19,G=20,g=21,f=22,E=23,I=24,C=25,F=26,N=27,K=28,A=29,H=30,L=31,D=32,B=33,M=34,J=35,OO=36,$O=37,QO=38,iO=39,yO=40,aO=41,SO=42,zO=43,PO=44,TO=45,tO=46,WO=47,eO=48,sO=49,XO=50,qO=51,nO=52,dO=53,oO=54,xO=55,RO=56,VO=57,UO=58,vO=59,rO=60,bO=61,P=62,lO=63,uO=64,mO=65,YO={abstract:v,and:r,array:b,as:l,true:e,false:e,break:u,case:m,catch:Y,clone:p,const:_,continue:Z,declare:k,default:c,do:j,echo:w,else:h,elseif:G,enddeclare:g,endfor:f,endforeach:E,endif:I,endswitch:C,endwhile:F,enum:N,extends:K,final:A,finally:H,fn:L,for:D,foreach:B,from:M,function:J,global:OO,goto:$O,if:QO,implements:iO,include:yO,include_once:aO,instanceof:SO,insteadof:zO,interface:PO,list:TO,match:tO,namespace:WO,new:eO,null:sO,or:XO,print:qO,require:nO,require_once:dO,return:oO,switch:xO,throw:RO,trait:VO,try:UO,unset:vO,use:rO,var:bO,public:P,private:P,protected:P,while:lO,xor:uO,yield:mO,__proto__:null};function pO(O){let Q=YO[O.toLowerCase()];return Q??-1}function s(O){return O==9||O==10||O==13||O==32}function X(O){return O>=97&&O<=122||O>=65&&O<=90}function a(O){return O==95||O>=128||X(O)}function T(O){return O>=48&&O<=55||O>=97&&O<=102||O>=65&&O<=70}const _O={int:!0,integer:!0,bool:!0,boolean:!0,float:!0,double:!0,real:!0,string:!0,array:!0,object:!0,unset:!0,__proto__:null},ZO=new S(O=>{if(O.next==40){O.advance();let Q=0;for(;s(O.peek(Q));)Q++;let i="",y;for(;X(y=O.peek(Q));)i+=String.fromCharCode(y),Q++;for(;s(O.peek(Q));)Q++;O.peek(Q)==41&&_O[i.toLowerCase()]&&O.acceptToken(d)}else if(O.next==60&&O.peek(1)==60&&O.peek(2)==60){for(let y=0;y<3;y++)O.advance();for(;O.next==32||O.next==9;)O.advance();let Q=O.next==39;if(Q&&O.advance(),!a(O.next))return;let i=String.fromCharCode(O.next);for(;O.advance(),!(!a(O.next)&&!(O.next>=48&&O.next<=55));)i+=String.fromCharCode(O.next);if(Q){if(O.next!=39)return;O.advance()}if(O.next!=10&&O.next!=13)return;for(;;){let y=O.next==10||O.next==13;if(O.advance(),O.next<0)return;if(y){for(;O.next==32||O.next==9;)O.advance();let t=!0;for(let z=0;z{O.next<0&&O.acceptToken(U)}),kO=new S((O,Q)=>{O.next==63&&Q.canShift(W)&&O.peek(1)==62&&O.acceptToken(W)});function jO(O){let Q=O.peek(1);if(Q==110||Q==114||Q==116||Q==118||Q==101||Q==102||Q==92||Q==36||Q==34||Q==123)return 2;if(Q>=48&&Q<=55){let i=2,y;for(;i<5&&(y=O.peek(i))>=48&&y<=55;)i++;return i}if(Q==120&&T(O.peek(2)))return T(O.peek(3))?4:3;if(Q==117&&O.peek(2)==123)for(let i=3;;i++){let y=O.peek(i);if(y==125)return i==2?0:i+1;if(!T(y))break}return 0}const wO=new S((O,Q)=>{let i=!1;for(;!(O.next==34||O.next<0||O.next==36&&(a(O.peek(1))||O.peek(1)==123)||O.next==123&&O.peek(1)==36);i=!0){if(O.next==92){let y=jO(O);if(y){if(i)break;return O.acceptToken(R,y)}}else if(!i&&(O.next==91||O.next==45&&O.peek(1)==62&&a(O.peek(2))||O.next==63&&O.peek(1)==45&&O.peek(2)==62&&a(O.peek(3)))&&Q.canShift(V))break;O.advance()}i&&O.acceptToken(x)}),hO=n({"Visibility abstract final static":$.modifier,"for foreach while do if else elseif switch try catch finally return throw break continue default case":$.controlKeyword,"endif endfor endforeach endswitch endwhile declare enddeclare goto match":$.controlKeyword,"and or xor yield unset clone instanceof insteadof":$.operatorKeyword,"function fn class trait implements extends const enum global interface use var":$.definitionKeyword,"include include_once require require_once namespace":$.moduleKeyword,"new from echo print array list as":$.keyword,null:$.null,Boolean:$.bool,VariableName:$.variableName,"NamespaceName/...":$.namespace,"NamedType/...":$.typeName,Name:$.name,"CallExpression/Name":$.function($.variableName),"LabelStatement/Name":$.labelName,"MemberExpression/Name":$.propertyName,"MemberExpression/VariableName":$.special($.propertyName),"ScopedExpression/ClassMemberName/Name":$.propertyName,"ScopedExpression/ClassMemberName/VariableName":$.special($.propertyName),"CallExpression/MemberExpression/Name":$.function($.propertyName),"CallExpression/ScopedExpression/ClassMemberName/Name":$.function($.propertyName),"MethodDeclaration/Name":$.function($.definition($.variableName)),"FunctionDefinition/Name":$.function($.definition($.variableName)),"ClassDeclaration/Name":$.definition($.className),UpdateOp:$.updateOperator,ArithOp:$.arithmeticOperator,LogicOp:$.logicOperator,BitOp:$.bitwiseOperator,CompareOp:$.compareOperator,ControlOp:$.controlOperator,AssignOp:$.definitionOperator,"$ ConcatOp":$.operator,LineComment:$.lineComment,BlockComment:$.blockComment,Integer:$.integer,Float:$.float,String:$.string,ShellExpression:$.special($.string),"=> ->":$.punctuation,"( )":$.paren,"#[ [ ]":$.squareBracket,"${ { }":$.brace,"-> ?->":$.derefOperator,", ; :: : \\":$.separator,"PhpOpen PhpClose":$.processingInstruction}),GO={__proto__:null,static:311,STATIC:311,class:333,CLASS:333},gO=q.deserialize({version:14,states:"$GSQ`OWOOQhQaOOP%oO`OOOOO#t'#H_'#H_O%tO#|O'#DtOOO#u'#Dw'#DwQ&SOWO'#DwO&XO$VOOOOQ#u'#Dx'#DxO&lQaO'#D|O(mQdO'#E}O(tQdO'#EQO*kQaO'#EWO,zQ`O'#ETO-PQ`O'#E^O/nQaO'#E^O/uQ`O'#EfO/zQ`O'#EoO*kQaO'#EoO0VQ`O'#HhO0[Q`O'#E{O0[Q`O'#E{OOQS'#Ic'#IcO0aQ`O'#EvOOQS'#IZ'#IZO2oQdO'#IWO6tQeO'#FUO*kQaO'#FeO*kQaO'#FfO*kQaO'#FgO*kQaO'#FhO*kQaO'#FhO*kQaO'#FkOOQO'#Id'#IdO7RQ`O'#FqOOQO'#Hi'#HiO7ZQ`O'#HOO7uQ`O'#FlO8QQ`O'#H]O8]Q`O'#FvO8eQaO'#FwO*kQaO'#GVO*kQaO'#GYO8}OrO'#G]OOQS'#Iq'#IqOOQS'#Ip'#IpOOQS'#IW'#IWO,zQ`O'#GdO,zQ`O'#GfO,zQ`O'#GkOhQaO'#GmO9UQ`O'#GnO9ZQ`O'#GqO9`Q`O'#GtO9eQeO'#GuO9eQeO'#GvO9eQeO'#GwO9oQ`O'#GxO9tQ`O'#GzO9yQaO'#G{OS,5>SOJ[QdO,5;gOOQO-E;f-E;fOL^Q`O,5;gOLcQpO,5;bO0aQ`O'#EyOLkQtO'#E}OOQS'#Ez'#EzOOQS'#Ib'#IbOM`QaO,5:wO*kQaO,5;nOOQS,5;p,5;pO*kQaO,5;pOMgQdO,5UQaO,5=hO!-eQ`O'#F}O!-jQdO'#IlO!&WQdO,5=iOOQ#u,5=j,5=jO!-uQ`O,5=lO!-xQ`O,5=mO!-}Q`O,5=nO!.YQdO,5=qOOQ#u,5=q,5=qO!.eQ`O,5=rO!.eQ`O,5=rO!.mQdO'#IwO!.{Q`O'#HXO!&WQdO,5=rO!/ZQ`O,5=rO!/fQdO'#IYO!&WQdO,5=vOOQ#u-E;_-E;_O!1RQ`O,5=kOOO#u,5:^,5:^O!1^O#|O,5:^OOO#u-E;^-E;^OOOO,5>p,5>pOOQ#y1G0S1G0SO!1fQ`O1G0XO*kQaO1G0XO!2xQ`O1G0pOOQS1G0p1G0pO!4[Q`O1G0pOOQS'#I_'#I_O*kQaO'#I_OOQS1G0q1G0qO!4cQ`O'#IaO!7lQ`O'#E}O!7yQaO'#EuOOQO'#Ia'#IaO!8TQ`O'#I`O!8]Q`O,5;_OOQS'#FQ'#FQOOQS1G1U1G1UO!8bQdO1G1]O!:dQdO1G1]O!wO#(fQaO'#HdO#(vQ`O,5>vOOQS1G0d1G0dO#)OQ`O1G0dO#)TQ`O'#I^O#*mQ`O'#I^O#*uQ`O,5;ROIbQaO,5;ROOQS1G0u1G0uPOQO'#E}'#E}O#+fQdO1G1RO0aQ`O'#HgO#-hQtO,5;cO#.YQaO1G0|OOQS,5;e,5;eO#0iQtO,5;gO#0vQdO1G0cO*kQaO1G0cO#2cQdO1G1YO#4OQdO1G1[OOQO,5<^,5<^O#4`Q`O'#HjO#4nQ`O,5?ROOQO1G1w1G1wO#4vQ`O,5?ZO!&WQdO1G3TO<_Q`O1G3TOOQ#u1G3U1G3UO#4{Q`O1G3YO!1RQ`O1G3VO#5WQ`O1G3VO#5]QpO'#FoO#5kQ`O'#FoO#5{Q`O'#FoO#6WQ`O'#FoO#6`Q`O'#FsO#6eQ`O'#FtOOQO'#If'#IfO#6lQ`O'#IeO#6tQ`O,5tOOQ#u1G3b1G3bOOQ#u1G3V1G3VO!-xQ`O1G3VO!1UQ`O1G3VOOO#u1G/x1G/xO*kQaO7+%sO#MuQdO7+%sOOQS7+&[7+&[O$ bQ`O,5>yO>UQaO,5;`O$ iQ`O,5;aO$#OQaO'#HfO$#YQ`O,5>zOOQS1G0y1G0yO$#bQ`O'#EYO$#gQ`O'#IXO$#oQ`O,5:sOOQS1G0e1G0eO$#tQ`O1G0eO$#yQ`O1G0iO9yQaO1G0iOOQO,5>O,5>OOOQO-E;b-E;bOOQS7+&O7+&OO>UQaO,5;SO$%`QaO'#HeO$%jQ`O,5>xOOQS1G0m1G0mO$%rQ`O1G0mOOQS,5>R,5>ROOQS-E;e-E;eO$%wQdO7+&hO$'yQtO1G1RO$(WQdO7+%}OOQS1G0i1G0iOOQO,5>U,5>UOOQO-E;h-E;hOOQ#u7+(o7+(oO!&WQdO7+(oOOQ#u7+(t7+(tO#KmQ`O7+(tO0aQ`O7+(tOOQ#u7+(q7+(qO!-xQ`O7+(qO!1UQ`O7+(qO!1RQ`O7+(qO$)sQ`O,5UQaO,5],5>]OOQS-E;o-E;oO$.iQdO7+'hO$.yQpO7+'hO$/RQdO'#IiOOQO,5dOOQ#u,5>d,5>dOOQ#u-E;v-E;vO$;lQaO7+(lO$cOOQS-E;u-E;uO!&WQdO7+(nO$=mQdO1G2TOOQS,5>[,5>[OOQS-E;n-E;nOOQ#u7+(r7+(rO$?nQ`O'#GQO$?uQ`O'#GQO$@ZQ`O'#HUOOQO'#Hy'#HyO$@`Q`O,5=oOOQ#u,5=o,5=oO$@gQpO7+(tOOQ#u7+(x7+(xO!&WQdO7+(xO$@rQdO,5>fOOQS-E;x-E;xO$AQQdO1G4}O$A]Q`O,5=tO$AbQ`O,5=tO$AmQ`O'#H{O$BRQ`O,5?dOOQS1G3_1G3_O#KrQ`O7+(xO$BZQdO,5=|OOQS-E;`-E;`O$CvQdO<Q,5>QOOQO-E;d-E;dO$8YQaO,5:tO$FxQaO'#HcO$GVQ`O,5>sOOQS1G0_1G0_OOQS7+&P7+&PO$G_Q`O7+&TO$HtQ`O1G0nO$JZQ`O,5>POOQO,5>P,5>POOQO-E;c-E;cOOQS7+&X7+&XOOQS7+&T7+&TOOQ#u<UQaO1G1uO$KsQ`O1G1uO$LOQ`O1G1yOOQO1G1y1G1yO$LTQ`O1G1uO$L]Q`O1G1uO$MrQ`O1G1zO>UQaO1G1zOOQO,5>V,5>VOOQO-E;i-E;iOOQS<`OOQ#u-E;r-E;rOhQaO<aOOQO-E;s-E;sO!&WQdO<g,5>gOOQO-E;y-E;yO!&WQdO<UQaO,5;TOOQ#uANAzANAzO#KmQ`OANAzOOQ#uANAwANAwO!-xQ`OANAwO%)vQ`O7+'aO>UQaO7+'aOOQO7+'e7+'eO%+]Q`O7+'aO%+hQ`O7+'eO>UQaO7+'fO%+mQ`O7+'fO%-SQ`O'#HlO%-bQ`O,5?SO%-bQ`O,5?SOOQO1G1{1G1{O$+qQpOAN@dOOQSAN@dAN@dO0aQ`OAN@dO%-jQtOANCgO%-xQ`OAN@dO*kQaOAN@nO%.QQdOAN@nO%.bQpOAN@nOOQS,5>X,5>XOOQS-E;k-E;kOOQO1G2U1G2UO!&WQdO1G2UO$/dQpO1G2UO<_Q`O1G2SO!.YQdO1G2WO!&WQdO1G2SOOQO1G2W1G2WOOQO1G2S1G2SO%.jQaO'#GSOOQO1G2X1G2XOOQSAN@oAN@oOOOQ<UQaO<W,5>WO%6wQ`O,5>WOOQO-E;j-E;jO%6|Q`O1G4nOOQSG26OG26OO$+qQpOG26OO0aQ`OG26OO%7UQdOG26YO*kQaOG26YOOQO7+'p7+'pO!&WQdO7+'pO!&WQdO7+'nOOQO7+'r7+'rOOQO7+'n7+'nO%7fQ`OLD+tO%8uQ`O'#E}O%9PQ`O'#IZO!&WQdO'#HrO%:|QaO,5^,5>^OOQP-E;p-E;pOOQO1G2Y1G2YOOQ#uLD,bLD,bOOQTG27RG27RO!&WQdOLD,xO!&WQdO<wO&EPQdO1G0cO#.YQaO1G0cO&F{QdO1G1YO&HwQdO1G1[O#.YQaO1G1|O#.YQaO7+%sO&JsQdO7+%sO&LoQdO7+%}O#.YQaO7+'hO&NkQdO7+'hO'!gQdO<lQdO,5>wO(@nQdO1G0cO'.QQaO1G0cO(BpQdO1G1YO(DrQdO1G1[O'.QQaO1G1|O'.QQaO7+%sO(FtQdO7+%sO(HvQdO7+%}O'.QQaO7+'hO(JxQdO7+'hO(LzQdO<wO*1sQaO'#HdO*2TQ`O,5>vO*2]QdO1G0cO9yQaO1G0cO*4XQdO1G1YO*6TQdO1G1[O9yQaO1G1|O>UQaO'#HwO*8PQ`O,5=[O*8XQaO'#HbO*8cQ`O,5>tO9yQaO7+%sO*8kQdO7+%sO*:gQ`O1G0iO>UQaO1G0iO*;|QdO7+%}O9yQaO7+'hO*=xQdO7+'hO*?tQ`O,5>cO*AZQ`O,5=|O*BpQdO<UQaO'#FeO>UQaO'#FfO>UQaO'#FgO>UQaO'#FhO>UQaO'#FhO>UQaO'#FkO+'XQaO'#FwO>UQaO'#GVO>UQaO'#GYO+'`QaO,5:mO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO>UQaO,5;qO+'gQ`O'#I]O$8YQaO'#EaO+)PQaOG26YO$8YQaO'#I]O+*{Q`O'#I[O++TQaO,5:wO>UQaO,5;nO>UQaO,5;pO++[Q`O,5UQaO1G0XO+9hQ`O1G1]O+;TQ`O1G1]O+]Q`O1G1]O+?xQ`O1G1]O+AeQ`O1G1]O+CQQ`O1G1]O+DmQ`O1G1]O+FYQ`O1G1]O+GuQ`O1G1]O+IbQ`O1G1]O+J}Q`O1G1]O+LjQ`O1G1]O+NVQ`O1G1]O, rQ`O1G1]O,#_Q`O1G0cO>UQaO1G0cO,$zQ`O1G1YO,&gQ`O1G1[O,(SQ`O1G1|O>UQaO1G1|O>UQaO7+%sO,([Q`O7+%sO,)wQ`O7+%}O>UQaO7+'hO,+dQ`O7+'hO,+lQ`O7+'hO,-XQpO7+'hO,-aQ`O<UQaO<UQaOAN@nO,0qQ`OAN@nO,2^QpOAN@nO,2fQ`OG26YO>UQaOG26YO,4RQ`OLD+tO,5nQaO,5:}O>UQaO1G0iO,5uQ`O'#I]O$8YQaO'#FeO$8YQaO'#FfO$8YQaO'#FgO$8YQaO'#FhO$8YQaO'#FhO+)PQaO'#FhO$8YQaO'#FkO,6SQaO'#FwO,6ZQaO'#FwO$8YQaO'#GVO+)PQaO'#GVO$8YQaO'#GYO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO$8YQaO,5;qO+)PQaO,5;qO,8YQ`O'#FlO>UQaO'#EaO>UQaO'#I]O,8bQaO,5:wO,8iQaO,5:wO$8YQaO,5;nO+)PQaO,5;nO$8YQaO,5;pO,:hQ`O,5wO-IcQ`O1G0cO-KOQ`O1G0cO$8YQaO1G0cO+)PQaO1G0cO-L_Q`O1G1YO-MzQ`O1G1YO. ZQ`O1G1[O$8YQaO1G1|O$8YQaO7+%sO+)PQaO7+%sO.!vQ`O7+%sO.$cQ`O7+%sO.%rQ`O7+%}O.'_Q`O7+%}O$8YQaO7+'hO.(nQ`O7+'hO.*ZQ`O<fQ`O,5>wO.@RQ`O1G1|O!%WQ`O1G1|O0aQ`O1G1|O0aQ`O7+'hO.@ZQ`O7+'hO.@cQpO7+'hO.@kQpO<UO#X&PO~P>UO!o&SO!s&RO#b&RO~OPgOQ|OU^OW}O[8lOo=yOs#hOx8jOy8jO}`O!O]O!Q8pO!R}O!T8oO!U8kO!V8kO!Y8rO!c8iO!s&VO!y[O#U&WO#W_O#bhO#daO#ebO#peO$T8nO$]8mO$^8nO$aqO$z8qO${!OO$}}O%O}O%V|O'g{O~O!x'SP~PAOO!s&[O#b&[O~OT#TOz#RO!S#UO!b#VO!o!{O!v!yO!y!}O#S#QO#W!zO#`!|O#a!|O#s#PO#z#SO#{#WO#|#XO#}#YO$O#ZO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dO~O!x&nO~PCqO!x'VX!}'VX#O'VX#X'VX!n'VXV'VX!q'VX#u'VX#w'VXw'VX~P&sO!y$hO#S&oO~Oo$mOs$lO~O!o&pO~O!}&sO#S;dO#U;cO!x'OP~P9yOT6iOz6gO!S6jO!b6kO!o!{O!v8sO!y!}O#S#QO#W!zO#`!|O#a!|O#s#PO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}'PX#X'PX~O#O&tO~PGSO!}&wO#X'OX~O#X&yO~O!}'OO!x'QP~P9yO!n'PO~PCqO!m#oa!o#oa#S#oa#p#qX&s#oa!x#oa#O#oaw#oa~OT#oaz#oa!S#oa!b#oa!v#oa!y#oa#W#oa#`#oa#a#oa#s#oa#z#oa#{#oa#|#oa#}#oa$O#oa$Q#oa$R#oa$S#oa$T#oa$U#oa$V#oa$W#oa$z#oa!}#oa#X#oa!n#oaV#oa!q#oa#u#oa#w#oa~PIpO!s'RO~O!x'UO#l'SO~O!x'VX#l'VX#p#qX#S'VX#U'VX#b'VX!o'VX#O'VXw'VX!m'VX&s'VX~O#S'YO~P*kO!m$Xa&s$Xa!x$Xa!n$Xa~PCqO!m$Ya&s$Ya!x$Ya!n$Ya~PCqO!m$Za&s$Za!x$Za!n$Za~PCqO!m$[a&s$[a!x$[a!n$[a~PCqO!o!{O!y!}O#W!zO#`!|O#a!|O#s#PO$z#dOT$[a!S$[a!b$[a!m$[a!v$[a#S$[a#z$[a#{$[a#|$[a#}$[a$O$[a$Q$[a$R$[a$S$[a$T$[a$U$[a$V$[a$W$[a&s$[a!x$[a!n$[a~Oz#RO~PNyO!m$_a&s$_a!x$_a!n$_a~PCqO!y!}O!}$fX#X$fX~O!}'^O#X'ZX~O#X'`O~O!s$kO#S'aO~O]'cO~O!s'eO~O!s'fO~O$l'gO~O!`'mO#S'kO#U'lO#b'jO$drO!x'XP~P0aO!^'sO!oXO!q'rO~O!s'uO!y$hO~O!y$hO#S'wO~O!y$hO#S'yO~O#u'zO!m$sX!}$sX&s$sX~O!}'{O!m'bX&s'bX~O!m#cO&s#cO~O!q(PO#O(OO~O!m$ka&s$ka!x$ka!n$ka~PCqOl(ROw(SO!o(TO!y!}O~O!o!{O!y!}O#W!zO#`!|O#a!|O#s#PO~OT$yaz$ya!S$ya!b$ya!m$ya!v$ya#S$ya#z$ya#{$ya#|$ya#}$ya$O$ya$Q$ya$R$ya$S$ya$T$ya$U$ya$V$ya$W$ya$z$ya&s$ya!x$ya!}$ya#O$ya#X$ya!n$ya!q$yaV$ya#u$ya#w$ya~P!'WO!m$|a&s$|a!x$|a!n$|a~PCqO#W([O#`(YO#a(YO&r(ZOR&gX!o&gX#b&gX#e&gX&q&gX'f&gX~O'f(_O~P8lO!q(`O~PhO!o(cO!q(dO~O!q(`O&s(gO~PhO!a(kO~O!m(lO~P9yOZ(wOn(xO~O!s(zO~OT6iOz6gO!S6jO!b6kO!v8sO!}({O#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!m'jX&s'jX~P!'WO#u)PO~O!})QO!m'`X&s'`X~Ol(RO!o(TO~Ow(SO!o)WO!q)ZO~O!m#cO!oXO&s#cO~O!o%pO!s#yO~OV)aO!})_O!m'kX&s'kX~O])cOs)cO!s#gO#peO~O!o%pO!s#gO#p)hO~OT6iOz6gO!S6jO!b6kO!v8sO!})iO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!m&|X&s&|X#O&|X~P!'WOl(ROw(SO!o(TO~O!i)oO&t)oO~OT8vOz8tO!S8wO!b8xO!q)pO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#X)rO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO~P!'WO!n)rO~PCqOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x'TX!}'TX~P!'WOT'VXz'VX!S'VX!b'VX!o'VX!v'VX!y'VX#S'VX#W'VX#`'VX#a'VX#p#qX#s'VX#z'VX#{'VX#|'VX#}'VX$O'VX$Q'VX$R'VX$S'VX$T'VX$U'VX$V'VX$W'VX$z'VX~O!q)tO!x'VX!}'VX~P!5xO!x#iX!}#iX~P>UO!})vO!x'SX~O!x)xO~O$z#dOT#yiz#yi!S#yi!b#yi!m#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$T#yi$U#yi$V#yi$W#yi&s#yi!x#yi!}#yi#O#yi#X#yi!n#yi!q#yiV#yi#u#yi#w#yi~P!'WOz#RO#S#QO#z#SO#{#WO#|#XO#}#YO$O#ZO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi&s#yi!x#yi!n#yi~P!'WOz#RO!v!yO#S#QO#z#SO#{#WO#|#XO#}#YO$O#ZO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT#yi!S#yi!b#yi!m#yi&s#yi!x#yi!n#yi~P!'WOT#TOz#RO!b#VO!v!yO#S#QO#z#SO#{#WO#|#XO#}#YO$O#ZO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dO!S#yi!m#yi&s#yi!x#yi!n#yi~P!'WOT#TOz#RO!v!yO#S#QO#z#SO#{#WO#|#XO#}#YO$O#ZO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dO!S#yi!b#yi!m#yi&s#yi!x#yi!n#yi~P!'WOz#RO#S#QO#|#XO#}#YO$O#ZO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#z#yi#{#yi&s#yi!x#yi!n#yi~P!'WOz#RO#S#QO#}#YO$O#ZO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#z#yi#{#yi#|#yi&s#yi!x#yi!n#yi~P!'WOz#RO#S#QO$O#ZO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#z#yi#{#yi#|#yi#}#yi&s#yi!x#yi!n#yi~P!'WOz#RO#S#QO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#z#yi#{#yi#|#yi#}#yi$O#yi&s#yi!x#yi!n#yi~P!'WOz#RO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi&s#yi!x#yi!n#yi~P!'WOz#RO$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi&s#yi!x#yi!n#yi~P!'WOz#RO$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi&s#yi!x#yi!n#yi~P!'WOz#RO$T#`O$V#bO$W#bO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$U#yi&s#yi!x#yi!n#yi~P!'WOz#RO$V#bO$W#bO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$T#yi$U#yi&s#yi!x#yi!n#yi~P!'WOz#RO$S#_O$T#`O$V#bO$W#bO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$U#yi&s#yi!x#yi!n#yi~P!'WOz#RO$W#bO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$T#yi$U#yi$V#yi&s#yi!x#yi!n#yi~P!'WO_)yO~P9yO!x)|O~O#S*PO~P9yOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}#Ta#X#Ta#O#Ta!m#Ta&s#Ta!x#Ta!n#TaV#Ta!q#Ta~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}'Pa#X'Pa#O'Pa!m'Pa&s'Pa!x'Pa!n'PaV'Pa!q'Pa~P!'WO#S#oO#U#nO!}&WX#X&WX~P9yO!}&wO#X'Oa~O#X*SO~OT6iOz6gO!S6jO!b6kO!v8sO!}*UO#O*TO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!x'QX~P!'WO!}*UO!x'QX~O!x*WO~O!m#oi!o#oi#S#oi#p#qX&s#oi!x#oi#O#oiw#oi~OT#oiz#oi!S#oi!b#oi!v#oi!y#oi#W#oi#`#oi#a#oi#s#oi#z#oi#{#oi#|#oi#}#oi$O#oi$Q#oi$R#oi$S#oi$T#oi$U#oi$V#oi$W#oi$z#oi!}#oi#X#oi!n#oiV#oi!q#oi#u#oi#w#oi~P#*zO#l'SO!x#ka#S#ka#U#ka#b#ka!o#ka#O#kaw#ka!m#ka&s#ka~OPgOQ|OU^OW}O[4OOo5xOs#hOx3zOy3zO}`O!O]O!Q2^O!R}O!T4UO!U3|O!V3|O!Y2`O!c3xO!s#gO!y[O#W_O#bhO#daO#ebO#peO$T4SO$]4QO$^4SO$aqO$z2_O${!OO$}}O%O}O%V|O'g{O~O#l#oa#U#oa#b#oa~PIpOz#RO!v!yO#S#QO#z#SO#{#WO#|#XO#}#YO$O#ZO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT#Pi!S#Pi!b#Pi!m#Pi&s#Pi!x#Pi!n#Pi~P!'WOz#RO!v!yO#S#QO#z#SO#{#WO#|#XO#}#YO$O#ZO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT#vi!S#vi!b#vi!m#vi&s#vi!x#vi!n#vi~P!'WO!m#xi&s#xi!x#xi!n#xi~PCqO!s#gO#peO!}&^X#X&^X~O!}'^O#X'Za~O!s'uO~Ow(SO!o)WO!q*fO~O!s*jO~O#S*lO#U*mO#b*kO#l'SO~O#S*lO#U*mO#b*kO$drO~P0aO#u*oO!x$cX!}$cX~O#U*mO#b*kO~O#b*pO~O#b*rO~P0aO!}*sO!x'XX~O!x*uO~O!y*wO~O!^*{O!oXO!q*zO~O!q*}O!o'ci!m'ci&s'ci~O!q+QO#O+PO~O#b$nO!m&eX!}&eX&s&eX~O!}'{O!m'ba&s'ba~OT$kiz$ki!S$ki!b$ki!m$ki!o$ki!v$ki!y$ki#S$ki#W$ki#`$ki#a$ki#s$ki#u#fa#w#fa#z$ki#{$ki#|$ki#}$ki$O$ki$Q$ki$R$ki$S$ki$T$ki$U$ki$V$ki$W$ki$z$ki&s$ki!x$ki!}$ki#O$ki#X$ki!n$ki!q$kiV$ki~OS+^O]+aOm+^Os$aO!^+dO!_+^O!`+^O!n+hO#b$nO$aqO$drO~P0aO!s+lO~O#W+nO#`+mO#a+mO~O!s+pO#b+pO$}+pO%T+oO~O!n+qO~PCqOc%XXd%XXh%XXj%XXf%XXg%XXe%XX~PhOc+uOd+sOP%WiQ%WiS%WiU%WiW%WiX%Wi[%Wi]%Wi^%Wi`%Wia%Wib%Wik%Wim%Wio%Wip%Wiq%Wis%Wit%Wiu%Wiv%Wix%Wiy%Wi|%Wi}%Wi!O%Wi!P%Wi!Q%Wi!R%Wi!T%Wi!U%Wi!V%Wi!W%Wi!X%Wi!Y%Wi!Z%Wi![%Wi!]%Wi!^%Wi!`%Wi!a%Wi!c%Wi!m%Wi!o%Wi!s%Wi!y%Wi#W%Wi#b%Wi#d%Wi#e%Wi#p%Wi$T%Wi$]%Wi$^%Wi$a%Wi$d%Wi$l%Wi$z%Wi${%Wi$}%Wi%O%Wi%V%Wi&p%Wi'g%Wi&t%Wi!n%Wih%Wij%Wif%Wig%WiY%Wi_%Wii%Wie%Wi~Oc+yOd+vOh+xO~OY+zO_+{O!n,OO~OY+zO_+{Oi%^X~Oi,QO~Oj,RO~O!m,TO~P9yO!m,VO~Of,WO~OT6iOV,XOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO~P!'WOg,YO~O!y,ZO~OZ(wOn(xOP%liQ%liS%liU%liW%liX%li[%li]%li^%li`%lia%lib%lik%lim%lio%lip%liq%lis%lit%liu%liv%lix%liy%li|%li}%li!O%li!P%li!Q%li!R%li!T%li!U%li!V%li!W%li!X%li!Y%li!Z%li![%li!]%li!^%li!`%li!a%li!c%li!m%li!o%li!s%li!y%li#W%li#b%li#d%li#e%li#p%li$T%li$]%li$^%li$a%li$d%li$l%li$z%li${%li$}%li%O%li%V%li&p%li'g%li&t%li!n%lic%lid%lih%lij%lif%lig%liY%li_%lii%lie%li~O#u,_O~O!}({O!m%da&s%da~O!x,bO~O!s%dO!m&dX!}&dX&s&dX~O!})QO!m'`a&s'`a~OS+^OY,iOm+^Os$aO!^+dO!_+^O!`+^O$aqO$drO~O!n,lO~P#JwO!o)WO~O!o%pO!s'RO~O!s#gO#peO!m&nX!}&nX&s&nX~O!})_O!m'ka&s'ka~O!s,rO~OV,sO!n%|X!}%|X~O!},uO!n'lX~O!n,wO~O!m&UX!}&UX&s&UX#O&UX~P9yO!})iO!m&|a&s&|a#O&|a~Oz#RO#S#QO#z#SO#{#WO#|#XO#}#YO$O#ZO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT!uq!S!uq!b!uq!m!uq!v!uq&s!uq!x!uq!n!uq~P!'WO!n,|O~PCqOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x#ia!}#ia~P!'WO!x&YX!}&YX~PAOO!})vO!x'Sa~O#O-QO~O!}-RO!n&{X~O!n-TO~O!x-UO~OT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}#Vi#X#Vi~P!'WO!x&XX!}&XX~P9yO!}*UO!x'Qa~O!x-[O~OT#jqz#jq!S#jq!b#jq!m#jq!v#jq#S#jq#u#jq#w#jq#z#jq#{#jq#|#jq#}#jq$O#jq$Q#jq$R#jq$S#jq$T#jq$U#jq$V#jq$W#jq$z#jq&s#jq!x#jq!}#jq#O#jq#X#jq!n#jq!q#jqV#jq~P!'WO#l#oi#U#oi#b#oi~P#*zOz#RO!v!yO#S#QO#z#SO#{#WO#|#XO#}#YO$O#ZO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT#Pq!S#Pq!b#Pq!m#Pq&s#Pq!x#Pq!n#Pq~P!'WO#u-dO!x$ca!}$ca~O#U-fO#b-eO~O#b-gO~O#S-hO#U-fO#b-eO#l'SO~O#b-jO#l'SO~O#u-kO!x$ha!}$ha~O!`'mO#S'kO#U'lO#b'jO$drO!x&_X!}&_X~P0aO!}*sO!x'Xa~O!oXO#l'SO~O#S-pO#b-oO!x'[P~O!oXO!q-rO~O!q-uO!o'cq!m'cq&s'cq~O!^-wO!oXO!q-rO~O!q-{O#O-zO~OT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!m$si!}$si&s$si~P!'WO!m$jq&s$jq!x$jq!n$jq~PCqO#O-zO#l'SO~O!}-|Ow']X!o']X!m']X&s']X~O#b$nO#l'SO~OS+^O].ROm+^Os$aO!_+^O!`+^O#b$nO$aqO$drO~P0aOS+^O].ROm+^Os$aO!_+^O!`+^O#b$nO$aqO~P0aOS+^O]+aOm+^Os$aO!^+dO!_+^O!`+^O!n.ZO#b$nO$aqO$drO~P0aO!s.^O~O!s._O#b._O$}._O%T+oO~O$}.`O~O#X.aO~Oc%Xad%Xah%Xaj%Xaf%Xag%Xae%Xa~PhOc.dOd+sOP%WqQ%WqS%WqU%WqW%WqX%Wq[%Wq]%Wq^%Wq`%Wqa%Wqb%Wqk%Wqm%Wqo%Wqp%Wqq%Wqs%Wqt%Wqu%Wqv%Wqx%Wqy%Wq|%Wq}%Wq!O%Wq!P%Wq!Q%Wq!R%Wq!T%Wq!U%Wq!V%Wq!W%Wq!X%Wq!Y%Wq!Z%Wq![%Wq!]%Wq!^%Wq!`%Wq!a%Wq!c%Wq!m%Wq!o%Wq!s%Wq!y%Wq#W%Wq#b%Wq#d%Wq#e%Wq#p%Wq$T%Wq$]%Wq$^%Wq$a%Wq$d%Wq$l%Wq$z%Wq${%Wq$}%Wq%O%Wq%V%Wq&p%Wq'g%Wq&t%Wq!n%Wqh%Wqj%Wqf%Wqg%WqY%Wq_%Wqi%Wqe%Wq~Oc.iOd+vOh.hO~O!q(`O~OP6]OQ|OU^OW}O[:fOo>ROs#hOx:dOy:dO}`O!O]O!Q:kO!R}O!T:jO!U:eO!V:eO!Y:oO!c8gO!s#gO!y[O#W_O#bhO#daO#ebO#peO$T:hO$]:gO$^:hO$aqO$z:mO${!OO$}}O%O}O%V|O'g{O~O!m.lO!q.lO~OY+zO_+{O!n.nO~OY+zO_+{Oi%^a~O!x.rO~P>UO!m.tO~O!m.tO~P9yOQ|OW}O!R}O$}}O%O}O%V|O'g{O~OT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!m&ka!}&ka&s&ka~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!m$qi!}$qi&s$qi~P!'WOS+^Om+^Os$aO!_+^O!`+^O$aqO$drO~OY/PO~P$?VOS+^Om+^Os$aO!_+^O!`+^O$aqO~O!s/QO~O!n/SO~P#JwOw(SO!o)WO#l'SO~OV/VO!m&na!}&na&s&na~O!})_O!m'ki&s'ki~O!s/XO~OV/YO!n%|a!}%|a~O]/[Os/[O!s#gO#peO!n&oX!}&oX~O!},uO!n'la~OT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!m&Ua!}&Ua&s&Ua#O&Ua~P!'WOz#RO#S#QO#z#SO#{#WO#|#XO#}#YO$O#ZO$Q#]O$R#^O$S#_O$T#`O$U#aO$V#bO$W#bO$z#dOT!uy!S!uy!b!uy!m!uy!v!uy&s!uy!x!uy!n!uy~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x#hi!}#hi~P!'WO_)yO!n&VX!}&VX~P9yO!}-RO!n&{a~OT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}#Vq#X#Vq~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x#[i!}#[i~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#O/cO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!x&Xa!}&Xa~P!'WO#u/iO!x$ci!}$ci~O#b/jO~O#U/lO#b/kO~OT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x$ci!}$ci~P!'WO#u/mO!x$hi!}$hi~O!}/oO!x'[X~O#b/qO~O!x/rO~O!oXO!q/uO~O#l'SO!o'cy!m'cy&s'cy~O!m$jy&s$jy!x$jy!n$jy~PCqO#O/xO#l'SO~O!s#gO#peOw&aX!o&aX!}&aX!m&aX&s&aX~O!}-|Ow']a!o']a!m']a&s']a~OU$PO]0QO!R$PO!s$OO!v#}O#b$nO#p2XO~P$?uO!m#cO!o0VO&s#cO~O#X0YO~Oh0_O~OT:tOz:pO!S:vO!b:xO!m0`O!q0`O!v=mO#S#QO#z:rO#{:zO#|:|O#};OO$O;QO$Q;UO$R;WO$S;YO$T;[O$U;^O$V;`O$W;`O$z#dO~P!'WOY%]a_%]a!n%]ai%]a~PhO!x0bO~O!x0bO~P>UO!m0dO~OT6iOz6gO!S6jO!b6kO!v8sO!x0fO#O0eO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO~P!'WO!x0fO~O!x0gO#b0hO#l'SO~O!x0iO~O!s0jO~O!m#cO#u0lO&s#cO~O!s0mO~O!})_O!m'kq&s'kq~O!s0nO~OV0oO!n%}X!}%}X~OT:tOz:pO!S:vO!b:xO!v=mO#S#QO#z:rO#{:zO#|:|O#};OO$O;QO$Q;UO$R;WO$S;YO$T;[O$U;^O$V;`O$W;`O$z#dO!n!|i!}!|i~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x$cq!}$cq~P!'WO#u0vO!x$cq!}$cq~O#b0wO~OT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x$hq!}$hq~P!'WO#S0zO#b0yO!x&`X!}&`X~O!}/oO!x'[a~O#l'SO!o'c!R!m'c!R&s'c!R~O!oXO!q1PO~O!m$j!R&s$j!R!x$j!R!n$j!R~PCqO#O1RO#l'SO~OP6]OU^O[9WOo>SOs#hOx9WOy9WO}`O!O]O!Q:lO!T9WO!U9WO!V9WO!Y9WO!c8hO!n1^O!s1YO!y[O#W_O#bhO#daO#ebO#peO$T:iO$]9WO$^:iO$aqO$z:nO${!OO~P$;lOh1_O~OY%[i_%[i!n%[ii%[i~PhOY%]i_%]i!n%]ii%]i~PhO!x1bO~O!x1bO~P>UO!x1eO~O!m#cO#u1iO&s#cO~O$}1jO%V1jO~O!s1kO~OV1lO!n%}a!}%}a~OT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x#]i!}#]i~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x$cy!}$cy~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x$hy!}$hy~P!'WO#b1nO~O!}/oO!x'[i~O!m$j!Z&s$j!Z!x$j!Z!n$j!Z~PCqOT:uOz:qO!S:wO!b:yO!v=nO#S#QO#z:sO#{:{O#|:}O#};PO$O;RO$Q;VO$R;XO$S;ZO$T;]O$U;_O$V;aO$W;aO$z#dO~P!'WOV1uO{1tO~P!5xOV1uO{1tOT&}Xz&}X!S&}X!b&}X!o&}X!v&}X!y&}X#S&}X#W&}X#`&}X#a&}X#s&}X#u&}X#w&}X#z&}X#{&}X#|&}X#}&}X$O&}X$Q&}X$R&}X$S&}X$T&}X$U&}X$V&}X$W&}X$z&}X~OP6]OU^O[9WOo>SOs#hOx9WOy9WO}`O!O]O!Q:lO!T9WO!U9WO!V9WO!Y9WO!c8hO!n1xO!s1YO!y[O#W_O#bhO#daO#ebO#peO$T:iO$]9WO$^:iO$aqO$z:nO${!OO~P$;lOY%[q_%[q!n%[qi%[q~PhO!x1zO~O!x%gi~PCqOe1{O~O$}1|O%V1|O~O!s2OO~OT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x$c!R!}$c!R~P!'WO!m$j!c&s$j!c!x$j!c!n$j!c~PCqO!s2QO~O!`2SO!s2RO~O!s2VO!m$xi&s$xi~O!s'WO~O!s*]O~OT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m$ka#u$ka#w$ka&s$ka!x$ka!n$ka!q$ka#X$ka!}$ka~P!'WO#S2]O~P*kO$l$tO~P#.YOT6iOz6gO!S6jO!b6kO!v8sO#O2[O#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!m'PX&s'PX!x'PX!n'PX~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#O3uO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}'PX#X'PX#u'PX#w'PX!m'PX&s'PX!x'PX!n'PXV'PX!q'PX~P!'WO#S3dO~P#.YOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m$Xa#u$Xa#w$Xa&s$Xa!x$Xa!n$Xa!q$Xa#X$Xa!}$Xa~P!'WOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m$Ya#u$Ya#w$Ya&s$Ya!x$Ya!n$Ya!q$Ya#X$Ya!}$Ya~P!'WOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m$Za#u$Za#w$Za&s$Za!x$Za!n$Za!q$Za#X$Za!}$Za~P!'WOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m$[a#u$[a#w$[a&s$[a!x$[a!n$[a!q$[a#X$[a!}$[a~P!'WOz2aO#u$[a#w$[a!q$[a#X$[a!}$[a~PNyOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m$_a#u$_a#w$_a&s$_a!x$_a!n$_a!q$_a#X$_a!}$_a~P!'WOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m$|a#u$|a#w$|a&s$|a!x$|a!n$|a!q$|a#X$|a!}$|a~P!'WOz2aO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#u#yi#w#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOz2aO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT#yi!S#yi!b#yi!m#yi#u#yi#w#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOT2cOz2aO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!S#yi!m#yi#u#yi#w#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOT2cOz2aO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!S#yi!b#yi!m#yi#u#yi#w#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOz2aO#S#QO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#u#yi#w#yi#z#yi#{#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOz2aO#S#QO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#u#yi#w#yi#z#yi#{#yi#|#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOz2aO#S#QO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOz2aO#S#QO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOz2aO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOz2aO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOz2aO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOz2aO$T2nO$V2pO$W2pO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$U#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOz2aO$V2pO$W2pO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$T#yi$U#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOz2aO$S2mO$T2nO$V2pO$W2pO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$U#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOz2aO$W2pO$z#dOT#yi!S#yi!b#yi!m#yi!v#yi#S#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$T#yi$U#yi$V#yi&s#yi!x#yi!n#yi!q#yi#X#yi!}#yi~P!'WOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m#Ta#u#Ta#w#Ta&s#Ta!x#Ta!n#Ta!q#Ta#X#Ta!}#Ta~P!'WOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m'Pa#u'Pa#w'Pa&s'Pa!x'Pa!n'Pa!q'Pa#X'Pa!}'Pa~P!'WOz2aO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT#Pi!S#Pi!b#Pi!m#Pi#u#Pi#w#Pi&s#Pi!x#Pi!n#Pi!q#Pi#X#Pi!}#Pi~P!'WOz2aO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT#vi!S#vi!b#vi!m#vi#u#vi#w#vi&s#vi!x#vi!n#vi!q#vi#X#vi!}#vi~P!'WOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m#xi#u#xi#w#xi&s#xi!x#xi!n#xi!q#xi#X#xi!}#xi~P!'WOz2aO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT!uq!S!uq!b!uq!m!uq!v!uq#u!uq#w!uq&s!uq!x!uq!n!uq!q!uq#X!uq!}!uq~P!'WOz2aO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT#Pq!S#Pq!b#Pq!m#Pq#u#Pq#w#Pq&s#Pq!x#Pq!n#Pq!q#Pq#X#Pq!}#Pq~P!'WOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m$jq#u$jq#w$jq&s$jq!x$jq!n$jq!q$jq#X$jq!}$jq~P!'WOz2aO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dOT!uy!S!uy!b!uy!m!uy!v!uy#u!uy#w!uy&s!uy!x!uy!n!uy!q!uy#X!uy!}!uy~P!'WOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m$jy#u$jy#w$jy&s$jy!x$jy!n$jy!q$jy#X$jy!}$jy~P!'WOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m$j!R#u$j!R#w$j!R&s$j!R!x$j!R!n$j!R!q$j!R#X$j!R!}$j!R~P!'WOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m$j!Z#u$j!Z#w$j!Z&s$j!Z!x$j!Z!n$j!Z!q$j!Z#X$j!Z!}$j!Z~P!'WOT2cOz2aO!S2dO!b2eO!v4WO#S#QO#z2bO#{2fO#|2gO#}2hO$O2iO$Q2kO$R2lO$S2mO$T2nO$U2oO$V2pO$W2pO$z#dO!m$j!c#u$j!c#w$j!c&s$j!c!x$j!c!n$j!c!q$j!c#X$j!c!}$j!c~P!'WOP6]OU^O[4POo8^Os#hOx3{Oy3{O}`O!O]O!Q4aO!T4VO!U3}O!V3}O!Y4cO!c3yO!s#gO!y[O#S3vO#W_O#bhO#daO#ebO#peO$T4TO$]4RO$^4TO$aqO$z4bO${!OO~P$;lOP6]OU^O[4POo8^Os#hOx3{Oy3{O}`O!O]O!Q4aO!T4VO!U3}O!V3}O!Y4cO!c3yO!s#gO!y[O#W_O#bhO#daO#ebO#peO$T4TO$]4RO$^4TO$aqO$z4bO${!OO~P$;lO#u2uO#w2vO!q&zX#X&zX!}&zX~P0rOP6]OU^O[4POo8^Or2wOs#hOx3{Oy3{O}`O!O]O!Q4aO!T4VO!U3}O!V3}O!Y4cO!c3yO!s#gO!y[O#S2tO#U2sO#W_O#bhO#daO#ebO#peO$T4TO$]4RO$^4TO$aqO$z4bO${!OOT#xXz#xX!S#xX!b#xX!m#xX!o#xX!v#xX#`#xX#a#xX#s#xX#u#xX#w#xX#z#xX#{#xX#|#xX#}#xX$O#xX$Q#xX$R#xX$S#xX$U#xX$V#xX$W#xX&s#xX!x#xX!n#xX!q#xX#X#xX!}#xX~P$;lOP6]OU^O[4POo8^Or4xOs#hOx3{Oy3{O}`O!O]O!Q4aO!T4VO!U3}O!V3}O!Y4cO!c3yO!s#gO!y[O#S4uO#U4tO#W_O#bhO#daO#ebO#peO$T4TO$]4RO$^4TO$aqO$z4bO${!OOT#xXz#xX!S#xX!b#xX!o#xX!v#xX!}#xX#O#xX#X#xX#`#xX#a#xX#s#xX#u#xX#w#xX#z#xX#{#xX#|#xX#}#xX$O#xX$Q#xX$R#xX$S#xX$U#xX$V#xX$W#xX!m#xX&s#xX!x#xX!n#xXV#xX!q#xX~P$;lO!q3PO~P>UO!q5}O#O3gO~OT8vOz8tO!S8wO!b8xO!q3hO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO~P!'WO!q6OO#O3kO~O!q6PO#O3oO~O#O3oO#l'SO~O#O3pO#l'SO~O#O3sO#l'SO~OP6]OU^O[4POo8^Os#hOx3{Oy3{O}`O!O]O!Q4aO!T4VO!U3}O!V3}O!Y4cO!c3yO!s#gO!y[O#W_O#bhO#daO#ebO#peO$T4TO$]4RO$^4TO$aqO$l$tO$z4bO${!OO~P$;lOP6]OU^O[4POo8^Os#hOx3{Oy3{O}`O!O]O!Q4aO!T4VO!U3}O!V3}O!Y4cO!c3yO!s#gO!y[O#S5eO#W_O#bhO#daO#ebO#peO$T4TO$]4RO$^4TO$aqO$z4bO${!OO~P$;lOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}$Xa#O$Xa#X$Xa#u$Xa#w$Xa!m$Xa&s$Xa!x$Xa!n$XaV$Xa!q$Xa~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}$Ya#O$Ya#X$Ya#u$Ya#w$Ya!m$Ya&s$Ya!x$Ya!n$YaV$Ya!q$Ya~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}$Za#O$Za#X$Za#u$Za#w$Za!m$Za&s$Za!x$Za!n$ZaV$Za!q$Za~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}$[a#O$[a#X$[a#u$[a#w$[a!m$[a&s$[a!x$[a!n$[aV$[a!q$[a~P!'WOz4dO!}$[a#O$[a#X$[a#u$[a#w$[aV$[a!q$[a~PNyOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}$_a#O$_a#X$_a#u$_a#w$_a!m$_a&s$_a!x$_a!n$_aV$_a!q$_a~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}$|a#O$|a#X$|a#u$|a#w$|a!m$|a&s$|a!x$|a!n$|aV$|a!q$|a~P!'WOz4dO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#X#yi#u#yi#w#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz4dO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT#yi!S#yi!b#yi!}#yi#O#yi#X#yi#u#yi#w#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOT4fOz4dO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!S#yi!}#yi#O#yi#X#yi#u#yi#w#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOT4fOz4dO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!S#yi!b#yi!}#yi#O#yi#X#yi#u#yi#w#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz4dO#S#QO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#X#yi#u#yi#w#yi#z#yi#{#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz4dO#S#QO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#X#yi#u#yi#w#yi#z#yi#{#yi#|#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz4dO#S#QO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#X#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz4dO#S#QO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#X#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz4dO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz4dO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz4dO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz4dO$T4qO$V4sO$W4sO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$U#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz4dO$V4sO$W4sO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$T#yi$U#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz4dO$S4pO$T4qO$V4sO$W4sO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$U#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz4dO$W4sO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#u#yi#w#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$T#yi$U#yi$V#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}#Ta#O#Ta#X#Ta#u#Ta#w#Ta!m#Ta&s#Ta!x#Ta!n#TaV#Ta!q#Ta~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}'Pa#O'Pa#X'Pa#u'Pa#w'Pa!m'Pa&s'Pa!x'Pa!n'PaV'Pa!q'Pa~P!'WOz4dO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT#Pi!S#Pi!b#Pi!}#Pi#O#Pi#X#Pi#u#Pi#w#Pi!m#Pi&s#Pi!x#Pi!n#PiV#Pi!q#Pi~P!'WOz4dO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT#vi!S#vi!b#vi!}#vi#O#vi#X#vi#u#vi#w#vi!m#vi&s#vi!x#vi!n#viV#vi!q#vi~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}#xi#O#xi#X#xi#u#xi#w#xi!m#xi&s#xi!x#xi!n#xiV#xi!q#xi~P!'WOz4dO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT!uq!S!uq!b!uq!v!uq!}!uq#O!uq#X!uq#u!uq#w!uq!m!uq&s!uq!x!uq!n!uqV!uq!q!uq~P!'WOz4dO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT#Pq!S#Pq!b#Pq!}#Pq#O#Pq#X#Pq#u#Pq#w#Pq!m#Pq&s#Pq!x#Pq!n#PqV#Pq!q#Pq~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}$jq#O$jq#X$jq#u$jq#w$jq!m$jq&s$jq!x$jq!n$jqV$jq!q$jq~P!'WOz4dO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dOT!uy!S!uy!b!uy!v!uy!}!uy#O!uy#X!uy#u!uy#w!uy!m!uy&s!uy!x!uy!n!uyV!uy!q!uy~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}$jy#O$jy#X$jy#u$jy#w$jy!m$jy&s$jy!x$jy!n$jyV$jy!q$jy~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}$j!R#O$j!R#X$j!R#u$j!R#w$j!R!m$j!R&s$j!R!x$j!R!n$j!RV$j!R!q$j!R~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}$j!Z#O$j!Z#X$j!Z#u$j!Z#w$j!Z!m$j!Z&s$j!Z!x$j!Z!n$j!ZV$j!Z!q$j!Z~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}$j!c#O$j!c#X$j!c#u$j!c#w$j!c!m$j!c&s$j!c!x$j!c!n$j!cV$j!c!q$j!c~P!'WO#S5wO~P#.YO!y$hO#S5{O~O!x4ZO#l'SO~O!y$hO#S5|O~OT4fOz4dO!S4gO!b4hO!v6TO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!}$ka#O$ka#X$ka#u$ka#w$ka!m$ka&s$ka!x$ka!n$kaV$ka!q$ka~P!'WOT4fOz4dO!S4gO!b4hO!v6TO#O5vO#S#QO#z4eO#{4iO#|4jO#}4kO$O4lO$Q4nO$R4oO$S4pO$T4qO$U4rO$V4sO$W4sO$z#dO!m'PX#u'PX#w'PX&s'PX!x'PX!n'PX!q'PX#X'PX!}'PX~P!'WO#u4vO#w4wO!}&zX#O&zX#X&zXV&zX!q&zX~P0rO!q5QO~P>UO!q8bO#O5hO~OT8vOz8tO!S8wO!b8xO!q5iO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO~P!'WO!q8cO#O5lO~O!q8dO#O5pO~O#O5pO#l'SO~O#O5qO#l'SO~O#O5tO#l'SO~O$l$tO~P9yOo5zOs$lO~O#S7oO~P9yOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}$Xa#O$Xa#X$Xa!m$Xa&s$Xa!x$Xa!n$XaV$Xa!q$Xa~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}$Ya#O$Ya#X$Ya!m$Ya&s$Ya!x$Ya!n$YaV$Ya!q$Ya~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}$Za#O$Za#X$Za!m$Za&s$Za!x$Za!n$ZaV$Za!q$Za~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}$[a#O$[a#X$[a!m$[a&s$[a!x$[a!n$[aV$[a!q$[a~P!'WOz6gO!}$[a#O$[a#X$[aV$[a!q$[a~PNyOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}$_a#O$_a#X$_a!m$_a&s$_a!x$_a!n$_aV$_a!q$_a~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}$ka#O$ka#X$ka!m$ka&s$ka!x$ka!n$kaV$ka!q$ka~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}$|a#O$|a#X$|a!m$|a&s$|a!x$|a!n$|aV$|a!q$|a~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO!}7sO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x'jX~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO!}7uO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x&|X~P!'WOz6gO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#X#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz6gO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT#yi!S#yi!b#yi!}#yi#O#yi#X#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOT6iOz6gO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!S#yi!}#yi#O#yi#X#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOT6iOz6gO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!S#yi!b#yi!}#yi#O#yi#X#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz6gO#S#QO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#X#yi#z#yi#{#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz6gO#S#QO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#X#yi#z#yi#{#yi#|#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz6gO#S#QO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#X#yi#z#yi#{#yi#|#yi#}#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz6gO#S#QO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#X#yi#z#yi#{#yi#|#yi#}#yi$O#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz6gO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#z#yi#{#yi#|#yi#}#yi$O#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz6gO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz6gO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz6gO$T6tO$V6vO$W6vO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$U#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz6gO$V6vO$W6vO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$T#yi$U#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz6gO$S6sO$T6tO$V6vO$W6vO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$U#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WOz6gO$W6vO$z#dOT#yi!S#yi!b#yi!v#yi!}#yi#O#yi#S#yi#X#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$T#yi$U#yi$V#yi!m#yi&s#yi!x#yi!n#yiV#yi!q#yi~P!'WO#S7zO~P>UO!m#Ta&s#Ta!x#Ta!n#Ta~PCqO!m'Pa&s'Pa!x'Pa!n'Pa~PCqO#S;dO#U;cO!x&WX!}&WX~P9yO!}7lO!x'Oa~Oz6gO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT#Pi!S#Pi!b#Pi!}#Pi#O#Pi#X#Pi!m#Pi&s#Pi!x#Pi!n#PiV#Pi!q#Pi~P!'WOz6gO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT#vi!S#vi!b#vi!}#vi#O#vi#X#vi!m#vi&s#vi!x#vi!n#viV#vi!q#vi~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}#xi#O#xi#X#xi!m#xi&s#xi!x#xi!n#xiV#xi!q#xi~P!'WO!}7sO!x%da~O!x&UX!}&UX~P>UO!}7uO!x&|a~Oz6gO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT!uq!S!uq!b!uq!v!uq!}!uq#O!uq#X!uq!m!uq&s!uq!x!uq!n!uqV!uq!q!uq~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x#Vi!}#Vi~P!'WOz6gO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT#Pq!S#Pq!b#Pq!}#Pq#O#Pq#X#Pq!m#Pq&s#Pq!x#Pq!n#PqV#Pq!q#Pq~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}$jq#O$jq#X$jq!m$jq&s$jq!x$jq!n$jqV$jq!q$jq~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x&ka!}&ka~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x&Ua!}&Ua~P!'WOz6gO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dOT!uy!S!uy!b!uy!v!uy!}!uy#O!uy#X!uy!m!uy&s!uy!x!uy!n!uyV!uy!q!uy~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!x#Vq!}#Vq~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}$jy#O$jy#X$jy!m$jy&s$jy!x$jy!n$jyV$jy!q$jy~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}$j!R#O$j!R#X$j!R!m$j!R&s$j!R!x$j!R!n$j!RV$j!R!q$j!R~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}$j!Z#O$j!Z#X$j!Z!m$j!Z&s$j!Z!x$j!Z!n$j!ZV$j!Z!q$j!Z~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!}$j!c#O$j!c#X$j!c!m$j!c&s$j!c!x$j!c!n$j!cV$j!c!q$j!c~P!'WO#S8[O~P9yO#O8ZO!m'PX&s'PX!x'PX!n'PXV'PX!q'PX~PGSO!y$hO#S8`O~O!y$hO#S8aO~O#u6zO#w6{O!}&zX#O&zX#X&zXV&zX!q&zX~P0rOr6|O#S#oO#U#nO!}#xX#O#xX#X#xXV#xX!q#xX~P2yOr;iO#S9XO#U9VOT#xXz#xX!S#xX!b#xX!m#xX!o#xX!q#xX!v#xX#`#xX#a#xX#s#xX#z#xX#{#xX#|#xX#}#xX$O#xX$Q#xX$R#xX$S#xX$U#xX$V#xX$W#xX!n#xX!}#xX~P9yOr9WO#S9WO#U9WOT#xXz#xX!S#xX!b#xX!o#xX!v#xX#`#xX#a#xX#s#xX#z#xX#{#xX#|#xX#}#xX$O#xX$Q#xX$R#xX$S#xX$U#xX$V#xX$W#xX~P9yOr9]O#S;dO#U;cOT#xXz#xX!S#xX!b#xX!o#xX!q#xX!v#xX#`#xX#a#xX#s#xX#z#xX#{#xX#|#xX#}#xX$O#xX$Q#xX$R#xX$S#xX$U#xX$V#xX$W#xX#X#xX!x#xX!}#xX~P9yO$l$tO~P>UO!q7XO~P>UOT6iOz6gO!S6jO!b6kO!v8sO#O7iO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!x'PX!}'PX~P!'WOP6]OU^O[9WOo>SOs#hOx9WOy9WO}`O!O]O!Q:lO!T9WO!U9WO!V9WO!Y9WO!c8hO!s#gO!y[O#W_O#bhO#daO#ebO#peO$T:iO$]9WO$^:iO$aqO$z:nO${!OO~P$;lO!}7lO!x'OX~O#S9yO~P>UOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!q$Xa#X$Xa!x$Xa!}$Xa~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!q$Ya#X$Ya!x$Ya!}$Ya~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!q$Za#X$Za!x$Za!}$Za~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!q$[a#X$[a!x$[a!}$[a~P!'WOz8tO$z#dOT$[a!S$[a!b$[a!q$[a!v$[a#S$[a#z$[a#{$[a#|$[a#}$[a$O$[a$Q$[a$R$[a$S$[a$T$[a$U$[a$V$[a$W$[a#X$[a!x$[a!}$[a~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!q$_a#X$_a!x$_a!}$_a~P!'WO!q=dO#O7rO~OT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!q$ka#X$ka!x$ka!}$ka~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!q$|a#X$|a!x$|a!}$|a~P!'WOT8vOz8tO!S8wO!b8xO!q7wO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO~P!'WOz8tO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT#yi!S#yi!b#yi!q#yi!v#yi#X#yi!x#yi!}#yi~P!'WOz8tO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT#yi!S#yi!b#yi!q#yi#X#yi!x#yi!}#yi~P!'WOT8vOz8tO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!S#yi!q#yi#X#yi!x#yi!}#yi~P!'WOT8vOz8tO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!S#yi!b#yi!q#yi#X#yi!x#yi!}#yi~P!'WOz8tO#S#QO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT#yi!S#yi!b#yi!q#yi!v#yi#z#yi#{#yi#X#yi!x#yi!}#yi~P!'WOz8tO#S#QO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT#yi!S#yi!b#yi!q#yi!v#yi#z#yi#{#yi#|#yi#X#yi!x#yi!}#yi~P!'WOz8tO#S#QO$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT#yi!S#yi!b#yi!q#yi!v#yi#z#yi#{#yi#|#yi#}#yi#X#yi!x#yi!}#yi~P!'WOz8tO#S#QO$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT#yi!S#yi!b#yi!q#yi!v#yi#z#yi#{#yi#|#yi#}#yi$O#yi#X#yi!x#yi!}#yi~P!'WOz8tO$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT#yi!S#yi!b#yi!q#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi#X#yi!x#yi!}#yi~P!'WOz8tO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT#yi!S#yi!b#yi!q#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi#X#yi!x#yi!}#yi~P!'WOz8tO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT#yi!S#yi!b#yi!q#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi#X#yi!x#yi!}#yi~P!'WOz8tO$T9RO$V9TO$W9TO$z#dOT#yi!S#yi!b#yi!q#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$U#yi#X#yi!x#yi!}#yi~P!'WOz8tO$V9TO$W9TO$z#dOT#yi!S#yi!b#yi!q#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$T#yi$U#yi#X#yi!x#yi!}#yi~P!'WOz8tO$S9QO$T9RO$V9TO$W9TO$z#dOT#yi!S#yi!b#yi!q#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$U#yi#X#yi!x#yi!}#yi~P!'WOz8tO$W9TO$z#dOT#yi!S#yi!b#yi!q#yi!v#yi#S#yi#z#yi#{#yi#|#yi#}#yi$O#yi$Q#yi$R#yi$S#yi$T#yi$U#yi$V#yi#X#yi!x#yi!}#yi~P!'WOz8tO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT#Pi!S#Pi!b#Pi!q#Pi#X#Pi!x#Pi!}#Pi~P!'WOz8tO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT#vi!S#vi!b#vi!q#vi#X#vi!x#vi!}#vi~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!q#xi#X#xi!x#xi!}#xi~P!'WO!q=eO#O7|O~Oz8tO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT!uq!S!uq!b!uq!q!uq!v!uq#X!uq!x!uq!}!uq~P!'WOz8tO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT#Pq!S#Pq!b#Pq!q#Pq#X#Pq!x#Pq!}#Pq~P!'WO!q=iO#O8TO~OT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!q$jq#X$jq!x$jq!}$jq~P!'WO#O8TO#l'SO~Oz8tO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dOT!uy!S!uy!b!uy!q!uy!v!uy#X!uy!x!uy!}!uy~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!q$jy#X$jy!x$jy!}$jy~P!'WO#O8UO#l'SO~OT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!q$j!R#X$j!R!x$j!R!}$j!R~P!'WO#O8XO#l'SO~OT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!q$j!Z#X$j!Z!x$j!Z!}$j!Z~P!'WOT8vOz8tO!S8wO!b8xO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO!q$j!c#X$j!c!x$j!c!}$j!c~P!'WO#S:bO~P>UO#O:aO!q'PX!x'PX~PGSO$l$tO~P$8YOP6]OU^O[9WOo>SOs#hOx9WOy9WO}`O!O]O!Q:lO!T9WO!U9WO!V9WO!Y9WO!c8hO!s#gO!y[O#W_O#bhO#daO#ebO#peO$T:iO$]9WO$^:iO$aqO$l$tO$z:nO${!OO~P$;lOo8_Os$lO~O#SSOs#hOx9WOy9WO}`O!O]O!Q:lO!T9WO!U9WO!V9WO!Y9WO!c8hO!s#gO!y[O#SSOs#hOx9WOy9WO}`O!O]O!Q:lO!T9WO!U9WO!V9WO!Y9WO!c8hO!s#gO!y[O#S=UO#W_O#bhO#daO#ebO#peO$T:iO$]9WO$^:iO$aqO$z:nO${!OO~P$;lOT6iOz6gO!S6jO!b6kO!v8sO#O=SO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO~P!'WOT6iOz6gO!S6jO!b6kO!v8sO#O=RO#S#QO#z6hO#{6lO#|6mO#}6nO$O6oO$Q6qO$R6rO$S6sO$T6tO$U6uO$V6vO$W6vO$z#dO!m'PX!q'PX!n'PX!}'PX~P!'WOT&zXz&zX!S&zX!b&zX!o&zX!q&zX!v&zX!y&zX#S&zX#W&zX#`&zX#a&zX#s&zX#z&zX#{&zX#|&zX#}&zX$O&zX$Q&zX$R&zX$S&zX$T&zX$U&zX$V&zX$W&zX$z&zX!}&zX~O#u9ZO#w9[O#X&zX!x&zX~P.8oO!y$hO#S=^O~O!q9hO~P>UO!y$hO#S=cO~O!q>OO#O9}O~OT8vOz8tO!S8wO!b8xO!q:OO!v=ZO#S#QO#z8uO#{8yO#|8zO#}8{O$O8|O$Q9OO$R9PO$S9QO$T9RO$U9SO$V9TO$W9TO$z#dO~P!'WOT:tOz:pO!S:vO!b:xO!v=mO#S#QO#z:rO#{:zO#|:|O#};OO$O;QO$Q;UO$R;WO$S;YO$T;[O$U;^O$V;`O$W;`O$z#dO!m#Ta!q#Ta!n#Ta!}#Ta~P!'WOT:tOz:pO!S:vO!b:xO!v=mO#S#QO#z:rO#{:zO#|:|O#};OO$O;QO$Q;UO$R;WO$S;YO$T;[O$U;^O$V;`O$W;`O$z#dO!m'Pa!q'Pa!n'Pa!}'Pa~P!'WO!q>PO#O:RO~O!q>QO#O:YO~O#O:YO#l'SO~O#O:ZO#l'SO~O#O:_O#l'SO~O#u;eO#w;gO!m&zX!n&zX~P.8oO#u;fO#w;hOT&zXz&zX!S&zX!b&zX!o&zX!v&zX!y&zX#S&zX#W&zX#`&zX#a&zX#s&zX#z&zX#{&zX#|&zX#}&zX$O&zX$Q&zX$R&zX$S&zX$T&zX$U&zX$V&zX$W&zX$z&zX~O!q;tO~P>UO!q;uO~P>UO!q>XO#OYO#O9WO~OT8vOz8tO!S8wO!b8xO!qZO#O[O#O<{O~O#O<{O#l'SO~O#O9WO#l'SO~O#O<|O#l'SO~O#O=PO#l'SO~O!y$hO#S=|O~Oo=[Os$lO~O!y$hO#S=}O~O!y$hO#S>UO~O!y$hO#S>VO~O!y$hO#S>WO~Oo={Os$lO~Oo>TOs$lO~Oo>SOs$lO~O%O$U$}$d!d$V#b%V#e'g!s#d~",goto:"%&y'mPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP'nP'uPP'{(OPPP(hP(OP(O*ZP*ZPP2W:j:mPP*Z:sBpPBsPBsPP:sCSCVCZ:s:sPPPC^PP:sK^!$S!$S:s!$WP!$W!$W!%UP!.]!7pP!?oP*ZP*Z*ZPPPPP!?rPPPPPPP*Z*Z*Z*ZPP*Z*ZP!E]!GRP!GV!Gy!GR!GR!HP*Z*ZP!HY!Hl!Ib!J`!Jd!J`!Jo!J}!J}!KV!KY!KY*ZPP*ZPP!K^#%[#%[#%`P#%fP(O#%j(O#&S#&V#&V#&](O#&`(O(O#&f#&i(O#&r#&u(O(O(O(O(O#&x(O(O(O(O(O(O(O(O(O#&{!KR(O(O#'_#'o#'r(O(OP#'u#'|#(S#(o#(y#)P#)Z#)b#)h#*d#4X#5T#5Z#5a#5k#5q#5w#6]#6c#6i#6o#6u#6{#7R#7]#7g#7m#7s#7}PPPPPPPP#8T#8X#8}#NO#NR#N]$(f$(r$)X$)_$)b$)e$)k$,X$5v$>_$>b$>h$>k$>n$>w$>{$?X$?k$Bk$CO$C{$K{PP%%y%%}%&Z%&p%&vQ!nQT!qV!rQUOR%x!mRVO}!hPVX!S!j!r!s!w$}%P%S%U(`+r+u.b.d.l0`0a0i1a|!hPVX!S!j!r!s!w$}%P%S%U(`+r+u.b.d.l0`0a0i1aQ%^!ZQ%g!aQ%l!eQ'd$dQ'q$iQ)[%kQ*y'tQ,](xU-n*v*x+OQ.W+cQ.{,[S/t-s-tQ0T.SS0}/s/wQ1V0RQ1o1OR2P1p0u!OPVX[_bjklmnopxyz!S!W!X!Y!]!g!j!r!s!w!y!z!{!}#R#S#T#U#V#W#X#Y#Z#[#]#^#_#`#a#b#k#n#o#s#t$R$S$U$y$}%P%R%S%T%U%c%}&S&W&p&s&t&w'O'U'Y'z(O(`(l({)P)i)p)t)v*P*T*U*o+P+r+u+z,T,V,X-Q-R-d-k-z.b.d.l.t/c/i/m/x0V0`0a0d0e0i0v1R1]1a2[2]2^2_2`2a2b2c2d2e2f2g2h2i2j2k2l2m2n2o2p2s2t2u2v2w3P3d3g3h3k3o3p3s3u3v3x3y3z3{3|3}4O4P4Q4R4S4T4U4V4W4Z4a4b4c4d4e4f4g4h4i4j4k4l4m4n4o4p4q4r4s4t4u4v4w4x5Q5e5h5i5l5p5q5t5v5w6T6^6_6`6a6b6c6d6e6f6g6h6i6j6k6l6m6n6o6p6q6r6s6t6u6v6x6y6z6{6|7X7i7l7o7r7s7u7w7z7|8T8U8X8Z8[8f8g8h8i8j8k8l8m8n8o8p8q8r8s8t8u8v8w8x8y8z8{8|8}9O9P9Q9R9S9T9V9W9X9Z9[9]9h9y9}:O:R:Y:Z:_:a:b:d:e:f:g:h:i:j:k:l:m:n:o:p:q:r:s:t:u:v:w:x:y:z:{:|:};O;P;Q;R;S;T;U;V;W;X;Y;Z;[;];^;_;`;a;c;d;e;f;g;h;i;t;uO>P>Q>X>Y>Z>[3ZfPVX[_bgjklmnoprxyz!S!W!X!Y!]!e!f!g!j!r!s!w!y!z!{!}#R#S#T#U#V#W#X#Y#Z#[#]#^#_#`#a#b#k#n#o#s#t#}$R$S$U$h$y$}%P%R%S%T%U%c%p%r%}&S&W&p&s&t&w'O'S'U'Y'^'i'm'r'z(O(P(R(S(T(`(l({)P)Z)_)c)i)p)t)v*P*T*U*f*o*s*z*}+P+Q+]+`+d+g+r+u+z,T,V,X,Z,u-Q-R-d-k-r-u-z-{-|.Q.b.d.l.t/[/c/i/m/u/x0V0`0a0d0e0i0v1P1R1]1a2[2]2^2_2`2a2b2c2d2e2f2g2h2i2j2k2l2m2n2o2p2s2t2u2v2w3P3d3g3h3k3o3p3s3u3v3x3y3z3{3|3}4O4P4Q4R4S4T4U4V4W4Z4a4b4c4d4e4f4g4h4i4j4k4l4m4n4o4p4q4r4s4t4u4v4w4x5Q5e5h5i5l5p5q5t5v5w5}6O6P6T6]6^6_6`6a6b6c6d6e6f6g6h6i6j6k6l6m6n6o6p6q6r6s6t6u6v6x6y6z6{6|7X7i7l7o7r7s7u7w7z7|8T8U8X8Z8[8b8c8d8f8g8h8i8j8k8l8m8n8o8p8q8r8s8t8u8v8w8x8y8z8{8|8}9O9P9Q9R9S9T9V9W9X9Z9[9]9h9y9}:O:R:Y:Z:_:a:b:d:e:f:g:h:i:j:k:l:m:n:o:p:q:r:s:t:u:v:w:x:y:z:{:|:};O;P;Q;R;S;T;U;V;W;X;Y;Z;[;];^;_;`;a;c;d;e;f;g;h;i;t;uO>P>Q>X>Y>Z>[3scPVX[_bdegjklmnoprxyz!S!W!X!Y!]!e!f!g!j!r!s!w!y!z!{!}#R#S#T#U#V#W#X#Y#Z#[#]#^#_#`#a#b#k#n#o#s#t#{#}$R$S$U$h$y$}%P%R%S%T%U%c%m%n%p%r%}&S&W&p&s&t&w'O'S'U'Y'^'i'm'r'z(O(P(R(S(T(`(l({)P)Z)^)_)c)g)h)i)p)t)v*P*T*U*f*o*s*z*}+P+Q+]+`+d+g+r+u+z,T,V,X,Z,u,x-Q-R-d-k-r-u-z-{-|.Q.b.d.l.t/[/c/i/m/u/x0V0`0a0d0e0i0v1P1R1]1a2W2X2Y2[2]2^2_2`2a2b2c2d2e2f2g2h2i2j2k2l2m2n2o2p2s2t2u2v2w3P3d3g3h3k3o3p3s3u3v3x3y3z3{3|3}4O4P4Q4R4S4T4U4V4W4Z4a4b4c4d4e4f4g4h4i4j4k4l4m4n4o4p4q4r4s4t4u4v4w4x5Q5e5h5i5l5p5q5t5v5w5}6O6P6T6]6^6_6`6a6b6c6d6e6f6g6h6i6j6k6l6m6n6o6p6q6r6s6t6u6v6x6y6z6{6|7X7i7l7o7r7s7u7w7z7|8T8U8X8Z8[8b8c8d8f8g8h8i8j8k8l8m8n8o8p8q8r8s8t8u8v8w8x8y8z8{8|8}9O9P9Q9R9S9T9V9W9X9Z9[9]9h9y9}:O:R:Y:Z:_:a:b:d:e:f:g:h:i:j:k:l:m:n:o:p:q:r:s:t:u:v:w:x:y:z:{:|:};O;P;Q;R;S;T;U;V;W;X;Y;Z;[;];^;_;`;a;c;d;e;f;g;h;i;t;uO>P>Q>X>Y>Z>[0phPVX[_bjklmnopxyz!S!W!X!Y!]!g!j!r!s!w!y!z!{!}#R#S#T#U#V#W#X#Y#Z#[#]#^#_#`#a#b#k#n#o#s#t$R$S$U$y$}%P%R%S%T%U%c%}&S&W&p&s&t&w'O'U'Y'z(O(`(l({)P)i)p)t)v*P*T*U*o+P+r+u+z,T,V,X-Q-R-d-k-z.b.d.l.t/c/i/m/x0`0a0d0e0i0v1R1a2[2]2^2_2`2a2b2c2d2e2f2g2h2i2j2k2l2m2n2o2p2s2t2u2v2w3P3d3g3h3k3o3p3s3u3v3x3y3z3{3|3}4O4P4Q4R4S4T4U4V4W4Z4a4b4c4d4e4f4g4h4i4j4k4l4m4n4o4p4q4r4s4t4u4v4w4x5Q5e5h5i5l5p5q5t5v5w6T6^6_6`6a6b6c6d6e6f6g6h6i6j6k6l6m6n6o6p6q6r6s6t6u6v6x6y6z6{6|7X7i7l7o7r7s7u7w7z7|8T8U8X8Z8[8f8g8h8i8j8k8l8m8n8o8p8q8r8s8t8u8v8w8x8y8z8{8|8}9O9P9Q9R9S9T9V9W9X9Z9[9]9h9y9}:O:R:Y:Z:_:a:b:d:e:f:g:h:i:j:k:l:m:n:o:p:q:r:s:t:u:v:w:x:y:z:{:|:};O;P;Q;R;S;T;U;V;W;X;Y;Z;[;];^;_;`;a;c;d;e;f;g;h;i;t;uRS=p>S>VS=s>T>UR=t>WT'n$h*s!csPVXt!S!j!r!s!w$h$}%P%S%U'i(T(`)W*s+]+g+r+u,g,k.b.d.l0`0a0i1aQ$^rR*`'^Q*x'sQ-t*{R/w-wQ(W$tQ)U%hQ)n%vQ*i'fQ+k(XR-c*jQ(V$tQ)Y%jQ)m%vQ*e'eS*h'f)nS+j(W(XS-b*i*jQ.]+kQ/T,mQ/e-`R/g-cQ(U$tQ)T%hQ)V%iQ)l%vU*g'f)m)nU+i(V(W(XQ,f)UU-a*h*i*jS.[+j+kS/f-b-cQ0X.]R0t/gT+e(T+g[%e!_$b'c+a.R0QR,d)Qb$ov(T+[+]+`+g.P.Q0PR+T'{S+e(T+gT,j)W,kR0W.XT1[0V1]0w|PVX[_bjklmnopxyz!S!W!X!Y!]!g!j!r!s!w!y!z!{!}#R#S#T#U#V#W#X#Y#Z#[#]#^#_#`#a#b#k#n#o#s#t$R$S$U$y$}%P%R%S%T%U%c%}&S&W&p&s&t&w'O'U'Y'z(O(`(l({)P)i)p)t)v*P*T*U*o+P+r+u+z,T,V,X,_-Q-R-d-k-z.b.d.l.t/c/i/m/x0V0`0a0d0e0i0v1R1]1a2[2]2^2_2`2a2b2c2d2e2f2g2h2i2j2k2l2m2n2o2p2s2t2u2v2w3P3d3g3h3k3o3p3s3u3v3x3y3z3{3|3}4O4P4Q4R4S4T4U4V4W4Z4a4b4c4d4e4f4g4h4i4j4k4l4m4n4o4p4q4r4s4t4u4v4w4x5Q5e5h5i5l5p5q5t5v5w6T6^6_6`6a6b6c6d6e6f6g6h6i6j6k6l6m6n6o6p6q6r6s6t6u6v6x6y6z6{6|7X7i7l7o7r7s7u7w7z7|8T8U8X8Z8[8f8g8h8i8j8k8l8m8n8o8p8q8r8s8t8u8v8w8x8y8z8{8|8}9O9P9Q9R9S9T9V9W9X9Z9[9]9h9y9}:O:R:Y:Z:_:a:b:d:e:f:g:h:i:j:k:l:m:n:o:p:q:r:s:t:u:v:w:x:y:z:{:|:};O;P;Q;R;S;T;U;V;W;X;Y;Z;[;];^;_;`;a;c;d;e;f;g;h;i;t;uO>P>Q>X>Y>Z>[R2Y2X|tPVX!S!j!r!s!w$}%P%S%U(`+r+u.b.d.l0`0a0i1aW$`t'i+],gS'i$h*sS+](T+gT,g)W,kQ'_$^R*a'_Q*t'oR-m*tQ/p-oS0{/p0|R0|/qQ-}+XR/|-}Q+g(TR.Y+gS+`(T+gS,h)W,kQ.Q+]W.T+`,h.Q/OR/O,gQ)R%eR,e)RQ'|$oR+U'|Q1]0VR1w1]Q${{R(^${Q+t(aR.c+tQ+w(bR.g+wQ+}(cQ,P(dT.m+},PQ(|%`S,a(|7tR7t7VQ(y%^R,^(yQ,k)WR/R,kQ)`%oS,q)`/WR/W,rQ,v)dR/^,vT!uV!rj!iPVX!j!r!s!w(`+r.l0`0a1aQ%Q!SQ(a$}W(h%P%S%U0iQ.e+uQ0Z.bR0[.d|ZPVX!S!j!r!s!w$}%P%S%U(`+r+u.b.d.l0`0a0i1aQ#f[U#m_#s&wQ#wbQ$VkQ$WlQ$XmQ$YnQ$ZoQ$[pQ$sx^$uy2_4b6e8q:m:nQ$vzQ%W!WQ%Y!XQ%[!YW%`!]%R(l,VU%s!g&p-RQ%|!yQ&O!zQ&Q!{S&U!})v^&^#R2a4d6g8t:p:qQ&_#SQ&`#TQ&a#UQ&b#VQ&c#WQ&d#XQ&e#YQ&f#ZQ&g#[Q&h#]Q&i#^Q&j#_Q&k#`Q&l#aQ&m#bQ&u#nQ&v#oS&{#t'OQ'X$RQ'Z$SQ'[$UQ(]$yQ(p%TQ)q%}Q)s&SQ)u&WQ*O&tS*['U4ZQ*^'Y^*_2[3u5v8Z:a=R=SQ+S'zQ+V(OQ,`({Q,c)PQ,y)iQ,{)pQ,})tQ-V*PQ-W*TQ-X*U^-]2]3v5w8[:b=T=UQ-i*oQ-x+PQ.k+zQ.w,XQ/`-QQ/h-dQ/n-kQ/y-zQ0r/cQ0u/iQ0x/mQ1Q/xU1X0V1]9WQ1d0eQ1m0vQ1q1RQ2Z2^Q2qjQ2r3yQ2x3zQ2y3|Q2z4OQ2{4QQ2|4SQ2}4UQ3O2`Q3Q2bQ3R2cQ3S2dQ3T2eQ3U2fQ3V2gQ3W2hQ3X2iQ3Y2jQ3Z2kQ3[2lQ3]2mQ3^2nQ3_2oQ3`2pQ3a2sQ3b2tQ3c2uQ3e2vQ3f2wQ3i3PQ3j3dQ3l3gQ3m3hQ3n3kQ3q3oQ3r3pQ3t3sQ4Y4WQ4y3{Q4z3}Q4{4PQ4|4RQ4}4TQ5O4VQ5P4cQ5R4eQ5S4fQ5T4gQ5U4hQ5V4iQ5W4jQ5X4kQ5Y4lQ5Z4mQ5[4nQ5]4oQ5^4pQ5_4qQ5`4rQ5a4sQ5b4tQ5c4uQ5d4vQ5f4wQ5g4xQ5j5QQ5k5eQ5m5hQ5n5iQ5o5lQ5r5pQ5s5qQ5u5tQ6Q4aQ6R3xQ6V6TQ6}6^Q7O6_Q7P6`Q7Q6aQ7R6bQ7S6cQ7T6dQ7U6fU7V,T.t0dQ7W%cQ7Y6hQ7Z6iQ7[6jQ7]6kQ7^6lQ7_6mQ7`6nQ7a6oQ7b6pQ7c6qQ7d6rQ7e6sQ7f6tQ7g6uQ7h6vQ7j6xQ7k6yQ7n6zQ7p6{Q7q6|Q7x7XQ7y7iQ7{7oQ7}7rQ8O7sQ8P7uQ8Q7wQ8R7zQ8S7|Q8V8TQ8W8UQ8Y8XQ8]8fU9U#k&s7lQ9^8jQ9_8kQ9`8lQ9a8mQ9b8nQ9c8oQ9e8pQ9f8rQ9g8sQ9i8uQ9j8vQ9k8wQ9l8xQ9m8yQ9n8zQ9o8{Q9p8|Q9q8}Q9r9OQ9s9PQ9t9QQ9u9RQ9v9SQ9w9TQ9x9ZQ9z9[Q9{9]Q:P9hQ:Q9yQ:T9}Q:V:OQ:W:RQ:[:YQ:^:ZQ:`:_Q:c8iQ;j:dQ;k:eQ;l:fQ;m:gQ;n:hQ;o:iQ;p:jQ;q:kQ;r:lQ;s:oQ;v:rQ;w:sQ;x:tQ;y:uQ;z:vQ;{:wQ;|:xQ;}:yQOQ=h>PQ=j>QQ=u>XQ=v>YQ=w>ZR=x>[0t!OPVX[_bjklmnopxyz!S!W!X!Y!]!g!j!r!s!w!y!z!{!}#R#S#T#U#V#W#X#Y#Z#[#]#^#_#`#a#b#k#n#o#s#t$R$S$U$y$}%P%R%S%T%U%c%}&S&W&p&s&t&w'O'U'Y'z(O(`(l({)P)i)p)t)v*P*T*U*o+P+r+u+z,T,V,X-Q-R-d-k-z.b.d.l.t/c/i/m/x0V0`0a0d0e0i0v1R1]1a2[2]2^2_2`2a2b2c2d2e2f2g2h2i2j2k2l2m2n2o2p2s2t2u2v2w3P3d3g3h3k3o3p3s3u3v3x3y3z3{3|3}4O4P4Q4R4S4T4U4V4W4Z4a4b4c4d4e4f4g4h4i4j4k4l4m4n4o4p4q4r4s4t4u4v4w4x5Q5e5h5i5l5p5q5t5v5w6T6^6_6`6a6b6c6d6e6f6g6h6i6j6k6l6m6n6o6p6q6r6s6t6u6v6x6y6z6{6|7X7i7l7o7r7s7u7w7z7|8T8U8X8Z8[8f8g8h8i8j8k8l8m8n8o8p8q8r8s8t8u8v8w8x8y8z8{8|8}9O9P9Q9R9S9T9V9W9X9Z9[9]9h9y9}:O:R:Y:Z:_:a:b:d:e:f:g:h:i:j:k:l:m:n:o:p:q:r:s:t:u:v:w:x:y:z:{:|:};O;P;Q;R;S;T;U;V;W;X;Y;Z;[;];^;_;`;a;c;d;e;f;g;h;i;t;uO>P>Q>X>Y>Z>[S$]r'^Q%k!eS%o!f%rQ)b%pU+X(R(S+dQ,p)_Q,t)cQ/Z,uQ/{-|R0p/[|vPVX!S!j!r!s!w$}%P%S%U(`+r+u.b.d.l0`0a0i1a#U#i[bklmnopxyz!W!X!Y!{#R#S#T#U#V#W#X#Y#Z#[#]#^#_#`#a#b$R$S$U$y%}&S'Y(O)p+P-z/x0e1R2[2]6x6yd+^(T)W+]+`+g,g,h,k.Q/O!t6w'U2^2_2`2a2b2c2d2e2f2g2h2i2j2k2l2m2n2o2p2s2t2u2v2w3P3d3g3h3k3o3p3s3z3|4O4Q4S4U5v5w!x;b3u3v3x3y3{3}4P4R4T4V4Z4a4b4c4d4e4f4g4h4i4j4k4l4m4n4o4p4q4r4s4t4u4v4w4x5Q5e5h5i5l5p5q5t$O=z_j!]!g#k#n#o#s#t%R%T&p&s&t&w'O'z(l({)P)i*P*U,V,X-R6^6_6`6a6b6c6d6e6f6g6h6i6j6k6l6m6n6o6p6q6r6s6t6u6v6z6{6|7X7l7o7r7w7|8T8U8X8Z8[8f8g8h8i#|>]!y!z!}%c&W)t)v*T*o,T-d-k.t/c/i/m0d0v4W6T7i7s7u7z8j8k8l8m8n8o8p8q8r8s8t8u8v8w8x8y8z8{8|8}9O9P9Q9R9S9T9Z9[9]9h9y9}:O:R:Y:Z:_:a:b;c;d=Z=m=n!v>^+z-Q9V9X:d:e:f:g:h:j:k:m:o:p:r:t:v:x:z:|;O;Q;S;U;W;Y;[;^;`;e;g;i;t_0V1]9W:i:l:n:q:s:u:w:y:{:};P;R;T;V;X;Z;];_;a;f;h;u AssignmentExpression ArrayExpression ValueList & VariadicUnpacking ... Pair [ ] ListExpression ValueList Pair Pair SubscriptExpression MemberExpression -> ?-> VariableName DynamicVariable $ ${ CallExpression ArgList NamedArgument SpreadArgument CastExpression UnionType LogicOp OptionalType NamedType QualifiedName \\ NamespaceName ScopedExpression :: ClassMemberName AssignOp UpdateExpression UpdateOp YieldExpression BinaryExpression LogicOp LogicOp LogicOp BitOp BitOp BitOp CompareOp CompareOp BitOp ArithOp ConcatOp ArithOp ArithOp IncludeExpression RequireExpression CloneExpression UnaryExpression ControlOp LogicOp PrintIntrinsic FunctionExpression static ParamList Parameter #[ Attributes Attribute VariadicParameter PropertyParameter UseList ArrowFunction NewExpression class BaseClause ClassInterfaceClause DeclarationList ConstDeclaration VariableDeclarator PropertyDeclaration VariableDeclarator MethodDeclaration UseDeclaration UseList UseInsteadOfClause UseAsClause UpdateExpression ArithOp ShellExpression ThrowExpression Integer Float String MemberExpression SubscriptExpression UnaryExpression ArithOp Interpolation String IfStatement ColonBlock SwitchStatement Block CaseStatement DefaultStatement ColonBlock WhileStatement EmptyStatement DoStatement ForStatement ForSpec SequenceExpression ForeachStatement ForSpec Pair GotoStatement ContinueStatement BreakStatement ReturnStatement TryStatement CatchDeclarator DeclareStatement EchoStatement UnsetStatement ConstDeclaration FunctionDefinition ClassDeclaration InterfaceDeclaration TraitDeclaration EnumDeclaration EnumBody EnumCase NamespaceDefinition NamespaceUseDeclaration UseGroup UseClause UseClause GlobalDeclaration FunctionStaticDeclaration Program",maxTerm:304,nodeProps:[["group",-36,2,8,49,81,83,85,88,93,94,102,106,107,110,111,114,118,123,126,130,132,133,147,148,149,150,153,154,164,165,179,181,182,183,184,185,191,"Expression",-28,74,78,80,82,192,194,199,201,202,205,208,209,210,211,212,214,215,216,217,218,219,220,221,222,225,226,230,231,"Statement",-3,119,121,122,"Type"],["openedBy",69,"phpOpen",76,"{",86,"(",101,"#["],["closedBy",71,"phpClose",77,"}",87,")",158,"]"]],propSources:[hO],skippedNodes:[0],repeatNodeCount:29,tokenData:"!5h_R!ZOX$tXY%nYZ&}Z]$t]^%n^p$tpq%nqr(]rs)wst*atu/nuv2_vw3`wx4gxy8Oyz8fz{8|{|:W|};_}!O;u!O!P=R!P!QBl!Q!RFr!R![Hn![!]Nz!]!^!!O!^!_!!f!_!`!%R!`!a!&V!a!b!'Z!b!c!*T!c!d!*k!d!e!+q!e!}!*k!}#O!-k#O#P!.R#P#Q!.i#Q#R!/P#R#S!*k#S#T!/j#T#U!*k#U#V!+q#V#o!*k#o#p!2y#p#q!3a#q#r!4j#r#s!5Q#s$f$t$f$g%n$g&j!*k&j$I_$t$I_$I`%n$I`$KW$t$KW$KX%n$KX?HT$t?HT?HU%n?HU~$tP$yT&wPOY$tYZ%YZ!^$t!^!_%_!_~$tP%_O&wPP%bSOY$tYZ%YZ!a$t!b~$tV%ub&wP&vUOX$tXY%nYZ&}Z]$t]^%n^p$tpq%nq!^$t!^!_%_!_$f$t$f$g%n$g$I_$t$I_$I`%n$I`$KW$t$KW$KX%n$KX?HT$t?HT?HU%n?HU~$tV'UW&wP&vUXY'nYZ'n]^'npq'n$f$g'n$I_$I`'n$KW$KX'n?HT?HU'nU'sW&vUXY'nYZ'n]^'npq'n$f$g'n$I_$I`'n$KW$KX'n?HT?HU'nR(dU$^Q&wPOY$tYZ%YZ!^$t!^!_%_!_!`(v!`~$tR(}U$QQ&wPOY$tYZ%YZ!^$t!^!_%_!_!`)a!`~$tR)hT$QQ&wPOY$tYZ%YZ!^$t!^!_%_!_~$tV*QT'fS&wP'gQOY$tYZ%YZ!^$t!^!_%_!_~$tV*hZ&wP!dUOY+ZYZ%YZ]+Z]^$t^!^+Z!^!_+}!_!a+Z!a!b-i!b!}+Z!}#O.x#O~+ZV+bX&wP!dUOY+ZYZ%YZ]+Z]^$t^!^+Z!^!_+}!_!a+Z!a!b-i!b~+ZV,SV!dUOY+ZYZ%YZ]+Z]^$t^!a+Z!a!b,i!b~+ZU,lUOY-OYZ-dZ]-O]^-d^!`-O!a~-OU-TT!dUOY-OZ]-O^!a-O!a!b,i!b~-OU-iO!dUV-nX&wPOY+ZYZ.ZZ]+Z]^.b^!^+Z!^!_+}!_!`+Z!`!a$t!a~+ZV.bO&wP!dUV.iT&wP!dUOY$tYZ%YZ!^$t!^!_%_!_~$tV/RX&wP$dQ!dUOY+ZYZ%YZ]+Z]^$t^!^+Z!^!_+}!_!a+Z!a!b-i!b~+Z_/u^&wP#dQOY$tYZ%YZ!^$t!^!_%_!_!c$t!c!}0q!}#R$t#R#S0q#S#T$t#T#o0q#o#p1w#p$g$t$g&j0q&j~$t_0x_&wP#b^OY$tYZ%YZ!Q$t!Q![0q![!^$t!^!_%_!_!c$t!c!}0q!}#R$t#R#S0q#S#T$t#T#o0q#o$g$t$g&j0q&j~$tV2OT&wP#eUOY$tYZ%YZ!^$t!^!_%_!_~$tR2fU&wP$VQOY$tYZ%YZ!^$t!^!_%_!_!`2x!`~$tR3PT#wQ&wPOY$tYZ%YZ!^$t!^!_%_!_~$tV3gW#SU&wPOY$tYZ%YZv$tvw4Pw!^$t!^!_%_!_!`2x!`~$tR4WT#|Q&wPOY$tYZ%YZ!^$t!^!_%_!_~$tR4nX&wP%VQOY4gYZ5ZZw4gwx6bx!^4g!^!_6x!_#O4g#O#P7j#P~4gR5bT&wP%VQOw5qwx6Vx#O5q#O#P6[#P~5qQ5vT%VQOw5qwx6Vx#O5q#O#P6[#P~5qQ6[O%VQQ6_PO~5qR6iT&wP%VQOY$tYZ%YZ!^$t!^!_%_!_~$tR6}X%VQOY4gYZ5ZZw4gwx6bx!a4g!a!b5q!b#O4g#O#P7j#P~4gR7oT&wPOY4gYZ5ZZ!^4g!^!_6x!_~4gR8VT!yQ&wPOY$tYZ%YZ!^$t!^!_%_!_~$tV8mT!xU&wPOY$tYZ%YZ!^$t!^!_%_!_~$tR9TW&wP$VQOY$tYZ%YZz$tz{9m{!^$t!^!_%_!_!`2x!`~$tR9tU$WQ&wPOY$tYZ%YZ!^$t!^!_%_!_!`2x!`~$tR:_W$TQ&wPOY$tYZ%YZ{$t{|:w|!^$t!^!_%_!_!`2x!`~$tR;OT$zQ&wPOY$tYZ%YZ!^$t!^!_%_!_~$tR;fT!}Q&wPOY$tYZ%YZ!^$t!^!_%_!_~$t_z![!^$t!^!_%_!_!`2x!`~$tV=}V&wPOY$tYZ%YZ!O$t!O!P>d!P!^$t!^!_%_!_~$tV>kT#UU&wPOY$tYZ%YZ!^$t!^!_%_!_~$tR?R]&wP%OQOY$tYZ%YZ!Q$t!Q![>z![!^$t!^!_%_!_!g$t!g!h?z!h#R$t#R#SBQ#S#X$t#X#Y?z#Y~$tR@PZ&wPOY$tYZ%YZ{$t{|@r|}$t}!O@r!O!Q$t!Q![A^![!^$t!^!_%_!_~$tR@wV&wPOY$tYZ%YZ!Q$t!Q![A^![!^$t!^!_%_!_~$tRAeX&wP%OQOY$tYZ%YZ!Q$t!Q![A^![!^$t!^!_%_!_#R$t#R#S@r#S~$tRBVV&wPOY$tYZ%YZ!Q$t!Q![>z![!^$t!^!_%_!_~$tVBsY&wP$VQOY$tYZ%YZz$tz{Cc{!P$t!P!Q+Z!Q!^$t!^!_%_!_!`2x!`~$tVChV&wPOYCcYZC}ZzCcz{EQ{!^Cc!^!_FY!_~CcVDSR&wPOzD]z{Di{~D]UD`ROzD]z{Di{~D]UDlTOzD]z{Di{!PD]!P!QD{!Q~D]UEQO!eUVEVX&wPOYCcYZC}ZzCcz{EQ{!PCc!P!QEr!Q!^Cc!^!_FY!_~CcVEyT!eU&wPOY$tYZ%YZ!^$t!^!_%_!_~$tVF]VOYCcYZC}ZzCcz{EQ{!aCc!a!bD]!b~CcZFyk&wP$}YOY$tYZ%YZ!O$t!O!P>z!P!Q$t!Q![Hn![!^$t!^!_%_!_!d$t!d!eJ`!e!g$t!g!h?z!h!q$t!q!rKt!r!z$t!z!{MS!{#R$t#R#SIt#S#U$t#U#VJ`#V#X$t#X#Y?z#Y#c$t#c#dKt#d#l$t#l#mMS#m~$tZHu_&wP$}YOY$tYZ%YZ!O$t!O!P>z!P!Q$t!Q![Hn![!^$t!^!_%_!_!g$t!g!h?z!h#R$t#R#SIt#S#X$t#X#Y?z#Y~$tZIyV&wPOY$tYZ%YZ!Q$t!Q![Hn![!^$t!^!_%_!_~$tZJeW&wPOY$tYZ%YZ!Q$t!Q!RJ}!R!SJ}!S!^$t!^!_%_!_~$tZKUY&wP$}YOY$tYZ%YZ!Q$t!Q!RJ}!R!SJ}!S!^$t!^!_%_!_#R$t#R#SJ`#S~$tZKyV&wPOY$tYZ%YZ!Q$t!Q!YL`!Y!^$t!^!_%_!_~$tZLgX&wP$}YOY$tYZ%YZ!Q$t!Q!YL`!Y!^$t!^!_%_!_#R$t#R#SKt#S~$tZMXZ&wPOY$tYZ%YZ!Q$t!Q![Mz![!^$t!^!_%_!_!c$t!c!iMz!i#T$t#T#ZMz#Z~$tZNR]&wP$}YOY$tYZ%YZ!Q$t!Q![Mz![!^$t!^!_%_!_!c$t!c!iMz!i#R$t#R#SMS#S#T$t#T#ZMz#Z~$tR! RV!qQ&wPOY$tYZ%YZ![$t![!]! h!]!^$t!^!_%_!_~$tR! oT#sQ&wPOY$tYZ%YZ!^$t!^!_%_!_~$tV!!VT!mU&wPOY$tYZ%YZ!^$t!^!_%_!_~$tR!!kW$RQOY$tYZ%YZ!^$t!^!_!#T!_!`!#n!`!a)a!a!b!$[!b~$tR!#[U$SQ&wPOY$tYZ%YZ!^$t!^!_%_!_!`2x!`~$tR!#uV$RQ&wPOY$tYZ%YZ!^$t!^!_%_!_!`$t!`!a)a!a~$tP!$aR!iP!_!`!$j!r!s!$o#d#e!$oP!$oO!iPP!$rQ!j!k!$x#[#]!$xP!${Q!r!s!$j#d#e!$jV!%YV#uQ&wPOY$tYZ%YZ!^$t!^!_%_!_!`(v!`!a!%o!a~$tV!%vT#OU&wPOY$tYZ%YZ!^$t!^!_%_!_~$tR!&^V$RQ&wPOY$tYZ%YZ!^$t!^!_%_!_!`!&s!`!a!#T!a~$tR!&zT$RQ&wPOY$tYZ%YZ!^$t!^!_%_!_~$tV!'bY!vQ&wPOY$tYZ%YZ}$t}!O!(Q!O!^$t!^!_%_!_!`$t!`!a!)S!a!b!)j!b~$tV!(VV&wPOY$tYZ%YZ!^$t!^!_%_!_!`$t!`!a!(l!a~$tV!(sT#aU&wPOY$tYZ%YZ!^$t!^!_%_!_~$tV!)ZT!gU&wPOY$tYZ%YZ!^$t!^!_%_!_~$tR!)qU#zQ&wPOY$tYZ%YZ!^$t!^!_%_!_!`2x!`~$tR!*[T$]Q&wPOY$tYZ%YZ!^$t!^!_%_!_~$t_!*r_&wP!s^OY$tYZ%YZ!Q$t!Q![!*k![!^$t!^!_%_!_!c$t!c!}!*k!}#R$t#R#S!*k#S#T$t#T#o!*k#o$g$t$g&j!*k&j~$t_!+xc&wP!s^OY$tYZ%YZr$trs!-Tsw$twx4gx!Q$t!Q![!*k![!^$t!^!_%_!_!c$t!c!}!*k!}#R$t#R#S!*k#S#T$t#T#o!*k#o$g$t$g&j!*k&j~$tR!-[T&wP'gQOY$tYZ%YZ!^$t!^!_%_!_~$tV!-rT#WU&wPOY$tYZ%YZ!^$t!^!_%_!_~$tV!.YT#pU&wPOY$tYZ%YZ!^$t!^!_%_!_~$tR!.pT#XQ&wPOY$tYZ%YZ!^$t!^!_%_!_~$tR!/WU$OQ&wPOY$tYZ%YZ!^$t!^!_%_!_!`2x!`~$tR!/oX&wPOY!/jYZ!0[Z!^!/j!^!_!1_!_#O!/j#O#P!1}#P#S!/j#S#T!2c#T~!/jR!0aT&wPO#O!0p#O#P!1S#P#S!0p#S#T!1Y#T~!0pQ!0sTO#O!0p#O#P!1S#P#S!0p#S#T!1Y#T~!0pQ!1VPO~!0pQ!1_O${QR!1bXOY!/jYZ!0[Z!a!/j!a!b!0p!b#O!/j#O#P!1}#P#S!/j#S#T!2c#T~!/jR!2ST&wPOY!/jYZ!0[Z!^!/j!^!_!1_!_~!/jR!2jT${Q&wPOY$tYZ%YZ!^$t!^!_%_!_~$tV!3QT!oU&wPOY$tYZ%YZ!^$t!^!_%_!_~$tV!3jW#}Q#lS&wPOY$tYZ%YZ!^$t!^!_%_!_!`2x!`#p$t#p#q!4S#q~$tR!4ZT#{Q&wPOY$tYZ%YZ!^$t!^!_%_!_~$tR!4qT!nQ&wPOY$tYZ%YZ!^$t!^!_%_!_~$tR!5XT$^Q&wPOY$tYZ%YZ!^$t!^!_%_!_~$t",tokenizers:[ZO,wO,kO,0,1,2,3,cO],topRules:{Template:[0,72],Program:[1,232]},dynamicPrecedences:{284:1},specialized:[{term:81,get:(O,Q)=>pO(O)<<1},{term:81,get:O=>GO[O]||-1}],tokenPrec:29354});export{gO as parser}; diff --git a/Resources/Public/JavaScript/Contrib/@lezer/xml.js b/Resources/Public/JavaScript/Contrib/@lezer/xml.js new file mode 100644 index 0000000..532f3db --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/@lezer/xml.js @@ -0,0 +1 @@ +import{ContextTracker as W,ExternalTokenizer as n,LRParser as l}from"@lezer/lr";import{styleTags as w,tags as d}from"@lezer/highlight";const s=1,g=2,i=3,v=4,C=5,p=35,X=36,m=37,z=11,h=13;function Q(O){return O==45||O==46||O==58||O>=65&&O<=90||O==95||O>=97&&O<=122||O>=161}function _(O){return O==9||O==10||O==13||O==32}let o=null,P=null,T=0;function $(O,e){let t=O.pos+e;if(P==O&&T==t)return o;for(;_(O.peek(e));)e++;let r="";for(;;){let a=O.peek(e);if(!Q(a))break;r+=String.fromCharCode(a),e++}return P=O,T=t,o=r||null}function c(O,e){this.name=O,this.parent=e,this.hash=e?e.hash:0;for(let t=0;t{if(O.next==60){if(O.advance(),O.next==47){O.advance();let t=$(O,0);if(!t)return O.acceptToken(C);if(e.context&&t==e.context.name)return O.acceptToken(g);for(let r=e.context;r;r=r.parent)if(r.name==t)return O.acceptToken(i,-2);O.acceptToken(v)}else if(O.next!=33&&O.next!=63)return O.acceptToken(s)}},{contextual:!0});function k(O,e){return new n(t=>{for(let r=0,a=0;;a++){if(t.next<0){a&&t.acceptToken(O);break}if(t.next==e.charCodeAt(r)){if(r++,r==e.length){a>e.length&&t.acceptToken(O,1-e.length);break}}else r=t.next==e.charCodeAt(0)?1:0;t.advance()}})}const f=k(p,"-->"),b=k(X,"?>"),x=k(m,"]]>"),R=w({Text:d.content,"StartTag StartCloseTag EndTag SelfCloseEndTag":d.angleBracket,TagName:d.tagName,"MismatchedCloseTag/Tagname":[d.tagName,d.invalid],AttributeName:d.attributeName,AttributeValue:d.attributeValue,Is:d.definitionOperator,"EntityReference CharacterReference":d.character,Comment:d.blockComment,ProcessingInst:d.processingInstruction,DoctypeDecl:d.documentMeta,Cdata:d.special(d.string)}),u=l.deserialize({version:14,states:",SOQOaOOOrOxO'#CfOzOpO'#CiO!tOaO'#CgOOOP'#Cg'#CgO!{OrO'#CrO#TOtO'#CsO#]OpO'#CtOOOP'#DS'#DSOOOP'#Cv'#CvQQOaOOOOOW'#Cw'#CwO#eOxO,59QOOOP,59Q,59QOOOO'#Cx'#CxO#mOpO,59TO#uO!bO,59TOOOP'#C{'#C{O$TOaO,59RO$[OpO'#CoOOOP,59R,59ROOOQ'#C|'#C|O$dOrO,59^OOOP,59^,59^OOOS'#C}'#C}O$lOtO,59_OOOP,59_,59_O$tOpO,59`O$|OpO,59`OOOP-E6t-E6tOOOW-E6u-E6uOOOP1G.l1G.lOOOO-E6v-E6vO%UO!bO1G.oO%UO!bO1G.oO%dOpO'#CkO%lO!bO'#CyO%zO!bO1G.oOOOP1G.o1G.oOOOP1G.w1G.wOOOP-E6y-E6yOOOP1G.m1G.mO&VOpO,59ZO&_OpO,59ZOOOQ-E6z-E6zOOOP1G.x1G.xOOOS-E6{-E6{OOOP1G.y1G.yO&gOpO1G.zO&gOpO1G.zOOOP1G.z1G.zO&oO!bO7+$ZO&}O!bO7+$ZOOOP7+$Z7+$ZOOOP7+$c7+$cO'YOpO,59VO'bOpO,59VO'jO!bO,59eOOOO-E6w-E6wO'xOpO1G.uO'xOpO1G.uOOOP1G.u1G.uO(QOpO7+$fOOOP7+$f7+$fO(YO!bO<U!a!b>q!b!c$k!c!}+z!}#P$k#P#Q?}#Q#R$k#R#S+z#S#T$k#T#o+z#o%W$k%W%o+z%o%p$k%p&a+z&a&b$k&b1p+z1p4U$k4U4d+z4d4e$k4e$IS+z$IS$I`$k$I`$Ib+z$Ib$Kh$k$Kh%#t+z%#t&/x$k&/x&Et+z&Et&FV$k&FV;'S+z;'S;:j/S;:j?&r$k?&r?Ah+z?Ah?BY$k?BY?Mn+z?Mn~$kX$rUVP{WOr$krs%Usv$kw!^$k!^!_%d!_~$kP%ZRVPOv%Uw!^%U!_~%UW%iR{WOr%dsv%dw~%d_%{]VP{WyUOX$kXY%rYZ%rZ]$k]^%r^p$kpq%rqr$krs%Usv$kw!^$k!^!_%d!_~$kZ&{RzYVPOv%Uw!^%U!_~%U~'XTOp'hqs'hst(Pt!]'h!^~'h~'kTOp'hqs'ht!]'h!]!^'z!^~'h~(POW~~(SROp(]q!](]!^~(]~(`SOp(]q!](]!]!^(l!^~(]~(qOX~Z(xWVP{WOr$krs%Usv$kw}$k}!O)b!O!^$k!^!_%d!_~$kZ)iWVP{WOr$krs%Usv$kw!^$k!^!_%d!_!`$k!`!a*R!a~$kZ*[U|QVP{WOr$krs%Usv$kw!^$k!^!_%d!_~$k]*uWVP{WOr$krs%Usv$kw!^$k!^!_%d!_!`$k!`!a+_!a~$k]+hUdSVP{WOr$krs%Usv$kw!^$k!^!_%d!_~$k_,V}`S^QVP{WOr$krs%Usv$kw}$k}!O+z!O!P+z!P!Q$k!Q![+z![!]+z!]!^$k!^!_%d!_!c$k!c!}+z!}#R$k#R#S+z#S#T$k#T#o+z#o$}$k$}%O+z%O%W$k%W%o+z%o%p$k%p&a+z&a&b$k&b1p+z1p4U+z4U4d+z4d4e$k4e$IS+z$IS$I`$k$I`$Ib+z$Ib$Je$k$Je$Jg+z$Jg$Kh$k$Kh%#t+z%#t&/x$k&/x&Et+z&Et&FV$k&FV;'S+z;'S;:j/S;:j?&r$k?&r?Ah+z?Ah?BY$k?BY?Mn+z?Mn~$k_/ZWVP{WOr$krs%Usv$kw!^$k!^!_%d!_;=`$k;=`<%l+z<%l~$kX/xU{WOq%dqr0[sv%dw!a%d!a!b=X!b~%dX0aZ{WOr%dsv%dw}%d}!O1S!O!f%d!f!g1x!g!}%d!}#O5s#O#W%d#W#X:k#X~%dX1XT{WOr%dsv%dw}%d}!O1h!O~%dX1oR}P{WOr%dsv%dw~%dX1}T{WOr%dsv%dw!q%d!q!r2^!r~%dX2cT{WOr%dsv%dw!e%d!e!f2r!f~%dX2wT{WOr%dsv%dw!v%d!v!w3W!w~%dX3]T{WOr%dsv%dw!{%d!{!|3l!|~%dX3qT{WOr%dsv%dw!r%d!r!s4Q!s~%dX4VT{WOr%dsv%dw!g%d!g!h4f!h~%dX4kV{WOr4frs5Qsv4fvw5Qw!`4f!`!a5c!a~4fP5TRO!`5Q!`!a5^!a~5QP5cOiPX5jRiP{WOr%dsv%dw~%dX5xV{WOr%dsv%dw!e%d!e!f6_!f#V%d#V#W8w#W~%dX6dT{WOr%dsv%dw!f%d!f!g6s!g~%dX6xT{WOr%dsv%dw!c%d!c!d7X!d~%dX7^T{WOr%dsv%dw!v%d!v!w7m!w~%dX7rT{WOr%dsv%dw!c%d!c!d8R!d~%dX8WT{WOr%dsv%dw!}%d!}#O8g#O~%dX8nR{WxPOr%dsv%dw~%dX8|T{WOr%dsv%dw#W%d#W#X9]#X~%dX9bT{WOr%dsv%dw#T%d#T#U9q#U~%dX9vT{WOr%dsv%dw#h%d#h#i:V#i~%dX:[T{WOr%dsv%dw#T%d#T#U8R#U~%dX:pT{WOr%dsv%dw#c%d#c#d;P#d~%dX;UT{WOr%dsv%dw#V%d#V#W;e#W~%dX;jT{WOr%dsv%dw#h%d#h#i;y#i~%dX_U[UVP{WOr$krs%Usv$kw!^$k!^!_%d!_~$kZ>xWVP{WOr$krs%Usv$kw!^$k!^!_%d!_!`$k!`!a?b!a~$kZ?kU!OQVP{WOr$krs%Usv$kw!^$k!^!_%d!_~$kZ@UWVP{WOr$krs%Usv$kw!^$k!^!_%d!_#P$k#P#Q@n#Q~$kZ@uWVP{WOr$krs%Usv$kw!^$k!^!_%d!_!`$k!`!aA_!a~$kZAhUwQVP{WOr$krs%Usv$kw!^$k!^!_%d!_~$k",tokenizers:[U,f,b,x,0,1,2,3],topRules:{Document:[0,6]},tokenPrec:0});export{u as parser}; diff --git a/Resources/Public/JavaScript/Contrib/alwan.js b/Resources/Public/JavaScript/Contrib/alwan.js new file mode 100644 index 0000000..095634a --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/alwan.js @@ -0,0 +1 @@ +const je={id:"",classname:"",theme:"light",parent:"",toggle:!0,popover:!0,position:"bottom-start",margin:4,preset:!0,color:"#000",default:"#000",target:"",disabled:!1,format:"rgb",singleInput:!1,inputs:!0,opacity:!0,preview:!0,copy:!0,swatches:[],toggleSwatches:!1,closeOnScroll:!1,i18n:{picker:"Color picker",buttons:{copy:"Copy color to clipboard",changeFormat:"Change color format",swatch:"Color swatch: %label%",toggleSwatches:"Toggle Swatches"},sliders:{hue:"Change hue",alpha:"Change opacity"}}},k=document,de=k.documentElement,Q="#000000",we="button",be="open",Pe="close",ze="color",D="click",Fe="pointerdown",Re="pointermove",We="pointerup",Ze="scroll",me="keydown",F="input",N="change",X="rgb",fe="hsl",qe={capture:!0},Ke=["hex",X,fe],M=t=>typeof t=="string",Ue=t=>t instanceof Element,ve=t=>Number.isFinite(M(t)&&t.trim()!==""?+t:t),{keys:dt,assign:Y,setPrototypeOf:wt,prototype:bt}=Object,{isArray:ee}=Array,ye=t=>t!=null&&typeof t=="object"&&!ee(t)&&!Ue(t),T=(t,e)=>dt(t).forEach(a=>e(a,t[a])),te=(t,e)=>(T(e,(a,r)=>{Y(t,{[a]:ye(r)?te(t[a]||{},r):r})}),t),Ge=()=>k.body,ae=(t,e=de)=>M(t)&&t.trim()?[...e.querySelectorAll(t)]:Ue(t)?[t]:[],Je=t=>ae(`${F},${we},[tabindex]`,t),O=(t,e,a,r={},o)=>{const s=k.createElement(t);return e&&(s.className=e.trim()),a&&(M(a)?s.innerHTML=a:s.append(...(ee(a)?a:[a]).filter(n=>!!n))),T(M(o)?{...r,"aria-label":o}:r,(n,l)=>s.setAttribute(n,l+"")),s},$=(t,e,a)=>O("div",e,t,{},a),se=(t,e)=>(t&&t!==e&&t.replaceWith(e),e),R=(t="",e="",a,r=t)=>O(we,"alwan__button "+e,a,{type:we,title:r},t),Qe=(t,e,a,r=1)=>O(F,"alwan__slider alwan__"+e,"",{type:"range",max:a,step:r},t),Xe=(t,e)=>t.style.setProperty("--color",e),_e=(t,e,a)=>t.classList.toggle("alwan--"+e,a),xe=(t,e,a)=>{t.style.transform=`translate(${e}px,${a}px)`},j=t=>{const{x:e,y:a,width:r,height:o}=t.getBoundingClientRect();return[e,a,r,o,r+e,o+a]},Ye=t=>$(t,"alwan__container"),$e=(t,e)=>t.style.display=e?"none":"",v=(t,e,a,r)=>t.addEventListener(e,a,r),oe=(t,e,a)=>t.removeEventListener(e,a),ne=(t,e=X)=>e?e+(e===X?`(${t.r}, ${t.g}, ${t.b}`:`(${t.h}, ${t.s}%, ${t.l}%`)+(t.a<1?`, ${t.a})`:")"):Q,{min:W,max:Se,abs:mt,round:B,PI:ft}=Math,Z=(t,e=100,a=0)=>t>e?e:tB((t%=360)<0?t+360:t),Le=t=>parseInt(t,16),vt=t=>{let e,a,{config:r,s:o}=t,s=!1;const n=()=>{const i={};s||(o.t(),s=!0),T(e,(h,w)=>i[h]=w.value),o.o(i[a]||ne(i,a),!0)},l=()=>{e={};const i=a==="hex"||r.singleInput?[a]:[...a+(r.opacity?"a":"")];return $(i.map(h=>O("label","",[e[h]=O(F,"alwan__input",[],{type:"text",value:o.i[h]}),O("span","",h)])),"alwan__inputs")};return{p({inputs:i,format:h,i18n:w}){let f,p,g,b,u=Ke;return i!==!0&&(i=i||{},u=u.filter(c=>i[c])),b=u.length,u=b?u:Ke,a=u[Se(u.indexOf(h),0)],o.u(a),b?(b>1&&(g=R(w.buttons.changeFormat,"",''),v(g,D,()=>{a=u[(u.indexOf(a)+1)%b],o.u(a),f=se(f,l())})),f=l(),p=$(f),v(p,F,n),v(p,N,()=>{o._(),s=!1}),v(p,"focusin",c=>c.target.select()),v(p,me,c=>c.key==="Enter"&&t.c.v(!1)),Ye([p,g])):null},m(i){s||T(e||{},(h,w)=>w.value=i[h]+"")}}},yt={ArrowLeft:-1,ArrowRight:1},_t={ArrowUp:-1,ArrowDown:1},xt=({s:t})=>{let e,a,r,o,s;const n={s:0,l:0},l=(p,g)=>{let b,u,[,,c,_]=s;r=p=Z(p,c),o=g=Z(g,_),xe(a,p,g),b=1-g/_,u=b*(1-p/(2*c)),n.s=u===1||u===0?0:(b-u)/W(u,1-u)*100,n.l=100*u,t.$(n)},i=({x:p,y:g,buttons:b})=>{b?l(p-s[0],g-s[1]):h()},h=()=>{t._(),oe(k,Re,i),oe(k,We,h)},w=p=>{e.setPointerCapture(p.pointerId),t.t(),s=j(e),l(p.x-s[0],p.y-s[1]),v(k,Re,i),v(k,We,h)},f=p=>{const g=p.key,b=yt[g]||0,u=_t[g]||0;(b||u)&&(p.preventDefault(),s=j(e),t.t(),l(r+b*s[2]/100,o+u*s[3]/100),t._())};return{p:({i18n:p,disabled:g})=>(a=$("","alwan__cursor"),e=$(a,"alwan__selector",p.picker||p.palette),g||(e.tabIndex=0,v(e,Fe,w),v(e,me,f)),e),A(p,g){p=(g/=100)+p/100*W(g,1-g),s=j(e),r=(p?2*(1-g/p):0)*s[2],o=(1-p)*s[3],xe(a,r,o)}}},$t=({s:t,e})=>{let a,r,o;return{p:({opacity:s,i18n:{sliders:n}})=>(a=Qe(n.hue,"hue",360),r=s?Qe(n.alpha,"alpha",1,.01):null,o=$([a,r]),v(o,N,()=>e.S(N)),v(o,F,({target:l})=>t.$({[l===a?"h":"a"]:l.value})),o),m(s,n){a.value=s+"",r&&(r.value=n+"")}}},re=t=>(t<16?"0":"")+t.toString(16),ke=(t,e,a)=>(t%=12,B(255*(a-e*W(a,1-a)*Se(-1,W(t-3,9-t,1))))),Ae=O("canvas").getContext("2d"),St={turn:360,rad:180/ft,grad:.9},Lt=/a?\(\s*([+-]?\d*\.?\d+)(\w*)?\s*[\s,]\s*([+-]?\d*\.?\d+)%?\s*,?\s*([+-]?\d*\.?\d+)%?(?:\s*[\/,]\s*([+-]?\d*\.?\d+)(%)?)?\s*\)?$/,tt=t=>M(t)?t:ne(t,(e=>ye(e)&&[fe,X].find(a=>[...a].every(r=>ve(e[r])))||"")(t)),at=t=>(Ae.fillStyle=Q,Ae.fillStyle=t,Ae.fillStyle),kt=(t,e)=>{let a,r,o=tt(t).trim();if(/^hsl/.test(o)){const[s,n,l,i,h,w="1",f]=Lt.exec(o)||[];s&&(r={h:et(+n*(St[l]||1)),s:Z(+i),l:Z(+h),a:Z(+w/(f?100:1),1)})}if(!r){if(/^[\da-f]+$/i.test(o)&&(o="#"+o),o=at(o),o[0]==="#")a={r:Le(o[1]+o[2]),g:Le(o[3]+o[4]),b:Le(o[5]+o[6]),a:1};else{const[s,n,l,i]=o.match(/[\d\.]+/g).map(Number);a={r:s,g:n,b:l,a:i}}r=(({r:s,g:n,b:l,a:i})=>{const h=Se(s/=255,n/=255,l/=255),w=W(s,n,l),f=h-w,p=(h+w)/2;return{h:et(60*(f===0?0:h===s?(n-l)/f%6:h===n?(l-s)/f+2:h===l?(s-n)/f+4:0)),s:f?f/(1-mt(2*p-1))*100:0,l:100*p,a:i}})(a)}return r.a=e?B(100*r.a)/100:1,a&&(a.a=r.a),[r,a]},At=t=>{let e=!1;const a=(r,o)=>{let s,n,l,i;var h;return ye(h=r)&&"color"in h?{color:n,label:i}=r:n=r,l=tt(n),l=at(l)===Q?Q:l,i=M(i)?i:l,s=R(o.replace("%label%",i),"alwan__swatch","",i),Xe(s,l),v(s,D,()=>t.s.o(l,!0,!0)),s};return{p({swatches:r,toggleSwatches:o,i18n:{buttons:s}}){let n,l,i;return ee(r)&&r.length?(n=$(r.map(h=>a(h,s.swatch)),"alwan__swatches"),o?(i=(h=!e)=>{e=h,_e(n,"collapse",e),t.c.k()},l=R(s.toggleSwatches,"alwan__toggle-button",''),v(l,D,()=>i()),i(e),$([n,l])):n):n}}},Ct=t=>({p({preview:e,copy:a,i18n:r}){let o,s,n,l,i;return a&&(o=R(r.buttons.copy,"alwan__cp",''),[n,l]=o.children,s=h=>{$e(n,h),$e(l,!h)},s(!1),i=navigator.clipboard,i&&(v(o,D,()=>i.writeText(t.s.C()).then(()=>s(!0))),v(o,"blur",()=>s()),v(o,"mouseleave",()=>o.blur()))),e?$(o,"alwan__preview"):o}}),st=(t,e)=>t.map(a=>ee(a)?Ye(st(a,e)):a.p(e)),ot={top:[1,5,4,0],bottom:[5,1,4,0],right:[4,0,1,5],left:[0,4,1,5]},nt={start:[0,1,2],center:[1,0,2],end:[2,1,0]},Ht=(t,e)=>{let a,r,o=$(),s=!1,n=null;const{config:l,s:i}=t,h=((c,_)=>{const L=R(),P=L.className+" alwan__ref ";let S;return c&&c.id&&(L.id=c.id),{H:q=>(S=se(S||c,q.preset||!c?L:c),S===L&&(S.className=(P+q.classname).trim(),S.parentNode||Ge().append(S)),v(S,D,_),S),O(){c?(oe(c,D,_),se(S,c)):S.remove()}}})(e,()=>t.toggle()),w=$(o,"alwan"),[f,p,g,b,u]=(c=>[xt,Ct,$t,vt,At].map(_=>_(c)))(t);return i.V(c=>{Xe(a,c.rgb),o.style.cssText=`--rgb:${c.r},${c.g},${c.b};--a:${c.a};--h:${c.h}`,b.m(c)},({a:c,h:_,s:L,l:P})=>{g.m(_,c),f.A(L,P)}),{L(c){const _=this,{id:L,color:P=i.i.hsl}=c,{theme:S,parent:q,toggle:le,popover:Ce,target:rt,disabled:He}=te(l,c);a=h.H(l);let K=ae(q)[0],Me=ae(rt)[0]||a;L&&(w.id=L),_e(w,"dark",S==="dark"),o=se(o,$(st([f,[p,g],b,u],l))),$e(a,!Ce&&!le),n&&(n.M(),n=null),Ce?(K=K||le&&Ge(),r=$(w,"alwan__popover-container"),n=((A,E,Oe,ie,{margin:I,position:Be,closeOnScroll:lt,toggle:ce,disabled:it},{v:U,B:ct})=>{let z;I=ve(I)?+I:0;let G=ct();const[ht,pt]=M(Be)?Be.split("-"):[],he=E.style,pe=A.getRootNode(),Ee=Je(E),ge=Ee[0],Ie=Ee.pop(),ue=()=>{const d=[de.clientWidth,de.clientHeight],y=j(A),x=j(E),J=j(Oe),V=[-1,-1];he.height="",!G||!z||y[4]<0||y[5]<0||y[0]>d[0]||y[1]>d[1]||((ot[ht]||ot.bottom).some(C=>{let m=C%2,H=y[C]+(C<=1?-x[m+2]-I:I);return!(H<0||H+x[m+2]+I>d[m])&&(V[m]=H,m=+!m,(nt[pt]||nt.center).some(Te=>(H=Te===0?y[m]:y[m+4]-(Te===2?x[m+2]:(x[m+2]+y[m+2])/2),!(H<0||H+x[m+2]>d[m]||(V[m]=H,0)))))}),xe(E,...V.map((C,m)=>(m&&C===-1&&x[3]>d[m]&&(he.height=d[m]-6+"px",x[3]=d[m]-3),B((C>=0?C:(d[m]-x[m+2])/2)-J[m])))))};ce&&v(E,me,d=>{let y,{key:x,target:J,shiftKey:V}=d;x==="Escape"?U(!1):x==="Tab"&&(J===ge&&V?y=Ie:J!==Ie||V||(y=ge),y&&(y.focus(),d.preventDefault()))});const gt=[E,ie,...ie.labels||[]],ut=d=>{const y=d.composedPath();G&&!gt.some(x=>y.includes(x))&&U(!1)},Ve=new IntersectionObserver(([d])=>{z=d.isIntersecting,U(!it&&z&&(!ce||G),!0)}),De=({target:d})=>{(pe instanceof ShadowRoot&&d.contains(pe.host)||d.contains(A))&&(ue(),lt&&U(!1))},Ne=d=>{d(window,"resize",ue),d(k,Ze,De,qe),d(pe,Ze,De,qe),d(k,Fe,ut)};return Ve.observe(A),Ne(v),{I:()=>z,k(d,y){G=d,z&&(ue(),ce&&y!==d&&(d?ge:ie).focus())},M(){he.cssText="",Ve.unobserve(A),Ne(oe),Oe.remove()}}})(Me,w,r,a,l,_)):(r=w,_.v(!le||!He&&s,!0)),K?K.append(r):Me.after(r),He&&[a,...Je(w)].forEach(A=>{A.disabled=!0}),i.o(P)},v(c=!s,_=!1){(c!==s&&(!n||n.I())&&!l.disabled&&l.toggle||_)&&(_e(w,be,c),n&&n.k(c,s),s=c,t.e.S(s?be:Pe))},B:()=>s,k(){n&&n.k(s,s)},M(){w.remove(),n&&n.M(),h.O()}}},Mt=t=>{const e={h:0,s:0,l:0,r:0,g:0,b:0,a:1,rgb:"",hsl:"",hex:""},a=t.config,r=t.e.S;let o,s,n,l;return{i:e,C:()=>e[n],V(i,h){o=i,s=h},u(i){n=a.format=i},$(i,h,w=!0,f){const p=e.hex;Y(e,i),Y(e,h||(({h:g,s:b,l:u,a:c})=>({r:ke(g/=30,b/=100,u/=100),g:ke(g+8,b,u),b:ke(g+4,b,u),a:c}))(e)),e.s=B(e.s),e.l=B(e.l),e.rgb=ne(e),e.hsl=ne(e,fe),e.hex=(({r:g,g:b,b:u,a:c})=>"#"+re(g)+re(b)+re(u)+(c<1?re(B(255*c)):""))(e),o(e),p!==e.hex&&(w&&r(ze,e),f&&r(N,e))},o(i,h=!1,w){this.$(...kt(i,a.opacity),h,w),s(e)},t(){l=e[n]},_(){l!==e[n]&&r(N,e)}}};class Ot{static version(){return"2.3.0"}static setDefaults(e){te(je,e)}constructor(e,a){this.config=te({},je),this.e=(r=>{const o={[be]:[],[Pe]:[],[N]:[],[ze]:[]};return{S(s,n=r.s.i){(o[s]||[]).forEach(l=>l(Y({type:s,source:r},n)))},j(s,n){n&&!(o[s]||[]).includes(n)&&o[s].push(n)},D(s,n){s?o[s]&&(o[s]=n?o[s].filter(l=>l!==n):[]):T(o,l=>{o[l]=[]})}}})(this),this.s=Mt(this),this.c=Ht(this,ae(e)[0]),this.c.L(a||{})}setOptions(e){e&&this.c.L(e)}setColor(e){return this.s.o(e),this}getColor(){return{...this.s.i}}isOpen(){return this.c.B()}open(){this.c.v(!0)}close(){this.c.v(!1)}toggle(){this.c.v()}on(e,a){this.e.j(e,a)}off(e,a){this.e.D(e,a)}addSwatches(...e){this.c.L({swatches:this.config.swatches.concat(e)})}removeSwatches(...e){this.c.L({swatches:this.config.swatches.filter((a,r)=>!e.some(o=>ve(o)?+o===r:o===a))})}enable(){this.c.L({disabled:!1})}disable(){this.c.L({disabled:!0})}reset(){this.s.o(this.config.default)}reposition(){this.c.k()}trigger(e){this.e.S(e)}destroy(){this.c.M(),T(this,e=>{this[e]=null}),wt(this,bt)}}export{Ot as default}; diff --git a/Resources/Public/JavaScript/Contrib/bootstrap.js b/Resources/Public/JavaScript/Contrib/bootstrap.js new file mode 100644 index 0000000..57d75e3 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/bootstrap.js @@ -0,0 +1 @@ +var yo=Object.defineProperty;var wo=(e,t)=>{for(var r in t)yo(e,r,{get:t[r],enumerable:!0})};var ot=new Map,ae={set(e,t,r){ot.has(e)||ot.set(e,new Map);let o=ot.get(e);if(!o.has(t)&&o.size!==0){console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(o.keys())[0]}.`);return}o.set(t,r)},get(e,t){return ot.has(e)&&ot.get(e).get(t)||null},remove(e,t){if(!ot.has(e))return;let r=ot.get(e);r.delete(t),r.size===0&&ot.delete(e)}};var Re="transitionend",Me=e=>(e&&window.CSS&&window.CSS.escape&&(e=e.replace(/#([^\s"#']+)/g,(t,r)=>`#${CSS.escape(r)}`)),e),Cr=e=>e==null?`${e}`:Object.prototype.toString.call(e).match(/\s([a-z]+)/i)[1].toLowerCase(),Sr=e=>{do e+=Math.floor(Math.random()*1e6);while(document.getElementById(e));return e},xo=e=>{if(!e)return 0;let{transitionDuration:t,transitionDelay:r}=window.getComputedStyle(e),o=Number.parseFloat(t),i=Number.parseFloat(r);return!o&&!i?0:(t=t.split(",")[0],r=r.split(",")[0],(Number.parseFloat(t)+Number.parseFloat(r))*1e3)},He=e=>{e.dispatchEvent(new Event(Re))},it=e=>!e||typeof e!="object"?!1:(typeof e.jquery<"u"&&(e=e[0]),typeof e.nodeType<"u"),J=e=>it(e)?e.jquery?e[0]:e:typeof e=="string"&&e.length>0?document.querySelector(Me(e)):null,le=e=>{if(!it(e)||e.getClientRects().length===0)return!1;let t=getComputedStyle(e).getPropertyValue("visibility")==="visible",r=e.closest("details:not([open])");if(!r)return t;if(r!==e){let o=e.closest("summary");if(o&&o.parentNode!==r||o===null)return!1}return t},Nr=e=>!e||e.nodeType!==Node.ELEMENT_NODE||e.classList.contains("disabled")?!0:typeof e.disabled<"u"?e.disabled:e.hasAttribute("disabled")&&e.getAttribute("disabled")!=="false",ke=e=>{if(!document.documentElement.attachShadow)return null;if(typeof e.getRootNode=="function"){let t=e.getRootNode();return t instanceof ShadowRoot?t:null}return e instanceof ShadowRoot?e:e.parentNode?ke(e.parentNode):null},$e=()=>{},ce=e=>{e.offsetHeight},Ve=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,Ie=[],To=e=>{document.readyState==="loading"?(Ie.length||document.addEventListener("DOMContentLoaded",()=>{for(let t of Ie)t()}),Ie.push(e)):e()},At=()=>document.documentElement.dir==="rtl",nt=e=>{To(()=>{let t=Ve();if(t){let r=e.NAME,o=t.fn[r];t.fn[r]=e.jQueryInterface,t.fn[r].Constructor=e,t.fn[r].noConflict=()=>(t.fn[r]=o,e.jQueryInterface)}})},K=(e,t=[],r=e)=>typeof e=="function"?e(...t):r,Dr=(e,t,r=!0)=>{if(!r){K(e);return}let i=xo(t)+5,n=!1,s=({target:a})=>{a===t&&(n=!0,t.removeEventListener(Re,s),K(e))};t.addEventListener(Re,s),setTimeout(()=>{n||He(t)},i)},Lr=(e,t,r,o)=>{let i=e.length,n=e.indexOf(t);return n===-1?!r&&o?e[i-1]:e[0]:(n+=r?1:-1,o&&(n=(n+i)%i),e[Math.max(0,Math.min(n,i-1))])};var Ao=/[^.]*(?=\..*)\.|.*/,Oo=/\..*/,Co=/::\d+$/,je={},Pr=1,Rr={mouseenter:"mouseover",mouseleave:"mouseout"},So=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function Mr(e,t){return t&&`${t}::${Pr++}`||e.uidEvent||Pr++}function Hr(e){let t=Mr(e);return e.uidEvent=t,je[t]=je[t]||{},je[t]}function No(e,t){return function r(o){return Fe(o,{delegateTarget:e}),r.oneOff&&We.off(e,o.type,t),t.apply(e,[o])}}function Do(e,t,r){return function o(i){let n=e.querySelectorAll(t);for(let{target:s}=i;s&&s!==this;s=s.parentNode)for(let a of n)if(a===s)return Fe(i,{delegateTarget:s}),o.oneOff&&We.off(e,i.type,t,r),r.apply(s,[i])}}function kr(e,t,r=null){return Object.values(e).find(o=>o.callable===t&&o.delegationSelector===r)}function $r(e,t,r){let o=typeof t=="string",i=o?r:t||r,n=Vr(e);return So.has(n)||(n=e),[o,i,n]}function Ir(e,t,r,o,i){if(typeof t!="string"||!e)return;let[n,s,a]=$r(t,r,o);t in Rr&&(s=(b=>function(d){if(!d.relatedTarget||d.relatedTarget!==d.delegateTarget&&!d.delegateTarget.contains(d.relatedTarget))return b.call(this,d)})(s));let l=Hr(e),f=l[a]||(l[a]={}),c=kr(f,s,n?r:null);if(c){c.oneOff=c.oneOff&&i;return}let u=Mr(s,t.replace(Ao,"")),m=n?Do(e,r,s):No(e,s);m.delegationSelector=n?r:null,m.callable=s,m.oneOff=i,m.uidEvent=u,f[u]=m,e.addEventListener(a,m,n)}function Be(e,t,r,o,i){let n=kr(t[r],o,i);n&&(e.removeEventListener(r,n,!!i),delete t[r][n.uidEvent])}function Lo(e,t,r,o){let i=t[r]||{};for(let[n,s]of Object.entries(i))n.includes(o)&&Be(e,t,r,s.callable,s.delegationSelector)}function Vr(e){return e=e.replace(Oo,""),Rr[e]||e}var We={on(e,t,r,o){Ir(e,t,r,o,!1)},one(e,t,r,o){Ir(e,t,r,o,!0)},off(e,t,r,o){if(typeof t!="string"||!e)return;let[i,n,s]=$r(t,r,o),a=s!==t,l=Hr(e),f=l[s]||{},c=t.startsWith(".");if(typeof n<"u"){if(!Object.keys(f).length)return;Be(e,l,s,n,i?r:null);return}if(c)for(let u of Object.keys(l))Lo(e,l,u,t.slice(1));for(let[u,m]of Object.entries(f)){let p=u.replace(Co,"");(!a||t.includes(p))&&Be(e,l,s,m.callable,m.delegationSelector)}},trigger(e,t,r){if(typeof t!="string"||!e)return null;let o=Ve(),i=Vr(t),n=t!==i,s=null,a=!0,l=!0,f=!1;n&&o&&(s=o.Event(t,r),o(e).trigger(s),a=!s.isPropagationStopped(),l=!s.isImmediatePropagationStopped(),f=s.isDefaultPrevented());let c=Fe(new Event(t,{bubbles:a,cancelable:!0}),r);return f&&c.preventDefault(),l&&e.dispatchEvent(c),c.defaultPrevented&&s&&s.preventDefault(),c}};function Fe(e,t={}){for(let[r,o]of Object.entries(t))try{e[r]=o}catch{Object.defineProperty(e,r,{configurable:!0,get(){return o}})}return e}var h=We;function jr(e){if(e==="true")return!0;if(e==="false")return!1;if(e===Number(e).toString())return Number(e);if(e===""||e==="null")return null;if(typeof e!="string")return e;try{return JSON.parse(decodeURIComponent(e))}catch{return e}}function Ue(e){return e.replace(/[A-Z]/g,t=>`-${t.toLowerCase()}`)}var Po={setDataAttribute(e,t,r){e.setAttribute(`data-bs-${Ue(t)}`,r)},removeDataAttribute(e,t){e.removeAttribute(`data-bs-${Ue(t)}`)},getDataAttributes(e){if(!e)return{};let t={},r=Object.keys(e.dataset).filter(o=>o.startsWith("bs")&&!o.startsWith("bsConfig"));for(let o of r){let i=o.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),t[i]=jr(e.dataset[o])}return t},getDataAttribute(e,t){return jr(e.getAttribute(`data-bs-${Ue(t)}`))}},dt=Po;var ze=class{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,r){let o=it(r)?dt.getDataAttribute(r,"config"):{};return{...this.constructor.Default,...typeof o=="object"?o:{},...it(r)?dt.getDataAttributes(r):{},...typeof t=="object"?t:{}}}_typeCheckConfig(t,r=this.constructor.DefaultType){for(let[o,i]of Object.entries(r)){let n=t[o],s=it(n)?"element":Cr(n);if(!new RegExp(i).test(s))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${o}" provided type "${s}" but expected type "${i}".`)}}},Ot=ze;var Io="5.3.2",Xe=class extends Ot{constructor(t,r){super(),t=J(t),t&&(this._element=t,this._config=this._getConfig(r),ae.set(this._element,this.constructor.DATA_KEY,this))}dispose(){ae.remove(this._element,this.constructor.DATA_KEY),h.off(this._element,this.constructor.EVENT_KEY);for(let t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,r,o=!0){Dr(t,r,o)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return ae.get(J(t),this.DATA_KEY)}static getOrCreateInstance(t,r={}){return this.getInstance(t)||new this(t,typeof r=="object"?r:null)}static get VERSION(){return Io}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}},Ct=Xe;var Ye=e=>{let t=e.getAttribute("data-bs-target");if(!t||t==="#"){let r=e.getAttribute("href");if(!r||!r.includes("#")&&!r.startsWith("."))return null;r.includes("#")&&!r.startsWith("#")&&(r=`#${r.split("#")[1]}`),t=r&&r!=="#"?Me(r.trim()):null}return t},fe={find(e,t=document.documentElement){return[].concat(...Element.prototype.querySelectorAll.call(t,e))},findOne(e,t=document.documentElement){return Element.prototype.querySelector.call(t,e)},children(e,t){return[].concat(...e.children).filter(r=>r.matches(t))},parents(e,t){let r=[],o=e.parentNode.closest(t);for(;o;)r.push(o),o=o.parentNode.closest(t);return r},prev(e,t){let r=e.previousElementSibling;for(;r;){if(r.matches(t))return[r];r=r.previousElementSibling}return[]},next(e,t){let r=e.nextElementSibling;for(;r;){if(r.matches(t))return[r];r=r.nextElementSibling}return[]},focusableChildren(e){let t=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(r=>`${r}:not([tabindex^="-"])`).join(",");return this.find(t,e).filter(r=>!Nr(r)&&le(r))},getSelectorFromElement(e){let t=Ye(e);return t&&fe.findOne(t)?t:null},getElementFromSelector(e){let t=Ye(e);return t?fe.findOne(t):null},getMultipleElementsFromSelector(e){let t=Ye(e);return t?fe.find(t):[]}},N=fe;var Ro="swipe",St=".bs.swipe",Mo=`touchstart${St}`,Ho=`touchmove${St}`,ko=`touchend${St}`,$o=`pointerdown${St}`,Vo=`pointerup${St}`,jo="touch",Bo="pen",Wo="pointer-event",Fo=40,Uo={endCallback:null,leftCallback:null,rightCallback:null},zo={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"},Ke=class e extends Ot{constructor(t,r){super(),this._element=t,!(!t||!e.isSupported())&&(this._config=this._getConfig(r),this._deltaX=0,this._supportPointerEvents=!!window.PointerEvent,this._initEvents())}static get Default(){return Uo}static get DefaultType(){return zo}static get NAME(){return Ro}dispose(){h.off(this._element,St)}_start(t){if(!this._supportPointerEvents){this._deltaX=t.touches[0].clientX;return}this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX)}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),K(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){let t=Math.abs(this._deltaX);if(t<=Fo)return;let r=t/this._deltaX;this._deltaX=0,r&&K(r>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(h.on(this._element,$o,t=>this._start(t)),h.on(this._element,Vo,t=>this._end(t)),this._element.classList.add(Wo)):(h.on(this._element,Mo,t=>this._start(t)),h.on(this._element,Ho,t=>this._move(t)),h.on(this._element,ko,t=>this._end(t)))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&(t.pointerType===Bo||t.pointerType===jo)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}},qe=Ke;var Xo="carousel",Yo="bs.carousel",st=`.${Yo}`,Br=".data-api",Ko="ArrowLeft",qo="ArrowRight",Go=500,Ft="next",Nt="prev",Dt="left",ue="right",Qo=`slide${st}`,Ge=`slid${st}`,Jo=`keydown${st}`,Zo=`mouseenter${st}`,ti=`mouseleave${st}`,ei=`dragstart${st}`,ri=`load${st}${Br}`,oi=`click${st}${Br}`,Wr="carousel",pe="active",ii="slide",ni="carousel-item-end",si="carousel-item-start",ai="carousel-item-next",li="carousel-item-prev",Fr=".active",Ur=".carousel-item",ci=Fr+Ur,fi=".carousel-item img",pi=".carousel-indicators",ui="[data-bs-slide], [data-bs-slide-to]",mi='[data-bs-ride="carousel"]',di={[Ko]:ue,[qo]:Dt},hi={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},gi={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"},Lt=class e extends Ct{constructor(t,r){super(t,r),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=N.findOne(pi,this._element),this._addEventListeners(),this._config.ride===Wr&&this.cycle()}static get Default(){return hi}static get DefaultType(){return gi}static get NAME(){return Xo}next(){this._slide(Ft)}nextWhenVisible(){!document.hidden&&le(this._element)&&this.next()}prev(){this._slide(Nt)}pause(){this._isSliding&&He(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval(()=>this.nextWhenVisible(),this._config.interval)}_maybeEnableCycle(){if(this._config.ride){if(this._isSliding){h.one(this._element,Ge,()=>this.cycle());return}this.cycle()}}to(t){let r=this._getItems();if(t>r.length-1||t<0)return;if(this._isSliding){h.one(this._element,Ge,()=>this.to(t));return}let o=this._getItemIndex(this._getActive());if(o===t)return;let i=t>o?Ft:Nt;this._slide(i,r[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&h.on(this._element,Jo,t=>this._keydown(t)),this._config.pause==="hover"&&(h.on(this._element,Zo,()=>this.pause()),h.on(this._element,ti,()=>this._maybeEnableCycle())),this._config.touch&&qe.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(let o of N.find(fi,this._element))h.on(o,ei,i=>i.preventDefault());let r={leftCallback:()=>this._slide(this._directionToOrder(Dt)),rightCallback:()=>this._slide(this._directionToOrder(ue)),endCallback:()=>{this._config.pause==="hover"&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(()=>this._maybeEnableCycle(),Go+this._config.interval))}};this._swipeHelper=new qe(this._element,r)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;let r=di[t.key];r&&(t.preventDefault(),this._slide(this._directionToOrder(r)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;let r=N.findOne(Fr,this._indicatorsElement);r.classList.remove(pe),r.removeAttribute("aria-current");let o=N.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);o&&(o.classList.add(pe),o.setAttribute("aria-current","true"))}_updateInterval(){let t=this._activeElement||this._getActive();if(!t)return;let r=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=r||this._config.defaultInterval}_slide(t,r=null){if(this._isSliding)return;let o=this._getActive(),i=t===Ft,n=r||Lr(this._getItems(),o,i,this._config.wrap);if(n===o)return;let s=this._getItemIndex(n),a=p=>h.trigger(this._element,p,{relatedTarget:n,direction:this._orderToDirection(t),from:this._getItemIndex(o),to:s});if(a(Qo).defaultPrevented||!o||!n)return;let f=!!this._interval;this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(s),this._activeElement=n;let c=i?si:ni,u=i?ai:li;n.classList.add(u),ce(n),o.classList.add(c),n.classList.add(c);let m=()=>{n.classList.remove(c,u),n.classList.add(pe),o.classList.remove(pe,u,c),this._isSliding=!1,a(Ge)};this._queueCallback(m,o,this._isAnimated()),f&&this.cycle()}_isAnimated(){return this._element.classList.contains(ii)}_getActive(){return N.findOne(ci,this._element)}_getItems(){return N.find(Ur,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return At()?t===Dt?Nt:Ft:t===Dt?Ft:Nt}_orderToDirection(t){return At()?t===Nt?Dt:ue:t===Nt?ue:Dt}static jQueryInterface(t){return this.each(function(){let r=e.getOrCreateInstance(this,t);if(typeof t=="number"){r.to(t);return}if(typeof t=="string"){if(r[t]===void 0||t.startsWith("_")||t==="constructor")throw new TypeError(`No method named "${t}"`);r[t]()}})}};h.on(document,oi,ui,function(e){let t=N.getElementFromSelector(this);if(!t||!t.classList.contains(Wr))return;e.preventDefault();let r=Lt.getOrCreateInstance(t),o=this.getAttribute("data-bs-slide-to");if(o){r.to(o),r._maybeEnableCycle();return}if(dt.getDataAttribute(this,"slide")==="next"){r.next(),r._maybeEnableCycle();return}r.prev(),r._maybeEnableCycle()});h.on(window,ri,()=>{let e=N.find(mi);for(let t of e)Lt.getOrCreateInstance(t)});nt(Lt);var vi=Lt;var _i="collapse",Ei="bs.collapse",zt=`.${Ei}`,bi=".data-api",yi=`show${zt}`,wi=`shown${zt}`,xi=`hide${zt}`,Ti=`hidden${zt}`,Ai=`click${zt}${bi}`,Qe="show",Pt="collapse",me="collapsing",Oi="collapsed",Ci=`:scope .${Pt} .${Pt}`,Si="collapse-horizontal",Ni="width",Di="height",Li=".collapse.show, .collapse.collapsing",Je='[data-bs-toggle="collapse"]',Pi={parent:null,toggle:!0},Ii={parent:"(null|element)",toggle:"boolean"},Ut=class e extends Ct{constructor(t,r){super(t,r),this._isTransitioning=!1,this._triggerArray=[];let o=N.find(Je);for(let i of o){let n=N.getSelectorFromElement(i),s=N.find(n).filter(a=>a===this._element);n!==null&&s.length&&this._triggerArray.push(i)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Pi}static get DefaultType(){return Ii}static get NAME(){return _i}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(Li).filter(a=>a!==this._element).map(a=>e.getOrCreateInstance(a,{toggle:!1}))),t.length&&t[0]._isTransitioning||h.trigger(this._element,yi).defaultPrevented)return;for(let a of t)a.hide();let o=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(me),this._element.style[o]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;let i=()=>{this._isTransitioning=!1,this._element.classList.remove(me),this._element.classList.add(Pt,Qe),this._element.style[o]="",h.trigger(this._element,wi)},s=`scroll${o[0].toUpperCase()+o.slice(1)}`;this._queueCallback(i,this._element,!0),this._element.style[o]=`${this._element[s]}px`}hide(){if(this._isTransitioning||!this._isShown()||h.trigger(this._element,xi).defaultPrevented)return;let r=this._getDimension();this._element.style[r]=`${this._element.getBoundingClientRect()[r]}px`,ce(this._element),this._element.classList.add(me),this._element.classList.remove(Pt,Qe);for(let i of this._triggerArray){let n=N.getElementFromSelector(i);n&&!this._isShown(n)&&this._addAriaAndCollapsedClass([i],!1)}this._isTransitioning=!0;let o=()=>{this._isTransitioning=!1,this._element.classList.remove(me),this._element.classList.add(Pt),h.trigger(this._element,Ti)};this._element.style[r]="",this._queueCallback(o,this._element,!0)}_isShown(t=this._element){return t.classList.contains(Qe)}_configAfterMerge(t){return t.toggle=!!t.toggle,t.parent=J(t.parent),t}_getDimension(){return this._element.classList.contains(Si)?Ni:Di}_initializeChildren(){if(!this._config.parent)return;let t=this._getFirstLevelChildren(Je);for(let r of t){let o=N.getElementFromSelector(r);o&&this._addAriaAndCollapsedClass([r],this._isShown(o))}}_getFirstLevelChildren(t){let r=N.find(Ci,this._config.parent);return N.find(t,this._config.parent).filter(o=>!r.includes(o))}_addAriaAndCollapsedClass(t,r){if(t.length)for(let o of t)o.classList.toggle(Oi,!r),o.setAttribute("aria-expanded",r)}static jQueryInterface(t){let r={};return typeof t=="string"&&/show|hide/.test(t)&&(r.toggle=!1),this.each(function(){let o=e.getOrCreateInstance(this,r);if(typeof t=="string"){if(typeof o[t]>"u")throw new TypeError(`No method named "${t}"`);o[t]()}})}};h.on(document,Ai,Je,function(e){(e.target.tagName==="A"||e.delegateTarget&&e.delegateTarget.tagName==="A")&&e.preventDefault();for(let t of N.getMultipleElementsFromSelector(this))Ut.getOrCreateInstance(t,{toggle:!1}).toggle()});nt(Ut);var Ri=Ut;import"@typo3/backend/dropdown.js";var dr={};wo(dr,{afterMain:()=>Gr,afterRead:()=>Yr,afterWrite:()=>Zr,applyStyles:()=>Rt,arrow:()=>he,auto:()=>Xt,basePlacements:()=>Z,beforeMain:()=>Kr,beforeRead:()=>zr,beforeWrite:()=>Qr,bottom:()=>D,clippingParents:()=>Ze,computeStyles:()=>Ht,createPopper:()=>xe,createPopperBase:()=>lo,createPopperLite:()=>co,detectOverflow:()=>B,end:()=>at,eventListeners:()=>kt,flip:()=>Ee,hide:()=>be,left:()=>A,main:()=>qr,modifierPhases:()=>er,offset:()=>ye,placements:()=>Kt,popper:()=>ht,popperGenerator:()=>xt,popperOffsets:()=>jt,preventOverflow:()=>we,read:()=>Xr,reference:()=>tr,right:()=>S,start:()=>Q,top:()=>x,variationPlacements:()=>de,viewport:()=>Yt,write:()=>Jr});var x="top",D="bottom",S="right",A="left",Xt="auto",Z=[x,D,S,A],Q="start",at="end",Ze="clippingParents",Yt="viewport",ht="popper",tr="reference",de=Z.reduce(function(e,t){return e.concat([t+"-"+Q,t+"-"+at])},[]),Kt=[].concat(Z,[Xt]).reduce(function(e,t){return e.concat([t,t+"-"+Q,t+"-"+at])},[]),zr="beforeRead",Xr="read",Yr="afterRead",Kr="beforeMain",qr="main",Gr="afterMain",Qr="beforeWrite",Jr="write",Zr="afterWrite",er=[zr,Xr,Yr,Kr,qr,Gr,Qr,Jr,Zr];function R(e){return e?(e.nodeName||"").toLowerCase():null}function w(e){if(e==null)return window;if(e.toString()!=="[object Window]"){var t=e.ownerDocument;return t&&t.defaultView||window}return e}function F(e){var t=w(e).Element;return e instanceof t||e instanceof Element}function P(e){var t=w(e).HTMLElement;return e instanceof t||e instanceof HTMLElement}function It(e){if(typeof ShadowRoot>"u")return!1;var t=w(e).ShadowRoot;return e instanceof t||e instanceof ShadowRoot}function Mi(e){var t=e.state;Object.keys(t.elements).forEach(function(r){var o=t.styles[r]||{},i=t.attributes[r]||{},n=t.elements[r];!P(n)||!R(n)||(Object.assign(n.style,o),Object.keys(i).forEach(function(s){var a=i[s];a===!1?n.removeAttribute(s):n.setAttribute(s,a===!0?"":a)}))})}function Hi(e){var t=e.state,r={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,r.popper),t.styles=r,t.elements.arrow&&Object.assign(t.elements.arrow.style,r.arrow),function(){Object.keys(t.elements).forEach(function(o){var i=t.elements[o],n=t.attributes[o]||{},s=Object.keys(t.styles.hasOwnProperty(o)?t.styles[o]:r[o]),a=s.reduce(function(l,f){return l[f]="",l},{});!P(i)||!R(i)||(Object.assign(i.style,a),Object.keys(n).forEach(function(l){i.removeAttribute(l)}))})}}var Rt={name:"applyStyles",enabled:!0,phase:"write",fn:Mi,effect:Hi,requires:["computeStyles"]};function M(e){return e.split("-")[0]}var q=Math.max,gt=Math.min,tt=Math.round;function Mt(){var e=navigator.userAgentData;return e!=null&&e.brands&&Array.isArray(e.brands)?e.brands.map(function(t){return t.brand+"/"+t.version}).join(" "):navigator.userAgent}function qt(){return!/^((?!chrome|android).)*safari/i.test(Mt())}function U(e,t,r){t===void 0&&(t=!1),r===void 0&&(r=!1);var o=e.getBoundingClientRect(),i=1,n=1;t&&P(e)&&(i=e.offsetWidth>0&&tt(o.width)/e.offsetWidth||1,n=e.offsetHeight>0&&tt(o.height)/e.offsetHeight||1);var s=F(e)?w(e):window,a=s.visualViewport,l=!qt()&&r,f=(o.left+(l&&a?a.offsetLeft:0))/i,c=(o.top+(l&&a?a.offsetTop:0))/n,u=o.width/i,m=o.height/n;return{width:u,height:m,top:c,right:f+u,bottom:c+m,left:f,x:f,y:c}}function vt(e){var t=U(e),r=e.offsetWidth,o=e.offsetHeight;return Math.abs(t.width-r)<=1&&(r=t.width),Math.abs(t.height-o)<=1&&(o=t.height),{x:e.offsetLeft,y:e.offsetTop,width:r,height:o}}function Gt(e,t){var r=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(r&&It(r)){var o=t;do{if(o&&e.isSameNode(o))return!0;o=o.parentNode||o.host}while(o)}return!1}function j(e){return w(e).getComputedStyle(e)}function rr(e){return["table","td","th"].indexOf(R(e))>=0}function k(e){return((F(e)?e.ownerDocument:e.document)||window.document).documentElement}function et(e){return R(e)==="html"?e:e.assignedSlot||e.parentNode||(It(e)?e.host:null)||k(e)}function to(e){return!P(e)||j(e).position==="fixed"?null:e.offsetParent}function ki(e){var t=/firefox/i.test(Mt()),r=/Trident/i.test(Mt());if(r&&P(e)){var o=j(e);if(o.position==="fixed")return null}var i=et(e);for(It(i)&&(i=i.host);P(i)&&["html","body"].indexOf(R(i))<0;){var n=j(i);if(n.transform!=="none"||n.perspective!=="none"||n.contain==="paint"||["transform","perspective"].indexOf(n.willChange)!==-1||t&&n.willChange==="filter"||t&&n.filter&&n.filter!=="none")return i;i=i.parentNode}return null}function G(e){for(var t=w(e),r=to(e);r&&rr(r)&&j(r).position==="static";)r=to(r);return r&&(R(r)==="html"||R(r)==="body"&&j(r).position==="static")?t:r||ki(e)||t}function _t(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function Et(e,t,r){return q(e,gt(t,r))}function eo(e,t,r){var o=Et(e,t,r);return o>r?r:o}function Qt(){return{top:0,right:0,bottom:0,left:0}}function Jt(e){return Object.assign({},Qt(),e)}function Zt(e,t){return t.reduce(function(r,o){return r[o]=e,r},{})}var $i=function(t,r){return t=typeof t=="function"?t(Object.assign({},r.rects,{placement:r.placement})):t,Jt(typeof t!="number"?t:Zt(t,Z))};function Vi(e){var t,r=e.state,o=e.name,i=e.options,n=r.elements.arrow,s=r.modifiersData.popperOffsets,a=M(r.placement),l=_t(a),f=[A,S].indexOf(a)>=0,c=f?"height":"width";if(!(!n||!s)){var u=$i(i.padding,r),m=vt(n),p=l==="y"?x:A,b=l==="y"?D:S,d=r.rects.reference[c]+r.rects.reference[l]-s[l]-r.rects.popper[c],v=s[l]-r.rects.reference[l],y=G(n),O=y?l==="y"?y.clientHeight||0:y.clientWidth||0:0,C=d/2-v/2,g=u[p],_=O-m[c]-u[b],E=O/2-m[c]/2+C,T=Et(g,E,_),H=l;r.modifiersData[o]=(t={},t[H]=T,t.centerOffset=T-E,t)}}function ji(e){var t=e.state,r=e.options,o=r.element,i=o===void 0?"[data-popper-arrow]":o;i!=null&&(typeof i=="string"&&(i=t.elements.popper.querySelector(i),!i)||Gt(t.elements.popper,i)&&(t.elements.arrow=i))}var he={name:"arrow",enabled:!0,phase:"main",fn:Vi,effect:ji,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function z(e){return e.split("-")[1]}var Bi={top:"auto",right:"auto",bottom:"auto",left:"auto"};function Wi(e,t){var r=e.x,o=e.y,i=t.devicePixelRatio||1;return{x:tt(r*i)/i||0,y:tt(o*i)/i||0}}function ro(e){var t,r=e.popper,o=e.popperRect,i=e.placement,n=e.variation,s=e.offsets,a=e.position,l=e.gpuAcceleration,f=e.adaptive,c=e.roundOffsets,u=e.isFixed,m=s.x,p=m===void 0?0:m,b=s.y,d=b===void 0?0:b,v=typeof c=="function"?c({x:p,y:d}):{x:p,y:d};p=v.x,d=v.y;var y=s.hasOwnProperty("x"),O=s.hasOwnProperty("y"),C=A,g=x,_=window;if(f){var E=G(r),T="clientHeight",H="clientWidth";if(E===w(r)&&(E=k(r),j(E).position!=="static"&&a==="absolute"&&(T="scrollHeight",H="scrollWidth")),E=E,i===x||(i===A||i===S)&&n===at){g=D;var I=u&&E===_&&_.visualViewport?_.visualViewport.height:E[T];d-=I-o.height,d*=l?1:-1}if(i===A||(i===x||i===D)&&n===at){C=S;var L=u&&E===_&&_.visualViewport?_.visualViewport.width:E[H];p-=L-o.width,p*=l?1:-1}}var $=Object.assign({position:a},f&&Bi),X=c===!0?Wi({x:p,y:d},w(r)):{x:p,y:d};if(p=X.x,d=X.y,l){var V;return Object.assign({},$,(V={},V[g]=O?"0":"",V[C]=y?"0":"",V.transform=(_.devicePixelRatio||1)<=1?"translate("+p+"px, "+d+"px)":"translate3d("+p+"px, "+d+"px, 0)",V))}return Object.assign({},$,(t={},t[g]=O?d+"px":"",t[C]=y?p+"px":"",t.transform="",t))}function Fi(e){var t=e.state,r=e.options,o=r.gpuAcceleration,i=o===void 0?!0:o,n=r.adaptive,s=n===void 0?!0:n,a=r.roundOffsets,l=a===void 0?!0:a,f={placement:M(t.placement),variation:z(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:i,isFixed:t.options.strategy==="fixed"};t.modifiersData.popperOffsets!=null&&(t.styles.popper=Object.assign({},t.styles.popper,ro(Object.assign({},f,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:s,roundOffsets:l})))),t.modifiersData.arrow!=null&&(t.styles.arrow=Object.assign({},t.styles.arrow,ro(Object.assign({},f,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})}var Ht={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:Fi,data:{}};var ge={passive:!0};function Ui(e){var t=e.state,r=e.instance,o=e.options,i=o.scroll,n=i===void 0?!0:i,s=o.resize,a=s===void 0?!0:s,l=w(t.elements.popper),f=[].concat(t.scrollParents.reference,t.scrollParents.popper);return n&&f.forEach(function(c){c.addEventListener("scroll",r.update,ge)}),a&&l.addEventListener("resize",r.update,ge),function(){n&&f.forEach(function(c){c.removeEventListener("scroll",r.update,ge)}),a&&l.removeEventListener("resize",r.update,ge)}}var kt={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:Ui,data:{}};var zi={left:"right",right:"left",bottom:"top",top:"bottom"};function $t(e){return e.replace(/left|right|bottom|top/g,function(t){return zi[t]})}var Xi={start:"end",end:"start"};function ve(e){return e.replace(/start|end/g,function(t){return Xi[t]})}function bt(e){var t=w(e),r=t.pageXOffset,o=t.pageYOffset;return{scrollLeft:r,scrollTop:o}}function yt(e){return U(k(e)).left+bt(e).scrollLeft}function or(e,t){var r=w(e),o=k(e),i=r.visualViewport,n=o.clientWidth,s=o.clientHeight,a=0,l=0;if(i){n=i.width,s=i.height;var f=qt();(f||!f&&t==="fixed")&&(a=i.offsetLeft,l=i.offsetTop)}return{width:n,height:s,x:a+yt(e),y:l}}function ir(e){var t,r=k(e),o=bt(e),i=(t=e.ownerDocument)==null?void 0:t.body,n=q(r.scrollWidth,r.clientWidth,i?i.scrollWidth:0,i?i.clientWidth:0),s=q(r.scrollHeight,r.clientHeight,i?i.scrollHeight:0,i?i.clientHeight:0),a=-o.scrollLeft+yt(e),l=-o.scrollTop;return j(i||r).direction==="rtl"&&(a+=q(r.clientWidth,i?i.clientWidth:0)-n),{width:n,height:s,x:a,y:l}}function wt(e){var t=j(e),r=t.overflow,o=t.overflowX,i=t.overflowY;return/auto|scroll|overlay|hidden/.test(r+i+o)}function _e(e){return["html","body","#document"].indexOf(R(e))>=0?e.ownerDocument.body:P(e)&&wt(e)?e:_e(et(e))}function lt(e,t){var r;t===void 0&&(t=[]);var o=_e(e),i=o===((r=e.ownerDocument)==null?void 0:r.body),n=w(o),s=i?[n].concat(n.visualViewport||[],wt(o)?o:[]):o,a=t.concat(s);return i?a:a.concat(lt(et(s)))}function Vt(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function Yi(e,t){var r=U(e,!1,t==="fixed");return r.top=r.top+e.clientTop,r.left=r.left+e.clientLeft,r.bottom=r.top+e.clientHeight,r.right=r.left+e.clientWidth,r.width=e.clientWidth,r.height=e.clientHeight,r.x=r.left,r.y=r.top,r}function oo(e,t,r){return t===Yt?Vt(or(e,r)):F(t)?Yi(t,r):Vt(ir(k(e)))}function Ki(e){var t=lt(et(e)),r=["absolute","fixed"].indexOf(j(e).position)>=0,o=r&&P(e)?G(e):e;return F(o)?t.filter(function(i){return F(i)&&Gt(i,o)&&R(i)!=="body"}):[]}function nr(e,t,r,o){var i=t==="clippingParents"?Ki(e):[].concat(t),n=[].concat(i,[r]),s=n[0],a=n.reduce(function(l,f){var c=oo(e,f,o);return l.top=q(c.top,l.top),l.right=gt(c.right,l.right),l.bottom=gt(c.bottom,l.bottom),l.left=q(c.left,l.left),l},oo(e,s,o));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}function te(e){var t=e.reference,r=e.element,o=e.placement,i=o?M(o):null,n=o?z(o):null,s=t.x+t.width/2-r.width/2,a=t.y+t.height/2-r.height/2,l;switch(i){case x:l={x:s,y:t.y-r.height};break;case D:l={x:s,y:t.y+t.height};break;case S:l={x:t.x+t.width,y:a};break;case A:l={x:t.x-r.width,y:a};break;default:l={x:t.x,y:t.y}}var f=i?_t(i):null;if(f!=null){var c=f==="y"?"height":"width";switch(n){case Q:l[f]=l[f]-(t[c]/2-r[c]/2);break;case at:l[f]=l[f]+(t[c]/2-r[c]/2);break;default:}}return l}function B(e,t){t===void 0&&(t={});var r=t,o=r.placement,i=o===void 0?e.placement:o,n=r.strategy,s=n===void 0?e.strategy:n,a=r.boundary,l=a===void 0?Ze:a,f=r.rootBoundary,c=f===void 0?Yt:f,u=r.elementContext,m=u===void 0?ht:u,p=r.altBoundary,b=p===void 0?!1:p,d=r.padding,v=d===void 0?0:d,y=Jt(typeof v!="number"?v:Zt(v,Z)),O=m===ht?tr:ht,C=e.rects.popper,g=e.elements[b?O:m],_=nr(F(g)?g:g.contextElement||k(e.elements.popper),l,c,s),E=U(e.elements.reference),T=te({reference:E,element:C,strategy:"absolute",placement:i}),H=Vt(Object.assign({},C,T)),I=m===ht?H:E,L={top:_.top-I.top+y.top,bottom:I.bottom-_.bottom+y.bottom,left:_.left-I.left+y.left,right:I.right-_.right+y.right},$=e.modifiersData.offset;if(m===ht&&$){var X=$[i];Object.keys(L).forEach(function(V){var ct=[S,D].indexOf(V)>=0?1:-1,ft=[x,D].indexOf(V)>=0?"y":"x";L[V]+=X[ft]*ct})}return L}function sr(e,t){t===void 0&&(t={});var r=t,o=r.placement,i=r.boundary,n=r.rootBoundary,s=r.padding,a=r.flipVariations,l=r.allowedAutoPlacements,f=l===void 0?Kt:l,c=z(o),u=c?a?de:de.filter(function(b){return z(b)===c}):Z,m=u.filter(function(b){return f.indexOf(b)>=0});m.length===0&&(m=u);var p=m.reduce(function(b,d){return b[d]=B(e,{placement:d,boundary:i,rootBoundary:n,padding:s})[M(d)],b},{});return Object.keys(p).sort(function(b,d){return p[b]-p[d]})}function qi(e){if(M(e)===Xt)return[];var t=$t(e);return[ve(e),t,ve(t)]}function Gi(e){var t=e.state,r=e.options,o=e.name;if(!t.modifiersData[o]._skip){for(var i=r.mainAxis,n=i===void 0?!0:i,s=r.altAxis,a=s===void 0?!0:s,l=r.fallbackPlacements,f=r.padding,c=r.boundary,u=r.rootBoundary,m=r.altBoundary,p=r.flipVariations,b=p===void 0?!0:p,d=r.allowedAutoPlacements,v=t.options.placement,y=M(v),O=y===v,C=l||(O||!b?[$t(v)]:qi(v)),g=[v].concat(C).reduce(function(Tt,rt){return Tt.concat(M(rt)===Xt?sr(t,{placement:rt,boundary:c,rootBoundary:u,padding:f,flipVariations:b,allowedAutoPlacements:d}):rt)},[]),_=t.rects.reference,E=t.rects.popper,T=new Map,H=!0,I=g[0],L=0;L=0,ft=ct?"width":"height",W=B(t,{placement:$,boundary:c,rootBoundary:u,altBoundary:m,padding:f}),Y=ct?V?S:A:V?D:x;_[ft]>E[ft]&&(Y=$t(Y));var re=$t(Y),pt=[];if(n&&pt.push(W[X]<=0),a&&pt.push(W[Y]<=0,W[re]<=0),pt.every(function(Tt){return Tt})){I=$,H=!1;break}T.set($,pt)}if(H)for(var oe=b?3:1,Ne=function(rt){var Wt=g.find(function(ne){var ut=T.get(ne);if(ut)return ut.slice(0,rt).every(function(De){return De})});if(Wt)return I=Wt,"break"},Bt=oe;Bt>0;Bt--){var ie=Ne(Bt);if(ie==="break")break}t.placement!==I&&(t.modifiersData[o]._skip=!0,t.placement=I,t.reset=!0)}}var Ee={name:"flip",enabled:!0,phase:"main",fn:Gi,requiresIfExists:["offset"],data:{_skip:!1}};function io(e,t,r){return r===void 0&&(r={x:0,y:0}),{top:e.top-t.height-r.y,right:e.right-t.width+r.x,bottom:e.bottom-t.height+r.y,left:e.left-t.width-r.x}}function no(e){return[x,S,D,A].some(function(t){return e[t]>=0})}function Qi(e){var t=e.state,r=e.name,o=t.rects.reference,i=t.rects.popper,n=t.modifiersData.preventOverflow,s=B(t,{elementContext:"reference"}),a=B(t,{altBoundary:!0}),l=io(s,o),f=io(a,i,n),c=no(l),u=no(f);t.modifiersData[r]={referenceClippingOffsets:l,popperEscapeOffsets:f,isReferenceHidden:c,hasPopperEscaped:u},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":c,"data-popper-escaped":u})}var be={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:Qi};function Ji(e,t,r){var o=M(e),i=[A,x].indexOf(o)>=0?-1:1,n=typeof r=="function"?r(Object.assign({},t,{placement:e})):r,s=n[0],a=n[1];return s=s||0,a=(a||0)*i,[A,S].indexOf(o)>=0?{x:a,y:s}:{x:s,y:a}}function Zi(e){var t=e.state,r=e.options,o=e.name,i=r.offset,n=i===void 0?[0,0]:i,s=Kt.reduce(function(c,u){return c[u]=Ji(u,t.rects,n),c},{}),a=s[t.placement],l=a.x,f=a.y;t.modifiersData.popperOffsets!=null&&(t.modifiersData.popperOffsets.x+=l,t.modifiersData.popperOffsets.y+=f),t.modifiersData[o]=s}var ye={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:Zi};function tn(e){var t=e.state,r=e.name;t.modifiersData[r]=te({reference:t.rects.reference,element:t.rects.popper,strategy:"absolute",placement:t.placement})}var jt={name:"popperOffsets",enabled:!0,phase:"read",fn:tn,data:{}};function ar(e){return e==="x"?"y":"x"}function en(e){var t=e.state,r=e.options,o=e.name,i=r.mainAxis,n=i===void 0?!0:i,s=r.altAxis,a=s===void 0?!1:s,l=r.boundary,f=r.rootBoundary,c=r.altBoundary,u=r.padding,m=r.tether,p=m===void 0?!0:m,b=r.tetherOffset,d=b===void 0?0:b,v=B(t,{boundary:l,rootBoundary:f,padding:u,altBoundary:c}),y=M(t.placement),O=z(t.placement),C=!O,g=_t(y),_=ar(g),E=t.modifiersData.popperOffsets,T=t.rects.reference,H=t.rects.popper,I=typeof d=="function"?d(Object.assign({},t.rects,{placement:t.placement})):d,L=typeof I=="number"?{mainAxis:I,altAxis:I}:Object.assign({mainAxis:0,altAxis:0},I),$=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,X={x:0,y:0};if(E){if(n){var V,ct=g==="y"?x:A,ft=g==="y"?D:S,W=g==="y"?"height":"width",Y=E[g],re=Y+v[ct],pt=Y-v[ft],oe=p?-H[W]/2:0,Ne=O===Q?T[W]:H[W],Bt=O===Q?-H[W]:-T[W],ie=t.elements.arrow,Tt=p&&ie?vt(ie):{width:0,height:0},rt=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:Qt(),Wt=rt[ct],ne=rt[ft],ut=Et(0,T[W],Tt[W]),De=C?T[W]/2-oe-ut-Wt-L.mainAxis:Ne-ut-Wt-L.mainAxis,ho=C?-T[W]/2+oe+ut+ne+L.mainAxis:Bt+ut+ne+L.mainAxis,Le=t.elements.arrow&&G(t.elements.arrow),go=Le?g==="y"?Le.clientTop||0:Le.clientLeft||0:0,_r=(V=$?.[g])!=null?V:0,vo=Y+De-_r-go,_o=Y+ho-_r,Er=Et(p?gt(re,vo):re,Y,p?q(pt,_o):pt);E[g]=Er,X[g]=Er-Y}if(a){var br,Eo=g==="x"?x:A,bo=g==="x"?D:S,mt=E[_],se=_==="y"?"height":"width",yr=mt+v[Eo],wr=mt-v[bo],Pe=[x,A].indexOf(y)!==-1,xr=(br=$?.[_])!=null?br:0,Tr=Pe?yr:mt-T[se]-H[se]-xr+L.altAxis,Ar=Pe?mt+T[se]+H[se]-xr-L.altAxis:wr,Or=p&&Pe?eo(Tr,mt,Ar):Et(p?Tr:yr,mt,p?Ar:wr);E[_]=Or,X[_]=Or-mt}t.modifiersData[o]=X}}var we={name:"preventOverflow",enabled:!0,phase:"main",fn:en,requiresIfExists:["offset"]};function lr(e){return{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}}function cr(e){return e===w(e)||!P(e)?bt(e):lr(e)}function rn(e){var t=e.getBoundingClientRect(),r=tt(t.width)/e.offsetWidth||1,o=tt(t.height)/e.offsetHeight||1;return r!==1||o!==1}function fr(e,t,r){r===void 0&&(r=!1);var o=P(t),i=P(t)&&rn(t),n=k(t),s=U(e,i,r),a={scrollLeft:0,scrollTop:0},l={x:0,y:0};return(o||!o&&!r)&&((R(t)!=="body"||wt(n))&&(a=cr(t)),P(t)?(l=U(t,!0),l.x+=t.clientLeft,l.y+=t.clientTop):n&&(l.x=yt(n))),{x:s.left+a.scrollLeft-l.x,y:s.top+a.scrollTop-l.y,width:s.width,height:s.height}}function on(e){var t=new Map,r=new Set,o=[];e.forEach(function(n){t.set(n.name,n)});function i(n){r.add(n.name);var s=[].concat(n.requires||[],n.requiresIfExists||[]);s.forEach(function(a){if(!r.has(a)){var l=t.get(a);l&&i(l)}}),o.push(n)}return e.forEach(function(n){r.has(n.name)||i(n)}),o}function pr(e){var t=on(e);return er.reduce(function(r,o){return r.concat(t.filter(function(i){return i.phase===o}))},[])}function ur(e){var t;return function(){return t||(t=new Promise(function(r){Promise.resolve().then(function(){t=void 0,r(e())})})),t}}function mr(e){var t=e.reduce(function(r,o){var i=r[o.name];return r[o.name]=i?Object.assign({},i,o,{options:Object.assign({},i.options,o.options),data:Object.assign({},i.data,o.data)}):o,r},{});return Object.keys(t).map(function(r){return t[r]})}var so={placement:"bottom",modifiers:[],strategy:"absolute"};function ao(){for(var e=arguments.length,t=new Array(e),r=0;r{let r=e.nodeName.toLowerCase();return t.includes(r)?ln.has(r)?!!cn.test(e.nodeValue):!0:t.filter(o=>o instanceof RegExp).some(o=>o.test(r))};function fo(e,t,r){if(!e.length)return e;if(r&&typeof r=="function")return r(e);let i=new window.DOMParser().parseFromString(e,"text/html"),n=[].concat(...i.body.querySelectorAll("*"));for(let s of n){let a=s.nodeName.toLowerCase();if(!Object.keys(t).includes(a)){s.remove();continue}let l=[].concat(...s.attributes),f=[].concat(t["*"]||[],t[a]||[]);for(let c of l)fn(c,f)||s.removeAttribute(c.nodeName)}return i.body.innerHTML}var pn="TemplateFactory",un={allowList:Te,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
    "},mn={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},dn={entry:"(string|element|function|null)",selector:"(string|element)"},hr=class extends Ot{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return un}static get DefaultType(){return mn}static get NAME(){return pn}getContent(){return Object.values(this._config.content).map(t=>this._resolvePossibleFunction(t)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){let t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(let[i,n]of Object.entries(this._config.content))this._setContent(t,n,i);let r=t.children[0],o=this._resolvePossibleFunction(this._config.extraClass);return o&&r.classList.add(...o.split(" ")),r}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(let[r,o]of Object.entries(t))super._typeCheckConfig({selector:r,entry:o},dn)}_setContent(t,r,o){let i=N.findOne(o,t);if(i){if(r=this._resolvePossibleFunction(r),!r){i.remove();return}if(it(r)){this._putElementInTemplate(J(r),i);return}if(this._config.html){i.innerHTML=this._maybeSanitize(r);return}i.textContent=r}}_maybeSanitize(t){return this._config.sanitize?fo(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return K(t,[this])}_putElementInTemplate(t,r){if(this._config.html){r.innerHTML="",r.append(t);return}r.textContent=t.textContent}},po=hr;var hn="tooltip",gn=new Set(["sanitize","allowList","sanitizeFn"]),gr="fade",vn="modal",Ae="show",_n=".tooltip-inner",uo=`.${vn}`,mo="hide.bs.modal",ee="hover",vr="focus",En="click",bn="manual",yn="hide",wn="hidden",xn="show",Tn="shown",An="inserted",On="click",Cn="focusin",Sn="focusout",Nn="mouseenter",Dn="mouseleave",Ln={AUTO:"auto",TOP:"top",RIGHT:At()?"left":"right",BOTTOM:"bottom",LEFT:At()?"right":"left"},Pn={allowList:Te,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},In={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"},Oe=class e extends Ct{constructor(t,r){if(typeof dr>"u")throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,r),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Pn}static get DefaultType(){return In}static get NAME(){return hn}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){if(this._isEnabled){if(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()){this._leave();return}this._enter()}}dispose(){clearTimeout(this._timeout),h.off(this._element.closest(uo),mo,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if(this._element.style.display==="none")throw new Error("Please use show on visible elements");if(!(this._isWithContent()&&this._isEnabled))return;let t=h.trigger(this._element,this.constructor.eventName(xn)),o=(ke(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!o)return;this._disposePopper();let i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));let{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),h.trigger(this._element,this.constructor.eventName(An))),this._popper=this._createPopper(i),i.classList.add(Ae),"ontouchstart"in document.documentElement)for(let a of[].concat(...document.body.children))h.on(a,"mouseover",$e);let s=()=>{h.trigger(this._element,this.constructor.eventName(Tn)),this._isHovered===!1&&this._leave(),this._isHovered=!1};this._queueCallback(s,this.tip,this._isAnimated())}hide(){if(!this._isShown()||h.trigger(this._element,this.constructor.eventName(yn)).defaultPrevented)return;if(this._getTipElement().classList.remove(Ae),"ontouchstart"in document.documentElement)for(let i of[].concat(...document.body.children))h.off(i,"mouseover",$e);this._activeTrigger[En]=!1,this._activeTrigger[vr]=!1,this._activeTrigger[ee]=!1,this._isHovered=null;let o=()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),h.trigger(this._element,this.constructor.eventName(wn)))};this._queueCallback(o,this.tip,this._isAnimated())}update(){this._popper&&this._popper.update()}_isWithContent(){return!!this._getTitle()}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){let r=this._getTemplateFactory(t).toHtml();if(!r)return null;r.classList.remove(gr,Ae),r.classList.add(`bs-${this.constructor.NAME}-auto`);let o=Sr(this.constructor.NAME).toString();return r.setAttribute("id",o),this._isAnimated()&&r.classList.add(gr),r}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new po({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[_n]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(gr)}_isShown(){return this.tip&&this.tip.classList.contains(Ae)}_createPopper(t){let r=K(this._config.placement,[this,t,this._element]),o=Ln[r.toUpperCase()];return xe(this._element,t,this._getPopperConfig(o))}_getOffset(){let{offset:t}=this._config;return typeof t=="string"?t.split(",").map(r=>Number.parseInt(r,10)):typeof t=="function"?r=>t(r,this._element):t}_resolvePossibleFunction(t){return K(t,[this._element])}_getPopperConfig(t){let r={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:o=>{this._getTipElement().setAttribute("data-popper-placement",o.state.placement)}}]};return{...r,...K(this._config.popperConfig,[r])}}_setListeners(){let t=this._config.trigger.split(" ");for(let r of t)if(r==="click")h.on(this._element,this.constructor.eventName(On),this._config.selector,o=>{this._initializeOnDelegatedTarget(o).toggle()});else if(r!==bn){let o=r===ee?this.constructor.eventName(Nn):this.constructor.eventName(Cn),i=r===ee?this.constructor.eventName(Dn):this.constructor.eventName(Sn);h.on(this._element,o,this._config.selector,n=>{let s=this._initializeOnDelegatedTarget(n);s._activeTrigger[n.type==="focusin"?vr:ee]=!0,s._enter()}),h.on(this._element,i,this._config.selector,n=>{let s=this._initializeOnDelegatedTarget(n);s._activeTrigger[n.type==="focusout"?vr:ee]=s._element.contains(n.relatedTarget),s._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},h.on(this._element.closest(uo),mo,this._hideModalHandler)}_fixTitle(){let t=this._element.getAttribute("title");t&&(!this._element.getAttribute("aria-label")&&!this._element.textContent.trim()&&this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){if(this._isShown()||this._isHovered){this._isHovered=!0;return}this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show)}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide))}_setTimeout(t,r){clearTimeout(this._timeout),this._timeout=setTimeout(t,r)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){let r=dt.getDataAttributes(this._element);for(let o of Object.keys(r))gn.has(o)&&delete r[o];return t={...r,...typeof t=="object"&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=t.container===!1?document.body:J(t.container),typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),typeof t.title=="number"&&(t.title=t.title.toString()),typeof t.content=="number"&&(t.content=t.content.toString()),t}_getDelegateConfig(){let t={};for(let[r,o]of Object.entries(this._config))this.constructor.Default[r]!==o&&(t[r]=o);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each(function(){let r=e.getOrCreateInstance(this,t);if(typeof t=="string"){if(typeof r[t]>"u")throw new TypeError(`No method named "${t}"`);r[t]()}})}};nt(Oe);var Ce=Oe;var Rn="popover",Mn=".popover-header",Hn=".popover-body",kn={...Ce.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},$n={...Ce.DefaultType,content:"(null|string|element|function)"},Se=class e extends Ce{static get Default(){return kn}static get DefaultType(){return $n}static get NAME(){return Rn}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[Mn]:this._getTitle(),[Hn]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each(function(){let r=e.getOrCreateInstance(this,t);if(typeof t=="string"){if(typeof r[t]>"u")throw new TypeError(`No method named "${t}"`);r[t]()}})}};nt(Se);var Vn=Se;import"@typo3/backend/tab.js";export{vi as Carousel,Ri as Collapse,Vn as Popover}; diff --git a/Resources/Public/JavaScript/Contrib/crelt.js b/Resources/Public/JavaScript/Contrib/crelt.js new file mode 100644 index 0000000..7c3f289 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/crelt.js @@ -0,0 +1 @@ +function s(){var r=arguments[0];typeof r=="string"&&(r=document.createElement(r));var e=1,t=arguments[1];if(t&&typeof t=="object"&&t.nodeType==null&&!Array.isArray(t)){for(var n in t)if(Object.prototype.hasOwnProperty.call(t,n)){var o=t[n];typeof o=="string"?r.setAttribute(n,o):o!=null&&(r[n]=o)}e++}for(;e0){if(++t>=ix)return arguments[0]}else t=0;return r.apply(void 0,arguments)}}var ko=px;var ux=ko(Wo),qo=ux;var sx=/\{\n\/\* \[wrapped with (.+)\] \*/,lx=/,? & /;function dx(r){var t=r.match(sx);return t?t[1].split(lx):[]}var $p=dx;var cx=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/;function xx(r,t){var e=t.length;if(!e)return r;var o=e-1;return t[o]=(e>1?"& ":"")+t[o],t=t.join(e>2?", ":" "),r.replace(cx,`{ +/* [wrapped with `+t+`] */ +`)}var Zp=xx;function hx(r){return function(){return r}}var Ft=hx;var gx=function(){try{var r=Er(Object,"defineProperty");return r({},"",{}),r}catch{}}(),fe=gx;var bx=fe?function(r,t){return fe(r,"toString",{configurable:!0,enumerable:!1,value:Ft(t),writable:!0})}:D,Vp=bx;var yx=ko(Vp),ae=yx;function vx(r,t){for(var e=-1,o=r==null?0:r.length;++e-1}var xt=Ox;var Sx=1,Tx=2,wx=8,Lx=16,Ex=32,Px=64,Cx=128,Mx=256,Fx=512,Wx=[["ary",Cx],["bind",Sx],["bindKey",Tx],["curry",wx],["curryRight",Lx],["flip",Fx],["partial",Ex],["partialRight",Px],["rearg",Mx]];function Bx(r,t){return fr(Wx,function(e){var o="_."+e[0];t&e[1]&&!xt(r,o)&&r.push(o)}),r.sort()}var Qp=Bx;function Nx(r,t,e){var o=t+"";return ae(r,Zp(o,Qp($p(o),e)))}var Ho=Nx;var Dx=1,jx=2,Ux=4,Gx=8,ru=32,tu=64;function kx(r,t,e,o,f,a,n,m,p,u){var s=t&Gx,l=s?n:void 0,g=s?void 0:n,y=s?a:void 0,I=s?void 0:a;t|=s?ru:tu,t&=~(s?tu:ru),t&Ux||(t&=~(Dx|jx));var L=[r,t,f,y,l,I,g,m,p,u],A=e.apply(void 0,L);return Xe(r)&&qo(A,L),A.placeholder=o,Ho(A,r,t)}var Ko=kx;function qx(r){var t=r;return t.placeholder}var Gr=qx;var zx=9007199254740991,Hx=/^(?:0|[1-9]\d*)$/;function Kx(r,t){var e=typeof r;return t=t??zx,!!t&&(e=="number"||e!="symbol"&&Hx.test(r))&&r>-1&&r%1==0&&r1&&F.reverse(),s&&p-1&&r%1==0&&r<=Ah}var it=Ih;function Rh(r){return r!=null&&it(r.length)&&!pr(r)}var G=Rh;function Oh(r,t,e){if(!S(e))return!1;var o=typeof t;return(o=="number"?G(e)&&lr(t,e.length):o=="string"&&t in e)?Q(e[t],r):!1}var k=Oh;function Sh(r){return h(function(t,e){var o=-1,f=e.length,a=f>1?e[f-1]:void 0,n=f>2?e[2]:void 0;for(a=r.length>3&&typeof a=="function"?(f--,a):void 0,n&&k(e[0],e[1],n)&&(a=f<3?void 0:a,f=1),t=Object(t);++o-1}var Mu=tb;function eb(r,t){var e=this.__data__,o=ht(e,r);return o<0?(++this.size,e.push([r,t])):e[o][1]=t,this}var Fu=eb;function le(r){var t=-1,e=r==null?0:r.length;for(this.clear();++t0&&e(m)?t>1?zu(m,t-1,e,o,f):Ar(f,m):o||(f[f.length]=m)}return f}var B=zu;function Sb(r){var t=r==null?0:r.length;return t?B(r,1):[]}var Qe=Sb;function Tb(r){return ae(Yo(r,void 0,Qe),r+"")}var xr=Tb;var wb=xr(ce),ua=wb;var Lb=$o(Object.getPrototypeOf,Object),vt=Lb;var Eb="[object Object]",Pb=Function.prototype,Cb=Object.prototype,Hu=Pb.toString,Mb=Cb.hasOwnProperty,Fb=Hu.call(Object);function Wb(r){if(!T(r)||U(r)!=Eb)return!1;var t=vt(r);if(t===null)return!0;var e=Mb.call(t,"constructor")&&t.constructor;return typeof e=="function"&&e instanceof e&&Hu.call(e)==Fb}var Xr=Wb;var Bb="[object DOMException]",Nb="[object Error]";function Db(r){if(!T(r))return!1;var t=U(r);return t==Nb||t==Bb||typeof r.message=="string"&&typeof r.name=="string"&&!Xr(r)}var Ut=Db;var jb=h(function(r,t){try{return X(r,void 0,t)}catch(e){return Ut(e)?e:new Error(e)}}),ro=jb;var Ub="Expected a function";function Gb(r,t){var e;if(typeof t!="function")throw new TypeError(Ub);return r=x(r),function(){return--r>0&&(e=t.apply(this,arguments)),r<=1&&(t=void 0),e}}var to=Gb;var kb=1,qb=32,sa=h(function(r,t,e){var o=kb;if(e.length){var f=wr(e,Gr(sa));o|=qb}return dr(r,o,t,e,f)});sa.placeholder={};var eo=sa;var zb=xr(function(r,t){return fr(t,function(e){e=ar(e),ir(r,e,eo(r[e],r))}),r}),la=zb;var Hb=1,Kb=2,Yb=32,da=h(function(r,t,e){var o=Hb|Kb;if(e.length){var f=wr(e,Gr(da));o|=Yb}return dr(t,o,r,e,f)});da.placeholder={};var ca=da;function Xb(r,t,e){var o=-1,f=r.length;t<0&&(t=-t>f?0:f+t),e=e>f?f:e,e<0&&(e+=f),f=t>e?0:e-t>>>0,t>>>=0;for(var a=Array(f);++o=o?r:rr(r,t,e)}var Ir=$b;var Zb="\\ud800-\\udfff",Vb="\\u0300-\\u036f",Jb="\\ufe20-\\ufe2f",Qb="\\u20d0-\\u20ff",ry=Vb+Jb+Qb,ty="\\ufe0e\\ufe0f",ey="\\u200d",oy=RegExp("["+ey+Zb+ry+ty+"]");function fy(r){return oy.test(r)}var qr=fy;function ay(r){return r.split("")}var Ku=ay;var Yu="\\ud800-\\udfff",iy="\\u0300-\\u036f",ny="\\ufe20-\\ufe2f",my="\\u20d0-\\u20ff",py=iy+ny+my,uy="\\ufe0e\\ufe0f",sy="["+Yu+"]",xa="["+py+"]",ha="\\ud83c[\\udffb-\\udfff]",ly="(?:"+xa+"|"+ha+")",Xu="[^"+Yu+"]",$u="(?:\\ud83c[\\udde6-\\uddff]){2}",Zu="[\\ud800-\\udbff][\\udc00-\\udfff]",dy="\\u200d",Vu=ly+"?",Ju="["+uy+"]?",cy="(?:"+dy+"(?:"+[Xu,$u,Zu].join("|")+")"+Ju+Vu+")*",xy=Ju+Vu+cy,hy="(?:"+[Xu+xa+"?",xa,$u,Zu,sy].join("|")+")",gy=RegExp(ha+"(?="+ha+")|"+hy+xy,"g");function by(r){return r.match(gy)||[]}var Qu=by;function yy(r){return qr(r)?Qu(r):Ku(r)}var ur=yy;function vy(r){return function(t){t=v(t);var e=qr(t)?ur(t):void 0,o=e?e[0]:t.charAt(0),f=e?Ir(e,1).join(""):t.slice(1);return o[r]()+f}}var Vo=vy;var _y=Vo("toUpperCase"),Gt=_y;function Ay(r){return Gt(v(r).toLowerCase())}var oo=Ay;function Iy(r,t,e,o){var f=-1,a=r==null?0:r.length;for(o&&a&&(e=r[++f]);++f=t?r:t)),r}var Mr=Av;function Iv(r,t,e){return e===void 0&&(e=t,t=void 0),e!==void 0&&(e=or(e),e=e===e?e:0),t!==void 0&&(t=or(t),t=t===t?t:0),Mr(or(r),t,e)}var _a=Iv;function Rv(){this.__data__=new gt,this.size=0}var _s=Rv;function Ov(r){var t=this.__data__,e=t.delete(r);return this.size=t.size,e}var As=Ov;function Sv(r){return this.__data__.get(r)}var Is=Sv;function Tv(r){return this.__data__.has(r)}var Rs=Tv;var wv=200;function Lv(r,t){var e=this.__data__;if(e instanceof gt){var o=e.__data__;if(!bt||o.lengthm))return!1;var u=a.get(r),s=a.get(t);if(u&&s)return u==t&&s==r;var l=-1,g=!0,y=e&LA?new At:void 0;for(a.set(r,t),a.set(t,r);++l=t||Yr<0||l&&ot>=a}function P(){var er=Ht();if(A(er))return F(er);m=setTimeout(P,L(er))}function F(er){return m=void 0,g&&o?y(er):(o=f=void 0,n)}function Fr(){m!==void 0&&clearTimeout(m),u=0,o=p=f=m=void 0}function Sr(){return m===void 0?n:F(Ht())}function Wr(){var er=Ht(),Yr=A(er);if(o=arguments,f=this,p=er,Yr){if(m===void 0)return I(p);if(l)return clearTimeout(m),m=setTimeout(P,t),y(p)}return m===void 0&&(m=setTimeout(P,t)),n}return Wr.cancel=Fr,Wr.flush=Sr,Wr}var so=k0;function q0(r,t){return r==null||r!==r?t:r}var Da=q0;var cl=Object.prototype,z0=cl.hasOwnProperty,H0=h(function(r,t){r=Object(r);var e=-1,o=t.length,f=o>2?t[2]:void 0;for(f&&k(t[0],t[1],f)&&(o=1);++e=fI&&(a=mt,n=!1,t=new At(t));r:for(;++f=0&&r.slice(e,f)==t}var Va=II;function RI(r,t){return O(t,function(e){return[e,r[e]]})}var vl=RI;function OI(r){var t=-1,e=Array(r.size);return r.forEach(function(o){e[++t]=[o,o]}),e}var _l=OI;var SI="[object Map]",TI="[object Set]";function wI(r){return function(t){var e=sr(t);return e==SI?Oe(t):e==TI?_l(t):vl(t,r(t))}}var If=wI;var LI=If(E),Xt=LI;var EI=If(q),$t=EI;var PI={"&":"&","<":"<",">":">",'"':""","'":"'"},CI=he(PI),Al=CI;var Il=/[&<>"']/g,MI=RegExp(Il.source);function FI(r){return r=v(r),r&&MI.test(r)?r.replace(Il,Al):r}var go=FI;var Rl=/[\\^$.*+?()[\]{}|]/g,WI=RegExp(Rl.source);function BI(r){return r=v(r),r&&WI.test(r)?r.replace(Rl,"\\$&"):r}var Ja=BI;function NI(r,t){for(var e=-1,o=r==null?0:r.length;++ef?0:f+e),o=o===void 0||o>f?f:x(o),o<0&&(o+=f),o=e>o?0:bo(o);e-1?f[a?t[n]:n]:void 0}}var Sf=KI;var YI=Math.max;function XI(r,t,e){var o=r==null?0:r.length;if(!o)return-1;var f=e==null?0:x(e);return f<0&&(f=YI(o+f,0)),ct(r,d(t,3),f)}var yo=XI;var $I=Sf(yo),ei=$I;function ZI(r,t,e){var o;return e(r,function(f,a,n){if(t(f,a,n))return o=a,!1}),o}var Tf=ZI;function VI(r,t){return Tf(r,d(t,3),gr)}var oi=VI;var JI=Math.max,QI=Math.min;function r1(r,t,e){var o=r==null?0:r.length;if(!o)return-1;var f=o-1;return e!==void 0&&(f=x(e),f=e<0?JI(o+f,0):QI(f,o-1)),ct(r,d(t,3),f,!0)}var vo=r1;var t1=Sf(vo),fi=t1;function e1(r,t){return Tf(r,d(t,3),Ce)}var ai=e1;function o1(r){return r&&r.length?r[0]:void 0}var Zt=o1;function f1(r,t){var e=-1,o=G(r)?Array(r.length):[];return Or(r,function(f,a,n){o[++e]=t(f,a,n)}),o}var wf=f1;function a1(r,t){var e=c(r)?O:wf;return e(r,d(t,3))}var pt=a1;function i1(r,t){return B(pt(r,t),1)}var ii=i1;var n1=1/0;function m1(r,t){return B(pt(r,t),n1)}var ni=m1;function p1(r,t,e){return e=e===void 0?1:x(e),B(pt(r,t),e)}var mi=p1;var u1=1/0;function s1(r){var t=r==null?0:r.length;return t?B(r,u1):[]}var pi=s1;function l1(r,t){var e=r==null?0:r.length;return e?(t=t===void 0?1:x(t),B(r,t)):[]}var ui=l1;var d1=512;function c1(r){return dr(r,d1)}var si=c1;var x1=be("floor"),li=x1;var h1="Expected a function",g1=8,b1=32,y1=128,v1=256;function _1(r){return xr(function(t){var e=t.length,o=e,f=yr.prototype.thru;for(r&&t.reverse();o--;){var a=t[o];if(typeof a!="function")throw new TypeError(h1);if(f&&!n&&oe(a)=="wrapper")var n=new yr([],!0)}for(o=n?o:e;++ot}var Me=W1;function B1(r){return function(t,e){return typeof t=="string"&&typeof e=="string"||(t=or(t),e=or(e)),r(t,e)}}var wt=B1;var N1=wt(Me),Ii=N1;var D1=wt(function(r,t){return r>=t}),Ri=D1;var j1=Object.prototype,U1=j1.hasOwnProperty;function G1(r,t){return r!=null&&U1.call(r,t)}var Tl=G1;function k1(r,t){return r!=null&&xf(r,t,Tl)}var Oi=k1;var q1=Math.max,z1=Math.min;function H1(r,t,e){return r>=z1(t,e)&&r-1:!!f&&Ur(r,t,e)>-1}var Ti=J1;var Q1=Math.max;function rR(r,t,e){var o=r==null?0:r.length;if(!o)return-1;var f=e==null?0:x(e);return f<0&&(f=Q1(o+f,0)),Ur(r,t,f)}var wi=rR;function tR(r){var t=r==null?0:r.length;return t?rr(r,0,-1):[]}var Li=tR;var eR=Math.min;function oR(r,t,e){for(var o=e?Pe:xt,f=r[0].length,a=r.length,n=a,m=Array(a),p=1/0,u=[];n--;){var s=r[n];n&&t&&(s=O(s,Z(t))),p=eR(s.length,p),m[n]=!e&&(t||f>=120&&s.length>=120)?new At(n&&s):void 0}s=r[0];var l=-1,g=m[0];r:for(;++l=-Dl&&r<=Dl}var Vi=JR;function QR(r){return r===void 0}var Ji=QR;var rO="[object WeakMap]";function tO(r){return T(r)&&sr(r)==rO}var Qi=tO;var eO="[object WeakSet]";function oO(r){return T(r)&&U(r)==eO}var rn=oO;var fO=1;function aO(r){return d(typeof r=="function"?r:hr(r,fO))}var tn=aO;var iO=Array.prototype,nO=iO.join;function mO(r,t){return r==null?"":nO.call(r,t)}var en=mO;var pO=zr(function(r,t,e){return r+(e?"-":"")+t.toLowerCase()}),on=pO;var uO=Ot(function(r,t,e){ir(r,e,t)}),fn=uO;function sO(r,t,e){for(var o=e+1;o--;)if(r[o]===t)return o;return o}var jl=sO;var lO=Math.max,dO=Math.min;function cO(r,t,e){var o=r==null?0:r.length;if(!o)return-1;var f=o;return e!==void 0&&(f=x(e),f=f<0?lO(o+f,0):dO(f,o-1)),t===t?jl(r,t,f):ct(r,zo,f,!0)}var an=cO;var xO=zr(function(r,t,e){return r+(e?" ":"")+t.toLowerCase()}),nn=xO;var hO=Vo("toLowerCase"),mn=hO;function gO(r,t){return r=this.__values__.length,t=r?void 0:this.__values__[this.__index__++];return{done:r,value:t}}var Mf=YO;function XO(r,t){var e=r.length;if(e)return t+=t<0?e:0,lr(t,e)?r[t]:void 0}var Ff=XO;function $O(r,t){return r&&r.length?Ff(r,x(t)):void 0}var Sn=$O;function ZO(r){return r=x(r),h(function(t){return Ff(t,r)})}var Tn=ZO;var VO=Object.prototype,JO=VO.hasOwnProperty;function QO(r,t){t=_r(t,r);var e=-1,o=t.length;if(!o)return!0;for(;++e1),a}),nr(r,_e(r),e),o&&(e=hr(e,tS|eS|oS,Gl));for(var f=t.length;f--;)je(e,t[f]);return e}),wn=fS;function aS(r,t,e,o){if(!S(r))return r;t=_r(t,r);for(var f=-1,a=t.length,n=a-1,m=r;m!=null&&++ft||a&&n&&p&&!m&&!u||o&&n&&p||!e&&p||!f)return 1;if(!o&&!a&&!u&&r=m)return p;var u=e[o];return p*(u=="desc"?-1:1)}}return r.index-t.index}var ql=lS;function dS(r,t,e){t.length?t=O(t,function(a){return c(a)?function(n){return Cr(n,a.length===1?a[0]:a)}:a}):t=[D];var o=-1;t=O(t,Z(d));var f=wf(r,function(a,n,m){var p=O(t,function(u){return u(a)});return{criteria:p,index:++o,value:a}});return kl(f,function(a,n){return ql(a,n,e)})}var Nf=dS;function cS(r,t,e,o){return r==null?[]:(c(t)||(t=t==null?[]:[t]),e=o?void 0:e,c(e)||(e=e==null?[]:[e]),Nf(r,t,e))}var Pn=cS;function xS(r){return xr(function(t){return t=O(t,Z(d)),h(function(e){var o=this;return r(t,function(f){return X(f,o,e)})})})}var Ue=xS;var hS=Ue(O),Cn=hS;var gS=h,zl=gS;var bS=Math.min,yS=zl(function(r,t){t=t.length==1&&c(t[0])?O(t[0],Z(d)):O(B(t,1),Z(d));var e=t.length;return h(function(o){for(var f=-1,a=bS(o.length,e);++fAS)return e;do t%2&&(e+=r),t=IS(t/2),t&&(r+=r);while(t);return e}var So=RS;var OS=we("length"),Hl=OS;var Yl="\\ud800-\\udfff",SS="\\u0300-\\u036f",TS="\\ufe20-\\ufe2f",wS="\\u20d0-\\u20ff",LS=SS+TS+wS,ES="\\ufe0e\\ufe0f",PS="["+Yl+"]",Bn="["+LS+"]",Nn="\\ud83c[\\udffb-\\udfff]",CS="(?:"+Bn+"|"+Nn+")",Xl="[^"+Yl+"]",$l="(?:\\ud83c[\\udde6-\\uddff]){2}",Zl="[\\ud800-\\udbff][\\udc00-\\udfff]",MS="\\u200d",Vl=CS+"?",Jl="["+ES+"]?",FS="(?:"+MS+"(?:"+[Xl,$l,Zl].join("|")+")"+Jl+Vl+")*",WS=Jl+Vl+FS,BS="(?:"+[Xl+Bn+"?",Bn,$l,Zl,PS].join("|")+")",Kl=RegExp(Nn+"(?="+Nn+")|"+BS+WS,"g");function NS(r){for(var t=Kl.lastIndex=0;Kl.test(r);)++t;return t}var Ql=NS;function DS(r){return qr(r)?Ql(r):Hl(r)}var Hr=DS;var jS=Math.ceil;function US(r,t){t=t===void 0?" ":tr(t);var e=t.length;if(e<2)return e?So(t,r):t;var o=So(t,jS(r/Hr(t)));return qr(t)?Ir(ur(o),0,r).join(""):o.slice(0,r)}var Jt=US;var GS=Math.ceil,kS=Math.floor;function qS(r,t,e){r=v(r),t=x(t);var o=t?Hr(r):0;if(!t||o>=t)return r;var f=(t-o)/2;return Jt(kS(f),e)+r+Jt(GS(f),e)}var Dn=qS;function zS(r,t,e){r=v(r),t=x(t);var o=t?Hr(r):0;return t&&o-1;)m!==r&&ed.call(m,p,1),ed.call(r,p,1);return r}var Ge=fT;function aT(r,t){return r&&r.length&&t&&t.length?Ge(r,t):r}var wo=aT;var iT=h(wo),Xn=iT;function nT(r,t,e){return r&&r.length&&t&&t.length?Ge(r,t,d(e,2)):r}var $n=nT;function mT(r,t,e){return r&&r.length&&t&&t.length?Ge(r,t,void 0,e):r}var Zn=mT;var pT=Array.prototype,uT=pT.splice;function sT(r,t){for(var e=r?t.length:0,o=e-1;e--;){var f=t[e];if(e==o||f!==a){var a=f;lr(f)?uT.call(r,f,1):je(r,f)}}return r}var jf=sT;var lT=xr(function(r,t){var e=r==null?0:r.length,o=ce(r,t);return jf(r,O(t,function(f){return lr(f,e)?+f:f}).sort(Bf)),o}),Vn=lT;var dT=Math.floor,cT=Math.random;function xT(r,t){return r+dT(cT()*(t-r+1))}var ke=xT;var hT=parseFloat,gT=Math.min,bT=Math.random;function yT(r,t,e){if(e&&typeof e!="boolean"&&k(r,t,e)&&(t=e=void 0),e===void 0&&(typeof t=="boolean"?(e=t,t=void 0):typeof r=="boolean"&&(e=r,r=void 0)),r===void 0&&t===void 0?(r=0,t=1):(r=Tr(r),t===void 0?(t=r,r=0):t=Tr(t)),r>t){var o=r;r=t,t=o}if(e||r%1||t%1){var f=bT();return gT(r+f*(t-r+hT("1e-"+((f+"").length-1))),t)}return ke(r,t)}var Jn=yT;var vT=Math.ceil,_T=Math.max;function AT(r,t,e,o){for(var f=-1,a=_T(vT((t-r)/(e||1)),0),n=Array(a);a--;)n[o?a:++f]=r,r+=e;return n}var od=AT;function IT(r){return function(t,e,o){return o&&typeof o!="number"&&k(t,e,o)&&(e=o=void 0),t=Tr(t),e===void 0?(e=t,t=0):e=Tr(e),o=o===void 0?t1&&k(r,t[0],t[1])?t=[]:e>2&&k(t[0],t[1],t[2])&&(t=[t[0]]),Nf(r,B(t,1),[])}),vm=mw;var pw=4294967295,uw=pw-1,sw=Math.floor,lw=Math.min;function dw(r,t,e,o){var f=0,a=r==null?0:r.length;if(a===0)return 0;t=e(t);for(var n=t!==t,m=t===null,p=Y(t),u=t===void 0;f>>1;function hw(r,t,e){var o=0,f=r==null?o:r.length;if(typeof t=="number"&&t===t&&f<=xw){for(;o>>1,n=r[a];n!==null&&!Y(n)&&(e?n<=t:n>>0,e?(r=v(r),r&&(typeof t=="string"||t!=null&&!Vt(t))&&(t=tr(t),!t&&qr(r))?Ir(ur(r),0,e):r.split(t,e)):[]}var Lm=Tw;var ww="Expected a function",Lw=Math.max;function Ew(r,t){if(typeof r!="function")throw new TypeError(ww);return t=t==null?0:Lw(x(t),0),h(function(e){var o=e[t],f=Ir(e,0,t);return o&&Ar(f,o),X(r,this,f)})}var Em=Ew;var Pw=zr(function(r,t,e){return r+(e?" ":"")+Gt(t)}),Pm=Pw;function Cw(r,t,e){return r=v(r),e=e==null?0:Mr(x(e),0,r.length),t=tr(t),r.slice(e,e+t.length)==t}var Cm=Cw;function Mw(){return{}}var Mm=Mw;function Fw(){return""}var Fm=Fw;function Ww(){return!0}var Wm=Ww;var Bw=st(function(r,t){return r-t},0),Bm=Bw;function Nw(r){return r&&r.length?De(r,D):0}var Nm=Nw;function Dw(r,t){return r&&r.length?De(r,d(t,2)):0}var Dm=Dw;function jw(r){var t=r==null?0:r.length;return t?rr(r,1,t):[]}var jm=jw;function Uw(r,t,e){return r&&r.length?(t=e||t===void 0?1:x(t),rr(r,0,t<0?0:t)):[]}var Um=Uw;function Gw(r,t,e){var o=r==null?0:r.length;return o?(t=e||t===void 0?1:x(t),t=o-t,rr(r,t<0?0:t,o)):[]}var Gm=Gw;function kw(r,t){return r&&r.length?St(r,d(t,3),!1,!0):[]}var km=kw;function qw(r,t){return r&&r.length?St(r,d(t,3)):[]}var qm=qw;function zw(r,t){return t(r),r}var zm=zw;var sd=Object.prototype,Hw=sd.hasOwnProperty;function Kw(r,t,e,o){return r===void 0||Q(r,sd[e])&&!Hw.call(o,e)?t:r}var Hm=Kw;var Yw={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"};function Xw(r){return"\\"+Yw[r]}var ld=Xw;var $w=/<%=([\s\S]+?)%>/g,zf=$w;var Zw=/<%-([\s\S]+?)%>/g,dd=Zw;var Vw=/<%([\s\S]+?)%>/g,cd=Vw;var Jw={escape:dd,evaluate:cd,interpolate:zf,variable:"",imports:{_:{escape:go}}},He=Jw;var Qw="Invalid `variable` option passed into `_.template`",rL="Invalid `imports` option passed into `_.template`",tL=/\b__p \+= '';/g,eL=/\b(__p \+=) '' \+/g,oL=/(__e\(.*?\)|\b__t\)) \+\n'';/g,xd=/[()=,{}\[\]\/\s]/,fL=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,Hf=/($^)/,aL=/['\n\r\u2028\u2029\\]/g,iL=Object.prototype,hd=iL.hasOwnProperty;function nL(r,t,e){var o=He.imports._.templateSettings||He;e&&k(r,t,e)&&(t=void 0),r=v(r),t=pe({},t,o,Hm);var f=pe({},t.imports,o.imports,Hm),a=E(f),n=Fe(f,a);fr(a,function(A){if(xd.test(A))throw new Error(rL)});var m,p,u=0,s=t.interpolate||Hf,l="__p += '",g=RegExp((t.escape||Hf).source+"|"+s.source+"|"+(s===zf?fL:Hf).source+"|"+(t.evaluate||Hf).source+"|$","g"),y=hd.call(t,"sourceURL")?"//# sourceURL="+(t.sourceURL+"").replace(/\s/g," ")+` +`:"";r.replace(g,function(A,P,F,Fr,Sr,Wr){return F||(F=Fr),l+=r.slice(u,Wr).replace(aL,ld),P&&(m=!0,l+=`' + +__e(`+P+`) + +'`),Sr&&(p=!0,l+=`'; +`+Sr+`; +__p += '`),F&&(l+=`' + +((__t = (`+F+`)) == null ? '' : __t) + +'`),u=Wr+A.length,A}),l+=`'; +`;var I=hd.call(t,"variable")&&t.variable;if(!I)l=`with (obj) { +`+l+` +} +`;else if(xd.test(I))throw new Error(Qw);l=(p?l.replace(tL,""):l).replace(eL,"$1").replace(oL,"$1;"),l="function("+(I||"obj")+`) { +`+(I?"":`obj || (obj = {}); +`)+"var __t, __p = ''"+(m?", __e = _.escape":"")+(p?`, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +`:`; +`)+l+`return __p +}`;var L=ro(function(){return Function(a,y+"return "+l).apply(void 0,n)});if(L.source=l,Ut(L))throw L;return L}var Km=nL;var mL="Expected a function";function pL(r,t,e){var o=!0,f=!0;if(typeof r!="function")throw new TypeError(mL);return S(e)&&(o="leading"in e?!!e.leading:o,f="trailing"in e?!!e.trailing:f),so(r,t,{leading:o,maxWait:t,trailing:f})}var Ym=pL;function uL(r,t){return t(r)}var et=uL;var sL=9007199254740991,Xm=4294967295,lL=Math.min;function dL(r,t){if(r=x(r),r<1||r>sL)return[];var e=Xm,o=lL(r,Xm);t=mr(t),r-=Xm;for(var f=ne(o,t);++e-1;);return e}var Xf=AL;function IL(r,t){for(var e=-1,o=r.length;++e-1;);return e}var $f=IL;function RL(r,t,e){if(r=v(r),r&&(e||t===void 0))return Mo(r);if(!r||!(t=tr(t)))return r;var o=ur(r),f=ur(t),a=$f(o,f),n=Xf(o,f)+1;return Ir(o,a,n).join("")}var tp=RL;function OL(r,t,e){if(r=v(r),r&&(e||t===void 0))return r.slice(0,Co(r)+1);if(!r||!(t=tr(t)))return r;var o=ur(r),f=Xf(o,ur(t))+1;return Ir(o,0,f).join("")}var ep=OL;var SL=/^\s+/;function TL(r,t,e){if(r=v(r),r&&(e||t===void 0))return r.replace(SL,"");if(!r||!(t=tr(t)))return r;var o=ur(r),f=$f(o,ur(t));return Ir(o,f).join("")}var op=TL;var wL=30,LL="...",EL=/\w*$/;function PL(r,t){var e=wL,o=LL;if(S(t)){var f="separator"in t?t.separator:f;e="length"in t?x(t.length):e,o="omission"in t?tr(t.omission):o}r=v(r);var a=r.length;if(qr(r)){var n=ur(r);a=n.length}if(e>=a)return r;var m=e-Hr(o);if(m<1)return o;var p=n?Ir(n,0,m).join(""):r.slice(0,m);if(f===void 0)return p+o;if(n&&(m+=p.length-m),Vt(f)){if(r.slice(m).search(f)){var u,s=p;for(f.global||(f=RegExp(f.source,v(EL.exec(f))+"g")),f.lastIndex=0;u=f.exec(s);)var l=u.index;p=p.slice(0,l===void 0?m:l)}}else if(r.indexOf(tr(f),m)!=m){var g=p.lastIndexOf(f);g>-1&&(p=p.slice(0,g))}return p+o}var fp=PL;function CL(r){return Ze(r,1)}var ap=CL;var ML={"&":"&","<":"<",">":">",""":'"',"'":"'"},FL=he(ML),bd=FL;var yd=/&(?:amp|lt|gt|quot|#39);/g,WL=RegExp(yd.source);function BL(r){return r=v(r),r&&WL.test(r)?r.replace(yd,bd):r}var ip=BL;var NL=1/0,DL=_t&&1/It(new _t([,-0]))[1]==NL?function(r){return new _t(r)}:Mt,vd=DL;var jL=200;function UL(r,t,e){var o=-1,f=xt,a=r.length,n=!0,m=[],p=m;if(e)n=!1,f=Pe;else if(a>=jL){var u=t?null:vd(r);if(u)return It(u);n=!1,f=mt,p=new At}else p=t?[]:m;r:for(;++o1||this.__actions__.length||!(o instanceof w)||!lr(e)?this.thru(f):(o=o.slice(e,+e+(t?1:0)),o.__actions__.push({func:et,args:[f],thisArg:void 0}),new yr(o,this.__chain__).thru(function(a){return t&&!a.length&&a.push(void 0),a}))}),_p=iE;function nE(){return io(this)}var Ap=nE;function mE(){var r=this.__wrapped__;if(r instanceof w){var t=r;return this.__actions__.length&&(t=new w(this)),t=t.reverse(),t.__actions__.push({func:et,args:[qe],thisArg:void 0}),new yr(t,this.__chain__)}return this.thru(qe)}var Ip=mE;function pE(r,t,e){var o=r.length;if(o<2)return o?Lr(r[0]):[];for(var f=-1,a=Array(o);++f1?r[t-1]:void 0;return e=typeof e=="function"?(r.pop(),e):void 0,Lo(r,e)}),Ep=gE;var b={chunk:va,compact:Sa,concat:Ta,difference:qa,differenceBy:za,differenceWith:Ha,drop:Ya,dropRight:Xa,dropRightWhile:$a,dropWhile:Za,fill:ri,findIndex:yo,findLastIndex:vo,first:Zt,flatten:Qe,flattenDeep:pi,flattenDepth:ui,fromPairs:yi,head:Zt,indexOf:wi,initial:Li,intersection:Ei,intersectionBy:Pi,intersectionWith:Ci,join:en,last:K,lastIndexOf:an,nth:Sn,pull:Xn,pullAll:wo,pullAllBy:$n,pullAllWith:Zn,pullAt:Vn,remove:am,reverse:qe,slice:gm,sortedIndex:_m,sortedIndexBy:Am,sortedIndexOf:Im,sortedLastIndex:Rm,sortedLastIndexBy:Om,sortedLastIndexOf:Sm,sortedUniq:Tm,sortedUniqBy:wm,tail:jm,take:Um,takeRight:Gm,takeRightWhile:km,takeWhile:qm,union:np,unionBy:mp,unionWith:pp,uniq:up,uniqBy:sp,uniqWith:lp,unzip:Qt,unzipWith:Lo,without:yp,xor:Rp,xorBy:Op,xorWith:Sp,zip:Tp,zipObject:wp,zipObjectDeep:Lp,zipWith:Ep};var j={countBy:Ca,each:Kt,eachRight:Yt,every:Qa,filter:ti,find:ei,findLast:fi,flatMap:ii,flatMapDeep:ni,flatMapDepth:mi,forEach:Kt,forEachRight:Yt,groupBy:Ai,includes:Ti,invokeMap:Bi,keyBy:fn,map:pt,orderBy:Pn,partition:Hn,reduce:em,reduceRight:om,reject:fm,sample:sm,sampleSize:lm,shuffle:xm,size:hm,some:ym,sortBy:vm};var Pp={now:Ht};var J={after:ra,ary:Ze,before:to,bind:eo,bindKey:ca,curry:Wa,curryRight:Na,debounce:so,defer:Ga,delay:ka,flip:si,memoize:Je,negate:rt,once:En,overArgs:Mn,partial:To,partialRight:zn,rearg:tm,rest:mm,spread:Em,throttle:Ym,unary:ap,wrap:vp};var _={castArray:ba,clone:Aa,cloneDeep:Ia,cloneDeepWith:Ra,cloneWith:Oa,conformsTo:Pa,eq:Q,gt:Ii,gte:Ri,isArguments:Pr,isArray:c,isArrayBuffer:Ni,isArrayLike:G,isArrayLikeObject:M,isBoolean:Di,isBuffer:vr,isDate:ji,isElement:Ui,isEmpty:Gi,isEqual:ki,isEqualWith:qi,isError:Ut,isFinite:zi,isFunction:pr,isInteger:_o,isLength:it,isMap:mo,isMatch:Hi,isMatchWith:Ki,isNaN:Yi,isNative:Xi,isNil:$i,isNull:Zi,isNumber:Ao,isObject:S,isObjectLike:T,isPlainObject:Xr,isRegExp:Vt,isSafeInteger:Vi,isSet:po,isString:ut,isSymbol:Y,isTypedArray:Br,isUndefined:Ji,isWeakMap:Qi,isWeakSet:rn,lt:pn,lte:un,toArray:Ro,toFinite:Tr,toInteger:x,toLength:bo,toNumber:or,toPlainObject:xo,toSafeInteger:Jm,toString:v};var br={add:Qf,ceil:ya,divide:Ka,floor:li,max:xn,maxBy:hn,mean:gn,meanBy:bn,min:An,minBy:In,multiply:Rn,round:um,subtract:Bm,sum:Nm,sumBy:Dm};var Eo={clamp:_a,inRange:Si,random:Jn};var R={assign:na,assignIn:Bt,assignInWith:Nt,assignWith:pe,at:ua,create:Ma,defaults:ja,defaultsDeep:Ua,entries:Xt,entriesIn:$t,extend:Bt,extendWith:Nt,findKey:oi,findLastKey:ai,forIn:xi,forInRight:hi,forOwn:gi,forOwnRight:bi,functions:vi,functionsIn:_i,get:jt,has:Oi,hasIn:zt,invert:Mi,invertBy:Fi,invoke:Wi,keys:E,keysIn:q,mapKeys:sn,mapValues:ln,merge:yn,mergeWith:ho,omit:wn,omitBy:Ln,pick:Kn,pickBy:Oo,result:pm,set:dm,setWith:cm,toPairs:Xt,toPairsIn:$t,transform:rp,unset:cp,update:xp,updateWith:hp,values:Nr,valuesIn:bp};var Kr={at:_p,chain:io,commit:mf,lodash:i,next:Mf,plant:Df,reverse:Ip,tap:zm,thru:et,toIterator:Kf,toJSON:Dr,value:Dr,valueOf:Dr,wrapperChain:Ap};var W={camelCase:ga,capitalize:oo,deburr:fo,endsWith:Va,escape:go,escapeRegExp:Ja,kebabCase:on,lowerCase:nn,lowerFirst:mn,pad:Dn,padEnd:jn,padStart:Un,parseInt:Gn,repeat:im,replace:nm,snakeCase:bm,split:Lm,startCase:Pm,startsWith:Cm,template:Km,templateSettings:He,toLower:Zm,toUpper:Qm,trim:tp,trimEnd:ep,trimStart:op,truncate:fp,unescape:ip,upperCase:gp,upperFirst:Gt,words:ao};var N={attempt:ro,bindAll:la,cond:La,conforms:Ea,constant:Ft,defaultTo:Da,flow:di,flowRight:ci,identity:D,iteratee:tn,matches:dn,matchesProperty:cn,method:vn,methodOf:_n,mixin:Io,noop:Mt,nthArg:Tn,over:Cn,overEvery:Fn,overSome:Wn,property:uo,propertyOf:Yn,range:Qn,rangeRight:rm,stubArray:kt,stubFalse:Wt,stubObject:Mm,stubString:Fm,stubTrue:Wm,times:$m,toPath:Vm,uniqueId:dp};function bE(){var r=new w(this.__wrapped__);return r.__actions__=z(this.__actions__),r.__dir__=this.__dir__,r.__filtered__=this.__filtered__,r.__iteratees__=z(this.__iteratees__),r.__takeCount__=this.__takeCount__,r.__views__=z(this.__views__),r}var _d=bE;function yE(){if(this.__filtered__){var r=new w(this);r.__dir__=-1,r.__filtered__=!0}else r=this.clone(),r.__dir__*=-1;return r}var Ad=yE;var vE=Math.max,_E=Math.min;function AE(r,t,e){for(var o=-1,f=e.length;++o0||t<0)?new w(e):(r<0?e=e.takeRight(-r):r&&(e=e.drop(r)),t!==void 0&&(t=x(t),e=t<0?e.dropRight(-t):e.take(t-r)),e)};w.prototype.takeRightWhile=function(r){return this.reverse().takeWhile(r).reverse()};w.prototype.toArray=function(){return this.take(Td)};gr(w.prototype,function(r,t){var e=/^(?:filter|find|map|reject)|While$/.test(t),o=/^(?:head|last)$/.test(t),f=i[o?"take"+(t=="last"?"Right":""):t],a=o||/^find/.test(t);f&&(i.prototype[t]=function(){var n=this.__wrapped__,m=o?[1]:arguments,p=n instanceof w,u=m[0],s=p||c(n),l=function(P){var F=f.apply(i,Ar([P],m));return o&&g?F[0]:F};s&&e&&typeof u=="function"&&u.length!=1&&(p=s=!1);var g=this.__chain__,y=!!this.__actions__.length,I=a&&!g,L=p&&!y;if(!a&&s){n=L?n:new w(this);var A=r.apply(n,m);return A.__actions__.push({func:et,args:[l],thisArg:void 0}),new yr(A,g)}return I&&L?r.apply(this,m):(A=this.thru(l),I?o?A.value()[0]:A.value():A)})});fr(["pop","push","shift","sort","splice","unshift"],function(r){var t=PE[r],e=/^(?:push|sort|unshift)$/.test(r)?"tap":"thru",o=/^(?:pop|shift)$/.test(r);i.prototype[r]=function(){var f=arguments;if(o&&!this.__chain__){var a=this.value();return t.apply(c(a)?a:[],f)}return this[e](function(n){return t.apply(c(n)?n:[],f)})}});gr(w.prototype,function(r,t){var e=i[t];if(e){var o=e.name+"";wd.call(dt,o)||(dt[o]=[]),dt[o].push({name:t,func:e})}});dt[ie(void 0,wE).name]=[{name:"wrapper",func:void 0}];w.prototype.clone=_d;w.prototype.reverse=Ad;w.prototype.value=Rd;i.prototype.at=Kr.at;i.prototype.chain=Kr.wrapperChain;i.prototype.commit=Kr.commit;i.prototype.next=Kr.next;i.prototype.plant=Kr.plant;i.prototype.reverse=Kr.reverse;i.prototype.toJSON=i.prototype.valueOf=i.prototype.value=Kr.value;i.prototype.first=i.prototype.head;Od&&(i.prototype[Od]=Kr.toIterator);var FE=i;export{Qf as add,ra as after,Ze as ary,na as assign,Bt as assignIn,Nt as assignInWith,pe as assignWith,ua as at,ro as attempt,to as before,eo as bind,la as bindAll,ca as bindKey,ga as camelCase,oo as capitalize,ba as castArray,ya as ceil,io as chain,va as chunk,_a as clamp,Aa as clone,Ia as cloneDeep,Ra as cloneDeepWith,Oa as cloneWith,mf as commit,Sa as compact,Ta as concat,La as cond,Ea as conforms,Pa as conformsTo,Ft as constant,Ca as countBy,Ma as create,Wa as curry,Na as curryRight,so as debounce,fo as deburr,FE as default,Da as defaultTo,ja as defaults,Ua as defaultsDeep,Ga as defer,ka as delay,qa as difference,za as differenceBy,Ha as differenceWith,Ka as divide,Ya as drop,Xa as dropRight,$a as dropRightWhile,Za as dropWhile,Kt as each,Yt as eachRight,Va as endsWith,Xt as entries,$t as entriesIn,Q as eq,go as escape,Ja as escapeRegExp,Qa as every,Bt as extend,Nt as extendWith,ri as fill,ti as filter,ei as find,yo as findIndex,oi as findKey,fi as findLast,vo as findLastIndex,ai as findLastKey,Zt as first,ii as flatMap,ni as flatMapDeep,mi as flatMapDepth,Qe as flatten,pi as flattenDeep,ui as flattenDepth,si as flip,li as floor,di as flow,ci as flowRight,Kt as forEach,Yt as forEachRight,xi as forIn,hi as forInRight,gi as forOwn,bi as forOwnRight,yi as fromPairs,vi as functions,_i as functionsIn,jt as get,Ai as groupBy,Ii as gt,Ri as gte,Oi as has,zt as hasIn,Zt as head,D as identity,Si as inRange,Ti as includes,wi as indexOf,Li as initial,Ei as intersection,Pi as intersectionBy,Ci as intersectionWith,Mi as invert,Fi as invertBy,Wi as invoke,Bi as invokeMap,Pr as isArguments,c as isArray,Ni as isArrayBuffer,G as isArrayLike,M as isArrayLikeObject,Di as isBoolean,vr as isBuffer,ji as isDate,Ui as isElement,Gi as isEmpty,ki as isEqual,qi as isEqualWith,Ut as isError,zi as isFinite,pr as isFunction,_o as isInteger,it as isLength,mo as isMap,Hi as isMatch,Ki as isMatchWith,Yi as isNaN,Xi as isNative,$i as isNil,Zi as isNull,Ao as isNumber,S as isObject,T as isObjectLike,Xr as isPlainObject,Vt as isRegExp,Vi as isSafeInteger,po as isSet,ut as isString,Y as isSymbol,Br as isTypedArray,Ji as isUndefined,Qi as isWeakMap,rn as isWeakSet,tn as iteratee,en as join,on as kebabCase,fn as keyBy,E as keys,q as keysIn,K as last,an as lastIndexOf,i as lodash,nn as lowerCase,mn as lowerFirst,pn as lt,un as lte,pt as map,sn as mapKeys,ln as mapValues,dn as matches,cn as matchesProperty,xn as max,hn as maxBy,gn as mean,bn as meanBy,Je as memoize,yn as merge,ho as mergeWith,vn as method,_n as methodOf,An as min,In as minBy,Io as mixin,Rn as multiply,rt as negate,Mf as next,Mt as noop,Ht as now,Sn as nth,Tn as nthArg,wn as omit,Ln as omitBy,En as once,Pn as orderBy,Cn as over,Mn as overArgs,Fn as overEvery,Wn as overSome,Dn as pad,jn as padEnd,Un as padStart,Gn as parseInt,To as partial,zn as partialRight,Hn as partition,Kn as pick,Oo as pickBy,Df as plant,uo as property,Yn as propertyOf,Xn as pull,wo as pullAll,$n as pullAllBy,Zn as pullAllWith,Vn as pullAt,Jn as random,Qn as range,rm as rangeRight,tm as rearg,em as reduce,om as reduceRight,fm as reject,am as remove,im as repeat,nm as replace,mm as rest,pm as result,qe as reverse,um as round,sm as sample,lm as sampleSize,dm as set,cm as setWith,xm as shuffle,hm as size,gm as slice,bm as snakeCase,ym as some,vm as sortBy,_m as sortedIndex,Am as sortedIndexBy,Im as sortedIndexOf,Rm as sortedLastIndex,Om as sortedLastIndexBy,Sm as sortedLastIndexOf,Tm as sortedUniq,wm as sortedUniqBy,Lm as split,Em as spread,Pm as startCase,Cm as startsWith,kt as stubArray,Wt as stubFalse,Mm as stubObject,Fm as stubString,Wm as stubTrue,Bm as subtract,Nm as sum,Dm as sumBy,jm as tail,Um as take,Gm as takeRight,km as takeRightWhile,qm as takeWhile,zm as tap,Km as template,He as templateSettings,Ym as throttle,et as thru,$m as times,Ro as toArray,Tr as toFinite,x as toInteger,Kf as toIterator,Dr as toJSON,bo as toLength,Zm as toLower,or as toNumber,Xt as toPairs,$t as toPairsIn,Vm as toPath,xo as toPlainObject,Jm as toSafeInteger,v as toString,Qm as toUpper,rp as transform,tp as trim,ep as trimEnd,op as trimStart,fp as truncate,ap as unary,ip as unescape,np as union,mp as unionBy,pp as unionWith,up as uniq,sp as uniqBy,lp as uniqWith,dp as uniqueId,cp as unset,Qt as unzip,Lo as unzipWith,xp as update,hp as updateWith,gp as upperCase,Gt as upperFirst,Dr as value,Dr as valueOf,Nr as values,bp as valuesIn,yp as without,ao as words,vp as wrap,_p as wrapperAt,Ap as wrapperChain,mf as wrapperCommit,i as wrapperLodash,Mf as wrapperNext,Df as wrapperPlant,Ip as wrapperReverse,Kf as wrapperToIterator,Dr as wrapperValue,Rp as xor,Op as xorBy,Sp as xorWith,Tp as zip,wp as zipObject,Lp as zipObjectDeep,Ep as zipWith}; +/*! Bundled license information: + +lodash-es/lodash.default.js: + (** + * @license + * Lodash (Custom Build) + * Build: `lodash modularize exports="es" --repo lodash/lodash#4.18.1 -o ./` + * Copyright OpenJS Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + *) + +lodash-es/lodash.js: + (** + * @license + * Lodash (Custom Build) + * Build: `lodash modularize exports="es" --repo lodash/lodash#4.18.1 -o ./` + * Copyright OpenJS Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + *) +*/ diff --git a/Resources/Public/JavaScript/Contrib/markjs.js b/Resources/Public/JavaScript/Contrib/markjs.js new file mode 100644 index 0000000..95ac305 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/markjs.js @@ -0,0 +1,6 @@ +var b=(y,v)=>()=>(v||y((v={exports:{}}).exports,v),v.exports);var T=b((E,k)=>{/*!*************************************************** +* mark.js v8.11.1 +* https://markjs.io/ +* Copyright (c) 2014–2018, Julian Kühnel +* Released under the MIT license https://git.io/vwTVl +*****************************************************/(function(y,v){typeof E=="object"&&typeof k<"u"?k.exports=v():typeof define=="function"&&define.amd?define(v):y.Mark=v()})(E,function(){"use strict";var y=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(f){return typeof f}:function(f){return f&&typeof Symbol=="function"&&f.constructor===Symbol&&f!==Symbol.prototype?"symbol":typeof f},v=function(f,o){if(!(f instanceof o))throw new TypeError("Cannot call a class as a function")},I=function(){function f(o,e){for(var n=0;n1&&arguments[1]!==void 0?arguments[1]:!0,n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:[],t=arguments.length>3&&arguments[3]!==void 0?arguments[3]:5e3;v(this,f),this.ctx=o,this.iframes=e,this.exclude=n,this.iframesTimeout=t}return I(f,[{key:"getContexts",value:function(){var e=void 0,n=[];return typeof this.ctx>"u"||!this.ctx?e=[]:NodeList.prototype.isPrototypeOf(this.ctx)?e=Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?e=this.ctx:typeof this.ctx=="string"?e=Array.prototype.slice.call(document.querySelectorAll(this.ctx)):e=[this.ctx],e.forEach(function(t){var a=n.filter(function(r){return r.contains(t)}).length>0;n.indexOf(t)===-1&&!a&&n.push(t)}),n}},{key:"getIframeContents",value:function(e,n){var t=arguments.length>2&&arguments[2]!==void 0?arguments[2]:function(){},a=void 0;try{var r=e.contentWindow;if(a=r.document,!r||!a)throw new Error("iframe inaccessible")}catch{t()}a&&n(a)}},{key:"isIframeBlank",value:function(e){var n="about:blank",t=e.getAttribute("src").trim(),a=e.contentWindow.location.href;return a===n&&t!==n&&t}},{key:"observeIframeLoad",value:function(e,n,t){var a=this,r=!1,s=null,i=function c(){if(!r){r=!0,clearTimeout(s);try{a.isIframeBlank(e)||(e.removeEventListener("load",c),a.getIframeContents(e,n,t))}catch{t()}}};e.addEventListener("load",i),s=setTimeout(i,this.iframesTimeout)}},{key:"onIframeReady",value:function(e,n,t){try{e.contentWindow.document.readyState==="complete"?this.isIframeBlank(e)?this.observeIframeLoad(e,n,t):this.getIframeContents(e,n,t):this.observeIframeLoad(e,n,t)}catch{t()}}},{key:"waitForIframes",value:function(e,n){var t=this,a=0;this.forEachIframe(e,function(){return!0},function(r){a++,t.waitForIframes(r.querySelector("html"),function(){--a||n()})},function(r){r||n()})}},{key:"forEachIframe",value:function(e,n,t){var a=this,r=arguments.length>3&&arguments[3]!==void 0?arguments[3]:function(){},s=e.querySelectorAll("iframe"),i=s.length,c=0;s=Array.prototype.slice.call(s);var u=function(){--i<=0&&r(c)};i||u(),s.forEach(function(l){f.matches(l,a.exclude)?u():a.onIframeReady(l,function(h){n(l)&&(c++,t(h)),u()},u)})}},{key:"createIterator",value:function(e,n,t){return document.createNodeIterator(e,n,t,!1)}},{key:"createInstanceOnIframe",value:function(e){return new f(e.querySelector("html"),this.iframes)}},{key:"compareNodeIframe",value:function(e,n,t){var a=e.compareDocumentPosition(t),r=Node.DOCUMENT_POSITION_PRECEDING;if(a&r)if(n!==null){var s=n.compareDocumentPosition(t),i=Node.DOCUMENT_POSITION_FOLLOWING;if(s&i)return!0}else return!0;return!1}},{key:"getIteratorNode",value:function(e){var n=e.previousNode(),t=void 0;return n===null?t=e.nextNode():t=e.nextNode()&&e.nextNode(),{prevNode:n,node:t}}},{key:"checkIframeFilter",value:function(e,n,t,a){var r=!1,s=!1;return a.forEach(function(i,c){i.val===t&&(r=c,s=i.handled)}),this.compareNodeIframe(e,n,t)?(r===!1&&!s?a.push({val:t,handled:!0}):r!==!1&&!s&&(a[r].handled=!0),!0):(r===!1&&a.push({val:t,handled:!1}),!1)}},{key:"handleOpenIframes",value:function(e,n,t,a){var r=this;e.forEach(function(s){s.handled||r.getIframeContents(s.val,function(i){r.createInstanceOnIframe(i).forEachNode(n,t,a)})})}},{key:"iterateThroughNodes",value:function(e,n,t,a,r){for(var s=this,i=this.createIterator(n,e,a),c=[],u=[],l=void 0,h=void 0,d=function(){var g=s.getIteratorNode(i);return h=g.prevNode,l=g.node,l};d();)this.iframes&&this.forEachIframe(n,function(p){return s.checkIframeFilter(l,h,p,c)},function(p){s.createInstanceOnIframe(p).forEachNode(e,function(g){return u.push(g)},a)}),u.push(l);u.forEach(function(p){t(p)}),this.iframes&&this.handleOpenIframes(c,e,t,a),r()}},{key:"forEachNode",value:function(e,n,t){var a=this,r=arguments.length>3&&arguments[3]!==void 0?arguments[3]:function(){},s=this.getContexts(),i=s.length;i||r(),s.forEach(function(c){var u=function(){a.iterateThroughNodes(e,c,n,t,function(){--i<=0&&r()})};a.iframes?a.waitForIframes(c,u):u()})}}],[{key:"matches",value:function(e,n){var t=typeof n=="string"?[n]:n,a=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector;if(a){var r=!1;return t.every(function(s){return a.call(e,s)?(r=!0,!1):!0}),r}else return!1}}]),f}(),R=function(){function f(o){v(this,f),this.ctx=o,this.ie=!1;var e=window.navigator.userAgent;(e.indexOf("MSIE")>-1||e.indexOf("Trident")>-1)&&(this.ie=!0)}return I(f,[{key:"log",value:function(e){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:"debug",t=this.opt.log;this.opt.debug&&(typeof t>"u"?"undefined":y(t))==="object"&&typeof t[n]=="function"&&t[n]("mark.js: "+e)}},{key:"escapeStr",value:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}},{key:"createRegExp",value:function(e){return this.opt.wildcards!=="disabled"&&(e=this.setupWildcardsRegExp(e)),e=this.escapeStr(e),Object.keys(this.opt.synonyms).length&&(e=this.createSynonymsRegExp(e)),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),this.opt.diacritics&&(e=this.createDiacriticsRegExp(e)),e=this.createMergedBlanksRegExp(e),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.createJoinersRegExp(e)),this.opt.wildcards!=="disabled"&&(e=this.createWildcardsRegExp(e)),e=this.createAccuracyRegExp(e),e}},{key:"createSynonymsRegExp",value:function(e){var n=this.opt.synonyms,t=this.opt.caseSensitive?"":"i",a=this.opt.ignoreJoiners||this.opt.ignorePunctuation.length?"\0":"";for(var r in n)if(n.hasOwnProperty(r)){var s=n[r],i=this.opt.wildcards!=="disabled"?this.setupWildcardsRegExp(r):this.escapeStr(r),c=this.opt.wildcards!=="disabled"?this.setupWildcardsRegExp(s):this.escapeStr(s);i!==""&&c!==""&&(e=e.replace(new RegExp("("+this.escapeStr(i)+"|"+this.escapeStr(c)+")","gm"+t),a+("("+this.processSynomyms(i)+"|")+(this.processSynomyms(c)+")")+a))}return e}},{key:"processSynomyms",value:function(e){return(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),e}},{key:"setupWildcardsRegExp",value:function(e){return e=e.replace(/(?:\\)*\?/g,function(n){return n.charAt(0)==="\\"?"?":""}),e.replace(/(?:\\)*\*/g,function(n){return n.charAt(0)==="\\"?"*":""})}},{key:"createWildcardsRegExp",value:function(e){var n=this.opt.wildcards==="withSpaces";return e.replace(/\u0001/g,n?"[\\S\\s]?":"\\S?").replace(/\u0002/g,n?"[\\S\\s]*?":"\\S*")}},{key:"setupIgnoreJoinersRegExp",value:function(e){return e.replace(/[^(|)\\]/g,function(n,t,a){var r=a.charAt(t+1);return/[(|)\\]/.test(r)||r===""?n:n+"\0"})}},{key:"createJoinersRegExp",value:function(e){var n=[],t=this.opt.ignorePunctuation;return Array.isArray(t)&&t.length&&n.push(this.escapeStr(t.join(""))),this.opt.ignoreJoiners&&n.push("\\u00ad\\u200b\\u200c\\u200d"),n.length?e.split(/\u0000+/).join("["+n.join("")+"]*"):e}},{key:"createDiacriticsRegExp",value:function(e){var n=this.opt.caseSensitive?"":"i",t=this.opt.caseSensitive?["a\xE0\xE1\u1EA3\xE3\u1EA1\u0103\u1EB1\u1EAF\u1EB3\u1EB5\u1EB7\xE2\u1EA7\u1EA5\u1EA9\u1EAB\u1EAD\xE4\xE5\u0101\u0105","A\xC0\xC1\u1EA2\xC3\u1EA0\u0102\u1EB0\u1EAE\u1EB2\u1EB4\u1EB6\xC2\u1EA6\u1EA4\u1EA8\u1EAA\u1EAC\xC4\xC5\u0100\u0104","c\xE7\u0107\u010D","C\xC7\u0106\u010C","d\u0111\u010F","D\u0110\u010E","e\xE8\xE9\u1EBB\u1EBD\u1EB9\xEA\u1EC1\u1EBF\u1EC3\u1EC5\u1EC7\xEB\u011B\u0113\u0119","E\xC8\xC9\u1EBA\u1EBC\u1EB8\xCA\u1EC0\u1EBE\u1EC2\u1EC4\u1EC6\xCB\u011A\u0112\u0118","i\xEC\xED\u1EC9\u0129\u1ECB\xEE\xEF\u012B","I\xCC\xCD\u1EC8\u0128\u1ECA\xCE\xCF\u012A","l\u0142","L\u0141","n\xF1\u0148\u0144","N\xD1\u0147\u0143","o\xF2\xF3\u1ECF\xF5\u1ECD\xF4\u1ED3\u1ED1\u1ED5\u1ED7\u1ED9\u01A1\u1EDF\u1EE1\u1EDB\u1EDD\u1EE3\xF6\xF8\u014D","O\xD2\xD3\u1ECE\xD5\u1ECC\xD4\u1ED2\u1ED0\u1ED4\u1ED6\u1ED8\u01A0\u1EDE\u1EE0\u1EDA\u1EDC\u1EE2\xD6\xD8\u014C","r\u0159","R\u0158","s\u0161\u015B\u0219\u015F","S\u0160\u015A\u0218\u015E","t\u0165\u021B\u0163","T\u0164\u021A\u0162","u\xF9\xFA\u1EE7\u0169\u1EE5\u01B0\u1EEB\u1EE9\u1EED\u1EEF\u1EF1\xFB\xFC\u016F\u016B","U\xD9\xDA\u1EE6\u0168\u1EE4\u01AF\u1EEA\u1EE8\u1EEC\u1EEE\u1EF0\xDB\xDC\u016E\u016A","y\xFD\u1EF3\u1EF7\u1EF9\u1EF5\xFF","Y\xDD\u1EF2\u1EF6\u1EF8\u1EF4\u0178","z\u017E\u017C\u017A","Z\u017D\u017B\u0179"]:["a\xE0\xE1\u1EA3\xE3\u1EA1\u0103\u1EB1\u1EAF\u1EB3\u1EB5\u1EB7\xE2\u1EA7\u1EA5\u1EA9\u1EAB\u1EAD\xE4\xE5\u0101\u0105A\xC0\xC1\u1EA2\xC3\u1EA0\u0102\u1EB0\u1EAE\u1EB2\u1EB4\u1EB6\xC2\u1EA6\u1EA4\u1EA8\u1EAA\u1EAC\xC4\xC5\u0100\u0104","c\xE7\u0107\u010DC\xC7\u0106\u010C","d\u0111\u010FD\u0110\u010E","e\xE8\xE9\u1EBB\u1EBD\u1EB9\xEA\u1EC1\u1EBF\u1EC3\u1EC5\u1EC7\xEB\u011B\u0113\u0119E\xC8\xC9\u1EBA\u1EBC\u1EB8\xCA\u1EC0\u1EBE\u1EC2\u1EC4\u1EC6\xCB\u011A\u0112\u0118","i\xEC\xED\u1EC9\u0129\u1ECB\xEE\xEF\u012BI\xCC\xCD\u1EC8\u0128\u1ECA\xCE\xCF\u012A","l\u0142L\u0141","n\xF1\u0148\u0144N\xD1\u0147\u0143","o\xF2\xF3\u1ECF\xF5\u1ECD\xF4\u1ED3\u1ED1\u1ED5\u1ED7\u1ED9\u01A1\u1EDF\u1EE1\u1EDB\u1EDD\u1EE3\xF6\xF8\u014DO\xD2\xD3\u1ECE\xD5\u1ECC\xD4\u1ED2\u1ED0\u1ED4\u1ED6\u1ED8\u01A0\u1EDE\u1EE0\u1EDA\u1EDC\u1EE2\xD6\xD8\u014C","r\u0159R\u0158","s\u0161\u015B\u0219\u015FS\u0160\u015A\u0218\u015E","t\u0165\u021B\u0163T\u0164\u021A\u0162","u\xF9\xFA\u1EE7\u0169\u1EE5\u01B0\u1EEB\u1EE9\u1EED\u1EEF\u1EF1\xFB\xFC\u016F\u016BU\xD9\xDA\u1EE6\u0168\u1EE4\u01AF\u1EEA\u1EE8\u1EEC\u1EEE\u1EF0\xDB\xDC\u016E\u016A","y\xFD\u1EF3\u1EF7\u1EF9\u1EF5\xFFY\xDD\u1EF2\u1EF6\u1EF8\u1EF4\u0178","z\u017E\u017C\u017AZ\u017D\u017B\u0179"],a=[];return e.split("").forEach(function(r){t.every(function(s){if(s.indexOf(r)!==-1){if(a.indexOf(s)>-1)return!1;e=e.replace(new RegExp("["+s+"]","gm"+n),"["+s+"]"),a.push(s)}return!0})}),e}},{key:"createMergedBlanksRegExp",value:function(e){return e.replace(/[\s]+/gmi,"[\\s]+")}},{key:"createAccuracyRegExp",value:function(e){var n=this,t="!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~\xA1\xBF",a=this.opt.accuracy,r=typeof a=="string"?a:a.value,s=typeof a=="string"?[]:a.limiters,i="";switch(s.forEach(function(c){i+="|"+n.escapeStr(c)}),r){case"partially":default:return"()("+e+")";case"complementary":return i="\\s"+(i||this.escapeStr(t)),"()([^"+i+"]*"+e+"[^"+i+"]*)";case"exactly":return"(^|\\s"+i+")("+e+")(?=$|\\s"+i+")"}}},{key:"getSeparatedKeywords",value:function(e){var n=this,t=[];return e.forEach(function(a){n.opt.separateWordSearch?a.split(" ").forEach(function(r){r.trim()&&t.indexOf(r)===-1&&t.push(r)}):a.trim()&&t.indexOf(a)===-1&&t.push(a)}),{keywords:t.sort(function(a,r){return r.length-a.length}),length:t.length}}},{key:"isNumeric",value:function(e){return Number(parseFloat(e))==e}},{key:"checkRanges",value:function(e){var n=this;if(!Array.isArray(e)||Object.prototype.toString.call(e[0])!=="[object Object]")return this.log("markRanges() will only accept an array of objects"),this.opt.noMatch(e),[];var t=[],a=0;return e.sort(function(r,s){return r.start-s.start}).forEach(function(r){var s=n.callNoMatchOnInvalidRanges(r,a),i=s.start,c=s.end,u=s.valid;u&&(r.start=i,r.length=c-i,t.push(r),a=c)}),t}},{key:"callNoMatchOnInvalidRanges",value:function(e,n){var t=void 0,a=void 0,r=!1;return e&&typeof e.start<"u"?(t=parseInt(e.start,10),a=t+parseInt(e.length,10),this.isNumeric(e.start)&&this.isNumeric(e.length)&&a-n>0&&a-t>0?r=!0:(this.log("Ignoring invalid or overlapping range: "+(""+JSON.stringify(e))),this.opt.noMatch(e))):(this.log("Ignoring invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:t,end:a,valid:r}}},{key:"checkWhitespaceRanges",value:function(e,n,t){var a=void 0,r=!0,s=t.length,i=n-s,c=parseInt(e.start,10)-i;return c=c>s?s:c,a=c+parseInt(e.length,10),a>s&&(a=s,this.log("End range automatically set to the max value of "+s)),c<0||a-c<0||c>s||a>s?(r=!1,this.log("Invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)):t.substring(c,a).replace(/\s+/g,"")===""&&(r=!1,this.log("Skipping whitespace only range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:c,end:a,valid:r}}},{key:"getTextNodes",value:function(e){var n=this,t="",a=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,function(r){a.push({start:t.length,end:(t+=r.textContent).length,node:r})},function(r){return n.matchesExclude(r.parentNode)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},function(){e({value:t,nodes:a})})}},{key:"matchesExclude",value:function(e){return x.matches(e,this.opt.exclude.concat(["script","style","title","head","html"]))}},{key:"wrapRangeInTextNode",value:function(e,n,t){var a=this.opt.element?this.opt.element:"mark",r=e.splitText(n),s=r.splitText(t-n),i=document.createElement(a);return i.setAttribute("data-markjs","true"),this.opt.className&&i.setAttribute("class",this.opt.className),i.textContent=r.textContent,r.parentNode.replaceChild(i,r),s}},{key:"wrapRangeInMappedTextNode",value:function(e,n,t,a,r){var s=this;e.nodes.every(function(i,c){var u=e.nodes[c+1];if(typeof u>"u"||u.start>n){if(!a(i.node))return!1;var l=n-i.start,h=(t>i.end?i.end:t)-i.start,d=e.value.substr(0,i.start),p=e.value.substr(h+i.start);if(i.node=s.wrapRangeInTextNode(i.node,l,h),e.value=d+p,e.nodes.forEach(function(g,m){m>=c&&(e.nodes[m].start>0&&m!==c&&(e.nodes[m].start-=h),e.nodes[m].end-=h)}),t-=h,r(i.node.previousSibling,i.start),t>i.end)n=i.end;else return!1}return!0})}},{key:"wrapMatches",value:function(e,n,t,a,r){var s=this,i=n===0?0:n+1;this.getTextNodes(function(c){c.nodes.forEach(function(u){u=u.node;for(var l=void 0;(l=e.exec(u.textContent))!==null&&l[i]!=="";)if(t(l[i],u)){var h=l.index;if(i!==0)for(var d=1;d{for(var t in e)k(n,t,{get:e[t],enumerable:!0})};var g={};I(g,{OptionPure:()=>p,SelectPure:()=>o});function v(n){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?v=function(t){return typeof t}:v=function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},v(n)}function a(n,e,t){var i=t.value;if(typeof i!="function")throw new TypeError("@boundMethod decorator can only be applied to methods not: ".concat(v(i)));var r=!1;return{configurable:!0,get:function(){if(r||this===n.prototype||this.hasOwnProperty(e)||typeof i!="function")return i;var d=i.bind(this);return r=!0,Object.defineProperty(this,e,{configurable:!0,get:function(){return d},set:function(C){i=C,delete this[e]}}),r=!1,d},set:function(d){i=d}}}import{LitElement as _,html as $}from"lit";import{ifDefined as P}from"lit-html/directives/if-defined.js";import{customElement as D}from"lit/decorators/custom-element.js";import{property as f}from"lit/decorators/property.js";var b={ENTER:"Enter",TAB:"Tab"};var y=()=>{};var O={label:"",value:"",select:y,unselect:y,disabled:!1,hidden:!1,selected:!1};import{css as E}from"lit";var x=E` + .select-wrapper { + position: relative; + } + .select { + bottom: 0; + display: flex; + flex-wrap: wrap; + left: 0; + position: absolute; + right: 0; + top: 0; + width: var(--select-width, 100%); + } + .label:focus { + outline: var(--select-outline, 2px solid #e3e3e3); + } + .label:after { + border-bottom: 1px solid var(--color, #000); + border-right: 1px solid var(--color, #000); + box-sizing: border-box; + content: ""; + display: block; + height: 10px; + margin-top: -2px; + transform: rotate(45deg); + transition: 0.2s ease-in-out; + width: 10px; + } + .label.visible:after { + margin-bottom: -4px; + margin-top: 0; + transform: rotate(225deg); + } + select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + position: relative; + opacity: 0; + } + select[multiple] { + z-index: 0; + } + select, + .label { + align-items: center; + background-color: var(--background-color, #fff); + border-radius: var(--border-radius, 4px); + border: var(--border-width, 1px) solid var(--border-color, #000); + box-sizing: border-box; + color: var(--color, #000); + cursor: pointer; + display: flex; + font-family: var(--font-family, inherit); + font-size: var(--font-size, 14px); + font-weight: var(--font-weight, 400); + min-height: var(--select-height, 44px); + justify-content: space-between; + padding: var(--padding, 0 10px); + width: 100%; + z-index: 1; + } + @media only screen and (hover: none) and (pointer: coarse){ + select { + z-index: 2; + } + } + .dropdown { + background-color: var(--border-color, #000); + border-radius: var(--border-radius, 4px); + border: var(--border-width, 1px) solid var(--border-color, #000); + display: none; + flex-direction: column; + gap: var(--border-width, 1px); + justify-content: space-between; + max-height: calc(var(--select-height, 44px) * var(--dropdown-items, 4) + var(--border-width, 1px) * calc(var(--dropdown-items, 4) - 1)); + overflow-y: scroll; + position: absolute; + top: calc(var(--select-height, 44px) + var(--dropdown-gap, 0px)); + width: calc(100% - var(--border-width, 1px) * 2); + z-index: var(--dropdown-z-index, 2); + } + .dropdown.visible { + display: flex; + z-index: 100; + } + .disabled { + background-color: var(--disabled-background-color, #bdc3c7); + color: var(--disabled-color, #ecf0f1); + cursor: default; + } + .multi-selected { + background-color: var(--selected-background-color, #e3e3e3); + border-radius: var(--border-radius, 4px); + color: var(--selected-color, #000); + display: flex; + gap: 8px; + justify-content: space-between; + padding: 2px 4px; + } + .multi-selected-wrapper { + display: flex; + flex-wrap: wrap; + gap: 4px; + width: calc(100% - 30px); + } + .cross:after { + content: '\\00d7'; + display: inline-block; + height: 100%; + text-align: center; + width: 12px; + } +`;import{css as L}from"lit";var S=L` + .option { + align-items: center; + background-color: var(--background-color, #fff); + box-sizing: border-box; + color: var(--color, #000); + cursor: pointer; + display: flex; + font-family: var(--font-family, inherit); + font-size: var(--font-size, 14px); + font-weight: var(--font-weight, 400); + min-height: var(--select-height, 44px); + justify-content: flex-start; + padding: var(--padding, 0 10px); + width: 100%; + } + .option:not(.disabled):focus, .option:not(.disabled):not(.selected):hover { + background-color: var(--hover-background-color, #e3e3e3); + color: var(--hover-color, #000); + } + .selected { + background-color: var(--selected-background-color, #e3e3e3); + color: var(--selected-color, #000); + } + .disabled { + background-color: var(--disabled-background-color, #e3e3e3); + color: var(--disabled-color, #000); + cursor: default; + } +`;var u=function(n,e,t,i){var r=arguments.length,s=r<3?e:i===null?i=Object.getOwnPropertyDescriptor(e,t):i,d;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")s=Reflect.decorate(n,e,t,i);else for(var h=n.length-1;h>=0;h--)(d=n[h])&&(s=(r<3?d(s):r>3?d(e,t,s):d(e,t))||s);return r>3&&s&&Object.defineProperty(e,t,s),s},p=class extends _{constructor(){super(...arguments),this.isSelected=!1,this.isDisabled=!1,this.isHidden=!1,this.optionValue="",this.displayedLabel="",this.optionIndex=-1}static get styles(){return S}connectedCallback(){super.connectedCallback(),this.isSelected=this.getAttribute("selected")!==null,this.isDisabled=this.getAttribute("disabled")!==null,this.isHidden=this.getAttribute("hidden")!==null,this.optionValue=this.getAttribute("value")||"",this.assignDisplayedLabel(),this.fireOnReadyCallback()}getOption(){return{label:this.displayedLabel,value:this.optionValue,select:this.select,unselect:this.unselect,selected:this.isSelected,disabled:this.isDisabled,hidden:this.isHidden}}select(){this.isSelected=!0,this.setAttribute("selected","")}unselect(){this.isSelected=!1,this.removeAttribute("selected")}setOnReadyCallback(e,t){this.onReady=e,this.optionIndex=t}setOnSelectCallback(e){this.onSelect=e}render(){let e=["option"];return this.isSelected&&e.push("selected"),this.isDisabled&&e.push("disabled"),$` +
    + + ${this.displayedLabel} +
    + `}assignDisplayedLabel(){if(this.textContent){this.displayedLabel=this.textContent;return}this.getAttribute("label")&&(this.displayedLabel=this.getAttribute("label")||"")}fireOnReadyCallback(){this.onReady&&this.onReady(this.getOption(),this.optionIndex)}fireOnSelectCallback(e){e.stopPropagation(),!(!this.onSelect||this.isDisabled)&&this.onSelect(this.optionValue)}fireOnSelectIfEnterPressed(e){e.key===b.ENTER&&this.fireOnSelectCallback(e)}};u([f()],p.prototype,"isSelected",void 0);u([f()],p.prototype,"isDisabled",void 0);u([f()],p.prototype,"isHidden",void 0);u([f()],p.prototype,"optionValue",void 0);u([f()],p.prototype,"displayedLabel",void 0);u([f()],p.prototype,"optionIndex",void 0);u([a],p.prototype,"getOption",null);u([a],p.prototype,"select",null);u([a],p.prototype,"unselect",null);u([a],p.prototype,"fireOnReadyCallback",null);p=u([D("option-pure")],p);import{LitElement as A,html as m}from"lit";import{ifDefined as w}from"lit-html/directives/if-defined.js";import{customElement as R}from"lit/decorators/custom-element.js";import{property as c}from"lit/decorators/property.js";import{query as z}from"lit/decorators/query.js";var l=function(n,e,t,i){var r=arguments.length,s=r<3?e:i===null?i=Object.getOwnPropertyDescriptor(e,t):i,d;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")s=Reflect.decorate(n,e,t,i);else for(var h=n.length-1;h>=0;h--)(d=n[h])&&(s=(r<3?d(s):r>3?d(e,t,s):d(e,t))||s);return r>3&&s&&Object.defineProperty(e,t,s),s},o=class extends A{constructor(){super(...arguments),this.options=[],this.visible=!1,this.selectedOption=O,this._selectedOptions=[],this.disabled=!1,this.isMultipleSelect=!1,this.name="",this._id="",this.formName="",this.value="",this.values=[],this.defaultLabel="",this.totalRenderedChildOptions=-1,this.form=null,this.hiddenInput=null}static get styles(){return x}connectedCallback(){super.connectedCallback(),this.disabled=this.getAttribute("disabled")!==null,this.isMultipleSelect=this.getAttribute("multiple")!==null,this.name=this.getAttribute("name")||"",this._id=this.getAttribute("id")||"",this.formName=this.name||this.id,this.defaultLabel=this.getAttribute("default-label")||""}open(){this.disabled||(this.visible=!0,this.removeEventListeners(),document.body.addEventListener("click",this.close,!0))}close(e){e&&this.contains(e.target)||(this.visible=!1,this.removeEventListeners())}enable(){this.disabled=!1}disable(){this.disabled=!0}get selectedIndex(){var e;return(e=this.nativeSelect)===null||e===void 0?void 0:e.selectedIndex}set selectedIndex(e){!e&&e!==0||this.selectOptionByValue(this.options[e].value)}get selectedOptions(){var e;return(e=this.nativeSelect)===null||e===void 0?void 0:e.selectedOptions}render(){let e=["label"];return this.disabled&&e.push("disabled"),this.visible&&e.push("visible"),m` +
    + +
    +
    + ${this.getDisplayedLabel()} +
    + +
    +
    + `}handleNativeSelectChange(){var e;this.selectedIndex=(e=this.nativeSelect)===null||e===void 0?void 0:e.selectedIndex}getNativeOptionsHtml(){return this.options.map(this.getSingleNativeOptionHtml)}getSingleNativeOptionHtml({value:e,label:t,hidden:i,disabled:r}){return m` + + `}isOptionSelected(e){let t=this.selectedOption.value===e;return this.isMultipleSelect&&(t=!!this._selectedOptions.find(i=>i.value===e)),t}openDropdownIfProperKeyIsPressed(e){(e.key===b.ENTER||e.key===b.TAB)&&this.open()}getDisplayedLabel(){return this.isMultipleSelect&&this._selectedOptions.length?this.getMultiSelectLabelHtml():this.selectedOption.label||this.defaultLabel}getMultiSelectLabelHtml(){return m` +
    + ${this._selectedOptions.map(this.getMultiSelectSelectedOptionHtml)} +
    + `}getMultiSelectSelectedOptionHtml({label:e,value:t}){return m` + + ${e} + this.fireOnSelectCallback(i,t)} + > + + + `}fireOnSelectCallback(e,t){e.stopPropagation(),this.selectOptionByValue(t)}initializeSelect(){this.processChildOptions(),this.selectDefaultOptionIfNoneSelected(),this.appendHiddenInputToClosestForm()}processChildOptions(){let e=this.querySelectorAll("option-pure");this.totalRenderedChildOptions=e.length;for(let t=0;ti===e);t&&this.setSelectValue(t)}setSelectValue(e){this.isMultipleSelect?this.setMultiSelectValue(e):this.setSingleSelectValue(e),this.updateHiddenInputInForm(),this.dispatchChangeEvent()}dispatchChangeEvent(){this.dispatchEvent(new Event("change"))}setMultiSelectValue(e){let t=this._selectedOptions.indexOf(e);t!==-1?(this.values.splice(t,1),this._selectedOptions.splice(t,1),e.unselect()):(this.values.push(e.value),this._selectedOptions.push(e),e.select()),this.requestUpdate()}setSingleSelectValue(e){this.unselectAllOptions(),this.close(),this.selectedOption=e,this.value=e.value,e.select()}updateHiddenInputInForm(){if(!this.form||!this.hiddenInput)return;this.hiddenInput.value=this.isMultipleSelect?this.values.join(","):this.value;let e=new Event("change",{bubbles:!0});this.hiddenInput.dispatchEvent(e)}};l([c()],o.prototype,"options",void 0);l([c()],o.prototype,"visible",void 0);l([c()],o.prototype,"selectedOption",void 0);l([c()],o.prototype,"_selectedOptions",void 0);l([c()],o.prototype,"disabled",void 0);l([c()],o.prototype,"isMultipleSelect",void 0);l([c()],o.prototype,"name",void 0);l([c()],o.prototype,"_id",void 0);l([c()],o.prototype,"formName",void 0);l([c()],o.prototype,"value",void 0);l([c()],o.prototype,"values",void 0);l([c()],o.prototype,"defaultLabel",void 0);l([c()],o.prototype,"totalRenderedChildOptions",void 0);l([z("select")],o.prototype,"nativeSelect",void 0);l([a],o.prototype,"close",null);l([a],o.prototype,"getSingleNativeOptionHtml",null);l([a],o.prototype,"getMultiSelectLabelHtml",null);l([a],o.prototype,"getMultiSelectSelectedOptionHtml",null);l([a],o.prototype,"initializeSelect",null);l([a],o.prototype,"initializeSingleOption",null);l([a],o.prototype,"removeEventListeners",null);l([a],o.prototype,"appendHiddenInputToClosestForm",null);l([a],o.prototype,"selectOptionByValue",null);o=l([R("select-pure")],o);var xe=g;export{xe as default}; diff --git a/Resources/Public/JavaScript/Contrib/style-mod.js b/Resources/Public/JavaScript/Contrib/style-mod.js new file mode 100644 index 0000000..deae39b --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/style-mod.js @@ -0,0 +1,3 @@ +const y="\u037C",m=typeof Symbol>"u"?"__"+y:Symbol.for(y),S=typeof Symbol>"u"?"__styleSet"+Math.floor(Math.random()*1e8):Symbol("styleSet"),g=typeof globalThis<"u"?globalThis:typeof window<"u"?window:{};class C{constructor(e,n){this.rules=[];let{finish:i}=n||{};function s(t){return/^@/.test(t)?[t]:t.split(/,\s*/)}function l(t,h,a,T){let f=[],r=/^@(\w+)\b/.exec(t[0]),c=r&&r[1]=="keyframes";if(r&&h==null)return a.push(t[0]+";");for(let u in h){let o=h[u];if(/&/.test(u))l(u.split(/,\s*/).map(d=>t.map(p=>d.replace(/&/,p))).reduce((d,p)=>d.concat(p)),o,a);else if(o&&typeof o=="object"){if(!r)throw new RangeError("The value of a property ("+u+") should be a primitive value.");l(s(u),o,f,c)}else o!=null&&f.push(u.replace(/_.*/,"").replace(/[A-Z]/g,d=>"-"+d.toLowerCase())+": "+o+";")}(f.length||c)&&a.push((i&&!r&&!T?t.map(i):t).join(", ")+" {"+f.join(" ")+"}")}for(let t in e)l(s(t),e[t],this.rules)}getRules(){return this.rules.join(` +`)}static newName(){let e=g[m]||1;return g[m]=e+1,y+e.toString(36)}static mount(e,n,i){let s=e[S],l=i&&i.nonce;s?l&&s.setNonce(l):s=new A(e,l),s.mount(Array.isArray(n)?n:[n])}}let w=new Map;class A{constructor(e,n){let i=e.ownerDocument||e,s=i.defaultView;if(!e.head&&e.adoptedStyleSheets&&s.CSSStyleSheet){let l=w.get(i);if(l)return e.adoptedStyleSheets=[l.sheet,...e.adoptedStyleSheets],e[S]=l;this.sheet=new s.CSSStyleSheet,e.adoptedStyleSheets=[this.sheet,...e.adoptedStyleSheets],w.set(i,this)}else{this.styleTag=i.createElement("style"),n&&this.styleTag.setAttribute("nonce",n);let l=e.head||e;l.insertBefore(this.styleTag,l.firstChild)}this.modules=[],e[S]=this}mount(e){let n=this.sheet,i=0,s=0;for(let l=0;l-1&&(this.modules.splice(h,1),s--,h=-1),h==-1){if(this.modules.splice(s++,0,t),n)for(let a=0;a",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"'};for(var n=typeof navigator<"u"&&/Chrome\/(\d+)/.exec(navigator.userAgent),y=typeof navigator<"u"&&/Gecko\/\d+/.test(navigator.userAgent),g=typeof navigator<"u"&&/Mac/.test(navigator.platform),d=typeof navigator<"u"&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent),c=g||n&&+n[1]<57,r=0;r<10;r++)t[48+r]=t[96+r]=String(r);for(var r=1;r<=24;r++)t[r+111]="F"+r;for(var r=65;r<=90;r++)t[r]=String.fromCharCode(r+32),a[r]=String.fromCharCode(r);for(var i in t)a.hasOwnProperty(i)||(a[i]=t[i]);function p(o){var f=c&&(o.ctrlKey||o.altKey||o.metaKey)||d&&o.shiftKey&&o.key&&o.key.length==1||o.key=="Unidentified",e=!f&&o.key||(o.shiftKey?a:t)[o.keyCode]||o.key||"Unidentified";return e=="Esc"&&(e="Escape"),e=="Del"&&(e="Delete"),e=="Left"&&(e="ArrowLeft"),e=="Up"&&(e="ArrowUp"),e=="Right"&&(e="ArrowRight"),e=="Down"&&(e="ArrowDown"),e}export{t as base,p as keyName,a as shift}; diff --git a/Resources/Public/JavaScript/action-button/abstract-action.js b/Resources/Public/JavaScript/action-button/abstract-action.js new file mode 100644 index 0000000..ea8324a --- /dev/null +++ b/Resources/Public/JavaScript/action-button/abstract-action.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class t{constructor(c){this.callback=c}}export{t as AbstractAction}; diff --git a/Resources/Public/JavaScript/action-button/deferred-action.js b/Resources/Public/JavaScript/action-button/deferred-action.js new file mode 100644 index 0000000..6f24015 --- /dev/null +++ b/Resources/Public/JavaScript/action-button/deferred-action.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{AbstractAction as s}from"@typo3/backend/action-button/abstract-action.js";import a from"@typo3/backend/icons.js";class i extends s{async execute(e){return e.dataset.actionLabel=e.innerText,e.classList.add("disabled"),a.getIcon("spinner-circle",a.sizes.small).then(t=>{e.innerHTML=t}),await this.executeCallback(e)}async executeCallback(e){return await Promise.resolve(this.callback()).finally(()=>{e.innerText=e.dataset.actionLabel,e.classList.remove("disabled")})}}export{i as default}; diff --git a/Resources/Public/JavaScript/action-button/immediate-action.js b/Resources/Public/JavaScript/action-button/immediate-action.js new file mode 100644 index 0000000..1c33e3b --- /dev/null +++ b/Resources/Public/JavaScript/action-button/immediate-action.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{AbstractAction as e}from"@typo3/backend/action-button/abstract-action.js";class t extends e{execute(){return this.executeCallback()}async executeCallback(){return Promise.resolve(this.callback())}}export{t as default}; diff --git a/Resources/Public/JavaScript/action-dispatcher.js b/Resources/Public/JavaScript/action-dispatcher.js new file mode 100644 index 0000000..708b545 --- /dev/null +++ b/Resources/Public/JavaScript/action-dispatcher.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import c from"@typo3/backend/info-window.js";import p from"@typo3/core/event/regular-event.js";import o from"@typo3/backend/window-manager.js";import d from"@typo3/backend/module-menu.js";import u from"@typo3/core/document-service.js";import l from"@typo3/backend/utility.js";class i{constructor(){this.delegates={},this.createDelegates(),u.ready().then(()=>this.registerEvents())}static resolveArguments(e){if(e.dataset.dispatchArgs){const t=e.dataset.dispatchArgs.replace(/"/g,'"'),a=JSON.parse(t);return a instanceof Array?l.trimItems(a):null}else if(e.dataset.dispatchArgsList){const t=e.dataset.dispatchArgsList.split(",");return l.trimItems(t)}return null}createDelegates(){this.delegates={"TYPO3.InfoWindow.showItem":c.showItem.bind(null),"TYPO3.WindowManager.localOpen":o.localOpen.bind(o),"TYPO3.ModuleMenu.showModule":d.App.showModule.bind(d.App)}}registerEvents(){new p("click",this.handleClickEvent.bind(this)).delegateTo(document,"[data-dispatch-action]")}handleClickEvent(e,t){e.preventDefault(),this.delegateTo(e,t)}delegateTo(e,t){if(t.hasAttribute("data-dispatch-disabled"))return;const r=t.dataset.dispatchAction;let s=i.resolveArguments(t);s instanceof Array&&(s=s.map(n=>{switch(n){case"{$target}":return t;case"{$event}":return e;default:return n}})),this.delegates[r]&&this.delegates[r].apply(null,s||[])}}var h=new i;export{h as default}; diff --git a/Resources/Public/JavaScript/ajax-data-handler.js b/Resources/Public/JavaScript/ajax-data-handler.js new file mode 100644 index 0000000..9b6b2fd --- /dev/null +++ b/Resources/Public/JavaScript/ajax-data-handler.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{BroadcastMessage as c}from"@typo3/backend/broadcast-message.js";import d from"@typo3/core/ajax/ajax-request.js";import i from"@typo3/backend/broadcast-service.js";import m from"@typo3/backend/notification.js";import{sudoModeInterceptor as p}from"@typo3/backend/security/sudo-mode-interceptor.js";class a{static call(e){return new d(TYPO3.settings.ajaxUrls.record_process).addMiddleware(p).withQueryArguments(e).get().then(async r=>await r.resolve())}async process(e,r){return a.call(e).then(s=>{if(s.hasErrors&&this.handleErrors(s),r){const o={...r,hasErrors:s.hasErrors},t=new c("datahandler","process",o);i.post(t);const n=new CustomEvent("typo3:datahandler:process",{detail:{payload:o}});document.dispatchEvent(n)}return s})}handleErrors(e){for(const r of e.messages)m.error(r.title,r.message)}}var l=new a;export{l as default}; diff --git a/Resources/Public/JavaScript/backend-exception.js b/Resources/Public/JavaScript/backend-exception.js new file mode 100644 index 0000000..1b4e02f --- /dev/null +++ b/Resources/Public/JavaScript/backend-exception.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class e{constructor(s="",c=0){this.message=s,this.code=c}}export{e as BackendException}; diff --git a/Resources/Public/JavaScript/bookmark/bookmark-manager.js b/Resources/Public/JavaScript/bookmark/bookmark-manager.js new file mode 100644 index 0000000..3cf0100 --- /dev/null +++ b/Resources/Public/JavaScript/bookmark/bookmark-manager.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as F,html as r,nothing as y}from"lit";import{property as T,state as w,customElement as D}from"lit/decorators.js";import{classMap as I}from"lit/directives/class-map.js";import{repeat as x}from"lit/directives/repeat.js";import p,{BookmarkStoreChangedEvent as z,BookmarkGroupType as S}from"@typo3/backend/bookmark/bookmark-store.js";import $ from"@typo3/backend/modal.js";import m from"@typo3/backend/notification.js";import{SeverityEnum as B}from"@typo3/backend/enum/severity.js";import{PseudoButtonLitElement as R}from"@typo3/backend/element/pseudo-button.js";import"@typo3/backend/element/spinner-element.js";import"@typo3/backend/element/icon-element.js";import a from"~labels/core.bookmarks";var g=function(k,e,t,o){var i=arguments.length,s=i<3?e:o===null?o=Object.getOwnPropertyDescriptor(e,t):o,l;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")s=Reflect.decorate(k,e,t,o);else for(var d=k.length-1;d>=0;d--)(l=k[d])&&(s=(i<3?l(s):i>3?l(e,t,s):l(e,t))||s);return i>3&&s&&Object.defineProperty(e,t,s),s};class M{constructor(){this.view="list"}}class V{constructor(e){this.bookmark=e,this.view="editBookmark"}}class E{constructor(){this.view="createGroup",this.draft={label:""}}}class L{constructor(e){this.group=e,this.view="editGroup"}}class C{constructor(){this.view="manageGroups"}}let h=class extends F{constructor(){super(...arguments),this.editId=null,this.bookmarks=[],this.groups=[],this.selectedIds=new Set,this.draggedItem=null,this.dropTarget=null,this.groupedBookmarks=new Map,this.viewState=new M,this.handleStoreUpdate=()=>{this.syncFromStore()},this.handleBookmarkBulkDelete=()=>{const e=Array.from(this.selectedIds),t=$.confirm(a.get("confirmDeleteMultiple.title"),a.get("confirmDeleteMultiple.message",[e.length]),B.notice,[{text:a.get("action.cancel"),btnClass:"btn-default",name:"cancel",trigger:()=>t.hideModal()},{text:a.get("action.delete"),btnClass:"btn-primary",name:"delete",trigger:async()=>{await p.deleteMultiple(e)?(m.success(a.get("success.deletedMultiple.title"),a.get("success.deletedMultiple.message",[e.length])),this.selectedIds=new Set):m.error(a.get("error.deleteFailed.title"),a.get("error.deleteFailed.message")),t.hideModal()}}])},this.handleDragOver=e=>{e.preventDefault(),e.dataTransfer&&(e.dataTransfer.dropEffect="move")},this.handleItemDragLeave=e=>{const t=e.relatedTarget;t&&e.currentTarget.contains(t)||(this.dropTarget=null)},this.handleDragEnd=()=>{this.draggedItem=null,this.dropTarget=null}}connectedCallback(){super.connectedCallback(),document.addEventListener(z,this.handleStoreUpdate),this.initFromStore()}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(z,this.handleStoreUpdate)}createRenderRoot(){return this}render(){let e;return this.viewState instanceof V?e=this.renderBookmarkEditView(this.viewState):this.viewState instanceof C?e=this.renderGroupListView():this.viewState instanceof E?e=this.renderGroupCreateView(this.viewState):this.viewState instanceof L?e=this.renderGroupEditView(this.viewState):e=this.renderBookmarkListView(),r`
    ${e.toolbar}
    ${e.content}
    `}async initFromStore(){if(await this.syncFromStore(),this.editId!==null){const e=this.bookmarks.find(t=>t.id===this.editId);e&&this.navigateToBookmarkEditView(e)}}navigateToBookmarkListView(){this.viewState=new M}navigateToBookmarkEditView(e){this.viewState=new V(e)}navigateToGroupListView(){this.viewState=new C}navigateToGroupCreateView(){this.viewState=new E}navigateToGroupEditView(e){this.viewState=new L(e)}renderBookmarkListView(){const e=this.groupedBookmarks,t=this.selectedIds.size>0,o=this.bookmarks.length>0,i=this.selectedIds.size===this.bookmarks.length&&this.bookmarks.length>0,s=this.helperGetGroupSections(this.getSelectableGroups()),l=r`
    {this.selectedIds=n.target.checked?new Set(this.bookmarks.map(b=>b.id)):new Set}}>
    ${t?r`
    `:y}
    `,d=r`${o?y:r`
    ${a.get("empty")}
    `} ${x(Array.from(e.entries()),([n])=>n,([n,b])=>{const u=this.groups.find(v=>v.id===n)?.label||a.get("notGrouped"),c=`bookmark-group-${typeof n=="number"?n:n.replace(/-/g,"")}`;return r`
    this.handleDrop(v,n)}>
    ${x(b,v=>v.id,v=>this.renderBookmarkRow(v))}
    `})}`;return{toolbar:l,content:d}}renderBookmarkRow(e){const t=this.selectedIds.has(e.id),o=this.draggedItem?.id===e.id,i=this.dropTarget?.item.id===e.id,s=this.dropTarget?.position,l=I({"table-active":t,"opacity-50":o,"row-drop-before":i&&s==="before","row-drop-after":i&&s==="after"}),d=()=>{const c=new Set(this.selectedIds);c.has(e.id)?c.delete(e.id):c.add(e.id),this.selectedIds=c},n=this.groupedBookmarks.get(e.groupId)??[],b=n.findIndex(c=>c.id===e.id),f=b===0,u=b===n.length-1;return r`this.handleDragStart(c,"bookmark",e)} @dragover=${c=>this.handleItemDragOver(c,e)} @dragleave=${this.handleItemDragLeave} @drop=${c=>this.handleDrop(c,e)} @dragend=${this.handleDragEnd}>${e.editable?r` c.stopPropagation()}> `:r``}${e.accessible?r``:r`${e.title}`}${e.editable?r`
    ${f?r` `:r``} ${u?r` `:r``} ${a.get("manager.dragToReorder")}
    `:y}`}renderBookmarkEditView(e){const{bookmark:t}=e,o=r`
    `,i=this.helperParseParameters(t.arguments),s=this.helperGetGroupSections(this.getSelectableGroups()),l=r`
    this.handleBookmarkUpdate(d,t)}>
    {t.title=d.target.value,this.requestUpdate()}} required>
    ${t.href?r``:y}${i!==null?r``:y}
    ${a.get("details.url")}${this.helperStripToken(t.href)}
    ${a.get("details.route")}${t.route}
    ${a.get("details.parameters")}${i}
    `;return{toolbar:o,content:l}}renderGroupListView(){const e=this.groups.filter(i=>i.editable),t=r`
    `,o=r`
    ${e.length>0?r`
    ${e.map(i=>this.renderGroupRow(i))}
    `:r`
    ${a.get("manager.noGroups")}
    `}
    `;return{toolbar:t,content:o}}renderGroupRow(e){const t=this.bookmarks.filter(u=>u.groupId===e.id).length,o=this.draggedItem?.id===e.id,i=this.dropTarget?.item.id===e.id,s=this.dropTarget?.position,l=I({"opacity-50":o,"row-drop-before":i&&s==="before","row-drop-after":i&&s==="after"}),d=this.groups.filter(u=>u.editable),n=d.findIndex(u=>u.id===e.id),b=n===0,f=n===d.length-1;return r`this.handleDragStart(u,"group",e)} @dragover=${u=>this.handleItemDragOver(u,e)} @dragleave=${this.handleItemDragLeave} @drop=${u=>this.handleDrop(u,e)} @dragend=${this.handleDragEnd}>
    ${b?r` `:r``} ${f?r` `:r``} ${a.get("manager.dragToReorder")}
    `}renderGroupCreateView(e){const{draft:t}=e,o=r`
    `,i=r`
    this.handleGroupCreate(s,t)}>
    {t.label=s.target.value,this.requestUpdate()}} required autofocus>
    `;return{toolbar:o,content:i}}renderGroupEditView(e){const{group:t}=e,o=r`
    `,i=r`
    this.handleGroupUpdate(s,t)}>
    {t.label=s.target.value,this.requestUpdate()}} required>
    `;return{toolbar:o,content:i}}async syncFromStore(){this.bookmarks=await p.getBookmarks(),this.groups=await p.getGroups(),this.groupedBookmarks=await p.getGroupedBookmarks()}getSelectableGroups(){return this.groups.filter(e=>e.selectable)}handleBookmarkNavigate(e){this.closest("typo3-backend-modal")?.hideModal(),p.navigate(e)}async handleBookmarkUpdate(e,t){e.preventDefault();const o=await p.update(t.id,t.title,t.groupId);o.success?(m.success(a.get("success.updated.title"),a.get("success.updated.message")),this.navigateToBookmarkListView()):m.error(a.get("error.updateFailed.title"),o.error||a.get("error.unknown.message"))}handleBookmarkDelete(e){const t=$.confirm(a.get("confirmDelete.title"),a.get("confirmDelete.message"),B.notice,[{text:a.get("action.cancel"),btnClass:"btn-default",name:"cancel",trigger:()=>t.hideModal()},{text:a.get("action.delete"),btnClass:"btn-primary",name:"delete",trigger:async()=>{await p.deleteMultiple([e])?(m.success(a.get("success.deleted.title"),a.get("success.deleted.message")),this.navigateToBookmarkListView()):m.error(a.get("error.deleteFailed.title"),a.get("error.deleteFailed.message")),t.hideModal()}}])}async handleBookmarkBulkMove(e){const t=Array.from(this.selectedIds);await p.move(t,e)?(m.success(a.get("success.moved.title"),a.get("success.moved.message",[t.length])),this.selectedIds=new Set):m.error(a.get("error.moveFailed.title"),a.get("error.moveFailed.message"))}async handleBookmarkMoveUp(e){await this.reorderBookmarkRelative(e,-1),await this.restoreFocus("bookmark",e.id,"up")}async handleBookmarkMoveDown(e){await this.reorderBookmarkRelative(e,1),await this.restoreFocus("bookmark",e.id,"down")}async handleGroupMoveUp(e){await this.reorderGroupRelative(e,-1),await this.restoreFocus("group",e.id,"up")}async handleGroupMoveDown(e){await this.reorderGroupRelative(e,1),await this.restoreFocus("group",e.id,"down")}async handleGroupCreate(e,t){if(e.preventDefault(),!t.label.trim())return;const o=await p.createGroup(t.label.trim());o.success?(m.success(a.get("success.groupCreated.title"),a.get("success.groupCreated.message")),this.navigateToGroupListView()):m.error(a.get("error.groupCreateFailed.title"),o.error||a.get("error.groupCreateFailed.message"))}async handleGroupUpdate(e,t){if(e.preventDefault(),!t.label.trim()||!t.editable)return;const o=await p.updateGroup(t.id,t.label.trim());o.success?(m.success(a.get("success.groupUpdated.title"),a.get("success.groupUpdated.message")),this.navigateToGroupListView()):m.error(a.get("error.groupUpdateFailed.title"),o.error||a.get("error.groupUpdateFailed.message"))}handleGroupDelete(e){if(!e.editable)return;const t=$.confirm(a.get("confirmDeleteGroup.title"),a.get("confirmDeleteGroup.message",[e.label]),B.notice,[{text:a.get("action.cancel"),btnClass:"btn-default",name:"cancel",trigger:()=>t.hideModal()},{text:a.get("action.delete"),btnClass:"btn-primary",name:"delete",trigger:async()=>{const o=await p.deleteGroup(e.id);o.success?(m.success(a.get("success.groupDeleted.title"),a.get("success.groupDeleted.message")),this.navigateToGroupListView()):m.error(a.get("error.groupDeleteFailed.title"),o.error||a.get("error.groupDeleteFailed.message")),t.hideModal()}}])}async restoreFocus(e,t,o){await this.updateComplete;const i=e==="bookmark"?`tr[data-bookmark-id="${t}"]`:`tr[data-group-id="${t}"]`,s=this.querySelector(i);if(!s)return;const l=o==="up"?"move-up":"move-down",d=o==="up"?"move-down":"move-up";(s.querySelector(`button[data-action="${l}"]`)??s.querySelector(`button[data-action="${d}"]`))?.focus()}async reorderBookmarkRelative(e,t){const o=[...this.groupedBookmarks.get(e.groupId)??[]],i=o.findIndex(l=>l.id===e.id),s=i+t;i<0||s<0||s>=o.length||([o[i],o[s]]=[o[s],o[i]],await p.reorder(o.map(l=>l.id)),await this.syncFromStore())}async reorderBookmarkToPosition(e,t,o){e.groupId!==t&&await p.update(e.id,e.title,t);const i=[...this.groupedBookmarks.get(t)??[]].filter(s=>s.id!==e.id);i.splice(o,0,e),await p.reorder(i.map(s=>s.id)),await this.syncFromStore()}async reorderGroupRelative(e,t){const o=this.groups.filter(l=>l.editable),i=o.findIndex(l=>l.id===e.id),s=i+t;i<0||s<0||s>=o.length||([o[i],o[s]]=[o[s],o[i]],await p.reorderGroups(o.map(l=>l.id)),await this.syncFromStore())}async reorderGroupToPosition(e,t){const o=this.groups.filter(i=>i.editable&&i.id!==e.id);o.splice(t,0,e),await p.reorderGroups(o.map(i=>i.id)),await this.syncFromStore()}handleDragStart(e,t,o){if(this.draggedItem=o,e.dataTransfer){e.dataTransfer.effectAllowed="move";const i={type:t,id:o.id};e.dataTransfer.setData("application/json",JSON.stringify(i))}}handleItemDragOver(e,t){if(e.preventDefault(),e.stopPropagation(),this.draggedItem===null||this.draggedItem.id===t.id)return;e.dataTransfer&&(e.dataTransfer.dropEffect="move");const o=e.currentTarget.getBoundingClientRect(),i=o.top+o.height/2,s=e.clientYn.id!==e.id).findIndex(n=>n.id===i.id),d=o==="before"?l:l+1;await this.reorderBookmarkToPosition(e,i.groupId,d)}else{const i=t;if(e.groupId!==i){const s=[...this.groupedBookmarks.get(i)??[]];await this.reorderBookmarkToPosition(e,i,s.length)}}}async handleDropGroup(e,t,o){if(e.id===t.id)return;const s=this.groups.filter(d=>d.editable&&d.id!==e.id).findIndex(d=>d.id===t.id),l=o==="before"?s:s+1;await this.reorderGroupToPosition(e,l)}helperGetGroupSections(e){const t=Map.groupBy(e,i=>i.type),o={[S.USER]:a.get("groupType.user"),[S.SYSTEM]:a.get("groupType.system"),[S.GLOBAL]:a.get("groupType.global")};return Array.from(t.entries()).map(([i,s])=>({label:o[i]??i,groups:s}))}helperParseParameters(e){if(!e)return null;try{const t=Object.entries(JSON.parse(e));if(t.length===0)return null;const o=i=>typeof i=="object"&&i!==null?JSON.stringify(i):String(i);return r`
      ${t.map(([i,s])=>r`
    • ${i}: ${o(s)}
    • `)}
    `}catch{return r`${e}`}}helperStripToken(e){try{const t=new URL(e,window.location.origin);return t.searchParams.delete("token"),decodeURIComponent(t.pathname+t.search)}catch{return e}}};g([T({type:Number})],h.prototype,"editId",void 0),g([w()],h.prototype,"bookmarks",void 0),g([w()],h.prototype,"groups",void 0),g([w()],h.prototype,"selectedIds",void 0),g([w()],h.prototype,"draggedItem",void 0),g([w()],h.prototype,"dropTarget",void 0),g([w()],h.prototype,"groupedBookmarks",void 0),g([w()],h.prototype,"viewState",void 0),h=g([D("typo3-backend-bookmark-manager-content")],h);let G=class extends R{createRenderRoot(){return this}buttonActivated(){const t=(top?.document??document).createElement("typo3-backend-bookmark-manager-content");this.editId!==void 0&&(t.editId=this.editId),$.advanced({type:$.types.default,title:a.get("manage"),size:$.sizes.medium,severity:B.notice,content:t,buttons:[],staticBackdrop:!0})}};g([T({type:Number,attribute:"edit-id"})],G.prototype,"editId",void 0),G=g([D("typo3-backend-bookmark-manager-button")],G);export{G as BookmarkManagerButtonElement,h as BookmarkManagerContentElement}; diff --git a/Resources/Public/JavaScript/bookmark/bookmark-store.js b/Resources/Public/JavaScript/bookmark/bookmark-store.js new file mode 100644 index 0000000..9f6e83a --- /dev/null +++ b/Resources/Public/JavaScript/bookmark/bookmark-store.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import n from"@typo3/core/ajax/ajax-request.js";import i from"~labels/core.bookmarks";import g from"~labels/core.mod_web_list";import F from"@typo3/backend/modal.js";import p from"@typo3/backend/notification.js";import{SeverityEnum as _}from"@typo3/backend/enum/severity.js";import{BroadcastMessage as B}from"@typo3/backend/broadcast-message.js";import E from"@typo3/backend/broadcast-service.js";const h="typo3:bookmark-store:changed";var k;(function(c){c.SYSTEM="system",c.GLOBAL="global",c.USER="user"})(k||(k={}));class f{constructor(){this.bookmarks=new Map,this.groups=[],this.isLoaded=!1,this.loadPromise=null,document.addEventListener("typo3:bookmark:broadcast",e=>this.handleBroadcast(e))}getBookmarkKey(e,r){return`${e}::${r}`}async isBookmarked(e){return await this.getBookmark(e)!==null}async getBookmark(e){await this.ready();for(const r of this.bookmarks.values())if(this.getBookmarkKey(r.route,r.arguments)===e)return r;return null}async getBookmarks(){return await this.ready(),[...this.bookmarks.values()]}async getGroups(){return await this.ready(),[...this.groups]}async getGroupedBookmarks(e){let r=await this.getBookmarks();e?.accessibleOnly&&(r=r.filter(a=>a.accessible)),e?.groupId!==void 0&&(r=r.filter(a=>a.groupId===e.groupId)),e?.limit!==void 0&&e.limit>0&&r.length>e.limit&&(r=r.slice(0,e.limit));const o=new Map,t=new Map,s=new Map;this.groups.forEach(a=>{t.set(a.id,a.priority),s.set(a.id,a.sorting)});const b=[...new Set(r.map(a=>a.groupId))].sort((a,l)=>{const d=t.get(a)??Number.MAX_SAFE_INTEGER,y=t.get(l)??Number.MAX_SAFE_INTEGER,m=d-y;if(m!==0)return m;const w=s.get(a)??Number.MAX_SAFE_INTEGER,v=s.get(l)??Number.MAX_SAFE_INTEGER;return w-v});for(const a of b){const l=r.filter(d=>d.groupId===a);l.length>0&&o.set(a,l)}return o}initialize(e,r){this.isLoaded||this.load({bookmarks:e,groups:r})}async create(e,r,o){try{const s=await(await new n(TYPO3.settings.ajaxUrls.bookmark_create).post({routeIdentifier:e,arguments:r,displayName:o||""})).resolve();return s.success&&s.bookmark&&(this.bookmarks.set(s.bookmark.id,s.bookmark),this.notifyChanged()),s}catch(t){return console.error("Failed to create bookmark:",t),{success:!1,error:i.get("error.createFailed.message")}}}async update(e,r,o){try{const s=await(await new n(TYPO3.settings.ajaxUrls.bookmark_update).post({bookmarkId:e,bookmarkTitle:r,bookmarkGroup:o})).resolve();return s.success&&s.bookmark&&(this.bookmarks.set(s.bookmark.id,s.bookmark),this.notifyChanged()),s}catch(t){return console.error("Failed to update bookmark:",t),{success:!1,error:i.get("error.updateFailed.message")}}}requestDelete(e){if(!this.bookmarks.get(e))return;const o=F.confirm(i.get("delete"),i.get("confirmDelete.title"),_.warning,[{text:g.get("button.cancel"),btnClass:"btn-default",name:"cancel",trigger:()=>o.hideModal()},{text:g.get("button.delete"),btnClass:"btn-danger",name:"delete",trigger:async()=>{o.hideModal();const t=await this.delete(e);t.success?p.success(i.get("success.deleted.title"),i.get("success.deleted.message")):p.error(i.get("error.deleteFailed.title"),t.error||i.get("error.deleteFailed.message"))}}])}async delete(e){try{const o=await(await new n(TYPO3.settings.ajaxUrls.bookmark_delete).post({bookmarkId:e})).resolve();return o.success&&(this.bookmarks.delete(e),this.notifyChanged()),o}catch(r){return console.error("Failed to delete bookmark:",r),{success:!1,error:i.get("error.deleteFailed.message")}}}async reorder(e){try{const o=await(await new n(TYPO3.settings.ajaxUrls.bookmark_reorder).post({bookmarkIds:e})).resolve();if(o.success&&o.bookmarks){this.bookmarks.clear();for(const t of o.bookmarks)this.bookmarks.set(t.id,t);return this.notifyChanged(),!0}return!1}catch(r){return console.error("Failed to reorder bookmarks:",r),!1}}async deleteMultiple(e){try{if((await(await new n(TYPO3.settings.ajaxUrls.bookmark_delete_multiple).post({bookmarkIds:e})).resolve()).success){for(const t of e)this.bookmarks.delete(t);return this.notifyChanged(),!0}return!1}catch(r){return console.error("Failed to delete multiple bookmarks:",r),!1}}async move(e,r){try{if((await(await new n(TYPO3.settings.ajaxUrls.bookmark_move).post({bookmarkIds:e,groupId:r})).resolve()).success){for(const s of e){const u=this.bookmarks.get(s);u&&this.bookmarks.set(s,{...u,groupId:r})}return this.notifyChanged(),!0}return!1}catch(o){return console.error("Failed to move bookmarks:",o),!1}}async refresh(){this.isLoaded=!1,this.loadPromise=null,await this.fetchAll()}async createGroup(e){try{const o=await(await new n(TYPO3.settings.ajaxUrls.bookmark_group_create).post({label:e})).resolve();return o.success&&o.group&&(this.groups.push(o.group),this.notifyChanged()),o}catch(r){return console.error("Failed to create group:",r),{success:!1,error:i.get("error.groupCreateFailed.message")}}}async updateGroup(e,r){try{const t=await(await new n(TYPO3.settings.ajaxUrls.bookmark_group_update).post({uuid:e,label:r})).resolve();return t.success&&t.groups&&(this.groups=t.groups,this.notifyChanged()),t}catch(o){return console.error("Failed to update group:",o),{success:!1,error:i.get("error.groupUpdateFailed.message")}}}async deleteGroup(e){try{const o=await(await new n(TYPO3.settings.ajaxUrls.bookmark_group_delete).post({uuid:e})).resolve();return o.success&&o.groups&&(this.groups=o.groups,await this.refresh(),this.notifyChanged()),o}catch(r){return console.error("Failed to delete group:",r),{success:!1,error:i.get("error.groupDeleteFailed.message")}}}async reorderGroups(e){try{const o=await(await new n(TYPO3.settings.ajaxUrls.bookmark_group_reorder).post({uuids:e})).resolve();return o.success&&o.groups&&(this.groups=o.groups,this.notifyChanged()),o}catch(r){return console.error("Failed to reorder groups:",r),{success:!1,error:i.get("error.groupReorderFailed.message")}}}navigate(e){const r=document.querySelector("typo3-backend-module-router");if(r===null)throw new Error("Router not available.");r.setAttribute("endpoint",e.href),r.setAttribute("module",e.module)}async ready(){this.isLoaded||await this.fetchAll()}async fetchAll(){return this.isLoaded?[...this.bookmarks.values()]:(this.loadPromise||(this.loadPromise=this.doFetch()),await this.loadPromise,[...this.bookmarks.values()])}sortGroups(e){return e.sort((r,o)=>{const t=r.priority-o.priority;return t!==0?t:r.sorting-o.sorting})}handleBroadcast(e){this.load(e.detail.payload),this.notifyChanged(!1)}load(e){this.bookmarks.clear();for(const r of e.bookmarks)this.bookmarks.set(r.id,r);this.groups=this.sortGroups(e.groups),this.isLoaded=!0}notifyChanged(e=!0){const r=new CustomEvent(h);document.dispatchEvent(r);for(let o=0;o=0;h--)(l=c[h])&&(i=(d<3?l(i):d>3?l(e,o,i):l(e,o))||i);return d>3&&i&&Object.defineProperty(e,o,i),i};let t=class extends y{constructor(){super(...arguments),this.route="",this.arguments="",this.displayName="",this.hideLabelText=!1,this.isBookmarked=!1,this.isProcessing=!1,this.handleStoreUpdate=async()=>{await this.checkIfBookmarked(),this.updateState()}}get bookmarkKey(){return n.getBookmarkKey(this.route,this.arguments)}connectedCallback(){super.connectedCallback(),document.addEventListener(u,this.handleStoreUpdate),this.setup()}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(u,this.handleStoreUpdate)}createRenderRoot(){return this}render(){const e=this.isBookmarked?s.get("action.remove"):s.get("action.create"),o=this.hideLabelText?m`${e}`:m`${e}`;if(this.isProcessing)return m`${o}`;const a=this.isBookmarked?"actions-bookmark-remove":"actions-bookmark-add";return m`${o}`}async buttonActivated(){if(!this.isProcessing){this.isProcessing=!0,this.updateState();try{if(this.isBookmarked){const e=await n.getBookmark(this.bookmarkKey);e&&n.requestDelete(e.id)}else{const e=await n.create(this.route,this.arguments,this.displayName);e.success?b.success(s.get("success.created.title"),s.get("success.created.message")):b.error(s.get("error.createFailed.title"),e.error||s.get("error.createFailed.message"))}}finally{this.isProcessing=!1,await this.checkIfBookmarked(),this.updateState()}}}async setup(){await this.checkIfBookmarked(),this.updateState()}async checkIfBookmarked(){this.isBookmarked=await n.isBookmarked(this.bookmarkKey)}updateState(){this.isProcessing?(this.setAttribute("aria-disabled","true"),this.tabIndex=-1):(this.removeAttribute("aria-disabled"),this.tabIndex=0),this.setAttribute("aria-pressed",this.isBookmarked?"true":"false"),this.title=this.isBookmarked?s.get("action.remove"):s.get("action.create")}};r([k({type:String})],t.prototype,"route",void 0),r([k({type:String})],t.prototype,"arguments",void 0),r([k({type:String,attribute:"display-name"})],t.prototype,"displayName",void 0),r([k({type:Boolean,attribute:"hide-label-text"})],t.prototype,"hideLabelText",void 0),r([p()],t.prototype,"isBookmarked",void 0),r([p()],t.prototype,"isProcessing",void 0),t=r([f("typo3-backend-bookmark-button")],t);export{t as BookmarkButtonElement}; diff --git a/Resources/Public/JavaScript/bookmark/toolbar/bookmark-menu-element.js b/Resources/Public/JavaScript/bookmark/toolbar/bookmark-menu-element.js new file mode 100644 index 0000000..d9cada2 --- /dev/null +++ b/Resources/Public/JavaScript/bookmark/toolbar/bookmark-menu-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as b,html as i,nothing as u}from"lit";import{state as h,customElement as f}from"lit/decorators.js";import{repeat as k}from"lit/directives/repeat.js";import c,{BookmarkStoreChangedEvent as g}from"@typo3/backend/bookmark/bookmark-store.js";import"@typo3/backend/element/icon-element.js";import"@typo3/backend/bookmark/bookmark-manager.js";import l from"~labels/core.bookmarks";var m=function(p,e,o,n){var a=arguments.length,t=a<3?e:n===null?n=Object.getOwnPropertyDescriptor(e,o):n,s;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(p,e,o,n);else for(var r=p.length-1;r>=0;r--)(s=p[r])&&(t=(a<3?s(t):a>3?s(e,o,t):s(e,o))||t);return a>3&&t&&Object.defineProperty(e,o,t),t};let d=class extends b{constructor(){super(...arguments),this.groupedBookmarks=new Map,this.groups=[],this.handleStoreUpdate=()=>{this.syncFromStore()}}connectedCallback(){super.connectedCallback(),document.addEventListener(g,this.handleStoreUpdate),this.syncFromStore()}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(g,this.handleStoreUpdate)}createRenderRoot(){return this}render(){return i`${this.renderContent()} ${l.get("manage")} `}async syncFromStore(){this.groupedBookmarks=await c.getGroupedBookmarks({accessibleOnly:!0}),this.groups=await c.getGroups()}renderContent(){if(this.groupedBookmarks.size===0)return i``;let e=!0;return i`${k(Array.from(this.groupedBookmarks.entries()),([o])=>o,([o,n])=>{const a=this.renderGroup(o,n,e);return e=!1,a})}`}renderGroup(e,o,n){const t=this.groups.find(r=>r.id===e)?.label,s=t||this.groups.length>1;return i`${n?u:i``} ${s?i``:u}`}renderBookmarkItem(e,o){return i`
  • `}handleNavigate(e){c.navigate(e)}};m([h()],d.prototype,"groupedBookmarks",void 0),m([h()],d.prototype,"groups",void 0),d=m([f("typo3-backend-bookmark-menu")],d);export{d as BookmarkMenuElement}; diff --git a/Resources/Public/JavaScript/broadcast-message.js b/Resources/Public/JavaScript/broadcast-message.js new file mode 100644 index 0000000..f40bca9 --- /dev/null +++ b/Resources/Public/JavaScript/broadcast-message.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class n{constructor(e,t,o){if(!e||!t)throw new Error("Properties componentName and eventName have to be defined");this.componentName=e,this.eventName=t,this.payload=o||{}}static fromData(e){const t=Object.assign({},e);return delete t.componentName,delete t.eventName,new n(e.componentName,e.eventName,t)}createCustomEvent(e="typo3"){return new CustomEvent([e,this.componentName,this.eventName].join(":"),{detail:this.payload})}}export{n as BroadcastMessage}; diff --git a/Resources/Public/JavaScript/broadcast-service.js b/Resources/Public/JavaScript/broadcast-service.js new file mode 100644 index 0000000..5cd7a09 --- /dev/null +++ b/Resources/Public/JavaScript/broadcast-service.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{BroadcastMessage as n}from"@typo3/backend/broadcast-message.js";import{MessageUtility as a}from"@typo3/backend/utility/message-utility.js";class s{constructor(){this.channel=new BroadcastChannel("typo3")}get isListening(){return typeof this.channel.onmessage=="function"}static onMessage(e){if(!a.verifyOrigin(e.origin))throw"Denied message sent by "+e.origin;const t=n.fromData(e.data);document.dispatchEvent(t.createCustomEvent("typo3"))}listen(){this.isListening||(this.channel.onmessage=s.onMessage)}post(e){this.channel.postMessage(e)}}var i=new s;export{i as default}; diff --git a/Resources/Public/JavaScript/browse-database.js b/Resources/Public/JavaScript/browse-database.js new file mode 100644 index 0000000..3e874bb --- /dev/null +++ b/Resources/Public/JavaScript/browse-database.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import s from"@typo3/backend/element-browser.js";import o from"@typo3/core/event/regular-event.js";class r{constructor(){new o("click",(a,t)=>{a.preventDefault();const e=t.closest("span").dataset;s.insertElement(e.table,e.uid,e.title,"",parseInt(t.dataset.close||"0",10)===1)}).delegateTo(document,"[data-close]")}}var l=new r;export{l as default}; diff --git a/Resources/Public/JavaScript/clear-cache.js b/Resources/Public/JavaScript/clear-cache.js new file mode 100644 index 0000000..4692391 --- /dev/null +++ b/Resources/Public/JavaScript/clear-cache.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import l from"@typo3/backend/notification.js";import o from"@typo3/backend/icons.js";import u from"@typo3/core/event/regular-event.js";import d from"@typo3/core/ajax/ajax-request.js";import i from"~labels/core.cache";var a;(function(g){g.clearCache=".t3js-clear-page-cache",g.icon=".t3js-icon"})(a||(a={}));class c{constructor(){this.registerClickHandler()}static setDisabled(t,e){t.disabled=e,t.classList.toggle("disabled",e)}static sendClearCacheRequest(t){const e=new d(TYPO3.settings.ajaxUrls.clearcache_page).post({id:t});return e.then(async s=>{const r=await s.resolve();r?.success===!1?l.error(r.title??i.get("notification.error.title"),r.message??i.get("notification.error.message")):l.success(r?.title??i.get("notification.success.title"),r?.message??i.get("notification.success.message"))},()=>{l.error(i.get("notification.error.title"),i.get("notification.error.message"))}),e}registerClickHandler(){const t=document.querySelector(`${a.clearCache}:not([disabled])`);t!==null&&new u("click",e=>{e.preventDefault();const s=e.currentTarget,r=parseInt(s.dataset.id,10);c.setDisabled(s,!0),o.getIcon("spinner-circle",o.sizes.small,null,"disabled").then(n=>{s.querySelector(a.icon).outerHTML=n}),c.sendClearCacheRequest(r).finally(()=>{o.getIcon("actions-system-cache-clear",o.sizes.small).then(n=>{s.querySelector(a.icon).outerHTML=n}),c.setDisabled(s,!1)})}).bindTo(t)}}var f=new c;export{f as default}; diff --git a/Resources/Public/JavaScript/clipboard-panel.js b/Resources/Public/JavaScript/clipboard-panel.js new file mode 100644 index 0000000..6b57f8b --- /dev/null +++ b/Resources/Public/JavaScript/clipboard-panel.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as h,html as n,nothing as a}from"lit";import{property as m,customElement as $}from"lit/decorators.js";import{until as k}from"lit/directives/until.js";import{unsafeHTML as u}from"lit/directives/unsafe-html.js";import{classMap as C}from"lit/directives/class-map.js";import y from"@typo3/core/ajax/ajax-request.js";import v from"@typo3/backend/notification.js";import"@typo3/backend/element/spinner-element.js";import"@typo3/backend/element/icon-element.js";var b=function(l,e,o,t){var r=arguments.length,i=r<3?e:t===null?t=Object.getOwnPropertyDescriptor(e,o):t,d;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")i=Reflect.decorate(l,e,o,t);else for(var p=l.length-1;p>=0;p--)(d=l[p])&&(i=(r<3?d(i):r>3?d(e,o,i):d(e,o))||i);return r>3&&i&&Object.defineProperty(e,o,i),i},f,c;(function(l){l.cut="cut",l.copy="copy"})(c||(c={}));let s=f=class extends h{constructor(){super(...arguments),this.returnUrl="",this.table=""}static renderLoader(){return n`
    `}createRenderRoot(){return this}render(){return n`${k(this.renderPanel(),f.renderLoader())}`}renderPanel(){return new y(top.TYPO3.settings.Clipboard.moduleUrl).withQueryArguments({action:"getClipboardData"}).post({table:this.table}).then(async e=>{const o=await e.resolve();if(o.success===!0&&o.data){const t=o.data;return n`
    ${t.labels.clipboard}
    ${t.tabs.map(r=>this.renderTab(r,t))}
    `}else return n`
    Clipboard data could not be fetched
    `}).catch(()=>n`
    An error occurred while fetching clipboard data
    `)}renderTab(e,o){return n`${o.current!==e.identifier?a:n`
    this.updateClipboard(t,{CB:{setCopyMode:"1"}})}> this.updateClipboard(t,{CB:{setCopyMode:"0"}})}>
    ${o.elementCount?n``:a}
    `}${o.current===e.identifier&&e.items?e.items.map(t=>this.renderTabItem(t,e.identifier,o)):a}`}renderTabItem(e,o,t){return n`${u(e.icon)}${u(e.title)} ${o==="normal"?n`(${t.copyMode===c.copy?n`${t.labels.copy}`:n`${t.labels.cut}`})`:a} ${e.thumb?n`
    ${u(e.thumb)}
    `:a}
    ${e.infoDataDispatch?n``:a} ${e.identifier?n``:a}
    `}updateClipboard(e,o){e.preventDefault();const t=e.currentTarget;new y(top.TYPO3.settings.Clipboard.moduleUrl).post(o).then(async r=>{const i=await r.resolve();i.success===!0?(t.dataset.action&&t.dispatchEvent(new CustomEvent("typo3:clipboard:"+t.dataset.action,{detail:{payload:o,response:i},bubbles:!0,cancelable:!1})),this.reloadModule()):v.error("Clipboard data could not be updated")}).catch(()=>{v.error("An error occurred while updating clipboard data")})}reloadModule(){this.returnUrl?this.ownerDocument.location.href=this.returnUrl:this.ownerDocument.location.reload()}};b([m({type:String,attribute:"return-url"})],s.prototype,"returnUrl",void 0),b([m({type:String})],s.prototype,"table",void 0),s=f=b([$("typo3-backend-clipboard-panel")],s);export{s as ClipboardPanel}; diff --git a/Resources/Public/JavaScript/close-current-window.js b/Resources/Public/JavaScript/close-current-window.js new file mode 100644 index 0000000..c3dd251 --- /dev/null +++ b/Resources/Public/JavaScript/close-current-window.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +self.close(),window.opener.location.reload(); diff --git a/Resources/Public/JavaScript/code-editor/autocomplete/completion-result.js b/Resources/Public/JavaScript/code-editor/autocomplete/completion-result.js new file mode 100644 index 0000000..219fc8a --- /dev/null +++ b/Resources/Public/JavaScript/code-editor/autocomplete/completion-result.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class d{constructor(t,n){this.tsRef=t,this.tsTreeNode=n}getType(){const t=this.tsTreeNode.getValue();return this.tsRef.isType(t)?this.tsRef.getType(t):null}getFilteredProposals(t){const n={},r=[],o=this.tsTreeNode.getChildNodes(),u=this.tsTreeNode.getValue();for(const e in o)if(typeof o[e].value<"u"&&o[e].value!==null){const s={};s.word=e,this.tsRef.typeHasProperty(u,o[e].name)?(this.tsRef.cssClass="definedTSREFProperty",s.type=o[e].value):(s.cssClass="userProperty",this.tsRef.isType(o[e].value)?s.type=o[e].value:s.type=""),r.push(s),n[e]=!0}const i=this.tsRef.getPropertiesFromTypeId(this.tsTreeNode.getValue());for(const e in i)if(typeof i[e].value<"u"&&n[e]!==!0){const s={word:e,cssClass:"undefinedTSREFProperty",type:i[e].value};r.push(s)}const l=[];let p="";for(let e=0;e{this.extTsObjTree.c=await t.resolve(),this.resolveExtReferencesRec(this.extTsObjTree.c)})}resolveExtReferencesRec(e){for(const t of Object.keys(e)){let s;if(e[t].v&&e[t].v.startsWith("<")&&!e[t].v.includes(">")){const r=e[t].v.replace(/"u"||typeof t.c[o]>"u")return null;t=t.c[o]}return t}getFilter(e){return e.completingAfterDot?"":e.token.string.replace(".","").replace(/\s/g,"")}}export{a as TsCodeCompletion}; diff --git a/Resources/Public/JavaScript/code-editor/autocomplete/ts-parser.js b/Resources/Public/JavaScript/code-editor/autocomplete/ts-parser.js new file mode 100644 index 0000000..f601e7c --- /dev/null +++ b/Resources/Public/JavaScript/code-editor/autocomplete/ts-parser.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class c{constructor(t,s){this.childNodes={},this.extPath="",this.parent=null,this.name=t,this.childNodes={},this.extPath="",this.value="",this.isExternal=!1,this.tsParser=s}getChildNodes(){const t=this.getExtNode();if(t!==null&&typeof t.c=="object")for(const s of Object.keys(t.c)){const e=new c(s,this.tsParser);e.global=!0,e.value=t.c[s].v?t.c[s].v:"",e.isExternal=!0,this.childNodes[s]=e}return this.childNodes}getValue(){if(this.value)return this.value;const t=this.getExtNode();if(t&&t.v)return t.v;const s=this.getNodeTypeFromTsref();return s||""}getNodeTypeFromTsref(){const t=this.extPath.split("."),s=t.pop(),e=this.parent.getValue();return e&&this.tsParser.tsRef.typeHasProperty(e,s)?this.tsParser.tsRef.getType(e).properties[s].value:""}getExtNode(){let t=this.tsParser.extTsObjTree;if(this.extPath==="")return t;const s=this.extPath.split(".");for(let e=0;e"u"||typeof t.c[r]>"u")return null;t=t.c[r]}return t}}class g extends Array{lastElementEquals(t){return this.length>0&&this[this.length-1]===t}popIfLastElementEquals(t){return this.lastElementEquals(t)?(this.pop(),!0):!1}}class E{constructor(t,s){this.tsRef=t,this.extTsObjTree=s,this.tsTree=new c("_L_",this)}getOperator(t){const s=[":=","=<","<",">","="];for(let e=0;e")>-1?"=":r}return-1}buildTsObjTree(t){this.tsTree=new c("",this),this.tsTree.value="TLO";let s=1,e="",r=!1,o=!1;const n=new g,l=[];let i;for(;s<=t.currentLineNumber;){e="";const h=t.lineTokens[s-1];for(let f=0;f<=h.length;++f)if(f0){const a=h[f].string;a.startsWith("#")?n.push("#"):a==="("?n.push("("):a.startsWith("/*")?n.push("/*"):a==="{"&&this.getOperator(e)===-1&&(n.push("{"),l.push(e.trim()),r=!0),a.search(/^\s*\[.*\]/)!==-1&&e.search(/\S/)===-1&&a.search(/^\s*\[(global|end|GLOBAL|END)\]/)===-1&&!n.lastElementEquals("#")&&!n.lastElementEquals("/*")&&!n.lastElementEquals("{")&&!n.lastElementEquals("(")&&(o=!0,r=!0),e.search(/\S/)===-1&&!n.lastElementEquals("#")&&!n.lastElementEquals("/*")&&!n.lastElementEquals("(")&&(a.search(/^\s*\[(global|end|GLOBAL|END)\]/)!==-1&&!n.lastElementEquals("{")||a.search(/^\s*\[(global|GLOBAL)\]/)!==-1)&&(o=!1,r=!0),a===")"&&n.popIfLastElementEquals("("),a.startsWith("*/")&&(n.popIfLastElementEquals("/*"),r=!0),a==="}"&&e.replace(/\s/g,"")===""&&(n.popIfLastElementEquals("{"),l.length>0&&l.pop(),r=!0),n.lastElementEquals("#")||(e+=a)}else{if(!n.lastElementEquals("/*")&&!n.lastElementEquals("(")&&!r&&!o){e=e.trim();const a=this.getOperator(e);if(a!==-1){const d=e.indexOf(a);i=e.substring(0,d),l.length>0&&(i=l.join(".")+"."+i);let u=e.substring(d+a.length,e.length).trim();switch(i=i.trim(),a){case"=":i.search(/\s/g)===-1&&i.length>0&&this.setTreeNodeValue(i,u);break;case"=<":l.length>0&&u.substr(0,1)==="."&&(u=l.join(".")+u),i.search(/\s/g)===-1&&i.length>0&&u.search(/\s/g)===-1&&u.length>0&&this.setReference(i,u);break;case"<":l.length>0&&u.substr(0,1)==="."&&(u=l.join(".")+u),i.search(/\s/g)===-1&&i.length>0&&u.search(/\s/g)===-1&&u.length>0&&this.setCopy(i,u);break;case">":this.deleteTreeNodeValue(i);break;case":=":break;default:break}}}n.popIfLastElementEquals("#"),r=!1}s++}if(!n.lastElementEquals("/*")&&!n.lastElementEquals("(")&&!r){const h=e.indexOf("<");h!==-1?(i=e.substring(h+1,e.length).trim(),l.length>0&&i.substr(0,1)==="."&&(i=l.join(".")+i)):(i=e,l.length>0&&(i=l.join(".")+"."+i,i=i.replace(/\s/g,"")));const f=i.lastIndexOf(".");i=i.substring(0,f)}return this.getTreeNode(i)}getTreeNode(t){if(t=t.trim(),t.length===0)return this.tsTree;const s=t.split(".");let e=this.tsTree.childNodes,r,o=this.tsTree;for(let n=0;n"u"||typeof e[r].childNodes>"u"){e[r]=new c(r,this),e[r].parent=o;let l=o.extPath;l&&(l+="."),l+=r,e[r].extPath=l}if(n===s.length-1)return e[r];o=e[r],e=e[r].childNodes}}setTreeNodeValue(t,s){const e=this.getTreeNode(t);e.parent!==null&&e.parent.value==="GIFBUILDER"&&s==="TEXT"&&(s="GB_TEXT"),e.parent!==null&&e.parent.value==="GIFBUILDER"&&s==="IMAGE"&&(s="GB_IMAGE"),this.tsRef.isType(s)&&(e.value=s)}deleteTreeNodeValue(t){const s=this.getTreeNode(t);s.value=null,s.childNodes={}}setReference(t,s){const e=t.split("."),r=e[e.length-1],o=this.getTreeNode(t),n=this.getTreeNode(s);o.parent!==null?o.parent.childNodes[r]=n:this.tsTree.childNodes[r]=n}setCopy(t,s){this.clone=l=>{if(typeof l!="object")return l;const i={};for(const h in l)h!=="tsParser"&&(h!=="parent"?typeof l[h]=="object"?i[h]=this.clone(l[h]):i[h]=l[h]:"parent"in l&&(i.parent=l.parent));return i};const e=t.split("."),r=e[e.length-1],o=this.getTreeNode(t),n=this.getTreeNode(s);o.parent!==null?o.parent.childNodes[r]=this.clone(n):this.tsTree.childNodes[r]=this.clone(n)}}export{c as TreeNode,E as TsParser}; diff --git a/Resources/Public/JavaScript/code-editor/autocomplete/ts-ref.js b/Resources/Public/JavaScript/code-editor/autocomplete/ts-ref.js new file mode 100644 index 0000000..99c6f8c --- /dev/null +++ b/Resources/Public/JavaScript/code-editor/autocomplete/ts-ref.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import h from"@typo3/core/ajax/ajax-request.js";class d{constructor(e,s,t){this.properties={},this.typeId=e,this.extends=s,this.properties=t}}class o{constructor(e,s,t){this.parentType=e,this.name=s,this.value=t}}class f{constructor(){this.typeTree={},this.doc=null}async loadTsrefAsync(){const e=await new h(TYPO3.settings.ajaxUrls.codeeditor_tsref).get();this.doc=await e.resolve(),this.buildTree()}buildTree(){for(const e of Object.keys(this.doc)){const s=this.doc[e];this.typeTree[e]=new d(e,s.extends||void 0,Object.fromEntries(Object.entries(s.properties).map(([t,r])=>[t,new o(e,t,r.type)])))}for(const e of Object.keys(this.typeTree))typeof this.typeTree[e].extends<"u"&&this.addPropertiesToType(this.typeTree[e],this.typeTree[e].extends,100)}addPropertiesToType(e,s,t){if(t<0)throw"Maximum recursion depth exceeded while trying to resolve the extends in the TSREF!";const r=s.split(",");for(let i=0;i"u"&&(e.properties[p]=y[p])}}getPropertiesFromTypeId(e){return typeof this.typeTree[e]<"u"?(this.typeTree[e].properties.clone=function(){const s={};for(const t of Object.keys(this))s[t]=new o(this[t].parentType,this[t].name,this[t].value);return s},this.typeTree[e].properties):{}}typeHasProperty(e,s){return typeof this.typeTree[e]<"u"&&typeof this.typeTree[e].properties[s]<"u"}getType(e){return this.typeTree[e]}isType(e){return typeof this.typeTree[e]<"u"}}export{f as TsRef,o as TsRefProperty,d as TsRefType}; diff --git a/Resources/Public/JavaScript/code-editor/element/code-mirror-element.js b/Resources/Public/JavaScript/code-editor/element/code-mirror-element.js new file mode 100644 index 0000000..ba93e2c --- /dev/null +++ b/Resources/Public/JavaScript/code-editor/element/code-mirror-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as g,css as b,html as h}from"lit";import{property as l,state as u,customElement as v}from"lit/decorators.js";import{EditorView as c,lineNumbers as w,highlightSpecialChars as k,drawSelection as E,placeholder as S,keymap as M}from"@codemirror/view";import{Compartment as C,EditorState as m}from"@codemirror/state";import{syntaxHighlighting as x,defaultHighlightStyle as D}from"@codemirror/language";import{defaultKeymap as V,indentWithTab as z}from"@codemirror/commands";import{oneDark as K}from"@codemirror/theme-one-dark";import{executeJavaScriptModuleInstruction as f,loadModule as O,resolveSubjectRef as R}from"@typo3/core/java-script-item-processor.js";import"@typo3/backend/element/spinner-element.js";var i=function(p,e,o,r){var n=arguments.length,a=n<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,o):r,s;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")a=Reflect.decorate(p,e,o,r);else for(var d=p.length-1;d>=0;d--)(s=p[d])&&(a=(n<3?s(a):n>3?s(e,o,a):s(e,o))||a);return n>3&&a&&Object.defineProperty(e,o,a),a};let t=class extends g{constructor(){super(...arguments),this.mode=null,this.addons=[],this.keymaps=[],this.lineDigits=0,this.autoheight=!1,this.nolazyload=!1,this.readonly=!1,this.lineWrapping=!1,this.fullscreen=!1,this.panel="bottom",this.editorTheme=null,this.editorView=null}static{this.styles=b`:host{position:relative;display:block}:host([fullscreen]){position:fixed;inset:64px 0 0;z-index:9}:host([fullscreen]) .cm-scroller{min-height:auto;max-height:100%}:host([autoheight]) .cm-scroller{max-height:none}.codemirror-label{font-size:.875em;opacity:.75}.codemirror-label-top{margin-bottom:.25rem}.codemirror-label-bottom{margin-top:.25rem}typo3-backend-spinner{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.cm-editor{overflow:hidden;border-radius:var(--typo3-input-border-radius);border:var(--typo3-input-border-width) solid var(--typo3-input-border-color);transition:outline-color .15s ease-in-out,box-shadow .15s ease-in-out}.cm-focused{border-color:var(--typo3-input-focus-border-color);outline-offset:0;outline:.25rem solid color-mix(in srgb,var(--typo3-form-control-focus-border-color),transparent 25%)}.cm-gutters{height:auto!important;position:relative!important}.cm-content{min-height:calc(8px + 16.8px*var(--rows, 18))!important}.cm-scroller{min-height:100%;max-height:calc(100dvh - 10rem)}`}setContent(e){this.editorView!==null&&this.editorView.dispatch({changes:{from:0,to:this.editorView.state.doc.length,insert:e}})}getContent(){return this.editorView.state.doc.toString()}render(){return h`${this.label&&this.panel==="top"?h`
    ${this.label}
    `:""}
    this.onKeydown(e)}>
    ${this.label&&this.panel==="bottom"?h`
    ${this.label}
    `:""} ${this.editorView===null?h``:""}`}firstUpdated(){if(this.nolazyload){this.initializeEditor(this.firstElementChild);return}const e={root:document.body},o=new IntersectionObserver(r=>{r.forEach(n=>{n.intersectionRatio>0&&(o.unobserve(n.target),this.firstElementChild&&this.firstElementChild.nodeName.toLowerCase()==="textarea"&&this.initializeEditor(this.firstElementChild))})},e);o.observe(this)}onKeydown(e){e.ctrlKey&&e.altKey&&e.key==="f"&&(e.preventDefault(),this.fullscreen=!0),e.key==="Escape"&&this.fullscreen&&(e.preventDefault(),this.fullscreen=!1)}async initializeEditor(e){const o=c.updateListener.of(s=>{s.docChanged&&(e.value=s.state.doc.toString(),e.dispatchEvent(new CustomEvent("change",{bubbles:!0})))});this.lineDigits>0?this.style.setProperty("--rows",this.lineDigits.toString()):e.getAttribute("rows")&&this.style.setProperty("--rows",e.getAttribute("rows")),this.editorTheme=new C;const r=[this.editorTheme.of([]),o,w(),k(),E(),m.allowMultipleSelections.of(!0),x(D,{fallback:!0})];if(this.lineWrapping&&r.push(c.lineWrapping),this.readonly&&r.push(m.readOnly.of(!0)),this.placeholder&&r.push(S(this.placeholder)),this.mode){const s=await f(this.mode);r.push(...s)}this.addons.length>0&&r.push(...await Promise.all(this.addons.map(s=>f(s))));const n=[...V,z];this.keymaps.length>0&&(await Promise.all(this.keymaps.map(d=>O(d).then(y=>R(y,d))))).forEach(d=>n.push(...d)),r.push(M.of(n)),this.editorView=new c({state:m.create({doc:e.value,extensions:r}),parent:this.renderRoot.querySelector("#codemirror-parent"),root:this.renderRoot}),this.toggleDarkMode(this.darkModeEnabled()),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{this.toggleDarkMode(this.darkModeEnabled())})}darkModeEnabled(){const o=window.getComputedStyle(this).colorScheme;return o==="light only"||o==="light"?!1:o==="dark only"||o==="dark"?!0:window.matchMedia("(prefers-color-scheme: dark)").matches}toggleDarkMode(e){this.editorView.dispatch({effects:this.editorTheme.reconfigure(e?K:[])})}};i([l({type:Object})],t.prototype,"mode",void 0),i([l({type:Array})],t.prototype,"addons",void 0),i([l({type:Array})],t.prototype,"keymaps",void 0),i([l({type:Number})],t.prototype,"lineDigits",void 0),i([l({type:Boolean,reflect:!0})],t.prototype,"autoheight",void 0),i([l({type:Boolean})],t.prototype,"nolazyload",void 0),i([l({type:Boolean})],t.prototype,"readonly",void 0),i([l({type:Boolean,attribute:"linewrapping"})],t.prototype,"lineWrapping",void 0),i([l({type:Boolean,reflect:!0})],t.prototype,"fullscreen",void 0),i([l({type:String})],t.prototype,"label",void 0),i([l({type:String})],t.prototype,"placeholder",void 0),i([l({type:String})],t.prototype,"panel",void 0),i([u()],t.prototype,"editorTheme",void 0),i([u()],t.prototype,"editorView",void 0),t=i([v("typo3-t3editor-codemirror")],t);export{t as CodeMirrorElement}; diff --git a/Resources/Public/JavaScript/code-editor/language/typoscript.js b/Resources/Public/JavaScript/code-editor/language/typoscript.js new file mode 100644 index 0000000..d4a465f --- /dev/null +++ b/Resources/Public/JavaScript/code-editor/language/typoscript.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import f from"@typo3/core/document-service.js";import{StreamLanguage as g,LanguageSupport as d,syntaxTree as p}from"@codemirror/language";import{TypoScriptStreamParserFactory as y}from"@typo3/backend/code-editor/stream-parser/typoscript.js";import{TsCodeCompletion as h}from"@typo3/backend/code-editor/autocomplete/ts-code-completion.js";function C(){const e=g.define(new y().create()),t=e.data.of({autocomplete:u});return new d(e,[t])}const D=(async()=>{await f.ready();const e=parseInt(document.querySelector('input[name="effectivePid"]')?.value,10);return new h(e)})();async function u(e){if(!e.explicit)return null;const t=k(e),s=e.pos-(t.completingAfterDot?1:0),n=p(e.state).resolveInner(s,-1),r=n.name==="Document"||t.completingAfterDot?"":e.state.sliceDoc(n.from,s),o=n.name==="Document"||t.completingAfterDot?e.pos:n.from;let i={start:n.from,end:s,string:r,type:n.name};/^[\w$_]*$/.test(r)||(i={start:e.pos,end:e.pos,string:"",type:r==="."?"property":null}),t.token=i;const l=(await D).refreshCodeCompletion(t);if((n.name==="string"||n.name==="comment")&&w(r,l))return null;const m=T(r,l);return{from:o,options:m.map(a=>({label:a,type:"keyword"}))}}function k(e){const t=e.state.sliceDoc().split(e.state.lineBreak).length,s=e.state.sliceDoc(0,e.pos).split(e.state.lineBreak).length,n=e.state.sliceDoc().split(e.state.lineBreak)[s-1],o=e.state.sliceDoc(e.pos-1,e.pos)===".";return{lineTokens:S(t,e),currentLineNumber:s,currentLine:n,lineCount:t,completingAfterDot:o}}function S(e,t){const s=Array(e).fill("").map(()=>[]);let n=0,r=1;return p(t.state).cursor().iterate(o=>{const i=o.type.name||o.name;if(i==="Document")return;const c=o.from,l=o.to;n{a&&(s[Math.min(r-1,e-1)].push({type:null,string:a,start:n,end:n+a.length}),r++,n+=a.length)});const m=t.state.sliceDoc(o.from,o.to);r=t.state.sliceDoc(0,o.from).split(t.state.lineBreak).length,s[r-1].push({type:i,string:m,start:c,end:l}),n=l}),n{o.lastIndexOf(e,0)===0&&!s.has(o)&&s.add(o)};for(let o=0,i=t.length;o)$/.test(g.lastType)}class oe{create(){const g=/[\w$\xa1-\uffff]/,b={name:"typoscript",localVars:void 0,doubleIndentSwitch:!1},I=function(){function e(a){return{type:a,style:"keyword"}}const r=e("keyword a"),t=e("keyword b");return{_CSS_DEFAULT_STYLE:e("_CSS_DEFAULT_STYLE"),_LOCAL_LANG:e("_LOCAL_LANG"),_offset:e("_offset"),absRefPrefix:e("absRefPrefix"),accessibility:e("accessibility"),ACT:t,ACTIFSUB:t,ACTIFSUBRO:e("ACTIFSUBRO"),ACTRO:t,addAttributes:e("addAttributes"),addExtUrlsAndShortCuts:e("addExtUrlsAndShortCuts"),addItems:e("addItems"),additionalHeaders:e("additionalHeaders"),additionalParams:e("additionalParams"),addQueryString:e("addQueryString"),adjustItemsH:e("adjustItemsH"),adjustSubItemsH:e("adjustSubItemsH"),admPanel:r,after:e("after"),afterImg:e("afterImg"),afterImgLink:e("afterImgLink"),afterImgTagParams:e("afterImgTagParams"),afterROImg:e("afterROImg"),afterWrap:e("afterWrap"),age:e("age"),alertPopups:e("alertPopups"),align:e("align"),all:t,allow:e("allow"),allowCaching:e("allowCaching"),allowedAttribs:e("allowedAttribs"),allowedClasses:e("allowedClasses"),allowedCols:e("allowedCols"),allowedNewTables:e("allowedNewTables"),allowTags:e("allowTags"),allStdWrap:e("allStdWrap"),allWrap:e("allWrap"),alt_print:r,alternativeSortingField:e("alternativeSortingField"),altIcons:e("altIcons"),altImgResource:e("altImgResource"),altLabels:e("altLabels"),altTarget:e("altTarget"),altText:e("altText"),altUrl:e("altUrl"),altUrl_noDefaultParams:e("altUrl_noDefaultParams"),altWrap:e("altWrap"),always:e("always"),alwaysActivePIDlist:e("alwaysActivePIDlist"),alwaysLink:e("alwaysLink"),andWhere:e("andWhere"),angle:e("angle"),antiAlias:e("antiAlias"),append:e("append"),applyTotalH:e("applyTotalH"),applyTotalW:e("applyTotalW"),archive:e("archive"),ascii:t,ATagAfterWrap:e("ATagAfterWrap"),ATagBeforeWrap:e("ATagBeforeWrap"),ATagParams:e("ATagParams"),ATagTitle:e("ATagTitle"),atLeast:t,atMost:t,attribute:e("attribute"),auth:r,autoLevels:e("autoLevels"),autonumber:e("autonumber"),backColor:e("backColor"),background:e("background"),baseURL:e("baseURL"),BE:t,be_groups:t,be_users:t,before:e("before"),beforeImg:e("beforeImg"),beforeImgLink:e("beforeImgLink"),beforeImgTagParams:e("beforeImgTagParams"),beforeROImg:e("beforeROImg"),beforeWrap:e("beforeWrap"),begin:e("begin"),bgCol:e("bgCol"),bgImg:e("bgImg"),blur:e("blur"),bm:e("bm"),bodyTag:e("bodyTag"),bodyTagAdd:e("bodyTagAdd"),bodyTagCObject:e("bodyTagCObject"),bodytext:e("bodytext"),borderCol:e("borderCol"),borderThick:e("borderThick"),bottomBackColor:e("bottomBackColor"),bottomContent:e("bottomContent"),bottomHeight:e("bottomHeight"),bottomImg:e("bottomImg"),bottomImg_mask:e("bottomImg_mask"),BOX:t,br:e("br"),browse:t,browser:r,brTag:e("brTag"),bullet:e("bullet"),bulletlist:e("bulletlist"),bullets:t,bytes:e("bytes"),cache:r,cache_clearAtMidnight:e("cache_clearAtMidnight"),cache_period:e("cache_period"),caption:e("caption"),caption_stdWrap:e("caption_stdWrap"),captionHeader:e("captionHeader"),captionSplit:e("captionSplit"),CARRAY:e("CARRAY"),CASE:e("CASE"),case:e("case"),casesensitiveComp:e("casesensitiveComp"),cellpadding:e("cellpadding"),cellspacing:e("cellspacing"),char:e("char"),charcoal:e("charcoal"),charMapConfig:e("charMapConfig"),CHECK:r,check:e("check"),class:e("class"),classesAnchor:e("classesAnchor"),classesCharacter:e("classesCharacter"),classesImage:e("classesImage"),classesParagraph:e("classesParagraph"),clear:e("clear"),clearCache:e("clearCache"),clearCache_disable:e("clearCache_disable"),clearCache_pageGrandParent:e("clearCache_pageGrandParent"),clearCache_pageSiblingChildren:e("clearCache_pageSiblingChildren"),clearCacheCmd:e("clearCacheCmd"),clearCacheLevels:e("clearCacheLevels"),clearCacheOfPages:e("clearCacheOfPages"),clickTitleMode:e("clickTitleMode"),clipboardNumberPads:e("clipboardNumberPads"),cMargins:e("cMargins"),COA:e("COA"),COA_INT:e("COA_INT"),cObj:r,COBJ_ARRAY:e("COBJ_ARRAY"),cObject:r,cObjNum:e("cObjNum"),collapse:e("collapse"),collections:e("collections"),color:e("color"),color1:e("color1"),color2:e("color2"),color3:e("color3"),color4:e("color4"),colors:e("colors"),colour:e("colour"),colPos_list:e("colPos_list"),colRelations:e("colRelations"),cols:e("cols"),colSpace:e("colSpace"),COMMENT:r,comment_auto:e("comment_auto"),commentWrap:e("commentWrap"),compX:e("compX"),compY:e("compY"),conf:e("conf"),CONFIG:e("CONFIG"),config:r,CONSTANTS:e("CONSTANTS"),constants:e("constants"),CONTENT:e("CONTENT"),content:r,contextMenu:e("contextMenu"),copy:r,copyLevels:e("copyLevels"),count_HMENU_MENUOBJ:e("count_HMENU_MENUOBJ"),count_menuItems:e("count_menuItems"),count_MENUOBJ:e("count_MENUOBJ"),create:e("create"),crop:e("crop"),csConv:e("csConv"),CType:e("CType"),CUR:t,CURIFSUB:t,CURIFSUBRO:t,current:e("current"),CURRO:t,curUid:e("curUid"),cut:r,cWidth:e("cWidth"),data:e("data"),dataArray:r,dataWrap:e("dataWrap"),date:e("date"),date_stdWrap:e("date_stdWrap"),datePrefix:e("datePrefix"),dayofmonth:r,dayofweek:r,DB:e("DB"),db_list:r,debug:e("debug"),debugData:e("debugData"),debugFunc:e("debugFunc"),debugItemConf:e("debugItemConf"),default:t,defaultAlign:e("defaultAlign"),defaultCmd:e("defaultCmd"),defaultHeaderType:e("defaultHeaderType"),defaultOutput:e("defaultOutput"),defaults:e("defaults"),defaultType:e("defaultType"),delete:e("delete"),denyTags:e("denyTags"),depth:e("depth"),DESC:e("DESC"),description:t,dimensions:e("dimensions"),direction:e("direction"),directory:t,directReturn:t,disableAllHeaderCode:e("disableAllHeaderCode"),disableAltText:e("disableAltText"),disableBodyTag:e("disableBodyTag"),disabled:e("disabled"),disableDelete:e("disableDelete"),disableHideAtCopy:e("disableHideAtCopy"),disableItems:e("disableItems"),disableNoMatchingValueElement:e("disableNoMatchingValueElement"),disablePrefixComment:e("disablePrefixComment"),disablePrependAtCopy:e("disablePrependAtCopy"),disableSearchBox:e("disableSearchBox"),disableSingleTableView:e("disableSingleTableView"),displayContent:e("displayContent"),displayFieldIcons:e("displayFieldIcons"),displayIcons:e("displayIcons"),displayMessages:e("displayMessages"),displayRecord:e("displayRecord"),displayTimes:e("displayTimes"),distributeX:e("distributeX"),distributeY:e("distributeY"),div:t,DIV:e("DIV"),doctype:e("doctype"),DOCUMENT_BODY:e("DOCUMENT_BODY"),doktype:e("doktype"),doNotLinkIt:e("doNotLinkIt"),doNotShowLink:e("doNotShowLink"),doNotStripHTML:e("doNotStripHTML"),dontCheckPid:e("dontCheckPid"),dontLinkIfSubmenu:e("dontLinkIfSubmenu"),dontWrapInTable:e("dontWrapInTable"),doubleBrTag:e("doubleBrTag"),dynCSS:r,edge:e("edge"),edit:r,edit_access:r,edit_docModuleUpload:e("edit_docModuleUpload"),EFFECT:e("EFFECT"),elements:e("elements"),else:t,email:t,emailMeAtLogin:e("emailMeAtLogin"),emailMess:e("emailMess"),emboss:e("emboss"),enable:e("enable"),encapsLines:e("encapsLines"),encapsLinesStdWrap:e("encapsLinesStdWrap"),encapsTagList:e("encapsTagList"),end:t,entryLevel:e("entryLevel"),equalH:e("equalH"),equals:t,everybody:e("everybody"),excludeDoktypes:e("excludeDoktypes"),excludeUidList:e("excludeUidList"),expAll:e("expAll"),expand:e("expand"),explode:e("explode"),ext:e("ext"),external:t,externalBlocks:e("externalBlocks"),extTarget:e("extTarget"),face:e("face"),false:t,FE:t,fe_adminLib:e("fe_adminLib"),fe_groups:t,fe_users:t,feadmin:t,field:e("field"),fieldName:e("fieldName"),fieldOrder:e("fieldOrder"),fieldRequired:e("fieldRequired"),fields:e("fields"),fieldWrap:e("fieldWrap"),file:e("file"),file1:e("file1"),file2:e("file2"),file3:e("file3"),file4:e("file4"),file5:e("file5"),FILES:e("FILES"),files:e("files"),firstLabel:e("firstLabel"),firstLabelGeneral:e("firstLabelGeneral"),fixAttrib:e("fixAttrib"),flip:e("flip"),flop:e("flop"),FLUIDTEMPLATE:e("FLUIDTEMPLATE"),folder:r,folders:e("folders"),folderTree:r,foldoutMenu:r,fontColor:e("fontColor"),fontFile:e("fontFile"),fontOffset:e("fontOffset"),fontSize:e("fontSize"),fontSizeMultiplicator:e("fontSizeMultiplicator"),forceDisplayFieldIcons:e("forceDisplayFieldIcons"),forceDisplayIcons:e("forceDisplayIcons"),forceTemplateParsing:e("forceTemplateParsing"),forceTypeValue:e("forceTypeValue"),FORM:e("FORM"),format:e("format"),function:e("function"),Functions:r,gamma:e("gamma"),gapBgCol:e("gapBgCol"),gapLineCol:e("gapLineCol"),gapLineThickness:e("gapLineThickness"),gapWidth:e("gapWidth"),get:e("get"),getBorder:e("getBorder"),getLeft:e("getLeft"),getRight:e("getRight"),GIFBUILDER:e("GIFBUILDER"),global:e("global"),globalNesting:e("globalNesting"),globalString:e("globalString"),globalVar:e("globalVar"),GP:e("GP"),gray:e("gray"),group:e("group"),groupBy:e("groupBy"),groupid:e("groupid"),header:t,header_layout:e("header_layout"),headerComment:e("headerComment"),headerData:e("headerData"),headerSpace:e("headerSpace"),headTag:e("headTag"),height:e("height"),helpText:e("helpText"),hidden:e("hidden"),hiddenFields:e("hiddenFields"),hide:e("hide"),hidePStyleItems:e("hidePStyleItems"),hideRecords:e("hideRecords"),highColor:e("highColor"),history:e("history"),HMENU:e("HMENU"),hostname:r,hour:r,HTML:e("HTML"),html:t,HTMLparser:e("HTMLparser"),HTMLparser_tags:e("HTMLparser_tags"),htmlSpecialChars:e("htmlSpecialChars"),htmlTag_setParams:e("htmlTag_setParams"),http:e("http"),icon:e("icon"),icon_image_ext_list:e("icon_image_ext_list"),icon_link:e("icon_link"),iconCObject:e("iconCObject"),id:t,IENV:e("IENV"),if:t,ifEmpty:t,IFSUB:t,IFSUBRO:t,IMAGE:e("IMAGE"),image:t,image_frames:e("image_frames"),imageLinkWrap:e("imageLinkWrap"),imagePath:e("imagePath"),images:e("images"),imageWrapIfAny:e("imageWrapIfAny"),IMG_RESOURCE:e("IMG_RESOURCE"),imgList:r,imgMax:e("imgMax"),imgNameNotRandom:e("imgNameNotRandom"),imgNamePrefix:e("imgNamePrefix"),imgObjNum:e("imgObjNum"),imgParams:e("imgParams"),imgPath:e("imgPath"),imgResource:r,imgStart:e("imgStart"),IMGTEXT:e("IMGTEXT"),imgText:r,import:e("import"),inBranch:t,inc:e("inc"),includeCSS:e("includeCSS"),includeLibrary:e("includeLibrary"),includeNotInMenu:e("includeNotInMenu"),index:e("index"),index_descrLgd:e("index_descrLgd"),index_enable:e("index_enable"),index_externals:e("index_externals"),info:r,inlineStyle2TempFile:e("inlineStyle2TempFile"),innerStdWrap:e("innerStdWrap"),innerStdWrap_all:e("innerStdWrap_all"),innerWrap:e("innerWrap"),innerWrap2:e("innerWrap2"),input:e("input"),inputLevels:e("inputLevels"),insertData:e("insertData"),intensity:e("intensity"),intTarget:e("intTarget"),intval:e("intval"),invert:e("invert"),IP:r,IProcFunc:e("IProcFunc"),isFalse:t,isGreaterThan:t,isInList:t,isLessThan:t,isPositive:t,isTrue:t,itemArrayProcFunc:e("itemArrayProcFunc"),itemH:e("itemH"),items:e("items"),itemsProcFunc:e("itemsProcFunc"),itemsProcessors:e("itemsProcessors"),iterations:e("iterations"),join:e("join"),JSwindow:r,JSWindow:e("JSWindow"),JSwindow_params:e("JSwindow_params"),keep:e("keep"),keepEntries:e("keepEntries"),keepNonMatchedTags:e("keepNonMatchedTags"),key:e("key"),keyword3:t,LABEL:r,label:e("label"),labelStdWrap:e("labelStdWrap"),labelWrap:e("labelWrap"),lang:e("lang"),languageField:e("languageField"),layout:r,left:e("left"),leftjoin:e("leftjoin"),levels:e("levels"),leveltitle:t,leveluid:e("leveluid"),lib:r,limit:e("limit"),line:e("line"),lineColor:e("lineColor"),lineThickness:e("lineThickness"),linkPrefix:e("linkPrefix"),linkTitleToSelf:e("linkTitleToSelf"),linkVars:e("linkVars"),linkWrap:e("linkWrap"),list:t,listNum:e("listNum"),listOnlyInSingleTableView:e("listOnlyInSingleTableView"),LIT:e("LIT"),lm:e("lm"),LOAD_REGISTER:e("LOAD_REGISTER"),locale_all:e("locale_all"),localNesting:e("localNesting"),locationData:e("locationData"),login:t,loginUser:r,lowColor:e("lowColor"),lower:e("lower"),LR:e("LR"),mailform:t,mailto:e("mailto"),main:e("main"),makelinks:e("makelinks"),markerWrap:e("markerWrap"),marks:r,mask:e("mask"),max:e("max"),maxAge:e("maxAge"),maxChars:e("maxChars"),maxH:e("maxH"),maxHeight:e("maxHeight"),maxItems:e("maxItems"),maxW:e("maxW"),maxWidth:e("maxWidth"),maxWInText:e("maxWInText"),media:t,menu:t,menuHeight:e("menuHeight"),menuName:e("menuName"),menuOffset:e("menuOffset"),menuWidth:e("menuWidth"),message_preview:e("message_preview"),META:e("META"),meta:e("meta"),method:e("method"),min:e("min"),minH:e("minH"),minItems:e("minItems"),minute:r,minW:e("minW"),mod:t,mode:e("mode"),module:r,month:r,move_wizard:r,MP_defaults:e("MP_defaults"),MP_disableTypolinkClosestMPvalue:e("MP_disableTypolinkClosestMPvalue"),MP_mapRootPoints:e("MP_mapRootPoints"),MULTIMEDIA:e("MULTIMEDIA"),multimedia:t,name:e("name"),negate:t,nesting:e("nesting"),neverHideAtCopy:e("neverHideAtCopy"),new:r,NEW:t,new_wizard:r,newPageWiz:e("newPageWiz"),newRecordFromTable:e("newRecordFromTable"),newWindow:e("newWindow"),newWizards:e("newWizards"),next:e("next"),niceText:e("niceText"),nicetext:e("nicetext"),NO:t,no_cache:e("no_cache"),no_search:e("no_search"),noAttrib:e("noAttrib"),noCache:e("noCache"),noCreateRecordsLink:e("noCreateRecordsLink"),noLink:e("noLink"),noMatchingValue_label:e("noMatchingValue_label"),nonCachedSubst:e("nonCachedSubst"),none:t,nonTypoTagStdWrap:e("nonTypoTagStdWrap"),nonTypoTagUserFunc:e("nonTypoTagUserFunc"),nonWrappedTag:e("nonWrappedTag"),noOrderBy:e("noOrderBy"),noPageTitle:e("noPageTitle"),noResultObj:r,noThumbsInEB:e("noThumbsInEB"),noTrimWrap:e("noTrimWrap"),noValueInsert:e("noValueInsert"),numRows:r,obj:e("obj"),offset:e("offset"),onlineWorkspaceInfo:e("onlineWorkspaceInfo"),onlyCurrentPid:e("onlyCurrentPid"),opacity:e("opacity"),options:r,orderBy:e("orderBy"),outerWrap:e("outerWrap"),outline:e("outline"),outputLevels:e("outputLevels"),override:e("override"),overrideAttribs:e("overrideAttribs"),overrideId:e("overrideId"),PAGE:e("PAGE"),page:r,PAGE_TARGET:e("PAGE_TARGET"),PAGE_TSCONFIG_ID:e("PAGE_TSCONFIG_ID"),PAGE_TSCONFIG_IDLIST:e("PAGE_TSCONFIG_IDLIST"),PAGE_TSCONFIG_STR:e("PAGE_TSCONFIG_STR"),pageFrameObj:e("pageFrameObj"),pages:t,pageTitleFirst:e("pageTitleFirst"),pageTree:r,parameter:e("parameter"),params:e("params"),parseFunc:e("parseFunc"),parseFunc_RTE:t,parser:e("parser"),password:e("password"),paste:r,path:e("path"),permissions:e("permissions"),perms:r,pid:t,pid_list:e("pid_list"),pidInList:e("pidInList"),PIDinRootline:r,PIDupinRootline:r,pixelSpaceFontSizeRef:e("pixelSpaceFontSizeRef"),plaintextLib:e("plaintextLib"),plainTextStdWrap:e("plainTextStdWrap"),plugin:r,postCObject:e("postCObject"),postLineBlanks:e("postLineBlanks"),postLineChar:e("postLineChar"),postLineLen:e("postLineLen"),postUserFunc:e("postUserFunc"),postUserFuncInt:e("postUserFuncInt"),preBlanks:e("preBlanks"),preCObject:e("preCObject"),prefix:e("prefix"),prefixComment:e("prefixComment"),prefixRelPathWith:e("prefixRelPathWith"),preIfEmptyListNum:e("preIfEmptyListNum"),preLineBlanks:e("preLineBlanks"),preLineChar:e("preLineChar"),preLineLen:e("preLineLen"),prepend:e("prepend"),preserveEntities:e("preserveEntities"),preUserFunc:e("preUserFunc"),prev:e("prev"),preview:r,previewBorder:e("previewBorder"),prevnextToSection:e("prevnextToSection"),prioriCalc:e("prioriCalc"),proc:e("proc"),processor_allowUpscaling:e("processor_allowUpscaling"),properties:e("properties"),protect:e("protect"),protectLvar:e("protectLvar"),publish:r,publish_levels:e("publish_levels"),quality:e("quality"),RADIO:r,radio:e("radio"),radioWrap:e("radioWrap"),range:e("range"),rawUrlEncode:e("rawUrlEncode"),recipient:e("recipient"),RECORDS:e("RECORDS"),recursive:e("recursive"),redirect:e("redirect"),redirectToURL:e("redirectToURL"),references:e("references"),register:e("register"),relPathPrefix:e("relPathPrefix"),remap:e("remap"),remapTag:e("remapTag"),REMOTE_ADDR:e("REMOTE_ADDR"),removeDefaultJS:e("removeDefaultJS"),removeIfEquals:e("removeIfEquals"),removeIfFalse:e("removeIfFalse"),removeItems:e("removeItems"),removeObjectsOfDummy:e("removeObjectsOfDummy"),removePrependedNumbers:e("removePrependedNumbers"),removeTags:e("removeTags"),removeWrapping:e("removeWrapping"),renderObj:r,renderWrap:e("renderWrap"),REQ:r,required:t,reset:e("reset"),resources:e("resources"),RESTORE_REGISTER:e("RESTORE_REGISTER"),resultObj:e("resultObj"),returnLast:e("returnLast"),returnUrl:e("returnUrl"),rightjoin:e("rightjoin"),rm:e("rm"),rmTagIfNoAttrib:e("rmTagIfNoAttrib"),RO:t,rootline:t,rotate:e("rotate"),rows:e("rows"),rowSpace:e("rowSpace"),RTE:r,RTE_compliant:r,rules:e("rules"),sample:e("sample"),saveClipboard:e("saveClipboard"),saveDocNew:e("saveDocNew"),script:t,search:t,SEARCHRESULT:e("SEARCHRESULT"),secondRow:e("secondRow"),section:e("section"),sectionIndex:e("sectionIndex"),select:r,selectFields:e("selectFields"),separator:e("separator"),set:e("set"),setContentToCurrent:e("setContentToCurrent"),setCurrent:e("setCurrent"),setfixed:e("setfixed"),setOnly:e("setOnly"),setup:r,shadow:e("shadow"),SHARED:e("SHARED"),sharpen:e("sharpen"),shear:e("shear"),short:e("short"),shortcut:t,shortcutFrame:e("shortcutFrame"),shortcutIcon:e("shortcutIcon"),show:e("show"),showAccessRestrictedPages:e("showAccessRestrictedPages"),showActive:e("showActive"),showFirst:e("showFirst"),showHiddenPages:e("showHiddenPages"),showHiddenRecords:e("showHiddenRecords"),showHistory:e("showHistory"),showPageIdWithTitle:e("showPageIdWithTitle"),showTagFreeClasses:e("showTagFreeClasses"),showWebsiteTitle:e("showWebsiteTitle"),simulateDate:e("simulateDate"),simulateUserGroup:e("simulateUserGroup"),singlePid:e("singlePid"),site_author:e("site_author"),site_reserved:e("site_reserved"),sitemap:t,siteUrl:e("siteUrl"),size:e("size"),solarize:e("solarize"),sorting:e("sorting"),source:e("source"),space:e("space"),spaceBelowAbove:e("spaceBelowAbove"),spaceLeft:e("spaceLeft"),spaceRight:e("spaceRight"),spacing:e("spacing"),spamProtectEmailAddresses:e("spamProtectEmailAddresses"),spamProtectEmailAddresses_atSubst:e("spamProtectEmailAddresses_atSubst"),spamProtectEmailAddresses_lastDotSubst:e("spamProtectEmailAddresses_lastDotSubst"),SPC:t,special:e("special"),split:r,splitChar:e("splitChar"),splitRendering:e("splitRendering"),src:e("src"),stdWrap:r,stdWrap2:e("stdWrap2"),strftime:e("strftime"),stripHtml:e("stripHtml"),styles:e("styles"),submenuObjSuffixes:e("submenuObjSuffixes"),subMenuOffset:e("subMenuOffset"),submit:e("submit"),subparts:r,subst_elementUid:e("subst_elementUid"),substMarksSeparately:e("substMarksSeparately"),substring:e("substring"),swirl:e("swirl"),sys_dmail:t,sys_filemounts:t,sys_note:t,sys_template:t,system:r,table:t,tableCellColor:e("tableCellColor"),tableParams:e("tableParams"),tables:e("tables"),tableStdWrap:e("tableStdWrap"),tableWidth:e("tableWidth"),tags:e("tags"),target:e("target"),TCAdefaults:e("TCAdefaults"),TCEFORM:e("TCEFORM"),TCEMAIN:e("TCEMAIN"),TDparams:e("TDparams"),temp:r,template:r,templateContent:e("templateContent"),templateFile:e("templateFile"),TEXT:e("TEXT"),text:t,textarea:e("textarea"),textMargin:e("textMargin"),textMargin_outOfText:e("textMargin_outOfText"),textMaxLength:e("textMaxLength"),textObjNum:e("textObjNum"),textpic:t,textPos:e("textPos"),thickness:e("thickness"),this:t,tile:e("tile"),time_stdWrap:e("time_stdWrap"),tipafriendLib:e("tipafriendLib"),title:e("title"),titleLen:e("titleLen"),titleText:e("titleText"),tm:e("tm"),TMENU:e("TMENU"),TMENUITEM:e("TMENUITEM"),token:e("token"),top:t,totalWidth:e("totalWidth"),transparentBackground:e("transparentBackground"),transparentColor:e("transparentColor"),treeLevel:r,trim:e("trim"),true:t,tsdebug:r,tsdebug_tree:e("tsdebug_tree"),TSFE:e("TSFE"),type:e("type"),typeNum:e("typeNum"),types:e("types"),typolink:r,uid:t,uidInList:e("uidInList"),uniqueGlobal:t,uniqueLocal:t,unset:e("unset"),unsetEmpty:t,updated:t,uploads:t,upper:e("upper"),url:r,us:t,useLargestItemX:e("useLargestItemX"),useLargestItemY:e("useLargestItemY"),USER:e("USER"),user:e("user"),USER_INT:e("USER_INT"),user_task:t,useragent:r,USERDEF1:t,USERDEF1RO:t,USERDEF2:t,USERDEF2RO:t,userdefined:e("userdefined"),userFunc:r,userfunction:e("userfunction"),usergroup:t,userid:e("userid"),userProc:e("userProc"),USR:t,USRRO:t,value:e("value"),valueArray:e("valueArray"),version:r,view:r,wave:e("wave"),web_func:t,web_info:t,web_layout:t,records:t,web_ts:e("web_ts"),where:e("where"),width:e("width"),wiz:e("wiz"),wordSpacing:e("wordSpacing"),workArea:e("workArea"),workOnSubpart:r,wrap:e("wrap"),wrap1:e("wrap1"),wrap2:e("wrap2"),wrap3:e("wrap3"),wrapAfterTags:e("wrapAfterTags"),wrapAlign:e("wrapAlign"),wrapFieldName:e("wrapFieldName"),wrapItemAndSub:e("wrapItemAndSub"),wrapNonWrappedLines:e("wrapNonWrappedLines"),wraps:e("wraps"),xhtml_strict:t,xhtml_trans:t,xmlprologue:e("xmlprologue"),XY:t}}(),h=/[\+\-\*\&\%\/=<>!\?]/;let T=!1;function z(e){let r=!1,t,a=!1;for(;(t=e.next())!==void 0;){if(!r){if(t==="/"&&!a)return;t==="["?a=!0:a&&t==="]"&&(a=!1)}r=!r&&t==="\\"}}let w,_;function s(e,r,t){return w=e,_=t,r}function P(e,r){const t=e.next();if(typeof t=="string"){if(t===` +`&&(T=!1),t==="."&&e.match(/^\d+(?:[eE][+\-]?\d+)?/))return s("number","number");if(t==="."&&e.match(".."))return s("spread","meta");if(t&&/[\[\]{}\(\),;\:\.]/.test(t))return s(t);if(t==="<"||t===">"||t==="."||t==="="&&e.peek()!=="<")return T=!0,s(t,"operator");if(!T&&t&&/[\[\]\(\),;\:\.\<\>\=]/.test(t))return s(t,"operator");if(t==="0"&&e.eat(/x/i))return e.eatWhile(/[\da-f]/i),s("number","number");if(t==="0"&&e.eat(/o/i))return e.eatWhile(/[0-7]/i),s("number","number");if(t==="0"&&e.eat(/b/i))return e.eatWhile(/[01]/i),s("number","number");if(t&&/\d/.test(t))return e.match(/^\d*(?:\.\d+)?(?:[eE][+\-]?\d+)?/),s("number","number");if(t==="/")return e.eat("*")?(r.tokenize=L,L(e,r)):e.eat("/")?(e.skipToEnd(),s("comment","comment")):V(e,r)?(z(e),e.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/),s("regexp","string-2")):(e.eatWhile(h),s("operator","operator",e.current()));if(t==="#")return e.skipToEnd(),s("comment","comment");if(h.test(t))return(t!==">"||!r.lexical||r.lexical.type!==">")&&e.eatWhile(h),s("operator","operator",e.current());if(g.test(t)){e.eatWhile(g);const a=e.current();if(I.propertyIsEnumerable(a)){const o=I[a];return s(o.type,o.style,a)}return T?s("string","string",a):s("variable",void 0,a)}}}function L(e,r){let t=!1,a;for(;(a=e.next())!==void 0;){if(a==="/"&&t){r.tokenize=P;break}t=a==="*"}return s("comment","comment")}const Y={atom:!0,number:!0,variable:!0,string:!0,regexp:!0};function J(e,r){for(let t=e.localVars;t;t=t.next)if(t.name==r)return!0;for(let t=e.context;t;t=t.prev)for(let a=t.vars;a;a=a.next)if(a.name==r)return!0}const l={state:null,column:null,marked:null,cc:null};function q(e,r,t,a,o){const n=e.cc;for(l.state=e,l.stream=o,l.marked=null,l.cc=n,l.style=r,e.lexical.hasOwnProperty("align")||(e.lexical.align=!0);;){const p=n.length?n.pop():C;if(typeof p=="function"&&p(t,a)){for(;n.length&&n[n.length-1]&&n[n.length-1].lex;)n.pop()();return l.marked?l.marked:t==="variable"&&J(e,a)?"variable-2":r}}}function d(...e){for(let r=e.length-1;r>=0;r--)l.cc.push(e[r])}function i(...e){return d(...e),!0}function v(e){function r(a){for(let o=a;o;o=o.next)if(o.name==e)return!0;return!1}const t=l.state;if(l.marked="def",t.context){if(r(t.localVars))return;t.localVars={name:e,next:t.localVars}}}function u(e,r){const t=function(){const a=l.state;let o=a.indented;if(a.lexical.type==="stat")o=a.lexical.indented;else for(let n=a.lexical;n&&n.type===")"&&n.align;n=n.prev)o=n.indented;a.lexical=new j(o,l.stream.column(),e,null,a.lexical,r)};return t.lex=!0,t}function c(){const e=l.state;e.lexical.prev&&(e.lexical.type===")"&&(e.indented=e.lexical.indented),e.lexical=e.lexical.prev)}c.lex=!0;function f(e){function r(t){return t==e?i():e===";"?d():i(r)}return r}function C(e,r){return e==="keyword a"?i(u("form"),X,C,c):e==="keyword b"?i(u("form"),C,c):e==="{"?i(u("}"),D,c):e===";"?i():e==="variable"?i(u("stat"),$):e==="import"?i(u("stat"),te,c):r==="@"?i(m,C):d(u("stat"),m,f(";"),c)}function m(e){return U(e,!1)}function y(e){return U(e,!0)}function X(e){return e!=="("?d():i(u(")"),m,f(")"),c)}function U(e,r){const t=r?A:E;return Y.hasOwnProperty(e)?i(t):e==="keyword c"?i(r?Q:W):e==="("?i(u(")"),W,f(")"),c,t):e==="operator"?i(r?y:m):e==="{"?k(O,"}",null,t):i()}function W(e){return e.match(/[;\}\)\],]/)?d():d(m)}function Q(e){return e.match(/[;\}\)\],]/)?d():d(y)}function E(e,r){return e===","?i(m):A(e,r,!1)}function A(e,r,t){const a=t==!1?E:A,o=t==!1?m:y;if(e==="operator")return/\+\+|--/.test(r)?i(a):r==="?"?i(m,f(":"),o):i(o);if(e!==";"){if(e==="(")return k(y,")","call",a);if(e===".")return i(K,a);if(e==="[")return i(u("]"),W,f("]"),c,a)}}function $(e){return e===":"?i(c,C):d(E,f(";"),c)}function K(e){if(e==="variable")return l.marked="property",i()}function O(e){if(e==="async")return l.marked="property",i(O);if(e==="variable"||l.style==="keyword")return l.marked="property",i(M);if(e==="number"||e==="string")return l.marked=l.style+" property",i(M);if(e==="modifier")return i(O);if(e===":")return d(M)}function M(e){if(e===":")return i(y)}function Z(e,r,t){function a(o,n){if(t?t.indexOf(o)>-1:o===","){const p=l.state.lexical;return p.info==="call"&&(p.pos=(p.pos||0)+1),i(function(x,S){return x==r||S==r?d():d(e)},a)}return o==r||n==r?i():i(f(r))}return function(o,n){return o==r||n==r?i():d(e,a)}}function k(e,r,t,...a){for(const o of a)l.cc.push(o);return i(u(r,t),Z(e,r),c)}function D(e){return e==="}"?i():d(C,D)}function B(e,r){if(e==="modifier")return i(B);if(e==="variable")return v(r),i();if(e==="{")return k(ee,"}")}function ee(e,r){return e==="variable"&&!l.stream.match(/^\s*:/,!1)?(v(r),i(H)):(e==="variable"&&(l.marked="property"),e==="}"?d():i(f(":"),B,H))}function H(e,r){if(r==="=")return i(y)}function te(e){return e==="string"?i():d(R,G,ae)}function R(e,r){return e==="{"?k(R,"}"):(e==="variable"&&v(r),r==="*"&&(l.marked="keyword"),i(re))}function G(e){if(e===",")return i(R,G)}function re(e,r){if(r==="as")return l.marked="keyword",i(R)}function ae(e,r){if(r==="from")return l.marked="keyword",i(m)}function ie(e,r){return e.lastType==="operator"||e.lastType===","||h.test(r.charAt(0))||/[,.]/.test(r.charAt(0))}class ne{constructor(){this.electricInput=/^\s*(?:case .*?:|default:|\{|\})$/,this.blockCommentStart="/*",this.blockCommentEnd="*/",this.lineComment="#",this.fold="brace",this.closeBrackets=`(){}''""\``,this.helperType="typoscript",this.name="TypoScript"}startState(r){return{tokenize:P,lastType:"sof",cc:[],lexical:new j(-r,0,"block",!1),localVars:b.localVars,context:b.localVars&&{vars:b.localVars},indented:0}}token(r,t){if(r.sol()&&(t.lexical.hasOwnProperty("align")||(t.lexical.align=!1),t.indented=r.indentation()),t.tokenize!=L&&r.eatSpace())return null;const a=t.tokenize(r,t);return w==="comment"?a:(t.lastType=w==="operator"&&(_==="++"||_==="--")?"incdec":w,q(t,a,w,_,r))}indent(r,t,a){if(r.tokenize==L)return null;if(r.tokenize!=P)return 0;const o=t&&t.charAt(0);let n=r.lexical,p;for(;(n.type==="stat"||n.type==="form")&&(o==="}"||(p=r.cc[r.cc.length-1])&&(p==E||p==A)&&!/^[,\.=+\-*:?[\(]/.test(t));)n=n.prev;const x=n.type,S=o==x;return x==="form"&&o==="{"?n.indented:x==="form"?n.indented+a.unit:x==="stat"?n.indented+(ie(r,t)?a.unit:0):n.info==="switch"&&!S&&b.doubleIndentSwitch!=!1?n.indented+(/^(?:case|default)\b/.test(t)?a.unit:2*a.unit):n.align?n.column+(S?0:1):n.indented+(S?0:a.unit)}expressionAllowed(r,t){return V(r,t)}}return new ne}}export{oe as TypoScriptStreamParserFactory}; diff --git a/Resources/Public/JavaScript/color-picker.js b/Resources/Public/JavaScript/color-picker.js new file mode 100644 index 0000000..1a05c7c --- /dev/null +++ b/Resources/Public/JavaScript/color-picker.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as s,query as d,customElement as h}from"lit/decorators.js";import{LitElement as b,css as u,html as g}from"lit";import k from"alwan";import v from"@typo3/core/event/regular-event.js";import y from"@typo3/core/document-service.js";var i=function(n,o,e,r){var a=arguments.length,t=a<3?o:r===null?r=Object.getOwnPropertyDescriptor(o,e):r,l;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(n,o,e,r);else for(var p=n.length-1;p>=0;p--)(l=n[p])&&(t=(a<3?l(t):a>3?l(o,e,t):l(o,e))||t);return a>3&&t&&Object.defineProperty(o,e,t),t};let c=class extends b{constructor(){super(...arguments),this.color="",this.opacity=!1,this.swatches=[]}static{this.styles=u`:host{display:inline-block;position:relative}.color-picker-preview{--typo3-colorpicker-preview-width:1.25rem;--typo3-colorpicker-preview-height:1.25rem;--typo3-bg-checkerboard-pattern-size:calc(var(--typo3-colorpicker-preview-width)/2);--typo3-bg-checkerboard-background-color:light-dark(var(--token-color-neutral-10),var(--token-color-neutral-85));--typo3-bg-checkerboard-background-image-color:light-dark(var(--token-color-neutral-0),var(--token-color-neutral-90));display:block;position:absolute;width:var(--typo3-colorpicker-preview-width);height:var(--typo3-colorpicker-preview-height);top:50%;inset-inline-start:var(--typo3-input-sm-padding-x);transform:translateY(-50%);background:var(--typo3-bg-checkerboard-background-color);background-image:linear-gradient(45deg,var(--typo3-bg-checkerboard-background-image-color) 25%,transparent 25%),linear-gradient(135deg,var(--typo3-bg-checkerboard-background-image-color) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,var(--typo3-bg-checkerboard-background-image-color) 75%),linear-gradient(135deg,transparent 75%,var(--typo3-bg-checkerboard-background-image-color) 75%);background-position:0 0,calc(var(--typo3-bg-checkerboard-pattern-size)/2) 0,calc(var(--typo3-bg-checkerboard-pattern-size)/2) calc(var(--typo3-bg-checkerboard-pattern-size)/2*-1),0 calc(var(--typo3-bg-checkerboard-pattern-size)/2);background-size:var(--typo3-bg-checkerboard-pattern-size) var(--typo3-bg-checkerboard-pattern-size);background-clip:padding-box;border-radius:var(--typo3-component-border-radius);pointer-events:none}.color-picker-preview-color{position:absolute;inset:0;border-radius:2px;background-color:var(--color,transparent)}`}async firstUpdated(){await y.ready();const o=this.getInputElement();if(!o||(!o.value&&this.color?o.value=this.color:this.color=o.value,o.disabled||o.readOnly))return;const e=new k(o,{position:"bottom-start",format:"hex",opacity:this.opacity,swatches:this.swatches,preset:!1,color:this.color,parent:this.closest("dialog")??""});e.on("open",()=>{o.focus()}),e.on("color",r=>{this.color=r.hex,o.value=this.color,o.dispatchEvent(new Event("blur"))}),["input","change"].forEach(r=>{new v(r,a=>{const t=a.target;this.color=t.value,e.setColor(this.color)}).bindTo(o)})}render(){return g``}getInputElement(){const o=this.slotEl.assignedNodes();for(const e of o)if(e instanceof HTMLInputElement)return e;return console.warn("No input element found in the slot."),null}};i([s({type:String})],c.prototype,"color",void 0),i([s({type:Boolean})],c.prototype,"opacity",void 0),i([s({type:Array})],c.prototype,"swatches",void 0),i([d("slot")],c.prototype,"slotEl",void 0),c=i([h("typo3-backend-color-picker")],c);class f{initialize(o,e={}){if(o.parentElement instanceof c)return;const r=document.createElement("typo3-backend-color-picker");r.swatches=e.swatches.map(a=>({color:a,label:a})),r.opacity=e.opacity??!1,o.parentNode.insertBefore(r,o),r.appendChild(o)}}var m=new f;export{c as Typo3BackendColorPicker,m as default}; diff --git a/Resources/Public/JavaScript/color-scheme-switch.js b/Resources/Public/JavaScript/color-scheme-switch.js new file mode 100644 index 0000000..1d35226 --- /dev/null +++ b/Resources/Public/JavaScript/color-scheme-switch.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as u,html as s,nothing as m}from"lit";import{property as d,state as p,customElement as b}from"lit/decorators.js";import v from"@typo3/core/ajax/ajax-request.js";import"@typo3/backend/element/icon-element.js";var a=function(c,e,t,i){var r=arguments.length,n=r<3?e:i===null?i=Object.getOwnPropertyDescriptor(e,t):i,l;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(c,e,t,i);else for(var h=c.length-1;h>=0;h--)(l=c[h])&&(n=(r<3?l(n):r>3?l(e,t,n):l(e,t))||n);return r>3&&n&&Object.defineProperty(e,t,n),n};let o=class extends u{constructor(){super(...arguments),this.activeColorScheme=null,this.colorSchemes=null,this.advancedOptionsExpanded=!1,this.autoDetect=null,this.enabled=null,this.colorSchemeMql=null,this.forcedColorsMql=null,this.colorSchemeMediaQueryListener=e=>this.autoDetect=e.matches?"dark":"light",this.forcedColorsMediaQueryListener=e=>this.enabled=!e.matches}connectedCallback(){super.connectedCallback(),this.colorSchemeMql=window.matchMedia("(prefers-color-scheme: dark)"),this.forcedColorsMql=window.matchMedia("(forced-colors: active)"),this.colorSchemeMediaQueryListener(this.colorSchemeMql),this.forcedColorsMediaQueryListener(this.forcedColorsMql),this.colorSchemeMql.addEventListener("change",this.colorSchemeMediaQueryListener),this.forcedColorsMql.addEventListener("change",this.forcedColorsMediaQueryListener)}disconnectedCallback(){super.disconnectedCallback(),this.colorSchemeMql.removeEventListener("change",this.colorSchemeMediaQueryListener),this.forcedColorsMql.removeEventListener("change",this.forcedColorsMediaQueryListener),this.colorSchemeMql=null,this.forcedColorsMql=null}createRenderRoot(){return this}getRealColorScheme(){return this.activeColorScheme==="auto"?this.autoDetect??"light":this.activeColorScheme??"light"}render(){return s`
    ${this.advancedOptionsExpanded===!1?m:s``}`}getIcon(e){return this.colorSchemes.find(t=>t.value===e)?.icon??"auto"}getLabel(e){return this.colorSchemes.find(t=>t.value===e)?.label??""}renderItem(e){return s`
  • `}async toggle(e){e.preventDefault(),e.stopPropagation();let i=this.getRealColorScheme()==="dark"?"light":"dark";i===this.autoDetect&&(i="auto"),this.triggerSchemeUpdate(i),await this.persistSchemeUpdate(i)}async handleClick(e,t){e.preventDefault(),e.stopPropagation(),this.triggerSchemeUpdate(t),await this.persistSchemeUpdate(t),this.advancedOptionsExpanded=!1}async persistSchemeUpdate(e){const t=new URL(TYPO3.settings.ajaxUrls.color_scheme_update,window.location.origin);return await new v(t).post({colorScheme:e})}triggerSchemeUpdate(e){document.dispatchEvent(new CustomEvent("typo3:color-scheme:update",{detail:{colorScheme:e}}))}};a([d({type:String})],o.prototype,"activeColorScheme",void 0),a([d({type:Array})],o.prototype,"colorSchemes",void 0),a([d({type:String})],o.prototype,"toggleLabel",void 0),a([d({type:String})],o.prototype,"disabledLabel",void 0),a([p()],o.prototype,"advancedOptionsExpanded",void 0),a([p()],o.prototype,"autoDetect",void 0),a([p()],o.prototype,"enabled",void 0),o=a([b("typo3-backend-color-scheme-switch")],o);export{o as ColorSchemeSwitchElement}; diff --git a/Resources/Public/JavaScript/column-selector-button.js b/Resources/Public/JavaScript/column-selector-button.js new file mode 100644 index 0000000..17cfee0 --- /dev/null +++ b/Resources/Public/JavaScript/column-selector-button.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as p,customElement as C}from"lit/decorators.js";import{PseudoButtonLitElement as k}from"@typo3/backend/element/pseudo-button.js";import{SeverityEnum as E}from"@typo3/backend/enum/severity.js";import h from"@typo3/backend/modal.js";import L from"@typo3/core/ajax/ajax-request.js";import S from"@typo3/backend/notification.js";import y from"~labels/core.mod_web_list";var m=function(a,t,n,e){var r=arguments.length,o=r<3?t:e===null?e=Object.getOwnPropertyDescriptor(t,n):e,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")o=Reflect.decorate(a,t,n,e);else for(var d=a.length-1;d>=0;d--)(c=a[d])&&(o=(r<3?c(o):r>3?c(t,n,o):c(t,n))||o);return r>3&&o&&Object.defineProperty(t,n,o),o},l,f;(function(a){a.columnsSelector=".t3js-column-selector",a.columnsContainerSelector=".t3js-column-selector-container",a.columnsFilterSelector='input[name="columns-filter"]',a.columnsSelectorActionsSelector=".t3js-column-selector-actions"})(f||(f={}));var g;(function(a){a.toggle="select-toggle",a.all="select-all",a.none="select-none"})(g||(g={}));let u=l=class extends k{constructor(){super(...arguments),this.modalTitle="Show columns",this.buttonOk=y.get("button.ok"),this.buttonClose=y.get("button.close"),this.errorMessage="Could not update columns"}static toggleSelectorActions(t,n,e,r=!1){n.classList.add("disabled");for(let o=0;o{const r=e.closest(f.columnsContainerSelector);if(!e.disabled&&r!==null){const o=r.querySelector(".form-check-label")?.textContent;o&&o.length&&r.classList.toggle("hidden",t.value!==""&&!RegExp(t.value,"i").test(o.trim().replace(/\[\]/g,"").replace(/\s+/g," ")))}})}buttonActivated(){this.showColumnSelectorModal()}showColumnSelectorModal(){if(!this.modalUrl||!this.modalTarget)return;const t=h.advanced({content:this.modalUrl,title:this.modalTitle,severity:E.notice,size:h.sizes.medium,type:h.types.ajax,buttons:[{text:this.buttonClose,active:!0,btnClass:"btn-default",name:"cancel",trigger:(n,e)=>e.hideModal()},{text:this.buttonOk,btnClass:"btn-primary",name:"update",trigger:(n,e)=>this.processSelection(e)}],ajaxCallback:()=>this.handleModalContentLoaded(t)})}processSelection(t){const n=t.querySelector("form");if(n===null){this.abortSelection();return}new L(TYPO3.settings.ajaxUrls.show_columns).post(new FormData(n)).then(async e=>{const r=await e.resolve();r.success===!0?(this.ownerDocument.location.href=this.modalTarget,this.ownerDocument.location.reload()):S.error(r.message||"No update was performed"),h.dismiss()}).catch(()=>{this.abortSelection()})}handleModalContentLoaded(t){const n=t.querySelector("form");if(n===null)return;n.addEventListener("submit",s=>{s.preventDefault()});const e=t.querySelectorAll(f.columnsSelector),r=t.querySelector(f.columnsFilterSelector),o=t.querySelector(f.columnsSelectorActionsSelector),c=o.querySelector('button[data-action="'+g.all+'"]'),d=o.querySelector('button[data-action="'+g.none+'"]');!e.length||r===null||c===null||d===null||(l.toggleSelectorActions(e,c,d,!0),e.forEach(s=>{s.addEventListener("change",()=>{l.toggleSelectorActions(e,c,d)})}),r.addEventListener("keydown",s=>{const b=s.target;s.code==="Escape"&&(s.stopImmediatePropagation(),b.value="")}),r.addEventListener("keyup",s=>{l.filterColumns(s.target,e),l.toggleSelectorActions(e,c,d)}),r.addEventListener("search",s=>{l.filterColumns(s.target,e),l.toggleSelectorActions(e,c,d)}),o.querySelectorAll("button[data-action]").forEach(s=>{s.addEventListener("click",b=>{b.preventDefault();const v=b.currentTarget;if(v.dataset.action){switch(v.dataset.action){case g.toggle:e.forEach(i=>{!i.disabled&&!l.isColumnHidden(i)&&(i.checked=!i.checked)});break;case g.all:e.forEach(i=>{!i.disabled&&!l.isColumnHidden(i)&&(i.checked=!0)});break;case g.none:e.forEach(i=>{!i.disabled&&!l.isColumnHidden(i)&&(i.checked=!1)});break;default:S.warning("Unknown selector action")}l.toggleSelectorActions(e,c,d)}})}))}abortSelection(){S.error(this.errorMessage),h.dismiss()}};m([p({type:String,attribute:"data-url"})],u.prototype,"modalUrl",void 0),m([p({type:String,attribute:"data-target"})],u.prototype,"modalTarget",void 0),m([p({type:String,attribute:"data-title"})],u.prototype,"modalTitle",void 0),m([p({type:String,attribute:"data-button-ok"})],u.prototype,"buttonOk",void 0),m([p({type:String,attribute:"data-button-close"})],u.prototype,"buttonClose",void 0),m([p({type:String,attribute:"data-error-message"})],u.prototype,"errorMessage",void 0),u=l=m([C("typo3-backend-column-selector-button")],u);export{u as ColumnSelectorButton}; diff --git a/Resources/Public/JavaScript/context-help.js b/Resources/Public/JavaScript/context-help.js new file mode 100644 index 0000000..48574c5 --- /dev/null +++ b/Resources/Public/JavaScript/context-help.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import"bootstrap";import i from"@typo3/backend/popover.js";import r from"@typo3/core/event/regular-event.js";import c from"@typo3/core/document-service.js";class n{constructor(){this.trigger="click",this.placement="auto",this.selector=".help-link",this.initialize()}async initialize(){await c.ready();const s=document.querySelectorAll(this.selector);s.forEach(t=>{t.dataset.bsHtml="true",t.dataset.bsPlacement=this.placement,t.dataset.bsTrigger=this.trigger,i.popover(t)}),new r("show.bs.popover",t=>{const e=t.target,o=e.dataset.description;if(o){const a={title:e.dataset.title||"",content:o};i.setOptions(e,a)}}).delegateTo(document,this.selector),new r("click",t=>{const e=t.target;s.forEach(o=>{o.isEqualNode(e)||i.hide(o)})}).delegateTo(document,"body")}}var l=new n;export{l as default}; diff --git a/Resources/Public/JavaScript/context-menu-actions.js b/Resources/Public/JavaScript/context-menu-actions.js new file mode 100644 index 0000000..fd0ace7 --- /dev/null +++ b/Resources/Public/JavaScript/context-menu-actions.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{SeverityEnum as g}from"@typo3/backend/enum/severity.js";import f from"@typo3/backend/ajax-data-handler.js";import d from"@typo3/core/ajax/ajax-request.js";import U from"@typo3/backend/info-window.js";import l from"@typo3/backend/modal.js";import C from"@typo3/backend/module-menu.js";import m from"@typo3/backend/notification.js";import i from"@typo3/backend/viewport.js";import"@typo3/backend/new-record-wizard.js";import b from"@typo3/backend/utility.js";import{topLevelModuleImport as w}from"@typo3/backend/utility/top-level-module-import.js";import{html as h}from"lit";import p from"~labels/core.common";import c from"~labels/core.cache";import u from"~labels/core.mod_web_list";import R from"~labels/backend.layout";import{openPageWizardModal as v}from"@typo3/backend/page-wizard/helper/wizard-helper.js";class n{static getReturnUrl(){return encodeURIComponent(top.list_frame.document.location.pathname+top.list_frame.document.location.search)}static editRecord(t,o,e){const r=e.pagesLanguageUid;let a="";r&&(a="&overrideVals[pages][sys_language_uid]="+r),i.ContentContainer.setUrl(top.TYPO3.settings.FormEngine.moduleUrl+"&edit["+t+"]["+o+"]=edit"+a+"&module="+encodeURIComponent(top.TYPO3.ModuleMenu.App.getCurrentModule())+"&returnUrl="+n.getReturnUrl())}static viewRecord(t,o,e){const r=e.previewUrl;if(r){const a=window.open(r,"newTYPO3frontendWindow");a.focus(),b.urlsPointToSameServerSideResource(a.location.href,r)&&a.location.reload()}}static async showQrCode(t,o,e){const r=e.previewUrl;r&&(await w("@typo3/backend/element/qrcode-element.js"),l.advanced({title:R.get("showPageQrCode.modalTitle"),size:l.sizes.small,content:h`
    `,buttons:[{text:u.get("button.close"),btnClass:"btn-default",name:"close",trigger:(a,s)=>s.hideModal()}]}))}static openInfoPopUp(t,o){U.showItem(t,o)}static mountAsTreeRoot(t,o){if(t==="pages"){const e=new CustomEvent("typo3:pagetree:mountPoint",{detail:{pageId:o}});top.document.dispatchEvent(e)}}static newPageWizard(t,o,e){const r=e.pagesNewWizardUrl;i.ContentContainer.setUrl(r+"&returnUrl="+n.getReturnUrl())}static newContentWizard(t,o,e){let r=e.newWizardUrl;r&&(r+="&returnUrl="+n.getReturnUrl(),l.advanced({title:e.title,type:l.types.ajax,size:l.sizes.large,content:r,severity:g.notice}))}static newRecord(t,o){if(t==="pages"){v({positionData:{pageUid:parseInt(String(o),10),insertPosition:"inside"}});return}i.ContentContainer.setUrl(top.TYPO3.settings.FormEngine.moduleUrl+"&edit["+t+"]["+(t!=="pages"?"-":"")+o+"]=new&returnUrl="+n.getReturnUrl())}static openHistoryPopUp(t,o){i.ContentContainer.setUrl(top.TYPO3.settings.RecordHistory.moduleUrl+"&element="+t+":"+o+"&returnUrl="+n.getReturnUrl())}static openListModule(t,o,e){const r=t==="pages"?o:e.pageUid;C.App.showModule("records","id="+r)}static pagesSort(t,o,e){const r=e.pagesSortUrl;r&&i.ContentContainer.setUrl(r)}static openSiteSettings(t,o,e){const r=e.siteSettingsUrl;r&&i.ContentContainer.setUrl(r+"&returnUrl="+n.getReturnUrl())}static openSiteConfiguration(t,o,e){const r=e.siteConfigurationUrl;r&&i.ContentContainer.setUrl(r+"&returnUrl="+n.getReturnUrl())}static pagesNewMultiple(t,o,e){const r=e.pagesNewMultipleUrl;r&&i.ContentContainer.setUrl(r)}static disableRecord(t,o,e){const r=e.disableField||"hidden";i.ContentContainer.setUrl(top.TYPO3.settings.RecordCommit.moduleUrl+"&data["+t+"]["+o+"]["+r+"]=1&redirect="+n.getReturnUrl())}static enableRecord(t,o,e){const r=e.disableField||"hidden";i.ContentContainer.setUrl(top.TYPO3.settings.RecordCommit.moduleUrl+"&data["+t+"]["+o+"]["+r+"]=0&redirect="+n.getReturnUrl())}static showInMenus(t,o){i.ContentContainer.setUrl(top.TYPO3.settings.RecordCommit.moduleUrl+"&data["+t+"]["+o+"][nav_hide]=0&redirect="+n.getReturnUrl())}static hideInMenus(t,o){i.ContentContainer.setUrl(top.TYPO3.settings.RecordCommit.moduleUrl+"&data["+t+"]["+o+"][nav_hide]=1&redirect="+n.getReturnUrl())}static deleteRecord(t,o,e){const r=l.confirm(e.title,e.message,g.warning,[{text:e.buttonCloseText||p.get("cancel"),active:!0,btnClass:"btn-default",name:"cancel"},{text:e.buttonOkText||u.get("button.delete"),btnClass:"btn-warning",name:"delete"}]);r.addEventListener("button.clicked",a=>{if(a.target.getAttribute("name")==="delete"){const s={component:"contextmenu",action:"delete",table:t,uid:o};f.process("cmd["+t+"]["+o+"][delete]=1",s).then(()=>{t==="pages"&&n.refreshPageTree(),n.triggerRefresh(i.ContentContainer.get().location.href)})}r.hideModal()})}static copy(t,o){const e=TYPO3.settings.ajaxUrls.contextmenu_clipboard+"&CB[el]["+t+"%7C"+o+"]=1&CB[setCopyMode]=1";new d(e).get().finally(()=>{n.triggerRefresh(i.ContentContainer.get().location.href)})}static clipboardRelease(t,o){const e=TYPO3.settings.ajaxUrls.contextmenu_clipboard+"&CB[el]["+t+"%7C"+o+"]=0";new d(e).get().finally(()=>{n.triggerRefresh(i.ContentContainer.get().location.href)})}static cut(t,o){const e=TYPO3.settings.ajaxUrls.contextmenu_clipboard+"&CB[el]["+t+"%7C"+o+"]=1&CB[setCopyMode]=0";new d(e).get().finally(()=>{n.triggerRefresh(i.ContentContainer.get().location.href)})}static triggerRefresh(t){t.includes("record%2Fedit")||i.ContentContainer.refresh()}static clearCache(t,o){new d(TYPO3.settings.ajaxUrls.clearcache_page).post({id:o}).then(async e=>{const r=await e.resolve();r?.success===!1?m.error(r.title??c.get("notification.error.title"),r.message??c.get("notification.error.message")):m.success(r?.title??c.get("notification.success.title"),r?.message??c.get("notification.success.message"))},()=>{m.error(c.get("notification.error.title"),c.get("notification.error.message"))})}static pasteAfter(t,o,e){n.pasteInto(t,-o,e)}static pasteInto(t,o,e){const r=()=>{const s="&CB[paste]="+t+"%7C"+o+"&CB[pad]=normal&redirect="+n.getReturnUrl();i.ContentContainer.setUrl(top.TYPO3.settings.RecordCommit.moduleUrl+s)};if(!e.title){r();return}const a=l.confirm(e.title,e.message,g.warning,[{text:e.buttonCloseText||p.get("cancel"),active:!0,btnClass:"btn-default",name:"cancel"},{text:e.buttonOkText||p.get("ok"),btnClass:"btn-warning",name:"ok"}]);a.addEventListener("button.clicked",s=>{s.target.getAttribute("name")==="ok"&&r(),a.hideModal()})}static refreshPageTree(){top.document.dispatchEvent(new CustomEvent("typo3:pagetree:refresh"))}}export{n as default}; diff --git a/Resources/Public/JavaScript/context-menu.js b/Resources/Public/JavaScript/context-menu.js new file mode 100644 index 0000000..1d391fb --- /dev/null +++ b/Resources/Public/JavaScript/context-menu.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{AjaxResponse as I}from"@typo3/core/ajax/ajax-response.js";import M from"@typo3/core/ajax/ajax-request.js";import N from"@typo3/backend/context-menu-actions.js";import"@typo3/backend/element/spinner-element.js";import{state as m,queryAll as b,customElement as k}from"lit/decorators.js";import{LitElement as C,nothing as x,html as f}from"lit";import{delay as S}from"@typo3/core/lit-helper.js";import{styleMap as v}from"lit/directives/style-map.js";import{unsafeHTML as y}from"lit/directives/unsafe-html.js";import{Task as T,initialState as E}from"@lit/task";import w from"@typo3/backend/notification.js";var d=function(h,t,e,n){var i=arguments.length,o=i<3?t:n===null?n=Object.getOwnPropertyDescriptor(t,e):n,s;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")o=Reflect.decorate(h,t,e,n);else for(var r=h.length-1;r>=0;r--)(s=h[r])&&(o=(i<3?s(o):i>3?s(t,e,o):s(t,e))||o);return i>3&&o&&Object.defineProperty(t,e,o),o},p;(function(h){h.open="typo3:contextmenu:open",h.close="typo3:contextmenu:close"})(p||(p={}));class F{constructor(){document.addEventListener("click",t=>{this.handleTriggerEvent(t)}),document.addEventListener("contextmenu",t=>{this.handleTriggerEvent(t)})}show(t,e,n,i,o,s=null,r=null){const a=new CustomEvent(p.open,{detail:{table:t,uid:e,context:n,eventSource:s,originalEvent:r},bubbles:!0,composed:!0});top.document.dispatchEvent(a)}handleTriggerEvent(t){if(!(t.target instanceof Element))return;const e=t.target.closest("[data-contextmenu-trigger]");if(e instanceof HTMLElement){this.handleContextMenuEvent(t,e);return}}handleContextMenuEvent(t,e){const n=e.dataset.contextmenuTrigger;if(n==="click"||n===t.type){if(t.preventDefault(),(e.dataset.contextmenuTable??"")==="")throw console.error("Referenced element misses data-contextmenu-table",e),new Error("No data-contextmenu-table attribute provided in contextmenu trigger markup");if((e.dataset.contextmenuUid??"")==="")throw console.error("Referenced element misses data-contextmenu-uid",e),new Error("No data-contextmenu-uid attribute provided in contextmenu trigger markup");this.show(e.dataset.contextmenuTable??"",e.dataset.contextmenuUid??"",e.dataset.contextmenuContext??"","","",e,t)}}}var A=new F;let l=class extends C{constructor(){super(...arguments),this.open=!1,this.table="",this.uid="",this.context="",this.rootPositionY=0,this.rootPositionX=0,this.eventSource=null,this.rootIdentifier="root",this.focusFirstElement=!1,this.fetchTask=new T(this,{autoRun:!1,args:()=>[this.table,this.uid,this.context,this.open],task:async([t,e,n,i],{signal:o})=>{if(!i)return E;const s=new URLSearchParams;if(t!==""&&s.set("table",t),e!==""&&s.set("uid",e.toString()),(n??"")!==""&&s.set("context",n),s.size===0)return E;const r=TYPO3.settings.ajaxUrls.contextmenu,a=await new M(r).withQueryArguments(s).get({signal:o}),c=Object.values(await a.resolve());return c.length===0?E:this.enhanceNodes("root",c)},onComplete:()=>{this.focusFirstElement=!0},onError:t=>{t instanceof I?w.error("",t.response.status+" "+t.response.statusText,5):w.error("",t.message)}}),this.show=t=>{const e=t.detail;if(this.open=!0,this.table=e.table,this.uid=e.uid,this.context=e.context,this.eventSource=e.eventSource,e.originalEvent!==null){const n=this.calculateIframeOffset(e.originalEvent.view,window);let i,o;if(e.originalEvent.pointerId===-1&&e.originalEvent.target){const s=e.originalEvent.target.getBoundingClientRect();o=s.top+s.height/2,i=s.left}else o=e.originalEvent.clientY,i=e.originalEvent.clientX;this.rootPositionY=o+n.y,this.rootPositionX=i+n.x}this.fetchTask.run()},this.hide=async()=>{this.open&&(this.open=!1,this.fetchTask.run(),await this.updateComplete,this.eventSource?.focus())}}get nodes(){return this.fetchTask.value??[]}connectedCallback(){super.connectedCallback(),window.addEventListener("resize",this.hide),document.addEventListener(p.open,this.show),document.addEventListener(p.close,this.hide)}disconnectedCallback(){super.disconnectedCallback(),window.removeEventListener("resize",this.hide),document.removeEventListener(p.open,this.show),document.removeEventListener(p.close,this.hide)}updated(){this.focusFirstElement&&(this.contextMenuItemElements.length>0&&Array.from(this.contextMenuItemElements).at(0).focus(),this.focusFirstElement=!1),this.updateContextMenuPositions()}createRenderRoot(){return this}render(){return this.fetchTask.render({initial:()=>x,pending:()=>[this.renderOverlay(),S(80,()=>this.renderMenu(null,this.rootIdentifier,this.rootPositionY,this.rootPositionX))],complete:t=>[this.renderOverlay(),this.renderMenu(t,this.rootIdentifier,this.rootPositionY,this.rootPositionX),this.flattenMenuItems(t).filter(e=>e.type==="submenu").map(e=>this.isNodeExpanded(e)?this.renderMenu(e.childItems,this.getNodeIdentifier(e),this.rootPositionY,this.rootPositionX):x)]})}renderOverlay(){return f`
    this.handleOverlayClick(t)} @contextmenu=${t=>this.handleOverlayClick(t)}>
    `}renderMenu(t,e,n,i){const o={top:n+"px",insetInlineStart:i+"px"};return t===null?f`
    `:t.length===0?x:f`
    `}renderMenuItem(t){return t.type==="divider"?f`
    `:f``}showSubmenu(t){t.__expanded=!0}hideSubmenu(t){t.__expanded=!1}handleNodeClick(t,e){if(e.type==="submenu"){this.isNodeExpanded(e)?this.hideSubmenu(e):this.showSubmenu(e);return}const n=this.extractDataAttributesAndConvertToDataset(e.additionalAttributes),i=e.callbackAction,{callbackModule:o,...s}=n;o?import(o+".js").then(({default:r})=>{r[i](this.table,this.uid,s)}):N&&typeof N[i]=="function"?N[i](this.table,this.uid,s):console.error("action: "+i+" not found"),this.hide()}mapIframeTarget(t,e){if(t.tagName!=="IFRAME")return t;const n=t;let i;try{i=n.contentWindow}catch{return t}const o=n.getBoundingClientRect();return i.document.elementFromPoint(e.clientX-o.x,e.clientY-o.y)??t}async handleOverlayClick(t){t.preventDefault(),t.stopPropagation(),this.eventSource=null,await this.hide();let e=document.elementFromPoint(t.clientX,t.clientY);e&&e!==t.currentTarget&&(e=this.mapIframeTarget(e,t),e.dispatchEvent(new PointerEvent(t.type,t)),(e.tagName==="INPUT"||e.tagName==="TEXTAREA")&&e.focus())}async handleNodeKeyDown(t,e){if(!["ArrowDown","ArrowUp","ArrowLeft","ArrowRight","Home","End","Enter","Space","Escape","Tab"].includes(t.code)||t.altKey||t.ctrlKey)return;t.preventDefault(),t.stopPropagation();const i=this.getParralellNodesForNavigation(e),o=this.getFirstNode(i),s=this.getLastNode(i),r=this.getParentNode(e),a=this.getPreviousNode(e),c=this.getNextNode(e);switch(t.code){case"Enter":case"Space":if(e.type==="submenu"){this.showSubmenu(e),await this.updateComplete;const u=this.getFirstNode(e.childItems);u&&this.getElementFromNode(u)?.focus()}else this.getElementFromNode(e)?.click();break;case"Tab":this.hide();break;case"Escape":r?(this.hideSubmenu(r),await this.updateComplete,this.getElementFromNode(r)?.focus()):this.hide();break;case"ArrowUp":a&&this.getElementFromNode(a)?.focus();break;case"ArrowDown":c&&this.getElementFromNode(c)?.focus();break;case"ArrowRight":if(e.type==="submenu"){this.showSubmenu(e),await this.updateComplete;const u=this.getFirstNode(e.childItems);u&&this.getElementFromNode(u)?.focus()}break;case"ArrowLeft":r&&(this.hideSubmenu(r),await this.updateComplete,this.getElementFromNode(r)?.focus());break;case"Home":this.getElementFromNode(o)?.focus();break;case"End":this.getElementFromNode(s)?.focus();break;default:return}}getNodeIdentifier(t){return t.__contextMenuIdentifier}getNodeParentIdentifier(t){return t.__contextMenuParentIdentifier}getNodePositionInSet(t){return this.getParralellNodesForNavigation(t).indexOf(t)+1}getNodeSetSize(t){return this.getParralellNodesForNavigation(t).length}getParentNode(t){const e=this.getNodeParentIdentifier(t);return this.getNodeByIdentifier(e)}getNodeByIdentifier(t){return this.flattenMenuItems(this.nodes).find(n=>this.getNodeIdentifier(n)===t)??null}getPreviousNode(t){const e=this.getParralellNodesForNavigation(t),i=e.indexOf(t)-1;return e[i]?e[i]:this.getLastNode(e)}getNextNode(t){const e=this.getParralellNodesForNavigation(t),i=e.indexOf(t)+1;return e[i]?e[i]:this.getFirstNode(e)}getFirstNode(t){return t.at(0)??null}getLastNode(t){return t.at(-1)??null}isNodeExpanded(t){return t.__expanded===!0}getElementFromNode(t){return this.querySelector('[data-contextmenu-id="'+this.getNodeIdentifier(t)+'"]')}enhanceNodes(t,e){return e.reduce((i,o)=>{const s=t+"_"+o.identifier,r={...o,__contextMenuIdentifier:s,__contextMenuParentIdentifier:t,__expanded:!1,childItems:this.enhanceNodes(s,Object.values(o.childItems??{}))},a=this;return[...i,new Proxy(r,{set(c,u,g){return c[u]!==g&&(c[u]=g,a.requestUpdate()),!0}})]},[])}calculateIframeOffset(t,e){let n=0,i=0;if(t===e)return{x:n,y:i};const o=this.calculateIframeOffset(t.parent,e);n+=o.x,i+=o.y;const s=t.frameElement;if(s){const r=s.getBoundingClientRect();n+=r.x,i+=r.y}return{x:n,y:i}}extractDataAttributesAndConvertToDataset(t){const e=n=>n.replace(/^data-/,"").replace(/-([a-z])/g,(i,o)=>o.toUpperCase());return Object.fromEntries(Object.entries(t).filter(([n])=>n.startsWith("data-")).map(([n,i])=>[e(n),String(i)]))}updateContextMenuPositions(){if(this.contextMenuElements.length>0){const t=this.contextMenuElements[0];this.updateContextMenuPosition(t,this.rootPositionY,this.rootPositionX);const e=[...this.contextMenuElements].slice(1),n=this.getDocumentDirection();e.forEach(i=>{const o=this.getNodeByIdentifier(i.dataset.contextmenuParent),r=this.getElementFromNode(o).getBoundingClientRect(),a=r.top-7,c=n==="ltr"?r.right:r.left;this.updateContextMenuPosition(i,a,c)})}}updateContextMenuPosition(t,e,n){const s=this.getDocumentDirection(),r={width:t.offsetWidth,height:t.offsetHeight},a={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight};let c=0,u=0;u=e,c=s==="ltr"?n:a.width-n;const g=u+r.height+10+5n.type!=="divider"):this.nodes.filter(n=>n.type!=="divider")}getDocumentDirection(){return document.querySelector("html").dir==="rtl"?"rtl":"ltr"}};d([m()],l.prototype,"open",void 0),d([m()],l.prototype,"table",void 0),d([m()],l.prototype,"uid",void 0),d([m()],l.prototype,"context",void 0),d([m()],l.prototype,"rootPositionY",void 0),d([m()],l.prototype,"rootPositionX",void 0),d([m()],l.prototype,"eventSource",void 0),d([b(".context-menu")],l.prototype,"contextMenuElements",void 0),d([b(".context-menu-item")],l.prototype,"contextMenuItemElements",void 0),l=d([k("typo3-backend-context-menu")],l);export{l as ContextMenuElement,A as default}; diff --git a/Resources/Public/JavaScript/contextual-record-edit.js b/Resources/Public/JavaScript/contextual-record-edit.js new file mode 100644 index 0000000..fcd929f --- /dev/null +++ b/Resources/Public/JavaScript/contextual-record-edit.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import s from"@typo3/backend/form-engine.js";import a from"@typo3/backend/modal.js";import r from"@typo3/backend/severity.js";import l from"@typo3/core/document-service.js";import"@typo3/backend/element/icon-element.js";import i from"~labels/backend.alt_doc";class c{constructor(t={}){this.closeButton=null,this.fullscreenButton=null,this.options=t,l.ready().then(()=>{this.closeButton=document.querySelector(".t3js-contextual-close"),this.fullscreenButton=document.querySelector(".t3js-contextual-fullscreen"),this.initialize()})}initialize(){this.initializeCloseButton(),this.initializeFullscreenButton(),this.initializeEscapeKey(),this.initializeParentMessages(),this.handlePostSaveState(),this.options.justSaved||this.focusFirstElement()}initializeCloseButton(){this.closeButton&&this.closeButton.addEventListener("click",()=>{this.requestClose()})}initializeFullscreenButton(){this.fullscreenButton&&this.fullscreenButton.addEventListener("click",t=>{t.preventDefault();const e=()=>{document.querySelectorAll(".has-change").forEach(o=>o.classList.remove("has-change")),top?.TYPO3?.Backend?.ContentContainer?.setUrl(this.fullscreenButton.href),window.parent.postMessage({actionName:"typo3:editform:navigate"},window.location.origin)};s.hasChange()?a.confirm(i.get("label.confirm.close_without_save.title"),i.get("label.confirm.close_without_save.content"),r.warning,[{text:i.get("buttons.confirm.close_without_save.no"),btnClass:"btn-default",trigger:(o,n)=>{n.hideModal()}},{text:i.get("buttons.confirm.close_without_save.yes"),btnClass:"btn-warning",trigger:(o,n)=>{n.hideModal(),e()}}]):e()})}initializeEscapeKey(){document.addEventListener("keydown",t=>{t.key==="Escape"&&(t.preventDefault(),this.requestClose())})}initializeParentMessages(){window.addEventListener("message",t=>{t.origin===window.location.origin&&t.data?.actionName==="typo3:editform:requestclose"&&this.requestClose()})}handlePostSaveState(){this.options.justSaved&&(window.parent.postMessage({actionName:"typo3:editform:saved",recordTitle:this.options.savedRecordTitle??""},window.location.origin),this.showSavedIndicator()),this.options.closed&&this.notifyParentClose()}requestClose(){s.hasChange()?a.confirm(i.get("label.confirm.close_without_save.title"),i.get("label.confirm.close_without_save.content"),r.warning,[{text:i.get("buttons.confirm.close_without_save.no"),btnClass:"btn-default",trigger:(t,e)=>{e.hideModal()}},{text:i.get("buttons.confirm.close_without_save.yes"),btnClass:"btn-default",trigger:(t,e)=>{e.hideModal(),this.notifyParentClose()}},{text:i.get("buttons.confirm.save_and_close"),btnClass:"btn-primary",active:!0,trigger:(t,e)=>{e.hideModal(),s.saveAndCloseDocument()}}]):this.notifyParentClose()}showSavedIndicator(){const t=document.querySelector(".contextual-record-edit-actions");if(!t)return;const e=document.createElement("span");e.className="contextual-record-edit-saved-indicator",e.innerHTML=' '+i.get("notification.record_saved.title.singular"),t.prepend(e),setTimeout(()=>{e.style.opacity="0",e.addEventListener("transitionend",()=>e.remove())},2e3)}focusFirstElement(){(this.fullscreenButton??this.closeButton)?.focus()}notifyParentClose(){window.parent.postMessage({actionName:"typo3:editform:closed"},window.location.origin)}}export{c as default}; diff --git a/Resources/Public/JavaScript/copy-to-clipboard.js b/Resources/Public/JavaScript/copy-to-clipboard.js new file mode 100644 index 0000000..27cc3be --- /dev/null +++ b/Resources/Public/JavaScript/copy-to-clipboard.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as u,customElement as m}from"lit/decorators.js";import{PseudoButtonLitElement as y}from"@typo3/backend/element/pseudo-button.js";import c from"@typo3/backend/notification.js";import p from"~labels/backend.copytoclipboard";var l=function(t,o,e,n){var a=arguments.length,r=a<3?o:n===null?n=Object.getOwnPropertyDescriptor(o,e):n,s;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")r=Reflect.decorate(t,o,e,n);else for(var d=t.length-1;d>=0;d--)(s=t[d])&&(r=(a<3?s(r):a>3?s(o,e,r):s(o,e))||r);return a>3&&r&&Object.defineProperty(o,e,r),r};function f(t,o=!1){if(!t.length){console.warn("No text for copy to clipboard given."),o||c.error(p.get("copyToClipboard.error"));return}if(navigator.clipboard)navigator.clipboard.writeText(t).then(()=>{document.dispatchEvent(new CustomEvent("copy-to-clipboard-success")),o||c.success(p.get("copyToClipboard.success"),"",1)}).catch(()=>{document.dispatchEvent(new CustomEvent("copy-to-clipboard-error")),o||c.error(p.get("copyToClipboard.error"))});else{const e=document.createElement("textarea");e.value=t,document.body.appendChild(e),e.focus(),e.select();try{document.execCommand("copy")?(document.dispatchEvent(new CustomEvent("copy-to-clipboard-success")),o||c.success(p.get("copyToClipboard.success"),"",1)):o||(document.dispatchEvent(new CustomEvent("copy-to-clipboard-error")),c.error(p.get("copyToClipboard.error")))}catch{o||(document.dispatchEvent(new CustomEvent("copy-to-clipboard-error")),c.error(p.get("copyToClipboard.error")))}document.body.removeChild(e)}}let i=class extends y{constructor(){super(...arguments),this.silent=!1}buttonActivated(){if(typeof this.text!="string"){console.warn("No text for copy to clipboard given."),this.silent||(document.dispatchEvent(new CustomEvent("copy-to-clipboard-error")),c.error(p.get("copyToClipboard.error")));return}f(this.text,this.silent)}};l([u({type:String})],i.prototype,"text",void 0),l([u({type:Boolean})],i.prototype,"silent",void 0),i=l([m("typo3-copy-to-clipboard")],i);export{i as CopyToClipboard,f as copyToClipboard}; diff --git a/Resources/Public/JavaScript/date-time-picker.js b/Resources/Public/JavaScript/date-time-picker.js new file mode 100644 index 0000000..c9ef41b --- /dev/null +++ b/Resources/Public/JavaScript/date-time-picker.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import m from"flatpickr";import p from"@typo3/backend/storage/persistent.js";import f from"shortcut-buttons-flatpickr";import{DateTime as u}from"luxon";import D from"@typo3/backend/utility/dom-helper.js";import g from"@typo3/core/event/throttle-event.js";import"@typo3/backend/input/clearable.js";import h from"~labels/core.core";const c="ISO8601_LOCALTIME";class b{constructor(){this.format=(typeof opener?.top?.TYPO3<"u"?opener.top:top).TYPO3.settings.DateConfiguration}initialize(n,i=null){if(!(n instanceof HTMLInputElement)||typeof n.dataset.datepickerInitialized<"u")return;let o=document.documentElement.lang;!o||o==="en"?o="default":o==="ch"&&(o="zh"),n.dataset.datepickerInitialized="1",import("flatpickr/dist/l10n").then(()=>{this.initializeField(n,o,i)})}initializeField(n,i,o=null){const r=this.getDateOptions(n);r.locale=i,o&&(r.appendTo=o);const e=p.get("dateTimeFirstDayOfWeek");if(e!=null&&e!==""){const t=parseInt(e,10)-1;m.l10ns[i].firstDayOfWeek=t,m.l10ns.default.firstDayOfWeek=t}if(o instanceof HTMLElement&&o.localName==="typo3-formengine-element-datetime")r.position=()=>{},r.onOpen=[(t,d,l)=>{l.calendarContainer.hasAttribute("popover")||l.calendarContainer.setAttribute("popover","manual"),l.calendarContainer.showPopover()}],r.onClose=(t,d,l)=>{l.calendarContainer.matches(":popover-open")&&l.calendarContainer.hidePopover()};else{const t=this.getScrollEvent();r.onOpen=[()=>{t.bindTo(D.scrollEventTarget(n))}],r.onClose=()=>{t.release()}}const a=m(n,r);a.altInput instanceof HTMLInputElement&&a.input.addEventListener("typo3:internal:clear",()=>{a.clear()}),a._input.addEventListener("change",t=>{const d=t.target.value,l=a.parseDate(d,a.config.altFormat);a.setDate(l,!0)}),a._input.addEventListener("keyup",t=>{t.key==="Escape"&&a.close()})}getScrollEvent(){return new g("scroll",()=>{const n=document.querySelector(".flatpickr-input.active");if(n===null)return;const i=n.getBoundingClientRect(),o=2,r=n._flatpickr.calendarContainer.offsetHeight,s=window.innerHeight-i.bottomr;let a,t;s?(a=i.y-r-o,t="arrowBottom"):(a=i.y+i.height+o,t="arrowTop"),n._flatpickr.calendarContainer.style.top=a+"px",n._flatpickr.calendarContainer.classList.remove("arrowBottom","arrowTop"),n._flatpickr.calendarContainer.classList.add(t)},15)}getDateOptions(n){const i=this.format,o=n.dataset.dateType,r=new Date,e={altFormat:"",allowInput:!0,altInput:!0,ariaDateFormat:"DDDD",dateFormat:c,defaultHour:r.getHours(),defaultMinute:r.getMinutes(),enableSeconds:!1,enableTime:!1,formatDate:(s,a)=>{const t=u.fromJSDate(s);return a===c?t.toISO({suppressMilliseconds:!0,includeOffset:!1}):t.toFormat(a)},parseDate:(s,a)=>{if(a===c){const t=u.fromISO(s);if(!t.isValid)throw new Error("Invalid ISO8601 date: "+s);return t.toJSDate()}return u.fromFormat(s,a).toJSDate()},onReady:(s,a,t)=>{if(t.altInput!==void 0){t.altInput.id=t.input.id,t.input.removeAttribute("id"),t.altInput.setAttribute("autocomplete","off"),t.altInput.clearable(),t.input.dataset.formengineInputName!==void 0&&(t.altInput.dataset.formengineDatepickerRealInputName=t.input.dataset.formengineInputName),t.altInput.form.addEventListener("t3-formengine-postfieldvalidation",l=>{l.detail.field===t.input&&t.altInput.classList.toggle("has-error",!l.detail.isValid)});const d=t.altInput.closest(".form-control-clearable-wrapper");d!==null&&d.insertAdjacentElement("afterend",t.input)}},onChange:(s,a,t)=>{t.input.dispatchEvent(new Event("formengine.dp.change"))},maxDate:"",minDate:"",minuteIncrement:1,noCalendar:!1,showMonths:1,monthSelectorType:o.startsWith("date")?"dropdown":"static",weekNumbers:!0,time_24hr:!Intl.DateTimeFormat(navigator.language,{hour:"numeric"}).resolvedOptions().hour12,plugins:[f({theme:"typo3",button:[{label:h.get("labels.datepicker.today")}],onClick:(s,a)=>{a.setDate(new Date,!0)}})]};switch(o){case"datetime":e.altFormat=i.formats.datetime,e.enableTime=!0;break;case"date":e.altFormat=i.formats.date;break;case"time":e.altFormat="HH:mm",e.enableTime=!0,e.noCalendar=!0;break;case"timesec":e.altFormat="HH:mm:ss",e.enableSeconds=!0,e.enableTime=!0,e.noCalendar=!0;break;case"datetimesec":e.altFormat=i.formats.date+" HH:mm:ss",e.enableSeconds=!0,e.enableTime=!0;break;case"year":e.altFormat="yyyy";break;default:}return n.dataset.dateMinDate!==void 0&&(e.minDate=e.parseDate(n.dataset.dateMinDate,c),e.minDate.setSeconds(0)),n.dataset.dateMaxDate!==void 0&&(e.maxDate=e.parseDate(n.dataset.dateMaxDate,c),e.maxDate.setSeconds(59)),e}}var v=new b;export{v as default}; diff --git a/Resources/Public/JavaScript/drag-tooltip.js b/Resources/Public/JavaScript/drag-tooltip.js new file mode 100644 index 0000000..9b6eb15 --- /dev/null +++ b/Resources/Public/JavaScript/drag-tooltip.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as f,nothing as p,html as c}from"lit";import{property as l,state as g,customElement as m}from"lit/decorators.js";import{BroadcastMessage as u}from"@typo3/backend/broadcast-message.js";import b from"@typo3/backend/broadcast-service.js";import{DataTransferTypes as v}from"@typo3/backend/enum/data-transfer-types.js";import{ThumbnailSize as y}from"@typo3/backend/element/thumbnail-element.js";var s=function(h,t,e,i){var o=arguments.length,r=o<3?t:i===null?i=Object.getOwnPropertyDescriptor(t,e):i,n;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")r=Reflect.decorate(h,t,e,i);else for(var d=h.length-1;d>=0;d--)(n=h[d])&&(r=(o<3?n(r):o>3?n(t,e,r):n(t,e))||r);return o>3&&r&&Object.defineProperty(t,e,r),r};let a=class extends f{constructor(){super(),this.active=!1,this.statusIconIdentifier="apps-pagetree-drag-move-into",this.tooltipIconIdentifier=null,this.thumbnails=[],this.visible=!1,this.posX=0,this.posY=0,this.dragAllowed=!1,this.skipNextUpdateBroadcast=!1,this.eventAbortController=null,this.updatePositionFromDragEvent=t=>{this.visible=!(t.clientX===0&&t.clientY===0);const e=this.calculateIframeOffset(t.view,window);this.posX=t.clientX+e.x,this.posY=t.clientY+e.y,this.visible&&this.broadcast("visible")},this.trackDragOverAllowed=t=>{this.dragAllowed=t.defaultPrevented},this.trackDragEnd=()=>{this.active=!1},this.trackDragStart=t=>{if(!t.defaultPrevented&&t.dataTransfer.types.includes(v.dragTooltip)){t.dataTransfer.setDragImage(this.ghostImage,0,0);const e=JSON.parse(t.dataTransfer.getData(v.dragTooltip));this.reset(),Object.assign(this,e),this.broadcast("visible")}},this.onMetadataUpdate=t=>{const e=t.detail;Object.assign(this,e)},this.onBroadcastVisible=()=>{this.visible=!1},this.onBroadcastChangedProperties=t=>{const e=t.detail.payload;Object.keys(e).forEach(i=>{this[i]=e[i]}),this.skipNextUpdateBroadcast=!0},this.onIframeLoaded=t=>{let e;try{e=t.target.querySelector("iframe")?.contentWindow}catch{return}if(e){this.eventAbortController?.abort(),this.eventAbortController=new AbortController;const{signal:i}=this.eventAbortController,o=!0,r=!0;e.addEventListener("dragover",this.updatePositionFromDragEvent,{capture:o,passive:r,signal:i}),e.addEventListener("dragover",this.trackDragOverAllowed,{passive:r,signal:i}),e.addEventListener("dragend",this.trackDragEnd,{capture:o,passive:r,signal:i}),e.addEventListener("dragstart",this.trackDragStart,{passive:r,signal:i})}},this.ghostImage=new Image,this.ghostImage.src="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="}connectedCallback(){super.connectedCallback();const t=!0,e=!0;window.addEventListener("dragover",this.updatePositionFromDragEvent,{capture:t,passive:e}),window.addEventListener("dragover",this.trackDragOverAllowed,{passive:e}),window.addEventListener("dragend",this.trackDragEnd,{capture:t,passive:e}),window.addEventListener("dragstart",this.trackDragStart,{passive:e}),document.addEventListener("typo3:drag-tooltip:visible",this.onBroadcastVisible),document.addEventListener("typo3:drag-tooltip:changedProperties",this.onBroadcastChangedProperties),document.addEventListener("typo3:drag-tooltip:metadata-update",this.onMetadataUpdate),document.addEventListener("typo3-iframe-loaded",this.onIframeLoaded),this.eventAbortController?.abort(),this.eventAbortController=new AbortController}disconnectedCallback(){super.disconnectedCallback();const t=!0;window.removeEventListener("dragover",this.updatePositionFromDragEvent,{capture:t}),window.removeEventListener("dragover",this.trackDragOverAllowed),window.removeEventListener("dragend",this.trackDragEnd,{capture:t}),window.removeEventListener("dragstart",this.trackDragStart),document.removeEventListener("typo3:drag-tooltip:visible",this.onBroadcastVisible),document.removeEventListener("typo3:drag-tooltip:changedProperties",this.onBroadcastChangedProperties),document.removeEventListener("typo3:drag-tooltip:metadata-update",this.onMetadataUpdate),document.removeEventListener("typo3-iframe-loaded",this.onIframeLoaded),this.eventAbortController?.abort(),this.eventAbortController=null}reset(){this.active=!0,this.visible=!0,this.statusIconIdentifier="apps-pagetree-drag-move-into",this.tooltipIconIdentifier="",this.tooltipLabel="",this.tooltipDescription="",this.thumbnails=[],this.posX=0,this.posY=0,this.dragAllowed=!1}updated(t){if(this.skipNextUpdateBroadcast){this.skipNextUpdateBroadcast=!1;return}const e=[...t.keys()].filter(o=>this.constructor.elementProperties.get(o).attribute!==!1);if(e.length===0)return;const i=e.map(o=>[o,this[o]]);this.broadcast("changedProperties",Object.fromEntries(i))}broadcast(t,e){b.post(new u("drag-tooltip",t,e||{}))}calculateIframeOffset(t,e){let i=0,o=0;if(t===e)return{x:i,y:o};const r=this.calculateIframeOffset(t.parent,e);i+=r.x,o+=r.y;const n=t.frameElement;if(n){const d=n.getBoundingClientRect();i+=d.x,o+=d.y}return{x:i,y:o}}createRenderRoot(){return this}render(){return!this.active||!this.visible?p:this.posX===0&&this.posY===0?p:c`
    ${this.thumbnails.length===0?p:c`
    ${this.thumbnails.slice(0,3).map(t=>c``)}
    `}
    `}};s([l({type:Boolean,reflect:!0})],a.prototype,"active",void 0),s([l({type:String,reflect:!0})],a.prototype,"statusIconIdentifier",void 0),s([l({type:String})],a.prototype,"tooltipIconIdentifier",void 0),s([l({type:String})],a.prototype,"tooltipLabel",void 0),s([l({type:String})],a.prototype,"tooltipDescription",void 0),s([l({type:Array})],a.prototype,"thumbnails",void 0),s([g()],a.prototype,"visible",void 0),s([g()],a.prototype,"posX",void 0),s([g()],a.prototype,"posY",void 0),s([g()],a.prototype,"dragAllowed",void 0),a=s([m("typo3-backend-drag-tooltip")],a);export{a as DragToolTip}; diff --git a/Resources/Public/JavaScript/drag-uploader.js b/Resources/Public/JavaScript/drag-uploader.js new file mode 100644 index 0000000..71eb4c8 --- /dev/null +++ b/Resources/Public/JavaScript/drag-uploader.js @@ -0,0 +1,55 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import L from"@typo3/core/document-service.js";import{SeverityEnum as c}from"@typo3/backend/enum/severity.js";import{MessageUtility as w}from"@typo3/backend/utility/message-utility.js";import S from"@typo3/core/ajax/ajax-request.js";import b,{Sizes as z}from"@typo3/backend/modal.js";import m from"@typo3/backend/notification.js";import A from"@typo3/backend/action-button/immediate-action.js";import x from"@typo3/backend/hashing/md5.js";import"@typo3/backend/element/icon-element.js";import E from"@typo3/core/event/regular-event.js";import k from"@typo3/backend/utility/dom-helper.js";import{KeyTypesEnum as D}from"@typo3/backend/enum/key-types.js";import"@typo3/backend/element/progress-bar-element.js";import{topLevelModuleImport as I}from"@typo3/backend/utility/top-level-module-import.js";import{FormatUtility as u}from"@typo3/backend/utility/format-utility.js";import r from"~labels/core.core";import U from"~labels/core.common";import v from"~labels/filelist.mod_file_list";var n;(function(h){h.OVERRIDE="replace",h.RENAME="rename",h.SKIP="cancel",h.USE_EXISTING="useExisting"})(n||(n={}));var d;(function(h){h.MANAGE="manage",h.BROWSE="browse"})(d||(d={}));const g="typo3:drag-uploader:upload-finished";class p{constructor(t){this.askForOverride=[],this.percentagePerFile=1,this.overallProgressBar=null,this.dragStartedInDocument=!1,this.closeDropzone=()=>{this.dropzone.hidden=!0,this.dropzone.classList.remove("drop-status-ok"),this.irreObjectUid!==void 0&&(this.fileList.parentElement.hidden=!0),this.manuallyTriggered=!1},this.hideDropzone=e=>{e.stopPropagation(),e.preventDefault(),this.closeDropzone()},this.dragFileIntoDocument=e=>(this.dragStartedInDocument||!e.dataTransfer.types.includes("Files")||(e.stopPropagation(),e.preventDefault(),e.currentTarget.classList.add("drop-in-progress"),this.element.offsetParent&&this.showDropzone()),!1),this.dragAborted=e=>(e.stopPropagation(),e.preventDefault(),e.currentTarget.classList.remove("drop-in-progress"),this.dragStartedInDocument=!1,!1),this.ignoreDrop=e=>(e.stopPropagation(),e.preventDefault(),this.dragAborted(e),!1),this.handleDrop=e=>{this.ignoreDrop(e),this.hideDropzone(e),this.processFiles(e.dataTransfer.files)},this.fileInDropzone=()=>{this.dropzone.classList.add("drop-status-ok")},this.fileOutOfDropzone=()=>{this.dropzone.classList.remove("drop-status-ok"),this.manuallyTriggered||this.dropzone.setAttribute("hidden","hidden")},this.body=document.querySelector("body"),this.element=t,this.trigger=document.querySelector(this.element.dataset.dropzoneTrigger),this.defaultAction=this.element.dataset.defaultAction||n.SKIP,this.dropzone=document.createElement("div"),this.dropzone.classList.add("dropzone"),this.dropzone.setAttribute("hidden","hidden"),this.irreObjectUid=this.element.dataset.fileIrreObject,this.manualTable=this.element.hasAttribute("data-manual-table");const s=document.querySelector(this.element.dataset.dropzoneTarget);if(this.irreObjectUid&&k.nextAll(s).length!==0?(this.dropZoneInsertBefore=!0,s.before(this.dropzone)):(this.dropZoneInsertBefore=!1,s.after(this.dropzone)),this.fileInput=document.createElement("input"),this.fileInput.setAttribute("type","file"),this.fileInput.setAttribute("multiple","multiple"),this.fileInput.setAttribute("name","files[]"),this.fileInput.classList.add("upload-file-picker"),this.body.append(this.fileInput),this.fileList=document.querySelector(this.element.dataset.progressContainer),this.fileListColumnCount=this.fileList?.querySelectorAll("thead tr:first-child th").length+1,this.filesExtensionsAllowed=this.element.dataset.fileAllowed,this.filesExtensionsDisallowed=this.element.dataset.fileDisallowed,this.fileDenyPattern=this.element.dataset.fileDenyPattern?new RegExp(this.element.dataset.fileDenyPattern,"i"):null,this.maxFileSize=parseInt(this.element.dataset.maxFileSize,10),this.target=this.element.dataset.targetFolder,this.reloadUrl=this.element.dataset.reloadUrl,this.browserCapabilities={fileReader:typeof FileReader<"u",DnD:"draggable"in document.createElement("span"),Progress:"upload"in new XMLHttpRequest},!this.browserCapabilities.DnD){console.warn("Browser has no Drag and drop capabilities; cannot initialize DragUploader");return}this.body.addEventListener("dragstart",()=>{this.dragStartedInDocument=!0}),this.body.addEventListener("dragover",this.dragFileIntoDocument),this.body.addEventListener("dragend",this.dragAborted),this.body.addEventListener("drop",this.ignoreDrop),this.dropzone.innerHTML='"),this.dragUploader.irreObjectUid?(p.addFileToIrre(this.dragUploader.irreObjectUid,t.upload[0]),setTimeout(()=>{this.row.remove(),this.dragUploader.fileList.querySelectorAll("tr").length===0&&(this.dragUploader.fileList.setAttribute("hidden","hidden"),this.dragUploader.fileList.closest(".t3-filelist-container")?.classList.add("hidden"),this.dragUploader.trigger?.dispatchEvent(new CustomEvent("uploadSuccess",{detail:[this,t]})))},3e3)):setTimeout(()=>{this.showFileInfo(t.upload[0]),this.dragUploader.trigger?.dispatchEvent(new CustomEvent("uploadSuccess",{detail:[this,t]}))},3e3)}}showFileInfo(t){if(this.removeProgress(),document.querySelector("#filelist-searchterm")?.value){const i=document.createElement("td");i.textContent=t.path,this.row.append(i)}const s=document.createElement("td");s.classList.add("col-control"),this.row.append(s);const o=document.createElement("td");o.textContent=U.get("file")+" ("+t.extension.toUpperCase()+")",this.row.append(o);const e=document.createElement("td");if(e.textContent=u.fileSizeAsString(t.size),this.row.append(e),this.mode===d.MANAGE){let i="";t.permissions.read&&(i+=''+v.get("read")+""),t.permissions.write&&(i+=''+v.get("write")+"");const a=document.createElement("td");a.innerHTML=i,this.row.append(a);const l=document.createElement("td");l.textContent="-",this.row.append(l)}for(let i=this.row.querySelectorAll("td").length;i{const t=e.currentTarget,r=e.relatedTarget;if(r===null||t.contains(r))return;const o=r.closest(".dropdown-menu[popover]");if(o){const n=o.id;if(n&&t.querySelector(`[popovertarget="${n}"]`))return}t.hidePopover()},this.handleKeydown=e=>{const t=e.currentTarget,r=document.activeElement;if(r&&r.closest(".dropdown-menu")!==t)return;const o=this.getFocusableItems(t);if(o.length===0)return;const n=o.findIndex(s=>s===r),u=this.isRtl(t),i=e.key==="ArrowRight"&&!u||e.key==="ArrowLeft"&&u,l=e.key==="ArrowLeft"&&!u||e.key==="ArrowRight"&&u;switch(e.key){case"ArrowDown":e.preventDefault(),o[(n+1)%o.length].focus();break;case"ArrowUp":e.preventDefault(),o[(n-1+o.length)%o.length].focus();break;case"ArrowRight":case"ArrowLeft":if(i){if(r?.hasAttribute("popovertarget")){e.preventDefault(),e.stopPropagation();const s=r.getAttribute("popovertarget"),a=s?document.getElementById(s):null;if(a?.matches("[popover]")){a.showPopover();const d=this.getFocusableItems(a);d.length>0&&d[0].focus()}}}else if(l){const s=document.querySelector(`[popovertarget="${t.id}"]`);s?.closest(".dropdown-menu")&&(e.preventDefault(),e.stopPropagation(),t.hidePopover(),s.focus())}break;case"Escape":{e.preventDefault(),e.stopPropagation();const s=document.querySelector(`[popovertarget="${t.id}"]`);t.hidePopover(),s?.focus();break}case"Home":e.preventDefault(),o[0].focus();break;case"End":e.preventDefault(),o[o.length-1].focus();break;default:break}},document.addEventListener("toggle",e=>{const t=e.target;t.matches(".dropdown-menu[popover]")&&(e.newState==="open"?(t.addEventListener("keydown",this.handleKeydown),t.addEventListener("focusout",this.handleFocusout)):(t.removeEventListener("keydown",this.handleKeydown),t.removeEventListener("focusout",this.handleFocusout)))},!0),document.addEventListener("keydown",e=>{const t=e.target?.closest(".dropdown-toggle[popovertarget]");if(t&&!t.closest(".dropdown-menu")&&(e.key==="ArrowDown"||e.key==="ArrowUp")){const r=t.getAttribute("popovertarget"),o=r?document.getElementById(r):null;if(!o?.matches("[popover]"))return;e.preventDefault(),o.showPopover();const n=this.getFocusableItems(o);n.length>0&&(e.key==="ArrowDown"?n[0].focus():n[n.length-1].focus())}}),window.addEventListener("blur",()=>{document.querySelectorAll(".dropdown-menu[popover]:popover-open").forEach(e=>{e.hidePopover()})}),document.addEventListener("click",e=>{const r=e.target.closest(".dropdown-item");if(!r||r.hasAttribute("popovertarget"))return;let o=r.closest(".dropdown-menu[popover]");for(;o;)o.hidePopover(),o=o.closest(".dropdown-menu[popover]:popover-open")})}getFocusableItems(e){return Array.from(e.querySelectorAll(".dropdown-item:not(:disabled):not(.disabled)")).filter(t=>t.closest(".dropdown-menu")===e)}isRtl(e){return getComputedStyle(e).direction==="rtl"}}new m;class f{constructor(){this.dropdownIdCounter=0,document.addEventListener("click",e=>{const t=e.target?.closest('[data-bs-toggle="dropdown"]');if(t){e.preventDefault();const r=this.getMenu(t);this.convert(t),r?.togglePopover()}}),p.ready().then(()=>this.convertAll())}convertAnchorToButton(e){const t=document.createElement("button");t.type="button";for(const r of e.attributes)if(r.name==="href"){const o=r.value;o.startsWith("#")&&!e.hasAttribute("data-bs-target")&&t.setAttribute("data-bs-target",o)}else t.setAttribute(r.name,r.value);return t.innerHTML=e.innerHTML,e.replaceWith(t),t}getMenu(e){const t=e.dataset.bsTarget;if(t){const o=t.startsWith("#")?t:"#"+t;return document.querySelector(o)}const r=e.getAttribute("href");return r?.startsWith("#")?document.querySelector(r):e.nextElementSibling?.matches(".dropdown-menu")?e.nextElementSibling:e.closest(".dropdown")?.querySelector(".dropdown-menu")??null}convert(e){if(e.hasAttribute("popovertarget"))return null;const t=this.getMenu(e);return t?(t.id||(t.id="dropdown-menu-"+this.dropdownIdCounter++),t.setAttribute("popover",""),e.tagName==="A"&&(e=this.convertAnchorToButton(e)),e.closest(".dropdown")||e.parentElement?.classList.add("dropdown"),e.setAttribute("popovertarget",t.id),e.removeAttribute("data-bs-toggle"),e.removeAttribute("data-bs-target"),e.removeAttribute("data-bs-offset"),e.removeAttribute("data-bs-auto-close"),e.removeAttribute("data-bs-reference"),e.removeAttribute("data-bs-display"),e.removeAttribute("data-bs-boundary"),e.removeAttribute("aria-haspopup"),e.removeAttribute("aria-expanded"),e):null}convertAll(){document.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(e=>this.convert(e))}}new f; diff --git a/Resources/Public/JavaScript/element-browser.js b/Resources/Public/JavaScript/element-browser.js new file mode 100644 index 0000000..e54eb14 --- /dev/null +++ b/Resources/Public/JavaScript/element-browser.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{MessageUtility as o}from"@typo3/backend/utility/message-utility.js";import a from"@typo3/core/document-service.js";import d from"@typo3/backend/modal.js";class f{constructor(){this.opener=null,this.fieldReference="",this.irre={objectId:""},this.focusOpenerAndClose=()=>{this.getParent()&&this.getParent().focus(),d.dismiss(),close()},a.ready().then(()=>{const e=document.body.dataset;this.fieldReference=e.fieldReference,this.irre.objectId=e.irreObjectId})}getParent(){const e=typeof window.frames<"u"&&typeof window.frames.frameElement<"u"&&window.frames.frameElement.classList.contains("t3js-modal-iframe"),t=Array.from(top.frames||[]).filter(n=>{try{return typeof n.frameElement<"u"&&n.frameElement.classList.contains("t3js-modal-iframe")&&n.frameElement!==window.frames.frameElement}catch{return!1}});if(this.opener===null&&(e&&t.length>0?this.opener=t.pop():typeof window.parent<"u"&&typeof window.parent.document.list_frame<"u"&&window.parent.document.list_frame.parent.document.querySelector(".t3js-modal-iframe")!==null?this.opener=window.parent.document.list_frame:typeof window.parent<"u"&&typeof window.parent.frames.list_frame<"u"&&window.parent.frames.list_frame.parent.document.querySelector(".t3js-modal-iframe")!==null?this.opener=window.parent.frames.list_frame:typeof window.frames<"u"&&typeof window.frames.frameElement<"u"&&window.frames.frameElement!==null&&window.frames.frameElement.classList.contains("t3js-modal-iframe")?this.opener=window.frames.frameElement.contentWindow.parent:window.opener&&(this.opener=window.opener),this.opener&&!this.windowHasEditForm(this.opener))){const n=this.findEditFormWindow();n&&(this.opener=n)}return this.opener}insertElement(e,t,n,r,i){if(this.irre.objectId){if(this.getParent()){const s={actionName:"typo3:foreignRelation:insert",objectGroup:this.irre.objectId,table:e,uid:t};o.send(s,this.getParent())}else alert("Error - reference to main window is not set properly!"),this.focusOpenerAndClose();return i&&setTimeout(()=>this.focusOpenerAndClose(),0),!0}return this.fieldReference&&this.addElement(n,r||e+"_"+t,i),!1}windowHasEditForm(e){return e?.document?.querySelector('form[name="editform"]')!==null}findEditFormWindow(){try{for(let e=0;ethis.focusOpenerAndClose(),0)):(alert("Error - reference to main window is not set properly!"),this.focusOpenerAndClose())}dispatch(e){window.frameElement.dispatchEvent(new CustomEvent("typo3:element-browser:message",{bubbles:!0,detail:e}))}}var m=new f;export{m as default}; diff --git a/Resources/Public/JavaScript/element/alert-element.js b/Resources/Public/JavaScript/element/alert-element.js new file mode 100644 index 0000000..c1948d0 --- /dev/null +++ b/Resources/Public/JavaScript/element/alert-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as r,customElement as b}from"lit/decorators.js";import{LitElement as v,html as p,nothing as h}from"lit";import{classMap as y}from"lit/directives/class-map.js";import{SeverityEnum as n}from"@typo3/backend/enum/severity.js";import g from"@typo3/backend/severity.js";import"@typo3/backend/element/icon-element.js";import u from"~labels/core.mod_web_list";var s=function(a,t,o,l){var c=arguments.length,i=c<3?t:l===null?l=Object.getOwnPropertyDescriptor(t,o):l,d;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")i=Reflect.decorate(a,t,o,l);else for(var m=a.length-1;m>=0;m--)(d=a[m])&&(i=(c<3?d(i):c>3?d(t,o,i):d(t,o))||i);return c>3&&i&&Object.defineProperty(t,o,i),i},f;let e=f=class extends v{constructor(){super(...arguments),this.severity=n.info,this.dismissible=!1,this.visible=!0,this.heading=null,this.message=null,this.showIcon=!1,this.randomSuffix=Math.random().toString(36).substring(7)}static getIconIdentifier(t){return{[n.notice]:"actions-lightbulb",[n.ok]:"actions-check",[n.warning]:"actions-exclamation",[n.error]:"actions-close",[n.info]:"actions-info"}[t]||"actions-info"}createRenderRoot(){return this}render(){return p``}getClasses(){return{alert:!0,["alert-"+g.getCssClass(this.severity)]:!0,"alert-dismissible":this.dismissible,fade:!0,show:this.visible,hidden:!this.visible}}renderDismissButton(){return p``}};s([r({type:Number})],e.prototype,"severity",void 0),s([r({type:Boolean})],e.prototype,"dismissible",void 0),s([r({type:Boolean})],e.prototype,"visible",void 0),s([r({type:String})],e.prototype,"heading",void 0),s([r({type:String})],e.prototype,"message",void 0),s([r({type:Boolean,attribute:"show-icon"})],e.prototype,"showIcon",void 0),e=f=s([b("typo3-backend-alert")],e);export{e as AlertElement}; diff --git a/Resources/Public/JavaScript/element/breadcrumb.js b/Resources/Public/JavaScript/element/breadcrumb.js new file mode 100644 index 0000000..4ae93ff --- /dev/null +++ b/Resources/Public/JavaScript/element/breadcrumb.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as v,html as n,nothing as l,render as m}from"lit";import{property as b,customElement as C}from"lit/decorators.js";import{classMap as f}from"lit/directives/class-map.js";import"@typo3/backend/element/icon-element.js";import"@typo3/backend/dropdown.js";var p=function(d,e,t,i){var s=arguments.length,r=s<3?e:i===null?i=Object.getOwnPropertyDescriptor(e,t):i,o;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")r=Reflect.decorate(d,e,t,i);else for(var c=d.length-1;c>=0;c--)(o=d[c])&&(r=(s<3?o(r):s>3?o(e,t,r):o(e,t))||r);return s>3&&r&&Object.defineProperty(e,t,r),r},a;(function(d){d.FULL="full",d.PATH="path"})(a||(a={}));let u=class extends v{constructor(){super(...arguments),this.nodes=[],this.alignRight=!1,this.label="Breadcrumb",this.mode=a.FULL,this.collapsedNodes=[],this.adjustRafId=null,this.dropdownButtonWidth=null,this.ellipsisWidth=null,this.measurementContainer=null}connectedCallback(){super.connectedCallback(),this.identifier=Date.now().toString(36)+Math.random().toString(36).slice(2),this.resizeObserver=new ResizeObserver(()=>this.scheduleAdjust()),this.resizeObserver.observe(this),this.intersectionObserver=new IntersectionObserver(e=>{e.some(t=>t.isIntersecting)&&this.scheduleAdjust()}),this.intersectionObserver.observe(this)}disconnectedCallback(){this.resizeObserver.disconnect(),this.intersectionObserver.disconnect(),this.adjustRafId!==null&&(cancelAnimationFrame(this.adjustRafId),this.adjustRafId=null),super.disconnectedCallback()}createRenderRoot(){return this}willUpdate(e){e.has("nodes")&&(this.createMeasurementContainer(),this.measureEllipsis(),this.measureDropdownToggle(),this.measureNodeWidths())}render(){const e=this.nodes.filter(s=>!this.collapsedNodes.includes(s)),t=f({breadcrumb:!0,"breadcrumb-collapsible":!0,"breadcrumb-right":this.alignRight,"breadcrumb-condensed":this.mode===a.PATH}),i=n`${this.renderCollapsedNodesIndicator()} ${e.map(s=>{const r=f({"breadcrumb-item":!0,"breadcrumb-item-first":this.isFirstNode(s),"breadcrumb-item-last":this.isLastNode(s)});return n``})}`;return this.mode===a.FULL?n``:n` ${this.label?`${this.label}: `:l}${this.nodes.map(s=>s.label).join(" / ")} `}scheduleAdjust(){this.adjustRafId===null&&(this.adjustRafId=requestAnimationFrame(()=>{this.adjustRafId=null,this.adjustBreadcrumb()}))}createMeasurementContainer(){this.measurementContainer===null&&(this.measurementContainer=document.createElement("div"),this.measurementContainer.classList.add("breadcrumb-measurement"),this.measurementContainer.ariaHidden="true",this.measurementContainer.style.position="absolute",this.measurementContainer.style.visibility="hidden",this.measurementContainer.style.pointerEvents="none",this.measurementContainer.style.width="auto",this.measurementContainer.style.whiteSpace="nowrap",this.renderRoot.appendChild(this.measurementContainer)),this.measurementContainer.classList.toggle("breadcrumb-condensed",this.mode===a.PATH)}measureDropdownToggle(){if(this.dropdownButtonWidth===null){const e=n``;m(e,this.measurementContainer);const t=this.measurementContainer.querySelector("div");this.dropdownButtonWidth=t?.offsetWidth??0,m(l,this.measurementContainer)}}measureEllipsis(){if(this.ellipsisWidth===null){m(this.renderEllipsis(),this.measurementContainer);const e=this.measurementContainer.querySelector("div");this.ellipsisWidth=e?.offsetWidth??0,m(l,this.measurementContainer)}}measureNodeWidths(){const e=this.nodes,t=this.nodes.map(s=>{if(s.__width)return s;const r=n``;m(r,this.measurementContainer);const c=this.measurementContainer.querySelector("div")?.offsetWidth??0;return m(l,this.measurementContainer),{...s,__width:c}});!this.areNodeCollectionsEqual(e,t)&&(this.nodes=t)}adjustBreadcrumb(){const e=this.renderRoot?.querySelector(".breadcrumb");if(!e)return;this.ellipsisWidth||(this.ellipsisWidth=null,this.measureEllipsis()),this.dropdownButtonWidth||(this.dropdownButtonWidth=null,this.measureDropdownToggle()),this.nodes.some(h=>!h.__width)&&this.measureNodeWidths();const t=this.mode===a.PATH?this.ellipsisWidth:this.dropdownButtonWidth;let s=this.nodes[this.nodes.length-1]?.__width||0,r=!1;const o=[];for(const h of this.nodes.slice(0,-1).reverse())!r&&e.clientWidth>s+t+h.__width?s+=h.__width:(r=!0,o.push(h));o.reverse(),(o.length!==this.collapsedNodes.length||o.some((h,g)=>h!==this.collapsedNodes[g]))&&(this.collapsedNodes=o,this.requestUpdate())}renderCollapsedNodesIndicator(){return this.collapsedNodes.length===0?l:this.mode===a.PATH?this.renderEllipsis():this.renderCollapsedNodesDropdown()}renderEllipsis(){return n``}renderCollapsedNodesDropdown(){return n``}renderDropdownToggle(){return n``}renderDropdownItem(e){return e.url!==null?n``:n``}renderBreadcrumbElement(e){const t=this.mode===a.FULL&&(e.forceShowIcon||this.isLastNode(e));return this.mode===a.FULL&&e.url!==null?n``:n``}renderIcon(e){return e.icon?n``:null}triggerAction(e,t){import("@typo3/backend/viewport.js").then(({default:i})=>{i.ContentContainer.setUrl(t.url)})}isFirstNode(e){return this.nodes[0]===e}isLastNode(e){return this.nodes[this.nodes.length-1]===e}areNodeCollectionsEqual(e,t){if(t.length!==e.length)return!1;for(let i=0;i=0;h--)(l=u[h])&&(n=(s<3?l(n):s>3?l(t,e,n):l(t,e))||n);return s>3&&n&&Object.defineProperty(t,e,n),n};const d={INPUT:"input",NAVIGATION:"navigation"},f={NONE:"none",CURRENT:"current"};let E=0,a=class extends y{constructor(){super(...arguments),this.value="",this.icon="",this.disabled=!1,this.isSelected=!1}static{this.styles=v`*,:after,:before{box-sizing:border-box}:host{display:flex;align-items:center;width:100%;user-select:none}`}connectedCallback(){super.connectedCallback(),this.setAttribute("role","option"),this.setAttribute("aria-selected","false"),!this.value&&this.textContent&&(this.value=this.textContent.trim()),this.updateAriaLabel()}setSelected(t){this.isSelected=t}updated(t){t.has("value")&&this.updateAriaLabel()}render(){const t=this.getLabel(),e=this.value&&this.value!==t;return r` ${this.icon?r``:""} ${t} ${e?r`(${this.value})`:""} `}getLabel(){return this.textContent?.trim()||this.value}updateAriaLabel(){const t=this.getLabel(),e=this.value&&this.value!==t?` (${this.value})`:"";this.setAttribute("aria-label",`${t}${e}`)}};o([g({type:String})],a.prototype,"value",void 0),o([g({type:String})],a.prototype,"icon",void 0),o([g({type:Boolean})],a.prototype,"disabled",void 0),o([b()],a.prototype,"isSelected",void 0),a=o([C("typo3-backend-combobox-choice")],a);let c=class extends y{static{this.styles=v`*,:after,:before{box-sizing:border-box}:host{position:relative;display:block;width:100%}.controls{position:absolute;top:50%;inset-inline-end:0;transform:translateY(-50%);gap:.25rem;padding-inline-end:var(--typo3-form-combobox-padding-x,.75rem)}.clear-button,.controls{display:flex;align-items:center}.clear-button{justify-content:center;cursor:pointer;color:inherit;opacity:.3;transition:opacity .2s ease}.clear-button:hover{opacity:.5}.separator{width:1px;height:1.25rem;background-color:var(--typo3-input-border-color,currentColor)}.indicator{display:flex;align-items:center;justify-content:center}.listbox{position:absolute;top:100%;left:0;width:100%;z-index:1000}`}constructor(){super(),this.isOpen=!1,this.inputHasValue=!1,this.highlightedIndex=-1,this.inputIsDisabledOrReadonly=!1,this.choiceIdCounter=0,this.delayedCloseTimeout=null,this.internalId=null,this.lastAction=null,this.lastHighlightedIndex=null,this.userSelectionIntent=null,this.handleChoiceMouseEnter=t=>{const e=t.currentTarget,s=this.getChoiceElements().indexOf(e);s>=0&&this.setHighlightedIndex(s)},this.handleInputInit=()=>{this.updateInputHasValue(),this.updateSelectedState()},this.handleInputInput=()=>{this.lastAction=d.INPUT,this.highlightedIndex!==-1&&(this.lastHighlightedIndex=this.highlightedIndex,this.setHighlightedIndex(-1)),this.updateInputHasValue(),this.updateSelectedState()},this.handleInputPointerdown=t=>{t.button===0&&this.setOpen()},this.handleInputBlur=t=>{this.contains(t.relatedTarget)?this.getInput()?.focus():this.dispatchClose()},this.handleInputKeydown=t=>{const e=this.getInput();if(e?.disabled||e?.readOnly)return;const s=this.getChoiceElements().length;switch(t.key){case"ArrowDown":t.preventDefault(),t.stopPropagation(),t.altKey&&!this.isOpen?this.userSelectionIntent=f.NONE:(this.lastAction=d.NAVIGATION,this.setHighlightedIndex(this.getNextHighlightIndex(1,s))),this.setOpen();break;case"ArrowUp":t.preventDefault(),t.stopPropagation(),t.altKey&&!this.isOpen?this.userSelectionIntent=f.NONE:(this.lastAction=d.NAVIGATION,this.setHighlightedIndex(this.getNextHighlightIndex(-1,s))),this.setOpen();break;case"Enter":this.isOpen&&(this.highlightedIndex>=0&&this.lastAction===d.NAVIGATION&&(t.preventDefault(),this.selectChoiceOption(this.highlightedIndex)),this.immediateClose());break;case"Tab":this.isOpen&&(this.highlightedIndex>=0&&this.lastAction===d.NAVIGATION&&this.selectChoiceOption(this.highlightedIndex),this.immediateClose(),t.shiftKey||t.preventDefault());break;case"Escape":if(this.lastAction=d.NAVIGATION,this.isOpen)this.isOpen=!1,t.preventDefault(),t.stopPropagation();else{const n=this.getInput();n&&n.value&&!n.readOnly&&!n.disabled&&(this.clearInput(),this.isOpen=!1,t.preventDefault(),t.stopPropagation())}break;default:break}},this.handleClearClick=t=>{t.stopPropagation(),this.clearInput(),this.getInput()?.focus()},this.handleIndicatorClick=t=>{t.stopPropagation(),this.isOpen?this.immediateClose():(this.setOpen(),this.getInput()?.focus())},this.handleChoiceClick=t=>{const e=t.target,i=e.closest("typo3-backend-combobox-choice"),s=this.getInput();if(i&&!i.disabled){const l=this.getChoiceElements().indexOf(i);if(l>=0){this.selectChoiceOption(l),this.immediateClose(),s.focus();return}}s&&e!==s&&(s.focus(),this.setOpen())},this.addEventListener("click",this.handleChoiceClick)}focus(t){this.getInput()?.focus(t)}blur(){this.getInput()?.blur()}willUpdate(t){if(t.has("isOpen")&&this.isOpen&&this.userSelectionIntent!==f.NONE){const e=this.findCurrentSelectionChoice();if(e){const s=this.getChoiceElements().indexOf(e);this.setHighlightedIndex(s)}}}render(){const t=this.getInput();return t&&(t.setAttribute("aria-expanded",this.isOpen.toString()),t.setAttribute("aria-activedescendant",this.getActiveDescendantId())),r`${this.inputIsDisabledOrReadonly?"":r`
    ${this.inputHasValue?r` `:""}
    `}`}firstUpdated(){this.setup()}updated(t){t.has("isOpen")&&(this.isOpen?(this.positionDropdown(),this.scrollToHighlightedOption()):(this.setHighlightedIndex(-1),this.lastAction=null,this.userSelectionIntent=null))}getId(){return this.internalId??=this.id!==""?this.id:"combobox-"+E++,this.internalId}positionDropdown(){const t=this.shadowRoot?.querySelector(".listbox");if(!t)return;const e=this.getBoundingClientRect(),i=window.innerHeight,s=window.scrollY,n=window.getComputedStyle(t),l=n.maxHeight!=="none"?parseFloat(n.maxHeight):1/0,h=Math.min(t.scrollHeight,l),I=i-(e.bottom-s),m=e.top-s,x=II,p=x?m:I;requestAnimationFrame(()=>{x?(t.style.top="auto",t.style.bottom="100%",h>p?t.style.maxHeight=`${Math.floor(p)}px`:t.style.removeProperty("max-height")):(t.style.top="100%",t.style.bottom="auto",h>p?t.style.maxHeight=`${Math.floor(p)}px`:t.style.removeProperty("max-height"))})}scrollToHighlightedOption(){if(this.highlightedIndex<0)return;const e=this.getChoiceElements()[this.highlightedIndex];e&&e.scrollIntoView({block:"nearest",inline:"nearest"})}selectChoiceOption(t){const i=this.getChoiceElements()[t];if(!i||i.disabled)return;const s=i.value,n=this.getInput();n&&(n.value=s,this.dispatchInputEvents(n)),this.updateInputHasValue(),this.updateSelectedState(),this.setHighlightedIndex(t)}setHighlightedIndex(t){this.getChoiceElements().forEach((i,s)=>{i.setAttribute("aria-selected",s===t?"true":"false")}),this.highlightedIndex=t,this.scrollToHighlightedOption()}getNextHighlightIndex(t,e){if(this.highlightedIndex<0){const{lastHighlightedIndex:i}=this;if(i!==null&&i!==-1)return this.lastHighlightedIndex=null,i;const s=this.findCurrentSelectionChoice();return s?this.getChoiceElements().indexOf(s):t>0?0:e-1}return t>0?Math.min(this.highlightedIndex+1,e-1):Math.max(this.highlightedIndex-1,0)}setup(){this.setupInputAttributes(),this.setupChoiceElements(),this.updateInputHasValue(),this.updateSelectedState()}handleSlotChange(){this.setup()}setupChoiceElements(){this.querySelectorAll('typo3-backend-combobox-choice:not([slot="choices"])').forEach(e=>{e.slot="choices",e.tabIndex=-1,e.id||(e.id=`${this.getId()}-option-${++this.choiceIdCounter}`),e.addEventListener("mouseenter",this.handleChoiceMouseEnter)})}setupInputAttributes(){const t=this.getInput();t&&(t.removeEventListener("formengine:input:initialized",this.handleInputInit),t.removeEventListener("input",this.handleInputInput),t.removeEventListener("pointerdown",this.handleInputPointerdown),t.removeEventListener("blur",this.handleInputBlur),t.removeEventListener("keydown",this.handleInputKeydown),t.setAttribute("role","combobox"),t.setAttribute("aria-expanded","false"),t.setAttribute("aria-haspopup","listbox"),t.setAttribute("aria-controls",`${this.getId()}-listbox`),t.setAttribute("autocomplete","off"),t.addEventListener("formengine:input:initialized",this.handleInputInit),t.addEventListener("input",this.handleInputInput),t.addEventListener("pointerdown",this.handleInputPointerdown),t.addEventListener("blur",this.handleInputBlur),t.addEventListener("keydown",this.handleInputKeydown),this.updateInputDisabledState())}getInput(){const t=this.shadowRoot?.querySelector("slot:not([name])");return t?t.assignedElements().find(i=>i.tagName==="INPUT"):null}setOpen(){const t=this.getInput();this.updateInputDisabledState(),!(t?.disabled||t?.readOnly)&&(this.isOpen=!0,this.delayedCloseTimeout!==null&&(clearTimeout(this.delayedCloseTimeout),this.delayedCloseTimeout=null))}immediateClose(){this.isOpen=!1}dispatchClose(){this.delayedCloseTimeout===null&&(this.delayedCloseTimeout=setTimeout(()=>{this.immediateClose()},100))}getChoiceElements(){return Array.from(this.querySelectorAll("typo3-backend-combobox-choice"))}findCurrentSelectionChoice(){const t=this.getInput();return t?.value&&this.getChoiceElements().find(i=>i.value===t.value)||null}getActiveDescendantId(){const t=this.getChoiceElements();return this.highlightedIndex>=0&&t[this.highlightedIndex]?.id||""}updateSelectedState(){const t=this.getInput();this.getChoiceElements().forEach(i=>{const s=i.value===t?.value;i.setSelected(s)})}updateInputHasValue(){const t=this.getInput();this.inputHasValue=(t?.value||"").length>0}updateInputDisabledState(){const t=this.getInput();this.inputIsDisabledOrReadonly=t?.disabled||t?.readOnly||!1}dispatchInputEvents(t){t.dispatchEvent(new Event("input",{bubbles:!0})),t.dispatchEvent(new Event("change",{bubbles:!0}))}clearInput(){const t=this.getInput();t&&(t.value="",this.dispatchInputEvents(t),this.updateInputHasValue(),this.setHighlightedIndex(-1))}};o([b()],c.prototype,"isOpen",void 0),o([g({type:Boolean,reflect:!0,attribute:"data-has-value"})],c.prototype,"inputHasValue",void 0),o([b()],c.prototype,"highlightedIndex",void 0),o([b()],c.prototype,"inputIsDisabledOrReadonly",void 0),c=o([C("typo3-backend-combobox")],c);export{a as ComboboxChoiceElement,c as ComboboxElement}; diff --git a/Resources/Public/JavaScript/element/contextual-record-edit-trigger.js b/Resources/Public/JavaScript/element/contextual-record-edit-trigger.js new file mode 100644 index 0000000..c3d3f3d --- /dev/null +++ b/Resources/Public/JavaScript/element/contextual-record-edit-trigger.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as l,customElement as m}from"lit/decorators.js";import{PseudoButtonLitElement as u}from"@typo3/backend/element/pseudo-button.js";import g,{Positions as y,Sizes as h,Types as v}from"@typo3/backend/modal.js";import E from"@typo3/backend/notification.js";import p from"@typo3/backend/storage/persistent.js";import f from"~labels/backend.alt_doc";var c=function(s,e,i,n){var r=arguments.length,t=r<3?e:n===null?n=Object.getOwnPropertyDescriptor(e,i):n,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(s,e,i,n);else for(var o=s.length-1;o>=0;o--)(a=s[o])&&(t=(r<3?a(t):r>3?a(e,i,t):a(e,i))||t);return r>3&&t&&Object.defineProperty(e,i,t),t};let d=class extends u{async buttonActivated(){if(p.isset("contextualRecordEdit")&&p.get("contextualRecordEdit")==0){top?.TYPO3?.Backend?.ContentContainer&&top.TYPO3.Backend.ContentContainer.setUrl(this.editUrl);return}const e=g.advanced({type:v.iframe,title:"",content:this.url,size:h.expand,position:y.sheet,hideHeader:!0});this.setupMessageHandling(e)}setupMessageHandling(e){const i=top;let n="",r=!1,t=!1;const a=o=>{o.origin===window.location.origin&&(o.data?.actionName==="typo3:editform:saved"&&(r=!0,n=o.data.recordTitle??""),o.data?.actionName==="typo3:editform:closed"&&(t=!0,e.hideModal()),o.data?.actionName==="typo3:editform:navigate"&&(t=!0,e.hideModal()))};i.addEventListener("message",a),e.addEventListener("typo3-modal-hide",o=>{if(t)return;o.preventDefault(),e.querySelector("iframe")?.contentWindow?.postMessage({actionName:"typo3:editform:requestclose"},window.location.origin)}),e.addEventListener("typo3-modal-hidden",()=>{i.removeEventListener("message",a),r?(top.document.dispatchEvent(new CustomEvent("typo3:pagetree:refresh")),top.TYPO3?.Backend?.ContentContainer&&top.TYPO3.Backend.ContentContainer.refresh(),E.success(f.get("notification.record_updated.title"),n!==""?f.get("notification.record_updated.message",[n]):void 0)):this.focus()})}};c([l({type:String})],d.prototype,"url",void 0),c([l({type:String,attribute:"edit-url"})],d.prototype,"editUrl",void 0),d=c([m("typo3-backend-contextual-record-edit-trigger")],d);export{d as ContextualRecordEditTriggerElement}; diff --git a/Resources/Public/JavaScript/element/datetime-element.js b/Resources/Public/JavaScript/element/datetime-element.js new file mode 100644 index 0000000..b04a1a3 --- /dev/null +++ b/Resources/Public/JavaScript/element/datetime-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as h,html as p,nothing as f}from"lit";import{property as c,state as T,customElement as D}from"lit/decorators.js";import{DateTime as u}from"luxon";var m=function(n,t,e,i){var o=arguments.length,a=o<3?t:i===null?i=Object.getOwnPropertyDescriptor(t,e):i,r;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")a=Reflect.decorate(n,t,e,i);else for(var l=n.length-1;l>=0;l--)(r=n[l])&&(a=(o<3?r(a):o>3?r(t,e,a):r(t,e))||a);return o>3&&a&&Object.defineProperty(t,e,a),a},d;(function(n){n.Relative="relative",n.Absolute="absolute"})(d||(d={}));let s=class extends h{constructor(){super(...arguments),this.datetime="",this.mode=d.Absolute,this.format="date",this.thresholdDays=0,this.relativeShowSeconds=!1,this.displayTime="",this.updateTimeoutId=null}disconnectedCallback(){super.disconnectedCallback(),this.clearUpdateTimeout()}willUpdate(t){(t.has("datetime")||t.has("mode")||t.has("format")||t.has("relativeShowSeconds"))&&(this.updateDisplayTime(),this.clearUpdateTimeout(),this.startUpdateTimeout())}createRenderRoot(){return this}render(){const t=this.parseDateTime(this.datetime),e=t?.toISO()||this.datetime,i=t&&this.mode===d.Relative?this.formatAbsoluteDateTime(this.getLocalizedDateTime(t)):"",o=i?`${this.displayTime||this.datetime}. ${i}`:null;return p``}clearUpdateTimeout(){this.updateTimeoutId!==null&&(window.clearTimeout(this.updateTimeoutId),this.updateTimeoutId=null)}startUpdateTimeout(){if(this.mode===d.Relative){const t=this.parseDateTime(this.datetime),e=t?this.calculateNextStateChange(t):null,i=e?Math.abs(e.diffNow("milliseconds").milliseconds):1e3;this.updateTimeoutId=window.setTimeout(()=>{this.updateTimeoutId=null,this.updateDisplayTime(),this.startUpdateTimeout()},i)}}calculateNextStateChange(t){const e=["years","months","days","hours","minutes","seconds"],i=t.diffNow(e);for(const o of e){const a=i.get(o);if(a!==0){const r=a<0?-1:1;return t.minus({[o]:a+r})}}return t.plus({seconds:1})}updateDisplayTime(){const t=this.parseDateTime(this.datetime);t?this.displayTime=this.mode===d.Absolute?this.formatAbsoluteTime(t):this.formatRelativeTime(t):this.displayTime=this.datetime}formatAbsoluteTime(t){const e=this.getLocalizedDateTime(t),i=this.getConfiguredDateFormats();return this.format==="date"&&i?e.toFormat(i.formats.date):this.format==="datetime"&&i?e.toFormat(i.formats.datetime):this.format?e.toFormat(this.format):i?e.toFormat(i.formats.date):e.toLocaleString(u.DATE_SHORT)}parseDateTime(t){if(!t)return null;const i=this.getConfiguredDateFormats()?.timezone,o=Number(t);if(!isNaN(o)&&o>0){const r=i?{zone:i}:{},l=u.fromSeconds(o,r);if(l.isValid)return l}const a=u.fromISO(t);return a.isValid?a:null}getLocalizedDateTime(t){let e=document.documentElement.lang||"en";e==="ch"&&(e="zh");const o=this.getConfiguredDateFormats()?.timezone;let a=t.setLocale(e);return o&&(a=a.setZone(o)),a}formatRelativeTime(t){const e=this.getLocalizedDateTime(t);if(this.thresholdDays>0&&Math.abs(u.now().diff(e,"days").days)>this.thresholdDays)return this.formatAbsoluteDate(e);if(!this.relativeShowSeconds){const o=e.diffNow("seconds").seconds;if(Math.abs(o)<60)return e.toRelative({unit:"minutes",rounding:"expand"})||this.formatAbsoluteDateTime(e)}return e.toRelative()||this.formatAbsoluteDateTime(e)}formatAbsoluteDate(t){const e=this.getLocalizedDateTime(t),i=this.getConfiguredDateFormats();return i?e.toFormat(i.formats.date):e.toLocaleString(u.DATE_SHORT)}formatAbsoluteDateTime(t){const e=this.getLocalizedDateTime(t),i=this.getConfiguredDateFormats();return i?e.toFormat(i.formats.datetime):e.toLocaleString(u.DATETIME_SHORT)}getConfiguredDateFormats(){try{return(typeof opener?.top?.TYPO3<"u"?opener.top:top).TYPO3.settings.DateConfiguration}catch{return null}}};m([c({type:String})],s.prototype,"datetime",void 0),m([c()],s.prototype,"mode",void 0),m([c()],s.prototype,"format",void 0),m([c({type:Number,attribute:"threshold-days"})],s.prototype,"thresholdDays",void 0),m([c({type:Boolean,attribute:"relative-show-seconds"})],s.prototype,"relativeShowSeconds",void 0),m([T()],s.prototype,"displayTime",void 0),s=m([D("typo3-backend-datetime")],s);export{d as DateTimeDisplayMode,s as DateTimeElement}; diff --git a/Resources/Public/JavaScript/element/dispatch-modal-button.js b/Resources/Public/JavaScript/element/dispatch-modal-button.js new file mode 100644 index 0000000..ea78920 --- /dev/null +++ b/Resources/Public/JavaScript/element/dispatch-modal-button.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as s,customElement as m}from"lit/decorators.js";import{PseudoButtonLitElement as a}from"@typo3/backend/element/pseudo-button.js";import u from"@typo3/backend/modal.js";import{SeverityEnum as d}from"@typo3/backend/enum/severity.js";var f=function(i,e,o,n){var p=arguments.length,t=p<3?e:n===null?n=Object.getOwnPropertyDescriptor(e,o):n,l;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(i,e,o,n);else for(var c=i.length-1;c>=0;c--)(l=i[c])&&(t=(p<3?l(t):p>3?l(e,o,t):l(e,o))||t);return p>3&&t&&Object.defineProperty(e,o,t),t};let r=class extends a{buttonActivated(){this.url&&u.advanced({content:this.url,title:this.subject,severity:d.notice,size:u.sizes.large,type:u.types.iframe})}};f([s({type:String})],r.prototype,"url",void 0),f([s({type:String})],r.prototype,"subject",void 0),r=f([m("typo3-backend-dispatch-modal-button")],r);export{r as DispatchModalButton}; diff --git a/Resources/Public/JavaScript/element/draggable-resizable-element.js b/Resources/Public/JavaScript/element/draggable-resizable-element.js new file mode 100644 index 0000000..e772983 --- /dev/null +++ b/Resources/Public/JavaScript/element/draggable-resizable-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as u,html as f}from"lit";import{property as c,state as p,customElement as z}from"lit/decorators.js";import{Offset as v}from"@typo3/backend/offset.js";var l=function(n,e,t,r){var o=arguments.length,i=o<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,t):r,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")i=Reflect.decorate(n,e,t,r);else for(var d=n.length-1;d>=0;d--)(a=n[d])&&(i=(o<3?a(i):o>3?a(e,t,i):a(e,t))||i);return o>3&&i&&Object.defineProperty(e,t,i),i},s;(function(n){n.move="move",n.resizeN="resizeN",n.resizeE="resizeE",n.resizeS="resizeS",n.resizeW="resizeW",n.resizeSE="resizeSE",n.resizeSW="resizeSW",n.resizeNE="resizeNE",n.resizeNW="resizeNW"})(s||(s={}));const g=[s.resizeNW,s.resizeN,s.resizeNE],m=[s.resizeNE,s.resizeE,s.resizeSE],b=[s.resizeSE,s.resizeS,s.resizeSW],E=[s.resizeSW,s.resizeW,s.resizeNW];let h=class extends u{constructor(){super(...arguments),this.reverting=!1,this.action=null,this.originOffset=null,this.originPosition=null,this.handleStart=e=>{const t=e.target;if(!(e.buttons!==1||!this.contains(t))){if(t.dataset.resize){const r="resize"+t.dataset.resize.toUpperCase();this.action=s[r]}else this.action=s.move;this.reverting=!1,this.originOffset=this.offset.clone(),this.originPosition={x:e.clientX,y:e.clientY},this.dispatchEvent(this.createEvent("draggable-resizable-started",{action:this.action,originOffset:this.originOffset}))}},this.handleUpdate=e=>{if(!this.action)return;const t={x:e.clientX-this.originPosition.x,y:e.clientY-this.originPosition.y};this.offset=this.adjustOffset(this.originOffset,t),this.dispatchEvent(this.createEvent("draggable-resizable-updated",{action:this.action,originOffset:this.originOffset}))},this.handleFinish=()=>{this.action&&(this.dispatchEvent(this.createEvent("draggable-resizable-finished",{action:this.action,originOffset:this.originOffset})),this.action=null,this.originOffset=null,this.originPosition=null)}}revert(e){this.reverting=!0,this.offset=e,setTimeout(()=>this.reverting=!1,500)}connectedCallback(){super.connectedCallback(),this.pointerEventNames.pointerDown.forEach(e=>document.addEventListener(e,this.handleStart,!0)),this.pointerEventNames.pointerMove.forEach(e=>document.addEventListener(e,this.handleUpdate,!0)),this.pointerEventNames.pointerUp.forEach(e=>document.addEventListener(e,this.handleFinish,!0))}disconnectedCallback(){super.disconnectedCallback(),this.pointerEventNames.pointerDown.forEach(e=>document.removeEventListener(e,this.handleStart,!0)),this.pointerEventNames.pointerMove.forEach(e=>document.removeEventListener(e,this.handleUpdate,!0)),this.pointerEventNames.pointerUp.forEach(e=>document.removeEventListener(e,this.handleFinish,!0))}render(){return f`
    `}update(e){super.update(e),Object.assign(this.style,this.getOffsetStyles(this.offset))}createRenderRoot(){return this}adjustOffset(e,t){const o=this.parentElement.getBoundingClientRect(),i=e.clone();if(this.action===s.move&&(i.left=this.minMax(i.left+t.x,0,o.width-i.width),i.top=this.minMax(i.top+t.y,0,o.height-i.height)),g.includes(this.action)){const a=this.minMax(t.y,-i.top,i.height-2);i.top+=a,i.height-=a}else b.includes(this.action)&&(i.height=this.minMax(i.height+t.y,2,o.height-i.top));if(E.includes(this.action)){const a=this.minMax(t.x,-i.left,i.width-2);i.left+=a,i.width-=a}else m.includes(this.action)&&(i.width+=t.x);return i}minMax(e,t,r){return er?r:e}createEvent(e,t){return new CustomEvent(e,{detail:t,bubbles:!0,composed:!0})}getOffsetStyles(e){return{left:`${e.left}px`,top:`${e.top}px`,width:`${e.width}px`,height:`${e.height}px`}}};l([c({type:Object,converter:n=>v.fromObject(JSON.parse(n)),reflect:!0})],h.prototype,"offset",void 0),l([c({type:Object})],h.prototype,"pointerEventNames",void 0),l([c({type:Boolean,reflect:!0})],h.prototype,"reverting",void 0),l([p()],h.prototype,"action",void 0),h=l([z("typo3-backend-draggable-resizable")],h);export{h as DraggableResizableElement}; diff --git a/Resources/Public/JavaScript/element/editable-page-title.js b/Resources/Public/JavaScript/element/editable-page-title.js new file mode 100644 index 0000000..8a23d46 --- /dev/null +++ b/Resources/Public/JavaScript/element/editable-page-title.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as f,css as m,nothing as c,html as d}from"lit";import{property as p,state as u,customElement as v}from"lit/decorators.js";import"@typo3/backend/element/icon-element.js";import y from"@typo3/backend/ajax-data-handler.js";import h from"~labels/core.common";import g from"~labels/backend.layout";var r=function(l,t,n,s){var o=arguments.length,e=o<3?t:s===null?s=Object.getOwnPropertyDescriptor(t,n):s,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(l,t,n,s);else for(var b=l.length-1;b>=0;b--)(a=l[b])&&(e=(o<3?a(e):o>3?a(t,n,e):a(t,n))||e);return o>3&&e&&Object.defineProperty(t,n,e),e};let i=class extends f{constructor(){super(...arguments),this.pageTitle="",this.pageId=0,this.localizedPageId=0,this.editable=!1,this._isEditing=!1,this._isSubmitting=!1,this.labels={input:g.get("editPageTitle.input.field.label"),edit:g.get("editPageTitle"),save:h.get("save"),cancel:h.get("cancel")}}static{this.styles=m`:host{display:block;--input-border-color:#bebebe;--input-hover-border-color:#bebebe;--input-focus-border-color:#bebebe;--button-border-radius:var(--typo3-input-border-radius);--button-color:inherit;--button-bg:transparent;--button-border-color:transparent;--button-hover-color:inherit;--button-hover-bg:#cacaca;--button-hover-border-color:#bebebe;--button-focus-color:inherit;--button-focus-bg:#cacaca;--button-focus-border-color:#bebebe;--button-padding-x:var(--typo3-input-padding-x);--button-padding-y:var(--typo3-input-padding-y)}h1{display:block;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;padding:1px 0}h1,input{font-weight:inherit;font-size:inherit;font-family:inherit;line-height:inherit;margin:0}input{outline:none;background:transparent;padding:0;border:0;border-top:1px solid transparent;border-bottom:1px dashed var(--input-border-color);width:100%;outline-offset:0}input:hover{--input-border-color:var(--input-hover-border-color)}input:focus{--input-border-color:var(--input-focus-border-color)}input:focus-visible{outline:.25rem solid color-mix(in srgb,var(--input-border-color),transparent 25%)}.wrapper{position:relative;margin:-1px 0;padding-inline-end:1.5em}.wrapper:has(>form) .page-title{visibility:hidden;pointer-events:none}.wrapper>form{left:0;right:0;padding-inline-end:2.5em}.wrapper>form,button{position:absolute;top:0}button{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:inherit;line-height:inherit;padding:var(--button-padding-y) var(--button-padding-x);height:100%;width:1em;border-radius:var(--button-border-radius);overflow:hidden;outline:none;color:var(--button-color);background:var(--button-bg);border:1px solid var(--button-border-color);opacity:.8;outline-offset:0;transition:all .2s ease-in-out}button:hover{opacity:1;--button-color:var(--button-hover-color);--button-bg:var(--button-hover-bg);--button-border-color:var(--button-hover-border-color)}button:focus{opacity:1;--button-color:var(--button-focus-color);--button-bg:var(--button-focus-bg);--button-border-color:var(--button-focus-border-color)}button:focus-visible{outline:.25rem solid color-mix(in srgb,var(--button-border-color),transparent 25%)}button[data-action=edit]{inset-inline-end:0}button[data-action=save]{inset-inline-end:calc(1em + 2px)}button[data-action=close]{inset-inline-end:0}.screen-reader{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}`}async startEditing(){if(this.isEditable()){this._isEditing=!0,await this.updateComplete;const t=this.shadowRoot.querySelector("input");t?.focus(),t?.setSelectionRange(t.value.length,t.value.length)}}render(){return this.pageTitle===""?c:this.isEditable()?d`

    {this.startEditing()}}>${this.pageTitle}

    ${this.composeEditButton()}
    ${this._isEditing?this.composeEditForm():c}
    `:d`

    ${this.pageTitle}

    `}isEditable(){return this.editable&&this.pageId>0}async endEditing(){this.isEditable()&&(this._isEditing=!1,await new Promise(t=>requestAnimationFrame(()=>requestAnimationFrame(()=>t()))),this.shadowRoot.querySelector('button[data-action="edit"]')?.focus())}updatePageTitle(t){t.preventDefault();const n=new FormData(t.target),o=Object.fromEntries(n).newPageTitle.toString();if(this.pageTitle===o){this.endEditing();return}this._isSubmitting=!0;let e=this.pageId;this.localizedPageId>0&&(e=this.localizedPageId);const a={data:{pages:{[e]:{title:o}}}};y.process(a).then(()=>{this.pageTitle=o,top.document.dispatchEvent(new CustomEvent("typo3:pagetree:refresh"))}).finally(()=>{this.endEditing(),this._isSubmitting=!1})}composeEditButton(){return d``}composeEditForm(){return d`
    {t.key==="Escape"&&this.endEditing()}}>
    `}};r([p({type:String})],i.prototype,"pageTitle",void 0),r([p({type:Number})],i.prototype,"pageId",void 0),r([p({type:Number})],i.prototype,"localizedPageId",void 0),r([p({type:Boolean})],i.prototype,"editable",void 0),r([u()],i.prototype,"_isEditing",void 0),r([u()],i.prototype,"_isSubmitting",void 0),i=r([v("typo3-backend-editable-page-title")],i);export{i as EditablePageTitle}; diff --git a/Resources/Public/JavaScript/element/icon-element.js b/Resources/Public/JavaScript/element/icon-element.js new file mode 100644 index 0000000..77d2683 --- /dev/null +++ b/Resources/Public/JavaScript/element/icon-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as d,html as c,nothing as m}from"lit";import{Task as u}from"@lit/task";import{property as a,customElement as y}from"lit/decorators.js";import{unsafeHTML as h}from"lit/directives/unsafe-html.js";import{Sizes as v,States as g,MarkupIdentifiers as z}from"@typo3/backend/enum/icon-types.js";import w,{IconStyles as S}from"@typo3/backend/icons.js";import"@typo3/backend/element/spinner-element.js";var n=function(l,t,r,o){var s=arguments.length,e=s<3?t:o===null?o=Object.getOwnPropertyDescriptor(t,r):o,p;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(l,t,r,o);else for(var f=l.length-1;f>=0;f--)(p=l[f])&&(e=(s<3?p(e):s>3?p(t,r,e):p(t,r))||e);return s>3&&e&&Object.defineProperty(t,r,e),e};let i=class extends d{constructor(){super(...arguments),this.size=v.default,this.state=g.default,this.overlay=null,this.markup=z.inline,this.raw=null,this.iconTask=new u(this,{task:async([t,r,o,s,e],{signal:p})=>await w.getIcon(t,r,o,s,e,p),args:()=>[this.identifier,this.size,this.overlay,this.state,this.markup]})}static{this.styles=S.getStyles()}render(){return this.raw?c`${h(this.raw)}`:this.identifier?this.iconTask.render({pending:()=>c``,complete:t=>c`${h(t)}`,error:()=>c``}):m}};n([a({type:String,reflect:!0})],i.prototype,"identifier",void 0),n([a({type:String,reflect:!0})],i.prototype,"size",void 0),n([a({type:String})],i.prototype,"state",void 0),n([a({type:String})],i.prototype,"overlay",void 0),n([a({type:String})],i.prototype,"markup",void 0),n([a({type:String})],i.prototype,"raw",void 0),i=n([y("typo3-backend-icon")],i);export{i as IconElement}; diff --git a/Resources/Public/JavaScript/element/immediate-action-element.js b/Resources/Public/JavaScript/element/immediate-action-element.js new file mode 100644 index 0000000..7d7436a --- /dev/null +++ b/Resources/Public/JavaScript/element/immediate-action-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import n from"@typo3/backend/utility.js";import{EventDispatcher as i}from"@typo3/backend/event/event-dispatcher.js";class r extends HTMLElement{constructor(){super(...arguments),this.args=[]}static get observedAttributes(){return["action","args","args-list"]}static async getDelegate(t){switch(t){case"TYPO3.ModuleMenu.App.refreshMenu":const{default:s}=await import("@typo3/backend/module-menu.js");return s.App.refreshMenu.bind(s.App);case"TYPO3.Backend.Topbar.refresh":const{default:e}=await import("@typo3/backend/viewport.js");return e.Topbar.refresh.bind(e.Topbar);case"TYPO3.WindowManager.localOpen":const{default:a}=await import("@typo3/backend/window-manager.js");return a.localOpen.bind(a);case"TYPO3.Backend.Storage.ModuleStateStorage.update":return(await import("@typo3/backend/storage/module-state-storage.js")).ModuleStateStorage.update;case"TYPO3.Backend.Storage.ModuleStateStorage.updateWithCurrentMount":return(await import("@typo3/backend/storage/module-state-storage.js")).ModuleStateStorage.updateWithCurrentMount;case"TYPO3.Backend.Event.EventDispatcher.dispatchCustomEvent":return i.dispatchCustomEvent;default:throw Error('Unknown action "'+t+'"')}}attributeChangedCallback(t,s,e){if(t==="action")this.action=e;else if(t==="args"){const a=e.replace(/"/g,'"'),o=JSON.parse(a);this.args=o instanceof Array?n.trimItems(o):[]}else if(t==="args-list"){const a=e.split(",");this.args=n.trimItems(a)}}connectedCallback(){if(!this.action)throw new Error("Missing mandatory action attribute");r.getDelegate(this.action).then(t=>t(...this.args))}}window.customElements.define("typo3-immediate-action",r);export{r as ImmediateActionElement}; diff --git a/Resources/Public/JavaScript/element/link-browser-download-element.js b/Resources/Public/JavaScript/element/link-browser-download-element.js new file mode 100644 index 0000000..5725b69 --- /dev/null +++ b/Resources/Public/JavaScript/element/link-browser-download-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as u,html as d,nothing as m}from"lit";import{property as f,state as s,customElement as p}from"lit/decorators.js";var r=function(o,e,n,a){var i=arguments.length,t=i<3?e:a===null?a=Object.getOwnPropertyDescriptor(e,n):a,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(o,e,n,a);else for(var h=o.length-1;h>=0;h--)(c=o[h])&&(t=(i<3?c(t):i>3?c(e,n,t):c(e,n))||t);return i>3&&t&&Object.defineProperty(e,n,t),t};let l=class extends u{constructor(){super(...arguments),this.value="",this.checked=!1,this.filename=""}connectedCallback(){super.connectedCallback(),this.checked=this.value!=="",this.filename=this.value!==""&&this.value!=="true"?this.value:""}createRenderRoot(){return this}render(){return d`
    ${this.checked?d`
    `:m}`}computeValue(){return this.checked?this.filename!==""?this.filename:"true":""}handleCheckboxChange(e){this.checked=e.target.checked}handleFilenameInput(e){this.filename=e.target.value}};r([f({type:String})],l.prototype,"value",void 0),r([s()],l.prototype,"checked",void 0),r([s()],l.prototype,"filename",void 0),l=r([p("typo3-backend-link-browser-download")],l);export{l as LinkBrowserDownloadElement}; diff --git a/Resources/Public/JavaScript/element/pagination.js b/Resources/Public/JavaScript/element/pagination.js new file mode 100644 index 0000000..084c11b --- /dev/null +++ b/Resources/Public/JavaScript/element/pagination.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as u,customElement as m}from"lit/decorators.js";import{LitElement as d,html as g}from"lit";import{range as b}from"lit/directives/range.js";import{map as f}from"lit/directives/map.js";import{classMap as l}from"lit/directives/class-map.js";var c=function(i,t,n,a){var o=arguments.length,e=o<3?t:a===null?a=Object.getOwnPropertyDescriptor(t,n):a,r;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(i,t,n,a);else for(var s=i.length-1;s>=0;s--)(r=i[s])&&(e=(o<3?r(e):o>3?r(t,n,e):r(t,n))||e);return o>3&&e&&Object.defineProperty(t,n,e),e};let p=class extends d{constructor(){super(...arguments),this.paging=null}createRenderRoot(){return this}render(){return g`
    • ${f(b(1,this.paging.totalPages+1),t=>g`
    • `)}
    `}};c([u({type:Object})],p.prototype,"paging",void 0),p=c([m("typo3-backend-pagination")],p);export{p as PaginationElement}; diff --git a/Resources/Public/JavaScript/element/progress-bar-element.js b/Resources/Public/JavaScript/element/progress-bar-element.js new file mode 100644 index 0000000..f77aed8 --- /dev/null +++ b/Resources/Public/JavaScript/element/progress-bar-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as h,css as b,nothing as l,html as u}from"lit";import{property as n,customElement as g}from"lit/decorators.js";import{classMap as m}from"lit/directives/class-map.js";import{styleMap as v}from"lit/directives/style-map.js";import f from"@typo3/backend/severity.js";import"@typo3/backend/enum/severity.js";var o=function(d,r,e,t){var s=arguments.length,a=s<3?r:t===null?t=Object.getOwnPropertyDescriptor(r,e):t,p;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")a=Reflect.decorate(d,r,e,t);else for(var c=d.length-1;c>=0;c--)(p=d[c])&&(a=(s<3?p(a):s>3?p(r,e,a):p(r,e))||a);return s>3&&a&&Object.defineProperty(r,e,a),a};let i=class extends h{constructor(){super(...arguments),this.value=void 0,this.max=100,this.severity=void 0,this.isHidden=!1,this.isHiding=!1,this.abortController=new AbortController}static{this.styles=b`@keyframes progress-indeterminate{0%{inset-inline-start:-33%}to{inset-inline-start:100%}}@keyframes fade-out{0%{opacity:1}to{opacity:0}}:host{--progress-bar-height:4px;--progress-bar-color-primary:var(--typo3-state-primary-border-color);--progress-bar-color-success:var(--typo3-state-success-border-color);--progress-bar-color-warning:var(--typo3-state-warning-border-color);--progress-bar-color-danger:var(--typo3-state-danger-border-color);--progress-bar-color-info:var(--typo3-state-info-border-color);--progress-bar-color:var(--progress-bar-color-primary);--progress-track-color:light-dark(var(--token-color-neutral-20),var(--token-color-neutral-80));--progress-border-radius:var(--typo3-component-border-radius);display:block;width:100%;border-radius:var(--progress-border-radius)}:host([hidden]){display:none}:host([is-hiding]){animation:fade-out .3s ease-out forwards}.progress{position:relative;overflow:hidden;height:var(--progress-bar-height);border-radius:var(--progress-border-radius)}.value{display:block;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.track{background:var(--progress-track-color);inset:0}.bar{background:var(--progress-bar-color);transition:width .5s ease-in-out;&.bar-success{--progress-bar-color:var(--progress-bar-color-success)}&.bar-warning{--progress-bar-color:var(--progress-bar-color-warning)}&.bar-danger{--progress-bar-color:var(--progress-bar-color-danger)}&.bar-info{--progress-bar-color:var(--progress-bar-color-info)}&.indeterminate{animation-name:progress-indeterminate;animation-duration:3s;animation-iteration-count:infinite;animation-timing-function:linear;width:33%;background-image:linear-gradient(to right,var(--progress-track-color) 0,transparent 50%,var(--progress-track-color) 100%)}}.bar,.track{position:absolute;height:var(--progress-bar-height)}.label{margin-top:.5rem}`}isRunning(){return!this.abortController.signal.aborted}start(r=!1){this.abortController.abort(),this.abortController=new AbortController,this.isHiding=!1,this.isHidden=!1,this.value=r?void 0:0}inc(r=5){let e=this.value??0;e=this.clamp(e+r,0,this.max),this.value=e>=this.max?this.max:e}async done(){const r=this.abortController.signal;if((this.value??0)>=this.max){this.hide();return}if(isNaN(this.value)&&(this.value=0),await this.updateComplete,r.aborted||(await new Promise(s=>requestAnimationFrame(()=>requestAnimationFrame(s))),r.aborted)||(this.value=this.max,await this.updateComplete,r.aborted))return;this.shadowRoot.querySelector(".bar").addEventListener("transitionend",()=>{this.hide()},{once:!0,signal:r})}hide(){this.isHiding=!0,this.addEventListener("animationend",()=>{this.remove()},{once:!0,signal:this.abortController.signal})}connectedCallback(){super.connectedCallback(),this.updateHostClass()}disconnectedCallback(){super.disconnectedCallback()}render(){const r="progress-label-"+(Math.random()+1).toString(36).substring(2),e=this.label!==void 0&&this.label,t=isNaN(this.value),s=m({bar:!0,["bar-"+f.getCssClass(this.severity)]:!t&&this.severity!==void 0,indeterminate:t}),a=t?l:v({width:(this.clamp(this.value,0,this.max)/this.max*100).toString()+"%"});return u`
    ${t?l:u`${this.value}%`}
    ${e?u`
    ${this.label}
    `:l}
    `}updated(r){super.updated(r),r.has("isHiding")&&this.updateHostClass()}updateHostClass(){this.isHiding?this.setAttribute("is-hiding",""):this.removeAttribute("is-hiding")}clamp(r,e,t){return Math.min(t,Math.max(e,r))}};o([n({type:Number,reflect:!0})],i.prototype,"value",void 0),o([n({type:Number,reflect:!0})],i.prototype,"max",void 0),o([n({type:Number,reflect:!0})],i.prototype,"severity",void 0),o([n({type:String,reflect:!0})],i.prototype,"label",void 0),o([n({type:Boolean,reflect:!0,attribute:"hidden"})],i.prototype,"isHidden",void 0),o([n({type:Boolean,state:!0})],i.prototype,"isHiding",void 0),i=o([g("typo3-backend-progress-bar")],i);export{i as ProgressBarElement}; diff --git a/Resources/Public/JavaScript/element/progress-tracker-element.js b/Resources/Public/JavaScript/element/progress-tracker-element.js new file mode 100644 index 0000000..557b3fe --- /dev/null +++ b/Resources/Public/JavaScript/element/progress-tracker-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as l,css as d,nothing as h,html as b}from"lit";import{property as n,customElement as k}from"lit/decorators.js";import{styleMap as v}from"lit/directives/style-map.js";import m from"~labels/backend.layout";var p=function(s,r,t,i){var o=arguments.length,e=o<3?r:i===null?i=Object.getOwnPropertyDescriptor(r,t):i,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(s,r,t,i);else for(var g=s.length-1;g>=0;g--)(c=s[g])&&(e=(o<3?c(e):o>3?c(r,t,e):c(r,t))||e);return o>3&&e&&Object.defineProperty(r,t,e),e};let a=class extends l{constructor(){super(...arguments),this.stages=[],this.activeStage=0}static{this.styles=d`:host{--progress-tracker-margin:var(--typo3-spacing);--progress-tracker-stage:0;--progress-tracker-stages:0;--progress-tracker-gap:.25rem;--progress-tracker-bar-height:6px;--progress-tracker-bar-border-radius:3px;--progress-tracker-bar-bg:light-dark(var(--token-color-neutral-20),var(--token-color-neutral-80));--progress-tracker-bar-progress-bg:var(--typo3-state-primary-border-color);display:block;width:100%;margin-bottom:var(--progress-tracker-margin)}.tracker{position:relative;display:grid;gap:var(--progress-tracker-gap);grid-template-columns:auto min-content;grid-template-areas:"stage step" "bar bar"}.tracker-stage{grid-area:stage}.tracker-step{grid-area:step;white-space:nowrap}.tracker-bar{grid-area:bar;position:relative;width:100%;height:var(--progress-tracker-bar-height);border-radius:var(--progress-tracker-bar-border-radius);background-color:var(--progress-tracker-bar-bg)}.tracker-bar:after{content:"";height:100%;width:calc(100%/var(--progress-tracker-stages)*var(--progress-tracker-stage));display:block;border-radius:inherit;background-color:var(--progress-tracker-bar-progress-bg)}@media (prefers-reduced-motion:no-preference){.tracker-bar:after{transition:width .5s ease-in-out}}`}render(){if(this.stages.length<2)return h;this.activeStage=Math.min(Math.max(this.activeStage,this.stages.length>0?1:0),this.stages.length);const r=this.stages[this.activeStage-1],t=v({"--progress-tracker-stage":`${this.activeStage}`,"--progress-tracker-stages":`${this.stages.length}`});return b`
    ${r}
    ${m.get("progressTracker.steps",[this.activeStage,this.stages.length])||`Step ${this.activeStage} of ${this.stages.length}`}
    `}};p([n({attribute:"stages",type:Array})],a.prototype,"stages",void 0),p([n({attribute:"active",type:Number,reflect:!0})],a.prototype,"activeStage",void 0),a=p([k("typo3-backend-progress-tracker")],a);export{a as ProgressTrackerElement}; diff --git a/Resources/Public/JavaScript/element/pseudo-button.js b/Resources/Public/JavaScript/element/pseudo-button.js new file mode 100644 index 0000000..bc6ea60 --- /dev/null +++ b/Resources/Public/JavaScript/element/pseudo-button.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as f}from"lit/decorators.js";import{LitElement as a,css as d,html as v}from"lit";import{KeyTypesEnum as l}from"@typo3/backend/enum/key-types.js";var c=function(n,t,r,o){var i=arguments.length,e=i<3?t:o===null?o=Object.getOwnPropertyDescriptor(t,r):o,p;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(n,t,r,o);else for(var s=n.length-1;s>=0;s--)(p=n[s])&&(e=(i<3?p(e):i>3?p(t,r,e):p(t,r))||e);return i>3&&e&&Object.defineProperty(t,r,e),e};class u extends a{static{this.styles=[d`:host{cursor:pointer;appearance:button}`]}constructor(){super(),this.role="button",this.tabIndex=0,this.addEventListener("click",t=>{t.preventDefault(),this.buttonActivated(t)}),this.addEventListener("keydown",t=>{t.key===l.SPACE&&t.preventDefault(),t.key===l.ENTER&&(t.preventDefault(),this.buttonActivated(t))}),this.addEventListener("keyup",t=>{t.key===l.SPACE&&(t.preventDefault(),this.buttonActivated(t))})}render(){return v``}}c([f({type:String,reflect:!0})],u.prototype,"role",void 0),c([f({type:String,reflect:!0})],u.prototype,"tabIndex",void 0);export{u as PseudoButtonLitElement}; diff --git a/Resources/Public/JavaScript/element/qrcode-element.js b/Resources/Public/JavaScript/element/qrcode-element.js new file mode 100644 index 0000000..570aa21 --- /dev/null +++ b/Resources/Public/JavaScript/element/qrcode-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as d,state as h,customElement as b}from"lit/decorators.js";import{LitElement as y,html as s,nothing as f}from"lit";import v from"@typo3/core/ajax/ajax-request.js";import"@typo3/backend/element/spinner-element.js";import"@typo3/backend/element/icon-element.js";import"@typo3/backend/copy-to-clipboard.js";import{unsafeHTML as g}from"lit/directives/unsafe-html.js";import i from"~labels/backend.qrcode";var a=function(t,e,o,l){var c=arguments.length,r=c<3?e:l===null?l=Object.getOwnPropertyDescriptor(e,o):l,p;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")r=Reflect.decorate(t,e,o,l);else for(var m=t.length-1;m>=0;m--)(p=t[m])&&(r=(c<3?p(r):c>3?p(e,o,r):p(e,o))||r);return c>3&&r&&Object.defineProperty(e,o,r),r},u;(function(t){t[t.small=64]="small",t[t.medium=128]="medium",t[t.large=256]="large",t[t.mega=512]="mega"})(u||(u={}));let n=class extends y{constructor(){super(),this.content="",this.showDownload=!1,this.showUrl=!1,this.size=u.small,this.qrcodePreview=s``,document.addEventListener("copy-to-clipboard-success",this.showCopySuccess.bind(this)),document.addEventListener("copy-to-clipboard-error",this.showCopyError.bind(this))}connectedCallback(){super.connectedCallback(),this.loadQrCode()}render(){return s`
    ${this.qrcodePreview}
    ${this.getUrlSection()} ${this.getControls()}`}createRenderRoot(){return this}getUrlSection(){if(!this.showUrl)return f;const e=i.get("qrcode.url"),o=i.get("qrcode.copyUrl");return s`
    ${o}
    `}getControls(){if(!this.showDownload)return s`${f}`;const e=i.get("qrcode.format.png"),o=i.get("qrcode.format.svg"),l=i.get("qrcode.format"),c=i.get("qrcode.size"),r=i.get("qrcode.download");return s`
    `}getSizeOptions(){return Object.entries(u).filter(([,e])=>typeof e=="number").map(([e,o])=>s``)}async loadQrCode(){if(this.content===""){this.qrcodePreview=s``;return}await new v(TYPO3.settings.ajaxUrls.qrcode_generator).withQueryArguments({content:this.content,size:this.size}).get({cache:"no-cache"}).then(async e=>{this.qrcodePreview=s`${g(await e.resolve())}`})}showCopySuccess(){const e=this.querySelector(".url-info-section");e!==null&&(e.classList.add("copy-success"),setTimeout(()=>e.classList.remove("copy-success"),500))}showCopyError(){const e=this.querySelector(".url-info-section");e!==null&&(e.classList.add("copy-error"),setTimeout(()=>e.classList.remove("copy-error"),750))}};a([d({type:String,reflect:!0})],n.prototype,"content",void 0),a([d({type:Boolean,reflect:!0,attribute:"show-download"})],n.prototype,"showDownload",void 0),a([d({type:Boolean,reflect:!0,attribute:"show-url"})],n.prototype,"showUrl",void 0),a([d({type:String,reflect:!0})],n.prototype,"size",void 0),a([h()],n.prototype,"qrcodePreview",void 0),n=a([b("typo3-qrcode")],n);export{n as QrCodeElement}; diff --git a/Resources/Public/JavaScript/element/qrcode-modal-button.js b/Resources/Public/JavaScript/element/qrcode-modal-button.js new file mode 100644 index 0000000..bcd6c60 --- /dev/null +++ b/Resources/Public/JavaScript/element/qrcode-modal-button.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as d,customElement as m}from"lit/decorators.js";import{PseudoButtonLitElement as u}from"@typo3/backend/element/pseudo-button.js";import c from"@typo3/backend/modal.js";import{html as f}from"lit";import{topLevelModuleImport as w}from"@typo3/backend/utility/top-level-module-import.js";import h from"~labels/core.mod_web_list";var l=function(r,t,o,i){var a=arguments.length,e=a<3?t:i===null?i=Object.getOwnPropertyDescriptor(t,o):i,s;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(r,t,o,i);else for(var p=r.length-1;p>=0;p--)(s=r[p])&&(e=(a<3?s(e):a>3?s(t,o,e):s(t,o))||e);return a>3&&e&&Object.defineProperty(t,o,e),e};let n=class extends u{constructor(){super(...arguments),this.showUrl=!1,this.showDownload=!1}buttonActivated(){this.modalOpen()}async loadModuleFrameAgnostic(t){window.location!==window.parent.location?await w(t):await import(t)}async modalOpen(){await this.loadModuleFrameAgnostic("@typo3/backend/element/qrcode-element.js"),c.advanced({title:this.modalTitle||"QR Code",size:c.sizes.small,content:f`
    `,buttons:[{text:h.get("button.close"),name:"close",trigger:function(t,o){o.hideModal()}}]})}};l([d({type:String,attribute:"modal-title"})],n.prototype,"modalTitle",void 0),l([d({type:String})],n.prototype,"content",void 0),l([d({type:Boolean,attribute:"show-url"})],n.prototype,"showUrl",void 0),l([d({type:Boolean,attribute:"show-download"})],n.prototype,"showDownload",void 0),n=l([m("typo3-qrcode-modal-button")],n);export{n as QrCodeModalButton}; diff --git a/Resources/Public/JavaScript/element/sidebar-toggle-element.js b/Resources/Public/JavaScript/element/sidebar-toggle-element.js new file mode 100644 index 0000000..0e4f3a1 --- /dev/null +++ b/Resources/Public/JavaScript/element/sidebar-toggle-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{css as h,html as f}from"lit";import{property as b,state as m,customElement as g}from"lit/decorators.js";import{PseudoButtonLitElement as c}from"@typo3/backend/element/pseudo-button.js";import{ScaffoldState as r,ScaffoldSidebarToggleEvent as u}from"@typo3/backend/viewport/scaffold-state.js";import"@typo3/backend/element/icon-element.js";var l=function(d,e,a,s){var n=arguments.length,t=n<3?e:s===null?s=Object.getOwnPropertyDescriptor(e,a):s,o;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(d,e,a,s);else for(var p=d.length-1;p>=0;p--)(o=d[p])&&(t=(n<3?o(t):n>3?o(e,a,t):o(e,a))||t);return n>3&&t&&Object.defineProperty(e,a,t),t};let i=class extends c{constructor(){super(...arguments),this.labelCollapse="Collapse sidebar",this.labelExpand="Expand sidebar",this.disabled=!1,this.expanded=!1,this.handleSidebarToggle=e=>{this.expanded=e.detail.expanded}}static{this.styles=[...c.styles,h`:host{display:inline-flex;align-items:center;justify-content:center;user-select:none}:host([disabled]){pointer-events:none;opacity:.5;cursor:default}`]}connectedCallback(){super.connectedCallback(),this.expanded=r.isLargeScreen()?r.isSidebarExpanded():r.isSidebarVisible(),this.updateLabels(),document.addEventListener(u.eventName,this.handleSidebarToggle)}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(u.eventName,this.handleSidebarToggle)}updated(){this.updateLabels(),this.updateDisabledState()}render(){const e=this.expanded?"actions-menu-sidebar-collapsed":"actions-menu-sidebar-expanded",a=this.disabled?"overlay-readonly":null;return f``}buttonActivated(){this.disabled||r.toggleSidebar()}updateLabels(){const e=this.expanded?this.labelCollapse:this.labelExpand;this.title=e,this.setAttribute("aria-label",e)}updateDisabledState(){this.disabled?(this.setAttribute("aria-disabled","true"),this.tabIndex=-1):(this.removeAttribute("aria-disabled"),this.tabIndex=0)}};l([b({type:String,attribute:"label-collapse"})],i.prototype,"labelCollapse",void 0),l([b({type:String,attribute:"label-expand"})],i.prototype,"labelExpand",void 0),l([b({type:Boolean,reflect:!0})],i.prototype,"disabled",void 0),l([m()],i.prototype,"expanded",void 0),i=l([g("typo3-backend-sidebar-toggle")],i);export{i as SidebarToggleElement}; diff --git a/Resources/Public/JavaScript/element/spinner-element.js b/Resources/Public/JavaScript/element/spinner-element.js new file mode 100644 index 0000000..01ecf8c --- /dev/null +++ b/Resources/Public/JavaScript/element/spinner-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as a,html as m}from"lit";import{property as f,customElement as u}from"lit/decorators.js";import{Sizes as h}from"@typo3/backend/enum/icon-types.js";import{IconStyles as v}from"@typo3/backend/icons.js";var l=function(r,t,n,s){var o=arguments.length,e=o<3?t:s===null?s=Object.getOwnPropertyDescriptor(t,n):s,i;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(r,t,n,s);else for(var c=r.length-1;c>=0;c--)(i=r[c])&&(e=(o<3?i(e):o>3?i(t,n,e):i(t,n))||e);return o>3&&e&&Object.defineProperty(t,n,e),e};let p=class extends a{constructor(){super(...arguments),this.size=h.default}static{this.styles=v.getStyles()}render(){return m` `}};l([f({type:String})],p.prototype,"size",void 0),p=l([u("typo3-backend-spinner")],p);export{p as SpinnerElement}; diff --git a/Resources/Public/JavaScript/element/status-indicator-element.js b/Resources/Public/JavaScript/element/status-indicator-element.js new file mode 100644 index 0000000..dc033bf --- /dev/null +++ b/Resources/Public/JavaScript/element/status-indicator-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as p,customElement as d}from"lit/decorators.js";import{LitElement as h,html as f,nothing as c}from"lit";import{classMap as m}from"lit/directives/class-map.js";var l=function(n,t,o,a){var s=arguments.length,e=s<3?t:a===null?a=Object.getOwnPropertyDescriptor(t,o):a,r;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(n,t,o,a);else for(var u=n.length-1;u>=0;u--)(r=n[u])&&(e=(s<3?r(e):s>3?r(t,o,e):r(t,o))||e);return s>3&&e&&Object.defineProperty(t,o,e),e};const g={online:"live",running:"loading"};let i=class extends h{constructor(){super(...arguments),this.state="default",this.live=!1,this.loading=!1,this.label=null}createRenderRoot(){return this}render(){const t=this.label!==null&&this.label!=="";return f``}getClasses(){const t=g[this.state];return{"status-indicator":!0,["status-indicator-"+this.state]:!0,"status-indicator-live":this.live||t==="live","status-indicator-loading":this.loading||t==="loading"}}};l([p({type:String})],i.prototype,"state",void 0),l([p({type:Boolean})],i.prototype,"live",void 0),l([p({type:Boolean})],i.prototype,"loading",void 0),l([p({type:String})],i.prototype,"label",void 0),i=l([d("typo3-backend-status-indicator")],i);export{i as StatusIndicatorElement}; diff --git a/Resources/Public/JavaScript/element/thumbnail-element.js b/Resources/Public/JavaScript/element/thumbnail-element.js new file mode 100644 index 0000000..b875da1 --- /dev/null +++ b/Resources/Public/JavaScript/element/thumbnail-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as p,customElement as d}from"lit/decorators.js";import{LitElement as b,html as c}from"lit";import{Task as y}from"@lit/task";import"@typo3/backend/element/spinner-element.js";import"@typo3/backend/element/icon-element.js";var l=function(m,t,n,s){var a=arguments.length,e=a<3?t:s===null?s=Object.getOwnPropertyDescriptor(t,n):s,i;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(m,t,n,s);else for(var r=m.length-1;r>=0;r--)(i=m[r])&&(e=(a<3?i(e):a>3?i(t,n,e):i(t,n))||e);return a>3&&e&&Object.defineProperty(t,n,e),e};const u={default:"default",small:"small",medium:"medium",large:"large"};let o=class extends b{constructor(){super(...arguments),this.size=u.default,this.keepAspectRatio=!1,this.thumbnailTask=new y(this,{task:async([t,n,s,a,e])=>{const i=new URL(t,window.origin);i.searchParams.set("size",n),i.searchParams.set("keepAspectRatio",s?"1":"0"),i.searchParams.set("bust",Date.now().toString(10));const r=new Image;return r.src=i.toString(),a>0&&(r.width=a),e>0&&!s&&(r.height=e),await new Promise((h,f)=>{r.onload=()=>h(),r.onerror=()=>f()}),c`${r}`},args:()=>[this.url,this.size,this.keepAspectRatio,this.width,this.height]})}createRenderRoot(){return this}render(){return this.thumbnailTask.render({pending:()=>c``,complete:t=>c`${t}`,error:()=>c``})}};l([p({type:String,reflect:!0})],o.prototype,"url",void 0),l([p({type:String,reflect:!0})],o.prototype,"size",void 0),l([p({type:Boolean,reflect:!0})],o.prototype,"keepAspectRatio",void 0),l([p({type:Number,reflect:!0})],o.prototype,"width",void 0),l([p({type:Number,reflect:!0})],o.prototype,"height",void 0),o=l([d("typo3-backend-thumbnail")],o);export{o as ThumbnailElement,u as ThumbnailSize}; diff --git a/Resources/Public/JavaScript/enum/data-transfer-types.js b/Resources/Public/JavaScript/enum/data-transfer-types.js new file mode 100644 index 0000000..241c1a2 --- /dev/null +++ b/Resources/Public/JavaScript/enum/data-transfer-types.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +var t;(function(o){o.treenode="application/x-typo3-treenode",o.newTreenode="application/x-typo3-new-treenode+json",o.pages="application/x-typo3-record-pages+json",o.falResources="application/x-typo3-fal-resources+json",o.dragTooltip="application/x-typo3-drag-tooltip+json",o.content="application/x-typo3-content+json"})(t||(t={}));export{t as DataTransferTypes}; diff --git a/Resources/Public/JavaScript/enum/icon-types.js b/Resources/Public/JavaScript/enum/icon-types.js new file mode 100644 index 0000000..af29425 --- /dev/null +++ b/Resources/Public/JavaScript/enum/icon-types.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +var l;(function(a){a.default="default",a.small="small",a.medium="medium",a.large="large",a.mega="mega",a.overlay="overlay"})(l||(l={}));var e;(function(a){a.default="default",a.disabled="disabled"})(e||(e={}));var d;(function(a){a.default="default",a.inline="inline"})(d||(d={}));export{d as MarkupIdentifiers,l as Sizes,e as States}; diff --git a/Resources/Public/JavaScript/enum/key-types.js b/Resources/Public/JavaScript/enum/key-types.js new file mode 100644 index 0000000..8045a46 --- /dev/null +++ b/Resources/Public/JavaScript/enum/key-types.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +var o;(function(r){r.TAB="Tab",r.ENTER="Enter",r.ESCAPE="Escape",r.SPACE=" ",r.END="End",r.HOME="Home",r.LEFT="ArrowLeft",r.UP="ArrowUp",r.RIGHT="ArrowRight",r.DOWN="ArrowDown",r.PAGE_UP="PageUp",r.PAGE_DOWN="PageDown"})(o||(o={}));export{o as KeyTypesEnum}; diff --git a/Resources/Public/JavaScript/enum/severity.js b/Resources/Public/JavaScript/enum/severity.js new file mode 100644 index 0000000..62a2209 --- /dev/null +++ b/Resources/Public/JavaScript/enum/severity.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +var a;(function(o){o[o.notice=-2]="notice",o[o.info=-1]="info",o[o.ok=0]="ok",o[o.warning=1]="warning",o[o.error=2]="error"})(a||(a={}));export{a as SeverityEnum}; diff --git a/Resources/Public/JavaScript/enum/viewport/scaffold-identifier.js b/Resources/Public/JavaScript/enum/viewport/scaffold-identifier.js new file mode 100644 index 0000000..6824392 --- /dev/null +++ b/Resources/Public/JavaScript/enum/viewport/scaffold-identifier.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{ContentNavigationSlotEnum as n}from"@typo3/backend/viewport/content-navigation.js";var e;(function(t){t.scaffold=".t3js-scaffold",t.header=".t3js-scaffold-header",t.sidebar=".t3js-scaffold-sidebar",t.content=".t3js-scaffold-content",t.contentModuleRouter="typo3-backend-module-router",t.contentModuleIframe=".t3js-scaffold-content-module-iframe"})(e||(e={}));class a{static{this.selector='typo3-backend-content-navigation[identifier="backend"]'}static getContentNavigation(){return document.querySelector(this.selector)}static getNavigationContainer(){return this.getContentNavigation()?.querySelector(`[slot="${n.navigation}"]`)??null}static getContentContainer(){return this.getContentNavigation()?.querySelector(`[slot="${n.content}"]`)??null}}export{a as ScaffoldContentArea,e as ScaffoldIdentifierEnum}; diff --git a/Resources/Public/JavaScript/event/client-request.js b/Resources/Public/JavaScript/event/client-request.js new file mode 100644 index 0000000..5c5388d --- /dev/null +++ b/Resources/Public/JavaScript/event/client-request.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import s from"@typo3/backend/event/interaction-request.js";class n extends s{constructor(t,e=null){super(t),this.clientEvent=e}}export{n as default}; diff --git a/Resources/Public/JavaScript/event/consumer-scope.js b/Resources/Public/JavaScript/event/consumer-scope.js new file mode 100644 index 0000000..5ab42e3 --- /dev/null +++ b/Resources/Public/JavaScript/event/consumer-scope.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class t{constructor(){this.consumers=[]}getConsumers(){return this.consumers}hasConsumer(s){return this.consumers.includes(s)}attach(s){this.hasConsumer(s)||this.consumers.push(s)}detach(s){this.consumers=this.consumers.filter(e=>e!==s)}async invoke(s){const e=[];this.consumers.forEach(o=>{const r=o.consume.call(o,s);r&&e.push(r)}),await Promise.all(e)}}var c=new t;export{c as default}; diff --git a/Resources/Public/JavaScript/event/event-dispatcher.js b/Resources/Public/JavaScript/event/event-dispatcher.js new file mode 100644 index 0000000..8bb74c3 --- /dev/null +++ b/Resources/Public/JavaScript/event/event-dispatcher.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class c{static dispatchCustomEvent(e,n=null,s=!1){const t=new CustomEvent(e,{detail:n});s?typeof top<"u"&&top.document.dispatchEvent(t):document.dispatchEvent(t)}}export{c as EventDispatcher}; diff --git a/Resources/Public/JavaScript/event/form-engine-link-browser-set-link-event.js b/Resources/Public/JavaScript/event/form-engine-link-browser-set-link-event.js new file mode 100644 index 0000000..d81747a --- /dev/null +++ b/Resources/Public/JavaScript/event/form-engine-link-browser-set-link-event.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class e extends Event{static{this.eventName="typo3:form-engine:link-browser:set-link"}constructor(t,s){super(e.eventName,{bubbles:!0,composed:!0,cancelable:!1}),this.value=t,this.onFieldChangeItems=s}}export{e as FormEngineLinkBrowserSetLinkEvent}; diff --git a/Resources/Public/JavaScript/event/interaction-request-map.js b/Resources/Public/JavaScript/event/interaction-request-map.js new file mode 100644 index 0000000..a99469c --- /dev/null +++ b/Resources/Public/JavaScript/event/interaction-request-map.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class r{constructor(){this.assignments=[]}attachFor(t,s){let e=this.getFor(t);e===null&&(e={request:t,deferreds:[]},this.assignments.push(e)),e.deferreds.push(s)}detachFor(t){const s=this.getFor(t);this.assignments=this.assignments.filter(e=>e===s)}getFor(t){let s=null;return this.assignments.some(e=>e.request===t?(s=e,!0):!1),s}resolveFor(t){const s=this.getFor(t);return s===null?!1:(s.deferreds.forEach(e=>e.resolve()),this.detachFor(t),!0)}rejectFor(t){const s=this.getFor(t);return s===null?!1:(s.deferreds.forEach(e=>e.reject()),this.detachFor(t),!0)}}var n=new r;export{n as default}; diff --git a/Resources/Public/JavaScript/event/interaction-request.js b/Resources/Public/JavaScript/event/interaction-request.js new file mode 100644 index 0000000..18af0c1 --- /dev/null +++ b/Resources/Public/JavaScript/event/interaction-request.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class s{constructor(e,t=null){this.processed=!1,this.processedData=null,this.type=e,this.parentRequest=t}get outerMostRequest(){let e=this;for(;e.parentRequest instanceof s;)e=e.parentRequest;return e}isProcessed(){return this.processed}getProcessedData(){return this.processedData}setProcessedData(e=null){this.processed=!0,this.processedData=e}}export{s as default}; diff --git a/Resources/Public/JavaScript/event/trigger-request.js b/Resources/Public/JavaScript/event/trigger-request.js new file mode 100644 index 0000000..1b95af7 --- /dev/null +++ b/Resources/Public/JavaScript/event/trigger-request.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import r from"@typo3/backend/event/interaction-request.js";class n extends r{constructor(t,e=null){super(t,e)}concerns(t){if(this===t)return!0;for(let e=this.parentRequest;e instanceof r;e=e.parentRequest)if(e===t)return!0;return!1}concernsTypes(t){if(t.includes(this.type))return!0;for(let e=this.parentRequest;e instanceof r;e=e.parentRequest)if(t.includes(e.type))return!0;return!1}}export{n as default}; diff --git a/Resources/Public/JavaScript/form-engine-link-browser-adapter.js b/Resources/Public/JavaScript/form-engine-link-browser-adapter.js new file mode 100644 index 0000000..e4d903d --- /dev/null +++ b/Resources/Public/JavaScript/form-engine-link-browser-adapter.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import r from"@typo3/backend/link-browser.js";import o from"@typo3/core/ajax/ajax-request.js";import{FormEngineLinkBrowserSetLinkEvent as s}from"@typo3/backend/event/form-engine-link-browser-set-link-event.js";var a=function(){const e={onFieldChangeItems:null};return e.setOnFieldChangeItems=function(n){e.onFieldChangeItems=n},r.finalizeFunction=async n=>{const i=await new o(TYPO3.settings.ajaxUrls.link_browser_encodetypolink).withQueryArguments({...r.getLinkAttributeValues(),url:n}).get(),{typoLink:t}=await i.resolve();t&&window.frameElement.dispatchEvent(new s(t,e.onFieldChangeItems))},e}();export{a as default}; diff --git a/Resources/Public/JavaScript/form-engine-review.js b/Resources/Public/JavaScript/form-engine-review.js new file mode 100644 index 0000000..82c186c --- /dev/null +++ b/Resources/Public/JavaScript/form-engine-review.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import"bootstrap";import a from"@typo3/core/document-service.js";import{selector as d}from"@typo3/core/literals.js";import"@typo3/backend/element/icon-element.js";import r from"@typo3/backend/popover.js";import{Tab as c}from"@typo3/backend/tab.js";import m from"@typo3/backend/utility/dom-helper.js";import u from"~labels/backend.alt_doc";class h{constructor(e){this.formElement=e,this.toggleButtonClass="t3js-toggle-review-panel",this.labelSelector=".t3js-formengine-label",this.invalidFields=new Set,this.initialize()}initialize(){this.formElement.addEventListener("t3-formengine-postfieldvalidation",e=>{const i=e.detail.field;e.detail.isValid?this.invalidFields.delete(i):this.invalidFields.add(i),this.checkForReviewableField()}),a.ready().then(()=>{this.attachButtonToModuleHeader(),this.checkForReviewableField()})}attachButtonToModuleHeader(){const e=document.querySelector(".t3js-module-docheader-buttons")?.lastElementChild?.querySelector('[role="toolbar"]');if(!e)return;const i=document.createElement("typo3-backend-icon");i.setAttribute("identifier","actions-exclamation-circle"),i.setAttribute("size","small");const t=document.createElement("button");t.type="button",t.classList.add("btn","btn-danger","btn-sm","hidden",this.toggleButtonClass),t.title=u.get("buttons.reviewFailedValidationFields"),t.appendChild(i),r.popover(t),e.prepend(t)}checkForReviewableField(){const e=document.querySelector("."+this.toggleButtonClass);if(e!==null)if(this.invalidFields.size>0){const i=document.createElement("div");i.classList.add("list-group");for(const t of this.invalidFields){const o=t.closest(".t3js-formengine-validation-marker");if(o===null)throw console.error(t),new Error("Could not find an element containing the `t3js-formengine-validation-marker` class for the previously logged input field.");const l=o.querySelector("[data-formengine-validation-rules]");if(l===null)throw console.error(o),new Error("Could not find an element containing the `data-formengine-validation-rules` attribute for the previously logged container.");const n=document.createElement("a");n.classList.add("list-group-item"),n.href="#",n.textContent=o.querySelector(this.labelSelector)?.textContent||"",n.addEventListener("click",s=>{this.switchToField(s,o,l)}),i.append(n)}e.classList.remove("hidden"),r.setOptions(e,{html:!0,content:i})}else e.classList.add("hidden"),r.hide(e)}switchToField(e,i,t){e.preventDefault();let o=t;for(;o;){if(o.matches('[id][role="tabpanel"]')){const l=document.querySelector(d`[aria-controls="${o.id}"]`);c.show(l)}o=o.parentElement}t.checkVisibility()?t.focus():m.scrollIntoViewIfNeeded(i)}}export{h as FormEngineReview}; diff --git a/Resources/Public/JavaScript/form-engine-suggest.js b/Resources/Public/JavaScript/form-engine-suggest.js new file mode 100644 index 0000000..f87d7a5 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine-suggest.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import"@typo3/backend/form-engine/element/suggest/result-container.js";import o from"@typo3/core/document-service.js";import i from"@typo3/backend/form-engine.js";import n from"@typo3/core/event/regular-event.js";import u from"@typo3/core/event/debounce-event.js";import a from"@typo3/core/ajax/ajax-request.js";import{selector as d}from"@typo3/core/literals.js";class m{constructor(t){this.currentRequest=null,this.handleKeyDown=e=>{if(e.key==="ArrowDown"){e.preventDefault(),JSON.parse(this.resultContainer.getAttribute("results"))?.length>0&&(this.resultContainer.hidden=!1),this.resultContainer.querySelector("typo3-backend-formengine-suggest-result-item")?.focus();return}e.key==="Escape"&&(e.preventDefault(),this.resultContainer.hidden=!0)},this.element=t,o.ready().then(()=>{this.initialize(t),this.registerEvents()})}initialize(t){const e=t.closest(".t3-form-suggest-container");this.resultContainer=document.createElement("typo3-backend-formengine-suggest-result-container"),this.resultContainer.hidden=!0,e.append(this.resultContainer)}registerEvents(){new n("typo3:formengine:suggest-item-chosen",t=>{let e="";this.element.dataset.fieldtype==="select"?e=t.detail.element.uid:e=t.detail.element.table+"_"+t.detail.element.uid,i.setSelectOptionFromExternalSource(this.element.dataset.field,e,t.detail.element.label,t.detail.element.label),i.markFieldAsChanged(document.querySelector(d`input[name="${this.element.dataset.field}"]`)),this.resultContainer.hidden=!0}).bindTo(this.resultContainer),new n("focus",()=>{JSON.parse(this.resultContainer.getAttribute("results"))?.length>0&&(this.resultContainer.hidden=!1)}).bindTo(this.element),new n("blur",t=>{t.relatedTarget?.tagName.toLowerCase()!=="typo3-backend-formengine-suggest-result-item"&&(this.resultContainer.hidden=!0)}).bindTo(this.element),new u("input",t=>{this.currentRequest instanceof a&&this.currentRequest.abort();const e=t.target;if(e.value.length{const l=await s.raw().text();this.resultContainer.setAttribute("results",l),this.resultContainer.hidden=!1})}).bindTo(this.element),new n("keydown",this.handleKeyDown).bindTo(this.element)}}export{m as default}; diff --git a/Resources/Public/JavaScript/form-engine-validation.js b/Resources/Public/JavaScript/form-engine-validation.js new file mode 100644 index 0000000..5dd3bad --- /dev/null +++ b/Resources/Public/JavaScript/form-engine-validation.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{DateTime as p}from"luxon";import y from"@typo3/backend/hashing/md5.js";import g from"@typo3/backend/modal.js";import w from"@typo3/backend/severity.js";import b from"@typo3/backend/utility.js";import k from"@typo3/core/event/regular-event.js";import E from"@typo3/core/event/throttle-event.js";import x from"@typo3/backend/utility/dom-helper.js";import{selector as d}from"@typo3/core/literals.js";import N from"@typo3/backend/form/submit-interceptor.js";import{FormEngineReview as T}from"@typo3/backend/form-engine-review.js";import v from"~labels/core.core";import C from"~labels/core.mod_web_list";let o,S=!1;const h=new Map;class a{static{this.rulesSelector="[data-formengine-validation-rules]"}static{this.inputSelector="[data-formengine-input-params]"}static{this.markerSelector=".t3js-formengine-validation-marker"}static{this.labelSelector=".t3js-formengine-label"}static{this.errorClass="has-error"}static{this.validationErrorClass="has-validation-error"}static{this.passwordDummy="********"}static initialize(t){o=t,o.formElement.querySelectorAll("."+a.errorClass).forEach(e=>e.classList.remove(a.errorClass)),a.initializeInputFields(),new T(o.formElement),new k("change",(e,s)=>{a.validateField(s),o.markFieldAsChanged(s)}).delegateTo(o.formElement,a.rulesSelector),new E("input",(e,s)=>{a.validateField(s)},100).delegateTo(o.formElement,a.rulesSelector),a.registerSubmitCallback(),a.validate()}static initializeInputFields(){o.formElement.querySelectorAll(a.inputSelector).forEach(t=>{if("formengineInputInitialized"in t.dataset)return;const s=JSON.parse(t.dataset.formengineInputParams).field,r=o.formElement.querySelector(d`[name="${s}"]`);r.dataset.config=t.dataset.formengineInputParams,a.initializeInputField(s)})}static initializeInputField(t){const e=o.formElement.querySelector(d`[name="${t}"]`),s=o.formElement.querySelector(d`[data-formengine-input-name="${t}"]`);if(e.dataset.config!==void 0){const r=JSON.parse(e.dataset.config),i=a.formatByEvals(r,e.value);i.length&&(s.value=i)}new k("change",()=>{a.updateInputField(s.dataset.formengineInputName)}).bindTo(s),s.dataset.formengineInputInitialized="true",s.dispatchEvent(new Event("formengine:input:initialized"))}static registerCustomEvaluation(t,e){h.has(t)||h.set(t,e)}static formatByEvals(t,e){if(t.evalList!==void 0){const s=b.trimExplode(",",t.evalList);for(const r of s)e=a.formatValue(r,e)}return e}static formatValue(t,e){switch(t){case"date":case"datetime":case"time":case"timesec":case"datetimesec":if(e==="")return"";const s=p.fromISO(String(e));if(!s.isValid)throw new Error("Invalid ISO8601 DateTime string: "+e);return s.toISO({suppressMilliseconds:!0,includeOffset:!1});case"password":return e?a.passwordDummy:"";default:return e.toString()}}static updateInputField(t){const e=o.formElement.querySelector(d`[name="${t}"]`),s=o.formElement.querySelector(d`[data-formengine-input-name="${t}"]`);if(e.dataset.config!==void 0){const r=JSON.parse(e.dataset.config),i=a.processByEvals(r,s.value),u=a.formatByEvals(r,i);e.value!==i&&(e.disabled&&e.dataset.enableOnModification&&(e.disabled=!1),e.value=i,e.dispatchEvent(new Event("change"))),s.value!==u&&(s.value=u)}}static validateField(t){if(t.dataset.formengineValidationRules===void 0)return;let e=t.value||"";const s=JSON.parse(t.dataset.formengineValidationRules);let r=!1,i=0,u,n,c;Array.isArray(e)||(e=e.trimStart());for(const l of s){if(r)break;switch(l.type){case"required":e===""&&(r=!0,t.classList.add(a.errorClass),t.closest(a.markerSelector)?.querySelector(a.labelSelector)?.classList.add(a.errorClass));break;case"range":if(e!==""){if((l.minItems||l.maxItems)&&(u=o.formElement.querySelector(d`[name="${t.dataset.relatedfieldname}"]`),u!==null?i=b.trimExplode(",",u.value).length:i=parseInt(t.value,10),l.minItems!==void 0&&(n=l.minItems*1,!isNaN(n)&&ic&&(r=!0))),l.lower!==void 0)if(t.dataset.inputType==="datetimepicker"){const m=p.fromISO(e,{zone:"utc"}),I=p.fromISO(l.lower,{zone:"utc"});(!m.isValid||mI.plus((59-I.second)*1e3))&&(r=!0)}else{const m=l.upper*1;!isNaN(m)&&parseInt(e,10)>m&&(r=!0)}}break;case"select":case"category":(l.minItems||l.maxItems)&&(u=o.formElement.querySelector(d`[name="${t.dataset.relatedfieldname}"]`),u!==null?i=b.trimExplode(",",u.value).length:t instanceof HTMLSelectElement?i=t.querySelectorAll("option:checked").length:i=t.querySelectorAll("input[value]:checked").length,l.minItems!==void 0&&(n=l.minItems*1,!isNaN(n)&&ic&&(r=!0)));break;case"group":case"folder":(l.minItems||l.maxItems)&&(i=b.trimExplode(",",t.value).length,l.minItems!==void 0&&(n=l.minItems*1,!isNaN(n)&&ic&&(r=!0)));break;case"inline":(l.minItems||l.maxItems)&&(i=b.trimExplode(",",t.value).length,l.minItems!==void 0&&(n=l.minItems*1,!isNaN(n)&&ic&&(r=!0)));break;case"min":(t instanceof HTMLInputElement||t instanceof HTMLTextAreaElement)&&t.value.length>0&&t.value.length="a"&&c<="z"||c>="A"&&c<="Z",m=c>="0"&&c<="9";switch(t){case"alphanum":f=!1;break;case"alpha":m=!1,f=!1;break;case"num":l=!1,f=!1;break;default:break}(l||m||f)&&(r+=c)}r!==e&&(n=r);break;case"is_in":if(s.is_in){i=""+e,s.is_in=s.is_in.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&");const c=new RegExp("[^"+s.is_in+"]+","g");r=i.replace(c,"")}else r=i;n=r;break;case"nospace":n=(""+e).replace(/ /g,"");break;case"md5":e!==""&&(n=y.hash(e));break;case"upper":n=e.toUpperCase();break;case"lower":n=e.toLowerCase();break;case"integer":e!==""&&(n=a.parseInt(e).toString());break;case"decimal":e!==""&&(n=a.parseDouble(e));break;case"trim":n=String(e).trim();break;case"time":case"timesec":e!==""&&(n=p.fromISO(e).set({year:1970,month:1,day:1}).toISO({suppressMilliseconds:!0,includeOffset:!1}));break;case"null":break;case"password":break;default:h.has(t)?n=h.get(t).call(null,e):typeof TBE_EDITOR=="object"&&TBE_EDITOR.customEvalFunctions!==void 0&&typeof TBE_EDITOR.customEvalFunctions[t]=="function"&&(n=TBE_EDITOR.customEvalFunctions[t](e))}return n}static validate(t){(typeof t>"u"||t instanceof Document)&&o.formElement.querySelectorAll(a.markerSelector+', [role="tablist"] > .nav-item').forEach(s=>{s.classList.remove(a.validationErrorClass)});const e=t||document;for(const s of e.querySelectorAll(a.rulesSelector))s.closest(".t3js-flex-section-deleted, .t3js-inline-record-deleted, .t3js-file-reference-deleted")===null&&a.validateField(s)}static parseInt(t){const e=""+t;if(!t)return 0;const s=parseInt(e,10);return isNaN(s)?0:s}static parseDouble(t,e=2){let s=""+t;s=s.replace(/[^0-9,.-]/g,"");const r=s.startsWith("-");s=s.replace(/-/g,""),s=s.replace(/,/g,"."),s.indexOf(".")===-1&&(s+=".0");const i=s.split("."),u=i.pop();let n=+(i.join("")+"."+u);return r&&(n*=-1),s=n.toFixed(e),s}static markParentTab(t,e){x.parents(t,".tab-pane").forEach(r=>{e&&(e=r.querySelector(".has-error")===null);const i=r.id;o.formElement.querySelector(d`[data-typo3-tab="${"#"+i}"]`).closest(".nav-item").classList.toggle(a.validationErrorClass,!e)})}static suspend(){S=!0}static resume(){S=!1}static isValid(){return document.querySelector("."+a.errorClass)===null}static showErrorModal(){const t=g.confirm(v.get("labels.fieldsMissing.title"),v.get("labels.fieldsMissing"),w.error,[{text:C.get("button.ok"),active:!0,btnClass:"btn-default",name:"ok"}]);t.addEventListener("button.clicked",()=>t.hideModal())}static registerSubmitCallback(){new N(o.formElement).addPreSubmitCallback(()=>S||a.isValid()?!0:(a.showErrorModal(),!1))}}export{a as default}; diff --git a/Resources/Public/JavaScript/form-engine.js b/Resources/Public/JavaScript/form-engine.js new file mode 100644 index 0000000..70742dd --- /dev/null +++ b/Resources/Public/JavaScript/form-engine.js @@ -0,0 +1,15 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import T from"@typo3/core/document-service.js";import D from"@typo3/backend/form-engine-validation.js";import p from"@typo3/backend/modal.js";import*as j from"@typo3/backend/utility/message-utility.js";import w from"@typo3/backend/severity.js";import*as P from"@typo3/backend/backend-exception.js";import I from"@typo3/backend/event/interaction-request-map.js";import N from"@typo3/backend/utility.js";import{selector as y}from"@typo3/core/literals.js";import"@typo3/backend/form-engine/element/extra/char-counter.js";import M,{ModifierKeys as O}from"@typo3/backend/hotkeys.js";import v from"@typo3/core/event/regular-event.js";import s from"~labels/backend.alt_doc";import _ from"~labels/core.core";var C;(function(E){E.save="_savedok",E.saveAndClose="_saveandclosedok",E.saveAndView="_savedokview",E.saveAndNew="_savedoknew",E.duplicate="_duplicatedoc"})(C||(C={}));var z=function(){function E(t,n){n?e.interactionRequestMap.resolveFor(t):e.interactionRequestMap.rejectFor(t)}const k=new Map;k.set("typo3-backend-form-update-value",t=>{const n=document.querySelector(y`[name="${t.elementName}"]`),a=document.querySelector(y`[data-formengine-input-name="${t.elementName}"]`);e.Validation.updateInputField(t.elementName),n!==null&&(e.markFieldAsChanged(n),e.Validation.validateField(n)),a!==null&&a!==n&&e.Validation.validateField(a)}),k.set("typo3-backend-form-reload",t=>{const n=()=>{e.Validation.suspend(),e.saveDocument(),e.Validation.resume()};if(!t.confirmation){n();return}const a=p.advanced({title:_.get("mess.refreshRequired.title"),content:_.get("mess.refreshRequired.content"),severity:w.warning,staticBackdrop:!0,buttons:[{text:_.get("mess.refreshRequired.cancel"),active:!0,btnClass:"btn-default",name:"cancel",trigger:()=>{a.hideModal()}},{text:_.get("mess.refreshRequired.confirm"),btnClass:"btn-"+w.getCssClass(w.warning),name:"ok",trigger:()=>{e.closeModalsRecursive(),n()}}]})}),k.set("typo3-backend-form-update-bitmask",(t,n)=>{const a=n.target,o=e.formElement[t.elementName],i=a.checked!==t.invert,l=Math.pow(2,t.position),u=Math.pow(2,t.total)-l-1;o.value=i?o.value|l:o.value&u,o.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0}))});let A=!1,q=null;const e={consumeTypes:["typo3.setUrl","typo3.beforeSetUrl","typo3.refresh"],Validation:D,interactionRequestMap:I,formName:"editform",formElement:void 0,openedPopupWindow:null,browserUrl:""};Object.defineProperty(e,"formElement",{get:()=>document.forms.namedItem(e.formName),enumerable:!0,configurable:!1}),e.ready=async function(){return q??(q=async function(){A||await new Promise(n=>e.formElement.addEventListener("typo3:form-engine:ready",()=>n(),{once:!0}))}())},e.openPopupWindow=function(t,n,a,o,i,l,u){const c={mode:t};return n&&(c.fieldReference=n),a&&(c.allowedTypes=a),o&&(c.disallowedFileExtensions=o),i&&(c.irreObjectId=i),l&&(t==="db"?c.expandPage=l:c.expandFolder=l),c.useEvents=u?"1":"0",p.advanced({type:p.types.iframe,content:e.browserUrl+"&"+new URLSearchParams(c).toString(),size:p.sizes.large})},e.setSelectOptionFromExternalSource=function(t,n,a,o,i=[],l=void 0){let u=!1,c=!1,r=e.getFieldElement(t);const m=r;if(m===null||n==="--div--"||m instanceof HTMLOptGroupElement)return;const f=e.getFieldElement(t,"_list",!0);if(f!==null&&(r=f,u=r.multiple&&r.size!==1,c=!0),u||c){const h=e.getFieldElement(t,"_avail");if(h===null)return;if(!u){for(const d of r.querySelectorAll("option")){const b=h.querySelector(y`option[value="${d.value}"]`);b!==null&&(b.classList.remove("hidden"),b.disabled=!1,e.enableOptGroup(b))}r.replaceChildren()}if(i.length>0){let d=!1;const b=r.querySelectorAll("option");(i.includes(n)||b.length===1&&i.includes(b[0].value))&&(r.replaceChildren(),d=!0),d&&typeof l<"u"&&l.closest("select").querySelectorAll("[disabled]").forEach(function(x){x.classList.remove("hidden"),x.disabled=!1,e.enableOptGroup(x)})}let g=!0;const S=e.getFieldElement(t,"_mul",!0);if(S===null||Number(S.value)===0){for(const d of r.querySelectorAll("option"))if(d.value===n){g=!1;break}if(g&&typeof l<"u"){l.classList.add("hidden"),l.disabled=!0;const d=l.parentElement;d instanceof HTMLOptGroupElement&&d.querySelectorAll("option:not([disabled]):not([hidden]):not(.hidden)").length===0&&(d.disabled=!0,d.classList.add("hidden"))}}if(g){const d=document.createElement("option");d.value=n,d.title=o,d.text=a,r.append(d),e.updateHiddenFieldValueFromSelect(r,m),e.markFieldAsChanged(m),e.Validation.validateField(r),e.Validation.validateField(h)}}else{const h=/_(\d+)$/,g=n.toString().match(h);g!=null&&(n=g[1]),r.value=n,e.Validation.validateField(r)}},e.updateHiddenFieldValueFromSelect=function(t,n){const a=Array.from(t.options).map(o=>o.value);n.value=a.join(","),n.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0}))},e.getFieldElement=function(t,n,a){if(n){let i;switch(n){case"_list":i=e.formElement.querySelector(y`:is(input, select, textarea)[data-formengine-input-name="${t}"]:not([type=hidden])`);break;case"_avail":i=e.formElement.querySelector(y`:is(input, select, textarea)[data-relatedfieldname="${t}"]`);break;case"_mul":i=e.formElement.querySelector(y`input[type=hidden][data-formengine-input-name="${t}"]`);break;default:i=null;break}if(i!==null||a===!0)return i}const o=e.formElement.elements.namedItem(t);return o instanceof HTMLElement?o:null},e.delegatesInitialized=!1,e.initializeDelegates=function(){e.delegatesInitialized||(new v("click",t=>{t.preventDefault(),e.preventExitIfNotSaved(e.preventExitIfNotSavedCallback)}).delegateTo(document,".t3js-editform-close"),new v("click",t=>{t.preventDefault(),e.previewAction(t,e.previewActionCallback)}).delegateTo(document,".t3js-editform-view"),new v("click",t=>{t.preventDefault(),e.newAction(t,e.newActionCallback)}).delegateTo(document,".t3js-editform-new"),new v("click",t=>{t.preventDefault(),e.duplicateAction(t,e.duplicateActionCallback)}).delegateTo(document,".t3js-editform-duplicate"),new v("click",t=>{t.preventDefault(),e.deleteAction(t,e.deleteActionCallback)}).delegateTo(document,".t3js-editform-delete-record"),new v("change",(t,n)=>{n.closest(".t3js-formengine-field-item").classList.toggle("disabled")}).delegateTo(document,'.t3-form-field-eval-null-checkbox input[type="checkbox"]'),new v("change",(t,n)=>{e.toggleCheckboxField(n),e.markFieldAsChanged(n)}).delegateTo(document,'.t3js-form-field-eval-null-placeholder-checkbox input[type="checkbox"]'),new v("click",(t,n)=>{t.preventDefault(),t.stopPropagation();const a=n.dataset.mode,o=n.dataset.fieldReference??"",i=n.dataset.allowedTypes??"",l=n.dataset.disallowedTypes??"",u=n.dataset.irreObjectId??"",c=n.dataset.entryPoint,r=n.dataset.useEvents==="true",m=e.openPopupWindow(a,o,i,l,u,c,r);r&&m.addEventListener("typo3:element-browser:message",f=>{const{actionName:h,close:g}=f.detail;if(h==="typo3:elementBrowser:elementAdded"){const{fieldName:S,value:d,label:b}=f.detail;e.setSelectOptionFromExternalSource(S,d,b||d,b||d)}g&&(m.hideModal(),n.focus())})}).delegateTo(document,".t3js-element-browser"),new v("click",(t,n)=>{const a=JSON.parse(n.dataset.formengineFieldChangeItems);e.processOnFieldChange(a,t)}).delegateTo(document,'[data-formengine-field-change-event="click"]'),new v("change",(t,n)=>{const a=JSON.parse(n.dataset.formengineFieldChangeItems);e.processOnFieldChange(a,t)}).delegateTo(document,'[data-formengine-field-change-event="change"]'),window.addEventListener("message",e.handlePostMessage),e.delegatesInitialized=!0)},e.initializeEvents=function(){top.TYPO3&&typeof top.TYPO3.Backend<"u"&&(top.TYPO3.Backend.consumerScope.attach(e),window.addEventListener("pagehide",()=>top.TYPO3.Backend.consumerScope.detach(e),{once:!0}))},e.consume=function(t){if(!t)throw new P.BackendException("No interaction request given",1496589980);let n;const a=new Promise((o,i)=>{n={resolve:o,reject:i}});if(t.concernsTypes(e.consumeTypes)){const o=t.outerMostRequest;e.interactionRequestMap.attachFor(o,n),o.isProcessed()?E(o,o.getProcessedData().response):e.hasChange()||e.isNew()?e.preventExitIfNotSaved(function(i){o.setProcessedData({response:i}),E(o,i)}):e.interactionRequestMap.resolveFor(o)}return a},e.handlePostMessage=function(t){if(!j.MessageUtility.verifyOrigin(t.origin))throw"Denied message sent by "+t.origin;if(t.data.actionName==="typo3:elementBrowser:elementAdded"){if(typeof t.data.fieldName>"u")throw"fieldName not defined in message";if(typeof t.data.value>"u")throw"value not defined in message";const n=t.data.label||t.data.value,a=t.data.title||n,o=N.trimExplode(",",t.data?.exclusiveValues??"");e.setSelectOptionFromExternalSource(t.data.fieldName,t.data.value,n,a,o)}},e.initializeRemainingCharacterViews=function(){document.querySelectorAll('[maxlength]:not([data-input-type="datetimepicker"]):not(.t3js-color-picker)').forEach(n=>{const a=n.closest(".t3js-formengine-field-item");if(a!==null&&a.querySelector("typo3-backend-formengine-char-counter")===null){const o=document.createElement("typo3-backend-formengine-char-counter");o.setAttribute("target",`[data-formengine-input-name="${y`${n.dataset.formengineInputName}`}"]`),a.append(o)}})},e.initializeMinimumCharactersLeftViews=function(){const t=(o,i)=>{const l=i.currentTarget.closest(".t3js-formengine-field-item"),u=l.querySelector(".t3js-charcounter-min"),c=_.get("labels.remainingCharacters",{0:o});if(u)u.querySelector("span").innerHTML=c;else{const r=document.createElement("div");r.classList.add("t3js-charcounter-min");const m=document.createElement("span");m.classList.add("badge","badge-danger"),m.innerHTML=c,r.append(m);let f=l.querySelector(".t3js-charcounter-wrapper");f||(f=document.createElement("div"),f.classList.add("t3js-charcounter-wrapper"),l.append(f)),f.prepend(r)}},n=o=>{const l=o.currentTarget.closest(".t3js-formengine-field-item").querySelector(".t3js-charcounter-min");l&&l.remove()};document.querySelectorAll('[minlength]:not([data-input-type="datetimepicker"]):not(.t3js-charcounter-min-initialized)').forEach(o=>{o.addEventListener("focus",i=>{const l=e.getMinCharacterLeftCount(o);l>0&&t(l,i)}),o.addEventListener("blur",n),o.addEventListener("keyup",i=>{const l=e.getMinCharacterLeftCount(o);l>0?t(l,i):n(i)})})},e.getMinCharacterLeftCount=function(t){const n=t.value,a=t.minLength,o=n.length;if(o===0)return 0;const i=(n.match(/\n/g)||[]).length;return a-o-i},e.initializeNullNoPlaceholderCheckboxes=function(){document.querySelectorAll(".t3-form-field-eval-null-checkbox").forEach(t=>{const n=t.querySelector('input[type="checkbox"]'),a=t.closest(".t3js-formengine-field-item");n.checked||a.classList.add("disabled")})},e.initializeNullWithPlaceholderCheckboxes=function(){document.querySelectorAll(".t3js-form-field-eval-null-placeholder-checkbox").forEach(t=>{e.toggleCheckboxField(t.querySelector('input[type="checkbox"]'),!1)})},e.toggleCheckboxField=function(t,n=!0){const a=t.closest(".t3js-formengine-field-item"),o=a.querySelector(".t3js-formengine-placeholder-placeholder"),i=a.querySelector(".t3js-formengine-placeholder-formfield");t.checked?(o.hidden=!0,i.hidden=!1,n&&i.querySelector("input,select,textarea")?.focus()):(o.hidden=!1,i.hidden=!0)},e.reinitialize=function(){const t=document.querySelectorAll(".t3js-clearable");t.length>0&&import("@typo3/backend/input/clearable.js").then(function(){t.forEach(n=>n.clearable())}),e.initializeNullNoPlaceholderCheckboxes(),e.initializeNullWithPlaceholderCheckboxes(),e.initializeLocalizationStateSelector(),e.initializeMinimumCharactersLeftViews(),e.initializeRemainingCharacterViews()},e.initializeLocalizationStateSelector=function(){document.querySelectorAll(".t3js-l10n-state-container").forEach(t=>{const n=t.closest(".t3js-formengine-field-item")?.querySelector("[data-formengine-input-name]");if(n==null)return;const a=t.querySelector('input[type="radio"]:checked')?.value;a===void 0&&console.warn("The localization state of the field "+n.dataset.formengineInputName+" cannot be determined. This smells like a DataHandler bug."),(a==="parent"||a==="source")&&(n.disabled=!0)})},e.hasChange=function(){const t=document.querySelector(y`form[name="${e.formName}"] .has-change`)!==null,n=document.querySelector('[name^="data["].has-change')!==null;return t||n},e.isNew=function(){return document.querySelector('form[name="'+e.formName+'"] .typo3-TCEforms.is-new')!==null},e.markFieldAsChanged=function(t){t.classList.add("has-change");const n=t.closest(".t3js-formengine-palette-field")?.querySelector(".t3js-formengine-label");n?.classList.add("has-change")},e.preventExitIfNotSavedCallback=()=>{e.closeDocument()},e.preventFollowLinkIfNotSaved=function(t){return e.preventExitIfNotSaved(function(){window.location.href=t}),!1},e.preventExitIfNotSaved=function(t){if(t=t||e.preventExitIfNotSavedCallback,e.hasChange()||e.isNew()){const n=s.get("label.confirm.close_without_save.title"),a=s.get("label.confirm.close_without_save.content"),o=[{text:s.get("buttons.confirm.close_without_save.no"),btnClass:"btn-default",name:"no"},{text:s.get("buttons.confirm.close_without_save.yes"),btnClass:"btn-default",name:"yes"}];document.querySelector(".has-error")===null&&o.push({text:s.get("buttons.confirm.save_and_close"),btnClass:"btn-primary",name:"save",active:!0});const i=p.confirm(n,a,w.warning,o);i.addEventListener("button.clicked",function(l){l.target.name==="no"?i.hideModal():l.target.name==="yes"?(i.hideModal(),t.call(null,!0)):l.target.name==="save"&&(i.hideModal(),e.saveAndCloseDocument())})}else t.call(null,!0)},e.preventSaveIfHasErrors=function(){if(document.querySelector(".has-error")!==null){const t=s.get("label.alert.save_with_error.title"),n=s.get("label.alert.save_with_error.content"),a=p.confirm(t,n,w.error,[{text:s.get("buttons.alert.save_with_error.ok"),btnClass:"btn-danger",name:"ok"}]);return a.addEventListener("button.clicked",function(o){o.target.name==="ok"&&a.hideModal()}),!1}return!0},e.processOnFieldChange=function(t,n){t.forEach(a=>{const o=k.get(a.name);o instanceof Function&&o.call(null,a.data||null,n)})},e.registerOnFieldChangeHandler=function(t,n){k.has(t)&&console.warn("Handler for onFieldChange name `"+t+"` has been overridden."),k.set(t,n)},e.closeModalsRecursive=function(){typeof p.currentModal<"u"&&p.currentModal!==null&&(p.currentModal.addEventListener("typo3-modal-hidden",function(){e.closeModalsRecursive()}),p.currentModal.hideModal())},e.enableDocHeaderButtons=function(){const t=document.querySelector(".t3js-module-docheader-buttons");t&&t.querySelectorAll('button, a, input[type="submit"]').forEach(n=>{n instanceof HTMLButtonElement||n instanceof HTMLInputElement?n.disabled=!1:n instanceof HTMLAnchorElement&&(n.classList.remove("disabled"),n.removeAttribute("aria-disabled"))})};const L=t=>{const n=document.createElement("input");return n.type="hidden",n.name=t,n.value="1",n};e.previewAction=function(t,n){n=n||e.previewActionCallback;const a=t.target.href,o="isNew"in t.target.dataset,i=L(C.saveAndView);e.hasChange()||e.isNew()?e.showPreviewModal(a,o,i,n):(e.formElement.append(i),window.open("","newTYPO3frontendWindow"),e.formElement.submit())},e.previewActionCallback=function(t,n,a){switch(p.dismiss(),t){case"discard":const o=window.open(n,"newTYPO3frontendWindow");o.focus(),N.urlsPointToSameServerSideResource(o.location.href,n)&&o.location.reload();break;case"save":e.formElement.append(a),window.open("","newTYPO3frontendWindow"),e.saveDocument();break;default:break}},e.showPreviewModal=function(t,n,a,o){const i=s.get("label.confirm.view_record_changed.title"),l={text:s.get("buttons.confirm.view_record_changed.cancel"),btnClass:"btn-default",name:"cancel"},u={text:s.get("buttons.confirm.view_record_changed.no-save"),btnClass:"btn-default",name:"discard"},c={text:s.get("buttons.confirm.view_record_changed.save"),btnClass:"btn-primary",name:"save",active:!0};let r=[],m=[];if(n){if(!e.Validation.isValid()){e.Validation.showErrorModal();return}r=[l,c],m=[s.get("label.confirm.view_record_changed.content.is-new-page")]}else r=[l,u],m=[s.get("label.confirm.view_record_changed.content")],e.Validation.isValid()?r.push(c):m.push(s.get("label.confirm.view_record_changed.invalid_form"));const f=document.createElement("p");m.forEach((g,S)=>{f.append(g),S!==m.length-1&&f.append(document.createElement("br"))});const h=p.confirm(i,f,w.info,r);h.addEventListener("button.clicked",function(g){o(g.target.name,t,a,h)})},e.newAction=function(t,n){n=n||e.newActionCallback;const a=L(C.saveAndNew),o="isNew"in t.target.dataset;e.hasChange()||e.isNew()?e.showNewModal(o,a,n):(e.formElement.append(a),e.formElement.submit())},e.newActionCallback=function(t,n){switch(p.dismiss(),t){case"no":e.formElement.append(n),e.formElement.submit();break;case"yes":e.formElement.append(n),e.saveDocument();break;default:break}},e.showNewModal=function(t,n,a){const o=s.get("label.confirm.new_record_changed.title"),i=s.get("label.confirm.new_record_changed.content");let l=[];const u={text:s.get("buttons.confirm.new_record_changed.cancel"),btnClass:"btn-default",name:"cancel"},c={text:s.get("buttons.confirm.new_record_changed.no"),btnClass:"btn-default",name:"no"},r={text:s.get("buttons.confirm.new_record_changed.yes"),btnClass:"btn-primary",name:"yes",active:!0};t?l=[u,r]:l=[u,c,r],p.confirm(o,i,w.info,l).addEventListener("button.clicked",function(f){a(f.target.name,n)})},e.duplicateAction=function(t,n){n=n||e.duplicateActionCallback;const a=L(C.duplicate),o="isNew"in t.target.dataset;e.hasChange()||e.isNew()?e.showDuplicateModal(o,a,n):(e.formElement.append(a),e.formElement.submit())},e.duplicateActionCallback=function(t,n){switch(p.dismiss(),t){case"no":e.formElement.append(n),e.formElement.submit();break;case"yes":e.formElement.append(n),e.saveDocument();break;default:break}},e.showDuplicateModal=function(t,n,a){const o=s.get("label.confirm.duplicate_record_changed.title"),i=s.get("label.confirm.duplicate_record_changed.content");let l=[];const u={text:s.get("buttons.confirm.duplicate_record_changed.cancel"),btnClass:"btn-default",name:"cancel"},c={text:s.get("button.confirm.duplicate_record_changed.no"),btnClass:"btn-default",name:"no"},r={text:s.get("buttons.confirm.duplicate_record_changed.yes"),btnClass:"btn-primary",name:"yes",active:!0};t?l=[u,r]:l=[u,c,r],p.confirm(o,i,w.info,l).addEventListener("button.clicked",function(f){a(f.target.name,n)})},e.deleteAction=function(t,n){n=n||e.deleteActionCallback;const a=t.target.closest(".t3js-editform-delete-record");e.showDeleteModal(a,n)},e.deleteActionCallback=function(t,n){p.dismiss(),t==="yes"&&e.invokeRecordDeletion(n)},e.showDeleteModal=function(t,n){const a=s.get("label.confirm.delete_record.title");let o=s.get("label.confirm.delete_record.content",[t.dataset.recordInfo]);t.dataset.referenceCountMessage&&(o+=` +`+t.dataset.referenceCountMessage),t.dataset.translationCountMessage&&(o+=` +`+t.dataset.translationCountMessage),p.confirm(a,o,w.warning,[{text:s.get("buttons.confirm.delete_record.no"),btnClass:"btn-default",name:"no"},{text:s.get("buttons.confirm.delete_record.yes"),btnClass:"btn-warning",name:"yes",active:!0}]).addEventListener("button.clicked",function(l){n(l.target.name,t)})},e.enableOptGroup=function(t){const n=t.parentElement;n instanceof HTMLOptGroupElement&&n.querySelectorAll("option:not([hidden]):not([disabled]):not(.hidden)").length&&(n.hidden=!1,n.disabled=!1,n.classList.remove("hidden"))},e.closeDocument=function(){e.formElement.closeDoc.value=1,e.formElement.submit()};const F=t=>{const n=document.activeElement;(n instanceof HTMLInputElement||n instanceof HTMLSelectElement||n instanceof HTMLTextAreaElement)&&n.blur();const a=document.querySelector(y`button[name="${t}"][form="${e.formElement.id}"]`);a!==null?e.formElement.requestSubmit(a):(e.formElement.append(L(t)),e.formElement.requestSubmit())};return e.saveDocument=function(){F(C.save)},e.saveAndCloseDocument=function(){F(C.saveAndClose)},e.initialize=function(t){e.browserUrl=t,e.initializeDelegates(),T.ready().then(()=>{e.initializeEvents(),e.Validation.initialize(this),e.reinitialize(),document.getElementById("t3js-ui-block")?.remove(),e.enableDocHeaderButtons(),e.formElement.dispatchEvent(new Event("typo3:form-engine:ready")),A=!0,M.setScope("backend/form-engine"),M.register([M.normalizedCtrlModifierKey,"s"],n=>{n.preventDefault(),e.saveDocument()},{scope:"backend/form-engine",allowOnEditables:!0}),M.register([M.normalizedCtrlModifierKey,O.SHIFT,"s"],n=>{n.preventDefault(),e.saveAndCloseDocument()},{scope:"backend/form-engine",allowOnEditables:!0})})},e.invokeRecordDeletion=function(t){window.location.href=t.href},TYPO3.FormEngine=e,e}();export{z as default}; diff --git a/Resources/Public/JavaScript/form-engine/container/flex-form-container-container.js b/Resources/Public/JavaScript/form-engine/container/flex-form-container-container.js new file mode 100644 index 0000000..275e2be --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/container/flex-form-container-container.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{Collapse as d}from"bootstrap";import p from"@typo3/core/security-utility.js";import f from"@typo3/backend/modal.js";import s from"@typo3/core/event/regular-event.js";import u from"@typo3/backend/severity.js";import g from"@typo3/backend/storage/client.js";import m from"@typo3/backend/form-engine.js";import C from"~labels/core.core";import a from"~labels/backend.alt_doc";var n;(function(r){r.actionFieldSelector=".t3js-flex-control-action",r.sectionContentContainerSelector=".t3js-flex-section-content",r.sectionContainerLabelSelector=".t3js-formengine-label",r.deleteContainerButtonSelector=".t3js-delete",r.contentPreviewSelector=".content-preview"})(n||(n={}));class l{constructor(e,t){this.securityUtility=new p,this.parentContainer=e,this.container=t,this.containerContent=t.querySelector(n.sectionContentContainerSelector),this.containerId=t.dataset.flexformContainerId,this.toggleKeyInLocalStorage=`formengine-flex-${e.getSectionContainer().id}-${this.containerId}-collapse`,this.panelButton=t.querySelector(":scope > .panel-heading .panel-button"),this.panelHeading=this.panelButton.closest(".panel-heading"),this.registerEvents()}static getCollapseInstance(e,t){return d.getInstance(e)??new d(e,{toggle:t})}getCollapseContent(){return this.containerContent}getStatus(){return{id:this.containerId,collapsed:this.panelButton.getAttribute("aria-expanded")==="false"}}async registerEvents(){this.parentContainer.isRestructuringAllowed()&&this.registerDelete(),this.registerPanelToggle(),await this.registerToggle(),this.registerPreviewUpdate()}registerDelete(){new s("click",()=>{const e=a.get("flexform.section.delete.title"),t=a.get("flexform.section.delete.message"),i=f.confirm(e,t,u.warning,[{text:a.get("buttons.confirm.delete_record.no"),active:!0,btnClass:"btn-default",name:"no"},{text:a.get("buttons.confirm.delete_record.yes"),btnClass:"btn-warning",name:"yes"}]);i.addEventListener("button.clicked",o=>{if(o.target.name==="yes"){const c=this.container.querySelector(n.actionFieldSelector);c.value="DELETE",this.container.appendChild(c),this.container.classList.add("t3-flex-section--deleted"),this.container.closest(".t3-form-field-container.t3-form-flex")?.querySelector(n.sectionContainerLabelSelector)?.classList.add("has-change"),new s("transitionend",()=>{this.container.classList.add("hidden");const h=new CustomEvent("formengine:flexform:container-deleted",{detail:{containerId:this.containerId}});this.parentContainer.getContainer().dispatchEvent(h)}).bindTo(this.container)}i.hideModal()})}).bindTo(this.container.querySelector(n.deleteContainerButtonSelector))}async registerToggle(){const e=(g.get(this.toggleKeyInLocalStorage)??"1")==="1";l.getCollapseInstance(this.containerContent,!e),await m.ready(),this.generatePreview()}registerPanelToggle(){["hide.bs.collapse","show.bs.collapse"].forEach(e=>{new s(e,t=>{if(t.target!==this.containerContent)return;const i=t.type==="hide.bs.collapse";g.set(this.toggleKeyInLocalStorage,i?"1":"0")}).bindTo(this.containerContent)})}registerPreviewUpdate(){["input","change"].forEach(e=>{new s(e,()=>{this.generatePreview()}).delegateTo(this.containerContent,'input[type="text"], textarea')})}generatePreview(){let e="";const t=this.containerContent.querySelectorAll('input[type="text"], textarea');for(const i of t){let o=this.securityUtility.stripHtml(i.value);o.length>50&&(o=o.substring(0,50)+"..."),e+=(e?" / ":"")+o}e===""&&(e="["+C.get("labels.no_title")+"]"),this.panelHeading.querySelector(n.contentPreviewSelector).textContent=e}}export{l as default}; diff --git a/Resources/Public/JavaScript/form-engine/container/flex-form-section-container.js b/Resources/Public/JavaScript/form-engine/container/flex-form-section-container.js new file mode 100644 index 0000000..0ae9da3 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/container/flex-form-section-container.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{Collapse as h}from"bootstrap";import C from"sortablejs";import g from"@typo3/core/ajax/ajax-request.js";import u from"@typo3/core/document-service.js";import m from"@typo3/backend/form-engine/container/flex-form-container-container.js";import l from"@typo3/backend/form-engine.js";import f from"@typo3/core/event/regular-event.js";import{JavaScriptItemProcessor as p}from"@typo3/core/java-script-item-processor.js";var o;(function(r){r.toggleAllSelector=".t3-form-flexsection-toggle",r.addContainerSelector=".t3js-flex-container-add",r.actionFieldSelector=".t3js-flex-control-action",r.sectionContainerSelector=".t3js-flex-section",r.sectionContentContainerSelector=".t3js-flex-section-content",r.sectionContainerLabelSelector=".t3js-formengine-label",r.sortContainerButtonSelector=".t3js-sortable-handle"})(o||(o={}));class c{constructor(e){this.allowRestructure=!1,this.flexformContainerContainers=[],this.updateSorting=t=>{this.container.querySelectorAll(o.actionFieldSelector).forEach((i,s)=>{i.value=s.toString()}),this.flexformContainerContainers.splice(t.newIndex,0,this.flexformContainerContainers.splice(t.oldIndex,1)[0])},u.ready().then(t=>{this.container=t.getElementById(e),this.sectionContainer=this.container.querySelector(this.container.dataset.section),this.allowRestructure=this.sectionContainer.dataset.t3FlexAllowRestructure==="1",this.registerEvents(),this.registerContainers()})}static getCollapseInstance(e){return h.getInstance(e)??new h(e,{toggle:!1})}getContainer(){return this.container}getSectionContainer(){return this.sectionContainer}isRestructuringAllowed(){return this.allowRestructure}registerEvents(){this.allowRestructure&&(this.registerSortable(),this.registerContainerDeleted()),this.registerToggleAll(),this.registerCreateNewContainer()}registerContainers(){const e=this.container.querySelectorAll(o.sectionContainerSelector);for(const t of e)this.flexformContainerContainers.push(new m(this,t))}getToggleAllButton(){return this.container.querySelector(o.toggleAllSelector)}registerSortable(){new C(this.sectionContainer,{group:this.sectionContainer.id,handle:o.sortContainerButtonSelector,onSort:this.updateSorting})}registerToggleAll(){new f("click",()=>{const e=this.flexformContainerContainers.some(t=>t.getStatus().collapsed);for(const t of this.flexformContainerContainers){const n=t.getCollapseContent();n!==null&&(e?c.getCollapseInstance(n).show():c.getCollapseInstance(n).hide())}}).bindTo(this.getToggleAllButton())}registerCreateNewContainer(){new f("click",(e,t)=>{e.preventDefault(),this.createNewContainer(t.dataset)}).delegateTo(this.container,o.addContainerSelector)}createNewContainer(e){new g(TYPO3.settings.ajaxUrls.record_flex_container_add).post({vanillaUid:e.vanillauid,databaseRowUid:e.databaserowuid,command:e.command,tableName:e.tablename,fieldName:e.fieldname,recordTypeValue:e.recordtypevalue,flexFormSheetName:e.flexformsheetname,flexFormFieldName:e.flexformfieldname,flexFormContainerName:e.flexformcontainername}).then(async t=>{const n=await t.resolve(),i=new DOMParser().parseFromString(n.html,"text/html").body.firstElementChild;this.flexformContainerContainers.push(new m(this,i));const s=document.querySelector(e.target);if(s.insertAdjacentElement("beforeend",i),n.scriptItems instanceof Array&&n.scriptItems.length>0&&new p().processItems(n.scriptItems),n.stylesheetFiles&&n.stylesheetFiles.length>0)for(const d of n.stylesheetFiles){const a=document.createElement("link");a.rel="stylesheet",a.type="text/css",a.href=d,document.head.appendChild(a)}l.reinitialize(),l.Validation.initializeInputFields(),l.Validation.validate(s),this.container.querySelector(o.sectionContainerLabelSelector)?.classList.add("has-change")})}registerContainerDeleted(){new f("formengine:flexform:container-deleted",e=>{const t=e.detail.containerId;this.flexformContainerContainers=this.flexformContainerContainers.filter(n=>n.getStatus().id!==t),l.Validation.validate(this.container)}).bindTo(this.container)}}export{c as default}; diff --git a/Resources/Public/JavaScript/form-engine/container/inline-control-container.js b/Resources/Public/JavaScript/form-engine/container/inline-control-container.js new file mode 100644 index 0000000..8c0e562 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/container/inline-control-container.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{MessageUtility as R}from"@typo3/backend/utility/message-utility.js";import{Collapse as x}from"bootstrap";import{AjaxDispatcher as w}from"@typo3/backend/form-engine/inline-relation/ajax-dispatcher.js";import F from"@typo3/core/document-service.js";import"@typo3/backend/element/progress-bar-element.js";import E from"sortablejs";import j from"@typo3/backend/form-engine.js";import v from"@typo3/backend/form-engine-validation.js";import S from"@typo3/backend/icons.js";import B from"@typo3/backend/info-window.js";import O from"@typo3/backend/modal.js";import D from"@typo3/backend/notification.js";import h from"@typo3/core/event/regular-event.js";import U from"@typo3/backend/severity.js";import g from"@typo3/backend/utility.js";import{selector as p}from"@typo3/core/literals.js";import C from"~labels/backend.alt_doc";import G from"~labels/core.core";var f;(function(c){c.controlSectionSelector=".t3js-formengine-irre-control",c.createNewRecordButtonSelector=".t3js-create-new-button",c.createNewRecordBySelectorSelector=".t3js-create-new-selector",c.createNewRecordByPresetSelector=".t3js-create-new-preset",c.deleteRecordButtonSelector=".t3js-editform-delete-inline-record",c.enableDisableRecordButtonSelector=".t3js-toggle-visibility-button",c.infoWindowButton='[data-action="infowindow"]',c.synchronizeLocalizeRecordButtonSelector=".t3js-synchronizelocalize-button",c.uniqueValueSelectors="select.t3js-inline-unique",c.revertUniqueness=".t3js-revert-unique",c.controlContainer=".t3js-inline-controls"})(f||(f={}));var q;(function(c){c.new="inlineIsNewRecord",c.notLoaded="t3js-not-loaded"})(q||(q={}));var m;(function(c){c.structureSeparator="-"})(m||(m={}));var b;(function(c){c.DOWN="down",c.UP="up"})(b||(b={}));class d extends HTMLElement{constructor(){super(...arguments),this.ajaxDispatcher=null,this.recordsContainer=null,this.requestQueue={},this.progressQueue={},this.noTitleString=G.get("labels.no_title"),this.handlePostMessage=e=>{if(!R.verifyOrigin(e.origin))throw"Denied message sent by "+e.origin;if(e.data.actionName==="typo3:foreignRelation:insert"){if(typeof e.data.objectGroup>"u")throw"No object group defined for message";if(e.data.objectGroup!==this.objectGroup)return;if(this.isUniqueElementUsed(parseInt(e.data.uid,10),e.data.table)){D.error("There is already a relation to the selected element");return}this.importRecord([e.data.objectGroup,e.data.uid]).then(()=>{if(e.source){const t={actionName:"typo3:foreignRelation:inserted",objectGroup:e.data.objectId,table:e.data.table,uid:e.data.uid};R.send(t,e.source)}})}if(e.data.actionName==="typo3:foreignRelation:delete"){if(e.data.objectGroup!==this.objectGroup)return;const t=e.data.directRemoval||!1,o=[e.data.objectGroup,e.data.uid].join("-");this.deleteRecord(o,t)}}}get objectGroup(){return this.dataset.objectGroup}get formField(){return this.dataset.formField}get expandSingle(){return this.dataset.expandSingle==="true"}get sortable(){return this.dataset.sortable==="true"}get min(){return parseInt(this.dataset.min,10)||0}get max(){return parseInt(this.dataset.max,10)||0}get type(){const e=this.dataset.type;return e==="file"||e==="language"?e:"record"}get endpoints(){switch(this.type){case"file":return{create:"file_reference_create",details:"file_reference_details",synchronizelocalize:"file_reference_synchronizelocalize",expandcollapse:"file_reference_expandcollapse"};case"language":return{create:"site_configuration_inline_create",details:"site_configuration_inline_details",synchronizelocalize:null,expandcollapse:null};default:return{create:"record_inline_create",details:"record_inline_details",synchronizelocalize:"record_inline_synchronizelocalize",expandcollapse:"record_inline_expandcollapse"}}}static getValuesFromHashMap(e){return Object.keys(e).map(t=>e[t])}static selectOptionValueExists(e,t){return e.querySelector(p`option[value="${t}"]`)!==null}static removeSelectOptionByValue(e,t){const o=e.querySelector(p`option[value="${t}"]`);o!==null&&o.remove()}static reAddSelectOption(e,t,o){if(d.selectOptionValueExists(e,t))return;const i=e.querySelectorAll("option");let n=-1;for(const a of Object.keys(o.possible)){if(a===t)break;for(let s=0;s{this.updateSorting()}})}}registerToggle(){new h("show.bs.collapse",e=>{const o=e.target.parentElement;if(o.closest("typo3-formengine-container-inline")!==this)return;const i=o.dataset.objectId;if(o.classList.contains(q.notLoaded)){e.preventDefault(),this.loadRecordDetails(i);return}this.expandSingle&&this.collapseAllRecords(o.dataset.objectUid)}).bindTo(this),new h("shown.bs.collapse",e=>{const o=e.target.closest("[data-object-id]");o===null||o.closest("typo3-formengine-container-inline")!==this||this.persistExpandCollapseState(o.dataset.objectId,!0)}).bindTo(this),new h("hidden.bs.collapse",e=>{const o=e.target.closest("[data-object-id]");o===null||o.closest("typo3-formengine-container-inline")!==this||this.persistExpandCollapseState(o.dataset.objectId,!1)}).bindTo(this)}registerSort(){new h("click",(e,t)=>{e.preventDefault(),e.stopImmediatePropagation(),this.changeSortingByButton(t.closest("[data-object-id]").dataset.objectId,t.dataset.direction)}).delegateTo(this,f.controlSectionSelector+' [data-action="sort"]')}registerCreateRecordButton(){new h("click",(e,t)=>{if(e.preventDefault(),e.stopImmediatePropagation(),this.isBelowMax()){let o=this.objectGroup;typeof t.dataset.recordUid<"u"&&(o+=m.structureSeparator+t.dataset.recordUid),this.importRecord([o,this.querySelector(f.createNewRecordBySelectorSelector)?.value],t.dataset.recordUid??null)}}).delegateTo(this,f.createNewRecordButtonSelector)}registerCreateRecordBySelector(){new h("change",(e,t)=>{e.preventDefault(),e.stopImmediatePropagation();const o=t,i=o.options[o.selectedIndex].getAttribute("value");this.importRecord([this.objectGroup,i])}).delegateTo(this,f.createNewRecordBySelectorSelector)}registerCreateRecordByPresetSelector(){new h("change",e=>{e.preventDefault(),e.stopImmediatePropagation();const t=this.querySelector(f.createNewRecordByPresetSelector),o=t?.value;o!==""&&(t.value="",this.importRecord([this.objectGroup,"",o]))}).delegateTo(this,f.createNewRecordByPresetSelector)}createRecord(e,t,o=null,i=null){let n=this.objectGroup;o!==null&&(n+=m.structureSeparator+o),o!==null?(this.getRecordContainer(n).insertAdjacentHTML("afterend",t),this.memorizeAddRecord(e,o,i)):(this.recordsContainer.insertAdjacentHTML("beforeend",t),this.memorizeAddRecord(e,null,i))}async importRecord(e,t){return this.ajaxDispatcher.send(this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint(this.endpoints.create)),e).then(async o=>{this.isBelowMax()&&this.createRecord(o.compilerInput.uid,o.data,typeof t<"u"?t:null,typeof o.compilerInput.childChildUid<"u"?o.compilerInput.childChildUid:null)})}registerEnableDisableButton(){new h("click",(e,t)=>{e.preventDefault(),e.stopImmediatePropagation();const o=t.closest("[data-object-id]").dataset.objectId,i=this.getRecordContainer(o),n=p`data${i.dataset.fieldName}[${t.dataset.hiddenField}]`,r=this.querySelector('[data-formengine-input-name="'+n+'"'),a=this.querySelector('[name="'+n+'"');if(r!==null&&a!==null){r.checked=!r.checked;const y=r.checked!==(r.dataset.invertStateDisplay==="true");a.value=y?"1":"0",j.markFieldAsChanged(r)}const s="panel-hidden",l=i.classList.contains(s);let u;l?(u="actions-edit-hide",i.classList.remove(s)):(u="actions-edit-unhide",i.classList.add(s)),S.getIcon(u,S.sizes.small).then(y=>{t.replaceChild(document.createRange().createContextualFragment(y),t.querySelector(".t3js-icon"))})}).delegateTo(this,f.enableDisableRecordButtonSelector)}registerInfoButton(){new h("click",function(e){e.preventDefault(),e.stopImmediatePropagation(),B.showItem(this.dataset.infoTable,this.dataset.infoUid)}).delegateTo(this,f.infoWindowButton)}registerDeleteButton(){new h("click",(e,t)=>{e.preventDefault(),e.stopImmediatePropagation();const o=C.get("label.confirm.delete_record.title"),i=C.get("label.confirm.delete_record.content",[t.dataset.recordInfo]),n=O.confirm(o,i,U.warning,[{text:C.get("buttons.confirm.delete_record.no"),active:!0,btnClass:"btn-default",name:"no"},{text:C.get("buttons.confirm.delete_record.yes"),btnClass:"btn-warning",name:"yes"}]);n.addEventListener("button.clicked",r=>{if(r.target.name==="yes"){const a=t.closest("[data-object-id]").dataset.objectId;this.deleteRecord(a)}n.hideModal()})}).delegateTo(this,f.deleteRecordButtonSelector)}registerSynchronizeLocalize(){new h("click",(e,t)=>{if(e.preventDefault(),e.stopImmediatePropagation(),this.endpoints.synchronizelocalize===null){console.error(`Synchronize/localize is not supported for type "${this.type}"`);return}this.ajaxDispatcher.send(this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint(this.endpoints.synchronizelocalize)),[this.objectGroup,t.dataset.type]).then(async o=>{this.recordsContainer.insertAdjacentHTML("beforeend",o.data);const i=this.objectGroup+m.structureSeparator;for(const n of o.compilerInput.delete)this.deleteRecord(i+n,!0);for(const n of Object.values(o.compilerInput.localize)){if(typeof n.remove<"u"){const r=this.getRecordContainer(i+n.remove);r.parentElement.removeChild(r)}this.memorizeAddRecord(n.uid,null,n.selectedValue)}})}).delegateTo(this,f.synchronizeLocalizeRecordButtonSelector)}registerUniqueSelectFieldChanged(){new h("change",(e,t)=>{e.preventDefault(),e.stopImmediatePropagation();const o=t.closest("[data-object-id]");if(o!==null){const i=o.dataset.objectId,n=o.dataset.objectUid;this.handleChangedField(t,i);const r=this.getFormFieldForElements();if(r===null)return;this.updateUnique(t,r,n)}}).delegateTo(this,f.uniqueValueSelectors)}registerRevertUniquenessAction(){new h("click",(e,t)=>{e.preventDefault(),e.stopImmediatePropagation(),this.revertUnique(t.dataset.uid)}).delegateTo(this,f.revertUniqueness)}loadRecordDetails(e){const t=this.getCollapseContent(e),o=this.getRecordContainer(e),i=typeof this.requestQueue[e]<"u",n=this.getProgress(e);if(i)this.requestQueue[e].abort(),delete this.requestQueue[e],delete this.progressQueue[e],n.done();else{const r=this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint(this.endpoints.details));this.ajaxDispatcher.send(r,[e]).then(async s=>{if(delete this.requestQueue[e],delete this.progressQueue[e],o.classList.remove(q.notLoaded),t.innerHTML=s.data,n.done(),this.expandSingle&&this.collapseAllRecords(o.dataset.objectUid),x.getOrCreateInstance(t).show(),j.reinitialize(),v.initializeInputFields(),v.validate(this),this.hasObjectGroupDefinedUniqueConstraints()){const l=this.getRecordContainer(e);this.removeUsed(l)}}).catch(s=>{if(!(s instanceof DOMException&&s.name==="AbortError"))throw s}),this.requestQueue[e]=r,n.start()}}persistExpandCollapseState(e,t){if(this.endpoints.expandcollapse===null)return;if(this.isNewRecord(e)){this.updateExpandedCollapsedStateLocally(e,t);return}const o=this.getRecordContainer(e),i=t?o.dataset.objectUid:"",n=t?"":o.dataset.objectUid;this.ajaxDispatcher.send(this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint(this.endpoints.expandcollapse)),[e,i,n])}memorizeAddRecord(e,t=null,o=null){const i=this.getFormFieldForElements();if(i===null)return;let n=g.trimExplode(",",i.value);if(t){const r=[];for(let a=0;a-1&&(o.splice(i,1),t.value=o.join(","),j.markFieldAsChanged(t),document.dispatchEvent(new Event("change")),this.redrawSortingButtons(this.objectGroup,o)),o}changeSortingByButton(e,t){const o=this.getRecordContainer(e),i=o.dataset.objectUid,n=this.recordsContainer,r=Array.from(n.children).map(l=>l.dataset.objectUid),a=r.indexOf(i);let s=!1;if(t===b.UP&&a>0?(r[a]=r[a-1],r[a-1]=i,s=!0):t===b.DOWN&&ai.dataset.objectUid);e.value=o.join(","),j.markFieldAsChanged(e),document.dispatchEvent(new Event("change")),this.redrawSortingButtons(this.objectGroup,o)}deleteRecord(e,t=!1){const o=this.getRecordContainer(e),i=o.dataset.objectUid;if(o.classList.add("t3js-inline-record-deleted"),!this.isNewRecord(e)&&!t){const n=this.querySelector(p`[name="cmd${o.dataset.fieldName}[delete]"]`);n.removeAttribute("disabled"),o.parentElement.insertAdjacentElement("afterbegin",n)}new h("transitionend",()=>{o.remove(),v.validate(this)}).bindTo(o),this.revertUnique(i),this.memorizeRemoveRecord(i),o.classList.add("form-irre-object--deleted"),this.isBelowMax()&&this.toggleContainerControls(!0)}toggleContainerControls(e){this.querySelectorAll(":scope > "+f.controlContainer).forEach(o=>{o.querySelectorAll("button, a").forEach(n=>{n.hidden=!e})})}getProgress(e){let t;if(typeof this.progressQueue[e]<"u")t=this.progressQueue[e];else{t=document.createElement("typo3-backend-progress-bar");const o=this.getRecordContainer(e);o.insertBefore(t,o.firstChild),this.progressQueue[e]=t}return t}collapseAllRecords(e){const t=this.getFormFieldForElements();if(t!==null){const o=g.trimExplode(",",t.value);for(const i of o){if(i===e)continue;const n=this.objectGroup+m.structureSeparator+i;this.getCollapseContent(n)?.classList.contains("show")&&this.collapseElement(n)}}}getFormFieldForElements(){return this.querySelector(p`[name="${this.formField}"]`)}redrawSortingButtons(e,t=[]){if(t.length===0){const o=this.getFormFieldForElements();o!==null&&(t=g.trimExplode(",",o.value))}t.length!==0&&t.forEach((o,i)=>{const n=this.getRecordContainer(e+m.structureSeparator+o);if(n===null)return;const r=n.querySelector(p`[data-action="sort"][data-direction="${b.UP}"]`);if(r!==null){let s="actions-move-up";i===0?(r.classList.add("disabled"),s="empty-empty"):r.classList.remove("disabled"),S.getIcon(s,S.sizes.small).then(l=>{r.replaceChild(document.createRange().createContextualFragment(l),r.querySelector(".t3js-icon"))})}const a=n.querySelector(p`[data-action="sort"][data-direction="${b.DOWN}"]`);if(a!==null){let s="actions-move-down";i===t.length-1?(a.classList.add("disabled"),s="empty-empty"):a.classList.remove("disabled"),S.getIcon(s,S.sizes.small).then(l=>{a.replaceChild(document.createRange().createContextualFragment(l),a.querySelector(".t3js-icon"))})}})}isBelowMax(){const e=this.getFormFieldForElements();if(e===null)return!0;const t=g.trimExplode(",",e.value);if(this.max>0&&t.length>=this.max)return!1;if(this.hasObjectGroupDefinedUniqueConstraints()){const o=TYPO3.settings.FormEngineInline.unique[this.objectGroup];if(o.used.length>=o.max&&o.max>=0)return!1}return!0}isUniqueElementUsed(e,t){if(!this.hasObjectGroupDefinedUniqueConstraints())return!1;const o=TYPO3.settings.FormEngineInline.unique[this.objectGroup],i=d.getValuesFromHashMap(o.used);if(o.type==="select"&&i.indexOf(e)!==-1)return!0;if(o.type==="groupdb"){for(let n=i.length-1;n>=0;n--)if(i[n].table===t&&i[n].uid===e)return!0}return!1}removeUsed(e){if(!this.hasObjectGroupDefinedUniqueConstraints())return;const t=TYPO3.settings.FormEngineInline.unique[this.objectGroup];if(t.type!=="select")return;const o=e.querySelector('[name="data['+t.table+"]["+e.dataset.objectUid+"]["+t.field+']"]'),i=d.getValuesFromHashMap(t.used);if(o!==null){const n=o.options[o.selectedIndex].value;for(const r of i)r!==n&&d.removeSelectOptionByValue(o,r)}}setUnique(e,t){if(!this.hasObjectGroupDefinedUniqueConstraints())return;const o=this.querySelector(p`[id="${this.objectGroup}_selector"]`),i=TYPO3.settings.FormEngineInline.unique[this.objectGroup];if(i.type==="select"){if(!(i.selector&&i.max===-1)){const n=this.getFormFieldForElements(),r=this.objectGroup+m.structureSeparator+e;let s=this.getRecordContainer(r).querySelector('[name="data['+i.table+"]["+e+"]["+i.field+']"]');const l=d.getValuesFromHashMap(i.used);if(o!==null){if(s!==null){for(const u of l)d.removeSelectOptionByValue(s,u);i.selector||(t=s.options[0].value,s.options[0].selected=!0,this.updateUnique(s,n,e),this.handleChangedField(s,this.objectGroup+"["+e+"]"))}for(const u of l)d.removeSelectOptionByValue(s,u);typeof i.used.length<"u"&&(i.used={}),i.used[e]={table:i.elTable,uid:t}}if(n!==null&&d.selectOptionValueExists(o,t)){const u=g.trimExplode(",",n.value);for(const y of u)s=this.querySelector('[name="data['+i.table+"]["+y+"]["+i.field+']"]'),s!==null&&y!==e&&d.removeSelectOptionByValue(s,t)}}}else i.type==="groupdb"&&(i.used[e]={table:i.elTable,uid:t});i.selector==="select"&&d.selectOptionValueExists(o,t)&&(d.removeSelectOptionByValue(o,t),i.used[e]={table:i.elTable,uid:t})}updateUnique(e,t,o){if(!this.hasObjectGroupDefinedUniqueConstraints())return;const i=TYPO3.settings.FormEngineInline.unique[this.objectGroup],n=i.used[o];if(i.selector==="select"){const s=this.querySelector(p`[id="${this.objectGroup}_selector"]`);d.removeSelectOptionByValue(s,e.value),typeof n<"u"&&d.reAddSelectOption(s,n,i)}if(i.selector&&i.max===-1||!i||t===null)return;const r=g.trimExplode(",",t.value);let a;for(const s of r)a=this.querySelector('[name="data['+i.table+"]["+s+"]["+i.field+']"]'),a!==null&&a!==e&&(d.removeSelectOptionByValue(a,e.value),typeof n<"u"&&d.reAddSelectOption(a,n,i));i.used[o]=e.value}revertUnique(e){if(!this.hasObjectGroupDefinedUniqueConstraints())return;const t=TYPO3.settings.FormEngineInline.unique[this.objectGroup],o=this.objectGroup+m.structureSeparator+e,i=this.getRecordContainer(o),n=i.querySelector('[name="data['+t.table+"]["+i.dataset.objectUid+"]["+t.field+']"]');if(t.type==="select"){let r;if(n!==null)r=n.value;else if(i.dataset.tableUniqueOriginalValue!=="")r=i.dataset.tableUniqueOriginalValue;else return;if(t.selector==="select"&&!isNaN(parseInt(r,10))){const u=this.querySelector(p`[id="${this.objectGroup}_selector"]`);d.reAddSelectOption(u,r,t)}if(t.selector&&t.max===-1)return;const a=this.getFormFieldForElements();if(a===null)return;const s=g.trimExplode(",",a.value);let l;for(let u=0;u{this.registerKeyboardEventHandler(o);const t=o.closest(".form-wizards-wrap").querySelector(".form-wizards-item-aside");t!==null&&t.addEventListener("click",r=>{const n=r.target.closest(".t3js-btn-option");if(n===null)return;r.preventDefault();const e=n.dataset.fieldname,l=a.getFieldElement(e),s=a.getFieldElement(e,"_avail");l===null||s===null||(n.classList.contains("t3js-btn-moveoption-top")?i.moveOptionToTop(o):n.classList.contains("t3js-btn-moveoption-up")?i.moveOptionUp(o):n.classList.contains("t3js-btn-moveoption-down")?i.moveOptionDown(o):n.classList.contains("t3js-btn-moveoption-bottom")?i.moveOptionToBottom(o):n.classList.contains("t3js-btn-removeoption")&&i.removeOption(o,s),a.updateHiddenFieldValueFromSelect(o,l),a.markFieldAsChanged(s),c.validateField(s))})},this.registerKeyboardEventHandler=o=>{const t=o.dataset.formengineInputName,r=a.getFieldElement(t),n=a.getFieldElement(t,"_avail");r===null||n===null||new d("keydown",e=>{(e.code==="Delete"||e.code==="Backspace")&&(e.preventDefault(),i.removeOption(o,n)),e.code==="ArrowUp"&&e.altKey&&(e.preventDefault(),i.moveOptionUp(o)),e.code==="ArrowDown"&&e.altKey&&(e.preventDefault(),i.moveOptionDown(o)),e.code==="ArrowUp"&&e.altKey&&e.shiftKey&&(e.preventDefault(),i.moveOptionToTop(o)),e.code==="ArrowDown"&&e.altKey&&e.shiftKey&&(e.preventDefault(),i.moveOptionToBottom(o)),e.defaultPrevented&&(a.updateHiddenFieldValueFromSelect(o,r),a.markFieldAsChanged(n),c.validateField(n))}).bindTo(o)}}static moveOptionToTop(o){Array.from(o.querySelectorAll(":checked")).reverse().forEach(t=>{o.insertBefore(t,o.firstElementChild)})}static moveOptionToBottom(o){o.querySelectorAll(":checked").forEach(t=>{o.insertBefore(t,null)})}static moveOptionUp(o){const t=Array.from(o.children),r=Array.from(o.querySelectorAll(":checked"));for(const n of r){if(t.indexOf(n)===0&&n.previousElementSibling===null)break;o.insertBefore(n,n.previousElementSibling)}}static moveOptionDown(o){const t=Array.from(o.children).reverse(),r=Array.from(o.querySelectorAll(":checked")).reverse();for(const n of r){if(t.indexOf(n)===0&&n.nextElementSibling===null)break;o.insertBefore(n,n.nextElementSibling.nextElementSibling)}}static removeOption(o,t){const r=o.selectedIndex;o.querySelectorAll(":checked").forEach(n=>{const e=t.querySelector(p`option[value="${n.value}"]`);e!==null&&(e.classList.remove("hidden"),e.disabled=!1,a.enableOptGroup(e)),o.removeChild(n)}),o.selectedIndex=r>0?r-1:0}}export{i as AbstractSortableSelectItems}; diff --git a/Resources/Public/JavaScript/form-engine/element/category-element.js b/Resources/Public/JavaScript/form-engine/element/category-element.js new file mode 100644 index 0000000..440fae0 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/category-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import d from"@typo3/core/document-service.js";import{selector as a}from"@typo3/core/literals.js";import"@typo3/backend/form-engine/element/select-tree.js";import"@typo3/backend/form-engine/element/select-tree-toolbar.js";import"@typo3/backend/element/icon-element.js";class s extends HTMLElement{constructor(){super(...arguments),this.recordField=null,this.treeWrapper=null,this.tree=null,this.selectNode=e=>{const t=e.detail.node;this.updateAncestorsIndeterminateState(t),this.calculateIndeterminate(this.tree.nodes),this.saveCheckboxes(),this.tree.setup.input.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0}))},this.loadDataAfter=e=>{this.tree.nodes=e.detail.nodes.map(t=>(t.__indeterminate=!1,t)),this.calculateIndeterminate(this.tree.nodes)},this.saveCheckboxes=()=>{this.recordField.value=this.tree.getSelectedNodes().map(e=>e.identifier).join(",")}}async connectedCallback(){this.tree===null&&(await d.ready(),this.recordField=this.querySelector(a`#${this.getAttribute("recordFieldId")||""}`),this.treeWrapper=this.querySelector(a`#${this.getAttribute("treeWrapperId")||""}`),!(!this.recordField||!this.treeWrapper)&&(this.tree=document.createElement("typo3-backend-form-selecttree"),this.tree.setup={id:this.treeWrapper.id,dataUrl:this.generateDataUrl(),readOnlyMode:this.recordField.dataset.readOnly,input:this.recordField,exclusiveNodesIdentifiers:this.recordField.dataset.treeExclusiveKeys,validation:JSON.parse(this.recordField.dataset.formengineValidationRules)[0],expandUpToLevel:this.recordField.dataset.treeExpandUpToLevel,unselectableElements:[]},this.treeWrapper.append(this.tree),this.registerTreeEventListeners()))}registerTreeEventListeners(){this.tree.addEventListener("typo3:tree:nodes-prepared",this.loadDataAfter),this.tree.addEventListener("typo3:tree:node-selected",this.selectNode),this.tree.addEventListener("tree:initialized",()=>{if(this.recordField.dataset.treeShowToolbar){const e=document.createElement("typo3-backend-form-selecttree-toolbar");e.tree=this.tree,this.tree.prepend(e)}})}generateDataUrl(){return TYPO3.settings.ajaxUrls.record_tree_data+"&"+new URLSearchParams({uid:this.recordField.dataset.uid,command:this.recordField.dataset.command,tableName:this.recordField.dataset.tablename,fieldName:this.recordField.dataset.fieldname,defaultValues:this.recordField.dataset.defaultvalues,overrideValues:this.recordField.dataset.overridevalues,recordTypeValue:this.recordField.dataset.recordtypevalue,flexFormSheetName:this.recordField.dataset.flexformsheetname,flexFormFieldName:this.recordField.dataset.flexformfieldname,flexFormContainerName:this.recordField.dataset.flexformcontainername,dataStructureIdentifier:this.recordField.dataset.datastructureidentifier,flexFormContainerFieldName:this.recordField.dataset.flexformcontainerfieldname,flexFormContainerIdentifier:this.recordField.dataset.flexformcontaineridentifier,flexFormSectionContainerIsNew:this.recordField.dataset.flexformsectioncontainerisnew}).toString()}updateAncestorsIndeterminateState(e){let t=!1;e.__treeParents.forEach(i=>{const r=this.tree.getNodeByTreeIdentifier(i);r.__indeterminate=e.checked||e.__indeterminate||t,t=r.checked||r.__indeterminate||e.checked||e.__indeterminate})}calculateIndeterminate(e){e.forEach(t=>{(t.checked||t.__indeterminate)&&t.__treeParents&&t.__treeParents.length>0&&t.__treeParents.forEach(i=>{const r=this.tree.getNodeByTreeIdentifier(i);r.__indeterminate=!0})})}}window.customElements.define("typo3-formengine-element-category",s); diff --git a/Resources/Public/JavaScript/form-engine/element/color-element.js b/Resources/Public/JavaScript/form-engine/element/color-element.js new file mode 100644 index 0000000..6b44e42 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/color-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import l from"@typo3/core/document-service.js";import n from"@typo3/core/event/regular-event.js";import i from"@typo3/backend/form-engine-validation.js";import{selector as r}from"@typo3/core/literals.js";import"@typo3/backend/color-picker.js";import o from"@typo3/backend/form-engine.js";class m extends HTMLElement{constructor(){super(...arguments),this.element=null}async connectedCallback(){if(this.element!==null)return;const e=this.getAttribute("recordFieldId");e!==null&&(await l.ready(),this.element=this.querySelector(r`#${e}`),this.element&&this.registerEventHandler())}registerEventHandler(){const e=document.querySelector(r`input[name="${this.element.dataset.formengineInputName}"]`);new n("blur",t=>{e.value=t.target.value,this.handleEvent(t)}).bindTo(this.element),new n("formengine.cp.change",t=>{this.handleEvent(t)}).bindTo(this.element)}handleEvent(e){i.validateField(e.target),o.markFieldAsChanged(e.target),document.querySelectorAll(".module-docheader-bar .btn").forEach(t=>{t.classList.remove("disabled"),t.disabled=!1})}}window.customElements.define("typo3-formengine-element-color",m); diff --git a/Resources/Public/JavaScript/form-engine/element/datetime-element.js b/Resources/Public/JavaScript/form-engine/element/datetime-element.js new file mode 100644 index 0000000..2a963bd --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/datetime-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import n from"@typo3/core/document-service.js";import i from"@typo3/backend/form-engine-validation.js";import r from"@typo3/core/event/regular-event.js";import l from"@typo3/backend/form-engine.js";class m extends HTMLElement{constructor(){super(...arguments),this.element=null}async connectedCallback(){this.element===null&&(await n.ready(),this.element=document.getElementById(this.getAttribute("recordFieldId")||""),this.element&&(this.registerEventHandler(),import("@typo3/backend/date-time-picker.js").then(({default:e})=>{e.initialize(this.element,this)})))}registerEventHandler(){new r("formengine.dp.change",e=>{i.validateField(e.target),l.markFieldAsChanged(e.target),document.querySelectorAll(".module-docheader-bar .btn").forEach(t=>{t.classList.remove("disabled"),t.disabled=!1})}).bindTo(this.element)}}window.customElements.define("typo3-formengine-element-datetime",m); diff --git a/Resources/Public/JavaScript/form-engine/element/extra/char-counter.js b/Resources/Public/JavaScript/form-engine/element/extra/char-counter.js new file mode 100644 index 0000000..9260c1e --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/extra/char-counter.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as c,state as m,customElement as u}from"lit/decorators.js";import{LitElement as g,html as d}from"lit";import f from"~labels/core.core";var o=function(a,e,t,n){var i=arguments.length,r=i<3?e:n===null?n=Object.getOwnPropertyDescriptor(e,t):n,h;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")r=Reflect.decorate(a,e,t,n);else for(var l=a.length-1;l>=0;l--)(h=a[l])&&(r=(i<3?h(r):i>3?h(e,t,r):h(e,t))||r);return i>3&&r&&Object.defineProperty(e,t,r),r};let s=class extends g{constructor(){super(...arguments),this.remainingCharacters=0,this.targetElement=null,this.threshold=15,this.onInput=e=>{this.determineRemainingCharacters(e.target)},this.onFocus=e=>{this.determineRemainingCharacters(e.target),this.hidden=!1},this.onBlur=()=>{this.hidden=!0}}connectedCallback(){super.connectedCallback(),this.registerCallbacks(),this.hidden=!0}disconnectedCallback(){super.disconnectedCallback(),this.removeCallbacks()}createRenderRoot(){return this}updated(e){e.has("target")&&(this.removeCallbacks(),this.targetElement=document.querySelector(this.target),this.registerCallbacks())}render(){return d` ${f.get("labels.remainingCharacters",{0:this.remainingCharacters.toString(10)})} `}registerCallbacks(){this.targetElement!==null&&(this.targetElement.addEventListener("input",this.onInput),this.targetElement.addEventListener("focus",this.onFocus),this.targetElement.addEventListener("blur",this.onBlur))}removeCallbacks(){this.targetElement!==null&&(this.targetElement.removeEventListener("input",this.onInput),this.targetElement.removeEventListener("focus",this.onFocus),this.targetElement.removeEventListener("blur",this.onBlur))}determineRemainingCharacters(e){const t=e.value,n=t.length,i=(t.match(/\n/g)||[]).length;this.remainingCharacters=this.targetElement.maxLength-n-i}determineCounterClass(){return this.remainingCharacters{this.filter(e.target.value)}).delegateTo(t,l.filterTextFieldSelector),new r("search",e=>{this.filter(e.target.value)}).delegateTo(t,l.filterTextFieldSelector),new r("change",e=>{this.filter(e.target.value)}).delegateTo(t,l.filterSelectFieldSelector))}filter(t){this.availableOptions===null&&(this.availableOptions=this.selectElement.querySelectorAll("option"));const e=t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),a=new RegExp(e,"i");this.availableOptions.forEach(n=>{n.hidden=t.length>0&&n.textContent.match(a)===null,s.toggleOptGroup(n)})}}export{s as default}; diff --git a/Resources/Public/JavaScript/form-engine/element/folder-element.js b/Resources/Public/JavaScript/form-engine/element/folder-element.js new file mode 100644 index 0000000..c41ba51 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/folder-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import t from"@typo3/core/document-service.js";import{AbstractSortableSelectItems as l}from"@typo3/backend/form-engine/element/abstract-sortable-select-items.js";import{selector as n}from"@typo3/core/literals.js";class o extends l{registerEventHandler(e){this.registerSortableEventHandler(e)}}class s extends HTMLElement{constructor(){super(...arguments),this.recordField=null}async connectedCallback(){if(this.recordField!==null)return;const e=this.getAttribute("recordFieldId");e!==null&&(await t.ready(),this.recordField=this.querySelector(n`#${e}`),this.recordField&&this.registerEventHandler())}registerEventHandler(){new o().registerEventHandler(this.recordField)}}window.customElements.define("typo3-formengine-element-folder",s); diff --git a/Resources/Public/JavaScript/form-engine/element/group-element.js b/Resources/Public/JavaScript/form-engine/element/group-element.js new file mode 100644 index 0000000..7647e39 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/group-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{AbstractSortableSelectItems as t}from"@typo3/backend/form-engine/element/abstract-sortable-select-items.js";import r from"@typo3/core/document-service.js";import s from"@typo3/backend/form-engine-suggest.js";class n extends t{constructor(e){super(),this.element=null,r.ready().then(()=>{this.element=document.getElementById(e),this.element!==null&&(this.registerEventHandler(),this.registerSuggest())})}registerEventHandler(){this.registerSortableEventHandler(this.element)}registerSuggest(){let e;(e=this.element.closest(".t3js-formengine-field-item").querySelector(".t3-form-suggest"))!==null&&new s(e)}}export{n as default}; diff --git a/Resources/Public/JavaScript/form-engine/element/json-element.js b/Resources/Public/JavaScript/form-engine/element/json-element.js new file mode 100644 index 0000000..2154941 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/json-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import e from"@typo3/core/document-service.js";import{Resizable as t}from"@typo3/backend/form-engine/element/modifier/resizable.js";import{Tabbable as n}from"@typo3/backend/form-engine/element/modifier/tabbable.js";class l extends HTMLElement{constructor(){super(...arguments),this.element=null}async connectedCallback(){this.element===null&&(await e.ready(),this.element=document.getElementById(this.getAttribute("recordFieldId")||""),this.element&&(t.enable(this.element),n.enable(this.element)))}}window.customElements.define("typo3-formengine-element-json",l); diff --git a/Resources/Public/JavaScript/form-engine/element/link-element.js b/Resources/Public/JavaScript/form-engine/element/link-element.js new file mode 100644 index 0000000..ed33e80 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/link-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{selector as r}from"@typo3/core/literals.js";var t;(function(i){i.toggleSelector=".t3js-form-field-link-explanation-toggle",i.inputFieldSelector=".t3js-form-field-link-input",i.explanationSelector=".t3js-form-field-link-explanation",i.iconSelector=".t3js-form-field-link-icon",i.containerSelector=".t3js-form-field-link"})(t||(t={}));class a extends HTMLElement{constructor(){super(),this.addEventListener("click",e=>this.handleClick(e)),this.addEventListener("change",e=>this.handleChange(e))}get element(){const e=this.getAttribute("recordFieldId");if(e===null)throw new Error("Missing recordFieldId attribute on ");const n=this.querySelector(r`#${e}`);if(n===null)throw new Error(`recordFieldId #${e} not found in `);return n}get container(){return this.element.closest(t.containerSelector)}get toggleSelector(){return this.container.querySelector(t.toggleSelector)}get explanationField(){return this.container.querySelector(t.explanationSelector)}get icon(){return this.container.querySelector(t.iconSelector)}handleClick(e){e.target.closest(t.toggleSelector)!==null&&(e.preventDefault(),this.explanationField.hasAttribute("hidden")?this.showExplanation():this.hideExplanation())}handleChange(e){e.target.closest(t.inputFieldSelector)!==null&&(!this.explanationField.hasAttribute("hidden")&&this.hideExplanation(),this.disableToggle(),this.clearIcon())}showExplanation(){this.explanationField.removeAttribute("hidden"),this.element.setAttribute("hidden","")}hideExplanation(){this.explanationField.setAttribute("hidden",""),this.element.removeAttribute("hidden")}disableToggle(){this.toggleSelector.setAttribute("disabled","")}clearIcon(){this.icon.replaceChildren()}}window.customElements.define("typo3-formengine-element-link",a); diff --git a/Resources/Public/JavaScript/form-engine/element/mfa-info-element.js b/Resources/Public/JavaScript/form-engine/element/mfa-info-element.js new file mode 100644 index 0000000..32579f7 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/mfa-info-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import l from"@typo3/core/ajax/ajax-request.js";import u from"@typo3/core/document-service.js";import o from"@typo3/core/event/regular-event.js";import d from"@typo3/backend/notification.js";import c from"@typo3/backend/modal.js";import{SeverityEnum as f}from"@typo3/backend/enum/severity.js";import{selector as v}from"@typo3/core/literals.js";import{sudoModeInterceptor as m}from"@typo3/backend/security/sudo-mode-interceptor.js";var a;(function(s){s.deactivteProviderButton=".t3js-deactivate-provider-button",s.deactivteMfaButton=".t3js-deactivate-mfa-button",s.providerslist=".t3js-mfa-active-providers-list",s.mfaStatusLabel=".t3js-mfa-status-label"})(a||(a={}));class h{constructor(t,e){this.options=null,this.fullElement=null,this.deactivteProviderButtons=null,this.deactivteMfaButton=null,this.providersList=null,this.mfaStatusLabel=null,this.request=null,this.options=e,u.ready().then(i=>{this.fullElement=i.querySelector(t),this.deactivteProviderButtons=this.fullElement.querySelectorAll(a.deactivteProviderButton),this.deactivteMfaButton=this.fullElement.querySelector(a.deactivteMfaButton),this.providersList=this.fullElement.querySelector(a.providerslist),this.mfaStatusLabel=this.fullElement.parentElement.querySelector(a.mfaStatusLabel),this.registerEvents()})}registerEvents(){new o("click",t=>{t.preventDefault(),this.prepareDeactivateRequest(this.deactivteMfaButton)}).bindTo(this.deactivteMfaButton),this.deactivteProviderButtons.forEach(t=>{new o("click",e=>{e.preventDefault(),this.prepareDeactivateRequest(t)}).bindTo(t)})}prepareDeactivateRequest(t){const e=c.show(t.dataset.confirmationTitle||t.getAttribute("title")||"Deactivate provider(s)",t.dataset.confirmationContent||"Are you sure you want to continue? This action cannot be undone and will be applied immediately!",f.warning,[{text:t.dataset.confirmationCancelText||"Cancel",active:!0,btnClass:"btn-default",name:"cancel"},{text:t.dataset.confirmationDeactivateText||"Deactivate",btnClass:"btn-warning",name:"deactivate",trigger:()=>{this.sendDeactivateRequest(t.dataset.provider)}}]);e.addEventListener("button.clicked",()=>{e.hideModal()})}sendDeactivateRequest(t){this.request instanceof l&&this.request.abort(),this.request=new l(TYPO3.settings.ajaxUrls.mfa).addMiddleware(m),this.request.post({action:"deactivate",provider:t,userId:this.options.userId,tableName:this.options.tableName}).then(async e=>{const i=await e.resolve();if(i.status.length>0&&i.status.forEach(r=>{i.success?d.success(r.title,r.message):d.error(r.title,r.message)}),!i.success)return;if(t===void 0||i.remaining===0){this.deactivateMfa();return}if(this.providersList===null)return;const n=this.providersList.querySelector(v`li#provider-${t}`);if(n===null)return;n.remove(),this.providersList.querySelectorAll("li").length===0&&this.deactivateMfa()}).finally(()=>{this.request=null})}deactivateMfa(){this.deactivteMfaButton.classList.add("disabled"),this.deactivteMfaButton.setAttribute("disabled","disabled"),this.providersList!==null&&this.providersList.remove(),this.mfaStatusLabel!==null&&(this.mfaStatusLabel.innerText=this.mfaStatusLabel.dataset.alternativeLabel,this.mfaStatusLabel.classList.remove("badge-success"),this.mfaStatusLabel.classList.add("badge-danger"))}}export{h as default}; diff --git a/Resources/Public/JavaScript/form-engine/element/modifier/resizable.js b/Resources/Public/JavaScript/form-engine/element/modifier/resizable.js new file mode 100644 index 0000000..f83e45c --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/modifier/resizable.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import a from"autosize";class s{static enable(e){a(e)}}export{s as Resizable}; diff --git a/Resources/Public/JavaScript/form-engine/element/modifier/tabbable.js b/Resources/Public/JavaScript/form-engine/element/modifier/tabbable.js new file mode 100644 index 0000000..a4e1312 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/modifier/tabbable.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class a{static enable(s){s.classList.contains("t3js-enable-tab")&&import("taboverride").then(({default:t})=>{t.set(s)})}}export{a as Tabbable}; diff --git a/Resources/Public/JavaScript/form-engine/element/online-media-form-element.js b/Resources/Public/JavaScript/form-engine/element/online-media-form-element.js new file mode 100644 index 0000000..e5c8601 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/online-media-form-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as d,customElement as c}from"lit/decorators.js";import{LitElement as f,html as m}from"lit";var s=function(i,e,n,o){var l=arguments.length,t=l<3?e:o===null?o=Object.getOwnPropertyDescriptor(e,n):o,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(i,e,n,o);else for(var p=i.length-1;p>=0;p--)(a=i[p])&&(t=(l<3?a(t):l>3?a(e,n,t):a(e,n))||t);return l>3&&t&&Object.defineProperty(e,n,t),t};let r=class extends f{createRenderRoot(){return this}render(){return m`
    ${this.allowedExtensionsHelpText}
      ${this.allowedExtensions.split(",").map(e=>m`
    • ${e.trim().toUpperCase()}
    • `)}
    `}dispatchSubmitEvent(e){e.preventDefault();const n=new FormData(e.target),o=Object.fromEntries(n);this.dispatchEvent(new CustomEvent("typo3:formengine:online-media-added",{detail:o}))}};s([d({type:String})],r.prototype,"placeholder",void 0),s([d({type:String,attribute:"help-text"})],r.prototype,"allowedExtensionsHelpText",void 0),s([d({type:String,attribute:"extensions"})],r.prototype,"allowedExtensions",void 0),r=s([c("typo3-backend-formengine-online-media-form")],r);export{r as OnlineMediaFormElement}; diff --git a/Resources/Public/JavaScript/form-engine/element/password-element.js b/Resources/Public/JavaScript/form-engine/element/password-element.js new file mode 100644 index 0000000..ccdc596 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/password-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import s from"@typo3/core/document-service.js";import{selector as t}from"@typo3/core/literals.js";class i extends HTMLElement{constructor(){super(...arguments),this.element=null,this.passwordPolicyInfo=null,this.passwordPolicySet=!1}async connectedCallback(){if(this.element!==null)return;const e=this.getAttribute("recordFieldId");e!==null&&(await s.ready(),this.element=this.querySelector(t`#${e}`),this.element&&(this.passwordPolicyInfo=this.querySelector(t`#password-policy-info-${this.element.id}`),this.passwordPolicySet=(this.getAttribute("passwordPolicy")||"")!=="",this.registerEventHandler()))}registerEventHandler(){this.passwordPolicySet&&this.passwordPolicyInfo!==null&&(this.element.addEventListener("focusin",()=>{this.passwordPolicyInfo.classList.remove("hidden")}),this.element.addEventListener("focusout",()=>{this.passwordPolicyInfo.classList.add("hidden")}))}}window.customElements.define("typo3-formengine-element-password",i); diff --git a/Resources/Public/JavaScript/form-engine/element/select-country-element.js b/Resources/Public/JavaScript/form-engine/element/select-country-element.js new file mode 100644 index 0000000..0320c02 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/select-country-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import o from"@typo3/core/event/regular-event.js";import m from"@typo3/core/document-service.js";import d from"@typo3/backend/form-engine.js";import{selector as f}from"@typo3/core/literals.js";class u{constructor(){this.initialize=(c,t)=>{const n=document.querySelector(c);n!==null&&(t=t||{},new o("change",l=>{const e=l.target,i=e.parentElement.querySelector(".input-group-icon");i!==null&&(i.innerHTML=e.options[e.selectedIndex].dataset.icon);const s=e.closest(".t3js-formengine-field-item").querySelector(".t3js-forms-select-single-icons");if(s!==null){const r=s.querySelector(".form-wizard-icon-list-item a.active");r!==null&&r.classList.remove("active");const a=s.querySelector(f`[data-select-index="${e.selectedIndex.toString(10)}"]`);a!==null&&a.closest(".form-wizard-icon-list-item a").classList.add("active")}}).bindTo(n),t.onChange instanceof Array&&new o("change",()=>d.processOnFieldChange(t.onChange)).bindTo(n),new o("click",(l,e)=>{const i=e.closest(".t3js-forms-select-single-icons").querySelector(".form-wizard-icon-list-item a.active");i!==null&&i.classList.remove("active"),n.selectedIndex=parseInt(e.dataset.selectIndex,10),n.dispatchEvent(new Event("change")),e.closest(".form-wizard-icon-list-item a").classList.add("active")}).delegateTo(n.closest(".form-control-wrap"),".t3js-forms-select-single-icons .form-wizard-icon-list-item a:not(.active)"))}}initializeOnReady(c,t){m.ready().then(()=>{this.initialize(c,t)})}}var g=new u;export{g as default}; diff --git a/Resources/Public/JavaScript/form-engine/element/select-multiple-side-by-side-element.js b/Resources/Public/JavaScript/form-engine/element/select-multiple-side-by-side-element.js new file mode 100644 index 0000000..186041a --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/select-multiple-side-by-side-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{AbstractSortableSelectItems as r}from"@typo3/backend/form-engine/element/abstract-sortable-select-items.js";import s from"@typo3/core/document-service.js";import a from"@typo3/backend/form-engine.js";import o from"@typo3/backend/form-engine/element/extra/select-box-filter.js";import c from"@typo3/core/event/regular-event.js";import d from"@typo3/backend/utility.js";class m extends r{constructor(e,t){super(),this.selectedOptionsElement=null,this.availableOptionsElement=null,s.ready().then(l=>{this.selectedOptionsElement=l.getElementById(e),this.availableOptionsElement=l.getElementById(t),!(this.selectedOptionsElement===null||this.availableOptionsElement===null)&&this.registerEventHandler()})}registerEventHandler(){this.registerSortableEventHandler(this.selectedOptionsElement),this.registerKeyboardEvents(),this.availableOptionsElement.addEventListener("click",e=>{const t=e.currentTarget;this.handleOptionChecked(t)}),new o(this.availableOptionsElement)}handleOptionChecked(e){const t=e.dataset.relatedfieldname;if(t){const l=d.trimExplode(",",e.dataset?.exclusivevalues??""),n=e.querySelectorAll("option:checked");n.length>0&&n.forEach(i=>{a.setSelectOptionFromExternalSource(t,i.value,i.textContent,i.getAttribute("title"),l,i)})}}registerKeyboardEvents(){new c("keydown",e=>{const t=e.currentTarget;e.code==="Enter"&&(e.preventDefault(),this.handleOptionChecked(t))}).bindTo(this.availableOptionsElement)}}export{m as default}; diff --git a/Resources/Public/JavaScript/form-engine/element/select-single-element.js b/Resources/Public/JavaScript/form-engine/element/select-single-element.js new file mode 100644 index 0000000..14b1f58 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/select-single-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import s from"@typo3/core/event/regular-event.js";import m from"@typo3/core/document-service.js";import d from"@typo3/backend/form-engine.js";import{selector as f}from"@typo3/core/literals.js";class u{constructor(){this.initialize=(o,t)=>{const i=document.querySelector(o);i!==null&&(t=t||{},new s("change",l=>{const e=l.target,n=e.parentElement.querySelector(".input-group-icon");n!==null&&(n.innerHTML=e.options[e.selectedIndex].dataset.icon);const c=e.closest(".t3js-formengine-field-item").querySelector(".t3js-forms-select-single-icons");if(c!==null){const r=c.querySelector(".form-wizard-icon-list-item button.active, .form-wizard-icon-list-item a.active");r!==null&&r.classList.remove("active");const a=c.querySelector(f`[data-select-index="${e.selectedIndex.toString(10)}"]`);a!==null&&a.closest(".form-wizard-icon-list-item button, .form-wizard-icon-list-item a").classList.add("active")}}).bindTo(i),t.onChange instanceof Array&&new s("change",()=>d.processOnFieldChange(t.onChange)).bindTo(i),new s("click",(l,e)=>{const n=e.closest(".t3js-forms-select-single-icons").querySelector(".form-wizard-icon-list-item button.active, .form-wizard-icon-list-item a.active");n!==null&&n.classList.remove("active"),i.selectedIndex=parseInt(e.dataset.selectIndex,10),i.dispatchEvent(new Event("change")),e.closest(".form-wizard-icon-list-item button, .form-wizard-icon-list-item a").classList.add("active")}).delegateTo(i.closest(".form-control-wrap"),".t3js-forms-select-single-icons .form-wizard-icon-list-item button:not(.active), .t3js-forms-select-single-icons .form-wizard-icon-list-item a:not(.active)"))}}initializeOnReady(o,t){m.ready().then(()=>{this.initialize(o,t)})}}var g=new u;export{g as default}; diff --git a/Resources/Public/JavaScript/form-engine/element/select-tree-element.js b/Resources/Public/JavaScript/form-engine/element/select-tree-element.js new file mode 100644 index 0000000..b4c7c99 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/select-tree-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import"@typo3/backend/form-engine/element/select-tree.js";import"@typo3/backend/form-engine/element/select-tree-toolbar.js";import"@typo3/backend/element/icon-element.js";import n from"@typo3/core/document-service.js";import s from"@typo3/backend/form-engine.js";class o{constructor(e,t,i,r){if(this.recordField=null,this.tree=null,this.selectNode=a=>{const d=a.detail.node;this.updateAncestorsIndeterminateState(d),this.calculateIndeterminate(this.tree.nodes),this.saveCheckboxes(),this.tree.setup.input.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0}))},this.loadDataAfter=a=>{this.tree.nodes=a.detail.nodes.map(d=>(d.__indeterminate=!1,d)),this.calculateIndeterminate(this.tree.nodes)},this.saveCheckboxes=()=>{typeof this.recordField>"u"||(this.recordField.value=this.tree.getSelectedNodes().map(a=>a.identifier).join(","))},i instanceof Function)throw new Error("Function `callback` is not supported anymore since TYPO3 v12.0");n.ready().then(()=>{this.initialize(e,t,r)})}initialize(e,t,i){this.recordField=document.getElementById(t);const r=document.getElementById(e);this.tree=document.createElement("typo3-backend-form-selecttree"),this.tree.addEventListener("typo3:tree:nodes-prepared",this.loadDataAfter),this.tree.addEventListener("typo3:tree:node-selected",this.selectNode),i instanceof Array&&this.tree.addEventListener("typo3:tree:node-selected",()=>{s.processOnFieldChange(i)});const a={id:e,dataUrl:this.generateRequestUrl(),readOnlyMode:parseInt(this.recordField.dataset.readOnly,10)===1,input:this.recordField,exclusiveNodesIdentifiers:this.recordField.dataset.treeExclusiveKeys,validation:JSON.parse(this.recordField.dataset.formengineValidationRules)[0],expandUpToLevel:this.recordField.dataset.treeExpandUpToLevel,unselectableElements:[]};this.tree.addEventListener("tree:initialized",()=>{if(this.recordField.dataset.treeShowToolbar){const d=document.createElement("typo3-backend-form-selecttree-toolbar");d.tree=this.tree,this.tree.prepend(d)}}),this.tree.setup=a,r.append(this.tree)}generateRequestUrl(){const e={tableName:this.recordField.dataset.tablename,fieldName:this.recordField.dataset.fieldname,uid:this.recordField.dataset.uid,defaultValues:this.recordField.dataset.defaultvalues,overrideValues:this.recordField.dataset.overridevalues,recordTypeValue:this.recordField.dataset.recordtypevalue,dataStructureIdentifier:this.recordField.dataset.datastructureidentifier,flexFormSheetName:this.recordField.dataset.flexformsheetname,flexFormFieldName:this.recordField.dataset.flexformfieldname,flexFormContainerName:this.recordField.dataset.flexformcontainername,flexFormContainerIdentifier:this.recordField.dataset.flexformcontaineridentifier,flexFormContainerFieldName:this.recordField.dataset.flexformcontainerfieldname,flexFormSectionContainerIsNew:this.recordField.dataset.flexformsectioncontainerisnew,command:this.recordField.dataset.command};return TYPO3.settings.ajaxUrls.record_tree_data+"&"+new URLSearchParams(e).toString()}updateAncestorsIndeterminateState(e){let t=!1;e.__treeParents.forEach(i=>{const r=this.tree.getNodeByTreeIdentifier(i);r.__indeterminate=e.checked||e.__indeterminate||t,t=r.checked||r.__indeterminate||e.checked||e.__indeterminate})}calculateIndeterminate(e){e.forEach(t=>{(t.checked||t.__indeterminate)&&t.__treeParents&&t.__treeParents.length>0&&t.__treeParents.forEach(i=>{const r=this.tree.getNodeByTreeIdentifier(i);r.__indeterminate=!0})})}}export{o as SelectTreeElement}; diff --git a/Resources/Public/JavaScript/form-engine/element/select-tree-toolbar.js b/Resources/Public/JavaScript/form-engine/element/select-tree-toolbar.js new file mode 100644 index 0000000..f4f8fc0 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/select-tree-toolbar.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as d,html as p}from"lit";import{customElement as h}from"lit/decorators.js";import s from"~labels/backend.alt_doc";var f=function(n,e,t,c){var i=arguments.length,l=i<3?e:c===null?c=Object.getOwnPropertyDescriptor(e,t):c,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")l=Reflect.decorate(n,e,t,c);else for(var o=n.length-1;o>=0;o--)(a=n[o])&&(l=(i<3?a(l):i>3?a(e,t,l):a(e,t))||l);return i>3&&l&&Object.defineProperty(e,t,l),l};let r=class extends d{constructor(){super(...arguments),this.settings={collapseAllBtn:"collapse-all-btn",expandAllBtn:"expand-all-btn",searchInput:"search-input",toggleHideUnchecked:"hide-unchecked-btn"},this.hideUncheckedState=!1}createRenderRoot(){return this}render(){return p`
    this.filter(e)}>
    `}collapseAll(e){e.preventDefault(),this.tree.nodes.forEach(t=>{t.__parents.length&&this.tree.hideChildren(t)})}expandAll(){this.tree.expandAll()}filter(e){const t=e.target;this.tree.filter(t.value.trim())}toggleHideUnchecked(){this.hideUncheckedState=!this.hideUncheckedState,this.hideUncheckedState?this.tree.nodes.forEach(e=>{e.checked?(this.tree.showParents(e),e.expanded=!0,e.__hidden=!1):(e.expanded=!1,e.__hidden=!0)}):this.tree.nodes.forEach(e=>e.__hidden=!1)}};r=f([h("typo3-backend-form-selecttree-toolbar")],r);export{r as SelectTreeToolbar}; diff --git a/Resources/Public/JavaScript/form-engine/element/select-tree.js b/Resources/Public/JavaScript/form-engine/element/select-tree.js new file mode 100644 index 0000000..4e08063 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/select-tree.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as a}from"lit";import{Tree as f}from"@typo3/backend/tree/tree.js";import{state as h,customElement as u}from"lit/decorators.js";var o=function(r,e,t,s){var l=arguments.length,n=l<3?e:s===null?s=Object.getOwnPropertyDescriptor(e,t):s,i;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(r,e,t,s);else for(var d=r.length-1;d>=0;d--)(i=r[d])&&(n=(l<3?i(n):l>3?i(e,t,n):i(e,t))||n);return l>3&&n&&Object.defineProperty(e,t,n),n};let c=class extends f{constructor(){super(),this.settings={unselectableElements:[],exclusiveNodesIdentifiers:"",validation:{},readOnlyMode:!1,showIcons:!0,width:300,dataUrl:"",defaultProperties:{},expandUpToLevel:null},this.exclusiveSelectedNode=null,this.addEventListener("typo3:tree:nodes-prepared",this.prepareLoadedNodes)}expandAll(){this.nodes.forEach(e=>{this.showChildren(e)})}selectNode(e,t=!0){if(!this.isNodeSelectable(e))return;const s=e.checked;this.handleExclusiveNodeSelection(e),!s&&this.settings.validation.maxItems==1&&this.getSelectedNodes().length>0&&(this.getSelectedNodes()[0].checked=!1),!(this.settings.validation&&this.settings.validation.maxItems&&!s&&this.getSelectedNodes().length>=this.settings.validation.maxItems)&&(e.checked=!s,this.dispatchEvent(new CustomEvent("typo3:tree:node-selected",{detail:{node:e,propagate:t}})))}filter(e){const t=[];this.searchTerm=e,this.nodes.length&&(this.nodes[0].__expanded=!1);const s=this.nodes[0],l=new RegExp(e,"i");this.nodes.forEach(i=>{i!==s&&(i.__expanded=!1,i.__hidden=!0,l.test(i.name)&&t.push(i))}),t.forEach(i=>{i.__hidden=!1,this.showParents(i)}),this.nodes.filter(i=>t.some(d=>i.__parents.includes(d.identifier))).forEach(i=>{i.__hidden=!1})}showParents(e){if(e.__parents.length===0)return;const t=this.nodes.find(s=>s.identifier===e.__parents.at(-1));t.__hidden=!1,t.__expanded=!0,this.showParents(t)}isNodeSelectable(e){return!this.settings.readOnlyMode&&this.settings.unselectableElements.indexOf(e.identifier)===-1}createNodeContent(e){return a`${this.renderCheckbox(e)} ${super.createNodeContent(e)}`}renderCheckbox(e){const t=!!e.checked;let s="actions-square";return!this.isNodeSelectable(e)&&!t?s="actions-minus-circle":e.checked?s="actions-check-square":e.__indeterminate&&!t&&(s="actions-minus-square"),a` `}prepareLoadedNodes(e){const t=e.detail.nodes;e.detail.nodes=t.map(s=>(s.selectable===!1&&this.settings.unselectableElements.push(s.identifier),s))}handleExclusiveNodeSelection(e){const t=this.settings.exclusiveNodesIdentifiers.split(",");this.settings.exclusiveNodesIdentifiers.length&&e.checked===!1&&(t.indexOf(""+e.identifier)>-1?(this.resetSelectedNodes(),this.exclusiveSelectedNode=e):t.indexOf(""+e.identifier)===-1&&this.exclusiveSelectedNode&&(this.exclusiveSelectedNode.checked=!1,this.exclusiveSelectedNode=null))}};o([h()],c.prototype,"settings",void 0),o([h()],c.prototype,"exclusiveSelectedNode",void 0),c=o([u("typo3-backend-form-selecttree")],c);export{c as SelectTree}; diff --git a/Resources/Public/JavaScript/form-engine/element/slug-element.js b/Resources/Public/JavaScript/form-engine/element/slug-element.js new file mode 100644 index 0000000..1ad3603 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/slug-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import u from"@typo3/core/ajax/ajax-request.js";import p from"@typo3/core/document-service.js";import h from"@typo3/core/event/debounce-event.js";import c from"@typo3/core/event/regular-event.js";var a;(function(n){n.toggleButton=".t3js-form-field-slug-toggle",n.recreateButton=".t3js-form-field-slug-recreate",n.inputField=".t3js-form-field-slug-input",n.readOnlyField=".t3js-form-field-slug-readonly",n.hiddenField=".t3js-form-field-slug-hidden"})(a||(a={}));var o;(function(n){n.AUTO="auto",n.RECREATE="recreate",n.MANUAL="manual"})(o||(o={}));class f{constructor(t,i){this.options=null,this.fullElement=null,this.manuallyChanged=!1,this.readOnlyField=null,this.inputField=null,this.hiddenField=null,this.request=null,this.fieldsToListenOn={},this.options=i,this.fieldsToListenOn=this.options.listenerFieldNames||{},p.ready().then(l=>{this.fullElement=l.querySelector(t),this.inputField=this.fullElement.querySelector(a.inputField),this.readOnlyField=this.fullElement.querySelector(a.readOnlyField),this.hiddenField=this.fullElement.querySelector(a.hiddenField),this.registerEvents()})}registerEvents(){const t=Object.values(this.getAvailableFieldsForProposalGeneration()).map(e=>e.dataset.slugSelector),i=this.fullElement.querySelector(a.recreateButton);t.length>0&&this.options.command==="new"&&new h("input",()=>{this.manuallyChanged||this.sendSlugProposal(o.AUTO)}).delegateTo(document,t.join(",")),t.length>0||this.hasPostModifiersDefined()?new c("click",e=>{e.preventDefault(),this.readOnlyField.classList.contains("hidden")&&(this.readOnlyField.classList.toggle("hidden",!1),this.inputField.classList.toggle("hidden",!0)),this.sendSlugProposal(o.RECREATE)}).bindTo(i):(i.classList.add("disabled"),i.disabled=!0),new h("input",()=>{this.manuallyChanged=!0,this.sendSlugProposal(o.MANUAL)}).bindTo(this.inputField);const l=this.fullElement.querySelector(a.toggleButton);new c("click",e=>{e.preventDefault();const s=this.readOnlyField.classList.contains("hidden");if(this.readOnlyField.classList.toggle("hidden",!s),this.inputField.classList.toggle("hidden",s),!s){this.hiddenField.value=this.inputField.value;return}this.inputField.value!==this.readOnlyField.value?this.readOnlyField.value=this.inputField.value:(this.manuallyChanged=!1,this.fullElement.querySelector(".t3js-form-proposal-accepted").classList.add("hidden"),this.fullElement.querySelector(".t3js-form-proposal-different").classList.add("hidden")),this.hiddenField.value=this.readOnlyField.value}).bindTo(l)}sendSlugProposal(t){const i={};t===o.AUTO||t===o.RECREATE?(Object.entries(this.getAvailableFieldsForProposalGeneration()).forEach(l=>{i[l[0]]=l[1].value}),this.options.includeUidInValues===!0&&(i.uid=this.options.recordId.toString())):i.manual=this.inputField.value,this.request instanceof u&&this.request.abort(),this.request=new u(TYPO3.settings.ajaxUrls.record_slug_suggest),this.request.post({values:i,mode:t,tableName:this.options.tableName,pageId:this.options.pageId,parentPageId:this.options.parentPageId,recordId:this.options.recordId,language:this.options.language,fieldName:this.options.fieldName,command:this.options.command,signature:this.options.signature}).then(async l=>{const e=await l.resolve(),s="/"+e.proposal.replace(/^\//,""),d=this.fullElement.querySelector(".t3js-form-proposal-accepted"),r=this.fullElement.querySelector(".t3js-form-proposal-different");d.classList.toggle("hidden",e.hasConflicts),r.classList.toggle("hidden",!e.hasConflicts),(e.hasConflicts?r:d).querySelector("span").innerText=s,this.hiddenField.value!==e.proposal&&this.fullElement.querySelector("input[data-formengine-input-name]").dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0})),t===o.AUTO||t===o.RECREATE?(this.readOnlyField.value=e.proposal,this.hiddenField.value=e.proposal,this.inputField.value=e.proposal):this.hiddenField.value=e.proposal}).finally(()=>{this.request=null})}getAvailableFieldsForProposalGeneration(){const t={};for(const[i,l]of Object.entries(this.fieldsToListenOn)){const e=['[data-formengine-input-name="'+l+'"]','[name="'+l+'"]'];let s,d="";e.some(r=>(s=document.querySelector(r),d=r,s!==null)),s!==null&&(s.dataset.slugSelector=d,t[i]=s)}return t}hasPostModifiersDefined(){return Array.isArray(this.options.config.generatorOptions.postModifiers)&&this.options.config.generatorOptions.postModifiers.length>0}}export{f as default}; diff --git a/Resources/Public/JavaScript/form-engine/element/suggest/result-container.js b/Resources/Public/JavaScript/form-engine/element/suggest/result-container.js new file mode 100644 index 0000000..a4e884d --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/suggest/result-container.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as p,customElement as f}from"lit/decorators.js";import{LitElement as m,html as r,css as g}from"lit";import"@typo3/backend/form-engine/element/suggest/result-item.js";import h from"~labels/backend.alt_doc";var a=function(s,e,t,l){var o=arguments.length,n=o<3?e:l===null?l=Object.getOwnPropertyDescriptor(e,t):l,i;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(s,e,t,l);else for(var u=s.length-1;u>=0;u--)(i=s[u])&&(n=(o<3?i(n):o>3?i(e,t,n):i(e,t))||n);return o>3&&n&&Object.defineProperty(e,t,n),n};let c=class extends m{constructor(){super(...arguments),this.results=null}connectedCallback(){super.connectedCallback(),this.addEventListener("keydown",this.handleKeyDown)}disconnectedCallback(){this.removeEventListener("keydown",this.handleKeyDown),super.disconnectedCallback()}createRenderRoot(){return this}render(){let e;return this.results!==null&&(this.results.length===0?e=r`
    ${h.get("search.no_records_found")}
    `:e=r`${this.results.map(t=>this.renderResultItem(t))}`),r`${e}`}renderResultItem(e){return r``}handleKeyDown(e){if(e.preventDefault(),e.key==="Escape"){this.closest(".t3-form-suggest-container").querySelector('input[type="search"]').focus(),this.hidden=!0;return}if(!["ArrowDown","ArrowUp"].includes(e.key)||document.activeElement.tagName.toLowerCase()!=="typo3-backend-formengine-suggest-result-item")return;let t;e.key==="ArrowDown"?t=document.activeElement.nextElementSibling:(t=document.activeElement.previousElementSibling,t===null&&(t=this.closest(".t3-form-suggest-container").querySelector('input[type="search"]'))),t!==null&&t.focus()}};a([p({type:Object})],c.prototype,"results",void 0),c=a([f("typo3-backend-formengine-suggest-result-container")],c);let d=class extends m{static{this.styles=g`:host{display:block}`}render(){return r``}};d=a([f("typo3-backend-formengine-suggest-result-list")],d);export{c as ResultContainer,d as ResultList}; diff --git a/Resources/Public/JavaScript/form-engine/element/suggest/result-item.js b/Resources/Public/JavaScript/form-engine/element/suggest/result-item.js new file mode 100644 index 0000000..ab81133 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/suggest/result-item.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as u}from"lit";import{property as l,customElement as m}from"lit/decorators.js";import{PseudoButtonLitElement as d}from"@typo3/backend/element/pseudo-button.js";import"@typo3/backend/element/icon-element.js";var s=function(c,e,t,n){var r=arguments.length,o=r<3?e:n===null?n=Object.getOwnPropertyDescriptor(e,t):n,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")o=Reflect.decorate(c,e,t,n);else for(var p=c.length-1;p>=0;p--)(a=c[p])&&(o=(r<3?a(o):r>3?a(e,t,o):a(e,t))||o);return r>3&&o&&Object.defineProperty(e,t,o),o};let i=class extends d{constructor(){super(),this.addEventListener("blur",this.onBlur)}buttonActivated(e){this.dispatchItemChosenEvent(e.currentTarget)}createRenderRoot(){return this}render(){return u`
    ${this.label} [${this.uid}] ${this.path}
    `}onBlur(e){let t=!0;const n=e.relatedTarget,r=this.closest("typo3-backend-formengine-suggest-result-container");n?.tagName.toLowerCase()==="typo3-backend-formengine-suggest-result-item"&&(t=!1),n?.matches('input[type="search"]')&&r.contains(n)&&(t=!1),r.hidden=t}dispatchItemChosenEvent(e){e.closest("typo3-backend-formengine-suggest-result-container").dispatchEvent(new CustomEvent("typo3:formengine:suggest-item-chosen",{detail:{element:e}}))}};s([l({type:Object})],i.prototype,"icon",void 0),s([l({type:Number})],i.prototype,"uid",void 0),s([l({type:String})],i.prototype,"table",void 0),s([l({type:String})],i.prototype,"label",void 0),s([l({type:String})],i.prototype,"path",void 0),i=s([m("typo3-backend-formengine-suggest-result-item")],i);export{i as ResultItem}; diff --git a/Resources/Public/JavaScript/form-engine/element/table-permission-element.js b/Resources/Public/JavaScript/form-engine/element/table-permission-element.js new file mode 100644 index 0000000..c7ae375 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/table-permission-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import o from"@typo3/core/document-service.js";import n from"@typo3/core/event/regular-event.js";import{selector as a}from"@typo3/core/literals.js";import{MultiRecordSelectionSelectors as m}from"@typo3/backend/multi-record-selection.js";var l;(function(s){s.none="none",s.select="select",s.modify="modify"})(l||(l={}));class r extends HTMLElement{constructor(){super(...arguments),this.selectStateField=null,this.modifyStateField=null}async connectedCallback(){await o.ready(),this.selectStateField=this.querySelector(a`input[name=${this.getAttribute("selectStateFieldName")||""}]`),this.modifyStateField=this.querySelector(a`input[name=${this.getAttribute("modifyStateFieldName")||""}]`),!(this.selectStateField===null||this.modifyStateField===null)&&this.registerEventHandler()}registerEventHandler(){new n("change",e=>{this.handleSingleItemChange(e.target)}).delegateTo(this.querySelector("table"),".t3js-table-permissions-item"),new n("multiRecordSelection:checkbox:state:changed",e=>{const t=e.target.name;if(this.querySelectorAll(a`input[name="${t}"]:checked`).length===0){const i=this.querySelector(a`input[name="${t}"]`);i.value=l.none,this.handleSingleItemChange(i),this.querySelector(a`input[name="${t}"][value="${l.none}"]`).checked=!0}}).delegateTo(this.querySelector("table"),m.checkboxSelector)}handleSingleItemChange(e){switch(e.value){case l.select:this.addItem(e.dataset.table,this.selectStateField),this.removeItem(e.dataset.table,this.modifyStateField);break;case l.modify:this.addItem(e.dataset.table,this.selectStateField),this.addItem(e.dataset.table,this.modifyStateField);break;case l.none:default:this.removeItem(e.dataset.table,this.selectStateField),this.removeItem(e.dataset.table,this.modifyStateField);break}}removeItem(e,t){t.value=(t.value.length?t.value.split(","):[]).filter(i=>i!==e).join(",")}addItem(e,t){const i=t.value.length?t.value.split(","):[];i.includes(e)||(i.push(e),t.value=i.join(","))}}window.customElements.define("typo3-formengine-element-tablepermission",r); diff --git a/Resources/Public/JavaScript/form-engine/element/table-wizard-element.js b/Resources/Public/JavaScript/form-engine/element/table-wizard-element.js new file mode 100644 index 0000000..f432ab6 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/element/table-wizard-element.js @@ -0,0 +1,16 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as g,html as r}from"lit";import{property as u,customElement as v}from"lit/decorators.js";import s from"~labels/core.wizards";import m from"~labels/core.core";import"@typo3/backend/element/icon-element.js";import h from"@typo3/backend/modal.js";var d=function(p,t,e,a){var l=arguments.length,n=l<3?t:a===null?a=Object.getOwnPropertyDescriptor(t,e):a,i;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(p,t,e,a);else for(var o=p.length-1;o>=0;o--)(i=p[o])&&(n=(l<3?i(n):l>3?i(t,e,n):i(t,e))||n);return l>3&&n&&Object.defineProperty(t,e,n),n};let c=class extends g{constructor(){super(...arguments),this.type="textarea",this.selectorData="",this.delimiter="|",this.enclosure="",this.appendRows=1,this.table=[]}get firstRow(){return this.table[0]||[]}connectedCallback(){super.connectedCallback(),this.selectorData=this.getAttribute("selector"),this.delimiter=this.getAttribute("delimiter"),this.enclosure=this.getAttribute("enclosure")||"",this.readTableFromTextarea()}createRenderRoot(){return this}render(){return this.renderTemplate()}provideMinimalTable(){(this.table.length===0||this.firstRow.length===0)&&(this.table=[[""]])}readTableFromTextarea(){const t=document.querySelector(this.selectorData),e=[];t.value.split(` +`).forEach(a=>{if(a!==""){this.enclosure&&(a=a.replace(new RegExp(this.enclosure,"g"),""));const l=a.split(this.delimiter);e.push(l)}}),this.table=e}writeTableSyntaxToTextarea(){const t=document.querySelector(this.selectorData);let e="";this.table.forEach(a=>{const l=a.length;e+=a.reduce((n,i,o)=>{const b=l-1===o?"":this.delimiter;return i=i.replace(/\r?\n/g,"
    "),n+this.enclosure+i+this.enclosure+b},"")+` +`}),t.value=e,t.dispatchEvent(new CustomEvent("change",{bubbles:!0}))}modifyTable(t,e,a){const l=t.target;this.table[e][a]=l.value,this.writeTableSyntaxToTextarea(),this.requestUpdate()}toggleType(){this.type=this.type==="input"?"textarea":"input"}moveColumn(t,e){this.table=this.table.map(a=>{const l=a.splice(t,1);return a.splice(e,0,...l),a}),this.writeTableSyntaxToTextarea(),this.requestUpdate()}appendColumn(t,e){this.table=this.table.map(a=>(a.splice(e+1,0,""),a)),this.writeTableSyntaxToTextarea(),this.requestUpdate()}removeColumn(t,e){this.table=this.table.map(a=>(a.splice(e,1),a)),this.writeTableSyntaxToTextarea(),this.requestUpdate()}moveRow(t,e,a){const l=this.table.splice(e,1);this.table.splice(a,0,...l),this.writeTableSyntaxToTextarea(),this.requestUpdate()}appendRow(t,e){const a=this.firstRow.concat().fill(""),l=new Array(this.appendRows).fill(a);this.table.splice(e+1,0,...l),this.writeTableSyntaxToTextarea(),this.requestUpdate()}removeRow(t,e){this.table.splice(e,1),this.writeTableSyntaxToTextarea(),this.requestUpdate()}renderTemplate(){this.provideMinimalTable();const t=Object.keys(this.firstRow).map(l=>parseInt(l,10)),e=t[t.length-1],a=this.table.length-1;return r`
    ${t.map(l=>r``)}${this.table.map((l,n)=>r`${l.map((i,o)=>r``)}`)}
    ${this.renderTypeButton()}${this.renderColButtons(l,e)}
    ${this.renderRowButtons(n,a)}${this.renderDataElement(i,n,o)}
    `}renderDataElement(t,e,a){const l=n=>this.modifyTable(n,e,a);switch(this.type){case"input":return r`")}>`;case"textarea":default:return r``}}renderTypeButton(){return r` `}renderColButtons(t,e){const a={title:t===0?s.get("table_end"):s.get("table_left"),class:t===0?"bar-right":"left",target:t===0?e:t-1},l={title:t===e?s.get("table_start"):s.get("table_right"),class:t===e?"bar-left":"right",target:t===e?0:t+1};return r` `}renderRowButtons(t,e){const a={title:t===0?s.get("table_bottom"):s.get("table_up"),class:t===0?"bar-down":"up",target:t===0?e:t-1},l={title:t===e?s.get("table_top"):s.get("table_down"),class:t===e?"bar-up":"down",target:t===e?0:t+1};return r` `}showTableConfigurationModal(t){const e=this.firstRow.length,a=this.table.length,l=a||1,n=e||1,i=h.advanced({content:r`
    `,title:s.get("table_setCountHeadline"),size:h.sizes.small,buttons:[{text:m.get("labels.cancel"),btnClass:"btn-default",name:"cancel",trigger:()=>i.hideModal()},{text:s.get("table_buttonUpdate"),active:!0,btnClass:"btn-primary",name:"apply",trigger:()=>{const o=i.querySelector("#t3js-expand-rows"),b=i.querySelector("#t3js-expand-cols");if(!(o===null||b===null))if(o.checkValidity()&&b.checkValidity()){const y=Number(o.value)-a,f=Number(b.value)-e;this.setColAndRowCount(t,f,y),i.hideModal()}else o.reportValidity(),b.reportValidity()}}]})}showTableSyntax(){const t=document.querySelector(this.selectorData),e=h.advanced({content:r`
    `,title:s.get("table_showCodeHeadline"),size:h.sizes.small,buttons:[{text:m.get("labels.cancel"),btnClass:"btn-default",name:"cancel",trigger:()=>e.hideModal()},{text:s.get("table_buttonUpdate"),active:!0,btnClass:"btn-primary",name:"apply",trigger:()=>{t.value=e.querySelector("textarea").value,t.dispatchEvent(new CustomEvent("change",{bubbles:!0})),this.readTableFromTextarea(),this.requestUpdate(),e.hideModal()}}]})}setColAndRowCount(t,e,a){const l=this.table.length;if(a>0)for(let n=0;n0)for(let n=0;n{this.element=document.getElementById(e),t.enable(this.element),l.enable(this.element)})}}export{n as default}; diff --git a/Resources/Public/JavaScript/form-engine/field-control/add-record.js b/Resources/Public/JavaScript/form-engine/field-control/add-record.js new file mode 100644 index 0000000..e380a2c --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/field-control/add-record.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import r from"@typo3/core/document-service.js";import o from"@typo3/backend/form-engine.js";class n{constructor(e){this.controlElement=null,this.registerClickHandler=t=>{t.preventDefault(),o.preventFollowLinkIfNotSaved(this.controlElement.getAttribute("href"))},r.ready().then(()=>{this.controlElement=document.querySelector(e),this.controlElement.addEventListener("click",this.registerClickHandler)})}}export{n as default}; diff --git a/Resources/Public/JavaScript/form-engine/field-control/edit-popup.js b/Resources/Public/JavaScript/form-engine/field-control/edit-popup.js new file mode 100644 index 0000000..f9af27a --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/field-control/edit-popup.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import l from"@typo3/core/document-service.js";class r{constructor(s){this.controlElement=null,this.assignedFormField=null,this.registerChangeHandler=()=>{this.controlElement.classList.toggle("disabled",this.assignedFormField.options.selectedIndex===-1)},this.registerClickHandler=n=>{n.preventDefault();const t=[];for(let e=0;e{this.controlElement=document.querySelector(s),this.assignedFormField=document.querySelector('select[data-formengine-input-name="'+this.controlElement.dataset.element+'"]'),this.assignedFormField.options.selectedIndex===-1&&this.controlElement.classList.add("disabled"),this.assignedFormField.addEventListener("change",this.registerChangeHandler),this.controlElement.addEventListener("click",this.registerClickHandler)})}}export{r as default}; diff --git a/Resources/Public/JavaScript/form-engine/field-control/insert-clipboard.js b/Resources/Public/JavaScript/form-engine/field-control/insert-clipboard.js new file mode 100644 index 0000000..0c95cde --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/field-control/insert-clipboard.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import n from"@typo3/core/document-service.js";import s from"@typo3/backend/form-engine.js";class i{constructor(t){this.controlElement=null,this.registerClickHandler=r=>{r.preventDefault();const o=this.controlElement.dataset.element,l=JSON.parse(this.controlElement.dataset.clipboardItems);for(const e of l)s.setSelectOptionFromExternalSource(o,e.value,e.title,e.title)},n.ready().then(()=>{this.controlElement=document.querySelector(t),this.controlElement.addEventListener("click",this.registerClickHandler)})}}export{i as default}; diff --git a/Resources/Public/JavaScript/form-engine/field-control/link-popup.js b/Resources/Public/JavaScript/form-engine/field-control/link-popup.js new file mode 100644 index 0000000..8722652 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/field-control/link-popup.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import u from"@typo3/core/document-service.js";import h from"@typo3/backend/form-engine.js";import{selector as l}from"@typo3/core/literals.js";import t from"@typo3/backend/modal.js";import{FormEngineLinkBrowserSetLinkEvent as p}from"@typo3/backend/event/form-engine-link-browser-set-link-event.js";class f{constructor(a){this.controlElement=null,this.handleControlClick=i=>{i.preventDefault();const e=this.controlElement.dataset.itemName,c=document.querySelector(l`[name="${e}"]`),n=document.querySelector(l`[data-formengine-input-name="${e}"]`),m=this.controlElement.getAttribute("href")+"&P[currentValue]="+encodeURIComponent(document.forms.namedItem("editform")[e].value)+"&P[currentSelectedValues]="+encodeURIComponent(c.value),o=t.advanced({type:t.types.iframe,content:m,size:t.sizes.large});o.addEventListener(p.eventName,s=>{const{value:d,onFieldChangeItems:r}=s;n.value=d,n.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0})),Array.isArray(r)&&h.processOnFieldChange(r),o.hideModal()})},u.ready().then(()=>{this.controlElement=document.querySelector(a),this.controlElement!==null&&this.controlElement.addEventListener("click",this.handleControlClick)})}}export{f as default}; diff --git a/Resources/Public/JavaScript/form-engine/field-control/list-module.js b/Resources/Public/JavaScript/form-engine/field-control/list-module.js new file mode 100644 index 0000000..8d3f7d3 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/field-control/list-module.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import r from"@typo3/core/document-service.js";import o from"@typo3/backend/form-engine.js";class l{constructor(e){this.controlElement=null,this.registerClickHandler=t=>{t.preventDefault(),o.preventFollowLinkIfNotSaved(this.controlElement.getAttribute("href"))},r.ready().then(()=>{this.controlElement=document.querySelector(e),this.controlElement.addEventListener("click",this.registerClickHandler)})}}export{l as default}; diff --git a/Resources/Public/JavaScript/form-engine/field-control/password-generator.js b/Resources/Public/JavaScript/form-engine/field-control/password-generator.js new file mode 100644 index 0000000..82f118f --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/field-control/password-generator.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import s from"@typo3/core/document-service.js";import n from"@typo3/backend/form-engine.js";import o from"@typo3/backend/form-engine-validation.js";import d from"@typo3/core/ajax/ajax-request.js";import l from"@typo3/backend/notification.js";import i from"~labels/core.core";class r{constructor(a){this.controlElement=null,this.humanReadableField=null,this.hiddenField=null,this.passwordPolicy=null,s.ready().then(()=>{if(this.controlElement=document.getElementById(a),this.humanReadableField=document.querySelector('input[data-formengine-input-name="'+this.controlElement.dataset.itemName+'"]'),this.hiddenField=document.querySelector('input[name="'+this.controlElement.dataset.itemName+'"]'),this.passwordPolicy=this.controlElement.dataset.passwordPolicy||null,!this.controlElement.dataset.allowEdit&&(this.humanReadableField.disabled=!0,this.humanReadableField.readOnly=!0,this.humanReadableField.isClearable||this.humanReadableField.classList.contains("t3js-clearable"))){this.humanReadableField.classList.remove("t3js-clearable");const e=this.humanReadableField.closest("div.form-control-clearable-wrapper");if(e){e.classList.remove("form-control-clearable");const t=e.querySelector("button.close");t&&e.removeChild(t)}}this.controlElement.addEventListener("click",this.generatePassword.bind(this))})}generatePassword(a){a.preventDefault(),new d(TYPO3.settings.ajaxUrls.password_generate).post({passwordPolicy:this.passwordPolicy}).then(async e=>{const t=await e.resolve();t.success===!0?(this.humanReadableField.type="text",this.humanReadableField.value=t.password,this.humanReadableField.dispatchEvent(new Event("change")),this.hiddenField&&(this.humanReadableField.value=this.hiddenField.value),o.validateField(this.humanReadableField),n.markFieldAsChanged(this.humanReadableField)):l.warning(i.get("labels.generatePassword.failed"))}).catch(()=>{l.warning(i.get("labels.generatePassword.failed"))})}}export{r as default}; diff --git a/Resources/Public/JavaScript/form-engine/field-control/reset-selection.js b/Resources/Public/JavaScript/form-engine/field-control/reset-selection.js new file mode 100644 index 0000000..c4f8db5 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/field-control/reset-selection.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import c from"@typo3/core/document-service.js";class r{constructor(t){this.controlElement=null,this.registerClickHandler=n=>{n.preventDefault();const o=this.controlElement.dataset.itemName,l=JSON.parse(this.controlElement.dataset.selectedIndices),e=document.forms.namedItem("editform").querySelector('[name="'+o+'[]"]');e.selectedIndex=-1;for(const s of l)e.options[s].selected=!0},c.ready().then(()=>{this.controlElement=document.querySelector(t),this.controlElement!==null&&this.controlElement.addEventListener("click",this.registerClickHandler)})}}export{r as default}; diff --git a/Resources/Public/JavaScript/form-engine/field-wizard/localization-state-selector.js b/Resources/Public/JavaScript/form-engine/field-wizard/localization-state-selector.js new file mode 100644 index 0000000..17663ab --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/field-wizard/localization-state-selector.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import o from"@typo3/core/document-service.js";import u from"@typo3/core/event/regular-event.js";var n;(function(s){s.CUSTOM="custom"})(n||(n={}));class c{constructor(r){o.ready().then(()=>{this.registerEventHandler(r)})}registerEventHandler(r){new u("change",i=>{const t=i.target,e=t.closest(".t3js-formengine-field-item")?.querySelector("[data-formengine-input-name]");if(!e)return;const a=e.dataset.lastL10nState||!1,l=t.value;a&&l===a||(l===n.CUSTOM?(a&&(t.dataset.originalLanguageValue=e.value),e.disabled=!1):(a===n.CUSTOM&&(t.closest(".t3js-l10n-state-container").querySelector(".t3js-l10n-state-custom").dataset.originalLanguageValue=e.value),e.disabled=!0),e.value=t.dataset.originalLanguageValue,e.dispatchEvent(new Event("change")),e.dataset.lastL10nState=t.value)}).delegateTo(document,'.t3js-l10n-state-container input[type="radio"][name="'+r+'"]')}}export{c as default}; diff --git a/Resources/Public/JavaScript/form-engine/field-wizard/shortcut-validation.js b/Resources/Public/JavaScript/form-engine/field-wizard/shortcut-validation.js new file mode 100644 index 0000000..ac68add --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/field-wizard/shortcut-validation.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import d from"@typo3/core/document-service.js";import o from"@typo3/backend/form-engine.js";class u{constructor(){d.ready().then(()=>{this.run()})}run(){const e=document.querySelector('[name$="[shortcut]"]'),t=document.querySelector('[name$="[shortcut_mode]"]'),i=e?.closest("form");!e||!t||!i||t.addEventListener("change",()=>{this.apply(e,i,t)})}async apply(e,t,i){const a=parseInt(i.value,10)===0,n=JSON.parse(e.dataset.formengineValidationRules||"[]"),r=n.findIndex(s=>s.type==="required");a&&r===-1?n.push({type:"required"}):!a&&r!==-1&&n.splice(r,1),e.dataset.formengineValidationRules=JSON.stringify(n),o.reinitialize(),o.Validation.initializeInputFields(),o.Validation.validate(t)}}var c=new u;export{c as default}; diff --git a/Resources/Public/JavaScript/form-engine/field-wizard/value-picker.js b/Resources/Public/JavaScript/form-engine/field-wizard/value-picker.js new file mode 100644 index 0000000..6f862c7 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine/field-wizard/value-picker.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class t extends HTMLElement{constructor(){super(),this.valuePicker=null,this.linkedField=null,this.initialValueSet=!1,this.onChange=()=>{this.setValue(),this.valuePicker.blur()},this.linkedFieldOnChange=()=>{this.valuePicker!==null&&this.selectValue(this.linkedField.value)};const e=document.createElement("slot");e.addEventListener("slotchange",()=>this.initializeValuePicker(e)),this.attachShadow({mode:"open"}).append(e)}connectedCallback(){this.linkedField=document.querySelector(this.getAttribute("linked-field")),this.linkedField?.addEventListener("change",this.linkedFieldOnChange),this.initializeValuePicker(this.shadowRoot.querySelector("slot"))}disconnectedCallback(){this.linkedField?.removeEventListener("change",this.linkedFieldOnChange),this.linkedField=null}initializeValuePicker(e){const i=e.assignedElements()[0]??null;if(i!==null&&i.tagName.toLowerCase()!=="select")throw new Error(`ValuePicker could not be initialized. Expected `:l``}getNewCell(){return structuredClone(this.defaultCell)}writeConfig(e){this.field.value=e;const t=e.split(` +`);let i="";for(const a of t)a&&(i+=" "+a+` +`);const r=`mod.web_layout.BackendLayouts { + exampleKey { + title = Example + icon = content-container-columns-2 + config { +`+i.replace(new RegExp("\\t","g")," ")+` } + } +} +`,n=this.previewAreaRef.value;n instanceof HTMLTextAreaElement&&(n.value=r);const s=this.codeMirrorRef.value;s instanceof j&&s.setContent(r)}addRowTop(){const e=[];for(let t=0;t1&&this.removeRowspan(e,t-1),!0):!1}removeColumn(){if(this.colCount<=1)return!1;const e=[];for(let t=0;t1&&this.removeColspan(e-1,t),!0):!1}addColumn(){for(let e=0;e{const T=D!=="none"?D:"",E=m[D],y=document.createElement("option");y.value=E,y.text=T,y.selected=E===i.slideMode?.toString(),C.appendChild(y)}),H.append($,C),n.append(S,N,_,H);const M=A.show(o.get("grid_windowTitle"),n,q.notice,[{active:!0,btnClass:"btn-default",name:"cancel",text:I.get("cancel")},{btnClass:"btn-primary",name:"ok",text:I.get("ok")}]);return M.userData.col=e,M.userData.row=t,M.addEventListener("button.clicked",this.modalButtonClickHandler),!0}getCell(e,t){return e>this.colCount-1||t>this.rowCount-1?!1:this.data.length>t-1&&this.data[t].length>e-1?this.data[t][e]:null}cellCanSpanRight(e,t){if(e===this.colCount-1)return!1;const i=this.getCell(e,t);if(!i)return!1;let r;if(i.rowspan>1){for(let n=t;n1||r.rowspan>1)return!1}else if(r=this.getCell(e+i.colspan,t),!r||i.spanned===1||r.spanned===1||r.colspan>1||r.rowspan>1)return!1;return!0}cellCanSpanDown(e,t){if(t===this.rowCount-1)return!1;const i=this.getCell(e,t);if(!i)return!1;let r;if(i.colspan>1){for(let n=e;n1||r.rowspan>1)return!1}else if(r=this.getCell(e,t+i.rowspan),!r||i.spanned===1||r.spanned===1||r.colspan>1||r.rowspan>1)return!1;return!0}cellCanShrinkLeft(e,t){return this.data[t][e].colspan>1}cellCanShrinkUp(e,t){return this.data[t][e].rowspan>1}addColspan(e,t){const i=this.getCell(e,t);if(!i||!this.cellCanSpanRight(e,t))return!1;for(let r=t;r1&&(e+=" colspan = "+n.colspan+` +`),n.rowspan>1&&(e+=" rowspan = "+n.rowspan+` +`),typeof n.column=="number"&&(e+=" colPos = "+n.column+` +`),typeof n.identifier=="string"&&n.identifier.length&&(e+=" identifier = "+n.identifier+` +`),n.slideMode!==void 0&&n.slideMode!==m.none&&(e+=" slideMode = "+n.slideMode.toString()+` +`),e+=` } +`}}e+=` } +`,e+=` } +`}return e+=` } +} +`,e}addVisibilityObserver(e){e.offsetParent===null&&new IntersectionObserver(t=>{t.forEach(i=>{const r=this.codeMirrorRef.value;i.intersectionRatio>0&&r instanceof j&&r.requestUpdate()})}).observe(e)}};f([w({type:Number})],u.prototype,"colCount",void 0),f([w({type:Number})],u.prototype,"rowCount",void 0),f([w({type:Boolean})],u.prototype,"readOnly",void 0),f([w({type:String})],u.prototype,"fieldName",void 0),f([w({type:Array})],u.prototype,"data",void 0),f([w({type:Object})],u.prototype,"codeMirrorConfig",void 0),u=c=f([z("typo3-backend-grid-editor")],u);export{u as GridEditor}; diff --git a/Resources/Public/JavaScript/hashing/md5.js b/Resources/Public/JavaScript/hashing/md5.js new file mode 100644 index 0000000..d11a3c3 --- /dev/null +++ b/Resources/Public/JavaScript/hashing/md5.js @@ -0,0 +1,14 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +/*! Based on http://www.webtoolkit.info/javascript_md5.html */class n{static hash(S){let t,s,c,a,C,e,r,F,o;S=n.utf8Encode(S);const x=n.convertToWordArray(S);for(e=1732584193,r=4023233417,F=2562383102,o=271733878,t=0;t>>32-t}static addUnsigned(S,t){const s=S&2147483648,c=t&2147483648,a=S&1073741824,C=t&1073741824,e=(S&1073741823)+(t&1073741823);return a&C?e^2147483648^s^c:a|C?e&1073741824?e^3221225472^s^c:e^1073741824^s^c:e^s^c}static F(S,t,s){return S&t|~S&s}static G(S,t,s){return S&s|t&~s}static H(S,t,s){return S^t^s}static I(S,t,s){return t^(S|~s)}static FF(S,t,s,c,a,C,e){return S=n.addUnsigned(S,n.addUnsigned(n.addUnsigned(n.F(t,s,c),a),e)),n.addUnsigned(n.rotateLeft(S,C),t)}static GG(S,t,s,c,a,C,e){return S=n.addUnsigned(S,n.addUnsigned(n.addUnsigned(n.G(t,s,c),a),e)),n.addUnsigned(n.rotateLeft(S,C),t)}static HH(S,t,s,c,a,C,e){return S=n.addUnsigned(S,n.addUnsigned(n.addUnsigned(n.H(t,s,c),a),e)),n.addUnsigned(n.rotateLeft(S,C),t)}static II(S,t,s,c,a,C,e){return S=n.addUnsigned(S,n.addUnsigned(n.addUnsigned(n.I(t,s,c),a),e)),n.addUnsigned(n.rotateLeft(S,C),t)}static convertToWordArray(S){let t;const s=S.length,c=s+8,C=((c-c%64)/64+1)*16,e=Array(C-1);let r=0,F=0;for(;F>>29,e}static wordToHex(S){let t="",s="",c,a;for(a=0;a<=3;a++)c=S>>>a*8&255,s="0"+c.toString(16),t=t+s.substr(s.length-2,2);return t}static utf8Encode(S){S=S.replace(/\r\n/g,` +`);let t="";for(let s=0;s127&&c<2048?(t+=String.fromCharCode(c>>6|192),t+=String.fromCharCode(c&63|128)):(t+=String.fromCharCode(c>>12|224),t+=String.fromCharCode(c>>6&63|128),t+=String.fromCharCode(c&63|128))}return t}}export{n as default}; diff --git a/Resources/Public/JavaScript/helper.js b/Resources/Public/JavaScript/helper.js new file mode 100644 index 0000000..084f7ef --- /dev/null +++ b/Resources/Public/JavaScript/helper.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +if(document.currentScript)switch(document.currentScript.dataset.action){case"window.close":window.close();break;default:} diff --git a/Resources/Public/JavaScript/hotkeys.js b/Resources/Public/JavaScript/hotkeys.js new file mode 100644 index 0000000..ff4852c --- /dev/null +++ b/Resources/Public/JavaScript/hotkeys.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import p from"@typo3/backend/hotkeys/hotkey-storage.js";import h from"@typo3/core/event/regular-event.js";import{HotkeyStruct as f}from"@typo3/backend/hotkeys/hotkey-struct.js";import{HotkeyRequestedEvent as y,HotkeyDispatchedEvent as m}from"@typo3/backend/hotkeys/events.js";var a;(function(c){c.META="meta",c.CTRL="control",c.SHIFT="shift",c.ALT="alt"})(a||(a={}));class k{constructor(){this.normalizedCtrlModifierKey=navigator.platform.toLowerCase().startsWith("mac")?a.META:a.CTRL,this.defaultOptions={scope:"all",allowOnEditables:!1,allowRepeat:!1,bindElement:void 0},this.scopedHotkeyMap=p.getScopedHotkeyMap(),this.setScope("all"),this.registerEventHandler()}setScope(t){p.activeScope=t}getScope(){return p.activeScope}register(t,n,o={}){if(t.filter(i=>!Object.values(a).includes(i)).length===0)throw new Error('Attempted to register hotkey "'+t.join("+")+'" without a non-modifier key.');t=t.map(i=>i.toLowerCase());const e={...this.defaultOptions,...o};this.scopedHotkeyMap.has(e.scope)||this.scopedHotkeyMap.set(e.scope,new Map);const s=this.scopedHotkeyMap.get(e.scope),r=f.fromHotkey(t),l=r.toString();if(s.has(l)&&(s.get(l).options.bindElement?.removeAttribute("aria-keyshortcuts"),s.delete(l)),s.set(l,{struct:r,handler:n,options:e}),e.bindElement instanceof Element){const i=e.bindElement.getAttribute("aria-keyshortcuts");let u=this.composeAriaKeyShortcut(t);i!==null&&!i.includes(u)&&(u=i+" "+u),e.bindElement.setAttribute("aria-keyshortcuts",u)}}registerEventHandler(){new h("keydown",t=>{f.fromEvent(t).hasAnyModifier()&&top.document.dispatchEvent(new y(t))}).bindTo(document),new h(m.eventName,t=>{const{keyboardEvent:n}=t,o=this.findHotkeySetup(n);if(o!==null){if(t.preventDefault(),n.repeat&&!o.options.allowRepeat)return;if(!o.options.allowOnEditables){const e=n.target;if(e.isContentEditable||["INPUT","TEXTAREA","SELECT"].includes(e.tagName)&&!n.target.readOnly)return}o.handler(n)}}).bindTo(document)}findHotkeySetup(t){const n=[...new Set(["all",p.activeScope])],e=f.fromEvent(t).toString();for(const s of n){const r=this.scopedHotkeyMap.get(s);if(r.has(e))return r.get(e)}return null}composeAriaKeyShortcut(t){const n=[];for(let o of t)o==="+"?o="plus":o=o.replace(/[\u00A0-\u9999<>&]/g,e=>"&#"+e.charCodeAt(0)+";"),n.push(o);return n.sort((o,e)=>{const s=Object.values(a).includes(o),r=Object.values(a).includes(e);return s&&!r?-1:!s&&r?1:s&&r?-1:0}),n.join("+")}}let d;TYPO3.Hotkeys?d=TYPO3.Hotkeys:(d=new k,TYPO3.Hotkeys=d);var S=d;export{a as ModifierKeys,S as default}; diff --git a/Resources/Public/JavaScript/hotkeys/events.js b/Resources/Public/JavaScript/hotkeys/events.js new file mode 100644 index 0000000..5d6617e --- /dev/null +++ b/Resources/Public/JavaScript/hotkeys/events.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class t extends Event{static{this.eventName="typo3:hotkey:requested"}constructor(e){super(t.eventName,{bubbles:!0,composed:!0,cancelable:!1}),this.keyboardEvent=e}}class s extends Event{static{this.eventName="typo3:hotkey:dispatched"}constructor(e){super(s.eventName,{bubbles:!1,composed:!0,cancelable:!0}),this.keyboardEvent=e}}export{s as HotkeyDispatchedEvent,t as HotkeyRequestedEvent}; diff --git a/Resources/Public/JavaScript/hotkeys/hotkey-storage.js b/Resources/Public/JavaScript/hotkeys/hotkey-storage.js new file mode 100644 index 0000000..9ccc23f --- /dev/null +++ b/Resources/Public/JavaScript/hotkeys/hotkey-storage.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class a{constructor(e=new Map([["all",new Map]]),t="all"){this.scopedHotkeyMap=e,this.activeScope=t}getScopedHotkeyMap(){return this.scopedHotkeyMap}}var o=new a;export{o as default}; diff --git a/Resources/Public/JavaScript/hotkeys/hotkey-struct.js b/Resources/Public/JavaScript/hotkeys/hotkey-struct.js new file mode 100644 index 0000000..a25d230 --- /dev/null +++ b/Resources/Public/JavaScript/hotkeys/hotkey-struct.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +var i;(function(e){e.META="meta",e.CTRL="control",e.SHIFT="shift",e.ALT="alt"})(i||(i={}));class r{constructor(t,n,s,o,a){this.ctrl=t,this.meta=n,this.alt=s,this.shift=o,this.key=a}static fromEvent(t){return new r(t.ctrlKey,t.metaKey,t.altKey,t.shiftKey,t.key.toLowerCase())}static fromHotkey(t){const n=t.filter(s=>!Object.values(i).includes(s));if(n.length>1)throw new Error('Cannot create HotkeyStruct with more than one non-modifier key, "'+n.join("+")+'" given.');return new r(t.includes(i.CTRL),t.includes(i.META),t.includes(i.ALT),t.includes(i.SHIFT),n[0].toLowerCase())}hasAnyModifier(){return this.ctrl||this.meta||this.alt||this.shift}toString(){return JSON.stringify(this)}}export{r as HotkeyStruct}; diff --git a/Resources/Public/JavaScript/hotkeys/negotiator.js b/Resources/Public/JavaScript/hotkeys/negotiator.js new file mode 100644 index 0000000..8807e79 --- /dev/null +++ b/Resources/Public/JavaScript/hotkeys/negotiator.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{HotkeyRequestedEvent as o,HotkeyDispatchedEvent as r}from"@typo3/backend/hotkeys/events.js";class s{constructor(){this.registerEventHandler()}registerEventHandler(){document.addEventListener(o.eventName,t=>{const e=new r(t.keyboardEvent);for(const n of this.collectDocuments())if(n.dispatchEvent(e)===!1)break})}collectDocuments(){const t=[document];for(let e=0;e{const o=await new p(TYPO3.settings.ajaxUrls.icons).withQueryArguments({icon:JSON.stringify(t)}).get({signal:s}),i=await o.resolve();return!o.response.redirected&&i.startsWith("')&&e.set("icon_"+n,i),i},c)}fetchFromLocal(t){return e.isset("icon_"+t)?Promise.resolve(e.get("icon_"+t)):Promise.reject()}}let r;r||(r=new v,typeof TYPO3<"u"&&(TYPO3.Icons=r));var w=r;export{z as IconStyles,w as default}; diff --git a/Resources/Public/JavaScript/image-manipulation.js b/Resources/Public/JavaScript/image-manipulation.js new file mode 100644 index 0000000..00d0f4c --- /dev/null +++ b/Resources/Public/JavaScript/image-manipulation.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as f}from"lit";import{unsafeHTML as S}from"lit/directives/unsafe-html.js";import{styleMap as m}from"lit/directives/style-map.js";import O from"@typo3/core/ajax/ajax-request.js";import p from"@typo3/core/event/regular-event.js";import V from"@typo3/backend/form-engine.js";import w from"cropperjs";import v from"@typo3/backend/modal.js";import"@typo3/backend/element/spinner-element.js";import{renderNodes as x}from"@typo3/core/lit-helper.js";import{topLevelModuleImport as M}from"@typo3/backend/utility/top-level-module-import.js";import{Offset as P}from"@typo3/backend/offset.js";import b from"@typo3/backend/storage/browser-session.js";class c{constructor(){this.syncAvailable=!1,this.syncEnabled=!1,this.initialized=!1,this.triggerListener=null,this.cropImageSelector="#t3js-crop-image",this.coverAreaSelector=".t3js-cropper-cover-area",this.cropInfoSelector=".t3js-cropper-info-crop",this.focusAreaSelector="typo3-backend-draggable-resizable",this.focusAreaVisualElementSelector="typo3-backend-draggable-resizable .cropper-focus-area",this.defaultFocusArea={height:1/3,width:1/3,x:0,y:0},this.defaultOpts={autoCrop:!0,autoCropArea:.7,dragMode:"crop",guides:!0,responsive:!0,viewMode:1,zoomable:!1,checkCrossOrigin:!1},this.cropBuiltHandler=()=>{this.initialized=!0;const t=this.cropper.getImageData(),e=this.currentModal.querySelector(this.cropImageSelector);this.currentModal.querySelector(".cropper-canvas img")?.classList.remove("cropper-hide"),this.imageOriginalSizeFactor=parseInt(e.dataset.originalWidth,10)/t.naturalWidth,this.cropVariantTriggers.forEach(r=>{const a=r.dataset.cropVariantId,s=this.convertRelativeToAbsoluteCropArea(this.data[a].cropArea,t),i=Object.assign({},this.data[a],{cropArea:s});this.updatePreviewThumbnail(i,r),this.currentModal.querySelector(`[data-crop-variant-container="${i.id}"]`)?.querySelector(`[data-bs-option="${i.selectedRatio}"]`)?.classList.add("active")}),this.currentCropVariant.cropArea=this.convertRelativeToAbsoluteCropArea(this.currentCropVariant.cropArea,t),this.cropBox=this.currentModal.querySelector(".cropper-crop-box"),this.currentCropVariant.selectedRatio&&this.currentModal.querySelector(`[data-bs-option='${this.currentCropVariant.selectedRatio}']`)?.classList.add("active")},this.cropMoveHandler=t=>{if(!this.initialized)return;const e=15,r=15;let a=Math.floor(t.detail.width),s=Math.floor(t.detail.height);if((a{let h=null;this.cropVariantTriggers.forEach(d=>{d.dataset.cropVariantId===l&&(h=d)});const u=h.dataset.excludeFromSync.toLowerCase()==="true";if(h&&!u){const d=Object.assign({},n[l],{cropArea:this.currentCropVariant.cropArea,cropVariant:l});this.updatePreviewThumbnail(d,h),this.updateCropVariantData(d)}})}this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger),this.updateCropVariantData(this.currentCropVariant);const i=Math.round(this.currentCropVariant.cropArea.width*this.imageOriginalSizeFactor),o=Math.round(this.currentCropVariant.cropArea.height*this.imageOriginalSizeFactor);this.cropInfo.innerText=`${i}\xD7${o} px`}}static wait(t,e){window.setTimeout(t,e)}static toCssPercent(t){return`${t*100}%`}static serializeCropVariants(t){return JSON.stringify(t,(r,a)=>r==="id"||r==="title"||r==="allowedAspectRatios"||r==="coverAreas"?void 0:a)}static isEmptyObject(t){return!t||typeof t!="object"||Object.keys(t).length===0||JSON.stringify(t)==="{}"}static resolvePointerEventNames(){const t=typeof window<"u"&&typeof window.document<"u",e=t&&window.document.documentElement?"ontouchstart"in window.document.documentElement:!1,r=t?"PointerEvent"in window:!1,a=e?["touchmove"]:["mousemove"],s=e?["touchstart"]:["mousedown"],i=e?["touchend","touchcancel"]:["mouseup"];return{touchStart:s,touchMove:a,touchEnd:i,pointerDown:r?["pointerdown"]:s,pointerMove:r?["pointermove"]:a,pointerUp:r?["pointerup","pointercancel"]:i}}initializeTrigger(){this.triggerListener||(this.triggerListener=new p("click",(t,e)=>{t.preventDefault(),this.trigger=e,this.show()}),this.triggerListener.delegateTo(document,".t3js-image-manipulation-trigger"))}async initializeCropperModal(){const t=this.currentModal.querySelector(this.cropImageSelector);await new Promise(e=>{t.complete?e():t.addEventListener("load",()=>e())}),this.init()}show(){const t=this.trigger.dataset,e=t.modalTitle,r=t.buttonPreviewText,a=t.buttonDismissText,s=t.buttonSaveText,i=t.url,o=JSON.parse(t.payload);this.currentModal=v.advanced({additionalCssClasses:["modal-image-manipulation","cropper"],buttons:[{btnClass:"btn-default float-start",name:"preview",icon:"actions-view",text:r},{btnClass:"btn-default",name:"dismiss",icon:"actions-close",text:a},{btnClass:"btn-primary",name:"save",icon:"actions-document-save",text:s}],content:f``,size:v.sizes.full,style:v.styles.dark,title:e,staticBackdrop:!0}),this.currentModal.addEventListener("typo3-modal-shown",()=>{new O(i).post(o).then(async n=>{const l=await n.resolve();this.currentModal.templateResultContent=f`${S(l)}`,this.currentModal.updateComplete.then(()=>this.initializeCropperModal())})}),this.currentModal.addEventListener("typo3-modal-hide",()=>{this.destroy()})}init(){const t=this.currentModal.querySelector(this.cropImageSelector),e=this.trigger.dataset.cropVariants;if(!e)throw new TypeError("ImageManipulation: No cropVariants data found for image");this.data=c.isEmptyObject(this.data)?JSON.parse(e):this.data,this.cropVariantTriggers=this.currentModal.querySelectorAll(".t3js-crop-variant-trigger"),this.activeCropVariantTrigger=this.currentModal.querySelector(".t3js-crop-variant-trigger.is-active"),this.cropInfo=this.currentModal.querySelector(this.cropInfoSelector),this.currentCropVariant=this.data[this.activeCropVariantTrigger.dataset.cropVariantId],this.cropVariantTriggers.forEach(a=>a.addEventListener("click",s=>{if(s.currentTarget.classList.contains("is-active")){s.stopPropagation(),s.preventDefault();return}this.activeCropVariantTrigger.classList.remove("is-active"),s.currentTarget.classList.add("is-active"),this.activeCropVariantTrigger=s.currentTarget;const i=this.data[this.activeCropVariantTrigger.dataset.cropVariantId],o=this.cropper.getImageData();i.cropArea=this.convertRelativeToAbsoluteCropArea(i.cropArea,o),this.currentCropVariant=Object.assign({},i),this.update(i)})),new p("click",(a,s)=>{const i=s.dataset.bsOption;this.handleAspectRatioChange(i)}).delegateTo(this.currentModal,"label[data-method=setAspectRatio]"),new p("keydown",(a,s)=>{if(!["Enter","Space"].includes(a.code))return;a.preventDefault(),a.stopImmediatePropagation();const i=s.closest('label[data-method="setAspectRatio"]'),o=i.dataset.bsOption;i.querySelector("input").checked=!0,this.handleAspectRatioChange(o)}).delegateTo(this.currentModal,'label[data-method="setAspectRatio"] input[type="radio"]'),new p("click",()=>this.save(this.data)).delegateTo(this.currentModal,"button[name=save]"),this.trigger.dataset.previewUrl?new p("click",()=>this.openPreview(this.data)).delegateTo(this.currentModal,"button[name=preview]"):this.currentModal.querySelectorAll("button[name=preview]").forEach(a=>a.style.display="none"),new p("click",()=>this.currentModal.hideModal()).delegateTo(this.currentModal,"button[name=dismiss]"),new p("click",(a,s)=>{const i=this.cropper.getImageData(),o=s.dataset.cropVariant;if(a.preventDefault(),a.stopPropagation(),!o)throw new TypeError("TYPO3 Cropper: No cropVariant data attribute found on reset element.");const n=JSON.parse(o),l=this.convertRelativeToAbsoluteCropArea(n.cropArea,i);this.currentCropVariant=Object.assign({},n,{cropArea:l}),this.update(this.currentCropVariant)}).delegateTo(this.currentModal,"button[name=reset]"),c.isEmptyObject(this.currentCropVariant.cropArea)&&(this.defaultOpts=Object.assign({autoCropArea:1},this.defaultOpts)),this.cropper=new w(t,Object.assign({},this.defaultOpts,{ready:()=>{this.cropBuiltHandler(),this.update(this.currentCropVariant)},crop:this.cropMoveHandler,data:this.currentCropVariant.cropArea})),this.syncAvailable=this.trigger.dataset.syncAvailable.toLowerCase()==="true";const r=this.trigger.dataset.imageUid;if(this.syncAvailable&&(this.currentModal.querySelector("#sync-crop-variants-container").classList.remove("d-none"),new p("click",(s,i)=>{if(s.stopPropagation(),this.syncEnabled=i.checked,b.set("sync-active-"+r,this.syncEnabled?"true":"false"),this.syncEnabled){if(this.currentModal.querySelectorAll('.panel-button[data-exclude-from-sync="true"]').forEach(n=>n.setAttribute("disabled","disabled")),this.currentModal.querySelector('.panel-button[data-crop-variant-id="'+this.currentCropVariant.id+'"][data-exclude-from-sync="true"]')!==null){const n=this.currentModal.querySelector('.panel-button[data-exclude-from-sync="false"]');n&&n.click()}}else this.currentModal.querySelectorAll(".panel-button[data-exclude-from-sync]").forEach(o=>o.removeAttribute("disabled"))}).delegateTo(this.currentModal,"#sync-crop-variants"),(b.get("sync-active-"+r)??"false")==="true")){const s=this.currentModal.querySelector("#sync-crop-variants");s&&window.setTimeout(()=>{s.click()},100)}}handleAspectRatioChange(t){const e=Object.assign({},this.currentCropVariant),r=e.allowedAspectRatios[t];if(this.setAspectRatio(r),this.setCropArea(e.cropArea),this.currentCropVariant=Object.assign({},e,{selectedRatio:t}),this.syncAvailable&&this.syncEnabled){const a=this.data;Object.keys(this.data).forEach(s=>{let i=null;this.cropVariantTriggers.forEach(n=>{n.dataset.cropVariantId===s&&(i=n)});const o=i.dataset.excludeFromSync.toLowerCase()==="true";if(i&&!o){const n=Object.assign({},a[s],{selectedRatio:t,cropArea:e.cropArea});this.update(n),this.updatePreviewThumbnail(n,i)}})}this.update(this.currentCropVariant)}async update(t){const e=Object.assign({},t),r=t.allowedAspectRatios[t.selectedRatio];this.cropInfo=this.currentModal.querySelector(`[data-crop-variant-container="${t.id}"]`)?.querySelector(this.cropInfoSelector),this.currentModal.querySelector(`[data-crop-variant-container="${t.id}"]`)?.querySelector("[data-bs-option].active")?.classList.remove("active"),this.currentModal.querySelector(`[data-crop-variant-container="${t.id}"]`)?.querySelector(`[data-bs-option="${t.selectedRatio}"]`)?.classList.add("active"),this.setAspectRatio(r),this.setCropArea(e.cropArea),this.currentCropVariant=Object.assign({},e,t),this.cropBox?.querySelectorAll(this.coverAreaSelector)?.forEach(a=>a.remove()),this.cropBox?.querySelectorAll(this.focusAreaSelector)?.forEach(a=>a.remove()),t.focusArea?(c.isEmptyObject(t.focusArea)&&(this.currentCropVariant.focusArea=Object.assign({},this.defaultFocusArea)),this.focusAreaEl=this.initFocusArea(this.cropBox)):this.focusAreaEl=null,t.coverAreas&&this.initCoverAreas(this.cropBox,this.currentCropVariant.coverAreas),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger)}initFocusArea(t){M("@typo3/backend/element/draggable-resizable-element.js");const e=top.document.createElement("typo3-backend-draggable-resizable");return e.setAttribute("offset",JSON.stringify(this.convertAreaToOffset(this.currentCropVariant.focusArea,t))),e.setAttribute("pointereventnames",JSON.stringify(c.resolvePointerEventNames())),e.addEventListener("draggable-resizable-started",()=>{this.cropper.disable()}),e.addEventListener("draggable-resizable-updated",()=>{const r=this.currentCropVariant.coverAreas,a=this.convertOffsetToArea(e.offset,t),s=e.querySelector(this.focusAreaVisualElementSelector);this.checkFocusAndCoverAreasCollision(a,r)?s.classList.add("has-nodrop"):s.classList.remove("has-nodrop")}),e.addEventListener("draggable-resizable-finished",r=>{const a=this.currentCropVariant.coverAreas,s=this.convertOffsetToArea(e.offset,t);this.checkFocusAndCoverAreasCollision(s,a)?e.revert(r.detail.originOffset):this.scaleAndMoveFocusArea(s),e.querySelector(this.focusAreaVisualElementSelector).classList.remove("has-nodrop"),this.cropper.enable()}),t.appendChild(e),this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea),e}initCoverAreas(t,e){e.forEach(r=>{const a={height:c.toCssPercent(r.height),left:c.toCssPercent(r.x),top:c.toCssPercent(r.y),width:c.toCssPercent(r.width)},s=f`
    `;this.renderElements(s,t)})}updatePreviewThumbnail(t,e){const r=e.querySelector(".t3js-cropper-preview-thumbnail-crop-area"),a=e.querySelector(".t3js-cropper-preview-thumbnail-crop-image"),s=e.querySelector(".t3js-cropper-preview-thumbnail-focus-area"),i=this.cropper.getImageData();Object.assign(r.style,{height:c.toCssPercent(t.cropArea.height/i.naturalHeight),left:c.toCssPercent(t.cropArea.x/i.naturalWidth),top:c.toCssPercent(t.cropArea.y/i.naturalHeight),width:c.toCssPercent(t.cropArea.width/i.naturalWidth)}),t.focusArea&&Object.assign(s.style,{height:c.toCssPercent(t.focusArea.height),left:c.toCssPercent(t.focusArea.x),top:c.toCssPercent(t.focusArea.y),width:c.toCssPercent(t.focusArea.width)});const o=getComputedStyle(r),n={width:o.getPropertyValue("width"),height:o.getPropertyValue("height"),left:o.getPropertyValue("left"),top:o.getPropertyValue("top")};Object.assign(a.style,{height:`${parseFloat(n.height)*(1/(t.cropArea.height/i.naturalHeight))}px`,margin:`${-1*parseFloat(n.left)}px`,marginTop:`${-1*parseFloat(n.top)}px`,width:`${parseFloat(n.width)*(1/(t.cropArea.width/i.naturalWidth))}px`})}scaleAndMoveFocusArea(t){this.currentCropVariant.focusArea=t,this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger),this.updateCropVariantData(this.currentCropVariant)}updateCropVariantData(t){const e=this.cropper.getImageData(),r=this.convertAbsoluteToRelativeCropArea(t.cropArea,e);this.data[t.id]=Object.assign({},t,{cropArea:r})}setAspectRatio(t){this.cropper.setAspectRatio(t.value)}setCropArea(t){const e=this.currentCropVariant.allowedAspectRatios[this.currentCropVariant.selectedRatio];e.value===0?this.cropper.setData({height:t.height,width:t.width,x:t.x,y:t.y}):this.cropper.setData({height:t.height,width:t.height*e.value,x:t.x,y:t.y})}checkFocusAndCoverAreasCollision(t,e){return e?e.some(r=>t.x{const s=t[a],i=this.convertRelativeToAbsoluteCropArea(s.cropArea,r),o=this.trigger.closest(".form-group").querySelector(`.t3js-image-manipulation-preview[data-crop-variant-id="${a}"]`),n=this.trigger.closest(".form-group").querySelector(`.t3js-image-manipulation-selected-ratio[data-crop-variant-id="${a}"]`);if(!(o instanceof HTMLElement))return;let l=o.getBoundingClientRect().width,h=parseInt(o.dataset.previewHeight,10);const u=i.width/i.height,d=l/u;d>h?l=h*u:h=d,l>i.width&&(l=i.width,h=i.height);const g=l/i.width,C={height:`${r.naturalHeight*g}px`,left:`${-i.x*g}px`,top:`${-i.y*g}px`,width:`${r.naturalWidth*g}px`},A={width:`${l}px`,height:`${h}px`},T=f`
    `;for(;o.firstChild;)o.removeChild(o.firstChild);this.renderElements(T,o);const E=this.currentModal.ownerDocument.defaultView,y=this.currentModal.querySelector(`.t3-js-ratio-title[data-ratio-id="${s.id}${s.selectedRatio}"]`);n instanceof HTMLElement&&y instanceof E.HTMLElement&&(n.innerText=y.innerText)})}openPreview(t){const e=c.serializeCropVariants(t);let r=this.trigger.dataset.previewUrl;r=r+(r.includes("?")?"&":"?")+"cropVariants="+encodeURIComponent(e),window.open(r,"TYPO3ImageManipulationPreview")}save(t){const e=c.serializeCropVariants(t),r=document.querySelector(`#${this.trigger.dataset.field}`);this.trigger.dataset.cropVariants=JSON.stringify(t),this.setPreviewImages(t),r.value=e,V.markFieldAsChanged(r),this.currentModal.hideModal()}destroy(){this.currentModal&&(this.cropper instanceof w&&this.cropper.destroy(),this.initialized=!1,this.cropper=null,this.currentModal=null,this.data=null)}convertAreaToOffset(t,e){const r=e.getBoundingClientRect();return new P(t.x*r.width,t.y*r.height,t.width*r.width,t.height*r.height)}convertOffsetToArea(t,e){const r=e.getBoundingClientRect();return{x:t.left/r.width,y:t.top/r.height,width:t.width/r.width,height:t.height/r.height}}renderElements(t,e,r){const a=x(t);return Array.from(a).filter(i=>i instanceof HTMLElement).forEach(i=>e.appendChild(i)),r?e.querySelector(r):null}}var R=new c;export{R as default}; diff --git a/Resources/Public/JavaScript/info-window.js b/Resources/Public/JavaScript/info-window.js new file mode 100644 index 0000000..3cf889e --- /dev/null +++ b/Resources/Public/JavaScript/info-window.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{SeverityEnum as i}from"@typo3/backend/enum/severity.js";import e from"@typo3/backend/modal.js";class t{static showItem(n,o){e.advanced({type:e.types.iframe,size:e.sizes.large,content:top.TYPO3.settings.ShowItem.moduleUrl+"&table="+encodeURIComponent(n)+"&uid="+(typeof o=="number"?o:encodeURIComponent(o)),severity:i.notice})}}top.TYPO3.InfoWindow||(top.TYPO3.InfoWindow=t),TYPO3.InfoWindow=t;export{t as default}; diff --git a/Resources/Public/JavaScript/input/clearable.js b/Resources/Public/JavaScript/input/clearable.js new file mode 100644 index 0000000..867871a --- /dev/null +++ b/Resources/Public/JavaScript/input/clearable.js @@ -0,0 +1,23 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class c{constructor(){typeof HTMLInputElement.prototype.clearable!="function"&&this.registerClearable()}static createCloseButton(t){const l=` + + + + + + + + + + `,e=document.createElement("button");return e.type="button",e.tabIndex=-1,e.title=t,e.ariaLabel=t,e.innerHTML=l,e.style.visibility="hidden",e.classList.add("close"),e}registerClearable(){HTMLInputElement.prototype.clearable=async function(t={}){if(this.isClearable)return;if(typeof t!="object")throw new Error("Passed options must be an object, "+typeof t+" given");this.classList.add("form-control-clearable");const l=document.activeElement===this,e=document.createElement("div");e.classList.add("form-control-clearable-wrapper"),this.parentNode.insertBefore(e,this),e.appendChild(this);let i="Clear input";if(this.dataset.clearableLabel)i=this.dataset.clearableLabel;else{const{default:s}=await import("~labels/core.core");i=s.get("labels.inputfield.clearButton.title")}const n=c.createCloseButton(i),a=()=>{n.style.visibility=this.value.length===0?"hidden":"visible"};n.addEventListener("click",s=>{s.preventDefault(),this.value="",typeof t.onClear=="function"&&t.onClear(this),this.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0})),this.dispatchEvent(new CustomEvent("typo3:internal:clear")),a(),this.focus()}),e.appendChild(n),this.addEventListener("blur",s=>{this.parentNode.contains(s.relatedTarget)&&this.focus()}),this.addEventListener("focus",a),this.addEventListener("keyup",a),a(),this.isClearable=!0,l&&this.focus()}}}var r=new c;export{r as default}; diff --git a/Resources/Public/JavaScript/java-script-module-import-event-handler.js b/Resources/Public/JavaScript/java-script-module-import-event-handler.js new file mode 100644 index 0000000..3c234eb --- /dev/null +++ b/Resources/Public/JavaScript/java-script-module-import-event-handler.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +document.addEventListener("typo3:import-javascript-module",t=>{t.detail.importPromise=import(t.detail.specifier)}); diff --git a/Resources/Public/JavaScript/layout-module/drag-drop.js b/Resources/Public/JavaScript/layout-module/drag-drop.js new file mode 100644 index 0000000..7845449 --- /dev/null +++ b/Resources/Public/JavaScript/layout-module/drag-drop.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import w from"@typo3/core/document-service.js";import I from"@typo3/backend/ajax-data-handler.js";import v from"@typo3/backend/icons.js";import s from"@typo3/core/event/regular-event.js";import{DataTransferTypes as f}from"@typo3/backend/enum/data-transfer-types.js";import S from"@typo3/backend/broadcast-service.js";import{BroadcastMessage as P}from"@typo3/backend/broadcast-message.js";import m from"@typo3/backend/utility/drag-drop-utility.js";var o;(function(r){r.content=".t3js-page-ce",r.draggableContentHandle='.t3js-page-ce-header[draggable="true"]',r.dropZone=".t3js-page-ce-dropzone-available",r.column=".t3js-page-column",r.addContent=".t3js-page-new-ce"})(o||(o={}));var g;(function(r){r.validDropZoneClass="active",r.dropPossibleHoverClass="t3-page-ce-dropzone-possible"})(g||(g={}));class Z{constructor(){w.ready().then(()=>{this.initialize()})}initialize(){new s("mousedown",(e,t)=>{const n=e.target.closest("a,img");n!==null&&t.contains(n)}).delegateTo(document,o.draggableContentHandle),new s("dragstart",this.onDragStart.bind(this)).delegateTo(document,o.draggableContentHandle),new s("dragenter",this.onDragEnter.bind(this)).delegateTo(document,o.draggableContentHandle),new s("dragend",this.onDragEnd.bind(this)).delegateTo(document,o.draggableContentHandle),new s("dragenter",(e,t)=>{t.classList.add(g.dropPossibleHoverClass),m.updateEventAndTooltipToReflectCopyMoveIntention(e)}).delegateTo(document,o.dropZone),new s("dragover",e=>{e.preventDefault(),m.updateEventAndTooltipToReflectCopyMoveIntention(e)}).delegateTo(document,o.dropZone),new s("dragleave",(e,t)=>{e.preventDefault(),t.classList.remove(g.dropPossibleHoverClass)}).delegateTo(document,o.dropZone),new s("drop",this.onDrop.bind(this),{capture:!0,passive:!0}).delegateTo(document,o.dropZone),new s("typo3:page-layout-drag-drop:elementChanged",this.onBroadcastElementChanged.bind(this)).bindTo(top.document)}onDragEnter(e){e.preventDefault(),m.updateEventAndTooltipToReflectCopyMoveIntention(e),this.showDropZones()}onDragStart(e,t){const n=t.closest(o.content);e.dataTransfer.setData(f.content,JSON.stringify({pid:this.getCurrentPageId(),uid:parseInt(n.dataset.uid,10),language:parseInt(n.dataset.languageUid,10),content:n.outerHTML,moveElementUrl:n.dataset.moveElementUrl}));const d=this.getDragTooltipMetadataFromContentElement(n);e.dataTransfer.setData(f.dragTooltip,JSON.stringify(d)),e.dataTransfer.effectAllowed="copyMove",m.updateEventAndTooltipToReflectCopyMoveIntention(e),n.querySelector(o.dropZone).hidden=!0}onDragEnd(){this.hideDropZones()}onDrop(e,t){let n;if(t.classList.remove(g.dropPossibleHoverClass),!e.dataTransfer.types.includes(f.content))return;const d=this.getColumnPositionForElement(t),a=JSON.parse(e.dataTransfer.getData(f.content));if(n=document.querySelector(`${o.content}[data-uid="${a.uid}"]`),n||(n=document.createRange().createContextualFragment(a.content).firstElementChild),typeof a.uid=="number"&&a.uid>0){const u={},i=t.closest(o.content).dataset.uid;let l;i===void 0?l=parseInt(t.closest("[data-page]").dataset.page,10):l=0-parseInt(i,10);let c=a.language;c!==-1&&(c=parseInt(t.closest("[data-language-uid]").dataset.languageUid,10));let p=0;l!==0&&(p=d);const h=m.isCopyModifierFromEvent(e)||t.classList.contains("t3js-paste-copy"),T=h?"copy":"move";u.cmd={tt_content:{[a.uid]:{[T]:{action:"paste",target:l,update:{colPos:p,sys_language_uid:c}}}}},this.ajaxAction(u,h).then(()=>{t.parentElement.classList.contains(o.content.substring(1))?t.closest(o.content).after(n):t.closest(o.dropZone).after(n),this.broadcast("elementChanged",{pid:a.pid,uid:a.uid,targetPid:this.getCurrentPageId(),action:h?"copy":"move"});const y=document.querySelector(`.t3-page-column-lang-name[data-language-uid="${c}"]`);if(y===null)return;const C=y.dataset.flagIdentifier,b=y.dataset.languageTitle;v.getIcon(C,v.sizes.small).then(E=>{const D=n.querySelector(".t3js-flag");D.title=b,D.innerHTML=E})})}}onBroadcastElementChanged(e){e.detail.payload.pid===this.getCurrentPageId()&&e.detail.payload.targetPid!==e.detail.payload.pid&&e.detail.payload.action==="move"&&document.querySelector(`${o.content}[data-uid="${e.detail.payload.uid}"]`).remove()}ajaxAction(e,t){const n=Object.keys(e.cmd).shift(),d=parseInt(Object.keys(e.cmd[n]).shift(),10),a={component:"dragdrop",action:t?"copy":"move",table:n,uid:d},u=document.querySelector(".t3-grid-container");return I.process(e,a).then(i=>{if(i.hasErrors)throw i.messages;(t||u?.dataset.multiLanguages==="1")&&self.location.reload()})}getColumnPositionForElement(e){const t=e.closest("[data-colpos]");return t!==null&&t.dataset.colpos!==void 0?parseInt(t.dataset.colpos,10):!1}getDragTooltipMetadataFromContentElement(e){let t,n;const d=[],u=e.querySelector(".t3-page-ce-header-title").innerText,i=e.querySelector(".element-preview");i&&(t=i.innerText,t.length>80&&(t=t.substring(0,80)+"..."));const l=e.querySelector(".t3js-icon");l&&(n=l.dataset.identifier);const c=e.querySelectorAll(".preview-thumbnails-element-image img");return c.length>0&&c.forEach(p=>{d.push({src:p.src,height:p.height,width:p.width})}),{statusIconIdentifier:"actions-move",tooltipIconIdentifier:n,tooltipLabel:u,tooltipDescription:t,thumbnails:d}}getCurrentPageId(){return parseInt(document.querySelector("[data-page]").dataset.page,10)}broadcast(e,t){S.post(new P("page-layout-drag-drop",e,t||{}))}showDropZones(){document.querySelectorAll(o.dropZone).forEach(e=>{e.hidden=!1;const t=e.parentElement.querySelector(o.addContent);t!==null&&(t.hidden=!0,e.classList.add(g.validDropZoneClass))})}hideDropZones(){document.querySelectorAll(o.dropZone).forEach(e=>{e.hidden=!0;const t=e.parentElement.querySelector(o.addContent);t!==null&&(t.hidden=!1),e.classList.remove(g.validDropZoneClass)})}}var q=new Z;export{q as default}; diff --git a/Resources/Public/JavaScript/layout-module/page-layout-event.js b/Resources/Public/JavaScript/layout-module/page-layout-event.js new file mode 100644 index 0000000..ba02742 --- /dev/null +++ b/Resources/Public/JavaScript/layout-module/page-layout-event.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class t extends CustomEvent{static{this.eventName="typo3:page-layout:hidden-content-count-changed"}constructor(e){super(t.eventName,{detail:{count:e}})}}export{t as HiddenContentCountChangedEvent}; diff --git a/Resources/Public/JavaScript/layout-module/page-layout.js b/Resources/Public/JavaScript/layout-module/page-layout.js new file mode 100644 index 0000000..fe1b272 --- /dev/null +++ b/Resources/Public/JavaScript/layout-module/page-layout.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import p from"@typo3/core/ajax/ajax-request.js";import f from"@typo3/backend/notification.js";import b from"@typo3/core/event/regular-event.js";import{sudoModeInterceptor as y}from"@typo3/backend/security/sudo-mode-interceptor.js";import r from"~labels/backend.layout";import{HiddenContentCountChangedEvent as h}from"@typo3/backend/layout-module/page-layout-event.js";import{HiddenContentCountChangedEvent as L}from"@typo3/backend/layout-module/page-layout-event.js";import"@typo3/backend/element/icon-element.js";new b("click",async(s,d)=>{s.preventDefault(),s.stopPropagation();const i=d;if(i.disabled)return;i.disabled=!0;const e=i.closest(".t3js-page-ce");if(e===null){i.disabled=!1;return}const c=e.dataset.table,l=parseInt(e.dataset.uid??"0",10),g=e.classList.contains("t3js-hidden-record"),u=i.querySelector("typo3-backend-icon");try{const t=await(await new p(TYPO3.settings.ajaxUrls.record_toggle_visibility).addMiddleware(y).post({table:c,uid:l,action:g?"show":"hide"})).resolve();if(e.classList.toggle("t3-page-ce-hidden",!t.isVisible),e.classList.toggle("t3js-hidden-record",!t.isVisible),t.isVisible)e.style.display="";else{const a=document.querySelector("typo3-backend-page-layout-toggle-hidden");a&&!a.active&&(e.style.display="none")}u?.setAttribute("identifier",t.isVisible?"actions-edit-hide":"actions-edit-unhide"),i.title=t.isVisible?r.get("hide"):r.get("unHide");const o=e.querySelector(".t3-page-ce-header-left [data-contextmenu-trigger] .t3js-icon");o&&t.icon&&o.replaceWith(document.createRange().createContextualFragment(t.icon));const m=document.querySelectorAll(".t3js-hidden-record").length;document.dispatchEvent(new h(m))}catch(n){if(n&&typeof n.resolve=="function"){const t=await n.resolve();for(const o of t.messages??[])f.error(o.title,o.message)}}finally{i.disabled=!1}}).delegateTo(document,'button[data-action="content-element-visibility-toggle"]');export{L as HiddenContentCountChangedEvent}; diff --git a/Resources/Public/JavaScript/layout-module/paste.js b/Resources/Public/JavaScript/layout-module/paste.js new file mode 100644 index 0000000..140337b --- /dev/null +++ b/Resources/Public/JavaScript/layout-module/paste.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import d from"@typo3/core/document-service.js";import m from"@typo3/backend/ajax-data-handler.js";import u from"@typo3/backend/modal.js";import h from"@typo3/backend/severity.js";import"@typo3/backend/element/icon-element.js";import{SeverityEnum as p}from"@typo3/backend/enum/severity.js";import g from"@typo3/core/event/regular-event.js";import a from"~labels/backend.layout";class l{constructor(t){this.itemOnClipboardUid=0,this.itemOnClipboardTitle="",this.copyMode="",this.elementIdentifier=".t3js-page-ce",this.pasteAfterLinkTemplate="",this.pasteIntoLinkTemplate="",this.itemOnClipboardUid=t.itemOnClipboardUid,this.itemOnClipboardTitle=t.itemOnClipboardTitle,this.copyMode=t.copyMode,d.ready().then(()=>{document.querySelectorAll(".t3js-page-columns").length>0&&(this.generateButtonTemplates(),this.activatePasteIcons(),this.initializeEvents())})}static determineColumn(t){const e=t.closest("[data-colpos]");return parseInt(e?.dataset?.colpos??"0",10)}initializeEvents(){new g("click",(t,e)=>{t.preventDefault(),this.activatePasteModal(e)}).delegateTo(document,".t3js-paste")}generateButtonTemplates(){this.itemOnClipboardUid&&(this.pasteAfterLinkTemplate='',this.pasteIntoLinkTemplate='')}activatePasteIcons(){this.pasteAfterLinkTemplate&&this.pasteIntoLinkTemplate&&document.querySelectorAll(".t3js-page-new-ce").forEach(t=>{const e=t.parentElement.dataset.page?this.pasteIntoLinkTemplate:this.pasteAfterLinkTemplate;t.append(document.createRange().createContextualFragment(e))})}activatePasteModal(t){const e=a.get("paste.modal.title.paste")+': "'+this.itemOnClipboardTitle+'"',i=a.get("paste.modal.paste");let s=[];s=[{text:a.get("paste.modal.button.cancel"),active:!0,btnClass:"btn-default",trigger:(n,o)=>o.hideModal()},{text:a.get("paste.modal.button.paste"),btnClass:"btn-"+h.getCssClass(p.warning),trigger:(n,o)=>{o.hideModal(),this.execute(t)}}],u.show(e,i,p.warning,s)}execute(t){const e=l.determineColumn(t),i=t.closest(this.elementIdentifier),s=i.dataset.uid;let n;typeof s>"u"?n=parseInt(i.dataset.page,10):n=0-parseInt(s,10);const o=parseInt(t.closest("[data-language-uid]").dataset.languageUid,10),r={CB:{paste:"tt_content|"+n,pad:"normal",update:{colPos:e,sys_language_uid:o}}};m.process(r).then(c=>{c.hasErrors||window.location.reload()})}}export{l as default}; diff --git a/Resources/Public/JavaScript/layout-module/toggle-hidden-element.js b/Resources/Public/JavaScript/layout-module/toggle-hidden-element.js new file mode 100644 index 0000000..92cb78d --- /dev/null +++ b/Resources/Public/JavaScript/layout-module/toggle-hidden-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as h}from"lit";import{property as c,customElement as p}from"lit/decorators.js";import{PseudoButtonLitElement as m}from"@typo3/backend/element/pseudo-button.js";import y from"@typo3/backend/storage/persistent.js";import f from"~labels/backend.layout";import"@typo3/backend/element/icon-element.js";import{HiddenContentCountChangedEvent as u}from"@typo3/backend/layout-module/page-layout-event.js";var l=function(r,t,n,e){var i=arguments.length,o=i<3?t:e===null?e=Object.getOwnPropertyDescriptor(t,n):e,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")o=Reflect.decorate(r,t,n,e);else for(var d=r.length-1;d>=0;d--)(a=r[d])&&(o=(i<3?a(o):i>3?a(t,n,o):a(t,n))||o);return i>3&&o&&Object.defineProperty(t,n,o),o};let s=class extends m{constructor(){super(...arguments),this.active=!1,this.count=0,this.onCountChanged=t=>{this.count=t.detail.count}}connectedCallback(){super.connectedCallback(),this.syncDropdownToggleStatus(),document.addEventListener(u.eventName,this.onCountChanged)}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(u.eventName,this.onCountChanged)}updated(t){t.has("active")&&this.syncDropdownToggleStatus()}createRenderRoot(){return this}render(){return h`${f.get("hiddenCE")} (${this.count})`}buttonActivated(){if(this.hasAttribute("disabled"))return;this.setAttribute("disabled","");const t=!this.active,n=document.querySelectorAll(".t3js-hidden-record");for(const e of n){e.style.display="flow-root";const i=e.scrollHeight;e.style.overflow="clip",t?(e.addEventListener("transitionend",()=>{e.style.display="",e.style.overflow="",e.style.height=""},{once:!0}),e.style.height=i+"px"):(e.addEventListener("transitionend",()=>{e.style.display="none",e.style.overflow=""},{once:!0}),requestAnimationFrame(()=>{e.style.height=i+"px",requestAnimationFrame(()=>{e.style.height="0px"})}))}this.active=t,y.set("moduleData.web_layout.showHidden",t?"1":"0").then(()=>{this.removeAttribute("disabled")})}syncDropdownToggleStatus(){this.dataset.dropdowntoggleStatus=this.active?"active":"inactive"}};l([c({type:Boolean,reflect:!0})],s.prototype,"active",void 0),l([c({type:Number})],s.prototype,"count",void 0),s=l([p("typo3-backend-page-layout-toggle-hidden")],s);export{s as PageLayoutToggleHidden}; diff --git a/Resources/Public/JavaScript/layout-module/velocity-scroll.js b/Resources/Public/JavaScript/layout-module/velocity-scroll.js new file mode 100644 index 0000000..51b315a --- /dev/null +++ b/Resources/Public/JavaScript/layout-module/velocity-scroll.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import S from"@typo3/core/document-service.js";import T from"@typo3/core/event/regular-event.js";const u=.2,f=2.5;S.ready().then(()=>{new T("dragstart",l=>{E(l)}).delegateTo(document,'[draggable="true"]')});function E(l){const t=document.scrollingElement;if(!(t instanceof HTMLElement)){console.warn("Scrolling element is not an HTMLElement. Velocity scroll will not work.");return}const e=p(l.target)??t;let a=0,s=0,m=!0,h=performance.now();const v=t.style.scrollBehavior,H=e.style.scrollBehavior;t.style.scrollBehavior="auto",e.style.scrollBehavior="auto";const w=o=>{const n=(o-h)/1e3;h=o;const r=t.scrollHeight-t.clientHeight,c=e.scrollWidth-e.clientWidth;t.scrollTop=Math.max(0,Math.min(t.scrollTop+a*n,r)),e.scrollLeft=Math.max(0,Math.min(e.scrollLeft+s*n,c)),m&&requestAnimationFrame(w)};requestAnimationFrame(w);const i=(o,n,r)=>o{const n=window.innerHeight*u,r=window.innerWidth*u,c=window.innerHeight*f,g=window.innerWidth*f;a=i(window.innerHeight-o.clientY,n,c)-i(o.clientY,n,c),s=i(window.innerWidth-o.clientX,r,g)-i(o.clientX,r,g)};d(l),window.addEventListener("dragover",d),window.addEventListener("dragend",()=>{window.removeEventListener("dragover",d),a=0,s=0,m=!1,t.style.scrollBehavior=v,e.style.scrollBehavior=H},{once:!0})}function p(l){let t=l;for(;t instanceof HTMLElement;){if(t.scrollWidth>t.clientWidth){const e=window.getComputedStyle(t).overflowX;if(e==="auto"||e==="scroll")return t}t=t.parentElement}return null}export{E as initVelocityScroll}; diff --git a/Resources/Public/JavaScript/link-browser.js b/Resources/Public/JavaScript/link-browser.js new file mode 100644 index 0000000..b651613 --- /dev/null +++ b/Resources/Public/JavaScript/link-browser.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import i from"@typo3/core/event/regular-event.js";import"@typo3/backend/element/combobox-element.js";import"@typo3/backend/element/link-browser-download-element.js";class n{constructor(){this.parameters=JSON.parse(document.body.dataset.linkbrowserParameters||"{}"),this.linkAttributeFields=JSON.parse(document.body.dataset.linkbrowserAttributeFields||"{}"),new i("click",e=>{e.preventDefault(),this.finalizeFunction(document.body.dataset.linkbrowserCurrentLink)}).delegateTo(document,"button.t3js-linkCurrent")}getLinkAttributeValues(){const e={};for(const t of this.linkAttributeFields.values()){const r=document.querySelector('[name="l'+t+'"]');r!==null&&(e[t]=r.value)}return e}finalizeFunction(e){throw"The link browser requires the finalizeFunction to be set in order for "+e+" to be handled. Seems like you discovered a major bug."}}var o=new n;export{o as default}; diff --git a/Resources/Public/JavaScript/live-search/element/backend-search.js b/Resources/Public/JavaScript/live-search/element/backend-search.js new file mode 100644 index 0000000..23682e1 --- /dev/null +++ b/Resources/Public/JavaScript/live-search/element/backend-search.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{customElement as p}from"lit/decorators.js";import{LitElement as a}from"lit";var m=function(n,t,r,c){var o=arguments.length,e=o<3?t:c===null?c=Object.getOwnPropertyDescriptor(t,r):c,f;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(n,t,r,c);else for(var l=n.length-1;l>=0;l--)(f=n[l])&&(e=(o<3?f(e):o>3?f(t,r,e):f(t,r))||e);return o>3&&e&&Object.defineProperty(t,r,e),e};let i=class extends a{createRenderRoot(){return this}};i=m([p("typo3-backend-live-search")],i);export{i as BackendSearch}; diff --git a/Resources/Public/JavaScript/live-search/element/hint.js b/Resources/Public/JavaScript/live-search/element/hint.js new file mode 100644 index 0000000..94fd94e --- /dev/null +++ b/Resources/Public/JavaScript/live-search/element/hint.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as m,customElement as h}from"lit/decorators.js";import{LitElement as a,nothing as u,html as d}from"lit";import{markdown as s}from"@typo3/core/directive/markdown.js";import"@typo3/backend/element/icon-element.js";var f=function(i,e,n,r){var o=arguments.length,t=o<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,n):r,l;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(i,e,n,r);else for(var p=i.length-1;p>=0;p--)(l=i[p])&&(t=(o<3?l(t):o>3?l(e,n,t):l(e,n))||t);return o>3&&t&&Object.defineProperty(e,n,t),t};let c=class extends a{createRenderRoot(){return this}render(){return this.hint===""?u:d`${s(this.hint,"minimal")}`}};f([m({type:String})],c.prototype,"hint",void 0),c=f([h("typo3-backend-live-search-hint")],c);export{c as Hint}; diff --git a/Resources/Public/JavaScript/live-search/element/provider/default-result-item.js b/Resources/Public/JavaScript/live-search/element/provider/default-result-item.js new file mode 100644 index 0000000..3e5f999 --- /dev/null +++ b/Resources/Public/JavaScript/live-search/element/provider/default-result-item.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as l,customElement as m}from"lit/decorators.js";import{LitElement as v,html as d,nothing as u}from"lit";import"@typo3/backend/element/icon-element.js";import f from"~labels/core.misc";var r=function(o,i,a,n){var s=arguments.length,e=s<3?i:n===null?n=Object.getOwnPropertyDescriptor(i,a):n,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(o,i,a,n);else for(var p=o.length-1;p>=0;p--)(c=o[p])&&(e=(s<3?c(e):s>3?c(i,a,e):c(i,a))||e);return s>3&&e&&Object.defineProperty(i,a,e),e};let t=class extends v{constructor(){super(...arguments),this.language=null}createRenderRoot(){return this}render(){return d`
    ${this.language?d``:u}
    ${this.extraData.inWorkspace?d`
    `:u}
    ${this.extraData.breadcrumb!==void 0?d`${this.extraData.breadcrumb}`:u}
    `}};r([l({type:Object,attribute:!1})],t.prototype,"icon",void 0),r([l({type:Object,attribute:!1})],t.prototype,"language",void 0),r([l({type:String,attribute:!1})],t.prototype,"itemTitle",void 0),r([l({type:String,attribute:!1})],t.prototype,"typeLabel",void 0),r([l({type:Object,attribute:!1})],t.prototype,"extraData",void 0),t=r([m("typo3-backend-live-search-result-item-default")],t);export{t as DefaultProviderResultItem}; diff --git a/Resources/Public/JavaScript/live-search/element/provider/page-provider-result-item.js b/Resources/Public/JavaScript/live-search/element/provider/page-provider-result-item.js new file mode 100644 index 0000000..fb02963 --- /dev/null +++ b/Resources/Public/JavaScript/live-search/element/provider/page-provider-result-item.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as l,customElement as m}from"lit/decorators.js";import{LitElement as v,html as p,nothing as u}from"lit";import"@typo3/backend/element/icon-element.js";import f from"~labels/core.misc";var r=function(o,i,a,s){var n=arguments.length,e=n<3?i:s===null?s=Object.getOwnPropertyDescriptor(i,a):s,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(o,i,a,s);else for(var d=o.length-1;d>=0;d--)(c=o[d])&&(e=(n<3?c(e):n>3?c(i,a,e):c(i,a))||e);return n>3&&e&&Object.defineProperty(i,a,e),e};let t=class extends v{constructor(){super(...arguments),this.language=null}createRenderRoot(){return this}render(){return p`
    ${this.language?p``:u}
    ${this.extraData.inWorkspace?p`
    `:u}
    ${this.extraData.breadcrumb}
    `}};r([l({type:Object,attribute:!1})],t.prototype,"icon",void 0),r([l({type:Object,attribute:!1})],t.prototype,"language",void 0),r([l({type:String,attribute:!1})],t.prototype,"itemTitle",void 0),r([l({type:String,attribute:!1})],t.prototype,"typeLabel",void 0),r([l({type:Object,attribute:!1})],t.prototype,"extraData",void 0),t=r([m("typo3-backend-live-search-result-item-page-provider")],t);var b=t;export{b as default}; diff --git a/Resources/Public/JavaScript/live-search/element/result/item/action/action-container.js b/Resources/Public/JavaScript/live-search/element/result/item/action/action-container.js new file mode 100644 index 0000000..9815700 --- /dev/null +++ b/Resources/Public/JavaScript/live-search/element/result/item/action/action-container.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import p from"@typo3/core/document-service.js";import{property as v,customElement as d}from"lit/decorators.js";import{LitElement as h,html as a,css as y}from"lit";import"@typo3/backend/live-search/element/result/item/action/action.js";var u=function(r,e,t,i){var o=arguments.length,n=o<3?e:i===null?i=Object.getOwnPropertyDescriptor(e,t):i,s;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(r,e,t,i);else for(var l=r.length-1;l>=0;l--)(s=r[l])&&(n=(o<3?s(n):o>3?s(e,t,n):s(e,t))||n);return o>3&&n&&Object.defineProperty(e,t,n),n};const f="typo3-backend-live-search-result-item-action-container";let c=class extends h{constructor(){super(...arguments),this.resultItem=null}createRenderRoot(){return this}render(){return a`${this.resultItem.actions.map(e=>this.renderActionItem(this.resultItem,e))}`}renderActionItem(e,t){return a`this.invokeAction(this.resultItem,t)}>`}invokeAction(e,t){this.closest("typo3-backend-live-search-result-container").dispatchEvent(new CustomEvent("livesearch:invoke-action",{detail:{resultItem:e,action:t}}))}};u([v({type:Object,attribute:!1})],c.prototype,"resultItem",void 0),c=u([d("typo3-backend-live-search-result-item-action-container")],c);let m=class extends h{static{this.styles=y`:host{display:block}`}async connectedCallback(){await p.ready(),this.parentContainer=this.closest("typo3-backend-live-search-result-container"),this.resultItemContainer=this.parentContainer.querySelector("typo3-backend-live-search-result-item-container"),super.connectedCallback(),this.addEventListener("keydown",this.handleKeyDown),this.addEventListener("keyup",this.handleKeyUp)}disconnectedCallback(){this.removeEventListener("keydown",this.handleKeyDown),this.removeEventListener("keyup",this.handleKeyUp),super.disconnectedCallback()}render(){return a``}handleKeyDown(e){if(!["ArrowDown","ArrowUp","ArrowLeft"].includes(e.key)||document.activeElement.tagName.toLowerCase()!=="typo3-backend-live-search-result-item-action")return;e.preventDefault();let t;e.key==="ArrowDown"?t=document.activeElement.nextElementSibling:e.key==="ArrowUp"?t=document.activeElement.previousElementSibling:e.key==="ArrowLeft"&&(t=this.resultItemContainer.querySelector("typo3-backend-live-search-result-item.active")),t!==null&&t.focus()}handleKeyUp(e){if(!["Enter"," "].includes(e.key))return;e.preventDefault();const t=e.target;this.parentContainer.dispatchEvent(new CustomEvent("livesearch:invoke-action",{detail:{resultItem:t.resultItem,action:t.resultItemAction}}))}};m=u([d("typo3-backend-live-search-result-action-list")],m);export{c as ActionContainer,m as ActionList,f as componentName}; diff --git a/Resources/Public/JavaScript/live-search/element/result/item/action/action.js b/Resources/Public/JavaScript/live-search/element/result/item/action/action.js new file mode 100644 index 0000000..57cbf42 --- /dev/null +++ b/Resources/Public/JavaScript/live-search/element/result/item/action/action.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as p,customElement as d}from"lit/decorators.js";import{ifDefined as f}from"lit/directives/if-defined.js";import{LitElement as m,html as u}from"lit";import"@typo3/backend/element/icon-element.js";var a=function(o,e,i,n){var c=arguments.length,t=c<3?e:n===null?n=Object.getOwnPropertyDescriptor(e,i):n,l;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(o,e,i,n);else for(var s=o.length-1;s>=0;s--)(l=o[s])&&(t=(c<3?l(t):c>3?l(e,i,t):l(e,i))||t);return c>3&&t&&Object.defineProperty(e,i,t),t};let r=class extends m{connectedCallback(){super.connectedCallback(),this.hasAttribute("tabindex")||this.setAttribute("tabindex","0")}createRenderRoot(){return this}render(){return u`
    ${this.resultItemAction.label}
    `}};a([p({type:Object,attribute:!1})],r.prototype,"resultItem",void 0),a([p({type:Object,attribute:!1})],r.prototype,"resultItemAction",void 0),r=a([d("typo3-backend-live-search-result-item-action")],r);export{r as Action}; diff --git a/Resources/Public/JavaScript/live-search/element/result/item/item-container.js b/Resources/Public/JavaScript/live-search/element/result/item/item-container.js new file mode 100644 index 0000000..f565b1c --- /dev/null +++ b/Resources/Public/JavaScript/live-search/element/result/item/item-container.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import"@typo3/backend/element/spinner-element.js";import m from"@typo3/backend/live-search/live-search-configurator.js";import{LitElement as p,html as l,css as v}from"lit";import{property as y,customElement as h}from"lit/decorators.js";import{until as b}from"lit/directives/until.js";import"@typo3/backend/live-search/element/provider/default-result-item.js";import"@typo3/backend/live-search/element/result/item/item.js";var u=function(o,e,t,n){var i=arguments.length,r=i<3?e:n===null?n=Object.getOwnPropertyDescriptor(e,t):n,s;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")r=Reflect.decorate(o,e,t,n);else for(var c=o.length-1;c>=0;c--)(s=o[c])&&(r=(i<3?s(r):i>3?s(e,t,r):s(e,t))||r);return i>3&&r&&Object.defineProperty(e,t,r),r};const f="typo3-backend-live-search-result-item-container";let a=class extends p{constructor(){super(...arguments),this.results=null}connectedCallback(){super.connectedCallback(),this.addEventListener("scroll",this.onScroll)}disconnectedCallback(){this.removeEventListener("scroll",this.onScroll),super.disconnectedCallback()}createRenderRoot(){return this}render(){const e={},t=this.results.filter(n=>n!==null);return t.length!==this.results.length&&console.warn('The result set contained "null" values, indicating something went wrong while building the search results. Affected values were removed to no break the user interface.'),t.forEach(n=>{n.typeLabel in e?e[n.typeLabel].push(n):e[n.typeLabel]=[n]}),l`${this.renderGroupedResults(e)}`}renderGroupedResults(e){const t=[];for(const[n,i]of Object.entries(e)){const r=i.length;t.push(l`
    ${n} (${r})
    `),t.push(...i.map(s=>l`${b(this.renderResultItem(s),l``)}`))}return l`${t}`}async renderResultItem(e){const t=m.getRenderers();let n;return t[e.provider]!==void 0?(await import(t[e.provider].module),n=t[e.provider].callback(e)):n=l``,l`this.invokeAction(e,e.defaultAction)} @focus=${()=>this.requestActions(e)}>${n}`}requestActions(e){this.parentElement.dispatchEvent(new CustomEvent("livesearch:request-actions",{detail:{resultItem:e}}))}invokeAction(e,t){this.parentElement.dispatchEvent(new CustomEvent("livesearch:invoke-action",{detail:{resultItem:e,action:t}}))}onScroll(e){this.querySelectorAll(".livesearch-result-item-group-label").forEach(t=>{t.classList.toggle("sticky",t.offsetTop<=e.target.scrollTop)})}};u([y({type:Object,attribute:!1})],a.prototype,"results",void 0),a=u([h("typo3-backend-live-search-result-item-container")],a);let d=class extends p{static{this.styles=v`:host{display:block}`}connectedCallback(){this.parentContainer=this.closest("typo3-backend-live-search-result-container"),this.resultItemDetailContainer=this.parentContainer.querySelector("typo3-backend-live-search-result-item-detail-container"),super.connectedCallback(),this.addEventListener("keydown",this.handleKeyDown),this.addEventListener("keyup",this.handleKeyUp)}disconnectedCallback(){this.removeEventListener("keydown",this.handleKeyDown),this.removeEventListener("keyup",this.handleKeyUp),super.disconnectedCallback()}render(){return l``}handleKeyDown(e){if(!["ArrowDown","ArrowUp","ArrowRight"].includes(e.key))return;const t="typo3-backend-live-search-result-item";if(document.activeElement.tagName.toLowerCase()!==t)return;e.preventDefault();let n;if(e.key==="ArrowDown"){let i=document.activeElement.nextElementSibling;for(;i!==null&&i.tagName.toLowerCase()!==t;)i=i.nextElementSibling;n=i}else if(e.key==="ArrowUp"){let i=document.activeElement.previousElementSibling;for(;i!==null&&i.tagName.toLowerCase()!==t;)i=i.previousElementSibling;n=i,n===null&&(n=document.querySelector("typo3-backend-live-search").querySelector('input[type="search"]'))}else e.key==="ArrowRight"&&(n=this.resultItemDetailContainer.querySelector("typo3-backend-live-search-result-item-action"));n!==null&&n.focus()}handleKeyUp(e){if(!["Enter"," "].includes(e.key))return;e.preventDefault();const t=e.target.resultItem;this.invokeAction(t)}invokeAction(e){this.parentContainer.dispatchEvent(new CustomEvent("livesearch:invoke-action",{detail:{resultItem:e,action:e.actions[0]}}))}};d=u([h("typo3-backend-live-search-result-list")],d);export{a as ItemContainer,d as ResultList,f as componentName}; diff --git a/Resources/Public/JavaScript/live-search/element/result/item/item.js b/Resources/Public/JavaScript/live-search/element/result/item/item.js new file mode 100644 index 0000000..700d466 --- /dev/null +++ b/Resources/Public/JavaScript/live-search/element/result/item/item.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as d,customElement as p}from"lit/decorators.js";import{LitElement as u,html as f}from"lit";import"@typo3/backend/element/icon-element.js";var l=function(n,e,t,o){var i=arguments.length,c=i<3?e:o===null?o=Object.getOwnPropertyDescriptor(e,t):o,r;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")c=Reflect.decorate(n,e,t,o);else for(var a=n.length-1;a>=0;a--)(r=n[a])&&(c=(i<3?r(c):i>3?r(e,t,c):r(e,t))||c);return i>3&&c&&Object.defineProperty(e,t,c),c};let s=class extends u{connectedCallback(){super.connectedCallback(),this.hasAttribute("tabindex")||this.setAttribute("tabindex","0"),this.addEventListener("focus",this.onFocus)}disconnectedCallback(){this.removeEventListener("focus",this.onFocus),super.disconnectedCallback()}createRenderRoot(){return this}render(){return f`
    {e.stopPropagation(),this.focus()}}>
    `}onFocus(e){const t=e.target;t.parentElement.querySelector(".active")?.classList.remove("active"),t.classList.add("active")}};l([d({type:Object,attribute:!1})],s.prototype,"resultItem",void 0),s=l([p("typo3-backend-live-search-result-item")],s);export{s as Item}; diff --git a/Resources/Public/JavaScript/live-search/element/result/result-container.js b/Resources/Public/JavaScript/live-search/element/result/result-container.js new file mode 100644 index 0000000..dd61a48 --- /dev/null +++ b/Resources/Public/JavaScript/live-search/element/result/result-container.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import h from"@typo3/backend/live-search/live-search-configurator.js";import m from"@typo3/backend/viewport.js";import{property as u,query as p,customElement as f}from"lit/decorators.js";import{LitElement as y,html as c,nothing as b}from"lit";import"@typo3/backend/live-search/element/result/item/item-container.js";import"@typo3/backend/live-search/element/result/result-detail-container.js";import v from"~labels/core.misc";var s=function(l,e,n,r){var t=arguments.length,i=t<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,n):r,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")i=Reflect.decorate(l,e,n,r);else for(var d=l.length-1;d>=0;d--)(a=l[d])&&(i=(t<3?a(i):t>3?a(e,n,i):a(e,n))||i);return t>3&&i&&Object.defineProperty(e,n,i),i};const k="typo3-backend-live-search-result-container";let o=class extends y{constructor(){super(...arguments),this.results=null,this.hasErrors=!1,this.loading=!1}connectedCallback(){super.connectedCallback(),this.addEventListener("livesearch:request-actions",this.onActionsRequested),this.addEventListener("livesearch:invoke-action",this.onActionInvoked)}disconnectedCallback(){this.removeEventListener("livesearch:request-actions",this.onActionsRequested),this.removeEventListener("livesearch:invoke-action",this.onActionInvoked),super.disconnectedCallback()}createRenderRoot(){return this}render(){return this.loading?c`
    `:this.hasErrors?c`
    ${v.get("liveSearch_hasErrors")}
    `:this.results===null?b:this.results.length===0?c`
    ${v.get("liveSearch_listEmptyText")}
    `:c``}onActionsRequested(e){this.resultDetailContainer.resultItem=e.detail.resultItem}onActionInvoked(e){const n=h.getInvokeHandlers(),r=e.detail.resultItem,t=e.detail.action;t!==void 0&&(typeof n[r.provider+"_"+t.identifier]=="function"?n[r.provider+"_"+t.identifier](r,t):m.ContentContainer.setUrl(t.url),this.dispatchEvent(new CustomEvent("live-search:item-chosen",{detail:{resultItem:r}})))}};s([u({type:Object})],o.prototype,"results",void 0),s([u({type:Boolean,attribute:!1})],o.prototype,"hasErrors",void 0),s([u({type:Boolean,attribute:!1})],o.prototype,"loading",void 0),s([p("typo3-backend-live-search-result-item-container")],o.prototype,"itemContainer",void 0),s([p("typo3-backend-live-search-result-item-detail-container")],o.prototype,"resultDetailContainer",void 0),o=s([f("typo3-backend-live-search-result-container")],o);export{o as ResultContainer,k as componentName}; diff --git a/Resources/Public/JavaScript/live-search/element/result/result-detail-container.js b/Resources/Public/JavaScript/live-search/element/result/result-detail-container.js new file mode 100644 index 0000000..8acd357 --- /dev/null +++ b/Resources/Public/JavaScript/live-search/element/result/result-detail-container.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as u,customElement as d}from"lit/decorators.js";import{LitElement as h,nothing as m,html as l}from"lit";import"@typo3/backend/live-search/element/result/item/action/action-container.js";var p=function(n,e,r,i){var s=arguments.length,t=s<3?e:i===null?i=Object.getOwnPropertyDescriptor(e,r):i,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(n,e,r,i);else for(var c=n.length-1;c>=0;c--)(a=n[c])&&(t=(s<3?a(t):s>3?a(e,r,t):a(e,r))||t);return s>3&&t&&Object.defineProperty(e,r,t),t};const b="typo3-backend-live-search-result-item-detail-container";let o=class extends h{constructor(){super(...arguments),this.resultItem=null}createRenderRoot(){return this}render(){if(this.resultItem===null)return m;const e=Object.entries(this.resultItem.properties??{});return l`
    ${this.resultItem.thumbnailUrl?l`
    `:l``}

    ${this.resultItem.itemTitle}

    ${this.resultItem.typeLabel}

    ${e.length>0?l`
    ${e.map(([r,i])=>l`
    ${r}
    ${i}
    `)}
    `:m}`}};p([u({type:Object,attribute:!1})],o.prototype,"resultItem",void 0),o=p([d("typo3-backend-live-search-result-item-detail-container")],o);export{o as ResultDetailContainer,b as componentName}; diff --git a/Resources/Public/JavaScript/live-search/element/result/result-pagination.js b/Resources/Public/JavaScript/live-search/element/result/result-pagination.js new file mode 100644 index 0000000..1094e71 --- /dev/null +++ b/Resources/Public/JavaScript/live-search/element/result/result-pagination.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as h,customElement as u}from"lit/decorators.js";import{LitElement as P,nothing as i,html as t}from"lit";import"@typo3/backend/element/icon-element.js";var n=function(s,e,l,r){var g=arguments.length,a=g<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,l):r,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")a=Reflect.decorate(s,e,l,r);else for(var d=s.length-1;d>=0;d--)(c=s[d])&&(a=(g<3?c(a):g>3?c(e,l,a):c(e,l))||a);return g>3&&a&&Object.defineProperty(e,l,a),a};let p=class extends P{constructor(){super(...arguments),this.pagination=null,this.loading=!1}createRenderRoot(){return this}render(){return this.loading||this.pagination===null||this.pagination.allPageNumbers.length<=1?i:t``}};n([h({type:Object})],p.prototype,"pagination",void 0),n([h({type:Boolean,attribute:!1})],p.prototype,"loading",void 0),p=n([u("typo3-backend-live-search-result-pagination")],p);let o=class extends P{connectedCallback(){super.connectedCallback(),this.addEventListener("click",this.dispatchPaginationEvent)}disconnectedCallback(){this.removeEventListener("click",this.dispatchPaginationEvent),super.disconnectedCallback()}createRenderRoot(){return this}render(){return i}dispatchPaginationEvent(){this.closest("typo3-backend-live-search").dispatchEvent(new CustomEvent("livesearch:pagination-selected",{detail:{offset:(this.page-1)*this.perPage}}))}};n([h({type:Number})],o.prototype,"page",void 0),n([h({type:Number})],o.prototype,"perPage",void 0),o=n([u("typo3-backend-live-search-result-page")],o);export{p as ResultPagination,o as ResultPaginationPage}; diff --git a/Resources/Public/JavaScript/live-search/element/search-option-item.js b/Resources/Public/JavaScript/live-search/element/search-option-item.js new file mode 100644 index 0000000..6b526dc --- /dev/null +++ b/Resources/Public/JavaScript/live-search/element/search-option-item.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as s,customElement as h}from"lit/decorators.js";import{LitElement as d,html as v}from"lit";import m from"@typo3/backend/storage/browser-session.js";var n=function(r,o,i,p){var c=arguments.length,t=c<3?o:p===null?p=Object.getOwnPropertyDescriptor(o,i):p,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(r,o,i,p);else for(var l=r.length-1;l>=0;l--)(a=r[l])&&(t=(c<3?a(t):c>3?a(o,i,t):a(o,i))||t);return c>3&&t&&Object.defineProperty(o,i,t),t};let e=class extends d{constructor(){super(...arguments),this.active=!1}connectedCallback(){this.parentContainer=this.closest("typo3-backend-live-search"),super.connectedCallback()}createRenderRoot(){return this}render(){return v`
    `}getStorageKey(){return`livesearch-option-${this.optionName}-${this.optionId}`}handleInput(){this.active=!this.active,this.parentContainer.dispatchEvent(new CustomEvent("typo3:live-search:option-invoked",{detail:{active:this.active}})),m.set(this.getStorageKey(),this.active?"1":"0")}};n([s({type:Boolean})],e.prototype,"active",void 0),n([s({type:String})],e.prototype,"optionId",void 0),n([s({type:String})],e.prototype,"optionName",void 0),n([s({type:String})],e.prototype,"optionLabel",void 0),e=n([h("typo3-backend-live-search-option-item")],e);export{e as SearchOptionItem}; diff --git a/Resources/Public/JavaScript/live-search/live-search-configurator.js b/Resources/Public/JavaScript/live-search/live-search-configurator.js new file mode 100644 index 0000000..13bf90d --- /dev/null +++ b/Resources/Public/JavaScript/live-search/live-search-configurator.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class o{constructor(){this.renderers={},this.invokeHandlers={}}getRenderers(){return this.renderers}addRenderer(r,t,n){this.renderers[r]={module:t,callback:n}}getInvokeHandlers(){return this.invokeHandlers}addInvokeHandler(r,t,n){this.invokeHandlers[r+"_"+t]=n}}let e;top.TYPO3.LiveSearchConfigurator?e=top.TYPO3.LiveSearchConfigurator:(e=new o,top.TYPO3.LiveSearchConfigurator=e);var a=e;export{a as default}; diff --git a/Resources/Public/JavaScript/live-search/live-search-shortcut.js b/Resources/Public/JavaScript/live-search/live-search-shortcut.js new file mode 100644 index 0000000..12f7233 --- /dev/null +++ b/Resources/Public/JavaScript/live-search/live-search-shortcut.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import e from"@typo3/backend/hotkeys.js";import r from"@typo3/core/document-service.js";class o{constructor(){r.ready().then(()=>{e.register([e.normalizedCtrlModifierKey,"k"],t=>{t.preventDefault(),top.document.dispatchEvent(new CustomEvent("typo3:live-search:trigger-open"))},{allowOnEditables:!0})})}}var a=new o;export{a as default}; diff --git a/Resources/Public/JavaScript/live-search/live-search.js b/Resources/Public/JavaScript/live-search/live-search.js new file mode 100644 index 0000000..2c2f0af --- /dev/null +++ b/Resources/Public/JavaScript/live-search/live-search.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import m,{Sizes as b}from"@typo3/backend/modal.js";import"@typo3/backend/element/icon-element.js";import"@typo3/backend/live-search/element/hint.js";import"@typo3/backend/live-search/element/result/result-pagination.js";import"@typo3/backend/live-search/element/search-option-item.js";import"@typo3/backend/live-search/live-search-shortcut.js";import q from"@typo3/core/document-service.js";import c from"@typo3/core/event/regular-event.js";import w from"@typo3/core/event/debounce-event.js";import{SeverityEnum as E}from"@typo3/backend/enum/severity.js";import R from"@typo3/core/ajax/ajax-request.js";import d from"@typo3/backend/storage/browser-session.js";import{componentName as k}from"@typo3/backend/live-search/element/result/result-container.js";import{ModuleStateStorage as O}from"@typo3/backend/storage/module-state-storage.js";import T from"~labels/core.core";class C{constructor(){this.currentSearchRequest=null,this.search=async t=>{if(t.get("query").toString()==="")this.updateSearchResults();else{const o=document.querySelector(k),l=document.querySelector("typo3-backend-live-search-result-pagination");o.loading=!0,l.loading=!0,this.currentSearchRequest?.abort();try{this.currentSearchRequest=new R(TYPO3.settings.ajaxUrls.livesearch);const r=await(await this.currentSearchRequest.post(t)).raw().json();this.currentSearchRequest=null,this.updateSearchResults(r)}catch(s){if(this.updateSearchResults(null,!0),s instanceof DOMException&&s.name==="AbortError")return;throw s}}},q.ready().then(()=>{this.registerEvents()})}registerEvents(){new c("typo3:live-search:trigger-open",()=>{m.currentModal||this.openSearchModal()}).bindTo(document)}openSearchModal(){const t=new URL(TYPO3.settings.ajaxUrls.livesearch_form,window.location.origin),n=O.current("web");n.identifier&&t.searchParams.set("pageId",n.identifier),t.searchParams.set("query",d.get("livesearch-term")??""),t.searchParams.set("offset",d.get("livesearch-offset")??"0");const o=Object.entries(d.getByPrefix("livesearch-option-")).filter(r=>r[1]==="1").map(r=>{const e=r[0].replace("livesearch-option-",""),[a,p]=e.split("-",2);return{key:a,value:p}}),l=this.composeSearchOptions(o);for(const[r,e]of Object.entries(l))for(const a of e)t.searchParams.append(`${r}[]`,a);const s=m.advanced({type:m.types.ajax,content:t.toString(),title:T.get("labels.search"),severity:E.notice,size:b.large,ajaxCallback:()=>{const r=s.querySelector("typo3-backend-live-search"),e=r.querySelector("form"),a=e.querySelector('input[type="search"]'),p=e.querySelector('input[name="offset"]');new c("livesearch:demand-changed",()=>{p.value="0",e.requestSubmit()}).bindTo(r),new c("livesearch:pagination-selected",h=>{p.value=h.detail.offset.toString(10),e.requestSubmit()}).bindTo(r),new c("submit",h=>{h.preventDefault();const u=new FormData(e);this.search(u).then(()=>{const g=u.get("query").toString(),v=u.get("offset")?.toString();d.set("livesearch-term",g),v&&d.set("livesearch-offset",v)});const i=e.querySelector("[data-active-options-counter]"),S=parseInt(i.dataset.activeOptionsCounter,10);i.querySelector("output").textContent=S.toString(10),i.classList.toggle("hidden",S===0)}).bindTo(e),new c("search",()=>{a.value===""&&e.requestSubmit()}).bindTo(a);const y=document.querySelector("typo3-backend-live-search-result-container");new c("live-search:item-chosen",()=>{m.dismiss()}).bindTo(y),new c("typo3:live-search:option-invoked",h=>{const u=e.querySelector("[data-active-options-counter]");let i=parseInt(u.dataset.activeOptionsCounter,10);i=h.detail.active?i+1:i-1,u.dataset.activeOptionsCounter=i.toString(10),r.dispatchEvent(new CustomEvent("livesearch:demand-changed"))}).bindTo(r),new w("input",()=>{r.dispatchEvent(new CustomEvent("livesearch:demand-changed"))}).bindTo(a),new c("keydown",this.handleKeyDown).bindTo(a),e.requestSubmit()}});["modal-loaded","typo3-modal-shown"].forEach(r=>{s.addEventListener(r,()=>{const e=s.querySelector('input[type="search"]');e!==null&&(e.focus(),e.select())})})}composeSearchOptions(t){const n={};return t.forEach(o=>{n[o.key]===void 0&&(n[o.key]=[]),n[o.key].push(o.value)}),n}handleKeyDown(t){if(t.key!=="ArrowDown")return;t.preventDefault(),document.querySelector("typo3-backend-live-search").querySelector("typo3-backend-live-search-result-item")?.focus()}updateSearchResults(t=null,n=!1){const o=document.querySelector("typo3-backend-live-search-result-container");o.results=t?.results??null,o.loading=!1,o.hasErrors=n;const l=document.querySelector("typo3-backend-live-search-result-pagination");l.pagination=t?.pagination??null,l.loading=!1}}let f;top.TYPO3.LiveSearch?f=top.TYPO3.LiveSearch:(f=new C,top.TYPO3.LiveSearch=f);var j=f;export{j as default}; diff --git a/Resources/Public/JavaScript/live-search/result-types/backend-module-result-type.js b/Resources/Public/JavaScript/live-search/result-types/backend-module-result-type.js new file mode 100644 index 0000000..8c8f433 --- /dev/null +++ b/Resources/Public/JavaScript/live-search/result-types/backend-module-result-type.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import r from"@typo3/backend/live-search/live-search-configurator.js";function d(e){r.addInvokeHandler(e,"open_module",o=>{TYPO3.ModuleMenu.App.showModule(o.extraData.moduleIdentifier)})}export{d as registerType}; diff --git a/Resources/Public/JavaScript/live-search/result-types/default-result-type.js b/Resources/Public/JavaScript/live-search/result-types/default-result-type.js new file mode 100644 index 0000000..c517423 --- /dev/null +++ b/Resources/Public/JavaScript/live-search/result-types/default-result-type.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import a from"@typo3/backend/live-search/live-search-configurator.js";import"@typo3/backend/live-search/element/provider/page-provider-result-item.js";import i from"@typo3/core/ajax/ajax-request.js";import n from"@typo3/backend/notification.js";import s from"@typo3/backend/window-manager.js";function u(t){a.addInvokeHandler(t,"switch_backend_user",o=>{new i(TYPO3.settings.ajaxUrls.switch_user).post({targetUser:o.extraData.uid}).then(async r=>{const e=await r.resolve();e.success===!0&&e.url?top.window.location.href=e.url:n.error("Switching to user went wrong.")})}),a.addInvokeHandler(t,"preview",(o,r)=>{s.localOpen(r.url,!0)})}export{u as registerType}; diff --git a/Resources/Public/JavaScript/live-search/result-types/page-result-type.js b/Resources/Public/JavaScript/live-search/result-types/page-result-type.js new file mode 100644 index 0000000..adeadbb --- /dev/null +++ b/Resources/Public/JavaScript/live-search/result-types/page-result-type.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import a from"@typo3/backend/live-search/live-search-configurator.js";import{html as i}from"lit";import l from"@typo3/backend/window-manager.js";function n(r){a.addRenderer(r,"@typo3/backend/live-search/element/provider/page-provider-result-item.js",e=>i``),a.addInvokeHandler(r,"preview",(e,o)=>{l.localOpen(o.url,!0)})}export{n as registerRenderer}; diff --git a/Resources/Public/JavaScript/live-search/toolbar/live-search-toolbar-item.js b/Resources/Public/JavaScript/live-search/toolbar/live-search-toolbar-item.js new file mode 100644 index 0000000..2dcf96f --- /dev/null +++ b/Resources/Public/JavaScript/live-search/toolbar/live-search-toolbar-item.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import e from"@typo3/core/event/regular-event.js";class t{constructor(){new e("click",()=>{document.dispatchEvent(new CustomEvent("typo3:live-search:trigger-open"))}).delegateTo(document,".t3js-topbar-button-search")}}var o=new t;export{o as default}; diff --git a/Resources/Public/JavaScript/localization.js b/Resources/Public/JavaScript/localization.js new file mode 100644 index 0000000..be23e0c --- /dev/null +++ b/Resources/Public/JavaScript/localization.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import"@typo3/backend/localization/localization-button.js";import"@typo3/backend/localization/localization-wizard.js"; diff --git a/Resources/Public/JavaScript/localization/finisher/localization-submission-service.js b/Resources/Public/JavaScript/localization/finisher/localization-submission-service.js new file mode 100644 index 0000000..04d2261 --- /dev/null +++ b/Resources/Public/JavaScript/localization/finisher/localization-submission-service.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import e from"@typo3/core/ajax/ajax-request.js";class o{constructor(t){this.context=t}async execute(){return await(await new e(TYPO3.settings.ajaxUrls.wizard_localization_localize).post({recordType:this.context.recordType,recordUid:this.context.recordUid,data:this.context.getDataStore()})).resolve()}}export{o as LocalizationSubmissionService}; diff --git a/Resources/Public/JavaScript/localization/label-provider.js b/Resources/Public/JavaScript/localization/label-provider.js new file mode 100644 index 0000000..6f93d0c --- /dev/null +++ b/Resources/Public/JavaScript/localization/label-provider.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{IntlMessageFormat as s}from"intl-messageformat";import{DateTime as l}from"luxon";class f{constructor(t){this.labels=t}get(t,e){const r=this.render(t,e);return Array.isArray(r)?r.join(""):r}render(t,e){if(!(t in this.labels))throw new Error("Label is not defined: "+String(t));const r=this.labels[t];if(e===void 0)return r;if(Array.isArray(e))return this.sprintf(r,e);const i=this.getFormatter(r).formatToParts(e);return i.length===1?i[0].value:i.map(o=>o.value)}sprintf(t,e){let r=0;return t.replace(/%[sdf]/g,i=>{const o=e[r++];switch(i){case"%s":return String(o);case"%d":return String(typeof o=="number"?o:parseInt(String(o),10));case"%f":return String(typeof o=="number"?o:parseFloat(o).toFixed(2));default:return i}})}getFormatter(t){return a("message",d)(t)}}const m=p();function d(n){const t=m?.timezone??void 0,e={short:{timeZone:t,dateStyle:"short"},medium:{timeZone:t,dateStyle:"medium"},long:{timeZone:t,dateStyle:"long"},full:{timeZone:t,dateStyle:"full"}},r={short:{timeZone:t,timeStyle:"short"},medium:{timeZone:t,timeStyle:"medium"},long:{timeZone:t,timeStyle:"long"},full:{timeZone:t,timeStyle:"full"}};return new s(n,y(),{date:e,time:r},{formatters:c})}const c={getNumberFormat:a("number",(n,t)=>new Intl.NumberFormat(n,t)),getDateTimeFormat:a("datetime",(n,t)=>{const{dateStyle:e,timeStyle:r,timeZone:i}=t;return m&&(e==="medium"||r==="medium")?{format:u=>l.fromJSDate(new Date(u),{zone:i}).setLocale(n).toFormat(e==="medium"&&r==="medium"?m.formats.datetime:e==="medium"?m.formats.date:m.formats.time)}:new Intl.DateTimeFormat(n,{dateStyle:e,timeStyle:r,timeZone:i})}),getPluralRules:a("date",(n,t)=>new Intl.PluralRules(n,t))},g={};function a(n,t){return(...e)=>{const r=JSON.stringify({context:n,args:e});return g[r]??=t(...e)}}function p(){try{return(typeof opener?.top?.TYPO3<"u"?opener.top:top).TYPO3.settings.DateConfiguration}catch{return null}}function y(){const n=document.documentElement.lang||"en";return n==="ch"?"zh":n}export{f as LabelProvider}; diff --git a/Resources/Public/JavaScript/localization/localization-button.js b/Resources/Public/JavaScript/localization/localization-button.js new file mode 100644 index 0000000..0da0559 --- /dev/null +++ b/Resources/Public/JavaScript/localization/localization-button.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as m}from"lit";import{PseudoButtonLitElement as f}from"@typo3/backend/element/pseudo-button.js";import{property as p,customElement as y}from"lit/decorators.js";import{SeverityEnum as b}from"@typo3/backend/enum/severity.js";import u from"@typo3/backend/modal.js";import s from"~labels/backend.wizards.localization";var l=function(i,t,r,a){var n=arguments.length,e=n<3?t:a===null?a=Object.getOwnPropertyDescriptor(t,r):a,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(i,t,r,a);else for(var d=i.length-1;d>=0;d--)(c=i[d])&&(e=(n<3?c(e):n>3?c(t,r,e):c(t,r))||e);return n>3&&e&&Object.defineProperty(t,r,e),e};let o=class extends f{buttonActivated(){const t=m``;u.advanced({title:s.get("localization_wizard.modal.title"),content:t,severity:b.notice,size:u.sizes.medium,staticBackdrop:!0,buttons:[]})}};l([p({type:String,attribute:"record-type"})],o.prototype,"recordType",void 0),l([p({type:Number,attribute:"record-uid"})],o.prototype,"recordUid",void 0),l([p({type:Number,attribute:"target-language"})],o.prototype,"targetLanguage",void 0),o=l([y("typo3-backend-localization-button")],o);export{o as LocalizationButton}; diff --git a/Resources/Public/JavaScript/localization/localization-wizard.js b/Resources/Public/JavaScript/localization/localization-wizard.js new file mode 100644 index 0000000..c71df89 --- /dev/null +++ b/Resources/Public/JavaScript/localization/localization-wizard.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import"@typo3/backend/wizard/wizard.js";import{property as p,query as y,state as l,customElement as w}from"lit/decorators.js";import{TargetLanguageStep as b}from"@typo3/backend/localization/steps/target-language-step.js";import{SourceLanguageStep as z}from"@typo3/backend/localization/steps/source-language-step.js";import{ContentRecordSelectionStep as S}from"@typo3/backend/localization/steps/content-record-selection-step.js";import{ModeStep as g}from"@typo3/backend/localization/steps/mode-step.js";import{HandlerSelectionStep as v}from"@typo3/backend/localization/steps/handler-selection-step.js";import{Task as T}from"@lit/task";import k from"@typo3/core/ajax/ajax-request.js";import m from"@typo3/backend/modal.js";import{LitElement as L,html as u}from"lit";import{AutoAdvanceEvent as D}from"@typo3/backend/wizard/events/auto-advance-event.js";import{StepSummaryEvent as h}from"@typo3/backend/wizard/events/step-summary-event.js";import{LocalizationSubmissionService as $}from"@typo3/backend/localization/finisher/localization-submission-service.js";import f from"~labels/backend.wizards.localization";var n=function(s,t,e,o){var i=arguments.length,a=i<3?t:o===null?o=Object.getOwnPropertyDescriptor(t,e):o,d;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")a=Reflect.decorate(s,t,e,o);else for(var c=s.length-1;c>=0;c--)(d=s[c])&&(a=(i<3?d(a):i>3?d(t,e,a):d(t,e))||a);return i>3&&a&&Object.defineProperty(t,e,a),a};let r=class extends L{constructor(){super(...arguments),this.steps=[],this.recordInfoTask=new T(this,{task:async([t,e])=>{try{const i=await(await new k(TYPO3.settings.ajaxUrls.wizard_localization_get_record).withQueryArguments({recordType:t,recordUid:e}).get()).resolve();return this.closest("typo3-backend-modal")!==null&&m.currentModal&&(m.currentModal.modalTitle=`${f.get("localization_wizard.modal.title.record_prefix")} ${i.typeName}, ${i.title}`),i}catch(o){throw console.warn("Failed to fetch record info:",o),o}},args:()=>[this.recordType,this.recordUid]})}connectedCallback(){super.connectedCallback(),this.addEventListener(h.eventName,this.handleStepSummary)}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener(h.eventName,this.handleStepSummary)}firstUpdated(t){super.firstUpdated(t);const e={wizard:this.wizard,recordType:this.recordType,recordUid:this.recordUid,recordInfo:this.recordInfoTask.value,targetLanguage:this.targetLanguage,getStoreData:this.wizard.getStoreData.bind(this.wizard),setStoreData:this.wizard.setStoreData.bind(this.wizard),clearStoreData:this.wizard.clearStoreData.bind(this.wizard),getDataStore:this.wizard.getDataStore.bind(this.wizard),dispatchAutoAdvance:()=>this.wizard.dispatchEvent(new D)};this.submissionService=new $(e),this.steps=[new b(e),new z(e),...this.recordType==="pages"?[new S(e)]:[],new g(e),new v(e)]}createRenderRoot(){return this}render(){return u``}handleStepSummary(t){const e=this.recordInfoTask.value;t.detail.summaryData=[{label:e.typeName,value:u`${e.title} [${e.type}:${e.uid}]`},...t.detail.summaryData]}};n([p({type:String,attribute:"record-type"})],r.prototype,"recordType",void 0),n([p({type:Number,attribute:"record-uid"})],r.prototype,"recordUid",void 0),n([p({type:Number,attribute:"target-language"})],r.prototype,"targetLanguage",void 0),n([y("typo3-backend-wizard")],r.prototype,"wizard",void 0),n([l()],r.prototype,"steps",void 0),n([l()],r.prototype,"submissionService",void 0),r=n([w("typo3-backend-localization-wizard")],r);export{r as LocalizationWizard}; diff --git a/Resources/Public/JavaScript/localization/steps/content-record-selection-step.js b/Resources/Public/JavaScript/localization/steps/content-record-selection-step.js new file mode 100644 index 0000000..6dc3544 --- /dev/null +++ b/Resources/Public/JavaScript/localization/steps/content-record-selection-step.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as l,nothing as p}from"lit";import{live as d}from"lit/directives/live.js";import{styleMap as f}from"lit/directives/style-map.js";import{repeat as u}from"lit/directives/repeat.js";import{Task as m,TaskStatus as h}from"@lit/task";import y from"@typo3/core/ajax/ajax-request.js";import"@typo3/backend/element/alert-element.js";import"@typo3/backend/element/icon-element.js";import"@typo3/backend/element/spinner-element.js";import n from"~labels/backend.wizards.localization";class g{constructor(e){this.context=e,this.key="contentRecordSelection",this.title=n.get("step.content_selection.title"),this.autoAdvance=!0,this.lastTargetLanguage=null,this.lastSourceLanguage=null,this.hasDispatchedAutoAdvance=!1,this.hasUserInteracted=!1,this.selectedRecordUids=[],this.task=new m(this.context.wizard,{task:async([t,i,s])=>await(await new y(TYPO3.settings.ajaxUrls.wizard_localization_get_content).withQueryArguments({pageUid:t,targetLanguage:i,sourceLanguage:s}).get()).resolve(),args:()=>[this.context.recordUid,this.context.getStoreData("targetLanguage"),this.context.getStoreData("sourceLanguage")],autoRun:!1})}isComplete(){return!0}render(){const e=this.context.getStoreData("targetLanguage"),t=this.context.getStoreData("sourceLanguage");return(this.lastTargetLanguage!==e||this.lastSourceLanguage!==t||this.task.status===h.INITIAL)&&(this.lastTargetLanguage=e,this.lastSourceLanguage=t,this.hasDispatchedAutoAdvance=!1,this.hasUserInteracted=!1,this.task.run()),this.task.render({complete:s=>{if(this.getValue().length===0&&!this.hasUserInteracted){const o=this.context.getStoreData("selectedRecordUids");o&&o.length>0&&this.setValue(o)}if(s.layout.elementCount===0&&!this.hasDispatchedAutoAdvance)return this.hasDispatchedAutoAdvance=!0,this.context.dispatchAutoAdvance(),this.context.wizard.renderLoader();const a=this.getAllRecordUids(s.layout);this.getValue().length===0&&a.length>0&&!this.hasUserInteracted&&this.setValue(a);const c=a.length>0&&a.every(o=>this.getValue().includes(o));return l`

    ${n.get("step.content_selection.headline")}

    ${n.get("step.content_selection.description")}

    ${a.length>0?l``:p}
    ${a.length===0?l``:l`
    ${this.renderLayout(s.layout)}
    `}
    `},error:s=>this.context.wizard.renderError(n.get("step.content_selection.error.message"),s),pending:()=>this.context.wizard.renderLoader()})}reset(){this.setValue([]),this.context.clearStoreData("selectedRecordUids"),this.hasUserInteracted=!1}getValue(){return this.selectedRecordUids}setValue(e){this.selectedRecordUids=e,this.context.setStoreData("selectedRecordUids",e)}beforeAdvance(){this.context.setStoreData("selectedRecordUids",this.getValue())}getSelectedRecordsWithDetails(){const e=this.getValue(),t=[];return this.task.value&&this.task.value.layout.rows.forEach(i=>{i.columns.forEach(s=>{s.records.forEach(a=>{e.includes(a.uid)&&t.push(a)})})}),t}getSummaryData(){const e=this.getSelectedRecordsWithDetails();return e.length===0?[]:[{label:n.get("step.content_selection.summary.title"),value:l`
      ${e.map(i=>l`
    • ${i.title}
    • `)}
    `}]}renderLayout(e){let t=1;return l`
    ${e.rows.map(i=>{const s=this.renderRow(i,t);return t+=1,s})}
    `}renderRow(e,t){let i=1;return l`${u(e.columns,s=>s.position,s=>{const a=this.renderColumn(s,t,i);return i+=s.colspan||1,a})}`}renderColumn(e,t,i){const s=e.records.filter(r=>this.getValue().includes(r.uid)),a=s.length===e.records.length,c=s.length>0&&s.length
    ${e.records.length>0?l`
    this.handleColumnToggle(r,e)}>
    `:l`
    ${e.label}
    `}
    ${u(e.records,r=>r.uid,r=>this.renderRecord(r))}
    `}renderRecord(e){return l`
    this.handleRecordToggle(t,e)}>
    `}handleColumnToggle(e,t){const i=e.currentTarget,s=t.records.map(a=>a.uid);if(i.checked){const a=s.filter(c=>!this.getValue().includes(c));this.setValue([...this.getValue(),...a])}else this.setValue(this.getValue().filter(a=>!s.includes(a)));this.hasUserInteracted=!0}handleRecordToggle(e,t){e.currentTarget.checked?this.getValue().includes(t.uid)||this.setValue([...this.getValue(),t.uid]):this.setValue(this.getValue().filter(s=>s!==t.uid)),this.hasUserInteracted=!0}getAllRecordUids(e){const t=[];return e.rows.forEach(i=>{i.columns.forEach(s=>{s.records.forEach(a=>{t.push(a.uid)})})}),t}handleSelectionToggle(){if(!this.task||this.task.status!==h.COMPLETE||!this.task.value)return;this.hasUserInteracted=!0;const e=this.getAllRecordUids(this.task.value.layout),t=this.getValue().length===0;this.setValue(t?e:[])}}export{g as ContentRecordSelectionStep,g as default}; diff --git a/Resources/Public/JavaScript/localization/steps/handler-selection-step.js b/Resources/Public/JavaScript/localization/steps/handler-selection-step.js new file mode 100644 index 0000000..2ad69de --- /dev/null +++ b/Resources/Public/JavaScript/localization/steps/handler-selection-step.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as r}from"lit";import{live as c}from"lit/directives/live.js";import{Task as d,TaskStatus as h}from"@lit/task";import u from"@typo3/core/ajax/ajax-request.js";import i from"~labels/backend.wizards.localization";class p{constructor(e){this.context=e,this.key="handler",this.title=i.get("step.handler_selection.title"),this.autoAdvance=!0,this.hasDispatchedAutoAdvance=!1,this.selectedHandler=null,this.task=new d(this.context.wizard,{task:async([t,a,n,s,l])=>{if(n==null||s==null||l==null)return[];try{return await(await new u(TYPO3.settings.ajaxUrls.wizard_localization_get_handlers).withQueryArguments({recordType:t,recordUid:a,sourceLanguage:n,targetLanguage:s,mode:l}).get()).resolve()}catch(o){return console.warn("Failed to fetch handlers:",o),[]}},args:()=>[this.context.recordType,this.context.recordUid,this.context.getStoreData("sourceLanguage"),this.context.getStoreData("targetLanguage"),this.context.getStoreData("localizationMode")],autoRun:!1})}isComplete(){return this.getValue()!==null}render(){return this.task.status===h.INITIAL&&this.task.run(),this.task.render({complete:e=>{if(this.getValue()==null){const a=this.context.getStoreData("localizationHandler");a!=null&&this.setValue(a)}let t=!1;return this.getValue()==null&&e.length>0&&(this.setValue(e[0].identifier),e.length===1&&(t=!0)),t&&!this.hasDispatchedAutoAdvance?(this.hasDispatchedAutoAdvance=!0,this.context.dispatchAutoAdvance(),this.context.wizard.renderLoader()):e.length===0?r`

    ${i.get("step.handler_selection.no_handlers")}

    `:r`

    ${i.get("step.handler_selection.headline")}

    ${i.get("step.handler_selection.description")}

    ${e.map(a=>this.renderHandlerOption(a))}
    `},error:e=>this.context.wizard.renderError(i.get("step.handler_selection.error"),e),pending:()=>this.context.wizard.renderLoader()})}reset(){this.setValue(null),this.context.clearStoreData("localizationHandler")}getValue(){return this.selectedHandler}setValue(e){this.selectedHandler=e}beforeAdvance(){this.context.setStoreData("localizationHandler",this.getValue())}getSummaryData(){const e=this.context.getStoreData("localizationHandler");if(e==null||!this.task.value)return[];const t=this.task.value.find(a=>a.identifier===e);return t?[{label:i.get("step.handler_selection.summary_label"),value:r`${t.label}`}]:[]}renderHandlerOption(e){const t=this.getValue()===e.identifier;return r`
    this.setValue(e.identifier)}>
    `}}export{p as HandlerSelectionStep}; diff --git a/Resources/Public/JavaScript/localization/steps/mode-step.js b/Resources/Public/JavaScript/localization/steps/mode-step.js new file mode 100644 index 0000000..a537916 --- /dev/null +++ b/Resources/Public/JavaScript/localization/steps/mode-step.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{nothing as c,html as o}from"lit";import{live as d}from"lit/directives/live.js";import{unsafeHTML as h}from"lit/directives/unsafe-html.js";import{Task as u,TaskStatus as p}from"@lit/task";import m from"@typo3/core/ajax/ajax-request.js";import i from"~labels/backend.wizards.localization";class l{constructor(e){this.context=e,this.key="mode",this.title=i.get("step.modes.title"),this.autoAdvance=!0,this.hasDispatchedAutoAdvance=!1,this.selectedMode=null,this.task=new u(this.context.wizard,{task:async([a,s,t,r])=>{if(t==null||r==null)return[];try{return await(await new m(TYPO3.settings.ajaxUrls.wizard_localization_get_modes).withQueryArguments({recordType:a,recordUid:s,targetLanguage:t,sourceLanguage:r}).get()).resolve()}catch(n){return console.warn("Failed to fetch localization modes:",n),[]}},args:()=>[this.context.recordType,this.context.recordUid,this.context.getStoreData("targetLanguage"),this.context.getStoreData("sourceLanguage")],autoRun:!1})}isComplete(){return this.getValue()!==null}render(){return this.task.status===p.INITIAL&&this.task.run(),this.task.render({complete:e=>{if(this.getValue()==null){const t=this.context.getStoreData("localizationMode");t!=null&&this.setValue(t)}let a=!1;if(this.getValue()==null&&e.length>0&&(this.setValue(e[0].key),e.length===1&&(a=!0)),a&&!this.hasDispatchedAutoAdvance)return this.hasDispatchedAutoAdvance=!0,this.context.dispatchAutoAdvance(),this.context.wizard.renderLoader();let s=c;return e.length===0?s=o`

    ${i.get("step.modes.none_available")}

    `:s=o`

    ${i.get("step.modes.description")}

    ${e.map(t=>o`
    this.setValue(t.key)}>
    `)}
    `,o`

    ${i.get("step.modes.headline")}

    ${s}
    `},error:e=>this.context.wizard.renderError(i.get("step.modes.error.message"),e),pending:()=>this.context.wizard.renderLoader()})}reset(){this.setValue(null),this.context.clearStoreData("localizationMode")}getValue(){return this.selectedMode}setValue(e){this.selectedMode=e}beforeAdvance(){this.context.setStoreData("localizationMode",this.getValue())}getSummaryData(){const e=this.context.getStoreData("localizationMode");if(!e||!this.task.value)return[];const a=this.task.value.find(s=>s.key===e);return a?[{label:i.get("step.modes.summary.title"),value:o`${a.label}`}]:[]}}export{l as ModeStep,l as default}; diff --git a/Resources/Public/JavaScript/localization/steps/source-language-step.js b/Resources/Public/JavaScript/localization/steps/source-language-step.js new file mode 100644 index 0000000..d26638b --- /dev/null +++ b/Resources/Public/JavaScript/localization/steps/source-language-step.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as i}from"lit";import{live as o}from"lit/directives/live.js";import{Task as l,TaskStatus as u}from"@lit/task";import d from"@typo3/core/ajax/ajax-request.js";import s from"~labels/backend.wizards.localization";class c{constructor(t){this.context=t,this.key="sourceLanguage",this.title=s.get("step.source_language.title"),this.autoAdvance=!0,this.hasDispatchedAutoAdvance=!1,this.selectedLanguage=null,this.task=new l(this.context.wizard,{task:async([a,e,r])=>{if(r==null)return[];try{return await(await new d(TYPO3.settings.ajaxUrls.wizard_localization_get_sources).withQueryArguments({recordType:a,recordUid:e,targetLanguage:r}).get()).resolve()}catch(n){return console.warn("Failed to fetch source languages:",n),[]}},args:()=>[this.context.recordType,this.context.recordUid,this.context.getStoreData("targetLanguage")],autoRun:!1})}isComplete(){return this.getValue()!=null}render(){return this.task.status===u.INITIAL&&this.task.run(),this.task.render({complete:t=>{if(this.getValue()==null){const e=this.context.getStoreData("sourceLanguage");e!=null&&this.setValue(e)}let a=!1;if(this.getValue()==null&&t.length>0){const e=t.find(r=>r.uid===0)||t[0];this.setValue(e.uid),t.length===1&&(a=!0)}return a&&!this.hasDispatchedAutoAdvance?(this.hasDispatchedAutoAdvance=!0,this.context.dispatchAutoAdvance(),this.context.wizard.renderLoader()):t.length===0?i`

    ${s.get("step.source_language.headline")}

    ${s.get("step.source_language.none_available")}

    `:i`

    ${s.get("step.source_language.headline")}

    ${s.get("step.source_language.description")}

    ${t.map(e=>i`
    this.setValue(e.uid)}>
    `)}
    `},error:t=>this.context.wizard.renderError(s.get("step.source_language.error.message"),t),pending:()=>this.context.wizard.renderLoader()})}reset(){this.setValue(null),this.context.clearStoreData("sourceLanguage")}getValue(){return this.selectedLanguage}setValue(t){this.selectedLanguage=t}beforeAdvance(){this.context.setStoreData("sourceLanguage",this.getValue())}getSummaryData(){const t=this.context.getStoreData("sourceLanguage");if(t==null||!this.task.value)return[];const a=this.task.value.find(e=>e.uid===t);return a?[{label:s.get("step.source_language.summary.title"),value:i`${a.title}`}]:[]}}export{c as SourceLanguageStep,c as default}; diff --git a/Resources/Public/JavaScript/localization/steps/target-language-step.js b/Resources/Public/JavaScript/localization/steps/target-language-step.js new file mode 100644 index 0000000..b09aa8a --- /dev/null +++ b/Resources/Public/JavaScript/localization/steps/target-language-step.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as s}from"lit";import{live as g}from"lit/directives/live.js";import{Task as u,TaskStatus as d}from"@lit/task";import h from"@typo3/core/ajax/ajax-request.js";import"@typo3/backend/localization/localization-wizard.js";import r from"~labels/backend.wizards.localization";class c{constructor(t){this.context=t,this.key="targetLanguage",this.title=r.get("step.target_language.title"),this.autoAdvance=!0,this.hasDispatchedAutoAdvance=!1,this.selectedLanguage=null,this.task=new u(this.context.wizard,{task:async([a,e])=>{try{let i=await(await new h(TYPO3.settings.ajaxUrls.wizard_localization_get_targets).withQueryArguments({recordType:a,recordUid:e}).get()).resolve();const l=this.context.targetLanguage;return l!=null&&(i=i.filter(o=>o.uid===l)),i}catch(n){return console.warn("Failed to fetch target languages:",n),[]}},args:()=>[this.context.recordType,this.context.recordUid],autoRun:!1})}isComplete(){return this.getValue()!=null}render(){return this.task.status===d.INITIAL&&this.task.run(),this.task.render({complete:t=>{if(this.getValue()==null){const e=this.context.getStoreData("targetLanguage");e!=null&&this.setValue(e)}let a=!1;if(this.getValue()==null){const e=this.context.targetLanguage;if(e!=null?(this.setValue(e),a=!0):t.length>0&&(this.setValue(t[0].uid),t.length===1&&(a=!0)),a&&!this.hasDispatchedAutoAdvance)return this.hasDispatchedAutoAdvance=!0,this.context.dispatchAutoAdvance(),this.context.wizard.renderLoader()}return t.length===0?s`

    ${r.get("step.target_language.headline")}

    ${r.get("step.target_language.none_available")}

    `:s`

    ${r.get("step.target_language.headline")}

    ${r.get("step.target_language.description")}

    ${t.map(e=>s`
    this.setValue(e.uid)}>
    `)}
    `},error:t=>this.context.wizard.renderError(r.get("step.target_language.error.message"),t),pending:()=>this.context.wizard.renderLoader()})}reset(){this.setValue(null),this.context.clearStoreData("targetLanguage")}getValue(){return this.selectedLanguage}setValue(t){this.selectedLanguage=t}beforeAdvance(){this.context.setStoreData("targetLanguage",this.getValue())}getSummaryData(){const t=this.context.getStoreData("targetLanguage");if(t==null||!this.task.value)return[];const a=this.task.value.find(e=>e.uid===t);return a?[{label:r.get("step.target_language.summary.title"),value:s`${a.title}`}]:[]}}export{c as TargetLanguageStep,c as default}; diff --git a/Resources/Public/JavaScript/login-refresh.js b/Resources/Public/JavaScript/login-refresh.js new file mode 100644 index 0000000..bfe38b2 --- /dev/null +++ b/Resources/Public/JavaScript/login-refresh.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as u,LitElement as y}from"lit";import{state as M,customElement as F}from"lit/decorators.js";import m,{Sizes as f,Styles as p}from"@typo3/backend/modal.js";import{SeverityEnum as v}from"@typo3/backend/enum/severity.js";import c from"@typo3/core/ajax/ajax-request.js";import b from"@typo3/backend/notification.js";import"@typo3/backend/element/progress-bar-element.js";import o from"~labels/core.core";var L=function(r,e,t,n){var i=arguments.length,s=i<3?e:n===null?n=Object.getOwnPropertyDescriptor(e,t):n,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")s=Reflect.decorate(r,e,t,n);else for(var l=r.length-1;l>=0;l--)(a=r[l])&&(s=(i<3?a(s):i>3?a(e,t,s):a(e,t))||s);return i>3&&s&&Object.defineProperty(e,t,s),s},h;(function(r){r.loginrefresh="t3js-modal-loginrefresh",r.lockedModal="t3js-modal-backendlocked",r.loginFormModal="t3js-modal-backendloginform"})(h||(h={}));class P{constructor(){this.intervalTime=60,this.intervalId=null,this.backendIsLocked=!1,this.timeoutModal=null,this.backendLockedModal=null,this.loginForm=null,this.requestTokenUrl="",this.loginFramesetUrl="",this.logoutUrl="",this.submitForm=async(e,t)=>{e.preventDefault();const i=await(await new c(this.requestTokenUrl).post({})).resolve("application/json");if(!i.headerName||!i.requestToken)return;const s=t.querySelector("input[name=p_field]"),a=t.querySelector("input[name=userident]"),l=s.value;if(l===""&&a.value===""){b.error(o.get("mess.refresh_login_failed"),o.get("mess.refresh_login_emptyPassword")),s.focus();return}l&&(a.value=l,s.value="");const k={login_status:"login"};for(const[_,T]of new FormData(t))k[_]=T.toString();const w=new Headers;w.set(i.headerName,i.requestToken),(await(await new c(t.getAttribute("action")).post(k,{headers:w})).resolve()).login.success?this.hideLoginForm():(b.error(o.get("mess.refresh_login_failed"),o.get("mess.refresh_login_failed_message")),s.focus())},this.checkActiveSession=async()=>{try{const t=await(await new c(TYPO3.settings.ajaxUrls.login_timedout).get()).resolve();t.login.locked?this.backendIsLocked||(this.backendIsLocked=!0,this.showBackendLockedModal()):this.backendIsLocked&&(this.backendIsLocked=!1,this.hideBackendLockedModal()),this.backendIsLocked||(t.login.timed_out||t.login.will_time_out)&&(t.login.timed_out?this.showLoginForm():this.showTimeoutModal())}catch{this.backendIsLocked=!0,this.showBackendLockedModal()}}}initialize(e){typeof e=="object"&&this.applyOptions(e),this.startTask()}startTask(){if(this.intervalId!==null)return;const e=this.intervalTime*1e3;this.intervalId=setInterval(this.checkActiveSession,e)}stopTask(){clearInterval(this.intervalId),this.intervalId=null}setIntervalTime(e){this.intervalTime=Math.min(e,86400)}setLogoutUrl(e){this.logoutUrl=e}setLoginFramesetUrl(e){this.loginFramesetUrl=e}showTimeoutModal(){this.timeoutModal=this.createTimeoutModal(),this.timeoutModal.addEventListener("typo3-modal-hidden",()=>this.timeoutModal=null),this.timeoutModal.addEventListener("show-login-form",()=>{this.timeoutModal.hideModal(),this.showLoginForm()})}hideTimeoutModal(){this.timeoutModal?.hideModal()}showBackendLockedModal(){this.backendLockedModal||(this.backendLockedModal=this.createBackendLockedModal(),this.backendLockedModal.addEventListener("typo3-modal-hidden",()=>this.backendLockedModal=null))}hideBackendLockedModal(){this.backendLockedModal?.hideModal()}showLoginForm(){this.loginForm||new c(TYPO3.settings.ajaxUrls.logout).get().then(()=>{TYPO3.configuration.showRefreshLoginPopup?this.showLoginPopup():(this.loginForm=this.createLoginFormModal(),this.loginForm.addEventListener("typo3-modal-hidden",()=>this.loginForm=null))})}showLoginPopup(){const e=window.open(this.loginFramesetUrl,"relogin_"+Math.random().toString(16).slice(2),"height=450,width=700,status=0,menubar=0,location=1");e&&e.focus()}hideLoginForm(){this.loginForm?.hideModal()}createBackendLockedModal(){return m.advanced({additionalCssClasses:[h.lockedModal],title:o.get("mess.please_wait"),severity:v.notice,style:p.light,size:f.small,staticBackdrop:!0,hideCloseButton:!0,content:u`

    ${o.get("mess.be_locked")}

    `})}createTimeoutModal(){const e=m.advanced({additionalCssClasses:[h.loginrefresh],title:o.get("mess.login_about_to_expire_title"),severity:v.notice,style:p.light,size:f.small,staticBackdrop:!0,hideCloseButton:!0,buttons:[{text:o.get("mess.refresh_login_logout_button"),active:!1,btnClass:"btn-default",name:"logout",trigger:()=>top.location.href=this.logoutUrl},{text:o.get("mess.refresh_login_refresh_button"),active:!0,btnClass:"btn-primary",name:"refreshSession",trigger:async(t,n)=>{const s=await(await new c(TYPO3.settings.ajaxUrls.login_refresh).get()).resolve();n.hideModal(),s.refresh.success||n.dispatchEvent(new Event("show-login-form"))}}],content:u`

    ${o.get("mess.login_about_to_expire")}

    e.dispatchEvent(new Event("show-login-form"))}>`});return e.addEventListener("typo3-modal-hidden",()=>{this.startTask()}),e.addEventListener("typo3-modal-shown",()=>{this.stopTask()}),e}createLoginFormModal(){const e=o.get("mess.refresh_login_title",[TYPO3.configuration.username]),t=m.advanced({additionalCssClasses:[h.loginFormModal],title:e,severity:v.notice,style:p.light,size:f.small,staticBackdrop:!0,hideCloseButton:!0,buttons:[{text:o.get("mess.refresh_exit_button"),active:!1,btnClass:"btn-default",name:"logout",trigger:()=>top.location.href=this.logoutUrl},{text:o.get("mess.refresh_login_button"),active:!1,btnClass:"btn-primary",name:"refreshSession",trigger:async(n,i)=>{i.querySelector("form").requestSubmit();const a=await(await new c(TYPO3.settings.ajaxUrls.login_refresh).get()).resolve();i.hideModal(),a.refresh.success||i.dispatchEvent(new Event("show-login-form"))}}],content:u`

    ${o.get("mess.login_expired")}

    this.submitForm(n,n.currentTarget)}>
    `});return t.addEventListener("typo3-modal-hidden",()=>{this.startTask()}),t.addEventListener("typo3-modal-shown",()=>{this.stopTask()}),t}applyOptions(e){e.intervalTime!==void 0&&this.setIntervalTime(e.intervalTime),e.loginFramesetUrl!==void 0&&this.setLoginFramesetUrl(e.loginFramesetUrl),e.logoutUrl!==void 0&&this.setLogoutUrl(e.logoutUrl),e.requestTokenUrl!==void 0&&(this.requestTokenUrl=e.requestTokenUrl)}}let g=class extends y{constructor(){super(...arguments),this.current=0,this.max=100,this.advanceProgressBar=()=>{this.current++,this.current>=this.max&&this.dispatchEvent(new Event("progress-bar-overdue"))}}connectedCallback(){super.connectedCallback(),this.intervalId&&clearInterval(this.intervalId),this.intervalId=setInterval(this.advanceProgressBar,300)}disconnectedCallback(){super.disconnectedCallback(),this.intervalId&&(clearInterval(this.intervalId),this.intervalId=null)}createRenderRoot(){return this}render(){return u``}};L([M()],g.prototype,"current",void 0),g=L([F("typo3-login-refresh-progress-bar")],g);let d;try{window.opener&&window.opener.TYPO3&&window.opener.TYPO3.LoginRefresh&&(d=window.opener.TYPO3.LoginRefresh),parent&&parent.window.TYPO3&&parent.window.TYPO3.LoginRefresh&&(d=parent.window.TYPO3.LoginRefresh),top&&top.TYPO3&&top.TYPO3.LoginRefresh&&(d=top.TYPO3.LoginRefresh)}catch{}d||(d=new P,typeof TYPO3<"u"&&(TYPO3.LoginRefresh=d));var O=d;export{g as SelfFillingProgressBarElement,O as default}; diff --git a/Resources/Public/JavaScript/login.js b/Resources/Public/JavaScript/login.js new file mode 100644 index 0000000..ad6a568 --- /dev/null +++ b/Resources/Public/JavaScript/login.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import"bootstrap";import"@typo3/backend/input/clearable.js";import a from"@typo3/core/ajax/ajax-request.js";import n from"@typo3/core/event/regular-event.js";import r from"@typo3/backend/login/pasted-password-checker.js";import c from"@typo3/core/document-service.js";class d{constructor(){this.ready=!0,this.options={error:".t3js-login-error",errorNoCookies:".t3js-login-error-nocookies",errorNoReferrer:".t3js-login-error-noreferrer",warningPasswordWhitespace:".t3js-login-warning-password-whitespace",warningPasswordWhitespaceActionLink:".t3js-login-warning-password-whitespace-action",formFields:".t3js-login-formfields",loginForm:"#typo3-login-form",loginUrlLink:"t3js-login-url",submitButton:".t3js-login-submit",submitHandler:null,useridentField:".t3js-login-userident-field",passwordField:"#t3-password"},this.checkLoginRefresh(),this.checkCookieSupport(),this.checkDocumentReferrerSupport(),this.initializeEvents(),top.location.href!==location.href&&(this.ready=!1,top.location.href=location.href),this.ready&&document.body.setAttribute("data-typo3-login-ready","true")}showLoginProcess(){this.showLoadingIndicator(),document.querySelector(this.options.error)?.classList.add("hidden"),document.querySelector(this.options.errorNoCookies)?.classList.add("hidden")}showLoadingIndicator(){const e=document.querySelector(this.options.submitButton);e.innerHTML=e.dataset.loadingText}handleSubmit(e){this.showLoginProcess(),typeof this.options.submitHandler=="function"&&this.options.submitHandler(e)}checkDocumentReferrerSupport(){const e=document.getElementById(this.options.loginUrlLink);e!==null&&typeof e.dataset.referrerCheckEnabled>"u"&&e.dataset.referrerCheckEnabled!=="1"||typeof TYPO3.settings>"u"||typeof TYPO3.settings.ajaxUrls>"u"||new a(TYPO3.settings.ajaxUrls.login_preflight).get().then(async o=>{(await o.resolve("application/json")).capabilities.referrer!==!0&&document.querySelectorAll(this.options.errorNoReferrer).forEach(i=>i.classList.remove("hidden"))})}showCookieWarning(){document.querySelector(this.options.formFields)?.classList.add("hidden"),document.querySelector(this.options.errorNoCookies)?.classList.remove("hidden")}showWhitespaceAroundPasswordWarning(){document.querySelector(this.options.warningPasswordWhitespace)?.classList.remove("hidden")}removeWhitespaceAroundPasswordWarning(){document.querySelector(this.options.warningPasswordWhitespace)?.classList.add("hidden")}checkLoginRefresh(){const e=document.querySelector(this.options.loginForm+' input[name="loginRefresh"]');e instanceof HTMLInputElement&&e.value&&window.opener&&window.opener.TYPO3&&window.opener.TYPO3.LoginRefresh&&(window.opener.TYPO3.LoginRefresh.startTask(),window.close())}checkCookieSupport(){const e=navigator.cookieEnabled;e===!1?this.showCookieWarning():!document.cookie&&e===null&&(document.cookie="typo3-login-cookiecheck=1",document.cookie?document.cookie="typo3-login-cookiecheck=; expires="+new Date(0).toUTCString():this.showCookieWarning())}async initializeEvents(){await c.ready();const e=document.querySelector(this.options.loginForm);e!==null&&new n("submit",this.handleSubmit.bind(this)).bindTo(e),document.querySelectorAll(".t3js-clearable").forEach(i=>i.clearable());const o=document.querySelector(this.options.passwordField);if(o===null)return;new n("paste",i=>{const s=i.clipboardData.getData("text/plain");r.hasSurroundingWhitespace(s)&&this.showWhitespaceAroundPasswordWarning()}).bindTo(o);const t=document.querySelector(this.options.warningPasswordWhitespaceActionLink);t!==null&&new n("click",()=>{o.value=r.removeSurroundingWhitespace(o.value),this.removeWhitespaceAroundPasswordWarning()}).bindTo(t)}}var l=new d;export{l as default}; diff --git a/Resources/Public/JavaScript/login/pasted-password-checker.js b/Resources/Public/JavaScript/login/pasted-password-checker.js new file mode 100644 index 0000000..c1eb51f --- /dev/null +++ b/Resources/Public/JavaScript/login/pasted-password-checker.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +var r={hasSurroundingWhitespace:e=>e.match(/^(\s+.+(?:\s+)?|(?:\s+)?.+\s+)$/)!==null,removeSurroundingWhitespace:e=>e.replace(/^\s+/,"").replace(/\s+$/,"")};export{r as default}; diff --git a/Resources/Public/JavaScript/mail-link-handler.js b/Resources/Public/JavaScript/mail-link-handler.js new file mode 100644 index 0000000..6812554 --- /dev/null +++ b/Resources/Public/JavaScript/mail-link-handler.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import r from"@typo3/backend/link-browser.js";import m from"@typo3/core/event/regular-event.js";class c{constructor(){new m("submit",(o,t)=>{o.preventDefault();const i=t.querySelector('[name="lemail"]').value,e=new URLSearchParams;for(const a of["subject","cc","bcc","body"]){const l=t.querySelector('[data-mailto-part="'+a+'"]');l?.value.length&&e.set(a,encodeURIComponent(l.value))}let n="mailto:"+i;[...e].length>0&&(n+="?"+e.toString()),r.finalizeFunction(n)}).delegateTo(document,"#lmailform")}}var u=new c;export{u as default}; diff --git a/Resources/Public/JavaScript/modal.js b/Resources/Public/JavaScript/modal.js new file mode 100644 index 0000000..955c655 --- /dev/null +++ b/Resources/Public/JavaScript/modal.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as S,html as r,nothing as B}from"lit";import{property as c,state as q,query as N,customElement as Y}from"lit/decorators.js";import{unsafeHTML as A}from"lit/directives/unsafe-html.js";import{classMap as D}from"lit/directives/class-map.js";import{ifDefined as $}from"lit/directives/if-defined.js";import{classesArrayToClassInfo as E}from"@typo3/core/lit-helper.js";import I from"@typo3/core/event/regular-event.js";import{SeverityEnum as y}from"@typo3/backend/enum/severity.js";import F from"@typo3/core/ajax/ajax-request.js";import R from"@typo3/backend/severity.js";import"@typo3/backend/element/icon-element.js";import"@typo3/backend/element/spinner-element.js";import U from"~labels/core.core";import j from"~labels/core.mod_web_list";var n=function(s,t,e,i){var a=arguments.length,l=a<3?t:i===null?i=Object.getOwnPropertyDescriptor(t,e):i,u;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")l=Reflect.decorate(s,t,e,i);else for(var m=s.length-1;m>=0;m--)(u=s[m])&&(l=(a<3?u(l):a>3?u(t,e,l):u(t,e))||l);return a>3&&l&&Object.defineProperty(t,e,l),l},T;(function(s){s.modal=".t3js-modal",s.header=".t3js-modal-header",s.close=".t3js-modal-close",s.body=".t3js-modal-body",s.footer=".t3js-modal-footer"})(T||(T={}));var h;(function(s){s.small="small",s.default="default",s.medium="medium",s.large="large",s.full="full",s.expand="expand"})(h||(h={}));var x;(function(s){s.small="small",s.default="default",s.medium="medium",s.large="large",s.full="full"})(x||(x={}));const O=s=>{if(typeof s=="string")return s in h;if(typeof s!="object"||s===null)return!1;const t=s;return Object.keys(s).length>0&&Object.keys(s).every(e=>e==="width"||e==="height")&&(t.width===void 0||t.width in x)&&(t.height===void 0||t.height in x)};var p;(function(s){s.center="center",s.top="top",s.end="end",s.bottom="bottom",s.start="start",s.sheet="sheet"})(p||(p={}));var v;(function(s){s.default="default",s.light="light",s.dark="dark"})(v||(v={}));var d;(function(s){s.default="default",s.template="template",s.ajax="ajax",s.iframe="iframe"})(d||(d={}));let K=0,o=class extends S{#t;constructor(){super(),this.modalTitle="",this.content="",this.type=d.default,this.severity=y.notice,this.variant=v.default,this.position=p.center,this.staticBackdrop=!1,this.hideCloseButton=!1,this.hideHeader=!1,this.additionalCssClasses=[],this.buttons=[],this.templateResultContent=null,this.activeButton=null,this.callback=null,this.ajaxCallback=null,this.userData={},this.#t=h.default,this.handleIframeKeydown=t=>{if(t.key==="Escape"){if(t.target instanceof t.view.window.HTMLInputElement&&t.target.type==="search"){t.target.value===""&&this.requestClose();return}if(t.defaultPrevented)return;this.requestClose()}},this.uniqueId=++K}get size(){return this.#t}set size(t){const e=O(t);if(this.#t=e?t:h.default,!e&&this.isConnected){const i=typeof this.#t=="string"?this.#t:JSON.stringify(this.#t);this.getAttribute("size")!==i&&this.setAttribute("size",i)}}setContent(t){this.templateResultContent=t}hideModal(){this.doHideModal()}async doHideModal(){if(this.trigger("typo3-modal-hide",!0).defaultPrevented)return;this.dialog.classList.add("modal-closing");const e=new Promise(a=>this.dialog.addEventListener("transitionend",a,{once:!0})),i=new Promise(a=>setTimeout(a,305));await Promise.race([e,i]),this.dialog.classList.remove("modal-closing"),this.dialog.close()}createRenderRoot(){return this}async showModal(){await new Promise(i=>requestAnimationFrame(i)),this.trigger("typo3-modal-show"),this.dialog.showModal();const t=new Promise(i=>this.dialog.addEventListener("transitionend",i,{once:!0})),e=new Promise(i=>setTimeout(i,305));await Promise.race([t,e]),this.trigger("typo3-modal-shown")}firstUpdated(){this.showModal(),this.callback&&this.callback(this)}updated(t){t.has("templateResultContent")&&this.dispatchEvent(new CustomEvent("modal-updated",{bubbles:!0}))}render(){const t=E(["modal","t3js-modal",`modal-type-${this.type}`,`modal-style-${this.variant}`,`modal-severity-${R.getCssClass(this.severity)}`,...this.getSizeClasses(),`modal-position-${this.position}`,...this.additionalCssClasses]);return r`${this.hideHeader?B:r``}${this.buttons.length===0?B:r``}`}getSizeClasses(){if(typeof this.size=="string")return[`modal-size-${this.size}`];const t=[];return this.size.width!==void 0&&t.push(`modal-width-${this.size.width}`),this.size.height!==void 0&&t.push(`modal-height-${this.size.height}`),t}handleDialogClose(){this.trigger("typo3-modal-hidden")}handleDialogCancel(t){this.hideCloseButton&&t.preventDefault(),t.defaultPrevented?this.shake():(t.preventDefault(),this.hideModal())}handleDialogClick(t){t.target===this.dialog&&(this.staticBackdrop?this.shake():this.requestClose())}_buttonClick(t,e){const i=t.currentTarget;e.action?(this.activeButton=e,e.action.execute(i).then(()=>this.hideModal())):e.trigger&&e.trigger(t,this),i.dispatchEvent(new CustomEvent("button.clicked",{bubbles:!0}))}renderAjaxBody(){return this.templateResultContent===null?(new F(this.content).get().then(async t=>{const e=await t.raw().text();this.templateResultContent=r`${A(e)}`,this.updateComplete.then(()=>{this.ajaxCallback&&this.ajaxCallback(this),this.dispatchEvent(new CustomEvent("modal-loaded"))})}).catch(async t=>{const e=await t.raw().text();e?this.templateResultContent=r`${A(e)}`:this.templateResultContent=r`

    Oops, received a ${t.response.status} response from ${this.content}.

    `}),r``):this.templateResultContent}renderModalBody(){if(this.type===d.iframe){const t=e=>{const i=e.currentTarget;i.contentDocument.title&&(this.modalTitle=i.contentDocument.title),i.contentDocument.addEventListener("keydown",this.handleIframeKeydown)};return r``}return this.type===d.ajax?this.renderAjaxBody():this.type===d.template?this.templateResultContent:r`

    ${this.content}

    `}renderModalButton(t){const i={btn:!0,[t.btnClass||"btn-default"]:!0,"t3js-active":t.active,disabled:this.activeButton&&this.activeButton!==t};return r``}trigger(t,e=!1){const i=new CustomEvent(t,{bubbles:!0,composed:!0,cancelable:e});return this.dispatchEvent(i),i}requestClose(){"requestClose"in this.dialog?this.dialog.requestClose():this.hideCloseButton?this.hideModal():this.shake()}shake(){window.matchMedia("(prefers-reduced-motion: reduce)").matches||this.dialog.animate([{transform:"translateX(0px)"},{transform:"translateX(-2px)"},{transform:"translateX(0px)"},{transform:"translateX(2px)"},{transform:"translateX(0px)"}],150)}};n([c({type:String,reflect:!0})],o.prototype,"modalTitle",void 0),n([c({type:String,reflect:!0})],o.prototype,"content",void 0),n([c({type:String,reflect:!0})],o.prototype,"type",void 0),n([c({type:String,reflect:!0})],o.prototype,"severity",void 0),n([c({type:String,reflect:!0})],o.prototype,"variant",void 0),n([c({type:String,reflect:!0})],o.prototype,"position",void 0),n([c({type:Boolean})],o.prototype,"staticBackdrop",void 0),n([c({type:Boolean})],o.prototype,"hideCloseButton",void 0),n([c({type:Boolean})],o.prototype,"hideHeader",void 0),n([c({type:Array})],o.prototype,"additionalCssClasses",void 0),n([c({type:Array,attribute:!1})],o.prototype,"buttons",void 0),n([q()],o.prototype,"templateResultContent",void 0),n([q()],o.prototype,"activeButton",void 0),n([N("dialog",!0)],o.prototype,"dialog",void 0),n([c({reflect:!0,converter:{toAttribute:s=>typeof s=="string"?s:JSON.stringify(s),fromAttribute:s=>{if(s===null)return h.default;if(O(s))return s;try{return JSON.parse(s)}catch{return s}}}})],o.prototype,"size",null),o=n([Y("typo3-backend-modal")],o);class z{constructor(){this.sizes=h,this.styles=v,this.types=d,this.positions=p,this.currentModal=null,this.instances=[],this.defaultConfiguration={type:d.default,title:"Information",content:"No content provided, please check your Modal configuration.",severity:y.notice,buttons:[],style:v.default,size:h.default,position:p.center,additionalCssClasses:[],callback:null,ajaxCallback:null,staticBackdrop:!1,hideCloseButton:!1,hideHeader:!1},this.initializeMarkupTrigger(document)}static createModalResponseEventFromElement(t,e){return t.dataset.eventName?new CustomEvent(t.dataset.eventName,{bubbles:!0,detail:{result:e,payload:t.dataset.eventPayload||null}}):null}dismiss(){this.currentModal&&this.currentModal.hideModal()}confirm(t,e,i=y.warning,a=[],l){a.length===0&&a.push({text:j.get("button.cancel"),active:!0,btnClass:"btn-default",name:"cancel"},{text:j.get("button.ok"),btnClass:"btn-"+R.getCssClass(i),name:"ok"});const u=this.advanced({title:t,content:e,severity:i,buttons:a,additionalCssClasses:l});return u.addEventListener("button.clicked",m=>{const b=m.target;b.getAttribute("name")==="cancel"?b.dispatchEvent(new CustomEvent("confirm.button.cancel",{bubbles:!0})):b.getAttribute("name")==="ok"&&b.dispatchEvent(new CustomEvent("confirm.button.ok",{bubbles:!0}))}),u}loadUrl(t,e=y.info,i,a,l){return this.advanced({type:d.ajax,title:t,severity:e,buttons:i,ajaxCallback:l,content:a})}show(t,e,i=y.info,a,l){return this.advanced({type:d.default,title:t,content:e,severity:i,buttons:a,additionalCssClasses:l})}advanced(t){return t.type=typeof t.type=="string"&&t.type in d?t.type:this.defaultConfiguration.type,t.title=typeof t.title=="string"?t.title:this.defaultConfiguration.title,t.content=typeof t.content=="string"||typeof t.content=="object"?t.content:this.defaultConfiguration.content,t.severity=typeof t.severity<"u"?t.severity:this.defaultConfiguration.severity,t.buttons=t.buttons||this.defaultConfiguration.buttons,t.size=O(t.size)?t.size:this.defaultConfiguration.size,t.style=typeof t.style=="string"&&t.style in v?t.style:this.defaultConfiguration.style,t.position=typeof t.position=="string"&&t.position in p?t.position:this.defaultConfiguration.position,t.additionalCssClasses=t.additionalCssClasses||this.defaultConfiguration.additionalCssClasses,t.callback=typeof t.callback=="function"?t.callback:this.defaultConfiguration.callback,t.ajaxCallback=typeof t.ajaxCallback=="function"?t.ajaxCallback:this.defaultConfiguration.ajaxCallback,t.hideCloseButton=t.hideCloseButton||this.defaultConfiguration.hideCloseButton,t.staticBackdrop=t.staticBackdrop||this.defaultConfiguration.staticBackdrop,t.hideHeader=t.hideHeader||this.defaultConfiguration.hideHeader,this.generate(t)}setButtons(t){return this.currentModal.buttons=t,this.currentModal}initializeMarkupTrigger(t){const e=(i,a)=>{i.preventDefault(),"bsContent"in a.dataset&&!("content"in a.dataset)&&console.error("TYPO3 v14 modal trigger dropped support for the legacy `data-bs-content` attribute. Use `data-content` instead. Affected element:",a);const l=a.dataset.content||U.get("message.confirmation");let u=y.notice;if(a.dataset.severity in y){const f=a.dataset.severity;u=y[f]}let m=h.default;if(a.dataset.size in h){const f=a.dataset.size;m=h[f]}let b=p.center;if(a.dataset.position in p){const f=a.dataset.position;b=p[f]}const H=a.dataset.hideHeader!==void 0,L=a.dataset.staticBackdrop!==void 0;let C=a.dataset.url||null;if(C!==null){const f=C.includes("?")?"&":"?",w=new URLSearchParams(a.dataset).toString();C=C+f+w}this.advanced({type:C!==null?d.ajax:d.default,title:a.dataset.title||"Alert",content:C!==null?C:l,size:m,severity:u,position:b,hideHeader:H,staticBackdrop:L,buttons:[{text:a.dataset.buttonCloseText||j.get("button.close"),active:!0,btnClass:"btn-default",trigger:(f,w)=>{w.hideModal();const M=z.createModalResponseEventFromElement(a,!1);M!==null&&a.dispatchEvent(M)}},{text:a.dataset.buttonOkText||j.get("button.ok"),btnClass:"btn-"+R.getCssClass(u),trigger:(f,w)=>{w.hideModal();const M=z.createModalResponseEventFromElement(a,!0);M!==null&&a.dispatchEvent(M);const g=a.dataset.uri||a.dataset.href||a.getAttribute("href");if(g&&g!=="#"&&(a.ownerDocument.location.href=g),a.getAttribute("type")==="submit"&&(a.tagName==="BUTTON"||a.tagName==="INPUT")){const P=a;P.form?.requestSubmit(P)}a.dataset.targetForm&&a.ownerDocument.querySelector("form#"+a.dataset.targetForm)?.submit()}}]})};new I("click",e).delegateTo(t,".t3js-modal-trigger")}generate(t){const e=document.createElement("typo3-backend-modal");return e.type=t.type,typeof t.content=="string"?e.content=t.content:t.type===d.default&&(e.type=d.template,e.templateResultContent=t.content),e.severity=t.severity,e.variant=t.style,e.size=t.size,e.position=t.position,e.modalTitle=t.title,e.additionalCssClasses=t.additionalCssClasses,e.buttons=t.buttons,e.hideCloseButton=t.hideCloseButton,e.staticBackdrop=t.staticBackdrop,e.hideHeader=t.hideHeader,t.callback&&(e.callback=t.callback),t.ajaxCallback&&(e.ajaxCallback=t.ajaxCallback),e.addEventListener("typo3-modal-shown",()=>{const i=e.querySelector(`${T.footer} .t3js-active`);i!==null&&i.focus()}),e.addEventListener("typo3-modal-hide",()=>{if(this.instances.length>0){const i=this.instances.length-1;this.instances.splice(i,1),this.currentModal=this.instances[i-1]}}),e.addEventListener("typo3-modal-hidden",()=>{e.remove()}),e.addEventListener("typo3-modal-show",()=>{this.currentModal=e,this.instances.push(e)}),document.body.appendChild(e),e}}let k=null;try{parent&&parent.window.TYPO3&&parent.window.TYPO3.Modal?(parent.window.TYPO3.Modal.initializeMarkupTrigger(document),k=parent.window.TYPO3.Modal):top&&top.TYPO3.Modal&&(top.TYPO3.Modal.initializeMarkupTrigger(document),k=top.TYPO3.Modal)}catch{}k||(k=new z,typeof TYPO3<"u"&&(TYPO3.Modal=k));var X=k;export{T as Identifiers,o as ModalElement,p as Positions,x as Size,h as Sizes,v as Styles,d as Types,X as default}; diff --git a/Resources/Public/JavaScript/module-menu.js b/Resources/Public/JavaScript/module-menu.js new file mode 100644 index 0000000..e63c9cd --- /dev/null +++ b/Resources/Public/JavaScript/module-menu.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{ModuleUtility as u,flushModuleCache as h,ModuleSelector as C}from"@typo3/backend/module.js";import d from"@typo3/backend/storage/persistent.js";import m from"@typo3/backend/viewport.js";import I from"@typo3/backend/event/client-request.js";import f from"@typo3/backend/event/trigger-request.js";import E from"@typo3/core/ajax/ajax-request.js";import c from"@typo3/core/event/regular-event.js";import{ModuleStateStorage as v}from"@typo3/backend/storage/module-state-storage.js";import{selector as b}from"@typo3/core/literals.js";import y from"@typo3/core/document-service.js";import{Collapse as S}from"bootstrap";import{KeyTypesEnum as a}from"@typo3/backend/enum/key-types.js";var l;(function(p){p.menu="[data-modulemenu]",p.item="[data-modulemenu-identifier]",p.collapsible='[data-modulemenu-collapsible="true"]'})(l||(l={}));class o{constructor(){this.loadedModule=null,y.ready().then(()=>{this.initialize()})}static getModuleMenuItemFromElement(e){return{identifier:e.dataset.modulemenuIdentifier,level:e.parentElement.dataset.modulemenuLevel?parseInt(e.parentElement.dataset.modulemenuLevel,10):null,collapsible:e.dataset.modulemenuCollapsible==="true",expanded:e.attributes.getNamedItem("aria-expanded")?.value==="true",element:e}}static getCollapsedMainMenuItems(){return d.isset("modulemenu")?JSON.parse(d.get("modulemenu")):{}}static addCollapsedMainMenuItem(e){const n=o.getCollapsedMainMenuItems();n[e]=!0,d.set("modulemenu",JSON.stringify(n))}static removeCollapseMainMenuItem(e){const n=this.getCollapsedMainMenuItems();delete n[e],d.set("modulemenu",JSON.stringify(n))}static includeId(e,n){if(!e.navigationComponentId||n.includes("id"))return n;let t="";e.navigationComponentId==="@typo3/backend/tree/page-tree-element"?t="web":t=e.name.split("_")[0];const i=v.current(t);return i.identifier&&(n="id="+encodeURIComponent(i.identifier)+"&"+n),n}static toggleModuleGroup(e,n){const t=o.getModuleMenuItemFromElement(e),i=t.element.closest(".modulemenu-group"),r=i.querySelector(".modulemenu-group-container"),s=S.getOrCreateInstance(r,{toggle:!1});if(n===void 0)n=!t.expanded;else if(n===t.expanded)return;n?(o.removeCollapseMainMenuItem(t.identifier),s.show()):(o.addCollapsedMainMenuItem(t.identifier),s.hide()),i.classList.toggle("modulemenu-group-collapsed",!n),i.classList.toggle("modulemenu-group-expanded",n),e.setAttribute("aria-expanded",n.toString())}static highlightModule(e){document.querySelector(l.menu).querySelectorAll(l.item).forEach(i=>{i.classList.remove("modulemenu-action-active"),i.removeAttribute("aria-current")});const t=u.getFromName(e);this.highlightModuleMenuItem(t,!0)}static highlightModuleMenuItem(e,n=!0){const i=document.querySelector(l.menu).querySelectorAll(l.item+b`[data-modulemenu-identifier="${e.name}"]`);i.forEach(r=>{r.classList.add("modulemenu-action-active"),n&&r.setAttribute("aria-current","location")}),i.length>0&&(n=!1),e.parent!==""&&this.highlightModuleMenuItem(u.getFromName(e.parent),n)}static getPreviousItem(e){const n=e.parentElement.previousElementSibling;return n===null?o.getLastItem(e):n.firstElementChild}static getNextItem(e){const n=e.parentElement.nextElementSibling;return n===null?o.getFirstItem(e):n.firstElementChild}static getFirstItem(e){return e.parentElement.parentElement.firstElementChild.firstElementChild}static getLastItem(e){return e.parentElement.parentElement.lastElementChild.firstElementChild}static getParentItem(e){return e.parentElement.parentElement.parentElement.firstElementChild}static getFirstChildItem(e){return e.nextElementSibling.firstElementChild.firstElementChild}refreshMenu(){return new E(TYPO3.settings.ajaxUrls.modulemenu).get().then(async e=>{const n=await e.resolve();document.getElementById("modulemenu").outerHTML=n.menu,h(),this.initializeModuleMenuEvents(),this.loadedModule&&o.highlightModule(this.loadedModule)})}getCurrentModule(){return this.loadedModule}reloadFrames(){m.ContentContainer.refresh()}showModule(e,n,t=null,i=null){n=n||"";const r=u.getFromName(e);return this.loadModuleComponents(r,i,n,new I("typo3.showModule",t))}initialize(){document.querySelector(l.menu)!==null&&(this.initializeModuleMenuEvents(),this.initializeModuleLoadListeners())}keyboardNavigation(e,n){const t=o.getModuleMenuItemFromElement(n);let i=null;switch(e.key){case a.UP:i=o.getPreviousItem(t.element);break;case a.DOWN:i=o.getNextItem(t.element);break;case a.LEFT:t.collapsible&&o.toggleModuleGroup(t.element,!1),t.level>1&&(i=o.getParentItem(t.element));break;case a.RIGHT:t.collapsible&&(o.toggleModuleGroup(t.element,!0),i=o.getFirstChildItem(t.element));break;case a.HOME:if(e.ctrlKey&&t.level>1){i=document.querySelector(l.menu+" "+l.item);break}i=o.getFirstItem(t.element);break;case a.END:e.ctrlKey&&t.level>1?i=o.getLastItem(document.querySelector(l.menu+" "+l.item)):i=o.getLastItem(t.element);break;case a.SPACE:case a.ENTER:if(e.preventDefault(),e.repeat)break;t.collapsible?(o.toggleModuleGroup(t.element,!0),i=o.getFirstChildItem(t.element)):t.element.click();break;case a.ESCAPE:t.level>1?i=o.getParentItem(t.element):t.level===1&&t.collapsible&&(i=t.element),i!==null&&o.toggleModuleGroup(i,!1);break;default:i=null}i!==null&&(e.preventDefault(),i.focus())}initializeModuleMenuEvents(){const e=document.querySelector(l.menu);new c("keydown",this.keyboardNavigation).delegateTo(e,l.item),new c("click",(n,t)=>{n.preventDefault();const i=u.getRouteFromElement(t);this.showModule(i.identifier,i.params,n)}).delegateTo(e,C.link),new c("click",(n,t)=>{n.preventDefault(),o.toggleModuleGroup(t)}).delegateTo(e,l.collapsible),new c("shown.bs.collapse",(n,t)=>{t.scrollIntoView({behavior:"smooth",block:"nearest",inline:"start"})}).delegateTo(e,".modulemenu-group")}initializeModuleLoadListeners(){const e=n=>{const t=n.detail.module;if(!t||this.loadedModule===t)return;const i=u.getFromName(t);i.link&&(o.highlightModule(t),this.loadedModule=t,i.navigationComponentId?m.NavigationContainer.showComponent(i.navigationComponentId):m.NavigationContainer.hide())};document.addEventListener("typo3-module-load",e),document.addEventListener("typo3-module-loaded",e)}loadModuleComponents(e,n,t,i){const r=e.name,s=m.ContentContainer.beforeSetUrl(i);return s.then(()=>{e.navigationComponentId?m.NavigationContainer.showComponent(e.navigationComponentId):m.NavigationContainer.hide(),o.highlightModule(r),this.loadedModule=r,t=o.includeId(e,t),this.openInContentContainer(r,n??e.link,t,new f("typo3.loadModuleComponents",i))}),s}openInContentContainer(e,n,t,i){const r=n+(t?(n.includes("?")?"&":"?")+t:"");return m.ContentContainer.setUrl(r,new f("typo3.openInContentFrame",i),e)}}let g=top?.TYPO3?.ModuleMenu;g||(g={App:new o},top.TYPO3!==void 0&&(top.TYPO3.ModuleMenu=g));var k=g;export{k as default}; diff --git a/Resources/Public/JavaScript/module.js b/Resources/Public/JavaScript/module.js new file mode 100644 index 0000000..a3ee730 --- /dev/null +++ b/Resources/Public/JavaScript/module.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +var r;(function(o){o.link="[data-moduleroute-identifier]"})(r||(r={}));class i{static getRouteFromElement(e){return{identifier:e.dataset.modulerouteIdentifier,params:e.dataset.modulerouteParams}}static getFromName(e){const n=d(e);return n===null?{name:e,aliases:[],component:"",navigationComponentId:"",parent:"",link:""}:{name:e,aliases:n.aliases||[],component:n.component||"",navigationComponentId:n.navigationComponentId||"",parent:n.parent||"",link:n.link||""}}}let t=null;function l(){t=null}function u(){if(t===null){const o=String(document.querySelector("[data-modulemenu]")?.dataset.modulesInformation||"");if(o!=="")try{t=JSON.parse(o)}catch{console.error("Invalid modules information provided."),t=null}}return t}function d(o){const e=u();if(e!==null){for(const[n,a]of Object.entries(e))if(o===n||a.aliases.includes(o))return e[n]}return null}export{r as ModuleSelector,i as ModuleUtility,l as flushModuleCache}; diff --git a/Resources/Public/JavaScript/module/iframe.js b/Resources/Public/JavaScript/module/iframe.js new file mode 100644 index 0000000..d407148 --- /dev/null +++ b/Resources/Public/JavaScript/module/iframe.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as s,nothing as m,html as u}from"lit";import{property as f,query as p,customElement as h}from"lit/decorators.js";import g from"~labels/core.core";var c=function(i,e,t,o){var a=arguments.length,n=a<3?e:o===null?o=Object.getOwnPropertyDescriptor(e,t):o,l;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(i,e,t,o);else for(var d=i.length-1;d>=0;d--)(l=i[d])&&(n=(a<3?l(n):a>3?l(e,t,n):l(e,t))||n);return a>3&&n&&Object.defineProperty(e,t,n),n};const b="typo3-iframe-module";let r=class extends s{constructor(){super(...arguments),this.endpoint=""}attributeChangedCallback(e,t,o){super.attributeChangedCallback(e,t,o),e==="endpoint"&&o===t&&this.iframe.setAttribute("src",o)}connectedCallback(){super.connectedCallback(),this.endpoint&&this.dispatch("typo3-iframe-load",{url:this.endpoint,title:null})}createRenderRoot(){return this}render(){return this.endpoint?u``:m}registerPagehideHandler(e){try{e.contentWindow.addEventListener("pagehide",t=>this._pagehide(t,e),{once:!0})}catch(t){throw console.error("Failed to access contentWindow of module iframe \u2013 using a foreign origin?"),t}}retrieveModuleStateFromIFrame(e){try{return{url:e.contentWindow.location.href,title:e.contentDocument.title,module:e.contentDocument.body.querySelector(".module[data-module-name]")?.getAttribute("data-module-name")}}catch{return console.error("Failed to access contentWindow of module iframe \u2013 using a foreign origin?"),{url:this.endpoint,title:null}}}_loaded({target:e}){const t=e;this.registerPagehideHandler(t);const o=this.retrieveModuleStateFromIFrame(t);this.dispatch("typo3-iframe-loaded",o)}_pagehide(e,t){new Promise(o=>window.setTimeout(o,0)).then(()=>{t.contentWindow!==null&&this.dispatch("typo3-iframe-load",{url:t.contentWindow.location.href,title:null})})}dispatch(e,t){this.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,composed:!0}))}};c([f({type:String})],r.prototype,"endpoint",void 0),c([p("iframe",!0)],r.prototype,"iframe",void 0),r=c([h("typo3-iframe-module")],r);export{r as IframeModuleElement,b as componentName}; diff --git a/Resources/Public/JavaScript/module/router.js b/Resources/Public/JavaScript/module/router.js new file mode 100644 index 0000000..133b0d0 --- /dev/null +++ b/Resources/Public/JavaScript/module/router.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as h,css as f,html as b}from"lit";import{property as a,query as y,customElement as g}from"lit/decorators.js";import{ModuleUtility as c}from"@typo3/backend/module.js";var l=function(d,e,t,o){var n=arguments.length,i=n<3?e:o===null?o=Object.getOwnPropertyDescriptor(e,t):o,r;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")i=Reflect.decorate(d,e,t,o);else for(var m=d.length-1;m>=0;m--)(r=d[m])&&(i=(n<3?r(i):n>3?r(e,t,i):r(e,t))||i);return n>3&&i&&Object.defineProperty(e,t,i),i};const u="@typo3/backend/module/iframe",p=()=>!0;let s=class extends h{static{this.styles=f`:host{width:100%;min-height:100%;flex:1 0 auto;display:flex;flex-direction:row}::slotted(*){min-height:100%;width:100%}`}constructor(){super(),this.module="",this.endpoint="",this.sitenameFirst=!1,this.titleComponents=null,this.addEventListener("typo3-module-load",({target:e,detail:t})=>{const o=e.getAttribute("slot");this.pushState({slotName:o,detail:t})}),this.addEventListener("typo3-module-loaded",({detail:e})=>{this.updateBrowserState(e)}),this.addEventListener("typo3-iframe-load",({detail:e})=>{let t={slotName:u,detail:e};if(t.detail.url.includes(this.stateTrackerUrl+"?state=")){const o=t.detail.url.split("?state=");t=JSON.parse(decodeURIComponent(o[1]||"{}"))}this.slotElement.getAttribute("name")!==t.slotName&&this.slotElement.setAttribute("name",t.slotName),this.markActive(t.slotName,this.slotElement.getAttribute("name")===u?null:t.detail.url,!1),this.updateBrowserState(t.detail),this.parentElement.dispatchEvent(new CustomEvent("typo3-module-load",{bubbles:!0,composed:!0,detail:t.detail}))}),this.addEventListener("typo3-iframe-loaded",({detail:e})=>{this.updateBrowserState(e),this.parentElement.dispatchEvent(new CustomEvent("typo3-module-loaded",{bubbles:!0,composed:!0,detail:e}))})}static get observedAttributes(){return[...super.observedAttributes,"sitename-first"]}connectedCallback(){super.connectedCallback(),this.sitenameFirst=this.hasAttribute("sitename-first")}attributeChangedCallback(e,t,o){super.attributeChangedCallback(e,t,o),e==="sitename-first"&&(this.sitenameFirst=o!==null,this.updateBrowserTitle())}render(){const t=c.getFromName(this.module).component||u;return b``}updated(){const t=c.getFromName(this.module).component||u;this.markActive(t,this.endpoint)}async markActive(e,t,o=!0){const n=await this.getModuleElement(e);t&&(o||n.getAttribute("endpoint")!==t)&&n.setAttribute("endpoint",t),n.hasAttribute("active")||n.setAttribute("active","");for(let i=n.previousElementSibling;i!==null;i=i.previousElementSibling)i.removeAttribute("active");for(let i=n.nextElementSibling;i!==null;i=i.nextElementSibling)i.removeAttribute("active")}async getModuleElement(e){let t=this.querySelector(`*[slot="${e}"]`);if(t!==null)return t;try{const o=await import(e+".js");if(t=this.querySelector(`*[slot="${e}"]`),t!==null)return t;if(!("componentName"in o))throw new Error(`module ${e} is missing the "componentName" export`);t=document.createElement(o.componentName)}catch(o){throw console.error({msg:`Error importing ${e} as backend module`,err:o}),o}return t.setAttribute("slot",e),this.appendChild(t),t}async pushState(e){const t=this.stateTrackerUrl+"?state="+encodeURIComponent(JSON.stringify(e));(await this.getModuleElement(u)).setAttribute("endpoint",t)}updateBrowserTitle(){let{titleComponents:e}=this;e!==null&&(this.sitenameFirst&&(e=e.toReversed()),document.title=e.join(" \xB7 "))}updateBrowserState(e){const t=new URL(e.url||"",window.location.origin),o=new URLSearchParams(t.search),n="title"in e?e.title:"";if(n!==null){const r=[this.sitename];n!==""&&r.unshift(n),this.titleComponents=r,this.updateBrowserTitle()}if(o.has("token"))o.delete("token"),t.search=o.toString();else if(o.has("install[controller]")){const r=o.get("install[controller]");t.pathname=this.entryPoint+"module/system/"+r,t.search=""}else return;const i=t.toString();window.history.replaceState(e,"",i)}};l([a({type:String,hasChanged:p})],s.prototype,"module",void 0),l([a({type:String,hasChanged:p})],s.prototype,"endpoint",void 0),l([a({type:String,attribute:"state-tracker"})],s.prototype,"stateTrackerUrl",void 0),l([a({type:String,attribute:"sitename"})],s.prototype,"sitename",void 0),l([a({type:String,attribute:"entry-point"})],s.prototype,"entryPoint",void 0),l([y("slot",!0)],s.prototype,"slotElement",void 0),s=l([g("typo3-backend-module-router")],s);export{s as ModuleRouter}; diff --git a/Resources/Public/JavaScript/multi-record-selection-action.js b/Resources/Public/JavaScript/multi-record-selection-action.js new file mode 100644 index 0000000..2aad0d0 --- /dev/null +++ b/Resources/Public/JavaScript/multi-record-selection-action.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{MultiRecordSelectionSelectors as c}from"@typo3/backend/multi-record-selection.js";class n{static getEntityIdentifiers(t){const i=[];return t.checkboxes.forEach(o=>{const e=o.closest(c.elementSelector);e!==null&&e.dataset[t.configuration.idField]&&i.push(e.dataset[t.configuration.idField])}),i}}export{n as MultiRecordSelectionAction}; diff --git a/Resources/Public/JavaScript/multi-record-selection-delete-action.js b/Resources/Public/JavaScript/multi-record-selection-delete-action.js new file mode 100644 index 0000000..fb16d87 --- /dev/null +++ b/Resources/Public/JavaScript/multi-record-selection-delete-action.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import d from"@typo3/core/event/regular-event.js";import{MultiRecordSelectionAction as u}from"@typo3/backend/multi-record-selection-action.js";import f from"@typo3/backend/modal.js";import{SeverityEnum as c}from"@typo3/backend/enum/severity.js";import g from"@typo3/backend/severity.js";import p from"@typo3/backend/ajax-data-handler.js";import b from"@typo3/backend/notification.js";import h from"~labels/core.common";import w from"~labels/core.mod_web_list";class y{constructor(){new d("multiRecordSelection:action:delete",this.delete).bindTo(document)}delete(t){t.preventDefault();const r=t.detail,n=u.getEntityIdentifiers(r);if(!n.length)return;const e=r.configuration,i=e.tableName||"";if(i==="")return;const l=e.returnUrl||"";f.advanced({title:e.title||"Delete",content:e.content||"Are you sure you want to delete those records?",severity:c.warning,buttons:[{text:e.cancel||h.get("cancel"),active:!0,btnClass:"btn-default",name:"cancel",trigger:(s,o)=>o.hideModal()},{text:e.ok||w.get("button.delete"),btnClass:"btn-"+g.getCssClass(c.warning),name:"delete",trigger:async(s,o)=>{o.hideModal();try{const a=await p.process({cmd:{[i]:Object.fromEntries(n.map(m=>[m,{delete:1}]))}});if(a.hasErrors)throw a.messages;l!==""?t.target.ownerDocument.location.href=l:t.target.ownerDocument.location.reload()}catch{b.error("Could not delete records")}}}]})}}var D=new y;export{D as default}; diff --git a/Resources/Public/JavaScript/multi-record-selection-edit-action.js b/Resources/Public/JavaScript/multi-record-selection-edit-action.js new file mode 100644 index 0000000..e59a21b --- /dev/null +++ b/Resources/Public/JavaScript/multi-record-selection-edit-action.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import r from"@typo3/core/event/regular-event.js";import{MultiRecordSelectionAction as c}from"@typo3/backend/multi-record-selection-action.js";class l{constructor(){new r("multiRecordSelection:action:edit",this.edit).bindTo(document)}edit(t){t.preventDefault();const e=t.detail,n=c.getEntityIdentifiers(e);if(!n.length)return;const o=e.configuration,i=o.tableName||"";i!==""&&(window.location.href=top.TYPO3.settings.FormEngine.moduleUrl+"&edit["+i+"]["+n.join(",")+"]=edit&module="+encodeURIComponent(top.TYPO3.ModuleMenu.App.getCurrentModule())+"&returnUrl="+encodeURIComponent(o.returnUrl||""))}}var d=new l;export{d as default}; diff --git a/Resources/Public/JavaScript/multi-record-selection.js b/Resources/Public/JavaScript/multi-record-selection.js new file mode 100644 index 0000000..4296793 --- /dev/null +++ b/Resources/Public/JavaScript/multi-record-selection.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import f from"@typo3/backend/notification.js";import m from"@typo3/core/document-service.js";import h from"@typo3/core/event/regular-event.js";import{selector as u}from"@typo3/core/literals.js";var a;(function(s){s.actionsSelector=".t3js-multi-record-selection-actions",s.checkboxSelector=".t3js-multi-record-selection-check",s.checkboxActionsSelector=".t3js-multi-record-selection-check-actions",s.checkboxActionsToggleSelector=".t3js-multi-record-selection-check-actions-toggle",s.elementSelector="[data-multi-record-selection-element]"})(a||(a={}));var b;(function(s){s.actionButton="button[data-multi-record-selection-action]",s.checkboxActionButton="button[data-multi-record-selection-check-action]"})(b||(b={}));var d;(function(s){s.checkAll="check-all",s.checkNone="check-none",s.toggle="toggle"})(d||(d={}));var r;(function(s){s.any="",s.checked=":checked",s.unchecked=":not(:checked)"})(r||(r={}));class c{static{this.activeClass="active"}constructor(){this.lastChecked=null,m.ready().then(()=>{c.restoreTemporaryState(),this.registerActions(),this.registerActionsEventHandlers(),this.registerCheckboxActions(),this.registerCheckboxKeyboardActions(),this.registerCheckboxTableRowSelectionAction(),this.registerToggleCheckboxActions(),this.registerDispatchCheckboxStateChangedEvent(),this.registerCheckboxStateChangedEventHandler()})}static getCheckboxes(e=r.any,t=""){return document.querySelectorAll(c.getCombinedSelector(a.checkboxSelector+e,t))}static getCombinedSelector(e,t){return t!==""?[u`[data-multi-record-selection-identifier="${t}"]`,e].join(" "):e}static getIdentifier(e){return e.closest("[data-multi-record-selection-identifier]")?.dataset.multiRecordSelectionIdentifier||""}static changeCheckboxState(e,t){e.disabled||e.checked===t||e.dataset.manuallyChanged||(e.checked=t,e.dispatchEvent(new Event("change",{bubbles:!0})))}static restoreTemporaryState(){const e=c.getCheckboxes(r.checked);if(!e.length)return;let t=!1;const o=[];e.forEach(n=>{n.closest(a.elementSelector)?.classList.add(c.activeClass);const i=c.getIdentifier(n);i!==""&&!o.includes(i)&&(o.push(i),t=!0,c.toggleActionsState(i))}),t||c.toggleActionsState()}static toggleActionsState(e=""){const t=document.querySelectorAll(c.getCombinedSelector(a.actionsSelector,e));if(!t.length)return;if(!c.getCheckboxes(r.checked,e).length){t.forEach(n=>c.changeActionContainerVisibility(n,!1));return}t.forEach(n=>c.changeActionContainerVisibility(n));const o=document.querySelectorAll([c.getCombinedSelector(a.actionsSelector,e),b.actionButton].join(" "));o.length&&o.forEach(n=>{if(!n.dataset.multiRecordSelectionActionConfig)return;const i=JSON.parse(n.dataset.multiRecordSelectionActionConfig);if(!i.idField)return;n.disabled=!0;const l=c.getCheckboxes(r.checked,e);for(let g=0;g{t.removeAttribute("data-manually-changed")})}registerActions(){new h("click",(e,t)=>{t.dataset.multiRecordSelectionAction;const o=c.getIdentifier(t),n=JSON.parse(t.dataset.multiRecordSelectionActionConfig||"{}"),i=c.getCheckboxes(r.checked,o);i.length&&t.dispatchEvent(new CustomEvent("multiRecordSelection:action:"+t.dataset.multiRecordSelectionAction,{detail:{identifier:o,checkboxes:i,configuration:n},bubbles:!0,cancelable:!1}))}).delegateTo(document,[a.actionsSelector,b.actionButton].join(" "))}registerActionsEventHandlers(){new h("multiRecordSelection:actions:show",e=>{const t=e.detail?.identifier||"";document.querySelectorAll(c.getCombinedSelector(a.actionsSelector,t)).forEach(n=>c.changeActionContainerVisibility(n))}).bindTo(document),new h("multiRecordSelection:actions:hide",e=>{const t=e.detail?.identifier||"";document.querySelectorAll(c.getCombinedSelector(a.actionsSelector,t)).forEach(n=>c.changeActionContainerVisibility(n,!1))}).bindTo(document),new h("multiRecordSelection:checkboxes:check",e=>{const t=e.detail?.identifier||"";c.getCheckboxes(r.any,t).forEach(o=>c.changeCheckboxState(o,!0))}).bindTo(document),new h("multiRecordSelection:checkboxes:uncheck",e=>{const t=e.detail?.identifier||"";c.getCheckboxes(r.any,t).forEach(o=>c.changeCheckboxState(o,!1))}).bindTo(document)}registerCheckboxActions(){new h("click",(e,t)=>{if(e.preventDefault(),!t.dataset.multiRecordSelectionCheckAction)return;const o=c.getIdentifier(t),n=c.getCheckboxes(r.any,o);if(n.length){switch(c.unsetManuallyChangedAttribute(o),t.dataset.multiRecordSelectionCheckAction){case d.checkAll:n.forEach(i=>{c.changeCheckboxState(i,!0)});break;case d.checkNone:n.forEach(i=>{c.changeCheckboxState(i,!1)});break;case d.toggle:n.forEach(i=>{c.changeCheckboxState(i,!i.checked)});break;default:f.warning("Unknown checkbox action")}c.unsetManuallyChangedAttribute(o)}}).delegateTo(document,[a.checkboxActionsSelector,b.checkboxActionButton].join(" "))}registerCheckboxKeyboardActions(){new h("click",(e,t)=>this.handleCheckboxKeyboardActions(e,t)).delegateTo(document,a.checkboxSelector)}registerCheckboxTableRowSelectionAction(){new h("click",(e,t)=>{const o=e.target.tagName;if(o!=="TH"&&o!=="TD")return;const n=t.querySelector(a.checkboxSelector);n!==null&&(c.changeCheckboxState(n,!n.checked),this.handleCheckboxKeyboardActions(e,n,!1))}).delegateTo(document,a.elementSelector),new h("mousedown",e=>(e.shiftKey||e.altKey||e.ctrlKey)&&e.preventDefault()).delegateTo(document,a.elementSelector)}registerDispatchCheckboxStateChangedEvent(){new h("change",(e,t)=>{t.dispatchEvent(new CustomEvent("multiRecordSelection:checkbox:state:changed",{detail:{identifier:c.getIdentifier(t)},bubbles:!0,cancelable:!1}))}).delegateTo(document,a.checkboxSelector)}registerCheckboxStateChangedEventHandler(){new h("multiRecordSelection:checkbox:state:changed",e=>{const t=e.target,o=e.detail?.identifier||"";t.checked?t.closest(a.elementSelector).classList.add(c.activeClass):t.closest(a.elementSelector).classList.remove(c.activeClass),c.toggleActionsState(o)}).bindTo(document)}registerToggleCheckboxActions(){new h("click",(e,t)=>{const o=c.getIdentifier(t),n=document.querySelector([c.getCombinedSelector(a.checkboxActionsSelector,o),'button[data-multi-record-selection-check-action="'+d.checkAll+'"]'].join(" "));n!==null&&(n.disabled=!c.getCheckboxes(r.unchecked,o).length);const i=document.querySelector([c.getCombinedSelector(a.checkboxActionsSelector,o),'button[data-multi-record-selection-check-action="'+d.checkNone+'"]'].join(" "));i!==null&&(i.disabled=!c.getCheckboxes(r.checked,o).length);const l=document.querySelector([c.getCombinedSelector(a.checkboxActionsSelector,o),'button[data-multi-record-selection-check-action="'+d.toggle+'"]'].join(" "));l!==null&&(l.disabled=!c.getCheckboxes(r.any,o).length)}).delegateTo(document,a.checkboxActionsToggleSelector)}handleCheckboxKeyboardActions(e,t,o=!0){const n=c.getIdentifier(t);if(!this.lastChecked||!document.body.contains(this.lastChecked)||c.getIdentifier(this.lastChecked)!==n||!e.shiftKey&&!e.altKey&&!e.ctrlKey){this.lastChecked=t;return}if(o&&c.unsetManuallyChangedAttribute(n),e.shiftKey){const i=Array.from(c.getCheckboxes(r.any,n)),l=i.indexOf(t),g=i.indexOf(this.lastChecked);i.slice(Math.min(l,g),Math.max(l,g)+1).forEach(k=>{k!==t&&c.changeCheckboxState(k,t.checked)})}this.lastChecked=t,(e.altKey||e.ctrlKey)&&c.getCheckboxes(r.any,n).forEach(i=>{i!==t&&c.changeCheckboxState(i,!i.checked)}),c.unsetManuallyChangedAttribute(n)}}var C=new c;export{a as MultiRecordSelectionSelectors,C as default}; diff --git a/Resources/Public/JavaScript/new-content-element-wizard-button.js b/Resources/Public/JavaScript/new-content-element-wizard-button.js new file mode 100644 index 0000000..3437603 --- /dev/null +++ b/Resources/Public/JavaScript/new-content-element-wizard-button.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as f,customElement as s}from"lit/decorators.js";import{PseudoButtonLitElement as a}from"@typo3/backend/element/pseudo-button.js";import c from"@typo3/backend/modal.js";import{SeverityEnum as d}from"@typo3/backend/enum/severity.js";import"@typo3/backend/new-record-wizard.js";var m=function(r,e,o,i){var p=arguments.length,t=p<3?e:i===null?i=Object.getOwnPropertyDescriptor(e,o):i,l;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(r,e,o,i);else for(var u=r.length-1;u>=0;u--)(l=r[u])&&(t=(p<3?l(t):p>3?l(e,o,t):l(e,o))||t);return p>3&&t&&Object.defineProperty(e,o,t),t};let n=class extends a{buttonActivated(){this.url&&c.advanced({content:this.url,title:this.subject,severity:d.notice,size:c.sizes.large,type:c.types.ajax})}};m([f({type:String})],n.prototype,"url",void 0),m([f({type:String})],n.prototype,"subject",void 0),n=m([s("typo3-backend-new-content-element-wizard-button")],n);export{n as NewContentElementWizardButton}; diff --git a/Resources/Public/JavaScript/new-multiple-pages.js b/Resources/Public/JavaScript/new-multiple-pages.js new file mode 100644 index 0000000..e669d46 --- /dev/null +++ b/Resources/Public/JavaScript/new-multiple-pages.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import c from"@typo3/core/document-service.js";import r from"@typo3/core/event/regular-event.js";var t;(function(n){n.containerSelector=".t3js-newmultiplepages-container",n.addMoreFieldsButtonSelector=".t3js-newmultiplepages-createnewfields",n.pageTitleSelector=".t3js-newmultiplepages-page-title",n.doktypeSelector=".t3js-newmultiplepages-select-doktype",n.resetFieldsSelector=".t3js-newmultiplepages-reset-fields",n.templateRow=".t3js-newmultiplepages-newlinetemplate"})(t||(t={}));class a{constructor(){this.lineCounter=5,c.ready().then(()=>{this.initializeEvents()})}initializeEvents(){new r("click",this.createNewFormFields.bind(this)).delegateTo(document,t.addMoreFieldsButtonSelector),new r("change",this.actOnPageTitleChange).delegateTo(document,t.pageTitleSelector),new r("change",this.actOnTypeSelectChange).delegateTo(document,t.doktypeSelector),new r("click",this.resetFieldAttributes).delegateTo(document,t.resetFieldsSelector)}createNewFormFields(){const e=document.querySelector(t.containerSelector),o=document.querySelector(t.templateRow)?.innerHTML||"";if(!(e===null||o==="")){for(let l=0;l<5;l++){const i=this.lineCounter+l+1;e.innerHTML+=o.replace(/\[0\]/g,(this.lineCounter+l).toString()).replace(/\[1\]/g,i.toString())}this.lineCounter+=5}}actOnPageTitleChange(){this.setAttribute("value",this.value)}actOnTypeSelectChange(){for(const l of this.options)l.removeAttribute("selected");const e=this.options[this.selectedIndex],o=document.querySelector(this.dataset.target);e!==null&&o!==null&&(e.setAttribute("selected","selected"),o.innerHTML=e.dataset.icon)}resetFieldAttributes(){document.querySelectorAll(t.containerSelector+" "+t.pageTitleSelector).forEach(e=>{e.removeAttribute("value")}),document.querySelectorAll(t.containerSelector+" "+t.doktypeSelector).forEach(e=>{for(const i of e)i.removeAttribute("selected");const o=e.options[0]?.dataset.icon,l=document.querySelector(e.dataset.target);o&&l!==null&&(l.innerHTML=o)})}}var s=new a;export{s as default}; diff --git a/Resources/Public/JavaScript/new-record-wizard.js b/Resources/Public/JavaScript/new-record-wizard.js new file mode 100644 index 0000000..1bcd392 --- /dev/null +++ b/Resources/Public/JavaScript/new-record-wizard.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as c,customElement as M}from"lit/decorators.js";import{LitElement as R,css as z,html as r,nothing as p}from"lit";import g from"@typo3/backend/modal.js";import"@typo3/backend/element/icon-element.js";import T from"@typo3/core/ajax/ajax-request.js";import U from"@typo3/backend/notification.js";import b from"@typo3/backend/viewport.js";import A from"@typo3/core/event/regular-event.js";import{KeyTypesEnum as y}from"@typo3/backend/enum/key-types.js";import{RecordUsageStore as E}from"@typo3/backend/record-usage/record-usage-store.js";import w from"@typo3/backend/storage/client.js";import C from"@typo3/backend/storage/persistent.js";import v from"~labels/core.misc";var l=function(d,e,t,o){var a=arguments.length,i=a<3?e:o===null?o=Object.getOwnPropertyDescriptor(e,t):o,n;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")i=Reflect.decorate(d,e,t,o);else for(var h=d.length-1;h>=0;h--)(n=d[h])&&(i=(a<3?n(i):a>3?n(e,t,i):n(e,t))||i);return a>3&&i&&Object.defineProperty(e,t,i),i};const $={fromAttribute:d=>d===null?!0:d.toLowerCase()==="true",toAttribute:d=>d?"true":"false"};class f{constructor(e,t,o,a,i,n,h,x,k,N){this.identifier=e,this.label=t,this.description=o,this.icon=a,this.iconOverlay=i,this.url=n,this.requestType=h,this.defaultValues=x,this.saveAndClose=k,this.event=N,this.visible=!0}static fromData(e){return new f(e.identifier,e.label,e.description,e.icon,e.iconOverlay,e.url??null,e.requestType??"location",e.defaultValues??[],e.saveAndClose??!1,e.event??null)}reset(){this.visible=!0}}class m{constructor(e,t,o,a,i){this.identifier=e,this.label=t,this.items=o,this.icon=a,this.featured=i,this.disabled=!1}static fromData(e){return new m(e.identifier,e.label,e.items.map(t=>f.fromData(t)))}reset(){this.disabled=!1,this.items.forEach(e=>{e.reset()})}activeItems(){return this.items.filter(e=>e.visible)??[]}}class u{constructor(e){this.items=e}static fromData(e){return new u(Object.values(e).map(t=>m.fromData(t)))}reset(){this.items.forEach(e=>{e.reset()})}categoriesWithItems(){return this.items.filter(e=>e.activeItems().length>0)??[]}}const S="wizard-last-category/";let s=class extends R{constructor(){super(...arguments),this.categories=new u([]),this.searchPlaceholder=v.get("newRecordWizard.filter.placeholder"),this.searchNothingFoundLabel=v.get("newRecordWizard.filter.noResults"),this.userNotAllowedLabel=v.get("newContentElement.filter.userNotAllowed"),this.displayMenu=!0,this.displayFilter=!0,this.selectedCategory=null,this.searchTerm="",this.messages=[],this.toggleMenu=!1,this.storeName=null,this.hasNavigation=!1}static{this.styles=[z`:host{display:block;container-type:inline-size;height:100%}.element{gap:var(--typo3-spacing);font-size:var(--typo3-component-font-size);line-height:var(--typo3-component-line-height);height:100%}.element,.main{display:flex;flex-direction:column}.main{width:100%;gap:calc(var(--typo3-spacing)*2)}@container (min-width: 500px){.main{flex-direction:row;overflow:hidden}}.main>*{flex-grow:1;padding-inline-end:calc(var(--typo3-spacing)/4)}.navigation{position:relative;flex-shrink:0}@container (min-width: 500px){.navigation{flex-grow:0;width:200px;overflow-block:auto}.navigation-toggle{display:none!important}}.navigation-list{display:none;flex-direction:column;gap:2px;list-style:none;padding:0;margin:0}.navigation-list.show{display:flex}@container (max-width: 499px){.navigation-list{z-index:1;position:absolute;top:calc(100% + 2px);padding:2px;background:var(--typo3-component-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:var(--typo3-component-border-radius);box-shadow:var(--typo3-component-box-shadow)}}@container (min-width: 500px){.navigation-list{display:flex}}.navigation-item{cursor:pointer;align-items:center;display:flex;width:100%;gap:calc(var(--typo3-spacing)/2);text-align:start;color:inherit;background:transparent;border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:var(--typo3-component-border-radius);padding:var(--typo3-list-item-padding-y) var(--typo3-list-item-padding-x)}.navigation-item-featured:has(+.navigation-item:not(.navigation-item-featured)){margin-bottom:calc(var(--typo3-spacing)/2)}@container (max-width: 499px){.navigation-item{--typo3-component-border-color:transparent;margin-bottom:0!important;border-radius:calc(var(--typo3-component-border-radius) - var(--typo3-component-border-width))}}.navigation-item:hover{color:var(--typo3-component-hover-color);background:var(--typo3-component-hover-bg);border-color:var(--typo3-component-hover-border-color)}.navigation-item:focus{outline:none;color:var(--typo3-component-focus-color);background:var(--typo3-component-focus-bg);border-color:var(--typo3-component-focus-border-color)}.navigation-item.active{color:var(--typo3-component-active-color);background:var(--typo3-component-active-bg);border-color:var(--typo3-component-active-border-color)}.navigation-item.active:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-component-active-border-color),transparent 25%)}.navigation-item[disabled]{cursor:not-allowed;color:var(--typo3-component-disabled-color);background:var(--typo3-component-disabled-bg);border-color:var(--typo3-component-disabled-border-color)}.navigation-item-label{flex-grow:1}.navigation-item-count{opacity:.75;flex-shrink:0}.content{container-type:inline-size;overflow-block:auto}.elementwizard-categories{display:grid;gap:var(--typo3-spacing)}.elementwizard-category-headline{font-weight:700;color:var(--typo3-text-color-variant);margin-bottom:calc(var(--typo3-spacing)/2)}.elementwizard-category-items{display:grid;grid-template-columns:repeat(1,1fr);gap:var(--typo3-spacing)}@container (min-width: 500px){.elementwizard-category-items{grid-template-columns:repeat(2,1fr)}}@container (min-width: 750px){.elementwizard-category-items{grid-template-columns:repeat(3,1fr)}}.item{cursor:pointer;display:flex;gap:calc(var(--typo3-spacing)/2);text-align:start;border:var(--typo3-component-border-width) solid transparent;border-radius:var(--typo3-component-border-radius);padding:var(--typo3-list-item-padding-y) var(--typo3-list-item-padding-x);background:transparent;color:inherit}.item:hover{color:var(--typo3-component-hover-color);background:var(--typo3-component-hover-bg);border-color:var(--typo3-component-hover-border-color)}.item:focus{outline:none;color:var(--typo3-component-focus-color);background:var(--typo3-component-focus-bg);border-color:var(--typo3-component-focus-border-color)}.item-body-label{text-wrap:balance;font-weight:700;margin-bottom:.25rem}.item-body-description{opacity:.75;text-wrap:pretty}`]}firstUpdated(){const e=document.createElement("link");e.setAttribute("rel","stylesheet"),e.setAttribute("href",TYPO3.settings.cssUrls.backend),this.shadowRoot.appendChild(e),this.displayFilter===!0&&this.renderRoot.querySelector('input[name="search"]').focus();const t=C.isset("displayRecentlyUsed")?!!JSON.parse(C.get("displayRecentlyUsed")):!0;this.storeName&&(this.recordUsageStore=new E(this.storeName),t&&this.addRecentlyUsedCategory()),this.selectAvailableCategory()}addRecentlyUsedCategory(){const e=this.recordUsageStore.getUsage();if(Object.keys(e).length===0)return;const t=this.categories.items.flatMap(o=>o.items.filter(a=>a.identifier in e)).sort((o,a)=>{const i=e[o.identifier],n=e[a.identifier];return n.count!==i.count?n.count-i.count:n.lastUsed-i.lastUsed}).slice(0,10);if(t.length>0){const o=new m("recently-used",v.get("newRecordWizard.recentlyUsed"),t,"actions-history",!0);this.categories.items.unshift(o)}}selectAvailableCategory(){let e=null;this.storeName&&(e=w.get(this.getCategoryLocalStorageKey())),this.categories.categoriesWithItems().filter(o=>o===this.selectedCategory).length===0&&(e?this.selectedCategory=this.categories.categoriesWithItems().find(o=>o.identifier===e)??this.categories.categoriesWithItems()[0]??null:this.selectedCategory=this.categories.categoriesWithItems()[0]??null),this.messages=[],this.categories.items.length===0?this.messages=[{message:this.userNotAllowedLabel,severity:"info"}]:this.selectedCategory===null&&(this.messages=[{message:this.searchNothingFoundLabel,severity:"info"}])}filter(e){this.searchTerm=e,this.categories.reset(),this.categories.items.forEach(t=>{const o=t.label.trim().replace(/\s+/g," ");!(this.searchTerm!==""&&!RegExp(this.searchTerm,"i").test(o))||t.items.forEach(i=>{const n=i.label.trim().replace(/\s+/g," ")+i.description?.trim().replace(/\s+/g," ");i.visible=!(this.searchTerm!==""&&!RegExp(this.searchTerm,"i").test(n))}),t.disabled=t.items.filter(i=>i.visible).length===0}),this.selectAvailableCategory()}willUpdate(){const e=this.selectedCategory!==null&&this.displayMenu===!0&&this.categories.items.length>1;this.hasNavigation!==e&&(this.hasNavigation=e)}render(){return r`
    ${this.displayFilter===!0?this.renderFilter():p} ${this.renderMessages()} ${this.selectedCategory===null?p:r`
    ${this.categories.items.length>1&&this.displayMenu===!0?r``:p}
    ${this.renderCategories()}
    `}
    `}renderFilter(){return r`
    e.preventDefault()}>{this.filter(e.target.value)}} @keydown=${e=>{e.key===y.ESCAPE&&(e.stopImmediatePropagation(),this.filter(""))}} placeholder=${this.searchPlaceholder}>
    `}renderMessages(){return r`${this.messages.length>0?r`
    ${this.messages.map(e=>r``)}
    `:p}`}renderNavigationToggle(){return r``}renderNavigationList(){return r``}handleNavigationClick(e){this.selectedCategory=e,this.toggleMenu=!1,this.storeName&&w.set(this.getCategoryLocalStorageKey(),e.identifier)}handleNavigationKeydown(e,t){const o=this.categories.categoriesWithItems(),a=o.findIndex(n=>n.identifier===t.identifier);let i;if(e.key===y.UP)i=o[a-1]??void 0;else if(e.key===y.DOWN)i=o[a+1]??void 0;else return;i&&(this.shadowRoot.querySelector(`button[data-identifier="${i.identifier}"]`).focus(),this.handleNavigationClick(i))}renderCategories(){return r`
    ${this.categories.items.map(e=>this.renderCategory(e))}
    `}renderCategory(e){return r`${(this.selectedCategory===e||this.displayMenu===!1)&&!e.disabled?r`
    ${this.displayMenu===!1?r`
    ${e.icon?r``:p} ${e.label}
    `:p}
    ${e.items.map(t=>this.renderCategoryItem(t))}
    `:p}`}renderCategoryItem(e){return r`${e.visible?r``:p}`}handleItemClick(e){if(this.storeName&&this.recordUsageStore.track(e.identifier),e.requestType==="event"){const t=new CustomEvent(e.event,{detail:{item:e}});this.dispatchEvent(t),g.dismiss();return}if(e.url.trim()!==""){if(e.requestType==="location"){b.ContentContainer.setUrl(e.url.replace(/_CURRENT_MODULE_/g,top.TYPO3.ModuleMenu.App.getCurrentModule())),g.dismiss();return}e.requestType==="ajax"&&new T(e.url).post({defVals:e.defaultValues,saveAndClose:e.saveAndClose?"1":"0"}).then(async t=>{const o=document.createRange().createContextualFragment(await t.resolve());g.currentModal.addEventListener("modal-updated",()=>{new A("click",(a,i)=>{a.preventDefault();const n=i.dataset.target;n&&(b.ContentContainer.setUrl(n),g.dismiss())}).delegateTo(g.currentModal,"button[data-target]")}),g.currentModal.setContent(o)}).catch(()=>{U.error("Could not load module data")})}}getCategoryLocalStorageKey(){return S+this.storeName}};l([c({type:Object,converter:{fromAttribute:d=>{const e=JSON.parse(d);return u.fromData(e)}}})],s.prototype,"categories",void 0),l([c({type:String})],s.prototype,"searchPlaceholder",void 0),l([c({type:String})],s.prototype,"searchNothingFoundLabel",void 0),l([c({type:String})],s.prototype,"userNotAllowedLabel",void 0),l([c({type:Boolean,converter:$})],s.prototype,"displayMenu",void 0),l([c({type:Boolean,converter:$})],s.prototype,"displayFilter",void 0),l([c({type:String,attribute:!1})],s.prototype,"selectedCategory",void 0),l([c({type:String,attribute:!1})],s.prototype,"searchTerm",void 0),l([c({type:Array,attribute:!1})],s.prototype,"messages",void 0),l([c({type:Boolean,attribute:!1})],s.prototype,"toggleMenu",void 0),l([c({type:String})],s.prototype,"storeName",void 0),l([c({type:Boolean,reflect:!0,attribute:"has-navigation"})],s.prototype,"hasNavigation",void 0),s=l([M("typo3-backend-new-record-wizard")],s);export{u as Categories,m as Category,s as NewRecordWizard}; diff --git a/Resources/Public/JavaScript/notification.js b/Resources/Public/JavaScript/notification.js new file mode 100644 index 0000000..d5f6165 --- /dev/null +++ b/Resources/Public/JavaScript/notification.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as b,html as u}from"lit";import{property as l,customElement as y,state as g}from"lit/decorators.js";import{classMap as v}from"lit/directives/class-map.js";import{ifDefined as w}from"lit/directives/if-defined.js";import{SeverityEnum as s}from"@typo3/backend/enum/severity.js";import C from"@typo3/backend/severity.js";import A from"~labels/core.core";import"@typo3/backend/element/icon-element.js";var c=function(h,i,t,n){var e=arguments.length,a=e<3?i:n===null?n=Object.getOwnPropertyDescriptor(i,t):n,o;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")a=Reflect.decorate(h,i,t,n);else for(var m=h.length-1;m>=0;m--)(o=h[m])&&(a=(e<3?o(a):e>3?o(i,t,a):o(i,t))||a);return e>3&&a&&Object.defineProperty(i,t,a),a};class d{static{this.duration=5}static{this.showClearAllButtonCount=2}static{this.totalNotifications=0}static{this.messageContainer=null}static{this.notificationList=null}static{this.clearAllButton=null}static notice(i,t,n,e){d.showMessage(i,t,s.notice,n,e)}static info(i,t,n,e){d.showMessage(i,t,s.info,n,e)}static success(i,t,n,e){d.showMessage(i,t,s.ok,n,e)}static warning(i,t,n,e){d.showMessage(i,t,s.warning,n,e)}static error(i,t,n=0,e){d.showMessage(i,t,s.error,n,e)}static showMessage(i,t,n=s.info,e,a=[]){typeof e>"u"&&(e=n===s.error?0:this.duration),(this.messageContainer===null||document.getElementById("alert-container")===null)&&(this.messageContainer=document.createElement("div"),this.messageContainer.setAttribute("id","alert-container"),this.notificationList=document.createElement("div"),this.notificationList.setAttribute("class","alert-list"),this.notificationList.setAttribute("tabindex","0"),this.messageContainer.appendChild(this.notificationList),this.clearAllButton=document.createElement("typo3-notification-clear-all"),this.containerItemVisibility(),this.messageContainer.prepend(this.clearAllButton),document.body.appendChild(this.messageContainer),document.addEventListener("typo3-notification-open",()=>{this.totalNotifications++,this.containerItemVisibility()}),document.addEventListener("typo3-notification-clear",()=>{this.totalNotifications>0&&this.totalNotifications--,this.containerItemVisibility()}));const o=document.createElement("typo3-notification-message");o.setAttribute("notification-id","notification-"+Math.random().toString(36).substring(2,6)),o.setAttribute("notification-title",i),t&&o.setAttribute("notification-message",t),o.setAttribute("notification-severity",n.toString()),o.setAttribute("notification-duration",e.toString()),o.actions=a,setTimeout(()=>{this.notificationList.querySelector("typo3-notification-message:last-child").scrollIntoView()},Number(e)),this.notificationList.appendChild(o)}static containerItemVisibility(){this.clearAllButton.hidden=this.totalNotifications`}};c([l({type:String,attribute:"notification-container"})],p.prototype,"notificationId",void 0),p=c([y("typo3-notification-clear-all")],p);let r=class extends b{constructor(){super(...arguments),this.notificationSeverity=s.info,this.notificationDuration=0,this.actions=[],this.executingAction=-1}async firstUpdated(){document.addEventListener("typo3-notification-clear-all",async()=>{this.clear()});const i=new CustomEvent("typo3-notification-open",{bubbles:!0,composed:!0});this.dispatchEvent(i),await new Promise(t=>window.setTimeout(t,200)),await this.requestUpdate(),this.notificationDuration>0&&(await new Promise(t=>window.setTimeout(t,this.notificationDuration*1e3)),this.clear())}async clear(){this.dispatchEvent(new CustomEvent("typo3-notification-clear",{bubbles:!0,composed:!0})),this.addEventListener("typo3-notification-clear-finish",()=>{this.parentNode?.removeChild(this)});const i=()=>{this.dispatchEvent(new CustomEvent("typo3-notification-clear-finish"))};!window.matchMedia("(prefers-reduced-motion: reduce)").matches&&"animate"in this?(this.style.overflow="hidden",this.style.display="block",this.animate([{height:this.getBoundingClientRect().height+"px"},{height:0,opacity:0,marginTop:0}],{duration:400,easing:"cubic-bezier(.02, .01, .47, 1)"}).onfinish=i):i()}createRenderRoot(){return this}render(){const i=C.getCssClass(this.notificationSeverity);let t="";switch(this.notificationSeverity){case s.notice:t="actions-lightbulb";break;case s.ok:t="actions-check";break;case s.warning:t="actions-exclamation";break;case s.error:t="actions-close";break;case s.info:default:t="actions-info"}const n=(Math.random()+1).toString(36).substring(2);return u``}};c([l({type:String,attribute:"notification-id"})],r.prototype,"notificationId",void 0),c([l({type:String,attribute:"notification-title"})],r.prototype,"notificationTitle",void 0),c([l({type:String,attribute:"notification-message"})],r.prototype,"notificationMessage",void 0),c([l({type:Number,attribute:"notification-severity"})],r.prototype,"notificationSeverity",void 0),c([l({type:Number,attribute:"notification-duration"})],r.prototype,"notificationDuration",void 0),c([l({type:Array,attribute:!1})],r.prototype,"actions",void 0),c([g()],r.prototype,"executingAction",void 0),r=c([y("typo3-notification-message")],r);let f;try{parent&&parent.window.TYPO3&&parent.window.TYPO3.Notification&&(f=parent.window.TYPO3.Notification),top&&top.TYPO3.Notification&&(f=top.TYPO3.Notification)}catch{}f||(f=d,typeof TYPO3<"u"&&(TYPO3.Notification=f));var E=f;export{p as ClearNotificationMessages,r as NotificationMessage,E as default}; diff --git a/Resources/Public/JavaScript/offset.js b/Resources/Public/JavaScript/offset.js new file mode 100644 index 0000000..90d4417 --- /dev/null +++ b/Resources/Public/JavaScript/offset.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class t{constructor(h,i,e,s){this.left=h,this.top=i,this.width=e,this.height=s}get right(){return this.left+this.width}get bottom(){return this.top+this.height}static fromObject({left:h,top:i,width:e,height:s}){return new t(h,i,e,s)}clone(){return new t(this.left,this.top,this.width,this.height)}}export{t as Offset}; diff --git a/Resources/Public/JavaScript/online-media.js b/Resources/Public/JavaScript/online-media.js new file mode 100644 index 0000000..c98e3fa --- /dev/null +++ b/Resources/Public/JavaScript/online-media.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import d from"@typo3/core/document-service.js";import{MessageUtility as m}from"@typo3/backend/utility/message-utility.js";import"@typo3/backend/element/progress-bar-element.js";import c from"@typo3/core/ajax/ajax-request.js";import p,{Types as f}from"@typo3/backend/modal.js";import u from"@typo3/backend/notification.js";import b from"@typo3/backend/severity.js";import y from"@typo3/core/event/regular-event.js";import{topLevelModuleImport as h}from"@typo3/backend/utility/top-level-module-import.js";import g from"~labels/core.core";class M{constructor(){this.progressBar=null,d.ready().then(async()=>{await h("@typo3/backend/form-engine/element/online-media-form-element.js"),this.registerEvents()})}registerEvents(){new y("click",(e,o)=>{this.triggerModal(o)}).delegateTo(document,".t3js-online-media-add-btn")}addOnlineMedia(e,o,r){const n=e.dataset.targetFolder,t=e.dataset.onlineMediaAllowed,a=e.dataset.fileIrreObject;this.progressBar=document.createElement("typo3-backend-progress-bar"),document.body.appendChild(this.progressBar),this.progressBar.start(),new c(TYPO3.settings.ajaxUrls.online_media_create).post({url:r,targetFolder:n,allowed:t}).then(async i=>{const s=await i.resolve();if(s.file){const l={actionName:"typo3:foreignRelation:insert",objectGroup:a,table:"sys_file",uid:s.file};m.send(l),o.hideModal()}else u.error(g.get("online_media.error.new_media.failed"),s.error);this.progressBar&&this.progressBar.done()})}triggerModal(e){const o=e.dataset.btnSubmit||"Add",r=e.dataset.placeholder||"Paste media url here...",n=e.dataset.onlineMediaAllowedHelpText||"Allow to embed from sources:",t=document.createElement("typo3-backend-formengine-online-media-form");t.placeholder=r,t.setAttribute("help-text",n),t.setAttribute("extensions",e.dataset.onlineMediaAllowed),p.advanced({type:f.default,title:e.title,content:t,severity:b.notice,callback:a=>{a.querySelector("typo3-backend-formengine-online-media-form").addEventListener("typo3:formengine:online-media-added",i=>{this.addOnlineMedia(e,a,i.detail["online-media-url"])})},buttons:[{text:o,btnClass:"btn btn-primary",name:"ok",trigger:()=>{t.querySelector("form").requestSubmit()}}]})}}var w=new M;export{w as default}; diff --git a/Resources/Public/JavaScript/page-link-handler.js b/Resources/Public/JavaScript/page-link-handler.js new file mode 100644 index 0000000..6454e7d --- /dev/null +++ b/Resources/Public/JavaScript/page-link-handler.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import i from"@typo3/backend/link-browser.js";import a from"@typo3/core/event/regular-event.js";class l{constructor(){this.linkPageByTextfield=()=>{let e=document.getElementById("luid").value;if(!e)return;const n=parseInt(e,10);isNaN(n)||(e="t3://page?uid="+n),i.finalizeFunction(e)},new a("click",(t,e)=>{t.preventDefault(),i.finalizeFunction(e.getAttribute("href"))}).delegateTo(document,"a.t3js-pageLink"),new a("click",t=>{t.preventDefault(),this.linkPageByTextfield()}).delegateTo(document,"input.t3js-pageLink")}}var r=new l;export{r as default}; diff --git a/Resources/Public/JavaScript/page-wizard/finisher/page-wizard-submission-service.js b/Resources/Public/JavaScript/page-wizard/finisher/page-wizard-submission-service.js new file mode 100644 index 0000000..1bfc958 --- /dev/null +++ b/Resources/Public/JavaScript/page-wizard/finisher/page-wizard-submission-service.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import o from"@typo3/core/ajax/ajax-request.js";class r{constructor(e){this.context=e}async execute(){const{fields:e,...t}=this.context.getDataStore(),s=Object.assign({},t,...Object.values(e||{})),a=await(await new o(TYPO3.settings.ajaxUrls.wizard_submit).withQueryArguments({mode:"page_wizard"}).post(s)).resolve();return document.dispatchEvent(new CustomEvent("typo3:pagetree:refresh")),a}}export{r as PageWizardSubmissionService}; diff --git a/Resources/Public/JavaScript/page-wizard/helper/wizard-helper.js b/Resources/Public/JavaScript/page-wizard/helper/wizard-helper.js new file mode 100644 index 0000000..69c820d --- /dev/null +++ b/Resources/Public/JavaScript/page-wizard/helper/wizard-helper.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{topLevelModuleImport as o}from"@typo3/backend/utility/top-level-module-import.js";import{html as a}from"lit";import i,{Size as e}from"@typo3/backend/modal.js";import{SeverityEnum as r}from"@typo3/backend/enum/severity.js";import p from"~labels/backend.layout";const d=async t=>{await o("@typo3/backend/page-wizard/page-wizard.js"),i.advanced({title:p.get("newPage"),content:a``,severity:r.notice,size:{width:e.medium,height:e.large},staticBackdrop:!0,buttons:[]})};export{d as openPageWizardModal}; diff --git a/Resources/Public/JavaScript/page-wizard/new-page-wizard-button.js b/Resources/Public/JavaScript/page-wizard/new-page-wizard-button.js new file mode 100644 index 0000000..b5acb81 --- /dev/null +++ b/Resources/Public/JavaScript/page-wizard/new-page-wizard-button.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as a,customElement as l}from"lit/decorators.js";import{PseudoButtonLitElement as m}from"@typo3/backend/element/pseudo-button.js";import"@typo3/backend/new-record-wizard.js";import{openPageWizardModal as d}from"@typo3/backend/page-wizard/helper/wizard-helper.js";var f=function(n,e,o,r){var i=arguments.length,t=i<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,o):r,p;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(n,e,o,r);else for(var c=n.length-1;c>=0;c--)(p=n[c])&&(t=(i<3?p(t):i>3?p(e,o,t):p(e,o))||t);return i>3&&t&&Object.defineProperty(e,o,t),t};let u=class extends m{constructor(){super(...arguments),this.configuration=null}buttonActivated(){d(this.configuration)}};f([a({type:Object})],u.prototype,"configuration",void 0),u=f([l("typo3-backend-new-page-wizard-button")],u);export{u as NewPageWizardButton}; diff --git a/Resources/Public/JavaScript/page-wizard/page-wizard.js b/Resources/Public/JavaScript/page-wizard/page-wizard.js new file mode 100644 index 0000000..fff27bc --- /dev/null +++ b/Resources/Public/JavaScript/page-wizard/page-wizard.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as h,state as c,query as f,customElement as m}from"lit/decorators.js";import{LitElement as u,html as l}from"lit";import w from"~labels/backend.layout";import z from"~labels/backend.wizards.general";import S from"@typo3/backend/page-wizard/steps/doktype-step.js";import{AutoAdvanceEvent as b}from"@typo3/backend/wizard/events/auto-advance-event.js";import y from"@typo3/backend/page-wizard/steps/position-step.js";import{loadDynamicSteps as g}from"@typo3/backend/wizard/helper/dynamic-steps-loader.js";import{PageWizardSubmissionService as v}from"@typo3/backend/page-wizard/finisher/page-wizard-submission-service.js";var o=function(s,t,e,a){var n=arguments.length,i=n<3?t:a===null?a=Object.getOwnPropertyDescriptor(t,e):a,p;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")i=Reflect.decorate(s,t,e,a);else for(var d=s.length-1;d>=0;d--)(p=s[d])&&(i=(n<3?p(i):n>3?p(t,e,i):p(t,e))||i);return n>3&&i&&Object.defineProperty(t,e,i),i};let r=class extends u{constructor(){super(...arguments),this.configuration=null,this.steps=[],this.fixedSteps=[]}firstUpdated(t){super.firstUpdated(t),this.context={wizard:this.wizard,configuration:this.configuration,getStoreData:this.wizard.getStoreData.bind(this.wizard),setStoreData:this.wizard.setStoreData.bind(this.wizard),clearStoreData:this.wizard.clearStoreData.bind(this.wizard),getDataStore:this.wizard.getDataStore.bind(this.wizard),dispatchAutoAdvance:()=>this.wizard.dispatchEvent(new b)},this.steps=this.fixedSteps=[new y(this.context),new S(this.context)],this.submissionService=new v(this.context)}createRenderRoot(){return this}render(){return l``}loadDynamicStepsAfterDoktype(t){t.detail.currentStepKey==="doktype"&&(t.detail.result=g("page_wizard",this.context).then(e=>{this.steps=[...this.fixedSteps,...e]}).catch(e=>{this.wizard.renderError(z.get("wizard.status.error.message"),e)}))}};o([h({type:Object})],r.prototype,"configuration",void 0),o([c()],r.prototype,"steps",void 0),o([c()],r.prototype,"submissionService",void 0),o([f("typo3-backend-wizard")],r.prototype,"wizard",void 0),r=o([m("typo3-backend-page-wizard")],r);export{r as PageWizard}; diff --git a/Resources/Public/JavaScript/page-wizard/steps/doktype-step.js b/Resources/Public/JavaScript/page-wizard/steps/doktype-step.js new file mode 100644 index 0000000..ffd7d1f --- /dev/null +++ b/Resources/Public/JavaScript/page-wizard/steps/doktype-step.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{nothing as d,html as i}from"lit";import{live as k}from"lit/directives/live.js";import{TaskStatus as v,Task as g}from"@lit/task";import o from"~labels/backend.wizards.page";import y from"~labels/core.db.pages";import p from"~labels/core.core";import b from"@typo3/core/ajax/ajax-request.js";import{SeverityEnum as $}from"@typo3/backend/enum/severity.js";class m{constructor(t){this.context=t,this.key="doktype",this.title=y.get("doktype"),this.autoAdvance=!0,this.selectedDoktype=null,this.searchTerm="",this.hasDispatchedAutoAdvance=!1,this.initDoktypesTask()}isComplete(){return this.getValue()!==null}render(){return this.task.status===v.INITIAL&&this.task.run(),this.task.render({complete:t=>{let a=!1,n=d;if(this.getValue()===null){const e=this.context.getStoreData(this.key),s=this.context?.configuration?.doktype??null,c=t.filter(r=>r.value!=="--div--");if(e!=null)this.setValue(e);else if(s!==null)c.some(r=>r.value===s)?(this.setValue(s),a=!0):n=i``;else if(c.length>0){const[r]=c;this.setValue(r.value),a=c.length===1}}if(a&&!this.hasDispatchedAutoAdvance)return this.hasDispatchedAutoAdvance=!0,this.context.dispatchAutoAdvance(),this.context.wizard.renderLoader();const f=t.filter(e=>e.value==="--div--"||e.label.toLowerCase().includes(this.searchTerm.toLowerCase())),h=[];let l={label:"",items:[]};f.forEach(e=>{e.value==="--div--"?(l.items.length>0&&h.push(l),l={label:e.label,items:[]}):l.items.push(e)}),l.items.length>0&&h.push(l);const u=h.filter(e=>e.items.length>0);return i`${n}

    ${o.get("step.doktype.headline")}

    ${u.length==0?i``:d}
    ${u.map(e=>i`${e.label?i`

    ${e.label}

    `:d} ${e.items.map(s=>i`
    this.setValue(s.value)}>
    `)}`)}
    `},error:t=>this.context.wizard.renderError(o.get("step.doktype.load_error"),t),pending:()=>this.context.wizard.renderLoader()})}reset(){this.setValue(null),this.context.clearStoreData(this.key),this.initDoktypesTask()}getValue(){return this.selectedDoktype}setValue(t){this.selectedDoktype=t,this.context.wizard.requestUpdate()}beforeAdvance(){this.context.setStoreData(this.key,this.getValue())}getSummaryData(){const t=this.context.getStoreData(this.key);if(!t||!this.task.value)return[];const a=this.task.value.find(n=>n.value===t);return a?[{label:this.title,value:i`${a.label}`}]:[]}handleSearch(t){this.searchTerm=t.target.value,this.context.wizard.requestUpdate()}initDoktypesTask(){this.task=new g(this.context.wizard,{task:async()=>await(await new b(TYPO3.settings.ajaxUrls.wizard_page_get_doktypes).withQueryArguments({data:this.context.getDataStore()}).get()).resolve(),autoRun:!1})}}export{m as DoktypeStep,m as default}; diff --git a/Resources/Public/JavaScript/page-wizard/steps/form-engine-step.js b/Resources/Public/JavaScript/page-wizard/steps/form-engine-step.js new file mode 100644 index 0000000..acdc400 --- /dev/null +++ b/Resources/Public/JavaScript/page-wizard/steps/form-engine-step.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import"@typo3/backend/tree/page-position-select.js";import{html as m}from"lit";import{unsafeHTML as c}from"lit/directives/unsafe-html.js";import{executeJavaScriptModuleInstruction as u}from"@typo3/core/java-script-item-processor.js";import d from"@typo3/core/ajax/ajax-request.js";class l{constructor(t,e){this.context=t,this.autoAdvance=!1,this.key="",this.title="",this.html="",this.modules=[],this.labels={},this.key=e.key,this.title=e.title,this.html=e.html,this.modules=e.modules,this.labels=e.labels}getValue(){const t=this.context.wizard.querySelector('form[name="editform"]');if(!t)return null;const e=this.context.getStoreData("fields")||{},s=new FormData(t);return e[this.key]=Object.fromEntries(Array.from(s.entries()).filter(([,r])=>r!=="")),e}setValue(t){const e=this.context.wizard.querySelector('form[name="editform"]'),s=t?.[this.key];if(!(!e||!s))for(const[r,a]of Object.entries(s)){const i=e.elements.namedItem(r);i&&(delete i.dataset.formengineInputInitialized,i.type==="checkbox"?i.checked=!!a:i.value=a)}}render(){return m`${c(this.html)}`}getSummaryData(){return this.summary}async afterRender(){if(this.setValue(this.context.getStoreData("fields")),this.modules.length>0&&await this.loadModules(),TYPO3.FormEngine){TYPO3.FormEngine.reinitialize();const t=this.context.wizard.querySelector('form[name="editform"]');if(t){t.addEventListener("t3-formengine-postfieldvalidation",()=>{this.context.wizard.requestUpdate()}),t.addEventListener("submit",s=>{s.preventDefault(),this.context.wizard.goToNextStep()});const e=t.querySelector(".has-error");e&&e.focus()}}}isComplete(){return TYPO3.FormEngine&&TYPO3.FormEngine.Validation?TYPO3.FormEngine.Validation.isValid():!0}async beforeAdvance(){const t=this.context.getStoreData("position")?.pageUid||0,e=this.getValue(),s=e[this.key]||{},r={};for(const[o,n]of Object.entries(s))r[this.getLabelKey(o)]=n;const i=await(await new d(TYPO3.settings.ajaxUrls.wizard_page_get_processed_value).withQueryArguments({fields:r,pageUid:t}).get()).resolve();this.summary=Object.keys(r).map(o=>{const n=r[o];return{label:this.labels[o]||o,value:i[o]??n}}),this.context.setStoreData("fields",e)}async loadModules(){const t=this.modules.map(e=>u(e));await Promise.all(t)}getLabelKey(t){const e=t.match(/\[([^\]]+)\]$/);return e?e[1]:t}}export{l as FormEngineStep,l as default}; diff --git a/Resources/Public/JavaScript/page-wizard/steps/position-step.js b/Resources/Public/JavaScript/page-wizard/steps/position-step.js new file mode 100644 index 0000000..c7df552 --- /dev/null +++ b/Resources/Public/JavaScript/page-wizard/steps/position-step.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as o}from"lit";import{TaskStatus as c,Task as l}from"@lit/task";import{insertPositionOptions as u}from"@typo3/backend/tree/page-position-select.js";import n from"~labels/core.misc";import h from"@typo3/core/ajax/ajax-request.js";class r{constructor(t){this.context=t,this.key="position",this.title=n.get("selectPosition"),this.autoAdvance=!0,this.positionData=null,this.hasDispatchedAutoAdvance=!1,this.initSummaryTask()}isComplete(){return Number.isFinite(this.positionData?.pageUid)}render(){if(this.getValue()===null){const e=this.context.getStoreData(this.key),s=this.context?.configuration?.positionData??null;if(e!=null)this.setValue(e);else if(s!==null&&(this.setValue(s),(this.context?.configuration?.preventPositionAutoAdvance??!1)==!1&&!this.hasDispatchedAutoAdvance))return this.hasDispatchedAutoAdvance=!0,this.context.dispatchAutoAdvance(),this.context.wizard.renderLoader()}const t=this.getValue();return o`this.handleInsertPositionChange(e)} @typo3:page-position-select-tree:insert-position-confirm=${()=>this.handleInsertPositionConfirm()}>`}reset(){this.setValue(null),this.context.clearStoreData(this.key)}getValue(){return this.positionData}setValue(t){this.positionData=t,this.context.wizard.requestUpdate()}beforeAdvance(){this.context.setStoreData(this.key,this.getValue()),this.initSummaryTask()}getSummaryData(){const t=this.context.getStoreData(this.key),e=t.pageUid,s=u.find(i=>i.value===t.insertPosition);this.summaryTask.status===c.INITIAL&&this.summaryTask.run([e]);const a=this.summaryTask.render({complete:i=>o`${i.title} [pages:${i.uid}]`,error:()=>this.context.wizard.renderError(n.get("pageSelectPositionError")),pending:()=>this.context.wizard.renderLoader()});return[{label:n.get("pageSelectPosition"),value:o`${a} (${s?.label})`}]}handleInsertPositionChange(t){this.setValue({pageUid:t.detail.pageUid,insertPosition:t.detail.position})}handleInsertPositionConfirm(){this.context.dispatchAutoAdvance()}initSummaryTask(){this.summaryTask=new l(this.context.wizard,{task:async([t])=>await(await new h(TYPO3.settings.ajaxUrls.wizard_page_get_page_detail).withQueryArguments({pageUid:t}).get()).resolve(),autoRun:!1})}}export{r as PositionStep,r as default}; diff --git a/Resources/Public/JavaScript/pagetsconfig/pagetsconfig-includes.js b/Resources/Public/JavaScript/pagetsconfig/pagetsconfig-includes.js new file mode 100644 index 0000000..7772e73 --- /dev/null +++ b/Resources/Public/JavaScript/pagetsconfig/pagetsconfig-includes.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import d from"@typo3/core/document-service.js";import o from"@typo3/backend/modal.js";import{topLevelModuleImport as l}from"@typo3/backend/utility/top-level-module-import.js";import{html as r}from"lit";import{until as m}from"lit/directives/until.js";import p from"@typo3/core/ajax/ajax-request.js";class f{constructor(){this.registerEventListeners()}async registerEventListeners(){await d.ready(),document.querySelectorAll(".t3js-pagetsconfig-includes-modal").forEach(e=>{e.addEventListener("click",s=>{s.preventDefault();const t=o.types.default,a=e.dataset.modalTitle||e.textContent.trim(),n=e.getAttribute("href"),c=o.sizes.large,i=r`${m(this.fetchModalContent(n),r``)}`;o.advanced({type:t,title:a,size:c,content:i})})})}async fetchModalContent(e){l("@typo3/backend/code-editor/element/code-mirror-element.js");const t=await(await new p(e).get()).resolve();return r``}}var g=new f;export{g as default}; diff --git a/Resources/Public/JavaScript/popover.js b/Resources/Public/JavaScript/popover.js new file mode 100644 index 0000000..03977b0 --- /dev/null +++ b/Resources/Public/JavaScript/popover.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{Popover as o}from"bootstrap";class l{constructor(){this.DEFAULT_SELECTOR='[data-bs-toggle="popover"]',this.initialize()}initialize(t){t=t||this.DEFAULT_SELECTOR,document.querySelectorAll(t).forEach(e=>{this.applyTitleIfAvailable(e),new o(e)})}popover(t){this.toIterable(t).forEach(e=>{this.applyTitleIfAvailable(e),new o(e)})}setOptions(t,e){e=e||{};const n=e.title||t.dataset.title||t.dataset.bsTitle||"",s=e.content||t.dataset.bsContent||"";t.dataset.bsTitle=n,t.dataset.bsOriginalTitle=n,t.dataset.bsContent=s,t.dataset.bsPlacement="auto",delete e.title,delete e.content;const a=o.getInstance(t);if(a===null){console.warn("Failed to get popover instance for element.");return}a.setContent({".popover-header":n,".popover-body":s});for(const[i,r]of Object.entries(e))a._config[i]=r}show(t){const e=o.getInstance(t);if(e===null){console.warn("Failed to get popover instance for element.");return}e.show()}hide(t){const e=o.getInstance(t);if(e===null){console.warn("Failed to get popover instance for element.");return}e.hide()}destroy(t){const e=o.getInstance(t);if(e===null){console.warn("Failed to get popover instance for element.");return}e.dispose()}toggle(t){const e=o.getInstance(t);if(e===null){console.warn("Failed to get popover instance for element.");return}e.toggle()}toIterable(t){let e;if(t instanceof HTMLElement)e=[t];else if(t instanceof NodeList)e=t;else throw`Cannot consume element of type ${t.constructor.name}, expected NodeListOf or HTMLElement`;return e}applyTitleIfAvailable(t){const e=t.title||t.dataset.title||"";e&&(t.dataset.bsTitle=e)}}var c=new l;export{c as default}; diff --git a/Resources/Public/JavaScript/record-download-button.js b/Resources/Public/JavaScript/record-download-button.js new file mode 100644 index 0000000..24f1309 --- /dev/null +++ b/Resources/Public/JavaScript/record-download-button.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as c,customElement as p}from"lit/decorators.js";import{PseudoButtonLitElement as y}from"@typo3/backend/element/pseudo-button.js";import{SeverityEnum as b}from"@typo3/backend/enum/severity.js";import m from"@typo3/backend/modal.js";import u from"~labels/core.mod_web_list";var s=function(n,t,e,l){var i=arguments.length,o=i<3?t:l===null?l=Object.getOwnPropertyDescriptor(t,e):l,r;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")o=Reflect.decorate(n,t,e,l);else for(var f=n.length-1;f>=0;f--)(r=n[f])&&(o=(i<3?r(o):i>3?r(t,e,o):r(t,e))||o);return i>3&&o&&Object.defineProperty(t,e,o),o},d;(function(n){n.formatSelector=".t3js-record-download-format-selector",n.formatOptions=".t3js-record-download-format-option"})(d||(d={}));let a=class extends y{buttonActivated(){this.showDownloadConfigurationModal()}showDownloadConfigurationModal(){if(!this.url)return;const t=m.advanced({content:this.url,title:this.subject||"Download records",severity:b.notice,size:m.sizes.small,type:m.types.ajax,buttons:[{text:this.close||u.get("button.close"),active:!0,btnClass:"btn-default",name:"cancel",trigger:()=>t.hideModal()},{text:this.ok||u.get("button.ok"),btnClass:"btn-primary",name:"download",trigger:()=>{t.querySelector("form")?.submit(),t.hideModal()}}],ajaxCallback:()=>{const e=t.querySelector(d.formatSelector),l=t.querySelectorAll(d.formatOptions);e===null||!l.length||e.addEventListener("change",i=>{const o=i.target.value;l.forEach(r=>{r.dataset.formatname!==o?r.classList.add("hide"):r.classList.remove("hide")})})}})}};s([c({type:String})],a.prototype,"url",void 0),s([c({type:String})],a.prototype,"subject",void 0),s([c({type:String})],a.prototype,"ok",void 0),s([c({type:String})],a.prototype,"close",void 0),a=s([p("typo3-recordlist-record-download-button")],a);export{a as RecordDownloadButton}; diff --git a/Resources/Public/JavaScript/record-link-handler.js b/Resources/Public/JavaScript/record-link-handler.js new file mode 100644 index 0000000..e97a359 --- /dev/null +++ b/Resources/Public/JavaScript/record-link-handler.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import a from"@typo3/backend/link-browser.js";import n from"@typo3/core/event/regular-event.js";class o{constructor(){new n("click",(e,t)=>{e.preventDefault();const r=t.closest("span").dataset;a.finalizeFunction(document.body.dataset.linkbrowserIdentifier+r.uid)}).delegateTo(document,"[data-close]")}}var d=new o;export{d as default}; diff --git a/Resources/Public/JavaScript/record-search.js b/Resources/Public/JavaScript/record-search.js new file mode 100644 index 0000000..6bd4aa5 --- /dev/null +++ b/Resources/Public/JavaScript/record-search.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import c from"@typo3/core/document-service.js";import s from"@typo3/core/event/regular-event.js";var e;(function(r){r.searchFieldSelector="#recordsearchbox-searchterm"})(e||(e={}));class t{constructor(){this.searchField=document.querySelector(e.searchFieldSelector),this.activeSearch=this.searchField?this.searchField.value!=="":!1,c.ready().then(()=>{this.searchField&&new s("search",()=>{this.searchField.value===""&&this.activeSearch&&this.searchField.closest("form").submit()}).bindTo(this.searchField)})}}var a=new t;export{a as default}; diff --git a/Resources/Public/JavaScript/record-usage/record-usage-store.js b/Resources/Public/JavaScript/record-usage/record-usage-store.js new file mode 100644 index 0000000..5f34e1d --- /dev/null +++ b/Resources/Public/JavaScript/record-usage/record-usage-store.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import s from"@typo3/backend/storage/client.js";const r="record-usage/";class a{constructor(e){this.storeName=e}track(e){const t=this.load();t.usage[e]={lastUsed:Date.now(),count:(t.usage[e]?.count??0)+1},this.save(t)}getUsage(){return this.load().usage}load(){const e=s.get(this.localStorageKey());return e===null?{usage:{}}:this.removeOldItems(JSON.parse(e))}save(e){s.set(this.localStorageKey(),JSON.stringify(e))}removeOldItems(e){const t=Date.now()-2592e6;return e.usage=Object.fromEntries(Object.entries(e.usage).filter(([,o])=>o.lastUsed>=t)),e}localStorageKey(){return r+this.storeName}}export{a as RecordUsageStore}; diff --git a/Resources/Public/JavaScript/recordlist.js b/Resources/Public/JavaScript/recordlist.js new file mode 100644 index 0000000..ce69b14 --- /dev/null +++ b/Resources/Public/JavaScript/recordlist.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import m from"@typo3/backend/icons.js";import b from"@typo3/backend/storage/persistent.js";import u from"@typo3/core/event/regular-event.js";import w from"@typo3/core/document-service.js";import{MultiRecordSelectionSelectors as S}from"@typo3/backend/multi-record-selection.js";import{selector as g}from"@typo3/core/literals.js";import E from"@typo3/backend/notification.js";import x from"@typo3/core/ajax/ajax-request.js";import"@typo3/core/ajax/ajax-response.js";import{sudoModeInterceptor as C}from"@typo3/backend/security/sudo-mode-interceptor.js";class p{constructor(){this.identifier={entity:".t3js-entity",toggle:".t3js-toggle-recordlist",localize:".t3js-action-localize",hide:'button[data-datahandler-action="visibility"]',delete:".t3js-record-delete",editMultiple:".t3js-record-edit-multiple",icons:{collapse:"actions-view-list-collapse",expand:"actions-view-list-expand"}},this.toggleClick=(i,e)=>{i.preventDefault();const t=e.dataset.table,n=document.querySelector(e.dataset.bsTarget),o=n.dataset.state==="expanded",c=e.querySelector(".t3js-icon"),d=o?this.identifier.icons.expand:this.identifier.icons.collapse;m.getIcon(d,m.sizes.small).then(l=>{c.replaceWith(document.createRange().createContextualFragment(l))});let a={};b.isset("moduleData.records.collapsedTables")&&(a=b.get("moduleData.records.collapsedTables"));const s={};s[t]=o?1:0,a=Object.assign(a,s),b.set("moduleData.records.collapsedTables",a).then(()=>{n.dataset.state=o?"collapsed":"expanded"})},this.onEditMultiple=(i,e)=>{i.preventDefault();let t="",n="",o=[];const c=[];if(i.type==="multiRecordSelection:action:edit"){const a=i.detail,s=a.configuration;if(n=s.returnUrl||"",o=s.columnsOnly||[],t=s.tableName||"",t==="")return;a.checkboxes.forEach(l=>{const r=l.closest(S.elementSelector);r!==null&&r.dataset[s.idField]&&c.push(r.dataset[s.idField])})}else{const a=e.closest("[data-table]");if(a===null||(t=a.dataset.table||"",t===""))return;n=e.dataset.returnUrl||"",o=JSON.parse(e.dataset.columnsOnly||"{}");const s=a.querySelectorAll(this.identifier.entity+'[data-uid][data-table="'+t+'"] td.col-checkbox input[type="checkbox"]:checked');if(s.length)s.forEach(l=>{c.push(l.closest(this.identifier.entity+g`[data-uid][data-table="${t}"]`).dataset.uid)});else{const l=a.querySelectorAll(this.identifier.entity+g`[data-uid][data-table="${t}"]`);if(!l.length)return;l.forEach(r=>{c.push(r.dataset.uid)})}}if(!c.length)return;let d=top.TYPO3.settings.FormEngine.moduleUrl+"&edit["+t+"]["+c.join(",")+"]=edit&module="+encodeURIComponent(top.TYPO3.ModuleMenu.App.getCurrentModule())+"&returnUrl="+p.getReturnUrl(n);o.length>0&&(d+=o.map((a,s)=>"&columnsOnly["+t+"]["+s+"]="+a).join("")),window.location.href=d},this.disableButton=(i,e)=>{e.setAttribute("disabled","disabled"),e.classList.add("disabled")},this.toggleVisibility=(i,e)=>{e.disabled=!0;const t=e.querySelector(".t3js-icon"),n=t.cloneNode(!0);m.getIcon("spinner-circle",m.sizes.small).then(l=>{t.replaceWith(document.createRange().createContextualFragment(l))});const o=e.closest("tr[data-uid]"),c=o.dataset.table,d=parseInt(o.dataset.uid,10),s=e.dataset.datahandlerStatus==="visible"?"hide":"show";new x(TYPO3.settings.ajaxUrls.record_toggle_visibility).addMiddleware(C).post({table:c,uid:d,action:s}).then(async l=>{const r=await l.resolve();e.setAttribute("data-datahandler-status",r.isVisible?"visible":"hidden");const h=r.isVisible?e.dataset.datahandlerVisibleLabel:e.dataset.datahandlerHiddenLabel;e.setAttribute("title",h);const y=r.isVisible?"actions-edit-hide":"actions-edit-unhide";m.getIcon(y,m.sizes.small).then(v=>{e.querySelector(".t3js-icon").replaceWith(document.createRange().createContextualFragment(v))}),o.querySelector(".col-icon .t3js-icon").replaceWith(document.createRange().createContextualFragment(r.icon));const f=new u("animationend",()=>{o.classList.remove("record-pulse"),f.release()});f.bindTo(o),o.classList.add("record-pulse"),c==="pages"&&top.document.dispatchEvent(new CustomEvent("typo3:pagetree:refresh"))}).catch(async l=>{e.querySelector(".t3js-icon").replaceWith(n);const r=await l.resolve();for(const h of r.messages)E.error(h.title,h.message)}).finally(()=>{e.disabled=!1})},this.registerPaginationEvents=()=>{document.querySelectorAll(".t3js-recordlist-paging").forEach(i=>{i.addEventListener("keyup",e=>{e.preventDefault();let t=Number(i.value);const n=Number(i.min),o=Number(i.max);if(n&&to&&(t=o),i.value=t.toString(10),e.key==="Enter"&&t!==Number(i.dataset.currentpage)){const c=i.closest('form[name^="list-table-form-"]'),d=new URL(c.action,window.origin);d.searchParams.set("pointer",t.toString()),window.location.href=d.toString()}})})},new u("click",this.toggleClick).delegateTo(document,this.identifier.toggle),new u("click",this.onEditMultiple).delegateTo(document,this.identifier.editMultiple),new u("click",this.disableButton).delegateTo(document,this.identifier.localize),new u("click",this.toggleVisibility).delegateTo(document,this.identifier.hide),w.ready().then(()=>{this.registerPaginationEvents()}),new u("multiRecordSelection:action:edit",this.onEditMultiple).bindTo(document),new u("multiRecordSelection:action:copyMarked",i=>{p.submitClipboardFormWithCommand("copyMarked",i.target)}).bindTo(document),new u("multiRecordSelection:action:removeMarked",i=>{p.submitClipboardFormWithCommand("removeMarked",i.target)}).bindTo(document)}static submitClipboardFormWithCommand(i,e){const t=e.closest("form");if(!t)return;const n=t.querySelector('input[name="cmd"]');n&&(n.value=i,t.submit())}static getReturnUrl(i){return i===""&&(i=top.list_frame.document.location.pathname+top.list_frame.document.location.search),encodeURIComponent(i)}}var I=new p;export{I as default}; diff --git a/Resources/Public/JavaScript/resource/resource.js b/Resources/Public/JavaScript/resource/resource.js new file mode 100644 index 0000000..278d164 --- /dev/null +++ b/Resources/Public/JavaScript/resource/resource.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class a{constructor(t,i,s,e=!1,l=null,h=null,u=null,n=null,r=null){this.type=t,this.identifier=i,this.name=s,this.hasPreview=e,this.uid=l,this.metaUid=h,this.url=u,this.createdAt=n,this.size=r}}export{a as Resource}; diff --git a/Resources/Public/JavaScript/security/element/csp-reports.js b/Resources/Public/JavaScript/security/element/csp-reports.js new file mode 100644 index 0000000..c3b185d --- /dev/null +++ b/Resources/Public/JavaScript/security/element/csp-reports.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as b,state as h,customElement as $}from"lit/decorators.js";import{LitElement as v,html as s,nothing as d}from"lit";import{classMap as R}from"lit/directives/class-map.js";import p from"@typo3/core/ajax/ajax-request.js";import y from"@typo3/core/event/regular-event.js";import i from"~labels/backend.modules.content_security_policy";import"bootstrap";var a=function(r,e,t,l){var o=arguments.length,c=o<3?e:l===null?l=Object.getOwnPropertyDescriptor(e,t):l,u;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")c=Reflect.decorate(r,e,t,l);else for(var m=r.length-1;m>=0;m--)(u=r[m])&&(c=(o<3?u(c):o>3?u(e,t,c):u(e,t))||c);return o>3&&c&&Object.defineProperty(e,t,c),c},f;(function(r){r.fixable="fixable",r.irrelevant="irrelevant",r.suspicious="suspicious"})(f||(f={}));let n=class extends v{constructor(){super(...arguments),this.selectedScope=null,this.reports=[],this.selectedReport=null,this.suggestions=[],this.previouslyFocusedRow=null}connectedCallback(){super.connectedCallback(),this.fetchReports(),this.peripheralEvent=new y("click",(e,t)=>{t.dataset.cspReportsHandler==="refresh"&&(e.preventDefault(),this.fetchReports())}),this.peripheralEvent.delegateTo(document,"[data-csp-reports-handler]")}disconnectedCallback(){super.disconnectedCallback(),this.peripheralEvent?.release()}updated(e){super.updated(e),e.has("selectedReport")&&this.selectedReport!==null&&this.updateComplete.then(()=>{this.querySelector("#report-details")?.focus()})}createRenderRoot(){return this}render(){return s`
    ${this.renderNavigation()}
    ${this.reports.length===0?s``:d} ${this.reports.map(e=>s`this.selectReport(e)} @keydown=${t=>this.handleReportKeydown(t,e)} tabindex=0 role=button aria-label=${i.get("module.label.showDetails",[e.details.effectiveDirective,e.details.blockedUri])}>`)}
    ${i.get("module.label.created")}${i.get("module.label.scope")}${i.get("module.label.violation")}${i.get("module.label.uri")}
    ${i.get("module.label.noEntriesAvailable")}
    ${e.created}${e.scope}${e.count} ${e.details.effectiveDirective}${this.shortenUri(e.details.blockedUri)}${e.attributes.join(", ")}
    ${this.renderGuide()} ${this.renderSelectedReport()}
    `}renderNavigation(){return s`
    `}renderGuide(){return s`${this.selectedReport?d:s`

    ${i.get("module.label.guide.no_record_selected")}

    `}`}renderSelectedReport(){const e=this.selectedReport;return s`${e?s`
    this.handleDetailsKeydown(t)}>

    ${i.get("module.label.details")}

    ${i.get("module.label.directive")} / ${i.get("module.label.disposition")}
    ${e.details.effectiveDirective} / ${e.details.disposition}
    ${i.get("module.label.document_uri")}
    ${e.details.documentUri} ${this.renderCodeLocation(e)}
    ${e.details.sourceFile&&e.details.sourceFile!==e.details.documentUri?s`
    ${i.get("module.label.source_file")}
    ${e.details.sourceFile}
    `:d}
    ${i.get("module.label.blocked_uri")}
    ${e.details.blockedUri}
    ${e.details.scriptSample?s`
    ${i.get("module.label.sample")}
    ${e.details.scriptSample}
    `:d} ${e.meta.agent?s`
    ${i.get("module.label.user_agent")}
    ${e.meta.agent}
    `:d}
    ${i.get("module.label.uuid")}
    ${e.uuid}
    ${i.get("module.label.summary")}
    ${e.summary}
    ${this.suggestions.length>0?s`

    ${i.get("module.label.suggestions")}

    `:d} ${this.suggestions.map(t=>s`

    ${t.label||t.identifier}

    ${t.collection.mutations.map(l=>s`

    ${l.mode} ${l.directive}: ${l.sources.join(" ")}

    `)}
    `)}
    `:d}`}renderCodeLocation(e){if(!e.details.lineNumber)return d;const t=[e.details.lineNumber];return e.details.columnNumber&&t.push(e.details.columnNumber),s`(${t.join(":")})`}selectReport(e){this.suggestions=[],e!==null&&this.selectedReport!==e?(this.previouslyFocusedRow=document.activeElement,this.selectedReport=e,this.invokeHandleReportAction(e).then(t=>this.suggestions=t)):(this.selectedReport=null,this.previouslyFocusedRow&&this.updateComplete.then(()=>{this.previouslyFocusedRow?.focus(),this.previouslyFocusedRow=null}))}handleReportKeydown(e,t){if(e.key==="Enter"||e.key==="Space"||e.key===" "){e.preventDefault(),this.selectReport(t);return}const l=e.currentTarget;let o=null;switch(e.key){case"ArrowDown":e.preventDefault(),o=l.nextElementSibling;break;case"ArrowUp":e.preventDefault(),o=l.previousElementSibling;break;case"Home":e.preventDefault(),o=l.parentElement?.firstElementChild;break;case"End":e.preventDefault(),o=l.parentElement?.lastElementChild;break;default:return}o&&o.hasAttribute("tabindex")&&o.focus()}handleDetailsKeydown(e){e.key==="Escape"&&(e.preventDefault(),this.selectReport(null))}focusFirstReportRow(){this.updateComplete.then(()=>{this.querySelector('tbody tr[tabindex="0"]')?.focus()})}selectScope(e){this.selectedScope=e,this.fetchReports()}fetchReports(){this.invokeFetchReportsAction().then(e=>this.reports=e)}filterReports(...e){e.includes(this.selectedReport?.uuid)&&(this.selectedReport=null),this.reports=this.reports.filter(t=>!e.includes(t.uuid))}invokeFetchReportsAction(){return new p(this.controlUri).post({action:"fetchReports",scope:this.selectedScope||""}).then(e=>e.resolve("application/json"))}invokeHandleReportAction(e){return new p(this.controlUri).post({action:"handleReport",uuid:e.uuid}).then(t=>t.resolve("application/json"))}invokeMutateReportAction(e,t){const l=this.reports.filter(o=>o.mutationHashes.includes(t.hash)).map(o=>o.summary);return new p(this.controlUri).post({action:"mutateReport",scope:e.scope,hmac:t.hmac,suggestion:t,summaries:l}).then(o=>o.resolve("application/json")).then(o=>this.filterReports(...o.uuids))}invokeMuteReportAction(e){new p(this.controlUri).post({action:"muteReport",summaries:[e.summary]}).then(t=>t.resolve("application/json")).then(t=>this.filterReports(...t.uuids)).then(()=>this.focusFirstReportRow())}invokeDeleteReportAction(e){new p(this.controlUri).post({action:"deleteReport",summaries:[e.summary]}).then(t=>t.resolve("application/json")).then(t=>this.filterReports(...t.uuids)).then(()=>this.focusFirstReportRow())}invokeDeleteReportsAction(){new p(this.controlUri).post({action:"deleteReports",scope:this.selectedScope||""}).then(e=>e.resolve("application/json")).then(()=>this.fetchReports()).then(()=>this.selectReport(null))}shortenUri(e){if(e==="inline")return e;try{return new URL(e).hostname}catch{return e}}};a([b({type:Array})],n.prototype,"scopes",void 0),a([b({type:String})],n.prototype,"controlUri",void 0),a([h()],n.prototype,"selectedScope",void 0),a([h()],n.prototype,"reports",void 0),a([h()],n.prototype,"selectedReport",void 0),a([h()],n.prototype,"suggestions",void 0),n=a([$("typo3-backend-security-csp-reports")],n);export{n as CspReports}; diff --git a/Resources/Public/JavaScript/security/element/sudo-mode.js b/Resources/Public/JavaScript/security/element/sudo-mode.js new file mode 100644 index 0000000..3b25bbc --- /dev/null +++ b/Resources/Public/JavaScript/security/element/sudo-mode.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as n,customElement as m,state as w,query as b}from"lit/decorators.js";import{LitElement as P,nothing as u,html as d}from"lit";import I from"@typo3/core/ajax/ajax-request.js";import{AjaxResponse as g}from"@typo3/core/ajax/ajax-response.js";import E from"@typo3/backend/viewport.js";import{topLevelModuleImport as T}from"@typo3/backend/utility/top-level-module-import.js";import M,{Sizes as $}from"@typo3/backend/modal.js";import{SeverityEnum as y}from"@typo3/backend/enum/severity.js";var r=function(i,e,s,o){var a=arguments.length,t=a<3?e:o===null?o=Object.getOwnPropertyDescriptor(e,s):o,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(i,e,s,o);else for(var h=i.length-1;h>=0;h--)(c=i[h])&&(t=(a<3?c(t):a>3?c(e,s,t):c(e,s))||t);return a>3&&t&&Object.defineProperty(e,s,t),t};class l extends P{}r([n({type:String})],l.prototype,"verifyActionUri",void 0),r([n({type:String})],l.prototype,"cancelUri",void 0),r([n({type:Boolean})],l.prototype,"isAjax",void 0),r([n({type:Boolean,attribute:"has-fatal-error"})],l.prototype,"hasFatalError",void 0),r([n({type:Boolean,attribute:"allow-install-tool-password"})],l.prototype,"allowInstallToolPassword",void 0),r([n({type:Object})],l.prototype,"labels",void 0);const v=async i=>{window.location!==window.parent.location&&T("@typo3/backend/security/element/sudo-mode.js");const s=top.document.createElement("typo3-backend-security-sudo-mode");return Object.assign(s,i),s.windowRef=window,top.document.body.append(s),new Promise((o,a)=>{s.addEventListener("typo3:sudo-mode:verified",()=>o()),s.addEventListener("typo3:sudo-mode:finished",()=>a())})};let f=class extends l{render(){return u}async firstUpdated(){if(window.location!==window.parent.location){try{await v(this.getPropertyValues())}catch{history.go(-1)}return}M.advanced({title:this.hasFatalError?this.labels.verificationFailed:this.labels.verifyWithUserPassword,severity:this.hasFatalError?y.error:y.notice,size:$.small,additionalCssClasses:["modal-sudo-mode-verification"],buttons:[this.hasFatalError?{text:this.labels.cancel,btnClass:"btn-default",trigger:()=>{top.location.href=this.cancelUri}}:{text:this.labels.verify,name:"verify",form:"verify-sudo-mode",btnClass:"btn-primary"}],content:d`this.dispatchEvent(new Event("typo3:sudo-mode:verified"))}>`}).addEventListener("typo3-modal-hidden",()=>{this.dispatchEvent(new Event("typo3:sudo-mode:finished")),this.remove()})}getPropertyValues(){const e={},s=this.constructor;for(const o of s.elementProperties.keys())e[o]=this[o];return e}};f=r([m("typo3-backend-security-sudo-mode")],f);let p=class extends l{constructor(){super(...arguments),this.useInstallToolPassword=!1,this.errorMessage=null}createRenderRoot(){return this}render(){return this.hasFatalError?d`
    ${this.labels.verificationExpired}
    `:d`
    ${this.errorMessage?d`
    ${this.labels[this.errorMessage]||this.errorMessage}
    `:u}

    ${this.useInstallToolPassword?this.labels.sudoModeInstallToolPasswordExplanation:this.labels.sudoModeUserPasswordExplanation}

    this.verifyPassword(e)}>${this.useInstallToolPassword?u:d``}
    ${this.allowInstallToolPassword?d``:u}
    `}updated(e){e.has("useInstallToolPassword")&&(this.closest("typo3-backend-modal").modalTitle=this.getModalTitle())}getModalTitle(){return this.hasFatalError?this.labels.verificationFailed:this.useInstallToolPassword?this.labels.verifyWithInstallToolPassword:this.labels.verifyWithUserPassword}async verifyPassword(e){e.preventDefault(),this.errorMessage=null;try{const o=await(await new I(this.verifyActionUri).post({password:this.passwordElement.value,useInstallToolPassword:this.useInstallToolPassword?1:0})).resolve("application/json");if(this.dispatchEvent(new Event("typo3:sudo-mode:verified")),this.closest("typo3-backend-modal").hideModal(),!this.isAjax&&o.redirect){const{uri:a}=o.redirect,t=this.windowRef??window;t.name==="list_frame"?E.ContentContainer.setUrl(a):t.location.assign(a)}}catch(s){if(s instanceof g){const o=await s.resolve("application/json");this.errorMessage=o.message}else throw s}}toggleUseInstallToolPassword(e){e.preventDefault(),this.useInstallToolPassword=!this.useInstallToolPassword}};r([w()],p.prototype,"useInstallToolPassword",void 0),r([w()],p.prototype,"errorMessage",void 0),r([b("#password")],p.prototype,"passwordElement",void 0),p=r([m("typo3-backend-security-sudo-mode-form")],p);export{f as SudoMode,p as SudoModeForm,v as initiateSudoModeModal}; diff --git a/Resources/Public/JavaScript/security/sudo-mode-interceptor.js b/Resources/Public/JavaScript/security/sudo-mode-interceptor.js new file mode 100644 index 0000000..cc659b4 --- /dev/null +++ b/Resources/Public/JavaScript/security/sudo-mode-interceptor.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +const s=async(o,a)=>{const n=o.clone(),t=await a(o);if(t.status===422){const{initiateSudoModeModal:e}=await import("@typo3/backend/security/element/sudo-mode.js"),i=await t.json();try{await e(i.sudoModeInitialization)}catch{return t}return a(n)}return t};export{s as sudoModeInterceptor}; diff --git a/Resources/Public/JavaScript/settings/editor.js b/Resources/Public/JavaScript/settings/editor.js new file mode 100644 index 0000000..cf76daa --- /dev/null +++ b/Resources/Public/JavaScript/settings/editor.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as S,html as a,nothing as c}from"lit";import{property as m,state as v,customElement as T}from"lit/decorators.js";import{live as C}from"lit/directives/live.js";import"@typo3/backend/element/spinner-element.js";import"@typo3/backend/element/icon-element.js";import k from"@typo3/backend/notification.js";import f from"@typo3/backend/utility/dom-helper.js";import R from"@typo3/core/ajax/ajax-request.js";import{copyToClipboard as w}from"@typo3/backend/copy-to-clipboard.js";import{markdown as E}from"@typo3/core/directive/markdown.js";import"@typo3/backend/settings/editor/editable-setting.js";import{SettingsMode as g,sanitizeSettingsMode as _}from"@typo3/backend/settings/enum/settings-mode.enum.js";import p from"~labels/backend.settingseditor";import D from"~labels/backend.copytoclipboard";import"@typo3/backend/settings/type/bool.js";import"@typo3/backend/settings/type/int.js";import"@typo3/backend/settings/type/number.js";import"@typo3/backend/settings/type/string.js";import"@typo3/backend/settings/type/stringlist.js";var d=function(u,i,e,t){var s=arguments.length,n=s<3?i:t===null?t=Object.getOwnPropertyDescriptor(i,e):t,o;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(u,i,e,t);else for(var r=u.length-1;r>=0;r--)(o=u[r])&&(n=(s<3?o(n):s>3?o(i,e,n):o(i,e))||n);return s>3&&n&&Object.defineProperty(i,e,n),n};class b extends Event{static{this.eventName="typo3:settings-editor:submit"}constructor(i,e){super(b.eventName,{bubbles:!0,composed:!0,cancelable:!1}),this.originalEvent=i,this.formData=e}}let l=class extends S{constructor(){super(...arguments),this.formName="settings_form",this.customFormData={},this.mode=g.basic,this.searchTerm="",this.activeCategory="",this.visibleCategories={},this.observer=null}disconnectedCallback(){super.disconnectedCallback(),this.observer?.disconnect()}createRenderRoot(){return this}adjustNavigationSize(){const i=f.scrollableParent(this),e=this.querySelector(".settings-navigation-inner"),t=this.querySelector(".settings-search"),s=this.querySelector(".settings-navigation");if(s!==null&&e){const n=i.getBoundingClientRect(),o=s.getBoundingClientRect(),r=t?.getBoundingClientRect().bottom??Math.max(n.top,e.getBoundingClientRect().top),h=Math.min(n.bottom,window.innerHeight),y=Math.max(0,h-o.bottom),$=h-r-y;e.style.maxHeight=`${$}px`}}firstUpdated(){f.scrollEventTarget(this).addEventListener("scroll",()=>{this.adjustNavigationSize()})}updated(i){if(i.has("mode")&&this.mode===g.minimal)this.observer?.disconnect(),this.observer=null;else if(i.has("mode")&&this.mode!==g.minimal){const e=f.scrollableParent(this);this.observer=new IntersectionObserver(t=>{t.forEach(o=>{const r=o.target.dataset.key;this.visibleCategories[r]=o.isIntersecting});const s=o=>o.reduce((r,h)=>[...r,h.key,...s(h.categories)],[]),n=s(this.categories).filter(o=>this.visibleCategories[o])[0]||"";n&&(this.activeCategory=n)},{root:e===document.documentElement?null:e,threshold:.1})}if([...this.renderRoot.querySelectorAll(".settings-category")].map(e=>this.observer?.observe(e)),this.adjustNavigationSize(),i.has("activeCategory")){const e=this.querySelector(".settings-navigation-inner"),t=this.querySelector(".settings-navigation-item.active");if(e&&t){const s=e.scrollTop,n=e.getBoundingClientRect().height,o=t.getBoundingClientRect().height,r=t.offsetTop>=s,h=t.offsetTop+o<=s+n;r?h||this.querySelector(".settings-navigation-inner").scrollTo({top:Math.max(0,t.offsetTop+o),behavior:"auto"}):this.querySelector(".settings-navigation-inner").scrollTo({top:Math.max(0,t.offsetTop-o),behavior:"auto"})}}}renderCategoryTree(i,e){return a`
      ${i.map(t=>a`
    • ${t.categories.length===0?c:a`${this.renderCategoryTree(t.categories,e+1)}`}
    • `)}
    `}renderSettings(i,e){return i.map(t=>a`
    ${this.renderHeadline(Math.min(e+1,6),`category-headline-${t.key}`,t.icon,a`${t.label}`)}
    ${t.description?E(t.description,"minimal"):c}
    ${t.settings.map(s=>a``)}
    ${t.categories.length===0?c:a`${this.renderSettings(t.categories,e+1)}`}`)}renderHeadline(i,e,t,s){switch(i){case 1:return a`

    ${t?a``:c}${s}

    `;case 2:return a`

    ${t?a``:c}${s}

    `;case 3:return a`

    ${t?a``:c}${s}

    `;case 4:return a`

    ${t?a``:c}${s}

    `;case 5:return a`
    ${t?a``:c}${s}
    `;case 6:return a`
    ${t?a``:c}${s}
    `;default:throw new Error(`Invalid header level: ${i}`)}}selectCategory(i){const e=`#category-headline-${i.key}`,t=this.renderRoot.querySelector(e.replaceAll(".","\\.")),s=f.scrollableParent(this),n=this.renderRoot.querySelector(".settings-search").offsetHeight,o=parseInt(window.getComputedStyle(this.renderRoot.querySelector(".settings-body-inner")).paddingTop,10),r=t.offsetTop-n-o;s.scrollTo({top:r,behavior:"smooth"}),this.activeCategory=i.key}async onSubmit(i){const e=i.target,t=new FormData(e),s={settings:{}};if(t.forEach((n,o)=>{const r=o.match(/^settings\[(.+?)\]$/);r?s.settings[r[1]]=typeof n=="string"?n:n.name:s[o]=typeof n=="string"?n:n.name}),this.dispatchEvent(new b(i,s)),!i.defaultPrevented&&i.submitter?.value==="export"){i.preventDefault();const n=new FormData(e),r=await(await new R(this.dumpUrl).post(n)).resolve();typeof r.yaml=="string"?w(r.yaml):(console.warn("Value can not be copied to clipboard.",typeof r.yaml),k.error(D.get("copyToClipboard.error")))}}async onSearch(i){i.preventDefault(),this.searchTerm=i.currentTarget.value}render(){const i=this.filterCategories(),e=i.filter(t=>!t.__hidden).length>0;return a`
    this.onSubmit(t)}>${Object.entries(this.customFormData).map(([t,s])=>a``)}
    ${this.mode!==g.minimal?a``:c} ${this.mode!==g.minimal?a`
    this.adjustNavigationSize()}>${this.renderCategoryTree(i??[],1)}
    `:c}
    ${this.renderSettings(i??[],1)}
    ${e?c:a`
    ${p.get("settingseditor.search.noResultsTitle")}

    ${p.get("settingseditor.search.noResultsMessage")}

    `}
    `}filterCategories(i=null){return i??=this.categories,i.map(e=>{const t=this.filterSettings(e.settings),s=this.filterCategories(e.categories),n=t.filter(r=>!r.__hidden).length>0,o=s.filter(r=>!r.__hidden).length>0;return{...e,settings:t,categories:s,__hidden:!n&&!o}})}filterSettings(i){return i.map(e=>({...e,__hidden:!(this.matchesSearchTerm(e.definition.key)||this.matchesSearchTerm(e.definition.label)||this.matchesSearchTerm(e.definition.description??"")||this.valueMatchesSearchTerm(e.value)||e.definition.tags.filter(t=>this.matchesSearchTerm(t)).length>0)}))}matchesSearchTerm(i){return this.searchTerm===""?!0:this.matchesSubstring(i,this.searchTerm)}valueMatchesSearchTerm(i){return typeof i=="string"?this.matchesSearchTerm(i):Array.isArray(i)?i.filter(e=>typeof e=="string"&&this.matchesSearchTerm(e)).length>0:!1}matchesSubstring(i,e){return i.toLowerCase().includes(e.toLowerCase())}};d([m({type:Array})],l.prototype,"categories",void 0),d([m({type:String,attribute:"form-name"})],l.prototype,"formName",void 0),d([m({type:String,attribute:"action-url"})],l.prototype,"actionUrl",void 0),d([m({type:String,attribute:"dump-url"})],l.prototype,"dumpUrl",void 0),d([m({type:Object,attribute:"custom-form-data"})],l.prototype,"customFormData",void 0),d([m({type:String,converter:_})],l.prototype,"mode",void 0),d([v()],l.prototype,"searchTerm",void 0),d([v()],l.prototype,"activeCategory",void 0),l=d([T("typo3-backend-settings-editor")],l);export{l as SettingsEditorElement,b as SettingsEditorSubmitEvent}; diff --git a/Resources/Public/JavaScript/settings/editor/editable-setting.js b/Resources/Public/JavaScript/settings/editor/editable-setting.js new file mode 100644 index 0000000..7427abe --- /dev/null +++ b/Resources/Public/JavaScript/settings/editor/editable-setting.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as f,html as r,nothing as p}from"lit";import{property as u,state as g,customElement as b}from"lit/decorators.js";import{until as h}from"lit/directives/until.js";import"@typo3/backend/element/icon-element.js";import{copyToClipboard as v}from"@typo3/backend/copy-to-clipboard.js";import $ from"@typo3/backend/notification.js";import{markdown as w}from"@typo3/core/directive/markdown.js";import k from"@typo3/core/ajax/ajax-request.js";import{SettingsMode as c,sanitizeSettingsMode as A}from"@typo3/backend/settings/enum/settings-mode.enum.js";import y from"~labels/backend.settingseditor";import S from"~labels/backend.copytoclipboard";import"bootstrap";var d=function(l,e,t,i){var o=arguments.length,n=o<3?e:i===null?i=Object.getOwnPropertyDescriptor(e,t):i,s;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(l,e,t,i);else for(var m=l.length-1;m>=0;m--)(s=l[m])&&(n=(o<3?s(n):o>3?s(e,t,n):s(e,t))||n);return o>3&&n&&Object.defineProperty(e,t,n),n};let a=class extends f{constructor(){super(...arguments),this.mode=c.basic,this.hasChange=!1,this.typeElement=null}createRenderRoot(){return this}render(){const{value:e,systemDefault:t,definition:i}=this.setting;return r`
    ${w(i.description??"","minimal")}
    ${this.mode===c.advanced?r`
    ${i.key}
    `:p}
    ${h(this.renderField(),p)}
    ${this.renderActions()}
    `}renderField(){return this.typeElement!==null?(this.updateFieldAttributes(this.typeElement),this.typeElement):(async()=>{const{typeImplementation:e}=this.setting,t=await import(e);if(!("componentName"in t))throw new Error(`module ${e} is missing the "componentName" export`);const i=document.createElement(t.componentName);return i.addEventListener("typo3:setting:changed",o=>{this.hasChange=JSON.stringify(this.setting.value)!==JSON.stringify(o.detail.value)}),this.updateFieldAttributes(i),this.typeElement=i,i})()}updateFieldAttributes(e){const{definition:t,value:i}=this.setting,o={key:t.key,formid:`setting-${t.key}`,name:`settings[${t.key}]`,value:Array.isArray(i)?JSON.stringify(i):String(i),debug:this.mode===c.advanced,readonly:t.readonly,enum:Object.keys(t.enum).length>0?JSON.stringify(t.enum):!1,default:Array.isArray(t.default)?JSON.stringify(t.default):String(t.default),options:JSON.stringify(t.options)};for(const[n,s]of Object.entries(o)){if(typeof s=="boolean"){s&&!e.hasAttribute(n)&&e.setAttribute(n,""),!s&&e.hasAttribute(n)&&e.removeAttribute(n);continue}e.getAttribute(n)!==s&&e.setAttribute(n,s)}}renderActions(){const{definition:e}=this.setting;return r``}setToDefaultValue(){this.typeElement&&(this.typeElement.value=this.setting.systemDefault,this.typeElement.requestUpdate("value"))}async copyAsYaml(){const e=new FormData(this.typeElement.form),t=`settings[${this.setting.definition.key}]`,i=e.get(t),o=new FormData;o.append("specificSetting",this.setting.definition.key),o.append(t,i);const s=await(await new k(this.dumpuri).post(o)).resolve();typeof s.yaml=="string"?v(s.yaml):(console.warn("Value can not be copied to clipboard.",typeof s.yaml),$.error(S.get("copyToClipboard.error")))}};d([u({type:Object})],a.prototype,"setting",void 0),d([u({type:String})],a.prototype,"dumpuri",void 0),d([u({type:String,converter:A})],a.prototype,"mode",void 0),d([g()],a.prototype,"hasChange",void 0),a=d([b("typo3-backend-editable-setting")],a);export{a as EditableSettingElement}; diff --git a/Resources/Public/JavaScript/settings/enum/settings-mode.enum.js b/Resources/Public/JavaScript/settings/enum/settings-mode.enum.js new file mode 100644 index 0000000..55b751a --- /dev/null +++ b/Resources/Public/JavaScript/settings/enum/settings-mode.enum.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +var a;(function(i){i.minimal="minimal",i.basic="basic",i.advanced="advanced"})(a||(a={}));function n(i){return Object.values(a).includes(i)?i:a.basic}export{a as SettingsMode,n as sanitizeSettingsMode}; diff --git a/Resources/Public/JavaScript/settings/type/base.js b/Resources/Public/JavaScript/settings/type/base.js new file mode 100644 index 0000000..9e82994 --- /dev/null +++ b/Resources/Public/JavaScript/settings/type/base.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as f}from"lit";import{defaultConverter as v}from"@lit/reactive-element";import{property as a}from"lit/decorators.js";var i=function(u,t,e,r){var s=arguments.length,l=s<3?t:r===null?r=Object.getOwnPropertyDescriptor(t,e):r,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")l=Reflect.decorate(u,t,e,r);else for(var b=u.length-1;b>=0;b--)(c=u[b])&&(l=(s<3?c(l):s>3?c(t,e,l):c(t,e))||l);return s>3&&l&&Object.defineProperty(t,e,l),l};const n=Symbol("internals"),h=Symbol("privateInternals"),p=Symbol("getFormValue"),d=Symbol("getFormState");class o extends f{constructor(){super(...arguments),this.readonly=!1,this.debug=!1}static{this.formAssociated=!0}createRenderRoot(){return this}get[n](){return this[h]||(this[h]=this.attachInternals()),this[h]}get form(){return this[n].form}get labels(){return this[n].labels}get name(){return this.getAttribute("name")??""}set name(t){this.setAttribute("name",t)}get disabled(){return this.hasAttribute("disabled")}set disabled(t){this.toggleAttribute("disabled",t)}attributeChangedCallback(t,e,r){if(t==="name"||t==="disabled"){const s=t==="disabled"?e!==null:e;this.requestUpdate(t,s);return}super.attributeChangedCallback(t,e,r)}requestUpdate(t,e,r){super.requestUpdate(t,e,r),t==="value"&&(this.dispatchEvent(new CustomEvent("typo3:setting:changed",{detail:{value:this.value}})),this[n].setFormValue(this[p](),this[d]()))}formDisabledCallback(t){this.disabled=t}formResetCallback(){const t=this.value,e=this.getAttribute("value");this.attributeChangedCallback("value",this.valueToString(t),null),this.attributeChangedCallback("value",null,e)}formStateRestoreCallback(t){if(typeof t=="string")this.attributeChangedCallback("value",this.valueToString(this.value),null),this.attributeChangedCallback("value",null,t);else throw new Error(`formStateRestoreCallback() needs to be implemented for <${this.localName}> for state type "${typeof t}"`)}[d](){return this[p]()}[p](){return this.valueToString(this.value)}valueToString(t){const r=this.constructor.getPropertyOptions("value");return(typeof r.converter=="object"&&typeof r.converter?.toAttribute=="function"?r.converter.toAttribute:v.toAttribute)(t,r.type)}}i([a({type:String})],o.prototype,"key",void 0),i([a({type:String})],o.prototype,"formid",void 0),i([a({type:Boolean})],o.prototype,"readonly",void 0),i([a({type:Object})],o.prototype,"enum",void 0),i([a({type:Boolean})],o.prototype,"debug",void 0),i([a({type:Object})],o.prototype,"options",void 0),i([a({noAccessor:!0})],o.prototype,"name",null),i([a({type:Boolean,noAccessor:!0})],o.prototype,"disabled",null);export{o as BaseElement,d as getFormState,p as getFormValue,n as internals}; diff --git a/Resources/Public/JavaScript/settings/type/bool.js b/Resources/Public/JavaScript/settings/type/bool.js new file mode 100644 index 0000000..a29733e --- /dev/null +++ b/Resources/Public/JavaScript/settings/type/bool.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as u}from"lit";import{property as s,customElement as a}from"lit/decorators.js";import{BaseElement as h}from"@typo3/backend/settings/type/base.js";var m=function(e,t,r,n){var c=arguments.length,o=c<3?t:n===null?n=Object.getOwnPropertyDescriptor(t,r):n,l;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")o=Reflect.decorate(e,t,r,n);else for(var i=e.length-1;i>=0;i--)(l=e[i])&&(o=(c<3?l(o):c>3?l(t,r,o):l(t,r))||o);return c>3&&o&&Object.defineProperty(t,r,o),o};const f="typo3-backend-settings-type-bool";let p=class extends h{render(){return u`
    this.value=t.target.checked}>
    `}};m([s({type:Boolean,converter:{toAttribute:e=>e?"1":"0",fromAttribute:e=>e==="1"||e==="true"}})],p.prototype,"value",void 0),p=m([a(f)],p);export{p as BoolTypeElement,f as componentName}; diff --git a/Resources/Public/JavaScript/settings/type/color.js b/Resources/Public/JavaScript/settings/type/color.js new file mode 100644 index 0000000..c3c476b --- /dev/null +++ b/Resources/Public/JavaScript/settings/type/color.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as m}from"lit";import{property as f,customElement as s}from"lit/decorators.js";import{BaseElement as d}from"@typo3/backend/settings/type/base.js";import"@typo3/backend/color-picker.js";import y from"@typo3/core/event/regular-event.js";var a=function(r,e,o,n){var l=arguments.length,t=l<3?e:n===null?n=Object.getOwnPropertyDescriptor(e,o):n,p;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(r,e,o,n);else for(var u=r.length-1;u>=0;u--)(p=r[u])&&(t=(l<3?p(t):l>3?p(e,o,t):p(e,o))||t);return l>3&&t&&Object.defineProperty(e,o,t),t};const c="typo3-backend-settings-type-color";let i=class extends d{firstUpdated(){const e=this.getInputElement();e&&new y("blur",o=>{this.updateValue(o.target.value)}).bindTo(e)}updateValue(e){this.value=e}render(){return m`this.updateValue(e.target.value)}>`}getInputElement(){return this.querySelector("input")}};a([f({type:String})],i.prototype,"value",void 0),i=a([s(c)],i);export{i as ColorTypeElement,c as componentName}; diff --git a/Resources/Public/JavaScript/settings/type/int.js b/Resources/Public/JavaScript/settings/type/int.js new file mode 100644 index 0000000..1d790ef --- /dev/null +++ b/Resources/Public/JavaScript/settings/type/int.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as l,nothing as p}from"lit";import{property as f,customElement as d}from"lit/decorators.js";import{live as h}from"lit/directives/live.js";import{BaseElement as $}from"@typo3/backend/settings/type/base.js";var u=function(i,e,t,o){var r=arguments.length,n=r<3?e:o===null?o=Object.getOwnPropertyDescriptor(e,t):o,s;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(i,e,t,o);else for(var m=i.length-1;m>=0;m--)(s=i[m])&&(n=(r<3?s(n):r>3?s(e,t,n):s(e,t))||n);return r>3&&n&&Object.defineProperty(e,t,n),n};const c="typo3-backend-settings-type-int";let a=class extends ${handleChange(e){const t=e.target;t.reportValidity()&&(t instanceof HTMLInputElement?this.value=t.valueAsNumber:this.value=parseInt(t.value,10))}renderEnum(){return l``}render(){return typeof this.enum=="object"?this.renderEnum():l``}};u([f({type:Number})],a.prototype,"value",void 0),a=u([d(c)],a);export{a as IntTypeElement,c as componentName}; diff --git a/Resources/Public/JavaScript/settings/type/number.js b/Resources/Public/JavaScript/settings/type/number.js new file mode 100644 index 0000000..f9da1cf --- /dev/null +++ b/Resources/Public/JavaScript/settings/type/number.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as f,nothing as s}from"lit";import{property as c,customElement as h}from"lit/decorators.js";import{live as d}from"lit/directives/live.js";import{BaseElement as y}from"@typo3/backend/settings/type/base.js";var a=function(o,e,t,r){var i=arguments.length,n=i<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,t):r,m;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(o,e,t,r);else for(var l=o.length-1;l>=0;l--)(m=o[l])&&(n=(i<3?m(n):i>3?m(e,t,n):m(e,t))||n);return i>3&&n&&Object.defineProperty(e,t,n),n};const u="typo3-backend-settings-type-number";let p=class extends y{handleChange(e){const t=e.target;t.reportValidity()&&(this.value=t.valueAsNumber)}render(){return f``}};a([c({type:Number})],p.prototype,"value",void 0),p=a([h(u)],p);export{p as NumberTypeElement,u as componentName}; diff --git a/Resources/Public/JavaScript/settings/type/page.js b/Resources/Public/JavaScript/settings/type/page.js new file mode 100644 index 0000000..e5580db --- /dev/null +++ b/Resources/Public/JavaScript/settings/type/page.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as m,nothing as c}from"lit";import{property as u,customElement as g}from"lit/decorators.js";import{MessageUtility as y}from"@typo3/backend/utility/message-utility.js";import{BaseElement as h}from"@typo3/backend/settings/type/base.js";import d from"@typo3/backend/modal.js";import v from"~labels/backend.settingseditor";import"@typo3/backend/element/icon-element.js";var p=function(o,e,n,r){var i=arguments.length,t=i<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,n):r,s;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(o,e,n,r);else for(var l=o.length-1;l>=0;l--)(s=o[l])&&(t=(i<3?s(t):i>3?s(e,n,t):s(e,n))||t);return i>3&&t&&Object.defineProperty(e,n,t),t};const f="typo3-backend-settings-type-page";let a=class extends h{constructor(){super(...arguments),this.elementBrowserListener=e=>{if(!y.verifyOrigin(e.origin))throw"Denied message sent by "+e.origin;if(e.data.actionName==="typo3:elementBrowser:elementAdded"){if(typeof e.data.fieldName>"u")throw"fieldName not defined in message";if(typeof e.data.value>"u")throw"value not defined in message";this.value=e.data.value.split("_").pop()}}}render(){return m`
    this.value=parseInt(e.target.value,10)}> ${this.canUseElementBrowser()?m``:c}
    `}canUseElementBrowser(){return top.TYPO3.settings?.Wizards?.elementBrowserUrl!==void 0}openElementBrowser(){const e="db",n=new URLSearchParams({mode:e,fieldReference:this.formid,allowedTypes:"pages"}),r=d.advanced({type:d.types.iframe,content:top.TYPO3.settings.Wizards.elementBrowserUrl+"&"+n.toString(),size:d.sizes.large});window.addEventListener("message",this.elementBrowserListener),r.addEventListener("typo3-modal-hide",()=>{window.removeEventListener("message",this.elementBrowserListener)})}};p([u({type:Number})],a.prototype,"value",void 0),a=p([g(f)],a);export{a as PageTypeElement,f as componentName}; diff --git a/Resources/Public/JavaScript/settings/type/string.js b/Resources/Public/JavaScript/settings/type/string.js new file mode 100644 index 0000000..e2d8d90 --- /dev/null +++ b/Resources/Public/JavaScript/settings/type/string.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as s,nothing as m}from"lit";import{property as f,customElement as d}from"lit/decorators.js";import{live as p}from"lit/directives/live.js";import{BaseElement as $}from"@typo3/backend/settings/type/base.js";var c=function(i,e,t,o){var r=arguments.length,n=r<3?e:o===null?o=Object.getOwnPropertyDescriptor(e,t):o,l;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(i,e,t,o);else for(var h=i.length-1;h>=0;h--)(l=i[h])&&(n=(r<3?l(n):r>3?l(e,t,n):l(e,t))||n);return r>3&&n&&Object.defineProperty(e,t,n),n};const u="typo3-backend-settings-type-string";let a=class extends ${handleChange(e){const t=e.target;t.reportValidity()&&(this.value=t.value)}renderEnum(){return s``}render(){return typeof this.enum=="object"?this.renderEnum():s``}};c([f({type:String})],a.prototype,"value",void 0),a=c([d(u)],a);export{a as StringTypeElement,u as componentName}; diff --git a/Resources/Public/JavaScript/settings/type/stringlist.js b/Resources/Public/JavaScript/settings/type/stringlist.js new file mode 100644 index 0000000..07a9805 --- /dev/null +++ b/Resources/Public/JavaScript/settings/type/stringlist.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as d}from"lit";import{property as p,customElement as b}from"lit/decorators.js";import{BaseElement as m}from"@typo3/backend/settings/type/base.js";import{live as f}from"lit/directives/live.js";var c=function(i,t,e,l){var n=arguments.length,o=n<3?t:l===null?l=Object.getOwnPropertyDescriptor(t,e):l,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")o=Reflect.decorate(i,t,e,l);else for(var s=i.length-1;s>=0;s--)(a=i[s])&&(o=(n<3?a(o):n>3?a(t,e,o):a(t,e))||o);return n>3&&o&&Object.defineProperty(t,e,o),o};const u="typo3-backend-settings-type-stringlist";let r=class extends m{updateValue(t,e){const l=[...this.value];l[e]=t,this.value=l}addValue(t,e=""){this.value=this.value.toSpliced(t+1,0,e)}removeValue(t){this.value=this.value.toSpliced(t,1)}renderItem(t,e){return d`0?"-"+e:""}`} type=text class=form-control ?readonly=${this.readonly} .value=${f(t)} @change=${l=>this.updateValue(l.target.value,e)}>
    `}render(){const t=this.value||[];return t.length===0?d``:d`
    ${t.map((e,l)=>this.renderItem(e,l))}
    `}};c([p({type:Array})],r.prototype,"value",void 0),r=c([b(u)],r);export{r as StringlistTypeElement,u as componentName}; diff --git a/Resources/Public/JavaScript/settings/type/url.js b/Resources/Public/JavaScript/settings/type/url.js new file mode 100644 index 0000000..99d6567 --- /dev/null +++ b/Resources/Public/JavaScript/settings/type/url.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as u,nothing as c}from"lit";import{property as f,customElement as h}from"lit/decorators.js";import{live as d}from"lit/directives/live.js";import{BaseElement as v}from"@typo3/backend/settings/type/base.js";var m=function(n,e,t,o){var l=arguments.length,r=l<3?e:o===null?o=Object.getOwnPropertyDescriptor(e,t):o,i;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")r=Reflect.decorate(n,e,t,o);else for(var a=n.length-1;a>=0;a--)(i=n[a])&&(r=(l<3?i(r):l>3?i(e,t,r):i(e,t))||r);return l>3&&r&&Object.defineProperty(e,t,r),r};const s="typo3-backend-settings-type-url";let p=class extends v{constructor(){super(...arguments),this.value=""}handleChange(e){const t=e.target;t.reportValidity()&&(this.value=t.value.trim())}render(){return u``}};m([f({type:String})],p.prototype,"value",void 0),p=m([h(s)],p);export{p as UrlTypeElement,s as componentName}; diff --git a/Resources/Public/JavaScript/setup-module.js b/Resources/Public/JavaScript/setup-module.js new file mode 100644 index 0000000..2c5f167 --- /dev/null +++ b/Resources/Public/JavaScript/setup-module.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{MessageUtility as r}from"@typo3/backend/utility/message-utility.js";import d from"@typo3/backend/storage/client.js";import s from"@typo3/core/event/regular-event.js";import o from"@typo3/backend/modal.js";class i{constructor(){new s("setup:confirmation:response",i.handleConfirmationResponse).delegateTo(document,'[data-event-name="setup:confirmation:response"]'),new s("click",(e,t)=>{const a=new CustomEvent(t.dataset.eventName,{bubbles:!0,detail:{payload:t.dataset.eventPayload}});t.dispatchEvent(a)}).delegateTo(document,'[data-event="click"][data-event-name]'),document.querySelectorAll("[data-setup-avatar-field]").forEach(e=>{const t=e.dataset.setupAvatarField,a=document.getElementById("clear_button_"+t),n=document.getElementById("add_button_"+t);n.addEventListener("click",()=>this.avatarOpenFileBrowser(n.dataset.setupAvatarUrl)),a?.addEventListener("click",()=>this.avatarClearExistingImage(t))}),document.querySelector("[data-setup-avatar-field]")!==null&&this.initializeMessageListener()}static handleConfirmationResponse(e){if(e.detail.result&&e.detail.payload==="resetConfiguration"){d.unsetByPrefix("");const t=document.querySelector("#setValuesToDefault");t.value="1",t.form.submit()}}static hideElement(e){e.style.display="none"}initializeMessageListener(){window.addEventListener("message",e=>{if(!r.verifyOrigin(e.origin))throw new Error("Denied message sent by "+e.origin);if(e.data.actionName==="typo3:foreignRelation:insert"){if(typeof e.data.objectGroup>"u")throw new Error("No object group defined for message");const t=e.data.objectGroup.match(/(?:^|-)avatar-(.+)$/);if(t===null)return;this.avatarSetFileUid(t[1],e.data.uid)}})}avatarOpenFileBrowser(e){o.advanced({type:o.types.iframe,content:e,size:o.sizes.large})}avatarClearExistingImage(e){const t=document.getElementById("field_"+e),a=document.getElementById("image_"+e),n=document.getElementById("clear_button_"+e);n&&i.hideElement(n),a&&i.hideElement(a),t.value="delete"}avatarSetFileUid(e,t){this.avatarClearExistingImage(e);const a=document.getElementById("field_"+e),n=document.getElementById("add_button_"+e);a.value=t,n.classList.remove("btn-default"),n.classList.add("btn-info"),this.avatarWindowRef instanceof Window&&!this.avatarWindowRef.closed&&(this.avatarWindowRef.close(),this.avatarWindowRef=null)}}var l=new i;export{l as default}; diff --git a/Resources/Public/JavaScript/severity.js b/Resources/Public/JavaScript/severity.js new file mode 100644 index 0000000..fef5d90 --- /dev/null +++ b/Resources/Public/JavaScript/severity.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{SeverityEnum as e}from"@typo3/backend/enum/severity.js";class r{static{this.notice=e.notice}static{this.info=e.info}static{this.ok=e.ok}static{this.warning=e.warning}static{this.error=e.error}static getCssClass(n){let t;switch(n){case e.notice:t="notice";break;case e.ok:t="success";break;case e.warning:t="warning";break;case e.error:t="danger";break;case e.info:default:t="info"}return t}}let i;try{window.opener&&window.opener.TYPO3&&window.opener.TYPO3.Severity&&(i=window.opener.TYPO3.Severity),parent&&parent.window.TYPO3&&parent.window.TYPO3.Severity&&(i=parent.window.TYPO3.Severity),top&&top.TYPO3&&top.TYPO3.Severity&&(i=top.TYPO3.Severity)}catch{}i||(i=r,typeof TYPO3<"u"&&(TYPO3.Severity=i));export{r as default}; diff --git a/Resources/Public/JavaScript/site-inline-actions.js b/Resources/Public/JavaScript/site-inline-actions.js new file mode 100644 index 0000000..ecd1bed --- /dev/null +++ b/Resources/Public/JavaScript/site-inline-actions.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import e from"@typo3/core/document-service.js";class i{constructor(){e.ready().then(()=>{TYPO3.settings.ajaxUrls.record_inline_details=TYPO3.settings.ajaxUrls.site_configuration_inline_details,TYPO3.settings.ajaxUrls.record_inline_create=TYPO3.settings.ajaxUrls.site_configuration_inline_create})}}var t=new i;export{t as default}; diff --git a/Resources/Public/JavaScript/sortable-table.js b/Resources/Public/JavaScript/sortable-table.js new file mode 100644 index 0000000..b372947 --- /dev/null +++ b/Resources/Public/JavaScript/sortable-table.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import x from"tablesort";import"tablesort.dotsep.js";import"tablesort.number.js";import{IconElement as L}from"@typo3/backend/element/icon-element.js";import{Sizes as A}from"@typo3/backend/enum/icon-types.js";import D from"~labels/core.core";class I extends x{init(o,g){let r,l;if(this.table=o,this.thead=!1,this.options=g,o.rows&&o.rows.length>0)if(o.tHead&&o.tHead.rows.length>0){for(let t=0;t{t.preventDefault(),t.stopImmediatePropagation();const n=t.currentTarget.dataset.sortDirection,e=t.currentTarget.closest("th, td");this.current===e&&this.current.ariaSort===n||(e.ariaSort=n==="ascending"?"descending":"ascending",this.current&&this.current!==e&&this.current.removeAttribute("aria-sort"),this.current=e,this.sortTable(e))};for(let t=0;t{const r=g.target;r.tHead.querySelectorAll(".dropdown-toggle[data-sorting-toggle]").forEach(s=>{s.querySelector(":scope > div").classList.remove("text-primary");const a=s.querySelector("typo3-backend-icon");a.identifier="empty-empty",a.classList.remove("text-primary")}),r.tHead.querySelectorAll(".dropdown-toggle[data-sorting-toggle] + .dropdown-menu typo3-backend-icon").forEach(s=>{s.identifier="empty-empty"});const t=r.tHead.querySelector("th[aria-sort]"),n=t.querySelector(".dropdown-toggle[data-sorting-toggle]");n.querySelector(":scope > div").classList.add("text-primary");const c=n.querySelector("typo3-backend-icon"),m=t.querySelector(".dropdown-menu");t.ariaSort==="ascending"?c.identifier="actions-sort-amount-up":c.identifier="actions-sort-amount-down",m.querySelectorAll(".dropdown-item").forEach(s=>{const i=s.dataset.sortDirection,a=s.querySelector("typo3-backend-icon");i===t.ariaSort?a.identifier="actions-dot":a.identifier="empty-empty"})}),new I(o)}}export{W as default}; diff --git a/Resources/Public/JavaScript/storage/abstract-client-storage.js b/Resources/Public/JavaScript/storage/abstract-client-storage.js new file mode 100644 index 0000000..41c10ce --- /dev/null +++ b/Resources/Public/JavaScript/storage/abstract-client-storage.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class r{constructor(){this.keyPrefix="t3-",this.storage=null}get(t){return this.storage===null?null:this.storage.getItem(this.keyPrefix+t)}getByPrefix(t){if(this.storage===null)return{};const e=Object.entries(this.storage).filter(s=>s[0].startsWith(this.keyPrefix+t)).map(s=>[s[0].substring(this.keyPrefix.length),s[1]]);return Object.fromEntries(e)}set(t,e){this.storage!==null&&this.storage.setItem(this.keyPrefix+t,e)}unset(t){this.storage!==null&&this.storage.removeItem(this.keyPrefix+t)}unsetByPrefix(t){this.storage!==null&&(t=this.keyPrefix+t,Object.keys(this.storage).filter(e=>e.startsWith(t)).forEach(e=>this.storage.removeItem(e)))}clear(){this.storage!==null&&this.storage.clear()}isset(t){return this.storage===null?!1:this.get(t)!==null}}export{r as default}; diff --git a/Resources/Public/JavaScript/storage/browser-session.js b/Resources/Public/JavaScript/storage/browser-session.js new file mode 100644 index 0000000..eda2572 --- /dev/null +++ b/Resources/Public/JavaScript/storage/browser-session.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import s from"@typo3/backend/storage/abstract-client-storage.js";class e extends s{constructor(){super(),this.storage=sessionStorage}}var r=new e;export{r as default}; diff --git a/Resources/Public/JavaScript/storage/client.js b/Resources/Public/JavaScript/storage/client.js new file mode 100644 index 0000000..ae19113 --- /dev/null +++ b/Resources/Public/JavaScript/storage/client.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import t from"@typo3/backend/storage/abstract-client-storage.js";class e extends t{constructor(){super(),this.storage=localStorage}}var r=new e;export{r as default}; diff --git a/Resources/Public/JavaScript/storage/module-state-storage.js b/Resources/Public/JavaScript/storage/module-state-storage.js new file mode 100644 index 0000000..87b3ab7 --- /dev/null +++ b/Resources/Public/JavaScript/storage/module-state-storage.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class e{static{this.prefix="t3-module-state-"}static update(s,t){if(t===null&&(t=0),typeof t=="number")t=t.toString(10);else if(typeof t!="string")throw new SyntaxError("identifier must be of type string");const r=e.current(s),n=t===r.identifier?r.treeIdentifier:null,i={identifier:t,treeIdentifier:n};return e.commit(s,"update",i),i}static updateWithTreeIdentifier(s,t,r){if(typeof t=="number")t=t.toString(10);else if(typeof t!="string")throw new SyntaxError("identifier must be of type string");if(typeof r=="number")r=r.toString(10);else if(typeof r!="string")throw new SyntaxError("treeIdentifier must be of type string");const n={identifier:t,treeIdentifier:r};return e.commit(s,"update-with-tree-identifier",n),n}static updateWithCurrentMount(s,t){e.update(s,t)}static current(s){return{...e.getInitialState(),...e.fetch(s)??{}}}static purge(){Object.keys(sessionStorage).filter(s=>s.startsWith(e.prefix)).forEach(s=>sessionStorage.removeItem(s))}static fetch(s){const t=sessionStorage.getItem(e.prefix+s);return t===null?null:JSON.parse(t)}static async commit(s,t,r){const n=e.current(s);sessionStorage.setItem(e.prefix+s,JSON.stringify(r)),top.document.dispatchEvent(new CustomEvent("typo3:module-state-storage:"+t+":"+s,{detail:{state:r,oldState:n}}))}static getInitialState(){return{identifier:"",treeIdentifier:null}}}window.ModuleStateStorage=e;export{e as ModuleStateStorage}; diff --git a/Resources/Public/JavaScript/storage/persistent.js b/Resources/Public/JavaScript/storage/persistent.js new file mode 100644 index 0000000..0b38ed2 --- /dev/null +++ b/Resources/Public/JavaScript/storage/persistent.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import r from"@typo3/core/ajax/ajax-request.js";class o{constructor(){this.data=null}get(s){return this.data===null&&(this.data=this.loadFromServer()),this.getRecursiveDataByDeepKey(this.data,s.split("."))}set(s,e){return this.data!==null&&(this.data=this.setRecursiveDataByDeepKey(this.data,s.split("."),e)),this.storeOnServer(s,e)}async addToList(s,e){const t=await new r(TYPO3.settings.ajaxUrls.usersettings_process).post({action:"addToList",key:s,value:e});return this.resolveResponse(t)}async removeFromList(s,e){const t=await new r(TYPO3.settings.ajaxUrls.usersettings_process).post({action:"removeFromList",key:s,value:e});return this.resolveResponse(t)}async unset(s){const e=await new r(TYPO3.settings.ajaxUrls.usersettings_process).post({action:"unset",key:s});return this.resolveResponse(e)}clear(){new r(TYPO3.settings.ajaxUrls.usersettings_process).post({action:"clear"}),this.data=null}isset(s){const e=this.get(s);return typeof e<"u"&&e!==null}load(s){this.data=s}loadFromServer(){const s=new URL(location.origin+TYPO3.settings.ajaxUrls.usersettings_process);s.searchParams.set("action","getAll");const e=new XMLHttpRequest;if(e.open("GET",s.toString(),!1),e.send(),e.status===200)return JSON.parse(e.responseText);throw`Unexpected response code ${e.status}, reason: ${e.responseText}`}async storeOnServer(s,e){const t=await new r(TYPO3.settings.ajaxUrls.usersettings_process).post({action:"set",key:s,value:e});return this.resolveResponse(t)}getRecursiveDataByDeepKey(s,e){if(e.length===1)return(s||{})[e[0]];const t=e.shift();return this.getRecursiveDataByDeepKey(s[t]||{},e)}setRecursiveDataByDeepKey(s,e,t){if(e.length===1)s=s||{},s[e[0]]=t;else{const n=e.shift();s[n]=this.setRecursiveDataByDeepKey(s[n]||{},e,t)}return s}async resolveResponse(s){const e=await s.resolve();return this.data=e,e}}var i=new o;export{i as default}; diff --git a/Resources/Public/JavaScript/switch-user.js b/Resources/Public/JavaScript/switch-user.js new file mode 100644 index 0000000..6d6b4b6 --- /dev/null +++ b/Resources/Public/JavaScript/switch-user.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{PseudoButtonLitElement as p}from"@typo3/backend/element/pseudo-button.js";import{property as l,customElement as d}from"lit/decorators.js";import f from"@typo3/core/ajax/ajax-request.js";import u from"@typo3/backend/notification.js";var w=function(i,e,t,n){var c=arguments.length,r=c<3?e:n===null?n=Object.getOwnPropertyDescriptor(e,t):n,h;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")r=Reflect.decorate(i,e,t,n);else for(var a=i.length-1;a>=0;a--)(h=i[a])&&(r=(c<3?h(r):c>3?h(e,t,r):h(e,t))||r);return c>3&&r&&Object.defineProperty(e,t,r),r},o;(function(i){i.switch="switch",i.exit="exit"})(o||(o={}));let s=class extends p{constructor(){super(...arguments),this.mode=o.switch}buttonActivated(){this.mode===o.switch?this.handleSwitchUser():this.mode===o.exit&&this.handleExitSwitchUser()}handleSwitchUser(){if(!this.targetUser){u.error("Switching to user went wrong.");return}new f(TYPO3.settings.ajaxUrls.switch_user).post({targetUser:this.targetUser}).then(async e=>{const t=await e.resolve();t.success===!0&&t.url?top.window.location.href=t.url:u.error("Switching to user went wrong.")})}handleExitSwitchUser(){new f(TYPO3.settings.ajaxUrls.switch_user_exit).post({}).then(async e=>{const t=await e.resolve();t.success===!0&&t.url?top.window.location.href=t.url:u.error("Exiting current user went wrong.")})}};w([l({type:String})],s.prototype,"targetUser",void 0),w([l({type:o})],s.prototype,"mode",void 0),s=w([d("typo3-backend-switch-user")],s);export{s as SwitchUser}; diff --git a/Resources/Public/JavaScript/tab.js b/Resources/Public/JavaScript/tab.js new file mode 100644 index 0000000..6d4c9a4 --- /dev/null +++ b/Resources/Public/JavaScript/tab.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as v,html as m}from"lit";import{customElement as p}from"lit/decorators.js";import b from"@typo3/core/document-service.js";import h from"@typo3/backend/storage/browser-session.js";import y from"@typo3/backend/storage/client.js";import"@typo3/backend/element/icon-element.js";var A=function(l,t,e,s){var n=arguments.length,r=n<3?t:s===null?s=Object.getOwnPropertyDescriptor(t,e):s,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")r=Reflect.decorate(l,t,e,s);else for(var i=l.length-1;i>=0;i--)(a=l[i])&&(r=(n<3?a(r):n>3?a(t,e,r):a(t,e))||r);return n>3&&r&&Object.defineProperty(t,e,r),r};class c extends CustomEvent{static{this.eventName="typo3:tab:show"}constructor(t){super(c.eventName,{bubbles:!0,cancelable:!0,detail:{relatedTarget:t}})}}class u extends CustomEvent{static{this.eventName="typo3:tab:shown"}constructor(t){super(u.eventName,{bubbles:!0,detail:{relatedTarget:t}})}}class o{static{this.idCounter=0}constructor(){document.addEventListener("click",t=>{const e=t.target?.closest('[data-typo3-tab], [data-bs-toggle="tab"]');e&&(t.preventDefault(),o.show(e))}),document.addEventListener("keydown",t=>{const e=t.target;if(!e.matches?.('[role="tab"]'))return;const s=e.closest('[role="tablist"]');if(!s)return;const n=Array.from(s.querySelectorAll('[role="tab"]:not([disabled])'));if(n.length<2)return;const r=getComputedStyle(s).direction==="rtl";let a=null;switch(t.key){case"ArrowLeft":a=n[(n.indexOf(e)-(r?-1:1)+n.length)%n.length];break;case"ArrowRight":a=n[(n.indexOf(e)+(r?-1:1)+n.length)%n.length];break;case"Home":a=n[0];break;case"End":a=n[n.length-1];break;default:return}t.preventDefault(),a.focus(),a.closest(".nav-item")?.scrollIntoView({behavior:"smooth",inline:"nearest",block:"nearest"})}),b.ready().then(()=>{o.initialize()})}static initialize(t=document){for(const e of t.querySelectorAll('[role="tablist"]'))for(const s of e.querySelectorAll('[role="tab"], [data-bs-toggle="tab"]')){const n=o.migrate(s),r=n.classList.contains("active");n.setAttribute("aria-selected",r?"true":"false"),n.setAttribute("tabindex",r?"0":"-1");const a=o.getPanel(n);n.id||(n.id=a?a.id+"-tab":"typo3-tab-"+ ++o.idCounter),a&&(n.setAttribute("aria-controls",a.id),a.setAttribute("aria-labelledby",n.id),a.setAttribute("role","tabpanel"))}}static show(t){const e=t.closest('[role="tablist"]');if(!e)return;o.initialize(e);const s=o.getPanel(t);if(!s||t.classList.contains("active"))return;const n=s.closest(".tab-content"),r=e.querySelector(":scope .nav-link.active")??null,a=new c(r);if(t.dispatchEvent(a),!a.defaultPrevented){if(e)for(const i of e.querySelectorAll(".nav-link.active"))i.classList.remove("active"),i.setAttribute("aria-selected","false"),i.setAttribute("tabindex","-1");if(n)for(const i of n.querySelectorAll(":scope > .tab-pane.active"))i.classList.remove("active");t.classList.add("active"),t.setAttribute("aria-selected","true"),t.setAttribute("tabindex","0"),s.classList.add("active"),t.dispatchEvent(new u(r))}}static getTargetIdentifier(t){return t.dataset.typo3Tab?.replace("#","")||null}static getPanel(t){const e=o.getTargetIdentifier(t);return e?document.getElementById(e):null}static migrate(t){if(t.dataset.typo3Tab)return t;const e=t.dataset.bsTarget||(t instanceof HTMLAnchorElement?t.getAttribute("href"):"")||"";if(t instanceof HTMLAnchorElement&&!e.startsWith("#"))return t;if(t instanceof HTMLAnchorElement){const s=document.createElement("button");s.type="button";for(const n of t.attributes)["href","data-bs-toggle","data-bs-target"].includes(n.name)||s.setAttribute(n.name,n.value);return s.innerHTML=t.innerHTML,s.setAttribute("data-typo3-tab",e),t.replaceWith(s),s}return t.setAttribute("data-typo3-tab",e),t.removeAttribute("data-bs-toggle"),t.removeAttribute("data-bs-target"),t}}class B{constructor(t){const e=document.createElement("typo3-backend-tab-scroller");t.parentNode instanceof d||(t.parentNode.insertBefore(e,t),e.appendChild(t))}}let d=class extends v{constructor(){super(),this.navTabs=null,this.handleScroll=()=>{this.updateArrows()},this.addEventListener(u.eventName,t=>{t.target.closest(".nav-item")?.scrollIntoView({container:"nearest",behavior:"smooth",inline:"nearest",block:"nearest"})}),new ResizeObserver(()=>{this.scrollActiveLinkIntoView(),this.updateArrows()}).observe(this)}connectedCallback(){super.connectedCallback(),this.createButtons(),this.initializeTabs(),this.updateArrows()}render(){return m``}createButtons(){this.startButton=document.createElement("button"),this.startButton.slot="start-button",this.startButton.type="button",this.startButton.className="nav-tabs-scroll nav-tabs-scroll-start",this.startButton.hidden=!0,this.startButton.setAttribute("aria-hidden","true"),this.startButton.setAttribute("tabindex","-1"),this.startButton.innerHTML='',this.endButton=document.createElement("button"),this.endButton.slot="end-button",this.endButton.type="button",this.endButton.className="nav-tabs-scroll nav-tabs-scroll-end",this.endButton.hidden=!0,this.endButton.setAttribute("aria-hidden","true"),this.endButton.setAttribute("tabindex","-1"),this.endButton.innerHTML='',this.startButton.addEventListener("click",()=>{const{navTabs:t}=this;if(t===null)return;const e=getComputedStyle(t).direction==="rtl";t.scrollBy({left:(e?1:-1)*t.clientWidth*.75,behavior:"smooth"})}),this.endButton.addEventListener("click",()=>{const{navTabs:t}=this;if(t===null)return;const e=getComputedStyle(t).direction==="rtl";t.scrollBy({left:(e?-1:1)*t.clientWidth*.75,behavior:"smooth"})}),this.appendChild(this.startButton),this.appendChild(this.endButton)}initializeTabs(){const t=this.querySelector(".nav-tabs");if(t===null){this.navTabs?.removeEventListener("scroll",this.handleScroll),this.navTabs=null;return}this.navTabs!==t&&(t.addEventListener("scroll",this.handleScroll,{passive:!0}),this.scrollActiveLinkIntoView(),this.navTabs=t)}scrollActiveLinkIntoView(){const t=this.querySelector(".nav-link.active");t&&t.closest(".nav-item")?.scrollIntoView({container:"nearest",behavior:"instant",inline:"nearest",block:"nearest"})}updateArrows(){const{navTabs:t}=this;if(t===null)return;const{scrollLeft:e,scrollWidth:s,clientWidth:n}=t,r=s>n,a=getComputedStyle(t).direction==="rtl",i=a?e>=0:e<=0,f=a?e<=-(s-n):e>=s-n-1;this.startButton.hidden=!r||i,this.endButton.hidden=!r||f}};d=A([p("typo3-backend-tab-scroller")],d);class L{constructor(){b.ready().then(()=>{document.querySelectorAll("[data-store-last-tab]").forEach(t=>{this.restore(t),t.addEventListener(c.eventName,e=>{this.store(e.currentTarget,e.target)})})}),y.unsetByPrefix("tabs-")}restore(t){const e=h.get(t.id);if(e){const s=t.querySelector('[data-typo3-tab="#'+e+'"]');s&&o.show(s)}}store(t,e){const s=e.dataset.typo3Tab?.replace("#","")??"";h.set(t.id,s)}}new o,new L,b.ready().then(()=>{document.querySelectorAll(".nav-tabs").forEach(l=>new B(l))});export{o as Tab,d as TabScrollerElement,c as TabShowEvent,u as TabShownEvent}; diff --git a/Resources/Public/JavaScript/tabs.js b/Resources/Public/JavaScript/tabs.js new file mode 100644 index 0000000..9da2164 --- /dev/null +++ b/Resources/Public/JavaScript/tabs.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import"@typo3/backend/tabs.js";console.warn("Please use @typo3/backend/tab.js instead of @typo3/backend/tabs.js"); diff --git a/Resources/Public/JavaScript/telephone-link-handler.js b/Resources/Public/JavaScript/telephone-link-handler.js new file mode 100644 index 0000000..e9ccb0f --- /dev/null +++ b/Resources/Public/JavaScript/telephone-link-handler.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import n from"@typo3/backend/link-browser.js";import r from"@typo3/core/event/regular-event.js";class o{constructor(){new r("submit",(t,l)=>{t.preventDefault();let e=l.querySelector('[name="ltelephone"]').value;e!=="tel:"&&(e.startsWith("tel:")&&(e=e.substr(4)),n.finalizeFunction("tel:"+e))}).delegateTo(document,"#ltelephoneform")}}var i=new o;export{i as default}; diff --git a/Resources/Public/JavaScript/toolbar.js b/Resources/Public/JavaScript/toolbar.js new file mode 100644 index 0000000..28a3e0e --- /dev/null +++ b/Resources/Public/JavaScript/toolbar.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import i from"@typo3/core/document-service.js";import e from"@typo3/core/event/regular-event.js";import{ScaffoldState as n,ToolbarToggleRequestEvent as o,SearchToggleRequestEvent as a}from"@typo3/backend/viewport/scaffold-state.js";class t{static initialize(){n.initialize(),t.initializeEvents()}static initializeEvents(){new e("click",()=>{document.dispatchEvent(new o)}).bindTo(document.querySelector(".t3js-topbar-button-toolbar")),new e("click",()=>{document.dispatchEvent(new a)}).bindTo(document.querySelector(".t3js-topbar-button-search"))}}i.ready().then(t.initialize); diff --git a/Resources/Public/JavaScript/toolbar/clear-cache-menu.js b/Resources/Public/JavaScript/toolbar/clear-cache-menu.js new file mode 100644 index 0000000..6f4809c --- /dev/null +++ b/Resources/Public/JavaScript/toolbar/clear-cache-menu.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import u from"@typo3/core/ajax/ajax-request.js";import l from"@typo3/backend/icons.js";import s from"@typo3/backend/notification.js";import f from"@typo3/backend/viewport.js";import g from"@typo3/core/event/regular-event.js";import t from"~labels/core.cache";var e;(function(a){a.containerSelector="#typo3-cms-backend-backend-toolbaritems-clearcachetoolbaritem",a.menuItemSelector=".t3js-toolbar-cache-flush-action",a.toolbarIconSelector=".toolbar-item-icon .t3js-icon"})(e||(e={}));class p{constructor(){this.initializeEvents=()=>{const n=document.querySelector(e.containerSelector);new g("click",(o,r)=>{o.preventDefault(),r.dataset.endpoint&&this.clearCache(r.dataset.endpoint)}).delegateTo(n,e.menuItemSelector)},f.Topbar.Toolbar.registerEvent(this.initializeEvents)}clearCache(n){const o=document.querySelector(e.containerSelector);o.classList.remove("open");const r=o.querySelector(e.toolbarIconSelector),m=r.cloneNode(!0);l.getIcon("spinner-circle",l.sizes.small).then(i=>{r.replaceWith(document.createRange().createContextualFragment(i))}),new u(n).post({}).then(async i=>{const c=await i.resolve();c?.success===!1?s.error(c.title??t.get("notification.error.title"),c.message??t.get("notification.error.message")):s.success(c?.title??t.get("notification.success.title"),c?.message??t.get("notification.success.message"))},()=>{s.error(t.get("notification.error.title"),t.get("notification.error.message"))}).finally(()=>{o.querySelector(e.toolbarIconSelector).replaceWith(m)})}}var b=new p;export{b as default}; diff --git a/Resources/Public/JavaScript/toolbar/system-information-menu.js b/Resources/Public/JavaScript/toolbar/system-information-menu.js new file mode 100644 index 0000000..fa879ce --- /dev/null +++ b/Resources/Public/JavaScript/toolbar/system-information-menu.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import u from"@typo3/core/ajax/ajax-request.js";import g from"@typo3/core/event/regular-event.js";import d from"@typo3/backend/icons.js";import r from"@typo3/backend/storage/persistent.js";import c from"@typo3/backend/viewport.js";import"@typo3/backend/element/status-indicator-element.js";var a;(function(o){o.element="#typo3-cms-backend-backend-toolbaritems-systeminformationtoolbaritem",o.icon="#typo3-cms-backend-backend-toolbaritems-systeminformationtoolbaritem .toolbar-item-icon .t3js-icon",o.menu="#typo3-cms-backend-backend-toolbaritems-systeminformationtoolbaritem .dropdown-menu",o.data="[data-systeminformation-data]",o.badge="[data-systeminformation-badge]",o.message="[data-systeminformation-message-module]",o.messageLink="[data-systeminformation-message-module] a"})(a||(a={}));class n{constructor(){this.timer=null,this.updateMenu=()=>{const t=document.querySelector(a.icon),e=t.cloneNode(!0);this.timer!==null&&(clearTimeout(this.timer),this.timer=null),d.getIcon("spinner-circle",d.sizes.small).then(s=>{t.replaceWith(document.createRange().createContextualFragment(s))}),new u(TYPO3.settings.ajaxUrls.systeminformation_render).get().then(async s=>{document.querySelector(a.menu).innerHTML=await s.resolve(),n.updateBadge()}).finally(()=>{document.querySelector(a.icon).replaceWith(e),this.timer=setTimeout(this.updateMenu,1e3*300)})},new g("click",this.handleMessageLinkClick).delegateTo(document,a.messageLink),c.Topbar.Toolbar.registerEvent(this.updateMenu),document.addEventListener("typo3:system-information-menu:update",()=>this.updateMenu())}static getData(){const e=document.querySelector(a.data)?.dataset;return{count:e?.systeminformationDataCount?parseInt(e.systeminformationDataCount,10):0,severityBadgeClass:e?.systeminformationDataSeveritybadgeclass??""}}static getMessageDataFromElement(t){const e=t.dataset;return{count:e.systeminformationMessageCount?parseInt(e.systeminformationMessageCount,10):0,status:e.systeminformationMessageStatus??"",module:e.systeminformationMessageModule??"",params:e.systeminformationMessageParams??""}}static updateBadge(){const t=n.getData(),e=document.querySelector(a.badge);e.removeAttribute("class"),e.classList.add("toolbar-item-badge"),e.classList.add("badge"),e.classList.add("badge-pill"),t.severityBadgeClass!==""&&e.classList.add(t.severityBadgeClass),e.textContent=t.count.toString(),e.classList.toggle("hidden",!(t.count>0))}handleMessageLinkClick(t,e){const s=n.getMessageDataFromElement(e.closest(a.message));if(s.module==="")return;t.preventDefault(),t.stopPropagation();const m={},l=Math.floor(Date.now()/1e3);let i={};r.isset("systeminformation")&&(i=JSON.parse(r.get("systeminformation"))),m[s.module]={lastAccess:l},Object.assign(i,m),r.set("systeminformation",JSON.stringify(i)).then(()=>{TYPO3.ModuleMenu.App.showModule(s.module,s.params),c.Topbar.refresh()})}}var p=new n;export{p as default}; diff --git a/Resources/Public/JavaScript/tree/file-storage-browser.js b/Resources/Public/JavaScript/tree/file-storage-browser.js new file mode 100644 index 0000000..a74fe2f --- /dev/null +++ b/Resources/Public/JavaScript/tree/file-storage-browser.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as l,LitElement as m}from"lit";import{customElement as f,query as u}from"lit/decorators.js";import h from"@typo3/core/ajax/ajax-request.js";import"@typo3/backend/tree/tree-toolbar.js";import g from"@typo3/backend/element-browser.js";import b from"@typo3/backend/link-browser.js";import"@typo3/backend/element/icon-element.js";import{FileStorageTree as w}from"@typo3/backend/tree/file-storage-tree.js";var d=function(n,e,t,r){var o=arguments.length,i=o<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,t):r,s;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")i=Reflect.decorate(n,e,t,r);else for(var c=n.length-1;c>=0;c--)(s=n[c])&&(i=(o<3?s(i):o>3?s(e,t,i):s(e,t))||i);return o>3&&i&&Object.defineProperty(e,t,i),i};let p=class extends w{createNodeContentAction(e){return this.settings.actions.includes("link")?l`this.linkItem(e)}> `:this.settings.actions.includes("select")?l`this.selectItem(e)}> `:super.createNodeContentAction(e)}linkItem(e){b.finalizeFunction("t3://folder?storage="+e.storage+"&identifier="+e.pathIdentifier)}selectItem(e){g.insertElement(e.recordType,e.identifier,e.name,e.identifier,!0)}};p=d([f("typo3-backend-component-filestorage-browser-tree")],p);let a=class extends m{constructor(){super(...arguments),this.activeFolder="",this.actions=[],this.selectActiveNode=e=>{const t=e.detail.nodes;e.detail.nodes=t.map(r=>(decodeURIComponent(r.identifier)===this.activeFolder&&(r.checked=!0),r))},this.loadFolderDetails=e=>{const t=e.detail.node;if(!t.checked)return;const r=document.location.href+"&contentOnly=1&expandFolder="+t.identifier;new h(r).get().then(o=>o.resolve()).then(o=>{const i=document.querySelector(".element-browser-main-content .element-browser-body");i.innerHTML=o})}}firstUpdated(){this.activeFolder=this.getAttribute("active-folder")||""}createRenderRoot(){return this}render(){this.hasAttribute("tree-actions")&&this.getAttribute("tree-actions").length&&(this.actions=JSON.parse(this.getAttribute("tree-actions")));const e={dataUrl:top.TYPO3.settings.ajaxUrls.filestorage_tree_data,rootlineUrl:top.TYPO3.settings.ajaxUrls.filestorage_tree_rootline,filterUrl:top.TYPO3.settings.ajaxUrls.filestorage_tree_filter,showIcons:!0,actions:this.actions},t=()=>{const r=this.querySelector("typo3-backend-tree-toolbar");r.tree=this.tree,this.activeFolder&&this.expandToActiveFolder()};return l``}async expandToActiveFolder(){if(!this.activeFolder||!this.tree.settings.rootlineUrl)return;const e=this.tree.nodes.find(t=>decodeURIComponent(t.identifier)===this.activeFolder);if(e){await this.tree.expandNodeParents(e);return}try{const t=new URL(this.tree.settings.rootlineUrl,window.location.origin);t.searchParams.set("identifier",this.activeFolder);const r=await new h(t.toString()).get({cache:"no-cache"}),{rootline:o}=await r.resolve();o&&o.length>0&&(o.pop(),await this.tree.expandParents(o.map(i=>encodeURIComponent(i))))}catch(t){console.debug("Could not expand to active folder:",t)}}};d([u("typo3-backend-component-filestorage-browser-tree")],a.prototype,"tree",void 0),a=d([f("typo3-backend-component-filestorage-browser")],a);export{a as FileStorageBrowser,p as FileStorageBrowserTree}; diff --git a/Resources/Public/JavaScript/tree/file-storage-tree-container.js b/Resources/Public/JavaScript/tree/file-storage-tree-container.js new file mode 100644 index 0000000..85dedd1 --- /dev/null +++ b/Resources/Public/JavaScript/tree/file-storage-tree-container.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as v,html as E}from"lit";import{customElement as N,query as T}from"lit/decorators.js";import"@typo3/backend/element/icon-element.js";import{SeverityEnum as C}from"@typo3/backend/enum/severity.js";import"@typo3/backend/tree/tree-toolbar.js";import{TreeNodePositionEnum as d}from"@typo3/backend/tree/tree-node.js";import{FileStorageTree as R}from"@typo3/backend/tree/file-storage-tree.js";import{TreeModuleState as I}from"@typo3/backend/tree/tree-module-state.js";import b from"@typo3/backend/context-menu.js";import y from"@typo3/backend/notification.js";import{ModuleStateStorage as F}from"@typo3/backend/storage/module-state-storage.js";import{ModuleUtility as O}from"@typo3/backend/module.js";import{FileListDragDropEvent as U}from"@typo3/filelist/file-list-dragdrop.js";import{Resource as S}from"@typo3/backend/resource/resource.js";import{DataTransferTypes as p}from"@typo3/backend/enum/data-transfer-types.js";import{UrlFactory as w}from"@typo3/core/factory/url-factory.js";import A from"~labels/core.core";import m from"~labels/backend.layout";var h=function(l,e,t,r){var o=arguments.length,n=o<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,t):r,i;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(l,e,t,r);else for(var s=l.length-1;s>=0;s--)(i=l[s])&&(n=(o<3?i(n):o>3?i(e,t,n):i(e,t))||n);return o>3&&n&&Object.defineProperty(e,t,n),n};const M="typo3-backend-navigation-component-filestoragetree";let g=class extends R{constructor(){super(...arguments),this.allowNodeDrag=!0}handleNodeMove(e,t,r){if(!this.isDropAllowed(t,e))return;const o=this.getDropCommandDetails(t,e,r);if(o===null)return;const n=f.fromNodePositionOptions(o),i=n.getConflictingOperationsForTreeNode(o.target);if(i.length>0){i.forEach(s=>{y.showMessage(m.get("drop.conflict"),m.get("mess.drop.conflict",[s.resource.name,decodeURIComponent(o.target.identifier)]),C.error)});return}this.initiateDropAction(n)}createDataTransferItemsFromNode(e){return[{type:p.treenode,data:this.getNodeTreeIdentifier(e)},{type:p.falResources,data:JSON.stringify([c.fromTreeNode(e)])}]}handleNodeDragOver(e){if(super.handleNodeDragOver(e))return!0;if(e.dataTransfer.types.includes(p.falResources)){const t=this.getNodeFromDragEvent(e);return t===null?!1:(this.cleanDrag(),this.getElementFromNode(t).classList.add("node-hover"),t.hasChildren&&!t.__expanded?this.openNodeTimeout.targetNode!=t&&(this.openNodeTimeout.targetNode=t,clearTimeout(this.openNodeTimeout.timeout),this.openNodeTimeout.timeout=setTimeout(()=>{this.showChildren(this.openNodeTimeout.targetNode),this.openNodeTimeout.targetNode=null,this.openNodeTimeout.timeout=null},1e3)):(clearTimeout(this.openNodeTimeout.timeout),this.openNodeTimeout.targetNode=null,this.openNodeTimeout.timeout=null),e.preventDefault(),!0)}return!1}getTooltipDescription(e){return decodeURIComponent(e.identifier)}handleNodeDrop(e){if(super.handleNodeDrop(e))return!0;if(e.dataTransfer.types.includes(p.falResources)){const t=this.getNodeFromDragEvent(e);if(t===null)return!1;if(t){const r=c.fromTreeNode(t),o=f.fromDataTransfer(e.dataTransfer,r),n=o.getConflictingOperationsForTreeNode(t);return n.length>0?(n.forEach(i=>{y.showMessage(m.get("drop.conflict"),m.get("mess.drop.conflict",[i.resource.name,decodeURIComponent(t.identifier)]),C.error)}),!1):(e.preventDefault(),this.initiateDropAction(o),!0)}}return!1}getDropCommandDetails(e,t,r){const o=this.nodes,n=t.identifier;let i=e;if(n===i.identifier)return null;if(r===d.BEFORE){const s=o.indexOf(e),a=this.setNodePositionAndTarget(s);if(a===null)return null;r=a.position,i=a.target}return{node:t,identifier:n,target:i,position:r}}setNodePositionAndTarget(e){const t=this.nodes,o=t[e].depth;e>0&&e--;const i=t[e].depth,s=this.nodes[e];if(i===o)return{position:d.AFTER,target:s};if(i=0;a--){if(t[a].depth===o)return{position:d.AFTER,target:this.nodes[a]};if(t[a].depth{this.tree.refreshOrFilterTree()},this.selectFirstNode=()=>{const e=this.tree.nodes[0];e&&this.tree.selectNode(e,!0)},this.loadContent=e=>{const t=e.detail.node;if(!t?.checked||(F.updateWithTreeIdentifier("media",decodeURIComponent(t.identifier),decodeURIComponent(t.__treeIdentifier)),e.detail.propagate===!1))return;const r=top.TYPO3.ModuleMenu.App,o=w.createUrl(O.getFromName(r.getCurrentModule()).link,{id:decodeURIComponent(t.identifier)});top.TYPO3.Backend.ContentContainer.setUrl(o)},this.showContextMenu=e=>{const t=e.detail.node;t&&b.show(t.recordType,decodeURIComponent(t.identifier),"tree","","",this.tree.getElementFromNode(t),e.detail.originalEvent)}}connectedCallback(){super.connectedCallback(),document.addEventListener("typo3:filestoragetree:refresh",this.refresh),document.addEventListener("typo3:filestoragetree:selectFirstNode",this.selectFirstNode)}disconnectedCallback(){document.removeEventListener("typo3:filestoragetree:refresh",this.refresh),document.removeEventListener("typo3:filestoragetree:selectFirstNode",this.selectFirstNode),super.disconnectedCallback()}createRenderRoot(){return this}render(){const e={dataUrl:top.TYPO3.settings.ajaxUrls.filestorage_tree_data,rootlineUrl:top.TYPO3.settings.ajaxUrls.filestorage_tree_rootline,filterUrl:top.TYPO3.settings.ajaxUrls.filestorage_tree_filter,showIcons:!0,searchPlaceholder:A.get("tree.searchFolderTree")};return E``}firstUpdated(){this.toolbar.tree=this.tree}transformModuleStateIdentifierToNodeIdentifier(e){return encodeURIComponent(e)}transformNodeIdentifierToModuleStateIdentifier(e){return decodeURIComponent(e)}};h([T("typo3-backend-navigation-component-filestorage-tree")],u.prototype,"tree",void 0),h([T("typo3-backend-tree-toolbar")],u.prototype,"toolbar",void 0),u=h([N("typo3-backend-navigation-component-filestoragetree")],u);class D{constructor(e,t=d.INSIDE){this.resource=e,this.position=t}hasConflictWithTreeNode(e){return this.resource.type==="folder"&&(e.identifier===this.resource.identifier||e.__parents[0]==this.resource.identifier||e.__parents.includes(this.resource.identifier))}}class c extends S{static fromTreeNode(e){return new c(decodeURIComponent(e.resourceType),decodeURIComponent(e.identifier),decodeURIComponent(e.name))}}class f{constructor(e,t){this.operations=e,this.target=t}static fromDataTransfer(e,t){return f.fromArray(JSON.parse(e.getData(p.falResources)),t)}static fromArray(e,t){const r=[];for(const o of e)r.push(new D(o,d.INSIDE));return new f(r,t)}static fromNodePositionOptions(e){const t=c.fromTreeNode(e.node),r=c.fromTreeNode(e.target),o=[new D(t,e.position)];return new f(o,r)}getConflictingOperationsForTreeNode(e){return this.operations.filter(t=>t.hasConflictWithTreeNode(e))}getResources(){const e=[];return this.operations.forEach(t=>{e.push(t.resource)}),e}}export{g as EditableFileStorageTree,u as FileStorageTreeNavigationComponent,M as navigationComponentName}; diff --git a/Resources/Public/JavaScript/tree/file-storage-tree.js b/Resources/Public/JavaScript/tree/file-storage-tree.js new file mode 100644 index 0000000..b831fb5 --- /dev/null +++ b/Resources/Public/JavaScript/tree/file-storage-tree.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{Tree as o}from"@typo3/backend/tree/tree.js";class i extends o{constructor(){super(),this.settings.defaultProperties={hasChildren:!1,nameSourceField:"title",type:"sys_file",prefix:"",suffix:"",locked:!1,loaded:!1,overlayIcon:"",selectable:!0,expanded:!1,checked:!1}}getNodeTitle(e){let t=decodeURIComponent(e.name);const s=this.getNodeLabels(e);s.length&&(t+="; "+s.map(l=>l.label).join("; "));const a=this.getNodeStatusInformation(e);return a.length&&(t+="; "+a.map(l=>l.label).join("; ")),t}}export{i as FileStorageTree}; diff --git a/Resources/Public/JavaScript/tree/page-browser.js b/Resources/Public/JavaScript/tree/page-browser.js new file mode 100644 index 0000000..a09ce36 --- /dev/null +++ b/Resources/Public/JavaScript/tree/page-browser.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as s,LitElement as m,nothing as g}from"lit";import{customElement as h,property as f,query as P}from"lit/decorators.js";import{until as y}from"lit/directives/until.js";import{PageTree as b}from"@typo3/backend/tree/page-tree.js";import p from"@typo3/core/ajax/ajax-request.js";import"@typo3/backend/tree/tree-toolbar.js";import k from"@typo3/backend/element-browser.js";import v from"@typo3/backend/link-browser.js";import"@typo3/backend/element/icon-element.js";import w from"@typo3/backend/storage/persistent.js";import T from"~labels/core.core";var l=function(r,e,t,n){var i=arguments.length,o=i<3?e:n===null?n=Object.getOwnPropertyDescriptor(e,t):n,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")o=Reflect.decorate(r,e,t,n);else for(var d=r.length-1;d>=0;d--)(c=r[d])&&(o=(i<3?c(o):i>3?c(e,t,o):c(e,t))||o);return i>3&&o&&Object.defineProperty(e,t,o),o};let u=class extends b{getNodeClasses(e){const t=super.getNodeClasses(e);return this.settings.actions.includes("link")&&(this.isLinkable(e)||t.push("node-disabled")),t}createNodeContentAction(e){return this.settings.actions.includes("link")?this.isLinkable(e)?s`this.linkItem(e)}> `:super.createNodeContentAction(e):this.settings.actions.includes("select")?s`this.selectItem(e)}> `:super.createNodeContentAction(e)}linkItem(e){v.finalizeFunction("t3://page?uid="+e.identifier)}isLinkable(e){return!(this.settings.nonViewableDoktypes??[199,254]).includes(e.doktype)}selectItem(e){k.insertElement(e.recordType,e.identifier,e.name,"",!0)}};u=l([h("typo3-backend-component-page-browser-tree")],u);let a=class extends m{constructor(){super(...arguments),this.mountPointPath=null,this.activePageId=0,this.actions=[],this.configuration=null,this.selectActivePageInTree=e=>{const t=e.detail.nodes;e.detail.nodes=t.map(n=>(parseInt(n.identifier,10)===this.activePageId&&(n.checked=!0),n))},this.loadRecordsOfPage=e=>{const t=e.detail.node;if(!t.checked)return;const n=new URL(document.location.href,window.location.origin);n.searchParams.set("contentOnly","1"),n.searchParams.set("expandPage",t.identifier),new p(n).get().then(i=>i.resolve()).then(i=>{const o=document.querySelector(".element-browser-main-content .element-browser-body");o.innerHTML=i})},this.setMountPoint=e=>{this.setTemporaryMountPoint(e.detail.pageId)}}connectedCallback(){super.connectedCallback(),document.addEventListener("typo3:pagetree:mountPoint",this.setMountPoint)}disconnectedCallback(){document.removeEventListener("typo3:pagetree:mountPoint",this.setMountPoint),super.disconnectedCallback()}firstUpdated(){this.activePageId=parseInt(this.getAttribute("active-page"),10),this.actions=JSON.parse(this.getAttribute("tree-actions")??"[]")}createRenderRoot(){return this}getConfiguration(){if(this.configuration!==null)return Promise.resolve(this.configuration);const e=top.TYPO3.settings.ajaxUrls.page_tree_browser_configuration,t=this.hasAttribute("alternative-entry-points")?JSON.parse(this.getAttribute("alternative-entry-points")):[];let n=new p(e);return t.length&&(n=n.withQueryArguments("alternativeEntryPoints="+encodeURIComponent(t))),n.get().then(async i=>{const o=await i.resolve("json");return o.actions=this.actions,this.configuration=o,this.mountPointPath=o.temporaryMountPoint||null,o})}render(){return s`${y(this.renderTree(),"")}`}renderTree(){return this.getConfiguration().then(e=>{const t=()=>{this.activePageId&&this.tree.ensureActiveNodeLoaded(this.activePageId);const n=this.querySelector("typo3-backend-tree-toolbar");n.tree=this.tree};return s`${this.renderMountPoint()}`})}unsetTemporaryMountPoint(){w.unset("pageTree_temporaryMountPoint").then(()=>{this.mountPointPath=null})}renderMountPoint(){return this.mountPointPath===null?g:s`
    ${this.mountPointPath}
    this.unsetTemporaryMountPoint()} title=${T.get("labels.temporaryPageTreeEntryPoints")}>
    `}setTemporaryMountPoint(e){new p(this.configuration.setTemporaryMountPointUrl).post("pid="+e,{headers:{"Content-Type":"application/x-www-form-urlencoded","X-Requested-With":"XMLHttpRequest"}}).then(t=>t.resolve()).then(t=>{t&&t.hasErrors?(this.tree.errorNotification(t.message),this.tree.loadData()):this.mountPointPath=t.mountPointPath}).catch(t=>{this.tree.errorNotification(t),this.tree.loadData()})}};l([f({type:String})],a.prototype,"mountPointPath",void 0),l([P("typo3-backend-component-page-browser-tree")],a.prototype,"tree",void 0),a=l([h("typo3-backend-component-page-browser")],a);export{a as PageBrowser,u as PageBrowserTree}; diff --git a/Resources/Public/JavaScript/tree/page-position-select.js b/Resources/Public/JavaScript/tree/page-position-select.js new file mode 100644 index 0000000..6bd3aab --- /dev/null +++ b/Resources/Public/JavaScript/tree/page-position-select.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as b,html as v,nothing as I}from"lit";import{customElement as y,property as u,query as w,state as P}from"lit/decorators.js";import{PageTree as N}from"@typo3/backend/tree/page-tree.js";import A from"@typo3/core/ajax/ajax-request.js";import"@typo3/backend/tree/tree-toolbar.js";import"@typo3/backend/element/icon-element.js";import"@typo3/backend/element/breadcrumb.js";import{TreeNodePositionEnum as g}from"@typo3/backend/tree/tree-node.js";import{cache as S}from"lit/directives/cache.js";import s from"~labels/core.core";var d=function(l,e,t,i){var r=arguments.length,n=r<3?e:i===null?i=Object.getOwnPropertyDescriptor(e,t):i,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(l,e,t,i);else for(var o=l.length-1;o>=0;o--)(c=l[o])&&(n=(r<3?c(n):r>3?c(e,t,n):c(e,t))||n);return r>3&&n&&Object.defineProperty(e,t,n),n};const _=[{label:s.get("insert_inside"),value:"inside",iconIdentifier:"actions-arrow-end"},{label:s.get("insert_after"),value:"after",iconIdentifier:"actions-arrow-down"}];class p extends CustomEvent{static{this.eventName="typo3:page-position-select-tree:insert-position-change"}constructor(e,t){super(p.eventName,{detail:{pageUid:e,position:t},bubbles:!0,composed:!0})}}class f extends CustomEvent{static{this.eventName="typo3:page-position-select-tree:insert-position-confirm"}constructor(e,t){super(f.eventName,{detail:{pageUid:e,position:t},bubbles:!0,composed:!0})}}let m=class extends N{async handleNodeAdd(e,t,i){this.requestUpdate()}};m=d([y("typo3-backend-component-page-position-select-tree")],m);let a=class extends b{constructor(){super(...arguments),this.activePageId=null,this.actions=["select"],this.insertPosition="inside",this.configuration=null,this.breadcrumbItems=[]}connectedCallback(){super.connectedCallback(),this.loadConfiguration()}createRenderRoot(){return this}loadConfiguration(){const e=top.TYPO3.settings.ajaxUrls.page_tree_browser_configuration,t=this.hasAttribute("alternative-entry-points")?JSON.parse(this.getAttribute("alternative-entry-points")):[];let i=new A(e);t.length&&(i=i.withQueryArguments("alternativeEntryPoints="+encodeURIComponent(t))),i.get().then(async r=>{const n=await r.resolve("json");n.actions=this.actions,this.configuration=n})}render(){return v`${this.configuration?S(this.renderTree()):I}`}renderTree(){const e=async()=>{const t=this.querySelector("typo3-backend-tree-toolbar");t.tree=this.tree,await this.tree.ensureActiveNodeLoaded(this.activePageId);const i=await this.applyActiveInsertPosition();i&&(await this.tree.updateComplete,this.tree.scrollNodeIntoViewIfNeeded(i),this.tree.focusNode(i))};return v`this.applyActiveInsertPosition()} @typo3:tree:filter-reset=${()=>this.applyActiveInsertPosition()}>`}async handleNodeSelected(e){const t=e.detail.node;if(this.isActivePagePositionNode(t)){this.dispatchEvent(new f(this.activePageId,this.insertPosition));return}const[i,r]=t.identifier.split("-");this.insertPosition=r??"inside",this.activePageId=Number(i);const n=/^\d+$/.test(t.identifier)?await this.applyActiveInsertPosition():this.syncActiveInsertPositionState();n&&this.tree.focusNode(n),this.dispatchEvent(new p(this.activePageId,this.insertPosition))}async applyActiveInsertPosition(){if(this.activePageId===null||this.activePageId===void 0)return null;let e=this.tree.nodes.find(t=>t.identifier===String(this.activePageId));return!e&&(await this.tree.ensureActiveNodeLoaded(this.activePageId),e=this.tree.nodes.find(t=>t.identifier===String(this.activePageId)),!e)?null:(await this.tree.expandNodeParents(e),await this.toggleDynamicInsertNodes(e),this.syncActiveInsertPositionState())}syncActiveInsertPositionState(){let e=null;return this.tree.nodes.forEach(t=>{t.checked=this.isActivePagePositionNode(t),t.checked&&(e=t)}),this.updateBreadcrumb(this.tree.nodes),this.tree.requestUpdate(),e}async toggleDynamicInsertNodes(e){const t=[];for(const o of this.tree.nodes)["-after","-inside"].some(h=>o.identifier.endsWith(h))&&t.push(o);for(const o of t)await this.tree.removeNode(o);const i={...e,__processed:!1,hasChildren:!1,loaded:!0,identifier:e.identifier+"-inside",parentIdentifier:e.identifier,name:s.get("insert_subpage"),icon:"actions-arrow-end",overlayIcon:"",tooltip:s.get("insert_inside")+" "+e.tooltip},n={...this.tree.nodes.find(o=>o.identifier===e.parentIdentifier),__processed:!1,hasChildren:!1,loaded:!0,identifier:`${e.identifier}-after`,parentIdentifier:e.parentIdentifier,name:s.get("insert_page"),icon:"actions-arrow-end",overlayIcon:"",tooltip:s.get("insert_after")+" "+e.tooltip};let c=[];e.hasChildren&&(c=this.tree.nodes.filter(o=>o.parentIdentifier===e.identifier)),await this.tree.addNode(i,e,g.INSIDE),e.identifier!=="0"&&await this.tree.addNode(n,e,g.AFTER);for(const o of c){const h={...o,__processed:!1,hasChildren:!1,loaded:!0,identifier:`${o.identifier}-after`,parentIdentifier:o.parentIdentifier,name:s.get("insert_page"),icon:"actions-arrow-end",overlayIcon:"",tooltip:s.get("insert_after")+" "+o.tooltip};await this.tree.addNode(h,o,g.AFTER)}}isActivePagePositionNode(e){return e.identifier===`${this.activePageId}-${this.insertPosition}`}updateBreadcrumb(e){let t=e.find(r=>r.identifier===String(this.activePageId));const i=[];for(;t;)i.push({identifier:t.identifier,label:t.name,icon:t.icon,iconOverlay:t.overlayIcon,url:null,forceShowIcon:!1}),t=e.find(r=>r.identifier===t.parentIdentifier);this.insertPosition==="after"&&i.shift(),i.unshift({identifier:`${this.activePageId}-${this.insertPosition}`,label:s.get("labels.createNew"),icon:"apps-pagetree-page-default",iconOverlay:"overlay-new",url:null,forceShowIcon:!1}),this.breadcrumbItems=i.reverse()}};d([u({type:Number,reflect:!0,attribute:"active-page"})],a.prototype,"activePageId",void 0),d([u({type:Array})],a.prototype,"actions",void 0),d([u({type:String,reflect:!0})],a.prototype,"insertPosition",void 0),d([w("typo3-backend-component-page-position-select-tree")],a.prototype,"tree",void 0),d([P()],a.prototype,"configuration",void 0),d([P()],a.prototype,"breadcrumbItems",void 0),a=d([y("typo3-backend-component-page-position-select")],a);export{p as InsertPositionChangeEvent,f as InsertPositionConfirmEvent,a as PagePositionSelect,m as PagePositionSelectTree,_ as insertPositionOptions}; diff --git a/Resources/Public/JavaScript/tree/page-tree-element.js b/Resources/Public/JavaScript/tree/page-tree-element.js new file mode 100644 index 0000000..697f7ad --- /dev/null +++ b/Resources/Public/JavaScript/tree/page-tree-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as M,html as a,nothing as g}from"lit";import{customElement as v,property as b,query as T,state as E}from"lit/decorators.js";import{until as S}from"lit/directives/until.js";import w from"@typo3/core/ajax/ajax-request.js";import P from"@typo3/backend/storage/persistent.js";import{ModuleUtility as $}from"@typo3/backend/module.js";import x from"@typo3/backend/context-menu.js";import{PageTree as F}from"@typo3/backend/tree/page-tree.js";import{TreeNodePositionEnum as u,TreeNodeCommandEnum as d}from"@typo3/backend/tree/tree-node.js";import{TreeToolbar as U}from"@typo3/backend/tree/tree-toolbar.js";import{TreeModuleState as D}from"@typo3/backend/tree/tree-module-state.js";import _ from"@typo3/backend/modal.js";import I from"@typo3/backend/severity.js";import{UrlFactory as z}from"@typo3/core/factory/url-factory.js";import{ModuleStateStorage as C}from"@typo3/backend/storage/module-state-storage.js";import{DataTransferTypes as f}from"@typo3/backend/enum/data-transfer-types.js";import"@typo3/backend/viewport/content-navigation-toggle.js";import"bootstrap";import r from"~labels/core.core";import O from"~labels/core.common";import R from"~labels/core.mod_web_list";import L from"~labels/backend.pages_new";import{openPageWizardModal as A}from"@typo3/backend/page-wizard/helper/wizard-helper.js";var s=function(p,e,t,n){var o=arguments.length,i=o<3?e:n===null?n=Object.getOwnPropertyDescriptor(e,t):n,l;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")i=Reflect.decorate(p,e,t,n);else for(var m=p.length-1;m>=0;m--)(l=p[m])&&(i=(o<3?l(i):o>3?l(e,t,i):l(e,t))||i);return o>3&&i&&Object.defineProperty(e,t,i),i};const B="typo3-backend-navigation-component-pagetree";let y=class extends F{constructor(){super(...arguments),this.allowNodeEdit=!0,this.allowNodeDrag=!0,this.allowNodeSorting=!0}sendChangeCommand(e){let t="",n="0";if(e.target)if(n=e.target.identifier,e.position===u.BEFORE){const o=this.getPreviousNode(e.target);n=(o.depth===e.target.depth?"-":"")+o.identifier}else e.position===u.AFTER&&(n="-"+n);if(e.command===d.NEW){const o=e;t="&data[pages]["+e.node.identifier+"][pid]="+encodeURIComponent(n)+"&data[pages]["+e.node.identifier+"][title]="+encodeURIComponent(o.title)+"&data[pages]["+e.node.identifier+"][doktype]="+encodeURIComponent(o.doktype)}else if(e.command===d.EDIT)t="&data[pages]["+e.node.identifier+"][title]="+encodeURIComponent(e.title);else if(e.command===d.DELETE){const o=C.current("web");e.node.identifier===o.identifier&&this.selectFirstNode(),t="&cmd[pages]["+e.node.identifier+"][delete]=1"}else t="cmd[pages]["+e.node.identifier+"]["+e.command+"]="+n;this.requestTreeUpdate(t).then(o=>{if(o&&o.hasErrors)this.errorNotification(o.messages);else if(e.command===d.NEW){const i=this.getParentNode(e.node);i.loaded=!1,this.loadChildren(i)}else this.refreshOrFilterTree()})}initializeDragForNode(){throw new Error("unused")}async handleNodeEdit(e,t){if(e.__loading=!0,e.identifier.startsWith("NEW")){const n=this.getPreviousNode(e),o=e.depth===n.depth?u.AFTER:u.INSIDE,i={command:d.NEW,node:e,title:t,position:o,target:n,doktype:e.doktype};await this.sendChangeCommand(i)}else{const n={command:d.EDIT,node:e,title:t};await this.sendChangeCommand(n)}e.__loading=!1}createDataTransferItemsFromNode(e){return[{type:f.treenode,data:this.getNodeTreeIdentifier(e)},{type:f.pages,data:JSON.stringify({records:[{identifier:e.identifier,tablename:"pages"}]})}]}async handleNodeAdd(e,t,n){this.updateComplete.then(()=>{this.editNode(e)})}handleNodeDelete(e){const t={node:e,command:d.DELETE};this.settings.displayDeleteConfirmation?_.confirm(r.get("mess.delete.title"),r.get("mess.delete",[t.node.name]),I.warning,[{text:r.get("labels.cancel"),active:!0,btnClass:"btn-default",name:"cancel"},{text:O.get("delete"),btnClass:"btn-warning",name:"delete"}]).addEventListener("button.clicked",o=>{o.target.name==="delete"&&this.sendChangeCommand(t),_.dismiss()}):this.sendChangeCommand(t)}handleNodeMove(e,t,n){const o={node:e,target:t,position:n,command:d.MOVE};let i="";const l=[e.name,t.name];switch(n){case u.BEFORE:i=r.get("mess.move_before",l);break;case u.AFTER:i=r.get("mess.move_after",l);break;default:i=r.get("mess.move_into",l);break}const m=_.confirm(R.get("move_page"),i,I.warning,[{text:r.get("labels.cancel"),active:!0,btnClass:"btn-default",name:"cancel"},{text:r.get("cm.copy"),btnClass:"btn-warning",name:"copy"},{text:r.get("labels.move"),btnClass:"btn-warning",name:"move"}]);m.addEventListener("button.clicked",N=>{const k=N.target;k.name==="move"?(o.command=d.MOVE,this.sendChangeCommand(o)):k.name==="copy"&&(o.command=d.COPY,this.sendChangeCommand(o)),m.hideModal()})}requestTreeUpdate(e){return new w(top.TYPO3.settings.ajaxUrls.record_process).post(e,{headers:{"Content-Type":"application/x-www-form-urlencoded","X-Requested-With":"XMLHttpRequest"}}).then(t=>t.resolve()).catch(t=>{this.errorNotification(t),this.loadData()})}};y=s([v("typo3-backend-navigation-component-pagetree-tree")],y);let h=class extends D(M){constructor(){super(...arguments),this.mountPointPath=null,this.moduleStateType="web",this.configuration=null,this.refresh=()=>{this.tree.refreshOrFilterTree()},this.setMountPoint=e=>{this.setTemporaryMountPoint(e.detail.pageId)},this.selectFirstNode=()=>{this.tree.selectFirstNode()},this.loadContent=e=>{const t=e.detail.node;if(!t?.checked||(C.updateWithTreeIdentifier("web",t.identifier,t.__treeIdentifier),e.detail.propagate===!1))return;const n=top.TYPO3.ModuleMenu.App,o=z.createUrl($.getFromName(n.getCurrentModule()).link,{id:t.identifier});top.TYPO3.Backend.ContentContainer.setUrl(o)},this.showContextMenu=e=>{const t=e.detail.node;t&&x.show(t.recordType,t.identifier,"tree","","",this.tree.getElementFromNode(t),e.detail.originalEvent)}}connectedCallback(){super.connectedCallback(),document.addEventListener("typo3:pagetree:refresh",this.refresh),document.addEventListener("typo3:pagetree:mountPoint",this.setMountPoint),document.addEventListener("typo3:pagetree:selectFirstNode",this.selectFirstNode)}disconnectedCallback(){document.removeEventListener("typo3:pagetree:refresh",this.refresh),document.removeEventListener("typo3:pagetree:mountPoint",this.setMountPoint),document.removeEventListener("typo3:pagetree:selectFirstNode",this.selectFirstNode),super.disconnectedCallback()}createRenderRoot(){return this}render(){return a`${S(this.renderTree(),"")}`}getConfiguration(){if(this.configuration!==null)return Promise.resolve(this.configuration);const e=top.TYPO3.settings.ajaxUrls.page_tree_configuration;return new w(e).get().then(async t=>{const n=await t.resolve("json");return this.configuration=n,this.mountPointPath=n.temporaryMountPoint||null,n})}async renderTree(){const e=await this.getConfiguration();return a`${this.renderMountPoint()}{this.toolbar.tree=this.tree,this.fetchActiveNodeIfMissing()}} @typo3:tree:node-selected=${this.loadContent} @typo3:tree:node-context=${this.showContextMenu} @typo3:tree:nodes-prepared=${this.selectActiveNodeInLoadedNodes}>`}unsetTemporaryMountPoint(){P.unset("pageTree_temporaryMountPoint").then(()=>{this.mountPointPath=null})}renderMountPoint(){return this.mountPointPath===null?g:a`
    ${this.mountPointPath}
    this.unsetTemporaryMountPoint()} title=${r.get("labels.temporaryPageTreeEntryPoints")}>
    `}setTemporaryMountPoint(e){new w(this.configuration.setTemporaryMountPointUrl).post("pid="+e,{headers:{"Content-Type":"application/x-www-form-urlencoded","X-Requested-With":"XMLHttpRequest"}}).then(t=>t.resolve()).then(t=>{t&&t.hasErrors?(this.tree.errorNotification(t.message),this.tree.loadData()):this.mountPointPath=t.mountPointPath}).catch(t=>{this.tree.errorNotification(t),this.tree.loadData()})}};s([b({type:String})],h.prototype,"mountPointPath",void 0),s([T("typo3-backend-navigation-component-pagetree-tree")],h.prototype,"tree",void 0),s([T("typo3-backend-navigation-component-pagetree-toolbar")],h.prototype,"toolbar",void 0),h=s([v("typo3-backend-navigation-component-pagetree")],h);let c=class extends U{constructor(){super(...arguments),this.tree=null,this.searchInTranslatedPages=!1,this.searchByFrontendUri=!1,this.subMenuItemsExpanded=!1,this.handleResize=this.checkHiddenSubmenuItems.bind(this)}disconnectedCallback(){super.disconnectedCallback(),this.resizeObserver.disconnect(),window.removeEventListener("resize",this.handleResize)}firstUpdated(){super.firstUpdated(),this.resizeObserver=new ResizeObserver(()=>this.checkHiddenSubmenuItems()),this.resizeObserver.observe(this.submenuItemsContainer),window.addEventListener("resize",this.handleResize),this.checkHiddenSubmenuItems()}updated(e){super.updated(e),e.has("tree")&&(this.tree?.settings?.searchInTranslatedPagesEnabled!==void 0&&(this.searchInTranslatedPages=this.tree.settings.searchInTranslatedPagesEnabled),this.tree?.settings?.searchByFrontendUriEnabled!==void 0&&(this.searchByFrontendUri=this.tree.settings.searchByFrontendUriEnabled),this.checkHiddenSubmenuItems())}render(){return a`
    ${this.renderToolbarSubmenu()}
    `}renderToolbarSubmenu(){const e=[];return this.tree?.settings?.doktypes?.length&&(e.push(a``),e.push(this.tree.settings.doktypes.map(t=>a``))),a`
    ${e}
    ${this.hasHiddenSubMenuItems?a``:g}
    `}renderSearchOptions(){const e=this.tree?.settings?.searchInTranslatedPagesAvailable,t=this.tree?.settings?.searchByFrontendUriAvailable;return!e&&!t?g:a`
  • ${e?a`
  • `:g} ${t?a`
  • `:g}`}async toggleTranslationSearch(){const e=!this.searchInTranslatedPages;try{await P.set("pageTree_searchInTranslatedPages",e?"1":"0"),this.searchInTranslatedPages=e,this.tree?.settings&&(this.tree.settings.searchInTranslatedPagesEnabled=e);const t=this.querySelector(".search-input");t&&t.value.trim()!==""&&this.refreshTree()}catch(t){console.error("Failed to toggle translation search:",t)}}async toggleFrontendUriSearch(){const e=!this.searchByFrontendUri;try{await P.set("pageTree_searchByFrontendUri",e?"1":"0"),this.searchByFrontendUri=e,this.tree?.settings&&(this.tree.settings.searchByFrontendUriEnabled=e);const t=this.querySelector(".search-input");t&&t.value.trim()!==""&&this.refreshTree()}catch(t){console.error("Failed to toggle frontend URI search:",t)}}handleDragStart(e,t){const n={__hidden:!1,__expanded:!1,__indeterminate:!1,__loading:!1,__processed:!1,__treeDragAction:"",__treeIdentifier:"",__treeParents:[""],__parents:[""],__x:0,__y:0,deletable:!1,depth:0,editable:!0,hasChildren:!1,icon:t.icon,overlayIcon:"",identifier:"NEW"+Math.floor(Math.random()*1e9).toString(16),loaded:!1,name:"",note:"",parentIdentifier:"",prefix:"",recordType:"pages",suffix:"",tooltip:"",type:"PageTreeItem",doktype:t.nodeType,statusInformation:[],labels:[]};this.tree.draggingNode=n,this.tree.nodeDragMode=d.NEW,e.dataTransfer.clearData();const o={statusIconIdentifier:this.tree.getNodeDragStatusIcon(),tooltipIconIdentifier:t.icon,tooltipLabel:t.title};e.dataTransfer.setData(f.dragTooltip,JSON.stringify(o)),e.dataTransfer.setData(f.newTreenode,JSON.stringify(n)),e.dataTransfer.effectAllowed="move"}checkHiddenSubmenuItems(){requestAnimationFrame(()=>{const e=this.subMenuItemsExpanded;e&&this.submenuItemsContainer.classList.remove("tree-toolbar__submenu-items--expanded"),this.hasHiddenSubMenuItems=this.submenuItemsContainer.scrollHeight>this.submenuItemsContainer.clientHeight,e&&this.submenuItemsContainer.classList.add("tree-toolbar__submenu-items--expanded")})}toggleSubmenu(e){e.stopPropagation(),this.subMenuItemsExpanded=!this.subMenuItemsExpanded}launchPageWizard(){const e=this.tree.getSelectedNodes();A({positionData:{pageUid:parseInt(e[0]?.identifier,10),insertPosition:"inside"},preventPositionAutoAdvance:!0})}};s([b({type:y})],c.prototype,"tree",void 0),s([b({type:Boolean})],c.prototype,"searchInTranslatedPages",void 0),s([b({type:Boolean})],c.prototype,"searchByFrontendUri",void 0),s([E()],c.prototype,"subMenuItemsExpanded",void 0),s([E()],c.prototype,"hasHiddenSubMenuItems",void 0),s([T(".tree-toolbar__submenu-items")],c.prototype,"submenuItemsContainer",void 0),c=s([v("typo3-backend-navigation-component-pagetree-toolbar")],c);export{y as EditablePageTree,h as PageTreeNavigationComponent,B as navigationComponentName}; diff --git a/Resources/Public/JavaScript/tree/page-tree.js b/Resources/Public/JavaScript/tree/page-tree.js new file mode 100644 index 0000000..cbf1095 --- /dev/null +++ b/Resources/Public/JavaScript/tree/page-tree.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{Tree as l}from"@typo3/backend/tree/tree.js";import{html as a}from"lit";import{DataTransferTypes as i}from"@typo3/backend/enum/data-transfer-types.js";import n from"@typo3/backend/modal.js";import{SeverityEnum as u}from"@typo3/backend/enum/severity.js";import s from"@typo3/backend/utility/drag-drop-utility.js";import p from"@typo3/core/ajax/ajax-request.js";class m extends l{constructor(){super(),this.settings.defaultProperties={hasChildren:!1,nameSourceField:"title",prefix:"",suffix:"",locked:!1,loaded:!1,overlayIcon:"",selectable:!0,expanded:!1,checked:!1,stopPageTree:!1}}getDataUrl(e=null){return e===null?this.settings.dataUrl:this.settings.dataUrl+"&parent="+e.identifier+"&mount="+e.mountPoint+"&depth="+e.depth}ensureActiveNodeLoaded(e){return!e||this.nodes.find(t=>t.checked)?Promise.resolve():new p(TYPO3.settings.ajaxUrls.page_tree_rootline).withQueryArguments({identifier:e}).get({cache:"no-cache"}).then(t=>t.resolve()).then(t=>{const{rootline:r}=t;return r.pop(),this.expandParents(r)})}createNodeToggle(e){return a`${e.stopPageTree&&e.depth!==0?a`{t.preventDefault(),t.stopImmediatePropagation(),document.dispatchEvent(new CustomEvent("typo3:pagetree:mountPoint",{detail:{pageId:parseInt(e.identifier,10)}}))}}> `:super.createNodeToggle(e)}`}handleNodeDragOver(e){if(super.handleNodeDragOver(e))return!0;if(e.dataTransfer.types.includes(i.content)){const t=this.getNodeFromDragEvent(e);return t===null?!1:(this.cleanDrag(),this.getElementFromNode(t).classList.add("node-hover"),t.hasChildren&&!t.__expanded?this.openNodeTimeout.targetNode!=t&&(this.openNodeTimeout.targetNode=t,clearTimeout(this.openNodeTimeout.timeout),this.openNodeTimeout.timeout=setTimeout(()=>{this.showChildren(this.openNodeTimeout.targetNode),this.openNodeTimeout.targetNode=null,this.openNodeTimeout.timeout=null},1e3)):(clearTimeout(this.openNodeTimeout.timeout),this.openNodeTimeout.targetNode=null,this.openNodeTimeout.timeout=null),e.preventDefault(),s.updateEventAndTooltipToReflectCopyMoveIntention(e),!0)}return!1}handleNodeDrop(e){if(super.handleNodeDrop(e))return!0;if(e.dataTransfer.types.includes(i.content)){const t=this.getNodeFromDragEvent(e);if(t===null)return!1;const r=e.dataTransfer.getData(i.content),d=JSON.parse(r);e.preventDefault();const o=new URL(d.moveElementUrl,window.origin);return o.searchParams.set("expandPage",t.identifier),o.searchParams.set("originalPid",t.identifier),s.isCopyModifierFromEvent(e)&&o.searchParams.set("makeCopy","1"),n.advanced({content:o.toString(),severity:u.notice,size:n.sizes.large,type:n.types.iframe}),!0}return!1}}export{m as PageTree}; diff --git a/Resources/Public/JavaScript/tree/tree-module-state.js b/Resources/Public/JavaScript/tree/tree-module-state.js new file mode 100644 index 0000000..d4d50e5 --- /dev/null +++ b/Resources/Public/JavaScript/tree/tree-module-state.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import l from"@typo3/core/ajax/ajax-request.js";import{ModuleStateStorage as a}from"@typo3/backend/storage/module-state-storage.js";const u=f=>{class c extends f{constructor(){super(...arguments),this.selectActiveNodeInLoadedNodes=t=>{let e=a.current(this.moduleStateType);if(!e.identifier)return;const{nodes:r}=t.detail,i=this.transformModuleStateIdentifierToNodeIdentifier(e.treeIdentifier),o=this.transformModuleStateIdentifierToNodeIdentifier(e.identifier),n=r.find(s=>e.treeIdentifier!==null&&s.__treeIdentifier===i||e.treeIdentifier===null&&s.identifier===o);if(!n)return;e.treeIdentifier===null&&(e=a.updateWithTreeIdentifier(this.moduleStateType,this.transformNodeIdentifierToModuleStateIdentifier(n.identifier),this.transformNodeIdentifierToModuleStateIdentifier(n.__treeIdentifier))),n.checked=!0;const d=r.find(s=>s.__treeIdentifier===n.__parents.join("_"));d&&!d.__expanded&&this.tree.updateComplete.then(()=>this.tree.expandNodeParents(n))},this.fetchActiveNodeIfMissing=async()=>{const t=a.current(this.moduleStateType);t.identifier&&(this.tree.nodes.find(e=>e.checked)||await this.selectActiveViaRootline(t.identifier))},this.moduleStateUpdated=async t=>{const e=t.detail.state.identifier;if(!this.tree||e&&e===t.detail.oldState.identifier&&this.tree.nodes.find(d=>d.checked))return;if(!e){console.error("invalid identifier",t.detail);return}this.tree.loading&&await this.tree.loadComplete;const r=this.transformModuleStateIdentifierToNodeIdentifier(e),i=this.tree.nodes.find(d=>d.identifier===r),o=this.tree.nodes.find(d=>d.checked);if(i&&i===o){await this.tree.expandNodeParents(i);return}const n=!1;if(i){await this.selectActiveNodeByParents(e,i.__parents,n);return}await this.selectActiveViaRootline(e)}}connectedCallback(){super.connectedCallback(),document.addEventListener("typo3:module-state-storage:update:"+this.moduleStateType,this.moduleStateUpdated)}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("typo3:module-state-storage:update:"+this.moduleStateType,this.moduleStateUpdated)}transformModuleStateIdentifierToNodeIdentifier(t){return t}transformNodeIdentifierToModuleStateIdentifier(t){return t}async selectActiveViaRootline(t){const e=new URL(this.tree.settings.rootlineUrl,window.location.origin);e.searchParams.set("identifier",t);const r=await new l(e.toString()).get({cache:"no-cache"}),{rootline:i}=await r.resolve();i.pop(),await this.selectActiveNodeByParents(t,i.map(n=>this.transformModuleStateIdentifierToNodeIdentifier(n)),!1)}async selectActiveNodeByParents(t,e,r=!0){await this.tree.expandParents(e);const i=this.transformModuleStateIdentifierToNodeIdentifier(t),o=this.tree.nodes.find(n=>n.identifier===i);o&&this.tree.selectNode(o,r)}}return c};export{u as TreeModuleState}; diff --git a/Resources/Public/JavaScript/tree/tree-node-toggle.js b/Resources/Public/JavaScript/tree/tree-node-toggle.js new file mode 100644 index 0000000..063a624 --- /dev/null +++ b/Resources/Public/JavaScript/tree/tree-node-toggle.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{property as a,customElement as f}from"lit/decorators.js";import{LitElement as u,html as s}from"lit";import"@typo3/backend/element/icon-element.js";var p=function(r,t,o,n){var d=arguments.length,e=d<3?t:n===null?n=Object.getOwnPropertyDescriptor(t,o):n,l;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")e=Reflect.decorate(r,t,o,n);else for(var i=r.length-1;i>=0;i--)(l=r[i])&&(e=(d<3?l(e):d>3?l(t,o,e):l(t,o))||e);return d>3&&e&&Object.defineProperty(t,o,e),e};let c=class extends u{constructor(){super(...arguments),this.expanded="false"}render(){return s``}};p([a({type:String,reflect:!0,attribute:"aria-expanded"})],c.prototype,"expanded",void 0),c=p([f("typo3-backend-tree-node-toggle")],c);var m=c;export{m as default}; diff --git a/Resources/Public/JavaScript/tree/tree-node.js b/Resources/Public/JavaScript/tree/tree-node.js new file mode 100644 index 0000000..d780bc7 --- /dev/null +++ b/Resources/Public/JavaScript/tree/tree-node.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +var t;(function(E){E.COPY="copy",E.EDIT="edit",E.MOVE="move",E.DELETE="delete",E.NEW="new"})(t||(t={}));var f;(function(E){E.INSIDE="inside",E.BEFORE="before",E.AFTER="after"})(f||(f={}));export{t as TreeNodeCommandEnum,f as TreeNodePositionEnum}; diff --git a/Resources/Public/JavaScript/tree/tree-toolbar.js b/Resources/Public/JavaScript/tree/tree-toolbar.js new file mode 100644 index 0000000..d2e40b3 --- /dev/null +++ b/Resources/Public/JavaScript/tree/tree-toolbar.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as u,html as d}from"lit";import{property as m,customElement as b}from"lit/decorators.js";import h from"@typo3/core/event/debounce-event.js";import"@typo3/backend/element/icon-element.js";import"@typo3/backend/viewport/content-navigation-toggle.js";import{Tree as f}from"@typo3/backend/tree/tree.js";import r from"~labels/core.core";import"bootstrap";var p=function(s,e,t,n){var i=arguments.length,o=i<3?e:n===null?n=Object.getOwnPropertyDescriptor(e,t):n,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")o=Reflect.decorate(s,e,t,n);else for(var c=s.length-1;c>=0;c--)(a=s[c])&&(o=(i<3?a(o):i>3?a(e,t,o):a(e,t))||o);return i>3&&o&&Object.defineProperty(e,t,o),o};let l=class extends u{constructor(){super(...arguments),this.tree=null,this.showRefresh=!0,this.settings={searchInput:".search-input",filterTimeout:450}}createRenderRoot(){return this}firstUpdated(){const e=this.querySelector(this.settings.searchInput);e&&new h("input",t=>{const n=t.target;this.tree.filter(n.value.trim())},this.settings.filterTimeout).bindTo(e)}render(){return d`
    `}refreshTree(){this.tree.refreshOrFilterTree()}collapseAll(e){e.preventDefault(),this.tree.nodes.forEach(t=>{t.__parents.length&&this.tree.hideChildren(t)})}};p([m({type:f})],l.prototype,"tree",void 0),p([m({type:Boolean})],l.prototype,"showRefresh",void 0),l=p([b("typo3-backend-tree-toolbar")],l);export{l as TreeToolbar}; diff --git a/Resources/Public/JavaScript/tree/tree.js b/Resources/Public/JavaScript/tree/tree.js new file mode 100644 index 0000000..4fcb505 --- /dev/null +++ b/Resources/Public/JavaScript/tree/tree.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as x,html as a,nothing as N}from"lit";import{property as C,state as p,query as $}from"lit/decorators.js";import{repeat as F}from"lit/directives/repeat.js";import{styleMap as b}from"lit/directives/style-map.js";import{ifDefined as D}from"lit/directives/if-defined.js";import{TreeNodePositionEnum as u,TreeNodeCommandEnum as h}from"@typo3/backend/tree/tree-node.js";import w from"@typo3/core/ajax/ajax-request.js";import P from"@typo3/backend/notification.js";import{KeyTypesEnum as l}from"@typo3/backend/enum/key-types.js";import"@typo3/backend/element/icon-element.js";import E from"@typo3/backend/storage/client.js";import{DataTransferTypes as m}from"@typo3/backend/enum/data-transfer-types.js";import k from"@typo3/backend/severity.js";import S from"~labels/core.misc";import R from"~labels/backend.layout";import{openPageWizardModal as O}from"@typo3/backend/page-wizard/helper/wizard-helper.js";var f=function(_,e,t,r){var s=arguments.length,i=s<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,t):r,o;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")i=Reflect.decorate(_,e,t,r);else for(var n=_.length-1;n>=0;n--)(o=_[n])&&(i=(s<3?o(i):s>3?o(e,t,i):o(e,t))||i);return s>3&&i&&Object.defineProperty(e,t,i),i};class y{constructor(e){this.treeNodes=e,this.updateIndexes()}get length(){return this.treeNodes.length}toArray(){return this.treeNodes}splice(e,t,...r){this.treeNodes.splice(e,t,...r),this.updateIndexes()}getNodeByTreeIdentifier(e){const t=this.treeIdentifierIndex[e]??null;return this.treeNodes[t]??null}updateIndexes(){this.treeIdentifierIndex=Object.fromEntries(this.treeNodes.map((e,t)=>[e.__treeIdentifier,t]))}}class T extends CustomEvent{static{this.eventName="typo3:tree:filter-applied"}constructor(e,t){super(T.eventName,{detail:{searchTerm:e,resultCount:t},bubbles:!0,composed:!0})}}class v extends Event{static{this.eventName="typo3:tree:filter-reset"}constructor(){super(v.eventName,{bubbles:!0,composed:!0})}}class c extends x{constructor(){super(...arguments),this.setup=null,this.settings={showIcons:!1,width:300,dataUrl:"",filterUrl:"",defaultProperties:{},expandUpToLevel:null,actions:[]},this.nodeMap=new y([]),this.currentScrollPosition=0,this.currentVisibleHeight=0,this.searchTerm=null,this.searchResults=0,this.loading=!1,this.hoveredNode=null,this.nodeDragAllowed=!1,this.isOverRoot=!1,this.nodeDragPosition=null,this.nodeDragMode=null,this.draggingNode=null,this.nodeHeight=32,this.indentWidth=20,this.displayNodes=[],this.focusedNode=null,this.lastFocusedNode=null,this.editingNode=null,this.openNodeTimeout={targetNode:null,timeout:null},this.unfilteredNodes="",this.muteErrorNotifications=!1,this.networkErrorTitle=S.get("tree_networkError"),this.networkErrorMessage=S.get("tree_networkErrorDescription"),this.allowNodeEdit=!1,this.allowNodeDrag=!1,this.allowNodeSorting=!1,this.currentFilterRequest=null,this.__loadPromise=new Promise(e=>this.__loadFinished=e),this.lastRenderScrollPosition=null}get loadComplete(){return this.__loadPromise}get nodes(){return this.nodeMap.toArray()}set nodes(e){this.nodeMap=new y(e)}getNodeFromElement(e){return e===null||!("treeId"in e.dataset)?null:this.getNodeByTreeIdentifier(e.dataset.treeId)}getElementFromNode(e){return this.querySelector('[data-tree-id="'+this.getNodeTreeIdentifier(e)+'"]')}hideChildren(e){e.__expanded=!1,this.saveNodeStatus(e),this.dispatchEvent(new CustomEvent("typo3:tree:expand-toggle",{detail:{node:e}}))}async showChildren(e){e.__expanded=!0,await this.loadChildren(e),this.saveNodeStatus(e),this.dispatchEvent(new CustomEvent("typo3:tree:expand-toggle",{detail:{node:e}}))}getDataUrl(e=null){return e===null?this.settings.dataUrl:this.settings.dataUrl+"&parent="+e.identifier+"&depth="+e.depth}getFilterUrl(){return this.settings.filterUrl+"&q="+this.searchTerm}async loadData(){this.loading=!0,this.nodeMap=new y(this.prepareNodes(await this.fetchData())),this.__loadFinished(),this.__loadPromise=new Promise(e=>this.__loadFinished=e),this.loading=!1}async fetchData(e=null){try{let r=await(await new w(this.getDataUrl(e)).get({cache:"no-cache"})).resolve();return Array.isArray(r)?(e!==null&&(r=r.filter(i=>i.identifier!==e.identifier),r.unshift(e)),r=this.enhanceNodes(r),e!==null&&r.shift(),(await Promise.all(r.map(async i=>{const o=i.__parents.join("_"),n=r.find(g=>g.__treeIdentifier===o)||null,d=n===null||n.__expanded;if(!i.loaded&&i.hasChildren&&i.__expanded&&d){const g=await this.fetchData(i);return i.loaded=!0,[i,...g]}else return[i]}))).flat()):[]}catch(t){return this.errorNotification(t),[]}}async loadChildren(e){try{if(e.loaded){await Promise.all(this.nodes.filter(i=>i.__parents.join("_")===e.__treeIdentifier&&!i.loaded&&i.hasChildren&&i.__expanded).map(i=>this.loadChildren(i)));return}e.__loading=!0;const t=this.prepareNodes(await this.fetchData(e)),r=this.nodes.indexOf(e)+1;let s=0;for(let i=r;i{this.getElementFromNode(this.focusedNode)?.focus()})}async editNode(e){this.isNodeEditable(e)&&(this.editingNode=e,this.requestUpdate(),this.updateComplete.then(()=>{const t=this.getElementFromNode(this.editingNode)?.querySelector(".node-edit");t&&(t.focus(),t.select())}))}scrollNodeIntoViewIfNeeded(e){const t=e.__y+this.nodeHeight/2,r=this.root.scrollTop,s=r+this.root.clientHeight,i=t-this.root.clientHeight/2;(ts)&&(this.root.scrollTop=i)}async deleteNode(e){if(!e.deletable){console.error("The Node cannot be deleted.");return}this.handleNodeDelete(e)}async moveNode(e,t,r){this.handleNodeMove(e,t,r)}async addNode(e,t,r){let s=this.nodes.indexOf(t);const i=r===u.INSIDE?t:this.getParentNode(t),o=this.enhanceNodes([i,{...e,depth:i?i.depth+1:0}]).pop();if(i&&(i.hasChildren&&!i.__expanded&&await this.showChildren(i),i.hasChildren||(i.hasChildren=!0,i.__expanded=!0)),r===u.INSIDE)s+=1;else if(r===u.AFTER){const n=t.depth;let d=s+1;for(;dn;)d++;s=d}this.nodeMap.splice(s,0,o),this.handleNodeAdd(o,t,r)}async removeNode(e){const t=this.nodes.indexOf(e),r=this.getParentNode(e);t>-1&&this.nodeMap.splice(t,1),this.requestUpdate(),this.updateComplete.then(()=>{r?.__expanded&&r.hasChildren&&this.getNodeChildren(r).length===0&&(r.hasChildren=!1,r.__expanded=!1)})}filter(e){typeof e=="string"&&(this.searchTerm=e),this.searchTerm&&this.settings.filterUrl?(this.loading=!0,this.currentFilterRequest?.abort(),this.currentFilterRequest=new w(this.getFilterUrl()),this.currentFilterRequest.get({cache:"no-cache"}).then(t=>t.resolve()).then(t=>{const r=Array.isArray(t)?t:[];this.searchResults=r.length,r.length>0&&(this.unfilteredNodes===""&&(this.unfilteredNodes=JSON.stringify(this.nodes)),this.nodeMap=new y(this.enhanceNodes(r)),this.searchResults=r.length)}).catch(t=>{if(!(t instanceof DOMException&&t.name==="AbortError"))throw this.errorNotification(t),t}).then(()=>{this.loading=!1,this.currentFilterRequest=null,this.dispatchEvent(new T(this.searchTerm,this.searchResults))})):this.resetFilter().then(()=>{this.loading=!1,this.dispatchEvent(new v)})}async resetFilter(){if(this.searchTerm="",this.searchResults=0,this.unfilteredNodes.length>0){const e=this.getSelectedNodes()[0];if(typeof e>"u"){await this.loadData();return}this.nodeMap=new y(this.enhanceNodes(JSON.parse(this.unfilteredNodes))),this.unfilteredNodes="";const t=this.getNodeByTreeIdentifier(e.__treeIdentifier);t?this.selectNode(t,!1):await this.loadData()}else await this.loadData()}errorNotification(e=null){if(!this.muteErrorNotifications)if(Array.isArray(e))e.forEach(t=>{P.error(t.title,t.message)});else{let t=this.networkErrorTitle;e&&e.target&&(e.target.status||e.target.statusText)&&(t+=" - "+(e.target.status||"")+" "+(e.target.statusText||"")),P.error(t,this.networkErrorMessage)}}getSelectedNodes(){return this.nodes.filter(e=>e.checked)}getNodeByTreeIdentifier(e){return this.nodeMap.getNodeByTreeIdentifier(e)}getNodeDragStatusIcon(){return this.nodeDragMode===h.DELETE?"actions-delete":this.nodeDragMode===h.NEW?"actions-add":this.nodeDragPosition===u.BEFORE?"apps-pagetree-drag-move-above":this.nodeDragPosition===u.INSIDE?"apps-pagetree-drag-move-into":this.nodeDragPosition===u.AFTER?"apps-pagetree-drag-move-below":"actions-ban"}async expandParents(e){for(const t of e){const r=this.nodes.find(s=>s.identifier===t.toString());if(!r)return;r.__expanded||await this.showChildren(r)}}async expandNodeParents(e){await this.expandParents(e.__parents)}prepareNodes(e){const t=new CustomEvent("typo3:tree:nodes-prepared",{detail:{nodes:e},bubbles:!1});return this.dispatchEvent(t),t.detail.nodes}enhanceNodes(e){const t=e.reduce((s,i)=>{if(i?.__processed===!0)return[...s,i];i=Object.assign({},this.settings.defaultProperties,i),i.__parents=[];const o=i.depth>0?s.findLast(d=>d.depths.depth===0).length===1&&(t[0].__expanded=!0),t}createRenderRoot(){return this}shouldUpdate(e){return!(e.size===1&&e.has("currentScrollPosition")&&this.lastRenderScrollPosition!==null&&Math.abs(this.currentScrollPosition-this.lastRenderScrollPosition)/this.nodeHeight<20)}render(){const e=this.loading?a`
    `:N;return a`
    ${e}
    {this.currentScrollPosition=t.currentTarget.scrollTop}} @mouseover=${()=>this.isOverRoot=!0} @mouseout=${()=>this.isOverRoot=!1} @keydown=${t=>this.handleKeyboardInteraction(t)}>${this.renderVisibleNodes()}
    `}renderVisibleNodes(){this.displayNodes=this.nodes.filter(s=>s.__hidden!==!0&&!s.__treeParents.some(i=>this.getNodeByTreeIdentifier(i).__expanded===!1)),this.displayNodes.forEach((s,i)=>{s.__x=s.depth*this.indentWidth,s.__y=i*this.nodeHeight}),this.lastRenderScrollPosition=this.currentScrollPosition;const e=Math.ceil(this.currentVisibleHeight/this.nodeHeight),t=Math.floor(this.currentScrollPosition/this.nodeHeight),r=this.displayNodes.filter((s,i)=>this.getFirstNode()===s||this.focusedNode===s||this.lastFocusedNode===s?!0:i+40>=t&&i-40${F(r,s=>this.getNodeTreeIdentifier(s),s=>a`
    {this.handleNodeDragOver(i)}} @dragstart=${i=>{this.handleNodeDragStart(i,s)}} @dragleave=${i=>{this.handleNodeDragLeave(i)}} @dragend=${i=>{this.handleNodeDragEnd(i)}} @drop=${i=>{this.handleNodeDrop(i)}} @click=${i=>{this.handleNodeClick(i,s)}} @dblclick=${i=>{this.handleNodeDoubleClick(i,s)}} @focusin=${()=>{this.focusedNode=s}} @focusout=${()=>{this.focusedNode===s&&(this.lastFocusedNode=s,this.focusedNode=null)}} @contextmenu=${i=>{i.preventDefault(),i.stopPropagation(),this.dispatchEvent(new CustomEvent("typo3:tree:node-context",{detail:{node:s,originalEvent:i}}))}}>${this.createNodeLabel(s)} ${this.createNodeGuides(s)} ${this.createNodeLoader(s)||this.createNodeToggle(s)||N} ${this.createNodeContent(s)} ${this.createNodeStatusInformation(s)} ${this.createNodeDeleteDropZone(s)}
    `)}`}async firstUpdated(){new ResizeObserver(t=>{for(const r of t)r.target===this.root&&(this.currentVisibleHeight=r.target.getBoundingClientRect().height)}).observe(this.root),Object.assign(this.settings,this.setup||{}),this.registerUnloadHandler(),await this.loadData(),this.dispatchEvent(new Event("tree:initialized"))}resetSelectedNodes(){this.getSelectedNodes().forEach(e=>{e.checked===!0&&(e.checked=!1)})}isNodeSelectable(e){return!0}isNodeEditable(e){return e.editable&&this.allowNodeEdit}handleNodeClick(e,t){e.detail===1&&(e.preventDefault(),e.stopPropagation(),this.editingNode!==t&&this.selectNode(t,!0))}handleNodeDoubleClick(e,t){e.preventDefault(),e.stopPropagation(),this.editingNode!==t&&this.editNode(t)}cleanDrag(){this.querySelectorAll(".node").forEach(function(t){t.classList.remove("node-dragging-before"),t.classList.remove("node-dragging-after"),t.classList.remove("node-hover")})}getNodeFromDragEvent(e){const t=e.target;return this.getNodeFromElement(t.closest("[data-tree-id]"))}getTooltipDescription(e){return"ID: "+e.identifier}handleNodeDragStart(e,t){if(this.allowNodeDrag===!1||t.depth===0){e.preventDefault();return}this.draggingNode=t,this.requestUpdate(),e.dataTransfer.clearData();const r={statusIconIdentifier:this.getNodeDragStatusIcon(),tooltipIconIdentifier:t.icon,tooltipLabel:t.name,tooltipDescription:this.getTooltipDescription(t)};e.dataTransfer.setData(m.dragTooltip,JSON.stringify(r)),this.createDataTransferItemsFromNode(t).forEach(({data:s,type:i})=>e.dataTransfer.items.add(s,i)),e.dataTransfer.effectAllowed="move"}handleNodeDragOver(e){if(!e.dataTransfer.types.includes(m.treenode)&&!e.dataTransfer.types.includes(m.newTreenode))return!1;const t=e.target,r=this.getNodeFromDragEvent(e);if(r===null||this.draggingNode===null)return!1;this.cleanDrag(),this.refreshDragToolTip(),this.nodeDragMode=null,this.nodeDragPosition=null;const s=this.getElementFromNode(r);if(s.classList.add("node-hover"),r.hasChildren&&!r.__expanded?this.openNodeTimeout.targetNode!=r&&(this.openNodeTimeout.targetNode=r,clearTimeout(this.openNodeTimeout.timeout),this.openNodeTimeout.timeout=setTimeout(()=>{this.showChildren(this.openNodeTimeout.targetNode),this.openNodeTimeout.targetNode=null,this.openNodeTimeout.timeout=null},1e3)):(clearTimeout(this.openNodeTimeout.timeout),this.openNodeTimeout.targetNode=null,this.openNodeTimeout.timeout=null),this.draggingNode==r)return t.dataset.treeDropzone==="delete"?(this.nodeDragMode=h.DELETE,e.preventDefault(),this.refreshDragToolTip(),!0):(this.refreshDragToolTip(),!0);if(r.__parents.includes(this.draggingNode.identifier))return this.refreshDragToolTip(),!0;if(this.nodeDragMode=h.MOVE,e.dataTransfer.types.includes(m.newTreenode)&&(this.nodeDragMode=h.NEW),this.nodeDragPosition=u.INSIDE,r.depth===0||this.allowNodeSorting===!1)return this.refreshDragToolTip(),e.preventDefault(),!0;const o=this.getElementFromNode(r).getBoundingClientRect(),n=e.clientY-o.y;return n<6?(this.nodeDragPosition=u.BEFORE,s.classList.add("node-dragging-before")):this.nodeHeight-n<6&&r.hasChildren===!1&&r.__expanded===!1&&(this.nodeDragPosition=u.AFTER,s.classList.add("node-dragging-after")),this.refreshDragToolTip(),e.preventDefault(),!0}handleNodeDragLeave(e){this.draggingNode!==null&&this.cleanDrag()}handleNodeDragEnd(e){this.cleanDrag(),this.draggingNode=null,this.requestUpdate()}handleNodeDrop(e){if(this.cleanDrag(),e.dataTransfer.types.includes(m.treenode)){e.preventDefault();const t=e.dataTransfer.getData(m.treenode),r=this.getNodeByTreeIdentifier(t);this.nodeDragMode===h.DELETE&&this.deleteNode(r);const s=this.getNodeFromDragEvent(e);return s===null?!1:(this.nodeDragMode===h.MOVE&&this.moveNode(r,s,this.nodeDragPosition),this.nodeDragMode=null,this.nodeDragPosition=null,!0)}if(e.dataTransfer.types.includes(m.newTreenode)){e.preventDefault();let t=this.getNodeFromDragEvent(e);if(t===null)return!1;const r=JSON.parse(e.dataTransfer.getData(m.newTreenode));let s="inside";if(this.nodeDragPosition===u.AFTER)s="after";else if(this.nodeDragPosition===u.BEFORE){const i=this.getPreviousNode(t);s=i.depth==t.depth?"after":"inside",t=i}return O({doktype:String(r.doktype),positionData:{pageUid:parseInt(t.identifier,10),insertPosition:s}}),this.nodeDragMode=null,this.nodeDragPosition=null,!0}return!1}refreshDragToolTip(){top.document.dispatchEvent(new CustomEvent("typo3:drag-tooltip:metadata-update",{detail:{statusIconIdentifier:this.getNodeDragStatusIcon()}}))}createNodeLabel(e){const t=this.getNodeLabels(e);if(t.length===0)return a`${N}`;const s={backgroundColor:t[0].color};return a``}createNodeGuides(e){const t=e.__treeParents.map(r=>{const s=this.getNodeByTreeIdentifier(r);let i="none";return this.getNodeSetsize(s)!==this.getNodePositionInSet(s)&&(i="line"),a`
    `});return this.getNodeSetsize(e)===this.getNodePositionInSet(e)?t.push(a`
    `):t.push(a`
    `),a`
    ${t}
    `}createNodeLoader(e){return e.__loading===!0?a` `:null}createNodeToggle(e){return e.hasChildren===!0?a`{t.preventDefault(),t.stopImmediatePropagation(),this.handleNodeToggle(e)}}> `:null}createNodeContent(e){return a`
    ${this.createNodeContentIcon(e)} ${this.editingNode===e?this.createNodeForm(e):this.createNodeContentLabel(e)} ${this.createNodeContentAction(e)}
    `}createNodeContentIcon(e){return this.settings.showIcons?a`{t.preventDefault(),t.stopImmediatePropagation(),this.dispatchEvent(new CustomEvent("typo3:tree:node-context",{detail:{node:e,originalEvent:t}}))}} @dblclick=${t=>{t.preventDefault(),t.stopImmediatePropagation()}}> `:a`${N}`}createNodeContentLabel(e){const t=(e.prefix||"")+e.name+(e.suffix||"");let r=t;if(this.searchTerm&&this.searchResults<=100&&(this.searchTerm.length>1||/\d/.test(this.searchTerm))){const s=new RegExp(`(${this.searchTerm.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")})`,"gi"),i=t.split(s);i.length>1&&(r=i.map((o,n)=>n%2===1?a`${o}`:o))}return a``}createNodeStatusInformation(e){const t=this.getNodeStatusInformation(e);if(t.length===0)return a`${N}`;const r=t[0],s=k.getCssClass(r.severity),i=r.icon!==""?r.icon:"actions-dot",o=r.overlayIcon!==""?r.overlayIcon:void 0;return a` `}createNodeDeleteDropZone(e){return this.draggingNode===e&&e.deletable?a`
    ${R.get("deleteItem")}
    `:a`${N}`}createNodeForm(e){const t=e.identifier.startsWith("NEW")?h.NEW:h.EDIT;return a`{i.stopImmediatePropagation()}} @blur=${i=>{if(this.editingNode!==null){this.editingNode=null;const n=i.target.value.trim();n!==e.name&&n!==""?this.handleNodeEdit(e,n):t===h.NEW&&this.removeNode(e),this.requestUpdate()}}} @keydown=${i=>{const o=i.key;if([l.ENTER,l.TAB].includes(o)){const d=i.target.value.trim();this.editingNode=null,this.requestUpdate(),d!==e.name&&d!==""?(this.handleNodeEdit(e,d),this.focusNode(e)):t===h.NEW&&d===""?this.removeNode(e):this.focusNode(e)}else[l.ESCAPE].includes(o)&&(this.editingNode=null,this.requestUpdate(),t===h.NEW?this.removeNode(e):this.focusNode(e))}} value=${e.name}>`}async handleNodeEdit(e,t){console.error("The function Tree->handleNodeEdit is not implemented.")}handleNodeDelete(e){console.error("The function Tree->handleNodeDelete is not implemented.")}handleNodeMove(e,t,r){console.error("The function Tree->handleNodeMove is not implemented.")}async handleNodeAdd(e,t,r){console.error("The function Tree->handleNodeAdd is not implemented.")}createNodeContentAction(e){return a`${N}`}createDataTransferItemsFromNode(e){throw new Error("The function Tree->createDataTransferItemFromNode is not implemented.")}getNodeIdentifier(e){return e.identifier}getNodeTreeIdentifier(e){return e.__treeIdentifier}getNodeParentTreeIdentifier(e){return e.__parents.join("_")}getNodeClasses(e){const t=["node"];return e.checked&&t.push("node-selected"),this.draggingNode===e&&t.push("node-dragging"),t}getNodeLabels(e){let t=e.labels;if(t?.length>0)return t=t.sort((s,i)=>i.priority-s.priority),t;const r=this.getParentNode(e);return r===null?[]:this.getNodeLabels(r).filter(s=>s.inheritByChildren)}getNodeStatusInformation(e){return e.statusInformation?.length?e.statusInformation.sort((r,s)=>r.severity!==s.severity?s.severity-r.severity:s.priority-r.priority):[]}getNodeDepth(e){return e.depth}getNodeTabindex(e){return this.focusedNode?this.focusedNode===e?0:-1:this.lastFocusedNode?this.lastFocusedNode===e?0:-1:this.getFirstNode()===e?0:-1}getNodeChildren(e){return e?.hasChildren?this.displayNodes.filter(t=>e===this.getParentNode(t)):[]}getNodeSetsize(e){if(e.depth===0)return this.displayNodes.filter(s=>s.depth===0).length;const t=this.getParentNode(e);return this.getNodeChildren(t).length}getNodePositionInSet(e){const t=this.getParentNode(e);let r=[];return e.depth===0?r=this.displayNodes.filter(s=>s.depth===0):t!==null&&(r=this.getNodeChildren(t)),r.indexOf(e)+1}getFirstNode(){return this.displayNodes.length?this.displayNodes[0]:null}getPreviousNode(e){const r=this.displayNodes.indexOf(e)-1;return this.displayNodes[r]?this.displayNodes[r]:null}getNextNode(e){const r=this.displayNodes.indexOf(e)+1;return this.displayNodes[r]?this.displayNodes[r]:null}getLastNode(){return this.displayNodes.length?this.displayNodes[this.displayNodes.length-1]:null}getParentNode(e){return e.__parents.length?this.getNodeByTreeIdentifier(this.getNodeParentTreeIdentifier(e)):null}getNodeTitle(e){let t=e.tooltip?e.tooltip:"uid="+e.identifier+" "+e.name;const r=this.getNodeLabels(e);r.length&&(t+="; "+r.map(i=>i.label).join("; "));const s=this.getNodeStatusInformation(e);return s.length&&(t+="; "+s.map(i=>i.label).join("; ")),t}handleNodeToggle(e){e.__expanded?this.hideChildren(e):this.showChildren(e)}handleKeyboardInteraction(e){if(this.editingNode!==null||[l.ENTER,l.SPACE,l.END,l.HOME,l.LEFT,l.UP,l.RIGHT,l.DOWN].includes(e.key)===!1)return;const r=e.target,s=this.getNodeFromElement(r);if(s===null)return;const i=this.getParentNode(s),o=this.getFirstNode(),n=this.getPreviousNode(s),d=this.getNextNode(s),g=this.getLastNode();switch(e.preventDefault(),e.key){case l.HOME:o!==null&&(this.scrollNodeIntoVisibleArea(o),this.focusNode(o));break;case l.END:g!==null&&(this.scrollNodeIntoVisibleArea(g),this.focusNode(g));break;case l.UP:n!==null&&(this.scrollNodeIntoVisibleArea(n),this.focusNode(n));break;case l.DOWN:d!==null&&(this.scrollNodeIntoVisibleArea(d),this.focusNode(d));break;case l.LEFT:s.__expanded?s.hasChildren&&this.hideChildren(s):i&&(this.scrollNodeIntoVisibleArea(i),this.focusNode(i));break;case l.RIGHT:s.__expanded&&d?(this.scrollNodeIntoVisibleArea(d),this.focusNode(d)):s.hasChildren&&this.showChildren(s);break;case l.ENTER:case l.SPACE:this.selectNode(s);break;default:}}scrollNodeIntoVisibleArea(e){const t=e.__y,r=e.__y+this.nodeHeight,s=t>=this.currentScrollPosition,i=r<=this.currentScrollPosition+this.currentVisibleHeight;if(!(s&&i)){let n=this.currentScrollPosition;!s&&!i?n=r-this.currentVisibleHeight:s?i||(n=r-this.currentVisibleHeight):n=t,n<0&&(n=0),this.root.scrollTo({top:n})}}registerUnloadHandler(){try{if(!window.frameElement)return;window.addEventListener("pagehide",()=>this.muteErrorNotifications=!0,{once:!0})}catch{console.error("Failed to check the existence of window.frameElement \u2013 using a foreign origin?")}}}f([C({type:Object})],c.prototype,"setup",void 0),f([p()],c.prototype,"settings",void 0),f([$(".nodes-root")],c.prototype,"root",void 0),f([p()],c.prototype,"nodeMap",void 0),f([p()],c.prototype,"currentScrollPosition",void 0),f([p()],c.prototype,"currentVisibleHeight",void 0),f([p()],c.prototype,"searchTerm",void 0),f([p()],c.prototype,"searchResults",void 0),f([p()],c.prototype,"loading",void 0),f([p()],c.prototype,"hoveredNode",void 0),f([p()],c.prototype,"nodeDragAllowed",void 0);export{c as Tree,T as TreeFilterAppliedEvent,v as TreeFilterResetEvent,y as TreeNodeMap}; diff --git a/Resources/Public/JavaScript/url-link-handler.js b/Resources/Public/JavaScript/url-link-handler.js new file mode 100644 index 0000000..e354864 --- /dev/null +++ b/Resources/Public/JavaScript/url-link-handler.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import t from"@typo3/backend/link-browser.js";import l from"@typo3/core/event/regular-event.js";class u{constructor(){new l("submit",(r,n)=>{r.preventDefault();const e=n.querySelector('[name="lurl"]').value.trim();e!==""&&t.finalizeFunction(e)}).delegateTo(document,"#lurlform")}}var i=new u;export{i as default}; diff --git a/Resources/Public/JavaScript/user-pass-login.js b/Resources/Public/JavaScript/user-pass-login.js new file mode 100644 index 0000000..23e59f7 --- /dev/null +++ b/Resources/Public/JavaScript/user-pass-login.js @@ -0,0 +1,25 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import c from"@typo3/backend/login.js";import r from"@typo3/core/event/regular-event.js";import p from"@typo3/core/document-service.js";class g{constructor(){this.resetPassword=()=>{const t=document.querySelector(this.options.passwordField);if(t===null||t.value==="")return;const e=document.querySelector(c.options.useridentField);e&&(e.value=t.value),t.value=""},this.toggleCopyright=t=>{t.key===" "&&t.target.click()},this.attachCapslockWarning=(t,e,n)=>{const s=t.closest(".input-group");if(!s||s.querySelector(".input-group-text-warning-capslock"))return;const a=t.parentElement,l=` + + + + + + `,i=document.createElement("span");i.classList.add("visually-hidden"),i.textContent=n;const o=document.createElement("span");if(o.classList.add("input-group-text","input-group-text-warning","input-group-text-warning-capslock"),o.role="status",o.innerHTML=l,o.title=e,o.appendChild(i),a.classList.contains("form-control-clearable-wrapper")){a.insertAdjacentElement("afterend",o);return}t.insertAdjacentElement("afterend",o)},this.removeCapslockWarning=t=>{const e=t.closest(".input-group");if(!e)return;const n=e.querySelector(".input-group-text-warning-capslock");n&&n.remove()},this.showCapslockWarning=t=>{const e=t.target,n=e.dataset.capslockwarningTitle,s=e.dataset.capslockwarningMessage;this.isCapslockEnabled(t)?this.attachCapslockWarning(e,n,s):this.removeCapslockWarning(e)},this.attachPasswordToggle=t=>{const e=t.closest(".input-group");if(!e||e.querySelector(".t3js-login-toggle-password"))return;const n=` + + + + + + `,s=document.createElement("button");s.type="button",s.classList.add("btn","btn-default","t3js-login-toggle-password"),s.ariaLabel=t.dataset.passwordtoggleLabel??"",s.innerHTML=n,s.addEventListener("click",()=>{s.classList.contains("active")?(s.classList.remove("active"),t.type="password"):(s.classList.add("active"),t.type="text")}),e.insertAdjacentElement("beforeend",s)},this.removePasswordToggle=t=>{const e=t.closest(".input-group");if(!e)return;const n=e.querySelector(".t3js-login-toggle-password");n&&(n.remove(),t.type="password")},this.showPasswordToggle=t=>{const e=t.target;if(e.value===""){this.removePasswordToggle(e);return}else this.attachPasswordToggle(e)},this.init()}async init(){await p.ready(),this.options={usernameField:".t3js-login-username-field",passwordField:".t3js-login-password-field",copyrightLink:".t3js-login-copyright-link"};const t=document.querySelector(this.options.usernameField),e=document.querySelector(this.options.passwordField),n=document.querySelector(this.options.copyrightLink);c.options.submitHandler=this.resetPassword,[t,e].forEach(s=>new r("keypress",this.showCapslockWarning).bindTo(s)),["input","change"].forEach(s=>new r(s,this.showPasswordToggle).bindTo(e)),new r("keydown",this.toggleCopyright).bindTo(n),parent.opener?.TYPO3?.configuration?.username&&(t.value=parent.opener.TYPO3.configuration.username),t.value===""?t.focus():e.focus()}isCapslockEnabled(t){const e=t||window.event;if(!e)return!1;let n=-1;e.which?n=e.which:e.keyCode&&(n=e.keyCode);let s=!1;return e.shiftKey?s=e.shiftKey:e.modifiers&&(s=!!(e.modifiers&4)),n>=65&&n<=90&&!s||n>=97&&n<=122&&s}}var d=new g;export{d as default}; diff --git a/Resources/Public/JavaScript/user-settings-manager.js b/Resources/Public/JavaScript/user-settings-manager.js new file mode 100644 index 0000000..3174bbf --- /dev/null +++ b/Resources/Public/JavaScript/user-settings-manager.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{BroadcastMessage as s}from"@typo3/backend/broadcast-message.js";import o from"@typo3/backend/broadcast-service.js";import h from"@typo3/backend/storage/persistent.js";import u from"@typo3/backend/modal.js";import{SeverityEnum as p}from"@typo3/backend/enum/severity.js";import i from"~labels/backend.messages";var d;(function(c){c.colorSchemeSwitch="typo3-backend-color-scheme-switch"})(d||(d={}));class g{constructor(){document.addEventListener("typo3:color-scheme:update",e=>this.onColorSchemeUpdate(e.detail)),document.addEventListener("typo3:theme:update",e=>this.onThemeUpdate(e.detail)),document.addEventListener("typo3:title-format:update",e=>this.onTitleFormatUpdate(e.detail)),document.addEventListener("typo3:backend-language:update",e=>this.onBackendLanguageFormatUpdate(e.detail)),document.addEventListener("typo3:persistent:update",e=>this.onPersistentUpdate(e.detail)),document.addEventListener("typo3:date-time-first-day-of-week:update",e=>this.onDateTimeFirstDayOfWeekUpdate(e.detail)),document.addEventListener("typo3:color-scheme:broadcast",e=>this.activateColorScheme(e.detail.payload.colorScheme)),document.addEventListener("typo3:theme:broadcast",e=>this.activateTheme(e.detail.payload.theme)),document.addEventListener("typo3:title-format:broadcast",e=>this.activateTitleFormat(e.detail.payload.format)),document.addEventListener("typo3:backend-language:broadcast",()=>this.requestBackendLanguageRefresh()),document.addEventListener("typo3:persistent:broadcast",e=>this.updatePersistent(e.detail.payload.fieldName,e.detail.payload.value))}onColorSchemeUpdate(e){const{colorScheme:t}=e;this.activateColorScheme(t),o.post(new s("color-scheme","broadcast",{colorScheme:t}))}onThemeUpdate(e){const{theme:t}=e;this.activateTheme(t),o.post(new s("theme","broadcast",{theme:t}))}onTitleFormatUpdate(e){const{format:t}=e;this.activateTitleFormat(t),o.post(new s("title-format","broadcast",{format:t}))}onDateTimeFirstDayOfWeekUpdate(e){const{dow:t}=e;this.activateDateTimeFirstDayOfWeek(t),o.post(new s("date-time-first-day-of-week","broadcast",{dow:t}))}onBackendLanguageFormatUpdate(e){const{language:t}=e;this.requestBackendLanguageRefresh(),o.post(new s("backend-language","broadcast",{language:t}))}onPersistentUpdate(e){const{fieldName:t,value:a}=e;this.updatePersistent(t,a),o.post(new s("personalization","broadcast",{fieldName:t,value:a}))}activateColorScheme(e){const t=document.querySelector(d.colorSchemeSwitch);t&&(t.activeColorScheme=e),this.setStyleChangingDocumentAttribute("data-color-scheme",e)}activateTheme(e){this.setStyleChangingDocumentAttribute("data-theme",e)}activateTitleFormat(e){e==="sitenameFirst"?document.querySelector("typo3-backend-module-router")?.setAttribute("sitename-first",""):document.querySelector("typo3-backend-module-router")?.removeAttribute("sitename-first")}activateDateTimeFirstDayOfWeek(e){this.updatePersistent("dateTimeFirstDayOfWeek",e)}requestBackendLanguageRefresh(){const e="t3js-request-backend-language-refresh";u.currentModal?.querySelector("dialog")?.classList.contains(e)||u.confirm(i.get("userSettings.requestBackendLanguageRefresh.title"),i.get("userSettings.requestBackendLanguageRefresh.message"),p.notice,[{text:i.get("userSettings.requestBackendLanguageRefresh.buttonCancel"),btnClass:"btn-default",trigger:(t,a)=>a.hideModal(),name:"cancel"},{text:i.get("userSettings.requestBackendLanguageRefresh.buttonReload"),active:!0,btnClass:"btn-primary",trigger:()=>top.window.location.reload(),name:"ok"}],[e])}updatePersistent(e,t){h.set(e,t)}async setStyleChangingDocumentAttribute(e,t){const a=document.documentElement,n=window.frames.list_frame?.document.documentElement,m=()=>{a.classList.add("t3js-disable-transitions"),n?.classList.add("t3js-disable-transitions"),a.setAttribute(e,t),n?.setAttribute(e,t)},l=()=>{a.classList.remove("t3js-disable-transitions"),n?.classList.remove("t3js-disable-transitions")};if(window.matchMedia("(prefers-reduced-motion: reduce)").matches||!("startViewTransition"in document)||typeof document.startViewTransition!="function"){m(),await new Promise(r=>requestAnimationFrame(r)),n&&await new Promise(r=>window.frames.list_frame.requestAnimationFrame(r)),l();return}await document.startViewTransition(m).finished,l()}}var f=new g;export{f as default}; diff --git a/Resources/Public/JavaScript/utility.js b/Resources/Public/JavaScript/utility.js new file mode 100644 index 0000000..89af3c6 --- /dev/null +++ b/Resources/Public/JavaScript/utility.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class c{static trimExplode(e,r){return r.split(e).map(t=>t.trim()).filter(t=>t!=="")}static trimItems(e){return e.map(r=>r instanceof String?r.trim():r)}static intExplode(e,r,t=!1){return r.split(e).map(i=>parseInt(i,10)).filter(i=>!isNaN(i)||t&&i===0)}static isNumber(e){return!isNaN(parseFloat(e.toString()))&&isFinite(e)}static convertFormToObject(e){const r={};return e.querySelectorAll("input, select, textarea").forEach(t=>{const i=t.name,n=t.value;if(i)if(t.tagName.toLowerCase()==="input"&&t.type=="checkbox"){const s=t;r[i]===void 0&&(r[i]=[]),s.checked&&r[i].push(n)}else r[i]=n}),r}static mergeDeep(...e){const r=t=>t&&typeof t=="object";return e.reduce((t,i)=>(Object.keys(i).forEach(n=>{const s=t[n],a=i[n];Array.isArray(s)&&Array.isArray(a)?t[n]=s.concat(...a):r(s)&&r(a)?t[n]=c.mergeDeep(s,a):t[n]=a}),t),{})}static urlsPointToSameServerSideResource(e,r){if(!e||!r)return!1;const t=window.location.origin;try{const i=new URL(e,c.isValidUrl(e)?void 0:t),n=new URL(r,c.isValidUrl(r)?void 0:t),s=i.origin+i.pathname+i.search,a=n.origin+n.pathname+n.search;return s===a}catch{return!1}}static isValidUrl(e){try{return new URL(e),!0}catch{return!1}}}export{c as default}; diff --git a/Resources/Public/JavaScript/utility/collapse-state-persister.js b/Resources/Public/JavaScript/utility/collapse-state-persister.js new file mode 100644 index 0000000..2396a87 --- /dev/null +++ b/Resources/Public/JavaScript/utility/collapse-state-persister.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import h from"@typo3/backend/storage/client.js";import S from"@typo3/core/document-service.js";import d from"@typo3/core/event/regular-event.js";class u{constructor(){this.localStorageKey="collapse-states-",this.localStorageKeyDefaultSuffix="general",this.searchValueSelector=".t3js-collapse-search-term",this.searchField=null,this.searchForm=null,this.stateCache=new Map,S.ready().then(()=>{this.searchField=document.querySelector(this.searchValueSelector),this.searchField!==null&&(this.searchForm=this.searchField.closest("form"),this.searchField.value=h.get(this.searchField.dataset.persistCollapseSearchKey)??""),this.registerEventListener(),this.recoverStates()})}registerEventListener(){const a='.collapse[data-persist-collapse-state="true"]';new d("show.bs.collapse",t=>{const e=t.target;e.dataset.persistCollapseState==="true"&&(this.searchField!==null&&this.searchField.value===""||e.dataset.persistCollapseStateNotIfSearch===void 0)&&this.toStorage(e,!0)}).delegateTo(document,a),new d("hide.bs.collapse",t=>{const e=t.target;e.dataset.persistCollapseState==="true"&&(this.searchField!==null&&this.searchField.value===""||e.dataset.persistCollapseStateNotIfSearch===void 0)&&this.toStorage(e,!1)}).delegateTo(document,a),this.searchForm!==null&&new d("submit",t=>{t.preventDefault(),this.searchField!==null&&this.searchField.value===""&&this.recoverStates()}).bindTo(this.searchForm)}recoverStates(){document.querySelectorAll('.collapse[data-persist-collapse-state="true"]').forEach(t=>{const e=t.dataset.persistCollapseStateSuffix??this.localStorageKeyDefaultSuffix,l=this.fromStorage(e),s=t.id;if(s===""||!this.shallRecoverState(t))return;const o=(t.dataset.persistCollapseStateIfState??"shown")==="shown",i=(t.dataset.persistCollapseStateIfState??"hidden")==="hidden",c=t.classList.contains("show");if(o===!0){if(l[s]===!0){if(!c){const r=document.querySelector('[data-bs-target="#'+s+'"]');r.classList.remove("collapsed"),r.setAttribute("aria-expanded","true"),t.classList.add("show")}}else if(c){const r=document.querySelector('[data-bs-target="#'+s+'"]');r.classList.add("collapsed"),r.setAttribute("aria-expanded","false"),t.classList.remove("show")}}if(i===!0){if(l[s]===!1){if(c){const r=document.querySelector('[data-bs-target="#'+s+'"]');r.classList.add("collapsed"),r.setAttribute("aria-expanded","false"),t.classList.remove("show")}}else if(!c){const r=document.querySelector('[data-bs-target="#'+s+'"]');r.classList.remove("collapsed"),r.setAttribute("aria-expanded","true"),t.classList.add("show")}}})}shallRecoverState(a){return a.dataset.persistCollapseStateNotIfSearch===void 0||a.dataset.persistCollapseStateNotIfSearch==="false"?!0:this.searchField!==null&&this.searchField.value===""}fromStorage(a){let t;if(this.stateCache.has(this.localStorageKey+a))t=this.stateCache.get(this.localStorageKey+a);else{const e=h.get(this.localStorageKey+a);t=e!==null?JSON.parse(e):{},this.stateCache.set(this.localStorageKey+a,t)}return t}toStorage(a,t){const e=a.id,l=a.dataset.persistCollapseStateSuffix??this.localStorageKeyDefaultSuffix,s=this.fromStorage(l),o=(a.dataset.persistCollapseStateIfState??"shown")==="shown",i=(a.dataset.persistCollapseStateIfState??"hidden")==="hidden";t===!0&&o===!0&&s[e]!==!0&&(s[e]=!0,this.updateStates(this.localStorageKey+l,s)),t===!0&&i===!0&&s[e]===!1&&(delete s[e],this.updateStates(this.localStorageKey+l,s)),t===!1&&i===!0&&s[e]!==!1&&(s[e]=!1,this.updateStates(this.localStorageKey+l,s)),t===!1&&o===!0&&s[e]===!0&&(delete s[e],this.updateStates(this.localStorageKey+l,s))}updateStates(a,t){h.set(a,JSON.stringify(t)),this.stateCache.set(a,t)}}var n=new u;export{u as CollapseStatePersister,n as default}; diff --git a/Resources/Public/JavaScript/utility/collapse-state-search.js b/Resources/Public/JavaScript/utility/collapse-state-search.js new file mode 100644 index 0000000..a2601cc --- /dev/null +++ b/Resources/Public/JavaScript/utility/collapse-state-search.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import h from"@typo3/backend/storage/client.js";import f from"@typo3/core/document-service.js";import m from"@typo3/core/event/debounce-event.js";import d from"@typo3/core/event/regular-event.js";import p from"mark.js";import S from"@typo3/backend/utility/dom-helper.js";import y from"~labels/backend.messages";class g{constructor(){this.searchValueSelector=".t3js-collapse-search-term",this.searchValue="",this.markInstances=[],f.ready().then(()=>{if(this.treeContainers=document.querySelectorAll(".t3js-collapse-states-search-tree"),this.treeContainers.length!==0){this.numberOfSearchMatchesContainer=document.querySelectorAll(".t3js-collapse-states-search-numberOfSearchMatches"),this.searchField=document.querySelector(this.searchValueSelector),this.searchForm=this.searchField.closest("form"),this.searchSessionKey=this.searchField.dataset.persistCollapseSearchKey,this.searchValue=h.get(this.searchSessionKey)??"",this.registerEvents();for(let e=0;e{this.searchField.value===""&&this.searchForm.requestSubmit()}).bindTo(this.searchField),new m("input",()=>{this.searchForm.requestSubmit()}).bindTo(this.searchField),new d("submit",e=>{e.preventDefault();for(let t=0;t{if(r===null)return;const o=r.parentElement.querySelector('[data-bs-toggle="collapse"]')?.dataset.bsTarget;o!==void 0&&n.add(o.substring(1));const l=S.parents(r,".collapse");for(const i of l)n.add(i.id)});const u=Array.from(t.querySelectorAll(".collapse"));for(const r of u){const o=r.classList.contains("show"),l=r.id;if(n.has(l)){if(!o){const i=document.querySelector('[data-bs-target="#'+l+'"]');i.classList.remove("collapsed"),i.setAttribute("aria-expanded","true"),r.classList.add("show")}}else if(o){const i=document.querySelector('[data-bs-target="#'+l+'"]');i.classList.add("collapsed"),i.setAttribute("aria-expanded","false"),r.classList.remove("show")}}a.mark(e,{element:"span",className:"text-highlight"})}findNodesByIdentifier(e,t){return Array.from(t.querySelectorAll(".treelist-label")).filter(s=>s.textContent.toLowerCase().includes(e))}findNodesByValue(e,t){return Array.from(t.querySelectorAll(".treelist-value")).filter(a=>a.textContent.toLowerCase().includes(e)).map(a=>a.previousElementSibling)}findNodesByComment(e,t){return Array.from(t.querySelectorAll(".treelist-comment")).filter(s=>s.textContent.toLowerCase().includes(e))}findNodesByConstantSubstitution(e,t){return Array.from(t.querySelectorAll(".treelist-constant-substitution")).filter(s=>s.textContent.toLowerCase().includes(e))}}var b=new g;export{b as default}; diff --git a/Resources/Public/JavaScript/utility/dom-helper.js b/Resources/Public/JavaScript/utility/dom-helper.js new file mode 100644 index 0000000..df27dda --- /dev/null +++ b/Resources/Public/JavaScript/utility/dom-helper.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class o{static parents(t,e){const n=[];let l;for(;(l=t.parentElement.closest(e))!==null;)t=l,n.push(l);return n}static scrollableParent(t){let e=t.parentElement;for(;e;){const l=window.getComputedStyle(e).overflowY;if(l==="auto"||l==="scroll")return e;e=e.parentElement}return document.documentElement}static scrollEventTarget(t){const e=this.scrollableParent(t);return e===document.documentElement?document:e}static nextAll(t){const e=[];let n=t.nextElementSibling;for(;n!==null;)e.push(n),n=n.nextElementSibling;return e}static scrollIntoViewIfNeeded(t,e=!1){if(!e&&"scrollIntoViewIfNeeded"in t&&typeof t.scrollIntoViewIfNeeded=="function")t.scrollIntoViewIfNeeded(!0);else{const n=t.getBoundingClientRect();n.top>=0&&n.left>=0&&n.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&n.right<=(window.innerWidth||document.documentElement.clientWidth)||(e?t.scrollIntoView({behavior:"smooth",block:"center",inline:"center"}):t.scrollIntoView())}}}export{o as default}; diff --git a/Resources/Public/JavaScript/utility/drag-drop-utility.js b/Resources/Public/JavaScript/utility/drag-drop-utility.js new file mode 100644 index 0000000..bed53d0 --- /dev/null +++ b/Resources/Public/JavaScript/utility/drag-drop-utility.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class e{static isCopyModifierFromEvent(t){return t.dataTransfer.dropEffect==="copy"?!0:t.dataTransfer.dropEffect==="move"?!1:navigator.userAgent.includes("Mac")?t.dataTransfer.effectAllowed==="copy"||t.altKey:t.ctrlKey}static updateEventAndTooltipToReflectCopyMoveIntention(t){const o=e.isCopyModifierFromEvent(t);t.dataTransfer.dropEffect=o?"copy":"move",top.document.dispatchEvent(new CustomEvent("typo3:drag-tooltip:metadata-update",{detail:{statusIconIdentifier:o?"actions-duplicate":"actions-move"}}))}}export{e as default}; diff --git a/Resources/Public/JavaScript/utility/format-utility.js b/Resources/Public/JavaScript/utility/format-utility.js new file mode 100644 index 0000000..ae1f987 --- /dev/null +++ b/Resources/Public/JavaScript/utility/format-utility.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class s{static fileSizeAsString(t,B="iec"){const a={iec:{base:1024,labels:[" "," KiB"," MiB"," GiB"," TiB"," PiB"," EiB"," ZiB"," YiB"]},si:{base:1e3,labels:[" "," kB"," MB"," GB"," TB"," PB"," EB"," ZB"," YB"]}}[B],i=t===0?0:Math.floor(Math.log(t)/Math.log(a.base));return+(t/Math.pow(a.base,i)).toFixed(2)+a.labels[i]}}export{s as FormatUtility}; diff --git a/Resources/Public/JavaScript/utility/message-utility.js b/Resources/Public/JavaScript/utility/message-utility.js new file mode 100644 index 0000000..ad37112 --- /dev/null +++ b/Resources/Public/JavaScript/utility/message-utility.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class i{static getOrigin(){return window.origin}static verifyOrigin(n){return i.getOrigin()===n}static send(n,r=window){r.postMessage(n,i.getOrigin())}}export{i as MessageUtility}; diff --git a/Resources/Public/JavaScript/utility/top-level-module-import.js b/Resources/Public/JavaScript/utility/top-level-module-import.js new file mode 100644 index 0000000..3487da7 --- /dev/null +++ b/Resources/Public/JavaScript/utility/top-level-module-import.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +function o(t){const e=new CustomEvent("typo3:import-javascript-module",{detail:{specifier:t,importPromise:null}});return top.document.dispatchEvent(e),e.detail.importPromise?e.detail.importPromise:Promise.reject(new Error("Top level did not respond with a promise."))}export{o as topLevelModuleImport}; diff --git a/Resources/Public/JavaScript/viewport.js b/Resources/Public/JavaScript/viewport.js new file mode 100644 index 0000000..97483e1 --- /dev/null +++ b/Resources/Public/JavaScript/viewport.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import t from"@typo3/backend/viewport/content-container.js";import e from"@typo3/backend/event/consumer-scope.js";import n from"@typo3/backend/viewport/loader.js";import i from"@typo3/backend/viewport/navigation-container.js";import r from"@typo3/backend/viewport/topbar.js";class a{constructor(){this.Loader=n,this.NavigationContainer=null,this.ContentContainer=null,this.consumerScope=e,this.Topbar=new r,this.NavigationContainer=new i(this.consumerScope),this.ContentContainer=new t(this.consumerScope)}}let o;!top.TYPO3||!top.TYPO3.Backend?(o=new a,typeof top.TYPO3<"u"&&(top.TYPO3.Backend=o)):o=top.TYPO3.Backend;var p=o;export{p as default}; diff --git a/Resources/Public/JavaScript/viewport/abstract-container.js b/Resources/Public/JavaScript/viewport/abstract-container.js new file mode 100644 index 0000000..72199d5 --- /dev/null +++ b/Resources/Public/JavaScript/viewport/abstract-container.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import r from"@typo3/backend/event/consumer-scope.js";class c{constructor(o){this.consumerScope=r,this.consumerScope=o}}export{c as AbstractContainer}; diff --git a/Resources/Public/JavaScript/viewport/content-container.js b/Resources/Public/JavaScript/viewport/content-container.js new file mode 100644 index 0000000..b5d4f05 --- /dev/null +++ b/Resources/Public/JavaScript/viewport/content-container.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{ScaffoldIdentifierEnum as o}from"@typo3/backend/enum/viewport/scaffold-identifier.js";import{AbstractContainer as u}from"@typo3/backend/viewport/abstract-container.js";import m from"@typo3/backend/event/client-request.js";import c from"@typo3/backend/event/interaction-request.js";import l from"@typo3/backend/viewport/loader.js";import i from"@typo3/backend/event/trigger-request.js";class f extends u{get(){return document.querySelector(o.contentModuleIframe).contentWindow}beforeSetUrl(e){return this.consumerScope.invoke(new i("typo3.beforeSetUrl",e))}setUrl(e,t,r){e instanceof URL&&(e=e.toString());const n=this.resolveRouterElement();if(n===null&&self!==top)return Promise.reject(new Error("Content container used in unsupported frame context"));t instanceof c||(t=new m("typo3.setUrl",null));const s=this.consumerScope.invoke(new i("typo3.setUrl",t));return s.then(()=>{n!==null?(l.start(),n.setAttribute("endpoint",e),n.setAttribute("module",r||null),n.parentElement.addEventListener("typo3-module-loaded",()=>l.finish(),{once:!0})):document.location.assign(e)}),s}getUrl(){return this.resolveRouterElement().getAttribute("endpoint")}refresh(e){const t=this.resolveIFrameElement();if(t===null)return Promise.reject();const r=this.consumerScope.invoke(new i("typo3.refresh",e));return r.then(()=>{t.contentWindow.location.reload()}),r}getIdFromUrl(){if(this.getUrl()){const e=new URL(this.getUrl(),window.location.origin).searchParams.get("id")??"";return parseInt(e,10)}return 0}resolveIFrameElement(){return document.querySelector(o.contentModuleIframe)}resolveRouterElement(){return document.querySelector(o.contentModuleRouter)}}export{f as default}; diff --git a/Resources/Public/JavaScript/viewport/content-navigation-toggle.js b/Resources/Public/JavaScript/viewport/content-navigation-toggle.js new file mode 100644 index 0000000..5f5bafd --- /dev/null +++ b/Resources/Public/JavaScript/viewport/content-navigation-toggle.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as d,nothing as g}from"lit";import{property as b,state as m,customElement as f}from"lit/decorators.js";import{PseudoButtonLitElement as x}from"@typo3/backend/element/pseudo-button.js";import{ContentNavigationSlotEnum as u,NavigationStateChangeEvent as p,NavigationToggleEvent as v}from"@typo3/backend/viewport/content-navigation.js";import"@typo3/backend/element/icon-element.js";var h=function(i,t,e,o){var r=arguments.length,n=r<3?t:o===null?o=Object.getOwnPropertyDescriptor(t,e):o,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(i,t,e,o);else for(var l=i.length-1;l>=0;l--)(c=i[l])&&(n=(r<3?c(n):r>3?c(t,e,n):c(t,e))||n);return r>3&&n&&Object.defineProperty(t,e,n),n},s;(function(i){i.collapse="collapse",i.expand="expand"})(s||(s={}));let a=class extends x{constructor(){super(...arguments),this.context=null,this.mutationObserver=null,this.resizeObserver=null,this.boundStateChangeHandler=this.handleStateChange.bind(this),this.boundFocusRequestHandler=this.handleFocusRequest.bind(this),this.pageHideHandler=()=>{this.releaseTopLevelListeners()}}connectedCallback(){if(super.connectedCallback(),this.hidden=!0,!this.action){console.error(' requires an "action" attribute (collapsed or expanded)');return}this.discoverContext(),this.setupStateSync(),this.setupFocusListener(),this.setupResizeObserver(),window.addEventListener("pagehide",this.pageHideHandler)}disconnectedCallback(){super.disconnectedCallback(),this.releaseTopLevelListeners(),window.removeEventListener("pagehide",this.pageHideHandler)}render(){if(!this.context||!this.action)return d`${g}`;const t=this.action===s.collapse?"actions-panel-collapse-start":"actions-panel-expand-start";return d``}buttonActivated(){this.context?.contentNavigation.toggleNavigation()}shouldBeVisible(){if(!this.context||!this.action)return!1;const{contentNavigation:t}=this.context;return this.action===s.collapse?t.shouldShowCollapseButton():t.shouldShowExpandButton()}shouldRender(){return this.context!==null&&!this.hidden}discoverContext(){const t=this.findContentNavigation();t&&(this.context={contentNavigation:t,slot:this.detectSlot(t)},this.updateVisibility())}findContentNavigation(){const t=this.closest("typo3-backend-content-navigation");if(t)return t;try{const e=window.frameElement;if(e)return e.closest("typo3-backend-content-navigation")}catch{}return null}detectSlot(t){let e=this.parentElement;for(;e!==null;){if(e.parentElement===t){if(e.getAttribute("slot")===u.navigation)return u.navigation;break}e=e.parentElement}return u.content}getTargetDocument(){try{return window.top?.document??document}catch{return document}}setupStateSync(){if(!this.context)return;const{contentNavigation:t}=this.context;this.mutationObserver=new MutationObserver(e=>{for(const o of e)o.type==="attributes"&&this.updateVisibility()}),this.mutationObserver.observe(t,{attributes:!0,attributeFilter:["navigation-collapsed","navigation-hidden"]}),this.getTargetDocument().addEventListener(p.eventName,this.boundStateChangeHandler)}cleanupStateSync(){this.mutationObserver&&(this.mutationObserver.disconnect(),this.mutationObserver=null),this.getTargetDocument().removeEventListener(p.eventName,this.boundStateChangeHandler)}releaseTopLevelListeners(){this.cleanupStateSync(),this.cleanupFocusListener(),this.cleanupResizeObserver()}handleStateChange(t){const{contentNavigation:e}=this.context||{};e&&t.target===e&&this.updateVisibility()}updateVisibility(){this.hidden=!this.shouldBeVisible(),this.updateTitle()}updateTitle(){if(!this.context||!this.action)return;const{contentNavigation:t}=this.context;this.title=this.action===s.collapse?t.navigationLabelCollapse:t.navigationLabelExpand}setupFocusListener(){this.context&&this.context.contentNavigation.addEventListener(v.eventName,this.boundFocusRequestHandler)}cleanupFocusListener(){this.context&&this.context.contentNavigation.removeEventListener(v.eventName,this.boundFocusRequestHandler)}setupResizeObserver(){this.context&&(this.resizeObserver=new ResizeObserver(()=>{this.updateVisibility()}),this.resizeObserver.observe(this.context.contentNavigation),this.updateVisibility())}cleanupResizeObserver(){this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null)}handleFocusRequest(t){const{slot:e}=this.context||{};e===t.detail.focusTarget&&this.shouldRender()&&this.updateComplete.then(()=>{this.focus()})}};h([b({type:String})],a.prototype,"action",void 0),h([m()],a.prototype,"context",void 0),a=h([f("typo3-backend-content-navigation-toggle")],a);export{a as ContentNavigationToggle,s as ContentNavigationToggleActionEnum}; diff --git a/Resources/Public/JavaScript/viewport/content-navigation.js b/Resources/Public/JavaScript/viewport/content-navigation.js new file mode 100644 index 0000000..aeada51 --- /dev/null +++ b/Resources/Public/JavaScript/viewport/content-navigation.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as W,css as b,html as g}from"lit";import{property as d,query as f,state as w,customElement as C}from"lit/decorators.js";import{classMap as m}from"lit/directives/class-map.js";import{styleMap as M}from"lit/directives/style-map.js";import y from"@typo3/backend/storage/persistent.js";import x from"~labels/backend.messages";var o=function(r,t,i,e){var s=arguments.length,n=s<3?t:e===null?e=Object.getOwnPropertyDescriptor(t,i):e,l;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(r,t,i,e);else for(var c=r.length-1;c>=0;c--)(l=r[c])&&(n=(s<3?l(n):s>3?l(t,i,n):l(t,i))||n);return s>3&&n&&Object.defineProperty(t,i,n),n},u,h;(function(r){r.navigation="navigation",r.content="content"})(h||(h={}));class v extends CustomEvent{static{this.eventName="typo3:content-navigation:toggle"}constructor(t){super(v.eventName,{bubbles:!1,detail:{focusTarget:t}})}}class p extends CustomEvent{static{this.eventName="typo3:content-navigation:state-change"}constructor(t,i,e){super(p.eventName,{bubbles:!0,composed:!0,detail:{collapsed:t,hidden:i,identifier:e}})}}let a=class extends W{constructor(){super(...arguments),this.identifier="",this.navigationMinWidth=280,this.navigationHidden=!1,this.navigationCollapsed=!1,this.navigationLabelCollapse=x.get("viewport.navigation.hide"),this.navigationLabelExpand=x.get("viewport.navigation.show"),this.resizing=!1,this.resizeReferencePosition=0,this.startResize=t=>{if(this.isFlyoutMode()||t.button!==0)return;t.stopPropagation(),t.preventDefault();const i=this.shadowRoot?.querySelector('[data-panel="navigation"]');if(i){const s=i.getBoundingClientRect();this.resizeReferencePosition=this.isRtl()?s.right:s.left}this.resizing=!0;const e=t.target;e.setPointerCapture(t.pointerId),e.addEventListener("pointermove",this.handlePointerMove),e.addEventListener("pointerup",this.handlePointerUp),e.addEventListener("pointercancel",this.handlePointerUp),e.addEventListener("lostpointercapture",this.handlePointerUp)},this.handlePointerMove=t=>{this.resizeNavigation(t.clientX)},this.handlePointerUp=t=>{const i=t.currentTarget;i.removeEventListener("pointermove",this.handlePointerMove),i.removeEventListener("pointerup",this.handlePointerUp),i.removeEventListener("pointercancel",this.handlePointerUp),i.removeEventListener("lostpointercapture",this.handlePointerUp),this.stopResize()},this.resizeNavigation=t=>{if(!this.resizing)return;const i=this.shadowRoot?.querySelector('[data-panel="navigation"]');if(!i)return;const e=this.navigationMinWidth,s=this.getMaxWidth();let n=this.isRtl()?Math.round(this.resizeReferencePosition-t):Math.round(t-this.resizeReferencePosition);n=Math.max(e,Math.min(n,s)),i.style.width=`${n}px`,this.navigationWidth=n},this.stopResize=()=>{this.resizing=!1,this.persistWidth()},this.handleWindowResize=()=>{if(this.navigationWidth&&!this.isFlyoutMode()){const t=this.getMaxWidth();this.navigationWidth>t&&this.setNavigationWidth(t)}},this.handleNodeSelected=()=>{this.isFlyoutMode()&&!this.navigationCollapsed&&this.collapseNavigation()}}static{u=this}static{this.styles=b`:host{--content-navigation-divider-color:transparent;--content-navigation-divider-color-active:currentColor;--content-navigation-divider-width:0.5rem;--content-navigation-flyout-box-shadow:0 8px 16px rgba(0,0,0,.15);display:flex;position:relative;width:100%;height:100%;container-type:inline-size}:host([resizing]){cursor:col-resize;user-select:none}:host([resizing]) *{cursor:col-resize}.panel{height:100%}.panel--navigation{position:relative;flex:0 0 auto}.panel--content{position:relative;flex:1 1 auto;min-width:0}.panel--content ::slotted(*){position:relative}.panel--collapsed{display:none}.divider{position:relative;z-index:1;flex:0 0 1px;background-color:var(--content-navigation-divider-color)}.divider-handle{position:absolute;inset-block:0;inset-inline:calc(var(--content-navigation-divider-width)*-.5);width:var(--content-navigation-divider-width);cursor:col-resize;touch-action:none;transition:background-color .2s ease-in-out}.divider-handle:hover,.divider.resizing .divider-handle{background-color:var(--content-navigation-divider-color-active)}@container (max-width: 750px){.panel--navigation{position:absolute;inset-block:0;inset-inline-start:0;z-index:2;border-inline-end:1px solid var(--content-navigation-divider-color);box-shadow:var(--content-navigation-flyout-box-shadow);transition:transform .2s ease-in-out,box-shadow .2s ease-in-out;max-width:100%}.panel--navigation.panel--collapsed{display:block;transform:translateX(-100%);box-shadow:none}:host(:dir(rtl)) .panel--navigation.panel--collapsed,:host([dir=rtl]) .panel--navigation.panel--collapsed{transform:translateX(100%)}.divider{display:none}.panel--content{flex:1 1 100%;z-index:1}}`}static{this.FLYOUT_BREAKPOINT=750}connectedCallback(){super.connectedCallback(),this.loadPersistedWidth(),window.addEventListener("resize",this.handleWindowResize,{passive:!0}),this.addEventListener("typo3:tree:node-selected",this.handleNodeSelected)}disconnectedCallback(){super.disconnectedCallback(),window.removeEventListener("resize",this.handleWindowResize),this.removeEventListener("typo3:tree:node-selected",this.handleNodeSelected)}expandNavigation(){this.navigationCollapsed&&this.toggleNavigation()}collapseNavigation(){this.navigationCollapsed||this.toggleNavigation()}toggleNavigation(){this.navigationCollapsed=!this.navigationCollapsed;const t=this.navigationCollapsed?h.content:h.navigation;this.updateComplete.then(()=>{this.dispatchEvent(new v(t))})}isCollapsed(){return this.navigationCollapsed}showNavigation(){this.navigationHidden=!1}hideNavigation(){this.navigationHidden=!0}isNavigationHidden(){return this.navigationHidden}isFlyoutMode(){return this.getBoundingClientRect().width${this.navigationCollapsed?"":g`
    `}`}
    `}updated(t){super.updated(t),(t.has("navigationCollapsed")||t.has("navigationHidden"))&&this.dispatchEvent(new p(this.navigationCollapsed,this.navigationHidden,this.identifier))}getPersistenceKey(){return this.identifier?`resize.${this.identifier}.navigation`:null}loadPersistedWidth(){const t=this.getPersistenceKey();if(t){const i=y.get(t);if(i){const e=parseInt(i,10);if(!isNaN(e)&&e>0){this.navigationWidth=e;return}}}this.navigationInitialWidth&&!this.navigationWidth&&(this.navigationWidth=this.navigationInitialWidth)}persistWidth(){const t=this.getPersistenceKey();t&&this.navigationWidth&&y.set(t,String(this.navigationWidth))}getMaxWidth(){const t=this.getBoundingClientRect().width;let i=Math.round(t/2);return this.navigationMaxWidth&&(i=Math.min(i,this.navigationMaxWidth)),i}isRtl(){return getComputedStyle(this).direction==="rtl"}updateNavigationElement(t){const i=this.shadowRoot?.querySelector('[data-panel="navigation"]');i&&(i.style.width=`${t}px`)}};o([d({type:String})],a.prototype,"identifier",void 0),o([d({type:Number,attribute:"navigation-min-width"})],a.prototype,"navigationMinWidth",void 0),o([d({type:Number,attribute:"navigation-max-width"})],a.prototype,"navigationMaxWidth",void 0),o([d({type:Number,attribute:"navigation-initial-width"})],a.prototype,"navigationInitialWidth",void 0),o([d({type:Boolean,attribute:"navigation-hidden",reflect:!0})],a.prototype,"navigationHidden",void 0),o([d({type:Boolean,attribute:"navigation-collapsed",reflect:!0})],a.prototype,"navigationCollapsed",void 0),o([d({type:String,attribute:"navigation-label-collapse"})],a.prototype,"navigationLabelCollapse",void 0),o([d({type:String,attribute:"navigation-label-expand"})],a.prototype,"navigationLabelExpand",void 0),o([d({type:Boolean,reflect:!0})],a.prototype,"resizing",void 0),o([f('slot[name="navigation"]')],a.prototype,"navigationSlot",void 0),o([f('slot[name="content"]')],a.prototype,"contentSlot",void 0),o([w()],a.prototype,"navigationWidth",void 0),a=u=o([C("typo3-backend-content-navigation")],a);export{a as ContentNavigation,h as ContentNavigationSlotEnum,p as NavigationStateChangeEvent,v as NavigationToggleEvent}; diff --git a/Resources/Public/JavaScript/viewport/loader.js b/Resources/Public/JavaScript/viewport/loader.js new file mode 100644 index 0000000..0c4a67c --- /dev/null +++ b/Resources/Public/JavaScript/viewport/loader.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{ScaffoldContentArea as t}from"@typo3/backend/enum/viewport/scaffold-identifier.js";import"@typo3/backend/element/progress-bar-element.js";class e{static{this.el=null}static start(){(!this.el||!this.el.isConnected)&&(this.el=document.createElement("typo3-backend-progress-bar"),t.getContentContainer()?.appendChild(this.el)),this.el.start(!0)}static async finish(){this.el&&(await this.el.done(),this.el.isRunning()||(this.el=null))}}export{e as default}; diff --git a/Resources/Public/JavaScript/viewport/navigation-container.js b/Resources/Public/JavaScript/viewport/navigation-container.js new file mode 100644 index 0000000..9a37c6b --- /dev/null +++ b/Resources/Public/JavaScript/viewport/navigation-container.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{ScaffoldContentArea as c}from"@typo3/backend/enum/viewport/scaffold-identifier.js";import{AbstractContainer as m}from"@typo3/backend/viewport/abstract-container.js";import l from"@typo3/backend/event/trigger-request.js";import{selector as p}from"@typo3/core/literals.js";class h extends m{constructor(t){super(t),this.activeComponentId=""}get contentNavigation(){return c.getContentNavigation()}get navigationContainer(){return c.getNavigationContainer()}showComponent(t){const n=this.contentNavigation,e=this.navigationContainer;if(!n||!e||(this.show(t),t===this.activeComponentId))return;if(this.activeComponentId!==""){const o=e.querySelector("#navigationComponent-"+this.activeComponentId.replace(/[/@]/g,"_"));o&&(o.style.display="none")}const i="navigationComponent-"+t.replace(/[/@]/g,"_");if(e.querySelectorAll(p`[data-component="${t}"]`).length===1){this.show(t),this.activeComponentId=t;return}import(t+".js").then(o=>{if(typeof o.navigationComponentName=="string"){const r=o.navigationComponentName,s=document.createElement(r);s.setAttribute("id",i),s.dataset.component=t,e.append(s)}else e.insertAdjacentHTML("beforeend",'
    '),Object.values(o)[0].initialize("#"+i);this.show(t),this.activeComponentId=t})}hide(){this.contentNavigation?.hideNavigation()}show(t){const n=this.contentNavigation,e=this.navigationContainer;if(!n||!e)return;e.querySelectorAll("[data-component]").forEach(i=>i.style.display="none"),n.showNavigation();const a=e.querySelector('[data-component="'+t+'"]');a&&(a.style.display=null)}setUrl(t,n){const e=this.consumerScope.invoke(new l("typo3.setUrl",n));return e.then(()=>{this.contentNavigation?.showNavigation()}),e}}export{h as default}; diff --git a/Resources/Public/JavaScript/viewport/scaffold-state.js b/Resources/Public/JavaScript/viewport/scaffold-state.js new file mode 100644 index 0000000..5122a12 --- /dev/null +++ b/Resources/Public/JavaScript/viewport/scaffold-state.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{ScaffoldIdentifierEnum as c}from"@typo3/backend/enum/viewport/scaffold-identifier.js";import n from"@typo3/backend/storage/persistent.js";var t;(function(l){l.sidebarExpanded="scaffold-sidebar-expanded",l.sidebarFlyout="scaffold-sidebar-flyout",l.toolbarExpanded="scaffold-toolbar-expanded"})(t||(t={}));class i extends CustomEvent{static{this.eventName="typo3:scaffold:sidebar:toggle"}constructor(e){super(i.eventName,{detail:{expanded:e},bubbles:!0,composed:!0})}}class a extends CustomEvent{static{this.eventName="typo3:scaffold:toolbar:toggle"}constructor(e){super(a.eventName,{detail:{expanded:e},bubbles:!0,composed:!0})}}class r extends CustomEvent{static{this.eventName="typo3:scaffold:toolbar:toggle-request"}constructor(){super(r.eventName,{bubbles:!0,composed:!0})}}class o extends CustomEvent{static{this.eventName="typo3:scaffold:search:toggle-request"}constructor(){super(o.eventName,{bubbles:!0,composed:!0})}}class d{static{this.STORAGE_KEY="typo3-sidebar-collapsed"}static{this.LARGE_SCREEN_QUERY="(min-width: 992px)"}static{this.mediaQuery=null}static{this.sidebarPreferExpanded=!0}static isSidebarExpanded(){return this.getScaffold()?.classList.contains(t.sidebarExpanded)??!1}static isSidebarFlyout(){return this.getScaffold()?.classList.contains(t.sidebarFlyout)??!1}static isSidebarVisible(){return this.isLargeScreen()?!0:this.isSidebarFlyout()}static isToolbarExpanded(){return this.getScaffold()?.classList.contains(t.toolbarExpanded)??!1}static isLargeScreen(){return window.matchMedia(this.LARGE_SCREEN_QUERY).matches}static initialize(){if(this.mediaQuery=window.matchMedia(this.LARGE_SCREEN_QUERY),this.mediaQuery.addEventListener("change",this.handleMediaQueryChange),document.body.dataset.context==="install"){const e=localStorage.getItem(this.STORAGE_KEY)==="true";this.sidebarPreferExpanded=!e}else this.sidebarPreferExpanded=this.isSidebarExpanded();this.initializeOverlay(),this.initializeEventListeners(),this.applyStateForViewport()}static toggleSidebar(e){this.getScaffold()&&(this.isLargeScreen()?this.toggleSidebarExpanded(e):this.toggleSidebarFlyout(e))}static toggleToolbar(e){const s=this.getScaffold();s&&(this.isLargeScreen()||(typeof e>"u"&&(e=!this.isToolbarExpanded()),s.classList.toggle(t.toolbarExpanded,e),e&&(s.classList.remove(t.sidebarExpanded),this.toggleSidebarFlyout(!1)),document.dispatchEvent(new a(e))))}static collapseAll(){const e=this.getScaffold();e&&e.classList.remove(t.sidebarExpanded,t.sidebarFlyout,t.toolbarExpanded)}static toggleSidebarExpanded(e){const s=this.getScaffold();s&&(e=e??!this.isSidebarExpanded(),this.sidebarPreferExpanded=e,s.classList.toggle(t.sidebarExpanded,e),s.classList.remove(t.sidebarFlyout,t.toolbarExpanded),this.persistSidebarState(e),document.dispatchEvent(new i(e)))}static toggleSidebarFlyout(e){const s=this.getScaffold();s&&(this.isLargeScreen()||(e=e??!this.isSidebarFlyout(),s.classList.toggle(t.sidebarFlyout,e),e&&s.classList.remove(t.sidebarExpanded,t.toolbarExpanded),document.dispatchEvent(new i(e))))}static initializeEventListeners(){document.addEventListener("typo3-module-load",()=>{this.toggleSidebarFlyout(!1),this.toggleToolbar(!1)}),document.addEventListener(r.eventName,()=>{this.toggleToolbar()}),document.addEventListener(o.eventName,()=>{this.collapseAll()})}static applyStateForViewport(){const e=this.getScaffold();e&&(this.isLargeScreen()?(e.classList.remove(t.sidebarFlyout),e.classList.toggle(t.sidebarExpanded,this.sidebarPreferExpanded)):e.classList.remove(t.sidebarExpanded,t.sidebarFlyout),document.dispatchEvent(new i(this.isSidebarVisible())))}static{this.handleMediaQueryChange=()=>{d.applyStateForViewport()}}static persistSidebarState(e){document.body.dataset.context==="install"?localStorage.setItem(this.STORAGE_KEY,e?"false":"true"):n.set("BackendComponents.States.typo3-sidebar",{collapsed:!e})}static getScaffold(){return document.querySelector(c.scaffold)}static initializeOverlay(){document.querySelector(".scaffold-overlay")?.addEventListener("click",s=>{s.preventDefault(),d.toggleSidebar(!1)})}}export{i as ScaffoldSidebarToggleEvent,d as ScaffoldState,t as ScaffoldStateClass,a as ScaffoldToolbarToggleEvent,o as SearchToggleRequestEvent,r as ToolbarToggleRequestEvent}; diff --git a/Resources/Public/JavaScript/viewport/toolbar.js b/Resources/Public/JavaScript/viewport/toolbar.js new file mode 100644 index 0000000..ff9c942 --- /dev/null +++ b/Resources/Public/JavaScript/viewport/toolbar.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{ScaffoldIdentifierEnum as m}from"@typo3/backend/enum/viewport/scaffold-identifier.js";import d from"@typo3/core/document-service.js";import u from"@typo3/core/event/regular-event.js";import{ModuleUtility as l,ModuleSelector as a}from"@typo3/backend/module.js";import{selector as c}from"@typo3/core/literals.js";import s from"@typo3/backend/module-menu.js";class n{static{this.toolbarSelector=".t3js-scaffold-toolbar"}constructor(){d.ready().then(()=>{this.initializeEvents()})}registerEvent(t){d.ready().then(()=>{t()}),new u("t3-topbar-update",t).bindTo(document.querySelector(m.header))}initializeEvents(){const t=document.querySelector(n.toolbarSelector);if(t===null)return;new u("click",(r,e)=>{r.preventDefault();const i=l.getRouteFromElement(e);s.App.showModule(i.identifier,i.params,r)}).delegateTo(t,a.link);const o=r=>{const e=r.detail.module;!e||!l.getFromName(e).link||this.highlightModule(e)};document.addEventListener("typo3-module-load",o),document.addEventListener("typo3-module-loaded",o)}highlightModule(t){const o=document.querySelector(n.toolbarSelector);if(o===null)return;o.querySelectorAll(a.link+".dropdown-item").forEach(e=>{e.classList.remove("active"),e.removeAttribute("aria-current")});const r=l.getFromName(t);this.highlightModuleItem(o,r,!0)}highlightModuleItem(t,o,r){const e=t.querySelectorAll(a.link+c`[data-moduleroute-identifier="${o.name}"].dropdown-item`);return e.forEach(i=>{i.classList.add("active"),r&&i.setAttribute("aria-current","location")}),e.length>0&&(r=!1),o.parent!==""&&this.highlightModuleItem(t,l.getFromName(o.parent),r),e.length>0}}export{n as default}; diff --git a/Resources/Public/JavaScript/viewport/topbar.js b/Resources/Public/JavaScript/viewport/topbar.js new file mode 100644 index 0000000..c7b9e4a --- /dev/null +++ b/Resources/Public/JavaScript/viewport/topbar.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{ScaffoldIdentifierEnum as a}from"@typo3/backend/enum/viewport/scaffold-identifier.js";import n from"@typo3/backend/viewport/toolbar.js";import s from"@typo3/core/ajax/ajax-request.js";class e{static{this.topbarSelector=a.header}constructor(){this.Toolbar=new n}refresh(){new s(TYPO3.settings.ajaxUrls.topbar).get().then(async r=>{const o=await r.resolve(),t=document.querySelector(e.topbarSelector);t!==null&&(t.innerHTML=o.topbar,t.dispatchEvent(new Event("t3-topbar-update")))})}}export{e as default}; diff --git a/Resources/Public/JavaScript/window-manager.js b/Resources/Public/JavaScript/window-manager.js new file mode 100644 index 0000000..980d68f --- /dev/null +++ b/Resources/Public/JavaScript/window-manager.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import p from"@typo3/backend/utility.js";class a{constructor(){this.windows={},this.localOpen=(n,o,t="newTYPO3frontendWindow",l="")=>this._localOpen(n,o,t,l)}open(...n){return this._localOpen.apply(null,n)}globalOpen(...n){return this._localOpen.apply(null,n)}_localOpen(n,o,t="newTYPO3frontendWindow",l=""){if(!n)return null;o===null?o=!window.opener:o===void 0&&(o=!0);const e=this.windows[t]??window.open("",t,l);let r=!1;try{r=e.constructor.name==="Window"}catch{}const s=r&&!e.closed?e.location.href:null;if(p.urlsPointToSameServerSideResource(n,s))return e.location.replace(n),e.location.reload(),e.focus(),e;const i=window.open(n,t,l);return this.windows[t]=i,o&&i.focus(),i}}const d=new a;top.TYPO3.WindowManager||(top.document===window.document?top.TYPO3.WindowManager=d:top.TYPO3.WindowManager=new a);export{d as default}; diff --git a/Resources/Public/JavaScript/wizard/events/auto-advance-event.js b/Resources/Public/JavaScript/wizard/events/auto-advance-event.js new file mode 100644 index 0000000..6c87d17 --- /dev/null +++ b/Resources/Public/JavaScript/wizard/events/auto-advance-event.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class e extends CustomEvent{constructor(){super("auto-advance",{bubbles:!0,composed:!0})}}export{e as AutoAdvanceEvent}; diff --git a/Resources/Public/JavaScript/wizard/events/before-next-step-event.js b/Resources/Public/JavaScript/wizard/events/before-next-step-event.js new file mode 100644 index 0000000..ad31dee --- /dev/null +++ b/Resources/Public/JavaScript/wizard/events/before-next-step-event.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class e extends CustomEvent{static{this.eventName="wizard-before-next-step"}constructor(t,s){super(e.eventName,{detail:{currentStepKey:t,currentStepIndex:s}})}}export{e as BeforeNextStepEvent}; diff --git a/Resources/Public/JavaScript/wizard/events/step-summary-event.js b/Resources/Public/JavaScript/wizard/events/step-summary-event.js new file mode 100644 index 0000000..c3a9298 --- /dev/null +++ b/Resources/Public/JavaScript/wizard/events/step-summary-event.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +class e extends CustomEvent{static{this.eventName="wizard-step-summary"}constructor(t){super(e.eventName,{detail:{summaryData:t},bubbles:!0,composed:!0})}}export{e as StepSummaryEvent}; diff --git a/Resources/Public/JavaScript/wizard/finisher/noop-finisher.js b/Resources/Public/JavaScript/wizard/finisher/noop-finisher.js new file mode 100644 index 0000000..e254980 --- /dev/null +++ b/Resources/Public/JavaScript/wizard/finisher/noop-finisher.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as n}from"lit";class o{setConfig(e){this.config=e}async render(){return n``}async execute(){window.top&&window.top!==window.self&&window.top.TYPO3?.Backend?.ContentContainer?window.top.TYPO3.Backend.ContentContainer.refresh():window.location.reload()}}export{o as default}; diff --git a/Resources/Public/JavaScript/wizard/finisher/redirect-finisher.js b/Resources/Public/JavaScript/wizard/finisher/redirect-finisher.js new file mode 100644 index 0000000..7aa3c6c --- /dev/null +++ b/Resources/Public/JavaScript/wizard/finisher/redirect-finisher.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as n}from"lit";class o{setConfig(e){this.config=e}async render(){return n``}async execute(){const e=this.config.data.url;if(!e){console.warn("Redirect finisher called without URL");return}window.opener&&!window.opener.closed&&window.self===window.top?(window.opener.location.href=e,window.close()):window.top&&window.top.TYPO3?.Backend?.ContentContainer?window.top.TYPO3.Backend.ContentContainer.setUrl(e):window.location.href=e}}export{o as default}; diff --git a/Resources/Public/JavaScript/wizard/finisher/reload-finisher.js b/Resources/Public/JavaScript/wizard/finisher/reload-finisher.js new file mode 100644 index 0000000..e254980 --- /dev/null +++ b/Resources/Public/JavaScript/wizard/finisher/reload-finisher.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as n}from"lit";class o{setConfig(e){this.config=e}async render(){return n``}async execute(){window.top&&window.top!==window.self&&window.top.TYPO3?.Backend?.ContentContainer?window.top.TYPO3.Backend.ContentContainer.refresh():window.location.reload()}}export{o as default}; diff --git a/Resources/Public/JavaScript/wizard/helper/dynamic-steps-loader.js b/Resources/Public/JavaScript/wizard/helper/dynamic-steps-loader.js new file mode 100644 index 0000000..81850cf --- /dev/null +++ b/Resources/Public/JavaScript/wizard/helper/dynamic-steps-loader.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import n from"@typo3/core/ajax/ajax-request.js";function i(r,a){return new n(TYPO3.settings.ajaxUrls.wizard_config).withQueryArguments({mode:r,data:a.getDataStore()}).get().then(e=>e.resolve()).then(async e=>await Promise.all(e.steps.map(async t=>{if(!t.module)throw new Error("Step data does not contain a module path");const{default:o}=await import(t.module);if(!o)throw new Error(`Step module ${t.module} does not export a default class`);return new o(a,t.configurationData)})))}export{i as loadDynamicSteps}; diff --git a/Resources/Public/JavaScript/wizard/move-content-element.js b/Resources/Public/JavaScript/wizard/move-content-element.js new file mode 100644 index 0000000..7732b4f --- /dev/null +++ b/Resources/Public/JavaScript/wizard/move-content-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import p from"@typo3/core/event/regular-event.js";import y from"@typo3/core/document-service.js";import h from"@typo3/backend/ajax-data-handler.js";import w from"@typo3/backend/modal.js";import b from"@typo3/backend/notification.js";import C from"@typo3/backend/action-button/immediate-action.js";import g from"@typo3/backend/viewport.js";import e from"~labels/backend.wizards.move_content_elements";import f from"~labels/core.misc";class E{constructor(){this.initialize()}async initialize(){await y.ready(),this.registerEvents(document.querySelector(".element-browser-body"))}registerEvents(l){new p("change",async r=>{const o=document.querySelector("#elementRecordTitle").value,n=new URL(window.location.href).searchParams.get("uid"),i=document.querySelector("h2");if(i){const t=[o,Number(n)];i.innerText=r.target.checked?e.get("headline.copy",t):e.get("headline.move",t)}const a=r.target.checked?f.get("copyElementToHere"):f.get("moveElementToHere");document.querySelectorAll('[data-action="paste"]').forEach(t=>{t.querySelector("span.t3js-button-label").textContent=a})}).delegateTo(l,"#makeCopy"),new p("click",async(r,o)=>{const m=document.querySelector("#makeCopy"),n=document.querySelector("#elementRecordTitle").value,i=document.querySelector("#pageRecordTitle").value,a=document.querySelector("#pageUid").value,t=new URL(window.location.href),d=t.searchParams.get("uid"),c=new URL(t.searchParams.get("returnUrl"),window.origin),s=m.checked,v=s?"copy":"move",u={cmd:{tt_content:{[d]:{[v]:o.dataset.position}}}};o.dataset.colpos!==void 0&&(u.data={tt_content:{[d]:{colPos:o.dataset.colpos}}}),h.process(u).then(()=>{w.dismiss(),b.success(s?e.get("moveElement.notification.elementCopied.title"):e.get("moveElement.notification.elementMoved.title"),s?e.get("moveElement.notification.elementCopied.message",[n]):e.get("moveElement.notification.elementMoved.message",[n]),10,[{label:e.get("moveElement.notification.elementPasted.action.dismiss")},{label:e.get("moveElement.notification.elementPasted.action.open",[i]),action:new C(()=>{c.searchParams.set("id",a),g.ContentContainer.setUrl(c)})}]),g.ContentContainer.setUrl(c)})}).delegateTo(l,'[data-action="paste"]')}}export{E as MoveContentElement}; diff --git a/Resources/Public/JavaScript/wizard/move-page.js b/Resources/Public/JavaScript/wizard/move-page.js new file mode 100644 index 0000000..1a47f17 --- /dev/null +++ b/Resources/Public/JavaScript/wizard/move-page.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import m from"@typo3/core/event/regular-event.js";import d from"@typo3/core/document-service.js";import p from"@typo3/backend/ajax-data-handler.js";import g from"@typo3/backend/modal.js";import i from"@typo3/backend/module-menu.js";import l from"@typo3/backend/notification.js";import u from"@typo3/backend/action-button/immediate-action.js";import e from"~labels/backend.wizards.move_page";class f{constructor(){this.initialize()}async initialize(){await d.ready(),this.registerEvents(document.querySelector(".element-browser-body"))}registerEvents(r){const o=document.querySelector("#elementRecordTitle").value,t=new URL(window.location.href);new m("click",async(P,s)=>{const a=document.querySelector("#makeCopy").checked,n=a?"copy":"move",c={cmd:{[t.searchParams.get("table")]:{[t.searchParams.get("uid")]:{[n]:s.dataset.position}}}};p.process(c).then(()=>{g.dismiss(),l.success(a?e.get("movePage.notification.pageCopied.title"):e.get("movePage.notification.pageMoved.title"),a?e.get("movePage.notification.pageCopied.message",[o]):e.get("movePage.notification.pageMoved.message",[o]),10,[{label:e.get("movePage.notification.pagePasted.action.dismiss")},{label:e.get("movePage.notification.pagePasted.action.open",[o]),action:new u(()=>{i.App.showModule("records","id="+t.searchParams.get("uid"))})}]),top.document.dispatchEvent(new CustomEvent("typo3:pagetree:refresh")),i.App.showModule("records","id="+t.searchParams.get("expandPage"))})}).delegateTo(r,'[data-action="paste"]')}}export{f as MovePage}; diff --git a/Resources/Public/JavaScript/wizard/steps/confirm-step.js b/Resources/Public/JavaScript/wizard/steps/confirm-step.js new file mode 100644 index 0000000..cbc10d8 --- /dev/null +++ b/Resources/Public/JavaScript/wizard/steps/confirm-step.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as i}from"lit";import{StepSummaryEvent as o}from"@typo3/backend/wizard/events/step-summary-event.js";import e from"~labels/backend.wizards.general";class r{constructor(t){this.wizard=t,this.key="confirm",this.title=e.get("step.confirmation.title"),this.autoAdvance=!1}isComplete(){return!0}render(){const t=new o(this.wizard.getStepSummaries());this.wizard.dispatchEvent(t);const s=t.detail.summaryData;return i`

    ${e.get("step.confirmation.headline")}

    ${e.get("step.confirmation.description")}

    ${s.map(a=>i``)}
    ${a.label}${a.value}
    `}}export{r as ConfirmStep,r as default}; diff --git a/Resources/Public/JavaScript/wizard/steps/finisher-step.js b/Resources/Public/JavaScript/wizard/steps/finisher-step.js new file mode 100644 index 0000000..e4b6a82 --- /dev/null +++ b/Resources/Public/JavaScript/wizard/steps/finisher-step.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{html as n,nothing as d}from"lit";import{until as a}from"lit/directives/until.js";import{Task as u,TaskStatus as o}from"@lit/task";import t from"~labels/backend.wizards.general";class h{constructor(r,i){this.wizard=r,this.finisher=i,this.key="finisher",this.title=t.get("step.finisher.title"),this.autoAdvance=!1,this.resetButtonTitle=null,this.finisherInstance=null,this.hasError=!1,this.task=new u(this.wizard,{task:async([e])=>e.execute(),args:()=>[this.finisher],autoRun:!1})}isComplete(){return this.task.status===o.COMPLETE}async beforeAdvance(){if(this.hasError){this.wizard.dismissWizard();return}if(!this.finisherInstance)throw new Error("Finisher instance not loaded");this.wizard.dismissWizard(),await this.finisherInstance.execute()}render(){return this.task.status===o.INITIAL&&this.task.run(),this.task.render({pending:()=>this.wizard.renderLoader(t.get("wizard.status.pending.message")),error:r=>(this.hasError=!0,this.wizard.renderError(t.get("wizard.status.error.message"),r)),complete:r=>r.success===!1?(this.hasError=!0,this.wizard.renderError(t.get("wizard.status.error.message"),r.errors)):(r?.finisher?.data?.resetButtonTitle&&(this.resetButtonTitle=String(r?.finisher?.data?.resetButtonTitle)),this.renderFinisher(r.finisher))})}renderFinisher(r){if(!this.finisherInstance){const i=this.loadFinisher(r).then(e=>(this.finisherInstance=e,e.render())).catch(e=>(console.error("Failed to load finisher:",e),this.hasError=!0,this.wizard.renderError(t.get("wizard.finisher.load_error.message"),e)));return n`${a(i,this.wizard.renderLoader(t.get("wizard.loading_finisher")))}`}return n`${a(this.finisherInstance.render(),d)}`}async loadFinisher(r){if(!r.module)throw new Error("Finisher data does not contain a module path");const e=(await import(r.module)).default;if(!e)throw new Error(`Finisher module ${r.module} does not export a default class`);const s=new e;return s.setConfig(r),s}}export{h as FinisherStep,h as default}; diff --git a/Resources/Public/JavaScript/wizard/wizard.js b/Resources/Public/JavaScript/wizard/wizard.js new file mode 100644 index 0000000..978a9d2 --- /dev/null +++ b/Resources/Public/JavaScript/wizard/wizard.js @@ -0,0 +1,16 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as f,html as a,nothing as m}from"lit";import{property as d,state as l,customElement as y}from"lit/decorators.js";import{classMap as S}from"lit/directives/class-map.js";import{createRef as b,ref as v}from"lit/directives/ref.js";import{KeyTypesEnum as g}from"@typo3/backend/enum/key-types.js";import k from"@typo3/backend/modal.js";import"@typo3/backend/element/alert-element.js";import"@typo3/backend/element/icon-element.js";import"@typo3/backend/element/progress-tracker-element.js";import"@typo3/backend/element/spinner-element.js";import w from"@typo3/backend/wizard/steps/confirm-step.js";import x from"@typo3/backend/wizard/steps/finisher-step.js";import c from"~labels/backend.wizards.general";import{BeforeNextStepEvent as T}from"@typo3/backend/wizard/events/before-next-step-event.js";var o=function(p,t,e,r){var s=arguments.length,i=s<3?t:r===null?r=Object.getOwnPropertyDescriptor(t,e):r,u;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")i=Reflect.decorate(p,t,e,r);else for(var h=p.length-1;h>=0;h--)(u=p[h])&&(i=(s<3?u(i):s>3?u(t,e,i):u(t,e))||i);return s>3&&i&&Object.defineProperty(t,e,i),i};let n=class extends f{constructor(){super(...arguments),this.steps=[],this.skipSummary=!1,this.currentStepIndex=0,this.dataStore={},this.allSteps=[],this.progressTracker=null,this.primaryButtonRef=b(),this.focusPendingStep=null,this.handleAutoAdvance=()=>{this.tryAutoAdvance()},this.handleContentKeydown=t=>{if(t.key!==g.ENTER)return;const e=t.target;!(e instanceof HTMLInputElement)||e.form||e.type==="search"||e.type==="checkbox"||(t.preventDefault(),this.handleNext())}}connectedCallback(){super.connectedCallback(),this.addEventListener("auto-advance",this.handleAutoAdvance),this.progressTracker=document.createElement("typo3-backend-progress-tracker")}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener("auto-advance",this.handleAutoAdvance),this.progressTracker=null}getStepSummaries(){const t=[];for(const e of this.allSteps)if(this.hasSummary(e)){const r=e.getSummaryData();t.push(...r)}return t}getStoreData(t){return this.dataStore[t]??null}setStoreData(t,e){this.dataStore={...this.dataStore,[t]:e}}getDataStore(){return this.dataStore}clearStoreData(t){const e={...this.dataStore};delete e[t],this.dataStore=e}tryAutoAdvance(){const t=this.getCurrentStep();t.autoAdvance&&t.isComplete()&&this.goToNextStep()}dismissWizard(){k.dismiss()}renderLoader(t){return a`

    ${t??c.get("wizard.loading")}

    `}renderError(t,e,r){let s="";e instanceof Error?s=e.message:Array.isArray(e)?s=e.join(` +`):e&&typeof e=="string"&&(s=e);const i=s?`${t} + +${s}`:t;return a`
    `}render(){return a`
    ${this.renderProgressTracker()}
    ${this.currentStep?.render()}
    ${this.renderWizardButtons()}
    `}goToNextStep(){this.currentStepIndex0&&this.goToStep(this.currentStepIndex-1)}createRenderRoot(){return this}async updated(t){super.updated(t),(t.has("steps")||t.has("submissionService"))&&this.steps?.length&&this.submissionService&&(this.allSteps=[...this.steps,...this.skipSummary?[]:[new w(this)],new x(this,this.submissionService)],this.progressTracker.requestUpdate(),this.currentStep||(this.currentStep=this.allSteps[this.currentStepIndex])),this.currentStep&&t.has("currentStep")&&(this.hasAfterRender(this.currentStep)&&(await this.currentStep.afterRender(),this.requestUpdate()),this.focusPendingStep=this.currentStep),await this.moveFocusIntoStep()}async moveFocusIntoStep(){const t=this.focusPendingStep;if(!t||t!==this.currentStep){this.focusPendingStep=null;return}await this.updateComplete,t===this.currentStep&&this.focusStepContent()&&(this.focusPendingStep=null)}focusStepContent(){const t=this.renderRoot.querySelector(".wizard-content");if(t){if(t.contains(this.ownerDocument.activeElement))return!0;const r=this.findFocusTarget(t);if(r)return r.setAttribute("autofocus",""),r.focus(),!0}return this.renderRoot.querySelector(".wizard-actions")?.contains(this.ownerDocument.activeElement)||this.primaryButtonRef.value?.focus(),!1}findFocusTarget(t){const e=r=>{for(const s of t.querySelectorAll(r))if(!s.matches(":disabled")&&s.checkVisibility())return s;return null};return e("[autofocus]")??e('input[type="radio"]:checked')??e('input:not([type="hidden"]):not([type="radio"]):not([readonly]), select, button, textarea:not([readonly]), [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"])')}hasAfterRender(t){return t&&typeof t.afterRender=="function"}hasSummary(t){return"getSummaryData"in t}hasValue(t){return"getValue"in t&&"setValue"in t&&"reset"in t}renderWizardButtons(){return a`${this.renderPreviousButton()} ${this.renderNextButtons()}`}renderPreviousButton(){const t=this.currentStepIndex===0,e=this.currentStepIndex===this.allSteps.length-1;return a``}renderNextButtons(){const t=this.currentStep?.isComplete();let e,r=m;if(this.currentStep?.key==="finisher"){e=c.get("wizard.buttons.finish");const s=this.currentStep;s?.resetButtonTitle&&(r=a``)}else this.currentStep?.key==="confirm"?e=this.confirmButtonLabel:e=c.get("wizard.buttons.next");return a`
    ${r}
    `}getCurrentStep(){return this.currentStep}async goToStep(t){if(!(t<0||t>=this.allSteps.length||t===this.currentStepIndex)){if(t>this.currentStepIndex){for(let r=this.currentStepIndex;rt;e--){const r=this.allSteps[e];this.hasValue(r)&&r.reset()}this.currentStepIndex=t,this.currentStep=this.allSteps[t],await this.updateComplete}}getProgressSteps(){return this.allSteps.map(t=>({key:t.key,title:t.title}))}getCurrentStepIndex(){return this.currentStepIndex}renderProgressTracker(){const t=this.getProgressSteps(),e=this.getCurrentStepIndex(),r=t.map(i=>i.title),s=e+1;return this.progressTracker&&(this.progressTracker.stages=r,this.progressTracker.activeStage=s),a`${this.progressTracker}`}handlePrevious(){const t=this.currentStepIndex===0,e=this.currentStepIndex===this.allSteps.length-1;t||e||this.goToPreviousStep()}async handleNext(){if(!this.currentStep.isComplete())return;if(this.currentStepIndex===this.allSteps.length-1){this.currentStep.beforeAdvance&&await this.currentStep.beforeAdvance();return}this.goToNextStep()}handleRestart(){this.dataStore={},this.goToStep(0).then(()=>this.updateComplete)}};o([d({type:Array,attribute:!1})],n.prototype,"steps",void 0),o([d({type:String,attribute:"confirm-button-label"})],n.prototype,"confirmButtonLabel",void 0),o([d({type:Boolean,attribute:"skip-summary"})],n.prototype,"skipSummary",void 0),o([d({type:Object,attribute:!1})],n.prototype,"submissionService",void 0),o([l()],n.prototype,"currentStepIndex",void 0),o([l()],n.prototype,"currentStep",void 0),o([l()],n.prototype,"dataStore",void 0),o([l()],n.prototype,"allSteps",void 0),n=o([y("typo3-backend-wizard")],n);export{n as Wizard}; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ecc3439 --- /dev/null +++ b/composer.json @@ -0,0 +1,76 @@ +{ + "name": "typo3/cms-backend", + "type": "typo3-cms-framework", + "description": "TYPO3 CMS Backend", + "homepage": "https://typo3.community/", + "funding": [ + { + "type": "membership", + "url": "https://typo3.org/membership" + } + ], + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "support": { + "issues": "https://forge.typo3.org/issues/", + "forum": "https://talk.typo3.org/", + "source": "https://github.com/TYPO3/typo3/", + "docs": "https://docs.typo3.org/", + "rss": "https://news.typo3.com/rss/", + "chat": "https://typo3.community/meet/slack/", + "security": "https://typo3.org/security/" + }, + "config": { + "sort-packages": true + }, + "require": { + "ext-intl": "*", + "ext-libxml": "*", + "psr/event-dispatcher": "^1.0", + "typo3/cms-core": "15.0.*@dev" + }, + "suggest": { + "typo3/cms-install": "Displays a link to the Environment module in the System Information toolbar." + }, + "conflict": { + "typo3/cms": "*" + }, + "replace": { + "typo3/cms-about": "self.version", + "typo3/cms-context-help": "self.version", + "typo3/cms-cshmanual": "self.version", + "typo3/cms-func-wizards": "self.version", + "typo3/cms-recordlist": "self.version", + "typo3/cms-t3editor": "self.version", + "typo3/cms-wizard-crpages": "self.version", + "typo3/cms-setup": "self.version", + "typo3/cms-wizard-sortpages": "self.version" + }, + "extra": { + "branch-alias": { + "dev-main": "15.0.x-dev" + }, + "typo3/cms": { + "Package": { + "serviceProvider": "TYPO3\\CMS\\Backend\\ServiceProvider", + "protected": true, + "partOfFactoryDefault": true, + "partOfMinimalUsableSystem": true + }, + "extension-key": "backend" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Backend\\": "Classes/" + } + } +} diff --git a/ext_conf_template.txt b/ext_conf_template.txt new file mode 100644 index 0000000..9b7ec00 --- /dev/null +++ b/ext_conf_template.txt @@ -0,0 +1,24 @@ +################### +### LOGINSCREEN ### +################### + +# cat=Login; type=string; label=LLL:EXT:backend/Resources/Private/Language/locallang.xlf:config.loginLogo +loginLogo = + +# cat=Login; type=string; label=LLL:EXT:backend/Resources/Private/Language/locallang.xlf:config.loginLogoAlt +loginLogoAlt = + +# cat=Login; type=color; label=LLL:EXT:backend/Resources/Private/Language/locallang.xlf:config.loginHighlightColor +loginHighlightColor = + +# cat=Login; type=string; label=LLL:EXT:backend/Resources/Private/Language/locallang.xlf:config.loginBackgroundImage +loginBackgroundImage = + +# cat=Login; type=string; label=LLL:EXT:backend/Resources/Private/Language/locallang.xlf:config.loginFootnote +loginFootnote = + +# cat=Backend; type=string; label=LLL:EXT:backend/Resources/Private/Language/locallang.xlf:config.backendLogo +backendLogo = + +# cat=Backend; type=string; label=LLL:EXT:backend/Resources/Private/Language/locallang.xlf:config.backendFavicon +backendFavicon = diff --git a/ext_localconf.php b/ext_localconf.php new file mode 100644 index 0000000..784b69f --- /dev/null +++ b/ext_localconf.php @@ -0,0 +1,26 @@ + UsernamePasswordLoginProvider::class, + 'sorting' => 50, + 'iconIdentifier' => 'actions-key', + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:login.link', +]; + +// Register search key shortcuts +$GLOBALS['TYPO3_CONF_VARS']['SYS']['livesearch']['page'] = 'pages'; + +$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = DataHandlerAuthenticationContext::class; + +// Giving the two hook registrations a unique name to allow unsetting this hook if it is not wanted. +// ext:container does this to apply own logic. +$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['contentElementRestriction'] = DataHandlerContentElementRestrictionHook::class; +$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass']['contentElementRestriction'] = DataHandlerContentElementRestrictionHook::class; diff --git a/ext_tables.sql b/ext_tables.sql new file mode 100644 index 0000000..4c8208b --- /dev/null +++ b/ext_tables.sql @@ -0,0 +1,4 @@ +CREATE TABLE be_users ( + # No TCA column defined + password_reset_token varchar(128) DEFAULT '' NOT NULL +);