Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 06/11/2024 in all areas

  1. Hi everyone, We have a new module for you: the UpdMostViewed module. It's an addition to your ProcessWire site that enables you to track page views and deliver a list of your most visited pages within a given time range. This data is particularly useful for creating frontend features like a "Most Read Articles of the Week" widget. Installation You can choose to: Head into your ProcessWire backend, go to Modules > New and search for the Module UpdMostViewed. Get the module directly from the latest releases on our GitHub repository. If you're using Composer for your project, you can add the module by running the command composer require update-switzerland/updmostviewed in your project root directory. Once downloaded, you can install the module via the ProcessWire admin. Configuration The UpdMostViewed module provides you with a variety of configuration options. You can exclude certain branches, specific pages, certain IPs, restrict counting to specific templates, define which user roles to count, ignore views from search engine crawlers, and more. Moreover, you can also customize the time ranges for the "most viewed" data. Links For more detailed information on usage, updates, or to download the module, please visit our GitHub repository and check out the module in the Module Directory.
    8 points
  2. When I had a similar situation and huge variants in image dimensions, I solved this a different way by moving a logo "section" from a page block to providing a full page that is dedicated to partner/sponsor organizations. It enabled me to provide the organization name (which isn't always immediately evident from just a logo [for people; bots would have indexed the ALT text just fine]), the website address (so if someone saved an image of the page the website location still exists), and a short summary description of the organization - which could be anything: how they've partnered/helped, a description of their organization, a pulled meta description from their website (if available), etc. I could display all of them at once since it was a dedicated page, I could decide to order them alphabetically, by level of sponsorship, or randomly, thereby removing unexpected bias between the sponsors (or at least defending the decision), and although it wasn't necessarily quite as nice as having a clean block of images to separate out content on the prior page, it did clean things up when it had its own dedicated space - and the original image section became a call-to-action area to link to the dedicated page. It allows a bit more freedom for design options, too.
    3 points
  3. @ryan That sounds promising, quote: TinyMCE for Open Source We recognize that the GPL may not align with the needs of certain free and open source (FOSS) projects. In response, we are introducing the TinyMCE for Open Source program that will provide a custom license to qualified open-source projects. This custom license will permit the use of TinyMCE 7+ in open-source projects with a license that is incompatible with GPLv2+. To apply for the Tiny for Open Source Program, please fill out and submit an application "there".
    3 points
  4. Over the years there's been a growing part of me that's wanted to be a full-time Linux user. I've been using Windows from the beginning and attempted to switch to macOS a few years ago (given the excitement of M1) however I gave up after 8 solid months because I came to dislike some issues that I couldn't circumvent in macOS. Due to some fear, impatience and most importantly, software compatibility, I have not made the switch to Linux, but times have changed. I've played around with the big distributions (Ubuntu, Debian, Fedora, etc.) in some way or another. I've grown extremely comfortable with Bash, the command line and have the confidence in working my way out of issues (a lot of this is due to the hardware business I was involved with for 7 years which sharpened by skills). I've dealt with installing Nvidia drivers in every which way too (if you know, you know). Whenever Ubuntu releases an LTS version, I tend to experiment with it and I did so last night with 24.04. Really annoying things like not being able to move the taskbar from the left to the bottom without having to use extensions that could break, are now natively supported (IIRC you couldn't do this so easily before). That sounds like a minor nit-pick but if there's something I know about myself, it's that if I don't have to reprogram my muscle memory, I will have an easier time. The mouse movement feels Windows-like (this truly annoyed me with macOS despite trying every program and tweak in existence) and font rendering doesn't seem to bother me as much as macOS. Again these seem like minor nit-pick type things but to me they matter. At this point, it comes down to software compatibility. I'm not some hardcore Office user and I barely use it beyond basic word processing, so that's not an issue. The only other software suite is Adobe CC. I do rely on Photoshop and Illustrator, but not for "creating" but rather tweaking. I've built some muscle memory with those apps as well but I can't let them hold me back. I know Photopea exists which many have raved about being a great Photoshop alternative and which supports PSD quite well from what I've heard. So that's always an option combined with Gimp or Krita if I don't have to deal with actual PSD files. Maybe I'll set up a VM with Windows to help ease the transition. So at this point, there's nothing holding me back. I installed Ubuntu 24.04 on my main system along-side Windows (dual boot). In a couple months, I will try to switch. Any other desktop Linux users here? BTW, I'm sticking with Ubuntu because I like how it's Debian-based. I know there's some disagreements with snaps being used, etc., but because it's the most popular Linux distro and feels polished enough and similar to Windows, it's the best place to start (and maybe stay).
    2 points
  5. Spoiler: I didn't switch to linux. I hope it's ok to share my journey as I found it very interesting to read others'. Would be interesting to hear those!
    2 points
  6. Here! Moved away from Mac over a decade ago and went with Windows for quite some time. Then about 6+ years ago I made the switch to full-time Linux. Never looked back. Enjoyed distro-hopping between Ubuntu, Fedora, Manjaro, Debian, Arch, Zorin and finally settled with Ubuntu because of the broad support even for tools like ScreamingFrog (.deb and .rpm). Moved away from Gnome to i3wm to have a super minimal environment. Everything works just by pressing keys - from moving windows around workspaces, opening or starting apps, to window tiling. I am a happy camper now. ? As you are on Ubuntu as well I suggest looking at deb-get - a little helper to install most common apps without hunting down the .deb files: https://github.com/wimpysworld/deb-get/blob/main/01-main/README.md You may have seen over on X/Twitter the journey of DHH He released his very own setup as a script to make tons of changes, installing software, fonts, and all kinds of stuff. See here: https://x.com/dhh/status/1798466733222838758 His journey explained here: https://x.com/dhh/status/1799185008378171885
    2 points
  7. In my experience: Just show them all, on first sight, without need to tap, even if it is ugly ? If not, you will inevitably have to deal with one or more logo partners deeply dissatisfied for lacking visibility. "No one will tap 10 times until they arrive at our logo..." Just my 2cents...
    2 points
  8. Double Commander is the best. I use it on Windows too. Powerful as... itself!
    2 points
  9. KDE Plama user here and very happy with it (currently KDE Neon but sometimes Kubuntu). I do also own a Mac becasue we sometimes build apps which needs me to use XCode, and I have a laptop that I can dual boot into Windows but I can't even remember the last time I had to do that and most of my day is spent in Linux. I'd agree that the main drawback is the gap in graphics software. I use Affinity Designer on the Mac which is great, but whilst it nearly runs in Wine it's not quite there yet. Apart from that my development stack on Maxc and Linux is pretty much identical so it's dead easy to swap between them. The designers I work with nearly all use Figma these days so it's been a while since I was given a PSD anyway (which Affinity Desginer deals with very well). You should definitely give Plasma a go if you want to be able to tweak your desktop .... although I warn you will spend a lot of time tweaking your desktop ....
    2 points
  10. Long time Linux user here. Over 20 years since Fedora Core 1. I am using fedora 40 now. Very happy.
    2 points
  11. Yes, I switched from Windows to desktop linux in 2004 and have not regretted it. I do have a Windows VM for running one legacy app, which I boot once every 2 months and run for a few hours. Apart from that I also use DDEV to run local development containers (thanks for the pointer @bernhard) and am using the Cinnamon desktop - which I do prefer over gnome.
    2 points
  12. This week I'm releasing the ProcessWire Fieldtype/Inputfield module "Functional Fields" as open source in the modules directory. This was originally built in 2017 for ProFields, but hasn't required much in terms of support resources, so I thought I should release this newest version (v4) publicly. While this module is completely different from other Fieldtypes, I think it's quite useful and fulfills some needs better than other Field options. Give it a try and please let me know what you think. For an introduction to Functional Fields, please revisit this blog post which covers all the details: https://processwire.com/blog/posts/functional-fields/ Functional Fields is available for download in the modules directory here: https://processwire.com/modules/fieldtype-functional/ Thanks and have a great weekend!
    2 points
  13. System templates have the Template::flagSystem flag. So the "proper" way: $non_system_templates = new TemplatesArray(); foreach($templates as $template) { // Skip templates with the system flag if($template->flags & Template::flagSystem) continue; $non_system_templates->add($template); } The lazy way that is likely to work 99.9% of the time: $non_system_templates = $templates->find("flags=0"); For some reason templates that are used for the custom fields for files/images feature don't have the system flag, so you will have to exclude those separately if you have any. The name of such templates starts with "field-".
    1 point
  14. What about excluding all pages that have the parent with ID 2 - as that would be the admin? pages->find('!hasParent=2, template=all|my|templates')
    1 point
  15. I wrote about it here to an extent: Indeed. While I don't use laptops, I did have a Intel NUC for a while and that totally drove me insane with the fan whizzing. However a new era is now upon us with Qualcomm Snapdragon X, the non-Apple alternative Apple Silicon. When Framework laptops get this chip and Linux support is 100%, I'm making the switch and saying goodbye to X86 (and Windows).
    1 point
  16. What I always do is to write those scripts in an object oriented way right from the beginning. That's as easy as adding one method to the Site.module.php Then I add $site->doSomething() to ready.php and RockFrontend will live reload the page as soon as I update Site.module.php and I'll see all the dumps in Tracy. No need to switch screens. No need to switch windows. No need to use the mouse. Just write code, save, see the dumps. And once everything works, everything is just an api call away! And, of course, it's under GIT. $site->party() ? PS: And if you happen to need that for a cron or similar: <?php include "index.php"; $site->party();
    1 point
  17. If I enable guest dumps (via the button on the panel selector) and use bd() instead of d() then after calling a script via the command line, I can reload the site in the browser and the bd() calls will be displayed. Does that help? The only other solution I can think of at the moment is to maybe check if the snippet is called via the CLI and convert d() calls to print_r() automatically? I've done some quick experiments on that idea and seems fairly useful. By the way - just making sure that you're familiar with the Console panel's "Code Prefix" settings field for adding that: <?php namespace ProcessWire; if(!defined("PROCESSWIRE")) include(dirname(__FILE__).'/../../../../index.php'); block automatically to each snippet.
    1 point
  18. Awesome. Ok. He is convincing quite a few right now. It's quite interesting to follow the situation. Didn't know about Pinta. Might give it a try. Another thing in regards to Gimp and Inkscape (an Illustrator alternative) - great tutorials here: https://www.youtube.com/c/LogosByNick He offers courses to master both for very little money. Might help to keep things moving in that space.
    1 point
  19. Yes yes, DHH's journey is part of what's pushing me as well. I'm following the repo and picking out various tools Omakub is using that seem to fit my way of doing things (ex: Pinta).
    1 point
  20. Yes Ubuntu adds their own philosophy over the Gnome desktop. With Gnome in Manjaro Linux I can move the dash in 4 screen edges and have more options to tweak. You could try Manjaro since it's based on the rolling release Arch Linux distribution, so you get only recent versions of each software. It's not considered as stable as Ubuntu tho (in theory, because of rolling release philosophy), but practically there's really few chances to have a serious issue. In the past I ran Adobe CS5 under a virtualized Windows with Virtual Box and that was working fine, I mainly used Photoshop and Illustrator. Maybe make a try. It's necessary to enable CPU virualization in BIOS and activating some options in VirtualBox (like graphical acceleration) after creating the Windows VM and installing the package virtualbox guest additions.
    1 point
  21. Carousel of some kind, rotating through all the logos? Not sure I like it, but it might work.
    1 point
  22. Nice to see Linux users here. As for KDE, even though it's more advanced user oriented (my opinion), I like the polish and feel of Gnome more. Also, I want to avoid the whole ricing thing altogether. My need to tweak is very minimal... just a few changes to satisfy my muscle memory with Windows. The feel of the mouse alone without any tweaks is already a huge win (I abhor macOS mouse acceleration). When System76's new Cosmic is done, I will definitely give Pop_OS! a try (it's based on Ubuntu which is good for me). I bought Affinity Designer last year, so I will try to run that in Wine. In the meantime I've installed a bunch of other graphics programs like Photopea, Vectorpea, Gimp, Krita, Inkscape, Pinta. I'm going to install every graphics software I find! ? I also installed the Segoe UI font and enabled it as the default font for Gnome. Segoe UI is a Microsoft font which is the default font for the Windows 11 UI. This change makes me feel more at home and it still looks great with Gnome (sorry Ubuntu Sans). Seriously, not having to adjust my eyes to a new font is a huge win for me. https://github.com/mrbvrz/segoe-ui-linux One other piece of software that is absolutely great on Windows is XYplorer. It has a bunch of features, but one that I really miss is the ability to tag files with colors (similar to how macOS allows it in Finder). Any file manager recommendations? I've tried many but maybe I'm missing something important.
    1 point
  23. Thanks @szabesz, I should've quoted that part like you did, that would've been more meaningfully useful than what I provided. ?
    1 point
  24. How about this? https://processwire.com/modules/process-media-lister/
    1 point
  25. I solved this by creating a new field and saving it with a hook, essentially... if($page->template == "news-post") { $page->set('has_media', $page->image->count || $page->slideshow->count || !empty($page->video_embed)); } now I can just check if has_media=1 But I'm still curious why my old approach stopped working
    1 point
  26. Version 0.0.22 adds the ability to use the inputfield in module config fields (i.e. in a module's getModuleConfigInputfields method) but you need to create a measurement object and convert it to an array before it is saved. For example: public function getModuleConfigInputfields(InputfieldWrapper $inputfields) { $modules = $this->wire()->modules; ..... //Timeouts $m = $modules->get('FieldtypeMeasurement'); $f= $modules->InputfieldMeasurement; $field = new Field(); $field->setFieldtype($m); $field->set('quantity', 'Time'); $field->set('units', ['second', 'minute', 'hour', 'day']); $f->setField($field); $f->setPage($this->page); $f_name = 'sessionTimeOut'; $f->name = $f_name; $f->label = $this->_('Session Timeout'); $f->description = $this->_('Set the session timeout period'); $f->notes = $this->_('Max recommended = 1 day'); $f->columnWidth = 50; // Get the posted values $postedMagnitude = $this->input->post["{$f_name}_magnitude"]; $postedUnit = $this->input->post["{$f_name}_unit"]; if($postedMagnitude && $postedUnit) { // Create a new Measurement object $measurement = $m->measurement("Time", $postedUnit, $postedMagnitude); // Save the posted value $f->value = $m->measurement_to_array($measurement); $this->$f_name = $f->value; } else { // Set the value of the input field to the saved value $f->value = $m->array_to_measurement($this->$f_name); } $inputfields->add($f); } ..... }
    1 point
  27. I can't remember exactly but Ryan mentioned some time ago that in some situations it's not enough to set the template as string. ProcessWire usually converts that into a template object, but in the wrong context this does not work. When calling the method statically this seems to be the case. So just like wire()->pages->get() works for the parent you can also use wire()->templates->get(...) for the template instead of a plain template name. When using a function call like @Jonathan Lahijani does in the issue the context is different again and makes the code work.
    1 point
  28. I've created a GitHub issue here: https://github.com/processwire/processwire-issues/issues/1938
    1 point
  29. Thx! I knew this must be possible. I think I even saw this problem recently, but forgot ? .modal-buttons { display: flex; flex-wrap: wrap; gap: 5px 1rem; > button { flex: 1; min-width: fit-content; } } Solved that one as well! Im using an absolutely positioned (left-0 bottom-0 right-0) wrapper inside the button. There I use flex + flex-wrap so that elements stack on top of each other if there is not enough space. And then I use overflow-hidden on the button to hide overflowing words ?
    1 point
  30. From the comments of the linked article, there was a link to this information which covers end-of-life for v6, as well as the ability for open source projects to apply for a separate license for v7: https://github.com/tinymce/tinymce/discussions/9609
    1 point
  31. Here is a CSS-only solution to this exact problem from Kevin Powell:
    1 point
  32. Welcome to the ProcessWire forums! This is a great question and I think ProcessWire is a great platform to begin transitioning into OOP because ProcessWire itself is object oriented and is built using OOP. It includes powerful tools and features that can help make your code cleaner, more efficient, and reusable. I recommend starting with custom Page classes. Custom page classes lets you use OOP principles to extend ProcessWire and add additional custom behaviors by thinking with objects. There are a couple of examples in that link, but I'll provide one here that specifically contrasts different methods of doing the same thing. This example is a real-world case that I use on many projects, and because of how it's written I can replicate this feature easily when I start new projects. This is just a simple example of code I use that hopefully opens the door to thinking in OOP when working with ProcessWire. On blog posts I like to add something that shows how long it will take to read it, a la "5 minute read" like articles on Medium do. I wrote code that implements Medium's own method of calculating read time and use it often. The code that calculates the reading time is real, but I threw this together for illustration so please excuse any errors. Lets assume that you have a template called blog-post.php where you calculate reading time and output that value to the page. Here is what that looks like using procedural code: <?php namespace ProcessWire; // site/templates/blog-post.php // Calculate the read time for this article by using the words contained in the title, summary, and // body fields $text = "{$page->title} {$page->summary} {$page->blog_body}"; $blogText = explode( ' ', $sanitizer->chars($text, '[alpha][digit] ')); $wordCount = count($blogText); $dom = new \DOMDocument; @$dom->loadHTML( "<?xml encoding=\"UTF-8\"><div>{$blogText}</div>", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); // Account for images in text and add to read time // Starts at 12 seconds for first img, 11, 10, 9, etc. until floor of 3 secs $secondsPerImage = 12; $imageReadTimeInSeconds = 0; $imageCount = $dom->getElementsByTagName('img')->length; while ($imageCount > 0) { $imageReadTimeInSeconds += $secondsPerImage; $imageCount--; $secondsPerImage > 3 && $secondsPerImage--; } // Word count divided by average adult reading speed per minute with image read time added $readTime = (int) (ceil($wordCount / 275) + ceil($imageReadTimeInSeconds / 60)); ?> <!DOCTYPE html> <html lang="en"> <head> <title><?= $page->title; ?></title> </head> <body> <h1><?= $page->headline; ?></h1> <span class="read-time"><?= $readTime; ?> minute read</span> <div class="summary"> <?= $page->summary; ?> </div> <div class="blog-content"> <?= $page->blog_body; ?> </div> </body> </html> So, there's nothing wrong with that- gets the job done! But it could be better... It adds a lot of logic to our template and makes it harder to read, imagine if we had to add more logic for other features Mixing raw PHP and HTML works, but can be confusing when it comes to managing and maintaining our code We can't reuse the the code that calculates reading time, if we wrote this in another place then we have to make sure both are bug free and accurate Of course, we could create a function called readTime() that does the same thing and cleans up the template. But now we are writing functions that do a specific thing but exist without context and are harder to organize and maintain flexibility. Luckily, there's a better way. I'll let the notes by Ryan in that link I shared above explain how to start using custom Page classes, so I'll assume you have that set up. So now lets think in objects and use OOP to improve our code. Now we have our template, blog-post.php, and a custom Page class called BlogPostPage.php. Lets refactor. Here's our BlogPostPage.php file: <?php namespace ProcessWire; // site/classes/BlogPostPage.php class BlogPostPage extends Page { private const INITIAL_SECONDS_PER_IMAGE = 12; private const WORDS_READ_PER_MINUTE = 275; private const ALLOWED_CHARACTERS = '[alpha][digit] '; public function readTime(): int { $text = "{$this->title} {$this->summary} {$this->blog_body}"; $blogText = explode( ' ', wire('sanitizer')->chars($text, self::ALLOWED_CHARACTERS)); $wordCount = count($blogText); $dom = new \DOMDocument; @$dom->loadHTML( "<?xml encoding=\"UTF-8\"><div>{$blogText}</div>", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); $secondsPerImage = self::INITIAL_SECONDS_PER_IMAGE; $imageReadTimeInSeconds = 0; $imageCount = $dom->getElementsByTagName('img')->length; while ($imageCount > 0) { $imageReadTimeInSeconds += $secondsPerImage; $imageCount--; $secondsPerImage > 3 && $secondsPerImage--; } $time = ceil($wordCount / self::WORDS_READ_PER_MINUTE) + ceil($imageReadTimeInSeconds / 60); return (int) $time; } } Now that our code is in the context of a class method, we've made a couple of extra changes: $page->{name of field} is now $this->{name of field} because we're now working within BlogPostPage which extends the Page class itself. We've added constants to define values that would otherwise be a little difficult to understand at first glance. Seeing self::WORDS_READ_PER_MINUTE is clear and self-documenting where only using the integer 275 in place doesn't really state what that number means We switched $sanitizer to wire('sanitizer') because the $sanitizer variable does not exist in the method scope and the wire() function makes the entire ProcessWire API available to us both inside and outside classes ProcessWire will use BlogPostPage class to create the $page object we use in our templates when it boots, executes our code, and renders content via our templates. Thanks to OOP inheritance, BlogPostPage has all of the methods and properties available in the Page class and can be used in our templates with the $page object. And now let's go back to our blog-post.php template: <?php namespace ProcessWire; // site/templates/blog-post.php ?> <!DOCTYPE html> <html lang="en"> <head> <title><?= $page->title; ?></title> </head> <body> <h1><?= $page->headline; ?></h1> <span class="read-time"><?= $page->readTime(); ?> minute read</span> <div class="summary"> <?= $page->summary; ?> </div> <div class="blog-content"> <?= $page->blog_body; ?> </div> </body> </html> Now we're talking. With a little extra code and some OOP we've created a method on the Page object. Some benefits: Our template is cleaner and easier to maintain We've made the BlogPostPage class extend Page, so it inherits all of the methods and properties you access via $page The $page object is used to output the reading time to the page, just like $page outputs our field content, so our custom behavior is predictable and and feels at home with the core ProcessWire API It's easier to find where your programming logic is and keep a separation of concerns What's even better is that because we have used OOP to extend the Page class and add new functionality, we can use this a lot more places in our templates (so now it's reusable too). Let's say that you want to add a blog feed to the home page that shows the latest 3 blog posts and displays them with their title, read time, summary, and a link to read the post. <?php namespace ProcessWire; // site/templates/home.php ?> <!DOCTYPE html> <html lang="en"> <head> <title><?= $page->title; ?></title> </head> <body> <header> <h1><?= $page->headline; ?></h1> </header> <section class="blog-feed"> <!-- Create an <article> preview card for each blog post --> <?php foreach ($pages->get('template=blog-post')->slice(0, 3) as $blogPost): ?> <article> <h2><?= $blogPost->title; ?></h2> <span class="read-time"> <?= $blogPost->readTime(); ?> minute read </span> <div class="post-summary"> <?= $blogPost->summary; ?> </div> <a href="<?= $blogPost->url; ?>">Read More</a> </article> <?php endforeach ?> </section> </body> </html> That would be a lot harder to do if you had to write more procedural code to calculate the read time for each blog post. Thanks to that method in BlogPostPage, we can use it anywhere we reference a blog post. Think we can make this better? Let's improve it using PHP traits. Using a Trait will allow us to reuse our code that calculates reading time in many places thanks to OOP. We'll create another file called CalculatesReadingTime.php and put it in a new folder at /site/classes/traits. Time to refactor, here's our new trait file: <?php namespace ProcessWire; // site/classes/traits/CalculatesReadingTime.php trait CalculatesReadingTime { private const INITIAL_SECONDS_PER_IMAGE = 12; private const WORDS_READ_PER_MINUTE = 275; private const ALLOWED_CHARACTERS = '[alpha][digit] '; /** * Takes an arbitrary number of field values and calculates the total reading time * @param string $fieldValues Contents of fields to calculate reading time for * @return int Total read time, in minutes */ public function calculateReadingTime(string ...$fieldValues): int { $text = array_reduce( $fieldValues, fn ($content, $fieldValue) => $content = trim("{$content} {$fieldValue}"), '' ); $text = explode( ' ', wire('sanitizer')->chars($text, self::ALLOWED_CHARACTERS)); $wordCount = count($text); $dom = new \DOMDocument; @$dom->loadHTML( "<?xml encoding=\"UTF-8\"><div>{$text}</div>", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); $secondsPerImage = self::INITIAL_SECONDS_PER_IMAGE; $imageReadTimeInSeconds = 0; $imageCount = $dom->getElementsByTagName('img')->length; while ($imageCount > 0) { $imageReadTimeInSeconds += $secondsPerImage; $imageCount--; $secondsPerImage > 3 && $secondsPerImage--; } $time = ceil($wordCount / self::WORDS_READ_PER_MINUTE) + ceil($imageReadTimeInSeconds / 60); return (int) $time; } } Let's take a look at that before we go back to our other files. Our new trait has a name that is an "action" because now, as we'll see, we can add this ability to other classes so they can calculate reading times We've abstracted our code. Now instead of referring to content as $blogText, we are calling it $text because it can be used in many places and many contexts but provide the same behavior The readTime() method is now called calculateReadingTime() and has been converted to a variadic function so you can pass as many field values as needed We've taken an extra step of type hinting our parameters as strings to make sure the method is always getting the proper type of data to work with. This will help a lot as this method becomes used more in different places Our docblock is more robust to help understand what this method does, what parameters it takes, and what it will return, another extra step to help us as we use this method in more places Now back to our BlogPostPage.php file <?php namespace ProcessWire; // site/classes/BlogPostPage.php require_once __DIR__ . '/traits/CalculatesReadingTime.php'; class BlogPostPage extends Page { use CalculatesReadingTime; public function readTime(): int { return $this->calculateReadingTime($this->title, $this->summary, $this->blog_body); } } Now we're giving our BlogPostPage class the ability to calculate reading time and all of the functionality has been kept the same. We were able to abstract the logic for calculating reading time to a reusable trait that can be included in any Page class Our readTime() method still exists and is available to all of the templates where we were already using $page->readTime() Now our readTime() method calls the calculateReadingTime() method and passes the fields we know we need as they exist in our blog-post template This code is clean, concise, and easy to maintain. Right now it looks like OOP has made things look nice but caused some extra work... but then the phone rings. Your client wants to add Press Releases to the blog section of the site and it's going to need a new layout and different fields, but they love your reading time calculator so much that they want it on the new Press Release pages too. So we create our new files- a press-release.php template, and a PressReleasePage.php file. We'll skip writing out the press-release.php HTML, but while creating the new template in ProcessWire you've created new fields. The new fields are 'pr_abstract', 'pr_body', 'company_information', and 'pr_contact_info'. Here's our new PressReleasePage.php file: <?php namespace ProcessWire; // site/classes/PressReleasePage.php require_once __DIR__ . '/traits/CalculatesReadingTime.php'; class PressReleasePage extends Page { use CalculatesReadingTime; public function readTime(): int { return $this->calculateReadingTime( $this->pr_abstract, $this->pr_body, $this->company_information, $this->pr_contact_info ); } } Since we've created this simple class that uses the CalculatesReadingTime trait, we can use $page->readTime() in all of our Press Release pages, and anywhere that a Press Release $page object is present. Very nice. Now OOP has really shown how useful it is and we can appreciate how ProcessWire uses objects and provides tools for us extend that power with our own code. There's also some other OOP things happening here: Our BlogPostPage and PressReleasePage classes do one thing and one thing only: handle logic and features for their respective pages. So, they have a single responsibility The calculateReadingTime() and readTime() methods do one thing and one thing only, calculate reading time based on content. They only care about one thing and have no side effects. Our BlogPostPage and PressReleasePage clases can both calculate reading time, but other hypothetical page classes, like "HomePage" and "AboutUsPage" aren't required to have a readTime() method that isn't used, thanks to making use of traits to share behavior only where it's needed. So, that's composition over inheritance Our readTime() method does not expose how it calculates reading time and it provides an interface to only expose information we want our object to make available. So, readTime() is read-only and can safely be used knowing that the value will never be overwritten or modified except when content is changed by editing the page. This is a great tool that shows the difference between setting a value to $page->title and getting a value from readTime(), each have their purposes and roles. Our code is modular and easy to maintain. If we had to adjust how reading time was calculated- we could for example adjust the value of WORDS_READ_PER_MINUTE in our CalculatesReadingTime trait. Then all of our Press Releases and Blog Posts would have their reading time correctly calculated with one change. We can also add CalculatesReadingTime to any future page classes that need it. ProcessWire's strong OOP foundation and the way that it uses objects that are created from classes for everything (like $page, $config, $input, etc.) is the reason that the API is easy to work with, enjoyable, and powerful. If you get the hang of working with OOP in ProcessWire you can build even more powerful websites and applications, better understand the ProcessWire core code, and write your own modules (which is actually pretty fun). Wasn't sure of your overall exposure to OOP but hopefully this helps and inspires!
    1 point
  33. Hey @ceberlin thanks!!! I thought to do it anyway and add more functionalities in the next update. For now, change the function checkTextLength in SeoTextWidth.js // Text length checking and warning (updated) function checkTextLength(input, fieldConfig) { const span = $('<span class="detail"></span>').insertAfter(input); input.keyup(function() { const text = $(this).val(); const width = Math.round(calculateTextWidth(text, fieldConfig.font)); // Round to whole number if (width > fieldConfig.maxWidth) { span.html(`<i class="fa fa-exclamation-circle"></i> <span class="text-danger">Warning:</span> Text exceeds max width (${fieldConfig.maxWidth}px).`); span.addClass('text-danger'); } else { span.text(`Estimated Width: ${width}px`); // Display rounded value span.removeClass('text-danger'); } }); }
    1 point
  34. 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.
    1 point
  35. Funny you say that because I wrote a similar list as well. Here it is with some added commentary. As you can see I solved some things but listed them anyway and other things I didn't list because I solved them. general when clicking something in an unfocused window, the first click will focus the window; another click must be made to actually do something in the window I didn't realize how much this annoyed me. I thought Front and Center fixes this but it doesn't. finder (and Finder replacements) can't move or even remove the finder icon in the dock I don't want Finder to be first; to counteract, I've added a bunch of spacers so it's visually separated from my core programs (vscode, chrome, forklift, iterm) there is no "cut" option in finder (must copy, then use the move/paste with command + alt + v -- requires 2 hands) can't have custom categories in finder (just favorites, locations, icloud and tags) can't have the main folders load from a custom location like you can in windows; must use symlink instead this messes up how Finder search works hitting 'enter' on a file or folder does rename instead of launching the file / entering the folder (naturally, 'enter' seems like the right key to hit to ENTER a folder, not rename it) this means i have to use whatever the keyboard shortcut macOS wants you to use, and I don't remember what it is, but I remember I must use 2 hands -- ridiculous! the files list does not refresh automatically if a new file was written somewhere else (!) I use ForkLift as a replacement to Finder, but nothing in the macOS ecosystem comes even close to XYplorer (I used that for 13 years and it's updated constantly; it's like the ProcessWire of file managers... a Swiss Army Knife) mouse mouse wheel uses acceleration which makes sense for touchpads and mice without an actual WHEEL; it's not linear like you would expect counteract with https://github.com/emreyolcu/discrete-scroll the fact that macOS uses acceleration based scrolling for notch-wheel mice is ridiculous. that doesn't make any sense. macOS mouse movement physics is weird (at least compared to Windows) using outdated steelseries exactmouse to counteract that program is from 2010 (!); there's nothing more recent! no middle-click + scroll use "AutoScroll" extension in Chrome; it doesn't have the same feel like in Windows chrome can't open a new tab inside a chrome app like you can in windows; must first focus a regular chrome window other software Transmit (the SFTP client) doesn't support folder bookmarks (really wish it did so I don't have to use FileZilla; I don't want to use ForkLift for SFTP)
    1 point
  36. If you have the "Preset field values from GET variables?" setting checked you can use embed option B (Template Embed) and set field values in a second argument to $forms->embed() echo $forms->embed('your_form_name', [ 'vacancy' => $page->title, 'pageid' => $page->id ]);
    1 point
  37. bd() and d() - I've learned SO much about ProcessWire and PHP and OOP in general, can't thank you enough @adrian console - it's a dream to work with! request info panel - invaluable, especially when working with RockMigrations user switcher - handy refresh feature - modules refresh not moving away from current page That one is also great for working with RockMigrations: Usually I create a migrate() method that is triggerd on modules refresh. Then I can add a new field for example to a page and when I'm editing this page, I add the new field in code and just hit "modules refresh" via tracy and I end up on the same page edit screen just with a new field ready to be populated ? Oh and I've been using adminer for backup/restore lately quite often...
    1 point
  38. Well, I'm no pro at this and you could probably improve it, but here's my attempt which serves my current needs pretty well: /** * Creates a repeater field with associated fieldgroup, template, and page * * @param string $repeaterName The name of your repeater field * @param string $repeaterFields List of field names to add to the repeater, separated by spaces * @param string $repeaterLabel The label for your repeater * @param string $repeaterTags Tags for the repeater field * @return Returns the new Repeater field * */ public function createRepeater($repeaterName,$repeaterFields,$repeaterLabel,$repeaterTags) { $fieldsArray = explode(' ',$repeaterFields); $f = new Field(); $f->type = $this->modules->get("FieldtypeRepeater"); $f->name = $repeaterName; $f->label = $repeaterLabel; $f->tags = $repeaterTags; $f->repeaterReadyItems = 3; //Create fieldgroup $repeaterFg = new Fieldgroup(); $repeaterFg->name = "repeater_$repeaterName"; //Add fields to fieldgroup foreach($fieldsArray as $field) { $repeaterFg->append($this->fields->get($field)); } $repeaterFg->save(); //Create template $repeaterT = new Template(); $repeaterT->name = "repeater_$repeaterName"; $repeaterT->flags = 8; $repeaterT->noChildren = 1; $repeaterT->noParents = 1; $repeaterT->noGlobal = 1; $repeaterT->slashUrls = 1; $repeaterT->fieldgroup = $repeaterFg; $repeaterT->save(); //Setup page for the repeater - Very important $repeaterPage = "for-field-{$f->id}"; $f->parent_id = $this->pages->get("name=$repeaterPage")->id; $f->template_id = $repeaterT->id; $f->repeaterReadyItems = 3; //Now, add the fields directly to the repeater field foreach($fieldsArray as $field) { $f->repeaterFields = $this->fields->get($field); } $f->save(); return $f; } And here's an example of calling it: $f = $this->createRepeater("sc_promos","sc_promo_active sc_promo_code sc_promo_discount","Promotional Offer","shoppingCart"); You can then use $f to add your new repeater field to a fieldgroup/template.
    1 point
×
×
  • Create New...