🧩Typora 结合 LightBox 放大查看图片


Typoraopen in new window默认是没有点击图片放大功能的,图片一旦插入,最大宽度只能是页面宽度,而无法放大查看,有时,原图片比较大,放到 Typora 中,完全无法看清内容,比如要看截图中的代码

这时就需要我们自己配置。

这里使用LightBox,来实现类似 WPS 放大预览图片的功能。

从 Github 下载 LightBox

LightBoxopen in new window

image-20230516223833762

拷贝 dist 的文件

将 dist 文件夹下的内容拷贝到 Typora 安装目录的对应路径下

image-20230516224234229

修改 window.html 文件

window.html这个文件实际就是 Typora 的主界面!!

image-20230516224310774

image-20230516224347762

引入 CSS 文件

搜索</head>,放在其前面即可

<link rel="stylesheet" href="./extend/lightbox2/css/lightbox.min.css" crossorigin="anonymous" />

image-20230516224449159

引入 JS 文件

搜索<script src="./appsrc/window/frame.js" defer="defer"></script>,放在其后面即可

<script type="text/javascript" src="./extend/lightbox2/js/lightbox.js" defer="defer"></script>

image-20230516224543185

修改 lightbox.js 文件

因为 lightbox 需要有 a 标签包围着 img 标签,相应的点击事件是绑定在 a 标签上的,且需要 hrefdata-lightboxdata-alt 等属性,而 Typora 中的图片只有 img 标签,父级没有 a 标签,也没有相应要求的属性。因此需要改造一下 lightbox.js 文件。

image-20230516224756785

为 img 标签绑定双击事件(修改函数)

搜索Lightbox.prototype.enable,在其后添加代码

//为img绑定双击事件,但要排除本就是双击放大展示的图片
$('body').on('dblclick', "img:not([class='lb-image'])", function (event) {
  self.start($(event.currentTarget))
  return false
})

image-20230516224953170

lightbox 用到三个属性:althreftitle,这三个属性在 img 标签中均有对应的。 修改 start 函数,换成 img 对应属性:

搜索Lightbox.prototype.start,添加以下代码

function addToAlbum($link) {
  self.album.push({
    alt: $link.attr('data-alt') || $link.attr('alt'),
    link: $link.attr('href') || $link.attr('src'),
    title: $link.attr('data-title') || $link.attr('title')
  })
}

image-20230516225042133

注释 data-lightbox

还有另外一个属性data-lightbox,img 没有该属性,因此这里不使用,lightbox 中有一段判断是否有该属性的代码,修改不存在该属性时的逻辑代码(注释掉原有的,添加addToAlbum($link);): 搜索addToAlbum($link);

image-20230516225115241

调整图片位置居中

此时图片是位于最上面的,修改\extend\lightbox2\css\lightbox.min.css中的.lb-image,给顶部和底部预留 20%空间即可

image-20230516225314504

修改完毕,保存即可。

添加滚轮滑动放大和双击关闭功能

将以下代码直接替换到lightbox.js即可

