20分钟创建基于Flex的Text Editor

本文介绍如何使用Flex自定义一个简易的文本编辑器,包括字体、大小、样式等基本功能,并探讨其工作原理和不足之处。
选择开源文本编辑器如TinyMCE、FCKeditor,当然是不错的选择,但定制自己的Text Editor也不困难。最近使用Flex开发Editor,研究了一下Flex自带的RichTextEditor,发现其工作原理非常简单。为了研究其工作原理及不足,我使用两个核心类自定义了一个Editor,效果图见附件。欢迎拍砖。


目标:用Flex定制自己的Text Editor
功能:字体,大小,粗体,斜体,下划线,列表(Bullet),对齐方式(Alignment),字体颜色。
开发环境:Flex Builder 3

[b]简介[/b]:

熟悉《设计模式》一书的开发人员都知道,此书从文档编辑器(document editor)展开讨论,引出数十种设计模式。本文对此不作讨论,我们所关注的是如何实现文档编辑器本身。更深入更复杂的例子,可以参考Jexi(参考价值更大,但没有实现Table),TinyMCE。


[b]相关内容[/b]:

[b]核心类--TextField和TextFormat[/b]

TextField和TextFormat是实现文本编辑器的核心部件。TextField包含了文本的输入、显示和格式化功能,还有光标、选中(selection)处理。TextField是DisplayObject,可以作为子元素添加到Container上。TextFormat是Util类,负责表示用户设置的格式(Style)。

[b]事件处理[/b]

用户交互,少不了事件处理。TextEvent类专门用于处理和Text相关的事件,例如用户键盘输入或点击一个超链接(Link)。除了TextEvent,还需要处理一些自定义的事件,例如文本区域的内容、格式改变。

[b]如何创建[/b]:

[b]1.新建Flex Application,工程命名为test[/b]。
[b] 2.新建MXML Component,基类是Panel类[/b]。

声明Event

<mx:Metadata>
[Event(name="change", type="flash.events.Event")]
[DefaultTriggerEvent("change")]
</mx:Metadata>


声明变量。

private var previousTextFormat:TextFormat = null;


重载createChildren()方法(初始化)

////////////////////////////////
//overridden
////////////////////////////////
override protected function createChildren():void{
super.createChildren();
createTextField(-1);
}


创建TextField控件本身,添加事件处理器。

private function createTextField(childIndex:int):void{
if(textField)return;

textField = IUITextField(createInFontContext(UITextField));
//textField.
textField.autoSize = TextFieldAutoSize.NONE;
textField.enabled = enabled;
textField.ignorePadding = true;
textField.multiline = true;
textField.selectable = true;
textField.styleName = this;
textField.tabEnabled = true;
textField.type = TextFieldType.INPUT;
textField.useRichTextClipboard = true;
textField.wordWrap = true;

textField.addEventListener(Event.CHANGE, textField_changeHandler);
textField.addEventListener(Event.SCROLL, textField_scrollHandler);
textField.addEventListener(TextEvent.TEXT_INPUT,
textField_textInputHandler);


if (childIndex == -1){
addChild(DisplayObject(textField));
//updateDisplayList();
}

}


应用文本格式
public function setTextStyles(type:String, value:Object = null):void{
var tf:TextFormat;

var beginIndex:int = textField.selectionBeginIndex;
var endIndex:int = textField.selectionEndIndex;

if (beginIndex == endIndex)
{
tf = previousTextFormat;
}
else
tf = new TextFormat();

if (type == "bold" || type == "italic" || type == "underline")
{
tf[type] = value;
}
else if (type == "align" || type == "bullet")
{
if (beginIndex == endIndex)
{
tf = new TextFormat();
}

// Apply the paragraph styles to the whole paragraph instead of just
// the selected text
beginIndex = textField.getFirstCharInParagraph(beginIndex) - 1;
beginIndex = Math.max(0, beginIndex);
endIndex = textField.getFirstCharInParagraph(endIndex) +
textField.getParagraphLength(endIndex) - 1;
tf[type] = value;
if(!previousTextFormat)
previousTextFormat = new TextFormat();
previousTextFormat[type] = value;
if (!endIndex)
textField.defaultTextFormat = tf;
}
else if (type == "font")
{
tf[type] = fontFamilyCombo.text;
}
else if (type == "size")
{
var fontSize:uint = uint(fontSizeCombo.text);
if (fontSize > 0)
tf[type] = fontSize;
}
else if (type == "color")
{
tf[type] = uint(colorPicker.selectedColor);
}

if (beginIndex == endIndex)
{
previousTextFormat = tf;
}
else
{
textField.setTextFormat(tf,beginIndex,endIndex);
}

dispatchEvent(new Event("change"));

var caretIndex:int = textField.caretIndex;
var lineIndex:int = textField.getLineIndexOfChar(caretIndex);

textField.invalidateDisplayList();

callLater(textField.setFocus);
}


添加ControlBar,编辑工具条
			<mx:ControlBar width="100%" > 
