Created
February 20, 2022 06:41
-
-
Save lb-/55fea7ec9a0be6b6c2d9184a9d77f711 to your computer and use it in GitHub Desktop.
Diff - adding Diagram via jQuery
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/products/edit_handlers.py b/products/edit_handlers.py | |
new file mode 100644 | |
index 0000000000000000000000000000000000000000..44c5faabbb435ca9962b92985115543e60924f2e | |
--- /dev/null | |
+++ b/products/edit_handlers.py | |
@@ -0,0 +1,29 @@ | |
+from wagtail.images.edit_handlers import ImageChooserPanel | |
+from wagtail.images.widgets import AdminImageChooser | |
+ | |
+ | |
+class AdminPreviewImageChooser(AdminImageChooser): | |
+ """ | |
+ Generates a larger version of the AdminImageChooser | |
+ Currently limited to showing the large image on load only. | |
+ """ | |
+ | |
+ def get_value_data(self, value): | |
+ value_data = super().get_value_data(value) | |
+ | |
+ if value_data: | |
+ image = self.image_model.objects.get(pk=value_data["id"]) | |
+ # note: the image string here should match what is used in the template | |
+ preview_image = image.get_rendition("fill-400x400") | |
+ value_data["preview"] = { | |
+ "width": preview_image.width, | |
+ "height": preview_image.height, | |
+ "url": preview_image.url, | |
+ } | |
+ | |
+ return value_data | |
+ | |
+ | |
+class PreviewImageChooserPanel(ImageChooserPanel): | |
+ def widget_overrides(self): | |
+ return {self.field_name: AdminPreviewImageChooser} | |
diff --git a/products/migrations/0017_productschematic.py b/products/migrations/0017_productschematic.py | |
new file mode 100644 | |
index 0000000000000000000000000000000000000000..ca97426f7047fbc1fe0f9af789f785b899061d59 | |
--- /dev/null | |
+++ b/products/migrations/0017_productschematic.py | |
@@ -0,0 +1,27 @@ | |
+# Generated by Django 3.2.7 on 2021-11-02 16:12 | |
+ | |
+from django.db import migrations, models | |
+import django.db.models.deletion | |
+import wagtail.search.index | |
+ | |
+ | |
+class Migration(migrations.Migration): | |
+ | |
+ dependencies = [ | |
+ ('base', '0006_customdocument_customimage_customrendition_footersettings_footersettingsrelatedlinks'), | |
+ ('products', '0016_alter_guidepage_related_items'), | |
+ ] | |
+ | |
+ operations = [ | |
+ migrations.CreateModel( | |
+ name='ProductSchematic', | |
+ fields=[ | |
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |
+ ('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.customimage')), | |
+ ], | |
+ options={ | |
+ 'abstract': False, | |
+ }, | |
+ bases=(wagtail.search.index.Indexed, models.Model), | |
+ ), | |
+ ] | |
diff --git a/products/migrations/0018_delete_productschematic.py b/products/migrations/0018_delete_productschematic.py | |
new file mode 100644 | |
index 0000000000000000000000000000000000000000..42b9f0fd162c6b5b081d1e568a0f534614eef2bc | |
--- /dev/null | |
+++ b/products/migrations/0018_delete_productschematic.py | |
@@ -0,0 +1,16 @@ | |
+# Generated by Django 3.2.7 on 2021-11-02 16:13 | |
+ | |
+from django.db import migrations | |
+ | |
+ | |
+class Migration(migrations.Migration): | |
+ | |
+ dependencies = [ | |
+ ('products', '0017_productschematic'), | |
+ ] | |
+ | |
+ operations = [ | |
+ migrations.DeleteModel( | |
+ name='ProductSchematic', | |
+ ), | |
+ ] | |
diff --git a/products/migrations/0019_schematic_schematicpoint.py b/products/migrations/0019_schematic_schematicpoint.py | |
new file mode 100644 | |
index 0000000000000000000000000000000000000000..f82758dfaa7504a3c03484e86d877bae0b10e8f8 | |
--- /dev/null | |
+++ b/products/migrations/0019_schematic_schematicpoint.py | |
@@ -0,0 +1,41 @@ | |
+# Generated by Django 3.2.7 on 2021-11-02 16:14 | |
+ | |
+from django.db import migrations, models | |
+import django.db.models.deletion | |
+import modelcluster.fields | |
+import wagtail.search.index | |
+ | |
+ | |
+class Migration(migrations.Migration): | |
+ | |
+ dependencies = [ | |
+ ('base', '0006_customdocument_customimage_customrendition_footersettings_footersettingsrelatedlinks'), | |
+ ('products', '0018_delete_productschematic'), | |
+ ] | |
+ | |
+ operations = [ | |
+ migrations.CreateModel( | |
+ name='Schematic', | |
+ fields=[ | |
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |
+ ('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.customimage')), | |
+ ], | |
+ options={ | |
+ 'abstract': False, | |
+ }, | |
+ bases=(wagtail.search.index.Indexed, models.Model), | |
+ ), | |
+ migrations.CreateModel( | |
+ name='SchematicPoint', | |
+ fields=[ | |
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |
+ ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), | |
+ ('label', models.CharField(max_length=255)), | |
+ ('schematic', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='points', to='products.schematic')), | |
+ ], | |
+ options={ | |
+ 'ordering': ['sort_order'], | |
+ 'abstract': False, | |
+ }, | |
+ ), | |
+ ] | |
diff --git a/products/migrations/0020_schematic_title.py b/products/migrations/0020_schematic_title.py | |
new file mode 100644 | |
index 0000000000000000000000000000000000000000..6cf79ba12e41cdead07dacd32e287b89dd52444b | |
--- /dev/null | |
+++ b/products/migrations/0020_schematic_title.py | |
@@ -0,0 +1,18 @@ | |
+# Generated by Django 3.2.7 on 2021-11-02 16:16 | |
+ | |
+from django.db import migrations, models | |
+ | |
+ | |
+class Migration(migrations.Migration): | |
+ | |
+ dependencies = [ | |
+ ('products', '0019_schematic_schematicpoint'), | |
+ ] | |
+ | |
+ operations = [ | |
+ migrations.AddField( | |
+ model_name='schematic', | |
+ name='title', | |
+ field=models.CharField(blank=True, max_length=255, null=True), | |
+ ), | |
+ ] | |
diff --git a/products/migrations/0021_auto_20211103_0525.py b/products/migrations/0021_auto_20211103_0525.py | |
new file mode 100644 | |
index 0000000000000000000000000000000000000000..38fa3366663f1ae360a73d4442ba6122d9db7206 | |
--- /dev/null | |
+++ b/products/migrations/0021_auto_20211103_0525.py | |
@@ -0,0 +1,24 @@ | |
+# Generated by Django 3.2.7 on 2021-11-03 05:25 | |
+ | |
+import django.core.validators | |
+from django.db import migrations, models | |
+ | |
+ | |
+class Migration(migrations.Migration): | |
+ | |
+ dependencies = [ | |
+ ('products', '0020_schematic_title'), | |
+ ] | |
+ | |
+ operations = [ | |
+ migrations.AddField( | |
+ model_name='schematicpoint', | |
+ name='x_percent', | |
+ field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)]), | |
+ ), | |
+ migrations.AddField( | |
+ model_name='schematicpoint', | |
+ name='y_percent', | |
+ field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)]), | |
+ ), | |
+ ] | |
diff --git a/products/migrations/0022_auto_20211103_0526.py b/products/migrations/0022_auto_20211103_0526.py | |
new file mode 100644 | |
index 0000000000000000000000000000000000000000..695367f6acc76caeccfd0e51db54d1f2d2e7a114 | |
--- /dev/null | |
+++ b/products/migrations/0022_auto_20211103_0526.py | |
@@ -0,0 +1,24 @@ | |
+# Generated by Django 3.2.7 on 2021-11-03 05:26 | |
+ | |
+import django.core.validators | |
+from django.db import migrations, models | |
+ | |
+ | |
+class Migration(migrations.Migration): | |
+ | |
+ dependencies = [ | |
+ ('products', '0021_auto_20211103_0525'), | |
+ ] | |
+ | |
+ operations = [ | |
+ migrations.AlterField( | |
+ model_name='schematicpoint', | |
+ name='x_percent', | |
+ field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)], verbose_name='X'), | |
+ ), | |
+ migrations.AlterField( | |
+ model_name='schematicpoint', | |
+ name='y_percent', | |
+ field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)], verbose_name='Y'), | |
+ ), | |
+ ] | |
diff --git a/products/migrations/0023_auto_20211108_1558.py b/products/migrations/0023_auto_20211108_1558.py | |
new file mode 100644 | |
index 0000000000000000000000000000000000000000..46c1a0e8d41b2346c27b0fe9bbbefa0e3992a719 | |
--- /dev/null | |
+++ b/products/migrations/0023_auto_20211108_1558.py | |
@@ -0,0 +1,24 @@ | |
+# Generated by Django 3.2.7 on 2021-11-08 15:58 | |
+ | |
+import django.core.validators | |
+from django.db import migrations, models | |
+ | |
+ | |
+class Migration(migrations.Migration): | |
+ | |
+ dependencies = [ | |
+ ('products', '0022_auto_20211103_0526'), | |
+ ] | |
+ | |
+ operations = [ | |
+ migrations.AlterField( | |
+ model_name='schematicpoint', | |
+ name='x_percent', | |
+ field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)], verbose_name='X →'), | |
+ ), | |
+ migrations.AlterField( | |
+ model_name='schematicpoint', | |
+ name='y_percent', | |
+ field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)], verbose_name='Y ↑'), | |
+ ), | |
+ ] | |
diff --git a/products/migrations/0024_auto_20211113_2341.py b/products/migrations/0024_auto_20211113_2341.py | |
new file mode 100644 | |
index 0000000000000000000000000000000000000000..fd6e0c7349db78e9041c05041c54a3b340ec9ad7 | |
--- /dev/null | |
+++ b/products/migrations/0024_auto_20211113_2341.py | |
@@ -0,0 +1,23 @@ | |
+# Generated by Django 3.2.7 on 2021-11-13 23:41 | |
+ | |
+from django.db import migrations | |
+ | |
+ | |
+class Migration(migrations.Migration): | |
+ | |
+ dependencies = [ | |
+ ('products', '0023_auto_20211108_1558'), | |
+ ] | |
+ | |
+ operations = [ | |
+ migrations.RemoveField( | |
+ model_name='schematicpoint', | |
+ name='schematic', | |
+ ), | |
+ migrations.DeleteModel( | |
+ name='Schematic', | |
+ ), | |
+ migrations.DeleteModel( | |
+ name='SchematicPoint', | |
+ ), | |
+ ] | |
diff --git a/products/migrations/0025_diagrampoint.py b/products/migrations/0025_diagrampoint.py | |
new file mode 100644 | |
index 0000000000000000000000000000000000000000..f1911ee9e199222228d02235115c32f46b01312b | |
--- /dev/null | |
+++ b/products/migrations/0025_diagrampoint.py | |
@@ -0,0 +1,32 @@ | |
+# Generated by Django 3.2.7 on 2021-11-13 23:48 | |
+ | |
+import django.core.validators | |
+from django.db import migrations, models | |
+import django.db.models.deletion | |
+import modelcluster.fields | |
+ | |
+ | |
+class Migration(migrations.Migration): | |
+ | |
+ dependencies = [ | |
+ ('wagtailcore', '0062_comment_models_and_pagesubscription'), | |
+ ('products', '0024_auto_20211113_2341'), | |
+ ] | |
+ | |
+ operations = [ | |
+ migrations.CreateModel( | |
+ name='DiagramPoint', | |
+ fields=[ | |
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |
+ ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), | |
+ ('x_percent', models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)], verbose_name='X →')), | |
+ ('y_percent', models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)], verbose_name='Y ↑')), | |
+ ('page', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.page')), | |
+ ('product', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='points', to='products.productpage')), | |
+ ], | |
+ options={ | |
+ 'ordering': ['sort_order'], | |
+ 'abstract': False, | |
+ }, | |
+ ), | |
+ ] | |
diff --git a/products/models.py b/products/models.py | |
index 9a33fdc8f8025576fab777b90d746f3d4cc259e3..370b146e99c712dee2a7738f85b272e640b07533 100644 | |
--- a/products/models.py | |
+++ b/products/models.py | |
@@ -1,16 +1,21 @@ | |
from collections import OrderedDict | |
+from django import forms | |
+from django.core.validators import MaxValueValidator, MinValueValidator | |
from django.db import models | |
-from django.db.models.fields import CharField | |
+ | |
+from modelcluster.fields import ParentalKey | |
from wagtail.admin.edit_handlers import ( | |
FieldPanel, | |
+ FieldRowPanel, | |
InlinePanel, | |
MultiFieldPanel, | |
StreamFieldPanel, | |
) | |
+from wagtail.admin.edit_handlers import PageChooserPanel | |
from wagtail.core.fields import RichTextField, StreamField | |
-from wagtail.core.models import Page | |
+from wagtail.core.models import Orderable, Page | |
from wagtail.documents import get_document_model_string | |
from wagtail.documents.edit_handlers import DocumentChooserPanel | |
from wagtail.images import get_image_model_string | |
@@ -21,6 +26,52 @@ from materials.models import Package | |
from metrics.models import TrackablePageMixin | |
from .blocks import RelatedItemBlock | |
+from .edit_handlers import PreviewImageChooserPanel | |
+ | |
+ | |
+class DiagramPoint(Orderable, models.Model): | |
+ page = ParentalKey( | |
+ Page, | |
+ on_delete=models.CASCADE, | |
+ ) | |
+ product = ParentalKey( | |
+ "products.ProductPage", | |
+ on_delete=models.CASCADE, | |
+ related_name="points", | |
+ ) | |
+ x_percent = models.PositiveSmallIntegerField( | |
+ verbose_name="X →", | |
+ default=0, | |
+ validators=[MaxValueValidator(100), MinValueValidator(0)], | |
+ ) | |
+ y_percent = models.PositiveSmallIntegerField( | |
+ verbose_name="Y ↑", | |
+ default=0, | |
+ validators=[MaxValueValidator(100), MinValueValidator(0)], | |
+ ) | |
+ | |
+ panels = [ | |
+ PageChooserPanel( | |
+ "page", | |
+ ["products.ProductPage", "products.GuidePage"], | |
+ ), | |
+ FieldRowPanel( | |
+ [ | |
+ FieldPanel( | |
+ "x_percent", | |
+ widget=forms.NumberInput(attrs={"min": 0, "max": 100}), | |
+ ), | |
+ FieldPanel( | |
+ "y_percent", | |
+ widget=forms.NumberInput(attrs={"min": 0, "max": 100}), | |
+ ), | |
+ ] | |
+ ), | |
+ ] | |
+ | |
+ def __str__(self): | |
+ label = getattr(self.page, "title", "Point") | |
+ return f"{label}" | |
class ProductPage(TrackablePageMixin, Page): | |
@@ -48,7 +99,10 @@ class ProductPage(TrackablePageMixin, Page): | |
# Editor panels configuration | |
content_panels = Page.content_panels + [ | |
- ImageChooserPanel("cover"), | |
+ MultiFieldPanel( | |
+ [PreviewImageChooserPanel("cover"), InlinePanel("points")], | |
+ classname="points-field-panel", | |
+ ), | |
FieldPanel("body", classname="full"), | |
MultiFieldPanel( | |
[ | |
diff --git a/products/templates/products/product_page.html b/products/templates/products/product_page.html | |
index 3856cca910201a4facfec206a1b48cfb68c27d62..2d15cd9a4725936f5020069291402092e9515134 100644 | |
--- a/products/templates/products/product_page.html | |
+++ b/products/templates/products/product_page.html | |
@@ -6,7 +6,16 @@ | |
<div class="container"> | |
<div class="row mb-3"> | |
<div class="col"> | |
- {% image page.cover width-400 %} | |
+ <div class="diagram-image-container"> | |
+ {% image page.cover fill-400x400 class="product-image" %} | |
+ {% for point in page.points.all %} | |
+ {% if point.page %} | |
+ <a class="point" style="left: {{ point.x_percent }}%; bottom: {{ point.y_percent }}%;" title="{{ point }}" href="{{ point.page.get_url }}""> | |
+ <span class="label sr-only">{{ point.label }}</span> | |
+ </a> | |
+ {% endif %} | |
+ {% endfor %} | |
+ </div> | |
</div> | |
<div class="col"> | |
<div class="accordion" id="sections-accordion"> | |
@@ -15,7 +24,7 @@ | |
<div class="accordion-item"> | |
<h2 class="accordion-header" id="{{id}}-heading"> | |
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#{{id}}-collapse" aria-expanded="true" aria-controls="{{id}}-collapse"> | |
- {{ label }} | |
+ <span>{{ label }}</span> | |
</button> | |
</h2> | |
<div class="accordion-collapse collapse show" id="{{id}}-collapse" aria-labelledby="{{id}}-heading" data-bs-parent="#sections-accordion"> | |
diff --git a/products/wagtail_hooks.py b/products/wagtail_hooks.py | |
new file mode 100644 | |
index 0000000000000000000000000000000000000000..05e1163fdf2a5a4a1dd3d251580ae42d30d94987 | |
--- /dev/null | |
+++ b/products/wagtail_hooks.py | |
@@ -0,0 +1,20 @@ | |
+from django.templatetags.static import static | |
+from django.utils.html import format_html | |
+ | |
+from wagtail.core import hooks | |
+ | |
+ | |
+@hooks.register("insert_editor_css") | |
+def editor_css(): | |
+ return format_html( | |
+ '<link rel="stylesheet" href="{}">', | |
+ static("css/custom-admin-editor.css"), | |
+ ) | |
+ | |
+ | |
+@hooks.register("insert_editor_js") | |
+def points_field_panel_js(): | |
+ return format_html( | |
+ '<script src="{}"></script>', | |
+ static("js/points-field-panel.js"), | |
+ ) | |
diff --git a/static/css/custom-admin-editor.css b/static/css/custom-admin-editor.css | |
new file mode 100644 | |
index 0000000000000000000000000000000000000000..2fff16a1893d44a78ab47f5d357b0d6b28d0e053 | |
--- /dev/null | |
+++ b/static/css/custom-admin-editor.css | |
@@ -0,0 +1,68 @@ | |
+/* ---- Admin Preview Image Chooser ---- */ | |
+ | |
+.admin_preview_image_chooser label { | |
+ /* hide the image label - ensure screen readers read it */ | |
+ position: absolute; | |
+ width: 1px; | |
+ height: 1px; | |
+ padding: 0; | |
+ margin: -1px; | |
+ overflow: hidden; | |
+ clip: rect(0,0,0,0); | |
+ border: 0; | |
+} | |
+ | |
+ | |
+.admin_preview_image_chooser .chosen { | |
+ display: flex; | |
+ align-items: center; | |
+ padding-left: 0; | |
+ flex-direction: column; | |
+} | |
+ | |
+.admin_preview_image_chooser .chosen .preview-image { | |
+ float: none; | |
+ margin: 1.5rem 0; | |
+ max-width: none; | |
+ position: relative; | |
+} | |
+ | |
+.admin_preview_image_chooser .chosen .preview-image img { | |
+ max-width: none; | |
+ max-height: none; | |
+} | |
+ | |
+/* ---- Diagram Points ---- */ | |
+ | |
+.points-field-panel .point { | |
+ color: black; | |
+ font-style: normal; | |
+ text-align: center; | |
+ height: 1.5rem; | |
+ width: 1.5rem; | |
+ display: block; | |
+ background: red; | |
+ border-radius: 50%; | |
+ position: absolute; | |
+ transform: translate(-0.75rem, 0.75rem); | |
+} | |
+ | |
+.points-field-panel .multiple { | |
+ counter-reset: points-counter; | |
+} | |
+ | |
+.points-field-panel .multiple > li { | |
+ counter-increment: points-counter; | |
+} | |
+ | |
+.points-field-panel .multiple > li:before { | |
+ color: black; | |
+ content: counter(points-counter); | |
+ background: lightblue; | |
+ border-radius: 50%; | |
+ height: 1.5rem; | |
+ width: 1.5rem; | |
+ text-align: center; | |
+ position: absolute; | |
+ top: 3rem; | |
+} | |
\ No newline at end of file | |
diff --git a/static/js/points-field-panel.js b/static/js/points-field-panel.js | |
new file mode 100644 | |
index 0000000000000000000000000000000000000000..9349c7bb268c2aca41c2a800421e4ee134ac0798 | |
--- /dev/null | |
+++ b/static/js/points-field-panel.js | |
@@ -0,0 +1,165 @@ | |
+document.addEventListener("DOMContentLoaded", () => { | |
+ /** | |
+ * Create a `point` element which will be positioned inside the div that wraps | |
+ * the image (.preview-image). | |
+ * | |
+ * data-inline-panel-child - has an id which we will need to use to reference the input | |
+ * | |
+ * @param {{}} options | |
+ * @param {number} options.order | |
+ * @param {string} options.panelId | |
+ * @param {number} options.xPercent - integer (e.g. 30 for 30%) | |
+ * @param {number} options.yPercent - integer (e.g. 30 for 30%) | |
+ * @returns | |
+ */ | |
+ const createPoint = ({ order, panelId, xPercent, yPercent }) => { | |
+ const point = document.createElement("i"); | |
+ | |
+ point.className = "point"; | |
+ point.id = `${panelId}-point`; | |
+ point.style = `left: ${xPercent}%; bottom: ${yPercent}%;`; | |
+ // transform set to ensure the centre of the point aligns with % values | |
+ point.textContent = order; | |
+ point.setAttribute("draggable", true); | |
+ point.setAttribute("data-for-inline-panel", panelId); | |
+ | |
+ return point; | |
+ }; | |
+ | |
+ document.querySelectorAll(".points-field-panel").forEach((panel) => { | |
+ const imageContainer = panel.querySelector(".preview-image"); | |
+ | |
+ /** | |
+ * Synchronises the point's text content with the re-ordered position of | |
+ * their matching inline panel element. | |
+ * Note: cannot use the order input directly as this is only a relative order. | |
+ */ | |
+ const syncPointOrdering = () => { | |
+ const inlinePanelsEnabled = Array.from( | |
+ panel.querySelectorAll('input[id$="-DELETE"]:not([value])') | |
+ ).map((elem) => elem.closest("[data-inline-panel-child]").id); | |
+ | |
+ panel.querySelectorAll("i.point").forEach((point) => { | |
+ const inlinePanelId = point.attributes["data-for-inline-panel"].value; | |
+ const order = inlinePanelsEnabled.indexOf(inlinePanelId) + 1; | |
+ point.textContent = order; | |
+ }); | |
+ }; | |
+ | |
+ /** | |
+ * Initialise the inline panel element's listeners and create a point for the element. | |
+ * | |
+ * @param {HTMLElement} inlinePanel | |
+ */ | |
+ const initInlinePanel = (inlinePanel) => { | |
+ const panelId = inlinePanel.id; | |
+ const orderInput = inlinePanel.querySelector('input[id$="-ORDER"]'); | |
+ const order = orderInput.value; | |
+ const xInput = inlinePanel.querySelector("input[id*=x"); | |
+ const xPercent = xInput.value; | |
+ const yInput = inlinePanel.querySelector("input[id*=y"); | |
+ const yPercent = yInput.value; | |
+ | |
+ const point = createPoint({ order, panelId, xPercent, yPercent }); | |
+ | |
+ imageContainer.appendChild(point); | |
+ | |
+ /** | |
+ * Add a listener to allow dragging behaviour. | |
+ */ | |
+ point.addEventListener("dragstart", (event) => { | |
+ event.dataTransfer.setData("text/plain", event.target.id); | |
+ event.dataTransfer.effectAllowed = "move"; | |
+ }); | |
+ | |
+ /** | |
+ * Update the point's x or y' position (via css variables) | |
+ * When the x or y inputs are changed manually. | |
+ */ | |
+ [xInput, yInput].forEach((input, i) => { | |
+ const key = i === 0 ? "x" : "y"; | |
+ const styleAttr = { x: "left", y: "bottom" }[key]; | |
+ input.addEventListener("change", ({ target }) => { | |
+ const value = parseInt(target.value, 10); | |
+ point.style.setProperty(styleAttr, `${value}%`); | |
+ }); | |
+ }); | |
+ | |
+ /** | |
+ * Add listener to the delete button which will remove the point and | |
+ * sync the text of the points to match the new ordering. | |
+ */ | |
+ inlinePanel | |
+ .querySelector('button[id$="-DELETE-button"]') | |
+ .addEventListener("click", () => { | |
+ point.remove(); | |
+ syncPointOrdering(); | |
+ }); | |
+ | |
+ /** | |
+ * Add listeners to each of the panels' move buttons (up/down) so that | |
+ * when they are clicked the text of each point is updated. | |
+ */ | |
+ inlinePanel | |
+ .querySelectorAll('button[id*="-move-"]') | |
+ .forEach((moveButton) => { | |
+ moveButton.addEventListener("click", syncPointOrdering); | |
+ }); | |
+ }; | |
+ | |
+ /** | |
+ * Initialise each panel on page load, which will create the points | |
+ * and all all listeners. | |
+ */ | |
+ panel | |
+ .querySelectorAll("[data-inline-panel-child]") | |
+ .forEach(initInlinePanel); | |
+ | |
+ /** | |
+ * Add click listener to add button so that the panel's data can be | |
+ * initialised to show the point and add listeners. | |
+ */ | |
+ panel.querySelector('[id$="-ADD"]').addEventListener("click", () => { | |
+ initInlinePanel( | |
+ panel.querySelector("[data-inline-panel-child]:last-child") | |
+ ); | |
+ }); | |
+ | |
+ /** | |
+ * Allow drag over the image container for dragging points. | |
+ * | |
+ * @param {object} event | |
+ */ | |
+ imageContainer.ondragover = (event) => { | |
+ event.preventDefault(); | |
+ event.dataTransfer.dropEffect = "move"; | |
+ }; | |
+ | |
+ /** | |
+ * Allow drop over the image container for dropping points. | |
+ * Also ensure we update the point's x/y css position variables | |
+ * and also update the point's related inline panel x/y fields. | |
+ * | |
+ * @param {object} event | |
+ */ | |
+ imageContainer.ondrop = (event) => { | |
+ event.preventDefault(); | |
+ | |
+ pointId = event.dataTransfer.getData("text/plain"); | |
+ const { height, width } = imageContainer.getBoundingClientRect(); | |
+ const xPercent = Math.round((event.offsetX / width) * 100); | |
+ const yPercent = Math.round((1 - event.offsetY / height) * 100); | |
+ | |
+ const point = document.getElementById(pointId); | |
+ point.style.setProperty("left", `${xPercent}%`); | |
+ point.style.setProperty("bottom", `${yPercent}%`); | |
+ | |
+ const inlinePanel = document.getElementById( | |
+ point.attributes["data-for-inline-panel"].value | |
+ ); | |
+ | |
+ inlinePanel.querySelector("input[id*=x").value = xPercent; | |
+ inlinePanel.querySelector("input[id*=y").value = yPercent; | |
+ }; | |
+ }); | |
+}); | |
diff --git a/static/main.css b/static/main.css | |
index 30f87ba6601513303970bf903884c522bbec7949..55d62eb1e5cbe9b40e51f55a8fdc62fbc7370275 100644 | |
--- a/static/main.css | |
+++ b/static/main.css | |
@@ -13,4 +13,26 @@ main { | |
footer { | |
flex-shrink: 0; | |
-} | |
\ No newline at end of file | |
+} | |
+ | |
+/* ---- Product Page (Diagram) ---- */ | |
+ | |
+.diagram-image-container { | |
+ display: inline-block; | |
+ position: relative; | |
+} | |
+ | |
+.diagram-image-container .point { | |
+ position: absolute; | |
+ width: 10px; | |
+ height: 10px; | |
+ background: #3aa734; | |
+ border-radius: 50%; | |
+ box-shadow: 0 0 0 3px rgba(58,167,52,.3); | |
+ display: block; | |
+ transition: .5s; | |
+} | |
+ | |
+.diagram-image-container .point:hover { | |
+ transform: scale(1.6); | |
+} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment