-
Posts
240 -
Joined
-
Last visited
-
Days Won
9
Everything posted by d'Hinnisdaël
-
Generate image placeholders for smoother lazyloading. Currently supports ThumbHash, BlurHash, and average color placeholders. I've been using the wonderful ImageBlurhash module for this in the past, but unfortunately it's no longer in active development. This new module adds ThumbHash and Average Color placeholder algorithms, improves performance by caching generated placeholders, fixes an issue when replacing images, and allows regenerating and clearing placeholders via the admin interface. Try it out using the installation instructions below or check out the GitHub repo for details. Why use image placeholders? Low-Quality Image Placeholders (LQIP) are used to improve the perceived performance of sites by displaying a small, low-quality version of an image while the high-quality version is being loaded. The LQIP technique is often used in combination with progressive lazyloading. How it works This module will automatically generate a small blurry image placeholder for each image that is uploaded to fields configured to use them. In your frontend templates, you can access the image placeholder as a data URI string to display while the high-quality image is loading. See below for markup examples. Placeholder types The module supports generating various types of image placeholders. The recommended type is ThumbHash which encodes most detail and supports transparent images. ThumbHash is a newer image placeholder algorithm with improved color rendering and support for transparency. BlurHash is the original placeholder algorithm, developed at Wolt. It currently has no support for alpha channels and will render transparency in black. Average color calculates the average color of the image. Installation Install the module using composer from the root of your ProcessWire installation. composer require daun/processwire-image-placeholders Open the admin panel of your site and navigate to Modules → Site → ImagePlaceholders to finish installation. Configuration You'll need to configure your image fields to generate image placeholders. Setup → Fields → [images] → Details → Image placeholders There, you can choose the type of placeholder to generate. If you're installing the module on an existing site, you can also choose to batch-generate placeholders for any existing images. Usage Accessing an image's lqip property will return a data URI string of its placeholder. $page->image->lqip; // data:image/png;base64,R0lGODlhEAAQAMQAA Accessing it as a method allows setting a custom width and/or height of the placeholder. $page->image->lqip(300, 200); // 300x200px Markup Using a lazyloading library like lazysizes or vanilla-lazyload, you can show a placeholder image by using its data URI as src of the image. <!-- Using the placeholder as src while lazyloading the image --> <img src="<?= $page->image->lqip ?>" data-src="<?= $page->image->url ?>" data-lazyload /> Another technique is rendering the placeholder and the original image as separate images on top of each other. This allows smoother animations between the blurry unloaded and the final loaded state. <!-- Display placeholder and image on top of each other --> <div class="ratio-box"> <img src="<?= $page->image->lqip ?>" aria-hidden="true"> <img data-src="<?= $page->image->url ?>" data-lazyload> </div>
- 19 replies
-
- 19
-
-
-
@markus_blue_tomato I'm also thinking of adding support for alternative hashing methods, specifically ThumbHash. The colors with ThumbHash seem to be a bit closer to the original and it also supports alpha channels. Would you be willing to accept a PR for that? I'd love to avoid creating a separate module, as the core logic would just be duplicated. Adding a new setting is much more elegant and allows experimenting with both hashing methods more easily. If it helps and it's possible, I'd be willing to take over maintenance of the module and take it from there.
- 37 replies
-
- 2
-
-
- image
- lazy loading
-
(and 2 more)
Tagged with:
-
Hi @markus_blue_tomato I've finally come around to investigating the issue where newly uploaded images would keep the old blurhash. I have a working backward-compatible fix ready in a fork, but I see that the module itself has been marked as readonly for a while and I can't create any PRs against it. Would you accept this fix via some other means?
- 37 replies
-
- 2
-
-
- image
- lazy loading
-
(and 2 more)
Tagged with:
-
Thanks for reporting the issue; I've pushed a fix as 1.0.6. Updating the composer package should fix it: composer update daun/template-engine-latte
-
@maetmar When rendering templates using Latte, I've found the built-in field rendering a bit cumbersome to set up since you'll need an intermediary PHP file to render the Latte view from. The straight-forward version would be using a loop and including a file based on the matrix type. This will look for a view partial at site/templates/views/partials/blocks/image.latte, when rendering a matrix page of the type "image" in a matrix field named "blocks": {foreach $page->blocks as $block} <div class="block" data-block="{$block->type}"> {include 'partials.blocks.' . $block->type, block => $block} </div> {/foreach}
-
@artfulrobot Great catch. Could you repost this in the support thread of the main TemplateEngineFactory module? I have no control over either that module or its repository; I merely created an integration for it adding support for the Latte language.
-
Add support for the Latte templating language to the Template Engine Factory. Available on the module directory and on GitHub. The latest version uses Latte v3. Configuration The module offers the usual the following configuration: Template files suffix The suffix of the Latte template files, defaults to latte. Default layout file Layout that all views will extend from unless overwritten. Provide ProcessWire API variables in Latte templates API variables ($pages, $input, $config...) are accessible in Latte, e.g. {$config} for the config API variable. Simplified path resolution Enable Blade-style dot syntax for directory traversal, e.g. partials.navigation instead of ../../partials/navigation.latte Auto refresh templates (recompile) Recompile templates whenever the source code changes.
-
module ProcessWire Dashboard
d'Hinnisdaël replied to d'Hinnisdaël's topic in Module/Plugin Development
I‘m not familiar with RockGrid but if I understand you correctly it can export pages? In that case, using a Collection panel should get you there. However I suspect what you‘re looking for is showing the RockGrid UI inside a panel — in that case you could look into rendering a template file inside a panel and implementing the custom logic there. -
@netcarver Yes, I've created a gist that should do.
-
@netcarver I've been using Latte in Laravel almost exclusively. Blade is great, but Latte is even better. There's a composer package for integrating Latte, but it was out of date when I last checked so I wrote my own which was quite straightforward. Interestingly, you can mix and match, calling Blade views from Latte and vice versa, which is a lifesaver when dealing with vendor files and third-party packages.
-
module ProcessWire Dashboard
d'Hinnisdaël replied to d'Hinnisdaël's topic in Module/Plugin Development
The PageList panel uses ProcessWire's internal ProcessPageList component. As far as I know, it works by displaying the actual children of a parent page and doesn't support rendering arbitrary pages that aren't siblings. -
Did you run into any breaking changes upgrading from Latte 2 to 3? I haven't yet updated the Latte renderer for TemplateEngineFactory as I haven't had enough time to give Latte 3 a go.
-
And a few macros as well: <?php use Latte\Compiler; use Latte\MacroNode; use Latte\Macros\MacroSet; use Latte\PhpWriter; /** * Latte macro provider * */ final class Macros extends MacroSet { /** * Install available macros. */ public static function install(Compiler $compiler): void { $me = new static($compiler); $me->addMacro('ifispage', [$me, 'macroIsPage'], '}'); $me->addMacro('icon', [$me, 'macroSvgIcon']); $me->addMacro('trim', '', [$me, 'macroTrimEnd'], null); $me->addMacro('minify', [$me, 'macroMinifyHtml'], [$me, 'macroMinifyHtmlEnd'], null, self::ALLOWED_IN_HEAD); } /** * {ifispage $page} */ public function macroIsPage(MacroNode $node, PhpWriter $writer) { return $writer->write(' $isObj = %node.word && is_object(%node.word); $isCustomPage = $isObj && is_a(%node.word, "ProcessWire\DefaultPage"); $isCorePage = $isObj && get_class(%node.word) === "ProcessWire\Page"; $isPage = ($isCustomPage || $isCorePage) && %node.word->id; if ($isPage) { '); } /** * {icon 'search'} */ public function macroSvgIcon(MacroNode $node, PhpWriter $writer) { return $writer->write(' $icon = %node.word; $ratio = "xMinYMid"; $ratioAttr = $ratio ? "preserveAspectRatio=\"{$ratio}\"" : ""; $html = "<svg class=\'icon icon-{$icon}\' {$ratioAttr} aria-hidden=\'true\'><use xlink:href=\'#icon-{$icon}\' /></svg>"; echo $html; '); } /** * n:trim */ public function macroTrimEnd(MacroNode $node, PhpWriter $writer) { $node->validate(false); $node->openingCode = "<?php ob_start(); ?>"; $node->closingCode = '<?php $__output = ob_get_clean(); ?>'; if ($node->prefix === MacroNode::PREFIX_INNER) { // Simple case: n:inner-trim -> just trim content $node->closingCode .= '<?php $__output = trim($__output); ?>'; } else { // Trickier case: n:trim // remove whitespace *inside* outer tags // while preserving whitespace *outside* of outer tags // Before: " <h1> Title </h1> " // After: " <h1>Title</h1> " $node->closingCode = $node->closingCode . // Remove whitespace after opening tag '<?php $__output = preg_replace(\'~^(\s*<[^>]+>)\s+~s\', "\$1", $__output); ?>' . // Remove whitespace before closing tag '<?php $__output = preg_replace(\'~\s+(</[^>]+>\s*)$~s\', "\$1", $__output); ?>'; } $node->closingCode .= '<?php echo $__output; ?>'; } /** * {minify} */ public function macroMinifyHtml(MacroNode $node, PhpWriter $writer) { return $writer->write(' ob_start(function ($s, $phase) { // 0: replace newlines //$html = preg_replace("/\r\n|\r|\n/", "", $s); // 1: remove whitespace from between tags that are not on the same line. $html=preg_replace(\'~>\s*\n\s*<~\', \'><\', $s); // 2: replace all repeated whitespace with a single space. static $strip = true; $html=LR\Filters::spacelessHtml($html, $phase, $strip); return $html; }, 4096); '); } /** * {/minify} */ public function macroMinifyHtmlEnd(MacroNode $node, PhpWriter $writer) { return $writer->write(' ob_end_flush(); '); } }
-
Sure! Here's a few that might be of use to ProcessWire sites: <?php use Latte\Engine; /** * Latte filter provider * */ final class Filters { /** * Install available filters. */ public static function install(Engine $latte) { foreach ((new static())->provide() as $name => $callback) { $latte->addFilter($name, $callback); } } /** * @return array<string, callable> */ public function provide(): array { return [ // Sanitize values using ProcessWire's sanitizer API variable 'sanitize' => function ($value, $sanitizer, $options = null) { if (!$options) { return sanitizer()->$sanitizer($value); } else { $args = func_get_args(); unset($args[1]); // remove $sanitizer arg return call_user_func_array([sanitizer(), $sanitizer], array_values($args)); } }, // Render FormBuilder form, but allow prepending and appending fields 'form' => function ($form, $options = []) { if (!$form) { return ''; } $output = $form->render(); if ($options['append'] ?? false) { $output = str_ireplace('</form>', $options['append'], $output); } if ($options['prepend'] ?? false) { if (stripos($output, '<form') !== false) { $output = preg_replace('#(<form[^>]*>)#i', '\\1' . $options['prepend'], $output); } else { $output = $output . $options['prepend']; } } return $output; }, // URL slug / page name 'slug' => function ($str, $length = 128) { $str = sanitizer()->unentities($str); $str = sanitizer()->pageNameTranslate($str, $length); return $str; }, // Truncate string 'truncate' => function ($str, $length = 200) { return sanitizer()->truncate($str, $length, ['visible' => true]); }, // Render Markdown 'markdown' => function ($str) { $str = "{$str}"; modules()->get("TextformatterMarkdownExtra")->formatValue(new Page(), new Field(), $str); return $str; }, // Unwrap ProcessWire value objects 'value' => function ($object) { if (is_object($object)) { if ($object instanceof SelectableOptionArray) { return $object->implode(' ', 'value'); } else { return $object->value ?? ''; } } elseif (is_array($object)) { return implode(' ', $object); } else { return (string) $object; } }, // Join a string with a custom separator at the end 'join' => function ($list, $separator = ', ', $lastSeparator = ' & ') { if (count($list) > 1) { $last = array_pop($list); return implode($separator, $list) . $lastSeparator . $last; } else { return implode($separator, $list); } }, // Prettify URL by removing protocol and www 'prettyUrl' => function ($url, $options) { if (null === $url) { return false; } $url = trim($url); if ($options['www'] ?? true) { $url = str_replace('www.', '', $url); } if ($options['http'] ?? true) { $url = str_replace(array('https://', 'http://'), '', $url); } if ($options['slash'] ?? true) { $url = rtrim($url, '/'); } return $url; }, ]; } }
-
Latte is great indeed! I've been using it for all projects since discovering it. Everything is so much more concise and elegant, especially once your start building your own filters and macros. Quick note that you can also use Latte with Wanze's TemplateEngineFactory. I've found that the easiest way to get started; one composer install and you're ready to go ?
-
module ProcessWire Dashboard
d'Hinnisdaël replied to d'Hinnisdaël's topic in Module/Plugin Development
I'd love to hear how you accomplished the filtering part — I'm assuming you've built a panel emitting custom events that trigger panel reloads. Or is it plain old get params? -
module ProcessWire Dashboard
d'Hinnisdaël replied to d'Hinnisdaël's topic in Module/Plugin Development
If you create a panel group and add the tabs to that, it should work. -
module ProcessWire Dashboard
d'Hinnisdaël replied to d'Hinnisdaël's topic in Module/Plugin Development
Yeah, the migration guide looks pretty daunting. In that case I prefer leaving it as-is for now. If you find a simple fix, let me know and I'll happily review/merge any input. -
module ProcessWire Dashboard
d'Hinnisdaël replied to d'Hinnisdaël's topic in Module/Plugin Development
Realistically speaking, I won't get to refactoring the chart panel until later this year. Updating the chart.js library in the meantime sounds good, though. Do you happen to know if the update contains any breaking changes that would apply to its use in the context of this dashboard? I haven't been actively following along and I'd like to avoid breaking people's existing dashboards if possible. -
module ImageOptim — automatic image optimization
d'Hinnisdaël replied to d'Hinnisdaël's topic in Modules/Plugins
Yes, I have it working in production on a few up-to-date sites. If you find it doesn't work for some reason, feel free to raise the issue here or on GitHub. -
module ProcessWire Dashboard
d'Hinnisdaël replied to d'Hinnisdaël's topic in Module/Plugin Development
Sounds good, happy to integrate that if you submit a pull request on the GitHub repo. -
module ProcessWire Dashboard
d'Hinnisdaël replied to d'Hinnisdaël's topic in Module/Plugin Development
Good catch. Now that I think about it, returning all repeaters makes sense in the admin context — otherwise you wouldn't be able to edit all repeater items on page edit screens. But as you said, you'll have to remember to properly mark/mask unpublished repeaters yourself. -
module ProcessWire Dashboard
d'Hinnisdaël replied to d'Hinnisdaël's topic in Module/Plugin Development
@wbmnfktr I just created a quick test case and you're right, ProcessWire seems to have access checks disabled for repeaters in this context. Output formatting seems to be turned on, however. Is this usually the case in the admin? -
Another good option for partial HTML updates is morphdom which transforms the existing dom nodes to match the new incoming HTML without discarding any elements. That way, any dom events, scroll positions or css transition states will be kept on the existing elements. No specific markup required, nor changes to how you set up dom events. It's used in Phoenix LiveView which is pretty close to our use case (live updates from server-rendered templates).
-
Untested code, but along those lines: wire()->addHookAfter('ProcessPageEdit::buildForm', null, function (HookEvent $event) { $form = $event->return; $statusField = $form->find("name=status")->first(); if ($statusField) { $statusField->collapsed = Inputfield::collapsedHidden; } });