Try   HackMD

Laravel 使用 TinyMCE 以及處理上傳圖片(驗證,防呆,刪除)

版本

Laravel 8

TinyMCE 6

初始化以及引入TinyMCE

1. 創建新項目

composer create-project laravel/laravel my-tiny-app

2. 到項目根目錄

cd my-tiny-app

3. 新增可重用組件component

php artisan make:component Head/tinymceConfig

創建好後並編輯,初始化tiny

tinymce-config.blade.php

使用API建立tinymce

no-api-key替換成你的api key,到Tiny註冊

<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script> <script> tinymce.init({ selector: 'textarea#myeditorinstance', // Replace this CSS selector to match the placeholder element for TinyMCE plugins: 'code table lists', toolbar: 'undo redo | blocks | bold italic | alignleft aligncenter alignright | indent outdent | bullist numlist | code | table' }); </script>

自建tinymce

1. 使用 Composer 將 TinyMCE 添加到項目中:

​​​​`composer require tinymce/tinymce`

2. 添加一個 Laravel Mix 任務,在 Mix 運行時將 TinyMCE 複製到公共文件中:

​​​​檔案: `webpack.mix.js`

​​​​`mix.copyDirectory('vendor/tinymce/tinymce', 'public/js/tinymce');`

3. 運行 Laravel Mix 將 TinyMCE 複製到 public/js/ 目錄

​​​​`npx mix`

4. 新增表單的組件

php artisan make:component Forms/tinymceEditor

新增後編輯tinymce-editor.blade.php

<form method="post"> <textarea id="myeditorinstance">Hello, World!</textarea> </form>

5. 編輯welcome.blade.php

<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>TinyMCE in Laravel</title> <!-- Insert the blade containing the TinyMCE configuration and source script --> <x-head.tinymce-config/> </head> <body> <h1>TinyMCE in Laravel</h1> <!-- Insert the blade containing the TinyMCE placeholder HTML element --> <x-forms.tinymce-editor/> </body> </html>

6. 啟動服務

在本地運行就可以看到編輯器了

php artisan serve

7. 語言設置

api方式

加上對應語言

查看語言

<script> tinymce.init({ // ... plugins: 'code table lists', language: "zh_TW", // ... }); </script>

自建

將要使用語言文件解壓縮到path/to/tinymce/langs/文件夾中。

下載語言包

<script> tinymce.init({ // ... plugins: 'code table lists', language: "zh_TW", // ... }); </script>

關閉高級升級促銷

升級促銷出現在 TinyMCE 菜單欄未使用的角落。如果菜單欄被禁用,它就不會出現。

tinymce.init({ selector: "textarea", // change this value according to your HTML promotion: true });

tiny上傳圖片

1. 修改tinymce-config.blade.php

增加上傳圖片的Url

tinymce.init({ selector: 'textarea#myeditorinstance', // Replace this CSS selector to match the placeholder element for TinyMCE plugins: 'code table lists', toolbar: 'undo redo | blocks | bold italic | alignleft aligncenter alignright | indent outdent | bullist numlist | code | table' images_upload_url: "/upload/image", });

2.撰寫上傳控制器程式碼

public function upload(Request $request) { $fileName = date("mdY") . $request->file('file')->getClientOriginalName(); $path = $request->file('file')->storeAs('uploads', $fileName, 'public'); return response()->json(['location' => "/storage/$path"]); }

3. 開啟storage的web連結

public磁盤使用local驅動程序並將其文件存儲在storage/app/public.

要使這些文件可以從 Web 訪問,運行

php artisan storage:link

4. 忽略csrf驗證

更改VerifyCsrfToken.php

protected $except = [ '/upload/image', ];

5. 效果

範例Tiny設定

tinymce.init({ selector: 'textarea#myeditorinstance', // Replace this CSS selector to match the placeholder element for TinyMCE plugins: "a11ychecker advcode casechange export emoticons formatpainter image editimage linkchecker autolink lists checklist media mediaembed pageembed permanentpen powerpaste preview table advtable tableofcontents tinycomments tinymcespellchecker", toolbar: "image preview table media" + "undo redo | styles | bold italic | " + "alignleft aligncenter alignright alignjustify | " + "outdent indent | numlist bullist | emoticons", // 工具欄 toolbar_mode: "floating", tinycomments_mode: "embedded", tinycomments_author: "Author name", language: "zh_TW", // 介面語言 mobile: { menubar: true, }, image_title: true, file_picker_types: 'image', images_upload_url: "/upload/image", relative_urls: false, // 更改後編輯頁面才看的到圖片 // images_upload_base_path: "/", });

更多設定

TinyMCE 6 Documentation

處理圖片驗證

改寫原先images_upload_handler函式

默認 JavaScript 上傳處理程序函式替換為自定義邏輯的函式

XMLHttpRequest寫法

改寫原先官方寫法

<script> const my_image_upload_handler = (blobInfo, progress) => new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.withCredentials = false; xhr.open('POST', '/upload/image'); xhr.upload.onprogress = (e) => { progress(e.loaded / e.total * 100); }; xhr.onload = () => { if (xhr.status === 403) { reject({ message: 'HTTP Error: ' + xhr.status, remove: true }); return; } if (xhr.status < 200 || xhr.status >= 300) { reject('HTTP Error: ' + xhr.status); return; } const json = JSON.parse(xhr.responseText); // 增加此行 if (json.error) { reject(json.error.join('\n')); return; } if (!json || typeof json.location != 'string') { reject('Invalid JSON: ' + xhr.responseText); return; } resolve(json.location); }; xhr.onerror = () => { reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status); }; const formData = new FormData(); formData.append('file', blobInfo.blob(), blobInfo.filename()); xhr.send(formData); }); tinymce.init({ selector: 'textarea#myeditorinstance', // Replace this CSS selector to match the placeholder element for TinyMCE images_upload_handler: my_image_upload_handler, plugins: "a11ychecker advcode casechange export emoticons formatpainter image editimage linkchecker autolink lists checklist media mediaembed pageembed permanentpen powerpaste preview table advtable tableofcontents tinycomments tinymcespellchecker", toolbar: "image preview table media" + "undo redo | styles | bold italic | " + "alignleft aligncenter alignright alignjustify | " + "outdent indent | numlist bullist | emoticons", toolbar_mode: "floating", tinycomments_mode: "embedded", tinycomments_author: "Author name", language: "zh_TW", mobile: { menubar: true, }, image_title: true, file_picker_types: 'image', images_upload_url: "/upload/image", relative_urls: false, }); </script>

axios寫法

如沒有引入axios cdn記得引入

<!-- 如未引入才需要 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.26.1/axios.min.js" integrity="sha512-bPh3uwgU5qEMipS/VOmRqynnMXGGSRv+72H/N260MQeXZIK4PG48401Bsby9Nq5P5fz7hy5UGNmC/W1Z51h2GQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script> const my_image_upload_handler = (blobInfo, progress) => new Promise((resolve, reject) => { const formData = new FormData(); formData.append('file', blobInfo.blob(), blobInfo.filename()); axios.post('/upload/image', formData) .then(function (response) { if (response.status === 403) { reject({ message: 'HTTP Error: ' + response.status, remove: true }); return; } if (response.status < 200 || response.status >= 300) { reject('HTTP Error: ' + response.status); return; } if (response.data.error) { reject(response.data.error.join('\n')); return; } if (!response.data || typeof response.data.location != 'string') { reject('Invalid JSON: ' + xhr.responseText); return; } resolve(response.data.location); }) .catch(function (error) { reject('Image upload failed due to a XHR Transport error. Error: ' + error); }); }); tinymce.init({ selector: 'textarea#myeditorinstance', // Replace this CSS selector to match the placeholder element for TinyMCE images_upload_handler: my_image_upload_handler, // ....... }); </script>

改寫上傳圖片controller

對上傳的圖片進行驗證

use Illuminate\Support\Facades\Validator; public function upload(Request $request) { $messages = [ 'file.max' => '圖片檔案不能大於2000KB', ]; $validator = Validator::make($request->all(), [ 'file' => 'max:2000', ], $messages); if ($validator->fails()) { return response()->json([ 'error' => $validator->errors()->all(), ], 200, [], JSON_UNESCAPED_UNICODE); } $fileName = date("mdY") . $request->file('file')->getClientOriginalName(); $path = $request->file('file')->storeAs('uploads', $fileName, 'public'); return response()->json(['location' => "/storage/$path"]); }

效果

僅在表單提交時才將圖像上傳到服務器

錯誤訊息alert

有錯誤時回報給使用者

Tinymce 通知

function createErrorNotification(name) { tinymce.activeEditor.notificationManager.open({ text: `圖片名稱:${name} 檔案過大,請重新上傳`, type: 'error' }); }

監聽使用者點擊送出表單,成功則送出,失敗則回傳錯誤訊息

當使用者點擊送出,先取消送出動作,檢視圖片狀態,當有錯誤回報給使用者

效果如下:

tiny設定加上automatic_uploads: false,不自動上傳圖片,當呼叫editor.uploadImages()才進行上傳動作

// ... setup(editor) { editor.on('submit', function(e) { e.preventDefault(); // 取消送出動作 editor.uploadImages() .then(function(blobInfo, progress) { $status = blobInfo.map(el => { if (el.status == false) { createErrorNotification(el.blobInfo.filename()); } return el.status; }) if (!$status.includes(false)) { $('#your_form_id').submit(); // 送出表單的ID } }) .catch(function(error) { console.log(error); }); }); },

監聽使用者利用按鍵刪除圖片

假如有A跟B兩張圖片,A圖片符合規則,B圖片不符合,但後臺這時已經儲存A圖片,B圖片不儲存並回傳錯訊息,
如果使用者將A圖片刪除不使用,A圖片就被困在我們的資料夾裡,
為了解決這情況,就利用監聽鍵盤按鈕來觸發刪除圖片事件

首先監聽Backspace以及Delete

// ... setup(editor) { editor.on("keydown", function(e){ if ((e.keyCode == 8 || e.keyCode == 46) && tinymce.activeEditor.selection) { let selectedNode = tinymce.activeEditor.selection.getNode(); if (selectedNode && selectedNode.nodeName == 'IMG') { let imageSrc = selectedNode.src; if (imageSrc.split("storage")[1]) { let imageName = imageSrc.split("storage")[1]; axios.post('/delete/post/image', { fileName: imageName }) .then(function (response) { if (response.status === 200) { console.log('刪除成功'); } }) .catch(function (error) { console.log('刪除失敗'); }); } } } }); },

刪除圖片程式碼

記得去新增路由

use Illuminate\Support\Facades\Storage; public function deleteUpload(Request $request) { $fileName = $request->fileName; if (Storage::disk('public')->exists($fileName)) { Storage::disk('public')->delete($fileName); } return response()->json(['success' => '刪除成功']); }

效果

完整tinymce-config.blade.php

下台一鞠躬

<script src="https://cdn.tiny.cloud/1/nsoqryhl188m8ah7y44z7ln6aj2dujg7aoyc4bijnv04nsqj/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script> {{-- <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.26.1/axios.min.js" integrity="sha512-bPh3uwgU5qEMipS/VOmRqynnMXGGSRv+72H/N260MQeXZIK4PG48401Bsby9Nq5P5fz7hy5UGNmC/W1Z51h2GQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> --}} <script> const my_image_upload_handler = (blobInfo, progress) => new Promise((resolve, reject) => { const formData = new FormData(); formData.append('file', blobInfo.blob(), blobInfo.filename()); axios.post('/upload/image', formData) .then(function (response) { if (response.status === 403) { reject({ message: 'HTTP Error: ' + response.status, remove: true }); return; } if (response.status < 200 || response.status >= 300) { reject('HTTP Error: ' + response.status); return; } if (response.data.error) { reject(response.data.error.join('\n')); return; } if (!response.data || typeof response.data.location != 'string') { reject('Invalid JSON: ' + xhr.responseText); return; } resolve(response.data.location); }) .catch(function (error) { reject('Image upload failed due to a XHR Transport error. Error: ' + error); }); }); function createErrorNotification(name) { tinymce.activeEditor.notificationManager.open({ text: `圖片名稱:${name} 檔案過大,請重新上傳`, type: 'error' }); } tinymce.init({ selector: 'textarea#myeditorinstance', // Replace this CSS selector to match the placeholder element for TinyMCE images_upload_handler: my_image_upload_handler, plugins: "a11ychecker advcode casechange export emoticons formatpainter image editimage linkchecker autolink lists checklist media mediaembed pageembed permanentpen powerpaste preview table advtable tableofcontents tinycomments tinymcespellchecker", toolbar: "image preview table media" + "undo redo | styles | bold italic | " + "alignleft aligncenter alignright alignjustify | " + "outdent indent | numlist bullist | emoticons", toolbar_mode: "floating", tinycomments_mode: "embedded", tinycomments_author: "Author name", language: "zh_TW", mobile: { menubar: true, }, file_picker_types: 'image', relative_urls: false, image_title: true, automatic_uploads: false, images_upload_url: "/upload/image", setup(editor) { editor.on("keydown", function(e){ if ((e.keyCode == 8 || e.keyCode == 46) && tinymce.activeEditor.selection) { let selectedNode = tinymce.activeEditor.selection.getNode(); if (selectedNode && selectedNode.nodeName == 'IMG') { let imageSrc = selectedNode.src; if (imageSrc.split("storage")[1]) { let imageName = imageSrc.split("storage")[1]; axios.post('/delete/image', { fileName: imageName }) .then(function (response) { if (response.status === 200) { console.log('刪除成功'); } }) .catch(function (error) { console.log('刪除失敗'); }); } } } }); editor.on('submit', function(e) { e.preventDefault(); // 取消送出動作 editor.uploadImages() .then(function(blobInfo, progress) { $status = blobInfo.map(el => { if (el.status == false) { createErrorNotification(el.blobInfo.filename()); // 錯誤訊息alert } return el.status; }) if (!$status.includes(false)) { $('#your_form_id').submit(); // 你的表單ID } }) .catch(function(error) { console.log(error); }); }); }, }); </script>

刪除時連帶刪除本地圖片

利用正規表達式找到img tag,如果是本地圖片就刪除

public function destroy(Request $request, $id) { $post = Post::findOrFail(id); preg_match_all('/<img [^>]*src="[^"]*"[^>]*>/', $post->content, $matches); foreach ($matches[0] as $match) { preg_match('/.*src="([^"]*)".*/', $match, $match); $url = explode("storage", $match[1]); if (count($url) > 1) { $this->delPic($url[1]); } } $post->delete(); return response()->json(['success' => '文章刪除成功']); } public function delPic($fileName) { if (Storage::disk('public')->exists($fileName)) { Storage::disk('public')->delete($fileName); } }