diff --git a/src/components/block/api.ts b/src/components/block/api.ts index d760ab63e..35ce7c8c0 100644 --- a/src/components/block/api.ts +++ b/src/components/block/api.ts @@ -1,7 +1,7 @@ import Block from './index'; -import { BlockToolData, ToolConfig } from '../../../types/tools'; -import { SavedData } from '../../../types/data-formats'; -import { BlockAPI as BlockAPIInterface } from '../../../types/api'; +import {BlockToolData, ToolConfig} from '../../../types/tools'; +import {SavedData} from '../../../types/data-formats'; +import {BlockAPI as BlockAPIInterface} from '../../../types/api'; /** * Constructs new BlockAPI object @@ -100,10 +100,14 @@ function BlockAPI( * * @returns {Promise} */ - save(): Promise { + save(): Promise { return block.save(); }, + markdown(): Promise { + return block.markdown(); + }, + /** * Validate Block data * diff --git a/src/components/block/index.ts b/src/components/block/index.ts index b47fe7811..a6de57953 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -620,6 +620,13 @@ export default class Block extends EventsDispatcher { return isValid; } + public markdown(): Promise { + if (this.toolInstance.markdown instanceof Function) { + return this.toolInstance.markdown(); + } + return null; + } + /** * Returns data to render in tunes menu. * Splits block tunes settings into 2 groups: popover items and custom html. diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts index ee2d00c32..fd9eab57f 100644 --- a/src/components/modules/blockEvents.ts +++ b/src/components/modules/blockEvents.ts @@ -6,7 +6,7 @@ import * as _ from '../utils'; import SelectionUtils from '../selection'; import Flipper from '../flipper'; import type Block from '../block'; -import { areBlocksMergeable } from '../utils/blocks'; +import {areBlocksMergeable} from '../utils/blocks'; /** * @@ -38,7 +38,9 @@ export default class BlockEvents extends Module { case _.keyCodes.ENTER: this.enter(event); break; - + case _.keyCodes.TAB: + this.enter(event); + break; case _.keyCodes.DOWN: case _.keyCodes.RIGHT: this.arrowRightAndDown(event); @@ -49,7 +51,7 @@ export default class BlockEvents extends Module { this.arrowLeftAndUp(event); break; - case _.keyCodes.TAB: + case (event.ctrlKey || event.metaKey) && _.keyCodes.SLASH: this.tabPressed(event); break; } @@ -124,7 +126,7 @@ export default class BlockEvents extends Module { */ this.Editor.BlockSelection.clearSelection(event); - const { BlockManager, InlineToolbar, ConversionToolbar } = this.Editor; + const {BlockManager, InlineToolbar, ConversionToolbar} = this.Editor; const currentBlock = BlockManager.currentBlock; if (!currentBlock) { @@ -176,7 +178,7 @@ export default class BlockEvents extends Module { * @param {ClipboardEvent} event - clipboard event */ public handleCommandC(event: ClipboardEvent): void { - const { BlockSelection } = this.Editor; + const {BlockSelection} = this.Editor; if (!BlockSelection.anyBlockSelected) { return; @@ -192,7 +194,7 @@ export default class BlockEvents extends Module { * @param {ClipboardEvent} event - clipboard event */ public handleCommandX(event: ClipboardEvent): void { - const { BlockSelection, BlockManager, Caret } = this.Editor; + const {BlockSelection, BlockManager, Caret} = this.Editor; if (!BlockSelection.anyBlockSelected) { return; @@ -219,7 +221,7 @@ export default class BlockEvents extends Module { * @param {KeyboardEvent} event - keydown */ private enter(event: KeyboardEvent): void { - const { BlockManager, UI } = this.Editor; + const {BlockManager, UI} = this.Editor; const currentBlock = BlockManager.currentBlock; /** @@ -245,18 +247,28 @@ export default class BlockEvents extends Module { return; } + /** + * 处理普通文本首行tab + * @author asan + */ + if (event.keyCode === _.keyCodes.TAB && currentBlock.name === 'paragraph') { + event.preventDefault(); + return; + } + let newCurrent = this.Editor.BlockManager.currentBlock; + /** * If enter has been pressed at the start of the text, just insert paragraph Block above */ if (this.Editor.Caret.isAtStart && !this.Editor.BlockManager.currentBlock.hasMedia) { this.Editor.BlockManager.insertDefaultBlockAtIndex(this.Editor.BlockManager.currentBlockIndex); - /** - * If caret is at very end of the block, just append the new block without splitting - * to prevent unnecessary dom mutation observing - */ + /** + * If caret is at very end of the block, just append the new block without splitting + * to prevent unnecessary dom mutation observing + */ } else if (this.Editor.Caret.isAtEnd) { newCurrent = this.Editor.BlockManager.insertDefaultBlockAtIndex(this.Editor.BlockManager.currentBlockIndex + 1); } else { @@ -283,8 +295,16 @@ export default class BlockEvents extends Module { * @param {KeyboardEvent} event - keydown */ private backspace(event: KeyboardEvent): void { - const { BlockManager, Caret } = this.Editor; - const { currentBlock, previousBlock } = BlockManager; + const {BlockManager, Caret} = this.Editor; + const {currentBlock, previousBlock} = BlockManager; + + + /** + * 文本在非首行首列时,不处理backspace事件 + */ + if (currentBlock.name === 'paragraph' && !Caret.isAtStartForText) { + return; + } /** * If some fragment is selected, leave native behaviour @@ -366,9 +386,12 @@ export default class BlockEvents extends Module { * @param {KeyboardEvent} event - keydown */ private delete(event: KeyboardEvent): void { - const { BlockManager, Caret } = this.Editor; - const { currentBlock, nextBlock } = BlockManager; + const {BlockManager, Caret} = this.Editor; + const {currentBlock, nextBlock} = BlockManager; + if (currentBlock.name === 'paragraph' && !Caret.isAtStartForText) { + return; + } /** * If some fragment is selected, leave native behaviour */ @@ -447,7 +470,7 @@ export default class BlockEvents extends Module { * @param blockToMerge - what Block we want to merge */ private mergeBlocks(targetBlock: Block, blockToMerge: Block): void { - const { BlockManager, Caret, Toolbar } = this.Editor; + const {BlockManager, Caret, Toolbar} = this.Editor; Caret.createShadow(targetBlock.pluginsContent); @@ -511,7 +534,7 @@ export default class BlockEvents extends Module { if (this.Editor.BlockManager.currentBlock) { this.Editor.BlockManager.currentBlock.updateCurrentInput(); } - // eslint-disable-next-line @typescript-eslint/no-magic-numbers + // eslint-disable-next-line @typescript-eslint/no-magic-numbers }, 20)(); } @@ -527,6 +550,13 @@ export default class BlockEvents extends Module { * @param {KeyboardEvent} event - keyboard event */ private arrowLeftAndUp(event: KeyboardEvent): void { + const {BlockManager, Caret} = this.Editor; + const {currentBlock} = BlockManager; + + if (currentBlock.name === 'paragraph' && !Caret.isAtStartForText) { + return; + } + /** * Arrows might be handled on toolbars by flipper * Check for Flipper.usedKeys to allow navigate by UP and disallow by LEFT @@ -570,7 +600,7 @@ export default class BlockEvents extends Module { if (this.Editor.BlockManager.currentBlock) { this.Editor.BlockManager.currentBlock.updateCurrentInput(); } - // eslint-disable-next-line @typescript-eslint/no-magic-numbers + // eslint-disable-next-line @typescript-eslint/no-magic-numbers }, 20)(); } @@ -587,10 +617,10 @@ export default class BlockEvents extends Module { */ private needToolbarClosing(event: KeyboardEvent): boolean { const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbar.toolbox.opened), - blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened), - inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened), - conversionToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.ConversionToolbar.opened), - flippingToolbarItems = event.keyCode === _.keyCodes.TAB; + blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened), + inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened), + conversionToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.ConversionToolbar.opened), + flippingToolbarItems = event.keyCode === _.keyCodes.TAB; /** * Do not close Toolbar in cases: diff --git a/src/components/modules/caret.ts b/src/components/modules/caret.ts index de64dc683..c5b376557 100644 --- a/src/components/modules/caret.ts +++ b/src/components/modules/caret.ts @@ -132,6 +132,96 @@ export default class Caret extends Module { return firstNode === null || (focusNode === firstNode && focusOffset <= firstLetterPosition); } + public get isAtStartForText(): boolean { + const selection = Selection.get(); + if (selection['focusOffset'] !== 0) { + return false; + } + const firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput); + let focusNode = selection.focusNode; + + /** In case lastNode is native input */ + if ($.isNativeInput(firstNode)) { + return (firstNode as HTMLInputElement).selectionEnd === 0; + } + + /** Case when selection have been cleared programmatically, for example after CBS */ + if (!selection.anchorNode) { + return false; + } + + /** + * Workaround case when caret in the text like " |Hello!" + * selection.anchorOffset is 1, but real caret visible position is 0 + * + * @type {number} + */ + + let firstLetterPosition = focusNode.textContent.search(/\S/); + + if (firstLetterPosition === -1) { // empty text + firstLetterPosition = 0; + } + + /** + * If caret was set by external code, it might be set to text node wrapper. + *
|hello
<---- Selection references to
instead of text node + * + * In this case, anchor node has ELEMENT_NODE node type. + * Anchor offset shows amount of children between start of the element and caret position. + * + * So we use child with focusOffset index as new anchorNode. + */ + let focusOffset = selection.focusOffset; + + if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length) { + if (focusNode.childNodes[focusOffset]) { + focusNode = focusNode.childNodes[focusOffset]; + focusOffset = 0; + } else { + focusNode = focusNode.childNodes[focusOffset - 1]; + focusOffset = focusNode.textContent.length; + } + } + + /** + * In case of + *
+ *

<-- first (and deepest) node is + * |adaddad <-- focus node + *
+ */ + if ($.isLineBreakTag(firstNode as HTMLElement) || $.isEmpty(firstNode)) { + const leftSiblings = this.getHigherLevelSiblings(focusNode as HTMLElement, 'left'); + const nothingAtLeft = leftSiblings.every((node) => { + /** + * Workaround case when block starts with several
's (created by SHIFT+ENTER) + * + * @see https://github.com/codex-team/editor.js/issues/726 + * We need to allow to delete such line breaks, so in this case caret IS NOT AT START + */ + const regularLineBreak = $.isLineBreakTag(node); + /** + * Workaround SHIFT+ENTER in Safari, that creates

instead of
+ */ + const lineBreakInSafari = node.children.length === 1 && $.isLineBreakTag(node.children[0] as HTMLElement); + const isLineBreak = regularLineBreak || lineBreakInSafari; + + return $.isEmpty(node) && !isLineBreak; + }); + + if (nothingAtLeft && focusOffset === firstLetterPosition) { + return true; + } + } + + /** + * We use <= comparison for case: + * "| Hello" <--- selection.anchorOffset is 0, but firstLetterPosition is 1 + */ + return firstNode === null || (focusNode === firstNode && focusOffset <= firstLetterPosition); + } + /** * Get's deepest last node and checks if offset is last node text length * diff --git a/src/components/modules/paste.ts b/src/components/modules/paste.ts index 2324a5d66..24afe2118 100644 --- a/src/components/modules/paste.ts +++ b/src/components/modules/paste.ts @@ -9,8 +9,8 @@ import { SanitizerRule } from '../../../types'; import Block from '../block'; -import { SavedData } from '../../../types/data-formats'; -import { clean, sanitizeBlocks } from '../utils/sanitizer'; +import {SavedData} from '../../../types/data-formats'; +import {clean, sanitizeBlocks} from '../utils/sanitizer'; import BlockTool from '../tools/block'; /** @@ -165,13 +165,13 @@ export default class Paste extends Module { * @param {boolean} isDragNDrop - true if data transfer comes from drag'n'drop events */ public async processDataTransfer(dataTransfer: DataTransfer, isDragNDrop = false): Promise { - const { Tools } = this.Editor; + const {Tools} = this.Editor; const types = dataTransfer.types; /** * In Microsoft Edge types is DOMStringList. So 'contains' is used to check if 'Files' type included */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any const includesFiles = types.includes ? types.includes('Files') : (types as any).contains('Files'); if (includesFiles && !_.isEmpty(this.toolsFiles)) { @@ -192,7 +192,8 @@ export default class Paste extends Module { this.insertEditorJSData(JSON.parse(editorJSData)); return; - } catch (e) { } // Do nothing and continue execution as usual if error appears + } catch (e) { + } // Do nothing and continue execution as usual if error appears } /** @@ -213,7 +214,7 @@ export default class Paste extends Module { return result; }, {}); - const customConfig = Object.assign({}, toolsTags, Tools.getAllInlineToolsSanitizeConfig(), { br: {} }); + const customConfig = Object.assign({}, toolsTags, Tools.getAllInlineToolsSanitizeConfig(), {br: {}}); const cleanData = clean(htmlData, customConfig); /** If there is no HTML or HTML string is equal to plain one, process it as plain text */ @@ -231,7 +232,7 @@ export default class Paste extends Module { * @param {boolean} isHTML - if passed string is HTML, this parameter should be true */ public async processText(data: string, isHTML = false): Promise { - const { Caret, BlockManager } = this.Editor; + const {Caret, BlockManager} = this.Editor; const dataToInsert = isHTML ? this.processHTML(data) : this.processPlain(data); if (!dataToInsert.length) { @@ -327,7 +328,7 @@ export default class Paste extends Module { * If string, then it is a tag name. */ if (_.isString(tagOrSanitizeConfig)) { - return [ tagOrSanitizeConfig ]; + return [tagOrSanitizeConfig]; } /** * If object, then its keys are tags. @@ -395,8 +396,8 @@ export default class Paste extends Module { return; } - const { files = {} } = tool.pasteConfig; - let { extensions, mimeTypes } = files; + const {files = {}} = tool.pasteConfig; + let {extensions, mimeTypes} = files; if (!extensions && !mimeTypes) { return; @@ -477,16 +478,17 @@ export default class Paste extends Module { * @param {ClipboardEvent} event - clipboard event */ private handlePasteEvent = async (event: ClipboardEvent): Promise => { - const { BlockManager, Toolbar } = this.Editor; + const {BlockManager, Toolbar} = this.Editor; /** * When someone pasting into a block, its more stable to set current block by event target, instead of relying on current block set before */ const currentBlock = BlockManager.setCurrentBlockByChildNode(event.target as HTMLElement); + /** If target is native input or is not Block, use browser behaviour */ if ( - !currentBlock || (this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files')) + !currentBlock || currentBlock.name === 'threeWireMeter' || (this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files')) ) { return; } @@ -511,7 +513,7 @@ export default class Paste extends Module { * @param {FileList} items - pasted or dropped items */ private async processFiles(items: FileList): Promise { - const { BlockManager } = this.Editor; + const {BlockManager} = this.Editor; let dataToInsert: { type: string; event: PasteEvent }[]; @@ -543,7 +545,7 @@ export default class Paste extends Module { const foundConfig = Object .entries(this.toolsFiles) // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars - .find(([toolName, { mimeTypes, extensions } ]) => { + .find(([toolName, {mimeTypes, extensions}]) => { const [fileType, fileSubtype] = file.type.split('/'); const foundExt = extensions.find((ext) => ext.toLowerCase() === extension.toLowerCase()); @@ -560,7 +562,7 @@ export default class Paste extends Module { return; } - const [ tool ] = foundConfig; + const [tool] = foundConfig; const pasteEvent = this.composePasteEvent('file', { file, }); @@ -578,7 +580,7 @@ export default class Paste extends Module { * @returns {PasteData[]} */ private processHTML(innerHTML: string): PasteData[] { - const { Tools } = this.Editor; + const {Tools} = this.Editor; /** * @todo Research, do we really need to always wrap innerHTML to a div: @@ -622,7 +624,7 @@ export default class Paste extends Module { /** * Returns empty array if there is no paste config */ - const { tags: tagsOrSanitizeConfigs } = tool.pasteConfig || { tags: [] }; + const {tags: tagsOrSanitizeConfigs} = tool.pasteConfig || {tags: []}; /** * Reduce the tags or sanitize configs to a single array of sanitize config. @@ -708,7 +710,7 @@ export default class Paste extends Module { * @returns {PasteData[]} */ private processPlain(plain: string): PasteData[] { - const { defaultBlock } = this.config as { defaultBlock: string }; + const {defaultBlock} = this.config as { defaultBlock: string }; if (!plain) { return []; @@ -743,8 +745,8 @@ export default class Paste extends Module { * @param {PasteData} dataToInsert - data of Block to insert */ private async processSingleBlock(dataToInsert: PasteData): Promise { - const { Caret, BlockManager } = this.Editor; - const { currentBlock } = BlockManager; + const {Caret, BlockManager} = this.Editor; + const {currentBlock} = BlockManager; /** * If pasted tool isn`t equal current Block or if pasted content contains block elements, insert it as new Block @@ -771,8 +773,8 @@ export default class Paste extends Module { * @param {PasteData} dataToInsert - data of Block to insert */ private async processInlinePaste(dataToInsert: PasteData): Promise { - const { BlockManager, Caret } = this.Editor; - const { content } = dataToInsert; + const {BlockManager, Caret} = this.Editor; + const {content} = dataToInsert; const currentBlockIsDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault; @@ -846,8 +848,8 @@ export default class Paste extends Module { * @returns {void} */ private insertBlock(data: PasteData, canReplaceCurrentBlock = false): void { - const { BlockManager, Caret } = this.Editor; - const { currentBlock } = BlockManager; + const {BlockManager, Caret} = this.Editor; + const {currentBlock} = BlockManager; let block: Block; if (canReplaceCurrentBlock && currentBlock && currentBlock.isEmpty) { @@ -869,12 +871,12 @@ export default class Paste extends Module { * @returns {void} */ private insertEditorJSData(blocks: Pick[]): void { - const { BlockManager, Caret, Tools } = this.Editor; + const {BlockManager, Caret, Tools} = this.Editor; const sanitizedBlocks = sanitizeBlocks(blocks, (name) => Tools.blockTools.get(name).sanitizeConfig ); - sanitizedBlocks.forEach(({ tool, data }, i) => { + sanitizedBlocks.forEach(({tool, data}, i) => { let needToReplaceCurrentBlock = false; if (i === 0) { @@ -905,7 +907,7 @@ export default class Paste extends Module { const element = node as HTMLElement; - const { tool } = this.toolsTags[element.tagName] || {}; + const {tool} = this.toolsTags[element.tagName] || {}; const toolTags = this.tagsByTool[tool?.name] || []; const isSubstitutable = tags.includes(element.tagName); @@ -913,11 +915,11 @@ export default class Paste extends Module { const containsAnotherToolTags = Array .from(element.children) .some( - ({ tagName }) => tags.includes(tagName) && !toolTags.includes(tagName) + ({tagName}) => tags.includes(tagName) && !toolTags.includes(tagName) ); const containsBlockElements = Array.from(element.children).some( - ({ tagName }) => $.blockElements.includes(tagName.toLowerCase()) + ({tagName}) => $.blockElements.includes(tagName.toLowerCase()) ); /** Append inline elements to previous fragment */ diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index 575a87865..d20c0ecd0 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -2,13 +2,13 @@ import Module from '../../__module'; import $ from '../../dom'; import * as _ from '../../utils'; import I18n from '../../i18n'; -import { I18nInternalNS } from '../../i18n/namespace-internal'; +import {I18nInternalNS} from '../../i18n/namespace-internal'; import * as tooltip from '../../utils/tooltip'; -import { ModuleConfig } from '../../../types-internal/module-config'; +import {ModuleConfig} from '../../../types-internal/module-config'; import Block from '../../block'; -import Toolbox, { ToolboxEvent } from '../../ui/toolbox'; -import { IconMenu, IconPlus } from '@codexteam/icons'; -import { BlockHovered } from '../../events/BlockHovered'; +import Toolbox, {ToolboxEvent} from '../../ui/toolbox'; +import {IconMenu, IconPlus} from '@codexteam/icons'; +import {BlockHovered} from '../../events/BlockHovered'; /** * @todo Tab on non-empty block should open Block Settings of the hoveredBlock (not where caret is set) @@ -39,6 +39,7 @@ interface ToolbarNodes { plusButton: HTMLElement; settingsToggler: HTMLElement; } + /** * * «Toolbar» is the node that moves up/down over current block @@ -108,7 +109,7 @@ export default class Toolbar extends Module { * @param moduleConfiguration.config - Editor's config * @param moduleConfiguration.eventsDispatcher - Editor's event dispatcher */ - constructor({ config, eventsDispatcher }: ModuleConfig) { + constructor({config, eventsDispatcher}: ModuleConfig) { super({ config, eventsDispatcher, @@ -155,7 +156,7 @@ export default class Toolbar extends Module { open: () => void; toggle: () => void; hasFocus: () => boolean | undefined; - } { + } { return { opened: this.toolboxInstance?.opened, close: () => { @@ -165,7 +166,7 @@ export default class Toolbar extends Module { /** * If Toolbox is not initialized yet, do nothing */ - if (this.toolboxInstance === null) { + if (this.toolboxInstance === null) { _.log('toolbox.open() called before initialization is finished', 'warn'); return; @@ -182,7 +183,7 @@ export default class Toolbar extends Module { /** * If Toolbox is not initialized yet, do nothing */ - if (this.toolboxInstance === null) { + if (this.toolboxInstance === null) { _.log('toolbox.toggle() called before initialization is finished', 'warn'); return; @@ -228,7 +229,7 @@ export default class Toolbar extends Module { window.requestIdleCallback(() => { this.drawUI(); this.enableModuleBindings(); - }, { timeout: 2000 }); + }, {timeout: 2000}); } else { this.destroy(); this.Editor.BlockSettings.destroy(); @@ -245,7 +246,7 @@ export default class Toolbar extends Module { /** * Some UI elements creates inside requestIdleCallback, so the can be not ready yet */ - if (this.toolboxInstance === null) { + if (this.toolboxInstance === null) { _.log('Can\'t open Toolbar since Editor initialization is not finished yet', 'warn'); return; @@ -272,7 +273,7 @@ export default class Toolbar extends Module { this.hoveredBlock = block; const targetBlockHolder = block.holder; - const { isMobile } = this.Editor.UI; + const {isMobile} = this.Editor.UI; const renderedContent = block.pluginsContent; const renderedContentStyle = window.getComputedStyle(renderedContent); const blockRenderedElementPaddingTop = parseInt(renderedContentStyle.paddingTop, 10); @@ -390,9 +391,9 @@ export default class Toolbar extends Module { */ const tooltipContent = $.make('div'); - tooltipContent.appendChild(document.createTextNode(I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Add'))); + // tooltipContent.appendChild(document.createTextNode(I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Add'))); tooltipContent.appendChild($.make('div', this.CSS.plusButtonShortcut, { - textContent: '⇥ Tab', + textContent: _.beautifyShortcut('ctrl+/'), })); tooltip.onHover(this.nodes.plusButton, tooltipContent, { @@ -455,8 +456,8 @@ export default class Toolbar extends Module { this.Editor.UI.nodes.wrapper.classList.remove(this.CSS.openedToolboxHolderModifier); }); - this.toolboxInstance.on(ToolboxEvent.BlockAdded, ({ block }) => { - const { BlockManager, Caret } = this.Editor; + this.toolboxInstance.on(ToolboxEvent.BlockAdded, ({block}) => { + const {BlockManager, Caret} = this.Editor; const newBlock = BlockManager.getBlockById(block.id); /** diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index 5bcc70512..bc651b2d9 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -11,10 +11,11 @@ import * as _ from '../utils'; import Selection from '../selection'; import Block from '../block'; import Flipper from '../flipper'; -import { mobileScreenBreakpoint } from '../utils'; +import {mobileScreenBreakpoint} from '../utils'; import styles from '../../styles/main.css?inline'; -import { BlockHovered } from '../events/BlockHovered'; +import {BlockHovered} from '../events/BlockHovered'; + /** * HTML Elements used for UI */ @@ -49,7 +50,7 @@ export default class UI extends Module { public get CSS(): { editorWrapper: string; editorWrapperNarrow: string; editorZone: string; editorZoneHidden: string; editorEmpty: string; editorRtlFix: string; - } { + } { return { editorWrapper: 'codex-editor', editorWrapperNarrow: 'codex-editor--narrow', @@ -110,7 +111,7 @@ export default class UI extends Module { */ private resizeDebouncer: () => void = _.debounce(() => { this.windowResize(); - // eslint-disable-next-line @typescript-eslint/no-magic-numbers + // eslint-disable-next-line @typescript-eslint/no-magic-numbers }, 200); /** @@ -165,7 +166,7 @@ export default class UI extends Module { * Check if Editor is empty and set CSS class to wrapper */ public checkEmptiness(): void { - const { BlockManager } = this.Editor; + const {BlockManager} = this.Editor; this.nodes.wrapper.classList.toggle(this.CSS.editorEmpty, BlockManager.isEditorEmpty); } @@ -177,7 +178,7 @@ export default class UI extends Module { * @returns {boolean} */ public get someToolbarOpened(): boolean { - const { Toolbar, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor; + const {Toolbar, BlockSettings, InlineToolbar, ConversionToolbar} = this.Editor; return BlockSettings.opened || InlineToolbar.opened || ConversionToolbar.opened || Toolbar.toolbox.opened; } @@ -216,7 +217,7 @@ export default class UI extends Module { * Close all Editor's toolbars */ public closeAllToolbars(): void { - const { Toolbar, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor; + const {Toolbar, BlockSettings, InlineToolbar, ConversionToolbar} = this.Editor; BlockSettings.close(); InlineToolbar.close(); @@ -247,7 +248,7 @@ export default class UI extends Module { */ this.nodes.wrapper = $.make('div', [ this.CSS.editorWrapper, - ...(this.isRtl ? [ this.CSS.editorRtlFix ] : []), + ...(this.isRtl ? [this.CSS.editorRtlFix] : []), ]); this.nodes.redactor = $.make('div', this.CSS.editorZone); @@ -276,7 +277,7 @@ export default class UI extends Module { /** * Load CSS */ - // eslint-disable-next-line @typescript-eslint/no-var-requires + // eslint-disable-next-line @typescript-eslint/no-var-requires const styleTagId = 'editor-js-styles'; /** @@ -381,7 +382,7 @@ export default class UI extends Module { this.eventsDispatcher.emit(BlockHovered, { block: this.Editor.BlockManager.getBlockByChildNode(hoveredBlock), }); - // eslint-disable-next-line @typescript-eslint/no-magic-numbers + // eslint-disable-next-line @typescript-eslint/no-magic-numbers }, 20), { passive: true, }); @@ -441,7 +442,7 @@ export default class UI extends Module { * @param {KeyboardEvent} event - keyboard event */ private defaultBehaviour(event: KeyboardEvent): void { - const { currentBlock } = this.Editor.BlockManager; + const {currentBlock} = this.Editor.BlockManager; const keyDownOnEditor = (event.target as HTMLElement).closest(`.${this.CSS.editorWrapper}`); const isMetaKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; @@ -476,7 +477,7 @@ export default class UI extends Module { * @param {KeyboardEvent} event - keyboard event */ private backspacePressed(event: KeyboardEvent): void { - const { BlockManager, BlockSelection, Caret } = this.Editor; + const {BlockManager, BlockSelection, Caret} = this.Editor; /** * If any block selected and selection doesn't exists on the page (that means no other editable element is focused), @@ -535,7 +536,7 @@ export default class UI extends Module { * @param {KeyboardEvent} event - keyboard event */ private enterPressed(event: KeyboardEvent): void { - const { BlockManager, BlockSelection } = this.Editor; + const {BlockManager, BlockSelection} = this.Editor; const hasPointerToBlock = BlockManager.currentBlockIndex >= 0; /** @@ -660,9 +661,22 @@ export default class UI extends Module { * If click was fired on Editor`s wrapper, try to get clicked node by elementFromPoint method */ if (clickedNode === this.nodes.redactor) { - const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX; - const clientY = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY; - + // let clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX; + // let clientY = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY; + let clientX: number, clientY: number; + + if (event instanceof MouseEvent) { + clientX = event.clientX; + clientY = event.clientY; + } else if (event.touches) { + if (event.touches.length > 1) { + clientX = event.touches[0].clientX; + clientY = event.touches[0].clientY; + } + } else if (event instanceof MouseEvent) { + clientX = event.clientX; + clientY = event.clientY; + } clickedNode = document.elementFromPoint(clientX, clientY) as HTMLElement; } @@ -740,7 +754,7 @@ export default class UI extends Module { const lastBlockBottomCoord = $.offset(lastBlock.holder).bottom; const clickedCoord = event.pageY; - const { BlockSelection } = this.Editor; + const {BlockSelection} = this.Editor; const isClickedBottom = event.target instanceof Element && event.target.isEqualNode(this.nodes.redactor) && /** @@ -757,7 +771,7 @@ export default class UI extends Module { event.stopImmediatePropagation(); event.stopPropagation(); - const { BlockManager, Caret, Toolbar } = this.Editor; + const {BlockManager, Caret, Toolbar} = this.Editor; /** * Insert a default-block at the bottom if: @@ -782,7 +796,7 @@ export default class UI extends Module { * Uses for showing the Inline Toolbar */ private selectionChanged(): void { - const { CrossBlockSelection, BlockSelection } = this.Editor; + const {CrossBlockSelection, BlockSelection} = this.Editor; const focusedElement = Selection.anchorElement; if (CrossBlockSelection.isCrossBlockSelectionStarted) { diff --git a/src/components/selection.ts b/src/components/selection.ts index fe5f961a8..6f1c7d2b8 100644 --- a/src/components/selection.ts +++ b/src/components/selection.ts @@ -368,7 +368,7 @@ export default class SelectionUtils { } /** - * Adds fake cursor to the current range + * adds fake cursor to the current range */ public static addFakeCursor(): void { const range = SelectionUtils.range; diff --git a/src/components/utils.ts b/src/components/utils.ts index 6e30817b1..d9a2fb287 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -56,6 +56,7 @@ export const keyCodes = { RIGHT: 39, DELETE: 46, META: 91, + SLASH:191, }; /** diff --git a/types/api/block.d.ts b/types/api/block.d.ts index c20e46222..8f8fc246c 100644 --- a/types/api/block.d.ts +++ b/types/api/block.d.ts @@ -52,6 +52,13 @@ export interface BlockAPI { */ call(methodName: string, param?: object): void; + /** + * support markdown file export + * @return {string} + * @author asan + */ + markdown(): Promise; + /** * Save Block content * diff --git a/types/tools/block-tool.d.ts b/types/tools/block-tool.d.ts index 8c9bb858a..e9bcc2775 100644 --- a/types/tools/block-tool.d.ts +++ b/types/tools/block-tool.d.ts @@ -24,6 +24,8 @@ export interface BlockTool extends BaseTool { */ save(block: HTMLElement): BlockToolData; + markdown?(): Promise; + /** * Create Block's settings block */