пятница, 14 июля 2023 г.

React Hook Portal

 import React, {useEffect, useRef} from 'react';

import ReactDOM from 'react-dom';


interface Props {

    children: React.ReactNode;

}


export function Portal (props: Props): JSX.Element {


    const container: React.MutableRefObject<HTMLDivElement> = useRef<HTMLDivElement>(document.createElement('div'));


    useEffect(function (): () => void {

        document.body.appendChild(container.current);

        const elementForRemoval: HTMLDivElement = container.current;


        return function (): void {

            document.body.removeChild(elementForRemoval);

        };

    }, [container]);


    return ReactDOM.createPortal(props.children, container.current);


}


interface DialogProps {

    open: boolean;

    children?: React.ReactNode;

}


export function Dialog (props: DialogProps): JSX.Element {

    if (props.open) {

        return (

            <Portal>

                <div>{props.children}</div>

            </Portal>

        );

    } else {

        return null;

    }

}

React Hook Disable parent DIV click

 import React, {useState} from 'react';


const [mouseButtonDownTime, setMouseButtonDownTime] = useState<Date>(null);


function handleSetMouseButtonDownTime (event: React.MouseEvent<HTMLDivElement>): void {

    if (event.button === 0) {

        setMouseButtonDownTime(new Date());

    }

}


function hasParent (child): boolean {

    let node = child.parentNode;

    while (node !== null) {

        if (node.classList && node.classList.contains('disabled-parent-click')) {

            return true;

        }

        node = node.parentNode;

    }

    return false;

}


function handleParentDivClick (message: string): (event: React.MouseEvent<HTMLDivElement>) => void {

    return function (event: React.MouseEvent<HTMLDivElement>): void {

        if (

            event.button === 0 &&

            mouseButtonDownTime !== null &&

            (new Date().getTime() - mouseButtonDownTime.getTime() < 200)

        ) {

            if (hasParent(event.target)) {

                return;

            }

            console.log(message);

        }

    };

}


function handleChildDivClick (): void {

    console.log('Child DIV click.');

}


return  (

    <div onMouseDown={handleSetMouseButtonDownTime} onMouseUp={handleParentDivClick('Parent DIV click.')}>   

        <span>Parent DIV.</span>

        <div className={'disabled-parent-click'} onClick={handleChildDivClick}>Child DIV.</div>

    </div>

);

React Hook useReduxState

 /* eslint-disable react-hooks/rules-of-hooks */


import {useSelector} from 'react-redux';


import get from 'lodash-es/get';


import {RootStateInterface} from './interfaces';


type RootStateKey = keyof RootStateInterface;

type StateKeyWithDots = `.${string}`;

export type RootOrDotsStateKey = RootStateKey | StateKeyWithDots;


interface SelectedStateInterface extends Partial<RootStateInterface> {

    [key: StateKeyWithDots]: any;

}


export function useReduxState (firstStateKey: RootOrDotsStateKey, ...stateKeys: RootOrDotsStateKey[]): SelectedStateInterface {

    stateKeys.push(firstStateKey);

    const selectedState: SelectedStateInterface = {};

    stateKeys.forEach(function (stateKey: RootOrDotsStateKey): void {

        selectedState[stateKey] = useSelector(function (state: RootStateInterface): any {

            if (stateKey.indexOf('.') === 0) {

                return get(state, stateKey.slice(1));

            }

            return state[stateKey];

        });

    });

    return selectedState;

}


    const {

       projectData: {

            value: projectDataValue

        },

        clusterSelect: {

            value: custerValue

        }

        '.buildVersion.ui.requestData': uiVersion

    } = useReduxState(

        'projectsData',

        'clusterSelect',

        'buildVersion'

    );

React Hook useState with callback

 import {

    useState, useRef, useCallback, useEffect

} from 'react';


