1
Fork 0

Hacky "fix" for 4.1.1 support for now

This commit is contained in:
Jason Dilworth 2023-02-20 11:00:31 +00:00
parent c32a8d7498
commit ad7facef5c
4 changed files with 342 additions and 225 deletions

View file

@ -1,13 +1,13 @@
from django.apps import apps from django.apps import apps
from django.template.loader import render_to_string from django.template.loader import render_to_string
from draftjs_exporter.dom import DOM from draftjs_exporter.dom import DOM
from wagtail.admin.rich_text.converters.contentstate_models import Entity from wagtail.admin.rich_text.converters.contentstate_models import Entity
from wagtail.admin.rich_text.converters.html_to_contentstate import LinkElementHandler, AtomicBlockEntityElementHandler from wagtail.admin.rich_text.converters.html_to_contentstate import (
AtomicBlockEntityElementHandler, LinkElementHandler)
from wagtail.core.rich_text import EmbedHandler, LinkHandler from wagtail.core.rich_text import EmbedHandler, LinkHandler
from .utils import get_snippet_link_frontend_template, get_snippet_embed_frontend_template from .utils import (get_snippet_embed_frontend_template,
get_snippet_link_frontend_template)
# Snippet Link # Snippet Link

View file

@ -1,15 +1,21 @@
SNIPPET_MODEL_CHOOSER_MODAL_ONLOAD_HANDLERS = { SNIPPET_MODEL_CHOOSER_MODAL_ONLOAD_HANDLERS = {
'choose': function(modal, jsonData) { choose: function (modal, jsonData) {
function getSelectedModelMeta(context) { function getSelectedModelMeta(context) {
$('a.snippet-model-choice', modal.body).on('click', function(event) { $("a.snippet-model-choice", modal.body).on(
event.preventDefault(); "click",
let modelMeta = {'appName': this.dataset.appName, 'modelName': this.dataset.modelName}; function (event) {
modal.respond('snippetModelChosen', modelMeta); event.preventDefault();
modal.close(); let modelMeta = {
$(".modal-backdrop").remove(); appName: this.dataset.appName,
}); modelName: this.dataset.modelName,
} };
modal.respond("snippetModelChosen", modelMeta);
modal.close();
$(".modal-backdrop").remove();
}
);
}
getSelectedModelMeta(modal.body); getSelectedModelMeta(modal.body);
}, },
}; };

View file

