Merge branch 'feature/lightbox-carousel' into 'master'

Feature/lightbox-carousel

See merge request idotj/mastodon-embed-timeline!32
This commit is contained in:
i.j 2024-03-09 11:31:09 +00:00
commit 164c45bf9e
11 changed files with 787 additions and 205 deletions

View File

@ -1,3 +1,9 @@
v4.3.5 - 09/03/2024
- Add a carousel/lightbox for pictures and videos in a post
- Improve data set for media items
- Enable loop for videos by default
- Small UI improvements
v4.3.3 - 01/03/2024 v4.3.3 - 01/03/2024
- Fix click conflict on user name - Fix click conflict on user name
- Render emojos in user name - Render emojos in user name
@ -58,7 +64,7 @@ v3.13.1 - 12/01/2024
v3.12.0 - 11/12/2023 v3.12.0 - 11/12/2023
- Fix link preview event on click - Fix link preview event on click
v3.11.0 - 4/11/2023 v3.11.0 - 04/11/2023
- Update icons - Update icons
- Improve loader spinner - Improve loader spinner
@ -103,7 +109,7 @@ v3.8.1 - 14/08/2023
- Improve JS comments - Improve JS comments
v3.8.0 - 04/08/2023 v3.8.0 - 04/08/2023
- Add spoiler/sensitive button for reblog content - Add sensitive/spoiler button for reblog content
v3.7.2 - 25/07/2023 v3.7.2 - 25/07/2023
- Use window.onload to take async attribute into account - Use window.onload to take async attribute into account

View File

@ -2,7 +2,7 @@
![Mastodon timeline widget screenshot](screenshot-light-dark.jpg "Mastodon timeline widget screenshot") ![Mastodon timeline widget screenshot](screenshot-light-dark.jpg "Mastodon timeline widget screenshot")
Embed a mastodon timeline in your page, only with a CSS and JS file. Embed a Mastodon timeline in your page, only with a CSS and JS file.
Demo running: Demo running:
<https://codepen.io/ipuntoj/pen/MWppNGL> <https://codepen.io/ipuntoj/pen/MWppNGL>
@ -65,11 +65,11 @@ This option allows you to start without the need to upload any files on your ser
Copy the following CSS and JS links to include them in your project: Copy the following CSS and JS links to include them in your project:
```html ```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@idotj/mastodon-embed-timeline@4.3.3/dist/mastodon-timeline.min.css" integrity="sha256-n6277x0TxwslF9uskcdwCPorYZnoSB9Wbv1O1w7Ahds=" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@idotj/mastodon-embed-timeline@4.3.5/dist/mastodon-timeline.min.css" crossorigin="anonymous">
``` ```
```html ```html
<script src="https://cdn.jsdelivr.net/npm/@idotj/mastodon-embed-timeline@4.3.3/dist/mastodon-timeline.umd.js" integrity="sha256-H+NYFuLL1tG4+iQmE5LRh5zBg1bToiCK/k4uJi4n3ks=" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@idotj/mastodon-embed-timeline@4.3.5/dist/mastodon-timeline.umd.js" crossorigin="anonymous"></script>
``` ```
### Package manager ### Package manager
@ -108,7 +108,7 @@ To get your timeline up add the following HTML structure in your page:
</div> </div>
``` ```
Then after that you can initialize the script by running: Now you can then initialize the script running:
```js ```js
const myTimeline = new MastodonTimeline.Init(); const myTimeline = new MastodonTimeline.Init();
@ -246,7 +246,7 @@ Here you have all the options available to quickly setup and customize your time
// Default: don't hide // Default: don't hide
hidePinnedPosts: false, hidePinnedPosts: false,
// Hide user account under the user name // Hide the user account under the user name
// Default: don't hide // Default: don't hide
hideUserAccount: false, hideUserAccount: false,
@ -270,15 +270,24 @@ Here you have all the options available to quickly setup and customize your time
// Default: don't apply // Default: don't apply
markdownBlockquote: false, markdownBlockquote: false,
// Show a carousel/lightbox when the user clicks on a picture in a post
// Default: not disabled
disableCarousel: false,
// Customize the text of the buttons used for the carousel/lightbox
carouselCloseTxt: "Close carousel",
carouselPrevTxt: "Previous media item",
carouselNextTxt: "Next media item",
// Limit the text content to a maximum number of lines // Limit the text content to a maximum number of lines
// Default: 0 (unlimited) // Default: 0 (unlimited)
txtMaxLines: "0", txtMaxLines: "0",
// Customize the text of the button used for showing/hiding sensitive/spolier text // Customize the text of the button used for showing/hiding sensitive/spoiler text
btnShowMore: "SHOW MORE", btnShowMore: "SHOW MORE",
btnShowLess: "SHOW LESS", btnShowLess: "SHOW LESS",
// Customize the text of the button used for showing sensitive/spolier media content // Customize the text of the button used for showing sensitive/spoiler media content
btnShowContent: "SHOW CONTENT", btnShowContent: "SHOW CONTENT",
// Customize the text of the button pointing to the Mastodon page placed at the end of the timeline // Customize the text of the button pointing to the Mastodon page placed at the end of the timeline

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -60,24 +60,42 @@
margin: 2rem 0; margin: 2rem 0;
} }
/* Customized CSS styles */ /* Example of customized CSS styles */
.mt-container { .mt-container {
font-family: monospace;
background-color: transparent; background-color: transparent;
border: 1px solid white; border: 1px solid white;
} }
.mt-post {
border-bottom: none;
box-shadow: 2px 2px 6px 2px rgba(0, 0, 0, 0.25);
margin-bottom: 1rem;
}
.mt-post-avatar-image-big img {
border-radius: 0;
}
.mt-container a, .mt-container a,
.mt-container a:active, .mt-container a:active,
.mt-container a:link { .mt-container a:link {
color: darkgreen; color: darkgreen;
} }
.mt-post-header {
margin-bottom: 0;
}
.mt-post-avatar-image-big img {
border-radius: 0;
}
.mt-post-header-user {
margin-right: auto;
}
.mt-post-header-date {
position: absolute;
bottom: 1rem;
left: 3.5rem;
}
.mt-post {
border-bottom: none;
box-shadow: 2px 2px 6px 2px rgba(0, 0, 0, 0.25);
margin-bottom: 1rem;
padding-bottom: 2rem;
}
.mt-post-txt,
.mt-post-media-wrapper {
width: calc(100% - 3rem);
margin-left: auto;
}
</style> </style>
</head> </head>
@ -105,8 +123,7 @@
<p> <p>
At JS level, it defaults to the light theme and the date is displayed At JS level, it defaults to the light theme and the date is displayed
in US format using digits only. In order to achieve a minimalist in US format using digits only. In order to achieve a minimalist
style, the following options have been changed at its style, the following options have been changed at its initialization:
initialization:
</p> </p>
<pre> <pre>
<code> <code>

189
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "@idotj/mastodon-embed-timeline", "name": "@idotj/mastodon-embed-timeline",
"version": "4.3.3", "version": "4.3.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@idotj/mastodon-embed-timeline", "name": "@idotj/mastodon-embed-timeline",
"version": "4.3.3", "version": "4.3.5",
"license": "GNU", "license": "GNU",
"devDependencies": { "devDependencies": {
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
@ -15,14 +15,14 @@
} }
}, },
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/set-array": "^1.0.1", "@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9" "@jridgewell/trace-mapping": "^0.3.24"
}, },
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@ -38,9 +38,9 @@
} }
}, },
"node_modules/@jridgewell/set-array": { "node_modules/@jridgewell/set-array": {
"version": "1.1.2", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@ -63,9 +63,9 @@
"dev": true "dev": true
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.22", "version": "0.3.24",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.24.tgz",
"integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "integrity": "sha512-+VaWXDa6+l6MhflBvVXjIEAzb59nQ2JUK3bwRp2zRpPtU+8TFRy9Gg/5oIcNlkEL5PGlBFGfemUVvIgLnTzq7Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
@ -94,6 +94,110 @@
} }
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz",
"integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz",
"integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz",
"integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz",
"integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz",
"integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz",
"integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz",
"integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz",
"integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.12.0", "version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz",
@ -120,6 +224,45 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz",
"integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz",
"integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz",
"integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@ -281,6 +424,20 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true "dev": true
}, },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@ -530,9 +687,9 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.27.1", "version": "5.28.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.27.1.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.28.1.tgz",
"integrity": "sha512-29wAr6UU/oQpnTw5HoadwjUZnFQXGdOfj0LjZ4sVxzqwHh/QVkvr7m8y9WoR4iN3FRitVduTc6KdjcW38Npsug==", "integrity": "sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",

View File

@ -1,6 +1,6 @@
{ {
"name": "@idotj/mastodon-embed-timeline", "name": "@idotj/mastodon-embed-timeline",
"version": "4.3.3", "version": "4.3.5",
"description": "Displays Mastodon timeline with posts embed in your website. Very easy to setup, no dependencies, no trackers, cross-browser, WCAG compliant and fully responsive.", "description": "Displays Mastodon timeline with posts embed in your website. Very easy to setup, no dependencies, no trackers, cross-browser, WCAG compliant and fully responsive.",
"license": "GNU", "license": "GNU",
"author": { "author": {

View File

@ -1,10 +1,12 @@
/* Mastodon embed timeline v4.3.3 */ /* Mastodon embed timeline v4.3.5 */
/* More info at: */ /* More info at: */
/* https://gitlab.com/idotj/mastodon-embed-timeline */ /* https://gitlab.com/idotj/mastodon-embed-timeline */
/* Variables */ /* Variables */
.mt-container, .mt-container,
.mt-container[data-theme="light"] { .mt-dialog,
.mt-container[data-theme="light"],
.mt-dialog[data-theme="light"] {
--mt-txt-max-lines: none; --mt-txt-max-lines: none;
--mt-color-bg: #fff; --mt-color-bg: #fff;
--mt-color-bg-hover: #d9e1e8; --mt-color-bg-hover: #d9e1e8;
@ -16,8 +18,10 @@
--mt-color-btn-bg: #6364ff; --mt-color-btn-bg: #6364ff;
--mt-color-btn-bg-hover: #563acc; --mt-color-btn-bg-hover: #563acc;
--mt-color-btn-txt: #fff; --mt-color-btn-txt: #fff;
--mt-color-backdrop: #00000090;
} }
.mt-container[data-theme="dark"] { .mt-container[data-theme="dark"],
.mt-dialog[data-theme="dark"] {
--mt-color-bg: #282c37; --mt-color-bg: #282c37;
--mt-color-bg-hover: #313543; --mt-color-bg-hover: #313543;
--mt-color-line-gray: #393f4f; --mt-color-line-gray: #393f4f;
@ -28,11 +32,13 @@
} }
/* Reset CSS */ /* Reset CSS */
.mt-container button { .mt-container button,
.mt-dialog button {
font: inherit; font: inherit;
} }
.mt-container a, .mt-container a,
.mt-container button { .mt-container button,
.mt-dialog button {
cursor: pointer; cursor: pointer;
} }
@ -254,18 +260,30 @@
} }
/* Medias */ /* Medias */
.mt-post-media-wrapper {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1rem;
}
.mt-post-media { .mt-post-media {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
margin-bottom: 1rem;
} }
.mt-post-media-spoiler > img, .mt-post-media-spoiler > img,
.mt-post-media-spoiler > audio, .mt-post-media-spoiler > audio,
.mt-post-media-spoiler > video, .mt-post-media-spoiler > video,
.mt-post-media-spoiler > .mt-post-media-play-icon { .mt-post-media-spoiler > .mt-btn-play {
filter: blur(2rem); filter: blur(2rem);
pointer-events: none; pointer-events: none;
} }
.mt-post-media,
.mt-post-media-spoiler > img,
.mt-post-media-spoiler > audio,
.mt-post-media > img,
.mt-post-media > video {
border-radius: 0.5rem;
}
.mt-post-media > audio { .mt-post-media > audio {
width: 100%; width: 100%;
position: relative; position: relative;
@ -282,28 +300,93 @@
text-align: center; text-align: center;
color: var(--mt-color-content-txt); color: var(--mt-color-content-txt);
} }
.mt-post-media.mt-loading-spinner .mt-post-media-play-icon {
display: none; /* Dialog - modal */
body:has(dialog.mt-dialog[open]) {
overflow: hidden;
} }
.mt-post-media-play-icon { .mt-dialog {
display: flex; display: flex;
position: absolute; flex-direction: column;
width: 3rem;
height: 3rem;
top: calc(50% - 1.5rem);
left: calc(50% - 1.5rem);
justify-content: center;
align-items: center; align-items: center;
justify-content: center;
width: 100%;
height: 100%;
border: none;
color: var(--mt-color-content-txt);
background-color: transparent;
padding: 0;
margin: 1rem;
overflow: hidden;
}
.mt-dialog::backdrop {
background-color: var(--mt-color-backdrop);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
/* Carousel/lightbox */
.mt-carousel-header {
position: absolute;
top: 0;
right: 0;
z-index: 2;
}
.mt-carousel-body {
width: 100%;
height: 100%;
}
.mt-carousel-scroll {
display: flex;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
list-style: none;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none;
}
.mt-carousel-scroll::-webkit-scrollbar {
display: none;
-webkit-appearance: none;
}
.mt-carousel-item {
scroll-snap-align: center;
width: 100%;
height: 100%;
}
.mt-carousel-media-wrapper {
width: calc(100vw - 2.5rem);
height: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.mt-carousel-media {
width: 100%;
height: 100%;
object-fit: contain;
padding: 2rem;
}
.mt-carousel-prev,
.mt-carousel-next {
position: absolute;
background-color: transparent; background-color: transparent;
border: none; border: none;
cursor: pointer; padding: 0.5rem;
z-index: 2;
} }
.mt-post-media-play-icon > svg { .mt-carousel-prev {
width: 2.5rem; left: 0;
height: 2.5rem; padding-left: 0;
fill: var(--mt-color-bg); }
stroke: var(--mt-color-content-txt); .mt-carousel-next {
stroke-width: 1px; right: 0;
padding-right: 0;
} }
/* Preview link */ /* Preview link */
@ -372,7 +455,31 @@
} }
/* Buttons */ /* Buttons */
.mt-container .mt-btn-dark { .mt-btn-play {
display: flex;
position: absolute;
width: 3rem;
height: 3rem;
top: calc(50% - 1.5rem);
left: calc(50% - 1.5rem);
justify-content: center;
align-items: center;
background-color: transparent;
border: none;
cursor: pointer;
}
.mt-btn-play > svg {
width: 2.5rem;
height: 2.5rem;
fill: var(--mt-color-bg);
stroke: var(--mt-color-content-txt);
stroke-width: 1px;
}
.mt-post-media.mt-loading-spinner .mt-btn-play {
display: none;
}
.mt-container .mt-btn-dark,
.mt-dialog .mt-btn-dark {
display: flex; display: flex;
border-radius: 0.25rem; border-radius: 0.25rem;
background-color: var(--mt-color-line-gray); background-color: var(--mt-color-line-gray);
@ -381,11 +488,15 @@
font-weight: 600; font-weight: 600;
font-size: 0.75rem; font-size: 0.75rem;
text-align: center; text-align: center;
padding: 0 0.5rem; padding: 0.25rem 0.5rem;
line-height: 1.25rem; line-height: 1.25rem;
vertical-align: top; vertical-align: top;
} }
.mt-dialog .mt-btn-dark {
margin-left: auto;
}
.mt-dialog .mt-btn-violet,
.mt-dialog a.mt-btn-violet,
.mt-container .mt-btn-violet, .mt-container .mt-btn-violet,
.mt-container a.mt-btn-violet { .mt-container a.mt-btn-violet {
display: flex; display: flex;
@ -400,6 +511,8 @@
background-color: var(--mt-color-btn-bg); background-color: var(--mt-color-btn-bg);
color: var(--mt-color-btn-txt); color: var(--mt-color-btn-txt);
} }
.mt-dialog .mt-btn-violet:hover,
.mt-dialog a.mt-btn-violet:hover,
.mt-container .mt-btn-violet:hover, .mt-container .mt-btn-violet:hover,
.mt-container a.mt-btn-violet:hover { .mt-container a.mt-btn-violet:hover {
background-color: var(--mt-color-btn-bg-hover); background-color: var(--mt-color-btn-bg-hover);
@ -407,6 +520,7 @@
} }
.mt-post-txt .mt-btn-spoiler { .mt-post-txt .mt-btn-spoiler {
display: inline-block; display: inline-block;
vertical-align: middle;
} }
.mt-post-media.mt-loading-spinner > .mt-btn-spoiler { .mt-post-media.mt-loading-spinner > .mt-btn-spoiler {
display: none; display: none;
@ -436,6 +550,7 @@
} }
.mt-error-icon { .mt-error-icon {
font-size: 2rem; font-size: 2rem;
margin-bottom: 1rem;
} }
.mt-error-message { .mt-error-message {
width: 100%; width: 100%;

View File

@ -1,7 +1,7 @@
/** /**
* Mastodon embed timeline * Mastodon embed timeline
* @author idotj * @author idotj
* @version 4.3.3 * @version 4.3.5
* @url https://gitlab.com/idotj/mastodon-embed-timeline * @url https://gitlab.com/idotj/mastodon-embed-timeline
* @license GNU AGPLv3 * @license GNU AGPLv3
*/ */
@ -36,6 +36,10 @@ export class Init {
hidePreviewLink: false, hidePreviewLink: false,
hideCounterBar: false, hideCounterBar: false,
markdownBlockquote: false, markdownBlockquote: false,
disableCarousel: false,
carouselCloseTxt: "Close carousel",
carouselPrevTxt: "Previous media item",
carouselNextTxt: "Next media item",
txtMaxLines: "0", txtMaxLines: "0",
btnShowMore: "SHOW MORE", btnShowMore: "SHOW MORE",
btnShowLess: "SHOW LESS", btnShowLess: "SHOW LESS",
@ -172,42 +176,17 @@ export class Init {
* Requests to the server to collect all the data * Requests to the server to collect all the data
* @returns {object} Data container * @returns {object} Data container
*/ */
#fetchTimelineData() { #getTimelineData() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
/** const instanceApiUrl = `${this.mtSettings.instanceUrl}/api/v1/`;
* Fetch data from server
* @param {string} url address to fetch
* @returns {array} List of objects
*/
async function fetchData(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
"Failed to fetch the following Url: <br>" +
url +
"<hr>" +
"Error status: " +
response.status +
"<hr>" +
"Error message: " +
response.statusText
);
}
const data = await response.json();
return data;
}
// Urls to fetch
let urls = {}; let urls = {};
if (this.mtSettings.instanceUrl) { if (this.mtSettings.instanceUrl) {
if (this.mtSettings.timelineType === "profile") { if (this.mtSettings.timelineType === "profile") {
if (this.mtSettings.userId) { if (this.mtSettings.userId) {
urls.timeline = `${this.mtSettings.instanceUrl}/api/v1/accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`; urls.timeline = `${instanceApiUrl}accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`;
if (!this.mtSettings.hidePinnedPosts) { if (!this.mtSettings.hidePinnedPosts) {
urls.pinned = `${this.mtSettings.instanceUrl}/api/v1/accounts/${this.mtSettings.userId}/statuses?pinned=true`; urls.pinned = `${instanceApiUrl}accounts/${this.mtSettings.userId}/statuses?pinned=true`;
} }
} else { } else {
this.#showError( this.#showError(
@ -217,7 +196,7 @@ export class Init {
} }
} else if (this.mtSettings.timelineType === "hashtag") { } else if (this.mtSettings.timelineType === "hashtag") {
if (this.mtSettings.hashtagName) { if (this.mtSettings.hashtagName) {
urls.timeline = `${this.mtSettings.instanceUrl}/api/v1/timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`; urls.timeline = `${instanceApiUrl}timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`;
} else { } else {
this.#showError( this.#showError(
"Please check your <strong>hashtagName</strong> value", "Please check your <strong>hashtagName</strong> value",
@ -225,7 +204,7 @@ export class Init {
); );
} }
} else if (this.mtSettings.timelineType === "local") { } else if (this.mtSettings.timelineType === "local") {
urls.timeline = `${this.mtSettings.instanceUrl}/api/v1/timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`; urls.timeline = `${instanceApiUrl}timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`;
} else { } else {
this.#showError( this.#showError(
"Please check your <strong>timelineType</strong> value", "Please check your <strong>timelineType</strong> value",
@ -239,15 +218,15 @@ export class Init {
); );
} }
if (!this.mtSettings.hideEmojos) { if (!this.mtSettings.hideEmojos) {
urls.emojos = this.mtSettings.instanceUrl + "/api/v1/custom_emojis"; urls.emojos = `${instanceApiUrl}custom_emojis`;
} }
const urlsPromises = Object.entries(urls).map(([key, url]) => { const urlsPromises = Object.entries(urls).map(([key, url]) => {
return fetchData(url) return this.#fetchData(url)
.then((data) => ({ [key]: data })) .then((data) => ({ [key]: data }))
.catch((error) => { .catch((error) => {
reject( reject(
new Error("Something went wrong fetching data from: " + url) new Error(`Something went wrong fetching data from: ${url}`)
); );
this.#showError(error.message); this.#showError(error.message);
return { [key]: [] }; return { [key]: [] };
@ -266,12 +245,30 @@ export class Init {
}); });
} }
/**
* Fetch data from server
* @param {string} url address to fetch
* @returns {array} List of objects
*/
async #fetchData(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`
Failed to fetch the following Url:<br/>${url}<hr>Error status: ${response.status}<hr>Error message: ${response.statusText}
`);
}
const data = await response.json();
return data;
}
/** /**
* Filter all fetched posts and append them on the timeline * Filter all fetched posts and append them on the timeline
* @param {string} t Type of build (new or reload) * @param {string} t Type of build (new or reload)
*/ */
async #buildTimeline(t) { async #buildTimeline(t) {
await this.#fetchTimelineData(); await this.#getTimelineData();
// Merge pinned posts with timeline posts // Merge pinned posts with timeline posts
let posts; let posts;
@ -320,10 +317,9 @@ export class Init {
// If there are no posts to display, show an error message // If there are no posts to display, show an error message
if (this.mtBodyNode.innerHTML === "") { if (this.mtBodyNode.innerHTML === "") {
const errorMessage = const errorMessage = `No posts to show<hr/>${
"No posts to show <hr/>" + posts?.length || 0
(posts?.length || 0) + } posts have been fetched from the server<hr/>This may be due to an incorrect configuration with the parameters or with the filters applied (to hide certains type of posts)`;
" posts have been fetched from the server <hr/>This may be due to an incorrect configuration in the parameters or to filters applied (to hide certains type of posts)";
this.#showError(errorMessage, "📭"); this.#showError(errorMessage, "📭");
} else { } else {
if (t === "newTimeline") { if (t === "newTimeline") {
@ -379,14 +375,14 @@ export class Init {
'<img src="' + '<img src="' +
c.reblog.account.avatar + c.reblog.account.avatar +
'" alt="' + '" alt="' +
this.#escapeHtml(c.reblog.account.username) + this.#escapeHTML(c.reblog.account.username) +
' avatar" loading="lazy" />' + ' avatar" loading="lazy" />' +
"</div>" + "</div>" +
'<div class="mt-post-avatar-image-small">' + '<div class="mt-post-avatar-image-small">' +
'<img src="' + '<img src="' +
c.account.avatar + c.account.avatar +
'" alt="' + '" alt="' +
this.#escapeHtml(c.account.username) + this.#escapeHTML(c.account.username) +
' avatar" loading="lazy" />' + ' avatar" loading="lazy" />' +
"</div>" + "</div>" +
"</div>" + "</div>" +
@ -399,7 +395,9 @@ export class Init {
c.reblog.account.emojis c.reblog.account.emojis
); );
} else { } else {
userName = c.reblog.account.display_name ? c.reblog.account.display_name : c.reblog.account.username; userName = c.reblog.account.display_name
? c.reblog.account.display_name
: c.reblog.account.username;
} }
if (!this.mtSettings.hideUserAccount) { if (!this.mtSettings.hideUserAccount) {
@ -446,7 +444,7 @@ export class Init {
'<img src="' + '<img src="' +
c.account.avatar + c.account.avatar +
'" alt="' + '" alt="' +
this.#escapeHtml(c.account.username) + this.#escapeHTML(c.account.username) +
' avatar" loading="lazy" />' + ' avatar" loading="lazy" />' +
"</div>" + "</div>" +
"</div>" + "</div>" +
@ -459,7 +457,9 @@ export class Init {
c.account.emojis c.account.emojis
); );
} else { } else {
userName = c.account.display_name ? c.account.display_name : c.account.username; userName = c.account.display_name
? c.account.display_name
: c.account.username;
} }
if (!this.mtSettings.hideUserAccount) { if (!this.mtSettings.hideUserAccount) {
@ -495,22 +495,20 @@ export class Init {
// Date // Date
formattedDate = this.#formatDate(date); formattedDate = this.#formatDate(date);
const timestamp = const timestamp = `
'<div class="mt-post-header-date">' + <div class="mt-post-header-date">
(c.pinned ${
c.pinned
? '<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" class="mt-post-pinned" aria-hidden="true"><path d="m640-480 80 80v80H520v240l-40 40-40-40v-240H240v-80l80-80v-280h-40v-80h400v80h-40v280Zm-286 80h252l-46-46v-314H400v314l-46 46Zm126 0Z"></path></svg>' ? '<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" class="mt-post-pinned" aria-hidden="true"><path d="m640-480 80 80v80H520v240l-40 40-40-40v-240H240v-80l80-80v-280h-40v-80h400v80h-40v280Zm-286 80h252l-46-46v-314H400v314l-46 46Zm126 0Z"></path></svg>'
: "") + : ""
'<a href="' + }
url + <a href="${url}" rel="nofollow noopener noreferrer" target="_blank">
'" rel="nofollow noopener noreferrer" target="_blank">' + <time datetime="${date}">
'<time datetime="' + ${formattedDate}
date + </time>
'">' + ${c.edited_at ? " *" : ""}
formattedDate + </a>
"</time>" + </div>`;
(c.edited_at ? " *" : "") +
"</a>" +
"</div>";
// Main text // Main text
let txtCss = ""; let txtCss = "";
@ -587,6 +585,7 @@ export class Init {
); );
} }
} }
media = `<div class="mt-post-media-wrapper">${media.join("")}</div>`;
// Preview link // Preview link
let previewLink = ""; let previewLink = "";
@ -655,7 +654,7 @@ export class Init {
timestamp + timestamp +
"</div>" + "</div>" +
content + content +
media.join("") + media +
previewLink + previewLink +
poll + poll +
counterBar + counterBar +
@ -743,7 +742,7 @@ export class Init {
* @param {string} s String * @param {string} s String
* @returns {string} String * @returns {string} String
*/ */
#escapeHtml(s) { #escapeHTML(s) {
return (s ?? "") return (s ?? "")
.replaceAll("&", "&amp;") .replaceAll("&", "&amp;")
.replaceAll("<", "&lt;") .replaceAll("<", "&lt;")
@ -793,7 +792,7 @@ export class Init {
/** /**
* Create media element * Create media element
* @param {object} m Media content * @param {object} m Media content
* @param {boolean} s Spoiler/Sensitive status * @param {boolean} s Sensitive/spoiler status
* @returns {string} Media in HTML format * @returns {string} Media in HTML format
*/ */
#createMedia(m, s) { #createMedia(m, s) {
@ -806,6 +805,16 @@ export class Init {
'<div class="mt-post-media ' + '<div class="mt-post-media ' +
(spoiler ? "mt-post-media-spoiler " : "") + (spoiler ? "mt-post-media-spoiler " : "") +
this.mtSettings.spinnerClass + this.mtSettings.spinnerClass +
'" data-media-type="' +
m.type +
'" data-media-url-hd="' +
m.url +
'" data-media-alt-txt="' +
(m.description ? this.#escapeHTML(m.description) : "") +
'" data-media-width-hd="' +
m.meta.original.width +
'" data-media-height-hd="' +
m.meta.original.height +
'" style="padding-top: calc(100%/' + '" style="padding-top: calc(100%/' +
m.meta.small.aspect + m.meta.small.aspect +
')">' + ')">' +
@ -817,7 +826,7 @@ export class Init {
'<img src="' + '<img src="' +
m.preview_url + m.preview_url +
'" alt="' + '" alt="' +
(m.description ? this.#escapeHtml(m.description) : "") + (m.description ? this.#escapeHTML(m.description) : "") +
'" loading="lazy" />' + '" loading="lazy" />' +
"</div>"; "</div>";
} }
@ -828,6 +837,16 @@ export class Init {
'<div class="mt-post-media ' + '<div class="mt-post-media ' +
(spoiler ? "mt-post-media-spoiler " : "") + (spoiler ? "mt-post-media-spoiler " : "") +
this.mtSettings.spinnerClass + this.mtSettings.spinnerClass +
'" data-media-type="' +
m.type +
'" data-media-url-hd="' +
m.preview_url +
'" data-media-alt-txt="' +
(m.description ? this.#escapeHTML(m.description) : "") +
'" data-media-width-hd="' +
m.meta.small.width +
'" data-media-height-hd="' +
m.meta.small.height +
'" style="padding-top: calc(100%/' + '" style="padding-top: calc(100%/' +
m.meta.small.aspect + m.meta.small.aspect +
')">' + ')">' +
@ -842,13 +861,15 @@ export class Init {
'<img src="' + '<img src="' +
m.preview_url + m.preview_url +
'" alt="' + '" alt="' +
(m.description ? this.#escapeHtml(m.description) : "") + (m.description ? this.#escapeHTML(m.description) : "") +
'" loading="lazy" />' + '" loading="lazy" />' +
"</div>"; "</div>";
} else { } else {
media = media =
'<div class="mt-post-media ' + '<div class="mt-post-media ' +
(spoiler ? "mt-post-media-spoiler " : "") + (spoiler ? "mt-post-media-spoiler " : "") +
'" data-media-type="' +
m.type +
'">' + '">' +
(spoiler (spoiler
? '<button class="mt-btn-dark mt-btn-spoiler">' + ? '<button class="mt-btn-dark mt-btn-spoiler">' +
@ -868,8 +889,16 @@ export class Init {
'<div class="mt-post-media ' + '<div class="mt-post-media ' +
(spoiler ? "mt-post-media-spoiler " : "") + (spoiler ? "mt-post-media-spoiler " : "") +
this.mtSettings.spinnerClass + this.mtSettings.spinnerClass +
'" data-video-url="' + '" data-media-type="' +
m.type +
'" data-media-url-hd="' +
m.url + m.url +
'" data-media-alt-txt="' +
(m.description ? this.#escapeHTML(m.description) : "") +
'" data-media-width-hd="' +
m.meta.original.width +
'" data-media-height-hd="' +
m.meta.original.height +
'" style="padding-top: calc(100%/' + '" style="padding-top: calc(100%/' +
m.meta.small.aspect + m.meta.small.aspect +
')">' + ')">' +
@ -881,14 +910,24 @@ export class Init {
'<img src="' + '<img src="' +
m.preview_url + m.preview_url +
'" alt="' + '" alt="' +
(m.description ? this.#escapeHtml(m.description) : "") + (m.description ? this.#escapeHTML(m.description) : "") +
'" loading="lazy" />' + '" loading="lazy" />' +
'<button class="mt-post-media-play-icon" title="Load video"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 14"><path d="M9.5 7l-9 6.3V.7z"/></svg></button>' + '<button class="mt-btn-play" title="Load video"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 14"><path d="M9.5 7l-9 6.3V.7z"/></svg></button>' +
"</div>"; "</div>";
} else { } else {
media = media =
'<div class="mt-post-media ' + '<div class="mt-post-media ' +
(spoiler ? "mt-post-media-spoiler " : "") + (spoiler ? "mt-post-media-spoiler " : "") +
'" data-media-type="' +
m.type +
'" data-media-url-hd="' +
m.url +
'" data-media-alt-txt="' +
(m.description ? this.#escapeHTML(m.description) : "") +
'" data-media-width-hd="' +
m.meta.original.width +
'" data-media-width-hd="' +
m.meta.original.height +
'" style="padding-top: calc(100%/' + '" style="padding-top: calc(100%/' +
m.meta.small.aspect + m.meta.small.aspect +
')">' + ')">' +
@ -899,7 +938,7 @@ export class Init {
: "") + : "") +
'<video controls src="' + '<video controls src="' +
m.url + m.url +
'"></video>' + '" loop></video>' +
"</div>"; "</div>";
} }
} }
@ -907,16 +946,234 @@ export class Init {
return media; return media;
} }
/**
* Open a dialog/modal with the styles of Mastodon timeline
* @param {string} i Dialog Id name
* @param {string} c Dialog HTML content
*/
#openDialog(i, c) {
let dialog = document.createElement("dialog");
dialog.id = i;
dialog.classList.add("mt-dialog");
dialog.dataset.theme = this.mtContainerNode.getAttribute("data-theme");
dialog.innerHTML = c;
document.body.prepend(dialog);
dialog.showModal();
dialog.addEventListener("close", () => {
document.body.removeChild(dialog);
});
}
/**
* Build a carousel/lightbox with the media content in the post clicked
* @param {event} e User interaction trigger
*/
#buildCarousel(e) {
// List all medias in the post and remove sensitive/spoiler medias
const mediaSiblings = Array.from(
e.target.parentNode.parentNode.children
).filter((element) => !element.classList.contains("mt-post-media-spoiler"));
const mediaClickedIndex = mediaSiblings.indexOf(e.target.parentNode) + 1;
// Build media element and wrapper
let mediaItems = [];
mediaSiblings.forEach((sibling, i) => {
let mediaElement = "";
if (
sibling.getAttribute("data-media-type") === "gifv" ||
sibling.getAttribute("data-media-type") === "video"
) {
mediaElement = `
<video controls src="${sibling.getAttribute(
"data-media-url-hd"
)}" width="${sibling.getAttribute(
"data-media-width-hd"
)}" height="${sibling.getAttribute(
"data-media-height-hd"
)}" class="mt-carousel-media" style="max-width:${sibling.getAttribute(
"data-media-width-hd"
)}px; max-height:${sibling.getAttribute(
"data-media-height-hd"
)}px" loop>
</video>
`;
} else {
mediaElement = `
<img src="${sibling.getAttribute(
"data-media-url-hd"
)}" width="${sibling.getAttribute(
"data-media-width-hd"
)}" height="${sibling.getAttribute(
"data-media-height-hd"
)}" class="mt-carousel-media mt-loading-spinner" alt="${sibling.getAttribute(
"data-media-alt-txt"
)}" style="max-width:${sibling.getAttribute(
"data-media-width-hd"
)}px; max-height:${sibling.getAttribute(
"data-media-height-hd"
)}px" dragabble="false" />
`;
}
const mediaWrapper = `
<li class="mt-carousel-item">
<div id="mt-carousel-${
i + 1
}" class="mt-carousel-media-wrapper" data-media-type="${sibling.getAttribute(
"data-media-type"
)}">
${mediaElement}
</div>
</li>
`;
mediaItems.push(mediaWrapper);
});
// Build carousel
const carouselHTML = `
<div class="mt-carousel-header">
<form method="dialog">
<button id="mt-carousel-close" class="mt-btn-dark" title="${
this.mtSettings.carouselCloseTxt
}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path fill-rule="evenodd" d="M5.293 5.293a1 1 0 0 1 1.414 0L12 10.586l5.293-5.293a1 1 0 0 1 1.414 1.414L13.414 12l5.293 5.293a1 1 0 0 1-1.414 1.414L12 13.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L10.586 12 5.293 6.707a1 1 0 0 1 0-1.414z" fill="var(--mt-color-content-txt)"/>
</svg>
</button>
</form>
</div>
<div class="mt-carousel-body">
<ul id="mt-carousel-scroll" class="mt-carousel-scroll">
${mediaItems.join("")}
</ul>
</div>
<button id="mt-carousel-prev" class="mt-carousel-prev" title="${
this.mtSettings.carouselPrevTxt
}" ${mediaClickedIndex === 1 ? "hidden" : ""}>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" aria-hidden="true">
<path d="M560-240 320-480l240-240 56 56-184 184 184 184-56 56Z" fill="var(--mt-color-content-txt)"></path>
</svg>
</button>
<button id="mt-carousel-next" class="mt-carousel-next" title="${
this.mtSettings.carouselNextTxt
}" ${mediaClickedIndex === mediaSiblings.length ? "hidden" : ""}>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" aria-hidden="true">
<path d="M504-480 320-664l56-56 240 240-240 240-56-56 184-184Z" fill="var(--mt-color-content-txt)"></path>
</svg>
</button>
`;
// Call dialog/modal with carousel "id" and HTML content
this.#openDialog("mt-carousel", carouselHTML);
// Set carousel interactions for horizontal scroll and buttons
if (mediaItems.length >= 2) {
this.#setCarouselInteractions(mediaSiblings.length, mediaClickedIndex);
}
}
/**
* Add interactions for the carousel
* @param {number} t Total number of medias loaded
* @param {number} m Index position of media clicked by user
*/
#setCarouselInteractions(t, m) {
let currentMediaIndex = m;
const carousel = document.getElementById("mt-carousel-scroll");
let scrollTimeout = 0;
let userScrolling = false;
const prevBtn = document.getElementById("mt-carousel-prev");
const nextBtn = document.getElementById("mt-carousel-next");
// Scroll the carusel to the media element
const scrollCarouselTo = (i, behavior = "smooth") => {
document
.getElementById("mt-carousel-" + i)
.scrollIntoView({ behavior: behavior });
};
// First run, place the scroll on clicked media
scrollCarouselTo(currentMediaIndex, "instant");
// Get current index of the media shown on screen
const updateMediaIndex = () => {
const scrolledMedia =
(carousel.scrollLeft + carousel.clientWidth) / carousel.clientWidth;
return Math.round(scrolledMedia + Number.EPSILON);
};
// Scroll interactions
const isScrolling = () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
if (userScrolling) {
currentMediaIndex = updateMediaIndex();
checkBtnsVisibility();
}
userScrolling = true;
}, 60);
};
carousel.addEventListener("scroll", isScrolling);
// Click interactions
const checkBtnsVisibility = () => {
prevBtn.hidden = currentMediaIndex === 1;
nextBtn.hidden = currentMediaIndex === t;
};
const userClick = (e) => {
const idTarget = e.target.closest("button")?.id;
// Prev/next buttons
if (idTarget === "mt-carousel-next") {
userScrolling = false;
++currentMediaIndex;
if (currentMediaIndex > t) currentMediaIndex = t;
scrollCarouselTo(currentMediaIndex);
checkBtnsVisibility();
} else if (idTarget === "mt-carousel-prev") {
userScrolling = false;
--currentMediaIndex;
if (currentMediaIndex < 1) currentMediaIndex = 1;
scrollCarouselTo(currentMediaIndex);
checkBtnsVisibility();
}
// Close button
if (idTarget === "mt-carousel-close") {
killEventListeners();
}
};
document.addEventListener("click", userClick);
// Keyboard interactions
const userKeyDown = (e) => {
if (e.key === "Escape" || e.keyCode === 27) {
killEventListeners();
}
};
document.addEventListener("keydown", userKeyDown);
// Kill carousel listeners
const killEventListeners = () => {
carousel.removeEventListener("scroll", isScrolling);
document.removeEventListener("click", userClick);
document.removeEventListener("keydown", userKeyDown);
};
}
/** /**
* Replace the video preview image by the video player * Replace the video preview image by the video player
* @param {event} e User interaction trigger * @param {event} e User interaction trigger
*/ */
#loadPostVideo(e) { #loadPostVideo(e) {
const parentNode = e.target.closest("[data-video-url]"); const parentNode = e.target.closest("[data-media-type]");
const videoUrl = parentNode.dataset.videoUrl; const urlVideo = parentNode.dataset.mediaUrlHd;
parentNode.replaceChildren(); parentNode.replaceChildren();
parentNode.innerHTML = parentNode.innerHTML = `<video controls src="${urlVideo}" autoplay loop></video>`;
'<video controls src="' + videoUrl + '" autoplay></video>';
} }
/** /**
@ -924,28 +1181,29 @@ export class Init {
* @param {event} e User interaction trigger * @param {event} e User interaction trigger
*/ */
#toogleSpoiler(e) { #toogleSpoiler(e) {
const nextSibling = e.target.nextSibling; const target = e.target;
const nextSibling = target.nextSibling;
if ( if (
nextSibling.localName === "img" || nextSibling.localName === "img" ||
nextSibling.localName === "audio" || nextSibling.localName === "audio" ||
nextSibling.localName === "video" nextSibling.localName === "video"
) { ) {
e.target.parentNode.classList.remove("mt-post-media-spoiler"); target.parentNode.classList.remove("mt-post-media-spoiler");
e.target.style.display = "none"; target.style.display = "none";
} else if ( } else if (
nextSibling.classList.contains("spoiler-txt-hidden") || nextSibling.classList.contains("spoiler-txt-hidden") ||
nextSibling.classList.contains("spoiler-txt-visible") nextSibling.classList.contains("spoiler-txt-visible")
) { ) {
if (e.target.textContent == this.mtSettings.btnShowMore) { if (target.textContent == this.mtSettings.btnShowMore) {
nextSibling.classList.remove("spoiler-txt-hidden"); nextSibling.classList.remove("spoiler-txt-hidden");
nextSibling.classList.add("spoiler-txt-visible"); nextSibling.classList.add("spoiler-txt-visible");
e.target.setAttribute("aria-expanded", "true"); target.setAttribute("aria-expanded", "true");
e.target.textContent = this.mtSettings.btnShowLess; target.textContent = this.mtSettings.btnShowLess;
} else { } else {
nextSibling.classList.remove("spoiler-txt-visible"); nextSibling.classList.remove("spoiler-txt-visible");
nextSibling.classList.add("spoiler-txt-hidden"); nextSibling.classList.add("spoiler-txt-hidden");
e.target.setAttribute("aria-expanded", "false"); target.setAttribute("aria-expanded", "false");
e.target.textContent = this.mtSettings.btnShowMore; target.textContent = this.mtSettings.btnShowMore;
} }
} }
} }
@ -966,7 +1224,7 @@ export class Init {
'"><img src="' + '"><img src="' +
c.image + c.image +
'" alt="' + '" alt="' +
this.#escapeHtml(c.image_description) + this.#escapeHTML(c.image_description) +
'" loading="lazy" /></div>' '" loading="lazy" /></div>'
: '<div class="mt-post-preview-noImage">📄</div>') + : '<div class="mt-post-preview-noImage">📄</div>') +
"</div>" + "</div>" +
@ -1032,25 +1290,26 @@ export class Init {
} else if (this.mtSettings.timelineType === "local") { } else if (this.mtSettings.timelineType === "local") {
btnSeeMorePath = "public/local"; btnSeeMorePath = "public/local";
} }
const btnSeeMoreHTML = const btnSeeMoreHTML = `
'<a class="mt-btn-violet btn-see-more" href="' + <a class="mt-btn-violet btn-see-more" href="${
this.mtSettings.instanceUrl + this.mtSettings.instanceUrl
"/" + }/${this.#escapeHTML(
this.#escapeHtml(btnSeeMorePath) + btnSeeMorePath
'" rel="nofollow noopener noreferrer" target="_blank">' + )}" rel="nofollow noopener noreferrer" target="_blank">
this.mtSettings.btnSeeMore + ${this.mtSettings.btnSeeMore}
"</a>"; </a>`;
containerFooter.insertAdjacentHTML("beforeend", btnSeeMoreHTML); containerFooter.insertAdjacentHTML("beforeend", btnSeeMoreHTML);
} }
// Create button to refresh the timeline // Create button to refresh the timeline
if (this.mtSettings.btnReload) { if (this.mtSettings.btnReload) {
const btnReloadHTML = const btnReloadHTML = `
'<button class="mt-btn-violet btn-refresh">' + <button class="mt-btn-violet btn-refresh">
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M21 3v5m0 0h-5m5 0l-3-2.708C16.408 3.867 14.305 3 12 3a9 9 0 1 0 0 18c4.283 0 7.868-2.992 8.777-7" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M21 3v5m0 0h-5m5 0l-3-2.708C16.408 3.867 14.305 3 12 3a9 9 0 1 0 0 18c4.283 0 7.868-2.992 8.777-7" stroke="var(--mt-color-btn-txt)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
this.mtSettings.btnReload + </svg>
"</button>"; ${this.mtSettings.btnReload}
</button>`;
containerFooter.insertAdjacentHTML("beforeend", btnReloadHTML); containerFooter.insertAdjacentHTML("beforeend", btnReloadHTML);
@ -1068,39 +1327,57 @@ export class Init {
*/ */
#setPostsInteracion() { #setPostsInteracion() {
this.mtBodyNode.addEventListener("click", (e) => { this.mtBodyNode.addEventListener("click", (e) => {
const target = e.target;
const localName = target.localName;
const parentNode = target.parentNode;
// Check if post cointainer was clicked // Check if post cointainer was clicked
if ( if (
e.target.localName == "article" || localName == "article" ||
e.target.offsetParent?.localName == "article" || target.offsetParent?.localName == "article" ||
(e.target.localName == "img" && (localName == "img" &&
!e.target.parentNode.getAttribute("data-video-url")) this.mtSettings.disableCarousel &&
parentNode.getAttribute("data-media-type") !== "video" &&
parentNode.getAttribute("data-media-type") !== "gifv")
) { ) {
this.#openPostUrl(e); this.#openPostUrl(e);
} }
// Check if Show More/Less button was clicked // Check if Show More/Less button was clicked
if ( if (
e.target.localName == "button" && localName == "button" &&
e.target.classList.contains("mt-btn-spoiler") target.classList.contains("mt-btn-spoiler")
) { ) {
this.#toogleSpoiler(e); this.#toogleSpoiler(e);
} }
// Check if image in post was clicked
if (
localName == "img" &&
!this.mtSettings.disableCarousel &&
parentNode.getAttribute("data-media-type") !== "video" &&
parentNode.getAttribute("data-media-type") !== "gifv"
) {
this.#buildCarousel(e);
}
// Check if video preview image or play icon/button was clicked // Check if video preview image or play icon/button was clicked
if ( if (
e.target.className == "mt-post-media-play-icon" || target.className == "mt-btn-play" ||
(e.target.localName == "svg" && (localName == "svg" && parentNode.className == "mt-btn-play") ||
e.target.parentNode.className == "mt-post-media-play-icon") || (localName == "path" &&
(e.target.localName == "path" && parentNode.parentNode.className == "mt-btn-play") ||
e.target.parentNode.parentNode.className == (localName == "img" &&
"mt-post-media-play-icon") || (parentNode.getAttribute("data-media-type") === "video" ||
(e.target.localName == "img" && parentNode.getAttribute("data-media-type") === "gifv"))
e.target.parentNode.getAttribute("data-video-url"))
) { ) {
this.#loadPostVideo(e); this.#loadPostVideo(e);
} }
}); });
this.mtBodyNode.addEventListener("keydown", (e) => { this.mtBodyNode.addEventListener("keydown", (e) => {
const localName = e.target.localName;
// Check if Enter key was pressed with focus in an article // Check if Enter key was pressed with focus in an article
if (e.key === "Enter" && e.target.localName == "article") { if (e.key === "Enter" && localName == "article") {
this.#openPostUrl(e); this.#openPostUrl(e);
} }
}); });
@ -1118,6 +1395,7 @@ export class Init {
e.target.localName !== "button" && e.target.localName !== "button" &&
e.target.localName !== "bdi" && e.target.localName !== "bdi" &&
e.target.localName !== "time" && e.target.localName !== "time" &&
!e.target.classList.contains("mt-post-media-spoiler") &&
e.target.className !== "mt-post-preview-noImage" && e.target.className !== "mt-post-preview-noImage" &&
e.target.parentNode.className !== "mt-post-avatar-image-big" && e.target.parentNode.className !== "mt-post-avatar-image-big" &&
e.target.parentNode.className !== "mt-post-avatar-image-small" && e.target.parentNode.className !== "mt-post-avatar-image-small" &&
@ -1155,12 +1433,12 @@ export class Init {
*/ */
#showError(t, i) { #showError(t, i) {
const icon = i || "❌"; const icon = i || "❌";
this.mtBodyNode.innerHTML = this.mtBodyNode.innerHTML = `
'<div class="mt-error"><span class="mt-error-icon">' + <div class="mt-error">
icon + <span class="mt-error-icon">${icon}</span>
'</span><br/><strong>Oops, something\'s happened:</strong><br/><div class="mt-error-message">' + <strong>Oops, something's happened:</strong>
t + <div class="mt-error-message">${t}</div>
"</div></div>"; </div>`;
this.mtBodyNode.setAttribute("role", "none"); this.mtBodyNode.setAttribute("role", "none");
throw new Error( throw new Error(
"Stopping the script due to an error building the timeline." "Stopping the script due to an error building the timeline."