export function useStateWithCallback <T> (initialState: T | (() => T), alwayExecuteCallback: boolean = false): [T, (newState: T, callback: (state?: T) => void) => void] {


    // Хук работает следующим образом.

    // Сначала он вызывает стандартный хук useState() до первого рендеринга для создания стартового состояния state и функции setState() для его последующего изменения.

    // Для того, чтобы сделать изменение состояния state, будет вызываться возвращаемая функция setStateCallback(), которая записывает в ref переданную функцию callback(), чтобы ее потом можно было вызвать внутри useEffect().

    // Далее вызов переданной функции callback() будет производиться внутри useEffect(), вызываемого после каждого рендеринга компонента, вызванного изменением состояния state.


    // Создание стартового состояния и функции для его последующего изменения.

    const [state, setState] = useState<T>(initialState);


    // Ссылка на функцию callback(), которая будет вызываться внутри useEffect() после изменения состояния state.

    const callbackRef: React.MutableRefObject<((state: T) => void) | undefined> = useRef<((state: T) => void) | undefined>(undefined);


    // Функция изменения состояния и установки ссылки на переданную функцию обратного вызова.

    // С помощью useCallback() выполняется мемоизация функции при первом рендеринге компонента с целью оптимизации для того, чтобы функция каждый раз не создавалась при вызове хука useStateWithCallback().

    const setStateCallback = useCallback(function setStateCallback (newState: T, callback: (state?: T) => void): void {

        callbackRef.current = callback; // Переданную функцию callback нужно передать в ref, чтобы во время выполнения useEffect() достать её оттуда.

        setState(newState); // Вызвать обновление состояния, которое затем приведет к вызову useEffect().

    }, []);


    useEffect(function (): void {

        // При первом рендере ссылка callbackRef.current равна undefined. Она становится равна функции callback() после того, как будет во внешнем коде вызвана функция setStateCallback().

        if (callbackRef.current) {

            callbackRef.current(state); // Вызываем переданную функцию callback().

            callbackRef.current = undefined; // Удаляем ссылку на функцию callback() после того, как она выполнилась, для того, чтобы она не вызывалась повторно при следующем выполнении хука useEffect().

        }

    }, alwayExecuteCallback ? undefined : [state]); // eslint-disable-line react-hooks/exhaustive-deps


    return [state, setStateCallback]; // Возвращаем из хука аналоги стандартных state и setState с возможностью вызова функции callback().


}


// Пример использования хука useStateWithCallback.

// const [value, setValue] = useStateWithCallback(1);

// setValue(2, function callback (state) {console.log(state);}); // В консоль будет выведено: 2

React Hook Click Outside

import React, {useEffect} from 'react';


export function useOutsideClickEventHandler <T extends HTMLElement = HTMLElement> (ref: React.RefObject<T>, handler: (event: Event) => void): void {

    useEffect(function (): () => void {

        function handleClickOutside (event: Event): void {

            if (ref.current && !ref.current.contains(event.target as Node)) {

                handler(event);

            }

        }

        document.addEventListener('mousedown', handleClickOutside);

        return function (): void {

            document.removeEventListener('mousedown', handleClickOutside);

        };

    }, [ref, handler]);

}


const elementRef: React.MutableRefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);

    

useOutsideClickEventHandler(elementRef, function (): void {

    console.log('You clicked outside div.');

});


return <div ref={elementRef}>Try to click outside this div.</div>;

вторник, 27 декабря 2022 г.

Простой WYSIWYG HTML Editor на JavaScript

Файл index.html


<link href='https://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css' rel='stylesheet' type='text/css'>

 <link href='https://fonts.googleapis.com/css?family=Euphoria+Script' rel='stylesheet' type='text/css'>


<body>

  <div class="content">

     <h1 id="head">Simple wysiwyg editor</h1>

        <div id="editparent">

          <div id='editControls' style='text-align:center; padding:5px;'>

            <div class='btn-group'>

              <a class='btn' data-role='undo' href='#'><i class='icon-undo'></i></a>

              <a class='btn' data-role='redo' href='#'><i class='icon-repeat'></i></a>

            </div>

            <div class='btn-group'>

              <a class='btn' data-role='bold' href='#'><b>Bold</b></a>

              <a class='btn' data-role='italic' href='#'><em>Italic</em></a>

              <a class='btn' data-role='underline' href='#'><u><b>U</b></u></a>

              <a class='btn' data-role='strikeThrough' href='#'><strike>abc</strike></a>

            </div>

            <div class='btn-group'>

              <a class='btn' data-role='justifyLeft' href='#'><i class='icon-align-left'></i></a>

              <a class='btn' data-role='justifyCenter' href='#'><i class='icon-align-center'></i></a>

              <a class='btn' data-role='justifyRight' href='#'><i class='icon-align-right'></i></a>

              <a class='btn' data-role='justifyFull' href='#'><i class='icon-align-justify'></i></a>

            </div>

            <div class='btn-group'>

              <a class='btn' data-role='indent' href='#'><i class='icon-indent-right'></i></a>

              <a class='btn' data-role='outdent' href='#'><i class='icon-indent-left'></i></a>

            </div>

            <div class='btn-group'>

              <a class='btn' data-role='insertUnorderedList' href='#'><i class='icon-list-ul'></i></a>

              <a class='btn' data-role='insertOrderedList' href='#'><i class='icon-list-ol'></i></a>

            </div>

            <div class='btn-group'>

              <a class='btn' data-role='h1' href='#'>h<sup>1</sup></a>

              <a class='btn' data-role='h2' href='#'>h<sup>2</sup></a>

              <a class='btn' data-role='p' href='#'>p</a>

            </div>

            <div class='btn-group'>

              <a class='btn' data-role='subscript' href='#'><i class='icon-subscript'></i></a>

              <a class='btn' data-role='superscript' href='#'><i class='icon-superscript'></i></a>

            </div>

          </div>

          <div id='editor' style='' contenteditable>

