1
0
mirror of https://github.com/typemill/typemill.git synced 2025-08-02 12:21:03 +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

2
cache/lastCache.txt vendored
View File

@@ -1 +1 @@
1530867542
1532420761

View File

@@ -1,5 +0,0 @@
# Übermaß: A simple encoding test
This is just a test for character encoding. If you see the correct german word "Übermaß" in the left navigation, and if you can click the navigation link to get to this page, then everything works fine.
I still encourage you to use only english characters to name your content files, because many special characters and many languages won't work. I even doubt, that german or european characters will work in special server environments. So you can try it, but if it does not work, you only option is to avoid special characters in your file-names.

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";
},
}
})