iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 19
0
Modern Web

從coding到上線-打造自己的blog系統系列 第 19

Day19 線上編輯器

editor

現在用在前端的編輯器還挺多的,我這裡使用的是TOAST UI Editor,支援一般所見即所得和markdown

官方給的文檔寫的挺清楚的,先在view/component創建editor.html寫入

{{ define "editor"}}
    <div id="editorContainer">
      <form id="NewBlogForm" style="display: block;">
        <label for="NewBlogName">Blog Name</label>
        <input id="NewBlogName" type="text" placeholder="限制100字(必填)">
        <p class="invisible">超過100字</p>

        <label for="NewBlogDescription">簡述</label>
        <input id="NewBlogDescription" type="text" placeholder="能吸引人點進來看全文的簡短前言 (限制255字)">
        <p class="invisible">超過255字</p>

        <div class="input-group mb-3">
          <div class="input-group-prepend">
            <label class="input-group-text" for="inputGroupSelect01">類型</label>
          </div>
          <select id="TypeSelect">
            <option value="1">Project</option>
            <option value="2">Article</option>
          </select>
        </div>

        <label for="editor">內文</label>
        <div id="editor"></div>
        <p class="invisible">內文不得為空</p>

        <button id="EditorSubmitBtn" type="submit">Submit</button>
      </form>
      <!-- Local JS-->
      <script src="http://asset.dcreater.com/static/js/editor.js"></script>
    </div>
{{ end }}

在js創建editor.js寫入

let editor;
// option of blog type
let blogType;
// initial value of blog content
let ititialContent = "";

// check hashtag
HandleNewPost();

function HandleNewPost() {
	ShowForm()
	editor = CreateEditor();
}

// create a new editor
function CreateEditor() {
    const {Editor} = toastui;
    const {codeSyntaxHighlight, colorSyntax, tableMergedCell} = Editor.plugin;

    return Editor.factory({
        el: document.querySelector('#editor'),
        usageStatistics: false,
        height: '500px',
        initialEditType: 'wysiwyg',
        previewStyle: 'tab',
        initialValue: ititialContent,
        plugins: [codeSyntaxHighlight, colorSyntax, tableMergedCell],
    })
}

// show blog or project form
function ShowForm() {
    $("#editorContainer").show();
}

// prevent color picker reload page
$("#NewBlogForm").submit(function (e) {
    e.preventDefault();
});


// choose type event
$('#TypeSelect').on('click', function () {
    blogType = $(this).val();
});

解釋:

  • plugin分別是程式碼highlight,文字顏色變更,還有表格
  • colorSyntax有個bug,傳送表單會直接重新載入網頁,所以把submit默認功能取消,我們改寫在button click事件。

記得用cdn引入,載入網頁就能看到editor了,由於默認的圖片上傳toast ui會將他轉回base64直接存在文章,使的文章變得太大,所以我們要利用他提供的hook處理圖片上傳,稍微修改代碼

// create a new editor
function CreateEditor() {
    const {Editor} = toastui;
    const {codeSyntaxHighlight, colorSyntax, tableMergedCell} = Editor.plugin;

    return Editor.factory({
        el: document.querySelector('#editor'),
        usageStatistics: false,
        height: '500px',
        initialEditType: 'wysiwyg',
        previewStyle: 'tab',
        initialValue: initialContent,
        plugins: [codeSyntaxHighlight, colorSyntax, tableMergedCell],
        hooks: {
            'addImageBlobHook': imageUpload
        }
    })
}