<p><b>Lorem</b> <i>ipsum</i> <u>dolor</u> <strike>sit</strike> amet, consectetur adipisicing elit. Necessitatibus natus vero voluptatem aliquam molestias dicta aperiam dignissimos laudantium accusamus saepe!</p>

          </div>

    </div>

  </div>


Файл index.css


html, body {

  background: #f5f5f5 !important;

}


.content {

  margin: 50px auto 0;

}


h1#head {

  font-family: 'Euphoria Script';

  text-align: center;

  margin: 0 0 30px;  

  font-size: 100px;

  line-height: 90px;

  text-shadow: 1px 1px 1px #343434;

  font-weight: normal;

}


#editor {

  resize:vertical;

  overflow:auto;

  border:1px solid silver;

  border-radius:5px;

  min-height:100px;

  box-shadow: inset 0 0 10px silver;

  padding:1em;

  background: white;

  margin: 0 auto;

  width: 90%;

}


Файл index.js


$(function() {

  $('#editControls a').click(function(e) {

    switch($(this).data('role')) {

      case 'h1':

      case 'h2':

      case 'p':

        document.execCommand('formatBlock', false, $(this).data('role'));

        break;

      default:

        document.execCommand($(this).data('role'), false, null);

        break;

    }

    

  });

});

понедельник, 19 декабря 2022 г.

Текстовый редактор JavaScript Editor

Contenteditable – текстовый редактор

https://snipp.ru/php/contenteditable

Если добавить атрибут contenteditable к элементу, его содержимое становится доступно для редактирования пользователю, а с использованием JS и JQuery можно сделать простой WYSIWYG редактор.

<div class="editor" contenteditable="true"></div>
HTML

JS-метод document.execCommand(сommand, showDefaultUI, аrgument) применяет команды к элементу с атрибутом contenteditable, который находится в фокусе. Т.к. нет единого стандарта, логика команд может отличатся в разных браузерах так и их поддержка, описание команд на https://developer.mozilla.org.

Проверить доступность команды можно методом queryCommandSupported(сommand).

var enable = document.queryCommandSupported('paste');
if (enable) {
...
}
JS

Поддержка команд в браузере:

backColorheadingjustifyLeft
boldhiliteColorjustifyRight
contentReadOnlyincreaseFontSizeoutdent
copyindentpaste
createLinkinsertBrOnReturnredo
cutinsertHorizontalRuleremoveFormat
decreaseFontSizeinsertHTMLselectAll
deleteinsertImagestrikeThrough
enableInlineTableEditinginsertOrderedListsubscript
enableObjectResizinginsertUnorderedListsuperscript
fontNameinsertParagraphunderline
fontSizeinsertTextundo
foreColoritalicunlink
formatBlockjustifyCenteruseCSS
forwardDeletejustifyFullstyleWithCSS

Жирный

Включает и отключает у выделенного текста жирное начертание тегом <b>, IE использует тег <strong>.

document.execCommand('bold', false, null);
JS

Курсив

Тег <i>, IE использует <em>.

document.execCommand('italic', false, null);
JS

Подчеркнутый

Тег <u>.

document.execCommand('underline', false, null);
JS

Перечёркнутый

Тег <strike>.

document.execCommand('strikethrough', false, null);
JS

Верхний индекс

Тег <sup>.

document.execCommand('superscript', false, null);
JS

Нижний индекс

Тег <sub>.

document.execCommand('subscript', false, null);
JS

Маркированный список

