浏览代码

Version 1.2.2: Draft Management

Sebastian 7 年之前
父节点
当前提交
c38783c4f6

+ 1 - 1
cache/lastCache.txt

@@ -1 +1 @@
-1530867542
+1532420761

+ 0 - 5
content/5_info/20_Übermaß.md

@@ -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.

+ 232 - 0
system/Controllers/ContentApiController.php

@@ -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 */
+	}	
+}

+ 73 - 0
system/Controllers/ContentBackendController.php

@@ -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 ));
+	}
+}

+ 184 - 157
system/Controllers/ContentController.php

@@ -2,216 +2,243 @@
 
 
 namespace Typemill\Controllers;
 namespace Typemill\Controllers;
 
 
-use Slim\Views\Twig;
 use Slim\Http\Request;
 use Slim\Http\Request;
 use Slim\Http\Response;
 use Slim\Http\Response;
+use Interop\Container\ContainerInterface;
 use Typemill\Models\Validation;
 use Typemill\Models\Validation;
 use Typemill\Models\Folder;
 use Typemill\Models\Folder;
 use Typemill\Models\Write;
 use Typemill\Models\Write;
-use Typemill\Models\WriteYaml;
 use Typemill\Models\WriteCache;
 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;
+	
+	# holds the params from request
+	protected $params;
+		
+	# holds the slim-uri-object
+	protected $uri;
+	
+	# 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;
 	
 	
-	/**
-	* 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 name of the structure-file with drafts for author environment 
+	protected $structureDraftName;
 	
 	
-	public function showContent(Request $request, Response $response, $args)
+	# 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)
+		$this->c 					= $c;
+		$this->settings				= $this->c->get('settings');
+		$this->structureLiveName 	= 'structure.txt';
+		$this->structureDraftName 	= 'structure-draft.txt';
+	}
+	
+	protected function render($response, $route, $data)
+	{
+		if(isset($_SESSION['old']))
 		{
 		{
-			return $this->render($response, 'content/content.twig', array( 'navigation' => true, 'content' => 'Nothing found in content folder.' ));
+			unset($_SESSION['old']);
 		}
 		}
-
-		/* 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)
+		if($this->c->request->getUri()->getScheme() == 'https')
 		{
 		{
-			return $this->render($response, 'content/content.twig', array( 'navigation' => true, 'content' => 'Nothing found in content folder.' ));
+			$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 it is the startpage */
-		if(empty($args))
+		if(is_array($vResult))
+		{ 
+			$this->errors = ['errors' => $vResult];
+			return false;
+		}
+		return true;
+	}
+	
+	protected function setStructure($draft = false, $cache = true)
+	{
+		# name of structure-file for draft or live
+		$filename = $draft ? $this->structureDraftName : $this->structureLiveName;
+		
+		# set variables and objects
+		$this->write = new writeCache();
+		
+		# check, if cached structure is still valid 
+		if($cache && $this->write->validate('cache', 'lastCache.txt', 600))
 		{
 		{
-			/* 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';
-			}
+			# get the cached structure
+			$structure = $this->write->getCache('cache', $filename);
 		}
 		}
 		else
 		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')
+			# 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)
 			{
 			{
-				$filePath = $pathToContent . $item->path;
+				$this->errors = ['errors' => ['message' => 'content folder is empty']];
+				return false;
 			}
 			}
 
 
-			/* add the modified date for the file */
-			$item->modified	= isset($filePath) ? filemtime($filePath) : false;
+			# create an array of object with the whole content of the folder
+			$structure = Folder::getFolderContentDetails($structure, $this->uri->getBaseUrl(), $this->uri->getBasePath());
 
 
-			/* read the content of the file */
-			$contentMD 		= isset($filePath) ? file_get_contents($filePath) : false;
+			# cache navigation
+			$this->write->updateCache('cache', $filename, 'lastCache.txt', $structure);
 		}
 		}
 		
 		
-		$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->structure = $structure;
+		return true;
 	}
 	}
 
 