// handle for editor upload image
function imageUpload(blob, callback) {
    let data = new FormData();
    let path = window.location.pathname.split("/").slice(1);
    let oid = path.length > 1 ? meta.boid : meta.oid;
    let name = geTimeNowStr();
    let url = "http://asset.dcreater.com/img/" + oid + "/" + name;
    data.append('content', blob);
    data.append('fileName', name);
    data.append('oid', oid);

    $.ajax({
        url: url,
        data: data,
        cache: false,
        contentType: false,
        processData: false,
        method: 'POST',
        mimeType: 'multipart/form-data',
        xhrFields: {
            withCredentials: true
        },
        success: function (data) {
            // run callback
            callback(url);
        },
        error: function (jqXHR, textStatus, errorThrown) {
            alert("sorry, something error in upload image. Please try again later");
        }
    });

    function geTimeNowStr() {
        let date = new Date(Date.now());
        return date.toISOString();
    }
}

解釋:

  • hooks在上傳圖片時會呼叫imageUpload這個function
  • 將上傳的時間作為圖片名
  • 呼叫callback(url)會打印上圖片,src為"url"

這個editor有可能是寫新的文章,也有可能是修改文章,如果是修改,我們需要把原本blog的資料還原上來以供修改,我們這邊不註冊新的網址路徑,直接單頁定義"#NewBlog", "#UpdateBlog"這兩個錨點來區別

// create a new blog or update exist blog
$("#EditorSubmitBtn").on("click", function (e) {
    let blogType = {"1": "project", "2": "article"};

    let name = $("#NewBlogName").val();
    let newName = (name === meta.bname ? "" : name);
    let description = $("#NewBlogDescription").val();
    let type = blogType[$("#TypeSelect").val()];
    let content = new Blob([editor.getMarkdown()], {type: 'text/plain'});
    let url = window.location.pathname + (window.location.hash === "#UpdateBlog" ? "" : "/" + name);
    let path = window.location.pathname.split("/").slice(1);
    let oid = path.length > 1 ? meta.boid : meta.oid;
    let method = 'POST';
    let newSuperPath =  GetSuperURL(path);
    let superid = (window.location.hash === "#UpdateBlog" ? meta.bsuper : (path.length > 1 ? meta.bid : 0));

    let data = new FormData();
    data.append('descript', description);
    data.append('blogType', type);
    data.append("superid", superid);
    data.append("oid", oid);
    if (type === "article") {
        data.append("content", content);
    }
    if (window.location.hash === "#UpdateBlog") {
        // update
        data.append("bid", meta.bid);
        data.append("newsuperid", -1);
        data.append("newsuperUrl", newSuperPath);
        data.append("newname", newName);
        method = 'put';
    }

    $.ajax({
        url: url,
        data: data,
        cache: false,
        contentType: false,
        processData: false,
        method:  method,
        mimeType: 'multipart/form-data',
        withCredentials: true,
        success: function (data) {
            // redirect
            window.location.href = window.location.hash === "#UpdateBlog" ? newSuperPath + "/" + (newName === "" ? meta.bname : newName) : url;
        },
        error: function (jqXHR, textStatus, errorThrown) {
            alert("sorry, something error in create blog. Please try again later");
        }
    });

    e.preventDefault();
});

// show blog or project form
function ShowForm() {
    $("#editorContainer").show();
    FillEditor();
}

// fill form of update blog
function FillEditor() {
    let content = $('#content');
    if (content.length > 0) {
        $("#NewBlogName").val(meta.bname);
        $("#NewBlogDescription").val(meta.bdescription);
        $("#TypeSelect").val(meta.btype).prop('disabled', true);
        initialContent = $("#metaContent").html();
    }
}

最後來處裡editor的出現與隱藏,只有updata與發新聞張需要顯示,平常顯示正常blog內容就好

function HandleNewPost() {
    // editflag that create editor
    let editFlag = ["#NewBlog", "#UpdateBlog"];
    if (editFlag.includes(window.location.hash)) {
        HandleShowForm();
        editor = ShowEditor();
    } else {
        HandleHideForm();
        HideEditor();
    }

}

//  hashtag event
$(window).on('hashchange', function () {
    HandleNewPost();
});

function HandleShowForm() {
    HideBlogList();
    ShowReturnBtn();
    HideNewBlogBtn();
    ShowForm();
}

function HandleHideForm() {
    ShowBlogList();
    HideReturnBtn();
    ShowNewBlogBtn();
    HideForm();
}

