Thank you for your interest in spreading the word on HWJMA.
'
+email_article_note: '
NOTE: We only request your email address so that the person you are recommending the page to knows that you wanted them to see it, and that it is not junk mail. We do not capture any email address.
'
+message_subject: '(Your Name) has sent you a message from HWJMA'
+message_body: '(Your Name) thought you would like to see the HWJMA web site.'
+thanks_msg_display: 1
+email_article_note_display: 1
+message_subject_display: 1
+message_body_display: 1
+highwire_rr_email_sender: rajat.gupta@highwirepress.com
+highwire_rr_email_subject: 'Thank you for your response to'
+highwire_rr_email_response_sent: "
Thank you for your submission. Below is a copy of your eLetter as we received it. Your eLetter, if accepted, should be\r\n viewable within a few days.
\r\n\r\n
Sincerely, \r\n The Editorial staff of !journal_name
\r\n ----------------------------------------\r\n
!article_link
\r\n
The eLetter !eletter_title was submitted on !submitted_date:
\r\n
!eletter_text
"
+highwire_e_letter_submission_message: 'Thank you for your response. We intend to publish as rapidly as possible all responses that contribute substantially to the topic under discussion.'
+moderator_distribution_email_list: rajat.gupta@highwirepress.com
+moderator_site_feedback_email_list: rajat.gupta@highwirepress.com
+moderator_notification_from_address: 'HWJMA Response Notification '
+moderator_notification_to_address: 'HWJMA Response Moderator '
+moderator_notification_email_subject: 'HWJMA response: "[node:title]"'
+moderator_notification_response_text: "
A new eLetter has been submitted to !article_title and is awaiting moderation.
\r\n
!article_link
\r\n
The eLetter was submitted on [node:created:custom:d m Y]:
\r\n
eLetter copy: [site:url]!eLetter_link_copy
\r\n
!eletter_text
"
+eletter_rule_action_flag: 1
+eLetter_publication_notification_from_address_to_eletterauthor: rajat.gupta@highwirepress.com
+eLetter_publication_notification_subject_to_eletterauthor: 'Your Response has been published'
+eLetter_publication_notification_eletterauthor_response_text: "
Dear !article_author_firstname_lastname,
\r\n\r\n
An eLetter has been published on [node:field-highwire-c-response-to:field-highwire-a-journal:title]'s web site. To view it, navigate to your article and click on \"eLetter\" tab or click the link below.
The Editorial Staff of [node:field-highwire-c-response-to:field-highwire-a-journal:title]
"
+eLetter_publication_notification_from_address_to_articleauthor: rajat.gupta@highwirepress.com
+eLetter_publication_notification_subject_to_articleauthor: 'A Response regarding your article has been published'
+eLetter_publication_notification_articleauthor_response_text: "
Dear !article_author_firstname_lastname,
\r\n\r\n
An eLetter has been published on [node:field-highwire-c-response-to:field-highwire-a-journal:title]'s web site. To view it, navigate to your article and click on \"eLetter\" tab or click the link below.
An eLetter was submitted to !journal_name for your article. It is our intention to post this letter on \r\n the web site and we would like to invite your feedback on the eLetter.
\r\n\r\n
\r\n\r\n
Your article (citation):
\r\n\r\n
\r\n\r\n
!article_citation
\r\n\r\n
\r\n\r\n
!article_link
\r\n\r\n
\r\n\r\n
The eLetter was submitted on !submitted_date:
\r\n\r\n
\r\n\r\n
!eletter_citation
\r\n\r\n
\r\n\r\n
\r\n\r\n
\r\n\r\n
If you would like to submit a response to this eLetter, please click on the \"Submit a Response to This Article\" link in the eLetter tab of your article. \r\n Please let us know if you have any other questions or reactions to this process.
\r\n\r\n
\r\n\r\n
Sincerely, \r\n The Editorial staff of !journal_name
It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like). Hello
There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.
');
+ count++;
+ }
+ if (count > 0) {
+ return $list;
+ }
+ }
+ }
+ };
+
+})(jQuery, Drupal, drupalSettings);
diff --git a/web/modules/highwire/hwjma_ecommerce/src/Plugin/Block/DisplayPrice.php b/web/modules/highwire/hwjma_ecommerce/src/Plugin/Block/DisplayPrice.php
new file mode 100644
index 000000000..556e8ff36
--- /dev/null
+++ b/web/modules/highwire/hwjma_ecommerce/src/Plugin/Block/DisplayPrice.php
@@ -0,0 +1,1550 @@
+entityTypeManager = $entity_type_manager;
+ $this->moduleHandler = $module_handler;
+ $this->entityDisplayRepository = $entity_display_repository;
+ $this->addToCartLink = $add_to_cart_link;
+ $this->lookup = $lookup;
+ $this->catalog = $catalog;
+ $this->requestStack = $request_stack;
+ $this->userAccess = FALSE;
+ $this->logger = $logger;
+ $this->apikey = $config_factory->get('highwire_ecommerce')->get('apikey');
+ $this->atomx = $atomx;
+ $this->defaultCache = $default_cache;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('entity_type.manager'),
+ $container->get('module_handler'),
+ $container->get('entity_display.repository'),
+ $container->get('highwire_ecommerce.add_to_cart_link'),
+ $container->get('highwire_content.lookup'),
+ $container->get('highwire_client.factory')->get('hwhwjmaclient:catalog'),
+ $container->get('request_stack'),
+ $container->get('config.factory'),
+ $container->get('logger.factory')->get('hwjma_ecommerce'),
+ $container->get('hwhwjma.atomx'),
+ $container->get('cache.default')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(): array {
+
+ // Create block render array.
+ $build = ['#theme' => 'hwjma_access_panel', '#user_access' => FALSE];
+
+ // Get the current node data.
+ $this->contextNode = $this->getContextValue('node');
+
+ // Check if user has full access to this content.
+ if ($this->userHasAccess($this->contextNode)) {
+
+ // Return has access panel.
+ $this->userAccess = TRUE;
+ $build['#user_access'] = TRUE;
+ return $build;
+ }
+ $this->checkAccessChildren();
+ if ($this->userAccess) {
+ $build['#user_access'] = TRUE;
+ return $build;
+ }
+
+ // Temp for p2: add node type
+ $build['#access_content_type'] = $this->contextNode->getType();
+
+ // Build display for children the user already has access to.
+ if ($this->showChildren()) {
+ $build['#user_access_already'] = $this->buildUserAccessChildren();
+ }
+
+ // Build display for children the user already has access to.
+ if (in_array($this->contextNode->getType(), HW_NODE_TYPE_TOC)) {
+ $build['#user_access'] = TRUE;
+ }
+
+ // Get the catalog data for the context data.
+ $node_apath = isset($this->contextNode->apath) ? $this->contextNode->apath->value : '';
+ try {
+ $offers_response = $this->catalog->getOffer([$node_apath], TRUE, TRUE);
+ $offers = $offers_response->getData();
+ }
+ catch (\Exception $ex) {
+ $this->logger->error($ex->getMessage());
+ return $build;
+ }
+ // Load all the nodes for apaths in the Catalog response.
+ $apaths = $offers->getAllApaths();
+ try {
+ if ($nids = $this->lookup->nidsFromApaths($apaths));
+ }
+ catch (ApathNotFoundException $ex) {
+ if (empty($nids)) {
+ return $build;
+ }
+ }
+
+ // Add nodes for lookup later.
+ $this->nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids);
+
+ // Get the User's Currency
+ try {
+ $user_ip = $this->requestStack->getCurrentRequest()->getClientIp();
+ $user_currency = $this->catalog->getUserCurrency($user_ip)->getData();
+ if (empty($user_currency)) {
+ return $build;
+ }
+ }
+ catch (\Exception $ex) {
+ $this->logger->error($ex->getMessage());
+ return $build;
+ }
+
+ // Get apaths for products.
+ $product_apaths = $offers->getAllProductApaths();
+ $pricing_items = $offers->getPricingItems();
+
+ // Loop through offers response.
+ foreach ($pricing_items as $pricing_item) {
+ $grouped_products = $this->groupProductsByType($pricing_item);
+ $types = array_keys($grouped_products);
+ $container_type = end($types);
+
+ // Build pricing item list per type.
+ $pricing_item_products = [];
+ foreach ($grouped_products as $type => $products) {
+ $title = $this->buildPurchaseOffersTitle($type, $container_type);
+ $products_list = $this->buildPurchaseOffers($products, $user_currency, !empty($pricing_item_products) ? 'Or ' . lcfirst($title) : $title);
+ if (!empty($products_list)) {
+ $pricing_item_products[] = $products_list;
+ }
+ }
+
+ // Add pricing item lists to render array.
+ if (!empty($pricing_item_products)) {
+ $build['#pricing_items'][] = $pricing_item_products;
+ }
+ }
+
+ // Build child offers prompt text.
+ $child_offers_prompt = $this->buildChildOffersPrompt();
+ if (!empty($child_offers_prompt)) {
+ $build['#purchase_children'] = $child_offers_prompt;
+ }
+ $build['#cache']['contexts'][] = 'user';
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration(): array {
+ return [
+ 'list_titles' => [
+ 'user_access_already' => 'You already have access to these :types:',
+ 'purchase_offers_current' => 'Get access to this :type:',
+ 'purchase_offers_container' => 'Get access to the entire :type:',
+ 'purchase_offers_default' => 'Get access to the :type:',
+ ],
+ 'child_offers_prompt' => [
+ 'title' => 'Get access to individual :types:',
+ 'text' => 'Open :a :type to see its purchasing options.',
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blockForm($form, FormStateInterface $form_state): array {
+ $form = parent::blockForm($form, $form_state);
+ $form['list_titles'] = [
+ '#type' => 'container',
+ '#tree' => TRUE,
+ '#suffix' => '
Note: For the above titles, you may use ":type" and ":types" to represent the singular or plural label of the respective content type.
',
+ ];
+
+ $form['list_titles']['user_access_already'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Title for list of items user already has access to'),
+ '#description' => $this->t('Displayed when there are children the user has access to (e.g. list of chapters on book page).'),
+ '#default_value' => isset($this->configuration['list_titles']['user_access_already']) ? $this->configuration['list_titles']['user_access_already'] : '',
+ ];
+
+ $form['list_titles']['purchase_offers_current'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Title for purchase offers for currently viewed item'),
+ '#description' => $this->t('Displayed above list of purchase offers for the item being viewed.'),
+ '#default_value' => isset($this->configuration['list_titles']['purchase_offers_current']) ? $this->configuration['list_titles']['purchase_offers_current'] : '',
+ ];
+
+ $form['list_titles']['purchase_offers_container'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Title for purchase offers for the currently viewed item\'s root parent element'),
+ '#description' => $this->t('Displayed when there are purchase offers for the root parent element of the currently viewed item (e.g. the book on a chapter page, or the journal on an article page).'),
+ '#default_value' => isset($this->configuration['list_titles']['purchase_offers_container']) ? $this->configuration['list_titles']['purchase_offers_container'] : '',
+ ];
+
+ $form['list_titles']['purchase_offers_default'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Title for purchase offers for other items besides the current and root parent'),
+ '#description' => $this->t('Displayed when there are purchase offers for other items besides the currently viewed item or its root parent (e.g. issues on an article page).'),
+ '#default_value' => isset($this->configuration['list_titles']['purchase_offers_default']) ? $this->configuration['list_titles']['purchase_offers_default'] : '',
+ ];
+
+ $form['child_offers_prompt'] = [
+ '#type' => 'container',
+ '#tree' => TRUE,
+ '#suffix' => '
Note: For the above titles, you may use ":type" and ":types" to represent the singular or plural label of the respective content type. You can also use ":a" before the type token to dynamically change based on whether the type string starts with a vowel.
',
+ ];
+
+ $form['child_offers_prompt']['title'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Title of prompt to instruct users how to buy child content'),
+ '#description' => $this->t('Displayed when there are purchase offers for children of the currently viewed item (e.g. chapters on a book page).'),
+ '#default_value' => isset($this->configuration['child_offers_prompt']['title']) ? $this->configuration['child_offers_prompt']['title'] : '',
+ ];
+
+ $form['child_offers_prompt']['text'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Text of prompt to instruct users how to buy child content'),
+ '#description' => $this->t('Displayed when there are purchase offers for children of the currently viewed item (e.g. chapters on a book page).'),
+ '#default_value' => isset($this->configuration['child_offers_prompt']['text']) ? $this->configuration['child_offers_prompt']['text'] : '',
+ ];
+
+ // Add display mode options per node type.
+ $form['view_modes'] = [
+ '#type' => 'container',
+ '#tree' => TRUE,
+ ];
+
+ // Try to filter the node types based on the selection criteria of a page variant.
+ $node_types = [];
+ $storage = $form_state->getStorage();
+ if (!empty($storage['machine_name']) && strpos($storage['machine_name'], '--') !== FALSE) {
+ list($page_id, $page_variant_id) = explode('--', $storage['machine_name']);
+ try {
+ $page = $this->entityTypeManager->getStorage('page')->load($page_id);
+ $page_variant = $page->getVariant($page_variant_id);
+ $page_variant_config = $page_variant->getSelectionConditions()->getConfiguration();
+ }
+ catch (\Exception $e) {
+ $page_variant_config = [];
+ }
+
+ if (!empty($page_variant_config)) {
+
+ // Look for the node type selection criteria.
+ foreach ($page_variant_config as $variant_config) {
+ if (in_array($variant_config['id'], ['entity_bundle:node', 'node_type']) && !empty($variant_config['bundles'])) {
+ $node_types = array_keys($variant_config['bundles']);
+ }
+ }
+ }
+ }
+
+ // Only display types related to the node type selection criteria
+ // (i.e. books + book chunks, journals + journal chunks, refbooks + refbook chunks).
+ foreach ($node_types as $type) {
+ if (in_array($type, hwjma_core_get_book_chunk_types()) || $type == HW_NODE_TYPE_MONOGRAPH) {
+ $node_types = hwjma_core_get_book_chunk_types();
+ $node_types[] = HW_NODE_TYPE_MONOGRAPH;
+ break;
+ }
+
+ if (in_array($type, hwjma_core_get_book_chunk_types()) || $type == HW_NODE_TYPE_REPORT_GUIDELINE) {
+ $node_types = hwjma_core_get_book_chunk_types();
+ $node_types[] = HW_NODE_TYPE_REPORT_GUIDELINE;
+ break;
+ }
+
+ if (in_array($type, hwjma_core_get_journal_chunk_types()) || $type == HW_NODE_TYPE_JOURNAL) {
+ $node_types = hwjma_core_get_journal_chunk_types();
+ $node_types[] = HW_NODE_TYPE_JOURNAL;
+ break;
+ }
+
+ if ($type == HW_NODE_TYPE_TEST_REVIEW) {
+ $node_types[] = HW_NODE_TYPE_TEST_REVIEW;
+ break;
+ }
+ }
+
+ // If we weren't able to find a panel page with a node type config,
+ // default to all highwire node types.
+ if (empty($node_types)) {
+ $node_types = $this->lookup->getHighWireContentTypes();
+ }
+ foreach ($node_types as $node_type) {
+ $view_modes = ['' => t('None')];
+ $view_modes += $this->entityDisplayRepository->getViewModeOptionsByBundle('node', $node_type);
+ $form['view_modes'][$node_type] = [
+ '#type' => 'select',
+ '#title' => $this->t('View mode for bundle %bundle', ['%bundle' => $node_type]),
+ '#options' => $view_modes,
+ '#default_value' => !empty($this->configuration['view_modes'][$node_type]) ? $this->configuration['view_modes'][$node_type] : 'default',
+ ];
+ }
+
+ // Add Access Control Rule.
+ if ($this->moduleHandler->moduleExists('highwire_access_control')) {
+ $entities = $this->entityTypeManager->getStorage('access_control_rule')->getQuery()
+ ->execute();
+ if (!empty($entities) && is_array($entities)) {
+ $access_options = [];
+ $access_options[''] = '--None--';
+ foreach ($entities as $entity_id) {
+ $access_rule = $this->entityTypeManager->getStorage('access_control_rule')->load($entity_id);
+ $access_options[$entity_id] = $access_rule->get('label');
+ }
+ $form['access_control_rule'] = [
+ '#type' => 'select',
+ '#title' => t('Access Control Rule'),
+ '#options' => $access_options,
+ '#default_value' => !empty($this->configuration['access_control_rule']) ? $this->configuration['access_control_rule'] : '',
+ ];
+ }
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blockSubmit($form, FormStateInterface $form_state) {
+ parent::blockSubmit($form, $form_state);
+ foreach ($form_state->getValues() as $k => $v) {
+ $this->configuration[$k] = $v;
+ }
+ }
+
+ /**
+ * Build product node display.
+ *
+ * @param \Drupal\node\Entity\Node $product_node
+ * The drupal node for the product.
+ *
+ * @return array
+ * Render array for the product node (as either a view mode or a link).
+ */
+ public function getProductDisplay(Node $product_node): array {
+ $view_builder = $this->entityTypeManager->getViewBuilder('node');
+ $node_type = $product_node->getType();
+ $build = [];
+
+ $view_mode = '';
+ if (!empty($this->configuration['view_modes']) && !empty($this->configuration['view_modes'][$node_type])) {
+ $view_mode = $this->configuration['view_modes'][$node_type];
+ }
+ if (!empty($view_mode)) {
+ $build = $view_builder->view($product_node, $view_mode);
+ }
+ else {
+ $product_link = $product_node->toLink();
+ if ($product_link) {
+ $build = $product_link->toRenderable();
+ }
+ }
+ return $build;
+ }
+
+ /**
+ * Get the node for a product.
+ *
+ * @param \HighWire\Clients\Catalog\Product $product
+ * The product from the Catalog request.
+ *
+ * @return \Drupal\node\Entity\Node
+ * The Drupal node for the product.
+ */
+ public function getProductNode(Product $product) {
+ $product_id = $product->getId();
+ foreach ($this->nodes as $node) {
+ if ($node->apath->value == $product_id) {
+ return $node;
+ }
+ }
+ }
+
+ /**
+ * Get the node by product id.
+ *
+ * @param string $id
+ * Id / apath for the node to be returned.
+ *
+ * @return \Drupal\node\Entity\Node
+ * A drupal node object.
+ */
+ public function getProductNodeById(string $id) {
+ foreach ($this->nodes as $node) {
+ if ($node->apath->value == $id) {
+ return $node;
+ }
+ }
+ }
+
+ /**
+ * Check whether the current user has access to a given node.
+ *
+ * @param Drupal\node\Entity\Node $node
+ * Node to check access for.
+ *
+ * @return bool
+ * TRUE if the current user has access to the given node, FALSE if not.
+ */
+ public function userHasAccess(Node $node) {
+ if (!$this->moduleHandler->moduleExists('highwire_access_control') || empty($this->configuration['access_control_rule'])) {
+ return FALSE;
+ }
+ $access_rule = $this->entityTypeManager->getStorage('access_control_rule')->load($this->configuration['access_control_rule']);
+ if (empty($node->apath) || empty($access_rule)) {
+ return FALSE;
+ }
+ $apath = $node->apath->value;
+
+ // Check user access.
+ if (empty($apath)) {
+ return FALSE;
+ }
+ $access_apaths = $access_rule->userHasAccess([$apath]);
+ return !empty($access_apaths[$apath]);
+ }
+
+ /**
+ * Function to check user access for all children.
+ */
+ protected function checkAccessChildren() {
+ if (!$this->showChildren() || !$this->moduleHandler->moduleExists('highwire_access_control') || empty($this->configuration['access_control_rule'])) {
+ return;
+ }
+ $access_rule = $this->entityTypeManager->getStorage('access_control_rule')->load($this->configuration['access_control_rule']);
+ if (empty($access_rule)) {
+ return;
+ }
+
+ // Get children of current node.
+ $node = !empty($this->contextNode) ? $this->contextNode : $this->getContextValue('node');
+
+ //$child_apaths = $this->getChildApaths($node);
+ $apath = $node->get('apath')->value;
+
+ // Check access and group apaths.
+ $user_access_children = $user_noaccess_children = [];
+ if (!empty($child_apaths)) {
+ $access_apaths = $access_rule->userHasAccess([$apath]);
+ foreach ($access_apaths as $apath => $has_access) {
+ if ($has_access) {
+ $user_access_children[$apath] = $apath;
+ $this->userAccess = TRUE;
+ }
+ else {
+ $user_noaccess_children[$apath] = $apath;
+ }
+ }
+ }
+
+ // Store apaths for later.
+ $this->userAccessChildren = $user_access_children;
+ $this->userNoAccessChildren = $user_noaccess_children;
+
+ // If the user has access to all the children, set access to true.
+ // if (count($child_apaths) !== 0 && count($child_apaths) == count($user_access_children)) {
+ // $this->userAccess = TRUE;
+ // }
+ }
+
+ /**
+ * Get render array for list of children the current user already has access to.
+ *
+ * @return array
+ * Render array for an item_list of nodes.
+ */
+ protected function buildUserAccessChildren(): array {
+ $build = [];
+
+ // Check AC for items we have access for.
+ $user_access = $this->userAccessChildren;
+ $children_users_have = $this->getChildrenUserHasAccessDisplay($user_access);
+ if (!empty($children_users_have)) {
+ $access_items = [];
+
+ // Split children up by content type
+ foreach ($children_users_have as $child) {
+ if (isset($child['#node'])) {
+
+ // Group book entry content types.
+ if (in_array($child['#node']->getType(), ['item_front_matter', 'item_back_matter', 'item_chapter'])) {
+ $access_items['book_entry'][] = $child;
+ }
+ else {
+ $access_items[$child['#node']->getType()][] = $child;
+ }
+ }
+ }
+ $build = [];
+ $title_config = !empty($this->configuration['list_titles']) ? $this->configuration['list_titles'] : [];
+ foreach (array_keys($access_items) as $item_section) {
+ $title = '';
+ if (!empty($title_config['user_access_already'])) {
+ $t_values = [':type' => $this->getTypeLabel($item_section), ':types' => $this->getTypeLabelPlural($item_section)];
+ $title = $this->t($title_config['user_access_already'], $t_values);
+ }
+
+ $build[] = [
+ '#title' => $title,
+ '#theme' => 'item_list',
+ '#items' => $access_items[$item_section],
+ '#context' => ['list_style' => 'ecommerce_children'],
+ '#attributes' => ['class' => ['list-unstyled']],
+ ];
+ }
+ }
+ return $build;
+ }
+
+ /**
+ * Recursive function to check children access.
+ *
+ * @param \Drupal\node\Entity\Node $node
+ * The node to check children.
+ */
+ protected function getChildApaths(Node $node) {
+ $cid = "hwjma_child_ac_paths:{$node->Id()}";
+ $cache = $this->defaultCache->get($cid);
+ $cache = FALSE;
+ $results = [];
+ if (!empty($cache->data)) {
+ return $cache->data;
+ }
+ switch($node->getType()) {
+ case HW_NODE_TYPE_JOURNAL:
+ try {
+ $corpus = $node->get('corpus')->value;
+ $policy = $node->get('extract_policy')->value;
+ $search_query = '{
+ "size": 5000,
+ "_source": ["apath"],
+ "query": {
+ "bool" : {
+ "should": [
+ {"term": {"has-full-text": true}},
+ {"term": {"has-full-text-pdf": true}}
+ ]
+ }
+ }
+ }
+ ';
+ $this->atomx->setIndexes([$policy . ":" . $corpus]);
+ $results = array_keys($this->atomx->search($search_query));
+ $this->defaultCache->set($cid, $results);
+ return $results;
+ }
+ catch (\Exception $e) {
+ return [];
+ }
+ break;
+
+ case HW_NODE_TYPE_ISSUE:
+ try {
+ $apath = $node->get('apath')->value;
+ $corpus = $node->get('corpus')->value;
+ $policy = $node->get('extract_policy')->value;
+ $search_query = '{
+ "size": 5000,
+ "_source": ["_type"],
+ "query": {
+ "bool" : {
+ "must": [
+ {"term": {"parent-issue": "'. $apath .'"}},
+ {"bool": {
+ "should": [
+ {"term": {"has-full-text": true}},
+ {"term": {"has-full-text-pdf": true}}
+ ]
+ }
+ }
+ ]
+ }
+ }
+ }
+ ';
+ $this->atomx->setIndexes([$policy . ":" . $corpus]);
+ $results = array_keys($this->atomx->search($search_query));
+ $this->defaultCache->set($cid, $results);
+ return $results;
+ }
+ catch (\Exception $e) {
+ return [];
+ }
+ break;
+
+ default:
+ return [];
+ break;
+ }
+
+ // $children = isset($node->children) && !$node->children->isEmpty() ? $node->children : [];
+ // if (!isset($node->children) || $node->children->isEmpty()) {
+ // return;
+ // }
+ // foreach ($node->children as $child) {
+ // $child_node = $child->entity;
+ // $process_children = FALSE;
+
+ // // Books
+ // if ($child_node && isset($child_node->book_has_body) && !empty($child_node->book_has_body->value) && isset($child_node->apath)) {
+ // $process_children = TRUE;
+ // }
+
+ // // Journal articles
+ // if ($child_node && isset($child_node->has_full_text) && (!empty($child_node->has_full_text->value) || !empty($child_node->has_full_text_pdf->value)) && isset($child_node->apath)) {
+ // $process_children = TRUE;
+ // }
+
+ // // Journal
+ // if ($child_node->getType() == HW_NODE_TYPE_JOURNAL || $child_node->getType() == HW_NODE_TYPE_ISSUE) {
+ // $process_children = TRUE;
+ // }
+
+ // if ($process_children) {
+ // $child_apath = $child_node->apath->value;
+ // if (!empty($child_apath) && !in_array($child_apath, $child_apaths)) {
+ // $child_apaths[] = $child_node->apath->value;
+ // }
+ // }
+ // $this->getChildApathsRecursive($child_node, $child_apaths);
+ // }
+ }
+
+ /**
+ * Get the display for children the user has access to.
+ *
+ * @param array $user_access_apaths
+ * An array of apaths the user has access to.
+ *
+ * @return array
+ * Render array of node links.
+ */
+ public function getChildrenUserHasAccessDisplay(array $user_access_apaths): array {
+ $access_nids = $this->lookup->nidsFromApaths($user_access_apaths);
+ $access_nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($access_nids);
+ $view_builder = $this->entityTypeManager->getViewBuilder('node');
+ $purchased_parents = [];
+ $access_items = [];
+ foreach ($access_nodes as $access_node) {
+ $access_node_type = $access_node->getType();
+
+ // Books
+ if (in_array($access_node_type, hwjma_core_get_book_chunk_types())) {
+ $access_items[] = $this->getProductDisplay($access_node);
+ }
+
+ // Journals
+ if (in_array($access_node_type, hwjma_core_get_journal_chunk_types())) {
+
+ // Journals and Journal issues.
+ if ($access_node_type == HW_NODE_TYPE_JOURNAL || $access_node_type == HW_NODE_TYPE_ISSUE) {
+ $purchased_parents[] = $access_node->apath->value;
+ }
+
+ // Journal article chunks
+ if ($access_node->hasField('parent_issue')) {
+ if (!in_array($access_node->parent_issue->get(0)->apath, $purchased_parents)) {
+ $access_items[] = $this->getProductDisplay($access_node);
+ }
+ }
+ }
+ }
+ return $access_items;
+ }
+
+ /**
+ * Get the display for a group of products.
+ *
+ * @param array $products
+ * Array of product objects.
+ *
+ * @param string $currency_code
+ * Three character currency code.
+ *
+ * @return array
+ * Render array.
+ */
+ public function buildPurchaseOffers(array $products, string $currency_code, string $title = ''): array {
+
+ // Build list item display.
+ $items = [];
+ foreach ($products as $product) {
+ $product_item = $this->buildPurchaseOffer($product, $currency_code);
+ if (!empty($product_item)) {
+ $items[] = $product_item;
+ }
+ }
+ if (empty($items)) {
+ return [];
+ }
+
+ // Build list.
+ $build = [
+ '#theme' => 'item_list',
+ '#title' => $title,
+ '#items' => $items,
+ '#attributes' => ['class' => ['list-unstyled']],
+ '#context' => ['list_style' => 'ecommerce_purchase_offers'],
+ ];
+ return $build;
+ }
+
+ /**
+ * Get render array for a purchase offer item.
+ *
+ * @param \HighWire\Clients\Catalog\Product $product
+ * The product to build render array for.
+ * @param string $currency_code
+ * Three character currency code.
+ *
+ * @return array
+ * A render array for a purchase offer item.
+ */
+ public function buildPurchaseOffer(Product $product, string $currency_code): array {
+ $build = [];
+ $product_node = $this->getProductNode($product);
+ $purchase_options = $product->getPurchaseOptions();
+ if (empty($product_node) || empty($purchase_options)) {
+ return $build;
+ }
+
+ // Build product node display.
+ $product_display = $this->getProductDisplay($product_node);
+
+ // Get additional product node vars for purchase link.
+ $product_node_vars = [
+ 'image' => $this->getProductImage($product_node),
+ 'product_source' => html_entity_decode($this->getProductSource($product_node)),
+ 'title_suffix' => $this->getProductTitleSuffix($product_node),
+ ];
+
+ // Build purchase options display.
+ $purchase_options_build = [];
+ foreach ($purchase_options as $disposition => $purchase_option) {
+ if (empty($purchase_option)) {
+ continue;
+ }
+ foreach ($purchase_option as $interval => $prices) {
+ $price = !empty($prices[$currency_code]) ? $prices[$currency_code] : [];
+ if (empty($price)) {
+ continue;
+ }
+
+ // Build purchase option.
+ $purchase_link = $this->buildPurchaseLink($product, $product_node, $price, $product_node_vars);
+ if (!empty($purchase_link)) {
+ $purchase_options_build[] = [
+ '#theme' => 'hwjma_purchase_offer_item',
+ '#type' => $disposition,
+ '#duration' => $price->getInterval(IntervalFormatter::hours()) . ' hours',
+ '#purchase_link' => $purchase_link,
+ ];
+ }
+ }
+ }
+
+ if (!empty($product_display) && !empty($purchase_options_build)) {
+ $build = [
+ '#theme' => 'hwjma_purchase_offer',
+ '#product' => $product_display,
+ '#purchase_offers' => $purchase_options_build,
+ ];
+ }
+ return $build;
+ }
+
+ /**
+ * Get render array for a purchase link.
+ *
+ * @param \HighWire\Clients\Catalog\Product $product
+ * The product to build purchase link render array for.
+ * @param \Drupal\node\Entity\Node $product_node
+ * Drupal node for the product.
+ * @param \HighWire\Clients\Catalog\Price $price
+ * The price to build purchase link render array for.
+ * @param array $add_query_params
+ * Any additional query paramters to add to the purcahse link.
+ *
+ * @return array
+ * A render array for a purchase link.
+ */
+ public function buildPurchaseLink(Product $product, Node $product_node, Price $price, array $add_query_params = []): array {
+ $args = [];
+
+ // Update Issue nodes with full journal issue title.
+ if ($product_node->getType() == HW_NODE_TYPE_ISSUE) {
+ if ($journal_title_field = $product_node->get('journal_title')->first()) {
+ $journal_title = $journal_title_field->getString() . ' | ';
+ }
+ else {
+ $journal_title = '';
+ }
+ if ($vol_field = $product_node->get('volume')->first()) {
+ $vol = 'Volume ' . $vol_field->getString();
+ }
+ else {
+ $vol = '';
+ }
+ if ($issue_field = $product_node->get('issue')->first()) {
+ $issue = 'Issue ' . $issue_field->getString();
+ }
+ else {
+ $issue = '';
+ }
+ $product_node->setTitle($journal_title . $vol . ($vol && $issue ? ', ' : '') . $issue);
+ }
+
+ // Exclude the apath query parameter from foxycart links for Journal subscriptions.
+ if ($product_node->getType() == HW_NODE_TYPE_JOURNAL && $price->getDisposition() == 'rental') {
+ $args['exclude_apath'] = TRUE;
+ }
+
+ $purchase_link = $this->addToCartLink->getLink($product, $product_node, $price, $args);
+ if (empty($purchase_link)) {
+ return [];
+ }
+
+ // Alter display of purchase link.
+ $purchase_link['#attributes']['class'][] = 'btn';
+ $purchase_link['#attributes']['class'][] = 'btn-primary';
+ $user_ip = $this->requestStack->getCurrentRequest()->getClientIp();
+ $user_currency = $this->catalog->getUserCurrency($user_ip)->getData();
+ $currencysymbol = $this->getCurrencySymbol($user_currency);
+
+ //Check for dicount given
+ $discountdata = $this->addToCartLink->getDiscountFlag();
+ $display_price = $price->getDisplayPrice();
+ if (!empty($display_price)) {
+ if (!empty($discountdata)) {
+ $purchase_link['#title'] = $this->t('Add to cart (:symbol:discount) (:price)',
+ [':symbol' => $currencysymbol,
+ ':price' => $display_price,
+ ':discount' => $discountdata
+ ]
+ );
+ $purchase_link['#attributes']['data-original-price'] = '(' . $currencysymbol . $discountdata . ')';
+ $purchase_link['#attributes']['data-discounted-price'] = '(' . $display_price . ')';
+ }
+ else {
+ $purchase_link['#title'] = $this->t('Add to cart (:price)', [':price' => $display_price]);
+ }
+ $purchase_link['#attributes']['data-purchase-text'] = $purchase_link['#title'];
+ }
+
+ // Add extra query parameters.
+ if (!empty($add_query_params)) {
+ $purchase_link_query = $purchase_link['#url']->getOption('query');
+ $sku = $product->getSku();
+ foreach ($add_query_params as $k => $v) {
+ if (empty($v)) {
+ continue;
+ }
+ $purchase_link_query[$this->addToCartLink->getHmac($k, $v, $sku, $this->apikey)] = $v;
+ }
+ $purchase_link['#url']->setOption('query', $purchase_link_query);
+ $purchase_link['#attributes']['data-purchase-url'] = $purchase_link['#url']->toString();
+ }
+ return $purchase_link;
+ }
+
+ /**
+ * Get image for a given product node.
+ *
+ * @param \Drupal\node\Entity\Node $product_node
+ * The product node.
+ *
+ * @return string
+ * The url to the given product's image.
+ */
+ public function getProductImage(Node $product_node): string {
+ $type = $product_node->getType();
+ $img_url = '';
+ if (in_array($type, hwjma_core_get_book_chunk_types())) {
+ $img_url = Url::fromUserInput('/themes/scolaris_hwjma/images/icon-chapter.png', ['absolute' => TRUE])->toString();
+ }
+ elseif ($type == HW_NODE_TYPE_ARTICLE) {
+ // Get Parent Issue to fetch its image as article dont have its own
+ $parent_issue_id = $product_node->get('parent_issue')->getString();
+ if (!empty($parent_issue_id)) {
+ $parent_issue_data = Node::load($parent_issue_id);
+ if (!empty($parent_issue_data) && $parent_issue_data->hasField('variant_cover_image') && !$parent_issue_data->get('variant_cover_image')->isEmpty()) {
+ $img_field = $parent_issue_data->variant_cover_image->first()->getValue();
+ if (!empty($img_field['uri'])) {
+ $img_url = ImageStyle::load('cover_results_item')->buildUrl($img_field['uri']);
+ }
+ }
+ }
+ }
+ else {
+ $img_field = [];
+ switch ($type) {
+ case HW_NODE_TYPE_MONOGRAPH:
+ case HW_NODE_TYPE_REPORT_GUIDELINE:
+ case HW_NODE_TYPE_TEST_REVIEW:
+ if (!empty($product_node) && $product_node->hasField('cover_image') && !$product_node->get('cover_image')->isEmpty()) {
+ $img_field = $product_node->cover_image->first()->getValue();
+ }
+ break;
+
+ case HW_NODE_TYPE_JOURNAL:
+ case HW_NODE_TYPE_ISSUE:
+ if (!empty($product_node) && $product_node->hasField('variant_cover_image') && !$product_node->get('variant_cover_image')->isEmpty()) {
+ $img_field = $product_node->variant_cover_image->first()->getValue();
+ }
+ break;
+
+ }
+ if (!empty($img_field['uri'])) {
+ $img_url = ImageStyle::load('cover_results_item')->buildUrl($img_field['uri']);
+ }
+ }
+ return $img_url;
+ }
+
+ /**
+ * Get source title for a given product node.
+ *
+ * @param \Drupal\node\Entity\Node $product_node
+ * The product node.
+ *
+ * @return string
+ * The title of the given product's source content.
+ */
+ public function getProductSource(Node $product_node): string {
+ $type = $product_node->getType();
+ $source = '';
+ if ($type == HW_NODE_TYPE_MONOGRAPH || $type == HW_NODE_TYPE_REPORT_GUIDELINE || $type == HW_NODE_TYPE_JOURNAL || $type == HW_NODE_TYPE_TEST_REVIEW) {
+ return $source;
+ }
+ if (in_array($type, hwjma_core_get_book_chunk_types())) {
+ if ($product_node->hasField('parent_book') && !$product_node->get('parent_book')->isEmpty()) {
+ $source_node_data = $product_node->parent_book->first()->getValue();
+ if (!empty($source_node_data['target_id']) && !empty($this->nodes[$source_node_data['target_id']])) {
+ $source_node = $this->nodes[$source_node_data['target_id']];
+ $source = $source_node->getTitle();
+ $title_suffix = $this->getProductTitleSuffix($source_node);
+ if (!empty($title_suffix)) {
+ $source .= ', ' . $title_suffix;
+ }
+ }
+ }
+ }
+
+ // todo: refactor book and journal sources code.
+ if (in_array($type, hwjma_core_get_journal_chunk_types())) {
+ if ($product_node->hasField('parent-journal') && !$product_node->get('parent-journal')->isEmpty()) {
+ $source_node_data = $product_node->journal->first()->getValue();
+ if (!empty($source_node_data['target_id']) && !empty($this->nodes[$source_node_data['target_id']])) {
+ $source_node = $this->nodes[$source_node_data['target_id']];
+ $source = $source_node->getTitle();
+ $title_suffix = $this->getProductTitleSuffix($source_node);
+ if (!empty($title_suffix)) {
+ $source .= ', ' . $title_suffix;
+ }
+ }
+ }
+ }
+
+ // @TODO: Add support for issue & article types.
+ return $source;
+ }
+
+ /**
+ * Get title suffix for a given product node.
+ *
+ * @param \Drupal\node\Entity\Node $product_node
+ * The product node.
+ *
+ * @return string
+ * The title suffix for the given product.
+ */
+ public function getProductTitleSuffix(Node $product_node): string {
+ $type = $product_node->getType();
+ $title_suffix = '';
+ switch ($type) {
+ case HW_NODE_TYPE_MONOGRAPH:
+ case HW_NODE_TYPE_REPORT_GUIDELINE:
+ if ($product_node->hasField('edition') && !$product_node->get('edition')->isEmpty()) {
+ $edition = $product_node->get('edition')->getString();
+ if (is_numeric($edition) && intval($edition) > 1) {
+ $ordinalFormatter = new \NumberFormatter("en-US", \NumberFormatter::ORDINAL);
+ $edition = $ordinalFormatter->format($edition);
+ $title_suffix = $this->t(':edition Edition', [':edition' => $edition]);
+ }
+ }
+ break;
+ }
+ return $title_suffix;
+ }
+
+ /**
+ * Get product list title based on type and container type.
+ *
+ * @param string $type
+ * The type of product.
+ * @param string $container_type
+ * The product's root container product type.
+ *
+ * @return string
+ * Title for purchase offers list.
+ */
+ public function buildPurchaseOffersTitle(string $type, string $container_type = ''): string {
+ $title = '';
+ $display_node_type = '';
+ $product_node_types = $this->getProductNodeTypes($type);
+ $title_config = !empty($this->configuration['list_titles']) ? $this->configuration['list_titles'] : [];
+ $node_type = !empty($this->contextNode) ? $this->contextNode->getType() : $this->getContextValue('node')->getType();
+ if (in_array($node_type, $product_node_types)) {
+
+ // Label for this content type.
+ $display_node_type = $node_type;
+ $title = !empty($title_config['purchase_offers_current']) ? $title_config['purchase_offers_current'] : '';
+ }
+ else {
+ $display_node_type = reset($product_node_types);
+ if ($type == $container_type) {
+
+ // Label for container type.
+ $title = !empty($title_config['purchase_offers_container']) ? $title_config['purchase_offers_container'] : '';
+ }
+ else {
+
+ // Label for parent type that is not the root container.
+ $title = !empty($title_config['purchase_offers_default']) ? $title_config['purchase_offers_default'] : '';
+ }
+ }
+ $t_values = !empty($display_node_type) ? [':type' => $this->getTypeLabel($display_node_type), ':types' => $this->getTypeLabelPlural($display_node_type)] : [];
+ if (!empty($title)) {
+ $title = $this->t($title, $t_values);
+ }
+ return $title;
+ }
+
+ /**
+ * Build child offer prompt display.
+ */
+ public function buildChildOffersPrompt(): array {
+ $build = [];
+ $child_prompt_config = !empty($this->configuration['child_offers_prompt']) ? $this->configuration['child_offers_prompt'] : [];
+ if (empty($child_prompt_config) || (empty($child_prompt_config['title']) && empty($child_prompt_config['text']))) {
+ return $build;
+ }
+ $node_type = !empty($this->contextNode) ? $this->contextNode->getType() : $this->getContextValue('node')->getType();
+ $child_types = $this->getChildTypes($node_type);
+ if (empty($child_types) || empty($this->userNoAccessChildren)) {
+ return $build;
+ }
+
+ // Check if there are purchasable items the user doesn't already have access to.
+ try {
+ $offers_response = $this->catalog->getOffer($this->userNoAccessChildren);
+ $offers = $offers_response->getData();
+ }
+ catch (\Exception $ex) {
+ return $build;
+ }
+ $child_offers = FALSE;
+ if (!empty($offers->getPricingItems())) {
+ foreach ($offers->getPricingItems() as $pricing_item) {
+ if (!empty($pricing_item->getProducts())) {
+ $child_offers = TRUE;
+ break;
+ }
+ }
+ }
+ if (!$child_offers) {
+ return $build;
+ }
+
+ // Get string replacements.
+ $t_values = $child_type_labels = $child_type_labels_plural = [];
+ foreach ($child_types as $child_type) {
+ $child_type_labels[] = $this->getTypeLabel($child_type);
+ $child_type_labels_plural[] = $this->getTypeLabelPlural($child_type);
+ }
+ if (!empty($child_type_labels)) {
+ $t_values[':type'] = implode(' or ', $child_type_labels);
+ }
+ if (!empty($child_type_labels_plural)) {
+ $t_values[':types'] = implode(' or ', $child_type_labels_plural);
+ }
+ $starts_with_vowel = in_array(reset($t_values)[0], ['a', 'e', 'i', 'o', 'u']) ? TRUE : FALSE;
+ $t_values[':a'] = $starts_with_vowel ? 'an' : 'a';
+
+ // Build display.
+ $build['title'] = !empty($child_prompt_config['title']) ? $this->t($child_prompt_config['title'], $t_values) : '';
+ $build['text'] = !empty($child_prompt_config['text']) ? $this->t($child_prompt_config['text'], $t_values) : '';
+ return $build;
+ }
+
+ /**
+ * Returns whether to show item children in access panel or not.
+ *
+ * @return boolean
+ */
+ public function showChildren(): bool {
+ $node_type = !empty($this->contextNode) ? $this->contextNode->getType() : $this->getContextValue('node')->getType();
+ switch ($node_type) {
+ case HW_NODE_TYPE_MONOGRAPH:
+ case HW_NODE_TYPE_REPORT_GUIDELINE:
+ case HW_NODE_TYPE_JOURNAL:
+ case HW_NODE_TYPE_ISSUE:
+ return TRUE;
+ default:
+ return FALSE;
+ }
+ }
+
+ /**
+ * Get child content types based on a given node type.
+ *
+ * @param string $node_type
+ * Node type to get child types for.
+ *
+ * @return array
+ * An array of child node types.
+ */
+ public function getChildTypes(string $node_type): array {
+ switch ($node_type) {
+ case HW_NODE_TYPE_MONOGRAPH:
+ case HW_NODE_TYPE_REPORT_GUIDELINE:
+ $chapter_types = hwjma_core_get_book_chunk_types();
+ $types = [reset($chapter_types)];
+ break;
+
+ case HW_NODE_TYPE_ISSUE:
+ $types = [HW_NODE_TYPE_ARTICLE];
+ break;
+
+ case HW_NODE_TYPE_JOURNAL:
+ $types = [HW_NODE_TYPE_ISSUE, HW_NODE_TYPE_ARTICLE];
+ break;
+
+ default:
+ $types = [];
+ break;
+
+ }
+ return $types;
+ }
+
+ /**
+ * Get display label (singular) for a given node type.
+ *
+ * @param string $node_type
+ * Node type to get the display label for.
+ *
+ * @return string
+ * Display label for the given node type.
+ */
+ public function getTypeLabel(string $node_type): string {
+ $label = '';
+ if (in_array($node_type, hwjma_core_get_book_chunk_types())) {
+ $label = 'chapter';
+ }
+ else {
+ switch ($node_type) {
+ case HW_NODE_TYPE_MONOGRAPH:
+ $label = 'monograph';
+ break;
+
+ case HW_NODE_TYPE_REPORT_GUIDELINE:
+ $label = 'report-guideline';
+ break;
+
+ case HW_NODE_TYPE_ISSUE:
+ $label = 'whole issue';
+ break;
+
+ case HW_NODE_TYPE_ARTICLE:
+ $label = 'article';
+ break;
+
+ case HW_NODE_TYPE_JOURNAL:
+ $label = 'journal';
+ break;
+
+ case HW_NODE_TYPE_TEST_REVIEW:
+ $label = 'test-review';
+ break;
+ }
+ }
+ return $label;
+ }
+
+ /**
+ * Get display label (plural) for a given node type.
+ *
+ * @param string $node_type
+ * Node type to get the display label for.
+ *
+ * @return string
+ * Display label for the given node type.
+ */
+ public function getTypeLabelPlural(string $node_type): string {
+ $label = '';
+ if (in_array($node_type, hwjma_core_get_book_chunk_types())) {
+ $label = 'chapters';
+ }
+ else {
+ switch ($node_type) {
+ case HW_NODE_TYPE_MONOGRAPH:
+ $label = 'monographs';
+ break;
+
+ case HW_NODE_TYPE_REPORT_GUIDELINE:
+ $label = 'report-guidelines';
+ break;
+
+ case HW_NODE_TYPE_ISSUE:
+ $label = 'issues';
+ break;
+
+ case HW_NODE_TYPE_ARTICLE:
+ $label = 'articles';
+ break;
+
+ case HW_NODE_TYPE_JOURNAL:
+ $label = 'journals';
+ break;
+
+ case HW_NODE_TYPE_TEST_REVIEW:
+ $label = 'test-reviews';
+ break;
+
+ }
+ }
+ return $label;
+ }
+
+ /**
+ * Given a purchase item type, get the corresponding node type(s).
+ *
+ * @string $product_type
+ * The product type as returned by the offers service.
+ *
+ * @return array
+ * An array of corresponding node type(s).
+ */
+ public function getProductNodeTypes(string $product_type): array {
+ $type_map = [
+ 'monograph' => [HW_NODE_TYPE_MONOGRAPH],
+ 'report-guideline' => [HW_NODE_TYPE_REPORT_GUIDELINE],
+ 'monograph-chapter' => hwjma_core_get_book_chunk_types(),
+ 'report-guideline-chapter' => hwjma_core_get_book_chunk_types(),
+ 'journal' => [HW_NODE_TYPE_JOURNAL],
+ 'issue' => [HW_NODE_TYPE_ISSUE],
+ 'article' => [HW_NODE_TYPE_ARTICLE],
+ 'test-review' => [HW_NODE_TYPE_TEST_REVIEW],
+ ];
+ if (!empty($type_map[$product_type])) {
+ return $type_map[$product_type];
+ }
+
+ return [];
+ }
+
+ /**
+ * Get products grouped by type for a given pricing item.
+ *
+ * @param \HighWire\Clients\Catalog\PricingItem $pricing_item
+ * The pricing item to get grouped products list for.
+ *
+ * @return array
+ * An array of product objects, keyed by content type.
+ */
+ protected function groupProductsByType(PricingItem $pricing_item): array {
+ $node_type = $this->contextNode->getType();
+ $chapter_type = $this->contextNode->get('chapter_type')->getString();
+ $chapter_type_order = ($chapter_type == 'monograph-item-chapter') ? 'monograph' : 'report-guideline';
+ $type_order_unit = ($chapter_type == 'monograph-item-chapter') ? 'monograph-chapter' : 'report-guideline-chapter';
+ $type_order = [];
+ if (in_array($node_type, hwjma_core_get_book_chunk_types())) {
+ $type_order = [$type_order_unit, $chapter_type_order];
+ }
+ else {
+ switch ($node_type) {
+ case HW_NODE_TYPE_MONOGRAPH:
+ $type_order = ['monograph'];
+ break;
+
+ case HW_NODE_TYPE_REPORT_GUIDELINE:
+ $type_order = ['report-guideline'];
+ break;
+
+ case HW_NODE_TYPE_JOURNAL:
+ $type_order = ['journal'];
+ break;
+
+ case HW_NODE_TYPE_ISSUE:
+ $type_order = ['issue', 'journal'];
+ break;
+
+ case HW_NODE_TYPE_ARTICLE:
+ $type_order = ['article', 'issue', 'journal'];
+ break;
+
+ case HW_NODE_TYPE_TEST_REVIEW:
+ $type_order = ['test-review'];
+ break;
+ }
+ }
+ if (empty($type_order)) {
+ return [];
+ }
+ $products = [];
+ foreach ($type_order as $type) {
+ $products[$type] = $pricing_item->getProductByType($type);
+ }
+ return $products;
+ }
+
+ /**
+ * Get products currency symbol.
+ *
+ */
+ public function getCurrencySymbol($cur) {
+ if (!$cur) {
+ return false;
+ }
+ $currencies = array(
+ 'USD' => '$', // US Dollar
+ 'EUR' => '€', // Euro
+ 'CRC' => '₡', // Costa Rican Colón
+ 'GBP' => '£', // British Pound Sterling
+ 'ILS' => '₪', // Israeli New Sheqel
+ 'INR' => '₹', // Indian Rupee
+ 'JPY' => '¥', // Japanese Yen
+ 'KRW' => '₩', // South Korean Won
+ 'NGN' => '₦', // Nigerian Naira
+ );
+ if (array_key_exists($cur, $currencies)) {
+ return $currencies[$cur];
+ }
+ else {
+ return $cur;
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/modules/highwire/hwjma_ecommerce/templates/hwjma-access-panel.html.twig b/web/modules/highwire/hwjma_ecommerce/templates/hwjma-access-panel.html.twig
new file mode 100644
index 000000000..6ed5eab8a
--- /dev/null
+++ b/web/modules/highwire/hwjma_ecommerce/templates/hwjma-access-panel.html.twig
@@ -0,0 +1,14 @@
+{% if not user_access %}
+
+{% endif %}
\ No newline at end of file
diff --git a/web/modules/highwire/hwjma_ecommerce/templates/hwjma-purchase-offer-item.html.twig b/web/modules/highwire/hwjma_ecommerce/templates/hwjma-purchase-offer-item.html.twig
new file mode 100644
index 000000000..258bf6e06
--- /dev/null
+++ b/web/modules/highwire/hwjma_ecommerce/templates/hwjma-purchase-offer-item.html.twig
@@ -0,0 +1,14 @@
+{% if label or purchase_link %}
+
+{% endif %}
\ No newline at end of file
diff --git a/web/modules/highwire/hwjma_ecommerce/templates/hwjma-purchase-offer.html.twig b/web/modules/highwire/hwjma_ecommerce/templates/hwjma-purchase-offer.html.twig
new file mode 100644
index 000000000..d2f5388fb
--- /dev/null
+++ b/web/modules/highwire/hwjma_ecommerce/templates/hwjma-purchase-offer.html.twig
@@ -0,0 +1,12 @@
+
+ {% if product is not empty %}
+
{{ product }}
+ {% endif %}
+ {% if purchase_offers is not empty %}
+
+ {% for purchase_offer in purchase_offers %}
+
{{ purchase_offer }}
+ {% endfor %}
+
+ {% endif %}
+
\ No newline at end of file
diff --git a/web/modules/highwire/hwjma_search/hwjma_search.info.yml b/web/modules/highwire/hwjma_search/hwjma_search.info.yml
new file mode 100644
index 000000000..be245716b
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/hwjma_search.info.yml
@@ -0,0 +1,9 @@
+name: HWJMA Search
+type: module
+core: 8.x
+dependencies:
+ - search_api
+ - search_api_autocomplete
+ - search_api_solr
+version: 8.x-1.1
+package: 'HWJMA'
diff --git a/web/modules/highwire/hwjma_search/hwjma_search.libraries.yml b/web/modules/highwire/hwjma_search/hwjma_search.libraries.yml
new file mode 100644
index 000000000..2bdf7d21d
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/hwjma_search.libraries.yml
@@ -0,0 +1,40 @@
+drupal.hwjma_search.facets:
+ version: VERSION
+ js:
+ js/facets.js: {}
+ dependencies:
+ - core/jquery
+ - core/drupal
+ - core/jquery.once
+
+drupal.hwjma_search.facets-daterange:
+ version: VERSION
+ js:
+ js/facets-daterange.js: {}
+ dependencies:
+ - core/jquery
+ - core/drupal
+ - core/jquery.once
+ - hwjma_search/parsley.parsleyjs
+
+drupal.hwjma_search.google-analytics:
+ version: VERSION
+ js:
+ js/hwjma_search_ga.js: {}
+ dependencies:
+ - core/jquery
+ - core/drupal
+ - core/jquery.once
+ - core/drupalSettings
+
+parsley.parsleyjs:
+ remote: https://github.com/guillaumepotier/Parsley.js
+ version: 2.8.0
+ license:
+ name: MIT
+ url: https://github.com/guillaumepotier/Parsley.js/blob/master/LICENSE
+ js:
+ https://cdnjs.cloudflare.com/ajax/libs/parsley.js/2.8.0/parsley.min.js:
+ { type: external, minified: true }
+ dependencies:
+ - core/jquery
diff --git a/web/modules/highwire/hwjma_search/hwjma_search.module b/web/modules/highwire/hwjma_search/hwjma_search.module
new file mode 100644
index 000000000..2191f967f
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/hwjma_search.module
@@ -0,0 +1,247 @@
+total_rows) && !empty($view->getItemsPerPage())) {
+ $request = \Drupal::request();
+ $total_pages = ceil($view->total_rows / $view->getItemsPerPage());
+ $total_pages--;
+ $current_page = $request->query->get('page');
+
+ if ($current_page > $total_pages) {
+ $request->query->set('page', $total_pages);
+ $options['query'] = $request->query->all();
+ $url = \Drupal\Core\Url::fromUri('internal:/search', $options);
+ $response = new \Symfony\Component\HttpFoundation\RedirectResponse($url->toString());
+ $response->send();
+ }
+ }
+}
+
+/**
+ * Custom solr query tweaking for hwjma.
+ *
+ * @param \Solarium\Core\Query\QueryInterface $solarium_query
+ * The Solarium query object, as generated from the Search API query.
+ * @param \Drupal\search_api\Query\QueryInterface $query
+ * The Search API query object representing the executed search query.
+ */
+function hwjma_search_search_api_solr_query_alter(\Solarium\Core\Query\QueryInterface $solarium_query, \Drupal\search_api\Query\QueryInterface $query) {
+ $solarium_query->addParam('defType', 'edismax');
+
+ //Query field boost
+ //Set boosting for related article block
+ if ($query->getSearchId() == 'views_block:related_sidebar_blocks__block_6') {
+ $query_fields = [
+ 'tm_fulltext_search^3',
+ 'tm_title_plain^8',
+ ];
+ $solarium_query->setBoost(true);
+ $solarium_query->setQueryFields($query_fields);
+ $solarium_query->setMatchInclude(true);
+ }
+
+ //Set boosting for search page
+ if ($query->getSearchId() == 'views_page:search__page_1' || $query->getSearchId() == 'views_rest:search__rest_export_1') {
+ $fragment_boost = '-700.0';
+ if (isset($_GET['fg_boost'])) {
+ $fragment_boost = $_GET['fg_boost'];
+ }
+ $section_boost = 2.7;
+ $query_text = $query->getKeys();
+ if (!empty($query_text)) {
+ // Escape special character.
+ if (!strstr($query_text, "\(")) {
+ $user_query = str_replace("(", "\(", $query_text);
+ }
+
+ if (!strstr($user_query, "\)")) {
+ $user_query = str_replace(")", "\)", $user_query);
+ }
+
+ if (!strstr($user_query, "\,")) {
+ $user_query = str_replace(",", "\,", $user_query);
+ }
+ // Set new querystring as a user query in SOLR.
+ $query->keys($user_query);
+ }
+ $matches = [];
+ $boolean_search = FALSE;
+ preg_match("%.*(\WAND|OR|NOT\W).*%", $query_text, $matches);
+ if (empty($matches)) {
+ $mm = "100%";
+
+ if (!empty($_GET['mm'])) {
+ $mm = $_GET['mm'];
+ $solarium_query->addParam("mm", $mm);
+ $solarium_query->addParam("mm.autoRelax", "true");
+ }
+
+ // Allow setting the parameter to blank so that it
+ // Removes it from search.
+ elseif (!isset($_GET["mm"])) {
+ $solarium_query->addParam("mm", $mm);
+ $solarium_query->addParam("mm.autoRelax", "true");
+ }
+ }
+ $solarium_query->addParam('sow', 'false');
+
+ // AI Keyword boost
+ $index_fields = $query->getIndex()->getFields();
+ if (!empty($index_fields)) {
+ $boost_functions = $solarium_query->getEDisMax()->getBoostFunctions();
+ $backend_manager = \Drupal::service('plugin.manager.search_api.backend');
+ $solr_backend = $backend_manager->createInstance('search_api_solr');
+ $solr_field_mapping = $solr_backend->getSolrFieldNames($query->getIndex());
+ $multiplier = isset($_GET['multiplier']) ? $_GET['multiplier'] : 10000;
+ $payload_queries = [];
+ $solarium_query->addParam('userQuery', $query->getKeys());
+ if (!empty($query->getKeys())) {
+ foreach ($index_fields as $field) {
+ if ($field->getType() == 'hw_string_payload_boost' && !empty($solr_field_mapping[$field->getPropertyPath()])) {
+ $solr_field = $solr_field_mapping[$field->getPropertyPath()];
+ $payload_boost_func = 'if($' . $solr_field . ',product(add($' . $solr_field . ',1),' . $multiplier . '),0)';
+ if (empty($boost_functions)) {
+ $boost_functions = $payload_boost_func;
+ }
+ else {
+ $boost_functions = $boost_functions . " " . $payload_boost_func;
+ }
+ $solarium_query->getDisMax()->setBoostFunctions($boost_functions);
+ $solarium_query->addParam($solr_field, 'payload(' . $solr_field . ',$userQuery)');
+ }
+ }
+ }
+ }
+ $components = $solarium_query->getComponents();
+ if (!empty($components['edismax'])) {
+ $phrase_fields = '';
+ if (isset($_GET['pf'])) {
+ $phrase_fields = $_GET['pf'];
+ $components['edismax']->setPhraseFields($phrase_fields);
+ }
+ else {
+ $phrase_fields = [
+ 'tm_title_plain^8',
+ 'itm_taxonomy_terms^5',
+ 'tm_keywords^2',
+ 'tm_fulltext_search^3',
+ 'tm_authors_full_name^2',
+ 'tm_doi^2',
+ ];
+
+ $phrase_fields = implode(' ', $phrase_fields);
+ $components['edismax']->setPhraseFields($phrase_fields);
+ }
+ $query_fields = '';
+ if (isset($_GET['qf'])) {
+ $query_fields = $_GET['qf'];
+ $components['edismax']->setQueryFields($query_fields);
+ }
+ else {
+ $query_fields = [
+ 'tm_title_plain^8',
+ 'itm_taxonomy_terms^5',
+ 'tm_keywords^2',
+ 'tm_fulltext_search^3',
+ 'tm_authors_full_name^2',
+ 'tm_doi^2',
+ ];
+ $query_fields = implode(" ", $query_fields);
+ $components['edismax']->setQueryFields($query_fields);
+ }
+ $slop = 10;
+ if (isset($_GET['ps'])) {
+ $slop = $_GET['ps'];
+ }
+ $components['edismax']->setPhraseSlop($slop);
+ }
+ }
+
+ if ($query->getSearchID() == 'views_attachment:search__attachment_1') {
+ $query_lowercase = strtolower($solarium_query->getFilterQuery('filters_1')->getQuery());
+ $solarium_query->getFilterQuery('filters_1')->setQuery($query_lowercase);
+ }
+
+ if ($query->getSearchId() == 'views_page:search__page_1' || $query->getSearchId() == 'views_rest:search__rest_export_1') {
+ $components = $solarium_query->getComponents();
+ $date_min = \Drupal::request()->query->get('date-min');
+ if (!empty($date_min)) {
+ $date_min = "$date_min-01-01T00:00:00Z";
+ }
+
+ $date_max = \Drupal::request()->query->get('date-max');
+ if (!empty($date_max)) {
+ $date_max = "$date_max-12-31T00:00:00Z";
+ }
+
+ $date_filter = '';
+ $date_date_field = 'ds_date_ppub_facet:';
+ if (!empty($date_min) && empty($date_max)) {
+ $date_filter = $date_date_field . "[$date_min TO NOW]";
+ }
+ elseif (!empty($date_min) && !empty($date_max)) {
+ $date_filter = $date_date_field . "[$date_min TO $date_max]";
+ }
+ elseif (empty($date_min) && !empty($date_max)) {
+ $date_filter = $date_date_field . "[ * TO $date_max]";
+ }
+
+ if (!empty($date_filter)) {
+ $solarium_query->createFilterQuery("date_ppub")->setQuery($date_filter);
+ }
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function hwjma_search_theme() {
+ return [
+ 'hwjma_facet_summary' => [
+ 'template' => 'hwjma-facet-summary',
+ 'variables' => ['facet_name' => '', 'active_count' => 0, 'active_items' => []],
+ ],
+ ];
+}
+
+/**
+ * Implements template_preprocess_THEME().
+ */
+function template_preprocess_hwjma_facet_summary(&$variables) {
+ $variables['attributes']['class'][] = 'facet-summary-toggle';
+ $variables['facet_id'] = Html::getUniqueId($variables['facet_name']);
+}
+/**
+ * Implements hook_form_ID_alter().
+ */
+function hwjma_search_form_hwjma_search_browse_alter(&$form, FormStateInterface $form_state) {
+ $form['form_id']['#access'] = FALSE;
+ $form['form_build_id']['#access'] = FALSE;
+
+}
+
+// Alter the date_ppub facet of solr before indexing so that it can ignore timezone
+function hwjma_search_search_api_solr_documents_alter(array &$documents, IndexInterface $index, array $items) {
+ foreach ($documents as $document) {
+ $fields = $document->getFields();
+ // check if field exist and has value
+ if(!empty($fields['ds_date_ppub_facet'])) {
+ //code to convert 2020-11-23T11:57:17Z to "2020-11-23T00:00:00Z" so that timezone become same and sort work on dates only
+ $append_timestamp = '00:00:00Z';
+ $without_timestamp = explode('T', $fields['ds_date_ppub_facet']);
+ if(is_array($without_timestamp) && (!empty($without_timestamp))) {
+ $unformatted_date = $without_timestamp[0]."T".$append_timestamp;
+ $document->setField('ds_date_ppub_facet', $unformatted_date);
+ }
+ }
+ }
+}
+
diff --git a/web/modules/highwire/hwjma_search/js/facets-daterange.js b/web/modules/highwire/hwjma_search/js/facets-daterange.js
new file mode 100644
index 000000000..ed712f2a0
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/js/facets-daterange.js
@@ -0,0 +1,218 @@
+/**
+ * @file
+ * Adds interaction behavior to search facets date range form.
+ */
+
+(function ($) {
+
+ 'use strict';
+
+ Drupal.hwjma_search_daterange = Drupal.hwjma_search_daterange || {};
+ Drupal.behaviors.hwjmaSearchDateRanges = {
+ attach: function (context, settings) {
+ var $form = $('form.facets-date-range', context);
+ var $rangeField = $form.find('.facets-date-range__inputs input');
+
+ if ($form.attr('data-parsley-validate') != undefined) {
+ $form.once('hwjma-daterange-form-validate').each(Drupal.hwjma_search_daterange.formValidate);
+ $rangeField.once('hwjma-daterange-field-validate').each(Drupal.hwjma_search_daterange.rangeFieldValidate);
+ }
+
+ $form.find('input[name="include_options"]').once('hwjma-daterange-change-radios').change(Drupal.hwjma_search_daterange.changeRadios);
+ $rangeField.once('hwjma-daterange-focus-rangefields').focus(Drupal.hwjma_search_daterange.focusRangefields);
+ $rangeField.once('hwjma-daterange-keyup-rangefields').keyup(Drupal.hwjma_search_daterange.changeRangefields);
+ $rangeField.once('hwjma-daterange-change-rangefields').change(Drupal.hwjma_search_daterange.changeRangefields);
+ },
+ };
+
+ /**
+ * Behavior for parsley form validation.
+ */
+ Drupal.hwjma_search_daterange.formValidate = function () {
+ var $form = $(this);
+ var formInstance = $form.parsley();
+ if (typeof (formInstance) == undefined) {
+ return;
+ }
+ formInstance.on('field:validated', function () {
+ var isValid = formInstance.isValid({ group: 'range', force: false });
+ if (!isValid) {
+ $form.children('.datefields-error').removeClass('hidden');
+ $form.find('.form-actions button').attr('disabled', true);
+ }
+ else {
+ $form.children('.datefields-error').addClass('hidden');
+ $form.find('.form-actions button').removeAttr('disabled');
+ }
+ });
+ }
+
+ /**
+ * Behavior for parsley range field validation.
+ */
+ Drupal.hwjma_search_daterange.rangeFieldValidate = function () {
+ var $rangeField = $(this);
+ var $errorMsg = $rangeField.parents('form.facets-date-range').last().children('.datefields-error');
+ var fieldInstance = $rangeField.parsley();
+ if (typeof (fieldInstance) == "undefined") {
+ return;
+ }
+ fieldInstance.on('field:error', function (fieldInstance) {
+ $rangeField.attr('aria-invalid', '').attr('aria-described-by', $errorMsg.attr('id')).parent().addClass('error has-error');
+ });
+ fieldInstance.on('field:success', function (fieldInstance) {
+ $rangeField.removeAttr('aria-invalid aria-described-by').parent().removeClass('error has-error');
+ });
+ }
+
+ /**
+ * Behavior for when radios are changed.
+ */
+ Drupal.hwjma_search_daterange.changeRadios = function () {
+ var radio = this;
+ var $parent = $(radio).parents('form.facets-date-range').last();
+ var $rangeFields = $parent.find('.facets-date-range__inputs input');
+ var $actions = $parent.find('.form-actions');
+
+ // Hide submit button if radios have been reset to default value.
+ if ($(radio).data('default-value') == radio.value) {
+ $actions.addClass('hidden');
+ }
+
+ if (radio.value == 0) {
+ // "Include all" was selected, so clear input fields.
+ $rangeFields.val('').attr('data-val-reset', '');
+ if ($parent.attr('data-parsley-validate') != undefined) {
+ $rangeFields.each(function (i) {
+ $(this).parsley().validate();
+ });
+ }
+
+ // Only show submit button if including all will clear active items.
+ if ($(radio).data('default-value') !== 0) {
+ $actions.removeClass('hidden');
+ }
+ }
+ else {
+ // "Include some" was selected, so focus the first textfield.
+ $rangeFields.first().focus();
+ }
+ }
+
+ /**
+ * Behavior for when date range input fields are changed.
+ */
+ Drupal.hwjma_search_daterange.changeRangefields = function (e) {
+ // 'this' references rangefield element.
+ var $parent = $(this).parents('form.facets-date-range').last();
+ var $rangeFields = $parent.find('.facets-date-range__inputs input');
+ var $actions = $parent.find('.form-actions');
+
+ // Exclude keys that don't alter input value.
+ var discardKeyCode = [
+ 16, // shift
+ 17, // ctrl
+ 18, // alt
+ 20, // caps lock
+ 33, // page up
+ 34, // page down
+ 35, // end
+ 36, // home
+ 37, // left arrow
+ 38, // up arrow
+ 39, // right arrow
+ 40, // down arrow
+ 9, // tab
+ 13, // enter
+ 27 // esc
+ ];
+ if (e.type != 'keyup' || $.inArray(e.keyCode, discardKeyCode) === -1) {
+ if (Drupal.hwjma_search_daterange.checkDefaultState($rangeFields)) {
+ // Hide submit button.
+ $actions.addClass('hidden');
+ }
+ else {
+ // Show submit button.
+ $actions.removeClass('hidden');
+ }
+ }
+ }
+
+ /**
+ * Behavior for when date range input fields are focused.
+ */
+ Drupal.hwjma_search_daterange.focusRangefields = function () {
+ // 'this' references input element.
+ var $parent = $(this).parents('form.facets-date-range').last();
+ var $rangeFields = $parent.find('.facets-date-range__inputs input');
+ var $actions = $parent.find('.form-actions');
+
+ // Make sure 'include some' option is checked.
+ var $includeSome = $parent.find('input[name="include_options"][value=1]');
+ if ($includeSome.prop('checked') == false) {
+ $rangeFields.attr('data-val-reset', '');
+ $includeSome.prop('checked', true);
+ }
+
+ // Set default value of input fields.
+ $rangeFields.each(Drupal.hwjma_search_daterange.setDefaultValue);
+ if (Drupal.hwjma_search_daterange.checkDefaultState($rangeFields)) {
+ // Hide submit button.
+ $actions.addClass('hidden');
+ }
+ else {
+ // Show submit button.
+ $actions.removeClass('hidden');
+ }
+ }
+
+ /**
+ * Function to check if list of input elements are all in their default states.
+ */
+ Drupal.hwjma_search_daterange.checkDefaultState = function ($inputs) {
+ var isDefaultState = true;
+ $inputs.each(function () {
+ if (String($(this).data('default-value')) !== this.value) {
+ isDefaultState = false;
+ return false;
+ }
+ });
+
+ return isDefaultState;
+ }
+
+ /**
+ * Set default value of date range input fields.
+ */
+ Drupal.hwjma_search_daterange.setDefaultValue = function () {
+ var $rangeField = $(this);
+ var is_value_reset = ($rangeField.attr('data-val-reset') != undefined) ? true : false;
+ var defaultValue = $rangeField.data('default-value');
+ if (is_value_reset && typeof (defaultValue) !== undefined && $rangeField.val() == '') {
+ $rangeField.val(defaultValue);
+ $rangeField.removeAttr('data-val-reset');
+ }
+ }
+
+ /**
+ * Function to completely reset date range form.
+ */
+ Drupal.hwjma_search_daterange.resetValues = function () {
+ var $form = $(this);
+ $form.find('input[type="radio"]').each(function () {
+ var radio = this;
+ if (String($(radio).data('default-value')) == radio.value) {
+ $(radio).prop('checked', true);
+ }
+ });
+ $form.find('.facets-date-range__inputs input').each(function () {
+ var $input = $(this);
+ if (typeof ($input.data('default-value')) !== 'undefined') {
+ $input.val($input.data('default-value')).trigger('change');
+ }
+ });
+ if (typeof ($form.parsley()) !== 'undefined') {
+ $form.parsley().validate();
+ }
+ }
+}(jQuery));
diff --git a/web/modules/highwire/hwjma_search/js/facets.js b/web/modules/highwire/hwjma_search/js/facets.js
new file mode 100644
index 000000000..15b984813
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/js/facets.js
@@ -0,0 +1,394 @@
+/**
+ * @file
+ * Transforms links into checkboxes.
+ */
+
+(function ($) {
+
+ 'use strict';
+ Drupal.hwjma_search = Drupal.hwjma_search || {};
+ Drupal.behaviors.facetsCheckboxWidgets = {
+ attach: function (context, settings) {
+ Drupal.hwjma_search.makeCheckboxes();
+ $('.hwjma-checkbox-multiple-apply').once('hwjma-checkbox-multiple-apply-click').each(Drupal.hwjma_search.applyClick);
+ $('.hwjma-checkbox-multiple-cancel').once('hwjma-checkbox-multiple-cancel-click').click(Drupal.hwjma_search.cancelClick);
+ $('.facets-checkbox', context).once('hwjma-checkbox-toggle-multiple').change(Drupal.hwjma_search.facetCheckboxMutliple);
+ $('.facet-item-include-all .form-checkbox', context).once('hwjma-include-all-multiple-toggle').change(Drupal.hwjma_search.includeAllChange);
+ }
+ };
+
+ /**
+ * When cancel is applied revert to original state.
+ */
+ Drupal.hwjma_search.cancelClick = function () {
+ var $cancelButton = $(this);
+ var $parent = $cancelButton.closest('ul.hwjma-checkbox-multiple-facet');
+ $parent.find('input[type="checkbox"]').each(function (e) {
+ var $checkbox = $(this);
+ var checked = $checkbox.data('facet-checkboxes-saved-state');
+ if (checked != undefined) {
+ if ($checkbox.hasClass('facets-checkbox-include-all')) {
+ Drupal.hwjma_search.checkIncludeAll($checkbox);
+ }
+ else {
+ $checkbox.prop('checked', true);
+ }
+ }
+ else {
+ if ($checkbox.hasClass('facets-checkbox-include-all')) {
+ Drupal.hwjma_search.uncheckIncludeAll($checkbox);
+ }
+ else {
+ $checkbox.prop('checked', false);
+ }
+ }
+ });
+ $parent.find('.hwjma-checkbox-multiple-apply').attr('disabled', '').removeAttr('data-clear-facet-url');
+ }
+ Drupal.hwjma_search.facetCheckboxMutliple = function () {
+ var $checkbox = $(this);
+ var $root = $checkbox.parents('.js-facets-checkbox-links').last();
+ var $includeAllCheckbox = $root.find('.facet-item-include-all input[type="checkbox"]');
+ var $parents = $checkbox.parents('li.facet-item');
+ var activeCount = $root.attr('data-drupal-facet-active-item-count') ? $root.attr('data-drupal-facet-active-item-count') : 0;
+ var initCheckedCount = $root.attr('data-drupal-facet-checked-item-count') ? $root.attr('data-drupal-facet-checked-item-count') : 0;
+
+ // Toggle children when ancestor is clicked/toggle ancestor when children are clicked
+ $parents.first().children('ul .list-facet-items').find('.facets-checkbox').each(function () {
+ $(this).prop('checked', false);
+ });
+
+ // Uncheck ancestor checkboxes.
+ $parents.each(function (i) {
+ if (i != 0) {
+ $(this).find('> .facets-checkbox').attr('checked', false);
+ }
+ });
+
+ // If all siblings are checked, check parent
+ if ($checkbox.is(':checked') === true) {
+ var siblingsChecked = true;
+ var $siblingCheckboxes = $parents.first().siblings('.facet-item').find('> .facets-checkbox');
+
+ $siblingCheckboxes.each(function () {
+
+ if ($(this).is(':checked') !== true) {
+ siblingsChecked = false;
+ return;
+ }
+ });
+
+ if (siblingsChecked === true) {
+ $checkbox.prop('checked', false);
+ $siblingCheckboxes.each(function () {
+ $(this).prop('checked', false);
+ });
+
+ // Check the parent check box.
+ var $parentNode = $checkbox.parents().eq(3);
+ if ($parentNode !== undefined) {
+ $parentNode.find('.facets-checkbox-include-all').attr('checked', true).trigger('change')
+ }
+ }
+ }
+
+ var $applyButton = $root.find('.hwjma-checkbox-multiple-apply');
+ var resetFacet = $applyButton.attr('data-clear-facet-url') ? true : false;
+
+ // Find all checkboxes in this group.
+ var boxesChecked = false;
+ var $allCheckboxes = $checkbox.closest('.js-facets-checkbox-links').find('input.facets-checkbox');
+ var $checkedCheckboxes = $allCheckboxes.filter(':checked');
+
+ if ((resetFacet || initCheckedCount == activeCount) && $checkedCheckboxes.length == $allCheckboxes.length) {
+ $allCheckboxes.each(function () {
+ $(this).prop('checked', false);
+ Drupal.hwjma_search.checkIncludeAll($includeAllCheckbox);
+ });
+ } else if ($checkedCheckboxes.length > 0) {
+ Drupal.hwjma_search.uncheckIncludeAll($includeAllCheckbox);
+ }
+ else if ((resetFacet || initCheckedCount == activeCount) && $checkedCheckboxes.length == 0) {
+ Drupal.hwjma_search.checkIncludeAll($includeAllCheckbox);
+ }
+
+ // Set/remove disabled attribute on apply button depending on default state of checkboxes.
+ var isDefaultState = true;
+ if (resetFacet && $checkedCheckboxes.length != activeCount) {
+ isDefaultState = false;
+ }
+ else {
+ $allCheckboxes.each(function () {
+ var thisCheckbox = this;
+ if ((thisCheckbox.hasAttribute('data-facet-checkboxes-saved-state') && !thisCheckbox.checked) || (!thisCheckbox.hasAttribute('data-facet-checkboxes-saved-state') && thisCheckbox.checked)) {
+ isDefaultState = false;
+ return false;
+ }
+ });
+ }
+ if (isDefaultState) {
+ $applyButton.attr('disabled', '');
+ }
+ else {
+ $applyButton.removeAttr('disabled');
+ }
+ };
+
+ Drupal.hwjma_search.uncheckIncludeAll = function ($includeAllCheckbox) {
+ $includeAllCheckbox.removeAttr('checked').removeAttr('disabled');
+ $includeAllCheckbox.parents('.facet-item-include-all').removeClass('form-disabled');
+ };
+
+ Drupal.hwjma_search.checkIncludeAll = function ($includeAllCheckbox) {
+ $includeAllCheckbox.prop('checked', true).prop('disabled', true);
+ $includeAllCheckbox.parents('.facet-item-include-all').addClass('form-disabled');
+ };
+
+ Drupal.hwjma_search.includeAllChange = function () {
+ var $includeAllCheckbox = $(this);
+ var url = $includeAllCheckbox.attr('data-url');
+ var checked = this.checked;
+ if (checked) {
+ Drupal.hwjma_search.checkIncludeAll($includeAllCheckbox);
+ $includeAllCheckbox.parents('li.facet-item-include-all').siblings().find('.facets-checkbox').attr('checked', false);
+ // Redirect to facet url.
+ var isMultiple = $includeAllCheckbox.closest('.js-facets-checkbox-links').hasClass('hwjma-checkbox-multiple-facet');
+ if (typeof (url) !== 'undefined' && isMultiple == false) {
+ window.location.href = url;
+ }
+ }
+ // Set/remove disabled attribute on apply button depending on default state of include all checkbox.
+ var defaultChecked = $includeAllCheckbox.data('facet-checkboxes-saved-state') != undefined ? true : false;
+ if (checked && defaultChecked || !checked && !defaultChecked) {
+ $includeAllCheckbox.closest('.hwjma-checkbox-multiple-facet').find('.hwjma-checkbox-multiple-apply').attr('disabled', '');
+ }
+ else {
+ var $applyButton = $includeAllCheckbox.closest('.hwjma-checkbox-multiple-facet').find('.hwjma-checkbox-multiple-apply');
+ $applyButton.removeAttr('disabled');
+ if (url) {
+ $applyButton.attr('data-clear-facet-url', url);
+ }
+ }
+ };
+
+ Drupal.hwjma_search.applyClick = function () {
+ var $applyButton = $(this);
+ $applyButton.click(function (e) {
+ var args = [];
+ var unchecked = [];
+ // Build new url based on clear facet url or current url, depending on whether the
+ // 'include all' checkbox has been clicked.
+ var l = location;
+ if ($applyButton.attr('data-clear-facet-url')) {
+ l = Drupal.hwjma_search.getLocation($applyButton.attr('data-clear-facet-url'));
+ }
+ var getParams = Drupal.hwjma_search.getQueryStringAsObject(l.search);
+ var facetId = $applyButton.closest('.js-facets-checkbox-links').data('drupal-facet-id');
+ var base_url = l.protocol + '//' + l.host + l.pathname;
+ var totalCheckboxes = $applyButton.closest('.js-facets-checkbox-links').find('input.facets-checkbox').length;
+
+ $applyButton.closest('.js-facets-checkbox-links').find('input.facets-checkbox').each(function (e) {
+ var $checkbox = $(this)
+ var arg = $checkbox.data('facetarg');
+ if ($checkbox.is(':checked')) {
+ args.push(arg);
+ }
+ else {
+ unchecked.push(arg);
+ }
+ });
+
+ var startingArgFacetIndex = 0;
+
+ // Find the starting index for new facet filters.
+ var currentIndexes = [];
+ var facetParamReg = /f\[((\d)+)\]/;
+
+ Object.keys(getParams).forEach(function (key, value) {
+ // Figure out the last index value.
+ var facetIndexMatch = facetParamReg.exec(key);
+ if (facetIndexMatch != null) {
+ currentIndexes.push(facetIndexMatch[1]);
+ }
+
+ if (unchecked.indexOf(getParams[key]) >= 0) {
+ delete getParams[key];
+ }
+ });
+
+ if (currentIndexes.length > 0) {
+ currentIndexes = currentIndexes.sort(function (a, b) {
+ return a.index - b.index;
+ });
+ startingArgFacetIndex = parseInt(currentIndexes.slice(-1).pop()) + 1;
+ }
+
+ for (var i = 0; i < args.length; i++) {
+ getParams["f[" + startingArgFacetIndex + "]"] = args[i];
+ startingArgFacetIndex++;
+ }
+
+ if (getParams != null) {
+ base_url = base_url + '?' + Drupal.hwjma_search.encodeQueryData(getParams);
+ }
+
+ window.location.href = base_url;
+ });
+ };
+
+ Drupal.hwjma_search.encodeQueryData = function (data) {
+ var ret = [];
+ for (var d in data)
+ ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d]));
+ return ret.join('&');
+ }
+
+ /**
+ * Turns all facet links into checkboxes.
+ */
+ Drupal.hwjma_search.makeCheckboxes = function () {
+ // Find all checkbox facet links and give them a checkbox.
+ var $list = $('.js-facets-checkbox-links');
+ var $links = $list.find('.facet-item a');
+ var checkedCount = $links.filter('.is-active').length;
+ $list.once('facets-checked-count').attr('data-drupal-facet-checked-item-count', checkedCount);
+ $links.once('facets-checkbox-transform').each(Drupal.hwjma_search.makeCheckbox);
+ };
+
+ /**
+ * Replace a link with a checked checkbox.
+ */
+ Drupal.hwjma_search.makeCheckbox = function () {
+ var $link = $(this);
+ var isMultiple = !!$link.closest('ul.hwjma-checkbox-multiple-facet').length;
+ var active = $link.hasClass('is-active');
+ var description = $link.html();
+ var href = $link.attr('href');
+ var id = $link.data('drupal-facet-item-id');
+ var facetArg = $link.data('drupal-facet-item-arg');
+
+ var checkbox = $('')
+ .attr('id', id)
+ .data('facetarg', facetArg)
+ .data('facetsredir', href);
+ var label = $('');
+
+ if (isMultiple == false) {
+ checkbox.on('change.facets', function (e) {
+ Drupal.hwjma_search.disableFacet($link.parents('.js-facets-checkbox-links'));
+ window.location.href = $(this).data('facetsredir');
+ });
+ }
+
+
+ if (active) {
+ checkbox.attr('checked', true);
+ checkbox.attr('data-facet-checkboxes-saved-state', 'checked');
+
+ label.find('.js-facet-deactivate').remove();
+ }
+
+ $link.before(checkbox).before(label).remove();
+
+ };
+
+ /**
+ * Disable all facet checkboxes in the facet and apply a 'disabled' class.
+ *
+ * @param {object} $facet
+ * jQuery object of the facet.
+ */
+ Drupal.hwjma_search.disableFacet = function ($facet) {
+ $facet.addClass('facets-disabled');
+ $('input.facets-checkbox').click(Drupal.hwjma_search.preventDefault);
+ $('input.facetapi-checkbox', $facet).attr('disabled', true);
+ };
+
+ /**
+ * Event listener for easy prevention of event propagation.
+ *
+ * @param {object} e
+ * Event.
+ */
+ Drupal.hwjma_search.preventDefault = function (e) {
+ e.preventDefault();
+ };
+
+ Drupal.hwjma_search.getQueryStringKey = function (key, queryString) {
+ queryString = typeof (queryString) !== "undefined" ? queryString : window.location.search;
+ return Drupal.hwjma_search.getQueryStringAsObject(queryString)[key];
+ };
+
+ Drupal.hwjma_search.getQueryStringAsObject = function (queryString) {
+ queryString = typeof (queryString) !== "undefined" ? queryString : window.location.search;
+ var b, cv, e, k, ma, sk, v, r = {},
+ d = function (v) { return decodeURIComponent(v).replace(/\+/g, " "); }, //# d(ecode) the v(alue)
+ q = queryString.substring(1), //# suggested: q = decodeURIComponent(window.location.search.substring(1)),
+ s = /([^&;=]+)=?([^&;]*)/g //# original regex that does not allow for ; as a delimiter: /([^&=]+)=?([^&]*)/g
+ ;
+
+ //# ma(make array) out of the v(alue)
+ ma = function (v) {
+ //# If the passed v(alue) hasn't been setup as an object
+ if (typeof v != "object") {
+ //# Grab the cv(current value) then setup the v(alue) as an object
+ cv = v;
+ v = {};
+ v.length = 0;
+
+ //# If there was a cv(current value), .push it into the new v(alue)'s array
+ //# NOTE: This may or may not be 100% logical to do... but it's better than loosing the original value
+ if (cv) { Array.prototype.push.call(v, cv); }
+ }
+ return v;
+ };
+
+ //# While we still have key-value e(ntries) from the q(uerystring) via the s(earch regex)...
+ while (e = s.exec(q)) { //# while((e = s.exec(q)) !== null) {
+ //# Collect the open b(racket) location (if any) then set the d(ecoded) v(alue) from the above split key-value e(ntry)
+ b = e[1].indexOf("[");
+ v = d(e[2]);
+
+ //# As long as this is NOT a hash[]-style key-value e(ntry)
+ if (b < 0) { //# b == "-1"
+ //# d(ecode) the simple k(ey)
+ k = d(e[1]);
+
+ //# If the k(ey) already exists
+ if (r[k]) {
+ //# ma(make array) out of the k(ey) then .push the v(alue) into the k(ey)'s array in the r(eturn value)
+ r[k] = ma(r[k]);
+ Array.prototype.push.call(r[k], v);
+ }
+ //# Else this is a new k(ey), so just add the k(ey)/v(alue) into the r(eturn value)
+ else {
+ r[k] = v;
+ }
+ }
+ //# Else we've got ourselves a hash[]-style key-value e(ntry)
+ else {
+ //# Collect the d(ecoded) k(ey) and the d(ecoded) sk(sub-key) based on the b(racket) locations
+ k = d(e[1].slice(0, b));
+ sk = d(e[1].slice(b + 1, e[1].indexOf("]", b)));
+
+ //# ma(make array) out of the k(ey)
+ r[k] = ma(r[k]);
+
+ //# If we have a sk(sub-key), plug the v(alue) into it
+ if (sk) { r[k][sk] = v; }
+ //# Else .push the v(alue) into the k(ey)'s array
+ else { Array.prototype.push.call(r[k], v); }
+ }
+ }
+
+ //# Return the r(eturn value)
+ return r;
+ };
+
+ Drupal.hwjma_search.getLocation = function (href) {
+ var l = document.createElement("a");
+ l.href = href;
+ return l;
+ }
+
+})(jQuery);
\ No newline at end of file
diff --git a/web/modules/highwire/hwjma_search/src/Form/FacetsDateRangeForm.php b/web/modules/highwire/hwjma_search/src/Form/FacetsDateRangeForm.php
new file mode 100644
index 000000000..4495f6043
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/src/Form/FacetsDateRangeForm.php
@@ -0,0 +1,154 @@
+ 'radios',
+ '#title' => $this->t('Include options'),
+ '#title_display' => 'invisible',
+ '#options' => [
+ 0 => $this->t('Include all'),
+ 1 => $this->t('Include date published'),
+ ],
+ '#default_value' => !empty($items['include_options']) ? $items['include_options'] : 0,
+ '#attributes' => ['data-default-value' => 0],
+ ];
+
+ // Date range input fields.
+ $form['range'] = [
+ '#type' => 'container',
+ '#attributes' => ['class' => ['facets-date-range__inputs']],
+ ];
+
+ $input_attr = [
+ 'placeholder' => 'YYYY',
+ 'maxlength' => 4,
+ 'data-default-value' => '',
+ 'data-parsley-group' => 'range',
+ 'data-parsley-trigger' => 'focusout',
+ 'pattern' => '\d{4}',
+ 'data-parsley-pattern' => '\d{4}',
+ 'data-parsley-errors-messages-disabled' => '',
+ ];
+
+
+ foreach (['min', 'max'] as $k) {
+ $label = $k == 'min' ? $this->t('From') : $this->t('To');
+ $form['range'][$k] = [
+ '#type' => 'textfield',
+ '#title' => $label,
+ '#default_value' => '',
+ '#attributes' => $input_attr,
+ ];
+
+ $form['range'][$k]['#date_time_element'] = 'none';
+ }
+
+ $form['range_error'] = [
+ '#type' => 'html_tag',
+ '#tag' => 'div',
+ '#attributes' => ['class' => ['datefields-error', 'hidden'], 'id' => Html::getUniqueId('datefields-error')],
+ '#value' => $this->t('Please enter @datetype in the form %format', [
+ '@datetype' => 'years',
+ '%format' => 'YYYY',
+ ]),
+ ];
+
+ $request = $this->getRequest();
+ $query = $request->query;
+ $query = !empty($query) ? $query->all() : [];
+
+ if (!empty($query['date-min'])) {
+ $form['range']['min']['#default_value'] = $query['date-min'];
+ $form['range']['min']['#attributes']['data-default-value'] = $form['range']['min']['#default_value'];
+ }
+
+ if (!empty($query['date-max'])) {
+ $form['range']['max']['#default_value'] = $query['date-max'];
+ $form['range']['max']['#attributes']['data-default-value'] = $form['range']['max']['#default_value'];
+ }
+
+ // Make sure 'include some' is checked'.
+ $form['include_options']['#default_value'] = !empty($items['include_options']) ? $items['include_options'] : 0;
+ $form['include_options']['#attributes']['data-default-value'] = !empty($items['include_options']) ? $items['include_options'] : 0;
+
+
+ // Form actions.
+ $form['actions'] = ['#type' => 'actions', '#attributes' => ['class' => ['hidden']]];
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Apply'),
+ '#attributes' => ['class' => ['btn', 'btn-primary']],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $values = $form_state->getValues();
+
+ // Build redirect url.
+ $request = $this->getRequest();
+ $query = $request->query;
+
+ $options = ['query' => !empty($query) ? $query->all() : []];
+
+ // If 'include all' option was submitted, remove any facets.
+ if ($values['include_options'] == '0') {
+ unset($options['query']['date-min']);
+ unset($options['query']['date-max']);
+ }
+
+ // Include published dates.
+ else {
+ if (!empty($values['min'])) {
+ $options['query']['date-min'] = $values['min'];
+ }
+
+ if (!empty($values['max'])) {
+ $options['query']['date-max'] = $values['max'];
+ }
+ }
+
+ $path = $request->getPathInfo();
+ $redirect_url = Url::fromUserInput(urldecode($path), $options);
+
+ $form_state->setRedirectUrl($redirect_url);
+ }
+
+}
diff --git a/web/modules/highwire/hwjma_search/src/Form/searchBrowse.php b/web/modules/highwire/hwjma_search/src/Form/searchBrowse.php
new file mode 100644
index 000000000..995253058
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/src/Form/searchBrowse.php
@@ -0,0 +1,122 @@
+getPath();
+ $placeholders = explode("/",$current_path);
+ $nid = $placeholders['2'];
+ $node = \Drupal::entityManager()->getStorage('node')->load($nid);
+ $form['#action'] = '/search';
+ $form['#method'] = 'get';
+ $facets = [];
+ $content_type_labels = [];
+ $content_type_labels[HW_NODE_TYPE_REPORT_GUIDELINE] = 'report or guideline';
+ $content_type_labels[HW_NODE_TYPE_MONOGRAPH] = 'monograph';
+ $pid = $node->get('parent')->getString();
+
+ // get the parent node detail
+ if (empty($pid)) {
+ $pnodetype = $node->bundle();
+ $pnid = $node->get('nid')->getString();
+ $nodetitle = $node->get('title')->getString();
+ }
+ else {
+ $pnode = \Drupal::entityManager()->getStorage('node')->load($pid);
+ $pnodetype = $pnode->bundle();
+ $nodetitle = $pnode->get('title')->getString();
+ $pnid = $pnode->get('nid')->getString();
+ }
+ switch ($node->bundle()) {
+ case 'journal':
+ $type_label = 'periodical';
+ $facets['journal_title_facet'] = $node->get('title')->getString();
+ $facets['chapter_type'] = "periodical-article";
+ break;
+ case 'journal_issue':
+ $type_label = 'issue';
+ $facets['issue'] = $node->get('issue')->getString();
+ $facets['volume'] = $node->get('volume')->getString();
+ $facets['journal_title_facet'] = $node->get('journal_title')->getString();
+ $facets['chapter_type'] = "periodical-article";
+ break;
+ case 'item_report_guideline':
+ case 'item_chapter':
+ case 'item_monograph':
+ case 'item_back_matter':
+ case 'item_front_matter':
+ $type_label = $content_type_labels[$pnodetype];
+ $facets['parent_facet'] = $pnid;
+ $facets['chapter_type'] = ($pnodetype == 'item_report_guideline') ? "report-guideline-item-chapter" : "monograph-item-chapter";
+ break;
+ default:
+ return NULL;
+ }
+ $placeholder = "Search within this $type_label";
+ $i = 0;
+ foreach ($facets as $facet_field => $facet_value) {
+ if (is_array($facet_value)) {
+ foreach ($facet_value as $val) {
+ $form["f[$i]"] = [
+ '#type' => 'hidden',
+ '#value' => $facet_field . ':' . trim(strip_tags($val)),
+ ];
+ $i++;
+ }
+ }
+ else {
+ $form["f[$i]"] = [
+ '#type' => 'hidden',
+ '#value' => $facet_field . ':' . trim(strip_tags($facet_value)),
+ ];
+ $i++;
+ }
+ }
+ $form['query'] = array(
+ '#type' => 'textfield',
+ '#placeholder' => $placeholder,
+ '#required' => TRUE,
+ '#attributes' => array('class' => array('form-control journal-article__searchbox__input')),
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#attributes' => array('class' => array('btn btn btn-journal-article__searchbox-submit')),
+ '#value' => $this->t('Search'),
+ '#name' => '',
+ );
+ return $form;
+ }
+
+ /**
+ * Form submission handler.
+ *
+ * This is included to fulfill the Drupal\Core\Form\FormInterface interface, but does nothing.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Do nothing.
+ }
+
+}
\ No newline at end of file
diff --git a/web/modules/highwire/hwjma_search/src/Plugin/Block/DateRangeFilter.php b/web/modules/highwire/hwjma_search/src/Plugin/Block/DateRangeFilter.php
new file mode 100644
index 000000000..1e3e0e045
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/src/Plugin/Block/DateRangeFilter.php
@@ -0,0 +1,182 @@
+requestStack = $request_stack;
+ $this->logger = $logger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('request_stack'),
+ $container->get('logger.factory')->get('hwjma_search')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(): array {
+ $build = [];
+
+ $query = $this->requestStack->getCurrentRequest()->query;
+ $options = ['query' => !empty($query) ? $query->all() : []];
+
+ // Set the correct default value when date facet is active.
+ $items['include_options'] = 0;
+ if (!empty($options['query']['date-max']) || !empty($options['query']['date-min'])) {
+ $items['include_options'] = 1;
+ }
+
+ $build['range'] = \Drupal::formBuilder()->getForm('Drupal\hwjma_search\Form\FacetsDateRangeForm', $items);
+
+ // Build active items for render array.
+ $active_items_render = [];
+ if (!empty($options['query']['date-min'])) {
+ $path = $this->requestStack->getCurrentRequest()->getPathInfo();
+ $remove_min_query = $this->requestStack->getCurrentRequest()->query->all();
+ if (!empty($remove_min_query['date-min'])) {
+ unset($remove_min_query['date-min']);
+ }
+ $remove_min_options = ['query' => !empty($remove_min_query) ? $remove_min_query : []];
+
+ $remove_url = Url::fromUserInput(urldecode($path), $remove_min_options);
+ $title = [
+ '#markup' => $this->t('From @date-start', ['@date-start' => $options['query']['date-min']]),
+ '#prefix' => '',
+ '#suffix' => '',
+ ];
+ $active_items_render['min'] = (new Link($title, $remove_url))->toRenderable();
+ $active_items_render['min']['#wrapper_attributes'] = ['class' => ['facet-item']];
+ $active_items_render['min']['#attributes']['data-drupal-facet-item-value'] = $options['query']['date-min'];
+ }
+
+ if (!empty($options['query']['date-max'])) {
+ $path = $this->requestStack->getCurrentRequest()->getPathInfo();
+ $remove_max_query = $this->requestStack->getCurrentRequest()->query->all();
+ if (!empty($remove_max_query['date-max'])) {
+ unset($remove_max_query['date-max']);
+ }
+ $remove_max_options = ['query' => !empty($remove_max_query) ? $remove_max_query : []];
+
+ $remove_url = Url::fromUserInput(urldecode($path), $remove_max_options);
+ $title = [
+ '#markup' => $this->t('To @date-end', ['@date-end' => $options['query']['date-max']]),
+ '#prefix' => '',
+ '#suffix' => '',
+ ];
+ $active_items_render['max'] = (new Link($title, $remove_url))->toRenderable();
+ $active_items_render['max']['#wrapper_attributes'] = ['class' => ['facet-item']];
+ $active_items_render['max']['#attributes']['data-drupal-facet-item-value'] = $options['query']['date-max'];
+ }
+
+ $build['active_items'] = [
+ '#theme' => 'item_list',
+ '#items' => $active_items_render,
+ '#attributes' => ['data-drupal-facet-id' => 'hwjma_search_date_filter', 'class' => ['list-facet-items--active', 'list-unstyled']],
+ '#cache' => [
+ 'contexts' => [
+ 'url.path',
+ 'url.query_args',
+ ],
+ ],
+ ];
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration(): array {
+ return [
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blockForm($form, FormStateInterface $form_state): array {
+ $form = parent::blockForm($form, $form_state);
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blockSubmit($form, FormStateInterface $form_state) {
+ parent::blockSubmit($form, $form_state);
+ foreach ($form_state->getValues() as $k => $v) {
+ $this->configuration[$k] = $v;
+ }
+ }
+
+}
diff --git a/web/modules/highwire/hwjma_search/src/Plugin/Block/search.php b/web/modules/highwire/hwjma_search/src/Plugin/Block/search.php
new file mode 100644
index 000000000..631dc00ca
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/src/Plugin/Block/search.php
@@ -0,0 +1,45 @@
+
+
+ ';
+
+ return array(
+ '#type' => 'markup',
+ '#markup' => $output,
+ '#cache' => array(
+ 'max-age' => 0,
+ ),
+ '#allowed_tags' => ['div', 'form', 'input', 'button'],
+ );
+ }
+}
\ No newline at end of file
diff --git a/web/modules/highwire/hwjma_search/src/Plugin/Block/searchBrowse.php b/web/modules/highwire/hwjma_search/src/Plugin/Block/searchBrowse.php
new file mode 100644
index 000000000..c0c9498eb
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/src/Plugin/Block/searchBrowse.php
@@ -0,0 +1,32 @@
+getForm('Drupal\hwjma_search\Form\searchBrowse',$node);
+
+ return $form;
+ }
+}
\ No newline at end of file
diff --git a/web/modules/highwire/hwjma_search/src/Plugin/facets/processor/FacetSortCountAlpha.php b/web/modules/highwire/hwjma_search/src/Plugin/facets/processor/FacetSortCountAlpha.php
new file mode 100644
index 000000000..65c46520e
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/src/Plugin/facets/processor/FacetSortCountAlpha.php
@@ -0,0 +1,33 @@
+getCount() == $b->getCount()) {
+ return strcasecmp($a->getDisplayValue(), $b->getDisplayValue());
+ }
+
+ return 0;
+ }
+
+}
diff --git a/web/modules/highwire/hwjma_search/src/Plugin/facets/processor/HideFacetNoItems.php b/web/modules/highwire/hwjma_search/src/Plugin/facets/processor/HideFacetNoItems.php
new file mode 100644
index 000000000..cd8a76ce3
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/src/Plugin/facets/processor/HideFacetNoItems.php
@@ -0,0 +1,49 @@
+Note: Only takes effect when result minimum count is 0."),
+ * stages = {
+ * "build" = 25
+ * }
+ * )
+ */
+class HideFacetNoItems extends ProcessorPluginBase implements BuildProcessorInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet, array $results) {
+ // Don't do anything if results are already omitted when empty,
+ // or if there are active items to be displayed.
+ if ($facet->getMinCount() > 0 || count($facet->getActiveItems()) > 0) {
+ return $results;
+ }
+
+ // Check if all results have a count of 0.
+ $all_results_empty = TRUE;
+ foreach ($results as $result) {
+ if ($result->getCount() > 0) {
+ $all_results_empty = FALSE;
+ break;
+ }
+ }
+
+ if ($all_results_empty) {
+ $results = [];
+ }
+
+ return $results;
+ }
+
+}
diff --git a/web/modules/highwire/hwjma_search/src/Plugin/facets/processor/InsertMissingActiveResults.php b/web/modules/highwire/hwjma_search/src/Plugin/facets/processor/InsertMissingActiveResults.php
new file mode 100644
index 000000000..29678886a
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/src/Plugin/facets/processor/InsertMissingActiveResults.php
@@ -0,0 +1,55 @@
+Note: missing active items will be created with a count of -1."),
+ * stages = {
+ * "build" = -25
+ * }
+ * )
+ */
+class InsertMissingActiveResults extends ProcessorPluginBase implements BuildProcessorInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet, array $results) {
+ // Don't do anything if results haven't been limited or if there are no active items.
+ $active_items = $facet->getActiveItems();
+ if ($facet->getHardLimit() === 0 || empty($active_items)) {
+ return $results;
+ }
+
+ // Build index of active results.
+ $active_results = [];
+ /** @var \Drupal\facets\Result\ResultInterface $result */
+ foreach($results as $result) {
+ if ($result->isActive()) {
+ $active_results[$result->getRawValue()] = TRUE;
+ }
+ }
+
+ // Add missing active items as results with a count of -1.
+ foreach($active_items as $active_item) {
+ if (!isset($active_results[$active_item])) {
+ $new_result = new Result($facet, $active_item, $active_item, -1);
+ $new_result->setActiveState(TRUE);
+ $results[] = $new_result;
+ }
+ }
+
+ return $results;
+ }
+
+}
diff --git a/web/modules/highwire/hwjma_search/src/Plugin/facets/processor/LastResultClearFacet.php b/web/modules/highwire/hwjma_search/src/Plugin/facets/processor/LastResultClearFacet.php
new file mode 100644
index 000000000..1e97d192e
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/src/Plugin/facets/processor/LastResultClearFacet.php
@@ -0,0 +1,57 @@
+getActiveItems());
+
+ if (empty($active_items)) {
+ return $results;
+ }
+
+ $num_results = count($facet->getResults());
+ if ($num_results == $active_items + 1) {
+ /** @var \Drupal\facets\Result\ResultInterface $result */
+ foreach ($results as $id => $result) {
+ if (!$result->isActive()) {
+ $facet_url_alias = $facet->getUrlAlias();
+ if (!empty($facet_url_alias) && $result->getUrl()) {
+ $url_options = $result->getUrl()->getOptions();
+ if (!empty($url_options['query']['f'])) {
+ foreach ($url_options['query']['f'] as $k => $facet_query_param) {
+ if (strpos($facet_query_param, $facet_url_alias . ':') === 0) {
+ unset($url_options['query']['f'][$k]);
+ }
+ }
+ $result->setUrl($result->getUrl()->setOptions($url_options));
+ }
+ }
+ }
+ }
+ }
+
+ return $results;
+ }
+
+}
diff --git a/web/modules/highwire/hwjma_search/src/Plugin/facets/widget/HWJMAContentTypeSelectCheckboxes.php b/web/modules/highwire/hwjma_search/src/Plugin/facets/widget/HWJMAContentTypeSelectCheckboxes.php
new file mode 100644
index 000000000..56a3c99e7
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/src/Plugin/facets/widget/HWJMAContentTypeSelectCheckboxes.php
@@ -0,0 +1,150 @@
+ FALSE,
+ 'reset_text' => $this->t('Everything'),
+ 'show_reset_count' => FALSE,
+ ] + parent::defaultConfiguration();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
+ $form = parent::buildConfigurationForm($form, $form_state, $facet);
+
+ $form['show_reset_link'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Show reset link'),
+ '#default_value' => $this->getConfiguration()['show_reset_link'],
+ ];
+ $form['reset_text'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Reset text'),
+ '#default_value' => $this->getConfiguration()['reset_text'],
+ '#states' => [
+ 'visible' => [
+ ':input[name="widget_config[show_reset_link]"]' => ['checked' => TRUE],
+ ],
+ 'required' => [
+ ':input[name="widget_config[show_reset_link]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ $form['reset_id'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Reset id'),
+ '#default_value' => $this->getConfiguration()['reset_id'],
+ '#states' => [
+ 'visible' => [
+ ':input[name="widget_config[show_reset_link]"]' => ['checked' => TRUE],
+ ],
+ 'required' => [
+ ':input[name="widget_config[show_reset_link]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ $form['show_reset_count'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Show reset count'),
+ '#default_value' => $this->getConfiguration()['show_reset_count'],
+ ];
+
+ return $form;
+ }
+
+
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet) {
+ $build = parent::build($facet);
+ $build['#attributes']['class'][] = 'js-facets-checkbox-links';
+ $build['#attached']['library'][] = 'hwjma_search/drupal.hwjma_search.facets';
+ $build['#attached']['library'][] = 'facets/drupal.facets.checkbox-widget';
+ $query_string = $facet->getUrlAlias();
+ // Check straight away if show reset link is selected
+ if ($this->getConfiguration()['show_reset_link']) {
+ // Setup total count of all items
+ $totalCount = NULL;
+ if ($this->getConfiguration()['show_reset_count']) {
+ $count = 0;
+ foreach ($facet->getResults() as $results) {
+ $totalCount += $results->getCount();
+ }
+ }
+
+ // Build url for 'include all' option from first result.
+ $query_string = $facet->getUrlAlias();
+ $result_url = $facet->getResults()[0]->getUrl();
+ $include_all_url = '';
+ if ($result_url) {
+ $include_all_url = new Url($result_url->getRouteName(), $result_url->getRouteParameters(), $result_url->getOptions());
+ }
+ if (!empty($include_all_url)) {
+ $options = $include_all_url->getOptions();
+ if (!empty($options['query']['f'])) {
+ foreach ($options['query']['f'] as $k => $facet_parameter) {
+ if (strpos($facet_parameter, $query_string . ':') === 0) {
+ unset($options['query']['f'][$k]);
+ }
+ }
+ }
+ $reset_url = $include_all_url->setOptions($options);
+ }
+ if (array_key_exists('#items', $build)) {
+ $include_all = [
+ '#type' => 'link',
+ '#url' => $reset_url,
+ '#title' => [
+ '#theme' => "facets_result_item",
+ '#is_active' => FALSE,
+ '#value' => $this->getConfiguration()['reset_text'],
+ '#show_count' => $this->getConfiguration()['show_reset_count'],
+ '#count' => $totalCount,
+ '#facet' => $facet,
+ ],
+ '#wrapper_attributes' => [
+ 'class' => [
+ 'facet-item',
+ ],
+ ],
+ '#attributes' => [
+ 'data-drupal-facet-item-id' => $this->getConfiguration()['reset_id'],
+ 'data-drupal-facet-item-value' => "reset",
+ ],
+ ];
+ // Add active class to 'all items' link if there are no active facet items.
+ if (empty($facet->getActiveItems())) {
+ $include_all['#attributes']['class'][] = 'is-active';
+ }
+ array_unshift($build['#items'], $include_all);
+
+ }
+ }
+ return $build;
+ }
+}
\ No newline at end of file
diff --git a/web/modules/highwire/hwjma_search/src/Plugin/facets/widget/IncludeAllCheckBoxes.php b/web/modules/highwire/hwjma_search/src/Plugin/facets/widget/IncludeAllCheckBoxes.php
new file mode 100644
index 000000000..91cf322d2
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/src/Plugin/facets/widget/IncludeAllCheckBoxes.php
@@ -0,0 +1,212 @@
+ FALSE,
+ 'display_active_items_summary' => FALSE,
+ ];
+ return $default + parent::defaultConfiguration();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet) {
+ $build = parent::build($facet);
+
+ if (!isset($build['#items'])) {
+ return $build;
+ }
+
+ $config = $this->getConfiguration();
+ $separate_active = !empty($config['separate_active_items']) ? TRUE : FALSE;
+ $include_summary = !empty($config['display_active_items_summary']) ? TRUE : FALSE;
+ $build['#attributes']['class'][] = 'js-facets-checkbox-links';
+ $build['#attached']['library'][] = 'hwjma_search/drupal.hwjma_search.facets';
+
+ // Add form element.
+ $include_all = [
+ '#type' => 'checkbox',
+ '#title' => 'Include all',
+ '#wrapper_attributes' => [
+ 'class' => ['facet-item', 'facet-item-include-all'],
+ ],
+ '#attributes' => ['class' => ['facets-checkbox-include-all']],
+ ];
+
+ $active_items = $summary = [];
+ if (empty($facet->getActiveItems())) {
+ // If there are no active items, "Include All" should be checked & disabled.
+ $include_all['#attributes']['data-facet-checkboxes-saved-state'] = 'checked';
+ $include_all['#attributes']['checked'] = 'checked';
+ $include_all['#attributes']['disabled'] = 'disabled';
+ }
+ else {
+ // Build url for 'include all' option from first result.
+ $query_string = $facet->getUrlAlias();
+ if (!empty($facet->getResults()[0])) {
+ $result_url = $facet->getResults()[0]->getUrl();
+ $include_all_url = '';
+ if ($result_url) {
+ $include_all_url = new Url($result_url->getRouteName(), $result_url->getRouteParameters(), $result_url->getOptions());
+ }
+ }
+
+ if (!empty($include_all_url)) {
+ $options = $include_all_url->getOptions();
+ if (!empty($options['query']['f'])) {
+ foreach ($options['query']['f'] as $k => $facet_parameter) {
+ if (strpos($facet_parameter, $query_string . ':') === 0) {
+ unset($options['query']['f'][$k]);
+ }
+ }
+ }
+ $include_all_url->setOptions($options);
+ }
+ $include_all['#attributes']['data-url'] = $include_all_url->toString();
+
+ // Add separate list of active items.
+ if ($separate_active || $include_summary) {
+ $active_items = $this->getActiveResults($facet->getResults(), $facet, $include_summary);
+ }
+ }
+
+ if (!empty($active_items['summary'])) {
+ $summary = [
+ '#theme' => 'hwjma_facet_summary',
+ '#active_count' => count($active_items['summary']),
+ '#active_items' => $active_items['summary'],
+ '#facet_name' => $query_string,
+ '#wrapper_attributes' => [
+ 'class' => ['facet-item', 'facet-item-facet-summary'],
+ ],
+ ];
+ }
+
+ array_unshift($build['#items'], $include_all);
+ $build_copy = ['facet_items' => $build];
+ $build['#wrapper_attributes'] = ['class' => ['facet-item', 'facet-item-facet-items']];
+ $build_copy['#items'] = !empty($summary) ? [$summary, $build] : [$build];
+
+
+ if ($separate_active && !empty($active_items['active_results'])) {
+ $build_copy['active_items'] = [
+ '#theme' => $this->getFacetItemListThemeHook($facet),
+ '#items' => $active_items['active_results'],
+ '#context' => ['list_item' => 'facet_list_active_items'],
+ '#attributes' => ['data-drupal-facet-id' => $facet->id(), 'class' => ['list-facet-items--active']],
+ ];
+ }
+ return $build_copy;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function buildListItems(FacetInterface $facet, ResultInterface $result) {
+ if ($result->getCount() == -1) {
+ return [];
+ }
+
+ $items = parent::buildListItems($facet, $result);
+ return $items;
+ }
+
+ /**
+ * Recursive function to get active facet results.
+ *
+ * @param array $results
+ * An array of facet Result objects
+ * @param FacetInterface $facet
+ * The facet object
+ * @param bool $include_summary
+ * Whether to include active items summary.
+ *
+ * @return array
+ * An array of active results as links.
+ */
+ protected function getActiveResults(array $results, FacetInterface $facet, bool $include_summary = FALSE) {
+ $active = [];
+ $summary = [];
+ foreach($results as $result) {
+ if ($result->isActive()) {
+ $link = $this->prepareLink($result);
+ $link['#title']['#show_count'] = FALSE;
+ $link['#attributes'] = [
+ 'class' => ['is-active'],
+ 'data-drupal-facet-item-id' => $facet->getUrlAlias() . '-' . str_replace(' ', '-', $result->getRawValue()),
+ 'data-drupal-facet-item-value' => $result->getRawValue(),
+ ];
+ $link['#wrapper_attributes'] = ['class' => ['facet-item']];
+ $active[$result->getRawValue()] = $link;
+ if ($include_summary) {
+ $summary[] = $result->getDisplayValue();
+ }
+ }
+ if ($result->hasActiveChildren()) {
+ $active_children = $this->getActiveResults($result->getChildren(), $facet, $include_summary);
+ if (!empty($active_children['active_results'])) {
+ $active = array_merge($active, $active_children['active_results']);
+ }
+ if (!empty($active_children['summary'])) {
+ $summary += $active_children['summary'];
+ }
+ }
+ }
+
+ return [
+ 'active_results' => $active,
+ 'summary' => $summary,
+ ];
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
+ $form = parent::buildConfigurationForm($form, $form_state, $facet);
+
+ $form['separate_active_items'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Separate Active Items'),
+ '#default_value' => $this->getConfiguration()['separate_active_items'],
+ '#description' => $this->t('Add active items as a separate group for theming purposes'),
+ ];
+
+ $form['display_active_items_summary'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Display Summary of Active Items'),
+ '#default_value' => $this->getConfiguration()['display_active_items_summary'],
+ '#description' => $this->t('Display a summary of active items for this facet.'),
+ ];
+
+ return $form;
+ }
+
+}
\ No newline at end of file
diff --git a/web/modules/highwire/hwjma_search/src/Plugin/facets/widget/IncludeAllCheckBoxesMultiple.php b/web/modules/highwire/hwjma_search/src/Plugin/facets/widget/IncludeAllCheckBoxesMultiple.php
new file mode 100644
index 000000000..efd3225e0
--- /dev/null
+++ b/web/modules/highwire/hwjma_search/src/Plugin/facets/widget/IncludeAllCheckBoxesMultiple.php
@@ -0,0 +1,109 @@
+ 'button',
+ '#value' => $this->t('Cancel'),
+ '#attributes' => ['id' => [$facet->get('id') . '-cancel'], 'class' => ['hwjma-checkbox-multiple-cancel', 'btn-secondary'],'data-bs-dismiss'=>['modal'],]
+ ];
+
+ $apply_button = [
+ '#type' => 'button',
+ '#value' => $this->t('Apply'),
+ '#attributes' => ['id' => [$facet->get('id') . '-apply'], 'class' => ['hwjma-checkbox-multiple-apply', 'btn-primary'], 'disabled' => 'disabled']
+ ];
+ $build['facet_items']['#items'][] = [
+ $cancel_button,
+ $apply_button,
+ '#wrapper_attributes' => [
+ 'class' => ['hwjma-facets-form-actions'],
+ ],
+ ];
+
+ $build['facet_items']['#attributes']['class'][] = 'hwjma-checkbox-multiple-facet';
+ $build['facet_items']['#attributes']['data-drupal-facet-active-item-count'] = count($facet->getActiveItems());
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function buildListItems(FacetInterface $facet, ResultInterface $result) {
+ if ($result->getCount() == -1) {
+ return [];
+ }
+
+ $classes = ['facet-item'];
+ $items = $this->prepareLink($result);
+
+ $children = $result->getChildren();
+ // Check if we need to expand this result.
+ if ($children && ($this->facet->getExpandHierarchy() || $result->isActive() || $result->hasActiveChildren())) {
+
+ $child_items = [];
+ $classes[] = 'facet-item--expanded';
+ foreach ($children as $child) {
+ $child_items[] = $this->buildListItems($facet, $child);
+ }
+
+ $items['children'] = [
+ '#items' => $child_items,
+ ];
+
+ if ($result->hasActiveChildren()) {
+ $classes[] = 'facet-item--active-trail';
+ }
+
+ }
+ else {
+ if ($children) {
+ $classes[] = 'facet-item--collapsed';
+ }
+ }
+
+ if ($result->isActive()) {
+ $items['#attributes'] = ['class' => ['is-active']];
+ }
+
+ // Custom hwjma code for getting the url arg into a data attribute.
+ $url_processor = NULL;
+ $processors = $this->facet->getProcessors();
+ foreach ($processors as $processor) {
+ if ($processor instanceof UrlProcessorHandler) {
+ $url_processor = $processor->getProcessor();
+ }
+ }
+
+ $items['#wrapper_attributes'] = ['class' => $classes];
+ $items['#attributes']['data-drupal-facet-item-id'] = $this->facet->getUrlAlias() . '-' . str_replace(' ', '-', $result->getRawValue());
+
+ // Custom hwjma code for adding arg data attribute.
+ $items['#attributes']['data-drupal-facet-item-arg'] = $this->facet->getUrlAlias() . $url_processor->getSeparator() . $result->getRawValue();
+
+ $items['#attributes']['data-drupal-facet-item-value'] = $result->getRawValue();
+ return $items;
+ }
+
+}
\ No newline at end of file
diff --git a/web/modules/highwire/journal_article_detail/config/install/journal_article_detail.settings.yml b/web/modules/highwire/journal_article_detail/config/install/journal_article_detail.settings.yml
new file mode 100644
index 000000000..6c4432e6d
--- /dev/null
+++ b/web/modules/highwire/journal_article_detail/config/install/journal_article_detail.settings.yml
@@ -0,0 +1,28 @@
+default_view: LastSixMonths
+date_filters:
+ ArticleLifetime: ArticleLifetime
+ LastSixMonths: LastSixMonths
+ ThisMonth: ThisMonth
+ ViewRange: ViewRange
+display_settings: '1'
+metric_types:
+ abstract: abstract
+ full: full
+ pdf: pdf
+ source: source
+ total: 0
+override_title: 'Usage statistics'
+altmetric_statistics_tag: ''
+altmetric_override_title: 'Statistics from Altmetric.com'
+empty_text: 'No statistics are available.'
+default_source_name: Highwire
+display_grand_total: 1
+email_article_display: 1
+thanks_msg: '
Thank you for your interest in spreading the word on HWJMA.
'
+email_article_note: '
NOTE: We only request your email address so that the person you are recommending the page to knows that you wanted them to see it, and that it is not junk mail. We do not capture any email address.
Thank you for your submission. Below is a copy of your eLetter as we received it. Your eLetter, if accepted, should be
+ viewable within a few days.
+
+
Sincerely,
+ The Editorial staff of !journal_name
+ ----------------------------------------
+
!article_link
+
The eLetter !eletter_title was submitted on !submitted_date:
+
!eletter_text
';
+
+ $form['highwire_rr_email_response_sent'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Message to send to the sender of an eLetter:'),
+ '#default_value' => !empty($config->get('highwire_rr_email_response_sent')) ? $config->get('highwire_rr_email_response_sent') : $highwire_rr_email_response_text,
+ );
+
+ // E-letter submission message.
+ $highwire_e_letter_submission_message = 'Thank you for your response. We intend to publish as rapidly as possible all responses that contribute substantially to the topic under discussion.';
+ $form['highwire_e_letter_submission_message'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Message to display when user submits an eLetter:'),
+ '#default_value' => !empty($config->get('highwire_e_letter_submission_message')) ? $config->get('highwire_e_letter_submission_message') : $highwire_e_letter_submission_message,
+ );
+
+ // Notification for Moderator.
+ $form['moderator_notification'] = array(
+ '#title' => t('Notification for Moderators'),
+ '#type' => 'fieldset',
+ );
+
+ $moderator_distribution_email_list = $site_mail;
+ $form['moderator_notification']['moderator_distribution_email_list'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Moderator Distribution Email'),
+ '#required' => TRUE,
+ '#description' => t('The moderator distribution list is created externally to the journal site. The moderator is a publisher employee who manages which eLetters get published, and which do not. (If multiple emails required, please enter one email per line)'),
+ '#default_value' => !empty($config->get('moderator_distribution_email_list')) ? $config->get('moderator_distribution_email_list') : $moderator_distribution_email_list,
+ );
+
+ $moderator_site_feedback_email = $site_mail;
+ $form['moderator_notification']['moderator_site_feedback_email_list'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Site Feedback Email Address'),
+ '#required' => TRUE,
+ '#description' => t('Default site feedback email address'),
+ '#default_value' => !empty($config->get('moderator_site_feedback_email_list')) ? $config->get('moderator_site_feedback_email_list') : $moderator_site_feedback_email,
+ );
+
+ $moderator_notification_from_address = $site_name . ' Response Notification ';
+ $form['moderator_notification']['moderator_notification_from_address'] = array(
+ '#type' => 'textarea',
+ '#title' => t('E-mail to send the emails from'),
+ '#required' => TRUE,
+ '#description' => t('From address, a configurable parameter, to send a moderator notification from this'),
+ '#default_value' => !empty($config->get('moderator_notification_from_address')) ? $config->get('moderator_notification_from_address') : $moderator_notification_from_address,
+ );
+
+ $moderator_notification_to_address = $site_name . ' Response Moderator ';
+ $form['moderator_notification']['moderator_notification_to_address'] = array(
+ '#type' => 'textarea',
+ '#title' => t('E-mail to send the emails to'),
+ '#required' => TRUE,
+ '#description' => t('To address, a configurable parameter, to send a moderator notification to this configured value'),
+ '#default_value' => !empty($config->get('moderator_notification_to_address')) ? $config->get('moderator_notification_to_address') : $moderator_notification_to_address,
+ );
+
+ $moderator_notification_email_subject = $site_name . ' response: "[node:title]""';
+ $form['moderator_notification']['moderator_notification_email_subject'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Subject Line for the Moderator response emails'),
+ '#required' => TRUE,
+ '#description' => t('Email subject line a configurable parameter for the moderator notification alert email'),
+ '#default_value' => !empty($config->get('moderator_notification_email_subject')) ? $config->get('moderator_notification_email_subject') : $moderator_notification_email_subject,
+ );
+
+ $moderator_notification_response_text = '
A new eLetter has been submitted to !article_title and is awaiting moderation.
+
!article_link
+
The eLetter was submitted on [node:created:custom:d m Y]:
+
eLetter copy: [site:url]!eLetter_link_copy
+
!eletter_text
';
+ $form['moderator_notification']['moderator_notification_response_text'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Message to send to the Moderator of an eLetter'),
+ '#required' => TRUE,
+ '#description' => t('Response text a configurable parameter that goes in the body of the moderator notification email'),
+ '#default_value' => !empty($config->get('moderator_notification_response_text')) ? $config->get('moderator_notification_response_text') : $moderator_notification_response_text,
+ );
+
+ // Notification on eLetter publication for Article authors and eLetter author.
+ $form['eLetter_publication_notification'] = array(
+ '#title' => t('Notification on eLetter publication for Article authors and eLetter author'),
+ '#type' => 'fieldset',
+ );
+
+ $form['eLetter_publication_notification']['eletter_rule_action_flag'] = array(
+ '#title' => t('Rule Action Status'),
+ '#type' => 'checkbox',
+ '#description' => t('If selected then rule action will be enabled for "Set eLetter to Release status" button, otherwise no'),
+ '#default_value' => !empty($config->get('eletter_rule_action_flag')) ? $config->get('eletter_rule_action_flag') : 0,
+ );
+
+ $from_address_to_eletterauthor = $site_mail;
+ $form['eLetter_publication_notification']['eLetter_publication_notification_from_address_to_eletterauthor'] = array(
+ '#type' => 'textarea',
+ '#title' => t('E-mail to send the emails from to eLetter Author'),
+ '#required' => TRUE,
+ '#description' => t('From address a configurable parameter, to send a published notification from this to eLetter author'),
+ '#element_validate' => array('highwire_responses_moderation_email_validate'),
+ '#default_value' => !empty($config->get('eLetter_publication_notification_from_address_to_eletterauthor')) ? $config->get('eLetter_publication_notification_from_address_to_eletterauthor') : $from_address_to_eletterauthor,
+ );
+
+ $subject_to_eletterauthor = 'Your Response has been published';
+ $form['eLetter_publication_notification']['eLetter_publication_notification_subject_to_eletterauthor'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Subject line for the notification to eLetter Author'),
+ '#required' => TRUE,
+ '#description' => t('Email subject line a configurable parameter for the published notification alert email'),
+ '#default_value' => !empty($config->get('eLetter_publication_notification_subject_to_eletterauthor')) ? $config->get('eLetter_publication_notification_subject_to_eletterauthor') : $subject_to_eletterauthor,
+ );
+
+ $eletterauthor_response_text = '
Dear !article_author_firstname_lastname,
+
+
An eLetter has been published on [node:field-highwire-c-response-to:field-highwire-a-journal:title]\'s web site. To view it, navigate to your article and click on "eLetter" tab or click the link below.
The Editorial Staff of [node:field-highwire-c-response-to:field-highwire-a-journal:title]
';
+ $form['eLetter_publication_notification']['eLetter_publication_notification_eletterauthor_response_text'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Message to send to the eLetter author on eLetter publication'),
+ '#required' => TRUE,
+ '#description' => t('Response text a configurable parameter that goes in the body of the published notification email'),
+ '#default_value' => !empty($config->get('eLetter_publication_notification_eletterauthor_response_text')) ? $config->get('eLetter_publication_notification_eletterauthor_response_text') : $eletterauthor_response_text,
+ );
+
+ $from_address_to_articleauthor = $site_mail;
+ $form['eLetter_publication_notification']['eLetter_publication_notification_from_address_to_articleauthor'] = array(
+ '#type' => 'textarea',
+ '#title' => t('E-mail to send the emails from to Article Author'),
+ '#required' => TRUE,
+ '#description' => t('From address a configurable parameter, to send a published notification from this to article author'),
+ '#element_validate' => array('highwire_responses_moderation_email_validate'),
+ '#default_value' => !empty($config->get('eLetter_publication_notification_from_address_to_articleauthor')) ? $config->get('eLetter_publication_notification_from_address_to_articleauthor') : $from_address_to_articleauthor,
+ );
+
+ $subject_to_articleauthor = 'A Response regarding your article has been published';
+ $form['eLetter_publication_notification']['eLetter_publication_notification_subject_to_articleauthor'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Subject line for the notification to Article Author'),
+ '#required' => TRUE,
+ '#description' => t('Email subject line a configurable parameter for the published notification alert email'),
+ '#default_value' => !empty($config->get('eLetter_publication_notification_subject_to_articleauthor')) ? $config->get('eLetter_publication_notification_subject_to_articleauthor') : $subject_to_articleauthor,
+ );
+
+ $articleauthor_response_text = '
Dear !article_author_firstname_lastname,
+
+
An eLetter has been published on [node:field-highwire-c-response-to:field-highwire-a-journal:title]\'s web site. To view it, navigate to your article and click on "eLetter" tab or click the link below.
The Editorial Staff of [node:field-highwire-c-response-to:field-highwire-a-journal:title]
';
+
+ $form['eLetter_publication_notification']['eLetter_publication_notification_articleauthor_response_text'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Message to send to the Article author on eLetter publication'),
+ '#required' => TRUE,
+ '#description' => t('Response text a configurable parameter that goes in the body of the published notification email'),
+ '#default_value' => !empty($config->get('eLetter_publication_notification_articleauthor_response_text')) ? $config->get('eLetter_publication_notification_articleauthor_response_text') : $articleauthor_response_text,
+ );
+
+ $distribution_email_list = $site_mail;
+ $form['eLetter_publication_notification']['eLetter_publication_notification_distribution_email_list'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Moderator Distribution Email'),
+ '#required' => TRUE,
+ '#description' => t('The moderator distribution list is created externally to the journal site. The moderator is a publisher employee who manages which eLetters get published, and which do not. (If multiple emails required, please enter one email per line)'),
+ '#default_value' => !empty($config->get('eLetter_publication_notification_distribution_email_list')) ? $config->get('eLetter_publication_notification_distribution_email_list') : $distribution_email_list,
+ );
+
+ // Notification for article author.
+ $form['author_notification'] = array(
+ '#title' => t("Notification for Article's authors"),
+ '#type' => 'fieldset',
+ );
+
+ $form['author_notification']['show_set_to_wait_status'] = array(
+ '#title' => t('Show "Set eLetter to wait status"'),
+ '#type' => 'checkbox',
+ '#default_value' => !empty($config->get('show_set_to_wait_status')) ? $config->get('show_set_to_wait_status') : 0,
+ '#description' => t('If selected then moderation page will have "Set eLetter to wait status" button, otherwise no'),
+ );
+
+ $form['author_notification']['hw_authors_email_from'] = array(
+ '#title' => t('E-mail address: From'),
+ '#type' => 'textfield',
+ '#required' => TRUE,
+ '#default_value' => !empty($config->get('hw_authors_email_from')) ? $config->get('hw_authors_email_from') : $site_mail,
+ );
+
+ $email_subject = 'CMAJ response: "!eletter_title"';
+ $form['author_notification']['author_notification_email_subject'] = array(
+ '#title' => t('Email subject'),
+ '#type' => 'textfield',
+ '#default_value' => !empty($config->get('author_notification_email_subject')) ? $config->get('author_notification_email_subject') : $email_subject,
+ );
+
+ // Email body.
+ $defaut_value = '
Dear Author,
+
An eLetter was submitted to !journal_name for your article. It is our intention to post this letter on
+ the web site and we would like to invite your feedback on the eLetter.
+
Your article (citation):
+
!article_citation
+
!article_link
+
The eLetter was submitted on !submitted_date:
+
!eletter_citation
+
+
If you would like to submit a response to this eLetter, please click on the "Submit a Response to This Article" link in the eLetter tab of your article.
+ Please let us know if you have any other questions or reactions to this process.
NOTE: We only request your email address so that the person you are recommending the page to knows that you wanted them to see it, and that it is not junk mail. We do not capture any email address.
\ No newline at end of file
diff --git a/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/form/form--hwjma-search-browse.html.twig b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/form/form--hwjma-search-browse.html.twig
new file mode 100644
index 000000000..76ad933d0
--- /dev/null
+++ b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/form/form--hwjma-search-browse.html.twig
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/form/form--hwjma-search-facets-daterange-form--2.html.twig b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/form/form--hwjma-search-facets-daterange-form--2.html.twig
new file mode 100644
index 000000000..75503e25c
--- /dev/null
+++ b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/form/form--hwjma-search-facets-daterange-form--2.html.twig
@@ -0,0 +1,18 @@
+{#
+/**
+ * @file
+ * Theme override for a 'form' element.
+ *
+ * Available variables
+ * - attributes: A list of HTML attributes for the wrapper element.
+ * - children: The child elements of the form.
+ *
+ * @see template_preprocess_form()
+ */
+#}
+
+
+ {{ node.field_please_describe_the_compet.value | raw }}
+
+
+
References
+
+ {% for key,item in node.field_references %}
+
{{ item.value }}
+ {% endfor %}
+
+
+
+
+
diff --git a/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal--browse-list-item.html.twig b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal--browse-list-item.html.twig
new file mode 100644
index 000000000..7f8a44c7b
--- /dev/null
+++ b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal--browse-list-item.html.twig
@@ -0,0 +1,48 @@
+{% include "@components/card/_card-lr-column.twig" with {
+ card_lr_column:[{
+ left_col_width: 'article__card-image',
+ middle_col_width: 'article__card-content',
+ leftimage:{
+ title: cover_image_title,
+ alt: cover_image_alt,
+ href: url,
+ src: cover_image_uri
+ },
+ card_heading_link: [{
+ anchorHead: {
+ level: 5,
+ class: "article__heading",
+ button: {
+ href: url,
+ text: html_entity_decoded_title
+ }
+ }
+ }],
+ card_description:[{
+ paragraph:{
+ class: 'article__authorname',
+ label:'Edited by: ',
+ text: journal_editors
+ }
+ }],
+ card_type_item:[{
+ label:'ISSN',
+ value:journal.eissn,
+ card_status:'(online)'
+ }],
+ card_link:[{
+ href: '#',
+ class:'article__btn show-click-toggle',
+ text:'Show description'
+ }, {
+ href: current_issue_link['#url'],
+ class:'article__btn-calender-icon',
+ text:'Current issue'
+ }, {
+ href: pap_tabs_link,
+ class:'article__btn-early',
+ text:'Early Release'
+ }],
+ card_toggle: journal_description
+ }]
+} only %}
\ No newline at end of file
diff --git a/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal--content-details.html.twig b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal--content-details.html.twig
new file mode 100644
index 000000000..241f2d6d7
--- /dev/null
+++ b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal--content-details.html.twig
@@ -0,0 +1,188 @@
+{#
+/**
+ * @file
+ * Theme override to display a node.
+ *
+ * Available variables:
+ * - node: The node entity with limited access to object properties and methods.
+ Only "getter" methods (method names starting with "get", "has", or "is")
+ and a few common methods such as "id" and "label" are available. Calling
+ other methods (such as node.delete) will result in an exception.
+ * - label: The title of the node.
+ * - content: All node items. Use {{ content }} to print them all,
+ * or print a subset such as {{ content.field_example }}. Use
+ * {{ content|without('field_example') }} to temporarily suppress the printing
+ * of a given child element.
+ * - author_picture: The node author user entity, rendered using the "compact"
+ * view mode.
+ * - metadata: Metadata for this node.
+ * - date: Themed creation date field.
+ * - author_name: Themed author name field.
+ * - url: Direct URL of the current node.
+ * - display_submitted: Whether submission information should be displayed.
+ * - attributes: HTML attributes for the containing element.
+ * The attributes.class element may contain one or more of the following
+ * classes:
+ * - node: The current template type (also known as a "theming hook").
+ * - node--type-[type]: The current node type. For example, if the node is an
+ * "Article" it would result in "node--type-article". Note that the machine
+ * name will often be in a short form of the human readable label.
+ * - node--view-mode-[view_mode]: The View Mode of the node; for example, a
+ * teaser would result in: "node--view-mode-teaser", and
+ * full: "node--view-mode-full".
+ * The following are controlled through the node publishing options.
+ * - node--promoted: Appears on nodes promoted to the front page.
+ * - node--sticky: Appears on nodes ordered above other non-sticky nodes in
+ * teaser listings.
+ * - node--unpublished: Appears on unpublished nodes visible only to site
+ * admins.
+ * - title_attributes: Same as attributes, except applied to the main title
+ * tag that appears in the template.
+ * - content_attributes: Same as attributes, except applied to the main
+ * content tag that appears in the template.
+ * - author_attributes: Same as attributes, except applied to the author of
+ * the node tag that appears in the template.
+ * - title_prefix: Additional output populated by modules, intended to be
+ * displayed in front of the main title tag that appears in the template.
+ * - title_suffix: Additional output populated by modules, intended to be
+ * displayed after the main title tag that appears in the template.
+ * - view_mode: View mode; for example, "teaser" or "full".
+ * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'.
+ * - page: Flag for the full page state. Will be true if view_mode is 'full'.
+ * - readmore: Flag for more state. Will be true if the teaser content of the
+ * node cannot hold the main body content.
+ * - logged_in: Flag for authenticated user status. Will be true when the
+ * current user is a logged-in member.
+ * - is_admin: Flag for admin user status. Will be true when the current user
+ * is an administrator.
+ *
+ * @ingroup templates
+ *
+ * @see template_preprocess_node()
+ *
+ * @todo Remove the id attribute (or make it a class), because if that gets
+ * rendered twice on a page this is invalid CSS for example: two lists
+ * in different view modes.
+ */
+#}
+
+
+
+
+ {% if journal_coverimage %}
+
+
+ {% include "@atoms/images/_image.twig" with {
+ image: {
+ src: journal_coverimage,
+ alt: 'Card Image',
+ title: 'Card Image'
+ }
+ } only %}
+
+
\ No newline at end of file
diff --git a/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal--full.html.twig b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal--full.html.twig
new file mode 100644
index 000000000..c5718ebc2
--- /dev/null
+++ b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal--full.html.twig
@@ -0,0 +1,71 @@
+{#
+/**
+ * @file
+ * Theme override to display a node.
+ *
+ * Available variables:
+ * - node: The node entity with limited access to object properties and methods.
+ Only "getter" methods (method names starting with "get", "has", or "is")
+ and a few common methods such as "id" and "label" are available. Calling
+ other methods (such as node.delete) will result in an exception.
+ * - label: The title of the node.
+ * - content: All node items. Use {{ content }} to print them all,
+ * or print a subset such as {{ content.field_example }}. Use
+ * {{ content|without('field_example') }} to temporarily suppress the printing
+ * of a given child element.
+ * - author_picture: The node author user entity, rendered using the "compact"
+ * view mode.
+ * - metadata: Metadata for this node.
+ * - date: Themed creation date field.
+ * - author_name: Themed author name field.
+ * - url: Direct URL of the current node.
+ * - display_submitted: Whether submission information should be displayed.
+ * - attributes: HTML attributes for the containing element.
+ * The attributes.class element may contain one or more of the following
+ * classes:
+ * - node: The current template type (also known as a "theming hook").
+ * - node--type-[type]: The current node type. For example, if the node is an
+ * "Article" it would result in "node--type-article". Note that the machine
+ * name will often be in a short form of the human readable label.
+ * - node--view-mode-[view_mode]: The View Mode of the node; for example, a
+ * teaser would result in: "node--view-mode-teaser", and
+ * full: "node--view-mode-full".
+ * The following are controlled through the node publishing options.
+ * - node--promoted: Appears on nodes promoted to the front page.
+ * - node--sticky: Appears on nodes ordered above other non-sticky nodes in
+ * teaser listings.
+ * - node--unpublished: Appears on unpublished nodes visible only to site
+ * admins.
+ * - title_attributes: Same as attributes, except applied to the main title
+ * tag that appears in the template.
+ * - content_attributes: Same as attributes, except applied to the main
+ * content tag that appears in the template.
+ * - author_attributes: Same as attributes, except applied to the author of
+ * the node tag that appears in the template.
+ * - title_prefix: Additional output populated by modules, intended to be
+ * displayed in front of the main title tag that appears in the template.
+ * - title_suffix: Additional output populated by modules, intended to be
+ * displayed after the main title tag that appears in the template.
+ * - view_mode: View mode; for example, "teaser" or "full".
+ * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'.
+ * - page: Flag for the full page state. Will be true if view_mode is 'full'.
+ * - readmore: Flag for more state. Will be true if the teaser content of the
+ * node cannot hold the main body content.
+ * - logged_in: Flag for authenticated user status. Will be true when the
+ * current user is a logged-in member.
+ * - is_admin: Flag for admin user status. Will be true when the current user
+ * is an administrator.
+ *
+ * @ingroup templates
+ *
+ * @see template_preprocess_node()
+ *
+ * @todo Remove the id attribute (or make it a class), because if that gets
+ * rendered twice on a page this is invalid CSS for example: two lists
+ * in different view modes.
+ */
+ #}
+
+
+ {{ content }}
+
\ No newline at end of file
diff --git a/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--collection-detail-short-desc.html.twig b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--collection-detail-short-desc.html.twig
new file mode 100644
index 000000000..799950e61
--- /dev/null
+++ b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--collection-detail-short-desc.html.twig
@@ -0,0 +1,78 @@
+{#
+/**
+ * @file
+ * Theme override to display a node.
+ *
+ * Available variables:
+ * - node: The node entity with limited access to object properties and methods.
+ Only "getter" methods (method names starting with "get", "has", or "is")
+ and a few common methods such as "id" and "label" are available. Calling
+ other methods (such as node.delete) will result in an exception.
+ * - label: The title of the node.
+ * - content: All node items. Use {{ content }} to print them all,
+ * or print a subset such as {{ content.field_example }}. Use
+ * {{ content|without('field_example') }} to temporarily suppress the printing
+ * of a given child element.
+ * - author_picture: The node author user entity, rendered using the "compact"
+ * view mode.
+ * - metadata: Metadata for this node.
+ * - date: Themed creation date field.
+ * - author_name: Themed author name field.
+ * - url: Direct URL of the current node.
+ * - display_submitted: Whether submission information should be displayed.
+ * - attributes: HTML attributes for the containing element.
+ * The attributes.class element may contain one or more of the following
+ * classes:
+ * - node: The current template type (also known as a "theming hook").
+ * - node--type-[type]: The current node type. For example, if the node is an
+ * "Article" it would result in "node--type-article". Note that the machine
+ * name will often be in a short form of the human readable label.
+ * - node--view-mode-[view_mode]: The View Mode of the node; for example, a
+ * teaser would result in: "node--view-mode-teaser", and
+ * full: "node--view-mode-full".
+ * The following are controlled through the node publishing options.
+ * - node--promoted: Appears on nodes promoted to the front page.
+ * - node--sticky: Appears on nodes ordered above other non-sticky nodes in
+ * teaser listings.
+ * - node--unpublished: Appears on unpublished nodes visible only to site
+ * admins.
+ * - title_attributes: Same as attributes, except applied to the main title
+ * tag that appears in the template.
+ * - content_attributes: Same as attributes, except applied to the main
+ * content tag that appears in the template.
+ * - author_attributes: Same as attributes, except applied to the author of
+ * the node tag that appears in the template.
+ * - title_prefix: Additional output populated by modules, intended to be
+ * displayed in front of the main title tag that appears in the template.
+ * - title_suffix: Additional output populated by modules, intended to be
+ * displayed after the main title tag that appears in the template.
+ * - view_mode: View mode; for example, "teaser" or "full".
+ * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'.
+ * - page: Flag for the full page state. Will be true if view_mode is 'full'.
+ * - readmore: Flag for more state. Will be true if the teaser content of the
+ * node cannot hold the main body content.
+ * - logged_in: Flag for authenticated user status. Will be true when the
+ * current user is a logged-in member.
+ * - is_admin: Flag for admin user status. Will be true when the current user
+ * is an administrator.
+ *
+ * @ingroup templates
+ *
+ * @see template_preprocess_node()
+ *
+ * @todo Remove the id attribute (or make it a class), because if that gets
+ * rendered twice on a page this is invalid CSS for example: two lists
+ * in different view modes.
+ */
+#}
+{%
+ set classes = [
+ node.bundle|clean_class,
+ node.isPromoted() ? 'is-promoted',
+ node.isSticky() ? 'is-sticky',
+ not node.isPublished() ? 'is-unpublished',
+ view_mode ? view_mode|clean_class,
+ 'clearfix',
+ ]
+%}
+{{ content|render|striptags }}
\ No newline at end of file
diff --git a/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--content-details.html.twig b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--content-details.html.twig
new file mode 100644
index 000000000..287a64fe5
--- /dev/null
+++ b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--content-details.html.twig
@@ -0,0 +1,94 @@
+{{journal_title}}
+
+ {% if periodical_items.cover_img %}
+
+ {% include "@atoms/images/_image.twig" with {
+ image: {
+ src: periodical_items.cover_img,
+ alt: 'cover of ' ~ periodical_items.title,
+ title: 'cover of ' ~ periodical_items.title
+ }
+ } only %}
+
+ {% endif %}
+ {% include "@atoms/text/_heading.twig" with{
+ heading :{
+ class:'article__image_title',
+ text: "This article appears in:",
+ level:6
+ }
+ } only %}
+
+ {% include "@atoms/button/_anchor-link.twig" with{
+ href: periodical_items.periodical_url,
+ text: periodical_items.title
+ } only %}
+
+
+ {% include "@atoms/button/_anchor-link.twig" with{
+ href: periodical_items.article_url,
+ text: periodical_items.vol_issue
+ } only %}
+
+
+ {% endif %}
+
+
+
+
diff --git a/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--focus-view.html.twig b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--focus-view.html.twig
new file mode 100644
index 000000000..7b2049849
--- /dev/null
+++ b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--focus-view.html.twig
@@ -0,0 +1,83 @@
+{#
+/**
+ * @file
+ * Theme override to display a node.
+ *
+ * Available variables:
+ * - node: The node entity with limited access to object properties and methods.
+ Only "getter" methods (method names starting with "get", "has", or "is")
+ and a few common methods such as "id" and "label" are available. Calling
+ other methods (such as node.delete) will result in an exception.
+ * - label: The title of the node.
+ * - content: All node items. Use {{ content }} to print them all,
+ * or print a subset such as {{ content.field_example }}. Use
+ * {{ content|without('field_example') }} to temporarily suppress the printing
+ * of a given child element.
+ * - author_picture: The node author user entity, rendered using the "compact"
+ * view mode.
+ * - metadata: Metadata for this node.
+ * - date: Themed creation date field.
+ * - author_name: Themed author name field.
+ * - url: Direct URL of the current node.
+ * - display_submitted: Whether submission information should be displayed.
+ * - attributes: HTML attributes for the containing element.
+ * The attributes.class element may contain one or more of the following
+ * classes:
+ * - node: The current template type (also known as a "theming hook").
+ * - node--type-[type]: The current node type. For example, if the node is an
+ * "Article" it would result in "node--type-article". Note that the machine
+ * name will often be in a short form of the human readable label.
+ * - node--view-mode-[view_mode]: The View Mode of the node; for example, a
+ * teaser would result in: "node--view-mode-teaser", and
+ * full: "node--view-mode-full".
+ * The following are controlled through the node publishing options.
+ * - node--promoted: Appears on nodes promoted to the front page.
+ * - node--sticky: Appears on nodes ordered above other non-sticky nodes in
+ * teaser listings.
+ * - node--unpublished: Appears on unpublished nodes visible only to site
+ * admins.
+ * - title_attributes: Same as attributes, except applied to the main title
+ * tag that appears in the template.
+ * - content_attributes: Same as attributes, except applied to the main
+ * content tag that appears in the template.
+ * - author_attributes: Same as attributes, except applied to the author of
+ * the node tag that appears in the template.
+ * - title_prefix: Additional output populated by modules, intended to be
+ * displayed in front of the main title tag that appears in the template.
+ * - title_suffix: Additional output populated by modules, intended to be
+ * displayed after the main title tag that appears in the template.
+ * - view_mode: View mode; for example, "teaser" or "full".
+ * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'.
+ * - page: Flag for the full page state. Will be true if view_mode is 'full'.
+ * - readmore: Flag for more state. Will be true if the teaser content of the
+ * node cannot hold the main body content.
+ * - logged_in: Flag for authenticated user status. Will be true when the
+ * current user is a logged-in member.
+ * - is_admin: Flag for admin user status. Will be true when the current user
+ * is an administrator.
+ *
+ * @ingroup templates
+ *
+ * @see template_preprocess_node()
+ *
+ * @todo Remove the id attribute (or make it a class), because if that gets
+ * rendered twice on a page this is invalid CSS for example: two lists
+ * in different view modes.
+ */
+#}
+{%
+ set classes = [
+ node.isPromoted() ? 'is-promoted',
+ node.isSticky() ? 'is-sticky',
+ not node.isPublished() ? 'is-unpublished',
+ view_mode ? view_mode|clean_class,
+ 'clearfix',
+ ]
+%}
+
+ {% block node_content %}
+
+ {{ content }}
+
+ {% endblock %}
+
\ No newline at end of file
diff --git a/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--full.html.twig b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--full.html.twig
new file mode 100644
index 000000000..0a8eac4c0
--- /dev/null
+++ b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--full.html.twig
@@ -0,0 +1,85 @@
+{#
+/**
+ * @file
+ * Theme override to display a node.
+ *
+ * Available variables:
+ * - node: The node entity with limited access to object properties and methods.
+ Only "getter" methods (method names starting with "get", "has", or "is")
+ and a few common methods such as "id" and "label" are available. Calling
+ other methods (such as node.delete) will result in an exception.
+ * - label: The title of the node.
+ * - content: All node items. Use {{ content }} to print them all,
+ * or print a subset such as {{ content.field_example }}. Use
+ * {{ content|without('field_example') }} to temporarily suppress the printing
+ * of a given child element.
+ * - author_picture: The node author user entity, rendered using the "compact"
+ * view mode.
+ * - metadata: Metadata for this node.
+ * - date: Themed creation date field.
+ * - author_name: Themed author name field.
+ * - url: Direct URL of the current node.
+ * - display_submitted: Whether submission information should be displayed.
+ * - attributes: HTML attributes for the containing element.
+ * The attributes.class element may contain one or more of the following
+ * classes:
+ * - node: The current template type (also known as a "theming hook").
+ * - node--type-[type]: The current node type. For example, if the node is an
+ * "Article" it would result in "node--type-article". Note that the machine
+ * name will often be in a short form of the human readable label.
+ * - node--view-mode-[view_mode]: The View Mode of the node; for example, a
+ * teaser would result in: "node--view-mode-teaser", and
+ * full: "node--view-mode-full".
+ * The following are controlled through the node publishing options.
+ * - node--promoted: Appears on nodes promoted to the front page.
+ * - node--sticky: Appears on nodes ordered above other non-sticky nodes in
+ * teaser listings.
+ * - node--unpublished: Appears on unpublished nodes visible only to site
+ * admins.
+ * - title_attributes: Same as attributes, except applied to the main title
+ * tag that appears in the template.
+ * - content_attributes: Same as attributes, except applied to the main
+ * content tag that appears in the template.
+ * - author_attributes: Same as attributes, except applied to the author of
+ * the node tag that appears in the template.
+ * - title_prefix: Additional output populated by modules, intended to be
+ * displayed in front of the main title tag that appears in the template.
+ * - title_suffix: Additional output populated by modules, intended to be
+ * displayed after the main title tag that appears in the template.
+ * - view_mode: View mode; for example, "teaser" or "full".
+ * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'.
+ * - page: Flag for the full page state. Will be true if view_mode is 'full'.
+ * - readmore: Flag for more state. Will be true if the teaser content of the
+ * node cannot hold the main body content.
+ * - logged_in: Flag for authenticated user status. Will be true when the
+ * current user is a logged-in member.
+ * - is_admin: Flag for admin user status. Will be true when the current user
+ * is an administrator.
+ *
+ * @ingroup templates
+ *
+ * @see template_preprocess_node()
+ *
+ * @todo Remove the id attribute (or make it a class), because if that gets
+ * rendered twice on a page this is invalid CSS for example: two lists
+ * in different view modes.
+ */
+#}
+{%
+ set classes = [
+ node.isPromoted() ? 'is-promoted',
+ node.isSticky() ? 'is-sticky',
+ not node.isPublished() ? 'is-unpublished',
+ view_mode ? view_mode|clean_class,
+ 'clearfix',
+ ]
+%}
+
+
+ {% block node_content %}
+
+ {{ content }}
+
+ {% endblock %}
+
+
diff --git a/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--toc-list.html.twig b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--toc-list.html.twig
new file mode 100644
index 000000000..8a2113b22
--- /dev/null
+++ b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-article--toc-list.html.twig
@@ -0,0 +1,99 @@
+{#
+/**
+* @file
+* Theme override to display a node.
+*
+* Available variables:
+* - node: The node entity with limited access to object properties and methods. Only "getter" methods (method names starting with "get", "has", or "is") and a few common methods such as "id" and "label" are available. Calling other methods (such as node.delete) will result in an exception.
+* - label: The title of the node.
+* - content: All node items. Use {{ content }} to print them all,
+* or print a subset such as {{ content.field_example }}. Use
+* {{ content|without('field_example') }} to temporarily suppress the printing
+* of a given child element.
+* - author_picture: The node author user entity, rendered using the "compact"
+* view mode.
+* - metadata: Metadata for this node.
+* - date: Themed creation date field.
+* - author_name: Themed author name field.
+* - url: Direct URL of the current node.
+* - display_submitted: Whether submission information should be displayed.
+* - attributes: HTML attributes for the containing element.
+* The attributes.class element may contain one or more of the following
+* classes:
+* - node: The current template type (also known as a "theming hook").
+* - node--type-[type]: The current node type. For example, if the node is an
+* "Article" it would result in "node--type-article". Note that the machine
+* name will often be in a short form of the human readable label.
+* - node--view-mode-[view_mode]: The View Mode of the node; for example, a
+* teaser would result in: "node--view-mode-teaser", and
+* full: "node--view-mode-full".
+* The following are controlled through the node publishing options.
+* - node--promoted: Appears on nodes promoted to the front page.
+* - node--sticky: Appears on nodes ordered above other non-sticky nodes in
+* teaser listings.
+* - node--unpublished: Appears on unpublished nodes visible only to site
+* admins.
+* - title_attributes: Same as attributes, except applied to the main title
+* tag that appears in the template.
+* - content_attributes: Same as attributes, except applied to the main
+* content tag that appears in the template.
+* - author_attributes: Same as attributes, except applied to the author of
+* the node tag that appears in the template.
+* - title_prefix: Additional output populated by modules, intended to be
+* displayed in front of the main title tag that appears in the template.
+* - title_suffix: Additional output populated by modules, intended to be
+* displayed after the main title tag that appears in the template.
+* - view_mode: View mode; for example, "teaser" or "full".
+* - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'.
+* - page: Flag for the full page state. Will be true if view_mode is 'full'.
+* - readmore: Flag for more state. Will be true if the teaser content of the
+* node cannot hold the main body content.
+* - logged_in: Flag for authenticated user status. Will be true when the
+* current user is a logged-in member.
+* - is_admin: Flag for admin user status. Will be true when the current user
+* is an administrator.
+*
+* @ingroup templates
+*
+* @see template_preprocess_node()
+*
+* @todo Remove the id attribute (or make it a class), because if that gets
+* rendered twice on a page this is invalid CSS for example: two lists
+* in different view modes.
+*/
+#}
+{%
+ set classes = [
+ node.bundle|clean_class,
+ node.isPromoted() ? 'is-promoted',
+ node.isSticky() ? 'is-sticky',
+ not node.isPublished() ? 'is-unpublished',
+ view_mode ? view_mode|clean_class,
+ 'clearfix',
+ 'article-section'
+ ]
+%}
+
+ {% include "@components/card/_card-lr-column.twig" with {
+ card_lr_column: {
+ item:{
+ card_heading_link: {
+ item: {
+ anchorHead: {
+ level: 5,
+ class: 'article-section__heading',
+ button: {
+ text: label,
+ href: url,
+ }
+ },
+ }
+ },
+ card_description: content.author_names,
+ card_link: content.action_links,
+ card_download_pdf: content.variant_full_text_pdf,
+ card_toggle: content.article_abstract
+ }
+ }
+ } only %}
+
\ No newline at end of file
diff --git a/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-issue--content-details.html.twig b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-issue--content-details.html.twig
new file mode 100644
index 000000000..76d129cff
--- /dev/null
+++ b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--journal-issue--content-details.html.twig
@@ -0,0 +1,141 @@
+{#
+ /**
+ * @file
+ * Theme override to display a node.
+ *
+ * Available variables:
+ * - node: The node entity with limited access to object properties and methods.
+ Only "getter" methods (method names starting with "get", "has", or "is")
+ and a few common methods such as "id" and "label" are available. Calling
+ other methods (such as node.delete) will result in an exception.
+ * - label: The title of the node.
+ * - content: All node items. Use {{ content }} to print them all,
+ * or print a subset such as {{ content.field_example }}. Use
+ * {{ content|without('field_example') }} to temporarily suppress the printing
+ * of a given child element.
+ * - author_picture: The node author user entity, rendered using the "compact"
+ * view mode.
+ * - metadata: Metadata for this node.
+ * - date: Themed creation date field.
+ * - author_name: Themed author name field.
+ * - url: Direct URL of the current node.
+ * - display_submitted: Whether submission information should be displayed.
+ * - attributes: HTML attributes for the containing element.
+ * The attributes.class element may contain one or more of the following
+ * classes:
+ * - node: The current template type (also known as a "theming hook").
+ * - node--type-[type]: The current node type. For example, if the node is an
+ * "Article" it would result in "node--type-article". Note that the machine
+ * name will often be in a short form of the human readable label.
+ * - node--view-mode-[view_mode]: The View Mode of the node; for example, a
+ * teaser would result in: "node--view-mode-teaser", and
+ * full: "node--view-mode-full".
+ * The following are controlled through the node publishing options.
+ * - node--promoted: Appears on nodes promoted to the front page.
+ * - node--sticky: Appears on nodes ordered above other non-sticky nodes in
+ * teaser listings.
+ * - node--unpublished: Appears on unpublished nodes visible only to site
+ * admins.
+ * - title_attributes: Same as attributes, except applied to the main title
+ * tag that appears in the template.
+ * - content_attributes: Same as attributes, except applied to the main
+ * content tag that appears in the template.
+ * - author_attributes: Same as attributes, except applied to the author of
+ * the node tag that appears in the template.
+ * - title_prefix: Additional output populated by modules, intended to be
+ * displayed in front of the main title tag that appears in the template.
+ * - title_suffix: Additional output populated by modules, intended to be
+ * displayed after the main title tag that appears in the template.
+ * - view_mode: View mode; for example, "teaser" or "full".
+ * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'.
+ * - page: Flag for the full page state. Will be true if view_mode is 'full'.
+ * - readmore: Flag for more state. Will be true if the teaser content of the
+ * node cannot hold the main body content.
+ * - logged_in: Flag for authenticated user status. Will be true when the
+ * current user is a logged-in member.
+ * - is_admin: Flag for admin user status. Will be true when the current user
+ * is an administrator.
+ *
+ * @ingroup templates
+ *
+ * @see template_preprocess_node()
+ *
+ * @todo Remove the id attribute (or make it a class), because if that gets
+ * rendered twice on a page this is invalid CSS for example: two lists
+ * in different view modes.
+ */
+#}
+
+
+
+
+ {% if Journalissue %}
+
+
+ {% include "@atoms/images/_image.twig" with {
+ image: {
+ href: Journalissue.periodical_url,
+ src: Journalissue.variant_coverimage01,
+ alt: Journalissue.variant_coverimage01_alt,
+ title:Journalissue.variant_coverimage01_alt
+ }
+ } only %}
+
+
\ No newline at end of file
+{{ cms_nav }}
+{{ content }}
\ No newline at end of file
diff --git a/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--search-result.html.twig b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--search-result.html.twig
new file mode 100644
index 000000000..2f0857ef7
--- /dev/null
+++ b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--search-result.html.twig
@@ -0,0 +1,112 @@
+{#
+/**
+ * @file
+ * Theme override to display a node.
+ *
+ * Available variables:
+ * - node: The node entity with limited access to object properties and methods.
+ Only "getter" methods (method names starting with "get", "has", or "is")
+ and a few common methods such as "id" and "label" are available. Calling
+ other methods (such as node.delete) will result in an exception.
+ * - label: The title of the node.
+ * - content: All node items. Use {{ content }} to print them all,
+ * or print a subset such as {{ content.field_example }}. Use
+ * {{ content|without('field_example') }} to temporarily suppress the printing
+ * of a given child element.
+ * - author_picture: The node author user entity, rendered using the "compact"
+ * view mode.
+ * - metadata: Metadata for this node.
+ * - date: Themed creation date field.
+ * - author_name: Themed author name field.
+ * - url: Direct URL of the current node.
+ * - display_submitted: Whether submission information should be displayed.
+ * - attributes: HTML attributes for the containing element.
+ * The attributes.class element may contain one or more of the following
+ * classes:
+ * - node: The current template type (also known as a "theming hook").
+ * - node--type-[type]: The current node type. For example, if the node is an
+ * "Article" it would result in "node--type-article". Note that the machine
+ * name will often be in a short form of the human readable label.
+ * - node--view-mode-[view_mode]: The View Mode of the node; for example, a
+ * teaser would result in: "node--view-mode-teaser", and
+ * full: "node--view-mode-full".
+ * The following are controlled through the node publishing options.
+ * - node--promoted: Appears on nodes promoted to the front page.
+ * - node--sticky: Appears on nodes ordered above other non-sticky nodes in
+ * teaser listings.
+ * - node--unpublished: Appears on unpublished nodes visible only to site
+ * admins.
+ * - title_attributes: Same as attributes, except applied to the main title
+ * tag that appears in the template.
+ * - content_attributes: Same as attributes, except applied to the main
+ * content tag that appears in the template.
+ * - author_attributes: Same as attributes, except applied to the author of
+ * the node tag that appears in the template.
+ * - title_prefix: Additional output populated by modules, intended to be
+ * displayed in front of the main title tag that appears in the template.
+ * - title_suffix: Additional output populated by modules, intended to be
+ * displayed after the main title tag that appears in the template.
+ * - view_mode: View mode; for example, "teaser" or "full".
+ * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'.
+ * - page: Flag for the full page state. Will be true if view_mode is 'full'.
+ * - readmore: Flag for more state. Will be true if the teaser content of the
+ * node cannot hold the main body content.
+ * - logged_in: Flag for authenticated user status. Will be true when the
+ * current user is a logged-in member.
+ * - is_admin: Flag for admin user status. Will be true when the current user
+ * is an administrator.
+ *
+ * @ingroup templates
+ *
+ * @see template_preprocess_node()
+ *
+ * @todo Remove the id attribute (or make it a class), because if that gets
+ * rendered twice on a page this is invalid CSS for example: two lists
+ * in different view modes.
+ */
+#}
+{% include "@components/site/_search-result-middle.twig" with {
+ favorite_loop: [{
+ image: {
+ src: result.img,
+ href: result.url,
+ title: result.title
+ },
+ fav_action: {
+ date: marker_link_date,
+ href: marker_link,
+ text: "Remove from favourites",
+ popupdata: marker_popupdata,
+ },
+ card_heading: [{
+ heading: {
+ text: result.type,
+ level:6
+ }
+ }],
+ card_heading_link: [{
+ anchorHead: {
+ level:4 ,
+ button: {
+ href: result.url,
+ title: result.title,
+ text: result.title
+ }
+ }
+ }],
+ card_description: [{
+ paragraph: {
+ class: 'search-result__description',
+ text: result.short_description
+ }
+ }],
+ card_link: result.img,
+ source: result.source,
+ sourceLink: {
+ href: result.source_url,
+ title: result.source_title,
+ text: result.source_title
+ },
+ access_icon_placeholder: access_icon_placeholder
+ }]
+} only %}
diff --git a/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--search-short-description.html.twig b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--search-short-description.html.twig
new file mode 100644
index 000000000..20bb19ca8
--- /dev/null
+++ b/web/themes/highwire/hwjma_theme/apps/drupal-default/hwjma_theme/templates/node/node--search-short-description.html.twig
@@ -0,0 +1,82 @@
+{#
+/**
+ * @file
+ * Theme override to display a node.
+ *
+ * Available variables:
+ * - node: The node entity with limited access to object properties and methods.
+ Only "getter" methods (method names starting with "get", "has", or "is")
+ and a few common methods such as "id" and "label" are available. Calling
+ other methods (such as node.delete) will result in an exception.
+ * - label: The title of the node.
+ * - content: All node items. Use {{ content }} to print them all,
+ * or print a subset such as {{ content.field_example }}. Use
+ * {{ content|without('field_example') }} to temporarily suppress the printing
+ * of a given child element.
+ * - author_picture: The node author user entity, rendered using the "compact"
+ * view mode.
+ * - metadata: Metadata for this node.
+ * - date: Themed creation date field.
+ * - author_name: Themed author name field.
+ * - url: Direct URL of the current node.
+ * - display_submitted: Whether submission information should be displayed.
+ * - attributes: HTML attributes for the containing element.
+ * The attributes.class element may contain one or more of the following
+ * classes:
+ * - node: The current template type (also known as a "theming hook").
+ * - node--type-[type]: The current node type. For example, if the node is an
+ * "Article" it would result in "node--type-article". Note that the machine
+ * name will often be in a short form of the human readable label.
+ * - node--view-mode-[view_mode]: The View Mode of the node; for example, a
+ * teaser would result in: "node--view-mode-teaser", and
+ * full: "node--view-mode-full".
+ * The following are controlled through the node publishing options.
+ * - node--promoted: Appears on nodes promoted to the front page.
+ * - node--sticky: Appears on nodes ordered above other non-sticky nodes in
+ * teaser listings.
+ * - node--unpublished: Appears on unpublished nodes visible only to site
+ * admins.
+ * - title_attributes: Same as attributes, except applied to the main title
+ * tag that appears in the template.
+ * - content_attributes: Same as attributes, except applied to the main
+ * content tag that appears in the template.
+ * - author_attributes: Same as attributes, except applied to the author of
+ * the node tag that appears in the template.
+ * - title_prefix: Additional output populated by modules, intended to be
+ * displayed in front of the main title tag that appears in the template.
+ * - title_suffix: Additional output populated by modules, intended to be
+ * displayed after the main title tag that appears in the template.
+ * - view_mode: View mode; for example, "teaser" or "full".
+ * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'.
+ * - page: Flag for the full page state. Will be true if view_mode is 'full'.
+ * - readmore: Flag for more state. Will be true if the teaser content of the
+ * node cannot hold the main body content.
+ * - logged_in: Flag for authenticated user status. Will be true when the
+ * current user is a logged-in member.
+ * - is_admin: Flag for admin user status. Will be true when the current user
+ * is an administrator.
+ *
+ * @ingroup templates
+ *
+ * @see template_preprocess_node()
+ *
+ * @todo Remove the id attribute (or make it a class), because if that gets
+ * rendered twice on a page this is invalid CSS for example: two lists
+ * in different view modes.
+ */
+#}
+{%
+ set classes = [
+ node.bundle|clean_class,
+ node.isPromoted() ? 'is-promoted',
+ node.isSticky() ? 'is-sticky',
+ not node.isPublished() ? 'is-unpublished',
+ view_mode ? view_mode|clean_class,
+ 'clearfix',
+ ]
+%}
+{% block node_content %}
+
Submit a Response to This Article