diff --git a/src/editor/assets/css/control/select.css b/src/editor/assets/css/control/select.css new file mode 100644 index 0000000..010c9ad --- /dev/null +++ b/src/editor/assets/css/control/select.css @@ -0,0 +1,44 @@ +.select-control-popup { + max-width: 160px; + min-width: 69px; + max-height: 225px; + position: absolute; + z-index: 1; + border: 1px solid #e4e7ed; + border-radius: 4px; + background-color: #fff; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1); + box-sizing: border-box; + margin: 5px 0; + overflow-y: auto; +} + +.select-control-popup ul { + list-style: none; + padding: 3px 0; + margin: 0; + box-sizing: border-box; +} + +.select-control-popup ul li { + font-size: 13px; + padding: 0 20px; + position: relative; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: #666; + height: 36px; + line-height: 36px; + box-sizing: border-box; + cursor: pointer; +} + +.select-control-popup ul li:hover { + background-color: #EEF2FD; +} + +.select-control-popup ul li.active { + color: var(--COLOR-HOVER, #5175f4); + font-weight: 700; +} \ No newline at end of file diff --git a/src/editor/assets/css/index.css b/src/editor/assets/css/index.css index d976bd2..2b3c201 100644 --- a/src/editor/assets/css/index.css +++ b/src/editor/assets/css/index.css @@ -1,3 +1,5 @@ +@import './control/select.css'; + .inputarea { width: 0; height: 12px; diff --git a/src/editor/core/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts index f0748ec..1113a21 100644 --- a/src/editor/core/draw/control/Control.ts +++ b/src/editor/core/draw/control/Control.ts @@ -1,9 +1,10 @@ import { ControlComponent, ControlType } from '../../../dataset/enum/Control' import { ElementType } from '../../../dataset/enum/Element' import { IControlInitOption, IControlInstance, IControlOption } from '../../../interface/Control' -import { IElement } from '../../../interface/Element' +import { IElement, IElementPosition } from '../../../interface/Element' import { RangeManager } from '../../range/RangeManager' import { Draw } from '../Draw' +import { SelectControl } from './select/SelectControl' import { TextControl } from './text/TextControl' interface IMoveCursorResult { @@ -40,14 +41,34 @@ export class Control { return false } + public getContainer(): HTMLDivElement { + return this.draw.getContainer() + } + public getElementList(): IElement[] { return this.draw.getElementList() } + public getPosition(): IElementPosition | null { + const positionList = this.draw.getPosition().getPositionList() + const { endIndex } = this.range.getRange() + return positionList[endIndex] || null + } + + public getPreY(): number { + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + return this.draw.getPageNo() * (height + pageGap) + } + public getRange() { return this.range.getRange() } + public shrinkBoundary() { + this.range.shrinkBoundary() + } + public getActiveControl(): IControlInstance | null { return this.activeControl } @@ -58,23 +79,42 @@ export class Control { const element = elementList[range.startIndex] // 判断控件是否已经激活 if (this.activeControl) { + // 列举控件唤醒下拉弹窗 + if (this.activeControl instanceof SelectControl) { + this.activeControl.awake() + } const controlElement = this.activeControl.getElement() if (element.controlId === controlElement.controlId) return } // 销毁旧激活控件 this.destroyControl() // 激活控件 - if (element.control!.type === ControlType.TEXT) { + const control = element.control! + if (control.type === ControlType.TEXT) { this.activeControl = new TextControl(element, this) + } else if (control.type === ControlType.SELECT) { + const selectControl = new SelectControl(element, this) + this.activeControl = selectControl + selectControl.awake() } } public destroyControl() { if (this.activeControl) { + if (this.activeControl instanceof SelectControl) { + this.activeControl.destroy() + } this.activeControl = null } } + public repaintControl(curIndex: number) { + this.range.setRange(curIndex, curIndex) + this.draw.render({ + curIndex + }) + } + public moveCursor(position: IControlInitOption): IMoveCursorResult { const { index, trIndex, tdIndex, tdValueIndex } = position let elementList = this.draw.getOriginalElementList() @@ -169,97 +209,12 @@ export class Control { } nextIndex++ } - if (!~leftIndex || !~rightIndex) return -1 + if (!~leftIndex || !~rightIndex) return startIndex // 删除元素 elementList.splice(leftIndex + 1, rightIndex - leftIndex) return leftIndex } - public shrinkBoundary() { - const elementList = this.getElementList() - const range = this.getRange() - const { startIndex, endIndex } = range - const startElement = elementList[startIndex] - const endElement = elementList[endIndex] - if (startIndex === endIndex) { - if (startElement.controlComponent === ControlComponent.PLACEHOLDER) { - // 找到第一个placeholder字符 - let index = startIndex - 1 - while (index > 0) { - const preElement = elementList[index] - if ( - preElement.controlId !== startElement.controlId || - preElement.controlComponent === ControlComponent.PREFIX - ) { - console.log(index) - range.startIndex = index - range.endIndex = index - break - } - index-- - } - } - } else { - // 首、尾为占位符时,收缩到最后一个前缀字符后 - if ( - startElement.controlComponent === ControlComponent.PLACEHOLDER || - endElement.controlComponent === ControlComponent.PLACEHOLDER - ) { - let index = endIndex - 1 - while (index > 0) { - const preElement = elementList[index] - if ( - preElement.controlId !== endElement.controlId - || preElement.controlComponent === ControlComponent.PREFIX - ) { - range.startIndex = index - range.endIndex = index - return - } - index-- - } - } - // 向右查找到第一个Value - if (startElement.controlComponent === ControlComponent.PREFIX) { - let index = startIndex + 1 - while (index < elementList.length) { - const nextElement = elementList[index] - if ( - nextElement.controlId !== startElement.controlId - || nextElement.controlComponent === ControlComponent.VALUE - ) { - range.startIndex = index - 1 - break - } else if (nextElement.controlComponent === ControlComponent.PLACEHOLDER) { - range.startIndex = index - 1 - range.endIndex = index - 1 - return - } - index++ - } - } - // 向左查找到第一个Value - if (endElement.controlComponent !== ControlComponent.VALUE) { - let index = startIndex - 1 - while (index > 0) { - const preElement = elementList[index] - if ( - preElement.controlId !== startElement.controlId - || preElement.controlComponent === ControlComponent.VALUE - ) { - range.startIndex = index - break - } else if (preElement.controlComponent === ControlComponent.PLACEHOLDER) { - range.startIndex = index - range.endIndex = index - return - } - index-- - } - } - } - } - public removePlaceholder(startIndex: number) { const elementList = this.getElementList() const startElement = elementList[startIndex] diff --git a/src/editor/core/draw/control/select/SelectControl.ts b/src/editor/core/draw/control/select/SelectControl.ts index e3608fa..9677995 100644 --- a/src/editor/core/draw/control/select/SelectControl.ts +++ b/src/editor/core/draw/control/select/SelectControl.ts @@ -1,3 +1,254 @@ -export class SelectControl { +import { EDITOR_COMPONENT } from '../../../../dataset/constant/Editor' +import { ControlComponent } from '../../../../dataset/enum/Control' +import { EditorComponent } from '../../../../dataset/enum/Editor' +import { KeyMap } from '../../../../dataset/enum/Keymap' +import { IControlInstance } from '../../../../interface/Control' +import { IElement } from '../../../../interface/Element' +import { Control } from '../Control' + +export class SelectControl implements IControlInstance { + + private element: IElement + private control: Control + private isPopup: boolean + private selectDom: HTMLDivElement | null + + constructor(element: IElement, control: Control) { + this.element = element + this.control = control + this.isPopup = false + this.selectDom = null + } + + public getElement(): IElement { + return this.element + } + + public getCode(): string | null { + return this.element.control?.code || null + } + + public getValue(): IElement[] { + const elementList = this.control.getElementList() + const { startIndex } = this.control.getRange() + const startElement = elementList[startIndex] + const data: IElement[] = [] + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX + ) { + break + } + if (preElement.controlComponent === ControlComponent.VALUE) { + data.unshift(preElement) + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.POSTFIX + ) { + break + } + if (nextElement.controlComponent === ControlComponent.VALUE) { + data.push(nextElement) + } + nextIndex++ + } + return data + } + + public setValue(): number { + const range = this.control.getRange() + return range.endIndex + } + + public keydown(evt: KeyboardEvent): number { + const elementList = this.control.getElementList() + const range = this.control.getRange() + // 收缩边界到Value内 + this.control.shrinkBoundary() + const { startIndex, endIndex } = range + const startElement = elementList[startIndex] + const endElement = elementList[endIndex] + // backspace + if (evt.key === KeyMap.Backspace) { + // 清空选项 + if (startIndex !== endIndex) { + return this.clearSelect() + } else { + if ( + startElement.controlComponent === ControlComponent.PREFIX || + endElement.controlComponent === ControlComponent.POSTFIX || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 清空选项 + return this.clearSelect() + } + } + } else if (evt.key === KeyMap.Delete) { + // 移除选区元素 + if (startIndex !== endIndex) { + // 清空选项 + return this.clearSelect() + } else { + const endNextElement = elementList[endIndex + 1] + if (startElement.controlComponent === ControlComponent.PREFIX || + endNextElement.controlComponent === ControlComponent.POSTFIX || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 清空选项 + return this.clearSelect() + } + } + } + return endIndex + } + + public cut(): number { + this.control.shrinkBoundary() + const { startIndex, endIndex } = this.control.getRange() + if (startIndex === endIndex) { + return startIndex + } + // 清空选项 + return this.clearSelect() + } + + public clearSelect(): number { + const elementList = this.control.getElementList() + const { startIndex } = this.control.getRange() + const startElement = elementList[startIndex] + let leftIndex = -1 + let rightIndex = -1 + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX + ) { + leftIndex = preIndex + break + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.POSTFIX + ) { + rightIndex = nextIndex - 1 + break + } + nextIndex++ + } + if (!~leftIndex || !~rightIndex) return -1 + // 删除元素 + elementList.splice(leftIndex + 1, rightIndex - leftIndex) + // 增加占位符 + this.control.addPlaceholder(preIndex) + this.element.control!.code = null + return preIndex + } + + public setSelect(code: string) { + const control = this.element.control! + const valueSets = control.valueSets + if (!Array.isArray(valueSets) || !valueSets.length) return + // 转换code + const valueSet = valueSets.find(v => v.code === code) + if (!valueSet) return + // 清空选项 + const startIndex = this.clearSelect() + this.control.removePlaceholder(startIndex) + // 插入 + const elementList = this.control.getElementList() + const startElement = elementList[startIndex] + const start = startIndex + 1 + const data = valueSet.value.split('') + for (let i = 0; i < data.length; i++) { + elementList.splice(start + i, 0, { + ...startElement, + value: data[i], + controlComponent: ControlComponent.VALUE + }) + } + // render + const newIndex = start + data.length - 1 + this.control.repaintControl(newIndex) + // 设置状态 + this.element.control!.code = code + this.destroy() + } + + private _createSelectPopupDom() { + const control = this.element.control! + const valueSets = control.valueSets + if (!Array.isArray(valueSets) || !valueSets.length) return + const position = this.control.getPosition() + if (!position) return + // dom树:
+ const selectPopupContainer = document.createElement('div') + selectPopupContainer.classList.add('select-control-popup') + selectPopupContainer.setAttribute(EDITOR_COMPONENT, EditorComponent.POPUP) + const ul = document.createElement('ul') + for (let v = 0; v < valueSets.length; v++) { + const valueSet = valueSets[v] + const li = document.createElement('li') + const code = this.getCode() + if (code === valueSet.code) { + li.classList.add('active') + } + li.onclick = () => { + this.setSelect(valueSet.code) + } + li.append(document.createTextNode(valueSet.value)) + ul.append(li) + } + selectPopupContainer.append(ul) + // 定位 + const { coordinate: { leftTop: [left, top] }, lineHeight } = position + const preY = this.control.getPreY() + selectPopupContainer.style.left = `${left}px` + selectPopupContainer.style.top = `${top + preY + lineHeight}px` + // 追加至container + const container = this.control.getContainer() + container.append(selectPopupContainer) + this.selectDom = selectPopupContainer + } + + public awake() { + if (this.isPopup) return + const { startIndex } = this.control.getRange() + const elementList = this.control.getElementList() + if (elementList[startIndex + 1]?.controlId !== this.element.controlId) return + this._createSelectPopupDom() + this.isPopup = true + } + + public destroy() { + if (!this.isPopup) return + this.selectDom?.remove() + this.isPopup = false + } } \ No newline at end of file diff --git a/src/editor/core/draw/control/text/TextControl.ts b/src/editor/core/draw/control/text/TextControl.ts index 7068ac1..3d5521e 100644 --- a/src/editor/core/draw/control/text/TextControl.ts +++ b/src/editor/core/draw/control/text/TextControl.ts @@ -146,7 +146,7 @@ export class TextControl implements IControlInstance { } } } - return -1 + return endIndex } public cut(): number { diff --git a/src/editor/core/range/RangeManager.ts b/src/editor/core/range/RangeManager.ts index 3a02e5d..34d6507 100644 --- a/src/editor/core/range/RangeManager.ts +++ b/src/editor/core/range/RangeManager.ts @@ -1,4 +1,5 @@ import { ElementType } from '../..' +import { ControlComponent } from '../../dataset/enum/Control' import { IEditorOption } from '../../interface/Editor' import { IElement } from '../../interface/Element' import { IRange } from '../../interface/Range' @@ -137,6 +138,90 @@ export class RangeManager { }) } + public shrinkBoundary() { + const elementList = this.draw.getElementList() + const range = this.getRange() + const { startIndex, endIndex } = range + const startElement = elementList[startIndex] + const endElement = elementList[endIndex] + if (startIndex === endIndex) { + if (startElement.controlComponent === ControlComponent.PLACEHOLDER) { + // 找到第一个placeholder字符 + let index = startIndex - 1 + while (index > 0) { + const preElement = elementList[index] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX + ) { + range.startIndex = index + range.endIndex = index + break + } + index-- + } + } + } else { + // 首、尾为占位符时,收缩到最后一个前缀字符后 + if ( + startElement.controlComponent === ControlComponent.PLACEHOLDER || + endElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + let index = endIndex - 1 + while (index > 0) { + const preElement = elementList[index] + if ( + preElement.controlId !== endElement.controlId + || preElement.controlComponent === ControlComponent.PREFIX + ) { + range.startIndex = index + range.endIndex = index + return + } + index-- + } + } + // 向右查找到第一个Value + if (startElement.controlComponent === ControlComponent.PREFIX) { + let index = startIndex + 1 + while (index < elementList.length) { + const nextElement = elementList[index] + if ( + nextElement.controlId !== startElement.controlId + || nextElement.controlComponent === ControlComponent.VALUE + ) { + range.startIndex = index - 1 + break + } else if (nextElement.controlComponent === ControlComponent.PLACEHOLDER) { + range.startIndex = index - 1 + range.endIndex = index - 1 + return + } + index++ + } + } + // 向左查找到第一个Value + if (endElement.controlComponent !== ControlComponent.VALUE) { + let index = startIndex - 1 + while (index > 0) { + const preElement = elementList[index] + if ( + preElement.controlId !== startElement.controlId + || preElement.controlComponent === ControlComponent.VALUE + ) { + range.startIndex = index + break + } else if (preElement.controlComponent === ControlComponent.PLACEHOLDER) { + range.startIndex = index + range.endIndex = index + return + } + index-- + } + } + } + } + public render(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) { ctx.save() ctx.globalAlpha = this.options.rangeAlpha diff --git a/src/editor/dataset/enum/Editor.ts b/src/editor/dataset/enum/Editor.ts index f483592..7624423 100644 --- a/src/editor/dataset/enum/Editor.ts +++ b/src/editor/dataset/enum/Editor.ts @@ -3,7 +3,8 @@ export enum EditorComponent { MENU = 'menu', MAIN = 'main', FOOTER = 'footer', - CONTEXTMENU = 'contextmenu' + CONTEXTMENU = 'contextmenu', + POPUP = 'popup' } export enum EditorContext { diff --git a/src/editor/interface/Control.ts b/src/editor/interface/Control.ts index b7ededd..b0d773f 100644 --- a/src/editor/interface/Control.ts +++ b/src/editor/interface/Control.ts @@ -7,6 +7,7 @@ export interface IValueSet { } export interface IControlSelect { + code: string | null; valueSets: IValueSet[]; } diff --git a/src/editor/utils/element.ts b/src/editor/utils/element.ts index e1e89a1..89cf9f4 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -3,7 +3,7 @@ import { ElementType, IEditorOption, IElement } from '..' import { ZERO } from '../dataset/constant/Common' import { defaultControlOption } from '../dataset/constant/Control' import { EDITOR_ELEMENT_ZIP_ATTR } from '../dataset/constant/Element' -import { ControlComponent } from '../dataset/enum/Control' +import { ControlComponent, ControlType } from '../dataset/enum/Control' interface IFormatElementListOption { isHandleFirstElement?: boolean; @@ -71,7 +71,7 @@ export function formatElementList(elementList: IElement[], options: IFormatEleme } i-- } else if (el.type === ElementType.CONTROL) { - const { prefix, postfix, value, placeholder } = el.control! + const { prefix, postfix, value, placeholder, code, type, valueSets } = el.control! const controlId = getUUID() // 移除父节点 elementList.splice(i, 1) @@ -95,9 +95,23 @@ export function formatElementList(elementList: IElement[], options: IFormatEleme i++ } // 值 - if (value && value.length) { - for (let v = 0; v < value.length; v++) { - const element = value[v] + if ( + (value && value.length) || + (type === ControlType.SELECT && code && (!value || !value.length)) + ) { + let valueList: IElement[] = value || [] + if (!value || !value.length) { + if (Array.isArray(valueSets) && valueSets.length) { + const valueSet = valueSets.find(v => v.code === code) + if (valueSet) { + valueList = [{ + value: valueSet.value + }] + } + } + } + for (let v = 0; v < valueList.length; v++) { + const element = valueList[v] const valueStrList = element.value.split('') for (let e = 0; e < valueStrList.length; e++) { const value = valueStrList[e] diff --git a/src/mock.ts b/src/mock.ts index dd02f25..16627b2 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -91,6 +91,7 @@ elementList.splice(112, 0, { control: { type: ControlType.SELECT, value: null, + code: null, placeholder: '有无', prefix: '{', postfix: '}', @@ -100,6 +101,9 @@ elementList.splice(112, 0, { }, { value: '无', code: '98176' + }, { + value: '不详', + code: '98177' }] } })