I want my editor have "Default font".
Which means, by default we have an editor where the very first <p></p> tag will be wrapped with <span style="font-size:18pt; font-family:Arial"> and additionally be wrapped with <em>, <strong>, <u> tags if needed (as example)
I have tried to do it like this:
const {
toggleBold,
toggleItalic,
toggleUnderline,
setTextColor,
focus,
setFontSize,
setFontFamily,
selectAll,
} = useCommands();
const view = useEditorView();
const { getFontSizeForSelection } = useHelpers();
useEffect(() => {
selectAll();
if (defaultFont?.size) {
setFontSize(defaultFont?.size + 'pt', view.state.selection);
activeToolbarItems.fontSize();
}
if (defaultFont?.family) {
setFontFamily(defaultFont?.family);
activeToolbarItems.fontFamily();
}
if (defaultFont?.attributes?.bold) {
toggleBold();
}
if (defaultFont?.attributes?.underline) {
toggleUnderline();
}
if (defaultFont?.attributes?.italic) {
toggleItalic();
}
}, []);
But this is not working. No matter do I have any text in the editor or not. And I do not understand why... What am I doing wrong here?
I also tried to prepare "Initial html string" and pass it as "content" for "useRemirror" hook. But it resets after any change in the editor.
Any advices?
P.S. All extensions are included.
const { manager, state, setState } = useRemirror({
extensions: () => [
new BoldExtension(),
new ItalicExtension(),
new UnderlineExtension(),
new TextColorExtension(),
new FontFamilyExtension(),
new FontSizeExtension(),
],
content: defaultValue,
selection: 'start',
stringHandler: 'html',
});
Related
I've been thinking about how to do this for days and if you could help me.
I expose you, I have followed the CKEditor 5 tutorial to the point of including the mentions, this is where my problem begins.
Following the tutorial we come to the part of the output of the mention, this as they do in the tutorial I have transformed it from <span> to <a> together with its class, its URL and its data. Well the editor shows it fine until you want to edit the post.
That is, imagine this message:
Hello world I am the first code of #undercover
Well when I include it in the database everything is correct, but when we return that same message to the editor it becomes:
Hello world I am the first code of #undercover
Investigating and as my Javascript is quite low I have been trying things.
The conversion. I've tried but there is something I can't understand and it's like passing the values to the function. Let me explain, when I pass that <a> that I save in the database, if I transform it into a <span> and then insert it if it tries to make the change to mention but the class attribute and the href attribute are "orphaned".
Well, I have 3 ideas and I can't do any of them at some point I get stuck, so I ask you for help.
My idea is to return the text I have in the database and the editor reads it fine.
Idea 1: Put the text in Javascript and identify and exchange the mentions that are in the database by the function of the mentions command, this is really complicated for me because it is very abstract, even so I am still looking for how to do it.
Idea 2: Save the value in the database in another way, this has been a last idea, how to search and put the of the mention but with the custom values. Even if {mention: {id: #undercover}} were saved in the database, I wouldn't care as long as it was later transformed correctly in the editor.
Idea 3: The use of conversions, I have managed to understand this and it has cost me that its function is to identify the mention within the editor and exchange it for the data you want. In this idea I can't understand how to pass the values other than manually, that is, how to pass the class and href attributes.
Here I leave you the section of the code, I hope you can give me a hand and thank you very much.
function MentionCustomization( editor ) {
// The upcast converter will convert <a class="mention" href="" data-user-id="">
// elements to the model 'mention' attribute.
editor.conversion.for( 'upcast' ).elementToAttribute( {
view: {
name: 'a',
key: 'data-mention',
classes: 'mention',
attributes: {
href: true,
'data-user-id': true,
}
},
model: {
key: 'mention',
value: viewItem => {
// The mention feature expects that the mention attribute value
// in the model is a plain object with a set of additional attributes.
// In order to create a proper object, use the toMentionAttribute helper method:
const mentionAttribute = editor.plugins.get( 'Mention' ).toMentionAttribute( viewItem, {
// Add any other properties that you need.
link: viewItem.getAttribute( 'href' ),
userId: viewItem.getAttribute( 'data-user-id' )
} );
return mentionAttribute;
}
},
converterPriority: 'high'
} );
// Downcast the model 'mention' text attribute to a view <a> element.
editor.conversion.for( 'downcast' ).attributeToElement( {
model: 'mention',
view: ( modelAttributeValue, { writer } ) => {
// Do not convert empty attributes (lack of value means no mention).
if ( !modelAttributeValue ) {
return;
}
return writer.createAttributeElement( 'a', {
class: 'group-color-'+modelAttributeValue.group,
'data-mention': modelAttributeValue.id,
// 'data-user-id': modelAttributeValue.userId,
'href': '/member/profile/'+modelAttributeValue.user_id,
}, {
// Make mention attribute to be wrapped by other attribute elements.
priority: 20,
// Prevent merging mentions together.
id: modelAttributeValue.uid,
} );
},
converterPriority: 'high'
} );
}
$.ajax({
type: "POST",
dataType: "json",
url: "/members/list_json",
success: function(info){
ClassicEditor
.create( document.querySelector( '#comment' ), {
extraPlugins: [ MentionCustomization ],
updateSourceElementOnDestroy: true,
language: 'es',
toolbar: [ 'bold', 'italic', '|' , 'link', '|', 'bulletedList'],
mention: {
feeds: [
{
marker: '#',
feed: getFeedItems,
minimumCharacters: 2,
itemRenderer: customItemRenderer,
}
]
}
} )
.then( editor => {
window.editor = editor;
/*
*/
} )
.catch( err => {
console.error( err.stack );
} );
let list_members = [];
for(let i = 0; i < info.length; i++){
var member = info[i];
list_members.push(member);
}
function getFeedItems( queryText ) {
return new Promise( resolve => {
setTimeout( () => {
const itemsToDisplay = list_members
.filter( isItemMatching )
.slice( 0, 10 );
resolve( itemsToDisplay );
}, 100 );
} );
function isItemMatching( item ) {
const searchString = queryText.toLowerCase();
return (
item.username.toLowerCase().includes( searchString )
);
}
}
},
});
function customItemRenderer( item ) {
const itemElement = document.createElement( 'span' );
const avatar = document.createElement( 'img' );
const userNameElement = document.createElement( 'span' );
itemElement.classList.add( 'mention__item');
avatar.src = `${ item.avatar }`;
avatar.classList.add('image-fluid', 'img-thumbnail', 'rounded-circle');
userNameElement.classList.add( 'mention__item__user-name' );
userNameElement.style.cssText = 'color: '+ item.group_color +';';
userNameElement.textContent = item.id;
itemElement.appendChild( avatar );
itemElement.appendChild( userNameElement );
return itemElement;
}
I try to build a simple Text Editor using Electron.
At the moment I want to add a custom title bar which doesn't really work as the buttons are not clickable...
I added an onclick tag to the buttons.
main.js:
//-- variables --\\
const { BrowserWindow, app, Menu, dialog, ipcMain } = require("electron")
let window
let filePath = null //currently opened file
let openFiles = [] //opened files
//-- startup --\\
app.whenReady().then(function()
{
createWindow()
createMenu()
})
//-- functions --\\
function createWindow()
{
window = new BrowserWindow
({
width: 1000,
height: 600,
frame: false,
icon: "./assets/icon.png",
darkTheme: true,
autoHideMenuBar: true,
webPreferences:
{
nodeIntegration: true,
contextIsolation: false
}
})
//window.maximize()
window.loadFile("./frontend/index.html")
window.webContents.openDevTools()
}
function createMenu()
{
Menu.setApplicationMenu(Menu.buildFromTemplate
([
{
label: "File",
submenu:
[
{
label: "Save",
click: saveFile,
accelerator: "CmdOrCtrl+S"
},
{
label: "Open",
click: openFile,
accelerator: "CmdOrCtrl+O"
},
{
label: "New",
click: newFile,
accelerator: "CmdOrCtrl+N"
}
]
}
]))
}
async function promptFilePathOpen()
{
await dialog.showOpenDialog
({ properties: ["openFile"] }).then(function(res)
{
if(res.canceled) return
filePath = res.filePaths[0]
})
}
async function promptFilePathSave()
{
await dialog.showSaveDialog().then(function(res)
{
if(res.canceled) return
filePath = res.filePath
})
}
async function openFile()
{
await promptFilePathOpen()
//openFiles.push(filePath)
window.webContents.send("crd-openFile", filePath)
}
async function saveFile()
{
if(filePath == null || undefined) await promptFilePathSave()
window.webContents.send("crd-saveFile", filePath)
}
async function newFile()
{
filePath = null
await promptFilePathSave()
window.webContents.send("crd-resetEditor")
window.webContents.send("crd-saveFile", filePath)
}
//-- listeners --\\
app.on("window-all-closed", function()
{
if(process.platform != "darwin") app.quit()
})
ipcMain.on("crd-minimizeWindow", function() //does not get called by the renderer
{
//coming soon
})
ipcMain.on("crd-toggleWindowSize", function() //does not get called by the renderer
{
//coming soon
})
ipcMain.on("crd-closeWindow", function() //does not get called by the renderer
{
console.log("quit")
})
renderer/script.js:
//-- imports --\\
const { ipcRenderer } = require("electron")
const fs = require("fs")
const editorElem = document.querySelector("textarea.editor")
//-- functions --\\
function minimizeWindow() // does not get called by the button (index.html)
{
ipcRenderer.send("crd-minimizeWindow")
}
function toggleWindowSize() // does not get called by the button (index.html)
{
ipcRenderer.send("crd-toggleWindowSize")
}
function closeWindow() // does not get called by the button (index.html)
{
ipcRenderer.send("crd-closeWindow")
}
//-- listeners --\\
ipcRenderer.on("crd-openFile", function(e, path)
{
editorElem.value = fs.readFileSync(path, "utf-8")
})
ipcRenderer.on("crd-saveFile", function(e, path)
{
fs.writeFile(path, editorElem.value , function(res, err)
{
if(err) throw err
})
})
ipcRenderer.on("crd-resetEditor", function()
{
editorElem.value = ""
})
The entire code is avalable on my GitHub, because I do not want the whole question consisting of code :)
Two issues here:
You defined functions like closeWindow, but you didn't actually add an event listener for them. You mention onclick but I can't see that in your code. So the first step would be to add document.querySelector('.closeWindow').addEventListener('click', closeWindow) .
You made the whole title bar draggable, including the buttons. That means that the role of the buttons is also a draggable area, so when you click them, you start the drag operation instead of sending a click event. The solution is therefore to make sure the button area does not have the -webkit-app-region: drag style but only the area left to them has. This will probably require you to redesign the HTML layout for the title bar a bit, since this won't work well with the whole thing being a grid.
For more details, see this tutorial.
i'm having a problem using draft-js text editor in react. My intention is to create a text editor where I can get the text to create a post, but I also need to get the image that you uploaded to the editor to place it as the cover of the post.
The following happens every time I load an image, it makes the request to the server. What I need is that the request is made once the user has submitted at the end of the creation of the post. So in this way I should not store images that the user regretted and then select another image.
frontend code:
export function EditorPost(props:any):JSX.Element{
let {postcontent, setpostcontent} = props;
const [content, setContent] = useState(EditorState.createWithContent(ContentState.createFromBlockArray(
convertFromHTML("<h2>hola nena</h2>")
)));
const onEditorStateChange = (editorState:any) => {
setContent(editorState); //UPDATE EDITOR STATE AND VALUE
setpostcontent({
...postcontent,
body:draftToHtml(convertToRaw(content.getCurrentContent())) //GET ONLY CONTENT OF THE FORM
});
console.log(postcontent);
}
function uploadImageCallBack(file:any):Promise<any> {
return new Promise(
async (resolve, reject) => {
const formdata = new FormData()
formdata.append('image', file)
try{
let resp = await axiosIntance.post('/upload', formdata);
resolve(resp)
}catch(err){
reject(err);
}
}
);
}
return (
<>
<Editor
editorState={content}
onEditorStateChange={onEditorStateChange}
toolbar={{
inline: { inDropdown: true },
list: { inDropdown: true },
textAlign: { inDropdown: true },
link: { inDropdown: true },
history: { inDropdown: true },
image: { uploadCallback: uploadImageCallBack, alt: { present: true, mandatory: true } },
}}
/>
</>
)
}
on the server i am using node and mysql. and create a model in the database for the posts, which contains the following columns: title, image (obtained from the text editor), body (html obtained from the text editor)
I am building a simple rich text editor for writing blogs. I want to enforce that first Node always be the title (h1 tag) which user should not be able to delete!
Is there a way to achieve this?
You can achieve this via Custom Document, where you can define the document structure. Tip tap has an example page here https://tiptap.dev/examples/custom-document.
You could put it in content when initializing:
const editor = useEditor({
extensions: [
StarterKit.configure({
heading: {
levels: [1, 2],
},
}),
Dropcursor.configure({
color: "#4B5563",
}),
Placeholder.configure({
showOnlyWhenEditable: true,
placeholder: "Write something or press Enter to add a new block",
}),
CodeBlockLowlight.configure({
lowlight,
}),
TaskList,
CustomTaskItem,
ListItem,
Blockquote,
CustomOrderedList,
CustomHorizontalRule,
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
TableCell,
],
editorProps: {
attributes: {
class: "focus:outline-none subpixel-antialiased",
},
},
autofocus: true,
// THIS
content: `
<h1>Hello there </h1>
`,
})
Extend Document:
const CustomDocument = Document.extend({
// https://tiptap.dev/api/schema#content
content: 'heading block*',
})
Load the CustomDocument and add default placeholder for Heading:
new Editor({
extensions: [
CustomDocument,
Placeholder.configure({
placeholder: ({ node }) => {
if (node.type.name === 'heading' && node.attrs.level == 1) {
return 'H1 placeholder here';
}
},
}),
]
})
After that, you have to find a solution for prevent bold/italic/H2/H3/etc. on your H1. If you have a solution for it, please give the solution in comment.
I followed the initial plugin tutorial and got the Image Insert to work, but I would like to display a custom modal with two input fields instead of the prompt to set some more attributes.
How would I best implement this?
I know how to implement a normal modal in plain JS/CSS but I am a bit confused as to where to put the HTML for the modal to be displayed on the button click.
class Test extends Plugin {
init() {
editor = this.editor
editor.ui.componentFactory.add('SampleView', (locale) => {
const view = new ButtonView(locale)
view.set({
label: 'test',
icon: imageIcon,
tooltip: true
})
view.on('execute', () => {
//here I would like to open the modal instead of the prompt
})
})
}
}
For example, you can try to use SweetAlert2 which is zero-dependency pure javascript replacement for default popups.
import swal from 'sweetalert2';
...
view.on( 'execute', () => {
swal( {
input: 'text',
inputPlaceholder: 'Your image URL'
} )
.then ( result => {
editor.model.change( writer => {
const imageElement = writer.createElement( 'image', {
src: result.value
} );
editor.model.insertContent( imageElement, editor.model.document.selection );
} );
} )
} );