
abdus
Members-
Posts
743 -
Joined
-
Last visited
-
Days Won
42
Everything posted by abdus
-
I think it's important to sync `jobs` `currentJobs` fields such that `currentJobs` doesnt have more jobs than `jobs` field has. You can use the following hook to update `currentJobs`. wire()->addHookBefore('Pages::saveReady', function (HookEvent $event) { $page = $event->arguments('page'); if ($page->template != 'person') return; // make sure currentJobs items <= jobs items if ($page->isChanged('jobs')) { foreach ($page->currentJobs as $cj) { if (!$page->jobs->has($cj)) $page->currentJobs->remove($cj); } } });
-
I'd still keep the Page fields, and use two fields to determine all jobs and current jobs a person has. What you should do is to pull up your sleeves and get down to build a select field, maybe extend @ryan's asmSelect, or more modern alternatives (chosen.js select2.js etc) that have tagging support. In fact, I had some free time and went ahead and created a signup form that does this. I used selectize.js for selecting/creating jobs on the frontend. Options load with AJAX at page load Once you submit the form, you get this On the backend, I get the input, sanitize it and filter values, if a new job name is specified, I create the page, and add it to `jobs` or `currentJobs`. Here's the code in all its glory: <?php namespace ProcessWire; /* @var $input WireInput */ /* @var $sanitizer Sanitizer */ /* @var $pages Pages */ if (input()->requestMethod('GET') && input()->urlSegment1 === 'jobs.json') { // serve json for input fields $jobs = pages('template=profession, sort=title'); echo wireEncodeJSON($jobs->explode(['id', 'title'])); return $this->halt(); } elseif (input()->urlSegment1) { // don't allow other url segments throw new Wire404Exception(); } elseif (input()->requestMethod('POST')) { $jobs = $input->post->jobs; $currentJobs = $input->post->currentJobs; // sanitize inputs $saneJobs = []; foreach ($jobs as $j) { $jobId = $sanitizer->int($j); $jobTitle = $sanitizer->text($j); if ($jobId) $saneJobs[$jobId] = false; // not current job elseif ($jobTitle) $saneJobs[$jobTitle] = false; // not current job } // check if a job is one of current jobs foreach ($currentJobs as $cj) { $currId = $sanitizer->int($cj); $currTitle = $sanitizer->text($cj); // if person has the job, set it to current if (isset($saneJobs[$currId])) $saneJobs[$currId] = true; // set to current elseif (isset($saneJobs[$currTitle])) $saneJobs[$currTitle] = true; // set to current } // save info $person = $pages->get(1228); $person->of(false); $addableJobs = []; $addableCurrents = []; foreach ($saneJobs as $j => $isCurrent) { // $j is job id or new job title $job = null; // does job already exist? if (is_int($j)) { $job = $pages->get($j); } else { // create a new job and add it $job = new Page(); $job->template = 'profession'; $job->parent = 1027; $job->name = $sanitizer->pageName($j); $job->title = ucwords($j); // already sanitized, but capitalize it $job->save(); } if (!$job) continue; $addableJobs[] = $job; if ($isCurrent) $addableCurrents[] = $job; } $person->jobs->add($addableJobs); $person->currentJobs->add($addableCurrents); $person->save(); } ?> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.4/js/standalone/selectize.js"></script> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> <link rel="stylesheet" href="https://rawgit.com/selectize/selectize.js/master/dist/css/selectize.bootstrap3.css"> <div class="flex"> <form method="POST" class="signup-form"> <div class="form-group"> <label for="name">Name</label> <input name="name" required type="text" class="form-control" id="name" placeholder="Name Lastname"> </div> <div class="form-group"> <label for="jobs">Your Jobs</label> <select name="jobs[]" required id="jobs" multiple class="form-control"> <option>Loading...</option> </select> </div> <button class="btn" type="submit">Save</button> </form> </div> <script> $jobs = $('#jobs'); // fetch all jobs $.getJSON('./jobs.json', function (data) { initSelect(data); }); function initSelect(data) { const makeOption = (it) => ({text: it.title, value: it.id}); let selected = data.splice(0, 5).map(makeOption); window.jobs = data; $jobs.selectize({ create: true, render: { item: function (data, escape) { return `<div class='option'> <span class="option__label">${escape(data.text)}</span> <input class="option__current" type="checkbox" name="currentJobs[]" value="${escape(data.value)}"> </div>`; } }, options: data.map(makeOption), // populate the field with the jobs person already has // items: data.splice(1,4).map(i => i.id) }); } </script> <style> .flex { display: flex; justify-content: center; margin-top: 10rem; } .signup-form { width: 500px; margin: auto; } .option { width: 100%; } .option:after { content: ''; display: table; } .option__label { float: left; } .option__current { float: right; } </style>
-
Maybe you need an InputfieldAsmSelect populated with the pages you want? Here's one that allows textformatter selection for text fields $f = $this->modules->get('InputfieldAsmSelect'); $f->setAttribute('name', 'textformatters'); $f->label = $this->_('Text formatters (for file descriptions)'); $f->description = $this->_('Select one or more text formatters (and their order) that will be applied to the file description when output formatting is active. The HTML Entity Encoder is recommended as a minimum.'); foreach($this->wire('modules') as $module) { $className = $module->className(); if(strpos($className, 'Textformatter') !== 0) continue; $info = $this->wire('modules')->getModuleInfo($module); $f->addOption($className, "$info[title]"); } You'd get the pages you want with $this->pages->find(selector) and build your options with addOption() or addOptions() methods
-
It's certainly better than putting plain <img> tags on the page. How many images you have on the page doesnt really matter. Images either have a 1x1px transparent GIF or a low quality placeholder. As visitor scrolls down the page JS swaps placeholders with a real/HQ image and browser starts loading the high quality version. If user doesnt scroll down to an image, it doesnt load. Pages load much quicker and you get save bandwidth in the process.
-
Did you consider using lazy loading instead? http://afarkas.github.io/lazysizes/ https://github.com/conclurer/TextformatterSrcset
-
I've just checked the source for InputfieldImage and it definitely originates from the backend. Somehow along the way json gets corrupted/wrong data is returned. You need to check the server response using developer console to get an idea of what it really is. https://developers.google.com/web/tools/chrome-devtools/network-performance/reference Just to cross it off the list, try downloading the latest source from GitHub and replacing /wire/ directory.
- 12 replies
-
Hmm that error means either pw returns empty json or you echo a string somewhere and json becomes invalid. On your developer console (F12) activate network tab then try adding an image, inspect the request and response values, check if something weird is going on
- 12 replies
-
Check your browser console (F12), is there an error?
- 12 replies
-
There doesnt have to be a separate basic Repeater field. FieldtypeFieldsetPage extends Repeater fields, that's why the ProcessPageEditLink crashes. But with @ryan's fix, it's gone now ( just tried).
-
The code is to show how to add new records to a Table field Unless you develop your own module, there's not a native way to handle 2-way relations from Table <--> Page Reference field. This means you'll still have to use a code similar to the one in the first post. Can you show the whole form? Also, one thing to note is that the pages you create from Page field input aren't created instantaneously. They're created once you submit (to perform permission checks, sanitizations etc). This would mean page has to reload to add new jobs in the first place, and you wont be able to dynamically render/populate currentJobs field using JS. So it seems you will have to either instruct user to save the page, or replace repeaters with Table field, or find other ways to shave time off of your process
-
Since I had nothing to do, I went ahead, and implemented a form as a proof of concept. This works with `jobs` and `currentJobs` setup. When you submit and check the person page Here's the code: <?php namespace ProcessWire; /* @var $input WireInput */ /* @var $sanitizer Sanitizer */ /* @var $pages Pages */ if(input()->requestMethod('POST')) { $jobs = []; foreach ($input->post->hasJob as $jobId) { $jobs[] = $sanitizer->int($jobId); } $currentJobs = []; foreach ($input->post->currentJobs as $currId) { // make sure person has the current job in his jobs if(!in_array($currId, $jobs)) continue; $currentJobs[] = $sanitizer->int($currId); } $p = new Page(); $p->template = 'person'; $p->parent = 1076; $p->name = 'name-lastname'; $p->jobs = $jobs; $p->currentJobs = $currentJobs; $p->save(); return $this->halt(); } $jobs = pages('template=profession'); ?> <form method="POST"> Pick your jobs: <table> <thead> <tr> <td></td> <td>Job Title</td> <td>Current?</td> </tr> </thead> <tbody> <?php foreach ($jobs as $job): ?> <tr class="job"> <td><input class="job__has" id="job<?= $job->id ?>" type="checkbox" name="hasJobs[]" value="<?= $job->id ?>"></td> <td><label class="job__title" for="job<?= $job->id ?>"><?= $job->title ?></label></td> <td><input class="job__current" disabled type="checkbox" name="currentJobs[]" value="<?= $job->id ?>"></td> </tr> <?php endforeach; ?> </tbody> </table> <button type="submit" name="submit" value="1">Submit</button> </form> <script> let jobs = document.querySelectorAll('.job'); [].slice.call(jobs).forEach(job => { job.addEventListener('click', function (e) { if(!e.target.classList.contains('job__has')) return; if (e.target.checked) { job.querySelector('.job__current').removeAttribute('disabled'); } else { let current = job.querySelector('.job__current'); current.checked = false; current.setAttribute('disabled', 'disabled'); } }); }); </script>
-
Page table is basically a custom table in DB, so you can add any column you want. Instead of using pages like repeaters, with table field, every record is a row in DB with the columns you define. $page->of(false); // turn off output formatting, if necessary $award = $page->awards->makeBlankItem(); $award->title = "Tallest Building"; $award->date = "2014-05-01"; $award->category = "Engineering"; $award->url = "http://di.net/tallest/building/award/"; $page->awards->add($award); $page->save('awards'); As you see using Table field is very straightforward. But one glaring problem is to get 2-way relations for getting jobs a certain person have or people that have a certain job. For that you probably need a custom module that updates a job page with the person that added a job to its table field and vice versa. But now thinking of it you can still use page fields `jobs` and `currentJobs` and make it work. I'm not sure how you render your form on the front end, but you can render a list of jobs with a checkbox to mark as current. Then when you post your data with AJAX or HTTP request, you add all jobs to `jobs` field and filter current jobs (that user checked) and add those to `currentJobs` field. I'm not sure if that was clear, I can rephrase it better if you want.
-
In your first post, there's this line <?php $langname = $user->language->title; //get current user language?> What type of field is it? (You can use Page field for that too ) Is it a basic text field or what?
-
When building your markup, to decide whether a job is current you can use WireArray::has($item) method. <?php namespace ProcessWire; $persons = pages('template=person'); ?> <ul class="persons"> <?php foreach ($persons as $person): ?> <li class="person"> Name: <?= $person->title ?> <?php if ($person->jobs->count): ?> Jobs: <ul class="person__jobs"> <?php foreach ($person->jobs as $job): ?> <?php $isCurrent = $person->currentJobs->has($p); ?> <li class="job <?= $isCurrent ? 'job--current' : '' ?>"> <?= $job->title ?> </li> <?php endforeach; ?> </ul> <?php endif; ?> </li> <?php endforeach; ?> </ul>
-
OK, I got it working. Using an extra `currentJobs` field works. Go to `jobs` field and clone it from Actions tab Disable `create new pages` option Change inputfield type to `checkboxes` Add field to `person` template Edit your /site/ready.php to include this hook wire()->addHookAfter('InputfieldPage::getSelectablePages', function (HookEvent $e) { /** @var Page $page */ $field = $e->object; if(!$field == 'currentJobs') return; $page = $e->arguments(0); $jobs = $page->jobs; $e->return = $jobs; }); Once you add jobs to a person and save the page, currentJobs field is populated with jobs, and you can pick current jobs.
-
Then I'd add another page field called `currentJobs` just like `jobs` field, and when outputting you can combine both to get the markup you want. A better way would be hooking into Page::saved to copy `jobs` field value to `currentJobs` and changing `currentJobs` input field to checkboxes. After you save the page, you can use checkboxes to mark the current jobs. I can try to see if this would even work, but I need to eat my lunch first. Also, this is where Page reference field falls short. It just references and nothing more. If you have ProFields, you can use Table field to get a very lean and performant repeater-like experience
-
To mark the current job, you need a different approach, Page field only references other pages. For this situation, I'd pick the first job as the current one, (or the last) because it makes sense if you're ordering jobs chronologically. Oh by the way, install Autocomplete input from Code modules (it's not installed by default) and change input type of the page fields to `Multiple page selection (sortable)` > `Autocomplete`. This way you can order job entries and simulate a chronological order. Then in your templates you can do something like this <ul class="persons"> <?php foreach ($persons as $p): ?> <li class="person"> Name: <?= $p->title ?> <?php $jobs = $p->jobs; $lastJob = $jobs->pop(); ?> <ul class="person__jobs"> <li class="job job--current"> Current Job: <?= $lastJob->title ?> </li> <?php foreach ($jobs as $j): ?> <li class="job"> <?= $j->title ?> </li> <?php endforeach; ?> </ul> </li> <?php endforeach; ?> </ul> On my multi language setup there's no `lang` field, I'm not sure it it's a native field. Is it a field that you've created yourself?
-
$jobs = $pages->find('template=profession, persons.count>0'); Find all professions that has at least one person referenced (i.e. where at least one person has this profession)
-
Yes, that's what I meant You can use the same method for those as well.
-
This looks like a job for Page fields instead of Repeaters, and the module ConnectPageFields by @Robin S which makes 2-way relations possible. This way you wont have to use 4 nested loops to build your markup. You need a set up templates and fields like this PAGES: collection/ Peter : person Maria : person Paul : person professions/ Doctor : profession Actor : profession FIELDS: professions Page field, limited to `profession` template, and professions under /professions Enable create new pages persons Page field, limited to `person` template, and persons under /collection Add `professions` field to `person` template, and `persons` field to `profession` template. Then set up a synced pair from ConnectPageFields module settings When you add a new profession to a person, it will be created under /professions Then in your templates you can just use: <ul class="jobs"> <?php foreach ($jobs as $j): ?> <li class="job"> <ul class="job__persons"> <?php foreach($j->persons as $p): ?> <li class="person"> <?= $p->title ?> </li> <?php endforeach; ?> </ul> </li> <?php endforeach; ?> </ul> and you're done. I'm not sure how Residency system works, but if range of its values is not too wide, Page field can be replace repeaters for that too. Also, instead of getting all pages then filtering, you can extend your selector to include the filter, so you wont overload DB and RAM for nothing. Also I've found using IDs for speed-critical operations to be more performant. Around 2ms for id based fetches, >6ms for non-id based fetches $persons = $pages->find("parent=$collectionId, lang=$user->language"); // $user->language is interpreted as page id for the user's language
-
Also, if your data doesnt really change often, you can use WireCache class to cache all relevant pages beforehand and perform searches in memory instead of db. This helps you reduce the server load and probably be a lot faster for large sets. https://processwire.com/api/ref/cache/
-
Here's how I did it. Searching, sorting, filtering, pagination all works <?php namespace ProcessWire; /* @var $config Config */ /* @var $pages Pages */ /* @var $input WireInput */ /* @var $sanitizer Sanitizer */ // fields to return $fields = ['id', 'name', 'publishedStr', 'createdStr']; if ($config->ajax) { // sanitize inputs $search = $sanitizer->selectorValue($input->get->queries['search']); $sorts = []; if ($input->get->sorts) { foreach ($input->get->sorts as $f => $direction) { $key = $sanitizer->fieldName($f); $direction = $sanitizer->int($direction, ['min' => -1, 'max' => 1]); if ((!$key) || (!$direction)) continue; $sorts[$key] = $direction > 0 ? '' : '-'; // sort=field or sort=-field } } $page = $sanitizer->int($input->get->page, ['min' => 0, 'blankValue' => 1]); $perPage = $sanitizer->int($input->get->perPage, ['min' => 10, 'max' => 100, 'blankValue' => 10]); $offset = $sanitizer->int($input->get->offset, ['min' => 0]); // base selector $selector = [ "id>0", "include=all", ]; $selectorFiltered = array_merge($selector, [ "name|title*=$search", // change fields to search ]); $selectorFilteredLimited = array_merge($selectorFiltered, [ "limit=$perPage", "start=$offset" ]); // include sorts as sort=(-)fieldName foreach ($sorts as $f => $d) { $selectorFilteredLimited[] = "sort={$d}{$f}"; } // perform database query $totalCount = $pages->count(join(', ', $selector)); // # of all pages $queryCount = $pages->count(join(', ', $selectorFiltered)); // # of filtered pages $pageData = $pages->find(join(', ', $selectorFilteredLimited))->explode($fields); // data to return // output json header("Content-type: application/json"); $data = [ 'records' => array_values($pageData), 'queryRecordCount' => $queryCount, 'totalRecordCount' => $totalCount ]; echo json_encode($data); // stop return $this->halt(); } ?> <?php if (!$config->ajax): ?> <table class="table"> <thead> <tr> <?php foreach ($fields as $field): ?> <td><?= $field ?></td> <?php endforeach; ?> </tr> </thead> <tbody></tbody> </table> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/Dynatable/0.3.1/jquery.dynatable.min.js"></script> <script> $('table').dynatable({ dataset: { ajax: true, ajaxUrl: '.', // current page ajaxOnLoad: true, records: [] } }); </script> <?php endif; ?> Here's a screenshot Although I think the code is clear, feel free to ask me if you have any questions. https://processwire.com/api/ref/wire-array/explode/ https://processwire.com/api/ref/sanitizer/ https://processwire.com/api/ref/pages/count/ https://processwire.com/blog/posts/processwire-2.6.8-brings-new-version-of-reno-admin-theme-and-more/#new-this-gt-halt-method-for-use-in-template-files
-
Although PW has composer support, not every setup has composer. This means some modules cannot function as their dependencies cannot be installed during module installation. When developing a module, if I need a library, I install it with composer inside module directory and require __DIR__ . '/vendor/autoload.php' inside __construct(), or init() or ready(). All references with use keyword resolve automatically.
-
Hmm, most lightboxes use <a> with an <img> inside. Href for <a> points to portrait image, and src of the image points to square image. For instance: baguetteBox uses following scheme for responsive images (for non-responsive, remove data-at-# attributes) <a href="img/2-1.jpg" data-at-450="img/thumbs/2-1.jpg" data-at-800="img/small/2-1.jpg" data-at-1366="img/medium/2-1.jpg" data-at-1920="img/big/2-1.jpg"> <img src="img/thumbs/2-1.jpg"> </a> This can be achieved with a code like this. You can modify hook to get rendered markup as well. <?php $image = $page->images->first; $square = $image->size(400, 400); // cropped around center $portrait = $image->width(600)->url; // non-cropped $portrait2x = $image->width(1200)->url; $markup = "<a href='$portrait' data-at-1200='$portrait2x'><img src='$square' /></a>"; echo $markup; You can specify different crop location as a third argument to size() method https://processwire.com/api/ref/pageimage/size/
-
How do you determine the $mode? What do you mean by selecting image? Also, where will you display images? On page edit screen? Frontend? Would a hook work for you? <?php namespace ProcessWire; // /site/ready.php wire()->addHookMethod('Pageimage::mode', function (HookEvent $e) { $img = $e->object; $mode = $e->arguments(0) $size = $e->arguments(1) ?? 400; if ($mode === 'summary') { $e->return = $img->size($size, $size)->url; // 400x400 image } else { $e->return = $img->width($size)->url; // 400x600 (or longer) image } }); // In your templates ?> <img src="<?= $page->images->first->mode('summary') ?>" /> <img src="<?= $page->images->first->mode('summary', 600) ?>" />