├── types ├── block-tunes │ ├── index.d.ts │ ├── block-tune-data.d.ts │ └── block-tune.d.ts ├── data-formats │ ├── index.d.ts │ ├── block-data.d.ts │ └── output-data.d.ts ├── tools │ ├── tool-config.d.ts │ ├── block-tool-data.d.ts │ ├── hook-events.d.ts │ ├── index.d.ts │ ├── tool.d.ts │ ├── paste-events.d.ts │ ├── inline-tool.d.ts │ ├── tool-settings.d.ts │ └── block-tool.d.ts ├── configs │ ├── log-levels.d.ts │ ├── index.d.ts │ ├── i18n-config.d.ts │ ├── paste-config.d.ts │ ├── conversion-config.ts │ ├── sanitizer-config.d.ts │ ├── i18n-dictionary.d.ts │ └── editor-config.d.ts ├── api │ ├── inline-toolbar.d.ts │ ├── i18n.d.ts │ ├── saver.d.ts │ ├── readonly.d.ts │ ├── sanitizer.d.ts │ ├── notifier.d.ts │ ├── toolbar.d.ts │ ├── index.d.ts │ ├── ui.d.ts │ ├── selection.d.ts │ ├── events.d.ts │ ├── styles.d.ts │ ├── tooltip.d.ts │ ├── listeners.d.ts │ ├── block.d.ts │ ├── caret.d.ts │ └── blocks.d.ts └── events │ └── block │ └── mutation-type.ts ├── CODEOWNERS ├── tsconfig.build.json ├── .eslintignore ├── example ├── assets │ ├── codex2x.png │ └── json-preview.js └── example-multiple.html ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── discussion.md │ ├── config.yml │ ├── feature_request.md │ ├── issue--discussion.md │ └── bug_report.md ├── workflows │ ├── eslint.yml │ ├── cypress.yml │ ├── publish-package-to-npm.yml │ └── bump-version-on-merge-next.yml └── CODE_OF_CONDUCT.md ├── src ├── components │ ├── errors │ │ └── critical.ts │ ├── utils │ │ ├── notifier.ts │ │ ├── scroll-locker.ts │ │ ├── tooltip.ts │ │ ├── shortcuts.ts │ │ └── events.ts │ ├── modules │ │ ├── api │ │ │ ├── inlineToolbar.ts │ │ │ ├── ui.ts │ │ │ ├── styles.ts │ │ │ ├── saver.ts │ │ │ ├── sanitizer.ts │ │ │ ├── readonly.ts │ │ │ ├── selection.ts │ │ │ ├── i18n.ts │ │ │ ├── events.ts │ │ │ ├── notifier.ts │ │ │ ├── toolbar.ts │ │ │ ├── index.ts │ │ │ ├── listeners.ts │ │ │ └── tooltip.ts │ │ ├── modificationsObserver.ts │ │ ├── renderer.ts │ │ ├── readonly.ts │ │ └── dragNDrop.ts │ ├── i18n │ │ ├── locales │ │ │ └── en │ │ │ │ └── messages.json │ │ ├── namespace-internal.ts │ │ └── index.ts │ ├── tools │ │ ├── tune.ts │ │ ├── inline.ts │ │ ├── collection.ts │ │ └── factory.ts │ ├── inline-tools │ │ ├── inline-tool-italic.ts │ │ └── inline-tool-bold.ts │ ├── block-tunes │ │ ├── block-tune-move-down.ts │ │ └── block-tune-delete.ts │ └── block │ │ └── api.ts ├── types-internal │ ├── svg.d.ts │ ├── module-config.d.ts │ ├── html-janitor.d.ts │ ├── i18n-internal-namespace.d.ts │ └── editor-modules.d.ts ├── assets │ ├── link.svg │ ├── plus.svg │ ├── toggler-down.svg │ ├── search.svg │ ├── arrow-up.svg │ ├── arrow-down.svg │ ├── unlink.svg │ ├── dots.svg │ ├── cross.svg │ ├── italic.svg │ ├── sad-face.svg │ └── bold.svg ├── styles │ ├── main.css │ ├── toolbox.css │ ├── stub.css │ ├── input.css │ ├── toolbar.css │ ├── rtl.css │ ├── settings.css │ ├── conversion-toolbar.css │ ├── block.css │ ├── export.css │ ├── animations.css │ ├── inline-toolbar.css │ ├── ui.css │ └── popover.css └── tools │ └── stub │ └── index.ts ├── .editorconfig ├── test └── cypress │ ├── fixtures │ └── test.html │ ├── tsconfig.json │ ├── .eslintrc │ ├── support │ ├── index.ts │ └── index.d.ts │ ├── tests │ ├── selection.spec.ts │ ├── initialization.spec.ts │ ├── readOnly.spec.ts │ ├── utils.spec.ts │ ├── api │ │ └── block.spec.ts │ ├── tools │ │ └── ToolsFactory.spec.ts │ ├── sanitisation.spec.ts │ ├── i18n.spec.ts │ └── block-ids.spec.ts │ └── plugins │ └── index.ts ├── .gitignore ├── .npmignore ├── cypress.json ├── .babelrc ├── tsconfig.json ├── docs ├── sanitizer.md ├── caret.md └── toolbar-settings.md ├── tslint.json ├── .eslintrc ├── .postcssrc.yml ├── .gitmodules ├── webpack.config.js └── devserver.js /types/block-tunes/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './block-tune'; 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @neSpecc @gohabereg @TatianaFomina @ilyamore88 2 | 3 | -------------------------------------------------------------------------------- /types/block-tunes/block-tune-data.d.ts: -------------------------------------------------------------------------------- 1 | export type BlockTuneData = any; 2 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["test"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.d.ts 3 | src/components/tools/paragraph 4 | src/polyfills.ts 5 | -------------------------------------------------------------------------------- /types/data-formats/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './block-data'; 2 | export * from './output-data'; 3 | -------------------------------------------------------------------------------- /example/assets/codex2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaaaaaaaaaaai/editor.js/next/example/assets/codex2x.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: neSpecc 4 | patreon: editorjs 5 | open_collective: editorjs 6 | -------------------------------------------------------------------------------- /src/components/errors/critical.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This type of exception will destroy the Editor! Be careful when using it 3 | */ 4 | export class CriticalError extends Error { 5 | } 6 | -------------------------------------------------------------------------------- /types/tools/tool-config.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tool configuration object. Specified by Tool developer, so leave it as object 3 | */ 4 | export type ToolConfig = T; 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/types-internal/svg.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Allow to import .svg from components/modules/ui from TypeScript file 3 | */ 4 | declare module '*.svg' { 5 | const content: string; 6 | export default content; 7 | } 8 | -------------------------------------------------------------------------------- /types/configs/log-levels.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Available log levels 3 | */ 4 | export enum LogLevels { 5 | VERBOSE = 'VERBOSE', 6 | INFO = 'INFO', 7 | WARN = 'WARN', 8 | ERROR = 'ERROR', 9 | } 10 | -------------------------------------------------------------------------------- /types/tools/block-tool-data.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Object returned by Tool's {@link BlockTool#save} method 3 | * Specified by Tool developer, so leave it as object 4 | */ 5 | export type BlockToolData = T; 6 | -------------------------------------------------------------------------------- /src/assets/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/cypress/fixtures/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Editor.js test page

7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/toggler-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /types/configs/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './editor-config'; 2 | export * from './sanitizer-config'; 3 | export * from './paste-config'; 4 | export * from './conversion-config'; 5 | export * from './log-levels'; 6 | export * from './i18n-config'; 7 | export * from './i18n-dictionary'; 8 | -------------------------------------------------------------------------------- /types/api/inline-toolbar.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes InlineToolbar API methods 3 | */ 4 | export interface InlineToolbar { 5 | /** 6 | * Closes InlineToolbar 7 | */ 8 | close(): void; 9 | 10 | /** 11 | * Opens InlineToolbar 12 | */ 13 | open(): void; 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # --- proj files --- 2 | .DS_Store 3 | Thumbs.db 4 | /.idea/ 5 | /*.sublime-project 6 | /*.sublime-workspace 7 | 8 | node_modules/* 9 | 10 | npm-debug.log 11 | yarn-error.log 12 | 13 | test/cypress/screenshots 14 | test/cypress/videos 15 | 16 | dist/ 17 | 18 | coverage/ 19 | .nyc_output/ 20 | -------------------------------------------------------------------------------- /src/assets/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /types/api/i18n.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes Editor`s I18n API 3 | */ 4 | export interface I18n { 5 | /** 6 | * Perform translation with automatically added namespace like `tools.${toolName}` or `blockTunes.${tuneName}` 7 | * 8 | * @param dictKey - what to translate 9 | */ 10 | t(dictKey: string): string; 11 | } 12 | -------------------------------------------------------------------------------- /types/api/saver.d.ts: -------------------------------------------------------------------------------- 1 | import {OutputData} from '../data-formats/output-data'; 2 | 3 | /** 4 | * Describes Editor`s saver API 5 | */ 6 | export interface Saver { 7 | /** 8 | * Saves Editors data and returns promise with it 9 | * 10 | * @returns {Promise} 11 | */ 12 | save(): Promise; 13 | } 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .github 3 | docs 4 | example 5 | src 6 | test 7 | .babelrc 8 | .editorconfig 9 | .eslintignore 10 | .eslintrc 11 | .git 12 | .gitmodules 13 | .jshintrc 14 | .postcssrc.yml 15 | .stylelintrc 16 | CODEOWNERS 17 | cypress.json 18 | tsconfig.json 19 | tslint.json 20 | webpack.config.js 21 | yarn.lock 22 | devserver.js 23 | -------------------------------------------------------------------------------- /src/assets/arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/types-internal/module-config.d.ts: -------------------------------------------------------------------------------- 1 | import { EditorConfig } from '../../types/index'; 2 | import EventsDispatcher from '../components/utils/events'; 3 | 4 | /** 5 | * Describes object passed to Editor modules constructor 6 | */ 7 | export interface ModuleConfig { 8 | config: EditorConfig; 9 | eventsDispatcher: EventsDispatcher; 10 | } 11 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "NODE_ENV": "test" 4 | }, 5 | "fixturesFolder": "test/cypress/fixtures", 6 | "integrationFolder": "test/cypress/tests", 7 | "screenshotsFolder": "test/cypress/screenshots", 8 | "videosFolder": "test/cypress/videos", 9 | "supportFile": "test/cypress/support/index.ts", 10 | "pluginsFile": "test/cypress/plugins/index.ts" 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "es2017", "es2018"], 5 | "moduleResolution": "node", 6 | "resolveJsonModule": true, 7 | "allowSyntheticDefaultImports": true, 8 | "experimentalDecorators": true 9 | }, 10 | "include": [ 11 | "../../**/*.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /types/configs/i18n-config.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Available options of i18n config property 3 | */ 4 | import { I18nDictionary } from './i18n-dictionary'; 5 | 6 | export interface I18nConfig { 7 | /** 8 | * Dictionary used for translation 9 | */ 10 | messages?: I18nDictionary; 11 | 12 | /** 13 | * Text direction. If not set, uses ltr 14 | */ 15 | direction?: 'ltr' | 'rtl'; 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/discussion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Discussion 3 | about: Any question about the Editor.js to discuss 4 | title: '' 5 | labels: discussion 6 | assignees: '' 7 | 8 | --- 9 | 10 | The question. 11 | 12 | Why and how the question has come up. 13 | 14 | 18 | -------------------------------------------------------------------------------- /src/assets/unlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "modules": "umd", 5 | "useBuiltIns": "entry", 6 | "corejs": 3 7 | }] 8 | ], 9 | "plugins": [ 10 | "babel-plugin-add-module-exports", 11 | "babel-plugin-class-display-name", 12 | "@babel/plugin-transform-runtime" 13 | ], 14 | "env": { 15 | "test": { 16 | "plugins": [ "istanbul" ] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | @import './variables.css'; 2 | @import './ui.css'; 3 | @import './toolbar.css'; 4 | @import './toolbox.css'; 5 | @import './inline-toolbar.css'; 6 | @import './conversion-toolbar.css'; 7 | @import './settings.css'; 8 | @import './block.css'; 9 | @import './animations.css'; 10 | @import './export.css'; 11 | @import './stub.css'; 12 | @import './rtl.css'; 13 | @import './popover.css'; 14 | @import './input.css'; 15 | -------------------------------------------------------------------------------- /types/api/readonly.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ReadOnly API 3 | */ 4 | export interface ReadOnly { 5 | /** 6 | * Set or toggle read-only state 7 | * 8 | * @param {Boolean|undefined} state - set or toggle state 9 | * @returns {Promise} current value 10 | */ 11 | toggle: (state?: boolean) => Promise; 12 | 13 | /** 14 | * Contains current read-only state 15 | */ 16 | isEnabled: boolean; 17 | } 18 | -------------------------------------------------------------------------------- /types/api/sanitizer.d.ts: -------------------------------------------------------------------------------- 1 | import {SanitizerConfig} from '../index'; 2 | 3 | /** 4 | * Describes Editor`s sanitizer API 5 | */ 6 | export interface Sanitizer { 7 | /** 8 | * Clean taint string with html and returns clean string 9 | * 10 | * @param {string} taintString 11 | * @param {SanitizerConfig} config - configuration for sanitizer 12 | */ 13 | clean(taintString: string, config: SanitizerConfig): string; 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/cypress/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "cypress", 4 | "chai-friendly" 5 | ], 6 | "env": { 7 | "cypress/globals": true 8 | }, 9 | "extends": [ 10 | "plugin:cypress/recommended", 11 | "plugin:chai-friendly/recommended" 12 | ], 13 | "rules": { 14 | "cypress/require-data-selectors": 2 15 | }, 16 | "globals": { 17 | "EditorJS": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /types/api/notifier.d.ts: -------------------------------------------------------------------------------- 1 | import {ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions} from 'codex-notifier'; 2 | 3 | /** 4 | * Notifier API 5 | */ 6 | export interface Notifier { 7 | 8 | /** 9 | * Show web notification 10 | * 11 | * @param {NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions} 12 | */ 13 | show: (options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) => void; 14 | } 15 | -------------------------------------------------------------------------------- /types/api/toolbar.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes Toolbar API methods 3 | */ 4 | export interface Toolbar { 5 | /** 6 | * Closes Toolbar 7 | */ 8 | close(): void; 9 | 10 | /** 11 | * Opens Toolbar 12 | */ 13 | open(): void; 14 | 15 | /** 16 | * Toggles Block Setting of the current block 17 | * @param {boolean} openingState — opening state of Block Setting 18 | */ 19 | toggleBlockSettings(openingState?: boolean): void; 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Team 4 | url: mailto:team@codex.so 5 | about: Direct team contact. 6 | - name: Editor.js Telegram chat 7 | url: https://t.me/codex_editor 8 | about: Telegram chat for Editor.js users communication. 9 | - name: Editor.js contributors Telegram chat 10 | url: https://t.me/editorjsdev 11 | about: Telegram chat for Editor.js contributors communication. 12 | -------------------------------------------------------------------------------- /types/data-formats/block-data.d.ts: -------------------------------------------------------------------------------- 1 | import {BlockToolData} from '../tools'; 2 | 3 | /** 4 | * Tool's saved data 5 | */ 6 | export interface SavedData { 7 | id: string; 8 | tool: string; 9 | data: BlockToolData; 10 | time: number; 11 | } 12 | 13 | /** 14 | * Tool's data after validation 15 | */ 16 | export interface ValidatedData { 17 | id?: string; 18 | tool?: string; 19 | data?: BlockToolData; 20 | time?: number; 21 | isValid: boolean; 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea to improve Editor.js 4 | title: "\U0001F4A1" 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | 1. Describe a problem. 11 | 12 | 2. Describe the solution you'd like. Mockups are welcome. 13 | 14 | 3. Are there any alternatives? 15 | 16 | 20 | -------------------------------------------------------------------------------- /types/events/block/mutation-type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * What kind of modification happened with the Block 3 | */ 4 | export enum BlockMutationType { 5 | /** 6 | * New Block added 7 | */ 8 | Added = 'block-added', 9 | 10 | /** 11 | * On Block deletion 12 | */ 13 | Removed = 'block-removed', 14 | 15 | /** 16 | * Moving of a Block 17 | */ 18 | Moved = 'block-moved', 19 | 20 | /** 21 | * Any changes inside the Block 22 | */ 23 | Changed = 'block-changed', 24 | } 25 | -------------------------------------------------------------------------------- /src/styles/toolbox.css: -------------------------------------------------------------------------------- 1 | .ce-toolbox { 2 | --gap: 8px; 3 | 4 | @media (--not-mobile){ 5 | position: absolute; 6 | top: calc(var(--toolbox-buttons-size) + var(--gap)); 7 | left: 0; 8 | 9 | &--opened-top { 10 | top: calc(-1 * (var(--gap) + var(--popover-height))); 11 | } 12 | } 13 | } 14 | 15 | .codex-editor--narrow .ce-toolbox { 16 | @media (--not-mobile){ 17 | left: auto; 18 | right: 0; 19 | 20 | .ce-popover { 21 | right: 0; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /types/api/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './blocks'; 2 | export * from './events'; 3 | export * from './listeners'; 4 | export * from './sanitizer'; 5 | export * from './saver'; 6 | export * from './selection'; 7 | export * from './styles'; 8 | export * from './caret'; 9 | export * from './toolbar'; 10 | export * from './notifier'; 11 | export * from './tooltip'; 12 | export * from './inline-toolbar'; 13 | export * from './block'; 14 | export * from './readonly'; 15 | export * from './i18n'; 16 | export * from './ui'; 17 | -------------------------------------------------------------------------------- /src/styles/stub.css: -------------------------------------------------------------------------------- 1 | .ce-stub { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 100%; 6 | padding: 3.5em 0; 7 | margin: 17px 0; 8 | border-radius: 3px; 9 | background: #fcf7f7; 10 | color: #b46262; 11 | 12 | &__info { 13 | margin-left: 20px; 14 | } 15 | 16 | &__title { 17 | margin-bottom: 3px; 18 | font-weight: 600; 19 | font-size: 18px; 20 | text-transform: capitalize; 21 | } 22 | 23 | &__subtitle { 24 | font-size: 16px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is processed and 3 | * loaded automatically before the test files. 4 | * 5 | * This is a great place to put global configuration and 6 | * behavior that modifies Cypress. 7 | */ 8 | 9 | import '@cypress/code-coverage/support'; 10 | 11 | /** 12 | * File with the helpful commands 13 | */ 14 | import './commands'; 15 | 16 | /** 17 | * Before-each hook for the cypress tests 18 | */ 19 | beforeEach((): void => { 20 | cy.visit('test/cypress/fixtures/test.html'); 21 | }); 22 | -------------------------------------------------------------------------------- /src/assets/italic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /types/api/ui.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes API module allowing to access some Editor UI elements and methods 3 | */ 4 | export interface Ui { 5 | /** 6 | * Allows accessing some Editor UI elements 7 | */ 8 | nodes: UiNodes, 9 | } 10 | 11 | /** 12 | * Allows accessing some Editor UI elements 13 | */ 14 | export interface UiNodes { 15 | /** 16 | * Top-level editor instance wrapper 17 | */ 18 | wrapper: HTMLElement, 19 | 20 | /** 21 | * Element that holds all the Blocks 22 | */ 23 | redactor: HTMLElement, 24 | } 25 | -------------------------------------------------------------------------------- /types/tools/hook-events.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Event detail for block relocation 3 | */ 4 | export interface MoveEventDetail { 5 | /** 6 | * index the block was moved from 7 | */ 8 | fromIndex: number; 9 | /** 10 | * index the block was moved to 11 | */ 12 | toIndex: number; 13 | } 14 | 15 | /** 16 | * Move event for block relocation 17 | */ 18 | export interface MoveEvent extends CustomEvent { 19 | /** 20 | * Override detail property of CustomEvent by MoveEvent hook 21 | */ 22 | readonly detail: MoveEventDetail; 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions" : { 3 | "sourceMap": true, 4 | "target": "es2017", 5 | "declaration": false, 6 | "moduleResolution": "node", // This resolution strategy attempts to mimic the Node.js module resolution mechanism at runtime 7 | "lib": ["dom", "es2017", "es2018"], 8 | 9 | // allows to import .json files for i18n 10 | "resolveJsonModule": true, 11 | 12 | // allows to omit export default in .json files 13 | "allowSyntheticDefaultImports": true, 14 | "experimentalDecorators": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue--discussion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Issue: Discussion' 3 | about: Any question about the project to discuss 4 | title: "❓" 5 | labels: discussion 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Question** 11 | 12 | A clear and consistent question about the project. Ex. How can I do smth? Why smth works this way? etc. 13 | 14 | **Context** 15 | 16 | Why and how the question has come up 17 | 18 | **Related issues** 19 | 20 | If there are related issues which describe a bugs or features, put them here 21 | 22 | **Comments** 23 | 24 | Any thoughts about the question 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve Editor.js 4 | title: "[Bug]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | Describe a bug. 11 | 12 | Steps to reproduce: 13 | 1. Go to … 14 | 2. Click on … 15 | 3. … 16 | 17 | Expected behavior: 18 | 19 | Screenshots: 20 | 21 | Device, Browser, OS: 22 | 23 | Editor.js version: 24 | 25 | Plugins you use with their versions: 26 | 27 | 31 | -------------------------------------------------------------------------------- /types/api/selection.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes methods for work with Selections 3 | */ 4 | export interface Selection { 5 | /** 6 | * Looks ahead from selection and find passed tag with class name 7 | * @param {string} tagName - tag to find 8 | * @param {string} className - tag's class name 9 | * @return {HTMLElement|null} 10 | */ 11 | findParentTag(tagName: string, className?: string): HTMLElement|null; 12 | 13 | /** 14 | * Expand selection to passed tag 15 | * @param {HTMLElement} node - tag that should contain selection 16 | */ 17 | expandToTag(node: HTMLElement): void; 18 | } 19 | -------------------------------------------------------------------------------- /types/configs/paste-config.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tool onPaste configuration object 3 | */ 4 | export interface PasteConfig { 5 | /** 6 | * Array of tags Tool can substitute 7 | * @type string[] 8 | */ 9 | tags?: string[]; 10 | 11 | /** 12 | * Object of string patterns Tool can substitute. 13 | * Key is your internal key and value is RegExp 14 | * 15 | * @type {{[key: string]: Regexp}} 16 | */ 17 | patterns?: {[key: string]: RegExp}; 18 | 19 | /** 20 | * Object with arrays of extensions and MIME types Tool can substitute 21 | */ 22 | files?: {extensions?: string[], mimeTypes?: string[]}; 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | name: ESLint CodeX 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | lint: 7 | name: ESlint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Cache node modules 13 | uses: actions/cache@v1 14 | with: 15 | path: node_modules 16 | key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} 17 | restore-keys: | 18 | ${{ runner.OS }}-build-${{ env.cache-name }}- 19 | ${{ runner.OS }}-build- 20 | ${{ runner.OS }}- 21 | 22 | - run: yarn install 23 | 24 | - run: yarn lint 25 | -------------------------------------------------------------------------------- /src/assets/sad-face.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/sanitizer.md: -------------------------------------------------------------------------------- 1 | # Editor.js Sanitizer Module 2 | 3 | The `Sanitizer` module represents a set of methods that clears taint strings. 4 | Uses lightweight npm package with simple API [html-janitor](https://www.npmjs.com/package/html-janitor) 5 | 6 | ## Methods 7 | 8 | ### clean 9 | 10 | ```javascript 11 | clean(taintString, customConfig) 12 | ``` 13 | 14 | > Cleans up the passed taint string 15 | 16 | #### params 17 | 18 | | Param | Type | Description| 19 | | -------------|------ |:-------------:| 20 | | taintString | String | string that needs to be cleaned| 21 | | customConfig | Object | Can be passed new config per usage (Default: uses default configuration)| 22 | 23 | -------------------------------------------------------------------------------- /src/components/utils/notifier.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use external package module for notifications 3 | * 4 | * @see https://github.com/codex-team/js-notifier 5 | */ 6 | import notifier, { ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions } from 'codex-notifier'; 7 | 8 | /** 9 | * Util for showing notifications 10 | */ 11 | export default class Notifier { 12 | /** 13 | * Show web notification 14 | * 15 | * @param {NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions} options - notification options 16 | */ 17 | public show(options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions): void { 18 | notifier.show(options); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /types/tools/index.d.ts: -------------------------------------------------------------------------------- 1 | import { BlockTool, BlockToolConstructable } from './block-tool'; 2 | import { InlineTool, InlineToolConstructable } from './inline-tool'; 3 | import { BlockTune, BlockTuneConstructable } from '../block-tunes'; 4 | 5 | export * from './block-tool'; 6 | export * from './block-tool-data'; 7 | export * from './inline-tool'; 8 | export * from './tool'; 9 | export * from './tool-config'; 10 | export * from './tool-settings'; 11 | export * from './paste-events'; 12 | export * from './hook-events'; 13 | 14 | export type Tool = BlockTool | InlineTool | BlockTune; 15 | export type ToolConstructable = BlockToolConstructable | InlineToolConstructable | BlockTuneConstructable; 16 | -------------------------------------------------------------------------------- /types/api/events.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes Editor`s events API 3 | */ 4 | export interface Events { 5 | /** 6 | * Emits event 7 | * 8 | * @param {string} eventName 9 | * @param {any} data 10 | */ 11 | emit(eventName: string, data: any): void; 12 | 13 | /** 14 | * Unsubscribe from event 15 | * 16 | * @param {string} eventName 17 | * @param {(data: any) => void} callback 18 | */ 19 | off(eventName: string, callback: (data?: any) => void): void; 20 | 21 | /** 22 | * Subscribe to event 23 | * 24 | * @param {string} eventName 25 | * @param {(data: any) => void} callback 26 | */ 27 | on(eventName: string, callback: (data?: any) => void): void; 28 | } 29 | -------------------------------------------------------------------------------- /src/types-internal/html-janitor.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Declaration for external JS module 3 | * After that we can use it at the TS modules 4 | */ 5 | declare module 'html-janitor' { 6 | /** 7 | * Sanitizer config of each HTML element 8 | * @see {@link https://github.com/guardian/html-janitor#options} 9 | */ 10 | type TagConfig = boolean | { [attr: string]: boolean | string }; 11 | 12 | interface Config { 13 | tags: { 14 | [key: string]: TagConfig | ((el: Element) => TagConfig) 15 | }; 16 | } 17 | 18 | export class HTMLJanitor { 19 | constructor(config: Config); 20 | 21 | public clean(taintString: string): string; 22 | } 23 | 24 | /** 25 | * Default export 26 | */ 27 | export default HTMLJanitor; 28 | } 29 | -------------------------------------------------------------------------------- /src/assets/bold.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /types/api/styles.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes styles API 3 | */ 4 | export interface Styles { 5 | /** 6 | * Main Editor`s block styles 7 | */ 8 | block: string; 9 | 10 | /** 11 | * Styles for Inline Toolbar button 12 | */ 13 | inlineToolButton: string; 14 | 15 | /** 16 | * Styles for active Inline Toolbar button 17 | */ 18 | inlineToolButtonActive: string; 19 | 20 | /** 21 | * Styles for inputs 22 | */ 23 | input: string; 24 | 25 | /** 26 | * Loader styles 27 | */ 28 | loader: string; 29 | 30 | /** 31 | * Styles for Settings box buttons 32 | */ 33 | settingsButton: string; 34 | 35 | /** 36 | * Styles for active Settings box buttons 37 | */ 38 | settingsButtonActive: string; 39 | 40 | /** 41 | * Styles for buttons 42 | */ 43 | button: string; 44 | } 45 | -------------------------------------------------------------------------------- /types/api/tooltip.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tooltip API 3 | */ 4 | import {TooltipContent, TooltipOptions} from 'codex-tooltip'; 5 | 6 | export interface Tooltip { 7 | /** 8 | * Show tooltip 9 | * 10 | * @param {HTMLElement} element 11 | * @param {TooltipContent} content 12 | * @param {TooltipOptions} options 13 | */ 14 | show: (element: HTMLElement, content: TooltipContent, options?: TooltipOptions) => void; 15 | 16 | /** 17 | * Hides tooltip 18 | */ 19 | hide: () => void; 20 | 21 | /** 22 | * Decorator for showing Tooltip by mouseenter/mouseleave 23 | * 24 | * @param {HTMLElement} element 25 | * @param {TooltipContent} content 26 | * @param {TooltipOptions} options 27 | */ 28 | onHover: (element: HTMLElement, content: TooltipContent, options?: TooltipOptions) => void; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "linterOptions": { 4 | "exclude": [ 5 | "node_modules" 6 | ] 7 | }, 8 | "rules": { 9 | "indent": [true, "spaces", 2], 10 | "interface-name": false, 11 | "quotemark": [true, "single"], 12 | "no-console": false, 13 | "no-empty-interface": false, 14 | "one-variable-per-declaration": false, 15 | "object-literal-sort-keys": false, 16 | "ordered-imports": [true, { 17 | "import-sources-order": "any", 18 | "named-imports-order": "case-insensitive" 19 | }], 20 | "no-string-literal": false, 21 | "no-empty": false, 22 | "no-namespace": false, 23 | "variable-name": [true, "allow-leading-underscore", "allow-pascal-case"], 24 | "no-reference": false 25 | }, 26 | "globals": { 27 | "require": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/modules/api/inlineToolbar.ts: -------------------------------------------------------------------------------- 1 | import { InlineToolbar } from '../../../../types/api/inline-toolbar'; 2 | import Module from '../../__module'; 3 | 4 | /** 5 | * @class InlineToolbarAPI 6 | * Provides methods for working with the Inline Toolbar 7 | */ 8 | export default class InlineToolbarAPI extends Module { 9 | /** 10 | * Available methods 11 | * 12 | * @returns {InlineToolbar} 13 | */ 14 | public get methods(): InlineToolbar { 15 | return { 16 | close: (): void => this.close(), 17 | open: (): void => this.open(), 18 | }; 19 | } 20 | 21 | /** 22 | * Open Inline Toolbar 23 | */ 24 | public open(): void { 25 | this.Editor.InlineToolbar.tryToShow(); 26 | } 27 | 28 | /** 29 | * Close Inline Toolbar 30 | */ 31 | public close(): void { 32 | this.Editor.InlineToolbar.close(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/modules/api/ui.ts: -------------------------------------------------------------------------------- 1 | import Module from '../../__module'; 2 | import { Ui, UiNodes } from '../../../../types/api'; 3 | 4 | /** 5 | * API module allowing to access some Editor UI elements 6 | */ 7 | export default class UiAPI extends Module { 8 | /** 9 | * Available methods / getters 10 | */ 11 | public get methods(): Ui { 12 | return { 13 | nodes: this.editorNodes, 14 | /** 15 | * There can be added some UI methods, like toggleThinMode() etc 16 | */ 17 | }; 18 | } 19 | 20 | /** 21 | * Exported classes 22 | */ 23 | private get editorNodes(): UiNodes { 24 | return { 25 | /** 26 | * Top-level editor instance wrapper 27 | */ 28 | wrapper: this.Editor.UI.nodes.wrapper, 29 | 30 | /** 31 | * Element that holds all the Blocks 32 | */ 33 | redactor: this.Editor.UI.nodes.redactor, 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/modules/api/styles.ts: -------------------------------------------------------------------------------- 1 | import { Styles } from '../../../../types/api'; 2 | import Module from '../../__module'; 3 | 4 | /** 5 | * 6 | */ 7 | export default class StylesAPI extends Module { 8 | /** 9 | * Exported classes 10 | */ 11 | public get classes(): Styles { 12 | return { 13 | /** 14 | * Base Block styles 15 | */ 16 | block: 'cdx-block', 17 | 18 | /** 19 | * Inline Tools styles 20 | */ 21 | inlineToolButton: 'ce-inline-tool', 22 | inlineToolButtonActive: 'ce-inline-tool--active', 23 | 24 | /** 25 | * UI elements 26 | */ 27 | input: 'cdx-input', 28 | loader: 'cdx-loader', 29 | button: 'cdx-button', 30 | 31 | /** 32 | * Settings styles 33 | */ 34 | settingsButton: 'cdx-settings-button', 35 | settingsButtonActive: 'cdx-settings-button--active', 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /types/configs/conversion-config.ts: -------------------------------------------------------------------------------- 1 | import {BlockToolData} from '../tools'; 2 | 3 | /** 4 | * Config allows Tool to specify how it can be converted into/from another Tool 5 | */ 6 | export interface ConversionConfig { 7 | /** 8 | * How to import string to this Tool. 9 | * 10 | * Can be a String or Function: 11 | * 12 | * 1. String — the key of Tool data object to fill it with imported string on render. 13 | * 2. Function — method that accepts importing string and composes Tool data to render. 14 | */ 15 | import: ((data: string) => string) | string; 16 | 17 | /** 18 | * How to export this Tool to make other Block. 19 | * 20 | * Can be a String or Function: 21 | * 22 | * 1. String — which property of saved Tool data should be used as exported string. 23 | * 2. Function — accepts saved Tool data and create a string to export 24 | */ 25 | export: ((data: BlockToolData) => string) | string; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/i18n/locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "ui": { 3 | "blockTunes": { 4 | "toggler": { 5 | "Click to tune": "", 6 | "or drag to move": "" 7 | } 8 | }, 9 | "inlineToolbar": { 10 | "converter": { 11 | "Convert to": "" 12 | } 13 | }, 14 | "toolbar": { 15 | "toolbox": { 16 | "Add": "", 17 | "Filter": "", 18 | "Nothing found": "" 19 | } 20 | } 21 | }, 22 | "toolNames": { 23 | "Text": "", 24 | "Link": "", 25 | "Bold": "", 26 | "Italic": "" 27 | }, 28 | "tools": { 29 | "link": { 30 | "Add a link": "" 31 | }, 32 | "stub": { 33 | "The block can not be displayed correctly.": "" 34 | } 35 | }, 36 | "blockTunes": { 37 | "delete": { 38 | "Delete": "" 39 | }, 40 | "moveUp": { 41 | "Move up": "" 42 | }, 43 | "moveDown": { 44 | "Move down": "" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/cypress/tests/selection.spec.ts: -------------------------------------------------------------------------------- 1 | import * as _ from '../../../src/components/utils'; 2 | 3 | describe('Blocks selection', () => { 4 | beforeEach(() => { 5 | if (this && this.editorInstance) { 6 | this.editorInstance.destroy(); 7 | } else { 8 | cy.createEditor({}).as('editorInstance'); 9 | } 10 | }); 11 | 12 | it('should remove block selection on click', () => { 13 | cy.get('[data-cy=editorjs]') 14 | .find('div.ce-block') 15 | .click() 16 | .type('First block{enter}'); 17 | 18 | cy.get('[data-cy=editorjs') 19 | .find('div.ce-block') 20 | .next() 21 | .type('Second block') 22 | .type('{movetostart}') 23 | .trigger('keydown', { 24 | shiftKey: true, 25 | keyCode: _.keyCodes.UP, 26 | }); 27 | 28 | cy.get('[data-cy=editorjs') 29 | .click() 30 | .find('div.ce-block') 31 | .should('not.have.class', '.ce-block--selected'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/modules/modificationsObserver.ts: -------------------------------------------------------------------------------- 1 | import Module from '../__module'; 2 | import * as _ from '../utils'; 3 | 4 | /** 5 | * Single entry point for Block mutation events 6 | */ 7 | export default class ModificationsObserver extends Module { 8 | /** 9 | * Flag shows onChange event is disabled 10 | */ 11 | private disabled = false; 12 | 13 | /** 14 | * Enables onChange event 15 | */ 16 | public enable(): void { 17 | this.disabled = false; 18 | } 19 | 20 | /** 21 | * Disables onChange event 22 | */ 23 | public disable(): void { 24 | this.disabled = true; 25 | } 26 | 27 | /** 28 | * Call onChange event passed to Editor.js configuration 29 | * 30 | * @param event - some of our custom change events 31 | */ 32 | public onChange(event: CustomEvent): void { 33 | if (this.disabled || !_.isFunction(this.config.onChange)) { 34 | return; 35 | } 36 | 37 | this.config.onChange(this.Editor.API.methods, event); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/modules/api/saver.ts: -------------------------------------------------------------------------------- 1 | import { Saver } from '../../../../types/api'; 2 | import { OutputData } from '../../../../types'; 3 | import * as _ from '../../utils'; 4 | import Module from '../../__module'; 5 | 6 | /** 7 | * @class SaverAPI 8 | * provides with methods to save data 9 | */ 10 | export default class SaverAPI extends Module { 11 | /** 12 | * Available methods 13 | * 14 | * @returns {Saver} 15 | */ 16 | public get methods(): Saver { 17 | return { 18 | save: (): Promise => this.save(), 19 | }; 20 | } 21 | 22 | /** 23 | * Return Editor's data 24 | * 25 | * @returns {OutputData} 26 | */ 27 | public save(): Promise { 28 | const errorText = 'Editor\'s content can not be saved in read-only mode'; 29 | 30 | if (this.Editor.ReadOnly.isEnabled) { 31 | _.logLabeled(errorText, 'warn'); 32 | 33 | return Promise.reject(new Error(errorText)); 34 | } 35 | 36 | return this.Editor.Saver.save(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /types/api/listeners.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes Editor`s listeners API 3 | */ 4 | export interface Listeners { 5 | /** 6 | * Subscribe to event dispatched on passed element. Returns listener id. 7 | * 8 | * @param {Element} element 9 | * @param {string} eventType 10 | * @param {(event: Event) => void}handler 11 | * @param {boolean} useCapture 12 | */ 13 | on(element: Element, eventType: string, handler: (event?: Event) => void, useCapture?: boolean): string; 14 | 15 | /** 16 | * Unsubscribe from event dispatched on passed element 17 | * 18 | * @param {Element} element 19 | * @param {string} eventType 20 | * @param {(event: Event) => void}handler 21 | * @param {boolean} useCapture 22 | */ 23 | off(element: Element, eventType: string, handler: (event?: Event) => void, useCapture?: boolean): void; 24 | 25 | 26 | /** 27 | * Unsubscribe from event dispatched by the listener id 28 | * 29 | * @param id - id of the listener to remove 30 | */ 31 | offById(id: string): void; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/modules/api/sanitizer.ts: -------------------------------------------------------------------------------- 1 | import { Sanitizer as ISanitizer } from '../../../../types/api'; 2 | import { SanitizerConfig } from '../../../../types/configs'; 3 | import Module from '../../__module'; 4 | import { clean } from '../../utils/sanitizer'; 5 | 6 | /** 7 | * @class SanitizerAPI 8 | * Provides Editor.js Sanitizer that allows developers to clean their HTML 9 | */ 10 | export default class SanitizerAPI extends Module { 11 | /** 12 | * Available methods 13 | * 14 | * @returns {SanitizerConfig} 15 | */ 16 | public get methods(): ISanitizer { 17 | return { 18 | clean: (taintString, config): string => this.clean(taintString, config), 19 | }; 20 | } 21 | 22 | /** 23 | * Perform sanitizing of a string 24 | * 25 | * @param {string} taintString - what to sanitize 26 | * @param {SanitizerConfig} config - sanitizer config 27 | * 28 | * @returns {string} 29 | */ 30 | public clean(taintString: string, config: SanitizerConfig): string { 31 | return clean(taintString, config); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /types/data-formats/output-data.d.ts: -------------------------------------------------------------------------------- 1 | import {BlockToolData} from '../tools'; 2 | import {BlockTuneData} from '../block-tunes/block-tune-data'; 3 | 4 | /** 5 | * Output of one Tool 6 | * 7 | * @template Type - the string literal describing a tool type 8 | * @template Data - the structure describing a data object supported by the tool 9 | */ 10 | export interface OutputBlockData { 11 | /** 12 | * Unique Id of the block 13 | */ 14 | id?: string; 15 | /** 16 | * Tool type 17 | */ 18 | type: Type; 19 | /** 20 | * Saved Block data 21 | */ 22 | data: BlockToolData; 23 | 24 | /** 25 | * Block Tunes data 26 | */ 27 | tunes?: {[name: string]: BlockTuneData}; 28 | } 29 | 30 | export interface OutputData { 31 | /** 32 | * Editor's version 33 | */ 34 | version?: string; 35 | 36 | /** 37 | * Timestamp of saving in milliseconds 38 | */ 39 | time?: number; 40 | 41 | /** 42 | * Saved Blocks 43 | */ 44 | blocks: OutputBlockData[]; 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/cypress.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [pull_request] 3 | jobs: 4 | firefox: 5 | runs-on: ubuntu-latest 6 | container: 7 | image: cypress/browsers:node14.16.0-chrome89-ff86 8 | options: --user 1001 9 | steps: 10 | - uses: actions/checkout@v2 11 | - run: yarn ci:pull_paragraph 12 | - uses: cypress-io/github-action@v2 13 | with: 14 | config: video=false 15 | browser: firefox 16 | build: yarn build 17 | chrome: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v2 21 | - run: yarn ci:pull_paragraph 22 | - uses: cypress-io/github-action@v2 23 | with: 24 | config: video=false 25 | browser: chrome 26 | build: yarn build 27 | edge: 28 | runs-on: windows-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | - run: yarn ci:pull_paragraph 32 | - uses: cypress-io/github-action@v2 33 | with: 34 | config: video=false 35 | browser: edge 36 | build: yarn build 37 | -------------------------------------------------------------------------------- /src/components/tools/tune.ts: -------------------------------------------------------------------------------- 1 | import BaseTool, { ToolType } from './base'; 2 | import { BlockAPI, BlockTune as IBlockTune, BlockTuneConstructable } from '../../../types'; 3 | import { BlockTuneData } from '../../../types/block-tunes/block-tune-data'; 4 | 5 | /** 6 | * Stub class for BlockTunes 7 | * 8 | * @todo Implement 9 | */ 10 | export default class BlockTune extends BaseTool { 11 | /** 12 | * Tool type — Tune 13 | */ 14 | public type = ToolType.Tune; 15 | 16 | /** 17 | * Tool's constructable blueprint 18 | */ 19 | protected readonly constructable: BlockTuneConstructable; 20 | 21 | /** 22 | * Constructs new BlockTune instance from constructable 23 | * 24 | * @param data - Tune data 25 | * @param block - Block API object 26 | */ 27 | public create(data: BlockTuneData, block: BlockAPI): IBlockTune { 28 | // eslint-disable-next-line new-cap 29 | return new this.constructable({ 30 | api: this.api.getMethodsForTool(this), 31 | config: this.settings, 32 | block, 33 | data, 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/tools/inline.ts: -------------------------------------------------------------------------------- 1 | import BaseTool, { InternalInlineToolSettings, ToolType } from './base'; 2 | import { InlineTool as IInlineTool, InlineToolConstructable } from '../../../types'; 3 | 4 | /** 5 | * InlineTool object to work with Inline Tools constructables 6 | */ 7 | export default class InlineTool extends BaseTool { 8 | /** 9 | * Tool type — Inline 10 | */ 11 | public type = ToolType.Inline; 12 | 13 | /** 14 | * Tool's constructable blueprint 15 | */ 16 | protected constructable: InlineToolConstructable; 17 | 18 | /** 19 | * Returns title for Inline Tool if specified by user 20 | */ 21 | public get title(): string { 22 | return this.constructable[InternalInlineToolSettings.Title]; 23 | } 24 | 25 | /** 26 | * Constructs new InlineTool instance from constructable 27 | */ 28 | public create(): IInlineTool { 29 | // eslint-disable-next-line new-cap 30 | return new this.constructable({ 31 | api: this.api.getMethodsForTool(this), 32 | config: this.settings, 33 | }) as IInlineTool; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/modules/api/readonly.ts: -------------------------------------------------------------------------------- 1 | import { ReadOnly } from '../../../../types/api'; 2 | import Module from '../../__module'; 3 | 4 | /** 5 | * @class ReadOnlyAPI 6 | * @classdesc ReadOnly API 7 | */ 8 | export default class ReadOnlyAPI extends Module { 9 | /** 10 | * Available methods 11 | */ 12 | public get methods(): ReadOnly { 13 | const getIsEnabled = (): boolean => this.isEnabled; 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-this-alias 16 | return { 17 | toggle: (state): Promise => this.toggle(state), 18 | get isEnabled(): boolean { 19 | return getIsEnabled(); 20 | }, 21 | }; 22 | } 23 | 24 | /** 25 | * Set or toggle read-only state 26 | * 27 | * @param {boolean|undefined} state - set or toggle state 28 | * 29 | * @returns {boolean} current value 30 | */ 31 | public toggle(state?: boolean): Promise { 32 | return this.Editor.ReadOnly.toggle(state); 33 | } 34 | 35 | /** 36 | * Returns current read-only state 37 | */ 38 | public get isEnabled(): boolean { 39 | return this.Editor.ReadOnly.isEnabled; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/styles/input.css: -------------------------------------------------------------------------------- 1 | .cdx-search-field { 2 | --icon-margin-right: 10px; 3 | 4 | background: rgba(232,232,235,0.49); 5 | border: 1px solid rgba(226,226,229,0.20); 6 | border-radius: 6px; 7 | padding: 2px; 8 | display: grid; 9 | grid-template-columns: auto auto 1fr; 10 | grid-template-rows: auto; 11 | 12 | &__icon { 13 | width: var(--toolbox-buttons-size); 14 | height: var(--toolbox-buttons-size); 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | margin-right: var(--icon-margin-right); 19 | 20 | .icon { 21 | width: 14px; 22 | height: 14px; 23 | color: var(--grayText); 24 | flex-shrink: 0; 25 | } 26 | } 27 | 28 | 29 | &__input { 30 | font-size: 14px; 31 | outline: none; 32 | font-weight: 500; 33 | font-family: inherit; 34 | border: 0; 35 | background: transparent; 36 | margin: 0; 37 | padding: 0; 38 | line-height: 22px; 39 | min-width: calc(100% - var(--toolbox-buttons-size) - var(--icon-margin-right)); 40 | 41 | &::placeholder { 42 | color: var(--grayText); 43 | font-weight: 500; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/cypress/plugins/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | /* tslint:disable:no-var-requires */ 3 | /** 4 | * This file contains connection of Cypres plugins 5 | */ 6 | const webpackConfig = require('../../../webpack.config.js'); 7 | const preprocessor = require('@cypress/webpack-preprocessor'); 8 | const codeCoverageTask = require('@cypress/code-coverage/task'); 9 | 10 | module.exports = (on, config): unknown => { 11 | /** 12 | * Add Cypress task to get code coverage 13 | */ 14 | codeCoverageTask(on, config); 15 | 16 | /** 17 | * Prepare webpack preprocessor options 18 | */ 19 | const options = preprocessor.defaultOptions; 20 | 21 | /** 22 | * Provide path to typescript package 23 | */ 24 | options.typescript = require.resolve('typescript'); 25 | 26 | /** 27 | * Provide our webpack config 28 | */ 29 | options.webpackOptions = webpackConfig({}, { mode: 'test' }); 30 | 31 | /** 32 | * Register webpack preprocessor 33 | */ 34 | on('file:preprocessor', preprocessor(options)); 35 | 36 | // It's IMPORTANT to return the config object 37 | // with any changed environment variables 38 | return config; 39 | }; 40 | -------------------------------------------------------------------------------- /types/tools/tool.d.ts: -------------------------------------------------------------------------------- 1 | import {API} from '../index'; 2 | import {ToolConfig} from './tool-config'; 3 | import {SanitizerConfig} from '../configs'; 4 | 5 | /** 6 | * Abstract interface of all Tools 7 | */ 8 | export interface BaseTool { 9 | /** 10 | * Tool`s render method 11 | * For inline Tools returns inline toolbar button 12 | * For block Tools returns tool`s wrapper 13 | */ 14 | render(): HTMLElement; 15 | } 16 | 17 | export interface BaseToolConstructable { 18 | /** 19 | * Define Tool type as Inline 20 | */ 21 | isInline?: boolean; 22 | 23 | /** 24 | * Tool`s sanitizer configuration 25 | */ 26 | sanitize?: SanitizerConfig; 27 | 28 | /** 29 | * Title of Inline Tool 30 | */ 31 | title?: string; 32 | 33 | /** 34 | * Describe constructor parameters 35 | */ 36 | new (config: {api: API, config?: ToolConfig}): BaseTool; 37 | 38 | /** 39 | * Tool`s prepare method. Can be async 40 | * @param data 41 | */ 42 | prepare?(data: {toolName: string, config: ToolConfig}): void | Promise; 43 | 44 | /** 45 | * Tool`s reset method to clean up anything set by prepare. Can be async 46 | */ 47 | reset?(): void | Promise; 48 | } 49 | -------------------------------------------------------------------------------- /docs/caret.md: -------------------------------------------------------------------------------- 1 | # Editor.js Caret Module 2 | 3 | The `Caret` module contains methods working with caret. Uses [Range](https://developer.mozilla.org/en-US/docs/Web/API/Range) methods to navigate caret 4 | between blocks. 5 | 6 | Caret class implements basic Module class that holds User configuration 7 | and default Editor.js instances 8 | 9 | ## Properties 10 | 11 | ## Methods 12 | 13 | ### setToBlock 14 | 15 | ```javascript 16 | Caret.setToBlock(block, position, offset) 17 | ``` 18 | 19 | > Method gets Block instance and puts caret to the text node with offset 20 | 21 | #### params 22 | 23 | | Param | Type | Description| 24 | | -------------|------ |:-------------:| 25 | | block | Object | Block instance that BlockManager created| 26 | | position | String | Can be 'start', 'end' or 'default'. Other values will be treated as 'default'. Shows position of the caret regarding to the Block.| 27 | | offset | Number | caret offset regarding to the text node (Default: 0)| 28 | 29 | 30 | ### setToTheLastBlock 31 | 32 | ```javascript 33 | Caret.setToTheLastBlock() 34 | ``` 35 | 36 | > sets Caret at the end of last Block 37 | If last block is not empty, inserts another empty Block which is passed as initial 38 | -------------------------------------------------------------------------------- /types/tools/paste-events.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Event detail for tag substitution on paste 3 | */ 4 | export interface HTMLPasteEventDetail { 5 | /** 6 | * Pasted element 7 | */ 8 | data: HTMLElement; 9 | } 10 | 11 | /** 12 | * Paste event for tag substitution 13 | */ 14 | export interface HTMLPasteEvent extends CustomEvent { 15 | readonly detail: HTMLPasteEventDetail; 16 | } 17 | 18 | /** 19 | * Event detail for file substitution on paste 20 | */ 21 | export interface FilePasteEventDetail { 22 | /** 23 | * Pasted file 24 | */ 25 | file: File; 26 | } 27 | 28 | export interface FilePasteEvent extends CustomEvent { 29 | readonly detail: FilePasteEventDetail; 30 | } 31 | 32 | /** 33 | * Event detail for pattern substitution on paste 34 | */ 35 | export interface PatternPasteEventDetail { 36 | /** 37 | * Pattern key 38 | */ 39 | key: string; 40 | 41 | /** 42 | * Pasted string 43 | */ 44 | data: string; 45 | } 46 | 47 | export interface PatternPasteEvent extends CustomEvent { 48 | readonly detail: PatternPasteEventDetail; 49 | } 50 | 51 | export type PasteEvent = HTMLPasteEvent | FilePasteEvent | PatternPasteEvent; 52 | export type PasteEventDetail = HTMLPasteEventDetail | FilePasteEventDetail | PatternPasteEventDetail; 53 | -------------------------------------------------------------------------------- /types/configs/sanitizer-config.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Sanitizer config of each HTML element 3 | * @see {@link https://github.com/guardian/html-janitor#options} 4 | */ 5 | type TagConfig = boolean | { [attr: string]: boolean | string }; 6 | 7 | export interface SanitizerConfig { 8 | /** 9 | * Tag name and params not to be stripped off 10 | * @see {@link https://github.com/guardian/html-janitor} 11 | * 12 | * @example Save P tags 13 | * p: true 14 | * 15 | * @example Save A tags and do not strip HREF attribute 16 | * a: { 17 | * href: true 18 | * } 19 | * 20 | * @example Save A tags with TARGET="_blank" attribute 21 | * a: function (aTag) { 22 | * return aTag.target === '_black'; 23 | * } 24 | * 25 | * @example Save U tags that are not empty 26 | * u: function(el){ 27 | * return el.textContent !== ''; 28 | * } 29 | * 30 | * @example For blockquote with class 'indent' save CLASS and STYLE attributes 31 | * Otherwise strip all attributes 32 | * blockquote: function(el) { 33 | * if (el.classList.contains('indent')) { 34 | * return { 'class': true, 'style': true }; 35 | * } else { 36 | * return {}; 37 | * } 38 | * } 39 | */ 40 | [key: string]: TagConfig | ((el: Element) => TagConfig); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/modules/api/selection.ts: -------------------------------------------------------------------------------- 1 | import SelectionUtils from '../../selection'; 2 | import { Selection as SelectionAPIInterface } from '../../../../types/api'; 3 | import Module from '../../__module'; 4 | 5 | /** 6 | * @class SelectionAPI 7 | * Provides with methods working with SelectionUtils 8 | */ 9 | export default class SelectionAPI extends Module { 10 | /** 11 | * Available methods 12 | * 13 | * @returns {SelectionAPIInterface} 14 | */ 15 | public get methods(): SelectionAPIInterface { 16 | return { 17 | findParentTag: (tagName: string, className?: string): HTMLElement | null => this.findParentTag(tagName, className), 18 | expandToTag: (node: HTMLElement): void => this.expandToTag(node), 19 | }; 20 | } 21 | 22 | /** 23 | * Looks ahead from selection and find passed tag with class name 24 | * 25 | * @param {string} tagName - tag to find 26 | * @param {string} className - tag's class name 27 | * 28 | * @returns {HTMLElement|null} 29 | */ 30 | public findParentTag(tagName: string, className?: string): HTMLElement | null { 31 | return new SelectionUtils().findParentTag(tagName, className); 32 | } 33 | 34 | /** 35 | * Expand selection to passed tag 36 | * 37 | * @param {HTMLElement} node - tag that should contain selection 38 | */ 39 | public expandToTag(node: HTMLElement): void { 40 | new SelectionUtils().expandToTag(node); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/modules/api/i18n.ts: -------------------------------------------------------------------------------- 1 | import { I18n } from '../../../../types/api'; 2 | import I18nInternal from '../../i18n'; 3 | import { logLabeled } from '../../utils'; 4 | import Module from '../../__module'; 5 | import { ToolClass } from '../../tools/collection'; 6 | 7 | /** 8 | * Provides methods for working with i18n 9 | */ 10 | export default class I18nAPI extends Module { 11 | /** 12 | * Return namespace section for tool or block tune 13 | * 14 | * @param tool - tool object 15 | */ 16 | private static getNamespace(tool: ToolClass): string { 17 | if (tool.isTune()) { 18 | return `blockTunes.${tool.name}`; 19 | } 20 | 21 | return `tools.${tool.name}`; 22 | } 23 | 24 | /** 25 | * Return I18n API methods with global dictionary access 26 | */ 27 | public get methods(): I18n { 28 | return { 29 | t: (): string | undefined => { 30 | logLabeled('I18n.t() method can be accessed only from Tools', 'warn'); 31 | 32 | return undefined; 33 | }, 34 | }; 35 | } 36 | 37 | /** 38 | * Return I18n API methods with tool namespaced dictionary 39 | * 40 | * @param tool - Tool object 41 | */ 42 | public getMethodsForTool(tool: ToolClass): I18n { 43 | return Object.assign( 44 | this.methods, 45 | { 46 | t: (dictKey: string): string => { 47 | return I18nInternal.t(I18nAPI.getNamespace(tool), dictKey); 48 | }, 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "codex" 4 | ], 5 | "rules": { 6 | /** 7 | * Temporary suppress some errors. We need to fix them partially in next patches 8 | */ 9 | "import/no-duplicates": ["warn"], 10 | "@typescript-eslint/triple-slash-reference": ["off"], 11 | "jsdoc/no-undefined-types": ["warn", {"definedTypes": [ 12 | "ConstructorOptions", 13 | "API", 14 | "BlockToolConstructable", 15 | "EditorConfig", 16 | "Tool", 17 | "ToolSettings" 18 | ]}] 19 | }, 20 | "settings": { 21 | "jsdoc": { 22 | "mode": "typescript" 23 | } 24 | }, 25 | "globals": { 26 | "Node": true, 27 | "Range": true, 28 | "HTMLElement": true, 29 | "HTMLDivElement": true, 30 | "Element": true, 31 | "Selection": true, 32 | "SVGElement": true, 33 | "Text": true, 34 | "InsertPosition": true, 35 | "PropertyKey": true, 36 | "MouseEvent": true, 37 | "TouchEvent": true, 38 | "KeyboardEvent": true, 39 | "ClipboardEvent": true, 40 | "DragEvent": true, 41 | "Event": true, 42 | "EventTarget": true, 43 | "Document": true, 44 | "NodeList": true, 45 | "File": true, 46 | "FileList": true, 47 | "MutationRecord": true, 48 | "AddEventListenerOptions": true, 49 | "DataTransfer": true, 50 | "DOMRect": true, 51 | "ClientRect": true, 52 | "ArrayLike": true, 53 | "InputEvent": true, 54 | "unknown": true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/cypress/tests/initialization.spec.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line spaced-comment 2 | /// 3 | 4 | describe('Editor basic initialization', () => { 5 | describe('Zero-config initialization', () => { 6 | /** 7 | * In this test suite we use zero (omitted) configuration 8 | */ 9 | const editorConfig = {}; 10 | 11 | beforeEach(() => { 12 | if (this && this.editorInstance) { 13 | this.editorInstance.destroy(); 14 | } else { 15 | cy.createEditor(editorConfig).as('editorInstance'); 16 | } 17 | }); 18 | 19 | it('should create a visible UI', () => { 20 | /** 21 | * Assert if created instance is visible or not. 22 | */ 23 | cy.get('[data-cy=editorjs]') 24 | .get('div.codex-editor') 25 | .should('be.visible'); 26 | }); 27 | }); 28 | 29 | describe('Configuration', () => { 30 | describe('readOnly', () => { 31 | beforeEach(() => { 32 | if (this && this.editorInstance) { 33 | this.editorInstance.destroy(); 34 | } 35 | }); 36 | 37 | it('should create editor without editing ability when true passed', () => { 38 | cy.createEditor({ 39 | readOnly: true, 40 | }).as('editorInstance'); 41 | 42 | cy.get('[data-cy=editorjs]') 43 | .get('div.codex-editor') 44 | .get('div.ce-paragraph') 45 | .invoke('attr', 'contenteditable') 46 | .should('eq', 'false'); 47 | }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/modules/api/events.ts: -------------------------------------------------------------------------------- 1 | import Module from '../../__module'; 2 | import { Events } from '../../../../types/api'; 3 | 4 | /** 5 | * @class EventsAPI 6 | * provides with methods working with Toolbar 7 | */ 8 | export default class EventsAPI extends Module { 9 | /** 10 | * Available methods 11 | * 12 | * @returns {Events} 13 | */ 14 | public get methods(): Events { 15 | return { 16 | emit: (eventName: string, data: object): void => this.emit(eventName, data), 17 | off: (eventName: string, callback: () => void): void => this.off(eventName, callback), 18 | on: (eventName: string, callback: () => void): void => this.on(eventName, callback), 19 | }; 20 | } 21 | 22 | /** 23 | * Subscribe on Events 24 | * 25 | * @param {string} eventName - event name to subscribe 26 | * @param {Function} callback - event handler 27 | */ 28 | public on(eventName, callback): void { 29 | this.eventsDispatcher.on(eventName, callback); 30 | } 31 | 32 | /** 33 | * Emit event with data 34 | * 35 | * @param {string} eventName - event to emit 36 | * @param {object} data - event's data 37 | */ 38 | public emit(eventName, data): void { 39 | this.eventsDispatcher.emit(eventName, data); 40 | } 41 | 42 | /** 43 | * Unsubscribe from Event 44 | * 45 | * @param {string} eventName - event to unsubscribe 46 | * @param {Function} callback - event handler 47 | */ 48 | public off(eventName, callback): void { 49 | this.eventsDispatcher.off(eventName, callback); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/modules/api/notifier.ts: -------------------------------------------------------------------------------- 1 | import EventsDispatcher from '../../utils/events'; 2 | import { Notifier as INotifier } from '../../../../types/api'; 3 | import Notifier from '../../utils/notifier'; 4 | import { ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions } from 'codex-notifier'; 5 | import Module from '../../__module'; 6 | import { ModuleConfig } from '../../../types-internal/module-config'; 7 | 8 | /** 9 | * 10 | */ 11 | export default class NotifierAPI extends Module { 12 | /** 13 | * Notifier utility Instance 14 | */ 15 | private notifier: Notifier; 16 | 17 | /** 18 | * @class 19 | * @param {object} moduleConfiguration - Module Configuration 20 | * @param {EditorConfig} moduleConfiguration.config - Editor's config 21 | * @param {EventsDispatcher} moduleConfiguration.eventsDispatcher - Editor's event dispatcher 22 | */ 23 | constructor({ config, eventsDispatcher }: ModuleConfig) { 24 | super({ 25 | config, 26 | eventsDispatcher, 27 | }); 28 | 29 | this.notifier = new Notifier(); 30 | } 31 | 32 | /** 33 | * Available methods 34 | */ 35 | public get methods(): INotifier { 36 | return { 37 | show: (options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions): void => this.show(options), 38 | }; 39 | } 40 | 41 | /** 42 | * Show notification 43 | * 44 | * @param {NotifierOptions} options - message option 45 | */ 46 | public show(options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions): void { 47 | return this.notifier.show(options); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/cypress/tests/readOnly.spec.ts: -------------------------------------------------------------------------------- 1 | import EditorJS, { EditorConfig } from '../../../types'; 2 | 3 | describe('ReadOnly API spec', () => { 4 | function createEditor(config?: EditorConfig): void { 5 | const editorConfig = Object.assign({}, config || {}); 6 | 7 | cy.createEditor(editorConfig).as('editorInstance'); 8 | } 9 | 10 | it('should return correct value for readOnly.isEnabled when editor initialized in normal mode', () => { 11 | createEditor(); 12 | 13 | cy 14 | .get('@editorInstance') 15 | .then(editor => { 16 | expect(editor.readOnly.isEnabled).to.be.false; 17 | }); 18 | }); 19 | 20 | it('should return correct value for readOnly.isEnabled when editor initialized in read-only mode', () => { 21 | createEditor({ 22 | readOnly: true, 23 | }); 24 | 25 | cy 26 | .get('@editorInstance') 27 | .then(editor => { 28 | expect(editor.readOnly.isEnabled).to.be.true; 29 | }); 30 | }); 31 | 32 | it('should return correct value for readOnly.isEnabled when read-only mode toggled', () => { 33 | createEditor(); 34 | 35 | cy 36 | .get('@editorInstance') 37 | .then(async editor => { 38 | expect(editor.readOnly.isEnabled).to.be.false; 39 | 40 | editor.readOnly.toggle() 41 | .then(() => { 42 | expect(editor.readOnly.isEnabled).to.be.true; 43 | }) 44 | .then(() => editor.readOnly.toggle()) 45 | .then(() => { 46 | expect(editor.readOnly.isEnabled).to.be.false; 47 | }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/styles/toolbar.css: -------------------------------------------------------------------------------- 1 | .ce-toolbar { 2 | position: absolute; 3 | left: 0; 4 | right: 0; 5 | top: 0; 6 | transition: opacity 100ms ease; 7 | will-change: opacity, top; 8 | 9 | display: none; 10 | 11 | &--opened { 12 | display: block; 13 | } 14 | 15 | &__content { 16 | max-width: var(--content-width); 17 | margin: 0 auto; 18 | position: relative; 19 | } 20 | 21 | &__plus { 22 | @apply --toolbox-button; 23 | flex-shrink: 0; 24 | 25 | &-shortcut { 26 | opacity: 0.6; 27 | word-spacing: -2px; 28 | margin-top: 5px; 29 | } 30 | 31 | @media (--mobile){ 32 | @apply --overlay-pane; 33 | position: static; 34 | } 35 | } 36 | 37 | /** 38 | * Block actions Zone 39 | * ------------------------- 40 | */ 41 | &__actions { 42 | position: absolute; 43 | right: 100%; 44 | opacity: 0; 45 | display: flex; 46 | padding-right: 5px; 47 | 48 | &--opened { 49 | opacity: 1; 50 | } 51 | 52 | @media (--mobile){ 53 | right: auto; 54 | } 55 | } 56 | 57 | &__settings-btn { 58 | @apply --toolbox-button; 59 | 60 | margin-left: 5px; 61 | cursor: pointer; 62 | user-select: none; 63 | 64 | @media (--not-mobile){ 65 | width: 18px; 66 | } 67 | 68 | &--hidden { 69 | display: none; 70 | } 71 | 72 | @media (--mobile){ 73 | @apply --overlay-pane; 74 | position: static; 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * Styles for Narrow mode 81 | */ 82 | .codex-editor--narrow .ce-toolbar__plus { 83 | @media (--not-mobile) { 84 | left: 5px; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /example/assets/json-preview.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module to compose output JSON preview 3 | */ 4 | const cPreview = (function (module) { 5 | /** 6 | * Shows JSON in pretty preview 7 | * @param {object} output - what to show 8 | * @param {Element} holder - where to show 9 | */ 10 | module.show = function(output, holder) { 11 | /** Make JSON pretty */ 12 | output = JSON.stringify( output, null, 4 ); 13 | /** Encode HTML entities */ 14 | output = encodeHTMLEntities( output ); 15 | /** Stylize! */ 16 | output = stylize( output ); 17 | holder.innerHTML = output; 18 | }; 19 | 20 | /** 21 | * Converts '>', '<', '&' symbols to entities 22 | */ 23 | function encodeHTMLEntities(string) { 24 | return string.replace(/&/g, '&').replace(//g, '>'); 25 | } 26 | 27 | /** 28 | * Some styling magic 29 | */ 30 | function stylize(string) { 31 | /** Stylize JSON keys */ 32 | string = string.replace( /"(\w+)"\s?:/g, '"$1" :'); 33 | /** Stylize tool names */ 34 | string = string.replace( /"(paragraph|quote|list|header|link|code|image|delimiter|raw|checklist|table|embed|warning)"/g, '"$1"'); 35 | /** Stylize HTML tags */ 36 | string = string.replace( /(<[\/a-z]+(>)?)/gi, '$1' ); 37 | /** Stylize strings */ 38 | string = string.replace( /"([^"]+)"/gi, '"$1"' ); 39 | /** Boolean/Null */ 40 | string = string.replace( /\b(true|false|null)\b/gi, '$1' ); 41 | return string; 42 | } 43 | 44 | return module; 45 | })({}); 46 | -------------------------------------------------------------------------------- /src/styles/rtl.css: -------------------------------------------------------------------------------- 1 | .codex-editor.codex-editor--rtl { 2 | direction: rtl; 3 | 4 | .cdx-list { 5 | padding-left: 0; 6 | padding-right: 40px; 7 | } 8 | 9 | .ce-toolbar { 10 | &__plus { 11 | right: calc(var(--toolbox-buttons-size) * -1); 12 | left: auto; 13 | } 14 | 15 | &__actions { 16 | right: auto; 17 | left: calc(var(--toolbox-buttons-size) * -1); 18 | 19 | @media (--mobile){ 20 | margin-left: 0; 21 | margin-right: auto; 22 | padding-right: 0; 23 | padding-left: 10px; 24 | } 25 | } 26 | } 27 | 28 | .ce-settings { 29 | left: 5px; 30 | right: auto; 31 | 32 | &::before{ 33 | right: auto; 34 | left: 25px; 35 | } 36 | 37 | &__button { 38 | &:not(:nth-child(3n+3)) { 39 | margin-left: 3px; 40 | margin-right: 0; 41 | } 42 | } 43 | } 44 | 45 | .ce-conversion-tool { 46 | &__icon { 47 | margin-right: 0px; 48 | margin-left: 10px; 49 | } 50 | } 51 | 52 | .ce-inline-toolbar { 53 | &__dropdown { 54 | border-right: 0px solid transparent; 55 | border-left: 1px solid var(--color-gray-border); 56 | margin: 0 -6px 0 6px; 57 | 58 | .icon--toggler-down { 59 | margin-left: 0px; 60 | margin-right: 4px; 61 | } 62 | } 63 | } 64 | 65 | } 66 | 67 | .codex-editor--narrow.codex-editor--rtl { 68 | .ce-toolbar__plus { 69 | @media (--not-mobile) { 70 | left: 0px; 71 | right: 5px; 72 | } 73 | } 74 | 75 | .ce-toolbar__actions { 76 | @media (--not-mobile) { 77 | left: -5px; 78 | } 79 | } 80 | } 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/components/modules/api/toolbar.ts: -------------------------------------------------------------------------------- 1 | import { Toolbar } from '../../../../types/api'; 2 | import Module from '../../__module'; 3 | import * as _ from './../../utils'; 4 | /** 5 | * @class ToolbarAPI 6 | * Provides methods for working with the Toolbar 7 | */ 8 | export default class ToolbarAPI extends Module { 9 | /** 10 | * Available methods 11 | * 12 | * @returns {Toolbar} 13 | */ 14 | public get methods(): Toolbar { 15 | return { 16 | close: (): void => this.close(), 17 | open: (): void => this.open(), 18 | toggleBlockSettings: (openingState?: boolean): void => this.toggleBlockSettings(openingState), 19 | }; 20 | } 21 | 22 | /** 23 | * Open toolbar 24 | */ 25 | public open(): void { 26 | this.Editor.Toolbar.moveAndOpen(); 27 | } 28 | 29 | /** 30 | * Close toolbar and all included elements 31 | */ 32 | public close(): void { 33 | this.Editor.Toolbar.close(); 34 | } 35 | 36 | /** 37 | * Toggles Block Setting of the current block 38 | * 39 | * @param {boolean} openingState — opening state of Block Setting 40 | */ 41 | public toggleBlockSettings(openingState?: boolean): void { 42 | if (this.Editor.BlockManager.currentBlockIndex === -1) { 43 | _.logLabeled('Could\'t toggle the Toolbar because there is no block selected ', 'warn'); 44 | 45 | return; 46 | } 47 | 48 | /** Check that opening state is set or not */ 49 | const canOpenBlockSettings = openingState ?? !this.Editor.BlockSettings.opened; 50 | 51 | if (canOpenBlockSettings) { 52 | this.Editor.Toolbar.moveAndOpen(); 53 | this.Editor.BlockSettings.open(); 54 | } else { 55 | this.Editor.BlockSettings.close(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/utils/scroll-locker.ts: -------------------------------------------------------------------------------- 1 | import { isIosDevice } from '../utils'; 2 | 3 | /** 4 | * Utility allowing to lock body scroll on demand 5 | */ 6 | export default class ScrollLocker { 7 | /** 8 | * Style classes 9 | */ 10 | private static CSS = { 11 | scrollLocked: 'ce-scroll-locked', 12 | scrollLockedHard: 'ce-scroll-locked--hard', 13 | } 14 | 15 | /** 16 | * Stores scroll position, used for hard scroll lock 17 | */ 18 | private scrollPosition: null|number 19 | 20 | /** 21 | * Locks body element scroll 22 | */ 23 | public lock(): void { 24 | if (isIosDevice) { 25 | this.lockHard(); 26 | } else { 27 | document.body.classList.add(ScrollLocker.CSS.scrollLocked); 28 | } 29 | } 30 | 31 | /** 32 | * Unlocks body element scroll 33 | */ 34 | public unlock(): void { 35 | if (isIosDevice) { 36 | this.unlockHard(); 37 | } else { 38 | document.body.classList.remove(ScrollLocker.CSS.scrollLocked); 39 | } 40 | } 41 | 42 | /** 43 | * Locks scroll in a hard way (via setting fixed position to body element) 44 | */ 45 | private lockHard(): void { 46 | this.scrollPosition = window.pageYOffset; 47 | document.documentElement.style.setProperty( 48 | '--window-scroll-offset', 49 | `${this.scrollPosition}px` 50 | ); 51 | document.body.classList.add(ScrollLocker.CSS.scrollLockedHard); 52 | } 53 | 54 | /** 55 | * Unlocks hard scroll lock 56 | */ 57 | private unlockHard(): void { 58 | document.body.classList.remove(ScrollLocker.CSS.scrollLockedHard); 59 | if (this.scrollPosition !== null) { 60 | window.scrollTo(0, this.scrollPosition); 61 | } 62 | this.scrollPosition = null; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/cypress/tests/utils.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import { isFunction } from '../../../src/components/utils'; 3 | 4 | function syncFunction(): void {} 5 | 6 | async function asyncFunction(): Promise {} 7 | 8 | const syncArrowFunction = (): void => {}; 9 | 10 | const asyncArrowFunction = async (): Promise => {}; 11 | 12 | describe('isFunction function', () => { 13 | it('should recognise sync functions', () => { 14 | /** 15 | * Act 16 | */ 17 | const commonFunctionResult = isFunction(syncFunction); 18 | const arrowFunctionResult = isFunction(syncArrowFunction); 19 | 20 | /** 21 | * Assert 22 | */ 23 | expect(commonFunctionResult).to.eq(true); 24 | expect(arrowFunctionResult).to.eq(true); 25 | }); 26 | 27 | it('should recognise async functions', () => { 28 | /** 29 | * Act 30 | */ 31 | const commonFunctionResult = isFunction(asyncFunction); 32 | const arrowFunctionResult = isFunction(asyncArrowFunction); 33 | 34 | /** 35 | * Assert 36 | */ 37 | expect(commonFunctionResult).to.eq(true); 38 | expect(arrowFunctionResult).to.eq(true); 39 | }); 40 | 41 | it('should return false if it isn\'t a function', () => { 42 | /** 43 | * Arrange 44 | */ 45 | const obj = {}; 46 | const num = 123; 47 | const str = '123'; 48 | 49 | /** 50 | * Act 51 | */ 52 | const objResult = isFunction(obj); 53 | const numResult = isFunction(num); 54 | const strResult = isFunction(str); 55 | 56 | /** 57 | * Assert 58 | */ 59 | expect(objResult).to.eq(false); 60 | expect(numResult).to.eq(false); 61 | expect(strResult).to.eq(false); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /types/tools/inline-tool.d.ts: -------------------------------------------------------------------------------- 1 | import {BaseTool, BaseToolConstructable} from './tool'; 2 | import {API, ToolConfig} from '../index'; 3 | /** 4 | * Base structure for the Inline Toolbar Tool 5 | */ 6 | export interface InlineTool extends BaseTool { 7 | /** 8 | * Shortcut for Tool 9 | * @type {string} 10 | */ 11 | shortcut?: string; 12 | 13 | /** 14 | * Method that accepts selected range and wrap it somehow 15 | * @param {Range} range - selection's range 16 | */ 17 | surround(range: Range): void; 18 | 19 | /** 20 | * Get SelectionUtils and detect if Tool was applied 21 | * For example, after that Tool can highlight button or show some details 22 | * @param {Selection} selection - current Selection 23 | */ 24 | checkState(selection: Selection): boolean; 25 | 26 | /** 27 | * Make additional element with actions 28 | * For example, input for the 'link' tool or textarea for the 'comment' tool 29 | */ 30 | renderActions?(): HTMLElement; 31 | 32 | /** 33 | * Function called with Inline Toolbar closing 34 | * @deprecated 2020 10/02 - The new instance will be created each time the button is rendered. So clear is not needed. 35 | * Better to create the 'destroy' method in a future. 36 | */ 37 | clear?(): void; 38 | } 39 | 40 | 41 | /** 42 | * Describe constructor parameters 43 | */ 44 | export interface InlineToolConstructorOptions { 45 | api: API; 46 | config?: ToolConfig; 47 | } 48 | 49 | export interface InlineToolConstructable extends BaseToolConstructable { 50 | /** 51 | * Constructor 52 | * 53 | * @param {InlineToolConstructorOptions} config - constructor parameters 54 | */ 55 | new(config: InlineToolConstructorOptions): BaseTool; 56 | } 57 | -------------------------------------------------------------------------------- /src/components/utils/tooltip.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/no-undefined-types */ 2 | /** 3 | * Use external module CodeX Tooltip 4 | */ 5 | import CodeXTooltips from 'codex-tooltip'; 6 | import type { TooltipOptions, TooltipContent } from 'codex-tooltip/types'; 7 | 8 | /** 9 | * Tooltip 10 | * 11 | * Decorates any tooltip module like adapter 12 | */ 13 | export default class Tooltip { 14 | /** 15 | * Tooltips lib: CodeX Tooltips 16 | * 17 | * @see https://github.com/codex-team/codex.tooltips 18 | */ 19 | private lib: CodeXTooltips = new CodeXTooltips(); 20 | 21 | /** 22 | * Release the library 23 | */ 24 | public destroy(): void { 25 | this.lib.destroy(); 26 | } 27 | 28 | /** 29 | * Shows tooltip on element with passed HTML content 30 | * 31 | * @param {HTMLElement} element - any HTML element in DOM 32 | * @param content - tooltip's content 33 | * @param options - showing settings 34 | */ 35 | public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void { 36 | this.lib.show(element, content, options); 37 | } 38 | 39 | /** 40 | * Hides tooltip 41 | * 42 | * @param skipHidingDelay — pass true to immediately hide the tooltip 43 | */ 44 | public hide(skipHidingDelay = false): void { 45 | this.lib.hide(skipHidingDelay); 46 | } 47 | 48 | /** 49 | * Binds 'mouseenter' and 'mouseleave' events that shows/hides the Tooltip 50 | * 51 | * @param {HTMLElement} element - any HTML element in DOM 52 | * @param content - tooltip's content 53 | * @param options - showing settings 54 | */ 55 | public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void { 56 | this.lib.onHover(element, content, options); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /types/block-tunes/block-tune.d.ts: -------------------------------------------------------------------------------- 1 | import {API, BlockAPI, SanitizerConfig, ToolConfig} from '../index'; 2 | import { BlockTuneData } from './block-tune-data'; 3 | 4 | /** 5 | * Describes BLockTune blueprint 6 | */ 7 | export interface BlockTune { 8 | /** 9 | * Returns block tune HTMLElement 10 | * 11 | * @return {HTMLElement} 12 | */ 13 | render(): HTMLElement; 14 | 15 | /** 16 | * Method called on Tool render. Pass Tool content as an argument. 17 | * 18 | * You can wrap Tool's content with any wrapper you want to provide Tune's UI 19 | * 20 | * @param {HTMLElement} pluginsContent — Tool's content wrapper 21 | * 22 | * @return {HTMLElement} 23 | */ 24 | wrap?(pluginsContent: HTMLElement): HTMLElement; 25 | 26 | /** 27 | * Called on Tool's saving. Should return any data Tune needs to save 28 | * 29 | * @return {BlockTuneData} 30 | */ 31 | save?(): BlockTuneData; 32 | } 33 | 34 | /** 35 | * Describes BlockTune class constructor function 36 | */ 37 | export interface BlockTuneConstructable { 38 | 39 | /** 40 | * Flag show Tool is Block Tune 41 | */ 42 | isTune: boolean; 43 | 44 | /** 45 | * Tune's sanitize configuration 46 | */ 47 | sanitize?: SanitizerConfig; 48 | 49 | /** 50 | * @constructor 51 | * 52 | * @param config - Block Tune config 53 | */ 54 | new(config: { 55 | api: API, 56 | config?: ToolConfig, 57 | block: BlockAPI, 58 | data: BlockTuneData, 59 | }): BlockTune; 60 | 61 | /** 62 | * Tune`s prepare method. Can be async 63 | * @param data 64 | */ 65 | prepare?(): Promise | void; 66 | 67 | /** 68 | * Tune`s reset method to clean up anything set by prepare. Can be async 69 | */ 70 | reset?(): void | Promise; 71 | } 72 | -------------------------------------------------------------------------------- /src/components/modules/api/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module API 3 | * @copyright 2018 4 | * 5 | * Each block has an Editor API instance to use provided public methods 6 | * if you cant to read more about how API works, please see docs 7 | */ 8 | import Module from '../../__module'; 9 | import { API as APIInterfaces } from '../../../../types'; 10 | import { ToolClass } from '../../tools/collection'; 11 | 12 | /** 13 | * @class API 14 | */ 15 | export default class API extends Module { 16 | /** 17 | * Editor.js Core API modules 18 | */ 19 | public get methods(): APIInterfaces { 20 | return { 21 | blocks: this.Editor.BlocksAPI.methods, 22 | caret: this.Editor.CaretAPI.methods, 23 | events: this.Editor.EventsAPI.methods, 24 | listeners: this.Editor.ListenersAPI.methods, 25 | notifier: this.Editor.NotifierAPI.methods, 26 | sanitizer: this.Editor.SanitizerAPI.methods, 27 | saver: this.Editor.SaverAPI.methods, 28 | selection: this.Editor.SelectionAPI.methods, 29 | styles: this.Editor.StylesAPI.classes, 30 | toolbar: this.Editor.ToolbarAPI.methods, 31 | inlineToolbar: this.Editor.InlineToolbarAPI.methods, 32 | tooltip: this.Editor.TooltipAPI.methods, 33 | i18n: this.Editor.I18nAPI.methods, 34 | readOnly: this.Editor.ReadOnlyAPI.methods, 35 | ui: this.Editor.UiAPI.methods, 36 | }; 37 | } 38 | 39 | /** 40 | * Returns Editor.js Core API methods for passed tool 41 | * 42 | * @param tool - tool object 43 | */ 44 | public getMethodsForTool(tool: ToolClass): APIInterfaces { 45 | return Object.assign( 46 | this.methods, 47 | { 48 | i18n: this.Editor.I18nAPI.getMethodsForTool(tool), 49 | } 50 | ) as APIInterfaces; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/cypress/support/index.d.ts: -------------------------------------------------------------------------------- 1 | // in cypress/support/index.d.ts 2 | // load type definitions that come with Cypress module 3 | /// 4 | 5 | import type { EditorConfig, OutputData } from './../../../types/index'; 6 | import type EditorJS from '../../../types/index' 7 | 8 | declare global { 9 | namespace Cypress { 10 | interface Chainable { 11 | /** 12 | * Custom command to select DOM element by data-cy attribute. 13 | * @param editorConfig - config to pass to the editor 14 | * @example cy.createEditor({}) 15 | */ 16 | createEditor(editorConfig: EditorConfig): Chainable 17 | 18 | /** 19 | * Paste command to dispatch paste event 20 | * 21 | * @usage 22 | * cy.get('div').paste({'text/plain': 'Text', 'text/html': 'Text'}) 23 | * 24 | * @param data - map with MIME type as a key and data as value 25 | */ 26 | paste(data: {[type: string]: string}): Chainable 27 | 28 | /** 29 | * Copy command to dispatch copy event on subject 30 | * 31 | * @usage 32 | * cy.get('div').copy().then(data => {}) 33 | */ 34 | copy(): Chainable<{ [type: string]: any }>; 35 | 36 | /** 37 | * Cut command to dispatch cut event on subject 38 | * 39 | * @usage 40 | * cy.get('div').cut().then(data => {}) 41 | */ 42 | cut(): Chainable<{ [type: string]: any }>; 43 | 44 | /** 45 | * Calls EditorJS API render method 46 | * 47 | * @param data — data to render 48 | */ 49 | render(data: OutputData): Chainable; 50 | } 51 | 52 | interface ApplicationWindow { 53 | EditorJS: typeof EditorJS 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/cypress/tests/api/block.spec.ts: -------------------------------------------------------------------------------- 1 | import { BlockMutationType } from '../../../../types/events/block/mutation-type'; 2 | 3 | /** 4 | * There will be described test cases of BlockAPI 5 | */ 6 | describe('BlockAPI', () => { 7 | const firstBlock = { 8 | id: 'bwnFX5LoX7', 9 | type: 'paragraph', 10 | data: { 11 | text: 'The first block content mock.', 12 | }, 13 | }; 14 | const editorDataMock = { 15 | blocks: [ 16 | firstBlock, 17 | ], 18 | }; 19 | 20 | /** 21 | * EditorJS API is passed as the first parameter of the onChange callback 22 | */ 23 | const EditorJSApiMock = Cypress.sinon.match.any; 24 | 25 | beforeEach(() => { 26 | if (this && this.editorInstance) { 27 | this.editorInstance.destroy(); 28 | } else { 29 | const config = { 30 | data: editorDataMock, 31 | onChange: (): void => { console.log('something changed'); }, 32 | }; 33 | 34 | cy.createEditor(config).as('editorInstance'); 35 | 36 | cy.spy(config, 'onChange').as('onChange'); 37 | } 38 | }); 39 | 40 | /** 41 | * block.dispatchChange(); 42 | */ 43 | describe('.dispatchChange()', () => { 44 | /** 45 | * Check that blocks.dispatchChange() triggers Editor 'onChange' callback 46 | */ 47 | it('should trigger onChange with corresponded block', () => { 48 | cy.get('@editorInstance').then(async (editor: any) => { 49 | const block = editor.blocks.getById(firstBlock.id); 50 | 51 | block.dispatchChange(); 52 | 53 | cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ 54 | type: BlockMutationType.Changed, 55 | detail: { 56 | index: 0, 57 | }, 58 | })); 59 | }); 60 | }); 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /src/styles/settings.css: -------------------------------------------------------------------------------- 1 | .ce-settings { 2 | @apply --overlay-pane; 3 | top: var(--toolbar-buttons-size); 4 | left: 0; 5 | min-width: 114px; 6 | box-sizing: content-box; 7 | 8 | @media (--mobile){ 9 | bottom: 40px; 10 | right: auto; 11 | top: auto; 12 | } 13 | 14 | &::before{ 15 | left: auto; 16 | right: 12px; 17 | 18 | @media (--mobile){ 19 | bottom: -5px; 20 | top: auto; 21 | } 22 | } 23 | 24 | display: none; 25 | 26 | &--opened { 27 | display: block; 28 | animation-duration: 0.1s; 29 | animation-name: panelShowing; 30 | } 31 | 32 | &__plugin-zone { 33 | &:not(:empty){ 34 | padding: 3px 3px 0; 35 | } 36 | } 37 | 38 | &__default-zone { 39 | &:not(:empty){ 40 | padding: 3px; 41 | } 42 | } 43 | 44 | &__button { 45 | @apply --toolbar-button; 46 | 47 | &:not(:nth-child(3n+3)) { 48 | margin-right: 3px; 49 | } 50 | 51 | &:nth-child(n+4) { 52 | margin-top: 3px; 53 | } 54 | 55 | line-height: 32px; 56 | 57 | &--disabled { 58 | cursor: not-allowed !important; 59 | opacity: .3; 60 | } 61 | 62 | &--selected { 63 | color: var(--color-active-icon); 64 | } 65 | 66 | &--delete { 67 | transition: background-color 300ms ease; 68 | will-change: background-color; 69 | 70 | .icon { 71 | transition: transform 200ms ease-out; 72 | will-change: transform; 73 | } 74 | } 75 | 76 | &--confirm { 77 | background-color: var(--color-confirm) !important; 78 | color: #fff; 79 | 80 | &:hover { 81 | background-color: color-mod(var(--color-confirm) blackness(+5%)) !important; 82 | } 83 | 84 | .icon { 85 | transform: rotate(90deg); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /.postcssrc.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | # Consumes files by @import rule 3 | # https://github.com/postcss/postcss-import 4 | postcss-import: {} 5 | 6 | # Apply custom property sets via @apply rule 7 | # https://github.com/pascalduez/postcss-apply 8 | postcss-apply: {} 9 | 10 | # Convert modern CSS into something most browsers can understand 11 | # https://github.com/csstools/postcss-preset-env 12 | postcss-preset-env: 13 | # Polyfill CSS features 14 | # https://github.com/csstools/postcss-preset-env#stage 15 | # 16 | # List of features with levels: https://cssdb.org/ 17 | stage: 0 18 | 19 | # Define polyfills based on browsers you are supporting 20 | # https://github.com/csstools/postcss-preset-env#browsers 21 | browsers: 22 | - 'last 2 versions' 23 | - '> 1%' 24 | 25 | # Instruct all plugins to omit pre-polyfilled CSS 26 | # https://github.com/csstools/postcss-preset-env#preserve 27 | preserve: false 28 | 29 | # Enable or disable specific polyfills 30 | # https://github.com/csstools/postcss-preset-env#features 31 | # 32 | # List of available plugins 33 | # https://github.com/csstools/postcss-preset-env/blob/master/src/lib/plugins-by-id.js 34 | features: 35 | # Modify colors using the color-mod() function in CSS 36 | # https://github.com/jonathantneal/postcss-color-mod-function 37 | color-mod-function: {} 38 | 39 | # Nested rules unwrapper 40 | # https://github.com/postcss/postcss-nested 41 | # 42 | # As you know 'postcss-preset-env' plugin has an ability to process 43 | # 'postcss-nesting' feature but it does not work with BEM 44 | # Report: https://github.com/csstools/postcss-preset-env/issues/40 45 | postcss-nested: {} 46 | 47 | # Compression tool 48 | # https://github.com/cssnano/cssnano 49 | cssnano: {} 50 | -------------------------------------------------------------------------------- /src/components/i18n/namespace-internal.ts: -------------------------------------------------------------------------------- 1 | import defaultDictionary from './locales/en/messages.json'; 2 | import { DictNamespaces } from '../../types-internal/i18n-internal-namespace'; 3 | import { isObject, isString } from '../utils'; 4 | 5 | /** 6 | * Evaluate messages dictionary and return object for namespace chaining 7 | * 8 | * @param dict - Messages dictionary 9 | * @param [keyPath] - subsection path (used in recursive call) 10 | */ 11 | function getNamespaces(dict: object, keyPath?: string): DictNamespaces { 12 | const result = {}; 13 | 14 | Object.entries(dict).forEach(([key, section]) => { 15 | if (isObject(section)) { 16 | const newPath = keyPath ? `${keyPath}.${key}` : key; 17 | 18 | /** 19 | * Check current section values, if all of them are strings, so there is the last section 20 | */ 21 | const isLastSection = Object.values(section).every((sectionValue) => { 22 | return isString(sectionValue); 23 | }); 24 | 25 | /** 26 | * In last section, we substitute namespace path instead of object with translates 27 | * 28 | * ui.toolbar.toolbox – "ui.toolbar.toolbox" 29 | * instead of 30 | * ui.toolbar.toolbox – {"Add": ""} 31 | */ 32 | if (isLastSection) { 33 | result[key] = newPath; 34 | } else { 35 | result[key] = getNamespaces(section, newPath); 36 | } 37 | 38 | return; 39 | } 40 | 41 | result[key] = section; 42 | }); 43 | 44 | return result as DictNamespaces; 45 | } 46 | 47 | /** 48 | * Type safe access to the internal messages dictionary sections 49 | * 50 | * @example I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'); 51 | */ 52 | export const I18nInternalNS = getNamespaces(defaultDictionary); 53 | -------------------------------------------------------------------------------- /types/api/block.d.ts: -------------------------------------------------------------------------------- 1 | import {BlockToolData, ToolConfig} from '../tools'; 2 | import {SavedData} from '../data-formats'; 3 | 4 | /** 5 | * @interface BlockAPI Describes Block API methods and properties 6 | */ 7 | export interface BlockAPI { 8 | /** 9 | * Block unique identifier 10 | */ 11 | readonly id: string; 12 | 13 | /** 14 | * Tool name 15 | */ 16 | readonly name: string; 17 | 18 | /** 19 | * Tool config passed on Editor's initialization 20 | */ 21 | readonly config: ToolConfig; 22 | 23 | /** 24 | * Wrapper of Tool's HTML element 25 | */ 26 | readonly holder: HTMLElement; 27 | 28 | /** 29 | * True if Block content is empty 30 | */ 31 | readonly isEmpty: boolean; 32 | 33 | /** 34 | * True if Block is selected with Cross-Block selection 35 | */ 36 | readonly selected: boolean; 37 | 38 | /** 39 | * Setter sets Block's stretch state 40 | * 41 | * Getter returns true if Block is stretched 42 | */ 43 | stretched: boolean; 44 | 45 | /** 46 | * Call Tool method with errors handler under-the-hood 47 | * 48 | * @param {string} methodName - method to call 49 | * @param {object} param - object with parameters 50 | * 51 | * @return {void} 52 | */ 53 | call(methodName: string, param?: object): void; 54 | 55 | /** 56 | * Save Block content 57 | * 58 | * @return {Promise} 59 | */ 60 | save(): Promise; 61 | 62 | /** 63 | * Validate Block data 64 | * 65 | * @param {BlockToolData} data 66 | * 67 | * @return {Promise} 68 | */ 69 | validate(data: BlockToolData): Promise; 70 | 71 | /** 72 | * Allows to say Editor that Block was changed. Used to manually trigger Editor's 'onChange' callback 73 | * Can be useful for block changes invisible for editor core. 74 | */ 75 | dispatchChange(): void; 76 | } 77 | -------------------------------------------------------------------------------- /types/tools/tool-settings.d.ts: -------------------------------------------------------------------------------- 1 | import {ToolConfig} from './tool-config'; 2 | import {ToolConstructable} from './index'; 3 | 4 | /** 5 | * Tool's Toolbox settings 6 | */ 7 | export interface ToolboxConfig { 8 | /** 9 | * Tool title for Toolbox 10 | */ 11 | title?: string; 12 | 13 | /** 14 | * HTML string with an icon for Toolbox 15 | */ 16 | icon?: string; 17 | } 18 | 19 | /** 20 | * Object passed to the Tool's constructor by {@link EditorConfig#tools} 21 | * 22 | * @template Config - the structure describing a config object supported by the tool 23 | */ 24 | export interface ExternalToolSettings { 25 | 26 | /** 27 | * Tool's class 28 | */ 29 | class: ToolConstructable; 30 | 31 | /** 32 | * User configuration object that will be passed to the Tool's constructor 33 | */ 34 | config?: ToolConfig; 35 | 36 | /** 37 | * Is need to show Inline Toolbar. 38 | * Can accept array of Tools for InlineToolbar or boolean. 39 | */ 40 | inlineToolbar?: boolean | string[]; 41 | 42 | /** 43 | * BlockTunes for Tool 44 | * Can accept array of tune names or boolean. 45 | */ 46 | tunes?: boolean | string[]; 47 | 48 | /** 49 | * Define shortcut that will render Tool 50 | */ 51 | shortcut?: string; 52 | 53 | /** 54 | * Tool's Toolbox settings 55 | * It will be hidden from Toolbox when false is specified. 56 | */ 57 | toolbox?: ToolboxConfig | false; 58 | } 59 | 60 | /** 61 | * For internal Tools 'class' property is optional 62 | */ 63 | export type InternalToolSettings = Omit, 'class'> & Partial, 'class'>>; 64 | 65 | /** 66 | * Union of external and internal Tools settings 67 | */ 68 | export type ToolSettings = InternalToolSettings | ExternalToolSettings; 69 | -------------------------------------------------------------------------------- /src/components/modules/api/listeners.ts: -------------------------------------------------------------------------------- 1 | import { Listeners } from '../../../../types/api'; 2 | import Module from '../../__module'; 3 | 4 | /** 5 | * @class ListenersAPI 6 | * Provides with methods working with DOM Listener 7 | */ 8 | export default class ListenersAPI extends Module { 9 | /** 10 | * Available methods 11 | * 12 | * @returns {Listeners} 13 | */ 14 | public get methods(): Listeners { 15 | return { 16 | on: (element: HTMLElement, eventType, handler, useCapture): string => this.on(element, eventType, handler, useCapture), 17 | off: (element, eventType, handler, useCapture): void => this.off(element, eventType, handler, useCapture), 18 | offById: (id): void => this.offById(id), 19 | }; 20 | } 21 | 22 | /** 23 | * Ads a DOM event listener. Return it's id. 24 | * 25 | * @param {HTMLElement} element - Element to set handler to 26 | * @param {string} eventType - event type 27 | * @param {() => void} handler - event handler 28 | * @param {boolean} useCapture - capture event or not 29 | */ 30 | public on(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): string { 31 | return this.listeners.on(element, eventType, handler, useCapture); 32 | } 33 | 34 | /** 35 | * Removes DOM listener from element 36 | * 37 | * @param {Element} element - Element to remove handler from 38 | * @param eventType - event type 39 | * @param handler - event handler 40 | * @param {boolean} useCapture - capture event or not 41 | */ 42 | public off(element: Element, eventType: string, handler: () => void, useCapture?: boolean): void { 43 | this.listeners.off(element, eventType, handler, useCapture); 44 | } 45 | 46 | /** 47 | * Removes DOM listener by the listener id 48 | * 49 | * @param id - id of the listener to remove 50 | */ 51 | public offById(id: string): void { 52 | this.listeners.offById(id); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /types/api/caret.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes Editor`s caret API 3 | */ 4 | export interface Caret { 5 | 6 | /** 7 | * Sets caret to the first Block 8 | * 9 | * @param {string} position - position where to set caret 10 | * @param {number} offset - caret offset 11 | * 12 | * @return {boolean} 13 | */ 14 | setToFirstBlock(position?: 'end'|'start'|'default', offset?: number): boolean; 15 | 16 | /** 17 | * Sets caret to the last Block 18 | * 19 | * @param {string} position - position where to set caret 20 | * @param {number} offset - caret offset 21 | * 22 | * @return {boolean} 23 | */ 24 | setToLastBlock(position?: 'end'|'start'|'default', offset?: number): boolean; 25 | 26 | /** 27 | * Sets caret to the previous Block 28 | * 29 | * @param {string} position - position where to set caret 30 | * @param {number} offset - caret offset 31 | * 32 | * @return {boolean} 33 | */ 34 | setToPreviousBlock(position?: 'end'|'start'|'default', offset?: number): boolean; 35 | 36 | /** 37 | * Sets caret to the next Block 38 | * 39 | * @param {string} position - position where to set caret 40 | * @param {number} offset - caret offset 41 | * 42 | * @return {boolean} 43 | */ 44 | setToNextBlock(position?: 'end'|'start'|'default', offset?: number): boolean; 45 | 46 | /** 47 | * Sets caret to the Block by passed index 48 | * 49 | * @param {number} index - index of Block where to set caret 50 | * @param {string} position - position where to set caret 51 | * @param {number} offset - caret offset 52 | * 53 | * @return {boolean} 54 | */ 55 | setToBlock(index: number, position?: 'end'|'start'|'default', offset?: number): boolean; 56 | 57 | /** 58 | * Sets caret to the Editor 59 | * 60 | * @param {boolean} atEnd - if true, set Caret to the end of the Editor 61 | * 62 | * @return {boolean} 63 | */ 64 | focus(atEnd?: boolean): boolean; 65 | } 66 | -------------------------------------------------------------------------------- /src/components/tools/collection.ts: -------------------------------------------------------------------------------- 1 | import BlockTool from './block'; 2 | import InlineTool from './inline'; 3 | import BlockTune from './tune'; 4 | 5 | export type ToolClass = BlockTool | InlineTool | BlockTune; 6 | 7 | /** 8 | * Class to store Editor Tools 9 | */ 10 | export default class ToolsCollection extends Map { 11 | /** 12 | * Returns Block Tools collection 13 | */ 14 | public get blockTools(): ToolsCollection { 15 | const tools = Array 16 | .from(this.entries()) 17 | .filter(([, tool]) => tool.isBlock()) as [string, BlockTool][]; 18 | 19 | return new ToolsCollection(tools); 20 | } 21 | 22 | /** 23 | * Returns Inline Tools collection 24 | */ 25 | public get inlineTools(): ToolsCollection { 26 | const tools = Array 27 | .from(this.entries()) 28 | .filter(([, tool]) => tool.isInline()) as [string, InlineTool][]; 29 | 30 | return new ToolsCollection(tools); 31 | } 32 | 33 | /** 34 | * Returns Block Tunes collection 35 | */ 36 | public get blockTunes(): ToolsCollection { 37 | const tools = Array 38 | .from(this.entries()) 39 | .filter(([, tool]) => tool.isTune()) as [string, BlockTune][]; 40 | 41 | return new ToolsCollection(tools); 42 | } 43 | 44 | /** 45 | * Returns internal Tools collection 46 | */ 47 | public get internalTools(): ToolsCollection { 48 | const tools = Array 49 | .from(this.entries()) 50 | .filter(([, tool]) => tool.isInternal); 51 | 52 | return new ToolsCollection(tools); 53 | } 54 | 55 | /** 56 | * Returns Tools collection provided by user 57 | */ 58 | public get externalTools(): ToolsCollection { 59 | const tools = Array 60 | .from(this.entries()) 61 | .filter(([, tool]) => !tool.isInternal); 62 | 63 | return new ToolsCollection(tools); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/cypress/tests/tools/ToolsFactory.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import LinkInlineTool from '../../../../src/components/inline-tools/inline-tool-link'; 3 | import MoveUpTune from '../../../../src/components/block-tunes/block-tune-move-up'; 4 | import ToolsFactory from '../../../../src/components/tools/factory'; 5 | import InlineTool from '../../../../src/components/tools/inline'; 6 | import BlockTool from '../../../../src/components/tools/block'; 7 | import BlockTune from '../../../../src/components/tools/tune'; 8 | import Paragraph from '../../../../src/tools/paragraph/dist/bundle'; 9 | 10 | describe('ToolsFactory', (): void => { 11 | let factory; 12 | const config = { 13 | paragraph: { 14 | class: Paragraph, 15 | }, 16 | link: { 17 | class: LinkInlineTool, 18 | }, 19 | moveUp: { 20 | class: MoveUpTune, 21 | }, 22 | }; 23 | 24 | beforeEach((): void => { 25 | factory = new ToolsFactory( 26 | config, 27 | { 28 | placeholder: 'Placeholder', 29 | defaultBlock: 'paragraph', 30 | } as any, 31 | {} as any 32 | ); 33 | }); 34 | 35 | context('.get', (): void => { 36 | it('should return appropriate tool object', (): void => { 37 | const tool = factory.get('link'); 38 | 39 | expect(tool.name).to.be.eq('link'); 40 | }); 41 | 42 | it('should return InlineTool object for inline tool', (): void => { 43 | const tool = factory.get('link'); 44 | 45 | expect(tool instanceof InlineTool).to.be.true; 46 | }); 47 | 48 | it('should return BlockTool object for block tool', (): void => { 49 | const tool = factory.get('paragraph'); 50 | 51 | expect(tool instanceof BlockTool).to.be.true; 52 | }); 53 | 54 | it('should return BlockTune object for tune', (): void => { 55 | const tool = factory.get('moveUp'); 56 | 57 | expect(tool instanceof BlockTune).to.be.true; 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/styles/conversion-toolbar.css: -------------------------------------------------------------------------------- 1 | .ce-conversion-toolbar { 2 | @apply --overlay-pane; 3 | 4 | opacity: 0; 5 | visibility: hidden; 6 | will-change: transform, opacity; 7 | transition: transform 100ms ease, opacity 100ms ease; 8 | transform: translateY(-8px); 9 | left: -1px; 10 | width: 150px; 11 | margin-top: 5px; 12 | box-sizing: content-box; 13 | 14 | &--showed { 15 | opacity: 1; 16 | visibility: visible; 17 | transform: none; 18 | } 19 | 20 | [hidden] { 21 | display: none !important; 22 | } 23 | 24 | &__buttons { 25 | display: flex; 26 | } 27 | 28 | &__label { 29 | color: var(--grayText); 30 | font-size: 11px; 31 | font-weight: 500; 32 | letter-spacing: 0.33px; 33 | padding: 10px 10px 5px; 34 | text-transform: uppercase; 35 | } 36 | } 37 | 38 | .ce-conversion-tool { 39 | display: flex; 40 | padding: 5px 10px; 41 | font-size: 14px; 42 | line-height: 20px; 43 | font-weight: 500; 44 | cursor: pointer; 45 | align-items: center; 46 | 47 | &--hidden { 48 | display: none; 49 | } 50 | 51 | &--focused { 52 | box-shadow: inset 0 0 0px 1px rgba(7, 161, 227, 0.08); 53 | background: rgba(34, 186, 255, 0.08) !important; 54 | 55 | &-animated { 56 | animation-name: buttonClicked; 57 | animation-duration: 250ms; 58 | } 59 | } 60 | 61 | &:hover { 62 | background: var(--bg-light); 63 | } 64 | 65 | &__icon { 66 | display: inline-flex; 67 | width: 20px; 68 | height: 20px; 69 | border: 1px solid var(--color-gray-border); 70 | border-radius: 3px; 71 | align-items: center; 72 | justify-content: center; 73 | margin-right: 10px; 74 | background: #fff; 75 | 76 | svg { 77 | width: 11px; 78 | height: 11px; 79 | } 80 | } 81 | 82 | &--last { 83 | margin-right: 0 !important; 84 | } 85 | 86 | &--active { 87 | color: var(--color-active-icon) !important; 88 | } 89 | 90 | &--active { 91 | animation: bounceIn 0.75s 1; 92 | animation-fill-mode: forwards; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /.github/workflows/publish-package-to-npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to NPM 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Checkout to target branch 13 | - uses: actions/checkout@v2 14 | with: 15 | # Pull submodules 16 | submodules: 'recursive' 17 | 18 | - name: Get package info 19 | id: package 20 | uses: codex-team/action-nodejs-package-info@v1 21 | 22 | # Setup node environment 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: 15 26 | registry-url: https://registry.npmjs.org/ 27 | 28 | # Prepare, build and publish project 29 | - name: Install dependencies 30 | run: yarn 31 | 32 | - name: Build output files 33 | run: yarn build 34 | 35 | - name: Publish the package with a NEXT tag 36 | run: yarn publish --access=public --tag=next 37 | env: 38 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 39 | 40 | - name: Add LATEST tag for the published package if this is not a prerelease version 41 | if: github.event.release.prerelease != true 42 | run: npm dist-tag add ${{ steps.package.outputs.name }}@${{ steps.package.outputs.version }} latest 43 | env: 44 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | 46 | notify: 47 | needs: publish 48 | runs-on: ubuntu-latest 49 | steps: 50 | # Checkout to target branch 51 | - uses: actions/checkout@v2 52 | 53 | - name: Get package info 54 | id: package 55 | uses: codex-team/action-nodejs-package-info@v1 56 | 57 | - name: Send a message 58 | uses: codex-team/action-codexbot-notify@v1 59 | with: 60 | webhook: ${{ secrets.CODEX_BOT_NOTIFY_EDITORJS_PUBLIC_CHAT }} 61 | message: '📦 [${{ steps.package.outputs.name }}](${{ steps.package.outputs.npmjs-link }}) ${{ steps.package.outputs.version }} was published' 62 | parse_mode: 'markdown' 63 | disable_web_page_preview: true 64 | -------------------------------------------------------------------------------- /src/types-internal/i18n-internal-namespace.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Decorator above the type object 3 | */ 4 | type Indexed = { [key: string]: T }; 5 | 6 | /** 7 | * Type for I18n dictionary values that can be strings or dictionary sub-sections 8 | * 9 | * Can be used as: 10 | * LeavesDictKeys 11 | * 12 | * where myDictionary is a JSON with messages 13 | */ 14 | export type LeavesDictKeys = D extends string 15 | /** 16 | * If generic type is string, just return it 17 | */ 18 | ? D 19 | /** 20 | * If generic type is object that has only one level and contains only strings, return it's keys union 21 | * 22 | * { key: "string", anotherKey: "string" } => "key" | "anotherKey" 23 | * 24 | */ 25 | : D extends Indexed 26 | ? keyof D 27 | /** 28 | * If generic type is object, but not the one described above, 29 | * use LeavesDictKey on it's values recursively and union the results 30 | * 31 | * { "rootKey": { "subKey": "string" }, "anotherRootKey": { "anotherSubKey": "string" } } => "subKey" | "anotherSubKey" 32 | * 33 | */ 34 | : D extends Indexed 35 | ? { [K in keyof D]: LeavesDictKeys }[keyof D] 36 | 37 | /** 38 | * In other cases, return never type 39 | */ 40 | : never; 41 | 42 | /** 43 | * Provide type-safe access to the available namespaces of the dictionary 44 | * 45 | * Can be uses as: 46 | * DictNamespaces 47 | * 48 | * where myDictionary is a JSON with messages 49 | */ 50 | export type DictNamespaces = { 51 | /** 52 | * Iterate through generic type keys 53 | * 54 | * If value under current key is object that has only one level and contains only strings, return string type 55 | */ 56 | [K in keyof D]: D[K] extends Indexed 57 | ? string 58 | /** 59 | * If value under current key is object with depth more than one, apply DictNamespaces recursively 60 | */ 61 | : D[K] extends Indexed 62 | ? DictNamespaces 63 | /** 64 | * In other cases, return never type 65 | */ 66 | : never; 67 | } 68 | 69 | -------------------------------------------------------------------------------- /test/cypress/tests/sanitisation.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | describe('Output sanitisation', () => { 3 | beforeEach(() => { 4 | if (this && this.editorInstance) { 5 | this.editorInstance.destroy(); 6 | } else { 7 | cy.createEditor({}).as('editorInstance'); 8 | } 9 | }); 10 | 11 | context('Output should save inline formatting', () => { 12 | it('should save initial formatting for paragraph', () => { 13 | cy.createEditor({ 14 | data: { 15 | blocks: [ { 16 | type: 'paragraph', 17 | data: { text: 'Bold text' }, 18 | } ], 19 | }, 20 | }).then(async editor => { 21 | const output = await (editor as any).save(); 22 | 23 | const boldText = output.blocks[0].data.text; 24 | 25 | expect(boldText).to.eq('Bold text'); 26 | }); 27 | }); 28 | 29 | it('should save formatting for paragraph', () => { 30 | cy.get('[data-cy=editorjs]') 31 | .get('div.ce-block') 32 | .click() 33 | .type('This text should be bold.{selectall}'); 34 | 35 | cy.get('[data-cy=editorjs]') 36 | .get('button.ce-inline-tool--bold') 37 | .click(); 38 | 39 | cy.get('[data-cy=editorjs]') 40 | .get('div.ce-block') 41 | .click(); 42 | 43 | cy.get('@editorInstance').then(async editorInstance => { 44 | const output = await (editorInstance as any).save(); 45 | 46 | const text = output.blocks[0].data.text; 47 | 48 | expect(text).to.match(/This text should be bold\.(
)?<\/b>/); 49 | }); 50 | }); 51 | 52 | it('should save formatting for paragraph on paste', () => { 53 | cy.get('[data-cy=editorjs]') 54 | .get('div.ce-block') 55 | .paste({ 'text/html': '

Text

Bold text

' }); 56 | 57 | cy.get('@editorInstance').then(async editorInstance => { 58 | const output = await (editorInstance as any).save(); 59 | 60 | const boldText = output.blocks[1].data.text; 61 | 62 | expect(boldText).to.eq('Bold text'); 63 | }); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/styles/block.css: -------------------------------------------------------------------------------- 1 | @keyframes fade-in { 2 | from { 3 | opacity: 0; 4 | } 5 | 6 | to { 7 | opacity: 1; 8 | } 9 | } 10 | 11 | .ce-block { 12 | animation: fade-in 300ms ease; 13 | animation-fill-mode: initial; 14 | 15 | &:first-of-type { 16 | margin-top: 0; 17 | } 18 | 19 | &--selected &__content { 20 | background: var(--selectionColor); 21 | 22 | /** 23 | * Workaround Safari case when user can select inline-fragment with cross-block-selection 24 | */ 25 | & [contenteditable] { 26 | -webkit-user-select: none; 27 | user-select: none; 28 | } 29 | 30 | img, 31 | .ce-stub { 32 | opacity: 0.55; 33 | } 34 | } 35 | 36 | &--stretched &__content { 37 | max-width: none; 38 | } 39 | 40 | &__content { 41 | position: relative; 42 | max-width: var(--content-width); 43 | margin: 0 auto; 44 | transition: background-color 150ms ease; 45 | } 46 | 47 | &--drop-target &__content { 48 | &:before { 49 | content: ''; 50 | position: absolute; 51 | top: 100%; 52 | left: -20px; 53 | margin-top: -1px; 54 | height: 8px; 55 | width: 8px; 56 | border: solid var(--color-active-icon); 57 | border-width: 1px 1px 0 0; 58 | transform-origin: right; 59 | transform: rotate(45deg); 60 | } 61 | 62 | &:after { 63 | content: ''; 64 | position: absolute; 65 | top: 100%; 66 | height: 1px; 67 | width: 100%; 68 | color: var(--color-active-icon); 69 | background: repeating-linear-gradient( 70 | 90deg, 71 | var(--color-active-icon), 72 | var(--color-active-icon) 1px, 73 | #fff 1px, 74 | #fff 6px 75 | ); 76 | } 77 | } 78 | 79 | a { 80 | cursor: pointer; 81 | text-decoration: underline; 82 | } 83 | 84 | b { 85 | font-weight: bold; 86 | } 87 | 88 | i { 89 | font-style: italic; 90 | } 91 | } 92 | 93 | .codex-editor--narrow .ce-block--focused { 94 | @media (--not-mobile) { 95 | margin-right: calc(var(--narrow-mode-right-padding) * -1); 96 | padding-right: var(--narrow-mode-right-padding); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "example/tools/inline-code"] 2 | path = example/tools/inline-code 3 | url = https://github.com/editor-js/inline-code 4 | [submodule "example/tools/header"] 5 | path = example/tools/header 6 | url = https://github.com/editor-js/header 7 | [submodule "example/tools/delimiter"] 8 | path = example/tools/delimiter 9 | url = https://github.com/editor-js/delimiter 10 | [submodule "example/tools/list"] 11 | path = example/tools/list 12 | url = https://github.com/editor-js/list 13 | [submodule "example/tools/quote"] 14 | path = example/tools/quote 15 | url = https://github.com/editor-js/quote 16 | [submodule "example/tools/simple-image"] 17 | path = example/tools/simple-image 18 | url = https://github.com/editor-js/simple-image 19 | [submodule "src/tools/paragraph"] 20 | path = src/tools/paragraph 21 | url = https://github.com/editor-js/paragraph 22 | [submodule "example/tools/marker"] 23 | path = example/tools/marker 24 | url = https://github.com/editor-js/marker 25 | [submodule "example/tools/code"] 26 | path = example/tools/code 27 | url = https://github.com/editor-js/code 28 | [submodule "example/tools/image"] 29 | path = example/tools/image 30 | url = https://github.com/editor-js/image 31 | [submodule "example/tools/embed"] 32 | path = example/tools/embed 33 | url = https://github.com/editor-js/embed 34 | [submodule "example/tools/table"] 35 | path = example/tools/table 36 | url = https://github.com/editor-js/table 37 | [submodule "example/tools/checklist"] 38 | path = example/tools/checklist 39 | url = https://github.com/editor-js/checklist 40 | [submodule "example/tools/link"] 41 | path = example/tools/link 42 | url = https://github.com/editor-js/link 43 | [submodule "example/tools/raw"] 44 | path = example/tools/raw 45 | url = https://github.com/editor-js/raw 46 | [submodule "example/tools/warning"] 47 | path = example/tools/warning 48 | url = https://github.com/editor-js/warning 49 | [submodule "example/tools/underline"] 50 | path = example/tools/underline 51 | url = https://github.com/editor-js/underline 52 | [submodule "example/tools/nested-list"] 53 | path = example/tools/nested-list 54 | url = https://github.com/editor-js/nested-list 55 | [submodule "example/tools/text-variant-tune"] 56 | path = example/tools/text-variant-tune 57 | url = https://github.com/editor-js/text-variant-tune 58 | -------------------------------------------------------------------------------- /src/components/inline-tools/inline-tool-italic.ts: -------------------------------------------------------------------------------- 1 | import $ from '../dom'; 2 | import { InlineTool, SanitizerConfig } from '../../../types'; 3 | 4 | /** 5 | * Italic Tool 6 | * 7 | * Inline Toolbar Tool 8 | * 9 | * Style selected text with italic 10 | */ 11 | export default class ItalicInlineTool implements InlineTool { 12 | /** 13 | * Specifies Tool as Inline Toolbar Tool 14 | * 15 | * @returns {boolean} 16 | */ 17 | public static isInline = true; 18 | 19 | /** 20 | * Title for hover-tooltip 21 | */ 22 | public static title = 'Italic'; 23 | 24 | /** 25 | * Sanitizer Rule 26 | * Leave tags 27 | * 28 | * @returns {object} 29 | */ 30 | public static get sanitize(): SanitizerConfig { 31 | return { 32 | i: {}, 33 | } as SanitizerConfig; 34 | } 35 | 36 | /** 37 | * Native Document's command that uses for Italic 38 | */ 39 | private readonly commandName: string = 'italic'; 40 | 41 | /** 42 | * Styles 43 | */ 44 | private readonly CSS = { 45 | button: 'ce-inline-tool', 46 | buttonActive: 'ce-inline-tool--active', 47 | buttonModifier: 'ce-inline-tool--italic', 48 | }; 49 | 50 | /** 51 | * Elements 52 | */ 53 | private nodes: {button: HTMLButtonElement} = { 54 | button: null, 55 | }; 56 | 57 | /** 58 | * Create button for Inline Toolbar 59 | */ 60 | public render(): HTMLElement { 61 | this.nodes.button = document.createElement('button') as HTMLButtonElement; 62 | this.nodes.button.type = 'button'; 63 | this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier); 64 | this.nodes.button.appendChild($.svg('italic', 4, 11)); 65 | 66 | return this.nodes.button; 67 | } 68 | 69 | /** 70 | * Wrap range with tag 71 | * 72 | * @param {Range} range - range to wrap 73 | */ 74 | public surround(range: Range): void { 75 | document.execCommand(this.commandName); 76 | } 77 | 78 | /** 79 | * Check selection and set activated state to button if there are tag 80 | * 81 | * @param {Selection} selection - selection to check 82 | */ 83 | public checkState(selection: Selection): boolean { 84 | const isActive = document.queryCommandState(this.commandName); 85 | 86 | this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive); 87 | 88 | return isActive; 89 | } 90 | 91 | /** 92 | * Set a shortcut 93 | */ 94 | public get shortcut(): string { 95 | return 'CMD+I'; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/components/tools/factory.ts: -------------------------------------------------------------------------------- 1 | import { ToolConstructable, ToolSettings } from '../../../types/tools'; 2 | import { InternalInlineToolSettings, InternalTuneSettings } from './base'; 3 | import InlineTool from './inline'; 4 | import BlockTune from './tune'; 5 | import BlockTool from './block'; 6 | import API from '../modules/api'; 7 | import { EditorConfig } from '../../../types/configs'; 8 | 9 | type ToolConstructor = typeof InlineTool | typeof BlockTool | typeof BlockTune; 10 | 11 | /** 12 | * Factory to construct classes to work with tools 13 | */ 14 | export default class ToolsFactory { 15 | /** 16 | * Tools configuration specified by user 17 | */ 18 | private config: {[name: string]: ToolSettings & { isInternal?: boolean }}; 19 | 20 | /** 21 | * EditorJS API Module 22 | */ 23 | private api: API; 24 | 25 | /** 26 | * EditorJS configuration 27 | */ 28 | private editorConfig: EditorConfig; 29 | 30 | /** 31 | * @class 32 | * 33 | * @param config - tools config 34 | * @param editorConfig - EditorJS config 35 | * @param api - EditorJS API module 36 | */ 37 | constructor( 38 | config: {[name: string]: ToolSettings & { isInternal?: boolean }}, 39 | editorConfig: EditorConfig, 40 | api: API 41 | ) { 42 | this.api = api; 43 | this.config = config; 44 | this.editorConfig = editorConfig; 45 | } 46 | 47 | /** 48 | * Returns Tool object based on it's type 49 | * 50 | * @param name - tool name 51 | */ 52 | public get(name: string): InlineTool | BlockTool | BlockTune { 53 | const { class: constructable, isInternal = false, ...config } = this.config[name]; 54 | 55 | const Constructor = this.getConstructor(constructable); 56 | 57 | return new Constructor({ 58 | name, 59 | constructable, 60 | config, 61 | api: this.api, 62 | isDefault: name === this.editorConfig.defaultBlock, 63 | defaultPlaceholder: this.editorConfig.placeholder, 64 | isInternal, 65 | }); 66 | } 67 | 68 | /** 69 | * Find appropriate Tool object constructor for Tool constructable 70 | * 71 | * @param constructable - Tools constructable 72 | */ 73 | private getConstructor(constructable: ToolConstructable): ToolConstructor { 74 | switch (true) { 75 | case constructable[InternalInlineToolSettings.IsInline]: 76 | return InlineTool; 77 | case constructable[InternalTuneSettings.IsTune]: 78 | return BlockTune; 79 | default: 80 | return BlockTool; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/styles/export.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Block Tool wrapper 3 | */ 4 | .cdx-block { 5 | padding: var(--block-padding-vertical) 0; 6 | 7 | &::-webkit-input-placeholder { 8 | line-height:normal!important; 9 | } 10 | } 11 | 12 | /** 13 | * Input 14 | */ 15 | .cdx-input { 16 | border: 1px solid var(--color-gray-border); 17 | box-shadow: inset 0 1px 2px 0 rgba(35, 44, 72, 0.06); 18 | border-radius: 3px; 19 | padding: 10px 12px; 20 | outline: none; 21 | width: 100%; 22 | box-sizing: border-box; 23 | 24 | /** 25 | * Workaround Firefox bug with cursor position on empty content editable elements with ::before pseudo 26 | * https://bugzilla.mozilla.org/show_bug.cgi?id=904846 27 | */ 28 | &[data-placeholder]::before { 29 | position: static !important; 30 | display: inline-block; 31 | width: 0; 32 | white-space: nowrap; 33 | pointer-events: none; 34 | } 35 | } 36 | 37 | /** 38 | * Settings 39 | */ 40 | .cdx-settings-button { 41 | @apply --toolbar-button; 42 | 43 | &:not(:nth-child(3n+3)) { 44 | margin-right: 3px; 45 | } 46 | 47 | &:nth-child(n+4) { 48 | margin-top: 3px; 49 | } 50 | 51 | &--active { 52 | color: var(--color-active-icon); 53 | } 54 | } 55 | 56 | /** 57 | * Loader 58 | */ 59 | .cdx-loader { 60 | position: relative; 61 | border: 1px solid var(--color-gray-border); 62 | 63 | &::before { 64 | content: ''; 65 | position: absolute; 66 | left: 50%; 67 | top: 50%; 68 | width: 18px; 69 | height: 18px; 70 | margin: -11px 0 0 -11px; 71 | border: 2px solid var(--color-gray-border); 72 | border-left-color: var(--color-active-icon); 73 | border-radius: 50%; 74 | animation: cdxRotation 1.2s infinite linear; 75 | } 76 | } 77 | 78 | @keyframes cdxRotation { 79 | 0% { 80 | transform: rotate(0deg); 81 | } 82 | 100% { 83 | transform: rotate(360deg); 84 | } 85 | } 86 | 87 | /** 88 | * Button 89 | */ 90 | .cdx-button { 91 | padding: 13px; 92 | border-radius: 3px; 93 | border: 1px solid var(--color-gray-border); 94 | font-size: 14.9px; 95 | background: #fff; 96 | box-shadow: 0 2px 2px 0 rgba(18,30,57,0.04); 97 | color: var(--grayText); 98 | text-align: center; 99 | cursor: pointer; 100 | 101 | &:hover { 102 | background: #FBFCFE; 103 | box-shadow: 0 1px 3px 0 rgba(18,30,57,0.08); 104 | } 105 | 106 | svg { 107 | height: 20px; 108 | margin-right: 0.2em; 109 | margin-top: -2px; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /types/configs/i18n-dictionary.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Structure of the i18n dictionary 3 | */ 4 | export interface I18nDictionary { 5 | /** 6 | * Section for translation Tool Names: both block and inline tools 7 | * Example: 8 | * "toolNames": { 9 | * "Text": "Параграф", 10 | * "Heading": "Заголовок", 11 | * "List": "Список", 12 | * ... 13 | * }, 14 | */ 15 | toolNames?: Dictionary; 16 | 17 | /** 18 | * Section for passing translations to the external tools classes 19 | * The first-level keys of this object should be equal of keys ot the 'tools' property of EditorConfig 20 | * Includes internal tools: "paragraph", "stub" 21 | * 22 | * Example: 23 | * "tools": { 24 | * "warning": { 25 | * "Title": "Название", 26 | * "Message": "Сообщение", 27 | * }, 28 | * "link": { 29 | * "Add a link": "Вставьте ссылку" 30 | * }, 31 | * }, 32 | */ 33 | tools?: Dictionary; 34 | 35 | /** 36 | * Section allows to translate Block Tunes 37 | * The first-level keys of this object should be equal of 'name' ot the 'tools..tunes' property of EditorConfig 38 | * Including some internal block-tunes: "delete", "moveUp", "moveDown 39 | * 40 | * Example: 41 | * "blockTunes": { 42 | * "delete": { 43 | * "Delete": "Удалить" 44 | * }, 45 | * "moveUp": { 46 | * "Move up": "Переместить вверх" 47 | * }, 48 | * "moveDown": { 49 | * "Move down": "Переместить вниз" 50 | * } 51 | * }, 52 | */ 53 | blockTunes?: Dictionary; 54 | 55 | /** 56 | * Translation of internal UI components of the editor.js core 57 | */ 58 | ui?: Dictionary; 59 | } 60 | 61 | /** 62 | * Represent item of the I18nDictionary config 63 | */ 64 | export interface Dictionary { 65 | /** 66 | * The keys of the object can represent two entities: 67 | * 1. Dictionary key usually is an original string from default locale, like "Convert to" 68 | * 2. Sub-namespace section, like "toolbar.converter.<...>" 69 | * 70 | * Example of 1: 71 | * toolbox: { 72 | * "Add": "Добавить", 73 | * } 74 | * 75 | * Example of 2: 76 | * ui: { 77 | * toolbar: { 78 | * toolbox: { <-- Example of 1 79 | * "Add": "Добавить" 80 | * } 81 | * } 82 | * } 83 | */ 84 | [key: string]: DictValue; 85 | } 86 | 87 | /** 88 | * The value of the dictionary can be: 89 | * - other dictionary 90 | * - result translate string 91 | */ 92 | export type DictValue = {[key: string]: Dictionary | string} | string; 93 | 94 | -------------------------------------------------------------------------------- /src/components/inline-tools/inline-tool-bold.ts: -------------------------------------------------------------------------------- 1 | import $ from '../dom'; 2 | import { InlineTool, SanitizerConfig } from '../../../types'; 3 | 4 | /** 5 | * Bold Tool 6 | * 7 | * Inline Toolbar Tool 8 | * 9 | * Makes selected text bolder 10 | */ 11 | export default class BoldInlineTool implements InlineTool { 12 | /** 13 | * Specifies Tool as Inline Toolbar Tool 14 | * 15 | * @returns {boolean} 16 | */ 17 | public static isInline = true; 18 | 19 | /** 20 | * Title for hover-tooltip 21 | */ 22 | public static title = 'Bold'; 23 | 24 | /** 25 | * Sanitizer Rule 26 | * Leave tags 27 | * 28 | * @returns {object} 29 | */ 30 | public static get sanitize(): SanitizerConfig { 31 | return { 32 | b: {}, 33 | } as SanitizerConfig; 34 | } 35 | 36 | /** 37 | * Native Document's command that uses for Bold 38 | */ 39 | private readonly commandName: string = 'bold'; 40 | 41 | /** 42 | * Styles 43 | */ 44 | private readonly CSS = { 45 | button: 'ce-inline-tool', 46 | buttonActive: 'ce-inline-tool--active', 47 | buttonModifier: 'ce-inline-tool--bold', 48 | }; 49 | 50 | /** 51 | * Elements 52 | */ 53 | private nodes: {button: HTMLButtonElement} = { 54 | button: undefined, 55 | }; 56 | 57 | /** 58 | * Create button for Inline Toolbar 59 | */ 60 | public render(): HTMLElement { 61 | this.nodes.button = document.createElement('button') as HTMLButtonElement; 62 | this.nodes.button.type = 'button'; 63 | this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier); 64 | this.nodes.button.appendChild($.svg('bold', 12, 14)); 65 | 66 | return this.nodes.button; 67 | } 68 | 69 | /** 70 | * Wrap range with tag 71 | * 72 | * @param {Range} range - range to wrap 73 | */ 74 | public surround(range: Range): void { 75 | document.execCommand(this.commandName); 76 | } 77 | 78 | /** 79 | * Check selection and set activated state to button if there are tag 80 | * 81 | * @param {Selection} selection - selection to check 82 | * 83 | * @returns {boolean} 84 | */ 85 | public checkState(selection: Selection): boolean { 86 | const isActive = document.queryCommandState(this.commandName); 87 | 88 | this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive); 89 | 90 | return isActive; 91 | } 92 | 93 | /** 94 | * Set a shortcut 95 | * 96 | * @returns {boolean} 97 | */ 98 | public get shortcut(): string { 99 | return 'CMD+B'; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /example/example-multiple.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | Editor.js 🤩🧦🤨 example: Multiple instances 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 | Plugins 22 | Usage 23 | Configuration 24 | API 25 |
26 |
27 |
28 |
29 | No core bundle file found. Run yarn build 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/styles/animations.css: -------------------------------------------------------------------------------- 1 | .wobble { 2 | animation-name: wobble; 3 | animation-duration: 400ms; 4 | } 5 | 6 | /** 7 | * @author Nick Pettit - https://github.com/nickpettit/glide 8 | */ 9 | @keyframes wobble { 10 | from { 11 | transform: translate3d(0, 0, 0); 12 | } 13 | 14 | 15% { 15 | transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -5deg); 16 | } 17 | 18 | 30% { 19 | transform: translate3d(2%, 0, 0) rotate3d(0, 0, 1, 3deg); 20 | } 21 | 22 | 45% { 23 | transform: translate3d(-3%, 0, 0) rotate3d(0, 0, 1, -3deg); 24 | } 25 | 26 | 60% { 27 | transform: translate3d(2%, 0, 0) rotate3d(0, 0, 1, 2deg); 28 | } 29 | 30 | 75% { 31 | transform: translate3d(-1%, 0, 0) rotate3d(0, 0, 1, -1deg); 32 | } 33 | 34 | to { 35 | transform: translate3d(0, 0, 0); 36 | } 37 | } 38 | 39 | @keyframes bounceIn { 40 | from, 41 | 20%, 42 | 40%, 43 | 60%, 44 | 80%, 45 | to { 46 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 47 | } 48 | 49 | 0% { 50 | transform: scale3d(0.9, 0.9, 0.9); 51 | } 52 | 53 | 20% { 54 | transform: scale3d(1.03, 1.03, 1.03); 55 | } 56 | 57 | 60% { 58 | transform: scale3d(1, 1, 1); 59 | } 60 | } 61 | 62 | @keyframes selectionBounce { 63 | from, 64 | 20%, 65 | 40%, 66 | 60%, 67 | 80%, 68 | to { 69 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 70 | } 71 | 72 | 50% { 73 | transform: scale3d(1.01, 1.01, 1.01); 74 | } 75 | 76 | 70% { 77 | transform: scale3d(1, 1, 1); 78 | } 79 | } 80 | 81 | @keyframes buttonClicked { 82 | from, 83 | 20%, 84 | 40%, 85 | 60%, 86 | 80%, 87 | to { 88 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 89 | } 90 | 91 | 0% { 92 | transform: scale3d(0.95, 0.95, 0.95); 93 | } 94 | 95 | 60% { 96 | transform: scale3d(1.02, 1.02, 1.02); 97 | } 98 | 99 | 80% { 100 | transform: scale3d(1, 1, 1); 101 | } 102 | } 103 | 104 | @keyframes panelShowing { 105 | from { 106 | opacity: 0; 107 | transform: translateY(-8px) scale(0.9); 108 | } 109 | 110 | 70% { 111 | opacity: 1; 112 | transform: translateY(2px); 113 | } 114 | 115 | to { 116 | 117 | transform: translateY(0); 118 | } 119 | } 120 | 121 | @keyframes panelShowingMobile { 122 | from { 123 | opacity: 0; 124 | transform: translateY(14px) scale(0.98); 125 | } 126 | 127 | 70% { 128 | opacity: 1; 129 | transform: translateY(-4px); 130 | } 131 | 132 | to { 133 | 134 | transform: translateY(0); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/components/modules/api/tooltip.ts: -------------------------------------------------------------------------------- 1 | import { Tooltip as ITooltip } from '../../../../types/api'; 2 | import type { TooltipOptions, TooltipContent } from 'codex-tooltip/types'; 3 | import Module from '../../__module'; 4 | import { ModuleConfig } from '../../../types-internal/module-config'; 5 | import Tooltip from '../../utils/tooltip'; 6 | /** 7 | * @class TooltipAPI 8 | * @classdesc Tooltip API 9 | */ 10 | export default class TooltipAPI extends Module { 11 | /** 12 | * Tooltip utility Instance 13 | */ 14 | private tooltip: Tooltip; 15 | /** 16 | * @class 17 | * @param moduleConfiguration - Module Configuration 18 | * @param moduleConfiguration.config - Editor's config 19 | * @param moduleConfiguration.eventsDispatcher - Editor's event dispatcher 20 | */ 21 | constructor({ config, eventsDispatcher }: ModuleConfig) { 22 | super({ 23 | config, 24 | eventsDispatcher, 25 | }); 26 | 27 | this.tooltip = new Tooltip(); 28 | } 29 | 30 | /** 31 | * Destroy Module 32 | */ 33 | public destroy(): void { 34 | this.tooltip.destroy(); 35 | } 36 | 37 | /** 38 | * Available methods 39 | */ 40 | public get methods(): ITooltip { 41 | return { 42 | show: (element: HTMLElement, 43 | content: TooltipContent, 44 | options?: TooltipOptions 45 | ): void => this.show(element, content, options), 46 | hide: (): void => this.hide(), 47 | onHover: (element: HTMLElement, 48 | content: TooltipContent, 49 | options?: TooltipOptions 50 | ): void => this.onHover(element, content, options), 51 | }; 52 | } 53 | 54 | /** 55 | * Method show tooltip on element with passed HTML content 56 | * 57 | * @param {HTMLElement} element - element on which tooltip should be shown 58 | * @param {TooltipContent} content - tooltip content 59 | * @param {TooltipOptions} options - tooltip options 60 | */ 61 | public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void { 62 | this.tooltip.show(element, content, options); 63 | } 64 | 65 | /** 66 | * Method hides tooltip on HTML page 67 | */ 68 | public hide(): void { 69 | this.tooltip.hide(); 70 | } 71 | 72 | /** 73 | * Decorator for showing Tooltip by mouseenter/mouseleave 74 | * 75 | * @param {HTMLElement} element - element on which tooltip should be shown 76 | * @param {TooltipContent} content - tooltip content 77 | * @param {TooltipOptions} options - tooltip options 78 | */ 79 | public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void { 80 | this.tooltip.onHover(element, content, options); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /docs/toolbar-settings.md: -------------------------------------------------------------------------------- 1 | # Editor.js Toolbar Block Settings Module 2 | 3 | Toolbar Module has space for Block settings. Settings divided into: 4 | - space for plugin's settings, that is described by «Plugin»'s Developer 5 | - space for default settings. This option is also can be implemented and expanded 6 | 7 | They difference between zones is that the first option is specified by plugin 8 | and each Block can have different options, when second option is for every Block 9 | regardless to the plugin's option. 10 | 11 | ### Let's look the examples: 12 | 13 | «Plugin»'s Developers need to expand «renderSettings» method that returns HTML. 14 | Every user action will be handled by itself. So, you can easily write 15 | callbacks that switches your content or makes better. For more information 16 | read [Tools](tools.md). 17 | 18 | --- 19 | 20 | «Tune»'s Developers need to implement core-provided interface to develop 21 | tunes that will be appeared in Toolbar default settings zone. 22 | 23 | Tunes must expand two important methods: 24 | - `render()` - returns HTML and it is appended to the default settings zone 25 | - `save()` - extracts important information to be saved 26 | 27 | No restrictions. Handle user action by yourself 28 | 29 | Create Class that implements block-tune.ts 30 | 31 | Your Tune's constructor gets argument as object and it includes: 32 | - {Object} api - object contains public methods from modules. @see [API](api.md) 33 | - {Object} settings - settings contains block default state. 34 | This object could have information about cover, anchor and so on. 35 | 36 | Example on TypeScript: 37 | 38 | ```js 39 | 40 | import IBlockTune from './block-tune'; 41 | 42 | export default class YourCustomTune implements IBlockTune { 43 | 44 | public constructor({api, settings}) { 45 | this.api = api; 46 | this.settings = settings; 47 | } 48 | 49 | render() { 50 | let someHTML = '...'; 51 | return someHTML; 52 | } 53 | 54 | save() { 55 | // Return the important data that needs to be saved 56 | return object 57 | } 58 | 59 | someMethod() { 60 | // moves current block down 61 | this.api.blocks.moveDown(); 62 | } 63 | } 64 | ``` 65 | 66 | Example on ES6 67 | 68 | ```js 69 | export default class YourCustomTune { 70 | 71 | constructor({api, settings}) { 72 | this.api = api; 73 | this.settings = settings; 74 | } 75 | 76 | render() { 77 | let someHTML = '...'; 78 | return someHTML; 79 | } 80 | 81 | save() { 82 | // Return the important data that needs to be saved 83 | return object 84 | } 85 | 86 | someMethod() { 87 | // moves current block down 88 | this.api.blocks.moveDown(); 89 | } 90 | } 91 | ``` 92 | -------------------------------------------------------------------------------- /test/cypress/tests/i18n.spec.ts: -------------------------------------------------------------------------------- 1 | import Header from '@editorjs/header'; 2 | import { ToolboxConfig } from '../../../types'; 3 | 4 | /** 5 | * Tool class allowing to test case when capitalized tool name is used as translation key if toolbox title is missing 6 | */ 7 | class TestTool { 8 | /** 9 | * Returns toolbox config without title 10 | */ 11 | public static get toolbox(): ToolboxConfig { 12 | return { 13 | title: '', 14 | icon: '', 15 | }; 16 | } 17 | } 18 | 19 | describe('Editor i18n', () => { 20 | context('Toolbox', () => { 21 | it('should translate tool title in a toolbox', () => { 22 | if (this && this.editorInstance) { 23 | this.editorInstance.destroy(); 24 | } 25 | const toolNamesDictionary = { 26 | Heading: 'Заголовок', 27 | }; 28 | 29 | cy.createEditor({ 30 | tools: { 31 | header: Header, 32 | }, 33 | i18n: { 34 | messages: { 35 | toolNames: toolNamesDictionary, 36 | }, 37 | }, 38 | }).as('editorInstance'); 39 | 40 | cy.get('[data-cy=editorjs]') 41 | .get('div.ce-block') 42 | .click(); 43 | 44 | cy.get('[data-cy=editorjs]') 45 | .get('div.ce-toolbar__plus') 46 | .click(); 47 | 48 | cy.get('[data-cy=editorjs]') 49 | .get('div.ce-popover__item[data-item-name=header]') 50 | .should('contain.text', toolNamesDictionary.Heading); 51 | }); 52 | 53 | it('should use capitalized tool name as translation key if toolbox title is missing', () => { 54 | if (this && this.editorInstance) { 55 | this.editorInstance.destroy(); 56 | } 57 | const toolNamesDictionary = { 58 | TestTool: 'ТестТул', 59 | }; 60 | 61 | cy.createEditor({ 62 | tools: { 63 | testTool: TestTool, 64 | }, 65 | i18n: { 66 | messages: { 67 | toolNames: toolNamesDictionary, 68 | }, 69 | }, 70 | }).as('editorInstance'); 71 | cy.get('[data-cy=editorjs]') 72 | .get('div.ce-block') 73 | .click(); 74 | 75 | cy.get('[data-cy=editorjs]') 76 | .get('div.ce-toolbar__plus') 77 | .click(); 78 | 79 | cy.get('[data-cy=editorjs]') 80 | .get('div.ce-popover__item[data-item-name=testTool]') 81 | .should('contain.text', toolNamesDictionary.TestTool); 82 | }); 83 | }); 84 | }); -------------------------------------------------------------------------------- /types/configs/editor-config.d.ts: -------------------------------------------------------------------------------- 1 | import {ToolConstructable, ToolSettings} from '../tools'; 2 | import {API, BlockAPI, LogLevels, OutputData} from '../index'; 3 | import {SanitizerConfig} from './sanitizer-config'; 4 | import {I18nConfig} from './i18n-config'; 5 | 6 | export interface EditorConfig { 7 | /** 8 | * Element where Editor will be append 9 | * @deprecated property will be removed in the next major release, use holder instead 10 | */ 11 | holderId?: string | HTMLElement; 12 | 13 | /** 14 | * Element where Editor will be appended 15 | */ 16 | holder?: string | HTMLElement; 17 | 18 | /** 19 | * If true, set caret at the first Block after Editor is ready 20 | */ 21 | autofocus?: boolean; 22 | 23 | /** 24 | * This Tool will be used as default 25 | * Name should be equal to one of Tool`s keys of passed tools 26 | * If not specified, Paragraph Tool will be used 27 | */ 28 | defaultBlock?: string; 29 | 30 | /** 31 | * @deprecated 32 | * This property will be deprecated in the next major release. 33 | * Use the 'defaultBlock' property instead. 34 | */ 35 | initialBlock?: string; 36 | 37 | /** 38 | * First Block placeholder 39 | */ 40 | placeholder?: string|false; 41 | 42 | /** 43 | * Define default sanitizer configuration 44 | * @see {@link sanitizer} 45 | */ 46 | sanitizer?: SanitizerConfig; 47 | 48 | /** 49 | * If true, toolbar won't be shown 50 | */ 51 | hideToolbar?: boolean; 52 | 53 | /** 54 | * Map of Tools to use 55 | */ 56 | tools?: { 57 | [toolName: string]: ToolConstructable|ToolSettings; 58 | } 59 | 60 | /** 61 | * Data to render on Editor start 62 | */ 63 | data?: OutputData; 64 | 65 | /** 66 | * Height of Editor's bottom area that allows to set focus on the last Block 67 | */ 68 | minHeight?: number; 69 | 70 | /** 71 | * Editors log level (how many logs you want to see) 72 | */ 73 | logLevel?: LogLevels; 74 | 75 | /** 76 | * Enable read-only mode 77 | */ 78 | readOnly?: boolean; 79 | 80 | /** 81 | * Internalization config 82 | */ 83 | i18n?: I18nConfig; 84 | 85 | /** 86 | * Fires when Editor is ready to work 87 | */ 88 | onReady?(): void; 89 | 90 | /** 91 | * Fires when something changed in DOM 92 | * @param {API} api - editor.js api 93 | * @param event - custom event describing mutation 94 | */ 95 | onChange?(api: API, event: CustomEvent): void; 96 | 97 | /** 98 | * Defines default toolbar for all tools. 99 | */ 100 | inlineToolbar?: string[]|boolean; 101 | 102 | /** 103 | * Common Block Tunes list. Will be added to all the blocks which do not specify their own 'tunes' set 104 | */ 105 | tunes?: string[]; 106 | } 107 | -------------------------------------------------------------------------------- /.github/workflows/bump-version-on-merge-next.yml: -------------------------------------------------------------------------------- 1 | name: Bump version on merge 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - next 7 | types: [closed] 8 | 9 | jobs: 10 | # If pull request was merged then we should check for a package version update 11 | check-for-no-version-changing: 12 | if: github.event.pull_request.merged == true 13 | runs-on: ubuntu-latest 14 | steps: 15 | # Checkout to target branch 16 | - uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | 20 | # Get package new version name 21 | - name: Get package info 22 | id: packageNew 23 | uses: codex-team/action-nodejs-package-info@v1 24 | 25 | # Checkout to the base commit before merge 26 | - name: Checkout to the base commit before merge 27 | run: git checkout ${{ github.event.pull_request.base.sha }} 28 | 29 | # Get package old version name 30 | - name: Get package info 31 | id: packageOld 32 | uses: codex-team/action-nodejs-package-info@v1 33 | 34 | # Stop workflow and do not bump version if it was changed already 35 | - name: Stop workflow and do not bump version if it was changed already 36 | uses: actions/github-script@v3 37 | if: steps.packageOld.outputs.version != steps.packageNew.outputs.version 38 | with: 39 | script: | 40 | core.setFailed('Version was changed! ${{ steps.packageOld.outputs.version }} -> ${{ steps.packageNew.outputs.version }}') 41 | 42 | bump-version: 43 | needs: check-for-no-version-changing 44 | runs-on: ubuntu-latest 45 | steps: 46 | # Checkout to target branch 47 | - uses: actions/checkout@v2 48 | 49 | # Setup node environment 50 | - uses: actions/setup-node@v1 51 | with: 52 | node-version: 15 53 | registry-url: https://registry.npmjs.org/ 54 | 55 | # Bump version to the next prerelease (patch) with rc suffix 56 | - name: Suggest the new version 57 | run: yarn version --prerelease --preid rc --no-git-tag-version 58 | 59 | # Get package new version name 60 | - name: Get package info 61 | id: package 62 | uses: codex-team/action-nodejs-package-info@v1 63 | 64 | # Create pull request with changes 65 | - name: Create Pull Request 66 | uses: peter-evans/create-pull-request@v3 67 | with: 68 | commit-message: Bump version 69 | committer: github-actions 70 | author: github-actions 71 | branch: auto-bump-version 72 | base: ${{ steps.vars.outputs.base_branch }} 73 | delete-branch: true 74 | title: "Bump version up to ${{ steps.package.outputs.version }}" 75 | body: | 76 | Auto-generated bump version suggestion because of PR: 77 | **${{ github.event.pull_request.title }}** #${{ github.event.pull_request.number }} 78 | -------------------------------------------------------------------------------- /src/components/utils/shortcuts.ts: -------------------------------------------------------------------------------- 1 | import Shortcut from '@codexteam/shortcuts'; 2 | 3 | /** 4 | * Contains keyboard and mouse events binded on each Block by Block Manager 5 | */ 6 | 7 | /** 8 | * ShortcutData interface 9 | * Each shortcut must have name and handler 10 | * `name` is a shortcut, like 'CMD+K', 'CMD+B' etc 11 | * `handler` is a callback 12 | * 13 | * @interface ShortcutData 14 | */ 15 | export interface ShortcutData { 16 | 17 | /** 18 | * Shortcut name 19 | * Ex. CMD+I, CMD+B .... 20 | */ 21 | name: string; 22 | 23 | /** 24 | * Shortcut handler 25 | */ 26 | handler(event): void; 27 | 28 | /** 29 | * Element handler should be added for 30 | */ 31 | on: HTMLElement; 32 | } 33 | 34 | /** 35 | * @class Shortcut 36 | * @classdesc Allows to register new shortcut 37 | * 38 | * Internal Shortcuts Module 39 | */ 40 | class Shortcuts { 41 | /** 42 | * All registered shortcuts 43 | * 44 | * @type {Map} 45 | */ 46 | private registeredShortcuts: Map = new Map(); 47 | 48 | /** 49 | * Register shortcut 50 | * 51 | * @param shortcut - shortcut options 52 | */ 53 | public add(shortcut: ShortcutData): void { 54 | const foundShortcut = this.findShortcut(shortcut.on, shortcut.name); 55 | 56 | if (foundShortcut) { 57 | throw Error( 58 | `Shortcut ${shortcut.name} is already registered for ${shortcut.on}. Please remove it before add a new handler.` 59 | ); 60 | } 61 | 62 | const newShortcut = new Shortcut({ 63 | name: shortcut.name, 64 | on: shortcut.on, 65 | callback: shortcut.handler, 66 | }); 67 | const shortcuts = this.registeredShortcuts.get(shortcut.on) || []; 68 | 69 | this.registeredShortcuts.set(shortcut.on, [...shortcuts, newShortcut]); 70 | } 71 | 72 | /** 73 | * Remove shortcut 74 | * 75 | * @param element - Element shortcut is set for 76 | * @param name - shortcut name 77 | */ 78 | public remove(element: Element, name: string): void { 79 | const shortcut = this.findShortcut(element, name); 80 | 81 | if (!shortcut) { 82 | return; 83 | } 84 | 85 | shortcut.remove(); 86 | 87 | const shortcuts = this.registeredShortcuts.get(element); 88 | 89 | this.registeredShortcuts.set(element, shortcuts.filter(el => el !== shortcut)); 90 | } 91 | 92 | /** 93 | * Get Shortcut instance if exist 94 | * 95 | * @param element - Element shorcut is set for 96 | * @param shortcut - shortcut name 97 | * 98 | * @returns {number} index - shortcut index if exist 99 | */ 100 | private findShortcut(element: Element, shortcut: string): Shortcut | void { 101 | const shortcuts = this.registeredShortcuts.get(element) || []; 102 | 103 | return shortcuts.find(({ name }) => name === shortcut); 104 | } 105 | } 106 | 107 | export default new Shortcuts(); 108 | -------------------------------------------------------------------------------- /src/tools/stub/index.ts: -------------------------------------------------------------------------------- 1 | import $ from '../../components/dom'; 2 | import { API, BlockTool, BlockToolConstructorOptions, BlockToolData } from '../../../types'; 3 | 4 | export interface StubData extends BlockToolData { 5 | title: string; 6 | savedData: BlockToolData; 7 | } 8 | 9 | /** 10 | * This tool will be shown in place of a block without corresponding plugin 11 | * It will store its data inside and pass it back with article saving 12 | */ 13 | export default class Stub implements BlockTool { 14 | /** 15 | * Notify core that tool supports read-only mode 16 | */ 17 | public static isReadOnlySupported = true; 18 | 19 | /** 20 | * Stub styles 21 | * 22 | * @type {{wrapper: string, info: string, title: string, subtitle: string}} 23 | */ 24 | private CSS = { 25 | wrapper: 'ce-stub', 26 | info: 'ce-stub__info', 27 | title: 'ce-stub__title', 28 | subtitle: 'ce-stub__subtitle', 29 | }; 30 | 31 | /** 32 | * Main stub wrapper 33 | */ 34 | private readonly wrapper: HTMLElement; 35 | 36 | /** 37 | * Editor.js API 38 | */ 39 | private readonly api: API; 40 | 41 | /** 42 | * Stub title — tool name 43 | */ 44 | private readonly title: string; 45 | 46 | /** 47 | * Stub hint 48 | */ 49 | private readonly subtitle: string; 50 | 51 | /** 52 | * Original Tool data 53 | */ 54 | private readonly savedData: BlockToolData; 55 | 56 | /** 57 | * @param options - constructor options 58 | * @param options.data - stub tool data 59 | * @param options.api - Editor.js API 60 | */ 61 | constructor({ data, api }: BlockToolConstructorOptions) { 62 | this.api = api; 63 | this.title = data.title || this.api.i18n.t('Error'); 64 | this.subtitle = this.api.i18n.t('The block can not be displayed correctly.'); 65 | this.savedData = data.savedData; 66 | 67 | this.wrapper = this.make(); 68 | } 69 | 70 | /** 71 | * Returns stub holder 72 | * 73 | * @returns {HTMLElement} 74 | */ 75 | public render(): HTMLElement { 76 | return this.wrapper; 77 | } 78 | 79 | /** 80 | * Return original Tool data 81 | * 82 | * @returns {BlockToolData} 83 | */ 84 | public save(): BlockToolData { 85 | return this.savedData; 86 | } 87 | 88 | /** 89 | * Create Tool html markup 90 | * 91 | * @returns {HTMLElement} 92 | */ 93 | private make(): HTMLElement { 94 | const wrapper = $.make('div', this.CSS.wrapper); 95 | const icon = $.svg('sad-face', 52, 52); 96 | const infoContainer = $.make('div', this.CSS.info); 97 | const title = $.make('div', this.CSS.title, { 98 | textContent: this.title, 99 | }); 100 | const subtitle = $.make('div', this.CSS.subtitle, { 101 | textContent: this.subtitle, 102 | }); 103 | 104 | wrapper.appendChild(icon); 105 | 106 | infoContainer.appendChild(title); 107 | infoContainer.appendChild(subtitle); 108 | 109 | wrapper.appendChild(infoContainer); 110 | 111 | return wrapper; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/styles/inline-toolbar.css: -------------------------------------------------------------------------------- 1 | .ce-inline-toolbar { 2 | @apply --overlay-pane; 3 | transform: translateX(-50%) translateY(8px) scale(0.9); 4 | opacity: 0; 5 | visibility: hidden; 6 | transition: transform 150ms ease, opacity 250ms ease; 7 | will-change: transform, opacity; 8 | top: 0; 9 | left: 0; 10 | z-index: 3; 11 | 12 | &--showed { 13 | opacity: 1; 14 | visibility: visible; 15 | transform: translateX(-50%) 16 | } 17 | 18 | &--left-oriented { 19 | transform: translateX(-23px) translateY(8px) scale(0.9); 20 | } 21 | 22 | &--left-oriented&--showed { 23 | transform: translateX(-23px); 24 | } 25 | 26 | &--right-oriented { 27 | transform: translateX(-100%) translateY(8px) scale(0.9); 28 | margin-left: 23px; 29 | } 30 | 31 | &--right-oriented&--showed { 32 | transform: translateX(-100%); 33 | } 34 | 35 | [hidden] { 36 | display: none !important; 37 | } 38 | 39 | &__toggler-and-button-wrapper { 40 | display: flex; 41 | width: 100%; 42 | padding: 0 6px; 43 | } 44 | 45 | &__buttons { 46 | display: flex; 47 | } 48 | 49 | &__actions { 50 | } 51 | 52 | &__dropdown { 53 | display: inline-flex; 54 | height: var(--toolbar-buttons-size); 55 | padding: 0 9px 0 10px; 56 | margin: 0 6px 0 -6px; 57 | align-items: center; 58 | cursor: pointer; 59 | border-right: 1px solid var(--color-gray-border); 60 | 61 | &:hover { 62 | background: var(--bg-light); 63 | } 64 | 65 | &--hidden { 66 | display: none; 67 | } 68 | 69 | &-content{ 70 | display: flex; 71 | font-weight: 500; 72 | font-size: 14px; 73 | 74 | svg { 75 | height: 12px; 76 | } 77 | } 78 | 79 | .icon--toggler-down { 80 | margin-left: 4px; 81 | } 82 | } 83 | 84 | &__shortcut { 85 | opacity: 0.6; 86 | word-spacing: -3px; 87 | margin-top: 3px; 88 | } 89 | } 90 | 91 | .ce-inline-tool { 92 | @apply --toolbar-button; 93 | border-radius: 0; 94 | line-height: normal; 95 | width: auto; 96 | padding: 0 5px !important; 97 | min-width: 24px; 98 | 99 | &:not(:last-of-type) { 100 | margin-right: 2px; 101 | } 102 | 103 | .icon { 104 | height: 12px; 105 | } 106 | 107 | &--link { 108 | .icon--unlink { 109 | display: none; 110 | } 111 | } 112 | 113 | &--unlink { 114 | .icon--link { 115 | display: none; 116 | } 117 | .icon--unlink { 118 | display: inline-block; 119 | margin-bottom: -1px; 120 | } 121 | } 122 | 123 | &-input { 124 | outline: none; 125 | border: 0; 126 | border-radius: 0 0 4px 4px; 127 | margin: 0; 128 | font-size: 13px; 129 | padding: 10px; 130 | width: 100%; 131 | box-sizing: border-box; 132 | display: none; 133 | font-weight: 500; 134 | border-top: 1px solid rgba(201,201,204,.48); 135 | 136 | &::placeholder { 137 | color: var(--grayText); 138 | } 139 | 140 | &--showed { 141 | display: block; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack configuration 3 | * 4 | * @author Codex Team 5 | * @copyright Khaydarov Murod 6 | */ 7 | 'use strict'; 8 | 9 | module.exports = (env, argv) => { 10 | const path = require('path'); 11 | const TerserPlugin = require('terser-webpack-plugin'); 12 | const { LicenseWebpackPlugin } = require('license-webpack-plugin'); 13 | const pkg = require('./package.json'); 14 | 15 | 16 | /** 17 | * Environment 18 | * 19 | * @type {any} 20 | */ 21 | const NODE_ENV = argv.mode || 'development'; 22 | const VERSION = process.env.VERSION || pkg.version; 23 | 24 | /** 25 | * Plugins for bundle 26 | * 27 | * @type {webpack} 28 | */ 29 | const webpack = require('webpack'); 30 | 31 | return { 32 | entry: { 33 | editor: ['@babel/polyfill/noConflict', './src/codex.ts'], 34 | }, 35 | 36 | output: { 37 | path: path.resolve(__dirname, 'dist'), 38 | filename: '[name].js', 39 | library: [ 'EditorJS' ], 40 | libraryTarget: 'umd', 41 | }, 42 | 43 | watchOptions: { 44 | aggregateTimeout: 50, 45 | }, 46 | 47 | /** 48 | * Tell webpack what directories should be searched when resolving modules. 49 | */ 50 | resolve: { 51 | modules: [path.join(__dirname, 'src'), 'node_modules'], 52 | extensions: ['.js', '.ts'], 53 | }, 54 | 55 | plugins: [ 56 | /** Pass variables into modules */ 57 | new webpack.DefinePlugin({ 58 | NODE_ENV: JSON.stringify(NODE_ENV), 59 | VERSION: JSON.stringify(VERSION), 60 | }), 61 | 62 | new webpack.BannerPlugin({ 63 | banner: `Editor.js\n\n@version ${VERSION}\n\n@licence Apache-2.0\n@author CodeX \n\n@uses html-janitor\n@licence Apache-2.0 (https://github.com/guardian/html-janitor/blob/master/LICENSE)`, 64 | }), 65 | 66 | new LicenseWebpackPlugin(), 67 | ], 68 | 69 | module: { 70 | rules: [ 71 | { 72 | test: /\.ts$/, 73 | use: [ 74 | { 75 | loader: 'babel-loader', 76 | options: { 77 | cacheDirectory: true, 78 | }, 79 | }, 80 | { 81 | loader: 'ts-loader', 82 | options: { 83 | configFile: NODE_ENV === 'production' ? 'tsconfig.build.json' : 'tsconfig.json', 84 | }, 85 | }, 86 | ], 87 | }, 88 | { 89 | test: /\.css$/, 90 | exclude: /node_modules/, 91 | use: [ 92 | 'postcss-loader', 93 | ], 94 | }, 95 | { 96 | test: /\.(svg)$/, 97 | use: [ 98 | { 99 | loader: 'raw-loader', 100 | }, 101 | ], 102 | }, 103 | ], 104 | }, 105 | 106 | devtool: NODE_ENV === 'development' ? 'source-map' : false, 107 | 108 | optimization: { 109 | minimizer: [ 110 | new TerserPlugin({ 111 | cache: true, 112 | parallel: true, 113 | }), 114 | ], 115 | }, 116 | }; 117 | }; 118 | -------------------------------------------------------------------------------- /src/styles/ui.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Editor wrapper 3 | */ 4 | .codex-editor { 5 | position: relative; 6 | box-sizing: border-box; 7 | z-index: 1; 8 | 9 | .hide { 10 | display: none; 11 | } 12 | 13 | &__redactor { 14 | &--hidden { 15 | display: none; 16 | } 17 | 18 | /** 19 | * Workaround firefox bug: empty content editable elements has collapsed height 20 | * https://bugzilla.mozilla.org/show_bug.cgi?id=1098151#c18 21 | */ 22 | [contenteditable]:empty::after { 23 | content: "\feff "; 24 | } 25 | } 26 | 27 | /** 28 | * Styles for narrow holder 29 | */ 30 | &--narrow &__redactor { 31 | @media (--not-mobile) { 32 | margin-right: var(--narrow-mode-right-padding); 33 | } 34 | } 35 | 36 | &--narrow&--rtl &__redactor { 37 | @media (--not-mobile) { 38 | margin-left: var(--narrow-mode-right-padding); 39 | margin-right: 0; 40 | } 41 | } 42 | 43 | &--narrow .ce-toolbar__actions { 44 | @media (--not-mobile) { 45 | right: -5px; 46 | } 47 | } 48 | 49 | &__loader { 50 | position: relative; 51 | height: 30vh; 52 | 53 | &::before { 54 | content: ''; 55 | position: absolute; 56 | left: 50%; 57 | top: 50%; 58 | width: 30px; 59 | height: 30px; 60 | margin-top: -15px; 61 | margin-left: -15px; 62 | border-radius: 50%; 63 | border: 2px solid var(--color-gray-border); 64 | border-top-color: transparent; 65 | box-sizing: border-box; 66 | animation: editor-loader-spin 800ms infinite linear; 67 | will-change: transform; 68 | } 69 | } 70 | 71 | &-copyable { 72 | position: absolute; 73 | height: 1px; 74 | width: 1px; 75 | top: -400%; 76 | opacity: 0.001; 77 | } 78 | 79 | &-overlay { 80 | position: fixed; 81 | top: 0px; 82 | left: 0px; 83 | right: 0px; 84 | bottom: 0px; 85 | z-index: 999; 86 | pointer-events: none; 87 | overflow: hidden; 88 | 89 | &__container { 90 | position: relative; 91 | pointer-events: auto; 92 | z-index: 0; 93 | } 94 | 95 | &__rectangle { 96 | position: absolute; 97 | pointer-events: none; 98 | background-color: rgba(46, 170, 220, 0.2); 99 | border: 1px solid transparent; 100 | } 101 | } 102 | 103 | svg { 104 | fill: currentColor; 105 | vertical-align: middle; 106 | max-height: 100%; 107 | } 108 | } 109 | 110 | /** 111 | * Set color for native selection 112 | */ 113 | ::selection{ 114 | background-color: var(--inlineSelectionColor); 115 | } 116 | 117 | .codex-editor--toolbox-opened [contentEditable=true][data-placeholder]:focus::before { 118 | opacity: 0 !important; 119 | } 120 | 121 | @keyframes editor-loader-spin { 122 | 0% { 123 | transform: rotate(0deg); 124 | } 125 | 126 | 100% { 127 | transform: rotate(360deg); 128 | } 129 | } 130 | 131 | .ce-scroll-locked { 132 | overflow: hidden; 133 | } 134 | 135 | .ce-scroll-locked--hard { 136 | overflow: hidden; 137 | top: calc(-1 * var(--window-scroll-offset)); 138 | position: fixed; 139 | width: 100%; 140 | } -------------------------------------------------------------------------------- /src/styles/popover.css: -------------------------------------------------------------------------------- 1 | .ce-popover { 2 | position: absolute; 3 | opacity: 0; 4 | will-change: opacity, transform; 5 | display: flex; 6 | flex-direction: column; 7 | padding: 6px; 8 | min-width: 200px; 9 | overflow: hidden; 10 | box-sizing: border-box; 11 | flex-shrink: 0; 12 | max-height: 0; 13 | pointer-events: none; 14 | 15 | @apply --overlay-pane; 16 | 17 | z-index: 4; 18 | flex-wrap: nowrap; 19 | 20 | &--opened { 21 | opacity: 1; 22 | max-height: 270px; 23 | pointer-events: auto; 24 | animation: panelShowing 100ms ease; 25 | 26 | @media (--mobile) { 27 | animation: panelShowingMobile 250ms ease; 28 | } 29 | } 30 | 31 | &::-webkit-scrollbar { 32 | width: 7px; 33 | } 34 | 35 | &::-webkit-scrollbar-thumb { 36 | box-sizing: border-box; 37 | box-shadow: inset 0 0 2px 2px var(--bg-light); 38 | border: 3px solid transparent; 39 | border-left-width: 0px; 40 | border-top-width: 4px; 41 | border-bottom-width: 4px; 42 | } 43 | 44 | @media (--mobile) { 45 | position: fixed; 46 | max-width: none; 47 | min-width: auto; 48 | left: 5px; 49 | right: 5px; 50 | bottom: calc(5px + env(safe-area-inset-bottom)); 51 | top: auto; 52 | border-radius: 10px; 53 | } 54 | 55 | &__items { 56 | overflow-y: auto; 57 | overscroll-behavior: contain; 58 | 59 | @media (--not-mobile) { 60 | margin-top: 5px; 61 | } 62 | } 63 | 64 | &__item { 65 | @apply --popover-button; 66 | 67 | &--focused { 68 | @apply --button-focused; 69 | } 70 | 71 | &--hidden { 72 | display: none; 73 | } 74 | 75 | &-icon { 76 | @apply --tool-icon; 77 | } 78 | 79 | &-label { 80 | &::after { 81 | content: ''; 82 | width: 25px; 83 | display: inline-block; 84 | } 85 | } 86 | 87 | &-secondary-label { 88 | color: var(--grayText); 89 | font-size: 12px; 90 | margin-left: auto; 91 | white-space: nowrap; 92 | letter-spacing: -0.1em; 93 | padding-right: 5px; 94 | margin-bottom: -2px; 95 | opacity: 0.6; 96 | 97 | @media (--mobile){ 98 | display: none; 99 | } 100 | } 101 | } 102 | 103 | &__no-found { 104 | @apply --popover-button; 105 | 106 | color: var(--grayText); 107 | display: none; 108 | cursor: default; 109 | 110 | &--shown { 111 | display: block; 112 | } 113 | 114 | &:hover { 115 | background-color: transparent; 116 | } 117 | } 118 | 119 | @media (--mobile) { 120 | &__overlay { 121 | position: fixed; 122 | top: 0; 123 | bottom: 0; 124 | left: 0; 125 | right: 0; 126 | background: var(--color-dark); 127 | opacity: 0.5; 128 | z-index: 3; 129 | transition: opacity 0.12s ease-in; 130 | will-change: opacity; 131 | visibility: visible; 132 | } 133 | 134 | .cdx-search-field { 135 | display: none; 136 | } 137 | } 138 | 139 | &__overlay--hidden { 140 | z-index: 0; 141 | opacity: 0; 142 | visibility: hidden; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/components/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import defaultDictionary from './locales/en/messages.json'; 2 | import { I18nDictionary, Dictionary } from '../../../types/configs'; 3 | import { LeavesDictKeys } from '../../types-internal/i18n-internal-namespace'; 4 | 5 | /** 6 | * Type for all available internal dictionary strings 7 | */ 8 | type DictKeys = LeavesDictKeys; 9 | 10 | /** 11 | * This class will responsible for the translation through the language dictionary 12 | */ 13 | export default class I18n { 14 | /** 15 | * Property that stores messages dictionary 16 | */ 17 | private static currentDictionary: I18nDictionary = defaultDictionary; 18 | 19 | /** 20 | * Type-safe translation for internal UI texts: 21 | * Perform translation of the string by namespace and a key 22 | * 23 | * @example I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune') 24 | * 25 | * @param internalNamespace - path to translated string in dictionary 26 | * @param dictKey - dictionary key. Better to use default locale original text 27 | */ 28 | public static ui(internalNamespace: string, dictKey: DictKeys): string { 29 | return I18n._t(internalNamespace, dictKey); 30 | } 31 | 32 | /** 33 | * Translate for external strings that is not presented in default dictionary. 34 | * For example, for user-specified tool names 35 | * 36 | * @param namespace - path to translated string in dictionary 37 | * @param dictKey - dictionary key. Better to use default locale original text 38 | */ 39 | public static t(namespace: string, dictKey: string): string { 40 | return I18n._t(namespace, dictKey); 41 | } 42 | 43 | /** 44 | * Adjust module for using external dictionary 45 | * 46 | * @param dictionary - new messages list to override default 47 | */ 48 | public static setDictionary(dictionary: I18nDictionary): void { 49 | I18n.currentDictionary = dictionary; 50 | } 51 | 52 | /** 53 | * Perform translation both for internal and external namespaces 54 | * If there is no translation found, returns passed key as a translated message 55 | * 56 | * @param namespace - path to translated string in dictionary 57 | * @param dictKey - dictionary key. Better to use default locale original text 58 | */ 59 | private static _t(namespace: string, dictKey: string): string { 60 | const section = I18n.getNamespace(namespace); 61 | 62 | /** 63 | * For Console Message to Check Section is defined or not 64 | * if (section === undefined) { 65 | * _.logLabeled('I18n: section %o was not found in current dictionary', 'log', namespace); 66 | * } 67 | */ 68 | 69 | if (!section || !section[dictKey]) { 70 | return dictKey; 71 | } 72 | 73 | return section[dictKey] as string; 74 | } 75 | 76 | /** 77 | * Find messages section by namespace path 78 | * 79 | * @param namespace - path to section 80 | */ 81 | private static getNamespace(namespace: string): Dictionary { 82 | const parts = namespace.split('.'); 83 | 84 | return parts.reduce((section, part) => { 85 | if (!section || !Object.keys(section).length) { 86 | return {}; 87 | } 88 | 89 | return section[part]; 90 | }, I18n.currentDictionary); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/types-internal/editor-modules.d.ts: -------------------------------------------------------------------------------- 1 | import UI from '../components/modules/ui'; 2 | import BlockEvents from '../components/modules/blockEvents'; 3 | import Toolbar from '../components/modules/toolbar/index'; 4 | import InlineToolbar from '../components/modules/toolbar/inline'; 5 | import BlockSettings from '../components/modules/toolbar/blockSettings'; 6 | import Paste from '../components/modules/paste'; 7 | import DragNDrop from '../components/modules/dragNDrop'; 8 | import Renderer from '../components/modules/renderer'; 9 | import Tools from '../components/modules/tools'; 10 | import API from '../components/modules/api/index'; 11 | import Caret from '../components/modules/caret'; 12 | import BlockManager from '../components/modules/blockManager'; 13 | import BlocksAPI from '../components/modules/api/blocks'; 14 | import CaretAPI from '../components/modules/api/caret'; 15 | import EventsAPI from '../components/modules/api/events'; 16 | import ListenersAPI from '../components/modules/api/listeners'; 17 | import SanitizerAPI from '../components/modules/api/sanitizer'; 18 | import ToolbarAPI from '../components/modules/api/toolbar'; 19 | import StylesAPI from '../components/modules/api/styles'; 20 | import SelectionAPI from '../components/modules/api/selection'; 21 | import NotifierAPI from '../components/modules/api/notifier'; 22 | import SaverAPI from '../components/modules/api/saver'; 23 | import Saver from '../components/modules/saver'; 24 | import BlockSelection from '../components/modules/blockSelection'; 25 | import RectangleSelection from '../components/modules/RectangleSelection'; 26 | import InlineToolbarAPI from '../components/modules/api/inlineToolbar'; 27 | import CrossBlockSelection from '../components/modules/crossBlockSelection'; 28 | import ConversionToolbar from '../components/modules/toolbar/conversion'; 29 | import TooltipAPI from '../components/modules/api/tooltip'; 30 | import ReadOnly from '../components/modules/readonly'; 31 | import ReadOnlyAPI from '../components/modules/api/readonly'; 32 | import I18nAPI from '../components/modules/api/i18n'; 33 | import UiAPI from '../components/modules/api/ui'; 34 | import ModificationsObserver from '../components/modules/modificationsObserver'; 35 | 36 | export interface EditorModules { 37 | UI: UI; 38 | BlockEvents: BlockEvents; 39 | BlockSelection: BlockSelection; 40 | RectangleSelection: RectangleSelection; 41 | Toolbar: Toolbar; 42 | InlineToolbar: InlineToolbar; 43 | BlockSettings: BlockSettings; 44 | ConversionToolbar: ConversionToolbar; 45 | Paste: Paste; 46 | DragNDrop: DragNDrop; 47 | Renderer: Renderer; 48 | Tools: Tools; 49 | API: API; 50 | Caret: Caret; 51 | Saver: Saver; 52 | BlockManager: BlockManager; 53 | BlocksAPI: BlocksAPI; 54 | CaretAPI: CaretAPI; 55 | EventsAPI: EventsAPI; 56 | ListenersAPI: ListenersAPI; 57 | SanitizerAPI: SanitizerAPI; 58 | SaverAPI: SaverAPI; 59 | SelectionAPI: SelectionAPI; 60 | StylesAPI: StylesAPI; 61 | ToolbarAPI: ToolbarAPI; 62 | InlineToolbarAPI: InlineToolbarAPI; 63 | CrossBlockSelection: CrossBlockSelection; 64 | NotifierAPI: NotifierAPI; 65 | TooltipAPI: TooltipAPI; 66 | ReadOnly: ReadOnly; 67 | ReadOnlyAPI: ReadOnlyAPI; 68 | I18nAPI: I18nAPI; 69 | UiAPI: UiAPI; 70 | ModificationsObserver: ModificationsObserver; 71 | } 72 | -------------------------------------------------------------------------------- /src/components/block-tunes/block-tune-move-down.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @class MoveDownTune 3 | * @classdesc Editor's default tune - Moves down highlighted block 4 | * 5 | * @copyright 2018 6 | */ 7 | 8 | import $ from '../dom'; 9 | import { API, BlockTune } from '../../../types'; 10 | 11 | /** 12 | * 13 | */ 14 | export default class MoveDownTune implements BlockTune { 15 | /** 16 | * Set Tool is Tune 17 | */ 18 | public static readonly isTune = true; 19 | 20 | /** 21 | * Property that contains Editor.js API methods 22 | * 23 | * @see {@link docs/api.md} 24 | */ 25 | private readonly api: API; 26 | 27 | /** 28 | * Styles 29 | * 30 | * @type {{wrapper: string}} 31 | */ 32 | private CSS = { 33 | button: 'ce-settings__button', 34 | wrapper: 'ce-tune-move-down', 35 | animation: 'wobble', 36 | }; 37 | 38 | /** 39 | * MoveDownTune constructor 40 | * 41 | * @param {API} api — Editor's API 42 | */ 43 | constructor({ api }) { 44 | this.api = api; 45 | } 46 | 47 | /** 48 | * Return 'move down' button 49 | * 50 | * @returns {HTMLElement} 51 | */ 52 | public render(): HTMLElement { 53 | const moveDownButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {}); 54 | 55 | moveDownButton.appendChild($.svg('arrow-down', 14, 14)); 56 | this.api.listeners.on( 57 | moveDownButton, 58 | 'click', 59 | (event) => this.handleClick(event as MouseEvent, moveDownButton), 60 | false 61 | ); 62 | 63 | /** 64 | * Enable tooltip module on button 65 | */ 66 | this.api.tooltip.onHover(moveDownButton, this.api.i18n.t('Move down'), { 67 | hidingDelay: 300, 68 | }); 69 | 70 | return moveDownButton; 71 | } 72 | 73 | /** 74 | * Handle clicks on 'move down' button 75 | * 76 | * @param {MouseEvent} event - click event 77 | * @param {HTMLElement} button - clicked button 78 | */ 79 | public handleClick(event: MouseEvent, button: HTMLElement): void { 80 | const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); 81 | const nextBlock = this.api.blocks.getBlockByIndex(currentBlockIndex + 1); 82 | 83 | // If Block is last do nothing 84 | if (!nextBlock) { 85 | button.classList.add(this.CSS.animation); 86 | 87 | window.setTimeout(() => { 88 | button.classList.remove(this.CSS.animation); 89 | }, 500); 90 | 91 | return; 92 | } 93 | 94 | const nextBlockElement = nextBlock.holder; 95 | const nextBlockCoords = nextBlockElement.getBoundingClientRect(); 96 | 97 | let scrollOffset = Math.abs(window.innerHeight - nextBlockElement.offsetHeight); 98 | 99 | /** 100 | * Next block ends on screen. 101 | * Increment scroll by next block's height to save element onscreen-position 102 | */ 103 | if (nextBlockCoords.top < window.innerHeight) { 104 | scrollOffset = window.scrollY + nextBlockElement.offsetHeight; 105 | } 106 | 107 | window.scrollTo(0, scrollOffset); 108 | 109 | /** Change blocks positions */ 110 | this.api.blocks.move(currentBlockIndex + 1); 111 | 112 | this.api.toolbar.toggleBlockSettings(true); 113 | 114 | /** Hide the Tooltip */ 115 | this.api.tooltip.hide(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/components/modules/renderer.ts: -------------------------------------------------------------------------------- 1 | import Module from '../__module'; 2 | import * as _ from '../utils'; 3 | import { OutputBlockData } from '../../../types'; 4 | import BlockTool from '../tools/block'; 5 | 6 | /** 7 | * Editor.js Renderer Module 8 | * 9 | * @module Renderer 10 | * @author CodeX Team 11 | * 12 | * @version 2.0.0 13 | */ 14 | export default class Renderer extends Module { 15 | /** 16 | * @typedef {object} RendererBlocks 17 | * @property {string} type - tool name 18 | * @property {object} data - tool data 19 | */ 20 | 21 | /** 22 | * @example 23 | * 24 | * blocks: [ 25 | * { 26 | * id : 'oDe-EVrGWA', 27 | * type : 'paragraph', 28 | * data : { 29 | * text : 'Hello from Codex!' 30 | * } 31 | * }, 32 | * { 33 | * id : 'Ld5BJjJCHs', 34 | * type : 'paragraph', 35 | * data : { 36 | * text : 'Leave feedback if you like it!' 37 | * } 38 | * }, 39 | * ] 40 | * 41 | */ 42 | 43 | /** 44 | * Make plugin blocks from array of plugin`s data 45 | * 46 | * @param {OutputBlockData[]} blocks - blocks to render 47 | */ 48 | public async render(blocks: OutputBlockData[]): Promise { 49 | const chainData = blocks.map((block) => ({ function: (): Promise => this.insertBlock(block) })); 50 | 51 | /** 52 | * Disable onChange callback on render to not to spam those events 53 | */ 54 | this.Editor.ModificationsObserver.disable(); 55 | 56 | const sequence = await _.sequence(chainData as _.ChainData[]); 57 | 58 | this.Editor.ModificationsObserver.enable(); 59 | 60 | this.Editor.UI.checkEmptiness(); 61 | 62 | return sequence; 63 | } 64 | 65 | /** 66 | * Get plugin instance 67 | * Add plugin instance to BlockManager 68 | * Insert block to working zone 69 | * 70 | * @param {object} item - Block data to insert 71 | * 72 | * @returns {Promise} 73 | */ 74 | public async insertBlock(item: OutputBlockData): Promise { 75 | const { Tools, BlockManager } = this.Editor; 76 | const { type: tool, data, tunes, id } = item; 77 | 78 | if (Tools.available.has(tool)) { 79 | try { 80 | BlockManager.insert({ 81 | id, 82 | tool, 83 | data, 84 | tunes, 85 | }); 86 | } catch (error) { 87 | _.log(`Block «${tool}» skipped because of plugins error`, 'warn', data); 88 | throw Error(error); 89 | } 90 | } else { 91 | /** If Tool is unavailable, create stub Block for it */ 92 | const stubData = { 93 | savedData: { 94 | id, 95 | type: tool, 96 | data, 97 | }, 98 | title: tool, 99 | }; 100 | 101 | if (Tools.unavailable.has(tool)) { 102 | const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox; 103 | 104 | stubData.title = toolboxSettings?.title || stubData.title; 105 | } 106 | 107 | const stub = BlockManager.insert({ 108 | id, 109 | tool: Tools.stubTool, 110 | data: stubData, 111 | }); 112 | 113 | stub.stretched = true; 114 | 115 | _.log(`Tool «${tool}» is not found. Check 'tools' property at your initial Editor.js config.`, 'warn'); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/components/block/api.ts: -------------------------------------------------------------------------------- 1 | import Block from './index'; 2 | import { BlockToolData, ToolConfig } from '../../../types/tools'; 3 | import { SavedData } from '../../../types/data-formats'; 4 | import { BlockAPI as BlockAPIInterface } from '../../../types/api'; 5 | 6 | /** 7 | * Constructs new BlockAPI object 8 | * 9 | * @class 10 | * 11 | * @param {Block} block - Block to expose 12 | */ 13 | function BlockAPI( 14 | block: Block 15 | ): void { 16 | const blockAPI: BlockAPIInterface = { 17 | /** 18 | * Block id 19 | * 20 | * @returns {string} 21 | */ 22 | get id(): string { 23 | return block.id; 24 | }, 25 | /** 26 | * Tool name 27 | * 28 | * @returns {string} 29 | */ 30 | get name(): string { 31 | return block.name; 32 | }, 33 | 34 | /** 35 | * Tool config passed on Editor's initialization 36 | * 37 | * @returns {ToolConfig} 38 | */ 39 | get config(): ToolConfig { 40 | return block.config; 41 | }, 42 | 43 | /** 44 | * .ce-block element, that wraps plugin contents 45 | * 46 | * @returns {HTMLElement} 47 | */ 48 | get holder(): HTMLElement { 49 | return block.holder; 50 | }, 51 | 52 | /** 53 | * True if Block content is empty 54 | * 55 | * @returns {boolean} 56 | */ 57 | get isEmpty(): boolean { 58 | return block.isEmpty; 59 | }, 60 | 61 | /** 62 | * True if Block is selected with Cross-Block selection 63 | * 64 | * @returns {boolean} 65 | */ 66 | get selected(): boolean { 67 | return block.selected; 68 | }, 69 | 70 | /** 71 | * Set Block's stretch state 72 | * 73 | * @param {boolean} state — state to set 74 | */ 75 | set stretched(state: boolean) { 76 | block.stretched = state; 77 | }, 78 | 79 | /** 80 | * True if Block is stretched 81 | * 82 | * @returns {boolean} 83 | */ 84 | get stretched(): boolean { 85 | return block.stretched; 86 | }, 87 | 88 | /** 89 | * Call Tool method with errors handler under-the-hood 90 | * 91 | * @param {string} methodName - method to call 92 | * @param {object} param - object with parameters 93 | * 94 | * @returns {unknown} 95 | */ 96 | call(methodName: string, param?: object): unknown { 97 | return block.call(methodName, param); 98 | }, 99 | 100 | /** 101 | * Save Block content 102 | * 103 | * @returns {Promise} 104 | */ 105 | save(): Promise { 106 | return block.save(); 107 | }, 108 | 109 | /** 110 | * Validate Block data 111 | * 112 | * @param {BlockToolData} data - data to validate 113 | * 114 | * @returns {Promise} 115 | */ 116 | validate(data: BlockToolData): Promise { 117 | return block.validate(data); 118 | }, 119 | 120 | /** 121 | * Allows to say Editor that Block was changed. Used to manually trigger Editor's 'onChange' callback 122 | * Can be useful for block changes invisible for editor core. 123 | */ 124 | dispatchChange(): void { 125 | block.dispatchChange(); 126 | }, 127 | }; 128 | 129 | Object.setPrototypeOf(this, blockAPI); 130 | } 131 | 132 | export default BlockAPI; 133 | -------------------------------------------------------------------------------- /src/components/modules/readonly.ts: -------------------------------------------------------------------------------- 1 | import Module from '../__module'; 2 | import { CriticalError } from '../errors/critical'; 3 | 4 | /** 5 | * @module ReadOnly 6 | * 7 | * Has one important method: 8 | * - {Function} toggleReadonly - Set read-only mode or toggle current state 9 | * 10 | * @version 1.0.0 11 | * 12 | * @typedef {ReadOnly} ReadOnly 13 | * @property {boolean} readOnlyEnabled - read-only state 14 | */ 15 | export default class ReadOnly extends Module { 16 | /** 17 | * Array of tools name which don't support read-only mode 18 | */ 19 | private toolsDontSupportReadOnly: string[] = []; 20 | 21 | /** 22 | * Value to track read-only state 23 | * 24 | * @type {boolean} 25 | */ 26 | private readOnlyEnabled = false; 27 | 28 | /** 29 | * Returns state of read only mode 30 | */ 31 | public get isEnabled(): boolean { 32 | return this.readOnlyEnabled; 33 | } 34 | 35 | /** 36 | * Set initial state 37 | */ 38 | public async prepare(): Promise { 39 | const { Tools } = this.Editor; 40 | const { blockTools } = Tools; 41 | const toolsDontSupportReadOnly: string[] = []; 42 | 43 | Array 44 | .from(blockTools.entries()) 45 | .forEach(([name, tool]) => { 46 | if (!tool.isReadOnlySupported) { 47 | toolsDontSupportReadOnly.push(name); 48 | } 49 | }); 50 | 51 | this.toolsDontSupportReadOnly = toolsDontSupportReadOnly; 52 | 53 | if (this.config.readOnly && toolsDontSupportReadOnly.length > 0) { 54 | this.throwCriticalError(); 55 | } 56 | 57 | this.toggle(this.config.readOnly); 58 | } 59 | 60 | /** 61 | * Set read-only mode or toggle current state 62 | * Call all Modules `toggleReadOnly` method and re-render Editor 63 | * 64 | * @param {boolean} state - (optional) read-only state or toggle 65 | */ 66 | public async toggle(state = !this.readOnlyEnabled): Promise { 67 | if (state && this.toolsDontSupportReadOnly.length > 0) { 68 | this.throwCriticalError(); 69 | } 70 | 71 | const oldState = this.readOnlyEnabled; 72 | 73 | this.readOnlyEnabled = state; 74 | 75 | for (const name in this.Editor) { 76 | /** 77 | * Verify module has method `toggleReadOnly` method 78 | */ 79 | if (!this.Editor[name].toggleReadOnly) { 80 | continue; 81 | } 82 | 83 | /** 84 | * set or toggle read-only state 85 | */ 86 | this.Editor[name].toggleReadOnly(state); 87 | } 88 | 89 | /** 90 | * If new state equals old one, do not re-render blocks 91 | */ 92 | if (oldState === state) { 93 | return this.readOnlyEnabled; 94 | } 95 | 96 | /** 97 | * Save current Editor Blocks and render again 98 | */ 99 | const savedBlocks = await this.Editor.Saver.save(); 100 | 101 | await this.Editor.BlockManager.clear(); 102 | await this.Editor.Renderer.render(savedBlocks.blocks); 103 | 104 | return this.readOnlyEnabled; 105 | } 106 | 107 | /** 108 | * Throws an error about tools which don't support read-only mode 109 | */ 110 | private throwCriticalError(): never { 111 | throw new CriticalError( 112 | `To enable read-only mode all connected tools should support it. Tools ${this.toolsDontSupportReadOnly.join(', ')} don't support read-only mode.` 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/components/utils/events.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from '../utils'; 2 | 3 | /** 4 | * @class EventDispatcher 5 | * 6 | * Has two important methods: 7 | * - {Function} on - appends subscriber to the event. If event doesn't exist - creates new one 8 | * - {Function} emit - fires all subscribers with data 9 | * - {Function off - unsubscribes callback 10 | * 11 | * @version 1.0.0 12 | * 13 | * @typedef {Events} Events 14 | * @property {object} subscribers - all subscribers grouped by event name 15 | */ 16 | export default class EventsDispatcher { 17 | /** 18 | * Object with events` names as key and array of callback functions as value 19 | * 20 | * @type {{}} 21 | */ 22 | private subscribers: {[name: string]: Array<(data?: object) => unknown>} = {}; 23 | 24 | /** 25 | * Subscribe any event on callback 26 | * 27 | * @param {string} eventName - event name 28 | * @param {Function} callback - subscriber 29 | */ 30 | public on(eventName: Events, callback: (data: object) => unknown): void { 31 | if (!(eventName in this.subscribers)) { 32 | this.subscribers[eventName] = []; 33 | } 34 | 35 | // group by events 36 | this.subscribers[eventName].push(callback); 37 | } 38 | 39 | /** 40 | * Subscribe any event on callback. Callback will be called once and be removed from subscribers array after call. 41 | * 42 | * @param {string} eventName - event name 43 | * @param {Function} callback - subscriber 44 | */ 45 | public once(eventName: Events, callback: (data: object) => unknown): void { 46 | if (!(eventName in this.subscribers)) { 47 | this.subscribers[eventName] = []; 48 | } 49 | 50 | const wrappedCallback = (data: object): unknown => { 51 | const result = callback(data); 52 | 53 | const indexOfHandler = this.subscribers[eventName].indexOf(wrappedCallback); 54 | 55 | if (indexOfHandler !== -1) { 56 | this.subscribers[eventName].splice(indexOfHandler, 1); 57 | } 58 | 59 | return result; 60 | }; 61 | 62 | // group by events 63 | this.subscribers[eventName].push(wrappedCallback); 64 | } 65 | 66 | /** 67 | * Emit callbacks with passed data 68 | * 69 | * @param {string} eventName - event name 70 | * @param {object} data - subscribers get this data when they were fired 71 | */ 72 | public emit(eventName: Events, data?: object): void { 73 | if (isEmpty(this.subscribers) || !this.subscribers[eventName]) { 74 | return; 75 | } 76 | 77 | this.subscribers[eventName].reduce((previousData, currentHandler) => { 78 | const newData = currentHandler(previousData); 79 | 80 | return newData || previousData; 81 | }, data); 82 | } 83 | 84 | /** 85 | * Unsubscribe callback from event 86 | * 87 | * @param {string} eventName - event name 88 | * @param {Function} callback - event handler 89 | */ 90 | public off(eventName: Events, callback: (data: object) => unknown): void { 91 | for (let i = 0; i < this.subscribers[eventName].length; i++) { 92 | if (this.subscribers[eventName][i] === callback) { 93 | delete this.subscribers[eventName][i]; 94 | break; 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * Destroyer 101 | * clears subscribers list 102 | */ 103 | public destroy(): void { 104 | this.subscribers = null; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/cypress/tests/block-ids.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import Header from '@editorjs/header'; 3 | import { nanoid } from 'nanoid'; 4 | 5 | describe.only('Block ids', () => { 6 | beforeEach(() => { 7 | if (this && this.editorInstance) { 8 | this.editorInstance.destroy(); 9 | } else { 10 | cy.createEditor({ 11 | tools: { 12 | header: Header, 13 | }, 14 | }).as('editorInstance'); 15 | } 16 | }); 17 | 18 | it('Should generate unique block ids for new blocks', () => { 19 | cy.get('[data-cy=editorjs]') 20 | .get('div.ce-block') 21 | .click() 22 | .type('First block ') 23 | .type('{enter}') 24 | .get('div.ce-block') 25 | .last() 26 | .type('Second block ') 27 | .type('{enter}'); 28 | 29 | cy.get('[data-cy=editorjs]') 30 | .get('div.ce-toolbar__plus') 31 | .click(); 32 | 33 | cy.get('[data-cy=editorjs]') 34 | .get('div.ce-popover__item[data-item-name=header]') 35 | .click(); 36 | 37 | cy.get('[data-cy=editorjs]') 38 | .get('div.ce-block') 39 | .last() 40 | .click() 41 | .type('Header'); 42 | 43 | cy.get('@editorInstance') 44 | .then(async (editor: any) => { 45 | const data = await editor.save(); 46 | 47 | data.blocks.forEach(block => { 48 | expect(typeof block.id).to.eq('string'); 49 | }); 50 | }); 51 | }); 52 | 53 | it('should preserve passed ids', () => { 54 | const blocks = [ 55 | { 56 | id: nanoid(), 57 | type: 'paragraph', 58 | data: { 59 | text: 'First block', 60 | }, 61 | }, 62 | { 63 | id: nanoid(), 64 | type: 'paragraph', 65 | data: { 66 | text: 'Second block', 67 | }, 68 | }, 69 | ]; 70 | 71 | cy.get('@editorInstance') 72 | .render({ 73 | blocks, 74 | }); 75 | 76 | cy.get('[data-cy=editorjs]') 77 | .get('div.ce-block') 78 | .first() 79 | .click() 80 | .type('{movetoend} Some more text'); 81 | 82 | cy.get('@editorInstance') 83 | .then(async (editor: any) => { 84 | const data = await editor.save(); 85 | 86 | data.blocks.forEach((block, index) => { 87 | expect(block.id).to.eq(blocks[index].id); 88 | }); 89 | }); 90 | }); 91 | 92 | it('should preserve passed ids if blocks were added', () => { 93 | const blocks = [ 94 | { 95 | id: nanoid(), 96 | type: 'paragraph', 97 | data: { 98 | text: 'First block', 99 | }, 100 | }, 101 | { 102 | id: nanoid(), 103 | type: 'paragraph', 104 | data: { 105 | text: 'Second block', 106 | }, 107 | }, 108 | ]; 109 | 110 | cy.get('@editorInstance') 111 | .render({ 112 | blocks, 113 | }); 114 | 115 | cy.get('[data-cy=editorjs]') 116 | .get('div.ce-block') 117 | .first() 118 | .click() 119 | .type('{enter}') 120 | .next() 121 | .type('Middle block'); 122 | 123 | cy.get('@editorInstance') 124 | .then(async (editor: any) => { 125 | const data = await editor.save(); 126 | 127 | expect(data.blocks[0].id).to.eq(blocks[0].id); 128 | expect(data.blocks[2].id).to.eq(blocks[1].id); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /types/api/blocks.d.ts: -------------------------------------------------------------------------------- 1 | import {OutputData} from '../data-formats/output-data'; 2 | import {BlockToolData, ToolConfig} from '../tools'; 3 | import {BlockAPI} from './block'; 4 | 5 | /** 6 | * Describes methods to manipulate with Editor`s blocks 7 | */ 8 | export interface Blocks { 9 | /** 10 | * Remove all blocks from Editor zone 11 | */ 12 | clear(): void; 13 | 14 | /** 15 | * Render passed data 16 | * 17 | * @param {OutputData} data - saved Block data 18 | * 19 | * @returns {Promise} 20 | */ 21 | render(data: OutputData): Promise; 22 | 23 | /** 24 | * Render passed HTML string 25 | * @param {string} data 26 | * @return {Promise} 27 | */ 28 | renderFromHTML(data: string): Promise; 29 | 30 | /** 31 | * Removes current Block 32 | * @param {number} index - index of a block to delete 33 | */ 34 | delete(index?: number): void; 35 | 36 | /** 37 | * Swaps two Blocks 38 | * @param {number} fromIndex - block to swap 39 | * @param {number} toIndex - block to swap with 40 | * @deprecated — use 'move' instead 41 | */ 42 | swap(fromIndex: number, toIndex: number): void; 43 | 44 | /** 45 | * Moves a block to a new index 46 | * @param {number} toIndex - index where the block is moved to 47 | * @param {number} fromIndex - block to move 48 | */ 49 | move(toIndex: number, fromIndex?: number): void; 50 | 51 | /** 52 | * Returns Block API object by passed Block index 53 | * @param {number} index 54 | */ 55 | getBlockByIndex(index: number): BlockAPI | void; 56 | 57 | /** 58 | * Returns Block API object by passed Block id 59 | * @param id - id of the block 60 | */ 61 | getById(id: string): BlockAPI | null; 62 | 63 | /** 64 | * Returns current Block index 65 | * @returns {number} 66 | */ 67 | getCurrentBlockIndex(): number; 68 | 69 | /** 70 | * Returns the index of Block by id; 71 | */ 72 | getBlockIndex(blockId: string): number; 73 | 74 | /** 75 | * Mark Block as stretched 76 | * @param {number} index - Block to mark 77 | * @param {boolean} status - stretch status 78 | * 79 | * @deprecated Use BlockAPI interface to stretch Blocks 80 | */ 81 | stretchBlock(index: number, status?: boolean): void; 82 | 83 | /** 84 | * Returns Blocks count 85 | * @return {number} 86 | */ 87 | getBlocksCount(): number; 88 | 89 | /** 90 | * Insert new Initial Block after current Block 91 | * 92 | * @deprecated 93 | */ 94 | insertNewBlock(): void; 95 | 96 | /** 97 | * Insert new Block and return inserted Block API 98 | * 99 | * @param {string} type — Tool name 100 | * @param {BlockToolData} data — Tool data to insert 101 | * @param {ToolConfig} config — Tool config 102 | * @param {number?} index — index where to insert new Block 103 | * @param {boolean?} needToFocus - flag to focus inserted Block 104 | * @param {boolean?} replace - should the existed Block on that index be replaced or not 105 | */ 106 | insert( 107 | type?: string, 108 | data?: BlockToolData, 109 | config?: ToolConfig, 110 | index?: number, 111 | needToFocus?: boolean, 112 | replace?: boolean, 113 | ): BlockAPI; 114 | 115 | 116 | /** 117 | * Updates block data by id 118 | * 119 | * @param id - id of the block to update 120 | * @param data - the new data 121 | */ 122 | update(id: string, data: BlockToolData): void; 123 | } 124 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at team@codex.so. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/components/modules/dragNDrop.ts: -------------------------------------------------------------------------------- 1 | import SelectionUtils from '../selection'; 2 | 3 | import Module from '../__module'; 4 | /** 5 | * 6 | */ 7 | export default class DragNDrop extends Module { 8 | /** 9 | * If drag has been started at editor, we save it 10 | * 11 | * @type {boolean} 12 | * @private 13 | */ 14 | private isStartedAtEditor = false; 15 | 16 | /** 17 | * Toggle read-only state 18 | * 19 | * if state is true: 20 | * - disable all drag-n-drop event handlers 21 | * 22 | * if state is false: 23 | * - restore drag-n-drop event handlers 24 | * 25 | * @param {boolean} readOnlyEnabled - "read only" state 26 | */ 27 | public toggleReadOnly(readOnlyEnabled: boolean): void { 28 | if (readOnlyEnabled) { 29 | this.disableModuleBindings(); 30 | } else { 31 | this.enableModuleBindings(); 32 | } 33 | } 34 | 35 | /** 36 | * Add drag events listeners to editor zone 37 | */ 38 | private enableModuleBindings(): void { 39 | const { UI } = this.Editor; 40 | 41 | this.readOnlyMutableListeners.on(UI.nodes.holder, 'drop', async (dropEvent: DragEvent) => { 42 | await this.processDrop(dropEvent); 43 | }, true); 44 | 45 | this.readOnlyMutableListeners.on(UI.nodes.holder, 'dragstart', () => { 46 | this.processDragStart(); 47 | }); 48 | 49 | /** 50 | * Prevent default browser behavior to allow drop on non-contenteditable elements 51 | */ 52 | this.readOnlyMutableListeners.on(UI.nodes.holder, 'dragover', (dragEvent: DragEvent) => { 53 | this.processDragOver(dragEvent); 54 | }, true); 55 | } 56 | 57 | /** 58 | * Unbind drag-n-drop event handlers 59 | */ 60 | private disableModuleBindings(): void { 61 | this.readOnlyMutableListeners.clearAll(); 62 | } 63 | 64 | /** 65 | * Handle drop event 66 | * 67 | * @param {DragEvent} dropEvent - drop event 68 | */ 69 | private async processDrop(dropEvent: DragEvent): Promise { 70 | const { 71 | BlockManager, 72 | Caret, 73 | Paste, 74 | } = this.Editor; 75 | 76 | dropEvent.preventDefault(); 77 | 78 | BlockManager.blocks.forEach((block) => { 79 | block.dropTarget = false; 80 | }); 81 | 82 | if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed && this.isStartedAtEditor) { 83 | document.execCommand('delete'); 84 | } 85 | 86 | this.isStartedAtEditor = false; 87 | 88 | /** 89 | * Try to set current block by drop target. 90 | * If drop target is not part of the Block, set last Block as current. 91 | */ 92 | const targetBlock = BlockManager.setCurrentBlockByChildNode(dropEvent.target as Node); 93 | 94 | if (targetBlock) { 95 | this.Editor.Caret.setToBlock(targetBlock, Caret.positions.END); 96 | } else { 97 | const lastBlock = BlockManager.setCurrentBlockByChildNode(BlockManager.lastBlock.holder); 98 | 99 | this.Editor.Caret.setToBlock(lastBlock, Caret.positions.END); 100 | } 101 | 102 | await Paste.processDataTransfer(dropEvent.dataTransfer, true); 103 | } 104 | 105 | /** 106 | * Handle drag start event 107 | */ 108 | private processDragStart(): void { 109 | if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed) { 110 | this.isStartedAtEditor = true; 111 | } 112 | 113 | this.Editor.InlineToolbar.close(); 114 | } 115 | 116 | /** 117 | * @param {DragEvent} dragEvent - drag event 118 | */ 119 | private processDragOver(dragEvent: DragEvent): void { 120 | dragEvent.preventDefault(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /types/tools/block-tool.d.ts: -------------------------------------------------------------------------------- 1 | import { ConversionConfig, PasteConfig, SanitizerConfig } from '../configs'; 2 | import { BlockToolData } from './block-tool-data'; 3 | import {BaseTool, BaseToolConstructable} from './tool'; 4 | import { ToolConfig } from './tool-config'; 5 | import {API, BlockAPI} from '../index'; 6 | import { PasteEvent } from './paste-events'; 7 | import { MoveEvent } from './hook-events'; 8 | 9 | /** 10 | * Describe Block Tool object 11 | * @see {@link docs/tools.md} 12 | */ 13 | export interface BlockTool extends BaseTool { 14 | /** 15 | * Sanitizer rules description 16 | */ 17 | sanitize?: SanitizerConfig; 18 | 19 | /** 20 | * Process Tool's element in DOM and return raw data 21 | * @param {HTMLElement} block - element created by {@link BlockTool#render} function 22 | * @return {BlockToolData} 23 | */ 24 | save(block: HTMLElement): BlockToolData; 25 | 26 | /** 27 | * Create Block's settings block 28 | * @return {HTMLElement} 29 | */ 30 | renderSettings?(): HTMLElement; 31 | 32 | /** 33 | * Validate Block's data 34 | * @param {BlockToolData} blockData 35 | * @return {boolean} 36 | */ 37 | validate?(blockData: BlockToolData): boolean; 38 | 39 | /** 40 | * Method that specified how to merge two Blocks with same type. 41 | * Called by backspace at the beginning of the Block 42 | * @param {BlockToolData} blockData 43 | */ 44 | merge?(blockData: BlockToolData): void; 45 | 46 | /** 47 | * On paste callback. Fired when pasted content can be substituted by a Tool 48 | * @param {PasteEvent} event 49 | */ 50 | onPaste?(event: PasteEvent): void; 51 | 52 | /** 53 | * Cleanup resources used by your tool here 54 | * Called when the editor is destroyed 55 | */ 56 | destroy?(): void; 57 | 58 | /** 59 | * Lifecycle hooks 60 | */ 61 | 62 | /** 63 | * Called after block content added to the page 64 | */ 65 | rendered?(): void; 66 | 67 | /** 68 | * Called each time block content is updated 69 | */ 70 | updated?(): void; 71 | 72 | /** 73 | * Called after block removed from the page but before instance is deleted 74 | */ 75 | removed?(): void; 76 | 77 | /** 78 | * Called after block was moved 79 | */ 80 | moved?(event: MoveEvent): void; 81 | } 82 | 83 | /** 84 | * Describe constructor parameters 85 | */ 86 | export interface BlockToolConstructorOptions { 87 | api: API; 88 | data: BlockToolData; 89 | config?: ToolConfig; 90 | block?: BlockAPI; 91 | readOnly: boolean; 92 | } 93 | 94 | export interface BlockToolConstructable extends BaseToolConstructable { 95 | /** 96 | * Tool's Toolbox settings 97 | */ 98 | toolbox?: { 99 | /** 100 | * HTML string with an icon for Toolbox 101 | */ 102 | icon: string; 103 | 104 | /** 105 | * Tool title for Toolbox 106 | */ 107 | title?: string; 108 | }; 109 | 110 | /** 111 | * Paste substitutions configuration 112 | */ 113 | pasteConfig?: PasteConfig | false; 114 | 115 | /** 116 | * Rules that specified how this Tool can be converted into/from another Tool 117 | */ 118 | conversionConfig?: ConversionConfig; 119 | 120 | /** 121 | * Is Tool supports read-only mode, this property should return true 122 | */ 123 | isReadOnlySupported?: boolean; 124 | 125 | /** 126 | * @constructor 127 | * 128 | * @param {BlockToolConstructorOptions} config - constructor parameters 129 | * 130 | * @return {BlockTool} 131 | */ 132 | new(config: BlockToolConstructorOptions): BlockTool; 133 | } 134 | -------------------------------------------------------------------------------- /src/components/block-tunes/block-tune-delete.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @class DeleteTune 3 | * @classdesc Editor's default tune that moves up selected block 4 | * 5 | * @copyright 2018 6 | */ 7 | import { API, BlockTune } from '../../../types'; 8 | import $ from '../dom'; 9 | 10 | /** 11 | * 12 | */ 13 | export default class DeleteTune implements BlockTune { 14 | /** 15 | * Set Tool is Tune 16 | */ 17 | public static readonly isTune = true; 18 | 19 | /** 20 | * Property that contains Editor.js API methods 21 | * 22 | * @see {@link docs/api.md} 23 | */ 24 | private readonly api: API; 25 | 26 | /** 27 | * Styles 28 | */ 29 | private CSS = { 30 | button: 'ce-settings__button', 31 | buttonDelete: 'ce-settings__button--delete', 32 | buttonConfirm: 'ce-settings__button--confirm', 33 | }; 34 | 35 | /** 36 | * Delete confirmation 37 | */ 38 | private needConfirmation: boolean; 39 | 40 | /** 41 | * set false confirmation state 42 | */ 43 | private readonly resetConfirmation: () => void; 44 | 45 | /** 46 | * Tune nodes 47 | */ 48 | private nodes: {button: HTMLElement} = { 49 | button: null, 50 | }; 51 | 52 | /** 53 | * DeleteTune constructor 54 | * 55 | * @param {API} api - Editor's API 56 | */ 57 | constructor({ api }) { 58 | this.api = api; 59 | 60 | this.resetConfirmation = (): void => { 61 | this.setConfirmation(false); 62 | }; 63 | } 64 | 65 | /** 66 | * Create "Delete" button and add click event listener 67 | * 68 | * @returns {HTMLElement} 69 | */ 70 | public render(): HTMLElement { 71 | this.nodes.button = $.make('div', [this.CSS.button, this.CSS.buttonDelete], {}); 72 | this.nodes.button.appendChild($.svg('cross', 12, 12)); 73 | this.api.listeners.on(this.nodes.button, 'click', (event: MouseEvent) => this.handleClick(event), false); 74 | 75 | /** 76 | * Enable tooltip module 77 | */ 78 | this.api.tooltip.onHover(this.nodes.button, this.api.i18n.t('Delete'), { 79 | hidingDelay: 300, 80 | }); 81 | 82 | return this.nodes.button; 83 | } 84 | 85 | /** 86 | * Delete block conditions passed 87 | * 88 | * @param {MouseEvent} event - click event 89 | */ 90 | public handleClick(event: MouseEvent): void { 91 | /** 92 | * if block is not waiting the confirmation, subscribe on block-settings-closing event to reset 93 | * otherwise delete block 94 | */ 95 | if (!this.needConfirmation) { 96 | this.setConfirmation(true); 97 | 98 | /** 99 | * Subscribe on event. 100 | * When toolbar block settings is closed but block deletion is not confirmed, 101 | * then reset confirmation state 102 | */ 103 | this.api.events.on('block-settings-closed', this.resetConfirmation); 104 | } else { 105 | /** 106 | * Unsubscribe from block-settings closing event 107 | */ 108 | this.api.events.off('block-settings-closed', this.resetConfirmation); 109 | 110 | this.api.blocks.delete(); 111 | this.api.toolbar.close(); 112 | this.api.tooltip.hide(); 113 | 114 | /** 115 | * Prevent firing ui~documentClicked that can drop currentBlock pointer 116 | */ 117 | event.stopPropagation(); 118 | } 119 | } 120 | 121 | /** 122 | * change tune state 123 | * 124 | * @param {boolean} state - delete confirmation state 125 | */ 126 | private setConfirmation(state: boolean): void { 127 | this.needConfirmation = state; 128 | this.nodes.button.classList.add(this.CSS.buttonConfirm); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /devserver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Server for testing example page on mobile devices. 3 | * 4 | * Usage: 5 | * 1. run `yarn devserver:start` 6 | * 2. Open `http://{ip_address}:3000/example/example-dev.html` 7 | * where {ip_address} is IP of your machine. 8 | * 9 | * Also, can serve static files from `/example` or `/dist` on any device in local network. 10 | */ 11 | const path = require('path'); 12 | const fs = require('fs'); 13 | const http = require('http'); 14 | const { networkInterfaces } = require('os'); 15 | 16 | const port = 3000; 17 | const localhost = '127.0.0.1'; 18 | const nonRoutableAddress = '0.0.0.0'; 19 | const host = getHost(); 20 | const server = http.createServer(serveStatic([ 21 | '/example', 22 | '/dist', 23 | ])); 24 | 25 | server.listen(port, nonRoutableAddress, () => { 26 | console.log(` 27 | 28 | ${wrapInColor('Editor.js 💖', consoleColors.hiColor)} devserver is running ᕕ(⌐■_■)ᕗ ✨ 29 | --------------------------------------------- 30 | ${wrapInColor('http://' + host + ':' + port + '/example/example-dev.html', consoleColors.fgGreen)} 31 | --------------------------------------------- 32 | Page can be opened from any device connected to the same local network. 33 | `); 34 | 35 | if (host === localhost) { 36 | console.log(wrapInColor('Looks like you are not connected to any Network so you couldn\'t debug the Editor on your mobile device at the moment.', consoleColors.fgRed)); 37 | } 38 | }); 39 | 40 | /** 41 | * Serves files from specified directories 42 | * 43 | * @param {string[]} paths - directories files from which should be served 44 | * @returns {Function} 45 | */ 46 | function serveStatic(paths) { 47 | return (request, response) => { 48 | const resource = request.url; 49 | const isPathAllowed = paths.find(p => resource.startsWith(p)); 50 | 51 | if (!isPathAllowed) { 52 | response.writeHead(404); 53 | response.end(); 54 | 55 | return; 56 | } 57 | const filePath = path.join(__dirname, resource); 58 | 59 | try { 60 | const stat = fs.statSync(filePath); 61 | 62 | response.writeHead(200, { 63 | 'Content-Length': stat.size, 64 | }); 65 | const readStream = fs.createReadStream(filePath); 66 | 67 | readStream.on('error', e => { 68 | throw e; 69 | }); 70 | readStream.pipe(response); 71 | } catch (e) { 72 | response.writeHead(500); 73 | response.end(e.toString()); 74 | } 75 | }; 76 | } 77 | 78 | /** 79 | * Returns IP address of a machine 80 | * 81 | * @returns {string} 82 | */ 83 | function getHost() { 84 | const nets = networkInterfaces(); 85 | const results = {}; 86 | 87 | for (const name of Object.keys(nets)) { 88 | for (const net of nets[name]) { 89 | // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses 90 | if (net.family === 'IPv4' && !net.internal) { 91 | if (!results[name]) { 92 | results[name] = []; 93 | } 94 | results[name].push(net.address); 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * Offline case 101 | */ 102 | if (Object.keys(results).length === 0) { 103 | return localhost; 104 | } 105 | 106 | return results['en0'][0]; 107 | } 108 | 109 | /** 110 | * Terminal output colors 111 | */ 112 | const consoleColors = { 113 | fgMagenta: 35, 114 | fgRed: 31, 115 | fgGreen: 32, 116 | hiColor: 1, 117 | }; 118 | 119 | /** 120 | * Set a terminal color to the message 121 | * 122 | * @param {string} msg - text to wrap 123 | * @param {string} color - color 124 | * @returns {string} 125 | */ 126 | function wrapInColor(msg, color) { 127 | return '\x1b[' + color + 'm' + msg + '\x1b[0m'; 128 | } 129 | --------------------------------------------------------------------------------