Просмотр исходного кода

ESLint upgrade from JSHint (#3906)

* ESLint upgrade from JSHint
* commit corresponding package.json
* `npm run fix` for automatic JS and CSS fixes
* Keep JSHint config for now
Alexandre Alapetite 4 лет назад
Родитель
Сommit
b438d8bb3d

+ 3 - 0
.eslintignore

@@ -0,0 +1,3 @@
+*.min.js
+node_modules/
+p/scripts/vendor/

+ 26 - 0
.eslintrc.json

@@ -0,0 +1,26 @@
+{
+	"env": {
+		"browser": true
+	},
+	"extends": [
+		"eslint:recommended",
+		"standard"
+	],
+	"rules": {
+		"camelcase": "off",
+		"comma-dangle": ["warn", "always-multiline"],
+		"eqeqeq": "off",
+		"indent": ["warn", "tab", { "SwitchCase": 1 }],
+		"linebreak-style": ["error", "unix"],
+		"max-len": ["warn", 165],
+		"no-tabs": "off",
+		"semi": ["warn", "always"],
+		"space-before-function-paren": ["warn", {
+			"anonymous": "always",
+			"named": "never",
+			"asyncArrow": "always"
+		}],
+		"yoda": "off"
+	},
+	"root": true
+}

+ 1 - 1
.gitignore

@@ -1,6 +1,6 @@
 /bin
 /node_modules
-package*.json
+package-lock.json
 constants.local.php
 
 # Temp files

+ 3 - 2
.jshintrc

@@ -1,8 +1,9 @@
 {
-	"esversion" : 6,
+	"esversion" : 8,
 	"browser" : true,
 	"globals": {
 		"confirm": true,
 		"console": true
-	}
+	},
+	"strict": "global"
 }

+ 3 - 5
.stylelintrc

