Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 12/09/2023 in all areas

  1. This week I've continued work on the page versions support that I wrote about last week. While the main PagesVersions module needs more work before it's ready to commit to the dev branch and test externally, it is so far working quite well in internal testing. I mentioned last week how it will support an interface where Fieldtypes can declare that they will handle their own versions. That interface has been pushed to the dev branch as FieldtypeDoesVersions. I've also implemented it with the Repeater Fieldtype, which is visible starting here. Repeaters are so far working really well with versions! As far as core Fieldtypes go, Repeater, PageTable and FieldsetPage are likely the only ones that will need custom implementations. For ProFields, RepeaterMatrix already works with the implementation in the core Repeater (already tested). It's possible that other ProFields will not need custom implementations, though not yet positive. The module that provides version capability is called PagesVersions and the plan so far is to make it a core module that provides API version support for pages. A separate module provides interactive version support in the page editor. I've built this module initially so that I can test versions more easily, but it'll be expanded to provide a lot more. Below is a screenshot if what it looks like in the page editor Settings tab so far: As you can see, you can Edit or Restore any existing version. You can also create a new version from the Live version, or any other version. And of course you can view and delete any versions. When you Restore a version, it essentially replaces the live version with the one that you are restoring. All of this can be done from the module's API as well. Note that the API is provided by a $pagesVersions API variable that is present when PagesVersions module is installed. The API method names and such are a bit verbose right now but may be simplified before it's final. // Get page and add a new version of it $page = $pages->get(1234); $page->title = 'New title'; $version = $pagesVersions->addPageVersion($page); echo $version; // i.e. "2" // Get version 2 of a page $pageV2 = $pagesVersions->getPageVersion($page, 2); // Update a version of a page $pageV2->title = "Updated title"; $pagesVersions->savePageVersion($pageV2); // Restore version to live page $pagesVersions->restorePageVersion($pageV2); // Delete page version $pagesVersions->deletePageVersion($pageV2); Thanks for reading! More next week.
    4 points
  2. Damned, I have always thought that custom page classes were usable only in template.php files. ?‍♂️ This evening I wanted to hack PW to create a template/CustomPageClass mapping usable everywhere with PW API (get, find, etc), and in the first 3 minutes I found it was already there. ? This is huge, I love it and am already refactoring some code and twig templates with a custom UserPage, using Traits to share some implementation with my RawUser class (an optimized class from direct database query). I'm quite excited! ?
    3 points
  3. I've just uploaded a video about RockPdf to processwire.rocks ? Until 30.11. you can get the module for 49€ @ https://www.baumrock.com/processwire/module/rockpdf/ Here are extensive docs for the module: https://www.baumrock.com/en/processwire/modules/rockpdf/docs/
    1 point
  4. I want to show a new website that I made at the beginning of this year using @bernhard's RockPageBuilder module: https://www.kurrat-terrassendaecher.de/ The client: Kurrat Terrassendächer (which translates to "Kurrat Terrace Roofs") is a dealer from germany that is specialized in building custom made terrace roofings, awnings and solar protection. The goal: The customer had a old website that was used as a basis for the new design. The new website should offer a more "catalogue-like" look with lots of information for the customer. Fortunately the client had access to high quality images of the products that really make the whole website shine. The page features three main categories: 1. Terrace Roofs 3. Solar Protection 3. Winter Gardens Each category subpage is made of modular content blocks: The user is able to make custom page layouts using these blocks directly in the frontend. With the RockPageBuilder module it is super fun an super straight forward to work with these content blocks. If you don't know this module yet I highly recommend to check it out! If you like the RepaterMatrix you will love this module. It is far superior IMHO! Inserting a content block looks like this: It is also possible to edit content on-the-fly in the frontend: As with the RepeaterMatrix each content block an also be added end edited in the backend of the page: Here you see the list of content blocks used on the home page. To edit a block, just click on it and you can edit it just like using the RepaterMatrix. Screenshots from the website: Modules used for this page: - RockFrontend (for asset bundling, minifying, etc.) - RockPageBuilder (instead of RepeaterMatrix! For building and editing all content in the frontend) - WireMailSMTP - PageImageSource (for creating webp image variations) - PrivacyWire (for cookie consent banner) - SEO Maestro - Redirects The frontend framework is UIKit
    1 point
  5. Something I've wanted in ProcessWire for a long time is full version support for pages. It's one of those things I've been trying to build since ProcessWire 1.0, but never totally got there. Versioning text and number fields (and similar types) is straightforward. But field types in ProcessWire are plugin modules, making any type of data storage possible. That just doesn't mix well with being version friendly, particularly when getting into repeaters and other complex types. ProDrafts got close, but full version support was dropped from it before the first version was released. It had just become too much to manage, and I wanted it to focus just on doing drafts, and doing them as well as we could. ProDrafts supports repeaters too, though nested repeaters became too complex to officially support, so there are still some inherent limitations. I tried again to get full version support with a module called PageSnapshots developed a couple years ago, and spent weeks developing it. But by the time I got it fully working with all the core Fieldtypes (including repeaters), I wasn't happy with it. It was functional but had become too complex for comfort. So it was never released. This happens with about 1/2 of the code I write – it gets thrown out or rewritten. It's part of the process. What I learned from all this is that it's not practical for any single module to effectively support versions across all Fieldtypes in ProcessWire. Instead, the Fieldtypes themselves have to manage versions of their own data, at least in the more complicated cases (repeaters, ProFields and such). The storage systems behind Fieldtypes are sometimes unique to the type, and version management needs to stay internal [to the Fieldtype] in those cases. Repeaters are a good example, as they literally use other pages as storage, in addition to the field_* tables. For the above reasons, I've been working on a core interface for Fieldtypes to provide their own version support. Alongside that, I've been working on something that vaguely resembles the Snapshots module's API. But rather than trying to manage versions for page field data itself, it delegates to the Fieldtypes when appropriate. If a Fieldtype implements the version interface, it calls upon that Fieldtype to save, get, restore and delete versions of its own data. It breaks the complexity down into smaller chunks, to the point where it's no longer "complexity", and thus reasonable and manageable. It's a work in progress and I've not committed any of it to the core yet, but some of this is functional already. So far it's going more smoothly than past attempts due to the different approach. My hope is to have core version support so that modules like ProDrafts and others can eventually use that API to handle their versioning needs rather than trying to do it all themselves. I also hope this will enable us to effectively version the repeater types (including nested). I'm not there yet, but it's in sight. If it all works out as intended, the plan is to have a page versions API, as part of the $pages API. I'll follow up more as work continues. Thanks for reading and have a great weekend!
    1 point
  6. @Juergen wow, that was quick! ? Installed it and everything is working as expected now. Thank you very much! ?
    1 point
  7. Sometimes, you want to search a text field by the length of its content. FieldtypeText doesn't provide this functionality (yet). It's possible to add the necessary SQL to a query by hooking into PageFinder::getQuery like @bernhard pointed out here, but that's of course not really straight forward. There's also an open feature request for FieldtypeText in processwire-requests. FieldtypeTextWithLength is an extension of FieldtypeText, so you can switch your field's type between the two without loosing any information. After switching your field to "Text with .length Selector", you can search by the (character, not bytes!) length of the content: $pagesWithShortHeadlines = $pages->find('headline.length<10'); You will also find a new "Length" subfield when using Pages -> Find in the PW backend:
    1 point
  8. Hello, I would really appreciate your comments on this blog post I am preparing for our site. =========== Creating a dynamic search with very little code in ProcessWire is easy. This search cannot compete with engines like Elasticsearch, Solr, and others. However, it is suitable for most "showcase" sites. Here is how we did it on the Spiria site using the small library htmx and its companion hyperscript. The goal The recipe Inclusion of libraries htmx and hyperscript (the latter is optional). A field of type textarea integrated to the page models that we want to index. A code for indexing the existing content in the file `ready.php` A search controller which we name here `api.php`. This controller will also be a page with the `api` template. A form placed in the pages requiring the search. Content indexing Before we can program, we need to index the content on which we want to apply our search. In my proof of concept, I have developed two strategies. This is probably overrated, because I am not sure of the gain in speed. Index for a single term search Indexing for a multiple term search To do this, we need to introduce two fields in each model where we want an indexation. The `search_text` field which will contain only one occurrence of each word on a page. The `search_text_long` field which will preserve all sentences without HTML tags. We place a hook in the `ready.php` page in this way: <?php namespace ProcessWire; pages()->addHookAfter("saveReady", function (HookEvent $event) { $p = $event->arguments[0]; switch ($p->template->name) { case "blog_article": $french = languages()->get('fr'); $english = languages()->get('default'); $txt_en = $p->page_content->getLanguageValue($english) . ' ' . $p->blog_summary->getLanguageValue($english); $txt_fr = $p->page_content->getLanguageValue($french) . ' ' . $p->blog_summary->getLanguageValue($french); $title_en = $p->title->getLanguageValue($english); $title_fr = $p->title->getLanguageValue($french); $resultEn = stripText($txt_en, $title_en); $resultFr = stripText($txt_fr, $title_fr); $p->setLanguageValue($english, "search_text", $resultEn[0]); $p->setLanguageValue($english, "search_text_long", $resultEn[1]); $p->setLanguageValue($french, "search_text", $resultFr[0]); $p->setLanguageValue($french, "search_text_long", $resultFr[1]); break; } }); And function stripText($t, $s) { $resultText = []; $t = strip_tags($t); $t .= " " . $s; $t = str_replace("\n", " ", $t); $t = str_replace(",", "", $t); $t = str_replace("“", "", $t); $t = str_replace("”", "", $t); $t = str_replace("'", "", $t); $t = str_replace("?", "", $t); $t = str_replace("!", "", $t); $t = str_replace(":", "", $t); $t = str_replace("«", "", $t); $t = str_replace("»", "", $t); $t = str_replace(",", "", $t); $t = str_replace(".", "", $t); $t = str_replace("l’", "", $t); $t = str_replace("d’", "", $t); $t = str_replace("&nbsp;", "", $t); $t = preg_replace('/\[\[.*\]\]/', '', $t); //$t = preg_replace('/\?|\[\[.*\]\]|“|”|«|»|\.|!|\&nbsp;|l’|d’|s’/','',$t); $arrayText = explode(" ", $t); foreach ($arrayText as $item) { if (strlen(trim($item)) > 3 && !in_array($item, $resultText)) { $resultText[] = $item; } } return [implode(" ", $resultText), $t]; } If you have the ListerPro module, it becomes easy to save all the pages to be indexed in batch and any new page will then be indexed as it is created. The `stripText()` function aims at cleaning up the text as we want. Note that, in my example, I distinguish between French and English. This little algorithm is entirely perfectible! I have commented a shorter way to clean up the text, but at the expense of comprehension. As mentioned before, it is probably unnecessary to create two search fields. The most important thing would be to optimize the text as much as possible, as many small words are useless. The current code restricts to words longer than three characters, which is tricky in a computing context like our site where names like `C#`, `C++`, `PHP` compete with `the`, `for`, `not`, etc. That said, perhaps this optimization is superfluous in the context of a simple content and not and limited in number. So now see the process and the research code. The structure structure.svg This scheme is classical and needs few explanations. The `htmx` library allows a simple Ajax call. The form code01.svg The form has a `get` method that returns to a conventional search page when the user presses the `enter` key. A hidden field with the secret key generated on the fly enhances security. The third field is the `input` of the dynamic search. It has a `htmx` syntax. The first command, `hx-post`, indicates the method of sending the data to the API. Here, it is a `post`. `htmx` allows to handle events on any DOM element. So we could have several calls on different elements of a form. The second line indicates where the API response will be sent when received, which is `div#searchResult` below the form. The `hx-trigger` command describes the context of sending to the API. Here, when the user releases a key, with a delay of 200 ms between each reading of the event. The `hx-indicator` command is optional. It tells the user that something is happening. In the example, the `#indexsearch` image is displayed. This is automatically handled by htmx. We have the possibility to pass other parameters to the search with the `hx-vals` command. The given example is simplified. We send the search language. The last command comes from the `hyperscript` syntax. It indicates that if you click anywhere outside the search field, you will make the contents of `#found` disappear, which will be described below. It is clear from this example that no javascript is called, except the [htmx] and [hyperscript] libraries. It is worth visiting the web site of these two libraries to understand their philosophy and possibilities. The Search API The API resides in a normal ProcessWire page. Although it is published, it remains "hidden" from CMS searches. This page allows requests to be answered and the correct functions to be called. Several requests to the CMS can be gathered in this type of page. <?php namespace ProcessWire; $secretsearch = session()->get('secretToken'); $request = input()->post(); $lang = sanitizer()->text($request["lang"]); if (isset($request['CSRFTokenBlog'])) { if (hash_equals($secretsearch, $request['CSRFTokenBlog'])) { if (!empty($request["search"])) { echo page()->querySite(sanitizer()->text($request["search"]),$lang); } } else { echo __("A problem occurred. We are sorry of the inconvenience."); } } exit; In this case : We extract the secret token of the session, token that will have been created in the search form page. We then process everything that is in the `post` query. Note that this is a simplified example. We compare the token with the one received in the query. If all goes well, we call the SQL query. Our example uses a class residing in `site/classes/ApiPage.php`; it can therefore be called directly by `page()`. Any other strategy is valid. The following code represents the core of the process. <?php namespace ProcessWire; public function querySite($q, $l) { $this->search = ""; $this->lang = $l == 'en' ? 'default' : 'fr'; user()->setLanguage($this->lang); $whatQuery = explode(" ", $q); $this->count = count($whatQuery); if ($this->count > 1) { $this->search = 'template=blog_article,has_parent!=1099,search_text_long~|*= "' . $q . '",sort=-created'; } elseif (strlen($q) > 1) { $this->search = 'template=blog_article,has_parent!=1099,search_text*=' . $q . ',sort=-created'; } if ($this->search !== "") { $this->result = pages()->find($this->search); return $this->formatResult(); } return ""; } protected function formatResult() { $html = '<ul id="found">'; if (count($this->result) > 0) { foreach ($this->result as $result) { $html .= '<li><a href="' . $result->url . '">' . $result->title . '</a></li>'; } } else { $html .= __('Nothing found'); } $html .= '</ul></div>'; return $html; } The `formatResult()` function is simple to understand and it is here that we see the `ul#found` tag appear which, remember, is deleted by the _hyperscript_ line of the form. _="on click from elsewhere remove #found" Actually, I've commented out this line in the production code so far. Instead of clicking anywhere, it would be better to add a background behind the form when doing a search in order to focus the `click` event. But again, all strategies are good! In the current code, it is not necessary to add CSS to display the result. It is placed in an empty `#searchResult` tag, so it is invisible at first. As soon as it is filled by the search result, everything becomes accessible, the CSS being targeted on the `ul#found` list and not on its parent. Conclusion The purpose of this article was to demonstrate htmx and the possibilities that this library allows. There is already an excellent search module for ProcessWire, SearchEngine, which can coexist very well with the code described here.
    1 point
×
×
  • Create New...