diff --git a/demo/stories/css-variables/CSSVariables.stories.tsx b/demo/stories/css-variables/CSSVariables.stories.tsx index 89a7faf1..5964f1ac 100644 --- a/demo/stories/css-variables/CSSVariables.stories.tsx +++ b/demo/stories/css-variables/CSSVariables.stories.tsx @@ -28,6 +28,30 @@ export const Story: StoryObj = { control: {type: 'text'}, description: 'Editor contents padding', }, + '--g-selection-bg-color': { + control: {type: 'text'}, + description: 'Editor selection bg color', + }, + '--g-selection-border': { + control: {type: 'text'}, + description: 'Editor selection border', + }, + '--g-selection-border-radius': { + control: {type: 'text'}, + description: 'Editor selection border radius', + }, + '--g-selection-outline': { + control: {type: 'text'}, + description: 'Editor selection outline', + }, + '--g-selection-background': { + control: {type: 'text'}, + description: 'Editor selection background', + }, + '--g-selection-box-shadow': { + control: {type: 'text'}, + description: 'Editor selection box-shadow', + }, }, }; Story.storyName = 'Custom CSS Variables'; diff --git a/docs/how-to-customize-the-editor.md b/docs/how-to-customize-the-editor.md index 3ae64cc7..69d93484 100644 --- a/docs/how-to-customize-the-editor.md +++ b/docs/how-to-customize-the-editor.md @@ -3,6 +3,7 @@ ## How to customize the editor You can use CSS variables to make editor contents fit your own needs +### Elements styles | **Variable** | **Description** | **CSS Property Type** | **Default** | | :---: | :---: | :---: | :---: | | `--g-md-toolbar-padding` | Toolbar padding | padding | 0px | @@ -11,3 +12,11 @@ You can use CSS variables to make editor contents fit your own needs | `--g-md-toolbar-sticky-offset` | Toolbar offset in sticky mode | top | 0px | | `--g-md-toolbar-sticky-border` | Toolbar border in sticky mode | border | 1px solid var(--g-color-line-generic-solid) | | `--g-md-editor-padding` | Editor contents padding | padding | 0px | + +### Selection styles +| **Variable** | **Description** | **CSS Property Type** | **Default** | +| `--g-selection-border` | Editor selection border | border | none | +| `--g-selection-border-radius` | Editor selection border radius | border-radius | 6px | +| `--g-selection-outline` | Editor selection outline | outline | none | +| `--g-selection-background` | Editor selection background | background | #e6e6e6 | +| `--g-selection-box-shadow` | Editor selection box shadow | box-shadow | none | diff --git a/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts b/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts index 47095856..07448fa3 100644 --- a/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts +++ b/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts @@ -54,6 +54,7 @@ export const BaseSchemaSpecs: ExtensionAuto = (builder, 0, ]; }, + selectable: true, placeholder: opts.paragraphPlaceholder ? { content: opts.paragraphPlaceholder, diff --git a/src/extensions/base/BaseStyles/index.scss b/src/extensions/base/BaseStyles/index.scss deleted file mode 100644 index 233c8fa3..00000000 --- a/src/extensions/base/BaseStyles/index.scss +++ /dev/null @@ -1,28 +0,0 @@ -@use '../../../../node_modules/prosemirror-view/style/prosemirror'; - -// Make outline appear only if ProseMirror is focused - -.ProseMirror-selectednode { - outline: none; -} - -.li.ProseMirror-selectednode:after { - border: none; -} - -.g-md-editor.ProseMirror-focused { - .ProseMirror-selectednode { - outline: 2px solid #8cf; - } - - li.ProseMirror-selectednode:after { - border: 2px solid #8cf; - } -} - -.g-md-editor.ProseMirror, -.g-md-editor .ProseMirror { - &:focus { - outline: none; - } -} diff --git a/src/extensions/base/BaseStyles/index.ts b/src/extensions/base/BaseStyles/index.ts index d96566b9..44f45c15 100644 --- a/src/extensions/base/BaseStyles/index.ts +++ b/src/extensions/base/BaseStyles/index.ts @@ -2,8 +2,6 @@ import {Plugin} from 'prosemirror-state'; import type {ExtensionAuto} from '../../../core'; -import './index.scss'; - export const BaseStyles: ExtensionAuto = (builder) => { builder.addPlugin( () => diff --git a/src/extensions/behavior/Selection/selection.scss b/src/extensions/behavior/Selection/selection.scss index 0d2efc45..c387b05d 100644 --- a/src/extensions/behavior/Selection/selection.scss +++ b/src/extensions/behavior/Selection/selection.scss @@ -1,3 +1,103 @@ -.g-md-editor.ProseMirror-focused .pm-node-selected { - box-shadow: var(--g-color-text-info) 0 0 0 1px; +$active-node-default-indent-top: calc(-1 * var(--g-spacing-1)); +$active-node-default-indent-bottom: calc(-1 * var(--g-spacing-1)); +$active-node-default-indent-left: calc(-1 * var(--g-spacing-2)); +$active-node-default-indent-right: calc(-1 * var(--g-spacing-2)); + +$active-node-selector: '.pm-node-selected'; +$basic-elements: h1, h2, h3, h4, h5, h6, p, ul, ol, span, pre, '*[data-html]', '.g-md-checkbox', '.g-md-table-wrapper'; +$yfm-elements: 'div[class^="yfm-"]'; + +$default-selection-border: none; +$default-selection-border-radius: var(--g-border-radius-m); +$default-selection-outline: none; +// TODO: CHANGE TO TOKEN +$default-selection-background: #e6e6e6; +$default-selection-box-shadow: none; + +$default-li-marker-width: var(--g-spacing-4); + +@mixin node-props { + position: relative; + + border: $default-selection-border; + border-radius: $default-selection-border-radius; + outline: $default-selection-outline; + background: var(--g-selection-background, $default-selection-background); + box-shadow: $default-selection-box-shadow; +} + +@mixin selection-props { + border: var(--g-selection-border, $default-selection-border); + border-radius: var(--g-selection-border-radius, $default-selection-border-radius); + outline: var(--g-selection-outline, $default-selection-outline); + background: var(--g-selection-background, $default-selection-background); + box-shadow: var(--g-selection-box-shadow, $default-selection-box-shadow); +} + +[class].g-md-editor { + @each $basic-element in $basic-elements { + & #{$basic-element}#{$active-node-selector} { + @include node-props; + + &::after { + position: absolute; + z-index: -1; + inset: $active-node-default-indent-top + $active-node-default-indent-right + $active-node-default-indent-bottom + $active-node-default-indent-left; + + content: ''; + + @include selection-props; + } + + & img { + mix-blend-mode: multiply; + } + } + + } + + & #{$yfm-elements}#{$active-node-selector} { + position: relative; + + &::after { + position: absolute; + z-index: -1; + inset: $active-node-default-indent-top + $active-node-default-indent-right + $active-node-default-indent-bottom + $active-node-default-indent-left; + + content: ''; + + @include selection-props; + } + } + + & li#{$active-node-selector} { + @include node-props; + + &::marker { + @include selection-props; + } + + &::after { + position: absolute; + z-index: -1; + inset: $active-node-default-indent-top $active-node-default-indent-right + $active-node-default-indent-bottom + calc( + $active-node-default-indent-left - max( + var(--li-marker-width, 0), + $default-li-marker-width + ) + ); + + content: ''; + + @include selection-props; + } + } } diff --git a/src/extensions/behavior/Selection/selection.ts b/src/extensions/behavior/Selection/selection.ts index 4c832d18..f437cf0d 100644 --- a/src/extensions/behavior/Selection/selection.ts +++ b/src/extensions/behavior/Selection/selection.ts @@ -99,18 +99,21 @@ const getTopLevelNodesFromSelection = (selection: Selection, doc: Node) => { const nodes: {node: Node; pos: number}[] = []; if (selection.from !== selection.to) { const {from, to} = selection; + doc.nodesBetween(from, to, (node, pos) => { const withinSelection = from <= pos && pos + node.nodeSize <= to; + if ( node && - node.type.name !== 'paragraph' && !node.isText && node.type.spec.selectable && withinSelection ) { nodes.push({node, pos}); + return false; } + return true; }); } diff --git a/src/extensions/markdown/Lists/ListsSpecs/index.ts b/src/extensions/markdown/Lists/ListsSpecs/index.ts index 734e8b9f..c8118fe8 100644 --- a/src/extensions/markdown/Lists/ListsSpecs/index.ts +++ b/src/extensions/markdown/Lists/ListsSpecs/index.ts @@ -17,6 +17,27 @@ export const ListsSpecs: ExtensionAuto = (builder) => { spec: schemaSpecs[ListNode.ListItem], toMd: serializerTokens[ListNode.ListItem], fromMd: {tokenSpec: parserTokens[ListNode.ListItem]}, + // @ts-expect-error + view: () => (node, view, getPos) => { + return { + update: (node_) => { + node = node_; + + const pos = getPos(); + if (pos === undefined) return false; + + const dom = view.domAtPos(pos + 1).node as HTMLElement; + + const markerWidth = Math.max( + Math.floor(parseFloat(getComputedStyle(dom, '::marker').width)), + ); + + dom.style.setProperty('--li-marker-width', `${markerWidth}px`); + + return true; + }, + }; + }, })) .addNode(ListNode.BulletList, () => ({ spec: schemaSpecs[ListNode.BulletList],