Jump to content

modifiedcontent

Members
  • Posts

    279
  • Joined

  • Last visited

Posts posted by modifiedcontent

  1. @BrendonKoz, when I started this thread I was not sure if PHP had anything to do with it, but looking into it further it became clear that it had. So I started another thread - sorry... - where I found the solution:

    PHP upgrade via EasyApache doesn't include the Apache mod_suexec module by default. Without that module 'the system executes PHP applications as the nobody system user' instead of your myusername. 

    'If you install the suEXEC module, the system executes PHP applications as the user that owns the VirtualHost that served the request.' I guess these modules were introduced somewhere between recent PHP versions?

    Enabling the mod_suexec module, included in EasyApache4, should fix most problems. There is also another Apache module mod_suphp that may be required to fix issues with some scripts.

    • Like 1
  2. Edit: Solution is at the end of this comment, after a few educational dead ends. 😉

    @gebeerI use EasyApache4; just standardized automated upgrade/changes on a VPS with a few PW websites that have been running fine for years. I have not improvised anything, not made any manual configuration changes.

    Of course there is a misconfiguration somewhere. I'm trying to find out where and created this thread to find out if anyone had seen something similar after PHP config changes. Here's one via a Google search - also uses CentOS, but with Apache vs Nginx issue, so problably not relevant. 

    Thanks for the file permissions link. I'll see if I can fix my problem with that, but they're not clearcut instructions what ownership and permissions each folder/file should have. I see my Apache now runs as 'nobody'; I think it used to be/should be 'myusername'? My PW sites were installed as 'myusername' and all folders/files are owned by that 'myusername'. 

    Edit: EasyApache apparently "updates" user settings or PHP handlers (?) and you have to turn that off? Here is a possible way to make Apache run as 'username' instead of 'nobody', I think, from 2012, so not risking that one...

    WHM has Apache mod_userdir Tweak where only 'DefaultHost (nobody)' has 'Exclude Protection' selected and all my other hosts/website have 'myusername'. No clue what to do with that info. Can I change DefaultHost to 'myusername' anywhere?

    There's this file on my CentOS server:

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #
    #   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    #   DO NOT EDIT. AUTOMATICALLY GENERATED.  USE INCLUDE FILES IF YOU NEED TO MAKE A CHANGE
    #   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    #
    #   Direct modifications to the Apache configuration file WILL be lost upon subsequent
    #   regeneration of this configuration file, or an Apache update.
    #
    #   To have your modifications retained, you should create/edit administrator-specific
    #   include files:
    #
    #       /etc/apache2/conf.d/includes/pre_main_global.conf
    #       /etc/apache2/conf.d/includes/pre_virtualhost_global.conf
    #       /etc/apache2/conf.d/includes/post_virtualhost_global.conf
    
    ...
    
    # These are hard-coded values that are required by cPanel & WHM
    PidFile /run/apache2/httpd.pid
    User nobody
    Group nobody
    
    ...

    Tried changing 'nobody' to 'myusername' in pre_main_global.conf, empty file with only this:

    User myusername
    Group myusername

    And then run these two commands from root in SSH to rebuild httpd.conf and restart Apache:

    /usr/local/cpanel/scripts/rebuildhttpdconf

    /usr/local/cpanel/scripts/restartsrv_httpd

    One site that was giving me nasty 500 server errors is suddenly back, but that could also be because I changed permissions on all folders to 777 - bad for security... <?php echo exec('whoami'); ?> is still giving me 'nobody' as Apache user. Apache mod_userdir Tweak in WHM still has 'DefaultHost (nobody)'.

    Another clue here: 'If you install the suEXEC module, the system executes PHP applications as the user that owns the VirtualHost that served the request. If you uninstall the suEXEC module, the system executes PHP applications as the nobody system user.'

    mod_suexec is an Apache Module that can be enabled in EasyApache4. Enabling that seems to fix most issues I had. <?php echo exec('whoami'); ?> now give the correct 'myusername'.

    Edit: There is also another Apache Module mod_suphp that may be required to fix issues with some scripts.

    • Like 1
  3. Quote

    Posting 4 different threads about 1 problem does not help and could be considered as spam.

    Bullshit. I don't know if this is 1 problem; there may be several completely unrelated things going on and I am trying to untangle them. I create threads when I try to solve distinct issues and mostly to keep track of my own findings. I had already acknowledged that my problem may be caused by something dumb in my script, but that is not what this thread is about. 

    This thread was about the proper PW way of doing PHP sessions, if there are any differences with regular PHP sessions to watch out for, how and where in PW sessions are stored, etc. @bernhard@BitPoet@Jan Romero had already answered most questions.

    The fact that my /site/assets/sessions/ folder is empty and sessions process suddenly had gone missing seems to point to other issues as far as I understand now, so I started another thread about that - or is /site/assets/sessions/ supposed to be empty?  

  4. After upgrading to PHP 8 I now get errors like this in one of my sites:

    Unable to load Modules
    Unable to create directory /home/myusername/public_html/mywebsite/site/assets/cache/FileCompiler/site/modules/JquerySelectize/
    
    ...and trying to clear caches:
    
    Fatal Error: Uncaught Error: Class "CacheControlTools" not found in site/modules/ProcessCacheControl/ProcessCacheControl.module:284

    Other sites were mostly fine now, including Cache Control, but earlier I've had several other issues with suddenly unreachable folders here and here and here after making PHP and Apache changes via EasyApache. 

    Are these known issues? Does anyone recognize them? How can I fix them? 

    I've tried triple-checking/resetting folder ownership/permissions, but can't find a definitive guide what those settings should be and am probably only making things worse. 

     

  5. Is there a way to do a clean reinstall and keep your current content; database and templates configuration?

    It is probably not as simple as simply backing up the /site folder and the database and uploading/importing those after a fresh reinstall or is it?

    Process Sessions module is missing from my current installation. I have no idea how that happened. It may or may not have something to do with wrong ownership/permissions on folders, that may or may not have been caused by PHP and Apache config changes. If there is another way to fix that, let me know as well, but I'm curious to find out if a full clean install is possible. 

  6. Trying to install the Session Handler Database module, I get this error that probably explains my problem 🙂 : 

    Your PHP has a configuration error with regard to sessions. It is configured to never clean up old session files. Please correct this by adding the following to your /site/config.php file: ini_set('session.gc_probability', 1); 

    Unfortunately just adding that to config does not fix my current session script, although the error message on the module is gone.

    Where are 'old session files' supposed to be stored? I still see nothing in my /site/assets/sessions/ folder. What are the correct ownership and permission settings for that folder? 

    I have installed the Session Handler Database module, but see no change. Am I supposed to adapt my session script to it? 

    Under Access in the admin area I now see this:

    SESSIONS
    The process module assigned to this page does not appear to be installed.
    
    
    and:
    
    
    Sessions
    This page has no process assigned.

    How did that happen? How can I fix it?

  7. I now get this error: 'destinationPath is not writable - Images'

    I haven't changed anything on this site; I have only done some EasyApache stuff on the VPS server, upgrading to PHP8 and back to 7.4 etc.

    Why do I get these errrors?

    What are the correct permissions for the for /site/assets etc.?

    What are the correct ownership settings for /site/assets etc.?  

    Edit: Doing this "fixed" the problem, but now I get "Fatal Error: Uncaught Error: Class 'SimplePie\SimplePie' not found in site/templates/simplepie/library/SimplePie" - I absolutely have SimplePie in that folder, so probably another permission and/or ownership error. What should the correct setting be and how are they getting messed up?

  8. Thanks @Jan Romero and @BitPoet

    My /site/assets/sessions/ folder is empty, except for a .htaccess file. Should there be something there? Do I still have write problems on those folders? I had already triple-checked permission/ownership - what are the correct settings?

    Sessions worked fine when I hardcoded the session variable in the script, so probably not related either way. I'll try other things tonight or next weekend and report back...

    Quote

    ... maybe you’re overwriting the value before reading it on the second page ...

    Yes, probably... 😕

    Edit:

    I'm still stuck with this. The session variable value event_number is set correctly on homepage, but then reverts to a previous value on the next page. I don't see where it could be overwritten in between - echoing the variable at the end of page 1, top of page 2. 

    Where are PW php sessions stored? On the client machine? In "memory" somewhere? Cookie? Should I see files in the /site/assets/sessions/ folder? Could register_globals interfere with how sessions are stored?It does work when I hard-code the event_number value, so session is stored somewhere?

    I'll look into the “Session Handler Database” module that @Jan Romero mentioned next...
     

  9. Thanks @BitPoet. My sites are on a VPS, not shared or "cloud" instances. What are the correct folder permissions? Is that listed anywhere?

    I had double-checked folder permissions/ownership and disabled FileCompiler on every template to fix these problems, which did the trick. I'll re-enable them and see if it has any impact on my current PHP session problem.

    With 'Use Compiled File' set to Auto on every template and namespace removed from all template files, everything looks normal, but session still doesn't work - one session ID across pages, but the variable doesn't stay updated. 

    When I just manually change the event number in the homepage script, PHP session works fine and that variable does get carried over to the next page:

    $session->set( 'event_number', 23 );

    This also works:

    $n = 15;
    $session->set( 'event_number', $n );

    But this fails:

    ... script that produces a valid $n ... 
    
    $session->set( 'event_number', $n );

    The $n variable does hold a correct number as far as I can tell. So the problem probably has nothing to do with PHP session or PW, just something dumb in my script.

    Does anyone recognize this type of error? What else could I try to troubleshoot?

  10. Thanks again @bernhard. Going the PW way, I still see the same problem. Should I still use session_start somewhere?

    Apparently one file didn't get included anymore - certain functions 'not found' etc. I had to add `namespace ProcessWire;` to the top of several template files. Is that now required since a recent version? Did something change?

    Adding `namespace ProcessWire;` to a bunch of files fixed missing functions errors, but not the sessions problem. I still get the same session ID across pages, but the variable does not get updated to the one set on the homepage.

    I guess I am setting the variable within a class/namespace (?!) IPinfo, so it's not available in namespace ProcessWire? Before the IP geolocation there is a line `use ipinfo\ipinfo\IPinfo;`. Does that interfere with namespace? I don't understand that namespace stuff at all. Should I do something to pass the variable between namespaces?

    I've tried using this, but then the event_number doesn't get set at all:

    	$session->setFor('ProcessWire', 'event_number', $n);

    Wrong syntax? Or just wrong?

    If the session ID stays constant between pages, why doesn't the session variable? How could I troubleshoot that?

  11. I use regular PHP Session in several conference websites to serve up the correct event content across a few pages; event number $n is set based on user IP location by a script on the homepage and used to display the relevant program on the next page, etc.

    It usually works nicely, but often breaks when I change the PHP version or configuration or look at it funny.

    At the moment I have the confirmed correct variable for event number $n at the end of homepage, but on the program page I keep getting another $n value even though it's the same session ID as on the homepage - I've tried several ways to unset and destroy the variable, change the location of session_start(); etc., but nothing works. Either way, the code worked fine before I messed with PHP config.

    Trying to troubleshoot this I see that PW has its own way to do sessions. Could that explain why my session variable keeps reverting to the wrong one? I have already turned off all caching and compiling as far as I know. The $session API variable / Session class documentation is super confusing to me, because it is all about login and authentication etc.

    What is the correct PW way to do this?

    Homepage:
    
    session_start();
    
    ... //PHP magic
    
    $_SESSION['event_number'] = $n;
    
    
    Other pages:
    
    session_start();
    
    if (isset($_SESSION['event_number'])) { 
    	$n = $_SESSION['event_number'];
    } else { 
    	$session->redirect('/'); 
    };

    I guess I have to use things like `$session->set('event_number', $n)`?

    I'll fiddle around with that after dinner, but any pointers would be much appreciated.
     

  12. My /assets/cache is messed up after changing PHP to 8 and then back to 7.4 - seems to have something to do with it, apparently had same problem 4 years ago. Manually deleting everything in that folder brings the whole site down with this error:

    Quote

    Error: Exception: Unable to create directory /site/templates/ (in wire/core/FileCompiler.php line 239)

    #0 wire/core/FileCompiler.php (359): FileCompiler->initTargetPath()
    #1 wire/core/Wire.php (416): FileCompiler->___compile('/home.php')
    #2 wire/core/WireHooks.php (952): Wire->_callMethod('___compile', Array)
    #3 wire/core/Wire.php (484): WireHooks->runHooks(Object(FileCompiler), 'compile', Array)
    #4 wire/modules/PageRender.module (561): Wire->__call('compile', Array)
    #5 wire/core/Wire.php (416): PageRender->___renderPage(Object(HookEvent))
    #6 wire/core/WireHooks.php (952)
     

    When I upload older cache files - only ones I had saved... - I can restore access, but then I get weird messed up old versions of my pages that I can't get rid of with any cache cleaning methods I know about. 

    How can I reset/clear/turn off any caching and compiling etc. to zero/factory settings? I only want template files to get current database content. 

    Edit: I guess I had a similar problem 4 years ago, also after changing/messing with PHP version. 

    I think I fixed it for now by turning off 'Use Compiled File' in Files in the template settings. Still curious what's going on here, so if anyone has any ideas, please post. 

  13. Very dumb question on a 5 year old thread; is jQuery still "native" anywhere in Processwire? If I choose to use jQuery - because I am old and slow... - should I just keep pulling it in with a link in <head> or does PW already load it somewhere else?

    What is the current state of this discussion; does PW now only have pure vanilla js? Or no js at all?

    I could probably find out combing through files...

  14. Very minor wishlist request/question: When I go to /access/users I always have to manually add the column with Created or Modified to be able to sort by date and get the latest activity. I would like to have that as the default; get the latest users access data and not with 'two months ago' but actual dates. 

    Or is there a way to configure that?

  15. Is there a way to detect if an element is already in viewport on load with IntersectionObserver, without having to scroll or resize?

    With my solution above you might get a #loadmore button when you only load one or two posts on initial load; I want to trigger that button to fill the page with posts until it's out of view. 

    You can work around that by using this function:

    Spoiler
    jLoad.fn.isInViewport = function() {
        var elementTop = jLoad(this).offset().top;
        var elementBottom = elementTop + jLoad(this).outerHeight();
    
        var viewportTop = jLoad(window).scrollTop();
        var viewportBottom = viewportTop + jLoad(window).height();
    
        return elementBottom > viewportTop && elementTop < viewportBottom;
    };

     

    And then this in the loadPosts() function below where the button is added in #loadmore:

    Spoiler
    			if (jLoad('#loadmore').isInViewport()) {
    				jLoad('#loadmore').trigger('click');
    			}

     

    That does the trick, but feels hacky and IntersectionObserver was supposed to replace that outerHeight/elementTop stuff.

    Is there any way to achieve the same by changing my IntersectionObserver code in some way? This version only triggers #loadmore when you scroll to it, not when it is already in view on load:

    Spoiler
    const io = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (!entry.isIntersecting) {
          return;
        }
            loadPosts();
      });
    });
    
    io.observe(document.getElementById('loadmore'));

     



    Edit: Thanks @gebeer for the suggestion below. That certainly looks cleaner. Should I just forget about IntersectionObserver? I won't get around to testing and looking into this until end of this month. 

  16. Very interesting/cool, but 'Htmx is a dependency-free, browser-oriented javascript library'. Is this really more efficient than jQuery or plain js? Does this leverage ProcessWire; is this the best PW-native solution?

    I have implemented this solution here in my sites and have to move on to other things to finish before the new year. I'll keep an eye on Htmx - it's The Future apparently... - and maybe implement this next Christmas. 😉


     

  17. I had a working version in my previous comment, but can't get PW $posts array to play nice with the rest of the script; I have to convert PW $posts to another $posts_array and then echo with the `$post['title']` syntax to make it work - why? what am I getting wrong?

    That works OK if you only want to show one or two fields, like `$post->title`, but obviously not if you want to do more complicated stuff. 

    So I'm trying another approach. I first grab a batch of IDs with $pages->findIDs() and then use those to lazy load each entire object as they are needed scrolling down the page. Should be a very efficient approach, right? Or not?

    I had trouble echoing the IDs with $post->id or $post['id'] or whatever, but apparently $post alone works with this simple one-dimensional array (?) - it's like all clever and stuff...

    This works 🙂 :

    Spoiler
    <?php 
    
    	if($config->ajax) {
    		
    			$template = $input->get('template');
    			
    			$batch = $input->get('batch');
    		
    			$posts = $pages->findIDs("template=$template, limit=$batch, sort=-date");
    						
    			// Default limit
    			$limit = isset($_GET['limit']) ? $_GET['limit'] : 2;
    			
    			// Default offset
    			$offset = isset($_GET['offset']) ? $_GET['offset'] : 0;
    			
    			$queried_posts = array_slice($posts, $offset, $limit);
    			
    			$total_posts = count($posts);
    			
    			// Get the total rows rounded up the nearest whole number
    			$total_rows = (int)ceil( $total_posts / $limit );
    			
    			$current_row = !$offset ? 1 : ( $offset / $limit ) + 1;
    			
    			if (count($queried_posts)) {
    
    			    foreach ($queried_posts as $post) {
    			    
    						$post = $pages->get($post);
    
    
    								echo '<a href="'. $post->url .'"><h3>'. $post->title .'</h3>';					
    								echo '<p class=dateline>' . $post->city->title .' '. date( 'F Y', $post->getUnformatted('date') ) .'</p>';
    								echo $post->body .'</a>';
    								
    								echo '</div>';
    
    			           
    						}
    
    			} else {
    			
    			    return false;
    			}
    		
    	return $this->halt();
    
    } else {
    
    include 'inc/head.php'; ?>
    
    <body>
    
         <div class=container style='padding: 8% 8% 50%'>
         <div id=loadposts></div>
         <button id=loadmore>Load more</button>
         </div>
    
    </body>
    
    <?php } ?>
    
    <script>
    var jLoad = jQuery.noConflict(); 
    
    var template = 'video|post';
    var batch = 32;
    var limit = 1;
    var offset = 0;
    var noMorePosts = false;
    var loadingInProgress = false;
    
    function loadPosts() {
        
        if (noMorePosts) return;
        
        let queries = {
    			'template' : template,
    			'batch' : batch,
    			'offset' : offset,
    			'limit' : limit
        };
        
        if (!loadingInProgress) {
            
    			loadingInProgress = true;
            
    			jLoad('#loadmore').html('Loading...').prop('disabled', true);
            
    			jLoad.get('', queries, function(data) {
                
                if(!data) {
    					noMorePosts = true;
    					jLoad('#loadmore').remove();
                }
                else {
                    offset += limit;               
                    jLoad('#loadposts').append(data);
                }
    
    			loadingInProgress = false;
                
    			jLoad('#loadmore').html('Load more').prop('disabled', false);            
                
            });
        }
    }
    
    
    const io = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (!entry.isIntersecting) {
          return;
        }
            loadPosts();
      });
    });
    
    io.observe(document.getElementById('loadmore'));
    
    
    
    jLoad(document).on('click', '#loadmore', function() {
        loadPosts();    
    });
    
    
    jLoad(document).ready(function() {
        loadPosts();
    })
    </script>

     

    Let me know if you see problems, possible improvements, bits that can be replaced with PW methods, etc. 

  18. Finally got the demo working in a PW template testpage.php:

    Spoiler
    <?php 
    
    	if($config->ajax) {
    			
    				$posts = array(
    			    	array(
    			    		'id' => '1',
    			    		'title' => 'Title of the first post',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),
    			    	array(
    			    		'id' => '2',
    			    		'title' => 'Second post here',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),
    			    	array(
    			    		'id' => '3',
    			    		'title' => 'And another one',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),
    			    	array(
    			    		'id' => '4',
    			    		'title' => 'Fourth post',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),
    			    	array(
    			    		'id' => '5',
    			    		'title' => 'Why is this the fifth?',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),
    			    	array(
    			    		'id' => '6',
    			    		'title' => 'Number six',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),
    			    	array(
    			    		'id' => '7',
    			    		'title' => 'Post number seven',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),
    			    	array(
    			    		'id' => '8',
    			    		'title' => 'Number eight here - more to go',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),
    			    	array(
    			    		'id' => '9',
    			    		'title' => 'Article number nine',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),    	
    			    	array(
    			    		'id' => '10',
    			    		'title' => 'Number ten - almost the end',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),    	
    			    	array(
    			    		'id' => '11',
    			    		'title' => 'Article number eleven',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),
    			    	array(
    			    		'id' => '12',
    			    		'title' => 'And the twelfth post',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),
    			    	array(
    			    		'id' => '13',
    			    		'title' => 'But we also have thirteen',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),
    			    	array(
    			    		'id' => '14',
    			    		'title' => 'And a fourteenth post',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	),
    			    	array(
    			    		'id' => '15',
    			    		'title' => 'And number fifteen',
    			    		'image' => 'https://via.placeholder.com/500x300.png'
    			    	)
    			);
    			
    			
    			// Default limit
    			$limit = isset($_GET['limit']) ? $_GET['limit'] : 2;
    			
    			// Default offset
    			$offset = isset($_GET['offset']) ? $_GET['offset'] : 0;
    			
    			$queried_posts = array_slice($posts, $offset, $limit);
    			
    			$total_posts = count($posts);
    			
    			// Get the total rows rounded up the nearest whole number
    			$total_rows = (int)ceil( $total_posts / $limit );
    			
    			$current_row = !$offset ? 1 : ( $offset / $limit ) + 1;
    			
    			
    			if (count($queried_posts)) {
    			    foreach ($queried_posts as $post) { ?>
    			    
    			       <h2><?php echo $post['title']; ?></h2>
    			       <img src="<?php echo $post['image']; ?>" />
    			           
    			     <?php }
    			}
    			
    			else {
    			    return false;
    			}
    			
    			if ( $current_row < $total_rows ) { ?>
    			    
    			    <div>
    			        <button id=load-more>Load more</button>
    			    </div>
    			    
    			<?php }
    			else {
    			
    			    return false;
    			}
    		
    	return $this->halt();
    
    } else {
    
    include 'inc/head.php'; ?>
    
    <body>
    
         <main class=container>
         <input type=hidden id=current-query value="" />
         <div id=all-posts></div>
         </main>
    
    </body>
    
    <?php } ?>
    
    <script>
    var jLoad = jQuery.noConflict(); 
    
    var limit = 2;
    var offset = 0;
    var noMorePosts = false;
    var loadingInProgress = false;
    
    function loadPosts() {
        
        if (noMorePosts) return;
        
        let queries = {
            'offset' : offset,
            'limit' : limit
        };
        
        if (!loadingInProgress) {
            
            loadingInProgress = true;
            
            jLoad.get('', queries, function(data) {
                
                if(!data) {
                    noMorePosts = true;
                    jLoad('#load-more').remove();
                }
                else {
                    offset += limit;
                    
                    jLoad('#load-more').remove();
                    
                    jLoad('#all-posts').append(data);
                    
                }
                
                loadingInProgress = false;
                
            });
        }
        
    }
    
    /************************************************
     * Adds Load More Button Functionality
     * Exclude this if only infinite scroll is needed
    *************************************************/
    jLoad(document).on('click', '#load-more', function() {
        
        let currentQuery = {};
        
        jLoad('#load-more').html('Loading...').prop('disabled', true);
        
        if(jLoad('#current-query').val().length) {
            currentQuery = JSON.parse(jLoad('#current-query').val());
        }
        loadPosts( currentQuery );
        
        
    });
    
    /************************************************
     * Adds Infinite Scroll Functionality
     * Exclude this if only load more button is needed
    *************************************************/
    
    jLoad(window).scroll(function() {
        
        if( ( jLoad(window).scrollTop() + jLoad(window).height() ) >= jLoad(document).height() ) {
            
            let currentQuery = {};
        
            if( jLoad('#current-query').val().length) {
                currentQuery = JSON.parse(jLoad('#current-query').val());
            }
    
            jLoad('#load-more').html('Loading...').prop('disabled', true);
            
            loadPosts(currentQuery);
            
        }
    });
    
    jLoad(document).ready(function() {
        loadPosts();
    })
    </script>

     

    I'll replace this step by step with PW native stuff and other updates. 

    I see I can't simply replace the hardcoded demo $posts array with `$posts = $pages->find("template=video, limit='12', sort=-date");`. I guess I need `"id" => "1"`, "id" => "2"`, etc. or rewrite something somewhere? Or JSON encode $posts?

    Why does $posts from $pages->find() not work when the manual/hardcoded $posts array from the demo works fine? I see the PW $posts array starts with the following:

    ProcessWire\PageArray Object ( [count] => 12 [total] => 118 [start] => 0 [limit] => 12 [pager] => 1 to 12 of 118 [items] => Array ( [Page:0] => Array ( [id] => 7457 [... etc.

    So there is the built-in pagination stuff. How can I work with that? I don't want "12 items per page". I want to grab a bunch of items and then add those to the page 1 by 1 (or 2) as you scroll and they come into view. How can I make $posts from PW behave like the demo script?

    This works, but it is not the PW way:

    Spoiler
    <?php 
    
    	if($config->ajax) {
    		
    			$template = $input->get('template');
    			
    			$batch = $input->get('batch');
    		
    			$posts = $pages->find("template=$template, limit=$batch, sort=-date");
    
    			$posts_array = array();
    			
    			foreach ($posts as $post) {
    			
    				$title = $post->title;
    			    	    
    			    $posts_array[] = array(
    			        'title' => $title
    			    );
    			}
    
    			// Default limit
    			$limit = isset($_GET['limit']) ? $_GET['limit'] : 2;
    			
    			// Default offset
    			$offset = isset($_GET['offset']) ? $_GET['offset'] : 0;
    			
    			$queried_posts = array_slice($posts_array, $offset, $limit);
    			
    			$total_posts = count($posts_array);
    			
    			// Get the total rows rounded up the nearest whole number
    			$total_rows = (int)ceil( $total_posts / $limit );
    			
    			$current_row = !$offset ? 1 : ( $offset / $limit ) + 1;
    			
    			if (count($queried_posts)) {
    
    			    foreach ($queried_posts as $post) { ?>
    			    
    			       <h2 style='height:400px'><?php echo $post['title']; ?></h2>
    			           
    			     <?php }
    
    			} else {
    			
    			    return false;
    			}
    		
    	return $this->halt();
    
    } else {
    
    include 'inc/head.php'; ?>
    
    <body>
    
         <div class=container style='padding: 8% 8% 50%'>
         <input type=hidden id=currentquery value='' />
         <div id=loadposts></div>
         <button id=loadmore>Load more</button>
         </div>
    
    </body>
    
    <?php } ?>
    
    <script>
    var jLoad = jQuery.noConflict(); 
    
    var template = 'video';
    var batch = 32;
    var limit = 2;
    var offset = 0;
    var noMorePosts = false;
    var loadingInProgress = false;
    
    function loadPosts() {
        
        if (noMorePosts) return;
        
        let queries = {
    			'template' : template,
    			'batch' : batch,
    			'offset' : offset,
    			'limit' : limit
        };
        
        if (!loadingInProgress) {
            
            loadingInProgress = true;
            
            jLoad.get('', queries, function(data) {
                
                if(!data) {
    					noMorePosts = true;
    					jLoad('#loadmore').remove();
                }
                else {
                    offset += limit;               
                    jLoad('#loadposts').append(data);
                }
                loadingInProgress = false;
            });
        }
    }
    
    
    const io = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (!entry.isIntersecting) {
          return;
        }
            let currentQuery = {};
        
            if( jLoad('#currentquery').val().length) {
                currentQuery = JSON.parse(jLoad('#currentquery').val());
            }
    
            jLoad('#loadmore').html('Loading...').prop('disabled', true);
            loadPosts(currentQuery);
      });
    });
    
    io.observe(document.getElementById('loadmore'));
    
    
    jLoad(document).ready(function() {
        loadPosts();
    })
    </script>

     

    It only works with `echo $post['title'];` in the output, not `echo $post->title;`. Why?

    But this does what I need, so I'll use this in my sites after further clean-up and updates. Are there bits that could easily be replaced with PW equivalents? Do you see obvious problems/downsides? Any other suggestions how to improve this much appreciated.

    I don't understand what `<input type=hidden id=currentquery value="" />` does, but removing it breaks the script... It looks hacky. Is there a better way to do whatever it does? 😕

    I guess the syntax `$limit = isset($_GET['limit']) ? $_GET['limit'] : 2;` doesn't work within PW and I should use `$input->get('limit')` with `if ... else` instead?

  19. Thanks again @gebeer. I am now puzzling with `if($config->ajax) { ...` - I have used Ajax elsewhere in my PW sites, but only with POST form data, not GET. 

    Yes, I would prefer to use ProcessWire built-in methods. As long as PaginatedArray doesn't require creation of stupid URLs like nextpage1, nextpage2, etc., happy to leverage that. Either way, it shouldn't need an external plugin.

    The documentation doesn't give me much to work with - no code examples for noobs etc. There are examples here, but only in combination with the MarkupPagerNav module, with all the page number and URL stuff I am trying to avoid. 

    Someone else was struggling with PaginatedArray here - 'pagination never works!' There is a code example in that thread that looks promising

    I have to go back to basics, because none of this is obvious to me. This works as a super minimal GET Ajax demo:
     

    Spoiler
    <?php 
    
    if($config->ajax) {
    
    	echo "We have no clue";
    		
    	return $this->halt();
    
    } else {
    
    include 'inc/head.php'; ?>
    
    <body>
    
        <div id=result></div>
        <button type=button>Click Here</button>
    
    </body>
    
    <?php } ?>
    
    <script>
    var jLoad = jQuery.noConflict(); 
    
    jLoad(document).ready(function(){
        jLoad("button").click(function(){
            
            jLoad.get("", function(data) {
                jLoad("#result").html(data);
            });
        });
    });
    </script>

     

     

  20. Thanks again for the feedback, @gebeer. Even if I don't (immediately) agree with all your points, it is super helpful to focus my attention, research things, sort out my priorities, etc.

    I avoid plugins because they have to be all things to all people. Plugins come with lots of built-in assumptions about how everybody does stuff. I prefer to have a minimal solution tailored to the needs of my site that I understand and can expand/improve as necessary.

    I have decided I absolutely don't want any pagination, not even 'just one link to the next page'. It goes against the logic of my site; you can browse the content by topic, location, etc., each with their own index pages and then limited continuous scroll + a search box. No page1, page2, page3, etc.

    I don't necessarily want search engine bots index anything other than the top pages by topic, location, etc. and the posts' own pages. It would still be good to have the first few posts of the post array in the page without requiring javascript; the first post would also be a "featured" post, styled differently etc. So I probably have to rewrite the scripts - any ideas appreciated.

    Quote

    I don't know if taking some seemingly outdated code to build upon is a good approach for a solution in 2023 ... visitors without JS ... jQuery dependency ... 

    The infinite-scroll.com plugin is originally from 2008, the granddaddy of continuous scrolling. The codeblock.co.za demo is from June 2020.

    Millennial developers have been claiming for the last ten years that everything will move to Javascript, with V8 in Chromium, React and all the other js frameworks. Is 'visitors without JS' still a serious issue? Or like nobody is developing for IE anymore. 

    jQuery is the uncool old js framework, but isn't it still default in Processwire? I use jQuery everywhere in my templates. 

    Quote

    ... client side scroll performance because of non-throttled $(window).scroll event listener. generally the applied scroll detecting technique seems outdated ...

    Yes, I noticed that. Thanks for the pointer. I'll have to rewrite that later. Any other suggestions how to improve that much appreciated. 

    The codeblock.co.za demo has a line like this:

            $.get('posts.php', queries, function(data) {

    In my version I want to just get a $posts array instead of an external file. How should I rewrite that? Syntax etc.? Or is that not possible? What should I google for to find the solution? Searching now, but if anyone knows, please let me know.

    This doesn't work. Does anyone spot the problem? Should I use `if($config->ajax) { ...` etc.? Using `jQuery.noConflict()` did not fix it, but I still leave that in, because it works fine in the other test version:

    Spoiler
    <?php include 'inc/head.php'; ?>
    
    <body>
    
         <main class=container>
         <input type=hidden id=current-query value="" />
         <div id=all-posts></div>
         </main>
    
    </body>
    
    <script>
    var jLoad = jQuery.noConflict(); 
    
    var limit = 2;
    var offset = 0;
    var noMorePosts = false;
    var loadingInProgress = false;
    
    function loadPosts() {
        
        if (noMorePosts) return;
        
        let queries = {
            'offset' : offset,
            'limit' : limit
        };
        
        if (!loadingInProgress) {
            
            loadingInProgress = true;
            
            jLoad.get('<?php echo $config->urls->templates ?>posts.php', queries, function(data) {
                
                if(!data) {
                    noMorePosts = true;
                    jLoad('#load-more').remove();
                }
                else {
                    offset += limit;
                    
                    jLoad('#load-more').remove();
                    
                    jLoad('#all-posts').append(data);
                    
                }
                
                loadingInProgress = false;
                
            });
        }
        
    }
    
    /************************************************
     * Adds Load More Button Functionality
     * Exclude this if only infinite scroll is needed
    *************************************************/
    jLoad(document).on('click', '#load-more', function() {
        
        let currentQuery = {};
        
        jLoad('#load-more').html('Loading...').prop('disabled', true);
        
        if(jLoad('#current-query').val().length) {
            currentQuery = JSON.parse(jLoad('#current-query').val());
        }
        loadPosts( currentQuery );
        
        
    });
    
    /************************************************
     * Adds Infinite Scroll Functionality
     * Exclude this if only load more button is needed
    *************************************************/
    
    jLoad(window).scroll(function() {
        
        if( ( jLoad(window).scrollTop() + jLoad(window).height() ) >= jLoad(document).height() ) {
            
            let currentQuery = {};
        
            if( jLoad('#current-query').val().length) {
                currentQuery = JSON.parse(jLoad('#current-query').val());
            }
    
            jLoad('#load-more').html('Loading...').prop('disabled', true);
            
            loadPosts(currentQuery);
            
        }
    });
    
    jLoad(document).ready(function() {
        loadPosts();
    })
    </script>

     

     

    posts.php looks like this:

    Spoiler
    <?php 
    
    $posts = $pages->find("template=video, limit='12', sort=-date");
    
    // Default limit
    $limit = isset($_GET['limit']) ? $_GET['limit'] : 2;
    
    // Default offset
    $offset = isset($_GET['offset']) ? $_GET['offset'] : 0;
    
    $queried_posts = array_slice($posts, $offset, $limit);
    
    $total_posts = count($posts);
    
    // Get the total rows rounded up the nearest whole number
    $total_rows = (int)ceil( $total_posts / $limit );
    
    $current_row = !$offset ? 1 : ( $offset / $limit ) + 1;
    
    
    if (count($queried_posts)) {
        foreach ($queried_posts as $post) { ?>
    
    			<div class=cell id=<?php echo $post->getUnformatted('date'); ?>>
    			<h2><?php echo $post->title; ?></h2>
    			</div>
    			           
         <?php }
    }
    
    else {
        return false;
    }
    
    if ( $current_row < $total_rows ) { ?>
        
        <div>
            <button id=load-more>Load more</button>
        </div>
        
    <?php }
    else {
    
        return false;
    }

     


    I can't figure out how to pull in that posts.php file into Javascript within PW; hardcoded paths that work fine in a folder outside of PW, like this, don't work within PW:

            jLoad.get('/myserverpath/posts.php', queries, function(data) {

     

  21. Thanks again @bernhard.

    I don't care about the SEO argument; those paginated nextpages should not exist. All content will have its own page and those pages will be reachable via search and limited continuous scrolling on index pages.

    Efficiency; good point. I guess PW's pagination solution takes care of caching and limiting database calls etc.? I have always avoided pagination, so will have to do some homework... 

    This is probably what I'm looking for; continuous scroll (and load more button) with items from a array, not pagination. This example includes a lot of stuff at once... - https://codeblock.co.za/infinite-scroll-load-more-jquery-php/

    I've downloaded the github files here and stripped out/dumbed down all the distracting css and fancy filtering, sorting, etc. to get a minimal working proof of concept that I can sort of understand and apply to a PW posts array later.

    This works as a demo:

    index.php

    Spoiler
    <!Doctype html>
    <html>
    
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Demo</title>
        
        <script  src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    
        <script src="js.js"  type=" text/javascript"></script>
    
    </head>
    
    <body>
         <main class=container>
         <input type=hidden id=current-query value="" />
         <div id=all-products></div>
         </main>
         
    </body>
    
    </html>

     

    js.js

    Spoiler
    var limit = 2;
    var offset = 0;
    var noMoreProducts = false;
    var loadingInProgress = false;
    
    function loadProducts() {
        
        if (noMoreProducts) return;
        
        let queries = {
            'offset' : offset,
            'limit' : limit
        };
        
        if (!loadingInProgress) {
            
            loadingInProgress = true;
            
            $.get('products.php', queries, function(data) {
                
                if(!data) {
                    noMoreProducts = true;
                    $('#load-more').remove();
                }
                else {
                    offset += limit;
                    
                    $('#load-more').remove();
                    
                    $('#all-products').append(data);
                    
                }
                
                loadingInProgress = false;
                
            });
        }
        
    }
    
    /************************************************
     * Adds Load More Button Functionality
     * Exclude this if only infinite scroll is needed
    *************************************************/
    $(document).on('click', '#load-more', function() {
        
        let currentQuery = {};
        
        $('#load-more').html('Loading...').prop('disabled', true);
        
        if($('#current-query').val().length) {
            currentQuery = JSON.parse($('#current-query').val());
        }
        loadProducts( currentQuery );
        
        
    });
    
    /************************************************
     * Adds Infinite Scroll Functionality
     * Exclude this if only load more button is needed
    *************************************************/
    
    $(window).scroll(function() {
        
        if( ( $(window).scrollTop() + $(window).height() ) >= $(document).height() ) {
            
            let currentQuery = {};
        
            if( $('#current-query').val().length) {
                currentQuery = JSON.parse($('#current-query').val());
            }
    
            $('#load-more').html('Loading...').prop('disabled', true);
            
            loadProducts(currentQuery);
            
        }
    });
    
    $(document).ready(function() {
        loadProducts();
    })

     

    products.php

    Spoiler
    <?php
    
    $products = array(
        	array(
        		'id' => '1',
        		'title' => 'Blue Shirt',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),
        	array(
        		'id' => '2',
        		'title' => 'Green Shirt',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),
        	array(
        		'id' => '3',
        		'title' => 'Yellow Shirt',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),
        	array(
        		'id' => '4',
        		'title' => 'Cargo Pants',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),
        	array(
        		'id' => '5',
        		'title' => 'Product 5',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),
        	array(
        		'id' => '6',
        		'title' => 'Formal Shirt',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),
        	array(
        		'id' => '7',
        		'title' => 'Peak Cap',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),
        	array(
        		'id' => '8',
        		'title' => 'Beret',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),
        	array(
        		'id' => '9',
        		'title' => 'Sleeveless Jacket',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),    	
        	array(
        		'id' => '10',
        		'title' => 'Windbreaker',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),    	
        	array(
        		'id' => '11',
        		'title' => 'Trousers',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),
        	array(
        		'id' => '12',
        		'title' => 'Bomber Jacket',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),
        	array(
        		'id' => '13',
        		'title' => 'Bomber Jacket Orange',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),
        	array(
        		'id' => '14',
        		'title' => 'Black T-Shirt',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	),
        	array(
        		'id' => '15',
        		'title' => 'Beanie',
        		'image' => 'https://via.placeholder.com/500x300.png'
        	)
    );
    
    
    // Default limit
    $limit = isset($_GET['limit']) ? $_GET['limit'] : 2;
    
    // Default offset
    $offset = isset($_GET['offset']) ? $_GET['offset'] : 0;
    
    $queried_products = array_slice($products, $offset, $limit);
    
    $total_products = count($products);
    
    // Get the total rows rounded up the nearest whole number
    $total_rows = (int)ceil( $total_products / $limit );
    
    $current_row = !$offset ? 1 : ( $offset / $limit ) + 1;
    
    
    if (count($queried_products)) {
        foreach ($queried_products as $product) { ?>
        
           <h2><?php echo $product['title']; ?></h2>
           <img src="<?php echo $product['image']; ?>" />
               
         <?php }
    }
    
    else {
        return false;
    }
    
    if ( $current_row < $total_rows ) { ?>
        
        <div>
            <button id=load-more>Load more</button>
        </div>
        
    <?php }
    else {
    
        return false;
    }

     


    I'll try to apply this in a PW page with an items array tomorrow... 

    Or do you see obvious problems with this approach?

     

  22. Thanks @bernhard and @gebeer, that is excellent input for me to start thinking through the options. 

    @gebeer, the 'infinite scroll' solution seems to presume that you already have a pagination system, with next pages etc., that you then convert into infinite scroll. Is that correct? Do I first have to set up pagination in Processwire - PaginatedArray etc.?

    I want to avoid/bypass the messiness of pagination, have a continuous scroll system instead that just adds items, keeping everything on one page, in combination with a 'more button' and a search field, with advanced search and sorting options where necessary.

    I see renderPager() from @bernhard's suggestion is also part of PW's pagination system. Is it unavoidable? Looking into it...

    My clunky solution 5 years ago was based on ID's in the items and then adding the next one, like this, via Ajax - still searching for the other bits and pieces in my archives...:

     

    Spoiler
    <?php
    
    $lastpost_id = $_GET ['lastpost_id']; // from the url to the loadcontent script
    
    $action = $_GET ['action'];
    if($action <> 'get') {
    
    $limit = '1';
    
    $topic = $pages->get("template=topic, title=$page->title"); 
    $videos = $pages->find("template=video, topic=$topic, date<$lastpost_id, limit=$limit, sort=-date");
    
    $lastpost_id = ''; // reset back to 0
    
    foreach ( $videos as $video ) { 
    
    $date= date( 'F Y', $video->getUnformatted('date') );
    
    echo '<div id='. $video->getUnformatted('date') .' class=post><a href='. $video->url .'><h3>'. $video->title .'</h3></a>';
    
    videoembed($video->video);
    
    echo '<p>'. $video->summary .' <span style="opacity: 0.3">  '. $date . ' </span></p></div>';
    
    };
    
    };
    
    ?>

     

     

    This worked in combination with this jquery:

     

    Spoiler
    var jLoad = jQuery.noConflict(); 
    
    jLoad(document).ready(function(){
    	jLoad(window).on('scroll', function() {
    	
    	if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
    	
        	jLoad('#moreloader').html('<img src=<?php echo $config->urls->templates ?>/img/loader.png>');
    		
    		var lastpost_id = '';
    		var lastpost_id = jLoad('.post:last').attr('id');
    
    		jLoad.ajax({
    			url: '?lastpost_id=' + jLoad('.post:last').attr('id'),
    			error: function(html) {
    				jLoad('#moreloader').remove();
    				},
    			success: function(html) {
    				if( html && lastpost_id == jLoad('.post:last').attr('id') ) {
    					jLoad('#posts').append(html);
    					jLoad('#moreloader').html('more');
    					} 
    				},
    			timeout: 7000 // sets timeout to 7 seconds
    		});
    		
    	}
    		
        });
    });

     

     

×
×
  • Create New...