原生API编写富文本编辑器004
遗留的问题:
- 设置的字体是使用 font属性,而非CSS
- 设置的字号只接受1-7, 并且是以 size 属性而非 CSS控制,超出大小无法设置。
- color使用HTML的input时,始终有一个input框在那里,并且如果手动触发click显示调色板,则调色板的位置无法自动跟随
- link 只能创建或取消,无法修改,无法指定是以何种方式打开
- link和image填写框聚焦时编辑器选区会被取消
设置字体字号使用的是HTML属性与标签,而非CSS
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z9SjtAUo-1670513126376)(https://gitee.com/hjb2722404/tuchuang/raw/master/img/202205131611343.png)]
可以看到,在默认情况下,我们对文本的大多数操作都是使用HTML属性或标签的方式完成样式设置的。
如果想让浏览器使用CSS来设置这些样式,那么在编辑器加载前,执行styleWithCSS
命令,将设置的模式设置为css模式即可:
window.onload= function() {
document.execCommand('styleWithCSS', false, '');
//...
可以看到,这样浏览器就使用css来设置对应样式了,但又有新的问题,即字号不是我们想的按照像素设置的,而是按照浏览器定义的大小描述来设置的。
link和image填写框聚焦时编辑器选区会被取消
这个问题可以通过两种方式解决:
- 我们现在的可编辑区域是一个div,而我们的input框与该div同属一个文档,所以当input获得焦点时,可编辑区域就会失去焦点从而失去选区,所以我们只需要将div换成一个
frame
,将可编辑区放置到iframe
里的文档中,这样就不会抢夺焦点了。 - 输入框不使用自己写的input,而是使用浏览器的prompt 框,这样也不会与div抢夺焦点。
我们后面使用第一种方式改造,第二种方式有兴趣的读者朋友可以自行尝试。
// index.html
<iframe id="editorContent" class="editor-content" contenteditable="true" frameborder="0"></iframe>
//index.css
.editor-content {
width: 100%;
height: 500px;
overflow: auto;
padding-top: 20px;
}
// index.js
var editor;
window.onload= function() {
editor = document.getElementById("editorContent").contentWindow;//获取iframe Window 对象
editor.document.designMode = 'On'; //打开设计模式
editor.document.contentEditable = true;// 设置元素为可编辑
editor.document.execCommand('styleWithCSS', false, '');
// 后续文件中所有document.execCommand 改为 editor.document.execCommand, 例如:
const rs = editor.document.execCommand('fontName', true, target.value);
其它问题
要解决其它问题,则需要引入浏览器的另外两个API:range
与 selection
;
我们下一节再说。
本系列文章代码可从gitee获取
以上代码可在1.0.5
分支上找到。
代码优化
之前我们为了讲解功能实现的具体逻辑和原理,使用的是过程式编码方式,看着很不优雅,而且有很多冗余,下来我们就一步一步优化一下实现方式。
工具条动态生成
我们现在的工具条所有按钮,都是写死在html中的,每个按钮一个li标签,但是这样,一是按钮越多,代码就越多,二是不方便扩展,每次新增一个功能按钮,都要去改html模板。
我们改为使用js动态生成dom的方式来改写。
// index.js
window.onload= function() {
createEditorBar();
// ...
function createEditorBar() {
let $tpl ='<ul>';
const commandsMap = {
'undo': {
icon: 'chexiao',
title: '撤销',
},
'redo': {
icon: 'zhongzuo',
title: '重做',
},
'copy': {
icon: 'fuzhi',
title: '复制',
},
'cut': {
icon: 'jianqie',
title: '剪切',
},
'fontName': {
icon: 'ziti',
title: '字体',
},
'fontSize': {
icon: 'zihao',
title: '字号',
},
'bold': {
icon: 'zitijiacu',
title: '加粗',
},
'italic': {
icon: 'zitixieti',
title: '斜体',
},
'underline': {
icon: 'zitixiahuaxian',
title: '下划线',
},
'strikeThrough': {
icon: 'zitishanchuxian',
title: '删除线',
},
'superscript': {
icon: 'zitishangbiao',
title: '上标',
},
'subscript': {
icon: 'zitixiabiao',
title: '下标',
},
'fontColor': {
icon: 'qianjingse',
title: '字体颜色',
},
'backColor': {
icon: 'zitibeijingse',
title: '字体背景色',
},
'removeFormat': {
icon: 'qingchugeshi',
title: '清除格式',
},
'insertOrderedList': {
icon: 'youxuliebiao',
title: '有序列表',
},
'insertUnorderedList': {
icon: 'wuxuliebiao',
title: '无序列表',
},
'justifyLeft': {
icon: 'juzuoduiqi',
title: '居左对齐',
},
'justifyRight': {
icon: 'juyouduiqi',
title: '居右对齐',
},
'justifyCenter': {
icon: 'juzhongduiqi',
title: '居中对齐',
},
'justifyFull': {
icon: 'liangduanduiqi',
title: '两端对齐',
},
'createLink': {
icon: 'charulianjie',
title: '插入链接',
},
'unlink': {
icon: 'quxiaolianjie',
title: '取消链接',
},
'indent': {
icon: 'shouhangsuojin',
title: '首行缩进',
},
'insertImage': {
icon: 'tupian',
title: '插入图片',
},
};
for (key in commandsMap) {
$tpl += `<li><button command="${key}"><i class="iconfont icon-${commandsMap[key].icon}" title="${commandsMap[key].title}"></i></button></li>`;
}
$tpl += '</ul>';
const editorBar = document.getElementById('editorBar');
editorBar.innerHTML = $tpl;
}
// index.html
<div id="editorBar" class="editor-toolbar"></div>
统一的下拉框生成方法
目前的下拉框,我们都是新生成按钮,然后再在编辑器初始化的时候动态生成将按钮替换掉的,而且每一个下拉框都有一个单独的生成方法,代码冗余比较多,我们统一使用相同方法生成下拉框的dom,并且在生成工具条的时候直接渲染。
// index.js
const commandsMap = {
//...
'fontName': {
icon: 'ziti',
title: '字体',
options: [
{
key: '仿宋',
value: "'仿宋'",
},
{
key: '黑体',
value: "'黑体'",
},
{
key: '楷体',
value: "'楷体'",
},
{
key: '宋体',
value: "'宋体'",
},
{
key: '微软雅黑',
value: "'微软雅黑'",
},
{
key: '新宋体',
value: "'新宋体'",
},
{
key: 'Calibri',
value: "'Calibri'",
},
{
key: 'Consolas',
value: "'Consolas'",
},
{
key: 'Droid Sans',
value: "'Droid Sans'",
},
{
key: 'Microsoft YaHei',
value: "'Microsoft YaHei'",
},
],
styleName: 'font-family',
},
'fontSize': {
icon: 'zihao',
title: '字号',
options: [
{
key: '12',
value: '12px',
},
{
key: '13',
value: '13px',
},
{
key: '16',
value: '16px',
},
{
key: '18',
value: '18px',
},
{
key: '24',
value: '24px',
},
{
key: '32',
value: '32px',
},
{
key: '48',
value: '48px',
},
],
styleName: 'font-size',
},
}
//...
for (key in commandsMap) {
if (commandsMap[key].options) {
let id = key + 'Selector';
let customStyleName = commandsMap[key].styleName;
$tpl += getSelectTpl(id, commandsMap[key].options, customStyleName);
} else {
$tpl += `<li><button command="${key}"><i class="iconfont icon-${commandsMap[key].icon}" title="${commandsMap[key].title}"></i></button></li>`;
}
}
function getSelectTpl(id, options, customStyleName) {
let $tpl= `<li><select id="${id}">`;
for (let i = 0; i < options.length; i++) {
$tpl += `<option value="${options[i].value}" style="${customStyleName}: ${options[i].value}">${options[i].key}</option>`;
}
$tpl += '</select></li>';
return $tpl;
}
const editorBar = document.getElementById('editorBar');
editorBar.innerHTML = $tpl;
addSelectorEventListener('fontName');
addSelectorEventListener('fontSize');
function addSelectorEventListener(key) {
const $el = document.getElementById(key + 'Selector');
$el.addEventListener('change', function(e) {
eval('select' + key.substr(0, 1).toUpperCase() + key.substr(1) + '()');
});
}
function selectFontName() {
const target = document.getElementById('fontNameSelector');
const rs = editor.document.execCommand('fontName', true, target.value);
}
function selectFontSize() {
const valueMap = {
'12px': 1,
'13px': 2,
'16px': 3,
'18px': 4,
'24px': 5,
'32px': 6,
'48px': 7,
};
const target = document.getElementById('fontSizeSelector');
const value = valueMap[target.value];
const rs = editor.document.execCommand('fontSize', true, value);
}
统一的对话框生成方法
目前输入超级链接和网络图片地址都使用了一个简单的对话框,这两部分的代码有很多重复和冗余,需要进行优化。
var dialogFun;
case 'createLink':
showDialog(btn, 'link');
break;
case 'insertImage':
showDialog(btn, 'image');
break;
function showDialog(btn, type) {
const upperType = firstLetterToUppercase(type);
const tpl = getDialogTpl(type);
showDialogTpl(btn, tpl);
const dialog = document.getElementById(type + 'Dialog');
dialog.focus();
const createDialogBtn = document.getElementById('create' + upperType + 'Btn');
dialogFun = createDialog.bind(this, type);
createDialogBtn.addEventListener('click', dialogFun, false);
}
function getDialogTpl(type) {
const upperType = firstLetterToUppercase(type);
const tpl = `
<input type="text" id="${type}Dialog" />
<button id="create${upperType}Btn">确定</button>
`;
return tpl;
}
function showDialogTpl(btn, tpl) {
const $dialog = document.getElementById('editorDialog');
$dialog.innerHTML = tpl;
$dialog.style.top = (btn.offsetTop + btn.offsetHeight + 15) + 'px';
$dialog.style.left = btn.offsetLeft + 'px';
$dialog.style.display = 'block';
}
function createDialog(type) {
const upperType = firstLetterToUppercase(type);
const dialog = document.getElementById(type + 'Dialog');
editor.document.execCommand('create' + upperType, 'false', dialog.value);
const createDialogBtn = document.getElementById('create' + upperType + 'Btn');
createDialogBtn.removeEventListener('click', dialogFun, false);
hideDialog();
}
function firstLetterToUppercase(str) {
return str.substr(0, 1).toUpperCase() + str.substr(1);
}
function hideDialog() {
const $dialog = document.getElementById('editorDialog');
$dialog.innerHTML = '';
$dialog.style.display = 'none';
}
至此,我们完成了基础的代码优化,其实就是提取了一些公共方法,通过参数不同来控制不同的输出。
本系列文章代码可从gitee获取
以上代码可以在 1.0.6
分支上找到
问题
现在又有新的问题了,现在我们的所有方法都是暴露在全局环境下的,甚至还有一些全局变量,如果我们的应用中只有一个编辑器实例还好,但是如果同一个页面有两个编辑器,就会很麻烦。
所以,下一节我们将对代码进行面向对象的改造,让同一个页面可以生成多个不同的编辑器实例,各个实例之间可以互不干扰。