添付ファイルにあるPDFをプレビューしたいと思うこと多々ありますよね。
なので今回はレコード詳細画面だけではなく、レコード一覧画面でもPDFをプレビューできるカスタマイズをご紹介します。
以下のライブラリはCybozu CDNから引用している。
今日もCustomize Editor プラグインを使って書いていく!
といっても作りはシンプルなのでパパっと👇のpreview.js と preview.css を書いた。
(($) => {
"use strict";
// 添付ファイルフィールド取得
const getFileFields = () => {
const fields = [];
const fieldList = cybozu.data.page.SCHEMA_DATA.table.fieldList;
for (const id in fieldList) {
if (fieldList[id].type != "FILE") continue;
fields.push(fieldList[id].var);
}
return fields;
}
// パラメータ取得
const getParam = (link, key) => {
const url = new URL(link);
const params = url.searchParams;
return params.get(key);
};
// ファイルダウンロード
const download = async (fileKey) => {
const fileUrl = `${kintone.api.url("/k/v1/file.json", true)}?fileKey=${fileKey}`;
try {
const headers = {
"X-Requested-With": "XMLHttpRequest"
};
const resp = await fetch(fileUrl, {
method: "GET",
headers
});
const blob = await resp.blob();
const url = window.URL || window.webkitURL;
const blobUrl = url.createObjectURL(blob);
return blobUrl;
} catch (err) {
console.log(err);
return null;
}
};
// プレビュー
const preview = async (files) => {
const $preview = $(`
<div id="_preview">
<div class="__preview_content">
<div class="__preview_header">
<div class="__name">${files[0].name}</div>
<div class="__prev disabled"><i class="fa-solid fa-chevron-left"></i></div>
<div class="__next"><i class="fa-solid fa-chevron-right"></i></div>
<div class="__download"><i class="fa-solid fa-download"></i></div>
<div class="__close"><i class="fa-regular fa-circle-xmark"></i></div>
</div>
<div class="__iframes"></div>
</div>
<div class="__background __close"></div>
</div>
`).appendTo(document.body);
// 1ファイルの場合
if (files.length == 1) {
$preview.find(".__prev, .__next").remove();
}
// ファイル追加
for (const [i, file] of files.entries()) {
const url = await download(file.fileKey);
const params = "#toolbar=0&navpanes=0";
const $iframe = $(`<iframe data-index=${i}></iframe>`)
.attr("src", `${url}${params}`)
.attr("data-url", url)
.attr("data-name", file.name)
.appendTo($preview.find(".__iframes"));
if (i != 0) $iframe.hide();
}
let index = 0;
$preview.on("click", ".__next", (evt) => {
// 次のファイル
const max = files.length - 1;
const nextIndex = index + 1;
if (index > max) return;
if (nextIndex == max) {
$preview.find(".__next").addClass("disabled");
}
$preview.find(".__prev").removeClass("disabled");
index = nextIndex;
const $target = $preview.find(`iframe[data-index=${nextIndex}]`);
$preview.find("iframe").hide();
$preview.find(".__name").text($target.attr("data-name"));
$target.show();
})
.on("click", ".__prev", (evt) => {
// 前のファイル
const nextIndex = index - 1;
if (index < 0) return;
if (nextIndex == 0) {
$preview.find(".__prev").addClass("disabled");
}
$preview.find(".__next").removeClass("disabled");
index = nextIndex;
const $target = $preview.find(`iframe[data-index=${nextIndex}]`);
$preview.find("iframe").hide();
$preview.find(".__name").text($target.attr("data-name"));
$target.show();
})
.on("click", ".__download", () => {
// ダウンロード
const url = $preview.find(`iframe[data-index=${index}]`).attr("data-url");
const name = $preview.find(`iframe[data-index=${index}]`).attr("data-name");
const a = document.createElement("a");
a.href = url;
a.download = name;
a.click();
a.remove();
})
.on("click", ".__close", () => {
// 閉じる
$preview.remove();
});
};
// 一覧画面
kintone.events.on("app.record.index.show", (e) => {
const records = e.records;
for (const code of getFileFields()) {
// 添付ファイルフィールド要素を取得
const elements = kintone.app.getFieldElements(code);
if (!elements) return;
for (const element of elements) {
if (!$(element).find("a").length) continue;
const link = $(element).find("a").attr("href");
if (!link) continue;
const id = getParam(link, "record");
const record = records.find(x => x.$id.value == id);
// PDFがあれば取得
const files = record[code].value.filter(x => x.contentType == "application/pdf");
if (files.length) {
const $button = $(`<button><i class="fa-solid fa-magnifying-glass"></i></button>`);
$button.data("files", files);
$(element).addClass("__preview").append($button);
$button.on("click", (evt) => {
preview(files);
});
}
}
}
return e;
});
// 詳細画面
kintone.events.on("app.record.detail.show", (e) => {
const record = e.record;
for (const code of getFileFields()) {
const element = kintone.app.record.getFieldElement(code);
if (!$(element).find("a").length) continue;
// PDFがあれば取得
const files = record[code].value.filter(x => x.contentType == "application/pdf");
if (files.length) {
const $button = $(`<button><i class="fa-solid fa-magnifying-glass"></i>プレビュー</button>`);
$button.data("files", files);
$(element).addClass("__preview").append($button);
$button.on("click", () => {
preview(files);
});
}
}
return e;
});
})(jQuery);
@charset "UTF-8";
/* 一覧画面 */
td.__preview {
position: relative;
padding-right: 30px;
}
td.__preview ul {
width: 100%;
}
td.__preview li {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
td.__preview button {
display: flex;
align-items: center;
position: absolute;
top: 0;
right: 0;
width: 30px;
height: calc(100% - 1px);
background: #fdfeff;
border: none;
border-left: 1px solid #e3e7e8;
cursor: pointer;
user-select: none;
}
td.__preview button:hover {
background: #f7f7f7;
}
td.__preview button svg {
color: #3498db;
font-size: 18px;
pointer-events: none;
}
/* 詳細画面 */
div.control-value-gaia.__preview > button {
margin-top: 8px;
padding: 0 24px;
width: 100%;
height: 40px;
background: #fdfeff;
box-shadow: none;
box-sizing: border-box;
border: 1px solid #e3e7e8;
color: #3498db;
cursor: pointer;
font-size: 14px;
line-height: 40px;
user-select: none;
}
div.control-value-gaia.__preview > button:hover {
background: #f7f7f7;
}
div.control-value-gaia.__preview > button svg {
margin-right: 4px;
pointer-events: none;
}
/* プレビュー */
#_preview {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
z-index: 300;
}
#_preview * {
box-sizing: border-box;
}
#_preview .__preview_content {
position: fixed;
padding: 0 20px 20px 20px;
width: 90vw;
height: 95vh;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.6);
border-radius: 16px;
z-index: 302;
}
#_preview .__background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.3);
z-index: 301;
}
#_preview .__preview_header {
display: flex;
column-gap: 8px;
padding: 10px 0;
width: 100%;
height: 60px;
white-space: nowrap;
}
#_preview .__preview_header .__prev,
#_preview .__preview_header .__next,
#_preview .__preview_header .__download,
#_preview .__preview_header .__close {
position: relative;
width: 40px;
height: 40px;
cursor: pointer;
}
#_preview .__preview_header .__prev svg,
#_preview .__preview_header .__next svg,
#_preview .__preview_header .__download svg,
#_preview .__preview_header .__close svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #eee;
font-size: 30px;
pointer-events: none;
}
#_preview .__preview_header .__prev.disabled,
#_preview .__preview_header .__next.disabled {
pointer-events: none;
}
#_preview .__preview_header .__prev.disabled svg,
#_preview .__preview_header .__next.disabled svg {
color: #888;
}
#_preview .__preview_header .__name {
margin-right: auto;
width: calc(90vw - 250px);
color: #eee;
overflow: hidden;
text-overflow: ellipsis;
line-height: 40px;
white-space: nowrap;
user-select: none;
}
#_preview .__iframes {
width: 100%;
height: calc(95vh - 80px);
}
#_preview .__iframes iframe {
width: 100%;
height: 100%;
border: none;
}
まずは添付ファイルのフィールドコードを取得する関数。直接指定してもいいのだが、添付ファイルが複数ある場合などに、自動で取得するようにしたかったので、cybozu.dataの中にあるフィールド情報から添付ファイルフィールドだけに絞ってフィールドをコードを取得するようになっている。
// 添付ファイルフィールド取得
const getFileFields = () => {
const fields = [];
const fieldList = cybozu.data.page.SCHEMA_DATA.table.fieldList;
for (const id in fieldList) {
if (fieldList[id].type != "FILE") continue;
fields.push(fieldList[id].var);
}
return fields;
};
URLからパラメータを取得する関数。これは一覧画面にて添付ファイルのURLにレコード番号があったので、それを取得する用の関数。
// パラメータ取得
const getParam = (link, key) => {
const url = new URL(link);
const params = url.searchParams;
return params.get(key);
};
ファイルをダウンロードBlobURLで返却する関数。
// ファイルダウンロード
const download = async (fileKey) => {
const fileUrl = `${kintone.api.url("/k/v1/file.json", true)}?fileKey=${fileKey}`;
try {
const headers = {
"X-Requested-With": "XMLHttpRequest"
};
const resp = await fetch(fileUrl, {
method: "GET",
headers
});
const blob = await resp.blob();
const url = window.URL || window.webkitURL;
const blobUrl = url.createObjectURL(blob);
return blobUrl;
} catch (err) {
console.log(err);
return null;
}
};
プレビューを表示する関数。
// プレビュー
const preview = async (files) => {
const $preview = $(`
<div id="_preview">
<div class="__preview_content">
<div class="__preview_header">
<div class="__name">${files[0].name}</div>
<div class="__prev disabled"><i class="fa-solid fa-chevron-left"></i></div>
<div class="__next"><i class="fa-solid fa-chevron-right"></i></div>
<div class="__download"><i class="fa-solid fa-download"></i></div>
<div class="__close"><i class="fa-regular fa-circle-xmark"></i></div>
</div>
<div class="__iframes"></div>
</div>
<div class="__background __close"></div>
</div>
`).appendTo(document.body);
// 1ファイルの場合
if (files.length == 1) {
$preview.find(".__prev, .__next").remove();
}
// ファイル追加
for (const [i, file] of files.entries()) {
const url = await download(file.fileKey);
const params = "#toolbar=0&navpanes=0";
const $iframe = $(`<iframe data-index=${i}></iframe>`)
.attr("src", `${url}${params}`)
.attr("data-url", url)
.attr("data-name", file.name)
.appendTo($preview.find(".__iframes"));
if (i != 0) $iframe.hide();
}
let index = 0;
$preview.on("click", ".__next", (evt) => {
// 次のファイル
const max = files.length - 1;
const nextIndex = index + 1;
if (index > max) return;
if (nextIndex == max) {
$preview.find(".__next").addClass("disabled");
}
$preview.find(".__prev").removeClass("disabled");
index = nextIndex;
const $target = $preview.find(`iframe[data-index=${nextIndex}]`);
$preview.find("iframe").hide();
$preview.find(".__name").text($target.attr("data-name"));
$target.show();
})
.on("click", ".__prev", (evt) => {
// 前のファイル
const nextIndex = index - 1;
if (index < 0) return;
if (nextIndex == 0) {
$preview.find(".__prev").addClass("disabled");
}
$preview.find(".__next").removeClass("disabled");
index = nextIndex;
const $target = $preview.find(`iframe[data-index=${nextIndex}]`);
$preview.find("iframe").hide();
$preview.find(".__name").text($target.attr("data-name"));
$target.show();
})
.on("click", ".__download", () => {
// ダウンロード
const url = $preview.find(`iframe[data-index=${index}]`).attr("data-url");
const name = $preview.find(`iframe[data-index=${index}]`).attr("data-name");
const a = document.createElement("a");
a.href = url;
a.download = name;
a.click();
a.remove();
})
.on("click", ".__close", () => {
// 閉じる
$preview.remove();
});
};
一覧画面表示イベント。添付ファイルフィールドにPDFがあればプレビューボタンを配置。
// 一覧画面
kintone.events.on("app.record.index.show", (e) => {
const records = e.records;
for (const code of getFileFields()) {
// 添付ファイルフィールド要素を取得
const elements = kintone.app.getFieldElements(code);
if (!elements) return;
for (const element of elements) {
if (!$(element).find("a").length) continue;
const link = $(element).find("a").attr("href");
if (!link) continue;
const id = getParam(link, "record");
const record = records.find(x => x.$id.value == id);
// PDFがあれば取得
const files = record[code].value.filter(x => x.contentType == "application/pdf");
if (files.length) {
const $button = $(`<button><i class="fa-solid fa-magnifying-glass"></i></button>`);
$button.data("files", files);
$(element).addClass("__preview").append($button);
$button.on("click", (evt) => {
preview(files);
});
}
}
}
return e;
});
詳細画面表示イベント。添付ファイルフィールドにPDFがあればプレビューボタンを配置。
// 詳細画面
kintone.events.on("app.record.detail.show", (e) => {
const record = e.record;
for (const code of getFileFields()) {
const element = kintone.app.record.getFieldElement(code);
// PDFがあれば取得
const files = record[code].value.filter(x => x.contentType == "application/pdf");
if (files.length) {
const $button = $(`<button><i class="fa-solid fa-magnifying-glass"></i>プレビュー</button>`);
$button.data("files", files);
$(element).addClass("__preview").append($button);
$button.on("click", () => {
preview(files);
});
}
}
return e;
});
以上です。
あまり細かくは説明してませんが、コード内のコメントを見ていただければと思いますm(__)m