A few days ago, I created this rich text editor, and posted it to reddit and got over 300 upvotes in less than 1 day. I am a beginner, so I don't believe that this project is very complex, but with your help I can improve it, and maybe someone will use this.
I put a lot of work into this and I just want it to get better. If you can help in any way I would appreciate it.
Live demo: https://panagram.herokuapp.com/
GitHub: https://github.com/Stoick001/Panagram.js
Editor:
import controlsPremade from './config';
const execute = (el, command, val) => {
if (el && !command.includes('List')) {
el.classList.toggle('active');
}
document.execCommand(command, false, val ? val: null);
}
export const init = (settings) => {
const controls = controlsPremade[settings.controls];
const ctrElement = settings.ctrElement;
const outElement = settings.outElement;
outElement.contentEditable = true;
outElement.classList.add('output-el');
['click', 'touch'].forEach((evn) => {
ctrElement.addEventListener(evn, (e) => {
outElement.focus();
});
});
outElement.addEventListener('keydown', event => {
if (event.key === 'Tab') {
event.preventDefault();
} else if ((event.key === 'Enter' && document.queryCommandValue('formatBlock') === 'blockquote') || outElement.innerHTML === "" || outElement.innerHTML === "<br>") {
setTimeout(() => {
execute(null, 'formatBlock', '<div>');
}, 0);
}
});
controls.forEach(control => {
const button = document.createElement('button');
button.innerHTML = control.icon;
button.title = control.title;
button.setAttribute('type', 'button');
button.classList.add('ctrl-btn');
if (control.short) {
document.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key.toLowerCase() === control.short) {
button.classList.toggle('active');
}
})
}
['click', 'touch'].forEach((evn) => {
if (control.state) {
button.addEventListener(evn, () => execute(button, control.comName));
['keyup', 'mouseup'].forEach(cnt => {
outElement.addEventListener(cnt, (e) => {
if (document.queryCommandState(control.comName) && !control.comName.includes('List')) {
button.classList.add('active');
} else if (button.classList.contains('active')) {
button.classList.remove('active');
}
});
});
} else if (control.formatBlock) {
button.addEventListener(evn, () => execute(null, control.formatBlock, control.comName));
} else {
button.addEventListener(evn, () => {
const val = (control.extra)();
if (val) {
execute(null, control.comName, val);
}
});
}
});
ctrElement.append(button);
});
}
Config file:
export default {
defaultControls: [
{
icon: '<b>B</b>', //fixed matching in tag surrounding icon text.
title: 'Bold',
comName: 'bold',
state: true,
short: 'b',
}, {
icon: '<i>I</i>',
title: 'Italic',
comName: 'italic',
state: true,
short: 'i',
}, {
icon: '<u>U</u>',
title: 'Underline',
comName: 'underline',
state: true,
short: 'u',
}, {
icon: '<b>H<sub>1</sub></b>',
title: 'Heading 1',
comName: '<h1>',
formatBlock: 'formatBlock'
}, {
icon: '<b>H<sub>2</sub></b>',
title: 'Heading 2',
comName: '<h2>',
formatBlock: 'formatBlock'
}, {
icon: 'P',
title: 'Paragraph',
comName: '<p>',
formatBlock: 'formatBlock'
}, {
icon: '“ ”',
title: 'Quote',
comName: '<blockquote>',
formatBlock: 'formatBlock'
}, {
icon: '#',
title: 'Ordered List',
comName: 'insertOrderedList',
state: true,
}, {
icon: '•',
title: 'Unordered List',
comName: 'insertUnorderedList',
state: true,
}, {
icon: '🔗',
title: 'Link',
comName: 'createLink',
extra: () => window.prompt('Enter the link URL'),
}, {
icon: '📷',
title: 'Image',
comName: 'insertImage',
extra: () => window.prompt('Enter the link URL'),
},
]
}
EDIT: All code is now up to date.