@ -1,224 +1,339 @@
(() => { (() => {
'use strict'; "use strict";
const React = window.React; const React = window.React;
const Modifier = window.DraftJS.Modifier; const Modifier = window.DraftJS.Modifier;
const AtomicBlockUtils = window.DraftJS.AtomicBlockUtils; const AtomicBlockUtils = window.DraftJS.AtomicBlockUtils;
const RichUtils = window.DraftJS.RichUtils; const RichUtils = window.DraftJS.RichUtils;
const EditorState = window.DraftJS.EditorState; const EditorState = window.DraftJS.EditorState;
const TooltipEntity = window.draftail.TooltipEntity; const TooltipEntity = window.draftail.TooltipEntity;
const global = globalThis; const global = globalThis;
const $ = global.jQuery; const $ = global.jQuery;
const MUTABILITY = {}; global.TEST_SNIPPET_CHOOSER_MODAL_ONLOAD_HANDLERS = {
MUTABILITY['SNIPPET'] = 'MUTABLE'; choose: function (modal, jsonData) {
MUTABILITY['SNIPPET-EMBED'] = 'IMMUTABLE'; function ajaxifyLinks(context) {
$("a.snippet-choice", modal.body).on("click", function () {
modal.loadUrl(this.href);
return false;
});
const getSnippetModelChooserConfig = (entityType) => { $("[data-chooser-modal-choice]", context).on(
let url; "click",
let urlParams; function (e) {
e.preventDefault();
if (entityType.type === 'SNIPPET') { global.TEST_SNIPPET_CHOOSER_MODAL_ONLOAD_HANDLERS.chosen(
return { modal,
url: global.chooserUrls.snippetLinkModelChooser, modal.loadUrl(this.href)
urlParams: {}, );
onload: global.SNIPPET_MODEL_CHOOSER_MODAL_ONLOAD_HANDLERS, }
}; );
}
else if (entityType.type === 'SNIPPET-EMBED') {
return {
url: global.chooserUrls.snippetEmbedModelChooser,
urlParams: {},
onload: global.SNIPPET_MODEL_CHOOSER_MODAL_ONLOAD_HANDLERS,
};
}
else {
return {
url: null,
urlParams: {},
onload: {},
};
}
};
const getSnippetModelObjectChooserConfig = () => { $(".pagination a", context).on("click", function () {
let url; loadResults(this.href);
let urlParams; return false;
});
}
return { var searchForm$ = $("form.search-bar", modal.body);
url: global.chooserUrls.snippetChooser.concat(window.snippetModelMeta.appName, '/', window.snippetModelMeta.modelName, '/'), var searchUrl = searchForm$.attr("action");
urlParams: {}, var request;
onload: global.SNIPPET_CHOOSER_MODAL_ONLOAD_HANDLERS,
function search() {
loadResults(searchUrl, searchForm$.serialize());
return false;
}
function loadResults(url, data) {
var opts = {
url: url,
success: function (data, status) {
request = null;
$("#search-results").html(data);
ajaxifyLinks($("#search-results"));
},
error: function () {
request = null;
},
};
if (data) {
opts.data = data;
}
request = $.ajax(opts);
}
searchForm$.on("submit", search);
$("#snippet-chooser-locale", modal.body).on("change", search);
$("#id_q").on("input", function () {
if (request) {
request.abort();
}
clearTimeout($.data(this, "timer"));
var wait = setTimeout(search, 200);
$(this).data("timer", wait);
});
ajaxifyLinks(modal.body);
},
chosen: function (modal, jsonData) {
// alert(`SNIPPET CHOSEN`);
if (jsonData) {
modal.respond("snippetChosen", jsonData["result"]);
modal.close();
}
},
}; };
};
const filterSnippetEntityData = (entityType, data) => { const MUTABILITY = {};
return { MUTABILITY["SNIPPET"] = "MUTABLE";
edit_link: data.edit_link, MUTABILITY["SNIPPET-EMBED"] = "IMMUTABLE";
string: data.string,
id: data.id,
app_name: window.snippetModelMeta.appName,
model_name: window.snippetModelMeta.modelName,
};
};
/** const getSnippetModelChooserConfig = (entityType) => {
* Interfaces with Wagtail's ModalWorkflow to open the chooser, let url;
* and create new content in Draft.js based on the data. let urlParams;
*/
class SnippetModalWorkflowSource extends React.Component {
constructor(props) {
super(props);
this.onChosen = this.onChosen.bind(this); if (entityType.type === "SNIPPET") {
this.onClose = this.onClose.bind(this); return {
this.onModelChosen = this.onModelChosen.bind(this); url: global.chooserUrls.snippetLinkModelChooser,
} urlParams: {},
onload: global.SNIPPET_MODEL_CHOOSER_MODAL_ONLOAD_HANDLERS,
componentDidMount() { };
const { onClose, entityType, entity, editorState } = this.props; } else if (entityType.type === "SNIPPET-EMBED") {
const { url, urlParams, onload } = getSnippetModelChooserConfig(entityType); return {
url: global.chooserUrls.snippetEmbedModelChooser,
$(document.body).on('hidden.bs.modal', this.onClose); urlParams: {},
onload: global.SNIPPET_MODEL_CHOOSER_MODAL_ONLOAD_HANDLERS,
// eslint-disable-next-line new-cap };
this.model_workflow = global.ModalWorkflow({
url,
urlParams,
onload,
responses: {
snippetModelChosen: this.onModelChosen,
},
onError: () => {
// eslint-disable-next-line no-alert
window.alert(global.wagtailConfig.STRINGS.SERVER_ERROR);
onClose();
},
});
}
componentWillUnmount() {
this.model_workflow = null;
this.workflow = null;
$(document.body).off('hidden.bs.modal', this.onClose);
}
onModelChosen(snippetModelMeta) {
window.snippetModelMeta = snippetModelMeta;
const { url, urlParams, onload } = getSnippetModelObjectChooserConfig();
this.model_workflow.close();
// eslint-disable-next-line new-cap
this.workflow = global.ModalWorkflow({
url,
urlParams,
onload,
responses: {
snippetChosen: this.onChosen,
},
onError: () => {
// eslint-disable-next-line no-alert
window.alert(global.wagtailConfig.STRINGS.SERVER_ERROR);
onClose();
},
});
}
onChosen(data) {
const { editorState, entityType, onComplete } = this.props;
const content = editorState.getCurrentContent();
const selection = editorState.getSelection();
const entityData = filterSnippetEntityData(entityType, data);
const mutability = MUTABILITY[entityType.type];
const contentWithEntity = content.createEntity(entityType.type, mutability, entityData);
const entityKey = contentWithEntity.getLastCreatedEntityKey();
let nextState;
if (entityType.block) {
// Only supports adding entities at the moment, not editing existing ones.
// See https://github.com/springload/draftail/blob/cdc8988fe2e3ac32374317f535a5338ab97e8637/examples/sources/ImageSource.js#L44-L62.
// See https://github.com/springload/draftail/blob/cdc8988fe2e3ac32374317f535a5338ab97e8637/examples/sources/EmbedSource.js#L64-L91
nextState = AtomicBlockUtils.insertAtomicBlock(editorState, entityKey, ' ');
} else {
// Replace text if the chooser demands it, or if there is no selected text in the first place.
const shouldReplaceText = data.prefer_this_title_as_link_text || selection.isCollapsed();
if (shouldReplaceText) {
// If there is a title attribute, use it. Otherwise we inject the URL.
const newText = data.string;
const newContent = Modifier.replaceText(content, selection, newText, null, entityKey);
nextState = EditorState.push(editorState, newContent, 'insert-characters');
} else { } else {
nextState = RichUtils.toggleLink(editorState, selection, entityKey); return {
url: null,
urlParams: {},
onload: {},
};
} }
} };
// IE11 crashes when rendering the new entity in contenteditable if the modal is still open. const getSnippetModelObjectChooserConfig = () => {
// Other browsers do not mind. This is probably a focus management problem. let url;
// From the user's perspective, this is all happening too fast to notice either way. let urlParams;
if (this.workflow) {
this.workflow.close();
}
onComplete(nextState); return {
url: global.chooserUrls.snippetChooser.concat(
window.snippetModelMeta.appName,
"/",
window.snippetModelMeta.modelName,
"/"
),
urlParams: {},
onload: TEST_SNIPPET_CHOOSER_MODAL_ONLOAD_HANDLERS,
};
};
const filterSnippetEntityData = (entityType, data) => {
return {
edit_link: data.edit_link,
string: data.string,
id: data.id,
app_name: window.snippetModelMeta.appName,
model_name: window.snippetModelMeta.modelName,
};
};
/**
* Interfaces with Wagtail's ModalWorkflow to open the chooser,
* and create new content in Draft.js based on the data.
*/
class SnippetModalWorkflowSource extends React.Component {
constructor(props) {
super(props);
this.onChosen = this.onChosen.bind(this);
this.onClose = this.onClose.bind(this);
this.onModelChosen = this.onModelChosen.bind(this);
}
componentDidMount() {
const { onClose, entityType, entity, editorState } = this.props;
const { url, urlParams, onload } =
getSnippetModelChooserConfig(entityType);
$(document.body).on("hidden.bs.modal", this.onClose);
// eslint-disable-next-line new-cap
this.model_workflow = global.ModalWorkflow({
url,
urlParams,
onload,
responses: {
snippetModelChosen: this.onModelChosen,
},
onError: () => {
// eslint-disable-next-line no-alert
window.alert(global.wagtailConfig.STRINGS.SERVER_ERROR);
onClose();
},
});
}
componentWillUnmount() {
this.model_workflow = null;
this.workflow = null;
$(document.body).off("hidden.bs.modal", this.onClose);
}
onModelChosen(snippetModelMeta) {
window.snippetModelMeta = snippetModelMeta;
const { url, urlParams, onload } =
getSnippetModelObjectChooserConfig();
this.model_workflow.close();
// eslint-disable-next-line new-cap
this.workflow = global.ModalWorkflow({
url,
urlParams,
onload,
responses: {
snippetChosen: this.onChosen,
chosen: this.onChosen,
},
onError: () => {
// eslint-disable-next-line no-alert
window.alert(global.wagtailConfig.STRINGS.SERVER_ERROR);
onClose();
},
});
}
onChosen(data) {
const { editorState, entityType, onComplete } = this.props;
const content = editorState.getCurrentContent();
const selection = editorState.getSelection();
const entityData = filterSnippetEntityData(entityType, data);
const mutability = MUTABILITY[entityType.type];
const contentWithEntity = content.createEntity(
entityType.type,
mutability,
entityData
);
const entityKey = contentWithEntity.getLastCreatedEntityKey();
let nextState;
if (entityType.block) {
// Only supports adding entities at the moment, not editing existing ones.
// See https://github.com/springload/draftail/blob/cdc8988fe2e3ac32374317f535a5338ab97e8637/examples/sources/ImageSource.js#L44-L62.
// See https://github.com/springload/draftail/blob/cdc8988fe2e3ac32374317f535a5338ab97e8637/examples/sources/EmbedSource.js#L64-L91
nextState = AtomicBlockUtils.insertAtomicBlock(
editorState,
entityKey,
" "
);
} else {
// Replace text if the chooser demands it, or if there is no selected text in the first place.
const shouldReplaceText =
data.prefer_this_title_as_link_text ||
selection.isCollapsed();
if (shouldReplaceText) {
// If there is a title attribute, use it. Otherwise we inject the URL.
const newText = data.string;
const newContent = Modifier.replaceText(
content,
selection,
newText,
null,
entityKey
);
nextState = EditorState.push(
editorState,
newContent,
"insert-characters"
);
} else {
nextState = RichUtils.toggleLink(
editorState,
selection,
entityKey
);
}
}
// IE11 crashes when rendering the new entity in contenteditable if the modal is still open.
// Other browsers do not mind. This is probably a focus management problem.
// From the user's perspective, this is all happening too fast to notice either way.
if (this.workflow) {
this.workflow.close();
}
onComplete(nextState);
}
onClose(e) {
const { onClose } = this.props;
e.preventDefault();
onClose();
}
render() {
return null;
}
} }
onClose(e) { const SnippetLink = (props) => {
const { onClose } = this.props; const { entityKey, contentState } = props;
e.preventDefault(); const data = contentState.getEntity(entityKey).getData();
onClose(); let icon = React.createElement(window.wagtail.components.Icon, {
} name: "snippet",
});
let label = data.string || "";
render() { return React.createElement(TooltipEntity, {
return null; entityKey: props.entityKey,
} children: props.children,
} onEdit: props.onEdit,
onRemove: props.onRemove,
icon: icon,
label: label,
});
};
const SnippetLink = props => { const SnippetEmbed = (props) => {
const { entityKey, contentState } = props; const { entity, onRemoveEntity, entityKey } = props.blockProps;
const data = contentState.getEntity(entityKey).getData(); const data = entity.getData();
let icon = React.createElement(window.wagtail.components.Icon, {name: 'snippet'}); let icon = React.createElement(window.wagtail.components.Icon, {
let label = data.string || ''; name: "snippet",
});
let label = data.string || "";
return React.createElement(TooltipEntity, { return React.createElement(
entityKey: props.entityKey, "div",
children: props.children, {
onEdit: props.onEdit, class: "MediaBlock",
onRemove: props.onRemove, },
icon: icon, icon,
label: label `${label}`
);
};
window.draftail.registerPlugin({
type: "SNIPPET",
source: SnippetModalWorkflowSource,
decorator: SnippetLink,
}); });
};
const SnippetEmbed = props => { window.draftail.registerPlugin({
const { entity, onRemoveEntity, entityKey } = props.blockProps; type: "SNIPPET-EMBED",
const data = entity.getData(); source: SnippetModalWorkflowSource,
block: SnippetEmbed,
let icon = React.createElement(window.wagtail.components.Icon, {name: 'snippet'}); });
let label = data.string || '';
return React.createElement("div", {
class: "MediaBlock"
}, icon, `${label}`);
};
window.draftail.registerPlugin({
type: 'SNIPPET',
source: SnippetModalWorkflowSource,
decorator: SnippetLink,
});
window.draftail.registerPlugin({
type: 'SNIPPET-EMBED',
source: SnippetModalWorkflowSource,
block: SnippetEmbed,
});
})(); })();