Тег <ul>.

document.execCommand('insertUnorderedList', false, null);
JS

Нумерованный список

Тег <ol>.

document.execCommand('insertOrderedList', false, null);
JS

Заголовки, параграф, цитата и т.д.

formatBlock добавляет блочный тег вокруг выделенного текста.

document.execCommand('formatBlock', false, 'h1');
document.execCommand('formatBlock', false, 'h2');
document.execCommand('formatBlock', false, 'h3');
document.execCommand('formatBlock', false, 'p');
document.execCommand('formatBlock', false, 'blockquote');
document.execCommand('formatBlock', false, 'div');
JS

Горизонтальная линия

Тег <hr>.

document.execCommand('insertHorizontalRule', false, null);
JS

Изображение

var url = prompt('Введите адрес изображения', '');
document.execCommand('insertImage', false, url);
JS

Ссылка

var url = prompt('Введите URL', '');
document.execCommand('CreateLink', false, url);
JS

Удаление ссылки:

document.execCommand('unlink', false, null);
JS

Вставка текста

var text = prompt('Введите текст', '');
document.execCommand('insertText', false, text);
JS

Вставка HTML

var html = prompt('Введите HTML код', '');
document.execCommand('insertHTML', false, html);
JS

С помощью insertHTML можно обернуть выделенный текст тегом, например <pre>:

document.execCommand('insertHTML', false, '<pre>' + document.getSelection().toString() + '</pre>');
JS

Выравнивание текста

Добавляет родительскому блоку CSS-свойство text-align.

По левому краю:

document.execCommand('justifyLeft', false, null);
JS

По центру:

document.execCommand('justifyCenter', false, null);
JS

По правому краю:

document.execCommand('justifyRight', false, null);
JS

По ширине:

document.execCommand('justifyFull', false, null);
JS

Шрифты

По умолчанию команды установки шрифта добавляются тегом <font> c атрибутами facesizecolor, что не очень хорошо.

Команда styleWithCSS переключает режим, в место семантических тегов добавляются <span style="...">.

После установки шрифта нужно переключить styleWithCSS обратно т.к. он распространяется на другие теги (<b><i><s> и т.д.).

Название шрифта

document.execCommand('styleWithCSS', false, true);
document.execCommand('fontName', false, 'monospace');
document.execCommand('styleWithCSS', false, false);
JS

Размер:

Размер шрифта задаётся в условных единицах (1-7), в режиме styleWithCSS некоторые браузеры используют font-size: xx-smallx-smallsmallmediumlargex-largexx-large.

document.execCommand('styleWithCSS', false, true);
document.execCommand('fontSize', false, '5');
document.execCommand('styleWithCSS', false, false);
JS

Цвет:

document.execCommand('styleWithCSS', false, true);
document.execCommand('foreColor', false, '#eeeeee');
document.execCommand('styleWithCSS', false, false);
JS

Фон:

document.execCommand('styleWithCSS', false, true);
document.execCommand('hiliteColor', false, 'orange');
document.execCommand('styleWithCSS', false, false);
JS

Undo и Redo

Отмена последнего действия, Ctrl + z.

document.execCommand('undo', false, null);
JS

Повтор последнего действия, Ctrl + y.

document.execCommand('redo', false, null);
JS

Выделить всё

document.execCommand('selectAll', false, null);
JS

Удаление выделенного

document.execCommand('delete', false, null);
JS

Очистить стили

Удаляет инлайновые теги и атрибуты style, все блочные теги остаются.

document.execCommand('removeFormat', false, null);
JS

Вырезать

document.execCommand('cut', false, null);
JS

Копировать