// Uses Node, AMD or browser globals to create a module.
;(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['jquery'], factory)
  } else if (typeof exports === 'object') {
    // Node. Does not work with strict CommonJS, but
    // only CommonJS-like environments that support module.exports,
    // like Node.
    module.exports = factory(require('jquery'))
  } else {
    // Browser globals (root is window)
    root.lightbox = factory(root.jQuery)
  }
})(this, function ($) {
  function Lightbox(options) {
    this.album = []
    this.currentImageIndex = void 0
    this.init()

    // options
    this.options = $.extend({}, this.constructor.defaults)
    this.option(options)
  }

  // Descriptions of all options available on the demo site:
  // http://lokeshdhakar.com/projects/lightbox2/index.html#options
  Lightbox.defaults = {
    albumLabel: 'Image %1 of %2',
    alwaysShowNavOnTouchDevices: false,
    fadeDuration: 0,
    fitImagesInViewport: true,
    imageFadeDuration: 0,
    // maxWidth: 800,
    // maxHeight: 600,
    positionFromTop: 0,
    resizeDuration: 0,
    showImageNumberLabel: true,
    wrapAround: false,
    disableScrolling: false,
    /*
Sanitize Title
If the caption data is trusted, for example you are hardcoding it in, then leave this to false.
This will free you to add html tags, such as links, in the caption.

If the caption data is user submitted or from some other untrusted source, then set this to true
to prevent xss and other injection attacks.
 */
    sanitizeTitle: false
  }

  Lightbox.prototype.option = function (options) {
    $.extend(this.options, options)
  }

  Lightbox.prototype.imageCountLabel = function (currentImageNum, totalImages) {
    return this.options.albumLabel.replace(/%1/g, currentImageNum).replace(/%2/g, totalImages)
  }

  Lightbox.prototype.init = function () {
    var self = this
    // Both enable and build methods require the body tag to be in the DOM.
    $(document).ready(function () {
      self.enable()
      self.build()
    })
  }

  // Loop through anchors and areamaps looking for either data-lightbox attributes or rel attributes
  // that contain 'lightbox'. When these are clicked, start lightbox.
  Lightbox.prototype.enable = function () {
    var self = this
    $('body').on('click', 'a[rel^=lightbox], area[rel^=lightbox], a[data-lightbox], area[data-lightbox]', function (event) {
      self.start($(event.currentTarget))
      return false
    })

    //为img绑定双击事件,但要排除本就是双击放大展示的图片
    $('body').on('dblclick', "img:not([class='lb-image'])", function (event) {
      self.start($(event.currentTarget))
      self.$image.css('zoom', '100%')
      return false
    })
  }

  // Build html for the lightbox and the overlay.
  // Attach event handlers to the new DOM elements. click click click
  Lightbox.prototype.build = function () {
    if ($('#lightbox').length > 0) {
      return
    }

    var self = this

    // The two root notes generated, #lightboxOverlay and #lightbox are given
    // tabindex attrs so they are focusable. We attach our keyboard event
    // listeners to these two elements, and not the document. Clicking anywhere
    // while Lightbox is opened will keep the focus on or inside one of these
    // two elements.
    //
    // We do this so we can prevent propogation of the Esc keypress when
    // Lightbox is open. This prevents it from intefering with other components
    // on the page below.
    //
    // Github issue: https://github.com/lokesh/lightbox2/issues/663
    $(
      '<div id="lightboxOverlay" tabindex="-1" class="lightboxOverlay"></div><div id="lightbox" tabindex="-1" class="lightbox"><div class="lb-outerContainer"><div class="lb-container"><img class="lb-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" alt=""/><div class="lb-nav"><a class="lb-prev" aria-label="Previous image" href="" ></a><a class="lb-next" aria-label="Next image" href="" ></a></div><div class="lb-loader"><a class="lb-cancel"></a></div></div></div><div class="lb-dataContainer"><div class="lb-data"><div class="lb-details"><span class="lb-caption"></span><span class="lb-number"></span></div><div class="lb-closeContainer"><a class="lb-close"></a></div></div></div></div>'
    ).appendTo($('body'))

    // Cache jQuery objects
    this.$lightbox = $('#lightbox')
    this.$overlay = $('#lightboxOverlay')
    this.$outerContainer = this.$lightbox.find('.lb-outerContainer')
    this.$container = this.$lightbox.find('.lb-container')
    this.$image = this.$lightbox.find('.lb-image')
    this.$nav = this.$lightbox.find('.lb-nav')

    // Store css values for future lookup
    this.containerPadding = {
      top: parseInt(this.$container.css('padding-top'), 10),
      right: parseInt(this.$container.css('padding-right'), 10),
      bottom: parseInt(this.$container.css('padding-bottom'), 10),
      left: parseInt(this.$container.css('padding-left'), 10)
    }

    this.imageBorderWidth = {
      top: parseInt(this.$image.css('border-top-width'), 10),
      right: parseInt(this.$image.css('border-right-width'), 10),
      bottom: parseInt(this.$image.css('border-bottom-width'), 10),
      left: parseInt(this.$image.css('border-left-width'), 10)
    }

    // Attach event handlers to the newly minted DOM elements
    this.$overlay.hide().on('click', function () {
      self.end()
      return false
    })

    this.$lightbox.hide().on('click', function (event) {
      if ($(event.target).attr('id') === 'lightbox') {
        self.end()
      }
    })

    this.$outerContainer.on('click', function (event) {
      if ($(event.target).attr('id') === 'lightbox') {
        self.end()
      }
      return false
    })

    this.$lightbox.find('.lb-prev').on('click', function () {
      if (self.currentImageIndex === 0) {
        self.changeImage(self.album.length - 1)
      } else {
        self.changeImage(self.currentImageIndex - 1)
      }
      return false
    })

    this.$lightbox.find('.lb-next').on('click', function () {
      if (self.currentImageIndex === self.album.length - 1) {
        self.changeImage(0)
      } else {
        self.changeImage(self.currentImageIndex + 1)
      }
      return false
    })

    /*
  Show context menu for image on right-click

  There is a div containing the navigation that spans the entire image and lives above of it. If
  you right-click, you are right clicking this div and not the image. This prevents users from
  saving the image or using other context menu actions with the image.

  To fix this, when we detect the right mouse button is pressed down, but not yet clicked, we
  set pointer-events to none on the nav div. This is so that the upcoming right-click event on
  the next mouseup will bubble down to the image. Once the right-click/contextmenu event occurs
  we set the pointer events back to auto for the nav div so it can capture hover and left-click
  events as usual.
 */
    this.$nav.on('mousedown', function (event) {
      if (event.which === 3) {
        self.$nav.css('pointer-events', 'none')

        self.$lightbox.one('contextmenu', function () {
          setTimeout(
            function () {
              this.$nav.css('pointer-events', 'auto')
            }.bind(self),
            0
          )
        })
      }
    })
    this.$container.on('mousewheel', function (event) {
      event.delta = event.originalEvent.wheelDelta ? event.originalEvent.wheelDelta / 120 : -(event.originalEvent.detail || 0) / 3

      var zoom = self.$image.css('zoom')
      zoom = zoom ? zoom * 100 : 100
      zoom += event.delta * 10
      if (zoom > 0) self.$image.css('zoom', zoom + '%')

      console.log(self.$image.width(), self.$image.height())

      imageWidth = self.$image.width()
      imageHeight = self.$image.height()
      imageWidth = imageWidth * zoom * 0.01
      imageHeight = imageHeight * zoom * 0.01
      self.sizeContainer(imageWidth, imageHeight)
      return false
    })

    //给放大的图片另外绑定双击关闭事件
    this.$container.on('dblclick', function (event) {
      self.end()
      return false
    })

    this.$lightbox.find('.lb-loader, .lb-close').on('click', function () {
      self.end()
      return false
    })
  }

  // Show overlay and lightbox. If the image is part of a set, add siblings to album array.
  Lightbox.prototype.start = function ($link) {
    var self = this
    var $window = $(window)

    $window.on('resize', $.proxy(this.sizeOverlay, this))

    this.sizeOverlay()

    this.album = []
    var imageNumber = 0

    function addToAlbum($link) {
      self.album.push({
        alt: $link.attr('data-alt') || $link.attr('alt'),
        link: $link.attr('href') || $link.attr('src'),
        title: $link.attr('data-title') || $link.attr('title')
      })
    }

    // Support both data-lightbox attribute and rel attribute implementations
    var dataLightboxValue = $link.attr('data-lightbox')
    var $links

    if (dataLightboxValue) {
      $links = $($link.prop('tagName') + '[data-lightbox="' + dataLightboxValue + '"]')
      for (var i = 0; i < $links.length; i = ++i) {
        addToAlbum($($links[i]))
        if ($links[i] === $link[0]) {
          imageNumber = i
        }
      }
    } else {
      // if ($link.attr('rel') === 'lightbox') {
      // If image is not part of a set
      // addToAlbum($link);
      // } else {
      // // If image is part of a set
      // $links = $($link.prop('tagName') + '[rel="' + $link.attr('rel') + '"]');
      // for (var j = 0; j < $links.length; j = ++j) {
      //   addToAlbum($($links[j]));
      //   if ($links[j] === $link[0]) {
      //     imageNumber = j;
      //   }
      // }
      addToAlbum($link)
      // }
    }

    // Position Lightbox
    var top = $window.scrollTop() + this.options.positionFromTop
    var left = $window.scrollLeft()
    this.$lightbox
      .css({
        top: top + 'px',
        left: left + 'px'
      })
      .fadeIn(this.options.fadeDuration)

    // Disable scrolling of the page while open
    if (this.options.disableScrolling) {
      $('body').addClass('lb-disable-scrolling')
    }

    this.changeImage(imageNumber)
  }

  // Hide most UI elements in preparation for the animated resizing of the lightbox.
  Lightbox.prototype.changeImage = function (imageNumber) {
    var self = this
    var filename = this.album[imageNumber].link
    var filetype = filename.split('.').slice(-1)[0]
    var $image = this.$lightbox.find('.lb-image')

    // Disable keyboard nav during transitions
    this.disableKeyboardNav()

    // Show loading state
    this.$overlay.fadeIn(this.options.fadeDuration)
    $('.lb-loader').fadeIn('slow')
    this.$lightbox.find('.lb-image, .lb-nav, .lb-prev, .lb-next, .lb-dataContainer, .lb-numbers, .lb-caption').hide()
    this.$outerContainer.addClass('animating')

    // When image to show is preloaded, we send the width and height to sizeContainer()
    var preloader = new Image()
    preloader.onload = function () {
      var $preloader
      var imageHeight
      var imageWidth
      var maxImageHeight
      var maxImageWidth
      var windowHeight
      var windowWidth

      $image.attr({
        alt: self.album[imageNumber].alt,
        src: filename
      })

      $preloader = $(preloader)

      $image.width(preloader.width)
      $image.height(preloader.height)
      windowWidth = $(window).width()
      windowHeight = $(window).height()

      // Calculate the max image dimensions for the current viewport.
      // Take into account the border around the image and an additional 10px gutter on each side.
      maxImageWidth = windowWidth - self.containerPadding.left - self.containerPadding.right - self.imageBorderWidth.left - self.imageBorderWidth.right - 20
      maxImageHeight = windowHeight - self.containerPadding.top - self.containerPadding.bottom - self.imageBorderWidth.top - self.imageBorderWidth.bottom - self.options.positionFromTop - 70

      /*
  Since many SVGs have small intrinsic dimensions, but they support scaling
  up without quality loss because of their vector format, max out their
  size.
  */
      if (filetype === 'svg') {
        $image.width(maxImageWidth)
        $image.height(maxImageHeight)
      }

      // Fit image inside the viewport.
      if (self.options.fitImagesInViewport) {
        // Check if image size is larger then maxWidth|maxHeight in settings
        if (self.options.maxWidth && self.options.maxWidth < maxImageWidth) {
          maxImageWidth = self.options.maxWidth
        }
        if (self.options.maxHeight && self.options.maxHeight < maxImageHeight) {
          maxImageHeight = self.options.maxHeight
        }
      } else {
        maxImageWidth = self.options.maxWidth || preloader.width || maxImageWidth
        maxImageHeight = self.options.maxHeight || preloader.height || maxImageHeight
      }

      // Is the current image's width or height is greater than the maxImageWidth or maxImageHeight
      // option than we need to size down while maintaining the aspect ratio.
      if (preloader.width > maxImageWidth || preloader.height > maxImageHeight) {
        if (preloader.width / maxImageWidth > preloader.height / maxImageHeight) {
          imageWidth = maxImageWidth
          imageHeight = parseInt(preloader.height / (preloader.width / imageWidth), 10)
          $image.width(imageWidth)
          $image.height(imageHeight)
        } else {
          imageHeight = maxImageHeight
          imageWidth = parseInt(preloader.width / (preloader.height / imageHeight), 10)
          $image.width(imageWidth)
          $image.height(imageHeight)
        }
      }
      self.sizeContainer($image.width(), $image.height())
    }

    // Preload image before showing
    preloader.src = this.album[imageNumber].link
    this.currentImageIndex = imageNumber
  }

  // Stretch overlay to fit the viewport
  Lightbox.prototype.sizeOverlay = function () {
    var self = this
    /*
We use a setTimeout 0 to pause JS execution and let the rendering catch-up.
Why do this? If the `disableScrolling` option is set to true, a class is added to the body
tag that disables scrolling and hides the scrollbar. We want to make sure the scrollbar is
hidden before we measure the document width, as the presence of the scrollbar will affect the
number.
*/
    setTimeout(function () {
      self.$overlay.width($(document).width()).height($(document).height())
    }, 0)
  }

  // Animate the size of the lightbox to fit the image we are showing
  // This method also shows the the image.
  Lightbox.prototype.sizeContainer = function (imageWidth, imageHeight) {
    var self = this

    var oldWidth = this.$outerContainer.outerWidth()
    var oldHeight = this.$outerContainer.outerHeight()
    var newWidth = imageWidth + this.containerPadding.left + this.containerPadding.right + this.imageBorderWidth.left + this.imageBorderWidth.right
    var newHeight = imageHeight + this.containerPadding.top + this.containerPadding.bottom + this.imageBorderWidth.top + this.imageBorderWidth.bottom

    //最大不超过屏幕宽高,若超过,滚动条出现
    windowWidth = $(window).width()
    windowHeight = $(window).height()
    if (newWidth > windowWidth) {
      newWidth = windowWidth
    }
    if (newHeight + self.$lightbox.find('.lb-dataContainer').height() > windowHeight) {
      newHeight = windowHeight - self.$lightbox.find('.lb-dataContainer').height() * 2
    }

    function postResize() {
      self.$lightbox.find('.lb-dataContainer').width(newWidth)
      self.$lightbox.find('.lb-prevLink').height(newHeight)
      self.$lightbox.find('.lb-nextLink').height(newHeight)

      // Set focus on one of the two root nodes so keyboard events are captured.
      self.$overlay.focus()

      self.showImage()
    }

    if (oldWidth !== newWidth || oldHeight !== newHeight) {
      this.$outerContainer.animate(
        {
          width: newWidth,
          height: newHeight
        },
        this.options.resizeDuration,
        'swing',
        function () {
          postResize()
        }
      )
    } else {
      postResize()
    }
  }

  // Display the image and its details and begin preload neighboring images.
  Lightbox.prototype.showImage = function () {
    this.$lightbox.find('.lb-loader').stop(true).hide()
    this.$lightbox.find('.lb-image').fadeIn(this.options.imageFadeDuration)

    this.updateNav()
    this.updateDetails()
    this.preloadNeighboringImages()
    this.enableKeyboardNav()
  }

  // Display previous and next navigation if appropriate.
  Lightbox.prototype.updateNav = function () {
    // Check to see if the browser supports touch events. If so, we take the conservative approach
    // and assume that mouse hover events are not supported and always show prev/next navigation
    // arrows in image sets.
    var alwaysShowNav = false
    try {
      document.createEvent('TouchEvent')
      alwaysShowNav = this.options.alwaysShowNavOnTouchDevices ? true : false
    } catch (e) {}

    this.$lightbox.find('.lb-nav').show()

    if (this.album.length > 1) {
      if (this.options.wrapAround) {
        if (alwaysShowNav) {
          this.$lightbox.find('.lb-prev, .lb-next').css('opacity', '1')
        }
        this.$lightbox.find('.lb-prev, .lb-next').show()
      } else {
        if (this.currentImageIndex > 0) {
          this.$lightbox.find('.lb-prev').show()
          if (alwaysShowNav) {
            this.$lightbox.find('.lb-prev').css('opacity', '1')
          }
        }
        if (this.currentImageIndex < this.album.length - 1) {
          this.$lightbox.find('.lb-next').show()
          if (alwaysShowNav) {
            this.$lightbox.find('.lb-next').css('opacity', '1')
          }
        }
      }
    }
  }

  // Display caption, image number, and closing button.
  Lightbox.prototype.updateDetails = function () {
    var self = this

    // Enable anchor clicks in the injected caption html.
    // Thanks Nate Wright for the fix. @https://github.com/NateWr
    if (typeof this.album[this.currentImageIndex].title !== 'undefined' && this.album[this.currentImageIndex].title !== '') {
      var $caption = this.$lightbox.find('.lb-caption')
      if (this.options.sanitizeTitle) {
        $caption.text(this.album[this.currentImageIndex].title)
      } else {
        $caption.html(this.album[this.currentImageIndex].title)
      }
      $caption.fadeIn('fast')
    }

    if (this.album.length > 1 && this.options.showImageNumberLabel) {
      var labelText = this.imageCountLabel(this.currentImageIndex + 1, this.album.length)
      this.$lightbox.find('.lb-number').text(labelText).fadeIn('fast')
    } else {
      this.$lightbox.find('.lb-number').hide()
    }

    this.$outerContainer.removeClass('animating')

    this.$lightbox.find('.lb-dataContainer').fadeIn(this.options.resizeDuration, function () {
      return self.sizeOverlay()
    })
  }

  // Preload previous and next images in set.
  Lightbox.prototype.preloadNeighboringImages = function () {
    if (this.album.length > this.currentImageIndex + 1) {
      var preloadNext = new Image()
      preloadNext.src = this.album[this.currentImageIndex + 1].link
    }
    if (this.currentImageIndex > 0) {
      var preloadPrev = new Image()
      preloadPrev.src = this.album[this.currentImageIndex - 1].link
    }
  }

  Lightbox.prototype.enableKeyboardNav = function () {
    this.$lightbox.on('keyup.keyboard', $.proxy(this.keyboardAction, this))
    this.$overlay.on('keyup.keyboard', $.proxy(this.keyboardAction, this))
  }

  Lightbox.prototype.disableKeyboardNav = function () {
    this.$lightbox.off('.keyboard')
    this.$overlay.off('.keyboard')
  }

  Lightbox.prototype.keyboardAction = function (event) {
    var KEYCODE_ESC = 27
    var KEYCODE_LEFTARROW = 37
    var KEYCODE_RIGHTARROW = 39

    var keycode = event.keyCode
    if (keycode === KEYCODE_ESC) {
      // Prevent bubbling so as to not affect other components on the page.
      event.stopPropagation()
      this.end()
    } else if (keycode === KEYCODE_LEFTARROW) {
      if (this.currentImageIndex !== 0) {
        this.changeImage(this.currentImageIndex - 1)
      } else if (this.options.wrapAround && this.album.length > 1) {
        this.changeImage(this.album.length - 1)
      }
    } else if (keycode === KEYCODE_RIGHTARROW) {
      if (this.currentImageIndex !== this.album.length - 1) {
        this.changeImage(this.currentImageIndex + 1)
      } else if (this.options.wrapAround && this.album.length > 1) {
        this.changeImage(0)
      }
    }
  }

  // Closing time. :-(
  Lightbox.prototype.end = function () {
    this.disableKeyboardNav()
    $(window).off('resize', this.sizeOverlay)
    this.$lightbox.fadeOut(this.options.fadeDuration)
    this.$overlay.fadeOut(this.options.fadeDuration)

    if (this.options.disableScrolling) {
      $('body').removeClass('lb-disable-scrolling')
    }
  }

  return new Lightbox()
})

image-20230516225619076

验证

重启 Typora,双击图片,可放大即配置成功。