mirror of
https://github.com/webslides/WebSlides.git
synced 2025-09-21 02:11:33 +02:00
Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
85db8868a1 | ||
|
5d2a24962a | ||
|
8b0f11ea3b | ||
|
2d8fb02f48 | ||
|
504ad61dce | ||
|
2723006399 | ||
|
ebb390f3e4 | ||
|
07e0c42871 | ||
|
b833a94c0b | ||
|
c6c263b63e | ||
|
c58e108184 | ||
|
7f53462d46 | ||
|
8f7707b996 | ||
|
dc0386cb49 | ||
|
21f7ba37ca | ||
|
9bbbd7362b | ||
|
6818e4c99c | ||
|
23ad0338cb | ||
|
440e1bf37e | ||
|
f086aefe71 | ||
|
8ae6954e5f | ||
|
09bdc31b64 | ||
|
5c4d2f8769 | ||
|
d03e0cad41 | ||
|
be0e498b2b | ||
|
12bac7afc5 | ||
|
78b6e74b82 | ||
|
91c00a2fe3 | ||
|
e65826dbb6 | ||
|
508cdafea5 | ||
|
50af5fe3df | ||
|
ebe42090ac | ||
|
14e1b5b7b8 | ||
|
2e6471e2ee | ||
|
bc64fcef7e | ||
|
e50193ebb1 | ||
|
b04f15f696 | ||
|
3d6fcef762 | ||
|
32ff8afa88 | ||
|
f082ff12ac | ||
|
62c6aba478 | ||
|
5a262f5460 | ||
|
4379122d1d | ||
|
83f1110947 | ||
|
b7503b1e9e | ||
|
521d708c22 | ||
|
33b1f3ad7a | ||
|
f6ccd39158 | ||
|
6674a4f203 | ||
|
f80106a4ab | ||
|
4fb151c886 | ||
|
b7a01c4319 | ||
|
28c08541e5 | ||
|
6bc8ab582d | ||
|
19cd5bdbc7 | ||
|
bb5ae82995 | ||
|
36c1f1997b | ||
|
5687d61a83 | ||
|
0d2c8c09d1 | ||
|
9e5a127590 | ||
|
7b86354ad4 | ||
|
a67f7d9d0b | ||
|
2234a02252 | ||
|
02834a3b4d | ||
|
3e13eb4025 | ||
|
28756b308f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,4 +15,3 @@ node_modules/
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
static/js/webslides*
|
||||
|
46
CHANGELOG.md
46
CHANGELOG.md
@@ -1,19 +1,53 @@
|
||||
# 1.2.1 (2017-03-02)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Scrollbar showing in Firefox
|
||||
|
||||
# 1.2.0 (2017-03-02)
|
||||
|
||||
## New Features
|
||||
|
||||
- [[#48](https://github.com/jlantunez/webslides/issues/48)] Allows to navigate with AvPag & RePag to allow presentation devices to work.
|
||||
- [[#49](https://github.com/jlantunez/webslides/issues/49)] Allowing to go to first and last slides by using home and end keys respectively.
|
||||
- [[#50](https://github.com/jlantunez/webslides/issues/50)] Using the keyboard on inputs and editable content won't trigger any events that might cause navigation.
|
||||
- [[#47](https://github.com/jlantunez/webslides/issues/47)] Allowing options to be configured. [Read More](/docs/technical.md#options).
|
||||
|
||||
# 1.1.0 (2017-02-28)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fixed a bug which caused Chrome on OSX to stutter a lot on vertical transitioning due to elastic scroll bounce.
|
||||
- [[#28](https://github.com/jlantunez/webslides/issues/28)] Fixed scroll on Firefox.
|
||||
- [[#38](https://github.com/jlantunez/webslides/issues/38)] Fixed a bug in Safari which lead to unexpected behaviour using any form of movements.
|
||||
- [[#10](https://github.com/jlantunez/webslides/issues/10)] Fixed animation flash on Safari.
|
||||
|
||||
## New Features
|
||||
|
||||
- [[#1](https://github.com/jlantunez/webslides/issues/1)] Adding option to click to go to the next slide. Read more [here](https://github.com/jlantunez/webslides/blob/master/docs/click-to-nav.md).
|
||||
- [[#1](https://github.com/jlantunez/webslides/issues/1)] Improved sliding with mouse scroll and touchpad. It's now possible to use scroll to move an horizontal presentation.
|
||||
It's also possible to scroll horizontally on horizontal presentations to move forward/backwards the presentation.
|
||||
|
||||
## Regression
|
||||
|
||||
- Introduced a minor bug on iOS Safari which leads to the bottom part of the page not being visible on the first scroll. This is likely a browser bug but it has been unearthed in this version due to a much needed improvement on scrolling behaviour bugs. We're trying to investigate a bit more and will provide a fix ASAP.
|
||||
|
||||
# 1.0.0 (2017-02-23)
|
||||
|
||||
This release is a special one since it sets up in the path of a better development environment. Although it's far from
|
||||
perfect, it's a solid beginning.
|
||||
perfect, it's a solid beginning.
|
||||
|
||||
All the code has been migrated from jQuery with ES5 to vanilla JavaScript with ES2015 (or ES6) and is fully modular.
|
||||
This means that WebSlides is a (base module)[src/js/modules/webslides.js] with a solid API (few public methods) and
|
||||
All the code has been migrated from **jQuery** with ES5 to **vanilla JavaScript with ES2015 (or ES6) and is fully modular**.
|
||||
This means that WebSlides is a (base module)[src/js/modules/webslides.js] with a solid API (few public methods) and
|
||||
it's extended by (plugins)[src/js/plugins]. This leads to more granularity and less code to dive through while fixing a
|
||||
bug.
|
||||
bug.
|
||||
|
||||
The benefit from this approach is that now it's really easy to extend WebSlides to achieve what you need. You can also
|
||||
**The benefit from this approach is that now it's really easy to extend WebSlides** to achieve what you need. You can also
|
||||
overwrite current plugins. Say you don't like the current navigation with arrows and want to create a menu instead, you
|
||||
can just write that for yourself with your custom needs and register it as `nav` and it will overwrite our nav with
|
||||
your code.
|
||||
|
||||
We hope this leads to a better environment in which WebSlides can grow better.
|
||||
We hope this leads to a better environment in which WebSlides can grow better.
|
||||
|
||||
All the technical specs live now in [this document](docs/technical.md).
|
||||
|
||||
|
31
README.md
31
README.md
@@ -3,25 +3,44 @@
|
||||
[](http://opensource.org/licenses/MIT)
|
||||
[](https://twitter.com/webslides)
|
||||
|
||||
Finally, everything you need to make HTML presentations in a beautiful way. Just the essentials. You can create your own presentation instantly. Simply choose a demo and customize it in minutes — https://webslides.tv/demos.
|
||||
Finally, everything you need to make HTML presentations in a beautiful way. Just the essentials. You can create your own presentation instantly. A new release (at least) every 8th day of the month — [https://webslides.tv/demos](https://webslides.tv/demos).
|
||||
|
||||
* * *
|
||||
### Download
|
||||
Simply choose a demo and customize it in minutes. Latest version: [webslides.tv/webslides-latest.zip](https://webslides.tv/webslides-latest.zip).
|
||||
* * *
|
||||
|
||||
A new release (at least) every 8th day of the month.
|
||||
|
||||
### Why WebSlides?
|
||||
Good karma and productivity. Just a basic knowledge of HTML and CSS is required. Designers, marketers, and journalists can now focus on the content.
|
||||
|
||||
### Features
|
||||
## Features
|
||||
|
||||
- Navigation (horizontal and vertical sliding): touchpad, keyboard shortcuts, and swipe.
|
||||
- Navigation (horizontal and vertical sliding): remote presenters, touchpad, keyboard shortcuts, and swipe.
|
||||
- Slide counter.
|
||||
- Permalinks: go to a specific slide.
|
||||
- Autoslide
|
||||
- Autoslide.
|
||||
- Click to nav. [Read more](docs/click-to-nav.md)!
|
||||
- Simple CSS alignments. Put content wherever you want (vertical centering...)
|
||||
- 40+ components: background images/videos, quotes, cards, covers...
|
||||
- Flexible blocks with auto-fill and equal height.
|
||||
- Fonts: Roboto, Maitree (Serif), and San Francisco.
|
||||
- Vertical rhythm (use multiples of 8).
|
||||
|
||||
### Key Navigation
|
||||
|
||||
There's a handful of keys that can be used to achieve navigation within WebSlides. Here's the list:
|
||||
|
||||
* `←`: If WebSlides is not vertical, it will go to the previous slide.
|
||||
* `→`: If WebSlides is not vertical, it will go to the next slide.
|
||||
* `↑`: If WebSlides is vertical, it will go to the previous slide.
|
||||
* `↓`: If WebSlides is vertical, it will go to the next slide.
|
||||
* `Page Up`: Go to the previous slide.
|
||||
* `Page Down`: Go to the next slide.
|
||||
* `Space`: Go to the next slide.
|
||||
* `Home`: Go to the first slide.
|
||||
* `End`: Go to the last slide.
|
||||
|
||||
## Markup
|
||||
|
||||
- Code is clean and scalable. It uses intuitive markup with popular naming conventions. There's no need to overuse classes or nesting.
|
||||
@@ -99,5 +118,5 @@ Please check out:
|
||||
### Credits
|
||||
|
||||
- WebSlides was created by [@jlantunez](https://twitter.com/jlantunez) using [Cactus](https://github.com/eudicots/Cactus).
|
||||
- Thanks [@LuisSacristan](https://twitter.com/luissacristan) for the javascript code :)
|
||||
- Javascript: [@Belelros](https://twitter.com/Belelros) and [@LuisSacristan](https://twitter.com/luissacristan).
|
||||
- Based on [SimpleSlides](https://github.com/jennschiffer/SimpleSlides), by [@JennSchiffer](https://twitter.com/jennschiffer).
|
||||
|
@@ -1910,7 +1910,7 @@
|
||||
<h2><strong>Start in seconds</strong> </h2>
|
||||
<p class="text-intro">Create your own presentation instantly. <br>120+ prebuilt slides ready to use.</p>
|
||||
<p>
|
||||
<a href="https://github.com/jlantunez/webslides/archive/master.zip" class="button" title="Download WebSlides">
|
||||
<a href="https://webslides.tv/webslides-latest.zip" class="button" title="Download WebSlides">
|
||||
<svg class="fa-cloud-download">
|
||||
<use xlink:href="#fa-cloud-download"></use>
|
||||
</svg>
|
||||
|
@@ -1733,7 +1733,7 @@
|
||||
<div class="cta-cover">
|
||||
<h1><strong>HTML Presentations</strong> Made Easy</h1>
|
||||
<p class="alignright">
|
||||
<a class="button" href="https://github.com/jlantunez/webslides/archive/master.zip" title="Download WebSlides">
|
||||
<a class="button" href="https://webslides.tv/webslides-latest.zip" title="Download WebSlides">
|
||||
<svg class="fa-cloud-download">
|
||||
<use xlink:href="#fa-cloud-download"></use>
|
||||
</svg>
|
||||
@@ -3103,7 +3103,7 @@
|
||||
<h2><strong>Start in Seconds</strong> </h2>
|
||||
<p class="text-intro">Create your own presentation instantly. <br>120+ prebuilt slides ready to use.</p>
|
||||
<p>
|
||||
<a href="https://github.com/jlantunez/webslides/archive/master.zip" class="button" title="Download WebSlides">
|
||||
<a href="https://webslides.tv/webslides-latest.zip" class="button" title="Download WebSlides">
|
||||
<svg class="fa-cloud-download">
|
||||
<use xlink:href="#fa-cloud-download"></use>
|
||||
</svg>
|
||||
|
@@ -199,7 +199,7 @@
|
||||
<h2><strong>Start in seconds</strong> </h2>
|
||||
<p class="text-intro">Create your own presentation instantly. <br>120+ premium slides ready to use.</p>
|
||||
<p>
|
||||
<a href="https://github.com/jlantunez/webslides/archive/master.zip" class="button" title="Download WebSlides">
|
||||
<a href="https://webslides.tv/webslides-latest.zip" class="button" title="Download WebSlides">
|
||||
<svg class="fa-cloud-download">
|
||||
<use xlink:href="#fa-cloud-download"></use>
|
||||
</svg>
|
||||
|
@@ -815,7 +815,7 @@
|
||||
<h2><strong>Start in seconds</strong></h2>
|
||||
<p class="text-intro">120+ prebuilt slides ready to use.</p>
|
||||
<p>
|
||||
<a href="https://github.com/jlantunez/webslides/archive/master.zip" class="button" title="Download WebSlides">
|
||||
<a href="https://webslides.tv/webslides-latest.zip" class="button" title="Download WebSlides">
|
||||
<svg class="fa-cloud-download">
|
||||
<use xlink:href="#fa-cloud-download"></use>
|
||||
</svg>
|
||||
|
@@ -236,7 +236,7 @@
|
||||
<div class="cta-cover">
|
||||
<h1><strong>HTML Presentations</strong> Made Easy</h1>
|
||||
<p class="alignright">
|
||||
<a class="button" href="https://github.com/jlantunez/webslides/archive/master.zip" title="Download WebSlides">
|
||||
<a class="button" href="https://webslides.tv/webslides-latest.zip" title="Download WebSlides">
|
||||
<svg class="fa-cloud-download">
|
||||
<use xlink:href="#fa-cloud-download"></use>
|
||||
</svg>
|
||||
|
@@ -105,7 +105,7 @@
|
||||
<div class="wrap aligncenter">
|
||||
<h1 class="text-landing">Portfolios</h1>
|
||||
<p class="text-symbols">* * * </p>
|
||||
<p><a class="button ghost" href="https://github.com/jlantunez/webslides/archive/master.zip" title="Download WebSlides for free"><svg class="fa-cloud-download">
|
||||
<p><a class="button ghost" href="https://webslides.tv/webslides-latest.zip" title="Download WebSlides for free"><svg class="fa-cloud-download">
|
||||
<use xlink:href="#fa-cloud-download"></use>
|
||||
</svg> WebSlides</a>
|
||||
</p>
|
||||
@@ -333,7 +333,7 @@
|
||||
</div>
|
||||
</section>
|
||||
<section class="bg-black-blue">
|
||||
<span class="background dark" style="background-image:url('https://source.unsplash.com/nw6xREmkXkg/')"></span>
|
||||
<span class="background dark" style="background-image:url('https://source.unsplash.com/LW3FskrgQ9M/')"></span>
|
||||
<div class="wrap size-50">
|
||||
<p class="text-subtitle">Mercedes-Benz</p>
|
||||
<h2>Defining a new platform for the connected car</h2>
|
||||
|
16
docs/click-to-nav.md
Normal file
16
docs/click-to-nav.md
Normal file
@@ -0,0 +1,16 @@
|
||||
## Click To Nav plugin
|
||||
|
||||
This plugin is included by default but disabled. In order to enable it, the option `changeOnClick` must be passed as
|
||||
`true`.
|
||||
|
||||
```javascript
|
||||
const ws = new WebSlides({ changeOnClick: true });
|
||||
```
|
||||
|
||||
This will make every click to navigate to the next slide except for clicks that happens on the following elements:
|
||||
|
||||
* `input`.
|
||||
* `select` or `option`.
|
||||
* `button`.
|
||||
* `a`.
|
||||
* Any element with the attribute `data-prevent-nav`.
|
@@ -24,11 +24,19 @@ WebSlides constructor accepts an object with options.
|
||||
| Param | Type | Default | Description |
|
||||
|-----------|----------------|-----------|-------------------------------------------------------------------------------|
|
||||
| `autoslide` | `number` or `boolean` | `false` | Amount of milliseconds to wait to go to next slide automatically. |
|
||||
| `changeOnClick` | `boolean` | `false` | If true, clicking on the page will go to the next slide unless it's a clickable element. See [ClickToNav docs](./click-to-nav.md) for more info. |
|
||||
| `minWheelDelta` | `number` | `40` | Controls the amount of scroll needed to trigger a navigation. Lower this number to decrease the scroll resistance. |
|
||||
| `scrollWait` | `number` | `450` | Controls the amount of time needed to wait for a scroll transition to happen again. |
|
||||
| `slideOffset` | `number` | `50` | Amount of sliding needed to trigger a new navigation. |
|
||||
|
||||
|
||||
```javascript
|
||||
const ws = new WebSlides({
|
||||
autoslide: false
|
||||
autoslide = false,
|
||||
changeOnClick = false,
|
||||
minWheelDelta = 40,
|
||||
scrollWait = 450,
|
||||
slideOffset = 50
|
||||
});
|
||||
```
|
||||
|
||||
@@ -102,9 +110,13 @@ Registers a plugin to be loaded when the instance is created. It allows
|
||||
|
||||
Those being:
|
||||
|
||||
- Navigation
|
||||
- Hash
|
||||
- Keyboard
|
||||
- ClickNav
|
||||
- Grid
|
||||
- Hash
|
||||
- Keyboard
|
||||
- Navigation
|
||||
- Scroll
|
||||
- Touch
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
|
@@ -109,7 +109,7 @@
|
||||
Just the essentials and using lovely CSS.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/jlantunez/webslides/archive/master.zip" class="button zoomIn" title="Download WebSlides for free">
|
||||
<a href="https://webslides.tv/webslides-latest.zip" class="button zoomIn" title="Download WebSlides for free">
|
||||
<svg class="fa-cloud-download">
|
||||
<use xlink:href="#fa-cloud-download"></use>
|
||||
</svg>
|
||||
@@ -329,7 +329,7 @@
|
||||
<h2><strong>Ready to Start?</strong> </h2>
|
||||
<p class="text-intro">Create your own presentation instantly. <br>120+ premium slides ready to use.</p>
|
||||
<p>
|
||||
<a href="https://github.com/jlantunez/webslides/archive/master.zip" class="button" title="Download WebSlides">
|
||||
<a href="https://webslides.tv/webslides-latest.zip" class="button" title="Download WebSlides">
|
||||
<svg class="fa-cloud-download">
|
||||
<use xlink:href="#fa-cloud-download"></use>
|
||||
</svg>
|
||||
|
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "webslides",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.1",
|
||||
"description": "Making HTML presentations easy",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -12,7 +12,16 @@
|
||||
"presentation",
|
||||
"css"
|
||||
],
|
||||
"author": "Jose Luís Antúnez",
|
||||
"author": "Jose Luís Antúnez <jlantunez@gmail.com>",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Luís Sacristán"
|
||||
},
|
||||
{
|
||||
"name": "Antonio Laguna",
|
||||
"email": "a.laguna@funcion13.com"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/jlantunez/webslides/issues"
|
||||
@@ -22,9 +31,11 @@
|
||||
"babel-cli": "^6.23.0",
|
||||
"babel-core": "^6.23.1",
|
||||
"babel-loader": "^6.3.2",
|
||||
"babel-preset-env": "^1.1.11",
|
||||
"babel-preset-es2015": "^6.22.0",
|
||||
"npm-run-all": "^4.0.2",
|
||||
"rimraf": "^2.6.0",
|
||||
"smart-banner-webpack-plugin": "^3.0.1",
|
||||
"webpack": "^2.2.1",
|
||||
"webpack-dev-server": "^2.4.1"
|
||||
},
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import Plugins from '../plugins/plugins';
|
||||
import Slide from './slide';
|
||||
import DOM from '../utils/dom';
|
||||
import ScrollHelper from '../utils/scroll-to';
|
||||
import scrollTo from '../utils/scroll-to';
|
||||
|
||||
const CLASSES = {
|
||||
VERTICAL: 'vertical'
|
||||
@@ -9,6 +9,7 @@ const CLASSES = {
|
||||
|
||||
// Default plugins
|
||||
const PLUGINS = {
|
||||
'clickNav': Plugins.ClickNav,
|
||||
'grid': Plugins.Grid,
|
||||
'hash': Plugins.Hash,
|
||||
'keyboard': Plugins.Keyboard,
|
||||
@@ -20,11 +21,23 @@ const PLUGINS = {
|
||||
export default class WebSlides {
|
||||
/**
|
||||
* Options for WebSlides
|
||||
* @param {number|boolean} autoslide Is false by default. If a number is
|
||||
* provided, it will autoslide every given milliseconds.
|
||||
* @param {number|boolean} autoslide If a number is provided, it will allow
|
||||
* autosliding by said amount of miliseconds.
|
||||
* @param {boolean} changeOnClick If true, it will allow
|
||||
* clicking on any place to change the slide.
|
||||
* @param {number} minWheelDelta Controls the amount of needed scroll to
|
||||
* trigger navigation.
|
||||
* @param {number} scrollWait Controls the amount of time to wait till
|
||||
* navigation can occur again with scroll.
|
||||
* @param {number} slideOffset Controls the amount of needed touch delta to
|
||||
* trigger navigation.
|
||||
*/
|
||||
constructor({
|
||||
autoslide = false
|
||||
autoslide = false,
|
||||
changeOnClick = false,
|
||||
minWheelDelta = 40,
|
||||
scrollWait = 450,
|
||||
slideOffset = 50
|
||||
} = {}) {
|
||||
/**
|
||||
* WebSlide element.
|
||||
@@ -76,12 +89,16 @@ export default class WebSlides {
|
||||
*/
|
||||
this.interval_ = null;
|
||||
/**
|
||||
* Amount of time to wait to go to next slide automatically or false to
|
||||
* disable the feature.
|
||||
* @type {boolean|number}
|
||||
* @private
|
||||
* Options dictionary.
|
||||
* @type {Object}
|
||||
*/
|
||||
this.autoslide_ = autoslide;
|
||||
this.options = {
|
||||
autoslide,
|
||||
changeOnClick,
|
||||
minWheelDelta,
|
||||
scrollWait,
|
||||
slideOffset
|
||||
};
|
||||
|
||||
if (!this.el) {
|
||||
throw new Error('Couldn\'t find the webslides container!');
|
||||
@@ -141,7 +158,7 @@ export default class WebSlides {
|
||||
* @private
|
||||
*/
|
||||
grabSlides_() {
|
||||
this.slides = Array.from(this.el.childNodes)
|
||||
this.slides = DOM.toArray(this.el.childNodes)
|
||||
.map((slide, i) => new Slide(slide, i));
|
||||
|
||||
this.maxSlide_ = this.slides.length;
|
||||
@@ -155,7 +172,9 @@ export default class WebSlides {
|
||||
* scroll animations.
|
||||
*/
|
||||
goToSlide(slideI, forward = null) {
|
||||
if (this.isValidIndexSlide_(slideI) && !this.isMoving) {
|
||||
if (this.isValidIndexSlide_(slideI) &&
|
||||
!this.isMoving &&
|
||||
this.currentSlideI_ !== slideI) {
|
||||
this.isMoving = true;
|
||||
let isMovingForward = false;
|
||||
|
||||
@@ -186,29 +205,27 @@ export default class WebSlides {
|
||||
* @param {Function} callback Callback to be called upon finishing. This is an
|
||||
* async function so it'll happen once the scroll animation finishes.
|
||||
* @private
|
||||
* @see DOM.lockScroll
|
||||
* @see DOM.unlockScroll
|
||||
* @see ScrollHelper.scrollTo
|
||||
* @see scrollTo
|
||||
*/
|
||||
scrollTransitionToSlide_(isMovingForward, nextSlide, callback) {
|
||||
DOM.lockScroll();
|
||||
this.el.style.overflow = 'hidden';
|
||||
|
||||
if (!isMovingForward) {
|
||||
nextSlide.moveBeforeFirst();
|
||||
nextSlide.show();
|
||||
ScrollHelper.scrollTo(this.currentSlide_.el.offsetTop, 0);
|
||||
scrollTo(this.currentSlide_.el.offsetTop, 0);
|
||||
} else {
|
||||
nextSlide.show();
|
||||
}
|
||||
|
||||
ScrollHelper.scrollTo(nextSlide.el.offsetTop, 500, () => {
|
||||
scrollTo(nextSlide.el.offsetTop, 500, () => {
|
||||
this.currentSlide_.hide();
|
||||
|
||||
if (isMovingForward) {
|
||||
this.currentSlide_.moveAfterLast();
|
||||
}
|
||||
|
||||
DOM.unlockScroll();
|
||||
this.el.style.overflow = 'auto';
|
||||
setTimeout(() => { callback.call(this, nextSlide); }, 150);
|
||||
});
|
||||
}
|
||||
@@ -222,7 +239,7 @@ export default class WebSlides {
|
||||
* @private
|
||||
*/
|
||||
transitionToSlide_(isMovingForward, nextSlide, callback) {
|
||||
ScrollHelper.scrollTo(0, 0);
|
||||
scrollTo(0, 0);
|
||||
|
||||
if (!isMovingForward) {
|
||||
nextSlide.moveBeforeFirst();
|
||||
@@ -345,9 +362,9 @@ export default class WebSlides {
|
||||
* automatically.
|
||||
*/
|
||||
play(time) {
|
||||
time = time || this.autoslide_;
|
||||
time = time || this.options.autoslide;
|
||||
|
||||
if (!this.interval_ && Number.isInteger(time) && time > 0) {
|
||||
if (!this.interval_ && typeof time === 'number' && time > 0) {
|
||||
this.interval_ = setInterval(this.goNext.bind(this), time);
|
||||
}
|
||||
}
|
||||
|
39
src/js/plugins/click-nav.js
Normal file
39
src/js/plugins/click-nav.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const CLICKABLE_ELS = [
|
||||
'INPUT',
|
||||
'SELECT',
|
||||
'OPTION',
|
||||
'BUTTON',
|
||||
'A',
|
||||
'TEXTAREA'
|
||||
];
|
||||
|
||||
export default class ClickNav {
|
||||
/**
|
||||
* ClickNav plugin that allows to click on the page to get to the next slide.
|
||||
* @param {WebSlides} wsInstance The WebSlides instance
|
||||
*/
|
||||
constructor(wsInstance) {
|
||||
/**
|
||||
* @type {WebSlides}
|
||||
* @private
|
||||
*/
|
||||
this.ws_ = wsInstance;
|
||||
|
||||
if (wsInstance.options.changeOnClick) {
|
||||
this.ws_.el.addEventListener('click', this.onClick_.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reacts to the click event. It will go to the next slide unless the element
|
||||
* has a data-prevent-nav attribute or is on the list of CLICKABLE_ELS.
|
||||
* @param {MouseEvent} event The click event.
|
||||
* @private
|
||||
*/
|
||||
onClick_(event) {
|
||||
if (CLICKABLE_ELS.indexOf(event.target.tagName) < 0 &&
|
||||
typeof event.target.dataset.preventNav === 'undefined') {
|
||||
this.ws_.goNext();
|
||||
}
|
||||
}
|
||||
}
|
@@ -47,7 +47,7 @@ export default class Hash {
|
||||
slide = parseInt(results[1], 10);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(slide) || slide < 0 || !Array.isArray(results)) {
|
||||
if (typeof slide !== 'number' || slide < 0 || !Array.isArray(results)) {
|
||||
slide = null;
|
||||
} else {
|
||||
slide--; // Convert to 0 index
|
||||
|
@@ -23,27 +23,52 @@ export default class Keyboard {
|
||||
*/
|
||||
onKeyPress_(event) {
|
||||
let method;
|
||||
let argument;
|
||||
|
||||
if (event.which === Keys.SPACE) {
|
||||
method = this.ws_.goNext;
|
||||
} else {
|
||||
if (this.ws_.isVertical) {
|
||||
if (event.which === Keys.DOWN) {
|
||||
method = this.ws_.goNext;
|
||||
} else if (event.which === Keys.UP) {
|
||||
method = this.ws_.goPrev;
|
||||
}
|
||||
} else {
|
||||
if (event.which === Keys.RIGHT) {
|
||||
method = this.ws_.goNext;
|
||||
} else if (event.which === Keys.LEFT) {
|
||||
method = this.ws_.goPrev;
|
||||
}
|
||||
// Check if there's a focused element that might use the keyboard.
|
||||
if (document.activeElement) {
|
||||
const isContentEditable = document.activeElement
|
||||
.contentEditable !== 'inherit';
|
||||
const isInput = ['INPUT', 'SELECT', 'OPTION', 'TEXTAREA']
|
||||
.indexOf(document.activeElement.tagName) > -1;
|
||||
|
||||
if (isInput || isContentEditable) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (event.which) {
|
||||
case Keys.AV_PAGE:
|
||||
case Keys.SPACE:
|
||||
method = this.ws_.goNext;
|
||||
break;
|
||||
case Keys.RE_PAGE:
|
||||
method = this.ws_.goPrev;
|
||||
break;
|
||||
case Keys.HOME:
|
||||
method = this.ws_.goToSlide;
|
||||
argument = 0;
|
||||
break;
|
||||
case Keys.END:
|
||||
method = this.ws_.goToSlide;
|
||||
argument = this.ws_.maxSlide_ - 1;
|
||||
break;
|
||||
case Keys.DOWN:
|
||||
method = this.ws_.isVertical ? this.ws_.goNext : null;
|
||||
break;
|
||||
case Keys.UP:
|
||||
method = this.ws_.isVertical ? this.ws_.goPrev : null;
|
||||
break;
|
||||
case Keys.LEFT:
|
||||
method = !this.ws_.isVertical ? this.ws_.goPrev : null;
|
||||
break;
|
||||
case Keys.RIGHT:
|
||||
method = !this.ws_.isVertical ? this.ws_.goNext : null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (method) {
|
||||
method.call(this.ws_);
|
||||
method.call(this.ws_, argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import ClickNav from './click-nav';
|
||||
import Grid from './grid';
|
||||
import Hash from './hash';
|
||||
import Keyboard from './keyboard';
|
||||
@@ -6,6 +7,7 @@ import Scroll from './scroll';
|
||||
import Touch from './touch';
|
||||
|
||||
export default {
|
||||
ClickNav,
|
||||
Grid,
|
||||
Hash,
|
||||
Keyboard,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
import ScrollHelper from '../utils/scroll-to';
|
||||
|
||||
const MIN_WHEEL_DELTA = 40;
|
||||
import MobileDetector from '../utils/mobile-detector';
|
||||
|
||||
export default class Scroll {
|
||||
/**
|
||||
@@ -13,16 +11,53 @@ export default class Scroll {
|
||||
* @private
|
||||
*/
|
||||
this.ws_ = wsInstance;
|
||||
|
||||
this.scrollContainer_ = ScrollHelper.getScrollableContainer();
|
||||
/**
|
||||
* Where the scroll is going to happen. The WebSlides element.
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
this.scrollContainer_ = wsInstance.el;
|
||||
/**
|
||||
* Whether movement is happening up or down.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.isGoingUp_ = false;
|
||||
/**
|
||||
* Whether movement is happening left or right.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.isGoingLeft_ = false;
|
||||
/**
|
||||
* Timeout id holder.
|
||||
* @type {?number}
|
||||
* @private
|
||||
*/
|
||||
this.timeout_ = null;
|
||||
|
||||
if (this.ws_.isVertical) {
|
||||
if (!MobileDetector.isAny()) {
|
||||
this.scrollContainer_.addEventListener(
|
||||
'wheel', this.onMouseWheel_.bind(this));
|
||||
|
||||
if (!wsInstance.isVertical) {
|
||||
wsInstance.el.addEventListener(
|
||||
'ws:slide-change', this.onSlideChange_.bind(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the slides change, set an inner timeout to avoid prematurely
|
||||
* changing to the next slide again.
|
||||
* @private
|
||||
*/
|
||||
onSlideChange_() {
|
||||
this.timeout_ = setTimeout(
|
||||
() => { this.timeout_ = null; },
|
||||
this.ws_.options.scrollWait);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reacts to the wheel event. Detects whether is going up or down and decides
|
||||
* if it needs to move the slide based on the amount of delta.
|
||||
@@ -30,15 +65,33 @@ export default class Scroll {
|
||||
* @private
|
||||
*/
|
||||
onMouseWheel_(event) {
|
||||
if (this.ws_.isMoving) {
|
||||
if (this.ws_.isMoving || this.timeout_) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const { deltaY: wheelDelta } = event;
|
||||
this.isGoingUp_ = wheelDelta < 0;
|
||||
const { deltaY: wheelDeltaY, deltaX: wheelDeltaX } = event;
|
||||
const isVertical = this.ws_.isVertical;
|
||||
const isHorizontalMovement = Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY);
|
||||
this.isGoingUp_ = wheelDeltaY < 0;
|
||||
this.isGoingLeft_ = wheelDeltaX < 0;
|
||||
|
||||
if (Math.abs(wheelDelta) >= MIN_WHEEL_DELTA) {
|
||||
if (this.isGoingUp_) {
|
||||
|
||||
// If we're mainly moving horizontally, prevent default
|
||||
if (isHorizontalMovement) {
|
||||
if (!isVertical) {
|
||||
event.preventDefault();
|
||||
} else {
|
||||
// If we're moving horizontally but this is vertical, return to avoid
|
||||
// unwanted navigation.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.abs(wheelDeltaY) >= this.ws_.options.minWheelDelta ||
|
||||
Math.abs(wheelDeltaX) >= this.ws_.options.minWheelDelta) {
|
||||
if ((isHorizontalMovement && this.isGoingLeft_) ||
|
||||
(!isHorizontalMovement && this.isGoingUp_)) {
|
||||
this.ws_.goPrev();
|
||||
} else {
|
||||
this.ws_.goNext();
|
||||
|
@@ -13,8 +13,6 @@ const EVENTS = {
|
||||
}
|
||||
};
|
||||
|
||||
const SLIDE_OFFSET = 50;
|
||||
|
||||
export default class Touch {
|
||||
/**
|
||||
* @param {WebSlides} wsInstance The WebSlides instance
|
||||
@@ -116,9 +114,9 @@ export default class Touch {
|
||||
|
||||
// It's an horizontal drag
|
||||
if (Math.abs(diffX) > Math.abs(diffY)) {
|
||||
if(diffX < -SLIDE_OFFSET) {
|
||||
if (diffX < -this.ws_.options.slideOffset) {
|
||||
this.ws_.goPrev();
|
||||
} else if(diffX > SLIDE_OFFSET) {
|
||||
} else if(diffX > this.ws_.options.slideOffset) {
|
||||
this.ws_.goNext();
|
||||
}
|
||||
}
|
||||
|
@@ -42,22 +42,6 @@ export default class DOM {
|
||||
el.style.display = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the scroll on the document by setting the HTML to have a hidden
|
||||
* overflow.
|
||||
*/
|
||||
static lockScroll() {
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks the scroll on the document by setting the HTML to have an auto
|
||||
* overflow.
|
||||
*/
|
||||
static unlockScroll() {
|
||||
document.documentElement.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires a custom event on the given target.
|
||||
* @param {Element} target The target of the event.
|
||||
@@ -72,4 +56,13 @@ export default class DOM {
|
||||
|
||||
target.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an iterable to an array.
|
||||
* @param {*} iterable Element to convert to array
|
||||
* @return {Array} the element casted to an array.
|
||||
*/
|
||||
static toArray(iterable) {
|
||||
return [].slice.call(iterable);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,10 @@
|
||||
const Keys = {
|
||||
ENTER: 13,
|
||||
SPACE: 32,
|
||||
RE_PAGE: 33,
|
||||
AV_PAGE: 34,
|
||||
END: 35,
|
||||
HOME: 36,
|
||||
LEFT: 37,
|
||||
UP: 38,
|
||||
RIGHT: 39,
|
||||
|
@@ -1,33 +1,6 @@
|
||||
import Easings from './easing';
|
||||
|
||||
let SCROLLABLE_CONTAINER = getScrollableContainer();
|
||||
|
||||
/**
|
||||
* Returns the correct DOM element to be used for scrolling the
|
||||
* page, due to Firefox not scrolling on document.body.
|
||||
* @return {Element} Scrollable Element.
|
||||
*/
|
||||
function getScrollableContainer() {
|
||||
if (SCROLLABLE_CONTAINER) {
|
||||
return SCROLLABLE_CONTAINER;
|
||||
}
|
||||
|
||||
const documentElement = window.document.documentElement;
|
||||
let scrollableContainer;
|
||||
|
||||
documentElement.scrollTop = 1;
|
||||
|
||||
if (documentElement.scrollTop === 1) {
|
||||
documentElement.scrollTop = 0;
|
||||
scrollableContainer = documentElement;
|
||||
} else {
|
||||
scrollableContainer = document.body;
|
||||
}
|
||||
|
||||
SCROLLABLE_CONTAINER = scrollableContainer;
|
||||
|
||||
return scrollableContainer;
|
||||
}
|
||||
let SCROLLABLE_CONTAINER = document.getElementById('webslides');
|
||||
|
||||
/**
|
||||
* Smoothly scrolls to a given Y position using Easing.Swing. It'll run a
|
||||
@@ -36,14 +9,13 @@ function getScrollableContainer() {
|
||||
* @param {number} duration Duration of the animation. 500ms by default.
|
||||
* @param {function} cb Callback function to call upon completion.
|
||||
*/
|
||||
function scrollTo(y, duration = 500, cb = () => {}) {
|
||||
const scrollableContainer = getScrollableContainer();
|
||||
const delta = y - scrollableContainer.scrollTop;
|
||||
const startLocation = scrollableContainer.scrollTop;
|
||||
export default function scrollTo(y, duration = 500, cb = () => {}) {
|
||||
const delta = y - SCROLLABLE_CONTAINER.scrollTop;
|
||||
const startLocation = SCROLLABLE_CONTAINER.scrollTop;
|
||||
const increment = 16;
|
||||
|
||||
if (!duration) {
|
||||
scrollableContainer.scrollTop = y;
|
||||
SCROLLABLE_CONTAINER.scrollTop = y;
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
@@ -58,7 +30,7 @@ function scrollTo(y, duration = 500, cb = () => {}) {
|
||||
delta,
|
||||
duration);
|
||||
|
||||
scrollableContainer.scrollTop = Math.floor(startLocation +
|
||||
SCROLLABLE_CONTAINER.scrollTop = Math.floor(startLocation +
|
||||
(easingP * delta));
|
||||
|
||||
if (elapsedTime < duration) {
|
||||
@@ -70,5 +42,3 @@ function scrollTo(y, duration = 500, cb = () => {}) {
|
||||
|
||||
animateScroll(0);
|
||||
}
|
||||
|
||||
export default { getScrollableContainer, scrollTo };
|
||||
|
@@ -56,7 +56,7 @@
|
||||
0. CSS Reset & Normalize
|
||||
=========================================== */
|
||||
|
||||
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, bbbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { border: 0; font-size: 100%; font: inherit; vertical-align: baseline; margin: 0; padding: 0 }
|
||||
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { border: 0; font-size: 100%; font: inherit; vertical-align: baseline; margin: 0; padding: 0 }
|
||||
|
||||
article, aside, details, figcaption, figure, footer, header, main, menu, nav, section, summary {
|
||||
display: block;
|
||||
@@ -181,9 +181,14 @@ hr {
|
||||
text-align: center;
|
||||
margin: 3.2rem auto;
|
||||
}
|
||||
h2 + hr, h3 + hr {margin-bottom: 4.8rem;
|
||||
|
||||
h2 + hr,
|
||||
h3 + hr {
|
||||
margin-bottom: 4.8rem;
|
||||
}
|
||||
p + hr {margin-bottom: 4rem;
|
||||
|
||||
p + hr {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -205,7 +210,7 @@ i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
bbbr,
|
||||
abbr,
|
||||
acronym {
|
||||
cursor: help;
|
||||
}
|
||||
@@ -238,7 +243,6 @@ img {
|
||||
}
|
||||
|
||||
img:hover {
|
||||
filter: alpha(opacity=9000);
|
||||
opacity: 0.90;
|
||||
filter: alpha(opacity=90);
|
||||
}
|
||||
@@ -298,11 +302,31 @@ dd {
|
||||
1. Base --> Baseline: 8px = .8rem
|
||||
=========================================== */
|
||||
|
||||
/* -- Disable elastic scrolling/bounce -- */
|
||||
|
||||
html,
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* == Prototype faster - Vertical rhythm == */
|
||||
#webslides {
|
||||
height: 100vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
/* -- Hide scrollbar, but still being able to scroll -- */
|
||||
|
||||
#webslides {
|
||||
-ms-overflow-style: none; /* IE 10+ */
|
||||
}
|
||||
#webslides::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
|
||||
/* -- Prototype faster - Vertical rhythm -- */
|
||||
|
||||
body.baseline {
|
||||
background: url(../images/baseline.png) left top .8rem/.8rem;
|
||||
@@ -472,11 +496,13 @@ pre code {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
/*=== 1.2 Animations ================
|
||||
Just 3 basic animations:
|
||||
.fadeIn, .fadeInUp, .zoomIn.
|
||||
Just 5 basic animations:
|
||||
.fadeIn, .fadeInUp, .zoomIn, .slideInLeft, slideInRight
|
||||
https://github.com/daneden/animate.css*/
|
||||
|
||||
/*-- fadeIn -- */
|
||||
@-webkit-keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -508,6 +534,7 @@ https://github.com/daneden/animate.css*/
|
||||
animation: fadeIn 1s;
|
||||
}
|
||||
|
||||
/*-- fadeInUp -- */
|
||||
@-webkit-keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -537,12 +564,11 @@ https://github.com/daneden/animate.css*/
|
||||
}
|
||||
|
||||
.fadeInUp {
|
||||
-webkit-animation-name: fadeInUp;
|
||||
animation-name: fadeInUp;
|
||||
-webkit-animation-duration: 1s;
|
||||
animation-duration: 1s;
|
||||
-webkit-animation: fadeInUp 1s;
|
||||
animation: fadeInUp 1s;
|
||||
}
|
||||
|
||||
/*-- zoomIn -- */
|
||||
@-webkit-keyframes zoomIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -572,6 +598,42 @@ https://github.com/daneden/animate.css*/
|
||||
animation: zoomIn 1s;
|
||||
}
|
||||
|
||||
/*-- slideInLeft -- */
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.slideInLeft {
|
||||
-webkit-animation: slideInLeft 1s;
|
||||
animation: slideInLeft 1s;
|
||||
}
|
||||
|
||||
/*-- slideInRight -- */
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translate3d(100%, 0, 0);
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.slideInRight {
|
||||
-webkit-animation: slideInRight 1s;
|
||||
animation: slideInRight 1s;
|
||||
}
|
||||
|
||||
/* Animated Background (Matrix) */
|
||||
@-webkit-keyframes anim {
|
||||
0% {
|
||||
@@ -604,7 +666,6 @@ https://github.com/daneden/animate.css*/
|
||||
animation-duration: 5s;
|
||||
}
|
||||
|
||||
|
||||
/* Transitions */
|
||||
|
||||
header,
|
||||
|
1807
static/js/webslides.js
Normal file
1807
static/js/webslides.js
Normal file
File diff suppressed because it is too large
Load Diff
9
static/js/webslides.min.js
vendored
Normal file
9
static/js/webslides.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,6 +1,8 @@
|
||||
const SmartBannerPlugin = require('smart-banner-webpack-plugin');
|
||||
const path = require('path');
|
||||
|
||||
const src = path.join(__dirname, 'src');
|
||||
const pkg = require('./package.json');
|
||||
|
||||
module.exports = {
|
||||
context: src,
|
||||
@@ -14,6 +16,7 @@ module.exports = {
|
||||
},
|
||||
devServer: {
|
||||
contentBase: __dirname,
|
||||
host: '0.0.0.0'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
@@ -23,5 +26,12 @@ module.exports = {
|
||||
include: src
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new SmartBannerPlugin({
|
||||
banner: `Name: WebSlides\nVersion: ${pkg.version}\nDate: ${new Date().toISOString().slice(0,10)}\nDescription: ${pkg.description}\nURL: ${pkg.homepage}\nCredits: @jlantunez, @LuisSacristan, @Belelros`,
|
||||
raw: false,
|
||||
entryOnly: true
|
||||
})
|
||||
],
|
||||
};
|
||||
|
Reference in New Issue
Block a user