[deploy] Redesign article editor (#7524)
This commit is contained in:
parent
e812f995fe
commit
a161d3ce31
93 changed files with 4342 additions and 2068 deletions
1
app/assets/images/x.svg
Normal file
1
app/assets/images/x.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" /></svg>
|
||||
|
After Width: | Height: | Size: 241 B |
|
|
@ -1,9 +1,29 @@
|
|||
@import '../config/import';
|
||||
|
||||
:root {
|
||||
--layout-site-width: var(--site-width);
|
||||
--layout-header-height: var(--header-height);
|
||||
--layout-sidebar-width: 200px;
|
||||
--layout-aside-width: 316px;
|
||||
--layout-gap: var(--su-6);
|
||||
--layout-padding: var(--su-6);
|
||||
--layout-article-content-padding-x: var(--su-3);
|
||||
--layout-article-content-padding-y: var(--su-3);
|
||||
--layout-narrow-sidebar-width: 66px;
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
--layout-article-content-padding-x: var(--su-4);
|
||||
--layout-article-content-padding-y: var(--su-4);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-m) {
|
||||
--layout-article-content-padding-x: var(--su-6);
|
||||
--layout-article-content-padding-y: var(--su-6);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-l) {
|
||||
--layout-article-content-padding-x: var(--su-8);
|
||||
}
|
||||
}
|
||||
|
||||
.crayons-layout {
|
||||
|
|
@ -12,12 +32,6 @@
|
|||
max-width: var(--site-width);
|
||||
margin: 0 auto;
|
||||
|
||||
&__sidebar {
|
||||
}
|
||||
|
||||
&__content {
|
||||
}
|
||||
|
||||
&--max-l {
|
||||
max-width: $breakpoint-l;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
min-width: 200px;
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
max-width: 320px;
|
||||
max-width: 360px;
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,8 +229,15 @@
|
|||
&--ghost:hover,
|
||||
&--ghost:focus,
|
||||
&--ghost[disabled] {
|
||||
all: unset;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
box-shadow: none;
|
||||
resize: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
.crayons-modal {
|
||||
--modal-max-width: 768px;
|
||||
--modal-body-padding: var(--su-2);
|
||||
--modal-body-padding: var(--su-3);
|
||||
--modal-header-padding: var(--su-2);
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
|
|
@ -11,8 +11,12 @@
|
|||
}
|
||||
|
||||
@media (min-width: $breakpoint-m) {
|
||||
--modal-body-padding: var(--su-7);
|
||||
--modal-header-padding: var(--su-4) var(--su-4) var(--su-4) var(--su-7);
|
||||
--modal-body-padding: var(--su-6);
|
||||
--modal-header-padding: var(--su-4) var(--su-4) var(--su-4) var(--su-6);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-l) {
|
||||
--modal-body-padding: var(--su-7) var(--su-8);
|
||||
}
|
||||
|
||||
position: fixed;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
.crayons-tabs {
|
||||
// Setup
|
||||
--border-width: 3px;
|
||||
--tab-vertical-padding: var(--su-3);
|
||||
--tab-vertical-padding: var(--su-2);
|
||||
--tab-horizontal-padding: var(--su-2);
|
||||
--font-size: var(--fs-base);
|
||||
display: flex;
|
||||
|
|
@ -31,6 +31,8 @@
|
|||
border-radius: var(--radius);
|
||||
transition: all var(--transition-props);
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
width: auto;
|
||||
|
|
|
|||
|
|
@ -613,7 +613,7 @@
|
|||
'heavy': var(--fw-heavy)
|
||||
),
|
||||
(),
|
||||
false
|
||||
true
|
||||
),
|
||||
(
|
||||
'list',
|
||||
|
|
|
|||
|
|
@ -22,3 +22,6 @@
|
|||
@import 'components/stories';
|
||||
@import 'components/tabs';
|
||||
@import 'components/tags';
|
||||
|
||||
@import 'views/article';
|
||||
@import 'views/article-form';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
|
||||
// Alternate base fonts
|
||||
body.sans-serif-article-body {
|
||||
.body {
|
||||
.body,
|
||||
.crayons-article__body {
|
||||
font-family: var(--ff-default);
|
||||
}
|
||||
.body p {
|
||||
|
|
@ -13,7 +14,8 @@ body.sans-serif-article-body {
|
|||
}
|
||||
|
||||
body.comic-sans-article-body {
|
||||
.body {
|
||||
.body,
|
||||
.crayons-article__body {
|
||||
font-family: 'Comic Sans MS', cursive, sans-serif;
|
||||
}
|
||||
.body p {
|
||||
|
|
@ -23,7 +25,8 @@ body.comic-sans-article-body {
|
|||
}
|
||||
|
||||
body.open-dyslexic-article-body {
|
||||
.body {
|
||||
.body,
|
||||
.crayons-article__body {
|
||||
font-display: swap;
|
||||
font-family: OpenDyslexic, sans-serif;
|
||||
}
|
||||
|
|
@ -33,12 +36,16 @@ body.open-dyslexic-article-body {
|
|||
font-size: 0.9em;
|
||||
}
|
||||
.body b,
|
||||
.body strong {
|
||||
.body strong,
|
||||
.crayons-article__body b,
|
||||
.crayons-article__body strong {
|
||||
font-family: OpenDyslexic-Bold, sans-serif;
|
||||
}
|
||||
|
||||
.body i,
|
||||
.body em {
|
||||
.body em,
|
||||
.crayons-article__body i,
|
||||
.crayons-article__body em {
|
||||
font-family: OpenDyslexic-Italic, sans-serif;
|
||||
}
|
||||
|
||||
|
|
@ -49,13 +56,22 @@ body.open-dyslexic-article-body {
|
|||
.body i > b,
|
||||
.body em > b,
|
||||
.body i > strong,
|
||||
.body em > strong {
|
||||
.body em > strong,
|
||||
.crayons-article__body b > i,
|
||||
.crayons-article__body b > em,
|
||||
.crayons-article__body strong > i,
|
||||
.crayons-article__body strong > em,
|
||||
.crayons-article__body i > b,
|
||||
.crayons-article__body em > b,
|
||||
.crayons-article__body i > strong,
|
||||
.crayons-article__body em > strong {
|
||||
font-family: OpenDyslexic-Bolditalic;
|
||||
}
|
||||
}
|
||||
|
||||
body.monospace-article-body {
|
||||
.body {
|
||||
.body,
|
||||
.crayons-article__body {
|
||||
font-family: var(--ff-accent);
|
||||
}
|
||||
.body p {
|
||||
|
|
|
|||
306
app/assets/stylesheets/views/article-form.scss
Normal file
306
app/assets/stylesheets/views/article-form.scss
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
@import '../config/import';
|
||||
|
||||
%layout {
|
||||
max-width: var(--layout-site-width);
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@mixin animationSlide($name) {
|
||||
animation-name: $name;
|
||||
|
||||
@keyframes #{$name} {
|
||||
from {
|
||||
opacity: 0;
|
||||
top: calc(var(--su-4) * -1);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.crayons-article-form {
|
||||
--article-form-actions-height: 56px;
|
||||
--layout-padding: var(--su-2);
|
||||
|
||||
display: grid;
|
||||
height: 100vh;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
background: var(--body-bg);
|
||||
font-size: var(--fs-base);
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
--article-form-actions-height: 72px;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-l) {
|
||||
--article-form-actions-height: 88px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
@extend %layout;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 var(--layout-padding);
|
||||
height: var(--layout-header-height);
|
||||
|
||||
@media (min-width: $breakpoint-l) {
|
||||
padding-right: calc(
|
||||
var(--layout-aside-width) + var(--layout-padding) + var(--layout-gap)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__main {
|
||||
@extend %layout;
|
||||
|
||||
@media (min-width: $breakpoint-m) {
|
||||
padding: 0 var(--layout-padding);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-l) {
|
||||
display: grid;
|
||||
grid-gap: var(--layout-gap);
|
||||
grid-template-columns:
|
||||
calc(100% - var(--layout-aside-width) - var(--layout-gap))
|
||||
var(--layout-aside-width);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-xl) {
|
||||
padding-left: calc(
|
||||
var(--layout-narrow-sidebar-width) + var(--layout-padding)
|
||||
);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: var(--layout-article-content-padding-y)
|
||||
var(--layout-article-content-padding-x);
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
overflow: auto;
|
||||
height: calc(
|
||||
100vh - var(--layout-header-height) -
|
||||
var(--article-form-actions-height)
|
||||
);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-m) {
|
||||
border: 1px solid var(--base-10);
|
||||
}
|
||||
}
|
||||
|
||||
&__aside {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $breakpoint-l) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
@extend %layout;
|
||||
background: var(--body-bg);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: 0 var(--layout-padding);
|
||||
height: var(--article-form-actions-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@media (min-width: $breakpoint-xl) {
|
||||
padding-left: calc(
|
||||
var(--layout-narrow-sidebar-width) + var(--layout-padding)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.crayons-article-form {
|
||||
&__logo {
|
||||
display: flex;
|
||||
margin-right: var(--su-1);
|
||||
@media (min-width: $breakpoint-s) {
|
||||
margin-right: var(--layout-gap);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 50px;
|
||||
height: 40px;
|
||||
padding: 0 7px 0 9px; // TODO: we have to fix SVG for logo a little...
|
||||
background: var(--logo-bg);
|
||||
fill: var(--logo-color);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
}
|
||||
|
||||
&__close {
|
||||
margin-left: var(--su-1);
|
||||
@media (min-width: $breakpoint-l) {
|
||||
position: absolute;
|
||||
right: var(--su-2);
|
||||
top: var(--su-2);
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
flex: 1 auto;
|
||||
position: relative;
|
||||
outline: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__field {
|
||||
flex: 1 auto;
|
||||
font-size: var(--fs-l);
|
||||
font-family: var(--ff-accent);
|
||||
}
|
||||
}
|
||||
|
||||
&__toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--base-0);
|
||||
padding: var(--su-2) var(--layout-article-content-padding-x);
|
||||
margin: calc(var(--layout-article-content-padding-y) * -1)
|
||||
calc(var(--layout-article-content-padding-x) * -1) var(--su-6)
|
||||
calc(var(--layout-article-content-padding-x) * -1);
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
top: calc(var(--layout-article-content-padding-y) * -1);
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
margin-left: calc(var(--su-2) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
&__cover {
|
||||
margin-bottom: var(--su-6);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__image {
|
||||
width: 250px;
|
||||
height: 105px;
|
||||
border-radius: var(--radius);
|
||||
object-fit: cover;
|
||||
margin-bottom: var(--su-2);
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
margin-bottom: 0;
|
||||
margin-right: var(--su-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-bottom: var(--su-2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__tagsfield {
|
||||
position: relative;
|
||||
margin-bottom: calc(var(--su-7) + var(--layout-article-content-padding-y));
|
||||
}
|
||||
|
||||
&__help {
|
||||
position: relative;
|
||||
animation-duration: 0.25s;
|
||||
animation-iteration-count: 1;
|
||||
animation-timing-function: var(--transition-func);
|
||||
|
||||
&--title {
|
||||
@include animationSlide(slideInTitle);
|
||||
}
|
||||
|
||||
&--tags {
|
||||
@include animationSlide(slideInTags);
|
||||
}
|
||||
|
||||
&--body {
|
||||
@include animationSlide(slideInBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.crayons-article-form {
|
||||
&__tagsoptions {
|
||||
@include generate-box(
|
||||
$level: 2,
|
||||
$bg: var(--card-bg),
|
||||
$border: var(--card-color),
|
||||
$color: var(--card-color)
|
||||
);
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: calc(100% + var(--su-4));
|
||||
z-index: 100;
|
||||
padding: var(--su-1);
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
left: calc(var(--su-2) * -1);
|
||||
right: calc(var(--su-2) * -1);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-m) {
|
||||
left: calc(var(--su-4) * -1);
|
||||
right: calc(var(--su-4) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
&__tagname {
|
||||
font-family: var(--ff-accent);
|
||||
color: var(--base-90);
|
||||
letter-spacing: -0.05em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__tagoptionrow {
|
||||
cursor: pointer;
|
||||
color: var(--link-color);
|
||||
padding: var(--su-3);
|
||||
|
||||
&:hover {
|
||||
background: var(--link-bg-hover);
|
||||
.crayons-tag {
|
||||
color: var(--link-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: var(--link-bg-hover);
|
||||
.crayons-article-form__tagname {
|
||||
color: var(--link-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__tagsoptionrulesbutton {
|
||||
display: none;
|
||||
}
|
||||
&__tagrules,
|
||||
&__tagrules--inactive {
|
||||
font-size: var(--fs-s);
|
||||
color: var(--base-70);
|
||||
}
|
||||
|
||||
&__tagsoptionsbottomrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
369
app/assets/stylesheets/views/article.scss
Normal file
369
app/assets/stylesheets/views/article.scss
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
@import '../config/import';
|
||||
|
||||
:root {
|
||||
--article-rythm: var(--su-2);
|
||||
|
||||
@media (min-width: $breakpoint-m) {
|
||||
--article-rythm: var(--su-4);
|
||||
}
|
||||
}
|
||||
|
||||
.crayons-article {
|
||||
&__cover {
|
||||
margin: calc(var(--layout-article-content-padding-y) * -1)
|
||||
calc(var(--layout-article-content-padding-x) * -1)
|
||||
var(--layout-article-content-padding-x)
|
||||
calc(var(--layout-article-content-padding-x) * -1);
|
||||
width: calc(100% + (var(--layout-article-content-padding-x) * 2));
|
||||
|
||||
&__image {
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
padding-top: 42%;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
margin-bottom: var(--su-6);
|
||||
}
|
||||
|
||||
&__tags {
|
||||
// TODO: replace with actual component one day...
|
||||
font-family: var(--ff-accent);
|
||||
font-size: var(--fs-s);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__body {
|
||||
font-size: var(--fs-l);
|
||||
font-family: var(--ff-serif);
|
||||
@media (min-width: $breakpoint-m) {
|
||||
font-size: var(--fs-xl);
|
||||
}
|
||||
|
||||
a {
|
||||
code {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
&.anchor {
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
top: -50px;
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: var(--ff-default);
|
||||
margin: var(--article-rythm) 0;
|
||||
margin-top: calc(var(--article-rythm) * 2);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.85em;
|
||||
line-height: 1.25em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.7em;
|
||||
font-weight: 400;
|
||||
line-height: 1.25em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.32em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 25%;
|
||||
opacity: 0.1;
|
||||
border: 1px solid var(--body-color);
|
||||
margin: 1.3em auto 1.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: var(--article-rythm) 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: var(--su-6);
|
||||
margin: var(--article-rythm) 0;
|
||||
br {
|
||||
line-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0 0.7em 0 1.9em;
|
||||
list-style-type: square;
|
||||
|
||||
ul {
|
||||
list-style-type: circle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
margin: var(--su-2) 0;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.4em;
|
||||
color: var(--body-color);
|
||||
opacity: 0.75;
|
||||
display: block;
|
||||
}
|
||||
|
||||
p + figcaption {
|
||||
margin-top: -0.8em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid var(--base-50);
|
||||
padding: 0 var(--su-4);
|
||||
margin: var(--article-rythm) 0;
|
||||
|
||||
p:first-child {
|
||||
margin: var(--su-2) 0;
|
||||
}
|
||||
|
||||
&.twitter-tweet {
|
||||
font-family: var(--ff-default);
|
||||
background: var(--body-color-inverted);
|
||||
max-width: 456px;
|
||||
border: 1px solid #e9eef2;
|
||||
font-size: 0.75em;
|
||||
line-height: 1.35em;
|
||||
border-radius: 5px;
|
||||
margin: 1.6em auto;
|
||||
padding: 60px 10px 18px;
|
||||
color: white;
|
||||
min-height: 60px;
|
||||
padding: 33px 15px 8px;
|
||||
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $breakpoint-s) {
|
||||
padding: 60px 22px 18px;
|
||||
min-height: 105px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
margin: auto;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
padding: 0.1em 0.3em 0;
|
||||
border-radius: 2px;
|
||||
font-size: 0.84em;
|
||||
vertical-align: 0;
|
||||
max-width: 100%;
|
||||
line-height: 1.6em;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-block;
|
||||
padding: 3px 5px;
|
||||
font-size: 11px;
|
||||
line-height: 10px;
|
||||
color: #444d56;
|
||||
vertical-align: middle;
|
||||
background-color: #fafbfc;
|
||||
border: solid 1px #c6cbd1;
|
||||
border-bottom-color: #959da5;
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 -1px 0 #959da5;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--base-100);
|
||||
overflow-x: auto;
|
||||
color: #eff0f9;
|
||||
padding: var(--su-6) var(--su-8);
|
||||
margin: var(--article-rythm) 0;
|
||||
margin-left: calc(var(--su-8) * -1);
|
||||
margin-right: calc(var(--su-8) * -1);
|
||||
width: calc(100% + var(--su-8) + var(--su-8));
|
||||
overflow-wrap: normal;
|
||||
border-radius: 0;
|
||||
|
||||
code {
|
||||
background: var(--base-100);
|
||||
color: #eff0f9;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: auto;
|
||||
left: -4%;
|
||||
max-width: 108%;
|
||||
|
||||
@media screen and (min-width: 430px) {
|
||||
left: -6px;
|
||||
max-width: calc(100% + 12px);
|
||||
}
|
||||
}
|
||||
|
||||
a.article-body-image-wrapper {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: var(--article-rythm) 0;
|
||||
}
|
||||
|
||||
.fluidvids {
|
||||
margin: var(--article-rythm) 0;
|
||||
|
||||
iframe {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper-paragraph {
|
||||
width: 100%;
|
||||
margin-left: 0%;
|
||||
overflow-x: auto;
|
||||
|
||||
@media screen and (min-width: 430px) {
|
||||
width: 108%;
|
||||
margin-left: -4%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
width: 110%;
|
||||
margin-left: -5%;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
font-size: 0.78em;
|
||||
margin: 0.8em auto 1.2em;
|
||||
table-layout: fixed;
|
||||
|
||||
@media screen and (min-width: 500px) {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
th {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 5px 1vw;
|
||||
background: var(--body-bg);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 5px 1vw;
|
||||
}
|
||||
}
|
||||
|
||||
.twitter-tweet {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.crayons-article {
|
||||
.article-collection-wrapper {
|
||||
font-family: var(--ff-default);
|
||||
font-size: 16px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--base-30);
|
||||
width: 92%;
|
||||
max-width: 500px;
|
||||
margin: calc(1.1vw + 5px) auto 1vw;
|
||||
p {
|
||||
margin: 0px auto;
|
||||
padding: calc(0.1vw + 9px);
|
||||
font-weight: bold;
|
||||
font-size: 0.96em;
|
||||
}
|
||||
|
||||
.article-collection {
|
||||
user-select: none;
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 8;
|
||||
a {
|
||||
color: var(--body-color);
|
||||
padding: calc(0.1vw + 9px);
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
border-top: 1px solid var(--base-30);
|
||||
font-size: 0.88em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
&.current-article {
|
||||
background: var(--accent-brand-lighter);
|
||||
color: var(--body-color-inverted);
|
||||
}
|
||||
|
||||
&.coming-soon {
|
||||
pointer-events: none;
|
||||
color: var(--base-80);
|
||||
}
|
||||
&.collection-link-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.article-collection-wrapper-bottom {
|
||||
margin-bottom: 3vw;
|
||||
|
||||
p {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,150 +2,307 @@
|
|||
|
||||
exports[`<ArticleForm /> renders properly 1`] = `
|
||||
<form
|
||||
class="articleform__form articleform__form--v2"
|
||||
class="crayons-article-form"
|
||||
id="article-form"
|
||||
onInput={[Function]}
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<textarea
|
||||
autoComplete="off"
|
||||
class="articleform__title"
|
||||
id="article-form-title"
|
||||
onChange={[Function]}
|
||||
onInput={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="Title"
|
||||
ref={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 0,
|
||||
}
|
||||
}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="articleform__detailfields"
|
||||
<div
|
||||
class="crayons-article-form__header crayons-layout"
|
||||
>
|
||||
<a
|
||||
aria-label="Home"
|
||||
class="crayons-article-form__logo"
|
||||
href="/"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="aokj84btta17w7mvt93i94l82beye8ez"
|
||||
class="logo"
|
||||
height="20%"
|
||||
role="img"
|
||||
version="1"
|
||||
viewBox="0 0 132.000000 65.000000"
|
||||
width="20%"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<div
|
||||
class="articleform__tagswrapper"
|
||||
<title
|
||||
id="aokj84btta17w7mvt93i94l82beye8ez"
|
||||
>
|
||||
<input
|
||||
App logo
|
||||
</title>
|
||||
<path
|
||||
d="M0 33v32h11.3c12.5 0 17.7-1.6 21.5-6.5 3.8-4.8 4.4-9 4-28-.3-16.8-.5-18.2-2.7-21.8C30.3 2.5 26.1 1 12 1H0v32zm23.1-19.1c2.3 1.9 2.4 2.3 2.4 18.5 0 15.7-.1 16.7-2.2 18.8-1.7 1.6-3.5 2.2-7 2.2l-4.8.1-.3-20.8L11 12h4.9c3.3 0 5.6.6 7.2 1.9zM46.1 3.6c-2 2.6-2.1 3.9-2.1 29.6v26.9l2.5 2.4c2.3 2.4 2.9 2.5 16 2.5H76V54.1l-10.2-.3-10.3-.3v-15l6.3-.3 6.2-.3V27H55V12h21V1H62.1c-13.9 0-14 0-16 2.6zM87 15.2c2.1 7.9 5.5 20.8 7.6 28.8 3.2 12.3 4.3 15 7 17.7 1.9 2 4.2 3.3 5.7 3.3 3.1 0 7.1-3.1 8.5-6.7 1-2.6 15.2-55.6 15.2-56.8 0-.3-2.8-.5-6.2-.3l-6.3.3-5.6 21.5c-3.5 13.6-5.8 20.8-6.2 19.5C105.9 40 96 1.9 96 1.4c0-.2-2.9-.4-6.4-.4h-6.4L87 15.2z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<div
|
||||
class="crayons-field__label flex items-center flex-1"
|
||||
>
|
||||
<span
|
||||
class="hidden s:inline-block whitespace-nowrap"
|
||||
>
|
||||
Write a new post
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-article-form__tabs crayons-tabs ml-auto"
|
||||
>
|
||||
<button
|
||||
class="crayons-tabs__item crayons-tabs__item--current"
|
||||
onClick={false}
|
||||
type="button"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
class="crayons-tabs__item false"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-article-form__close"
|
||||
>
|
||||
<a
|
||||
class="crayons-btn crayons-btn--ghost crayons-btn--icon"
|
||||
href="/"
|
||||
title="Close the editor"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="as1mn15llu5e032u2pgzlc6yhvss2myk"
|
||||
class="crayons-icon"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title
|
||||
id="as1mn15llu5e032u2pgzlc6yhvss2myk"
|
||||
>
|
||||
Close the editor
|
||||
</title>
|
||||
<path
|
||||
d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-article-form__main"
|
||||
>
|
||||
<div
|
||||
class="crayons-article-form__main__content crayons-card"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="crayons-article-form__cover"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="flex items-center"
|
||||
>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--outlined mr-2 whitespace-nowrap"
|
||||
disabled={false}
|
||||
type="button"
|
||||
>
|
||||
<label
|
||||
htmlFor="cover-image-input"
|
||||
>
|
||||
Add a cover image
|
||||
</label>
|
||||
<input
|
||||
accept="image/*"
|
||||
class="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer"
|
||||
data-max-file-size-mb="25"
|
||||
id="cover-image-input"
|
||||
onChange={[Function]}
|
||||
type="file"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-article-form__title"
|
||||
>
|
||||
<textarea
|
||||
autoComplete="off"
|
||||
class="articleform__tags"
|
||||
id="tag-input"
|
||||
name="listing[tag_list]"
|
||||
onBlur={[Function]}
|
||||
autofocus="true"
|
||||
class="crayons-textfield crayons-textfield--ghost fs-3xl s:fs-4xl l:fs-5xl fw-bold s:fw-heavy lh-tight"
|
||||
id="article-form-title"
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onInput={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="4 tags max, comma separated, no spaces or special characters"
|
||||
placeholder="New post title here..."
|
||||
ref={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 0,
|
||||
}
|
||||
}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="articleform__detailsButton articleform__detailsButton--image"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
<div
|
||||
class="crayons-article-form__tagsfield"
|
||||
>
|
||||
<img
|
||||
alt="Upload images"
|
||||
src=""
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="articleform__detailsButton articleform__detailsButton--moreconfig"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
<div
|
||||
class="crayons-article-form__tagswrapper"
|
||||
>
|
||||
<input
|
||||
autoComplete="off"
|
||||
class="crayons-textfield crayons-textfield--ghost ff-accent crayons-article-form__tags"
|
||||
id="tag-input"
|
||||
name="listing[tag_list]"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onInput={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="Add up to 4 tags..."
|
||||
ref={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-article-form__body"
|
||||
>
|
||||
<div
|
||||
class="crayons-article-form__toolbar false"
|
||||
>
|
||||
<img
|
||||
alt="Menu"
|
||||
src=""
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="flex items-center"
|
||||
>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--ghost crayons-btn--icon-left mr-2 fw-normal"
|
||||
disabled={false}
|
||||
tabIndex="-1"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="a17qec5pfhrwzk9w4kg0tp62v27qqu9t"
|
||||
class="crayons-icon"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title
|
||||
id="a17qec5pfhrwzk9w4kg0tp62v27qqu9t"
|
||||
>
|
||||
Upload image
|
||||
</title>
|
||||
<path
|
||||
d="M20 5H4v14l9.292-9.294a1 1 0 011.414 0L20 15.01V5zM2 3.993A1 1 0 012.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 01-.992.993H2.992A.993.993 0 012 20.007V3.993zM8 11a2 2 0 110-4 2 2 0 010 4z"
|
||||
/>
|
||||
</svg>
|
||||
Upload image
|
||||
<input
|
||||
accept="image/*"
|
||||
class="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer"
|
||||
data-max-file-size-mb="25"
|
||||
id="image-upload-field"
|
||||
multiple={true}
|
||||
onChange={[Function]}
|
||||
tabIndex="-1"
|
||||
type="file"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
class="crayons-textfield crayons-textfield--ghost crayons-article-form__body__field"
|
||||
id="article_body_markdown"
|
||||
name="body_markdown"
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onInput={[Function]}
|
||||
placeholder="Write your post content here..."
|
||||
ref={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 0,
|
||||
}
|
||||
}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
class="articleform__body"
|
||||
id="article_body_markdown"
|
||||
name="body_markdown"
|
||||
onInput={[Function]}
|
||||
placeholder="Body Markdown"
|
||||
value=""
|
||||
/>
|
||||
<button
|
||||
class="articleform__detailsButton articleform__detailsButton--image articleform__detailsButton--bottom"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
<div
|
||||
class="crayons-article-form__main__aside"
|
||||
>
|
||||
<img
|
||||
alt="upload images"
|
||||
src=""
|
||||
/>
|
||||
IMAGES
|
||||
</button>
|
||||
<button
|
||||
class="articleform__detailsButton articleform__detailsButton--moreconfig articleform__detailsButton--bottom"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<img
|
||||
alt="menu dots"
|
||||
src=""
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="articleform__buttons"
|
||||
>
|
||||
<button
|
||||
class="articleform__buttons--small inactive"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
?
|
||||
</button>
|
||||
<button
|
||||
class="inactive previewbutt"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
PREVIEW
|
||||
</button>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
SAVE DRAFT
|
||||
</button>
|
||||
<span>
|
||||
<p
|
||||
<div
|
||||
class="sticky"
|
||||
style={
|
||||
Object {
|
||||
"visibility": "hidden",
|
||||
"top": null,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-article-form__footer"
|
||||
>
|
||||
<button
|
||||
class="crayons-btn mr-2 whitespace-nowrap"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Publish
|
||||
</button>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--secondary mr-2 whitespace-nowrap"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Save
|
||||
<span
|
||||
class="hidden s:inline"
|
||||
>
|
||||
New Changes (
|
||||
<button
|
||||
class="clear-button"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
clear
|
||||
</button>
|
||||
)
|
||||
</p>
|
||||
draft
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
class="s:relative"
|
||||
>
|
||||
<button
|
||||
class="articleform__buttons--publish"
|
||||
class="crayons-btn crayons-btn--ghost crayons-btn--icon"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
title="Post options"
|
||||
type="button"
|
||||
>
|
||||
PUBLISH
|
||||
<svg
|
||||
aria-labelledby="75abcb76478519ca4eb9"
|
||||
class="crayons-icon"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title
|
||||
id="75abcb76478519ca4eb9"
|
||||
>
|
||||
Post options
|
||||
</title>
|
||||
<path
|
||||
d="M12 1l9.5 5.5v11L12 23l-9.5-5.5v-11L12 1zm0 2.311L4.5 7.653v8.694l7.5 4.342 7.5-4.342V7.653L12 3.311zM12 16a4 4 0 110-8 4 4 0 010 8zm0-2a2 2 0 100-4 2 2 0 000 4z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -80,16 +80,4 @@ describe('<ArticleForm />', () => {
|
|||
expect(form.state().bodyMarkdown).toBe('');
|
||||
});
|
||||
|
||||
it('toggles help on help button press', () => {
|
||||
const form = deep(getArticleForm());
|
||||
global.scrollTo = jest.fn();
|
||||
form
|
||||
.find('.articleform__buttons--small')
|
||||
.simulate('click', { preventDefault: () => {} });
|
||||
expect(form.state().helpShowing).toBe(true);
|
||||
form
|
||||
.find('.articleform__buttons--small')
|
||||
.simulate('click', { preventDefault: () => {} });
|
||||
expect(form.state().helpShowing).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export function previewArticle(payload, successCb, failureCb) {
|
|||
}),
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then((response) => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ export function getArticle() {}
|
|||
|
||||
function processPayload(payload) {
|
||||
const {
|
||||
/* eslint-disable no-unused-vars */
|
||||
previewShowing,
|
||||
helpShowing,
|
||||
previewResponse,
|
||||
|
|
@ -27,6 +28,7 @@ function processPayload(payload) {
|
|||
imageManagementShowing,
|
||||
moreConfigShowing,
|
||||
errors,
|
||||
/* eslint-enable no-unused-vars */
|
||||
...neededPayload
|
||||
} = payload;
|
||||
return neededPayload;
|
||||
|
|
@ -47,13 +49,14 @@ export function submitArticle(payload, clearStorage, errorCb, failureCb) {
|
|||
}),
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
.then((response) => response.json())
|
||||
.then((response) => {
|
||||
if (response.current_state_path) {
|
||||
clearStorage();
|
||||
window.location.replace(response.current_state_path);
|
||||
} else {
|
||||
errorCb(response);
|
||||
// If there is an error and the method is POST, we know they are trying to publish.
|
||||
errorCb(response, method === 'POST');
|
||||
}
|
||||
})
|
||||
.catch(failureCb);
|
||||
|
|
@ -83,8 +86,8 @@ export function generateMainImage(payload, successCb, failureCb) {
|
|||
body: generateUploadFormdata(payload),
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
if (json.error) {
|
||||
throw new Error(json.error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,19 @@
|
|||
import 'preact/devtools';
|
||||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import linkState from 'linkstate';
|
||||
import postscribe from 'postscribe';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import ImageUploadIcon from 'images/image-upload.svg';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import ThreeDotsIcon from 'images/three-dots.svg';
|
||||
import { submitArticle, previewArticle } from './actions';
|
||||
import BodyMarkdown from './elements/bodyMarkdown';
|
||||
import BodyPreview from './elements/bodyPreview';
|
||||
import PublishToggle from './elements/publishToggle';
|
||||
import Notice from './elements/notice';
|
||||
import Title from './elements/title';
|
||||
import MainImage from './elements/mainImage';
|
||||
import ImageManagement from './elements/imageManagement';
|
||||
import MoreConfig from './elements/moreConfig';
|
||||
import Errors from './elements/errors';
|
||||
import KeyboardShortcutsHandler from './elements/keyboardShortcutsHandler';
|
||||
import Tags from '../shared/components/tags';
|
||||
import { OrganizationPicker } from '../organization/OrganizationPicker';
|
||||
|
||||
/* global activateRunkitTags */
|
||||
|
||||
const SetupImageButton = ({
|
||||
className,
|
||||
imgSrc,
|
||||
imgAltText,
|
||||
onClickCallback,
|
||||
}) => (
|
||||
<button type="button" className={className} onClick={onClickCallback}>
|
||||
<img src={imgSrc} alt={imgAltText} />
|
||||
</button>
|
||||
);
|
||||
|
||||
SetupImageButton.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
imgSrc: PropTypes.string.isRequired,
|
||||
imgAltText: PropTypes.string.isRequired,
|
||||
onClickCallback: PropTypes.func.isRequired,
|
||||
};
|
||||
import {
|
||||
EditorActions,
|
||||
Form,
|
||||
Header,
|
||||
Help,
|
||||
Preview,
|
||||
KeyboardShortcutsHandler,
|
||||
} from './components';
|
||||
|
||||
/*
|
||||
Although the state fields: id, description, canonicalUrl, series, allSeries and
|
||||
|
|
@ -87,13 +61,9 @@ export default class ArticleForm extends Component {
|
|||
bodyMarkdown: this.article.body_markdown || '',
|
||||
published: this.article.published || false,
|
||||
previewShowing: false,
|
||||
helpShowing: false,
|
||||
previewResponse: '',
|
||||
helpHTML: document.getElementById('editor-help-guide').innerHTML,
|
||||
submitting: false,
|
||||
editing: this.article.id !== null, // eslint-disable-line react/no-unused-state
|
||||
imageManagementShowing: false,
|
||||
moreConfigShowing: false,
|
||||
mainImage: this.article.main_image || null,
|
||||
organizations,
|
||||
organizationId: this.article.organization_id,
|
||||
|
|
@ -101,6 +71,8 @@ export default class ArticleForm extends Component {
|
|||
edited: false,
|
||||
updatedAt: this.article.updated_at,
|
||||
version,
|
||||
helpFor: null,
|
||||
helpPosition: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -150,28 +122,17 @@ export default class ArticleForm extends Component {
|
|||
};
|
||||
|
||||
setCommonProps = ({
|
||||
helpShowing = false,
|
||||
previewShowing = false,
|
||||
imageManagementShowing = false,
|
||||
moreConfigShowing = false,
|
||||
helpFor = null,
|
||||
helpPosition = null,
|
||||
}) => {
|
||||
return {
|
||||
helpShowing,
|
||||
previewShowing,
|
||||
imageManagementShowing,
|
||||
moreConfigShowing,
|
||||
helpFor,
|
||||
helpPosition,
|
||||
};
|
||||
};
|
||||
|
||||
toggleHelp = (e) => {
|
||||
const { helpShowing } = this.state;
|
||||
e.preventDefault();
|
||||
window.scrollTo(0, 0);
|
||||
this.setState({
|
||||
...this.setCommonProps({ helpShowing: !helpShowing }),
|
||||
});
|
||||
};
|
||||
|
||||
fetchPreview = (e) => {
|
||||
const { previewShowing, bodyMarkdown } = this.state;
|
||||
e.preventDefault();
|
||||
|
|
@ -184,25 +145,6 @@ export default class ArticleForm extends Component {
|
|||
}
|
||||
};
|
||||
|
||||
toggleImageManagement = (e) => {
|
||||
const { imageManagementShowing } = this.state;
|
||||
e.preventDefault();
|
||||
window.scrollTo(0, 0);
|
||||
this.setState({
|
||||
...this.setCommonProps({
|
||||
imageManagementShowing: !imageManagementShowing,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
toggleMoreConfig = (e) => {
|
||||
const { moreConfigShowing } = this.state;
|
||||
e.preventDefault();
|
||||
this.setState({
|
||||
...this.setCommonProps({ moreConfigShowing: !moreConfigShowing }),
|
||||
});
|
||||
};
|
||||
|
||||
showPreview = (response) => {
|
||||
if (response.processed_html) {
|
||||
this.setState({
|
||||
|
|
@ -239,7 +181,6 @@ export default class ArticleForm extends Component {
|
|||
handleMainImageUrlChange = (payload) => {
|
||||
this.setState({
|
||||
mainImage: payload.links[0],
|
||||
imageManagementShowing: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -265,14 +206,6 @@ export default class ArticleForm extends Component {
|
|||
submitArticle(state, this.removeLocalStorage, this.handleArticleError);
|
||||
};
|
||||
|
||||
handleTitleKeyDown = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
handleBodyKeyDown = (_e) => {};
|
||||
|
||||
onClearChanges = (e) => {
|
||||
e.preventDefault();
|
||||
// eslint-disable-next-line no-alert
|
||||
|
|
@ -291,24 +224,24 @@ export default class ArticleForm extends Component {
|
|||
bodyMarkdown: this.article.body_markdown || '',
|
||||
published: this.article.published || false,
|
||||
previewShowing: false,
|
||||
helpShowing: false,
|
||||
previewResponse: '',
|
||||
helpHTML: document.getElementById('editor-help-guide').innerHTML,
|
||||
submitting: false,
|
||||
editing: this.article.id !== null, // eslint-disable-line react/no-unused-state
|
||||
imageManagementShowing: false,
|
||||
moreConfigShowing: false,
|
||||
mainImage: this.article.main_image || null,
|
||||
errors: null,
|
||||
edited: false,
|
||||
helpFor: null,
|
||||
helpPosition: 0,
|
||||
});
|
||||
};
|
||||
|
||||
handleArticleError = (response) => {
|
||||
handleArticleError = (response, publishFailed = false) => {
|
||||
window.scrollTo(0, 0);
|
||||
this.setState({
|
||||
errors: response,
|
||||
submitting: false,
|
||||
// Even if it's an update that failed, published will still be set to true
|
||||
published: !publishFailed,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -321,185 +254,94 @@ export default class ArticleForm extends Component {
|
|||
});
|
||||
};
|
||||
|
||||
switchHelpContext = ({ target }) => {
|
||||
this.setState({
|
||||
...this.setCommonProps({
|
||||
helpFor: target.id,
|
||||
helpPosition: target.getBoundingClientRect().y,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
// cover image url should asking for url OR providing option to upload an image
|
||||
const {
|
||||
title,
|
||||
tagList,
|
||||
bodyMarkdown,
|
||||
published,
|
||||
previewShowing,
|
||||
helpShowing,
|
||||
previewResponse,
|
||||
helpHTML,
|
||||
submitting,
|
||||
imageManagementShowing,
|
||||
moreConfigShowing,
|
||||
organizations,
|
||||
organizationId,
|
||||
mainImage,
|
||||
errors,
|
||||
edited,
|
||||
version,
|
||||
helpFor,
|
||||
helpPosition,
|
||||
} = this.state;
|
||||
const notice = submitting ? (
|
||||
<Notice published={published} version={version} />
|
||||
) : (
|
||||
''
|
||||
);
|
||||
const imageArea =
|
||||
mainImage && !previewShowing && version === 'v2' ? (
|
||||
<MainImage mainImage={mainImage} onEdit={this.toggleImageManagement} />
|
||||
) : (
|
||||
''
|
||||
);
|
||||
const imageManagement = imageManagementShowing ? (
|
||||
<ImageManagement
|
||||
onExit={this.toggleImageManagement}
|
||||
mainImage={mainImage}
|
||||
version={version}
|
||||
onMainImageUrlChange={this.handleMainImageUrlChange}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
const moreConfig = moreConfigShowing ? (
|
||||
<MoreConfig
|
||||
onExit={this.toggleMoreConfig}
|
||||
passedData={this.state}
|
||||
onSaveDraft={this.onSaveDraft}
|
||||
onConfigChange={this.handleConfigChange}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
const orgArea =
|
||||
organizations && organizations.length > 0 ? (
|
||||
<div className="articleform__orgsettings">
|
||||
Publish under an organization:
|
||||
<OrganizationPicker
|
||||
name="article[organization_id]"
|
||||
id="article_publish_under_org"
|
||||
organizations={organizations}
|
||||
organizationId={organizationId}
|
||||
onToggle={this.handleOrgIdChange}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
const errorsArea = errors ? <Errors errorsList={errors} /> : '';
|
||||
let editorView = '';
|
||||
if (previewShowing) {
|
||||
editorView = (
|
||||
<div>
|
||||
{errorsArea}
|
||||
{orgArea}
|
||||
{imageArea}
|
||||
<BodyPreview
|
||||
previewResponse={previewResponse}
|
||||
articleState={this.state}
|
||||
version="article-preview"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (helpShowing) {
|
||||
editorView = (
|
||||
<BodyPreview
|
||||
previewResponse={{ processed_html: helpHTML }}
|
||||
version="help"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
let controls = '';
|
||||
let moreConfigBottomButton = '';
|
||||
if (version === 'v2') {
|
||||
moreConfigBottomButton = (
|
||||
<SetupImageButton
|
||||
className="articleform__detailsButton articleform__detailsButton--moreconfig articleform__detailsButton--bottom"
|
||||
imgSrc={ThreeDotsIcon}
|
||||
imgAltText="menu dots"
|
||||
onClickCallback={this.toggleMoreConfig}
|
||||
/>
|
||||
);
|
||||
controls = (
|
||||
<div
|
||||
className={title.length > 128 ? 'articleform__titleTooLong' : ''}
|
||||
>
|
||||
<Title
|
||||
defaultValue={title}
|
||||
onKeyDown={this.handleTitleKeyDown}
|
||||
onChange={linkState(this, 'title')}
|
||||
/>
|
||||
<div className="articleform__detailfields">
|
||||
<Tags
|
||||
defaultValue={tagList}
|
||||
onInput={linkState(this, 'tagList')}
|
||||
maxTags={4}
|
||||
autoComplete="off"
|
||||
classPrefix="articleform"
|
||||
/>
|
||||
<SetupImageButton
|
||||
className="articleform__detailsButton articleform__detailsButton--image"
|
||||
imgSrc={ImageUploadIcon}
|
||||
imgAltText="Upload images"
|
||||
onClickCallback={this.toggleImageManagement}
|
||||
/>
|
||||
<SetupImageButton
|
||||
className="articleform__detailsButton articleform__detailsButton--moreconfig"
|
||||
imgSrc={ThreeDotsIcon}
|
||||
imgAltText="Menu"
|
||||
onClickCallback={this.toggleMoreConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
editorView = (
|
||||
<div>
|
||||
{errorsArea}
|
||||
{orgArea}
|
||||
{imageArea}
|
||||
{controls}
|
||||
<BodyMarkdown
|
||||
defaultValue={bodyMarkdown}
|
||||
onKeyDown={this.handleBodyKeyDown}
|
||||
onChange={linkState(this, 'bodyMarkdown')}
|
||||
/>
|
||||
<button
|
||||
className="articleform__detailsButton articleform__detailsButton--image articleform__detailsButton--bottom"
|
||||
onClick={this.toggleImageManagement}
|
||||
type="button"
|
||||
>
|
||||
<img src={ImageUploadIcon} alt="upload images" />
|
||||
IMAGES
|
||||
</button>
|
||||
{moreConfigBottomButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className={`articleform__form articleform__form--${version}`}
|
||||
id="article-form"
|
||||
className="crayons-article-form"
|
||||
onSubmit={this.onSubmit}
|
||||
onInput={this.toggleEdit}
|
||||
>
|
||||
{editorView}
|
||||
<PublishToggle
|
||||
<Header
|
||||
onPreview={this.fetchPreview}
|
||||
previewShowing={previewShowing}
|
||||
organizations={organizations}
|
||||
organizationId={organizationId}
|
||||
onToggle={this.handleOrgIdChange}
|
||||
/>
|
||||
|
||||
<div className="crayons-article-form__main">
|
||||
{previewShowing ? (
|
||||
<Preview
|
||||
previewResponse={previewResponse}
|
||||
articleState={this.state}
|
||||
errors={errors}
|
||||
/>
|
||||
) : (
|
||||
<Form
|
||||
titleDefaultValue={title}
|
||||
titleOnChange={linkState(this, 'title')}
|
||||
tagsDefaultValue={tagList}
|
||||
tagsOnInput={linkState(this, 'tagList')}
|
||||
bodyDefaultValue={bodyMarkdown}
|
||||
bodyOnChange={linkState(this, 'bodyMarkdown')}
|
||||
bodyHasFocus={false}
|
||||
version={version}
|
||||
mainImage={mainImage}
|
||||
onMainImageUrlChange={this.handleMainImageUrlChange}
|
||||
errors={errors}
|
||||
switchHelpContext={this.switchHelpContext}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Help
|
||||
previewShowing={previewShowing}
|
||||
helpFor={helpFor}
|
||||
helpPosition={helpPosition}
|
||||
version={version}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EditorActions
|
||||
published={published}
|
||||
version={version}
|
||||
previewShowing={previewShowing}
|
||||
helpShowing={helpShowing}
|
||||
onPreview={this.fetchPreview}
|
||||
onPublish={this.onPublish}
|
||||
onHelp={this.toggleHelp}
|
||||
onSaveDraft={this.onSaveDraft}
|
||||
onClearChanges={this.onClearChanges}
|
||||
edited={edited}
|
||||
onChange={linkState(this, 'published')}
|
||||
passedData={this.state}
|
||||
onConfigChange={this.handleConfigChange}
|
||||
submitting={submitting}
|
||||
/>
|
||||
|
||||
<KeyboardShortcutsHandler togglePreview={this.fetchPreview} />
|
||||
{notice}
|
||||
{imageManagement}
|
||||
{moreConfig}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
101
app/javascript/article-form/components/ArticleCoverImage.jsx
Normal file
101
app/javascript/article-form/components/ArticleCoverImage.jsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
import { generateMainImage } from '../actions';
|
||||
import { validateFileInputs } from '../../packs/validateFileInputs';
|
||||
|
||||
export class ArticleCoverImage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
uploadError: false,
|
||||
uploadErrorMessage: null,
|
||||
};
|
||||
}
|
||||
|
||||
handleMainImageUpload = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
this.clearUploadError();
|
||||
const validFileInputs = validateFileInputs();
|
||||
|
||||
if (validFileInputs) {
|
||||
const payload = { image: e.target.files, wrap_cloudinary: true };
|
||||
const { onMainImageUrlChange } = this.props;
|
||||
|
||||
generateMainImage(payload, onMainImageUrlChange, this.onUploadError);
|
||||
}
|
||||
};
|
||||
|
||||
clearUploadError = () => {
|
||||
this.setState({
|
||||
uploadError: false,
|
||||
uploadErrorMessage: null,
|
||||
});
|
||||
};
|
||||
|
||||
onUploadError = (error) => {
|
||||
this.setState({
|
||||
uploadError: true,
|
||||
uploadErrorMessage: error.message,
|
||||
});
|
||||
};
|
||||
|
||||
triggerMainImageRemoval = (e) => {
|
||||
e.preventDefault();
|
||||
const { onMainImageUrlChange } = this.props;
|
||||
onMainImageUrlChange({
|
||||
links: [],
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { mainImage } = this.props;
|
||||
const { uploadError, uploadErrorMessage } = this.state;
|
||||
const uploadLabel = mainImage ? 'Change' : 'Add a cover image';
|
||||
return (
|
||||
<div className="crayons-article-form__cover" role="presentation">
|
||||
{mainImage && (
|
||||
<img
|
||||
src={mainImage}
|
||||
className="crayons-article-form__cover__image"
|
||||
width="250"
|
||||
height="105"
|
||||
alt="Post cover"
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Button variant="outlined" className="mr-2 whitespace-nowrap">
|
||||
<label htmlFor="cover-image-input">{uploadLabel}</label>
|
||||
<input
|
||||
id="cover-image-input"
|
||||
type="file"
|
||||
onChange={this.handleMainImageUpload}
|
||||
accept="image/*"
|
||||
className="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer"
|
||||
data-max-file-size-mb="25"
|
||||
/>
|
||||
</Button>
|
||||
{mainImage && (
|
||||
<Button
|
||||
variant="ghost-danger"
|
||||
onClick={this.triggerMainImageRemoval}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{uploadError && (
|
||||
<p className="articleform__uploaderror">{uploadErrorMessage}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArticleCoverImage.propTypes = {
|
||||
mainImage: PropTypes.string.isRequired,
|
||||
onMainImageUrlChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ArticleCoverImage.displayName = 'ArticleCoverImage';
|
||||
67
app/javascript/article-form/components/ClipboardButton.jsx
Normal file
67
app/javascript/article-form/components/ClipboardButton.jsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
function linksToMarkdownForm(imageLinks) {
|
||||
return imageLinks.map((imageLink) => ``).join('\n');
|
||||
}
|
||||
|
||||
const CopyIcon = () => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="crayons-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-labelledby="fc5f15add1e114844f5e"
|
||||
>
|
||||
<title id="fc5f15add1e114844f5e">Copy Markdown for image</title>
|
||||
<path d="M7 6V3a1 1 0 011-1h12a1 1 0 011 1v14a1 1 0 01-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1 1 0 013 21l.003-14c0-.552.45-1 1.007-1H7zm2 0h8v10h2V4H9v2zm-2 5v2h6v-2H7zm0 4v2h6v-2H7z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
CopyIcon.displayName = 'CopyIcon';
|
||||
|
||||
export const ClipboardButton = ({
|
||||
onCopy,
|
||||
imageUrls,
|
||||
showCopyMessage = false,
|
||||
}) => (
|
||||
<clipboard-copy
|
||||
onClick={onCopy}
|
||||
for="image-markdown-copy-link-input"
|
||||
aria-live="polite"
|
||||
className="flex items-center flex-1"
|
||||
aria-controls="image-markdown-copy-link-announcer"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
className="crayons-textfield mr-2"
|
||||
id="image-markdown-copy-link-input"
|
||||
readOnly="true"
|
||||
value={linksToMarkdownForm(imageUrls)}
|
||||
/>
|
||||
<Button
|
||||
className="spec__image-markdown-copy"
|
||||
variant="ghost"
|
||||
contentType="icon"
|
||||
icon={CopyIcon}
|
||||
/>
|
||||
<span
|
||||
id="image-markdown-copy-link-announcer"
|
||||
role="alert"
|
||||
className={`fs-s ${showCopyMessage ? '' : 'opacity-0'}`}
|
||||
>
|
||||
Copied!
|
||||
</span>
|
||||
</clipboard-copy>
|
||||
);
|
||||
|
||||
ClipboardButton.displayName = 'ClipboardButton';
|
||||
|
||||
ClipboardButton.propTypes = {
|
||||
onCopy: PropTypes.func.isRequired,
|
||||
imageUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
showCopyMessage: PropTypes.bool.isRequired,
|
||||
};
|
||||
34
app/javascript/article-form/components/Close.jsx
Normal file
34
app/javascript/article-form/components/Close.jsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { h } from 'preact';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
export const Close = () => {
|
||||
const Icon = () => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="crayons-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-labelledby="as1mn15llu5e032u2pgzlc6yhvss2myk"
|
||||
>
|
||||
<title id="as1mn15llu5e032u2pgzlc6yhvss2myk">Close the editor</title>
|
||||
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="crayons-article-form__close">
|
||||
<Button
|
||||
variant="ghost"
|
||||
contentType="icon"
|
||||
url="/"
|
||||
tagName="a"
|
||||
icon={Icon}
|
||||
title="Close the editor"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Close.displayName = 'Close';
|
||||
134
app/javascript/article-form/components/EditorActions.jsx
Normal file
134
app/javascript/article-form/components/EditorActions.jsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
import { Options } from './Options';
|
||||
|
||||
const Icon = () => (
|
||||
<svg
|
||||
width="24"
|
||||
className="crayons-icon"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-labelledby="75abcb76478519ca4eb9"
|
||||
>
|
||||
<title id="75abcb76478519ca4eb9">Post options</title>
|
||||
<path d="M12 1l9.5 5.5v11L12 23l-9.5-5.5v-11L12 1zm0 2.311L4.5 7.653v8.694l7.5 4.342 7.5-4.342V7.653L12 3.311zM12 16a4 4 0 110-8 4 4 0 010 8zm0-2a2 2 0 100-4 2 2 0 000 4z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export class EditorActions extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
moreConfigShowing: false,
|
||||
};
|
||||
}
|
||||
|
||||
setCommonProps = ({ moreConfigShowing = false }) => {
|
||||
return {
|
||||
moreConfigShowing,
|
||||
};
|
||||
};
|
||||
|
||||
toggleMoreConfig = (e) => {
|
||||
const { moreConfigShowing } = this.state;
|
||||
e.preventDefault();
|
||||
this.setState({
|
||||
...this.setCommonProps({ moreConfigShowing: !moreConfigShowing }),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
onSaveDraft,
|
||||
onPublish,
|
||||
onClearChanges,
|
||||
published,
|
||||
edited,
|
||||
version,
|
||||
passedData,
|
||||
onConfigChange,
|
||||
submitting,
|
||||
} = this.props;
|
||||
|
||||
const { moreConfigShowing } = this.state;
|
||||
|
||||
return submitting ? (
|
||||
<div className="crayons-article-form__footer">
|
||||
<Button className="mr-2 whitespace-nowrap" onClick={onPublish} disabled>
|
||||
{published && version === 'v2'
|
||||
? 'Publishing...'
|
||||
: `Saving ${version === 'v2' ? 'draft' : ''}...`}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="crayons-article-form__footer">
|
||||
<Button className="mr-2 whitespace-nowrap" onClick={onPublish}>
|
||||
{published || version === 'v1' ? 'Save changes' : 'Publish'}
|
||||
</Button>
|
||||
|
||||
{published || version === 'v1' ? (
|
||||
''
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mr-2 whitespace-nowrap"
|
||||
onClick={onSaveDraft}
|
||||
>
|
||||
Save
|
||||
{' '}
|
||||
<span className="hidden s:inline">draft</span>
|
||||
</Button>
|
||||
)}
|
||||
{version === 'v2' && (
|
||||
<div className="s:relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
contentType="icon"
|
||||
icon={Icon}
|
||||
title="Post options"
|
||||
onClick={this.toggleMoreConfig}
|
||||
/>
|
||||
{moreConfigShowing && (
|
||||
<Options
|
||||
passedData={passedData}
|
||||
onConfigChange={onConfigChange}
|
||||
onSaveDraft={onSaveDraft}
|
||||
moreConfigShowing={moreConfigShowing}
|
||||
toggleMoreConfig={this.toggleMoreConfig}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{edited && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onClearChanges}
|
||||
className="whitespace-nowrap fw-normal"
|
||||
size="s"
|
||||
>
|
||||
Revert
|
||||
{' '}
|
||||
<span className="hidden s:inline">new changes</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditorActions.propTypes = {
|
||||
onSaveDraft: PropTypes.func.isRequired,
|
||||
onPublish: PropTypes.func.isRequired,
|
||||
published: PropTypes.bool.isRequired,
|
||||
edited: PropTypes.bool.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
onClearChanges: PropTypes.func.isRequired,
|
||||
passedData: PropTypes.string.isRequired,
|
||||
onConfigChange: PropTypes.func.isRequired,
|
||||
submitting: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
EditorActions.displayName = 'EditorActions';
|
||||
38
app/javascript/article-form/components/EditorBody.jsx
Normal file
38
app/javascript/article-form/components/EditorBody.jsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import Textarea from 'preact-textarea-autosize';
|
||||
import { Toolbar } from './Toolbar';
|
||||
|
||||
export const EditorBody = ({
|
||||
onChange,
|
||||
defaultValue,
|
||||
switchHelpContext,
|
||||
version,
|
||||
}) => {
|
||||
return (
|
||||
<div className="crayons-article-form__body">
|
||||
<Toolbar version={version} />
|
||||
|
||||
<Textarea
|
||||
className="crayons-textfield crayons-textfield--ghost crayons-article-form__body__field"
|
||||
id="article_body_markdown"
|
||||
placeholder="Write your post content here..."
|
||||
value={defaultValue}
|
||||
onInput={onChange}
|
||||
onFocus={(_event) => {
|
||||
switchHelpContext(_event);
|
||||
}}
|
||||
name="body_markdown"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
EditorBody.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
defaultValue: PropTypes.string.isRequired,
|
||||
switchHelpContext: PropTypes.func.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
EditorBody.displayName = 'EditorBody';
|
||||
27
app/javascript/article-form/components/ErrorList.jsx
Normal file
27
app/javascript/article-form/components/ErrorList.jsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const ErrorList = ({ errors }) => {
|
||||
return (
|
||||
<div className="crayons-notice crayons-notice--danger mb-6">
|
||||
<h3 className="fs-l mb-2 fw-bold">Whoops, something went wrong:</h3>
|
||||
<ul className="list-disc pl-6">
|
||||
{Object.keys(errors).map((key) => {
|
||||
return (
|
||||
<li>
|
||||
{key}
|
||||
{`: `}
|
||||
{errors[key]}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ErrorList.propTypes = {
|
||||
errors: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
ErrorList.displayName = 'ErrorList';
|
||||
63
app/javascript/article-form/components/Form.jsx
Normal file
63
app/javascript/article-form/components/Form.jsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EditorBody } from './EditorBody';
|
||||
import { Meta } from './Meta';
|
||||
import { ErrorList } from './ErrorList';
|
||||
|
||||
export const Form = ({
|
||||
titleDefaultValue,
|
||||
titleOnChange,
|
||||
tagsDefaultValue,
|
||||
tagsOnInput,
|
||||
bodyDefaultValue,
|
||||
bodyOnChange,
|
||||
bodyHasFocus,
|
||||
version,
|
||||
mainImage,
|
||||
onMainImageUrlChange,
|
||||
switchHelpContext,
|
||||
errors,
|
||||
}) => {
|
||||
return (
|
||||
<div className="crayons-article-form__main__content crayons-card">
|
||||
{errors && <ErrorList errors={errors} />}
|
||||
|
||||
{version === 'v2' && (
|
||||
<Meta
|
||||
titleDefaultValue={titleDefaultValue}
|
||||
titleOnChange={titleOnChange}
|
||||
tagsDefaultValue={tagsDefaultValue}
|
||||
tagsOnInput={tagsOnInput}
|
||||
mainImage={mainImage}
|
||||
onMainImageUrlChange={onMainImageUrlChange}
|
||||
switchHelpContext={switchHelpContext}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EditorBody
|
||||
defaultValue={bodyDefaultValue}
|
||||
onChange={bodyOnChange}
|
||||
hasFocus={bodyHasFocus}
|
||||
switchHelpContext={switchHelpContext}
|
||||
version={version}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Form.propTypes = {
|
||||
titleDefaultValue: PropTypes.string.isRequired,
|
||||
titleOnChange: PropTypes.func.isRequired,
|
||||
tagsDefaultValue: PropTypes.string.isRequired,
|
||||
tagsOnInput: PropTypes.func.isRequired,
|
||||
bodyDefaultValue: PropTypes.string.isRequired,
|
||||
bodyOnChange: PropTypes.func.isRequired,
|
||||
bodyHasFocus: PropTypes.bool.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
mainImage: PropTypes.string.isRequired,
|
||||
onMainImageUrlChange: PropTypes.func.isRequired,
|
||||
switchHelpContext: PropTypes.func.isRequired,
|
||||
errors: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Form.displayName = 'Form';
|
||||
47
app/javascript/article-form/components/Header.jsx
Normal file
47
app/javascript/article-form/components/Header.jsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Close } from './Close';
|
||||
import { Tabs } from './Tabs';
|
||||
import { PageTitle } from './PageTitle';
|
||||
|
||||
export const Header = ({onPreview, previewShowing, organizations, organizationId, onToggle}) => {
|
||||
return (
|
||||
<div className="crayons-article-form__header crayons-layout">
|
||||
<a href="/" className="crayons-article-form__logo" aria-label="Home">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1"
|
||||
width="20%"
|
||||
height="20%"
|
||||
viewBox="0 0 132.000000 65.000000"
|
||||
className="logo"
|
||||
role="img"
|
||||
aria-labelledby="aokj84btta17w7mvt93i94l82beye8ez"
|
||||
>
|
||||
<title id="aokj84btta17w7mvt93i94l82beye8ez">App logo</title>
|
||||
<path d="M0 33v32h11.3c12.5 0 17.7-1.6 21.5-6.5 3.8-4.8 4.4-9 4-28-.3-16.8-.5-18.2-2.7-21.8C30.3 2.5 26.1 1 12 1H0v32zm23.1-19.1c2.3 1.9 2.4 2.3 2.4 18.5 0 15.7-.1 16.7-2.2 18.8-1.7 1.6-3.5 2.2-7 2.2l-4.8.1-.3-20.8L11 12h4.9c3.3 0 5.6.6 7.2 1.9zM46.1 3.6c-2 2.6-2.1 3.9-2.1 29.6v26.9l2.5 2.4c2.3 2.4 2.9 2.5 16 2.5H76V54.1l-10.2-.3-10.3-.3v-15l6.3-.3 6.2-.3V27H55V12h21V1H62.1c-13.9 0-14 0-16 2.6zM87 15.2c2.1 7.9 5.5 20.8 7.6 28.8 3.2 12.3 4.3 15 7 17.7 1.9 2 4.2 3.3 5.7 3.3 3.1 0 7.1-3.1 8.5-6.7 1-2.6 15.2-55.6 15.2-56.8 0-.3-2.8-.5-6.2-.3l-6.3.3-5.6 21.5c-3.5 13.6-5.8 20.8-6.2 19.5C105.9 40 96 1.9 96 1.4c0-.2-2.9-.4-6.4-.4h-6.4L87 15.2z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<PageTitle
|
||||
organizations={organizations}
|
||||
organizationId={organizationId}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
<Tabs
|
||||
onPreview={onPreview}
|
||||
previewShowing={previewShowing}
|
||||
/>
|
||||
<Close />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
onPreview: PropTypes.func.isRequired,
|
||||
previewShowing: PropTypes.bool.isRequired,
|
||||
organizations: PropTypes.string.isRequired,
|
||||
organizationId: PropTypes.string.isRequired,
|
||||
onToggle: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
Header.displayName = 'Header';
|
||||
220
app/javascript/article-form/components/Help.jsx
Normal file
220
app/javascript/article-form/components/Help.jsx
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal } from '@crayons';
|
||||
|
||||
export class Help extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
liquidHelpHTML:
|
||||
document.getElementById('editor-liquid-help') &&
|
||||
document.getElementById('editor-liquid-help').innerHTML,
|
||||
markdownHelpHTML:
|
||||
document.getElementById('editor-markdown-help') &&
|
||||
document.getElementById('editor-markdown-help').innerHTML,
|
||||
frontmatterHelpHTML:
|
||||
document.getElementById('editor-frontmatter-help') &&
|
||||
document.getElementById('editor-frontmatter-help').innerHTML,
|
||||
};
|
||||
}
|
||||
|
||||
setCommonProps = ({
|
||||
liquidShowing = false,
|
||||
markdownShowing = false,
|
||||
frontmatterShowing = false,
|
||||
}) => {
|
||||
return {
|
||||
liquidShowing,
|
||||
markdownShowing,
|
||||
frontmatterShowing,
|
||||
};
|
||||
};
|
||||
|
||||
toggleModal = (varShowing) => (e) => {
|
||||
e.preventDefault();
|
||||
this.setState((prevState) => ({
|
||||
...this.setCommonProps({
|
||||
[varShowing]: !prevState[varShowing],
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
renderArticleFormTitleHelp = () => {
|
||||
return (
|
||||
<div className="spec__title-help crayons-article-form__help crayons-article-form__help--title">
|
||||
<h4 className="mb-2 fs-l">Writing a Great Post Title</h4>
|
||||
<ul className="list-disc pl-6 color-base-70">
|
||||
<li>
|
||||
Think of your post title as a super short (but compelling!)
|
||||
description — like an overview of the actual post in one short
|
||||
sentence.
|
||||
</li>
|
||||
<li>
|
||||
Use keywords where appropriate to help ensure people can find your
|
||||
post by search.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderTagInputHelp = () => {
|
||||
return (
|
||||
<div className="spec__basic-tag-input-help crayons-article-form__help crayons-article-form__help--tags">
|
||||
<h4 className="mb-2 fs-l">Tagging Guidelines</h4>
|
||||
<ul className="list-disc pl-6 color-base-70">
|
||||
<li>Tags will help the right people find your post.</li>
|
||||
<li>
|
||||
Think of tags as the topics or categories that best describe your
|
||||
post.
|
||||
</li>
|
||||
<li>
|
||||
Add up to four tags per post. Combine tags to reach the appropriate
|
||||
subcommunities.
|
||||
</li>
|
||||
<li>Try to use existing tags whenever possible.</li>
|
||||
<li>
|
||||
Some tags, such as “help” or “healthydebate”, have special posting
|
||||
guidelines.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderBasicEditorHelp = () => {
|
||||
return (
|
||||
<div className="spec__basic-editor-help crayons-card crayons-card--secondary p-4 mb-6">
|
||||
You are currently using the basic markdown editor that uses
|
||||
{' '}
|
||||
<a href="#frontmatter" onClick={this.toggleModal('frontmatterShowing')}>
|
||||
Jekyll front matter
|
||||
</a>
|
||||
. You can also use the
|
||||
{' '}
|
||||
<em>rich+markdown</em>
|
||||
{' '}
|
||||
editor you can find in
|
||||
{' '}
|
||||
<a href="/settings/ux">
|
||||
UX settings
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="crayons-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-labelledby="c038a36b2512ed25db907e179ab45cfc"
|
||||
>
|
||||
<title id="c038a36b2512ed25db907e179ab45cfc">
|
||||
Open UX settings
|
||||
</title>
|
||||
<path d="M10.667 8v1.333H7.333v7.334h7.334v-3.334H16v4a.666.666 0 01-.667.667H6.667A.666.666 0 016 17.333V8.667A.667.667 0 016.667 8h4zM18 6v5.333h-1.333V8.275l-5.196 5.196-.942-.942 5.194-5.196h-3.056V6H18z" />
|
||||
</svg>
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderFormatHelp = () => {
|
||||
return (
|
||||
<div className="spec__format-help crayons-article-form__help crayons-article-form__help--body">
|
||||
<h4 className="mb-2 fs-l">Editor Basics</h4>
|
||||
<ul className="list-disc pl-6 color-base-70">
|
||||
<li>
|
||||
Use
|
||||
{' '}
|
||||
<a href="#markdown" onClick={this.toggleModal('markdownShowing')}>
|
||||
Markdown
|
||||
</a>
|
||||
{' '}
|
||||
to write and format posts.
|
||||
</li>
|
||||
<li>
|
||||
You can use
|
||||
{' '}
|
||||
<a href="#liquid" onClick={this.toggleModal('liquidShowing')}>
|
||||
Liquid tags
|
||||
</a>
|
||||
{' '}
|
||||
to add rich content such as Tweets, YouTube videos, etc.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderModal = (onClose, title, helpHtml) => {
|
||||
return (
|
||||
<Modal onClose={onClose} title={title}>
|
||||
<div
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: helpHtml }}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { previewShowing, helpFor, helpPosition, version } = this.props;
|
||||
|
||||
const {
|
||||
liquidHelpHTML,
|
||||
markdownHelpHTML,
|
||||
frontmatterHelpHTML,
|
||||
liquidShowing,
|
||||
markdownShowing,
|
||||
frontmatterShowing,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div className="crayons-article-form__main__aside">
|
||||
{!previewShowing && (
|
||||
<div
|
||||
className="sticky"
|
||||
style={{ top: version === 'v1' ? '56px' : helpPosition }}
|
||||
>
|
||||
{helpFor === 'article-form-title' &&
|
||||
this.renderArticleFormTitleHelp()}
|
||||
{helpFor === 'tag-input' && this.renderTagInputHelp()}
|
||||
{version === 'v1' && this.renderBasicEditorHelp()}
|
||||
{(helpFor === 'article_body_markdown' || version === 'v1') &&
|
||||
this.renderFormatHelp()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{liquidShowing &&
|
||||
this.renderModal(
|
||||
this.toggleModal('liquidShowing'),
|
||||
'🌊 Liquid Tags',
|
||||
liquidHelpHTML,
|
||||
)}
|
||||
|
||||
{markdownShowing &&
|
||||
this.renderModal(
|
||||
this.toggleModal('markdownShowing'),
|
||||
'✍️ Markdown',
|
||||
markdownHelpHTML,
|
||||
)}
|
||||
|
||||
{frontmatterShowing &&
|
||||
this.renderModal(
|
||||
this.toggleModal('frontmatterShowing'),
|
||||
'Jekyll Front Matter',
|
||||
frontmatterHelpHTML,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Help.propTypes = {
|
||||
previewShowing: PropTypes.bool.isRequired,
|
||||
helpFor: PropTypes.string.isRequired,
|
||||
helpPosition: PropTypes.string.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
Help.displayName = 'Help';
|
||||
163
app/javascript/article-form/components/ImageUploader.jsx
Normal file
163
app/javascript/article-form/components/ImageUploader.jsx
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { h, Component } from 'preact';
|
||||
import { Button } from '@crayons';
|
||||
import { generateMainImage } from '../actions';
|
||||
import { validateFileInputs } from '../../packs/validateFileInputs';
|
||||
import { ClipboardButton } from './ClipboardButton';
|
||||
|
||||
function isNativeAndroid() {
|
||||
return (
|
||||
navigator.userAgent === 'DEV-Native-android' &&
|
||||
typeof AndroidBridge !== 'undefined' &&
|
||||
AndroidBridge !== null
|
||||
);
|
||||
}
|
||||
|
||||
function isClipboardSupported() {
|
||||
return (
|
||||
typeof navigator.clipboard !== 'undefined' && navigator.clipboard !== null
|
||||
);
|
||||
}
|
||||
|
||||
const ImageIcon = () => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="crayons-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-labelledby="a17qec5pfhrwzk9w4kg0tp62v27qqu9t"
|
||||
>
|
||||
<title id="a17qec5pfhrwzk9w4kg0tp62v27qqu9t">Upload image</title>
|
||||
<path d="M20 5H4v14l9.292-9.294a1 1 0 011.414 0L20 15.01V5zM2 3.993A1 1 0 012.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 01-.992.993H2.992A.993.993 0 012 20.007V3.993zM8 11a2 2 0 110-4 2 2 0 010 4z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
ImageIcon.displayName = 'ImageIcon';
|
||||
|
||||
export class ImageUploader extends Component {
|
||||
state = {
|
||||
insertionImageUrls: [],
|
||||
uploadError: false,
|
||||
uploadErrorMessage: null,
|
||||
showImageCopiedMessage: false,
|
||||
};
|
||||
|
||||
onUploadError = (error) => {
|
||||
this.setState({
|
||||
insertionImageUrls: [],
|
||||
uploadError: true,
|
||||
uploadErrorMessage: error.message,
|
||||
});
|
||||
};
|
||||
|
||||
copyText = () => {
|
||||
this.imageMarkdownInput = document.getElementById(
|
||||
'image-markdown-copy-link-input',
|
||||
);
|
||||
|
||||
if (isNativeAndroid()) {
|
||||
AndroidBridge.copyToClipboard(this.imageMarkdownInput.value);
|
||||
this.setState({ showImageCopiedMessage: true });
|
||||
} else if (isClipboardSupported()) {
|
||||
navigator.clipboard
|
||||
.writeText(this.imageMarkdownInput.value)
|
||||
.then(() => {
|
||||
this.setState({ showImageCopiedMessage: true });
|
||||
})
|
||||
.catch((_err) => {
|
||||
this.execCopyText();
|
||||
});
|
||||
} else {
|
||||
this.execCopyText();
|
||||
}
|
||||
};
|
||||
|
||||
handleInsertionImageUpload = (e) => {
|
||||
const { files } = e.target;
|
||||
|
||||
this.clearUploadError();
|
||||
const validFileInputs = validateFileInputs();
|
||||
|
||||
if (validFileInputs && files.length > 0) {
|
||||
const payload = { image: files };
|
||||
|
||||
this.setState({ showImageCopiedMessage: false });
|
||||
|
||||
generateMainImage(
|
||||
payload,
|
||||
this.handleInsertImageUploadSuccess,
|
||||
this.onUploadError,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleInsertImageUploadSuccess = (response) => {
|
||||
this.setState({
|
||||
insertionImageUrls: response.links,
|
||||
});
|
||||
};
|
||||
|
||||
execCopyText() {
|
||||
this.imageMarkdownInput.setSelectionRange(
|
||||
0,
|
||||
this.imageMarkdownInput.value.length,
|
||||
);
|
||||
document.execCommand('copy');
|
||||
this.setState({ showImageCopiedMessage: true });
|
||||
}
|
||||
|
||||
clearUploadError() {
|
||||
this.setState({
|
||||
uploadError: false,
|
||||
uploadErrorMessage: null,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
insertionImageUrls,
|
||||
uploadError,
|
||||
uploadErrorMessage,
|
||||
showImageCopiedMessage,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-2 fw-normal"
|
||||
variant="ghost"
|
||||
contentType="icon-left"
|
||||
icon={ImageIcon}
|
||||
tabIndex="-1"
|
||||
>
|
||||
Upload image
|
||||
<input
|
||||
type="file"
|
||||
id="image-upload-field"
|
||||
onChange={this.handleInsertionImageUpload}
|
||||
className="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer"
|
||||
multiple
|
||||
accept="image/*"
|
||||
data-max-file-size-mb="25"
|
||||
tabIndex="-1"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
{insertionImageUrls.length > 0 && (
|
||||
<ClipboardButton
|
||||
onCopy={this.copyText}
|
||||
imageUrls={insertionImageUrls}
|
||||
showCopyMessage={showImageCopiedMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{uploadError && (
|
||||
<span className="color-accent-danger">{uploadErrorMessage}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImageUploader.displayName = 'ImageUploader';
|
||||
46
app/javascript/article-form/components/Meta.jsx
Normal file
46
app/javascript/article-form/components/Meta.jsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ArticleCoverImage } from './ArticleCoverImage';
|
||||
import { TagsField } from './TagsField';
|
||||
import { Title } from './Title';
|
||||
|
||||
export const Meta = ({
|
||||
titleDefaultValue,
|
||||
titleOnChange,
|
||||
tagsDefaultValue,
|
||||
tagsOnInput,
|
||||
mainImage,
|
||||
onMainImageUrlChange,
|
||||
switchHelpContext,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<ArticleCoverImage
|
||||
mainImage={mainImage}
|
||||
onMainImageUrlChange={onMainImageUrlChange}
|
||||
/>
|
||||
<Title
|
||||
defaultValue={titleDefaultValue}
|
||||
onChange={titleOnChange}
|
||||
switchHelpContext={switchHelpContext}
|
||||
/>
|
||||
<TagsField
|
||||
defaultValue={tagsDefaultValue}
|
||||
onInput={tagsOnInput}
|
||||
switchHelpContext={switchHelpContext}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Meta.propTypes = {
|
||||
titleDefaultValue: PropTypes.string.isRequired,
|
||||
titleOnChange: PropTypes.func.isRequired,
|
||||
tagsDefaultValue: PropTypes.string.isRequired,
|
||||
tagsOnInput: PropTypes.func.isRequired,
|
||||
mainImage: PropTypes.string.isRequired,
|
||||
onMainImageUrlChange: PropTypes.func.isRequired,
|
||||
switchHelpContext: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Meta.displayName = 'Meta';
|
||||
123
app/javascript/article-form/components/Options.jsx
Normal file
123
app/javascript/article-form/components/Options.jsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dropdown, Button } from '@crayons';
|
||||
|
||||
export const Options = ({
|
||||
passedData: {
|
||||
published = false,
|
||||
allSeries = [],
|
||||
canonicalUrl = '',
|
||||
series = '',
|
||||
},
|
||||
onSaveDraft,
|
||||
onConfigChange,
|
||||
toggleMoreConfig,
|
||||
moreConfigShowing,
|
||||
}) => {
|
||||
let publishedField = '';
|
||||
let existingSeries = '';
|
||||
|
||||
if (allSeries.length > 0) {
|
||||
const seriesNames = allSeries.map((name) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
name="series"
|
||||
onClick={onConfigChange}
|
||||
value={name}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
existingSeries = (
|
||||
<p className="crayons-field__description">
|
||||
Existing series:
|
||||
{` `}
|
||||
{seriesNames}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (published) {
|
||||
publishedField = (
|
||||
<div className="spec__options-danger-zone crayons-field mb-6">
|
||||
<div className="crayons-field__label color-accent-danger">
|
||||
Danger Zone
|
||||
</div>
|
||||
<Button variant="danger" onClick={onSaveDraft}>
|
||||
Unpublish post
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Dropdown
|
||||
className={
|
||||
moreConfigShowing &&
|
||||
'inline-block bottom-100 left-2 s:left-0 right-2 s:left-auto'
|
||||
}
|
||||
>
|
||||
<h3 className="mb-6">Post options</h3>
|
||||
<div className="crayons-field mb-6">
|
||||
<label htmlFor="canonicalUrl" className="crayons-field__label">
|
||||
Canonical URL
|
||||
</label>
|
||||
<p className="crayons-field__description">
|
||||
Change meta tag
|
||||
{` `}
|
||||
<code>canonical_url</code>
|
||||
{` `}
|
||||
if this post was first published elsewhere (like your own blog).
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={canonicalUrl}
|
||||
className="crayons-textfield"
|
||||
placeholder="https://yoursite.com/post-title"
|
||||
name="canonicalUrl"
|
||||
onKeyUp={onConfigChange}
|
||||
id="canonicalUrl"
|
||||
/>
|
||||
</div>
|
||||
<div className="crayons-field mb-6">
|
||||
<label htmlFor="series" className="crayons-field__label">
|
||||
Series
|
||||
</label>
|
||||
<p className="crayons-field__description">
|
||||
Will this post be part of a series? Give the series a unique name.
|
||||
(Series visible once it has multiple posts)
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={series}
|
||||
className="crayons-textfield"
|
||||
name="series"
|
||||
onKeyUp={onConfigChange}
|
||||
id="series"
|
||||
placeholder="..."
|
||||
/>
|
||||
{existingSeries}
|
||||
</div>
|
||||
{publishedField}
|
||||
<Button className="w-100" data-content="exit" onClick={toggleMoreConfig}>
|
||||
Done
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
Options.propTypes = {
|
||||
passedData: PropTypes.shape({
|
||||
published: PropTypes.bool.isRequired,
|
||||
allSeries: PropTypes.array.isRequired,
|
||||
canonicalUrl: PropTypes.string.isRequired,
|
||||
series: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
onSaveDraft: PropTypes.func.isRequired,
|
||||
onConfigChange: PropTypes.func.isRequired,
|
||||
toggleMoreConfig: PropTypes.func.isRequired,
|
||||
moreConfigShowing: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
Options.displayName = 'Options';
|
||||
34
app/javascript/article-form/components/PageTitle.jsx
Normal file
34
app/javascript/article-form/components/PageTitle.jsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { OrganizationPicker } from '../../organization/OrganizationPicker';
|
||||
|
||||
export const PageTitle = ({organizations, organizationId, onToggle}) => {
|
||||
return (
|
||||
<div className="crayons-field__label flex items-center flex-1">
|
||||
<span className="hidden s:inline-block whitespace-nowrap">
|
||||
Write a new post
|
||||
</span>
|
||||
{organizations && organizations.length > 0 && (
|
||||
<div className="mx-2">
|
||||
<OrganizationPicker
|
||||
name="article[organization_id]"
|
||||
id="article_publish_under_org"
|
||||
className="crayons-select mt-0"
|
||||
organizations={organizations}
|
||||
organizationId={organizationId}
|
||||
onToggle={onToggle}
|
||||
emptyLabel="Personal"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PageTitle.propTypes = {
|
||||
organizations: PropTypes.string.isRequired,
|
||||
organizationId: PropTypes.string.isRequired,
|
||||
onToggle: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
PageTitle.displayName = 'Organization';
|
||||
|
|
@ -1,30 +1,19 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ErrorList } from './ErrorList';
|
||||
|
||||
const CoverImage = ({ className, imageSrc, imageAlt }) => (
|
||||
<div className={className}>
|
||||
<img src={imageSrc} alt={imageAlt} />
|
||||
</div>
|
||||
);
|
||||
|
||||
CoverImage.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
imageSrc: PropTypes.string.isRequired,
|
||||
imageAlt: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function titleArea(previewResponse, version, articleState) {
|
||||
if (version === 'help') {
|
||||
// possibly something different here in future.
|
||||
return '';
|
||||
}
|
||||
|
||||
function titleArea(previewResponse, articleState, errors) {
|
||||
const tagArray = previewResponse.tags || articleState.tagList.split(', ');
|
||||
let tags = '';
|
||||
if (tagArray.length > 0 && tagArray[0].length > 0) {
|
||||
tags = tagArray.map(tag => {
|
||||
tags = tagArray.map((tag) => {
|
||||
return (
|
||||
<span>{tag.length > 0 ? <div className="tag">{tag}</div> : ''}</span>
|
||||
tag.length > 0 && (
|
||||
<span className="crayons-tag mr-2">
|
||||
<span className="crayons-tag__prefix">#</span>
|
||||
{tag}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -43,64 +32,26 @@ function titleArea(previewResponse, version, articleState) {
|
|||
|
||||
const previewTitle = previewResponse.title || articleState.title || '';
|
||||
|
||||
let coverImageHTML = '';
|
||||
if (coverImage.length > 0) {
|
||||
coverImageHTML = (
|
||||
<CoverImage
|
||||
className="articleform__mainimage articleform__mainimagepreview"
|
||||
imageSrc={coverImage}
|
||||
imageAlt="cover"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{coverImageHTML}
|
||||
<div className="title" style={{ width: '90%', maxWidth: '1000px' }}>
|
||||
<h1
|
||||
className={
|
||||
previewTitle.length > 44 ? 'articleform_titlepreviewsmall' : ''
|
||||
}
|
||||
>
|
||||
{previewTitle}
|
||||
</h1>
|
||||
<h3>
|
||||
<img
|
||||
className="profile-pic"
|
||||
src={window.currentUser.profile_image_90}
|
||||
alt="profile"
|
||||
<header className="crayons-article__header">
|
||||
{coverImage.length > 0 && (
|
||||
<div className="crayons-article__cover">
|
||||
<div
|
||||
className="crayons-article__cover__image"
|
||||
style={{ backgroundImage: `url(${coverImage})` }}
|
||||
/>
|
||||
|
||||
<span>{window.currentUser.name}</span>
|
||||
</h3>
|
||||
<div className="tags">{tags}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{errors && <ErrorList errors={errors} />}
|
||||
<h1 className="fs-3xl s:fs-4xl l:fs-5xl fw-bold s:fw-heavy lh-tight mb-6 spec-article__title">
|
||||
{previewTitle}
|
||||
</h1>
|
||||
|
||||
<div className="crayons-article__tags">{tags}</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
const BodyPreview = ({ previewResponse, version, articleState }) => (
|
||||
<div
|
||||
className="container"
|
||||
style={{
|
||||
marginTop: '10px',
|
||||
minHeight: '508px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0px 0px 0px #fff',
|
||||
border: '0px',
|
||||
}}
|
||||
>
|
||||
{titleArea(previewResponse, version, articleState)}
|
||||
<div
|
||||
className="body"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: previewResponse.processed_html }}
|
||||
style={{ width: '90%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const previewResponsePropTypes = PropTypes.shape({
|
||||
processed_html: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
|
|
@ -108,9 +59,24 @@ const previewResponsePropTypes = PropTypes.shape({
|
|||
cover_image: PropTypes.string,
|
||||
});
|
||||
|
||||
BodyPreview.propTypes = {
|
||||
export const Preview = ({ previewResponse, articleState, errors }) => {
|
||||
return (
|
||||
<div className="crayons-article-form__main__content crayons-card">
|
||||
<article className="crayons-article">
|
||||
{titleArea(previewResponse, articleState, errors)}
|
||||
<div
|
||||
className="crayons-article__body"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: previewResponse.processed_html }}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Preview.propTypes = {
|
||||
previewResponse: previewResponsePropTypes.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
errors: PropTypes.string.isRequired,
|
||||
articleState: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
|
|
@ -143,4 +109,4 @@ BodyPreview.propTypes = {
|
|||
}).isRequired,
|
||||
};
|
||||
|
||||
export default BodyPreview;
|
||||
Preview.displayName = 'Preview';
|
||||
35
app/javascript/article-form/components/Tabs.jsx
Normal file
35
app/javascript/article-form/components/Tabs.jsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const Tabs = ({ onPreview, previewShowing }) => {
|
||||
return (
|
||||
<div className="crayons-article-form__tabs crayons-tabs ml-auto">
|
||||
<button
|
||||
className={`crayons-tabs__item ${
|
||||
!previewShowing && 'crayons-tabs__item--current'
|
||||
}`}
|
||||
onClick={previewShowing && onPreview}
|
||||
type="button"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className={`crayons-tabs__item ${
|
||||
previewShowing && 'crayons-tabs__item--current'
|
||||
}`}
|
||||
onClick={!previewShowing && onPreview}
|
||||
type="button"
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Tabs.propTypes = {
|
||||
previewShowing: PropTypes.bool.isRequired,
|
||||
onPreview: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Tabs.displayName = 'Tabs';
|
||||
26
app/javascript/article-form/components/TagsField.jsx
Normal file
26
app/javascript/article-form/components/TagsField.jsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import Tags from '../../shared/components/tags';
|
||||
|
||||
export const TagsField = ({ defaultValue, onInput, switchHelpContext }) => {
|
||||
return (
|
||||
<div className="crayons-article-form__tagsfield">
|
||||
<Tags
|
||||
defaultValue={defaultValue}
|
||||
maxTags="4"
|
||||
onInput={onInput}
|
||||
onFocus={switchHelpContext}
|
||||
classPrefix="crayons-article-form"
|
||||
fieldClassName="crayons-textfield crayons-textfield--ghost ff-accent"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TagsField.propTypes = {
|
||||
onInput: PropTypes.func.isRequired,
|
||||
defaultValue: PropTypes.string.isRequired,
|
||||
switchHelpContext: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
TagsField.displayName = 'TagsField';
|
||||
34
app/javascript/article-form/components/Title.jsx
Normal file
34
app/javascript/article-form/components/Title.jsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import Textarea from 'preact-textarea-autosize';
|
||||
|
||||
export const Title = ({ onChange, defaultValue, switchHelpContext }) => {
|
||||
return (
|
||||
<div className="crayons-article-form__title">
|
||||
<Textarea
|
||||
className="crayons-textfield crayons-textfield--ghost fs-3xl s:fs-4xl l:fs-5xl fw-bold s:fw-heavy lh-tight"
|
||||
type="text"
|
||||
id="article-form-title"
|
||||
placeholder="New post title here..."
|
||||
autoComplete="off"
|
||||
value={defaultValue}
|
||||
onFocus={switchHelpContext}
|
||||
onInput={onChange}
|
||||
autofocus="true"
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Title.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
defaultValue: PropTypes.string.isRequired,
|
||||
switchHelpContext: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Title.displayName = 'Title';
|
||||
21
app/javascript/article-form/components/Toolbar.jsx
Normal file
21
app/javascript/article-form/components/Toolbar.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ImageUploader } from './ImageUploader';
|
||||
|
||||
export const Toolbar = ({ version }) => {
|
||||
return (
|
||||
<div
|
||||
className={`crayons-article-form__toolbar ${
|
||||
version === 'v1' && 'border-t-0'
|
||||
}`}
|
||||
>
|
||||
<ImageUploader />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Toolbar.propTypes = {
|
||||
version: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
Toolbar.displayName = 'Toolbar';
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { deep, shallow } from 'preact-render-spy';
|
||||
import { ArticleCoverImage } from '../ArticleCoverImage';
|
||||
|
||||
describe('<ArticleCoverImage />', () => {
|
||||
it('renders properly', () => {
|
||||
const tree = render(
|
||||
<ArticleCoverImage
|
||||
mainImage="/i/r5tvutqpl7th0qhzcw7f.png"
|
||||
onMainImageUrlChange={null}
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows the correct view when there is an image uploaded', () => {
|
||||
const container = shallow(
|
||||
<ArticleCoverImage
|
||||
mainImage="/i/r5tvutqpl7th0qhzcw7f.png"
|
||||
onMainImageUrlChange={null}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container.find('.crayons-article-form__cover__image').exists(),
|
||||
).toEqual(true);
|
||||
expect(container.find('.crayons-article-form__cover').text()).toEqual(
|
||||
'ChangeRemove',
|
||||
);
|
||||
expect(container.find('.articleform__uploaderror').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
it('shows the correct view when there is no image uploaded', () => {
|
||||
const container = shallow(
|
||||
<ArticleCoverImage mainImage={null} onMainImageUrlChange={null} />,
|
||||
);
|
||||
expect(
|
||||
container.find('.crayons-article-form__cover__image').exists(),
|
||||
).toEqual(false);
|
||||
expect(container.find('.crayons-article-form__cover').text()).toEqual(
|
||||
'Add a cover image',
|
||||
);
|
||||
});
|
||||
|
||||
it('displays an upload error when necessary', () => {
|
||||
const context = shallow(
|
||||
<ArticleCoverImage mainImage={null} onMainImageUrlChange={null} />,
|
||||
);
|
||||
expect(context.component()).toBeInstanceOf(ArticleCoverImage);
|
||||
context.setState({
|
||||
uploadError: true,
|
||||
uploadErrorMessage: 'Some error message',
|
||||
});
|
||||
|
||||
expect(context.find('.articleform__uploaderror').exists()).toEqual(true);
|
||||
expect(context.find('.articleform__uploaderror').text()).toEqual(
|
||||
'Some error message',
|
||||
);
|
||||
});
|
||||
|
||||
it('should trigger onMainImageUrlChange when the Remove button is pressed', () => {
|
||||
const onMainImageUrlChange = jest.fn();
|
||||
|
||||
const context = deep(
|
||||
<ArticleCoverImage
|
||||
mainImage="/i/r5tvutqpl7th0qhzcw7f.png"
|
||||
onMainImageUrlChange={onMainImageUrlChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
context.find('.crayons-btn--ghost-danger').simulate('click', {
|
||||
preventDefault: () => {},
|
||||
});
|
||||
expect(onMainImageUrlChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { shallow } from 'preact-render-spy';
|
||||
import { Close } from '../Close';
|
||||
|
||||
describe('<Close />', () => {
|
||||
it('renders properly', () => {
|
||||
const tree = render(<Close />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows the modal', () => {
|
||||
const container = shallow(<Close />);
|
||||
expect(container.find('.crayons-article-form__close').exists()).toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
158
app/javascript/article-form/components/__tests__/Form.test.jsx
Normal file
158
app/javascript/article-form/components/__tests__/Form.test.jsx
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { deep } from 'preact-render-spy';
|
||||
import { Form } from '../Form';
|
||||
|
||||
let bodyMarkdown; let mainImage;
|
||||
|
||||
describe('<Form />', () => {
|
||||
describe('v1', () => {
|
||||
beforeEach(() => {
|
||||
bodyMarkdown =
|
||||
'---↵title: Test Title v1↵published: false↵description: some description↵tags: javascript, career↵cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/3/13d3b32a-d381-4549-b95e-ec665768ce8f.png↵---↵↵Lets do this v1 changes↵↵';
|
||||
mainImage =
|
||||
'https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/3/13d3b32a-d381-4549-b95e-ec665768ce8f.png';
|
||||
});
|
||||
|
||||
it('renders properly', () => {
|
||||
const tree = render(
|
||||
<Form
|
||||
titleDefaultValue="Test Title v1"
|
||||
titleOnChange={null}
|
||||
tagsDefaultValue="javascript, career"
|
||||
tagsOnInput={null}
|
||||
bodyDefaultValue={bodyMarkdown}
|
||||
bodyOnChange={null}
|
||||
bodyHasFocus={false}
|
||||
version="v1"
|
||||
mainImage={mainImage}
|
||||
onMainImageUrlChange={null}
|
||||
errors={null}
|
||||
switchHelpContext={null}
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('displays the correct elements', () => {
|
||||
const container = deep(
|
||||
<Form
|
||||
titleDefaultValue="Test Title v1"
|
||||
titleOnChange={null}
|
||||
tagsDefaultValue="javascript, career"
|
||||
tagsOnInput={null}
|
||||
bodyDefaultValue={bodyMarkdown}
|
||||
bodyOnChange={null}
|
||||
bodyHasFocus={false}
|
||||
version="v1"
|
||||
mainImage={mainImage}
|
||||
onMainImageUrlChange={null}
|
||||
errors={null}
|
||||
switchHelpContext={null}
|
||||
/>,
|
||||
);
|
||||
expect(container.find('.crayons-article-form__cover').exists()).toEqual(
|
||||
false,
|
||||
);
|
||||
expect(container.find('.crayons-article-form__title').exists()).toEqual(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
container.find('.crayons-article-form__tagsfield').exists(),
|
||||
).toEqual(false);
|
||||
expect(container.find('.crayons-article-form__body').exists()).toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('v2', () => {
|
||||
beforeEach(() => {
|
||||
bodyMarkdown =
|
||||
'---↵title: Test Title v2↵published: false↵description: some description↵tags: javascript, career↵cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/badge/badge_image/12/8_week_streak-Shadow.png↵---↵↵Lets do this v2 changes↵↵';
|
||||
mainImage =
|
||||
'https://dev-to-uploads.s3.amazonaws.com/uploads/badge/badge_image/12/8_week_streak-Shadow.png';
|
||||
});
|
||||
|
||||
it('renders properly', () => {
|
||||
const tree = render(
|
||||
<Form
|
||||
titleDefaultValue="Test Title v2"
|
||||
titleOnChange={null}
|
||||
tagsDefaultValue="javascript, career"
|
||||
tagsOnInput={null}
|
||||
bodyDefaultValue={bodyMarkdown}
|
||||
bodyOnChange={null}
|
||||
bodyHasFocus={false}
|
||||
version="v2"
|
||||
mainImage="https://dev-to-uploads.s3.amazonaws.com/uploads/badge/badge_image/12/8_week_streak-Shadow.png"
|
||||
onMainImageUrlChange={null}
|
||||
errors={null}
|
||||
switchHelpContext={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('displays the correct elements', () => {
|
||||
const container = deep(
|
||||
<Form
|
||||
titleDefaultValue="Test Title v2"
|
||||
titleOnChange={null}
|
||||
tagsDefaultValue="javascript, career"
|
||||
tagsOnInput={null}
|
||||
bodyDefaultValue={bodyMarkdown}
|
||||
bodyOnChange={null}
|
||||
bodyHasFocus={false}
|
||||
version="v2"
|
||||
mainImage={mainImage}
|
||||
onMainImageUrlChange={null}
|
||||
errors={null}
|
||||
switchHelpContext={null}
|
||||
/>,
|
||||
);
|
||||
expect(container.find('.crayons-article-form__cover').exists()).toEqual(
|
||||
true,
|
||||
);
|
||||
expect(container.find('.crayons-article-form__title').exists()).toEqual(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
container.find('.crayons-article-form__tagsfield').exists(),
|
||||
).toEqual(true);
|
||||
expect(container.find('.crayons-article-form__body').exists()).toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows errors if there are any', () => {
|
||||
const errors = {
|
||||
title: ["can't be blank"],
|
||||
main_image: ['is not a valid URL'],
|
||||
};
|
||||
const container = deep(
|
||||
<Form
|
||||
titleDefaultValue="Test Title v2"
|
||||
titleOnChange={null}
|
||||
tagsDefaultValue="javascript, career"
|
||||
tagsOnInput={null}
|
||||
bodyDefaultValue={bodyMarkdown}
|
||||
bodyOnChange={null}
|
||||
bodyHasFocus={false}
|
||||
version="v2"
|
||||
mainImage={mainImage}
|
||||
onMainImageUrlChange={null}
|
||||
errors={errors}
|
||||
switchHelpContext={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.find('.crayons-notice--danger').exists()).toEqual(true);
|
||||
expect(container.find('.crayons-notice--danger').text()).toMatch('title');
|
||||
expect(container.find('.crayons-notice--danger').text()).toMatch(
|
||||
'main_image',
|
||||
);
|
||||
});
|
||||
});
|
||||
139
app/javascript/article-form/components/__tests__/Help.test.jsx
Normal file
139
app/javascript/article-form/components/__tests__/Help.test.jsx
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { shallow } from 'preact-render-spy';
|
||||
import { Help } from '../Help';
|
||||
|
||||
describe('<Help />', () => {
|
||||
it('renders properly', () => {
|
||||
const tree = render(
|
||||
<Help
|
||||
previewShowing={false}
|
||||
helpFor={null}
|
||||
helpPosition={null}
|
||||
version="v1"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not render help if we are in preview mode', () => {
|
||||
const container = shallow(
|
||||
<Help
|
||||
previewShowing
|
||||
helpFor={null}
|
||||
helpPosition={null}
|
||||
version="v1"
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container.find('.crayons-article-form__main__aside').text().length,
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it('shows some help in edit mode', () => {
|
||||
const container = shallow(
|
||||
<Help
|
||||
previewShowing={false}
|
||||
helpFor={null}
|
||||
helpPosition={null}
|
||||
version="v1"
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container.find('.crayons-article-form__main__aside').text().length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('shows some help in edit mode', () => {
|
||||
const container = shallow(
|
||||
<Help
|
||||
previewShowing={false}
|
||||
helpFor={null}
|
||||
helpPosition={null}
|
||||
version="v1"
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container.find('.crayons-article-form__main__aside').text().length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('shows the correct help for v1', () => {
|
||||
const container = shallow(
|
||||
<Help
|
||||
previewShowing={false}
|
||||
helpFor={null}
|
||||
helpPosition={null}
|
||||
version="v1"
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container.find('.crayons-article-form__main__aside').text().length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(container.find('.spec__basic-editor-help').exists()).toEqual(true);
|
||||
expect(container.find('.spec__format-help').exists()).toEqual(true);
|
||||
expect(container.find('.spec__title-help').exists()).toEqual(false);
|
||||
expect(container.find('.spec__basic-tag-input-help').exists()).toEqual(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('shows the correct help section based on helpFor for v2', () => {
|
||||
const container = shallow(
|
||||
<Help
|
||||
previewShowing={false}
|
||||
helpFor="article-form-title"
|
||||
helpPosition={null}
|
||||
version="v2"
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container.find('.crayons-article-form__main__aside').text().length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(container.find('.spec__title-help').exists()).toEqual(true);
|
||||
expect(container.find('.spec__format-help').exists()).toEqual(false);
|
||||
expect(container.find('.spec__basic-editor-help').exists()).toEqual(false);
|
||||
expect(container.find('.spec__basic-tag-input-help').exists()).toEqual(
|
||||
false,
|
||||
);
|
||||
|
||||
const container2 = shallow(
|
||||
<Help
|
||||
previewShowing={false}
|
||||
helpFor="article_body_markdown"
|
||||
helpPosition={null}
|
||||
version="v2"
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container2.find('.crayons-article-form__main__aside').text().length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(container2.find('.spec__format-help').exists()).toEqual(true);
|
||||
expect(container2.find('.spec__basic-editor-help').exists()).toEqual(false);
|
||||
expect(container2.find('.spec__title-help').exists()).toEqual(false);
|
||||
expect(container2.find('.spec__basic-tag-input-help').exists()).toEqual(
|
||||
false,
|
||||
);
|
||||
|
||||
const container3 = shallow(
|
||||
<Help
|
||||
previewShowing={false}
|
||||
helpFor="tag-input"
|
||||
helpPosition={null}
|
||||
version="v2"
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container3.find('.crayons-article-form__main__aside').text().length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(container3.find('.spec__basic-tag-input-help').exists()).toEqual(
|
||||
true,
|
||||
);
|
||||
expect(container3.find('.spec__format-help').exists()).toEqual(false);
|
||||
expect(container3.find('.spec__basic-editor-help').exists()).toEqual(false);
|
||||
expect(container3.find('.spec__title-help').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
// TODO: test the modals
|
||||
});
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { shallow, deep } from 'preact-render-spy';
|
||||
import { ImageUploader } from '../ImageUploader';
|
||||
|
||||
describe('<ImageUploader />', () => {
|
||||
it('renders correctly without an image', () => {
|
||||
const tree = render(<ImageUploader />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('displays text to copy', () => {
|
||||
const context = deep(<ImageUploader />);
|
||||
expect(context.component()).toBeInstanceOf(ImageUploader);
|
||||
context.setState({
|
||||
insertionImageUrls: ['/i/jxuopxlscfy6wkfbbkvb.png'],
|
||||
uploadError: false,
|
||||
uploadErrorMessage: null,
|
||||
});
|
||||
|
||||
expect(context.find('#image-markdown-copy-link-input').exists()).toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('displays Copied! when clicking on the icon', () => {
|
||||
const context = deep(<ImageUploader />);
|
||||
expect(context.component()).toBeInstanceOf(ImageUploader);
|
||||
context.setState({
|
||||
insertionImageUrls: ['/i/jxuopxlscfy6wkfbbkvb.png'],
|
||||
uploadError: false,
|
||||
uploadErrorMessage: null,
|
||||
});
|
||||
|
||||
document.getElementById = jest.fn(() => {
|
||||
return {
|
||||
value: 'some copied text',
|
||||
setSelectionRange: () => {},
|
||||
};
|
||||
});
|
||||
|
||||
document.execCommand = jest.fn();
|
||||
|
||||
context.find('clipboard-copy').simulate('click');
|
||||
|
||||
expect(context.find('ClipboardButton').attrs().showCopyMessage).toEqual(
|
||||
true,
|
||||
);
|
||||
expect(context.find('#image-markdown-copy-link-announcer').text()).toEqual(
|
||||
'Copied!',
|
||||
);
|
||||
});
|
||||
|
||||
it('displays an error when one occurs', () => {
|
||||
const error = 'Some error message';
|
||||
const context = shallow(<ImageUploader />);
|
||||
expect(context.component()).toBeInstanceOf(ImageUploader);
|
||||
context.setState({
|
||||
insertionImageUrls: [],
|
||||
uploadError: true,
|
||||
uploadErrorMessage: error,
|
||||
});
|
||||
expect(context.find('.color-accent-danger').text()).toEqual(error);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { shallow } from 'preact-render-spy';
|
||||
import { Options } from '../Options';
|
||||
|
||||
let passedData;
|
||||
|
||||
const getOptions = (passedData) => (
|
||||
<Options
|
||||
passedData={passedData}
|
||||
onConfigChange={null}
|
||||
onSaveDraft={null}
|
||||
moreConfigShowing={null}
|
||||
toggleMoreConfig={null}
|
||||
/>
|
||||
);
|
||||
|
||||
describe('<Options />', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
passedData = {
|
||||
"id": null,
|
||||
"title": "Test v2 Title",
|
||||
"tagList": "javascript, career, ",
|
||||
"description": "",
|
||||
"canonicalUrl": "",
|
||||
"series": "",
|
||||
"allSeries": [
|
||||
"Learn Something new a day"
|
||||
],
|
||||
"bodyMarkdown": "\nLet's write something here...",
|
||||
"published": false,
|
||||
"previewShowing": false,
|
||||
"previewResponse": "",
|
||||
"submitting": false,
|
||||
"editing": false,
|
||||
"mainImage": "/i/9pouqdqxcl4f6rwk1yfd.jpg",
|
||||
"organizations": [
|
||||
{
|
||||
"id": 4,
|
||||
"bg_color_hex": "",
|
||||
"name": "DEV",
|
||||
"text_color_hex": "",
|
||||
"profile_image_90": "/uploads/organization/profile_image/4/1689e7ae-6306-43cd-acba-8bde7ed80a17.JPG"
|
||||
}
|
||||
],
|
||||
"organizationId": null,
|
||||
"errors": null,
|
||||
"edited": true,
|
||||
"updatedAt": null,
|
||||
"version": "v2",
|
||||
"helpFor": "article_body_markdown",
|
||||
"helpPosition": 421
|
||||
}
|
||||
});
|
||||
|
||||
it('renders properly', () => {
|
||||
const tree = render(getOptions(passedData));
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows the danger zone once an article is published', ()=> {
|
||||
passedData.published = true;
|
||||
const container = shallow(getOptions(passedData));
|
||||
expect(container.find('.spec__options-danger-zone').exists()).toEqual(true,)
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { h } from 'preact';
|
||||
import { shallow } from 'preact-render-spy';
|
||||
import { PageTitle } from '../PageTitle';
|
||||
|
||||
let organizations; let organizationId; let onToggle;
|
||||
|
||||
describe('<PageTitle/>', () => {
|
||||
beforeEach(() => {
|
||||
organizations = [
|
||||
{
|
||||
id: 4,
|
||||
bg_color_hex: '',
|
||||
name: 'DEV',
|
||||
text_color_hex: '',
|
||||
profile_image_90:
|
||||
'/uploads/organization/profile_image/4/1689e7ae-6306-43cd-acba-8bde7ed80a17.JPG',
|
||||
},
|
||||
];
|
||||
|
||||
organizationId = null;
|
||||
onToggle = null;
|
||||
});
|
||||
|
||||
it('shows the picker if there is more than one organisation', () => {
|
||||
const container = shallow(
|
||||
<PageTitle
|
||||
organizations={organizations}
|
||||
organizationId={organizationId}
|
||||
onToggle={onToggle}
|
||||
/>,
|
||||
);
|
||||
expect(container.find('#article_publish_under_org').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
it('does not show the picker if there is no organisations', () => {
|
||||
organizations = [];
|
||||
const container = shallow(
|
||||
<PageTitle
|
||||
organizations={organizations}
|
||||
organizationId={organizationId}
|
||||
onToggle={onToggle}
|
||||
/>,
|
||||
);
|
||||
expect(container.find('#article_publish_under_org').exists()).toEqual(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { shallow } from 'preact-render-spy';
|
||||
import { Preview } from '../Preview';
|
||||
|
||||
const doc = new JSDOM('<!doctype html><html><body></body></html>');
|
||||
global.document = doc;
|
||||
global.window = doc.defaultView;
|
||||
global.window.currentUser = {
|
||||
id: 1,
|
||||
name: 'Guy Fieri',
|
||||
username: 'guyfieri',
|
||||
profile_image_90:
|
||||
'/uploads/user/profile_image/41/0841dbe2-208c-4daa-b498-b2f01f3d37b2.png',
|
||||
};
|
||||
|
||||
let previewResponse;
|
||||
let articleState;
|
||||
let errors;
|
||||
|
||||
describe('<Preview />', () => {
|
||||
beforeEach(() => {
|
||||
previewResponse = {
|
||||
processed_html:
|
||||
'<p>My Awesome Post! Not very long, but still very awesome.</p>↵↵',
|
||||
title: 'My Awesome Post',
|
||||
tags: null,
|
||||
cover_image: 'http://lorempixel.com/400/200/',
|
||||
};
|
||||
|
||||
articleState = {
|
||||
id: null,
|
||||
title: 'My Awesome Post',
|
||||
tagList: 'javascript, career, ',
|
||||
description: 'Some description',
|
||||
canonicalUrl: '',
|
||||
series: '',
|
||||
allSeries: ['Learn Something new a day'],
|
||||
bodyMarkdown:
|
||||
'---↵title: My Awesome Post↵published: false↵description: ↵tags: ↵---↵↵My Awesome Post Not very long, but still very awesome! ↵',
|
||||
submitting: false,
|
||||
editing: false,
|
||||
mainImage: '/i/9ca8kb1cu34mobypm5yx.png',
|
||||
organizations: [
|
||||
{
|
||||
id: 4,
|
||||
bg_color_hex: '',
|
||||
name: 'DEV',
|
||||
text_color_hex: '',
|
||||
profile_image_90:
|
||||
'/uploads/organization/profile_image/4/1689e7ae-6306-43cd-acba-8bde7ed80a17.JPG',
|
||||
},
|
||||
],
|
||||
organizationId: null,
|
||||
errors: null,
|
||||
edited: true,
|
||||
updatedAt: null,
|
||||
version: 'v2',
|
||||
helpFor: null,
|
||||
helpPosition: null,
|
||||
};
|
||||
errors = null;
|
||||
});
|
||||
|
||||
it('renders properly', () => {
|
||||
const tree = render(
|
||||
<Preview
|
||||
previewResponse={previewResponse}
|
||||
articleState={articleState}
|
||||
errors={errors}
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows the correct title', () => {
|
||||
const container = shallow(
|
||||
<Preview
|
||||
previewResponse={previewResponse}
|
||||
articleState={articleState}
|
||||
errors={errors}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.find('.spec-article__title').text()).toEqual(
|
||||
previewResponse.title,
|
||||
);
|
||||
});
|
||||
|
||||
it('shows the correct tags', () => {
|
||||
const container = shallow(
|
||||
<Preview
|
||||
previewResponse={previewResponse}
|
||||
articleState={articleState}
|
||||
errors={errors}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.find('.crayons-article__tags').text()).toEqual(
|
||||
'#javascript#career',
|
||||
);
|
||||
});
|
||||
|
||||
it('shows a cover image in the preview if one exists', () => {
|
||||
const container = shallow(
|
||||
<Preview
|
||||
previewResponse={previewResponse}
|
||||
articleState={articleState}
|
||||
errors={errors}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.find('.crayons-article__cover__image').exists()).toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not show a cover image in the preview if one does not exists', () => {
|
||||
previewResponse.cover_image = null;
|
||||
articleState.mainImage = null;
|
||||
|
||||
const container = shallow(
|
||||
<Preview
|
||||
previewResponse={previewResponse}
|
||||
articleState={articleState}
|
||||
errors={errors}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.find('.crayons-article__cover__image').exists()).toEqual(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: need to write a test for the cover image for v1
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { shallow } from 'preact-render-spy';
|
||||
import { Tabs } from '../Tabs';
|
||||
|
||||
describe('<Tabs />', () => {
|
||||
it('renders properly', () => {
|
||||
const tree = render(<Tabs onPreview={null} previewShowing={false} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('highlights the current tab', () => {
|
||||
const container1 = shallow(<Tabs onPreview={null} previewShowing />);
|
||||
expect(container1.find('.crayons-tabs__item--current').text()).toEqual(
|
||||
'Preview',
|
||||
);
|
||||
|
||||
const container2 = shallow(
|
||||
<Tabs onPreview={null} previewShowing={false} />,
|
||||
);
|
||||
expect(container2.find('.crayons-tabs__item--current').text()).toEqual(
|
||||
'Edit',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { Title } from '../Title';
|
||||
|
||||
describe('<Title />', () => {
|
||||
it('renders properly', () => {
|
||||
const tree = render(
|
||||
<Title
|
||||
defaultValue="Test title"
|
||||
onChange={null}
|
||||
switchHelpContext={null}
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ArticleCoverImage /> renders properly 1`] = `
|
||||
<div
|
||||
class="crayons-article-form__cover"
|
||||
role="presentation"
|
||||
>
|
||||
<img
|
||||
alt="Post cover"
|
||||
class="crayons-article-form__cover__image"
|
||||
height="105"
|
||||
src="/i/r5tvutqpl7th0qhzcw7f.png"
|
||||
width="250"
|
||||
/>
|
||||
<div
|
||||
class="flex items-center"
|
||||
>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--outlined mr-2 whitespace-nowrap"
|
||||
disabled={false}
|
||||
type="button"
|
||||
>
|
||||
<label
|
||||
htmlFor="cover-image-input"
|
||||
>
|
||||
Change
|
||||
</label>
|
||||
<input
|
||||
accept="image/*"
|
||||
class="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer"
|
||||
data-max-file-size-mb="25"
|
||||
id="cover-image-input"
|
||||
onChange={[Function]}
|
||||
type="file"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--ghost-danger"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Close /> renders properly 1`] = `
|
||||
<div
|
||||
class="crayons-article-form__close"
|
||||
>
|
||||
<a
|
||||
class="crayons-btn crayons-btn--ghost crayons-btn--icon"
|
||||
href="/"
|
||||
title="Close the editor"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="as1mn15llu5e032u2pgzlc6yhvss2myk"
|
||||
class="crayons-icon"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title
|
||||
id="as1mn15llu5e032u2pgzlc6yhvss2myk"
|
||||
>
|
||||
Close the editor
|
||||
</title>
|
||||
<path
|
||||
d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Form /> v1 renders properly 1`] = `
|
||||
<div
|
||||
class="crayons-article-form__main__content crayons-card"
|
||||
>
|
||||
<div
|
||||
class="crayons-article-form__body"
|
||||
>
|
||||
<div
|
||||
class="crayons-article-form__toolbar border-t-0"
|
||||
>
|
||||
<div
|
||||
class="flex items-center"
|
||||
>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--ghost crayons-btn--icon-left mr-2 fw-normal"
|
||||
disabled={false}
|
||||
tabIndex="-1"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="a17qec5pfhrwzk9w4kg0tp62v27qqu9t"
|
||||
class="crayons-icon"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title
|
||||
id="a17qec5pfhrwzk9w4kg0tp62v27qqu9t"
|
||||
>
|
||||
Upload image
|
||||
</title>
|
||||
<path
|
||||
d="M20 5H4v14l9.292-9.294a1 1 0 011.414 0L20 15.01V5zM2 3.993A1 1 0 012.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 01-.992.993H2.992A.993.993 0 012 20.007V3.993zM8 11a2 2 0 110-4 2 2 0 010 4z"
|
||||
/>
|
||||
</svg>
|
||||
Upload image
|
||||
<input
|
||||
accept="image/*"
|
||||
class="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer"
|
||||
data-max-file-size-mb="25"
|
||||
id="image-upload-field"
|
||||
multiple={true}
|
||||
onChange={[Function]}
|
||||
tabIndex="-1"
|
||||
type="file"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
class="crayons-textfield crayons-textfield--ghost crayons-article-form__body__field"
|
||||
id="article_body_markdown"
|
||||
name="body_markdown"
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onInput={null}
|
||||
placeholder="Write your post content here..."
|
||||
ref={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 0,
|
||||
}
|
||||
}
|
||||
value="---↵title: Test Title v1↵published: false↵description: some description↵tags: javascript, career↵cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/3/13d3b32a-d381-4549-b95e-ec665768ce8f.png↵---↵↵Lets do this v1 changes↵↵"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<Form /> v2 renders properly 1`] = `
|
||||
<div
|
||||
class="crayons-article-form__main__content crayons-card"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="crayons-article-form__cover"
|
||||
role="presentation"
|
||||
>
|
||||
<img
|
||||
alt="Post cover"
|
||||
class="crayons-article-form__cover__image"
|
||||
height="105"
|
||||
src="https://dev-to-uploads.s3.amazonaws.com/uploads/badge/badge_image/12/8_week_streak-Shadow.png"
|
||||
width="250"
|
||||
/>
|
||||
<div
|
||||
class="flex items-center"
|
||||
>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--outlined mr-2 whitespace-nowrap"
|
||||
disabled={false}
|
||||
type="button"
|
||||
>
|
||||
<label
|
||||
htmlFor="cover-image-input"
|
||||
>
|
||||
Change
|
||||
</label>
|
||||
<input
|
||||
accept="image/*"
|
||||
class="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer"
|
||||
data-max-file-size-mb="25"
|
||||
id="cover-image-input"
|
||||
onChange={[Function]}
|
||||
type="file"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--ghost-danger"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-article-form__title"
|
||||
>
|
||||
<textarea
|
||||
autoComplete="off"
|
||||
autofocus="true"
|
||||
class="crayons-textfield crayons-textfield--ghost fs-3xl s:fs-4xl l:fs-5xl fw-bold s:fw-heavy lh-tight"
|
||||
id="article-form-title"
|
||||
onChange={[Function]}
|
||||
onFocus={null}
|
||||
onInput={null}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="New post title here..."
|
||||
ref={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 0,
|
||||
}
|
||||
}
|
||||
type="text"
|
||||
value="Test Title v2"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-article-form__tagsfield"
|
||||
>
|
||||
<div
|
||||
class="crayons-article-form__tagswrapper"
|
||||
>
|
||||
<input
|
||||
autoComplete="off"
|
||||
class="crayons-textfield crayons-textfield--ghost ff-accent crayons-article-form__tags"
|
||||
id="tag-input"
|
||||
name="listing[tag_list]"
|
||||
onBlur={[Function]}
|
||||
onFocus={null}
|
||||
onInput={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="Add up to 4 tags..."
|
||||
ref={[Function]}
|
||||
type="text"
|
||||
value="javascript, career"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-article-form__body"
|
||||
>
|
||||
<div
|
||||
class="crayons-article-form__toolbar false"
|
||||
>
|
||||
<div
|
||||
class="flex items-center"
|
||||
>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--ghost crayons-btn--icon-left mr-2 fw-normal"
|
||||
disabled={false}
|
||||
tabIndex="-1"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="a17qec5pfhrwzk9w4kg0tp62v27qqu9t"
|
||||
class="crayons-icon"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title
|
||||
id="a17qec5pfhrwzk9w4kg0tp62v27qqu9t"
|
||||
>
|
||||
Upload image
|
||||
</title>
|
||||
<path
|
||||
d="M20 5H4v14l9.292-9.294a1 1 0 011.414 0L20 15.01V5zM2 3.993A1 1 0 012.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 01-.992.993H2.992A.993.993 0 012 20.007V3.993zM8 11a2 2 0 110-4 2 2 0 010 4z"
|
||||
/>
|
||||
</svg>
|
||||
Upload image
|
||||
<input
|
||||
accept="image/*"
|
||||
class="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer"
|
||||
data-max-file-size-mb="25"
|
||||
id="image-upload-field"
|
||||
multiple={true}
|
||||
onChange={[Function]}
|
||||
tabIndex="-1"
|
||||
type="file"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
class="crayons-textfield crayons-textfield--ghost crayons-article-form__body__field"
|
||||
id="article_body_markdown"
|
||||
name="body_markdown"
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onInput={null}
|
||||
placeholder="Write your post content here..."
|
||||
ref={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 0,
|
||||
}
|
||||
}
|
||||
value="---↵title: Test Title v2↵published: false↵description: some description↵tags: javascript, career↵cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/badge/badge_image/12/8_week_streak-Shadow.png↵---↵↵Lets do this v2 changes↵↵"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Help /> renders properly 1`] = `
|
||||
<div
|
||||
class="crayons-article-form__main__aside"
|
||||
>
|
||||
<div
|
||||
class="sticky"
|
||||
style={
|
||||
Object {
|
||||
"top": "56px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
class="spec__basic-editor-help crayons-card crayons-card--secondary p-4 mb-6"
|
||||
>
|
||||
You are currently using the basic markdown editor that uses
|
||||
<a
|
||||
href="#frontmatter"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Jekyll front matter
|
||||
</a>
|
||||
. You can also use the
|
||||
<em>
|
||||
rich+markdown
|
||||
</em>
|
||||
editor you can find in
|
||||
<a
|
||||
href="/settings/ux"
|
||||
>
|
||||
UX settings
|
||||
<svg
|
||||
aria-labelledby="c038a36b2512ed25db907e179ab45cfc"
|
||||
class="crayons-icon"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title
|
||||
id="c038a36b2512ed25db907e179ab45cfc"
|
||||
>
|
||||
Open UX settings
|
||||
</title>
|
||||
<path
|
||||
d="M10.667 8v1.333H7.333v7.334h7.334v-3.334H16v4a.666.666 0 01-.667.667H6.667A.666.666 0 016 17.333V8.667A.667.667 0 016.667 8h4zM18 6v5.333h-1.333V8.275l-5.196 5.196-.942-.942 5.194-5.196h-3.056V6H18z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
<div
|
||||
class="spec__format-help crayons-article-form__help crayons-article-form__help--body"
|
||||
>
|
||||
<h4
|
||||
class="mb-2 fs-l"
|
||||
>
|
||||
Editor Basics
|
||||
</h4>
|
||||
<ul
|
||||
class="list-disc pl-6 color-base-70"
|
||||
>
|
||||
<li>
|
||||
Use
|
||||
<a
|
||||
href="#markdown"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Markdown
|
||||
</a>
|
||||
to write and format posts.
|
||||
</li>
|
||||
<li>
|
||||
You can use
|
||||
<a
|
||||
href="#liquid"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Liquid tags
|
||||
</a>
|
||||
to add rich content such as Tweets, YouTube videos, etc.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ImageUploader /> renders correctly without an image 1`] = `
|
||||
<div
|
||||
class="flex items-center"
|
||||
>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--ghost crayons-btn--icon-left mr-2 fw-normal"
|
||||
disabled={false}
|
||||
tabIndex="-1"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="a17qec5pfhrwzk9w4kg0tp62v27qqu9t"
|
||||
class="crayons-icon"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title
|
||||
id="a17qec5pfhrwzk9w4kg0tp62v27qqu9t"
|
||||
>
|
||||
Upload image
|
||||
</title>
|
||||
<path
|
||||
d="M20 5H4v14l9.292-9.294a1 1 0 011.414 0L20 15.01V5zM2 3.993A1 1 0 012.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 01-.992.993H2.992A.993.993 0 012 20.007V3.993zM8 11a2 2 0 110-4 2 2 0 010 4z"
|
||||
/>
|
||||
</svg>
|
||||
Upload image
|
||||
<input
|
||||
accept="image/*"
|
||||
class="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer"
|
||||
data-max-file-size-mb="25"
|
||||
id="image-upload-field"
|
||||
multiple={true}
|
||||
onChange={[Function]}
|
||||
tabIndex="-1"
|
||||
type="file"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Options /> renders properly 1`] = `
|
||||
<div
|
||||
class="crayons-dropdown"
|
||||
>
|
||||
<h3
|
||||
class="mb-6"
|
||||
>
|
||||
Post options
|
||||
</h3>
|
||||
<div
|
||||
class="crayons-field mb-6"
|
||||
>
|
||||
<label
|
||||
class="crayons-field__label"
|
||||
htmlFor="canonicalUrl"
|
||||
>
|
||||
Canonical URL
|
||||
</label>
|
||||
<p
|
||||
class="crayons-field__description"
|
||||
>
|
||||
Change meta tag
|
||||
<code>
|
||||
canonical_url
|
||||
</code>
|
||||
if this post was first published elsewhere (like your own blog).
|
||||
</p>
|
||||
<input
|
||||
class="crayons-textfield"
|
||||
id="canonicalUrl"
|
||||
name="canonicalUrl"
|
||||
onKeyUp={null}
|
||||
placeholder="https://yoursite.com/post-title"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="crayons-field mb-6"
|
||||
>
|
||||
<label
|
||||
class="crayons-field__label"
|
||||
htmlFor="series"
|
||||
>
|
||||
Series
|
||||
</label>
|
||||
<p
|
||||
class="crayons-field__description"
|
||||
>
|
||||
Will this post be part of a series? Give the series a unique name. (Series visible once it has multiple posts)
|
||||
</p>
|
||||
<input
|
||||
class="crayons-textfield"
|
||||
id="series"
|
||||
name="series"
|
||||
onKeyUp={null}
|
||||
placeholder="..."
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<p
|
||||
class="crayons-field__description"
|
||||
>
|
||||
Existing series:
|
||||
<button
|
||||
name="series"
|
||||
onClick={null}
|
||||
type="button"
|
||||
value="Learn Something new a day"
|
||||
>
|
||||
Learn Something new a day
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="crayons-btn w-100"
|
||||
disabled={false}
|
||||
onClick={null}
|
||||
type="button"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Preview /> renders properly 1`] = `
|
||||
<div
|
||||
class="crayons-article-form__main__content crayons-card"
|
||||
>
|
||||
<article
|
||||
class="crayons-article"
|
||||
>
|
||||
<header
|
||||
class="crayons-article__header"
|
||||
>
|
||||
<div
|
||||
class="crayons-article__cover"
|
||||
>
|
||||
<div
|
||||
class="crayons-article__cover__image"
|
||||
style={
|
||||
Object {
|
||||
"backgroundImage": "url(/i/9ca8kb1cu34mobypm5yx.png)",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<h1
|
||||
class="fs-3xl s:fs-4xl l:fs-5xl fw-bold s:fw-heavy lh-tight mb-6 spec-article__title"
|
||||
>
|
||||
My Awesome Post
|
||||
</h1>
|
||||
<div
|
||||
class="crayons-article__tags"
|
||||
>
|
||||
<span
|
||||
class="crayons-tag mr-2"
|
||||
>
|
||||
<span
|
||||
class="crayons-tag__prefix"
|
||||
>
|
||||
#
|
||||
</span>
|
||||
javascript
|
||||
</span>
|
||||
<span
|
||||
class="crayons-tag mr-2"
|
||||
>
|
||||
<span
|
||||
class="crayons-tag__prefix"
|
||||
>
|
||||
#
|
||||
</span>
|
||||
career
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
class="crayons-article__body"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>My Awesome Post! Not very long, but still very awesome.</p>↵↵",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Tabs /> renders properly 1`] = `
|
||||
<div
|
||||
class="crayons-article-form__tabs crayons-tabs ml-auto"
|
||||
>
|
||||
<button
|
||||
class="crayons-tabs__item crayons-tabs__item--current"
|
||||
onClick={false}
|
||||
type="button"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
class="crayons-tabs__item false"
|
||||
onClick={null}
|
||||
type="button"
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Title /> renders properly 1`] = `
|
||||
<div
|
||||
class="crayons-article-form__title"
|
||||
>
|
||||
<textarea
|
||||
autoComplete="off"
|
||||
autofocus="true"
|
||||
class="crayons-textfield crayons-textfield--ghost fs-3xl s:fs-4xl l:fs-5xl fw-bold s:fw-heavy lh-tight"
|
||||
id="article-form-title"
|
||||
onChange={[Function]}
|
||||
onFocus={null}
|
||||
onInput={null}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="New post title here..."
|
||||
ref={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 0,
|
||||
}
|
||||
}
|
||||
type="text"
|
||||
value="Test title"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
6
app/javascript/article-form/components/index.jsx
Normal file
6
app/javascript/article-form/components/index.jsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export * from './EditorActions';
|
||||
export * from './Form';
|
||||
export * from './Header';
|
||||
export * from './Help';
|
||||
export * from './Preview';
|
||||
export * from './keyboardShortcutsHandler';
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class KeyboardShortcutsHandler extends Component {
|
||||
export class KeyboardShortcutsHandler extends Component {
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.handleKeyDown());
|
||||
}
|
||||
|
|
@ -35,4 +35,4 @@ KeyboardShortcutsHandler.propTypes = {
|
|||
togglePreview: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default KeyboardShortcutsHandler;
|
||||
KeyboardShortcutsHandler.displayName = 'KeyboardShortcutsHandler';
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<bodyPreview version="v1" /> renders properly with an image 1`] = `
|
||||
<div
|
||||
class="container"
|
||||
style={
|
||||
Object {
|
||||
"border": "0px",
|
||||
"boxShadow": "0px 0px 0px #fff",
|
||||
"marginTop": "10px",
|
||||
"minHeight": "508px",
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="articleform__mainimage articleform__mainimagepreview"
|
||||
>
|
||||
<img
|
||||
alt="cover"
|
||||
src="http://lorempixel.com/400/200/"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="title"
|
||||
style={
|
||||
Object {
|
||||
"maxWidth": "1000px",
|
||||
"width": "90%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h1>
|
||||
My Awesome Post
|
||||
</h1>
|
||||
<h3>
|
||||
<img
|
||||
alt="profile"
|
||||
class="profile-pic"
|
||||
src="/uploads/user/profile_image/41/0841dbe2-208c-4daa-b498-b2f01f3d37b2.png"
|
||||
/>
|
||||
|
||||
<span>
|
||||
Guy Fieri
|
||||
</span>
|
||||
</h3>
|
||||
<div
|
||||
class="tags"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="body"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>My Awesome Post! Not very long, but still very awesome.</p>↵↵",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"width": "90%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<bodyPreview version="v1" /> renders properly without an image 1`] = `
|
||||
<div
|
||||
class="container"
|
||||
style={
|
||||
Object {
|
||||
"border": "0px",
|
||||
"boxShadow": "0px 0px 0px #fff",
|
||||
"marginTop": "10px",
|
||||
"minHeight": "508px",
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="title"
|
||||
style={
|
||||
Object {
|
||||
"maxWidth": "1000px",
|
||||
"width": "90%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h1>
|
||||
My Awesome Post
|
||||
</h1>
|
||||
<h3>
|
||||
<img
|
||||
alt="profile"
|
||||
class="profile-pic"
|
||||
src="/uploads/user/profile_image/41/0841dbe2-208c-4daa-b498-b2f01f3d37b2.png"
|
||||
/>
|
||||
|
||||
<span>
|
||||
Guy Fieri
|
||||
</span>
|
||||
</h3>
|
||||
<div
|
||||
class="tags"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="body"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>My Awesome Post! Not very long, but still very awesome.</p>↵↵",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"width": "90%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<bodyPreview version="v2" /> renders properly with an image 1`] = `
|
||||
<div
|
||||
class="container"
|
||||
style={
|
||||
Object {
|
||||
"border": "0px",
|
||||
"boxShadow": "0px 0px 0px #fff",
|
||||
"marginTop": "10px",
|
||||
"minHeight": "508px",
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="articleform__mainimage articleform__mainimagepreview"
|
||||
>
|
||||
<img
|
||||
alt="cover"
|
||||
src="http://lorempixel.com/400/200/"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="title"
|
||||
style={
|
||||
Object {
|
||||
"maxWidth": "1000px",
|
||||
"width": "90%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h1>
|
||||
My Awesome Post
|
||||
</h1>
|
||||
<h3>
|
||||
<img
|
||||
alt="profile"
|
||||
class="profile-pic"
|
||||
src="/uploads/user/profile_image/41/0841dbe2-208c-4daa-b498-b2f01f3d37b2.png"
|
||||
/>
|
||||
|
||||
<span>
|
||||
Guy Fieri
|
||||
</span>
|
||||
</h3>
|
||||
<div
|
||||
class="tags"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="body"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>My Awesome Post! Not very long, but still very awesome.</p>↵↵",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"width": "90%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<bodyPreview version="v2" /> renders properly without an image 1`] = `
|
||||
<div
|
||||
class="container"
|
||||
style={
|
||||
Object {
|
||||
"border": "0px",
|
||||
"boxShadow": "0px 0px 0px #fff",
|
||||
"marginTop": "10px",
|
||||
"minHeight": "508px",
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="title"
|
||||
style={
|
||||
Object {
|
||||
"maxWidth": "1000px",
|
||||
"width": "90%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h1>
|
||||
My Awesome Post
|
||||
</h1>
|
||||
<h3>
|
||||
<img
|
||||
alt="profile"
|
||||
class="profile-pic"
|
||||
src="/uploads/user/profile_image/41/0841dbe2-208c-4daa-b498-b2f01f3d37b2.png"
|
||||
/>
|
||||
|
||||
<span>
|
||||
Guy Fieri
|
||||
</span>
|
||||
</h3>
|
||||
<div
|
||||
class="tags"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="body"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>My Awesome Post! Not very long, but still very awesome.</p>↵↵",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"width": "90%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Description /> should render the description 1`] = `
|
||||
<textarea
|
||||
class="articleform__description"
|
||||
name="description"
|
||||
onChange={[Function]}
|
||||
placeholder="description"
|
||||
type="text"
|
||||
value="Some description"
|
||||
/>
|
||||
`;
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Errors /> renders properly 1`] = `
|
||||
<div
|
||||
class="articleform__errors"
|
||||
>
|
||||
<h2>
|
||||
<span
|
||||
aria-label="face screaming in fear"
|
||||
role="img"
|
||||
>
|
||||
😱
|
||||
</span>
|
||||
Heads up:
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
0: Error 1
|
||||
</li>
|
||||
<li>
|
||||
1: Error 2
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<MainImage /> renders properly 1`] = `
|
||||
<div
|
||||
class="articleform__mainimage"
|
||||
onClick={[MockFunction]}
|
||||
onKeyUp={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
src="http://lorempixel.com/400/200/"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<notice /> renders properly when published with a first version 1`] = `
|
||||
<div
|
||||
class="articleform__notice articleform__notice--publishing"
|
||||
>
|
||||
Saving ...
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<notice /> renders properly when published with a second version 1`] = `
|
||||
<div
|
||||
class="articleform__notice articleform__notice--publishing"
|
||||
>
|
||||
Publishing...
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<notice /> renders properly when unpublished with a version 1`] = `
|
||||
<div
|
||||
class="articleform__notice articleform__notice--draft"
|
||||
>
|
||||
Saving Draft...
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<notice /> renders properly when unpublished without a version 1`] = `
|
||||
<div
|
||||
class="articleform__notice articleform__notice--draft"
|
||||
>
|
||||
Saving ...
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Tags /> only allows 4 tags 1`] = `
|
||||
Object {
|
||||
"additionalTags": Array [],
|
||||
"cursorIdx": 0,
|
||||
"prevLen": 0,
|
||||
"searchResults": Array [],
|
||||
"selectedIndex": -1,
|
||||
"showingRulesForTag": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<Tags /> renders properly 1`] = `
|
||||
<div
|
||||
class="articleform__tagswrapper"
|
||||
>
|
||||
<input
|
||||
autoComplete="off"
|
||||
class="articleform__tags"
|
||||
id="tag-input"
|
||||
name="listing[tag_list]"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onInput={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="4 tags max, comma separated, no spaces or special characters"
|
||||
ref={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<Tags /> replaces tag when editing 1`] = `
|
||||
Object {
|
||||
"additionalTags": Array [],
|
||||
"cursorIdx": 0,
|
||||
"prevLen": 0,
|
||||
"searchResults": Array [],
|
||||
"selectedIndex": -1,
|
||||
"showingRulesForTag": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<Tags /> selects tag when you click on it 1`] = `
|
||||
Object {
|
||||
"additionalTags": Array [],
|
||||
"cursorIdx": 0,
|
||||
"prevLen": 0,
|
||||
"searchResults": Array [],
|
||||
"selectedIndex": -1,
|
||||
"showingRulesForTag": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<Tags /> shows tags as you search 1`] = `
|
||||
Object {
|
||||
"additionalTags": Array [],
|
||||
"cursorIdx": 2,
|
||||
"prevLen": 0,
|
||||
"searchResults": Array [
|
||||
Object {
|
||||
"hotness_score": 0,
|
||||
"name": "git",
|
||||
"short_summary": null,
|
||||
"supported": true,
|
||||
},
|
||||
],
|
||||
"selectedIndex": 0,
|
||||
"showingRulesForTag": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<Tags /> shows tags when editing 1`] = `
|
||||
Object {
|
||||
"additionalTags": Array [],
|
||||
"cursorIdx": 2,
|
||||
"prevLen": 0,
|
||||
"searchResults": Array [
|
||||
Object {
|
||||
"hotness_score": 0,
|
||||
"name": "git",
|
||||
"short_summary": null,
|
||||
"supported": true,
|
||||
},
|
||||
],
|
||||
"selectedIndex": 0,
|
||||
"showingRulesForTag": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<Tags /> skips the click handler if className is articleform__tagsoptionrulesbutton 1`] = `
|
||||
Object {
|
||||
"additionalTags": Array [],
|
||||
"cursorIdx": 0,
|
||||
"prevLen": 0,
|
||||
"searchResults": Array [],
|
||||
"selectedIndex": -1,
|
||||
"showingRulesForTag": null,
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { shallow } from 'preact-render-spy';
|
||||
import BodyPreview from '../bodyPreview';
|
||||
|
||||
const doc = new JSDOM('<!doctype html><html><body></body></html>');
|
||||
global.document = doc;
|
||||
global.window = doc.defaultView;
|
||||
global.window.currentUser = {
|
||||
id: 1,
|
||||
name: 'Guy Fieri',
|
||||
username: 'guyfieri',
|
||||
profile_image_90:
|
||||
'/uploads/user/profile_image/41/0841dbe2-208c-4daa-b498-b2f01f3d37b2.png',
|
||||
};
|
||||
|
||||
let previewResponse;
|
||||
let articleState;
|
||||
|
||||
describe('<bodyPreview version="v1" />', () => {
|
||||
const version = 'v1';
|
||||
beforeEach(() => {
|
||||
previewResponse = {
|
||||
processed_html:
|
||||
'<p>My Awesome Post! Not very long, but still very awesome.</p>↵↵',
|
||||
title: 'My Awesome Post',
|
||||
tags: null,
|
||||
cover_image: 'http://lorempixel.com/400/200/',
|
||||
};
|
||||
|
||||
articleState = {
|
||||
id: 1,
|
||||
title: 'My Awesome Post',
|
||||
tagList: '',
|
||||
bodyMarkdown:
|
||||
'---↵title: My Awesome Post↵published: false↵description: ↵tags: ↵---↵↵My Awesome Post Not very long, but still very awesome! ↵',
|
||||
published: false,
|
||||
previewShowing: true,
|
||||
previewResponse,
|
||||
};
|
||||
});
|
||||
|
||||
it('renders properly with an image', () => {
|
||||
const tree = render(
|
||||
<BodyPreview
|
||||
previewResponse={previewResponse}
|
||||
version={version}
|
||||
articleState={articleState}
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows an image in preview if one exists', () => {
|
||||
const container = shallow(
|
||||
<BodyPreview
|
||||
previewResponse={previewResponse}
|
||||
version={version}
|
||||
articleState={articleState}
|
||||
/>,
|
||||
);
|
||||
expect(container.find('.articleform__mainimagepreview').exists()).toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders properly without an image', () => {
|
||||
previewResponse.cover_image = null;
|
||||
const tree = render(
|
||||
<BodyPreview
|
||||
previewResponse={previewResponse}
|
||||
version={version}
|
||||
articleState={articleState}
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not show an image in preview if one does not exist', () => {
|
||||
previewResponse.cover_image = null;
|
||||
const container = shallow(
|
||||
<BodyPreview
|
||||
previewResponse={previewResponse}
|
||||
version={version}
|
||||
articleState={articleState}
|
||||
/>,
|
||||
);
|
||||
expect(container.find('.articleform__mainimagepreview').exists()).toEqual(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('<bodyPreview version="v2" />', () => {
|
||||
const version = 'v2';
|
||||
beforeEach(() => {
|
||||
previewResponse = {
|
||||
processed_html:
|
||||
'<p>My Awesome Post! Not very long, but still very awesome.</p>↵↵',
|
||||
};
|
||||
|
||||
articleState = {
|
||||
id: 1,
|
||||
title: 'My Awesome Post',
|
||||
tagList: '',
|
||||
bodyMarkdown:
|
||||
'---↵title: My Awesome Post↵published: false↵description: ↵tags: ↵---↵↵My Awesome Post Not very long, but still very awesome! ↵',
|
||||
mainImage: 'http://lorempixel.com/400/200/',
|
||||
published: false,
|
||||
previewShowing: true,
|
||||
previewResponse,
|
||||
};
|
||||
});
|
||||
|
||||
it('renders properly with an image', () => {
|
||||
const tree = render(
|
||||
<BodyPreview
|
||||
previewResponse={previewResponse}
|
||||
version={version}
|
||||
articleState={articleState}
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows an image in preview if one exists', () => {
|
||||
const container = shallow(
|
||||
<BodyPreview
|
||||
previewResponse={previewResponse}
|
||||
version={version}
|
||||
articleState={articleState}
|
||||
/>,
|
||||
);
|
||||
expect(container.find('.articleform__mainimagepreview').exists()).toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders properly without an image', () => {
|
||||
articleState.mainImage = null;
|
||||
const tree = render(
|
||||
<BodyPreview
|
||||
previewResponse={previewResponse}
|
||||
version={version}
|
||||
articleState={articleState}
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not show an image in preview if one does not exist', () => {
|
||||
articleState.mainImage = null;
|
||||
const container = shallow(
|
||||
<BodyPreview
|
||||
previewResponse={previewResponse}
|
||||
version={version}
|
||||
articleState={articleState}
|
||||
/>,
|
||||
);
|
||||
expect(container.find('.articleform__mainimagepreview').exists()).toEqual(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import Description from '../description';
|
||||
|
||||
describe('<Description />', () => {
|
||||
it('should render the description', () => {
|
||||
const context = render(
|
||||
<Description
|
||||
defaultValue="Some description"
|
||||
onChange={() => {
|
||||
return 'onChange';
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(context).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import Errors from '../errors';
|
||||
|
||||
const errorsList = [
|
||||
'Error 1',
|
||||
'Error 2'
|
||||
];
|
||||
|
||||
describe('<Errors />', () => {
|
||||
it('renders properly', () => {
|
||||
const tree = render(<Errors errorsList={errorsList} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { shallow } from 'preact-render-spy';
|
||||
import MainImage from '../mainImage';
|
||||
|
||||
describe('<MainImage />', () => {
|
||||
const editHandler = jest.fn();
|
||||
|
||||
it('renders properly', () => {
|
||||
const tree = render(<MainImage mainImage='http://lorempixel.com/400/200/' onEdit={editHandler} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('fires onEdit when clicked', () => {
|
||||
const container = shallow(<MainImage mainImage='http://lorempixel.com/400/200/' onEdit={editHandler} />);
|
||||
container.find('.articleform__mainimage').simulate('click');
|
||||
expect(editHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import Notice from '../notice';
|
||||
|
||||
describe('<notice />', () => {
|
||||
it('renders properly when published with a first version', () => {
|
||||
const tree = render(<Notice published version='v1' />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders properly when published with a second version', () => {
|
||||
const tree = render(<Notice published version='v2' />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders properly when unpublished without a version', () => {
|
||||
const tree = render(<Notice version='' />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders properly when unpublished with a version', () => {
|
||||
const tree = render(<Notice version='v2' />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
import { h, render as preactRender } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { shallow } from 'preact-render-spy';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import fetch from 'jest-fetch-mock';
|
||||
import Tags from '../../../shared/components/tags';
|
||||
|
||||
/* global globalThis */
|
||||
|
||||
const sampleResponse = JSON.stringify({
|
||||
result: [
|
||||
{
|
||||
name: 'git',
|
||||
hotness_score: 0,
|
||||
supported: true,
|
||||
short_summary: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe('<Tags />', () => {
|
||||
beforeAll(() => {
|
||||
const csrfToken = 'this-is-a-csrf-token';
|
||||
|
||||
globalThis.fetch = fetch;
|
||||
globalThis.getCsrfToken = async () => csrfToken;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete globalThis.fetch;
|
||||
delete globalThis.getCsrfToken;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const doc = new JSDOM('<!doctype html><html><body></body></html>');
|
||||
global.document = doc;
|
||||
global.window = doc.defaultView;
|
||||
fetch.mockResponse(sampleResponse);
|
||||
});
|
||||
|
||||
it('renders properly', () => {
|
||||
const tree = render(
|
||||
<Tags
|
||||
defaultValue=""
|
||||
onInput={jest.fn()}
|
||||
classPrefix="articleform"
|
||||
maxTags={4}
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows tags as you search', () => {
|
||||
const context = shallow(
|
||||
<Tags
|
||||
defaultValue=""
|
||||
onInput={jest.fn()}
|
||||
classPrefix="articleform"
|
||||
maxTags={4}
|
||||
/>,
|
||||
);
|
||||
const component = context.component();
|
||||
|
||||
return component
|
||||
.handleInput({ target: { value: 'gi', selectionStart: 2 } })
|
||||
.then(() => {
|
||||
expect(context.state()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('skips the click handler if className is articleform__tagsoptionrulesbutton', () => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const component = preactRender(
|
||||
<Tags
|
||||
defaultValue=""
|
||||
onInput={jest.fn()}
|
||||
classPrefix="articleform"
|
||||
maxTags={4}
|
||||
/>,
|
||||
document.body,
|
||||
document.body.firstElementChild,
|
||||
)._component;
|
||||
|
||||
component.handleTagClick({
|
||||
target: { className: 'articleform__tagsoptionrulesbutton' },
|
||||
});
|
||||
expect(component.state).toMatchSnapshot();
|
||||
expect(component.state.searchResults).toEqual([]);
|
||||
});
|
||||
|
||||
it('selects tag when you click on it', () => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const component = preactRender(
|
||||
<Tags
|
||||
defaultValue=""
|
||||
onInput={jest.fn()}
|
||||
classPrefix="articleform"
|
||||
maxTags={4}
|
||||
/>,
|
||||
document.body,
|
||||
document.body.firstElementChild,
|
||||
)._component;
|
||||
|
||||
component.handleTagClick({
|
||||
target: {},
|
||||
currentTarget: { dataset: { content: 'git' } },
|
||||
});
|
||||
expect(component.state).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('replaces tag when editing', () => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const component = preactRender(
|
||||
<Tags
|
||||
defaultValue=""
|
||||
onInput={jest.fn()}
|
||||
classPrefix="articleform"
|
||||
maxTags={4}
|
||||
/>,
|
||||
document.body,
|
||||
document.body.firstElementChild,
|
||||
)._component;
|
||||
|
||||
const input = document.getElementById('tag-input');
|
||||
input.value = 'java,javascript,linux';
|
||||
input.selectionStart = 2;
|
||||
|
||||
component.handleTagClick({
|
||||
target: {},
|
||||
currentTarget: { dataset: { content: 'git' } },
|
||||
});
|
||||
expect(component.state).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows tags when editing', () => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const component = preactRender(
|
||||
<Tags
|
||||
defaultValue=""
|
||||
onInput={jest.fn()}
|
||||
classPrefix="articleform"
|
||||
maxTags={4}
|
||||
/>,
|
||||
document.body,
|
||||
document.body.firstElementChild,
|
||||
)._component;
|
||||
|
||||
return component
|
||||
.handleInput({
|
||||
target: { value: 'gi, javascript, linux', selectionStart: 2 },
|
||||
})
|
||||
.then(() => {
|
||||
expect(component.state).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('only allows 4 tags', () => {
|
||||
const component = shallow(
|
||||
<Tags
|
||||
defaultValue=""
|
||||
onInput={jest.fn()}
|
||||
classPrefix="articleform"
|
||||
maxTags={4}
|
||||
/>,
|
||||
);
|
||||
|
||||
component.simulate('input', {
|
||||
target: { value: 'java, javascript, linux, productivity' },
|
||||
});
|
||||
|
||||
component.simulate('keydown', { keyCode: 188, preventDefault: jest.fn() });
|
||||
expect(component.state()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const BodyMarkdown = ({ onChange, defaultValue }) => (
|
||||
<textarea
|
||||
className="articleform__body"
|
||||
id="article_body_markdown"
|
||||
placeholder="Body Markdown"
|
||||
value={defaultValue}
|
||||
onInput={onChange}
|
||||
name="body_markdown"
|
||||
/>
|
||||
);
|
||||
|
||||
BodyMarkdown.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
defaultValue: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default BodyMarkdown;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Description = ({ onChange, defaultValue }) => (
|
||||
<textarea
|
||||
className="articleform__description"
|
||||
type="text"
|
||||
placeholder="description"
|
||||
name="description"
|
||||
value={defaultValue}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
Description.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
defaultValue: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Description;
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Errors = ({ errorsList }) => (
|
||||
<div className="articleform__errors">
|
||||
<h2>
|
||||
<span role="img" aria-label="face screaming in fear">
|
||||
😱
|
||||
</span>
|
||||
Heads up:
|
||||
</h2>
|
||||
<ul>
|
||||
{Object.keys(errorsList).map(key => {
|
||||
return (
|
||||
<li>
|
||||
{key}
|
||||
:
|
||||
{errorsList[key]}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
Errors.propTypes = {
|
||||
errorsList: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
export default Errors;
|
||||
|
|
@ -1,258 +0,0 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import linkCopyIcon from '../../../assets/images/content-copy.svg';
|
||||
import { generateMainImage } from '../actions';
|
||||
import { validateFileInputs } from '../../packs/validateFileInputs';
|
||||
|
||||
export default class ImageManagement extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
insertionImageUrls: [],
|
||||
uploadError: false,
|
||||
uploadErrorMessage: null,
|
||||
};
|
||||
}
|
||||
|
||||
handleMainImageUpload = e => {
|
||||
e.preventDefault();
|
||||
|
||||
this.clearUploadError();
|
||||
const validFileInputs = validateFileInputs();
|
||||
|
||||
if (validFileInputs) {
|
||||
const payload = { image: e.target.files, wrap_cloudinary: true };
|
||||
const { onMainImageUrlChange } = this.props;
|
||||
|
||||
generateMainImage(payload, onMainImageUrlChange, this.onUploadError);
|
||||
}
|
||||
};
|
||||
|
||||
handleInsertionImageUpload = e => {
|
||||
this.clearUploadError();
|
||||
|
||||
const validFileInputs = validateFileInputs();
|
||||
|
||||
if (validFileInputs) {
|
||||
const payload = { image: e.target.files };
|
||||
generateMainImage(
|
||||
payload,
|
||||
this.handleInsertImageUploadSuccess,
|
||||
this.onUploadError,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleInsertImageUploadSuccess = response => {
|
||||
this.setState({
|
||||
insertionImageUrls: response.links,
|
||||
});
|
||||
};
|
||||
|
||||
triggerMainImageRemoval = e => {
|
||||
e.preventDefault();
|
||||
|
||||
const { onMainImageUrlChange } = this.props;
|
||||
|
||||
onMainImageUrlChange({
|
||||
links: [],
|
||||
});
|
||||
};
|
||||
|
||||
clearUploadError = () => {
|
||||
this.setState({
|
||||
uploadError: false,
|
||||
uploadErrorMessage: null,
|
||||
});
|
||||
};
|
||||
|
||||
onUploadError = error => {
|
||||
this.setState({
|
||||
insertionImageUrls: [],
|
||||
uploadError: true,
|
||||
uploadErrorMessage: error.message,
|
||||
});
|
||||
};
|
||||
|
||||
execCopyText = () => {
|
||||
this.imageMarkdownInput.setSelectionRange(
|
||||
0,
|
||||
this.imageMarkdownInput.value.length,
|
||||
);
|
||||
document.execCommand('copy');
|
||||
this.imageMarkdownAnnouncer.hidden = false;
|
||||
}
|
||||
|
||||
copyText = () => {
|
||||
this.imageMarkdownAnnouncer = document.getElementById(
|
||||
'image-markdown-copy-link-announcer',
|
||||
);
|
||||
this.imageMarkdownInput = document.getElementById(
|
||||
'image-markdown-copy-link-input',
|
||||
);
|
||||
|
||||
const isNativeAndroid =
|
||||
navigator.userAgent === 'DEV-Native-android' &&
|
||||
typeof AndroidBridge !== "undefined" &&
|
||||
AndroidBridge !== null;
|
||||
|
||||
const isClipboardSupported =
|
||||
typeof navigator.clipboard !== "undefined" &&
|
||||
navigator.clipboard !== null;
|
||||
|
||||
if (isNativeAndroid) {
|
||||
AndroidBridge.copyToClipboard(this.imageMarkdownInput.value);
|
||||
this.imageMarkdownAnnouncer.hidden = false;
|
||||
} else if (isClipboardSupported) {
|
||||
navigator.clipboard.writeText(this.imageMarkdownInput.value)
|
||||
.then(() => {
|
||||
this.imageMarkdownAnnouncer.hidden = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.execCopyText();
|
||||
});
|
||||
} else {
|
||||
this.execCopyText();
|
||||
}
|
||||
};
|
||||
|
||||
linksToMarkdownForm = imageLinks => {
|
||||
return imageLinks.map(imageLink => ``).join('\n');
|
||||
};
|
||||
|
||||
linksToDirectForm = imageLinks => {
|
||||
return imageLinks.join('\n');
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onExit, mainImage, version } = this.props;
|
||||
const { insertionImageUrls, uploadError, uploadErrorMessage } = this.state;
|
||||
let mainImageArea;
|
||||
|
||||
if (mainImage) {
|
||||
mainImageArea = (
|
||||
<div>
|
||||
<img src={mainImage} alt="main" />
|
||||
<button type="button" onClick={this.triggerMainImageRemoval}>
|
||||
Remove Cover Image
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
mainImageArea = (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={this.handleMainImageUpload}
|
||||
accept="image/*"
|
||||
data-max-file-size-mb="25"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let insertionImageArea;
|
||||
if (insertionImageUrls.length > 0) {
|
||||
insertionImageArea = (
|
||||
<div>
|
||||
<h3>Markdown Images:</h3>
|
||||
<clipboard-copy
|
||||
onClick={this.copyText}
|
||||
for="image-markdown-copy-link-input"
|
||||
aria-live="polite"
|
||||
aria-controls="image-markdown-copy-link-announcer"
|
||||
>
|
||||
<textarea
|
||||
id="image-markdown-copy-link-input"
|
||||
value={this.linksToMarkdownForm(insertionImageUrls)}
|
||||
/>
|
||||
<img
|
||||
id="image-markdown-copy-icon"
|
||||
src={linkCopyIcon}
|
||||
alt="Copy to Clipboard"
|
||||
/>
|
||||
<span id="image-markdown-copy-link-announcer" role="alert" hidden>
|
||||
Copied to Clipboard
|
||||
</span>
|
||||
</clipboard-copy>
|
||||
<h3>Direct URLs:</h3>
|
||||
<textarea
|
||||
id="image-direct-copy-link-input"
|
||||
value={this.linksToDirectForm(insertionImageUrls)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
insertionImageArea = (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={this.handleInsertionImageUpload}
|
||||
multiple
|
||||
accept="image/*"
|
||||
data-max-file-size-mb="25"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let imageOptions;
|
||||
if (version === 'v1') {
|
||||
imageOptions = (
|
||||
<div>
|
||||
<h2>Upload Images</h2>
|
||||
{insertionImageArea}
|
||||
<div>
|
||||
<p>
|
||||
<em>
|
||||
To add a cover image for the post, add
|
||||
<code>cover_image: direct_url_to_image.jpg</code>
|
||||
to the frontmatter
|
||||
</em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
imageOptions = (
|
||||
<div>
|
||||
<h2>Cover Image</h2>
|
||||
{mainImageArea}
|
||||
<h2>Body Images</h2>
|
||||
{insertionImageArea}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="articleform__overlay">
|
||||
<button
|
||||
type="button"
|
||||
className="articleform__exitbutton"
|
||||
data-content="exit"
|
||||
onClick={onExit}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{uploadError && (
|
||||
<span className="articleform__uploaderror">{uploadErrorMessage}</span>
|
||||
)}
|
||||
{imageOptions}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="articleform__donebutton"
|
||||
onClick={onExit}
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImageManagement.propTypes = {
|
||||
onExit: PropTypes.func.isRequired,
|
||||
onMainImageUrlChange: PropTypes.func.isRequired,
|
||||
mainImage: PropTypes.string.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
};
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const MainImage = ({ mainImage, onEdit }) => (
|
||||
<div
|
||||
className="articleform__mainimage"
|
||||
role="presentation"
|
||||
onClick={onEdit}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
onEdit(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img src={mainImage} alt="" />
|
||||
</div>
|
||||
);
|
||||
|
||||
MainImage.defaultProps = {
|
||||
onEdit: () => {},
|
||||
};
|
||||
|
||||
MainImage.propTypes = {
|
||||
mainImage: PropTypes.string.isRequired,
|
||||
onEdit: PropTypes.func,
|
||||
};
|
||||
|
||||
export default MainImage;
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const TextField = ({ label, id, value, onKeyUp }) => {
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor={id}>{label}</label>
|
||||
<input type="text" value={value} name={id} onKeyUp={onKeyUp} id={id} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TextField.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onKeyUp: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default class MoreConfig extends Component {
|
||||
handleSeriesButtonClick = e => {
|
||||
e.preventDefault();
|
||||
const { onConfigChange } = this.props;
|
||||
onConfigChange(e);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
onExit,
|
||||
passedData: {
|
||||
published = false,
|
||||
allSeries = [],
|
||||
canonicalUrl = '',
|
||||
series = '',
|
||||
},
|
||||
onSaveDraft,
|
||||
onConfigChange,
|
||||
} = this.props;
|
||||
let publishedField = '';
|
||||
let seriesTip = (
|
||||
<small>
|
||||
Will this post be part of a series? Give the series a unique name.
|
||||
(Series visible once it has multiple posts)
|
||||
</small>
|
||||
);
|
||||
if (allSeries.length > 0) {
|
||||
const seriesNames = allSeries.map(name => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
name="series"
|
||||
onClick={onConfigChange}
|
||||
value={name}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
seriesTip = (
|
||||
<small>
|
||||
Existing series:
|
||||
{seriesNames}
|
||||
</small>
|
||||
);
|
||||
}
|
||||
if (published) {
|
||||
publishedField = (
|
||||
<div>
|
||||
<h4>Danger Zone</h4>
|
||||
<button type="button" onClick={onSaveDraft}>
|
||||
Unpublish Post
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="articleform__overlay">
|
||||
<h3>Additional Config/Settings</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="articleform__exitbutton"
|
||||
data-content="exit"
|
||||
onClick={onExit}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{TextField({
|
||||
label: 'Canonical URL',
|
||||
id: 'canonicalUrl',
|
||||
value: canonicalUrl,
|
||||
onKeyUp: onConfigChange,
|
||||
})}
|
||||
<small>
|
||||
Change meta tag
|
||||
{' '}
|
||||
<code>canonical_url</code>
|
||||
{' '}
|
||||
if this post was first
|
||||
published elsewhere (like your own blog)
|
||||
</small>
|
||||
{TextField({
|
||||
label: 'Series Name',
|
||||
id: 'series',
|
||||
value: series,
|
||||
onKeyUp: onConfigChange,
|
||||
})}
|
||||
{seriesTip}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="articleform__donebutton"
|
||||
onClick={onExit}
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
{publishedField}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MoreConfig.propTypes = {
|
||||
onExit: PropTypes.func.isRequired,
|
||||
passedData: PropTypes.shape({
|
||||
published: PropTypes.bool.isRequired,
|
||||
allSeries: PropTypes.array.isRequired,
|
||||
canonicalUrl: PropTypes.string.isRequired,
|
||||
series: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
onSaveDraft: PropTypes.func.isRequired,
|
||||
onConfigChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Notice = ({ published, version }) => (
|
||||
<div
|
||||
className={`articleform__notice articleform__notice--${
|
||||
published ? 'publishing' : 'draft'
|
||||
}`}
|
||||
>
|
||||
{(published && version === 'v2') ? 'Publishing...' : `Saving ${ version === 'v2' ? 'Draft' : ''}...`}
|
||||
</div>
|
||||
);
|
||||
|
||||
Notice.propTypes = {
|
||||
published: PropTypes.bool.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Notice;
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const PublishToggle = ({
|
||||
previewShowing,
|
||||
onPreview,
|
||||
onSaveDraft,
|
||||
onPublish,
|
||||
onHelp,
|
||||
published,
|
||||
helpShowing,
|
||||
edited,
|
||||
version,
|
||||
onClearChanges,
|
||||
}) => (
|
||||
<div className="articleform__buttons">
|
||||
<button
|
||||
onClick={onHelp}
|
||||
type="button"
|
||||
className={
|
||||
helpShowing
|
||||
? 'articleform__buttons--small active'
|
||||
: 'articleform__buttons--small inactive'
|
||||
}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
<button
|
||||
onClick={onPreview}
|
||||
type="button"
|
||||
className={previewShowing ? 'active previewbutt' : 'inactive previewbutt'}
|
||||
>
|
||||
{previewShowing ? 'EDIT' : 'PREVIEW'}
|
||||
</button>
|
||||
{published || version === 'v1' ? (
|
||||
''
|
||||
) : (
|
||||
<button onClick={onSaveDraft} type="button">
|
||||
SAVE DRAFT
|
||||
</button>
|
||||
)}
|
||||
<span>
|
||||
<p style={!edited && { visibility: 'hidden' }}>
|
||||
New Changes (
|
||||
<button onClick={onClearChanges} className="clear-button" type="button">
|
||||
clear
|
||||
</button>
|
||||
)
|
||||
</p>
|
||||
<button
|
||||
onClick={onPublish}
|
||||
className="articleform__buttons--publish"
|
||||
type="button"
|
||||
>
|
||||
{published || version === 'v1' ? 'SAVE CHANGES' : 'PUBLISH'}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
PublishToggle.propTypes = {
|
||||
previewShowing: PropTypes.bool.isRequired,
|
||||
onPreview: PropTypes.func.isRequired,
|
||||
onSaveDraft: PropTypes.func.isRequired,
|
||||
onPublish: PropTypes.func.isRequired,
|
||||
onHelp: PropTypes.func.isRequired,
|
||||
published: PropTypes.bool.isRequired,
|
||||
helpShowing: PropTypes.bool.isRequired,
|
||||
edited: PropTypes.bool.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
onClearChanges: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default PublishToggle;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import Textarea from 'preact-textarea-autosize';
|
||||
|
||||
const Title = ({ onChange, defaultValue, onKeyDown }) => (
|
||||
<Textarea
|
||||
className="articleform__title"
|
||||
type="text"
|
||||
id="article-form-title"
|
||||
placeholder="Title"
|
||||
autoComplete="off"
|
||||
value={defaultValue}
|
||||
onInput={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
);
|
||||
|
||||
Title.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
defaultValue: PropTypes.string.isRequired,
|
||||
onKeyDown: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Title;
|
||||
|
|
@ -57,6 +57,7 @@ export const Button = ({
|
|||
onFocus,
|
||||
onBlur,
|
||||
tabIndex,
|
||||
title,
|
||||
}) => {
|
||||
const ComponentName = tagName;
|
||||
const Icon = icon;
|
||||
|
|
@ -83,6 +84,7 @@ export const Button = ({
|
|||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
tabIndex={tabIndex}
|
||||
title={title}
|
||||
{...otherProps}
|
||||
>
|
||||
{contentType !== 'text' && contentType !== 'icon-right' && Icon && (
|
||||
|
|
@ -114,6 +116,7 @@ Button.defaultProps = {
|
|||
onFocus: undefined,
|
||||
onBlur: undefined,
|
||||
tabIndex: undefined,
|
||||
title: undefined
|
||||
};
|
||||
|
||||
Button.propTypes = {
|
||||
|
|
@ -150,4 +153,5 @@ Button.propTypes = {
|
|||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
tabIndex: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { h, Component } from 'preact';
|
||||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
import { defaultChildrenPropTypes } from '../../common-prop-types';
|
||||
|
|
@ -17,68 +17,62 @@ function getAdditionalClassNames({ size, className }) {
|
|||
return additionalClassNames;
|
||||
}
|
||||
|
||||
export class Modal extends Component {
|
||||
state = {
|
||||
visible: true,
|
||||
};
|
||||
export const Modal = ({
|
||||
children,
|
||||
size = 'default',
|
||||
className,
|
||||
title,
|
||||
overlay,
|
||||
onClose,
|
||||
}) => {
|
||||
const Icon = () => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="crayons-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-labelledby="714d29e78a3867c79b07f310e075e824"
|
||||
>
|
||||
<title id="714d29e78a3867c79b07f310e075e824">Close</title>
|
||||
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
render() {
|
||||
const { visible } = this.state;
|
||||
const {
|
||||
children,
|
||||
size = 'default',
|
||||
className,
|
||||
title,
|
||||
overlay,
|
||||
} = this.props;
|
||||
|
||||
const Icon = () => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="crayons-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
return visible ? (
|
||||
<div
|
||||
className={`crayons-modal${getAdditionalClassNames({
|
||||
size,
|
||||
className,
|
||||
})}`}
|
||||
>
|
||||
<div className="crayons-modal__box">
|
||||
{title.length > 0 && title && (
|
||||
<div className="crayons-modal__box__header">
|
||||
<h2>{title}</h2>
|
||||
<Button
|
||||
icon={Icon}
|
||||
variant="ghost"
|
||||
contentType="icon"
|
||||
title="Close"
|
||||
onClick={(_event) => {
|
||||
this.setState({ visible: !visible });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="crayons-modal__box__body">{children}</div>
|
||||
</div>
|
||||
{overlay && <div className="crayons-modal__overlay" />}
|
||||
return (
|
||||
<div
|
||||
className={`crayons-modal${getAdditionalClassNames({
|
||||
size,
|
||||
className,
|
||||
})}`}
|
||||
>
|
||||
<div className="crayons-modal__box">
|
||||
{title.length > 0 && title && (
|
||||
<div className="crayons-modal__box__header">
|
||||
<h2>{title}</h2>
|
||||
<Button
|
||||
icon={Icon}
|
||||
variant="ghost"
|
||||
contentType="icon"
|
||||
title="Close"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="crayons-modal__box__body">{children}</div>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
{overlay && <div className="crayons-modal__overlay" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.displayName = 'Modal';
|
||||
|
||||
Modal.defaultProps = {
|
||||
className: undefined,
|
||||
overlay: true,
|
||||
onClose: undefined,
|
||||
};
|
||||
|
||||
Modal.propTypes = {
|
||||
|
|
@ -86,5 +80,6 @@ Modal.propTypes = {
|
|||
className: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
overlay: PropTypes.bool,
|
||||
onClose: PropTypes.func,
|
||||
size: PropTypes.oneOf(['default', 's', 'm']).isRequired,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import { h } from 'preact';
|
||||
import { withKnobs, text, boolean, select } from '@storybook/addon-knobs/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { Modal, Button } from '@crayons';
|
||||
|
||||
import '../../storybook-utilities/designSystem.scss';
|
||||
|
||||
const commonProps = {
|
||||
onClose: action('close fired'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: '3_Components/Modals',
|
||||
decorator: [withKnobs],
|
||||
|
|
@ -26,6 +31,7 @@ export const Default = () => (
|
|||
className={text('className')}
|
||||
title={text('title', 'This is my Modal title')}
|
||||
overlay={boolean('overlay', true)}
|
||||
{...commonProps}
|
||||
>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { h } from 'preact';
|
|||
import PropTypes from 'prop-types';
|
||||
import { organizationPropType } from '../common-prop-types';
|
||||
|
||||
const orgOptions = (organizations, organizationId) => {
|
||||
const orgOptions = (organizations, organizationId, emptyLabel) => {
|
||||
const orgs = organizations.map((organization) => {
|
||||
if (organizationId === organization.id) {
|
||||
return (
|
||||
|
|
@ -16,10 +16,10 @@ const orgOptions = (organizations, organizationId) => {
|
|||
const nullOrgOption =
|
||||
organizationId === null ? (
|
||||
<option value="" selected>
|
||||
None
|
||||
{emptyLabel}
|
||||
</option>
|
||||
) : (
|
||||
<option value="">None</option>
|
||||
<option value="">{emptyLabel}</option>
|
||||
);
|
||||
|
||||
return [nullOrgOption, ...orgs];
|
||||
|
|
@ -28,18 +28,26 @@ const orgOptions = (organizations, organizationId) => {
|
|||
export const OrganizationPicker = ({
|
||||
name,
|
||||
id,
|
||||
className,
|
||||
organizations,
|
||||
organizationId,
|
||||
onToggle,
|
||||
emptyLabel
|
||||
}) => (
|
||||
<select name={name} id={id} onBlur={onToggle}>
|
||||
{orgOptions(organizations, organizationId)}
|
||||
<select name={name} id={id} className={className} onBlur={onToggle}>
|
||||
{orgOptions(organizations, organizationId, emptyLabel)}
|
||||
</select>
|
||||
);
|
||||
|
||||
OrganizationPicker.defaultProps = {
|
||||
emptyLabel: "None"
|
||||
}
|
||||
|
||||
OrganizationPicker.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
className: PropTypes.string.isRequired,
|
||||
emptyLabel: PropTypes.string,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
organizationId: PropTypes.number.isRequired,
|
||||
organizations: PropTypes.arrayOf(organizationPropType).isRequired,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ function loadForm() {
|
|||
window.currentUser = currentUser;
|
||||
window.csrfToken = csrfToken;
|
||||
|
||||
const root = document.getElementById('article-form');
|
||||
const root = document.getElementById('js-article-form');
|
||||
const { article, organizations, version } = root.dataset;
|
||||
|
||||
render(
|
||||
|
|
|
|||
|
|
@ -368,7 +368,14 @@ class Tags extends Component {
|
|||
render() {
|
||||
let searchResultsHTML = '';
|
||||
const { searchResults, selectedIndex, showingRulesForTag } = this.state;
|
||||
const { classPrefix, defaultValue, maxTags, listing } = this.props;
|
||||
const {
|
||||
classPrefix,
|
||||
defaultValue,
|
||||
maxTags,
|
||||
listing,
|
||||
fieldClassName,
|
||||
onFocus,
|
||||
} = this.props;
|
||||
const { activeElement } = document;
|
||||
const searchResultsRows = searchResults.map((tag, index) => (
|
||||
<div
|
||||
|
|
@ -381,7 +388,7 @@ class Tags extends Component {
|
|||
onKeyDown={this.handleTagEnter}
|
||||
data-content={tag.name}
|
||||
>
|
||||
{tag.name}
|
||||
<span className={`${classPrefix}__tagname`}>{tag.name}</span>
|
||||
{tag.rules_html && tag.rules_html.length > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -432,15 +439,15 @@ class Tags extends Component {
|
|||
this.textArea = t;
|
||||
return this.textArea;
|
||||
}}
|
||||
className={`${classPrefix}__tags`}
|
||||
className={`${`${fieldClassName} ${classPrefix}`}__tags`}
|
||||
name="listing[tag_list]"
|
||||
placeholder={`${maxTags} tags max, comma separated, no spaces or special characters`}
|
||||
placeholder={`Add up to ${maxTags} tags...`}
|
||||
autoComplete="off"
|
||||
value={defaultValue}
|
||||
onInput={this.handleInput}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onBlur={this.handleFocusChange}
|
||||
onFocus={this.handleFocusChange}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
{searchResultsHTML}
|
||||
</div>
|
||||
|
|
@ -453,8 +460,10 @@ Tags.propTypes = {
|
|||
onInput: PropTypes.func.isRequired,
|
||||
maxTags: PropTypes.number.isRequired,
|
||||
classPrefix: PropTypes.string.isRequired,
|
||||
fieldClassName: PropTypes.string.isRequired,
|
||||
listing: PropTypes.string.isRequired,
|
||||
category: PropTypes.string.isRequired,
|
||||
onFocus: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Tags;
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ module Articles
|
|||
|
||||
def user_editor_v1
|
||||
Article.new(
|
||||
body_markdown: "---\ntitle: \npublished: false\ndescription: \ntags: \n---\n\n",
|
||||
body_markdown: "---\ntitle: \npublished: false\ndescription: \ntags: \n//cover_image: https://direct_url_to_image.jpg\n---\n\n",
|
||||
processed_html: "",
|
||||
user_id: @user&.id,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,60 +1,101 @@
|
|||
<div class="articleformcontainer">
|
||||
<div class="articleform"
|
||||
id="article-form"
|
||||
data-article="<%= article.to_json(only: %i[id title cached_tag_list published body_markdown main_image organization_id canonical_url updated_at], methods: %i[series all_series]) %>"
|
||||
data-organizations="<%= organizations&.to_json(only: %i[id name bg_color_hex text_color_hex], methods: [:profile_image_90]) %>"
|
||||
data-version="<%= version %>">
|
||||
<form class="articleform__form articleform__form--<%= version %>">
|
||||
<% if organizations && organizations.size > 0 %>
|
||||
<div class="articleform__orgsettings">
|
||||
Publish under an organization:<select name="article[organization_id]" id="article_publish_under_org">
|
||||
<%= options_for_select({ "None" => "" }.merge(organizations.pluck(:name, :id).to_h), article.organization_id || "") %>
|
||||
<style>
|
||||
body {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
body > footer,
|
||||
#top-bar {
|
||||
display: none
|
||||
}
|
||||
|
||||
.universal-page-content-wrapper {
|
||||
overflow: unset;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="js-article-form"
|
||||
data-article="<%= article.to_json(only: %i[id title cached_tag_list published body_markdown main_image organization_id canonical_url updated_at], methods: %i[series all_series]) %>"
|
||||
data-organizations="<%= organizations&.to_json(only: %i[id name bg_color_hex text_color_hex], methods: [:profile_image_90]) %>"
|
||||
data-version="<%= version %>">
|
||||
<form class="crayons-article-form crayons-article-form--<%= version %>">
|
||||
|
||||
<div class="crayons-article-form__header">
|
||||
<a href="/" class="crayons-article-form__logo" aria-label="<%= community_name %> Home"><%= logo_svg %></a>
|
||||
|
||||
<div class="crayons-field__label">
|
||||
<span class="hidden s:inline-block">Write a new post</span>
|
||||
<% if organizations && organizations.size > 0 %>
|
||||
<select name="article[organization_id]" id="article_publish_under_org" class="crayons-select w-auto ml-2 mt-0">
|
||||
<%= options_for_select({ "Personal" => "" }.merge(organizations.pluck(:name, :id).to_h), article.organization_id || "") %>
|
||||
</select>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if version == "v2" %>
|
||||
<% if article.main_image.present? %>
|
||||
<div class="articleform__mainimage"><img src="<%= article.main_image %>"></div>
|
||||
<% end %>
|
||||
<input class="articleform__title articleform__titlepreview" type="text" value="<%= article.title %>" placeholder="Title" name="title" />
|
||||
<div class="articleform__detailfields">
|
||||
<div class="articleform__tagswrapper">
|
||||
<input type="text" class="articleform__tags" placeholder="4 tags max, comma separated, no spaces or special characters" name="tags" value="<%= article.cached_tag_list %>" />
|
||||
</div>
|
||||
<button class="articleform__detailsButton articleform__detailsButton--image"></button>
|
||||
<button class="articleform__detailsButton articleform__detailsButton--moreconfig"></button>
|
||||
</div>
|
||||
<% end %>
|
||||
<textarea class="articleform__body" placeholder="Body Markdown" name="body_markdown"><%= article.body_markdown %></textarea>
|
||||
<div>
|
||||
<button class="articleform__detailsButton articleform__detailsButton--image articleform__detailsButton--bottom"></button>
|
||||
<% if version == "v2" %>
|
||||
<button class="articleform__detailsButton articleform__detailsButton--moreconfig articleform__detailsButton--bottom"></button>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="articleform__buttons">
|
||||
<span style="visibility:hidden;">
|
||||
<p>New Changes <button class="clear-button">clear</button></p>
|
||||
<button class="articleform__buttons--publish">SAVE CHANGES</button>
|
||||
</span>
|
||||
|
||||
<div class="crayons-article-form__tabs crayons-tabs ml-auto">
|
||||
<button class="crayons-tabs__item crayons-tabs__item--current" type="button">Edit</button><button class="crayons-tabs__item " type="button">Preview</button>
|
||||
</div>
|
||||
|
||||
<div class="crayons-article-form__close">
|
||||
<a href="/" class="crayons-btn crayons-btn--ghost crayons-btn--icon" title="Close the editor">
|
||||
<%= inline_svg_tag("x.svg", aria: true, class: "crayons-icon", title: "Close") %>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
<div style="display:none">
|
||||
<% if version == "v2" %>
|
||||
<%= render "pages/editor_guide_text", version: "2" %>
|
||||
<% else %>
|
||||
<%= render "pages/editor_guide_text", version: "1" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="crayons-article-form__main">
|
||||
<div class="crayons-card crayons-article-form__main__content">
|
||||
<% if version == "v2" %>
|
||||
<div>
|
||||
<div class="crayons-article-form__cover" role="presentation">
|
||||
<% if article.main_image.present? %>
|
||||
<img src="<%= article.main_image %>" width="250" height="105" alt="Post cover" class="crayons-article-form__cover__image">
|
||||
<% end %>
|
||||
<button class="crayons-btn crayons-btn--outlined mr-2">
|
||||
<label for="cover-image-input">
|
||||
<% if article.main_image.present? %>Change<% else %>Add a cover image<% end %>
|
||||
</label>
|
||||
<input id="cover-image-input" type="file" accept="image/*" class="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer" data-max-file-size-mb="25">
|
||||
</button>
|
||||
</div>
|
||||
<div class="crayons-article-form__title">
|
||||
<textarea class="crayons-textfield crayons-textfield--ghost fs-3xl s:fs-4xl l:fs-5xl fw-bold s:fw-heavy lh-tight" type="text" id="article-form-title" placeholder="New post title here..." autocomplete="off" style="height: 60px; "><%= article.title %></textarea>
|
||||
</div>
|
||||
<div class="crayons-article-form__tagsfield">
|
||||
<input type="text" class="crayons-textfield crayons-textfield--ghost ff-accent" placeholder="Add up to 4 tags..." name="tags" value="<%= article.cached_tag_list %>" />
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="crayons-article-form__body">
|
||||
<div class="crayons-article-form__toolbar">
|
||||
<button class="crayons-btn crayons-btn--ghost crayons-btn--icon-left fw-normal" type="button">
|
||||
<%= (inline_svg_tag("image.svg", aria: true, class: "crayons-icon", title: "Image") + "Upload image").delete!("\n").html_safe %>
|
||||
<input type="file" id="image-upload-field" class="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer" multiple accept="image/*" data-max-file-size-mb="25" />
|
||||
</button>
|
||||
</div>
|
||||
<textarea class="crayons-article-form__body__field crayons-textfield crayons-textfield--ghost fs-l ff-accent" id="article_body_markdown" placeholder="Write your post content here..." name="body_markdown"><%= article.body_markdown %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="crayons-article-form__main_aside"></div>
|
||||
</div>
|
||||
|
||||
<div class="crayons-article-form__footer">
|
||||
<button class="crayons-btn mr-2" type="button">Publish</button>
|
||||
<button class="crayons-btn crayons-btn--secondary mr-2" type="button">Save draft</button>
|
||||
<div class="relative">
|
||||
<button class="crayons-btn crayons-btn--ghost crayons-btn--icon" type="button" title="Post options">
|
||||
<%= inline_svg_tag("cog.svg", aria: true, class: "crayons-icon", title: "Options") %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="hidden">
|
||||
<%= render "pages/editor_markdown_help" %>
|
||||
<%= render "pages/editor_liquid_help" %>
|
||||
<%= render "pages/editor_frontmatter_help" %>
|
||||
</div>
|
||||
|
||||
<script async>
|
||||
<%== RunkitTag.script %>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#footer-container {
|
||||
display: none
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
29
app/views/pages/_editor_frontmatter_help.html.erb
Normal file
29
app/views/pages/_editor_frontmatter_help.html.erb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<div id="editor-frontmatter-help">
|
||||
<div class="crayons-article">
|
||||
<div class="crayons-article__body">
|
||||
<div class="crayons-notice crayons-notice--info">
|
||||
<p><strong>We have two editor versions</strong>. If you'd prefer to edit title and tags etc. as separate fields, switch to the "rich + markdown" option in <a href="<%= user_settings_path(tab: :ux) %>">Settings » UX</a>. Otherwise, continue...</p>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<li>This editor is a markdown editor that uses <a href="https://jekyllrb.com/docs/frontmatter/">Jekyll front matter</a>.</li>
|
||||
<li>Most of the time, you can write inline HTML directly into your posts.</li>
|
||||
<li>We support native Liquid tags and created some fun custom ones, too! Trying embedding a Tweet or GitHub issue in your post.</li>
|
||||
<li>Links to unpublished posts are shareable for feedback/review.</li>
|
||||
<li>When you're ready to publish, set the published variable to <strong>true.</strong></li>
|
||||
</ul>
|
||||
|
||||
<h3>Front Matter</h3>
|
||||
<p>Custom variables set for each post, located between the triple-dashed lines in your editor. Here is a list of possibilities:</p>
|
||||
<ul>
|
||||
<li><strong>title:</strong> the title of your article</li>
|
||||
<li><strong>published:</strong> boolean that determines whether or not your article is published</li>
|
||||
<li><strong>description:</strong> description area in Twitter cards and open graph cards</li>
|
||||
<li><strong>tags:</strong> max of four tags, needs to be comma-separated</li>
|
||||
<li><strong>canonical_url:</strong> link for the canonical version of the content</li>
|
||||
<li><strong>cover_image:</strong> cover image for post, accepts a URL. <br>The best size is 1000 x 420.</li>
|
||||
<li><strong>series:</strong> post series name.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -277,7 +277,7 @@
|
|||
<h3><strong>Next Tech Embed</strong></h3>
|
||||
<p>All you need is the share URL for your sandbox. You can get the share URL by clicking
|
||||
the "Share" button in the top right when the sandbox is open.</p>
|
||||
<p><img src="https://thepracticaldev.s3.amazonaws.com/i/r449xp8cay3383i139qv.png" /></p>
|
||||
<p><img src="https://thepracticaldev.s3.amazonaws.com/i/r449xp8cay3383i139qv.png" alt="Share Replit sandbox" /></p>
|
||||
<code>{% nexttech https://nt.dev/s/6ba1fffbd09e %}</code>
|
||||
<h3><strong>Instagram Embed</strong></h3>
|
||||
<p>All you need is the Instagram post <code>id</code> from the URL:</p>
|
||||
|
|
|
|||
286
app/views/pages/_editor_liquid_help.html.erb
Normal file
286
app/views/pages/_editor_liquid_help.html.erb
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
<div id="editor-liquid-help">
|
||||
<div class="crayons-article">
|
||||
<div class="crayons-article__body">
|
||||
<p>We support native <a href="https://shopify.github.io/liquid/" target="_blank" rel="noopener">Liquid tags</a> in our editor, but have created our own custom tags listed below:</p>
|
||||
<h3><%= community_name %> Article/Post Embed</h3>
|
||||
<p>All you need is the full link of the article:</p>
|
||||
<code>{% link https://dev.to/kazz/boost-your-productivity-using-markdown-1be %}</code>
|
||||
<p>You can also use the slug like this:</p>
|
||||
<code>{% link kazz/boost-your-productivity-using-markdown-1be %}</code>
|
||||
<p>You can also use the alias post instead of link like this:</p>
|
||||
<code>{% post https://dev.to/kazz/boost-your-productivity-using-markdown-1be %}</code>
|
||||
<p>or this:</p>
|
||||
<code>{% post kazz/boost-your-productivity-using-markdown-1be %}</code>
|
||||
<h3><%= community_name %> User Embed</h3>
|
||||
<p>All you need is the <%= community_name %> username:</p>
|
||||
<code>{% user jess %}</code>
|
||||
<h3><%= community_name %> Tag Embed</h3>
|
||||
<p>All you need is the tag name:</p>
|
||||
<code>{% tag git %}</code>
|
||||
<h3><%= community_name %> Comment Embed</h3>
|
||||
<p>All you need is the
|
||||
<code>ID</code> at the end of a comment URL. To get the comment link, click either the timestamp or the menu button in the top right corner on a comment and then click "Permalink". Here's an example:
|
||||
</p>
|
||||
<code>{% devcomment 2d1a %}</code>
|
||||
<h3><%= community_name %> Podcast Episode Embed</h3>
|
||||
<p>All you need is the full link of the podcast episode:</p>
|
||||
<code>{% podcast https://dev.to/basecspodcast/s2e2--queues-irl %}</code>
|
||||
<h3><%= community_name %> Listing Embed</h3>
|
||||
<p>All you need is the full link of the listing:</p>
|
||||
<code>{% listing https://dev.to/listings/collabs/dev-is-open-source-823 %}</code>
|
||||
<p>You can also use the category and slug like this:</p>
|
||||
<code>{% listing collabs/dev-is-open-source-823 %}</code>
|
||||
<p>Note: Expired listings will raise an error. Make sure the listing is published or recently bumped.</p>
|
||||
<h3>Twitter Embed</h3>
|
||||
<p>Using the Twitter Liquid tag will allow the tweet to pre-render from the server, providing your reader with a better experience. All you need is the tweet
|
||||
<code>id</code> from the url.</p>
|
||||
<code>{% twitter 834439977220112384 %}</code>
|
||||
<h3>Glitch embed</h3>
|
||||
<p>All you need is the Glitch project slug</p>
|
||||
<code>{% glitch earthy-course %}</code>
|
||||
<p>There are several
|
||||
<a href="https://glitch.com/help/how-can-i-customize-a-glitch-app-embed/">optional attributes</a> you can use in your tag, just add them after the id, separated by spaces.
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>app</code> – Shows the app preview without the code.<br>
|
||||
<code>{% glitch earthy-course app %}</code>
|
||||
</li>
|
||||
<li><code>code</code> – Shows the code without the app preview.<br>
|
||||
<code>{% glitch earthy-course code %}</code></li>
|
||||
<li><code>preview-first</code> – Swap panes: Show the app preview on the left and the code on the right.<br>
|
||||
<code>{% glitch earthy-course preview-first %}</code></li>
|
||||
<li><code>no-attribution</code> – Hides the avatar of the creator(s).<br>
|
||||
<code>{% glitch earthy-course no-attribution %}</code></li>
|
||||
<li><code>no-files</code> – Hides the file browser.<br>
|
||||
<code>{% glitch earthy-course no-files %}</code></li>
|
||||
<li><code>file</code> – Lets you choose which file to display in the code panel. Defaults to index.html.<br>
|
||||
<code>{% glitch earthy-course file=script.js %}</code></li>
|
||||
</ul>
|
||||
<h3>GitHub Repo Embed</h3>
|
||||
<p>All you need is the GitHub username and repo:</p>
|
||||
<code>{% github thepracticaldev/dev.to %}</code>
|
||||
<dl>
|
||||
<dt><code>no-readme</code></dt>
|
||||
<dd>
|
||||
You can add a no-readme option to your GitHub tag to hide the readme file from the preview.<br>
|
||||
<code>{% github thepracticaldev/dev.to no-readme %}</code>
|
||||
</dd>
|
||||
</dl>
|
||||
<h3>GitHub Issue, Pull request or Comment Embed</h3>
|
||||
<p>All you need is the GitHub issue, PR or comment URL:</p>
|
||||
<code>{% github https://github.com/thepracticaldev/dev.to/issues/9 %}</code>
|
||||
<h3>GitHub Gist Embed</h3>
|
||||
<p>All you need is the gist link:</p>
|
||||
<code>
|
||||
{% gist https://gist.github.com/CristinaSolana/1885435 %}
|
||||
</code>
|
||||
<dl>
|
||||
<dt><code>Single File Embed</code></dt>
|
||||
<dd>
|
||||
<p>You can choose to embed a single gist file. <br>
|
||||
<code>{% gist https://gist.github.com/CristinaSolana/1885435 file=gistfile1.md %}</code></p>
|
||||
</dd>
|
||||
<dt><code>Specific Version Embed</code></dt>
|
||||
<dd>
|
||||
<p>You can choose to embed a specific version of a gist file. All you need
|
||||
the link and the commit hash for that specific version. <br>
|
||||
The format is <code>{% gist [gist-link]/[commit-hash] %}</code> <br>
|
||||
e.g. <br>
|
||||
<code>{% gist https://gist.github.com/suntong/3a31faf8129d3d7a380122d5a6d48ff6/f77d01e82defbf736ebf4879a812cf9c916a9252 %}</code></p>
|
||||
</dd>
|
||||
<dt><code>Specific Version File Embed</code></dt>
|
||||
<dd>
|
||||
<p>You can choose to embed a specific version of a gist file. All you need
|
||||
the link, the filename and the commit hash for that specific version . <br>
|
||||
The format is <code>{% gist [gist-link]/[commit-hash] file=[filename] %}</code> <br>
|
||||
e.g. <br>
|
||||
<code>
|
||||
{% gist https://gist.github.com/suntong/3a31faf8129d3d7a380122d5a6d48ff6/f77d01e82defbf736ebf4879a812cf9c916a9252 file=Images.tmpl %}
|
||||
</code></p>
|
||||
</dd>
|
||||
</dl>
|
||||
<h3>GitPitch Embed</h3>
|
||||
<p>All you need is the GitPitch link:</p>
|
||||
<code>
|
||||
{% gitpitch https://gitpitch.com/gitpitch/in-60-seconds %}
|
||||
</code>
|
||||
<h3>Video Embed</h3>
|
||||
<p>All you need is the <code>id</code> from the URL.</p>
|
||||
<ul>
|
||||
<li><strong>YouTube:</strong> <code>{% youtube dQw4w9WgXcQ %}</code></li>
|
||||
<li><strong>Vimeo:</strong> <code>{% vimeo 193110695 %}</code></li>
|
||||
</ul>
|
||||
<h3>Medium Embed</h3>
|
||||
<p>Just enter the full URL of the Medium article you are trying to embed.</p>
|
||||
<code>{% medium https://medium.com/s/story/boba-science-how-can-i-drink-a-bubble-tea-to-ensure-that-i-dont-finish-the-tea-before-the-bobas-7fc5fd0e442d %}</code>
|
||||
<h3>SlideShare Embed</h3>
|
||||
<p>All you need is the SlideShare <code>key</code>:</p>
|
||||
<code>{% slideshare rdOzN9kr1yK5eE %}</code>
|
||||
<h3>CodePen Embed</h3>
|
||||
<p>All you need is the full CodePen <code>link</code>, ending in the pen ID code, as follows:</p>
|
||||
<code>{% codepen https://codepen.io/twhite96/pen/XKqrJX %}</code>
|
||||
<dl>
|
||||
<dt><code>default-tab</code></dt>
|
||||
<dd>
|
||||
Add default-tab parameter to your CodePen embed tag. Default to <i>result</i><br>
|
||||
<code>{% codepen https://codepen.io/twhite96/pen/XKqrJX default-tab=js,result %}</code>
|
||||
</dd>
|
||||
</dl>
|
||||
<h3>Kotlin Playground</h3>
|
||||
<p>To create a runnable kotlin snippet, create a Kotlin Snippet at <a href="https://play.kotlinlang.org">https://play.kotlinlang.org</a></p>
|
||||
<p>Go to <code>Share</code> dialog and copy the full <code>link</code> from the <code>Medium</code> tab. Use it as follows:</p>
|
||||
<code>{% kotlin https://pl.kotl.in/owreUFFUG?theme=darcula&from=3&to=6&readOnly=true %}</code>
|
||||
<h3>RunKit Embed</h3>
|
||||
<p>Put executable code within a runkit liquid block, as follows:</p>
|
||||
<pre>{% runkit<br>// hidden setup JavaScript code goes in this preamble area<br>const hiddenVar = 42<br>%}<br>// visible, reader-editable JavaScript code goes here<br>console.log(hiddenVar)<br>{% endrunkit %}<br></pre>
|
||||
|
||||
<h3>KaTeX Embed</h3>
|
||||
<p>Place your mathematical expression within a KaTeX liquid block, as follows:</p>
|
||||
<pre>{% katex %}<br> c = \pm\sqrt{a^2 + b^2}<br>{% endkatex %}<br></pre>
|
||||
<p>To render KaTeX inline add the "inline" option:</p>
|
||||
<pre>{% katex inline %}<br> c = \pm\sqrt{a^2 + b^2}<br>{% endkatex %}<br></pre>
|
||||
|
||||
<h3>Stackblitz Embed</h3>
|
||||
<p>All you need is the ID of the Stackblitz:</p>
|
||||
<code>{% stackblitz ball-demo %}</code>
|
||||
<dl>
|
||||
<dt><code>Default view</code></dt>
|
||||
<dd>
|
||||
You can change the default view, the options are <i>both</i>, <i>preview</i>, <i>editor</i>. Defaults to
|
||||
<i>both</i><br>
|
||||
<code>{% stackblitz ball-demo view=preview %}</code>
|
||||
</dd>
|
||||
<dt><code>Default file</code></dt>
|
||||
<dd>
|
||||
You can change the default file you want your embed to point to<br>
|
||||
<code>{% stackblitz ball-demo file=style.css %}</code>
|
||||
</dd>
|
||||
</dl>
|
||||
<h3>CodeSandbox Embed</h3>
|
||||
<p>All you need is the ID of the Sandbox:</p>
|
||||
<code>{% codesandbox ppxnl191zx %}</code>
|
||||
<p>Of CodeSandbox's many
|
||||
<a href="https://codesandbox.io/docs/embedding#embed-options">optional attributes</a>, the following are supported by using them in your tag, just add them after the id, separated by spaces.
|
||||
</p>
|
||||
<dl>
|
||||
<dt><code>initialpath</code></dt>
|
||||
<dd>
|
||||
Which url to initially load in address bar.<br>
|
||||
<code>{% codesandbox ppxnl191zx initialpath=/initial/load/path %}</code>
|
||||
</dd>
|
||||
<dt><code>module</code></dt>
|
||||
<dd>
|
||||
Which module to open by default.<br>
|
||||
<code>{% codesandbox ppxnl191zx module=/path/to/module %}</code>
|
||||
</dd>
|
||||
<dt><code>runonclick</code></dt>
|
||||
<dd>
|
||||
Delays when code is ran if <code>1</code> <br>
|
||||
<code>{% codesandbox ppxnl191zx runonclick=1 %}</code>
|
||||
</dd>
|
||||
</dl>
|
||||
<h3>JSFiddle Embed</h3>
|
||||
<p>All you need is the full JSFiddle <code>link</code>, ending in the fiddle ID code, as follows:</p>
|
||||
<code>{% jsfiddle https://jsfiddle.net/link2twenty/v2kx9jcd %}</code>
|
||||
<dl>
|
||||
<dt><code>Custom tabs</code></dt>
|
||||
<dd>
|
||||
You can add a custom tab order to you JSFiddle embed tag. Defaults to <i>js,html,css,result</i><br>
|
||||
<code>{% jsfiddle https://jsfiddle.net/webdevem/Q8KVC result,html,css %}</code>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<h3>JSitor Liquid Tag</h3>
|
||||
<p>
|
||||
To use JSitor liquid tag you can use the JSitor full <code>link</code>, with or without the parameters
|
||||
</p>
|
||||
<code>{% jsitor https://jsitor.com/embed/B7FQ5tHbY %}</code>
|
||||
<br />
|
||||
<code>{% jsitor https://jsitor.com/embed/B7FQ5tHbY?html&js&css&result&light %}</code>
|
||||
<p>
|
||||
Other options to use JSitor liquid tag is just by its ID, you can add it with or without the parameters
|
||||
</p>
|
||||
<code>{% jsitor B7FQ5tHbY %}</code>
|
||||
<br />
|
||||
<code>{% jsitor B7FQ5tHbY?html&js&css&result&light %}</code>
|
||||
|
||||
<h3>repl.it Embed</h3>
|
||||
<p>All you need is the URL after the domain name:</p>
|
||||
<code>{% replit @WigWog/PositiveFineOpensource %}</code>
|
||||
<h3>Next Tech Embed</h3>
|
||||
<p>All you need is the share URL for your sandbox. You can get the share URL by clicking
|
||||
the "Share" button in the top right when the sandbox is open.</p>
|
||||
<p><img src="https://thepracticaldev.s3.amazonaws.com/i/r449xp8cay3383i139qv.png" alt="Share Replit sandbox" /></p>
|
||||
<code>{% nexttech https://nt.dev/s/6ba1fffbd09e %}</code>
|
||||
<h3>Instagram Embed</h3>
|
||||
<p>All you need is the Instagram post <code>id</code> from the URL:</p>
|
||||
<code>{% instagram BXgGcAUjM39 %}</code>
|
||||
<h3>Speakerdeck Tag</h3>
|
||||
<p>All you need is the data-id code from the embed link:</p>
|
||||
<pre><span style="color: gray;"># Given this embed link:</span><br><script async class="speakerdeck-embed"<br> data-id="<span style='color: orange;'>7e9f8c0fa0c949bd8025457181913fd0</span>"<br> data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script></pre>
|
||||
<pre>{% speakerdeck <span style="color: orange;">7e9f8c0fa0c949bd8025457181913fd0</span> %}</pre>
|
||||
<h3>Soundcloud Embed</h3>
|
||||
<p>Just enter the full URL of the Soundcloud track you are trying to embed.</p>
|
||||
<code>{% soundcloud https://soundcloud.com/user-261265215/dev-to-review-episode-1 %}</code>
|
||||
<h3>Spotify Embed</h3>
|
||||
<p>
|
||||
Enter the Spotify URI of the Spotify track / playlist /
|
||||
album / artist / podcast episode you are trying to embed.
|
||||
</p>
|
||||
<pre>{% spotify <span style="color: #1DB954;">spotify:episode:5V4XZWqZQJvbddd31n56mf</span> %}</pre>
|
||||
<h3>Blogcast Tag</h3>
|
||||
<p>All you need is the article id code from the embed code:</p>
|
||||
<pre>{% blogcast <span style="color: orange;">1234</span> %}</pre>
|
||||
<h3>Parler Tag</h3>
|
||||
<p>Enter the full url of the Parler.io audio file you want to embed. </p>
|
||||
<pre>{% parler https://www.parler.io/audio/73240183203/d53cff009eac2ab1bc9dd8821a638823c39cbcea.7dd28611-b7fc-4cf8-9977-b6e3aaf644a1.mp3 %}</pre>
|
||||
<h3>Stack Exchange / Stack Overflow Tag</h3>
|
||||
<p>
|
||||
You'll need the question or answer's ID code, and the site. When using <code>{% stackoverflow %}</code> as the tag, the site will default to Stack Overflow.
|
||||
For example:
|
||||
<br>
|
||||
<a href="https://stackoverflow.com/questions/24789130/colors-in-irb-rails-console">https://stackoverflow.com/questions/24789130/colors-in-irb-rails-console</a>
|
||||
<ul>
|
||||
<li>The question ID is: <code style="color: aquamarine; background-color: black;">24789130</code></li>
|
||||
</ul>
|
||||
<pre>{% stackoverflow <span style="color: aquamarine;">24789130</span> %}</pre>
|
||||
</p>
|
||||
<p>
|
||||
For other Stack Exchange network sites, you'll need to provide the site's name and use <code>{% stackexchange %}</code> as the tag. For example:
|
||||
<br>
|
||||
<a href="https://diy.stackexchange.com/questions/169988/base-for-refrigerator-wine-shelf">https://diy.stackexchange.com/questions/169988/base-for-refrigerator-wine-shelf</a>
|
||||
<ul>
|
||||
<li>The question ID is: <code style="color: aquamarine; background-color: black;">169988</code></li>
|
||||
<li>The site is <code style="color: orange; background-color: black;">diy</code></li>
|
||||
</ul>
|
||||
<pre>{% stackexchange <span style="color: aquamarine;">169988</span> <span style="color: orange;">diy</span> %}</pre>
|
||||
</p>
|
||||
<p>
|
||||
For answers, you can get the answer's ID code by from the answer's "Share" link. For example:
|
||||
<br>
|
||||
<a href="https://diy.stackexchange.com/a/170185">https://diy.stackexchange.com/a/170185</a>
|
||||
<ul>
|
||||
<li>The answer ID is: <code style="color: aquamarine; background-color: black;">170185</code></li>
|
||||
<li>The site is <code style="color: orange; background-color: black;">diy</code></li>
|
||||
</ul>
|
||||
<pre>{% stackexchange <span style="color: aquamarine;">170185</span> <span style="color: orange;">diy</span> %}</pre>
|
||||
</p>
|
||||
<h3>Wikipedia Embed</h3>
|
||||
<p>Enter the full URL of the Wikipedia article you want to embed, with or without the anchor.</p>
|
||||
<p>
|
||||
<code>{% wikipedia https://en.wikipedia.org/wiki/Wikipedia %}</code>
|
||||
<br />
|
||||
<code>{% wikipedia https://en.wikipedia.org/wiki/Wikipedia#Diversity %}</code>
|
||||
</p>
|
||||
<h3>Asciinema Embed</h3>
|
||||
<p>All you need is the Asciinema id:</p>
|
||||
<code>{% asciinema 239367 %}</code>
|
||||
<h3>Parsing Liquid Tags as a Code Example</h3>
|
||||
<p>To parse Liquid tags as code, simply wrap it with a single backtick or triple backticks.</p>
|
||||
<p><code>`{% mytag %}{{ site.SOMETHING }}{% endmytag %}`</code></p>
|
||||
<p>One specific edge case is with using the <code>raw</code> tag. To properly escape it, use this format:
|
||||
</p>
|
||||
<p><code>`{% raw %}{{site.SOMETHING }} {% ``endraw`` %}`</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
59
app/views/pages/_editor_markdown_help.html.erb
Normal file
59
app/views/pages/_editor_markdown_help.html.erb
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<div id="editor-markdown-help">
|
||||
<div class="crayons-article">
|
||||
<div class="crayons-article__body">
|
||||
<p>Below are some examples of commonly used markdown syntax. If you want to dive deeper, check out
|
||||
<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Here-Cheatsheet" target="_blank" rel="noopener">this cheat sheet.</a>
|
||||
</p>
|
||||
|
||||
<h3>Bold & Italic</h3>
|
||||
<p><em>Italics</em>: <code>*asterisks* or _underscores_</code></p>
|
||||
<p><strong>Bold</strong>: <code>**double asterisks** or __double underscores__</code></p>
|
||||
|
||||
<h3>Links</h3>
|
||||
<p><a href="<%= app_url %>">I'm an inline link</a>: <code>[I'm an inline link](put-link-here)</code></p>
|
||||
<p><a name="anchored">Anchored links</a> (For things like a Table of Contents)</p>
|
||||
<pre>
|
||||
## Table Of Contents
|
||||
* [Chapter 1](#chapter-1)
|
||||
* [Chapter 2](#chapter-2)
|
||||
|
||||
### Chapter 1 <%= "<a name=\"chapter-1\"></a>" %>
|
||||
</pre>
|
||||
|
||||
<h3>Inline Images</h3>
|
||||
<p>
|
||||
When adding GIFs to posts and comments, please note that there is a limit of 200 megapixels per frame/page.
|
||||
<img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OsLaFSo9--/c_fill,f_auto,fl_progressive,h_220,q_auto,w_220/https://thepracticaldev.s3.amazonaws.com/uploads/user/profile_image/31047/af153cd6-9994-4a68-83f4-8ddf3e13f0bf.jpg" alt="example image, with sloan" />
|
||||
</p>
|
||||
<pre></pre>
|
||||
<figcaption> You can even add a caption using the HTML <code>figcaption</code> tag!</figcaption>
|
||||
|
||||
<h3>Headers</h3>
|
||||
<p>Add a header to your post with this syntax:</p>
|
||||
<pre># One '#' for a h1 header<br>## Two '#'s for a h2 header<br>...<br>###### Six '#'s for a h6 header</pre>
|
||||
<h1>One '#' for a h1 header</h1>
|
||||
<h2>Two '#'s for a h2 header</h2>
|
||||
<h3>Three '#'s for a h3 header</h3>
|
||||
<h4>Four '#'s for a h4 header</h4>
|
||||
<h5>Five '#'s for a h5 header</h5>
|
||||
<h6>Six '#'s for a h6 header</h6>
|
||||
|
||||
<h3>Author Notes/Comments</h3>
|
||||
<p>Add some hidden notes/comments to your article with this syntax:</p>
|
||||
<pre><!-- This won't show up in the content! --></pre>
|
||||
|
||||
<h3>Common Gotchas</h3>
|
||||
<p>
|
||||
Lists are written just like any other Markdown editor.
|
||||
If you're adding an image in between numbered list, though, be sure to tab the image,
|
||||
otherwise it'll restart the number of the list.
|
||||
Here's an example of what to do:
|
||||
<img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HjVUshkb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/8l29vxiir8k5d097o9o8.png"
|
||||
alt="example image of writing lists with images in Markdown">
|
||||
</p>
|
||||
<p>
|
||||
Here's the <a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Here-Cheatsheet" target="_blank" rel="noopener">Markdown cheatsheet</a> again for reference.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
// Consistent timezone for testing.
|
||||
// This does not work on windows, see https://github.com/nodejs/node/issues/4230
|
||||
|
||||
/* global process, module */
|
||||
|
||||
process.env.TZ = 'UTC';
|
||||
|
||||
module.exports = {
|
||||
|
|
@ -14,8 +17,8 @@ module.exports = {
|
|||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 39,
|
||||
branches: 35,
|
||||
statements: 38,
|
||||
branches: 34,
|
||||
functions: 34,
|
||||
lines: 39,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ RSpec.describe "Articles", type: :request do
|
|||
it "shows v1 if article has frontmatter" do
|
||||
article = create(:article, user_id: user.id)
|
||||
get "#{article.path}/edit"
|
||||
expect(response.body).to include("articleform__form--v1")
|
||||
expect(response.body).to include("crayons-article-form--v1")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ RSpec.describe Articles::Builder, type: :service do
|
|||
context "when user_editor_v1" do
|
||||
let(:correct_attributes) do
|
||||
{
|
||||
body_markdown: "---\ntitle: \npublished: false\ndescription: \ntags: \n---\n\n",
|
||||
body_markdown: "---\ntitle: \npublished: false\ndescription: \ntags: \n//cover_image: https://direct_url_to_image.jpg\n---\n\n",
|
||||
processed_html: "",
|
||||
user_id: user.id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,14 +16,14 @@ RSpec.describe "Creating an article with the editor", type: :system do
|
|||
it "renders the page", js: true, percy: true do
|
||||
visit new_path
|
||||
fill_in "article_body_markdown", with: template
|
||||
click_button "SAVE CHANGES"
|
||||
click_button "Save changes"
|
||||
Percy.snapshot(page, name: "Creating an article: shows the title")
|
||||
end
|
||||
|
||||
it "creates a new article", js: true, retry: 3 do
|
||||
visit new_path
|
||||
fill_in "article_body_markdown", with: template
|
||||
click_button "SAVE CHANGES"
|
||||
click_button "Save changes"
|
||||
expect(page).to have_selector("header h1", text: "Sample Article")
|
||||
end
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ RSpec.describe "Creating an article with the editor", type: :system do
|
|||
it "creates a new article with a Runkit tag" do
|
||||
visit new_path
|
||||
fill_in "article_body_markdown", with: template_with_runkit_tag
|
||||
click_button "SAVE CHANGES"
|
||||
click_button "Save changes"
|
||||
|
||||
expect_runkit_tag_to_be_active
|
||||
end
|
||||
|
|
@ -39,15 +39,15 @@ RSpec.describe "Creating an article with the editor", type: :system do
|
|||
it "previews article with a Runkit tag and creates it" do
|
||||
visit new_path
|
||||
fill_in "article_body_markdown", with: template_with_runkit_tag
|
||||
click_button "PREVIEW"
|
||||
click_button "Preview"
|
||||
|
||||
expect_runkit_tag_to_be_active
|
||||
|
||||
click_button "EDIT"
|
||||
click_button "Edit"
|
||||
|
||||
expect_no_runkit_tag_to_be_active
|
||||
|
||||
click_button "SAVE CHANGES"
|
||||
click_button "Save changes"
|
||||
|
||||
expect_runkit_tag_to_be_active
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,38 +12,35 @@ RSpec.describe "Editing with an editor", type: :system, js: true do
|
|||
it "renders the page when user previews their changes", percy: true do
|
||||
visit "/#{user.username}/#{article.slug}/edit"
|
||||
fill_in "article_body_markdown", with: template.gsub("Suspendisse", "Yooo")
|
||||
click_button("PREVIEW")
|
||||
click_button("Preview")
|
||||
Percy.snapshot(page, name: "Editing an article: shows the title")
|
||||
end
|
||||
|
||||
it "user previews their changes" do
|
||||
visit "/#{user.username}/#{article.slug}/edit"
|
||||
fill_in "article_body_markdown", with: template.gsub("Suspendisse", "Yooo")
|
||||
click_button("PREVIEW")
|
||||
|
||||
click_button("Preview")
|
||||
expect(page).to have_text("Yooo")
|
||||
expect(find(".active").text).to have_text("EDIT")
|
||||
end
|
||||
|
||||
it "user updates their post" do
|
||||
visit "/#{user.username}/#{article.slug}/edit"
|
||||
fill_in "article_body_markdown", with: template.gsub("Suspendisse", "Yooo")
|
||||
click_button("SAVE CHANGES")
|
||||
click_button("Save changes")
|
||||
expect(page).to have_text("Yooo")
|
||||
end
|
||||
|
||||
it "user unpublishes their post" do
|
||||
visit "/#{user.username}/#{article.slug}/edit"
|
||||
fill_in "article_body_markdown", with: template.gsub("true", "false")
|
||||
click_button("SAVE CHANGES")
|
||||
|
||||
click_button("Save changes")
|
||||
expect(page).to have_text("Unpublished Post.")
|
||||
end
|
||||
|
||||
it "renders the page when user unpublishes their post", percy: true do
|
||||
visit "/#{user.username}/#{article.slug}/edit"
|
||||
fill_in "article_body_markdown", with: template.gsub("true", "false")
|
||||
click_button("SAVE CHANGES")
|
||||
click_button("Save changes")
|
||||
Percy.snapshot(page, name: "Editing an article: shows when unpublished")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ RSpec.describe "Using the editor", type: :system do
|
|||
before do
|
||||
fill_markdown_with(read_from_file(raw_text))
|
||||
page.execute_script("window.scrollTo(0, -100000)")
|
||||
find("button", text: /\APREVIEW\z/).click
|
||||
find("button", text: /\APreview\z/).click
|
||||
end
|
||||
|
||||
after do
|
||||
|
|
@ -38,7 +38,7 @@ RSpec.describe "Using the editor", type: :system do
|
|||
end
|
||||
|
||||
it "fills out form with rich content and click preview" do
|
||||
article_body = find("div.body")["innerHTML"]
|
||||
article_body = find("div.crayons-article__body")["innerHTML"]
|
||||
article_body.gsub!(/"https:\/\/res\.cloudinary\.com\/.{1,}"/, "cloudinary_link")
|
||||
|
||||
Approvals.verify(article_body, name: "user_preview_article_body", format: :html)
|
||||
|
|
@ -48,13 +48,13 @@ RSpec.describe "Using the editor", type: :system do
|
|||
describe "Submitting an article", js: true do
|
||||
it "renders the page", percy: true do
|
||||
fill_markdown_with(read_from_file(raw_text))
|
||||
find("button", text: /\ASAVE CHANGES\z/).click
|
||||
find("button", text: /\ASave changes\z/).click
|
||||
Percy.snapshot(page, name: "Using the editor: submit an article")
|
||||
end
|
||||
|
||||
it "fill out form and submit" do
|
||||
fill_markdown_with(read_from_file(raw_text))
|
||||
find("button", text: /\ASAVE CHANGES\z/).click
|
||||
find("button", text: /\ASave changes\z/).click
|
||||
article_body = find(:xpath, "//div[@id='article-body']")["innerHTML"]
|
||||
article_body.gsub!(/"https:\/\/res\.cloudinary\.com\/.{1,}"/, "cloudinary_link")
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ RSpec.describe "Using the editor", type: :system do
|
|||
|
||||
it "user write and publish an article" do
|
||||
fill_markdown_with(template.gsub("false", "true"))
|
||||
find("button", text: /\ASAVE CHANGES\z/).click
|
||||
find("button", text: /\ASave changes\z/).click
|
||||
["Sample Article", template[-200..], "test"].each do |text|
|
||||
expect(page).to have_text(text)
|
||||
end
|
||||
|
|
@ -72,7 +72,7 @@ RSpec.describe "Using the editor", type: :system do
|
|||
context "without a title", js: true do
|
||||
before do
|
||||
fill_markdown_with(template.gsub("Sample Article", ""))
|
||||
find("button", text: /\ASAVE CHANGES\z/).click
|
||||
find("button", text: /\ASave changes\z/).click
|
||||
end
|
||||
|
||||
it "renders the page", percy: true do
|
||||
|
|
@ -80,7 +80,7 @@ RSpec.describe "Using the editor", type: :system do
|
|||
end
|
||||
|
||||
it "shows a message that the title cannot be blank" do
|
||||
expect(page).to have_text(/title: can't be blank/)
|
||||
expect(page).to have_text(/title: can't be blank/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue