diff --git a/assets/bootstrap.js b/assets/bootstrap.js
new file mode 100644
index 000000000..c91af8043
--- /dev/null
+++ b/assets/bootstrap.js
@@ -0,0 +1,11 @@
+import { startStimulusApp } from '@symfony/stimulus-bridge';
+
+// Registers Stimulus controllers from controllers.json and in the controllers/ directory
+export default startStimulusApp(require.context(
+ '@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
+ true,
+ /\.[jt]sx?$/,
+));
+
+// register any custom, 3rd party controllers here
+// app.register('some_controller_name', SomeImportedController);
diff --git a/assets/controllers.json b/assets/controllers.json
new file mode 100644
index 000000000..a1c6e90cf
--- /dev/null
+++ b/assets/controllers.json
@@ -0,0 +1,4 @@
+{
+ "controllers": [],
+ "entrypoints": []
+}
diff --git a/assets/controllers/add_tag_controller.js b/assets/controllers/add_tag_controller.js
new file mode 100644
index 000000000..2afe9c2b6
--- /dev/null
+++ b/assets/controllers/add_tag_controller.js
@@ -0,0 +1,13 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ static targets = ['input'];
+
+ toggle() {
+ this.element.classList.toggle('hidden');
+
+ if (!this.element.classList.contains('hidden')) {
+ this.inputTarget.focus();
+ }
+ }
+}
diff --git a/assets/controllers/annotations_controller.js b/assets/controllers/annotations_controller.js
new file mode 100644
index 000000000..1fd4308f6
--- /dev/null
+++ b/assets/controllers/annotations_controller.js
@@ -0,0 +1,57 @@
+import { Controller } from '@hotwired/stimulus';
+import annotator from 'annotator';
+
+export default class extends Controller {
+ static values = {
+ entryId: Number,
+ createUrl: String,
+ updateUrl: String,
+ destroyUrl: String,
+ searchUrl: String,
+ };
+
+ connect() {
+ this.app = new annotator.App();
+
+ this.app.include(annotator.ui.main, {
+ element: this.element,
+ });
+
+ const authorization = {
+ permits() { return true; },
+ };
+ this.app.registry.registerUtility(authorization, 'authorizationPolicy');
+
+ this.app.include(annotator.storage.http, {
+ prefix: '',
+ urls: {
+ create: this.createUrlValue,
+ update: this.updateUrlValue,
+ destroy: this.destroyUrlValue,
+ search: this.searchUrlValue,
+ },
+ entryId: this.entryIdValue,
+ onError(msg, xhr) {
+ if (!Object.prototype.hasOwnProperty.call(xhr, 'responseJSON')) {
+ annotator.notification.banner('An error occurred', 'error');
+ return;
+ }
+ Object.values(xhr.responseJSON.children).forEach((v) => {
+ if (v.errors) {
+ Object.values(v.errors).forEach((errorText) => {
+ annotator.notification.banner(errorText, 'error');
+ });
+ }
+ });
+ },
+ });
+
+ this.app.start().then(() => {
+ this.app.annotations.load({ entry: this.entryIdValue });
+ });
+ }
+
+ disconnect() {
+ this.app.destroy();
+ }
+}
diff --git a/assets/controllers/batch_edit_controller.js b/assets/controllers/batch_edit_controller.js
new file mode 100644
index 000000000..e15fc41c5
--- /dev/null
+++ b/assets/controllers/batch_edit_controller.js
@@ -0,0 +1,15 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ static targets = ['item', 'tagAction'];
+
+ toggleSelection(e) {
+ this.itemTargets.forEach((item) => {
+ item.checked = e.currentTarget.checked; // eslint-disable-line no-param-reassign
+ });
+ }
+
+ tagSelection() {
+ this.element.requestSubmit(this.tagActionTarget);
+ }
+}
diff --git a/assets/controllers/clipboard_controller.js b/assets/controllers/clipboard_controller.js
new file mode 100644
index 000000000..eba297138
--- /dev/null
+++ b/assets/controllers/clipboard_controller.js
@@ -0,0 +1,16 @@
+import { Controller } from '@hotwired/stimulus';
+import ClipboardJS from 'clipboard';
+
+export default class extends Controller {
+ connect() {
+ this.clipboard = new ClipboardJS(this.element);
+
+ this.clipboard.on('success', (e) => {
+ e.clearSelection();
+ });
+ }
+
+ disconnect() {
+ this.clipboard.destroy();
+ }
+}
diff --git a/assets/controllers/config_controller.js b/assets/controllers/config_controller.js
new file mode 100644
index 000000000..804da826a
--- /dev/null
+++ b/assets/controllers/config_controller.js
@@ -0,0 +1,16 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ static targets = ['previewArticle', 'previewContent', 'font', 'fontSize', 'lineHeight', 'maxWidth'];
+
+ connect() {
+ this.updatePreview();
+ }
+
+ updatePreview() {
+ this.previewArticleTarget.style.maxWidth = `${this.maxWidthTarget.value}em`;
+ this.previewContentTarget.style.fontFamily = this.fontTarget.value;
+ this.previewContentTarget.style.fontSize = `${this.fontSizeTarget.value}em`;
+ this.previewContentTarget.style.lineHeight = `${this.lineHeightTarget.value}em`;
+ }
+}
diff --git a/assets/controllers/dark_theme_controller.js b/assets/controllers/dark_theme_controller.js
new file mode 100644
index 000000000..e19b364ea
--- /dev/null
+++ b/assets/controllers/dark_theme_controller.js
@@ -0,0 +1,39 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ connect() {
+ this.#choose();
+
+ this.mql = window.matchMedia('(prefers-color-scheme: dark)');
+ this.mql.addEventListener('change', this.#choose.bind(this));
+ }
+
+ useLight() {
+ this.element.classList.remove('dark-theme');
+ document.cookie = 'theme=light;samesite=Lax;path=/;max-age=31536000';
+ }
+
+ useDark() {
+ this.element.classList.add('dark-theme');
+ document.cookie = 'theme=dark;samesite=Lax;path=/;max-age=31536000';
+ }
+
+ useAuto() {
+ document.cookie = 'theme=auto;samesite=Lax;path=/;max-age=0';
+ this.#choose();
+ }
+
+ #choose() {
+ const themeCookieExists = document.cookie.split(';').some((cookie) => cookie.trim().startsWith('theme='));
+
+ if (themeCookieExists) {
+ return;
+ }
+
+ if (this.mql.matches) {
+ this.element.classList.add('dark-theme');
+ } else {
+ this.element.classList.remove('dark-theme');
+ }
+ }
+}
diff --git a/assets/controllers/entries_navigation_controller.js b/assets/controllers/entries_navigation_controller.js
new file mode 100644
index 000000000..52bbb1138
--- /dev/null
+++ b/assets/controllers/entries_navigation_controller.js
@@ -0,0 +1,58 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ static targets = ['card', 'paginationWrapper'];
+
+ connect() {
+ this.pagination = this.paginationWrapperTarget.querySelector('.pagination');
+
+ this.cardIndex = 0;
+ this.lastCardIndex = this.cardTargets.length - 1;
+
+ /* If we come from next page */
+ if (window.location.hash === '#prev') {
+ this.cardIndex = this.lastCardIndex;
+ }
+
+ this.currentCard = this.cardTargets[this.cardIndex];
+
+ this.currentCard.classList.add('z-depth-4');
+ }
+
+ selectRightCard() {
+ if (this.cardIndex >= 0 && this.cardIndex < this.lastCardIndex) {
+ this.currentCard.classList.remove('z-depth-4');
+ this.cardIndex += 1;
+ this.currentCard = this.cardTargets[this.cardIndex];
+ this.currentCard.classList.add('z-depth-4');
+
+ return;
+ }
+
+ if (this.pagination && this.pagination.querySelector('a[rel="next"]')) {
+ window.location.href = this.pagination.querySelector('a[rel="next"]').href;
+ }
+ }
+
+ selectLeftCard() {
+ if (this.cardIndex > 0 && this.cardIndex <= this.lastCardIndex) {
+ this.currentCard.classList.remove('z-depth-4');
+ this.cardIndex -= 1;
+ this.currentCard = this.cardTargets[this.cardIndex];
+ this.currentCard.classList.add('z-depth-4');
+
+ return;
+ }
+
+ if (this.pagination && this.pagination.querySelector('a[rel="prev"]')) {
+ window.location.href = `${this.pagination.querySelector('a[rel="prev"]').href}#prev`;
+ }
+ }
+
+ selectCurrentCard() {
+ const url = this.currentCard.querySelector('a.card-title').href;
+ if (url) {
+ window.location.href = url;
+ }
+ }
+}
diff --git a/assets/controllers/fake_radio_controller.js b/assets/controllers/fake_radio_controller.js
new file mode 100644
index 000000000..a9426a189
--- /dev/null
+++ b/assets/controllers/fake_radio_controller.js
@@ -0,0 +1,13 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ static targets = ['emailTwoFactor', 'googleTwoFactor'];
+
+ uncheckGoogle() {
+ this.googleTwoFactorTarget.checked = false;
+ }
+
+ uncheckEmail() {
+ this.emailTwoFactorTarget.checked = false;
+ }
+}
diff --git a/assets/controllers/highlight_controller.js b/assets/controllers/highlight_controller.js
new file mode 100644
index 000000000..8177b342e
--- /dev/null
+++ b/assets/controllers/highlight_controller.js
@@ -0,0 +1,11 @@
+import { Controller } from '@hotwired/stimulus';
+import 'highlight.js/styles/atom-one-light.css';
+import hljs from 'highlight.js';
+
+export default class extends Controller {
+ connect() {
+ this.element.querySelectorAll('pre code').forEach((element) => {
+ hljs.highlightElement(element);
+ });
+ }
+}
diff --git a/assets/controllers/leftbar_controller.js b/assets/controllers/leftbar_controller.js
new file mode 100644
index 000000000..45728a6f0
--- /dev/null
+++ b/assets/controllers/leftbar_controller.js
@@ -0,0 +1,7 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ toggleAddTagForm() {
+ this.dispatch('toggleAddTagForm');
+ }
+}
diff --git a/assets/controllers/materialize/collapsible_controller.js b/assets/controllers/materialize/collapsible_controller.js
new file mode 100644
index 000000000..b9e60f38c
--- /dev/null
+++ b/assets/controllers/materialize/collapsible_controller.js
@@ -0,0 +1,16 @@
+import { Controller } from '@hotwired/stimulus';
+import M from '@materializecss/materialize';
+
+export default class extends Controller {
+ static values = {
+ accordion: { type: Boolean, default: true },
+ };
+
+ connect() {
+ this.instance = M.Collapsible.init(this.element, { accordion: this.accordionValue });
+ }
+
+ disconnect() {
+ this.instance.destroy();
+ }
+}
diff --git a/assets/controllers/materialize/dropdown_controller.js b/assets/controllers/materialize/dropdown_controller.js
new file mode 100644
index 000000000..12a209d16
--- /dev/null
+++ b/assets/controllers/materialize/dropdown_controller.js
@@ -0,0 +1,16 @@
+import { Controller } from '@hotwired/stimulus';
+import M from '@materializecss/materialize';
+
+export default class extends Controller {
+ connect() {
+ this.instance = M.Dropdown.init(this.element, {
+ hover: false,
+ coverTrigger: false,
+ constrainWidth: false,
+ });
+ }
+
+ disconnect() {
+ this.instance.destroy();
+ }
+}
diff --git a/assets/controllers/materialize/fab_controller.js b/assets/controllers/materialize/fab_controller.js
new file mode 100644
index 000000000..1d11f38bd
--- /dev/null
+++ b/assets/controllers/materialize/fab_controller.js
@@ -0,0 +1,32 @@
+import { Controller } from '@hotwired/stimulus';
+import M from '@materializecss/materialize';
+
+export default class extends Controller {
+ static values = {
+ edge: { type: String, default: 'left' },
+ };
+
+ connect() {
+ this.instance = M.FloatingActionButton.init(this.element);
+ }
+
+ autoDisplay() {
+ const scrolled = (window.innerHeight + window.scrollY) >= document.body.offsetHeight;
+
+ if (scrolled) {
+ this.toggleScroll = true;
+ this.instance.open();
+ } else if (this.toggleScroll === true) {
+ this.toggleScroll = false;
+ this.instance.close();
+ }
+ }
+
+ click() {
+ this.dispatch('click');
+ }
+
+ disconnect() {
+ this.instance.destroy();
+ }
+}
diff --git a/assets/controllers/materialize/form_select_controller.js b/assets/controllers/materialize/form_select_controller.js
new file mode 100644
index 000000000..3a0fcf374
--- /dev/null
+++ b/assets/controllers/materialize/form_select_controller.js
@@ -0,0 +1,12 @@
+import { Controller } from '@hotwired/stimulus';
+import M from '@materializecss/materialize';
+
+export default class extends Controller {
+ connect() {
+ this.instance = M.FormSelect.init(this.element.querySelector('select'));
+ }
+
+ disconnect() {
+ this.instance.destroy();
+ }
+}
diff --git a/assets/controllers/materialize/sidenav_controller.js b/assets/controllers/materialize/sidenav_controller.js
new file mode 100644
index 000000000..c5c9fbd26
--- /dev/null
+++ b/assets/controllers/materialize/sidenav_controller.js
@@ -0,0 +1,24 @@
+import { Controller } from '@hotwired/stimulus';
+import M from '@materializecss/materialize';
+
+const mobileMaxWidth = 993;
+
+export default class extends Controller {
+ static values = {
+ edge: { type: String, default: 'left' },
+ };
+
+ connect() {
+ this.instance = M.Sidenav.init(this.element, { edge: this.edgeValue });
+ }
+
+ close() {
+ if (window.innerWidth < mobileMaxWidth) {
+ this.instance.close();
+ }
+ }
+
+ disconnect() {
+ this.instance.destroy();
+ }
+}
diff --git a/assets/controllers/materialize/tabs_controller.js b/assets/controllers/materialize/tabs_controller.js
new file mode 100644
index 000000000..312b486d4
--- /dev/null
+++ b/assets/controllers/materialize/tabs_controller.js
@@ -0,0 +1,12 @@
+import { Controller } from '@hotwired/stimulus';
+import M from '@materializecss/materialize';
+
+export default class extends Controller {
+ connect() {
+ this.instance = M.Tabs.init(this.element);
+ }
+
+ disconnect() {
+ this.instance.destroy();
+ }
+}
diff --git a/assets/controllers/materialize/toast_controller.js b/assets/controllers/materialize/toast_controller.js
new file mode 100644
index 000000000..ba9165473
--- /dev/null
+++ b/assets/controllers/materialize/toast_controller.js
@@ -0,0 +1,12 @@
+import { Controller } from '@hotwired/stimulus';
+import M from '@materializecss/materialize';
+
+export default class extends Controller {
+ connect() {
+ this.instance = M.toast({ text: this.element.innerText });
+ }
+
+ disconnect() {
+ this.instance.dismissAll();
+ }
+}
diff --git a/assets/controllers/materialize/tooltip_controller.js b/assets/controllers/materialize/tooltip_controller.js
new file mode 100644
index 000000000..99f2acf3a
--- /dev/null
+++ b/assets/controllers/materialize/tooltip_controller.js
@@ -0,0 +1,12 @@
+import { Controller } from '@hotwired/stimulus';
+import M from '@materializecss/materialize';
+
+export default class extends Controller {
+ connect() {
+ this.instance = M.Tooltip.init(this.element);
+ }
+
+ disconnect() {
+ this.instance.destroy();
+ }
+}
diff --git a/assets/controllers/qrcode_controller.js b/assets/controllers/qrcode_controller.js
new file mode 100644
index 000000000..c7818bca7
--- /dev/null
+++ b/assets/controllers/qrcode_controller.js
@@ -0,0 +1,10 @@
+import { Controller } from '@hotwired/stimulus';
+import jrQrcode from 'jr-qrcode';
+
+export default class extends Controller {
+ static values = { url: String };
+
+ connect() {
+ this.element.setAttribute('src', jrQrcode.getQrBase64(this.urlValue));
+ }
+}
diff --git a/assets/controllers/scroll_indicator_controller.js b/assets/controllers/scroll_indicator_controller.js
new file mode 100644
index 000000000..1fab9b4ed
--- /dev/null
+++ b/assets/controllers/scroll_indicator_controller.js
@@ -0,0 +1,10 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ updateWidth() {
+ const referenceHeight = document.body.offsetHeight - window.innerHeight;
+ const scrollPercent = (window.scrollY / referenceHeight) * 100;
+
+ this.element.style.width = `${scrollPercent}%`;
+ }
+}
diff --git a/assets/controllers/scroll_storage_controller.js b/assets/controllers/scroll_storage_controller.js
new file mode 100644
index 000000000..2bb6fc14b
--- /dev/null
+++ b/assets/controllers/scroll_storage_controller.js
@@ -0,0 +1,19 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ static values = { entryId: Number };
+
+ connect() {
+ window.scrollTo({
+ top: window.innerHeight * localStorage[`wallabag.article.${this.entryIdValue}.percent`],
+ behavior: 'smooth',
+ });
+ }
+
+ saveScroll() {
+ const scrollPercent = window.scrollY / window.innerHeight;
+ const scrollPercentRounded = Math.round(scrollPercent * 100) / 100;
+
+ localStorage[`wallabag.article.${this.entryIdValue}.percent`] = scrollPercentRounded;
+ }
+}
diff --git a/assets/controllers/shortcuts_controller.js b/assets/controllers/shortcuts_controller.js
new file mode 100644
index 000000000..921395fca
--- /dev/null
+++ b/assets/controllers/shortcuts_controller.js
@@ -0,0 +1,141 @@
+import { Controller } from '@hotwired/stimulus';
+import Mousetrap from 'mousetrap';
+
+export default class extends Controller {
+ static targets = ['openOriginal', 'markAsFavorite', 'markAsRead', 'deleteEntry', 'showAddUrl', 'showSearch', 'showActions'];
+
+ static outlets = ['entries-navigation'];
+
+ connect() {
+ /* Go to */
+ Mousetrap.bind('g u', () => {
+ window.location.href = Routing.generate('homepage');
+ });
+ Mousetrap.bind('g s', () => {
+ window.location.href = Routing.generate('starred');
+ });
+ Mousetrap.bind('g r', () => {
+ window.location.href = Routing.generate('archive');
+ });
+ Mousetrap.bind('g a', () => {
+ window.location.href = Routing.generate('all');
+ });
+ Mousetrap.bind('g t', () => {
+ window.location.href = Routing.generate('tag');
+ });
+ Mousetrap.bind('g c', () => {
+ window.location.href = Routing.generate('config');
+ });
+ Mousetrap.bind('g i', () => {
+ window.location.href = Routing.generate('import');
+ });
+ Mousetrap.bind('g d', () => {
+ window.location.href = Routing.generate('developer');
+ });
+ Mousetrap.bind('?', () => {
+ window.location.href = Routing.generate('howto');
+ });
+ Mousetrap.bind('g l', () => {
+ window.location.href = Routing.generate('fos_user_security_logout');
+ });
+
+ /* open original article */
+ Mousetrap.bind('o', () => {
+ if (!this.hasOpenOriginalTarget) {
+ return;
+ }
+
+ this.openOriginalTarget.click();
+ });
+
+ /* mark as favorite */
+ Mousetrap.bind('f', () => {
+ if (!this.hasMarkAsFavoriteTarget) {
+ return;
+ }
+
+ this.markAsFavoriteTarget.click();
+ });
+
+ /* mark as read */
+ Mousetrap.bind('a', () => {
+ if (!this.hasMarkAsReadTarget) {
+ return;
+ }
+
+ this.markAsReadTarget.click();
+ });
+
+ /* delete */
+ Mousetrap.bind('del', () => {
+ if (!this.hasDeleteEntryTarget) {
+ return;
+ }
+
+ this.deleteEntryTarget.click();
+ });
+
+ /* Actions */
+ Mousetrap.bind('g n', (e) => {
+ if (!this.hasShowAddUrlTarget) {
+ return;
+ }
+
+ e.preventDefault();
+ this.showAddUrlTarget.click();
+ });
+
+ Mousetrap.bind('s', (e) => {
+ if (!this.hasShowSearchTarget) {
+ return;
+ }
+
+ e.preventDefault();
+ this.showSearchTarget.click();
+ });
+
+ Mousetrap.bind('esc', (e) => {
+ if (!this.hasShowActionsTarget) {
+ return;
+ }
+
+ e.preventDefault();
+ this.showActionsTarget.click();
+ });
+
+ const originalStopCallback = Mousetrap.prototype.stopCallback;
+
+ Mousetrap.prototype.stopCallback = (e, element, combo) => {
+ // allow esc key to be used in input fields of topbar
+ if (combo === 'esc' && element.dataset.topbarTarget !== undefined) {
+ return false;
+ }
+
+ return originalStopCallback(e, element);
+ };
+
+ Mousetrap.bind('right', () => {
+ if (!this.hasEntriesNavigationOutlet) {
+ return;
+ }
+
+ this.entriesNavigationOutlet.selectRightCard();
+ });
+
+ Mousetrap.bind('left', () => {
+ if (!this.hasEntriesNavigationOutlet) {
+ return;
+ }
+
+ this.entriesNavigationOutlet.selectLeftCard();
+ });
+
+ Mousetrap.bind('enter', () => {
+ if (!this.hasEntriesNavigationOutlet) {
+ return;
+ }
+
+ this.entriesNavigationOutlet.selectCurrentCard();
+ });
+ }
+}
diff --git a/assets/controllers/sticky_nav_controller.js b/assets/controllers/sticky_nav_controller.js
new file mode 100644
index 000000000..12b405b0e
--- /dev/null
+++ b/assets/controllers/sticky_nav_controller.js
@@ -0,0 +1,7 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ toggle() {
+ this.element.classList.toggle('entry-nav-top--sticky');
+ }
+}
diff --git a/assets/controllers/tag_controller.js b/assets/controllers/tag_controller.js
new file mode 100644
index 000000000..3f2b6c054
--- /dev/null
+++ b/assets/controllers/tag_controller.js
@@ -0,0 +1,12 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ static targets = ['link', 'edit', 'form', 'input'];
+
+ showForm() {
+ this.formTarget.classList.remove('hidden');
+ this.editTarget.classList.add('hidden');
+ this.linkTarget.classList.add('hidden');
+ this.inputTarget.focus();
+ }
+}
diff --git a/assets/controllers/topbar_controller.js b/assets/controllers/topbar_controller.js
new file mode 100644
index 000000000..e200893ad
--- /dev/null
+++ b/assets/controllers/topbar_controller.js
@@ -0,0 +1,31 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ static targets = ['addUrl', 'addUrlInput', 'search', 'searchInput', 'actions'];
+
+ showAddUrl() {
+ this.actionsTarget.style.display = 'none';
+ this.addUrlTarget.style.display = 'flex';
+ this.searchTarget.style.display = 'none';
+ this.addUrlInputTarget.focus();
+ }
+
+ submittingUrl(e) {
+ e.currentTarget.disabled = true;
+ this.addUrlInputTarget.readOnly = true;
+ this.addUrlInputTarget.blur();
+ }
+
+ showSearch() {
+ this.actionsTarget.style.display = 'none';
+ this.addUrlTarget.style.display = 'none';
+ this.searchTarget.style.display = 'flex';
+ this.searchInputTarget.focus();
+ }
+
+ showActions() {
+ this.actionsTarget.style.display = 'flex';
+ this.addUrlTarget.style.display = 'none';
+ this.searchTarget.style.display = 'none';
+ }
+}
diff --git a/assets/index.js b/assets/index.js
index a8cfc4877..dbbd06fdc 100755
--- a/assets/index.js
+++ b/assets/index.js
@@ -1,18 +1,11 @@
-import $ from 'jquery';
+import './bootstrap';
/* Materialize imports */
import '@materializecss/materialize/dist/css/materialize.css';
-import M from '@materializecss/materialize/dist/js/materialize';
+import '@materializecss/materialize/dist/js/materialize';
-/* Annotations */
-import annotator from 'annotator';
-
-import ClipboardJS from 'clipboard';
import 'mathjax/es5/tex-svg';
-/* jrQrcode */
-import jrQrcode from 'jr-qrcode';
-
/* Fonts */
import 'material-design-icons-iconfont/dist/material-design-icons.css';
import 'lato-font/css/lato-font.css';
@@ -22,371 +15,5 @@ import '@fontsource/eb-garamond';
import '@fontsource/montserrat';
import '@fontsource/oswald';
-/* Highlight */
-import './js/highlight';
-
-/* Tools */
-import {
- savePercent, retrievePercent, initPreviewText,
-} from './js/tools';
-
-/* Import shortcuts */
-import './js/shortcuts/main';
-import './js/shortcuts/entry';
-
/* Theme style */
import './scss/index.scss';
-
-const mobileMaxWidth = 993;
-
-/* ==========================================================================
- Annotations & Remember position
- ========================================================================== */
-
-$(document).ready(() => {
- if ($('#article').length) {
- const app = new annotator.App();
-
- app.include(annotator.ui.main, {
- element: document.querySelector('article'),
- });
-
- const authorization = {
- permits() { return true; },
- };
- app.registry.registerUtility(authorization, 'authorizationPolicy');
-
- const x = JSON.parse($('#annotationroutes').html());
- app.include(annotator.storage.http, $.extend({}, x, {
- onError(msg, xhr) {
- if (!Object.prototype.hasOwnProperty.call(xhr, 'responseJSON')) {
- annotator.notification.banner('An error occurred', 'error');
- return;
- }
- $.each(xhr.responseJSON.children, (k, v) => {
- if (v.errors) {
- $.each(v.errors, (n, errorText) => {
- annotator.notification.banner(errorText, 'error');
- });
- }
- });
- },
- }));
-
- app.start().then(() => {
- app.annotations.load({ entry: x.entryId });
- });
-
- $(window).scroll(() => {
- const scrollTop = $(window).scrollTop();
- const docHeight = $(document).height();
- const scrollPercent = (scrollTop) / (docHeight);
- const scrollPercentRounded = Math.round(scrollPercent * 100) / 100;
- savePercent(x.entryId, scrollPercentRounded);
- });
-
- retrievePercent(x.entryId);
-
- $(window).resize(() => {
- retrievePercent(x.entryId, true);
- });
- }
-
- document.querySelectorAll('[data-handler=tag-rename]').forEach((item) => {
- const current = item;
- current.wallabag_edit_mode = false;
- current.onclick = (event) => {
- const target = event.currentTarget;
-
- if (target.wallabag_edit_mode === false) {
- $(target.parentNode.querySelector('[data-handle=tag-link]')).addClass('hidden');
- $(target.parentNode.querySelector('[data-handle=tag-rename-form]')).removeClass('hidden');
- target.parentNode.querySelector('[data-handle=tag-rename-form] input').focus();
- target.querySelector('.material-icons').innerHTML = 'done';
-
- target.wallabag_edit_mode = true;
- } else {
- target.parentNode.querySelector('[data-handle=tag-rename-form]').submit();
- }
- };
- });
-
- // mimic radio button because emailTwoFactor is a boolean
- $('#update_user_googleTwoFactor').on('change', () => {
- $('#update_user_emailTwoFactor').prop('checked', false);
- });
-
- $('#update_user_emailTwoFactor').on('change', () => {
- $('#update_user_googleTwoFactor').prop('checked', false);
- });
-
- // same mimic for super admin
- $('#user_googleTwoFactor').on('change', () => {
- $('#user_emailTwoFactor').prop('checked', false);
- });
-
- $('#user_emailTwoFactor').on('change', () => {
- $('#user_googleTwoFactor').prop('checked', false);
- });
-
- // handle copy to clipboard for developer stuff
- const clipboard = new ClipboardJS('.btn');
- clipboard.on('success', (e) => {
- e.clearSelection();
- });
-});
-
-(function darkTheme() {
- const rootEl = document.querySelector('html');
- const themeDom = {
- darkClass: 'dark-theme',
-
- toggleClass(el) {
- return el.classList.toggle(this.darkClass);
- },
-
- addClass(el) {
- return el.classList.add(this.darkClass);
- },
-
- removeClass(el) {
- return el.classList.remove(this.darkClass);
- },
- };
- const themeCookie = {
- values: {
- light: 'light',
- dark: 'dark',
- },
-
- name: 'theme',
-
- getValue(isDarkTheme) {
- return isDarkTheme ? this.values.dark : this.values.light;
- },
-
- setCookie(isDarkTheme) {
- const value = this.getValue(isDarkTheme);
- document.cookie = `${this.name}=${value};samesite=Lax;path=/;max-age=31536000`;
- },
-
- removeCookie() {
- document.cookie = `${this.name}=auto;samesite=Lax;path=/;max-age=0`;
- },
-
- exists() {
- return document.cookie.split(';').some((cookie) => cookie.trim().startsWith(`${this.name}=`));
- },
- };
- const preferedColorScheme = {
- choose() {
- const themeCookieExists = themeCookie.exists();
- if (this.isAvailable() && !themeCookieExists) {
- const isPreferedColorSchemeDark = window.matchMedia('(prefers-color-scheme: dark)').matches === true;
- if (!themeCookieExists) {
- themeDom[isPreferedColorSchemeDark ? 'addClass' : 'removeClass'](rootEl);
- }
- }
- },
-
- isAvailable() {
- return typeof window.matchMedia === 'function';
- },
-
- init() {
- if (!this.isAvailable()) {
- return false;
- }
- this.choose();
- window.matchMedia('(prefers-color-scheme: dark)').addListener(() => {
- this.choose();
- });
- return true;
- },
- };
-
- const addDarkThemeListeners = () => {
- $(document).ready(() => {
- const lightThemeButtons = document.querySelectorAll('.js-theme-toggle[data-theme="light"]');
- [...lightThemeButtons].map((lightThemeButton) => {
- lightThemeButton.addEventListener('click', (e) => {
- e.preventDefault();
- themeDom.removeClass(rootEl);
- themeCookie.setCookie(false);
- });
- return true;
- });
- const darkThemeButtons = document.querySelectorAll('.js-theme-toggle[data-theme="dark"]');
- [...darkThemeButtons].map((darkThemeButton) => {
- darkThemeButton.addEventListener('click', (e) => {
- e.preventDefault();
- themeDom.addClass(rootEl);
- themeCookie.setCookie(true);
- });
- return true;
- });
- const autoThemeButtons = document.querySelectorAll('.js-theme-toggle[data-theme="auto"]');
- [...autoThemeButtons].map((autoThemeButton) => {
- autoThemeButton.addEventListener('click', (e) => {
- e.preventDefault();
- themeCookie.removeCookie();
- preferedColorScheme.choose();
- });
- return true;
- });
- });
- };
-
- preferedColorScheme.init();
- addDarkThemeListeners();
-}());
-
-const stickyNav = () => {
- const nav = $('.js-entry-nav-top');
- $('[data-toggle="actions"]').click(() => {
- nav.toggleClass('entry-nav-top--sticky');
- });
-};
-
-const articleScroll = () => {
- const articleEl = $('#article');
- if (articleEl.length > 0) {
- $(window).scroll(() => {
- const s = $(window).scrollTop();
- const d = $(document).height();
- const c = $(window).height();
- const articleElBottom = articleEl.offset().top + articleEl.height();
- const scrollPercent = (s / (d - c)) * 100;
- $('.progress .determinate').css('width', `${scrollPercent}%`);
- const fixedActionBtn = $('.js-fixed-action-btn');
- const toggleScrollDataName = 'toggle-auto';
- if ((s + c) > articleElBottom) {
- fixedActionBtn.data(toggleScrollDataName, true);
- fixedActionBtn.floatingActionButton('open');
- } else if (fixedActionBtn.data(toggleScrollDataName) === true) {
- fixedActionBtn.data(toggleScrollDataName, false);
- fixedActionBtn.floatingActionButton('close');
- }
- });
- }
-};
-
-$(document).ready(() => {
- // sidenav
- document.querySelectorAll('.sidenav').forEach((element) => {
- $(element).sidenav({ edge: element.getAttribute('data-edge') ?? 'left' });
- });
-
- $('select').formSelect();
- $('.collapsible[data-collapsible="accordion"]').collapsible();
- $('.collapsible[data-collapsible="expandable"]').collapsible({
- accordion: false,
- });
-
- $('.dropdown-trigger').dropdown({ hover: false });
- $('.dropdown-trigger[data-covertrigger="false"][data-constrainwidth="false"]').dropdown({
- hover: false,
- coverTrigger: false,
- constrainWidth: false,
- });
-
- $('.tabs').tabs();
- $('.tooltipped').tooltip();
- $('.fixed-action-btn').floatingActionButton();
-
- stickyNav();
- articleScroll();
- initPreviewText();
-
- const toggleNav = (toShow, toFocus) => {
- $('.nav-panel-actions').hide();
- $(toShow).show();
- $(toFocus).focus();
- };
-
- $('#nav-btn-add-tag').on('click', () => {
- $('.nav-panel-add-tag').toggle();
- $('.nav-panel-menu').addClass('hidden');
- if (window.innerWidth < mobileMaxWidth) {
- $('.sidenav').sidenav('close');
- }
- $('#tag_label').focus();
- return false;
- });
-
- $('#nav-btn-add').on('click', () => {
- toggleNav('.nav-panel-add', '#entry_url');
- return false;
- });
-
- $('#config_fontsize').on('input', () => {
- const value = $('#config_fontsize').val();
- const css = `${value}em`;
- $('#preview-content').css('font-size', css);
- });
-
- $('#config_font').on('change', () => {
- const value = $('#config_font').val();
- $('#preview-content').css('font-family', value);
- });
-
- $('#config_lineHeight').on('input', () => {
- const value = $('#config_lineHeight').val();
- const css = `${value}em`;
- $('#preview-content').css('line-height', css);
- });
-
- $('#config_maxWidth').on('input', () => {
- const value = $('#config_maxWidth').val();
- const css = `${value}em`;
- $('#preview-article').css('max-width', css);
- });
-
- const materialAddForm = $('.nav-panel-add');
- materialAddForm.on('submit', () => {
- materialAddForm.addClass('disabled');
- $('input#entry_url', materialAddForm).prop('readonly', true).trigger('blur');
- });
-
- $('#nav-btn-search').on('click', () => {
- toggleNav('.nav-panel-search', '#search_entry_term');
- return false;
- });
-
- $('.close').on('click', (e) => {
- $(e.target).parent('.nav-panel-item').hide();
- $('.nav-panel-actions').show();
- return false;
- });
-
- const mainCheckboxes = document.querySelectorAll('[data-js="checkboxes-toggle"]');
- if (mainCheckboxes.length) {
- [...mainCheckboxes].forEach((el) => {
- el.addEventListener('click', () => {
- const checkboxes = document.querySelectorAll(el.dataset.toggle);
- [...checkboxes].forEach((checkbox) => {
- const checkboxClone = checkbox;
- checkboxClone.checked = el.checked;
- });
- });
- });
- }
- $('form[name="form_mass_action"] input[name="tags"]').on('keydown', (e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- $('form[name="form_mass_action"] button[name="tag"]').trigger('click');
- }
- });
-
- document.querySelectorAll('img.jr-qrcode').forEach((qrcode) => {
- const src = jrQrcode.getQrBase64(qrcode.getAttribute('data-url'));
-
- qrcode.setAttribute('src', src);
- });
-
- document.querySelectorAll('.material-toast').forEach((toast) => {
- M.toast({
- text: toast.innerText,
- });
- });
-});
diff --git a/assets/js/highlight.js b/assets/js/highlight.js
deleted file mode 100644
index f6f8349b4..000000000
--- a/assets/js/highlight.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import 'highlight.js/styles/atom-one-light.css';
-import hljs from 'highlight.js';
-
-window.addEventListener('load', () => {
- document.querySelectorAll('pre').forEach((element) => {
- hljs.highlightElement(element);
- });
-});
diff --git a/assets/js/shortcuts/entry.js b/assets/js/shortcuts/entry.js
deleted file mode 100644
index 3c15f3d29..000000000
--- a/assets/js/shortcuts/entry.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import Mousetrap from 'mousetrap';
-import $ from 'jquery';
-
-$(document).ready(() => {
- if ($('#article').length > 0) {
- /* open original article */
- Mousetrap.bind('o', () => {
- $('ul.sidenav a.original i')[0].click();
- });
-
- /* mark as favorite */
- Mousetrap.bind('f', () => {
- $('ul.sidenav a.favorite i')[0].click();
- });
-
- /* mark as read */
- Mousetrap.bind('a', () => {
- $('ul.sidenav a.markasread i')[0].click();
- });
-
- /* delete */
- Mousetrap.bind('del', () => {
- $('ul.sidenav a.delete i')[0].click();
- });
- }
-});
diff --git a/assets/js/shortcuts/main.js b/assets/js/shortcuts/main.js
deleted file mode 100644
index 8033d9651..000000000
--- a/assets/js/shortcuts/main.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import Mousetrap from 'mousetrap';
-import $ from 'jquery';
-
-/* Go to */
-Mousetrap.bind('g u', () => { window.location.href = Routing.generate('homepage'); });
-Mousetrap.bind('g s', () => { window.location.href = Routing.generate('starred'); });
-Mousetrap.bind('g r', () => { window.location.href = Routing.generate('archive'); });
-Mousetrap.bind('g a', () => { window.location.href = Routing.generate('all'); });
-Mousetrap.bind('g t', () => { window.location.href = Routing.generate('tag'); });
-Mousetrap.bind('g c', () => { window.location.href = Routing.generate('config'); });
-Mousetrap.bind('g i', () => { window.location.href = Routing.generate('import'); });
-Mousetrap.bind('g d', () => { window.location.href = Routing.generate('developer'); });
-Mousetrap.bind('?', () => { window.location.href = Routing.generate('howto'); });
-Mousetrap.bind('g l', () => { window.location.href = Routing.generate('fos_user_security_logout'); });
-
-function toggleFocus(cardToToogleFocus) {
- if (cardToToogleFocus) {
- $(cardToToogleFocus).toggleClass('z-depth-4');
- }
-}
-
-$(document).ready(() => {
- const cards = $('#content').find('.card');
- const cardNumber = cards.length;
- let cardIndex = 0;
- /* If we come from next page */
- if (window.location.hash === '#prev') {
- cardIndex = cardNumber - 1;
- }
- let card = cards[cardIndex];
- const pagination = $('.pagination');
-
- /* Show nothing on quickstart */
- if ($('#content > div.quickstart').length > 0) {
- return;
- }
-
- /* Show nothing on login/register page */
- if ($('#username').length > 0 || $('#fos_user_registration_form_username').length > 0) {
- return;
- }
-
- /* Show nothing on login/register page */
- if ($('#username').length > 0 || $('#fos_user_registration_form_username').length > 0) {
- return;
- }
-
- /* Focus current card */
- toggleFocus(card);
-
- /* Actions */
- Mousetrap.bind('g n', () => {
- $('#nav-btn-add').trigger('click');
- return false;
- });
-
- Mousetrap.bind('s', () => {
- $('#nav-btn-search').trigger('click');
- return false;
- });
-
- Mousetrap.bind('esc', () => {
- $('.close').trigger('click');
- });
-
- /* Select right card. If there's a next page, go to next page */
- Mousetrap.bind('right', () => {
- if (cardIndex >= 0 && cardIndex < cardNumber - 1) {
- toggleFocus(card);
- cardIndex += 1;
- card = cards[cardIndex];
- toggleFocus(card);
- return;
- }
- if (pagination.length > 0 && pagination.find('li.next:not(.disabled)').length > 0 && cardIndex === cardNumber - 1) {
- window.location.href = window.location.origin + $(pagination).find('li.next a').attr('href');
- }
- });
-
- /* Select previous card. If there's a previous page, go to next page */
- Mousetrap.bind('left', () => {
- if (cardIndex > 0 && cardIndex < cardNumber) {
- toggleFocus(card);
- cardIndex -= 1;
- card = cards[cardIndex];
- toggleFocus(card);
- return;
- }
- if (pagination.length > 0 && $(pagination).find('li.prev:not(.disabled)').length > 0 && cardIndex === 0) {
- window.location.href = `${window.location.origin + $(pagination).find('li.prev a').attr('href')}#prev`;
- }
- });
-
- Mousetrap.bind('enter', () => {
- if (typeof card !== 'object') {
- return;
- }
-
- const url = $(card).find('.card-title a').attr('href');
- if (typeof url === 'string' && url.length > 0) {
- window.location.href = window.location.origin + url;
- }
- });
-});
diff --git a/assets/js/tools.js b/assets/js/tools.js
deleted file mode 100644
index 18528548d..000000000
--- a/assets/js/tools.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import $ from 'jquery';
-
-function supportsLocalStorage() {
- try {
- return 'localStorage' in window && window.localStorage !== null;
- } catch (e) {
- return false;
- }
-}
-
-function savePercent(id, percent) {
- if (!supportsLocalStorage()) { return false; }
- localStorage[`wallabag.article.${id}.percent`] = percent;
- return true;
-}
-
-function retrievePercent(id, resized) {
- if (!supportsLocalStorage()) { return false; }
-
- const bheight = $(document).height();
- const percent = localStorage[`wallabag.article.${id}.percent`];
- const scroll = bheight * percent;
-
- if (!resized) {
- window.scrollTo({
- top: scroll,
- behavior: 'smooth',
- });
- }
-
- return true;
-}
-
-function initPreviewText() {
- // no display if preview_text not available
- if ($('div').is('#preview-article')) {
- const defaultFontFamily = $('#config_font').val();
- const defaultFontSize = $('#config_fontsize').val();
- const defaultLineHeight = $('#config_lineHeight').val();
- const defaultMaxWidth = $('#config_maxWidth').val();
- const previewContent = $('#preview-content');
-
- previewContent.css('font-family', defaultFontFamily);
- previewContent.css('font-size', `${defaultFontSize}em`);
- previewContent.css('line-height', `${defaultLineHeight}em`);
- $('#preview-article').css('max-width', `${defaultMaxWidth}em`);
- }
-}
-
-export {
- savePercent,
- retrievePercent,
- initPreviewText,
-};
diff --git a/assets/scss/_cards.scss b/assets/scss/_cards.scss
index b63fad649..6ae2b8e9b 100644
--- a/assets/scss/_cards.scss
+++ b/assets/scss/_cards.scss
@@ -186,6 +186,14 @@ a.original:not(.waves-effect) {
color: #fff;
}
+.card-tag-labels button {
+ background: transparent;
+ border: none;
+ font-weight: normal;
+ color: #fff;
+ cursor: pointer;
+}
+
.card-tag-link {
width: calc(100% - 24px);
line-height: 1.3;
@@ -196,6 +204,7 @@ a.original:not(.waves-effect) {
.card-tag-form {
display: flex;
+ align-items: center;
min-width: 100px;
flex-grow: 1;
}
diff --git a/package.json b/package.json
index 5ecdbc7af..c2675b005 100644
--- a/package.json
+++ b/package.json
@@ -80,14 +80,14 @@
"@fontsource/eb-garamond": "^5.2.5",
"@fontsource/montserrat": "^5.2.5",
"@fontsource/oswald": "^5.2.5",
+ "@hotwired/stimulus": "^3.2.2",
"@materializecss/materialize": "^1.2.2",
+ "@symfony/stimulus-bridge": "^4.0.0",
"annotator": "wallabag/annotator#master",
"clipboard": "^2.0.11",
"hammerjs": "^2.0.8",
"highlight.js": "^11.11.1",
"icomoon-free-npm": "^0.0.0",
- "jquery": "^3.7.1",
- "jquery.cookie": "^1.4.1",
"jr-qrcode": "^1.2.1",
"material-design-icons-iconfont": "^6.7.0",
"mathjax": "^3.2.2",
@@ -100,7 +100,7 @@
"build:dev": "encore dev",
"watch": "encore dev --watch",
"build:prod": "encore production --progress",
- "lint:js": "eslint assets/*.js assets/js/*.js assets/js/**/*.js",
+ "lint:js": "eslint assets/*.js assets/controllers/*.js",
"lint:scss": "stylelint assets/scss/*.scss assets/scss/**/*.scss"
}
}
diff --git a/templates/Config/index.html.twig b/templates/Config/index.html.twig
index add3d3577..51ec63776 100644
--- a/templates/Config/index.html.twig
+++ b/templates/Config/index.html.twig
@@ -10,7 +10,7 @@
-
+
{{ form_start(form.config) }}
{{ form_errors(form.config) }}
@@ -32,7 +32,7 @@
{{ form_label(form.config.items_per_page) }}
@@ -47,7 +47,7 @@
@@ -64,14 +64,14 @@