<mx:ToolBar id="toolbar" width="100%" horizontalGap="7">
<mx:ComboBox id="fontFamilyCombo" editable="true"
creationComplete=""
dataProvider = "{fontFamilyArray}"
close="setTextStyles('font');"
enter="setTextStyles('font');"/>
<mx:ComboBox id="fontSizeCombo" editable="true"
paddingLeft="2" paddingRight="2"
dataProvider = "{fontSizeArray}"
close="setTextStyles('size');"
enter="setTextStyles('size');"/>
<mx:HBox id="toolBar2" horizontalGap="0">
<mx:Button id="boldButton" width="20" toggle="true"
icon="@Embed('icon_style_bold.png')"
click="setTextStyles('bold', event.currentTarget.selected);" />
<mx:Button id="italicButton" width="20" toggle="true"
icon="@Embed('icon_style_italic.png')"
click="setTextStyles('italic', event.currentTarget.selected);" />
<mx:Button id="underlineButton" width="20" toggle="true"
icon="@Embed('icon_style_underline.png')"
click="setTextStyles('underline', event.currentTarget.selected);" />
</mx:HBox>
<mx:ColorPicker id="colorPicker" width="22" height="22"
close="setTextStyles('color');"/>
<mx:VRule height="{alignButtons.height}"/>
<mx:ToggleButtonBar id="alignButtons" buttonWidth="20"
itemClick="setTextStyles('align', ToggleButtonBar(event.currentTarget).dataProvider.getItemAt(ToggleButtonBar(event.currentTarget).selectedIndex).action); " >
<mx:dataProvider>
<mx:Array>
<mx:Object icon="@Embed('icon_align_left.png')" action="left"/>
<mx:Object icon="@Embed('icon_align_center.png')" action="center"/>
<mx:Object icon="@Embed('icon_align_right.png')" action="right"/>
<mx:Object icon="@Embed('icon_align_justify.png')" action="justify"/>
</mx:Array>
</mx:dataProvider>
</mx:ToggleButtonBar>
<mx:Button id="bulletButton" width="20" toggle="true"
icon="@Embed('icon_bullet.png')"
click="setTextStyles('bullet', event.currentTarget.selected);" />
</mx:ToolBar>
</mx:ControlBar>


3.在test.xml引用该组件

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:NewEditor="*">
<mx:Style>
Panel {
borderAlpha: 1;
borderThicknessLeft: 0;
borderThicknessTop: 0;
borderThicknessBottom: 0;
borderThicknessRight: 0;
roundedBottomCorners: true;
headerHeight: 23;
highlightAlphas: 0, 0.12;
headerColors: #4e84df, #0f6cc3;
footerColors: #efaa15, #e6780c;
titleStyleName: "mypanelTitle";
}
.mypanelTitle {
color: #ffffff;
fontSize: 12;
fontWeight:bold;
}
ControlBar {
paddingTop:2px;
paddingBottom:2px;
height:15px;
}
</mx:Style>
<NewEditor:NewEditor title="Text Editor" height="50%" />
</mx:Application>


注意,我们为Panel和ControlBar添加了样式(Style)。

[b]问题和不足[/b]:

上述自定义Editor基于RichTextEditor(基于TextArea),有兴趣的读者可以研究它们。本文探讨如何实现简易的文本Editor。真正的Editor,包含很多类型的图元编辑,例如文本、图像、表格。由于TextField和TextFormat仅限于文本功能,因此要实现图像和表格编辑,还有很多的工作要做,甚至要抛弃TextField和TextFormat,设计一套基于Glygh的图元渲染系统。限于篇幅,不再进一步讨论。
<template> <div style="display: flex; overflow: hidden; height: 100%;"> <div style="border: 1px solid #ccc; z-index: 9999999; flex: 1;"> <!-- 工具栏 --> <Toolbar :editor="editorRef" :mode="mode" style="border-bottom: 1px solid #ccc" /> <!-- 编辑器 --> <Editor :model-value="modelValue" :style="style" :disabled="disabled" :default-config="editorConfig" :mode="mode" @onCreated="handleCreated" @onChange="handleChange" /> </div> <div v-if="preview" style="width: 18vw; padding: 20px 20px; overflow: auto; margin-left: 20px; border: solid 2px rgb(193, 191, 191); border-radius: 40px; display: flex; flex-direction: column; overflow: hidden;"> <div style="text-align: center; font-size: 16px; margin-bottom: 20px;">预览</div> <div style="flex: 1; overflow: auto;" v-html="previewTxt"> </div> </div> </div> </template> <script lang="ts" setup> import "@wangeditor/editor/dist/css/style.css"; import { onBeforeUnmount, ref, shallowRef, watch } from "vue"; import { Editor, Toolbar } from "@wangeditor/editor-for-vue"; import { IDomEditor, IEditorConfig } from "@wangeditor/editor"; import app from "@/constants/app"; import { getToken } from "@/utils/cache"; const props = defineProps({ modelValue: { type: String, required: true }, mode: { type: String, default: "default" // 可选值:[default | simple] }, placeholder: { type: String, default: "" }, style: { type: String, default: "height: 300px;" }, disabled: { type: Boolean, default: false }, preview: { type: Boolean, default: false } }); // 编辑器实例,必须用 shallowRef const editorRef = shallowRef(); type InsertFnType = (url: string, alt: string, href: string) => void; // 编辑器配置 const editorConfig: Partial<IEditorConfig> = { placeholder: props.placeholder, readOnly: props.disabled, MENU_CONF: { uploadImage: { server: `${app.api}/sys/oss/upload?token=${getToken()}`, fieldName: "file", customInsert(res: any, insertFn: InsertFnType) { insertFn(res.data.src, "", ""); } } } }; // 组件销毁时,也及时销毁编辑器 onBeforeUnmount(() => { const editor = editorRef.value; if (editor == null) { return; } editor.destroy(); }); const handleCreated = (editor: IDomEditor) => { editorRef.value = editor; }; // 编辑器change事件触发 const emit = defineEmits(["update:modelValue"]); const handleChange = (editor: IDomEditor) => { emit("update:modelValue", editor.getHtml()); previewTxt.value = editor.getHtml(); }; const previewTxt = ref(""); </script> 这是我的源码 看看可以解决有关的这个问题?改好给我
08-22
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值