Version 1.3.6
This commit is contained in:
parent
2abef9bc72
commit
a20adbadf6
16 changed files with 260 additions and 20 deletions
8
content/.yaml
Normal file
8
content/.yaml
Normal 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'
|
13
media/files/neue-datei.tx6t
Normal file
13
media/files/neue-datei.tx6t
Normal 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
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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(".","..")))
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
136
system/author/js/vue-shared.js
Normal file
136
system/author/js/vue-shared.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
|
@ -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() }}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()) }}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue