Skip to content

Commit

Permalink
MDL-75401 core: sticky footer component
Browse files Browse the repository at this point in the history
  • Loading branch information
ferranrecio committed Oct 19, 2022
1 parent f8d28e4 commit 3d483ac
Show file tree
Hide file tree
Showing 14 changed files with 476 additions and 4 deletions.
130 changes: 130 additions & 0 deletions lib/classes/output/sticky_footer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace core\output;

use renderable;

/**
* Class to render a sticky footer element.
*
* Sticky footer can be rendered at any moment if the page (even inside a form) but
* it will be displayed at the bottom of the page.
*
* Important: note that pages can only display one sticky footer at once.
*
* Important: not all themes are compatible with sticky footer. If the current theme
* is not compatible it will be rendered as a standard div element.
*
* @package core
* @category output
* @copyright 2022 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sticky_footer implements named_templatable, renderable {

/**
* @var string content of the sticky footer.
*/
protected $stickycontent = '';

/**
* @var string extra CSS classes. By default, elements are justified to the end.
*/
protected $stickyclasses = 'justify-content-end';

/**
* @var array extra HTML attributes (attribute => value).
*/
protected $attributes = [];

/**
* Constructor.
*
* @param string $stickycontent the footer content
* @param string|null $stickyclasses extra CSS classes
* @param array $attributes extra html attributes (attribute => value)
*/
public function __construct(string $stickycontent = '', ?string $stickyclasses = null, array $attributes = []) {
$this->stickycontent = $stickycontent;
if ($stickyclasses !== null) {
$this->stickyclasses = $stickyclasses;
}
$this->attributes = $attributes;
}

/**
* Set the footer contents.
*
* @param string $stickycontent the footer content
*/
public function set_content(string $stickycontent) {
$this->stickycontent = $stickycontent;
}

/**
* Add extra classes to the sticky footer.
*
* @param string $stickyclasses the extra classes
*/
public function add_classes(string $stickyclasses) {
if (!empty($this->stickyclasses)) {
$this->stickyclasses .= ' ';
}
$this->stickyclasses = $stickyclasses;
}

/**
* Add extra attributes to the sticky footer element.
*
* @param string $atribute the attribute
* @param string $value the value
*/
public function add_attribute(string $atribute, string $value) {
$this->attributes[$atribute] = $value;
}

/**
* Export this data so it can be used as the context for a mustache template (core/inplace_editable).
*
* @param renderer_base $output typically, the renderer that's calling this function
* @return array data context for a mustache template
*/
public function export_for_template(\renderer_base $output) {
$extras = [];
foreach ($this->attributes as $attribute => $value) {
$extras[] = [
'attribute' => $attribute,
'value' => $value,
];
}
return [
'stickycontent' => (string)$this->stickycontent,
'stickyclasses' => $this->stickyclasses,
'extras' => $extras,
];
}

/**
* Get the name of the template to use for this templatable.
*
* @param \renderer_base $renderer The renderer requesting the template name
* @return string the template name
*/
public function get_template_name(\renderer_base $renderer): string {
return 'core/sticky_footer';
}
}
47 changes: 47 additions & 0 deletions lib/templates/sticky_footer.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/sticky_footer
Displays a page sticky footer element.
Sticky footer behaviour depends on the theme. The default template is
a regular element.
Example context (json):
{
"stickycontent" : "<a href=\"#\">Moodle</a>",
"extras" : [
{
"attribute" : "data-example",
"value" : "stickyfooter"
}
],
"stickyclasses" : "extraclasses"
}
}}
<div
id="sticky-footer"
class="{{$ stickyclasses }}{{stickyclasses}}{{/ stickyclasses }}"
{{#extras}}
{{attribute}}="{{value}}"
{{/extras}}
>
{{$ stickycontent }}
{{{stickycontent}}}
{{/ stickycontent }}
</div>
10 changes: 10 additions & 0 deletions theme/boost/amd/build/sticky-footer.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions theme/boost/amd/build/sticky-footer.min.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

107 changes: 107 additions & 0 deletions theme/boost/amd/src/sticky-footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* Sticky footer module.
*
* @module theme_boost/sticky-footer
* @copyright 2022 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

import Pending from 'core/pending';

const SELECTORS = {
STICKYFOOTER: '.stickyfooter',
PAGE: '#page',
};

const CLASSES = {
HASSTICKYFOOTER: 'hasstickyfooter',
};

let initialized = false;

let previousScrollPosition = 0;

/**
* Return the current page scroll position.
* @package
* @returns {number} the current scroll position
*/
const getScrollPosition = () => {
const page = document.querySelector(SELECTORS.PAGE);
if (page) {
return page.scrollTop;
}
return window.pageYOffset;
};

/**
* Scroll handler.
* @package
*/
const scrollSpy = () => {
// Ignore scroll if page size is not small.
if (document.body.clientWidth >= 768) {
return;
}
// Detect if scroll is going down.
let scrollPosition = getScrollPosition();
if (scrollPosition > previousScrollPosition) {
disableStickyFooter();
} else {
enableStickyFooter();
}
previousScrollPosition = scrollPosition;
};

/**
* Enable sticky footer in the page.
*/
export const enableStickyFooter = () => {
// We need some seconds to make sure the CSS animation is ready.
const pendingPromise = new Pending('theme_boost/sticky-footer:enabling');
const footer = document.querySelector(SELECTORS.STICKYFOOTER);
const page = document.querySelector(SELECTORS.PAGE);
if (footer && page) {
document.body.classList.add(CLASSES.HASSTICKYFOOTER);
page.classList.add(CLASSES.HASSTICKYFOOTER);
}
setTimeout(() => pendingPromise.resolve(), 1000);
};

/**
* Disable sticky footer in the page.
*/
export const disableStickyFooter = () => {
document.body.classList.remove(CLASSES.HASSTICKYFOOTER);
const page = document.querySelector(SELECTORS.PAGE);
page?.classList.remove(CLASSES.HASSTICKYFOOTER);
};

/**
* Initialize the module.
*/
export const init = () => {
// Prevent sticky footer in behat.
if (initialized || document.body.classList.contains('behat-site')) {
return;
}
initialized = true;
enableStickyFooter();
const content = document.querySelector(SELECTORS.PAGE) ?? document.body;
content.addEventListener("scroll", scrollSpy);
};
4 changes: 4 additions & 0 deletions theme/boost/scss/moodle/core.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2320,6 +2320,10 @@ $footer-link-color: $bg-inverse-link-color !default;
@include box-shadow($popover-box-shadow);
}

.hasstickyfooter .btn-footer-popover {
bottom: calc(2rem + #{$navbar-height});
}

.popover.footer {
.popover-body {
padding: 0;
Expand Down
8 changes: 7 additions & 1 deletion theme/boost/scss/moodle/debug.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ body.behat-site {
position: absolute;
}

// Sticky footer can overlap with elements so we keep it relative for behat.
&.hasstickyfooter .stickyfooter,
.stickyfooter {
position: inherit;
z-index: inherit;
}

// We need more spacing in action menus so behat does not click on the wrong menu item.
.dropdown-item {
margin-top: 4px !important; /* stylelint-disable declaration-no-important */
Expand Down Expand Up @@ -100,4 +107,3 @@ body > .debuggingmessage {
body > .debuggingmessage ~ .debuggingmessage {
margin-top: .5rem;
}

10 changes: 10 additions & 0 deletions theme/boost/scss/moodle/layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,13 @@
top: $navbar-height;
height: calc(100vh - #{$navbar-height});
}
.hasstickyfooter {
.drawer-left,
.drawer-right {
top: $navbar-height;
height: calc(100vh - #{$navbar-height} - #{$navbar-height});
}
}

#page.drawers {
position: relative;
Expand All @@ -217,6 +224,9 @@
margin-left: $drawer-left-width;
margin-right: $drawer-right-width;
}
&.hasstickyfooter {
height: calc(100vh - #{$navbar-height} - #{$navbar-height});
}
}
}

Expand Down
24 changes: 24 additions & 0 deletions theme/boost/scss/moodle/sticky-footer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ body {
height: 100%;
}

.stickyfooter {
position: fixed;
right: 0;
left: 0;
height: $navbar-height + 1px;
bottom: -$navbar-height;
transition: bottom .5s;
z-index: $zindex-dropdown;
overflow: hidden;
}

.hasstickyfooter .stickyfooter {
bottom: 0;
}

/* Standard components fixes for sticky footer. */

.stickyfooter ul.pagination {
margin-bottom: map-get($spacers, 1);
}


/* Breakpoints fixes. */

@include media-breakpoint-up(sm) {
#page-wrapper {
height: 100%;
Expand Down
Loading

0 comments on commit 3d483ac

Please sign in to comment.