一般而言一個組件庫都會設計一套相對來說符合大眾審美或產品需求的主題,但是主題定制需求永遠都存在,所以組件庫一般都會允許使用者自定義主題,我司的vue組件庫hui的定制主題簡單來說是通過修改預定義的scss變量的值來做到的,新體系下還做到了動態換膚,因為皮膚本質上是一種靜態資源(CSS文件和字體文件),所以只需要約定一種方式來每次動態請求加載不同的文件就可以了,為了方便這一需求,還配套開發了一個Vessel腳手架的插件,只需要以配置文件的方式列出你需要修改的變量和值,一個命令就可以幫你生成對應的皮膚。
但是目前的換膚還存在幾個問題, 一是不直觀,無法方便實時的看到修改后的組件效果,二是建議修改的變量比較少,這很大原因也是因為問題一,因為不直觀所以盲目修改后的效果可能達不到預期。
針對這幾個問題,所以實現一個在線主題編輯器是一個有意義的事情,目前最流行的組件庫之一的Element就支持主題在線編輯,地址:https://element.eleme.cn/#/zh-CN/theme,本項目是在參考了Element的設計思想和界面效果后開發完成的,本文將開發思路分享出來,如果有一些不合理地方或有一些更好的實現方式,歡迎指出來一起討論。
主題在線編輯的核心其實就是以一種可視化的方式來修改主題對應scss變量的值。
項目總體分為前端和后端兩個部分,前端主要負責管理主題列表、編輯主題和預覽主題,后端主要負責返回變量列表和編譯主題。
后端返回主題可修改的變量信息,前端生成對應的控件,用戶可進行修改,修改后立即將修改的變量和修改后的值發送給后端,后端進行合并編譯,生成css返回給前端,前端動態替換style標簽的內容達到實時預覽的效果。
主題列表頁面的主要功能是顯示官方主題列表和顯示自定義主題列表。
官方主題可進行的操作有預覽和復制,不能修改,修改的話會自動生成新主題。自定義主題可以編輯和下載,及進行修改名稱、復制、刪除操作。
官方主題列表后端返回,數據結構如下:
{
name: '官方主題-1', // 主題名稱
by: 'by hui', // 來源
description: '默認主題', // 描述
theme: {
// 主題改動點列表
common: {
'$--color-brand': '#e72528'
}
}
}
自定義主題保存在localstorage里,數據結構如下:
{
name: name, // 主題名稱
update: Date.now(), // 最后一次修改時間
theme: { // 主題改動點列表
common: {
//...
}
}
}
復制主題即把要復制的主題的theme.common數據復制到新主題上即可。
需要注意的就是新建主題時要判斷主題名稱是否重復,因為數據結構里并沒有類似id的字段。另外還有一個小問題是當預覽官方主題時修改的話會自動生成新主題,所以還需要自動生成可用的主題名,實現如下:
const USER_THEME_NAME_PREFIX='自定義主題-';
function getNextUserThemeName() {
let index=1
// 獲取已經存在的自定義主題列表
let list=getUserThemesFromStore()
let name=USER_THEME_NAME_PREFIX + index
let exist=()=> {
return list.some((item)=> {
return item.name===name
})
}
// 循環檢測主題名稱是否重復
while (exist()) {
index++
name=USER_THEME_NAME_PREFIX + index
}
return name
}
界面效果如下:
因為涉及到幾個頁面及不同組件間的互相通信,所以vuex是必須要使用的,vuex的state要存儲的內容如下:
const state={
// 官方主題列表
officialThemeList: [],
// 自定義主題列表
themeList: [],
// 當前編輯中的主題id
editingTheme: null,
// 當前編輯的變量類型
editingActionType: 'Color',
// 可編輯的變量列表數據
variableList: [],
// 操作歷史數據
historyIndex: 0,
themeHistoryList: [],
variableHistoryList: []
}
editingTheme是代表當前正在編輯的名字,主題編輯時依靠這個值來修改對應主題的數據,這個值也會在localstorage里存一份。
editingActionType是代表當前正在編輯中的變量所屬組件類型,主要作用是在切換要修改的組件類型后預覽列表滾動到對應的組件位置及用來渲染對應主題變量對應的編輯控件,如下:
頁面在vue實例化前先獲取官方主題、自定義主題、最后一次編輯的主題名稱,設置到vuex的store里。
編輯預覽頁面主要分兩部分,左側是組件列表,右側是編輯區域,界面效果如下:
組件預覽區域很簡單,無腦羅列出所有組件庫里的組件,就像這樣:
<div class="list">
<Color></Color>
<Button></Button>
<Radio></Radio>
<Checkbox></Checkbox>
<Inputer></Inputer>
<Autocomplete></Autocomplete>
<InputNumber></InputNumber>
//...
</div>
同時需要監聽一下editingActionType值的變化來滾動到對應組件的位置:
<script>
{
watch: {
'$store.state.editingActionType'(newVal) {
this.scrollTo(newVal)
}
},
methods:{
scrollTo(id) {
switch (id) {
case 'Input':
id='Inputer'
break;
default:
break;
}
let component=this.$children.find((item)=>{
return item.$options._componentTag===id
})
if (component) {
let el=component._vnode.elm
let top=el.getBoundingClientRect().top + document.documentElement.scrollTop
document.documentElement.scrollTop=top - 20
}
}
}
}
</script>
編輯區域主要分為三部分,工具欄、選擇欄、控件區。這部分是本項目的核心也是最復雜的一部分。
先看一下變量列表的數據結構:
{
"name": "Color",// 組件類型/類別
"config": [{// 配置列表
"type": "color",// 變量類型,根據此字段渲染對應類型的控件
"key": "$--color-brand",// sass變量名
"value": "#e72528",// sass變量對應的值,可以是具體的值,也可以是sass變量名
"category": "Brand Color"// 列表,用來分組進行顯示
}]
}
此列表是后端返回的,選擇器的選項是遍歷該列表取出所有的name字段的值而組成的。
因為有些變量的值是依賴另一個變量的,所依賴的變量也有可能還依賴另一個變量,所以需要對數據進行處理,替換成變量最終的值,實現方式就是循環遍歷數據,這就要求所有被依賴的變量也存在于這個列表中,否則就找不到了,只能顯示變量名,所以這個實現方式其實是有待商榷的,因為有些被依賴的變量它可能并不需要或不能可編輯,本項目目前版本是存在此問題的。
此外還需要和當前編輯中的主題變量的值進行合并,處理如下:
// Editor組件
async getVariable() {
try {
// 獲取變量列表,res.data就是變量列表,數據結構上面已經提到了
let res=await api.getVariable()
// 和當前主題變量進行合并
let curTheme=store.getUserThemeByNameFromStore(this.$store.state.editingTheme) || {}
let list=[]
// 合并
list=this.merge(res.data, curTheme.theme)
// 變量進行替換處理,因為目前存在該情況的只有顏色類型的變量,所以為了執行效率加上該過濾條件
list=store.replaceVariable(list, ['color'])
// 排序
list=this.sortVariable(list)
this.variableList=list
// 存儲到vuex
this.$store.commit('updateVariableList', this.variableList)
} catch (error) {
console.log(error)
}
}
merge方法就是遍歷合并對應變量key的值,主要看replaceVariable方法:
function replaceVariable(data, types) {
// 遍歷整體變量列表
for(let i=0; i < data.length; i++) {
let arr=data[i].config
// 遍歷某個類別下的變量列表
for(let j=0; j < arr.length; j++) {
// 如果不在替換類型范圍內的和值不是變量的話就跳過
if (!types.includes(arr[j].type) || !checkVariable(arr[j].value)) {
continue
}
// 替換處理
arr[j].value=findVariableReplaceValue(data, arr[j].value) || arr[j].value
}
}
return data
}
findVariableReplaceValue方法通過遞歸進行查找:
function findVariableReplaceValue(data, value) {
for(let i=0; i < data.length; i++) {
let arr=data[i].config
for(let j=0; j < arr.length; j++) {
if (arr[j].key===value) {
// 如果不是變量的話就是最終的值,返回就好了
if (!checkVariable(arr[j].value)) {
return arr[j].value
} else {// 如果還是變量的話就遞歸查找
return findVariableReplaceValue(data, arr[j].value)
}
}
}
}
}
接下來是具體的控件顯示邏輯,根據當前編輯中的類型對應的配置數據進行渲染,模板如下:
// Editor組件
<template>
<div class="editorContainer">
<div class="editorBlock" v-for="items in data" :key="items.name">
<div class="editorBlockTitle">{{items.name}}</div>
<ul class="editorList">
<li class="editorItem" v-for="item in items.list" :key="item.key">
<div class="editorItemTitle">{{parseName(item.key)}}</div>
<Control :data="item" @change="valueChange"></Control>
</li>
</ul>
</div>
</div>
</template>
data是對應變量類型里的config數據,是個計算屬性:
{
computed: {
data() {
// 找出當前編輯中的變量類別
let _data=this.$store.state.variableList.find(item=> {
return item.name===this.$store.state.editingActionType
})
if (!_data) {
return []
}
let config=_data.config
// 進行分組
let categorys=[]
config.forEach(item=> {
let category=categorys.find(c=> {
return c.name===item.category
})
if (!category) {
categorys.push({
name: item.category,
list: [item]
})
return false
}
category.list.push(item)
})
return categorys
}
}
}
Control是具體的控件顯示組件,某個變量具體是用輸入框還是下拉列表都在這個組件內進行判斷,核心是使用component動態組件:
// Control組件
<template>
<div class="controlContainer">
<component :is="showComponent" :data="data" :value="data.value" @change="emitChange" :extraColorList="extraColors"></component>
</div>
</template>
<script>
// 控件類型映射
const componentMap={
color: 'ColorPicker',
select: 'Selecter',
input: 'Inputer',
shadow: 'Shadow',
fontSize: 'Selecter',
fontWeight: 'Selecter',
fontLineHeight: 'Selecter',
borderRadius: 'Selecter',
height: 'Inputer',
padding: 'Inputer',
width: 'Inputer'
}
{
computed: {
showComponent() {
// 根據變量類型來顯示對應的控件
return componentMap[this.data.type]
}
}
}
</script>
一共有顏色選擇組件、輸入框組件、選擇器組件、陰影編輯組件,具體實現很簡單就不細說了,大概就是顯示初始傳入的變量,然后修改后觸發修改事件change,經Control組件傳遞到Editor組件,在Editor組件上進行變量修改及發送編譯請求,不過其中陰影組件的實現折磨了我半天,主要是如何解析陰影數據,這里用的是很暴力的一種解析方法,如果有更好的解析方式的話可以留言進行分享:
// 解析css陰影數據
// 因為rgb顏色值內也存在逗號,所以就不能簡單的用逗號進行切割解析
function parse() {
if (!this.value) {
return false
}
// 解析成復合值數組
// let value="0 0 2px 0 #666,0 0 2px 0 #666, 0 2px 4px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 hlsa(0, 0, 0, 0.12),0 2px 4px 0 #sdf, 0 2px 4px 0 hlsa(0, 0, 0, 0.12), 0 2px 0 hlsa(0, 0, 0, 0.12), 0 2px hlsa(0, 0, 0, 0.12), 0 2px 4px 0 hlsa(0, 0, 0, 0.12)"
// 根據右括號來進行分割成數組
let arr=this.value.split(/\)\s*,\s*/gim)
arr=arr.map(item=> {
// 補上右括號
if (item.includes('(') && !item.includes(')')) {
return item + ')'
} else {// 非rgb顏色值的直接返回
return item
}
})
let farr=[]
arr.forEach(item=> {
let quene=[]
let hasBrackets=false
// 逐個字符進行遍歷
for (let i=0; i < item.length; i++) {
// 遇到非顏色值內的逗號直接拼接目前隊列里的字符添加到數組
if (item[i]===',' && !hasBrackets) {
farr.push(quene.join('').trim())
quene=[]
} else if (item[i]==='(') {//遇到顏色值的左括號修改標志位
hasBrackets=true
quene.push(item[i])
} else if (item[i]===')') {//遇到右括號重置標志位
hasBrackets=false
quene.push(item[i])
} else {// 其他字符直接添加到隊列里
quene.push(item[i])
}
}
// 添加隊列剩余的數據
farr.push(quene.join('').trim())
})
// 解析出單個屬性
let list=[]
farr.forEach(item=> {
let colorRegs=[/#[a-zA-Z0-9]{3,6}$/, /rgba?\([^()]+\)$/gim, /hlsa?\([^()]+\)$/gim, /\s+[a-zA-z]+$/]
let last=''
let color=''
for (let i=0; i < colorRegs.length; i++) {
let reg=colorRegs[i]
let result=reg.exec(item)
if (result) {
color=result[0]
last=item.slice(0, result.index)
break
}
}
let props=last.split(/\s+/)
list.push({
xpx: parseInt(props[0]),
ypx: parseInt(props[1]),
spread: parseInt(props[2]) || 0,
blur: parseInt(props[3]) || 0,
color
})
})
this.list=list
}
回到Editor組件,編輯控件觸發了修改事件后需要更新變量列表里面對應的值及對應主題列表里面的值,同時要發送編譯請求:
// data是變量里config數組里的一項,value就是修改后的值
function valueChange(data, value) {
// 更新當前變量對應key的值
let cloneData=JSON.parse(JSON.stringify(this.$store.state.variableList))
let tarData=cloneData.find((item)=> {
return item.name===this.$store.state.editingActionType
})
tarData.config.forEach((item)=> {
if (item.key===data.key) {
item.value=value
}
})
// 因為是支持顏色值修改為某些變量的,所以要重新進行變量替換處理
cloneData=store.replaceVariable(cloneData, ['color'])
this.$store.commit('updateVariableList', cloneData)
// 更新當前主題
let curTheme=store.getUserThemeByNameFromStore(this.$store.state.editingTheme, true)
if (!curTheme) {// 當前是官方主題則創建新主題
let theme=store.createNewUserTheme('', {
[data.key]: value
})
this.$store.commit('updateEditingTheme', theme.name)
} else {// 修改的是自定義主題
curTheme.theme.common={
...curTheme.theme.common,
[data.key]: value
}
store.updateUserTheme(curTheme.name, {
theme: curTheme.theme
})
}
// 請求編譯
this.updateVariable()
}
接下來是發送編譯請求:
async function updateVariable() {
let curTheme=store.getUserThemeByNameFromStore(this.$store.state.editingTheme, true, true)
try {
let res=await api.updateVariable(curTheme.theme)
this.replaceTheme(res.data)
} catch (error) {
console.log(error)
}
}
參數為當前主題修改的變量數據,后端編譯完后返回css字符串,需要動態插入到head標簽里:
function replaceTheme(data) {
let id='HUI_PREVIEW_THEME'
let el=document.querySelector('#' + id)
if (el) {
el.innerHTML=data
} else {
el=document.createElement('style')
el.innerHTML=data
el.id=id
document.head.appendChild(el)
}
}
這樣就達到了修改變量后實時預覽的效果,下載主題也是類似,把當前編輯的主題的數據發送給后端編譯完后生成壓縮包進行下載。
下載:因為要發送主題變量進行編譯下載,所以不能使用get方法,但使用post方法進行下載比較麻煩,所以為了簡單起見,下載操作實際是在瀏覽器端做的。
function downloadTheme(data) {
axios({
url: '/api/v1/download',
method: 'post',
responseType: 'blob', // important
data
}).then((response)=> {
const url=window.URL.createObjectURL(new Blob([response.data]))
const link=document.createElement('a')
link.href=url
link.setAttribute('download', 'theme.zip')
link.click()
})
}
至此,主流程已經跑通,接下來是一些提升體驗的功能。
1.重置功能:重置理應是重置到某個主題復制來源的那個主題的,但是其實必要性也不是特別大,所以就簡單做,直接把當前主題的配置變量清空,即theme.common={},同時需要重新請求變量數據及請求編譯。
2.前進回退功能:前進回退功能說白了就是把每一步操作的數據都克隆一份并存到一個數組里,然后設置一個指針,比如index,指向當前所在的位置,前進就是index++,后退就是index--,然后取出對應數組里的數據替換當前的數據。對于本項目,需要存兩個東西,一個是主題數據,一個是變量數據。可以通過對象形式存到一個數組里,也可以向本項目一樣搞兩個數組。
具體實現:
1.先把初始的主題數據拷貝一份扔進歷史數組themeHistoryList里,請求到變量數據后扔進variableHistoryList數組里
2.每次修改后把修改后的變量數據和主題數據都復制一份扔進去,同時指針historyIndex加1
3.根據前進還是回退來設置historyIndex的值,同時取出對應位置的主題和變量數據替換當前的數據,然后請求編譯
需要注意的是在重置和返回主題列表頁面時要復位themeHistoryList、variableHistoryList、historyIndex
3.顏色預覽組件優化
因為顏色預覽組件是需要顯示當前顏色和顏色值的,那么就會有一個問題,字體顏色不能寫死,否則如果字體寫死白色,那么如果這個變量的顏色值又修改成白色,那么將一片白色,啥也看不見,所以需要動態判斷是用黑色還是白色,有興趣詳細了解判斷算法可閱讀:https://segmentfault.com/a/1190000018907560。
function const getContrastYIQ=(hexcolor)=> {
hexcolor=colorToHEX(hexcolor).substring(1)
let r=parseInt(hexcolor.substr(0, 2), 16)
let g=parseInt(hexcolor.substr(2, 2), 16)
let b=parseInt(hexcolor.substr(4, 2), 16)
let yiq=((r * 299) + (g * 587) + (b * 114)) / 1000
return (yiq >=128) ? 'black' : 'white'
}
colorToHEX是一個將各種類型的顏色值都轉為十六進制顏色的函數。
4.一些小細節
logo、導航、返回按鈕、返回頂部等小控件隨當前編輯中的主題色進行變色。
到這里前端部分就結束了,讓我們喝口水繼續。
后端用的是nodejs及eggjs框架,對eggjs不熟悉的話可先閱讀一下文檔:https://eggjs.org/zh-cn/,后端部分比較簡單,先看路由:
module.exports=app=> {
const { router, controller }=app
// 獲取官方主題列表
router.get(`${BASE_URL}/getOfficialThemes`, controller.index.getOfficialThemes)
// 返回變量數據
router.get(`${BASE_URL}/getVariable`, controller.index.getVariable)
// 編譯scss
router.post(`${BASE_URL}/updateVariable`, controller.index.updateVariable)
// 下載
router.post(`${BASE_URL}/download`, controller.index.download)
}
目前官方主題列表和變量數據都是一個寫死的json文件。所以核心只有兩部分,編譯scss和下載,先看編譯。
主題在線編輯能實現靠的就是scss的變量功能,編譯scss可用使用sass包或者node-sass包,前端傳過來的參數其實就一個json類型的對象,key是變量,value是值,但是這兩個包都不支持傳入額外的變量數據和本地的scss文件進行合并編譯,但是提供了一個配置項:importer,可以傳入函數數組,它會在編譯過程中遇到 @use
or @import
語法時執行這個函數,入參為url,可以返回一個對象:
{
contents: `
h1 {
font-size: 40px;
}
`
}
contents的內容即會替代原本要引入的對應scss文件的內容,詳情請看:https://sass-lang.com/documentation/js-api#importer
但是實際使用過程中,不知為何sass包的這個配置項是無效的,所以只能使用node-sass,這兩個包的api基本是一樣的,但是node-sass安裝起來比較麻煩,尤其是windows上,安裝方法大致有兩種:
1.
npm install -g node-gyp
npm install --global --production windows-build-tools
npm install node-sass --save-dev
2.
npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm install node-sass
因為主題的變量定義一般都在統一的一個或幾個文件內,像hui,是定義在var-common.scss和var.scss兩個文件內,所以可以讀取這兩個文件的內容然后將其中對應變量的值替換為前端傳過來的變量,替換完成后通過importer函數返回進行編譯,具體替換方式也有多種,我同事的方法是自己寫了個scss解析器,解析成對象,然后遍歷對象解析替換,而我,比較草率,直接用正則匹配解析修改,實現如下:
function(data) {
// 前端傳遞過來的數據
let updates=data.common
// 兩個文件的路徑
let commonScssPath=path.join(process.cwd(), 'node_modules/hui/packages/theme/common/var-common.scss')
let varScssPath=path.join(process.cwd(), 'node_modules/hui/packages/theme/common/var.scss')
// 讀取兩個文件的內容
let commonScssContent=fs.readFileSync(commonScssPath, {encoding: 'utf8'})
let varScssContent=fs.readFileSync(varScssPath, {encoding: 'utf8'})
// 遍歷要修改的變量數據
Object.keys(updates).forEach((key)=> {
let _key=key
// 正則匹配及替換
key=key.replace('$', '\\$')
let reg=new RegExp('(' +key + '\\s*:\\s*)([^:]+)(;)', 'img')
commonScssContent=commonScssContent.replace(reg, `$1${updates[_key]}$3`)
varScssContent=varScssContent.replace(reg, `$1${updates[_key]}$3`)
})
// 修改路徑為絕對路徑,否則會報錯
let mixinsPath=path.resolve(process.cwd(), 'node_modules/hui/packages/theme/mixins/_color-helpers.scss')
mixinsPath=mixinsPath.split('\\').join('/')
commonScssContent=commonScssContent.replace(`@import '../mixins/_color-helpers'`, `@import '${mixinsPath}'`)
let huiScssPath=path.join(process.cwd(), 'node_modules/hui/packages/theme/index.scss')
// 編譯scss
let result=sass.renderSync({
file: huiScssPath,
importer: [
function (url) {
if (url.includes('var-common')) {
return {
contents: commonScssContent
}
}else if (url.includes('var')) {
return {
contents: varScssContent
}
} else {
return null
}
}
]
})
return result.css.toString()
}
下載的主題包里有兩個數據,一個是配置源文件,另一個就是編譯后的主題包,包括css文件和字體文件。創建壓縮包使用的是jszip,可參考:https://github.com/Stuk/jszip。
主題包的目錄結構如下:
-theme
--fonts
--index.css
-config.json
實現如下:
async createThemeZip(data) {
let zip=new JSZip()
// 配置源文件
zip.file('config.json', JSON.stringify(data.common, null, 2))
// 編譯后的css主題包
let theme=zip.folder('theme')
let fontPath='node_modules/hui/packages/theme/fonts'
let fontsFolder=theme.folder('fonts')
// 遍歷添加字體文件
let loopAdd=(_path, folder)=> {
fs.readdirSync(_path).forEach((file)=> {
let curPath=path.join(_path, file)
if (fs.statSync(curPath).isDirectory()) {
let newFolder=folder.folder(file)
loopAdd(curPath, newFolder)
} else {
folder.file(file, fs.readFileSync(curPath))
}
})
}
loopAdd(fontPath, fontsFolder)
// 編譯后的css
let css=await huiComplier(data)
theme.file('index.css', css)
// 壓縮
let result=await zip.generateAsync({
type: 'nodebuffer'
})
// 保存到本地
// fs.writeFileSync('theme.zip', result, (err)=> {
// if (err){
// this.ctx.logger.warn('壓縮失敗', err)
// }
// this.ctx.logger.info('壓縮完成')
// })
return result
}
至此,前端和后端的核心實現都已介紹完畢。
本項目目前只是一個粗糙的實現,旨在提供一個實現思路,還有很多細節需要優化,比如之前提到的變量依賴問題,還有scss的解析合并方式,此外還有多語言、多版本的問題需要考慮。
家好,我是Echa.
富文本編輯器,Multi-function Text Editor, 簡稱 MTE, 是一種可內嵌于瀏覽器,所見即所得(WYSIWYG)的文本編輯器。今天就來介紹 10 款式前端開發常用的富文本編輯器插件!
Draft.js 是 Facebook 的一個開源項目,是 React 項目首選的是文本編輯器框架。這是一個健壯、可擴展和可定制的框架。Draft.js 遵循與 React 中的受控組件相同的范例,并提供了一個 Editor 呈現富文本輸入的組件。它還公開了一個EditorStateAPI 來處理/存儲Editor組件中的狀態更新。
該富文本編輯器的特點:
GitHub:https://github.com/facebook/draft-js
Slate.js 是受 Draft.js 啟發的富文本編輯器。它是一個可深度定制的富編輯器框架,專用于 React。與 Draft.js 類似,它具有良好的 API、強大的插件基礎設施以及與 React 的深度連接。此外,插件生態系統比 Draft.js 小一些,但它的插件質量會更好。
該富文本編輯器的特點:
GitHub:https://github.com/ianstormtaylor/slate
Quill.js 是一個具有跨平臺和跨瀏覽器支持的富文本編輯器。憑借其可擴展架構和富有表現力的 API,可以完全自定義它以滿足個性化的需求。由于其模塊化架構和富有表現力的 API,可以從 Quill 核心開始,然后根據需要自定義其模塊或將自己的擴展添加到這個富文本編輯器中。它提供了兩個用于更改編輯器外觀的主題,可以使用插件或覆蓋其 CSS 樣式表中的規則進一步自定義。Quill 還支持任何自定義內容和格式,因此可以添加嵌入式幻燈片、3D 模型等。
該富文本編輯器的特點:
GitHub:https://github.com/quilljs/quill/
TinyMCE 是一個熱門的富文本編輯器。它的目標是幫助其他開發人員構建精美的 Web 內容解決方案。它易于集成,可以部署在基于云的、自托管或混合環境中。該設置使得合并諸如 Angular、React 和 Vue 等框架成為可能。它還可以使用 50 多個插件進行擴展,每個插件都有 100 多個自定義選項。
TinyMCE 通過創建和編輯表格、建立字體系列、搜索和替換字體以及更改字體大小等功能,讓你可以完全控制你的設計。它還提供了多種云安全功能,包括 JSON Web 令牌和私有 RSA 密鑰,以更好地保護你的內容。
該富文本編輯器的特點:
GitHub:https://github.com/tinymce/tinymce
wangEditor 是一個使用Typescript 開發的 Web 富文本編輯器, 輕量、簡潔、易用、開源免費。它兼容常見的 PC 瀏覽器:Chrome,Firefox,Safar,Edge,QQ 瀏覽器,IE11。
GitHub:https://github.com/wangeditor-team/wangEditor/
ProseMirror 是一個基于 ContentEditable 的所見即所得 HTML 編輯器,功能強大,支持協作編輯和自定義文檔模式 ProseMirror 庫由多個單獨的模塊組成。一個理想的富文本編輯器產生結構化的、語義化的、有意義的文檔的同時還要能夠讓用戶能夠容易地理解與使用。ProseMirror 試著在 Markdown 編輯體驗和傳統的 WYSIWYG 編輯體驗中尋找一種融合的方法。它通過實現一個比普通的 HTML 具有更多的限制和結構化的 WYSIWYG 風格的接口來做到這點。你可以自定義編輯器創建的文檔的結構和內容,使它們符合你應用的需要。
該富文本編輯器的特點:
GitHub:https://github.com/prosemirror/
Tiptap 是一個基于 Vue 的無渲染的富文本編輯器,它基于 Prosemirror,完全可擴展且無渲染。可以輕松地將自定義節點添加為Vue組件。使用無渲染組件(函數式組件),幾乎完全控制標記和樣式。菜單的外觀或在DOM中的顯示位置。這完全取決于使用者。
該富文本編輯器的特點:
GitHub:https://github.com/ueberdosis/tiptap
CKEditor 是一個強大的富文本編輯器框架,具有模塊化架構、現代集成和協作編輯等功能。它可以通過基于插件的架構進行擴展,從而可以將必要的內容處理功能引入。CKEditor 在市場上已有近 15 年的歷史,因其具有廣泛的功能和舊版軟件兼容性而成為最負盛名的編輯器之一。
CKEditor 5 是一個超現代的 JavaScript 富文本編輯器,具有 MVC 架構、自定義數據模型和虛擬 DOM。它是在 ES6 中從頭開始編寫的,并且具有出色的 webpack支持。可以使用與Angular、React和Vue.js的原生集成。
該富文本編輯器的特點:
GitHub:https://github.com/ckeditor/ckeditor5
ContentTools 是一個開源的富文本編輯器,只需幾個步驟即可將其添加到任何 HTML 頁面。添加后,將在 HTML 頁面上看到一個鉛筆圖標。單擊時,會出現一個工具箱和檢查器欄。使用這兩個元素,可以在頁面內編輯、調整大小或拖放文本圖像、嵌入的視頻、表格和其他內容。
ContentTools 旨在提供可開箱即用的全功能內容編輯器和可用于構建您自己的編輯器的類和函數工具包。該工具包包括一組輕量級的用戶界面類、一組用于執行常見編輯任務的工具,以及一個用于管理撤消/重做的歷史堆棧。雖然工具包提供的組件可以很好地協同工作,但它們也可以根據需要使用或更換。
該富文本編輯器的特點:
GitHub:https://github.com/GetmeUK/ContentTools
Jodit 是一款使用純 TypeScript 編寫的(無需使用其他庫),美觀實用的所見即所得開源富文本編輯器,支持中文,超強自定義。
GitHub:https://github.com/xdan/jodit
圖編輯器介紹。
·(一)創建新地圖。在編輯器中選擇對應的坐標系,背景地圖提供:百度、高德、天地圖坐標系。搜索要繪制的場所,確定場景位置,開始繪制繪制地面,地面可進行多邊形調整。第一步準備完成。
·(二)導入參考圖。點擊進入制圖編輯器,對地圖示例增加F5樓層,點擊新增,進入F5樓層進行繪制室內地圖,將地面導入F5樓層,導入參考圖,對參考圖進行校正,參考圖校正完成。
·(三)開始地圖繪制。對地圖進行繪制,左側工具欄選擇矩形工具進行繪制店鋪,也可以用多邊形工具繪制店鋪。點擊右鍵選擇高級工具進行拆分,利用高級工具進行線段轉弧編輯,對店鋪的屬性進行修改。
·(四)添加POI點位。在左側工具欄選擇圖標,左側工具欄選擇路徑工具,開始繪制路網,路網繪制好后保存。
·(五)通行設施管理。在頂部工具欄中選擇通行設施添加通行節點,選擇直梯到達的樓層,在對應位置添加電梯點,增加通行節點,新增電梯點與通行節點進行關聯設置,開啟批量打點功能以便在所有樓層電梯位置標記為通行點,批量打點成功。對電梯點與路徑線進行咬合實現跨層導航,完成后保存發布后預覽。
主題編輯器操作:主題介紹:這是樓層設置,就是按類型修改面元素的顏色,是對單元素進行設置,其他與全局設置一致且可以改變的顏色設置,可以改標注的顏色,也可以改圖標的樣式,樓層公式是一樣。增加面元素的加頂面貼圖,就是對其中一個元素可以加一個頂面貼圖,這可以調整圖的大小,旋轉的方向,最后點發布即可。