I have grapesjs editor with mjml plugin and I decided to mark some components as not removable to protect users from remove critical parts of email template.
Sugested way how to do this set attribute of mjml element like this:
<div data-gjs-removable="false">...</div>
But it does not work for <mjml> and <mj-body> tags.
Can I do this after load a template?
yes you can.
After init of editor you need to traverse ComponentDom and set componenets to not removable. Do not forget to remove button from toolbox - it will not work anyway.
const editor = grapesJS.init({...});
editor.on('load', () => {
const notRemovableTags = ['mjml', 'mj-body'];
// recursive function to traverse component tree
const updateRecursive = componentModel => {
if (notRemovableTags.indexOf(componentModel.attributes.tagName) !== -1) {
// set not removable
componentModel.set({removable: false});
// remove remove icon from toolbar
componentModel.set({
toolbar: componentModel.get('toolbar')?.filter(tlb => tlb.command !== 'tlb-delete')
});
}
// recurse
componentModel.get('components').each(model => updateRecursive(model));
}
// start recursion
updateRecursive(this.editor.DomComponents.getComponent());
});
Related
I want to use the same logic when I press the APPLY button but with the ENTER key.
How do I do that?
filterParams: {
closeOnApply:true,
buttons: ['reset', 'apply'],
values: parentCampaignAndNodes.PaymentGroups
},
As you might have found out by now, you can remove the Apply button (by setting filterParams: { buttons: [] } in your column definitions) and then values are submitted onchange.
The solution you ask for is indeed still not available through the AgGrid API. I do have a workaround, but beware it uses direct DOM bindings which is not recommended when working with React.
const applyFilterOnEnter: AgGridReactProps['onFilterOpened'] = ev => {
const inputElem = ev.eGui.querySelector('.ag-filter-body .ag-input-field');
const applyButtonElem = ev.eGui.querySelector('.ag-filter-apply-panel button[ref=applyFilterButton]');
if (inputElem && applyButtonElem) {
inputElem.addEventListener('keydown', keyEv => {
if ((keyEv as KeyboardEvent).key === 'Enter') {
(applyButtonElem as HTMLButtonElement).click();
}
});
}
};
return <AgGridReact
onFilterOpened={applyFilterOnEnter}
/>
Hi I have to drag a component which is outside a frame and drop into a iframe body(dragable source)
but it is not draging my component into iframe dragable source
this is code which is am using for get into the iframe and it is working means it is going into the iframe and finding element
const getIframeDocument = () => {
return cy.get(".gjs-frame").its("0.contentDocument").should("exist");
};
const getIframeBody = () => {
// get the document
return (
getIframeDocument()
// automatically retries until body is loaded
.its("body")
.should("not.be.undefined")
// wraps "body" DOM element to allow
// chaining more Cypress commands, like ".find(...)"
.then(cy.wrap)
);
};
and
for drag and drop into iframe like this
getIframeBody().find("#wrapper").as("Target");
cy.get('[title="Url"]').drag("#Target");
this is not giving me any error this drag function working but at the same time drag and drop is not happening
drag and drop execution pic must see this
you can do this by using this code. You just need to change selectors according to your website.
// Define Locators.
drag_element = '.blockbuilder-content-tools .blockbuilder-content-tool:nth-
child(6)';
drop_element = '#u_body #u_row_2';
text_indentation = '[aria-label="Align center"]';
new_box_text = 'This is a new Text block. Change the text.'
// Get iframe body and then drag & drop element.
iframe_interaction(){
let ifm = cy.get('iframe');
ifm
.should(iframe => expect(iframe.contents().find('body')))
.then(iframe => cy.wrap(iframe.contents().find('body')))
.within({}, $iframe => {
cy.get(this.drag_element).drag(this.drop_element, { force: true })
cy.contains(this.new_box_text).dblclick({force: true})
cy.get(this.text_indentation).click()
})
Github Link: https://github.com/sardar-usman/Cypress--Drag-and-Drop
I'm currently creating a 'smartobject' widget. In the widgets dialog, the user can choose a 'smartobject', which simply put, generates some html, which should be added to the editor. Here comes the tricky part: the html sometimes div elements and sometimes simply span elements. In the case of the div variant, the widget should be wrapped in a div 'template'. In the case of a span variant, the widget should be wrapped in a span and the html should be added 'inline'.
In the widgets API I see the following way to define a template:
editor.widgets.add('smartobject', {
dialog: 'smartobject',
pathName: lang.pathName,
template: '<div class="cke_smartobject"></div>', // <------
upcast: function(element) {
return element.hasClass('smartObject');
},
init: function() {
this.setData('editorHtml', this.element.getOuterHtml());
},
data: function() {
var editorHtml = this.data.editorHtml;
var newElement = new CKEDITOR.dom.element.createFromHtml(editorHtml);
newElement.copyAttributes(this.element);
this.element.setText(newElement.getText());
}
});
But in my case, the template is more dynamic: sometimes a div and sometimes the span will do the correct thing..
How can I fix this without needing to create two widgets which will do the exact same thing, with only the wrapping element as difference?
I've already tried to replace the entire element in the 'data' method, like:
newElement.replace(this.element);
this.element = newElement;
But this seemed not supported: resulted in undefined errors after calling editor.getData().
I'm using ckeditor v4.5.9
Thanks for your help!
It seems I got it working (with a workaround).
The code:
CKEDITOR.dialog.add('smartobject', this.path + 'dialogs/smartobject.js');
editor.widgets.add('smartobject', {
pathName: lang.pathName,
// This template is needed, to activate the widget logic, but does nothing.
// The entire widgets html is defined and created in the dialog.
template: '<div class="cke_smartobject"></div>',
init: function() {
var widget = this;
widget.on('doubleclick', function(evt) {
editor.execCommand('smartobject');
}, null, null, 5);
},
upcast: function(element) {
return element.hasClass('smartObject');
}
});
// Add a custom command, instead of using the default widget command,
// otherwise multiple smartobject variants (div / span / img) are not supported.
editor.addCommand('smartobject', new CKEDITOR.dialogCommand('smartobject'));
editor.ui.addButton && editor.ui.addButton('CreateSmartobject', {
label: lang.toolbar,
command: 'smartobject',
toolbar: 'insert,5',
icon: 'smartobject'
});
And in the dialog, to insert code looks like:
return {
title: lang.title,
minWidth: 300,
minHeight: 80,
onOk: function() {
var element = CKEDITOR.dom.element.createFromHtml(smartobjectEditorHtml);
editor.insertElement(element);
// Trigge the setData method, so the widget html is transformed,
// to an actual widget!
editor.setData(editor.getData());
},
...etc.
UPDATE
I made the 'onOk' method a little bit better: the smartobject element is now selected after the insertion.
onOk: function() {
var element = CKEDITOR.dom.element.createFromHtml(smartobjectEditorHtml);
var elementId = "ckeditor-element-" + element.getUniqueId();
element.setAttribute("id", elementId);
editor.insertElement(element);
// Trigger the setData method, so the widget html is transformed,
// to an actual widget!
editor.setData(editor.getData());
// Get the element 'fresh' by it's ID, because the setData method,
// makes the element change into a widget, and thats the element which should be selected,
// after adding.
var refreshedElement = CKEDITOR.document.getById(elementId);
var widgetWrapperElement = CKEDITOR.document.getById(elementId).getParent();
// Better safe then sorry: if the fresh element doesn't have a parent, simply select the element itself.
var elementToSelect = widgetWrapperElement != null ? widgetWrapperElement : refreshedElement;
// Normally the 'insertElement' makes sure the inserted element is selected,
// but because we call the setData method (to ensure the element is transformed to a widget)
// the selection is cleared and the cursor points to the start of the editor.
editor.getSelection().selectElement(elementToSelect);
},
So in short, I partially used the widget API for the parts I wanted:
- Make the html of the widget not editable
- Make it moveable
But I created a custom dialog command, which simply bypasses the default widget insertion, so I can entirely decide my own html structure for the widget.
All seems to work like this.
Any suggestions, to make it better are appreciated:)!
As suggested in this ckeditor forum thread, the best approach would be to set the template to include all possible content elements. Then, in the data function, remove the unnecessary parts according to your specific logic.
So I made a directive for a toggle (drop down) menu in AngularJS. I used the directive for multiple items within the page but I have a small problem. When one item is open and I click another one I want the previous one to close. The event.preventDefault and event.stopPropagation stops the event for the previous item and doesn't close it. Any ideas on how to fix this? Is there a way to perhaps only stop the event within the scope?
app.directive('toggleMenu', function ($document) {
return {
restrict: 'CA',
link: function (scope, element, attrs) {
var opened = false;
var button = (attrs.menuButton ? angular.element(document.getElementById(attrs.menuButton)) : element.parent());
var closeButton = (attrs.closeButton ? angular.element(document.getElementById(attrs.closeButton)) : false);
var toggleMenu = function(){
(opened ? element.fadeOut('fast') : element.fadeIn('fast'));
};
button.bind('click', function(event){
event.preventDefault();
event.stopPropagation();
toggleMenu();
opened = ! opened;
});
element.bind('click', function(event){
if(attrs.stayOpen && event.target != closeButton[0]){
event.preventDefault();
event.stopPropagation();
}
});
$document.bind('click', function(){
if(opened){
toggleMenu();
opened = false;
}
});
}
};
And here's a Fiddle: http://jsfiddle.net/JknUJ/5/
Button opens content and content should close when clicked outside the div. When clicked on button 2 however content 1 doesn't close.
Basic idea is that you need to share the state between all your dropdown submenus, so when one of them is shown, all others are hidden. The simpliest way of storing state (such as opened or closed) are... CSS classes!
We'll create a pair of directives - one for menu, and another for sumbenu. It is more expressive that just divs.
Here is out markup.
<menu>
<submenu data-caption="Button 1">
Content 1
</submenu>
<submenu data-caption="Button 2">
Content 2
</submenu>
</menu>
Look how readable is it! Say thanks to directives:
plunker.directive("menu", function(){
return {
restrict : "E",
scope : {},
transclude : true,
replace : true,
template : "<div class='menu' data-ng-transclude></div>",
controller : function ($scope, $element, $attrs, $transclude){
$scope.submenus = [];
this.addSubmenu = function (submenu) {
$scope.submenus.push(submenu);
}
this.closeAllSubmenus = function (doNotTouch){
angular.forEach($scope.submenus, function(submenu){
if(submenu != doNotTouch){
submenu.close();
}
})
}
}
}
});
plunker.directive("submenu", function(){
return {
restrict : "E",
require : "^menu",
scope : {
caption : "#"
},
transclude : true,
replace : true,
template : "<div class='submenu'><label>{{caption}}</label><div class='submenu-content' data-ng-transclude></div></div>",
link : function ($scope, $iElement, $iAttrs, menuController) {
menuController.addSubmenu($scope);
$iElement.bind("click", function(event){
menuController.closeAllSubmenus($scope);
$iElement.toggleClass("active");
});
$scope.close = function (){
$iElement.removeClass("active");
}
}
}
});
Look thar we restricted them to HTML elements (restrict : "E"). submenu requires to be nested in menu (require : "^menu"), this allows us to inject menu controller to submenu's link function. transclude and replace controls the position of original markup in compiled HTML output (replace=true means that original markup will be replaced with compiled, transclude inserts parts of original markup to compiled output).
When we've done with this, we just say to menu close all your child menus! and menu iterates over submenus, forcing them to close.
We are adding childs to menu controller in addSubmenu function. It is called in submenus link function, thus every compiled instance of submenu adds itself to menu. Now, closing all submenus is as easy as iterating over all children, this is done by closeAllSubmenus in menu controller.
Here is a full Plunker to play with.
I am looking for a way to make the CKEDITOR wysiwyg content interactive. This means for example adding some onclick events to the specific elements. I can do something like this:
CKEDITOR.instances['editor1'].document.getById('someid').setAttribute('onclick','alert("blabla")');
After processing this action it works nice. But consequently if I change the mode to source-mode and then return to wysiwyg-mode, the javascript won't run. The onclick action is still visible in the source-mode, but is not rendered inside the textarea element.
However, it is interesting, that this works fine everytime:
CKEDITOR.instances['editor1'].document.getById('id1').setAttribute('style','cursor: pointer;');
And it is also not rendered inside the textarea element! How is it possible? What is the best way to work with onclick and onmouse events of CKEDITOR content elements?
I tried manually write this by the source-mode:
<html>
<head>
<title></title>
</head>
<body>
<p>
This is some <strong id="id1" onclick="alert('blabla');" style="cursor: pointer;">sample text</strong>. You are using CKEditor.</p>
</body>
</html>
And the Javascript (onclick action) does not work. Any ideas?
Thanks a lot!
My final solution:
editor.on('contentDom', function() {
var elements = editor.document.getElementsByTag('span');
for (var i = 0, c = elements.count(); i < c; i++) {
var e = new CKEDITOR.dom.element(elements.$.item(i));
if (hasSemanticAttribute(e)) {
// leve tlacitko mysi - obsluha
e.on('click', function() {
if (this.getAttribute('class') === marked) {
if (editor.document.$.getElementsByClassName(marked_unique).length > 0) {
this.removeAttribute('class');
} else {
this.setAttribute('class', marked_unique);
}
} else if (this.getAttribute('class') === marked_unique) {
this.removeAttribute('class');
} else {
this.setAttribute('class', marked);
}
});
}
}
});
Filtering (only CKEditor >=4.1)
This attribute is removed because CKEditor does not allow it. This filtering system is called Advanced Content Filter and you can read about it here:
http://ckeditor.com/blog/Upgrading-to-CKEditor-4.1
http://ckeditor.com/blog/CKEditor-4.1-Released
Advanced Content Filter guide
In short - you need to configure editor to allow onclick attributes on some elements. For example:
CKEDITOR.replace( 'editor1', {
extraAllowedContent: 'strong[onclick]'
} );
Read more here: config.extraAllowedContent.
on* attributes inside CKEditor
CKEditor encodes on* attributes when loading content so they are not breaking editing features. For example, onmouseout becomes data-cke-pa-onmouseout inside editor and then, when getting data from editor, this attributes is decoded.
There's no configuration option for this, because it wouldn't make sense.
Note: As you're setting attribute for element inside editor's editable element, you should set it to the protected format:
element.setAttribute( 'data-cke-pa-onclick', 'alert("blabla")' );
Clickable elements in CKEditor
If you want to observe click events in editor, then this is the correct solution:
editor.on( 'contentDom', function() {
var element = editor.document.getById( 'foo' );
editor.editable().attachListener( element, 'click', function( evt ) {
// ...
// Get the event target (needed for events delegation).
evt.data.getTarget();
} );
} );
Check the documentation for editor#contentDom event which is very important in such cases.