Procházet zdrojové kódy

Version 1.3.1: Improved Meta-Tabs and Translations

trendschau před 5 roky
rodič
revize
0d09fe60b1

+ 1 - 1
cache/lastCache.txt

@@ -1 +1 @@
-1577899842
+1578262565

+ 9 - 4
system/Controllers/MetaApiController.php

@@ -30,10 +30,13 @@ class MetaApiController extends ContentController
 		# loop through all plugins
 		foreach($this->settings['plugins'] as $name => $plugin)
 		{
-			$pluginSettings = \Typemill\Settings::getObjectSettings('plugins', $name);
-			if($pluginSettings && isset($pluginSettings['metatabs']))
+			if($plugin['active'])
 			{
-				$metatabs = array_merge_recursive($metatabs, $pluginSettings['metatabs']);
+				$pluginSettings = \Typemill\Settings::getObjectSettings('plugins', $name);
+				if($pluginSettings && isset($pluginSettings['metatabs']))
+				{
+					$metatabs = array_merge_recursive($metatabs, $pluginSettings['metatabs']);
+				}
 			}
 		}
 
@@ -155,4 +158,6 @@ class MetaApiController extends ContentController
 		# return with the new metadata
 		return $response->withJson(array('metadata' => $metaData, 'errors' => false));
 	}
-}
+}
+
+# check models -> writeYaml for getPageMeta and getPageMetaDefaults.

+ 2 - 2
system/Controllers/SettingsController.php

@@ -463,7 +463,7 @@ class SettingsController extends Controller
 
 			if($validate->newUser($params, $userroles))
 			{
-				$userdata	= array('username' => $params['username'], 'email' => $params['email'], 'userrole' => $params['userrole'], 'password' => $params['password']);
+				$userdata	= array('username' => $params['username'], 'firstname' => $params['firstname'], 'lastname' => $params['lastname'], 'email' => $params['email'], 'userrole' => $params['userrole'], 'password' => $params['password']);
 				
 				$user->createUser($userdata);
 
@@ -511,7 +511,7 @@ class SettingsController extends Controller
 	
 			if($validate->existingUser($params, $userroles))
 			{
-				$userdata	= array('username' => $params['username'], 'email' => $params['email'], 'userrole' => $params['userrole']);
+				$userdata	= array('username' => $params['username'], 'firstname' => $params['firstname'], 'lastname' => $params['lastname'], 'email' => $params['email'], 'userrole' => $params['userrole']);
 				
 				if(empty($params['password']) AND empty($params['newpassword']))
 				{

+ 1 - 0
system/Models/Field.php

@@ -64,6 +64,7 @@ class Field
 									'id',
 									'autocomplete',
 									'placeholder',
+									'maxlength',
 									'size',
 									'rows',
 									'cols',

+ 31 - 1
system/Models/User.php

@@ -38,6 +38,15 @@ class User extends WriteYaml
 						'password'	=> $this->generatePassword($params['password']),
 						'userrole' 	=> $params['userrole']
 					);
+
+		if(isset($params['firstname']))
+		{
+			$userdata['firstname'] = $params['firstname'];
+		}
+		if(isset($params['lastname']))
+		{
+			$userdata['lastname'] = $params['lastname'];
+		}
 	
 		if($this->updateYaml('settings/users', $userdata['username'] . '.yaml', $userdata))
 		{
@@ -58,8 +67,20 @@ class User extends WriteYaml
 		$update = array_merge($userdata, $params);
 		
 		$this->updateYaml('settings/users', $userdata['username'] . '.yaml', $update);
+
+		$_SESSION['user'] 	= $update['username'];
+		$_SESSION['role'] 	= $update['userrole'];
+
+		if(isset($update['firstname']))
+		{
+			$_SESSION['firstname'] = $update['firstname'];
+		}
+		if(isset($update['lastname']))
+		{
+			$_SESSION['lastname'] = $update['lastname'];
+		}
 		
-		return $userdata['username'];		
+		return $userdata['username'];
 	}
 	
 	public function deleteUser($username)
@@ -88,6 +109,15 @@ class User extends WriteYaml
 			$_SESSION['user'] 	= $user['username'];
 			$_SESSION['role'] 	= $user['userrole'];
 			$_SESSION['login'] 	= $user['lastlogin'];
+
+			if(isset($user['firstname']))
+			{
+				$_SESSION['firstname'] = $user['firstname'];
+			}
+			if(isset($user['lastname']))
+			{
+				$_SESSION['lastname'] = $user['lastname'];
+			}
 		}
 	}
 	

+ 37 - 11
system/Models/Validation.php