// Hide blog or project form
function HideForm() {
    $("#editorContainer").hide();
}

function HideEditor() {
    $('#editor').children().remove();
}

function ShowEditor() {
    return CreateEditor();
}

function HideBlogList() {
    $("main.container").hide();
}

function ShowBlogList() {
    $("main.container").show();
}

function ShowReturnBtn() {
    $("#ReturnNav").show();
}

function HideReturnBtn() {
    $("#ReturnNav").hide();
}

function ShowNewBlogBtn() {
    if (meta.btype === 1) {
        $("#NewBlogNav").show();
    } else {
        $("#NewBlogNav").hide();
    }
    $("#UpdateBlogNav").show();
}

function HideNewBlogBtn() {
    $("#NewBlogNav").hide();
    $("#UpdateBlogNav").hide();
}

這邊就可以看出jquery的劣勢,要做單頁應用哪些出現哪些消失控制得很繁瑣,vue之類的會叫為簡化

現在回來處理blog內容的顯示,把使用者寫的markdown parse成html,打開main.js補上

if (window.location.pathname === "/") {
    rend_root();
} else {
    rend_blog();
    rend_content();
}

// rend for blog content
function rend_content() {
    let content = $('#content');
    if (content.length <= 0) {
        return;
    }

    let context = content.html();

    const { Editor } = toastui;
    const { codeSyntaxHighlight, tableMergedCell } = Editor.plugin;

    // create viewer
    const viewer = Editor.factory({
        el: document.querySelector('#content'),
        viewer: true,
        height: '500px',
        initialValue: context,
        plugins: [codeSyntaxHighlight, tableMergedCell]
    });

    // hide origin
    content.after("<div id='metaContent' style='display:none'>" + context + "</div>");
}

總結

前端就這樣告一段落,有時間會把其他功能補上,基本上寫code就告一段落,之後會寫部屬相關的工具與服務

目前的工作環境

.
├── app
│   ├── apperr
│   │   ├── error.go
│   │   └── handle.go
│   ├── common
│   │   └── cookie.go
│   ├── config
│   │   └── app
│   │       ├── app.yaml
│   │       └── error.yaml
│   ├── database
│   │   ├── auth.go
│   │   ├── connect.go
│   │   ├── error.go
│   │   ├── main.go
│   │   └── scheme.go
│   ├── go.mod
│   ├── go.sum
│   ├── log
│   │   ├── logger.go
│   │   └── logging.go
│   ├── main.go
│   ├── middleware
│   │   ├── auth.go
│   │   ├── error.go
│   │   └── log.go
│   ├── router
│   │   ├── account.go
│   │   ├── asset.go
│   │   ├── host_switch.go
│   │   └── main.go
│   ├── serve
│   │   ├── account.go
│   │   ├── asset.go
│   │   ├── auth.go
│   │   ├── main.go
│   │   └── main_test.go
│   ├── setting
│   │   └── setting.go
│   ├── util
│   │   ├── debug
│   │   │   ├── stack.go
│   │   │   └── stack_test.go
│   │   ├── file
│   │   │   └── file.go
│   │   ├── hash
│   │   │   ├── hash.go
│   │   │   └── hash_test.go
│   │   └── random
│   │       └── random.go
│   └── view
│       ├── css
│       ├── html
│       │   ├── component
│       │   │   ├── blogContainer.html
│       │   │   ├── blogList.html
│       │   │   ├── editor.html
│       │   │   ├── navigation.html
│       │   │   └── signContainer.html
│       │   └── meta
│       │       ├── head.html
│       │       └── index.html
│       └── js
│           ├── account.js
│           ├── editor.js
│           ├── main.js
│           ├── owner.js
│           └── sign.js
└── database
    └── maindata

上一篇
Day18 前端串接
下一篇
Day20 nginx設定
系列文
從coding到上線-打造自己的blog系統30

尚未有邦友留言

立即登入留言