-	public function updateArticle(Request $request, Response $response, $args)
+	protected function setItem()
 	{
 	{
-		/* Extract the parameters from get-call */
-		$params 		= $request->getParams();
-		
-		/* validate input */
-		$validate		= new Validation();
-		$vResult		= $validate->editorInput($params);
-
-		if(is_array($vResult))
+		# if it is the homepage
+		if($this->params['url'] == $this->uri->getBasePath() OR $this->params['url'] == '/')
 		{
 		{
-			return $response->withJson(['errors' => $vResult], 422);
+			$item 					= new \stdClass;
+			$item->elementType 		= 'folder';
+			$item->path				= '';
+			$item->urlRel			= '/';
 		}
 		}
-		
-		/* 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)
+		else
+		{
+			# search for the url in the structure
+			$item = Folder::getItemForUrl($this->structure, $this->params['url']);
+		}
+				
+		if($item)
 		{
 		{
-			$structure 	= $this->getFreshStructure($pathToContent, $write, $uri);
-			if(!$structure)
+			if($item->elementType == 'file')
+			{
+				$pathParts 					= explode('.', $item->path);
+				$fileType 					= array_pop($pathParts);
+				$pathWithoutType 			= implode('.', $pathParts);
+				$item->pathWithoutType		= $pathWithoutType;
+			}
+			elseif($item->elementType == 'folder')
 			{
 			{
-				return $response->withJson(['errors' => ['message' => 'content folder is empty']], 404);
+				$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 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)
+		if(file_exists($this->settings['rootPath'] . $this->settings['contentFolder'] . $this->item->pathWithoutType . '.md'))
 		{
 		{
-			return $response->withJson(['errors' => ['message' => 'requested page-url not found']], 404);
+			$this->item->published = true;
+			
+			# add file-type in case it is a folder
+			$this->item->fileType = "md"; 
 		}
 		}
-				
-		if($item->elementType == 'folder')
+		elseif(file_exists($this->settings['rootPath'] . $this->settings['contentFolder'] . $this->item->pathWithoutType . '.txt'))
 		{
 		{
-			$path = $item->path . DIRECTORY_SEPARATOR . 'index.md';
+			$this->item->drafted = true;
+			
+			# add file-type in case it is a folder
+			$this->item->fileType = "txt"; 
 		}
 		}
-		elseif($item->elementType == 'file')
+		elseif($this->item->elementType == "folder")
 		{
 		{
-			$path = $item->path;
+			# 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'];
 		
 		
-		/* get the markdown file */
-		$mdFile	= $write->getFile($settings['contentFolder'], $path);		
-		if($mdFile)
+		foreach($fileTypes as $fileType)
 		{
 		{
-			/* 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
+			if(file_exists($basePath . $this->item->pathWithoutType . '.' . $fileType))
 			{
 			{
-				return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if file is writable']], 404);
-			}
+				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 $response->withJson(['errors' => ['message' => 'requested markdown-file not found']], 404);
+		
+		return true;
 	}
 	}
 	
 	
-	protected function getFreshStructure($pathToContent, $cache, $uri)
+	protected function setContent()
 	{
 	{
-		/* scan the content of the folder */
-		$structure = Folder::scanFolder($pathToContent);
-
-		/* if there is no content, render an empty page */
-		if(count($structure) == 0)
+		# 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;
 			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 $structure;
+		$this->content = $content;
+		return true;		
 	}
 	}
 }
 }

+ 5 - 5
system/Controllers/PageController.php

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

+ 100 - 2
system/Extensions/ParsedownExtension.php

@@ -17,7 +17,7 @@ class ParsedownExtension extends \ParsedownExtra
 		# table of content support
 		# table of content support
         array_unshift($this->BlockTypes['['], 'TableOfContents');
         array_unshift($this->BlockTypes['['], 'TableOfContents');
     }
     }
-	
+		
 	function text($text)
 	function text($text)
 	{
 	{
         $Elements = $this->textElements($text);
         $Elements = $this->textElements($text);
@@ -67,7 +67,7 @@ class ParsedownExtension extends \ParsedownExtra
 
 
     # Header
     # Header
 	
 	
-	private $headlines 			= array();
+	private $headlines = array();
 	
 	
     protected function blockHeader($Line)
     protected function blockHeader($Line)
     {
     {
@@ -266,4 +266,102 @@ class ParsedownExtension extends \ParsedownExtra
         $Block['element']['text'] = "\$\$\n" . $text . "\n\$\$";
         $Block['element']['text'] = "\$\$\n" . $text . "\n\$\$";
         return $Block;
         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;
+	}	
 }
 }

+ 32 - 7
system/Models/Folder.php

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

+ 6 - 1
system/Routes/Api.php

@@ -2,7 +2,12 @@
 
 
 use Typemill\Controllers\SettingsController;
 use Typemill\Controllers\SettingsController;
 use Typemill\Controllers\ContentController;
 use Typemill\Controllers\ContentController;
+use Typemill\Controllers\ContentApiController;
 use Typemill\Middleware\RestrictApiAccess;
 use Typemill\Middleware\RestrictApiAccess;
 
 
 $app->get('/api/v1/themes', SettingsController::class . ':getThemeSettings')->setName('api.themes')->add(new RestrictApiAccess($container['router']));
 $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']));

+ 2 - 2
system/Routes/Web.php

@@ -4,7 +4,7 @@ use Typemill\Controllers\PageController;
 use Typemill\Controllers\SetupController;
 use Typemill\Controllers\SetupController;
 use Typemill\Controllers\AuthController;
 use Typemill\Controllers\AuthController;
 use Typemill\Controllers\SettingsController;
 use Typemill\Controllers\SettingsController;
-use Typemill\Controllers\ContentController;
+use Typemill\Controllers\ContentBackendController;
 use Typemill\Middleware\RedirectIfUnauthenticated;
 use Typemill\Middleware\RedirectIfUnauthenticated;
 use Typemill\Middleware\RedirectIfAuthenticated;
 use Typemill\Middleware\RedirectIfAuthenticated;
 use Typemill\Middleware\RedirectIfNoAdmin;
 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/{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/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)
 foreach($routes as $pluginRoute)
 {
 {

+ 36 - 15
system/author/content/content.twig

@@ -5,35 +5,56 @@
 	
 	
 	<div class="formWrapper">
 	<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>
 					<fieldset>
 						<div class="large" :class="{'error' : errors.title}">
 						<div class="large" :class="{'error' : errors.title}">
 							<label for="title">Title*</label>
 							<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>
 							<span class="error" v-if="errors.title">${ errors.title }</span>
 						</div>
 						</div>
 						<div class="large" :class="{'error' : errors.content}">
 						<div class="large" :class="{'error' : errors.content}">
 							<label for="content">Content*</label>
 							<label for="content">Content*</label>
 							<resizable-textarea>
 							<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>
 							</resizable-textarea>
 							<span class="error" v-if="errors.content">${ errors.content }</span>
 							<span class="error" v-if="errors.content">${ errors.content }</span>
 						</div>
 						</div>
 						<input id="path" type="hidden" value="{{ item.urlRel }}" required readonly />
 						<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>
 					</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>
 	</div>
 	
 	

+ 9 - 0
system/author/css/fontello/LICENSE.txt

@@ -10,3 +10,12 @@ Font license info
    Homepage:  http://fortawesome.github.com/Font-Awesome/
    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/
+
+

+ 24 - 18
system/author/css/fontello/config.json

@@ -7,33 +7,21 @@
   "ascent": 850,
   "ascent": 850,
   "glyphs": [
   "glyphs": [
     {
     {
-      "uid": "381da2c2f7fd51f8de877c044d7f439d",
-      "css": "picture",
-      "code": 59392,
-      "src": "fontawesome"
-    },
-    {
-      "uid": "41087bc74d4b20b55059c60a33bf4008",
-      "css": "edit",
+      "uid": "8b9e6a8dd8f67f7c003ed8e7e5ee0857",
+      "css": "off",
       "code": 59393,
       "code": 59393,
       "src": "fontawesome"
       "src": "fontawesome"
     },
     },
     {
     {
-      "uid": "e15f0d620a7897e2035c18c80142f6d9",
-      "css": "link-ext",
-      "code": 61582,
+      "uid": "c5fd349cbd3d23e4ade333789c29c729",
+      "css": "eye",
+      "code": 59394,
       "src": "fontawesome"
       "src": "fontawesome"
     },
     },
     {
     {
       "uid": "e99461abfef3923546da8d745372c995",
       "uid": "e99461abfef3923546da8d745372c995",
       "css": "cog",
       "css": "cog",
-      "code": 59394,
-      "src": "fontawesome"
-    },
-    {
-      "uid": "8b9e6a8dd8f67f7c003ed8e7e5ee0857",
-      "css": "off",
-      "code": 59395,
+      "code": 59396,
       "src": "fontawesome"
       "src": "fontawesome"
     },
     },
     {
     {
@@ -41,6 +29,24 @@
       "css": "doc-text",
       "css": "doc-text",
       "code": 61686,
       "code": 61686,
       "src": "fontawesome"
       "src": "fontawesome"
+    },
+    {
+      "uid": "e15f0d620a7897e2035c18c80142f6d9",
+      "css": "link-ext",
+      "code": 61582,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "381da2c2f7fd51f8de877c044d7f439d",
+      "css": "picture",
+      "code": 59392,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "8da3ac534210aae9c0f0e13804c1df97",
+      "css": "cancel",
+      "code": 59395,
+      "src": "websymbols"
     }
     }
   ]
   ]
 }
 }

+ 4 - 3
system/author/css/fontello/css/fontello-codes.css

@@ -1,7 +1,8 @@
 
 
 .icon-picture:before { content: '\e800'; } /* '' */
 .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-link-ext:before { content: '\f08e'; } /* '' */
 .icon-doc-text:before { content: '\f0f6'; } /* '' */
 .icon-doc-text:before { content: '\f0f6'; } /* '' */

文件差异内容过多而无法显示
+ 3 - 3
system/author/css/fontello/css/fontello-embedded.css


+ 4 - 3
system/author/css/fontello/css/fontello-ie7-codes.css

@@ -1,7 +1,8 @@
 
 
 .icon-picture { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
 .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-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;'); }
 .icon-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f6;&nbsp;'); }

+ 4 - 3
system/author/css/fontello/css/fontello-ie7.css

@@ -11,8 +11,9 @@
 }
 }
  
  
 .icon-picture { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
 .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-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;'); }
 .icon-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f6;&nbsp;'); }

+ 11 - 10
system/author/css/fontello/css/fontello.css

@@ -1,11 +1,11 @@
 @font-face {
 @font-face {
   font-family: 'fontello';
   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-weight: normal;
   font-style: normal;
   font-style: normal;
 }
 }
@@ -15,7 +15,7 @@
 @media screen and (-webkit-min-device-pixel-ratio:0) {
 @media screen and (-webkit-min-device-pixel-ratio:0) {
   @font-face {
   @font-face {
     font-family: 'fontello';
     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-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-link-ext:before { content: '\f08e'; } /* '' */
 .icon-doc-text:before { content: '\f0f6'; } /* '' */
 .icon-doc-text:before { content: '\f0f6'; } /* '' */

+ 9 - 8
system/author/css/fontello/demo.html

@@ -229,11 +229,11 @@ body {
 }
 }
 @font-face {
 @font-face {
       font-family: 'fontello';
       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-weight: normal;
       font-style: normal;
       font-style: normal;
     }
     }
@@ -299,11 +299,12 @@ body {
     <div class="container" id="icons">
     <div class="container" id="icons">
       <div class="row">
       <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: 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>
       <div class="row">
       <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: 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 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>
       </div>

二进制
system/author/css/fontello/font/fontello.eot


+ 5 - 3
system/author/css/fontello/font/fontello.svg

@@ -8,11 +8,13 @@
 <missing-glyph horiz-adv-x="1000" />
 <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="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" />
 <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" />
 
 

二进制
system/author/css/fontello/font/fontello.ttf


二进制
system/author/css/fontello/font/fontello.woff


二进制
system/author/css/fontello/font/fontello.woff2


+ 137 - 36
system/author/css/style.css

@@ -1089,59 +1089,98 @@ label .help, .label .help{
 	padding: 15px 20px;
 	padding: 15px 20px;
 	color: #e0474c;
 	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{
 .editor button{
 	position: relative;
 	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;
 	color: #f9f8f6;
 	border: 2px solid #e0474c; 
 	border: 2px solid #e0474c; 
 	background: #e0474c; 
 	background: #e0474c; 
-	padding:10px;
-	min-width: 200px;
 }
 }
-.editor button:hover{
+.editor button:enabled:hover,
+.editor button[enabled]:hover{
 	border: 2px solid #cc4146; 
 	border: 2px solid #cc4146; 
 	background: #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;
 	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: '';
+
+.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:disabled:after, 
-.editor button[disabled]:after{
-	border: 8px solid #eee;
-	border-top: 8px solid #ccc;
-	background: #ccc;
-    animation: spin 2s linear infinite;	
+.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;
 }
 }
-@keyframes spin {
-    0% { transform: rotate(0deg); }
-    100% { transform: rotate(360deg); }
+.editor button.button--secondary__hightlight[enabled],
+.editor button.button--secondary__hightlight:enabled{
+	background: #66b0a3;
+	border: 1px solid #66b0a3;
+	color: #fff;
 }
 }
-.editor button.success:after,
-.editor button.fail:after{
-	border: 8px solid #eee;
+[v-cloak]{
+	display: none;
 }
 }
-.editor button.success:after{
-	background: #00cc00;
+.mobile{
+	display: block;
 }
 }
-.editor button.fail:after{
-	background: #e0474c;
+.desktop{
+	display: none;
 }
 }
-
 @media only screen and (min-width: 600px) {
 @media only screen and (min-width: 600px) {
 	header.headline{
 	header.headline{
 		padding: 0px 20px;
 		padding: 0px 20px;
@@ -1163,6 +1202,58 @@ label .help, .label .help{
 	.settings .medium a.button{
 	.settings .medium a.button{
 		display: inline-block;
 		display: inline-block;
 		width: auto;
 		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) {
 @media only screen and (min-width: 800px) {
@@ -1282,4 +1373,14 @@ label .help, .label .help{
 		border-top: 0px;
 		border-top: 0px;
 		border-left: 2px solid #70c1b3;
 		border-left: 2px solid #70c1b3;
 	}
 	}
+	.buttonset{
+		width: 76%;
+		padding: 10px 40px;
+		left: auto;
+		right: auto;
+		max-width: 900px;
+	}	
+	.editor button{
+		border-radius: 3px;
+	}	
 }
 }

+ 131 - 16
system/author/js/vue-editor.js

@@ -9,7 +9,6 @@ Vue.component('resizable-textarea', {
     this.$nextTick(() => {
     this.$nextTick(() => {
       this.$el.setAttribute('style', 'height:' + (this.$el.scrollHeight) + 'px;overflow-y:hidden;')
       this.$el.setAttribute('style', 'height:' + (this.$el.scrollHeight) + 'px;overflow-y:hidden;')
     })
     })
-
     this.$el.addEventListener('input', this.resizeTextarea)
     this.$el.addEventListener('input', this.resizeTextarea)
   },
   },
   beforeDestroy () {
   beforeDestroy () {
@@ -24,29 +23,81 @@ let app = new Vue({
     delimiters: ['${', '}'],
     delimiters: ['${', '}'],
 	el: '#editor',
 	el: '#editor',
 	data: {
 	data: {
+		root: document.getElementById("main").dataset.url,
 		form: {
 		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,
 			url: 		document.getElementById("path").value,
 			csrf_name: 	document.getElementById("csrf_name").value,
 			csrf_name: 	document.getElementById("csrf_name").value,
 			csrf_value:	document.getElementById("csrf_value").value,			
 			csrf_value:	document.getElementById("csrf_value").value,			
 		},
 		},
-		root: 		document.getElementById("main").dataset.url,
 		errors:{
 		errors:{
 			title: false,
 			title: false,
 			content: false,
 			content: false,
 			message: 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: {
 	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;
 			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 url = this.root + '/api/v1/article';
 			var method 	= 'PUT';
 			var method 	= 'PUT';
@@ -54,24 +105,88 @@ let app = new Vue({
 			sendJson(function(response, httpStatus)
 			sendJson(function(response, httpStatus)
 			{
 			{
 				if(response)
 				if(response)
-				{
-					self.bdisabled = false;
-					
+				{					
 					var result = JSON.parse(response);
 					var result = JSON.parse(response);
 					
 					
 					if(result.errors)
 					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.title){ self.errors.title = result.errors.title[0] };
 						if(result.errors.content){ self.errors.content = result.errors.content[0] };
 						if(result.errors.content){ self.errors.content = result.errors.content[0] };
 						if(result.errors.message){ self.errors.message = result.errors.message };
 						if(result.errors.message){ self.errors.message = result.errors.message };
 					}
 					}
 					else
 					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 );
 			}, method, url, this.form );
-		}
+		},
+		showModal: function(e){
+			this.modalWindow = "modal show";
+		},
+		hideModal: function(e){
+			this.modalWindow = "modal";
+		},
 	}
 	}
 })
 })

部分文件因为文件数量过多而无法显示