mirror of
https://github.com/jbcr/core.git
synced 2026-04-02 22:32:20 +02:00
Merge pull request #25 from bobdenotter/feature/uploader
First version of Filepond uploader
This commit is contained in:
@@ -43,4 +43,6 @@ $(document).ready(function() {
|
||||
// $(".ui.calendar").calendar({
|
||||
// ampm: false
|
||||
// });
|
||||
|
||||
var lightbox = $('a.lightbox').simpleLightbox();
|
||||
});
|
||||
|
||||
@@ -25,3 +25,13 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.sl-overlay {
|
||||
background: #000;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.sl-close {
|
||||
color: #FFF;
|
||||
}
|
||||
@@ -261,41 +261,40 @@ blocks:
|
||||
slug:
|
||||
type: slug
|
||||
uses: [ title ]
|
||||
selectfield:
|
||||
type: select
|
||||
values: [ foo, bar, baz ]
|
||||
selectfieldd:
|
||||
type: select
|
||||
values:
|
||||
foo: "Fooo"
|
||||
bar: Bario
|
||||
baz: Bazz
|
||||
multiselect:
|
||||
type: select
|
||||
values: [ A-tuin, Donatello, Rafael, Leonardo, Michelangelo, Koopa, Squirtle ]
|
||||
multiple: false
|
||||
postfix: "Select your favourite turtle(s)."
|
||||
|
||||
multiselect2:
|
||||
type: select
|
||||
values: [ A-tuin, Donatello, Rafael, Leonardo, Michelangelo, Koopa, Squirtle ]
|
||||
multiple: true
|
||||
required: true
|
||||
postfix: "Select your favourite turtle(s)."
|
||||
|
||||
|
||||
# content:
|
||||
# type: html
|
||||
# height: 150px
|
||||
# contentlink:
|
||||
# type: text
|
||||
# label: Link
|
||||
# placeholder: 'contenttype/slug or http://example.org/'
|
||||
# postfix: "Use this to add a link for this Block. This could either be an 'internal' link like <tt>page/about</tt>, if you use a contenttype/slug combination. Otherwise use a proper URL, like `http://example.org`."
|
||||
# image:
|
||||
# type: image
|
||||
# attrib: title
|
||||
# extensions: [ gif, jpg, png ]
|
||||
# selectfield:
|
||||
# type: select
|
||||
# values: [ foo, bar, baz ]
|
||||
# selectfieldd:
|
||||
# type: select
|
||||
# values:
|
||||
# foo: "Fooo"
|
||||
# bar: Bario
|
||||
# baz: Bazz
|
||||
# multiselect:
|
||||
# type: select
|
||||
# values: [ A-tuin, Donatello, Rafael, Leonardo, Michelangelo, Koopa, Squirtle ]
|
||||
# multiple: false
|
||||
# postfix: "Select your favourite turtle(s)."
|
||||
#
|
||||
# multiselect2:
|
||||
# type: select
|
||||
# values: [ A-tuin, Donatello, Rafael, Leonardo, Michelangelo, Koopa, Squirtle ]
|
||||
# multiple: true
|
||||
# required: true
|
||||
# postfix: "Select your favourite turtle(s)."
|
||||
content:
|
||||
type: html
|
||||
height: 150px
|
||||
contentlink:
|
||||
type: text
|
||||
label: Link
|
||||
placeholder: 'contenttype/slug or http://example.org/'
|
||||
postfix: "Use this to add a link for this Block. This could either be an 'internal' link like <tt>page/about</tt>, if you use a contenttype/slug combination. Otherwise use a proper URL, like `http://example.org`."
|
||||
image:
|
||||
type: image
|
||||
attrib: title
|
||||
extensions: [ gif, jpg, png ]
|
||||
show_on_dashboard: true
|
||||
viewless: true
|
||||
default_status: published
|
||||
|
||||
@@ -1 +1 @@
|
||||
nav.flex-column{background-color:#345}nav.flex-column hr{border-top-width:0;border-bottom:1px solid hsla(0,0%,100%,.2);margin:0}nav.flex-column .logo{color:#fff;background:#345;text-align:center;margin:1rem}nav.flex-column .logo h2{font-size:36px}nav.flex-column .nav-item{color:#ddd!important;padding-top:.6rem;padding-bottom:.6rem;text-align:left}nav.flex-column .nav-item a{color:#ddd!important}nav.flex-column .nav-item .fa-stack{height:2.3em;margin-right:.5rem}nav.flex-column .nav-item .fa-stack i:last-child{color:#444}nav.flex-column .nav-item>i.dropdown.icon{margin-top:10px}nav.flex-column .nav-item.separator{padding:1rem 1rem .5rem;color:hsla(0,0%,78%,.5)!important}nav.flex-column .nav-item.separator .fas{padding:0 1.1rem 0 .65rem}nav.flex-column .nav-item.active,nav.flex-column .nav-item.current{background-color:#41647f!important;color:#fff!important}nav.flex-column .nav-item.active>a,nav.flex-column .nav-item.current>a{color:#fff!important}nav.flex-column .dropdown-menu{transform:translateX(140px)!important;padding-bottom:0}nav.flex-column .dropdown-menu a{padding:.25rem .75rem}nav.flex-column .dropdown-menu .btn-group{width:100%;background-color:#eee;border-top:1px solid #ddd;margin-top:.5rem;display:flex}nav.flex-column .dropdown-menu .btn{background-color:#eee;border:0;flex:1;padding:.5rem 0}nav.flex-column .dropdown-menu .btn:hover{background-color:#ccc;border:0}.nav.btn-toolbar{margin:.5rem 1rem 0 3rem}.nav.btn-toolbar .nav-item{flex-grow:0;margin-right:.6rem}.nav.btn-toolbar .nav-item.topbar-title{font-family:Source Sans Pro,serif;font-size:22px;color:#222;overflow:hidden;text-overflow:ellipsis;text-align:left;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;flex-grow:1}.nav.btn-toolbar .btn-light{background-color:#eee;border:1px solid #d8d8d8}.status-published{color:#7fa800}.status-held{color:#ca2300}.status-timed{color:#a80}.status-draft{color:#0569e2}.card ul{padding-left:1.2rem}body,html{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:13px;line-height:1.5;height:100%;margin:0;padding:0}@media only screen and (min-width:1025px){body,html{font-size:14px}}@media only screen and (min-width:1281px){body,html{font-size:15px}}body{background:#f7f7f7}h1,h2,h3,h4,h5,h6{font-family:Source Sans Pro,serif;font-weight:400}h1{font-weight:700;font-size:2.5rem}h2{font-size:2rem;line-height:4rem}h3{font-size:1.75rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1rem}.wrapper{display:grid;width:100vw;height:100vh;grid-template-areas:"topbar" "sidebar" "content" "sidebar2" "footer"}@media only screen and (min-width:600px){.wrapper{grid-template-areas:"sidebar topbar topbar" "sidebar content aside" "sidebar footer footer"}.wrapper,.wrapper.has-widecontent{grid-template-columns:12.6rem auto 21rem;grid-template-rows:3.6rem auto 2rem}.wrapper.has-widecontent{grid-template-areas:"sidebar topbar topbar" "sidebar content content" "sidebar footer footer"}}#sidebar{grid-area:sidebar;background-color:#345;border-right:1px solid #233}header{grid-area:topbar;background-color:#fff;border-bottom:1px solid #ddd}#content,#vuecontent,#widecontent{grid-area:content;padding:2rem 3rem}#widecontent+aside{display:none}footer{grid-area:footer}aside{grid-area:aside;padding:2rem 2rem 2rem 0}.ui.basic.table{background-color:#fff!important}.ui.basic.table th{background-color:#f8f8f8}.ui.basic.table td{padding:.7em .4em}#editcontent input.large{font-size:140%;height:auto}#editcontent input[readonly]{background-color:#f2f2f2;color:#888}#editcontent .input-group-append .btn{font-weight:400;font-size:.9rem}.imageholder{width:100%;border-radius:5px;border:1px solid #ddd;background-color:#fff}.imageholder img{max-width:100%;display:block;margin:auto;border-radius:4px}.selectize-input>*{font-size:1rem;line-height:1.5rem}.clip-overflow{display:block;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;max-height:4.5rem}td.listing-excerpt{color:#333}td.listing-meta{font-size:.9rem;line-height:1.5rem}td.listing-meta span{color:#666;display:inline-block;white-space:nowrap}td.listing-thumb img{display:block;width:6.25rem;height:4.5rem}td.listing-actions{white-space:nowrap}td.listing-actions .btn-light{background-color:#e0e0e0;border:1px solid #d8d8d8;color:#444}td.listing-actions .show>.btn-light.dropdown-toggle{background-color:#ccc;border:1px solid #b8b8b8;color:#444}.selectize-dropdown-content,.selectize-input{font-size:1rem}
|
||||
nav.flex-column{background-color:#345}nav.flex-column hr{border-top-width:0;border-bottom:1px solid hsla(0,0%,100%,.2);margin:0}nav.flex-column .logo{color:#fff;background:#345;text-align:center;margin:1rem}nav.flex-column .logo h2{font-size:36px}nav.flex-column .nav-item{color:#ddd!important;padding-top:.6rem;padding-bottom:.6rem;text-align:left}nav.flex-column .nav-item a{color:#ddd!important}nav.flex-column .nav-item .fa-stack{height:2.3em;margin-right:.5rem}nav.flex-column .nav-item .fa-stack i:last-child{color:#444}nav.flex-column .nav-item>i.dropdown.icon{margin-top:10px}nav.flex-column .nav-item.separator{padding:1rem 1rem .5rem;color:hsla(0,0%,78%,.5)!important}nav.flex-column .nav-item.separator .fas{padding:0 1.1rem 0 .65rem}nav.flex-column .nav-item.active,nav.flex-column .nav-item.current{background-color:#41647f!important;color:#fff!important}nav.flex-column .nav-item.active>a,nav.flex-column .nav-item.current>a{color:#fff!important}nav.flex-column .dropdown-menu{transform:translateX(140px)!important;padding-bottom:0}nav.flex-column .dropdown-menu a{padding:.25rem .75rem}nav.flex-column .dropdown-menu .btn-group{width:100%;background-color:#eee;border-top:1px solid #ddd;margin-top:.5rem;display:flex}nav.flex-column .dropdown-menu .btn{background-color:#eee;border:0;flex:1;padding:.5rem 0}nav.flex-column .dropdown-menu .btn:hover{background-color:#ccc;border:0}.nav.btn-toolbar{margin:.5rem 1rem 0 3rem}.nav.btn-toolbar .nav-item{flex-grow:0;margin-right:.6rem}.nav.btn-toolbar .nav-item.topbar-title{font-family:Source Sans Pro,serif;font-size:22px;color:#222;overflow:hidden;text-overflow:ellipsis;text-align:left;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;flex-grow:1}.nav.btn-toolbar .btn-light{background-color:#eee;border:1px solid #d8d8d8}.status-published{color:#7fa800}.status-held{color:#ca2300}.status-timed{color:#a80}.status-draft{color:#0569e2}.card ul{padding-left:1.2rem}body,html{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:13px;line-height:1.5;height:100%;margin:0;padding:0}@media only screen and (min-width:1025px){body,html{font-size:14px}}@media only screen and (min-width:1281px){body,html{font-size:15px}}body{background:#f7f7f7}h1,h2,h3,h4,h5,h6{font-family:Source Sans Pro,serif;font-weight:400}h1{font-weight:700;font-size:2.5rem}h2{font-size:2rem;line-height:4rem}h3{font-size:1.75rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1rem}.wrapper{display:grid;width:100vw;height:100vh;grid-template-areas:"topbar" "sidebar" "content" "sidebar2" "footer"}@media only screen and (min-width:600px){.wrapper{grid-template-areas:"sidebar topbar topbar" "sidebar content aside" "sidebar footer footer"}.wrapper,.wrapper.has-widecontent{grid-template-columns:12.6rem auto 21rem;grid-template-rows:3.6rem auto 2rem}.wrapper.has-widecontent{grid-template-areas:"sidebar topbar topbar" "sidebar content content" "sidebar footer footer"}}#sidebar{grid-area:sidebar;background-color:#345;border-right:1px solid #233}header{grid-area:topbar;background-color:#fff;border-bottom:1px solid #ddd}#content,#vuecontent,#widecontent{grid-area:content;padding:2rem 3rem}#widecontent+aside{display:none}footer{grid-area:footer}aside{grid-area:aside;padding:2rem 2rem 2rem 0}.ui.basic.table{background-color:#fff!important}.ui.basic.table th{background-color:#f8f8f8}.ui.basic.table td{padding:.7em .4em}#editcontent input.large{font-size:140%;height:auto}#editcontent input[readonly]{background-color:#f2f2f2;color:#888}#editcontent .input-group-append .btn{font-weight:400;font-size:.9rem}.sl-overlay{background:#000;opacity:.85}.sl-close{color:#fff}.imageholder{width:100%;border-radius:5px;border:1px solid #ddd;background-color:#fff}.imageholder img{max-width:100%;display:block;margin:auto;border-radius:4px}.selectize-input>*{font-size:1rem;line-height:1.5rem}.clip-overflow{display:block;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;max-height:4.5rem}td.listing-excerpt{color:#333}td.listing-meta{font-size:.9rem;line-height:1.5rem}td.listing-meta span{color:#666;display:inline-block;white-space:nowrap}td.listing-thumb img{display:block;width:6.25rem;height:4.5rem}td.listing-actions{white-space:nowrap}td.listing-actions .btn-light{background-color:#e0e0e0;border:1px solid #d8d8d8;color:#444}td.listing-actions .show>.btn-light.dropdown-toggle{background-color:#ccc;border:1px solid #b8b8b8;color:#444}.selectize-dropdown-content,.selectize-input{font-size:1rem}
|
||||
File diff suppressed because one or more lines are too long
@@ -94,10 +94,11 @@ class Config
|
||||
/**
|
||||
* @param string $path
|
||||
* @param bool $absolute
|
||||
* @param mixed $additional
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPath(string $path, bool $absolute = true, string $additional = ''): string
|
||||
public function getPath(string $path, bool $absolute = true, $additional = null): string
|
||||
{
|
||||
return $this->pathResolver->resolve($path, $absolute, $additional);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Configuration;
|
||||
|
||||
use Exception;
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
use Webmozart\PathUtil\Path;
|
||||
|
||||
@@ -92,12 +93,13 @@ class PathResolver
|
||||
* - `foo/bar` - A relative path that will be resolved against the root path.
|
||||
* - `/tmp` - An absolute path will be returned as is.
|
||||
*
|
||||
* @param string $path the path
|
||||
* @param bool $absolute if the path is relative, resolve it against the root path
|
||||
* @param string $path the path
|
||||
* @param bool $absolute if the path is relative, resolve it against the root path
|
||||
* @param mixed $additional
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function resolve(string $path, bool $absolute = true, string $additional = ''): string
|
||||
public function resolve(string $path, bool $absolute = true, $additional = null): string
|
||||
{
|
||||
if (isset($this->paths[$path])) {
|
||||
$path = $this->paths[$path];
|
||||
@@ -129,8 +131,8 @@ class PathResolver
|
||||
$path = Path::makeAbsolute($path, $this->paths['root']);
|
||||
}
|
||||
|
||||
if ($additional !== '') {
|
||||
$path .= '/' . $additional;
|
||||
if (!empty($additional)) {
|
||||
$path .= \DIRECTORY_SEPARATOR . implode(\DIRECTORY_SEPARATOR, (array) $additional);
|
||||
}
|
||||
|
||||
// Make sure we don't have lingering unneeded dir-seperators
|
||||
|
||||
@@ -7,14 +7,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Content;
|
||||
|
||||
use Bolt\Common\Json;
|
||||
use Bolt\Configuration\Config;
|
||||
use Bolt\Entity\Media;
|
||||
use Bolt\Media\Item;
|
||||
use Bolt\Repository\MediaRepository;
|
||||
use Carbon\Carbon;
|
||||
use Cocur\Slugify\Slugify;
|
||||
use Faker\Factory;
|
||||
use PHPExif\Reader\Reader;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
use Webmozart\PathUtil\Path;
|
||||
|
||||
class MediaFactory
|
||||
{
|
||||
@@ -116,4 +121,56 @@ class MediaFactory
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Item $item
|
||||
* @param $params
|
||||
*
|
||||
* @return Media
|
||||
*/
|
||||
public function createFromUpload(Item $item, $params): Media
|
||||
{
|
||||
if (Json::test($params)) {
|
||||
$params = Json::parse($params);
|
||||
$addedPath = $params['path'];
|
||||
$area = $params['area'];
|
||||
} else {
|
||||
$addedPath = '';
|
||||
$area = 'files';
|
||||
}
|
||||
|
||||
$targetFilename = $addedPath . \DIRECTORY_SEPARATOR . $this->sanitiseFilename($item->getName());
|
||||
|
||||
$source = $this->config->getPath('cache', true, ['uploads', $item->getId(), $item->getName()]);
|
||||
$target = $this->config->getPath($area, true, $targetFilename);
|
||||
|
||||
$relPath = Path::getDirectory($targetFilename);
|
||||
$relName = Path::getFilename($targetFilename);
|
||||
|
||||
// Move the file over
|
||||
$fileSystem = new Filesystem();
|
||||
$fileSystem->rename($source, $target, true);
|
||||
|
||||
$file = new SplFileInfo($target, $relPath, $relName);
|
||||
|
||||
$media = $this->createOrUpdateMedia($file, $area);
|
||||
|
||||
return $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function sanitiseFilename(string $filename): string
|
||||
{
|
||||
$extensionSlug = new Slugify(['regexp' => '/([^a-z0-9]|-)+/']);
|
||||
$filenameSlug = new Slugify(['lowercase' => false]);
|
||||
|
||||
$extension = $extensionSlug->slugify(Path::getExtension($filename));
|
||||
$filename = $filenameSlug->slugify(Path::getFilenameWithoutExtension($filename));
|
||||
|
||||
return $filename . '.' . $extension;
|
||||
}
|
||||
}
|
||||
|
||||
58
src/Controller/Async/Uploader.php
Normal file
58
src/Controller/Async/Uploader.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Controller\Async;
|
||||
|
||||
use Bolt\Content\MediaFactory;
|
||||
use Bolt\Media\RequestHandler;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class Uploader
|
||||
{
|
||||
/** @var MediaFactory */
|
||||
private $mediaFactory;
|
||||
|
||||
/** @var RequestHandler */
|
||||
private $requestHandler;
|
||||
|
||||
/** @var ObjectManager */
|
||||
private $manager;
|
||||
|
||||
public function __construct(MediaFactory $mediaFactory, RequestHandler $requestHandler, ObjectManager $manager)
|
||||
{
|
||||
$this->mediaFactory = $mediaFactory;
|
||||
$this->requestHandler = $requestHandler;
|
||||
$this->manager = $manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/async/upload", name="bolt_upload_post", methods={"POST"})
|
||||
*/
|
||||
public function upload(Request $request)
|
||||
{
|
||||
// Get submitted field data item, will always be one item in case of async upload
|
||||
$items = $this->requestHandler->loadFilesByField('filepond');
|
||||
|
||||
// If no items, exit
|
||||
if (count($items) === 0) {
|
||||
// Something went wrong, most likely a field name mismatch
|
||||
http_response_code(400);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$params = $request->request->get('filepond', []);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$media = $this->mediaFactory->createFromUpload($item, current($params));
|
||||
$this->manager->persist($media);
|
||||
$this->manager->flush();
|
||||
}
|
||||
|
||||
return new Response($media->getPath() . $media->getFilename());
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,10 @@ class EditRecordController extends BaseController
|
||||
* @param Request $request
|
||||
* @param Content|null $content
|
||||
*
|
||||
* @throws \Twig_Error_Loader
|
||||
* @throws \Twig_Error_Runtime
|
||||
* @throws \Twig_Error_Syntax
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function edit(string $id, Request $request, Content $content = null): Response
|
||||
|
||||
71
src/Media/Item.php
Normal file
71
src/Media/Item.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Media;
|
||||
|
||||
/**
|
||||
* A wrapper class for easier access to $_FILES object.
|
||||
*/
|
||||
class Item
|
||||
{
|
||||
// counter that helps in ensuring each file receives a truly unique id
|
||||
public static $item_counter = 0;
|
||||
|
||||
// item props
|
||||
private $id;
|
||||
private $file;
|
||||
private $name;
|
||||
|
||||
public function __construct($file, $id = null)
|
||||
{
|
||||
$this->id = isset($id) ? $id : md5(uniqid(self::$item_counter++, true));
|
||||
$this->file = $file;
|
||||
$this->name = $file['name'];
|
||||
}
|
||||
|
||||
public function rename($name, $extension = null)
|
||||
{
|
||||
$info = pathinfo($this->name);
|
||||
$this->name = $name . '.' . (isset($extension) ? $extension : $info['extension']);
|
||||
}
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getFilename()
|
||||
{
|
||||
return $this->file['tmp_name'];
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return basename($this->name);
|
||||
}
|
||||
|
||||
public function getNameWithoutExtension()
|
||||
{
|
||||
$info = pathinfo($this->name);
|
||||
|
||||
return $info['filename'];
|
||||
}
|
||||
|
||||
public function getExtension()
|
||||
{
|
||||
$info = pathinfo($this->name);
|
||||
|
||||
return $info['extension'];
|
||||
}
|
||||
|
||||
public function getSize()
|
||||
{
|
||||
return $this->file['size'];
|
||||
}
|
||||
|
||||
public function getType()
|
||||
{
|
||||
return $this->file['mime'];
|
||||
}
|
||||
}
|
||||
469
src/Media/RequestHandler.php
Normal file
469
src/Media/RequestHandler.php
Normal file
@@ -0,0 +1,469 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Media;
|
||||
|
||||
use Bolt\Configuration\Config;
|
||||
|
||||
/**
|
||||
* FilePond RequestHandler helper class.
|
||||
*/
|
||||
|
||||
/*
|
||||
1. get files (from $files and $post)
|
||||
2. store files in tmp/ directory and give them a unique server id
|
||||
3. return server id's to client
|
||||
4. either client reverts upload or finalizes form
|
||||
5. call revert($server_id) to remove file from tmp/ directory
|
||||
6. call save() to save file to final directory
|
||||
*/
|
||||
class RequestHandler
|
||||
{
|
||||
// the default location to save tmp files to
|
||||
private $tmp_dir;
|
||||
|
||||
// regex to use for testing if a string is a file id
|
||||
private $file_id_format = '/^[0-9a-fA-F]{32}$/';
|
||||
|
||||
/** @var Config */
|
||||
private $config;
|
||||
|
||||
public function __construct(Config $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
|
||||
$this->tmp_dir = $this->config->getPath('cache', true, ['uploads']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $str
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFileId($str)
|
||||
{
|
||||
return preg_match($this->file_id_format, $str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $str
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isURL($str)
|
||||
{
|
||||
return filter_var($str, FILTER_VALIDATE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch all exceptions so we can return a 500 error when the server bugs out.
|
||||
*/
|
||||
public function catchExceptions()
|
||||
{
|
||||
set_exception_handler('FilePond\RequestHandler::handleException');
|
||||
}
|
||||
|
||||
public function handleException($ex)
|
||||
{
|
||||
// write to error log so we can still find out what's up
|
||||
error_log('Uncaught exception in class="' . get_class($ex) . '" message="' . $ex->getMessage() . '" line="' . $ex->getLine() . '"');
|
||||
|
||||
// clean up buffer
|
||||
ob_end_clean();
|
||||
|
||||
// server error mode go!
|
||||
http_response_code(500);
|
||||
}
|
||||
|
||||
private function createItem($file, $id = null)
|
||||
{
|
||||
return new Item($file, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fieldName
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function loadFilesByField($fieldName)
|
||||
{
|
||||
// See if files are posted as JSON string (each file being base64 encoded)
|
||||
$base64Items = $this->loadBase64FormattedFiles($fieldName);
|
||||
|
||||
// retrieves posted file objects
|
||||
$fileItems = $this->loadFileObjects($fieldName);
|
||||
|
||||
// retrieves files already on server
|
||||
$tmpItems = $this->loadFilesFromTemp($fieldName);
|
||||
|
||||
// save newly received files to temp files folder (tmp items already are in that folder)
|
||||
$this->saveAsTempFiles(array_merge($base64Items, $fileItems));
|
||||
|
||||
// return items
|
||||
return array_merge($base64Items, $fileItems, $tmpItems);
|
||||
}
|
||||
|
||||
private function loadFileObjects($fieldName)
|
||||
{
|
||||
$items = [];
|
||||
|
||||
if (!isset($_FILES[$fieldName])) {
|
||||
return $items;
|
||||
}
|
||||
|
||||
$FILE = $_FILES[$fieldName];
|
||||
|
||||
if (is_array($FILE['tmp_name'])) {
|
||||
foreach ($FILE['tmp_name'] as $index => $tmpName) {
|
||||
array_push($items, $this->createItem([
|
||||
'tmp_name' => $FILE['tmp_name'][$index],
|
||||
'name' => $FILE['name'][$index],
|
||||
'size' => $FILE['size'][$index],
|
||||
'error' => $FILE['error'][$index],
|
||||
'type' => $FILE['type'][$index],
|
||||
]));
|
||||
}
|
||||
} else {
|
||||
array_push($items, $this->createItem($FILE));
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function loadBase64FormattedFiles($fieldName)
|
||||
{
|
||||
/*
|
||||
// format:
|
||||
{
|
||||
"id": "iuhv2cpsu",
|
||||
"name": "picture.jpg",
|
||||
"type": "image/jpeg",
|
||||
"size": 20636,
|
||||
"metadata" : {...}
|
||||
"data": "/9j/4AAQSkZJRgABAQEASABIAA..."
|
||||
}
|
||||
*/
|
||||
|
||||
$items = [];
|
||||
|
||||
if (!isset($_POST[$fieldName])) {
|
||||
return $items;
|
||||
}
|
||||
|
||||
// Handle posted files array
|
||||
$values = $_POST[$fieldName];
|
||||
|
||||
// Turn values in array if is submitted as single value
|
||||
if (!is_array($values)) {
|
||||
$values = isset($values) ? [$values] : [];
|
||||
}
|
||||
|
||||
// If files are found, turn base64 strings into actual file objects
|
||||
foreach ($values as $value) {
|
||||
// suppress error messages, we'll just investigate the object later
|
||||
$obj = @json_decode($value);
|
||||
|
||||
// skip values that failed to be decoded
|
||||
if (!isset($obj)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// test if this is a file object (matches the object described above)
|
||||
if (!$this->isEncodedFile($obj)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
array_push($items, $this->createItem($this->createTempFile($obj)));
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function isEncodedFile($obj)
|
||||
{
|
||||
return isset($obj->id) && isset($obj->data) && isset($obj->name) && isset($obj->type) && isset($obj->size);
|
||||
}
|
||||
|
||||
private function loadFilesFromTemp($fieldName)
|
||||
{
|
||||
$items = [];
|
||||
|
||||
if (!isset($_POST[$fieldName])) {
|
||||
return $items;
|
||||
}
|
||||
|
||||
// Handle posted ids array
|
||||
$values = $_POST[$fieldName];
|
||||
|
||||
// Turn values in array if is submitted as single value
|
||||
if (!is_array($values)) {
|
||||
$values = isset($values) ? [$values] : [];
|
||||
}
|
||||
|
||||
// test if value is actually a file id
|
||||
foreach ($values as $value) {
|
||||
if ($this->isFileId($value)) {
|
||||
array_push($items, $this->createItem($this->getTempFile($value), $value));
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function save($items, $path = 'uploads' . \DIRECTORY_SEPARATOR)
|
||||
{
|
||||
// is list of files
|
||||
if (is_array($items)) {
|
||||
$results = [];
|
||||
foreach ($items as $item) {
|
||||
array_push($results, $this->saveFile($item, $path));
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
// is single item
|
||||
return $this->saveFile($items, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $file_id
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteTempFile($file_id)
|
||||
{
|
||||
return $this->deleteTempDirectory($file_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $url
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
public function getRemoteURLData($url)
|
||||
{
|
||||
try {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
$content = curl_exec($ch);
|
||||
if ($content === false) {
|
||||
throw new Exception(curl_error($ch), curl_errno($ch));
|
||||
}
|
||||
|
||||
$type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
$length = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
$success = $code >= 200 && $code < 300;
|
||||
|
||||
return [
|
||||
'code' => $code,
|
||||
'content' => $content,
|
||||
'type' => $type,
|
||||
'length' => $length,
|
||||
'success' => $success,
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function saveAsTempFiles($items)
|
||||
{
|
||||
foreach ($items as $item) {
|
||||
$this->saveTempFile($item);
|
||||
}
|
||||
}
|
||||
|
||||
private function saveTempFile($file)
|
||||
{
|
||||
// make sure path name is safe
|
||||
$path = $this->getSecureTempPath() . $file->getId() . \DIRECTORY_SEPARATOR;
|
||||
|
||||
// Creates a secure temporary directory to store the files in
|
||||
$this->createSecureDirectory($path);
|
||||
|
||||
// get source and target values
|
||||
$source = $file->getFilename();
|
||||
$target = $path . $file->getName();
|
||||
|
||||
// Move uploaded file to this new secure directory
|
||||
$result = $this->moveFile($source, $target);
|
||||
|
||||
// Was not saved
|
||||
if ($result !== true) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Make sure file is secure
|
||||
$this->setSecureFilePermissions($target);
|
||||
|
||||
// temp file stored successfully
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getTempFile($fileId)
|
||||
{
|
||||
// select all files in directory except .htaccess
|
||||
foreach (glob($this->getSecureTempPath() . $fileId . \DIRECTORY_SEPARATOR . '*.*') as $file) {
|
||||
try {
|
||||
$handle = fopen($file, 'rb');
|
||||
$content = fread($handle, filesize($file));
|
||||
fclose($handle);
|
||||
|
||||
return [
|
||||
'name' => basename($file),
|
||||
'content' => $content,
|
||||
'type' => mime_content_type($file),
|
||||
'length' => filesize($file),
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getFile($file, $path)
|
||||
{
|
||||
try {
|
||||
$filename = $path . \DIRECTORY_SEPARATOR . $file;
|
||||
$handle = fopen($filename, 'rb');
|
||||
$content = fread($handle, filesize($filename));
|
||||
fclose($handle);
|
||||
|
||||
return [
|
||||
'name' => basename($filename),
|
||||
'content' => $content,
|
||||
'type' => mime_content_type($filename),
|
||||
'length' => filesize($filename),
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function saveFile($item, $path)
|
||||
{
|
||||
// nope
|
||||
if (!isset($item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if is file id
|
||||
if (is_string($item)) {
|
||||
return $this->moveFileById($item, $path);
|
||||
}
|
||||
|
||||
// is file object
|
||||
|
||||
return $this->moveFileById($item->getId(), $path, $item->getName());
|
||||
}
|
||||
|
||||
private function moveFileById($fileId, $path, $fileName = null)
|
||||
{
|
||||
// select all files in directory except .htaccess
|
||||
foreach (glob($this->getSecureTempPath() . $fileId . \DIRECTORY_SEPARATOR . '*.*') as $file) {
|
||||
$source = $file;
|
||||
$target = $this->getSecurePath($path);
|
||||
|
||||
$this->createDirectory($target);
|
||||
|
||||
rename($source, $target . (isset($fileName) ? basename($fileName) : basename($file)));
|
||||
}
|
||||
|
||||
// remove directory
|
||||
$this->deleteTempDirectory($fileId);
|
||||
|
||||
// done!
|
||||
return true;
|
||||
}
|
||||
|
||||
private function deleteTempDirectory($id)
|
||||
{
|
||||
@array_map('unlink', glob($this->getSecureTempPath() . $id . \DIRECTORY_SEPARATOR . '{.,}*', GLOB_BRACE));
|
||||
|
||||
// remove temp directory
|
||||
@rmdir($this->getSecureTempPath() . $id);
|
||||
}
|
||||
|
||||
private function createTempFile($file)
|
||||
{
|
||||
$tmp = tmpfile();
|
||||
fwrite($tmp, base64_decode($file->data, true));
|
||||
$meta = stream_get_meta_data($tmp);
|
||||
$filename = $meta['uri'];
|
||||
|
||||
return [
|
||||
'error' => 0,
|
||||
'size' => filesize($filename),
|
||||
'type' => $file->type,
|
||||
'name' => $file->name,
|
||||
'tmp_name' => $filename,
|
||||
'tmp' => $tmp,
|
||||
];
|
||||
}
|
||||
|
||||
private function moveFile($source, $target)
|
||||
{
|
||||
if (is_uploaded_file($source)) {
|
||||
return move_uploaded_file($source, $target);
|
||||
}
|
||||
|
||||
$tmp = fopen($source, 'rb');
|
||||
$result = file_put_contents($target, fread($tmp, filesize($source)));
|
||||
fclose($tmp);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getSecurePath($path)
|
||||
{
|
||||
return pathinfo($path)['dirname'] . \DIRECTORY_SEPARATOR . basename($path) . \DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
private function getSecureTempPath()
|
||||
{
|
||||
return $this->getSecurePath($this->tmp_dir);
|
||||
}
|
||||
|
||||
private function setSecureFilePermissions($target)
|
||||
{
|
||||
$stat = stat(dirname($target));
|
||||
$perms = $stat['mode'] & 0000666;
|
||||
@chmod($target, $perms);
|
||||
}
|
||||
|
||||
private function createDirectory($path)
|
||||
{
|
||||
if (is_dir($path)) {
|
||||
return false;
|
||||
}
|
||||
mkdir($path, 0755, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createSecureDirectory($path)
|
||||
{
|
||||
// !! If directory already exists we assume security is handled !!
|
||||
|
||||
// Test if directory already exists and correct
|
||||
if ($this->createDirectory($path)) {
|
||||
// Add .htaccess file for security purposes
|
||||
$content = '# Don\'t list directory contents
|
||||
IndexIgnore *
|
||||
# Disable script execution
|
||||
AddHandler cgi-script .php .pl .jsp .asp .sh .cgi
|
||||
Options -ExecCGI -Indexes';
|
||||
file_put_contents($path . '.htaccess', $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/simplelightbox/1.14.0/simplelightbox.min.css" integrity="sha256-SKS4FkcNkeoVSuM7plVb7urk85SbYbpu13eMQa0XgcA=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ asset('assets/bolt.css') }}">
|
||||
{% endblock %}
|
||||
</head>
|
||||
@@ -48,6 +49,7 @@
|
||||
<script src="{{ asset('assets/bolt.js') }}"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplelightbox/1.14.0/simple-lightbox.min.js" integrity="sha256-QKuCZaHT3kehJCIS5bXUrC6ciUWfswFF7ylYYPWQ/9w=" crossorigin="anonymous"></script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
|
||||
@@ -3,13 +3,59 @@
|
||||
{% block field %}
|
||||
<label>{{ label }}</label>
|
||||
|
||||
<div class="inline field">
|
||||
<label>Filename</label>
|
||||
<input name="{{ name }}[filename]" placeholder="First Name" type="text" value="{{ field.get('filename') }}" class="{{ class }}" {{ attributes|default()|raw }}>
|
||||
<div class="row" style="margin-right: 0;">
|
||||
<div class="col-8">
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-3">
|
||||
<label>Filename</label>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<input name="{{ name }}[filename]" id="{{ id }}-filename" placeholder="filename.jpg" type="text" value="{{ field.get('filename') }}" class="{{ class }}" {{ attributes|default()|raw }}>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-3">
|
||||
<label>Alt</label>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<input name="{{ name }}[alt]" placeholder="An image depicting a kitten" type="text" value="{{ field.get('alt') }}" class="{{ class }}" {{ attributes|default()|raw }}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-3">
|
||||
<label>Title</label>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<input name="{{ name }}[alt]" placeholder="A description of this image" type="text" value="{{ field.get('title') }}" class="{{ class }}" {{ attributes|default()|raw }}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-12" style="text-align: right;">
|
||||
<a href="{{ field.get('filename')|thumbnail(width=1000, height=1000, area='files') }}" class="btn btn-secondary btn-small lightbox" style="color: #FFF;">view image</a>
|
||||
<a href="" onclick="document.querySelector('.filepond--drop-label label').click(); return false;" class="btn btn-secondary btn-small" style="color: #FFF;">Upload an image</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-4" id="{{ id }}-uploader" style="padding: 0; min-height: 180px;">
|
||||
<file-pond name="filepond[]" ref="pond" accepted-file-types="image/jpeg, image/png" server="/async/upload"></file-pond>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<label>Alt</label>
|
||||
<input name="{{ name }}[alt]" placeholder="First Name" type="text" value="{{ field.get('alt') }}" class="{{ class }}" {{ attributes|default()|raw }}>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<style>
|
||||
#{{ id }}-uploader {
|
||||
background-image: url({{ field.get('filename')|thumbnail(width=400, height=300, area='files') }});
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
.filepond--root {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,18 +7,19 @@
|
||||
<span class="input-group-text">{{ field.slugPrefix }}</span>
|
||||
</div>
|
||||
<input name="{{ name }}" placeholder="…" type="text" value="{{ value }}" class="{{ class }}" readonly="readonly">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fas fa-unlock"></i> Locked
|
||||
<div class="input-group-append" id="{{ id }}-dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<i class="fas fa-unlock fa-fw"></i> Locked
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item">
|
||||
<a class="dropdown-item" data-action="lock">
|
||||
<i class="fas fa-unlock fa-fw"></i> Locked
|
||||
</a>
|
||||
<a class="dropdown-item">
|
||||
<a class="dropdown-item" data-action="generate">
|
||||
<i class="fas fa-link fa-fw"></i> Generate from: {{ field.slugUseFields|join(', ') }}
|
||||
</a>
|
||||
<a class="dropdown-item">
|
||||
<a class="dropdown-item" data-action="edit">
|
||||
<i class="fas fa-pencil-alt fa-fw"></i> Edit
|
||||
</a>
|
||||
</div>
|
||||
|
||||
35
templates/editcontent/javascripts/image.twig
Normal file
35
templates/editcontent/javascripts/image.twig
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends '@bolt/editcontent/javascripts/_base.twig' %}
|
||||
|
||||
|
||||
{% block javascripts %}
|
||||
<script>
|
||||
FilePond.registerPlugin(
|
||||
FilePondPluginFileMetadata,
|
||||
FilePondPluginImagePreview
|
||||
);
|
||||
FilePond.setOptions({
|
||||
fileMetadataObject: {
|
||||
'area': 'files',
|
||||
'path': ''
|
||||
}
|
||||
});
|
||||
|
||||
new Vue({
|
||||
el: '#{{ id}}-uploader',
|
||||
components: {
|
||||
FilePond: vueFilePond.default()
|
||||
}
|
||||
});
|
||||
|
||||
const pond = document.querySelector('.filepond--root');
|
||||
pond.addEventListener('FilePond:processfile', e => {
|
||||
document.querySelector("#{{ id }}-filename").value = e.detail.file.serverId;
|
||||
});
|
||||
pond.addEventListener('FilePond:addfile', e => {
|
||||
document.querySelector('.filepond--wrapper').style.opacity = "0.5 !important";
|
||||
console.log('foooo');
|
||||
document.querySelector("#{{ id }}-uploader").style.background = 'none';
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
5
templates/editcontent/javascripts/image_include.twig
Normal file
5
templates/editcontent/javascripts/image_include.twig
Normal file
@@ -0,0 +1,5 @@
|
||||
<script src="https://unpkg.com/filepond-plugin-image-preview"></script>
|
||||
<script src="https://unpkg.com/filepond-plugin-file-metadata/dist/filepond-plugin-file-metadata.js"></script>
|
||||
<script src="https://unpkg.com/filepond"></script>
|
||||
<script src="https://unpkg.com/vue"></script>
|
||||
<script src="https://unpkg.com/vue-filepond"></script>
|
||||
64
templates/editcontent/javascripts/slug.twig
Normal file
64
templates/editcontent/javascripts/slug.twig
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends '@bolt/editcontent/javascripts/_base.twig' %}
|
||||
|
||||
{% block javascripts %}
|
||||
<script>
|
||||
var input = document.querySelector("input[name=\"{{ name }}\"]");
|
||||
var slugTick;
|
||||
var element = document.querySelector('#{{ id }}-dropdown');
|
||||
|
||||
element.addEventListener('click', function (event) {
|
||||
if (action = event.target.getAttribute('data-action')) {
|
||||
var target = event.target;
|
||||
} else if (action = event.target.parentElement.getAttribute('data-action')) {
|
||||
var target = event.target.parentElement;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
document.querySelector('#{{ id }}-dropdown button').innerHTML = target.innerHTML;
|
||||
|
||||
clearTimeout(slugTick);
|
||||
|
||||
if (action == 'lock') {
|
||||
input.readOnly = true;
|
||||
}
|
||||
if (action == 'generate') {
|
||||
input.readOnly = true;
|
||||
|
||||
var fromfields = ["{{ field.slugUseFields|join("', '") }}"];
|
||||
updateSlug(fromfields, input);
|
||||
}
|
||||
if (action == 'edit') {
|
||||
input.readOnly = false;
|
||||
input.select();
|
||||
}
|
||||
|
||||
}, false);
|
||||
|
||||
function updateSlug(fromfields, input) {
|
||||
var newSlug = '';
|
||||
fromfields.forEach(function (field) {
|
||||
newSlug = newSlug + document.querySelector("input[name=\"fields[" + field + "]\"]").value;
|
||||
});
|
||||
input.value = slugify(newSlug);
|
||||
slugTick = setTimeout(function() {
|
||||
updateSlug(fromfields, input);
|
||||
}, 400);
|
||||
}
|
||||
|
||||
function slugify(text)
|
||||
{
|
||||
return text.toString().toLowerCase()
|
||||
.replace(/\s+/g, '-') // Replace spaces with -
|
||||
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
|
||||
.replace(/\-\-+/g, '-') // Replace multiple - with single -
|
||||
.replace(/^-+/, '') // Trim - from start of text
|
||||
.replace(/-+$/, ''); // Trim - from end of text
|
||||
}
|
||||
|
||||
// If slug is empty, "click" the 'generate from' option.
|
||||
if (input.value == '') {
|
||||
document.querySelector("a[data-action=\"generate\"]").click();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
2
templates/editcontent/stylesheets/image_include.twig
Normal file
2
templates/editcontent/stylesheets/image_include.twig
Normal file
@@ -0,0 +1,2 @@
|
||||
<link rel="stylesheet" href="https://unpkg.com/filepond/dist/filepond.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css">
|
||||
@@ -24,7 +24,7 @@
|
||||
{% set dimensions = '' %}
|
||||
|
||||
{% if extension in imageformats %}
|
||||
{% set thumbnail = filename|thumbnail(100, 72) %}
|
||||
{% set thumbnail = filename|thumbnail(width = 100, height = 72, area = area) %}
|
||||
{% set icon = 'fa-image' %}
|
||||
{% set link = path('bolt_media_new', {'area': area, 'file': filename}) %}
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
</td>
|
||||
<td class="listing-thumb" style="padding: 0.2em;">
|
||||
{%- if thumbnail -%}
|
||||
<a href="{{ filename|thumbnail(width = 1000, height = 1000, area=area) }}" class="lightbox">
|
||||
<img src="{{ thumbnail }}" width="100" height="72">
|
||||
</a>
|
||||
{%- else -%}
|
||||
|
||||
{%- endif -%}
|
||||
|
||||
40
templates/finder/_uploader.twig
Normal file
40
templates/finder/_uploader.twig
Normal file
@@ -0,0 +1,40 @@
|
||||
<link rel="stylesheet" href="https://unpkg.com/filepond/dist/filepond.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css">
|
||||
|
||||
<script src="https://unpkg.com/filepond-plugin-image-preview"></script>
|
||||
<script src="https://unpkg.com/filepond-plugin-file-metadata/dist/filepond-plugin-file-metadata.js"></script>
|
||||
<script src="https://unpkg.com/filepond"></script>
|
||||
<script src="https://unpkg.com/vue"></script>
|
||||
<script src="https://unpkg.com/vue-filepond"></script>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">File uploader</div>
|
||||
<div id="uploader">
|
||||
<file-pond name="filepond[]"
|
||||
ref="pond"
|
||||
allow-multiple="true"
|
||||
accepted-file-types="image/jpeg, image/png"
|
||||
server="/async/upload"></file-pond>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
FilePond.registerPlugin(
|
||||
FilePondPluginFileMetadata,
|
||||
FilePondPluginImagePreview
|
||||
);
|
||||
FilePond.setOptions({
|
||||
fileMetadataObject: {
|
||||
'area': '{{ area }}',
|
||||
'path': '{{ path }}'
|
||||
}
|
||||
});
|
||||
|
||||
new Vue({
|
||||
el: '#uploader',
|
||||
components: {
|
||||
FilePond: vueFilePond.default()
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -18,13 +18,16 @@
|
||||
|
||||
{% block aside %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">Meta information</div>
|
||||
<div class="card-body">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include '@bolt/finder/_uploader.twig' %}
|
||||
|
||||
|
||||
|
||||
{% endblock aside %}
|
||||
|
||||
@@ -32,6 +35,11 @@
|
||||
{{ parent() }}
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.6/css/selectize.min.css" integrity="sha256-EhmqrzYSImS7269rfDxk4H+AHDyu/KwV1d8FDgIXScI=" crossorigin="anonymous" />
|
||||
<style>
|
||||
.filepond--root {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user