diff --git a/demo/stories/css-variables/CSSVariables.stories.tsx b/demo/stories/css-variables/CSSVariables.stories.tsx index 89a7faf1..f3463d6f 100644 --- a/demo/stories/css-variables/CSSVariables.stories.tsx +++ b/demo/stories/css-variables/CSSVariables.stories.tsx @@ -28,6 +28,26 @@ export const Story: StoryObj = { control: {type: 'text'}, description: 'Editor contents padding', }, + '--g-md-wysiwyg-selection-border': { + control: {type: 'text'}, + description: 'Editor selection border', + }, + '--g-md-wysiwyg-selection-border-radius': { + control: {type: 'text'}, + description: 'Editor selection border radius', + }, + '--g-md-wysiwyg-selection-outline': { + control: {type: 'text'}, + description: 'Editor selection outline', + }, + '--g-md-wysiwyg-selection-background': { + control: {type: 'text'}, + description: 'Editor selection background', + }, + '--g-md-wysiwyg-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..732b5198 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,12 @@ 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-md-wysiwyg-selection-border` | Editor selection border | border | none | +| `--g-md-wysiwyg-selection-border-radius` | Editor selection border radius | border-radius | 6px | +| `--g-md-wysiwyg-selection-outline` | Editor selection outline | outline | none | +| `--g-md-wysiwyg-selection-background` | Editor selection background | background | #e6e6e6 | +| `--g-md-wysiwyg-selection-box-shadow` | Editor selection box shadow | box-shadow | none | diff --git a/src/extensions/additional/Math/MathSpecs/index.ts b/src/extensions/additional/Math/MathSpecs/index.ts index 2d63e9bd..968cb75f 100644 --- a/src/extensions/additional/Math/MathSpecs/index.ts +++ b/src/extensions/additional/Math/MathSpecs/index.ts @@ -45,6 +45,7 @@ export const MathSpecs: ExtensionAuto = (builder) => { code: true, toDOM: () => ['div', {class: 'math-block'}, 0], parseDOM: [{tag: 'div.math-block', priority: 200}], + selectable: true, }, fromMd: { tokenName: 'math_block', 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..bc5eb011 100644 --- a/src/extensions/behavior/Selection/selection.scss +++ b/src/extensions/behavior/Selection/selection.scss @@ -1,3 +1,391 @@ -.g-md-editor.ProseMirror-focused .pm-node-selected { - box-shadow: var(--g-color-text-info) 0 0 0 1px; +@use '../../../../node_modules/prosemirror-view/style/prosemirror'; + +// Make outline appear only if ProseMirror is focused + +.ProseMirror-selectednode { + outline: none; +} + +.g-md-editor.ProseMirror, +.g-md-editor .ProseMirror { + &:focus { + outline: none; + } +} + +$marker-space: var(--g-spacing-7); +$active-node-indent-top: calc(-1 * var(--g-spacing-1)); +$active-node-indent-bottom: calc(-1 * var(--g-spacing-1)); +$active-node-indent-left: calc(-1 * var(--g-spacing-2)); +$active-node-indent-right: 0; +$active-node-with-marker-indent-right: calc(-1 * $marker-space); + +$default-selection-border: none; +$default-selection-border-radius: var(--g-border-radius-m); +$default-selection-outline: none; +$default-selection-background: var(--g-color-base-generic); +$default-selection-box-shadow: none; + +// TODO: add specific classes for nodes for more explicit differentiation + +$active-node-selector: '.pm-node-selected'; +$highlight-elements: h1, h2, h3, h4, h5, h6, p, ul, ol, blockquote, 'span:not(+ img)', + 'div:has(> hr)', '.g-md-checkbox'; +$highlight-elements-no-inset: pre, 'p:has(img)', '*[data-html]', '.math-container', + '.g-md-table-wrapper', '.mermaid-container', 'div[class^="yfm-"]'; +$offset-elements: h1, h2, h3, h4, h5, h6, 'p:not(:has(img))', 'span:not(+ img)', 'div:has(> hr)', + '.g-md-checkbox'; + +/* mixin for (primarily) vertical margins */ +// TODO: rewrite for selective margin specification + +@mixin elements-margins { + $margin-map: ( + p: 0 0 calc(var(--g-spacing-2) + var(--g-spacing-half)), + '.g-md-checkbox': 0 0 calc(var(--g-spacing-2) + var(--g-spacing-half)), + '.g-md-checkbox__label': 0 0 0 calc(var(--g-spacing-2) - var(--g-spacing-half)), + 'li > *': 0 0 calc(var(--g-spacing-2) + var(--g-spacing-half)) var(--g-spacing-2), + 'blockquote > *': 0 0 calc(var(--g-spacing-2) + var(--g-spacing-half)) var(--g-spacing-2), + // '.yfm-cut:has(+ *)': 0 0 5px, // ? + '.yfm-cut:has(+ .yfm-cut)': 0 0 calc(var(--g-spacing-half)), + ); + + @each $selector, $margin-values in $margin-map { + & #{$selector} { + margin: $margin-values; + } + } +} + +/* default node properties */ + +@mixin node-props { + position: relative; + + box-sizing: border-box; + + border-radius: var(--g-md-wysiwyg-selection-border-radius, $default-selection-border-radius); + + &, + tbody, + img { + mix-blend-mode: multiply; + + border-radius: var( + --g-md-wysiwyg-selection-border-radius, + $default-selection-border-radius + ); + } + + &#{$active-node-selector} { + &, + * { + &::selection { + color: inherit; + } + } + } +} + +/* default selection properties */ + +@mixin selection-props { + border: var(--g-md-wysiwyg-selection-border, $default-selection-border); + border-radius: var(--g-md-wysiwyg-selection-border-radius, $default-selection-border-radius); + outline: var(--g-md-wysiwyg-selection-outline, $default-selection-outline); + background: var(--g-md-wysiwyg-selection-background, $default-selection-background); + box-shadow: var(--g-md-wysiwyg-selection-box-shadow, $default-selection-box-shadow); +} + +/* try for lists customization */ +// @mixin custom-lists { +// & ol { +// counter-reset: li-count; + +// & li { +// position: relative; + +// list-style-type: decimal; + +// counter-increment: li-count; + +// &::marker { +// content: ''; +// } + +// &::before { +// position: absolute; +// right: 100%; + +// display: flex; +// justify-content: center; + +// width: $marker-space; +// height: 100%; + +// content: counter(li-count) '.'; +// } +// } +// } + +// & ul { +// & li { +// position: relative; + +// list-style-type: disc; + +// &::marker { +// content: ''; +// } + +// &::before { +// position: absolute; +// right: 100%; + +// display: flex; +// justify-content: center; + +// width: $marker-space; +// height: 100%; + +// content: '•'; +// } +// } +// } +// } + +[class].g-md-editor { + // REMOVE + &.ProseMirror { + padding: 40px; + } +} + +[class].g-md-editor-component { + // @include custom-lists; + /* ⬇ basic styles ⬇ */ + + @include elements-margins; + + @each $offset-element in $offset-elements { + & #{$offset-element} { + margin-left: var(--g-spacing-2); + } + } + + @each $highlight-element in $highlight-elements { + & #{$highlight-element} { + @include node-props; + + &#{$active-node-selector} { + &::after { + position: absolute; + z-index: -1; + inset: $active-node-indent-top + $active-node-indent-right + $active-node-indent-bottom + $active-node-indent-left; + + content: ''; + + @include selection-props; + } + } + } + } + + @each $highlight-element-no-inset in $highlight-elements-no-inset { + & #{$highlight-element-no-inset} { + @include node-props; + + &#{$active-node-selector} { + &::after { + position: absolute; + z-index: -1; + inset: 0; + + content: ''; + + @include selection-props; + } + } + } + } + + // REMOVE ? + & > * { + margin-top: 0; + margin-bottom: calc(var(--g-spacing-2) + var(--g-spacing-half)); + } + + /* ⬇ selective styles specification ⬇ */ + + & .yfm-cut { + border: none; + + @include node-props; + + &:hover::before { + position: absolute; + z-index: -1; + inset: 0; + + content: ''; + + border: 1px dashed var(--g-color-line-generic); + border-radius: var( + --g-md-wysiwyg-selection-border-radius, + $default-selection-border-radius + ); + } + + &-title { + left: calc(var(--g-spacing-2) - var(--g-spacing-half)); + + &::before { + left: var(--g-spacing-half); + + width: 16px; + height: 16px; + } + } + } + + & .yfm-tabs { + padding-left: var(--g-spacing-2); + + border: none; + + @include node-props; + + &:hover::before { + position: absolute; + z-index: -1; + inset: 0; + + content: ''; + + border: 1px dashed var(--g-color-line-generic); + border-radius: var( + --g-md-wysiwyg-selection-border-radius, + $default-selection-border-radius + ); + } + } + + & .yfm-note { + padding-left: var(--g-spacing-9); + + &-title { + margin-left: 0; + + &::before { + margin-left: calc(-1 * (var(--g-spacing-7) + var(--g-spacing-half))); + padding-right: 0; + scale: 0.7; + } + } + } + + & .yfm-file { + display: inline-block; + align-content: center; + + height: 28px; + margin-left: var(--g-spacing-9); + + &__icon { + position: absolute; + scale: 0.7; + + margin-left: -26px; + } + } + + & .g-md-checkbox { + &__input { + top: 0; + + width: 17px; + height: 17px; + } + } + + & .g-md-table-wrapper { + margin-bottom: 0; + } + + & div:has(> hr) { + margin-right: var(--g-spacing-2); + + @include node-props; + + &#{$active-node-selector} { + &::after { + top: -13px; + right: -8px; + bottom: -13px; + } + } + + & hr { + height: 2px; + margin: 20px 0; + } + } + + & pre code { + padding: var(--g-spacing-2); + } + + /* ⬇ ...for elements with marker zone ⬇ */ + + & ul, + ol, + blockquote { + padding-left: $marker-space; + } + + & li { + @include node-props; + + &#{$active-node-selector} { + &::after { + position: absolute; + z-index: -1; + inset: $active-node-indent-top $active-node-indent-right $active-node-indent-bottom + $active-node-with-marker-indent-right; + + content: ''; + + @include selection-props; + } + } + } + + & blockquote { + position: relative; + + border: none; + + &#{$active-node-selector} { + &::after { + left: 0; + } + } + + &::before { + position: absolute; + z-index: -1; + left: 15px; + + height: 100%; + + content: ''; + + border-left: 3px solid var(--yfm-color-accent); + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } } diff --git a/src/extensions/behavior/Selection/selection.ts b/src/extensions/behavior/Selection/selection.ts index 4c832d18..ee8272c3 100644 --- a/src/extensions/behavior/Selection/selection.ts +++ b/src/extensions/behavior/Selection/selection.ts @@ -99,18 +99,16 @@ 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 - ) { + + if (node && !node.isText && node.type.spec.selectable && withinSelection) { nodes.push({node, pos}); + return false; } + return true; }); } diff --git a/src/extensions/markdown/Blockquote/BlockquoteSpecs/index.ts b/src/extensions/markdown/Blockquote/BlockquoteSpecs/index.ts index cd8671dc..43614392 100644 --- a/src/extensions/markdown/Blockquote/BlockquoteSpecs/index.ts +++ b/src/extensions/markdown/Blockquote/BlockquoteSpecs/index.ts @@ -17,6 +17,7 @@ export const BlockquoteSpecs: ExtensionAuto = (builder) => { toDOM() { return ['blockquote', 0]; }, + selectable: true, }, fromMd: {tokenSpec: {name: blockquoteNodeName, type: 'block'}}, toMd: (state, node) => { diff --git a/src/extensions/markdown/HorizontalRule/HorizontalRuleSpecs/index.ts b/src/extensions/markdown/HorizontalRule/HorizontalRuleSpecs/index.ts index b7689ae9..52d2c8b1 100644 --- a/src/extensions/markdown/HorizontalRule/HorizontalRuleSpecs/index.ts +++ b/src/extensions/markdown/HorizontalRule/HorizontalRuleSpecs/index.ts @@ -14,6 +14,7 @@ export const HorizontalRuleSpecs: ExtensionAuto = (builder) => { toDOM() { return ['div', ['hr']]; }, + selectable: true, }, fromMd: { tokenName: 'hr', diff --git a/src/extensions/markdown/Lists/ListsSpecs/index.ts b/src/extensions/markdown/Lists/ListsSpecs/index.ts index 734e8b9f..3cb57615 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], diff --git a/src/extensions/yfm/YfmTabs/YfmTabsSpecs/schema.ts b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/schema.ts index 9a37d7b6..3b692052 100644 --- a/src/extensions/yfm/YfmTabs/YfmTabsSpecs/schema.ts +++ b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/schema.ts @@ -78,6 +78,7 @@ export const getSchemaSpecs: ( toDOM(node) { return ['div', node.attrs, 0]; }, + selectable: true, complex: 'root', },