From ad7facef5cbd653296ddadfb2bfb8cbef3fd7296 Mon Sep 17 00:00:00 2001 From: Jason Dilworth Date: Mon, 20 Feb 2023 11:00:31 +0000 Subject: [PATCH] Hacky "fix" for 4.1.1 support for now --- wagtail_draftail_snippet/richtext.py | 8 +- .../js/snippet-model-chooser-modal.js | 30 +- .../js/wagtail-draftail-snippet.js | 509 +++++++++++------- wagtail_draftail_snippet/wagtail_hooks.py | 20 +- 4 files changed, 342 insertions(+), 225 deletions(-) diff --git a/wagtail_draftail_snippet/richtext.py b/wagtail_draftail_snippet/richtext.py index 5320403..57a4ccc 100644 --- a/wagtail_draftail_snippet/richtext.py +++ b/wagtail_draftail_snippet/richtext.py @@ -1,13 +1,13 @@ from django.apps import apps from django.template.loader import render_to_string - from draftjs_exporter.dom import DOM 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 .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 diff --git a/wagtail_draftail_snippet/static/wagtail_draftail_snippet/js/snippet-model-chooser-modal.js b/wagtail_draftail_snippet/static/wagtail_draftail_snippet/js/snippet-model-chooser-modal.js index 640be2a..7ff0f1c 100644 --- a/wagtail_draftail_snippet/static/wagtail_draftail_snippet/js/snippet-model-chooser-modal.js +++ b/wagtail_draftail_snippet/static/wagtail_draftail_snippet/js/snippet-model-chooser-modal.js @@ -1,15 +1,21 @@ SNIPPET_MODEL_CHOOSER_MODAL_ONLOAD_HANDLERS = { - 'choose': function(modal, jsonData) { - function getSelectedModelMeta(context) { - $('a.snippet-model-choice', modal.body).on('click', function(event) { - event.preventDefault(); - let modelMeta = {'appName': this.dataset.appName, 'modelName': this.dataset.modelName}; - modal.respond('snippetModelChosen', modelMeta); - modal.close(); - $(".modal-backdrop").remove(); - }); - } + choose: function (modal, jsonData) { + function getSelectedModelMeta(context) { + $("a.snippet-model-choice", modal.body).on( + "click", + function (event) { + event.preventDefault(); + let modelMeta = { + appName: this.dataset.appName, + modelName: this.dataset.modelName, + }; + modal.respond("snippetModelChosen", modelMeta); + modal.close(); + $(".modal-backdrop").remove(); + } + ); + } - getSelectedModelMeta(modal.body); - }, + getSelectedModelMeta(modal.body); + }, }; diff --git a/wagtail_draftail_snippet/static/wagtail_draftail_snippet/js/wagtail-draftail-snippet.js b/wagtail_draftail_snippet/static/wagtail_draftail_snippet/js/wagtail-draftail-snippet.js index d5696de..20fd8da 100644 --- a/wagtail_draftail_snippet/static/wagtail_draftail_snippet/js/wagtail-draftail-snippet.js +++ b/wagtail_draftail_snippet/static/wagtail_draftail_snippet/js/wagtail-draftail-snippet.js @@ -1,224 +1,339 @@ (() => { - 'use strict'; + "use strict"; - const React = window.React; - const Modifier = window.DraftJS.Modifier; - const AtomicBlockUtils = window.DraftJS.AtomicBlockUtils; - const RichUtils = window.DraftJS.RichUtils; - const EditorState = window.DraftJS.EditorState; + const React = window.React; + const Modifier = window.DraftJS.Modifier; + const AtomicBlockUtils = window.DraftJS.AtomicBlockUtils; + const RichUtils = window.DraftJS.RichUtils; + const EditorState = window.DraftJS.EditorState; - const TooltipEntity = window.draftail.TooltipEntity; + const TooltipEntity = window.draftail.TooltipEntity; - const global = globalThis; - const $ = global.jQuery; + const global = globalThis; + const $ = global.jQuery; - const MUTABILITY = {}; - MUTABILITY['SNIPPET'] = 'MUTABLE'; - MUTABILITY['SNIPPET-EMBED'] = 'IMMUTABLE'; + global.TEST_SNIPPET_CHOOSER_MODAL_ONLOAD_HANDLERS = { + choose: function (modal, jsonData) { + function ajaxifyLinks(context) { + $("a.snippet-choice", modal.body).on("click", function () { + modal.loadUrl(this.href); + return false; + }); - const getSnippetModelChooserConfig = (entityType) => { - let url; - let urlParams; + $("[data-chooser-modal-choice]", context).on( + "click", + function (e) { + e.preventDefault(); - if (entityType.type === 'SNIPPET') { - return { - url: global.chooserUrls.snippetLinkModelChooser, - 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: {}, - }; - } - }; + global.TEST_SNIPPET_CHOOSER_MODAL_ONLOAD_HANDLERS.chosen( + modal, + modal.loadUrl(this.href) + ); + } + ); - const getSnippetModelObjectChooserConfig = () => { - let url; - let urlParams; + $(".pagination a", context).on("click", function () { + loadResults(this.href); + return false; + }); + } - return { - url: global.chooserUrls.snippetChooser.concat(window.snippetModelMeta.appName, '/', window.snippetModelMeta.modelName, '/'), - urlParams: {}, - onload: global.SNIPPET_CHOOSER_MODAL_ONLOAD_HANDLERS, + var searchForm$ = $("form.search-bar", modal.body); + var searchUrl = searchForm$.attr("action"); + var request; + + 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) => { - return { - edit_link: data.edit_link, - string: data.string, - id: data.id, - app_name: window.snippetModelMeta.appName, - model_name: window.snippetModelMeta.modelName, - }; - }; + const MUTABILITY = {}; + MUTABILITY["SNIPPET"] = "MUTABLE"; + MUTABILITY["SNIPPET-EMBED"] = "IMMUTABLE"; - /** - * 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); + const getSnippetModelChooserConfig = (entityType) => { + let url; + let urlParams; - 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, - }, - 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'); + if (entityType.type === "SNIPPET") { + return { + url: global.chooserUrls.snippetLinkModelChooser, + 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 { - 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. - // 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(); - } + const getSnippetModelObjectChooserConfig = () => { + let url; + let urlParams; - 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 { onClose } = this.props; - e.preventDefault(); + const SnippetLink = (props) => { + const { entityKey, contentState } = props; + const data = contentState.getEntity(entityKey).getData(); - onClose(); - } + let icon = React.createElement(window.wagtail.components.Icon, { + name: "snippet", + }); + let label = data.string || ""; - render() { - return null; - } - } + return React.createElement(TooltipEntity, { + entityKey: props.entityKey, + children: props.children, + onEdit: props.onEdit, + onRemove: props.onRemove, + icon: icon, + label: label, + }); + }; - const SnippetLink = props => { - const { entityKey, contentState } = props; - const data = contentState.getEntity(entityKey).getData(); + const SnippetEmbed = (props) => { + const { entity, onRemoveEntity, entityKey } = props.blockProps; + const data = entity.getData(); - let icon = React.createElement(window.wagtail.components.Icon, {name: 'snippet'}); - let label = data.string || ''; + let icon = React.createElement(window.wagtail.components.Icon, { + name: "snippet", + }); + let label = data.string || ""; - return React.createElement(TooltipEntity, { - entityKey: props.entityKey, - children: props.children, - onEdit: props.onEdit, - onRemove: props.onRemove, - icon: icon, - label: label + return React.createElement( + "div", + { + class: "MediaBlock", + }, + icon, + `${label}` + ); + }; + + window.draftail.registerPlugin({ + type: "SNIPPET", + source: SnippetModalWorkflowSource, + decorator: SnippetLink, }); - }; - const SnippetEmbed = props => { - const { entity, onRemoveEntity, entityKey } = props.blockProps; - const data = entity.getData(); - - 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, - }); + window.draftail.registerPlugin({ + type: "SNIPPET-EMBED", + source: SnippetModalWorkflowSource, + block: SnippetEmbed, + }); })(); diff --git a/wagtail_draftail_snippet/wagtail_hooks.py b/wagtail_draftail_snippet/wagtail_hooks.py index e5c696e..806f9fc 100644 --- a/wagtail_draftail_snippet/wagtail_hooks.py +++ b/wagtail_draftail_snippet/wagtail_hooks.py @@ -1,18 +1,14 @@ -from django.urls import include, path -from django.urls import reverse +from django.urls import include, path, reverse from django.utils.html import format_html from django.utils.translation import gettext - -from wagtail.admin.rich_text.editors.draftail import features as draftail_features +from wagtail.admin.rich_text.editors.draftail import \ + features as draftail_features from wagtail.core import hooks from . import urls -from .richtext import ( - ContentstateSnippetLinkConversionRule, - ContentstateSnippetEmbedConversionRule, - SnippetLinkHandler, - SnippetEmbedHandler, -) +from .richtext import (ContentstateSnippetEmbedConversionRule, + ContentstateSnippetLinkConversionRule, + SnippetEmbedHandler, SnippetLinkHandler) @hooks.register("register_rich_text_features") @@ -28,7 +24,7 @@ def register_snippet_link_feature(features): draftail_features.EntityFeature( {"type": type_, "icon": "snippet", "description": gettext("Snippet Link")}, 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/wagtail-draftail-snippet.js", ], @@ -53,7 +49,7 @@ def register_snippet_embed_feature(features): draftail_features.EntityFeature( {"type": type_, "icon": "code", "description": gettext("Snippet Embed")}, 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/wagtail-draftail-snippet.js", ],