trendschau 5 éve
szülő
commit
a20adbadf6

+ 8 - 0
content/.yaml

@@ -0,0 +1,8 @@
+meta:
+    title: Typemill
+    description: 'Typemill is a user-friendly and lightweight open source CMS for publishing text-works like prose, lyrics, manuals, documentations, studies and more. Just download and start.'
+    author: 'Sebastian Schürmanns'
+    created: '2020-04-27'
+    time: 14-20-18
+    navtitle: null
+    modified: '2020-04-27'

+ 13 - 0
media/files/neue-datei.tx6t

@@ -0,0 +1,13 @@
+Hallo Frau Zarth, 
+
+vielleicht sagt Ihnen als Leiterin Com & Tech die Fachseite CMSstash.de etwas? Wir behandeln ausschließlich CMS und bieten seit letzter Woche auch ein Dienstleisterverzeichnis an. Ist das interessant für Nexum als Tech-Partner von Magnolia, Typo3 und Co? 
+
+Herzliche Grüße
+
+
+Hallo Herr Timm,
+
+ich kenne unitb vor allem als Spezialist für AEM, Magnolia oder Drupal. Vielleicht ist für Ihre Agentur die wachsende Zielgruppe von CMSstash interessant?  Wir bieten seit letzter Woche ein Dienstleisterverzeichnis an, in dem sich CMS-Experten präsentieren können.
+
+Herzliche Grüße
+octet-stream

+ 2 - 2
system/Controllers/ArticleApiController.php

@@ -84,9 +84,9 @@ class ArticleApiController extends ContentController
 
 
 			# dispatch event
 			# dispatch event
 			$page = ['content' => $this->content, 'meta' => $meta, 'item' => $this->item];
 			$page = ['content' => $this->content, 'meta' => $meta, 'item' => $this->item];
-			$this->c->dispatcher->dispatch('onPagePublished', new OnPagePublished($page));
+			$page = $this->c->dispatcher->dispatch('onPagePublished', new OnPagePublished($page))->getData();
 
 
-			return $response->withJson(['success' => true, 'meta' => $meta], 200);
+			return $response->withJson(['success' => true, 'meta' => $page['meta']], 200);
 		}
 		}
 		else
 		else
 		{
 		{

+ 5 - 0
system/Controllers/MediaApiController.php

@@ -108,6 +108,11 @@ class MediaApiController extends ContentController
 
 
 		if($imageProcessor->createImage($this->params['image'], $this->params['name'], $this->settings['images']))
 		if($imageProcessor->createImage($this->params['image'], $this->params['name'], $this->settings['images']))
 		{
 		{
+			# publish image directly, used for example by image field for meta-tabs
+			if($this->params['publish'])
+			{
+				$imageProcessor->publishImage();
+			}
 			return $response->withJson(['name' => 'media/live/' . $imageProcessor->getFullName(),'errors' => false]);	
 			return $response->withJson(['name' => 'media/live/' . $imageProcessor->getFullName(),'errors' => false]);	
 		}
 		}
 
 

+ 2 - 2
system/Controllers/MetaApiController.php

@@ -272,10 +272,10 @@ class MetaApiController extends ContentController
 		}
 		}
 
 
 		# add the new/edited metadata
 		# add the new/edited metadata
-		$meta[$tab] = $metaInput;
+		$metaPage[$tab] = $metaInput;
 
 
 		# store the metadata
 		# store the metadata
