Add attachment preview insertion

This commit is contained in:
Biscuit 2025-10-16 14:39:18 +02:00
parent 146a6c01cc
commit fdb72ff3e9
6 changed files with 110 additions and 6 deletions

View File

@ -92,6 +92,32 @@ class Attachment extends Model implements OwnableInterface
return ['text/html' => $this->htmlLink(), 'text/plain' => $this->markdownLink()];
}
/**
* Determine if this attachment can provide preview content for editor insertion.
*/
public function hasPreviewContent(): bool
{
if ($this->external) {
return false;
}
$previewExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx'];
return in_array(strtolower($this->extension), $previewExtensions);
}
/**
* Get preview-friendly content for insertion into the editors.
*/
public function editorPreviewContent(): array
{
$html = '<div class="attachment-preview">'
. '<iframe class="attachment-preview-frame" src="' . e($this->getUrl(true)) . '" title="' . e($this->name) . '" loading="lazy"></iframe>'
. '</div>';
return ['text/html' => $html, 'text/plain' => $html];
}
/**
* Generate the HTML link to this attachment.
*/

View File

@ -369,6 +369,7 @@ return [
'attachments_link_url_hint' => 'Url of site or file',
'attach' => 'Attach',
'attachments_insert_link' => 'Add Attachment Link to Page',
'attachments_insert_preview' => 'Add Attachment Preview to Page',
'attachments_edit_file' => 'Edit File',
'attachments_edit_file_name' => 'File Name',
'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',

View File

@ -33,12 +33,11 @@ export class Attachments extends Component {
});
this.container.addEventListener('event-emit-select-insert', event => {
const insertContent = event.target.closest('[data-drag-content]').getAttribute('data-drag-content');
const contentTypes = JSON.parse(insertContent);
window.$events.emit('editor::insert', {
html: contentTypes['text/html'],
markdown: contentTypes['text/plain'],
});
this.insertContentFromCard(event, 'data-drag-content');
});
this.container.addEventListener('event-emit-select-insert-preview', event => {
this.insertContentFromCard(event, 'data-preview-content');
});
this.attachLinkButton.addEventListener('click', () => {
@ -66,6 +65,28 @@ export class Attachments extends Component {
});
}
insertContentFromCard(event, attributeName) {
const card = event.target.closest(`[${attributeName}]`);
if (!card) {
return;
}
const insertContent = card.getAttribute(attributeName);
if (!insertContent) {
return;
}
const contentTypes = JSON.parse(insertContent);
const html = contentTypes['text/html'] ?? '';
const markdown = contentTypes['text/plain'] ?? html;
if (!html && !markdown) {
return;
}
window.$events.emit('editor::insert', {html, markdown});
}
updateOrder(idOrder) {
window.$http.put(`/attachments/sort/page/${this.pageId}`, {order: idOrder}).then(resp => {
window.$events.emit('success', resp.data.message);

View File

@ -117,6 +117,19 @@
max-width: 100%;
}
.attachment-preview {
margin: vars.$m 0;
}
.attachment-preview-frame {
width: 100%;
min-height: 360px;
border-radius: 4px;
border: 1px solid;
@include mixins.lightDark(border-color, #DDD, #444);
@include mixins.lightDark(background-color, #FFF, #111);
}
a {
text-decoration: underline;
}

View File

@ -5,6 +5,9 @@
option:ajax-delete-row:url="{{ url('/attachments/' . $attachment->id) }}"
data-id="{{ $attachment->id }}"
data-drag-content="{{ json_encode($attachment->editorContent()) }}"
@if($attachment->hasPreviewContent())
data-preview-content="{{ json_encode($attachment->editorPreviewContent()) }}"
@endif
class="card drag-card">
<div class="handle">@icon('grip')</div>
<div class="py-s">
@ -16,6 +19,13 @@
type="button"
title="{{ trans('entities.attachments_insert_link') }}"
class="drag-card-action text-center text-link">@icon('link')</button>
@if($attachment->hasPreviewContent())
<button component="event-emit-select"
option:event-emit-select:name="insert-preview"
type="button"
title="{{ trans('entities.attachments_insert_preview') }}"
class="drag-card-action text-center text-link">@icon('expand-text')</button>
@endif
@if(userCan(\BookStack\Permissions\Permission::AttachmentUpdate, $attachment))
<button component="event-emit-select"
option:event-emit-select:name="edit"

View File

@ -468,4 +468,37 @@ class AttachmentTest extends TestCase
$this->files->deleteAllAttachmentFiles();
}
public function test_preview_content_available_for_supported_internal_files()
{
$attachment = Attachment::factory()->make([
'extension' => 'pdf',
'external' => false,
'name' => 'Preview.pdf',
]);
$attachment->id = 42;
$this->assertTrue($attachment->hasPreviewContent());
$preview = $attachment->editorPreviewContent();
$this->assertArrayHasKey('text/html', $preview);
$this->assertStringContainsString('<iframe', $preview['text/html']);
$this->assertStringContainsString('/attachments/42?open=true', $preview['text/html']);
$this->assertSame($preview['text/html'], $preview['text/plain']);
}
public function test_preview_content_not_available_for_external_or_unsupported_files()
{
$attachment = Attachment::factory()->make([
'extension' => 'txt',
'external' => false,
]);
$this->assertFalse($attachment->hasPreviewContent());
$externalAttachment = Attachment::factory()->make([
'extension' => 'pdf',
'external' => true,
]);
$this->assertFalse($externalAttachment->hasPreviewContent());
}
}