document.execCommand('copy', false, null);
JS
<link href="/font-awesome.min.css" rel="stylesheet">
<script src="/jquery.min.js"></script>
<div class="toolbar">
<a href="#" class="toolbar-b fas fa-bold" title="Жирный"></a>
<a href="#" class="toolbar-i fas fa-italic" title="Курсив"></a>
<a href="#" class="toolbar-u fas fa-underline" title="Подчёркнутый"></a>
<a href="#" class="toolbar-s fas fa-strikethrough" title="Зачёркнутый"></a>
<a href="#" class="toolbar-sup fas fa-superscript" title="Верхний индекс"></a>
<a href="#" class="toolbar-sub fas fa-subscript" title="Нижний индекс"></a>
<a href="#" class="toolbar-ul fas fa-list-ul" title="Маркированный список"></a>
<a href="#" class="toolbar-ol fas fa-list-ol" title="Нумерованный список"></a>
<a href="#" class="toolbar-p" title="Параграф">p</a>
<a href="#" class="toolbar-h1" title="Заголовок">H1</a>
<a href="#" class="toolbar-hr" title="Горизонтальная линия">hr</a>
<a href="#" class="toolbar-blockquote fas fa-quote-right" title="Цитата"></a>
<a href="#" class="toolbar-img far fa-image" title="Изображение"></a>
<a href="#" class="toolbar-a fas fa-link" title="Ссылка"></a>
<a href="#" class="toolbar-unlink fas fa-unlink" title="Удаление ссылки"></a>
<a href="#" class="toolbar-html" title="Вставить html">HTML</a>
<a href="#" class="toolbar-text" title="Вставить текст">Text</a>
<br>
<a href="#" class="toolbar-left fas fa-align-left" title="по левому краю"></a>
<a href="#" class="toolbar-center fas fa-align-center" title="по центру"></a>
<a href="#" class="toolbar-right fas fa-align-right" title="по правому краю"></a>
<a href="#" class="toolbar-justify fas fa-align-justify" title="по ширине"></a>
<select class="toolbar-font">
<option selected="selected" disabled="disabled">Шрифт</option>
<option value="arial">Arial</option>
<option value="Courier New">Courier New</option>
<option value="georgia">Georgia</option>
<option value="impact">Impact</option>
<option value="roboto">Tahoma</option>
<option value="Times New Roman">Times New Roman</option>
<option value="verdana">Verdana</option>
</select>
<select class="toolbar-size">
<option selected="selected" disabled="disabled">Размер</option>
<option value="1">10px</option>
<option value="2">12px</option>
<option value="3">14px</option>
<option value="4">16px</option>
<option value="5">18px</option>
<option value="6">21px</option>
<option value="7">26px</option>
</select>
<span>Цвет</span> <input class="toolbar-color" type="color" value="#ff0000">
<span>Фон</span> <input class="toolbar-bg" type="color" value="#ffff00">
<br>
<a href="#" class="toolbar-undo fas fa-undo" title="Отмена"></a>
<a href="#" class="toolbar-redo fas fa-redo" title="Повтор"></a>
<a href="#" class="toolbar-delete far fa-trash-alt" title="Удалить"></a>
<a href="#" class="toolbar-selectAll">Выделить всё</a>
<a href="#" class="toolbar-removeFormat">Очистить стили</a>
<a href="#" class="toolbar-cut fas fa-cut" title="Вырезать"></a>
<a href="#" class="toolbar-copy fas fa-copy" title="Копировать"></a>
</div>
<div class="editor" contenteditable="true">...</div>
HTML
CSS
.toolbar a {
display: inline-block;
border: 1px solid #888;
padding: 5px 8px;
margin: 0 5px 10px 0;
color: #000;
border-radius: 3px;
font-size: 12px;
box-shadow: 1px 1px 2px #ddd;
background: #fff;
vertical-align: top;
text-decoration: none;
}
.toolbar select {
display: inline-block;
height: 28px;
line-height: 28px;
background: #fff;
padding: 0;
margin: 0 5px 10px 0;
color: #000;
box-shadow: 1px 1px 2px #ddd;
border-radius: 3px;
vertical-align: top;
  font-size: 12px;
}
.toolbar input {
display: inline-block;
height: 28px;
line-height: 28px;
background: #fff;
padding: 0;
margin: 0 5px 10px 0;
color: #000;
box-shadow: 1px 1px 2px #ddd;
border-radius: 3px;
vertical-align: top;
font-size: 12px;
}
.toolbar span {
display: inline-block;
height: 30px;
line-height: 30px;
padding: 0;
margin: 0 0 10px 0;
color: #000;
vertical-align: top;
font-size: 12px;
}
.editor {
min-height: 150px;
border: 1px solid #ddd;
padding: 10px;
border-radius: 2px;
box-shadow: 1px 1px 2px #ddd;
background: #fff;
}

JavaScript
// Жирный (b)
$('body').on('click', '.toolbar-b', function(){
document.execCommand('bold', false, null);
return false;
});
 
// Курсив (i)
$('body').on('click', '.toolbar-i', function(){
document.execCommand('italic', false, null);
return false;
});
 
