mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-23 19:02:29 +03:00
Added Subheads behind a flag (#20265)
refs MOM-152 MOM-148 MOM-151 - Added Subheads behind a flag + toggle in settings. - Removes Excerpt fields from post settings if flag is enabled. - Added subhead toggle in newsletter settings. - Loads of styling --------- Co-authored-by: Sanne de Vries <sannedv@protonmail.com>
This commit is contained in:
parent
9099ab47c4
commit
fddcf3ffee
@ -21,6 +21,7 @@ export type Newsletter = {
|
||||
show_header_title: boolean;
|
||||
title_font_category: string;
|
||||
title_alignment: string;
|
||||
show_subhead: boolean;
|
||||
show_feature_image: boolean;
|
||||
body_font_category: string;
|
||||
footer_content: string | null;
|
||||
|
@ -19,6 +19,7 @@
|
||||
"show_header_title": true,
|
||||
"title_font_category": "serif",
|
||||
"title_alignment": "center",
|
||||
"show_subhead": true,
|
||||
"show_feature_image": true,
|
||||
"body_font_category": "serif",
|
||||
"footer_content": "",
|
||||
|
@ -67,6 +67,10 @@ const features = [{
|
||||
title: 'ActivityPub',
|
||||
description: '(Highly) Experimental support for ActivityPub.',
|
||||
flag: 'ActivityPub'
|
||||
},{
|
||||
title: 'Subhead',
|
||||
description: 'Using custom excerpts as subheads in editor and newsletter',
|
||||
flag: 'subhead'
|
||||
}];
|
||||
|
||||
const AlphaFeatures: React.FC = () => {
|
||||
|
@ -103,6 +103,7 @@ const Sidebar: React.FC<{
|
||||
const {mutateAsync: uploadImage} = useUploadImage();
|
||||
const [selectedTab, setSelectedTab] = useState('generalSettings');
|
||||
const hasEmailCustomization = useFeatureFlag('emailCustomization');
|
||||
const hasSubhead = useFeatureFlag('subhead');
|
||||
const {localSettings} = useSettingGroup();
|
||||
const [siteTitle] = getSettingValues(localSettings, ['title']) as string[];
|
||||
const handleError = useHandleError();
|
||||
@ -423,6 +424,14 @@ const Sidebar: React.FC<{
|
||||
title='Body style'
|
||||
onSelect={option => updateNewsletter({body_font_category: option?.value})}
|
||||
/>
|
||||
{hasSubhead &&
|
||||
<Toggle
|
||||
checked={newsletter.show_subhead}
|
||||
direction="rtl"
|
||||
label='Subhead'
|
||||
onChange={e => updateNewsletter({show_subhead: e.target.checked})}
|
||||
/>
|
||||
}
|
||||
<Toggle
|
||||
checked={newsletter.show_feature_image}
|
||||
direction="rtl"
|
||||
|
@ -110,6 +110,7 @@ const NewsletterPreview: React.FC<{newsletter: Newsletter}> = ({newsletter}) =>
|
||||
showFeedback={showFeedback}
|
||||
showLatestPosts={newsletter.show_latest_posts}
|
||||
showPostTitleSection={newsletter.show_post_title_section}
|
||||
showSubhead={newsletter.show_subhead}
|
||||
showSubscriptionDetails={newsletter.show_subscription_details}
|
||||
siteTitle={title}
|
||||
titleAlignment={newsletter.title_alignment}
|
||||
|
@ -18,6 +18,7 @@ const NewsletterPreviewContent: React.FC<{
|
||||
headerTitle?: string | null;
|
||||
headerSubtitle?: string | null;
|
||||
showPostTitleSection: boolean;
|
||||
showSubhead: boolean;
|
||||
titleAlignment?: string;
|
||||
titleFontCategory?: string;
|
||||
bodyFontCategory?: string;
|
||||
@ -49,6 +50,7 @@ const NewsletterPreviewContent: React.FC<{
|
||||
headerTitle,
|
||||
headerSubtitle,
|
||||
showPostTitleSection,
|
||||
showSubhead,
|
||||
titleAlignment,
|
||||
titleFontCategory,
|
||||
bodyFontCategory,
|
||||
@ -75,6 +77,7 @@ const NewsletterPreviewContent: React.FC<{
|
||||
const showHeader = headerIcon || headerTitle;
|
||||
const {config} = useGlobalData();
|
||||
const hasNewEmailAddresses = useFeatureFlag('newEmailAddresses');
|
||||
const hasSubhead = useFeatureFlag('subhead');
|
||||
|
||||
const currentDate = new Date().toLocaleDateString('default', {
|
||||
year: 'numeric',
|
||||
@ -131,6 +134,9 @@ const NewsletterPreviewContent: React.FC<{
|
||||
)} style={{color: titleColor}}>
|
||||
Your email newsletter
|
||||
</h2>
|
||||
{(hasSubhead && showSubhead) && (
|
||||
<p className="mb-4 text-[1.6rem] leading-[1.7] text-black">A subhead that highlights the key points of your newsletter.</p>
|
||||
)}
|
||||
<div className={clsx(
|
||||
'flex w-full justify-between text-center text-md leading-none text-grey-700',
|
||||
titleAlignment === 'center' ? 'flex-col' : 'flex-row'
|
||||
|
@ -56,6 +56,22 @@
|
||||
{{on "paste" this.cleanPastedTitle}}
|
||||
data-test-editor-title-input={{true}}
|
||||
/>
|
||||
|
||||
{{#if (feature 'subhead')}}
|
||||
<GhTextarea
|
||||
@class="gh-editor-subhead"
|
||||
@placeholder="Add a subhead..."
|
||||
@shouldFocus={{false}}
|
||||
@tabindex="1"
|
||||
@autoExpand=".gh-koenig-editor"
|
||||
@value={{readonly this.excerpt}}
|
||||
@input={{this.updateExcerpt}}
|
||||
@focus-out={{optional @blurExcerpt}}
|
||||
@keyDown={{this.onExcerptKeydown}}
|
||||
data-test-editor-subhead-input={{true}}
|
||||
/>
|
||||
<hr class="gh-editor-title-divider">
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<KoenigLexicalEditor
|
||||
|
@ -6,6 +6,7 @@ import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class GhKoenigEditorReactComponent extends Component {
|
||||
@service settings;
|
||||
@service feature;
|
||||
|
||||
containerElement = null;
|
||||
titleElement = null;
|
||||
@ -30,6 +31,10 @@ export default class GhKoenigEditorReactComponent extends Component {
|
||||
return color;
|
||||
}
|
||||
|
||||
get excerpt() {
|
||||
return this.args.excerpt || '';
|
||||
}
|
||||
|
||||
@action
|
||||
registerElement(element) {
|
||||
this.containerElement = element;
|
||||
@ -112,29 +117,66 @@ export default class GhKoenigEditorReactComponent extends Component {
|
||||
// - Enter, creating an empty paragraph when editor is not empty
|
||||
@action
|
||||
onTitleKeydown(event) {
|
||||
const {editorAPI} = this;
|
||||
if (this.feature.get('subhead')) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
const subheadElement = document.querySelector('.gh-editor-subhead');
|
||||
if (subheadElement) {
|
||||
subheadElement.focus();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const {editorAPI} = this;
|
||||
|
||||
if (!editorAPI || event.originalEvent.isComposing) {
|
||||
return;
|
||||
}
|
||||
if (!editorAPI || event.originalEvent.isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {key} = event;
|
||||
const {value, selectionStart} = event.target;
|
||||
const {key} = event;
|
||||
const {value, selectionStart} = event.target;
|
||||
|
||||
const couldLeaveTitle = !value || selectionStart === value.length;
|
||||
const arrowLeavingTitle = ['ArrowDown', 'ArrowRight'].includes(key) && couldLeaveTitle;
|
||||
const couldLeaveTitle = !value || selectionStart === value.length;
|
||||
const arrowLeavingTitle = ['ArrowDown', 'ArrowRight'].includes(key) && couldLeaveTitle;
|
||||
|
||||
if (key === 'Enter' || key === 'Tab' || arrowLeavingTitle) {
|
||||
event.preventDefault();
|
||||
if (key === 'Enter' || key === 'Tab' || arrowLeavingTitle) {
|
||||
event.preventDefault();
|
||||
|
||||
if (key === 'Enter' && !editorAPI.editorIsEmpty()) {
|
||||
editorAPI.insertParagraphAtTop({focus: true});
|
||||
} else {
|
||||
editorAPI.focusEditor({position: 'top'});
|
||||
if (key === 'Enter' && !editorAPI.editorIsEmpty()) {
|
||||
editorAPI.insertParagraphAtTop({focus: true});
|
||||
} else {
|
||||
editorAPI.focusEditor({position: 'top'});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subheader ("excerpt") Actions -------------------------------------------
|
||||
|
||||
@action
|
||||
updateExcerpt(event) {
|
||||
this.args.onExcerptChange?.(event.target.value);
|
||||
}
|
||||
|
||||
@action
|
||||
focusExcerpt() {
|
||||
this.args.onExcerptFocus?.();
|
||||
}
|
||||
|
||||
@action
|
||||
blurExcerpt() {
|
||||
this.args.onExcerptBlur?.();
|
||||
}
|
||||
|
||||
@action
|
||||
onExcerptKeydown(event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
this.editorAPI.focusEditor({position: 'top'});
|
||||
}
|
||||
}
|
||||
|
||||
// move cursor to the editor on
|
||||
|
||||
// Body actions ------------------------------------------------------------
|
||||
|
||||
@action
|
||||
|
@ -93,7 +93,7 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#unless (feature 'subhead')}}
|
||||
<GhFormGroup @errors={{this.post.errors}} @hasValidated={{this.post.hasValidated}} @property="customExcerpt">
|
||||
<label for="custom-excerpt">Excerpt</label>
|
||||
<GhTextarea
|
||||
@ -108,6 +108,7 @@
|
||||
/>
|
||||
<GhErrorMessage @errors={{this.post.errors}} @property="customExcerpt" data-test-error="custom-excerpt" />
|
||||
</GhFormGroup>
|
||||
{{/unless}}
|
||||
|
||||
{{#unless this.session.user.isAuthorOrContributor}}
|
||||
<GhFormGroup class="for-select mb8" @errors={{this.post.errors}} @hasValidated={{this.post.hasValidated}} @property="authors" data-test-input="authors">
|
||||
|
@ -284,6 +284,11 @@ export default class LexicalEditorController extends Controller {
|
||||
this.set('post.titleScratch', title);
|
||||
}
|
||||
|
||||
@action
|
||||
updateExcerptScratch(excerpt) {
|
||||
this.set('post.customExcerptScratch', excerpt);
|
||||
}
|
||||
|
||||
// updates local willPublish/Schedule values, does not get applied to
|
||||
// the post's `status` value until a save is triggered
|
||||
@action
|
||||
|
@ -24,6 +24,7 @@ export default class Newsletter extends Model.extend(ValidationEngine) {
|
||||
@attr({defaultValue: true}) showHeaderTitle;
|
||||
@attr({defaultValue: true}) showHeaderName;
|
||||
@attr({defaultValue: true}) showPostTitleSection;
|
||||
@attr({defaultValue: false}) showSubhead;
|
||||
@attr({defaultValue: true}) showCommentCta;
|
||||
@attr({defaultValue: false}) showSubscriptionDetails;
|
||||
@attr({defaultValue: false}) showLatestPosts;
|
||||
|
@ -83,6 +83,7 @@ export default class FeatureService extends Service {
|
||||
@feature('onboardingChecklist') onboardingChecklist;
|
||||
@feature('ActivityPub') ActivityPub;
|
||||
@feature('internalLinking') internalLinking;
|
||||
@feature('subhead') subhead;
|
||||
|
||||
_user = null;
|
||||
|
||||
|
@ -681,7 +681,7 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
|
||||
.gh-editor-feature-image-caption {
|
||||
width: 100%;
|
||||
min-height: 24px;
|
||||
margin: 0 0 1.7em 0;
|
||||
margin: 0 0 1.2rem 0;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
border-width: 0;
|
||||
@ -717,12 +717,25 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.gh-editor-title-container {
|
||||
position: relative;
|
||||
max-width: 740px;
|
||||
width: 100%;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.gh-editor-title {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
min-height: auto;
|
||||
margin-bottom: 1.2rem;
|
||||
margin: 0 0 1.6rem;
|
||||
padding: 0 0 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--black);
|
||||
font-size: 4.8rem;
|
||||
letter-spacing: -0.017em;
|
||||
@ -732,6 +745,18 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media (min-width: 500px) and (max-width: 768px) {
|
||||
.gh-editor-title {
|
||||
font-size: 3.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.gh-editor-title {
|
||||
font-size: 2.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.gh-editor-title:focus {
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
@ -741,6 +766,55 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.gh-editor-title::placeholder {
|
||||
color: var(--lightgrey-d1);
|
||||
font-weight: 700;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.gh-editor-hidden-indicator {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
height: 2.4rem;
|
||||
margin-left: -6rem;
|
||||
color: var(--midgrey-l2);
|
||||
}
|
||||
|
||||
.gh-editor-title-container .gh-editor-hidden-indicator {
|
||||
top: 1.8rem;
|
||||
}
|
||||
|
||||
.gh-editor-hidden-indicator svg {
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
.gh-editor-subhead {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
min-width: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--darkgrey);
|
||||
font-size: 1.9rem;
|
||||
font-weight: 440;
|
||||
line-height: 1.4em;
|
||||
letter-spacing: -.018em;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gh-editor-subhead:focus {
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.gh-editor-title-divider {
|
||||
margin: 1.6rem 0 4.8rem;
|
||||
}
|
||||
|
||||
.gh-editor .tk-indicator {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
@ -927,21 +1001,6 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.gh-editor-title {
|
||||
font-size: 3.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.gh-editor-title {
|
||||
padding: 0 0 4px;
|
||||
}
|
||||
|
||||
.gh-editor-title::placeholder {
|
||||
color: var(--lightgrey-d1);
|
||||
font-weight: 700;
|
||||
opacity: 1;
|
||||
}
|
||||
.gh-editor .editor-preview {
|
||||
height: auto;
|
||||
margin-top: 4px;
|
||||
@ -1168,61 +1227,6 @@ figure {
|
||||
/* Labs
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.gh-editor-title-container {
|
||||
position: relative;
|
||||
max-width: 740px;
|
||||
width: 100%;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.gh-editor .gh-editor-title {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
min-height: auto;
|
||||
margin: 0 0 1.2rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--black);
|
||||
font-size: 4.8rem;
|
||||
letter-spacing: -0.017em;
|
||||
line-height: 1.1em;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media (min-width: 500px) and (max-width: 768px) {
|
||||
.gh-editor .gh-editor-title {
|
||||
font-size: 3.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.gh-editor .gh-editor-title {
|
||||
font-size: 2.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.gh-editor-hidden-indicator {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
height: 2.4rem;
|
||||
margin-left: -6rem;
|
||||
color: var(--midgrey-l2);
|
||||
}
|
||||
|
||||
.gh-editor-title-container .gh-editor-hidden-indicator {
|
||||
top: 1.8rem;
|
||||
}
|
||||
|
||||
.gh-editor-hidden-indicator svg {
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
.gh-setting-error {
|
||||
margin-top: 1em;
|
||||
line-height: 1.3em;
|
||||
|
@ -199,3 +199,7 @@
|
||||
.gh-post-history-hidden-lexical {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gh-post-history .gh-editor-feature-image p {
|
||||
margin: 0 0 1.2rem;
|
||||
}
|
||||
|
@ -61,10 +61,12 @@
|
||||
--}}
|
||||
<GhKoenigEditorLexical
|
||||
@title={{readonly this.post.titleScratch}}
|
||||
@excerpt={{readonly this.post.customExcerpt}}
|
||||
@titleAutofocus={{this.shouldFocusTitle}}
|
||||
@titlePlaceholder={{concat (capitalize this.post.displayName) " title"}}
|
||||
@titleHasTk={{this.titleHasTk}}
|
||||
@onTitleChange={{this.updateTitleScratch}}
|
||||
@onExcerptChange={{this.updateExcerptScratch}}
|
||||
@onTitleBlur={{perform this.saveTitleTask}}
|
||||
@body={{readonly this.post.lexicalScratch}}
|
||||
@bodyPlaceholder={{concat "Begin writing your " this.post.displayName "..."}}
|
||||
|
@ -51,7 +51,8 @@ const ALPHA_FEATURES = [
|
||||
'tipsAndDonations',
|
||||
'importMemberTier',
|
||||
'lexicalIndicators',
|
||||
'adminXDemo'
|
||||
'adminXDemo',
|
||||
'subhead'
|
||||
];
|
||||
|
||||
module.exports.GA_KEYS = [...GA_FEATURES];
|
||||
|
Loading…
Reference in New Issue
Block a user