Skip to content

Latest commit

 

History

History
587 lines (517 loc) · 17.2 KB

example.md

File metadata and controls

587 lines (517 loc) · 17.2 KB

Example usage in the framework

This is just a simple example and is not a complete answer. If there are any examples or errors in other frameworks, please participate and correct them.

React Component

React class

1. Editor.tsx

import React, { Component, createRef } from "react";
import suneditor from "suneditor";
import { en } from "suneditor/src/lang";
import plugins from "suneditor/src/plugins";
import CodeMirror from "codemirror";
import katex from "katex";
import "suneditor/dist/css/suneditor.min.css";
import "codemirror/mode/htmlmixed/htmlmixed";
import "codemirror/lib/codemirror.css";
import "katex/dist/katex.min.css";
import "./Editor.scss"


interface Props {
    contents?: string;
    onBlur?: Function;
    onSave: Function;
}

interface State {
    imageList: any[];
    selectedImages: any[];
    imageSize: string;
}

class Editor extends Component<Props, State> {
    txtArea: any;
    editor: any;
        
    constructor(props: any) {
        super(props);
        this.txtArea = createRef();
        this.state = {
            imageList: [],
            selectedImages: [],
            imageSize: "0KB",
          };
    }

    componentDidMount() {
        const editor: any = this.editor = suneditor.create(this.txtArea.current, {
            plugins: plugins,
            lang: en,
            callBackSave: (contents: string) => this.props.onSave(contents),
            codeMirror: CodeMirror,
            stickyToolbar: 0,
            katex: katex,
            width: '100%',
            height: 'auto',
            minHeight: '400px',
            value: this.props.contents,
            // imageUploadUrl: `url`,
            imageMultipleFile: true,
            previewTemplate: `
                <div style="width:auto; max-width:1136px; min-height:400px; margin:auto;">
                {{contents}}
                </div>
            `,
            buttonList: [
                // default
                ['undo', 'redo'],
                ['font', 'fontSize', 'formatBlock'],
                ['paragraphStyle', 'blockquote'],
                ['bold', 'underline', 'italic', 'strike', 'subscript', 'superscript'],
                ['fontColor', 'hiliteColor', 'textStyle'],
                ['removeFormat'],
                ['outdent', 'indent'],
                ['align', 'horizontalRule', 'list', 'lineHeight'],
                ['table', 'link', 'image', 'video'],
                ['fullScreen', 'showBlocks', 'codeView'],
                ['preview'],
                ['save'],
                // responsive
                ['%1161', [
                    ['undo', 'redo'],
                    [':p-Formats-default.more_paragraph', 'font', 'fontSize', 'formatBlock', 'paragraphStyle', 'blockquote'],
                    ['bold', 'underline', 'italic', 'strike', 'subscript', 'superscript'],
                    ['fontColor', 'hiliteColor', 'textStyle'],
                    ['removeFormat'],
                    ['outdent', 'indent'],
                    ['align', 'horizontalRule', 'list', 'lineHeight'],
                    ['-right', 'save'],
                    ['-right', ':i-Etc-default.more_vertical', 'fullScreen', 'showBlocks', 'codeView', 'preview'],
                    ['-right', ':r-Table&Media-default.more_plus', 'table', 'link', 'image', 'video'],
                ]],
                ['%893', [
                    ['undo', 'redo'],
                    [':p-Formats-default.more_paragraph', 'font', 'fontSize', 'formatBlock', 'paragraphStyle', 'blockquote'],
                    ['bold', 'underline', 'italic', 'strike'],
                    [':t-Fonts-default.more_text', 'subscript', 'superscript', 'fontColor', 'hiliteColor', 'textStyle'],
                    ['removeFormat'],
                    ['outdent', 'indent'],
                    ['align', 'horizontalRule', 'list', 'lineHeight'],
                    ['-right', 'save'],
                    ['-right', ':i-Etc-default.more_vertical', 'fullScreen', 'showBlocks', 'codeView', 'preview'],
                    ['-right', ':r-Table&Media-default.more_plus', 'table', 'link', 'image', 'video'],
                ]],
                ['%855', [
                    ['undo', 'redo'],
                    [':p-Formats-default.more_paragraph', 'font', 'fontSize', 'formatBlock', 'paragraphStyle', 'blockquote'],
                    [':t-Fonts-default.more_text', 'bold', 'underline', 'italic', 'strike', 'subscript', 'superscript', 'fontColor', 'hiliteColor', 'textStyle'],
                    ['removeFormat'],
                    ['outdent', 'indent'],
                    ['align', 'horizontalRule', 'list', 'lineHeight'],
                    [':r-Table&Media-default.more_plus', 'table', 'link', 'image', 'video'],
                    ['-right', 'save'],
                    ['-right', ':i-Etc-default.more_vertical', 'fullScreen', 'showBlocks', 'codeView', 'preview'],
                ]],
                ['%563', [
                    ['undo', 'redo'],
                    [':p-Formats-default.more_paragraph', 'font', 'fontSize', 'formatBlock', 'paragraphStyle', 'blockquote'],
                    [':t-Fonts-default.more_text', 'bold', 'underline', 'italic', 'strike', 'subscript', 'superscript', 'fontColor', 'hiliteColor', 'textStyle'],
                    ['removeFormat'],
                    ['outdent', 'indent'],
                    [':e-List&Line-default.more_horizontal', 'align', 'horizontalRule', 'list', 'lineHeight'],
                    [':r-Table&Media-default.more_plus', 'table', 'link', 'image', 'video'],
                    ['-right', 'save'],
                    ['-right', ':i-Etc-default.more_vertical', 'fullScreen', 'showBlocks', 'codeView', 'preview'],
                ]],
                ['%458', [
                    ['undo', 'redo'],
                    [':p-Formats-default.more_paragraph', 'font', 'fontSize', 'formatBlock', 'paragraphStyle', 'blockquote'],
                    [':t-Fonts-default.more_text', 'bold', 'underline', 'italic', 'strike', 'subscript', 'superscript', 'fontColor', 'hiliteColor', 'textStyle', 'removeFormat'],
                    [':e-List&Line-default.more_horizontal', 'outdent', 'indent', 'align', 'horizontalRule', 'list', 'lineHeight'],
                    [':r-Table&Media-default.more_plus', 'table', 'link', 'image', 'video'],
                    ['-right', 'save'],
                    ['-right', ':i-Etc-default.more_vertical', 'fullScreen', 'showBlocks', 'codeView', 'preview'],
                ]]
            ]
        });

        editor.onBlur = () => {
            if (typeof this.props.onBlur === 'function') this.props.onBlur()
        }

        editor.onImageUpload = this.imageUpload.bind(this);
        // editor.onVideoUpload = videoUpload;
    }

