Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unique ids in blocks with nanoid #1667

Merged
merged 12 commits into from
Apr 27, 2021
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

### 2.21.0
- `New` - Blocks now have a unique ID [#873](https://github.com/codex-team/editor.js/issues/873)

### 2.20.2

- `Fix` — Append default Tunes if user tunes are provided for Block Tool [#1640](https://github.com/codex-team/editor.js/issues/1640)
Expand Down
14 changes: 14 additions & 0 deletions example/example-dev.html
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@
data: {
blocks: [
{
id: "zcKCF1S7X8",
type: "header",
data: {
text: "Editor.js",
Expand All @@ -205,19 +206,22 @@
},
{
type : 'paragraph',
id: "b6ji-DvaKb",
data : {
text : 'Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration.'
}
},
{
type: "header",
id: "7ItVl5biRo",
data: {
text: "Key features",
level: 3
}
},
{
type : 'list',
id: "SSBSguGvP7",
data : {
items : [
{
Expand All @@ -238,60 +242,70 @@
},
{
type: "header",
id: "QZFox1m_ul",
data: {
text: "What does it mean «block-styled editor»",
level: 3
}
},
{
type : 'paragraph',
id: "bwnFX5LoX7",
data : {
text : 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js <mark class=\"cdx-marker\">workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc</mark>. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\'s Core.'
}
},
{
type : 'paragraph',
id: "mTrPOHAQTe",
data : {
text : `There are dozens of <a href="https://github.com/editor-js">ready-to-use Blocks</a> and the <a href="https://editorjs.io/creating-a-block-tool">simple API</a> for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games.`
}
},
{
type: "header",
id: "1sYMhUrznu",
data: {
text: "What does it mean clean data output",
level: 3
}
},
{
type : 'paragraph',
id: "jpd7WEXrJG",
data : {
text : 'Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below'
}
},
{
type : 'paragraph',
id: "0lOGNUKxqt",
data : {
text : `Given data can be used as you want: render with HTML for <code class="inline-code">Web clients</code>, render natively for <code class="inline-code">mobile apps</code>, create markup for <code class="inline-code">Facebook Instant Articles</code> or <code class="inline-code">Google AMP</code>, generate an <code class="inline-code">audio version</code> and so on.`
}
},
{
type : 'paragraph',
id: "WvX7kBjp0I",
data : {
text : 'Clean data is useful to sanitize, validate and process on the backend.'
}
},
{
type : 'delimiter',
id: "H9LWKQ3NYd",
data : {}
},
{
type : 'paragraph',
id: "h298akk2Ad",
data : {
text : 'We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make its core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏'
}
},
{
type: 'image',
id: "9802bjaAA2",
data: {
url: 'assets/codex2x.png',
caption: '',
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
},
"dependencies": {
"codex-notifier": "^1.1.2",
"codex-tooltip": "^1.0.2"
"codex-tooltip": "^1.0.2",
"nanoid": "^3.1.22"
}
}
8 changes: 8 additions & 0 deletions src/components/block/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ function BlockAPI(
block: Block
): void {
const blockAPI: BlockAPIInterface = {
/**
* Block id
*
* @returns {string}
*/
get id(): string {
return block.id;
},
/**
* Tool name
*
Expand Down
14 changes: 14 additions & 0 deletions src/components/block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import ToolsCollection from '../tools/collection';
* Interface describes Block class constructor argument
*/
interface BlockConstructorOptions {
/**
* Block's id
*/
id: string;

/**
* Initial Block data
*/
Expand Down Expand Up @@ -98,6 +103,11 @@ export default class Block {
};
}

/**
* Block unique identifier
*/
public id: string;

/**
* Block Tool`s name
*/
Expand Down Expand Up @@ -206,20 +216,23 @@ export default class Block {

/**
* @param {object} options - block constructor options
* @param {string} options.id - block's unique id
* @param {BlockToolData} options.data - Tool's initial data
* @param {BlockToolConstructable} options.Tool — Tool's class
* @param {ToolSettings} options.settings - default tool's config
* @param options.api - Editor API module for pass it to the Block Tunes
* @param {boolean} options.readOnly - Read-Only flag
*/
constructor({
id,
data,
tool,
api,
readOnly,
tunesData,
}: BlockConstructorOptions) {
this.name = tool.name;
this.id = id;
this.settings = tool.settings;
this.config = tool.settings.config || {};
this.api = api;
Expand Down Expand Up @@ -567,6 +580,7 @@ export default class Block {
measuringEnd = window.performance.now();

return {
id: this.id,
tool: this.name,
data: finishedExtraction,
tunes: tunesData,
Expand Down
19 changes: 13 additions & 6 deletions src/components/modules/blockManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,18 +216,21 @@ export default class BlockManager extends Module {
*
* @param {object} options - block creation options
* @param {string} options.tool - tools passed in editor config {@link EditorConfig#tools}
* @param {string} [options.id] - unique id for this block
* @param {BlockToolData} [options.data] - constructor params
*
* @returns {Block}
*/
public composeBlock({
tool: name,
data = {},
id = _.generateBlockId(),
tunes: tunesData = {},
}: {tool: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}}): Block {
}: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}}): Block {
const readOnly = this.Editor.ReadOnly.isEnabled;
const tool = this.Editor.Tools.blockTools.get(name);
const block = new Block({
id,
data,
tool,
api: this.Editor.API,
Expand All @@ -246,22 +249,25 @@ export default class BlockManager extends Module {
* Insert new block into _blocks
*
* @param {object} options - insert options
* @param {string} options.tool - plugin name, by default method inserts the default block type
* @param {object} options.data - plugin data
* @param {number} options.index - index where to insert new Block
* @param {boolean} options.needToFocus - flag shows if needed to update current Block index
* @param {boolean} options.replace - flag shows if block by passed index should be replaced with inserted one
* @param {string} [options.id] - block's unique id
* @param {string} [options.tool] - plugin name, by default method inserts the default block type
* @param {object} [options.data] - plugin data
* @param {number} [options.index] - index where to insert new Block
* @param {boolean} [options.needToFocus] - flag shows if needed to update current Block index
* @param {boolean} [options.replace] - flag shows if block by passed index should be replaced with inserted one
*
* @returns {Block}
*/
public insert({
id,
tool = this.config.defaultBlock,
data = {},
index,
needToFocus = true,
replace = false,
tunes = {},
}: {
id?: string;
tool?: string;
data?: BlockToolData;
index?: number;
Expand All @@ -276,6 +282,7 @@ export default class BlockManager extends Module {
}

const block = this.composeBlock({
id,
tool,
data,
tunes,
Expand Down
2 changes: 1 addition & 1 deletion src/components/modules/paste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ export default class Paste extends Module {
*
* @returns {void}
*/
private insertEditorJSData(blocks: Pick<SavedData, 'data' | 'tool'>[]): void {
private insertEditorJSData(blocks: Pick<SavedData, 'id' | 'data' | 'tool'>[]): void {
const { BlockManager, Caret, Tools } = this.Editor;
const sanitizedBlocks = sanitizeBlocks(blocks, (name) =>
Tools.blockTools.get(name).sanitizeConfig
Expand Down
7 changes: 6 additions & 1 deletion src/components/modules/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ export default class Renderer extends Module {
*
* blocks: [
* {
* id : 'oDe-EVrGWA',
* type : 'paragraph',
* data : {
* text : 'Hello from Codex!'
* }
* },
* {
* id : 'Ld5BJjJCHs',
* type : 'paragraph',
* data : {
* text : 'Leave feedback if you like it!'
Expand Down Expand Up @@ -64,11 +66,12 @@ export default class Renderer extends Module {
*/
public async insertBlock(item: OutputBlockData): Promise<void> {
const { Tools, BlockManager } = this.Editor;
const { type: tool, data, tunes } = item;
const { type: tool, data, tunes, id } = item;

if (Tools.available.has(tool)) {
try {
BlockManager.insert({
id,
tool,
data,
tunes,
Expand All @@ -81,6 +84,7 @@ export default class Renderer extends Module {
/** If Tool is unavailable, create stub Block for it */
const stubData = {
savedData: {
id,
type: tool,
data,
},
Expand All @@ -94,6 +98,7 @@ export default class Renderer extends Module {
}

const stub = BlockManager.insert({
id,
tool: Tools.stubTool,
data: stubData,
});
Expand Down
3 changes: 2 additions & 1 deletion src/components/modules/saver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default class Saver extends Module {

_.log('[Editor.js saving]:', 'groupCollapsed');

allExtractedData.forEach(({ tool, data, tunes, time, isValid }) => {
allExtractedData.forEach(({ id, tool, data, tunes, time, isValid }) => {
totalTime += time;

/**
Expand All @@ -108,6 +108,7 @@ export default class Saver extends Module {
}

const output = {
id,
type: tool,
data,
...!_.isEmpty(tunes) && {
Expand Down
10 changes: 10 additions & 0 deletions src/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Class Util
*/

import { nanoid } from 'nanoid';
import Dom from './dom';

/**
Expand Down Expand Up @@ -607,6 +608,15 @@ export function getValidUrl(url: string): string {
}
}

/**
* Create a block id
*
* @returns {string}
*/
export function generateBlockId(): string {
return nanoid(10);
}

/**
* Opens new Tab with passed URL
*
Expand Down
13 changes: 12 additions & 1 deletion test/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* --------------------------------------------------
*/

import type { EditorConfig } from './../../../types/index';
import type { EditorConfig, OutputData } from './../../../types/index';
import type EditorJS from '../../../types/index';
import Chainable = Cypress.Chainable;

Expand Down Expand Up @@ -114,3 +114,14 @@ Cypress.Commands.add('cut', { prevSubject: true }, async (subject) => {

return clipboardData;
});

/**
* Calls EditorJS API render method
*
* @param data — data to render
*/
Cypress.Commands.add('render', { prevSubject: true }, async (subject: EditorJS, data: OutputData): Promise<EditorJS> => {
await subject.render(data);

return subject;
});
9 changes: 8 additions & 1 deletion test/cypress/support/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// load type definitions that come with Cypress module
/// <reference types="cypress" />

import type { EditorConfig } from './../../../types/index';
import type { EditorConfig, OutputData } from './../../../types/index';
import type EditorJS from '../../../types/index'

declare global {
Expand Down Expand Up @@ -40,6 +40,13 @@ declare global {
* cy.get('div').cut().then(data => {})
*/
cut(): Chainable<{ [type: string]: any }>;

/**
* Calls EditorJS API render method
*
* @param data — data to render
*/
render(data: OutputData): Chainable<EditorJS>;
}

interface ApplicationWindow {
Expand Down
Loading