convert react to vanilla js

This commit is contained in:
Dan Harrin
2025-05-28 20:59:46 +01:00
parent 899dbd59fd
commit f6f6c5f944
8 changed files with 205 additions and 85 deletions
+26
View File
@@ -17,6 +17,7 @@
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.6",
"@danharrin/alpine-mousetrap": "^0.1.6",
"@floating-ui/dom": "^1.7.0",
"@fontsource-variable/inter": "^5.0.18",
"@ryangjchandler/alpine-tooltip": "^2.0",
"@tiptap/core": "3.0.0-beta.5",
@@ -2197,6 +2198,31 @@
"node": ">=18"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
"integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==",
"dev": true,
"dependencies": {
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz",
"integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==",
"dev": true,
"dependencies": {
"@floating-ui/core": "^1.7.0",
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"dev": true
},
"node_modules/@fontsource-variable/inter": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.5.tgz",
+2 -1
View File
@@ -25,10 +25,10 @@
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.6",
"@danharrin/alpine-mousetrap": "^0.1.6",
"@floating-ui/dom": "^1.7.0",
"@fontsource-variable/inter": "^5.0.18",
"@ryangjchandler/alpine-tooltip": "^2.0",
"@tiptap/core": "3.0.0-beta.5",
"@tiptap/extensions": "3.0.0-beta.5",
"@tiptap/extension-blockquote": "3.0.0-beta.5",
"@tiptap/extension-bold": "3.0.0-beta.5",
"@tiptap/extension-code": "3.0.0-beta.5",
@@ -46,6 +46,7 @@
"@tiptap/extension-superscript": "3.0.0-beta.5",
"@tiptap/extension-text": "3.0.0-beta.5",
"@tiptap/extension-underline": "3.0.0-beta.5",
"@tiptap/extensions": "3.0.0-beta.5",
"@tiptap/pm": "3.0.0-beta.5",
"@tiptap/suggestion": "3.0.0-beta.5",
"alpine-lazy-load-assets": "^1.3.0",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -125,7 +125,7 @@ export default Node.create({
// When there are multiple types of merge tags, this attribute helps distinguish them
mergeTagSuggestionChar: {
default: '{{ ',
default: '@',
parseHTML: element => element.getAttribute('data-merge-tag-suggestion-char'),
renderHTML: attributes => {
return {
@@ -48,10 +48,7 @@ export default async ({
$wire: () => $wire,
}),
MergeTag.configure({
HTMLAttributes: {
class: 'merge-tag',
},
mergeTagSuggestions,
suggestion: mergeTagSuggestions,
}),
OrderedList,
Paragraph,
@@ -1,12 +1,26 @@
import { computePosition, flip, shift } from '@floating-ui/dom'
import { posToDOMRect, ReactRenderer } from '@tiptap/react'
const updatePosition = (editor, element) => {
const virtualElement = {
getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
const referenceElement = {
getBoundingClientRect: () => {
const { from, to } = editor.state.selection
const start = editor.view.coordsAtPos(from)
const end = editor.view.coordsAtPos(to)
return {
top: Math.min(start.top, end.top),
bottom: Math.max(start.bottom, end.bottom),
left: Math.min(start.left, end.left),
right: Math.max(start.right, end.right),
width: Math.abs(end.right - start.left),
height: Math.abs(end.bottom - start.top),
x: Math.min(start.left, end.left),
y: Math.min(start.top, end.top),
}
},
}
computePosition(virtualElement, element, {
computePosition(referenceElement, element, {
placement: 'bottom-start',
strategy: 'absolute',
middleware: [shift(), flip()],
@@ -31,67 +45,149 @@ export default {
},
render: () => {
let element
let selectedIndex = 0
let currentProps = null
const createDropdown = () => {
const dropdown = document.createElement('div')
dropdown.className = 'dropdown-menu'
return dropdown
}
const renderItems = () => {
if (!element || !currentProps) return
const items = currentProps.items || []
// Clear existing items
element.innerHTML = ''
if (items.length) {
items.forEach((item, index) => {
const button = document.createElement('button')
button.className = index === selectedIndex ? 'is-selected' : ''
button.textContent = item
button.addEventListener('click', () => selectItem(index))
element.appendChild(button)
})
} else {
const noResult = document.createElement('div')
noResult.className = 'item'
noResult.textContent = 'No result'
element.appendChild(noResult)
}
}
const selectItem = (index) => {
if (!currentProps) return
const items = currentProps.items || []
const item = items[index]
if (item) {
currentProps.command({ id: item })
}
}
const upHandler = () => {
if (!currentProps) return
const items = currentProps.items || []
if (items.length === 0) return
selectedIndex = (selectedIndex + items.length - 1) % items.length
renderItems()
}
const downHandler = () => {
if (!currentProps) return
const items = currentProps.items || []
if (items.length === 0) return
selectedIndex = (selectedIndex + 1) % items.length
renderItems()
}
const enterHandler = () => {
selectItem(selectedIndex)
}
return {
onStart: props => {
// Store current props
currentProps = props
// Create dropdown element
element = createDropdown()
element.style.position = 'absolute'
// Render initial items
renderItems()
// Append to DOM
document.body.appendChild(element)
// Reset selected index when items change
selectedIndex = 0
if (!props.clientRect) {
return
}
updatePosition(props.editor, element)
},
onUpdate(props) {
// Store current props
currentProps = props
// Reset selected index when items change
selectedIndex = 0
// Update dropdown items
renderItems()
if (!props.clientRect) {
return
}
updatePosition(props.editor, element)
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
if (element && element.parentNode) {
element.parentNode.removeChild(element)
}
return true
}
if (props.event.key === 'ArrowUp') {
upHandler()
return true
}
if (props.event.key === 'ArrowDown') {
downHandler()
return true
}
if (props.event.key === 'Enter') {
enterHandler()
return true
}
return false
},
onExit() {
if (element && element.parentNode) {
element.parentNode.removeChild(element)
}
},
}
// let component
//
// return {
// onStart: props => {
// component = new ReactRenderer(MentionList, {
// props,
// editor: props.editor,
// })
//
// if (!props.clientRect) {
// return
// }
//
// component.element.style.position = 'absolute'
//
// document.body.appendChild(component.element)
//
// updatePosition(props.editor, component.element)
// },
//
// onUpdate(props) {
// component.updateProps(props)
//
// if (!props.clientRect) {
// return
// }
//
// updatePosition(props.editor, component.element)
// },
//
// onKeyDown(props) {
// if (props.event.key === 'Escape') {
// component.destroy()
//
// return true
// }
//
// return component.ref?.onKeyDown(props)
// },
//
// onExit() {
// component.destroy()
// },
// }
},
}
+1 -1
View File
File diff suppressed because one or more lines are too long