Ember multiple sendAction failing - javascript

Update below
Here's an interesting one. I have a template which contains a form component, and that form component's template also includes a component:
NewObj.hbs -> obj-form.js / obj-form.hbs -> file-uploader.js
The file uploader uses ember-uploader and it works great. I used its events to make sendAction calls to tell the obj-form controller that things are happening, IE pass on the upload progress percentage and indicate when the upload is complete.
If however I add one more sendAction, for some reason obj-form never receives the event for onComplete. No matter how I try it, it just never happens - no errors anywhere.
The file-uploader component is invoked in the obj-form template like so:
{{file-uploader
url="/uploads"
onProgress="fileUploadProgress"
onComplete="fileUploadComplete"
value=newProjectDownloadFile}}
file-uploader.js itself looks like this:
App.FileUploaderComponent = EmberUploader.FileField.extend({
url: '',
onProgress: 'onProgress',
onComplete: 'onComplete',
filesDidChange: function(files) {
var uploadUrl = this.get('url');
var that = this;
var uploader = EmberUploader.Uploader.create({
url: uploadUrl
});
uploader.on('progress', function (e) {
that.sendAction('onProgress', e);
});
uploader.on('didUpload', function (response) {
that.sendAction('onComplete', response);
});
if (!Ember.isEmpty(files)) {
uploader.upload(files[0]);
}
}
});
And obj-form.js has these methods in the actions hash:
fileUploadProgress: function (e) {
this.set('uploadPercentage', e.percent.toFixed(2));
},
fileUploadComplete: function (response) {
this.set('newProjectDownloadFileUrl', response.url);
this.set('newProjectDownloadFileName', response.fileName);
this.send('addDownload');
},
It works great. But I wanted to make a progress bar only appear while uploading and disappear when upload was complete. I added the property uploadInProgress: false to the obj-form controller, and this:
isUploading: function () {
return this.uploadInProgress;
}.property('this.uploadInProgress')
I use {{#if isUploading}} to show/hide the progress bar.
I added:
this.set('uploadInProgress', false);
to the obj-form fileUploadComplete() method, and added this new method:
fileUploadStart: function () {
this.set('uploadInProgress', true);
},
I modified the file-uploader component call to this:
{{file-uploader
url="/connect/projectDownloads/file"
onStart="fileUploadStart"
onProgress="fileUploadProgress"
onComplete="fileUploadComplete"
value=newProjectDownloadFile}}
And changed file-uploader.js to look like this:
App.FileUploaderComponent = EmberUploader.FileField.extend({
url: '',
onStart: 'onStart',
onProgress: 'onProgress',
onComplete: 'onComplete',
filesDidChange: function(files) {
var uploadUrl = this.get('url');
var that = this;
var uploader = EmberUploader.Uploader.create({
url: uploadUrl
});
uploader.on('progress', function (e) {
that.sendAction('onProgress', e);
});
uploader.on('didUpload', function (response) {
that.sendAction('onComplete', response);
});
if (!Ember.isEmpty(files)) {
that.sendAction('onStart', true);
uploader.upload(files[0]);
}
}
});
I've tried putting the that.sendAction('onStart', true); bit in all kinds of places in file-uploader.js but if that line exists, then the obj-form controller never receives the onComplete action. I have absolutely no idea why. I take that line out, it works again.
UPDATE ok I've found something new. It's not the sendAction that breaks it, it's this line in obj-form.js:
this.set('uploadInProgress', true);
For some reason, when I set that to true, it all falls apart. I'm wondering if I'm doing something wrong with the way I'm trying to make that work?
Could it be something to do with didInsertElement? I noticed that's getting triggered when I set uploadInProgress to true - because that property is being used to determine whether the progress bar appears on the page or not. So maybe I'm using didInsertElement incorrectly? Here's my didInsertElement and willDestroyElement:
didInsertElement: function() {
// console.log('didInsertElement');
this.set( 'postType', this.get( 'content' ).get( 'postType' ) );
this.set( 'projectDownloads', this.get( 'content' ).get( 'projectDownloads' ) );
this.set( 'projectLinks', this.get( 'content' ).get( 'projectLinks' ) );
this.set( 'published', this.get( 'content' ).get( 'published' ) );
Ember.addObserver( this.get( 'content' ), 'postType', this, this.postTypeDidChange );
Ember.addObserver( this.get( 'content' ), 'projectDownloads', this, this.projectDownloadsDidChange );
Ember.addObserver( this.get( 'content' ), 'projectLinks', this, this.projectLinksDidChange );
Ember.addObserver( this.get( 'content' ), 'published', this, this.publishedDidChange );
},
willDestroyElement: function() {
console.log('willDestroyElement');
Ember.removeObserver( this.get( 'content' ), 'postType', this.postTypeDidChange );
Ember.removeObserver( this.get( 'content' ), 'projectDownloads', this.projectDownloadsDidChange );
Ember.removeObserver( this.get( 'content' ), 'projectLinks', this.projectLinksDidChange );
Ember.removeObserver( this.get( 'content' ), 'published', this.publishedDidChange );
}
I've tried modifying isUploading to the alias style you recommended, but that hasn't helped anything. The idea is when that gets set to true, it makes the progress bar appear and the fileupload form disappear. Then when it gets set to false the reverse happens. I'm open to other ways to make this happen in Ember, I just can't figure out what I'm doing that's breaking things.

Not the answer yet, but won't fit in comment correctly... this isn't necessary in the dependency chain, even easier is to change this to an alias.
From
isUploading: function () {
return this.uploadInProgress;
}.property('this.uploadInProgress')
To
isUploading: Ember.computed.alias('uploadInProgress')

Related

Track changes to multiple Ckeditor 5 instances in Javascript

I'm accessing an iframe thru fancybox 3 with multiple ckeditor. It's working well but I want to track if a user has inputted anything on the textboxes so that I can change the button of the fancybox and give them an alert once they hit close. This is my JS code on the iframe page:
window.editors = {};
document.querySelectorAll( '.editor' ).forEach( ( node, index ) => {
ClassicEditor
.create( node, {
removePlugins: ['Heading', 'BlockQuote', 'CKFinderUploadAdapter', 'CKFinder', 'EasyImage', 'Image', 'ImageCaption', 'ImageStyle', 'ImageToolbar', 'ImageUpload', 'MediaEmbed'],
} )
.then( newEditor => {
window.editors[ index ] = newEditor;
} );
editors[ index ].document.on( 'change', () => {
console.log( 'Alert: The document has changed!, are you sure you want to close without saving?' );
} );
} );
But it's giving me an error:
ifram.php:1070 Uncaught TypeError: Cannot read properties of undefined (reading 'document')
at ifram.php:1070
at NodeList.forEach (<anonymous>)
at ifram.php:1060
I also tried taking this out off document.querySelectorAll() function
editors.document.on( 'change', () => {
console.log( 'Alert: The document has changed!, are you sure you want to close without saving?' );
} );
But it gave me another error: Uncaught TypeError: Cannot read properties of undefined (reading 'on')
You could use newEditor.model.document.on('change:data') document
<script src="https://cdn.ckeditor.com/ckeditor5/30.0.0/classic/ckeditor.js"></script>
<textarea class="editor"></textarea>
<script>
window.editors = {};
document.querySelectorAll( '.editor' ).forEach( ( node, index ) => {
ClassicEditor
.create( node, {
removePlugins: ['Heading', 'BlockQuote', 'CKFinderUploadAdapter', 'CKFinder', 'EasyImage', 'Image', 'ImageCaption', 'ImageStyle', 'ImageToolbar', 'ImageUpload', 'MediaEmbed'],
} )
.then( newEditor => {
window.editors[ index ] = newEditor;
newEditor.model.document.on('change:data', ()=> {
console.log( 'Alert: The document has changed!, are you sure you want to close without saving?' );
});
} );
});
</script>

WordPress Gutenberg Blocks Get Author Info

I feel like what I want to accomplish is simple: create a custom WordPress Gutenberg Block that auto-populates with the author name and image and is not editable. Then render this block on the frontend post.
I've gone at this from a lot of different angles, and I think what I need is a Dynamic Block
https://developer.wordpress.org/block-editor/tutorials/block-tutorial/creating-dynamic-blocks/
First issue is the wp.data.select("core/editor").getCurrentPostId() doesn't load the post ID. But then I'll be honest, I don't know if the return would even have the author info I want anyway. Any guidance is most appreciated.
const post_id = wp.data.select("core/editor").getCurrentPostId();
var el = wp.element.createElement;
wp.blocks.registerBlockType('my-plugin/author-block', {
title: 'Author Footer',
icon: 'admin-users',
category: 'my-blocks',
attributes: {
// Nothing stored
},
edit: wp.data.withSelect( function(select){ // Get server stuff to pass to our block
return {
posts: select('core').getEntityRecords('postType', 'post', {per_page: 1, post_id})
};
}) (function(props) { // How our block renders in the editor
if ( ! props.posts ) {
return 'Loading...';
}
if ( props.posts.length === 0 ) {
return 'No posts';
}
var post = props.posts[0];
// Do stuff with the post like maybe get author name and image???
// ... Return elements to editor viewer
}), // End edit()
save: function(props) { // Save the block for rendering in frontend post
// ... Return saved elements
} // End save()
});
Finally figured something out so thought I'd share.
edit: function(props) { // How our block renders in the editor in edit mode
var postID = wp.data.select("core/editor").getCurrentPostId();
wp.apiFetch( { path: '/wp/v2/posts/'+postID } ).then( post => {
var authorID = post.author
wp.apiFetch( { path: '/wp/v2/users/'+authorID } ).then( author => {
authorName = author.name;
authorImage = author.avatar_urls['96'];
jQuery('.author-bottom').html('<img src="'+authorImage+'"><div><h6>Written by</h6><h6 class="author-name">'+authorName+'</h6></div>');
});
});
return el(
'div',
{
className: 'author-bottom'
},
el(
'img',
{
src: null
}
),
el(
'div',
null,
el(
'h6',
null,
'Written by'
),
el(
'h6',
{
className: 'author-name'
},
'Loading...'
)
)
); // End return
}, // End edit()
As soon as the block loads, it initiates a couple apiFetch calls (one to get the author ID, then another to get the author's name and image).
While that's happening, the author block loads with the temporary text "Loading..."
Once the apiFetch is complete I used jQuery to update the author block.
There is no save function because it's a dynamic block. I registered the block in my plugin php file like so:
function my_author_block_dynamic_render($attributes, $content) {
return '
<div class="author-bottom">
<img src="'.get_avatar_url(get_the_author_email(), ['size' => '200']).'">
<div>
<h6>Written by</h6>
<h6 class="author-name">'.get_the_author().'</h6>
</div>
</div>';
}
function my_dynamic_blocks() {
wp_register_script(
'my-blocks-script',
plugins_url( 'my-block.js', __FILE__ ),
array( 'wp-blocks', 'wp-element' ));
register_block_type( 'my-blocks/author-block', array(
'editor_script' => 'my-blocks-script',
'render_callback' => 'my_author_block_dynamic_render'));
}
add_action( 'init', 'my_dynamic_blocks' );
I couldn't find a good tutorial on creating a custom author block, so hopefully this example helps some poor soul down the road!

How to run a jQuery code after an angular function was loaded?

Basically I upload an image through angular with ng-file-upload and then I want to display it and initialize a jQuery plugin called Cropper.
How can I run the jQuery code from this one? The only way it works is when I have the $scope.loadedImage loaded before the upload.
HTML:
<img id="cropImage" ng-src="/{{loadedImage}}" />
Angular
$scope.upload = function (dataUrl) {
Upload.upload({
url: '/photocalc/upload/file',
method: 'POST',
file: dataUrl,
}).then(function (response) {
$timeout(function () {
$scope.result = response.status;
$scope.loadedImage = response.data;
jQuery('#cropImage').cropper({
viewMode: 0,
zoomable: false,
dragMode: 'crop',
guides: true,
highlight: true,
cropBoxMovable: true,
cropBoxResizable: true,
crop: function(e) {
// Output the result data for cropping image.
console.log(e.x);
console.log(e.y);
console.log(e.width);
console.log(e.height);
console.log(e.rotate);
console.log(e.scaleX);
console.log(e.scaleY);
}
});
});
}, function (response) {
if (response.status > 0) $scope.errorMsg = response.status
+ ': ' + response.data;
}, function (evt) {
$scope.progress = parseInt(100.0 * evt.loaded / evt.total);
});
}
I would write an own directive which will observe the src attribute and init the cropper when the image loaded.
In my mind the image.onload will only fire once so you would have to remove and place a new image but try it without removing at first :)
edit: as Kevin B said this thought was wrong
angular.module( "$name$" ).directive( "imageCropper", [ function () {
return {
restrict: "A",
link : function ( $scope, $el, $attr ) {
$attr.$observe( "src", function ( src ) {
$el.on( "load", function () {
$timeout( function () {
$scope.result = response.status;
$scope.loadedImage = response.data;
jQuery( '#cropImage' ).cropper( {
viewMode : 0,
zoomable : false,
dragMode : 'crop',
guides : true,
highlight : true,
cropBoxMovable : true,
cropBoxResizable: true,
crop : function ( e ) {
// Output the result data for cropping image.
console.log( e.x );
console.log( e.y );
console.log( e.width );
console.log( e.height );
console.log( e.rotate );
console.log( e.scaleX );
console.log( e.scaleY );
}
} );
} );
} );
} );
}
}
} ] );
For this kind of situation you can use setTimeout and execute your jquery code after a little delay. Because it took a little bit(a very little) of time for angular to bind data in the view. So you need a little delay.
Also I've written a blog post where I've given a generic solution to call a callback function after ng-repeat finish binding all its data. You can try that as well. Check this post here. Let me know if you have any confusion. Thanks.

Simple Backbone.js collection infinite paging

My server-side api follows a classic results-paging model, e.g.
/api/transactions/ => page 1 (10 items default limit)
/api/transactions/?p=2 => page 2
I want to build a infinite-scrolling system with Backbone views.
I already have non-paging collection+views setup. The parent view looks like this:
Backbone.View.extend({
initialize: function() {
this.collection = TransactionCollection;
this.fetch();
this.listenTo( this.collection, 'reset', this.renderEntries );
this.listenTo( this.collection, 'add', this.fetch );
this.rowViews = [];
this.render();
},
fetch: function() {
this.collection.fetch({ reset:true });
},
render: function() {
this.$el.html( template() );
this.renderEntries();
return this;
},
renderEntries: function() {
this.clearEntryRows();
this.collection.each(function(item) {
var row = new TransactionItemView( item );
this.rowViews.push( row );
this.$el.find('.entry-list').append( row.render().el );
}, this);
},
clearEntryRows: function() {
this.rowViews.forEach(function(v) {
if (v.close) v.close();
});
this.rowViews = [];
},
// ...
}
This is the relevant part of the view code (child views are simple item views, rendering themselves with a template).
The collection is still very basic:
var TransactionCollection = Backbone.Collection.extend({
model: Transaction,
url: '/api/transactions'
});
Now it's time to add pagination. I think I'm going to add a button "MORE...", after each renderEntries() call. That button will fetch for the next page (without resetting the collection) and another renderEntries is called. The this.clearEntryRows() will be moved to the reset callback.
My question is: how can I fetch the second page and add models without resetting the collection and intercept just that event, to render next entry pages?
I've read something about 'sync' event: in my understanding, 'reset' gets fired only when I fetch with reset:true, 'sync' gets fired every time I fetch the collection, anyway.
So, if this is correct, I can clear entry rows only on reset event and display rows in sync. But how can I display only the newly added (e.g. page 2) rows to my list?
I'm a little confused.
this.collection.fetch({ add: true, remove: false, merge: false, data: {p: 2} });
this allow you to fetch with specified get parameters and only add not existing entries to collection.
In your view:
initialize: function () {
this.listenTo(this.collection, 'add', handlerForRenderingNewEntries);
}
To render only new models, you can return them with specified attribute, extra property 'page' for example. Filter them by this attribute and send to rendrer.

Validate (custom) before adding file

I would like to make sure if the user is logged-in or not before anything is added to the uploader area (thumbnails).
it's easy enough checking stuff on the validate event callback, but I can't seem to figure out how to stop the item from being added to the DOM, even if I return false in my validation or submit events...the item just gets a class of qq-upload-fail... but I want to completly do NOTHING if the user isn't logged in...maybe the validation event handler is not the place to put this logic, but where else?
My initialization code (jQuery):
this.holderElm.fineUploader({
template: 'qq-simple-thumbnails-template',
// uploaderType: 'basic',
request: {
endpoint: '/image/upload_image/' + this.settings.uploadID
},
validation: {
allowedExtensions : ['jpeg', 'jpg', 'gif', 'png'],
sizeLimit : this.settings.sizeLimit,
itemLimit : this.settings.itemsLimit
},
retry: {
enableAuto: true
}
})
.on("validate", this.onValidate.bind(this) )
.on('submit', this.onSubmit.bind(this) )
.on('error', this.onError.bind(this) )
.on('upload', this.onUpload.bind(this) )
.on('complete', this.onComplete.bind(this) )
.on('allComplete', this.onAllComplete.bind(this) );
Use the onSubmit event -- which called is before the item has been added to the DOM -- and return false or a qq.Promise that will be rejected to disable the addition of that item to the file list.
var userLoggedIn = true;
// snippet ...
onSubmit: function(id, name){
return userLoggedIn;
},
or with promises:
function isUserLoggedIn(username){
var promise = new qq.Promise();
$.get("/api/user/"+username, function(err, data){
if (err) return promise.failure(err);
return promise.success(data);
});
return promise;
}
// snippet ...
onSubmit: function(id, name){
return isUserLoggedIn(username);
},

Categories