これからCKEditor5を使った画像のアップロードと、画像を含んだ文章の表示を実装します。
今回の記事で、アプリケーションは完成です。
最初に前回作成したREST APIのURLを、アプリケーションの置換文字列として参照できるようにします。
アプリケーション定義の置換を開き、置換文字列としてG_IMAGE_URL、置換値としてREST APIの完全なURLを入力します。
変更の適用をクリックします。
ページ・デザイナにてページ番号2のフォームのページを開きます。
ページ・プロパティのJavaScriptのファンクションおよびグローバル変数の宣言に、これからいくつかの場所に現れる値を宣言しておきます。
const apexSession = apex.env.APP_ID + ',' + apex.env.APP_SESSION;
const imageUrl = "&G_IMAGE_URL.";
ページ・アイテムP2_CONTENTの詳細のJavaScript初期化コードに、以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function(options){ | |
// 画像を扱うプラグインを有効化する。 | |
options.editorOptions.extraPlugins.push( | |
CKEditor5.image.Image, | |
CKEditor5.image.ImageResize, | |
CKEditor5.image.AutoImage, | |
CKEditor5.link.LinkImage, | |
CKEditor5.upload.SimpleUploadAdapter | |
); | |
// 画像とは関係しないが、コード・エディタも有効化する。 | |
options.editorOptions.codeBlock = { | |
languages: [ | |
{ language: 'javascript', label: 'JavaScript', class: 'js javascript js-code' } | |
] | |
}; | |
/* | |
* 画像のアップローダーを構成する。 | |
* | |
* uploadUrlとしてimageUrlつまり、ORDSのREST APIを指定している。 | |
* withCredentialsをtrueにして認証を有効にし、 | |
* Apex-Sessionヘッダーとともに、APEXのアプリケーションIDとセッションIDを送信している。 | |
*/ | |
options.editorOptions.simpleUpload = { | |
uploadUrl: imageUrl, | |
withCredentials: true, | |
headers: { | |
'Apex-Session': apexSession | |
} | |
}; | |
// ツールバーに画像をアップロードするボタンを追加する。 | |
let toolbar = options.editorOptions.toolbar; | |
toolbar.push("uploadImage"); | |
options.editorOptions.toolbar = toolbar; | |
// 画像のツールバーを構成する。 | |
options.editorOptions.image.toolbar = [ | |
'imageStyle:inline', | |
'imageStyle:wrapText', | |
'imageStyle:breakText', | |
'|', | |
'imageResize', | |
'|', | |
'toggleImageCaption', | |
'imageTextAlternative', | |
'|', | |
'LinkImage' | |
]; | |
/* | |
* 編集画面に適用されるeditingDowncastコンバーターを構成する。 | |
* CKEditorにはmodelとviewという考え方があり、MarkdownやHTMLはview、それをCKEditorはmodelに変換して取り込む。 | |
* データの編集はmodelに実施する。upcastはMarkdownやHTMLを取り込み、modelに変換する処理、 | |
* downcastはmodelからMarkdownやHTMLを生成する処理で、editingDowncastはmodelを編集するキャンパスに表示する | |
* 際に適用される。 | |
* | |
* editingDowncastを使って、画像URLが見つかったら(イメージを見つけるとattribute:srcのイベントが発生する)、 | |
* そのイベントに渡されるdata.attributeNewValueが画像のURLなので、そのURLに_apex_session=引数を | |
* 加えて認証に成功させている。 | |
*/ | |
options.executeOnInitialization = editor => { | |
editor.conversion.for( 'editingDowncast' ).add( dispatcher => { | |
dispatcher.on( | |
'attribute:src', | |
( evt, data, conversionApi ) => { | |
if ( !conversionApi.consumable.test( data.item, 'attribute:src' ) ) { | |
return; | |
} | |
if ( data.attributeNewValue ) { | |
if ( data.attributeNewValue.startsWith(imageUrl)) { | |
data.attributeNewValue += '?_apex_session=' + apexSession; | |
} | |
} | |
}, | |
{ priority: 'high' } | |
); | |
} ); | |
} | |
return options; | |
} |
JavaScript初期化コードでCKEditor5のeditingDowncastコンバーターを実装しています。この機能によりイメージURLに引数?_apex_session=セッションIDを付加し、REST APIの認証が通るようにしています。しかし、editingDowncastコンバーターが初期化されるのはページ・アイテムにデータがフェッチされた後(プロセス - 初期化フォームDocumentの実行後)になります。そのため、このままではeditingDowncastコンバーターが働きません。
CKEditor5の初期化後にデータが読み直されるよう、ページのロードのタイミングで動的アクションを作成して、ページ・アイテムP2_CONTENTにデータを再読み込みします。作成した動的アクションの識別の名前はP2_CONTENTの再ロードとします。
TRUEアクションの識別の名前はCONTENTの設定とします。アクションは値の設定です。設定のタイプの設定としてSQL Statementを選択し、SQL文に以下を記述します。
select content from rte_documents where id = :P2_ID
送信するアイテムとしてP2_IDを選択します。影響を受ける要素の選択タイプはアイテム、アイテムはP2_CONTENTを選択します。実行の初期化時に実行はOFFにします。
以上でページのロード時に、ページ・アイテムP2_CONTENTの内容が再度読み込まれます。
CKEditorを使ってアップロードされた画像は、他のドキュメントと共有されることはないという前提です。(手作業で画像URLを打ち込まない限りは、そのような状況は発生しない。)
画像の紐付け処理を行うため、プロセスを作成します。作成したプロセスは、ダイアログを閉じるの前に配置します。
識別の名前を画像の紐付け、タイプはコードの実行とし、以下のPL/SQLコードを記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
l_link varchar2(32767); | |
l_id varchar2(80); | |
i pls_integer; | |
l_images apex_t_varchar2; | |
begin | |
/* | |
* アップロードされたCLOBに含まれている画像URLを見つけ、 | |
* 表RTE_IMAGESのDOCUMENT_IDに、現在編集中のドキュメントIDを設定する。 | |
*/ | |
i := 1; | |
while true | |
loop | |
-- 画像URLを見つける。 | |
l_link := regexp_substr(:P2_CONTENT, :G_IMAGE_URL || '/([0-9]+)',1,i); | |
if l_link is null then | |
-- なければ終了。 | |
exit; | |
else | |
-- あれば画像URLよりID部分を取り出す。 | |
i := i + 1; | |
l_id := regexp_replace(l_link, :G_IMAGE_URL || '/([0-9]+)', '\1'); | |
apex_string.push(l_images,l_id); | |
end if; | |
end loop; | |
-- インサートの場合は表CKE_IMAGESに指定したドキュメントIDの列はない。なので、効果があるのはアップデート時のみ。 | |
update rte_images set document_id = null where document_id = :P2_ID; | |
-- CLOBデータに含まれている画像IDのDOCUMENT_IDを、現在作成または更新中の表RTE_DOCUMENTSのIDにする。 | |
-- アップロードされた画像はドキュメント間で共有されることはないため、これで問題ない。 | |
update rte_images set document_id = :P2_ID where id in (select column_value from table(l_images)); | |
/* | |
* 文章を記述中に画像を貼り付けた後に削除した場合、その画像は表RTE_IMAGESに | |
* DOCUMENT_IDがNULLの状態で残る。この画像については、定期的に実行するバッチなどで消去する | |
* 必要がある。 | |
*/ | |
end; |
対話モード・レポートにも画像が表示されるようにします。
ページ・デザイナにてページ番号1の対話モード・レポートのページを開きます。
対話モード・レポートのソースのタイプをSQL問合せに変更し、SQL問合せとして以下を記述します。
select
ID
, TITLE
-- , CONTENT
, regexp_replace(
content
, :G_IMAGE_URL || '/([0-9]+)'
, :G_IMAGE_URL || '/\1?_apex_session=' || :APP_ID || ',' || :APP_SESSION
) content
, AUTHOR
, PUBLISHED
, CREATED
, CREATED_BY
, UPDATED
, UPDATED_BY
from RTE_DOCUMENTS
以上でアプリケーションは完成です。
今回作成したアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/ckeditor-image-sample.zip
REST APIのエクスポートです。
https://github.com/ujnak/apexapps/blob/master/exports/ORDS_REST_WKSP_APEXDEV_rteditor_2023_07_04.sql
Oracle APEXのアプリケーション作成の参考になれば幸いです。
追記
置換文字列の指定について
今回のアプリケーションでは、アプリケーション定義の置換文字列としてG_IMAGE_URLを定義しています。その値にREST APIのURLを記載していますが、このような値をアプリケーションのエクスポートには含めたくない、といった場合は多いと思います。
このような場合、最初に置換値を共有しても問題ない文字列に置き換えます。例えば、G_IMAGE_URLであれば、以下のように置き換えます。
https://changeyouridentifier-apexdev.adb.us-ashburn-1.oraclecloudapps.com/ords/apexdev/rteditor/image
サポートするオブジェクトを開きます。
アプリケーション置換文字列を開きます。
置換文字列G_IMAGE_URLのプロンプトにチェックを入れ、プロンプト・テキストに画像アップロードURLを設定します。
この設定を行なった後に、アプリケーションをエクスポートします。
このアプリケーションをインポートすると、インポート中に置換文字列G_IMAGE_URLの設定を求められます。
ユーザーは置換文字列に新しい値を入力する必要があります。
インポートする環境に依存して変更が必要な値や、共有が望ましくない値について、このような設定を使うことができます。
マークダウンだとツールによる画像への設定が保存されない
今回は列CONTENTのフォーマットにマークダウンを選択しています。マークダウンにすると、編集ツールによる画像への設定が保存されません。
ページ・アイテムP2_CONTENTの再読み込みを行なう動的アクションの、特殊文字をエスケープはOFFにします。
CKEditorは、HTMLやMarkdownを一旦内部でmodelに変換するため、クロスサイト・スクリプティングの対応が行われていると考えられます。ただ、データベースにはHTMLが保存されていて、CKEditorを使わずに更新が行われる可能性もあります。
データから画像URLを見つける方法が甘い
現在は以下のコードにて、文章から画像URLを見つけています。
l_link := regexp_substr(:P2_CONTENT, :G_IMAGE_URL || '/([0-9]+)',1,i);
CKEditorで画像を貼り付ける以外の方法が使われないという前提です。厳密に行う場合は、マークダウンの場合は、HTMLの場合は<img src="URL">といった書式まで考慮した上で文章から画像URLを見つける必要があるでしょう。
ハウスキーピングのジョブは別途必要です
文書に貼り付けた画像を削除すると、表RTE_IMAGESには列DOCUMENT_IDがNULLとなった状態で行が残ります。この画像は文書からは参照されていません。
不要な画像を削除するSQLは以下になります。これを定期的に実行します。Oracle APEXの自動化の機能が使えるでしょう。
delete from rte_images where document_id is null;
完