Compare commits
104 commits
Author | SHA1 | Date | |
---|---|---|---|
29d68ecb47 | |||
d18eeb5dc8 | |||
50f43bb35d | |||
f78972ee09 | |||
bd1de56ef3 | |||
8276daa5ca | |||
238c909016 | |||
23858e0025 | |||
5eafda5ec4 | |||
efa0a34f8e | |||
c48fca1be3 | |||
1028577521 | |||
844272656e | |||
7821e35302 | |||
4118dc3aea | |||
e1c70b4fb8 | |||
54c4c234a5 | |||
5125f015dc | |||
ecdd73347c | |||
1320b047a2 | |||
b355f1673c | |||
bb2047c60f | |||
cc0d6de04b | |||
9b3e9615b0 | |||
ee6433201d | |||
e9b4c30407 | |||
8138ce95b2 | |||
7ff5faa46f | |||
b619946e8f | |||
9542551d84 | |||
4bdf7a12b7 | |||
c5ddf91189 | |||
11925ca980 | |||
cd29c52e51 | |||
ea2be754e7 | |||
5515c3b534 | |||
6385ab2a39 | |||
c5854eea14 | |||
348c1c7752 | |||
8726f8bdfa | |||
494d6e4b9b | |||
531ab06f63 | |||
5e8fe06dd6 | |||
af6b64a3e6 | |||
92ccfc5317 | |||
ee188bfe5d | |||
35337747c5 | |||
d936bf08b7 | |||
e8acc59487 | |||
bdb6eed9d1 | |||
b8cfcaec26 | |||
7d0db9b622 | |||
ca2bc803a1 | |||
c9014e670d | |||
ec569f9981 | |||
77eb320203 | |||
0369ff5827 | |||
59c8d28da5 | |||
f8e9414217 | |||
42612a0fd9 | |||
be0d33ce0f | |||
922dfd8741 | |||
b39996616c | |||
0e64665b0f | |||
022320940e | |||
41dc65274d | |||
b24bebbc40 | |||
2ab9eb13bc | |||
9e64003c0a | |||
c8993332e3 | |||
39018e07e4 | |||
1f74c5bec9 | |||
0d6c8bd4af | |||
26dce94e67 | |||
e6c265d0fa | |||
b7b00fd060 | |||
aafab1d395 | |||
0491155e33 | |||
707ff72a94 | |||
f5313fda40 | |||
d7f1853ca1 | |||
75379c66a9 | |||
c18011595a | |||
b7d754c93d | |||
5257b061c4 | |||
293135a795 | |||
e9e0146961 | |||
599d332aef | |||
839daf556f | |||
7cf5266a23 | |||
86c84d8555 | |||
153fc48bd6 | |||
447f980d4a | |||
9ae96b9e17 | |||
7bd5c53ab2 | |||
58d6869285 | |||
dfb07ec4e4 | |||
34d4b028c4 | |||
d4edbe162b | |||
07b1ce2aca | |||
551e3f10b9 | |||
d6cad60af3 | |||
24baf08c89 | |||
b4e2429476 |
19
.babelrc
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"presets": [
|
|
||||||
["@babel/preset-env", {
|
|
||||||
"modules": "umd",
|
|
||||||
"useBuiltIns": "entry",
|
|
||||||
"corejs": 3
|
|
||||||
}]
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"babel-plugin-add-module-exports",
|
|
||||||
"babel-plugin-class-display-name",
|
|
||||||
"@babel/plugin-transform-runtime"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"test": {
|
|
||||||
"plugins": [ "istanbul" ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -31,6 +31,8 @@
|
||||||
"ClientRect": true,
|
"ClientRect": true,
|
||||||
"ArrayLike": true,
|
"ArrayLike": true,
|
||||||
"InputEvent": true,
|
"InputEvent": true,
|
||||||
"unknown": true
|
"unknown": true,
|
||||||
|
"requestAnimationFrame": true,
|
||||||
|
"navigator": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2
.github/FUNDING.yml
vendored
|
@ -1,5 +1,5 @@
|
||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: neSpecc
|
|
||||||
patreon: editorjs
|
patreon: editorjs
|
||||||
open_collective: editorjs
|
open_collective: editorjs
|
||||||
|
custom: https://codex.so/donate
|
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve Editor.js
|
about: Create a report to help us improve Editor.js
|
||||||
title: "[Bug]"
|
title: ""
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
|
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -3,9 +3,9 @@ contact_links:
|
||||||
- name: Team
|
- name: Team
|
||||||
url: mailto:team@codex.so
|
url: mailto:team@codex.so
|
||||||
about: Direct team contact.
|
about: Direct team contact.
|
||||||
|
- name: 💬 Discussions
|
||||||
|
url: https://github.com/codex-team/editor.js/discussions
|
||||||
|
about: Use discussions if you have an issue draft, an idea for improvement or for asking questions.
|
||||||
- name: Editor.js Telegram chat
|
- name: Editor.js Telegram chat
|
||||||
url: https://t.me/codex_editor
|
url: https://t.me/codex_editor
|
||||||
about: Telegram chat for Editor.js users communication.
|
about: Telegram chat for Editor.js users communication.
|
||||||
- name: Editor.js contributors Telegram chat
|
|
||||||
url: https://t.me/editorjsdev
|
|
||||||
about: Telegram chat for Editor.js contributors communication.
|
|
||||||
|
|
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,19 +0,0 @@
|
||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea to improve Editor.js
|
|
||||||
title: "\U0001F4A1"
|
|
||||||
labels: feature
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
1. Describe a problem.
|
|
||||||
|
|
||||||
2. Describe the solution you'd like. Mockups are welcome.
|
|
||||||
|
|
||||||
3. Are there any alternatives?
|
|
||||||
|
|
||||||
<!--
|
|
||||||
🤫 If you like Editor.js, please consider supporting us via OpenCollective:
|
|
||||||
https://opencollective.com/editorjs
|
|
||||||
-->
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
name: Discussion
|
name: General Issue
|
||||||
about: Any question about the Editor.js to discuss
|
about: Well-designed, algorithmized feature/idea/improvement issue for Editor.js
|
||||||
title: ''
|
title: ''
|
||||||
labels: discussion
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
24
.github/ISSUE_TEMPLATE/issue--discussion.md
vendored
|
@ -1,24 +0,0 @@
|
||||||
---
|
|
||||||
name: 'Issue: Discussion'
|
|
||||||
about: Any question about the project to discuss
|
|
||||||
title: "❓"
|
|
||||||
labels: discussion
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Question**
|
|
||||||
|
|
||||||
A clear and consistent question about the project. Ex. How can I do smth? Why smth works this way? etc.
|
|
||||||
|
|
||||||
**Context**
|
|
||||||
|
|
||||||
Why and how the question has come up
|
|
||||||
|
|
||||||
**Related issues**
|
|
||||||
|
|
||||||
If there are related issues which describe a bugs or features, put them here
|
|
||||||
|
|
||||||
**Comments**
|
|
||||||
|
|
||||||
Any thoughts about the question
|
|
30
.github/workflows/bump-version-on-merge-next.yml
vendored
|
@ -1,7 +1,14 @@
|
||||||
name: Bump version on merge
|
name: Bump version on merge
|
||||||
|
|
||||||
|
# Caution:
|
||||||
|
# the use of "pull_request_target" trigger allows to successfully
|
||||||
|
# run workflow even when triggered from a fork. The trigger grants
|
||||||
|
# access to repo's secrets and gives write permission to the runner.
|
||||||
|
# This can be used to run malicious code on untrusted PR, so, please
|
||||||
|
# DO NOT checkout any PR's ongoing commits (aka github.event.pull_request.head.sha)
|
||||||
|
# while using this trigger.
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request_target:
|
||||||
branches:
|
branches:
|
||||||
- next
|
- next
|
||||||
types: [closed]
|
types: [closed]
|
||||||
|
@ -11,6 +18,8 @@ jobs:
|
||||||
check-for-no-version-changing:
|
check-for-no-version-changing:
|
||||||
if: github.event.pull_request.merged == true
|
if: github.event.pull_request.merged == true
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
steps:
|
steps:
|
||||||
# Checkout to target branch
|
# Checkout to target branch
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
@ -32,16 +41,22 @@ jobs:
|
||||||
uses: codex-team/action-nodejs-package-info@v1
|
uses: codex-team/action-nodejs-package-info@v1
|
||||||
|
|
||||||
# Stop workflow and do not bump version if it was changed already
|
# Stop workflow and do not bump version if it was changed already
|
||||||
- name: Stop workflow and do not bump version if it was changed already
|
- name: Stop workflow if version was changed already
|
||||||
uses: actions/github-script@v3
|
|
||||||
if: steps.packageOld.outputs.version != steps.packageNew.outputs.version
|
if: steps.packageOld.outputs.version != steps.packageNew.outputs.version
|
||||||
with:
|
run: |
|
||||||
script: |
|
curl -L \
|
||||||
core.setFailed('Version was changed! ${{ steps.packageOld.outputs.version }} -> ${{ steps.packageNew.outputs.version }}')
|
-X POST \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/cancel
|
||||||
|
|
||||||
bump-version:
|
bump-version:
|
||||||
needs: check-for-no-version-changing
|
needs: check-for-no-version-changing
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
# Checkout to target branch
|
# Checkout to target branch
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
@ -49,8 +64,7 @@ jobs:
|
||||||
# Setup node environment
|
# Setup node environment
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 15
|
node-version: 16
|
||||||
registry-url: https://registry.npmjs.org/
|
|
||||||
|
|
||||||
# Bump version to the next prerelease (patch) with rc suffix
|
# Bump version to the next prerelease (patch) with rc suffix
|
||||||
- name: Suggest the new version
|
- name: Suggest the new version
|
||||||
|
|
47
.github/workflows/create-a-release-draft.yml
vendored
|
@ -1,7 +1,14 @@
|
||||||
name: Create a release draft
|
name: Create a release draft
|
||||||
|
|
||||||
|
# Caution:
|
||||||
|
# the use of "pull_request_target" trigger allows to successfully
|
||||||
|
# run workflow even when triggered from a fork. The trigger grants
|
||||||
|
# access to repo's secrets and gives write permission to the runner.
|
||||||
|
# This can be used to run malicious code on untrusted PR, so, please
|
||||||
|
# DO NOT checkout any PR's ongoing commits (aka github.event.pull_request.head.sha)
|
||||||
|
# while using this trigger.
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request_target:
|
||||||
branches:
|
branches:
|
||||||
- next
|
- next
|
||||||
types: [closed]
|
types: [closed]
|
||||||
|
@ -11,7 +18,12 @@ jobs:
|
||||||
check-version-changing:
|
check-version-changing:
|
||||||
if: github.event.pull_request.merged == true
|
if: github.event.pull_request.merged == true
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
# Checkout to target branch
|
# Checkout to target branch
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
|
@ -33,16 +45,21 @@ jobs:
|
||||||
|
|
||||||
# Stop workflow if version was not changed
|
# Stop workflow if version was not changed
|
||||||
- name: Stop workflow if version was not changed
|
- name: Stop workflow if version was not changed
|
||||||
uses: actions/github-script@v3
|
|
||||||
if: steps.packageOld.outputs.version == steps.packageNew.outputs.version
|
if: steps.packageOld.outputs.version == steps.packageNew.outputs.version
|
||||||
with:
|
run: |
|
||||||
script: |
|
curl -L \
|
||||||
core.setFailed('No version changes. ${{ steps.packageOld.outputs.version }}')
|
-X POST \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/cancel
|
||||||
|
|
||||||
# Create a new draft release
|
# Create a new draft release
|
||||||
release-draft:
|
release-draft:
|
||||||
needs: check-version-changing
|
needs: check-version-changing
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
# Checkout to target branch
|
# Checkout to target branch
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
@ -53,8 +70,7 @@ jobs:
|
||||||
# Setup node environment
|
# Setup node environment
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.17.0
|
node-version: 16
|
||||||
registry-url: https://registry.npmjs.org/
|
|
||||||
|
|
||||||
# Prepare, build and publish project
|
# Prepare, build and publish project
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
@ -87,15 +103,26 @@ jobs:
|
||||||
# If version name contains "-rc" suffix than mark a "pre-release" checkbox
|
# If version name contains "-rc" suffix than mark a "pre-release" checkbox
|
||||||
prerelease: ${{ contains(steps.package.outputs.version, '-rc') }}
|
prerelease: ${{ contains(steps.package.outputs.version, '-rc') }}
|
||||||
|
|
||||||
# Build and upload target Editor.js build to release as artifact
|
# Build and upload target Editor.js UMD build to release as artifact
|
||||||
- name: Upload Release Asset
|
- name: Upload Release Asset
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
asset_path: dist/editor.js
|
asset_path: dist/editorjs.umd.js
|
||||||
asset_name: editor.js
|
asset_name: editorjs.umd.js
|
||||||
|
asset_content_type: application/javascript
|
||||||
|
|
||||||
|
# Build and upload target Editor.js MJS build to release as artifact
|
||||||
|
- name: Upload Release Asset
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: dist/editorjs.mjs
|
||||||
|
asset_name: editorjs.mjs
|
||||||
asset_content_type: application/javascript
|
asset_content_type: application/javascript
|
||||||
|
|
||||||
# Send a notification message
|
# Send a notification message
|
||||||
|
|
45
.github/workflows/cypress.yml
vendored
|
@ -1,36 +1,21 @@
|
||||||
name: Tests
|
name: Cypress
|
||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
firefox:
|
run-tests:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
container:
|
matrix:
|
||||||
image: cypress/browsers:node14.17.0-chrome88-ff89
|
browser: [firefox, chrome, edge]
|
||||||
options: --user 1001
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- run: yarn ci:pull_paragraph
|
|
||||||
- uses: cypress-io/github-action@v2
|
|
||||||
with:
|
|
||||||
config: video=false
|
|
||||||
browser: firefox
|
|
||||||
build: yarn build
|
|
||||||
chrome:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/setup-node@v3
|
||||||
- run: yarn ci:pull_paragraph
|
with:
|
||||||
- uses: cypress-io/github-action@v2
|
node-version: 18
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: cypress-io/github-action@v6
|
||||||
with:
|
with:
|
||||||
config: video=false
|
config: video=false
|
||||||
browser: chrome
|
browser: ${{ matrix.browser }}
|
||||||
build: yarn build
|
build: yarn build:test
|
||||||
edge:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- run: yarn ci:pull_paragraph
|
|
||||||
- uses: cypress-io/github-action@v2
|
|
||||||
with:
|
|
||||||
config: video=false
|
|
||||||
browser: edge
|
|
||||||
build: yarn build
|
|
||||||
|
|
3
.github/workflows/eslint.yml
vendored
|
@ -19,6 +19,5 @@ jobs:
|
||||||
${{ runner.OS }}-build-
|
${{ runner.OS }}-build-
|
||||||
${{ runner.OS }}-
|
${{ runner.OS }}-
|
||||||
|
|
||||||
- run: yarn install
|
- run: yarn
|
||||||
|
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
|
10
.github/workflows/publish-package-to-npm.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Checkout to target branch
|
# Checkout to target branch
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# Pull submodules
|
# Pull submodules
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
|
@ -22,7 +22,7 @@ jobs:
|
||||||
# Setup node environment
|
# Setup node environment
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.17.0
|
node-version: 16
|
||||||
registry-url: https://registry.npmjs.org/
|
registry-url: https://registry.npmjs.org/
|
||||||
|
|
||||||
# Prepare, build and publish project
|
# Prepare, build and publish project
|
||||||
|
@ -46,9 +46,11 @@ jobs:
|
||||||
notify:
|
notify:
|
||||||
needs: publish
|
needs: publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GITHUB_LINK: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }}
|
||||||
steps:
|
steps:
|
||||||
# Checkout to target branch
|
# Checkout to target branch
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get package info
|
- name: Get package info
|
||||||
id: package
|
id: package
|
||||||
|
@ -58,6 +60,6 @@ jobs:
|
||||||
uses: codex-team/action-codexbot-notify@v1
|
uses: codex-team/action-codexbot-notify@v1
|
||||||
with:
|
with:
|
||||||
webhook: ${{ secrets.CODEX_BOT_NOTIFY_EDITORJS_PUBLIC_CHAT }}
|
webhook: ${{ secrets.CODEX_BOT_NOTIFY_EDITORJS_PUBLIC_CHAT }}
|
||||||
message: '📦 [${{ steps.package.outputs.name }}](${{ steps.package.outputs.npmjs-link }}) ${{ steps.package.outputs.version }} was published'
|
message: '📦 [${{ steps.package.outputs.name }} ${{ steps.package.outputs.version }}](${{ env.GITHUB_LINK }}) was published'
|
||||||
parse_mode: 'markdown'
|
parse_mode: 'markdown'
|
||||||
disable_web_page_preview: true
|
disable_web_page_preview: true
|
1
.gitignore
vendored
|
@ -17,3 +17,4 @@ dist/
|
||||||
|
|
||||||
coverage/
|
coverage/
|
||||||
.nyc_output/
|
.nyc_output/
|
||||||
|
.vscode/launch.json
|
||||||
|
|
3
.gitmodules
vendored
|
@ -16,9 +16,6 @@
|
||||||
[submodule "example/tools/simple-image"]
|
[submodule "example/tools/simple-image"]
|
||||||
path = example/tools/simple-image
|
path = example/tools/simple-image
|
||||||
url = https://github.com/editor-js/simple-image
|
url = https://github.com/editor-js/simple-image
|
||||||
[submodule "src/tools/paragraph"]
|
|
||||||
path = src/tools/paragraph
|
|
||||||
url = https://github.com/editor-js/paragraph
|
|
||||||
[submodule "example/tools/marker"]
|
[submodule "example/tools/marker"]
|
||||||
path = example/tools/marker
|
path = example/tools/marker
|
||||||
url = https://github.com/editor-js/marker
|
url = https://github.com/editor-js/marker
|
||||||
|
|
28
.npmignore
|
@ -1,22 +1,6 @@
|
||||||
.idea
|
*
|
||||||
.github
|
!/dist/**/*
|
||||||
docs
|
!/types/**/*
|
||||||
example
|
!/LICENSE
|
||||||
src
|
!/README.md
|
||||||
test
|
!/package.json
|
||||||
.babelrc
|
|
||||||
.editorconfig
|
|
||||||
.eslintignore
|
|
||||||
.eslintrc
|
|
||||||
.git
|
|
||||||
.gitmodules
|
|
||||||
.jshintrc
|
|
||||||
.postcssrc.yml
|
|
||||||
.stylelintrc
|
|
||||||
CODEOWNERS
|
|
||||||
cypress.json
|
|
||||||
tsconfig.json
|
|
||||||
tslint.json
|
|
||||||
webpack.config.js
|
|
||||||
yarn.lock
|
|
||||||
devserver.js
|
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
plugins:
|
plugins:
|
||||||
# Consumes files by @import rule
|
|
||||||
# https://github.com/postcss/postcss-import
|
|
||||||
postcss-import: {}
|
|
||||||
|
|
||||||
# Apply custom property sets via @apply rule
|
# Apply custom property sets via @apply rule
|
||||||
# https://github.com/pascalduez/postcss-apply
|
# https://github.com/pascalduez/postcss-apply
|
||||||
postcss-apply: {}
|
postcss-apply: {}
|
||||||
|
@ -26,16 +22,6 @@ plugins:
|
||||||
# https://github.com/csstools/postcss-preset-env#preserve
|
# https://github.com/csstools/postcss-preset-env#preserve
|
||||||
preserve: false
|
preserve: false
|
||||||
|
|
||||||
# Enable or disable specific polyfills
|
|
||||||
# https://github.com/csstools/postcss-preset-env#features
|
|
||||||
#
|
|
||||||
# List of available plugins
|
|
||||||
# https://github.com/csstools/postcss-preset-env/blob/master/src/lib/plugins-by-id.js
|
|
||||||
features:
|
|
||||||
# Modify colors using the color-mod() function in CSS
|
|
||||||
# https://github.com/jonathantneal/postcss-color-mod-function
|
|
||||||
color-mod-function: {}
|
|
||||||
|
|
||||||
# Nested rules unwrapper
|
# Nested rules unwrapper
|
||||||
# https://github.com/postcss/postcss-nested
|
# https://github.com/postcss/postcss-nested
|
||||||
#
|
#
|
||||||
|
@ -43,7 +29,3 @@ plugins:
|
||||||
# 'postcss-nesting' feature but it does not work with BEM
|
# 'postcss-nesting' feature but it does not work with BEM
|
||||||
# Report: https://github.com/csstools/postcss-preset-env/issues/40
|
# Report: https://github.com/csstools/postcss-preset-env/issues/40
|
||||||
postcss-nested: {}
|
postcss-nested: {}
|
||||||
|
|
||||||
# Compression tool
|
|
||||||
# https://github.com/cssnano/cssnano
|
|
||||||
cssnano: {}
|
|
||||||
|
|
2
.vscode/settings.json
vendored
|
@ -8,6 +8,7 @@
|
||||||
"colspan",
|
"colspan",
|
||||||
"contenteditable",
|
"contenteditable",
|
||||||
"contentless",
|
"contentless",
|
||||||
|
"Convertable",
|
||||||
"cssnano",
|
"cssnano",
|
||||||
"cssnext",
|
"cssnext",
|
||||||
"Debouncer",
|
"Debouncer",
|
||||||
|
@ -34,6 +35,7 @@
|
||||||
"textareas",
|
"textareas",
|
||||||
"twitterwidget",
|
"twitterwidget",
|
||||||
"typeof",
|
"typeof",
|
||||||
|
"Unmergeable",
|
||||||
"viewports"
|
"viewports"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
423
README.md
|
@ -1,277 +1,234 @@
|
||||||
<a href="https://editorjs.io/"><p align="center"><img src="https://capella.pics/79ce946a-d636-41cd-aa96-d3bc5ecfde03.jpg"></p></a>
|
<p align="center">
|
||||||
|
<a href="https://editorjs.io/">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="./assets/logo_night.png">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="./assets/logo_day.png">
|
||||||
|
<img alt="Editor.js Logo" src="./assets/logo_day.png">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
[![](https://flat.badgen.net/npm/v/@editorjs/editorjs?icon=npm)](https://www.npmjs.com/package/@editorjs/editorjs)
|
<p align="center">
|
||||||
[![](https://flat.badgen.net/bundlephobia/min/@editorjs/editorjs?color=cyan)](https://www.npmjs.com/package/@editorjs/editorjs)
|
<a href="https://editorjs.io/">editorjs.io</a> |
|
||||||
[![](https://flat.badgen.net/bundlephobia/minzip/@editorjs/editorjs?color=green)](https://www.npmjs.com/package/@editorjs/editorjs)
|
<a href="https://editorjs.io/base-concepts/">documentation</a> |
|
||||||
[![Backers on Open Collective](https://opencollective.com/editorjs/backers/badge.svg)](#backers)
|
<a href="https://github.com/codex-team/editor.js/blob/next/docs/CHANGELOG.md">changelog</a>
|
||||||
[![Sponsors on Open Collective](https://opencollective.com/editorjs/sponsors/badge.svg)](#sponsors)
|
|
||||||
[![](https://img.shields.io/npm/l/@editorjs/editorjs?style=flat-square)](https://www.npmjs.com/package/@editorjs/editorjs)
|
|
||||||
[![Join the chat at https://gitter.im/codex-team/editor.js](https://badges.gitter.im/codex-team/editor.js.svg)](https://gitter.im/codex-team/editor.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
|
||||||
|
|
||||||
| | | | | | |
|
</p>
|
||||||
| --------- | --------- | --------- | --------- | --------- | --------- |
|
|
||||||
| <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="16px" height="16px" /> Edge 12+ | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="16px" height="16px" /> Firefox 18+ | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="16px" height="16px" /> Chrome 49+ | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="16px" height="16px" /> Safari 10+ | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari-ios/safari-ios_48x48.png" alt="iOS Safari" width="16px" height="16px" /> iOS Safari 10+ | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="16px" height="16px" /> Opera 36+
|
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/package/@editorjs/editorjs">
|
||||||
|
<img src="https://flat.badgen.net/npm/v/@editorjs/editorjs?icon=npm" alt="npm"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.npmjs.com/package/@editorjs/editorjs">
|
||||||
|
<img src="https://flat.badgen.net/bundlephobia/minzip/@editorjs/editorjs?color=green" alt="Minzipped size"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/codex-team/editor.js#backers">
|
||||||
|
<img src="https://opencollective.com/editorjs/backers/badge.svg" alt="Backers on Open Collective"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/codex-team/editor.js#sponsors">
|
||||||
|
<img src="https://opencollective.com/editorjs/sponsors/badge.svg" alt="Sponsors on Open Collective"/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
## Roadmap
|
Editor.js is an open-source text editor offering a variety of features to help users create and format content efficiently. It has a modern, block-style interface that allows users to easily add and arrange different types of content, such as text, images, lists, quotes, etc. Each Block is provided via a separate plugin making Editor.js extremely flexible.
|
||||||
|
|
||||||
<img align="right" width="342" src="https://user-images.githubusercontent.com/3684889/152149321-3f9c585d-7d87-4dad-ab14-def0d526e71c.png" hspace="50">
|
Editor.js outputs a clean JSON data instead of heavy HTML markup. Use it in Web, iOS, Android, AMP, Instant Articles, speech readers, AI chatbots — everywhere. Easy to sanitize, extend and integrate with your logic.
|
||||||
|
|
||||||
- Collaborative editing
|
- 😍 Modern UI out of the box
|
||||||
- [ ] Implement Inline Tools JSON format `In progress` [#1801](https://github.com/codex-team/editor.js/pull/1801)
|
- 💎 Clean JSON output
|
||||||
- [ ] Implement Operations creation and transformations
|
- ⚙️ Well-designed API
|
||||||
- [ ] Implement Tools API changes
|
- 🛍 Various Tools available
|
||||||
- [ ] Implement Server and communication
|
- 💌 Free and open source
|
||||||
- [ ] Update basic tools to fit the new API
|
|
||||||
|
|
||||||
- Unified Toolbox
|
<picture>
|
||||||
- [x] Block Tunes moved left [#1815](https://github.com/codex-team/editor.js/pull/1815)
|
<img alt="Editor.js Overview" src="./assets/overview.png">
|
||||||
- [x] Toolbox become vertical [#2014](https://github.com/codex-team/editor.js/pull/2014)
|
</picture>
|
||||||
- [x] Ability to display several Toolbox buttons by the single Tool [#2050](https://github.com/codex-team/editor.js/pull/2050)
|
|
||||||
- [ ] Conversion Toolbar uses Unified Toolbox `In progress`
|
|
||||||
- [ ] Block Tunes become vertical
|
|
||||||
- [ ] Conversion Toolbar added to the Block Tunes
|
|
||||||
- Ecosystem improvements
|
|
||||||
|
|
||||||
<a href="https://opencollective.com/editorjs/donate" target="_blank">
|
## Installation
|
||||||
<img width="432" alt="image" src="https://user-images.githubusercontent.com/3684889/166146666-e6f5fcc3-bccf-43b0-b48c-e8f1b25b2896.png">
|
|
||||||
</a>
|
|
||||||
|
|
||||||
##
|
It's quite simple:
|
||||||
|
|
||||||
|
1. Install Editor.js
|
||||||
|
2. Install tools you need
|
||||||
|
3. Initialize Editor's instance
|
||||||
|
|
||||||
## If you like a project 💗💗💗
|
Install using NPM, Yarn, or [CDN](https://www.jsdelivr.com/package/npm/@editorjs/editorjs):
|
||||||
|
|
||||||
If you like Editor.js you can support project improvements and development of new features with a donation to our collective.
|
```bash
|
||||||
|
|
||||||
👉 [https://opencollective.com/editorjs](https://opencollective.com/editorjs)
|
|
||||||
|
|
||||||
### Sponsors
|
|
||||||
|
|
||||||
Support us by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/editorjs/contribute/sir-8679/checkout)]
|
|
||||||
|
|
||||||
<a href="https://humm.earth/" target="_blank"><img src="https://images.opencollective.com/hummearth/2a8406a/logo/256.png" width="64"></a>
|
|
||||||
<a href="https://tesen.com/" target="_blank"><img src="https://images.opencollective.com/tesen-media-inc/b90cf6a/logo/256.png" width="64"></a>
|
|
||||||
<a href="https://slid.cc/" target="_blank"><img src="https://images.opencollective.com/slid_team/ff564d7/logo/256.png" width="64"></a>
|
|
||||||
|
|
||||||
|
|
||||||
### Backers
|
|
||||||
|
|
||||||
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/editorjs/contribute/backer-8632/checkout)]
|
|
||||||
|
|
||||||
<a href="https://opencollective.com/editorjs#backers" target="_blank"><img src="https://opencollective.com/editorjs/backers.svg?width=890"></a>
|
|
||||||
|
|
||||||
### Contributors
|
|
||||||
|
|
||||||
This project exists thanks to all the people who contribute. <img src="https://opencollective.com/editorjs/contributors.svg?width=890&button=false" />
|
|
||||||
|
|
||||||
We really welcome new contributors. If you want to make some code with us, please take a look at the [Good First Tasks](https://github.com/codex-team/editor.js/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+task%22). You can write to us on `team@codex.so` or via special [Telegram chat](https://t.me/editorjsdev), or any other way.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Please visit [https://editorjs.io/](https://editorjs.io) to view all documentation articles.
|
|
||||||
|
|
||||||
- [Base concepts](https://editorjs.io/base-concepts)
|
|
||||||
- [Getting started](https://editorjs.io/getting-started)
|
|
||||||
- [Configuration](https://editorjs.io/configuration)
|
|
||||||
- [How to create a Block Tool Plugin](https://editorjs.io/creating-a-block-tool)
|
|
||||||
- [How to create an Inline Tool Plugin](https://editorjs.io/creating-an-inline-tool)
|
|
||||||
- [API for Tools](https://editorjs.io/tools-api)
|
|
||||||
|
|
||||||
You can join a [Gitter-channel](https://gitter.im/codex-team/editor.js) or [Telegram-chat](//t.me/codex_editor) and ask a question.
|
|
||||||
|
|
||||||
## Changelog
|
|
||||||
|
|
||||||
See the whole [Changelog](/docs/CHANGELOG.md)
|
|
||||||
|
|
||||||
If you want to follow Editor.js updates, [subscribe to our Newsletter](http://digest.editorjs.io/).
|
|
||||||
|
|
||||||
## How to use Editor.js
|
|
||||||
|
|
||||||
### Basics
|
|
||||||
|
|
||||||
Editor.js is a Block-Styled editor. Blocks are structural units, of which the Entry is composed.
|
|
||||||
For example, `Paragraph`, `Heading`, `Image`, `Video`, `List` are Blocks. Each Block is represented by Plugin.
|
|
||||||
We have [many](http://github.com/editor-js/) ready-to-use Plugins and a [simple API](https://editorjs.io/tools-api) for creating new ones.
|
|
||||||
|
|
||||||
How to use the Editor after [Installation](https://editorjs.io/getting-started).
|
|
||||||
|
|
||||||
- Create new Blocks by pressing Enter or clicking the Plus Button
|
|
||||||
- Press `TAB` or click on the Plus Button to view the Toolbox
|
|
||||||
- Press `TAB` again to leaf Toolbox and select a Block you need. Then press Enter.
|
|
||||||
|
|
||||||
|
|
||||||
![](https://github.com/editor-js/list/raw/master/assets/example.gif)
|
|
||||||
|
|
||||||
- Select a text fragment and apply a style or insert a link from the Inline Toolbar
|
|
||||||
|
|
||||||
![](https://capella.pics/7ccbcfcd-1c49-4674-bea7-71021468a1bd.jpg)
|
|
||||||
|
|
||||||
- Use the «three-dots» button on the right to open Block Settings. From here, you can move and delete a Block
|
|
||||||
or apply a Tool's settings, if it provided. For example, you can set a Heading level or List style.
|
|
||||||
|
|
||||||
![](https://capella.pics/01a55381-46cd-47c7-b92e-34765434f2ca.jpg)
|
|
||||||
|
|
||||||
### Shortcuts
|
|
||||||
|
|
||||||
A few shortcuts are preset as available.
|
|
||||||
|
|
||||||
Shortcut | Action | Restrictions
|
|
||||||
-- | -- | --
|
|
||||||
`TAB` | Show/leaf a Toolbox. | On empty block
|
|
||||||
`SHIFT+TAB` | Leaf back a Toolbox. | While Toolbox is opened
|
|
||||||
`ENTER` | Create a Block | While Toolbox is opened and some Tool is selected
|
|
||||||
`CMD+B` | Bold style | On selection
|
|
||||||
`CMD+I` | Italic style | On selection
|
|
||||||
`CMD+K` | Insert a link | On selection
|
|
||||||
|
|
||||||
Each Tool can also have its own shortcuts. These are specified in the configuration of the Tool, for example:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var editor = new EditorJS({
|
|
||||||
//...
|
|
||||||
tools: {
|
|
||||||
header: {
|
|
||||||
class: Header,
|
|
||||||
shortcut: 'CMD+SHIFT+H'
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
class: List,
|
|
||||||
shortcut: 'CMD+SHIFT+L'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//...
|
|
||||||
});
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Installation Guide
|
|
||||||
|
|
||||||
There are few steps to run Editor.js on your site.
|
|
||||||
|
|
||||||
1. [Load Editor's core](#load-editors-core)
|
|
||||||
2. [Load Tools](#load-tools)
|
|
||||||
3. [Initialize Editor's instance](#create-editor-instance)
|
|
||||||
|
|
||||||
### Step 1. Load Editor's core
|
|
||||||
|
|
||||||
Get Editor.js itself. It is a [minified script](dist/editor.js) with Editor's core and some default must-have tools.
|
|
||||||
|
|
||||||
Choose the most usable method of getting Editor for you.
|
|
||||||
|
|
||||||
- Node package
|
|
||||||
- Source from CDN
|
|
||||||
|
|
||||||
##### Option A. NPM install
|
|
||||||
|
|
||||||
Install the package via NPM or Yarn
|
|
||||||
|
|
||||||
```shell
|
|
||||||
npm i @editorjs/editorjs
|
npm i @editorjs/editorjs
|
||||||
```
|
```
|
||||||
|
|
||||||
Include module in your application
|
Choose and install tools:
|
||||||
|
|
||||||
```javascript
|
- [Heading](https://github.com/editor-js/header)
|
||||||
import EditorJS from '@editorjs/editorjs';
|
- [Quote](https://github.com/editor-js/quote)
|
||||||
```
|
- [Image](https://github.com/editor-js/image)
|
||||||
|
- [Simple Image](https://github.com/editor-js/simple-image) (without backend requirement)
|
||||||
|
- [Nested List](https://github.com/editor-js/nested-list)
|
||||||
|
- [Checklist](https://github.com/editor-js/checklist)
|
||||||
|
- [Link embed](https://github.com/editor-js/link)
|
||||||
|
- [Embeds](https://github.com/editor-js/embed) (YouTube, Twitch, Vimeo, Gfycat, Instagram, Twitter, etc)
|
||||||
|
- [Table](https://github.com/editor-js/table)
|
||||||
|
- [Delimiter](https://github.com/editor-js/delimiter)
|
||||||
|
- [Warning](https://github.com/editor-js/warning)
|
||||||
|
- [Code](https://github.com/editor-js/code)
|
||||||
|
- [Raw HTML](https://github.com/editor-js/raw)
|
||||||
|
- [Attaches](https://github.com/editor-js/attaches)
|
||||||
|
- [Marker](https://github.com/editor-js/marker)
|
||||||
|
- [Inline Code](https://github.com/editor-js/inline-code)
|
||||||
|
|
||||||
##### Option B. Use a CDN
|
See the [😎 Awesome Editor.js](https://github.com/editor-js/awesome-editorjs) list for more tools.
|
||||||
|
|
||||||
You can load EditorJS directly from from [jsDelivr CDN](https://www.jsdelivr.com/package/npm/@editorjs/editorjs).
|
Initialize the Editor:
|
||||||
|
|
||||||
`https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest`
|
|
||||||
|
|
||||||
For example, place this in your HTML:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
Or download the bundle file and use it from your server.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="editor.js"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2. Load the Tools that you want to make available
|
|
||||||
|
|
||||||
Each Block is represented by a [Tool](docs/tools.md). Tools are simple external scripts with their own logic. For example, there is a [Header](https://github.com/editor-js/header) Tool into which you type your heading text. If you want to be able to use this, install the Header Tool the same way as the Editor (Node.js, CDN, local file).
|
|
||||||
|
|
||||||
**Example:** use Header from CDN
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/codex.editor.header@2.0.4/dist/bundle.js"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
Check [Editor.js's community](https://github.com/editor-js/) to see more ready-to-use Tools.
|
|
||||||
|
|
||||||
### Step 3. Create Editor instance
|
|
||||||
|
|
||||||
Create an instance of Editor.js and pass [Configuration Object](types/configs/editor-config.d.ts) with `holderId` and tools list.
|
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<div id="editorjs"></div>
|
<div id="editorjs"></div>
|
||||||
```
|
```
|
||||||
|
|
||||||
You can create a simple Editor with only default Paragraph Tool by passing a string with element's Id (wrapper for Editor) as a configuration param. Or use the default `editorjs` id for wrapper.
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var editor = new EditorJS(); /** Zero-configuration */
|
import EditorJS from '@editorjs/editorjs'
|
||||||
|
|
||||||
// equals
|
const editor = new EditorJS({
|
||||||
|
tools: {
|
||||||
var editor = new EditorJS('editorjs');
|
// ... your tools
|
||||||
|
}
|
||||||
|
})
|
||||||
````
|
````
|
||||||
|
|
||||||
Or pass a whole settings object.
|
See details about [Installation](https://editorjs.io/getting-started/) and [Configuration](https://editorjs.io/configuration/) at the documentation.
|
||||||
|
|
||||||
```javascript
|
|
||||||
var editor = new EditorJS({
|
|
||||||
/**
|
|
||||||
* Create a holder for the Editor and pass its ID
|
|
||||||
*/
|
|
||||||
holder : 'editorjs',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Available Tools list.
|
|
||||||
* Pass Tool's class or Settings object for each Tool you want to use
|
|
||||||
*/
|
|
||||||
tools: {
|
|
||||||
header: {
|
|
||||||
class: Header,
|
|
||||||
inlineToolbar : true
|
|
||||||
},
|
|
||||||
// ...
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Previously saved data that should be rendered
|
|
||||||
*/
|
|
||||||
data: {}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Saving Data
|
### Saving Data
|
||||||
|
|
||||||
Call `editor.save()` and handle returned Promise with saved data.
|
Call `editor.save()` and handle returned Promise with saved data.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
editor.save()
|
const data = await editor.save()
|
||||||
.then((savedData) => {
|
|
||||||
console.log(savedData);
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
Take a look at the [example.html](example/example.html) to view more detailed examples.
|
Take a look at the [example.html](example/example.html) to view more detailed examples.
|
||||||
|
|
||||||
## Credits and references
|
|
||||||
|
|
||||||
- We use [HTMLJanitor](https://github.com/guardian/html-janitor) module in our Sanitizer module.
|
## Roadmap
|
||||||
|
|
||||||
|
<img align="right" width="342" src="./assets/roadmap.png" style="margin-left: 30px">
|
||||||
|
|
||||||
|
- Unified Toolbox
|
||||||
|
- [x] Block Tunes moved left
|
||||||
|
- [x] Toolbox becomes vertical
|
||||||
|
- [x] Ability to display several Toolbox buttons by the single Tool
|
||||||
|
- [x] Block Tunes become vertical
|
||||||
|
- [ ] Block Tunes support nested menus
|
||||||
|
- [ ] Conversion Toolbar uses Unified Toolbox
|
||||||
|
- [ ] Conversion Toolbar added to the Block Tunes
|
||||||
|
- Collaborative editing
|
||||||
|
- [ ] Implement Inline Tools JSON format
|
||||||
|
- [ ] Operations Observer, Executor, Manager, Transformer
|
||||||
|
- [ ] Implement Undo/Redo Manager
|
||||||
|
- [ ] Implement Tools API changes
|
||||||
|
- [ ] Implement Server and communication
|
||||||
|
- [ ] Update basic tools to fit the new API
|
||||||
|
- Other features
|
||||||
|
- [ ] Blocks drag'n'drop
|
||||||
|
- [ ] New cross-block selection
|
||||||
|
- [ ] New cross-block caret moving
|
||||||
|
- Ecosystem improvements
|
||||||
|
- [x] CodeX Icons — the way to unify all tools and core icons
|
||||||
|
- [x] New Homepage and Docs
|
||||||
|
- [x] @editorjs/create-tool for Tools bootstrapping
|
||||||
|
- [ ] Editor.js DevTools — stand for core and tools development
|
||||||
|
- [ ] Editor.js Design System
|
||||||
|
- [ ] Editor.js Preset Env
|
||||||
|
- [ ] Editor.js ToolKit
|
||||||
|
- [ ] New core bundle system
|
||||||
|
- [ ] New documentation and guides
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/editorjs/donate" target="_blank">
|
||||||
|
<picture>
|
||||||
|
<source width="162px" media="(prefers-color-scheme: dark)" srcset="./assets/support_night.png">
|
||||||
|
<source width="162px" media="(prefers-color-scheme: light)" srcset="./assets/support_day.png">
|
||||||
|
<img width="162px" alt="Support Editor.js" src="./assets/support_day.png">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Like Editor.js?
|
||||||
|
|
||||||
|
You can support project improvement and development of new features with a donation to our team.
|
||||||
|
|
||||||
|
[Donate via OpenCollective](https://opencollective.com/editorjs)
|
||||||
|
\
|
||||||
|
[Donate via Crypto](https://codex.so/donate)
|
||||||
|
\
|
||||||
|
[Donate via Patreon](https://www.patreon.com/editorjs)
|
||||||
|
|
||||||
|
### Why donate
|
||||||
|
|
||||||
|
Donations to open-source products have several advantages for your business:
|
||||||
|
|
||||||
|
- If your business relies on Editor.js, you'll probably want it to be maintained
|
||||||
|
- It helps Editor.js to evolve and get the new features
|
||||||
|
- We can support contributors and the community around the project. You'll receive well organized docs, guides, etc.
|
||||||
|
- We need to pay for our infrastructure and maintain public resources (domain names, homepages, docs, etc). Supporting it guarantees you to access any resources at the time you need them.
|
||||||
|
- You can advertise by adding your brand assets and mentions on our public resources
|
||||||
|
|
||||||
|
|
||||||
|
### Sponsors
|
||||||
|
|
||||||
|
Support us by becoming a sponsor. Your logo will show up here with a link to your website.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://www.mister-auto.com/" target="_blank">
|
||||||
|
<img src="https://opencollective-production.s3.us-west-1.amazonaws.com/5131a030-5672-11ec-be79-1d003d12ec5f.png" width="50" alt="Mister Auto">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.uplucid.com/" target="_blank">
|
||||||
|
<img src="https://logo.clearbit.com/uplucid.com" width="50" alt="UPLUCID, K.K.">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.contentharmony.com/" target="_blank">
|
||||||
|
<img src="https://opencollective-production.s3.us-west-1.amazonaws.com/89edb1b0-7d82-11ed-b99e-ab6e6f9cb69f.png" width="50" alt="Kane Jamison">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.contentharmony.com/product/" target="_blank">
|
||||||
|
<img src="https://logo.clearbit.com/contentharmony.com" width="50" alt="Content Harmony">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[Become a Sponsor](https://opencollective.com/editorjs/contribute/sir-8679/checkout)
|
||||||
|
|
||||||
|
### Backers
|
||||||
|
Thank you to all our backers
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/editorjs#backers" target="_blank"><img src="https://opencollective.com/editorjs/backers.svg?width=890&avatarHeight=34"></a>
|
||||||
|
|
||||||
|
[Become a Backer](https://opencollective.com/editorjs/contribute/backer-8632/checkout)
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
|
||||||
|
This project exists thanks to all the people who contribute.
|
||||||
|
|
||||||
|
<p><img src="https://opencollective.com/editorjs/contributors.svg?width=890&button=false&avatarHeight=34" /></p>
|
||||||
|
|
||||||
|
### Need something special?
|
||||||
|
|
||||||
|
Hire CodeX experts to resolve technical challenges and match your product requirements.
|
||||||
|
|
||||||
|
- Resolve a problem that has high value for you
|
||||||
|
- Implement a new feature required by your business
|
||||||
|
- Help with integration or tool development
|
||||||
|
- Provide any consultation
|
||||||
|
|
||||||
|
Contact us via team@codex.so and share your details
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
- [Official Tools](https://github.com/editor-js)
|
||||||
|
- [Awesome Editor.js](https://github.com/editor-js/awesome-editorjs)
|
||||||
|
- [Good First Tasks](https://github.com/codex-team/editor.js/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+task%22)
|
||||||
|
- [Contributing](https://editorjs.io/contributing/)
|
||||||
|
- [Telegram Chat](https://t.me/codex_editor)
|
||||||
|
|
||||||
# About CodeX
|
# About CodeX
|
||||||
|
|
||||||
|
|
BIN
assets/logo_day.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
assets/logo_night.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
assets/overview.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/roadmap.png
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
assets/support_day.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
assets/support_night.png
Normal file
After Width: | Height: | Size: 12 KiB |
35
cypress.config.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { defineConfig } from 'cypress';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'test',
|
||||||
|
},
|
||||||
|
fixturesFolder: 'test/cypress/fixtures',
|
||||||
|
screenshotsFolder: 'test/cypress/screenshots',
|
||||||
|
video: false,
|
||||||
|
videosFolder: 'test/cypress/videos',
|
||||||
|
e2e: {
|
||||||
|
// We've imported your old cypress plugins here.
|
||||||
|
// You may want to clean this up later by importing these.
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
on('file:preprocessor', require('cypress-vite')(config));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin for cypress that adds better terminal output for easier debugging.
|
||||||
|
* Prints cy commands, browser console logs, cy.request and cy.intercept data. Great for your pipelines.
|
||||||
|
* https://github.com/archfz/cypress-terminal-report
|
||||||
|
*/
|
||||||
|
require('cypress-terminal-report/src/installLogsPrinter')(on);
|
||||||
|
|
||||||
|
require('./test/cypress/plugins/index.ts')(on, config);
|
||||||
|
},
|
||||||
|
specPattern: 'test/cypress/tests/**/*.cy.{js,jsx,ts,tsx}',
|
||||||
|
supportFile: 'test/cypress/support/index.ts',
|
||||||
|
},
|
||||||
|
'retries': {
|
||||||
|
// Configure retry attempts for `cypress run`
|
||||||
|
'runMode': 2,
|
||||||
|
// Configure retry attempts for `cypress open`
|
||||||
|
'openMode': 0,
|
||||||
|
},
|
||||||
|
});
|
11
cypress.json
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "test"
|
|
||||||
},
|
|
||||||
"fixturesFolder": "test/cypress/fixtures",
|
|
||||||
"integrationFolder": "test/cypress/tests",
|
|
||||||
"screenshotsFolder": "test/cypress/screenshots",
|
|
||||||
"videosFolder": "test/cypress/videos",
|
|
||||||
"supportFile": "test/cypress/support/index.ts",
|
|
||||||
"pluginsFile": "test/cypress/plugins/index.ts"
|
|
||||||
}
|
|
|
@ -1,5 +1,123 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
### 2.30.0
|
||||||
|
|
||||||
|
- `New` – Block Tunes now supports nesting items
|
||||||
|
- `New` – Block Tunes now supports separator items
|
||||||
|
- `New` – "Convert to" control is now also available in Block Tunes
|
||||||
|
- `Improvement` — The ability to merge blocks of different types (if both tools provide the conversionConfig)
|
||||||
|
- `Fix` — `onChange` will be called when removing the entire text within a descendant element of a block.
|
||||||
|
- `Fix` - Unexpected new line on Enter press with selected block without caret
|
||||||
|
- `Fix` - Search input autofocus loosing after Block Tunes opening
|
||||||
|
- `Fix` - Block removing while Enter press on Block Tunes
|
||||||
|
- `Fix` – Unwanted scroll on first typing on iOS devices
|
||||||
|
- `Fix` - Unwanted soft line break on Enter press after period and space (". |") on iOS devices
|
||||||
|
- `Fix` - Caret lost after block conversion on mobile devices.
|
||||||
|
- `Fix` - Caret lost after Backspace at the start of block when previoius block is not convertable
|
||||||
|
- `Improvement` - The API `blocks.convert()` now returns the new block API
|
||||||
|
- `Improvement` - The API `caret.setToBlock()` now can accept either BlockAPI or block index or block id
|
||||||
|
- `New` – *Menu Config* – New item type – HTML
|
||||||
|
– `Refactoring` – Switched to Vite as Cypress bundler
|
||||||
|
– `New` – *Menu Config* – Default and HTML items now support hints
|
||||||
|
|
||||||
|
### 2.29.1
|
||||||
|
|
||||||
|
- `Fix` — Toolbox wont be shown when Slash pressed with along with Shift or Alt
|
||||||
|
- `Fix` — Toolbox will be opened when Slash pressed in non-US keyboard layout where there is no physical '/' key.
|
||||||
|
|
||||||
|
### 2.29.0
|
||||||
|
|
||||||
|
- `New` — Editor Config now has the `style.nonce` attribute that could be used to allowlist editor style tag for Content Security Policy "style-src"
|
||||||
|
- `New` — Toolbox now will be opened by '/' in empty Block instead of Tab
|
||||||
|
- `New` — Block Tunes now will be opened by 'CMD+/' instead of Tab in non-empty block
|
||||||
|
- `New` — Tab now will navigate through Blocks. In last block Tab will navigate to the next input on page.
|
||||||
|
- `Fix` — Passing an empty array via initial data or `blocks.render()` won't break the editor
|
||||||
|
- `Fix` — Layout did not shrink when a large document cleared in Chrome
|
||||||
|
- `Fix` — Multiple Tooltip elements creation fixed
|
||||||
|
- `Fix` — When the focusing Block is out of the viewport, the page will be scrolled.
|
||||||
|
- `Fix` - Compiler error "This import is never used as a value and must use 'import type'..." fixed
|
||||||
|
- `Fix` — `blocks.render()` won't lead the `onChange` call in Safari
|
||||||
|
- `Fix` — Editor wrapper element growing on the Inline Toolbar close
|
||||||
|
- `Fix` — Fix errors thrown by clicks on a document when the editor is being initialized
|
||||||
|
- `Fix` — Caret losing on Mobile Devices when adding a block via Toolbox or via Backspace at the beginning of a Block
|
||||||
|
- `Improvement` — Now you can set focus via arrows/Tab to "contentless" (decorative) blocks like Delimiter which have no inputs.
|
||||||
|
- `Improvement` — Inline Toolbar sometimes opened in an incorrect position. Now it will be aligned by the left side of the selected text. And won't overflow the right side of the text column.
|
||||||
|
- `Improvement` - Now the `data-mutation-free` supports deep nesting, so you can mark some element with it to prevent the onChange call caused by child element mutating
|
||||||
|
- `Improvement` - Now the `data-mutation-free` also allows to skip "characterData" mutations (eg. text content change)
|
||||||
|
- `Refactoring` — `ce-block--focused` class toggling removed as unused.
|
||||||
|
|
||||||
|
### 2.28.2
|
||||||
|
|
||||||
|
- `Fix` — Get rid of redundant logs from the build
|
||||||
|
|
||||||
|
### 2.28.1
|
||||||
|
|
||||||
|
- `Fix` — Some Block were be skipped on saving after pasting them as HTML
|
||||||
|
|
||||||
|
### 2.28.0
|
||||||
|
|
||||||
|
- `New` - Block ids now displayed in DOM via a data-id attribute. Could be useful for plugins that want to access a Block's element by id.
|
||||||
|
- `New` - The `blocks.convert(blockId, newType)` API method was added. It allows to convert existing Block to a Block of another type.
|
||||||
|
- `New` - The `blocks.insertMany()` API method added. It allows to insert several Blocks to the specified index.
|
||||||
|
- `Improvement` - The Delete keydown at the end of the Block will now work opposite a Backspace at the start. Next Block will be removed (if empty) or merged with the current one.
|
||||||
|
- `Improvement` - The Delete keydown will work like a Backspace when several Blocks are selected.
|
||||||
|
- `Improvement` - If we have two empty Blocks, and press Backspace at the start of the second one, the previous will be removed instead of the current.
|
||||||
|
- `Improvement` - Tools shortcuts could be used to convert one Block to another.
|
||||||
|
- `Improvement` - Tools shortcuts displayed in the Conversion Toolbar
|
||||||
|
- `Improvement` - Initialization Loader has been removed.
|
||||||
|
- `Improvement` - Selection style won't override your custom style for `::selection` outside the editor.
|
||||||
|
- `Improvement` - Performance optimizations: initialization speed increased, `blocks.render()` API method optimized. Big documents will be displayed faster.
|
||||||
|
- `Improvement` - "Editor saving" log removed
|
||||||
|
- `Improvement` - "I'm ready" log removed
|
||||||
|
- `Improvement` - The stub-block style is simplified.
|
||||||
|
- `Improvement` - If some Block's tool throws an error during construction, we will show Stub block instead of skipping it during render
|
||||||
|
- `Improvement` - Call of `blocks.clear()` now will trigger onChange with "block-removed" event for all removed blocks.
|
||||||
|
- `Improvement` - The `blocks.clear()` now can be awaited.
|
||||||
|
- `Improvement` - `BlockMutationType` and `BlockMutationEvent` types exported
|
||||||
|
- `Improvement` - `blocks.update(id, data)` now can accept partial data object — it will update only passed properties, others will remain the same.
|
||||||
|
- `Improvement` - `blocks.update(id, data)` now will trigger onChange with only `block-change` event.
|
||||||
|
- `Improvement` - `blocks.update(id, data)` will return a promise with BlockAPI object of the changed block.
|
||||||
|
|
||||||
|
|
||||||
|
### 2.27.2
|
||||||
|
|
||||||
|
- `Fix` - `onChange` won't be called when element with data-mutation-free changes some attribute
|
||||||
|
|
||||||
|
### 2.27.1
|
||||||
|
|
||||||
|
- `Fix` - `onChange` will be called on removing the whole text in a block
|
||||||
|
|
||||||
|
### 2.27.0
|
||||||
|
|
||||||
|
- `New` — *Toolbar API* — Added a new method for toggling the toolbox.
|
||||||
|
- `New` — Added types for block mutation events
|
||||||
|
- `New` — Batching added to the `onChange` callback. Now the second argument can contain an array of CustomEvents as well as a single one. Multiple changes made in a short period of time will be batched under a single `onChange` call.
|
||||||
|
- `Improvement` — *Toolbox* — Number of `close()` method calls optimized.
|
||||||
|
- `Improvement` — The `onChange` callback can be muted if all mutations contain nodes with the `data-mutation-free` attribute.
|
||||||
|
- `Improvement` — Pressing "Enter" at the end of a Block won't lead to redundant `block-changed` event triggering. Only `block-added` event will be dispatched.
|
||||||
|
- `Improvement` — The block mutation handler is now called on every block change (including background changes), instead of only when a block is focused
|
||||||
|
- `Improvement` — Number of caret saving method calls optimized for Block Tunes opening/closing.
|
||||||
|
- `Improvement` — Package size reduced by removing redundant files.
|
||||||
|
- `Refactoring` — Switched from Webpack to Vite as the build system.
|
||||||
|
- `Refactoring` — *Dependencies* — Upgraded Cypress to v12 and related libraries to the latest versions.
|
||||||
|
- `Refactoring` — *Dependencies* — Upgraded TypeScript to v5.
|
||||||
|
- `Refactoring` — `EventDispatcher` types improved. Now we can pass `EventsMap` via generic to specify a map of event names and their payloads that can be used in a particular EventDispatcher instance.
|
||||||
|
- `Refactoring` — All events in common editor Event Bus now have own type declarations.
|
||||||
|
- `Refactoring` — Removed the block mutation observer from blocks and attached a single observer to the editor's blocks wrapper element.
|
||||||
|
- `Refactoring` — Removed the debounce from the block mutation handler and used batching instead.
|
||||||
|
- `Refactoring` — Refactored the popover class for better performance and maintenance.
|
||||||
|
- `Fix` — The `onChange` callback won't trigger when block tunes are opened or closed.
|
||||||
|
- `Fix` — Resolved a compiler error caused by importing the `BlockToolData` type.
|
||||||
|
- `Fix` — Resolved a problem where the document would scroll to the beginning after moving a block above the viewport.
|
||||||
|
- `Fix`- Fixed several bugs caused by browser extensions — Removed the search for a block's container in the DOM on saving and kept it in memory instead, updating it when the tool changes a container element.
|
||||||
|
- `Fix` — *ToolsAPI* — `pasteConfig` getter with `false` value could be used to disable paste handling by Editor.js core. Could be useful if your tool has its own paste handler.
|
||||||
|
- `CI` — Ubuntu container is now used for Edge tests runner.
|
||||||
|
- `CI` — Node 16 is used for GitHib Actions.
|
||||||
|
|
||||||
|
### 2.26.5
|
||||||
|
|
||||||
|
- `Fix` — *Types* — Remove unnecessary import that creates a dependency on the `cypress`.
|
||||||
|
|
||||||
### 2.26.4
|
### 2.26.4
|
||||||
|
|
||||||
- `Improvement` — *Menu Config* — Property `label` renamed to `title`.
|
- `Improvement` — *Menu Config* — Property `label` renamed to `title`.
|
||||||
|
@ -35,6 +153,7 @@
|
||||||
- `Improvement` — *CodeStyle* — [CodeX ESLint Config](https://github.com/codex-team/eslint-config) has bee updated. All ESLint/Spelling issues resolved
|
- `Improvement` — *CodeStyle* — [CodeX ESLint Config](https://github.com/codex-team/eslint-config) has bee updated. All ESLint/Spelling issues resolved
|
||||||
- `Improvement` — *ToolsAPI* — The `icon` property of the `toolbox` getter became optional.
|
- `Improvement` — *ToolsAPI* — The `icon` property of the `toolbox` getter became optional.
|
||||||
|
|
||||||
|
|
||||||
### 2.25.0
|
### 2.25.0
|
||||||
|
|
||||||
- `New` — *Tools API* — Introducing new feature — toolbox now can have multiple entries for one tool! <br>
|
- `New` — *Tools API* — Introducing new feature — toolbox now can have multiple entries for one tool! <br>
|
||||||
|
|
|
@ -180,7 +180,7 @@ this.api.notifier.show({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
![](https://capella.pics/14fcdbe4-d6eb-41d4-b66e-e0e86ccf1a4b.jpg)
|
![](assets/14fcdbe4-d6eb-41d4-b66e-e0e86ccf1a4b.jpg)
|
||||||
|
|
||||||
|
|
||||||
Check out [`codex-notifier` package page](https://github.com/codex-team/js-notifier) on GitHub to find docs, params and examples.
|
Check out [`codex-notifier` package page](https://github.com/codex-team/js-notifier) on GitHub to find docs, params and examples.
|
||||||
|
@ -203,8 +203,6 @@ After executing the `destroy` method, editor inctance becomes an empty object. T
|
||||||
|
|
||||||
Methods for showing Tooltip helper near your elements. Parameters are the same as in [CodeX Tooltips](http://github.com/codex-team/codex.tooltips) lib.
|
Methods for showing Tooltip helper near your elements. Parameters are the same as in [CodeX Tooltips](http://github.com/codex-team/codex.tooltips) lib.
|
||||||
|
|
||||||
![](https://capella.pics/00e7094a-fdb9-429b-8015-9c56f19b4ef5.jpg)
|
|
||||||
|
|
||||||
#### Show
|
#### Show
|
||||||
|
|
||||||
Method shows tooltip with custom content on passed element
|
Method shows tooltip with custom content on passed element
|
||||||
|
|
BIN
docs/assets/01a55381-46cd-47c7-b92e-34765434f2ca.jpg
Normal file
After Width: | Height: | Size: 328 KiB |
BIN
docs/assets/14fcdbe4-d6eb-41d4-b66e-e0e86ccf1a4b.jpg
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
docs/assets/57267bab-f2f0-411b-a9d1-69abee6abab5.jpg
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
docs/assets/6c1f708b-a30c-4ffd-a427-5b59a1a472e0.jpg
Normal file
After Width: | Height: | Size: 404 KiB |
BIN
docs/assets/796de9eb-bbe0-485c-bc8f-9a4cb76641b7.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
docs/assets/79ce946a-d636-41cd-aa96-d3bc5ecfde03.jpg
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
docs/assets/7ccbcfcd-1c49-4674-bea7-71021468a1bd.jpg
Normal file
After Width: | Height: | Size: 137 KiB |
|
@ -69,7 +69,7 @@ Check [Editor.js's community](https://github.com/editor-js/) to see Tools exampl
|
||||||
## Create Editor instance
|
## Create Editor instance
|
||||||
|
|
||||||
Create an instance of Editor.js and pass [Configuration Object](../src/types-internal/editor-config.ts).
|
Create an instance of Editor.js and pass [Configuration Object](../src/types-internal/editor-config.ts).
|
||||||
At least the `holderId` option is required.
|
At least the `holder` option is required.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<div id="editorjs"></div>
|
<div id="editorjs"></div>
|
||||||
|
@ -92,7 +92,7 @@ var editor = new EditorJS({
|
||||||
/**
|
/**
|
||||||
* Create a holder for the Editor and pass its ID
|
* Create a holder for the Editor and pass its ID
|
||||||
*/
|
*/
|
||||||
holderId : 'editorjs',
|
holder : 'editorjs',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Available Tools list.
|
* Available Tools list.
|
||||||
|
|
|
@ -33,7 +33,7 @@ There is a [workflow](.github/workflows/publish-package-to-npm.yml) that fired o
|
||||||
|
|
||||||
Use target version changelog as a description.
|
Use target version changelog as a description.
|
||||||
|
|
||||||
![](https://capella.pics/57267bab-f2f0-411b-a9d1-69abee6abab5.jpg)
|
![](assets/57267bab-f2f0-411b-a9d1-69abee6abab5.jpg)
|
||||||
|
|
||||||
Then you can publish the release and wait for package publishing via action.
|
Then you can publish the release and wait for package publishing via action.
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ This package version will be published to NPM with default `latest` tag.
|
||||||
If you want to publish release candidate version, use suffix `-rc.*` for package
|
If you want to publish release candidate version, use suffix `-rc.*` for package
|
||||||
version in package.json file and in tag on releases page. Workflow will detect it and mark a release as "pre-release".
|
version in package.json file and in tag on releases page. Workflow will detect it and mark a release as "pre-release".
|
||||||
|
|
||||||
![](https://capella.pics/796de9eb-bbe0-485c-bc8f-9a4cb76641b7.jpg)
|
![](assets/796de9eb-bbe0-485c-bc8f-9a4cb76641b7.jpg)
|
||||||
|
|
||||||
This package version will be published to NPM with `next` tag.
|
This package version will be published to NPM with `next` tag.
|
||||||
|
|
||||||
|
|
|
@ -129,8 +129,6 @@ Read more about Sanitizer configuration at the [Tools#sanitize](tools.md#sanitiz
|
||||||
You can pass your Tool's title via `title` static getter. It can be used, for example, in the Tooltip with
|
You can pass your Tool's title via `title` static getter. It can be used, for example, in the Tooltip with
|
||||||
icon description that appears by hover.
|
icon description that appears by hover.
|
||||||
|
|
||||||
![](https://capella.pics/00e7094a-fdb9-429b-8015-9c56f19b4ef5.jpg)
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export default class BoldInlineTool implements InlineTool {
|
export default class BoldInlineTool implements InlineTool {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -70,7 +70,7 @@ to the `tools` property of Editor Config.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var editor = new EditorJS({
|
var editor = new EditorJS({
|
||||||
holderId : 'editorjs',
|
holder : 'editorjs',
|
||||||
tools: {
|
tools: {
|
||||||
text: {
|
text: {
|
||||||
class: Text,
|
class: Text,
|
||||||
|
@ -410,7 +410,7 @@ static get sanitize() {
|
||||||
|
|
||||||
Editor.js has a Conversion Toolbar that allows user to convert one Block to another.
|
Editor.js has a Conversion Toolbar that allows user to convert one Block to another.
|
||||||
|
|
||||||
![](https://capella.pics/6c1f708b-a30c-4ffd-a427-5b59a1a472e0.jpg)
|
![](assets/6c1f708b-a30c-4ffd-a427-5b59a1a472e0.jpg)
|
||||||
|
|
||||||
1. You can add ability to your Tool to be converted. Specify «export» property of `conversionConfig`.
|
1. You can add ability to your Tool to be converted. Specify «export» property of `conversionConfig`.
|
||||||
2. You can add ability to convert other Tools to your Tool. Specify «import» property of `conversionConfig`.
|
2. You can add ability to convert other Tools to your Tool. Specify «import» property of `conversionConfig`.
|
||||||
|
|
|
@ -17,12 +17,12 @@ So how to use the Editor after [Installation](installation.md).
|
||||||
|
|
||||||
- Select text fragment and apply a style or insert a link from the Inline Toolbar
|
- Select text fragment and apply a style or insert a link from the Inline Toolbar
|
||||||
|
|
||||||
![](https://capella.pics/7ccbcfcd-1c49-4674-bea7-71021468a1bd.jpg)
|
![](assets/7ccbcfcd-1c49-4674-bea7-71021468a1bd.jpg)
|
||||||
|
|
||||||
- Use «three-dots» button on the right to open Block Settings. From here, you can move and delete a Block
|
- Use «three-dots» button on the right to open Block Settings. From here, you can move and delete a Block
|
||||||
or apply Tool's settings, if it provided. For example, set a Heading level or List style.
|
or apply Tool's settings, if it provided. For example, set a Heading level or List style.
|
||||||
|
|
||||||
![](https://capella.pics/01a55381-46cd-47c7-b92e-34765434f2ca.jpg)
|
![](assets/01a55381-46cd-47c7-b92e-34765434f2ca.jpg)
|
||||||
|
|
||||||
## Shortcuts
|
## Shortcuts
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Editor.js 🤩🧦🤨 example</title>
|
<title>Editor.js 🤩🧦🤨 example</title>
|
||||||
<link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
|
||||||
<link href="assets/demo.css" rel="stylesheet">
|
<link href="../public/assets/demo.css" rel="stylesheet">
|
||||||
<script src="assets/json-preview.js"></script>
|
<script src="../public/assets/json-preview.js"></script>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -89,25 +89,25 @@
|
||||||
Read more in Tool's README file. For example:
|
Read more in Tool's README file. For example:
|
||||||
https://github.com/editor-js/header#installation
|
https://github.com/editor-js/header#installation
|
||||||
-->
|
-->
|
||||||
<script src="./tools/header/dist/bundle.js" onload="document.getElementById('hint-tools').hidden = true"></script><!-- Header -->
|
<script src="./tools/header/dist/header.umd.js" onload="document.getElementById('hint-tools').hidden = true"></script><!-- Header -->
|
||||||
<script src="./tools/simple-image/dist/bundle.js"></script><!-- Image -->
|
<script src="./tools/simple-image/dist/simple-image.umd.js"></script><!-- Image -->
|
||||||
<script src="./tools/delimiter/dist/bundle.js"></script><!-- Delimiter -->
|
<script src="./tools/delimiter/dist/delimiter.umd.js"></script><!-- Delimiter -->
|
||||||
<!-- <script src="./tools/list/dist/bundle.js"></script> List-->
|
<!-- <script src="./tools/list/dist/list.umd.js"></script> List-->
|
||||||
<script src="./tools/nested-list/dist/nested-list.js"></script><!-- Nested List -->
|
<script src="./tools/nested-list/dist/nested-list.umd.js"></script><!-- Nested List -->
|
||||||
<script src="./tools/checklist/dist/bundle.js"></script><!-- Checklist -->
|
<script src="./tools/checklist/dist/checklist.umd.js"></script><!-- Checklist -->
|
||||||
<script src="./tools/quote/dist/bundle.js"></script><!-- Quote -->
|
<script src="./tools/quote/dist/quote.umd.js"></script><!-- Quote -->
|
||||||
<script src="./tools/code/dist/bundle.js"></script><!-- Code -->
|
<script src="./tools/code/dist/code.umd.js"></script><!-- Code -->
|
||||||
<script src="./tools/embed/dist/bundle.js"></script><!-- Embed -->
|
<script src="./tools/embed/dist/embed.umd.js"></script><!-- Embed -->
|
||||||
<script src="./tools/table/dist/table.js"></script><!-- Table -->
|
<script src="./tools/table/dist/table.umd.js"></script><!-- Table -->
|
||||||
<script src="./tools/link/dist/bundle.js"></script><!-- Link -->
|
<script src="./tools/link/dist/link.umd.js"></script><!-- Link -->
|
||||||
<script src="./tools/raw/dist/bundle.js"></script><!-- Raw -->
|
<script src="./tools/raw/dist/raw.umd.js"></script><!-- Raw -->
|
||||||
<script src="./tools/warning/dist/bundle.js"></script><!-- Warning -->
|
<script src="./tools/warning/dist/warning.umd.js"></script><!-- Warning -->
|
||||||
|
|
||||||
<script src="./tools/marker/dist/bundle.js"></script><!-- Marker -->
|
<script src="./tools/marker/dist/marker.umd.js"></script><!-- Marker -->
|
||||||
<script src="./tools/inline-code/dist/bundle.js"></script><!-- Inline Code -->
|
<script src="./tools/inline-code/dist/inline-code.umd.js"></script><!-- Inline Code -->
|
||||||
|
|
||||||
<!-- Load Editor.js's Core -->
|
<!-- Load Editor.js's Core -->
|
||||||
<script src="../dist/editor.js" onload="document.getElementById('hint-core').hidden = true;"></script>
|
<script src="../dist/editorjs.umd.js" onload="document.getElementById('hint-core').hidden = true;"></script>
|
||||||
|
|
||||||
<!-- Initialization -->
|
<!-- Initialization -->
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Editor.js 🤩🧦🤨 example</title>
|
<title>Editor.js 🤩🧦🤨 example</title>
|
||||||
<link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
|
||||||
<link href="assets/demo.css" rel="stylesheet">
|
<link href="../public/assets/demo.css" rel="stylesheet">
|
||||||
<script src="assets/json-preview.js"></script>
|
<script src="../public/assets/json-preview.js"></script>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/inline-code@latest"></script><!-- Inline Code -->
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/inline-code@latest"></script><!-- Inline Code -->
|
||||||
|
|
||||||
<!-- Load Editor.js's Core -->
|
<!-- Load Editor.js's Core -->
|
||||||
<script src="../dist/editor.js" onload="document.getElementById('hint-core').hidden = true"></script>
|
<script src="../dist/editorjs.umd.js" onload="document.getElementById('hint-core').hidden = true"></script>
|
||||||
|
|
||||||
<!-- Initialization -->
|
<!-- Initialization -->
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Editor.js 🤩🧦🤨 example: Multiple instances</title>
|
<title>Editor.js 🤩🧦🤨 example: Multiple instances</title>
|
||||||
<link href="assets/demo.css" rel="stylesheet">
|
<link href="../public/assets/demo.css" rel="stylesheet">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Load Editor.js's Core -->
|
<!-- Load Editor.js's Core -->
|
||||||
<script src="../dist/editor.js" onload="document.getElementById('hint-core').hidden = true"></script>
|
<script src="../dist/editorjs.umd.js" onload="document.getElementById('hint-core').hidden = true"></script>
|
||||||
<script src="./tools/header/dist/bundle.js"></script><!-- Header -->
|
<script src="./tools/header/dist/bundle.js"></script><!-- Header -->
|
||||||
|
|
||||||
<!-- Initialization -->
|
<!-- Initialization -->
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Editor.js 🤩🧦🤨 example: Popup</title>
|
<title>Editor.js 🤩🧦🤨 example: Popup</title>
|
||||||
<link href="assets/demo.css" rel="stylesheet">
|
<link href="../public/assets/demo.css" rel="stylesheet">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -111,7 +111,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Load Editor.js's Core -->
|
<!-- Load Editor.js's Core -->
|
||||||
<script src="../dist/editor.js" onload="document.getElementById('hint-core').hidden = true"></script>
|
<script src="../dist/editorjs.umd.js" onload="document.getElementById('hint-core').hidden = true"></script>
|
||||||
<script src="./tools/header/dist/bundle.js"></script><!-- Header -->
|
<script src="./tools/header/dist/bundle.js"></script><!-- Header -->
|
||||||
|
|
||||||
<!-- Initialization -->
|
<!-- Initialization -->
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Editor.js RTL example</title>
|
<title>Editor.js RTL example</title>
|
||||||
<link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
|
||||||
<link href="assets/demo.css" rel="stylesheet">
|
<link href="../public/assets/demo.css" rel="stylesheet">
|
||||||
<script src="assets/json-preview.js"></script>
|
<script src="../public/assets/json-preview.js"></script>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
<script src="./tools/inline-code/dist/bundle.js"></script><!-- Inline Code -->
|
<script src="./tools/inline-code/dist/bundle.js"></script><!-- Inline Code -->
|
||||||
|
|
||||||
<!-- Load Editor.js's Core -->
|
<!-- Load Editor.js's Core -->
|
||||||
<script src="../dist/editor.js" onload="document.getElementById('hint-core').hidden = true"></script>
|
<script src="../dist/editorjs.umd.js" onload="document.getElementById('hint-core').hidden = true"></script>
|
||||||
|
|
||||||
<!-- Initialization -->
|
<!-- Initialization -->
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Editor.js 🤩🧦🤨 example</title>
|
<title>Editor.js 🤩🧦🤨 example</title>
|
||||||
<link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
|
||||||
<link href="assets/demo.css" rel="stylesheet">
|
<link href="../public/assets/demo.css" rel="stylesheet">
|
||||||
<script src="assets/json-preview.js"></script>
|
<script src="../public/assets/json-preview.js"></script>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 4d96a39d38011f4e0134934673ba4b2eb334f279
|
Subproject commit 1c116d5e09e19951948d6166047aa2f30877aaf9
|
|
@ -1 +1 @@
|
||||||
Subproject commit 193f5f6f00288679a97bfe620a4d811e5acd9b16
|
Subproject commit f281996f82c7ac676172757e45687cae27443427
|
|
@ -1 +1 @@
|
||||||
Subproject commit 86e8c5501dcbb8eaaeec756e1145db49b8339160
|
Subproject commit 4ca1c1c972261f47dd34f6b8754763a4a79a4866
|
|
@ -1 +1 @@
|
||||||
Subproject commit 23de06be69bb9e636a2278b0d54f8c2d85d7ae13
|
Subproject commit dfdbf2423d2777f7026a7df768c6582e1a409db7
|
|
@ -1 +1 @@
|
||||||
Subproject commit 80278ee75146ff461e9dcaeff1a337167ef97162
|
Subproject commit 5118ce87a752515fb6b31325f234f4ccd62f42c9
|
|
@ -1 +1 @@
|
||||||
Subproject commit 5ee4e1ef338493b82a11fd76a3568844afe1c78b
|
Subproject commit 25d46cd8d3930851b14ddc26ee80fb5b485e1496
|
|
@ -1 +1 @@
|
||||||
Subproject commit 7cc94718e4c20d6f9db2c236a60b119c39d389e0
|
Subproject commit dcd4c17740c9ba636140751596aff1e9f6ef6b01
|
|
@ -1 +1 @@
|
||||||
Subproject commit 861de29b1d553bb9377dcbaf451af605b28b57bd
|
Subproject commit aaa69d5408bad34027d6252a3892d40f9fa121be
|
|
@ -1 +1 @@
|
||||||
Subproject commit f0e9f0110983cd973a1345f2885b18db4fd54636
|
Subproject commit a6dc6a692b88c9eff3d87223b239e7517b160c67
|
|
@ -1 +1 @@
|
||||||
Subproject commit 13e0b1cf72cfa706dc236e617683a5e349a021f5
|
Subproject commit 8d6897fca43e387bcdf4a681380be975fe8f2a07
|
|
@ -1 +1 @@
|
||||||
Subproject commit 53a12f3d816630e071ef8230c4a5870a7c0d0551
|
Subproject commit 95b37462dc93c19b83f0481f509034a40d436cf2
|
|
@ -1 +1 @@
|
||||||
Subproject commit 02e0db32a101ec5cfa61210de45be7de647c40c6
|
Subproject commit 9377ca713f552576b8b11f77cf371b67261ec00b
|
|
@ -1 +1 @@
|
||||||
Subproject commit b4164eac4d81259a15368d7681884e3554554662
|
Subproject commit cae470fded570ef9a82a45734526ccf45959e204
|
|
@ -1 +1 @@
|
||||||
Subproject commit e1275bdabbae5d933215bf3cbaeddc0c0fffd9a4
|
Subproject commit 963883520c7bbe5040366335c9a37bbdc7cf60fd
|
|
@ -1 +1 @@
|
||||||
Subproject commit ff9bf7d3627b463d88aed99313e87b495a4211fc
|
Subproject commit 2948cd7595e632f7555e2dc09e6bac050a2b87ea
|
|
@ -1 +1 @@
|
||||||
Subproject commit e2726a7b301c960d318aa1ec73bac97f474e3d68
|
Subproject commit e63e91aa833d774be9bf4a76013b1025a009989d
|
413
index.html
Normal file
|
@ -0,0 +1,413 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Editor.js 🤩🧦🤨 example</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
|
||||||
|
<link href="/assets/demo.css" rel="stylesheet">
|
||||||
|
<script src="/assets/json-preview.js"></script>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
if (localStorage.getItem('theme') === 'dark') {
|
||||||
|
document.body.classList.add("dark-mode");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<div class="ce-example">
|
||||||
|
<div class="ce-example__header">
|
||||||
|
<a class="ce-example__header-logo" href="https://codex.so/editor">Editor.js 🤩🧦🤨</a>
|
||||||
|
|
||||||
|
<div class="ce-example__header-menu">
|
||||||
|
<a href="https://github.com/editor-js" target="_blank">Plugins</a>
|
||||||
|
<a href="https://editorjs.io/usage" target="_blank">Usage</a>
|
||||||
|
<a href="https://editorjs.io/configuration" target="_blank">Configuration</a>
|
||||||
|
<a href="https://editorjs.io/creating-a-block-tool" target="_blank">API</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ce-example__content">
|
||||||
|
<div id="editorjs"></div>
|
||||||
|
<div class="ce-example__button" id="saveButton">
|
||||||
|
editor.save()
|
||||||
|
</div>
|
||||||
|
<div class="ce-example__statusbar">
|
||||||
|
<div class="ce-example__statusbar-item">
|
||||||
|
Readonly:
|
||||||
|
<b id="readonly-state">
|
||||||
|
Off
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<div class="ce-example__statusbar-button" id="toggleReadOnlyButton">
|
||||||
|
toggle
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ce-example__statusbar-item">
|
||||||
|
<div class="ce-example__statusbar-button" id="showBlocksBoundariesButton">
|
||||||
|
<span data-toggled-text="Hide">Show</span>
|
||||||
|
blocks boundaries
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ce-example__statusbar-item">
|
||||||
|
<div class="ce-example__statusbar-button" id="enableThinModeButton">
|
||||||
|
<span data-toggled-text="Disable">Enable</span>
|
||||||
|
thin mode
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ce-example__statusbar-item ce-example__statusbar-item--right">
|
||||||
|
<div class="ce-example__statusbar-toggler" id="darkThemeToggler">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ce-example__output">
|
||||||
|
<pre class="ce-example__output-content" id="output"></pre>
|
||||||
|
|
||||||
|
<div class="ce-example__output-footer">
|
||||||
|
<a href="https://codex.so" style="font-weight: bold;">Made by CodeX</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Load Tools -->
|
||||||
|
<!--
|
||||||
|
You can upload Tools to your project's directory and connect them by relative links.
|
||||||
|
|
||||||
|
Also you can load each Tool from CDN or use NPM/Yarn packages.
|
||||||
|
|
||||||
|
Read more at Tools Connection doc:
|
||||||
|
https://editorjs.io/getting-started#tools-connection
|
||||||
|
-->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script><!-- Header -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest"></script><!-- Image -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest"></script><!-- Delimiter -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/nested-list@latest"></script><!-- List -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest"></script><!-- Checklist -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"></script><!-- Quote -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/code@latest"></script><!-- Code -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script><!-- Embed -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/table@latest"></script><!-- Table -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/link@latest"></script><!-- Link -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/warning@latest"></script><!-- Warning -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/raw@latest"></script><!-- Raw -->
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@latest"></script><!-- Marker -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/inline-code@latest"></script><!-- Inline Code -->
|
||||||
|
|
||||||
|
<!-- Initialization -->
|
||||||
|
<script type="module">
|
||||||
|
import EditorJS from './src/codex.ts';
|
||||||
|
|
||||||
|
window.EditorJS = EditorJS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To initialize the Editor, create a new instance with configuration object
|
||||||
|
* @see docs/installation.md for mode details
|
||||||
|
*/
|
||||||
|
const editorConfig = {
|
||||||
|
/**
|
||||||
|
* Enable/Disable the read only mode
|
||||||
|
*/
|
||||||
|
readOnly: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper of Editor
|
||||||
|
*/
|
||||||
|
holder: 'editorjs',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common Inline Toolbar settings
|
||||||
|
* - if true (or not specified), the order from 'tool' property will be used
|
||||||
|
* - if an array of tool names, this order will be used
|
||||||
|
*/
|
||||||
|
// inlineToolbar: ['link', 'marker', 'bold', 'italic'],
|
||||||
|
// inlineToolbar: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tools list
|
||||||
|
*/
|
||||||
|
tools: {
|
||||||
|
/**
|
||||||
|
* Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}
|
||||||
|
*/
|
||||||
|
header: {
|
||||||
|
class: Header,
|
||||||
|
inlineToolbar: ['marker', 'link'],
|
||||||
|
config: {
|
||||||
|
placeholder: 'Header'
|
||||||
|
},
|
||||||
|
shortcut: 'CMD+SHIFT+H'
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Or pass class directly without any configuration
|
||||||
|
*/
|
||||||
|
image: SimpleImage,
|
||||||
|
|
||||||
|
list: {
|
||||||
|
class: NestedList,
|
||||||
|
inlineToolbar: true,
|
||||||
|
shortcut: 'CMD+SHIFT+L'
|
||||||
|
},
|
||||||
|
|
||||||
|
checklist: {
|
||||||
|
class: Checklist,
|
||||||
|
inlineToolbar: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
quote: {
|
||||||
|
class: Quote,
|
||||||
|
inlineToolbar: true,
|
||||||
|
config: {
|
||||||
|
quotePlaceholder: 'Enter a quote',
|
||||||
|
captionPlaceholder: 'Quote\'s author',
|
||||||
|
},
|
||||||
|
shortcut: 'CMD+SHIFT+O'
|
||||||
|
},
|
||||||
|
|
||||||
|
warning: Warning,
|
||||||
|
|
||||||
|
marker: {
|
||||||
|
class: Marker,
|
||||||
|
shortcut: 'CMD+SHIFT+M'
|
||||||
|
},
|
||||||
|
|
||||||
|
code: {
|
||||||
|
class: CodeTool,
|
||||||
|
shortcut: 'CMD+SHIFT+C'
|
||||||
|
},
|
||||||
|
|
||||||
|
delimiter: Delimiter,
|
||||||
|
|
||||||
|
inlineCode: {
|
||||||
|
class: InlineCode,
|
||||||
|
shortcut: 'CMD+SHIFT+C'
|
||||||
|
},
|
||||||
|
|
||||||
|
linkTool: LinkTool,
|
||||||
|
|
||||||
|
raw: RawTool,
|
||||||
|
|
||||||
|
embed: Embed,
|
||||||
|
|
||||||
|
table: {
|
||||||
|
class: Table,
|
||||||
|
inlineToolbar: true,
|
||||||
|
shortcut: 'CMD+ALT+T'
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Tool will be used as default
|
||||||
|
*/
|
||||||
|
// defaultBlock: 'paragraph',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial Editor data
|
||||||
|
*/
|
||||||
|
data: {
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: "zcKCF1S7X8",
|
||||||
|
type: "header",
|
||||||
|
data: {
|
||||||
|
text: "Editor.js",
|
||||||
|
level: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b6ji-DvaKb",
|
||||||
|
type: "paragraph",
|
||||||
|
data: {
|
||||||
|
text: "Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "header",
|
||||||
|
id: "7ItVl5biRo",
|
||||||
|
data: {
|
||||||
|
text: "Key features",
|
||||||
|
level: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type : 'list',
|
||||||
|
id: "SSBSguGvP7",
|
||||||
|
data : {
|
||||||
|
items : [
|
||||||
|
{
|
||||||
|
content: 'It is a block-styled editor',
|
||||||
|
items: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'It returns clean data output in JSON',
|
||||||
|
items: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'Designed to be extendable and pluggable with a simple API',
|
||||||
|
items: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
style: 'unordered'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "header",
|
||||||
|
id: "QZFox1m_ul",
|
||||||
|
data: {
|
||||||
|
text: "What does it mean «block-styled editor»",
|
||||||
|
level: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type : 'paragraph',
|
||||||
|
id: "bwnFX5LoX7",
|
||||||
|
data : {
|
||||||
|
text : 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js <mark class=\"cdx-marker\">workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc</mark>. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\'s Core.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type : 'paragraph',
|
||||||
|
id: "mTrPOHAQTe",
|
||||||
|
data : {
|
||||||
|
text : `There are dozens of <a href="https://github.com/editor-js">ready-to-use Blocks</a> and the <a href="https://editorjs.io/creating-a-block-tool">simple API</a> for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games.`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "header",
|
||||||
|
id: "1sYMhUrznu",
|
||||||
|
data: {
|
||||||
|
text: "What does it mean clean data output",
|
||||||
|
level: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type : 'paragraph',
|
||||||
|
id: "jpd7WEXrJG",
|
||||||
|
data : {
|
||||||
|
text : 'Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type : 'paragraph',
|
||||||
|
id: "0lOGNUKxqt",
|
||||||
|
data : {
|
||||||
|
text : `Given data can be used as you want: render with HTML for <code class="inline-code">Web clients</code>, render natively for <code class="inline-code">mobile apps</code>, create markup for <code class="inline-code">Facebook Instant Articles</code> or <code class="inline-code">Google AMP</code>, generate an <code class="inline-code">audio version</code> and so on.`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type : 'paragraph',
|
||||||
|
id: "WvX7kBjp0I",
|
||||||
|
data : {
|
||||||
|
text : 'Clean data is useful to sanitize, validate and process on the backend.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type : 'delimiter',
|
||||||
|
id: "H9LWKQ3NYd",
|
||||||
|
data : {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type : 'paragraph',
|
||||||
|
id: "h298akk2Ad",
|
||||||
|
data : {
|
||||||
|
text : 'We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make its core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
id: "9802bjaAA2",
|
||||||
|
data: {
|
||||||
|
url: '/assets/codex2x.png',
|
||||||
|
caption: '',
|
||||||
|
stretched: false,
|
||||||
|
withBorder: true,
|
||||||
|
withBackground: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
onReady: function(){
|
||||||
|
saveButton.click();
|
||||||
|
},
|
||||||
|
onChange: function(api, event) {
|
||||||
|
console.log('something changed', event);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* To initialize the Editor, create a new instance with configuration object
|
||||||
|
* @see docs/installation.md for mode details
|
||||||
|
*/
|
||||||
|
var editor = new EditorJS(editorConfig);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saving button
|
||||||
|
*/
|
||||||
|
const saveButton = document.getElementById('saveButton');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle read-only button
|
||||||
|
*/
|
||||||
|
const toggleReadOnlyButton = document.getElementById('toggleReadOnlyButton');
|
||||||
|
const readOnlyIndicator = document.getElementById('readonly-state');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saving example
|
||||||
|
*/
|
||||||
|
saveButton.addEventListener('click', function () {
|
||||||
|
editor.save()
|
||||||
|
.then((savedData) => {
|
||||||
|
cPreview.show(savedData, document.getElementById("output"));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Saving error', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle read-only example
|
||||||
|
*/
|
||||||
|
toggleReadOnlyButton.addEventListener('click', async () => {
|
||||||
|
const readOnlyState = await editor.readOnly.toggle();
|
||||||
|
|
||||||
|
readOnlyIndicator.textContent = readOnlyState ? 'On' : 'Off';
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button for displaying blocks borders. Useful for UI development
|
||||||
|
*/
|
||||||
|
const showBlocksBoundariesButton = document.getElementById("showBlocksBoundariesButton");
|
||||||
|
|
||||||
|
showBlocksBoundariesButton.addEventListener('click', () => {
|
||||||
|
document.body.classList.toggle("show-block-boundaries")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button for enabling the 'Thin' mode
|
||||||
|
*/
|
||||||
|
const enableThinModeButton = document.getElementById("enableThinModeButton");
|
||||||
|
|
||||||
|
enableThinModeButton.addEventListener('click', () => {
|
||||||
|
document.body.classList.toggle("thin-mode")
|
||||||
|
|
||||||
|
editor.destroy();
|
||||||
|
|
||||||
|
editor = new EditorJS(editorConfig);
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggler for toggling the dark mode
|
||||||
|
*/
|
||||||
|
const darkThemeToggler = document.getElementById("darkThemeToggler");
|
||||||
|
|
||||||
|
darkThemeToggler.addEventListener('click', () => {
|
||||||
|
document.body.classList.toggle("dark-mode");
|
||||||
|
|
||||||
|
localStorage.setItem('theme', document.body.classList.contains("dark-mode") ? 'dark' : 'default');
|
||||||
|
})
|
||||||
|
|
||||||
|
window.editor = editor;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
91
package.json
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "@editorjs/editorjs",
|
"name": "@editorjs/editorjs",
|
||||||
"version": "2.26.4",
|
"version": "2.30.0-rc.10",
|
||||||
"description": "Editor.js — Native JS, based on API and Open Source",
|
"description": "Editor.js — Native JS, based on API and Open Source",
|
||||||
"main": "dist/editor.js",
|
"main": "dist/editorjs.umd.js",
|
||||||
|
"module": "dist/editorjs.mjs",
|
||||||
"types": "./types/index.d.ts",
|
"types": "./types/index.d.ts",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"codex editor",
|
"codex editor",
|
||||||
|
@ -12,16 +13,13 @@
|
||||||
"editorjs"
|
"editorjs"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clear": "rimraf dist && mkdirp dist",
|
"dev": "vite",
|
||||||
"build": "yarn clear && yarn build:webpack:prod",
|
"build": "vite build --mode production",
|
||||||
"build:dev": "yarn clear && yarn build:webpack:dev",
|
"build:test": "vite build --mode test",
|
||||||
"build:webpack:dev": "webpack --mode development --progress --display-error-details --display-entrypoints --watch",
|
|
||||||
"build:webpack:prod": "webpack --mode production",
|
|
||||||
"lint": "eslint src/ --ext .ts && yarn lint:tests",
|
"lint": "eslint src/ --ext .ts && yarn lint:tests",
|
||||||
"lint:errors": "eslint src/ --ext .ts --quiet",
|
"lint:errors": "eslint src/ --ext .ts --quiet",
|
||||||
"lint:fix": "eslint src/ --ext .ts --fix",
|
"lint:fix": "eslint src/ --ext .ts --fix",
|
||||||
"lint:tests": "eslint test/ --ext .ts",
|
"lint:tests": "eslint test/ --ext .ts",
|
||||||
"ci:pull_paragraph": "git submodule update --init ./src/tools/paragraph",
|
|
||||||
"pull_tools": "git submodule update --init --recursive",
|
"pull_tools": "git submodule update --init --recursive",
|
||||||
"_tools:checkout": "git submodule foreach \"git checkout master || git checkout main\"",
|
"_tools:checkout": "git submodule foreach \"git checkout master || git checkout main\"",
|
||||||
"_tools:pull": "git submodule foreach git pull",
|
"_tools:pull": "git submodule foreach git pull",
|
||||||
|
@ -29,8 +27,8 @@
|
||||||
"_tools:build": "git submodule foreach yarn build",
|
"_tools:build": "git submodule foreach yarn build",
|
||||||
"_tools:make": "yarn _tools:yarn && yarn _tools:build",
|
"_tools:make": "yarn _tools:yarn && yarn _tools:build",
|
||||||
"tools:update": "yarn _tools:checkout && yarn _tools:pull && yarn _tools:make",
|
"tools:update": "yarn _tools:checkout && yarn _tools:pull && yarn _tools:make",
|
||||||
"test:e2e": "yarn build && cypress run",
|
"test:e2e": "yarn build:test && cypress run",
|
||||||
"test:e2e:open": "yarn build && cypress open",
|
"test:e2e:open": "yarn build:test && cypress open",
|
||||||
"devserver:start": "yarn build && node ./devserver.js"
|
"devserver:start": "yarn build && node ./devserver.js"
|
||||||
},
|
},
|
||||||
"author": "CodeX",
|
"author": "CodeX",
|
||||||
|
@ -40,64 +38,43 @@
|
||||||
"url": "git+https://github.com/codex-team/editor.js.git"
|
"url": "git+https://github.com/codex-team/editor.js.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.9.0",
|
"@babel/register": "^7.21.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.9.0",
|
"@codexteam/icons": "^0.3.0",
|
||||||
"@babel/polyfill": "^7.8.7",
|
|
||||||
"@babel/preset-env": "^7.9.5",
|
|
||||||
"@babel/preset-typescript": "^7.13.0",
|
|
||||||
"@babel/register": "^7.9.0",
|
|
||||||
"@babel/runtime": "^7.9.2",
|
|
||||||
"@codexteam/shortcuts": "^1.1.1",
|
"@codexteam/shortcuts": "^1.1.1",
|
||||||
"@cypress/code-coverage": "^3.9.2",
|
"@cypress/code-coverage": "^3.10.3",
|
||||||
"@cypress/webpack-preprocessor": "^5.6.0",
|
|
||||||
"@editorjs/code": "^2.7.0",
|
"@editorjs/code": "^2.7.0",
|
||||||
"@editorjs/delimiter": "^1.2.0",
|
"@editorjs/delimiter": "^1.2.0",
|
||||||
"@editorjs/header": "^2.6.1",
|
"@editorjs/header": "^2.7.0",
|
||||||
|
"@editorjs/paragraph": "^2.11.4",
|
||||||
"@editorjs/simple-image": "^1.4.1",
|
"@editorjs/simple-image": "^1.4.1",
|
||||||
"@types/node": "^14.14.35",
|
"@types/node": "^18.15.11",
|
||||||
"@types/webpack": "^4.41.12",
|
"chai-subset": "^1.6.0",
|
||||||
"@types/webpack-env": "^1.15.2",
|
"codex-notifier": "^1.1.2",
|
||||||
"babel-loader": "^8.1.0",
|
"codex-tooltip": "^1.0.5",
|
||||||
"babel-plugin-add-module-exports": "^1.0.0",
|
"core-js": "3.30.0",
|
||||||
"babel-plugin-class-display-name": "^2.1.0",
|
"cypress": "^13.7.1",
|
||||||
"babel-plugin-istanbul": "^6.0.0",
|
"cypress-intellij-reporter": "^0.0.7",
|
||||||
"core-js": "3.6.5",
|
"cypress-plugin-tab": "^1.0.5",
|
||||||
"css-loader": "^3.5.3",
|
"cypress-terminal-report": "^5.3.2",
|
||||||
"cssnano": "^4.1.10",
|
"cypress-vite": "^1.5.0",
|
||||||
"cypress": "^6.8.0",
|
"eslint": "^8.37.0",
|
||||||
"cypress-intellij-reporter": "^0.0.6",
|
|
||||||
"eslint": "^8.28.0",
|
|
||||||
"eslint-config-codex": "^1.7.1",
|
"eslint-config-codex": "^1.7.1",
|
||||||
"eslint-loader": "^4.0.2",
|
|
||||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||||
"eslint-plugin-cypress": "^2.12.1",
|
"eslint-plugin-cypress": "2.12.1",
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
"html-janitor": "^2.0.4",
|
||||||
"license-webpack-plugin": "^2.1.4",
|
"nanoid": "^4.0.2",
|
||||||
"mkdirp": "^1.0.4",
|
|
||||||
"postcss-apply": "^0.12.0",
|
"postcss-apply": "^0.12.0",
|
||||||
"postcss-import": "^12.0.1",
|
"postcss-nested": "4.1.2",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-preset-env": "^8.3.0",
|
||||||
"postcss-nested": "^4.1.2",
|
"rollup-plugin-license": "^3.0.1",
|
||||||
"postcss-nested-ancestors": "^2.0.0",
|
"stylelint": "^15.4.0",
|
||||||
"postcss-preset-env": "^6.6.0",
|
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"stylelint": "^13.3.3",
|
|
||||||
"terser-webpack-plugin": "^2.3.6",
|
|
||||||
"ts-loader": "^7.0.1",
|
|
||||||
"tslint": "^6.1.1",
|
"tslint": "^6.1.1",
|
||||||
"typescript": "3.8.3",
|
"typescript": "5.0.3",
|
||||||
"webpack": "^4.43.0",
|
"vite": "^4.2.1",
|
||||||
"webpack-cli": "^3.3.11"
|
"vite-plugin-css-injected-by-js": "^3.1.0"
|
||||||
},
|
},
|
||||||
"collective": {
|
"collective": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/editorjs"
|
"url": "https://opencollective.com/editorjs"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@codexteam/icons": "0.1.0",
|
|
||||||
"codex-notifier": "^1.1.2",
|
|
||||||
"codex-tooltip": "^1.0.5",
|
|
||||||
"html-janitor": "^2.0.4",
|
|
||||||
"nanoid": "^3.1.22"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
14
src/codex.ts
|
@ -7,20 +7,19 @@ import { EditorConfig } from '../types';
|
||||||
*/
|
*/
|
||||||
import '@babel/register';
|
import '@babel/register';
|
||||||
|
|
||||||
import 'components/polyfills';
|
import './components/polyfills';
|
||||||
import Core from './components/core';
|
import Core from './components/core';
|
||||||
import * as _ from './components/utils';
|
import * as _ from './components/utils';
|
||||||
|
import { destroy as destroyTooltip } from './components/utils/tooltip';
|
||||||
|
|
||||||
declare const VERSION: string;
|
declare const VERSION: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor.js
|
* Editor.js
|
||||||
*
|
*
|
||||||
* Short Description (눈_눈;)
|
|
||||||
*
|
|
||||||
* @version 2.18.0
|
|
||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
* @author CodeX-Team <https://ifmo.su>
|
* @see Editor.js <https://editorjs.io>
|
||||||
|
* @author CodeX Team <https://codex.so>
|
||||||
*/
|
*/
|
||||||
export default class EditorJS {
|
export default class EditorJS {
|
||||||
/**
|
/**
|
||||||
|
@ -69,6 +68,9 @@ export default class EditorJS {
|
||||||
*/
|
*/
|
||||||
this.isReady = editor.isReady.then(() => {
|
this.isReady = editor.isReady.then(() => {
|
||||||
this.exportAPI(editor);
|
this.exportAPI(editor);
|
||||||
|
/**
|
||||||
|
* @todo pass API as an argument. It will allow to use Editor's API when editor is ready
|
||||||
|
*/
|
||||||
onReady();
|
onReady();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -89,6 +91,8 @@ export default class EditorJS {
|
||||||
moduleInstance.listeners.removeAll();
|
moduleInstance.listeners.removeAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
destroyTooltip();
|
||||||
|
|
||||||
editor = null;
|
editor = null;
|
||||||
|
|
||||||
for (const field in this) {
|
for (const field in this) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { EditorConfig } from '../../types';
|
||||||
import { ModuleConfig } from '../types-internal/module-config';
|
import { ModuleConfig } from '../types-internal/module-config';
|
||||||
import Listeners from './utils/listeners';
|
import Listeners from './utils/listeners';
|
||||||
import EventsDispatcher from './utils/events';
|
import EventsDispatcher from './utils/events';
|
||||||
|
import { EditorEventMap } from './events';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type <T> of the Module generic.
|
* The type <T> of the Module generic.
|
||||||
|
@ -42,7 +43,7 @@ export default class Module<T extends ModuleNodes = Record<string, HTMLElement>>
|
||||||
/**
|
/**
|
||||||
* Editor event dispatcher class
|
* Editor event dispatcher class
|
||||||
*/
|
*/
|
||||||
protected eventsDispatcher: EventsDispatcher;
|
protected eventsDispatcher: EventsDispatcher<EditorEventMap>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Util for bind/unbind DOM event listeners
|
* Util for bind/unbind DOM event listeners
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { API, BlockTune } from '../../../types';
|
import { API, BlockTune } from '../../../types';
|
||||||
import Popover from '../utils/popover';
|
|
||||||
import { IconChevronDown } from '@codexteam/icons';
|
import { IconChevronDown } from '@codexteam/icons';
|
||||||
import { TunesMenuConfig } from '../../../types/tools';
|
import { TunesMenuConfig } from '../../../types/tools';
|
||||||
|
|
||||||
|
@ -49,34 +48,21 @@ export default class MoveDownTune implements BlockTune {
|
||||||
return {
|
return {
|
||||||
icon: IconChevronDown,
|
icon: IconChevronDown,
|
||||||
title: this.api.i18n.t('Move down'),
|
title: this.api.i18n.t('Move down'),
|
||||||
onActivate: (item, event): void => this.handleClick(event),
|
onActivate: (): void => this.handleClick(),
|
||||||
name: 'move-down',
|
name: 'move-down',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle clicks on 'move down' button
|
* Handle clicks on 'move down' button
|
||||||
*
|
|
||||||
* @param event - click event
|
|
||||||
*/
|
*/
|
||||||
public handleClick(event: MouseEvent): void {
|
public handleClick(): void {
|
||||||
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
||||||
const nextBlock = this.api.blocks.getBlockByIndex(currentBlockIndex + 1);
|
const nextBlock = this.api.blocks.getBlockByIndex(currentBlockIndex + 1);
|
||||||
|
|
||||||
// If Block is last do nothing
|
// If Block is last do nothing
|
||||||
if (!nextBlock) {
|
if (!nextBlock) {
|
||||||
const button = (event.target as HTMLElement)
|
throw new Error('Unable to move Block down since it is already the last');
|
||||||
.closest('.' + Popover.CSS.item)
|
|
||||||
.querySelector('.' + Popover.CSS.itemIcon);
|
|
||||||
|
|
||||||
button.classList.add(this.CSS.animation);
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
button.classList.remove(this.CSS.animation);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextBlockElement = nextBlock.holder;
|
const nextBlockElement = nextBlock.holder;
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* @copyright <CodeX Team> 2018
|
* @copyright <CodeX Team> 2018
|
||||||
*/
|
*/
|
||||||
import { API, BlockTune } from '../../../types';
|
import { API, BlockTune } from '../../../types';
|
||||||
import Popover from '../../components/utils/popover';
|
|
||||||
import { IconChevronUp } from '@codexteam/icons';
|
import { IconChevronUp } from '@codexteam/icons';
|
||||||
import { TunesMenuConfig } from '../../../types/tools';
|
import { TunesMenuConfig } from '../../../types/tools';
|
||||||
|
|
||||||
|
@ -47,34 +46,21 @@ export default class MoveUpTune implements BlockTune {
|
||||||
return {
|
return {
|
||||||
icon: IconChevronUp,
|
icon: IconChevronUp,
|
||||||
title: this.api.i18n.t('Move up'),
|
title: this.api.i18n.t('Move up'),
|
||||||
onActivate: (item, e): void => this.handleClick(e),
|
onActivate: (): void => this.handleClick(),
|
||||||
name: 'move-up',
|
name: 'move-up',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move current block up
|
* Move current block up
|
||||||
*
|
|
||||||
* @param {MouseEvent} event - click event
|
|
||||||
*/
|
*/
|
||||||
public handleClick(event: MouseEvent): void {
|
public handleClick(): void {
|
||||||
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
||||||
const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
|
const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
|
||||||
const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
|
const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
|
||||||
|
|
||||||
if (currentBlockIndex === 0 || !currentBlock || !previousBlock) {
|
if (currentBlockIndex === 0 || !currentBlock || !previousBlock) {
|
||||||
const button = (event.target as HTMLElement)
|
throw new Error('Unable to move Block up since it is already the first');
|
||||||
.closest('.' + Popover.CSS.item)
|
|
||||||
.querySelector('.' + Popover.CSS.itemIcon);
|
|
||||||
|
|
||||||
button.classList.add(this.CSS.animation);
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
button.classList.remove(this.CSS.animation);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentBlockElement = currentBlock.holder;
|
const currentBlockElement = currentBlock.holder;
|
||||||
|
@ -96,7 +82,7 @@ export default class MoveUpTune implements BlockTune {
|
||||||
if (previousBlockCoords.top > 0) {
|
if (previousBlockCoords.top > 0) {
|
||||||
scrollUpOffset = Math.abs(currentBlockCoords.top) - Math.abs(previousBlockCoords.top);
|
scrollUpOffset = Math.abs(currentBlockCoords.top) - Math.abs(previousBlockCoords.top);
|
||||||
} else {
|
} else {
|
||||||
scrollUpOffset = window.innerHeight - Math.abs(currentBlockCoords.top) + Math.abs(previousBlockCoords.top);
|
scrollUpOffset = Math.abs(currentBlockCoords.top) + previousBlockCoords.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.scrollBy(0, -1 * scrollUpOffset);
|
window.scrollBy(0, -1 * scrollUpOffset);
|
||||||
|
|
|
@ -84,6 +84,13 @@ function BlockAPI(
|
||||||
return block.stretched;
|
return block.stretched;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if Block has inputs to be focused
|
||||||
|
*/
|
||||||
|
get focusable(): boolean {
|
||||||
|
return block.focusable;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call Tool method with errors handler under-the-hood
|
* Call Tool method with errors handler under-the-hood
|
||||||
*
|
*
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
SanitizerConfig,
|
SanitizerConfig,
|
||||||
ToolConfig,
|
ToolConfig,
|
||||||
ToolboxConfigEntry,
|
ToolboxConfigEntry,
|
||||||
PopoverItem
|
PopoverItemParams
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
|
|
||||||
import { SavedData } from '../../../types/data-formats';
|
import { SavedData } from '../../../types/data-formats';
|
||||||
|
@ -22,6 +22,11 @@ import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
|
||||||
import ToolsCollection from '../tools/collection';
|
import ToolsCollection from '../tools/collection';
|
||||||
import EventsDispatcher from '../utils/events';
|
import EventsDispatcher from '../utils/events';
|
||||||
import { TunesMenuConfigItem } from '../../../types/tools';
|
import { TunesMenuConfigItem } from '../../../types/tools';
|
||||||
|
import { isMutationBelongsToElement } from '../utils/mutations';
|
||||||
|
import { EditorEventMap, FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events';
|
||||||
|
import { RedactorDomChangedPayload } from '../events/RedactorDomChanged';
|
||||||
|
import { convertBlockDataToString, isSameBlockData } from '../utils/blocks';
|
||||||
|
import { PopoverItemType } from '../utils/popover';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface describes Block class constructor argument
|
* Interface describes Block class constructor argument
|
||||||
|
@ -84,9 +89,11 @@ export enum BlockToolAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Names of events supported by Block class
|
* Names of events used in Block
|
||||||
*/
|
*/
|
||||||
type BlockEvents = 'didMutated';
|
interface BlockEvents {
|
||||||
|
'didMutated': Block,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
|
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
|
||||||
|
@ -105,7 +112,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
wrapper: 'ce-block',
|
wrapper: 'ce-block',
|
||||||
wrapperStretched: 'ce-block--stretched',
|
wrapperStretched: 'ce-block--stretched',
|
||||||
content: 'ce-block__content',
|
content: 'ce-block__content',
|
||||||
focused: 'ce-block--focused',
|
|
||||||
selected: 'ce-block--selected',
|
selected: 'ce-block--selected',
|
||||||
dropTarget: 'ce-block--drop-target',
|
dropTarget: 'ce-block--drop-target',
|
||||||
};
|
};
|
||||||
|
@ -153,6 +159,11 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
*/
|
*/
|
||||||
private cachedInputs: HTMLElement[] = [];
|
private cachedInputs: HTMLElement[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We'll store a reference to the tool's rendered element to access it later
|
||||||
|
*/
|
||||||
|
private toolRenderedElement: HTMLElement | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tool class instance
|
* Tool class instance
|
||||||
*/
|
*/
|
||||||
|
@ -187,53 +198,14 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
private inputIndex = 0;
|
private inputIndex = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mutation observer to handle DOM mutations
|
* Common editor event bus
|
||||||
*
|
|
||||||
* @type {MutationObserver}
|
|
||||||
*/
|
*/
|
||||||
private mutationObserver: MutationObserver;
|
private readonly editorEventBus: EventsDispatcher<EditorEventMap> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounce Timer
|
* Link to editor dom change callback. Used to remove listener on remove
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
*/
|
||||||
private readonly modificationDebounceTimer = 450;
|
private redactorDomChangedCallback: (payload: RedactorDomChangedPayload) => void;
|
||||||
|
|
||||||
/**
|
|
||||||
* Is fired when DOM mutation has been happened
|
|
||||||
*/
|
|
||||||
private didMutated = _.debounce((mutationsOrInputEvent: MutationRecord[] | InputEvent = []): void => {
|
|
||||||
const shouldFireUpdate = mutationsOrInputEvent instanceof InputEvent ||
|
|
||||||
!mutationsOrInputEvent.some(({
|
|
||||||
addedNodes = [],
|
|
||||||
removedNodes,
|
|
||||||
}) => {
|
|
||||||
return [...Array.from(addedNodes), ...Array.from(removedNodes)]
|
|
||||||
.some(node => $.isElement(node) && (node as HTMLElement).dataset.mutationFree === 'true');
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In case some mutation free elements are added or removed, do not trigger didMutated event
|
|
||||||
*/
|
|
||||||
if (!shouldFireUpdate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drop cache
|
|
||||||
*/
|
|
||||||
this.cachedInputs = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update current input
|
|
||||||
*/
|
|
||||||
this.updateCurrentInput();
|
|
||||||
|
|
||||||
this.call(BlockToolAPI.UPDATED);
|
|
||||||
|
|
||||||
this.emit('didMutated', this);
|
|
||||||
}, this.modificationDebounceTimer);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current block API interface
|
* Current block API interface
|
||||||
|
@ -241,12 +213,13 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
private readonly blockAPI: BlockAPIInterface;
|
private readonly blockAPI: BlockAPIInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} options - block constructor options
|
* @param options - block constructor options
|
||||||
* @param {string} [options.id] - block's id. Will be generated if omitted.
|
* @param [options.id] - block's id. Will be generated if omitted.
|
||||||
* @param {BlockToolData} options.data - Tool's initial data
|
* @param options.data - Tool's initial data
|
||||||
* @param {BlockTool} options.tool — block's tool
|
* @param options.tool — block's tool
|
||||||
* @param options.api - Editor API module for pass it to the Block Tunes
|
* @param options.api - Editor API module for pass it to the Block Tunes
|
||||||
* @param {boolean} options.readOnly - Read-Only flag
|
* @param options.readOnly - Read-Only flag
|
||||||
|
* @param [eventBus] - Editor common event bus. Allows to subscribe on some Editor events. Could be omitted when "virtual" Block is created. See BlocksAPI@composeBlockData.
|
||||||
*/
|
*/
|
||||||
constructor({
|
constructor({
|
||||||
id = _.generateBlockId(),
|
id = _.generateBlockId(),
|
||||||
|
@ -255,18 +228,16 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
api,
|
api,
|
||||||
readOnly,
|
readOnly,
|
||||||
tunesData,
|
tunesData,
|
||||||
}: BlockConstructorOptions) {
|
}: BlockConstructorOptions, eventBus?: EventsDispatcher<EditorEventMap>) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.name = tool.name;
|
this.name = tool.name;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.settings = tool.settings;
|
this.settings = tool.settings;
|
||||||
this.config = tool.settings.config || {};
|
this.config = tool.settings.config || {};
|
||||||
this.api = api;
|
this.api = api;
|
||||||
|
this.editorEventBus = eventBus || null;
|
||||||
this.blockAPI = new BlockAPI(this);
|
this.blockAPI = new BlockAPI(this);
|
||||||
|
|
||||||
this.mutationObserver = new MutationObserver(this.didMutated);
|
|
||||||
|
|
||||||
this.tool = tool;
|
this.tool = tool;
|
||||||
this.toolInstance = tool.create(data, this.blockAPI, readOnly);
|
this.toolInstance = tool.create(data, this.blockAPI, readOnly);
|
||||||
|
|
||||||
|
@ -278,6 +249,22 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
this.composeTunes(tunesData);
|
this.composeTunes(tunesData);
|
||||||
|
|
||||||
this.holder = this.compose();
|
this.holder = this.compose();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind block events in RIC for optimizing of constructing process time
|
||||||
|
*/
|
||||||
|
window.requestIdleCallback(() => {
|
||||||
|
/**
|
||||||
|
* Start watching block mutations
|
||||||
|
*/
|
||||||
|
this.watchBlockMutations();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutation observer doesn't track changes in "<input>" and "<textarea>"
|
||||||
|
* so we need to track focus events to update current input and clear cache.
|
||||||
|
*/
|
||||||
|
this.addInputEvents();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -404,13 +391,20 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
return _.isFunction(this.toolInstance.merge);
|
return _.isFunction(this.toolInstance.merge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If Block contains inputs, it is focusable
|
||||||
|
*/
|
||||||
|
public get focusable(): boolean {
|
||||||
|
return this.inputs.length !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check block for emptiness
|
* Check block for emptiness
|
||||||
*
|
*
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
public get isEmpty(): boolean {
|
public get isEmpty(): boolean {
|
||||||
const emptyText = $.isEmpty(this.pluginsContent);
|
const emptyText = $.isEmpty(this.pluginsContent, '/');
|
||||||
const emptyMedia = !this.hasMedia;
|
const emptyMedia = !this.hasMedia;
|
||||||
|
|
||||||
return emptyText && emptyMedia;
|
return emptyText && emptyMedia;
|
||||||
|
@ -441,22 +435,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
return !!this.holder.querySelector(mediaTags.join(','));
|
return !!this.holder.querySelector(mediaTags.join(','));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set focused state
|
|
||||||
*
|
|
||||||
* @param {boolean} state - 'true' to select, 'false' to remove selection
|
|
||||||
*/
|
|
||||||
public set focused(state: boolean) {
|
|
||||||
this.holder.classList.toggle(Block.CSS.focused, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Block's focused state
|
|
||||||
*/
|
|
||||||
public get focused(): boolean {
|
|
||||||
return this.holder.classList.contains(Block.CSS.focused);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set selected state
|
* Set selected state
|
||||||
* We don't need to mark Block as Selected when it is empty
|
* We don't need to mark Block as Selected when it is empty
|
||||||
|
@ -464,14 +442,21 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
* @param {boolean} state - 'true' to select, 'false' to remove selection
|
* @param {boolean} state - 'true' to select, 'false' to remove selection
|
||||||
*/
|
*/
|
||||||
public set selected(state: boolean) {
|
public set selected(state: boolean) {
|
||||||
if (state) {
|
this.holder.classList.toggle(Block.CSS.selected, state);
|
||||||
this.holder.classList.add(Block.CSS.selected);
|
|
||||||
|
|
||||||
SelectionUtils.addFakeCursor(this.holder);
|
const fakeCursorWillBeAdded = state === true && SelectionUtils.isRangeInsideContainer(this.holder);
|
||||||
} else {
|
const fakeCursorWillBeRemoved = state === false && SelectionUtils.isFakeCursorInsideContainer(this.holder);
|
||||||
this.holder.classList.remove(Block.CSS.selected);
|
|
||||||
|
|
||||||
SelectionUtils.removeFakeCursor(this.holder);
|
if (fakeCursorWillBeAdded || fakeCursorWillBeRemoved) {
|
||||||
|
this.editorEventBus?.emit(FakeCursorAboutToBeToggled, { state }); // mutex
|
||||||
|
|
||||||
|
if (fakeCursorWillBeAdded) {
|
||||||
|
SelectionUtils.addFakeCursor();
|
||||||
|
} else {
|
||||||
|
SelectionUtils.removeFakeCursor(this.holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editorEventBus?.emit(FakeCursorHaveBeenSet, { state });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -517,23 +502,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
public get pluginsContent(): HTMLElement {
|
public get pluginsContent(): HTMLElement {
|
||||||
const blockContentNodes = this.holder.querySelector(`.${Block.CSS.content}`);
|
return this.toolRenderedElement;
|
||||||
|
|
||||||
if (blockContentNodes && blockContentNodes.childNodes.length) {
|
|
||||||
/**
|
|
||||||
* Editors Block content can contain different Nodes from extensions
|
|
||||||
* We use DOM isExtensionNode to ignore such Nodes and return first Block that does not match filtering list
|
|
||||||
*/
|
|
||||||
for (let child = blockContentNodes.childNodes.length - 1; child >= 0; child--) {
|
|
||||||
const contentNode = blockContentNodes.childNodes[child];
|
|
||||||
|
|
||||||
if (!$.isExtensionNode(contentNode)) {
|
|
||||||
return contentNode as HTMLElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -581,7 +550,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
*
|
*
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
public async save(): Promise<void | SavedData> {
|
public async save(): Promise<undefined | SavedData> {
|
||||||
const extractedBlock = await this.toolInstance.save(this.pluginsContent as HTMLElement);
|
const extractedBlock = await this.toolInstance.save(this.pluginsContent as HTMLElement);
|
||||||
const tunesData: { [name: string]: BlockTuneData } = this.unavailableTunesData;
|
const tunesData: { [name: string]: BlockTuneData } = this.unavailableTunesData;
|
||||||
|
|
||||||
|
@ -642,33 +611,54 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns data to render in tunes menu.
|
* Returns data to render in Block Tunes menu.
|
||||||
* Splits block tunes settings into 2 groups: popover items and custom html.
|
* Splits block tunes into 2 groups: block specific tunes and common tunes
|
||||||
*/
|
*/
|
||||||
public getTunes(): [PopoverItem[], HTMLElement] {
|
public getTunes(): {
|
||||||
const customHtmlTunesContainer = document.createElement('div');
|
toolTunes: PopoverItemParams[];
|
||||||
const tunesItems: TunesMenuConfigItem[] = [];
|
commonTunes: PopoverItemParams[];
|
||||||
|
} {
|
||||||
|
const toolTunesPopoverParams: TunesMenuConfigItem[] = [];
|
||||||
|
const commonTunesPopoverParams: TunesMenuConfigItem[] = [];
|
||||||
|
|
||||||
/** Tool's tunes: may be defined as return value of optional renderSettings method */
|
/** Tool's tunes: may be defined as return value of optional renderSettings method */
|
||||||
const tunesDefinedInTool = typeof this.toolInstance.renderSettings === 'function' ? this.toolInstance.renderSettings() : [];
|
const tunesDefinedInTool = typeof this.toolInstance.renderSettings === 'function' ? this.toolInstance.renderSettings() : [];
|
||||||
|
|
||||||
|
if ($.isElement(tunesDefinedInTool)) {
|
||||||
|
toolTunesPopoverParams.push({
|
||||||
|
type: PopoverItemType.Html,
|
||||||
|
element: tunesDefinedInTool,
|
||||||
|
});
|
||||||
|
} else if (Array.isArray(tunesDefinedInTool)) {
|
||||||
|
toolTunesPopoverParams.push(...tunesDefinedInTool);
|
||||||
|
} else {
|
||||||
|
toolTunesPopoverParams.push(tunesDefinedInTool);
|
||||||
|
}
|
||||||
|
|
||||||
/** Common tunes: combination of default tunes (move up, move down, delete) and third-party tunes connected via tunes api */
|
/** Common tunes: combination of default tunes (move up, move down, delete) and third-party tunes connected via tunes api */
|
||||||
const commonTunes = [
|
const commonTunes = [
|
||||||
...this.tunesInstances.values(),
|
...this.tunesInstances.values(),
|
||||||
...this.defaultTunesInstances.values(),
|
...this.defaultTunesInstances.values(),
|
||||||
].map(tuneInstance => tuneInstance.render());
|
].map(tuneInstance => tuneInstance.render());
|
||||||
|
|
||||||
[tunesDefinedInTool, commonTunes].flat().forEach(rendered => {
|
/** Separate custom html from Popover items params for common tunes */
|
||||||
if ($.isElement(rendered)) {
|
commonTunes.forEach(tuneConfig => {
|
||||||
customHtmlTunesContainer.appendChild(rendered);
|
if ($.isElement(tuneConfig)) {
|
||||||
} else if (Array.isArray(rendered)) {
|
commonTunesPopoverParams.push({
|
||||||
tunesItems.push(...rendered);
|
type: PopoverItemType.Html,
|
||||||
|
element: tuneConfig,
|
||||||
|
});
|
||||||
|
} else if (Array.isArray(tuneConfig)) {
|
||||||
|
commonTunesPopoverParams.push(...tuneConfig);
|
||||||
} else {
|
} else {
|
||||||
tunesItems.push(rendered);
|
commonTunesPopoverParams.push(tuneConfig);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return [tunesItems, customHtmlTunesContainer];
|
return {
|
||||||
|
toolTunes: toolTunesPopoverParams,
|
||||||
|
commonTunes: commonTunesPopoverParams,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -686,38 +676,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
: SelectionUtils.anchorNode;
|
: SelectionUtils.anchorNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Is fired when Block will be selected as current
|
|
||||||
*/
|
|
||||||
public willSelect(): void {
|
|
||||||
/**
|
|
||||||
* Observe DOM mutations to update Block inputs
|
|
||||||
*/
|
|
||||||
this.mutationObserver.observe(
|
|
||||||
this.holder.firstElementChild,
|
|
||||||
{
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
characterData: true,
|
|
||||||
attributes: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mutation observer doesn't track changes in "<input>" and "<textarea>"
|
|
||||||
* so we need to track focus events to update current input and clear cache.
|
|
||||||
*/
|
|
||||||
this.addInputEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is fired when Block will be unselected
|
|
||||||
*/
|
|
||||||
public willUnselect(): void {
|
|
||||||
this.mutationObserver.disconnect();
|
|
||||||
this.removeInputEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows to say Editor that Block was changed. Used to manually trigger Editor's 'onChange' callback
|
* Allows to say Editor that Block was changed. Used to manually trigger Editor's 'onChange' callback
|
||||||
* Can be useful for block changes invisible for editor core.
|
* Can be useful for block changes invisible for editor core.
|
||||||
|
@ -730,6 +688,9 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
* Call Tool instance destroy method
|
* Call Tool instance destroy method
|
||||||
*/
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
|
this.unwatchBlockMutations();
|
||||||
|
this.removeInputEvents();
|
||||||
|
|
||||||
super.destroy();
|
super.destroy();
|
||||||
|
|
||||||
if (_.isFunction(this.toolInstance.destroy)) {
|
if (_.isFunction(this.toolInstance.destroy)) {
|
||||||
|
@ -771,14 +732,20 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
const blockData = await this.data;
|
const blockData = await this.data;
|
||||||
const toolboxItems = toolboxSettings;
|
const toolboxItems = toolboxSettings;
|
||||||
|
|
||||||
return toolboxItems.find((item) => {
|
return toolboxItems?.find((item) => {
|
||||||
return Object.entries(item.data)
|
return isSameBlockData(item.data, blockData);
|
||||||
.some(([propName, propValue]) => {
|
|
||||||
return blockData[propName] && _.equals(blockData[propName], propValue);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports Block data as string using conversion config
|
||||||
|
*/
|
||||||
|
public async exportDataAsString(): Promise<string> {
|
||||||
|
const blockData = await this.data;
|
||||||
|
|
||||||
|
return convertBlockDataToString(blockData, this.tool.conversionConfig);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make default Block wrappers and put Tool`s content there
|
* Make default Block wrappers and put Tool`s content there
|
||||||
*
|
*
|
||||||
|
@ -789,7 +756,22 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
contentNode = $.make('div', Block.CSS.content),
|
contentNode = $.make('div', Block.CSS.content),
|
||||||
pluginsContent = this.toolInstance.render();
|
pluginsContent = this.toolInstance.render();
|
||||||
|
|
||||||
contentNode.appendChild(pluginsContent);
|
if (import.meta.env.MODE === 'test') {
|
||||||
|
wrapper.setAttribute('data-cy', 'block-wrapper');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export id to the DOM three
|
||||||
|
* Useful for standalone modules development. For example, allows to identify Block by some child node. Or scroll to a particular Block by id.
|
||||||
|
*/
|
||||||
|
wrapper.dataset.id = this.id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saving a reference to plugin's content element for guaranteed accessing it later
|
||||||
|
*/
|
||||||
|
this.toolRenderedElement = pluginsContent;
|
||||||
|
|
||||||
|
contentNode.appendChild(this.toolRenderedElement);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Block Tunes might wrap Block's content node to provide any UI changes
|
* Block Tunes might wrap Block's content node to provide any UI changes
|
||||||
|
@ -846,9 +828,9 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
*/
|
*/
|
||||||
private handleFocus = (): void => {
|
private handleFocus = (): void => {
|
||||||
/**
|
/**
|
||||||
* Drop cache
|
* Drop inputs cache to query the new ones
|
||||||
*/
|
*/
|
||||||
this.cachedInputs = [];
|
this.dropInputsCache();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update current input
|
* Update current input
|
||||||
|
@ -867,7 +849,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
* If input is native input add oninput listener to observe changes
|
* If input is native input add oninput listener to observe changes
|
||||||
*/
|
*/
|
||||||
if ($.isNativeInput(input)) {
|
if ($.isNativeInput(input)) {
|
||||||
input.addEventListener('input', this.didMutated);
|
input.addEventListener('input', this.didMutated as EventListener);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -880,8 +862,151 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
input.removeEventListener('focus', this.handleFocus);
|
input.removeEventListener('focus', this.handleFocus);
|
||||||
|
|
||||||
if ($.isNativeInput(input)) {
|
if ($.isNativeInput(input)) {
|
||||||
input.removeEventListener('input', this.didMutated);
|
input.removeEventListener('input', this.didMutated as EventListener);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is fired when DOM mutation has been happened
|
||||||
|
*
|
||||||
|
* @param mutationsOrInputEvent - actual changes
|
||||||
|
* - MutationRecord[] - any DOM change
|
||||||
|
* - InputEvent — <input> change
|
||||||
|
* - undefined — manual triggering of block.dispatchChange()
|
||||||
|
*/
|
||||||
|
private readonly didMutated = (mutationsOrInputEvent: MutationRecord[] | InputEvent = undefined): void => {
|
||||||
|
/**
|
||||||
|
* Block API have dispatchChange() method. In this case, mutations list will be undefined.
|
||||||
|
*/
|
||||||
|
const isManuallyDispatched = mutationsOrInputEvent === undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if didMutated has been called as "input" event handler
|
||||||
|
*/
|
||||||
|
const isInputEventHandler = mutationsOrInputEvent instanceof InputEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If tool updates its own root element, we need to renew it in our memory
|
||||||
|
*/
|
||||||
|
if (!isManuallyDispatched && !isInputEventHandler) {
|
||||||
|
this.detectToolRootChange(mutationsOrInputEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We won't fire a Block mutation event if mutation contain only nodes marked with 'data-mutation-free' attributes
|
||||||
|
*/
|
||||||
|
let shouldFireUpdate;
|
||||||
|
|
||||||
|
if (isManuallyDispatched) {
|
||||||
|
shouldFireUpdate = true;
|
||||||
|
} else if (isInputEventHandler) {
|
||||||
|
shouldFireUpdate = true;
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* Update from 2023, Feb 17:
|
||||||
|
* Changed mutationsOrInputEvent.some() to mutationsOrInputEvent.every()
|
||||||
|
* since there could be a real mutations same-time with mutation-free changes,
|
||||||
|
* for example when Block Tune change: block is changing along with FakeCursor (mutation-free) removing
|
||||||
|
* — we should fire 'didMutated' event in that case
|
||||||
|
*/
|
||||||
|
const everyRecordIsMutationFree = mutationsOrInputEvent.length > 0 && mutationsOrInputEvent.every((record) => {
|
||||||
|
const { addedNodes, removedNodes, target } = record;
|
||||||
|
const changedNodes = [
|
||||||
|
...Array.from(addedNodes),
|
||||||
|
...Array.from(removedNodes),
|
||||||
|
target,
|
||||||
|
];
|
||||||
|
|
||||||
|
return changedNodes.some((node) => {
|
||||||
|
if (!$.isElement(node)) {
|
||||||
|
/**
|
||||||
|
* "characterData" mutation record has Text node as a target, so we need to get parent element to check it for mutation-free attribute
|
||||||
|
*/
|
||||||
|
node = node.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node && (node as HTMLElement).closest('[data-mutation-free="true"]') !== null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldFireUpdate = !everyRecordIsMutationFree;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In case some mutation free elements are added or removed, do not trigger didMutated event
|
||||||
|
*/
|
||||||
|
if (!shouldFireUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dropInputsCache();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update current input
|
||||||
|
*/
|
||||||
|
this.updateCurrentInput();
|
||||||
|
|
||||||
|
this.call(BlockToolAPI.UPDATED);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a Block Event with current Block instance.
|
||||||
|
* Block Manager subscribed to these events
|
||||||
|
*/
|
||||||
|
this.emit('didMutated', this);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen common editor Dom Changed event and detect mutations related to the Block
|
||||||
|
*/
|
||||||
|
private watchBlockMutations(): void {
|
||||||
|
/**
|
||||||
|
* Save callback to a property to remove it on Block destroy
|
||||||
|
*
|
||||||
|
* @param payload - event payload
|
||||||
|
*/
|
||||||
|
this.redactorDomChangedCallback = (payload) => {
|
||||||
|
const { mutations } = payload;
|
||||||
|
|
||||||
|
const mutationBelongsToBlock = mutations.some(record => isMutationBelongsToElement(record, this.toolRenderedElement));
|
||||||
|
|
||||||
|
if (mutationBelongsToBlock) {
|
||||||
|
this.didMutated(mutations);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.editorEventBus?.on(RedactorDomChanged, this.redactorDomChangedCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove redactor dom change event listener
|
||||||
|
*/
|
||||||
|
private unwatchBlockMutations(): void {
|
||||||
|
this.editorEventBus?.off(RedactorDomChanged, this.redactorDomChangedCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sometimes Tool can replace own main element, for example H2 -> H4 or UL -> OL
|
||||||
|
* We need to detect such changes and update a link to tools main element with the new one
|
||||||
|
*
|
||||||
|
* @param mutations - records of block content mutations
|
||||||
|
*/
|
||||||
|
private detectToolRootChange(mutations: MutationRecord[]): void {
|
||||||
|
mutations.forEach(record => {
|
||||||
|
const toolRootHasBeenUpdated = Array.from(record.removedNodes).includes(this.toolRenderedElement);
|
||||||
|
|
||||||
|
if (toolRootHasBeenUpdated) {
|
||||||
|
const newToolElement = record.addedNodes[record.addedNodes.length - 1];
|
||||||
|
|
||||||
|
this.toolRenderedElement = newToolElement as HTMLElement;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears inputs cached value
|
||||||
|
*/
|
||||||
|
private dropInputsCache(): void {
|
||||||
|
this.cachedInputs = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -220,6 +220,62 @@ export default class Blocks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces block under passed index with passed block
|
||||||
|
*
|
||||||
|
* @param index - index of existed block
|
||||||
|
* @param block - new block
|
||||||
|
*/
|
||||||
|
public replace(index: number, block: Block): void {
|
||||||
|
if (this.blocks[index] === undefined) {
|
||||||
|
throw Error('Incorrect index');
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevBlock = this.blocks[index];
|
||||||
|
|
||||||
|
prevBlock.holder.replaceWith(block.holder);
|
||||||
|
|
||||||
|
this.blocks[index] = block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts several blocks at once
|
||||||
|
*
|
||||||
|
* @param blocks - blocks to insert
|
||||||
|
* @param index - index to insert blocks at
|
||||||
|
*/
|
||||||
|
public insertMany(blocks: Block[], index: number ): void {
|
||||||
|
const fragment = new DocumentFragment();
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
fragment.appendChild(block.holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.length > 0) {
|
||||||
|
if (index > 0) {
|
||||||
|
const previousBlockIndex = Math.min(index - 1, this.length - 1);
|
||||||
|
const previousBlock = this.blocks[previousBlockIndex];
|
||||||
|
|
||||||
|
previousBlock.holder.after(fragment);
|
||||||
|
} else if (index === 0) {
|
||||||
|
this.workingArea.prepend(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert blocks to the array at the specified index
|
||||||
|
*/
|
||||||
|
this.blocks.splice(index, 0, ...blocks);
|
||||||
|
} else {
|
||||||
|
this.blocks.push(...blocks);
|
||||||
|
this.workingArea.appendChild(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call Rendered event for each block
|
||||||
|
*/
|
||||||
|
blocks.forEach((block) => block.call(BlockToolAPI.RENDERED));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove block
|
* Remove block
|
||||||
*
|
*
|
||||||
|
@ -267,7 +323,7 @@ export default class Blocks {
|
||||||
* @param {number} index — Block index
|
* @param {number} index — Block index
|
||||||
* @returns {Block}
|
* @returns {Block}
|
||||||
*/
|
*/
|
||||||
public get(index: number): Block {
|
public get(index: number): Block | undefined {
|
||||||
return this.blocks[index];
|
return this.blocks[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
5
src/components/constants.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/**
|
||||||
|
* Debounce timeout for selection change event
|
||||||
|
* {@link modules/ui.ts}
|
||||||
|
*/
|
||||||
|
export const selectionChangeDebounceTimeout = 180;
|
|
@ -5,35 +5,11 @@ import { EditorModules } from '../types-internal/editor-modules';
|
||||||
import I18n from './i18n';
|
import I18n from './i18n';
|
||||||
import { CriticalError } from './errors/critical';
|
import { CriticalError } from './errors/critical';
|
||||||
import EventsDispatcher from './utils/events';
|
import EventsDispatcher from './utils/events';
|
||||||
|
import Modules from './modules';
|
||||||
|
import { EditorEventMap } from './events';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Core} Core - editor core class
|
* Editor.js core class. Bootstraps modules.
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Require Editor modules places in components/modules dir
|
|
||||||
*/
|
|
||||||
const contextRequire = require.context('./modules', true);
|
|
||||||
|
|
||||||
const modules = [];
|
|
||||||
|
|
||||||
contextRequire.keys().forEach((filename) => {
|
|
||||||
/**
|
|
||||||
* Include files if:
|
|
||||||
* - extension is .js or .ts
|
|
||||||
* - does not starts with _
|
|
||||||
*/
|
|
||||||
if (filename.match(/^\.\/[^_][\w/]*\.([tj])s$/)) {
|
|
||||||
modules.push(contextRequire(filename));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class Core
|
|
||||||
* @classdesc Editor.js core class
|
|
||||||
* @property {EditorConfig} config - all settings
|
|
||||||
* @property {EditorModules} moduleInstances - constructed editor components
|
|
||||||
* @type {Core}
|
|
||||||
*/
|
*/
|
||||||
export default class Core {
|
export default class Core {
|
||||||
/**
|
/**
|
||||||
|
@ -52,9 +28,9 @@ export default class Core {
|
||||||
public isReady: Promise<void>;
|
public isReady: Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event Dispatcher util
|
* Common Editor Event Bus
|
||||||
*/
|
*/
|
||||||
private eventsDispatcher: EventsDispatcher = new EventsDispatcher();
|
private eventsDispatcher: EventsDispatcher<EditorEventMap> = new EventsDispatcher();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {EditorConfig} config - user configuration
|
* @param {EditorConfig} config - user configuration
|
||||||
|
@ -63,7 +39,8 @@ export default class Core {
|
||||||
/**
|
/**
|
||||||
* Ready promise. Resolved if Editor.js is ready to work, rejected otherwise
|
* Ready promise. Resolved if Editor.js is ready to work, rejected otherwise
|
||||||
*/
|
*/
|
||||||
let onReady, onFail;
|
let onReady: (value?: void | PromiseLike<void>) => void;
|
||||||
|
let onFail: (reason?: unknown) => void;
|
||||||
|
|
||||||
this.isReady = new Promise((resolve, reject) => {
|
this.isReady = new Promise((resolve, reject) => {
|
||||||
onReady = resolve;
|
onReady = resolve;
|
||||||
|
@ -74,33 +51,21 @@ export default class Core {
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
this.configuration = config;
|
this.configuration = config;
|
||||||
|
|
||||||
await this.validate();
|
this.validate();
|
||||||
await this.init();
|
this.init();
|
||||||
await this.start();
|
await this.start();
|
||||||
|
await this.render();
|
||||||
|
|
||||||
_.logLabeled('I\'m ready! (ノ◕ヮ◕)ノ*:・゚✧', 'log', '', 'color: #E24A75');
|
const { BlockManager, Caret, UI, ModificationsObserver } = this.moduleInstances;
|
||||||
|
|
||||||
setTimeout(async () => {
|
UI.checkEmptiness();
|
||||||
await this.render();
|
ModificationsObserver.enable();
|
||||||
|
|
||||||
if ((this.configuration as EditorConfig).autofocus) {
|
if ((this.configuration as EditorConfig).autofocus) {
|
||||||
const { BlockManager, Caret } = this.moduleInstances;
|
Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START);
|
||||||
|
}
|
||||||
|
|
||||||
Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START);
|
onReady();
|
||||||
BlockManager.highlightCurrentNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove loader, show content
|
|
||||||
*/
|
|
||||||
this.moduleInstances.UI.removeLoader();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve this.isReady promise
|
|
||||||
*/
|
|
||||||
onReady();
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
||||||
}, 500);
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
_.log(`Editor.js is not ready because of ${error}`, 'error');
|
_.log(`Editor.js is not ready because of ${error}`, 'error');
|
||||||
|
@ -228,16 +193,14 @@ export default class Core {
|
||||||
*
|
*
|
||||||
* @returns {EditorConfig}
|
* @returns {EditorConfig}
|
||||||
*/
|
*/
|
||||||
public get configuration(): EditorConfig|string {
|
public get configuration(): EditorConfig {
|
||||||
return this.config;
|
return this.config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for required fields in Editor's config
|
* Checks for required fields in Editor's config
|
||||||
*
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
public async validate(): Promise<void> {
|
public validate(): void {
|
||||||
const { holderId, holder } = this.config;
|
const { holderId, holder } = this.config;
|
||||||
|
|
||||||
if (holderId && holder) {
|
if (holderId && holder) {
|
||||||
|
@ -325,27 +288,14 @@ export default class Core {
|
||||||
* Make modules instances and save it to the @property this.moduleInstances
|
* Make modules instances and save it to the @property this.moduleInstances
|
||||||
*/
|
*/
|
||||||
private constructModules(): void {
|
private constructModules(): void {
|
||||||
modules.forEach((module) => {
|
Object.entries(Modules).forEach(([key, module]) => {
|
||||||
/**
|
|
||||||
* If module has non-default exports, passed object contains them all and default export as 'default' property
|
|
||||||
*/
|
|
||||||
const Module = _.isFunction(module) ? module : module.default;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/**
|
this.moduleInstances[key] = new module({
|
||||||
* We use class name provided by displayName property
|
|
||||||
*
|
|
||||||
* On build, Babel will transform all Classes to the Functions so, name will always be 'Function'
|
|
||||||
* To prevent this, we use 'babel-plugin-class-display-name' plugin
|
|
||||||
*
|
|
||||||
* @see https://www.npmjs.com/package/babel-plugin-class-display-name
|
|
||||||
*/
|
|
||||||
this.moduleInstances[Module.displayName] = new Module({
|
|
||||||
config: this.configuration,
|
config: this.configuration,
|
||||||
eventsDispatcher: this.eventsDispatcher,
|
eventsDispatcher: this.eventsDispatcher,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_.log(`Module ${Module.displayName} skipped because`, 'error', e);
|
_.log('[constructModules]', `Module ${key} skipped because`, 'error', e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,11 +52,13 @@ export default class Dom {
|
||||||
* @param {object} [attributes] - any attributes
|
* @param {object} [attributes] - any attributes
|
||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
public static make(tagName: string, classNames: string | string[] = null, attributes: object = {}): HTMLElement {
|
public static make(tagName: string, classNames: string | (string | undefined)[] | null = null, attributes: object = {}): HTMLElement {
|
||||||
const el = document.createElement(tagName);
|
const el = document.createElement(tagName);
|
||||||
|
|
||||||
if (Array.isArray(classNames)) {
|
if (Array.isArray(classNames)) {
|
||||||
el.classList.add(...classNames);
|
const validClassnames = classNames.filter(className => className !== undefined) as string[];
|
||||||
|
|
||||||
|
el.classList.add(...validClassnames);
|
||||||
} else if (classNames) {
|
} else if (classNames) {
|
||||||
el.classList.add(classNames);
|
el.classList.add(classNames);
|
||||||
}
|
}
|
||||||
|
@ -145,7 +147,7 @@ export default class Dom {
|
||||||
* @param {string} selector - searching string
|
* @param {string} selector - searching string
|
||||||
* @returns {Element}
|
* @returns {Element}
|
||||||
*/
|
*/
|
||||||
public static find(el: Element | Document = document, selector: string): Element {
|
public static find(el: Element | Document = document, selector: string): Element | null {
|
||||||
return el.querySelector(selector);
|
return el.querySelector(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,9 +350,10 @@ export default class Dom {
|
||||||
* @description Method checks simple Node without any childs for emptiness
|
* @description Method checks simple Node without any childs for emptiness
|
||||||
* If you have Node with 2 or more children id depth, you better use {@link Dom#isEmpty} method
|
* If you have Node with 2 or more children id depth, you better use {@link Dom#isEmpty} method
|
||||||
* @param {Node} node - node to check
|
* @param {Node} node - node to check
|
||||||
|
* @param {string} [ignoreChars] - char or substring to treat as empty
|
||||||
* @returns {boolean} true if it is empty
|
* @returns {boolean} true if it is empty
|
||||||
*/
|
*/
|
||||||
public static isNodeEmpty(node: Node): boolean {
|
public static isNodeEmpty(node: Node, ignoreChars?: string): boolean {
|
||||||
let nodeText;
|
let nodeText;
|
||||||
|
|
||||||
if (this.isSingleTag(node as HTMLElement) && !this.isLineBreakTag(node as HTMLElement)) {
|
if (this.isSingleTag(node as HTMLElement) && !this.isLineBreakTag(node as HTMLElement)) {
|
||||||
|
@ -363,6 +366,10 @@ export default class Dom {
|
||||||
nodeText = node.textContent.replace('\u200B', '');
|
nodeText = node.textContent.replace('\u200B', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ignoreChars) {
|
||||||
|
nodeText = nodeText.replace(new RegExp(ignoreChars, 'g'), '');
|
||||||
|
}
|
||||||
|
|
||||||
return nodeText.trim().length === 0;
|
return nodeText.trim().length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,9 +393,10 @@ export default class Dom {
|
||||||
*
|
*
|
||||||
* @description Pushes to stack all DOM leafs and checks for emptiness
|
* @description Pushes to stack all DOM leafs and checks for emptiness
|
||||||
* @param {Node} node - node to check
|
* @param {Node} node - node to check
|
||||||
|
* @param {string} [ignoreChars] - char or substring to treat as empty
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
public static isEmpty(node: Node): boolean {
|
public static isEmpty(node: Node, ignoreChars?: string): boolean {
|
||||||
/**
|
/**
|
||||||
* Normalize node to merge several text nodes to one to reduce tree walker iterations
|
* Normalize node to merge several text nodes to one to reduce tree walker iterations
|
||||||
*/
|
*/
|
||||||
|
@ -403,7 +411,7 @@ export default class Dom {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isLeaf(node) && !this.isNodeEmpty(node)) {
|
if (this.isLeaf(node) && !this.isNodeEmpty(node, ignoreChars)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -550,20 +558,6 @@ export default class Dom {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method checks passed Node if it is some extension Node
|
|
||||||
*
|
|
||||||
* @param {Node} node - any node
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
public static isExtensionNode(node: Node): boolean {
|
|
||||||
const extensions = [
|
|
||||||
'GRAMMARLY-EXTENSION',
|
|
||||||
];
|
|
||||||
|
|
||||||
return node && extensions.includes(node.nodeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if element is anchor (is A tag)
|
* Returns true if element is anchor (is A tag)
|
||||||
*
|
*
|
||||||
|
|
16
src/components/events/BlockChanged.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { BlockMutationEvent } from '../../../types/events/block';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when some block state has changed
|
||||||
|
*/
|
||||||
|
export const BlockChanged = 'block changed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload that will be passed with the event
|
||||||
|
*/
|
||||||
|
export interface BlockChangedPayload {
|
||||||
|
/**
|
||||||
|
* CustomEvent describing a block change
|
||||||
|
*/
|
||||||
|
event: BlockMutationEvent;
|
||||||
|
}
|
16
src/components/events/BlockHovered.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import type Block from '../block';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when some block is hovered by user
|
||||||
|
*/
|
||||||
|
export const BlockHovered = 'block hovered';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload that will be passed with the event
|
||||||
|
*/
|
||||||
|
export interface BlockHoveredPayload {
|
||||||
|
/**
|
||||||
|
* Hovered block
|
||||||
|
*/
|
||||||
|
block: Block;
|
||||||
|
}
|
15
src/components/events/EditorMobileLayoutToggled.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* Fired when editor mobile layout toggled
|
||||||
|
*/
|
||||||
|
export const EditorMobileLayoutToggled = 'editor mobile layout toggled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload that will be passed with the event
|
||||||
|
*/
|
||||||
|
export interface EditorMobileLayoutToggledPayload {
|
||||||
|
/**
|
||||||
|
* True, if mobile layout enabled
|
||||||
|
*/
|
||||||
|
isEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
17
src/components/events/FakeCursorAboutToBeToggled.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/**
|
||||||
|
* Fired before we're adding/removing a fake cursor.
|
||||||
|
*
|
||||||
|
* Allows to disable mutation observer to skip this block change
|
||||||
|
*/
|
||||||
|
export const FakeCursorAboutToBeToggled = 'fake cursor is about to be toggled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload that will be passed with the event
|
||||||
|
*/
|
||||||
|
export interface FakeCursorAboutToBeToggledPayload {
|
||||||
|
/**
|
||||||
|
* true - when added a cursor
|
||||||
|
* false - when removed
|
||||||
|
*/
|
||||||
|
state: boolean;
|
||||||
|
}
|
17
src/components/events/FakeCursorHaveBeenSet.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/**
|
||||||
|
* Fired after we've added/removed a fake cursor.
|
||||||
|
*
|
||||||
|
* Allows to enable mutation observer which was disabled before setting
|
||||||
|
*/
|
||||||
|
export const FakeCursorHaveBeenSet = 'fake cursor have been set';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload that will be passed with the event
|
||||||
|
*/
|
||||||
|
export interface FakeCursorHaveBeenSetPayload {
|
||||||
|
/**
|
||||||
|
* true - when added a cursor
|
||||||
|
* false - when removed
|
||||||
|
*/
|
||||||
|
state: boolean;
|
||||||
|
}
|
14
src/components/events/RedactorDomChanged.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Fired when blocks wrapper (.codex-editor-redactor) dom changed
|
||||||
|
*/
|
||||||
|
export const RedactorDomChanged = 'redactor dom changed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload that will be passed with the event
|
||||||
|
*/
|
||||||
|
export interface RedactorDomChangedPayload {
|
||||||
|
/**
|
||||||
|
* Mutations happened with blocks wrapper
|
||||||
|
*/
|
||||||
|
mutations: MutationRecord[];
|
||||||
|
}
|
29
src/components/events/index.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { RedactorDomChanged, RedactorDomChangedPayload } from './RedactorDomChanged';
|
||||||
|
import { BlockChanged, BlockChangedPayload } from './BlockChanged';
|
||||||
|
import { BlockHovered, BlockHoveredPayload } from './BlockHovered';
|
||||||
|
import { FakeCursorAboutToBeToggled, FakeCursorAboutToBeToggledPayload } from './FakeCursorAboutToBeToggled';
|
||||||
|
import { FakeCursorHaveBeenSet, FakeCursorHaveBeenSetPayload } from './FakeCursorHaveBeenSet';
|
||||||
|
import { EditorMobileLayoutToggled, EditorMobileLayoutToggledPayload } from './EditorMobileLayoutToggled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Events fired by Editor Event Dispatcher
|
||||||
|
*/
|
||||||
|
export {
|
||||||
|
RedactorDomChanged,
|
||||||
|
BlockChanged,
|
||||||
|
FakeCursorAboutToBeToggled,
|
||||||
|
FakeCursorHaveBeenSet,
|
||||||
|
EditorMobileLayoutToggled
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event name -> Event payload
|
||||||
|
*/
|
||||||
|
export interface EditorEventMap {
|
||||||
|
[BlockHovered]: BlockHoveredPayload;
|
||||||
|
[RedactorDomChanged]: RedactorDomChangedPayload;
|
||||||
|
[BlockChanged]: BlockChangedPayload;
|
||||||
|
[FakeCursorAboutToBeToggled]: FakeCursorAboutToBeToggledPayload;
|
||||||
|
[FakeCursorHaveBeenSet]: FakeCursorHaveBeenSetPayload;
|
||||||
|
[EditorMobileLayoutToggled]: EditorMobileLayoutToggledPayload
|
||||||
|
}
|
|
@ -49,15 +49,11 @@ export default class Flipper {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instance of flipper iterator
|
* Instance of flipper iterator
|
||||||
*
|
|
||||||
* @type {DomIterator|null}
|
|
||||||
*/
|
*/
|
||||||
private readonly iterator: DomIterator = null;
|
private readonly iterator: DomIterator | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag that defines activation status
|
* Flag that defines activation status
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
*/
|
||||||
private activated = false;
|
private activated = false;
|
||||||
|
|
||||||
|
@ -77,7 +73,7 @@ export default class Flipper {
|
||||||
private flipCallbacks: Array<() => void> = [];
|
private flipCallbacks: Array<() => void> = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {FlipperOptions} options - different constructing settings
|
* @param options - different constructing settings
|
||||||
*/
|
*/
|
||||||
constructor(options: FlipperOptions) {
|
constructor(options: FlipperOptions) {
|
||||||
this.iterator = new DomIterator(options.items, options.focusedItemClass);
|
this.iterator = new DomIterator(options.items, options.focusedItemClass);
|
||||||
|
@ -110,7 +106,6 @@ export default class Flipper {
|
||||||
*/
|
*/
|
||||||
public activate(items?: HTMLElement[], cursorPosition?: number): void {
|
public activate(items?: HTMLElement[], cursorPosition?: number): void {
|
||||||
this.activated = true;
|
this.activated = true;
|
||||||
|
|
||||||
if (items) {
|
if (items) {
|
||||||
this.iterator.setItems(items);
|
this.iterator.setItems(items);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,8 @@
|
||||||
},
|
},
|
||||||
"popover": {
|
"popover": {
|
||||||
"Filter": "",
|
"Filter": "",
|
||||||
"Nothing found": ""
|
"Nothing found": "",
|
||||||
|
"Convert to": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolNames": {
|
"toolNames": {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { BlockAPI as BlockAPIInterface, Blocks } from '../../../../types/api';
|
import type { BlockAPI as BlockAPIInterface, Blocks } from '../../../../types/api';
|
||||||
import { BlockToolData, OutputData, ToolConfig } from '../../../../types';
|
import { BlockToolData, OutputBlockData, OutputData, ToolConfig } from '../../../../types';
|
||||||
import * as _ from './../../utils';
|
import * as _ from './../../utils';
|
||||||
import BlockAPI from '../../block/api';
|
import BlockAPI from '../../block/api';
|
||||||
import Module from '../../__module';
|
import Module from '../../__module';
|
||||||
import Block from '../../block';
|
import Block from '../../block';
|
||||||
|
import { capitalize } from './../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class BlocksAPI
|
* @class BlocksAPI
|
||||||
|
@ -17,7 +18,7 @@ export default class BlocksAPI extends Module {
|
||||||
*/
|
*/
|
||||||
public get methods(): Blocks {
|
public get methods(): Blocks {
|
||||||
return {
|
return {
|
||||||
clear: (): void => this.clear(),
|
clear: (): Promise<void> => this.clear(),
|
||||||
render: (data: OutputData): Promise<void> => this.render(data),
|
render: (data: OutputData): Promise<void> => this.render(data),
|
||||||
renderFromHTML: (data: string): Promise<void> => this.renderFromHTML(data),
|
renderFromHTML: (data: string): Promise<void> => this.renderFromHTML(data),
|
||||||
delete: (index?: number): void => this.delete(index),
|
delete: (index?: number): void => this.delete(index),
|
||||||
|
@ -31,8 +32,10 @@ export default class BlocksAPI extends Module {
|
||||||
stretchBlock: (index: number, status = true): void => this.stretchBlock(index, status),
|
stretchBlock: (index: number, status = true): void => this.stretchBlock(index, status),
|
||||||
insertNewBlock: (): void => this.insertNewBlock(),
|
insertNewBlock: (): void => this.insertNewBlock(),
|
||||||
insert: this.insert,
|
insert: this.insert,
|
||||||
|
insertMany: this.insertMany,
|
||||||
update: this.update,
|
update: this.update,
|
||||||
composeBlockData: this.composeBlockData,
|
composeBlockData: this.composeBlockData,
|
||||||
|
convert: this.convert,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,9 +140,11 @@ export default class BlocksAPI extends Module {
|
||||||
*
|
*
|
||||||
* @param {number} blockIndex - index of Block to delete
|
* @param {number} blockIndex - index of Block to delete
|
||||||
*/
|
*/
|
||||||
public delete(blockIndex?: number): void {
|
public delete(blockIndex: number = this.Editor.BlockManager.currentBlockIndex): void {
|
||||||
try {
|
try {
|
||||||
this.Editor.BlockManager.removeBlock(blockIndex);
|
const block = this.Editor.BlockManager.getBlockByIndex(blockIndex);
|
||||||
|
|
||||||
|
this.Editor.BlockManager.removeBlock(block);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_.logLabeled(e, 'warn');
|
_.logLabeled(e, 'warn');
|
||||||
|
|
||||||
|
@ -167,8 +172,8 @@ export default class BlocksAPI extends Module {
|
||||||
/**
|
/**
|
||||||
* Clear Editor's area
|
* Clear Editor's area
|
||||||
*/
|
*/
|
||||||
public clear(): void {
|
public async clear(): Promise<void> {
|
||||||
this.Editor.BlockManager.clear(true);
|
await this.Editor.BlockManager.clear(true);
|
||||||
this.Editor.InlineToolbar.close();
|
this.Editor.InlineToolbar.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,10 +182,21 @@ export default class BlocksAPI extends Module {
|
||||||
*
|
*
|
||||||
* @param {OutputData} data — Saved Editor data
|
* @param {OutputData} data — Saved Editor data
|
||||||
*/
|
*/
|
||||||
public render(data: OutputData): Promise<void> {
|
public async render(data: OutputData): Promise<void> {
|
||||||
this.Editor.BlockManager.clear();
|
if (data === undefined || data.blocks === undefined) {
|
||||||
|
throw new Error('Incorrect data passed to the render() method');
|
||||||
|
}
|
||||||
|
|
||||||
return this.Editor.Renderer.render(data.blocks);
|
/**
|
||||||
|
* Semantic meaning of the "render" method: "Display the new document over the existing one that stays unchanged"
|
||||||
|
* So we need to disable modifications observer temporarily
|
||||||
|
*/
|
||||||
|
this.Editor.ModificationsObserver.disable();
|
||||||
|
|
||||||
|
await this.Editor.BlockManager.clear();
|
||||||
|
await this.Editor.Renderer.render(data.blocks);
|
||||||
|
|
||||||
|
this.Editor.ModificationsObserver.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -288,25 +304,105 @@ export default class BlocksAPI extends Module {
|
||||||
* @param id - id of the block to update
|
* @param id - id of the block to update
|
||||||
* @param data - the new data
|
* @param data - the new data
|
||||||
*/
|
*/
|
||||||
public update = (id: string, data: BlockToolData): void => {
|
public update = async (id: string, data: Partial<BlockToolData>): Promise<BlockAPIInterface> => {
|
||||||
const { BlockManager } = this.Editor;
|
const { BlockManager } = this.Editor;
|
||||||
const block = BlockManager.getBlockById(id);
|
const block = BlockManager.getBlockById(id);
|
||||||
|
|
||||||
if (!block) {
|
if (block === undefined) {
|
||||||
_.log('blocks.update(): Block with passed id was not found', 'warn');
|
throw new Error(`Block with id "${id}" not found`);
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockIndex = BlockManager.getBlockIndex(block);
|
const updatedBlock = await BlockManager.update(block, data);
|
||||||
|
|
||||||
BlockManager.insert({
|
// we cast to any because our BlockAPI has no "new" signature
|
||||||
id: block.id,
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
tool: block.name,
|
return new (BlockAPI as any)(updatedBlock);
|
||||||
data,
|
|
||||||
index: blockIndex,
|
|
||||||
replace: true,
|
|
||||||
tunes: block.tunes,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts block to another type. Both blocks should provide the conversionConfig.
|
||||||
|
*
|
||||||
|
* @param id - id of the existing block to convert. Should provide 'conversionConfig.export' method
|
||||||
|
* @param newType - new block type. Should provide 'conversionConfig.import' method
|
||||||
|
* @param dataOverrides - optional data overrides for the new block
|
||||||
|
* @throws Error if conversion is not possible
|
||||||
|
*/
|
||||||
|
private convert = async (id: string, newType: string, dataOverrides?: BlockToolData): Promise<BlockAPIInterface> => {
|
||||||
|
const { BlockManager, Tools } = this.Editor;
|
||||||
|
const blockToConvert = BlockManager.getBlockById(id);
|
||||||
|
|
||||||
|
if (!blockToConvert) {
|
||||||
|
throw new Error(`Block with id "${id}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalBlockTool = Tools.blockTools.get(blockToConvert.name);
|
||||||
|
const targetBlockTool = Tools.blockTools.get(newType);
|
||||||
|
|
||||||
|
if (!targetBlockTool) {
|
||||||
|
throw new Error(`Block Tool with type "${newType}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalBlockConvertable = originalBlockTool?.conversionConfig?.export !== undefined;
|
||||||
|
const targetBlockConvertable = targetBlockTool.conversionConfig?.import !== undefined;
|
||||||
|
|
||||||
|
if (originalBlockConvertable && targetBlockConvertable) {
|
||||||
|
const newBlock = await BlockManager.convert(blockToConvert, newType, dataOverrides);
|
||||||
|
|
||||||
|
return new BlockAPI(newBlock);
|
||||||
|
} else {
|
||||||
|
const unsupportedBlockTypes = [
|
||||||
|
!originalBlockConvertable ? capitalize(blockToConvert.name) : false,
|
||||||
|
!targetBlockConvertable ? capitalize(newType) : false,
|
||||||
|
].filter(Boolean).join(' and ');
|
||||||
|
|
||||||
|
throw new Error(`Conversion from "${blockToConvert.name}" to "${newType}" is not possible. ${unsupportedBlockTypes} tool(s) should provide a "conversionConfig"`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts several Blocks to a specified index
|
||||||
|
*
|
||||||
|
* @param blocks - blocks data to insert
|
||||||
|
* @param index - index to insert the blocks at
|
||||||
|
*/
|
||||||
|
private insertMany = (
|
||||||
|
blocks: OutputBlockData[],
|
||||||
|
index: number = this.Editor.BlockManager.blocks.length - 1
|
||||||
|
): BlockAPIInterface[] => {
|
||||||
|
this.validateIndex(index);
|
||||||
|
|
||||||
|
const blocksToInsert = blocks.map(({ id, type, data }) => {
|
||||||
|
return this.Editor.BlockManager.composeBlock({
|
||||||
|
id,
|
||||||
|
tool: type || (this.config.defaultBlock as string),
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.Editor.BlockManager.insertMany(blocksToInsert, index);
|
||||||
|
|
||||||
|
// we cast to any because our BlockAPI has no "new" signature
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return blocksToInsert.map((block) => new (BlockAPI as any)(block));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validated block index and throws an error if it's invalid
|
||||||
|
*
|
||||||
|
* @param index - index to validate
|
||||||
|
*/
|
||||||
|
private validateIndex(index: unknown): void {
|
||||||
|
if (typeof index !== 'number') {
|
||||||
|
throw new Error('Index should be a number');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
throw new Error(`Index should be greater than or equal to 0`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === null) {
|
||||||
|
throw new Error(`Index should be greater than or equal to 0`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Caret } from '../../../../types/api';
|
import { BlockAPI, Caret } from '../../../../types/api';
|
||||||
import Module from '../../__module';
|
import Module from '../../__module';
|
||||||
|
import { resolveBlock } from '../../utils/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class CaretAPI
|
* @class CaretAPI
|
||||||
|
@ -96,21 +97,23 @@ export default class CaretAPI extends Module {
|
||||||
/**
|
/**
|
||||||
* Sets caret to the Block by passed index
|
* Sets caret to the Block by passed index
|
||||||
*
|
*
|
||||||
* @param {number} index - index of Block where to set caret
|
* @param blockOrIdOrIndex - either BlockAPI or Block id or Block index
|
||||||
* @param {string} position - position where to set caret
|
* @param position - position where to set caret
|
||||||
* @param {number} offset - caret offset
|
* @param offset - caret offset
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
private setToBlock = (
|
private setToBlock = (
|
||||||
index: number,
|
blockOrIdOrIndex: BlockAPI | BlockAPI['id'] | number,
|
||||||
position: string = this.Editor.Caret.positions.DEFAULT,
|
position: string = this.Editor.Caret.positions.DEFAULT,
|
||||||
offset = 0
|
offset = 0
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (!this.Editor.BlockManager.blocks[index]) {
|
const block = resolveBlock(blockOrIdOrIndex, this.Editor);
|
||||||
|
|
||||||
|
if (block === undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.Editor.Caret.setToBlock(this.Editor.BlockManager.blocks[index], position, offset);
|
this.Editor.Caret.setToBlock(block, position, offset);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ export default class ToolbarAPI extends Module {
|
||||||
close: (): void => this.close(),
|
close: (): void => this.close(),
|
||||||
open: (): void => this.open(),
|
open: (): void => this.open(),
|
||||||
toggleBlockSettings: (openingState?: boolean): void => this.toggleBlockSettings(openingState),
|
toggleBlockSettings: (openingState?: boolean): void => this.toggleBlockSettings(openingState),
|
||||||
|
toggleToolbox: (openingState?: boolean): void => this.toggleToolbox(openingState),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,4 +56,27 @@ export default class ToolbarAPI extends Module {
|
||||||
this.Editor.BlockSettings.close();
|
this.Editor.BlockSettings.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open toolbox
|
||||||
|
*
|
||||||
|
* @param {boolean} openingState - Opening state of toolbox
|
||||||
|
*/
|
||||||
|
public toggleToolbox(openingState: boolean): void {
|
||||||
|
if (this.Editor.BlockManager.currentBlockIndex === -1) {
|
||||||
|
_.logLabeled('Could\'t toggle the Toolbox because there is no block selected ', 'warn');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canOpenToolbox = openingState ?? !this.Editor.Toolbar.toolbox.opened;
|
||||||
|
|
||||||
|
if (canOpenToolbox) {
|
||||||
|
this.Editor.Toolbar.moveAndOpen();
|
||||||
|
this.Editor.Toolbar.toolbox.open();
|
||||||
|
} else {
|
||||||
|
this.Editor.Toolbar.toolbox.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,12 @@ import { Tooltip as ITooltip } from '../../../../types/api';
|
||||||
import type { TooltipOptions, TooltipContent } from 'codex-tooltip/types';
|
import type { TooltipOptions, TooltipContent } from 'codex-tooltip/types';
|
||||||
import Module from '../../__module';
|
import Module from '../../__module';
|
||||||
import { ModuleConfig } from '../../../types-internal/module-config';
|
import { ModuleConfig } from '../../../types-internal/module-config';
|
||||||
import Tooltip from '../../utils/tooltip';
|
import * as tooltip from '../../utils/tooltip';
|
||||||
/**
|
/**
|
||||||
* @class TooltipAPI
|
* @class TooltipAPI
|
||||||
* @classdesc Tooltip API
|
* @classdesc Tooltip API
|
||||||
*/
|
*/
|
||||||
export default class TooltipAPI extends Module {
|
export default class TooltipAPI extends Module {
|
||||||
/**
|
|
||||||
* Tooltip utility Instance
|
|
||||||
*/
|
|
||||||
private tooltip: Tooltip;
|
|
||||||
/**
|
/**
|
||||||
* @class
|
* @class
|
||||||
* @param moduleConfiguration - Module Configuration
|
* @param moduleConfiguration - Module Configuration
|
||||||
|
@ -23,15 +19,6 @@ export default class TooltipAPI extends Module {
|
||||||
config,
|
config,
|
||||||
eventsDispatcher,
|
eventsDispatcher,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tooltip = new Tooltip();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy Module
|
|
||||||
*/
|
|
||||||
public destroy(): void {
|
|
||||||
this.tooltip.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,14 +46,14 @@ export default class TooltipAPI extends Module {
|
||||||
* @param {TooltipOptions} options - tooltip options
|
* @param {TooltipOptions} options - tooltip options
|
||||||
*/
|
*/
|
||||||
public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
|
public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
|
||||||
this.tooltip.show(element, content, options);
|
tooltip.show(element, content, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method hides tooltip on HTML page
|
* Method hides tooltip on HTML page
|
||||||
*/
|
*/
|
||||||
public hide(): void {
|
public hide(): void {
|
||||||
this.tooltip.hide();
|
tooltip.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,6 +64,6 @@ export default class TooltipAPI extends Module {
|
||||||
* @param {TooltipOptions} options - tooltip options
|
* @param {TooltipOptions} options - tooltip options
|
||||||
*/
|
*/
|
||||||
public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
|
public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
|
||||||
this.tooltip.onHover(element, content, options);
|
tooltip.onHover(element, content, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import Module from '../__module';
|
||||||
import * as _ from '../utils';
|
import * as _ from '../utils';
|
||||||
import SelectionUtils from '../selection';
|
import SelectionUtils from '../selection';
|
||||||
import Flipper from '../flipper';
|
import Flipper from '../flipper';
|
||||||
|
import type Block from '../block';
|
||||||
|
import { areBlocksMergeable } from '../utils/blocks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -29,6 +31,10 @@ export default class BlockEvents extends Module {
|
||||||
this.backspace(event);
|
this.backspace(event);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case _.keyCodes.DELETE:
|
||||||
|
this.delete(event);
|
||||||
|
break;
|
||||||
|
|
||||||
case _.keyCodes.ENTER:
|
case _.keyCodes.ENTER:
|
||||||
this.enter(event);
|
this.enter(event);
|
||||||
break;
|
break;
|
||||||
|
@ -47,6 +53,24 @@ export default class BlockEvents extends Module {
|
||||||
this.tabPressed(event);
|
this.tabPressed(event);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We check for "key" here since on different keyboard layouts "/" can be typed as "Shift + 7" etc
|
||||||
|
*
|
||||||
|
* @todo probably using "beforeInput" event would be better here
|
||||||
|
*/
|
||||||
|
if (event.key === '/' && !event.ctrlKey && !event.metaKey) {
|
||||||
|
this.slashPressed(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If user pressed "Ctrl + /" or "Cmd + /" — open Block Settings
|
||||||
|
* We check for "code" here since on different keyboard layouts there can be different keys in place of Slash.
|
||||||
|
*/
|
||||||
|
if (event.code === 'Slash' && (event.ctrlKey || event.metaKey)) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.commandSlashPressed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,7 +104,6 @@ export default class BlockEvents extends Module {
|
||||||
const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
|
const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
|
||||||
|
|
||||||
if (!isShortcut) {
|
if (!isShortcut) {
|
||||||
this.Editor.BlockManager.clearFocused();
|
|
||||||
this.Editor.BlockSelection.clearSelection(event);
|
this.Editor.BlockSelection.clearSelection(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,40 +130,6 @@ export default class BlockEvents extends Module {
|
||||||
this.Editor.UI.checkEmptiness();
|
this.Editor.UI.checkEmptiness();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Open Toolbox to leaf Tools
|
|
||||||
*
|
|
||||||
* @param {KeyboardEvent} event - tab keydown event
|
|
||||||
*/
|
|
||||||
public tabPressed(event): void {
|
|
||||||
/**
|
|
||||||
* Clear blocks selection by tab
|
|
||||||
*/
|
|
||||||
this.Editor.BlockSelection.clearSelection(event);
|
|
||||||
|
|
||||||
const { BlockManager, InlineToolbar, ConversionToolbar } = this.Editor;
|
|
||||||
const currentBlock = BlockManager.currentBlock;
|
|
||||||
|
|
||||||
if (!currentBlock) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEmptyBlock = currentBlock.isEmpty;
|
|
||||||
const canOpenToolbox = currentBlock.tool.isDefault && isEmptyBlock;
|
|
||||||
const conversionToolbarOpened = !isEmptyBlock && ConversionToolbar.opened;
|
|
||||||
const inlineToolbarOpened = !isEmptyBlock && !SelectionUtils.isCollapsed && InlineToolbar.opened;
|
|
||||||
const canOpenBlockTunes = !conversionToolbarOpened && !inlineToolbarOpened;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For empty Blocks we show Plus button via Toolbox only for default Blocks
|
|
||||||
*/
|
|
||||||
if (canOpenToolbox) {
|
|
||||||
this.activateToolbox();
|
|
||||||
} else if (canOpenBlockTunes) {
|
|
||||||
this.activateBlockSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add drop target styles
|
* Add drop target styles
|
||||||
*
|
*
|
||||||
|
@ -207,6 +196,71 @@ export default class BlockEvents extends Module {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab pressed inside a Block.
|
||||||
|
*
|
||||||
|
* @param {KeyboardEvent} event - keydown
|
||||||
|
*/
|
||||||
|
private tabPressed(event: KeyboardEvent): void {
|
||||||
|
const { InlineToolbar, ConversionToolbar, Caret } = this.Editor;
|
||||||
|
|
||||||
|
const isFlipperActivated = ConversionToolbar.opened || InlineToolbar.opened;
|
||||||
|
|
||||||
|
if (isFlipperActivated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNavigated = event.shiftKey ? Caret.navigatePrevious(true) : Caret.navigateNext(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we have next Block/input to focus, then focus it. Otherwise, leave native Tab behaviour
|
||||||
|
*/
|
||||||
|
if (isNavigated) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* '/' + 'command' keydown inside a Block
|
||||||
|
*/
|
||||||
|
private commandSlashPressed(): void {
|
||||||
|
if (this.Editor.BlockSelection.selectedBlocks.length > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activateBlockSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* '/' keydown inside a Block
|
||||||
|
*
|
||||||
|
* @param event - keydown
|
||||||
|
*/
|
||||||
|
private slashPressed(event: KeyboardEvent): void {
|
||||||
|
const currentBlock = this.Editor.BlockManager.currentBlock;
|
||||||
|
const canOpenToolbox = currentBlock.isEmpty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo Handle case when slash pressed when several blocks are selected
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toolbox will be opened only if Block is empty
|
||||||
|
*/
|
||||||
|
if (!canOpenToolbox) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Toolbox will be opened with immediate focus on the Search input,
|
||||||
|
* and '/' will be added in the search input by default — we need to prevent it and add '/' manually
|
||||||
|
*/
|
||||||
|
event.preventDefault();
|
||||||
|
this.Editor.Caret.insertContentAtCaretPosition('/');
|
||||||
|
|
||||||
|
this.activateToolbox();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ENTER pressed on block
|
* ENTER pressed on block
|
||||||
*
|
*
|
||||||
|
@ -234,8 +288,12 @@ export default class BlockEvents extends Module {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow to create line breaks by Shift+Enter
|
* Allow to create line breaks by Shift+Enter
|
||||||
|
*
|
||||||
|
* Note. On iOS devices, Safari automatically treats enter after a period+space (". |") as Shift+Enter
|
||||||
|
* (it used for capitalizing of the first letter of the next sentence)
|
||||||
|
* We don't need to lead soft line break in this case — new block should be created
|
||||||
*/
|
*/
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey && !_.isIosDevice) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,6 +304,13 @@ export default class BlockEvents extends Module {
|
||||||
*/
|
*/
|
||||||
if (this.Editor.Caret.isAtStart && !this.Editor.BlockManager.currentBlock.hasMedia) {
|
if (this.Editor.Caret.isAtStart && !this.Editor.BlockManager.currentBlock.hasMedia) {
|
||||||
this.Editor.BlockManager.insertDefaultBlockAtIndex(this.Editor.BlockManager.currentBlockIndex);
|
this.Editor.BlockManager.insertDefaultBlockAtIndex(this.Editor.BlockManager.currentBlockIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If caret is at very end of the block, just append the new block without splitting
|
||||||
|
* to prevent unnecessary dom mutation observing
|
||||||
|
*/
|
||||||
|
} else if (this.Editor.Caret.isAtEnd) {
|
||||||
|
newCurrent = this.Editor.BlockManager.insertDefaultBlockAtIndex(this.Editor.BlockManager.currentBlockIndex + 1);
|
||||||
} else {
|
} else {
|
||||||
/**
|
/**
|
||||||
* Split the Current Block into two blocks
|
* Split the Current Block into two blocks
|
||||||
|
@ -270,108 +335,179 @@ export default class BlockEvents extends Module {
|
||||||
* @param {KeyboardEvent} event - keydown
|
* @param {KeyboardEvent} event - keydown
|
||||||
*/
|
*/
|
||||||
private backspace(event: KeyboardEvent): void {
|
private backspace(event: KeyboardEvent): void {
|
||||||
const { BlockManager, BlockSelection, Caret } = this.Editor;
|
const { BlockManager, Caret } = this.Editor;
|
||||||
const currentBlock = BlockManager.currentBlock;
|
const { currentBlock, previousBlock } = BlockManager;
|
||||||
const tool = currentBlock.tool;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if Block should be removed by current Backspace keydown
|
* If some fragment is selected, leave native behaviour
|
||||||
*/
|
*/
|
||||||
if (currentBlock.selected || (currentBlock.isEmpty && currentBlock.currentInput === currentBlock.firstInput)) {
|
if (!SelectionUtils.isCollapsed) {
|
||||||
event.preventDefault();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const index = BlockManager.currentBlockIndex;
|
/**
|
||||||
|
* If caret is not at the start, leave native behaviour
|
||||||
|
*/
|
||||||
|
if (!Caret.isAtStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* All the cases below have custom behaviour, so we don't need a native one
|
||||||
|
*/
|
||||||
|
event.preventDefault();
|
||||||
|
this.Editor.Toolbar.close();
|
||||||
|
|
||||||
if (BlockManager.previousBlock && BlockManager.previousBlock.inputs.length === 0) {
|
const isFirstInputFocused = currentBlock.currentInput === currentBlock.firstInput;
|
||||||
/** If previous block doesn't contain inputs, remove it */
|
|
||||||
BlockManager.removeBlock(index - 1);
|
|
||||||
} else {
|
|
||||||
/** If block is empty, just remove it */
|
|
||||||
BlockManager.removeBlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
Caret.setToBlock(
|
/**
|
||||||
BlockManager.currentBlock,
|
* For example, caret at the start of the Quote second input (caption) — just navigate previous input
|
||||||
index ? Caret.positions.END : Caret.positions.START
|
*/
|
||||||
);
|
if (!isFirstInputFocused) {
|
||||||
|
Caret.navigatePrevious();
|
||||||
/** Close Toolbar */
|
|
||||||
this.Editor.Toolbar.close();
|
|
||||||
|
|
||||||
/** Clear selection */
|
|
||||||
BlockSelection.clearSelection(event);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Don't handle Backspaces when Tool sets enableLineBreaks to true.
|
* Backspace at the start of the first Block should do nothing
|
||||||
* Uses for Tools like <code> where line breaks should be handled by default behaviour.
|
|
||||||
*
|
|
||||||
* But if caret is at start of the block, we allow to remove it by backspaces
|
|
||||||
*/
|
*/
|
||||||
if (tool.isLineBreaksEnabled && !Caret.isAtStart) {
|
if (previousBlock === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFirstBlock = BlockManager.currentBlockIndex === 0;
|
/**
|
||||||
const canMergeBlocks = Caret.isAtStart &&
|
* If prev Block is empty, it should be removed just like a character
|
||||||
SelectionUtils.isCollapsed &&
|
*/
|
||||||
currentBlock.currentInput === currentBlock.firstInput &&
|
if (previousBlock.isEmpty) {
|
||||||
!isFirstBlock;
|
BlockManager.removeBlock(previousBlock);
|
||||||
|
|
||||||
if (canMergeBlocks) {
|
return;
|
||||||
/**
|
}
|
||||||
* preventing browser default behaviour
|
|
||||||
*/
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge Blocks
|
* If current Block is empty, just remove it and set cursor to the previous Block (like we're removing line break char)
|
||||||
*/
|
*/
|
||||||
this.mergeBlocks();
|
if (currentBlock.isEmpty) {
|
||||||
|
BlockManager.removeBlock(currentBlock);
|
||||||
|
|
||||||
|
const newCurrentBlock = BlockManager.currentBlock;
|
||||||
|
|
||||||
|
Caret.setToBlock(newCurrentBlock, Caret.positions.END);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bothBlocksMergeable = areBlocksMergeable(previousBlock, currentBlock);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If Blocks could be merged, do it
|
||||||
|
* Otherwise, just navigate previous block
|
||||||
|
*/
|
||||||
|
if (bothBlocksMergeable) {
|
||||||
|
this.mergeBlocks(previousBlock, currentBlock);
|
||||||
|
} else {
|
||||||
|
Caret.setToBlock(previousBlock, Caret.positions.END);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge current and previous Blocks if they have the same type
|
* Handles delete keydown on Block
|
||||||
|
* Removes char after the caret.
|
||||||
|
* If caret is at the end of the block, merge next block with current
|
||||||
|
*
|
||||||
|
* @param {KeyboardEvent} event - keydown
|
||||||
*/
|
*/
|
||||||
private mergeBlocks(): void {
|
private delete(event: KeyboardEvent): void {
|
||||||
const { BlockManager, Caret, Toolbar } = this.Editor;
|
const { BlockManager, Caret } = this.Editor;
|
||||||
const targetBlock = BlockManager.previousBlock;
|
const { currentBlock, nextBlock } = BlockManager;
|
||||||
const blockToMerge = BlockManager.currentBlock;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blocks that can be merged:
|
* If some fragment is selected, leave native behaviour
|
||||||
* 1) with the same Name
|
|
||||||
* 2) Tool has 'merge' method
|
|
||||||
*
|
|
||||||
* other case will handle as usual ARROW LEFT behaviour
|
|
||||||
*/
|
*/
|
||||||
if (blockToMerge.name !== targetBlock.name || !targetBlock.mergeable) {
|
if (!SelectionUtils.isCollapsed) {
|
||||||
/** If target Block doesn't contain inputs or empty, remove it */
|
return;
|
||||||
if (targetBlock.inputs.length === 0 || targetBlock.isEmpty) {
|
}
|
||||||
BlockManager.removeBlock(BlockManager.currentBlockIndex - 1);
|
|
||||||
|
|
||||||
Caret.setToBlock(BlockManager.currentBlock);
|
/**
|
||||||
Toolbar.close();
|
* If caret is not at the end, leave native behaviour
|
||||||
|
*/
|
||||||
|
if (!Caret.isAtEnd) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
/**
|
||||||
}
|
* All the cases below have custom behaviour, so we don't need a native one
|
||||||
|
*/
|
||||||
|
event.preventDefault();
|
||||||
|
this.Editor.Toolbar.close();
|
||||||
|
|
||||||
if (Caret.navigatePrevious()) {
|
const isLastInputFocused = currentBlock.currentInput === currentBlock.lastInput;
|
||||||
Toolbar.close();
|
|
||||||
}
|
/**
|
||||||
|
* For example, caret at the end of the Quote first input (quote text) — just navigate next input (caption)
|
||||||
|
*/
|
||||||
|
if (!isLastInputFocused) {
|
||||||
|
Caret.navigateNext();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Caret.createShadow(targetBlock.pluginsContent);
|
/**
|
||||||
BlockManager.mergeBlocks(targetBlock, blockToMerge)
|
* Delete at the end of the last Block should do nothing
|
||||||
|
*/
|
||||||
|
if (nextBlock === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If next Block is empty, it should be removed just like a character
|
||||||
|
*/
|
||||||
|
if (nextBlock.isEmpty) {
|
||||||
|
BlockManager.removeBlock(nextBlock);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If current Block is empty, just remove it and set cursor to the next Block (like we're removing line break char)
|
||||||
|
*/
|
||||||
|
if (currentBlock.isEmpty) {
|
||||||
|
BlockManager.removeBlock(currentBlock);
|
||||||
|
|
||||||
|
Caret.setToBlock(nextBlock, Caret.positions.START);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bothBlocksMergeable = areBlocksMergeable(currentBlock, nextBlock);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If Blocks could be merged, do it
|
||||||
|
* Otherwise, just navigate to the next block
|
||||||
|
*/
|
||||||
|
if (bothBlocksMergeable) {
|
||||||
|
this.mergeBlocks(currentBlock, nextBlock);
|
||||||
|
} else {
|
||||||
|
Caret.setToBlock(nextBlock, Caret.positions.START);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge passed Blocks
|
||||||
|
*
|
||||||
|
* @param targetBlock - to which Block we want to merge
|
||||||
|
* @param blockToMerge - what Block we want to merge
|
||||||
|
*/
|
||||||
|
private mergeBlocks(targetBlock: Block, blockToMerge: Block): void {
|
||||||
|
const { BlockManager, Caret, Toolbar } = this.Editor;
|
||||||
|
|
||||||
|
Caret.createShadow(targetBlock.lastInput);
|
||||||
|
|
||||||
|
BlockManager
|
||||||
|
.mergeBlocks(targetBlock, blockToMerge)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
/** Restore caret position after merge */
|
/** Restore caret position after merge */
|
||||||
Caret.restoreCaret(targetBlock.pluginsContent as HTMLElement);
|
Caret.restoreCaret(targetBlock.pluginsContent as HTMLElement);
|
||||||
targetBlock.pluginsContent.normalize();
|
|
||||||
Toolbar.close();
|
Toolbar.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -394,9 +530,8 @@ export default class BlockEvents extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close Toolbar and highlighting when user moves cursor
|
* Close Toolbar when user moves cursor
|
||||||
*/
|
*/
|
||||||
this.Editor.BlockManager.clearFocused();
|
|
||||||
this.Editor.Toolbar.close();
|
this.Editor.Toolbar.close();
|
||||||
|
|
||||||
const shouldEnableCBS = this.Editor.Caret.isAtEnd || this.Editor.BlockSelection.anyBlockSelected;
|
const shouldEnableCBS = this.Editor.Caret.isAtEnd || this.Editor.BlockSelection.anyBlockSelected;
|
||||||
|
@ -415,19 +550,21 @@ export default class BlockEvents extends Module {
|
||||||
* Default behaviour moves cursor by 1 character, we need to prevent it
|
* Default behaviour moves cursor by 1 character, we need to prevent it
|
||||||
*/
|
*/
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else {
|
|
||||||
/**
|
return;
|
||||||
* After caret is set, update Block input index
|
|
||||||
*/
|
|
||||||
_.delay(() => {
|
|
||||||
/** Check currentBlock for case when user moves selection out of Editor */
|
|
||||||
if (this.Editor.BlockManager.currentBlock) {
|
|
||||||
this.Editor.BlockManager.currentBlock.updateCurrentInput();
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
||||||
}, 20)();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After caret is set, update Block input index
|
||||||
|
*/
|
||||||
|
_.delay(() => {
|
||||||
|
/** Check currentBlock for case when user moves selection out of Editor */
|
||||||
|
if (this.Editor.BlockManager.currentBlock) {
|
||||||
|
this.Editor.BlockManager.currentBlock.updateCurrentInput();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||||
|
}, 20)();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear blocks selection by arrows
|
* Clear blocks selection by arrows
|
||||||
*/
|
*/
|
||||||
|
@ -453,9 +590,8 @@ export default class BlockEvents extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close Toolbar and highlighting when user moves cursor
|
* Close Toolbar when user moves cursor
|
||||||
*/
|
*/
|
||||||
this.Editor.BlockManager.clearFocused();
|
|
||||||
this.Editor.Toolbar.close();
|
this.Editor.Toolbar.close();
|
||||||
|
|
||||||
const shouldEnableCBS = this.Editor.Caret.isAtStart || this.Editor.BlockSelection.anyBlockSelected;
|
const shouldEnableCBS = this.Editor.Caret.isAtStart || this.Editor.BlockSelection.anyBlockSelected;
|
||||||
|
@ -474,19 +610,21 @@ export default class BlockEvents extends Module {
|
||||||
* Default behaviour moves cursor by 1 character, we need to prevent it
|
* Default behaviour moves cursor by 1 character, we need to prevent it
|
||||||
*/
|
*/
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else {
|
|
||||||
/**
|
return;
|
||||||
* After caret is set, update Block input index
|
|
||||||
*/
|
|
||||||
_.delay(() => {
|
|
||||||
/** Check currentBlock for case when user ends selection out of Editor and then press arrow-key */
|
|
||||||
if (this.Editor.BlockManager.currentBlock) {
|
|
||||||
this.Editor.BlockManager.currentBlock.updateCurrentInput();
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
||||||
}, 20)();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After caret is set, update Block input index
|
||||||
|
*/
|
||||||
|
_.delay(() => {
|
||||||
|
/** Check currentBlock for case when user ends selection out of Editor and then press arrow-key */
|
||||||
|
if (this.Editor.BlockManager.currentBlock) {
|
||||||
|
this.Editor.BlockManager.currentBlock.updateCurrentInput();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||||
|
}, 20)();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear blocks selection by arrows
|
* Clear blocks selection by arrows
|
||||||
*/
|
*/
|
||||||
|
@ -536,7 +674,6 @@ export default class BlockEvents extends Module {
|
||||||
*/
|
*/
|
||||||
private activateBlockSettings(): void {
|
private activateBlockSettings(): void {
|
||||||
if (!this.Editor.Toolbar.opened) {
|
if (!this.Editor.Toolbar.opened) {
|
||||||
this.Editor.BlockManager.currentBlock.focused = true;
|
|
||||||
this.Editor.Toolbar.moveAndOpen();
|
this.Editor.Toolbar.moveAndOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,15 @@ import Blocks from '../blocks';
|
||||||
import { BlockToolData, PasteEvent } from '../../../types';
|
import { BlockToolData, PasteEvent } from '../../../types';
|
||||||
import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
|
import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
|
||||||
import BlockAPI from '../block/api';
|
import BlockAPI from '../block/api';
|
||||||
import { BlockMutationType } from '../../../types/events/block/mutation-type';
|
import { BlockMutationEventMap, BlockMutationType } from '../../../types/events/block';
|
||||||
|
import { BlockRemovedMutationType } from '../../../types/events/block/BlockRemoved';
|
||||||
|
import { BlockAddedMutationType } from '../../../types/events/block/BlockAdded';
|
||||||
|
import { BlockMovedMutationType } from '../../../types/events/block/BlockMoved';
|
||||||
|
import { BlockChangedMutationType } from '../../../types/events/block/BlockChanged';
|
||||||
|
import { BlockChanged } from '../events';
|
||||||
|
import { clean, sanitizeBlocks } from '../utils/sanitizer';
|
||||||
|
import { convertStringToBlockData, isBlockConvertable } from '../utils/blocks';
|
||||||
|
import PromiseQueue from '../utils/promise-queue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {BlockManager} BlockManager
|
* @typedef {BlockManager} BlockManager
|
||||||
|
@ -35,14 +43,6 @@ export default class BlockManager extends Module {
|
||||||
* @param {number} newIndex - index of Block to set as current
|
* @param {number} newIndex - index of Block to set as current
|
||||||
*/
|
*/
|
||||||
public set currentBlockIndex(newIndex: number) {
|
public set currentBlockIndex(newIndex: number) {
|
||||||
if (this._blocks[this._currentBlockIndex]) {
|
|
||||||
this._blocks[this._currentBlockIndex].willUnselect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._blocks[newIndex]) {
|
|
||||||
this._blocks[newIndex].willSelect();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._currentBlockIndex = newIndex;
|
this._currentBlockIndex = newIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ export default class BlockManager extends Module {
|
||||||
*
|
*
|
||||||
* @returns {Block}
|
* @returns {Block}
|
||||||
*/
|
*/
|
||||||
public get currentBlock(): Block {
|
public get currentBlock(): Block | undefined {
|
||||||
return this._blocks[this.currentBlockIndex];
|
return this._blocks[this.currentBlockIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ export default class BlockManager extends Module {
|
||||||
*
|
*
|
||||||
* @returns {Block|null}
|
* @returns {Block|null}
|
||||||
*/
|
*/
|
||||||
public get nextBlock(): Block {
|
public get nextBlock(): Block | null {
|
||||||
const isLastBlock = this.currentBlockIndex === (this._blocks.length - 1);
|
const isLastBlock = this.currentBlockIndex === (this._blocks.length - 1);
|
||||||
|
|
||||||
if (isLastBlock) {
|
if (isLastBlock) {
|
||||||
|
@ -124,7 +124,7 @@ export default class BlockManager extends Module {
|
||||||
*
|
*
|
||||||
* @returns {Block|null}
|
* @returns {Block|null}
|
||||||
*/
|
*/
|
||||||
public get previousBlock(): Block {
|
public get previousBlock(): Block | null {
|
||||||
const isFirstBlock = this.currentBlockIndex === 0;
|
const isFirstBlock = this.currentBlockIndex === 0;
|
||||||
|
|
||||||
if (isFirstBlock) {
|
if (isFirstBlock) {
|
||||||
|
@ -242,10 +242,12 @@ export default class BlockManager extends Module {
|
||||||
api: this.Editor.API,
|
api: this.Editor.API,
|
||||||
readOnly,
|
readOnly,
|
||||||
tunesData,
|
tunesData,
|
||||||
});
|
}, this.eventsDispatcher);
|
||||||
|
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
this.bindBlockEvents(block);
|
window.requestIdleCallback(() => {
|
||||||
|
this.bindBlockEvents(block);
|
||||||
|
}, { timeout: 2000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return block;
|
return block;
|
||||||
|
@ -298,7 +300,7 @@ export default class BlockManager extends Module {
|
||||||
* we need to dispatch the 'block-removing' event for the replacing block
|
* we need to dispatch the 'block-removing' event for the replacing block
|
||||||
*/
|
*/
|
||||||
if (replace) {
|
if (replace) {
|
||||||
this.blockDidMutated(BlockMutationType.Removed, this.getBlockByIndex(newIndex), {
|
this.blockDidMutated(BlockRemovedMutationType, this.getBlockByIndex(newIndex), {
|
||||||
index: newIndex,
|
index: newIndex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -308,7 +310,7 @@ export default class BlockManager extends Module {
|
||||||
/**
|
/**
|
||||||
* Force call of didMutated event on Block insertion
|
* Force call of didMutated event on Block insertion
|
||||||
*/
|
*/
|
||||||
this.blockDidMutated(BlockMutationType.Added, block, {
|
this.blockDidMutated(BlockAddedMutationType, block, {
|
||||||
index: newIndex,
|
index: newIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -322,21 +324,59 @@ export default class BlockManager extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace current working block
|
* Inserts several blocks at once
|
||||||
*
|
*
|
||||||
* @param {object} options - replace options
|
* @param blocks - blocks to insert
|
||||||
* @param {string} options.tool — plugin name
|
* @param index - index where to insert
|
||||||
* @param {BlockToolData} options.data — plugin data
|
|
||||||
* @returns {Block}
|
|
||||||
*/
|
*/
|
||||||
public replace({
|
public insertMany(blocks: Block[], index = 0): void {
|
||||||
tool = this.config.defaultBlock,
|
this._blocks.insertMany(blocks, index);
|
||||||
data = {},
|
}
|
||||||
}): Block {
|
|
||||||
|
/**
|
||||||
|
* Update Block data.
|
||||||
|
*
|
||||||
|
* Currently we don't have an 'update' method in the Tools API, so we just create a new block with the same id and type
|
||||||
|
* Should not trigger 'block-removed' or 'block-added' events
|
||||||
|
*
|
||||||
|
* @param block - block to update
|
||||||
|
* @param data - new data
|
||||||
|
*/
|
||||||
|
public async update(block: Block, data: Partial<BlockToolData>): Promise<Block> {
|
||||||
|
const existingData = await block.data;
|
||||||
|
|
||||||
|
const newBlock = this.composeBlock({
|
||||||
|
id: block.id,
|
||||||
|
tool: block.name,
|
||||||
|
data: Object.assign({}, existingData, data),
|
||||||
|
tunes: block.tunes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blockIndex = this.getBlockIndex(block);
|
||||||
|
|
||||||
|
this._blocks.replace(blockIndex, newBlock);
|
||||||
|
|
||||||
|
this.blockDidMutated(BlockChangedMutationType, newBlock, {
|
||||||
|
index: blockIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
return newBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace passed Block with the new one with specified Tool and data
|
||||||
|
*
|
||||||
|
* @param block - block to replace
|
||||||
|
* @param newTool - new Tool name
|
||||||
|
* @param data - new Tool data
|
||||||
|
*/
|
||||||
|
public replace(block: Block, newTool: string, data: BlockToolData): Block {
|
||||||
|
const blockIndex = this.getBlockIndex(block);
|
||||||
|
|
||||||
return this.insert({
|
return this.insert({
|
||||||
tool,
|
tool: newTool,
|
||||||
data,
|
data,
|
||||||
index: this.currentBlockIndex,
|
index: blockIndex,
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -359,7 +399,16 @@ export default class BlockManager extends Module {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
block.call(BlockToolAPI.ON_PASTE, pasteEvent);
|
/**
|
||||||
|
* We need to call onPaste after Block will be ready
|
||||||
|
* because onPaste could change tool's root element, and we need to do that after block.watchBlockMutations() bound
|
||||||
|
* to detect tool root element change
|
||||||
|
*
|
||||||
|
* @todo make this.insert() awaitable and remove requestIdleCallback
|
||||||
|
*/
|
||||||
|
window.requestIdleCallback(() => {
|
||||||
|
block.call(BlockToolAPI.ON_PASTE, pasteEvent);
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_.log(`${toolName}: onPaste callback call is failed`, 'error', e);
|
_.log(`${toolName}: onPaste callback call is failed`, 'error', e);
|
||||||
}
|
}
|
||||||
|
@ -384,7 +433,7 @@ export default class BlockManager extends Module {
|
||||||
/**
|
/**
|
||||||
* Force call of didMutated event on Block insertion
|
* Force call of didMutated event on Block insertion
|
||||||
*/
|
*/
|
||||||
this.blockDidMutated(BlockMutationType.Added, block, {
|
this.blockDidMutated(BlockAddedMutationType, block, {
|
||||||
index,
|
index,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -422,61 +471,90 @@ export default class BlockManager extends Module {
|
||||||
* @returns {Promise} - the sequence that can be continued
|
* @returns {Promise} - the sequence that can be continued
|
||||||
*/
|
*/
|
||||||
public async mergeBlocks(targetBlock: Block, blockToMerge: Block): Promise<void> {
|
public async mergeBlocks(targetBlock: Block, blockToMerge: Block): Promise<void> {
|
||||||
const blockToMergeIndex = this._blocks.indexOf(blockToMerge);
|
let blockToMergeData: BlockToolData | undefined;
|
||||||
|
|
||||||
if (blockToMerge.isEmpty) {
|
/**
|
||||||
|
* We can merge:
|
||||||
|
* 1) Blocks with the same Tool if tool provides merge method
|
||||||
|
*/
|
||||||
|
if (targetBlock.name === blockToMerge.name && targetBlock.mergeable) {
|
||||||
|
const blockToMergeDataRaw = await blockToMerge.data;
|
||||||
|
|
||||||
|
if (_.isEmpty(blockToMergeDataRaw)) {
|
||||||
|
console.error('Could not merge Block. Failed to extract original Block data.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ cleanData ] = sanitizeBlocks([ blockToMergeDataRaw ], targetBlock.tool.sanitizeConfig);
|
||||||
|
|
||||||
|
blockToMergeData = cleanData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2) Blocks with different Tools if they provides conversionConfig
|
||||||
|
*/
|
||||||
|
} else if (targetBlock.mergeable && isBlockConvertable(blockToMerge, 'export') && isBlockConvertable(targetBlock, 'import')) {
|
||||||
|
const blockToMergeDataStringified = await blockToMerge.exportDataAsString();
|
||||||
|
const cleanData = clean(blockToMergeDataStringified, targetBlock.tool.sanitizeConfig);
|
||||||
|
|
||||||
|
blockToMergeData = convertStringToBlockData(cleanData, targetBlock.tool.conversionConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockToMergeData === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockToMergeData = await blockToMerge.data;
|
await targetBlock.mergeWith(blockToMergeData);
|
||||||
|
this.removeBlock(blockToMerge);
|
||||||
if (!_.isEmpty(blockToMergeData)) {
|
|
||||||
await targetBlock.mergeWith(blockToMergeData);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removeBlock(blockToMergeIndex);
|
|
||||||
this.currentBlockIndex = this._blocks.indexOf(targetBlock);
|
this.currentBlockIndex = this._blocks.indexOf(targetBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove block with passed index or remove last
|
* Remove passed Block
|
||||||
*
|
*
|
||||||
* @param {number|null} index - index of Block to remove
|
* @param block - Block to remove
|
||||||
* @throws {Error} if Block to remove is not found
|
* @param addLastBlock - if true, adds new default block at the end. @todo remove this logic and use event-bus instead
|
||||||
*/
|
*/
|
||||||
public removeBlock(index = this.currentBlockIndex): void {
|
public removeBlock(block: Block, addLastBlock = true): Promise<void> {
|
||||||
/**
|
return new Promise((resolve) => {
|
||||||
* If index is not passed and there is no block selected, show a warning
|
const index = this._blocks.indexOf(block);
|
||||||
*/
|
|
||||||
if (!this.validateIndex(index)) {
|
|
||||||
throw new Error('Can\'t find a Block to remove');
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockToRemove = this._blocks[index];
|
/**
|
||||||
|
* If index is not passed and there is no block selected, show a warning
|
||||||
|
*/
|
||||||
|
if (!this.validateIndex(index)) {
|
||||||
|
throw new Error('Can\'t find a Block to remove');
|
||||||
|
}
|
||||||
|
|
||||||
blockToRemove.destroy();
|
block.destroy();
|
||||||
this._blocks.remove(index);
|
this._blocks.remove(index);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force call of didMutated event on Block removal
|
* Force call of didMutated event on Block removal
|
||||||
*/
|
*/
|
||||||
this.blockDidMutated(BlockMutationType.Removed, blockToRemove, {
|
this.blockDidMutated(BlockRemovedMutationType, block, {
|
||||||
index,
|
index,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.currentBlockIndex >= index) {
|
||||||
|
this.currentBlockIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If first Block was removed, insert new Initial Block and set focus on it`s first input
|
||||||
|
*/
|
||||||
|
if (!this.blocks.length) {
|
||||||
|
this.currentBlockIndex = -1;
|
||||||
|
|
||||||
|
if (addLastBlock) {
|
||||||
|
this.insert();
|
||||||
|
}
|
||||||
|
} else if (index === 0) {
|
||||||
|
this.currentBlockIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.currentBlockIndex >= index) {
|
|
||||||
this.currentBlockIndex--;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If first Block was removed, insert new Initial Block and set focus on it`s first input
|
|
||||||
*/
|
|
||||||
if (!this.blocks.length) {
|
|
||||||
this.currentBlockIndex = -1;
|
|
||||||
this.insert();
|
|
||||||
} else if (index === 0) {
|
|
||||||
this.currentBlockIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -496,7 +574,7 @@ export default class BlockManager extends Module {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removeBlock(index);
|
this.removeBlock(this.blocks[index]);
|
||||||
firstSelectedBlockIndex = index;
|
firstSelectedBlockIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -546,13 +624,28 @@ export default class BlockManager extends Module {
|
||||||
return this.insert({ data });
|
return this.insert({ data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Block by passed index
|
||||||
|
*
|
||||||
|
* If we pass -1 as index, the last block will be returned
|
||||||
|
* There shouldn't be a case when there is no blocks at all — at least one always should exist
|
||||||
|
*/
|
||||||
|
public getBlockByIndex(index: -1): Block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Block by passed index.
|
||||||
|
*
|
||||||
|
* Could return undefined if there is no block with such index
|
||||||
|
*/
|
||||||
|
public getBlockByIndex(index: number): Block | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns Block by passed index
|
* Returns Block by passed index
|
||||||
*
|
*
|
||||||
* @param {number} index - index to get. -1 to get last
|
* @param {number} index - index to get. -1 to get last
|
||||||
* @returns {Block}
|
* @returns {Block}
|
||||||
*/
|
*/
|
||||||
public getBlockByIndex(index): Block {
|
public getBlockByIndex(index: number): Block | undefined {
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
index = this._blocks.length - 1;
|
index = this._blocks.length - 1;
|
||||||
}
|
}
|
||||||
|
@ -598,32 +691,6 @@ export default class BlockManager extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove selection from all Blocks then highlight only Current Block
|
|
||||||
*/
|
|
||||||
public highlightCurrentNode(): void {
|
|
||||||
/**
|
|
||||||
* Remove previous selected Block's state
|
|
||||||
*/
|
|
||||||
this.clearFocused();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark current Block as selected
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.currentBlock.focused = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove selection from all Blocks
|
|
||||||
*/
|
|
||||||
public clearFocused(): void {
|
|
||||||
this.blocks.forEach((block) => {
|
|
||||||
block.focused = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1) Find first-level Block from passed child Node
|
* 1) Find first-level Block from passed child Node
|
||||||
* 2) Mark it as current
|
* 2) Mark it as current
|
||||||
|
@ -679,7 +746,11 @@ export default class BlockManager extends Module {
|
||||||
* @param {Node} childNode - node to get Block by
|
* @param {Node} childNode - node to get Block by
|
||||||
* @returns {Block}
|
* @returns {Block}
|
||||||
*/
|
*/
|
||||||
public getBlockByChildNode(childNode: Node): Block {
|
public getBlockByChildNode(childNode: Node): Block | undefined {
|
||||||
|
if (!childNode || childNode instanceof Node === false) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If node is Text TextNode
|
* If node is Text TextNode
|
||||||
*/
|
*/
|
||||||
|
@ -736,19 +807,74 @@ export default class BlockManager extends Module {
|
||||||
/**
|
/**
|
||||||
* Force call of didMutated event on Block movement
|
* Force call of didMutated event on Block movement
|
||||||
*/
|
*/
|
||||||
this.blockDidMutated(BlockMutationType.Moved, this.currentBlock, {
|
this.blockDidMutated(BlockMovedMutationType, this.currentBlock, {
|
||||||
fromIndex,
|
fromIndex,
|
||||||
toIndex,
|
toIndex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts passed Block to the new Tool
|
||||||
|
* Uses Conversion Config
|
||||||
|
*
|
||||||
|
* @param blockToConvert - Block that should be converted
|
||||||
|
* @param targetToolName - name of the Tool to convert to
|
||||||
|
* @param blockDataOverrides - optional new Block data overrides
|
||||||
|
*/
|
||||||
|
public async convert(blockToConvert: Block, targetToolName: string, blockDataOverrides?: BlockToolData): Promise<Block> {
|
||||||
|
/**
|
||||||
|
* At first, we get current Block data
|
||||||
|
*/
|
||||||
|
const savedBlock = await blockToConvert.save();
|
||||||
|
|
||||||
|
if (!savedBlock) {
|
||||||
|
throw new Error('Could not convert Block. Failed to extract original Block data.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getting a class of the replacing Tool
|
||||||
|
*/
|
||||||
|
const replacingTool = this.Editor.Tools.blockTools.get(targetToolName);
|
||||||
|
|
||||||
|
if (!replacingTool) {
|
||||||
|
throw new Error(`Could not convert Block. Tool «${targetToolName}» not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Using Conversion Config "export" we get a stringified version of the Block data
|
||||||
|
*/
|
||||||
|
const exportedData = await blockToConvert.exportDataAsString();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean exported data with replacing sanitizer config
|
||||||
|
*/
|
||||||
|
const cleanData: string = clean(
|
||||||
|
exportedData,
|
||||||
|
replacingTool.sanitizeConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now using Conversion Config "import" we compose a new Block data
|
||||||
|
*/
|
||||||
|
let newBlockData = convertStringToBlockData(cleanData, replacingTool.conversionConfig);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional data overrides.
|
||||||
|
* Used for example, by the Multiple Toolbox Items feature, where a single Tool provides several Toolbox items with "data" overrides
|
||||||
|
*/
|
||||||
|
if (blockDataOverrides) {
|
||||||
|
newBlockData = Object.assign(newBlockData, blockDataOverrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.replace(blockToConvert, replacingTool.name, newBlockData);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets current Block Index -1 which means unknown
|
* Sets current Block Index -1 which means unknown
|
||||||
* and clear highlights
|
* and clear highlights
|
||||||
*/
|
*/
|
||||||
public dropPointer(): void {
|
public dropPointer(): void {
|
||||||
this.currentBlockIndex = -1;
|
this.currentBlockIndex = -1;
|
||||||
this.clearFocused();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -758,8 +884,17 @@ export default class BlockManager extends Module {
|
||||||
* we don't need to add an empty default block
|
* we don't need to add an empty default block
|
||||||
* 2) in api.blocks.clear we should add empty block
|
* 2) in api.blocks.clear we should add empty block
|
||||||
*/
|
*/
|
||||||
public clear(needToAddDefaultBlock = false): void {
|
public async clear(needToAddDefaultBlock = false): Promise<void> {
|
||||||
this._blocks.removeAll();
|
const queue = new PromiseQueue();
|
||||||
|
|
||||||
|
this.blocks.forEach((block) => {
|
||||||
|
queue.add(async () => {
|
||||||
|
await this.removeBlock(block, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await queue.completed;
|
||||||
|
|
||||||
this.dropPointer();
|
this.dropPointer();
|
||||||
|
|
||||||
if (needToAddDefaultBlock) {
|
if (needToAddDefaultBlock) {
|
||||||
|
@ -807,7 +942,7 @@ export default class BlockManager extends Module {
|
||||||
});
|
});
|
||||||
|
|
||||||
block.on('didMutated', (affectedBlock: Block) => {
|
block.on('didMutated', (affectedBlock: Block) => {
|
||||||
return this.blockDidMutated(BlockMutationType.Changed, affectedBlock, {
|
return this.blockDidMutated(BlockChangedMutationType, affectedBlock, {
|
||||||
index: this.getBlockIndex(affectedBlock),
|
index: this.getBlockIndex(affectedBlock),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -851,18 +986,25 @@ export default class BlockManager extends Module {
|
||||||
*
|
*
|
||||||
* @param mutationType - what happened with block
|
* @param mutationType - what happened with block
|
||||||
* @param block - mutated block
|
* @param block - mutated block
|
||||||
* @param details - additional data to pass with change event
|
* @param detailData - additional data to pass with change event
|
||||||
*/
|
*/
|
||||||
private blockDidMutated(mutationType: BlockMutationType, block: Block, details: Record<string, unknown> = {}): Block {
|
private blockDidMutated<Type extends BlockMutationType>(mutationType: Type, block: Block, detailData: BlockMutationEventDetailWithoutTarget<Type>): Block {
|
||||||
const event = new CustomEvent(mutationType, {
|
const event = new CustomEvent(mutationType, {
|
||||||
detail: {
|
detail: {
|
||||||
target: new BlockAPI(block),
|
target: new BlockAPI(block),
|
||||||
...details,
|
...detailData as BlockMutationEventDetailWithoutTarget<Type>,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.Editor.ModificationsObserver.onChange(event);
|
this.eventsDispatcher.emit(BlockChanged, {
|
||||||
|
event: event as BlockMutationEventMap[Type],
|
||||||
|
});
|
||||||
|
|
||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type alias for Block Mutation event without 'target' field, used in 'blockDidMutated' method
|
||||||
|
*/
|
||||||
|
type BlockMutationEventDetailWithoutTarget<Type extends BlockMutationType> = Omit<BlockMutationEventMap[Type]['detail'], 'target'>;
|
||||||
|
|
|
@ -321,26 +321,28 @@ export default class BlockSelection extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* select Block
|
* Select Block by its index
|
||||||
*
|
*
|
||||||
* @param {number?} index - Block index according to the BlockManager's indexes
|
* @param {number?} index - Block index according to the BlockManager's indexes
|
||||||
*/
|
*/
|
||||||
public selectBlockByIndex(index?): void {
|
public selectBlockByIndex(index: number): void {
|
||||||
const { BlockManager } = this.Editor;
|
const { BlockManager } = this.Editor;
|
||||||
|
|
||||||
/**
|
const block = BlockManager.getBlockByIndex(index);
|
||||||
* Remove previous focused Block's state
|
|
||||||
*/
|
|
||||||
BlockManager.clearFocused();
|
|
||||||
|
|
||||||
let block;
|
if (block === undefined) {
|
||||||
|
return;
|
||||||
if (isNaN(index)) {
|
|
||||||
block = BlockManager.currentBlock;
|
|
||||||
} else {
|
|
||||||
block = BlockManager.getBlockByIndex(index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.selectBlock(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select passed Block
|
||||||
|
*
|
||||||
|
* @param {Block} block - Block to select
|
||||||
|
*/
|
||||||
|
public selectBlock(block: Block): void {
|
||||||
/** Save selection */
|
/** Save selection */
|
||||||
this.selection.save();
|
this.selection.save();
|
||||||
SelectionUtils.get()
|
SelectionUtils.get()
|
||||||
|
@ -354,6 +356,17 @@ export default class BlockSelection extends Module {
|
||||||
this.Editor.InlineToolbar.close();
|
this.Editor.InlineToolbar.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove selection from passed Block
|
||||||
|
*
|
||||||
|
* @param {Block} block - Block to unselect
|
||||||
|
*/
|
||||||
|
public unselectBlock(block: Block): void {
|
||||||
|
block.selected = false;
|
||||||
|
|
||||||
|
this.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear anyBlockSelected cache
|
* Clear anyBlockSelected cache
|
||||||
*/
|
*/
|
||||||
|
@ -432,7 +445,7 @@ export default class BlockSelection extends Module {
|
||||||
/**
|
/**
|
||||||
* select working Block
|
* select working Block
|
||||||
*/
|
*/
|
||||||
this.selectBlockByIndex();
|
this.selectBlock(workingBlock);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable all Blocks selection if current Block is selected
|
* Enable all Blocks selection if current Block is selected
|
||||||
|
|
|
@ -11,7 +11,6 @@ import Selection from '../selection';
|
||||||
import Module from '../__module';
|
import Module from '../__module';
|
||||||
import Block from '../block';
|
import Block from '../block';
|
||||||
import $ from '../dom';
|
import $ from '../dom';
|
||||||
import * as _ from '../utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Caret} Caret
|
* @typedef {Caret} Caret
|
||||||
|
@ -46,8 +45,17 @@ export default class Caret extends Module {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
public get isAtStart(): boolean {
|
public get isAtStart(): boolean {
|
||||||
|
const { currentBlock } = this.Editor.BlockManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If Block does not contain inputs, treat caret as "at start"
|
||||||
|
*/
|
||||||
|
if (!currentBlock?.focusable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const selection = Selection.get();
|
const selection = Selection.get();
|
||||||
const firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput);
|
const firstNode = $.getDeepestNode(currentBlock.currentInput);
|
||||||
let focusNode = selection.focusNode;
|
let focusNode = selection.focusNode;
|
||||||
|
|
||||||
/** In case lastNode is native input */
|
/** In case lastNode is native input */
|
||||||
|
@ -138,10 +146,19 @@ export default class Caret extends Module {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
public get isAtEnd(): boolean {
|
public get isAtEnd(): boolean {
|
||||||
|
const { currentBlock } = this.Editor.BlockManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If Block does not contain inputs, treat caret as "at end"
|
||||||
|
*/
|
||||||
|
if (!currentBlock.focusable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const selection = Selection.get();
|
const selection = Selection.get();
|
||||||
let focusNode = selection.focusNode;
|
let focusNode = selection.focusNode;
|
||||||
|
|
||||||
const lastNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput, true);
|
const lastNode = $.getDeepestNode(currentBlock.currentInput, true);
|
||||||
|
|
||||||
/** In case lastNode is native input */
|
/** In case lastNode is native input */
|
||||||
if ($.isNativeInput(lastNode)) {
|
if ($.isNativeInput(lastNode)) {
|
||||||
|
@ -224,7 +241,31 @@ export default class Caret extends Module {
|
||||||
* @param {number} offset - caret offset regarding to the text node
|
* @param {number} offset - caret offset regarding to the text node
|
||||||
*/
|
*/
|
||||||
public setToBlock(block: Block, position: string = this.positions.DEFAULT, offset = 0): void {
|
public setToBlock(block: Block, position: string = this.positions.DEFAULT, offset = 0): void {
|
||||||
const { BlockManager } = this.Editor;
|
const { BlockManager, BlockSelection } = this.Editor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear previous selection since we possible will select the new Block
|
||||||
|
*/
|
||||||
|
BlockSelection.clearSelection();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If Block is not focusable, just select (highlight) it
|
||||||
|
*/
|
||||||
|
if (!block.focusable) {
|
||||||
|
/**
|
||||||
|
* Hide current cursor
|
||||||
|
*/
|
||||||
|
window.getSelection()?.removeAllRanges();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight Block
|
||||||
|
*/
|
||||||
|
BlockSelection.selectBlock(block);
|
||||||
|
BlockManager.currentBlock = block;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let element;
|
let element;
|
||||||
|
|
||||||
switch (position) {
|
switch (position) {
|
||||||
|
@ -255,13 +296,7 @@ export default class Caret extends Module {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
this.set(nodeToSet as HTMLElement, offset);
|
||||||
* @todo try to fix via Promises or use querySelectorAll to not to use timeout
|
|
||||||
*/
|
|
||||||
_.delay(() => {
|
|
||||||
this.set(nodeToSet as HTMLElement, offset);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
||||||
}, 20)();
|
|
||||||
|
|
||||||
BlockManager.setCurrentBlockByChildNode(block.holder);
|
BlockManager.setCurrentBlockByChildNode(block.holder);
|
||||||
BlockManager.currentBlock.currentInput = element;
|
BlockManager.currentBlock.currentInput = element;
|
||||||
|
@ -304,16 +339,17 @@ export default class Caret extends Module {
|
||||||
* @param {number} offset - offset
|
* @param {number} offset - offset
|
||||||
*/
|
*/
|
||||||
public set(element: HTMLElement, offset = 0): void {
|
public set(element: HTMLElement, offset = 0): void {
|
||||||
|
const scrollOffset = 30;
|
||||||
const { top, bottom } = Selection.setCursor(element, offset);
|
const { top, bottom } = Selection.setCursor(element, offset);
|
||||||
|
|
||||||
/** If new cursor position is not visible, scroll to it */
|
|
||||||
const { innerHeight } = window;
|
const { innerHeight } = window;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If new cursor position is not visible, scroll to it
|
||||||
|
*/
|
||||||
if (top < 0) {
|
if (top < 0) {
|
||||||
window.scrollBy(0, top);
|
window.scrollBy(0, top - scrollOffset);
|
||||||
}
|
} else if (bottom > innerHeight) {
|
||||||
if (bottom > innerHeight) {
|
window.scrollBy(0, bottom - innerHeight + scrollOffset);
|
||||||
window.scrollBy(0, bottom - innerHeight);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -387,17 +423,25 @@ export default class Caret extends Module {
|
||||||
* Before moving caret, we should check if caret position is at the end of Plugins node
|
* Before moving caret, we should check if caret position is at the end of Plugins node
|
||||||
* Using {@link Dom#getDeepestNode} to get a last node and match with current selection
|
* Using {@link Dom#getDeepestNode} to get a last node and match with current selection
|
||||||
*
|
*
|
||||||
* @returns {boolean}
|
* @param {boolean} force - pass true to skip check for caret position
|
||||||
*/
|
*/
|
||||||
public navigateNext(): boolean {
|
public navigateNext(force = false): boolean {
|
||||||
const { BlockManager } = this.Editor;
|
const { BlockManager } = this.Editor;
|
||||||
const { currentBlock, nextContentfulBlock } = BlockManager;
|
const { currentBlock, nextBlock } = BlockManager;
|
||||||
const { nextInput } = currentBlock;
|
const { nextInput } = currentBlock;
|
||||||
const isAtEnd = this.isAtEnd;
|
const isAtEnd = this.isAtEnd;
|
||||||
|
let blockToNavigate = nextBlock;
|
||||||
|
|
||||||
let nextBlock = nextContentfulBlock;
|
const navigationAllowed = force || isAtEnd;
|
||||||
|
|
||||||
if (!nextBlock && !nextInput) {
|
/** If next Tool`s input exists, focus on it. Otherwise set caret to the next Block */
|
||||||
|
if (nextInput && navigationAllowed) {
|
||||||
|
this.setToInput(nextInput, this.positions.START);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockToNavigate === null) {
|
||||||
/**
|
/**
|
||||||
* This code allows to exit from the last non-initial tool:
|
* This code allows to exit from the last non-initial tool:
|
||||||
* https://github.com/codex-team/editor.js/issues/1103
|
* https://github.com/codex-team/editor.js/issues/1103
|
||||||
|
@ -408,7 +452,7 @@ export default class Caret extends Module {
|
||||||
* 2. If there is a last block and it is non-default --> and caret not at the end <--, do nothing
|
* 2. If there is a last block and it is non-default --> and caret not at the end <--, do nothing
|
||||||
* (https://github.com/codex-team/editor.js/issues/1414)
|
* (https://github.com/codex-team/editor.js/issues/1414)
|
||||||
*/
|
*/
|
||||||
if (currentBlock.tool.isDefault || !isAtEnd) {
|
if (currentBlock.tool.isDefault || !navigationAllowed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,16 +460,11 @@ export default class Caret extends Module {
|
||||||
* If there is no nextBlock, but currentBlock is not default,
|
* If there is no nextBlock, but currentBlock is not default,
|
||||||
* insert new default block at the end and navigate to it
|
* insert new default block at the end and navigate to it
|
||||||
*/
|
*/
|
||||||
nextBlock = BlockManager.insertAtEnd();
|
blockToNavigate = BlockManager.insertAtEnd() as Block;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAtEnd) {
|
if (navigationAllowed) {
|
||||||
/** If next Tool`s input exists, focus on it. Otherwise set caret to the next Block */
|
this.setToBlock(blockToNavigate, this.positions.START);
|
||||||
if (!nextInput) {
|
|
||||||
this.setToBlock(nextBlock, this.positions.START);
|
|
||||||
} else {
|
|
||||||
this.setToInput(nextInput, this.positions.START);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -438,28 +477,27 @@ export default class Caret extends Module {
|
||||||
* Before moving caret, we should check if caret position is start of the Plugins node
|
* Before moving caret, we should check if caret position is start of the Plugins node
|
||||||
* Using {@link Dom#getDeepestNode} to get a last node and match with current selection
|
* Using {@link Dom#getDeepestNode} to get a last node and match with current selection
|
||||||
*
|
*
|
||||||
* @returns {boolean}
|
* @param {boolean} force - pass true to skip check for caret position
|
||||||
*/
|
*/
|
||||||
public navigatePrevious(): boolean {
|
public navigatePrevious(force = false): boolean {
|
||||||
const { currentBlock, previousContentfulBlock } = this.Editor.BlockManager;
|
const { currentBlock, previousBlock } = this.Editor.BlockManager;
|
||||||
|
|
||||||
if (!currentBlock) {
|
if (!currentBlock) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { previousInput } = currentBlock;
|
const { previousInput } = currentBlock;
|
||||||
|
const navigationAllowed = force || this.isAtStart;
|
||||||
|
|
||||||
if (!previousContentfulBlock && !previousInput) {
|
/** If previous Tool`s input exists, focus on it. Otherwise set caret to the previous Block */
|
||||||
return false;
|
if (previousInput && navigationAllowed) {
|
||||||
|
this.setToInput(previousInput, this.positions.END);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isAtStart) {
|
if (previousBlock !== null && navigationAllowed) {
|
||||||
/** If previous Tool`s input exists, focus on it. Otherwise set caret to the previous Block */
|
this.setToBlock(previousBlock as Block, this.positions.END);
|
||||||
if (!previousInput) {
|
|
||||||
this.setToBlock(previousContentfulBlock, this.positions.END);
|
|
||||||
} else {
|
|
||||||
this.setToInput(previousInput, this.positions.END);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -503,13 +541,10 @@ export default class Caret extends Module {
|
||||||
|
|
||||||
sel.expandToTag(shadowCaret as HTMLElement);
|
sel.expandToTag(shadowCaret as HTMLElement);
|
||||||
|
|
||||||
setTimeout(() => {
|
const newRange = document.createRange();
|
||||||
const newRange = document.createRange();
|
|
||||||
|
|
||||||
newRange.selectNode(shadowCaret);
|
newRange.selectNode(shadowCaret);
|
||||||
newRange.extractContents();
|
newRange.extractContents();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
||||||
}, 50);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -534,7 +569,7 @@ export default class Caret extends Module {
|
||||||
fragment.appendChild(new Text());
|
fragment.appendChild(new Text());
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastChild = fragment.lastChild;
|
const lastChild = fragment.lastChild as ChildNode;
|
||||||
|
|
||||||
range.deleteContents();
|
range.deleteContents();
|
||||||
range.insertNode(fragment);
|
range.insertNode(fragment);
|
||||||
|
@ -542,7 +577,11 @@ export default class Caret extends Module {
|
||||||
/** Cross-browser caret insertion */
|
/** Cross-browser caret insertion */
|
||||||
const newRange = document.createRange();
|
const newRange = document.createRange();
|
||||||
|
|
||||||
newRange.setStart(lastChild, lastChild.textContent.length);
|
const nodeToSetCaret = lastChild.nodeType === Node.TEXT_NODE ? lastChild : lastChild.firstChild;
|
||||||
|
|
||||||
|
if (nodeToSetCaret !== null && nodeToSetCaret.textContent !== null) {
|
||||||
|
newRange.setStart(nodeToSetCaret, nodeToSetCaret.textContent.length);
|
||||||
|
}
|
||||||
|
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(newRange);
|
selection.addRange(newRange);
|
||||||
|
|
|
@ -48,11 +48,11 @@ export default class CrossBlockSelection extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* return boolean is cross block selection started
|
* Return boolean is cross block selection started:
|
||||||
|
* there should be at least 2 selected blocks
|
||||||
*/
|
*/
|
||||||
public get isCrossBlockSelectionStarted(): boolean {
|
public get isCrossBlockSelectionStarted(): boolean {
|
||||||
return !!this.firstSelectedBlock &&
|
return !!this.firstSelectedBlock && !!this.lastSelectedBlock && this.firstSelectedBlock !== this.lastSelectedBlock;
|
||||||
!!this.lastSelectedBlock;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -130,11 +130,6 @@ export default class CrossBlockSelection extends Module {
|
||||||
default:
|
default:
|
||||||
Caret.setToBlock(BlockManager.blocks[Math.max(fIndex, lIndex)], Caret.positions.END);
|
Caret.setToBlock(BlockManager.blocks[Math.max(fIndex, lIndex)], Caret.positions.END);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
/**
|
|
||||||
* By default set caret at the end of the last selected block
|
|
||||||
*/
|
|
||||||
Caret.setToBlock(BlockManager.blocks[Math.max(fIndex, lIndex)], Caret.positions.END);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,6 +182,13 @@ export default class CrossBlockSelection extends Module {
|
||||||
private onMouseOver = (event: MouseEvent): void => {
|
private onMouseOver = (event: MouseEvent): void => {
|
||||||
const { BlockManager, BlockSelection } = this.Editor;
|
const { BlockManager, BlockSelection } = this.Editor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Probably, editor is not initialized yet
|
||||||
|
*/
|
||||||
|
if (event.relatedTarget === null && event.target === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const relatedBlock = BlockManager.getBlockByChildNode(event.relatedTarget as Node) || this.lastSelectedBlock;
|
const relatedBlock = BlockManager.getBlockByChildNode(event.relatedTarget as Node) || this.lastSelectedBlock;
|
||||||
const targetBlock = BlockManager.getBlockByChildNode(event.target as Node);
|
const targetBlock = BlockManager.getBlockByChildNode(event.target as Node);
|
||||||
|
|
||||||
|
|
81
src/components/modules/index.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/** ./api */
|
||||||
|
import BlocksAPI from './api/blocks';
|
||||||
|
import CaretAPI from './api/caret';
|
||||||
|
import EventsAPI from './api/events';
|
||||||
|
import I18nAPI from './api/i18n';
|
||||||
|
import API from './api/index';
|
||||||
|
import InlineToolbarAPI from './api/inlineToolbar';
|
||||||
|
import ListenersAPI from './api/listeners';
|
||||||
|
import NotifierAPI from './api/notifier';
|
||||||
|
import ReadOnlyAPI from './api/readonly';
|
||||||
|
import SanitizerAPI from './api/sanitizer';
|
||||||
|
import SaverAPI from './api/saver';
|
||||||
|
import SelectionAPI from './api/selection';
|
||||||
|
import StylesAPI from './api/styles';
|
||||||
|
import ToolbarAPI from './api/toolbar';
|
||||||
|
import TooltipAPI from './api/tooltip';
|
||||||
|
import UiAPI from './api/ui';
|
||||||
|
|
||||||
|
/** ./toolbar */
|
||||||
|
import BlockSettings from './toolbar/blockSettings';
|
||||||
|
import ConversionToolbar from './toolbar/conversion';
|
||||||
|
import Toolbar from './toolbar/index';
|
||||||
|
import InlineToolbar from './toolbar/inline';
|
||||||
|
|
||||||
|
/** . */
|
||||||
|
import BlockEvents from './blockEvents';
|
||||||
|
import BlockManager from './blockManager';
|
||||||
|
import BlockSelection from './blockSelection';
|
||||||
|
import Caret from './caret';
|
||||||
|
import CrossBlockSelection from './crossBlockSelection';
|
||||||
|
import DragNDrop from './dragNDrop';
|
||||||
|
import ModificationsObserver from './modificationsObserver';
|
||||||
|
import Paste from './paste';
|
||||||
|
import ReadOnly from './readonly';
|
||||||
|
import RectangleSelection from './rectangleSelection';
|
||||||
|
import Renderer from './renderer';
|
||||||
|
import Saver from './saver';
|
||||||
|
import Tools from './tools';
|
||||||
|
import UI from './ui';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// API Modules
|
||||||
|
BlocksAPI,
|
||||||
|
CaretAPI,
|
||||||
|
EventsAPI,
|
||||||
|
I18nAPI,
|
||||||
|
API,
|
||||||
|
InlineToolbarAPI,
|
||||||
|
ListenersAPI,
|
||||||
|
NotifierAPI,
|
||||||
|
ReadOnlyAPI,
|
||||||
|
SanitizerAPI,
|
||||||
|
SaverAPI,
|
||||||
|
SelectionAPI,
|
||||||
|
StylesAPI,
|
||||||
|
ToolbarAPI,
|
||||||
|
TooltipAPI,
|
||||||
|
UiAPI,
|
||||||
|
|
||||||
|
// Toolbar Modules
|
||||||
|
BlockSettings,
|
||||||
|
ConversionToolbar,
|
||||||
|
Toolbar,
|
||||||
|
InlineToolbar,
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
BlockEvents,
|
||||||
|
BlockManager,
|
||||||
|
BlockSelection,
|
||||||
|
Caret,
|
||||||
|
CrossBlockSelection,
|
||||||
|
DragNDrop,
|
||||||
|
ModificationsObserver,
|
||||||
|
Paste,
|
||||||
|
ReadOnly,
|
||||||
|
RectangleSelection,
|
||||||
|
Renderer,
|
||||||
|
Saver,
|
||||||
|
Tools,
|
||||||
|
UI,
|
||||||
|
};
|
|
@ -1,6 +1,15 @@
|
||||||
|
import { BlockId } from '../../../types';
|
||||||
|
import { BlockMutationEvent, BlockMutationType } from '../../../types/events/block';
|
||||||
|
import { ModuleConfig } from '../../types-internal/module-config';
|
||||||
import Module from '../__module';
|
import Module from '../__module';
|
||||||
|
import { BlockChanged, FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events';
|
||||||
import * as _ from '../utils';
|
import * as _ from '../utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We use map of block mutations to filter only unique events
|
||||||
|
*/
|
||||||
|
type UniqueBlockMutationKey = `block:${BlockId}:event:${BlockMutationType}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single entry point for Block mutation events
|
* Single entry point for Block mutation events
|
||||||
*/
|
*/
|
||||||
|
@ -10,10 +19,74 @@ export default class ModificationsObserver extends Module {
|
||||||
*/
|
*/
|
||||||
private disabled = false;
|
private disabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks wrapper mutation observer instance
|
||||||
|
*/
|
||||||
|
private readonly mutationObserver: MutationObserver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout used to batched several events in a single onChange call
|
||||||
|
*/
|
||||||
|
private batchingTimeout: null | ReturnType<typeof setTimeout> = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of onChange events used to batch them
|
||||||
|
*
|
||||||
|
* Map is used to filter duplicated events related to the same block
|
||||||
|
*/
|
||||||
|
private batchingOnChangeQueue = new Map<UniqueBlockMutationKey, BlockMutationEvent>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired onChange events will be batched by this time
|
||||||
|
*/
|
||||||
|
private readonly batchTime = 400;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the module
|
||||||
|
*
|
||||||
|
* @param options - options used by the modification observer module
|
||||||
|
* @param options.config - Editor configuration object
|
||||||
|
* @param options.eventsDispatcher - common Editor event bus
|
||||||
|
*/
|
||||||
|
constructor({ config, eventsDispatcher }: ModuleConfig) {
|
||||||
|
super({
|
||||||
|
config,
|
||||||
|
eventsDispatcher,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mutationObserver = new MutationObserver((mutations) => {
|
||||||
|
this.redactorChanged(mutations);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventsDispatcher.on(BlockChanged, (payload) => {
|
||||||
|
this.particularBlockChanged(payload.event);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutex for fake cursor setting/removing operation
|
||||||
|
*/
|
||||||
|
this.eventsDispatcher.on(FakeCursorAboutToBeToggled, () => {
|
||||||
|
this.disable();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventsDispatcher.on(FakeCursorHaveBeenSet, () => {
|
||||||
|
this.enable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables onChange event
|
* Enables onChange event
|
||||||
*/
|
*/
|
||||||
public enable(): void {
|
public enable(): void {
|
||||||
|
this.mutationObserver.observe(
|
||||||
|
this.Editor.UI.nodes.redactor,
|
||||||
|
{
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
characterData: true,
|
||||||
|
attributes: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
this.disabled = false;
|
this.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +94,7 @@ export default class ModificationsObserver extends Module {
|
||||||
* Disables onChange event
|
* Disables onChange event
|
||||||
*/
|
*/
|
||||||
public disable(): void {
|
public disable(): void {
|
||||||
|
this.mutationObserver.disconnect();
|
||||||
this.disabled = true;
|
this.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,11 +103,45 @@ export default class ModificationsObserver extends Module {
|
||||||
*
|
*
|
||||||
* @param event - some of our custom change events
|
* @param event - some of our custom change events
|
||||||
*/
|
*/
|
||||||
public onChange(event: CustomEvent): void {
|
private particularBlockChanged(event: BlockMutationEvent): void {
|
||||||
if (this.disabled || !_.isFunction(this.config.onChange)) {
|
if (this.disabled || !_.isFunction(this.config.onChange)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.config.onChange(this.Editor.API.methods, event);
|
this.batchingOnChangeQueue.set(`block:${event.detail.target.id}:event:${event.type as BlockMutationType}`, event);
|
||||||
|
|
||||||
|
if (this.batchingTimeout) {
|
||||||
|
clearTimeout(this.batchingTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.batchingTimeout = setTimeout(() => {
|
||||||
|
let eventsToEmit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ih we have only 1 event in a queue, unwrap it
|
||||||
|
*/
|
||||||
|
if (this.batchingOnChangeQueue.size === 1) {
|
||||||
|
eventsToEmit = this.batchingOnChangeQueue.values().next().value;
|
||||||
|
} else {
|
||||||
|
eventsToEmit = Array.from(this.batchingOnChangeQueue.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.onChange) {
|
||||||
|
this.config.onChange(this.Editor.API.methods, eventsToEmit);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.batchingOnChangeQueue.clear();
|
||||||
|
}, this.batchTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired on every blocks wrapper dom change
|
||||||
|
*
|
||||||
|
* @param mutations - mutations happened
|
||||||
|
*/
|
||||||
|
private redactorChanged(mutations: MutationRecord[]): void {
|
||||||
|
this.eventsDispatcher.emit(RedactorDomChanged, {
|
||||||
|
mutations,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -346,6 +346,10 @@ export default class Paste extends Module {
|
||||||
* @param tool - BlockTool object
|
* @param tool - BlockTool object
|
||||||
*/
|
*/
|
||||||
private getTagsConfig(tool: BlockTool): void {
|
private getTagsConfig(tool: BlockTool): void {
|
||||||
|
if (tool.pasteConfig === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tagsOrSanitizeConfigs = tool.pasteConfig.tags || [];
|
const tagsOrSanitizeConfigs = tool.pasteConfig.tags || [];
|
||||||
const toolTags = [];
|
const toolTags = [];
|
||||||
|
|
||||||
|
@ -387,6 +391,10 @@ export default class Paste extends Module {
|
||||||
* @param tool - BlockTool object
|
* @param tool - BlockTool object
|
||||||
*/
|
*/
|
||||||
private getFilesConfig(tool: BlockTool): void {
|
private getFilesConfig(tool: BlockTool): void {
|
||||||
|
if (tool.pasteConfig === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { files = {} } = tool.pasteConfig;
|
const { files = {} } = tool.pasteConfig;
|
||||||
let { extensions, mimeTypes } = files;
|
let { extensions, mimeTypes } = files;
|
||||||
|
|
||||||
|
@ -428,7 +436,11 @@ export default class Paste extends Module {
|
||||||
* @param tool - BlockTool object
|
* @param tool - BlockTool object
|
||||||
*/
|
*/
|
||||||
private getPatternsConfig(tool: BlockTool): void {
|
private getPatternsConfig(tool: BlockTool): void {
|
||||||
if (!tool.pasteConfig.patterns || _.isEmpty(tool.pasteConfig.patterns)) {
|
if (
|
||||||
|
tool.pasteConfig === false ||
|
||||||
|
!tool.pasteConfig.patterns ||
|
||||||
|
_.isEmpty(tool.pasteConfig.patterns)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -467,9 +479,14 @@ export default class Paste extends Module {
|
||||||
private handlePasteEvent = async (event: ClipboardEvent): Promise<void> => {
|
private handlePasteEvent = async (event: ClipboardEvent): Promise<void> => {
|
||||||
const { BlockManager, Toolbar } = this.Editor;
|
const { BlockManager, Toolbar } = this.Editor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When someone pasting into a block, its more stable to set current block by event target, instead of relying on current block set before
|
||||||
|
*/
|
||||||
|
const currentBlock = BlockManager.setCurrentBlockByChildNode(event.target as HTMLElement);
|
||||||
|
|
||||||
/** If target is native input or is not Block, use browser behaviour */
|
/** If target is native input or is not Block, use browser behaviour */
|
||||||
if (
|
if (
|
||||||
!BlockManager.currentBlock || (this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files'))
|
!currentBlock || (this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files'))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -477,14 +494,13 @@ export default class Paste extends Module {
|
||||||
/**
|
/**
|
||||||
* If Tools is in list of errors, skip processing of paste event
|
* If Tools is in list of errors, skip processing of paste event
|
||||||
*/
|
*/
|
||||||
if (BlockManager.currentBlock && this.exceptionList.includes(BlockManager.currentBlock.name)) {
|
if (currentBlock && this.exceptionList.includes(currentBlock.name)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.processDataTransfer(event.clipboardData);
|
this.processDataTransfer(event.clipboardData);
|
||||||
|
|
||||||
BlockManager.clearFocused();
|
|
||||||
Toolbar.close();
|
Toolbar.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -602,7 +618,10 @@ export default class Paste extends Module {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tags: tagsOrSanitizeConfigs } = tool.pasteConfig;
|
/**
|
||||||
|
* Returns empty array if there is no paste config
|
||||||
|
*/
|
||||||
|
const { tags: tagsOrSanitizeConfigs } = tool.pasteConfig || { tags: [] };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reduce the tags or sanitize configs to a single array of sanitize config.
|
* Reduce the tags or sanitize configs to a single array of sanitize config.
|
||||||
|
|
|
@ -1,116 +1,111 @@
|
||||||
import Module from '../__module';
|
import Module from '../__module';
|
||||||
import * as _ from '../utils';
|
import * as _ from '../utils';
|
||||||
import { OutputBlockData } from '../../../types';
|
import type { BlockId, BlockToolData, OutputBlockData } from '../../../types';
|
||||||
import BlockTool from '../tools/block';
|
import type BlockTool from '../tools/block';
|
||||||
|
import type { StubData } from '../../tools/stub';
|
||||||
|
import Block from '../block';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor.js Renderer Module
|
* Module that responsible for rendering Blocks on editor initialization
|
||||||
*
|
|
||||||
* @module Renderer
|
|
||||||
* @author CodeX Team
|
|
||||||
* @version 2.0.0
|
|
||||||
*/
|
*/
|
||||||
export default class Renderer extends Module {
|
export default class Renderer extends Module {
|
||||||
/**
|
/**
|
||||||
* @typedef {object} RendererBlocks
|
* Renders passed blocks as one batch
|
||||||
* @property {string} type - tool name
|
|
||||||
* @property {object} data - tool data
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @example
|
|
||||||
*
|
*
|
||||||
* blocks: [
|
* @param blocksData - blocks to render
|
||||||
* {
|
|
||||||
* id : 'oDe-EVrGWA',
|
|
||||||
* type : 'paragraph',
|
|
||||||
* data : {
|
|
||||||
* text : 'Hello from Codex!'
|
|
||||||
* }
|
|
||||||
* },
|
|
||||||
* {
|
|
||||||
* id : 'Ld5BJjJCHs',
|
|
||||||
* type : 'paragraph',
|
|
||||||
* data : {
|
|
||||||
* text : 'Leave feedback if you like it!'
|
|
||||||
* }
|
|
||||||
* },
|
|
||||||
* ]
|
|
||||||
*/
|
*/
|
||||||
|
public async render(blocksData: OutputBlockData[]): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const { Tools, BlockManager } = this.Editor;
|
||||||
|
|
||||||
/**
|
if (blocksData.length === 0) {
|
||||||
* Make plugin blocks from array of plugin`s data
|
BlockManager.insert();
|
||||||
*
|
} else {
|
||||||
* @param {OutputBlockData[]} blocks - blocks to render
|
/**
|
||||||
*/
|
* Create Blocks instances
|
||||||
public async render(blocks: OutputBlockData[]): Promise<void> {
|
*/
|
||||||
const chainData = blocks.map((block) => ({ function: (): Promise<void> => this.insertBlock(block) }));
|
const blocks = blocksData.map(({ type: tool, data, tunes, id }) => {
|
||||||
|
if (Tools.available.has(tool) === false) {
|
||||||
|
_.logLabeled(`Tool «${tool}» is not found. Check 'tools' property at the Editor.js config.`, 'warn');
|
||||||
|
|
||||||
/**
|
data = this.composeStubDataForTool(tool, data, id);
|
||||||
* Disable onChange callback on render to not to spam those events
|
tool = Tools.stubTool;
|
||||||
*/
|
}
|
||||||
this.Editor.ModificationsObserver.disable();
|
|
||||||
|
|
||||||
const sequence = await _.sequence(chainData as _.ChainData[]);
|
let block: Block;
|
||||||
|
|
||||||
this.Editor.ModificationsObserver.enable();
|
try {
|
||||||
|
block = BlockManager.composeBlock({
|
||||||
|
id,
|
||||||
|
tool,
|
||||||
|
data,
|
||||||
|
tunes,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
_.log(`Block «${tool}» skipped because of plugins error`, 'error', {
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
this.Editor.UI.checkEmptiness();
|
/**
|
||||||
|
* If tool throws an error during render, we should render stub instead of it
|
||||||
|
*/
|
||||||
|
data = this.composeStubDataForTool(tool, data, id);
|
||||||
|
tool = Tools.stubTool;
|
||||||
|
|
||||||
return sequence;
|
block = BlockManager.composeBlock({
|
||||||
|
id,
|
||||||
|
tool,
|
||||||
|
data,
|
||||||
|
tunes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return block;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert batch of Blocks
|
||||||
|
*/
|
||||||
|
BlockManager.insertMany(blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait till browser will render inserted Blocks and resolve a promise
|
||||||
|
*/
|
||||||
|
window.requestIdleCallback(() => {
|
||||||
|
resolve();
|
||||||
|
}, { timeout: 2000 });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get plugin instance
|
* Create data for the Stub Tool that will be used instead of unavailable tool
|
||||||
* Add plugin instance to BlockManager
|
|
||||||
* Insert block to working zone
|
|
||||||
*
|
*
|
||||||
* @param {object} item - Block data to insert
|
* @param tool - unavailable tool name to stub
|
||||||
* @returns {Promise<void>}
|
* @param data - data of unavailable block
|
||||||
|
* @param [id] - id of unavailable block
|
||||||
*/
|
*/
|
||||||
public async insertBlock(item: OutputBlockData): Promise<void> {
|
private composeStubDataForTool(tool: string, data: BlockToolData, id?: BlockId): StubData {
|
||||||
const { Tools, BlockManager } = this.Editor;
|
const { Tools } = this.Editor;
|
||||||
const { type: tool, data, tunes, id } = item;
|
|
||||||
|
|
||||||
if (Tools.available.has(tool)) {
|
let title = tool;
|
||||||
try {
|
|
||||||
BlockManager.insert({
|
if (Tools.unavailable.has(tool)) {
|
||||||
id,
|
const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox;
|
||||||
tool,
|
|
||||||
data,
|
if (toolboxSettings !== undefined && toolboxSettings[0].title !== undefined) {
|
||||||
tunes,
|
title = toolboxSettings[0].title;
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
_.log(`Block «${tool}» skipped because of plugins error`, 'warn', data);
|
|
||||||
throw Error(error);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
/** If Tool is unavailable, create stub Block for it */
|
|
||||||
const stubData = {
|
|
||||||
savedData: {
|
|
||||||
id,
|
|
||||||
type: tool,
|
|
||||||
data,
|
|
||||||
},
|
|
||||||
title: tool,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Tools.unavailable.has(tool)) {
|
|
||||||
const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox;
|
|
||||||
const toolboxTitle = toolboxSettings[0]?.title;
|
|
||||||
|
|
||||||
stubData.title = toolboxTitle || stubData.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stub = BlockManager.insert({
|
|
||||||
id,
|
|
||||||
tool: Tools.stubTool,
|
|
||||||
data: stubData,
|
|
||||||
});
|
|
||||||
|
|
||||||
stub.stretched = true;
|
|
||||||
|
|
||||||
_.log(`Tool «${tool}» is not found. Check 'tools' property at your initial Editor.js config.`, 'warn');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
savedData: {
|
||||||
|
id,
|
||||||
|
type: tool,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
title,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|