Skip to content

Instantly share code, notes, and snippets.

@lb-
Created February 20, 2022 06:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lb-/55fea7ec9a0be6b6c2d9184a9d77f711 to your computer and use it in GitHub Desktop.
Save lb-/55fea7ec9a0be6b6c2d9184a9d77f711 to your computer and use it in GitHub Desktop.
Diff - adding Diagram via jQuery
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