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
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>
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
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. 效果
alt="" />
範例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: "/",
});
更多設定
處理圖片驗證
改寫原先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"]);
}
效果
alt="" />
僅在表單提交時才將圖像上傳到服務器
錯誤訊息alert
有錯誤時回報給使用者
function createErrorNotification(name) {
tinymce.activeEditor.notificationManager.open({
text: `圖片名稱:${name} 檔案過大,請重新上傳`,
type: 'error'
});
}
監聽使用者點擊送出表單,成功則送出,失敗則回傳錯誤訊息
當使用者點擊送出,先取消送出動作,檢視圖片狀態,當有錯誤回報給使用者
效果如下:
alt="" />
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' => '刪除成功']);
}
效果
alt="" />
完整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);
}
}