Change wagtail admin form fields based on user input - wagtail

I've got a model called 'Block', which has multiple fields:
type. This is a dropdown where you can select 3 options.
URL
Search
Based on the type field, the URL or Search must be shown.
The search field needs to preform a search using a variable: API_HOST.
I've written JS to do make the form dynamic and I've extended the wagtailadmin/pages/edit.html template to inject the JS.
However, I'm not sure how to pass the API_HOST (defined in dev.py) down to the template.
dev.py
API_HOST = "http://localhost:8000/"
wagtailadmin/pages/edit.html:
{% extends "wagtailadmin/pages/edit.html" %}
{% block extra_js %}
{{ block.super }}
<script>
var api_host = "{{api_url}}";
console.log("api_host:", api_host);
</script>
<script src="/static/js/blocks_hook.js">
</script>
{% endblock %}
blocks_hook.js
$(function () {
var $type = $('#id_type');
var $search = $('#id_search');
var $url = $('#id_url');
var api = new API(api_host);
hide($search);
hide($url);
$type.on('change', function(){
if ($type.val() == 'search') {
show($search);
hide($url);
api.getProduct(function(product){
// .. do something with the product
});
} else if($type.val() == 'url') {
show($url);
hide($search);
}
});
How should I approach this situation?

I managed to fix this using the insert_editor_js hook mechanism, provided by Wagtail.
Make sure the variable you're passing in the method (#hooks.register) is actually present in your configuration file or else the hooks system crashes without any actionable debug information.

Related

Dynamic created map triggering warning in angular

In angular I want to dynamically create a map based on the address entered. I have successfully done this in VueJS. But in angular this triggers security warnings.
HTML:
<iframe
ng-src="https://maps.google.com/maps?&q={{encodeURIComponent(item.address)}}&output=embed"
allowfullscreen>
</iframe>
I have tried creating the following:
app.filter('trustAsResourceUrl', ['$sce', function ($sce) {
return function (val) {
return $sce.trustAsResourceUrl(val);
};
then piping it as so:
| trustAsResourceUrl}}
Which works if using an already established URL but not since I'm trying to form URL from address. I get the following:
Error: [$interpolate:noconcat] Error while interpolating: Strict
Contextual Escaping disallows interpolations that concatenate multiple
expressions when a trusted value is required. See
http://docs.angularjs.org/api/ng.$sce
VueJS was so simple but I can't use it in this project. I'll include it in case it gives any ideas:
methods: {
getmap: function(){
setTimeout(function () {
const searchInput = document.getElementById('searchTextField');
let addr = searchInput.value;
let embed = "<div class='form-group'><label for='exampleInputPassword1'>Map Preview</label><iframe frameborder = '0' scrolling= 'no' marginheight= '0' marginwidth= '0' src= 'https://maps.google.com/maps?&q=" + encodeURIComponent(addr) + "&output=embed' > </iframe></div>";
$('.place').html(embed); }, 200)
},
Essentially you can't use this filter. You have to create the string you want to use directly on the scope:
this.src = $sce.trustAsResourceUrl("https://maps.google.com/maps?q="
+ encodeURIComponent(item.address) + "&output=embed");
Then <iframe ng-src="{{$ctrl.src}}">
You will have to update the entire src rather than just item.address.

Convert Quill Delta to HTML

How do I convert Deltas to pure HTML? I'm using Quill as a rich text editor, but I'm not sure how I would display the existing Deltas in a HTML context. Creating multiple Quill instances wouldn't be reasonable, but I couldn't come up with anything better yet.
I did my research, and I didn't find any way to do this.
Not very elegant, but this is how I had to do it.
function quillGetHTML(inputDelta) {
var tempCont = document.createElement("div");
(new Quill(tempCont)).setContents(inputDelta);
return tempCont.getElementsByClassName("ql-editor")[0].innerHTML;
}
Obviously this needs quill.js.
I guess you want the HTML inside it. Its fairly simple.
quill.root.innerHTML
If I've understood you correctly, there's a quill thread of discussion here, with the key information you're after.
I've quoted what should be of most value to you below:
Quill has always used Deltas as a more consistent and easier to use (no parsing)
data structure. There's no reason for Quill to reimplement DOM APIs in
addition to this. quill.root.innerHTML or document.querySelector(".ql-editor").innerHTML works just fine (quill.container.firstChild.innerHTML is a bit more brittle as it depends on child ordering) and the previous getHTML implementation did little more than this.
Simple, solution is here:
https://www.scalablepath.com/blog/using-quill-js-build-wysiwyg-editor-website/
The main code is:
console.log(quill.root.innerHTML);
This is a very common confusion when it comes to Quilljs. The thing is you should NOT retrieve your html just to display it. You should render and display your Quill container just the same way you do when it is an editor. This is one of the major advantages to Quilljs and the ONLY thing you need to do is:
$conf.readOnly = true;
This will remove the toolbar and make the content not editable.
I have accomplished it in the backend using php.
My input is json encoded delta and my output is the html string.
here is the code , if it is of any help to you.This function is still to handle lists though and some other formats but you can always extend those in operate function.
function formatAnswer($answer){
$formattedAnswer = '';
$answer = json_decode($answer,true);
foreach($answer['ops'] as $key=>$element){
if(empty($element['insert']['image'])){
$result = $element['insert'];
if(!empty($element['attributes'])){
foreach($element['attributes'] as $key=>$attribute){
$result = operate($result,$key,$attribute);
}
}
}else{
$image = $element['insert']['image'];
// if you are getting the image as url
if(strpos($image,'http://') !== false || strpos($image,'https://') !== false){
$result = "<img src='".$image."' />";
}else{
//if the image is uploaded
//saving the image somewhere and replacing it with its url
$imageUrl = getImageUrl($image);
$result = "<img src='".$imageUrl."' />";
}
}
$formattedAnswer = $formattedAnswer.$result;
}
return nl2br($formattedAnswer);
}
function operate($text,$ops,$attribute){
$operatedText = null;
switch($ops){
case 'bold':
$operatedText = '<strong>'.$text.'</strong>';
break;
case 'italic':
$operatedText = '<i>'.$text.'</i>';
break;
case 'strike':
$operatedText = '<s>'.$text.'</s>';
break;
case 'underline':
$operatedText = '<u>'.$text.'</u>';
break;
case 'link':
$operatedText = ''.$text.'';
break;
default:
$operatedText = $text;
}
return $operatedText;
}
Here's a full function using quill.root.innerHTML, as the others didn't quite cover the complete usage of it:
function quillGetHTML(inputDelta) {
var tempQuill=new Quill(document.createElement("div"));
tempQuill.setContents(inputDelta);
return tempQuill.root.innerHTML;
}
This is just a slight different variation of km6 's answer.
For Quill version 1.3.6, just use:
quill.root.innerHTML;
Try it online: https://jsfiddle.net/Imabot/86dtuhap/
Detailed explaination on my blog
This link if you have to post the Quill HTML content in a form
quill.root.innerHTML on the quill object works perfectly.
$scope.setTerm = function (form) {
var contents = JSON.stringify(quill.root.innerHTML)
$("#note").val(contents)
$scope.main.submitFrm(form)
}
I put together a node package to convert html or plain text to and from a Quill Delta.
My team used it to update our data model to include both Quill's Delta and HTML. This allows us to render on the client without an instance of Quill.
See node-quill-converter.
It features the following functions:
- convertTextToDelta
- convertHtmlToDelta
- convertDeltaToHtml
Behind the scenes it uses an instance of JSDOM. This may make it best suited for migration scripts as performance has not been tested in a typical app request lifecycle.
Try
console.log ( $('.ql-editor').html() );
Here is how I did it, for you Express folks. It seems to have worked very well in conjunction with express-sanitizer.
app.js
import expressSanitizer from 'express-sanitizer'
app.use(expressSanitizer())
app.post('/route', async (req, res) => {
const title = req.body.article.title
const content = req.sanitize(req.body.article.content)
// Do stuff with content
})
new.ejs
<head>
<link href="https://cdn.quilljs.com/1.3.2/quill.snow.css" rel="stylesheet">
</head>
...
<form action="/route" method="POST">
<input type="text" name="article[title]" placeholder="Enter Title">
<div id="editor"></div>
<input type="submit" onclick="return quillContents()" />
</form>
...
<script src="https://cdn.quilljs.com/1.3.2/quill.js"></script>
<script>
const quill = new Quill('#editor', {
theme: 'snow'
})
const quillContents = () => {
const form = document.forms[0]
const editor = document.createElement('input')
editor.type = 'hidden'
editor.name = 'article[content]'
editor.value = document.querySelector('.ql-editor').innerHTML
form.appendChild(editor)
return form.submit()
}
</script>
express-sanitizer (https://www.npmjs.com/package/express-sanitizer)
document.forms (https://developer.mozilla.org/en-US/docs/Web/API/Document/forms)
My view only has one form, so I used document.forms[0], but if you have multiple or may extend your view in the future to have multiple forms, check out the MDN reference.
What we are doing here is creating a hidden form input that we assign the contents of the Quill Div, and then we bootleg the form submit and pass it through our function to finish it off.
Now, to test it, make a post with <script>alert()</script> in it, and you won't have to worry about injection exploits.
That's all there is to it.
Here is a proper way to do it.
var QuillDeltaToHtmlConverter = require('quill-delta-to-html').QuillDeltaToHtmlConverter;
// TypeScript / ES6:
// import { QuillDeltaToHtmlConverter } from 'quill-delta-to-html';
var deltaOps = [
{insert: "Hello\n"},
{insert: "This is colorful", attributes: {color: '#f00'}}
];
var cfg = {};
var converter = new QuillDeltaToHtmlConverter(deltaOps, cfg);
var html = converter.convert();
Refer https://github.com/nozer/quill-delta-to-html
For a jQuery-style solution that allows getting and setting the Quill value I am doing the following:
Quill.prototype.val = function(newVal) {
if (newVal) {
this.container.querySelector('.ql-editor').innerHTML = newVal;
} else {
return this.container.querySelector('.ql-editor').innerHTML;
}
};
let editor = new Quill( ... );
//set the value
editor.val('<h3>My new editor value</h3>');
//get the value
let theValue = editor.val();
quill-render looks like it's what you want. From the docs:
var render = require('quill-render');
render([
{
"attributes": {
"bold": true
},
"insert": "Hi mom"
}
]);
// => '<b>Hi mom</b>'
If you want to render quill using nodejs, there is a package quite simple based on jsdom, usefull to render backside (only one file & last update 18 days from now) render quill delta to html string on server
Just use this clean library to convert from delta from/to text/html
node-quill-converter
example:
const { convertDeltaToHtml } = require('node-quill-converter');
let html = convertDeltaToHtml(delta);
console.log(html) ; // '<p>hello, <strong>world</strong></p>'

Move if/then logic into view or model or keep in template?

after creating a web app using backbone for an engine design firm, I'm wondering if I should move the "if/then" logic out of the html templates.
To help clarify what I mean, here are two examples that I am currently using in production.
If I move the if/then logic out of the template, I would move it to the view, but I'm not sure if that's the "right" way or "backbone" way of doing things.
Am I making poor design decisions, or is what I've done OK?
Thanks!
Simple Example 1:
In the view:
//m is the model used by the view
return Backbone.View.extend({
template: _.template(tmpl, null, { variable: 'm' }),
In the template:
{% if(m.title) { %}
<h4> {%- m.title %} </h4>
{% } else { %}
<h4>Experiment Title Goes Here</h4>
{% } %}
Complex Example 2:
In the view:
//args are the model attributes passed into the view
initialize: function (args) {
this.currentEngine = args.currentEngine;
this.engineDisplay = args.engineDisplay;
this.engineType = args.engineType;
this.isCurrent = this.model.isCurrent(this.currentEngine);
},
render: function () {
this.$el.html(this.template({
engineDisplay: this.engineDisplay,
engineType: this.engineType,
isCurrent: this.isCurrent;
}));
In the template:
{% if(!engineDisplay) { %}
{% if (m.isCurrent && (engineType === 'GAS' || engineType === 'ECO')) { %}
<span>Not Available</span>
{% } else { %}
<span>
<span>Click here to select</span>
</span>
{% } %}
{% } %}
I think your first implementation was correct. Keep the logic out of the view. The "correct" way, or the "backbone" way is to keep the logic where it needs to be:
the model/collection houses code of "where" the data needs to come from.
the view houses code of "what" it needs to do/display. (what needs to happen if event X happens)
the template should house the code of "how" it needs to be displayed.
I'm sure im missing stuff.. i'll wait until the comments tells me how wrong i am and then i'll correct it.
-Sheers

Changing the template data not refreshing the elements

I have searched and tried suggestions mentioned in various posts but no luck so far.
Here is my issue.
I have created a custom element <month-view id="month-view-element"></month-view> in my mainpage.html. Inside mainpage.html when this page is initially loaded i created a empty json object for all the 30days of a month and print a placeholder type cards in UI. Using the code below.
var json = [];
for(var x = 0; x < total; x++) {
json.push({'hours': 0, 'day': x+1, 'year': year});
}
monthView.month = json; //Doing this line. Prints out the desired empty cards for me in the UI.
created a month-view.html something like below:
<dom-module id='month-view'>
<template>
<template is="dom-repeat" items= "{{month}}">
<paper-card class="day-paper-card" heading={{item.day}}>
<div class="card-content work">{{item.work}}</div>
<div class="card-actions containerDay layout horizontal">
<div style="display:inline-block" class="icon">
<paper-icon-button icon="icons:done" data-hours = "8" data-day$="{{item.day}}" data-month$={{item.month}} data-year$={{item.year}} on-click="updateWorkHours"></paper-icon-button>
<paper-tooltip>Full day</paper-tooltip>
</div>
</div>
</paper-card>
</template>
</template>
<script>
Polymer({
is: "month-view",
updateWorkHours: function (e, detail) {
console.log(e);
this.fire('updateWorkHour', {day: e.target.dataHost.dataset.day,
month: e.target.dataHost.dataset.month,
year: e.target.dataHost.dataset.year,
hours: e.target.dataHost.dataset.work
});
}
});
</script>
</dom-module>
There is another file script.js which contains the function document.addEventListener('updateWorkHour', function (e) { // doStuff });. I use this function to make a call to a google client API. I created a client request and then do request.execute(handleCallback);
Once this call is passed i landed in handleCallback function. In this function i do some processing of the response data and save parts of data into json variable available in the file already. And once all processing is done i did something like below.
monthView.month = json;
But this above line is not refreshing my UI with the latest data. Is there anything I am missing? Any suggestions or anything i am doing incorrectly.
You need to use 'set' or 'notifyPath' while changing Polymer Object or Arrays in javascript for the databinding/obserers to work. You can read more about it in https://www.polymer-project.org/1.0/docs/devguide/data-binding.html#path-binding
In your case try below code
monthView.set('month',json);
Updated suggestions:
Wrap your script on main page with. This is required for non-chrome browsers.
addEventListener('WebComponentsReady', function() {})
This could be scoping issue. Try executing 'document.querySelector('#month-view-element');' inside your callback addWorkHoursCallBack. Also, Use .notifyPath instead of .set.

WP Rest API Get Featured Image

I am building a relatively simply blog page that uses the WP Rest Api and AngularJs to show the data on the front-end.
On my home page I want to return the title, followed by the featured image, followed by the excerpt. I have pulled the title and excerpt in fine it seems that in the JSON the featured image is a media Id. What is the best way to pull this data in on the fly?
I have seen various things around the internet that use PHP functions but I think the best way to do it is within a angular controller, just looking for some advice on exactly what the controller would be
List View HTML
<ng-include src=" dir + '/form.html?v=2' "></ng-include>
<div class="row">
<div class="col-sm-8 col-lg-10 col-lg-push-1 post">
<div class="row-fluid">
<div class="col-sm-12">
<article ng-repeat="post in posts" class="projects">
<a class="title" href="#/post/{{post.slug}}"><h2>{{post.title.rendered}}</h2></a>
<p ng-bind-html="post.excerpt.rendered | to_trusted"></p>
</article>
</div>
</div>
</div>
</div>
Controller
.controller('listPage',['$scope','Posts', function($scope,Posts){
$scope.refreshPosts = function(){
Posts.query(function(res){
$scope.posts = res;
});
};
$scope.refreshPosts();
// CLEARFORMFUNCTION
$scope.clear = function(){
$scope.$root.openPost = false;
jQuery('#save').modal('hide');
};
// SAVEMODALOPEN/COSE
$scope.openSaveModal = function(){
jQuery('#save').modal('show');
}
$scope.closeSaveModal = function(){
jQuery('#save').modal('hide');
}
// DATEFUNCTION
$scope.datify = function(date){
$scope.date = newDate(date);
return $scope.date.getDate()+'/'+$scope.date.getMonth()+'/'+$scope.date.getYear();
};
}])
You could also modify the JSON response with PHP. This returns just what I need and is very fast (Using _embed is very slow in my experience)
I have the following code in a plugin (used for adding custom post types), but I imagine you could put it in your theme's function.php file.
php
add_action( 'rest_api_init', 'add_thumbnail_to_JSON' );
function add_thumbnail_to_JSON() {
//Add featured image
register_rest_field( 'post',
'featured_image_src', //NAME OF THE NEW FIELD TO BE ADDED - you can call this anything
array(
'get_callback' => 'get_image_src',
'update_callback' => null,
'schema' => null,
)
);
}
function get_image_src( $object, $field_name, $request ) {
$size = 'thumbnail'; // Change this to the size you want | 'medium' / 'large'
$feat_img_array = wp_get_attachment_image_src($object['featured_media'], $size, true);
return $feat_img_array[0];
}
Now in your JSON response you should see a new field called "featured_image_src": containing a url to the thumbnail.
Read more about modifying responses here:
http://v2.wp-api.org/extending/modifying/
And here's more information on the wp_get_attachment_image_src() function
https://developer.wordpress.org/reference/functions/wp_get_attachment_image_src/
**Note: Don't forget <?php ?> tags if this is a new php file!
Turns out, in my case, there is a new plugin available that solves this without having to make a secondary request. See my recent Q:
WP Rest API + AngularJS : How to grab Featured Image for display on page?
If you send the ?_embed param to the query, it will return more information in the response, like images, categories, and author data.
const result = await axios.get(`/wp-json/wp/v2/my-post-type?_embed`);
// Featured Image
result.data._embedded['wp:featuredmedia'][0].source_url;
// Thumbnail
result.data._embedded['wp:featuredmedia'][0]['media_details']['sizes']['medium']['source_url']

Resources