diff --git a/bs_lib.install b/bs_lib.install index 84a5224..44480f4 100644 --- a/bs_lib.install +++ b/bs_lib.install @@ -117,7 +117,8 @@ function bs_lib_update_8001() { $config->set('anchor_scroll', [ 'enable' => FALSE, 'offset' => 10, - 'fixedElements' => ".toolbar-bar\n.toolbar-tray.is-active.toolbar-tray-horizontal", + 'exclude_elements' => "", + 'fixed_elements' => ".toolbar-bar\n.toolbar-tray.is-active.toolbar-tray-horizontal", ]); $config->save(TRUE); } diff --git a/bs_lib.module b/bs_lib.module index df864cf..8896135 100644 --- a/bs_lib.module +++ b/bs_lib.module @@ -22,7 +22,8 @@ function bs_lib_theme() { function bs_lib_page_attachments(array &$attachments) { $anchor_scroll = \Drupal::config('bs_lib.settings')->get('anchor_scroll'); if ($anchor_scroll['enable']) { - $anchor_scroll['fixedElements'] = explode("\n", $anchor_scroll['fixedElements']); + $anchor_scroll['exclude_elements'] = explode("\n", $anchor_scroll['exclude_elements']); + $anchor_scroll['fixed_elements'] = explode("\n", $anchor_scroll['fixed_elements']); $attachments['#attached']['drupalSettings']['bs_lib']['anchor_scroll'] = $anchor_scroll; $attachments['#attached']['library'][] = 'bs_lib/anchor_scroll'; } diff --git a/bs_lib.routing.yml b/bs_lib.routing.yml new file mode 100644 index 0000000..3e5901d --- /dev/null +++ b/bs_lib.routing.yml @@ -0,0 +1,7 @@ +bs_lib.settings_form: + path: '/admin/config/system/bs-lib' + defaults: + _title: 'BS Lib Settings' + _form: 'Drupal\bs_lib\Form\SettingsForm' + requirements: + _permission: 'access administration pages' diff --git a/config/install/bs_lib.settings.yml b/config/install/bs_lib.settings.yml index 30e9cb9..f40c9fb 100644 --- a/config/install/bs_lib.settings.yml +++ b/config/install/bs_lib.settings.yml @@ -1,4 +1,5 @@ anchor_scroll: enable: false offset: 10 - fixedElements: ".toolbar-bar\n.toolbar-tray.is-active.toolbar-tray-horizontal" + exclude_elements: "" + fixed_elements: ".toolbar-bar\n.toolbar-tray.is-active.toolbar-tray-horizontal" diff --git a/config/schema/bs_lib.schema.yml b/config/schema/bs_lib.schema.yml index 2374571..d06ebbe 100644 --- a/config/schema/bs_lib.schema.yml +++ b/config/schema/bs_lib.schema.yml @@ -48,6 +48,9 @@ bs_lib.settings: offset: type: integer label: 'Additional offset from top of the viewport' - fixedElements: + exclude_elements: + type: string + label: 'List of exclude elements selectors' + fixed_elements: type: string label: 'List of fixed elements selectors' diff --git a/js/anchor-scroll.js b/js/anchor-scroll.js index 3bf14d8..278c7d7 100644 --- a/js/anchor-scroll.js +++ b/js/anchor-scroll.js @@ -22,16 +22,26 @@ if (targetElement) { var options = drupalSettings.bs_lib.anchor_scroll; - // Used to trigger sticky header with small amount of scroll to trigger - // sticky header so calculation is more accurate. - // @fixme - this is a hack, if possible fix it or remove it for now! - //window.scrollTo({top: 50}); + // Exclude elements. Height of this ones we remove from scroll if we + // still need to scroll pass them. + var excludeHeight = 0; + for (var i = 0; i < options.exclude_elements.length; ++i) { + var excludeElements = document.querySelector(options.exclude_elements[i]); + if (!excludeElements) { + continue; + } + var excludeElementOffset = excludeElements.getBoundingClientRect().top + excludeElements.getBoundingClientRect().height; + // If it is negative that means it is already pass the viewport and we + // ignore it. + excludeHeight += excludeElementOffset > 0 ? excludeElementOffset : 0; + } - // Find fixed element with a biggest offset from viewport top and use - // that one to calculate element scroll value. + // Fixed elements. This elements we always take into consideration. + // Fixed element with a biggest offset from viewport top will be used + // in calculation of a element scroll value. var biggestOffset = 0; - for (var i = 0; i < options.fixedElements.length; ++i) { - var fixedElement = document.querySelector(options.fixedElements[i]); + for (var j = 0; j < options.fixed_elements.length; ++j) { + var fixedElement = document.querySelector(options.fixed_elements[j]); if (!fixedElement) { continue; } @@ -42,7 +52,7 @@ // Focus the target element, important for accessibility. targetElement.focus(); if (document.activeElement !== targetElement) { - // If element is not focusable set tabindex first. + // If element is not focusable set tabindex first so we can focus it. targetElement.setAttribute('tabindex','-1'); targetElement.focus(); } @@ -51,7 +61,7 @@ // This needs to happen after focusing because focus will also trigger // scrolling. window.scrollTo({ - top: window.scrollY + targetElement.getBoundingClientRect().top - (options.offset + biggestOffset), + top: window.scrollY + targetElement.getBoundingClientRect().top + excludeHeight - (options.offset + biggestOffset), behavior: 'smooth' }); diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php new file mode 100644 index 0000000..3b99703 --- /dev/null +++ b/src/Form/SettingsForm.php @@ -0,0 +1,109 @@ +config('bs_lib.settings')->get('anchor_scroll'); + + $form['anchor_scroll'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Anchor scroll'), + '#tree' => TRUE, + ]; + + $form['anchor_scroll']['enable'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Enable'), + '#description' => $this->t('Enable custom anchor scrolling functionality.'), + '#default_value' => $config['enable'], + ]; + + $form['anchor_scroll']['offset'] = [ + '#type' => 'number', + '#title' => $this->t('Offset'), + '#description' => $this->t('Enter offset value in pixels. This is vertical distance from top of a viewport to target scroll element.'), + '#default_value' => $config['offset'], + '#states' => [ + 'visible' => [ + 'input[name="anchor_scroll[enable]"]' => ['checked' => TRUE], + ], + ], + ]; + + $form['anchor_scroll']['fixed_elements'] = [ + '#type' => 'textarea', + '#title' => $this->t('Fixed elements'), + '#description' => $this->t('Enter fixed elements CSS selectors. Enter one per line. Element with highest vertical offset will be used for additional scroll offset. Use this for sticky elements like toolbar, sticky navigation etc.'), + '#default_value' => $config['fixed_elements'], + '#states' => [ + 'visible' => [ + 'input[name="anchor_scroll[enable]"]' => ['checked' => TRUE], + ], + ], + ]; + + $form['anchor_scroll']['exclude_elements'] = [ + '#type' => 'textarea', + '#title' => $this->t('Exclude elements'), + '#description' => $this->t('Enter exclude elements CSS selectors. Enter one per line. Height of this elements will be removed from scrolling. Use this if you have an element which is before sticky navigation.'), + '#default_value' => $config['exclude_elements'], + '#states' => [ + 'visible' => [ + 'input[name="anchor_scroll[enable]"]' => ['checked' => TRUE], + ], + ], + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $config = $this->config('bs_lib.settings'); + $anchor_scroll = $config->get('anchor_scroll'); + $anchor_scroll['enable'] = $form_state->getValue(['anchor_scroll', 'enable']) === 1; + $anchor_scroll['offset'] = (int) $form_state->getValue(['anchor_scroll', 'offset']); + // Normalize new line. + $anchor_scroll['exclude_elements'] = implode("\n", preg_split("(\r\n?|\n)", $form_state->getValue(['anchor_scroll', 'exclude_elements']))); + $anchor_scroll['fixed_elements'] = implode("\n", preg_split("(\r\n?|\n)", $form_state->getValue(['anchor_scroll', 'fixed_elements']))); + $this->config('bs_lib.settings') + ->set('anchor_scroll', $anchor_scroll) + ->save(); + parent::submitForm($form, $form_state); + } + +}