I'm trying to create a text editor with tiptap using reactjs. I would like to create a button next to each "block" of the editor (paragraph block, blockquote block, codeblock block, ...) that allows the user to add a new empty block just before the selected block. It would look like this (Notion editor) :
So the way I tried to do this is to set the cursor's position at the end of the current node :
src/components/blocks/Paragraph.js
const Paragraph = (props) => {
// ...
return {
// ...
<button onMouseDown={() => {
// ...
// props.getPos() gives the position at the beginning of the block
// props.node.nodeSize gives the "length" of the node
const endPos = props.getPos() + props.node.nodeSize;
props.editor.commands.focus(endPos);
// ...
}}>
Add block below
</button>
}
}
So at this point, it works. But then, when I try to insert a new node at this position...
// It will insert a paragraph node containing text "New block added"
props.editor.commands.insertContent({
"type":"paragraph",
"content":[
{
"type":"text",
"text":"New block added"
}
]
})
... I get an error : TypeError: Cannot read property 'nodeType' of null
So, to let you see this error in details, I've made a sandbox on codesandbox.io. To reproduce the error, you just have to focus on the editor, write something random and then click on + button. You should see the error.
Thanks in advance for your help !
Current solution
Currently the best solution I've found :
const endPos = props.getPos() + props.node.nodeSize
// I focus the start of the editor because
// when the cursor is at the end of the node below which
// we want to add a block, it doesn't focus the next block
props.editor.commands.focus('start')
props.editor
.chain()
.insertContentAt(endPos, {type: "paragraph"})
.focus(endPos)
.run()
Related
I already checked the Cypress.io FAQ: https://docs.cypress.io/faq/questions/using-cypress-faq.html#How-do-I-get-an-element%E2%80%99s-text-contents
And with those tips I tryed by myself but it failed.
I want to get an element, grab it content, put it into variable, and use it later.
I want to achive same result like in Selenium element.getText().
describe("Training Test Room Suite", () => {
it("Copy content from an element and past it into next one", () => {
let contentText
cy.visit("http://the-internet.herokuapp.com/login");
cy.get(".example > h2").should(($h2) => {
contentText = $h2.text()
})
cy.get("#username").type(contentText);
})
})
CypressError:
cy.type() can only accept a string or number. You passed in: undefined
In above code, Im trying to copy text from the H2 , put it in the variable contentText, and paste it into input "username". Everything on a demo application http://the-internet.herokuapp.com/login
Can some one help me pls :)
I think it is a closure issue. Try this:
it("Copy content from an element and past it into next one", () => {
let contentText
cy.visit("http://the-internet.herokuapp.com/login");
cy.get(".example > h2").should(($h2) => {
contentText = Cypress.$($h2).text();
return contentText;
})
.then(() => {
cy.get("#username").type(contentText);
});
});
Check here the documentation: cypress then/closures
Is there a way to only show part of a document, or in monacos case of a model, while still getting intellisense for the whole document?
I only want a user to edit a part of a document, but the user should be able to get the right contextual intellisense.
It would be best for my usecase to hide the uneditable sections, but deactivating them would also be ok.
In case this is not possible, is there any embedded editor that can do this, or can this be achived by modifying the language server?
Monaco editor loads every line as a container under a section with the class name "view-lines". Once the editor content has loaded, set "display: none" to the corresponding container for each line that you want to hide.
Implementation: https://jsfiddle.net/renatodc/s6fxedo2/
let value = `function capitalizeFirstLetter(string) {
\treturn string.charAt(0).toUpperCase() + string.slice(1);
}
$(function() {
\tlet word = "script";
\tlet result = capitalizeFirstLetter(word);
\tconsole.log(result);
});
`
let linesToDisable = [1,2,3];
let editor = monaco.editor.create(document.getElementById('container'), {
value,
language: 'javascript',
theme: 'vs-dark',
scrollbar: {
vertical: "hidden",
handleMouseWheel: false
},
scrollBeyondLastLine: false
});
// onLoad event for Monaco Editor: https://github.com/Microsoft/monaco-editor/issues/115
let didScrollChangeDisposable = editor.onDidScrollChange(function() {
didScrollChangeDisposable.dispose();
setTimeout(function() {
$(".monaco-editor .view-lines > div").each(function(i) {
if(linesToDisable.includes(i+1)) {
$(this).css("display","none");
$(this).css("pointer-events","none");
}
});
},1000);
});
Scrolling from Monaco will render the lines again and break the implementation. To prevent this, disable the scrolling feature in Monaco, set a fixed height for the editor container, and use the browser or a parent container to scroll instead.
If you use the arrow keys 'up' or 'down' to navigate to the hidden content, the cursor will still work, and typing will break the implementation. You might be able to use the editor's onKeyDown event to prevent this.
If you're looking for a break-proof implementation, I would suggest loading Monaco editor only with the portion of the document that you wish to edit. Then extend the completion provider (Intellisense) as shown in this example: https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-completion-provider-example
monaco.languages.registerCompletionItemProvider('javascript', {
provideCompletionItems: function(model, position) {
return {
suggestions: [
{
label: "capitalizeFirstLetter",
kind: monaco.languages.CompletionItemKind.Method,
documentation: "Capitalize the first letter of a word",
insertText: "capitalizeFirstLetter("
}
]
};
}
});
monaco.editor.create(document.getElementById("container"), {
value: `$(function() {
\tlet word = "script";
\tlet result = capitalizeFirstLetter(word);
\tconsole.log(result);
});
`,
language: "javascript"
});
Use an AST parser like Esprima to get the identifiers from your source document, and plug these into the suggestions array.
So the problem I'm getting is probably something so, so simple (probably), but it's infuriating my as to why it won't work.
What I'd like to do is click on an Edit button on a website, change a value, and Save it.
However, there are over 120 of these changes to be made, and thus over 120 edit clicks, etc.
Hence why I thought I'd use a Data Table.
And here is my subsequent test code;
const { client } = require('nightwatch-cucumber')
const { defineSupportCode } = require('cucumber')
const globals = require('../../config/globals.js')
var emailEntry = (`input[name='administrator[email]']`)
var passwordEntry = (`input[name='administrator[password]']`)
var existingGtmKey = ("GTM-123456")
var newGtmKey = ("GTM-654321")
var gtmKey = (`//div/input[#value='${existingGtmKey}']`)
var saveButton = (`input[type=submit][value='Save']`)
defineSupportCode(({ Given, Then, When }) => {
Given(/^I've logged into Winit cms$/, () => {
return client
.url('http://winit-stage.bauerpublishing.com/admin/sign_in')
.waitForElementVisible('body', 5000)
// Enter winit email address
.moveToElement(`form#new_administrator ${emailEntry}`, 1,1)
.click(`form#new_administrator ${emailEntry}`)
.setValue(`${emailEntry}`, "*****.*****#*****.co.uk")
// Enter winit password
.moveToElement(`form#new_administrator ${passwordEntry}`, 1,1)
.click(`form#new_administrator ${passwordEntry}`)
.setValue(`${passwordEntry}`, "******")
// Click on the 'Sign in' button
.click("form#new_administrator input[type=submit][value='Sign in']")
})
Then (/^I'm able to change the gtm tag for that site (.*?)$/,
(siteedit) => {
return client
// Click on the 'sites' link
.useXpath()
.click("//a[normalize-space(text())='Sites']")
// Click on Edit button
.click(siteedit)
.click(`${gtmKey}`)
.clearValue(`${gtmKey}`)
// Set new gtm key value
.setValue(`${gtmKey}`, `${newGtmKey}`)
// Click save
.useCss()
.click(`${saveButton}`)
})
})
I thought (wrongly!) this would be pretty straightforward, the script would log into my account, click the edit button, enter new value, press the save key, then do the same for the next website edit button.
It works for the first edit button in the Data table, but then the test fails, giving the following error;
So it looks as though it's running the whole script, and not just the Then part of the script.
But I don't understand why??
Any help would be greatly appreciated.
Many thanks.
My Problem:
I am trying to click options in a dropdown with Nightwatch, using sections in page objects. I'm not sure if it's a problem with the section declaration or i'm missing something scope-related. Problem is that it finds the element as visible, but when it tries to click it will throw error that it cannot locate it using recursion.
What could i try to do to fix this issue using sections?
In the test:
var myPage = browser.page.searchPageObject();
var mySection = searchPage.section.setResults;
// [finding and clicking the dropdown so it opens and displays the options]
browser.pause (3000);
browser.expect.section('#setResults').to.be.visible.before(1000);
myPage.myFunction(mySection, '18');
In the page object:
var searchKeywordCommands = {
myFunction: function (section, x) {
section.expect.element('#set18').to.be.visible.before(2000);
if (x == '18') section.click('#set18');
//[...]
};
module.exports = {
//[.. other elements and commands..]
sections: {
setResults: {
selector: '.select-theme-result', //have also tried with '.select-content' and '.select-options' but with the same result
elements: {
set18: '.select-option[data-value="18"]',
set36: '.select-option[data-value="36"]' //etc
}}}}
Here is my source code:
When i run this piece of core, it seems to find the section, finds the element visible (i also can clearly see that it opens the dropdown and shows the options) but when trying to click any option, i get the error: ERROR: Unable to locate element: Section[name=setResults], Element[name=#set18]" using: recursion
Here is the full error:
My attempts:
I have tried to declare that set18 selector as an individual element instead of inside of the section and everything works fine this way, but won't work inside of the section. I have also tried all the selectors available to define the section's selector, but it won't work with any of them.
This is what i am doing with(LOL)
I assume steps would be (find dropbox - click dropbox - select value).
var getValueElement = {
getValueSelector: function (x) {
return 'li[data-value="'+ x + '"]';
}
};
module.exports = {
//[.. other elements and commands..]
sections: {
setResults: {
commands:[getValueElement],
selector: 'div[class*="select-theme-result"', //* mean contains,sometime class is too long and unique,also because i am lazy.
elements: {
setHighlight:'li[class*="select-option-highlight"]',
setSelected:'li[class*="select-option-selected"]',
//set18: 'li[data-value="18"]',
//set36: 'li[data-value="36"]'
// i think getValueFunction is better,what if you have 100+ of set.
}}}}
In your test
var myPage = browser.page.searchPageObject();
var mySection = searchPage.section.setResults;
// [finding and clicking the dropdown so it opens and displays the options]
mySection
.click('#dropboxSelector')
.waitForElementVisible('#setHighlight',5000,false,
function(){
var set18 = mySection.getValueElement(18);
mySection.click(set18);
});
Ps:in my case(i think your case also), dropbox or any small third-party js framework which is used many times in your web app, so better create a different PageObject for it,make pageObject/section is simple as possible.
I'm having some problems to apply a background-color in the textarea of a ckeditor instance.
When the user clicks on submit without adding any text, it's shown a message telling him to fill all the required fields, and these required fields areas all with the text-fields set with background-color: #CFC183;.
As the ckeditor is created with javascript code, I was using it to try to check if there's any text entered in the text area. if there's no character, I apply the changes.
When I apply in the console this code:
CKEDITOR.instances.body.document.getBody().setStyle('background-color', '#CFC183');
It applies the background exactly like I want to.
So, I added this javascript code in my javascript file to try to manage it, but doesn't seems to be working. Here's my code:
var editorInstance = CKEDITOR.replace('body', { toolbar : 'Full' });
editorInstance.on("instanceReady", function (ev) {
var editorCKE = CKEDITOR.instances.body; readyMap[editorCKE] = true;
editorCKE.setReadOnly(true);
});
var hasText = CKEDITOR.instances.body.document.getBody().getChild(0).getText();
if (!hasText) {
CKEDITOR.on('instanceCreated', function(e) {
e.editor.document.getBody().setStyle('background-color', '#CFC183');
});
}
Firebug shows this error message:
TypeError: CKEDITOR.instances.body.document is undefined
I'm not that good at Javascript, so is there anything wrong with my code?
I already checked this question here, so I believe there's something wrong with my javascript code and I want your help, please.
I guess that you've got an error in this line:
var hasText = CKEDITOR.instances.body.document.getBody().getChild(0).getText();
This is because you're trying to get document element before it's ready (before instanceReady event).
The same error will be thrown here:
if (!hasText) {
CKEDITOR.on('instanceCreated', function(e) {
e.editor.document.getBody().setStyle('background-color', '#CFC183');
});
}
Again - instanceCreated is still too early. You have to move all that code to instanceReady listener. You'll have something like (I'm not sure if I understand what you're trying to achieve):
var editor = CKEDITOR.replace( 'body', { toolbar: 'Full' } );
editor.on( 'instanceReady', function( evt ) {
readyMap[ editor.name ] = true;
editor.setReadOnly( true );
var hasText = editor.document.getBody().getFirst().getText();
if ( !hasText ) {
editor.document.getBody().setStyle( 'background-color', '#CFC183' );
}
} );
As you can see, there is one more issue in your code:
readyMap[editorCKE] = true;
In JS there are no weak maps (yet, but they will be introduced soon). Only strings can be used as a keys of an object. In your case toString() method will be called on editorCKE, which returns [object Object]. That's why I added name property there.