// Подчёркнутый текст (u)
$('body').on('click', '.toolbar-u', function(){
document.execCommand('underline', false, null);
return false;
});
 
// Зачёркнутый текст (strike)
$('body').on('click', '.toolbar-s', function(){
document.execCommand('strikethrough', false, null);
return false;
});
 
// Верхний индекс (sup)
$('body').on('click', '.toolbar-sup', function(){
document.execCommand('superscript', false, null);
return false;
});
 
// Нижний индекс (sub)
$('body').on('click', '.toolbar-sub', function(){
document.execCommand('subscript', false, null);
return false;
});
 
// Маркированный список (ul)
$('body').on('click', '.toolbar-ul', function(){
document.execCommand('insertUnorderedList', false, null);
return false;
});
 
// Нумерованный список (ol)
$('body').on('click', '.toolbar-ol', function(){
document.execCommand('insertOrderedList', false, null);
return false;
});
 
// Параграф (p)
$('body').on('click', '.toolbar-p', function(){
document.execCommand('formatBlock', false, 'p');
return false;
});
 
// Заголовок (h1)
$('body').on('click', '.toolbar-h1', function(){
document.execCommand('formatBlock', false, 'h1');
return false;
});
 
// Горизонтальная линия (hr) 
$('body').on('click', '.toolbar-hr', function(){
document.execCommand('insertHorizontalRule', false, null);
return false;
});
 
// Цитата (blockquote)
$('body').on('click', '.toolbar-blockquote', function(){
document.execCommand('formatBlock', false, 'blockquote');
return false;
});
 
// Изображение (img)
$('body').on('click', '.toolbar-img', function(){
var url = prompt('Введите адрес изображения', 'https://snipp.ru/demo/526/image.jpg');
document.execCommand('insertImage', false, url);
return false;
});
 
// Ссылка (a)
$('body').on('click', '.toolbar-a', function(){
var url = prompt('Введите URL', '');
document.execCommand('CreateLink', false, url);
return false;
});
 
// Удаление ссылки
$('body').on('click', '.toolbar-unlink', function(){
document.execCommand('unlink', false, null);
return false;
});
 
// Вставить html
$('body').on('click', '.toolbar-html', function(){
var html = prompt('Введите HTML код', '');
document.execCommand('insertHTML', false, html);
return false;
});
 
// Вставить текст
$('body').on('click', '.toolbar-text', function(){
var text = prompt('Введите текст', '');
document.execCommand('insertText', false, text);
return false;
});
 
// Выравнивание текста по левому краю
$('body').on('click', '.toolbar-left', function(){
document.execCommand('justifyLeft', false, null);
return false;
});
 
// Выравнивание текста по центру
$('body').on('click', '.toolbar-center', function(){
document.execCommand('justifyCenter', false, null);
return false;
});
 
// Выравнивание текста по правому краю
$('body').on('click', '.toolbar-right', function(){
document.execCommand('justifyRight', false, null);
return false;
});
 
// Выравнивание по ширине
$('body').on('click', '.toolbar-justify', function(){
document.execCommand('justifyFull', false, null);
return false;
});
 
// Шрифт
$('body').on('input', '.toolbar-font', function(){
var val = $(this).val();
document.execCommand('styleWithCSS', false, true);
document.execCommand('fontName', false, val);
document.execCommand('styleWithCSS', false, false);
});
 
// Размер шрифта
$('body').on('input', '.toolbar-size', function(){
var val = $(this).val();
document.execCommand('styleWithCSS', false, true);
document.execCommand('fontSize', false, val);
document.execCommand('styleWithCSS', false, false);
});
 
// Цвет шрифта
$('body').on('input', '.toolbar-color', function(){
var val = $(this).val();
document.execCommand('styleWithCSS', false, true);
document.execCommand('foreColor', false, val);
document.execCommand('styleWithCSS', false, false);
});
 
// Цвет фона
$('body').on('input', '.toolbar-bg', function(){
var val = $(this).val();
document.execCommand('styleWithCSS', false, true);
document.execCommand('hiliteColor', false, val);
document.execCommand('styleWithCSS', false, false);
});
 
// Отмена
$('body').on('click', '.toolbar-undo', function(){
document.execCommand('undo', false, null);
return false;
});
 
// Повтор
$('body').on('click', '.toolbar-redo', function(){
document.execCommand('redo', false, null);
return false;
});
 
// Удалить
$('body').on('click', '.toolbar-delete', function(){
document.execCommand('delete', false, null);
return false;
});
 