    componentDidUpdate(prevProps: any) {
        if (this.props.contents !== prevProps.contents) {
            this.editor.setContents(this.props.contents);
            this.editor.core.history.reset(true);
        }
    }

    componentWillUnmount() {
        if (this.editor) this.editor.destroy();
    }

    // image, video
    findIndex(arr: any[], index: number) {
        let idx = -1;
    
        arr.some(function (a, i) {
            if ((typeof a === 'number' ? a : a.index) === index) {
                idx = i;
                return true;
            }
            return false;
        })
    
        return idx;
    }
    
    imageUpload(targetElement: Element, index: number, state: string, imageInfo: Record<string, string>, remainingFilesCount: number) {
        if (state === 'delete') {
            this.state.imageList.splice(this.findIndex(this.state.imageList, index), 1)
            this.setState({
                imageList: this.state.imageList
            })
        } else {
            if (state === 'create') {
                const imageList = this.state.imageList;
                imageList.push(imageInfo)
                this.setState({
                    imageList: imageList
                })
            } else { // update
                //
            }
        }

        if (remainingFilesCount === 0) {
            this.setImageList()
        }
    }

    setImageList() {
        const imageList = this.state.imageList;    
        let size = 0;

        for (let i = 0; i < imageList.length; i++) {
            size += Number((imageList[i].size / 1000).toFixed(1));
        }

        this.setState({
            imageSize: size.toFixed(1) + 'KB'
        })
    }

    selectImage(evt: any, type: string, index: number) {
        evt.preventDefault();
        evt.stopPropagation();
        this.state.imageList[this.findIndex(this.state.imageList, index)][type]();
    }

    checkImage(index: number) {
        const selectedImages = this.state.selectedImages;
        const currentImageIdx = this.findIndex(selectedImages, index)

        if (currentImageIdx > -1) {
            selectedImages.splice(currentImageIdx, 1)
        } else {
            selectedImages.push(index)
        }

        this.setState({
            selectedImages: selectedImages
        })
    }

    deleteCheckedImages() {
        const iamgesInfo = this.editor.getImagesInfo();

        for (let i = 0; i < iamgesInfo.length; i++) {
            if (this.state.selectedImages.indexOf(iamgesInfo[i].index as number) > -1) {
                iamgesInfo[i].delete();
                i--;
            }
        }

        this.setState({
            selectedImages: [],
        })
    }

    fileUploadToEditor(e: any) {
        if (e.target.files) {
            this.editor.insertImage(e.target.files)
            e.target.value = ''
        }
    }

    render() {
        return <div>
            <textarea ref={this.txtArea} />
            <div className="component-list">
                <div className="file-list-info">
                    <span>Attach files</span>
                    <span className="xefu-btn">
                        <span className="files-text">Images</span>
                    </span>
                    <input type="file" id="files_upload" accept=".jpg, .jpeg, .png, .ico, .tif, .tiff, .gif, .bmp, .raw" multiple className="files-text files-input" onChange={(e: any) => this.fileUploadToEditor(e)} />
                    <span id="image_size" className="total-size text-small-2">{this.state.imageSize}</span>
                    <button className="btn btn-md btn-danger" id="image_remove" disabled={this.state.selectedImages.length === 0} onClick={() => this.deleteCheckedImages()}>삭제</button>
                </div>
                <div className="file-list">
                    <ul id="image_list">
                        {
                            this.state.imageList.map((v, i) => {
                                return <li key={i} onClick={() => this.checkImage(v.index)} className={this.state.selectedImages.includes(v.index) ? "checked" : ""}>
                                    <div>
                                        <div className="image-wrapper"><img src={v.src} /></div>
                                    </div>
                                    <a onClick={(evt: any) => this.selectImage(evt, "select", v.index)} className="image-size">{(v.size / 1000).toFixed(1)}KB</a>
                                    <div className="image-check"><svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-fa-i2svg=""><path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg></div>
                                </li>
                            })
                        }
                    </ul>
                </div>
            </div>
        </div>;
    }
}

export default Editor;

2 PostEdit.tsx

import React, { Component, createRef } from "react";
import Editor from "./Editor";

interface Props {}

interface State {
	content: string;
}

class PostEdit extends Component<Props, State> {
	editorRef: any = createRef();

	constructor(props: Props) {
		super(props);
		this.state = {
			content: "Test",
		};
	}

	save() {
		const contents = this.editorRef.current.editor.getContents();
		console.log("save", contents);
	}

	render() {
		return (
			<div>
				<Editor ref={this.editorRef} contents={this.state.content} onSave={this.save.bind(this)}></Editor>

				<button onClick={() => this.save()}>
					<span>Save</span>
				</button>
			</div>
		);
	}
}

export default PostEdit;

3. Editor file component Scss

.sun-editor .se-dialog .se-dialog-inner .se-dialog-content {
	margin: 100px auto !important;
}
.sun-editor .se-wrapper .se-wrapper-wysiwyg {
	padding-bottom: 200px !important;
}

.sun-editor-editable .se-component.__se__float-left {
	margin: 0 0 10px 0;
}
.sun-editor-editable .se-component.__se__float-right {
	margin: 0 0 10px 0;
}

.sun-editor-editable a {
	color: #004cff !important;
	text-decoration: none !important;
}

// image list
/** image list */
.component-list {
	display: flex;
	box-sizing: border-box;
	position: relative;
	width: 100%;
	margin: 10px 0 10px 0;
	padding: 4px;
	background: #fff;
}

