- reworked the internal logic to be simpler and more robust - removed the dedicated HTML editing mode markdown is a superset of HTML in this context - fixed layout of the css editor - introduces a quick edit mode: double-clicking the note's preview area now opens an inline Ace editor
878 lines
23 KiB
JavaScript
878 lines
23 KiB
JavaScript
/// <reference path="../types/typedefs.js" />
|
|
|
|
import { app } from '../../scripts/app.js'
|
|
|
|
import * as shared from './comfy_shared.js'
|
|
import * as mtb_ui from './mtb_ui.js'
|
|
import { infoLogger, successLogger, errorLogger } from './comfy_shared.js'
|
|
import {
|
|
DEFAULT_CSS,
|
|
// DEFAULT_HTML,
|
|
DEFAULT_MD,
|
|
DEFAULT_MODE,
|
|
DEFAULT_THEME,
|
|
THEMES,
|
|
CSS_RESET,
|
|
DEMO_CONTENT,
|
|
} from './note_plus.constants.js'
|
|
import { LocalStorageManager } from './comfy_shared.js'
|
|
|
|
const storage = new LocalStorageManager('mtb')
|
|
|
|
/**
|
|
* Uses `@mtb/markdown-parser` (a fork of marked)
|
|
* It is statically stored to avoid having
|
|
* more than 1 instance ever.
|
|
* The size difference between both libraries...
|
|
* ╭───┬────────────────────────────────┬──────────╮
|
|
* │ # │ name │ size │
|
|
* ├───┼────────────────────────────────┼──────────┤
|
|
* │ 0 │ web-dist/mtb_markdown_plus.mjs │ 1.2 MB │ <- with shiki
|
|
* │ 1 │ web-dist/mtb_markdown.mjs │ 44.7 KB │
|
|
* ╰───┴────────────────────────────────┴──────────╯
|
|
*/
|
|
let useShiki = storage.get('np-use-shiki', false)
|
|
|
|
const makeResizable = (dialog) => {
|
|
dialog.style.resize = 'both'
|
|
dialog.style.transformOrigin = 'top left'
|
|
dialog.style.overflow = 'auto'
|
|
}
|
|
|
|
const makeDraggable = (dialog, handle) => {
|
|
let offsetX = 0
|
|
let offsetY = 0
|
|
let isDragging = false
|
|
|
|
const onMouseMove = (e) => {
|
|
if (isDragging) {
|
|
dialog.style.left = `${e.clientX - offsetX}px`
|
|
dialog.style.top = `${e.clientY - offsetY}px`
|
|
}
|
|
}
|
|
|
|
const onMouseUp = () => {
|
|
isDragging = false
|
|
document.removeEventListener('mousemove', onMouseMove)
|
|
document.removeEventListener('mouseup', onMouseUp)
|
|
}
|
|
|
|
handle.addEventListener('mousedown', (e) => {
|
|
isDragging = true
|
|
offsetX = e.clientX - dialog.offsetLeft
|
|
offsetY = e.clientY - dialog.offsetTop
|
|
document.addEventListener('mousemove', onMouseMove)
|
|
document.addEventListener('mouseup', onMouseUp)
|
|
})
|
|
}
|
|
|
|
/** @extends {LGraphNode} */
|
|
class NotePlus extends LiteGraph.LGraphNode {
|
|
// same values as the comfy note
|
|
color = LGraphCanvas.node_colors.yellow.color
|
|
bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
|
|
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
|
|
|
|
/* NOTE: this is not serialized and only there to make multiple
|
|
* note+ nodes in the same graph unique.
|
|
*/
|
|
uuid
|
|
|
|
/** Stores the dialog observer*/
|
|
resizeObserver
|
|
|
|
/** Live update the preview*/
|
|
live = true
|
|
/** DOM height by adding child size together*/
|
|
calculated_height = 0
|
|
|
|
/** ????*/
|
|
_raw_html
|
|
|
|
/** might not be needed anymore */
|
|
inner
|
|
|
|
/** the dialog DOM widget*/
|
|
dialog
|
|
|
|
/** widgets*/
|
|
|
|
/** used to store the raw value and display the parsed html at the same time*/
|
|
html_widget
|
|
|
|
editorsContainer
|
|
/** ACE editors instances*/
|
|
html_editor
|
|
css_editor
|
|
|
|
/** quick edit mode*/
|
|
isEditing = false
|
|
quickEditor = null
|
|
quickEditorContainer = null
|
|
|
|
constructor() {
|
|
super()
|
|
this.uuid = shared.makeUUID()
|
|
|
|
infoLogger('Constructing Note+ instance')
|
|
shared.ensureMarkdownParser((_p) => {
|
|
this.updateHTML()
|
|
})
|
|
// - litegraph settings
|
|
this.properties = {
|
|
css: DEFAULT_CSS,
|
|
theme: DEFAULT_THEME,
|
|
}
|
|
this.collapsable = true
|
|
this.isVirtualNode = true
|
|
this.shape = LiteGraph.BOX_SHAPE
|
|
this.serialize_widgets = true
|
|
|
|
// - default values, serialization is done through widgets
|
|
this._raw_html = DEFAULT_MD
|
|
|
|
// - state
|
|
this.live = true
|
|
this.calculated_height = 0
|
|
|
|
// - add widgets
|
|
const cinner = document.createElement('div')
|
|
this.inner = document.createElement('div')
|
|
|
|
this.title = 'Note+'
|
|
|
|
cinner.append(this.inner)
|
|
this.inner.classList.add('note-plus-preview')
|
|
cinner.style.margin = '0'
|
|
cinner.style.padding = '0'
|
|
this.html_widget = this.addDOMWidget('HTML', 'html', cinner, {
|
|
setValue: (val) => {
|
|
this._raw_html = val
|
|
},
|
|
getValue: () => this._raw_html,
|
|
getMinHeight: () => this.calculated_height, // (the edit button),
|
|
onDraw: () => {
|
|
// HACK: dirty hack for now until it's addressed upstream...
|
|
// TODO: check if still needed
|
|
this.html_widget.element.style.pointerEvents = 'none'
|
|
// NOTE: not sure about this, it avoid the visual "bugs" but scrolling over the wrong area will affect zoom...
|
|
// this.html_widget.element.style.overflow = 'scroll'
|
|
},
|
|
hideOnZoom: false,
|
|
})
|
|
|
|
this.setupDialog()
|
|
this.loadAceEditor()
|
|
this.setupDoubleClickEdit()
|
|
}
|
|
|
|
/**
|
|
* @param {CanvasRenderingContext2D} ctx canvas context
|
|
* @param {any} _graphcanvas
|
|
*/
|
|
onDrawForeground(ctx, _graphcanvas) {
|
|
if (this.flags.collapsed) return
|
|
this.drawEditIcon(ctx)
|
|
|
|
// DEBUG BACKGROUND
|
|
// ctx.fillStyle = 'rgba(0, 255, 0, 0.3)'
|
|
// const rect = this.rect
|
|
// ctx.fillRect(rect.x, rect.y, rect.width, rect.height)
|
|
}
|
|
|
|
// drawSideHandle(ctx) {
|
|
|
|
drawEditIcon(ctx) {
|
|
const rect = this.iconRect
|
|
// DEBUG ICON POSITION
|
|
// ctx.fillStyle = 'rgba(0, 255, 0, 0.3)'
|
|
// ctx.fillRect(rect.x, rect.y, rect.width, rect.height)
|
|
|
|
const pencilPath = new Path2D(
|
|
'M21.28 6.4l-9.54 9.54c-.95.95-3.77 1.39-4.4.76-.63-.63-.2-3.45.75-4.4l9.55-9.55a2.58 2.58 0 1 1 3.64 3.65z',
|
|
)
|
|
const folderPath = new Path2D(
|
|
'M11 4H6a4 4 0 0 0-4 4v10a4 4 0 0 0 4 4h11c2.21 0 3-1.8 3-4v-5',
|
|
)
|
|
|
|
ctx.save()
|
|
ctx.translate(rect.x, rect.y)
|
|
ctx.scale(rect.width / 32, rect.height / 32)
|
|
ctx.strokeStyle = 'rgba(255,255,255,0.4)'
|
|
ctx.lineCap = 'round'
|
|
ctx.lineJoin = 'round'
|
|
ctx.lineWidth = 2.4
|
|
ctx.stroke(pencilPath)
|
|
ctx.stroke(folderPath)
|
|
ctx.restore()
|
|
}
|
|
/**
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @param {{x:number,y:number,width:number,height:number}} rect
|
|
* @returns {}
|
|
*/
|
|
inRect(x, y, rect) {
|
|
rect = rect || this.iconRect
|
|
return (
|
|
x >= rect.x &&
|
|
x <= rect.x + rect.width &&
|
|
y >= rect.y &&
|
|
y <= rect.y + rect.height
|
|
)
|
|
}
|
|
get rect() {
|
|
return {
|
|
x: 0,
|
|
y: 0,
|
|
width: this.size[0],
|
|
height: this.size[1],
|
|
}
|
|
}
|
|
get iconRect() {
|
|
let icon = {
|
|
size: 24,
|
|
margin: 5,
|
|
yoffset: -25,
|
|
}
|
|
|
|
if (window.inspector) {
|
|
if (window.inspector.get('note_icon') !== null) {
|
|
icon = window.inspector.get('note_icon')
|
|
} else {
|
|
window.inspector.set('note_icon', icon)
|
|
window.inspector.subscribe('note_icon', (k, v) => {
|
|
console.log(v)
|
|
})
|
|
|
|
window.inspector.set('note_icon_debug', true)
|
|
|
|
window.inspector.subscribe('note_icon_debug', (k, v) => {
|
|
this.debug_icon = v
|
|
})
|
|
}
|
|
}
|
|
|
|
return {
|
|
x: this.size[0] - icon.size - icon.margin,
|
|
y: icon.yoffset,
|
|
width: icon.size,
|
|
height: icon.size,
|
|
}
|
|
}
|
|
onMouseDown(_e, localPos, _graphcanvas) {
|
|
if (this.inRect(localPos[0], localPos[1])) {
|
|
this.openEditorDialog()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
onDblClick(e, localPos, graphcanvas) {
|
|
this.openEditorDialog()
|
|
return true
|
|
}
|
|
|
|
setupDialog() {
|
|
infoLogger('Setup dialog')
|
|
|
|
this.dialog = new app.ui.dialog.constructor()
|
|
this.dialog.element.style.width = '680px'
|
|
this.dialog.element.classList.add('comfy-settings')
|
|
|
|
Object.assign(this.dialog.element.style, {
|
|
position: 'absolute',
|
|
boxShadow: 'none',
|
|
})
|
|
|
|
const subcontainer = this.dialog.textElement.parentElement
|
|
|
|
if (subcontainer) {
|
|
Object.assign(subcontainer.style, {
|
|
width: '100%',
|
|
})
|
|
}
|
|
const closeButton = this.dialog.element.querySelector('button')
|
|
closeButton.textContent = 'CANCEL'
|
|
closeButton.id = 'cancel-editor-dialog'
|
|
closeButton.title =
|
|
"Cancel the changes since last opened (doesn't support live mode)"
|
|
closeButton.disabled = this.live
|
|
|
|
closeButton.style.background = this.live
|
|
? 'repeating-linear-gradient(45deg,#606dbc,#606dbc 10px,#465298 10px,#465298 20px)'
|
|
: ''
|
|
|
|
const saveButton = document.createElement('button')
|
|
saveButton.textContent = 'SAVE'
|
|
saveButton.onclick = () => {
|
|
this.closeEditorDialog(true)
|
|
}
|
|
closeButton.onclick = () => {
|
|
this.closeEditorDialog(false)
|
|
}
|
|
closeButton.before(saveButton)
|
|
}
|
|
|
|
teardownEditors() {
|
|
this.css_editor.destroy()
|
|
this.css_editor.container.remove()
|
|
|
|
this.html_editor.destroy()
|
|
this.html_editor.container.remove()
|
|
|
|
this.html_editor = null
|
|
this.css_editor = null
|
|
}
|
|
|
|
closeEditorDialog(accept) {
|
|
infoLogger('Closing editor dialog', accept)
|
|
if (accept && !this.live) {
|
|
this.updateHTML(this.html_editor.getValue())
|
|
this.updateCSS(this.css_editor.getValue())
|
|
}
|
|
if (this.resizeObserver) {
|
|
this.resizeObserver.disconnect()
|
|
this.resizeObserver = null
|
|
}
|
|
this.teardownEditors()
|
|
this.dialog.close()
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLElement} elem
|
|
*/
|
|
hookResize(elem) {
|
|
if (!this.resizeObserver) {
|
|
const observer = () => {
|
|
Object.assign(this.editorsContainer.style, {
|
|
minHeight: `${(this.dialog.element.clientHeight / 100) * 50}px`, //'200px',
|
|
})
|
|
// avoid the few ticks that can happen between destroying the editors
|
|
// and the watched dialog
|
|
if (this.html_editor) {
|
|
this.html_editor.resize()
|
|
this.css_editor.resize()
|
|
}
|
|
}
|
|
this.resizeObserver = new ResizeObserver(observer).observe(elem)
|
|
}
|
|
}
|
|
openEditorDialog() {
|
|
this.hookResize(this.dialog.element)
|
|
const container = mtb_ui.makeElement('div', {
|
|
display: 'flex',
|
|
gap: '10px',
|
|
flexDirection: 'column',
|
|
})
|
|
|
|
this.editorsContainer = mtb_ui.makeElement(
|
|
'div',
|
|
{
|
|
display: 'flex',
|
|
gap: '10px',
|
|
flexDirection: 'row',
|
|
minHeight: this.dialog.element.offsetHeight, //'200px',
|
|
width: '100%',
|
|
},
|
|
container,
|
|
)
|
|
|
|
this.dialog.show('')
|
|
this.dialog.textElement.append(container)
|
|
|
|
mtb_ui.makeElement(
|
|
'div.#noteplus-html-editor',
|
|
{
|
|
width: '100%',
|
|
height: '100%',
|
|
|
|
minWidth: '300px',
|
|
minHeight: 'inherit',
|
|
},
|
|
this.editorsContainer,
|
|
)
|
|
|
|
mtb_ui.makeElement(
|
|
'div.#noteplus-css-editor',
|
|
{
|
|
width: '100%',
|
|
height: '100%',
|
|
minHeight: 'inherit',
|
|
},
|
|
this.editorsContainer,
|
|
)
|
|
|
|
const live_edit = document.createElement('input')
|
|
live_edit.type = 'checkbox'
|
|
live_edit.checked = this.live
|
|
live_edit.onchange = () => {
|
|
this.live = live_edit.checked
|
|
const cancel_button = this.dialog.element.querySelector(
|
|
'#cancel-editor-dialog',
|
|
)
|
|
if (cancel_button) {
|
|
cancel_button.disabled = this.live
|
|
cancel_button.style.background = this.live
|
|
? 'repeating-linear-gradient(45deg,#606dbc,#606dbc 10px,#465298 10px,#465298 20px)'
|
|
: ''
|
|
}
|
|
}
|
|
|
|
//- "Dynamic" elements
|
|
const syncUI = () => {
|
|
// let convert_to_html =
|
|
|
|
// the header for dragging the dialog
|
|
const header = mtb_ui.makeElement('div', {
|
|
padding: '8px',
|
|
cursor: 'move',
|
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
userSelect: 'none',
|
|
borderBottom: '1px solid #ddd',
|
|
})
|
|
|
|
header.textContent = 'MTB Note+ Editor'
|
|
container.prepend(header)
|
|
makeDraggable(this.dialog.element, header)
|
|
makeResizable(this.dialog.element)
|
|
}
|
|
//- combobox
|
|
let theme_select = this.dialog.element.querySelector('#theme_select')
|
|
if (!theme_select) {
|
|
infoLogger('Creating combobox for select')
|
|
theme_select = document.createElement('select')
|
|
theme_select.name = 'theme'
|
|
theme_select.id = 'theme_select'
|
|
|
|
const addOption = (label) => {
|
|
const option = document.createElement('option')
|
|
option.value = label
|
|
option.textContent = label
|
|
theme_select.append(option)
|
|
}
|
|
for (const t of THEMES) {
|
|
addOption(t)
|
|
}
|
|
|
|
theme_select.addEventListener('change', (event) => {
|
|
const val = event.target.value
|
|
this.setTheme(val)
|
|
})
|
|
|
|
container.prepend(theme_select)
|
|
}
|
|
|
|
theme_select.value = this.properties.theme
|
|
|
|
// let select_mode = this.dialog.element.querySelector('#edit_mode')
|
|
|
|
syncUI()
|
|
|
|
const live_edit_label = document.createElement('label')
|
|
live_edit_label.textContent = 'Live Edit'
|
|
|
|
// add a tooltip
|
|
live_edit_label.title =
|
|
'When this is on, the editor will update the note+ whenever you change the text.'
|
|
|
|
live_edit_label.append(live_edit)
|
|
|
|
// select_mode.before(live_edit_label)
|
|
container.append(live_edit_label)
|
|
|
|
this.setupEditors()
|
|
}
|
|
loadAceEditor() {
|
|
if (window.MTB?.ace_loaded) {
|
|
return
|
|
}
|
|
let NEED_PATCH = false
|
|
if (window.ace) {
|
|
shared.infoLogger(
|
|
'A global ace was found in scope not loaded by mtb, this might lead to issues.',
|
|
)
|
|
NEED_PATCH = true
|
|
// window._backupAce = window.ace
|
|
// window.ace = null
|
|
}
|
|
|
|
shared
|
|
.loadScript('/mtb_async/ace/ace.js')
|
|
.then((m) => {
|
|
shared.infoLogger('ACE was loaded', m)
|
|
// window.MTB_ACE = window.ace
|
|
if (!window.MTB) {
|
|
errorLogger(
|
|
"window.MTB not found, this shouldn't happen at this stage",
|
|
)
|
|
window.MTB = {}
|
|
}
|
|
window.MTB.ace_loaded = true
|
|
})
|
|
.catch((e) => {
|
|
errorLogger(e)
|
|
})
|
|
.finally(() => {
|
|
if (NEED_PATCH) {
|
|
// console.log('Patching back window object')
|
|
// window.ace = window._backupAce
|
|
} else {
|
|
// delete window.ace
|
|
}
|
|
})
|
|
}
|
|
restoreNodeState(info) {
|
|
this.html_widget.element.id = `note-plus-${this.uuid}`
|
|
this.setMode('markdown')
|
|
this.setTheme(this.properties.theme)
|
|
this.updateHTML(this.html_widget.value)
|
|
this.updateCSS(this.properties.css)
|
|
if (info?.size) {
|
|
this.setSize(info.size)
|
|
}
|
|
}
|
|
configure(info) {
|
|
super.configure(info)
|
|
infoLogger('Restoring serialized values', info)
|
|
this.restoreNodeState(info)
|
|
// - update view from serialzed data
|
|
}
|
|
onNodeCreated() {
|
|
infoLogger('Node created', this.uuid)
|
|
this.restoreNodeState({})
|
|
// this.html_widget.element.id = `note-plus-${this.uuid}`
|
|
// this.setMode(this.edit_mode_widget.value)
|
|
// this.setTheme(this.theme_widget.value)
|
|
// this.updateHTML(this.html_widget.value) // widget is populated here since we called super
|
|
// this.updateCSS(this.css_widget.value)
|
|
}
|
|
// onRemoved() {
|
|
// infoLogger('Node removed', this?.uuid)
|
|
// }
|
|
getExtraMenuOptions() {
|
|
|
|
const debugItems = window.MTB?.DEBUG
|
|
? [
|
|
{
|
|
content: 'Replace with demo content (debug)',
|
|
callback: () => {
|
|
this.html_widget.value = DEMO_CONTENT
|
|
},
|
|
},
|
|
]
|
|
: []
|
|
|
|
return [
|
|
...debugItems,
|
|
]
|
|
}
|
|
|
|
_setupEditor(editor) {
|
|
this.setTheme(this.properties.theme)
|
|
|
|
editor.setShowPrintMargin(false)
|
|
editor.session.setUseWrapMode(true)
|
|
editor.renderer.setShowGutter(false)
|
|
editor.session.setTabSize(4)
|
|
editor.session.setUseSoftTabs(true)
|
|
editor.setFontSize(14)
|
|
editor.setReadOnly(false)
|
|
editor.setHighlightActiveLine(false)
|
|
editor.setShowFoldWidgets(true)
|
|
return editor
|
|
}
|
|
|
|
setTheme(theme) {
|
|
this.properties.theme = theme
|
|
if (this.html_editor) {
|
|
this.html_editor.setTheme(`ace/theme/${theme}`)
|
|
}
|
|
if (this.css_editor) {
|
|
this.css_editor.setTheme(`ace/theme/${theme}`)
|
|
}
|
|
if (this.quickEditor) {
|
|
this.quickEditor.setTheme(`ace/theme/${theme}`)
|
|
}
|
|
}
|
|
|
|
setMode(mode) {
|
|
if (this.html_editor) {
|
|
this.html_editor.session.setMode(`ace/mode/${mode}`)
|
|
}
|
|
if (this.quickEditor) {
|
|
this.quickEditor.session.setMode(`ace/mode/${mode}`)
|
|
}
|
|
|
|
this.updateHTML(this.html_widget.value)
|
|
}
|
|
setupEditors() {
|
|
infoLogger('NotePlus setupEditor')
|
|
|
|
if (!window.MTB?.ace_loaded) {
|
|
errorLogger('ACE editor not loaded. Cannot set up editors.')
|
|
return
|
|
}
|
|
|
|
if (!this.html_editor) {
|
|
this.html_editor = ace.edit('noteplus-html-editor')
|
|
this._setupEditor(this.html_editor)
|
|
this.html_editor.session.on('change', (_delta) => {
|
|
if (this.live) {
|
|
this.updateHTML(this.html_editor.getValue())
|
|
}
|
|
})
|
|
} else {
|
|
infoLogger('Reusing html editor')
|
|
}
|
|
|
|
if (!this.css_editor) {
|
|
this.css_editor = ace.edit('noteplus-css-editor')
|
|
this.css_editor.session.setMode('ace/mode/css')
|
|
this._setupEditor(this.css_editor)
|
|
this.css_editor.session.on('change', (_delta) => {
|
|
if (this.live) {
|
|
this.updateCSS(this.css_editor.getValue())
|
|
}
|
|
})
|
|
}
|
|
|
|
this.setMode(DEFAULT_MODE)
|
|
|
|
this.html_editor.setValue(this.html_widget.value, -1)
|
|
this.css_editor.setValue(this.properties.css, -1)
|
|
}
|
|
|
|
scopeCss(css, scopeId) {
|
|
return css
|
|
.split('}')
|
|
.map((rule) => {
|
|
if (rule.trim() === '') {
|
|
return ''
|
|
}
|
|
const scopedRule = rule
|
|
.split('{')
|
|
.map((segment, index) => {
|
|
if (index === 0) {
|
|
return `#${scopeId} ${segment.trim()}`
|
|
}
|
|
return `{${segment.trim()}`
|
|
})
|
|
.join(' ')
|
|
return `${scopedRule}}`
|
|
})
|
|
.join('\n')
|
|
}
|
|
getCssDom() {
|
|
const styleTagId = `note-plus-stylesheet-${this.uuid}`
|
|
|
|
let styleTag = document.head.querySelector(`#${styleTagId}`)
|
|
|
|
if (!styleTag) {
|
|
styleTag = document.createElement('style')
|
|
styleTag.type = 'text/css'
|
|
styleTag.id = styleTagId
|
|
document.head.appendChild(styleTag)
|
|
infoLogger(`Creating note-plus-stylesheet-${this.uuid}`, styleTag)
|
|
}
|
|
|
|
return styleTag
|
|
}
|
|
calculateHeight() {
|
|
this.calculated_height = shared.calculateTotalChildrenHeight(
|
|
this.html_widget.element,
|
|
)
|
|
this.setDirtyCanvas(true, true)
|
|
}
|
|
updateCSS(css) {
|
|
infoLogger('NotePlus updateCSS')
|
|
// this.html_widget.element.style = css
|
|
const scopedCss = this.scopeCss(
|
|
`${CSS_RESET}\n${css}`,
|
|
`note-plus-${this.uuid}`,
|
|
)
|
|
|
|
const cssDom = this.getCssDom()
|
|
cssDom.innerHTML = scopedCss
|
|
|
|
this.properties.css = css
|
|
this.calculateHeight()
|
|
infoLogger('NotePlus updateCSS', this.calculated_height)
|
|
// this.setSize(this.computeSize())
|
|
}
|
|
|
|
parserInitiated() {
|
|
if (window.MTB?.mdParser) return true
|
|
return false
|
|
}
|
|
|
|
/** to easilty swap purification methods*/
|
|
purify(content) {
|
|
return DOMPurify.sanitize(content, {
|
|
ADD_TAGS: ['iframe', 'detail', 'summary'],
|
|
})
|
|
}
|
|
|
|
updateHTML(val) {
|
|
if (!this.parserInitiated()) {
|
|
return
|
|
}
|
|
val = val || this.html_widget.value
|
|
|
|
const cleanHTML = this.purify(val)
|
|
|
|
const value = cleanHTML.replaceAll('>', '>').replaceAll('<', '<')
|
|
// .replaceAll('&', '&')
|
|
// .replaceAll('"', '"')
|
|
// .replaceAll(''', "'")
|
|
|
|
this.html_widget.value = value
|
|
|
|
MTB.mdParser.parse(value).then((e) => {
|
|
this.inner.innerHTML = e
|
|
})
|
|
this.calculateHeight()
|
|
}
|
|
|
|
/**
|
|
* Attaches the double-click listener to the preview area.
|
|
*/
|
|
setupDoubleClickEdit() {
|
|
this.html_widget.element.addEventListener('dblclick', (e) => {
|
|
e.stopPropagation()
|
|
e.preventDefault()
|
|
|
|
if (this.isEditing) {
|
|
return
|
|
}
|
|
|
|
if (!window.MTB?.ace_loaded) {
|
|
errorLogger('Ace editor not loaded. Cannot open quick edit.')
|
|
return
|
|
}
|
|
|
|
this.enterEditMode()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Switches the preview div to an editable textarea.
|
|
*/
|
|
enterEditMode() {
|
|
this.isEditing = true
|
|
|
|
const id = `noteplus-quick-editor-${this.uuid}`
|
|
this.quickEditorContainer = mtb_ui.makeElement(`div.#${id}`, {
|
|
position: 'absolute',
|
|
top: '0',
|
|
pointerEvents: 'auto',
|
|
left: '0',
|
|
right: '0',
|
|
bottom: '0',
|
|
zIndex: '10',
|
|
})
|
|
|
|
// hide the preview div and append the editor container
|
|
this.inner.style.display = 'none'
|
|
this.html_widget.element.appendChild(this.quickEditorContainer)
|
|
|
|
// init ace
|
|
this.quickEditor = ace.edit(this.quickEditorContainer)
|
|
this._setupEditor(this.quickEditor)
|
|
this.quickEditor.session.setMode('ace/mode/markdown')
|
|
this.quickEditor.setTheme(`ace/theme/${this.properties.theme}`)
|
|
|
|
this.quickEditor.setValue(this._raw_html, -1)
|
|
this.quickEditor.focus()
|
|
|
|
this.quickEditor.session.on('change', () => {
|
|
this.updateHTML(this.quickEditor.getValue())
|
|
})
|
|
|
|
this.quickEditor.textInput
|
|
.getElement()
|
|
.addEventListener('blur', () => this.exitEditMode(true))
|
|
|
|
this.quickEditor.textInput.getElement().addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault()
|
|
this.exitEditMode(true)
|
|
} else if (e.key === 'Escape') {
|
|
e.preventDefault()
|
|
this.exitEditMode(false)
|
|
}
|
|
})
|
|
|
|
this.quickEditor.resize()
|
|
this.setDirtyCanvas(true, true)
|
|
}
|
|
|
|
/**
|
|
* Switches the editable textarea back to the preview div.
|
|
* @param {boolean} saveChanges - Whether to save the textarea content.
|
|
*/
|
|
exitEditMode(saveChanges) {
|
|
if (!this.isEditing) {
|
|
return
|
|
}
|
|
|
|
this.isEditing = false
|
|
|
|
if (this.quickEditor) {
|
|
if (saveChanges) {
|
|
const newValue = this.quickEditor.getValue()
|
|
if (newValue !== this._raw_html) {
|
|
this._raw_html = newValue
|
|
this.updateHTML(newValue)
|
|
}
|
|
}
|
|
this.quickEditor.destroy()
|
|
this.quickEditor = null
|
|
}
|
|
|
|
if (this.quickEditorContainer && this.quickEditorContainer.parentNode) {
|
|
this.quickEditorContainer.parentNode.removeChild(
|
|
this.quickEditorContainer,
|
|
)
|
|
this.quickEditorContainer = null
|
|
}
|
|
|
|
this.inner.style.display = ''
|
|
this.setDirtyCanvas(true, true)
|
|
this.calculateHeight()
|
|
}
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: 'mtb.noteplus',
|
|
settings: [
|
|
{
|
|
id: 'mtb.noteplus.use-shiki',
|
|
category: ['mtb', 'Note+', 'use-shiki'],
|
|
name: 'Use shiki to highlight code',
|
|
tooltip:
|
|
'This will load a larger version of mtb/markdown-parser that bundles shiki, it supports all shiki transformers (supported langs: html,css,python,markdown)',
|
|
|
|
type: 'boolean',
|
|
defaultValue: false,
|
|
attrs: {
|
|
style: {
|
|
// fontFamily: 'monospace',
|
|
},
|
|
},
|
|
async onChange(value) {
|
|
storage.set('np-use-shiki', value)
|
|
useShiki = value === 'true'
|
|
},
|
|
},
|
|
],
|
|
|
|
registerCustomNodes() {
|
|
LiteGraph.registerNodeType('Note Plus (mtb)', NotePlus)
|
|
|
|
NotePlus.category = 'mtb/utils'
|
|
NotePlus.title = 'Note+ (mtb)'
|
|
|
|
},
|
|
})
|