diff --git a/.travis.yml b/.travis.yml index e23794d4f..3e0a4bb98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ services: # Package installation install: - python setup.py install - - pip install psycopg2 pyelasticsearch elasticutils==0.8.2 wand + - pip install psycopg2 pyelasticsearch elasticutils==0.8.2 wand embedly - pip install coveralls # Pre-test configuration before_script: diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b708cfd21..6c28594a9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,7 +2,15 @@ Changelog ========= 0.4 (xx.xx.20xx) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~ + * Added 'original' as a resizing rule supported by the 'image' tag + * Hallo.js updated to version 1.0.4 + * Snippets are now ordered alphabetically + * Removed the "More" section from the admin menu + * Added pagination to page listings in admin + * Fix: Animated GIFs are now coalesced before resizing + * Fix: Wand backend clones images before modifying them + * Fix: Admin breadcrumb now positioned correctly on mobile 0.3.1 (03.06.2014) ~~~~~~~~~~~~~~~~~~ diff --git a/docs/building_your_site/frontenddevelopers.rst b/docs/building_your_site/frontenddevelopers.rst index e5f583397..f0054eaab 100644 --- a/docs/building_your_site/frontenddevelopers.rst +++ b/docs/building_your_site/frontenddevelopers.rst @@ -94,7 +94,7 @@ The ``image`` tag inserts an XHTML-compatible ``img`` element into the page, set The syntax for the tag is thus:: - {% image [image] [method]-[dimension(s)] %} + {% image [image] [resize-rule] %} For example: @@ -108,16 +108,20 @@ For example: {% image self.photo fill-80x80 %} -In the above syntax ``[image]`` is the Django object refering to the image. If your page model defined a field called "photo" then ``[image]`` would probably be ``self.photo``. The ``[method]`` defines which resizing algorithm to use and ``[dimension(s)]`` provides height and/or width values (as ``[width|height]`` or ``[width]x[height]``) to refine that algorithm. +In the above syntax ``[image]`` is the Django object refering to the image. If your page model defined a field called "photo" then ``[image]`` would probably be ``self.photo``. The ``[resize-rule]`` defines how the image is to be resized when inserted into the page; various resizing methods are supported, to cater for different usage cases (e.g. lead images that span the whole width of the page, or thumbnails to be cropped to a fixed size). -Note that a space separates ``[image]`` and ``[method]``, but not ``[method]`` and ``[dimensions]``: a hyphen between ``[method]`` and ``[dimensions]`` is mandatory. Multiple dimensions must be separated by an ``x``. +Note that a space separates ``[image]`` and ``[resize-rule]``, but the resize rule must not contain spaces. -The available ``method`` s are: +The available resizing methods are: .. glossary:: ``max`` (takes two dimensions) + .. code-block:: django + + {% image self.photo max-1000x500 %} + Fit **within** the given dimensions. The longest edge will be reduced to the equivalent dimension size defined. e.g A portrait image of width 1000, height 2000, treated with the ``max`` dimensions ``1000x500`` (landscape) would result in the image shrunk so the *height* was 500 pixels and the width 250. @@ -125,6 +129,10 @@ The available ``method`` s are: ``min`` (takes two dimensions) + .. code-block:: django + + {% image self.photo min-500x200 %} + **Cover** the given dimensions. This may result in an image slightly **larger** than the dimensions you specify. e.g A square image of width 2000, height 2000, treated with the ``min`` dimensions ``500x200`` (landscape) would have it's height and width changed to 500, i.e matching the width required, but greater than the height. @@ -132,27 +140,45 @@ The available ``method`` s are: ``width`` (takes one dimension) + .. code-block:: django + + {% image self.photo width-640 %} + Reduces the width of the image to the dimension specified. ``height`` (takes one dimension) + .. code-block:: django + + {% image self.photo height-480 %} + Resize the height of the image to the dimension specified.. ``fill`` (takes two dimensions) + .. code-block:: django + + {% image self.photo fill-200x200 %} + Resize and **crop** to fill the **exact** dimensions. This can be particularly useful for websites requiring square thumbnails of arbitrary images. For example, a landscape image of width 2000, height 1000, treated with ``fill`` dimensions ``200x200`` would have its height reduced to 200, then its width (ordinarily 400) cropped to 200. **The crop always aligns on the centre of the image.** -.. Note:: - Wagtail does not allow deforming or stretching images. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions. + ``original`` + (takes no dimensions) + + .. code-block:: django + + {% image self.photo original %} + + Leaves the image at its original size - no resizing is performed. .. Note:: - Wagtail does not make the "original" version of an image explicitly available. To request it, you could rely on the lack of upscaling by requesting an image larger than its maximum dimensions. e.g to insert an image whose dimensions are unknown at its maximum size, try: ``{% image self.image width-10000 %}``. This assumes the image is unlikely to be larger than 10000px wide. + Wagtail does not allow deforming or stretching images. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions. .. _image_tag_alt: @@ -190,6 +216,32 @@ Only fields using ``RichTextField`` need this applied in the template. .. Note:: Note that the template tag loaded differs from the name of the filter. +Responsive Embeds +----------------- + +Wagtail embeds and images are included at their full width, which may overflow the bounds of the content container you've defined in your templates. To make images and embeds responsive -- meaning they'll resize to fit their container -- include the following CSS. + +.. code-block:: css + + .rich-text img { + max-width: 100%; + height: auto; + } + + .responsive-object { + position: relative; + } + .responsive-object iframe, + .responsive-object object, + .responsive-object embed { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + Internal links (tag) ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/editing_api.rst b/docs/editing_api.rst index 99e859437..88fc9458f 100644 --- a/docs/editing_api.rst +++ b/docs/editing_api.rst @@ -211,7 +211,7 @@ You can explicitly link ``Page``-derived models together using the ``Page`` mode Snippets -------- -Snippets are not subclasses, so you must include the model class directly. A chooser is provided which takes the field name snippet class. +Snippets are vanilla Django models you create yourself without a Wagtail-provided base class. So using them as a field in a page requires specifying your own ``appname.modelname``. A chooser, ``SnippetChooserPanel``, is provided which takes the field name and snippet class. .. code-block:: python @@ -248,6 +248,12 @@ Full-Width Input Use ``classname="full"`` to make a field (input element) stretch the full width of the Wagtail page editor. This will not work if the field is encapsulated in a ``MultiFieldPanel``, which places its child fields into a formset. +Titles +------ + +Use ``classname="title"`` to make Page's built-in title field stand out with more vertical padding. + + Required Fields --------------- @@ -264,19 +270,11 @@ Without a panel definition, a default form field (without label) will be used to .. _Django model field reference (editable): https://docs.djangoproject.com/en/dev/ref/models/fields/#editable - - - - - - - - - - MultiFieldPanel ~~~~~~~~~~~~~~~ +The ``MultiFieldPanel`` groups a list of child fields into a fieldset, which can also be collapsed into a heading bar to save space. + .. code-block:: python BOOK_FIELD_COLLECTION = [ @@ -294,8 +292,7 @@ MultiFieldPanel # ... ] - - +By default, ``MultiFieldPanel`` s are expanded and not collapsible. Adding the classname ``collapsible`` will enable the collapse control. Adding both ``collapsible`` and ``collapsed`` to the classname parameter will load the editor page with the ``MultiFieldPanel`` collapsed under its heading. .. _inline_panels: @@ -303,7 +300,55 @@ MultiFieldPanel Inline Panels and Model Clusters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``django-modelcluster`` module allows for streamlined relation of extra models to a Wagtail page. +The ``django-modelcluster`` module allows for streamlined relation of extra models to a Wagtail page. For instance, you can create objects related through a ``ForeignKey`` relationship on the fly and save them to a draft revision of a ``Page`` object. Normally, your related objects "cluster" would need to be created beforehand (or asynchronously) before linking them to a Page. + +Let's look at the example of adding related links to a ``Page``-derived model. We want to be able to add as many as we like, assign an order, and do all of this without leaving the page editing screen. + +.. code-block:: python + + from wagtail.wagtailcore.models import Orderable, Page + from modelcluster.fields import ParentalKey + + # The abstract model for related links, complete with panels + class RelatedLink(models.Model): + title = models.CharField(max_length=255) + link_external = models.URLField("External link", blank=True) + + panels = [ + FieldPanel('title'), + FieldPanel('link_external'), + ] + + class Meta: + abstract = True + + # The real model which combines the abstract model, an + # Orderable helper class, and what amounts to a ForeignKey link + # to the model we want to add related links to (BookPage) + class BookPageRelatedLinks(Orderable, RelatedLink): + page = ParentalKey('demo.BookPage', related_name='related_links') + + class BookPage( Page ): + # ... + + BookPage.content_panels = [ + # ... + InlinePanel( BookPage, 'related_links', label="Related Links" ), + ] + +The ``RelatedLink`` class is a vanilla Django abstract model. The ``BookPageRelatedLinks`` model extends it with capability for being ordered in the Wagtail interface via the ``Orderable`` class as well as adding a ``page`` property which links the model to the ``BookPage`` model we're adding the related links objects to. Finally, in the panel definitions for ``BookPage``, we'll add an ``InlinePanel`` to provide an interface for it all. Let's look again at the parameters that ``InlinePanel`` accepts: + +.. code-block:: python + + InlinePanel( base_model, relation_name, panels=None, label='', help_text='' ) + +``base_model`` is the model you're extending with the cluster. The ``relation_name`` is the ``related_name`` label given to the cluster's ``ParentalKey`` relation. You can add the ``panels`` manually or make them part of the cluster model. Finally, ``label`` and ``help_text`` provide a heading and caption, respectively, for the Wagtail editor. + +For another example of using model clusters, see :ref:`tagging` + +For more on ``django-modelcluster``, visit `the django-modelcluster github project page`_ ). + +.. _the django-modelcluster github page: https://github.com/torchbox/django-modelcluster .. _extending_wysiwyg: @@ -311,12 +356,237 @@ The ``django-modelcluster`` module allows for streamlined relation of extra mode Extending the WYSIWYG Editor (hallo.js) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Adding hallo.js plugins: -https://github.com/torchbox/wagtail/commit/1ecc215759142e6cafdacb185bbfd3f8e9cd3185 +To inject javascript into the Wagtail page editor, see the :ref:`insert_editor_js` hook. Once you have the hook in place and your hallo.js plugin loads into the Wagtail page editor, use the following Javascript to register the plugin with hallo.js. + +.. code-block:: javascript + + registerHalloPlugin(name, opts); + +hallo.js plugin names are prefixed with the ``"IKS."`` namespace, but the ``name`` you pass into ``registerHalloPlugin()`` should be without the prefix. ``opts`` is an object passed into the plugin. + +For information on developing custom hallo.js plugins, see the project's page: https://github.com/bergie/hallo Edit Handler API ~~~~~~~~~~~~~~~~ +Admin Hooks +----------- + +On loading, Wagtail will search for any app with the file ``wagtail_hooks.py`` and execute the contents. This provides a way to register your own functions to execute at certain points in Wagtail's execution, such as when a ``Page`` object is saved or when the main menu is constructed. + +Registering functions with a Wagtail hook follows the following pattern: + +.. code-block:: python + + from wagtail.wagtailadmin import hooks + + hooks.register('hook', function) + +Where ``'hook'`` is one of the following hook strings and ``function`` is a function you've defined to handle the hook. + +.. _construct_wagtail_edit_bird: + +``construct_wagtail_edit_bird`` + Add or remove items from the wagtail userbar. Add, edit, and moderation tools are provided by default. The callable passed into the hook must take the ``request`` object and a list of menu objects, ``items``. The menu item objects must have a ``render`` method which can take a ``request`` object and return the HTML string representing the menu item. See the userbar templates and menu item classes for more information. + + .. code-block:: python + + from wagtail.wagtailadmin import hooks + + class UserbarPuppyLinkItem(object): + def render(self, request): + return '
  • Puppies!
  • ' + + def add_puppy_link_item(request, items): + return items.append( UserbarPuppyLinkItem() ) + + hooks.register('construct_wagtail_edit_bird', add_puppy_link_item) + +.. _construct_homepage_panels: + +``construct_homepage_panels`` + Add or remove panels from the Wagtail admin homepage. The callable passed into this hook should take a ``request`` object and a list of ``panels``, objects which have a ``render()`` method returning a string. The objects also have an ``order`` property, an integer used for ordering the panels. The default panels use integers between ``100`` and ``300``. + + .. code-block:: python + + from django.utils.safestring import mark_safe + + from wagtail.wagtailadmin import hooks + + class WelcomePanel(object): + order = 50 + + def render(self): + return mark_safe(""" +
    +

    No, but seriously -- welcome to the admin homepage.

    +
    + """) + + def add_another_welcome_panel(request, panels): + return panels.append( WelcomePanel() ) + + hooks.register('construct_homepage_panels', add_another_welcome_panel) + +.. _after_create_page: + +``after_create_page`` + Do something with a ``Page`` object after it has been saved to the database (as a published page or a revision). The callable passed to this hook should take a ``request`` object and a ``page`` object. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the Explorer page for the new page's parent. + + .. code-block:: python + + from django.http import HttpResponse + + from wagtail.wagtailadmin import hooks + + def do_after_page_create(request, page): + return HttpResponse("Congrats on making content!", content_type="text/plain") + hooks.register('after_create_page', do_after_page_create) + +.. _after_edit_page: + +``after_edit_page`` + Do something with a ``Page`` object after it has been updated. Uses the same behavior as ``after_create_page``. + +.. _after_delete_page: + +``after_delete_page`` + Do something after a ``Page`` object is deleted. Uses the same behavior as ``after_create_page``. + +.. _register_admin_urls: + +``register_admin_urls`` + Register additional admin page URLs. The callable fed into this hook should return a list of Django URL patterns which define the structure of the pages and endpoints of your extension to the Wagtail admin. For more about vanilla Django URLconfs and views, see `url dispatcher`_. + + .. _url dispatcher: https://docs.djangoproject.com/en/dev/topics/http/urls/ + + .. code-block:: python + + from django.http import HttpResponse + from django.conf.urls import url + + from wagtail.wagtailadmin import hooks + + def admin_view( request ): + return HttpResponse( \ + "I have approximate knowledge of many things!", \ + content_type="text/plain") + + def urlconf_time(): + return [ + url(r'^how_did_you_almost_know_my_name/$', admin_view, name='frank' ), + ] + hooks.register('register_admin_urls', urlconf_time) + +.. _construct_main_menu: + +``construct_main_menu`` + Add, remove, or alter ``MenuItem`` objects from the Wagtail admin menu. The callable passed to this hook must take a ``request`` object and a list of ``menu_items``; it must return a list of menu items. New items can be constructed from the ``MenuItem`` class by passing in: a ``label`` which will be the text in the menu item, the URL of the admin page you want the menu item to link to (usually by calling ``reverse()`` on the admin view you've set up), CSS class ``name`` applied to the wrapping ``
  • `` of the menu item as ``"menu-{name}"``, CSS ``classnames`` which are used to give the link an icon, and an ``order`` integer which determine's the item's place in the menu. + + .. code-block:: python + + from django.core.urlresolvers import reverse + + from wagtail.wagtailadmin import hooks + from wagtail.wagtailadmin.menu import MenuItem + + def construct_main_menu(request, menu_items): + menu_items.append( + MenuItem( 'Frank', reverse('frank'), classnames='icon icon-folder-inverse', order=10000) + ) + hooks.register('construct_main_menu', construct_main_menu) + + +.. _insert_editor_js: + +``insert_editor_js`` + Add additional Javascript files or code snippets to the page editor. Output must be compatible with ``compress``, as local static includes or string. + + .. code-block:: python + + from django.utils.html import format_html, format_html_join + from django.conf import settings + + from wagtail.wagtailadmin import hooks + + def editor_js(): + js_files = [ + 'demo/js/hallo-plugins/hallo-demo-plugin.js', + ] + js_includes = format_html_join('\n', '', + ((settings.STATIC_URL, filename) for filename in js_files) + ) + return js_includes + format_html( + """ + + """ + ) + hooks.register('insert_editor_js', editor_js) + +.. _insert_editor_css: + +``insert_editor_css`` + Add additional CSS or SCSS files or snippets to the page editor. Output must be compatible with ``compress``, as local static includes or string. + + .. code-block:: python + + from django.utils.html import format_html + from django.conf import settings + + from wagtail.wagtailadmin import hooks + + def editor_css(): + return format_html('') + hooks.register('insert_editor_css', editor_css) + + +Image Formats in the Rich Text Editor +------------------------------------- + +On loading, Wagtail will search for any app with the file ``image_formats.py`` and execute the contents. This provides a way to customize the formatting options shown to the editor when inserting images in the ``RichTextField`` editor. + +As an example, add a "thumbnail" format: + +.. code-block:: python + # image_formats.py + from wagtail.wagtailimages.formats import Format, register_image_format + + register_image_format(Format('thumbnail', 'Thumbnail', 'richtext-image thumbnail', 'max-120x120')) + + +To begin, import the the ``Format`` class, ``register_image_format`` function, and optionally ``unregister_image_format`` function. To register a new ``Format``, call the ``register_image_format`` with the ``Format`` object as the argument. The ``Format`` takes the following init arguments: + +``name`` + The unique key used to identify the format. To unregister this format, call ``unregister_image_format`` with this string as the only argument. + +``label`` + The label used in the chooser form when inserting the image into the ``RichTextField``. + +``classnames`` + The string to assign to the ``class`` attribute of the generated ```` tag. + +``filter_spec`` + The string specification to create the image rendition. For more, see the :ref:`image_tag`. + + +To unregister, call ``unregister_image_format`` with the string of the ``name`` of the ``Format`` as the only argument. + + +Content Index Pages (CRUD) +-------------------------- + + +Custom Choosers +--------------- + + +Tests +----- diff --git a/docs/gettingstarted.rst b/docs/gettingstarted.rst index 458de387b..de635256c 100644 --- a/docs/gettingstarted.rst +++ b/docs/gettingstarted.rst @@ -65,7 +65,7 @@ Wagtail instance available as the basis for your new site: - Install `Vagrant `_ 1.1+ - Clone the demonstration site, create the Vagrant box and initialise Wagtail:: - git clone git@github.com:torchbox/wagtaildemo.git + git clone https://github.com/torchbox/wagtaildemo.git cd wagtaildemo vagrant up vagrant ssh diff --git a/docs/model_recipes.rst b/docs/model_recipes.rst index fe8a89e32..6a4abed47 100644 --- a/docs/model_recipes.rst +++ b/docs/model_recipes.rst @@ -118,6 +118,8 @@ Will return:: tauntaun kennel bed and breakfast +.. _tagging: + Tagging ------- diff --git a/docs/wagtail_search.rst b/docs/wagtail_search.rst index 642a5e741..3d8ea26b2 100644 --- a/docs/wagtail_search.rst +++ b/docs/wagtail_search.rst @@ -224,10 +224,13 @@ Prerequisites are the Elasticsearch service itself and, via pip, the `elasticuti .. code-block:: guess - pip install elasticutils pyelasticsearch + pip install elasticutils==0.8.2 pyelasticsearch .. note:: - The dependency on pyelasticsearch is scheduled to be replaced by a dependency on `elasticsearch-py`_. + ElasticUtils 0.9+ is not supported. + +.. note:: + The dependency on elasticutils and pyelasticsearch is scheduled to be replaced by a dependency on `elasticsearch-py`_. The backend is configured in settings: diff --git a/requirements-dev.txt b/requirements-dev.txt index d35d6b087..4a9ff5c5e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ # For coverage and PEP8 linting coverage==3.7.1 flake8==2.1.0 +mock==1.0.1 diff --git a/runtests.py b/runtests.py index 022c1b503..96653b40d 100755 --- a/runtests.py +++ b/runtests.py @@ -102,7 +102,9 @@ if not settings.configured: ), COMPRESS_ENABLED=False, # disable compression so that we can run tests on the content of the compress tag WAGTAILSEARCH_BACKENDS=WAGTAILSEARCH_BACKENDS, - WAGTAIL_SITE_NAME='Test Site' + WAGTAIL_SITE_NAME='Test Site', + LOGIN_REDIRECT_URL='wagtailadmin_home', + LOGIN_URL='wagtailadmin_login', ) diff --git a/wagtail/tests/models.py b/wagtail/tests/models.py index bbf91d278..e061a4bfe 100644 --- a/wagtail/tests/models.py +++ b/wagtail/tests/models.py @@ -255,18 +255,42 @@ FormPage.content_panels = [ ] +# Snippets + # Snippets class Advert(models.Model): - url = models.URLField(null=True, blank=True) - text = models.CharField(max_length=255) + url = models.URLField(null=True, blank=True) + text = models.CharField(max_length=255) - panels = [ - FieldPanel('url'), - FieldPanel('text'), - ] + panels = [ + FieldPanel('url'), + FieldPanel('text'), + ] + + def __unicode__(self): + return self.text - def __unicode__(self): - return self.text register_snippet(Advert) + + +# AlphaSnippet and ZuluSnippet are for testing ordering of +# snippets when registering. They are named as such to ensure +# thier ordering is clear. They are registered during testing +# to ensure specific [in]correct register ordering + +# AlphaSnippet is registered during TestSnippetOrdering +class AlphaSnippet(models.Model): + text = models.CharField(max_length=255) + + def __unicode__(self): + return self.text + + +# ZuluSnippet is registered during TestSnippetOrdering +class ZuluSnippet(models.Model): + text = models.CharField(max_length=255) + + def __unicode__(self): + return self.text diff --git a/wagtail/tests/utils.py b/wagtail/tests/utils.py index 6590e6dcc..0b13e5932 100644 --- a/wagtail/tests/utils.py +++ b/wagtail/tests/utils.py @@ -1,4 +1,7 @@ +from django.test import TestCase from django.contrib.auth.models import User +from django.utils.six.moves.urllib.parse import urlparse, ParseResult +from django.http import QueryDict # We need to make sure that we're using the same unittest library that Django uses internally # Otherwise, we get issues with the "SkipTest" and "ExpectedFailure" exceptions being recognised as errors @@ -12,11 +15,30 @@ except ImportError: import unittest -def login(client): - # Create a user - user = User.objects.create_superuser(username='test', email='test@email.com', password='password') +class WagtailTestUtils(object): + def login(self): + # Create a user + user = User.objects.create_superuser(username='test', email='test@email.com', password='password') - # Login - client.login(username='test', password='password') + # Login + self.client.login(username='test', password='password') - return user + return user + + # From: https://github.com/django/django/blob/255449c1ee61c14778658caae8c430fa4d76afd6/django/contrib/auth/tests/test_views.py#L70-L85 + def assertURLEqual(self, url, expected, parse_qs=False): + """ + Given two URLs, make sure all their components (the ones given by + urlparse) are equal, only comparing components that are present in both + URLs. + If `parse_qs` is True, then the querystrings are parsed with QueryDict. + This is useful if you don't want the order of parameters to matter. + Otherwise, the query strings are compared as-is. + """ + fields = ParseResult._fields + + for attr, x, y in zip(fields, urlparse(url), urlparse(expected)): + if parse_qs and attr == 'query': + x, y = QueryDict(x), QueryDict(y) + if x and y and x != y: + self.fail("%r != %r (%s doesn't match)" % (url, expected, attr)) diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/modal-workflow.js b/wagtail/wagtailadmin/static/wagtailadmin/js/modal-workflow.js index 9aa90f857..bba601468 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/js/modal-workflow.js +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/modal-workflow.js @@ -16,9 +16,12 @@ function ModalWorkflow(opts) { /* remove any previous modals before continuing (closing doesn't remove them from the dom) */ $('body > .modal').remove(); + // set default contents of container var container = $(''); + + // add container to body and hide it, so content can be added to it before display $('body').append(container); - container.modal(); + container.modal('hide'); self.body = container.find('.modal-body'); @@ -49,15 +52,19 @@ function ModalWorkflow(opts) { self.loadResponseText = function(responseText) { var response = eval('(' + responseText + ')'); + self.loadBody(response); }; - self.loadBody = function(body) { - if (body.html) { - self.body.html(body.html); + self.loadBody = function(response) { + if (response.html) { + // if the response is html + self.body.html(response.html); + container.modal('show'); } - if (body.onload) { - body.onload(self); + if (response.onload) { + // if the response is a function + response.onload(self); } }; diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/bootstrap-transition.js b/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/bootstrap-transition.js new file mode 100644 index 000000000..36141c4fc --- /dev/null +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/bootstrap-transition.js @@ -0,0 +1,65 @@ +/* ======================================================================== + * Bootstrap: transition.js v3.1.1 + * http://getbootstrap.com/javascript/#transitions + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function () { 'use strict'; + + (function (o_o) { + typeof define == 'function' && define.amd ? define(['jquery'], o_o) : + typeof exports == 'object' ? o_o(require('jquery')) : o_o(jQuery) + })(function ($) { + + // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) + // ============================================================ + + function transitionEnd() { + var el = document.createElement('bootstrap') + + var transEndEventNames = { + WebkitTransition : 'webkitTransitionEnd', + MozTransition : 'transitionend', + OTransition : 'oTransitionEnd otransitionend', + transition : 'transitionend' + } + + for (var name in transEndEventNames) { + if (el.style[name] !== undefined) { + return { end: transEndEventNames[name] } + } + } + + return false // explicit for ie8 ( ._.) + } + + // http://blog.alexmaccaw.com/css-transitions + $.fn.emulateTransitionEnd = function (duration) { + var called = false + var $el = this + $(this).one('bsTransitionEnd', function () { called = true }) + var callback = function () { if (!called) $($el).trigger($.support.transition.end) } + setTimeout(callback, duration) + return this + } + + $(function () { + $.support.transition = transitionEnd() + + if (!$.support.transition) return + + $.event.special.bsTransitionEnd = { + bindType: $.support.transition.end, + delegateType: $.support.transition.end, + handle: function (e) { + if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) + } + } + }) + + }) + +}(); \ No newline at end of file diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/hallo.js b/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/hallo.js index 7ba3ff2c0..af5950fdb 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/hallo.js +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/hallo.js @@ -1,671 +1,607 @@ -// Generated by CoffeeScript 1.3.3 -(function() { - - $.widget("ncri.hallohtml", { - options: { - editable: null, +/* Hallo 1.0.4 - rich text editor for jQuery UI +* by Henri Bergius and contributors. Available under the MIT license. +* See http://hallojs.org for more information +*/(function() { + (function(jQuery) { + return jQuery.widget('IKS.hallo', { toolbar: null, - uuid: "", - lang: 'en', - dialogOpts: { - autoOpen: false, - width: 600, - height: 'auto', - modal: false, - resizable: true, - draggable: true, - dialogClass: 'htmledit-dialog' + bound: false, + originalContent: '', + previousContent: '', + uuid: '', + selection: null, + _keepActivated: false, + originalHref: null, + options: { + editable: true, + plugins: {}, + toolbar: 'halloToolbarContextual', + parentElement: 'body', + buttonCssClass: null, + toolbarCssClass: null, + toolbarPositionAbove: false, + toolbarOptions: {}, + placeholder: '', + forceStructured: true, + checkTouch: true, + touchScreen: null }, - dialog: null, - buttonCssClass: null - }, - translations: { - en: { - title: 'Edit HTML', - update: 'Update' + _create: function() { + var options, plugin, _ref, + _this = this; + this.id = this._generateUUID(); + if (this.options.checkTouch && this.options.touchScreen === null) { + this.checkTouch(); + } + _ref = this.options.plugins; + for (plugin in _ref) { + options = _ref[plugin]; + if (!jQuery.isPlainObject(options)) { + options = {}; + } + jQuery.extend(options, { + editable: this, + uuid: this.id, + buttonCssClass: this.options.buttonCssClass + }); + jQuery(this.element)[plugin](options); + } + this.element.one('halloactivated', function() { + return _this._prepareToolbar(); + }); + return this.originalContent = this.getContents(); }, - de: { - title: 'HTML bearbeiten', - update: 'Aktualisieren' - } - }, - texts: null, - populateToolbar: function($toolbar) { - var $buttonHolder, $buttonset, id, widget; - widget = this; - this.texts = this.translations[this.options.lang]; - this.options.toolbar = $toolbar; - this.options.dialog = $("
    ").attr('id', "" + this.options.uuid + "-htmledit-dialog"); - $buttonset = $("").addClass(widget.widgetName); - id = "" + this.options.uuid + "-htmledit"; - $buttonHolder = $(''); - $buttonHolder.hallobutton({ - label: this.texts.title, - icon: 'icon-list-alt', - editable: this.options.editable, - command: null, - queryState: false, - uuid: this.options.uuid, - cssClass: this.options.buttonCssClass - }); - $buttonset.append($buttonHolder); - this.button = $buttonHolder; - this.button.click(function() { - if (widget.options.dialog.dialog("isOpen")) { - widget._closeDialog(); + _init: function() { + if (this.options.editable) { + return this.enable(); } else { - widget._openDialog(); + return this.disable(); + } + }, + destroy: function() { + var options, plugin, _ref; + this.disable(); + if (this.toolbar) { + this.toolbar.remove(); + this.element[this.options.toolbar]('destroy'); + } + _ref = this.options.plugins; + for (plugin in _ref) { + options = _ref[plugin]; + jQuery(this.element)[plugin]('destroy'); + } + return jQuery.Widget.prototype.destroy.call(this); + }, + disable: function() { + var _this = this; + this.element.attr("contentEditable", false); + this.element.off("focus", this._activated); + this.element.off("blur", this._deactivated); + this.element.off("keyup paste change", this._checkModified); + this.element.off("keyup", this._keys); + this.element.off("keyup mouseup", this._checkSelection); + this.bound = false; + jQuery(this.element).removeClass('isModified'); + jQuery(this.element).removeClass('inEditMode'); + this.element.parents('a').addBack().each(function(idx, elem) { + var element; + element = jQuery(elem); + if (!element.is('a')) { + return; + } + if (!_this.originalHref) { + return; + } + return element.attr('href', _this.originalHref); + }); + return this._trigger("disabled", null); + }, + enable: function() { + var _this = this; + this.element.parents('a[href]').addBack().each(function(idx, elem) { + var element; + element = jQuery(elem); + if (!element.is('a[href]')) { + return; + } + _this.originalHref = element.attr('href'); + return element.removeAttr('href'); + }); + this.element.attr("contentEditable", true); + if (!jQuery.parseHTML(this.element.html())) { + this.element.html(this.options.placeholder); + jQuery(this.element).addClass('inPlaceholderMode'); + this.element.css({ + 'min-width': this.element.innerWidth(), + 'min-height': this.element.innerHeight() + }); + } + if (!this.bound) { + this.element.on("focus", this, this._activated); + this.element.on("blur", this, this._deactivated); + this.element.on("keyup paste change", this, this._checkModified); + this.element.on("keyup", this, this._keys); + this.element.on("keyup mouseup", this, this._checkSelection); + this.bound = true; + } + if (this.options.forceStructured) { + this._forceStructured(); + } + return this._trigger("enabled", null); + }, + activate: function() { + return this.element.focus(); + }, + containsSelection: function() { + var range; + range = this.getSelection(); + return this.element.has(range.startContainer).length > 0; + }, + getSelection: function() { + var range, sel; + sel = rangy.getSelection(); + range = null; + if (sel.rangeCount > 0) { + range = sel.getRangeAt(0); + } else { + range = rangy.createRange(); + } + return range; + }, + restoreSelection: function(range) { + var sel; + sel = rangy.getSelection(); + return sel.setSingleRange(range); + }, + replaceSelection: function(cb) { + var newTextNode, r, range, sel, t; + if (navigator.appName === 'Microsoft Internet Explorer') { + t = document.selection.createRange().text; + r = document.selection.createRange(); + return r.pasteHTML(cb(t)); + } else { + sel = window.getSelection(); + range = sel.getRangeAt(0); + newTextNode = document.createTextNode(cb(range.extractContents())); + range.insertNode(newTextNode); + range.setStartAfter(newTextNode); + sel.removeAllRanges(); + return sel.addRange(range); + } + }, + removeAllSelections: function() { + if (navigator.appName === 'Microsoft Internet Explorer') { + return range.empty(); + } else { + return window.getSelection().removeAllRanges(); + } + }, + getPluginInstance: function(plugin) { + var instance; + instance = jQuery(this.element).data("IKS-" + plugin); + if (instance) { + return instance; + } + instance = jQuery(this.element).data(plugin); + if (instance) { + return instance; + } + throw new Error("Plugin " + plugin + " not found"); + }, + getContents: function() { + var cleanup, instance, plugin; + for (plugin in this.options.plugins) { + instance = this.getPluginInstance(plugin); + if (!instance) { + continue; + } + cleanup = instance.cleanupContentClone; + if (!jQuery.isFunction(cleanup)) { + continue; + } + jQuery(this.element)[plugin]('cleanupContentClone', this.element); + } + return this.element.html(); + }, + setContents: function(contents) { + return this.element.html(contents); + }, + isModified: function() { + if (!this.previousContent) { + this.previousContent = this.originalContent; + } + return this.previousContent !== this.getContents(); + }, + setUnmodified: function() { + jQuery(this.element).removeClass('isModified'); + return this.previousContent = this.getContents(); + }, + setModified: function() { + jQuery(this.element).addClass('isModified'); + return this._trigger('modified', null, { + editable: this, + content: this.getContents() + }); + }, + restoreOriginalContent: function() { + return this.element.html(this.originalContent); + }, + execute: function(command, value) { + if (document.execCommand(command, false, value)) { + return this.element.trigger("change"); + } + }, + protectFocusFrom: function(el) { + var _this = this; + return el.on("mousedown", function(event) { + event.preventDefault(); + _this._protectToolbarFocus = true; + return setTimeout(function() { + return _this._protectToolbarFocus = false; + }, 300); + }); + }, + keepActivated: function(_keepActivated) { + this._keepActivated = _keepActivated; + }, + _generateUUID: function() { + var S4; + S4 = function() { + return ((1 + Math.random()) * 0x10000 | 0).toString(16).substring(1); + }; + return "" + (S4()) + (S4()) + "-" + (S4()) + "-" + (S4()) + "-" + (S4()) + "-" + (S4()) + (S4()) + (S4()); + }, + _prepareToolbar: function() { + var defaults, instance, plugin, populate, toolbarOptions; + this.toolbar = jQuery('
    ').hide(); + if (this.options.toolbarCssClass) { + this.toolbar.addClass(this.options.toolbarCssClass); + } + defaults = { + editable: this, + parentElement: this.options.parentElement, + toolbar: this.toolbar, + positionAbove: this.options.toolbarPositionAbove + }; + toolbarOptions = jQuery.extend({}, defaults, this.options.toolbarOptions); + this.element[this.options.toolbar](toolbarOptions); + for (plugin in this.options.plugins) { + instance = this.getPluginInstance(plugin); + if (!instance) { + continue; + } + populate = instance.populateToolbar; + if (!jQuery.isFunction(populate)) { + continue; + } + this.element[plugin]('populateToolbar', this.toolbar); + } + this.element[this.options.toolbar]('setPosition'); + return this.protectFocusFrom(this.toolbar); + }, + changeToolbar: function(element, toolbar, hide) { + var originalToolbar; + if (hide == null) { + hide = false; + } + originalToolbar = this.options.toolbar; + this.options.parentElement = element; + if (toolbar) { + this.options.toolbar = toolbar; + } + if (!this.toolbar) { + return; + } + this.element[originalToolbar]('destroy'); + this.toolbar.remove(); + this._prepareToolbar(); + if (hide) { + return this.toolbar.hide(); + } + }, + _checkModified: function(event) { + var widget; + widget = event.data; + if (widget.isModified()) { + return widget.setModified(); + } + }, + _keys: function(event) { + var old, widget; + widget = event.data; + if (event.keyCode === 27) { + old = widget.getContents(); + widget.restoreOriginalContent(event); + widget._trigger("restored", null, { + editable: widget, + content: widget.getContents(), + thrown: old + }); + return widget.turnOff(); + } + }, + _rangesEqual: function(r1, r2) { + if (r1.startContainer !== r2.startContainer) { + return false; + } + if (r1.startOffset !== r2.startOffset) { + return false; + } + if (r1.endContainer !== r2.endContainer) { + return false; + } + if (r1.endOffset !== r2.endOffset) { + return false; + } + return true; + }, + _checkSelection: function(event) { + var widget; + if (event.keyCode === 27) { + return; + } + widget = event.data; + return setTimeout(function() { + var sel; + sel = widget.getSelection(); + if (widget._isEmptySelection(sel) || widget._isEmptyRange(sel)) { + if (widget.selection) { + widget.selection = null; + widget._trigger("unselected", null, { + editable: widget, + originalEvent: event + }); + } + return; + } + if (!widget.selection || !widget._rangesEqual(sel, widget.selection)) { + widget.selection = sel.cloneRange(); + return widget._trigger("selected", null, { + editable: widget, + selection: widget.selection, + ranges: [widget.selection], + originalEvent: event + }); + } + }, 0); + }, + _isEmptySelection: function(selection) { + if (selection.type === "Caret") { + return true; } return false; - }); - this.options.editable.element.on("hallodeactivated", function() { - return widget._closeDialog(); - }); - $toolbar.append($buttonset); - this.options.dialog.dialog(this.options.dialogOpts); - return this.options.dialog.dialog("option", "title", this.texts.title); - }, - _openDialog: function() { - var $editableEl, html, widget, xposition, yposition, - _this = this; - widget = this; - $editableEl = $(this.options.editable.element); - xposition = $editableEl.offset().left + $editableEl.outerWidth() + 10; - yposition = this.options.toolbar.offset().top - $(document).scrollTop(); - this.options.dialog.dialog("option", "position", [xposition, yposition]); - this.options.editable.keepActivated(true); - this.options.dialog.dialog("open"); - this.options.dialog.on('dialogclose', function() { - $('label', _this.button).removeClass('ui-state-active'); - _this.options.editable.element.focus(); - return _this.options.editable.keepActivated(false); - }); - this.options.dialog.html($("