kopia lustrzana https://github.com/wagtail/wagtail
Add save warning to page editor for comments and edits
rodzic
c619261565
commit
d64dad9739
|
@ -1,11 +1,7 @@
|
|||
footer {
|
||||
$border-curvature: 3px;
|
||||
@include transition(bottom 0.5s ease 1s);
|
||||
@include row();
|
||||
border-radius: 3px 3px 0 0;
|
||||
box-shadow: 0 0 2px rgba(255, 255, 255, 0.5);
|
||||
background: $color-grey-1;
|
||||
color: $color-white;
|
||||
margin-top: $mobile-nice-padding;
|
||||
|
||||
ul {
|
||||
@include unlist();
|
||||
|
@ -15,12 +11,48 @@ footer {
|
|||
float: left;
|
||||
margin-right: 1em;
|
||||
|
||||
li, // dropdown li
|
||||
.dropdown li, // dropdown li
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer__container {
|
||||
border-radius: $border-curvature $border-curvature 0 0;
|
||||
background: $color-grey-1;
|
||||
color: $color-white;
|
||||
margin-top: 0;
|
||||
transition: transform 1s;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
box-shadow: 0 0 2px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&.footer__container--hidden {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.footer__save-warning {
|
||||
font-size: 0.95em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
font-size: 1.2em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: -0.2em 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer__emphasise-span-tags span {
|
||||
color: $color-orange;
|
||||
}
|
||||
|
||||
.actions {
|
||||
width: 250px;
|
||||
|
||||
|
@ -67,10 +99,13 @@ footer {
|
|||
@include media-breakpoint-down(xs) {
|
||||
.actions,
|
||||
.preview,
|
||||
.footer__container,
|
||||
.preview .dropdown {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
margin-top: $mobile-nice-padding;
|
||||
|
||||
.meta {
|
||||
p {
|
||||
white-space: normal;
|
||||
|
@ -81,6 +116,22 @@ footer {
|
|||
left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.footer__container {
|
||||
&:not(:first-child) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.footer__save-warning {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
|
@ -89,6 +140,22 @@ footer {
|
|||
width: auto;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
padding: 0.75em;
|
||||
|
||||
> ul {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.footer__container {
|
||||
padding: 0.75em;
|
||||
margin-right: 0;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: -$border-curvature;
|
||||
}
|
||||
}
|
||||
|
||||
.footer__save-warning {
|
||||
margin-right: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ const remoteReply: CommentReply = {
|
|||
author: { id: 1, name: 'test user' },
|
||||
date: 0,
|
||||
text: 'a reply',
|
||||
originalText: 'a reply',
|
||||
newText: '',
|
||||
deleted: false,
|
||||
};
|
||||
|
@ -19,6 +20,7 @@ const localReply: CommentReply = {
|
|||
author: { id: 1, name: 'test user' },
|
||||
date: 0,
|
||||
text: 'another reply',
|
||||
originalText: 'another new reply',
|
||||
newText: '',
|
||||
deleted: false,
|
||||
};
|
||||
|
@ -34,6 +36,7 @@ const remoteComment: Comment = {
|
|||
author: { id: 1, name: 'test user' },
|
||||
date: 0,
|
||||
text: 'test text',
|
||||
originalText: 'test text',
|
||||
newReply: '',
|
||||
newText: '',
|
||||
remoteReplyCount: 1,
|
||||
|
@ -51,6 +54,7 @@ const localComment: Comment = {
|
|||
author: { id: 1, name: 'test user' },
|
||||
date: 0,
|
||||
text: 'unsaved comment',
|
||||
originalText: 'unsaved comment',
|
||||
newReply: '',
|
||||
newText: '',
|
||||
replies: new Map(),
|
||||
|
|
|
@ -21,7 +21,8 @@ import {
|
|||
selectCommentsForContentPathFactory,
|
||||
selectCommentFactory,
|
||||
selectEnabled,
|
||||
selectFocused
|
||||
selectFocused,
|
||||
selectIsDirty
|
||||
} from './selectors';
|
||||
import CommentComponent from './components/Comment';
|
||||
import { CommentFormSetComponent } from './components/Form';
|
||||
|
@ -139,7 +140,8 @@ export class CommentApp {
|
|||
selectors = {
|
||||
selectComments,
|
||||
selectEnabled,
|
||||
selectFocused
|
||||
selectFocused,
|
||||
selectIsDirty
|
||||
}
|
||||
actions = commentActionFunctions;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { State } from '../state';
|
|||
|
||||
export const selectComments = (state: State) => state.comments.comments;
|
||||
export const selectFocused = (state: State) => state.comments.focusedComment;
|
||||
export const selectRemoteCommentCount = (state: State) => state.comments.remoteCommentCount;
|
||||
|
||||
export function selectCommentsForContentPathFactory(contentpath: string) {
|
||||
return createSelector(selectComments, (comments) =>
|
||||
|
@ -27,3 +28,21 @@ export function selectCommentFactory(localId: number) {
|
|||
}
|
||||
|
||||
export const selectEnabled = (state: State) => state.settings.commentsEnabled;
|
||||
|
||||
export const selectIsDirty = createSelector(
|
||||
selectComments,
|
||||
selectRemoteCommentCount,
|
||||
(comments, remoteCommentCount) => {
|
||||
if (remoteCommentCount !== comments.size) {
|
||||
return true;
|
||||
}
|
||||
return Array.from(comments.values()).some(comment => {
|
||||
if (comment.deleted ||
|
||||
comment.replies.size !== comment.remoteReplyCount ||
|
||||
comment.originalText !== comment.text
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return Array.from(comment.replies.values()).some(reply => reply.deleted || reply.originalText !== reply.text);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
import { basicCommentsState } from '../__fixtures__/state';
|
||||
import { INITIAL_STATE } from '../state/settings';
|
||||
import { INITIAL_STATE as INITIAL_SETTINGS_STATE } from '../state/settings';
|
||||
import {
|
||||
INITIAL_STATE as INITIAL_COMMENTS_STATE,
|
||||
newComment,
|
||||
newCommentReply,
|
||||
} from '../state/comments';
|
||||
import { reducer } from '../state';
|
||||
|
||||
import { selectCommentsForContentPathFactory } from './index';
|
||||
import * as actions from '../actions/comments';
|
||||
|
||||
import { selectCommentsForContentPathFactory, selectIsDirty } from './index';
|
||||
|
||||
test('Select comments for contentpath', () => {
|
||||
// test that the selectCommentsForContentPathFactory can generate selectors for the two
|
||||
// contentpaths in basicCommentsState
|
||||
const state = {
|
||||
comments: basicCommentsState,
|
||||
settings: INITIAL_STATE,
|
||||
settings: INITIAL_SETTINGS_STATE,
|
||||
};
|
||||
const testContentPathSelector = selectCommentsForContentPathFactory(
|
||||
'test_contentpath'
|
||||
|
@ -23,3 +31,79 @@ test('Select comments for contentpath', () => {
|
|||
expect(otherSelectedComments.length).toBe(1);
|
||||
expect(otherSelectedComments[0].contentpath).toBe('test_contentpath_2');
|
||||
});
|
||||
|
||||
test('Select is dirty', () => {
|
||||
const state = {
|
||||
comments: INITIAL_COMMENTS_STATE,
|
||||
settings: INITIAL_SETTINGS_STATE
|
||||
};
|
||||
const stateWithUnsavedComment = reducer(state, actions.addComment(newComment(
|
||||
'test_contentpath',
|
||||
'test_position',
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
{
|
||||
remoteId: null,
|
||||
text: 'my new comment'
|
||||
}
|
||||
)));
|
||||
|
||||
expect(selectIsDirty(stateWithUnsavedComment)).toBe(true);
|
||||
|
||||
const stateWithSavedComment = reducer(state, actions.addComment(newComment(
|
||||
'test_contentpath',
|
||||
'test_position',
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
{
|
||||
remoteId: 1,
|
||||
text: 'my saved comment'
|
||||
}
|
||||
)));
|
||||
|
||||
expect(selectIsDirty(stateWithSavedComment)).toBe(false);
|
||||
|
||||
const stateWithDeletedComment = reducer(stateWithSavedComment, actions.deleteComment(1));
|
||||
|
||||
expect(selectIsDirty(stateWithDeletedComment)).toBe(true);
|
||||
|
||||
const stateWithEditedComment = reducer(stateWithSavedComment, actions.updateComment(1, { text: 'edited_text' }));
|
||||
|
||||
expect(selectIsDirty(stateWithEditedComment)).toBe(true);
|
||||
|
||||
const stateWithUnsavedReply = reducer(stateWithSavedComment, actions.addReply(1, newCommentReply(
|
||||
2,
|
||||
null,
|
||||
0,
|
||||
{
|
||||
remoteId: null,
|
||||
text: 'new reply'
|
||||
}
|
||||
)));
|
||||
|
||||
expect(selectIsDirty(stateWithUnsavedReply)).toBe(true);
|
||||
|
||||
const stateWithSavedReply = reducer(stateWithSavedComment, actions.addReply(1, newCommentReply(
|
||||
2,
|
||||
null,
|
||||
0,
|
||||
{
|
||||
remoteId: 2,
|
||||
text: 'new saved reply'
|
||||
}
|
||||
)));
|
||||
|
||||
expect(selectIsDirty(stateWithSavedReply)).toBe(false);
|
||||
|
||||
const stateWithDeletedReply = reducer(stateWithSavedReply, actions.deleteReply(1, 2));
|
||||
|
||||
expect(selectIsDirty(stateWithDeletedReply)).toBe(true);
|
||||
|
||||
const stateWithEditedReply = reducer(stateWithSavedReply, actions.updateReply(1, 2, { text: 'edited_text' }));
|
||||
|
||||
expect(selectIsDirty(stateWithEditedReply)).toBe(true);
|
||||
});
|
||||
|
|
|
@ -30,6 +30,7 @@ test('New comment added to state', () => {
|
|||
author: { id: 1, name: 'test user' },
|
||||
date: 0,
|
||||
text: 'new comment',
|
||||
originalText: 'new comment',
|
||||
newReply: '',
|
||||
newText: '',
|
||||
remoteReplyCount: 0,
|
||||
|
@ -55,6 +56,7 @@ test('Remote comment added to state', () => {
|
|||
author: { id: 1, name: 'test user' },
|
||||
date: 0,
|
||||
text: 'new comment',
|
||||
originalText: 'new comment',
|
||||
newReply: '',
|
||||
newText: '',
|
||||
remoteReplyCount: 0,
|
||||
|
@ -70,7 +72,7 @@ test('Remote comment added to state', () => {
|
|||
|
||||
test('Existing comment updated', () => {
|
||||
const commentUpdate: CommentUpdate = {
|
||||
mode: 'editing',
|
||||
mode: 'editing'
|
||||
};
|
||||
const updateAction = actions.updateComment(1, commentUpdate);
|
||||
const newState = reducer(basicCommentsState, updateAction);
|
||||
|
@ -125,6 +127,7 @@ test('Reply added', () => {
|
|||
author: { id: 1, name: 'test user' },
|
||||
date: 0,
|
||||
text: 'a new reply',
|
||||
originalText: 'a new reply',
|
||||
newText: '',
|
||||
deleted: false,
|
||||
};
|
||||
|
@ -149,6 +152,7 @@ test('Remote reply added', () => {
|
|||
author: { id: 1, name: 'test user' },
|
||||
date: 0,
|
||||
text: 'a new reply',
|
||||
originalText: 'a new reply',
|
||||
newText: '',
|
||||
deleted: false,
|
||||
};
|
||||
|
|
|
@ -28,9 +28,15 @@ export interface CommentReply {
|
|||
mode: CommentReplyMode;
|
||||
author: Author | null;
|
||||
date: number;
|
||||
text: string;
|
||||
newText: string;
|
||||
deleted: boolean;
|
||||
// There are three variables used for text
|
||||
// text is the canonical text, that will be output to the form
|
||||
// newText stores the edited version of the text until it is saved
|
||||
// originalText stores the text upon reply creation, and is
|
||||
// used to check whether existing replies have been edited
|
||||
text: string;
|
||||
originalText: string;
|
||||
newText: string;
|
||||
}
|
||||
|
||||
export interface NewReplyOptions {
|
||||
|
@ -56,12 +62,13 @@ export function newCommentReply(
|
|||
author,
|
||||
date,
|
||||
text,
|
||||
originalText: text,
|
||||
newText: '',
|
||||
deleted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export type CommentReplyUpdate = Partial<CommentReply>;
|
||||
export type CommentReplyUpdate = Partial<Omit<CommentReply, 'originalText'>>;
|
||||
|
||||
export type CommentMode =
|
||||
| 'default'
|
||||
|
@ -84,11 +91,17 @@ export interface Comment {
|
|||
deleted: boolean;
|
||||
author: Author | null;
|
||||
date: number;
|
||||
text: string;
|
||||
replies: Map<number, CommentReply>;
|
||||
newReply: string;
|
||||
newText: string;
|
||||
remoteReplyCount: number;
|
||||
// There are three variables used for text
|
||||
// text is the canonical text, that will be output to the form
|
||||
// newText stores the edited version of the text until it is saved
|
||||
// originalText stores the text upon comment creation, and is
|
||||
// used to check whether existing comments have been edited
|
||||
text: string;
|
||||
originalText: string;
|
||||
newText: string;
|
||||
}
|
||||
|
||||
export interface NewCommentOptions {
|
||||
|
@ -122,6 +135,7 @@ export function newComment(
|
|||
author,
|
||||
date,
|
||||
text,
|
||||
originalText: text,
|
||||
replies,
|
||||
newReply: '',
|
||||
newText: '',
|
||||
|
@ -133,7 +147,7 @@ export function newComment(
|
|||
};
|
||||
}
|
||||
|
||||
export type CommentUpdate = Partial<Comment>;
|
||||
export type CommentUpdate = Partial<Omit<Comment, 'originalText'>>;
|
||||
|
||||
export interface CommentsState {
|
||||
comments: Map<number, Comment>;
|
||||
|
@ -143,7 +157,7 @@ export interface CommentsState {
|
|||
remoteCommentCount: number;
|
||||
}
|
||||
|
||||
const INITIAL_STATE: CommentsState = {
|
||||
export const INITIAL_STATE: CommentsState = {
|
||||
comments: new Map(),
|
||||
focusedComment: null,
|
||||
pinnedComment: null,
|
||||
|
|
|
@ -44,7 +44,8 @@ window.initTagField = initTagField;
|
|||
|
||||
/*
|
||||
* Enables a "dirty form check", prompting the user if they are navigating away
|
||||
* from a page with unsaved changes.
|
||||
* from a page with unsaved changes, as well as optionally controlling other
|
||||
* behaviour via a callback
|
||||
*
|
||||
* It takes the following parameters:
|
||||
*
|
||||
|
@ -54,30 +55,139 @@ window.initTagField = initTagField;
|
|||
* - confirmationMessage - The message to display in the prompt.
|
||||
* - alwaysDirty - When set to true the form will always be considered dirty,
|
||||
* prompting the user even when nothing has been changed.
|
||||
* - commentApp - The CommentApp used by the commenting system, if the dirty check
|
||||
* should include comments
|
||||
* - callback - A function to be run when the dirty status of the form, or the comments
|
||||
* system (if using) changes, taking formDirty, commentsDirty as arguments
|
||||
*/
|
||||
|
||||
function enableDirtyFormCheck(formSelector, options) {
|
||||
const $form = $(formSelector);
|
||||
const confirmationMessage = options.confirmationMessage || ' ';
|
||||
const alwaysDirty = options.alwaysDirty || false;
|
||||
const commentApp = options.commentApp || null;
|
||||
const callback = options.callback || null;
|
||||
let initialData = null;
|
||||
let formSubmitted = false;
|
||||
|
||||
const updateCallback = (formDirty, commentsDirty) => {
|
||||
if (callback) {
|
||||
callback(formDirty, commentsDirty);
|
||||
}
|
||||
};
|
||||
|
||||
$form.on('submit', () => {
|
||||
formSubmitted = true;
|
||||
});
|
||||
|
||||
let isDirty = alwaysDirty;
|
||||
let isCommentsDirty = false;
|
||||
|
||||
let updateIsCommentsDirtyTimeout = -1;
|
||||
if (commentApp) {
|
||||
isCommentsDirty = commentApp.selectors.selectIsDirty(commentApp.store.getState());
|
||||
commentApp.store.subscribe(() => {
|
||||
// Update on a timeout to match the timings for responding to page form changes
|
||||
clearTimeout(updateIsCommentsDirtyTimeout);
|
||||
updateIsCommentsDirtyTimeout = setTimeout(() => {
|
||||
const newIsCommentsDirty = commentApp.selectors.selectIsDirty(commentApp.store.getState());
|
||||
if (newIsCommentsDirty !== isCommentsDirty) {
|
||||
isCommentsDirty = newIsCommentsDirty;
|
||||
updateCallback(isDirty, isCommentsDirty);
|
||||
}
|
||||
}, isCommentsDirty ? 3000 : 300);
|
||||
});
|
||||
}
|
||||
|
||||
updateCallback(isDirty, isCommentsDirty);
|
||||
|
||||
let updateIsDirtyTimeout = -1;
|
||||
|
||||
const isFormDirty = () => {
|
||||
if (alwaysDirty) {
|
||||
return true;
|
||||
} else if (!initialData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const formData = new FormData($form[0]);
|
||||
const keys = Array.from(formData.keys()).filter((key) => !key.startsWith('comments-'));
|
||||
if (keys.length !== initialData.size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return keys.some((key) => {
|
||||
const newValue = formData.getAll(key);
|
||||
const oldValue = initialData.get(key);
|
||||
if (newValue === oldValue) {
|
||||
return false;
|
||||
} else if (Array.isArray(newValue) && Array.isArray(oldValue)) {
|
||||
return newValue.length !== oldValue.length || newValue.some((value, index) => value !== oldValue[index]);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const updateIsDirty = () => {
|
||||
const previousIsDirty = isDirty;
|
||||
isDirty = isFormDirty();
|
||||
if (previousIsDirty !== isDirty) {
|
||||
updateCallback(isDirty, isCommentsDirty);
|
||||
}
|
||||
};
|
||||
|
||||
// Delay snapshotting the form’s data to avoid race conditions with form widgets that might process the values.
|
||||
// User interaction with the form within that delay also won’t trigger the confirmation message.
|
||||
setTimeout(() => {
|
||||
initialData = $form.serialize();
|
||||
}, 1000 * 10);
|
||||
if (!alwaysDirty) {
|
||||
setTimeout(() => {
|
||||
const initialFormData = new FormData($form[0]);
|
||||
initialData = new Map();
|
||||
Array.from(initialFormData.keys())
|
||||
.filter(key => !key.startsWith('comments-'))
|
||||
.forEach(key => initialData.set(key, initialFormData.getAll(key)));
|
||||
|
||||
const updateDirtyCheck = () => {
|
||||
clearTimeout(updateIsDirtyTimeout);
|
||||
// If the form is dirty, it is relatively unlikely to become clean again, so
|
||||
// run the dirty check on a relatively long timer that we reset on any form update
|
||||
// otherwise, use a short timer both for nicer UX and to ensure widgets
|
||||
// like Draftail have time to serialize their data
|
||||
updateIsDirtyTimeout = setTimeout(updateIsDirty, isDirty ? 3000 : 300);
|
||||
};
|
||||
|
||||
$form.on('change keyup', updateDirtyCheck).trigger('change');
|
||||
|
||||
const validInputNodeInList = (nodeList) => {
|
||||
for (const node of nodeList) {
|
||||
if (node.nodeType === node.ELEMENT_NODE && ['INPUT', 'TEXTAREA', 'SELECT'].includes(node.tagName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const observer = new MutationObserver((mutationList) => {
|
||||
for (const mutation of mutationList) {
|
||||
if (validInputNodeInList(mutation.addedNodes) || validInputNodeInList(mutation.removedNodes)) {
|
||||
updateDirtyCheck();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe($form[0], {
|
||||
childList: true,
|
||||
attributes: false,
|
||||
subtree: true
|
||||
});
|
||||
}, 1000 * 10);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
const isDirty = initialData && $form.serialize() !== initialData;
|
||||
clearTimeout(updateIsDirtyTimeout);
|
||||
updateIsDirty();
|
||||
const displayConfirmation = (
|
||||
!formSubmitted && (alwaysDirty || isDirty)
|
||||
!formSubmitted && (isDirty || isCommentsDirty)
|
||||
);
|
||||
|
||||
if (displayConfirmation) {
|
||||
|
|
|
@ -379,6 +379,42 @@ $(() => {
|
|||
});
|
||||
});
|
||||
|
||||
let updateFooterTextTimeout = -1;
|
||||
window.updateFooterSaveWarning = (formDirty, commentsDirty) => {
|
||||
const warningContainer = $('[data-unsaved-warning]');
|
||||
const warnings = warningContainer.find('[data-unsaved-type]');
|
||||
const anyDirty = formDirty || commentsDirty;
|
||||
const typeVisibility = {
|
||||
all: formDirty && commentsDirty,
|
||||
any: anyDirty,
|
||||
comments: commentsDirty && !formDirty,
|
||||
edits: formDirty && !commentsDirty
|
||||
};
|
||||
|
||||
let hiding = false;
|
||||
if (anyDirty) {
|
||||
warningContainer.removeClass('footer__container--hidden');
|
||||
} else {
|
||||
if (!warningContainer.hasClass('footer__container--hidden')) {
|
||||
hiding = true;
|
||||
}
|
||||
warningContainer.addClass('footer__container--hidden');
|
||||
}
|
||||
clearTimeout(updateFooterTextTimeout);
|
||||
const updateWarnings = () => {
|
||||
for (const warning of warnings) {
|
||||
const visible = typeVisibility[warning.dataset.unsavedType];
|
||||
warning.hidden = !visible;
|
||||
}
|
||||
};
|
||||
if (hiding) {
|
||||
// If hiding, we want to keep the text as-is before it disappears
|
||||
updateFooterTextTimeout = setTimeout(updateWarnings, 1050);
|
||||
} else {
|
||||
updateWarnings();
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports.cleanForSlug = cleanForSlug;
|
||||
}
|
||||
|
|
|
@ -8,3 +8,5 @@ import 'core-js/shim';
|
|||
import 'whatwg-fetch';
|
||||
// IE11.
|
||||
import 'element-closest';
|
||||
// IE11
|
||||
import 'formdata-polyfill';
|
||||
|
|
|
@ -5987,6 +5987,11 @@
|
|||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"formdata-polyfill": {
|
||||
"version": "3.0.20",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-3.0.20.tgz",
|
||||
"integrity": "sha512-TAaxIEwTBdoH1TWndtUH1T0/GisUHwmOKcV5hjkR/iTatHBJSOHb563FP86Lra5nXo3iNdhK7HPwMl5Ihg71pg=="
|
||||
},
|
||||
"fragment-cache": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
|
||||
|
|
|
@ -96,6 +96,7 @@
|
|||
"element-closest": "^2.0.2",
|
||||
"focus-trap-react": "^3.1.0",
|
||||
"immer": "^9.0.1",
|
||||
"formdata-polyfill": "^3.0.20",
|
||||
"postcss-calc": "^7.0.5",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.14.0",
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
{% load wagtailadmin_tags i18n %}
|
||||
<li class="footer__container footer__container--hidden footer__save-warning" data-unsaved-warning>
|
||||
<div hidden data-unsaved-type="any" class="footer__save-warning footer__emphasise-span-tags">
|
||||
<span>
|
||||
{% icon name="warning" class_name="default footer__emphasis" %}
|
||||
</span>
|
||||
<div>
|
||||
<b hidden data-unsaved-type="edits">{% blocktrans %}You have <span>unsaved edits</span>{% endblocktrans %}</b>
|
||||
<b hidden data-unsaved-type="comments">{% blocktrans %}You have <span>unsaved comments</span>{% endblocktrans %}</b>
|
||||
<b hidden data-unsaved-type="all">{% blocktrans %}You have <span>unsaved edits</span> and <span>comments</span>{% endblocktrans %}</b>
|
||||
<p>{% trans 'Save the page before leaving' %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
|
@ -6,7 +6,7 @@
|
|||
{% block bodyclass %}page-editor create model-{{ content_type.model }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="comments"></div>
|
||||
<header class="merged tab-merged">
|
||||
{% explorer_breadcrumb parent_page include_self=1 trailing_arrow=True %}
|
||||
|
||||
|
@ -38,38 +38,43 @@
|
|||
{{ edit_handler.render_form_content }}
|
||||
|
||||
<footer>
|
||||
<nav aria-label="{% trans 'Actions' %}">
|
||||
<ul>
|
||||
<li class="actions actions--primary">
|
||||
<div class="dropdown dropup dropdown-button dropdown-button--white match-width {% if is_revision %}warning{% endif %}">
|
||||
{{ action_menu.render_html }}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{% if preview_modes %}
|
||||
<li class="preview">
|
||||
{% trans 'Preview' as preview_label %}
|
||||
{% if preview_modes|length > 1 %}
|
||||
<div class="dropdown dropup dropdown-button match-width">
|
||||
{% include "wagtailadmin/pages/_preview_button_on_create.html" with label=preview_label icon=1 %}
|
||||
<div class="dropdown-toggle">{% icon name="arrow-up" %}</div>
|
||||
<ul>
|
||||
{% for mode_name, mode_display_name in preview_modes %}
|
||||
<li>
|
||||
{% include "wagtailadmin/pages/_preview_button_on_create.html" with mode=mode_name label=mode_display_name %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="footer__container">
|
||||
<nav aria-label="{% trans 'Actions' %}">
|
||||
<ul>
|
||||
<li class="actions actions--primary">
|
||||
<div class="dropdown dropup dropdown-button dropdown-button--white match-width {% if is_revision %}warning{% endif %}">
|
||||
{{ action_menu.render_html }}
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "wagtailadmin/pages/_preview_button_on_create.html" with label=preview_label icon=1 %}
|
||||
</li>
|
||||
|
||||
{% if preview_modes %}
|
||||
<li class="preview">
|
||||
{% trans 'Preview' as preview_label %}
|
||||
{% if preview_modes|length > 1 %}
|
||||
<div class="dropdown dropup dropdown-button match-width">
|
||||
{% include "wagtailadmin/pages/_preview_button_on_create.html" with label=preview_label icon=1 %}
|
||||
<div class="dropdown-toggle">{% icon name="arrow-up" %}</div>
|
||||
<ul>
|
||||
{% for mode_name, mode_display_name in preview_modes %}
|
||||
<li>
|
||||
{% include "wagtailadmin/pages/_preview_button_on_create.html" with mode=mode_name label=mode_display_name %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "wagtailadmin/pages/_preview_button_on_create.html" with label=preview_label icon=1 %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% block extra_footer_actions %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% block extra_footer_actions %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
</nav>
|
||||
</li>
|
||||
{% include "wagtailadmin/pages/_unsaved_changes_warning.html" %}
|
||||
</ul>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
|
@ -111,6 +116,8 @@
|
|||
{% if has_unsaved_changes %}
|
||||
alwaysDirty: true,
|
||||
{% endif %}
|
||||
commentApp: window.commentApp,
|
||||
callback: window.updateFooterSaveWarning
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -54,16 +54,18 @@
|
|||
{% endif %}
|
||||
|
||||
<footer>
|
||||
<nav aria-label="{% trans 'Actions' %}">
|
||||
<ul>
|
||||
<li class="actions actions--primary">
|
||||
<div class="dropdown dropup dropdown-button match-width {% if is_revision %}warning{% endif %}">
|
||||
<ul>
|
||||
<li class="footer__container">
|
||||
<nav aria-label="{% trans 'Actions' %}">
|
||||
<ul>
|
||||
<li class="actions actions--primary">
|
||||
<div class="dropdown dropup dropdown-button match-width {% if is_revision %}warning{% endif %}">
|
||||
{{ action_menu.render_html }}
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{% if preview_modes %}
|
||||
<li class="preview">
|
||||
{% if preview_modes %}
|
||||
<li class="preview">
|
||||
{% trans 'Preview' as preview_label %}
|
||||
{% if preview_modes|length > 1 %}
|
||||
<div class="dropdown dropup dropdown-button match-width">
|
||||
|
@ -80,13 +82,16 @@
|
|||
{% else %}
|
||||
{% include "wagtailadmin/pages/_preview_button_on_edit.html" with label=preview_label icon=1 %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% block extra_footer_actions %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% block extra_footer_actions %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
</nav>
|
||||
</li>
|
||||
{% include "wagtailadmin/pages/_unsaved_changes_warning.html" %}
|
||||
</ul>
|
||||
</footer>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
@ -158,6 +163,9 @@
|
|||
{% if has_unsaved_changes %}
|
||||
alwaysDirty: true,
|
||||
{% endif %}
|
||||
|
||||
commentApp: window.commentApp,
|
||||
callback: window.updateFooterSaveWarning
|
||||
}
|
||||
);
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue