From 4a343a5523772437794e2c450d42043083a3b676 Mon Sep 17 00:00:00 2001 From: mien Date: Thu, 1 Aug 2019 14:22:00 +0200 Subject: [PATCH] Add support for phone links in rich text This is mainly copy paste of Liam Brenner work (#3776) but with a few fixes to make it work with Draftail. Since mailto:-links is supported i think it is reasonable to support tel:-links as well --- .../components/Draftail/decorators/Link.js | 4 ++ .../Draftail/decorators/Link.test.js | 7 +++ .../Draftail/sources/ModalWorkflowSource.js | 4 ++ .../ModalWorkflowSource.test.js.snap | 5 ++ .../new_pages/inserting_links.rst | 1 + wagtail/admin/forms/choosers.py | 5 ++ .../js/hallo-plugins/hallo-wagtaillink.js | 7 ++- .../wagtailadmin/js/page-chooser-modal.js | 11 ++++ .../wagtailadmin/chooser/_link_types.html | 8 ++- .../wagtailadmin/chooser/phone_link.html | 17 ++++++ .../wagtailadmin/pages/_editor_js.html | 3 +- wagtail/admin/tests/test_page_chooser.py | 60 +++++++++++++++++++ wagtail/admin/urls/__init__.py | 1 + wagtail/admin/views/chooser.py | 38 +++++++++++- 14 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 wagtail/admin/templates/wagtailadmin/chooser/phone_link.html diff --git a/client/src/components/Draftail/decorators/Link.js b/client/src/components/Draftail/decorators/Link.js index 5e28ce390..aecb7483d 100644 --- a/client/src/components/Draftail/decorators/Link.js +++ b/client/src/components/Draftail/decorators/Link.js @@ -12,6 +12,7 @@ const BROKEN_LINK_ICON = ; const MAIL_ICON = ; const getEmailAddress = mailto => mailto.replace('mailto:', '').split('?')[0]; +const getPhoneNumber = tel => tel.replace('tel:', '').split('?')[0]; const getDomainName = url => url.replace(/(^\w+:|^)\/\//, '').split('/')[0]; // Determines how to display the link based on its type: page, mail, or external. @@ -29,6 +30,9 @@ export const getLinkAttributes = (data) => { } else if (url.startsWith('mailto:')) { icon = MAIL_ICON; label = getEmailAddress(url); + } else if (url.startsWith('tel:')) { + icon = LINK_ICON; + label = getPhoneNumber(url); } else { icon = LINK_ICON; label = getDomainName(url); diff --git a/client/src/components/Draftail/decorators/Link.test.js b/client/src/components/Draftail/decorators/Link.test.js index cab3ed979..f386c21a0 100644 --- a/client/src/components/Draftail/decorators/Link.test.js +++ b/client/src/components/Draftail/decorators/Link.test.js @@ -56,6 +56,13 @@ describe('Link', () => { }); }); + it('phone', () => { + expect(getLinkAttributes({ url: 'tel:+46700000000' })).toMatchObject({ + url: 'tel:+46700000000', + label: '+46700000000', + }); + }); + it('external', () => { expect(getLinkAttributes({ url: 'http://www.ex.com/' })).toMatchObject({ url: 'http://www.ex.com/', diff --git a/client/src/components/Draftail/sources/ModalWorkflowSource.js b/client/src/components/Draftail/sources/ModalWorkflowSource.js index 25cfe014c..6a6118ada 100644 --- a/client/src/components/Draftail/sources/ModalWorkflowSource.js +++ b/client/src/components/Draftail/sources/ModalWorkflowSource.js @@ -42,6 +42,7 @@ export const getChooserConfig = (entityType, entity, selectedText) => { page_type: 'wagtailcore.page', allow_external_link: true, allow_email_link: true, + allow_phone_link: true, link_text: selectedText, }; @@ -57,6 +58,9 @@ export const getChooserConfig = (entityType, entity, selectedText) => { } else if (data.url.startsWith('mailto:')) { url = global.chooserUrls.emailLinkChooser; urlParams.link_url = data.url.replace('mailto:', ''); + } else if (data.url.startsWith('tel:')) { + url = global.chooserUrls.phoneLinkChooser; + urlParams.link_url = data.url.replace('tel:', ''); } else { url = global.chooserUrls.externalLinkChooser; urlParams.link_url = data.url; diff --git a/client/src/components/Draftail/sources/__snapshots__/ModalWorkflowSource.test.js.snap b/client/src/components/Draftail/sources/__snapshots__/ModalWorkflowSource.test.js.snap index 2584f4035..95095fc54 100644 --- a/client/src/components/Draftail/sources/__snapshots__/ModalWorkflowSource.test.js.snap +++ b/client/src/components/Draftail/sources/__snapshots__/ModalWorkflowSource.test.js.snap @@ -57,6 +57,7 @@ Object { "urlParams": Object { "allow_email_link": true, "allow_external_link": true, + "allow_phone_link": true, "link_text": "", "link_url": "https://www.example.com/", "page_type": "wagtailcore.page", @@ -73,6 +74,7 @@ Object { "urlParams": Object { "allow_email_link": true, "allow_external_link": true, + "allow_phone_link": true, "link_text": "", "link_url": "test@example.com", "page_type": "wagtailcore.page", @@ -89,6 +91,7 @@ Object { "urlParams": Object { "allow_email_link": true, "allow_external_link": true, + "allow_phone_link": true, "link_text": "", "page_type": "wagtailcore.page", }, @@ -104,6 +107,7 @@ Object { "urlParams": Object { "allow_email_link": true, "allow_external_link": true, + "allow_phone_link": true, "link_text": "", "page_type": "wagtailcore.page", }, @@ -119,6 +123,7 @@ Object { "urlParams": Object { "allow_email_link": true, "allow_external_link": true, + "allow_phone_link": true, "link_text": "", "page_type": "wagtailcore.page", }, diff --git a/docs/editor_manual/new_pages/inserting_links.rst b/docs/editor_manual/new_pages/inserting_links.rst index 45e5fdedd..7eb9f2598 100644 --- a/docs/editor_manual/new_pages/inserting_links.rst +++ b/docs/editor_manual/new_pages/inserting_links.rst @@ -15,6 +15,7 @@ Whichever way you insert a link, you will be presented with the form displayed b * Internal link: A link to an existing page within your website. * External link: A link to a page on another website. * Email link: A link that will open the user's default email client with the email address prepopulated. + * Phone link: A link that will open the user's default client for initiating audio calls, with the phone number prepopulated. * You can also navigate through the website to find an internal link via the explorer. diff --git a/wagtail/admin/forms/choosers.py b/wagtail/admin/forms/choosers.py index 739060cf8..37ab1a7e3 100644 --- a/wagtail/admin/forms/choosers.py +++ b/wagtail/admin/forms/choosers.py @@ -34,3 +34,8 @@ class ExternalLinkChooserForm(forms.Form): class EmailLinkChooserForm(forms.Form): email_address = forms.EmailField(required=True) link_text = forms.CharField(required=False) + + +class PhoneLinkChooserForm(forms.Form): + phone_number = forms.CharField(required=True) + link_text = forms.CharField(required=False) diff --git a/wagtail/admin/static_src/wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js b/wagtail/admin/static_src/wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js index cbd0fc921..7e55e2509 100644 --- a/wagtail/admin/static_src/wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js +++ b/wagtail/admin/static_src/wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js @@ -37,7 +37,8 @@ url = window.chooserUrls.pageChooser; urlParams = { 'allow_external_link': true, - 'allow_email_link': true + 'allow_email_link': true, + 'allow_phone_link': true }; enclosingLink = getEnclosingLink(); @@ -56,6 +57,10 @@ url = window.chooserUrls.emailLinkChooser; href = href.replace('mailto:', ''); urlParams['link_url'] = href; + } else if (href.startsWith('tel:')) { + url = window.chooserUrls.phoneLinkChooser; + href = href.replace('tel:', ''); + urlParams['link_url'] = href; } else if (!linkType) { /* external link */ url = window.chooserUrls.externalLinkChooser; urlParams['link_url'] = href; diff --git a/wagtail/admin/static_src/wagtailadmin/js/page-chooser-modal.js b/wagtail/admin/static_src/wagtailadmin/js/page-chooser-modal.js index 1a43e1a5d..29e6ba447 100644 --- a/wagtail/admin/static_src/wagtailadmin/js/page-chooser-modal.js +++ b/wagtail/admin/static_src/wagtailadmin/js/page-chooser-modal.js @@ -121,6 +121,17 @@ PAGE_CHOOSER_MODAL_ONLOAD_HANDLERS = { return false; }); }, + 'phone_link': function(modal, jsonData) { + $('p.link-types a', modal.body).on('click', function() { + modal.loadUrl(this.href); + return false; + }); + + $('form', modal.body).on('submit', function() { + modal.postForm(this.action, $(this).serialize()); + return false; + }); + }, 'external_link': function(modal, jsonData) { $('p.link-types a', modal.body).on('click', function() { modal.loadUrl(this.href); diff --git a/wagtail/admin/templates/wagtailadmin/chooser/_link_types.html b/wagtail/admin/templates/wagtailadmin/chooser/_link_types.html index a4d4e31a0..b99cefb42 100644 --- a/wagtail/admin/templates/wagtailadmin/chooser/_link_types.html +++ b/wagtail/admin/templates/wagtailadmin/chooser/_link_types.html @@ -1,5 +1,5 @@ {% load i18n wagtailadmin_tags %} -{% if allow_external_link or allow_email_link or current == 'external' or current == 'email' %} +{% if allow_external_link or allow_email_link or allow_phone_link or current == 'external' or current == 'email' or current == 'phone' %} {% endif %} diff --git a/wagtail/admin/templates/wagtailadmin/chooser/phone_link.html b/wagtail/admin/templates/wagtailadmin/chooser/phone_link.html new file mode 100644 index 000000000..1db1e3cdd --- /dev/null +++ b/wagtail/admin/templates/wagtailadmin/chooser/phone_link.html @@ -0,0 +1,17 @@ +{% load i18n wagtailadmin_tags %} +{% trans "Add a phone link" as phone_str %} +{% include "wagtailadmin/shared/header.html" with title=phone_str %} + +
+ {% include 'wagtailadmin/chooser/_link_types.html' with current='phone' %} + +
+ {% csrf_token %} +
    + {% for field in form %} + {% include "wagtailadmin/shared/field_as_li.html" %} + {% endfor %} +
  • +
+
+
diff --git a/wagtail/admin/templates/wagtailadmin/pages/_editor_js.html b/wagtail/admin/templates/wagtailadmin/pages/_editor_js.html index ad143456c..1185fd93e 100644 --- a/wagtail/admin/templates/wagtailadmin/pages/_editor_js.html +++ b/wagtail/admin/templates/wagtailadmin/pages/_editor_js.html @@ -9,7 +9,8 @@ window.chooserUrls = { 'pageChooser': '{% url "wagtailadmin_choose_page" %}', 'externalLinkChooser': '{% url "wagtailadmin_choose_page_external_link" %}', - 'emailLinkChooser': '{% url "wagtailadmin_choose_page_email_link" %}' + 'emailLinkChooser': '{% url "wagtailadmin_choose_page_email_link" %}', + 'phoneLinkChooser': '{% url "wagtailadmin_choose_page_phone_link" %}', }; window.unicodeSlugsEnabled = {% if unicode_slugs_enabled %}true{% else %}false{% endif %}; diff --git a/wagtail/admin/tests/test_page_chooser.py b/wagtail/admin/tests/test_page_chooser.py index f19ab651a..92fd3e53a 100644 --- a/wagtail/admin/tests/test_page_chooser.py +++ b/wagtail/admin/tests/test_page_chooser.py @@ -626,6 +626,66 @@ class TestChooserEmailLink(TestCase, WagtailTestUtils): self.assertEqual(result['prefer_this_title_as_link_text'], True) +class TestChooserPhoneLink(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + + def get(self, params={}): + return self.client.get(reverse('wagtailadmin_choose_page_phone_link'), params) + + def post(self, post_data={}, url_params={}): + url = reverse('wagtailadmin_choose_page_phone_link') + if url_params: + url += '?' + urlencode(url_params) + return self.client.post(url, post_data) + + def test_simple(self): + response = self.get() + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailadmin/chooser/phone_link.html') + + def test_prepopulated_form(self): + response = self.get({'link_text': 'Example', 'link_url': '+123456789'}) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Example') + self.assertContains(response, '+123456789') + + def test_create_link(self): + response = self.post({'phone-link-chooser-phone_number': '+123456789', 'phone-link-chooser-link_text': 'call'}) + result = json.loads(response.content.decode())['result'] + self.assertEqual(result['url'], "tel:+123456789") + self.assertEqual(result['title'], "call") + self.assertEqual(result['prefer_this_title_as_link_text'], True) + + def test_create_link_without_text(self): + response = self.post({'phone-link-chooser-phone_number': '+123456789'}) + result = json.loads(response.content.decode())['result'] + self.assertEqual(result['url'], "tel:+123456789") + self.assertEqual(result['title'], "+123456789") # When no link text is given, it uses the phone number + self.assertEqual(result['prefer_this_title_as_link_text'], False) + + def test_notice_changes_to_link_text(self): + response = self.post( + {'phone-link-chooser-phone_number': '+222222222', 'phone-link-chooser-link_text': 'example'}, # POST data + {'link_url': '+111111111', 'link_text': 'example'} # GET params - initial data + ) + result = json.loads(response.content.decode())['result'] + self.assertEqual(result['url'], "tel:+222222222") + self.assertEqual(result['title'], "example") + # no change to link text, so prefer the existing link/selection content where available + self.assertEqual(result['prefer_this_title_as_link_text'], False) + + response = self.post( + {'phone-link-chooser-phone_number': '+222222222', 'phone-link-chooser-link_text': 'new example'}, # POST data + {'link_url': '+111111111', 'link_text': 'example'} # GET params - initial data + ) + result = json.loads(response.content.decode())['result'] + self.assertEqual(result['url'], "tel:+222222222") + self.assertEqual(result['title'], "new example") + # link text has changed, so tell the caller to use it + self.assertEqual(result['prefer_this_title_as_link_text'], True) + + class TestCanChoosePage(TestCase, WagtailTestUtils): fixtures = ['test.json'] diff --git a/wagtail/admin/urls/__init__.py b/wagtail/admin/urls/__init__.py index c56dcc357..902dcedfa 100644 --- a/wagtail/admin/urls/__init__.py +++ b/wagtail/admin/urls/__init__.py @@ -37,6 +37,7 @@ urlpatterns = [ url(r'^choose-page/search/$', chooser.search, name='wagtailadmin_choose_page_search'), url(r'^choose-external-link/$', chooser.external_link, name='wagtailadmin_choose_page_external_link'), url(r'^choose-email-link/$', chooser.email_link, name='wagtailadmin_choose_page_email_link'), + url(r'^choose-phone-link/$', chooser.phone_link, name='wagtailadmin_choose_page_phone_link'), url(r'^tag-autocomplete/$', tags.autocomplete, name='wagtailadmin_tag_autocomplete'), diff --git a/wagtail/admin/views/chooser.py b/wagtail/admin/views/chooser.py index a0f299d4c..319741647 100644 --- a/wagtail/admin/views/chooser.py +++ b/wagtail/admin/views/chooser.py @@ -2,7 +2,8 @@ from django.core.paginator import Paginator from django.http import Http404 from django.shortcuts import get_object_or_404, render -from wagtail.admin.forms.choosers import EmailLinkChooserForm, ExternalLinkChooserForm +from wagtail.admin.forms.choosers import ( + EmailLinkChooserForm, ExternalLinkChooserForm, PhoneLinkChooserForm) from wagtail.admin.forms.search import SearchForm from wagtail.admin.modal_workflow import render_modal_workflow from wagtail.core import hooks @@ -19,6 +20,7 @@ def shared_context(request, extra_context=None): 'parent_page_id': request.GET.get('parent_page_id'), 'allow_external_link': request.GET.get('allow_external_link'), 'allow_email_link': request.GET.get('allow_email_link'), + 'allow_phone_link': request.GET.get('allow_phone_link'), } if extra_context: context.update(extra_context) @@ -254,3 +256,37 @@ def email_link(request): 'form': form, }), json_data={'step': 'email_link'} ) + + +def phone_link(request): + initial_data = { + 'link_text': request.GET.get('link_text', ''), + 'phone_number': request.GET.get('link_url', ''), + } + + if request.method == 'POST': + form = PhoneLinkChooserForm(request.POST, initial=initial_data, prefix='phone-link-chooser') + + if form.is_valid(): + result = { + 'url': 'tel:' + form.cleaned_data['phone_number'], + 'title': form.cleaned_data['link_text'].strip() or form.cleaned_data['phone_number'], + # If the user has explicitly entered / edited something in the link_text field, + # always use that text. If not, we should favour keeping the existing link/selection + # text, where applicable. + 'prefer_this_title_as_link_text': ('link_text' in form.changed_data), + } + return render_modal_workflow( + request, None, None, + None, json_data={'step': 'external_link_chosen', 'result': result} + ) + else: + form = PhoneLinkChooserForm(initial=initial_data, prefix='phone-link-chooser') + + return render_modal_workflow( + request, + 'wagtailadmin/chooser/phone_link.html', None, + shared_context(request, { + 'form': form, + }), json_data={'step': 'phone_link'} + )