@@ -124,6 +124,10 @@ class Validation
 		$v->rule('lengthBetween', 'password', 5, 20)->message("Length between 5 - 20");
 		$v->rule('lengthBetween', 'username', 3, 20)->message("Length between 3 - 20"); 
 		$v->rule('userAvailable', 'username')->message("User already exists");
+		$v->rule('noHTML', 'firstname')->message(" contains HTML");
+		$v->rule('lengthBetween', 'firstname', 2, 40);
+		$v->rule('noHTML', 'lastname')->message(" contains HTML");
+		$v->rule('lengthBetween', 'lastname', 2, 40);
 		$v->rule('email', 'email')->message("e-mail is invalid");
 		$v->rule('in', 'userrole', $userroles);
 		
@@ -137,10 +141,14 @@ class Validation
 		$v->rule('alphaNum', 'username')->message("invalid");
 		$v->rule('lengthBetween', 'username', 3, 20)->message("Length between 3 - 20"); 
 		$v->rule('userExists', 'username')->message("user does not exist");
+		$v->rule('noHTML', 'firstname')->message(" contains HTML");
+		$v->rule('lengthBetween', 'firstname', 2, 40);
+		$v->rule('noHTML', 'lastname')->message(" contains HTML");
+		$v->rule('lengthBetween', 'lastname', 2, 40);		
 		$v->rule('email', 'email')->message("e-mail is invalid");
 		$v->rule('in', 'userrole', $userroles);
 
-		return $this->validationResult($v);		
+		return $this->validationResult($v);
 	}
 	
 	public function username($username)
@@ -329,7 +337,23 @@ class Validation
 		{
 			$v->rule('required', $fieldName);
 		}
-		
+		if(isset($fieldDefinitions['maxlength']))
+		{
+			$v->rule('lengthMax', $fieldName, $fieldDefinitions['maxlength']);
+		}
+		if(isset($fieldDefinitions['max']))
+		{
+			$v->rule('max', $fieldName, $fieldDefinitions['max']);
+		}
+		if(isset($fieldDefinitions['min']))
+		{
+			$v->rule('min', $fieldName, $fieldDefinitions['min']);
+		}
+		if(isset($fieldDefinitions['pattern']))
+		{
+			$v->rule('regex', $fieldName, '/^' . $fieldDefinitions['pattern'] . '$/');
+		}
+
 		switch($fieldDefinitions['type'])
 		{
 			case "select":
@@ -350,7 +374,7 @@ class Validation
 				{
 					$v->rule('in', $key, $options);
 				}
-				break;				
+				break;
 			case "color":
 				$v->rule('regex', $fieldName, '/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/');
 				break;
@@ -361,33 +385,35 @@ class Validation
 				$v->rule('date', $fieldName);
 				break;
 			case "checkbox":
-				$v->rule('accepted', $fieldName);
+				if(isset($fieldDefinitions['required']))
+				{
+				 	$v->rule('accepted', $fieldName);
+				}
 				break;
 			case "url":
-				$v->rule('lengthMax', $fieldName, 200);
 				$v->rule('url', $fieldName);
+				$v->rule('lengthMax', $fieldName, 200);
 				break;
 			case "text":
-				$v->rule('lengthMax', $fieldName, 200);
+				$v->rule('noHTML', $fieldName);
+				$v->rule('lengthMax', $fieldName, 500);
 				$v->rule('regex', $fieldName, '/^[\pL0-9_ \-\.\?\!\/\:]*$/u');
 				break;
 			case "textarea":
-				$v->rule('lengthMax', $fieldName, 1000);
 				$v->rule('noHTML', $fieldName);
-				// $v->rule('regex', $fieldName, '/<[^<]+>/');
+				$v->rule('lengthMax', $fieldName, 1000);
 				break;
 			case "paragraph":
-				$v->rule('lengthMax', $fieldName, 1000);
 				$v->rule('noHTML', $fieldName);
+				$v->rule('lengthMax', $fieldName, 1000);
 				break;
 			case "password":
 				$v->rule('lengthMax', $fieldName, 100);
 				break;
 			default:
 				$v->rule('lengthMax', $fieldName, 1000);
-				$v->rule('regex', $fieldName, '/^[\pL0-9_ \-]*$/u');				
+				$v->rule('regex', $fieldName, '/^[\pL0-9_ \-]*$/u');		
 		}
-
 		return $this->validationResult($v, $objectName);
 	}
 	

+ 16 - 1
system/Models/WriteYaml.php

@@ -86,12 +86,27 @@ class WriteYaml extends Write
 			$description 	= substr($description, 0, $lastSpace);
 		}
 
