Version 1.3.6

This commit is contained in:
trendschau 2020-04-30 08:48:29 +02:00
parent 2abef9bc72
commit a20adbadf6
16 changed files with 260 additions and 20 deletions

8
content/.yaml Normal file
View file

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

View file

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

View file

@ -84,9 +84,9 @@ class ArticleApiController extends ContentController
# dispatch event
$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
{

View file

@ -108,6 +108,11 @@ class MediaApiController extends ContentController
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]);
}

View file

@ -272,10 +272,10 @@ class MetaApiController extends ContentController
}
# add the new/edited metadata
$meta[$tab] = $metaInput;
$metaPage[$tab] = $metaInput;
# 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)
{

View file

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

View file

@ -342,7 +342,7 @@ class Validation
*/
public function objectField($fieldName, $fieldValue, $objectName, $fieldDefinitions, $skiprequired = NULL)
{
{
$v = new Validator(array($fieldName => $fieldValue));
if(isset($fieldDefinitions['required']) && !$skiprequired)
@ -412,8 +412,16 @@ class Validation
# $v->rule('regex', $fieldName, '/^[\pL0-9_ \-\.\?\!\/\:]*$/u');
break;
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;
case "paragraph":
$v->rule('noHTML', $fieldName);
@ -422,9 +430,13 @@ class Validation
case "password":
$v->rule('lengthMax', $fieldName, 100);
break;
case "image":
$v->rule('noHTML', $fieldName);
$v->rule('lengthMax', $fieldName, 1000);
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);
}
@ -435,6 +447,20 @@ class Validation
* @param obj $v the validation object.
* @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)
{

View file

@ -141,7 +141,7 @@ a.tm-download::before{
width: 30px;
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-weight: 900;
border: 2px solid #e0474c;

View file

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

View file

@ -2,12 +2,12 @@ const FormBus = new Vue();
Vue.filter('translate', function (value) {
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){
return value + '?'
return value
} 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', {
props: ['class', 'id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
data: function () {
return {
textareaclass: ''
}
},
template: '<div class="large">' +
'<label>{{ label|translate }}</label>' +
'<textarea ' +
'<textarea rows="8" ' +
' :id="id"' +
' :class="textareaclass"' +
' :readonly="readonly"' +
' :required="required"' +
' :disabled="disabled"' +
' :name="name"' +
' :placeholder="placeholder"' +
' :value="value"' +
' :value="formatValue(value)"' +
' @input="update($event, name)"></textarea>' +
'<span v-if="errors[name]" class="error">{{ errors[name] }}</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});
},
formatValue: function(value)
{
if(value !== null && typeof value === 'object')
{
this.textareaclass = 'codearea';
return JSON.stringify(value, undefined, 4);
}
return value;
},
},
})

View file

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

View file

@ -216,6 +216,7 @@
<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/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>
{{ assets.renderJS() }}

View file

@ -11,6 +11,13 @@ meta:
size: 160
class: large
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:
type: text
label: author

View file

@ -9,6 +9,11 @@
<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' %}
{{ markdown(field.getContent()) }}

View file

@ -605,7 +605,7 @@ a.tm-download::before{
width: 30px;
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-weight: 900;
border: 2px solid #e0474c;

View file

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