codex-team / codex-team/editor.js
[Bug] Error pasting bold text into Header blocks
- Dominant language
- TypeScript
- Stars
- 31.9k
- Forks
- 2.2k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 1
Description
Describe a bug.
Using the local copy of Header plugin with some minor changes, but when we try to paste any html content with bold or italics text the editor crashes with the following error.
```
editor.js?7524:2 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'reduce')
at eval (editor.js?7524:2:235044)
at Array.map ()
at w.value (editor.js?7524:2:234775)
at w.eval (editor.js?7524:2:230331)
at p (editor.js?7524:2:356625)
at Generator.eval (editor.js?7524:2:357976)
at Generator.eval [as next] (editor.js?7524:2:356996)
```
I suspect it has to do with missing with `pasteConfig` and `onPaste` handling, so we added them like this.
```
static get pasteConfig() {
return {
tags: ['b'],
}
}
onPaste(event) {
console.log(event)
}
```
This fixes the problem but I dont see any logs from the `onPaste(event)` method. It almost seems like its not getting called.
Can someone help confirm how this is suppose to work, as even using empty `tags:[]` inside pasteConfig, fixes our problem, but still no onPaste callback.
Here is our complete Custom header code
```
/**
* @typedef {any} TextData
* @description Tool's input and output data format
* @property {string} text — Text's content
*/
import { icons } from './utils'
/**
* @typedef {any} TextConfig
* @description Tool's config from Editor
* @property {string} placeholder — Block's placeholder
*/
export class Text {
/**
* Render plugin`s main Element and fill it with saved data
*
* @param {{data?: TextData, config?: TextConfig, api?: any, readOnly?: boolean}}
* data — previously saved data
* config - user config for Tool
* api - Editor.js API
* readOnly - read only mode flag
*/
constructor({ data, config, api, readOnly }) {
this.api = api
this.readOnly = readOnly
this.level = config.level || 1
/**
* Styles
*
* @type {any}
*/
this._CSS = {
block: this.api.styles.block,
wrapper: `editor-text${config.level}`,
}
/**
* Tool's settings passed from Editor
*
* @type {TextConfig}
* @private
*/
this._settings = config
/**
* Block's data
*
* @type {TextData}
* @private
*/
this._data = this.normalizeData(data)
/**
* Main Block wrapper
*
* @type {HTMLElement}
* @private
*/
this._element = this.getTag()
}
/**
* Handle H1-H6 tags on paste to substitute it with header Tool
*
* @param {PasteEvent} event - event with pasted content
*/
onPaste(event) {
console.log(event)
const content = event.detail.data;
/**
* Define default level value
*
* @type {number}
*/
let level = this.defaultLevel.number;
switch (content.tagName) {
case 'H1':
level = 1;
break;
case 'H2':
level = 2;
break;
case 'H3':
level = 3;
break;
}
if (this._settings.levels) {
// Fallback to nearest level when specified not available
level = this._settings.levels.reduce((prevLevel, currLevel) => {
return Math.abs(currLevel - level) < Math.abs(prevLevel - level) ? currLevel : prevLevel;
});
}
this.data = {
level,
text: content.innerHTML,
};
}
/**
* Used by Editor.js paste handling API.
* Provides configuration to handle H1-H6 tags.
*
*
*/
static get pasteConfig() {
return {
tags: [], // still works with empty tags
}
}
/**
* Normalize input data
*
* @param {TextData} data - saved data to process
*
* @returns {TextData}
* @private
*/
normalizeData(data) {
const newData = {}
if (typeof data !== 'object') {
data = {}
}
newData.text = data.text || ''
return newData
}
/**
* Return Tool's view
*
* @returns {HTMLElement}
* @public
*/
render() {
return this._element
}
/**
* Method that specified how to merge two Text blocks.
* Called by Editor.js by backspace at the beginning of the Block
*
* @param {TextData} data - saved data to merger with current block
* @public
*/
merge(data) {
const newData = {
text: this.data.text + data.text,
level: this.data.level,
}
this.data = newData
}
/**
* Validate Text block data:
* - check for emptiness
*
* @param {TextData} blockData — data received after saving
* @returns {boolean} false if saved data is not correct, otherwise true
* @public
*/
validate(blockData) {
// Keep empty string blocks instead of filtering it out.
// return blockData.text.trim() !== ''
return true
}
/**
* Extract Tool's data from the view
*
* @param {HTMLHeadingElement} toolsContent - Text tools rendered view
* @returns {TextData} - saved data
* @public
*/
save(toolsContent) {
return {
text: toolsContent.innerHTML,
}
}
/**
* Allow Text to be converted to/from other blocks
*/
static get conversionConfig() {
return {
export: 'text', // use 'text' property for other blocks
import: 'text', // fill 'text' property from other block's export string
}
}
/**
* Sanitizer Rules
*/
static get sanitize() {
return {
level: false,
text: {},
}
}
/**
* Returns true to notify core that read-only is supported
*
* @returns {boolean}
*/
static get isReadOnlySupported() {
return true
}
/**
* Get current Tools`s data
*
* @returns {TextData} Current data
* @private
*/
get data() {
this._data.text = this._element.innerHTML
return this._data
}
/**
* Store data in plugin:
* - at the this._data property
* - at the HTML
*
* @param {TextData} data — data to set
* @private
*/
set data(data) {
this._data = this.normalizeData(data)
/**
* If level is set and block in DOM
* then replace it to a new block
*/
if (data.level !== undefined && this._element.parentNode) {
/**
* Create a new tag
*
* @type {HTMLElement}
*/
const newText = this.getTag()
/**
* Save Block's content
*/
newText.innerHTML = this._element.innerHTML
/**
* Replace blocks
*/
this._element.parentNode.replaceChild(newText, this._element)
/**
* Save new block to private variable
*
* @type {HTMLElement}
* @private
*/
this._element = newText
}
/**
* If data.text was passed then update block's content
*/
if (data.text !== undefined) {
this._element.innerHTML = this._data.text || ''
}
}
/**
* Get tag for target level
* By default returns second-leveled text
*
* @returns {HTMLElement}
*/
getTag() {
/**
* Create element for current Block's level
*/
const tag = document.createElement(this.level < 4 ? 'h1' : 'p')
/**
* Add text to block
*/
tag.innerHTML = this._data.text || ''
/**
* Add styles class
*/
tag.classList.add(this._CSS.wrapper)
tag.classList.add('editor-text')
/**
* Make tag editable
*/
tag.contentEditable = this.readOnly ? 'false' : 'true'
/**
* Add Placeholder
*/
tag.dataset.placeholder = this.api.i18n.t(this._settings.placeholder || '')
return tag
}
/**
* Get Tool toolbox settings
* icon - Tool icon's SVG
* title - title to show in toolbox
*
* @returns {{icon: string, title: string}}
*/
static get toolbox() {
return {
icon: '',
title: 'Text',
}
}
/**
* Update block type.
*
* @param {string} type - new block type
*/
updateLevel(type) {
const currentIndex = this.api.blocks.getCurrentBlockIndex()
this.api.blocks.delete(currentIndex)
this.api.blocks.insert(type, { text: this.data.text }, null, currentIndex)
}
/**
* Create Block's settings block
*/
renderSettings() {
const textTypes = [
{ name: 'header1', label: 'Header 1', type: 'h1', icon: icons.header1 },
{ name: 'header2', label: 'Header 2', type: 'h2', icon: icons.header2 },
{ name: 'header3', label: 'Header 3', type: 'h3', icon: icons.header3 },
{ name: 'body', label: 'Body', type: 'body', icon: icons.body },
]
return textTypes.map(item => ({
...item,
closeOnActivate: true,
onActivate: () => this.updateLevel(item.type),
}))
}
}
```
Editor.js version: 2.26.4
Plugins you use with their versions:
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.