View file

@ -1,18 +1,14 @@
from django.urls import include, path from django.urls import include, path, reverse
from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext from django.utils.translation import gettext
from wagtail.admin.rich_text.editors.draftail import \
from wagtail.admin.rich_text.editors.draftail import features as draftail_features features as draftail_features
from wagtail.core import hooks from wagtail.core import hooks
from . import urls from . import urls
from .richtext import ( from .richtext import (ContentstateSnippetEmbedConversionRule,
ContentstateSnippetLinkConversionRule, ContentstateSnippetLinkConversionRule,
ContentstateSnippetEmbedConversionRule, SnippetEmbedHandler, SnippetLinkHandler)
SnippetLinkHandler,
SnippetEmbedHandler,
)
@hooks.register("register_rich_text_features") @hooks.register("register_rich_text_features")
@ -28,7 +24,7 @@ def register_snippet_link_feature(features):
draftail_features.EntityFeature( draftail_features.EntityFeature(
{"type": type_, "icon": "snippet", "description": gettext("Snippet Link")}, {"type": type_, "icon": "snippet", "description": gettext("Snippet Link")},
js=[ js=[
"wagtailsnippets/js/snippet-chooser-modal.js", "wagtailsnippets/js/snippet-chooser.js",
"wagtail_draftail_snippet/js/snippet-model-chooser-modal.js", "wagtail_draftail_snippet/js/snippet-model-chooser-modal.js",
"wagtail_draftail_snippet/js/wagtail-draftail-snippet.js", "wagtail_draftail_snippet/js/wagtail-draftail-snippet.js",
], ],
@ -53,7 +49,7 @@ def register_snippet_embed_feature(features):
draftail_features.EntityFeature( draftail_features.EntityFeature(
{"type": type_, "icon": "code", "description": gettext("Snippet Embed")}, {"type": type_, "icon": "code", "description": gettext("Snippet Embed")},
js=[ js=[
"wagtailsnippets/js/snippet-chooser-modal.js", "wagtailsnippets/js/snippet-chooser.js",
"wagtail_draftail_snippet/js/snippet-model-chooser-modal.js", "wagtail_draftail_snippet/js/snippet-model-chooser-modal.js",
"wagtail_draftail_snippet/js/wagtail-draftail-snippet.js", "wagtail_draftail_snippet/js/wagtail-draftail-snippet.js",
], ],