// Выделить всё
$('body').on('click', '.toolbar-selectAll', function(){
document.execCommand('selectAll', false, null);
return false;
});
 
// Очистить стили
$('body').on('click', '.toolbar-removeFormat', function(){
document.execCommand('removeFormat', false, null);
return false;
});
 
// Вырезать
$('body').on('click', '.toolbar-cut', function(){
document.execCommand('cut', false, null);
return false;
});
 
// Копировать
$('body').on('click', '.toolbar-copy', function(){
document.execCommand('copy', false, null);
return false;
});





В большинстве браузерах при нажатии на Enter, новая строка начнется с преведущего блочного элемента (p, li, blockquote).

Если редактор был пуст, то вставится <div>, в место него можно использовать <p> вызвав команду:

document.execCommand('defaultParagraphSeparator', false, 'p');
JS

Если редактору сделать display: inline-block, то будут вставляться только <br>.

В contenteditable клавиша Tab не добавляет отступ, а переключает фокус на следующий элемент. Включить табуляцию можно, добавив CSS-свойство white-space: pre-wrap чтобы начали выводится пробельные символы, но при этом переносы строк тоже заработают.

.editor {
white-space: pre-wrap;
tab-size: 3; /* Ширина табуляции */
}
CSS

И обработчик нажатия клавиши на JQuery:

$('body').on('keydown', '.editor', function(e){
if (e.keyCode === 9) {
e.preventDefault();
var editor = this;
var doc = editor.ownerDocument.defaultView;
var sel = doc.getSelection();
var range = sel.getRangeAt(0);
var tabNode = document.createTextNode("\t");
range.insertNode(tabNode);
range.setStartAfter(tabNode);
range.setEndAfter(tabNode);
sel.removeAllRanges();
sel.addRange(range);
}
});
JS

Результат:

for (let i = 0; i < 10; i++) { if (i % 2) { alert( i ); } }


При вставки текста из буфера переносятся все стили и теги скопированные из HTML страницы или Word файла, это можно предотвратить очисткой:

function escape_text(text) {
var map = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'};
return text.replace(/[&<>"']/g, function(m) {
return map[m];
});
}
$('body').on('paste', '.editor', function(e){
e.preventDefault();
var text = (e.originalEvent || e).clipboardData.getData('text/plain');
document.execCommand('insertHtml', false, escape_text(text));
});
JS
6
<div class="editor" contenteditable="true" placeholder="Введите текст"></div>
HTML
.editor[placeholder]:empty:before {
content: attr(placeholder);
color: #555;
}
.editor[placeholder]:empty:focus:before {
content: '';
}
CSS
$('body').on('focusout', '.editor', function() {
var element = $(this);
if (!element.text().replace(' ', '').length) {
element.empty();
}
});
JS

Показать исходный код можно заменив <div> на <textarea>.

<input type="checkbox" id="toggle-editor"> Показать исходный код
<div contenteditable="true" class="editor">
...
</div>
HTML
textarea.editor {
display: block;
width: 100%;
box-sizing: border-box;
white-space: pre-wrap;
}
CSS
$('#toggle-editor').click(function(){
var target = $('.editor');
if ($(this).is(':checked')){
target.replaceWith('<textarea class="editor">' + target.html() + '</textarea>');
} else {
target.replaceWith('<div contenteditable="true" class="editor">' + target.val() + '</div>');
}
});
JS

Для отправки текста вместе с формой нужно добавить скрытый <textarea>, при событии отправки формы скидывать в него содержимое редактора.

<form id="form" method="post" action="">
<div class="editor" contenteditable="true">...</div>
<textarea id="textarea" name="text" style="dispalay: none;"></textarea>
<input type="supmit" value="Отправить">
</form>
HTML

$('#form').on('submit', function(){
$('#textarea').val($('.editor').html());
return true;
});


Auto-Saving

To make the editor more user-friendly, we should add auto-saving. The first method automatically saves your work every five seconds.

1
setInterval(function() {
2
  for (var i = 0; i < editables.length; i++) {
3
    localStorage.setItem(editables[i].getAttribute('id'), editables[i].innerHTML);
4
  }
5
}, 5000);

You can also save the changes on every keydown event.

1
document.addEventListener('keydown', function(e) {
2
  for (var i = 0; i < editables.length; i++) {
3
    localStorage.setItem(editables[i].getAttribute('id'), editables[i].innerHTML);
4
  }
5
});