.xefu-btn {
	display: inline-block;
	*display: inline;
	margin: 0;
	padding: 0 12px !important;
	height: 24px !important;
	overflow: visible;
	border: 1px solid #bbb;
	border-radius: 2px;
	text-decoration: none !important;
	text-align: center;
	vertical-align: top;
	line-height: 24px !important;
	font-family: inherit;
	font-size: 12px;
	color: #333;
	*zoom: 1;
	cursor: pointer;
	box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
	background-color: #f5f5f5;
	*background-color: #e6e6e6;
	background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
	background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
	background-image: -webkit-gradient(top, #ffffff, #e6e6e6);
	background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
	background-image: linear-gradient(top, #ffffff, #e6e6e6);
	background-repeat: repeat-x;
	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff',endColorstr='#e6e6e6',GradientType=0);
	filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
}

.component-list .files-text {
	display: inline-block;
	margin: 0 -12px !important;
	padding: 0 12px !important;
	overflow: visible;
	width: auto;
	height: 24px;
	border: 0;
	vertical-align: top;
	text-decoration: none !important;
	line-height: 24px;
	font-family: inherit;
	font-size: 12px;
	color: #333;
	cursor: pointer;
	background: 0 0;
}

.component-list .files-input {
	position: absolute;
	width: 86px;
	left: 26px;
	top: 27px;
	opacity: 0;
	-ms-filter: "alpha(opacity=0)";
	font-size: 8px !important;
	direction: ltr;
	cursor: pointer;
}

.component-list * {
	box-sizing: border-box;
}

.component-list button {
	margin: 0 !important;
}

.component-list .file-list-info {
	float: left;
	white-space: nowrap;
	padding: 10px;
	background: #f5f5f5;
	border: 1px solid #ccc;
}

.component-list .file-list-info span {
	display: block;
	width: 100%;
	margin: 12px 0;
}

.component-list .file-list-info .total-size {
	color: #333;
}

.component-list .file-list {
	padding: 0 0 0 0;
	margin: 0 0 0 10px;
	border: none;
}

.component-list .file-list ul {
	margin: 0;
	padding: 0;
	height: auto;
	width: 100%;
	background-color: #f5f5f5;
	border: 1px solid #ccc;
}

.component-list .file-list ul li {
	position: relative;
	display: inline-block;
	margin: 3px;
	width: auto;
	height: auto;
	border: 3px solid #fff;
}

.component-list .file-list ul li .image-wrapper {
	width: 54px;
	height: auto;
}

.component-list .file-list ul li .file-wrapper {
	cursor: default;
	width: 70px;
	height: 21px;
	font-size: 11px;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: pre;
	word-break: break-all;
	padding-top: 4px;
}
.component-list .file-list ul li .file-wrapper svg {
    vertical-align: sub;
}

.component-list .file-list ul li.checked {
	border: 3px solid #dc3545;
}

.component-list .file-list ul li img {
	width: 100%;
	height: auto;
}

.component-list .file-list ul li .image-size {
	color: #666;
	font-size: 10px;
}

.component-list .file-list ul li .image-check {
	position: absolute;
	height: 12px;
	width: 12px;
	top: 0;
	left: auto;
	right: 0;
	margin: 0;
	padding: 1px 0 1px 2px;
	border: 0;
	border-radius: 0 0 0 5px;
	outline: none;
	background-color: #dc3545;
}

.component-list .file-list ul li.checked .image-check {
	display: block;
}

.component-list .file-list ul li:not(.checked) .image-check {
	display: none;
}

.component-list .file-list ul li .image-check svg {
	display: inline-block;
	font-size: 10px;
	height: 1em;
	width: 1em;
	overflow: visible;
	vertical-align: 0.875em;
	margin: 0;
	padding: 0;
	color: #fff;
}

/** video */
.component-list .component-file-list {
	width: 100%;
	padding: 0 0 0 0;
	margin: 0 0 0 10px;
	border: none;
}
.component-list .component-file-list ul {
	margin: 0;
	padding: 0;
	height: auto;
	width: 100%;
	background-color: #f5f5f5;
	border: 1px solid #ccc;
}
.component-list .component-file-list ul li {
	position: relative;
	display: inline-block;
	width: 100%;
	height: 24px;
	margin: 0;
	border: 0;
	overflow: hidden;
}
.component-list .component-file-list ul li button {
	width: 30px;
	height: 24px;
	padding: 0;
}
.component-list .component-file-list ul li a {
	color: #333;
}

Vue Component