diff --git a/client/src/components/Draftail/decorators/Link.js b/client/src/components/Draftail/decorators/Link.js index c605fda4a..9e22bb5ad 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, anchor 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 if (url.startsWith('#')) { icon = LINK_ICON; label = url; diff --git a/client/src/components/Draftail/decorators/Link.test.js b/client/src/components/Draftail/decorators/Link.test.js index 94df8a73e..3fceaff4b 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('anchor', () => { expect(getLinkAttributes({ url: '#testanchor' })).toMatchObject({ url: '#testanchor', diff --git a/client/src/components/Draftail/sources/ModalWorkflowSource.js b/client/src/components/Draftail/sources/ModalWorkflowSource.js index 18c2902a2..e0ec1c49e 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, allow_anchor_link: true, link_text: selectedText, }; @@ -58,6 +59,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 if (data.url.startsWith('#')) { url = global.chooserUrls.anchorLinkChooser; urlParams.link_url = data.url.replace('#', ''); 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 3ec8f54ca..7849a6c52 100644 --- a/client/src/components/Draftail/sources/__snapshots__/ModalWorkflowSource.test.js.snap +++ b/client/src/components/Draftail/sources/__snapshots__/ModalWorkflowSource.test.js.snap @@ -64,6 +64,7 @@ Object { "allow_anchor_link": true, "allow_email_link": true, "allow_external_link": true, + "allow_phone_link": true, "link_text": "", "link_url": "https://www.example.com/", "page_type": "wagtailcore.page", @@ -81,6 +82,7 @@ Object { "allow_anchor_link": true, "allow_email_link": true, "allow_external_link": true, + "allow_phone_link": true, "link_text": "", "link_url": "test@example.com", "page_type": "wagtailcore.page", @@ -98,6 +100,7 @@ Object { "allow_anchor_link": true, "allow_email_link": true, "allow_external_link": true, + "allow_phone_link": true, "link_text": "", "page_type": "wagtailcore.page", }, @@ -114,6 +117,7 @@ Object { "allow_anchor_link": true, "allow_email_link": true, "allow_external_link": true, + "allow_phone_link": true, "link_text": "", "page_type": "wagtailcore.page", }, @@ -130,6 +134,7 @@ Object { "allow_anchor_link": true, "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 a7972dcab..76776a4d3 100644 --- a/wagtail/admin/forms/choosers.py +++ b/wagtail/admin/forms/choosers.py @@ -39,3 +39,8 @@ class AnchorLinkChooserForm(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 76a8be65b..11438d8ed 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 @@ -38,6 +38,7 @@ urlParams = { 'allow_external_link': true, 'allow_email_link': true, + 'allow_phone_link': true, 'allow_anchor_link': true, }; @@ -57,6 +58,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 (href.startsWith('#')) { url = window.chooserUrls.anchorLinkChooser; href = href.replace('#', ''); 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 92c69c1f2..ce30a4f66 100644 --- a/wagtail/admin/static_src/wagtailadmin/js/page-chooser-modal.js +++ b/wagtail/admin/static_src/wagtailadmin/js/page-chooser-modal.js @@ -133,6 +133,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 8d7c8ab63..500789aeb 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 allow_anchor_link or current == 'external' or current == 'email' or current == 'anchor' %} +{% if allow_external_link or allow_email_link or allow_phone_link or allow_anchor_link or current == 'external' or current == 'email' or current == 'phone' or current == 'anchor' %}
+ {% 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 f8b3dde0b..6e5f851d8 100644 --- a/wagtail/admin/templates/wagtailadmin/pages/_editor_js.html +++ b/wagtail/admin/templates/wagtailadmin/pages/_editor_js.html @@ -10,6 +10,7 @@ 'pageChooser': '{% url "wagtailadmin_choose_page" %}', 'externalLinkChooser': '{% url "wagtailadmin_choose_page_external_link" %}', 'emailLinkChooser': '{% url "wagtailadmin_choose_page_email_link" %}', + 'phoneLinkChooser': '{% url "wagtailadmin_choose_page_phone_link" %}', 'anchorLinkChooser': '{% url "wagtailadmin_choose_page_anchor_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 ee2cd1d55..d631263e5 100644 --- a/wagtail/admin/tests/test_page_chooser.py +++ b/wagtail/admin/tests/test_page_chooser.py @@ -692,6 +692,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 dd7aa1af0..fb8b388f9 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'^choose-anchor-link/$', chooser.anchor_link, name='wagtailadmin_choose_page_anchor_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 94bfa4242..0123766bf 100644 --- a/wagtail/admin/views/chooser.py +++ b/wagtail/admin/views/chooser.py @@ -3,7 +3,8 @@ from django.http import Http404 from django.shortcuts import get_object_or_404, render from wagtail.admin.forms.choosers import ( - AnchorLinkChooserForm, EmailLinkChooserForm, ExternalLinkChooserForm) + AnchorLinkChooserForm, EmailLinkChooserForm, ExternalLinkChooserForm, PhoneLinkChooserForm) + from wagtail.admin.forms.search import SearchForm from wagtail.admin.modal_workflow import render_modal_workflow from wagtail.core import hooks @@ -20,6 +21,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'), 'allow_anchor_link': request.GET.get('allow_anchor_link'), } if extra_context: @@ -287,3 +289,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'} + )