846 lines
23 KiB
JavaScript
846 lines
23 KiB
JavaScript
/// <reference path="../types/typedefs.js" />
|
|
|
|
import { app } from '../../scripts/app.js'
|
|
|
|
import * as shared from './comfy_shared.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
|
|
|
|
/** hidden widgets for serialization*/
|
|
css_widget
|
|
edit_mode_widget
|
|
theme_widget
|
|
|
|
editorsContainer
|
|
/** ACE editors instances*/
|
|
html_editor
|
|
css_editor
|
|
|
|
constructor() {
|
|
super()
|
|
this.uuid = shared.makeUUID()
|
|
|
|
infoLogger('Constructing Note+ instance')
|
|
shared.ensureMarkdownParser((_p) => {
|
|
this.updateHTML()
|
|
})
|
|
// - litegraph settings
|
|
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_MODE === 'html' ? DEFAULT_HTML : DEFAULT_MD
|
|
|
|
// - state
|
|
this.live = true
|
|
this.calculated_height = 0
|
|
|
|
// - add widgets
|
|
const cinner = document.createElement('div')
|
|
this.inner = document.createElement('div')
|
|
|
|
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...
|
|
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.setupSerializationWidgets()
|
|
this.setupDialog()
|
|
this.loadAceEditor()
|
|
}
|
|
|
|
/**
|
|
* @param {CanvasRenderingContext2D} ctx canvas context
|
|
* @param {any} _graphcanvas
|
|
*/
|
|
onDrawForeground(ctx, _graphcanvas) {
|
|
if (this.flags.collapsed) return
|
|
this.drawEditIcon(ctx)
|
|
this.drawSideHandle(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) {
|
|
const handleRect = this.sideHandleRect
|
|
const chamfer = 20
|
|
ctx.beginPath()
|
|
|
|
// top left
|
|
ctx.moveTo(handleRect.x, handleRect.y + chamfer)
|
|
// top right
|
|
ctx.lineTo(handleRect.x + handleRect.width, handleRect.y)
|
|
|
|
// bottom right
|
|
ctx.lineTo(
|
|
handleRect.x + handleRect.width,
|
|
handleRect.y + handleRect.height,
|
|
)
|
|
// bottom left
|
|
ctx.lineTo(handleRect.x, handleRect.y + handleRect.height - chamfer)
|
|
ctx.closePath()
|
|
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.05)'
|
|
ctx.fill()
|
|
}
|
|
|
|
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 sideHandleRect() {
|
|
const w = this.size[0]
|
|
const h = this.size[1]
|
|
|
|
const bw = 32
|
|
const bho = 64
|
|
|
|
return {
|
|
x: w - bw,
|
|
y: bho,
|
|
width: bw,
|
|
height: h - bho * 1.5,
|
|
}
|
|
}
|
|
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)
|
|
})
|
|
}
|
|
}
|
|
|
|
return {
|
|
x: this.size[0] - icon.size - icon.margin,
|
|
y: icon.yoffset, //iconMargin * 1.5,
|
|
width: icon.size,
|
|
height: icon.size,
|
|
}
|
|
}
|
|
onMouseDown(_e, localPos, _graphcanvas) {
|
|
if (this.inRect(localPos[0], localPos[1])) {
|
|
this.openEditorDialog()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/* Hidden widgets to store note+ settings in the workflow (stripped in API)*/
|
|
setupSerializationWidgets() {
|
|
infoLogger('Setup Serializing widgets')
|
|
|
|
this.edit_mode_widget = this.addWidget(
|
|
'combo',
|
|
'Mode',
|
|
DEFAULT_MODE,
|
|
(me) => successLogger('Updating edit_mode', me),
|
|
{
|
|
values: ['html', 'markdown', 'raw'],
|
|
},
|
|
)
|
|
this.css_widget = this.addWidget('text', 'CSS', DEFAULT_CSS, (val) => {
|
|
successLogger(`Updating css ${val}`)
|
|
})
|
|
this.theme_widget = this.addWidget(
|
|
'text',
|
|
'Theme',
|
|
DEFAULT_THEME,
|
|
(val) => {
|
|
successLogger(`Setting theme ${val}`)
|
|
},
|
|
)
|
|
|
|
shared.hideWidgetForGood(this, this.edit_mode_widget)
|
|
shared.hideWidgetForGood(this, this.css_widget)
|
|
shared.hideWidgetForGood(this, this.theme_widget)
|
|
}
|
|
|
|
setupDialog() {
|
|
infoLogger('Setup dialog')
|
|
|
|
this.dialog = new app.ui.dialog.constructor()
|
|
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()
|
|
}
|
|
|
|
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 = () => {
|
|
this.html_editor.resize()
|
|
this.css_editor.resize()
|
|
Object.assign(this.editorsContainer.style, {
|
|
minHeight: `${(this.dialog.element.clientHeight / 100) * 50}px`, //'200px',
|
|
})
|
|
}
|
|
this.resizeObserver = new ResizeObserver(observer).observe(elem)
|
|
}
|
|
}
|
|
openEditorDialog() {
|
|
infoLogger(`Current edit mode ${this.edit_mode_widget.value}`)
|
|
this.hookResize(this.dialog.element)
|
|
const container = document.createElement('div')
|
|
Object.assign(container.style, {
|
|
display: 'flex',
|
|
gap: '10px',
|
|
flexDirection: 'column',
|
|
})
|
|
|
|
this.editorsContainer = document.createElement('div')
|
|
|
|
Object.assign(this.editorsContainer.style, {
|
|
display: 'flex',
|
|
gap: '10px',
|
|
flexDirection: 'row',
|
|
minHeight: this.dialog.element.offsetHeight, //'200px',
|
|
width: '100%',
|
|
})
|
|
|
|
container.append(this.editorsContainer)
|
|
|
|
this.dialog.show('')
|
|
this.dialog.textElement.append(container)
|
|
|
|
const aceHTML = document.createElement('div')
|
|
aceHTML.id = 'noteplus-html-editor'
|
|
Object.assign(aceHTML.style, {
|
|
width: '100%',
|
|
height: '100%',
|
|
|
|
minWidth: '300px',
|
|
minHeight: 'inherit',
|
|
})
|
|
|
|
this.editorsContainer.append(aceHTML)
|
|
|
|
const aceCSS = document.createElement('div')
|
|
aceCSS.id = 'noteplus-css-editor'
|
|
Object.assign(aceCSS.style, {
|
|
width: '100%',
|
|
height: '100%',
|
|
minHeight: 'inherit',
|
|
})
|
|
|
|
this.editorsContainer.append(aceCSS)
|
|
|
|
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 firstButton = this.dialog.element.querySelector('button')
|
|
const syncUI = () => {
|
|
let convert_to_html =
|
|
this.dialog.element.querySelector('#convert-to-html')
|
|
if (this.edit_mode_widget.value === 'markdown') {
|
|
if (convert_to_html == null) {
|
|
convert_to_html = document.createElement('button')
|
|
convert_to_html.textContent = 'Convert to HTML (NO UNDO!)'
|
|
convert_to_html.id = 'convert-to-html'
|
|
convert_to_html.onclick = () => {
|
|
const select_mode = this.dialog.element.querySelector('#edit_mode')
|
|
|
|
const md = this.html_editor.getValue()
|
|
this.edit_mode_widget.value = 'html'
|
|
select_mode.value = 'html'
|
|
MTB.mdParser.parse(md).then((content) => {
|
|
this.html_widget.value = content
|
|
this.html_editor.setValue(content)
|
|
this.html_editor.session.setMode('ace/mode/html')
|
|
this.updateHTML(this.html_widget.value)
|
|
convert_to_html.remove()
|
|
})
|
|
}
|
|
firstButton.before(convert_to_html)
|
|
}
|
|
} else {
|
|
if (convert_to_html != null) {
|
|
convert_to_html.remove()
|
|
convert_to_html = null
|
|
}
|
|
}
|
|
select_mode.value = this.edit_mode_widget.value
|
|
|
|
// the header for dragging the dialog
|
|
const header = document.createElement('div')
|
|
header.style.padding = '8px'
|
|
header.style.cursor = 'move'
|
|
header.style.backgroundColor = 'rgba(0,0,0,0.5)'
|
|
header.style.userSelect = 'none'
|
|
|
|
header.style.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.theme_widget.value
|
|
|
|
let select_mode = this.dialog.element.querySelector('#edit_mode')
|
|
|
|
if (!select_mode) {
|
|
infoLogger('Creating combobox for select')
|
|
select_mode = document.createElement('select')
|
|
select_mode.name = 'mode'
|
|
select_mode.id = 'edit_mode'
|
|
|
|
const addOption = (label) => {
|
|
const option = document.createElement('option')
|
|
option.value = label
|
|
option.textContent = label
|
|
select_mode.append(option)
|
|
}
|
|
addOption('markdown')
|
|
addOption('html')
|
|
|
|
select_mode.addEventListener('change', (event) => {
|
|
const val = event.target.value
|
|
this.edit_mode_widget.value = val
|
|
if (this.html_editor) {
|
|
this.html_editor.session.setMode(`ace/mode/${val}`)
|
|
this.updateHTML(this.html_editor.getValue())
|
|
|
|
syncUI()
|
|
}
|
|
})
|
|
|
|
container.append(select_mode)
|
|
}
|
|
select_mode.value = this.edit_mode_widget.value
|
|
|
|
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() {
|
|
shared.loadScript('/mtb_async/ace/ace.js').catch((e) => {
|
|
errorLogger(e)
|
|
})
|
|
}
|
|
onCreate() {
|
|
errorLogger('NotePlus onCreate')
|
|
}
|
|
restoreNodeState(info) {
|
|
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)
|
|
this.updateCSS(this.css_widget.value)
|
|
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 currentMode = this.edit_mode_widget.value
|
|
const newMode = currentMode === 'html' ? 'markdown' : 'html'
|
|
|
|
const debugItems = window.MTB?.DEBUG
|
|
? [
|
|
{
|
|
content: 'Replace with demo content (debug)',
|
|
callback: () => {
|
|
this.html_widget.value = DEMO_CONTENT
|
|
},
|
|
},
|
|
]
|
|
: []
|
|
|
|
return [
|
|
...debugItems,
|
|
{
|
|
content: `Set to ${newMode}`,
|
|
callback: () => {
|
|
this.edit_mode_widget.value = newMode
|
|
this.updateHTML(this.html_widget.value)
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
_setupEditor(editor) {
|
|
this.setTheme(this.theme_widget.value)
|
|
|
|
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.theme_widget.value = theme
|
|
if (this.html_editor) {
|
|
this.html_editor.setTheme(`ace/theme/${theme}`)
|
|
}
|
|
if (this.css_editor) {
|
|
this.css_editor.setTheme(`ace/theme/${theme}`)
|
|
}
|
|
}
|
|
|
|
setMode(mode) {
|
|
this.edit_mode_widget.value = mode
|
|
if (this.html_editor) {
|
|
this.html_editor.session.setMode(`ace/mode/${mode}`)
|
|
}
|
|
|
|
this.updateHTML(this.html_widget.value)
|
|
}
|
|
setupEditors() {
|
|
infoLogger('NotePlus setupEditor')
|
|
|
|
this.html_editor = ace.edit('noteplus-html-editor')
|
|
|
|
this.css_editor = ace.edit('noteplus-css-editor')
|
|
this.css_editor.session.setMode('ace/mode/css')
|
|
|
|
this.setMode(DEFAULT_MODE)
|
|
|
|
this._setupEditor(this.html_editor)
|
|
this._setupEditor(this.css_editor)
|
|
|
|
this.css_editor.session.on('change', (_delta) => {
|
|
// delta.start, delta.end, delta.lines, delta.action
|
|
if (this.live) {
|
|
this.updateCSS(this.css_editor.getValue())
|
|
}
|
|
})
|
|
|
|
this.html_editor.session.on('change', (_delta) => {
|
|
// delta.start, delta.end, delta.lines, delta.action
|
|
if (this.live) {
|
|
this.updateHTML(this.html_editor.getValue())
|
|
}
|
|
})
|
|
|
|
this.html_editor.setValue(this.html_widget.value)
|
|
this.css_editor.setValue(this.css_widget.value)
|
|
}
|
|
|
|
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.css_widget.value = 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 isHTML = this.edit_mode_widget.value === 'html'
|
|
|
|
const cleanHTML = this.purify(val)
|
|
|
|
const value = isHTML
|
|
? cleanHTML
|
|
: cleanHTML.replaceAll('>', '>').replaceAll('<', '<')
|
|
// .replaceAll('&', '&')
|
|
// .replaceAll('"', '"')
|
|
// .replaceAll(''', "'")
|
|
|
|
this.html_widget.value = value
|
|
|
|
if (isHTML) {
|
|
this.inner.innerHTML = value
|
|
} else {
|
|
MTB.mdParser.parse(value).then((e) => {
|
|
this.inner.innerHTML = e
|
|
})
|
|
}
|
|
// this.html_widget.element.innerHTML = `<div id="note-plus-spacer"></div>${value}`
|
|
this.calculateHeight()
|
|
// this.setSize(this.computeSize())
|
|
}
|
|
}
|
|
|
|
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
|
|
},
|
|
},
|
|
],
|
|
|
|
registerCustomNodes() {
|
|
LiteGraph.registerNodeType('Note Plus (mtb)', NotePlus)
|
|
|
|
NotePlus.category = 'mtb/utils'
|
|
NotePlus.title = 'Note+ (mtb)'
|
|
|
|
NotePlus.title_mode = LiteGraph.NO_TITLE
|
|
},
|
|
})
|