Jump to content

gebeer

Members
  • Posts

    1,514
  • Joined

  • Last visited

  • Days Won

    45

Everything posted by gebeer

  1. I totally agree and would have expected RM to work like that. No use for a module-related migration if the module is not installed.
  2. Are you using MJML for compiling your email HTML. You can compile theam as .php files. Is it a namespace issue, do you have namespace ProcessWire in those email HTML files?
  3. Can you rename the include file to .php? Then PW should pick it up for translation.
  4. Have you tried $body=$files->render('path/to/my/mail-template.html''); ? Are your files namespaced?
  5. I found the reason. It works if you only have 1 repeater matrix field. If you have multiple, you need to pass the field object as 2nd parameter: $field = $fields->get('content_product'); $repeaterMatrix = $modules->get('FieldtypeRepeaterMatrix'); $typeId = $repeaterMatrix->getMatrixTypeByName('teaser_gallery', $field); Same goes for $repeaterMatrix->getMatrixTypeLabel(string 'typename', Field $field)
  6. Thanks for sharing. Your 2nd example can also be written like this $fieldName = 'content_product'; $typeName = 'product_downloads'; $pagesWithType = $pages->find("{$fieldName}.type={$typeName}"); db($pagesWithType); No need for looping through all pages. find() accepts the subselector .type. Only drawback with my solution: you don't get the label for the type. Actually when trying your example, $typeId returned false and $typeName returned null $repeaterMatrix = $modules->get('FieldtypeRepeaterMatrix'); $typeId = $repeaterMatrix->getMatrixTypeByName('product_downloads'); db($typeId, '$typeId'); // returns false $typeName = $repeaterMatrix->getMatrixTypeLabel($typeId); db($typeName, '$typeName'); // returns null Which version of Repeater Matrix are you using?
  7. On Linux I'm using a combination of https://www.maartenbaert.be/simplescreenrecorder/ (available for most distros) https://www.openshot.org/ (also available for Windows/Mac) Have been looking for a all-in-one solution for a long time. https://obsproject.com/ is a complete solution but it is rather complex for the simple task of creating screencasts. SimpleScreenRecorder allows pause/resume and Openshot is a great easy to use tool for editing / adding captions etc.
  8. Your problem might be related to Autocomplete and PageListSelect inputfields not allowing for custom selector strings. See Just a guess
  9. You get this error because you are using $cache inside the hook function. There you need to use wire('cache') or $this->wire->cache, depending on the context.
  10. Instead of the nested foreach you could do foreach ($tags as $tag) { $usage = $matches->find("tax_tag={$tag}")->count; $label = "{$tag->get('title')} {$usage}"; … } As for optimizing speed, how many pages are there potentially in $matches and in $tags, hundreds or thousands? To speed up queries, you can use $pages->findRaw() and only get the properties that you need for output and then work with those arrays in memory. Example: // find all pages with template document that have a tax_tag assigned // returns associative array indexed by page id with fields title, body and tax_tag where tax_tag is an associative array indexed by tax tag id with fields id and title // [ // 1234 => [ // 'title' => 'Page title', // 'body' => 'Page body content', // 'tax_tag' => [ // 4567 => [ // 'id' => 4567, // 'title' => 'Tax tag title', // ], // ... // ], // ], // ... // ] $matches = $pages->findRaw("template=document, tax_tag!=''", ['title', 'body', 'tax_tag' => ['id', 'title']]); Now you can use the resulting associative array $matches and do operations on it in memory without further DB calls. If you want to get a unique array of tags with usage count that are inside $matches, you could do something like this: // loop through matches to construct available tags array $tagsAvailable = array(); foreach($matches as $m) { // loop through tax_tags of each item foreach($m['tax_tag'] as $key => $tag) { // if key found in $tagsAvailable, tag is already there and we continue if(array_key_exists($key, $tagsAvailable)) continue; // get count of occurences of tax_tag id inside $matches items // and assign count as field 'usage' to the tag $tag['usage'] = count(array_filter(array_column($matches, 'tax_tag'), function($arr) use($key) { return in_array($key, array_keys($arr)); })); // add tag to available $tags $tagsAvailable[$key] = $tag; } } $tagsAvailable will look somewhat like this [ 1137 => [ 'id' => 1137, 'title' => 'Tag Title 1', 'usage' => 4, ], 1140 => [ 'id' => 1140, 'title' => 'Tag Title 2', 'usage' => 7, ], ] Now you can use $tagsAvailable to render your tags and $matches to render your result list. EDIT: I did this with $matches only containing 22 items and $tagsAvilable resulting in 5 items and the whole operation took 0.01ms and used 2.6kB of memory. So this should scale to hundreds or even thousands of matches. Though if you have that many matches you might consider chunk processing and paginating them.
  11. Hi, you can completely customize the comment form and comments list. Have a look at https://github.com/processwire/processwire/blob/master/wire/modules/Fieldtype/FieldtypeComments/CommentFormCustom.php and https://github.com/processwire/processwire/blob/master/wire/modules/Fieldtype/FieldtypeComments/CommentListCustom.php The classes are well documented. This should get you started.
  12. Honestly, when I read your first question, I thought this was spam because I couldn't make sense of it. Sorry for that. May I kindly ask you to edit your last post and remove my clearname from the quote? When pasting from ChatGPT, that got transferred. I thought I had deleted it, but... Would be very much appreciated. Once that is done, I am happy to answer your question.
  13. Your question doesn't make sense. I thought it a good test case for ChatGPT. Here's the AI's answer: Webcomic Site Software Comparison Would ImageMagick work good for a webcomic site or is FileZilla the better option? How do they work and what’s the pros and cons of them? ImageMagick and FileZilla are two different types of software. ImageMagick is an image manipulation program, while FileZilla is a file transfer protocol (FTP) client. ImageMagick can be used to process and manipulate images, such as resizing, cropping, and converting file formats. It can be useful for a webcomic site if you need to process and edit images before uploading them to the site. FileZilla, on the other hand, is used to transfer files between a local computer and a server. It is a popular FTP client that can be used to upload files to a web server. FileZilla would be useful for uploading webcomic images to a website or web server. The main advantage of ImageMagick is its ability to process and manipulate images, which can save time and effort when creating and uploading webcomics. The main advantage of FileZilla is its ease of use and reliability when uploading files to a server. In summary, ImageMagick is an image manipulation tool, while FileZilla is a file transfer tool, both of them can be useful for a webcomic site, but for different purposes. The choice will depend on the specific requirements of your webcomic site.
  14. @bernhard could you please add PHP 8 as dependency for the latest Version 2.1.0. Thank you. Was updating a website today that still runs on 7.4 and got composer errors because mpdf requires PHP>=8.0.
  15. Looks Awesome. Thank you for sharing. Will definitely have a look. As I understand it from the docs, every block is being wrapped inside a <div> element. I wonder if this would have negative impact on machine readability and therefore SEO since page markup is not really semantically structured anymore. Maybe I'm wrong, and it is semantically ok to have something like this? div section div article div section div article etc. I know that this is an issue with most page builders. They need that extra markup.
  16. Have you tried $page->products_codes->sort("parent.title")?
  17. To guard against execution of scripts inside vendor folder there seem to be 2 common approaches. move vendor outside webroot protect vendor folder with a .htaccess file I 'm just wondering if option 2 might be the better in our case because it doesn't involve changing core index.php Other CMS include a .htaccess inside vendor. Drupal for example does it like this with following content in vendor/.htaccess # Deny all requests from Apache 2.4+. <IfModule mod_authz_core.c> Require all denied </IfModule> # Deny all requests from Apache 2.0-2.2. <IfModule !mod_authz_core.c> Deny from all </IfModule> # Turn off all options we don't need. Options -Indexes -ExecCGI -Includes -MultiViews # Set the catch-all handler to prevent scripts from being executed. SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006 <Files *> # Override the handler again if we're run later in the evaluation list. SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003 </Files> # If we know how to do it safely, disable the PHP engine entirely. <IfModule mod_php.c> php_flag engine off </IfModule> Other suggestions from SO for .htaccess are: Order allow,deny Deny from all Problem with this apporach seems to be that the .htaccess gets lost when you remove vendor and do a composer install for whatever reason. But this could be solved with composer scripts inside composer.json. For example a simple script that places an .htaccess file inside vendor after composer install is finished using the post-install-cmd event. In https://github.com/processwire/processwire/blob/master/composer.json after line 22 we could add , "scripts": { "post-install-cmd": "echo $'Order allow,deny\nDeny from all' > vendor/.htaccess" } Just tested this and it works. This is off topic but would you mind explaining why you are trying to avoid SMTP and what your preferred alternative is?
  18. Here's a good short comparison SSE vs polling: https://blog.axway.com/learning-center/apis/api-streaming/server-sent-events In short: use SSE!
  19. Have a look here https://stackoverflow.com/questions/53214116/intersectionobserver-callback-firing-immediately-on-page-load I made a codepen that does just that https://codepen.io/gebeer/pen/KKBMWzV?editors=1011 It uses getBoundingClientRect, though.
  20. Have a look at https://htmx.org/docs/#websockets-and-sse Server Sent Events might be a solution for you.
  21. You are making a good and important point here. My short tutorial serves as a proof of concept like stated in the introduction. For a full fledged implementation UX has to be taken in account. To improve upon my example, we can add browser history support by adding the hx-push-url attribute. So the hxAttributes part of my code would look like $hxAttributes = array(); $hxAttributes[] = 'hx-get="' . $nextPageUrl . '"'; $hxAttributes[] = 'hx-push-url="' . $nextPageUrl . '"'; $hxAttributes[] = 'hx-trigger="revealed"'; $hxAttributes[] = 'hx-swap="afterend"'; Now if a user follows a link to a single post and then returns to the overview via browser back button, they'd land on the same page they left off. To make this behaviour smarter and have the previous pages (batches) load on scrolling up, we could expand the whole logic by placing htmx attributes on the first post of every page (batch) with hx-swap="beforebegin". If a user came to /posts/page6 through a shared link, they could scroll up until they reach /posts/page1. IMHO you cannot compare htmx to jQuery since they serve completely different purposes. I think it leverages PW pagination capabilities quite well. I don't think there is a "best" solution for this use case. It depends very much on the context. It may fit well with some projects and might not be a good solution for others.
  22. Looks like https://processwire.com/modules/profields-table/ could be a good candidate for your use case.
  23. Hello all, I wanted to share a proof of concept approach on how we can have an infinite scroll pattern without writing Javascript. We can achieve this with htmx. What you will get An overview page with a list of posts (PW pages). On initial page load 5 posts will be shown. When scrolling down and reaching the last post, another 5 posts will be loaded via AJAX in the background and appended after the last post until no more pages are found. Prerequisites You are using the delayed output strategy, with a _main.php appended. Just like using the default site profile when installing ProcessWire You are using markup regions, in my _main.php I have a div#content that will be used for the output of the posts Inside site/config.php $config->useMarkupRegions = true; Inside site/templates/_main.php <!-- main content --> <div id='content'> </div> For script loading I am using a custom $config->bodyScripts FilenameArray Inside site/config.php $config->bodyScripts = new FilenameArray(); Inside site/templates/_main.php before the closing </body> tag <?php foreach ($config->bodyScripts as $file) : ?> <script src="<?= $file ?>"></script> <?php endforeach ?> </body> PW page structure for this tutorial In the page tree I have a parent page "Posts" with template "posts". All child pages of that page have template "post" In the template "posts" settings in the "URLs" tab, check "Allow Page Numbers" and save. Needed for pagination. When viewing the page "Posts" all logic happens inside site/templates/posts.php site/templates/posts.php <?php namespace ProcessWire; // posts.php template file // add htmx js from site/templates/scripts $config->bodyScripts->add($config->urls->templates . 'scripts/htmx.min.js'); $limit = 5; $posts = $pages->find("template=post, limit={$limit}"); $lastPost = $posts->last(); $nextPageUrl = $page->url . $input->pageNumStr((int) $input->pageNum() + 1); $hxAttributes = array(); $hxAttributes[] = 'hx-get="' . $nextPageUrl . '"'; $hxAttributes[] = 'hx-trigger="revealed"'; $hxAttributes[] = 'hx-swap="afterend"'; ?> <?php if (!$config->ajax) : ?> <section pw-append="content" class="posts" hx-headers='{"X-Requested-With": "XMLHttpRequest"}'> <?php endif ?> <?php foreach ($posts as $post) : ?> <article class="post" <?php if ($post == $lastPost) echo implode(' ', $hxAttributes) ?>> <header> <h3><?= $post->title ?></h3> </header> </article> <?php endforeach ?> <?php if ($config->ajax) return $this->halt() ?> <?php if (!$config->ajax) : ?> </section> <?php endif ?> And that is all there is to it. Not a single line of Javascript, thanks to htmx. I followed the infinite scroll pattern from the official htmx examples. Now let's break the code down into easily digestible chunks // add htmx js from site/templates/scripts $config->bodyScripts->add($config->urls->templates . 'scripts/htmx.min.js'); This adds site/templates/scripts/htmx.min.js to our custom $config->bodyScripts FilenameArray so it will be loaded in _main.php. You can get the script here from unpkg.com. $limit = 5; $posts = $pages->find("template=post, limit={$limit}"); Sets our pagination limit to 5 and loads the correct set of posts. $lastPost = $posts->last(); Saves the last post of each set. We use this later to determine whether the htmx attributes should be rendered. $nextPageUrl = $page->url . $input->pageNumStr((int) $input->pageNum() + 1); We are building the link to the next "page" with the next set of posts. Will result in something like "/posts/page2", "/posts/page3" etc. $hxAttributes = array(); $hxAttributes[] = 'hx-get="' . $nextPageUrl . '"'; $hxAttributes[] = 'hx-trigger="revealed"'; $hxAttributes[] = 'hx-swap="afterend"'; Define our htmx attributes as an array. They will be added to every last post's HTML. Note the hx-get attribute which will be the URL for the AJAX call in the background. That request is triggered whenever the last post becomes visible inside the viewport while scrolling down. hx-swap afterend tells htmx to append the next batch of posts after the last post. <?php if (!$config->ajax) : ?> <section pw-append="content" class="posts" hx-headers='{"X-Requested-With": "XMLHttpRequest"}'> <?php endif ?> // and <?php if (!$config->ajax) : ?> </section> <?php endif ?> Renders the wrapping section tag only on initial page load which is a none AJAX request. Note the hx-headers='{"X-Requested-With": "XMLHttpRequest"}'. This adds an additional header to all AJAX requests with htmx. We need this header so ProcessWire understands that it is an AJAX request. Otherwise $config->ajax would always return false. See https://htmx.org/attributes/hx-headers/ for more info <?php foreach ($posts as $post) : ?> <article class="post" <?php if ($post == $lastPost) echo implode(' ', $hxAttributes) ?>> <header> <h3><?= $post->title ?></h3> </header> </article> <?php endforeach ?> Render each posts's HTML. If it is the last post, also render our htmx attributes. For brevity in this example I only output the post title. <?php if ($config->ajax) return $this->halt() ?> For AJAX requests stop execution of the template file and everything that follows. This prevents appending of _main.php for ajax calls. So we only get the desired HTML for the list of posts and no head, footer etc. Summary Compared to other approaches, htmx lets us control all our AJAX logic with a few html attributes. Really neat and concise. Easypeasy. I like that and will surely use an approach like this in future when infinite scroll is needed. What I like in particular is how easy this is implemented with ProcessWire's powerful pagination capabilities. If you have the same page structure, the code in site/templates/posts.php is working out of the box as is. I have this running on a standard PW multilang site profile with additions/amendments mentioned above under "Prerequisites". Here's a visual of the result:
  24. $config->ajax works exactly the same for POST and GET requests. It tells you whether the request is a "normal" or an XMLHttpRequest , independent of the request type. Why would you think that those URLs are stupid? They are a conventional way of telling PW which set of pages to retrieve. Other frameworks are using this convention, too. They consist of 2 parts: {pagNumPrefix}: configured through $config->pageNumUrlPrefix, defaults to "page"; tells PW that this is not a regular URL but a "paginated" one {pageNum}: number of page, in other words: number of set of pages to retrieve; setStart() is using this number From these 2 PW is able to figure out which pages from a PaginatedArray it should return. Let's look at your example code in posts.php and see how that relates to PW terms and conventions: /** @var WireInput $input */ // Default limit $limit = isset($_GET['limit']) ? $_GET['limit'] : 12; // $_GET['limit'] translates to the limit= partof the $pages->find selector // Default offset $offset = isset($_GET['offset']) ? $_GET['offset'] : ($input->pageNum - 1) * $limit; // $_GET['offset'] translates to $input->pageNum -1 * $limit // $queried_posts = array_slice($posts, $offset, $limit); // translates to /** @var PaginatedArray $posts */ $posts = $pages->find("template=video, limit={$limit}, start={$offset}, sort=-date"); // if URL is /page2, $posts will contain 12 pages starting from page at 12th index You can already see how much simpler the code becomes when using "stupid" URLs :-) Now let's look at the etymology of the word "stupid": It originates from the Latin word "stupere" which means "to be amazed or stunned". Isn't it amazing how simple pagination can become when doing it the PW way? I personally am really stunned by that fact :-) Of course you could do it in a less amazing way. PW is flexible enough to allow that. But then you would have to calculate and pass the information about $limit and $offset back and forth manually in your ajax call. You can pass this inside an object, just like your example code does: let queries = { 'offset' : offset, 'limit' : limit }; jLoad.get('<?php echo $config->urls->templates ?>posts.php', queries, function(data)... Actually jQuery translates this to a request URL with GET parameters like /site/templates/posts.php?offset=0&limit=2 In PW this URL would not work because you cannot call site/templates/posts.php directly (see https://processwire.com/talk/topic/20300-run-independent-php-files-within-processwire/ ) You can solve that by including posts.php for AJAX calls inside your template (videos.php) if($config->ajax) include_once("posts.php"); This is not all there is to do. Inside your JS you would have to keep track of the offset. How would your JS know which offset we are on? You could pass the offset as data attribute somewhere in your HTML and then retrieve it in JS. You could also send it together with the response as JSON. You could store it as cookie. Many possibilities here. But still making everything more complicated than it has to be. When using a #load-more button with an amazing URL like /videos/page2, in your JS you just have to update the "2" to "3" so the amazing URL reads /videos/page3. By the way, the infinite-scroll plugin does exactly that. If you want to do it yourself and you have a link that points to the next page like <a id="load-more" href="/videos/page2">Load more</> You could use this JS to update href to 'videos/page3' var elem = $("#load-more"); var path = elem.attr('href'); var pathParts = path.split('/'); var thisPage = pathParts.pop(); var pageNum = parseInt(thisPage.match(/[0-9]+$/)); var pagePrefix = thisPage.slice(0, thisPage.match(/[0-9]+$/).index); var nextPage = pagePrefix.concat('', pageNum + 1); pathParts.push(nextPage); var newPath = pathParts.join('/'); elem.attr('href', newPath); Is this nice? No. Could it be improved? Certainly. While in the above there certainly are some code samples that you could use to achieve what you want, my intention was more to show that it is a lot more work to go this route. Look at my initial example to use with infinite-scroll plugin and how simple and intuitive that is. Sorry if I cannot (or rather don't want to) supply you with a readymade copy/paste solution and sorry if I might sound sarcastic. I'm just not seeing the point in reinventing the wheel of pagination for the xth time over. I don't see the advantage of a custom implementation for your use case over that which PW provides out of the box. If it is just the amazing URLs like page2, page3 that bug you, try to think of them like a tool that you can use to your advantage. And neither the site visitor nor search engines need to see them. As for site visitors, they would only see them in the browsers' dev tools. And search engines only if you allow them to (think of rel=nofollow). If you still want to do it your own way then I wish you the best of luck and happy coding :-)
  25. I can confirm that this hook is working on a clean PW3.0.208dev install. So problem is somewhere within my other install.
×
×
  • Create New...