Jump to content


  • Posts

  • Joined

  • Last visited

  • Days Won


Everything posted by gornycreative

  1. Yes. I actually tie this in with custom page classes, because often the page class matches a structured content type. When I am building a site that is using a repeater block system, there are three 'tracks' of data that get pulled in: structured data, page data and presentation data. Usually structured data is a php file that outputs json. Page data is a php file that does whatever calculations I need for a content block and passes a special latte variable into the latte variable array. Presentation data ties together the structured data and bring in a special computed variable from the latte variables I have set. This way the structured data logic, data cleaning prep logic and presentation logic remain separate.
  2. Hello all, I was running into an issue where a particular repeater matrix structure failed to index properly. In a structure like this: Template Repeater Matrix Field Matrix Type Page Field Page Id Name Title Headline Body Summary So where you have a page template with 'zones' that allow you to place a matrix type that acts like a block with a page selection field in it to insert a piece of content, that deeper piece of content was not getting properly indexed no matter what indexed fields you set in place. In SearchEngine/lib/Index.php within the function __getFieldIndex there is a section that overrides the $indexed_fields array with a hard-coded array. In the version below I have commented it out and restored the $indexed_fields array and the referenced child page content for Repeater Matrix Types now indexes properly: /** * Get index for a single field * * @param \ProcessWire\Field $field * @param \ProcessWire\WireData $object * @param array $indexed_fields * @param string $prefix * @param array $args * @return array */ protected function ___getFieldIndex(\ProcessWire\Field $field, \ProcessWire\WireData $object, array $indexed_fields = [], string $prefix = '', array $args = []): array { $index = []; if ($this->isRepeatableField($field)) { $index = $this->getRepeatableIndexValue($object, $field, $indexed_fields, $prefix); } else if ($field->type->className() == 'FieldtypeFieldsetPage') { $index = $this->getPageIndex( $this->getUnformattedFieldValue($object, $field->name), $indexed_fields, $prefix . $field->name . '.', $args ); } else if ($field->type instanceof \ProcessWire\FieldtypePage) { // Note: unlike with FieldtypeFieldsetPage above, here we want to check for both FieldtypePage // AND any class that might potentially extend it, which is why we're using instanceof. /** $index = $this->getPageReferenceIndexValue($object, $field, [ 'id', 'name', 'title', ], $prefix); */ $index = $this->getPageReferenceIndexValue($object, $field, $indexed_fields, $prefix); } else { $index_value = $this->getIndexValue($object, $field, $indexed_fields); $index[$prefix . $field->name] = $index_value->getValue(); foreach ($index_value->getMeta(true) as $meta_key => $meta_value) { $meta_value = explode(':', $meta_value); $index[self::META_PREFIX . $meta_key . '.' . $field->name . '.' . array_shift($meta_value) . ':'] = implode(':', $meta_value); } } return array_filter($index); } I don't know if this was set in place for performance reasons, but if you are using a page template structure with page fields you may have run into this problem where in some cases only the content titles is included in the search_index output.. This change does extend the indexing timeframe, but pages are indexed fully.
  3. I will add it to the pile for more testing. I don't see the original problem stated in the question, just my weird related one? And I have been running the snippet in createField from our first discussion - so quite some time now.
  4. I'm not sure. The only time I get a Field returned sometimes is if I run a setMatrixItems call from ready.php and I'll get a Field::setMatrixItems does not exist or is not callable in this context error.
  5. Hey @gebeer remember this code that we added in our DMs? // this will auto-generate the repeater matrix template if ($field->type instanceof FieldtypeRepeaterMatrix) { $field->type->getMatrixTemplate($field); } elseif ($field->type instanceof FieldtypeRepeater) { $field->type->getRepeaterTemplate($field); } It didn't make it into the final PR I don't think. We inserted this at the end of createField. The original (and currently live version) only works for returning a FieldtypeRepeater I think. https://github.com/baumrock/RockMigrations/blob/be40ee6ae51a9f9f3ab9a9622d3acc12e069df5e/RockMigrations.module.php#L1092
  6. I believe SearchEngine is able to index content within repeaters and add it to the searchindex field it creates. https://processwire.com/modules/search-engine/ It can also return JSON so it may be a drop in alternative or work side-by-side with the FieldtypeCache? Regarding repeater detection and indexing:
  7. So my cap is set to 2.44MB/s right now for I/o But I feel like I hit that a lot! My hosting is supposed to provide 15 MB/s for the service tier I have. These sets of numbers don't add up! I put in a ticket. What I/o disk throughput is typical for a production install with caching?
  8. I'm trying to troubleshoot a rough bottleneck I haven't dealt with before. I've got extreme i/o spikes coming from pw/index.php and ajax calls are unusably slow. I've been building out a template system that makes heavy use of RepeaterMatrix. I've spent the last few days loading a pretty large number of matrixtypes that have a few fields each. I've turned off prodrafts and rockmigrations, trying to strip things down to a point where I can get reasonable speed out of it. I thought it might have been some of the plugins I installed recently (Media Lister, Dynamic Selects) but removing them didn't help too much. I'm used to dealing with CPU issues, not i/o I am not entirely sure how to go about it. The slowdown seems to be mostly on the backend.
  9. Recently updated, alpha channels were rounding to integer rather than a float - now fixed. With the right arrangement, stacked gradients are possible.
  10. I don't use a lot of JS in new stuff, it's the already existing stuff - especially old stuff I've done where I was sloppy and used things without necessarily understanding the ins and outs - a lot of that I have tried to transition into vanilla JS just because jquery was total overkill, but I still run into it often enough. using gen ai is a good idea though. I bet it does a nice job of suggesting alternatives if I want to pop jquery out too.
  11. So it's a funny thing. I started using javascript a lot when I was still doing 'classic' ASP because it performed better than vbscript server-side, and then I shifted over into PHP around the time when Jquery was picking up speed, and then I got back into javascript when jquery was starting to get unpopular (at least here in the US it went through an extended grumble phase) and so I was doing a lot of vanilla js stuff and then with node, react/vue etc... My jquery knowledge is not that great. I know enough to do repairs and some debug but now as I start to work more with client side stuff in PW I need to sharpen up. Is there a faster track than the straight docs anyone would recommend?
  12. This is really helpful for folks who are used to having a single place to review imagery as found in other systems. Thanks!
  13. Can someone please make a documentation page about this file - just a short one - under Docs > Fields, types and input? I feel stupid for having wasted my time solving for this problem, and wasting even more time writing it up and trying to be helpful, when everyone has demonstrated it was entirely unnecessary. There's an entire area dedicated to the PHP API and literally nothing for this.
  14. Okay I've looked all over documentation and asked the web and I can't find anything on .on('reloaded', what it specifically does or how it works. How about a hint? EDIT: Never mind - inputfields.js - got it.
  15. Odds are pretty good that you have had to flip this switch: Because you wanted to use a field that uses javascript to stuff some sort of interface feature into the DOM. If you have the setting above flipped to anything but Off you know that the initialization methods of most javascript tools can only pop into place if the basic structure for the field already exists on the page. If the structure isn't there because either a Repeater item hasn't been added, or it hasn't been opened and AJAX is enabled, there is nothing in the DOM for your script to stuff. For the purposes of this conversation, Repeater and Repeater Matrix Inputfields function in the same way and create the same problem. There has been talk of setting up a global set of events and handlers that would provide signals that scripts could listen to. While that conversation continues, I decided I wanted to at least try to put together a design pattern that would allow these kinds of fields to instantiate with repeaters under any circumstance: The field exists on a page edit form without any complications - just the field by itself. The field exists in a repeater template fieldgroup, and that Repeater has no rows yet - they need to get added. The field exists in a repeater template fieldgroup, and the rows exists but they are closed. Case 2 and 3, if you are using the recommended switch setting, do not load the underlying inputfield structure until their state shifts from closed to open. Once they are open, the form fieldset structure loads, but the script calls that you have already made are done - the fields don't work. What sort of design pattern do we need to solve this problem? We need our inputfield/fieldtype module to load the appropriate javascript and css files to bring the methods we need to initialize the filed instances. We need to know if our fieldtype is on a page, and if it will show up in the markup for the Page Edit form. We need to know if there are Repeater fields on a page, if they include our field, and how we can specifically identify them. We need to be able to pass a list of these fields to the admin page on the client side. We need the admin page to initialize the fields it can see, and be ready to initialize fields that it can't see yet. Once these tasks are all accomplished, we will have a JS powered inputfield that works seamlessly whether it is flying solo on a page, or part of a Repeater field structure - whether open at load, closed at load, or brand new. There are parts that both the Fieldtype and Inputfield modules have to play. You will need to use both to get this job done. Most of the heavy lifting is done in the Inputfield portion, but I'll address each file separately as we look at each step. Loading the assets First of all, I'm assuming that you have got your extra module assets for whatever library you are working on in a vendor subfolder beneath your module directory. You could have several packages. Here's how I load assets in InputfieldGrapick: public function renderReady(Inputfield $parent = null, $renderValueMode = false) { // Add JS and CSS dependencies $config = $this->config; $info = $this->getModuleInfo(); $version = $info['version']; $mod_url = $config->urls->{$this}; $mod_scripts = [ $mod_url . "vendor/grapick_0_1_10/dist/grapick.min.js?v=$version", $mod_url . "vendor/spectrum_1_8_1/spectrum.js?v=$version", $mod_url . "{$this}.js?v=$version", ]; $mod_styles = [ $mod_url . "vendor/grapick_0_1_10/dist/grapick.min.css?v=$version", $mod_url . "vendor/spectrum_1_8_1/spectrum.css?v=$version", $mod_url . "{$this}.css?v=$version", ]; foreach($mod_scripts as $ms) { $config->scripts->add($ms); } foreach($mod_styles as $ms) { $config->styles->add($ms); } return parent::renderReady($parent, $renderValueMode); } We get our module and it's current version, add it as a GET parameter to break cache when we update, and add the assets to scripts and styles. Keep in mind that using renderReady() makes sure these scripts load on admin pages where the module's inputfield is loaded, and that they load with other scripts at the head of the document. Now that we've added our vendor files and custom module .js and .css files to the config arrays, it's time to build out the markup for the form. Setting up our target element on the form There are plenty of ways to design your markup for the form. I want to focus on the elements of the __render() method in the InputfieldModule file that are important for what we are trying to do. The main thing we need to get here is the 'name' property of the field instance. Might as well get the page and id as well: public function ___render() { $name = $this->attr('name'); $page = $this->page; $id = $this->attr('id'); ... Once I have started building my fieldset, there is one field I want to set apart as a special field. In the case of Grapick, this is the div that the javascript is going to replace with a gradient color selection UI. $inputfields = new InputfieldFieldset(); $inputfields->label = 'Grapick Gradient'; $f = $this->modules->get('InputfieldMarkup'); $f->id = $name.'-gradient'; $f->label = "Grapick Gradient"; $f->name = $name.'_gradient'; $f->columnWidth = 50; $f->value = "<div class='uk-height-small grapick' id='{$name}_grapick_control'></div><br><div class='uk-dark uk-border-rounded uk-padding-remove'><span class='uk-text-meta' id='{$name}-rule'>{$rule}</span></div>"; $inputfields->add($f); ... There are plenty of other fields on this form, but take a look at the value of this form. I've incorporated the field name into the id for the DIV container. I've done the same for the Rule div and I apply this principle to all of the input id values. In my case, it is always $name followed by a common target. In this case $field_name_grapick_control is the ID that is important. We want to be able to build a list of these IDs and pass them on to Javascript so that we can manipulate them on the frontend when we are editng the form. Bridging the gap between PHP and Javascript In order to bridge the gap between the PHP backend and the Javascript frontend, we use $config->jsConfig(); This function creates a keyed object with nested objects as properties - like a key => value array in PHP. It can be used the same way. You might be tempted to put the call for this here, at the end of the __render() function, and that would work if you didn't have to deal with repeaters. You could create an array such as: $js_array[$name][ 'field' => $name.'_grapick_control', 'loaded' => false, ]; and then add it to a jsConfig key: wire()->config->jsConfig('grapicks', $js_array); and I initially did this. If you only had this single control target on the page, you'd be all set. But with repeaters, every time an instance of the field is loaded it would overwrite your array. You'd end up with only one element on the JS side. Instead we need to move this functionality over to the FieldtypeModule file. Adding our singleton field to the bridge In the FieldtypeModule we will build out a function that provides us with the data we need to get the list of IDs for fields not yet conceived. We start with a hook: public function init() { $this->addHookAfter('ProcessPageEdit::buildFormContent', $this, 'setJsConfig'); } This will provide us with access to the InputfieldWrapper containing the form via $e->form. From here we have the following function (this is what all of the experienced folks really wanted to see): public function setJsConfig($e) { $form = $e->return; $js_array = []; foreach($form->children as $fc) { $f_class = $fc->className; switch($f_class) { case 'InputfieldGrapick' : $ctrl = $fc->name; $js_array[$ctrl]['loaded'] = false; break; case 'InputfieldRepeater' : case 'InputfieldRepeaterMatrix' : foreach($fc->value as $enum => $it) { $fg = $it->template->fieldgroup; foreach($fg as $r_field) { if($r_field->type == 'FieldtypeGrapick') { $ctrl = $r_field->name.'_repeater'.$it->id; $js_array[$ctrl]['loaded'] = false; } } } break; } } wire()->config->jsConfig('grapicks', $js_array); } What this function does is: Grab the form object. Init the js_array variable. Iterate through the children of the form. This provides you with an array of all the inputfields. Run a switch/case against the class name of the field - this gets you to your singleton field right away. Grab the name of the field if it yours, stuff it in the box with a nested array with a single 'loaded' element. In the case of Repeater or Repeater Matrix, we must go deeper. Grabbing the value of each element of the repeater field gives you the template for the element. From the template, you can get the fieldgroup. Iterate through each field in the fieldgroup - again looking for your module InoutfieldModule - but this time as the type of field. For each match, you have enough information to build out what the repeater field name will be when it is loaded from AJAX: your field's name, '_repeater' with the repeater page id. Finally, add that repeater field page id combo into the js_array with a false 'loaded' flag. Once that is done, you now has a $js_array of all the root field names that get used to manufacture the id of your inputfield. If you remember how we added $name in the __render() Inputfields markup for the $name_grapick_control id we assigned to that special div (and the names/ids for the other fields too - you can check out the code) that $name variable will get populated with that $field_name '_repeater' $id combination. We can now access that list on the javascript side. We don't know when the elements with our prized IDs will show up on the page, but we know what to expect when they do. Adding our uncreated repeater babies to the bridge As this point, the basic elements on the PHP side are done. Moving on to the javascript side. I won't go into too many details about the structure of the js file. Suffice to say all of the subroutine functions get stacked up top, then consts and then we get into the core functionality. The strategy here is first to get the array we just sent over the bridge from PHP. In my InoutfieldGrapick.js file, I do that here (skip past the trims and color conversion stuff): /** * Begin grapick implementation */ //globals var upType, unAngle, gp = []; const stopTxt = [], swType = [], swAngle = [], swOrigin = [], swSize = []; const pwConfig = ProcessWire.config.grapicks; console.log('pwConfig',pwConfig); The constant pwConfig is where I stick the array, which is Processwire.config.whatever_key_assigned - in this case 'grapicks' is the property I am grabbing. The console log is an object with a key for each field. I don't want to get bogged down with the details, but here's what we are doing with this list. First, the list has a 'loaded' flag because we need to keep track of what has been loaded from what we know and what has not. The reason for this, is that some fields are going to be available right away on the page, which other will require us to poll the page looking for the element to appear. Once it does, in the case of repeaters, we can initiate the instance and log in the pwConfig object that the field is loaded and ready to go. Once the repeater item is loaded you can collapse it and it doesn't disappear - once it is loaded we are good to go. So I have a pollingRun() method - this assumes everything is loaded until a target is missing: function pollingRun() { let done = true; for (let key in pwConfig) { if (pwConfig.hasOwnProperty(key)) { if(pwConfig[key]['loaded'] == false) { done = false; let ctrl = document.getElementById(key+'_grapick_control'); //console.log(key, pwConfig[key], ctrl); if(ctrl) { createGrapick(key); //This is really whatever function you need to use to instantiate your special field and it passes a modification of the field name to match the ID we set up in our inputfield __render(); } //console.log('Grapick field: ' + key); for (let key2 in pwConfig[key]) { //console.log('---->', key2, pwConfig[key][key2]); } } } } if(done) { //console.log('Done.'); clearInterval(poll); } } At the end of my function that initializes the instance of the javascript activated object - in this case createGrapick() - I set the loaded flag for the given key to 'true': var createGrapick = function(key) { gp[key] = new Grapick({ el: '#' + key + '_grapick_control', colorEl: '<input id="' + key + '_colorpicker"/>', // I'll use this for the custom color picker direction: 'right', min: 0, max: 100, height: '2.5rem', }); ... //Will allow an inputfield to be picked up as changed when using the advanced features of UserActivity. pwStopsWrap.classList.add('InputfieldStateChanged'); build_rule(key); }) gp[key].emit('change'); pwConfig[key]['loaded'] = true; }; Now there is something else I did here, because there is an 'on change' event handler for my particular javascript object - I add an 'InputfieldStateChanged' class to a critical inputfield. The reason that I did this was because I am using the experimental features of UserActivity that track the changes made to a page. If I don't set this extra class on the pwStopsWrap input (which is the main data store for FieldtypeGrapick) the contents of the input will not save because PW uses the existence of this class to detect that a field has changed on the form and needs to be updated when the page is saved. If you aren't using UserActivity with this setting: then the adding of that class doesn't help or hurt you - although you may see warnings that you have unsaved changes left on the page if you mess around with the controls and then try to walk away. At the very end of this function you see I set the pwConfig[key]['loaded'] flag to true. That field is loaded. Great! Now that the pollingRun() function is set, near the bottom of the page I set up an event listener on the DOM because I need to wait until the page is loaded before I do the first run of the pollingRun() function: document.addEventListener('DOMContentLoaded', pollingRun, false); var poll = setInterval(pollingRun, 1000); Once the page had loaded, the static plain version of our field is now active and loaded. If you were to look at the rest of the pwConfig object you'd see that you've got one 'true' and all of the repeater item fields are false, right? Well not always. If you have set your repeater field to remember which items are open, when you load the page you will discover that during the pollingRun() iteration those fields also instantiated. You also see I've set up a 1 second interval call. This runs the pollingRun() function every second, waiting for you to open one of those closed repeater items. Once you do, it detects the target, initializes it and sets the loaded flag for those. Once all of the fields in the pwConfig has true loaded flags, the job is done and the polling interval is stopped. Setting the watchman to catch singletons, hidden babies, revealed babies and unexpected babies At this point nearly all of our cases are covered, but there is one more. What happens when someone adds a new repeater item? There is no way to know in advance with the RepeaterPage id is going to be. So I have another function and another event listener set up: function getGrapickControlNodes() { const nodeSet = document.querySelectorAll('[id*="_grapick_control"]').forEach(item => { keyName = item.id.substring(0,item.id.indexOf('_grapick_control')); pwConfig[keyName] = {'loaded':true}; createGrapick(keyName); }); } This function may seem a little convoluted, but it builds a node set that looks for all of the control elements on the page, and for each one it pulls the root field name (minus the suffix we added - _grapick_control - to identify our javascript instance target) - adds an entry for the newly created repeater item into the pwConfig object, sets the 'loadedl' flag to true and loads the instance. If I didn't add the representative object to the pwConfig object, I'd get a javascript error at the end of the createGrapick() method run because there would be no flag to set. I could have set the flag to 'false' here and then let createGrapick set it to 'true' when it was done. There is no need to run this function very often. Most of the targets on the page have already been populated. But we do want to watch the DOM and see if any new nodes are added: document.addEventListener('DOMContentLoaded', () => { //Look for new grapick fields (to account for repeaters) // select the target node var target = document.getElementById('ProcessPageEdit'); // create an observer instance var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { var nodes = mutation.addedNodes; var node; for(var n = 0; node = nodes[n], n < nodes.length; n++) { test_id = node.id; if(node.tagName == 'LI' && test_id.includes('repeater_item')) { getGrapickControlNodes(); } }; }); }); // configuration of the observer: var config = { attributes: false, childList: true, subtree: true, characterData: false }; // pass in the target node, as well as the observer options observer.observe(target, config); }, false); I added this below our other DomContentLoaded listener. This function sets up a mutation observer to watch the DOM tree starting with the ProcessPageEdit id ancestor. If any nodes are added that are LI elements with a class setup that includes the 'repeater_item' class the getGrapickControlNodes() function will fire. Since both repeater items and repeater matrix items have this class, it will fire if either type of repeater field has an extra item added. So that's it! We set up our special inputfield to incorporate the field name along with a unique suffix. We got a list of all the fields in the ProcessPageEdit Inputfield set that exist on the PHP side. We passed that list via jsConfig() on to the Javascript side, We iterated through that list once the page loads to catch the targets that are already part of the DOM. We watch the page every second for the moment when the user opens an AJAX loading field and make sure those instances are started. We killed off the interval when the list is all loaded. We watch the DOM ProcessPageEdit node and it's descendant with a MutationObserver for any newcomers and instantiate them too. Granted there are many ways to do this I'm sure, but this is what I did with FieldtypeGrapick.
  16. This fieldtype has been released and is awaiting approval. I changed the angle input to be an integer entry. I did pull together a strategy for building instances within repeaters. I'll do a writeup of the design pattern as a brief tutorial, as I think until we have a proper set of event handlers for repeater actions/AJAX etc. this is a way to build out a field that requires javascript libraries that is repeater compatible. I feel pretty happy with the end result. It is a fun toy to play with if you have never really explored CSS gradients.
  17. I've seen it come up in the past for sure. Worth having a discussion. I mean, even just an on-page array of booleans that state if a field is loaded on the page would be something, but I agree with this: There are classes on page that indicate this for repeaters. InputfieldRepeaterItem InputfieldStateCollapsed <- class gets applied to LI element for closed repeater item InputfieldRepeaterItem InputfieldStateWasCollapsed <- class gets applied to LI once a repeater item is opened. Further, this gets applied once the AJAX content is loaded. The same applies to repeatermatrix - InputfieldRepeaterMatrixItem gets added to the above. For the immediate need I think what I am going to do is: populate a jsConfig array key with field instance names e.g. field_name, repeater_field_name2300 and a 'loaded' flag run an interval that looks for the target field I need to instantiate, looks for the wasCollapsed state and check for the target field. once it is found in the DOM, create the instance and set the field flag to true, and when all the flags are true break out of the interval. It's sloppy, I hate using intervals to poll the page but my hope is that if someone is working with repeaters they are going to want to dig into new items pretty quickly anyway.
  18. For your goals: $files->getCSV() - https://processwire.com/api/ref/wire-file-tools/get-c-s-v/ Images are a little trickier but if you know where they are stored on the server there's just a little post processing required. https://processwire.com/modules/process-page-field-select-creator/ - then populate the child template with fields you need editing As said above, turn off formatting, then you can iterate through using $pages->findRaw() and implode(), etc. https://processwire.com/api/ref/pages/find-raw/ Write to files using $files->putContents(); https://processwire.com/api/ref/wire-file-tools/file-put-contents/ Using a dedicated table breaks one of the features of PW vs WP - every field is a table! https://processwire.com/blog/posts/making-efficient-use-of-fields-in-processwire/ That being said, if you wanted to create a special field that compounded a bunch of related values together, you could look at this module: https://processwire.com/modules/fieldtype-events/ If you are looking for a tutorial that includes creating a custom class to store your value, a more advanced overview is here:
  19. Right, so if it is on the field will not instantiate. Turned off it works fine. The weird thing is that I have something that watches for new elements so that if you have items open that haven't loaded yet and add a new item, the other items will initialize.
  20. I have this working well, it's lined up to release in a little bit. I have run into issues dealing with repeaters because the lack of repeater JS events related to opening and AJAX loading. So there is some limited repeater support until I work out a solution. I will dig in a little deeper after the initial release. Any insight into this issue is welcomed.
  21. Has this problem ever been resolved? Is there an alternative strategy? I have run into this situation with my grapick module. I have a mutation observer watching ProcessPage Edit and initializing fields once nodes are added to the DOM, but it only works in the repeater items that are already open. It would be nice to have some sort of trigger. Alternatively, perhaps I could build a list of fields using my fieldtype, fieldgroups that contain that field, compare it with a list of fields used on a page template, and then I guess watch the repeater/repeater container for new nodes? Oh but that doesn't help because a repeater is a list in a list in a list right?
  • Create New...