@@ -428,6 +428,12 @@ export function executeFormat(contentEl, command, value) {
428428 case "mermaidBlock" :
429429 insertMermaidBlock ( contentEl ) ;
430430 break ;
431+ case "imageFromUrl" :
432+ showImageUrlDialog ( contentEl ) ;
433+ break ;
434+ case "imageUpload" :
435+ openImageFilePicker ( contentEl ) ;
436+ break ;
431437 }
432438
433439 broadcastSelectionState ( ) ;
@@ -495,6 +501,114 @@ function insertCodeBlock(contentEl) {
495501 } ) ;
496502}
497503
504+ const UPLOAD_PLACEHOLDER_SRC = "https://user-cdn.phcode.site/images/uploading.svg" ;
505+ const ALLOWED_IMAGE_TYPES = [ "image/jpeg" , "image/png" , "image/gif" , "image/webp" , "image/svg+xml" ] ;
506+
507+ function showImageUrlDialog ( contentEl ) {
508+ // Create a simple overlay dialog for entering image URL and alt text
509+ const backdrop = document . createElement ( "div" ) ;
510+ backdrop . className = "confirm-dialog-backdrop" ;
511+ backdrop . innerHTML = `
512+ <div class="confirm-dialog">
513+ <h3 class="confirm-dialog-title">${ t ( "image_dialog.title" ) || "Insert Image URL" } </h3>
514+ <div style="margin-bottom: 12px;">
515+ <input type="text" id="img-url-input" placeholder="${ t ( "image_dialog.url_placeholder" ) || "https://example.com/image.png" } "
516+ style="width: 100%; padding: 6px 8px; border: 1px solid var(--color-border); border-radius: 4px; background: var(--color-bg); color: var(--color-text); margin-bottom: 8px;" />
517+ <input type="text" id="img-alt-input" placeholder="${ t ( "image_dialog.alt_placeholder" ) || "Image description" } "
518+ style="width: 100%; padding: 6px 8px; border: 1px solid var(--color-border); border-radius: 4px; background: var(--color-bg); color: var(--color-text);" />
519+ </div>
520+ <div class="confirm-dialog-buttons">
521+ <button class="confirm-dialog-btn confirm-dialog-btn-cancel" id="img-dialog-cancel">${ t ( "dialog.cancel" ) || "Cancel" } </button>
522+ <button class="confirm-dialog-btn confirm-dialog-btn-save" id="img-dialog-insert">${ t ( "image_dialog.insert" ) || "Insert" } </button>
523+ </div>
524+ </div>` ;
525+ document . body . appendChild ( backdrop ) ;
526+
527+ const urlInput = backdrop . querySelector ( "#img-url-input" ) ;
528+ const altInput = backdrop . querySelector ( "#img-alt-input" ) ;
529+ urlInput . focus ( ) ;
530+
531+ function close ( ) {
532+ backdrop . remove ( ) ;
533+ contentEl . focus ( { preventScroll : true } ) ;
534+ }
535+
536+ backdrop . querySelector ( "#img-dialog-cancel" ) . addEventListener ( "click" , close ) ;
537+ backdrop . querySelector ( "#img-dialog-insert" ) . addEventListener ( "click" , ( ) => {
538+ const url = urlInput . value . trim ( ) ;
539+ const alt = altInput . value . trim ( ) ;
540+ if ( url ) {
541+ close ( ) ;
542+ const imgHtml = `<img src="${ url } " alt="${ alt } ">` ;
543+ document . execCommand ( "insertHTML" , false , imgHtml ) ;
544+ contentEl . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ;
545+ }
546+ } ) ;
547+
548+ // Enter key inserts, Escape cancels
549+ backdrop . addEventListener ( "keydown" , ( e ) => {
550+ if ( e . key === "Enter" ) {
551+ e . preventDefault ( ) ;
552+ backdrop . querySelector ( "#img-dialog-insert" ) . click ( ) ;
553+ } else if ( e . key === "Escape" ) {
554+ e . preventDefault ( ) ;
555+ close ( ) ;
556+ }
557+ } ) ;
558+
559+ // Click on backdrop closes
560+ backdrop . addEventListener ( "mousedown" , ( e ) => {
561+ if ( e . target === backdrop ) {
562+ close ( ) ;
563+ }
564+ } ) ;
565+ }
566+
567+ function _insertUploadPlaceholder ( contentEl ) {
568+ const uploadId = crypto . randomUUID ( ) ;
569+ const imgHtml = `<img src="${ UPLOAD_PLACEHOLDER_SRC } " alt="Uploading..." data-upload-id="${ uploadId } ">` ;
570+ document . execCommand ( "insertHTML" , false , imgHtml ) ;
571+ contentEl . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ;
572+ return uploadId ;
573+ }
574+
575+ function openImageFilePicker ( contentEl ) {
576+ const input = document . createElement ( "input" ) ;
577+ input . type = "file" ;
578+ input . accept = "image/*" ;
579+ input . addEventListener ( "change" , ( ) => {
580+ const file = input . files && input . files [ 0 ] ;
581+ if ( ! file || ! ALLOWED_IMAGE_TYPES . includes ( file . type ) ) {
582+ return ;
583+ }
584+ const uploadId = _insertUploadPlaceholder ( contentEl ) ;
585+ emit ( "bridge:uploadImage" , { blob : file , filename : file . name , uploadId } ) ;
586+ } ) ;
587+ input . click ( ) ;
588+ }
589+
590+ /**
591+ * Handle image paste in the mdviewer editor.
592+ * @return {boolean } true if an image was found and handled
593+ */
594+ function handleImagePaste ( e , contentEl ) {
595+ const items = e . clipboardData && e . clipboardData . items ;
596+ if ( ! items ) {
597+ return false ;
598+ }
599+ for ( let i = 0 ; i < items . length ; i ++ ) {
600+ if ( items [ i ] . kind === "file" && ALLOWED_IMAGE_TYPES . includes ( items [ i ] . type ) ) {
601+ e . preventDefault ( ) ;
602+ const blob = items [ i ] . getAsFile ( ) ;
603+ const fileName = blob . name || ( "image." + blob . type . split ( "/" ) [ 1 ] ) ;
604+ const uploadId = _insertUploadPlaceholder ( contentEl ) ;
605+ emit ( "bridge:uploadImage" , { blob, filename : fileName , uploadId } ) ;
606+ return true ;
607+ }
608+ }
609+ return false ;
610+ }
611+
498612// ——— Table editing helpers ———
499613
500614function getTableContext ( ) {
@@ -1046,6 +1160,11 @@ function sanitizePastedHTML(html) {
10461160}
10471161
10481162function handlePaste ( e , contentEl ) {
1163+ // Check for image paste first — upload to cloud
1164+ if ( handleImagePaste ( e , contentEl ) ) {
1165+ return ;
1166+ }
1167+
10491168 const mod = isModKey ( e ) ;
10501169
10511170 // Inside table cells: paste as single line plain text (newlines break tables)
0 commit comments