+		$author = $settings['author'];
+
+		if(isset($_SESSION))
+		{
+			if(isset($_SESSION['firstname']) && $_SESSION['firstname'] !='' && isset($_SESSION['lastname']) && $_SESSION['lastname'] != '')
+			{
+				$author = $_SESSION['firstname'] . ' ' . $_SESSION['lastname'];
+			}
+			elseif(isset($_SESSION['user']))
+			{
+				$author = $_SESSION['user'];
+			}
+		}
+
 		# create new meta-file
 		$meta = [
 			'meta' => [
 				'title' 		=> $title,
 				'description' 	=> $description,
-				'author' 		=> $settings['author'], # change to session, extend userdata
+				'author' 		=> $author,
+				'created'		=> date("Y-m-d"),
 			]
 		];
 

+ 1 - 2
system/author/css/style.css

@@ -1773,7 +1773,6 @@ button.format-item.close:hover{
 	top: 0;  
 	left: 0;  
 	transform: translate(-50%, -100%);  
-	transition: 0.2s all;  
 	display: flex;  
 	justify-content: center;  
 	align-items: center;
@@ -1809,7 +1808,7 @@ button.format-item.close:hover{
 	margin-right: 2px;
 }
 .urlinput{
-	width: 80%;
+	width: 75%;
 	min-height: auto;
 	background: #555;
 	color: #fff;

+ 21 - 0
system/author/editor/editor-raw.twig

@@ -5,6 +5,27 @@
 	
 	<div class="formWrapper">
 
+		<div id="metanav" class="metanav" v-cloak>
+		  
+		  <button
+		    v-for="tab in tabs"
+		    v-bind:key="tab"
+		    v-bind:class="['tab-button', { active: currentTab === tab }]"
+		    v-on:click="currentTab = tab"
+		  >${tab}</button>
+
+		  <component 
+		  	class="tab" 
+		  	v-bind:is="currentTabComponent" 
+		  	:saved="saved"
+		  	:errors="formErrors[currentTab]" 
+		  	:schema="formDefinitions[currentTab]"
+		  	:formdata="formData[currentTab]"
+		  	v-on:saveform="saveForm">
+		  </component>
+
+		</div>
+
 		<div id="editor" class="editor">
 			<form action="#" v-cloak>
 			

+ 18 - 3
system/author/js/vue-blox.js

@@ -393,7 +393,7 @@ const contentComponent = Vue.component('content-block', {
 })
 
 const inlineFormatsComponent = Vue.component('inline-formats', {
-	template: '<div><div :style="{ left: `${x}px`, top: `${y}px` }" @mousedown.prevent="" v-show="showInlineFormat" id="formatBar" class="inlineFormatBar">' + 
+	template: '<div><div :style="{ left: `${x}px`, top: `${y}px`, width: `${z}px` }" @mousedown.prevent="" v-show="showInlineFormat" id="formatBar" class="inlineFormatBar">' + 
 				  '<div  v-if="link">' + 
 				      '<input v-model="url" @keyup.13="formatLink" ref="urlinput" class="urlinput" type="text" placeholder="insert url">' + 
 					  '<span class="inlineFormatItem inlineFormatLink" @mousedown.prevent="formatLink"><svg class="icon icon-check"><use xlink:href="#icon-check"></use></svg></span>' + 
@@ -410,10 +410,12 @@ const inlineFormatsComponent = Vue.component('inline-formats', {
 	data: function(){
 		return {
 			formatBar: false,
+			formatElements: 0,
 			startX: 0,
 			startY: 0,
      		x: 0,
      		y: 0,
+     		z: 150,
      		textComponent: '',
      		selectedText: '',
      		startPos: false,
@@ -500,6 +502,10 @@ const inlineFormatsComponent = Vue.component('inline-formats', {
 
 		  	this.y = event.offsetY - 15;
 
+		  	/* calculate the width of the format bar */
+			this.formatElements = document.getElementsByClassName('inlineFormatItem').length;
+			this.z = this.formatElements * 30;
+
 			this.showInlineFormat = true;
 			this.selectedText = selectedText;
 		},
@@ -508,30 +514,35 @@ const inlineFormatsComponent = Vue.component('inline-formats', {
 			content = this.textComponent.value;
 			content = content.substring(0, this.startPos) + '**' + this.selectedText + '**' + content.substring(this.endPos, content.length);
 			this.$parent.updatemarkdown(content);
+		  	this.showInlineFormat = false;			
 		},
 		formatItalic()
 		{
 			content = this.textComponent.value;
 			content = content.substring(0, this.startPos) + '_' + this.selectedText + '_' + content.substring(this.endPos, content.length);
 			this.$parent.updatemarkdown(content);
+		  	this.showInlineFormat = false;			
 		},
 		formatCode()
 		{
 			content = this.textComponent.value;
 			content = content.substring(0, this.startPos) + '`' + this.selectedText + '`' + content.substring(this.endPos, content.length);
-			this.$parent.updatemarkdown(content);			
+			this.$parent.updatemarkdown(content);
+		  	this.showInlineFormat = false;						
 		},
 		formatMath()
 		{
 			content = this.textComponent.value;
 			content = content.substring(0, this.startPos) + '$' + this.selectedText + '$' + content.substring(this.endPos, content.length);
 			this.$parent.updatemarkdown(content);
+		  	this.showInlineFormat = false;			
 		},
 		formatLink()
 		{
 			if(this.url == "")
 			{
-				this.link = false; 
+				this.link = false;
+			  	this.showInlineFormat = false;
 				return;
 			}
 			content = this.textComponent.value;
@@ -543,11 +554,15 @@ const inlineFormatsComponent = Vue.component('inline-formats', {
 		openLink()
 		{
 			this.link = true;
+			this.url = '';
+			this.z = 200;
 			this.$nextTick(() => this.$refs.urlinput.focus());
 		},
 		closeLink()
 		{
 			this.link = false;
+			this.url = '';
+		  	this.showInlineFormat = false;
 		}
 	}
 })

+ 249 - 17
system/author/js/vue-meta.js

@@ -1,11 +1,172 @@
 const FormBus = new Vue();
 
 Vue.component('component-text', {
-	props: ['class', 'placeholder', 'label', 'name', 'type', 'size', 'value', 'errors'],
+	props: ['class', 'id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
 	template: '<div class="large">' +
 				'<label>{{ label }}</label>' +
-				'<input type="text" :name="name" :placeholder="placeholder" :value="value"  @input="update($event, name)">' +
+				'<input type="text"' + 
+					' :id="id"' +
+					' :maxlength="maxlength"' +
+					' :readonly="readonly"' +
+					' :required="required"' +
+					' :disabled="disabled"' +
+					' :name="name"' +
+					' :placeholder="placeholder"' +
+					' :value="value"' +
+					'@input="update($event, name)">' +
 			  	'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
+			  	'<span v-else class="fielddescription"><small>{{ description }}</small></span>' +
+			  '</div>',
+	methods: {
+		update: function($event, name)
+		{
+			FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
+		},
+	},
+})
+
+Vue.component('component-textarea', {
+	props: ['class', 'id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
+	template: '<div class="large">' +
+				'<label>{{ label }}</label>' +
+				'<textarea ' +
+					' :id="id"' +
+					' :readonly="readonly"' +
+					' :required="required"' +  
+					' :disabled="disabled"' +  
+					' :name="name"' +
+					' :placeholder="placeholder"' +
+					' :value="value"' +
+					' @input="update($event, name)"></textarea>' +
+			  	'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
+			  	'<span v-else class="fielddescription"><small>{{ description }}</small></span>' +
+			  '</div>',
+	methods: {
+		update: function($event, name)
+		{
+			FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
+		},
+	},
+})
+
+Vue.component('component-url', {
+	props: ['class', 'id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
+	template: '<div class="large">' +
+				'<label>{{ label }}</label>' +
+				'<input type="url"' + 
+					' :id="id"' +
+					' :maxlength="maxlength"' +
+					' :readonly="readonly"' +
+					' :required="required"' +
+					' :disabled="disabled"' +
+					' :name="name"' +
+					' :placeholder="placeholder"' +
+					' :value="value"' +
+					'@input="update($event, name)">' +
+			  	'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
+			  	'<span v-else class="fielddescription"><small>{{ description }}</small></span>' +			  	
+			  '</div>',
+	methods: {
+		update: function($event, name)
+		{
+			FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
+		},
+	},
+})
+
+Vue.component('component-number', {
+	props: ['class', 'id', 'description', 'min', 'max', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
+	template: '<div class="large">' +
+				'<label>{{ label }}</label>' +
+				'<input type="number"' + 
+					' :id="id"' +
+					' :min="min"' +
+					' :min="max"' +
+					' :maxlength="maxlength"' +
+					' :readonly="readonly"' +
+					' :required="required"' +
+					' :disabled="disabled"' +
+					' :name="name"' +
+					' :placeholder="placeholder"' +
+					' :value="value"' +
+					'@input="update($event, name)">' +
+			  	'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
+			  	'<span v-else class="fielddescription"><small>{{ description }}</small></span>' +
+			  '</div>',
+	methods: {
+		update: function($event, name)
+		{
+			FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
+		},
+	},
+})
+
+Vue.component('component-email', {
+	props: ['class', 'id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
+	template: '<div class="large">' +
+				'<label>{{ label }}</label>' +
+				'<input type="email"' + 
+					' :id="id"' +
+					' :maxlength="maxlength"' +
+					' :readonly="readonly"' +
+					' :required="required"' +
+					' :disabled="disabled"' +
+					' :name="name"' +
+					' :placeholder="placeholder"' +
+					' :value="value"' +
+					'@input="update($event, name)">' +
+			  	'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
+			  	'<span v-else class="fielddescription"><small>{{ description }}</small></span>' +
+			  '</div>',
+	methods: {
+		update: function($event, name)
+		{
+			FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
+		},
+	},
+})
+
+Vue.component('component-tel', {
+	props: ['class', 'id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
+	template: '<div class="large">' +
+				'<label>{{ label }}</label>' +
+				'<input type="tel"' + 
+					' :id="id"' +
+					' :maxlength="maxlength"' +
+					' :readonly="readonly"' +
+					' :required="required"' +
+					' :disabled="disabled"' +
+					' :name="name"' +
+					' :placeholder="placeholder"' +
+					' :value="value"' +
+					'@input="update($event, name)">' +
+			  	'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
+			  	'<span v-else class="fielddescription"><small>{{ description }}</small></span>' +
+			  '</div>',
+	methods: {
+		update: function($event, name)
+		{
+			FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
+		},
+	},
+})
+
+Vue.component('component-password', {
+	props: ['class', 'id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
+	template: '<div class="large">' +
+				'<label>{{ label }}</label>' +
+				'<input type="password"' + 
+					' :id="id"' +
+					' :maxlength="maxlength"' +
+					' :readonly="readonly"' +
+					' :required="required"' +
+					' :disabled="disabled"' +
+					' :name="name"' +
+					' :placeholder="placeholder"' +
+					' :value="value"' +
+					'@input="update($event, name)">' +
+			  	'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
+			  	'<span v-else class="fielddescription"><small>{{ description }}</small></span>' +
 			  '</div>',
 	methods: {
 		update: function($event, name)
@@ -16,11 +177,20 @@ Vue.component('component-text', {
 })
 
 Vue.component('component-date', {
-	props: ['class', 'placeholder', 'readonly', 'label', 'name', 'type', 'size', 'value', 'errors'],
+	props: ['class', 'id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
 	template: '<div class="large">' +
 				'<label>{{ label }}</label>' +
-				'<input type="date" :readonly="readonly" :name="name" :placeholder="placeholder" :value="value"  @input="update($event, name)">' +
+				'<input type="date" ' +
+					' :id="id"' +
+					' :readonly="readonly"' +
+					' :required="required"' +  
+					' :disabled="disabled"' +  
+					' :name="name"' +
+					' :placeholder="placeholder"' +
+					' :value="value"' +
+					' @input="update($event, name)">' +
 			  	'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
+			  	'<span v-else class="fielddescription"><small>{{ description }}</small></span>' +
 			  '</div>',
 	methods: {
 		update: function($event, name)
@@ -30,12 +200,21 @@ Vue.component('component-date', {
 	},
 })
 
-Vue.component('component-textarea', {
-	props: ['class', 'placeholder', 'label', 'name', 'type', 'size', 'value', 'errors'],
+Vue.component('component-color', {
+	props: ['class', 'id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
 	template: '<div class="large">' +
-				'<label>{{label}}</label>' +
-				'<textarea :name="name" v-model="value" @input="update($event, name)"></textarea>' +
+				'<label>{{ label }}</label>' +
+				'<input type="color" ' +
+					' :id="id"' +
+					' :readonly="readonly"' +
+					' :required="required"' +  
+					' :disabled="disabled"' +  
+					' :name="name"' +
+					' :placeholder="placeholder"' +
+					' :value="value"' +
+					' @input="update($event, name)">' +
 			  	'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
+			  	'<span v-else class="fielddescription"><small>{{ description }}</small></span>' +
 			  '</div>',
 	methods: {
 		update: function($event, name)
@@ -46,13 +225,20 @@ Vue.component('component-textarea', {
 })
 
 Vue.component('component-select', {
-	props: ['class', 'placeholder', 'label', 'name', 'type', 'size', 'options', 'value', 'errors'],
+	props: ['class', 'id', 'description', 'readonly', 'required', 'disabled', 'label', 'name', 'type', 'options', 'value', 'errors'],
 	template: '<div class="large">' +
 				'<label>{{label}}</label>' +
-			    '<select v-model="value" @change="update($event,name)">' +
-			      '<option v-for="option,optionkey in options" v-bind:value="optionkey">{{option}}</option>' +
+			    '<select' + 
+					' :id="id"' +
+					' :name="name"' +
+					' :required="required"' +  
+					' :disabled="disabled"' +
+					' v-model="value"' + 
+			    	' @change="update($event,name)">' +
+			      	'<option v-for="option,optionkey in options" v-bind:value="optionkey">{{option}}</option>' +
 			    '</select>' +
-			  	'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +  
+			  	'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
+			  	'<span v-else class="fielddescription"><small>{{ description }}</small></span>' +
 			  '</div>',
 	methods: {
 		update: function($event, name)
@@ -63,13 +249,21 @@ Vue.component('component-select', {
 })
 
 Vue.component('component-checkbox', {
-	props: ['class', 'label', 'checkboxlabel', 'name', 'type', 'value', 'errors'],
+	props: ['class', 'id', 'description', 'readonly', 'required', 'disabled', 'label', 'checkboxlabel', 'name', 'type', 'value', 'errors'],
 	template: '<div class="large">' +
 				'<label>{{ label }}</label>' +
 				'<label class="control-group">{{ checkboxlabel }}' +
-				  '<input type="checkbox" :name="name" v-model="value" @change="update($event, value, name)">' +				
+				  '<input type="checkbox"' + 
+					' :id="id"' +
+					' :readonly="readonly"' +
+					' :required="required"' +  
+					' :disabled="disabled"' +
+				    ' :name="name"' + 
+				    ' v-model="value"' +
+				    ' @change="update($event, value, name)">' +				
 			  	  '<span class="checkmark"></span>' +
 			  	  '<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
+				  '<span v-else class="fielddescription"><small>{{ description }}</small></span>' +
 			  	'</label>' +  
 			  '</div>',
 	methods: {
@@ -80,14 +274,50 @@ Vue.component('component-checkbox', {
 	},
 })
 
+Vue.component('component-checkboxlist', {
+	props: ['class', 'description', 'readonly', 'required', 'disabled', 'label', 'checkboxlabel', 'options', 'name', 'type', 'value', 'errors'],
+	template: '<div class="large">' +
+				'<label>{{ label }}</label>' +
+				'<label v-for="option, optionvalue in options" class="control-group">{{ option }}' +
+				  '<input type="checkbox"' + 
+					' :id="optionvalue"' +
+				  	' :value="optionvalue"' + 
+				  	' v-model="value" ' + 
+				  	' @change="update($event, value, optionvalue, name)">' +
+			  	  '<span class="checkmark"></span>' +
+			  	  '<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
+			  	  '<span v-else class="fielddescription"><small>{{ description }}</small></span>' +
+			  '</div>',
+	methods: {
+		update: function($event, value, optionvalue, name)
+		{
+			/* if value (array) for checkboxlist is not initialized yet */
+			if(value === true || value === false)
+			{
+				value = [optionvalue];
+			}
+			FormBus.$emit('forminput', {'name': name, 'value' : value});
+		},
+	},
+})
+
 Vue.component('component-radio', {
-	props: ['label', 'options', 'name', 'type', 'value', 'errors'],
-	template: '<div class="medium">' +
+	props: ['class', 'id', 'description', 'readonly', 'required', 'disabled', 'options', 'label', 'name', 'type', 'value', 'errors'],
+	template: '<div class="large">' +
 				'<label>{{ label }}</label>' +
 				'<label v-for="option,optionvalue in options" class="control-group">{{ option }}' +
-				  '<input type="radio" :name="name" :value="optionvalue" v-model="value" @change="update($event, value, name)">' +				
+				  '<input type="radio"' + 
+					' :id="id"' +
+					' :readonly="readonly"' +
+					' :required="required"' +  
+					' :disabled="disabled"' +
+				  	' :name="name"' +
+				  	' :value="optionvalue"' + 
+				  	' v-model="value" ' + 
+				  	' @change="update($event, value, name)">' +				
 			  	  '<span class="radiomark"></span>' +
 			  	  '<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
+				  '<span v-else class="fielddescription"><small>{{ description }}</small></span>' +
 			  	'</label>' +  
 			  '</div>',
 	methods: {
@@ -167,12 +397,14 @@ let meta = new Vue({
         .then(function (response) {
 
         	var formdefinitions = response.data.metadefinitions;
+        	
         	for (var key in formdefinitions) {
 				if (formdefinitions.hasOwnProperty(key)) {
 					self.tabs.push(key);
 					self.formErrors[key] = false;
 				}
 			}
+
 			self.formErrorsReset = self.formErrors;
 			self.formDefinitions = formdefinitions;
 

+ 15 - 0
system/author/layouts/layoutEditor.twig

@@ -20,6 +20,9 @@
 		<link rel="stylesheet" href="{{ base_url }}/system/author/css/normalize.css" />
 		<link rel="stylesheet" href="{{ base_url }}/system/author/css/style.css?20191231" />
 		<link rel="stylesheet" href="{{ base_url }}/system/author/css/color-picker.min.css" />
+
+		{{ assets.renderCSS() }}
+
 	</head>
 	<body>
 		<svg style="position: absolute; width: 0; height: 0; overflow: hidden" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
@@ -56,13 +59,25 @@
 			</article>
 			<footer></footer>
 		</div>
+		<script src="{{ base_url }}/system/author/js/axios.min.js?20191124"></script>
+		<script>
+			const myaxios = axios.create();
+			myaxios.defaults.baseURL =  "{{ base_url }}";
+		</script>		
 		<script src="{{ base_url }}/system/author/js/vue.min.js?20191231"></script>
 		<script src="{{ base_url }}/system/author/js/autosize.min.js?20191231"></script>
 		<script src="{{ base_url }}/system/author/js/sortable.min.js?20191231"></script>
 		<script src="{{ base_url }}/system/author/js/vuedraggable.umd.min.js?20191231"></script>
 		<script src="{{ base_url }}/system/author/js/author.js?20191231"></script>
+
+		{{ assets.renderEditorJS() }}		
+		
 		<script src="{{ base_url }}/system/author/js/vue-publishcontroller.js?20191231"></script>		
 		<script src="{{ base_url }}/system/author/js/vue-editor.js?20191231"></script>
+		<script src="{{ base_url }}/system/author/js/vue-meta.js?20191231"></script>		
 		<script src="{{ base_url }}/system/author/js/vue-navi.js?20191231"></script>
+
+		{{ assets.renderJS() }}
+		
 	</body>
 </html>

+ 14 - 3
system/author/metatabs.yaml

@@ -3,19 +3,30 @@ meta:
     title:
       type: text
       label: Meta title
-      size: 60
+      maxlength: 60
       class: large
     description:
       type: textarea
       label: Meta description
       size: 160
       class: large
+      description: If not filled, the description is extracted from content.
     author:
       type: text
       label: author
       class: large
+      description: Taken from your user account if set.
+    manualdate:
+      type: date
+      label: Manual date
     modified:
       type: date
-      label: Last modified at (readonly)
+      label: Last modified live (readonly)
+      readonly: readonly
+      class: medium
+      description: Used as fallback when no manual date is set.
+    created:
+      type: date
+      label: Created at (readonly)
       readonly: readonly
-      class: large
+      class: medium

+ 18 - 2
system/author/settings/user.twig

@@ -23,6 +23,22 @@
 							<span class="error">{{ errors.username | first }}</span>
 						{% endif %}
 					</div>
+
+					<div class="large{{ errors.firstname ? ' errors' : '' }}">
+						<label for="firstname">First Name</label>
+						<input type="text" name="firstname" value="{{ old.firstname ? old.firstname : userdata.firstname }}">
+						{% if errors.firstname %}
+							<span class="error">{{ errors.firstname | first }}</span>
+						{% endif %}
+					</div>
+
+					<div class="large{{ errors.lastname ? ' errors' : '' }}">
+						<label for="lastname">Last Name</label>
+						<input type="text" name="lastname" value="{{ old.lastname ? old.lastname : userdata.lastname }}">
+						{% if errors.lastname %}
+							<span class="error">{{ errors.lastname | first }}</span>
+						{% endif %}
+					</div>
 					
 					<div class="large{{ errors.email ? ' errors' : '' }}">
 						<label for="email">E-Mail <abbr title="required">*</abbr></label>
@@ -48,7 +64,7 @@
 					
 					<div class="large{{ errors.password ? ' errors' : '' }}">
 						<label for="password">Actual Password</label>
-						<input type="password" name="password">
+						<input type="password" name="password" autocomplete="off">
 						{% if errors.password %}
 							<span class="error">{{ errors.password | first }}</span>
 						{% endif %}
@@ -56,7 +72,7 @@
 
 					<div class="large{{ errors.newpassword ? ' errors' : '' }}">
 						<label for="newpassword">New Password</label>
-						<input type="password" name="newpassword">
+						<input type="password" name="newpassword" autocomplete="off">
 						{% if errors.newpassword %}
 							<span class="error">{{ errors.newpassword | first }}</span>
 						{% endif %}

+ 18 - 2
system/author/settings/usernew.twig

@@ -22,7 +22,23 @@
 							<span class="error">{{ errors.username | first }}</span>
 						{% endif %}
 					</div>
-					
+
+					<div class="large{{ errors.firstname ? ' errors' : '' }}">
+						<label for="firstname">First Name</label>
+						<input type="text" name="firstname" value="{{ old.firstname ? old.firstname : userdata.firstname }}">
+						{% if errors.firstname %}
+							<span class="error">{{ errors.firstname | first }}</span>
+						{% endif %}
+					</div>
+
+					<div class="large{{ errors.lastname ? ' errors' : '' }}">
+						<label for="lastname">Last Name</label>
+						<input type="text" name="lastname" value="{{ old.lastname ? old.lastname : userdata.lastname }}">
+						{% if errors.lastname %}
+							<span class="error">{{ errors.lastname | first }}</span>
+						{% endif %}
+					</div>					
+
 					<div class="large{{ errors.email ? ' errors' : '' }}">
 						<label for="email">E-Mail <abbr title="required">*</abbr></label>
 						<input type="text" name="email" value="{{ old.email ? old.email : '' }}" required>
@@ -45,7 +61,7 @@
 					
 					<div class="large{{ errors.password ? ' errors' : '' }}">
 						<label for="password">Password <abbr title="required">*</abbr></label>
-						<input type="password" name="password" required>
+						<input type="password" name="password" autocomplete="off" required>
 						{% if errors.password %}
 							<span class="error">{{ errors.password | first }}</span>
 						{% endif %}

+ 6 - 4
themes/typemill/page.twig

@@ -1,3 +1,5 @@
+{% set published = metatabs.meta.manualdate ? metatabs.meta.manualdate : metatabs.meta.modified %}
+
 	{% if content is empty %}
 
 		<h1>{{ item.name }}</h1>
@@ -9,10 +11,10 @@
 	{% if (settings.themes.typemill.socialPosition.top or settings.themes.typemill.modifiedPosition.top or settings.themes.typemill.authorPosition.top or settings.themes.typemill.gitPosition.top) %}	
 		<div class="meta-info">
 			{% if settings.themes.typemill.authorPosition.top %}
-				<small>{{ settings.themes.typemill.authorIntro }}: {{ settings.author }}</small>
+				<small>{{ settings.themes.typemill.authorIntro }}: {{ metatabs.meta.author|default(settings.author) }}</small>
 			{% endif %}
 			{% if settings.themes.typemill.modifiedPosition.top %}
-				<small>{{ settings.themes.typemill.modifiedText }}: {{ metatabs.meta.modified|date(settings.themes.typemill.modifiedFormat) }}</small>
+				<small>{{ settings.themes.typemill.modifiedText }}: {{ published|date(settings.themes.typemill.modifiedFormat)  }}</small>
 			{% endif %}
 			{% if settings.themes.typemill.socialPosition.top %}
 				<div id="share-icons" class="share-icons hide">
@@ -34,10 +36,10 @@
 	{% if (settings.themes.typemill.socialPosition.bottom or settings.themes.typemill.modifiedPosition.bottom or settings.themes.typemill.authorPosition.bottom or settings.themes.typemill.gitPosition.bottom) %}	
 		<div class="meta-info">
 			{% if settings.themes.typemill.authorPosition.bottom %}
-				<small>{{ settings.themes.typemill.authorIntro }}: {{ settings.author }}</small>
+				<small>{{ settings.themes.typemill.authorIntro }}: {{ metatabs.meta.author|default(settings.author) }}</small>
 			{% endif %}
 			{% if settings.themes.typemill.modifiedPosition.bottom %}
-				<small>{{ settings.themes.typemill.modifiedText }}: {{ metatabs.meta.modified|date(settings.themes.typemill.modifiedFormat) }}</small>
+				<small>{{ settings.themes.typemill.modifiedText }}: {{ published|date(settings.themes.typemill.modifiedFormat)  }}</small>
 			{% endif %}
 			{% if settings.themes.typemill.socialPosition.bottom %}
 				<div id="share-icons-bottom" class="share-icons hide">

+ 1 - 0
themes/typemill/partials/layout.twig

@@ -69,6 +69,7 @@
 			<script src="{{ base_url }}/themes/typemill/js/script.js"></script>
 			<script src="{{ base_url }}/system/author/js/lazy-video.js?20190602"></script>
 			<script>typemillUtilities.start();</script>
+
 			{{ assets.renderJS() }}
 		
 		{% endblock %}		

+ 1 - 1
themes/typemill/typemill.yaml

@@ -1,5 +1,5 @@
 name: Typemill Theme
-version: 1.2.0
+version: 1.2.1
 description: The standard theme for Typemill. Responsive, minimal and without any dependencies. It uses the system fonts Calibri and Helvetica. No JavaScript is used. 
 author: Sebastian Schürmanns
 homepage: https://typemill.net