H5 富文本快捷输入

✍ 记录一下

功能

  • 富文本功能
  • 快捷键关键词

效果

😂 有点儿粗糙

image

逻辑

按照Youtobe上的富文本视频教程进行拓展开发,使用iframe标签,通过修改designMode属性,将iframe的body标签修改成可编辑标签。再通过监听键盘按👇事件,判断是否是关键词,执行对应事件。点击通过window.parent.postMessage发送从iframe里向父节点发送消息,window.addEventListener("message",(ev)=>{})通过监听事件来获取回调id进行唤起选择框。

知识 丶

来自 https://developer.mozilla.org/zh-CN/docs/Web/API/

至于这些知识点我也怎么了解.

document.createDocumentFragment()

DocumentFragments (en-US) 是 DOM 节点。它们不是主 DOM 树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到 DOM 树。在 DOM 树中,文档片段被其所有的子元素所代替。
因为文档片段存在于内存中,并不在 DOM 树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。

document.execCommand()

当一个 HTML 文档切换到设计模式时,document暴露 **execCommand **方法,该方法允许运行命令来操纵可编辑内容区域的元素。
大多数命令影响document的 selection(粗体,斜体等),当其他命令插入新元素(添加链接)或影响整行(缩进)。当使用contentEditable时,调用 execCommand() 将影响当前活动的可编辑元素。

Window.getSelection()

返回一个 Selection 对象,表示用户选择的文本范围或光标的当前位置。

Selection.getRangeAt()

返回一个包含当前选区内容的区域对象。

Range.deleteContents()

不像Range.extractContents一样,本方法不会返回一个包含删除内容的文本片段DocumentFragment

Range.collapse()

折叠后的 Range 为空,不包含任何内容。
要确定 Range 是否已折叠,使用Range.collapsed 属性。
一个布尔值: true 折叠到 Range 的 start 节点,false 折叠到 end 节点。如果省略,则默认为 false

Selection.removeAllRanges()

Selection.removeAllRanges() 方法会从当前 selection 对象中移除所有的 range 对象,取消所有的选择只 留下anchorNode 和focusNode属性并将其设置为 null。

Range.setStartAfter()

Range.setStartAfter() 方法设置相对于节点的范围的起始位置。范围开始的父节点将与ReferenceCode的父节点相同。

源码

https://github.com/linyisonger/H5.Examples

<!DOCTYPE html>
<html lang="en">

