mirror of
https://github.com/typemill/typemill.git
synced 2025-04-20 20:11:53 +02:00
Version 1.3.0: Meta-Information
This commit is contained in:
parent
ead51d6540
commit
7d41c4894c
23
.gitignore
vendored
23
.gitignore
vendored
@ -1,22 +1,13 @@
|
||||
cache
|
||||
plugins/admin
|
||||
plugins/contactform
|
||||
plugins/demo
|
||||
plugins/disqus
|
||||
plugins/download
|
||||
plugins/finalwords
|
||||
plugins/hyer
|
||||
plugins/joblistings
|
||||
plugins/landingpage
|
||||
plugins/mail
|
||||
plugins/newsletter
|
||||
plugins/textadds
|
||||
plugins/version
|
||||
content/index.yaml
|
||||
content/00-Welcome/index.yaml
|
||||
content/00-Welcome/00-Setup.yaml
|
||||
content/00-Welcome/01-Write-Content.yaml
|
||||
content/00-Welcome/02-Get-Help.yaml
|
||||
content/00-Welcome/03-Markdown-Test.yaml
|
||||
settings/settings.yaml
|
||||
settings/formdata.yaml
|
||||
settings/users
|
||||
system/vendor
|
||||
themes/learn
|
||||
tests
|
||||
plugins/demo
|
||||
zips
|
||||
build.php
|
2
cache/lastCache.txt
vendored
2
cache/lastCache.txt
vendored
@ -1 +1 @@
|
||||
1574585614
|
||||
1577803430
|
@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Analytics;
|
||||
|
||||
use \Typemill\Plugin;
|
||||
|
||||
class Analytics extends Plugin
|
||||
{
|
||||
protected $settings;
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return array(
|
||||
'onSettingsLoaded' => 'onSettingsLoaded',
|
||||
'onTwigLoaded' => 'onTwigLoaded'
|
||||
);
|
||||
}
|
||||
|
||||
public function onSettingsLoaded($settings)
|
||||
{
|
||||
$this->settings = $settings->getData();
|
||||
}
|
||||
|
||||
public function onTwigLoaded()
|
||||
{
|
||||
/* get Twig Instance and add the cookieconsent template-folder to the path */
|
||||
$twig = $this->getTwig();
|
||||
$loader = $twig->getLoader();
|
||||
$loader->addPath(__DIR__ . '/templates');
|
||||
|
||||
$analyticSettings = $this->settings['settings']['plugins']['analytics'];
|
||||
|
||||
if(isset($analyticSettings['tool']))
|
||||
{
|
||||
/* fetch the template, render it with twig and add javascript with settings */
|
||||
if($analyticSettings['tool'] == 'piwik')
|
||||
{
|
||||
$this->addInlineJS($twig->fetch('/piwikanalytics.twig', $this->settings));
|
||||
}
|
||||
elseif($analyticSettings['tool'] == 'google')
|
||||
{
|
||||
$this->addJS('https://www.googletagmanager.com/gtag/js?id=' . $analyticSettings['google_id']);
|
||||
$this->addInlineJS($twig->fetch('/googleanalytics.twig', $analyticSettings));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
name: Analytics
|
||||
version: 1.0.0
|
||||
description: Integrate Piwik or Google Analytics Script
|
||||
author: Sebastian Schürmanns
|
||||
homepage: http://typemill.net
|
||||
licence: MIT
|
||||
|
||||
settings:
|
||||
tool: none
|
||||
|
||||
forms:
|
||||
fields:
|
||||
|
||||
tool:
|
||||
type: radio
|
||||
label: Choose Your Tool
|
||||
options:
|
||||
none: None
|
||||
piwik: Piwik
|
||||
google: Google Analytics
|
||||
|
||||
piwik_url:
|
||||
type: text
|
||||
label: Piwik URL
|
||||
help: Add the URL to your piwik installation without protocol like this: my-site.com.
|
||||
placeholder: 'url like my-piwik-installation.com'
|
||||
|
||||
piwik_id:
|
||||
type: number
|
||||
label: Piwik Site-ID
|
||||
help: You can find the id in Piwik under configuration and tracking code.
|
||||
placeholder: 'simple number like 8'
|
||||
|
||||
google_id:
|
||||
type: text
|
||||
label: Google Tracking ID
|
||||
help: You can find the tracking id in google under property. It starts with UA-
|
||||
placeholder: 'UA-12345-6'
|
@ -1,5 +0,0 @@
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', '{{ google_id }}');
|
@ -1,11 +0,0 @@
|
||||
var _paq = _paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
|
||||
(function(){
|
||||
var u="//{{ settings.plugins.analytics.piwik_url }}/";
|
||||
_paq.push(['setTrackerUrl', u+'piwik.php']);
|
||||
_paq.push(['setSiteId', '{{ settings.plugins.analytics.piwik_id }}']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\CookieConsent;
|
||||
|
||||
use \Typemill\Plugin;
|
||||
|
||||
class CookieConsent extends Plugin
|
||||
{
|
||||
protected $settings;
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return array(
|
||||
'onSettingsLoaded' => 'onSettingsLoaded',
|
||||
'onTwigLoaded' => 'onTwigLoaded'
|
||||
);
|
||||
}
|
||||
|
||||
public function onSettingsLoaded($settings)
|
||||
{
|
||||
$this->settings = $settings->getData();
|
||||
}
|
||||
|
||||
public function onTwigLoaded()
|
||||
{
|
||||
/* add external CSS and JavaScript */
|
||||
$this->addCSS('/cookieconsent/public/cookieconsent.min.css');
|
||||
$this->addJS('/cookieconsent/public/cookieconsent.min.js');
|
||||
|
||||
/* get Twig Instance and add the cookieconsent template-folder to the path */
|
||||
$twig = $this->getTwig();
|
||||
$loader = $twig->getLoader();
|
||||
$loader->addPath(__DIR__ . '/templates');
|
||||
|
||||
/* fetch the template, render it with twig and add it as inline-javascript */
|
||||
$this->addInlineJS($twig->fetch('/cookieconsent.twig', $this->settings));
|
||||
}
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
name: Cookie Consent
|
||||
version: 1.0.1
|
||||
description: Enables a cookie consent for websites
|
||||
author: Sebastian Schürmanns
|
||||
homepage: https://cookieconsent.insites.com/
|
||||
licence: MIT
|
||||
|
||||
settings:
|
||||
popup_background: '#70c1b3'
|
||||
popup_text: '#ffffff'
|
||||
button_background: '#66b0a3'
|
||||
button_text: '#ffffff'
|
||||
theme: 'edgeless'
|
||||
position: 'bottom'
|
||||
message: 'This website uses cookies to ensure you get the best experience on our website.'
|
||||
link: 'Learn More'
|
||||
href: 'https://cookiesandyou.com/'
|
||||
dismiss: 'Got It'
|
||||
|
||||
forms:
|
||||
fields:
|
||||
|
||||
theme:
|
||||
type: select
|
||||
label: Theme
|
||||
placeholder: 'Add name of theme'
|
||||
required: true
|
||||
options:
|
||||
edgeless: Edgeless
|
||||
block: Block
|
||||
classic: Classic
|
||||
|
||||
position:
|
||||
type: select
|
||||
label: Position of Cookie Banner
|
||||
options:
|
||||
bottom: Bottom
|
||||
top: Top
|
||||
bottom-left: Bottom left
|
||||
bottom-right: Bottom right
|
||||
|
||||
message:
|
||||
type: textarea
|
||||
label: Message
|
||||
placeholder: 'Message for cookie-popup'
|
||||
required: true
|
||||
|
||||
href:
|
||||
type: url
|
||||
label: Link to more informations
|
||||
placeholder: 'https://cookiesandyou.com/'
|
||||
required: true
|
||||
|
||||
link:
|
||||
type: text
|
||||
label: Label for Link
|
||||
placeholder: 'Link-Lable like More infos'
|
||||
required: true
|
||||
|
||||
dismiss:
|
||||
type: text
|
||||
label: Label for Button
|
||||
placeholder: 'Got it'
|
||||
required: true
|
||||
|
||||
popup_background:
|
||||
type: color
|
||||
label: Background Color of Popup
|
||||
placeholder: 'Add hex color value like #ffffff'
|
||||
required: true
|
||||
|
||||
popup_text:
|
||||
type: color
|
||||
label: Text Color of Popup
|
||||
placeholder: 'Add hex color value like #ffffff'
|
||||
required: true
|
||||
|
||||
button_background:
|
||||
type: color
|
||||
label: Background Color of Button
|
||||
placeholder: 'Add hex color value like #ffffff'
|
||||
required: true
|
||||
|
||||
button_text:
|
||||
type: color
|
||||
label: Text Color of Button
|
||||
placeholder: 'Add hex color value like #ffffff'
|
||||
required: true
|
@ -1,6 +0,0 @@
|
||||
.cc-window{opacity:1;transition:opacity 1s ease}.cc-window.cc-invisible{opacity:0}.cc-animate.cc-revoke{transition:transform 1s ease}.cc-animate.cc-revoke.cc-top{transform:translateY(-2em)}.cc-animate.cc-revoke.cc-bottom{transform:translateY(2em)}.cc-animate.cc-revoke.cc-active.cc-bottom,.cc-animate.cc-revoke.cc-active.cc-top,.cc-revoke:hover{transform:translateY(0)}.cc-grower{max-height:0;overflow:hidden;transition:max-height 1s}
|
||||
.cc-link,.cc-revoke:hover{text-decoration:underline}.cc-revoke,.cc-window{position:fixed;overflow:hidden;box-sizing:border-box;font-family:Helvetica,Calibri,Arial,sans-serif;font-size:16px;line-height:1.5em;display:-ms-flexbox;display:flex;-ms-flex-wrap:nowrap;flex-wrap:nowrap;z-index:9999}.cc-window.cc-static{position:static}.cc-window.cc-floating{padding:2em;max-width:24em;-ms-flex-direction:column;flex-direction:column}.cc-window.cc-banner{padding:1em 1.8em;width:100%;-ms-flex-direction:row;flex-direction:row}.cc-revoke{padding:.5em}.cc-header{font-size:18px;font-weight:700}.cc-btn,.cc-close,.cc-link,.cc-revoke{cursor:pointer}.cc-link{opacity:.8;display:inline-block;padding:.2em}.cc-link:hover{opacity:1}.cc-link:active,.cc-link:visited{color:initial}.cc-btn{display:block;padding:.4em .8em;font-size:.9em;font-weight:700;border-width:2px;border-style:solid;text-align:center;white-space:nowrap}.cc-highlight .cc-btn:first-child{background-color:transparent;border-color:transparent}.cc-highlight .cc-btn:first-child:focus,.cc-highlight .cc-btn:first-child:hover{background-color:transparent;text-decoration:underline}.cc-close{display:block;position:absolute;top:.5em;right:.5em;font-size:1.6em;opacity:.9;line-height:.75}.cc-close:focus,.cc-close:hover{opacity:1}
|
||||
.cc-revoke.cc-top{top:0;left:3em;border-bottom-left-radius:.5em;border-bottom-right-radius:.5em}.cc-revoke.cc-bottom{bottom:0;left:3em;border-top-left-radius:.5em;border-top-right-radius:.5em}.cc-revoke.cc-left{left:3em;right:unset}.cc-revoke.cc-right{right:3em;left:unset}.cc-top{top:1em}.cc-left{left:1em}.cc-right{right:1em}.cc-bottom{bottom:1em}.cc-floating>.cc-link{margin-bottom:1em}.cc-floating .cc-message{display:block;margin-bottom:1em}.cc-window.cc-floating .cc-compliance{-ms-flex:1 0 auto;flex:1 0 auto}.cc-window.cc-banner{-ms-flex-align:center;align-items:center}.cc-banner.cc-top{left:0;right:0;top:0}.cc-banner.cc-bottom{left:0;right:0;bottom:0}.cc-banner .cc-message{display:block;-ms-flex:1 1 auto;flex:1 1 auto;max-width:100%;margin-right:1em}.cc-compliance{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-line-pack:justify;align-content:space-between}.cc-floating .cc-compliance>.cc-btn{-ms-flex:1;flex:1}.cc-btn+.cc-btn{margin-left:.5em}
|
||||
@media print{.cc-revoke,.cc-window{display:none}}@media screen and (max-width:900px){.cc-btn{white-space:normal}}@media screen and (max-width:414px) and (orientation:portrait),screen and (max-width:736px) and (orientation:landscape){.cc-window.cc-top{top:0}.cc-window.cc-bottom{bottom:0}.cc-window.cc-banner,.cc-window.cc-floating,.cc-window.cc-left,.cc-window.cc-right{left:0;right:0}.cc-window.cc-banner{-ms-flex-direction:column;flex-direction:column}.cc-window.cc-banner .cc-compliance{-ms-flex:1 1 auto;flex:1 1 auto}.cc-window.cc-floating{max-width:none}.cc-window .cc-message{margin-bottom:1em}.cc-window.cc-banner{-ms-flex-align:unset;align-items:unset}.cc-window.cc-banner .cc-message{margin-right:0}}
|
||||
.cc-floating.cc-theme-classic{padding:1.2em;border-radius:5px}.cc-floating.cc-type-info.cc-theme-classic .cc-compliance{text-align:center;display:inline;-ms-flex:none;flex:none}.cc-theme-classic .cc-btn{border-radius:5px}.cc-theme-classic .cc-btn:last-child{min-width:140px}.cc-floating.cc-type-info.cc-theme-classic .cc-btn{display:inline-block}
|
||||
.cc-theme-edgeless.cc-window{padding:0}.cc-floating.cc-theme-edgeless .cc-message{margin:2em 2em 1.5em}.cc-banner.cc-theme-edgeless .cc-btn{margin:0;padding:.8em 1.8em;height:100%}.cc-banner.cc-theme-edgeless .cc-message{margin-left:1em}.cc-floating.cc-theme-edgeless .cc-btn+.cc-btn{margin-left:0}
|
File diff suppressed because one or more lines are too long
@ -1,22 +0,0 @@
|
||||
window.addEventListener("load", function(){
|
||||
window.cookieconsent.initialise({
|
||||
"palette": {
|
||||
"popup": {
|
||||
"background": "{{ settings.plugins.cookieconsent.popup_background }}",
|
||||
"text": "{{ settings.plugins.cookieconsent.popup_text }}"
|
||||
},
|
||||
"button": {
|
||||
"background": "{{ settings.plugins.cookieconsent.button_background }}",
|
||||
"text": "{{ settings.plugins.cookieconsent.button_text }}"
|
||||
}
|
||||
},
|
||||
"theme": "{{ settings.plugins.cookieconsent.theme }}",
|
||||
"position": "{{ settings.plugins.cookieconsent.position }}",
|
||||
"content": {
|
||||
"message": "{{ settings.plugins.cookieconsent.message }}",
|
||||
"dismiss": "{{ settings.plugins.cookieconsent.dismiss }}",
|
||||
"link": "{{ settings.plugins.cookieconsent.link }}",
|
||||
"href": "{{ settings.plugins.cookieconsent.href }}"
|
||||
}
|
||||
})
|
||||
});
|
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Highlight;
|
||||
|
||||
use \Typemill\Plugin;
|
||||
|
||||
class Highlight extends Plugin
|
||||
{
|
||||
protected $settings;
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return array(
|
||||
'onTwigLoaded' => 'onTwigLoaded'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function onTwigLoaded()
|
||||
{
|
||||
/* add external CSS and JavaScript */
|
||||
$this->addCSS('/highlight/public/default.css');
|
||||
$this->addJS('/highlight/public/highlight.pack.js');
|
||||
|
||||
/* initialize the script */
|
||||
$this->addInlineJS('hljs.initHighlightingOnLoad();');
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
name: Highlight
|
||||
version: 1.0.0
|
||||
description: Adds the famous javascript syntax highlighter.
|
||||
author: Sebastian Schürmanns
|
||||
homepage: https://highlightjs.org/
|
||||
licence: BSD
|
@ -1,24 +0,0 @@
|
||||
Copyright (c) 2006, Ivan Sagalaev
|
||||
All rights reserved.
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of highlight.js nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -1,99 +0,0 @@
|
||||
/*
|
||||
|
||||
Original highlight.js style (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
background: #F0F0F0;
|
||||
}
|
||||
|
||||
|
||||
/* Base color: saturation 0; */
|
||||
|
||||
.hljs,
|
||||
.hljs-subst {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.hljs-comment {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-attribute,
|
||||
.hljs-selector-tag,
|
||||
.hljs-meta-keyword,
|
||||
.hljs-doctag,
|
||||
.hljs-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
/* User color: hue: 0 */
|
||||
|
||||
.hljs-type,
|
||||
.hljs-string,
|
||||
.hljs-number,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-quote,
|
||||
.hljs-template-tag,
|
||||
.hljs-deletion {
|
||||
color: #880000;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #880000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-regexp,
|
||||
.hljs-symbol,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-link,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #BC6060;
|
||||
}
|
||||
|
||||
|
||||
/* Language color: hue: 90; */
|
||||
|
||||
.hljs-literal {
|
||||
color: #78A960;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-bullet,
|
||||
.hljs-code,
|
||||
.hljs-addition {
|
||||
color: #397300;
|
||||
}
|
||||
|
||||
|
||||
/* Meta color: hue: 200 */
|
||||
|
||||
.hljs-meta {
|
||||
color: #1f7199;
|
||||
}
|
||||
|
||||
.hljs-meta-string {
|
||||
color: #4d99bf;
|
||||
}
|
||||
|
||||
|
||||
/* Misc effects */
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Math;
|
||||
|
||||
use \Typemill\Plugin;
|
||||
|
||||
class Math extends Plugin
|
||||
{
|
||||
protected $settings;
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return array(
|
||||
'onSettingsLoaded' => 'onSettingsLoaded',
|
||||
'onTwigLoaded' => 'onTwigLoaded'
|
||||
);
|
||||
}
|
||||
|
||||
public function onSettingsLoaded($settings)
|
||||
{
|
||||
$this->settings = $settings->getData();
|
||||
}
|
||||
|
||||
public function onTwigLoaded()
|
||||
{
|
||||
$mathSettings = $this->settings['settings']['plugins']['math'];
|
||||
|
||||
if($mathSettings['tool'] == 'mathjax')
|
||||
{
|
||||
/* add external CSS and JavaScript */
|
||||
$this->addJS('https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/latest.js?config=TeX-MML-AM_CHTML');
|
||||
}
|
||||
|
||||
if($mathSettings['tool'] == 'katex')
|
||||
{
|
||||
$this->addJS('/math/public/katex.min.js');
|
||||
$this->addJS('/math/public/auto-render.min.js');
|
||||
$this->addCSS('/math/public/katex.min.css');
|
||||
|
||||
/* initialize autorendering of page only in frontend */
|
||||
if (strpos($this->getPath(), 'tm/content') === false)
|
||||
{
|
||||
$this->addInlineJs('renderMathInElement(document.body);');
|
||||
}
|
||||
}
|
||||
|
||||
# add math to the blox editor configuration
|
||||
|
||||
$this->addEditorJS('/math/public/math.js');
|
||||
$this->addSvgSymbol('<symbol id="icon-omega" viewBox="0 0 32 32">
|
||||
<title>omega</title>
|
||||
<path d="M22 28h8l2-4v8h-12v-6.694c4.097-1.765 7-6.161 7-11.306 0-6.701-4.925-11.946-11-11.946s-11 5.245-11 11.946c0 5.144 2.903 9.541 7 11.306v6.694h-12v-8l2 4h8v-1.018c-5.863-2.077-10-7.106-10-12.982 0-7.732 7.163-14 16-14s16 6.268 16 14c0 5.875-4.137 10.905-10 12.982v1.018z"></path>
|
||||
</symbol>');
|
||||
}
|
||||
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
name: Math
|
||||
version: 1.1.0
|
||||
description: Adds support for katex and mathjax.
|
||||
author: Sebastian Schürmanns
|
||||
homepage: https://mathjax.org/
|
||||
licence: Apache 2.0 / MIT
|
||||
|
||||
settings:
|
||||
tool: none
|
||||
|
||||
forms:
|
||||
fields:
|
||||
|
||||
tool:
|
||||
type: radio
|
||||
label: Choose Your Tool
|
||||
options:
|
||||
none: None
|
||||
katex: KaTex
|
||||
mathjax: MathJax
|
@ -1,140 +0,0 @@
|
||||
# [<img src="https://khan.github.io/KaTeX/katex-logo.svg" width="130" alt="KaTeX">](https://khan.github.io/KaTeX/)
|
||||
[](https://travis-ci.org/Khan/KaTeX)
|
||||
[](https://codecov.io/gh/Khan/KaTeX)
|
||||
[](https://gitter.im/Khan/KaTeX?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web.
|
||||
|
||||
* **Fast:** KaTeX renders its math synchronously and doesn't need to reflow the page. See how it compares to a competitor in [this speed test](http://www.intmath.com/cg5/katex-mathjax-comparison.php).
|
||||
* **Print quality:** KaTeX’s layout is based on Donald Knuth’s TeX, the gold standard for math typesetting.
|
||||
* **Self contained:** KaTeX has no dependencies and can easily be bundled with your website resources.
|
||||
* **Server side rendering:** KaTeX produces the same output regardless of browser or environment, so you can pre-render expressions using Node.js and send them as plain HTML.
|
||||
|
||||
KaTeX supports all major browsers, including Chrome, Safari, Firefox, Opera, Edge, and IE 9 - IE 11. More information can be found on the [list of supported commands](https://khan.github.io/KaTeX/function-support.html) and on the [wiki](https://github.com/khan/katex/wiki).
|
||||
|
||||
## Usage
|
||||
|
||||
You can [download KaTeX](https://github.com/khan/katex/releases) and host it on your server or include the `katex.min.js` and `katex.min.css` files on your page directly from a CDN:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/katex.min.css" integrity="sha384-VEnyslhHLHiYPca9KFkBB3CMeslnM9CzwjxsEbZTeA21JBm7tdLwKoZmCt3cZTYD" crossorigin="anonymous">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/katex.min.js" integrity="sha384-O4hpKqcplNCe+jLuBVEXC10Rn1QEqAmX98lKAIFBEDxZI0a+6Z2w2n8AEtQbR4CD" crossorigin="anonymous"></script>
|
||||
```
|
||||
|
||||
#### In-browser rendering
|
||||
|
||||
Call `katex.render` with a TeX expression and a DOM element to render into:
|
||||
|
||||
```js
|
||||
katex.render("c = \\pm\\sqrt{a^2 + b^2}", element);
|
||||
```
|
||||
|
||||
If KaTeX can't parse the expression, it throws a `katex.ParseError` error.
|
||||
|
||||
#### Server side rendering or rendering to a string
|
||||
|
||||
To generate HTML on the server or to generate an HTML string of the rendered math, you can use `katex.renderToString`:
|
||||
|
||||
```js
|
||||
var html = katex.renderToString("c = \\pm\\sqrt{a^2 + b^2}");
|
||||
// '<span class="katex">...</span>'
|
||||
```
|
||||
|
||||
Make sure to include the CSS and font files, but there is no need to include the JavaScript. Like `render`, `renderToString` throws if it can't parse the expression.
|
||||
|
||||
#### Security
|
||||
|
||||
Any HTML generated by KaTeX *should* be safe from `<script>` or other code
|
||||
injection attacks.
|
||||
(See `maxSize` below for preventing large width/height visual affronts.)
|
||||
Of course, it is always a good idea to sanitize the HTML, though you will need
|
||||
a rather generous whitelist (including some of SVG and MathML) to support
|
||||
all of KaTeX.
|
||||
|
||||
#### Handling errors
|
||||
|
||||
If KaTeX encounters an error (invalid or unsupported LaTeX), then it will
|
||||
throw an exception of type `katex.ParseError`. The message in this error
|
||||
includes some of the LaTeX source code, so needs to be escaped if you want
|
||||
to render it to HTML. In particular, you should convert `&`, `<`, `>`
|
||||
characters to `&`, `<`, `>` (e.g., using `_.escape`)
|
||||
before including either LaTeX source code or exception messages in your
|
||||
HTML/DOM. (Failure to escape in this way makes a `<script>` injection
|
||||
attack possible if your LaTeX source is untrusted.)
|
||||
|
||||
#### Rendering options
|
||||
|
||||
You can provide an object of options as the last argument to `katex.render` and `katex.renderToString`. Available options are:
|
||||
|
||||
- `displayMode`: `boolean`. If `true` the math will be rendered in display mode, which will put the math in display style (so `\int` and `\sum` are large, for example), and will center the math on the page on its own line. If `false` the math will be rendered in inline mode. (default: `false`)
|
||||
- `throwOnError`: `boolean`. If `true`, KaTeX will throw a `ParseError` when it encounters an unsupported command. If `false`, KaTeX will render the unsupported command as text in the color given by `errorColor`. (default: `true`)
|
||||
- `errorColor`: `string`. A color string given in the format `"#XXX"` or `"#XXXXXX"`. This option determines the color which unsupported commands are rendered in. (default: `#cc0000`)
|
||||
- `macros`: `object`. A collection of custom macros. Each macro is a property with a name like `\name` (written `"\\name"` in JavaScript) which maps to a string that describes the expansion of the macro. Single-character keys can also be included in which case the character will be redefined as the given macro (similar to TeX active characters).
|
||||
- `colorIsTextColor`: `boolean`. If `true`, `\color` will work like LaTeX's `\textcolor`, and take two arguments (e.g., `\color{blue}{hello}`), which restores the old behavior of KaTeX (pre-0.8.0). If `false` (the default), `\color` will work like LaTeX's `\color`, and take one argument (e.g., `\color{blue}hello`). In both cases, `\textcolor` works as in LaTeX (e.g., `\textcolor{blue}{hello}`).
|
||||
- `maxSize`: `number`. If non-zero, all user-specified sizes, e.g. in `\rule{500em}{500em}`, will be capped to `maxSize` ems. Otherwise, users can make elements and spaces arbitrarily large (the default behavior).
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
katex.render("c = \\pm\\sqrt{a^2 + b^2}\\in\\RR", element, {
|
||||
displayMode: true,
|
||||
macros: {
|
||||
"\\RR": "\\mathbb{R}"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Automatic rendering of math on a page
|
||||
|
||||
Math on the page can be automatically rendered using the auto-render extension. See [the Auto-render README](contrib/auto-render/README.md) for more information.
|
||||
|
||||
#### Font size and lengths
|
||||
|
||||
By default, KaTeX math is rendered in a 1.21× larger font than the surrounding
|
||||
context, which makes super- and subscripts easier to read. You can control
|
||||
this using CSS, for example:
|
||||
|
||||
```css
|
||||
.katex { font-size: 1.1em; }
|
||||
```
|
||||
|
||||
KaTeX supports all TeX units, including absolute units like `cm` and `in`.
|
||||
Absolute units are currently scaled relative to the default TeX font size of
|
||||
10pt, so that `\kern1cm` produces the same results as `\kern2.845275em`.
|
||||
As a result, relative and absolute units are both uniformly scaled relative
|
||||
to LaTeX with a 10pt font; for example, the rectangle `\rule{1cm}{1em}` has
|
||||
the same aspect ratio in KaTeX as in LaTeX. However, because most browsers
|
||||
default to a larger font size, this typically means that a 1cm kern in KaTeX
|
||||
will appear larger than 1cm in browser units.
|
||||
|
||||
### Common Issues
|
||||
- Many Markdown preprocessors, such as the one that Jekyll and GitHub Pages use,
|
||||
have a "smart quotes" feature. This changes `'` to `’` which is an issue for
|
||||
math containing primes, e.g. `f'`. This can be worked around by defining a
|
||||
single character macro which changes them back, e.g. `{"’", "'"}`.
|
||||
- KaTeX follows LaTeX's rendering of `aligned` and `matrix` environments unlike
|
||||
MathJax. When displaying fractions one above another in these vertical
|
||||
layouts there may not be enough space between rows for people who are used to
|
||||
MathJax's rendering. The distance between rows can be adjusted by using
|
||||
`\\[0.1em]` instead of the standard line separator distance.
|
||||
- KaTeX does not support the `align` environment because LaTeX doesn't support
|
||||
`align` in math mode. The `aligned` environment offers the same functionality
|
||||
but in math mode, so use that instead or define a macro that maps `align` to
|
||||
`aligned`.
|
||||
|
||||
## Libraries
|
||||
|
||||
### Angular2+
|
||||
- [ng-katex](https://github.com/garciparedes/ng-katex) Angular module to write beautiful math expressions with TeX syntax boosted by KaTeX library
|
||||
|
||||
### Ruby
|
||||
|
||||
- [katex-ruby](https://github.com/glebm/katex-ruby) Provides server-side rendering and integration with popular Ruby web frameworks (Rails, Hanami, and anything that uses Sprockets).
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
## License
|
||||
|
||||
KaTeX is licensed under the [MIT License](http://opensource.org/licenses/MIT).
|
1
plugins/math/public/auto-render.min.js
vendored
1
plugins/math/public/auto-render.min.js
vendored
@ -1 +0,0 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,function(e){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=1)}([function(t,r){t.exports=e},function(e,t,r){"use strict";r.r(t);var n=r(0),o=r.n(n),a=function(e,t,r){for(var n=r,o=0,a=e.length;n<t.length;){var i=t[n];if(o<=0&&t.slice(n,n+a)===e)return n;"\\"===i?n++:"{"===i?o++:"}"===i&&o--,n++}return-1},i=function(e,t,r,n){for(var o=[],i=0;i<e.length;i++)if("text"===e[i].type){var l=e[i].data,d=!0,s=0,f=void 0;for(-1!==(f=l.indexOf(t))&&(s=f,o.push({type:"text",data:l.slice(0,s)}),d=!1);;){if(d){if(-1===(f=l.indexOf(t,s)))break;o.push({type:"text",data:l.slice(s,f)}),s=f}else{if(-1===(f=a(r,l,s+t.length)))break;o.push({type:"math",data:l.slice(s+t.length,f),rawData:l.slice(s,f+r.length),display:n}),s=f+r.length}d=!d}o.push({type:"text",data:l.slice(s)})}else o.push(e[i]);return o},l=function(e,t){for(var r=function(e,t){for(var r=[{type:"text",data:e}],n=0;n<t.length;n++){var o=t[n];r=i(r,o.left,o.right,o.display||!1)}return r}(e,t.delimiters),n=document.createDocumentFragment(),a=0;a<r.length;a++)if("text"===r[a].type)n.appendChild(document.createTextNode(r[a].data));else{var l=document.createElement("span"),d=r[a].data;t.displayMode=r[a].display;try{t.preProcess&&(d=t.preProcess(d)),o.a.render(d,l,t)}catch(e){if(!(e instanceof o.a.ParseError))throw e;t.errorCallback("KaTeX auto-render: Failed to parse `"+r[a].data+"` with ",e),n.appendChild(document.createTextNode(r[a].rawData));continue}n.appendChild(l)}return n};t.default=function(e,t){if(!e)throw new Error("No element provided to render");var r={};for(var n in t)t.hasOwnProperty(n)&&(r[n]=t[n]);r.delimiters=r.delimiters||[{left:"$$",right:"$$",display:!0},{left:"\\(",right:"\\)",display:!1},{left:"\\[",right:"\\]",display:!0}],r.ignoredTags=r.ignoredTags||["script","noscript","style","textarea","pre","code"],r.ignoredClasses=r.ignoredClasses||[],r.errorCallback=r.errorCallback||console.error,r.macros=r.macros||{},function e(t,r){for(var n=0;n<t.childNodes.length;n++){var o=t.childNodes[n];if(3===o.nodeType){var a=l(o.textContent,r);n+=a.childNodes.length-1,t.replaceChild(a,o)}else 1===o.nodeType&&function(){var t=" "+o.className+" ";-1===r.ignoredTags.indexOf(o.nodeName.toLowerCase())&&r.ignoredClasses.every(function(e){return-1===t.indexOf(" "+e+" ")})&&e(o,r)}()}}(e,r)}}]).default});
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
plugins/math/public/katex.min.css
vendored
1
plugins/math/public/katex.min.css
vendored
File diff suppressed because one or more lines are too long
1
plugins/math/public/katex.min.js
vendored
1
plugins/math/public/katex.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,54 +0,0 @@
|
||||
determiner.math = function(block,lines,firstChar,secondChar,thirdChar){
|
||||
if( (firstChar == '\\' && secondChar == '[') || (firstChar == '$' && secondChar == '$') )
|
||||
{
|
||||
return "math-component";
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
bloxFormats.math = { label: '<svg class="icon icon-omega"><use xlink:href="#icon-omega"></use></svg>', title: 'Math', component: 'math-component' };
|
||||
|
||||
formatConfig.push('math');
|
||||
|
||||
const mathComponent = Vue.component('math-component', {
|
||||
props: ['compmarkdown', 'disabled'],
|
||||
template: '<div>' +
|
||||
'<input type="hidden" ref="markdown" :value="compmarkdown" :disabled="disabled" @input="updatemarkdown" />' +
|
||||
'<div class="contenttype"><svg class="icon icon-omega"><use xlink:href="#icon-omega"></use></svg></div>' +
|
||||
'<textarea class="mdcontent" ref="markdown" v-model="mathblock" :disabled="disabled" @input="createmarkdown"></textarea>' +
|
||||
'</div>',
|
||||
data: function(){
|
||||
return {
|
||||
mathblock: ''
|
||||
}
|
||||
},
|
||||
mounted: function(){
|
||||
this.$refs.markdown.focus();
|
||||
if(this.compmarkdown)
|
||||
{
|
||||
var dollarMath = new RegExp(/^\$\$[\S\s]+\$\$$/m);
|
||||
var bracketMath = new RegExp(/^\\\[[\S\s]+\\\]$/m);
|
||||
|
||||
if(dollarMath.test(this.compmarkdown) || bracketMath.test(this.compmarkdown))
|
||||
{
|
||||
var mathExpression = this.compmarkdown.substring(2,this.compmarkdown.length-2);
|
||||
this.mathblock = mathExpression.trim();
|
||||
}
|
||||
}
|
||||
this.$nextTick(function () {
|
||||
autosize(document.querySelectorAll('textarea'));
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
createmarkdown: function(event)
|
||||
{
|
||||
this.codeblock = event.target.value;
|
||||
var codeblock = '$$\n' + event.target.value + '\n$$';
|
||||
this.updatemarkdown(codeblock);
|
||||
},
|
||||
updatemarkdown: function(codeblock)
|
||||
{
|
||||
this.$emit('updatedMarkdown', codeblock);
|
||||
},
|
||||
},
|
||||
})
|
@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\search;
|
||||
|
||||
use \Typemill\Plugin;
|
||||
use \Typemill\Models\Write;
|
||||
use \Typemill\Models\WriteCache;
|
||||
|
||||
class Index extends Plugin
|
||||
{
|
||||
public static function getSubscribedEvents(){}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$write = new Write();
|
||||
|
||||
$index = $write->getFile('cache', 'index.json');
|
||||
if(!$index)
|
||||
{
|
||||
$this->createIndex();
|
||||
$index = $write->getFile('cache', 'index.json');
|
||||
}
|
||||
|
||||
return $this->returnJson($index);
|
||||
}
|
||||
|
||||
private function createIndex()
|
||||
{
|
||||
$write = new WriteCache();
|
||||
|
||||
# get content structure
|
||||
$structure = $write->getCache('cache', 'structure.txt');
|
||||
|
||||
# get data for search-index
|
||||
$index = $this->getAllContent($structure, $write);
|
||||
|
||||
# store the index file here
|
||||
$write->writeFile('cache', 'index.json', json_encode($index, JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
private function getAllContent($structure, $write, $index = NULL)
|
||||
{
|
||||
foreach($structure as $item)
|
||||
{
|
||||
if($item->elementType == "folder")
|
||||
{
|
||||
if($item->fileType == 'md')
|
||||
{
|
||||
$page = $write->getFileWithPath('content' . $item->path . DIRECTORY_SEPARATOR . 'index.md');
|
||||
$pageArray = $this->getPageContentArray($page, $item->urlAbs);
|
||||
$index[$pageArray['url']] = $pageArray;
|
||||
}
|
||||
|
||||
$index = $this->getAllContent($item->folderContent, $write, $index);
|
||||
}
|
||||
else
|
||||
{
|
||||
$page = $write->getFileWithPath('content' . $item->path);
|
||||
$pageArray = $this->getPageContentArray($page, $item->urlAbs);
|
||||
$index[$pageArray['url']] = $pageArray;
|
||||
}
|
||||
}
|
||||
return $index;
|
||||
}
|
||||
|
||||
private function getPageContentArray($page, $url)
|
||||
{
|
||||
$parts = explode("\n", $page, 2);
|
||||
|
||||
# get the title / headline
|
||||
$title = trim($parts[0], '# ');
|
||||
$title = str_replace(["\r\n", "\n", "\r"],' ', $title);
|
||||
|
||||
# get and cleanup the content
|
||||
$content = $parts[1];
|
||||
$content = strip_tags($content);
|
||||
$content = str_replace(["\r\n", "\n", "\r"],' ', $content);
|
||||
|
||||
$pageContent = [
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'url' => $url
|
||||
];
|
||||
|
||||
return $pageContent;
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
Copyright (C) 2013 by Oliver Nightingale
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
6
plugins/search/public/lunr.min.js
vendored
6
plugins/search/public/lunr.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,64 +0,0 @@
|
||||
.searchContainer{
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.searchContainer input{
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
border: 1px solid #ddd;
|
||||
font-size: 1rem;
|
||||
float: left;
|
||||
padding-left: 15px;
|
||||
border-radius: 2px;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
.searchContainer button{
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
border: none;
|
||||
background: #232833;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
color: #fff;
|
||||
font-size: 10pt;
|
||||
margin-left: -50px;
|
||||
}
|
||||
.searchContainer button:hover,.searchContainer button:focus, .searchContainer button:active{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#searchresult{
|
||||
}
|
||||
.resultwrapper{
|
||||
}
|
||||
button#closeSearchResult{
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
margin: 10px;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
font-size: 1rem;
|
||||
color: #fff;
|
||||
background: #000;
|
||||
padding: 15px;
|
||||
}
|
||||
button#closeSearchResult:hover,#closeSearchResult:focus{
|
||||
cursor: pointer;
|
||||
}
|
||||
.resultlist{
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
list-style:none;
|
||||
}
|
||||
.resultitem{
|
||||
|
||||
}
|
||||
.resultheader{
|
||||
|
||||
}
|
||||
.resultsnippet{
|
||||
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
var searchField = document.getElementById("searchField");
|
||||
var searchButton = document.getElementById("searchButton");
|
||||
|
||||
if(searchField && searchButton)
|
||||
{
|
||||
var searchIndex = false;
|
||||
var documents = false;
|
||||
var holdcontent = false;
|
||||
var contentwrapper = false;
|
||||
|
||||
searchField.addEventListener("focus", function(event){
|
||||
|
||||
if(!searchIndex)
|
||||
{
|
||||
myaxios.get('/indexrs51gfe2o2')
|
||||
.then(function (response) {
|
||||
|
||||
documents = JSON.parse(response.data);
|
||||
|
||||
searchIndex = lunr(function() {
|
||||
this.ref("id");
|
||||
this.field("title", { boost: 10 });
|
||||
this.field("content");
|
||||
for (var key in documents){
|
||||
this.add({
|
||||
"id": documents[key].url,
|
||||
"title": documents[key].title,
|
||||
"content": documents[key].content
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
.catch(function (error) {});
|
||||
}
|
||||
});
|
||||
|
||||
searchButton.addEventListener("click", function(event){
|
||||
event.preventDefault();
|
||||
|
||||
var term = document.getElementById('searchField').value;
|
||||
var results = searchIndex.search(term);
|
||||
|
||||
var resultPages = results.map(function (match) {
|
||||
return documents[match.ref];
|
||||
});
|
||||
|
||||
resultsString = "<div class='resultwrapper'><h1>Result for " + term + "</h1>";
|
||||
resultsString += "<button id='closeSearchResult'>close</button>";
|
||||
resultsString += "<ul class='resultlist'>";
|
||||
resultPages.forEach(function (r) {
|
||||
resultsString += "<li class='resultitem'>";
|
||||
resultsString += "<a class='resultheader' href='" + r.url + "?q=" + term + "'><h3>" + r.title + "</h3></a>";
|
||||
resultsString += "<div class='resultsnippet'>" + r.content.substring(0, 200) + " ...</div>";
|
||||
resultsString += "</li>"
|
||||
});
|
||||
resultsString += "</ul></div>";
|
||||
|
||||
if(!holdcontent)
|
||||
{
|
||||
contentwrapper = document.getElementById("searchresult").parentNode;
|
||||
holdcontent = contentwrapper.innerHTML;
|
||||
}
|
||||
|
||||
contentwrapper.innerHTML = resultsString;
|
||||
|
||||
document.getElementById("closeSearchResult").addEventListener("click", function(event){
|
||||
contentwrapper.innerHTML = holdcontent;
|
||||
});
|
||||
|
||||
}, false);
|
||||
}
|
||||
|
||||
/*
|
||||
var searchIndex = lunr(function() {
|
||||
this.ref("id");
|
||||
this.field("title", { boost: 10 });
|
||||
this.field("content");
|
||||
for (var key in window.pages) {
|
||||
this.add({
|
||||
"id": key,
|
||||
"title": pages[key].title,
|
||||
"content": pages[key].content
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function getQueryVariable(variable) {
|
||||
var query = window.location.search.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i = 0; i < vars.length; i++) {
|
||||
var pair = vars[i].split("=");
|
||||
if (pair[0] === variable) {
|
||||
return decodeURIComponent(pair[1].replace(/\+/g, "%20"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var searchTerm = getQueryVariable("q");
|
||||
// creation of searchIndex from earlier example
|
||||
var results = searchIndex.search(searchTerm);
|
||||
var resultPages = results.map(function (match) {
|
||||
return pages[match.ref];
|
||||
});
|
||||
|
||||
// resultPages from previous example
|
||||
resultsString = "";
|
||||
resultPages.forEach(function (r) {
|
||||
resultsString += "<li>";
|
||||
resultsString += "<a class='result' href='" + r.url + "?q=" + searchTerm + "'><h3>" + r.title + "</h3></a>";
|
||||
resultsString += "<div class='snippet'>" + r.content.substring(0, 200) + "</div>";
|
||||
resultsString += "</li>"
|
||||
});
|
||||
document.querySelector("#search-results").innerHTML = resultsString;
|
||||
*/
|
@ -1,128 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\search;
|
||||
|
||||
use \Typemill\Plugin;
|
||||
use \Typemill\Models\Write;
|
||||
|
||||
class Search extends index
|
||||
{
|
||||
protected $item;
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return array(
|
||||
'onSettingsLoaded' => 'onsettingsLoaded',
|
||||
'onContentArrayLoaded' => 'onContentArrayLoaded',
|
||||
'onPageReady' => 'onPageReady',
|
||||
'onPagePublished' => 'onPagePublished',
|
||||
'onPageUnpublished' => 'onPageUnpublished',
|
||||
'onPageSorted' => 'onPageSorted',
|
||||
'onPageDeleted' => 'onPageDeleted',
|
||||
);
|
||||
}
|
||||
|
||||
# get search.json with route
|
||||
# update search.json on publish
|
||||
|
||||
public static function addNewRoutes()
|
||||
{
|
||||
# the route for the api calls
|
||||
return array(
|
||||
array(
|
||||
'httpMethod' => 'get',
|
||||
'route' => '/indexrs51gfe2o2',
|
||||
'class' => 'Plugins\search\index:index'
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function onSettingsLoaded($settings)
|
||||
{
|
||||
$this->settings = $settings->getData();
|
||||
}
|
||||
|
||||
# at any of theses events, delete the old search index
|
||||
public function onPagePublished($item)
|
||||
{
|
||||
$this->deleteSearchIndex();
|
||||
}
|
||||
public function onPageUnpublished($item)
|
||||
{
|
||||
$this->deleteSearchIndex();
|
||||
}
|
||||
public function onPageSorted($inputParams)
|
||||
{
|
||||
$this->deleteSearchIndex();
|
||||
}
|
||||
public function onPageDeleted($item)
|
||||
{
|
||||
$this->deleteSearchIndex();
|
||||
}
|
||||
|
||||
private function deleteSearchIndex()
|
||||
{
|
||||
$write = new Write();
|
||||
|
||||
# store the index file here
|
||||
$write->deleteFileWithPath('cache' . DIRECTORY_SEPARATOR . 'index.json');
|
||||
}
|
||||
|
||||
public function onContentArrayLoaded($contentArray)
|
||||
{
|
||||
# get content array
|
||||
$content = $contentArray->getData();
|
||||
$settings = $this->getPluginSettings('search');
|
||||
$salt = "asPx9Derf2";
|
||||
|
||||
# activate axios and vue in frontend
|
||||
$this->activateAxios();
|
||||
$this->activateVue();
|
||||
|
||||
# add the css and vue application
|
||||
$this->addCSS('/search/public/search.css');
|
||||
$this->addJS('/search/public/lunr.min.js');
|
||||
$this->addJS('/search/public/search.js');
|
||||
|
||||
# simple security for first request
|
||||
$secret = time();
|
||||
$secret = substr($secret,0,-1);
|
||||
$secret = md5($secret . $salt);
|
||||
|
||||
# simple csrf protection with a session for long following requests
|
||||
if (session_status() == PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$length = 32;
|
||||
$token = substr(base_convert(sha1(uniqid(mt_rand())), 16, 36), 0, $length);
|
||||
$_SESSION['search'] = $token;
|
||||
$_SESSION['search-expire'] = time() + 1300; # 60 seconds * 30 minutes
|
||||
|
||||
# create div for vue app
|
||||
$search = '<div data-access="' . $secret . '" data-token="' . $token . '" id="searchresult"></div>';
|
||||
|
||||
# create content type
|
||||
$search = Array
|
||||
(
|
||||
'rawHtml' => $search,
|
||||
'allowRawHtmlInSafeMode' => true,
|
||||
'autobreak' => 1
|
||||
);
|
||||
|
||||
$content[] = $search;
|
||||
|
||||
$contentArray->setData($content);
|
||||
}
|
||||
|
||||
public function onPageReady($page)
|
||||
{
|
||||
$pageData = $page->getData($page);
|
||||
|
||||
$pageData['widgets']['search'] = '<div class="searchContainer" id="searchForm">'.
|
||||
'<input id="searchField" type="text" placeholder="search ..." />'.
|
||||
'<button id="searchButton" type="button">GO</button>'.
|
||||
'</div>';
|
||||
$page->setData($pageData);
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
name: Search
|
||||
version: 1.0.0
|
||||
description: Adds a search to typemill with lunr.js.
|
||||
author: Sebastian Schürmanns
|
||||
homepage: https://typemill.net
|
||||
licence: MIT
|
@ -17,7 +17,7 @@ class Assets
|
||||
$this->editorInlineJS = array();
|
||||
$this->svgSymbols = array();
|
||||
}
|
||||
|
||||
|
||||
public function addCSS($CSS)
|
||||
{
|
||||
$CSSfile = $this->getFileUrl($CSS);
|
||||
|
@ -6,6 +6,7 @@ use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
use Typemill\Models\Folder;
|
||||
use Typemill\Models\Write;
|
||||
use Typemill\Models\WriteYaml;
|
||||
use Typemill\Models\ProcessImage;
|
||||
use Typemill\Extensions\ParsedownExtension;
|
||||
use Typemill\Events\OnPagePublished;
|
||||
@ -220,7 +221,7 @@ class ContentApiController extends ContentController
|
||||
|
||||
if($this->item->elementType == 'file')
|
||||
{
|
||||
$delete = $this->deleteContentFiles(['md','txt']);
|
||||
$delete = $this->deleteContentFiles(['md','txt', 'yaml']);
|
||||
}
|
||||
elseif($this->item->elementType == 'folder')
|
||||
{
|
||||
@ -323,13 +324,19 @@ class ContentApiController extends ContentController
|
||||
$parentKeyFrom = explode('.', $this->params['parent_id_from']);
|
||||
$parentKeyTo = explode('.', $this->params['parent_id_to']);
|
||||
|
||||
/*
|
||||
echo '<pre>';
|
||||
print_r(array($itemKeyPath 0,$parentKeyFrom navi,$parentKeyTo 2));
|
||||
die();
|
||||
*/
|
||||
|
||||
# get the item from structure
|
||||
$item = Folder::getItemWithKeyPath($this->structure, $itemKeyPath);
|
||||
|
||||
if(!$item){ return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not find this page. Please refresh and try again.', 'url' => $url), 404); }
|
||||
|
||||
# if a folder is moved on the first level
|
||||
if($this->params['parent_id_from'] == 'navi')
|
||||
# if an item is moved to the first level
|
||||
if($this->params['parent_id_to'] == 'navi')
|
||||
{
|
||||
# create empty and default values so that the logic below still works
|
||||
$newFolder = new \stdClass();
|
||||
@ -388,7 +395,7 @@ class ContentApiController extends ContentController
|
||||
# get item for url and set it active again
|
||||
if(isset($this->params['url']))
|
||||
{
|
||||
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url']);
|
||||
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl());
|
||||
}
|
||||
|
||||
# keep the internal structure for response
|
||||
@ -434,7 +441,7 @@ class ContentApiController extends ContentController
|
||||
$nameParts = Folder::getStringParts($this->params['item_name']);
|
||||
$name = implode("-", $nameParts);
|
||||
$slug = $name;
|
||||
|
||||
|
||||
# initialize index
|
||||
$index = 0;
|
||||
|
||||
@ -465,8 +472,10 @@ class ContentApiController extends ContentController
|
||||
$namePath = $index > 9 ? $index . '-' . $name : '0' . $index . '-' . $name;
|
||||
$folderPath = 'content' . $folder->path;
|
||||
|
||||
$title = implode(" ", $nameParts);
|
||||
|
||||
# create default content
|
||||
$content = json_encode(['# Add Title', 'Add Content']);
|
||||
$content = json_encode(['# ' . $title, 'Content']);
|
||||
|
||||
if($this->params['type'] == 'file')
|
||||
{
|
||||
@ -490,7 +499,7 @@ class ContentApiController extends ContentController
|
||||
# get item for url and set it active again
|
||||
if(isset($this->params['url']))
|
||||
{
|
||||
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url']);
|
||||
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl());
|
||||
}
|
||||
|
||||
# activate this if you want to redirect after creating the page...
|
||||
@ -499,7 +508,7 @@ class ContentApiController extends ContentController
|
||||
return $response->withJson(array('data' => $this->structure, 'errors' => false, 'url' => $url));
|
||||
}
|
||||
|
||||
public function createBaseFolder(Request $request, Response $response, $args)
|
||||
public function createBaseItem(Request $request, Response $response, $args)
|
||||
{
|
||||
# get params from call
|
||||
$this->params = $request->getParams();
|
||||
@ -512,7 +521,7 @@ class ContentApiController extends ContentController
|
||||
if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); }
|
||||
|
||||
# validate input
|
||||
#if(!$this->validateBaseFolder()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Special Characters not allowed. Length between 1 and 20 chars.', 'url' => $url), 422); }
|
||||
if(!$this->validateBaseNaviItem()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Special Characters not allowed. Length between 1 and 20 chars.', 'url' => $url), 422); }
|
||||
|
||||
# create the name for the new item
|
||||
$nameParts = Folder::getStringParts($this->params['item_name']);
|
||||
@ -527,16 +536,16 @@ class ContentApiController extends ContentController
|
||||
|
||||
# iterate through the whole content of the new folder
|
||||
$writeError = false;
|
||||
|
||||
foreach($this->structure as $folder)
|
||||
|
||||
foreach($this->structure as $item)
|
||||
{
|
||||
# check, if the same name as new item, then return an error
|
||||
if($folder->slug == $slug)
|
||||
if($item->slug == $slug)
|
||||
{
|
||||
return $response->withJson(array('data' => $this->structure, 'errors' => 'There is already a page with this name. Please choose another name.', 'url' => $url), 404);
|
||||
}
|
||||
|
||||
if(!$write->moveElement($folder, '', $index))
|
||||
if(!$write->moveElement($item, '', $index))
|
||||
{
|
||||
$writeError = true;
|
||||
}
|
||||
@ -549,23 +558,32 @@ class ContentApiController extends ContentController
|
||||
$namePath = $index > 9 ? $index . '-' . $name : '0' . $index . '-' . $name;
|
||||
$folderPath = 'content';
|
||||
|
||||
if(!$write->checkPath($folderPath . DIRECTORY_SEPARATOR . $namePath))
|
||||
{
|
||||
return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the folder. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404);
|
||||
}
|
||||
|
||||
# create default content
|
||||
$content = json_encode(['# Add Title', 'Add Content']);
|
||||
|
||||
$write->writeFile($folderPath . DIRECTORY_SEPARATOR . $namePath, 'index.txt', $content);
|
||||
|
||||
if($this->params['type'] == 'file')
|
||||
{
|
||||
if(!$write->writeFile($folderPath, $namePath . '.txt', $content))
|
||||
{
|
||||
return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the file. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404);
|
||||
}
|
||||
}
|
||||
elseif($this->params['type'] == 'folder')
|
||||
{
|
||||
if(!$write->checkPath($folderPath . DIRECTORY_SEPARATOR . $namePath))
|
||||
{
|
||||
return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the folder. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404);
|
||||
}
|
||||
$write->writeFile($folderPath . DIRECTORY_SEPARATOR . $namePath, 'index.txt', $content);
|
||||
}
|
||||
|
||||
# update the structure for editor
|
||||
$this->setStructure($draft = true, $cache = false);
|
||||
|
||||
# get item for url and set it active again
|
||||
if(isset($this->params['url']))
|
||||
{
|
||||
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url']);
|
||||
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl());
|
||||
}
|
||||
|
||||
return $response->withJson(array('data' => $this->structure, 'errors' => false, 'url' => $url));
|
||||
@ -586,7 +604,7 @@ class ContentApiController extends ContentController
|
||||
# get item for url and set it active again
|
||||
if(isset($this->params['url']))
|
||||
{
|
||||
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url']);
|
||||
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl());
|
||||
}
|
||||
|
||||
return $response->withJson(array('data' => $this->structure, 'homepage' => $this->homepage, 'errors' => false));
|
||||
|
@ -92,6 +92,11 @@ abstract class ContentController
|
||||
return $this->c->view->render($response->withStatus(404), '/intern404.twig', $data);
|
||||
}
|
||||
|
||||
protected function getValidator()
|
||||
{
|
||||
return new Validation();
|
||||
}
|
||||
|
||||
protected function validateEditorInput()
|
||||
{
|
||||
$validate = new Validation();
|
||||
@ -151,6 +156,21 @@ abstract class ContentController
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function validateBaseNaviItem()
|
||||
{
|
||||
$validate = new Validation();
|
||||
$vResult = $validate->navigationBaseItem($this->params);
|
||||
|
||||
if(is_array($vResult))
|
||||
{
|
||||
$message = reset($vResult);
|
||||
$this->errors = ['errors' => $vResult];
|
||||
if(isset($message[0])){ $this->errors['errors']['message'] = $message[0]; }
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function setStructure($draft = false, $cache = true)
|
||||
{
|
||||
@ -221,35 +241,11 @@ abstract class ContentController
|
||||
|
||||
protected function setItem()
|
||||
{
|
||||
# if it is the homepage
|
||||
if($this->params['url'] == $this->uri->getBasePath() OR $this->params['url'] == '/')
|
||||
{
|
||||
$item = new \stdClass;
|
||||
$item->elementType = 'folder';
|
||||
$item->path = '';
|
||||
$item->urlRel = '/';
|
||||
}
|
||||
else
|
||||
{
|
||||
# search for the url in the structure
|
||||
$item = Folder::getItemForUrl($this->structure, $this->params['url']);
|
||||
}
|
||||
# search for the url in the structure
|
||||
$item = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBasePath());
|
||||
|
||||
if($item)
|
||||
{
|
||||
if($item->elementType == 'file')
|
||||
{
|
||||
$pathParts = explode('.', $item->path);
|
||||
$fileType = array_pop($pathParts);
|
||||
$pathWithoutType = implode('.', $pathParts);
|
||||
$item->pathWithoutType = $pathWithoutType;
|
||||
}
|
||||
elseif($item->elementType == 'folder')
|
||||
{
|
||||
$item->pathWithoutItem = $item->path;
|
||||
$item->path = $item->path . DIRECTORY_SEPARATOR . 'index';
|
||||
$item->pathWithoutType = $item->path;
|
||||
}
|
||||
$this->item = $item;
|
||||
return true;
|
||||
}
|
||||
@ -315,7 +311,7 @@ abstract class ContentController
|
||||
protected function deleteContentFolder()
|
||||
{
|
||||
$basePath = $this->settings['rootPath'] . $this->settings['contentFolder'];
|
||||
$path = $basePath . $this->item->pathWithoutItem;
|
||||
$path = $basePath . $this->item->path;
|
||||
|
||||
if(file_exists($path))
|
||||
{
|
||||
|
158
system/Controllers/MetaApiController.php
Normal file
158
system/Controllers/MetaApiController.php
Normal file
@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Typemill\Controllers;
|
||||
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
use Typemill\Models\WriteYaml;
|
||||
|
||||
class MetaApiController extends ContentController
|
||||
{
|
||||
# get the standard meta-definitions and the meta-definitions from plugins (same for all sites)
|
||||
public function getMetaDefinitions(Request $request, Response $response, $args)
|
||||
{
|
||||
$metatabs = $this->aggregateMetaDefinitions();
|
||||
|
||||
return $response->withJson(array('definitions' => $metatabs, 'errors' => false));
|
||||
}
|
||||
|
||||
# get the standard meta-definitions and the meta-definitions from plugins (same for all sites)
|
||||
public function aggregateMetaDefinitions()
|
||||
{
|
||||
$writeYaml = new writeYaml();
|
||||
|
||||
$metatabs = $writeYaml->getYaml('system' . DIRECTORY_SEPARATOR . 'author', 'metatabs.yaml');
|
||||
|
||||
# load cached metadefinitions
|
||||
# check if valid
|
||||
# if not, refresh cache
|
||||
|
||||
# loop through all plugins
|
||||
foreach($this->settings['plugins'] as $name => $plugin)
|
||||
{
|
||||
$pluginSettings = \Typemill\Settings::getObjectSettings('plugins', $name);
|
||||
if($pluginSettings && isset($pluginSettings['metatabs']))
|
||||
{
|
||||
$metatabs = array_merge_recursive($metatabs, $pluginSettings['metatabs']);
|
||||
}
|
||||
}
|
||||
|
||||
return $metatabs;
|
||||
}
|
||||
|
||||
public function getArticleMetaObject(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); }
|
||||
|
||||
$writeYaml = new writeYaml();
|
||||
|
||||
$pagemeta = $writeYaml->getPageMeta($this->settings, $this->item);
|
||||
|
||||
if(!$pagemeta)
|
||||
{
|
||||
# set the status for published and drafted
|
||||
$this->setPublishStatus();
|
||||
|
||||
# set path
|
||||
$this->setItemPath($this->item->fileType);
|
||||
|
||||
# read content from file
|
||||
if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
|
||||
|
||||
$pagemeta = $writeYaml->getPageMetaDefaults($this->content, $this->settings, $this->item);
|
||||
}
|
||||
|
||||
# get global metadefinitions
|
||||
$metadefinitions = $this->aggregateMetaDefinitions();
|
||||
|
||||
$metadata = [];
|
||||
|
||||
foreach($metadefinitions as $tabname => $tab )
|
||||
{
|
||||
$metadata[$tabname] = [];
|
||||
|
||||
foreach($tab['fields'] as $fieldname => $fielddefinitions)
|
||||
{
|
||||
$metadata[$tabname][$fieldname] = isset($pagemeta[$tabname][$fieldname]) ? $pagemeta[$tabname][$fieldname] : null;
|
||||
}
|
||||
}
|
||||
|
||||
return $response->withJson(array('metadata' => $metadata, 'metadefinitions' => $metadefinitions, 'errors' => false));
|
||||
}
|
||||
|
||||
public function updateArticleMeta(Request $request, Response $response, $args)
|
||||
{
|
||||
/* get params from call */
|
||||
$this->params = $request->getParams();
|
||||
$this->uri = $request->getUri();
|
||||
|
||||
$tab = isset($this->params['tab']) ? $this->params['tab'] : false;
|
||||
$metaData = isset($this->params['data']) ? $this->params['data'] : false ;
|
||||
$objectName = 'meta';
|
||||
$errors = false;
|
||||
|
||||
if(!$tab or !$metaData)
|
||||
{
|
||||
return $response->withJson($this->errors, 404);
|
||||
}
|
||||
|
||||
# load metadefinitions
|
||||
$metaDefinitions = $this->aggregateMetaDefinitions();
|
||||
|
||||
# create validation object
|
||||
$validate = $this->getValidator();
|
||||
|
||||
# take the user input data and iterate over all fields and values
|
||||
foreach($metaData as $fieldName => $fieldValue)
|
||||
{
|
||||
# get the corresponding field definition from original plugin settings */
|
||||
$fieldDefinition = isset($metaDefinitions[$tab]['fields'][$fieldName]) ? $metaDefinitions[$tab]['fields'][$fieldName] : false;
|
||||
|
||||
if(!$fieldDefinition)
|
||||
{
|
||||
$errors[$tab][$fieldName] = 'This field is not defined';
|
||||
}
|
||||
else
|
||||
{
|
||||
# validate user input for this field
|
||||
$result = $validate->objectField($fieldName, $fieldValue, $objectName, $fieldDefinition);
|
||||
|
||||
if($result !== true)
|
||||
{
|
||||
$errors[$tab][$fieldName] = $result[$fieldName][0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# return validation errors
|
||||
if($errors){ return $response->withJson(array('errors' => $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); }
|
||||
|
||||
$writeYaml = new writeYaml();
|
||||
|
||||
# get existing metadata for page
|
||||
$meta = $writeYaml->getYaml($this->settings['contentFolder'], $this->item->pathWithoutType . '.yaml');
|
||||
|
||||
# add the new/edited metadata
|
||||
$meta[$tab] = $metaData;
|
||||
|
||||
# store the metadata
|
||||
$writeYaml->updateYaml($this->settings['contentFolder'], $this->item->pathWithoutType . '.yaml', $meta);
|
||||
|
||||
# return with the new metadata
|
||||
return $response->withJson(array('metadata' => $metaData, 'errors' => false));
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ use Typemill\Events\OnPagetreeLoaded;
|
||||
use Typemill\Events\OnBreadcrumbLoaded;
|
||||
use Typemill\Events\OnItemLoaded;
|
||||
use Typemill\Events\OnOriginalLoaded;
|
||||
use Typemill\Events\OnMetaLoaded;
|
||||
use Typemill\Events\OnMarkdownLoaded;
|
||||
use Typemill\Events\OnContentArrayLoaded;
|
||||
use Typemill\Events\OnHtmlLoaded;
|
||||
@ -27,6 +28,7 @@ class PageController extends Controller
|
||||
$structure = false;
|
||||
$contentHTML = false;
|
||||
$item = false;
|
||||
$home = false;
|
||||
$breadcrumb = false;
|
||||
$description = '';
|
||||
$settings = $this->c->get('settings');
|
||||
@ -34,7 +36,7 @@ class PageController extends Controller
|
||||
$cache = new WriteCache();
|
||||
$uri = $request->getUri();
|
||||
$base_url = $uri->getBaseUrl();
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
/* if the cached structure is still valid, use it */
|
||||
@ -74,56 +76,68 @@ class PageController extends Controller
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/* if the user is on startpage */
|
||||
# if the user is on startpage
|
||||
if(empty($args))
|
||||
{
|
||||
/* 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;
|
||||
{
|
||||
$home = true;
|
||||
$item = Folder::getItemForUrl($structure, $uri->getBasePath(), $uri->getBasePath());
|
||||
}
|
||||
else
|
||||
{
|
||||
/* get the request url */
|
||||
$urlRel = $uri->getBasePath() . '/' . $args['params'];
|
||||
$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);
|
||||
|
||||
$item = Folder::getItemForUrl($structure, $urlRel, $uri->getBasePath());
|
||||
|
||||
/* 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 ));
|
||||
}
|
||||
|
||||
|
||||
/* get breadcrumb for page */
|
||||
$breadcrumb = Folder::getBreadcrumb($structure, $item->keyPathArray);
|
||||
$breadcrumb = $this->c->dispatcher->dispatch('onBreadcrumbLoaded', new OnBreadcrumbLoaded($breadcrumb))->getData();
|
||||
|
||||
/* add the paging to the item */
|
||||
$item = Folder::getPagingForItem($structure, $item);
|
||||
$item = $this->c->dispatcher->dispatch('onItemLoaded', new OnItemLoaded($item))->getData();
|
||||
|
||||
/* check if url is a folder. If so, check if there is an index-file in that folder */
|
||||
if($item->elementType == 'folder')
|
||||
{
|
||||
$filePath = $pathToContent . $item->path . DIRECTORY_SEPARATOR . 'index.md';
|
||||
}
|
||||
elseif($item->elementType == 'file')
|
||||
{
|
||||
$filePath = $pathToContent . $item->path;
|
||||
}
|
||||
|
||||
/* add the modified date for the file */
|
||||
$item->modified = isset($filePath) ? filemtime($filePath) : false;
|
||||
|
||||
/* read the content of the file */
|
||||
$contentMD = isset($filePath) ? file_get_contents($filePath) : false;
|
||||
}
|
||||
|
||||
# dispatch the item
|
||||
$item = $this->c->dispatcher->dispatch('onItemLoaded', new OnItemLoaded($item))->getData();
|
||||
|
||||
# set the filepath
|
||||
$filePath = $pathToContent . $item->path;
|
||||
|
||||
# check if url is a folder and add index.md
|
||||
if($item->elementType == 'folder')
|
||||
{
|
||||
$filePath = $filePath . DIRECTORY_SEPARATOR . 'index.md';
|
||||
}
|
||||
|
||||
# read the content of the file
|
||||
$contentMD = file_exists($filePath) ? file_get_contents($filePath) : false;
|
||||
|
||||
# dispatch the original content without plugin-manipulations for case anyone wants to use it
|
||||
$this->c->dispatcher->dispatch('onOriginalLoaded', new OnOriginalLoaded($contentMD));
|
||||
|
||||
$contentMD = $this->c->dispatcher->dispatch('onMarkdownLoaded', new OnMarkdownLoaded($contentMD))->getData();
|
||||
|
||||
# get meta-Information
|
||||
$writeYaml = new WriteYaml();
|
||||
|
||||
$metatabs = $writeYaml->getPageMeta($settings, $item);
|
||||
|
||||
if(!$metatabs)
|
||||
{
|
||||
$metatabs = $writeYaml->getPageMetaDefaults($contentMD, $settings, $item);
|
||||
}
|
||||
|
||||
# dispatch meta
|
||||
$metatabs = $this->c->dispatcher->dispatch('onMetaLoaded', new OnMetaLoaded($metatabs))->getData();
|
||||
|
||||
# dispatch content
|
||||
$contentMD = $this->c->dispatcher->dispatch('onMarkdownLoaded', new OnMarkdownLoaded($contentMD))->getData();
|
||||
|
||||
/* initialize parsedown */
|
||||
$parsedown = new ParsedownExtension();
|
||||
|
||||
@ -148,18 +162,23 @@ class PageController extends Controller
|
||||
$title = isset($contentParts[0]) ? strip_tags($contentParts[0]) : $settings['title'];
|
||||
|
||||
$contentHTML = isset($contentParts[1]) ? $contentParts[1] : $contentHTML;
|
||||
|
||||
/* create excerpt from content */
|
||||
$excerpt = substr($contentHTML,0,500);
|
||||
|
||||
/* create description from excerpt */
|
||||
$description = isset($excerpt) ? strip_tags($excerpt) : false;
|
||||
if($description)
|
||||
|
||||
# if there is not meta description
|
||||
if(!isset($metatabs['meta']['description']) or !$metatabs['meta']['description'])
|
||||
{
|
||||
$description = trim(preg_replace('/\s+/', ' ', $description));
|
||||
$description = substr($description, 0, 300);
|
||||
$lastSpace = strrpos($description, ' ');
|
||||
$description = substr($description, 0, $lastSpace);
|
||||
# create excerpt from html
|
||||
$excerpt = substr($contentHTML,0,500);
|
||||
|
||||
# create description from excerpt
|
||||
$description = isset($excerpt) ? strip_tags($excerpt) : false;
|
||||
if($description)
|
||||
{
|
||||
$description = trim(preg_replace('/\s+/', ' ', $description));
|
||||
$description = substr($description, 0, 300);
|
||||
$lastSpace = strrpos($description, ' ');
|
||||
|
||||
$metatabs['meta']['description'] = substr($description, 0, $lastSpace);
|
||||
}
|
||||
}
|
||||
|
||||
/* get url and alt-tag for first image, if exists */
|
||||
@ -174,20 +193,18 @@ class PageController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$home = empty($args) ? true : false;
|
||||
$theme = $settings['theme'];
|
||||
$route = empty($args) && isset($settings['themes'][$theme]['cover']) ? '/cover.twig' : '/index.twig';
|
||||
|
||||
return $this->render($response, $route, [
|
||||
'home' => $home,
|
||||
'navigation' => $structure,
|
||||
'title' => $title,
|
||||
'content' => $contentHTML,
|
||||
'item' => $item,
|
||||
'breadcrumb' => $breadcrumb,
|
||||
'settings' => $settings,
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'settings' => $settings,
|
||||
'metatabs' => $metatabs,
|
||||
'base_url' => $base_url,
|
||||
'image' => $firstImage ]);
|
||||
}
|
||||
|
14
system/Events/OnMetaLoaded.php
Normal file
14
system/Events/OnMetaLoaded.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Typemill\Events;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Event for markdown.
|
||||
*/
|
||||
|
||||
class OnMetaLoaded extends BaseEvent
|
||||
{
|
||||
|
||||
}
|
24
system/Extensions/TwigMetaExtension.php
Normal file
24
system/Extensions/TwigMetaExtension.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Typemill\Extensions;
|
||||
|
||||
use Typemill\Models\WriteYaml;
|
||||
|
||||
class TwigMetaExtension extends \Twig_Extension
|
||||
{
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new \Twig_SimpleFunction('getPageMeta', array($this, 'getMeta' ))
|
||||
];
|
||||
}
|
||||
|
||||
public function getMeta($settings, $item)
|
||||
{
|
||||
$write = new WriteYaml();
|
||||
|
||||
$meta = $write->getPageMeta($settings, $item);
|
||||
|
||||
return $meta;
|
||||
}
|
||||
}
|
@ -106,18 +106,18 @@ class Folder
|
||||
$fileType = '';
|
||||
if(in_array('index.md', $name))
|
||||
{
|
||||
$fileType = 'md';
|
||||
$status = 'published';
|
||||
$fileType = 'md';
|
||||
$status = 'published';
|
||||
}
|
||||
if(in_array('index.txt', $name))
|
||||
{
|
||||
$fileType = 'txt';
|
||||
$status = 'unpublished';
|
||||
$fileType = 'txt';
|
||||
$status = 'unpublished';
|
||||
}
|
||||
if(in_array('index.txtmd', $name))
|
||||
{
|
||||
$fileType = 'txt';
|
||||
$status = 'modified';
|
||||
$fileType = 'txt';
|
||||
$status = 'modified';
|
||||
}
|
||||
|
||||
$item->originalName = $key;
|
||||
@ -130,6 +130,7 @@ class Folder
|
||||
$item->slug = implode("-",$nameParts);
|
||||
$item->slug = URLify::filter(iconv(mb_detect_encoding($item->slug, mb_detect_order(), true), "UTF-8", $item->slug));
|
||||
$item->path = $fullPath . DIRECTORY_SEPARATOR . $key;
|
||||
$item->pathWithoutType = $fullPath . DIRECTORY_SEPARATOR . $key . DIRECTORY_SEPARATOR . 'index';
|
||||
$item->urlRelWoF = $fullSlugWithoutFolder . '/' . $item->slug;
|
||||
$item->urlRel = $fullSlugWithFolder . '/' . $item->slug;
|
||||
$item->urlAbs = $baseUrl . $fullSlugWithoutFolder . '/' . $item->slug;
|
||||
@ -139,20 +140,21 @@ class Folder
|
||||
$item->chapter = $chapter ? $chapter . '.' . $chapternr : $chapternr;
|
||||
$item->active = false;
|
||||
$item->activeParent = false;
|
||||
|
||||
|
||||
$item->folderContent = self::getFolderContentDetails($name, $baseUrl, $item->urlRel, $item->urlRelWoF, $item->path, $item->keyPath, $item->chapter);
|
||||
}
|
||||
else
|
||||
{
|
||||
# do not use files in base folder (only folders are allowed)
|
||||
if(!isset($keyPath)) continue;
|
||||
# if(!isset($keyPath)) continue;
|
||||
|
||||
# do not use index files
|
||||
if($name == 'index.md' || $name == 'index.txt' || $name == 'index.txtmd' ) continue;
|
||||
|
||||
$nameParts = self::getStringParts($name);
|
||||
$fileType = array_pop($nameParts);
|
||||
|
||||
$nameWithoutType = self::getNameWithoutType($name);
|
||||
|
||||
if($fileType == 'md')
|
||||
{
|
||||
$status = 'published';
|
||||
@ -177,8 +179,9 @@ class Folder
|
||||
$item->slug = implode("-",$nameParts);
|
||||
$item->slug = URLify::filter(iconv(mb_detect_encoding($item->slug, mb_detect_order(), true), "UTF-8", $item->slug));
|
||||
$item->path = $fullPath . DIRECTORY_SEPARATOR . $name;
|
||||
$item->pathWithoutType = $fullPath . DIRECTORY_SEPARATOR . $nameWithoutType;
|
||||
$item->key = $iteration;
|
||||
$item->keyPath = $keyPath . '.' . $iteration;
|
||||
$item->keyPath = isset($keyPath) ? $keyPath . '.' . $iteration : $iteration;
|
||||
$item->keyPathArray = explode('.',$item->keyPath);
|
||||
$item->chapter = $chapter . '.' . $chapternr;
|
||||
$item->urlRelWoF = $fullSlugWithoutFolder . '/' . $item->slug;
|
||||
@ -187,6 +190,7 @@ class Folder
|
||||
$item->active = false;
|
||||
$item->activeParent = false;
|
||||
}
|
||||
|
||||
$iteration++;
|
||||
$chapternr++;
|
||||
$contentDetails[] = $item;
|
||||
@ -194,8 +198,22 @@ class Folder
|
||||
return $contentDetails;
|
||||
}
|
||||
|
||||
public static function getItemForUrl($folderContentDetails, $url, $result = NULL)
|
||||
public static function getItemForUrl($folderContentDetails, $url, $baseUrl, $result = NULL)
|
||||
{
|
||||
|
||||
# if we are on the homepage
|
||||
if($url == '/' OR $url == $baseUrl)
|
||||
{
|
||||
# return a standard item-object
|
||||
$item = new \stdClass;
|
||||
$item->elementType = 'folder';
|
||||
$item->path = '';
|
||||
$item->urlRel = '/';
|
||||
$item->pathWithoutType = DIRECTORY_SEPARATOR . 'index';
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
foreach($folderContentDetails as $key => $item)
|
||||
{
|
||||
if($item->urlRel === $url)
|
||||
@ -206,7 +224,7 @@ class Folder
|
||||
}
|
||||
elseif($item->elementType === "folder")
|
||||
{
|
||||
$result = self::getItemForUrl($item->folderContent, $url, $result);
|
||||
$result = self::getItemForUrl($item->folderContent, $url, $baseUrl, $result);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
@ -357,6 +375,7 @@ class Folder
|
||||
/* get breadcrumb as copied array, set elements active in original and mark parent element in original */
|
||||
public static function getBreadcrumb($content, $searchArray, $i = NULL, $breadcrumb = NULL)
|
||||
{
|
||||
# if it is the first round, create an empty array
|
||||
if(!$i){ $i = 0; $breadcrumb = array();}
|
||||
|
||||
while($i < count($searchArray))
|
||||
@ -431,4 +450,10 @@ class Folder
|
||||
$parts = preg_split('/\./',$fileName);
|
||||
return $parts;
|
||||
}
|
||||
public static function getNameWithoutType($fileName)
|
||||
{
|
||||
$parts = preg_split('/\./',$fileName);
|
||||
return $parts[0];
|
||||
}
|
||||
|
||||
}
|
@ -291,6 +291,25 @@ class Validation
|
||||
return $v->errors();
|
||||
}
|
||||
}
|
||||
|
||||
public function navigationBaseItem(array $params)
|
||||
{
|
||||
$v = new Validator($params);
|
||||
|
||||
$v->rule('required', ['item_name', 'type', 'url']);
|
||||
$v->rule('noSpecialChars', 'item_name');
|
||||
$v->rule('lengthBetween', 'item_name', 1, 40);
|
||||
$v->rule('in', 'type', ['file', 'folder']);
|
||||
|
||||
if($v->validate())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return $v->errors();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* validation for dynamic fields ( settings for themes and plugins)
|
||||
@ -387,7 +406,11 @@ class Validation
|
||||
}
|
||||
else
|
||||
{
|
||||
if($name)
|
||||
if($name == 'meta')
|
||||
{
|
||||
return $v->errors();
|
||||
}
|
||||
elseif($name)
|
||||
{
|
||||
if(isset($_SESSION['errors'][$name]))
|
||||
{
|
||||
|
@ -112,7 +112,7 @@ class Write
|
||||
|
||||
public function moveElement($item, $folderPath, $index)
|
||||
{
|
||||
$filetypes = array('md', 'txt');
|
||||
$filetypes = array('md', 'txt', 'yaml');
|
||||
|
||||
# set new order as string
|
||||
$newOrder = ($index < 10) ? '0' . $index : $index;
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Typemill\Models;
|
||||
|
||||
use Typemill\Extensions\ParsedownExtension;
|
||||
|
||||
class WriteYaml extends Write
|
||||
{
|
||||
/**
|
||||
@ -35,4 +37,85 @@ class WriteYaml extends Write
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
# used by contentApiController (backend) and pageController (frontend)
|
||||
public function getPageMeta($settings, $item)
|
||||
{
|
||||
$meta = $this->getYaml($settings['contentFolder'], $item->pathWithoutType . '.yaml');
|
||||
|
||||
if(!$meta)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$meta = $this->addFileTimeToMeta($meta, $item, $settings);
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
# used by contentApiController (backend) and pageController (frontend)
|
||||
public function getPageMetaDefaults($content, $settings, $item)
|
||||
{
|
||||
# initialize parsedown extension
|
||||
$parsedown = new ParsedownExtension();
|
||||
|
||||
# if content is not an array, then transform it
|
||||
if(!is_array($content))
|
||||
{
|
||||
# turn markdown into an array of markdown-blocks
|
||||
$content = $parsedown->markdownToArrayBlocks($content);
|
||||
}
|
||||
|
||||
$title = false;
|
||||
|
||||
# delete markdown from title
|
||||
if(isset($content[0]))
|
||||
{
|
||||
$title = trim($content[0], "# ");
|
||||
}
|
||||
|
||||
$description = false;
|
||||
|
||||
# delete markdown from title
|
||||
if(isset($content[1]))
|
||||
{
|
||||
$firstLineArray = $parsedown->text($content[1]);
|
||||
$description = strip_tags($parsedown->markup($firstLineArray, $item->urlAbs));
|
||||
$description = substr($description, 0, 300);
|
||||
$lastSpace = strrpos($description, ' ');
|
||||
$description = substr($description, 0, $lastSpace);
|
||||
}
|
||||
|
||||
# create new meta-file
|
||||
$meta = [
|
||||
'meta' => [
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'author' => $settings['author'], # change to session, extend userdata
|
||||
]
|
||||
];
|
||||
|
||||
$this->updateYaml($settings['contentFolder'], $item->pathWithoutType . '.yaml', $meta);
|
||||
|
||||
$meta = $this->addFileTimeToMeta($meta, $item, $settings);
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
private function addFileTimeToMeta($meta, $item, $settings)
|
||||
{
|
||||
$filePath = $settings['contentFolder'] . $item->path;
|
||||
$fileType = isset($item->fileType) ? $item->fileType : 'md';
|
||||
|
||||
# check if url is a folder.
|
||||
if($item->elementType == 'folder')
|
||||
{
|
||||
$filePath = $settings['contentFolder'] . $item->path . DIRECTORY_SEPARATOR . 'index.'. $fileType;
|
||||
}
|
||||
|
||||
# add the modified date for the file
|
||||
$meta['meta']['modified'] = file_exists($filePath) ? date("Y-m-d",filemtime($filePath)) : false;
|
||||
|
||||
return $meta;
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
use Typemill\Controllers\SettingsController;
|
||||
use Typemill\Controllers\ContentController;
|
||||
use Typemill\Controllers\ContentApiController;
|
||||
use Typemill\Controllers\MetaApiController;
|
||||
use Typemill\Middleware\RestrictApiAccess;
|
||||
|
||||
$app->get('/api/v1/themes', SettingsController::class . ':getThemeSettings')->setName('api.themes')->add(new RestrictApiAccess($container['router']));
|
||||
@ -16,8 +17,12 @@ $app->post('/api/v1/article', ContentApiController::class . ':createArticle')->s
|
||||
$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/article/sort', ContentApiController::class . ':sortArticle')->setName('api.article.sort')->add(new RestrictApiAccess($container['router']));
|
||||
$app->post('/api/v1/basefolder', ContentApiController::class . ':createBaseFolder')->setName('api.basefolder.create')->add(new RestrictApiAccess($container['router']));
|
||||
$app->get('/api/v1/article/metaobject', MetaApiController::class . ':getArticleMetaobject')->setName('api.articlemetaobject.get')->add(new RestrictApiAccess($container['router']));
|
||||
$app->get('/api/v1/article/metadata', MetaApiController::class . ':getArticleMeta')->setName('api.articlemeta.get')->add(new RestrictApiAccess($container['router']));
|
||||
$app->post('/api/v1/article/metadata', MetaApiController::class . ':updateArticleMeta')->setName('api.articlemeta.update')->add(new RestrictApiAccess($container['router']));
|
||||
$app->post('/api/v1/baseitem', ContentApiController::class . ':createBaseItem')->setName('api.baseitem.create')->add(new RestrictApiAccess($container['router']));
|
||||
$app->get('/api/v1/navigation', ContentApiController::class . ':getNavigation')->setName('api.navigation.get')->add(new RestrictApiAccess($container['router']));
|
||||
$app->get('/api/v1/metadefinitions', MetaApiController::class . ':getMetaDefinitions')->setName('api.metadefinitions.get')->add(new RestrictApiAccess($container['router']));
|
||||
|
||||
$app->post('/api/v1/block', ContentApiController::class . ':addBlock')->setName('api.block.add')->add(new RestrictApiAccess($container['router']));
|
||||
$app->put('/api/v1/block', ContentApiController::class . ':updateBlock')->setName('api.block.update')->add(new RestrictApiAccess($container['router']));
|
||||
|
@ -80,7 +80,7 @@ class Settings
|
||||
|
||||
return $objectSettings;
|
||||
}
|
||||
|
||||
|
||||
public static function createSettings()
|
||||
{
|
||||
$yaml = new Models\WriteYaml();
|
||||
|
@ -11,7 +11,7 @@
|
||||
<h1>Hurra!</h1>
|
||||
<p>Your account has been created and you are logged in now.</p>
|
||||
<p><strong>Next step:</strong> Visit the author panel and setup your new website. You can configure the system, choose themes and add plugins.</p>
|
||||
<p><strong>New:</strong> You never use code-examples on your pages? Then disable the code-button and adjust the whole format-bar of the editor exactly to your needs.</p>
|
||||
<p><strong>New:</strong>Hurra! Version 1.3.0 is out and now you can edit meta-information like title and description.</p>
|
||||
<p><strong>Get help:</strong> If you have any questions, please consult the <a target="_blank" href="https://typemill.net/typemill"><i class="icon-link-ext"></i> docs</a> or open a new issue on <a target="_blank" href="https://github.com/typemill/typemill"><i class="icon-link-ext"></i> github</a>.</p>
|
||||
</div>
|
||||
<a class="button" href="{{ path_for('settings.show') }}">Configure your website</a>
|
||||
|
@ -2,7 +2,7 @@
|
||||
* TRANSITION *
|
||||
**********************/
|
||||
|
||||
a, a:link, a:visited, a:focus, a:hover, a:active, button, .button, input, .control-group, .sidebar-menu, .sidebar-menu--content, .menu-action, .button-arrow{
|
||||
a, a:link, a:visited, a:focus, a:hover, a:active, button, .button, .tab-button, input, .control-group, .sidebar-menu, .sidebar-menu--content, .menu-action, .button-arrow{
|
||||
-webkit-transition: color 0.2s ease;
|
||||
-moz-transition: color 0.2s ease;
|
||||
-o-transition: color 0.2s ease;
|
||||
@ -283,6 +283,9 @@ span.level-3{ padding-left: 50px; }
|
||||
span.level-4{ padding-left: 60px; }
|
||||
span.level-5{ padding-left: 70px; }
|
||||
|
||||
.addBaseItem{
|
||||
margin-left: -10px;
|
||||
}
|
||||
.addNaviItem{
|
||||
padding: 5px;
|
||||
display: block;
|
||||
@ -630,7 +633,41 @@ header.headline
|
||||
{
|
||||
padding: 0px 0px;
|
||||
}
|
||||
header a.button{
|
||||
float: right;
|
||||
margin: 1.5em 0.5em 0 0;
|
||||
}
|
||||
|
||||
/********************
|
||||
* Meta-Tabs *
|
||||
********************/
|
||||
|
||||
.metanav{
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
margin-top: -35px;
|
||||
z-index: 1;
|
||||
}
|
||||
.tab-button{
|
||||
font-size: 0.9em;
|
||||
background: transparent;
|
||||
padding: 10px 10px;
|
||||
margin: 0px 5px 0px 0px;
|
||||
min-width: 120px;
|
||||
border: 0px;
|
||||
}
|
||||
.tab-button:focus,
|
||||
.tab-button:hover,
|
||||
.tab-button:active,
|
||||
.tab-button.active{
|
||||
background: #fff;
|
||||
box-shadow: -2px -2px 4px #eee;
|
||||
outline: none;
|
||||
}
|
||||
section.tab{
|
||||
z-index: -1;
|
||||
padding: 40px;
|
||||
}
|
||||
/********************
|
||||
* Forms *
|
||||
********************/
|
||||
@ -972,7 +1009,7 @@ ul.cardInfo{
|
||||
.error p{
|
||||
color: #e0474c;
|
||||
}
|
||||
.errors .cardHead header{
|
||||
.errors .cardHead header{
|
||||
background: #e0474c;
|
||||
color: #fff;
|
||||
}
|
||||
@ -985,6 +1022,25 @@ span.error{
|
||||
margin-top: -8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.metaLarge{
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 18px 20px 0px;
|
||||
}
|
||||
.metaErrors,.metaSuccess{
|
||||
width: 100%;
|
||||
padding: 4px 18px;
|
||||
color: #fff;
|
||||
border-radius: 1px;
|
||||
text-align:center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.metaErrors{
|
||||
background: #e0474c;
|
||||
}
|
||||
.metaSuccess{
|
||||
background: #70c1b3;
|
||||
}
|
||||
|
||||
|
||||
/********************
|
||||
|
@ -5,8 +5,29 @@
|
||||
|
||||
<div class="formWrapper">
|
||||
|
||||
<section id="blox">
|
||||
|
||||
<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>
|
||||
|
||||
<section id="blox" :class="showBlox">
|
||||
|
||||
<div class="blox-body">
|
||||
|
||||
<transition name="fade" v-cloak>
|
||||
@ -55,7 +76,7 @@
|
||||
</section>
|
||||
|
||||
{% include 'editor/publish-controller.twig' %}
|
||||
|
||||
|
||||
<input id="path" type="hidden" value="{{ item.urlRel }}" required readonly />
|
||||
{{ csrf_field() | raw }}
|
||||
|
||||
|
@ -154,7 +154,7 @@
|
||||
pluginList += value[i].id + ',';
|
||||
}
|
||||
|
||||
url += pluginList;
|
||||
url = 'https://plugins.typemill.net/api/v1/checkversion?' + pluginList;
|
||||
}
|
||||
|
||||
if(name == 'theme')
|
||||
@ -165,7 +165,7 @@
|
||||
themeList += value[i].id + ',';
|
||||
}
|
||||
|
||||
url += themeList;
|
||||
url = 'https://themes.typemill.net/api/v1/checkversion?' + themeList;
|
||||
}
|
||||
|
||||
sendJson(function(response)
|
||||
@ -176,15 +176,15 @@
|
||||
|
||||
if(name == 'system' && versions.system)
|
||||
{
|
||||
updateVersions(versions.system);
|
||||
updateVersions(versions.system, 'system');
|
||||
}
|
||||
if(name == 'plugins' && versions.plugins)
|
||||
{
|
||||
updateVersions(versions.plugins);
|
||||
updateVersions(versions.plugins, 'plugins');
|
||||
}
|
||||
if(name == 'theme' && versions.themes)
|
||||
{
|
||||
updateVersions(versions.themes);
|
||||
updateVersions(versions.themes, 'themes');
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -194,7 +194,7 @@
|
||||
}, getPost, url, false, true);
|
||||
}
|
||||
|
||||
function updateVersions(elementVersions)
|
||||
function updateVersions(elementVersions,type)
|
||||
{
|
||||
for (var key in elementVersions)
|
||||
{
|
||||
@ -204,7 +204,20 @@
|
||||
|
||||
if(elementVersions[key] && singleElement && cmpVersions(elementVersions[key], singleElement.innerHTML) > 0)
|
||||
{
|
||||
singleElement.innerHTML = "<span>update<br/>to " + elementVersions[key] + "</span>";
|
||||
if(type == 'themes')
|
||||
{
|
||||
var html = '<a href="https://themes.typemill.net/' + key + '" target="blank"><span>update<br/>to ' + elementVersions[key] + '</span></a>';
|
||||
}
|
||||
else if (type == 'plugins')
|
||||
{
|
||||
var html = '<a href="https://plugins.typemill.net/' + key + '" target="blank"><span>update<br/>to ' + elementVersions[key] + '</span></a>';
|
||||
}
|
||||
else
|
||||
{
|
||||
var html = '<a href="https://typemill.net" target="blank"><span>update<br/>to ' + elementVersions[key] + '</span></a>';
|
||||
}
|
||||
|
||||
singleElement.innerHTML = html;
|
||||
singleElement.classList.add("show-banner");
|
||||
}
|
||||
}
|
||||
@ -262,60 +275,64 @@
|
||||
|
||||
var target = document.querySelectorAll('input[type=color]');
|
||||
// set hooks for each target element
|
||||
for (var i = 0, len = target.length; i < len; ++i)
|
||||
{
|
||||
var thisTarget = target[i];
|
||||
|
||||
(function(thisTarget){
|
||||
|
||||
if(target)
|
||||
{
|
||||
for (var i = 0, len = target.length; i < len; ++i)
|
||||
{
|
||||
var thisTarget = target[i];
|
||||
|
||||
/* hide the input field and show color box instead */
|
||||
var box = document.createElement('div');
|
||||
(function(thisTarget){
|
||||
|
||||
/* hide the input field and show color box instead */
|
||||
var box = document.createElement('div');
|
||||
|
||||
box.className = 'color-box';
|
||||
box.style.backgroundColor = thisTarget.value;
|
||||
box.setAttribute('data-color', thisTarget.value);
|
||||
thisTarget.parentNode.insertBefore(box, thisTarget);
|
||||
thisTarget.type = 'hidden';
|
||||
box.className = 'color-box';
|
||||
box.style.backgroundColor = thisTarget.value;
|
||||
box.setAttribute('data-color', thisTarget.value);
|
||||
thisTarget.parentNode.insertBefore(box, thisTarget);
|
||||
thisTarget.type = 'hidden';
|
||||
|
||||
var picker = new CP(box),
|
||||
code = document.createElement('input');
|
||||
|
||||
picker.target.onclick = function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
code.className = 'color-code';
|
||||
code.pattern = '^#[A-Fa-f0-9]{6}$';
|
||||
code.type = 'text';
|
||||
|
||||
picker.on("enter", function() {
|
||||
code.value = '#' + CP._HSV2HEX(this.get());
|
||||
});
|
||||
var picker = new CP(box),
|
||||
code = document.createElement('input');
|
||||
|
||||
picker.target.onclick = function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
code.className = 'color-code';
|
||||
code.pattern = '^#[A-Fa-f0-9]{6}$';
|
||||
code.type = 'text';
|
||||
|
||||
picker.on("enter", function() {
|
||||
code.value = '#' + CP._HSV2HEX(this.get());
|
||||
});
|
||||
|
||||
|
||||
picker.on("change", function(color) {
|
||||
thisTarget.value = '#' + color;
|
||||
this.target.style.backgroundColor = '#' + color;
|
||||
code.value = '#' + color;
|
||||
});
|
||||
|
||||
picker.picker.firstChild.appendChild(code);
|
||||
picker.on("change", function(color) {
|
||||
thisTarget.value = '#' + color;
|
||||
this.target.style.backgroundColor = '#' + color;
|
||||
code.value = '#' + color;
|
||||
});
|
||||
|
||||
picker.picker.firstChild.appendChild(code);
|
||||
|
||||
function update() {
|
||||
if (this.value.length) {
|
||||
picker.set(this.value);
|
||||
picker.trigger("change", [this.value.slice(1)]);
|
||||
function update() {
|
||||
if (this.value.length) {
|
||||
picker.set(this.value);
|
||||
picker.trigger("change", [this.value.slice(1)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code.oncut = update;
|
||||
code.onpaste = update;
|
||||
code.onkeyup = update;
|
||||
code.oninput = update;
|
||||
|
||||
|
||||
})(thisTarget);
|
||||
code.oncut = update;
|
||||
code.onpaste = update;
|
||||
code.onkeyup = update;
|
||||
code.oninput = update;
|
||||
|
||||
|
||||
})(thisTarget);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
5
system/author/js/sortable.min.js
vendored
5
system/author/js/sortable.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1441,25 +1441,6 @@ const imageComponent = Vue.component('image-component', {
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
let componentList = {
|
||||
'content-component': contentComponent,
|
||||
'markdown-component': markdownComponent,
|
||||
'hr-component': hrComponent,
|
||||
'toc-component': tocComponent,
|
||||
'title-component': titleComponent,
|
||||
'headline-component': headlineComponent,
|
||||
'image-component': imageComponent,
|
||||
'code-component': codeComponent,
|
||||
'quote-component': quoteComponent,
|
||||
'ulist-component': ulistComponent,
|
||||
'olist-component': olistComponent,
|
||||
'table-component': tableComponent,
|
||||
'definition-component': definitionComponent,
|
||||
'math-component': mathComponent
|
||||
}
|
||||
*/
|
||||
|
||||
let activeFormats = [];
|
||||
|
||||
for(var i = 0; i < formatConfig.length; i++)
|
||||
@ -1489,6 +1470,7 @@ let editor = new Vue({
|
||||
draftDisabled: true,
|
||||
bloxOverlay: false,
|
||||
sortdisabled: false,
|
||||
showBlox: 'show',
|
||||
formats: activeFormats
|
||||
},
|
||||
mounted: function(){
|
||||
|
220
system/author/js/vue-meta.js
Normal file
220
system/author/js/vue-meta.js
Normal file
@ -0,0 +1,220 @@
|
||||
const FormBus = new Vue();
|
||||
|
||||
Vue.component('component-text', {
|
||||
props: ['class', 'placeholder', 'label', 'name', 'type', 'size', 'value', 'errors'],
|
||||
template: '<div class="large">' +
|
||||
'<label>{{ label }}</label>' +
|
||||
'<input type="text" :name="name" :placeholder="placeholder" :value="value" @input="update($event, name)">' +
|
||||
'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
|
||||
'</div>',
|
||||
methods: {
|
||||
update: function($event, name)
|
||||
{
|
||||
FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Vue.component('component-date', {
|
||||
props: ['class', 'placeholder', 'readonly', 'label', 'name', 'type', 'size', 'value', 'errors'],
|
||||
template: '<div class="large">' +
|
||||
'<label>{{ label }}</label>' +
|
||||
'<input type="date" :readonly="readonly" :name="name" :placeholder="placeholder" :value="value" @input="update($event, name)">' +
|
||||
'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
|
||||
'</div>',
|
||||
methods: {
|
||||
update: function($event, name)
|
||||
{
|
||||
FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Vue.component('component-textarea', {
|
||||
props: ['class', 'placeholder', 'label', 'name', 'type', 'size', 'value', 'errors'],
|
||||
template: '<div class="large">' +
|
||||
'<label>{{label}}</label>' +
|
||||
'<textarea :name="name" v-model="value" @input="update($event, name)"></textarea>' +
|
||||
'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
|
||||
'</div>',
|
||||
methods: {
|
||||
update: function($event, name)
|
||||
{
|
||||
FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Vue.component('component-select', {
|
||||
props: ['class', 'placeholder', 'label', 'name', 'type', 'size', '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>' +
|
||||
'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
|
||||
'</div>',
|
||||
methods: {
|
||||
update: function($event, name)
|
||||
{
|
||||
FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Vue.component('component-checkbox', {
|
||||
props: ['class', '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)">' +
|
||||
'<span class="checkmark"></span>' +
|
||||
'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
|
||||
'</label>' +
|
||||
'</div>',
|
||||
methods: {
|
||||
update: function($event, value, name)
|
||||
{
|
||||
FormBus.$emit('forminput', {'name': name, 'value' : value});
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Vue.component('component-radio', {
|
||||
props: ['label', 'options', 'name', 'type', 'value', 'errors'],
|
||||
template: '<div class="medium">' +
|
||||
'<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)">' +
|
||||
'<span class="radiomark"></span>' +
|
||||
'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
|
||||
'</label>' +
|
||||
'</div>',
|
||||
methods: {
|
||||
update: function($event, value, name)
|
||||
{
|
||||
FormBus.$emit('forminput', {'name': name, 'value' : value});
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Vue.component('tab-meta', {
|
||||
props: ['saved', 'errors', 'formdata', 'schema'],
|
||||
template: '<section><form>' +
|
||||
'<component v-for="(field, index) in schema.fields"' +
|
||||
':key="index"' +
|
||||
':is="selectComponent(field)"' +
|
||||
':errors="errors"' +
|
||||
':name="index"' +
|
||||
'v-model="formdata[index]"' +
|
||||
'v-bind="field">' +
|
||||
'</component>' +
|
||||
'<div v-if="saved" class="metaLarge"><div class="metaSuccess">Saved successfully</div></div>' +
|
||||
'<div v-if="errors" class="metaLarge"><div class="metaErrors">Please correct the errors above</div></div>' +
|
||||
'<div class="large"><input type="submit" @click.prevent="saveInput" value="save"></input></div>' +
|
||||
'</form></section>',
|
||||
methods: {
|
||||
selectComponent: function(field)
|
||||
{
|
||||
return 'component-'+field.type;
|
||||
},
|
||||
saveInput: function()
|
||||
{
|
||||
this.$emit('saveform');
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
let meta = new Vue({
|
||||
delimiters: ['${', '}'],
|
||||
el: '#metanav',
|
||||
data: function () {
|
||||
return {
|
||||
root: document.getElementById("main").dataset.url, /* get url of current page */
|
||||
currentTab: 'Content',
|
||||
tabs: ['Content'],
|
||||
formDefinitions: [],
|
||||
formData: [],
|
||||
formErrors: {},
|
||||
formErrorsReset: {},
|
||||
saved: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentTabComponent: function () {
|
||||
if(this.currentTab == 'Content')
|
||||
{
|
||||
editor.showBlox = 'show';
|
||||
}
|
||||
else
|
||||
{
|
||||
editor.showBlox = 'hidden';
|
||||
}
|
||||
return 'tab-' + this.currentTab.toLowerCase()
|
||||
}
|
||||
},
|
||||
mounted: function(){
|
||||
|
||||
var self = this;
|
||||
|
||||
myaxios.get('/api/v1/article/metaobject',{
|
||||
params: {
|
||||
'url': document.getElementById("path").value,
|
||||
'csrf_name': document.getElementById("csrf_name").value,
|
||||
'csrf_value': document.getElementById("csrf_value").value,
|
||||
}
|
||||
})
|
||||
.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;
|
||||
|
||||
self.formData = response.data.metadata;
|
||||
|
||||
})
|
||||
.catch(function (error)
|
||||
{
|
||||
if(error.response)
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
FormBus.$on('forminput', formdata => {
|
||||
this.$set(this.formData[this.currentTab], formdata.name, formdata.value);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
saveForm: function()
|
||||
{
|
||||
this.saved = false;
|
||||
|
||||
self = this;
|
||||
|
||||
myaxios.post('/api/v1/article/metadata',{
|
||||
'url': document.getElementById("path").value,
|
||||
'csrf_name': document.getElementById("csrf_name").value,
|
||||
'csrf_value': document.getElementById("csrf_value").value,
|
||||
'tab': self.currentTab,
|
||||
'data': self.formData[self.currentTab]
|
||||
})
|
||||
.then(function (response) {
|
||||
self.saved = true;
|
||||
self.formErrors = self.formErrorsReset;
|
||||
})
|
||||
.catch(function (error)
|
||||
{
|
||||
if(error.response)
|
||||
{
|
||||
self.formErrors = error.response.data.errors;
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
@ -1,9 +1,10 @@
|
||||
const navcomponent = Vue.component('navigation', {
|
||||
template: '#navigation-template',
|
||||
props: ['homepage', 'name', 'newItem', 'parent', 'active', 'filetype', 'status', 'elementtype', 'element', 'folder', 'level', 'url', 'root', 'freeze'],
|
||||
props: ['homepage', 'showForm', 'name', 'newItem', 'parent', 'active', 'filetype', 'status', 'elementtype', 'element', 'folder', 'level', 'url', 'root', 'freeze'],
|
||||
data: function () {
|
||||
return {
|
||||
showForm: false,
|
||||
revert: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -11,7 +12,7 @@ const navcomponent = Vue.component('navigation', {
|
||||
{
|
||||
if(evt.dragged.classList.contains('folder') && evt.from.parentNode.id != evt.to.parentNode.id)
|
||||
{
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
if(evt.dragged.firstChild.className == 'active' && !editor.draftDisabled)
|
||||
{
|
||||
@ -21,7 +22,7 @@ const navcomponent = Vue.component('navigation', {
|
||||
return true;
|
||||
},
|
||||
onStart : function(evt)
|
||||
{
|
||||
{
|
||||
/* delete error messages if exist */
|
||||
publishController.errors.message = false;
|
||||
},
|
||||
@ -29,14 +30,14 @@ const navcomponent = Vue.component('navigation', {
|
||||
{
|
||||
var locator = {
|
||||
'item_id': evt.item.id,
|
||||
'parent_id_from': evt.from.parentNode.id,
|
||||
'parent_id_to': evt.to.parentNode.id,
|
||||
'parent_id_from': evt.from.parentNode.id,
|
||||
'parent_id_to': evt.to.parentNode.id,
|
||||
'index_old': evt.oldIndex,
|
||||
'index_new': evt.newIndex,
|
||||
'active': evt.item.getElementsByTagName('a')[0].className,
|
||||
'url': document.getElementById("path").value,
|
||||
'csrf_name': document.getElementById("csrf_name").value,
|
||||
'csrf_value': document.getElementById("csrf_value").value,
|
||||
'csrf_value': document.getElementById("csrf_value").value,
|
||||
};
|
||||
|
||||
if(locator.parent_id_from == locator.parent_id_to && locator.index_old == locator.index_new)
|
||||
@ -193,11 +194,26 @@ let navi = new Vue({
|
||||
modalWindow: false,
|
||||
format: /[!@#$%^&*()_+=\[\]{};':"\\|,.<>\/?]/,
|
||||
folderName: '',
|
||||
showForm: false,
|
||||
newItem: '',
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
checkMove: function(evt){
|
||||
/* this.$refs.draggit[0].checkMove(evt); */
|
||||
if(evt.dragged.classList.contains('folder') && evt.from.parentNode.id != evt.to.parentNode.id)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if(evt.dragged.firstChild.className == 'active' && !editor.draftDisabled)
|
||||
{
|
||||
publishController.errors.message = "Please save your changes before you move the file";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onStart: function(evt){
|
||||
this.$refs.draggit[0].onStart(evt);
|
||||
this.$refs.draggit[0].onStart(evt);
|
||||
},
|
||||
onEnd: function(evt){
|
||||
this.$refs.draggit[0].onEnd(evt);
|
||||
@ -208,29 +224,34 @@ let navi = new Vue({
|
||||
hideModal: function(e){
|
||||
this.modalWindow = false;
|
||||
},
|
||||
addFolder: function()
|
||||
toggleForm : function()
|
||||
{
|
||||
this.showForm = !this.showForm;
|
||||
},
|
||||
addFile : function(type)
|
||||
{
|
||||
publishController.errors.message = false;
|
||||
|
||||
if(this.format.test(this.folderName) || this.folderName < 1 || this.folderName.length > 20)
|
||||
{
|
||||
publishController.errors.message = 'Special Characters are not allowed. Length between 1 and 20.';
|
||||
if(this.format.test(this.newItem) || !this.newItem || this.newItem.length > 40)
|
||||
{
|
||||
publishController.errors.message = 'Special Characters are not allowed. Length between 1 and 40.';
|
||||
return;
|
||||
}
|
||||
|
||||
var newFolder = {
|
||||
'item_name': this.folderName,
|
||||
var newItem = {
|
||||
'item_name': this.newItem,
|
||||
'type': type,
|
||||
'url': document.getElementById("path").value,
|
||||
'csrf_name': document.getElementById("csrf_name").value,
|
||||
'csrf_value': document.getElementById("csrf_value").value,
|
||||
};
|
||||
|
||||
|
||||
var self = this;
|
||||
|
||||
self.freeze = true;
|
||||
self.errors = {title: false, content: false, message: false};
|
||||
|
||||
var url = this.root + '/api/v1/basefolder';
|
||||
var url = this.root + '/api/v1/baseitem';
|
||||
var method = 'POST';
|
||||
|
||||
sendJson(function(response, httpStatus)
|
||||
@ -251,9 +272,10 @@ let navi = new Vue({
|
||||
if(result.data)
|
||||
{
|
||||
self.items = result.data;
|
||||
self.showForm = false;
|
||||
}
|
||||
}
|
||||
}, method, url, newFolder );
|
||||
}, method, url, newItem );
|
||||
},
|
||||
getNavi: function()
|
||||
{
|
||||
|
2
system/author/js/vuedraggable.umd.min.js
vendored
2
system/author/js/vuedraggable.umd.min.js
vendored
File diff suppressed because one or more lines are too long
@ -15,7 +15,6 @@
|
||||
<link rel="icon" type="image/png" href="{{ base_url }}/system/author/img/favicon-16x16.png" sizes="16x16" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="{{ base_url }}/system/author/img/apple-touch-icon-144x144.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{{ base_url }}/system/author/img/apple-touch-icon-152x152.png" />
|
||||
|
||||
|
||||
<link rel="stylesheet" href="{{ base_url }}/system/author/css/normalize.css" />
|
||||
<link rel="stylesheet" href="{{ base_url }}/system/author/css/style.css?20191124" />
|
||||
@ -160,6 +159,11 @@
|
||||
</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?20191124"></script>
|
||||
<script src="{{ base_url }}/system/author/js/autosize.min.js?20191124"></script>
|
||||
<script src="{{ base_url }}/system/author/js/sortable.min.js?20191124"></script>
|
||||
@ -174,6 +178,7 @@
|
||||
{{ assets.renderEditorJS() }}
|
||||
|
||||
<script src="{{ base_url }}/system/author/js/vue-blox.js?20191124"></script>
|
||||
<script src="{{ base_url }}/system/author/js/vue-meta.js?20191124"></script>
|
||||
<script src="{{ base_url }}/system/author/js/vue-navi.js?20191124"></script>
|
||||
<script src="{{ base_url }}/system/author/js/lazy-video.js?2019124"></script>
|
||||
|
||||
|
21
system/author/metatabs.yaml
Normal file
21
system/author/metatabs.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
meta:
|
||||
fields:
|
||||
title:
|
||||
type: text
|
||||
label: Meta title
|
||||
size: 60
|
||||
class: large
|
||||
description:
|
||||
type: textarea
|
||||
label: Meta description
|
||||
size: 160
|
||||
class: large
|
||||
author:
|
||||
type: text
|
||||
label: author
|
||||
class: large
|
||||
modified:
|
||||
type: date
|
||||
label: Last modified at (readonly)
|
||||
readonly: readonly
|
||||
class: large
|
@ -5,17 +5,17 @@
|
||||
<div class="navi-list">
|
||||
<div class="navi-item folder">
|
||||
<div class="status" :class="homepage.status"></div>
|
||||
<a href="{{ base_url }}/tm/content/{{ settings.editor }}" :class="homepage.active"><span><span class="iconwrapper"><svg class="icon icon-plus"><use xlink:href="#icon-plus"></use></svg></span><span class="level-1">Homepage</span></a>
|
||||
<a href="{{ base_url }}/tm/content/{{ settings.editor }}" :class="homepage.active"><span><span class="iconwrapper"><svg class="icon icon-home"><use xlink:href="#icon-home"></use></svg></span><span class="level-1">Homepage</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<draggable class="navi-list"
|
||||
<draggable class="navi-list" tag="ul"
|
||||
@start="onStart"
|
||||
@end="onEnd"
|
||||
tag="ul"
|
||||
:list="items"
|
||||
group="folder"
|
||||
:move="checkMove"
|
||||
group="file"
|
||||
animation="150"
|
||||
:disabled="freeze">
|
||||
:disabled="freeze">
|
||||
<navigation
|
||||
v-for="item in items"
|
||||
ref="draggit"
|
||||
@ -34,13 +34,20 @@
|
||||
:folder="item.folderContent"
|
||||
></navigation>
|
||||
</draggable>
|
||||
<ul class="navi-list addBaseFolder">
|
||||
<ul class="navi-list addBaseItem">
|
||||
<li class="navi-item file">
|
||||
<span class="iconwrapper"><svg class="icon icon-plus"><use xlink:href="#icon-plus"></use></svg></span>
|
||||
<div class="addNaviForm">
|
||||
<input type="text" v-model="folderName" />
|
||||
<button class="fullWidth" @click="addFolder">add folder to base level</button>
|
||||
</div>
|
||||
<span class="iconwrapper">
|
||||
<svg class="icon icon-plus"><use xlink:href="#icon-plus"></use></svg>
|
||||
</span>
|
||||
<span class="addNaviItem">
|
||||
<a class="addNaviLink" href="#" @click.prevent="toggleForm">add item</a>
|
||||
</span>
|
||||
<transition name="fade">
|
||||
<div v-if="showForm" class="addNaviForm">
|
||||
<input v-model="newItem">
|
||||
<button class="b-left" @click="addFile('file')">add file</button><button class="b-right" @click="addFile('folder')">add folder</button>
|
||||
</div>
|
||||
</transition>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -55,10 +62,10 @@
|
||||
@start="onStart"
|
||||
@end="onEnd"
|
||||
:list="folder"
|
||||
:move="checkMove"
|
||||
:move="checkMove"
|
||||
group="file"
|
||||
animation="150"
|
||||
:disabled="freeze">
|
||||
:disabled="freeze">
|
||||
<navigation
|
||||
v-for="item in folder"
|
||||
ref="draggit"
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
<header class="headline">
|
||||
<h1>Plugins</h1>
|
||||
<a class="button" target="_blank" href="https://plugins.typemill.net">Plugin Store</a>
|
||||
</header>
|
||||
|
||||
{% for pluginName,plugin in plugins %}
|
||||
|
@ -51,7 +51,7 @@
|
||||
<span class="error">{{ errors.settings.year | first }}</span>
|
||||
{% endif %}
|
||||
</div><div class="medium{{ errors.settings.language ? ' error' : '' }}">
|
||||
<label for="settings[language]">Language</label>
|
||||
<label for="settings[language]">Language-Attribute <small>(HTML)</small></label>
|
||||
<select name="settings[language]" id="language">
|
||||
{% for key,lang in languages %}
|
||||
<option value="{{ key }}"{% if (key == old.settings.language or key == mylang) %} selected{% endif %}>{{ lang }}</option>
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
<header class="headline">
|
||||
<h1>Themes</h1>
|
||||
<a class="button" target="_blank" href="https://themes.typemill.net">Theme Store</a>
|
||||
</header>
|
||||
|
||||
{% for themeName, theme in themes %}
|
||||
|
@ -201,6 +201,7 @@ $container['view'] = function ($container)
|
||||
$view->addExtension(new Twig_Extension_Debug());
|
||||
$view->addExtension(new Typemill\Extensions\TwigUserExtension());
|
||||
$view->addExtension(new Typemill\Extensions\TwigMarkdownExtension());
|
||||
$view->addExtension(new Typemill\Extensions\TwigMetaExtension());
|
||||
|
||||
/* use {{ base_url() }} in twig templates */
|
||||
$view['base_url'] = $container['request']->getUri()->getBaseUrl();
|
||||
|
@ -193,11 +193,17 @@ header p{
|
||||
.main-menu li.folder.level-1{
|
||||
padding-left: 15px;
|
||||
}
|
||||
.main-menu li.file.level-1{
|
||||
padding-left: 20px;
|
||||
}
|
||||
.main-menu > ul > li
|
||||
{
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.main-menu > ul > li.folder
|
||||
{
|
||||
text-transform: uppercase;
|
||||
margin: 15px 0 5px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% extends '/partials/layout.twig' %}
|
||||
|
||||
{% block title %}{{ title }} | {{ settings.title }}{% endblock %}
|
||||
{% block title %}{{ metatabs.meta.title | default(title) }} | {{ settings.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
<small>{{ settings.themes.typemill.authorIntro }}: {{ settings.author }}</small>
|
||||
{% endif %}
|
||||
{% if settings.themes.typemill.modifiedPosition.top %}
|
||||
<small>{{ settings.themes.typemill.modifiedText }}: {{ item.modified|date(settings.themes.typemill.modifiedFormat) }}</small>
|
||||
<small>{{ settings.themes.typemill.modifiedText }}: {{ metatabs.meta.modified|date(settings.themes.typemill.modifiedFormat) }}</small>
|
||||
{% endif %}
|
||||
{% if settings.themes.typemill.socialPosition.top %}
|
||||
<div id="share-icons" class="share-icons hide">
|
||||
@ -37,7 +37,7 @@
|
||||
<small>{{ settings.themes.typemill.authorIntro }}: {{ settings.author }}</small>
|
||||
{% endif %}
|
||||
{% if settings.themes.typemill.modifiedPosition.bottom %}
|
||||
<small>{{ settings.themes.typemill.modifiedText }}: {{ item.modified|date(settings.themes.typemill.modifiedFormat) }}</small>
|
||||
<small>{{ settings.themes.typemill.modifiedText }}: {{ metatabs.meta.modified|date(settings.themes.typemill.modifiedFormat) }}</small>
|
||||
{% endif %}
|
||||
{% if settings.themes.typemill.socialPosition.bottom %}
|
||||
<div id="share-icons-bottom" class="share-icons hide">
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
<base href="{{ base_url }}/">
|
||||
|
||||
<meta name="description" content="{{ description }}" />
|
||||
<meta name="description" content="{{ metatabs.meta.description }}" />
|
||||
<meta name="author" content="{{ settings.author }}" />
|
||||
<meta name="generator" content="TYPEMILL" />
|
||||
<meta name="msapplication-TileColor" content="#F9F8F6" />
|
||||
@ -21,8 +21,8 @@
|
||||
<link rel="canonical" href="{{ item.urlAbs }}" />
|
||||
|
||||
<meta property="og:site_name" content="{{ settings.title }}">
|
||||
<meta property="og:title" content="{{ title }}">
|
||||
<meta property="og:description" content="{{ description }}">
|
||||
<meta property="og:title" content="{{ metatabs.meta.title }}">
|
||||
<meta property="og:description" content="{{ metatabs.meta.description }}">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="{{ item.urlAbs }}">
|
||||
<meta property="og:image" content="{{ image.img_url }}">
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
<base href="{{ base_url }}/">
|
||||
|
||||
<meta name="description" content="{{ description }}" />
|
||||
<meta name="description" content="{{ metatabs.meta.description }}" />
|
||||
<meta name="author" content="{{ settings.author }}" />
|
||||
<meta name="generator" content="TYPEMILL" />
|
||||
<meta name="msapplication-TileColor" content="#F9F8F6" />
|
||||
@ -21,8 +21,8 @@
|
||||
<link rel="canonical" href="{{ item.urlAbs }}" />
|
||||
|
||||
<meta property="og:site_name" content="{{ settings.title }}">
|
||||
<meta property="og:title" content="{{ title }}">
|
||||
<meta property="og:description" content="{{ description }}">
|
||||
<meta property="og:title" content="{{ metatabs.meta.title }}">
|
||||
<meta property="og:description" content="{{ metatabs.meta.description }}">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="{{ item.urlAbs }}">
|
||||
<meta property="og:image" content="{{ image.img_url }}">
|
||||
|
@ -1,5 +1,5 @@
|
||||
name: Typemill Theme
|
||||
version: 1.1.8
|
||||
version: 1.1.9
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user