0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-12-01 11:41:20 +01:00

Add ability to edit links in RichText editor

Instead of having the link button to remove an existing link,
it will open the chooser on the corresponding tab (page, link, email)
and pre-populate the form.
Therefore, a second button has been added to remove a link.
This commit is contained in:
Loic Teixeira 2016-03-29 16:10:37 +13:00 committed by Matt Westcott
parent 54e9e9c918
commit 32c68b5060
11 changed files with 748 additions and 440 deletions

View File

@ -55,19 +55,11 @@ class SearchForm(forms.Form):
class ExternalLinkChooserForm(forms.Form):
url = URLOrAbsolutePathField(required=True, label=ugettext_lazy("URL"))
class ExternalLinkChooserWithLinkTextForm(forms.Form):
url = URLOrAbsolutePathField(required=True, label=ugettext_lazy("URL"))
link_text = forms.CharField(required=True)
link_text = forms.CharField(required=False)
class EmailLinkChooserForm(forms.Form):
email_address = forms.EmailField(required=True)
class EmailLinkChooserWithLinkTextForm(forms.Form):
email_address = forms.EmailField(required=True)
link_text = forms.CharField(required=False)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -7,7 +7,7 @@
editable: null
},
populateToolbar: function(toolbar) {
var button, getEnclosingLink, widget;
var buttonSet, addButton, cancelButton, getEnclosingLink, widget;
widget = this;
getEnclosingLink = function() {
@ -17,61 +17,135 @@
return $(node).parents('a').get(0);
};
button = $('<span class="' + this.widgetName + '"></span>');
button.hallobutton({
uuid: this.options.uuid,
editable: this.options.editable,
label: 'Links',
buttonSet = $('<span class="' + this.widgetName + '"></span>');
addButton = $('<span></span>');
addButton = addButton.hallobutton({
uuid: widget.options.uuid,
editable: widget.options.editable,
label: 'Add/Edit Link',
icon: 'icon-link',
command: null,
queryState: function(event) {
return button.hallobutton('checked', !!getEnclosingLink());
return addButton.hallobutton('checked', !!getEnclosingLink());
}
});
addButton.on('click', function() {
var enclosingLink, lastSelection, url, urlParams, href, pageId;
toolbar.append(button);
return button.on('click', function(event) {
var enclosingLink, lastSelection, url;
// Defaults.
url = window.chooserUrls.pageChooser;
urlParams = {
'allow_external_link': true,
'allow_email_link': true
};
enclosingLink = getEnclosingLink();
lastSelection = widget.options.editable.getSelection();
if (enclosingLink) {
href = enclosingLink.getAttribute('href');
pageId = enclosingLink.getAttribute('data-id');
urlParams['link_text'] = enclosingLink.innerText;
if (pageId) {
// TODO: Actually show the parent not the page itself.
url = window.chooserUrls.pageChooser + pageId.toString() + '/';
} else if (href.startsWith('mailto:')) {
url = window.chooserUrls.emailLinkChooser;
href = href.replace('mailto:', '');
urlParams['link_url'] = href;
} else {
url = window.chooserUrls.externalLinkChooser;
urlParams['link_url'] = href;
}
} else if (!lastSelection.collapsed) {
urlParams['link_text'] = lastSelection.toString();
}
return ModalWorkflow({
url: url,
urlParams: urlParams,
responses: {
pageChosen: function(pageData) {
var a, text;
// Create link
a = document.createElement('a');
a.setAttribute('href', pageData.url);
if (pageData.id) {
a.setAttribute('data-id', pageData.id);
a.setAttribute('data-linktype', 'page');
}
if (pageData.id) {
// If it's a link to an internal page, `pageData.title` will not use the link_text
// like external and email responses do, overwriting selection text :(
if (!lastSelection.collapsed) {
text = lastSelection.toString();
} else if (enclosingLink) {
text = enclosingLink.innerHTML;
}
else {
text = pageData.title;
}
} else {
text = pageData.title;
}
a.appendChild(document.createTextNode(text));
// Remove existing nodes
if (enclosingLink && enclosingLink.parentNode) {
enclosingLink.parentNode.removeChild(enclosingLink);
}
lastSelection.deleteContents();
// Add new node
lastSelection.insertNode(a);
return widget.options.editable.element.trigger('change');
}
}
});
});
buttonSet.append(addButton);
cancelButton = $('<span></span>');
cancelButton = cancelButton.hallobutton({
uuid: widget.options.uuid,
editable: widget.options.editable,
label: 'Remove Link',
icon: 'icon-chain-broken',
command: null,
queryState: function(event) {
if (!!getEnclosingLink()) {
return cancelButton.hallobutton('enable');
} else {
return cancelButton.hallobutton('disable');
}
}
});
cancelButton.on('click', function() {
var enclosingLink, sel, range;
enclosingLink = getEnclosingLink();
if (enclosingLink) {
$(enclosingLink).replaceWith(enclosingLink.innerHTML);
button.hallobutton('checked', false);
return widget.options.editable.element.trigger('change');
} else {
lastSelection = widget.options.editable.getSelection();
if (lastSelection.collapsed) {
url = window.chooserUrls.pageChooser + '?allow_external_link=true&allow_email_link=true&prompt_for_link_text=true';
} else {
url = window.chooserUrls.pageChooser + '?allow_external_link=true&allow_email_link=true';
}
sel = rangy.getSelection();
range = sel.getRangeAt(0);
return ModalWorkflow({
url: url,
responses: {
pageChosen: function(pageData) {
var a;
range.setStartBefore(sel.anchorNode.parentNode);
range.setEndAfter(sel.anchorNode.parentNode);
a = document.createElement('a');
a.setAttribute('href', pageData.url);
if (pageData.id) {
a.setAttribute('data-id', pageData.id);
a.setAttribute('data-linktype', 'page');
}
sel.setSingleRange(range, false);
if ((!lastSelection.collapsed) && lastSelection.canSurroundContents()) {
lastSelection.surroundContents(a);
} else {
a.appendChild(document.createTextNode(pageData.title));
lastSelection.insertNode(a);
}
return widget.options.editable.element.trigger('change');
}
}
});
document.execCommand('unlink');
}
});
buttonSet.append(cancelButton);
buttonSet.hallobuttonset();
toolbar.append(buttonSet);
}
});
})(jQuery);

View File

@ -72,7 +72,8 @@ $icons: (
'title': '\f034',
'code': '\e601',
'openquote': '',
'horizontalrule': '\2014'
'horizontalrule': '\2014',
'chain-broken': '\e900'
);
$icons-after: (

View File

@ -6,7 +6,9 @@
<script>
window.chooserUrls = {
'pageChooser': '{% url "wagtailadmin_choose_page" %}'
'pageChooser': '{% url "wagtailadmin_choose_page" %}',
'externalLinkChooser': '{% url "wagtailadmin_choose_page_external_link" %}',
'emailLinkChooser': '{% url "wagtailadmin_choose_page_email_link" %}'
};
</script>

View File

@ -313,27 +313,34 @@ class TestChooserExternalLink(TestCase, WagtailTestUtils):
self.assertTemplateUsed(response, 'wagtailadmin/chooser/external_link.html')
def test_get_with_param(self):
self.assertEqual(self.get({'prompt_for_link_text': 'foo'}).status_code, 200)
self.assertEqual(self.get({'link_text': 'foo'}).status_code, 200)
def test_create_link(self):
response = self.post({'url': 'http://www.example.com/', 'link_text': 'example'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "'onload'") # indicates success / post back to calling page
self.assertContains(response, "'url': 'http://www.example.com/'")
self.assertContains(response, "'title': 'example'") # When link text is given, it is used
def test_create_link_without_text(self):
response = self.post({'url': 'http://www.example.com/'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "'onload'") # indicates success / post back to calling page
self.assertContains(response, "'url': 'http://www.example.com/',")
self.assertContains(response, "'title': 'http://www.example.com/'")
self.assertContains(response, "'url': 'http://www.example.com/'")
self.assertContains(response, "'title': 'http://www.example.com/'") # When no text is given, it uses the url
def test_invalid_url(self):
response = self.post({'url': 'ntp://www.example.com'})
response = self.post({'url': 'ntp://www.example.com', 'link_text': 'example'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "'html'") # indicates failure / show error message
self.assertContains(response, "Enter a valid URL.")
def test_allow_local_url(self):
response = self.post({'url': '/admin/'})
response = self.post({'url': '/admin/', 'link_text': 'admin'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "'onload'") # indicates success / post back to calling page
self.assertContains(response, "'url': '/admin/',")
self.assertContains(response, "'title': '/admin/'")
self.assertContains(response, "'title': 'admin'")
class TestChooserEmailLink(TestCase, WagtailTestUtils):
@ -352,9 +359,14 @@ class TestChooserEmailLink(TestCase, WagtailTestUtils):
self.assertTemplateUsed(response, 'wagtailadmin/chooser/email_link.html')
def test_get_with_param(self):
self.assertEqual(self.get({'prompt_for_link_text': 'foo'}).status_code, 200)
self.assertEqual(self.get({'link_text': 'foo'}).status_code, 200)
def test_create_link(self):
request = self.post({'email_address': 'example@example.com', 'link_text': 'contact'})
self.assertContains(request, "'url': 'mailto:example@example.com',")
self.assertContains(request, "'title': 'contact'") # When link text is given, it is used
def test_create_link_without_text(self):
request = self.post({'email_address': 'example@example.com'})
self.assertContains(request, "'url': 'mailto:example@example.com',")
self.assertContains(request, "'title': 'example@example.com'")
self.assertContains(request, "'title': 'example@example.com'") # When no link text is given, it uses the email

View File

@ -4,9 +4,7 @@ from django.http import Http404
from django.shortcuts import get_object_or_404, render
from wagtail.utils.pagination import paginate
from wagtail.wagtailadmin.forms import (
EmailLinkChooserForm, EmailLinkChooserWithLinkTextForm, ExternalLinkChooserForm,
ExternalLinkChooserWithLinkTextForm, SearchForm)
from wagtail.wagtailadmin.forms import EmailLinkChooserForm, ExternalLinkChooserForm, SearchForm
from wagtail.wagtailadmin.modal_workflow import render_modal_workflow
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.utils import resolve_model_string
@ -144,26 +142,23 @@ def search(request, parent_page_id=None):
def external_link(request):
prompt_for_link_text = bool(request.GET.get('prompt_for_link_text'))
if prompt_for_link_text:
form_class = ExternalLinkChooserWithLinkTextForm
else:
form_class = ExternalLinkChooserForm
link_text = request.GET.get('link_text', '')
link_url = request.GET.get('link_url', '')
if request.method == 'POST':
form = form_class(request.POST)
form = ExternalLinkChooserForm(request.POST)
if form.is_valid():
return render_modal_workflow(
request,
None, 'wagtailadmin/chooser/external_link_chosen.js',
{
'url': form.cleaned_data['url'],
'link_text': form.cleaned_data['link_text'] if prompt_for_link_text else form.cleaned_data['url']
'link_text': form.cleaned_data['link_text'].strip() or form.cleaned_data['url']
}
)
else:
form = form_class()
form = ExternalLinkChooserForm(initial={'url': link_url, 'link_text': link_text})
return render_modal_workflow(
request,
@ -175,28 +170,23 @@ def external_link(request):
def email_link(request):
prompt_for_link_text = bool(request.GET.get('prompt_for_link_text'))
if prompt_for_link_text:
form_class = EmailLinkChooserWithLinkTextForm
else:
form_class = EmailLinkChooserForm
link_text = request.GET.get('link_text', '')
link_url = request.GET.get('link_url', '')
if request.method == 'POST':
form = form_class(request.POST)
form = EmailLinkChooserForm(request.POST)
if form.is_valid():
return render_modal_workflow(
request,
None, 'wagtailadmin/chooser/external_link_chosen.js',
{
'url': 'mailto:' + form.cleaned_data['email_address'],
'link_text': form.cleaned_data['link_text'] if (
prompt_for_link_text and form.cleaned_data['link_text']
) else form.cleaned_data['email_address']
'link_text': form.cleaned_data['link_text'].strip() or form.cleaned_data['email_address']
}
)
else:
form = form_class()
form = EmailLinkChooserForm(initial={'email_address': link_url, 'link_text': link_text})
return render_modal_workflow(
request,