From e3db19df84d0fe4fb75a9556cc51a09cc6b172c2 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Thu, 13 Jan 2022 19:12:08 +0300 Subject: [PATCH 01/26] Fire onChange event for native inputs (#1832) * Fire onChange event for native inputs * Add changelog --- .eslintrc | 1 + docs/CHANGELOG.md | 1 + package.json | 1 + src/components/block/index.ts | 28 +++++++++++++++------------ test/cypress/tests/onchange.spec.ts | 30 +++++++++++++++++++++++++---- yarn.lock | 5 +++++ 6 files changed, 50 insertions(+), 16 deletions(-) diff --git a/.eslintrc b/.eslintrc index 6704e3b7..9d39bb96 100644 --- a/.eslintrc +++ b/.eslintrc @@ -50,6 +50,7 @@ "DOMRect": true, "ClientRect": true, "ArrayLike": true, + "InputEvent": true, "unknown": true } } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e9a4f930..a3919422 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -21,6 +21,7 @@ - `Refactoring` — Toolbox became a standalone class instead of a Module. It can be accessed only through the Toolbar module. - `Refactoring` — CI flow optimized. - `Fix` - Recognize async `onPaste` handlers in tools [#1803](https://github.com/codex-team/editor.js/issues/1803). +- `Fix` — Fire onChange event for native inputs [#1750](https://github.com/codex-team/editor.js/issues/1750) ### 2.22.3 diff --git a/package.json b/package.json index 886cc9fc..59bef858 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@codexteam/shortcuts": "^1.1.1", "@cypress/code-coverage": "^3.9.2", "@cypress/webpack-preprocessor": "^5.6.0", + "@editorjs/code": "^2.7.0", "@editorjs/header": "^2.6.1", "@editorjs/simple-image": "^1.4.1", "@types/node": "^14.14.35", diff --git a/src/components/block/index.ts b/src/components/block/index.ts index d3eabc8c..9a9f9851 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -52,7 +52,7 @@ interface BlockConstructorOptions { /** * Tunes data for current Block */ - tunesData: {[name: string]: BlockTuneData}; + tunesData: { [name: string]: BlockTuneData }; } /** @@ -98,7 +98,7 @@ export default class Block extends EventsDispatcher { * * @returns {{wrapper: string, content: string}} */ - public static get CSS(): {[name: string]: string} { + public static get CSS(): { [name: string]: string } { return { wrapper: 'ce-block', wrapperStretched: 'ce-block--stretched', @@ -170,7 +170,7 @@ export default class Block extends EventsDispatcher { * If there is saved data for Tune which is not available at the moment, * we will store it here and provide back on save so data is not lost */ - private unavailableTunesData: {[name: string]: BlockTuneData} = {}; + private unavailableTunesData: { [name: string]: BlockTuneData } = {}; /** * Editor`s API module @@ -201,11 +201,15 @@ export default class Block extends EventsDispatcher { /** * Is fired when DOM mutation has been happened */ - private didMutated = _.debounce((mutations: MutationRecord[] = []): void => { - const shouldFireUpdate = !mutations.some(({ addedNodes = [], removedNodes }) => { - return [...Array.from(addedNodes), ...Array.from(removedNodes)] - .some(node => $.isElement(node) && (node as HTMLElement).dataset.mutationFree === 'true'); - }); + 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 @@ -575,9 +579,9 @@ export default class Block extends EventsDispatcher { * * @returns {object} */ - public async save(): Promise { + public async save(): Promise { const extractedBlock = await this.toolInstance.save(this.pluginsContent as HTMLElement); - const tunesData: {[name: string]: BlockTuneData} = this.unavailableTunesData; + const tunesData: { [name: string]: BlockTuneData } = this.unavailableTunesData; [ ...this.tunesInstances.entries(), @@ -706,7 +710,7 @@ export default class Block extends EventsDispatcher { * 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. */ - public dispatchChange(): void{ + public dispatchChange(): void { this.didMutated(); } @@ -775,7 +779,7 @@ export default class Block extends EventsDispatcher { * @param tunesData - current Block tunes data * @private */ - private composeTunes(tunesData: {[name: string]: BlockTuneData}): void { + private composeTunes(tunesData: { [name: string]: BlockTuneData }): void { Array.from(this.tunes.values()).forEach((tune) => { const collection = tune.isInternal ? this.defaultTunesInstances : this.tunesInstances; diff --git a/test/cypress/tests/onchange.spec.ts b/test/cypress/tests/onchange.spec.ts index 8d410b96..3397c5a6 100644 --- a/test/cypress/tests/onchange.spec.ts +++ b/test/cypress/tests/onchange.spec.ts @@ -1,4 +1,5 @@ import Header from '@editorjs/header'; +import Code from '@editorjs/code'; import { BlockMutationType } from '../../../types/events/block/mutation-type'; /** @@ -16,6 +17,7 @@ describe('onChange callback', () => { const config = { tools: { header: Header, + code: Code, }, onChange: (api, event): void => { console.log('something changed', api, event); @@ -47,7 +49,7 @@ describe('onChange callback', () => { type: BlockMutationType.Added, detail: { target: { - name: 'paragraph' + name: 'paragraph', }, index: 0, }, @@ -67,7 +69,7 @@ describe('onChange callback', () => { type: BlockMutationType.Added, detail: { target: { - name: 'paragraph' + name: 'paragraph', }, index: 1, }, @@ -85,7 +87,7 @@ describe('onChange callback', () => { cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ type: BlockMutationType.Changed, detail: { - index: 0 + index: 0, }, })); }); @@ -185,7 +187,7 @@ describe('onChange callback', () => { cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ type: BlockMutationType.Removed, detail: { - index: 0 + index: 0, }, })); }); @@ -219,4 +221,24 @@ describe('onChange callback', () => { }, })); }); + + it('should fire onChange if something changed inside native input', () => { + createEditor([ { + type: 'code', + data: { + code: '', + }, + } ]); + + cy.get('[data-cy=editorjs') + .get('textarea') + .type('Some input to the textarea'); + + cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ + type: BlockMutationType.Changed, + detail: { + index: 0, + }, + })); + }); }); diff --git a/yarn.lock b/yarn.lock index 5839f153..94541399 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1423,6 +1423,11 @@ debug "^3.1.0" lodash.once "^4.1.1" +"@editorjs/code@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@editorjs/code/-/code-2.7.0.tgz#0a21de9ac15e4533605ffcc80969513ab2142ac5" + integrity sha512-gXtTce915fHp3H9i4IqhTxEDbbkT2heFfYiW/bhFHsCmZDpyGzfZxi94kmrEqDmbxXjV49ZZ6GZbR26If13KJw== + "@editorjs/header@^2.6.1": version "2.6.1" resolved "https://registry.yarnpkg.com/@editorjs/header/-/header-2.6.1.tgz#454a46e4dbb32ae3aa1db4d22b0ddf2cc36c3134" From 817beff551fafc5ad251813bc092d532e925df98 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Jan 2022 13:12:19 +0300 Subject: [PATCH 02/26] Bump nanoid from 3.1.22 to 3.1.31 (#1944) Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.22 to 3.1.31. - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/3.1.22...3.1.31) --- updated-dependencies: - dependency-name: nanoid dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 94541399..08bacc79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5963,8 +5963,9 @@ nanoid@3.1.20: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" nanoid@^3.1.22: - version "3.1.22" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" + version "3.1.31" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.31.tgz#f5b58a1ce1b7604da5f0605757840598d8974dc6" + integrity sha512-ZivnJm0o9bb13p2Ot5CpgC2rQdzB9Uxm/mFZweqm5eMViqOJe3PV6LU2E30SiLgheesmcPrjquqraoolONSA0A== nanomatch@^1.2.9: version "1.2.13" From 1188b409592e6b6d6671c7671b39f029c0aa53a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Jan 2022 17:01:42 +0300 Subject: [PATCH 03/26] Bump version (#1862) Co-authored-by: github-actions --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 59bef858..0a7ee6ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.23.0-rc.1", + "version": "2.23.0-rc.2", "description": "Editor.js — Native JS, based on API and Open Source", "main": "dist/editor.js", "types": "./types/index.d.ts", From 27aa9164a11aa61dee58c3ebb459180eac920404 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Wed, 2 Feb 2022 18:02:46 +0300 Subject: [PATCH 04/26] chore(readme): roadmap added (#1954) * chore(readme): roadmap added * Update README.md * Update README.md --- README.md | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dcc4fa39..86a2f130 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,38 @@ [![](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) -| [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [iOS Safari](http://godban.github.io/browsers-support-badges/)
iOS Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | +| | | | | | | | --------- | --------- | --------- | --------- | --------- | --------- | -| Edge 12+ | Firefox 18+ | Chrome 49+ | Safari 10+ | Safari 10+ | Opera 36+ +| IE / Edge Edge 12+ | Firefox Firefox 18+ | Chrome Chrome 49+ | Safari Safari 10+ | iOS Safari iOS Safari 10+ | Opera Opera 36+ + + + +## Roadmap + + + +- Collaborative editing + - [ ] Implement Inline Tools JSON format `In progress` [#1801](https://github.com/codex-team/editor.js/pull/1801) + - [ ] Implement Operations creation and transformations + - [ ] Implement Tools API changes + - [ ] Implement Server and communication + - [ ] Update basic tools to fit the new API + +- Unified Toolbox + - [x] Block Tunes moved left [#1815](https://github.com/codex-team/editor.js/pull/1815) + - [ ] Toolbox become vertical `In progress` + - [ ] Ability to display several Toolbox buttons by the single Tool + - [ ] Conversion Toolbar uses Unified Toolbox + - [ ] Block Tunes become vertical + - [ ] Conversion Toolbar added to the Block Tunes +- Ecosystem improvements + + + image + + +## + ## If you like a project 💗💗💗 @@ -27,7 +56,6 @@ Support us by becoming a sponsor. Your logo will show up here with a link to you - ### Backers Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/editorjs/contribute/backer-8632/checkout)] From c1bca10d12953d9bf973c989457f68365df989e8 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Tue, 8 Feb 2022 13:26:13 +0300 Subject: [PATCH 05/26] fix(ci): incorrect release tag fixed (#1964) --- docs/CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a3919422..00e1b6d1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.23.1 + +— `Fix` — Incorrect release tag fixed + ### 2.23.0 - `Improvement` — *EditorConfig* — The `onChange` callback now accepts two arguments: EditorJS API and the CustomEvent with `type` and `detail` allowing to determine what happened with a Block diff --git a/package.json b/package.json index 0a7ee6ef..7d373f04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.23.0-rc.2", + "version": "2.23.1", "description": "Editor.js — Native JS, based on API and Open Source", "main": "dist/editor.js", "types": "./types/index.d.ts", From 640b1a2d7e53728d7ba0418b29adb4b185fcefd9 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Wed, 9 Feb 2022 20:46:26 +0300 Subject: [PATCH 06/26] fix(readonly): crash with readonly property (#1969) * fix(readonly): fix readonly property Resolves #1968 * changelog added * Update CHANGELOG.md --- docs/CHANGELOG.md | 4 ++++ package.json | 2 +- src/components/modules/rectangleSelection.ts | 1 - src/components/modules/toolbar/index.ts | 9 ++++++-- test/cypress/tests/initialization.spec.ts | 22 ++++++++++++++++++++ 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 00e1b6d1..fd1e4df8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.23.2 + +— `Fix` — Crash on initialization in the read-only mode [#1968](https://github.com/codex-team/editor.js/issues/1968) + ### 2.23.1 — `Fix` — Incorrect release tag fixed diff --git a/package.json b/package.json index 7d373f04..ad0ded9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.23.1", + "version": "2.23.2", "description": "Editor.js — Native JS, based on API and Open Source", "main": "dist/editor.js", "types": "./types/index.d.ts", diff --git a/src/components/modules/rectangleSelection.ts b/src/components/modules/rectangleSelection.ts index f0124815..03f249f7 100644 --- a/src/components/modules/rectangleSelection.ts +++ b/src/components/modules/rectangleSelection.ts @@ -376,7 +376,6 @@ export default class RectangleSelection extends Module { this.inverseSelection(); SelectionUtils.get().removeAllRanges(); - event.preventDefault(); } /** diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index d60ffea1..864ec85b 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -227,7 +227,6 @@ export default class Toolbar extends Module { this.enableModuleBindings(); } else { this.destroy(); - this.toolboxInstance.destroy(); this.Editor.BlockSettings.destroy(); this.disableModuleBindings(); } @@ -295,6 +294,10 @@ export default class Toolbar extends Module { * Close the Toolbar */ public close(): void { + if (this.Editor.ReadOnly.isEnabled) { + return; + } + this.nodes.wrapper.classList.remove(this.CSS.toolbarOpened); /** Close components */ @@ -551,7 +554,9 @@ export default class Toolbar extends Module { */ private destroy(): void { this.removeAllNodes(); - this.toolboxInstance.destroy(); + if (this.toolboxInstance) { + this.toolboxInstance.destroy(); + } this.tooltip.destroy(); } } diff --git a/test/cypress/tests/initialization.spec.ts b/test/cypress/tests/initialization.spec.ts index ada89c62..f4992bdb 100644 --- a/test/cypress/tests/initialization.spec.ts +++ b/test/cypress/tests/initialization.spec.ts @@ -25,4 +25,26 @@ describe('Editor basic initialization', () => { .should('be.visible'); }); }); + + describe('Configuration', () => { + describe('readOnly', () => { + beforeEach(() => { + if (this && this.editorInstance) { + this.editorInstance.destroy(); + } + }); + + it('should create editor without editing ability when true passed', () => { + cy.createEditor({ + readOnly: true, + }).as('editorInstance'); + + cy.get('[data-cy=editorjs]') + .get('div.codex-editor') + .get('div.ce-paragraph') + .invoke('attr', 'contenteditable') + .should('eq', 'false'); + }); + }); + }); }); From 82e8ff9f40baab59efab2dbc8965a536b1402a45 Mon Sep 17 00:00:00 2001 From: "Umang G. Patel" <23169768+robonetphy@users.noreply.github.com> Date: Sun, 13 Mar 2022 21:36:51 +0530 Subject: [PATCH 07/26] example link changed (#1993) --- example/example-dev.html | 2 +- example/example-rtl.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/example-dev.html b/example/example-dev.html index cb7e9089..f8687184 100644 --- a/example/example-dev.html +++ b/example/example-dev.html @@ -98,7 +98,7 @@ - + diff --git a/example/example-rtl.html b/example/example-rtl.html index c8fa6662..298381dc 100644 --- a/example/example-rtl.html +++ b/example/example-rtl.html @@ -61,7 +61,7 @@ - + From 771437ed04d0b91cb26a68370825383997a9a175 Mon Sep 17 00:00:00 2001 From: Mikhail Popov Date: Tue, 15 Mar 2022 17:04:44 +0300 Subject: [PATCH 08/26] Bump codex-tooltip version (#1999) --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ad0ded9d..ac7a6b33 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ }, "dependencies": { "codex-notifier": "^1.1.2", - "codex-tooltip": "^1.0.4", + "codex-tooltip": "^1.0.5", "nanoid": "^3.1.22" } } diff --git a/yarn.lock b/yarn.lock index 08bacc79..57cc87fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2813,9 +2813,10 @@ codex-notifier@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/codex-notifier/-/codex-notifier-1.1.2.tgz#a733079185f4c927fa296f1d71eb8753fe080895" -codex-tooltip@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/codex-tooltip/-/codex-tooltip-1.0.4.tgz#bb8c6e0fe7accc68ce79cdcb7c71bf7b4bf1317a" +codex-tooltip@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/codex-tooltip/-/codex-tooltip-1.0.5.tgz#ba25fd5b3a58ba2f73fd667c2b46987ffd1edef2" + integrity sha512-IuA8LeyLU5p1B+HyhOsqR6oxyFQ11k3i9e9aXw40CrHFTRO2Y1npNBVU3W1SvhKAbUU7R/YikUBdcYFP0RcJag== coffeeify@3.0.1: version "3.0.1" From 32dcd3fa6d8d3aa11d90347c29df7c4fdb95b124 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Tue, 5 Apr 2022 19:49:16 +0300 Subject: [PATCH 09/26] feat(dx): add a dev-server for debugging on mobile devices from a local network (#2009) * Add server * Get rid of express * Cleanup * Revert lock * Revert lock 2 * Update script to substitute ip address automatically * terminal output improved * npm ignore devserver.js * rm spaces * handle offline state * Update devserver.js * Update changelog Co-authored-by: Peter Savchenko --- .npmignore | 1 + devserver.js | 128 ++++++++++++++++++++++++++++++++++++++++++++++ docs/CHANGELOG.md | 4 ++ package.json | 3 +- 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 devserver.js diff --git a/.npmignore b/.npmignore index c00f2cbb..98ed3edf 100644 --- a/.npmignore +++ b/.npmignore @@ -19,3 +19,4 @@ tsconfig.json tslint.json webpack.config.js yarn.lock +devserver.js diff --git a/devserver.js b/devserver.js new file mode 100644 index 00000000..5087a7b4 --- /dev/null +++ b/devserver.js @@ -0,0 +1,128 @@ +/** + * Server for testing example page on mobile devices. + * + * Usage: + * 1. run `yarn devserver:start` + * 2. Open `http://{ip_address}:3000/example/example-dev.html` + * where {ip_address} is IP of your machine. + * + * Also, can serve static files from `/example` or `/dist` on any device in local network. + */ +const path = require('path'); +const fs = require('fs'); +const http = require('http'); +const { networkInterfaces } = require('os'); + +const port = 3000; +const localhost = '127.0.0.1'; +const nonRoutableAddress = '0.0.0.0'; +const host = getHost(); +const server = http.createServer(serveStatic([ + '/example', + '/dist', +])); + +server.listen(port, nonRoutableAddress, () => { + console.log(` + +${wrapInColor('Editor.js 💖', consoleColors.hiColor)} devserver is running ᕕ(⌐■_■)ᕗ ✨ +--------------------------------------------- +${wrapInColor('http://' + host + ':' + port + '/example/example-dev.html', consoleColors.fgGreen)} +--------------------------------------------- +Page can be opened from any device connected to the same local network. +`); + + if (host === localhost) { + console.log(wrapInColor('Looks like you are not connected to any Network so you couldn\'t debug the Editor on your mobile device at the moment.', consoleColors.fgRed)); + } +}); + +/** + * Serves files from specified directories + * + * @param {string[]} paths - directories files from which should be served + * @returns {Function} + */ +function serveStatic(paths) { + return (request, response) => { + const resource = request.url; + const isPathAllowed = paths.find(p => resource.startsWith(p)); + + if (!isPathAllowed) { + response.writeHead(404); + response.end(); + + return; + } + const filePath = path.join(__dirname, resource); + + try { + const stat = fs.statSync(filePath); + + response.writeHead(200, { + 'Content-Length': stat.size, + }); + const readStream = fs.createReadStream(filePath); + + readStream.on('error', e => { + throw e; + }); + readStream.pipe(response); + } catch (e) { + response.writeHead(500); + response.end(e.toString()); + } + }; +} + +/** + * Returns IP address of a machine + * + * @returns {string} + */ +function getHost() { + const nets = networkInterfaces(); + const results = {}; + + for (const name of Object.keys(nets)) { + for (const net of nets[name]) { + // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses + if (net.family === 'IPv4' && !net.internal) { + if (!results[name]) { + results[name] = []; + } + results[name].push(net.address); + } + } + } + + /** + * Offline case + */ + if (Object.keys(results).length === 0) { + return localhost; + } + + return results['en0'][0]; +} + +/** + * Terminal output colors + */ +const consoleColors = { + fgMagenta: 35, + fgRed: 31, + fgGreen: 32, + hiColor: 1, +}; + +/** + * Set a terminal color to the message + * + * @param {string} msg - text to wrap + * @param {string} color - color + * @returns {string} + */ +function wrapInColor(msg, color) { + return '\x1b[' + color + 'm' + msg + '\x1b[0m'; +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fd1e4df8..2a762728 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.23.3 + +- `Improvement` — *Dev Example Page* - Server added to allow opening example page on other devices in network. + ### 2.23.2 — `Fix` — Crash on initialization in the read-only mode [#1968](https://github.com/codex-team/editor.js/issues/1968) diff --git a/package.json b/package.json index ac7a6b33..a1e2ae8e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "_tools:make": "yarn _tools:yarn && yarn _tools:build", "tools:update": "yarn _tools:checkout && yarn _tools:pull && yarn _tools:make", "test:e2e": "yarn build && cypress run", - "test:e2e:open": "yarn build && cypress open" + "test:e2e:open": "yarn build && cypress open", + "devserver:start": "yarn build && node ./devserver.js" }, "author": "CodeX", "license": "Apache-2.0", From 18feb06a889159625e688c1dac324f71dad3b7b0 Mon Sep 17 00:00:00 2001 From: "Umang G. Patel" <23169768+robonetphy@users.noreply.github.com> Date: Thu, 7 Apr 2022 14:33:09 +0530 Subject: [PATCH 10/26] Fix OnChange Event Invocation (#2007) * fix: call onchange event after block insert * changelog updated * patch version updated * removed the modification observer from saver * only changelog version added * delimiter added * feat: test case added for save inside the onchange --- docs/CHANGELOG.md | 3 +- package.json | 3 +- src/components/modules/saver.ts | 10 +--- test/cypress/tests/onchange.spec.ts | 74 +++++++++++++++++++++++++++++ yarn.lock | 5 ++ 5 files changed, 85 insertions(+), 10 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2a762728..c93c90f9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,7 @@ ### 2.23.3 - `Improvement` — *Dev Example Page* - Server added to allow opening example page on other devices in network. +- `Fix` — `OnChange` event invocation after block insertion. [#1997](https://github.com/codex-team/editor.js/issues/1997) ### 2.23.2 @@ -434,4 +435,4 @@ See a whole [Changelog](/docs/) - `New` New [Editor.js PHP](http://github.com/codex-team/codex.editor.backend) — example of server-side implementation with HTML purifying and data validation. - `Improvements` - Improvements of Toolbar's position calculation. - `Improvements` — Improved zero-configuration initialization. -- and many little improvements. +- and many little improvements. \ No newline at end of file diff --git a/package.json b/package.json index a1e2ae8e..22c05395 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.23.2", + "version": "2.23.3", "description": "Editor.js — Native JS, based on API and Open Source", "main": "dist/editor.js", "types": "./types/index.d.ts", @@ -52,6 +52,7 @@ "@cypress/code-coverage": "^3.9.2", "@cypress/webpack-preprocessor": "^5.6.0", "@editorjs/code": "^2.7.0", + "@editorjs/delimiter": "^1.2.0", "@editorjs/header": "^2.6.1", "@editorjs/simple-image": "^1.4.1", "@types/node": "^14.14.35", diff --git a/src/components/modules/saver.ts b/src/components/modules/saver.ts index 59725f65..f75f3019 100644 --- a/src/components/modules/saver.ts +++ b/src/components/modules/saver.ts @@ -28,15 +28,11 @@ export default class Saver extends Module { * @returns {OutputData} */ public async save(): Promise { - const { BlockManager, Tools, ModificationsObserver } = this.Editor; + const { BlockManager, Tools } = this.Editor; const blocks = BlockManager.blocks, chainData = []; try { - /** - * Disable onChange callback on save to not to spam those events - */ - ModificationsObserver.disable(); blocks.forEach((block: Block) => { chainData.push(this.getSavedData(block)); @@ -50,9 +46,7 @@ export default class Saver extends Module { return this.makeOutput(sanitizedData); } catch (e) { _.logLabeled(`Saving failed due to the Error %o`, 'error', e); - } finally { - ModificationsObserver.enable(); - } + } } /** diff --git a/test/cypress/tests/onchange.spec.ts b/test/cypress/tests/onchange.spec.ts index 3397c5a6..428a4ad0 100644 --- a/test/cypress/tests/onchange.spec.ts +++ b/test/cypress/tests/onchange.spec.ts @@ -1,5 +1,6 @@ import Header from '@editorjs/header'; import Code from '@editorjs/code'; +import Delimiter from '@editorjs/delimiter'; import { BlockMutationType } from '../../../types/events/block/mutation-type'; /** @@ -32,6 +33,32 @@ describe('onChange callback', () => { cy.createEditor(config).as('editorInstance'); } + /** + * Creates Editor instance with save inside the onChange event. + * + * @param blocks - list of blocks to prefill the editor + */ + function createEditorWithSave(blocks = null): void { + const config = { + tools: { + header: Header, + code: Code, + delimiter: Delimiter, + }, + onChange: (api, event): void => { + console.log('something changed', api, event); + api.saver.save(); + }, + data: blocks ? { + blocks, + } : null, + }; + + cy.spy(config, 'onChange').as('onChange'); + + cy.createEditor(config).as('editorInstance'); + } + /** * EditorJS API is passed as the first parameter of the onChange callback */ @@ -92,6 +119,53 @@ describe('onChange callback', () => { })); }); + it('should fire onChange callback on block insertion with save inside onChange', () => { + createEditorWithSave(); + + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('div.ce-toolbar__plus') + .click(); + + cy.get('[data-cy=editorjs]') + .get('li.ce-toolbox__button[data-tool=delimiter]') + .click(); + + cy.get('@onChange').should('be.calledThrice'); + cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ + type: BlockMutationType.Removed, + detail: { + index: 0, + target: { + name: 'paragraph', + }, + }, + })); + + cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ + type: BlockMutationType.Added, + detail: { + index: 0, + target: { + name: 'delimiter', + }, + }, + })); + + cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({ + type: BlockMutationType.Added, + detail: { + index: 1, + target: { + name: 'paragraph', + }, + }, + })); + }); + it('should fire onChange callback on block replacement for both of blocks', () => { createEditor(); diff --git a/yarn.lock b/yarn.lock index 57cc87fa..ab0d651c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1428,6 +1428,11 @@ resolved "https://registry.yarnpkg.com/@editorjs/code/-/code-2.7.0.tgz#0a21de9ac15e4533605ffcc80969513ab2142ac5" integrity sha512-gXtTce915fHp3H9i4IqhTxEDbbkT2heFfYiW/bhFHsCmZDpyGzfZxi94kmrEqDmbxXjV49ZZ6GZbR26If13KJw== +"@editorjs/delimiter@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@editorjs/delimiter/-/delimiter-1.2.0.tgz#5075f1a3e68765cfb6aec8694b316d81e2b41607" + integrity sha512-GKsCFPk85vH5FuCuVQ48NTLc9hk0T3DsBH9zABaicTYIJayFcUa8N4/Y+L3i4tduzDqqyvoxkv+5n43GmC5gEA== + "@editorjs/header@^2.6.1": version "2.6.1" resolved "https://registry.yarnpkg.com/@editorjs/header/-/header-2.6.1.tgz#454a46e4dbb32ae3aa1db4d22b0ddf2cc36c3134" From f9e280fcad1a8ddaa9167c8f16f887b969e11902 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Fri, 22 Apr 2022 18:24:33 +0300 Subject: [PATCH 11/26] chore(dev): update tools submodules to the last version (#2025) * update tools version * add text-variant-tune --- .gitmodules | 3 +++ example/tools/embed | 2 +- example/tools/link | 2 +- example/tools/list | 2 +- example/tools/nested-list | 2 +- example/tools/raw | 2 +- example/tools/table | 2 +- example/tools/text-variant-tune | 1 + 8 files changed, 10 insertions(+), 6 deletions(-) create mode 160000 example/tools/text-variant-tune diff --git a/.gitmodules b/.gitmodules index 7dc8f02c..ad185a9e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -52,3 +52,6 @@ [submodule "example/tools/nested-list"] path = example/tools/nested-list url = https://github.com/editor-js/nested-list +[submodule "example/tools/text-variant-tune"] + path = example/tools/text-variant-tune + url = https://github.com/editor-js/text-variant-tune diff --git a/example/tools/embed b/example/tools/embed index b3c87948..35742f01 160000 --- a/example/tools/embed +++ b/example/tools/embed @@ -1 +1 @@ -Subproject commit b3c87948d5a5926f2557c2029d64aa819ca61920 +Subproject commit 35742f01ae5875d442b145121d3c9b71b23aea56 diff --git a/example/tools/link b/example/tools/link index d452b461..0fc365ef 160000 --- a/example/tools/link +++ b/example/tools/link @@ -1 +1 @@ -Subproject commit d452b46106766b5fc4c5747e99888f613a3764fd +Subproject commit 0fc365ef256decb8f765fb72b060d5bef9254aa3 diff --git a/example/tools/list b/example/tools/list index 004a0645..58b5dc70 160000 --- a/example/tools/list +++ b/example/tools/list @@ -1 +1 @@ -Subproject commit 004a06458d3881e8d922506381c21847900e3c62 +Subproject commit 58b5dc7072ad92a048ebe9488d0ca8e7bfa069d2 diff --git a/example/tools/nested-list b/example/tools/nested-list index 8375ae17..9add9538 160000 --- a/example/tools/nested-list +++ b/example/tools/nested-list @@ -1 +1 @@ -Subproject commit 8375ae17756fa2677d57e716e12096437d01e8f8 +Subproject commit 9add95389afca0711c05260a92283fae8eb209eb diff --git a/example/tools/raw b/example/tools/raw index 84cc4f39..4f8010dd 160000 --- a/example/tools/raw +++ b/example/tools/raw @@ -1 +1 @@ -Subproject commit 84cc4f393db0939c6246c9a579377f2540dac289 +Subproject commit 4f8010dd640d847778220d9b1947fa8235b17f87 diff --git a/example/tools/table b/example/tools/table index ddbc1147..ad0d9012 160000 --- a/example/tools/table +++ b/example/tools/table @@ -1 +1 @@ -Subproject commit ddbc1147e127a727cfac4dbe0326b78e02dceb9e +Subproject commit ad0d9012d149e3ca4b41a5ce096b31767cc8c1fd diff --git a/example/tools/text-variant-tune b/example/tools/text-variant-tune new file mode 160000 index 00000000..8b1b032b --- /dev/null +++ b/example/tools/text-variant-tune @@ -0,0 +1 @@ +Subproject commit 8b1b032b7974c448224608fb0b4838a243cc48a5 From 8f156a87ea42fcf9f398719a2236e89accd85662 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 25 Apr 2022 18:28:58 +0300 Subject: [PATCH 12/26] feat(ui): the toolbox became vertical (#2014) * the popover component, vertical toolbox * toolbox position improved * popover width improved * always show the plus button * search field added * search input in popover * trying to create mobile toolbox * feat(toolbox): popover adapted for mobile devices (#2004) * FIx mobile popover fixed positioning * Add mobile popover overlay * Hide mobile popover on scroll * Alter toolbox buttons hover * Fix closing popover on overlay click * Tests fix * Fix onchange test * restore focus after toolbox closing by ESC * don't move toolbar by block-hover on mobile Resolves #1972 * popover mobile styles improved * Cleanup * Remove scroll event listener * Lock scroll on mobile * don't show shortcuts in mobile popover * Change data attr name * Remove unused styles * Remove unused listeners * disable hover on mobile popover * Scroll fix * Lint * Revert "Scroll fix" This reverts commit 82deae543eadd5c76b9466e7533bf3070d82ac4c. * Return back background color for active state of toolbox buttons Co-authored-by: Peter Savchenko * Vertical toolbox fixes (#2017) * Replace visibility property with display for hiding popover * Disable arrow right and left keys for popover * Revert "Replace visibility property with display for hiding popover" This reverts commit af521cf6f29fb06b71a0e2e8ec88d6a757f9144f. * Hide popover via setting max-height to 0 to fix animation in safari * Remove redundant condition * Extend element interface to avoid ts errors * Do not subscribe to block hovered if mobile * Add unsubscribing from overlay click event * Rename isMobile to isMobileScreen * Cleanup * fix: popover opening direction (#2022) * Change popover opening direction based on available space below it * Update check * Use cacheable decorator * Update src/components/flipper.ts Co-authored-by: George Berezhnoy * Fixes * Fix test * Clear search on popover hide * Fix popover width * Fix for tests * Update todos * Linter fixes * rm todo about beforeInsert because I have no idea what does it mean * i18n for search labels done * rm methods for hiding/showing of + * some code style update * Update CHANGELOG.md * make the list items a little bit compact * fix z-index issue caused by block-appearing animation also, improve popover padding for two reasons: - make the popover more consistent with the Table tool popover (in future, it can be done with the same api method) - make popover looks better Co-authored-by: Tanya Fomina Co-authored-by: George Berezhnoy --- docs/CHANGELOG.md | 7 +- example/example-i18n.html | 4 +- src/assets/search.svg | 3 + src/components/flipper.ts | 77 ++-- src/components/i18n/locales/en/messages.json | 4 +- src/components/modules/saver.ts | 3 +- src/components/modules/toolbar/index.ts | 95 +++-- src/components/modules/toolbar/inline.ts | 5 +- src/components/modules/ui.ts | 6 +- src/components/polyfills.ts | 43 ++ src/components/ui/toolbox.ts | 311 ++++++--------- src/components/utils.ts | 7 + src/components/utils/popover.ts | 391 +++++++++++++++++++ src/components/utils/search-input.ts | 152 +++++++ src/styles/animations.css | 17 + src/styles/block.css | 13 + src/styles/input.css | 46 +++ src/styles/main.css | 2 + src/styles/popover.css | 142 +++++++ src/styles/settings.css | 4 +- src/styles/toolbar.css | 61 +-- src/styles/toolbox.css | 50 +-- src/styles/ui.css | 9 + src/styles/variables.css | 94 ++++- test/cypress/tests/block-ids.spec.ts | 2 +- test/cypress/tests/onchange.spec.ts | 12 +- 26 files changed, 1187 insertions(+), 373 deletions(-) create mode 100644 src/assets/search.svg create mode 100644 src/components/utils/popover.ts create mode 100644 src/components/utils/search-input.ts create mode 100644 src/styles/input.css create mode 100644 src/styles/popover.css diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c93c90f9..b1ff985d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,8 +1,11 @@ # Changelog -### 2.23.3 +### 2.24.0 +- `New` — *UI* — The Toolbox became vertical 🥳 +- `Improvement` — *UI* — the Plus button will always be shown (previously, it appears only for empty blocks) - `Improvement` — *Dev Example Page* - Server added to allow opening example page on other devices in network. +- `Fix` - `UI` - the Toolbar won't move on hover at mobile viewports. Resolves [#1972](https://github.com/codex-team/editor.js/issues/1972) - `Fix` — `OnChange` event invocation after block insertion. [#1997](https://github.com/codex-team/editor.js/issues/1997) ### 2.23.2 @@ -435,4 +438,4 @@ See a whole [Changelog](/docs/) - `New` New [Editor.js PHP](http://github.com/codex-team/codex.editor.backend) — example of server-side implementation with HTML purifying and data validation. - `Improvements` - Improvements of Toolbar's position calculation. - `Improvements` — Improved zero-configuration initialization. -- and many little improvements. \ No newline at end of file +- and many little improvements. diff --git a/example/example-i18n.html b/example/example-i18n.html index b07a6722..0496fbbc 100644 --- a/example/example-i18n.html +++ b/example/example-i18n.html @@ -193,7 +193,9 @@ }, "toolbar": { "toolbox": { - "Add": "Добавить" + "Add": "Добавить", + "Filter": "Поиск", + "Noting found": "Ничего не найдено" } } }, diff --git a/src/assets/search.svg b/src/assets/search.svg new file mode 100644 index 00000000..1485338b --- /dev/null +++ b/src/assets/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/flipper.ts b/src/components/flipper.ts index a97216a6..7ab00fbb 100644 --- a/src/components/flipper.ts +++ b/src/components/flipper.ts @@ -17,21 +17,23 @@ export interface FlipperOptions { */ items?: HTMLElement[]; - /** - * Defines arrows usage. By default Flipper leafs items also via RIGHT/LEFT. - * - * true by default - * - * Pass 'false' if you don't need this behaviour - * (for example, Inline Toolbar should be closed by arrows, - * because it means caret moving with selection clearing) - */ - allowArrows?: boolean; - /** * Optional callback for button click */ activateCallback?: (item: HTMLElement) => void; + + /** + * List of keys allowed for handling. + * Can include codes of the following keys: + * - Tab + * - Enter + * - Arrow up + * - Arrow down + * - Arrow right + * - Arrow left + * If not specified all keys are enabled + */ + allowedKeys?: number[]; } /** @@ -53,11 +55,9 @@ export default class Flipper { private activated = false; /** - * Flag that allows arrows usage to flip items - * - * @type {boolean} + * List codes of the keys allowed for handling */ - private readonly allowArrows: boolean = true; + private readonly allowedKeys: number[]; /** * Call back for button click/enter @@ -68,9 +68,9 @@ export default class Flipper { * @param {FlipperOptions} options - different constructing settings */ constructor(options: FlipperOptions) { - this.allowArrows = _.isBoolean(options.allowArrows) ? options.allowArrows : true; this.iterator = new DomIterator(options.items, options.focusedItemClass); this.activateCallback = options.activateCallback; + this.allowedKeys = options.allowedKeys || Flipper.usedKeys; } /** @@ -120,15 +120,6 @@ export default class Flipper { document.removeEventListener('keydown', this.onKeyDown); } - /** - * Return current focused button - * - * @returns {HTMLElement|null} - */ - public get currentItem(): HTMLElement|null { - return this.iterator.currentItem; - } - /** * Focus first item */ @@ -142,6 +133,7 @@ export default class Flipper { */ public flipLeft(): void { this.iterator.previous(); + this.flipCallback(); } /** @@ -149,6 +141,14 @@ export default class Flipper { */ public flipRight(): void { this.iterator.next(); + this.flipCallback(); + } + + /** + * Return true if some button is focused + */ + public hasFocus(): boolean { + return !!this.iterator.currentItem; } /** @@ -206,23 +206,7 @@ export default class Flipper { * @returns {boolean} */ private isEventReadyForHandling(event: KeyboardEvent): boolean { - const handlingKeyCodeList = [ - _.keyCodes.TAB, - _.keyCodes.ENTER, - ]; - - const isCurrentItemIsFocusedInput = this.iterator.currentItem == document.activeElement; - - if (this.allowArrows && !isCurrentItemIsFocusedInput) { - handlingKeyCodeList.push( - _.keyCodes.LEFT, - _.keyCodes.RIGHT, - _.keyCodes.UP, - _.keyCodes.DOWN - ); - } - - return this.activated && handlingKeyCodeList.indexOf(event.keyCode) !== -1; + return this.activated && this.allowedKeys.includes(event.keyCode); } /** @@ -266,4 +250,13 @@ export default class Flipper { event.preventDefault(); event.stopPropagation(); } + + /** + * Fired after flipping in any direction + */ + private flipCallback(): void { + if (this.iterator.currentItem) { + this.iterator.currentItem.scrollIntoViewIfNeeded(); + } + } } diff --git a/src/components/i18n/locales/en/messages.json b/src/components/i18n/locales/en/messages.json index f02cac61..42a1520b 100644 --- a/src/components/i18n/locales/en/messages.json +++ b/src/components/i18n/locales/en/messages.json @@ -13,7 +13,9 @@ }, "toolbar": { "toolbox": { - "Add": "" + "Add": "", + "Filter": "", + "Noting found": "" } } }, diff --git a/src/components/modules/saver.ts b/src/components/modules/saver.ts index f75f3019..be5a16a5 100644 --- a/src/components/modules/saver.ts +++ b/src/components/modules/saver.ts @@ -33,7 +33,6 @@ export default class Saver extends Module { chainData = []; try { - blocks.forEach((block: Block) => { chainData.push(this.getSavedData(block)); }); @@ -46,7 +45,7 @@ export default class Saver extends Module { return this.makeOutput(sanitizedData); } catch (e) { _.logLabeled(`Saving failed due to the Error %o`, 'error', e); - } + } } /** diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index 864ec85b..df48ad3d 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -13,6 +13,10 @@ import Toolbox, { ToolboxEvent } from '../../ui/toolbox'; * @todo Tab on non-empty block should open Block Settings of the hoveredBlock (not where caret is set) * - make Block Settings a standalone module * + * @todo - Keyboard-only mode bug: + * press Tab, flip to the Checkbox. press Enter (block will be added), Press Tab + * (Block Tunes will be opened with Move up focused), press Enter, press Tab ———— both Block Tunes and Toolbox will be opened + * * @todo TESTCASE - show toggler after opening and closing the Inline Toolbar * @todo TESTCASE - Click outside Editor holder should close Toolbar and Clear Focused blocks * @todo TESTCASE - Click inside Editor holder should close Toolbar and Clear Focused blocks @@ -33,11 +37,7 @@ interface ToolbarNodes { content: HTMLElement; actions: HTMLElement; - // Content Zone plusButton: HTMLElement; - - // Actions Zone - blockActionsButtons: HTMLElement; settingsToggler: HTMLElement; } /** @@ -137,14 +137,10 @@ export default class Toolbar extends Module { toolbarOpened: 'ce-toolbar--opened', openedToolboxHolderModifier: 'codex-editor--toolbox-opened', - // Content Zone plusButton: 'ce-toolbar__plus', plusButtonShortcut: 'ce-toolbar__plus-shortcut', - plusButtonHidden: 'ce-toolbar__plus--hidden', - - // Actions Zone - blockActionsButtons: 'ce-toolbar__actions-buttons', settingsToggler: 'ce-toolbar__settings-btn', + settingsTogglerHidden: 'ce-toolbar__settings-btn--hidden', }; } @@ -157,23 +153,6 @@ export default class Toolbar extends Module { return this.nodes.wrapper.classList.contains(this.CSS.toolbarOpened); } - /** - * Plus Button public methods - * - * @returns {{hide: function(): void, show: function(): void}} - */ - public get plusButton(): { hide: () => void; show: () => void } { - return { - hide: (): void => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden), - show: (): void => { - if (this.toolboxInstance.isEmpty) { - return; - } - this.nodes.plusButton.classList.remove(this.CSS.plusButtonHidden); - }, - }; - } - /** * Public interface for accessing the Toolbox */ @@ -182,11 +161,14 @@ export default class Toolbar extends Module { close: () => void; open: () => void; toggle: () => void; - flipperHasFocus: boolean; + hasFocus: () => boolean; } { return { opened: this.toolboxInstance.opened, - close: (): void => this.toolboxInstance.close(), + close: (): void => { + this.toolboxInstance.close(); + this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock); + }, open: (): void => { /** * Set current block to cover the case when the Toolbar showed near hovered Block but caret is set to another Block. @@ -196,14 +178,12 @@ export default class Toolbar extends Module { this.toolboxInstance.open(); }, toggle: (): void => this.toolboxInstance.toggle(), - flipperHasFocus: this.toolboxInstance.flipperHasFocus, + hasFocus: (): boolean => this.toolboxInstance.hasFocus(), }; } /** * Block actions appearance manipulations - * - * @returns {{hide: function(): void, show: function(): void}} */ private get blockActions(): { hide: () => void; show: () => void } { return { @@ -216,6 +196,16 @@ export default class Toolbar extends Module { }; } + /** + * Methods for working with Block Tunes toggler + */ + private get blockTunesToggler(): { hide: () => void; show: () => void } { + return { + hide: (): void => this.nodes.settingsToggler.classList.add(this.CSS.settingsTogglerHidden), + show: (): void => this.nodes.settingsToggler.classList.remove(this.CSS.settingsTogglerHidden), + }; + } + /** * Toggles read-only mode * @@ -276,15 +266,15 @@ export default class Toolbar extends Module { /** * Move Toolbar to the Top coordinate of Block */ - this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(toolbarY)}px, 0)`; + this.nodes.wrapper.style.top = `${Math.floor(toolbarY)}px`; /** - * Plus Button should be shown only for __empty__ __default__ block + * Do not show Block Tunes Toggler near single and empty block */ - if (block.tool.isDefault && block.isEmpty) { - this.plusButton.show(); + if (this.Editor.BlockManager.blocks.length === 1 && block.isEmpty) { + this.blockTunesToggler.hide(); } else { - this.plusButton.hide(); + this.blockTunesToggler.show(); } this.open(); @@ -381,13 +371,11 @@ export default class Toolbar extends Module { * - Remove Block Button * - Settings Panel */ - this.nodes.blockActionsButtons = $.make('div', this.CSS.blockActionsButtons); this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler); const settingsIcon = $.svg('dots', 16, 16); $.append(this.nodes.settingsToggler, settingsIcon); - $.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler); - $.append(this.nodes.actions, this.nodes.blockActionsButtons); + $.append(this.nodes.actions, this.nodes.settingsToggler); this.tooltip.onHover( this.nodes.settingsToggler, @@ -400,7 +388,7 @@ export default class Toolbar extends Module { /** * Appending Toolbar components to itself */ - $.append(this.nodes.content, this.makeToolbox()); + $.append(this.nodes.actions, this.makeToolbox()); $.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper); /** @@ -419,6 +407,10 @@ export default class Toolbar extends Module { this.toolboxInstance = new Toolbox({ api: this.Editor.API.methods, tools: this.Editor.Tools.blockTools, + i18nLabels: { + filter: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Filter'), + nothingFound: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Noting found'), + }, }); this.toolboxInstance.on(ToolboxEvent.Opened, () => { @@ -487,18 +479,25 @@ export default class Toolbar extends Module { }, true); /** - * Subscribe to the 'block-hovered' event + * Subscribe to the 'block-hovered' event if currenct view is not mobile + * + * @see https://github.com/codex-team/editor.js/issues/1972 */ - this.eventsDispatcher.on(this.Editor.UI.events.blockHovered, (data: {block: Block}) => { + if (!_.isMobileScreen()) { /** - * Do not move toolbar if Block Settings or Toolbox opened + * Subscribe to the 'block-hovered' event */ - if (this.Editor.BlockSettings.opened || this.toolboxInstance.opened) { - return; - } + this.eventsDispatcher.on(this.Editor.UI.events.blockHovered, (data: {block: Block}) => { + /** + * Do not move toolbar if Block Settings or Toolbox opened + */ + if (this.Editor.BlockSettings.opened || this.toolboxInstance.opened) { + return; + } - this.moveAndOpen(data.block); - }); + this.moveAndOpen(data.block); + }); + } } /** diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts index 91ded10d..799b60fd 100644 --- a/src/components/modules/toolbar/inline.ts +++ b/src/components/modules/toolbar/inline.ts @@ -698,7 +698,10 @@ export default class InlineToolbar extends Module { private enableFlipper(): void { this.flipper = new Flipper({ focusedItemClass: this.CSS.focusedButton, - allowArrows: false, + allowedKeys: [ + _.keyCodes.ENTER, + _.keyCodes.TAB, + ], }); } } diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index aecd6de5..75bfef90 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -231,7 +231,7 @@ export default class UI extends Module { * Toolbar has internal module (Toolbox) that has own Flipper, * so we check it manually */ - if (this.Editor.Toolbar.toolbox.flipperHasFocus) { + if (this.Editor.Toolbar.toolbox.hasFocus()) { return true; } @@ -239,7 +239,7 @@ export default class UI extends Module { return moduleClass.flipper instanceof Flipper; }) .some(([moduleName, moduleClass]) => { - return moduleClass.flipper.currentItem; + return moduleClass.flipper.hasFocus(); }); } @@ -385,7 +385,7 @@ export default class UI extends Module { */ private watchBlockHoveredEvents(): void { /** - * Used to not to emit the same block multiple times to the 'block-hovered' event on every mousemove + * Used to not emit the same block multiple times to the 'block-hovered' event on every mousemove */ let blockHoveredEmitted; diff --git a/src/components/polyfills.ts b/src/components/polyfills.ts index cfa9af74..42b86a05 100644 --- a/src/components/polyfills.ts +++ b/src/components/polyfills.ts @@ -96,3 +96,46 @@ if (!Element.prototype.prepend) { this.insertBefore(docFrag, this.firstChild); }; } + +interface Element { + /** + * Scrolls the current element into the visible area of the browser window + * + * @param centerIfNeeded - true, if the element should be aligned so it is centered within the visible area of the scrollable ancestor. + */ + scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void; +} + +/** + * ScrollIntoViewIfNeeded polyfill by KilianSSL (forked from hsablonniere) + * + * @see {@link https://gist.github.com/KilianSSL/774297b76378566588f02538631c3137} + * @param centerIfNeeded - true, if the element should be aligned so it is centered within the visible area of the scrollable ancestor. + */ +if (!Element.prototype.scrollIntoViewIfNeeded) { + Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded): void { + centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded; + + const parent = this.parentNode, + parentComputedStyle = window.getComputedStyle(parent, null), + parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')), + parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')), + overTop = this.offsetTop - parent.offsetTop < parent.scrollTop, + overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight), + overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft, + overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth), + alignWithTop = overTop && !overBottom; + + if ((overTop || overBottom) && centerIfNeeded) { + parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2; + } + + if ((overLeft || overRight) && centerIfNeeded) { + parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2; + } + + if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) { + this.scrollIntoView(alignWithTop); + } + }; +} diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 2c813caf..89da4215 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -1,15 +1,15 @@ -import $ from '../dom'; import * as _ from '../utils'; -import Flipper from '../flipper'; import { BlockToolAPI } from '../block'; -import I18n from '../i18n'; -import { I18nInternalNS } from '../i18n/namespace-internal'; import Shortcuts from '../utils/shortcuts'; -import Tooltip from '../utils/tooltip'; import BlockTool from '../tools/block'; import ToolsCollection from '../tools/collection'; import { API } from '../../../types'; import EventsDispatcher from '../utils/events'; +import Popover, { PopoverEvent } from '../utils/popover'; + +/** + * @todo the first Tab on the Block — focus Plus Button, the second — focus Block Tunes Toggler, the third — focus next Block + */ /** * Event that can be triggered by the Toolbox @@ -31,6 +31,11 @@ export enum ToolboxEvent { BlockAdded = 'toolbox-block-added', } +/** + * Available i18n dict keys that should be passed to the constructor + */ +type toolboxTextLabelsKeys = 'filter' | 'nothingFound'; + /** * Toolbox * This UI element contains list of Block Tools available to be inserted @@ -45,7 +50,7 @@ export default class Toolbox extends EventsDispatcher { * @returns {boolean} */ public get isEmpty(): boolean { - return this.displayedToolsCount === 0; + return this.toolsToBeDisplayed.length === 0; } /** @@ -60,21 +65,29 @@ export default class Toolbox extends EventsDispatcher { */ private api: API; + /** + * Popover instance. There is a util for vertical lists. + */ + private popover: Popover; + /** * List of Tools available. Some of them will be shown in the Toolbox */ private tools: ToolsCollection; + /** + * Text labels used in the Toolbox. Should be passed from the i18n module + */ + private i18nLabels: Record; + /** * Current module HTML Elements */ private nodes: { toolbox: HTMLElement; - buttons: HTMLElement[]; } = { toolbox: null, - buttons: [], - } + }; /** * CSS styles @@ -84,34 +97,10 @@ export default class Toolbox extends EventsDispatcher { private static get CSS(): { [name: string]: string } { return { toolbox: 'ce-toolbox', - toolboxButton: 'ce-toolbox__button', - toolboxButtonActive: 'ce-toolbox__button--active', - toolboxOpened: 'ce-toolbox--opened', - - buttonTooltip: 'ce-toolbox-button-tooltip', - buttonShortcut: 'ce-toolbox-button-tooltip__shortcut', + toolboxOpenedTop: 'ce-toolbox--opened-top', }; } - /** - * How many tools displayed in Toolbox - * - * @type {number} - */ - private displayedToolsCount = 0; - - /** - * Instance of class that responses for leafing buttons by arrows/tab - * - * @type {Flipper|null} - */ - private flipper: Flipper = null; - - /** - * Tooltip utility Instance - */ - private tooltip: Tooltip; - /** * Id of listener added used to remove it on destroy() */ @@ -124,67 +113,78 @@ export default class Toolbox extends EventsDispatcher { * @param options.api - Editor API methods * @param options.tools - Tools available to check whether some of them should be displayed at the Toolbox or not */ - constructor({ api, tools }) { + constructor({ api, tools, i18nLabels }: {api: API; tools: ToolsCollection; i18nLabels: Record}) { super(); this.api = api; this.tools = tools; - - this.tooltip = new Tooltip(); - } - - /** - * Returns true if the Toolbox has the Flipper activated and the Flipper has selected button - */ - public get flipperHasFocus(): boolean { - return this.flipper && this.flipper.currentItem !== null; + this.i18nLabels = i18nLabels; } /** * Makes the Toolbox */ public make(): Element { - this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox); + this.popover = new Popover({ + className: Toolbox.CSS.toolbox, + searchable: true, + filterLabel: this.i18nLabels.filter, + nothingFoundLabel: this.i18nLabels.nothingFound, + items: this.toolsToBeDisplayed.map(tool => { + return { + icon: tool.toolbox.icon, + label: tool.toolbox.title, + name: tool.name, + onClick: (item): void => { + this.toolButtonActivated(tool.name); + }, + secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '', + }; + }), + }); - this.addTools(); - this.enableFlipper(); + this.popover.on(PopoverEvent.OverlayClicked, this.onOverlayClicked); + + /** + * Enable tools shortcuts + */ + this.enableShortcuts(); + + this.nodes.toolbox = this.popover.getElement(); return this.nodes.toolbox; } + /** + * Returns true if the Toolbox has the Flipper activated and the Flipper has selected button + */ + public hasFocus(): boolean { + return this.popover.hasFocus(); + } + /** * Destroy Module */ public destroy(): void { super.destroy(); - /** - * Sometimes (in read-only mode) there is no Flipper - */ - if (this.flipper) { - this.flipper.deactivate(); - this.flipper = null; - } - if (this.nodes && this.nodes.toolbox) { this.nodes.toolbox.remove(); this.nodes.toolbox = null; - this.nodes.buttons = []; } this.api.listeners.offById(this.clickListenerId); this.removeAllShortcuts(); - this.tooltip.destroy(); + this.popover.off(PopoverEvent.OverlayClicked, this.onOverlayClicked); } /** * Toolbox Tool's button click handler * - * @param {MouseEvent|KeyboardEvent} event - event that activates toolbox button - * @param {string} toolName - button to activate + * @param toolName - tool type to be activated */ - public toolButtonActivate(event: MouseEvent|KeyboardEvent, toolName: string): void { + public toolButtonActivated(toolName: string): void { this.insertNewBlock(toolName); } @@ -196,24 +196,28 @@ export default class Toolbox extends EventsDispatcher { return; } - this.emit(ToolboxEvent.Opened); - - this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpened); + /** + * Open the popover above the button + * if there is not enough available space below it + */ + if (!this.shouldOpenPopoverBottom) { + this.nodes.toolbox.style.setProperty('--popover-height', this.popover.calculateHeight() + 'px'); + this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpenedTop); + } + this.popover.show(); this.opened = true; - this.flipper.activate(); + this.emit(ToolboxEvent.Opened); } /** * Close Toolbox */ public close(): void { - this.emit(ToolboxEvent.Closed); - - this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpened); - + this.popover.hide(); this.opened = false; - this.flipper.deactivate(); + this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpenedTop); + this.emit(ToolboxEvent.Closed); } /** @@ -228,106 +232,65 @@ export default class Toolbox extends EventsDispatcher { } /** - * Iterates available tools and appends them to the Toolbox + * Checks if there popover should be opened downwards. + * It happens in case there is enough space below or not enough space above */ - private addTools(): void { - Array + private get shouldOpenPopoverBottom(): boolean { + const toolboxRect = this.nodes.toolbox.getBoundingClientRect(); + const editorElementRect = this.api.ui.nodes.redactor.getBoundingClientRect(); + const popoverHeight = this.popover.calculateHeight(); + const popoverPotentialBottomEdge = toolboxRect.top + popoverHeight; + const popoverPotentialTopEdge = toolboxRect.top - popoverHeight; + const bottomEdgeForComparison = Math.min(window.innerHeight, editorElementRect.bottom); + + return popoverPotentialTopEdge < editorElementRect.top || popoverPotentialBottomEdge <= bottomEdgeForComparison; + } + + /** + * Handles overlay click + */ + private onOverlayClicked = (): void => { + this.close(); + } + + /** + * Returns list of tools that enables the Toolbox (by specifying the 'toolbox' getter) + */ + @_.cacheable + private get toolsToBeDisplayed(): BlockTool[] { + return Array .from(this.tools.values()) - .forEach((tool) => this.addTool(tool)); + .filter(tool => { + const toolToolboxSettings = tool.toolbox; + + /** + * Skip tools that don't pass 'toolbox' property + */ + if (!toolToolboxSettings) { + return false; + } + + if (toolToolboxSettings && !toolToolboxSettings.icon) { + _.log('Toolbar icon is missed. Tool %o skipped', 'warn', tool.name); + + return false; + } + + return true; + }); } /** - * Append Tool to the Toolbox - * - * @param {BlockToolConstructable} tool - BlockTool object + * Iterate all tools and enable theirs shortcuts if specified */ - private addTool(tool: BlockTool): void { - const toolToolboxSettings = tool.toolbox; + private enableShortcuts(): void { + this.toolsToBeDisplayed.forEach((tool: BlockTool) => { + const shortcut = tool.shortcut; - /** - * Skip tools that don't pass 'toolbox' property - */ - if (!toolToolboxSettings) { - return; - } - - if (toolToolboxSettings && !toolToolboxSettings.icon) { - _.log('Toolbar icon is missed. Tool %o skipped', 'warn', tool.name); - - return; - } - - /** - * @todo Add checkup for the render method - */ - // if (typeof tool.render !== 'function') { - // _.log('render method missed. Tool %o skipped', 'warn', tool); - // return; - // } - - const button = $.make('li', [ Toolbox.CSS.toolboxButton ]); - - button.dataset.tool = tool.name; - button.innerHTML = toolToolboxSettings.icon; - - $.append(this.nodes.toolbox, button); - - this.nodes.toolbox.appendChild(button); - this.nodes.buttons.push(button); - - /** - * Add click listener - */ - this.clickListenerId = this.api.listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => { - this.toolButtonActivate(event, tool.name); + if (shortcut) { + this.enableShortcutForTool(tool.name, shortcut); + } }); - - /** - * Add listeners to show/hide toolbox tooltip - */ - const tooltipContent = this.drawTooltip(tool); - - this.tooltip.onHover(button, tooltipContent, { - placement: 'bottom', - hidingDelay: 200, - }); - - const shortcut = tool.shortcut; - - if (shortcut) { - this.enableShortcut(tool.name, shortcut); - } - - /** Increment Tools count */ - this.displayedToolsCount++; - } - - /** - * Draw tooltip for toolbox tools - * - * @param tool - BlockTool object - * @returns {HTMLElement} - */ - private drawTooltip(tool: BlockTool): HTMLElement { - const toolboxSettings = tool.toolbox || {}; - const name = I18n.t(I18nInternalNS.toolNames, toolboxSettings.title || tool.name); - - let shortcut = tool.shortcut; - - const tooltip = $.make('div', Toolbox.CSS.buttonTooltip); - const hint = document.createTextNode(_.capitalize(name)); - - tooltip.appendChild(hint); - - if (shortcut) { - shortcut = _.beautifyShortcut(shortcut); - - tooltip.appendChild($.make('div', Toolbox.CSS.buttonShortcut, { - textContent: shortcut, - })); - } - - return tooltip; } /** @@ -336,7 +299,7 @@ export default class Toolbox extends EventsDispatcher { * @param {string} toolName - Tool name * @param {string} shortcut - shortcut according to the ShortcutData Module format */ - private enableShortcut(toolName: string, shortcut: string): void { + private enableShortcutForTool(toolName: string, shortcut: string): void { Shortcuts.add({ name: shortcut, on: this.api.ui.nodes.redactor, @@ -352,26 +315,12 @@ export default class Toolbox extends EventsDispatcher { * Fired when the Read-Only mode is activated */ private removeAllShortcuts(): void { - Array - .from(this.tools.values()) - .forEach((tool) => { - const shortcut = tool.shortcut; + this.toolsToBeDisplayed.forEach((tool: BlockTool) => { + const shortcut = tool.shortcut; - if (shortcut) { - Shortcuts.remove(this.api.ui.nodes.redactor, shortcut); - } - }); - } - - /** - * Creates Flipper instance to be able to leaf tools - */ - private enableFlipper(): void { - const tools = Array.from(this.nodes.toolbox.childNodes) as HTMLElement[]; - - this.flipper = new Flipper({ - items: tools, - focusedItemClass: Toolbox.CSS.toolboxButtonActive, + if (shortcut) { + Shortcuts.remove(this.api.ui.nodes.redactor, shortcut); + } }); } diff --git a/src/components/utils.ts b/src/components/utils.ts index e1e756f6..ab824308 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -762,3 +762,10 @@ export function cacheable void; +} + +/** + * Event that can be triggered by the Popover + */ +export enum PopoverEvent { + /** + * When popover overlay is clicked + */ + OverlayClicked = 'overlay-clicked', +} + +/** + * Popover is the UI element for displaying vertical lists + */ +export default class Popover extends EventsDispatcher { + /** + * Items list to be displayed + */ + private readonly items: PopoverItem[]; + + /** + * Created nodes + */ + private nodes: { + wrapper: HTMLElement; + popover: HTMLElement; + items: HTMLElement; + nothingFound: HTMLElement; + overlay: HTMLElement; + } = { + wrapper: null, + popover: null, + items: null, + nothingFound: null, + overlay: null, + } + + /** + * Additional wrapper's class name + */ + private readonly className: string; + + /** + * Listeners util instance + */ + private listeners: Listeners; + + /** + * Flipper - module for keyboard iteration between elements + */ + private flipper: Flipper; + + /** + * Pass true to enable local search field + */ + private readonly searchable: boolean; + + /** + * Instance of the Search Input + */ + private search: SearchInput; + + /** + * Label for the 'Filter' placeholder + */ + private readonly filterLabel: string; + + /** + * Label for the 'Nothing found' message + */ + private readonly nothingFoundLabel: string; + + /** + * Style classes + */ + private static get CSS(): { + popover: string; + popoverOpened: string; + itemsWrapper: string; + item: string; + itemHidden: string; + itemFocused: string; + itemLabel: string; + itemIcon: string; + itemSecondaryLabel: string; + noFoundMessage: string; + noFoundMessageShown: string; + popoverOverlay: string; + popoverOverlayHidden: string; + documentScrollLocked: string; + } { + return { + popover: 'ce-popover', + popoverOpened: 'ce-popover--opened', + itemsWrapper: 'ce-popover__items', + item: 'ce-popover__item', + itemHidden: 'ce-popover__item--hidden', + itemFocused: 'ce-popover__item--focused', + itemLabel: 'ce-popover__item-label', + itemIcon: 'ce-popover__item-icon', + itemSecondaryLabel: 'ce-popover__item-secondary-label', + noFoundMessage: 'ce-popover__no-found', + noFoundMessageShown: 'ce-popover__no-found--shown', + popoverOverlay: 'ce-popover__overlay', + popoverOverlayHidden: 'ce-popover__overlay--hidden', + documentScrollLocked: 'ce-scroll-locked', + }; + } + + /** + * Creates the Popover + * + * @param options - config + * @param options.items - config for items to be displayed + * @param options.className - additional class name to be added to the popover wrapper + * @param options.filterLabel - label for the search Field + * @param options.nothingFoundLabel - label of the 'nothing found' message + */ + constructor({ items, className, searchable, filterLabel, nothingFoundLabel }: { + items: PopoverItem[]; + className?: string; + searchable?: boolean; + filterLabel: string; + nothingFoundLabel: string; + }) { + super(); + this.items = items; + this.className = className || ''; + this.searchable = searchable; + this.listeners = new Listeners(); + + this.filterLabel = filterLabel; + this.nothingFoundLabel = nothingFoundLabel; + + this.render(); + this.enableFlipper(); + } + + /** + * Returns rendered wrapper + */ + public getElement(): HTMLElement { + return this.nodes.wrapper; + } + + /** + * Shows the Popover + */ + public show(): void { + this.nodes.popover.classList.add(Popover.CSS.popoverOpened); + this.nodes.overlay.classList.remove(Popover.CSS.popoverOverlayHidden); + this.flipper.activate(); + + if (this.searchable) { + window.requestAnimationFrame(() => { + this.search.focus(); + }); + } + + if (isMobileScreen()) { + document.documentElement.classList.add(Popover.CSS.documentScrollLocked); + } + } + + /** + * Hides the Popover + */ + public hide(): void { + this.search.clear(); + this.nodes.popover.classList.remove(Popover.CSS.popoverOpened); + this.nodes.overlay.classList.add(Popover.CSS.popoverOverlayHidden); + this.flipper.deactivate(); + + if (isMobileScreen()) { + document.documentElement.classList.remove(Popover.CSS.documentScrollLocked); + } + } + + /** + * Clears memory + */ + public destroy(): void { + this.listeners.removeAll(); + } + + /** + * Returns true if some item is focused + */ + public hasFocus(): boolean { + return this.flipper.hasFocus(); + } + + /** + * Helps to calculate height of popover while it is not displayed on screen. + * Renders invisible clone of popover to get actual height. + */ + @cacheable + public calculateHeight(): number { + let height = 0; + const popoverClone = this.nodes.popover.cloneNode(true) as HTMLElement; + + popoverClone.style.visibility = 'hidden'; + popoverClone.style.position = 'absolute'; + popoverClone.style.top = '-1000px'; + popoverClone.classList.add(Popover.CSS.popoverOpened); + document.body.appendChild(popoverClone); + height = popoverClone.offsetHeight; + popoverClone.remove(); + + return height; + } + + /** + * Makes the UI + */ + private render(): void { + this.nodes.wrapper = Dom.make('div', this.className); + this.nodes.popover = Dom.make('div', Popover.CSS.popover); + this.nodes.wrapper.appendChild(this.nodes.popover); + + this.nodes.overlay = Dom.make('div', [Popover.CSS.popoverOverlay, Popover.CSS.popoverOverlayHidden]); + this.nodes.wrapper.appendChild(this.nodes.overlay); + + if (this.searchable) { + this.addSearch(this.nodes.popover); + } + + this.nodes.items = Dom.make('div', Popover.CSS.itemsWrapper); + this.items.forEach(item => { + this.nodes.items.appendChild(this.createItem(item)); + }); + + this.nodes.popover.appendChild(this.nodes.items); + this.nodes.nothingFound = Dom.make('div', [ Popover.CSS.noFoundMessage ], { + textContent: this.nothingFoundLabel, + }); + + this.nodes.popover.appendChild(this.nodes.nothingFound); + + this.listeners.on(this.nodes.popover, 'click', (event: KeyboardEvent|MouseEvent) => { + const clickedItem = (event.target as HTMLElement).closest(`.${Popover.CSS.item}`) as HTMLElement; + + if (clickedItem) { + this.itemClicked(clickedItem); + } + }); + + this.listeners.on(this.nodes.overlay, 'click', () => { + this.emit(PopoverEvent.OverlayClicked); + }); + } + + /** + * Adds the s4arch field to passed element + * + * @param holder - where to append search input + */ + private addSearch(holder: HTMLElement): void { + this.search = new SearchInput({ + items: this.items, + placeholder: this.filterLabel, + onSearch: (filteredItems): void => { + const itemsVisible = []; + + this.items.forEach((item, index) => { + const itemElement = this.nodes.items.children[index]; + + if (filteredItems.includes(item)) { + itemsVisible.push(itemElement); + itemElement.classList.remove(Popover.CSS.itemHidden); + } else { + itemElement.classList.add(Popover.CSS.itemHidden); + } + }); + + this.nodes.nothingFound.classList.toggle(Popover.CSS.noFoundMessageShown, itemsVisible.length === 0); + + /** + * Update flipper items with only visible + */ + this.flipper.deactivate(); + this.flipper.activate(itemsVisible); + this.flipper.focusFirst(); + }, + }); + + const searchField = this.search.getElement(); + + holder.appendChild(searchField); + } + + /** + * Renders the single item + * + * @param item - item data to be rendered + */ + private createItem(item: PopoverItem): HTMLElement { + const el = Dom.make('div', Popover.CSS.item); + + el.dataset.itemName = item.name; + const label = Dom.make('div', Popover.CSS.itemLabel, { + innerHTML: item.label, + }); + + if (item.icon) { + el.appendChild(Dom.make('div', Popover.CSS.itemIcon, { + innerHTML: item.icon, + })); + } + + el.appendChild(label); + + if (item.secondaryLabel) { + el.appendChild(Dom.make('div', Popover.CSS.itemSecondaryLabel, { + textContent: item.secondaryLabel, + })); + } + + return el; + } + + /** + * Item click handler + * + * @param itemEl - clicked item + */ + private itemClicked(itemEl: HTMLElement): void { + const allItems = this.nodes.wrapper.querySelectorAll(`.${Popover.CSS.item}`); + const itemIndex = Array.from(allItems).indexOf(itemEl); + const clickedItem = this.items[itemIndex]; + + clickedItem.onClick(clickedItem); + } + + /** + * Creates Flipper instance to be able to leaf tools + */ + private enableFlipper(): void { + const tools = Array.from(this.nodes.wrapper.querySelectorAll(`.${Popover.CSS.item}`)) as HTMLElement[]; + + this.flipper = new Flipper({ + items: tools, + focusedItemClass: Popover.CSS.itemFocused, + allowedKeys: [ + keyCodes.TAB, + keyCodes.UP, + keyCodes.DOWN, + keyCodes.ENTER, + ], + }); + } +} diff --git a/src/components/utils/search-input.ts b/src/components/utils/search-input.ts new file mode 100644 index 00000000..a5b8e3ae --- /dev/null +++ b/src/components/utils/search-input.ts @@ -0,0 +1,152 @@ +import Dom from '../dom'; +import Listeners from './listeners'; + +/** + * Item that could be searched + */ +interface SearchableItem { + label: string; +} + +/** + * Provides search input element and search logic + */ +export default class SearchInput { + /** + * Input wrapper element + */ + private wrapper: HTMLElement; + + /** + * Editable input itself + */ + private input: HTMLInputElement; + + /** + * The instance of the Listeners util + */ + private listeners: Listeners; + + /** + * Items for local search + */ + private items: SearchableItem[]; + + /** + * Current search query + */ + private searchQuery: string; + + /** + * Externally passed callback for the search + */ + private readonly onSearch: (items: SearchableItem[]) => void; + + /** + * Styles + */ + private static get CSS(): { + input: string; + icon: string; + wrapper: string; + } { + return { + wrapper: 'cdx-search-field', + icon: 'cdx-search-field__icon', + input: 'cdx-search-field__input', + }; + } + + /** + * @param options - available config + * @param options.items - searchable items list + * @param options.onSearch - search callback + * @param options.placeholder - input placeholder + */ + constructor({ items, onSearch, placeholder }: { + items: SearchableItem[]; + onSearch: (items: SearchableItem[]) => void; + placeholder: string; + }) { + this.listeners = new Listeners(); + this.items = items; + this.onSearch = onSearch; + + this.render(placeholder); + } + + /** + * Returns search field element + */ + public getElement(): HTMLElement { + return this.wrapper; + } + + /** + * Sets focus to the input + */ + public focus(): void { + this.input.focus(); + } + + /** + * Clears search query and results + */ + public clear(): void { + this.input.value = ''; + this.searchQuery = ''; + this.onSearch(this.foundItems); + } + + /** + * Clears memory + */ + public destroy(): void { + this.listeners.removeAll(); + } + + /** + * Creates the search field + * + * @param placeholder - input placeholder + */ + private render(placeholder: string): void { + this.wrapper = Dom.make('div', SearchInput.CSS.wrapper); + + const iconWrapper = Dom.make('div', SearchInput.CSS.icon); + const icon = Dom.svg('search', 16, 16); + + this.input = Dom.make('input', SearchInput.CSS.input, { + placeholder, + }) as HTMLInputElement; + + iconWrapper.appendChild(icon); + this.wrapper.appendChild(iconWrapper); + this.wrapper.appendChild(this.input); + + this.listeners.on(this.input, 'input', () => { + this.searchQuery = this.input.value; + + this.onSearch(this.foundItems); + }); + } + + /** + * Returns list of found items for the current search query + */ + private get foundItems(): SearchableItem[] { + return this.items.filter(item => this.checkItem(item)); + } + + /** + * Contains logic for checking whether passed item conforms the search query + * + * @param item - item to be checked + */ + private checkItem(item: SearchableItem): boolean { + const text = item.label.toLowerCase(); + const query = this.searchQuery.toLowerCase(); + + return text.includes(query); + } +} diff --git a/src/styles/animations.css b/src/styles/animations.css index fced4886..c8189902 100644 --- a/src/styles/animations.css +++ b/src/styles/animations.css @@ -117,3 +117,20 @@ transform: translateY(0); } } + +@keyframes panelShowingMobile { + from { + opacity: 0; + transform: translateY(14px) scale(0.98); + } + + 70% { + opacity: 1; + transform: translateY(-4px); + } + + to { + + transform: translateY(0); + } +} diff --git a/src/styles/block.css b/src/styles/block.css index 55bbc142..fb68133e 100644 --- a/src/styles/block.css +++ b/src/styles/block.css @@ -1,4 +1,17 @@ +@keyframes fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + .ce-block { + animation: fade-in 300ms ease; + animation-fill-mode: initial; + &:first-of-type { margin-top: 0; } diff --git a/src/styles/input.css b/src/styles/input.css new file mode 100644 index 00000000..1c94d8fe --- /dev/null +++ b/src/styles/input.css @@ -0,0 +1,46 @@ +.cdx-search-field { + --icon-margin-right: 10px; + + background: rgba(232,232,235,0.49); + border: 1px solid rgba(226,226,229,0.20); + border-radius: 6px; + padding: 2px; + display: grid; + grid-template-columns: auto auto 1fr; + grid-template-rows: auto; + + &__icon { + width: var(--toolbox-buttons-size); + height: var(--toolbox-buttons-size); + display: flex; + align-items: center; + justify-content: center; + margin-right: var(--icon-margin-right); + + .icon { + width: 14px; + height: 14px; + color: var(--grayText); + flex-shrink: 0; + } + } + + + &__input { + font-size: 14px; + outline: none; + font-weight: 500; + font-family: inherit; + border: 0; + background: transparent; + margin: 0; + padding: 0; + line-height: 22px; + min-width: calc(100% - var(--toolbox-buttons-size) - var(--icon-margin-right)); + + &::placeholder { + color: var(--grayText); + font-weight: 500; + } + } +} diff --git a/src/styles/main.css b/src/styles/main.css index 94fa26bd..e1adc48d 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -10,3 +10,5 @@ @import './export.css'; @import './stub.css'; @import './rtl.css'; +@import './popover.css'; +@import './input.css'; diff --git a/src/styles/popover.css b/src/styles/popover.css new file mode 100644 index 00000000..207907a6 --- /dev/null +++ b/src/styles/popover.css @@ -0,0 +1,142 @@ +.ce-popover { + position: absolute; + opacity: 0; + will-change: opacity, transform; + display: flex; + flex-direction: column; + padding: 6px; + min-width: 200px; + overflow: hidden; + box-sizing: border-box; + flex-shrink: 0; + max-height: 0; + + @apply --overlay-pane; + + z-index: 4; + flex-wrap: nowrap; + + &--opened { + opacity: 1; + max-height: 270px; + animation: panelShowing 100ms ease; + + @media (--mobile) { + animation: panelShowingMobile 250ms ease; + } + } + + &::-webkit-scrollbar { + width: 7px; + } + + &::-webkit-scrollbar-thumb { + box-sizing: border-box; + box-shadow: inset 0 0 2px 2px var(--bg-light); + border: 3px solid transparent; + border-left-width: 0px; + border-top-width: 4px; + border-bottom-width: 4px; + } + + @media (--mobile) { + position: fixed; + max-width: none; + min-width: auto; + left: 5px; + right: 5px; + bottom: calc(5px + env(safe-area-inset-bottom)); + top: auto; + border-radius: 10px; + } + + &__items { + overflow-y: auto; + overscroll-behavior: contain; + + @media (--not-mobile) { + margin-top: 5px; + } + } + + &__item { + @apply --popover-button; + + &--focused { + @apply --button-focused; + } + + &--hidden { + display: none; + } + + &-icon { + @apply --tool-icon; + } + + &-label { + &::after { + content: ''; + width: 25px; + display: inline-block; + } + } + + &-secondary-label { + color: var(--grayText); + font-size: 12px; + margin-left: auto; + white-space: nowrap; + letter-spacing: -0.1em; + padding-right: 5px; + margin-bottom: -2px; + opacity: 0.6; + + @media (--mobile){ + display: none; + } + } + } + + &__no-found { + @apply --popover-button; + + color: var(--grayText); + display: none; + cursor: default; + + &--shown { + display: block; + } + + &:hover { + background-color: transparent; + } + } + + @media (--mobile) { + &__overlay { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: var(--color-dark); + opacity: 0.5; + z-index: 3; + transition: opacity 0.12s ease-in; + will-change: opacity; + visibility: visible; + } + + .cdx-search-field { + display: none; + } + } + + &__overlay--hidden { + z-index: 0; + opacity: 0; + visibility: hidden; + } +} diff --git a/src/styles/settings.css b/src/styles/settings.css index c6693e85..78622a0b 100644 --- a/src/styles/settings.css +++ b/src/styles/settings.css @@ -1,7 +1,7 @@ .ce-settings { @apply --overlay-pane; - right: -1px; - top: 30px; + top: var(--toolbar-buttons-size); + left: 0; min-width: 114px; box-sizing: content-box; diff --git a/src/styles/toolbar.css b/src/styles/toolbar.css index ceceacb7..f3fef0ef 100644 --- a/src/styles/toolbar.css +++ b/src/styles/toolbar.css @@ -4,34 +4,18 @@ right: 0; top: 0; transition: opacity 100ms ease; - will-change: opacity, transform; - display: none; + will-change: opacity, top; - @media (--mobile) { - @apply --overlay-pane; - padding: 3px; - margin-top: 5px; - } + display: none; &--opened { display: block; - - @media (--mobile){ - display: flex; - } } &__content { max-width: var(--content-width); margin: 0 auto; position: relative; - - @media (--mobile){ - display: flex; - align-content: center; - margin: 0; - max-width: 100%; - } } &__plus { @@ -44,14 +28,9 @@ margin-top: 5px; } - &--hidden { - display: none; - } - @media (--mobile){ - display: inline-flex !important; + @apply --overlay-pane; position: static; - transform: none !important; } } @@ -64,37 +43,37 @@ right: 100%; opacity: 0; display: flex; - - @media (--mobile){ - position: absolute; - right: auto; - top: 50%; - transform: translateY(-50%); - display: flex; - align-items: center; - } + padding-right: 5px; &--opened { opacity: 1; } - &-buttons { - text-align: right; + @media (--mobile){ + right: auto; } } &__settings-btn { @apply --toolbox-button; - width: 18px; - margin: 0 5px; + margin-left: 5px; cursor: pointer; user-select: none; - } -} -.codex-editor--toolbox-opened .ce-toolbar__actions { - display: none; + @media (--not-mobile){ + width: 18px; + } + + &--hidden { + display: none; + } + + @media (--mobile){ + @apply --overlay-pane; + position: static; + } + } } /** diff --git a/src/styles/toolbox.css b/src/styles/toolbox.css index c02c5937..d8b60210 100644 --- a/src/styles/toolbox.css +++ b/src/styles/toolbox.css @@ -1,44 +1,24 @@ .ce-toolbox { - position: absolute; - visibility: hidden; - transition: opacity 100ms ease; - will-change: opacity; - display: flex; - flex-direction: row; + --gap: 8px; - @media (--mobile){ - position: static; - transform: none !important; - align-items: center; - overflow-x: auto; - } + @media (--not-mobile){ + position: absolute; + top: calc(var(--toolbox-buttons-size) + var(--gap)); + left: 0; - &--opened { - opacity: 1; - visibility: visible; - } - - &__button { - @apply --toolbox-button; - flex-shrink: 0; - margin-left: 5px; + &--opened-top { + top: calc(-1 * (var(--gap) + var(--popover-height))); + } } } -.ce-toolbox-button-tooltip { - &__shortcut { - opacity: 0.6; - word-spacing: -3px; - margin-top: 3px; - } -} - -/** - * Styles for Narrow mode - */ .codex-editor--narrow .ce-toolbox { - @media (--not-mobile) { - background: #fff; - z-index: 2; + @media (--not-mobile){ + left: auto; + right: 0; + + .ce-popover { + right: 0; + } } } diff --git a/src/styles/ui.css b/src/styles/ui.css index 4a240994..da65fc37 100644 --- a/src/styles/ui.css +++ b/src/styles/ui.css @@ -127,3 +127,12 @@ transform: rotate(360deg); } } + +.ce-scroll-locked, .ce-scroll-locked > body { + height: 100vh; + overflow: hidden; + /** + * Mobile Safari fix + */ + position: relative; +} \ No newline at end of file diff --git a/src/styles/variables.css b/src/styles/variables.css index d8528ea6..ca9b14b3 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -1,5 +1,9 @@ +/** + * Updating values in media queries should also include changes in utils.ts@isMobile + */ @custom-media --mobile (width <= 650px); @custom-media --not-mobile (width >= 651px); +@custom-media --can-hover (hover: hover); :root { /** @@ -21,7 +25,7 @@ /** * Gray icons hover */ - --color-dark: #1D202B; + --color-dark: #1D202B; /** * Blue icons @@ -95,6 +99,11 @@ } }; + --button-focused: { + box-shadow: inset 0 0 0px 1px rgba(7, 161, 227, 0.08); + background: rgba(34, 186, 255, 0.08) !important; + }; + /** * Styles for Toolbox Buttons and Plus Button */ @@ -103,22 +112,25 @@ cursor: pointer; width: var(--toolbox-buttons-size); height: var(--toolbox-buttons-size); - border-radius: 3px; + border-radius: 7px; display: inline-flex; justify-content: center; align-items: center; + user-select: none; @media (--mobile){ width: var(--toolbox-buttons-size--mobile); height: var(--toolbox-buttons-size--mobile); } - &:hover, - &--active { - background-color: var(--bg-light); + @media (--can-hover) { + &:hover { + background-color: var(--bg-light); + } } - &--active{ + &--active { + background-color: var(--bg-light); animation: bounceIn 0.75s 1; animation-fill-mode: forwards; } @@ -132,9 +144,9 @@ display: inline-flex; align-items: center; justify-content: center; - width: 34px; - height: 34px; - line-height: 34px; + width: var(--toolbar-buttons-size); + height: var(--toolbar-buttons-size); + line-height: var(--toolbar-buttons-size); padding: 0 !important; text-align: center; border-radius: 3px; @@ -155,8 +167,7 @@ } &--focused { - box-shadow: inset 0 0 0px 1px rgba(7, 161, 227, 0.08); - background: rgba(34, 186, 255, 0.08) !important; + @apply --button-focused; &-animated { animation-name: buttonClicked; @@ -164,5 +175,66 @@ } } }; + + /** + * Element of the Toolbox. Has icon and label + */ + --popover-button: { + display: grid; + grid-template-columns: auto auto 1fr; + grid-template-rows: auto; + justify-content: start; + white-space: nowrap; + padding: 3px; + font-size: 14px; + line-height: 20px; + font-weight: 500; + cursor: pointer; + align-items: center; + border-radius: 6px; + + &:not(:last-of-type){ + margin-bottom: 1px; + } + + @media (--can-hover) { + &:hover { + background-color: var(--bg-light); + } + } + + @media (--mobile) { + font-size: 16px; + padding: 4px; + } + }; + + /** + * Tool icon with border + */ + --tool-icon: { + display: inline-flex; + width: var(--toolbox-buttons-size); + height: var(--toolbox-buttons-size); + border: 1px solid var(--color-gray-border); + border-radius: 5px; + align-items: center; + justify-content: center; + background: #fff; + box-sizing: border-box; + flex-shrink: 0; + margin-right: 10px; + + @media (--mobile) { + width: var(--toolbox-buttons-size--mobile); + height: var(--toolbox-buttons-size--mobile); + border-radius: 8px; + } + + svg { + width: 12px; + height: 12px; + } + } } diff --git a/test/cypress/tests/block-ids.spec.ts b/test/cypress/tests/block-ids.spec.ts index 3fc1e209..e207e58f 100644 --- a/test/cypress/tests/block-ids.spec.ts +++ b/test/cypress/tests/block-ids.spec.ts @@ -31,7 +31,7 @@ describe.only('Block ids', () => { .click(); cy.get('[data-cy=editorjs]') - .get('li.ce-toolbox__button[data-tool=header]') + .get('div.ce-popover__item[data-item-name=header]') .click(); cy.get('[data-cy=editorjs]') diff --git a/test/cypress/tests/onchange.spec.ts b/test/cypress/tests/onchange.spec.ts index 428a4ad0..f4740c30 100644 --- a/test/cypress/tests/onchange.spec.ts +++ b/test/cypress/tests/onchange.spec.ts @@ -131,7 +131,7 @@ describe('onChange callback', () => { .click(); cy.get('[data-cy=editorjs]') - .get('li.ce-toolbox__button[data-tool=delimiter]') + .get('div.ce-popover__item[data-item-name=delimiter]') .click(); cy.get('@onChange').should('be.calledThrice'); @@ -178,7 +178,7 @@ describe('onChange callback', () => { .click(); cy.get('[data-cy=editorjs]') - .get('li.ce-toolbox__button[data-tool=header]') + .get('div.ce-popover__item[data-item-name=header]') .click(); cy.get('@onChange').should('be.calledTwice'); @@ -245,6 +245,14 @@ describe('onChange callback', () => { it('should fire onChange callback when block is removed', () => { createEditor(); + /** + * The only block does not have Tune menu, so need to create at least 2 blocks to test deleting + */ + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click() + .type('some text'); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click(); From 6cd6bd5de33a8369e66037a60b65fd9bab10dce4 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Mon, 25 Apr 2022 20:40:29 +0100 Subject: [PATCH 13/26] Fix readOnly.isEnabled getter (#1831) * Fix readOnly.isEnabled getter * Add tests * Update CHANGELOG.md Co-authored-by: Peter Savchenko --- docs/CHANGELOG.md | 3 +- src/components/modules/api/readonly.ts | 7 +++- test/cypress/tests/readOnly.spec.ts | 50 ++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 test/cypress/tests/readOnly.spec.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b1ff985d..bf56c3c2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,8 +5,9 @@ - `New` — *UI* — The Toolbox became vertical 🥳 - `Improvement` — *UI* — the Plus button will always be shown (previously, it appears only for empty blocks) - `Improvement` — *Dev Example Page* - Server added to allow opening example page on other devices in network. -- `Fix` - `UI` - the Toolbar won't move on hover at mobile viewports. Resolves [#1972](https://github.com/codex-team/editor.js/issues/1972) +- `Fix` — `UI` — the Toolbar won't move on hover at mobile viewports. Resolves [#1972](https://github.com/codex-team/editor.js/issues/1972) - `Fix` — `OnChange` event invocation after block insertion. [#1997](https://github.com/codex-team/editor.js/issues/1997) +- `Fix` — `ReadOnly` — the `readonly.isEnabled` API getter now works correctly after `readonly.toggle()` calling. Resolves [#1822](https://github.com/codex-team/editor.js/issues/1822) ### 2.23.2 diff --git a/src/components/modules/api/readonly.ts b/src/components/modules/api/readonly.ts index 19a8c2d9..7b804839 100644 --- a/src/components/modules/api/readonly.ts +++ b/src/components/modules/api/readonly.ts @@ -10,9 +10,14 @@ export default class ReadOnlyAPI extends Module { * Available methods */ public get methods(): ReadOnly { + const getIsEnabled = (): boolean => this.isEnabled; + + // eslint-disable-next-line @typescript-eslint/no-this-alias return { toggle: (state): Promise => this.toggle(state), - isEnabled: this.isEnabled, + get isEnabled(): boolean { + return getIsEnabled(); + }, }; } diff --git a/test/cypress/tests/readOnly.spec.ts b/test/cypress/tests/readOnly.spec.ts new file mode 100644 index 00000000..18dec6d2 --- /dev/null +++ b/test/cypress/tests/readOnly.spec.ts @@ -0,0 +1,50 @@ +import EditorJS, { EditorConfig } from '../../../types'; + +describe('ReadOnly API spec', () => { + function createEditor(config?: EditorConfig): void { + const editorConfig = Object.assign({}, config || {}); + + cy.createEditor(editorConfig).as('editorInstance'); + } + + it('should return correct value for readOnly.isEnabled when editor initialized in normal mode', () => { + createEditor(); + + cy + .get('@editorInstance') + .then(editor => { + expect(editor.readOnly.isEnabled).to.be.false; + }); + }); + + it('should return correct value for readOnly.isEnabled when editor initialized in read-only mode', () => { + createEditor({ + readOnly: true, + }); + + cy + .get('@editorInstance') + .then(editor => { + expect(editor.readOnly.isEnabled).to.be.true; + }); + }); + + it('should return correct value for readOnly.isEnabled when read-only mode toggled', () => { + createEditor(); + + cy + .get('@editorInstance') + .then(async editor => { + expect(editor.readOnly.isEnabled).to.be.false; + + editor.readOnly.toggle() + .then(() => { + expect(editor.readOnly.isEnabled).to.be.true; + }) + .then(() => editor.readOnly.toggle()) + .then(() => { + expect(editor.readOnly.isEnabled).to.be.false; + }); + }); + }); +}); From 1b84f66219066b34bb3bc58dd5e0510e9c82c527 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:14:01 +0300 Subject: [PATCH 14/26] Bump minimist from 1.2.5 to 1.2.6 (#2011) Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/substack/minimist/releases) - [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6) --- updated-dependencies: - dependency-name: minimist dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index ab0d651c..90abc838 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5816,8 +5816,9 @@ minimist-options@^4.0.1: is-plain-obj "^1.1.0" minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== minipass-collect@^1.0.2: version "1.0.2" From ee28354cb4348b11c060cecc79554180cfb7a628 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:37:56 +0300 Subject: [PATCH 15/26] Bump browserslist from 4.12.0 to 4.20.3 (#2028) Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.12.0 to 4.20.3. - [Release notes](https://github.com/browserslist/browserslist/releases) - [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md) - [Commits](https://github.com/browserslist/browserslist/compare/4.12.0...4.20.3) --- updated-dependencies: - dependency-name: browserslist dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 73 ++++++++++++++++++++----------------------------------- 1 file changed, 26 insertions(+), 47 deletions(-) diff --git a/yarn.lock b/yarn.lock index 90abc838..9c00ab6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2395,24 +2395,16 @@ browserify@^16.1.0: vm-browserify "^1.0.0" xtend "^4.0.0" -browserslist@^4.0.0, browserslist@^4.11.1, browserslist@^4.6.4, browserslist@^4.8.5, browserslist@^4.9.1: - version "4.12.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.0.tgz#06c6d5715a1ede6c51fc39ff67fd647f740b656d" +browserslist@^4.0.0, browserslist@^4.11.1, browserslist@^4.14.5, browserslist@^4.16.3, browserslist@^4.6.0, browserslist@^4.6.4, browserslist@^4.8.5, browserslist@^4.9.1: + version "4.20.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf" + integrity sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg== dependencies: - caniuse-lite "^1.0.30001043" - electron-to-chromium "^1.3.413" - node-releases "^1.1.53" - pkg-up "^2.0.0" - -browserslist@^4.14.5, browserslist@^4.16.3, browserslist@^4.6.0: - version "4.16.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" - dependencies: - caniuse-lite "^1.0.30001181" - colorette "^1.2.1" - electron-to-chromium "^1.3.649" + caniuse-lite "^1.0.30001332" + electron-to-chromium "^1.4.118" escalade "^3.1.1" - node-releases "^1.1.70" + node-releases "^2.0.3" + picocolors "^1.0.0" buffer-crc32@~0.2.3: version "0.2.13" @@ -2575,13 +2567,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001039, caniuse-lite@^1.0.30001043: - version "1.0.30001048" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001048.tgz#4bb4f1bc2eb304e5e1154da80b93dee3f1cf447e" - -caniuse-lite@^1.0.30001181: - version "1.0.30001202" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001202.tgz#4cb3bd5e8a808e8cd89e4e66c549989bc8137201" +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001039, caniuse-lite@^1.0.30001332: + version "1.0.30001332" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz#39476d3aa8d83ea76359c70302eafdd4a1d727dd" + integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw== caseless@~0.12.0: version "0.12.0" @@ -2879,10 +2868,6 @@ color@^3.0.0: color-convert "^1.9.1" color-string "^1.5.2" -colorette@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - colors@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -3569,13 +3554,10 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -electron-to-chromium@^1.3.413: - version "1.3.418" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.418.tgz#840021191f466b803a873e154113620c9f53cec6" - -electron-to-chromium@^1.3.649: - version "1.3.690" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.690.tgz#54df63ec42fba6b8e9e05fe4be52caeeedb6e634" +electron-to-chromium@^1.4.118: + version "1.4.122" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.122.tgz#56e518e8c4433876b01d4460eac0f653841ed510" + integrity sha512-VuLNxTIt8sBWIT2sd186xPd18Y8KcK8myLd9nMdSJOYZwFUxxbLVmX/T1VX+qqaytRlrYYQv39myxJdXtu7Ysw== elegant-spinner@^1.0.1: version "1.0.1" @@ -3690,6 +3672,7 @@ es6-promise@^2.3.0: escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== escape-string-regexp@4.0.0: version "4.0.0" @@ -6040,13 +6023,10 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" -node-releases@^1.1.53: - version "1.1.53" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4" - -node-releases@^1.1.70: - version "1.1.71" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" +node-releases@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96" + integrity sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw== normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" @@ -6482,6 +6462,11 @@ performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" @@ -6518,12 +6503,6 @@ pkg-dir@^4.1.0: dependencies: find-up "^4.0.0" -pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" - dependencies: - find-up "^2.1.0" - posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" From 06900968aca9d3cf930312c73e386828716bd69a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:38:49 +0300 Subject: [PATCH 16/26] Bump color-string from 1.5.3 to 1.9.1 (#2027) Bumps [color-string](https://github.com/Qix-/color-string) from 1.5.3 to 1.9.1. - [Release notes](https://github.com/Qix-/color-string/releases) - [Changelog](https://github.com/Qix-/color-string/blob/master/CHANGELOG.md) - [Commits](https://github.com/Qix-/color-string/commits/1.9.1) --- updated-dependencies: - dependency-name: color-string dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9c00ab6c..3b3512d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2855,8 +2855,9 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" color-string@^1.5.2: - version "1.5.3" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" @@ -4878,6 +4879,7 @@ is-arrayish@^0.2.1: is-arrayish@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== is-binary-path@^1.0.0: version "1.0.1" @@ -7810,6 +7812,7 @@ simple-concat@^1.0.0: simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= dependencies: is-arrayish "^0.3.1" From 0e30d8781e613b1a689dc5403905329424ed64ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:41:34 +0300 Subject: [PATCH 17/26] Bump moment from 2.29.1 to 2.29.2 (#2016) Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2. - [Release notes](https://github.com/moment/moment/releases) - [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md) - [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2) --- updated-dependencies: - dependency-name: moment dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Peter Savchenko --- yarn.lock | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3b3512d5..0507a34a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5916,8 +5916,9 @@ module-deps@^6.0.0, module-deps@^6.2.3: xtend "^4.0.0" moment@^2.29.1: - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + version "2.29.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4" + integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg== move-concurrently@^1.0.1: version "1.0.1" From 13b8e11a218886c4e98ad668488bcb25137e6b37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:43:02 +0300 Subject: [PATCH 18/26] Bump cached-path-relative from 1.0.2 to 1.1.0 (#1948) Bumps [cached-path-relative](https://github.com/ashaffer/cached-path-relative) from 1.0.2 to 1.1.0. - [Release notes](https://github.com/ashaffer/cached-path-relative/releases) - [Commits](https://github.com/ashaffer/cached-path-relative/commits) --- updated-dependencies: - dependency-name: cached-path-relative dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0507a34a..a25aeaad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2506,8 +2506,9 @@ cache-base@^1.0.1: unset-value "^1.0.0" cached-path-relative@^1.0.0, cached-path-relative@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db" + version "1.1.0" + resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.1.0.tgz#865576dfef39c0d6a7defde794d078f5308e3ef3" + integrity sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA== cachedir@^2.3.0: version "2.3.0" From 25f7ec24560cfa04ba20f84f6ac2c4e9382ff304 Mon Sep 17 00:00:00 2001 From: Oleh Date: Tue, 26 Apr 2022 22:42:23 +0300 Subject: [PATCH 19/26] Fixed sanitizer on pasting HTML (#1686) * Fixed pasting sanitizer * Actualized code * Update CHANGELOG.md Co-authored-by: Oleh Kotyay Co-authored-by: Peter Savchenko --- docs/CHANGELOG.md | 1 + src/components/modules/paste.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bf56c3c2..5244e7ea 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,6 +8,7 @@ - `Fix` — `UI` — the Toolbar won't move on hover at mobile viewports. Resolves [#1972](https://github.com/codex-team/editor.js/issues/1972) - `Fix` — `OnChange` event invocation after block insertion. [#1997](https://github.com/codex-team/editor.js/issues/1997) - `Fix` — `ReadOnly` — the `readonly.isEnabled` API getter now works correctly after `readonly.toggle()` calling. Resolves [#1822](https://github.com/codex-team/editor.js/issues/1822) +- `Fix` — `Paste` — the inline HTML tags now will be preserved on pasting. [#1686](https://github.com/codex-team/editor.js/pull/1686) ### 2.23.2 diff --git a/src/components/modules/paste.ts b/src/components/modules/paste.ts index 178e3cc1..e901897b 100644 --- a/src/components/modules/paste.ts +++ b/src/components/modules/paste.ts @@ -662,7 +662,7 @@ export default class Paste extends Module { /** If there is no pattern substitute - insert string as it is */ if (BlockManager.currentBlock && BlockManager.currentBlock.currentInput) { - const currentToolSanitizeConfig = BlockManager.currentBlock.tool.sanitizeConfig; + const currentToolSanitizeConfig = BlockManager.currentBlock.tool.baseSanitizeConfig; document.execCommand( 'insertHTML', From 8bf4dcde9fe3d7a0e41d9d0ee500e3f9b4bf2584 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 22:51:33 +0300 Subject: [PATCH 20/26] Bump version up to 2.24.0 (#1955) * Bump version * Update package.json Co-authored-by: github-actions Co-authored-by: Peter Savchenko --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 22c05395..1e27c69b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.23.3", + "version": "2.24.0", "description": "Editor.js — Native JS, based on API and Open Source", "main": "dist/editor.js", "types": "./types/index.d.ts", From 96c0bcb573cabd8f5cd0fab2da2b616ed7afc0db Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 29 Apr 2022 00:26:47 +0800 Subject: [PATCH 21/26] fix(ui): toolbox items labels i18n (#2031) * Fix toolbox item label translation * Update version * Update changelog * Update docs/CHANGELOG.md Co-authored-by: George Berezhnoy * Add fallback for missing toolbox title * Update docs/CHANGELOG.md Co-authored-by: Peter Savchenko * Add test * Add testcase for missing toolbox title Co-authored-by: George Berezhnoy Co-authored-by: Peter Savchenko --- docs/CHANGELOG.md | 4 ++ package.json | 2 +- src/components/ui/toolbox.ts | 4 +- test/cypress/tests/i18n.spec.ts | 84 +++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 test/cypress/tests/i18n.spec.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5244e7ea..fbaf1b95 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.24.1 + +— `Fix` — The I18n of Tools` titles at the Toolbox now works correctly [#2030](https://github.com/codex-team/editor.js/issues/2030) + ### 2.24.0 - `New` — *UI* — The Toolbox became vertical 🥳 diff --git a/package.json b/package.json index 1e27c69b..13621f60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.24.0", + "version": "2.24.1", "description": "Editor.js — Native JS, based on API and Open Source", "main": "dist/editor.js", "types": "./types/index.d.ts", diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 89da4215..b781aa28 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -6,6 +6,8 @@ import ToolsCollection from '../tools/collection'; import { API } from '../../../types'; import EventsDispatcher from '../utils/events'; import Popover, { PopoverEvent } from '../utils/popover'; +import I18n from '../i18n'; +import { I18nInternalNS } from '../i18n/namespace-internal'; /** * @todo the first Tab on the Block — focus Plus Button, the second — focus Block Tunes Toggler, the third — focus next Block @@ -133,7 +135,7 @@ export default class Toolbox extends EventsDispatcher { items: this.toolsToBeDisplayed.map(tool => { return { icon: tool.toolbox.icon, - label: tool.toolbox.title, + label: I18n.t(I18nInternalNS.toolNames, tool.toolbox.title || _.capitalize(tool.name)), name: tool.name, onClick: (item): void => { this.toolButtonActivated(tool.name); diff --git a/test/cypress/tests/i18n.spec.ts b/test/cypress/tests/i18n.spec.ts new file mode 100644 index 00000000..2f31d48c --- /dev/null +++ b/test/cypress/tests/i18n.spec.ts @@ -0,0 +1,84 @@ +import Header from '@editorjs/header'; +import { ToolboxConfig } from '../../../types'; + +/** + * Tool class allowing to test case when capitalized tool name is used as translation key if toolbox title is missing + */ +class TestTool { + /** + * Returns toolbox config without title + */ + public static get toolbox(): ToolboxConfig { + return { + title: '', + icon: '', + }; + } +} + +describe('Editor i18n', () => { + context('Toolbox', () => { + it('should translate tool title in a toolbox', () => { + if (this && this.editorInstance) { + this.editorInstance.destroy(); + } + const toolNamesDictionary = { + Heading: 'Заголовок', + }; + + cy.createEditor({ + tools: { + header: Header, + }, + i18n: { + messages: { + toolNames: toolNamesDictionary, + }, + }, + }).as('editorInstance'); + + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('div.ce-toolbar__plus') + .click(); + + cy.get('[data-cy=editorjs]') + .get('div.ce-popover__item[data-item-name=header]') + .should('contain.text', toolNamesDictionary.Heading); + }); + + it('should use capitalized tool name as translation key if toolbox title is missing', () => { + if (this && this.editorInstance) { + this.editorInstance.destroy(); + } + const toolNamesDictionary = { + TestTool: 'ТестТул', + }; + + cy.createEditor({ + tools: { + testTool: TestTool, + }, + i18n: { + messages: { + toolNames: toolNamesDictionary, + }, + }, + }).as('editorInstance'); + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('div.ce-toolbar__plus') + .click(); + + cy.get('[data-cy=editorjs]') + .get('div.ce-popover__item[data-item-name=testTool]') + .should('contain.text', toolNamesDictionary.TestTool); + }); + }); +}); \ No newline at end of file From 8ae8823dcd6877d63241fcb94694a8a18744485d Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 1 May 2022 19:43:56 +0800 Subject: [PATCH 22/26] fix(ui): Prevent scrolling top when opening toolbox on mobile (#2034) * Fix scrolling issue on mobile when locking scroll on body * Simplify * Fix typo * Add popup example * Update changelog * update popup example page * Use hard scroll lock only for ios * Update version in changelog * Remove unused css class name Co-authored-by: Peter Savchenko --- docs/CHANGELOG.md | 6 + example/assets/demo.css | 50 +++++++ example/example-i18n.html | 2 +- example/example-popup.html | 131 +++++++++++++++++++ src/components/i18n/locales/en/messages.json | 2 +- src/components/modules/toolbar/index.ts | 2 +- src/components/utils.ts | 10 ++ src/components/utils/popover.ts | 12 +- src/components/utils/scroll-locker.ts | 64 +++++++++ src/styles/ui.css | 14 +- 10 files changed, 280 insertions(+), 13 deletions(-) create mode 100644 example/example-popup.html create mode 100644 src/components/utils/scroll-locker.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fbaf1b95..5b3af79e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 2.24.2 + +- `Fix` — Scrolling issue when opening toolbox on mobile fixed +- `Fix` — Typo in toolbox empty placeholder fixed +- `Improvement` — *Dev Example Page* - Add popup example page + ### 2.24.1 — `Fix` — The I18n of Tools` titles at the Toolbox now works correctly [#2030](https://github.com/codex-team/editor.js/issues/2030) diff --git a/example/assets/demo.css b/example/assets/demo.css index cffebcd9..f1141266 100644 --- a/example/assets/demo.css +++ b/example/assets/demo.css @@ -270,6 +270,56 @@ body { padding: 30px; } + +/** + * Styles for the popup example page + */ +.ce-example--popup { + height: 100vh; + display: flex; + flex-direction: column; +} + +.ce-example--popup .ce-example__content { + flex-grow: 2; +} + +.ce-example-popup__overlay { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: #00000085; +} + +.ce-example-popup__popup { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%,-50%); + width: 800px; + max-width: 100%; + max-height: 90vh; + background: white; + padding: 20px; + border-radius: 8px; + overflow: auto; + box-sizing: border-box; +} + +@media all and (max-width: 730px){ + .ce-example-popup__popup { + top: 10px; + left: 10px; + width: calc(100% - 20px); + height: calc(100% - 20px); + transform: none; + max-height: none; + + } +} + .show-block-boundaries .ce-block { box-shadow: inset 0 0 0 1px #eff2f5; } diff --git a/example/example-i18n.html b/example/example-i18n.html index 0496fbbc..c123bd7b 100644 --- a/example/example-i18n.html +++ b/example/example-i18n.html @@ -195,7 +195,7 @@ "toolbox": { "Add": "Добавить", "Filter": "Поиск", - "Noting found": "Ничего не найдено" + "Nothing found": "Ничего не найдено" } } }, diff --git a/example/example-popup.html b/example/example-popup.html new file mode 100644 index 00000000..2579d27b --- /dev/null +++ b/example/example-popup.html @@ -0,0 +1,131 @@ + + + + + + + Editor.js 🤩🧦🤨 example: Popup + + + + +
+ +
+
+ No core bundle file found. Run yarn build +
+
+

Base concepts

+

+ Editor.js is a block-style editor for rich media stories. It outputs clean data in JSON instead of heavy HTML markup. And more important thing is that Editor.js is designed to be API extendable and pluggable. +

+

+ So there are a few key features: +

+
    +
  • Clean data output
  • +
  • API pluggable
  • +
  • Open source
  • +
+

+ What does it mean block-styled +

+

+ In other editors, the workspace is provided by single contenteditable element in where you can create different HTML markup. All of us saw permanent bugs with moving text fragments or scaling images, while page parts are jumping and twitches. Or highlighting big parts of the text in the case when you just want to make few words to be a heading or bold. +

+

+ The Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor's Core. +

+

+ At the same time, most useful features as arrow-navigation, copy & paste, cross block selection, and others works almost as in the familiar editors. +

+

+ What is clean data +

+

+ But the more interesting thing is, as mentioned above, that Editor.js returns clean data instead of HTML-markup. Take a look at the example. +

+

+ If our entry consists of few paragraphs and a heading, in popular Medium editor after saving we will have something like this: +

+

+ As you can see, there are only data we need: a list of structural Blocks with their content description. +

+

+ You can use this data to easily render in Web, native mobile/desktop application, pass to Audio Readers, create templates for Facebook Instant Articles, AMP, RSS, create chat-bots, and many others. +

+

+ Also, the clean data can be useful for backend processing: sanitizing, validation, injecting an advertising or other stuff, extracting Headings, make covers for social networks from Image Blocks, and other. +

+

+ API pluggable? +

+

+ A key value of the Editor is the API. All main functional units of the editor — Blocks, Inline Formatting Tools, Block Tunes — are provided by external plugins that use Editor's API. +

+

+ We decide to extract all these Tools to separate scripts to make Editor's Core more abstract and make API more powerful. Any challenges and tasks you are facing can be implemented by your own plugins using the API. +

+

+ At the same time, API is created to be easy-to-understand and simple-to-use. +

+

+ Open Source, so? +

+

+ Editor.js is more than just an editor. It is a big open-source community of developers and contributors. Anyone can suggest an improvement or a bug fix. Anyone can create new cool API features and plugins. +

+

+ We will support each developer of Editor.js plugins: the best solutions will be collected to the Awesome List and promoted to the community. Together we can create a big suite of different Blocks, Inline Tools, Block Tunes that can hit a wide specter of tasks. +

+

+ Thanks for your interest. Hope you enjoy Editor.js. +

+
+
+
+
+
+
+
+
+
+ +
+
+ + + + + + + + + diff --git a/src/components/i18n/locales/en/messages.json b/src/components/i18n/locales/en/messages.json index 42a1520b..d44df52c 100644 --- a/src/components/i18n/locales/en/messages.json +++ b/src/components/i18n/locales/en/messages.json @@ -15,7 +15,7 @@ "toolbox": { "Add": "", "Filter": "", - "Noting found": "" + "Nothing found": "" } } }, diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index df48ad3d..9f5a1db2 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -409,7 +409,7 @@ export default class Toolbar extends Module { tools: this.Editor.Tools.blockTools, i18nLabels: { filter: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Filter'), - nothingFound: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Noting found'), + nothingFound: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Nothing found'), }, }); diff --git a/src/components/utils.ts b/src/components/utils.ts index ab824308..2f1650bf 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -769,3 +769,13 @@ export function cacheable 1)); \ No newline at end of file diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index 2afc2897..1f084324 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -4,6 +4,7 @@ import Flipper from '../flipper'; import SearchInput from './search-input'; import EventsDispatcher from './events'; import { isMobileScreen, keyCodes, cacheable } from '../utils'; +import ScrollLocker from './scroll-locker'; /** * Describe parameters for rendering the single item of Popover @@ -126,7 +127,6 @@ export default class Popover extends EventsDispatcher { noFoundMessageShown: string; popoverOverlay: string; popoverOverlayHidden: string; - documentScrollLocked: string; } { return { popover: 'ce-popover', @@ -142,10 +142,14 @@ export default class Popover extends EventsDispatcher { noFoundMessageShown: 'ce-popover__no-found--shown', popoverOverlay: 'ce-popover__overlay', popoverOverlayHidden: 'ce-popover__overlay--hidden', - documentScrollLocked: 'ce-scroll-locked', }; } + /** + * ScrollLocker instance + */ + private scrollLocker = new ScrollLocker() + /** * Creates the Popover * @@ -197,7 +201,7 @@ export default class Popover extends EventsDispatcher { } if (isMobileScreen()) { - document.documentElement.classList.add(Popover.CSS.documentScrollLocked); + this.scrollLocker.lock(); } } @@ -211,7 +215,7 @@ export default class Popover extends EventsDispatcher { this.flipper.deactivate(); if (isMobileScreen()) { - document.documentElement.classList.remove(Popover.CSS.documentScrollLocked); + this.scrollLocker.unlock(); } } diff --git a/src/components/utils/scroll-locker.ts b/src/components/utils/scroll-locker.ts new file mode 100644 index 00000000..394ddcec --- /dev/null +++ b/src/components/utils/scroll-locker.ts @@ -0,0 +1,64 @@ +import { isIosDevice } from '../utils'; + +/** + * Utility allowing to lock body scroll on demand + */ +export default class ScrollLocker { + /** + * Style classes + */ + private static CSS = { + scrollLocked: 'ce-scroll-locked', + scrollLockedHard: 'ce-scroll-locked--hard', + } + + /** + * Stores scroll position, used for hard scroll lock + */ + private scrollPosition: null|number + + /** + * Locks body element scroll + */ + public lock(): void { + if (isIosDevice) { + this.lockHard(); + } else { + document.body.classList.add(ScrollLocker.CSS.scrollLocked); + } + } + + /** + * Unlocks body element scroll + */ + public unlock(): void { + if (isIosDevice) { + this.unlockHard(); + } else { + document.body.classList.remove(ScrollLocker.CSS.scrollLocked); + } + } + + /** + * Locks scroll in a hard way (via setting fixed position to body element) + */ + private lockHard(): void { + this.scrollPosition = window.pageYOffset; + document.documentElement.style.setProperty( + '--window-scroll-offset', + `${this.scrollPosition}px` + ); + document.body.classList.add(ScrollLocker.CSS.scrollLockedHard); + } + + /** + * Unlocks hard scroll lock + */ + private unlockHard(): void { + document.body.classList.remove(ScrollLocker.CSS.scrollLockedHard); + if (this.scrollPosition !== null) { + window.scrollTo(0, this.scrollPosition); + } + this.scrollPosition = null; + } +} diff --git a/src/styles/ui.css b/src/styles/ui.css index da65fc37..e594e071 100644 --- a/src/styles/ui.css +++ b/src/styles/ui.css @@ -128,11 +128,13 @@ } } -.ce-scroll-locked, .ce-scroll-locked > body { - height: 100vh; +.ce-scroll-locked { overflow: hidden; - /** - * Mobile Safari fix - */ - position: relative; +} + +.ce-scroll-locked--hard { + overflow: hidden; + top: calc(-1 * var(--window-scroll-offset)); + position: fixed; + width: 100%; } \ No newline at end of file From 6a15cc55c068899588bdc7d4754ae5519cb71e50 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Sun, 1 May 2022 15:09:16 +0300 Subject: [PATCH 23/26] fix(#2036): scrolling issue with block hovering (#2042) * fix scrolling issue caused by the popover.hide() * Update popover.ts * Update CHANGELOG.md * upd codeowners * naming of isShown improved --- CODEOWNERS | 3 ++- docs/CHANGELOG.md | 2 ++ src/components/utils/popover.ts | 24 +++++++++++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index aaa5e0be..d2625662 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1,2 @@ -* @neSpecc @gohabereg @khaydarov +* @neSpecc @gohabereg @TatianaFomina @ilyamore88 + diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5b3af79e..fd91f853 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,7 +4,9 @@ - `Fix` — Scrolling issue when opening toolbox on mobile fixed - `Fix` — Typo in toolbox empty placeholder fixed +- `Fix` — The issue with scroll jumping on block hovering have fixed [2036](https://github.com/codex-team/editor.js/issues/2036) - `Improvement` — *Dev Example Page* - Add popup example page +- `Improvement` — *UI* - The Toolbox will restore the internal scroll on every opening ### 2.24.1 diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index 1f084324..0a2c30c7 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -58,6 +58,11 @@ export default class Popover extends EventsDispatcher { */ private readonly items: PopoverItem[]; + /** + * Stores the visibility state. + */ + private isShown = false; + /** * Created nodes */ @@ -190,6 +195,12 @@ export default class Popover extends EventsDispatcher { * Shows the Popover */ public show(): void { + /** + * Clear search and items scrolling + */ + this.search.clear(); + this.nodes.items.scrollTop = 0; + this.nodes.popover.classList.add(Popover.CSS.popoverOpened); this.nodes.overlay.classList.remove(Popover.CSS.popoverOverlayHidden); this.flipper.activate(); @@ -203,13 +214,22 @@ export default class Popover extends EventsDispatcher { if (isMobileScreen()) { this.scrollLocker.lock(); } + + this.isShown = true; } /** * Hides the Popover */ public hide(): void { - this.search.clear(); + /** + * If it's already hidden, do nothing + * to prevent extra DOM operations + */ + if (!this.isShown) { + return; + } + this.nodes.popover.classList.remove(Popover.CSS.popoverOpened); this.nodes.overlay.classList.add(Popover.CSS.popoverOverlayHidden); this.flipper.deactivate(); @@ -217,6 +237,8 @@ export default class Popover extends EventsDispatcher { if (isMobileScreen()) { this.scrollLocker.unlock(); } + + this.isShown = false; } /** From 3cbe128af9e29c420b3c64a4c45bb95c1196a6ec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 1 May 2022 15:32:08 +0300 Subject: [PATCH 24/26] Bump version up to 2.24.2 (#2041) * Bump version * Change version to the 2.24.2 Co-authored-by: github-actions Co-authored-by: Peter Savchenko --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 13621f60..f6dc02f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.24.1", + "version": "2.24.2", "description": "Editor.js — Native JS, based on API and Open Source", "main": "dist/editor.js", "types": "./types/index.d.ts", From 07ea1ef035cea7becbbd36366cfe215cf8cd2ecc Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Sun, 1 May 2022 15:52:29 +0300 Subject: [PATCH 25/26] chore: update the roadmap at the readme (#2043) * Update text-variant-tune * Update README.md * Update README.md --- README.md | 8 ++++---- example/tools/text-variant-tune | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 86a2f130..320365fa 100644 --- a/README.md +++ b/README.md @@ -27,18 +27,18 @@ - Unified Toolbox - [x] Block Tunes moved left [#1815](https://github.com/codex-team/editor.js/pull/1815) - - [ ] Toolbox become vertical `In progress` - - [ ] Ability to display several Toolbox buttons by the single Tool + - [x] Toolbox become vertical [#2014](https://github.com/codex-team/editor.js/pull/2014) + - [ ] Ability to display several Toolbox buttons by the single Tool `In progress` - [ ] Conversion Toolbar uses Unified Toolbox - [ ] Block Tunes become vertical - [ ] Conversion Toolbar added to the Block Tunes - Ecosystem improvements - image + image -## +## ## If you like a project 💗💗💗 diff --git a/example/tools/text-variant-tune b/example/tools/text-variant-tune index 8b1b032b..02538b1d 160000 --- a/example/tools/text-variant-tune +++ b/example/tools/text-variant-tune @@ -1 +1 @@ -Subproject commit 8b1b032b7974c448224608fb0b4838a243cc48a5 +Subproject commit 02538b1daea722c854cc61b6fbca01b746c21717 From f2b19cc7660248f95316e6b9e37b257e25268371 Mon Sep 17 00:00:00 2001 From: "Umang G. Patel" <23169768+robonetphy@users.noreply.github.com> Date: Sun, 1 May 2022 18:57:40 +0530 Subject: [PATCH 26/26] update the script to support windows (#2033) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f6dc02f0..2319bcf5 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "svg": "svg-sprite-generate -d src/assets/ -o dist/sprite.svg", "ci:pull_paragraph": "git submodule update --init ./src/tools/paragraph", "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:yarn": "git submodule foreach yarn", "_tools:build": "git submodule foreach yarn build",