Repository: wagtail/wagtail

Issue:
Auto-collapsing StreamField blocks in the admin while editing a page
### Is your proposal related to a problem?

My clients love the Wagtail CMS but are getting increasingly frustrated when editing their pages. The major complain is that it's very difficult to have an overview of all the StreamField blocks as their pages become more content-rich.

### Describe the solution you'd like

Previously we used [wagtail-react-streamfield](https://github.com/wagtail/wagtail-react-streamfield) which had the exact feature I'd like re-implemented—on page load it's **auto-collapsing all the StreamField blocks** which allows faster scrolling and overview.

Since wagtail-react-streamfield isn't supported any longer, there's no way to have clean overview.

### Describe alternatives you've considered

Another alternative would perhaps to have a "remote control" in the top right corner of the page with in-page anchors to every StreamField block. However, this remote control would have to be updated dynamically every time a block is moved, added, or deleted.

### Additional context

Decided to create a feature request after discussion on Slack, worrying that the conversation would disappear into the void. 

Discussion:
squash-labs[bot]: <h2>Manage this branch in <a href="https://app.squash.io/accounts/">Squash</a></h2>Test this branch here: <a href='https://gasmanfeaturestreamfield-colla-chts4.squash.io' target='_blank'>https://gasmanfeaturestreamfield-colla-chts4.squash.io</a><br/>

Patch:
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index c1c3b218e1..d84d5f564a 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -6,6 +6,7 @@ Changelog
 
  * Added Aging Pages report (Tidjani Dia)
  * Add more SketchFab oEmbed patterns for models (Tom Usher)
+ * Add collapse option to `StreamField`, `StreamBlock`, and `ListBlock` which will load all sub-blocks initially collapsed (Matt Westcott)
  * Fix: Accessibility fixes for Windows high contrast mode; Dashboard icons colour and contrast (Sakshi Uppoor)
  * Fix: Rename additional 'spin' CSS animations to avoid clashes with other libraries (Kevin Gutiérrez)
 
diff --git a/client/src/components/StreamField/blocks/ListBlock.js b/client/src/components/StreamField/blocks/ListBlock.js
index bcc193141f..423b4e693d 100644
--- a/client/src/components/StreamField/blocks/ListBlock.js
+++ b/client/src/components/StreamField/blocks/ListBlock.js
@@ -98,6 +98,11 @@ export class ListBlock extends BaseSequenceBlock {
     this.sequenceContainer = dom.find('[data-streamfield-list-container]');
     this.container = dom;
     this.setState(initialState || []);
+    if (this.blockDef.meta.collapsed) {
+      this.children.forEach(block => {
+        block.collapse();
+      });
+    }
 
     if (initialError) {
       this.setError(initialError);
diff --git a/client/src/components/StreamField/blocks/StreamBlock.js b/client/src/components/StreamField/blocks/StreamBlock.js
index ad1f55341a..0c2f021274 100644
--- a/client/src/components/StreamField/blocks/StreamBlock.js
+++ b/client/src/components/StreamField/blocks/StreamBlock.js
@@ -209,6 +209,11 @@ export class StreamBlock extends BaseSequenceBlock {
     // server-side form handler knows to skip it)
     this.sequenceContainer = dom.find('[data-streamfield-stream-container]');
     this.setState(initialState || []);
+    if (this.blockDef.meta.collapsed) {
+      this.children.forEach(block => {
+        block.collapse();
+      });
+    }
     this.container = dom;
 
     if (initialError) {
diff --git a/docs/reference/streamfield/blocks.rst b/docs/reference/streamfield/blocks.rst
index a94b1b0faf..5e3f8a85e6 100644
--- a/docs/reference/streamfield/blocks.rst
+++ b/docs/reference/streamfield/blocks.rst
@@ -15,6 +15,7 @@ This document details the block types provided by Wagtail for use in :ref:`Strea
    :param min_num: Minimum number of sub-blocks that the stream must have.
    :param max_num: Maximum number of sub-blocks that the stream may have.
    :param block_counts: Specifies the minimum and maximum number of each block type, as a dictionary mapping block names to dicts with (optional) ``min_num`` and ``max_num`` fields.
+   :param collapsed: When true, all blocks are initially collapsed.
 
    .. code-block:: python
 
@@ -447,6 +448,7 @@ Structural block types
    :param form_classname: An HTML ``class`` attribute to set on the root element of this block as displayed in the editing interface.
    :param min_num: Minimum number of sub-blocks that the list must have.
    :param max_num: Maximum number of sub-blocks that the list may have.
+   :param collapsed: When true, all sub-blocks are initially collapsed.
 
 
 .. class:: wagtail.core.blocks.StreamBlock
@@ -501,6 +503,7 @@ Structural block types
    :param min_num: Minimum number of sub-blocks that the stream must have.
    :param max_num: Maximum number of sub-blocks that the stream may have.
    :param block_counts: Specifies the minimum and maximum number of each block type, as a dictionary mapping block names to dicts with (optional) ``min_num`` and ``max_num`` fields.
+   :param collapsed: When true, all sub-blocks are initially collapsed.
    :param form_classname: An HTML ``class`` attribute to set on the root element of this block as displayed in the editing interface.
 
     .. code-block:: python
diff --git a/docs/releases/2.16.md b/docs/releases/2.16.md
index 82eb0d29e9..e0594f2d59 100644
--- a/docs/releases/2.16.md
+++ b/docs/releases/2.16.md
@@ -13,6 +13,7 @@
 
  * Added Aging Pages report (Tidjani Dia)
  * Add more SketchFab oEmbed patterns for models (Tom Usher)
+ * Add collapse option to `StreamField`, `StreamBlock`, and `ListBlock` which will load all sub-blocks initially collapsed (Matt Westcott)
 
 
 ### Bug fixes
diff --git a/wagtail/core/blocks/list_block.py b/wagtail/core/blocks/list_block.py
index 6a780c3f80..84636e61a6 100644
--- a/wagtail/core/blocks/list_block.py
+++ b/wagtail/core/blocks/list_block.py
@@ -184,6 +184,7 @@ class ListBlock(Block):
         form_classname = None
         min_num = None
         max_num = None
+        collapsed = False
 
     MUTABLE_META_ATTRIBUTES = ['min_num', 'max_num']
 
@@ -194,6 +195,7 @@ class ListBlockAdapter(Adapter):
     def js_args(self, block):
         meta = {
             'label': block.label, 'icon': block.meta.icon, 'classname': block.meta.form_classname,
+            'collapsed': block.meta.collapsed,
             'strings': {
                 'MOVE_UP': _("Move up"),
                 'MOVE_DOWN': _("Move down"),
diff --git a/wagtail/core/blocks/stream_block.py b/wagtail/core/blocks/stream_block.py
index 14d9829dfa..2350d73f62 100644
--- a/wagtail/core/blocks/stream_block.py
+++ b/wagtail/core/blocks/stream_block.py
@@ -326,8 +326,9 @@ class BaseStreamBlock(Block):
         min_num = None
         max_num = None
         block_counts = {}
+        collapsed = False
 
-    MUTABLE_META_ATTRIBUTES = ['required', 'min_num', 'max_num', 'block_counts']
+    MUTABLE_META_ATTRIBUTES = ['required', 'min_num', 'max_num', 'block_counts', 'collapsed']
 
 
 class StreamBlock(BaseStreamBlock, metaclass=DeclarativeSubBlocksMetaclass):
@@ -590,6 +591,7 @@ class StreamBlockAdapter(Adapter):
             'classname': block.meta.form_classname,
             'maxNum': block.meta.max_num, 'minNum': block.meta.min_num,
             'blockCounts': block.meta.block_counts,
+            'collapsed': block.meta.collapsed,
             'strings': {
                 'MOVE_UP': _("Move up"),
                 'MOVE_DOWN': _("Move down"),
diff --git a/wagtail/core/fields.py b/wagtail/core/fields.py
index 63ce609bbb..93f2976069 100644
--- a/wagtail/core/fields.py
+++ b/wagtail/core/fields.py
@@ -63,7 +63,7 @@ class StreamField(models.Field):
 
         # extract kwargs that are to be passed on to the block, not handled by super
         block_opts = {}
-        for arg in ['min_num', 'max_num', 'block_counts']:
+        for arg in ['min_num', 'max_num', 'block_counts', 'collapsed']:
             if arg in kwargs:
                 block_opts[arg] = kwargs.pop(arg)
 


Test patch:
diff --git a/wagtail/core/tests/test_blocks.py b/wagtail/core/tests/test_blocks.py
index f54da32231..db9033bbfc 100644
--- a/wagtail/core/tests/test_blocks.py
+++ b/wagtail/core/tests/test_blocks.py
@@ -2170,6 +2170,7 @@ class TestListBlock(WagtailTestUtils, SimpleTestCase):
             'label': 'Test listblock',
             'icon': 'placeholder',
             'classname': None,
+            'collapsed': False,
             'strings': {
                 'DELETE': 'Delete',
                 'DUPLICATE': 'Duplicate',
@@ -2196,6 +2197,7 @@ class TestListBlock(WagtailTestUtils, SimpleTestCase):
             'label': 'Test listblock',
             'icon': 'placeholder',
             'classname': None,
+            'collapsed': False,
             'minNum': 2,
             'maxNum': 5,
             'strings': {
@@ -2321,6 +2323,7 @@ class TestListBlock(WagtailTestUtils, SimpleTestCase):
             'label': 'Test listblock',
             'icon': 'placeholder',
             'classname': 'special-list-class',
+            'collapsed': False,
             'strings': {
                 'DELETE': 'Delete',
                 'DUPLICATE': 'Duplicate',
@@ -2351,6 +2354,7 @@ class TestListBlock(WagtailTestUtils, SimpleTestCase):
             'label': 'Test listblock',
             'icon': 'placeholder',
             'classname': 'custom-list-class',
+            'collapsed': False,
             'strings': {
                 'DELETE': 'Delete',
                 'DUPLICATE': 'Duplicate',
@@ -2726,6 +2730,7 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase):
             'label': 'Test streamblock',
             'icon': 'placeholder',
             'classname': None,
+            'collapsed': False,
             'maxNum': None,
             'minNum': None,
             'blockCounts': {},
@@ -3329,6 +3334,7 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase):
             'minNum': None,
             'maxNum': None,
             'blockCounts': {},
+            'collapsed': False,
             'required': True,
             'classname': 'rocket-section',
             'strings': {
@@ -3360,6 +3366,7 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase):
             'minNum': None,
             'maxNum': None,
             'blockCounts': {},
+            'collapsed': False,
             'required': True,
             'classname': 'profile-block-large',
             'strings': {