In this tutorial, I'm sticking with the latter method. You are free to trigger auto-save based on any event that seems appropriate in your projects.


Retrieving Saved Content

If you make changes to any of the elements in the previous demo and reload the page, you will notice that the changes you made are gone. This is because there is no code in place to retrieve the saved data. Once the content has been saved in localStorage, we need to retrieve it later when a user visits the webpage again.

1
if (typeof(Storage) !== "undefined") {
2
3
  if (localStorage.getItem('title') !== null) {
4
    editables[0].innerHTML = localStorage.getItem('title');
5
  }
6
  
7
  if (localStorage.getItem('author') !== null) {
8
    editables[1].innerHTML = localStorage.getItem('author');
9
  }
10
  
11
  if (localStorage.getItem('content') !== null) {
12
    editables[2].innerHTML = localStorage.getItem('content');
13
  } 
14
}

The code above checks if the title, author, or content already exist in localStorage. If they do, we set the innerHTML of the respective elements to the retrieved values.


Creating the Editor

To create the inline editor, you need to have the ability to change the value of the contentEditable attribute whenever a user decides to edit something.

While toggling the contentEditable attribute, it is necessary to know what value the attribute holds currently. To accomplish that, you can use the isContentEditable property. If isContentEditable returns true for an element, then the element is currently editable—otherwise it is not. We will use this property shortly to determine the state of various elements in our document.

First, we need to create a directory called contenteditable-editor. Inside that, create a new file called index.html. You can use this as a skeleton for your HTML file.

1
<!DOCTYPE html>
2
<html lang="en">
3
  <head>
4
    <meta charset="UTF-8" />
5
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
    <title>Text Editor</title>
8
  </head>
9
  <body></body>
10
</html>

The first step in building the editor is the creation of a button to toggle editing and some editable elements. Inside the <body> element, put this:

1
<button id="editBtn" type="button">Edit Document</button>
2
3
<div id="editor">
4
    <h1 id="title">A Nice Heading.</h1>
5
    <p>Last Edited By - <span id="author">Monty Shokeen</span></p>
6
    <p id="content">Some content that needs correction.</p>
7
</div>

Each element that we intend to keep editable needs to have its own unique Id. This will be helpful when we have to save the changes or retrieve them later to replace the text inside each element.

The following JavaScript code handles all the editing and saving.

1
const editBtn = document.getElementById('editBtn');
2
const editables = document.querySelectorAll('#title, #author, #content')
3
4
editBtn.addEventListener('click', function(e) {
5
  if (!editables[0].isContentEditable) {
6
    editables[0].contentEditable = 'true';
7
    editables[1].contentEditable = 'true';
8
    editables[2].contentEditable = 'true';
9
    editBtn.innerHTML = 'Save Changes';
10
    editBtn.style.backgroundColor = '#6F9';
11
  } else {
12
    // Disable Editing 
13
    editables[0].contentEditable = 'false';
14
    editables[1].contentEditable = 'false';
15
    editables[2].contentEditable = 'false';
16
    // Change Button Text and Color 
17
    editBtn.innerHTML = 'Enable Editing';
18
    editBtn.style.backgroundColor = '#F96';
19
    // Save the data in localStorage 
20
    for (var i = 0; i < editables.length; i++) {
21
      localStorage.setItem(editables[i].getAttribute('id'), editables[i].innerHTML);
22
    }
23
  }
24
});

You can put this code in a <script> tag at the bottom of the <body> tag. We use querySelectorAll() to store all the editable elements in a variable. This method returns a NodeList which contains all the elements in our document that are matched by specified selectors. This way, it's easier to keep track of editable elements with one variable. For instance, the title of our document can be accessed by using editables[0], which is what we will do next.

Next, we add an event listener to our button's click event. Every time a user clicks on the Edit Document button, we check if the title is editable. If it is not editable, we set the contentEditable property on each of the editable elements to true. Moreover, the text 'Edit Document' changes to 'Save Changes'. After users have made some edits, they can click on the 'Save Changes' button and the changes made can be saved permanently.

If the title is editable, we set the contentEditable property on each of the editable elements to false. At this point, we can also save the content of our document on the server to retrieve later or synchronize the changes to a copy that exists somewhere else. In this tutorial, I am going to save everything in localStorage instead. When saving the value in localStorage, I am using the Id of each element to make sure that I don't overwrite anything.