Laravel 使用 TinyMCE 以及處理上傳圖片(驗證,防呆,刪除)
版本
Laravel 8
TinyMCE 6
初始化以及引入 TinyMCE
1. 創建新項目
composer create-project laravel/laravel my-tiny-app
|
2. 到項目根目錄
3. 新增可重用組件component
php artisan make:component Head/tinymceConfig
|
創建好後並編輯,初始化tiny
tinymce-config.blade.php
使用api
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', 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> <x-head.tinymce-config /> </head> <body> <h1>TinyMCE in Laravel</h1> <x-forms.tinymce-editor /> </body> </html>
|
6. 啟動服務
在本地運行就可以看到編輯器了
php artisan serve
7. 語言設置
api方式
加上對應語言
查看語言
<script> tinymce.init({ plugins: 'code table lists', language: "zh_TW", }); </script>
|
tiny 上傳圖片
1. 修改tinymce-config.blade.php
增加上傳圖片的Url
tinymce.init({ selector: 'textarea#myeditorinstance', 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
訪問,運行
4. 忽略 csrf 驗證
更改VerifyCsrfToken.php
protected $except = [ '/upload/image', ];
|
5. 效果
範例 Tiny 設定
tinymce.init({ selector: "textarea#myeditorinstance", 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, });
|
更多設定
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", 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", 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(); } }) .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", 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()); } return el.status; }); if (!$status.includes(false)) { $("#your_form_id").submit(); } }) .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); } }
|
Laravel 使用 TinyMCE 以及處理上傳圖片(驗證,防呆,刪除)