@@ -33,11 +33,9 @@
     "no-eol-whitespace": true,
     "property-no-vendor-prefix": true,
     "rule-empty-line-before": [
-      "always",
-      "except": [
-        "after-single-line-comment",
-        "first-nested"
-      ]
+      "always", {
+        "except": ["after-single-line-comment","first-nested"]
+      }
     ],
     "order/properties-order": [
       "margin",

+ 2 - 5
.travis.yml

@@ -45,12 +45,9 @@ jobs:
       env:
         - HADOLINT="$HOME/hadolint"
       install:
-        - npm install --save-dev jshint stylelint stylelint-order stylelint-scss stylelint-config-recommended-scss
+        - npm install
         - curl -sL -o ${HADOLINT} "https://github.com/hadolint/hadolint/releases/download/v1.18.0/hadolint-$(uname -s)-$(uname -m)" && chmod 700 ${HADOLINT}
       script:
-        - node_modules/jshint/bin/jshint .
-        # check SCSS separately
-        - stylelint --syntax scss "**/*.scss"
-        - stylelint "**/*.css"
+        - npm test
         - bash tests/shellchecks.sh
         - git ls-files --exclude='*Dockerfile*' --ignored | xargs --max-lines=1 "$HADOLINT"

+ 37 - 38
p/scripts/api.js

@@ -1,6 +1,5 @@
 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
-"use strict";
-/* jshint esversion:6, strict:global */
+'use strict';
 
 function check(url, next) {
 	if (!url || !next) {
@@ -10,8 +9,8 @@ function check(url, next) {
 	req.open('GET', url, true);
 	req.setRequestHeader('Authorization', 'GoogleLogin auth=test/1');
 	req.onerror = function (e) {
-			next('FAIL: HTTP ' + e);
-		};
+		next('FAIL: HTTP ' + e);
+	};
 	req.onload = function () {
 		if (this.status == 200) {
 			next(this.response);
@@ -25,40 +24,40 @@ function check(url, next) {
 const jsonVars = JSON.parse(document.getElementById('jsonVars').innerHTML);
 
 check(jsonVars.greader + '/check/compatibility', function next(result1) {
-		const greaderOutput = document.getElementById('greaderOutput');
-		if (result1 === 'PASS') {
-			greaderOutput.innerHTML = '✔️ ' + result1;
-		} else {
-			check(jsonVars.greader + '/check%2Fcompatibility', function next(result2) {
-				if (result2 === 'PASS') {
-					greaderOutput.innerHTML = '⚠️ WARN: no <code>%2F</code> support, so some clients will not work!';
-				} else {
-					check('./greader.php/check/compatibility', function next(result3) {
-						if (result3 === 'PASS') {
-							greaderOutput.innerHTML = '⚠️ WARN: Probable invalid base URL in ./data/config.php';
-						} else {
-							greaderOutput.innerHTML = '❌ ' + result1;
-						}
-					});
-				}
-			});
-		}
-	});
-
-check(jsonVars.fever + '?api', function next(result1) {
-		const feverOutput = document.getElementById('feverOutput');
-		try {
-			JSON.parse(result1);
-			feverOutput.innerHTML = '✔️ PASS';
-		} catch (ex) {
-			check('./fever.php?api', function next(result2) {
-					try {
-						JSON.parse(result2);
-						feverOutput.innerHTML = '⚠️ WARN: Probable invalid base URL in ./data/config.php';
-					} catch (ex) {
-						feverOutput.innerHTML = '❌ ' + result1;
+	const greaderOutput = document.getElementById('greaderOutput');
+	if (result1 === 'PASS') {
+		greaderOutput.innerHTML = '✔️ ' + result1;
+	} else {
+		check(jsonVars.greader + '/check%2Fcompatibility', function next(result2) {
+			if (result2 === 'PASS') {
+				greaderOutput.innerHTML = '⚠️ WARN: no <code>%2F</code> support, so some clients will not work!';
+			} else {
+				check('./greader.php/check/compatibility', function next(result3) {
+					if (result3 === 'PASS') {
+						greaderOutput.innerHTML = '⚠️ WARN: Probable invalid base URL in ./data/config.php';
+					} else {
+						greaderOutput.innerHTML = '❌ ' + result1;
 					}
 				});
-		}
-	});
+			}
+		});
+	}
+});
+
+check(jsonVars.fever + '?api', function next(result1) {
+	const feverOutput = document.getElementById('feverOutput');
+	try {
+		JSON.parse(result1);
+		feverOutput.innerHTML = '✔️ PASS';
+	} catch (ex) {
+		check('./fever.php?api', function next(result2) {
+			try {
+				JSON.parse(result2);
+				feverOutput.innerHTML = '⚠️ WARN: Probable invalid base URL in ./data/config.php';
+			} catch (ex) {
+				feverOutput.innerHTML = '❌ ' + result1;
+			}
+		});
+	}
+});
 // @license-end

+ 97 - 97
p/scripts/category.js

@@ -1,16 +1,15 @@
 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
-"use strict";
+'use strict';
 /* globals context */
-/* jshint esversion:6, strict:global */
 
-var loading = false,
-	dnd_successful = false;
+let loading = false;
+let dnd_successful = false;
 
 function dragend_process(t) {
 	t.setAttribute('draggable', 'false');
 
 	if (loading) {
-		setTimeout(function() {
+		setTimeout(function () {
 			dragend_process(t);
 		}, 50);
 		return;
@@ -25,13 +24,14 @@ function dragend_process(t) {
 		t.remove();
 
 		if (p.childElementCount <= 1) {
-			p.insertAdjacentHTML('afterbegin', '<li class="item feed disabled" dropzone="move"><div class="alert-warn">' + context.i18n.category_empty + '</div></li>');
+			p.insertAdjacentHTML('afterbegin',
+				'<li class="item feed disabled" dropzone="move"><div class="alert-warn">' + context.i18n.category_empty + '</div></li>');
 		}
 	}
 }
 
-var dragFeedId = '',
-	dragHtml = '';
+let dragFeedId = '';
+let dragHtml = '';
 
 function init_draggable() {
 	if (!window.context) {
@@ -42,99 +42,99 @@ function init_draggable() {
 		return;
 	}
 
-	const draggable = '[draggable="true"]',
-		dropzone = '[dropzone="move"]',
-		dropSection = document.querySelector('.drop-section');
-
-	dropSection.ondragstart = function(ev) {
-			const li = ev.target.closest ? ev.target.closest(draggable) : null;
-			if (li) {
-				const drag = ev.target.closest('[draggable]');
-				ev.dataTransfer.effectAllowed = 'move';
-				dragHtml = drag.outerHTML;
-				dragFeedId = drag.getAttribute('data-feed-id');
-				ev.dataTransfer.setData('text', dragFeedId);
-				drag.style.opacity = 0.3;
-				dnd_successful = false;
-			}
-		};
+	const draggable = '[draggable="true"]';
+	const dropzone = '[dropzone="move"]';
+	const dropSection = document.querySelector('.drop-section');
+
+	dropSection.ondragstart = function (ev) {
+		const li = ev.target.closest ? ev.target.closest(draggable) : null;
+		if (li) {
+			const drag = ev.target.closest('[draggable]');
+			ev.dataTransfer.effectAllowed = 'move';
+			dragHtml = drag.outerHTML;
+			dragFeedId = drag.getAttribute('data-feed-id');
+			ev.dataTransfer.setData('text', dragFeedId);
+			drag.style.opacity = 0.3;
+			dnd_successful = false;
+		}
+	};
 
-	dropSection.ondragend = function(ev) {
-			const li = ev.target.closest ? ev.target.closest(draggable) : null;
-			if (li) {
-				dragend_process(li);
-			}
-		};
+	dropSection.ondragend = function (ev) {
+		const li = ev.target.closest ? ev.target.closest(draggable) : null;
+		if (li) {
+			dragend_process(li);
+		}
+	};
 
-	dropSection.ondragenter = function(ev) {
-			const li = ev.target.closest ? ev.target.closest(dropzone) : null;
-			if (li) {
-				li.classList.add('drag-hover');
-				return false;
-			}
-		};
-
-	dropSection.onddragleave = function(ev) {
-			const li = ev.target.closest ? ev.target.closest(dropzone) : null;
-			if (li) {
-				const scroll_top = document.documentElement.scrollTop,
-					top = li.offsetTop,
-					left = li.offsetLeft,
-					right = left + li.clientWidth,
-					bottom = top + li.clientHeight,
-					mouse_x = ev.screenX,
-					mouse_y = ev.clientY + scroll_top;
-
-				if (left <= mouse_x && mouse_x <= right &&
+	dropSection.ondragenter = function (ev) {
+		const li = ev.target.closest ? ev.target.closest(dropzone) : null;
+		if (li) {
+			li.classList.add('drag-hover');
+			return false;
+		}
+	};
+
+	dropSection.onddragleave = function (ev) {
+		const li = ev.target.closest ? ev.target.closest(dropzone) : null;
+		if (li) {
+			const scroll_top = document.documentElement.scrollTop;
+			const top = li.offsetTop;
+			const left = li.offsetLeft;
+			const right = left + li.clientWidth;
+			const bottom = top + li.clientHeight;
+			const mouse_x = ev.screenX;
+			const mouse_y = ev.clientY + scroll_top;
+
+			if (left <= mouse_x && mouse_x <= right &&
 					top <= mouse_y && mouse_y <= bottom) {
-					// HACK because dragleave is triggered when hovering children!
-					return;
-				}
-				li.classList.remove('drag-hover');
+				// HACK because dragleave is triggered when hovering children!
+				return;
 			}
-		};
+			li.classList.remove('drag-hover');
+		}
+	};
 
-	dropSection.ondragover = function(ev) {
-			const li = ev.target.closest ? ev.target.closest(dropzone) : null;
-			if (li) {
-				ev.dataTransfer.dropEffect = "move";
-				return false;
-			}
-		};
-
-	dropSection.ondrop = function(ev) {
-			const li = ev.target.closest ? ev.target.closest(dropzone) : null;
-			if (li) {
-				loading = true;
-
-				const req = new XMLHttpRequest();
-				req.open('POST', './?c=feed&a=move', true);
-				req.responseType = 'json';
-				req.onload = function (e) {
-						if (this.status == 200) {
-							li.insertAdjacentHTML('afterend', dragHtml);
-							if (li.classList.contains('disabled')) {
-								li.remove();
-							}
-							dnd_successful = true;
-						}
-					};
-				req.onloadend = function (e) {
-						loading = false;
-						dragFeedId = '';
-						dragHtml = '';
-					};
-				req.setRequestHeader('Content-Type', 'application/json');
-				req.send(JSON.stringify({
-						f_id: dragFeedId,
-						c_id: li.parentElement.getAttribute('data-cat-id'),
-						_csrf: context.csrf,
-					}));
-
-				li.classList.remove('drag-hover');
-				return false;
-			}
-		};
+	dropSection.ondragover = function (ev) {
+		const li = ev.target.closest ? ev.target.closest(dropzone) : null;
+		if (li) {
+			ev.dataTransfer.dropEffect = 'move';
+			return false;
+		}
+	};
+
+	dropSection.ondrop = function (ev) {
+		const li = ev.target.closest ? ev.target.closest(dropzone) : null;
+		if (li) {
+			loading = true;
+
+			const req = new XMLHttpRequest();
+			req.open('POST', './?c=feed&a=move', true);
+			req.responseType = 'json';
+			req.onload = function (e) {
+				if (this.status == 200) {
+					li.insertAdjacentHTML('afterend', dragHtml);
+					if (li.classList.contains('disabled')) {
+						li.remove();
+					}
+					dnd_successful = true;
+				}
+			};
+			req.onloadend = function (e) {
+				loading = false;
+				dragFeedId = '';
+				dragHtml = '';
+			};
+			req.setRequestHeader('Content-Type', 'application/json');
+			req.send(JSON.stringify({
+				f_id: dragFeedId,
+				c_id: li.parentElement.getAttribute('data-cat-id'),
+				_csrf: context.csrf,
+			}));
+
+			li.classList.remove('drag-hover');
+			return false;
+		}
+	};
 }
 
 function archiving() {
@@ -143,7 +143,7 @@ function archiving() {
 		if (e.target.id === 'use_default_purge_options') {
 			slider.querySelectorAll('.archiving').forEach(function (element) {
 				element.hidden = e.target.checked;
-				if (!e.target.checked) element.style.visibility = 'visible'; 	//Help for Edge 44
+				if (!e.target.checked) element.style.visibility = 'visible'; 	// Help for Edge 44
 			});
 		}
 	});

+ 2 - 3
p/scripts/draggable.js

@@ -1,8 +1,7 @@
 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
-"use strict";
-/* jshint esversion:6, strict:global */
+'use strict';
 
-const init_draggable_list = function() {
+const init_draggable_list = function () {
 	if (!window.context) {
 		if (window.console) {
 			console.log('FreshRSS draggable list waiting for JS…');

+ 82 - 83
p/scripts/extra.js

@@ -1,7 +1,6 @@
 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
-"use strict";
+'use strict';
 /* globals context, openNotification, openPopupWithSource, xmlHttpRequestJson */
-/* jshint esversion:6, strict:global */
 
 function fix_popup_preview_selector() {
 	const link = document.getElementById('popup-preview-selector');
@@ -20,8 +19,8 @@ function fix_popup_preview_selector() {
 	});
 }
 
-//<crypto form (Web login)>
-function poormanSalt() {	//If crypto.getRandomValues is not available
+// <crypto form (Web login)>
+function poormanSalt() {	// If crypto.getRandomValues is not available
 	const base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789/abcdefghijklmnopqrstuvwxyz';
 	let text = '$2a$04$';
 	for (let i = 22; i > 0; i--) {
@@ -61,8 +60,8 @@ function init_crypto_form() {
 		const req = new XMLHttpRequest();
 		req.open('GET', './?c=javascript&a=nonce&user=' + document.getElementById('username').value, false);
 		req.onerror = function () {
-				openNotification('Communication error!', 'bad');
-			};
+			openNotification('Communication error!', 'bad');
+		};
 		req.send();
 		if (req.status == 200) {
 			const json = xmlHttpRequestJson(req);
@@ -70,9 +69,9 @@ function init_crypto_form() {
 				openNotification('Invalid user!', 'bad');
 			} else {
 				try {
-					const strong = window.Uint32Array && window.crypto && (typeof window.crypto.getRandomValues === 'function'),
-						s = dcodeIO.bcrypt.hashSync(document.getElementById('passwordPlain').value, json.salt1),
-						c = dcodeIO.bcrypt.hashSync(json.nonce + s, strong ? dcodeIO.bcrypt.genSaltSync(4) : poormanSalt());
+					const strong = window.Uint32Array && window.crypto && (typeof window.crypto.getRandomValues === 'function');
+					const s = dcodeIO.bcrypt.hashSync(document.getElementById('passwordPlain').value, json.salt1);
+					const c = dcodeIO.bcrypt.hashSync(json.nonce + s, strong ? dcodeIO.bcrypt.genSaltSync(4) : poormanSalt());
 					document.getElementById('challenge').value = c;
 					if (!s || !c) {
 						openNotification('Crypto error!', 'bad');
@@ -91,83 +90,83 @@ function init_crypto_form() {
 		return success;
 	};
 }
-//</crypto form (Web login)>
+// </crypto form (Web login)>
 
 function init_password_observers() {
 	document.querySelectorAll('.toggle-password').forEach(function (a) {
-			a.onmousedown = function (ev) {
-					const passwordField = document.getElementById(this.getAttribute('data-toggle'));
-					passwordField.setAttribute('type', 'text');
-					this.classList.add('active');
-					return false;
-				};
-			a.onmouseup = function (ev) {
-					const passwordField = document.getElementById(this.getAttribute('data-toggle'));
-					passwordField.setAttribute('type', 'password');
-					this.classList.remove('active');
-					return false;
-				};
-		});
+		a.onmousedown = function (ev) {
+			const passwordField = document.getElementById(this.getAttribute('data-toggle'));
+			passwordField.setAttribute('type', 'text');
+			this.classList.add('active');
+			return false;
+		};
+		a.onmouseup = function (ev) {
+			const passwordField = document.getElementById(this.getAttribute('data-toggle'));
+			passwordField.setAttribute('type', 'password');
+			this.classList.remove('active');
+			return false;
+		};
+	});
 }
 
 function init_select_observers() {
 	document.querySelectorAll('.select-change').forEach(function (s) {
-			s.onchange = function (ev) {
-					const opt = s.options[s.selectedIndex],
-						url = opt.getAttribute('data-url');
-					if (url) {
-						s.disabled = true;
-						s.value = '';
-						if (s.form) {
-							s.form.querySelectorAll('[type=submit]').forEach(function (b) {
-									b.disabled = true;
-								});
-						}
-						location.href = url;
-					}
-				};
-		});
+		s.onchange = function (ev) {
+			const opt = s.options[s.selectedIndex];
+			const url = opt.getAttribute('data-url');
+			if (url) {
+				s.disabled = true;
+				s.value = '';
+				if (s.form) {
+					s.form.querySelectorAll('[type=submit]').forEach(function (b) {
+						b.disabled = true;
+					});
+				}
+				location.href = url;
+			}
+		};
+	});
 }
 
 function init_slider_observers() {
-	const slider = document.getElementById('slider'),
-		closer = document.getElementById('close-slider');
+	const slider = document.getElementById('slider');
+	const closer = document.getElementById('close-slider');
 	if (!slider) {
 		return;
 	}
 
 	document.querySelector('.post').onclick = function (ev) {
-			const a = ev.target.closest('.open-slider');
-			if (a) {
-				if (!context.ajax_loading) {
-					context.ajax_loading = true;
-
-					const req = new XMLHttpRequest();
-					req.open('GET', a.href + '&ajax=1', true);
-					req.responseType = 'document';
-					req.onload = function (e) {
-							slider.innerHTML = this.response.body.innerHTML;
-							slider.classList.add('active');
-							closer.classList.add('active');
-							context.ajax_loading = false;
-							fix_popup_preview_selector();
-						};
-					req.send();
-					return false;
-				}
+		const a = ev.target.closest('.open-slider');
+		if (a) {
+			if (!context.ajax_loading) {
+				context.ajax_loading = true;
+
+				const req = new XMLHttpRequest();
+				req.open('GET', a.href + '&ajax=1', true);
+				req.responseType = 'document';
+				req.onload = function (e) {
+					slider.innerHTML = this.response.body.innerHTML;
+					slider.classList.add('active');
+					closer.classList.add('active');
+					context.ajax_loading = false;
+					fix_popup_preview_selector();
+				};
+				req.send();
+				return false;
 			}
-		};
+		}
+	};
 
 	closer.onclick = function (ev) {
-			if (data_leave_validation() || confirm(context.i18n.confirmation_default)) {
-				slider.querySelectorAll('form').forEach(function (f) { f.reset(); });
-				closer.classList.remove('active');
-				slider.classList.remove('active');
-				return true;
-			} else {
-				return false;
-			}
-		};
+		if (data_leave_validation() || confirm(context.i18n.confirmation_default)) {
+			slider.querySelectorAll('form').forEach(function (f) { f.reset(); });
+			closer.classList.remove('active');
+			slider.classList.remove('active');
+			return true;
+		} else {
+			return false;
+		}
+	};
 }
 
 function data_leave_validation() {
@@ -187,16 +186,16 @@ function data_leave_validation() {
 
 function init_configuration_alert() {
 	window.onsubmit = function (e) {
-			window.hasSubmit = true;
-		};
+		window.hasSubmit = true;
+	};
 	window.onbeforeunload = function (e) {
-			if (window.hasSubmit) {
-				return;
-			}
-			if (!data_leave_validation()) {
-				return false;
-			}
-		};
+		if (window.hasSubmit) {
+			return;
+		}
+		if (!data_leave_validation()) {
+			return false;
+		}
+	};
 }
 
 function init_extra() {
@@ -204,7 +203,7 @@ function init_extra() {
 		if (window.console) {
 			console.log('FreshRSS extra waiting for JS…');
 		}
-		window.setTimeout(init_extra, 50);	//Wait for all js to be loaded
+		window.setTimeout(init_extra, 50);	// Wait for all js to be loaded
 		return;
 	}
 	init_crypto_form();
@@ -219,10 +218,10 @@ if (document.readyState && document.readyState !== 'loading') {
 	init_extra();
 } else {
 	document.addEventListener('DOMContentLoaded', function () {
-			if (window.console) {
-				console.log('FreshRSS extra waiting for DOMContentLoaded…');
-			}
-			init_extra();
-		}, false);
+		if (window.console) {
+			console.log('FreshRSS extra waiting for DOMContentLoaded…');
+		}
+		init_extra();
+	}, false);
 }
 // @license-end

+ 60 - 61
p/scripts/global_view.js

@@ -1,9 +1,8 @@
 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
-"use strict";
+'use strict';
 /* globals context, init_load_more, init_posts, init_stream */
-/* jshint esversion:6, strict:global */
 
-var panel_loading = false;
+let panel_loading = false;
 
 function load_panel(link) {
 	if (panel_loading) {
@@ -16,63 +15,63 @@ function load_panel(link) {
 	req.open('GET', link, true);
 	req.responseType = 'document';
 	req.onload = function (e) {
-			if (this.status != 200) {
-				return;
+		if (this.status != 200) {
+			return;
+		}
+		const html = this.response;
+		const foreign = html.querySelectorAll('.nav_menu, #stream .day, #stream .flux, #stream .pagination, #stream.prompt');
+		const panel = document.getElementById('panel');
+		foreign.forEach(function (el) {
+			panel.appendChild(document.adoptNode(el));
+		});
+		panel.querySelectorAll('.nav_menu > :not([id="nav_menu_read_all"])').forEach(function (el) {
+			el.remove();
+		});
+
+		init_load_more(panel);
+		init_posts();
+
+		document.getElementById('overlay').classList.add('visible');
+		panel.classList.add('visible');
+
+		// force le démarrage du scroll en haut.
+		// Sans ça, si l'on scroll en lisant une catégorie par exemple,
+		// en en ouvrant une autre ensuite, on se retrouve au même point de scroll
+		panel.scrollTop = 0;
+		document.documentElement.scrollTop = 0;
+
+		// We already have a click listener in main.js
+		panel.addEventListener('click', function (ev) {
+			const b = ev.target.closest('#nav_menu_read_all button, #bigMarkAsRead');
+			if (b) {
+				console.log(b.formAction);
+
+				const req2 = new XMLHttpRequest();
+				req2.open('POST', b.formAction, false);
+				req2.setRequestHeader('Content-Type', 'application/json');
+				req2.send(JSON.stringify({
+					_csrf: context.csrf,
+				}));
+				if (req2.status == 200) {
+					location.reload(false);
+					return false;
+				}
 			}
-			const html = this.response,
-				foreign = html.querySelectorAll('.nav_menu, #stream .day, #stream .flux, #stream .pagination, #stream.prompt'),
-				panel = document.getElementById('panel');
-			foreign.forEach(function (el) {
-					panel.appendChild(document.adoptNode(el));
-				});
-			panel.querySelectorAll('.nav_menu > :not([id="nav_menu_read_all"])').forEach(function (el) {
-					el.remove();
-				});
-
-			init_load_more(panel);
-			init_posts();
-
-			document.getElementById('overlay').classList.add('visible');
-			panel.classList.add('visible');
-
-			// force le démarrage du scroll en haut.
-			// Sans ça, si l'on scroll en lisant une catégorie par exemple,
-			// en en ouvrant une autre ensuite, on se retrouve au même point de scroll
-			panel.scrollTop = 0;
-			document.documentElement.scrollTop = 0;
-
-			//We already have a click listener in main.js
-			panel.addEventListener('click', function (ev) {
-					const b = ev.target.closest('#nav_menu_read_all button, #bigMarkAsRead');
-					if (b) {
-						console.log(b.formAction);
-
-						const req2 = new XMLHttpRequest();
-						req2.open('POST', b.formAction, false);
-						req2.setRequestHeader('Content-Type', 'application/json');
-						req2.send(JSON.stringify({
-								_csrf: context.csrf,
-							}));
-						if (req2.status == 200) {
-							location.reload(false);
-							return false;
-						}
-					}
-				});
-
-			panel_loading = false;
-		};
+		});
+
+		panel_loading = false;
+	};
 	req.send();
 }
 
 function init_close_panel() {
 	const panel = document.getElementById('panel');
 	document.querySelector('#overlay .close').onclick = function (ev) {
-			panel.innerHTML = '';
-			panel.classList.remove('visible');
-			document.getElementById('overlay').classList.remove('visible');
-			return false;
-		};
+		panel.innerHTML = '';
+		panel.classList.remove('visible');
+		document.getElementById('overlay').classList.remove('visible');
+		return false;
+	};
 	document.addEventListener('keydown', ev => {
 		const k = (ev.key.trim() || ev.code).toUpperCase();
 		if (k === 'ESCAPE' || k === 'ESC') {
@@ -85,15 +84,15 @@ function init_close_panel() {
 function init_global_view() {
 	// TODO: should be based on generic classes
 	document.querySelectorAll('.box a').forEach(function (a) {
-			a.onclick = function (ev) {
-					load_panel(a.href);
-					return false;
-				};
-		});
+		a.onclick = function (ev) {
+			load_panel(a.href);
+			return false;
+		};
+	});
 
 	document.querySelectorAll('.nav_menu #nav_menu_read_all, .nav_menu .toggle_aside').forEach(function (el) {
-			el.remove();
-		});
+		el.remove();
+	});
 
 	const panel = document.getElementById('panel');
 	init_stream(panel);
@@ -104,7 +103,7 @@ function init_all_global_view() {
 		if (window.console) {
 			console.log('FreshRSS Global view waiting for JS…');
 		}
-		window.setTimeout(init_all_global_view, 50);	//Wait for all js to be loaded
+		window.setTimeout(init_all_global_view, 50);	// Wait for all js to be loaded
 		return;
 	}
 	init_global_view();

+ 4 - 5
p/scripts/install.js

@@ -1,6 +1,5 @@
 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
-"use strict";
-/* jshint esversion:6, strict:global */
+'use strict';
 
 function show_password(ev) {
 	const button = ev.currentTarget;
@@ -13,7 +12,7 @@ function hide_password(ev) {
 	const button = ev.currentTarget;
 	const passwordField = document.getElementById(button.getAttribute('data-toggle'));
 	passwordField.setAttribute('type', 'password');
-	button.className = button.className.replace(/(?:^|\s)active(?!\S)/g , '');
+	button.className = button.className.replace(/(?:^|\s)active(?!\S)/g, '');
 	return false;
 }
 const toggles = document.getElementsByClassName('toggle-password');
@@ -25,8 +24,8 @@ for (let i = 0; i < toggles.length; i++) {
 const auth_type = document.getElementById('auth_type');
 function auth_type_change() {
 	if (auth_type) {
-		const auth_value = auth_type.value,
-			password_input = document.getElementById('passwordPlain');
+		const auth_value = auth_type.value;
+		const password_input = document.getElementById('passwordPlain');
 
 		if (auth_value === 'form') {
 			password_input.required = true;

+ 2 - 3
p/scripts/integration.js

@@ -1,8 +1,7 @@
 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
-"use strict";
-/* jshint esversion:6, strict:global */
+'use strict';
 
-const init_integration = function() {
+const init_integration = function () {
 	if (!window.context) {
 		if (window.console) {
 			console.log('FreshRSS integration waiting for JS…');

Разница между файлами не показана из-за своего большого размера
+ 345 - 340
p/scripts/main.js


+ 5 - 8
p/scripts/preview.js

@@ -1,12 +1,10 @@
 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
-"use strict";
-/* jshint esversion:6, strict:global */
-
-let rendered_node = null,
-	rendered_view = null,
-	raw_node = null,
-	raw_view = null;
+'use strict';
 
+let rendered_node = null;
+let rendered_view = null;
+let raw_node = null;
+let raw_view = null;
 
 function update_ui() {
 	if (rendered_node.checked && !raw_node.checked) {
@@ -29,7 +27,6 @@ function init_afterDOM() {
 	raw_node.addEventListener('click', update_ui);
 }
 
-
 if (document.readyState && document.readyState !== 'loading') {
 	init_afterDOM();
 } else {

+ 46 - 47
p/scripts/statsWithChartjs.js

@@ -1,7 +1,6 @@
 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
-"use strict";
+'use strict';
 /* globals Chart */
-/* jshint esversion:6, strict:global */
 
 function initCharts() {
 	if (!window.Chart) {
@@ -14,13 +13,13 @@ function initCharts() {
 
 	const jsonData = document.getElementsByClassName('jsonData-stats');
 
-	var jsonDataParsed;
-	var chartConfig;
+	let jsonDataParsed;
+	let chartConfig;
 
-	for (var i = 0; i < jsonData.length; i++) {
+	for (let i = 0; i < jsonData.length; i++) {
 		jsonDataParsed = JSON.parse(jsonData[i].innerHTML);
 
-		switch(jsonDataParsed.charttype) {
+		switch (jsonDataParsed.charttype) {
 			case 'bar':
 				chartConfig = jsonChartBar(jsonDataParsed.label, jsonDataParsed.data, jsonDataParsed.xAxisLabels);
 				break;
@@ -28,13 +27,13 @@ function initCharts() {
 				chartConfig = jsonChartDoughnut(jsonDataParsed.labels, jsonDataParsed.data);
 				break;
 			case 'barWithAverage':
-				chartConfig = jsonChartBarWithAvarage(jsonDataParsed.labelBarChart, jsonDataParsed.dataBarChart, jsonDataParsed.labelAverage, jsonDataParsed.dataAverage, jsonDataParsed.xAxisLabels);
+				chartConfig = jsonChartBarWithAvarage(jsonDataParsed.labelBarChart, jsonDataParsed.dataBarChart,
+					jsonDataParsed.labelAverage, jsonDataParsed.dataAverage, jsonDataParsed.xAxisLabels);
 		}
 
-		new Chart(
-			document.getElementById(jsonDataParsed.canvasID),
-			chartConfig
-		);
+		/* eslint-disable no-new */
+		new Chart(document.getElementById(jsonDataParsed.canvasID), chartConfig);
+		/* eslint-enable no-new */
 	}
 
 	if (window.console) {
@@ -55,25 +54,25 @@ function jsonChartBar(label, data, xAxisLabels = '') {
 				barPercentage: 1.0,
 				categoryPercentage: 1.0,
 				order: 2,
-			}]
+			}],
 		},
 		options: {
 			scales: {
 				y: {
-					beginAtZero: true
+					beginAtZero: true,
 				},
 				x: {
 					grid: {
 						display: false,
-					}
-				}
+					},
+				},
 			},
 			plugins: {
 				legend: {
 					display: false,
-				}
-			}
-		}
+				},
+			},
+		},
 	};
 }
 
@@ -84,22 +83,22 @@ function jsonChartDoughnut(labels, data) {
 			labels: labels,
 			datasets: [{
 				backgroundColor: [
-					'#0b84a5',  //petrol
+					'#0b84a5', // petrol
 					'#f6c85f', // sand
-					'#6f4e7c', //purple
-					'#9dd866', //green
-					'#ca472f', //red
-					'#ffa056', //orange
+					'#6f4e7c', // purple
+					'#9dd866', // green
+					'#ca472f', // red
+					'#ffa056', // orange
 					'#8dddd0', // turkis
 					'#f6c85f', // sand
-					'#6f4e7c', //purple
-					'#9dd866', //green
-					'#ca472f', //red
-					'#ffa056', //orange
+					'#6f4e7c', // purple
+					'#9dd866', // green
+					'#ca472f', // red
+					'#ffa056', // orange
 					'#8dddd0', // turkis
 				],
 				data: data,
-			}]
+			}],
 		},
 		options: {
 			layout: {
@@ -109,9 +108,9 @@ function jsonChartDoughnut(labels, data) {
 				legend: {
 					position: 'bottom',
 					align: 'start',
-				}
-			}
-		}
+				},
+			},
+		},
 	};
 }
 
@@ -133,15 +132,15 @@ function jsonChartBarWithAvarage(labelBarChart, dataBarChart, labelAverage, data
 				{
 					// average line chart
 					type: 'line',
-					label: labelAverage,  // Todo: i18n
+					label: labelAverage, // Todo: i18n
 					borderColor: 'rgb(192,216,0)',
 					data: {
-						'-30' : dataAverage,
-						'-1' : dataAverage,
+						'-30': dataAverage,
+						'-1': dataAverage,
 					},
 					order: 1,
-				}
-			]
+				},
+			],
 		},
 
 		options: {
@@ -151,41 +150,41 @@ function jsonChartBarWithAvarage(labelBarChart, dataBarChart, labelAverage, data
 				},
 				x: {
 					ticks: {
-						callback: function(val){
+						callback: function (val) {
 							if (xAxisLabels.length > 0) {
 								return xAxisLabels[val];
 							} else {
 								return val;
 							}
-						}
+						},
 					},
 					grid: {
 						display: false,
-					}
-				}
+					},
+				},
 			},
 			elements: {
 				point: {
 					radius: 0,
-				}
+				},
 			},
 			plugins: {
 				tooltip: {
 					callbacks: {
-						title: function(tooltipitem) {
+						title: function (tooltipitem) {
 							if (xAxisLabels.length > 0) {
 								return xAxisLabels[tooltipitem[0].dataIndex];
 							} else {
 								return tooltipitem[0].label;
 							}
-						}
-					}
+						},
+					},
 				},
 				legend: {
 					display: false,
-				}
-			}
-		}
+				},
+			},
+		},
 	};
 }
 

+ 1 - 1
p/themes/base-theme/template.css

@@ -879,7 +879,7 @@ input[type="search"] {
 }
 
 .subtitle > div:not(:first-of-type)::before {
-	content: ' · ';
+	content: ' · ';
 }
 
 br {

+ 8 - 3
p/themes/base-theme/template.rtl.css

@@ -465,7 +465,7 @@ a.btn {
 
 /*=== Boxes */
 .box {
-	margin: 20px 10px;
+	margin: 20px 0 20px 20px;
 	display: inline-block;
 	max-width: 95%;
 	width: 20rem;
@@ -473,6 +473,11 @@ a.btn {
 	vertical-align: top;
 }
 
+.box.visible-semi {
+	border-style: dashed;
+	opacity: 0.5;
+}
+
 .box .box-title {
 	position: relative;
 	font-size: 1.2rem;
@@ -874,7 +879,7 @@ input[type="search"] {
 }
 
 .subtitle > div:not(:first-of-type)::before {
-	content: ' · ';
+	content: ' · ';
 }
 
 br {
@@ -1114,7 +1119,7 @@ br {
 	display: none;
 	position: fixed;
 	top: 2%; bottom: 2%;
-	left: 3%; right: 3%;
+	right: 3%; left: 3%;
 	overflow: auto;
 }
 

+ 42 - 0
package.json

@@ -0,0 +1,42 @@
+{
+  "name": "freshrss",
+  "description": "A free, self-hostable aggregator",
+  "homepage": "https://freshrss.org/",
+  "readmeFilename": "README.md",
+  "bugs": {
+    "url": "https://github.com/FreshRSS/FreshRSS/issues"
+  },
+  "keywords": [
+    "news",
+    "aggregator",
+    "RSS",
+    "Atom",
+    "WebSub"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/FreshRSS/FreshRSS.git"
+  },
+  "license": "AGPL-3.0",
+  "scripts": {
+    "eslint": "eslint --ext .js .",
+    "eslint_fix": "eslint --fix --ext .js .",
+    "rtlcss": "rtlcss -d p/themes && find . -type f -name '*.rtl.rtl.css' -delete",
+    "stylelint": "stylelint '**/*.css' && stylelint --syntax scss '**/*.scss'",
+    "stylelint_fix": "stylelint --fix '**/*.css' && stylelint --fix --syntax scss '**/*.scss'",
+    "test": "npm run eslint && npm run stylelint",
+    "fix": "npm run rtlcss && npm run stylelint_fix && npm run eslint_fix"
+  },
+  "devDependencies": {
+    "eslint": "^7.32.0",
+    "eslint-config-standard": "^16.0.3",
+    "eslint-plugin-import": "^2.24.2",
+    "eslint-plugin-node": "^11.1.0",
+    "eslint-plugin-promise": "^5.1.0",
+    "rtlcss": "^3.4.0",
+    "stylelint": "^13.13.1",
+    "stylelint-config-recommended-scss": "^4.3.0",
+    "stylelint-order": "^4.1.0",
+    "stylelint-scss": "^3.21.0"
+  }
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов