Laravel 8
TinyMCE 6
composer create-project laravel/laravel my-tiny-app
cd my-tiny-app
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>
`composer require tinymce/tinymce`
檔案: `webpack.mix.js`
`mix.copyDirectory('vendor/tinymce/tinymce', 'public/js/tinymce');`
`npx mix`
php artisan make:component Forms/tinymceEditor
新增後編輯tinymce-editor.blade.php
<form method="post">
<textarea id="myeditorinstance">Hello, World!</textarea>
</form>
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>
在本地運行就可以看到編輯器了
php artisan serve
加上對應語言
<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
});
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",
});
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"]);
}
public
磁盤使用local
驅動程序並將其文件存儲在storage/app/public
.
要使這些文件可以從 Web
訪問,運行
php artisan storage:link
更改VerifyCsrfToken.php
protected $except = [
'/upload/image',
];
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
上傳處理程序函式替換為自定義邏輯的函式
改寫原先官方寫法
<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 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>
對上傳的圖片進行驗證
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
有錯誤時回報給使用者
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);
}
}