|
|
|
|
| import { app } from '../../scripts/app.js'
|
|
|
| import * as shared from './comfy_shared.js'
|
| import * as mtb_ui from './mtb_ui.js'
|
| import { warnLogger, infoLogger, errorLogger } from './comfy_shared.js'
|
| import {
|
| DEFAULT_CSS,
|
|
|
| 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')
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| let _css_reset = app.extensionManager.setting.get(
|
| 'mtb.noteplus.css-reset',
|
| CSS_RESET,
|
| )
|
|
|
| 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)
|
| })
|
| }
|
|
|
|
|
| class NotePlus extends LiteGraph.LGraphNode {
|
|
|
| color = LGraphCanvas.node_colors.yellow.color
|
| bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
|
| groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
|
|
|
| |
| |
|
|
| uuid
|
|
|
|
|
| resizeObserver
|
|
|
|
|
|
|
| calculated_height = 0
|
|
|
|
|
| _raw_html
|
|
|
|
|
| inner
|
|
|
|
|
| dialog
|
|
|
|
|
|
|
|
|
| preview_widget
|
|
|
| editorsContainer
|
|
|
| html_editor
|
| css_editor
|
|
|
|
|
| isEditing = false
|
| quickEditor = null
|
| quickEditorContainer = null
|
|
|
| constructor() {
|
| super()
|
| this.uuid = shared.makeUUID()
|
|
|
| infoLogger('Constructing Note+ instance')
|
| shared.ensureMarkdownParser((_p) => {
|
| this.updateHTML()
|
| })
|
|
|
| this.properties = {
|
| css: DEFAULT_CSS,
|
| theme: DEFAULT_THEME,
|
| }
|
| this.collapsable = true
|
| this.isVirtualNode = true
|
| this.shape = LiteGraph.BOX_SHAPE
|
| this.serialize_widgets = true
|
|
|
|
|
| this._raw_html = DEFAULT_MD
|
|
|
|
|
| this.calculated_height = 0
|
|
|
|
|
| this.setupDebounce(100)
|
|
|
|
|
| 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.preview_widget = this.addDOMWidget('HTML', 'html', cinner, {
|
| setValue: (val) => {
|
| this._raw_html = val
|
| },
|
| getValue: () => this._raw_html,
|
| getMinHeight: () => {
|
| this.calculateHeight()
|
| return this.calculated_height
|
| },
|
|
|
| onDraw: () => {
|
|
|
|
|
| this.preview_widget.element.style.pointerEvents = 'none'
|
|
|
|
|
| },
|
| hideOnZoom: false,
|
| })
|
|
|
| this.setupDialog()
|
| this.loadAceEditor()
|
| this.setupDoubleClickEdit()
|
| }
|
|
|
| |
| |
| |
|
|
| onDrawForeground(ctx, _graphcanvas) {
|
| if (this.flags.collapsed) return
|
| this.drawEditIcon(ctx)
|
|
|
|
|
|
|
|
|
|
|
| }
|
|
|
| setupDebounce(ms) {
|
| if (this.calculatedHeight) {
|
| this.calculateHeight.cancel()
|
| }
|
| this.calculateHeight = shared.debounce(() => {
|
| this.calculated_height = shared.calculateTotalChildrenHeight(
|
| this.inner,
|
| )
|
| }, ms)
|
| }
|
|
|
|
|
|
|
| drawEditIcon(ctx) {
|
| const rect = this.iconRect
|
|
|
|
|
|
|
|
|
| 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()
|
| }
|
| |
| |
| |
| |
| |
|
|
| 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 saveButton = this.dialog.element.querySelector('button')
|
| saveButton.textContent = 'SAVE'
|
| saveButton.onclick = () => {
|
| this.closeEditorDialog(true)
|
| }
|
| }
|
|
|
| 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.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()
|
| this.setupDebounce(100)
|
| }
|
|
|
| |
| |
|
|
| hookResize(elem) {
|
| if (!this.resizeObserver) {
|
| const observer = () => {
|
| Object.assign(this.editorsContainer.style, {
|
| minHeight: `${(this.dialog.element.clientHeight / 100) * 50}px`,
|
| })
|
|
|
|
|
| if (this.html_editor) {
|
| this.html_editor.resize()
|
| this.css_editor.resize()
|
| }
|
| }
|
| this.resizeObserver = new ResizeObserver(observer).observe(elem)
|
| }
|
| }
|
| openEditorDialog() {
|
| this.setupDebounce(500)
|
| 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,
|
| 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 syncUI = () => {
|
|
|
|
|
|
|
| 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)
|
| }
|
|
|
| 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
|
|
|
|
|
|
|
| syncUI()
|
|
|
|
|
| this.setupEditors()
|
| }
|
| loadAceEditor() {
|
| if (window.MTB?.ace_loaded) {
|
| return
|
| }
|
| let NEED_PATCH = false
|
| if (window.ace) {
|
| infoLogger(
|
| 'A global ace was found in scope not loaded by mtb, this might lead to issues.',
|
| )
|
| NEED_PATCH = true
|
|
|
|
|
| }
|
|
|
| shared
|
| .loadScript('/mtb_async/ace/ace.js')
|
| .then((m) => {
|
| infoLogger('ACE was loaded', m)
|
|
|
| if (!window.MTB) {
|
| errorLogger(
|
| "window.MTB not found, this shouldn't happen at this stage",
|
| )
|
| window.MTB = {}
|
| }
|
| window.MTB.ace_loaded = true
|
| this.setMode('markdown')
|
| this.setTheme()
|
| this.updateView()
|
| })
|
| .catch((e) => {
|
| errorLogger(e)
|
| })
|
| .finally(() => {
|
| if (NEED_PATCH) {
|
|
|
|
|
| } else {
|
|
|
| }
|
| })
|
| }
|
| configure(info) {
|
| super.configure(info)
|
| infoLogger('Restoring serialized values', info)
|
| this.preview_widget.element.id = `note-plus-${this.uuid}`
|
| this.updateView()
|
| }
|
| getExtraMenuOptions() {
|
| const debugItems = window.MTB?.DEBUG
|
| ? [
|
| {
|
| content: 'Replace with demo content (debug)',
|
| callback: () => {
|
| this.preview_widget.value = DEMO_CONTENT
|
| },
|
| },
|
| ]
|
| : []
|
|
|
| return [...debugItems]
|
| }
|
|
|
| get fontSize() {
|
| return app.extensionManager.setting.get('Comfy.TextareaWidget.FontSize', 16)
|
| }
|
|
|
| _setupEditor(editor) {
|
| this.setTheme()
|
|
|
| editor.setShowPrintMargin(false)
|
| editor.session.setUseWrapMode(true)
|
| editor.renderer.setShowGutter(false)
|
| editor.session.setTabSize(4)
|
| editor.session.setUseSoftTabs(true)
|
| editor.setFontSize(this.fontSize)
|
| editor.setReadOnly(false)
|
| editor.setHighlightActiveLine(false)
|
| editor.setShowFoldWidgets(true)
|
| return editor
|
| }
|
|
|
| setTheme(theme) {
|
| theme = theme || this.properties.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}`)
|
| }
|
| }
|
|
|
| updateView() {
|
| this.updateHTML()
|
| this.updateCSS()
|
| this.calculateHeight()
|
| }
|
|
|
| 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.updateView()
|
| }
|
|
|
| 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) => {
|
| 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) => {
|
| this.updateCSS(this.css_editor.getValue())
|
| })
|
| }
|
|
|
| this.setMode(DEFAULT_MODE)
|
|
|
| this.html_editor.setValue(this.preview_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
|
| }
|
| updateCSS(css) {
|
| infoLogger('NotePlus updateCSS')
|
| css = css || this.properties.css
|
|
|
| const scopedCss = this.scopeCss(
|
| `${_css_reset}\n${css}`,
|
| `note-plus-${this.uuid}`,
|
| )
|
|
|
| const cssDom = this.getCssDom()
|
| cssDom.innerHTML = scopedCss
|
|
|
| this.properties.css = css
|
| }
|
|
|
| parserInitiated() {
|
| if (window.MTB?.mdParser) return true
|
| return false
|
| }
|
|
|
|
|
| purify(content) {
|
| return DOMPurify.sanitize(content, {
|
| ADD_TAGS: ['iframe', 'detail', 'summary'],
|
| })
|
| }
|
|
|
| updateHTML(val) {
|
| if (val) {
|
| this._raw_html = val
|
| }
|
|
|
| if (
|
| !this.parserInitiated() ||
|
| !window.MTB?.ace_loaded ||
|
| !this.preview_widget
|
| ) {
|
| return
|
| }
|
| val = val || this._raw_html
|
|
|
| const cleanHTML = this.purify(val)
|
|
|
| const value = cleanHTML.replaceAll('>', '>').replaceAll('<', '<')
|
|
|
|
|
|
|
|
|
| this.preview_widget.value = value
|
|
|
| MTB.mdParser
|
| .parse(value)
|
| .then((e) => {
|
| this.inner.innerHTML = e
|
| })
|
| .catch((e) => {
|
| if (e.name === 'ShikiError') {
|
| warnLogger(e.message)
|
| return
|
| }
|
| throw e
|
| })
|
| }
|
|
|
| |
| |
|
|
| setupDoubleClickEdit() {
|
| this.preview_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()
|
| })
|
| }
|
|
|
| |
| |
|
|
| enterEditMode() {
|
| this.isEditing = true
|
|
|
| this.setupDebounce(10000)
|
|
|
| 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',
|
| })
|
|
|
|
|
| this.inner.style.display = 'none'
|
| this.preview_widget.element.appendChild(this.quickEditorContainer)
|
|
|
|
|
| this.quickEditor = ace.edit(this.quickEditorContainer, {
|
| mode: 'ace/mode/markdown',
|
| theme: this.properties.theme,
|
| })
|
|
|
| this.quickEditor.setOptions({
|
| autoScrollEditorIntoView: true,
|
| copyWithEmptySelection: true,
|
| hasCssTransforms: true,
|
| })
|
| this._setupEditor(this.quickEditor)
|
|
|
| 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)
|
| }
|
|
|
| |
| |
| |
|
|
| exitEditMode(saveChanges) {
|
| if (!this.isEditing) {
|
| return
|
| }
|
|
|
| this.isEditing = false
|
| this.setupDebounce(100)
|
|
|
| if (this.quickEditor) {
|
| if (saveChanges) {
|
| const newValue = this.quickEditor.getValue()
|
| if (newValue !== this._raw_html) {
|
| this._raw_html = newValue
|
| this.updateView()
|
| }
|
| }
|
| this.quickEditor.destroy()
|
| this.quickEditor = null
|
| }
|
|
|
| if (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: {
|
|
|
| },
|
| },
|
| },
|
| {
|
| id: 'mtb.noteplus.css-reset',
|
| category: ['mtb', 'Note+', 'css-reset'],
|
| name: 'CSS reset',
|
| tooltip: 'This is prepended to all notes',
|
| type: 'string',
|
| defaultValue: CSS_RESET,
|
| attrs: {
|
| style: {
|
|
|
| },
|
| },
|
| async onChange(value) {
|
| _css_reset = value
|
| },
|
| },
|
| ],
|
|
|
| registerCustomNodes() {
|
| LiteGraph.registerNodeType('Note Plus (mtb)', NotePlus)
|
|
|
| NotePlus.category = 'mtb/utils'
|
| NotePlus.title = 'Note+ (mtb)'
|
| },
|
| })
|
|
|