-		$writeMeta->updateYaml($this->settings['contentFolder'], $this->item->pathWithoutType . '.yaml', $meta);
+		$writeMeta->updateYaml($this->settings['contentFolder'], $this->item->pathWithoutType . '.yaml', $metaPage);
 
 
 		if($structure)
 		if($structure)
 		{
 		{

+ 2 - 0
system/Models/ProcessImage.php

@@ -333,6 +333,8 @@ class ProcessImage extends ProcessAssets
 		# generate images from live folder to 'tmthumbs'
 		# generate images from live folder to 'tmthumbs'
 		$liveImages 	= scandir($this->liveFolder);
 		$liveImages 	= scandir($this->liveFolder);
 
 
+		$result = false;
+
 		foreach ($liveImages as $key => $name)
 		foreach ($liveImages as $key => $name)
 		{
 		{
 			if (!in_array($name, array(".","..")))
 			if (!in_array($name, array(".","..")))

+ 30 - 4
system/Models/Validation.php

@@ -342,7 +342,7 @@ class Validation
 	*/
 	*/
 	
 	
 	public function objectField($fieldName, $fieldValue, $objectName, $fieldDefinitions, $skiprequired = NULL)
 	public function objectField($fieldName, $fieldValue, $objectName, $fieldDefinitions, $skiprequired = NULL)
-	{	
+	{
 		$v = new Validator(array($fieldName => $fieldValue));
 		$v = new Validator(array($fieldName => $fieldValue));
 		
 		
 		if(isset($fieldDefinitions['required']) && !$skiprequired)
 		if(isset($fieldDefinitions['required']) && !$skiprequired)
@@ -412,8 +412,16 @@ class Validation
 #				$v->rule('regex', $fieldName, '/^[\pL0-9_ \-\.\?\!\/\:]*$/u');
 #				$v->rule('regex', $fieldName, '/^[\pL0-9_ \-\.\?\!\/\:]*$/u');
 				break;
 				break;
 			case "textarea":
 			case "textarea":
-				$v->rule('noHTML', $fieldName);
-				$v->rule('lengthMax', $fieldName, 1000);
+				# it understands array, json, yaml
+				if(is_array($fieldValue))
+				{
+					$v = $this->checkArray($fieldValue, $v);
+				}
+				else
+				{
+					$v->rule('noHTML', $fieldName);
+					$v->rule('lengthMax', $fieldName, 1000);
+				}
 				break;
 				break;
 			case "paragraph":
 			case "paragraph":
 				$v->rule('noHTML', $fieldName);
 				$v->rule('noHTML', $fieldName);
@@ -422,9 +430,13 @@ class Validation
 			case "password":
 			case "password":
 				$v->rule('lengthMax', $fieldName, 100);
 				$v->rule('lengthMax', $fieldName, 100);
 				break;
 				break;
+			case "image":
+				$v->rule('noHTML', $fieldName);
+				$v->rule('lengthMax', $fieldName, 1000);
+				break;
 			default:
 			default:
 				$v->rule('lengthMax', $fieldName, 1000);
 				$v->rule('lengthMax', $fieldName, 1000);
-				$v->rule('regex', $fieldName, '/^[\pL0-9_ \-]*$/u');		
+				$v->rule('regex', $fieldName, '/^[\pL0-9_ \-]*$/u');
 		}
 		}
 		return $this->validationResult($v, $objectName);
 		return $this->validationResult($v, $objectName);
 	}
 	}
@@ -435,6 +447,20 @@ class Validation
 	* @param obj $v the validation object.
 	* @param obj $v the validation object.
 	* @return bool
 	* @return bool
 	*/
 	*/
+
+	public function checkArray($arrayvalues, $v)
+	{		
+		foreach($arrayvalues as $key => $value)
+		{
+			if(is_array($value))
+			{
+				$this->checkArray($value, $v);
+			}
+			$v->rule('noHTML', $value);
+			$v->rule('lengthMax', $value, 1000);
+		}
+		return $v;
+	}
 	
 	
 	public function validationResult($v, $name = false)
 	public function validationResult($v, $name = false)
 	{
 	{

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

@@ -141,7 +141,7 @@ a.tm-download::before{
 	width: 30px;
 	width: 30px;
 	height: 30px;
 	height: 30px;
 	line-height: 30px;
 	line-height: 30px;
-	font-family: "Comic Sans MS",cursive,sans-serif;
+	font-family: Calibri, "Segoe UI", Roboto, Courier, Helvetica, -apple-system, BlinkMacSystemFont, sans-serif, Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
 	font-size: 1.3em;
 	font-size: 1.3em;
 	font-weight: 900;	
 	font-weight: 900;	
 	border: 2px solid #e0474c;
 	border: 2px solid #e0474c;

+ 4 - 4
system/author/js/vue-blox.js

@@ -2073,7 +2073,7 @@ const medialib = Vue.component('medialib', {
 
 
 				this.$parent.showmedialib = false;
 				this.$parent.showmedialib = false;
 
 
-				this.$parent.updatemarkdown(imgmarkdown);
+				this.$parent.updatemarkdown(imgmarkdown, image.src_live);
 			}
 			}
 			if(this.parentcomponent == 'files')
 			if(this.parentcomponent == 'files')
 			{
 			{
@@ -2084,7 +2084,7 @@ const medialib = Vue.component('medialib', {
 
 
 				this.$parent.showmedialib = false;
 				this.$parent.showmedialib = false;
 
 
-				this.$parent.updatemarkdown(filemarkdown);
+				this.$parent.updatemarkdown(filemarkdown, image.src_live);
 			}
 			}
 		},
 		},
 		selectFile: function(file)
 		selectFile: function(file)
@@ -2106,7 +2106,7 @@ const medialib = Vue.component('medialib', {
 
 
 				this.$parent.showmedialib = false;
 				this.$parent.showmedialib = false;
 
 
-				this.$parent.updatemarkdown(imgmarkdown);				
+				this.$parent.updatemarkdown(imgmarkdown, file.url);
 			}
 			}
 			if(this.parentcomponent == 'files')
 			if(this.parentcomponent == 'files')
 			{
 			{
@@ -2117,7 +2117,7 @@ const medialib = Vue.component('medialib', {
 				this.$parent.filemeta = true;
 				this.$parent.filemeta = true;
 				this.$parent.filetitle = file.info.filename + ' (' + file.info.extension.toUpperCase() + ')';
 				this.$parent.filetitle = file.info.filename + ' (' + file.info.extension.toUpperCase() + ')';
 
 
-				this.$parent.updatemarkdown(filemarkdown);
+				this.$parent.updatemarkdown(filemarkdown, file.url);
 			}
 			}
 			this.showFiles();
 			this.showFiles();
 		},		
 		},		

+ 39 - 6
system/author/js/vue-meta.js

@@ -2,12 +2,12 @@ const FormBus = new Vue();
 
 
 Vue.filter('translate', function (value) {
 Vue.filter('translate', function (value) {
   if (!value) return ''
   if (!value) return ''
-  value = value.replace(/[ ]/g,"_").replace(/[.]/g, "_").replace(/[-]/g, "_").replace(/[,]/g,"_").replace(/[(]/g,"_").replace(/[)]/g,"_").toUpperCase()
-  translated_string = labels[value]
+  transvalue = value.replace(/[ ]/g,"_").replace(/[.]/g, "_").replace(/[-]/g, "_").replace(/[,]/g,"_").replace(/[(]/g,"_").replace(/[)]/g,"_").toUpperCase()
+  translated_string = labels[transvalue]
   if(!translated_string || translated_string.length === 0){
   if(!translated_string || translated_string.length === 0){
-    return value + '?'
+    return value
   } else {
   } else {
-    return labels[value]
+    return labels[transvalue]
   }
   }
 })
 })
 
 
@@ -37,18 +37,42 @@ Vue.component('component-text', {
 	},
 	},
 })
 })
 
 
+Vue.component('component-hidden', {
+	props: ['class', 'id', 'maxlength', 'required', 'disabled', 'name', 'type', 'value', 'errors'],
+	template: '<div class="hidden">' +
+				'<input type="hidden"' + 
+					' :id="id"' +
+					' :maxlength="maxlength"' +
+					' :name="name"' +
+					' :value="value"' +
+					'@input="update($event, name)">' +
+			  '</div>',
+	methods: {
+		update: function($event, name)
+		{
+			FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
+		},
+	},
+})
+
 Vue.component('component-textarea', {
 Vue.component('component-textarea', {
 	props: ['class', 'id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
 	props: ['class', 'id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
+	data: function () {
+		return {
+			textareaclass: ''
+		 }
+	},
 	template: '<div class="large">' +
 	template: '<div class="large">' +
 				'<label>{{ label|translate }}</label>' +
 				'<label>{{ label|translate }}</label>' +
-				'<textarea ' +
+				'<textarea rows="8" ' +
 					' :id="id"' +
 					' :id="id"' +
+					' :class="textareaclass"' +
 					' :readonly="readonly"' +
 					' :readonly="readonly"' +
 					' :required="required"' +  
 					' :required="required"' +  
 					' :disabled="disabled"' +  
 					' :disabled="disabled"' +  
 					' :name="name"' +
 					' :name="name"' +
 					' :placeholder="placeholder"' +
 					' :placeholder="placeholder"' +
-					' :value="value"' +
+					' :value="formatValue(value)"' +
 					' @input="update($event, name)"></textarea>' +
 					' @input="update($event, name)"></textarea>' +
 			  	'<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|translate }}</small></span>' +
 			  	'<span v-else class="fielddescription"><small>{{ description|translate }}</small></span>' +
@@ -58,6 +82,15 @@ Vue.component('component-textarea', {
 		{
 		{
 			FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
 			FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
 		},
 		},
+		formatValue: function(value)
+		{
+			if(value !== null && typeof value === 'object')
+			{
+				this.textareaclass = 'codearea';
+				return JSON.stringify(value, undefined, 4);
+			}
+			return value;
+		},
 	},
 	},
 })
 })
 
 

+ 136 - 0
system/author/js/vue-shared.js

@@ -0,0 +1,136 @@
+Vue.component('component-image', {
+	props: ['class', 'id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
+	template: '<div class="large">' +
+				'<label>{{ label|translate }}</label>' +
+				'<div class="flex flex-wrap item-start">' +
+					'<div class="w-50">' +
+						'<div class="w6 h6 bg-black-40 dtc v-mid bg-chess">' +
+							'<img :src="imgpreview" class="mw6 max-h6 dt center">' +
+						'</div>' +
+					'</div>' +
+					'<div class="w-50 ph3 lh-copy f6 relative">' +
+						'<div class="relative dib w-100">' +
+							'<input class="absolute o-0 w-100 top-0 z-1 pointer" type="file" name="image" accept="image/*" @change="onFileChange( $event )" /> ' +
+							'<p class="relative w-100 bn br1 bg-tm-green white pa3 ma0 tc"><svg class="icon icon-upload baseline"><use xlink:href="#icon-upload"></use></svg> upload an image</p>'+
+						'</div>' +
+						'<div class="dib w-100 mt3">' +
+							'<button class="w-100 pointer bn br1 bg-tm-green white pa3 ma0 tc" @click.prevent="openmedialib()"><svg class="icon icon-image baseline"><use xlink:href="#icon-image"></use></svg> select from medialib</button>' +
+						'</div>' +
+						'<div class="dib w-100 mt3">' +
+							'<label>Image URL (read only)</label>' +
+							'<div class="flex">' +
+								'<button @click.prevent="deleteImage()" class="w-10 bg-tm-gray bn hover-bg-tm-red hover-white">x</button>' +
+								'<input class="w-90" type="text"' + 
+									' :id="id"' +
+									' :maxlength="maxlength"' +
+									' readonly="readonly"' +
+									' :hidden="hidden"' +
+									' :required="required"' +
+									' :disabled="disabled"' +
+									' :name="name"' +
+									' :placeholder="placeholder"' +
+									' :value="value"' +
+									'@input="update($event, name)">' +
+							'</div>' +
+						'</div>' +
+					  	'<div v-if="description" class="w-100 dib"><p>{{ description|translate }}</p></div>' +
+					  	'<div v-if="errors[name]" class="error">{{ errors[name] }}</div>' +
+					'</div>' +
+				'</div>' +
+				'<transition name="fade-editor">' +
+					'<div v-if="showmedialib" class="modalWindow">' +
+						'<medialib parentcomponent="images"></medialib>' + 
+					'</div>' +
+				'</transition>' +
+			  '</div>',
+	data: function(){
+		return {
+			maxsize: 5, // megabyte
+			imgpreview: false,
+			showmedialib: false,
+			load: false,
+		}
+	},
+	mounted: function(){
+		this.imgpreview = this.value;
+	},
+	methods: {
+		update: function(value)
+		{
+			FormBus.$emit('forminput', {'name' : this.name, 'value' : value});
+		},
+		updatemarkdown: function(markdown, url)
+		{
+			/* is called from child component medialib */
+			this.update(url);
+		},
+		deleteImage: function()
+		{
+			this.imgpreview = false;
+			this.update('');
+		},
+		openmedialib: function()
+		{
+			this.showmedialib = true;
+		},
+		onFileChange: function( e )
+		{
+			if(e.target.files.length > 0)
+			{
+				let imageFile = e.target.files[0];
+				let size = imageFile.size / 1024 / 1024;
+				
+				if (!imageFile.type.match('image.*'))
+				{
+					publishController.errors.message = "Only images are allowed.";
+				} 
+				else if (size > this.maxsize)
+				{
+					publishController.errors.message = "The maximal size of images is " + this.maxsize + " MB";
+				}
+				else
+				{
+					sharedself = this;
+					
+					let reader = new FileReader();
+					reader.readAsDataURL(imageFile);
+					reader.onload = function(e) 
+					{
+						sharedself.imgpreview = e.target.result;
+						
+						/* load image to server */
+						var url = sharedself.$root.$data.root + '/api/v1/image';
+						
+						var params = {
+							'url':				document.getElementById("path").value,
+							'image':			e.target.result,
+							'name': 			imageFile.name,
+							'publish':  		true,
+							'csrf_name': 		document.getElementById("csrf_name").value,
+							'csrf_value':		document.getElementById("csrf_value").value,
+						};
+
+						var method 	= 'POST';
+
+						sendJson(function(response, httpStatus)
+						{
+							if(response)
+							{
+								var result = JSON.parse(response);
+
+								if(result.errors)
+								{
+									publishController.errors.message = result.errors;
+								}
+								else
+								{
+									sharedself.update(result.name);
+								}
+							}
+						}, method, url, params);
+					}
+				}
+			}
+		}
+	},
+})

+ 1 - 0
system/author/layouts/layoutBlox.twig

@@ -216,6 +216,7 @@
 		<script src="{{ base_url }}/system/author/js/sortable.min.js?20200420"></script>
 		<script src="{{ base_url }}/system/author/js/sortable.min.js?20200420"></script>
 		<script src="{{ base_url }}/system/author/js/vuedraggable.umd.min.js?20200420"></script>
 		<script src="{{ base_url }}/system/author/js/vuedraggable.umd.min.js?20200420"></script>
 		<script src="{{ base_url }}/system/author/js/vue-navi.js?20200420"></script>
 		<script src="{{ base_url }}/system/author/js/vue-navi.js?20200420"></script>
+		<script src="{{ base_url }}/system/author/js/vue-shared.js?20200420"></script>
 		<script src="{{ base_url }}/system/author/js/vue-meta.js?20200420"></script>
 		<script src="{{ base_url }}/system/author/js/vue-meta.js?20200420"></script>
 
 
 		{{ assets.renderJS() }}
 		{{ assets.renderJS() }}

+ 7 - 0
system/author/metatabs.yaml

@@ -11,6 +11,13 @@ meta:
       size: 160
       size: 160
       class: large
       class: large
       description: If not filled, the description is extracted from content.
       description: If not filled, the description is extracted from content.
+    heroimage:
+      type: image
+      label: Hero Image
+      description: Maximum size for an image is 5 MB. Hero images are not supported by all themes.
+    heroimagealt:
+      type: text
+      label: Alternative Text for the hero image
     author:
     author:
       type: text
       type: text
       label: author
       label: author

+ 5 - 0
system/author/partials/fields.twig

@@ -9,6 +9,11 @@
 
 
 		<textarea id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]"{{field.getAttributeValues() }}{{ field.getAttributes() }}>{{ field.getContent() }}</textarea>
 		<textarea id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]"{{field.getAttributeValues() }}{{ field.getAttributes() }}>{{ field.getContent() }}</textarea>
 
 
+	{% elseif field.type == 'image' %}
+		<div class="imageupload dropbox">
+			<input id="{{itemName}}[{{ field.name }}]" class="input-file" name="{{itemName}}[{{ field.name }}]" type="file" accept="image/*"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
+			<p><svg class="icon icon-upload baseline"><use xlink:href="#icon-upload"></use></svg> upload an image</p>
+		</div>
 	{% elseif field.type == 'paragraph' %}
 	{% elseif field.type == 'paragraph' %}
 		
 		
 		{{ markdown(field.getContent()) }}	
 		{{ markdown(field.getContent()) }}	

+ 1 - 1
themes/typemill/css/style.css

@@ -605,7 +605,7 @@ a.tm-download::before{
 	width: 30px;
 	width: 30px;
 	height: 30px;
 	height: 30px;
 	line-height: 30px;
 	line-height: 30px;
-	font-family: "Comic Sans MS",cursive,sans-serif;
+	font-family: Calibri, "Segoe UI", Roboto, Courier, Helvetica, -apple-system, BlinkMacSystemFont, sans-serif, Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
 	font-size: 1.3em;
 	font-size: 1.3em;
 	font-weight: 900;	
 	font-weight: 900;	
 	border: 2px solid #e0474c;
 	border: 2px solid #e0474c;

+ 4 - 0
themes/typemill/typemill.yaml

@@ -27,6 +27,10 @@ forms:
       label: Different Design for Startpage
       label: Different Design for Startpage
       checkboxlabel: Activate Special Startpage-Design
       checkboxlabel: Activate Special Startpage-Design
 
 
+    test:
+      type: image
+      label: Image test
+
     coverlogo:
     coverlogo:
       type: checkbox
       type: checkbox
       label: Logo on startpage
       label: Logo on startpage