├── CODEOWNERS ├── 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 │ ├── tool-settings.d.ts │ ├── inline-tool.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 │ ├── readonly.d.ts │ ├── saver.d.ts │ ├── sanitizer.d.ts │ ├── notifier.d.ts │ ├── toolbar.d.ts │ ├── index.d.ts │ ├── selection.d.ts │ ├── events.d.ts │ ├── listeners.d.ts │ ├── styles.d.ts │ ├── tooltip.d.ts │ ├── block.d.ts │ ├── caret.d.ts │ └── blocks.d.ts └── index.d.ts ├── .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 │ │ ├── tooltip.ts │ │ ├── shortcuts.ts │ │ └── events.ts │ ├── modules │ │ ├── api │ │ │ ├── readonly.ts │ │ │ ├── inlineToolbar.ts │ │ │ ├── styles.ts │ │ │ ├── saver.ts │ │ │ ├── sanitizer.ts │ │ │ ├── selection.ts │ │ │ ├── i18n.ts │ │ │ ├── events.ts │ │ │ ├── notifier.ts │ │ │ ├── listeners.ts │ │ │ ├── index.ts │ │ │ ├── toolbar.ts │ │ │ └── tooltip.ts │ │ ├── renderer.ts │ │ ├── readonly.ts │ │ └── dragNDrop.ts │ ├── i18n │ │ ├── locales │ │ │ └── en │ │ │ │ └── messages.json │ │ ├── namespace-internal.ts │ │ └── index.ts │ ├── tools │ │ ├── inline.ts │ │ ├── tune.ts │ │ ├── collection.ts │ │ └── factory.ts │ ├── inline-tools │ │ ├── inline-tool-italic.ts │ │ └── inline-tool-bold.ts │ ├── block │ │ └── api.ts │ ├── block-tunes │ │ ├── block-tune-move-down.ts │ │ ├── block-tune-delete.ts │ │ └── block-tune-move-up.ts │ └── polyfills.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 │ ├── dots.svg │ ├── toggler-down.svg │ ├── plus.svg │ ├── arrow-up.svg │ ├── arrow-down.svg │ ├── unlink.svg │ ├── cross.svg │ ├── italic.svg │ ├── sad-face.svg │ └── bold.svg ├── styles │ ├── main.css │ ├── stub.css │ ├── toolbox.css │ ├── rtl.css │ ├── settings.css │ ├── block.css │ ├── conversion-toolbar.css │ ├── animations.css │ ├── export.css │ ├── toolbar.css │ ├── ui.css │ ├── inline-toolbar.css │ └── variables.css ├── tools │ └── stub │ │ └── index.ts └── codex.ts ├── .editorconfig ├── test └── cypress │ ├── fixtures │ └── test.html │ ├── tsconfig.json │ ├── .eslintrc │ ├── support │ ├── index.ts │ └── index.d.ts │ ├── tests │ ├── initialization.spec.ts │ ├── api │ │ └── blocks.spec.ts │ ├── tools │ │ └── ToolsFactory.spec.ts │ ├── sanitisation.spec.ts │ └── block-ids.spec.ts │ └── plugins │ └── index.ts ├── .gitignore ├── .npmignore ├── cypress.json ├── .babelrc ├── tsconfig.json ├── docs ├── sanitizer.md ├── caret.md ├── toolbar-settings.md └── usage.md ├── tslint.json ├── .eslintrc ├── .postcssrc.yml ├── .gitmodules ├── webpack.config.js └── package.json /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @neSpecc @gohabereg @khaydarov 2 | -------------------------------------------------------------------------------- /types/block-tunes/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './block-tune'; 2 | -------------------------------------------------------------------------------- /types/block-tunes/block-tune-data.d.ts: -------------------------------------------------------------------------------- 1 | export type BlockTuneData = any; 2 | -------------------------------------------------------------------------------- /.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/djyde/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/dots.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 | -------------------------------------------------------------------------------- /src/assets/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | return { 14 | toggle: (state): Promise => this.toggle(state), 15 | }; 16 | } 17 | 18 | /** 19 | * Set or toggle read-only state 20 | * 21 | * @param {boolean|undefined} state - set or toggle state 22 | * 23 | * @returns {boolean} current value 24 | */ 25 | public toggle(state?: boolean): Promise { 26 | return this.Editor.ReadOnly.toggle(state); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/assets/bold.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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 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): void; 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 | -------------------------------------------------------------------------------- /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/styles/toolbox.css: -------------------------------------------------------------------------------- 1 | .ce-toolbox { 2 | position: absolute; 3 | visibility: hidden; 4 | transition: opacity 100ms ease; 5 | will-change: opacity; 6 | display: flex; 7 | flex-direction: row; 8 | 9 | @media (--mobile){ 10 | position: static; 11 | transform: none !important; 12 | align-items: center; 13 | overflow-x: auto; 14 | } 15 | 16 | &--opened { 17 | opacity: 1; 18 | visibility: visible; 19 | } 20 | 21 | &__button { 22 | @apply --toolbox-button; 23 | flex-shrink: 0; 24 | } 25 | } 26 | 27 | .ce-toolbox-button-tooltip { 28 | &__shortcut { 29 | opacity: 0.6; 30 | word-spacing: -3px; 31 | margin-top: 3px; 32 | } 33 | } 34 | 35 | /** 36 | * Styles for Narrow mode 37 | */ 38 | .codex-editor--narrow .ce-toolbox { 39 | @media (--not-mobile) { 40 | background: #fff; 41 | z-index: 2; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } 18 | } 19 | }, 20 | "toolNames": { 21 | "Text": "", 22 | "Link": "", 23 | "Bold": "", 24 | "Italic": "" 25 | }, 26 | "tools": { 27 | "link": { 28 | "Add a link": "" 29 | }, 30 | "stub": { 31 | "The block can not be displayed correctly.": "" 32 | } 33 | }, 34 | "blockTunes": { 35 | "delete": { 36 | "Delete": "" 37 | }, 38 | "moveUp": { 39 | "Move up": "" 40 | }, 41 | "moveDown": { 42 | "Move down": "" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 pull_tools 12 | - uses: cypress-io/github-action@v2 13 | with: 14 | browser: firefox 15 | build: yarn build 16 | chrome: 17 | runs-on: ubuntu-16.04 18 | steps: 19 | - uses: actions/checkout@v2 20 | - run: yarn pull_tools 21 | - uses: cypress-io/github-action@v2 22 | with: 23 | browser: chrome 24 | build: yarn build 25 | edge: 26 | runs-on: windows-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - run: yarn pull_tools 30 | - uses: cypress-io/github-action@v2 31 | with: 32 | browser: edge 33 | build: yarn build 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 {Sanitizer} 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 | -------------------------------------------------------------------------------- /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/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 | settings: this.settings, 32 | block, 33 | data, 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/cypress/plugins/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-var-requires */ 2 | /** 3 | * This file contains connection of Cypres plugins 4 | */ 5 | const webpackConfig = require('../../../webpack.config.js'); 6 | const preprocessor = require('@cypress/webpack-preprocessor'); 7 | const codeCoverageTask = require('@cypress/code-coverage/task'); 8 | 9 | module.exports = (on, config): any => { 10 | /** 11 | * Add Cypress task to get code coverage 12 | */ 13 | codeCoverageTask(on, config); 14 | 15 | /** 16 | * Prepare webpack preprocessor options 17 | */ 18 | const options = preprocessor.defaultOptions; 19 | 20 | /** 21 | * Provide path to typescript package 22 | */ 23 | options.typescript = require.resolve('typescript'); 24 | 25 | /** 26 | * Provide our webpack config 27 | */ 28 | options.webpackOptions = webpackConfig({}, { mode: 'test' }); 29 | 30 | /** 31 | * Register webpack preprocessor 32 | */ 33 | on('file:preprocessor', preprocessor(options)); 34 | 35 | // It's IMPORTANT to return the config object 36 | // with any changed environment variables 37 | return config; 38 | }; 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | "unknown": true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 ToolSettings { 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 | -------------------------------------------------------------------------------- /test/cypress/tests/api/blocks.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * There will be described test cases of 'blocks.*' API 3 | */ 4 | describe('api.blocks', () => { 5 | const firstBlock = { 6 | id: 'bwnFX5LoX7', 7 | type: 'paragraph', 8 | data: { 9 | text: 'The first block content mock.', 10 | }, 11 | }; 12 | const editorDataMock = { 13 | blocks: [ 14 | firstBlock, 15 | ], 16 | }; 17 | 18 | beforeEach(() => { 19 | if (this && this.editorInstance) { 20 | this.editorInstance.destroy(); 21 | } else { 22 | cy.createEditor({ 23 | data: editorDataMock, 24 | }).as('editorInstance'); 25 | } 26 | }); 27 | 28 | /** 29 | * api.blocks.getById(id) 30 | */ 31 | describe('.getById()', () => { 32 | /** 33 | * Check that api.blocks.getByUd(id) returns the Block for existed id 34 | */ 35 | it('should return Block API for existed id', () => { 36 | cy.get('@editorInstance').then(async (editor: any) => { 37 | const block = editor.blocks.getById(firstBlock.id); 38 | 39 | expect(block).not.to.be.undefined; 40 | expect(block.id).to.be.eq(firstBlock.id); 41 | }); 42 | }); 43 | 44 | /** 45 | * Check that api.blocks.getByUd(id) returns null for the not-existed id 46 | */ 47 | it('should return null for not-existed id', () => { 48 | cy.get('@editorInstance').then(async (editor: any) => { 49 | expect(editor.blocks.getById('not-existed-id')).to.be.null; 50 | }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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): void => this.on(element, eventType, handler, useCapture), 17 | off: (element, eventType, handler, useCapture): void => this.off(element, eventType, handler, useCapture), 18 | }; 19 | } 20 | 21 | /** 22 | * adds DOM event listener 23 | * 24 | * @param {HTMLElement} element - Element to set handler to 25 | * @param {string} eventType - event type 26 | * @param {() => void} handler - event handler 27 | * @param {boolean} useCapture - capture event or not 28 | */ 29 | public on(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): void { 30 | this.listeners.on(element, eventType, handler, useCapture); 31 | } 32 | 33 | /** 34 | * Removes DOM listener from element 35 | * 36 | * @param {Element} element - Element to remove handler from 37 | * @param eventType - event type 38 | * @param handler - event handler 39 | * @param {boolean} useCapture - capture event or not 40 | */ 41 | public off(element: Element, eventType: string, handler: () => void, useCapture?: boolean): void { 42 | this.listeners.off(element, eventType, handler, useCapture); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /types/block-tunes/block-tune.d.ts: -------------------------------------------------------------------------------- 1 | import {API, BlockAPI, 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 | * @constructor 46 | * 47 | * @param config - Block Tune config 48 | */ 49 | new(config: { 50 | api: API, 51 | settings?: ToolConfig, 52 | block: BlockAPI, 53 | data: BlockTuneData, 54 | }): BlockTune; 55 | 56 | /** 57 | * Tune`s prepare method. Can be async 58 | * @param data 59 | */ 60 | prepare?(): Promise | void; 61 | 62 | /** 63 | * Tune`s reset method to clean up anything set by prepare. Can be async 64 | */ 65 | reset?(): void | Promise; 66 | } 67 | -------------------------------------------------------------------------------- /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/utils/tooltip.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/no-undefined-types */ 2 | /** 3 | * Use external module CodeX Tooltip 4 | */ 5 | import CodeXTooltips, { TooltipContent, TooltipOptions } from 'codex-tooltip'; 6 | 7 | /** 8 | * Tooltip 9 | * 10 | * Decorates any tooltip module like adapter 11 | */ 12 | export default class Tooltip { 13 | /** 14 | * Tooltips lib: CodeX Tooltips 15 | * 16 | * @see https://github.com/codex-team/codex.tooltips 17 | */ 18 | private lib: CodeXTooltips = new CodeXTooltips(); 19 | 20 | /** 21 | * Release the library 22 | */ 23 | public destroy(): void { 24 | this.lib.destroy(); 25 | } 26 | 27 | /** 28 | * Shows tooltip on element with passed HTML content 29 | * 30 | * @param {HTMLElement} element - any HTML element in DOM 31 | * @param {TooltipContent} content - tooltip's content 32 | * @param {TooltipOptions} options - showing settings 33 | */ 34 | public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void { 35 | this.lib.show(element, content, options); 36 | } 37 | 38 | /** 39 | * Hides tooltip 40 | */ 41 | public hide(): void { 42 | this.lib.hide(); 43 | } 44 | 45 | /** 46 | * Binds 'mouseenter' and 'mouseleave' events that shows/hides the Tooltip 47 | * 48 | * @param {HTMLElement} element - any HTML element in DOM 49 | * @param {TooltipContent} content - tooltip's content 50 | * @param {TooltipOptions} options - showing settings 51 | */ 52 | public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void { 53 | this.lib.onHover(element, content, options); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /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 | }; 36 | } 37 | 38 | /** 39 | * Returns Editor.js Core API methods for passed tool 40 | * 41 | * @param tool - tool object 42 | */ 43 | public getMethodsForTool(tool: ToolClass): APIInterfaces { 44 | return Object.assign( 45 | this.methods, 46 | { 47 | i18n: this.Editor.I18nAPI.getMethodsForTool(tool), 48 | } 49 | ) as APIInterfaces; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/styles/settings.css: -------------------------------------------------------------------------------- 1 | .ce-settings { 2 | @apply --overlay-pane; 3 | right: -1px; 4 | top: 30px; 5 | min-width: 114px; 6 | box-sizing: content-box; 7 | 8 | @media (--mobile){ 9 | bottom: 40px; 10 | right: -11px; 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 | -------------------------------------------------------------------------------- /test/cypress/tests/tools/ToolsFactory.spec.ts: -------------------------------------------------------------------------------- 1 | import LinkInlineTool from '../../../../src/components/inline-tools/inline-tool-link'; 2 | import MoveUpTune from '../../../../src/components/block-tunes/block-tune-move-up'; 3 | import ToolsFactory from '../../../../src/components/tools/factory'; 4 | import InlineTool from '../../../../src/components/tools/inline'; 5 | import BlockTool from '../../../../src/components/tools/block'; 6 | import BlockTune from '../../../../src/components/tools/tune'; 7 | import Paragraph from '../../../../src/tools/paragraph/dist/bundle'; 8 | 9 | describe('ToolsFactory', (): void => { 10 | let factory; 11 | const config = { 12 | paragraph: { 13 | class: Paragraph, 14 | }, 15 | link: { 16 | class: LinkInlineTool, 17 | }, 18 | moveUp: { 19 | class: MoveUpTune, 20 | }, 21 | }; 22 | 23 | beforeEach((): void => { 24 | factory = new ToolsFactory( 25 | config, 26 | { 27 | placeholder: 'Placeholder', 28 | defaultBlock: 'paragraph', 29 | } as any, 30 | {} as any 31 | ); 32 | }); 33 | 34 | context('.get', (): void => { 35 | it('should return appropriate tool object', (): void => { 36 | const tool = factory.get('link'); 37 | 38 | expect(tool.name).to.be.eq('link'); 39 | }); 40 | 41 | it('should return InlineTool object for inline tool', (): void => { 42 | const tool = factory.get('link'); 43 | 44 | expect(tool instanceof InlineTool).to.be.true; 45 | }); 46 | 47 | it('should return BlockTool object for block tool', (): void => { 48 | const tool = factory.get('paragraph'); 49 | 50 | expect(tool instanceof BlockTool).to.be.true; 51 | }); 52 | 53 | it('should return BlockTune object for tune', (): void => { 54 | const tool = factory.get('moveUp'); 55 | 56 | expect(tool instanceof BlockTune).to.be.true; 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/styles/block.css: -------------------------------------------------------------------------------- 1 | .ce-block { 2 | &:first-of-type { 3 | margin-top: 0; 4 | } 5 | 6 | &--selected &__content { 7 | background: var(--selectionColor); 8 | 9 | /** 10 | * Workaround Safari case when user can select inline-fragment with cross-block-selection 11 | */ 12 | & [contenteditable] { 13 | -webkit-user-select: none; 14 | user-select: none; 15 | } 16 | 17 | img, 18 | .ce-stub { 19 | opacity: 0.55; 20 | } 21 | } 22 | 23 | &--stretched &__content { 24 | max-width: none; 25 | } 26 | 27 | &__content { 28 | position: relative; 29 | max-width: var(--content-width); 30 | margin: 0 auto; 31 | transition: background-color 150ms ease; 32 | } 33 | 34 | &--drop-target &__content { 35 | &:before { 36 | content: ''; 37 | position: absolute; 38 | top: 100%; 39 | left: -20px; 40 | margin-top: -1px; 41 | height: 8px; 42 | width: 8px; 43 | border: solid var(--color-active-icon); 44 | border-width: 1px 1px 0 0; 45 | transform-origin: right; 46 | transform: rotate(45deg); 47 | } 48 | 49 | &:after { 50 | content: ''; 51 | position: absolute; 52 | top: 100%; 53 | height: 1px; 54 | width: 100%; 55 | color: var(--color-active-icon); 56 | background: repeating-linear-gradient( 57 | 90deg, 58 | var(--color-active-icon), 59 | var(--color-active-icon) 1px, 60 | #fff 1px, 61 | #fff 6px 62 | ); 63 | } 64 | } 65 | 66 | a { 67 | cursor: pointer; 68 | text-decoration: underline; 69 | } 70 | 71 | b { 72 | font-weight: bold; 73 | } 74 | 75 | i { 76 | font-style: italic; 77 | } 78 | } 79 | 80 | .codex-editor--narrow .ce-block--focused { 81 | @media (--not-mobile) { 82 | margin-right: calc(var(--narrow-mode-right-padding) * -1); 83 | padding-right: var(--narrow-mode-right-padding); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /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.open(); 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 | /** Check if state same as current state */ 52 | if (openingState === this.Editor.BlockSettings.opened) { 53 | return; 54 | } 55 | 56 | if (canOpenBlockSettings) { 57 | if (!this.Editor.Toolbar.opened) { 58 | this.Editor.Toolbar.open(true, false); 59 | this.Editor.Toolbar.plusButton.hide(); 60 | } 61 | this.Editor.BlockSettings.open(); 62 | } else { 63 | this.Editor.BlockSettings.close(); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /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: yarn 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 | -------------------------------------------------------------------------------- /test/cypress/tests/sanitisation.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Output sanitisation', () => { 2 | beforeEach(() => { 3 | if (this && this.editorInstance) { 4 | this.editorInstance.destroy(); 5 | } else { 6 | cy.createEditor({}).as('editorInstance'); 7 | } 8 | }); 9 | 10 | context('Output should save inline formatting', () => { 11 | it('should save initial formatting for paragraph', () => { 12 | cy.createEditor({ 13 | data: { 14 | blocks: [ { 15 | type: 'paragraph', 16 | data: { text: 'Bold text' }, 17 | } ], 18 | }, 19 | }).then(async editor => { 20 | const output = await (editor as any).save(); 21 | 22 | const boldText = output.blocks[0].data.text; 23 | 24 | expect(boldText).to.eq('Bold text'); 25 | }); 26 | }); 27 | 28 | it('should save formatting for paragraph', () => { 29 | cy.get('[data-cy=editorjs]') 30 | .get('div.ce-block') 31 | .click() 32 | .type('This text should be bold.{selectall}'); 33 | 34 | cy.get('[data-cy=editorjs]') 35 | .get('button.ce-inline-tool--bold') 36 | .click(); 37 | 38 | cy.get('[data-cy=editorjs]') 39 | .get('div.ce-block') 40 | .click(); 41 | 42 | cy.get('@editorInstance').then(async editorInstance => { 43 | const output = await (editorInstance as any).save(); 44 | 45 | const text = output.blocks[0].data.text; 46 | 47 | expect(text).to.match(/This text should be bold\.(
)?<\/b>/); 48 | }); 49 | }); 50 | 51 | it('should save formatting for paragraph on paste', () => { 52 | cy.get('[data-cy=editorjs]') 53 | .get('div.ce-block') 54 | .paste({ 'text/html': '

Text

Bold text

' }); 55 | 56 | cy.get('@editorInstance').then(async editorInstance => { 57 | const output = await (editorInstance as any).save(); 58 | 59 | const boldText = output.blocks[1].data.text; 60 | 61 | expect(boldText).to.eq('Bold text'); 62 | }); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/styles/export.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Block Tool wrapper 3 | */ 4 | .cdx-block { 5 | padding: 0.4em 0; 6 | } 7 | 8 | /** 9 | * Input 10 | */ 11 | .cdx-input { 12 | border: 1px solid var(--color-gray-border); 13 | box-shadow: inset 0 1px 2px 0 rgba(35, 44, 72, 0.06); 14 | border-radius: 3px; 15 | padding: 10px 12px; 16 | outline: none; 17 | width: 100%; 18 | box-sizing: border-box; 19 | 20 | /** 21 | * Workaround Firefox bug with cursor position on empty content editable elements with ::before pseudo 22 | * https://bugzilla.mozilla.org/show_bug.cgi?id=904846 23 | */ 24 | &[data-placeholder]::before { 25 | position: static !important; 26 | display: inline-block; 27 | width: 0; 28 | white-space: nowrap; 29 | pointer-events: none; 30 | } 31 | } 32 | 33 | /** 34 | * Settings 35 | */ 36 | .cdx-settings-button { 37 | @apply --toolbar-button; 38 | 39 | &:not(:nth-child(3n+3)) { 40 | margin-right: 3px; 41 | } 42 | 43 | &:nth-child(n+4) { 44 | margin-top: 3px; 45 | } 46 | 47 | &--active { 48 | color: var(--color-active-icon); 49 | } 50 | } 51 | 52 | /** 53 | * Loader 54 | */ 55 | .cdx-loader { 56 | position: relative; 57 | border: 1px solid var(--color-gray-border); 58 | 59 | &::before { 60 | content: ''; 61 | position: absolute; 62 | left: 50%; 63 | top: 50%; 64 | width: 18px; 65 | height: 18px; 66 | margin: -11px 0 0 -11px; 67 | border: 2px solid var(--color-gray-border); 68 | border-left-color: var(--color-active-icon); 69 | border-radius: 50%; 70 | animation: cdxRotation 1.2s infinite linear; 71 | } 72 | } 73 | 74 | @keyframes cdxRotation { 75 | 0% { 76 | transform: rotate(0deg); 77 | } 78 | 100% { 79 | transform: rotate(360deg); 80 | } 81 | } 82 | 83 | /** 84 | * Button 85 | */ 86 | .cdx-button { 87 | padding: 13px; 88 | border-radius: 3px; 89 | border: 1px solid var(--color-gray-border); 90 | font-size: 14.9px; 91 | background: #fff; 92 | box-shadow: 0 2px 2px 0 rgba(18,30,57,0.04); 93 | color: var(--grayText); 94 | text-align: center; 95 | cursor: pointer; 96 | 97 | &:hover { 98 | background: #FBFCFE; 99 | box-shadow: 0 1px 3px 0 rgba(18,30,57,0.08); 100 | } 101 | 102 | svg { 103 | height: 20px; 104 | margin-right: 0.2em; 105 | margin-top: -2px; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/toolbar.css: -------------------------------------------------------------------------------- 1 | .ce-toolbar { 2 | position: absolute; 3 | left: 0; 4 | right: 0; 5 | top: 0; 6 | /*opacity: 0;*/ 7 | /*visibility: hidden;*/ 8 | transition: opacity 100ms ease; 9 | will-change: opacity, transform; 10 | display: none; 11 | 12 | @media (--mobile) { 13 | @apply --overlay-pane; 14 | padding: 3px; 15 | margin-top: 5px; 16 | } 17 | 18 | &--opened { 19 | display: block; 20 | 21 | @media (--mobile){ 22 | display: flex; 23 | } 24 | } 25 | 26 | &__content { 27 | max-width: var(--content-width); 28 | margin: 0 auto; 29 | position: relative; 30 | 31 | @media (--mobile){ 32 | display: flex; 33 | align-content: center; 34 | margin: 0; 35 | max-width: calc(100% - 35px); 36 | } 37 | } 38 | 39 | &__plus { 40 | @apply --toolbox-button; 41 | 42 | position: absolute; 43 | left: calc(var(--toolbox-buttons-size) * -1); 44 | flex-shrink: 0; 45 | 46 | &-shortcut { 47 | opacity: 0.6; 48 | word-spacing: -2px; 49 | margin-top: 5px; 50 | } 51 | 52 | &--hidden { 53 | display: none; 54 | } 55 | 56 | @media (--mobile){ 57 | display: inline-flex !important; 58 | position: static; 59 | transform: none !important; 60 | } 61 | } 62 | 63 | &__plus, 64 | .ce-toolbox { 65 | top: 50%; 66 | transform: translateY(-50%); 67 | } 68 | 69 | /** 70 | * Block actions Zone 71 | * ------------------------- 72 | */ 73 | &__actions { 74 | position: absolute; 75 | right: -30px; 76 | top: 5px; 77 | opacity: 0; 78 | 79 | @media (--mobile){ 80 | position: absolute; 81 | right: -28px; 82 | top: 50%; 83 | transform: translateY(-50%); 84 | display: flex; 85 | align-items: center; 86 | } 87 | 88 | &--opened { 89 | opacity: 1; 90 | } 91 | 92 | &-buttons { 93 | text-align: right; 94 | } 95 | } 96 | 97 | &__settings-btn { 98 | display: flex; 99 | align-items: center; 100 | justify-content: center; 101 | width: 18px; 102 | height: 18px; 103 | color: var(--grayText); 104 | cursor: pointer; 105 | background: var(--bg-light); 106 | user-select: none; 107 | 108 | &:hover { 109 | color: var(--color-dark); 110 | } 111 | 112 | @media (--mobile){ 113 | background: transparent; 114 | } 115 | } 116 | } 117 | 118 | /** 119 | * Styles for Narrow mode 120 | */ 121 | .codex-editor--narrow .ce-toolbar__plus { 122 | @media (--not-mobile) { 123 | left: 5px; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /types/configs/editor-config.d.ts: -------------------------------------------------------------------------------- 1 | import {ToolConstructable, ToolSettings} from '../tools'; 2 | import {API, 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?: {[toolName: string]: ToolConstructable|ToolSettings}; 57 | 58 | /** 59 | * Data to render on Editor start 60 | */ 61 | data?: OutputData; 62 | 63 | /** 64 | * Height of Editor's bottom area that allows to set focus on the last Block 65 | */ 66 | minHeight?: number; 67 | 68 | /** 69 | * Editors log level (how many logs you want to see) 70 | */ 71 | logLevel?: LogLevels; 72 | 73 | /** 74 | * Enable read-only mode 75 | */ 76 | readOnly?: boolean; 77 | 78 | /** 79 | * Internalization config 80 | */ 81 | i18n?: I18nConfig; 82 | 83 | /** 84 | * Fires when Editor is ready to work 85 | */ 86 | onReady?(): void; 87 | 88 | /** 89 | * Fires when something changed in DOM 90 | * @param {API} api - editor.js api 91 | */ 92 | onChange?(api: API): void; 93 | 94 | /** 95 | * Defines default toolbar for all tools. 96 | */ 97 | inlineToolbar?: string[]|boolean; 98 | 99 | /** 100 | * Common Block Tunes list. Will be added to all the blocks which do not specify their own 'tunes' set 101 | */ 102 | tunes?: string[]; 103 | } 104 | -------------------------------------------------------------------------------- /src/components/modules/api/tooltip.ts: -------------------------------------------------------------------------------- 1 | import { Tooltip as ITooltip } from '../../../../types/api'; 2 | import { TooltipContent, TooltipOptions } from 'codex-tooltip'; 3 | import Module from '../../__module'; 4 | import { ModuleConfig } from '../../../types-internal/module-config'; 5 | import Tooltip from '../../utils/tooltip'; 6 | import EventsDispatcher from '../../utils/events'; 7 | import { EditorConfig } from '../../../../types'; 8 | /** 9 | * @class TooltipAPI 10 | * @classdesc Tooltip API 11 | */ 12 | export default class TooltipAPI extends Module { 13 | /** 14 | * Tooltip utility Instance 15 | */ 16 | private tooltip: Tooltip; 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.tooltip = new Tooltip(); 30 | } 31 | 32 | /** 33 | * Destroy Module 34 | */ 35 | public destroy(): void { 36 | this.tooltip.destroy(); 37 | } 38 | 39 | /** 40 | * Available methods 41 | */ 42 | public get methods(): ITooltip { 43 | return { 44 | show: (element: HTMLElement, 45 | content: TooltipContent, 46 | options?: TooltipOptions 47 | ): void => this.show(element, content, options), 48 | hide: (): void => this.hide(), 49 | onHover: (element: HTMLElement, 50 | content: TooltipContent, 51 | options?: TooltipOptions 52 | ): void => this.onHover(element, content, options), 53 | }; 54 | } 55 | 56 | /** 57 | * Method show tooltip on element with passed HTML content 58 | * 59 | * @param {HTMLElement} element - element on which tooltip should be shown 60 | * @param {TooltipContent} content - tooltip content 61 | * @param {TooltipOptions} options - tooltip options 62 | */ 63 | public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void { 64 | this.tooltip.show(element, content, options); 65 | } 66 | 67 | /** 68 | * Method hides tooltip on HTML page 69 | */ 70 | public hide(): void { 71 | this.tooltip.hide(); 72 | } 73 | 74 | /** 75 | * Decorator for showing Tooltip by mouseenter/mouseleave 76 | * 77 | * @param {HTMLElement} element - element on which tooltip should be shown 78 | * @param {TooltipContent} content - tooltip content 79 | * @param {TooltipOptions} options - tooltip options 80 | */ 81 | public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void { 82 | this.tooltip.onHover(element, content, options); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /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 | * Mark Block as stretched 71 | * @param {number} index - Block to mark 72 | * @param {boolean} status - stretch status 73 | * 74 | * @deprecated Use BlockAPI interface to stretch Blocks 75 | */ 76 | stretchBlock(index: number, status?: boolean): void; 77 | 78 | /** 79 | * Returns Blocks count 80 | * @return {number} 81 | */ 82 | getBlocksCount(): number; 83 | 84 | /** 85 | * Insert new Initial Block after current Block 86 | * 87 | * @deprecated 88 | */ 89 | insertNewBlock(): void; 90 | 91 | /** 92 | * Insert new Block 93 | * 94 | * @param {string} type — Tool name 95 | * @param {BlockToolData} data — Tool data to insert 96 | * @param {ToolConfig} config — Tool config 97 | * @param {number?} index — index where to insert new Block 98 | * @param {boolean?} needToFocus - flag to focus inserted Block 99 | */ 100 | insert( 101 | type?: string, 102 | data?: BlockToolData, 103 | config?: ToolConfig, 104 | index?: number, 105 | needToFocus?: boolean, 106 | ): void; 107 | 108 | } 109 | -------------------------------------------------------------------------------- /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 | * Environment 17 | * 18 | * @type {any} 19 | */ 20 | const NODE_ENV = argv.mode || 'development'; 21 | const VERSION = process.env.VERSION || pkg.version; 22 | 23 | /** 24 | * Plugins for bundle 25 | * 26 | * @type {webpack} 27 | */ 28 | const webpack = require('webpack'); 29 | 30 | return { 31 | entry: { 32 | editor: ['@babel/polyfill/noConflict', './src/codex.ts'], 33 | }, 34 | 35 | output: { 36 | path: path.resolve(__dirname, 'dist'), 37 | filename: '[name].js', 38 | library: [ 'EditorJS' ], 39 | libraryTarget: 'umd', 40 | }, 41 | 42 | watchOptions: { 43 | aggregateTimeout: 50, 44 | }, 45 | 46 | /** 47 | * Tell webpack what directories should be searched when resolving modules. 48 | */ 49 | resolve: { 50 | modules: [path.join(__dirname, 'src'), 'node_modules'], 51 | extensions: ['.js', '.ts'], 52 | }, 53 | 54 | plugins: [ 55 | /** Pass variables into modules */ 56 | new webpack.DefinePlugin({ 57 | NODE_ENV: JSON.stringify(NODE_ENV), 58 | VERSION: JSON.stringify(VERSION), 59 | }), 60 | 61 | new webpack.BannerPlugin({ 62 | 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)`, 63 | }), 64 | 65 | new LicenseWebpackPlugin(), 66 | ], 67 | 68 | module: { 69 | rules: [ 70 | { 71 | test: /\.ts$/, 72 | use: [ 73 | { 74 | loader: 'babel-loader', 75 | options: { 76 | cacheDirectory: true, 77 | }, 78 | }, 79 | { 80 | loader: 'ts-loader', 81 | }, 82 | ], 83 | }, 84 | { 85 | test: /\.css$/, 86 | exclude: /node_modules/, 87 | use: [ 88 | 'postcss-loader', 89 | ], 90 | }, 91 | { 92 | test: /\.(svg)$/, 93 | use: [ 94 | { 95 | loader: 'raw-loader', 96 | }, 97 | ], 98 | }, 99 | ], 100 | }, 101 | 102 | devtool: NODE_ENV === 'development' ? 'source-map' : false, 103 | 104 | optimization: { 105 | minimizer: [ 106 | new TerserPlugin({ 107 | cache: true, 108 | parallel: true, 109 | }), 110 | ], 111 | }, 112 | }; 113 | }; 114 | -------------------------------------------------------------------------------- /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 | 11 | &--showed { 12 | opacity: 1; 13 | visibility: visible; 14 | transform: translateX(-50%) 15 | } 16 | 17 | &--left-oriented { 18 | transform: translateX(-23px) translateY(8px) scale(0.9); 19 | } 20 | 21 | &--left-oriented&--showed { 22 | transform: translateX(-23px); 23 | } 24 | 25 | &--right-oriented { 26 | transform: translateX(-100%) translateY(8px) scale(0.9); 27 | margin-left: 23px; 28 | } 29 | 30 | &--right-oriented&--showed { 31 | transform: translateX(-100%); 32 | } 33 | 34 | [hidden] { 35 | display: none !important; 36 | } 37 | 38 | &__toggler-and-button-wrapper { 39 | display: flex; 40 | width: 100%; 41 | padding: 0 6px; 42 | } 43 | 44 | &__buttons { 45 | display: flex; 46 | } 47 | 48 | &__actions { 49 | } 50 | 51 | &__dropdown { 52 | display: inline-flex; 53 | height: var(--toolbar-buttons-size); 54 | padding: 0 9px 0 10px; 55 | margin: 0 6px 0 -6px; 56 | align-items: center; 57 | cursor: pointer; 58 | border-right: 1px solid var(--color-gray-border); 59 | 60 | &:hover { 61 | background: var(--bg-light); 62 | } 63 | 64 | &--hidden { 65 | display: none; 66 | } 67 | 68 | &-content{ 69 | display: flex; 70 | font-weight: 500; 71 | font-size: 14px; 72 | 73 | svg { 74 | height: 12px; 75 | } 76 | } 77 | 78 | .icon--toggler-down { 79 | margin-left: 4px; 80 | } 81 | } 82 | 83 | &__shortcut { 84 | opacity: 0.6; 85 | word-spacing: -3px; 86 | margin-top: 3px; 87 | } 88 | } 89 | 90 | .ce-inline-tool { 91 | @apply --toolbar-button; 92 | border-radius: 0; 93 | line-height: normal; 94 | width: auto; 95 | padding: 0 5px !important; 96 | min-width: 24px; 97 | 98 | &:not(:last-of-type) { 99 | margin-right: 2px; 100 | } 101 | 102 | .icon { 103 | height: 12px; 104 | } 105 | 106 | &--link { 107 | .icon--unlink { 108 | display: none; 109 | } 110 | } 111 | 112 | &--unlink { 113 | .icon--link { 114 | display: none; 115 | } 116 | .icon--unlink { 117 | display: inline-block; 118 | margin-bottom: -1px; 119 | } 120 | } 121 | 122 | &-input { 123 | outline: none; 124 | border: 0; 125 | border-radius: 0 0 4px 4px; 126 | margin: 0; 127 | font-size: 13px; 128 | padding: 10px; 129 | width: 100%; 130 | box-sizing: border-box; 131 | display: none; 132 | font-weight: 500; 133 | border-top: 1px solid rgba(201,201,204,.48); 134 | 135 | &::placeholder { 136 | color: var(--grayText); 137 | } 138 | 139 | &--showed { 140 | display: block; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /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 | Object.setPrototypeOf(this, blockAPI); 122 | } 123 | 124 | export default BlockAPI; 125 | -------------------------------------------------------------------------------- /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 | const sequence = await _.sequence(chainData as _.ChainData[]); 52 | 53 | this.Editor.UI.checkEmptiness(); 54 | 55 | return sequence; 56 | } 57 | 58 | /** 59 | * Get plugin instance 60 | * Add plugin instance to BlockManager 61 | * Insert block to working zone 62 | * 63 | * @param {object} item - Block data to insert 64 | * 65 | * @returns {Promise} 66 | */ 67 | public async insertBlock(item: OutputBlockData): Promise { 68 | const { Tools, BlockManager } = this.Editor; 69 | const { type: tool, data, tunes, id } = item; 70 | 71 | if (Tools.available.has(tool)) { 72 | try { 73 | BlockManager.insert({ 74 | id, 75 | tool, 76 | data, 77 | tunes, 78 | }); 79 | } catch (error) { 80 | _.log(`Block «${tool}» skipped because of plugins error`, 'warn', data); 81 | throw Error(error); 82 | } 83 | } else { 84 | /** If Tool is unavailable, create stub Block for it */ 85 | const stubData = { 86 | savedData: { 87 | id, 88 | type: tool, 89 | data, 90 | }, 91 | title: tool, 92 | }; 93 | 94 | if (Tools.unavailable.has(tool)) { 95 | const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox; 96 | 97 | stubData.title = toolboxSettings?.title || stubData.title; 98 | } 99 | 100 | const stub = BlockManager.insert({ 101 | id, 102 | tool: Tools.stubTool, 103 | data: stubData, 104 | }); 105 | 106 | stub.stretched = true; 107 | 108 | _.log(`Tool «${tool}» is not found. Check 'tools' property at your initial Editor.js config.`, 'warn'); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /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 | 68 | return moveDownButton; 69 | } 70 | 71 | /** 72 | * Handle clicks on 'move down' button 73 | * 74 | * @param {MouseEvent} event - click event 75 | * @param {HTMLElement} button - clicked button 76 | */ 77 | public handleClick(event: MouseEvent, button: HTMLElement): void { 78 | const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); 79 | const nextBlock = this.api.blocks.getBlockByIndex(currentBlockIndex + 1); 80 | 81 | // If Block is last do nothing 82 | if (!nextBlock) { 83 | button.classList.add(this.CSS.animation); 84 | 85 | window.setTimeout(() => { 86 | button.classList.remove(this.CSS.animation); 87 | }, 500); 88 | 89 | return; 90 | } 91 | 92 | const nextBlockElement = nextBlock.holder; 93 | const nextBlockCoords = nextBlockElement.getBoundingClientRect(); 94 | 95 | let scrollOffset = Math.abs(window.innerHeight - nextBlockElement.offsetHeight); 96 | 97 | /** 98 | * Next block ends on screen. 99 | * Increment scroll by next block's height to save element onscreen-position 100 | */ 101 | if (nextBlockCoords.top < window.innerHeight) { 102 | scrollOffset = window.scrollY + nextBlockElement.offsetHeight; 103 | } 104 | 105 | window.scrollTo(0, scrollOffset); 106 | 107 | /** Change blocks positions */ 108 | this.api.blocks.move(currentBlockIndex + 1); 109 | 110 | /** Hide the Tooltip */ 111 | this.api.tooltip.hide(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /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/components/polyfills.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Extend Element interface to include prefixed and experimental properties 5 | */ 6 | interface Element { 7 | matchesSelector: (selector: string) => boolean; 8 | mozMatchesSelector: (selector: string) => boolean; 9 | msMatchesSelector: (selector: string) => boolean; 10 | oMatchesSelector: (selector: string) => boolean; 11 | 12 | prepend: (...nodes: Array) => void; 13 | append: (...nodes: Array) => void; 14 | } 15 | 16 | /** 17 | * The Element.matches() method returns true if the element 18 | * would be selected by the specified selector string; 19 | * otherwise, returns false. 20 | * 21 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill} 22 | * 23 | * @param {string} s - selector 24 | */ 25 | if (!Element.prototype.matches) { 26 | Element.prototype.matches = Element.prototype.matchesSelector || 27 | Element.prototype.mozMatchesSelector || 28 | Element.prototype.msMatchesSelector || 29 | Element.prototype.oMatchesSelector || 30 | Element.prototype.webkitMatchesSelector || 31 | function (s): boolean { 32 | const matches = (this.document || this.ownerDocument).querySelectorAll(s); 33 | let i = matches.length; 34 | 35 | while (--i >= 0 && matches.item(i) !== this) { 36 | } 37 | 38 | return i > -1; 39 | }; 40 | } 41 | 42 | /** 43 | * The Element.closest() method returns the closest ancestor 44 | * of the current element (or the current element itself) which 45 | * matches the selectors given in parameter. 46 | * If there isn't such an ancestor, it returns null. 47 | * 48 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill} 49 | * 50 | * @param {string} s - selector 51 | */ 52 | if (!Element.prototype.closest) { 53 | Element.prototype.closest = function (s): Element | null { 54 | // eslint-disable-next-line @typescript-eslint/no-this-alias 55 | let el = this; 56 | 57 | if (!document.documentElement.contains(el)) { 58 | return null; 59 | } 60 | 61 | do { 62 | if (el.matches(s)) { 63 | return el; 64 | } 65 | 66 | el = el.parentElement || el.parentNode; 67 | } while (el !== null); 68 | 69 | return null; 70 | }; 71 | } 72 | 73 | /** 74 | * The ParentNode.prepend method inserts a set of Node objects 75 | * or DOMString objects before the first child of the ParentNode. 76 | * DOMString objects are inserted as equivalent Text nodes. 77 | * 78 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/prepend#Polyfill} 79 | * 80 | * @param {Node | Node[] | string | string[]} nodes - nodes to prepend 81 | */ 82 | if (!Element.prototype.prepend) { 83 | Element.prototype.prepend = function prepend(nodes: Array | Node | string): void { 84 | const docFrag = document.createDocumentFragment(); 85 | 86 | if (!Array.isArray(nodes)) { 87 | nodes = [ nodes ]; 88 | } 89 | 90 | nodes.forEach((node: Node | string) => { 91 | const isNode = node instanceof Node; 92 | 93 | docFrag.appendChild(isNode ? node as Node : document.createTextNode(node as string)); 94 | }); 95 | 96 | this.insertBefore(docFrag, this.firstChild); 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /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 Toolbox from '../components/modules/toolbar/toolbox'; 6 | import BlockSettings from '../components/modules/toolbar/blockSettings'; 7 | import Paste from '../components/modules/paste'; 8 | import DragNDrop from '../components/modules/dragNDrop'; 9 | import ModificationsObserver from '../components/modules/modificationsObserver'; 10 | import Renderer from '../components/modules/renderer'; 11 | import Tools from '../components/modules/tools'; 12 | import API from '../components/modules/api/index'; 13 | import Caret from '../components/modules/caret'; 14 | import BlockManager from '../components/modules/blockManager'; 15 | import BlocksAPI from '../components/modules/api/blocks'; 16 | import CaretAPI from '../components/modules/api/caret'; 17 | import EventsAPI from '../components/modules/api/events'; 18 | import ListenersAPI from '../components/modules/api/listeners'; 19 | import SanitizerAPI from '../components/modules/api/sanitizer'; 20 | import ToolbarAPI from '../components/modules/api/toolbar'; 21 | import StylesAPI from '../components/modules/api/styles'; 22 | import SelectionAPI from '../components/modules/api/selection'; 23 | import NotifierAPI from '../components/modules/api/notifier'; 24 | import SaverAPI from '../components/modules/api/saver'; 25 | import Saver from '../components/modules/saver'; 26 | import BlockSelection from '../components/modules/blockSelection'; 27 | import RectangleSelection from '../components/modules/RectangleSelection'; 28 | import InlineToolbarAPI from '../components/modules/api/inlineToolbar'; 29 | import CrossBlockSelection from '../components/modules/crossBlockSelection'; 30 | import ConversionToolbar from '../components/modules/toolbar/conversion'; 31 | import TooltipAPI from '../components/modules/api/tooltip'; 32 | import ReadOnly from '../components/modules/readonly'; 33 | import ReadOnlyAPI from '../components/modules/api/readonly'; 34 | import I18nAPI from '../components/modules/api/i18n'; 35 | 36 | export interface EditorModules { 37 | UI: UI; 38 | BlockEvents: BlockEvents; 39 | BlockSelection: BlockSelection; 40 | RectangleSelection: RectangleSelection; 41 | Toolbar: Toolbar; 42 | InlineToolbar: InlineToolbar; 43 | Toolbox: Toolbox; 44 | BlockSettings: BlockSettings; 45 | ConversionToolbar: ConversionToolbar; 46 | Paste: Paste; 47 | DragNDrop: DragNDrop; 48 | ModificationsObserver: ModificationsObserver; 49 | Renderer: Renderer; 50 | Tools: Tools; 51 | API: API; 52 | Caret: Caret; 53 | Saver: Saver; 54 | BlockManager: BlockManager; 55 | BlocksAPI: BlocksAPI; 56 | CaretAPI: CaretAPI; 57 | EventsAPI: EventsAPI; 58 | ListenersAPI: ListenersAPI; 59 | SanitizerAPI: SanitizerAPI; 60 | SaverAPI: SaverAPI; 61 | SelectionAPI: SelectionAPI; 62 | StylesAPI: StylesAPI; 63 | ToolbarAPI: ToolbarAPI; 64 | InlineToolbarAPI: InlineToolbarAPI; 65 | CrossBlockSelection: CrossBlockSelection; 66 | NotifierAPI: NotifierAPI; 67 | TooltipAPI: TooltipAPI; 68 | ReadOnly: ReadOnly; 69 | ReadOnlyAPI: ReadOnlyAPI; 70 | I18nAPI: I18nAPI; 71 | } 72 | -------------------------------------------------------------------------------- /src/components/utils/events.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @class EventDispatcher 3 | * 4 | * Has two important methods: 5 | * - {Function} on - appends subscriber to the event. If event doesn't exist - creates new one 6 | * - {Function} emit - fires all subscribers with data 7 | * - {Function off - unsubscribes callback 8 | * 9 | * @version 1.0.0 10 | * 11 | * @typedef {Events} Events 12 | * @property {object} subscribers - all subscribers grouped by event name 13 | */ 14 | export default class EventsDispatcher { 15 | /** 16 | * Object with events` names as key and array of callback functions as value 17 | * 18 | * @type {{}} 19 | */ 20 | private subscribers: {[name: string]: Array<(data?: object) => object>} = {}; 21 | 22 | /** 23 | * Subscribe any event on callback 24 | * 25 | * @param {string} eventName - event name 26 | * @param {Function} callback - subscriber 27 | */ 28 | public on(eventName: string, callback: (data: object) => object): void { 29 | if (!(eventName in this.subscribers)) { 30 | this.subscribers[eventName] = []; 31 | } 32 | 33 | // group by events 34 | this.subscribers[eventName].push(callback); 35 | } 36 | 37 | /** 38 | * Subscribe any event on callback. Callback will be called once and be removed from subscribers array after call. 39 | * 40 | * @param {string} eventName - event name 41 | * @param {Function} callback - subscriber 42 | */ 43 | public once(eventName: string, callback: (data: object) => object): void { 44 | if (!(eventName in this.subscribers)) { 45 | this.subscribers[eventName] = []; 46 | } 47 | 48 | const wrappedCallback = (data: object): object => { 49 | const result = callback(data); 50 | 51 | const indexOfHandler = this.subscribers[eventName].indexOf(wrappedCallback); 52 | 53 | if (indexOfHandler !== -1) { 54 | this.subscribers[eventName].splice(indexOfHandler, 1); 55 | } 56 | 57 | return result; 58 | }; 59 | 60 | // group by events 61 | this.subscribers[eventName].push(wrappedCallback); 62 | } 63 | 64 | /** 65 | * Emit callbacks with passed data 66 | * 67 | * @param {string} eventName - event name 68 | * @param {object} data - subscribers get this data when they were fired 69 | */ 70 | public emit(eventName: string, data?: object): void { 71 | if (!this.subscribers[eventName]) { 72 | return; 73 | } 74 | 75 | this.subscribers[eventName].reduce((previousData, currentHandler) => { 76 | const newData = currentHandler(previousData); 77 | 78 | return newData || previousData; 79 | }, data); 80 | } 81 | 82 | /** 83 | * Unsubscribe callback from event 84 | * 85 | * @param {string} eventName - event name 86 | * @param {Function} callback - event handler 87 | */ 88 | public off(eventName: string, callback: (data: object) => object): void { 89 | for (let i = 0; i < this.subscribers[eventName].length; i++) { 90 | if (this.subscribers[eventName][i] === callback) { 91 | delete this.subscribers[eventName][i]; 92 | break; 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * Destroyer 99 | * clears subsribers list 100 | */ 101 | public destroy(): void { 102 | this.subscribers = null; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/cypress/tests/block-ids.spec.ts: -------------------------------------------------------------------------------- 1 | import Header from '../../../example/tools/header'; 2 | import { nanoid } from 'nanoid'; 3 | 4 | describe.only('Block ids', () => { 5 | beforeEach(() => { 6 | if (this && this.editorInstance) { 7 | this.editorInstance.destroy(); 8 | } else { 9 | cy.createEditor({ 10 | tools: { 11 | header: Header, 12 | }, 13 | }).as('editorInstance'); 14 | } 15 | }); 16 | 17 | it('Should generate unique block ids for new blocks', () => { 18 | cy.get('[data-cy=editorjs]') 19 | .get('div.ce-block') 20 | .click() 21 | .type('First block ') 22 | .type('{enter}') 23 | .get('div.ce-block') 24 | .last() 25 | .type('Second block ') 26 | .type('{enter}'); 27 | 28 | cy.get('[data-cy=editorjs]') 29 | .get('div.ce-toolbar__plus') 30 | .click(); 31 | 32 | cy.get('[data-cy=editorjs]') 33 | .get('li.ce-toolbox__button[data-tool=header]') 34 | .click(); 35 | 36 | cy.get('[data-cy=editorjs]') 37 | .get('div.ce-block') 38 | .last() 39 | .click() 40 | .type('Header'); 41 | 42 | cy.get('@editorInstance') 43 | .then(async (editor: any) => { 44 | const data = await editor.save(); 45 | 46 | data.blocks.forEach(block => { 47 | expect(typeof block.id).to.eq('string'); 48 | }); 49 | }); 50 | }); 51 | 52 | it('should preserve passed ids', () => { 53 | const blocks = [ 54 | { 55 | id: nanoid(), 56 | type: 'paragraph', 57 | data: { 58 | text: 'First block', 59 | }, 60 | }, 61 | { 62 | id: nanoid(), 63 | type: 'paragraph', 64 | data: { 65 | text: 'Second block', 66 | }, 67 | }, 68 | ]; 69 | 70 | cy.get('@editorInstance') 71 | .render({ 72 | blocks, 73 | }); 74 | 75 | cy.get('[data-cy=editorjs]') 76 | .get('div.ce-block') 77 | .first() 78 | .click() 79 | .type('{movetoend} Some more text'); 80 | 81 | cy.get('@editorInstance') 82 | .then(async (editor: any) => { 83 | const data = await editor.save(); 84 | 85 | data.blocks.forEach((block, index) => { 86 | expect(block.id).to.eq(blocks[index].id); 87 | }); 88 | }); 89 | }); 90 | 91 | it('should preserve passed ids if blocks were added', () => { 92 | const blocks = [ 93 | { 94 | id: nanoid(), 95 | type: 'paragraph', 96 | data: { 97 | text: 'First block', 98 | }, 99 | }, 100 | { 101 | id: nanoid(), 102 | type: 'paragraph', 103 | data: { 104 | text: 'Second block', 105 | }, 106 | }, 107 | ]; 108 | 109 | cy.get('@editorInstance') 110 | .render({ 111 | blocks, 112 | }); 113 | 114 | cy.get('[data-cy=editorjs]') 115 | .get('div.ce-block') 116 | .first() 117 | .click() 118 | .type('{enter}') 119 | .next() 120 | .type('Middle block'); 121 | 122 | cy.get('@editorInstance') 123 | .then(async (editor: any) => { 124 | const data = await editor.save(); 125 | 126 | expect(data.blocks[0].id).to.eq(blocks[0].id); 127 | expect(data.blocks[2].id).to.eq(blocks[1].id); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@editorjs/editorjs", 3 | "version": "2.21.0", 4 | "description": "Editor.js — Native JS, based on API and Open Source", 5 | "main": "dist/editor.js", 6 | "types": "./types/index.d.ts", 7 | "keywords": [ 8 | "codex editor", 9 | "text editor", 10 | "editor", 11 | "editor.js", 12 | "editorjs" 13 | ], 14 | "scripts": { 15 | "clear": "rimraf dist && mkdirp dist", 16 | "build": "yarn clear && yarn svg && yarn build:webpack:prod", 17 | "build:dev": "yarn clear && yarn svg && yarn build:webpack:dev", 18 | "build:webpack:dev": "webpack --mode development --progress --display-error-details --display-entrypoints --watch", 19 | "build:webpack:prod": "webpack --mode production", 20 | "lint": "eslint src/ --ext .ts && yarn lint:tests", 21 | "lint:errors": "eslint src/ --ext .ts --quiet", 22 | "lint:fix": "eslint src/ --ext .ts --fix", 23 | "lint:tests": "eslint test/ --ext .ts", 24 | "svg": "svg-sprite-generate -d src/assets/ -o dist/sprite.svg", 25 | "pull_tools": "git submodule update --init --recursive", 26 | "checkout_tools": "git submodule foreach git pull origin master", 27 | "test:e2e": "yarn build && cypress run" 28 | }, 29 | "author": "CodeX", 30 | "license": "Apache-2.0", 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/codex-team/editor.js.git" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.9.0", 37 | "@babel/plugin-transform-runtime": "^7.9.0", 38 | "@babel/polyfill": "^7.8.7", 39 | "@babel/preset-env": "^7.9.5", 40 | "@babel/preset-typescript": "^7.13.0", 41 | "@babel/register": "^7.9.0", 42 | "@babel/runtime": "^7.9.2", 43 | "@codexteam/shortcuts": "^1.1.1", 44 | "@cypress/code-coverage": "^3.9.2", 45 | "@cypress/webpack-preprocessor": "^5.6.0", 46 | "@types/node": "^14.14.35", 47 | "@types/webpack": "^4.41.12", 48 | "@types/webpack-env": "^1.15.2", 49 | "babel-loader": "^8.1.0", 50 | "babel-plugin-add-module-exports": "^1.0.0", 51 | "babel-plugin-class-display-name": "^2.1.0", 52 | "babel-plugin-istanbul": "^6.0.0", 53 | "core-js": "3.6.5", 54 | "css-loader": "^3.5.3", 55 | "cssnano": "^4.1.10", 56 | "cypress": "^6.8.0", 57 | "cypress-intellij-reporter": "^0.0.6", 58 | "eslint": "^6.8.0", 59 | "eslint-config-codex": "^1.3.3", 60 | "eslint-loader": "^4.0.2", 61 | "eslint-plugin-chai-friendly": "^0.6.0", 62 | "eslint-plugin-cypress": "^2.11.2", 63 | "extract-text-webpack-plugin": "^3.0.2", 64 | "html-janitor": "^2.0.4", 65 | "license-webpack-plugin": "^2.1.4", 66 | "mkdirp": "^1.0.4", 67 | "postcss-apply": "^0.12.0", 68 | "postcss-import": "^12.0.1", 69 | "postcss-loader": "^3.0.0", 70 | "postcss-nested": "^4.1.2", 71 | "postcss-nested-ancestors": "^2.0.0", 72 | "postcss-preset-env": "^6.6.0", 73 | "raw-loader": "^4.0.1", 74 | "rimraf": "^3.0.2", 75 | "stylelint": "^13.3.3", 76 | "svg-sprite-generator": "^0.0.7", 77 | "terser-webpack-plugin": "^2.3.6", 78 | "ts-loader": "^7.0.1", 79 | "tslint": "^6.1.1", 80 | "typescript": "3.8.3", 81 | "webpack": "^4.43.0", 82 | "webpack-cli": "^3.3.11" 83 | }, 84 | "collective": { 85 | "type": "opencollective", 86 | "url": "https://opencollective.com/editorjs" 87 | }, 88 | "dependencies": { 89 | "codex-notifier": "^1.1.2", 90 | "codex-tooltip": "^1.0.2", 91 | "nanoid": "^3.1.22" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /.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/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 | 80 | return this.nodes.button; 81 | } 82 | 83 | /** 84 | * Delete block conditions passed 85 | * 86 | * @param {MouseEvent} event - click event 87 | */ 88 | public handleClick(event: MouseEvent): void { 89 | /** 90 | * if block is not waiting the confirmation, subscribe on block-settings-closing event to reset 91 | * otherwise delete block 92 | */ 93 | if (!this.needConfirmation) { 94 | this.setConfirmation(true); 95 | 96 | /** 97 | * Subscribe on event. 98 | * When toolbar block settings is closed but block deletion is not confirmed, 99 | * then reset confirmation state 100 | */ 101 | this.api.events.on('block-settings-closed', this.resetConfirmation); 102 | } else { 103 | /** 104 | * Unsubscribe from block-settings closing event 105 | */ 106 | this.api.events.off('block-settings-closed', this.resetConfirmation); 107 | 108 | this.api.blocks.delete(); 109 | this.api.toolbar.close(); 110 | this.api.tooltip.hide(); 111 | 112 | /** 113 | * Prevent firing ui~documentClicked that can drop currentBlock pointer 114 | */ 115 | event.stopPropagation(); 116 | } 117 | } 118 | 119 | /** 120 | * change tune state 121 | * 122 | * @param {boolean} state - delete confirmation state 123 | */ 124 | private setConfirmation(state: boolean): void { 125 | this.needConfirmation = state; 126 | this.nodes.button.classList.add(this.CSS.buttonConfirm); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /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/styles/variables.css: -------------------------------------------------------------------------------- 1 | @custom-media --mobile (width <= 650px); 2 | @custom-media --not-mobile (width >= 651px); 3 | 4 | :root { 5 | /** 6 | * Selection color 7 | */ 8 | --selectionColor: #e1f2ff; 9 | --inlineSelectionColor: #d4ecff; 10 | 11 | /** 12 | * Toolbar buttons 13 | */ 14 | --bg-light: #eff2f5; 15 | 16 | /** 17 | * All gray texts: placeholders, settings 18 | */ 19 | --grayText: #707684; 20 | 21 | /** 22 | * Gray icons hover 23 | */ 24 | --color-dark: #1D202B; 25 | 26 | /** 27 | * Blue icons 28 | */ 29 | --color-active-icon: #388AE5; 30 | 31 | /** 32 | * Gray border, loaders 33 | */ 34 | --color-gray-border: rgba(201, 201, 204, 0.48); 35 | 36 | /** 37 | * Block content width 38 | * Should be set in a constant at the modules/ui.js 39 | */ 40 | --content-width: 650px; 41 | 42 | /** 43 | * In narrow mode, we increase right zone contained Block Actions button 44 | */ 45 | --narrow-mode-right-padding: 50px; 46 | 47 | /** 48 | * Toolbar buttons height and width 49 | */ 50 | --toolbar-buttons-size: 34px; 51 | 52 | /** 53 | * Toolbar Plus Button and Toolbox buttons height and width 54 | */ 55 | --toolbox-buttons-size: 34px; 56 | 57 | /** 58 | * Confirm deletion bg 59 | */ 60 | --color-confirm: #E24A4A; 61 | 62 | --overlay-pane: { 63 | position: absolute; 64 | background-color: #FFFFFF; 65 | border: 1px solid #EAEAEA; 66 | box-shadow: 0 3px 15px -3px rgba(13,20,33,0.13); 67 | border-radius: 4px; 68 | z-index: 2; 69 | 70 | @media (--mobile){ 71 | box-shadow: 0 13px 7px -5px rgba(26, 38, 49, 0.09),6px 15px 34px -6px rgba(33, 48, 73, 0.29); 72 | border-bottom-color: #d5d7db; 73 | } 74 | 75 | &--left-oriented { 76 | &::before { 77 | left: 15px; 78 | margin-left: 0; 79 | } 80 | } 81 | 82 | &--right-oriented { 83 | &::before { 84 | left: auto; 85 | right: 15px; 86 | margin-left: 0; 87 | } 88 | } 89 | }; 90 | 91 | /** 92 | * Styles for Toolbox Buttons and Plus Button 93 | */ 94 | --toolbox-button: { 95 | color: var(--grayText); 96 | cursor: pointer; 97 | width: var(--toolbox-buttons-size); 98 | height: var(--toolbox-buttons-size); 99 | display: inline-flex; 100 | justify-content: center; 101 | align-items: center; 102 | 103 | &:hover, 104 | &--active { 105 | color: var(--color-active-icon); 106 | } 107 | 108 | &--active{ 109 | animation: bounceIn 0.75s 1; 110 | animation-fill-mode: forwards; 111 | } 112 | 113 | }; 114 | 115 | /** 116 | * Styles for Settings Button in Toolbar 117 | */ 118 | --toolbar-button: { 119 | display: inline-flex; 120 | align-items: center; 121 | justify-content: center; 122 | width: 34px; 123 | height: 34px; 124 | line-height: 34px; 125 | padding: 0 !important; 126 | text-align: center; 127 | border-radius: 3px; 128 | cursor: pointer; 129 | border: 0; 130 | outline: none; 131 | background-color: transparent; 132 | vertical-align: bottom; 133 | color: #000; 134 | margin: 0; 135 | 136 | &:hover { 137 | background-color: var(--bg-light); 138 | } 139 | 140 | &--active { 141 | color: var(--color-active-icon); 142 | } 143 | 144 | &--focused { 145 | box-shadow: inset 0 0 0px 1px rgba(7, 161, 227, 0.08); 146 | background: rgba(34, 186, 255, 0.08) !important; 147 | 148 | &-animated { 149 | animation-name: buttonClicked; 150 | animation-duration: 250ms; 151 | } 152 | } 153 | }; 154 | } 155 | -------------------------------------------------------------------------------- /src/components/block-tunes/block-tune-move-up.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @class MoveUpTune 3 | * @classdesc Editor's default tune that moves up selected block 4 | * 5 | * @copyright 2018 6 | */ 7 | import $ from '../dom'; 8 | import { API, BlockTune } from '../../../types'; 9 | 10 | /** 11 | * 12 | */ 13 | export default class MoveUpTune 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 | * @type {{wrapper: string}} 30 | */ 31 | private CSS = { 32 | button: 'ce-settings__button', 33 | wrapper: 'ce-tune-move-up', 34 | animation: 'wobble', 35 | }; 36 | 37 | /** 38 | * MoveUpTune constructor 39 | * 40 | * @param {API} api - Editor's API 41 | */ 42 | constructor({ api }) { 43 | this.api = api; 44 | } 45 | 46 | /** 47 | * Create "MoveUp" button and add click event listener 48 | * 49 | * @returns {HTMLElement} 50 | */ 51 | public render(): HTMLElement { 52 | const moveUpButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {}); 53 | 54 | moveUpButton.appendChild($.svg('arrow-up', 14, 14)); 55 | this.api.listeners.on( 56 | moveUpButton, 57 | 'click', 58 | (event) => this.handleClick(event as MouseEvent, moveUpButton), 59 | false 60 | ); 61 | 62 | /** 63 | * Enable tooltip module on button 64 | */ 65 | this.api.tooltip.onHover(moveUpButton, this.api.i18n.t('Move up')); 66 | 67 | return moveUpButton; 68 | } 69 | 70 | /** 71 | * Move current block up 72 | * 73 | * @param {MouseEvent} event - click event 74 | * @param {HTMLElement} button - clicked button 75 | */ 76 | public handleClick(event: MouseEvent, button: HTMLElement): void { 77 | const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); 78 | const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); 79 | const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1); 80 | 81 | if (currentBlockIndex === 0 || !currentBlock || !previousBlock) { 82 | button.classList.add(this.CSS.animation); 83 | 84 | window.setTimeout(() => { 85 | button.classList.remove(this.CSS.animation); 86 | }, 500); 87 | 88 | return; 89 | } 90 | 91 | const currentBlockElement = currentBlock.holder; 92 | const previousBlockElement = previousBlock.holder; 93 | 94 | /** 95 | * Here is two cases: 96 | * - when previous block has negative offset and part of it is visible on window, then we scroll 97 | * by window's height and add offset which is mathematically difference between two blocks 98 | * 99 | * - when previous block is visible and has offset from the window, 100 | * than we scroll window to the difference between this offsets. 101 | */ 102 | const currentBlockCoords = currentBlockElement.getBoundingClientRect(), 103 | previousBlockCoords = previousBlockElement.getBoundingClientRect(); 104 | 105 | let scrollUpOffset; 106 | 107 | if (previousBlockCoords.top > 0) { 108 | scrollUpOffset = Math.abs(currentBlockCoords.top) - Math.abs(previousBlockCoords.top); 109 | } else { 110 | scrollUpOffset = window.innerHeight - Math.abs(currentBlockCoords.top) + Math.abs(previousBlockCoords.top); 111 | } 112 | 113 | window.scrollBy(0, -1 * scrollUpOffset); 114 | 115 | /** Change blocks positions */ 116 | this.api.blocks.move(currentBlockIndex - 1); 117 | 118 | /** Hide the Tooltip */ 119 | this.api.tooltip.hide(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # So how to use Editor.js 2 | 3 | ## Basics 4 | 5 | Editor.js is a Block-Styled editor. Blocks is a structural units, of which the Entry is composed. 6 | For example, `Paragraph`, `Heading`, `Image`, `Video`, `List` are Blocks. Each Block is represented by a Plugin. 7 | We have [many](http://github.com/editor-js/) ready-to-use Plugins and the [simple API](tools.md) for creation new ones. 8 | 9 | So how to use the Editor after [Installation](installation.md). 10 | 11 | - Create new Blocks by Enter or with the Plus Button 12 | - Press `TAB` or click on the Plus Button to view the Toolbox 13 | - Press `TAB` again to leaf Toolbox and select a Block you need. Then press Enter. 14 | 15 | 16 | ![](https://github.com/editor-js/list/raw/master/assets/example.gif) 17 | 18 | - Select text fragment and apply a style or insert a link from the Inline Toolbar 19 | 20 | ![](https://capella.pics/7ccbcfcd-1c49-4674-bea7-71021468a1bd.jpg) 21 | 22 | - Use «three-dots» button on the right to open Block Settings. From here, you can move and delete a Block 23 | or apply Tool's settings, if it provided. For example, set a Heading level or List style. 24 | 25 | ![](https://capella.pics/01a55381-46cd-47c7-b92e-34765434f2ca.jpg) 26 | 27 | ## Shortcuts 28 | 29 | We really appreciate shortcuts. So there are few presets. 30 | 31 | Action | Shortcut | Restrictions 32 | -- | -- | -- 33 | `TAB` | Show/leaf a Toolbox. | On empty block 34 | `SHIFT+TAB` | Leaf back a Toolbox. | While Toolbox is opened 35 | `ENTER` | Create a Block | While Toolbox is opened and some Tool is selected 36 | `CMD+B` | Bold style | On selection 37 | `CMD+I` | Italic style | On selection 38 | `CMD+K` | Insert a link | On selection 39 | 40 | Also we support shortcuts on the all type of Tools. Specify a shortcut with the Tools configuration. For example: 41 | 42 | ```js 43 | var editor = new EditorJS({ 44 | //... 45 | tools: { 46 | header: { 47 | class: Header, 48 | shortcut: 'CMD+SHIFT+H' 49 | }, 50 | list: { 51 | class: List, 52 | shortcut: 'CMD+SHIFT+L' 53 | } 54 | } 55 | //... 56 | }); 57 | 58 | ``` 59 | 60 | ## Autofocus 61 | 62 | If you want to focus Editor after page has been loaded, you can enable autofocus by passing `autofocus` to the initial config 63 | 64 | 65 | ```js 66 | var editor = new EditorJS({ 67 | //... 68 | autofocus: true 69 | //... 70 | }); 71 | 72 | ``` 73 | 74 | ## Holder 75 | The `holder` property supports an id or a reference to dom element. 76 | 77 | ```js 78 | var editor = new EditorJS({ 79 | holder: document.querySelector('.editor'), 80 | }) 81 | 82 | var editor2 = new EditorJS({ 83 | holder: 'codex-editor' // like document.getElementById('codex-editor') 84 | }) 85 | ``` 86 | 87 | 88 | 89 | ## Placeholder 90 | 91 | By default Editor\`s placeholder is empty. 92 | 93 | You can pass your own placeholder via `placeholder` field: 94 | 95 | 96 | ```js 97 | var editor = new EditorJS({ 98 | //... 99 | placeholder: 'My awesome placeholder' 100 | //... 101 | }); 102 | 103 | ``` 104 | 105 | If you are using your custom `Initial Block`, `placeholder` property is passed in `config` to your Tool constructor. 106 | 107 | ## Log level 108 | 109 | You can specify log level for Editor.js console messages via `logLevel` property of configuration: 110 | 111 | ```js 112 | var editor = new EditorJS({ 113 | //... 114 | logLevel: 'WARN' 115 | //.. 116 | }) 117 | ``` 118 | 119 | Possible values: 120 | 121 | | Value | Description | 122 | | ----- | ---------------------------- | 123 | | `VERBOSE` | Show all messages | 124 | | `INFO` | Show info and debug messages | 125 | | `WARN` | Show errors and warns only | 126 | | `ERROR` | Show errors only | 127 | 128 | -------------------------------------------------------------------------------- /src/codex.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { EditorConfig } from '../types'; 4 | 5 | /** 6 | * Apply polyfills 7 | */ 8 | import '@babel/register'; 9 | 10 | import 'components/polyfills'; 11 | import Core from './components/core'; 12 | import * as _ from './components/utils'; 13 | 14 | declare const VERSION: string; 15 | 16 | /** 17 | * Editor.js 18 | * 19 | * Short Description (눈_눈;) 20 | * 21 | * @version 2.18.0 22 | * 23 | * @license Apache-2.0 24 | * @author CodeX-Team 25 | */ 26 | export default class EditorJS { 27 | /** 28 | * Promise that resolves when core modules are ready and UI is rendered on the page 29 | */ 30 | public isReady: Promise; 31 | 32 | /** 33 | * Stores destroy method implementation. 34 | * Clear heap occupied by Editor and remove UI components from the DOM. 35 | */ 36 | public destroy: () => void; 37 | 38 | /** Editor version */ 39 | public static get version(): string { 40 | return VERSION; 41 | } 42 | 43 | /** 44 | * @param {EditorConfig|string|undefined} [configuration] - user configuration 45 | */ 46 | constructor(configuration?: EditorConfig|string) { 47 | /** 48 | * Set default onReady function 49 | */ 50 | // eslint-disable-next-line @typescript-eslint/no-empty-function 51 | let onReady = (): void => {}; 52 | 53 | /** 54 | * If `onReady` was passed in `configuration` then redefine onReady function 55 | */ 56 | if (_.isObject(configuration) && _.isFunction(configuration.onReady)) { 57 | onReady = configuration.onReady; 58 | } 59 | 60 | /** 61 | * Create a Editor.js instance 62 | */ 63 | const editor = new Core(configuration); 64 | 65 | /** 66 | * We need to export isReady promise in the constructor 67 | * as it can be used before other API methods are exported 68 | * 69 | * @type {Promise} 70 | */ 71 | this.isReady = editor.isReady.then(() => { 72 | this.exportAPI(editor); 73 | onReady(); 74 | }); 75 | } 76 | 77 | /** 78 | * Export external API methods 79 | * 80 | * @param {Core} editor — Editor's instance 81 | */ 82 | public exportAPI(editor: Core): void { 83 | const fieldsToExport = [ 'configuration' ]; 84 | const destroy = (): void => { 85 | Object.values(editor.moduleInstances) 86 | .forEach((moduleInstance) => { 87 | if (_.isFunction(moduleInstance.destroy)) { 88 | moduleInstance.destroy(); 89 | } 90 | moduleInstance.listeners.removeAll(); 91 | }); 92 | 93 | editor = null; 94 | 95 | for (const field in this) { 96 | if (Object.prototype.hasOwnProperty.call(this, field)) { 97 | delete this[field]; 98 | } 99 | } 100 | 101 | Object.setPrototypeOf(this, null); 102 | }; 103 | 104 | fieldsToExport.forEach((field) => { 105 | this[field] = editor[field]; 106 | }); 107 | 108 | this.destroy = destroy; 109 | 110 | Object.setPrototypeOf(this, editor.moduleInstances.API.methods); 111 | 112 | delete this.exportAPI; 113 | 114 | const shorthands = { 115 | blocks: { 116 | clear: 'clear', 117 | render: 'render', 118 | }, 119 | caret: { 120 | focus: 'focus', 121 | }, 122 | events: { 123 | on: 'on', 124 | off: 'off', 125 | emit: 'emit', 126 | }, 127 | saver: { 128 | save: 'save', 129 | }, 130 | }; 131 | 132 | Object.entries(shorthands) 133 | .forEach(([key, methods]) => { 134 | Object.entries(methods) 135 | .forEach(([name, alias]) => { 136 | this[alias] = editor.moduleInstances.API.methods[key][name]; 137 | }); 138 | }); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * For export type there should be one entry point, 3 | * so we export all types from this file 4 | * ------------------------------------ 5 | */ 6 | 7 | import { 8 | Dictionary, 9 | DictValue, 10 | EditorConfig, 11 | I18nConfig, 12 | I18nDictionary, 13 | } from './configs'; 14 | 15 | import { 16 | Blocks, 17 | Caret, 18 | Events, 19 | InlineToolbar, 20 | Listeners, 21 | Notifier, 22 | ReadOnly, 23 | Sanitizer, 24 | Saver, 25 | Selection, 26 | Styles, 27 | Toolbar, 28 | Tooltip, 29 | I18n, 30 | } from './api'; 31 | 32 | import { OutputData } from './data-formats'; 33 | 34 | /** 35 | * Interfaces used for development 36 | */ 37 | export { 38 | BaseTool, 39 | BaseToolConstructable, 40 | InlineTool, 41 | InlineToolConstructable, 42 | InlineToolConstructorOptions, 43 | BlockToolConstructable, 44 | BlockToolConstructorOptions, 45 | BlockTool, 46 | BlockToolData, 47 | Tool, 48 | ToolConstructable, 49 | ToolboxConfig, 50 | ToolSettings, 51 | ToolConfig, 52 | PasteEvent, 53 | PasteEventDetail, 54 | PatternPasteEvent, 55 | PatternPasteEventDetail, 56 | HTMLPasteEvent, 57 | HTMLPasteEventDetail, 58 | FilePasteEvent, 59 | FilePasteEventDetail, 60 | } from './tools'; 61 | export {BlockTune, BlockTuneConstructable} from './block-tunes'; 62 | export { 63 | EditorConfig, 64 | SanitizerConfig, 65 | PasteConfig, 66 | LogLevels, 67 | ConversionConfig, 68 | I18nDictionary, 69 | Dictionary, 70 | DictValue, 71 | I18nConfig, 72 | } from './configs'; 73 | export {OutputData, OutputBlockData} from './data-formats/output-data'; 74 | export { BlockAPI } from './api' 75 | 76 | /** 77 | * We have a namespace API {@link ./api/index.d.ts} (APIMethods) but we can not use it as interface 78 | * So we should create new interface for exporting API type 79 | */ 80 | export interface API { 81 | blocks: Blocks; 82 | caret: Caret; 83 | events: Events; 84 | listeners: Listeners; 85 | notifier: Notifier; 86 | sanitizer: Sanitizer; 87 | saver: Saver; 88 | selection: Selection; 89 | styles: Styles; 90 | toolbar: Toolbar; 91 | inlineToolbar: InlineToolbar; 92 | tooltip: Tooltip; 93 | i18n: I18n; 94 | readOnly: ReadOnly; 95 | } 96 | 97 | /** 98 | * Main Editor class 99 | */ 100 | declare class EditorJS { 101 | public static version: string; 102 | 103 | public isReady: Promise; 104 | 105 | public blocks: Blocks; 106 | public caret: Caret; 107 | public sanitizer: Sanitizer; 108 | public saver: Saver; 109 | public selection: Selection; 110 | public styles: Styles; 111 | public toolbar: Toolbar; 112 | public inlineToolbar: InlineToolbar; 113 | public readOnly: ReadOnly; 114 | constructor(configuration?: EditorConfig|string); 115 | 116 | /** 117 | * API shorthands 118 | */ 119 | 120 | /** 121 | * @see Saver.save 122 | */ 123 | public save(): Promise; 124 | 125 | /** 126 | * @see Blocks.clear 127 | */ 128 | public clear(): void; 129 | 130 | /** 131 | * @see Blocks.render 132 | */ 133 | public render(data: OutputData): Promise; 134 | 135 | /** 136 | * @see Caret.focus 137 | */ 138 | public focus(atEnd?: boolean): boolean; 139 | 140 | /** 141 | * @see Events.on 142 | */ 143 | public on(eventName: string, callback: (data?: any) => void): void; 144 | 145 | /** 146 | * @see Events.off 147 | */ 148 | public off(eventName: string, callback: (data?: any) => void): void; 149 | 150 | /** 151 | * @see Events.emit 152 | */ 153 | public emit(eventName: string, data: any): void; 154 | 155 | /** 156 | * Destroy Editor instance and related DOM elements 157 | */ 158 | public destroy(): void; 159 | } 160 | 161 | export as namespace EditorJS; 162 | export default EditorJS; 163 | --------------------------------------------------------------------------------