<head>
    <title>Document</title>
    <script src="https://kit.fontawesome.com/af95122a22.js" crossorigin="anonymous"></script>
    <style>
        body {
            margin: 0;
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        .toolbar {
            margin: 10px 0;
            width: 1000px;

        }

        .toolbar__btn {
            border: none;
            outline: none;
            border-radius: 0;
            padding: 2px 7px;
            background: #fff;
            border: 1px solid #ddd;
            box-shadow: 2px 2px 2px 0 rgba(0, 0, 0, 0.2);
            cursor: pointer;
        }

        .toolbar>[class^='toolbar__'] {
            margin-top: 4px;
        }


        [class^='toolbar__'] {
            display: inline-flex;
            align-items: center;
        }

        [class^='toolbar__']>select {
            border: 1px solid #ddd;
            box-shadow: 2px 2px 2px 0 rgba(0, 0, 0, 0.2);
            padding: 2px 7px;
            outline: none;
        }

        [class^='toolbar__']>input[type='color'] {
            box-shadow: 2px 2px 2px 0 rgba(0, 0, 0, 0.2);
            width: 20px;
            height: 20px;
            border-radius: 50%;
            border: none;
            outline: none;
            appearance: textfield;
        }

        [class^='toolbar__']>input[type="color"]::-webkit-color-swatch-wrapper {
            display: none;
        }

        [class^='toolbar__']>input[type="color"]::-webkit-color-swatch {
            display: none;
        }

        .toolbar__label {
            display: inline;
            padding: 2px 7px;
            font-size: 14px;
            color: #777;
            margin-top: 2px;
        }

        .toolbar__btn:hover {
            background: #ddd;
        }

        .toolbar__btn[active="true"] {
            box-shadow: none;
            padding-top: 3px;
            border: 1px solid transparent;
            background: #ddd;
        }

        .toolbar__btn[active="true"]:hover {
            background: #fff;
        }

        iframe[name="richTextField"] {
            width: 1000px;
            height: 500px;
            border-color: #ddd;
            border-width: 1px;
            border-style: dashed;
        }

        iframe[name="richTextField"]:checked {
            border-color: #19c;
        }

        .select-down-list {
            border: 1px solid #19c;
            padding: 0;
            margin-top: 5px;
        }

        .select-down-list-option {
            padding: 2px 10px;
            cursor: default;
        }

        .select-down-list-option.active {
            background-color: #19c;
            color: #fff;
        }
    </style>
</head>

<body onload="enableEditMode();">
    <div class="toolbar">
        <button class="toolbar__btn" name="bold" onclick="execCmd('bold');">
            <i class="fa fa-bold"></i>
        </button>
        <button class="toolbar__btn" name="italic" onclick="execCmd('italic');">
            <i class="fa fa-italic"></i>
        </button>
        <button class="toolbar__btn" name="underline" onclick="execCmd('underline');">
            <i class="fa fa-underline"></i>
        </button>
        <button class="toolbar__btn" name="strikethrough" onclick="execCmd('strikeThrough');">
            <i class="fa fa-strikethrough"></i>
        </button>
        <button class="toolbar__btn" name="justifyLeft" onclick="execCmd('justifyLeft');">
            <i class="fa fa-align-left"></i>
        </button>
        <button class="toolbar__btn" name="justifyCenter" onclick="execCmd('justifyCenter');">
            <i class="fa fa-align-center"></i>
        </button>
        <button class="toolbar__btn" name="justifyRight" onclick="execCmd('justifyRight');">
            <i class="fa fa-align-right"></i>
        </button>
        <button class="toolbar__btn" name="justifyFull" onclick="execCmd('justifyFull');">
            <i class="fa fa-align-justify"></i>
        </button>
        <button class="toolbar__btn" name="cut" onclick="execCmd('cut');">
            <i class="fa fa-cut"></i>
        </button>
        <button class="toolbar__btn" name="copy" onclick="execCmd('copy');">
            <i class="fa fa-copy"></i>
        </button>
        <button class="toolbar__btn" name="indent" onclick="execCmd('indent');">
            <i class="fa fa-indent"></i>
        </button>
        <button class="toolbar__btn" name="outdent" onclick="execCmd('outdent');">
            <i class="fa fa-dedent"></i>
        </button>
        <button class="toolbar__btn" name="subscript" onclick="execCmd('subscript');">
            <i class="fa fa-subscript"></i>
        </button>
        <button class="toolbar__btn" name="superscript" onclick="execCmd('superscript');">
            <i class="fa fa-superscript"></i>
        </button>
        <button class="toolbar__btn" name="undo" onclick="execCmd('undo');">
            <i class="fa fa-undo"></i>
        </button>
        <button class="toolbar__btn" name="redo" onclick="execCmd('redo');">
            <i class="fa fa-repeat"></i>
        </button>
        <button class="toolbar__btn" name="insertUnorderedList" onclick="execCmd('insertUnorderedList');">
            <i class="fa fa-list-ul"></i>
        </button>
        <button class="toolbar__btn" name="insertOrderedList" onclick="execCmd('insertOrderedList');">
            <i class="fa fa-list-ol"></i>
        </button>
        <button class="toolbar__btn" name="insertParagraph" onclick="execCmd('insertParagraph');">
            <i class="fa fa-paragraph"></i>
        </button>
        <div class="toolbar__select">
            <select onclick="execCommandWithArg('formatBlock',this.value)">
                <option value="H1">H1</option>
                <option value="H2">H2</option>
                <option value="H3">H3</option>
                <option value="H4">H4</option>
                <option value="H5">H5</option>
                <option value="H6">H6</option>
            </select>
        </div>
        <button class="toolbar__btn" name="insertHorizontalRule" onclick="execCmd('insertHorizontalRule');">
            HR
        </button>
        <button class="toolbar__btn" name="createLink"
            onclick="execCommandWithArg('createLink',prompt('Enter a URL','http://'));">
            <i class="fa fa-link"></i>
        </button>
        <button class="toolbar__btn" name="unlink" onclick="execCmd('unlink');">
            <i class="fa fa-unlink"></i>
        </button>
        <button class="toolbar__btn" name="showingSourceCode" onclick="toggleSource();">
            <i class="fa fa-code"></i>
        </button>
        <button class="toolbar__btn" name="isInEditMode" onclick="toggleEdit();">
            开启编辑
        </button>
        <div class="toolbar__select">
            <select onclick="execCommandWithArg('fontName',this.value);">
                <option value="Arial">Arial</option>
                <option value="Comic Sanc MS">Comic Sanc MS</option>
                <option value="Courier">Courier</option>
                <option value="Georgia">Georgia</option>
                <option value="Tahoma">Tahoma</option>
                <option value="Times New Roman">Times New Roman</option>
                <option value="Verdana">Verdana</option>
            </select>
        </div>
        <div class="toolbar__select">
            <div class="toolbar__label">字号:</div>
            <select onclick="execCommandWithArg('fontSize',this.value);">
                <option value="1">1</option>
                <option value="2">2</option>
                <option value="3">3</option>
                <option value="4">4</option>
                <option value="5">5</option>
                <option value="6">6</option>
                <option value="7">7</option>
            </select>
        </div>
        <div class="toolbar__color">
            <div class="toolbar__label">字体颜色:</div>
            <input type="color" name="foreColor" onchange="execCommandWithArg('foreColor',this.value);" />
        </div>
        <div class="toolbar__color">
            <div class="toolbar__label">字体背景:</div>
            <input type="color" name="hiliteColor" onchange="execCommandWithArg('hiliteColor',this.value);" />
        </div>
        <button class="toolbar__btn" name="insertImage"
            onclick="execCommandWithArg('insertImage',prompt('Endter this image URL',''));">
            <i class="fa fa-file-image-o"></i>
        </button>
        <button class="toolbar__btn" name="selectAll" onclick="execCmd('selectAll');">
            全选
        </button>
    </div>
    <iframe name="richTextField" style="width: 1000px; height: 500px;"></iframe>

    <script type="text/javascript">
        /** 编辑类 */
        class Editor {
            /** @type {Boolean} */
            bold = false;
            /** @type {Boolean} */
            showingSourceCode = false;
            /** @type {Boolean} */
            isInEditMode = false;
            /** @type {HTMLDivElement} */
            toolbar = null
            /** @type {String} */
            foreColor = ''
            /** @type {String} */
            hiliteColor = ''
            /** @type {HTMLIFrameElement} */
            iframe = null
            /** @type {HTMLDivElement} */
            selectDownList = null
            /** @type {number} */
            selectDownIndex = 0
            /** @type {HTMLSpanElement} */
            selectSpan = null
            /** @type {Document} */
            get document() {
                return this.iframe.document;
            }

            get body() {
                let [body] = this.document.getElementsByTagName('body')
                return body
            }

            get value() {
                return this.body.innerHTML
            }

            get text() {
                return this.body.textContent
            }
        }

        /** @type {Editor} */
        let editor = null

        let keyOfActive = ['bold', 'isInEditMode', 'showingSourceCode']
        let keyOfColor = ['foreColor', 'hiliteColor']

        /** 启用编辑模式 */
        function enableEditMode() {
            editor = new Editor()
            initEditor();
            initToolbar();
            toggleEdit();
            execCommandWithArg('foreColor', 'rgb(0,0,0)');
            execCommandWithArg('hiliteColor', 'rgb(255,255,255)');
            // execCmd('AbsolutePosition');
            richTextField.focus()
        }
        /**
         * 初始化编辑器
         */
        function initEditor() {
            editor.iframe = richTextField;
            let script = document.createElement("script")
            script.type = 'text/javascript'
            script.innerHTML = `function showDownSelectList(e) { console.log(e); window.parent.postMessage({ type: 'showDownSelectList', id:e.id },"*") }`
            editor.document.head.appendChild(script)
            editor.iframe = richTextField;
            Object.keys(editor).forEach((key) => {
                let value = editor[key];
                Object.defineProperty(editor, key, {
                    enumerable: true,
                    configurable: true,
                    get() {
                        // console.log(`访问了属性:${key} -> 值:${value}`);
                        return value;
                    },
                    set(newValue) {
                        value = newValue;
                        // console.log(`属性${key}的值${value}修改为 -> ${newValue}`);
                        if (keyOfActive.includes(key)) setActive(editor.toolbar.querySelector(`[name='${key}']`), value);
                        if (keyOfColor.includes(key)) setColor(editor.toolbar.querySelector(`[name='${key}']`), value)
                    }
                })
            })
            window.addEventListener("message", e => {
                if (e.data.type === "showDownSelectList")
                    showDownSelectList(e.data.id)
            })
            editor.document.onkeydown = function (e) {
                if (editor.selectDownList) {
                    if (e.key == 'Enter') {
                        editor.selectSpan.textContent = editor.selectDownList.children.item(editor.selectDownIndex).textContent;
                        pasteHtmlAtCaret('')
                        deleteDownSelectList();
                        e.preventDefault();
                    }
                    else if (e.key == 'ArrowDown') {
                        editor.selectDownIndex++;
                        if (editor.selectDownIndex === editor.selectDownList.children.length) editor.selectDownIndex = 0
                        selectDownListOptionChange()
                        e.preventDefault();
                    }
                    else if (e.key == 'ArrowUp') {
                        editor.selectDownIndex--;
                        if (editor.selectDownIndex === -1) editor.selectDownIndex = editor.selectDownList.children.length - 1
                        selectDownListOptionChange()
                        e.preventDefault();
                    }
                    else {
                        deleteDownSelectList()
                    }
                }
            }
        }


        /** 获取工具栏 */
        function initToolbar() {
            const toolbar = document.querySelector('.toolbar')
            editor.toolbar = toolbar;
        }


        /**
         * @param {HTMLDivElement} document 
         * @param {Boolean} active 
         */
        function setActive(document, active) {
            document.setAttribute('active', active)
        }
        /**
         * @param {HTMLDivElement} document 
         * @param {string} color 
         */
        function setColor(document, color) {
            document.style.backgroundColor = color;
        }

        /**
         * 执行对应的命令
         * @param {string} command 
         */
        function execCmd(command) {
            editor.document.execCommand(command, false, null)
            if (keyOfActive.includes(command)) editor[command] = !editor[command]
        }


        function execCommandWithArg(command, arg) {
            editor.document.execCommand(command, false, arg)
            if (keyOfColor.includes(command)) editor[command] = arg
        }

        function toggleSource() {
            let [body] = editor.document.getElementsByTagName('body')
            if (editor.showingSourceCode) {
                body.innerHTML = body.textContent;
                editor.showingSourceCode = false;
            }
            else {
                body.textContent = body.innerHTML;
                editor.showingSourceCode = true;
            }
        }
        function toggleEdit() {
            if (editor.isInEditMode) {
                editor.document.designMode = 'Off'
                editor.isInEditMode = false;
                editor.iframe.onkeyup = keywordSelect
            }
            else {
                editor.document.designMode = 'On'
                editor.isInEditMode = true;
                editor.iframe.addEventListener("keyup", keywordSelect)
            }
        }

        /**
         * 插入元素
         */
        function pasteHtmlAtCaret(html) {
            var sel, range;
            if (editor.document.getSelection) {
                // IE9 and non-IE
                sel = editor.document.getSelection();
                if (sel.getRangeAt && sel.rangeCount) {
                    range = sel.getRangeAt(0);
                    range.deleteContents();
                    // Range.createContextualFragment() would be useful here but is
                    // non-standard and not supported in all browsers (IE9, for one)
                    var el = document.createElement("div");
                    el.innerHTML = html;
                    var frag = document.createDocumentFragment(), node, lastNode;
                    while ((node = el.firstChild)) {
                        lastNode = frag.appendChild(node);
                    }
                    range.insertNode(frag);

                    // Preserve the selection
                    if (lastNode) {
                        range = range.cloneRange();
                        range.setStartAfter(lastNode);
                        range.collapse(true);
                        sel.removeAllRanges();
                        sel.addRange(range);
                    }
                }
            } else if (document.selection && document.selection.type != "Control") {
                // IE < 9
                editor.document.selection.createRange().pasteHTML(html);
            }
        }

        function uuid() {
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
                var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16);
            });
        }

        /**
         * @param {KeyboardEvent} ev
         */
        function keywordSelect(ev) {
            if (ev.key === '[') {
                execCmd('delete')
                let id = `span_${uuid()}`
                let keywordSelect = getKeywordSelect(id);
                pasteHtmlAtCaret(keywordSelect)
                showDownSelectList(id)
            }
        }

        function getKeywordSelect(id) {
            return `<span id="${id}" contenteditable="false" style="color: rgb(17, 153, 255);display: inline-block;" οnclick="showDownSelectList(this)">[请选择]</span>`
        }


        /**
         * @param {string} id
         */
        function showDownSelectList(id) {
            if (editor.selectDownList) deleteDownSelectList()
            let span = editor.document.querySelector(`#${id}`)
            let select = document.createElement("div");
            let iframe = document.querySelector('iframe')
            let { offsetLeft, offsetTop, clientHeight } = span

            editor.selectSpan = span;
            select.className = 'select-down-list'
            select.style = `position: absolute; left:${iframe.offsetLeft + offsetLeft}px;top:${iframe.offsetTop + offsetTop + clientHeight}px;`

            for (let i = 0; i < 10; i++) {
                let option = document.createElement("div");
                option.setAttribute('value', i)
                option.textContent = '选项' + i
                option.className = 'select-down-list-option'
                option.onclick = function (e) {
                    editor.selectDownIndex = i;
                    editor.selectSpan.textContent = '选项' + i;
                    deleteDownSelectList();
                    pasteHtmlAtCaret('')
                }
                select.appendChild(option)
            }
            document.body.append(select)
            editor.selectDownList = select;
            selectDownListOptionChange(0);
        }

        function selectDownListOptionChange(index) {
            if (index !== undefined) editor.selectDownIndex = index;
            for (let i = 0; i < editor.selectDownList.children.length; i++) {
                const option = editor.selectDownList.children.item(i);
                option.className = i === editor.selectDownIndex ? 'select-down-list-option active' : 'select-down-list-option'
            }
        }

        function deleteDownSelectList() {
            editor.selectDownList.remove();
            editor.selectDownList = null;
        }
    </script>
</body>

</html>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林一怂儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值