一週的時間,幾乎天天都要工做十幾個小時,敲代碼+寫做文,界面原型算是完成了,下一步是寫內核的HTML處理引擎,純JS實現。本次實戰展現告一段落,等RXEditor下一個版本完成,再繼續分享吧。
剩下的功能:標籤式輸入、名值對輸入、對話框(modal dialog),邊框輸入,所有完成。css
演示地址:https://vular.cn/studio-ui/
css class輸入,樣式跟屬性輸入,效果:html
對話框(model dialog效果)vue
前幾期功能效果總覽:git
標籤輸入框用來輸入CSS class,名字一如既往的好聽,就叫RxLabelInput吧。
輸入值一個數組,由於有多處要操做數組,增、刪、改、克隆、比較等。比較好的一個方式是把Array類用繼承的方式重寫一下,把這寫方法加到裏面。可是RXEidtor內核用純JS實現,並放在一個iFrame裏面,它跟主界面只能經過windows message傳遞數據,帶有方法的類沒法做爲消息被傳遞,暫時先不用這個方法,只把相關功能抽取成獨立函數,放在valueOperate.js裏面。
若是之後數組操做量更大,再考慮轉成一個通用的數組類。
前幾期介紹過,使用計算屬性changed來標識數據是否被修改過,changed計算屬性內部,須要比較兩個值是否相等,普通字符串不會有問題,要比較數組用這樣的方式最方便,先排序、轉成字符串、比較字符串:github
aValue.sort().toString() === bValue.sort().toString()
數組的sort方法會改變原來的數組值,會引起數據刷新,從而再次調用計算屬性,造成死循環,調試了很長時間,就算空數組也會死循環。因此,須要把數據複製一份出來,再比較:windows
if(Array.isArray(a) && Array.isArray(b)){ //複製數組 let aValue = a.concat() //複製數組 let bValue = b.concat() //比較數組 return aValue.sort().toString() === bValue.sort().toString() }
組件代碼:數組
<template> <div class="label-list"> <div class="label-item" v-for = "val in inputValue" > {{val}} <span class="remove-button" @click="remove(val)" >×</span> </div> <div style="width: 100%"></div> <div class="add-button" @click="addClick" >+</div> <div style="width: 100%"></div> <input v-show="isAdding" v-model="newValue" autofocus="autofocus" :placeholder="$t('widgets.enter-message')" @keyup.13 = "finishAdd" ref="inputControl" /> </div> </template> <script> import {addToArray, removeFromArray} from './valueOperate' export default { props:{ value:{ default:[] }, }, computed:{ inputValue: { get:function() { return this.value; }, set:function(val) { this.$emit('input', val); }, }, }, data () { return { isAdding : false, newValue : '', } }, methods: { addClick(){ this.isAdding = true; this.$refs.inputControl.style.display = 'block' this.$refs.inputControl.focus() }, finishAdd(){ if(this.newValue){ this.newValue.split(' ').forEach((val)=>{ if(val){ addToArray(val, this.inputValue) } }) this.newValue = '' } this.isAdding = false }, remove(val){ removeFromArray(val, this.inputValue) } }, } </script> <style> .label-list{ background: rgba(0,0,0, 0.15); display: flex; flex-flow: row; flex-wrap: wrap; padding:10px; } .label-list .label-item{ padding:0 3px; background: rgba(255,255,255, 0.15); margin:1px; border-radius: 3px; height: 24px; display: flex; align-items: center; } .label-list .remove-button{ cursor: pointer; margin-left: 2px; } .label-list .add-button{ background: rgba(255,255,255, 0.15); width: 24px; height: 22px; display: flex; align-items: center; justify-content: center; border-radius: 3px; margin: 1px; margin-top:3px; font-size: 16px; padding-bottom:3px; cursor: pointer; } .label-list input{ outline: 0; border: 0; background: transparent; color: #fff; margin-top:4px; } </style>
用於輸入html屬性(attributes)和樣式(style)的名值對輸入控件,也有一個拉風的名字:RxNameValueInput。函數
這個控件的傳入值v-model是一個對象,做爲一個對象,動態增刪屬性再加排序,會稍微有些不便,因此組件內部處理時,把這個對象轉換成一個二維數組:工具
mounted () { for(var name in this.inputValue){ this.valueArray.push([name, this.inputValue[name]]) } },
而後watch這個數組,當它有變化時,逆向轉化成對象,至關於完成一個雙向綁定,逆向轉化代碼:學習
watch: { valueArray() { this.inputValue = {} for(var i = 0; i < this.valueArray.length; i++){ let name = this.valueArray[i][0] let value = this.valueArray[i][1] this.inputValue[name] = value } } }
整個組件的代碼:
<template> <div class="name-value-box"> <div class="name-value-row" v-for="(item, i) in valueArray" > <div class="name-input"> <input v-model="item[0]" @blur = "nameBlur(i)" > </div> <div class="separator">:</div> <div class="value-input"> <input v-model="item[1]"> </div> <div class="clear-button" @click="remove(i)" >×</div> </div> <div class="name-value-row"> <div class="name-input"> <input v-model="newName" @keyup.13 = "addNew" @blur = "newBlur" ref="newName" > </div> <div class="separator">:</div> <div class="value-input"> <input v-model="newValue" @keyup.13 = "addNew" @blur = "newBlur" > </div> <div class="button-placeholder" ></div> </div> </div> </template> <script> export default { props:{ value:{ default:{} }, }, computed:{ inputValue: { get:function() { return this.value; }, set:function(val) { this.$emit('input', val); }, }, }, data () { return { valueArray : [], newName : '', newValue : '', } }, mounted () { for(var name in this.inputValue){ this.valueArray.push([name, this.inputValue[name]]) } }, methods: { addClick(){ }, nameBlur(i){ this.valueArray[i][0] = this.valueArray[i][0].trim() if(!this.valueArray[i][0]){ this.remove(i) } }, remove(i){ this.valueArray.splice(i, 1) }, addNew(){ this.newName = this.newName.trim() if(this.newName && !this.exist(this.newName)){ this.valueArray.push([this.newName, this.newValue]) this.newName = '' this.newValue = '' this.$refs.newName.focus() } }, newBlur(){ this.newName = this.newName.trim() this.newValue = this.newValue.trim() if(this.newName && this.newValue){ this.addNew() } }, exist(name){ for(var i = 0; i < this.valueArray.length; i++){ if(this.valueArray[i][0] === name){ return true } } return false } }, watch: { valueArray() { this.inputValue = {} for(var i = 0; i < this.valueArray.length; i++){ let name = this.valueArray[i][0] let value = this.valueArray[i][1] this.inputValue[name] = value } } } } </script> <style> .name-value-box{ background: rgba(0,0,0, 0.15); display: flex; flex-flow: column; padding:10px; } .name-value-box .add-button{ background: rgba(255,255,255, 0.15); width: 24px; height: 22px; display: flex; align-items: center; justify-content: center; border-radius: 3px; margin: 1px; margin-top:3px; font-size: 16px; padding-bottom:3px; cursor: pointer; } .name-value-row{ width: 100%; display: flex; flex-flow: row; height: 24px; align-items: center; font-size: 11px; } .name-value-row .name-input input, .name-value-row .value-input input{ width: 100%; background: transparent; color:#bababa; outline: 0; border: 0; } .name-value-row .separator{ width: 5px; display: flex; justify-content: center; flex-shrink: 0; color: #bababa; } .name-value-row .name-input{ flex: 1; } .name-value-row .value-input{ flex: 1.5; padding-left:3px; } .name-value-row .clear-button{ display: flex; align-items: center; justify-content: center; width: 20px; height: 17px; background: rgba(255,255,255,0.1); border-radius: 3px; margin:1px; font-size: 12px; padding-bottom: 3px; cursor: pointer; } .name-value-row .button-placeholder{ width: 20px; height: 20px; background: transparent; } </style>
還實現了一個邊框輸入控件,這個控件沒有成長爲通用控件的潛力,就不介紹了,感興趣的直接看源碼,名字叫:RxBorderInput。
最後實現的一個控件時對話框 ,Modal Dialog,目前有兩處地方用到它,一處時主題選擇對話框,一處時關於(about)對話框。
這兩處共用了通用對話框Modal,經過v-model傳入控制對話框是否顯示的值,經過卡槽Slot傳入對話框內容,Modal代碼:
<template> <div v-if="inputValue" class="modal-mask" @click="inputValue = false"> <div class="modal" :style="{ top : top, left : left, width :width, height : height, }" @click="modalClick" > <slot></slot> </div> </div> </template> <script> export default { name: 'Modal', props:{ value:{ default:'' }, width:{ default: '800px'}, height:{ default: 'calc(100vh - 80px)'}, top:{default: '40px'}, left:{default: 'calc(50% - 400px)'}, }, computed:{ inputValue: { get:function() { return this.value; }, set:function(val) { this.$emit('input', val); }, }, }, data () { return { } }, methods: { modalClick(event){ event.stopPropagation() }, }, } </script> <style> .modal-mask{ position: fixed; z-index: 9999; top:0; left: 0; width: 100vw; height: 100vh; background: rgba(20, 20, 20, 0.9); } .modal-mask .modal{ position: fixed; top:50%; left:50%; background: #fff; box-shadow: 3px 3px 6px 3px rgba(0, 0, 0, 0.1); transform: all 0.3s; display: flex; flex-flow: column; color: #474747; } </style>
還能夠經過屬性傳入對話框寬、高、位置等信息。調用樣例,也是about對話框的代碼:
<template> <Modal v-model="inputValue" width='600px' height='400px' top ="calc(50% - 200px)" left ="calc(50% - 300px)" > <div class="dialog-head"> <div><i class="fas fa-question-circle"></i> {{$t('about.about-title')}} </div> <span class="close-button" @click="inputValue = false" >×</span> </div> <div class="dialog-body about-content"> 本程序是RXEditor第二版的界面原型。<br/> 基於VUE實現,代碼已轉入RXeditor項目。<br /> 本原型再也不維護,僅供學習參考。<br /> RXEditor是一個開源的,可視化的,HTML編輯工具,基於Bootstrap實現。<br /> RXEditor 代碼地址:<a href="https://github.com/vularsoft/rxeditor" target="_blank">https://github.com/vularsoft/rxeditor</a> 演示地址:<a href="https://vular.cn/rxeditor/" target="_blank" >https://vular.cn/rxeditor</a> </div> <div class="dialog-footer"> <div class="dialog-button confirm-btn" @click="inputValue = false" >{{$t('about.close')}}</div> </div> </Modal> </template> <script> import Modal from './Modal.vue' export default { name: 'AboutDialog', components:{ Modal, }, props:{ value:{ default:'' }, }, computed:{ inputValue: { get:function() { return this.value; }, set:function(val) { this.$emit('input', val); }, }, }, } </script> <style> .about-content{ display: flex; justify-content: center; align-items:flex-start; font-size:14px; line-height: 32px; padding-left: 40px; } .about-content a{ color: #75b325; } .about-content a:hover{ color: #60921e; text-decoration: underline; } </style>
到此爲止,本是實戰項目所有完成,感謝你們的閱讀、關注。接下來會把這些代碼應用在RxEditor中,具體是否要分享RxEditor內核,要看之後我的精力與時間。
本展現項目所有代碼,請參考Github:https://github.com/vularsoft/studio-ui如有有問題,請留言交流。