npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hakulex/editor

v0.4.0

Published

Core hakulex rich text editor for Vue 3

Readme

@hakulex/editor

編輯器本體。外掛都放在 RichEditor 裡面,文件用 v-model 綁定。

hakulex 的一部分:給 Vue 3 的富文本編輯器,建立在 Lexical 上。這個套件的說明和可以操作的範例在 hakulex.dev/packages/editor。

安裝

pnpm add @hakulex/editor lexical vue

樣式

套件的程式不會自己載入樣式。在應用程式的進入點引入一次;這個檔案包含設計 token、內容排版和編輯器本身的樣式。

import '@hakulex/editor/style.css'

使用

<script setup lang="ts">
import type { SerializedEditorState } from 'lexical'
import { RichEditor } from '@hakulex/editor'
import { shallowRef } from 'vue'

const doc = shallowRef<SerializedEditorState | null>(null)
</script>

<template>
  <RichEditor v-model="doc" placeholder="開始輸入…" />
</template>

上傳圖片

給 uploadImage,作者就能從電腦選圖、貼上或拖曳圖片進來。它收到檔案,把檔案存到你的儲存空間,回傳圖片的網址:

<script setup lang="ts">
import type { UploadImage } from '@hakulex/editor'
import type { SerializedEditorState } from 'lexical'
import { RichEditor } from '@hakulex/editor'
import { shallowRef } from 'vue'

const doc = shallowRef<SerializedEditorState | null>(null)

const uploadImage: UploadImage = async (file) => {
  const body = new FormData()
  body.append('file', file)
  const stored = await fetch('/api/files', { method: 'POST', body }).then(response => response.json())
  return { src: stored.url, fileId: stored.id }
}
</script>

<template>
  <RichEditor v-model="doc" :upload-image="uploadImage" />
</template>

回傳的物件裡 src 是必要的,width、height 和 altText 可以不給。丟出的錯誤會顯示給作者看。

檔案編號 fileId

fileId 是選用的:你的應用程式替這個檔案取的編號。給了非空的字串,它就會存進文件,讓你不用從網址反查,就能知道一份文件用到了哪些檔案。

  • 單張圖片存在節點的 fileId;圖庫存在 images 裡那一張的 fileId。
  • 它只被保存,不影響顯示。改替代文字、圖說、寬度或版面,以及複製貼上,都會留著。
  • 它跟著那一張圖:圖片的網址換掉時會移除,除非換上的是另一次帶 fileId 的上傳。
  • LiteXML 用 img 的 file-id 屬性讀寫它。

不要直接相信文件裡的 fileId。 文件的 JSON 可以被修改,也可以從別的文件或別的網站貼過來,所以裡面的編號不一定屬於正在編輯的這位使用者。在伺服器端拿它做判斷之前(例如「這個檔案還有文章在用,不能刪」),先確認這個編號是你的檔案、而且這位使用者有權使用它。

選擇網站已有的圖片

圖片已經存在你的媒體庫、素材庫或其他地方時,給 pickImages,作者就能直接選一張已有的圖片,不用重新上傳。編輯器不知道圖片在哪裡:插入圖片的對話框和圖庫多一個按鈕,按下時呼叫這個函式,由你打開自己的選擇畫面,再回傳作者選了哪些圖片。

<script setup lang="ts">
import type { PickImages } from '@hakulex/editor'
import type { SerializedEditorState } from 'lexical'
import { RichEditor } from '@hakulex/editor'
import { shallowRef } from 'vue'
import { openMediaLibrary } from './media-library'

const doc = shallowRef<SerializedEditorState | null>(null)

const pickImages: PickImages = async ({ multiple, signal }) => {
  // 編輯器的對話框先關掉時,signal 會中止:把你的選擇畫面也關掉。
  const files = await openMediaLibrary({ multiple, signal })
  return files.map(file => ({
    src: file.url,
    width: file.width,
    height: file.height,
    fileId: file.id
  }))
}
</script>

<template>
  <RichEditor v-model="doc" :pick-images="pickImages" pick-images-label="從媒體庫選擇" />
</template>
  • 函式收到 purpose('insert'、'replace' 或 'gallery')、multiple(替換一張圖片時是 false)和 signal。回傳空陣列表示作者沒有選;丟出的錯誤會以「選擇失敗:…」顯示給作者。
  • 回傳的每一張和 uploadImage 的結果一樣檢查:網址要安全,沒有寬高就載入圖片量取,fileId 是非空字串才保留。不能用的那張不加入,並告訴作者原因,其他照常加入。
  • 插入圖片時,選來的圖片和上傳的檔案列在同一份清單裡,插入時不會再上傳,也不能用圖片編輯器編輯。替換時只用回傳的第一張。圖庫把它們加在最後。
  • 文件裡已有的圖片,旁邊的面板也有這個按鈕(purpose: 'replace'、multiple: false),用選到的第一張換掉這張圖片:網址、寬高和 fileId 換成新的,替代文字、圖說、顯示寬度和版面留著,和「選擇檔案替換…」一樣。
  • 編輯器不限制一次能選幾張,要限制就在你的選擇畫面裡處理。
  • 你的選擇畫面開著的時候,編輯器的對話框和圖片面板不搶焦點,也不處理 Esc 和點外面關閉,所以作者在你的畫面按 Esc 只會關掉你的畫面。編輯器的對話框或面板被關閉(取消、編輯器卸載)時 signal 會中止,之後才回來的結果會被忽略。
  • 按鈕文字預設是「選擇圖片」,用 pickImagesLabel 換掉。沒有給 pickImages 時,畫面和以前一樣。

不提供網址輸入 allowImageUrl

預設作者也可以輸入圖片網址。設成 false 時,插入圖片的對話框和圖庫都不再提供網址輸入,圖片面板只顯示目前的網址、不能修改。在這些地方,作者只能上傳圖片或用 pickImages 選。適合要求圖片都有 fileId 的網站(輸入網址加入的圖片沒有 fileId)。

它只管這三個地方。下面這些方式仍然會加入沒有 fileId 的圖片:

  • 在編輯器裡打 Markdown 的 ![說明](網址),或貼上含有這種寫法的 Markdown。
  • 貼上從網頁複製的內容,裡面的 <img> 會變成圖片。
  • 用 @hakulex/plugin-litexml-paste 貼上 LiteXML。
  • 讓 AI 用 @hakulex/agent-core 新增或改寫圖片。它不會讓圖片帶上對不上的 fileId,但不會擋掉沒有 fileId 的圖片。

所以如果每張圖片都必須有 fileId,要在伺服器端儲存文件時檢查,沒有的就拒絕或另外處理。allowImageUrl 是讓作者在編輯器裡不容易誤加,不是保證。

<RichEditor v-model="doc" :upload-image="uploadImage" :pick-images="pickImages" :allow-image-url="false" />

授權

MIT