Kaynağa Gözat

Less jQuery (#2234)

* Less jQuery

Follow-up of https://github.com/FreshRSS/FreshRSS/pull/2199

* Even less jQuery + global view unread title fix

* Even less jQuery

* Yet even less jQuery

* Even less jQuery

* Reduce some events

* Even less jQuery

* jQuery gone from main view

+Fixed English i18n

* Fix feed folded view

* Remove Firefox 64 workaround

Remove workaround for Gecko bug 1514498 in Firefox 64, fixed in Firefox
65

* Split to extra.js

Avoid loading unneeded JavaScript code for the main view.
+ several adjustements

* Improve CSS transition fold category

* Rewrite shortcuts

Remove library. Much faster, shorter, one listener instead of many.
Control of the shortcut context.
Fix https://github.com/FreshRSS/FreshRSS/issues/2215

* Remove debug

* Minor syntax

* Filter out unwanted shortcut modifiers

* Menu overflow fix

* Typo

* Fix unfolding in mobile view

* Remove jQuery from category.js

* Remove jQuery from Global view
Alexandre Alapetite 7 yıl önce
ebeveyn
işleme
2374374ba9

+ 0 - 1
README.fr.md

@@ -210,7 +210,6 @@ Tout client supportant une API de type Fever ; Sélection :
 * [php-http-304](https://alexandre.alapetite.fr/doc-alex/php-http-304/)
 * [jQuery](https://jquery.com/)
 * [lib_opml](https://github.com/marienfressinaud/lib_opml)
-* [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/)
 * [flotr2](http://www.humblesoftware.com/flotr2)
 
 ## Uniquement pour certaines options ou configurations

+ 0 - 1
README.md

@@ -210,7 +210,6 @@ Supported clients are:
 * [php-http-304](https://alexandre.alapetite.fr/doc-alex/php-http-304/)
 * [jQuery](https://jquery.com/)
 * [lib_opml](https://github.com/marienfressinaud/lib_opml)
-* [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/)
 * [flotr2](http://www.humblesoftware.com/flotr2)
 
 ## Only for some options or configurations

+ 1 - 2
app/Controllers/authController.php

@@ -109,8 +109,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 	public function formLoginAction() {
 		invalidateHttpCache();
 
-		$file_mtime = @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js');
-		Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . $file_mtime));
+		Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
 
 		$conf = Minz_Configuration::get('system');
 		$limits = $conf->limits;

+ 1 - 0
app/Controllers/indexController.php

@@ -104,6 +104,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 			return;
 		}
 
+		Minz_View::appendScript(Minz_Url::display('/scripts/extra.js?' . @filemtime(PUBLIC_PATH . '/scripts/extra.js')));
 		Minz_View::appendScript(Minz_Url::display('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
 
 		try {

+ 1 - 0
app/Controllers/statsController.php

@@ -52,6 +52,7 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
 	 */
 	public function indexAction() {
 		$statsDAO = FreshRSS_Factory::createStatsDAO();
+		Minz_View::prependScript(Minz_Url::display('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js')));
 		Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
 		$this->view->repartition = $statsDAO->calculateEntryRepartition();
 		$entryCount = $statsDAO->calculateEntryCount();

+ 1 - 2
app/Controllers/subscriptionController.php

@@ -29,8 +29,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 	 * It displays categories and associated feeds.
 	 */
 	public function indexAction() {
-		Minz_View::appendScript(Minz_Url::display('/scripts/category.js?' .
-		                        @filemtime(PUBLIC_PATH . '/scripts/category.js')));
+		Minz_View::appendScript(Minz_Url::display('/scripts/category.js?' . @filemtime(PUBLIC_PATH . '/scripts/category.js')));
 		Minz_View::prependTitle(_t('sub.title') . ' · ');
 
 		$this->view->onlyFeedsWithError = Minz_Request::paramTernary('error');

+ 1 - 3
app/Controllers/userController.php

@@ -128,9 +128,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	public function profileAction() {
 		Minz_View::prependTitle(_t('conf.profile.title') . ' · ');
 
-		Minz_View::appendScript(Minz_Url::display(
-			'/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')
-		));
+		Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
 
 		if (Minz_Request::isPost()) {
 			$passwordPlain = Minz_Request::param('newPasswordPlain', '', true);

+ 3 - 2
app/FreshRSS.php

@@ -94,9 +94,10 @@ class FreshRSS extends Minz_FrontController {
 			}
 		}
 		//Use prepend to insert before extensions. Added in reverse order.
+		if (Minz_Request::controllerName() !== 'index') {
+			Minz_View::prependScript(Minz_Url::display('/scripts/extra.js?' . @filemtime(PUBLIC_PATH . '/scripts/extra.js')));
+		}
 		Minz_View::prependScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
-		Minz_View::prependScript(Minz_Url::display('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js')));
-		Minz_View::prependScript(Minz_Url::display('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js')));
 	}
 
 	private static function loadNotifications() {

+ 1 - 1
app/i18n/en/conf.php

@@ -92,7 +92,7 @@ return array(
 		'auto_remove_article' => 'Hide articles after reading',
 		'confirm_enabled' => 'Display a confirmation dialog on “mark all as read” actions',
 		'display_articles_unfolded' => 'Show articles unfolded by default',
-		'display_categories_unfolded' => 'Show categories folded by default',
+		'display_categories_unfolded' => 'Show categories unfolded by default',
 		'hide_read_feeds' => 'Hide categories & feeds with no unread articles (does not work with “Show all articles” configuration)',
 		'img_with_lazyload' => 'Use "lazy load" mode to load pictures',
 		'jump_next' => 'jump to next unread sibling (feed or category)',

+ 1 - 1
app/i18n/ru/conf.php

@@ -92,7 +92,7 @@ return array(
 		'auto_remove_article' => 'Hide articles after reading',	//TODO - Translation
 		'confirm_enabled' => 'Display a confirmation dialog on “mark all as read” actions',	//TODO - Translation
 		'display_articles_unfolded' => 'Show articles unfolded by default',	//TODO - Translation
-		'display_categories_unfolded' => 'Show categories folded by default',	//TODO - Translation
+		'display_categories_unfolded' => 'Show categories unfolded by default',	//TODO - Translation
 		'hide_read_feeds' => 'Hide categories & feeds with no unread article (does not work with “Show all articles” configuration)',	//TODO - Translation
 		'img_with_lazyload' => 'Use "lazy load" mode to load pictures',	//TODO - Translation
 		'jump_next' => 'jump to next unread sibling (feed or category)',	//TODO - Translation

+ 1 - 1
app/i18n/tr/conf.php

@@ -92,7 +92,7 @@ return array(
 		'auto_remove_article' => 'Okuduktan sonra makaleleri gizle',
 		'confirm_enabled' => '"Hepsini okundu say" eylemi için onay iste',
 		'display_articles_unfolded' => 'Show articles unfolded by default',
-		'display_categories_unfolded' => 'Show categories folded by default',
+		'display_categories_unfolded' => 'Show categories unfolded by default',
 		'hide_read_feeds' => 'Okunmamış makalesi olmayan kategori veya akışı gizle ("Tüm makaleleri göster" komutunda çalışmaz)',
 		'img_with_lazyload' => 'Resimleri yüklemek için "tembel modu" kullan',
 		'jump_next' => 'Bir sonraki benzer okunmamışa geç (akış veya kategori)',

+ 3 - 3
app/layout/aside_feed.phtml

@@ -37,13 +37,14 @@
 
 		<?php
 			$t_active = FreshRSS_Context::isCurrentGet('T');
+			$t_show = $t_active || FreshRSS_Context::$user_conf->display_categories;
 		?>
 		<li class="tree-folder category tags<?php echo $t_active ? ' active' : ''; ?>">
 			<div class="tree-folder-title">
 				<a class="dropdown-toggle" href="#"><?php echo _i($t_active ? 'up' : 'down'); ?></a>
 				<a class="title" data-unread="<?php echo format_number($this->nbUnreadTags); ?>" href="<?php echo _url('index', $actual_view, 'get', 'T'); ?>"><?php echo _t('index.menu.tags'); ?></a>
 			</div>
-			<ul class="tree-folder-items<?php echo $t_active ? ' active' : ''; ?>">
+			<ul class="tree-folder-items<?php echo $t_show ? ' active' : ''; ?>">
 				<?php
 					foreach ($this->tags as $tag):
 				?>
@@ -64,8 +65,7 @@
 				$feeds = $cat->feeds();
 				if (!empty($feeds)) {
 					$c_active = FreshRSS_Context::isCurrentGet('c_' . $cat->id());
-					$c_show = $c_active && (!FreshRSS_Context::$user_conf->display_categories ||
-					                        FreshRSS_Context::$current_get['feed']);
+					$c_show = $c_active || FreshRSS_Context::$user_conf->display_categories;
 		?>
 		<li class="tree-folder category<?php echo $c_active ? ' active' : ''; ?>" data-unread="<?php echo $cat->nbNotRead(); ?>">
 			<div class="tree-folder-title">

+ 1 - 1
app/views/helpers/index/normal/entry_bottom.phtml

@@ -93,7 +93,7 @@
 						<a target="_blank" rel="noreferrer" href="<?php echo $share->url(); ?>"><?php echo $share->name(); ?></a>
 					<?php } else {?>
 						<a href="POST"><?php echo $share->name(); ?></a>
-						<form method="POST" data-url="<?php echo $share->url(); ?>">
+						<form method="POST" action="<?php echo $share->url(); ?>" disabled="disabled">
 							<input type="hidden" value="<?php echo $link; ?>" name="<?php echo $share->field(); ?>"/>
 						</form>
 					<?php } ?>

+ 1 - 1
app/views/index/reader.phtml

@@ -42,7 +42,7 @@ if (!empty($this->entries)) {
 				<a class="bookmark" href="<?php echo Minz_Url::display($favoriteUrl); ?>">
 					<?php echo _i($item->isFavorite() ? 'starred' : 'non-starred'); ?>
 				</a>
-				<a href="<?php echo _url('index', 'reader', 'get', 'f_' . $feed->id()); ?>">
+				<a class="website" href="<?php echo _url('index', 'reader', 'get', 'f_' . $feed->id()); ?>">
 					<img class="favicon" src="<?php echo $feed->favicon(); ?>" alt="✇" /> <span><?php echo $feed->name(); ?></span>
 				</a>
 				<h1 class="title"><a target="_blank" rel="noreferrer" class="go_website" href="<?php echo $item->link(); ?>"><?php echo $item->title(); ?></a></h1>

+ 25 - 0
lib/Minz/Request.php

@@ -95,6 +95,7 @@ class Minz_Request {
 	 */
 	public static function init() {
 		self::magicQuotesOff();
+		self::initJSON();
 	}
 
 	/**
@@ -237,6 +238,30 @@ class Minz_Request {
 		}
 	}
 
+	/**
+	 * Allows receiving POST data as application/json
+	 */
+	private static function initJSON() {
+		$contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '';
+		if ($contentType == '') {	//PHP < 5.3.16
+			$contentType = isset($_SERVER['HTTP_CONTENT_TYPE']) ? $_SERVER['HTTP_CONTENT_TYPE'] : '';
+		}
+		$contentType = strtolower(trim($contentType));
+		if ($contentType === 'application/json') {
+			$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576);
+			if ($ORIGINAL_INPUT != '') {
+				$json = json_decode($ORIGINAL_INPUT, true);
+				if ($json != null) {
+					foreach ($json as $k => $v) {
+						if (!isset($_POST[$k])) {
+							$_POST[$k] = $v;
+						}
+					}
+				}
+			}
+		}
+	}
+
 	/**
 	 * Permet de récupérer une variable de type $_POST
 	 * @param $param nom de la variable

+ 99 - 79
p/scripts/category.js

@@ -1,6 +1,6 @@
 "use strict";
-/* globals i18n */
-/* jshint globalstrict: true */
+/* globals context */
+/* jshint esversion:6, strict:global */
 
 var loading = false,
 	dnd_successful = false;
@@ -9,7 +9,7 @@ function dragend_process(t) {
 	t.setAttribute('draggable', 'false');
 
 	if (loading) {
-		window.setTimeout(function() {
+		setTimeout(function() {
 			dragend_process(t);
 		}, 50);
 		return;
@@ -20,11 +20,11 @@ function dragend_process(t) {
 		t.style.opacity = '';
 		t.setAttribute('draggable', 'true');
 	} else {
-		var parent = $(t.parentNode);
-		$(t).remove();
+		const p = t.parentElement;
+		t.remove();
 
-		if (parent.children().length <= 0) {
-			parent.append('<li class="item disabled" dropzone="move">' + i18n.category_empty + '</li>');
+		if (p.childElementCount <= 0) {
+			p.insertAdjacentHTML('beforeend', '<li class="item disabled" dropzone="move">' + context.i18n.category_empty + '</li>');
 		}
 	}
 }
@@ -33,89 +33,109 @@ var dragFeedId = '',
 	dragHtml = '';
 
 function init_draggable() {
-	if (!(window.$ && window.i18n)) {
+	if (!window.context) {
 		if (window.console) {
-			console.log('FreshRSS waiting for JS…');
+			console.log('FreshRSS category waiting for JS…');
 		}
-		window.setTimeout(init_draggable, 50);
+		setTimeout(init_draggable, 50);
 		return;
 	}
 
-	var draggable = '[draggable="true"]',
-	    dropzone = '[dropzone="move"]';
-
-	$('.drop-section').on('dragstart', draggable, function(e) {
-		var drag = $(e.target).closest('[draggable]')[0];
-		e.originalEvent.dataTransfer.effectAllowed = 'move';
-		dragHtml = drag.outerHTML;
-		dragFeedId = drag.getAttribute('data-feed-id');
-		e.originalEvent.dataTransfer.setData('text', dragFeedId);
-		drag.style.opacity = 0.3;
-
-		dnd_successful = false;
-	});
-	$('.drop-section').on('dragend', draggable, function(e) {
-		dragend_process(e.target);
-	});
-
-	$('.drop-section').on('dragenter', dropzone, function(e) {
-		$(this).addClass('drag-hover');
-
-		e.preventDefault();
-	});
-	$('.drop-section').on('dragleave', dropzone, function(e) {
-		var pos_this = $(this).position(),
-		    scroll_top = $(document).scrollTop(),
-		    top = pos_this.top,
-		    left = pos_this.left,
-		    right = left + $(this).width(),
-		    bottom = top + $(this).height(),
-		    mouse_x = e.originalEvent.screenX,
-		    mouse_y = e.originalEvent.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;
-		}
-		$(this).removeClass('drag-hover');
-	});
-	$('.drop-section').on('dragover', dropzone, function(e) {
-		e.originalEvent.dataTransfer.dropEffect = "move";
-
-		e.preventDefault();
-		return false;
-	});
-	$('.drop-section').on('drop', dropzone, function(e) {
-		loading = true;
-
-		$.ajax({
-			type: 'POST',
-			url: './?c=feed&a=move',
-			data: {
-				f_id: dragFeedId,
-				c_id: e.target.parentNode.getAttribute('data-cat-id'),
-				_csrf: context.csrf,
+	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;
 			}
-		}).done(function() {
-			$(e.target).after(dragHtml);
-			if ($(e.target).hasClass('disabled')) {
-				$(e.target).remove();
+		};
+
+	dropSection.ondragend = function(ev) {
+			const li = ev.target.closest ? ev.target.closest(draggable) : null;
+			if (li) {
+				dragend_process(li);
 			}
-			dnd_successful = true;
-		}).always(function() {
-			loading = false;
-			dragFeedId = '';
-			dragHtml = '';
-		});
+		};
 
-		$(this).removeClass('drag-hover');
+	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 &&
+					top <= mouse_y && mouse_y <= bottom) {
+					// HACK because dragleave is triggered when hovering children!
+					return;
+				}
+				li.classList.remove('drag-hover');
+			}
+		};
 
-		e.preventDefault();
-	});
+	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;
+			}
+		};
 }
 
-
 if (document.readyState && document.readyState !== 'loading') {
 	init_draggable();
 } else if (document.addEventListener) {

+ 235 - 0
p/scripts/extra.js

@@ -0,0 +1,235 @@
+"use strict";
+/* globals context, openNotification, xmlHttpRequestJson */
+/* jshint esversion:6, strict:global */
+
+//<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--) {
+		text += base.charAt(Math.floor(Math.random() * 64));
+	}
+	return text;
+}
+
+function init_crypto_form() {
+	/* globals dcodeIO */
+	const crypto_form = document.getElementById('crypto-form');
+	if (!crypto_form) {
+		return;
+	}
+
+	if (!(window.dcodeIO)) {
+		if (window.console) {
+			console.log('FreshRSS waiting for bcrypt.js…');
+		}
+		setTimeout(init_crypto_form, 100);
+		return;
+	}
+
+	crypto_form.onsubmit = function (e) {
+		const submit_button = this.querySelector('button[type="submit"]');
+		submit_button.disabled = true;
+		let success = false;
+
+		const req = new XMLHttpRequest();
+		req.open('GET', './?c=javascript&a=nonce&user=' + document.getElementById('username').value, false);
+		req.onerror = function () {
+				openNotification('Communication error!', 'bad');
+			};
+		req.send();
+		if (req.status == 200) {
+			const json = xmlHttpRequestJson(req);
+			if (!json.salt1 || !json.nonce) {
+				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());
+					document.getElementById('challenge').value = c;
+					if (!s || !c) {
+						openNotification('Crypto error!', 'bad');
+					} else {
+						success = true;
+					}
+				} catch (ex) {
+					openNotification('Crypto exception! ' + ex, 'bad');
+				}
+			}
+		} else {
+			req.onerror();
+		}
+
+		submit_button.disabled = false;
+		return success;
+	};
+}
+//</crypto form (Web login)>
+
+function init_share_observers() {
+	let shares = document.querySelectorAll('.group-share').length;
+	const shareAdd = document.querySelector('.share.add');
+	if (shareAdd) {
+		shareAdd.onclick = function (ev) {
+				const s = this.parentElement.querySelector('select'),
+					opt = s.options[s.selectedIndex];
+				let row = this.closest('form').getAttribute('data-' + opt.getAttribute('data-form'));
+				row = row.replace(/##label##/g, opt.text);
+				row = row.replace(/##type##/g, opt.value);
+				row = row.replace(/##help##/g, opt.getAttribute('data-help'));
+				row = row.replace(/##key##/g, shares);
+				row = row.replace(/##method##/g, opt.getAttribute('data-method'));
+				row = row.replace(/##field##/g, opt.getAttribute('data-field'));
+				this.closest('.form-group').insertAdjacentHTML('beforebegin', row);
+				shares++;
+				return false;
+			};
+	}
+}
+
+
+function init_remove_observers() {
+	document.querySelectorAll('.post').forEach(function (div) {
+			div.onclick = function (ev) {
+					const a = ev.target.closest('a.remove');
+					if (a) {
+						const remove_what = a.getAttribute('data-remove');
+						if (remove_what !== undefined) {
+							const d = document.getElementById(remove_what);
+							if (d) {
+								d.remove();
+							}
+						}
+						return false;
+					}
+				};
+		});
+}
+
+function init_feed_observers() {
+	const s = document.getElementById('category');
+	if (s && s.matches('select')) {
+		s.onchange = function (ev) {
+				const detail = document.getElementById('new_category_name').parentElement;
+				if (this.value === 'nc') {
+					detail.setAttribute('aria-hidden', 'false');
+					detail.querySelector('input').focus();
+				} else {
+					detail.setAttribute('aria-hidden', 'true');
+				}
+			};
+	}
+}
+
+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;
+				};
+		});
+}
+
+function init_select_observers() {
+	document.querySelectorAll('.select-change').forEach(function (s) {
+			s.onchange = function (ev) {
+					const opt = s.options[s.selectedIndex];
+					location.href = opt.getAttribute('data-url');
+				};
+		});
+}
+
+function init_slider_observers() {
+	const slider = document.getElementById('slider'),
+		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;
+						};
+					req.send();
+					return false;
+				}
+			}
+		};
+
+	closer.onclick = function (ev) {
+			closer.classList.remove('active');
+			slider.classList.remove('active');
+			return false;
+		};
+}
+
+function init_configuration_alert() {
+	window.onsubmit = function (e) {
+			window.hasSubmit = true;
+		};
+	window.onbeforeunload = function (e) {
+			if (window.hasSubmit) {
+				return;
+			}
+			const ds = document.querySelectorAll('[data-leave-validation]');
+			for (let i = ds.length - 1; i >= 0; i--) {
+				const input = ds[i];
+				if (input.type === 'checkbox' || input.type === 'radio') {
+					if (input.checked != input.getAttribute('data-leave-validation')) {
+						return false;
+					}
+				} else if (input.value != input.getAttribute('data-leave-validation')) {
+					return false;
+				}
+			}
+		};
+}
+
+function init_extra() {
+	if (!window.context) {
+		if (window.console) {
+			console.log('FreshRSS extra waiting for JS…');
+		}
+		window.setTimeout(init_extra, 50);	//Wait for all js to be loaded
+		return;
+	}
+	init_crypto_form();
+	init_share_observers();
+	init_remove_observers();
+	init_feed_observers();
+	init_password_observers();
+	init_select_observers();
+	init_slider_observers();
+	init_configuration_alert();
+}
+
+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);
+}

+ 74 - 54
p/scripts/global_view.js

@@ -1,6 +1,6 @@
 "use strict";
-/* globals init_load_more, init_posts, init_stream */
-/* jshint globalstrict: true */
+/* globals context, init_load_more, init_posts, init_stream */
+/* jshint esversion:6, strict:global */
 
 var panel_loading = false;
 
@@ -11,68 +11,88 @@ function load_panel(link) {
 
 	panel_loading = true;
 
-	$.get(link, function (data) {
-		$("#panel").append($(".nav_menu, #stream .day, #stream .flux, #stream .pagination, #stream.prompt", data));
-
-		$("#panel .nav_menu").children().not("#nav_menu_read_all").remove();
-
-		init_load_more($("#panel"));
-		init_posts();
-
-		$("#overlay").fadeIn();
-		$("#panel").slideToggle();
-
-		// 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);
-		$(window).scrollTop(0);
-
-		$('#panel').on('click', '#nav_menu_read_all button, #bigMarkAsRead', function () {
-			console.log($(this).attr("formaction"));
-			$.ajax({
-				type: "POST",
-				url: $(this).attr("formaction"),
-				data: {
-					_csrf: context.csrf,
-				},
-				async: false
-			});
-			window.location.reload(false);
-			return false;
-		});
-
-		panel_loading = false;
-	});
+	const req = new XMLHttpRequest();
+	req.open('GET', link, true);
+	req.responseType = 'document';
+	req.onload = function (e) {
+			if (this.status != 200) {
+				return;
+			}
+			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;
+		};
+	req.send();
 }
 
 function init_close_panel() {
-	$("#overlay .close").click(function () {
-		$("#panel").html('');
-		$("#panel").slideToggle();
-		$("#overlay").fadeOut();
-
-		return false;
-	});
+	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;
+		};
 }
 
 function init_global_view() {
-	// TODO: should be based on generic classes.
-	$(".box a").click(function () {
-		var link = $(this).attr("href");
-
-		load_panel(link);
-
-		return false;
-	});
+	// TODO: should be based on generic classes
+	document.querySelectorAll('.box a').forEach(function (a) {
+			a.onclick = function (ev) {
+					load_panel(a.href);
+					return false;
+				};
+		});
 
-	$(".nav_menu #nav_menu_read_all, .nav_menu .toggle_aside").remove();
+	document.querySelectorAll('.nav_menu #nav_menu_read_all, .nav_menu .toggle_aside').forEach(function (el) {
+			el.remove();
+		});
 
-	init_stream($("#panel"));
+	const panel = document.getElementById('panel');
+	init_stream(panel);
 }
 
 function init_all_global_view() {
-	if (!(window.$ && window.init_stream)) {
+	if (!window.context) {
 		if (window.console) {
 			console.log('FreshRSS Global view waiting for JS…');
 		}
@@ -85,7 +105,7 @@ function init_all_global_view() {
 
 if (document.readyState && document.readyState !== 'loading') {
 	init_all_global_view();
-} else if (document.addEventListener) {
+} else {
 	document.addEventListener('DOMContentLoaded', function () {
 		init_all_global_view();
 	}, false);

Dosya farkı çok büyük olduğundan ihmal edildi
+ 491 - 474
p/scripts/main.js


+ 2 - 2
p/scripts/repartition.js

@@ -1,6 +1,6 @@
 "use strict";
 /* globals Flotr, numberFormat */
-/* jshint globalstrict: true */
+/* jshint esversion:6, strict:global */
 
 function initStats() {
 	if (!window.Flotr) {
@@ -10,7 +10,7 @@ function initStats() {
 		window.setTimeout(initStats, 50);
 		return;
 	}
-	var jsonRepartition = document.getElementById('jsonRepartition'),
+	const jsonRepartition = document.getElementById('jsonRepartition'),
 		stats = JSON.parse(jsonRepartition.innerHTML);
 	jsonRepartition.outerHTML = '';
 	// Entry per hour

+ 0 - 225
p/scripts/shortcut.js

@@ -1,225 +0,0 @@
-/**
- * http://www.openjs.com/scripts/events/keyboard_shortcuts/
- * Version : 2.01.B
- * By Binny V A
- * License : BSD
- */
-shortcut = {
-	'all_shortcuts':{},//All the shortcuts are stored in this array
-	'add': function(shortcut_combination,callback,opt) {
-		//Provide a set of default options
-		var default_options = {
-			'type':'keydown',
-			'propagate':false,
-			'disable_in_input':false,
-			'target':document,
-			'keycode':false
-		}
-		if(!opt) opt = default_options;
-		else {
-			for(var dfo in default_options) {
-				if(typeof opt[dfo] == 'undefined') opt[dfo] = default_options[dfo];
-			}
-		}
-
-		var ele = opt.target;
-		if(typeof opt.target == 'string') ele = document.getElementById(opt.target);
-		var ths = this;
-		shortcut_combination = shortcut_combination.toLowerCase();
-
-		//The function to be called at keypress
-		var func = function(e) {
-			e = e || window.event;
-			
-			if(opt['disable_in_input']) { //Don't enable shortcut keys in Input, Textarea fields
-				var element;
-				if(e.target) element=e.target;
-				else if(e.srcElement) element=e.srcElement;
-				if(element.nodeType==3) element=element.parentNode;
-
-				if(element.tagName == 'INPUT' || element.tagName == 'TEXTAREA') return;
-			}
-	
-			//Find Which key is pressed
-			if (e.keyCode) code = e.keyCode;
-			else if (e.which) code = e.which;
-			if (code == 32 || (code >= 48 && code <= 90) || (code >= 96 && code <= 111) || (code >= 186 && code <= 192) || (code >= 219 && code <= 222)) {	//FreshRSS
-				var character = String.fromCharCode(code).toLowerCase();
-			}
-			
-			if(code == 188) character=","; //If the user presses , when the type is onkeydown
-			if(code == 190) character="."; //If the user presses , when the type is onkeydown
-
-			var keys = shortcut_combination.split("+");
-			//Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked
-			var kp = 0;
-			
-			//Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken
-			var shift_nums = {
-				"`":"~",
-				"1":"!",
-				"2":"@",
-				"3":"#",
-				"4":"$",
-				"5":"%",
-				"6":"^",
-				"7":"&",
-				"8":"*",
-				"9":"(",
-				"0":")",
-				"-":"_",
-				"=":"+",
-				";":":",
-				"'":"\"",
-				",":"<",
-				".":">",
-				"/":"?",
-				"\\":"|"
-			}
-			//Special Keys - and their codes
-			var special_keys = {
-				'esc':27,
-				'escape':27,
-				'tab':9,
-				'space':32,
-				'return':13,
-				'enter':13,
-				'backspace':8,
-	
-				'scrolllock':145,
-				'scroll_lock':145,
-				'scroll':145,
-				'capslock':20,
-				'caps_lock':20,
-				'caps':20,
-				'numlock':144,
-				'num_lock':144,
-				'num':144,
-				
-				'pause':19,
-				'break':19,
-				
-				'insert':45,
-				'home':36,
-				'delete':46,
-				'end':35,
-				
-				'pageup':33,
-				'page_up':33,
-				'pu':33,
-	
-				'pagedown':34,
-				'page_down':34,
-				'pd':34,
-	
-				'left':37,
-				'up':38,
-				'right':39,
-				'down':40,
-	
-				'f1':112,
-				'f2':113,
-				'f3':114,
-				'f4':115,
-				'f5':116,
-				'f6':117,
-				'f7':118,
-				'f8':119,
-				'f9':120,
-				'f10':121,
-				'f11':122,
-				'f12':123
-			}
-	
-			var modifiers = { 
-				shift: { wanted:false, pressed:false},
-				ctrl : { wanted:false, pressed:false},
-				alt  : { wanted:false, pressed:false},
-				meta : { wanted:false, pressed:false}	//Meta is Mac specific
-			};
-                        
-			if(e.ctrlKey)	modifiers.ctrl.pressed = true;
-			if(e.shiftKey)	modifiers.shift.pressed = true;
-			if(e.altKey)	modifiers.alt.pressed = true;
-			if(e.metaKey)   modifiers.meta.pressed = true;
-                        
-			for(var i=0; k=keys[i],i<keys.length; i++) {
-				//Modifiers
-				if(k == 'ctrl' || k == 'control') {
-					kp++;
-					modifiers.ctrl.wanted = true;
-
-				} else if(k == 'shift') {
-					kp++;
-					modifiers.shift.wanted = true;
-
-				} else if(k == 'alt') {
-					kp++;
-					modifiers.alt.wanted = true;
-				} else if(k == 'meta') {
-					kp++;
-					modifiers.meta.wanted = true;
-				} else if(k.length > 1) { //If it is a special key
-					if(special_keys[k] == code) kp++;
-					
-				} else if(opt['keycode']) {
-					if(opt['keycode'] == code) kp++;
-
-				} else { //The special keys did not match
-					if(character == k) kp++;
-					else {
-						if(shift_nums[character] && e.shiftKey) { //Stupid Shift key bug created by using lowercase
-							character = shift_nums[character]; 
-							if(character == k) kp++;
-						}
-					}
-				}
-			}
-			
-			if(kp == keys.length && 
-						modifiers.ctrl.pressed == modifiers.ctrl.wanted &&
-						modifiers.shift.pressed == modifiers.shift.wanted &&
-						modifiers.alt.pressed == modifiers.alt.wanted &&
-						modifiers.meta.pressed == modifiers.meta.wanted) {
-				callback(e);
-	
-				if(!opt['propagate']) { //Stop the event
-					//e.cancelBubble is supported by IE - this will kill the bubbling process.
-					e.cancelBubble = true;
-					e.returnValue = false;
-	
-					//e.stopPropagation works in Firefox.
-					if (e.stopPropagation) {
-						e.stopPropagation();
-						e.preventDefault();
-					}
-					return false;
-				}
-			}
-		}
-		this.all_shortcuts[shortcut_combination] = {
-			'callback':func, 
-			'target':ele, 
-			'event': opt['type']
-		};
-		//Attach the function with the event
-		if(ele.addEventListener) ele.addEventListener(opt['type'], func, false);
-		else if(ele.attachEvent) ele.attachEvent('on'+opt['type'], func);
-		else ele['on'+opt['type']] = func;
-	},
-
-	//Remove the shortcut - just specify the shortcut and I will remove the binding
-	'remove':function(shortcut_combination) {
-		shortcut_combination = shortcut_combination.toLowerCase();
-		var binding = this.all_shortcuts[shortcut_combination];
-		delete(this.all_shortcuts[shortcut_combination])
-		if(!binding) return;
-		var type = binding['event'];
-		var ele = binding['target'];
-		var callback = binding['callback'];
-
-		if(ele.detachEvent) ele.detachEvent('on'+type, callback);
-		else if(ele.removeEventListener) ele.removeEventListener(type, callback, false);
-		else ele['on'+type] = false;
-	}
-}

+ 4 - 4
p/scripts/stats.js

@@ -1,6 +1,6 @@
 "use strict";
 /* globals Flotr, numberFormat */
-/* jshint globalstrict: true */
+/* jshint esversion:6, strict:global */
 
 function initStats() {
 	if (!window.Flotr) {
@@ -10,12 +10,12 @@ function initStats() {
 		window.setTimeout(initStats, 50);
 		return;
 	}
-	var jsonStats = document.getElementById('jsonStats'),
+	const jsonStats = document.getElementById('jsonStats'),
 		stats = JSON.parse(jsonStats.innerHTML);
 	jsonStats.outerHTML = '';
 	// Entry per day
-	var avg = [];
-	for (var i = -31; i <= 0; i++) {
+	const avg = [];
+	for (let i = -31; i <= 0; i++) {
 		avg.push([i, stats.average]);
 	}
 	Flotr.draw(document.getElementById('statsEntryPerDay'),

+ 31 - 9
p/themes/base-theme/template.css

@@ -280,7 +280,7 @@ a.btn {
 	left: 0; right: 0;
 	display: block;
 	z-index: -10;
-    cursor: default;
+	cursor: default;
 }
 .separator {
 	display: block;
@@ -418,8 +418,10 @@ a.btn {
 }
 
 .tree-folder-items {
-	padding: 0;
 	list-style: none;
+	max-height: 200em;
+	padding: 0;
+	transition: max-height .3s linear;
 }
 .tree-folder-title {
 	display: block;
@@ -502,7 +504,8 @@ a.btn {
 	padding: 0px 15px;
 }
 .aside_feed .tree-folder-items:not(.active) {
-	display: none;
+	max-height: 0;
+	overflow: hidden;
 }
 .aside_feed .tree-folder-items .dropdown {
 	vertical-align: top;
@@ -632,9 +635,13 @@ br + br + br {
 	z-index: 10;
 	background: #fff;
 	border: 1px solid #aaa;
+	opacity: 1;
+	visibility: visible;
+	transition: visibility 0s, opacity .3s linear;
 }
 .notification.closed {
-	display: none;
+	opacity: 0;
+	visibility: hidden;
 }
 .notification a.close {
 	position: absolute;
@@ -710,15 +717,15 @@ br + br + br {
 /*=== LOGIN VIEW */
 /*================*/
 .formLogin .header > .item {
-        padding: 10px 30px;
+	padding: 10px 30px;
 }
 
 .formLogin .header > .item.title {
-        text-align: left;
+	text-align: left;
 }
 
 .formLogin .header > .item.configure {
-        text-align: right;
+	text-align: right;
 }
 
 
@@ -731,14 +738,29 @@ br + br + br {
 #stream.global .box {
 	text-align: left;
 }
-
+#global > #panel {
+	bottom: 99vh;
+	display: block;
+	transition: visibility .3s, bottom .3s;
+	visibility: hidden;
+}
+#global > #panel.visible {
+	bottom: 1em;
+	visibility: visible;
+}
 /*=== Panel */
 #overlay {
-	display: none;
 	position: fixed;
 	top: 0; bottom: 0;
 	left: 0; right: 0;
 	background: rgba(0, 0, 0, 0.9);
+	opacity: 0;
+	transition: visibility .3s, opacity .3s;
+	visibility: hidden;
+}
+#overlay.visible {
+	opacity: 1;
+	visibility: visible;
 }
 #panel {
 	display: none;

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor