1
0
mirror of https://github.com/typemill/typemill.git synced 2025-10-16 23:28:52 +02:00

Version 1.2.2: Draft Management

This commit is contained in:
Sebastian
2018-07-24 10:43:34 +02:00
parent 252446e3e3
commit c38783c4f6
26 changed files with 1059 additions and 344 deletions

View File

@@ -0,0 +1,232 @@
<?php
namespace Typemill\Controllers;
use Slim\Http\Request;
use Slim\Http\Response;
use Typemill\Extensions\ParsedownExtension;
class ContentApiController extends ContentController
{
public function publishArticle(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
$this->uri = $request->getUri();
# validate input
if(!$this->validateEditorInput()){ return $response->withJson($this->errors,422); }
# set structure
if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); }
# set item
if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
# set the status for published and drafted
$this->setPublishStatus();
# set path for the file (or folder)
$this->setItemPath('md');
# merge title with content for complete markdown document
$updatedContent = '# ' . $this->params['title'] . "\r\n\r\n" . $this->params['content'];
# update the file
if($this->write->writeFile($this->settings['contentFolder'], $this->path, $updatedContent))
{
# update the file
$delete = $this->deleteContentFiles(['txt']);
# update the structure
$this->setStructure($draft = false, $cache = false);
return $response->withJson(['success'], 200);
}
else
{
return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404);
}
}
public function unpublishArticle(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
$this->uri = $request->getUri();
# set structure
if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); }
# set item
if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
# set the status for published and drafted
$this->setPublishStatus();
# check if draft exists, if not, create one.
if(!$this->item->drafted)
{
# set path for the file (or folder)
$this->setItemPath('md');
# set content of markdown-file
if(!$this->setContent()){ return $response->withJson($this->errors, 404); }
# initialize parsedown extension
$parsedown = new ParsedownExtension();
# turn markdown into an array of markdown-blocks
$contentArray = $parsedown->markdownToArrayBlocks($this->content);
# encode the content into json
$contentJson = json_encode($contentArray);
# set path for the file (or folder)
$this->setItemPath('txt');
/* update the file */
if(!$this->write->writeFile($this->settings['contentFolder'], $this->path, $contentJson))
{
return $response->withJson(['errors' => ['message' => 'Could not create a draft of the page. Please check if the folder is writable']], 404);
}
}
# update the file
$delete = $this->deleteContentFiles(['md']);
if($delete)
{
# update the live structure
$this->setStructure($draft = false, $cache = false);
return $response->withJson(['success'], 200);
}
else
{
return $response->withJson(['errors' => ['message' => "Could not delete some files. Please check if the files exists and are writable"]], 404);
}
}
public function deleteArticle(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
$this->uri = $request->getUri();
# set structure
if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); }
# set item
if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
# update the file
$delete = $this->deleteContentFiles(['md','txt']);
if($delete)
{
# update the live structure
$this->setStructure($draft = false, $cache = false);
#update the backend structure
$this->setStructure($draft = true, $cache = false);
return $response->withJson(['success'], 200);
}
else
{
return $response->withJson(['errors' => ['message' => "Could not delete some files. Please check if the files exists and are writable"]], 404);
}
}
public function updateArticle(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
$this->uri = $request->getUri();
# validate input
if(!$this->validateEditorInput()){ return $response->withJson($this->errors,422); }
# set structure
if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); }
# set item
if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
# set path for the file (or folder)
$this->setItemPath('txt');
# merge title with content for complete markdown document
$updatedContent = '# ' . $this->params['title'] . "\r\n\r\n" . $this->params['content'];
# initialize parsedown extension
$parsedown = new ParsedownExtension();
# turn markdown into an array of markdown-blocks
$contentArray = $parsedown->markdownToArrayBlocks($updatedContent);
# encode the content into json
$contentJson = json_encode($contentArray);
/* update the file */
if($this->write->writeFile($this->settings['contentFolder'], $this->path, $contentJson))
{
return $response->withJson(['success'], 200);
}
else
{
return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404);
}
}
public function createBlock(Request $request, Response $response, $args)
{
/* get params from call */
$this->params = $request->getParams();
$this->uri = $request->getUri();
/* validate input */
if(!$this->validateInput()){ return $response->withJson($this->errors,422); }
/* set structure */
if(!$this->setStructure()){ return $response->withJson($this->errors, 404); }
/* set item */
if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
/* set path */
$this->setItemPath();
/* get markdown-file */
if(!$this->setMarkdownFile()){ return $response->withJson($this->errors, 404); }
/* get txt-file with content array */
$contentArray = NULL;
/*
create a txt-file with parsedown-array.
you will have .md and .txt file.
scan folder with option to show drafts.
but what is with structure? We use the cached structure, do not forget!!!
if there is a draft, replace the md file with txt-file.
display content: you have to check if md or txt. if txt, then directly open the txt-file.
in here set markdown-file or
set txt-file.
if publish, render txt-content, replace markdown-file, delete txt-file
*/
/* initialize pagedown */
/* turn input into array */
/* add input to contentArray */
/* store updated contentArray */
/* transform input to html */
/* send html to client */
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Typemill\Controllers;
use Slim\Http\Request;
use Slim\Http\Response;
use Slim\Views\Twig;
use Typemill\Extensions\ParsedownExtension;
class ContentBackendController extends ContentController
{
/**
* Show Content
*
* @param obj $request the slim request object
* @param obj $response the slim response object
* @return obje $response with redirect to route
*/
public function showContent(Request $request, Response $response, $args)
{
# get params from call
$this->uri = $request->getUri();
$this->params = isset($args['params']) ? ['url' => $this->uri->getBasePath() . '/' . $args['params']] : ['url' => $this->uri->getBasePath()];
# set structure
if(!$this->setStructure($draft = true)){ die('no structure'); return $this->render404($response, array( 'navigation' => true, 'content' => $this->errors )); }
# set item
if(!$this->setItem()){ die('no item'); return $this->render404($response, array( 'navigation' => $this->structure, 'settings' => $this->settings, 'content' => $this->errors )); }
# set the status for published and drafted
$this->setPublishStatus();
# set path
$this->setItemPath($this->item->fileType);
# add the modified date for the file
$this->item->modified = ($this->item->published OR $this->item->drafted) ? filemtime($this->settings['contentFolder'] . $this->path) : false;
# read content from file
if(!$this->setContent()){ return $this->render404($response, array( 'navigation' => $this->structure, 'settings' => $this->settings, 'content' => $this->errors )); }
$content = $this->content;
$title = false;
# if content is an array, then it is a draft
if(is_array($content))
{
# transform array to markdown
$parsedown = new ParsedownExtension();
$content = $parsedown->arrayBlocksToMarkdown($content);
}
# if there is content
if($content != '')
{
# normalize linebreaks
$content = str_replace(array("\r\n", "\r"), "\n", $content);
$content = trim($content, "\n");
# and strip out title
if($content[0] == '#')
{
$contentParts = explode("\n", $content, 2);
$title = trim($contentParts[0], "# \t\n\r\0\x0B");
$content = trim($contentParts[1]);
}
}
return $this->render($response, 'content/content.twig', array('navigation' => $this->structure, 'title' => $title, 'content' => $content, 'item' => $this->item, 'settings' => $this->settings ));
}
}

View File

@@ -2,216 +2,243 @@
namespace Typemill\Controllers;
use Slim\Views\Twig;
use Slim\Http\Request;
use Slim\Http\Response;
use Interop\Container\ContainerInterface;
use Typemill\Models\Validation;
use Typemill\Models\Folder;
use Typemill\Models\Write;
use Typemill\Models\WriteYaml;
use Typemill\Models\WriteCache;
use \Symfony\Component\Yaml\Yaml;
use Typemill\Models\Helpers;
use Typemill\Extensions\ParsedownExtension;
use \Parsedown;
class ContentController extends Controller
abstract class ContentController
{
# holds the pimple container
protected $c;
/**
* Show Content
*
* @param obj $request the slim request object
* @param obj $response the slim response object
* @return obje $response with redirect to route
*/
# holds the params from request
protected $params;
# holds the slim-uri-object
protected $uri;
public function showContent(Request $request, Response $response, $args)
# holds the errors to output in frontend
protected $errors;
# holds a write object to write files
protected $write;
# holds the structure of content folder as a serialized array of objects
protected $structure;
# holds the name of the structure-file with drafts for author environment
protected $structureDraftName;
# holds the name of the structure-file without drafts for live site
protected $structureLiveName;
# hold the page-item as an object
protected $item;
# holds the path to the requested file
protected $path = false;
# holds the content of the page
protected $content;
public function __construct(ContainerInterface $c)
{
$settings = $this->c->get('settings');
$pathToContent = $settings['rootPath'] . $settings['contentFolder'];
$uri = $request->getUri();
/* scan the content of the folder */
$structure = Folder::scanFolder($pathToContent);
/* if there is no content, render an empty page */
if(count($structure) == 0)
{
return $this->render($response, 'content/content.twig', array( 'navigation' => true, 'content' => 'Nothing found in content folder.' ));
}
/* create an array of object with the whole content of the folder */
$structure = Folder::getFolderContentDetails($structure, $uri->getBaseUrl(), $uri->getBasePath());
/* if there is no structure at all, the content folder is probably empty */
if(!$structure)
{
return $this->render($response, 'content/content.twig', array( 'navigation' => true, 'content' => 'Nothing found in content folder.' ));
}
/* if it is the startpage */
if(empty($args))
{
/* check, if there is an index-file in the root of the content folder */
$contentMD = file_exists($pathToContent . DIRECTORY_SEPARATOR . 'index.md') ? file_get_contents($pathToContent . DIRECTORY_SEPARATOR . 'index.md') : NULL;
/* if there is content (index.md), then add a marker for frontend, so ajax calls for homepage-index-urls work */
if($contentMD)
{
$item = new \stdClass;
$item->urlRel = 'is_homepage_index';
}
}
else
{
/* get the request url */
$urlRel = $uri->getBasePath() . '/' . $args['params'];
/* find the url in the content-item-tree and return the item-object for the file */
$item = Folder::getItemForUrl($structure, $urlRel);
/* if there is still no item, return a 404-page */
if(!$item)
{
return $this->render404($response, array( 'navigation' => $structure, 'settings' => $settings, 'base_url' => $base_url ));
}
/* add the paging to the item */
$item = Folder::getPagingForItem($structure, $item);
/* check if url is a folder. If so, check if there is an index-file in that folder */
if($item->elementType == 'folder' && $item->index)
{
$filePath = $pathToContent . $item->path . DIRECTORY_SEPARATOR . 'index.md';
}
elseif($item->elementType == 'file')
{
$filePath = $pathToContent . $item->path;
}
/* add the modified date for the file */
$item->modified = isset($filePath) ? filemtime($filePath) : false;
/* read the content of the file */
$contentMD = isset($filePath) ? file_get_contents($filePath) : false;
}
$title = false;
$content = $contentMD;
$content = str_replace(array("\r\n", "\r"), "\n", $content);
$content = trim($content, "\n");
if($contentMD[0] == '#')
{
$contentParts = explode("\n", $contentMD, 2);
$title = trim($contentParts[0], "# \t\n\r\0\x0B");
$content = trim($contentParts[1]);
}
return $this->render($response, 'content/content.twig', array('navigation' => $structure, 'title' => $title, 'content' => $content, 'item' => $item, 'settings' => $settings ));
$this->c = $c;
$this->settings = $this->c->get('settings');
$this->structureLiveName = 'structure.txt';
$this->structureDraftName = 'structure-draft.txt';
}
public function updateArticle(Request $request, Response $response, $args)
protected function render($response, $route, $data)
{
/* Extract the parameters from get-call */
$params = $request->getParams();
if(isset($_SESSION['old']))
{
unset($_SESSION['old']);
}
/* validate input */
$validate = new Validation();
$vResult = $validate->editorInput($params);
if($this->c->request->getUri()->getScheme() == 'https')
{
$response = $response->withAddedHeader('Strict-Transport-Security', 'max-age=63072000');
}
$response = $response->withAddedHeader('X-Content-Type-Options', 'nosniff');
$response = $response->withAddedHeader('X-Frame-Options', 'SAMEORIGIN');
$response = $response->withAddedHeader('X-XSS-Protection', '1;mode=block');
$response = $response->withAddedHeader('Referrer-Policy', 'no-referrer-when-downgrade');
return $this->c->view->render($response, $route, $data);
}
protected function render404($response, $data = NULL)
{
return $this->c->view->render($response->withStatus(404), '/404.twig', $data);
}
protected function validateEditorInput()
{
$validate = new Validation();
$vResult = $validate->editorInput($this->params);
if(is_array($vResult))
{
return $response->withJson(['errors' => $vResult], 422);
}
/* initiate variables and objects that we need */
$settings = $this->c->get('settings');
$pathToContent = $settings['rootPath'] . $settings['contentFolder'];
$uri = $request->getUri();
$base_url = $uri->getBaseUrl();
$write = new writeCache();
/* we will use the cached structure to find the url for the page-update. It acts as whitelist and is more secure than a file-path, for example. */
$structure = $write->getCache('cache', 'structure.txt');
/* if there is no structure, create a fresh structure */
if(!$structure)
{
$structure = $this->getFreshStructure($pathToContent, $write, $uri);
if(!$structure)
{
return $response->withJson(['errors' => ['message' => 'content folder is empty']], 404);
}
}
/* if it is the homepage */
if($params['url'] == 'is_homepage_index')
{
$item = new \stdClass;
$item->elementType = 'folder';
$item->path = '';
}
else
{
/* search for the url in the structure */
$item = Folder::getItemForUrl($structure, $params['url']);
}
if(!$item)
{
return $response->withJson(['errors' => ['message' => 'requested page-url not found']], 404);
}
if($item->elementType == 'folder')
{
$path = $item->path . DIRECTORY_SEPARATOR . 'index.md';
}
elseif($item->elementType == 'file')
{
$path = $item->path;
}
/* get the markdown file */
$mdFile = $write->getFile($settings['contentFolder'], $path);
if($mdFile)
{
/* merge title with content forcomplete markdown document */
$updatedContent = '# ' . $params['title'] . "\r\n\r\n" . $params['content'];
/* update the file */
if($write->writeFile($settings['contentFolder'], $path, $updatedContent))
{
return $response->withJson(['success'], 200);
}
else
{
return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if file is writable']], 404);
}
}
return $response->withJson(['errors' => ['message' => 'requested markdown-file not found']], 404);
}
protected function getFreshStructure($pathToContent, $cache, $uri)
{
/* scan the content of the folder */
$structure = Folder::scanFolder($pathToContent);
/* if there is no content, render an empty page */
if(count($structure) == 0)
{
{
$this->errors = ['errors' => $vResult];
return false;
}
/* create an array of object with the whole content of the folder */
$structure = Folder::getFolderContentDetails($structure, $uri->getBaseUrl(), $uri->getBasePath());
/* cache navigation */
$cache->updateCache('cache', 'structure.txt', 'lastCache.txt', $structure);
return true;
}
protected function setStructure($draft = false, $cache = true)
{
# name of structure-file for draft or live
$filename = $draft ? $this->structureDraftName : $this->structureLiveName;
return $structure;
# set variables and objects
$this->write = new writeCache();
# check, if cached structure is still valid
if($cache && $this->write->validate('cache', 'lastCache.txt', 600))
{
# get the cached structure
$structure = $this->write->getCache('cache', $filename);
}
else
{
# scan the content of the folder
$structure = Folder::scanFolder($this->settings['rootPath'] . $this->settings['contentFolder'], $draft);
# if there is no content, render an empty page
if(count($structure) == 0)
{
$this->errors = ['errors' => ['message' => 'content folder is empty']];
return false;
}
# create an array of object with the whole content of the folder
$structure = Folder::getFolderContentDetails($structure, $this->uri->getBaseUrl(), $this->uri->getBasePath());
# cache navigation
$this->write->updateCache('cache', $filename, 'lastCache.txt', $structure);
}
$this->structure = $structure;
return true;
}
protected function setItem()
{
# if it is the homepage
if($this->params['url'] == $this->uri->getBasePath() OR $this->params['url'] == '/')
{
$item = new \stdClass;
$item->elementType = 'folder';
$item->path = '';
$item->urlRel = '/';
}
else
{
# search for the url in the structure
$item = Folder::getItemForUrl($this->structure, $this->params['url']);
}
if($item)
{
if($item->elementType == 'file')
{
$pathParts = explode('.', $item->path);
$fileType = array_pop($pathParts);
$pathWithoutType = implode('.', $pathParts);
$item->pathWithoutType = $pathWithoutType;
}
elseif($item->elementType == 'folder')
{
$item->path = $item->path . DIRECTORY_SEPARATOR . 'index';
$item->pathWithoutType = $item->path;
}
$this->item = $item;
return true;
}
$this->errors = ['errors' => ['message' => 'requested page-url not found']];
return false;
}
# determine if you want to write to published file (md) or to draft (txt)
protected function setItemPath($fileType)
{
$this->path = $this->item->pathWithoutType . '.' . $fileType;
}
protected function setPublishStatus()
{
$this->item->published = false;
$this->item->drafted = false;
if(file_exists($this->settings['rootPath'] . $this->settings['contentFolder'] . $this->item->pathWithoutType . '.md'))
{
$this->item->published = true;
# add file-type in case it is a folder
$this->item->fileType = "md";
}
elseif(file_exists($this->settings['rootPath'] . $this->settings['contentFolder'] . $this->item->pathWithoutType . '.txt'))
{
$this->item->drafted = true;
# add file-type in case it is a folder
$this->item->fileType = "txt";
}
elseif($this->item->elementType == "folder")
{
# set txt as default for a folder, so that we can create an index.txt for a folder.
$this->item->fileType = "txt";
}
}
protected function deleteContentFiles($fileTypes)
{
$basePath = $this->settings['rootPath'] . $this->settings['contentFolder'];
foreach($fileTypes as $fileType)
{
if(file_exists($basePath . $this->item->pathWithoutType . '.' . $fileType))
{
unlink($basePath . $this->item->pathWithoutType . '.' . $fileType);
# if file could not be deleted
# $this->errors = ['errors' => ['message' => 'Could not delete files, please check, if files are writable.']];
}
}
return true;
}
protected function setContent()
{
# if the file exists
if($this->item->published OR $this->item->drafted)
{
$content = $this->write->getFile($this->settings['contentFolder'], $this->path);
if($this->item->fileType == 'txt')
{
# decode the json-draft to an array
$content = json_decode($content);
}
}
elseif($this->item->elementType == "folder")
{
$content = '';
}
else
{
$this->errors = ['errors' => ['message' => 'requested file not found']];
return false;
}
$this->content = $content;
return true;
}
}

View File

@@ -9,6 +9,7 @@ use Typemill\Models\WriteYaml;
use \Symfony\Component\Yaml\Yaml;
use Typemill\Models\VersionCheck;
use Typemill\Models\Helpers;
use Typemill\Models\Markdown;
use Typemill\Events\OnPagetreeLoaded;
use Typemill\Events\OnBreadcrumbLoaded;
use Typemill\Events\OnItemLoaded;
@@ -21,7 +22,6 @@ class PageController extends Controller
{
public function index($request, $response, $args)
{
/* Initiate Variables */
$structure = false;
$contentHTML = false;
@@ -33,7 +33,7 @@ class PageController extends Controller
$cache = new WriteCache();
$uri = $request->getUri();
$base_url = $uri->getBaseUrl();
try
{
/* if the cached structure is still valid, use it */
@@ -72,7 +72,7 @@ class PageController extends Controller
echo $e->getMessage();
exit(1);
}
/* if the user is on startpage */
if(empty($args))
{
@@ -122,14 +122,14 @@ class PageController extends Controller
/* initialize parsedown */
$parsedown = new ParsedownExtension();
/* set safe mode to escape javascript and html in markdown */
$parsedown->setSafeMode(true);
/* parse markdown-file to content-array */
$contentArray = $parsedown->text($contentMD);
$contentArray = $this->c->dispatcher->dispatch('onContentArrayLoaded', new OnContentArrayLoaded($contentArray))->getData();
/* get the first image from content array */
$firstImage = $this->getFirstImage($contentArray);

View File

@@ -17,7 +17,7 @@ class ParsedownExtension extends \ParsedownExtra
# table of content support
array_unshift($this->BlockTypes['['], 'TableOfContents');
}
function text($text)
{
$Elements = $this->textElements($text);
@@ -67,7 +67,7 @@ class ParsedownExtension extends \ParsedownExtra
# Header
private $headlines = array();
private $headlines = array();
protected function blockHeader($Line)
{
@@ -266,4 +266,102 @@ class ParsedownExtension extends \ParsedownExtra
$Block['element']['text'] = "\$\$\n" . $text . "\n\$\$";
return $Block;
}
# turn markdown into an array of markdown blocks for typemill edit mode
function markdownToArrayBlocks($markdown)
{
# standardize line breaks
$markdown = str_replace(array("\r\n", "\r"), "\n", $markdown);
# remove surrounding line breaks
$markdown = trim($markdown, "\n");
# trim to maximum two linebreaks
# split text into blocks
$blocks = explode("\n\n", $markdown);
# clean up code blocks
$cleanBlocks = array();
# holds the content of codeblocks
$codeBlock = '';
# flag if codeblock is on or off.
$codeBlockOn = false;
foreach($blocks as $block)
{
// remove empty lines
if (chop($block) === '') continue;
// if the block starts with a fenced code
if(substr($block,0,2) == '``')
{
// and if we are in an open code-block
if($codeBlockOn)
{
// it must be the end of the codeblock, so add it to the codeblock
$block = $codeBlock . "\n" . $block;
// reset codeblock-value and close the codeblock.
$codeBlock = '';
$codeBlockOn = false;
}
else
{
// it must be the start of the codeblock.
$codeBlockOn = true;
}
}
if($codeBlockOn)
{
// if the codeblock is complete
if($this->isComplete($block))
{
$block = $codeBlock . "\n" . $block;
// reset codeblock-value and close the codeblock.
$codeBlock = '';
$codeBlockOn = false;
}
else
{
$codeBlock .= "\n" . $block;
continue;
}
}
$cleanBlocks[] = $block;
}
return $cleanBlocks;
}
protected function isComplete($codeblock)
{
$lines = explode("\n", $codeblock);
if(count($lines) > 1)
{
$lastLine = array_pop($lines);
if(substr($lastLine,0,2) == '``')
{
return true;
}
return false;
}
return false;
}
public function arrayBlocksToMarkdown(array $arrayBlocks)
{
$markdown = '';
foreach($arrayBlocks as $block)
{
$markdown .= $block . "\n\n";
}
return $markdown;
}
}

View File

@@ -11,7 +11,7 @@ class Folder
* vars: folder path as string
* returns: multi-dimensional array with names of folders and files
*/
public static function scanFolder($folderPath)
public static function scanFolder($folderPath, $draft = false)
{
$folderItems = scandir($folderPath);
$folderContent = array();
@@ -22,13 +22,31 @@ class Folder
{
if (is_dir($folderPath . DIRECTORY_SEPARATOR . $item))
{
/* TODO: if folder is empty or folder has only txt files, continue */
$subFolder = $item;
$folderContent[$subFolder] = self::scanFolder($folderPath . DIRECTORY_SEPARATOR . $subFolder);
$folderContent[$subFolder] = self::scanFolder($folderPath . DIRECTORY_SEPARATOR . $subFolder, $draft);
}
else
{
$file = $item;
$folderContent[] = $file;
$nameParts = self::getStringParts($item);
$fileType = array_pop($nameParts);
if($fileType == 'md')
{
$folderContent[] = $item;
}
if($draft === true && $fileType == 'txt')
{
if(isset($last) && ($last == implode($nameParts)) )
{
array_pop($folderContent);
}
$folderContent[] = $item;
}
/* store the name of the last file */
$last = implode($nameParts);
}
}
}
@@ -78,8 +96,9 @@ class Folder
$nameParts = self::getStringParts($name);
$fileType = array_pop($nameParts);
if($name == 'index.md' || $fileType !== 'md' ) continue;
# if($name == 'index.md' || $fileType !== 'md' ) continue;
if($name == 'index.md' || $name == 'index.txt' ) continue;
$item->originalName = $name;
$item->elementType = 'file';
$item->fileType = $fileType;
@@ -271,5 +290,11 @@ class Folder
{
$parts = preg_split('/\./',$fileName);
return end($parts);
}
}
public static function splitFileName($fileName)
{
$parts = preg_split('/\./',$fileName);
return $parts;
}
}

View File

@@ -2,7 +2,12 @@
use Typemill\Controllers\SettingsController;
use Typemill\Controllers\ContentController;
use Typemill\Controllers\ContentApiController;
use Typemill\Middleware\RestrictApiAccess;
$app->get('/api/v1/themes', SettingsController::class . ':getThemeSettings')->setName('api.themes')->add(new RestrictApiAccess($container['router']));
$app->put('/api/v1/article', ContentController::class . ':updateArticle')->setName('api.article.update')->add(new RestrictApiAccess($container['router']));
$app->post('/api/v1/article/publish', ContentApiController::class . ':publishArticle')->setName('api.article.publish')->add(new RestrictApiAccess($container['router']));
$app->delete('/api/v1/article/unpublish', ContentApiController::class . ':unpublishArticle')->setName('api.article.unpublish')->add(new RestrictApiAccess($container['router']));
$app->put('/api/v1/article', ContentApiController::class . ':updateArticle')->setName('api.article.update')->add(new RestrictApiAccess($container['router']));
$app->delete('/api/v1/article', ContentApiController::class . ':deleteArticle')->setName('api.article.delete')->add(new RestrictApiAccess($container['router']));
$app->post('/api/v1/block', ContentBackendController::class . ':createBlock')->setName('api.block.create')->add(new RestrictApiAccess($container['router']));

View File

@@ -4,7 +4,7 @@ use Typemill\Controllers\PageController;
use Typemill\Controllers\SetupController;
use Typemill\Controllers\AuthController;
use Typemill\Controllers\SettingsController;
use Typemill\Controllers\ContentController;
use Typemill\Controllers\ContentBackendController;
use Typemill\Middleware\RedirectIfUnauthenticated;
use Typemill\Middleware\RedirectIfAuthenticated;
use Typemill\Middleware\RedirectIfNoAdmin;
@@ -46,7 +46,7 @@ $app->post('/tm/user/delete', SettingsController::class . ':deleteUser')->setNam
$app->get('/tm/user/{username}', SettingsController::class . ':showUser')->setName('user.show')->add(new RedirectIfUnauthenticated($container['router'], $container['flash']));
$app->get('/tm/user', SettingsController::class . ':listUser')->setName('user.list')->add(new RedirectIfNoAdmin($container['router'], $container['flash']));
$app->get('/tm/content[/{params:.*}]', ContentController::class . ':showContent')->setName('content.show')->add(new RedirectIfUnauthenticated($container['router'], $container['flash']));
$app->get('/tm/content[/{params:.*}]', ContentBackendController::class . ':showContent')->setName('content.show')->add(new RedirectIfUnauthenticated($container['router'], $container['flash']));
foreach($routes as $pluginRoute)
{

View File

@@ -5,35 +5,56 @@
<div class="formWrapper">
<section>
<div id="editor" class="editor">
<form action="#" @submit.prevent="saveMarkdown">
<div id="editor" class="editor">
<form action="#" v-cloak>
<section>
<fieldset>
<div class="large" :class="{'error' : errors.title}">
<label for="title">Title*</label>
<input id="title" name="title" type="text" v-model="form.title" required />
<input @input="changeContent" id="title" name="title" type="text" v-model="form.title" value="{{title}}" required />
<span class="error" v-if="errors.title">${ errors.title }</span>
</div>
<div class="large" :class="{'error' : errors.content}">
<label for="content">Content*</label>
<resizable-textarea>
<textarea id="content" v-model="form.content" required></textarea>
<textarea @input="changeContent" id="content" v-model="form.content" @keyup.enter="submit" required>{{ content }}</textarea>
</resizable-textarea>
<span class="error" v-if="errors.content">${ errors.content }</span>
</div>
<input id="path" type="hidden" value="{{ item.urlRel }}" required readonly />
<div class="large">
<button :class="bresult" :disabled="bdisabled">Save</button>
<div v-if="errors.message" class="message error">${ errors.message }</div>
</div>
</fieldset>
</form>
</div>
<input id="origTitle" style="display:none" value="{{title}}">
<textarea id="origContent" style="display:none">{{ content }}</textarea>
{{ csrf_field() | raw }}
</section>
<div class="buttonset" id="publishController" data-published="{{ item.published }}" data-drafted="{{ item.drafted }}">
<div v-if="errors.message" class="message error">${ errors.message }</div>
<button @click.prevent="saveDraft" :class="draftResult" :disabled="draftDisabled"><span class="desktop">Save&nbsp;</span>Draft</button><button @click.prevent="publishDraft" :class="publishResult" :disabled="publishDisabled">Publish</button>
<div class="secondary">
<div class="secondary--block">
<button @click.prevent="depublishArticle" class="button--secondary button--secondary__hightlight" :disabled="publishStatus"><span class="desktop">${publishLabel}</span><span class="mobile">ON</span></button><button @click.prevent="showModal" class="button--secondary"><span class="desktop">delete</span><span class="mobile">X</mobile></button>
</div>
<!--
<div class="secondary--block">
<button class="button--secondary">md help</button>
<button class="button--secondary">raw mode</button>
<div>
-->
</div>
</div>
<div id="modalWindow" :class="modalWindow">
<div class="modalInner">
<div @click="hideModal" id="closeModal" class="closeModal">X</div>
<h2>Delete page</h2>
<p>Do you really want to delete this page? Please confirm.</p>
<button @click.prevent="deleteArticle" class="large" :class="deleteResult" :disabled="deleteDisabled">Delete Page</button>
</div>
</div>
</form>
</div>
</section>
{{ csrf_field() | raw }}
</div>

View File

@@ -10,3 +10,12 @@ Font license info
Homepage: http://fortawesome.github.com/Font-Awesome/
## Web Symbols
Copyright (c) 2011 by Just Be Nice studio. All rights reserved.
Author: Just Be Nice studio
License: SIL (http://scripts.sil.org/OFL)
Homepage: http://www.justbenicestudio.com/

View File

@@ -7,15 +7,27 @@
"ascent": 850,
"glyphs": [
{
"uid": "381da2c2f7fd51f8de877c044d7f439d",
"css": "picture",
"code": 59392,
"uid": "8b9e6a8dd8f67f7c003ed8e7e5ee0857",
"css": "off",
"code": 59393,
"src": "fontawesome"
},
{
"uid": "41087bc74d4b20b55059c60a33bf4008",
"css": "edit",
"code": 59393,
"uid": "c5fd349cbd3d23e4ade333789c29c729",
"css": "eye",
"code": 59394,
"src": "fontawesome"
},
{
"uid": "e99461abfef3923546da8d745372c995",
"css": "cog",
"code": 59396,
"src": "fontawesome"
},
{
"uid": "5408be43f7c42bccee419c6be53fdef5",
"css": "doc-text",
"code": 61686,
"src": "fontawesome"
},
{
@@ -25,22 +37,16 @@
"src": "fontawesome"
},
{
"uid": "e99461abfef3923546da8d745372c995",
"css": "cog",
"code": 59394,
"uid": "381da2c2f7fd51f8de877c044d7f439d",
"css": "picture",
"code": 59392,
"src": "fontawesome"
},
{
"uid": "8b9e6a8dd8f67f7c003ed8e7e5ee0857",
"css": "off",
"uid": "8da3ac534210aae9c0f0e13804c1df97",
"css": "cancel",
"code": 59395,
"src": "fontawesome"
},
{
"uid": "5408be43f7c42bccee419c6be53fdef5",
"css": "doc-text",
"code": 61686,
"src": "fontawesome"
"src": "websymbols"
}
]
}

View File

@@ -1,7 +1,8 @@
.icon-picture:before { content: '\e800'; } /* '' */
.icon-edit:before { content: '\e801'; } /* '' */
.icon-cog:before { content: '\e802'; } /* '' */
.icon-off:before { content: '\e803'; } /* '' */
.icon-off:before { content: '\e801'; } /* '' */
.icon-eye:before { content: '\e802'; } /* '' */
.icon-cancel:before { content: '\e803'; } /* '' */
.icon-cog:before { content: '\e804'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */
.icon-doc-text:before { content: '\f0f6'; } /* '' */

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,8 @@
.icon-picture { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.icon-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
.icon-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f6;&nbsp;'); }

View File

@@ -11,8 +11,9 @@
}
.icon-picture { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.icon-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
.icon-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f6;&nbsp;'); }

View File

@@ -1,11 +1,11 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?58763890');
src: url('../font/fontello.eot?58763890#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?58763890') format('woff2'),
url('../font/fontello.woff?58763890') format('woff'),
url('../font/fontello.ttf?58763890') format('truetype'),
url('../font/fontello.svg?58763890#fontello') format('svg');
src: url('../font/fontello.eot?88351620');
src: url('../font/fontello.eot?88351620#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?88351620') format('woff2'),
url('../font/fontello.woff?88351620') format('woff'),
url('../font/fontello.ttf?88351620') format('truetype'),
url('../font/fontello.svg?88351620#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
src: url('../font/fontello.svg?58763890#fontello') format('svg');
src: url('../font/fontello.svg?88351620#fontello') format('svg');
}
}
*/
@@ -56,8 +56,9 @@
}
.icon-picture:before { content: '\e800'; } /* '' */
.icon-edit:before { content: '\e801'; } /* '' */
.icon-cog:before { content: '\e802'; } /* '' */
.icon-off:before { content: '\e803'; } /* '' */
.icon-off:before { content: '\e801'; } /* '' */
.icon-eye:before { content: '\e802'; } /* '' */
.icon-cancel:before { content: '\e803'; } /* '' */
.icon-cog:before { content: '\e804'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */
.icon-doc-text:before { content: '\f0f6'; } /* '' */

View File

@@ -229,11 +229,11 @@ body {
}
@font-face {
font-family: 'fontello';
src: url('./font/fontello.eot?16442487');
src: url('./font/fontello.eot?16442487#iefix') format('embedded-opentype'),
url('./font/fontello.woff?16442487') format('woff'),
url('./font/fontello.ttf?16442487') format('truetype'),
url('./font/fontello.svg?16442487#fontello') format('svg');
src: url('./font/fontello.eot?8909408');
src: url('./font/fontello.eot?8909408#iefix') format('embedded-opentype'),
url('./font/fontello.woff?8909408') format('woff'),
url('./font/fontello.ttf?8909408') format('truetype'),
url('./font/fontello.svg?8909408#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@@ -299,11 +299,12 @@ body {
<div class="container" id="icons">
<div class="row">
<div class="the-icons span3" title="Code: 0xe800"><i class="demo-icon icon-picture">&#xe800;</i> <span class="i-name">icon-picture</span><span class="i-code">0xe800</span></div>
<div class="the-icons span3" title="Code: 0xe801"><i class="demo-icon icon-edit">&#xe801;</i> <span class="i-name">icon-edit</span><span class="i-code">0xe801</span></div>
<div class="the-icons span3" title="Code: 0xe802"><i class="demo-icon icon-cog">&#xe802;</i> <span class="i-name">icon-cog</span><span class="i-code">0xe802</span></div>
<div class="the-icons span3" title="Code: 0xe803"><i class="demo-icon icon-off">&#xe803;</i> <span class="i-name">icon-off</span><span class="i-code">0xe803</span></div>
<div class="the-icons span3" title="Code: 0xe801"><i class="demo-icon icon-off">&#xe801;</i> <span class="i-name">icon-off</span><span class="i-code">0xe801</span></div>
<div class="the-icons span3" title="Code: 0xe802"><i class="demo-icon icon-eye">&#xe802;</i> <span class="i-name">icon-eye</span><span class="i-code">0xe802</span></div>
<div class="the-icons span3" title="Code: 0xe803"><i class="demo-icon icon-cancel">&#xe803;</i> <span class="i-name">icon-cancel</span><span class="i-code">0xe803</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xe804"><i class="demo-icon icon-cog">&#xe804;</i> <span class="i-name">icon-cog</span><span class="i-code">0xe804</span></div>
<div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext">&#xf08e;</i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div>
<div class="the-icons span3" title="Code: 0xf0f6"><i class="demo-icon icon-doc-text">&#xf0f6;</i> <span class="i-name">icon-doc-text</span><span class="i-code">0xf0f6</span></div>
</div>

View File

@@ -8,11 +8,13 @@
<missing-glyph horiz-adv-x="1000" />
<glyph glyph-name="picture" unicode="&#xe800;" d="M357 529q0-45-31-76t-76-32-76 32-31 76 31 76 76 31 76-31 31-76z m572-215v-250h-786v107l178 179 90-89 285 285z m53 393h-893q-7 0-12-5t-6-13v-678q0-7 6-13t12-5h893q7 0 13 5t5 13v678q0 8-5 13t-13 5z m89-18v-678q0-37-26-63t-63-27h-893q-36 0-63 27t-26 63v678q0 37 26 63t63 27h893q37 0 63-27t26-63z" horiz-adv-x="1071.4" />
<glyph glyph-name="edit" unicode="&#xe801;" d="M496 189l64 65-85 85-64-65v-31h53v-54h32z m245 402q-9 9-18 0l-196-196q-9-9 0-18t18 0l196 196q9 9 0 18z m45-331v-106q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-8-8-18-4-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v70q0 7 5 12l36 36q8 8 20 4t11-16z m-54 411l161-160-375-375h-161v160z m248-73l-51-52-161 161 51 52q16 15 38 15t38-15l85-85q16-16 16-38t-16-38z" horiz-adv-x="1000" />
<glyph glyph-name="off" unicode="&#xe801;" d="M857 350q0-87-34-166t-91-137-137-92-166-34-167 34-136 92-92 137-34 166q0 102 45 191t126 151q24 18 54 14t46-28q18-23 14-53t-28-47q-54-41-84-101t-30-127q0-58 23-111t61-91 91-61 111-23 110 23 92 61 61 91 22 111q0 68-30 127t-84 101q-23 18-28 47t14 53q17 24 47 28t53-14q81-61 126-151t45-191z m-357 429v-358q0-29-21-50t-50-21-51 21-21 50v358q0 29 21 50t51 21 50-21 21-50z" horiz-adv-x="857.1" />
<glyph glyph-name="cog" unicode="&#xe802;" d="M571 350q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
<glyph glyph-name="eye" unicode="&#xe802;" d="M929 314q-85 132-213 197 34-58 34-125 0-103-73-177t-177-73-177 73-73 177q0 67 34 125-128-65-213-197 75-114 187-182t242-68 243 68 186 182z m-402 215q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m473-215q0-19-11-38-78-129-210-206t-279-77-279 77-210 206q-11 19-11 38t11 39q78 128 210 205t279 78 279-78 210-205q11-20 11-39z" horiz-adv-x="1000" />
<glyph glyph-name="off" unicode="&#xe803;" d="M857 350q0-87-34-166t-91-137-137-92-166-34-167 34-136 92-92 137-34 166q0 102 45 191t126 151q24 18 54 14t46-28q18-23 14-53t-28-47q-54-41-84-101t-30-127q0-58 23-111t61-91 91-61 111-23 110 23 92 61 61 91 22 111q0 68-30 127t-84 101q-23 18-28 47t14 53q17 24 47 28t53-14q81-61 126-151t45-191z m-357 429v-358q0-29-21-50t-50-21-51 21-21 50v358q0 29 21 50t51 21 50-21 21-50z" horiz-adv-x="857.1" />
<glyph glyph-name="cancel" unicode="&#xe803;" d="M654 349l346-346-154-154-346 346-346-346-154 154 346 346-346 346 154 154 346-346 346 346 154-154z" horiz-adv-x="1000" />
<glyph glyph-name="cog" unicode="&#xe804;" d="M571 350q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
<glyph glyph-name="link-ext" unicode="&#xf08e;" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1089,59 +1089,98 @@ label .help, .label .help{
padding: 15px 20px;
color: #e0474c;
}
.buttonset{
position: fixed;
display: block;
bottom: 0;
left: 0;
right: 0;
box-sizing: border-box;
padding: 2px;
max-width: 900px;
background: #fff;
box-shadow: 0 0 2px #ddd;
}
.buttonset .message.error{
position: absolute;
background: #e0474c;
color: #fff;
width: 100%;
left: 0;
top: -25px;
padding: 5px 40px;
box-sizing: border-box;
box-shadow: 0 0 2px #ddd;
}
.editor button{
position: relative;
border-radius: 3px;
border-radius: 0px;
padding:10px;
min-width: 70px;
font-size: 0.8em;
margin: 4px 2px;
}
.editor button:enabled,
.editor button[enabled]{
color: #f9f8f6;
border: 2px solid #e0474c;
background: #e0474c;
padding:10px;
min-width: 200px;
}
.editor button:hover{
.editor button:enabled:hover,
.editor button[enabled]:hover{
border: 2px solid #cc4146;
background: #cc4146;
}
.editor button:disabled, .editor button[disabled]{
border: 2px solid #cc4146;
background: #cc4146;
color: #eee;
.editor button:disabled,
.editor button[disabled]{
border: 2px solid #eee;
background: #eee;
color: #444;
cursor: default;
}
.editor button:disabled:after,
.editor button[disabled]:after,
.editor button.success:after,
.editor button.fail:after{
position: absolute;
right: 8px;
top: 8px;
width: 8px;
height: 8px;
border-radius: 50%;
content: '';
}
.editor button:disabled:after,
.editor button[disabled]:after{
border: 8px solid #eee;
border-top: 8px solid #ccc;
background: #ccc;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.editor button.success:after,
.editor button.fail:after{
border: 8px solid #eee;
}
.editor button.success:after{
background: #00cc00;
}
.editor button.fail:after{
background: #e0474c;
}
.buttonset .secondary{
display: inline-block;
float: right;
}
.buttonset .secondary--block{
display: inline-block;
}
.editor button.button--secondary{
display: inline-block;
min-width: 40px;
max-width: 40px;
background: #fff;
border: 1px solid #eee;
color: #444;
}
.editor button.button--secondary:hover{
background: #e0474c;
border: 1px solid #e0474c;
color: #eee;
}
.editor button.button--secondary[disabled],
.editor button.button--secondary:disabled{
border: 1px solid #eee;
background: #eee;
color: #444;
cursor: default;
}
.editor button.button--secondary__hightlight[enabled],
.editor button.button--secondary__hightlight:enabled{
background: #66b0a3;
border: 1px solid #66b0a3;
color: #fff;
}
[v-cloak]{
display: none;
}
.mobile{
display: block;
}
.desktop{
display: none;
}
@media only screen and (min-width: 600px) {
header.headline{
padding: 0px 20px;
@@ -1163,6 +1202,58 @@ label .help, .label .help{
.settings .medium a.button{
display: inline-block;
width: auto;
}
.editor button{
min-width: 150px;
font-size: 0.9em;
}
.editor button.load:after,
.editor button.success:after,
.editor button.fail:after{
position: absolute;
right: 8px;
top: 6px;
width: 8px;
height: 8px;
border-radius: 50%;
content: '';
}
.editor button.load:after{
border: 8px solid #fff;
border-top: 8px solid #ccc;
background: #ccc;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.editor button.success:after,
.editor button.fail:after{
border: 8px solid #eee;
}
.editor button.success:after{
background: #00cc00;
}
.editor button.fail:after{
background: #e0474c;
}
.buttonset .secondary--block{
display: inline-block;
}
.editor button.button--secondary{
display: inline-block;
width: auto;
min-width: auto;
max-width: inherit;
border: 1px solid #eee;
color: #444;
}
.mobile{
display: none;
}
.desktop{
display: inline-block;
}
}
@media only screen and (min-width: 800px) {
@@ -1282,4 +1373,14 @@ label .help, .label .help{
border-top: 0px;
border-left: 2px solid #70c1b3;
}
.buttonset{
width: 76%;
padding: 10px 40px;
left: auto;
right: auto;
max-width: 900px;
}
.editor button{
border-radius: 3px;
}
}

View File

@@ -9,7 +9,6 @@ Vue.component('resizable-textarea', {
this.$nextTick(() => {
this.$el.setAttribute('style', 'height:' + (this.$el.scrollHeight) + 'px;overflow-y:hidden;')
})
this.$el.addEventListener('input', this.resizeTextarea)
},
beforeDestroy () {
@@ -24,29 +23,81 @@ let app = new Vue({
delimiters: ['${', '}'],
el: '#editor',
data: {
root: document.getElementById("main").dataset.url,
form: {
title: document.getElementById("origTitle").value,
content: document.getElementById("origContent").value,
title: this.title = document.getElementById("title").value,
content: this.title = document.getElementById("content").value,
url: document.getElementById("path").value,
csrf_name: document.getElementById("csrf_name").value,
csrf_value: document.getElementById("csrf_value").value,
},
root: document.getElementById("main").dataset.url,
errors:{
title: false,
content: false,
message: false,
},
bdisabled: false,
bresult: false,
modalWindow: "modal hide",
draftDisabled: true,
publishDisabled: document.getElementById("publishController").dataset.drafted ? false : true,
deleteDisabled: false,
draftResult: "",
publishResult: "",
deleteResult: "",
publishStatus: document.getElementById("publishController").dataset.published ? false : true,
publishLabel: document.getElementById("publishController").dataset.published ? "online" : "offline",
},
methods: {
saveMarkdown: function(e){
submit: function(e){
/* count submits and react to line before. */
},
changeContent: function(e){
this.draftDisabled = false;
this.publishDisabled = false;
this.draftResult = "";
this.publishResult = "";
},
publishDraft: function(e){
var self = this;
self.errors = {title: false, content: false, message: false};
self.publishResult = "load";
self.publishDisabled = "disabled";
var url = this.root + '/api/v1/article/publish';
var method = 'POST';
sendJson(function(response, httpStatus)
{
if(response)
{
var result = JSON.parse(response);
if(result.errors)
{
self.publishDisabled = false;
self.publishResult = "fail";
if(result.errors.title){ self.errors.title = result.errors.title[0] };
if(result.errors.content){ self.errors.content = result.errors.content[0] };
if(result.errors.message){ self.errors.message = result.errors.message };
}
else
{
self.draftDisabled = "disabled";
self.publishResult = "success";
self.publishStatus = false;
self.publishLabel = "online";
}
}
}, method, url, this.form );
},
saveDraft: function(e){
var self = this;
self.errors = {title: false, content: false, message: false},
self.bresult = '';
self.bdisabled = "disabled";
self.errors = {title: false, content: false, message: false};
self.draftDisabled = "disabled";
self.draftResult = "load";
var url = this.root + '/api/v1/article';
var method = 'PUT';
@@ -54,24 +105,88 @@ let app = new Vue({
sendJson(function(response, httpStatus)
{
if(response)
{
self.bdisabled = false;
{
var result = JSON.parse(response);
if(result.errors)
{
self.bresult = 'fail';
self.draftDisabled = false;
self.draftResult = 'fail';
if(result.errors.title){ self.errors.title = result.errors.title[0] };
if(result.errors.content){ self.errors.content = result.errors.content[0] };
if(result.errors.message){ self.errors.message = result.errors.message };
}
else
{
self.bresult = 'success';
self.draftResult = 'success';
}
}
}, method, url, this.form );
}
},
depublishArticle: function(e){
var self = this;
self.errors = {title: false, content: false, message: false};
self.publishStatus = "disabled";
var url = this.root + '/api/v1/article/unpublish';
var method = 'DELETE';
sendJson(function(response, httpStatus)
{
if(response)
{
var result = JSON.parse(response);
if(result.errors)
{
self.publishStatus = false;
if(result.errors.message){ self.errors.message = result.errors.message };
}
else
{
self.publishResult = "";
self.publishLabel = "offline";
self.publishDisabled = false;
}
}
}, method, url, this.form );
},
deleteArticle: function(e){
var self = this;
self.errors = {title: false, content: false, message: false};
self.deleteDisabled = "disabled";
self.deleteResult = "load";
var url = this.root + '/api/v1/article';
var method = 'DELETE';
sendJson(function(response, httpStatus)
{
if(response)
{
var result = JSON.parse(response);
if(result.errors)
{
self.modalWindow = "modal";
if(result.errors.message){ self.errors.message = result.errors.message };
}
else
{
self.modalWindow = "modal";
window.location.replace(self.root + '/tm/content');
}
}
}, method, url, this.form );
},
showModal: function(e){
this.modalWindow = "modal show";
},
hideModal: function(e){
this.modalWindow = "modal";
},
}
})