Skip to content
This repository has been archived by the owner on Nov 7, 2022. It is now read-only.

Commit

Permalink
works!
Browse files Browse the repository at this point in the history
  • Loading branch information
Darmody committed Jun 30, 2022
1 parent e9856b9 commit 8422225
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 16 deletions.
Expand Up @@ -28,6 +28,8 @@ export const ParagraphView: FC<ParagraphViewProps> = props => {
const placeholderClassName = placeholderStyle()
const paragraphClassName = paragraphStyles()

console.log(props)

return (
<BlockContainer
node={node}
Expand Down
@@ -1,6 +1,6 @@
// https://github.com/ueberdosis/tiptap/tree/main/packages/extension-bubble-menu
import { Editor, posToDOMRect } from '@tiptap/core'
import { EditorState, Plugin, PluginKey, Selection } from 'prosemirror-state'
import { EditorState, Plugin, PluginKey, Selection, TextSelection } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
import tippy, { Instance, Props } from 'tippy.js'

Expand Down Expand Up @@ -118,6 +118,7 @@ export class BubbleMenuView {
}

createBubbleMenuPopover(view: EditorView, selection: Selection): void {
if (!(selection instanceof TextSelection)) return
this.createTooltip()

// support for CellSelections
Expand Down
Expand Up @@ -32,8 +32,11 @@ export class MultipleNodeSelection extends Selection {
const doc = $anchorPos.node(0)
const ranges: SelectionRange[] = []

doc.nodesBetween($anchorPos.pos, $headPos.pos, (node, pos) => {
ranges.push(new SelectionRange(doc.resolve(pos + 1), doc.resolve(pos + node.nodeSize)))
const from = Math.min($anchorPos.pos, $headPos.pos)
const to = Math.max($anchorPos.pos, $headPos.pos)

doc.nodesBetween(from, to, (node, pos) => {
ranges.push(new SelectionRange(doc.resolve(pos), doc.resolve(pos + node.nodeSize)))
return false
})

Expand Down Expand Up @@ -150,6 +153,7 @@ export class MultipleNodeSelection extends Selection {
* @returns
*/
static create(doc: Node, anchor: number, head: number = anchor): MultipleNodeSelection {
console.log(anchor, head)
return new MultipleNodeSelection(doc.resolve(anchor), doc.resolve(head))
}
}
Expand Down
184 changes: 171 additions & 13 deletions packages/editor/src/extensions/extensions/selection/selection.ts
@@ -1,6 +1,6 @@
import { Editor } from '@tiptap/core'
import { Plugin, PluginKey, EditorState, TextSelection } from 'prosemirror-state'
import { Decoration, DecorationSet } from 'prosemirror-view'
import { Decoration, DecorationSet, EditorView } from 'prosemirror-view'
import { findNodesInSelection } from '../../../helpers'
import { createExtension } from '../../common'
import { meta, SelectAttributes, SelectionOptions } from './meta'
Expand All @@ -13,6 +13,18 @@ import { meta as taskItemMeta } from '../../blocks/taskItem/meta'
import { meta as taskListMeta } from '../../blocks/taskList/meta'
import { meta as calloutMeta } from '../../blocks/callout/meta'
import { meta as blockquoteMeta } from '../../blocks/blockquote/meta'
import { MultipleNodeSelection } from './MultipleNodeSelection'

declare module '@tiptap/core' {
interface Commands<ReturnType> {
multipleNodeSelection: {
/**
* Set a multiple node selection
*/
setMultipleNodeSelection: (anchor: number, head: number) => ReturnType
}
}
}

const allowedNodeTypes = [
paragraphMeta.name,
Expand Down Expand Up @@ -56,15 +68,126 @@ export const isTextContentSelected = ({ editor, from, to }: { editor: Editor; fr

export const DEFAULT_SELECTION_CLASS = 'editor-selection'

function resolveCoordinates(
anchor: { x: number; y: number },
head: {
x: number
y: number
},
docRect: {
top: number
left: number
bottom: number
right: number
}
): null | {
anchor: {
x: number
y: number
}
head: {
x: number
y: number
}
} {
if (anchor.y < docRect.top && head.y < docRect.top) return null
if (anchor.y > docRect.bottom && head.y > docRect.bottom) return null
if (anchor.x < docRect.left && head.x < docRect.left) return null
if (anchor.x > docRect.right && head.x > docRect.right) return null

return {
anchor: {
x: Math.max(Math.min(docRect.right, anchor.x), docRect.left),
y: Math.max(Math.min(docRect.bottom, anchor.y), docRect.top)
},
head: {
x: Math.max(Math.min(docRect.right, head.x), docRect.left),
y: Math.max(Math.min(docRect.bottom, head.y), docRect.top)
}
}
}

function resolveSelection(editor: Editor, view: EditorView, pluginState: SelectionState, event: MouseEvent): void {
if (!pluginState.multiNodeSelecting) return

const { x, y } = pluginState.multiNodeSelecting
const rect = view.dom.getBoundingClientRect()

const coordinates = resolveCoordinates({ x, y }, { x: event.x, y: event.y }, rect)

if (!coordinates) return

const anchor = view.posAtCoords({ left: coordinates.anchor.x, top: coordinates.anchor.y })
const head = view.posAtCoords({ left: coordinates.head.x, top: coordinates.head.y })

if (!anchor || !head) return

if (anchor.pos === head.pos) return

editor.commands.setMultipleNodeSelection(anchor.pos, head.pos)
}

interface SelectionState {
multiNodeSelecting: boolean
multiNodeSelecting:
| false
| {
x: number
y: number
}
}

export const SelectionPluginKey = new PluginKey(meta.name)
export const SelectionPluginKey = new PluginKey<SelectionState>(meta.name)

class SelectionView {
view: EditorView
editor: Editor

constructor(editor: Editor, view: EditorView) {
this.editor = editor
this.view = view

window.addEventListener('mouseup', this.mouseup)
}

mouseup = (event: MouseEvent) => {
const pluginState = SelectionPluginKey.getState(this.view.state)
if (pluginState?.multiNodeSelecting) {
resolveSelection(this.editor, this.view, pluginState, event)
this.view.dispatch(this.view.state.tr.setMeta('multipleNodeSelectingEnd', true))
this.editor.commands.focus()
}
}

destroy(): void {
window.removeEventListener('mouseup', this.mouseup)
}
}

export const Selection = createExtension<SelectionOptions, SelectAttributes>({
name: meta.name,

addCommands() {
return {
setMultipleNodeSelection:
(anchor, head) =>
({ dispatch, tr }) => {
if (dispatch) {
const { doc } = tr
const minPos = TextSelection.atStart(doc).from
const maxPos = TextSelection.atEnd(doc).to

const resolvedAnchor = Math.max(Math.min(anchor, maxPos), minPos)
const resolvedHead = Math.max(Math.min(head, maxPos), minPos)
const selection = MultipleNodeSelection.create(doc, resolvedAnchor, resolvedHead)

tr.setSelection(selection)
}

return true
}
}
},

addProseMirrorPlugins() {
return [
new Plugin<SelectionState>({
Expand All @@ -76,9 +199,19 @@ export const Selection = createExtension<SelectionOptions, SelectAttributes>({
}
},
apply(tr, value, oldState, newState) {
const coordinates = tr.getMeta('multipleNodeSelectingStart')
if (coordinates) {
return { ...value, multiNodeSelecting: coordinates }
}

if (tr.getMeta('multipleNodeSelectingEnd')) {
return { ...value, multiNodeSelecting: false }
}

return { ...value }
}
},
view: view => new SelectionView(this.editor, view),
props: {
handleDOMEvents: {
mousedown: (view, event) => {
Expand All @@ -87,12 +220,21 @@ export const Selection = createExtension<SelectionOptions, SelectAttributes>({
// if click happens outside the doc nodes, assume a Multiple Node Selection will happen.
// so call blur to avoid Text Selection happen.
if (result?.inside === -1) {
view.dispatch(
view.state.tr.setMeta('multipleNodeSelectingStart', {
x: event.x,
y: event.y
})
)
this.editor.commands.blur()
}
return false
},
mouseup: () => {
this.editor.commands.focus()
mousemove: (view, event) => {
const pluginState = SelectionPluginKey.getState(view.state)
if (!pluginState) return false

resolveSelection(this.editor, view, pluginState, event)

return false
}
Expand All @@ -101,18 +243,34 @@ export const Selection = createExtension<SelectionOptions, SelectAttributes>({
const { from, to } = state.selection
const decorations: Decoration[] = []

if (!this.editor.isEditable || from === to) return
if (!this.editor.isEditable) return
if (state.selection instanceof TextSelection && from === to) return

if (state.selection instanceof TextSelection) {
const inlineDecoration = Decoration.inline(from, to, {
class: this.options.HTMLAttributes?.class ?? DEFAULT_SELECTION_CLASS,
style: this.options.HTMLAttributes?.style
})
decorations.push(inlineDecoration)

const inlineDecoration = Decoration.inline(from, to, {
class: this.options.HTMLAttributes?.class ?? DEFAULT_SELECTION_CLASS,
style: this.options.HTMLAttributes?.style
})
decorations.push(inlineDecoration)
return DecorationSet.create(this.editor.state.doc, decorations)
}

if (state.selection instanceof MultipleNodeSelection) {
state.selection.ranges.forEach(range => {
const nodeDecoration = Decoration.node(range.$from.pos, range.$to.pos, {
class: this.options.HTMLAttributes?.class ?? DEFAULT_SELECTION_CLASS,
style: this.options.HTMLAttributes?.style
})
decorations.push(nodeDecoration)
})

return DecorationSet.create(this.editor.state.doc, decorations)
return DecorationSet.create(this.editor.state.doc, decorations)
}
},
createSelectionBetween: (view, $anchor, $head) => {
if (!this.editor.isEditable || $anchor.pos === $head.pos) return
if (!this.editor.isEditable) return
if ($anchor.pos === $head.pos) return

// remove non-text node and empty text node selection by check: to - from > 1
// because we don't want non-text nodes at the end of Text Selection
Expand Down

0 comments on commit 8422225

Please sign in to comment.