@interaktivgmbh/volto-comparison-slider 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ {
2
+ "plugins": {
3
+ "../../core/packages/scripts/prepublish.js": {}
4
+ },
5
+ "hooks": {
6
+ "after:bump": [
7
+ "pipx run towncrier build --draft --yes --version ${version} > .changelog.draft",
8
+ "pipx run towncrier build --yes --version ${version}",
9
+ "cp ../../README.md ./ && cp CHANGELOG.md ../../CHANGELOG.md",
10
+ "python3 -c 'import json; data = json.load(open(\"../../package.json\")); data[\"version\"] = \"${version}\"; json.dump(data, open(\"../../package.json\", \"w\"), indent=2)'",
11
+ "git add ../../CHANGELOG.md ../../package.json"
12
+ ],
13
+ "after:release": "rm .changelog.draft README.md"
14
+ },
15
+ "npm": {
16
+ "publish": true
17
+ },
18
+ "plonePrePublish": {
19
+ "publish": true
20
+ },
21
+ "git": {
22
+ "changelog": "pipx run towncrier build --draft --yes --version 0.0.0",
23
+ "requireUpstream": false,
24
+ "requireCleanWorkingDir": false,
25
+ "commitMessage": "Release ${version}",
26
+ "tagName": "${version}",
27
+ "tagAnnotation": "Release ${version}"
28
+ },
29
+ "github": {
30
+ "release": true,
31
+ "releaseName": "${version}",
32
+ "releaseNotes": "cat .changelog.draft"
33
+ }
34
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ <!-- You should *NOT* be adding new change log entries to this file.
4
+ You should create a file in the news directory instead.
5
+ For helpful instructions, please see:
6
+ https://6.docs.plone.org/contributing/index.html#contributing-change-log-label
7
+ -->
8
+
9
+ <!-- towncrier release notes start -->
@@ -0,0 +1,17 @@
1
+ module.exports = function (api) {
2
+ api.cache(true);
3
+ const presets = ['razzle'];
4
+ const plugins = [
5
+ [
6
+ 'react-intl', // React Intl extractor, required for the whole i18n infrastructure to work
7
+ {
8
+ messagesDir: './build/messages/',
9
+ },
10
+ ],
11
+ ];
12
+
13
+ return {
14
+ plugins,
15
+ presets,
16
+ };
17
+ };
@@ -0,0 +1,63 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: volto-comparison-slider\n"
4
+ "Report-Msgid-Bugs-To: \n"
5
+ "POT-Creation-Date: \n"
6
+ "PO-Revision-Date: \n"
7
+ "Last-Translator: \n"
8
+ "Language: de\n"
9
+ "Language-Team: German\n"
10
+ "Content-Type: text/plain; charset=UTF-8\n"
11
+ "Content-Transfer-Encoding: 8bit\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+
14
+ msgid "Comparison Slider"
15
+ msgstr "Vergleichs-Slider"
16
+
17
+ msgid "Before Image"
18
+ msgstr "Vorher-Bild"
19
+
20
+ msgid "After Image"
21
+ msgstr "Nachher-Bild"
22
+
23
+ msgid "Before Label"
24
+ msgstr "Vorher-Beschriftung"
25
+
26
+ msgid "After Label"
27
+ msgstr "Nachher-Beschriftung"
28
+
29
+ msgid "Initial Position"
30
+ msgstr "Anfangsposition"
31
+
32
+ msgid "Initial slider position (0-100)"
33
+ msgstr "Anfangsposition des Sliders (0-100)"
34
+
35
+ msgid "Show Labels"
36
+ msgstr "Beschriftungen anzeigen"
37
+
38
+ msgid "Slider Orientation"
39
+ msgstr "Slider-Ausrichtung"
40
+
41
+ msgid "Horizontal"
42
+ msgstr "Horizontal"
43
+
44
+ msgid "Vertical"
45
+ msgstr "Vertikal"
46
+
47
+ msgid "Select before and after images in the sidebar"
48
+ msgstr "Wähle Vorher- und Nachher-Bilder in der Seitenleiste aus"
49
+
50
+ msgid "Handle Type"
51
+ msgstr "Griff-Typ"
52
+
53
+ msgid "Icon"
54
+ msgstr "Icon"
55
+
56
+ msgid "Text"
57
+ msgstr "Text"
58
+
59
+ msgid "Handle Text"
60
+ msgstr "Griff-Text"
61
+
62
+ msgid "Text displayed on the slider handle (e.g. DRAG)"
63
+ msgstr "Text auf dem Slider-Griff (z.B. ZIEHEN)"
@@ -0,0 +1,63 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: volto-comparison-slider\n"
4
+ "Report-Msgid-Bugs-To: \n"
5
+ "POT-Creation-Date: \n"
6
+ "PO-Revision-Date: \n"
7
+ "Last-Translator: \n"
8
+ "Language: en\n"
9
+ "Language-Team: English\n"
10
+ "Content-Type: text/plain; charset=UTF-8\n"
11
+ "Content-Transfer-Encoding: 8bit\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+
14
+ msgid "Comparison Slider"
15
+ msgstr ""
16
+
17
+ msgid "Before Image"
18
+ msgstr ""
19
+
20
+ msgid "After Image"
21
+ msgstr ""
22
+
23
+ msgid "Before Label"
24
+ msgstr ""
25
+
26
+ msgid "After Label"
27
+ msgstr ""
28
+
29
+ msgid "Initial Position"
30
+ msgstr ""
31
+
32
+ msgid "Initial slider position (0-100)"
33
+ msgstr ""
34
+
35
+ msgid "Show Labels"
36
+ msgstr ""
37
+
38
+ msgid "Slider Orientation"
39
+ msgstr ""
40
+
41
+ msgid "Horizontal"
42
+ msgstr ""
43
+
44
+ msgid "Vertical"
45
+ msgstr ""
46
+
47
+ msgid "Select before and after images in the sidebar"
48
+ msgstr ""
49
+
50
+ msgid "Handle Type"
51
+ msgstr ""
52
+
53
+ msgid "Icon"
54
+ msgstr ""
55
+
56
+ msgid "Text"
57
+ msgstr ""
58
+
59
+ msgid "Handle Text"
60
+ msgstr ""
61
+
62
+ msgid "Text displayed on the slider handle (e.g. DRAG)"
63
+ msgstr ""
@@ -0,0 +1,19 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: Plone\n"
4
+ "Report-Msgid-Bugs-To: \n"
5
+ "POT-Creation-Date: 2023-05-09 11:45-0400\n"
6
+ "PO-Revision-Date: 2023-05-10 11:34-0400\n"
7
+ "Last-Translator: Leonardo J. Caballero G. <leonardocaballero@gmail.com>\n"
8
+ "Language: es\n"
9
+ "Language-Team: ES <LL@li.org>\n"
10
+ "Content-Type: text/plain; charset=utf-8\n"
11
+ "Content-Transfer-Encoding: 8bit\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "Preferred-Encodings: utf-8\n"
14
+ "MIME-Version: 1.0\n"
15
+ "Language-Code: es\n"
16
+ "Language-Name: Español\n"
17
+ "Domain: volto\n"
18
+ "X-Is-Fallback-For: es-ar es-bo es-cl es-co es-cr es-do es-ec es-es es-sv es-gt es-hn es-mx es-ni es-pa es-py es-pe es-pr es-us es-uy es-ve\n"
19
+ "X-Generator: Poedit 2.2.1\n"
@@ -0,0 +1,17 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: Plone\n"
4
+ "Report-Msgid-Bugs-To: \n"
5
+ "POT-Creation-Date: 2023-04-06T16:01:32.969Z\n"
6
+ "PO-Revision-Date: \n"
7
+ "Last-Translator: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
8
+ "Language: \n"
9
+ "Language-Team: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
10
+ "Content-Type: text/plain; charset=utf-8\n"
11
+ "Content-Transfer-Encoding: 8bit\n"
12
+ "Plural-Forms: nplurals=1; plural=0;\n"
13
+ "MIME-Version: 1.0\n"
14
+ "Language-Code: pt_BR\n"
15
+ "Language-Name: Português do Brasil\n"
16
+ "Preferred-Encodings: utf-8\n"
17
+ "Domain: volto\n"
@@ -0,0 +1,14 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: Plone\n"
4
+ "POT-Creation-Date: 2024-03-22T12:43:34.158Z\n"
5
+ "Last-Translator: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
6
+ "Language-Team: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
7
+ "Content-Type: text/plain; charset=utf-8\n"
8
+ "Content-Transfer-Encoding: 8bit\n"
9
+ "Plural-Forms: nplurals=1; plural=0;\n"
10
+ "MIME-Version: 1.0\n"
11
+ "Language-Code: en\n"
12
+ "Language-Name: English\n"
13
+ "Preferred-Encodings: utf-8\n"
14
+ "Domain: volto\n"
package/news/.gitkeep ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@interaktivgmbh/volto-comparison-slider",
3
+ "version": "1.0.0",
4
+ "description": "A new add-on for Volto",
5
+ "main": "src/index.js",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "volto-addon",
9
+ "volto",
10
+ "plone",
11
+ "react"
12
+ ],
13
+ "author": "Interaktiv GmbH",
14
+ "homepage": "https://github.com/interaktivgmbh/volto-comparison-slider#readme",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git@github.com:interaktivgmbh/volto-comparison-slider"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "i18n": "rm -rf build/messages && NODE_ENV=production i18n --addon",
24
+ "dry-release": "release-it --dry-run",
25
+ "release": "release-it",
26
+ "release-major-alpha": "release-it major --preRelease=alpha",
27
+ "release-alpha": "release-it --preRelease=alpha"
28
+ },
29
+ "addons": [],
30
+ "theme": "",
31
+ "dependencies": {},
32
+ "peerDependencies": {
33
+ "react": "^18.2.0",
34
+ "react-dom": "^18.2.0",
35
+ "react-intl": "^6.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@plone/volto": "workspace:*",
39
+ "react-intl": "^6.0.0",
40
+ "@plone/registry": "workspace:*",
41
+ "@plone/scripts": "workspace:*",
42
+ "@plone/types": "workspace:*",
43
+ "release-it": "^19.0.5"
44
+ }
45
+ }
File without changes
@@ -0,0 +1,182 @@
1
+ import React, { useState, useRef, useCallback, useEffect } from 'react';
2
+ import { flattenToAppURL } from '@plone/volto/helpers';
3
+ import Image from '@plone/volto/components/theme/Image/Image';
4
+
5
+ const getImageUrl = (image) =>
6
+ image?.[0] ? flattenToAppURL(`${image[0]['@id']}/@@images/image`) : null;
7
+
8
+ const ComparisonSliderBody = ({ data, isEditMode = false }) => {
9
+ const {
10
+ beforeImage,
11
+ afterImage,
12
+ beforeLabel = 'Before',
13
+ afterLabel = 'After',
14
+ initialPosition = 50,
15
+ showLabels = true,
16
+ orientation = 'horizontal',
17
+ handleType = 'icon',
18
+ handleText = 'DRAG',
19
+ } = data;
20
+
21
+ const isHorizontal = orientation === 'horizontal';
22
+ const [sliderPosition, setSliderPosition] = useState(initialPosition);
23
+ const [isDragging, setIsDragging] = useState(false);
24
+ const containerRef = useRef(null);
25
+
26
+ const beforeUrl = getImageUrl(beforeImage);
27
+ const afterUrl = getImageUrl(afterImage);
28
+
29
+ const updatePosition = useCallback(
30
+ (clientX, clientY) => {
31
+ if (!containerRef.current) return;
32
+ const rect = containerRef.current.getBoundingClientRect();
33
+ const position = isHorizontal
34
+ ? ((clientX - rect.left) / rect.width) * 100
35
+ : ((clientY - rect.top) / rect.height) * 100;
36
+ setSliderPosition(Math.max(0, Math.min(100, position)));
37
+ },
38
+ [isHorizontal],
39
+ );
40
+
41
+ const handleStart = useCallback(
42
+ (e) => {
43
+ if (isEditMode) return;
44
+ e.preventDefault();
45
+ setIsDragging(true);
46
+ const point = e.touches?.[0] || e;
47
+ updatePosition(point.clientX, point.clientY);
48
+ },
49
+ [isEditMode, updatePosition],
50
+ );
51
+
52
+ const handleKeyDown = useCallback(
53
+ (e) => {
54
+ if (isEditMode) return;
55
+ const step = e.shiftKey ? 10 : 1;
56
+ const decreaseKeys = isHorizontal ? ['ArrowLeft'] : ['ArrowUp'];
57
+ const increaseKeys = isHorizontal ? ['ArrowRight'] : ['ArrowDown'];
58
+
59
+ if (decreaseKeys.includes(e.key)) {
60
+ e.preventDefault();
61
+ setSliderPosition((prev) => Math.max(0, prev - step));
62
+ } else if (increaseKeys.includes(e.key)) {
63
+ e.preventDefault();
64
+ setSliderPosition((prev) => Math.min(100, prev + step));
65
+ } else if (e.key === 'Home') {
66
+ e.preventDefault();
67
+ setSliderPosition(0);
68
+ } else if (e.key === 'End') {
69
+ e.preventDefault();
70
+ setSliderPosition(100);
71
+ }
72
+ },
73
+ [isEditMode, isHorizontal],
74
+ );
75
+
76
+ useEffect(() => {
77
+ if (!isDragging) return;
78
+
79
+ const handleMove = (e) => {
80
+ const point = e.touches?.[0] || e;
81
+ updatePosition(point.clientX, point.clientY);
82
+ };
83
+ const handleEnd = () => setIsDragging(false);
84
+
85
+ document.addEventListener('mousemove', handleMove);
86
+ document.addEventListener('mouseup', handleEnd);
87
+ document.addEventListener('touchmove', handleMove);
88
+ document.addEventListener('touchend', handleEnd);
89
+
90
+ return () => {
91
+ document.removeEventListener('mousemove', handleMove);
92
+ document.removeEventListener('mouseup', handleEnd);
93
+ document.removeEventListener('touchmove', handleMove);
94
+ document.removeEventListener('touchend', handleEnd);
95
+ };
96
+ }, [isDragging, updatePosition]);
97
+
98
+ useEffect(() => {
99
+ setSliderPosition(initialPosition);
100
+ }, [initialPosition]);
101
+
102
+ if (!beforeUrl || !afterUrl) {
103
+ return null;
104
+ }
105
+
106
+ const clipPath = isHorizontal
107
+ ? `inset(0 ${100 - sliderPosition}% 0 0)`
108
+ : `inset(0 0 ${100 - sliderPosition}% 0)`;
109
+
110
+ const sliderStyle = isHorizontal
111
+ ? { left: `${sliderPosition}%` }
112
+ : { top: `${sliderPosition}%` };
113
+
114
+ return (
115
+ <div
116
+ ref={containerRef}
117
+ className={`comparison-slider comparison-slider--${orientation} ${isDragging ? 'is-dragging' : ''}`}
118
+ role="slider"
119
+ tabIndex={isEditMode ? -1 : 0}
120
+ aria-valuenow={Math.round(sliderPosition)}
121
+ aria-valuemin={0}
122
+ aria-valuemax={100}
123
+ aria-label={`Image comparison slider: ${beforeLabel} vs ${afterLabel}`}
124
+ onMouseDown={handleStart}
125
+ onTouchStart={handleStart}
126
+ onKeyDown={handleKeyDown}
127
+ >
128
+ <div className="comparison-slider__image-container">
129
+ <Image
130
+ src={afterUrl}
131
+ alt={afterLabel}
132
+ className="comparison-slider__image comparison-slider__image--after"
133
+ draggable={false}
134
+ />
135
+ <Image
136
+ src={beforeUrl}
137
+ alt={beforeLabel}
138
+ className="comparison-slider__image comparison-slider__image--before"
139
+ style={{ clipPath }}
140
+ draggable={false}
141
+ />
142
+ </div>
143
+
144
+ <div
145
+ className={`comparison-slider__handle ${isHorizontal ? 'comparison-slider__handle--horizontal' : 'comparison-slider__handle--vertical'}`}
146
+ style={sliderStyle}
147
+ >
148
+ <div className="comparison-slider__handle-line" />
149
+ <div
150
+ className={`comparison-slider__handle-button ${handleType === 'text' ? 'comparison-slider__handle-button--text' : ''}`}
151
+ >
152
+ {handleType === 'text' ? (
153
+ <span className="comparison-slider__handle-text">{handleText}</span>
154
+ ) : (
155
+ <svg viewBox="0 0 24 24" fill="currentColor">
156
+ <path
157
+ d={
158
+ isHorizontal
159
+ ? 'M8 5v14l-7-7 7-7zm8 0v14l7-7-7-7z'
160
+ : 'M5 8h14l-7-7-7 7zm0 8h14l-7 7-7-7z'
161
+ }
162
+ />
163
+ </svg>
164
+ )}
165
+ </div>
166
+ </div>
167
+
168
+ {showLabels && (
169
+ <>
170
+ <span className="comparison-slider__label comparison-slider__label--before">
171
+ {beforeLabel}
172
+ </span>
173
+ <span className="comparison-slider__label comparison-slider__label--after">
174
+ {afterLabel}
175
+ </span>
176
+ </>
177
+ )}
178
+ </div>
179
+ );
180
+ };
181
+
182
+ export default ComparisonSliderBody;
@@ -0,0 +1,53 @@
1
+ import { defineMessages } from 'react-intl';
2
+ import { SidebarPortal, BlockDataForm } from '@plone/volto/components';
3
+ import ComparisonSliderBody from '@interaktivgmbh/volto-comparison-slider/components/Blocks/ComparisonSlider/ComparisonSliderBody';
4
+ import { ComparisonSliderSchema } from '@interaktivgmbh/volto-comparison-slider/components/Blocks/ComparisonSlider/schema';
5
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
6
+ import comparisonSliderSVG from '@interaktivgmbh/volto-comparison-slider/icons/comparison-slider.svg';
7
+
8
+ const messages = defineMessages({
9
+ selectImages: {
10
+ id: 'Select before and after images in the sidebar',
11
+ defaultMessage: 'Select before and after images in the sidebar',
12
+ },
13
+ });
14
+
15
+ function Edit({ data, block, onChangeBlock, selected, intl }) {
16
+ const schema = ComparisonSliderSchema(intl);
17
+
18
+ const hasImages = data.beforeImage?.[0] && data.afterImage?.[0];
19
+
20
+ return (
21
+ <div className="block comparison-slider-block">
22
+ {hasImages ? (
23
+ <ComparisonSliderBody data={data} isEditMode={true} />
24
+ ) : (
25
+ <div className="comparison-slider-placeholder">
26
+ <Icon
27
+ name={comparisonSliderSVG}
28
+ size="64px"
29
+ className="placeholder-icon"
30
+ />
31
+ <p>{intl.formatMessage(messages.selectImages)}</p>
32
+ </div>
33
+ )}
34
+
35
+ <SidebarPortal selected={selected}>
36
+ <BlockDataForm
37
+ schema={schema}
38
+ title={schema.title}
39
+ onChangeField={(id, value) => {
40
+ onChangeBlock(block, {
41
+ ...data,
42
+ [id]: value,
43
+ });
44
+ }}
45
+ formData={data}
46
+ block={block}
47
+ />
48
+ </SidebarPortal>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ export default Edit;
@@ -0,0 +1,19 @@
1
+ import ComparisonSliderBody from '@interaktivgmbh/volto-comparison-slider/components/Blocks/ComparisonSlider/ComparisonSliderBody';
2
+
3
+ function View(props) {
4
+ const { data, className } = props;
5
+ const beforeImage = data.beforeImage;
6
+ const afterImage = data.afterImage;
7
+
8
+ if (!beforeImage?.[0] || !afterImage?.[0]) {
9
+ return null;
10
+ }
11
+
12
+ return (
13
+ <div className={`block comparison-slider-block ${className || ''}`}>
14
+ <ComparisonSliderBody data={data} isEditMode={false} />
15
+ </div>
16
+ );
17
+ }
18
+
19
+ export default View;
@@ -0,0 +1,148 @@
1
+ import { defineMessages } from 'react-intl';
2
+
3
+ const messages = defineMessages({
4
+ comparisonSlider: {
5
+ id: 'Comparison Slider',
6
+ defaultMessage: 'Comparison Slider',
7
+ },
8
+ beforeImage: {
9
+ id: 'Before Image',
10
+ defaultMessage: 'Before Image',
11
+ },
12
+ afterImage: {
13
+ id: 'After Image',
14
+ defaultMessage: 'After Image',
15
+ },
16
+ beforeLabel: {
17
+ id: 'Before Label',
18
+ defaultMessage: 'Before Label',
19
+ },
20
+ afterLabel: {
21
+ id: 'After Label',
22
+ defaultMessage: 'After Label',
23
+ },
24
+ initialPosition: {
25
+ id: 'Initial Position',
26
+ defaultMessage: 'Initial Position',
27
+ },
28
+ initialPositionDescription: {
29
+ id: 'Initial slider position (0-100)',
30
+ defaultMessage: 'Initial slider position (0-100)',
31
+ },
32
+ showLabels: {
33
+ id: 'Show Labels',
34
+ defaultMessage: 'Show Labels',
35
+ },
36
+ sliderOrientation: {
37
+ id: 'Slider Orientation',
38
+ defaultMessage: 'Slider Orientation',
39
+ },
40
+ horizontal: {
41
+ id: 'Horizontal',
42
+ defaultMessage: 'Horizontal',
43
+ },
44
+ vertical: {
45
+ id: 'Vertical',
46
+ defaultMessage: 'Vertical',
47
+ },
48
+ handleType: {
49
+ id: 'Handle Type',
50
+ defaultMessage: 'Handle Type',
51
+ },
52
+ handleIcon: {
53
+ id: 'Icon',
54
+ defaultMessage: 'Icon',
55
+ },
56
+ handleText: {
57
+ id: 'Text',
58
+ defaultMessage: 'Text',
59
+ },
60
+ handleTextValue: {
61
+ id: 'Handle Text',
62
+ defaultMessage: 'Handle Text',
63
+ },
64
+ handleTextDescription: {
65
+ id: 'Text displayed on the slider handle (e.g. DRAG)',
66
+ defaultMessage: 'Text displayed on the slider handle (e.g. DRAG)',
67
+ },
68
+ });
69
+
70
+ export const ComparisonSliderSchema = (intl) => ({
71
+ title: intl.formatMessage(messages.comparisonSlider),
72
+ fieldsets: [
73
+ {
74
+ id: 'default',
75
+ title: 'Default',
76
+ fields: [
77
+ 'beforeImage',
78
+ 'afterImage',
79
+ 'beforeLabel',
80
+ 'afterLabel',
81
+ 'initialPosition',
82
+ 'showLabels',
83
+ 'orientation',
84
+ 'handleType',
85
+ 'handleText',
86
+ ],
87
+ },
88
+ ],
89
+ properties: {
90
+ beforeImage: {
91
+ title: intl.formatMessage(messages.beforeImage),
92
+ widget: 'object_browser',
93
+ mode: 'image',
94
+ allowExternals: true,
95
+ },
96
+ afterImage: {
97
+ title: intl.formatMessage(messages.afterImage),
98
+ widget: 'object_browser',
99
+ mode: 'image',
100
+ allowExternals: true,
101
+ },
102
+ beforeLabel: {
103
+ title: intl.formatMessage(messages.beforeLabel),
104
+ default: 'Before',
105
+ },
106
+ afterLabel: {
107
+ title: intl.formatMessage(messages.afterLabel),
108
+ default: 'After',
109
+ },
110
+ initialPosition: {
111
+ title: intl.formatMessage(messages.initialPosition),
112
+ description: intl.formatMessage(messages.initialPositionDescription),
113
+ type: 'integer',
114
+ minimum: 0,
115
+ maximum: 100,
116
+ default: 50,
117
+ },
118
+ showLabels: {
119
+ title: intl.formatMessage(messages.showLabels),
120
+ type: 'boolean',
121
+ default: true,
122
+ },
123
+ orientation: {
124
+ title: intl.formatMessage(messages.sliderOrientation),
125
+ choices: [
126
+ ['horizontal', intl.formatMessage(messages.horizontal)],
127
+ ['vertical', intl.formatMessage(messages.vertical)],
128
+ ],
129
+ default: 'horizontal',
130
+ },
131
+ handleType: {
132
+ title: intl.formatMessage(messages.handleType),
133
+ choices: [
134
+ ['icon', intl.formatMessage(messages.handleIcon)],
135
+ ['text', intl.formatMessage(messages.handleText)],
136
+ ],
137
+ default: 'icon',
138
+ },
139
+ handleText: {
140
+ title: intl.formatMessage(messages.handleTextValue),
141
+ description: intl.formatMessage(messages.handleTextDescription),
142
+ default: 'DRAG',
143
+ },
144
+ },
145
+ required: ['beforeImage', 'afterImage'],
146
+ });
147
+
148
+ export default ComparisonSliderSchema;
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36">
2
+ <rect x="2" y="6" width="32" height="24" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2"/>
3
+ <line x1="18" y1="6" x2="18" y2="30" stroke="currentColor" stroke-width="2"/>
4
+ <circle cx="18" cy="18" r="4" fill="currentColor"/>
5
+ <polygon points="14,18 10,14 10,22" fill="currentColor"/>
6
+ <polygon points="22,18 26,14 26,22" fill="currentColor"/>
7
+ <rect x="4" y="8" width="12" height="4" fill="currentColor" opacity="0.3"/>
8
+ <rect x="20" y="24" width="12" height="4" fill="currentColor" opacity="0.3"/>
9
+ </svg>
package/src/index.js ADDED
@@ -0,0 +1,24 @@
1
+ import View from '@interaktivgmbh/volto-comparison-slider/components/Blocks/ComparisonSlider/View';
2
+ import Edit from '@interaktivgmbh/volto-comparison-slider/components/Blocks/ComparisonSlider/Edit';
3
+ import { ComparisonSliderSchema } from '@interaktivgmbh/volto-comparison-slider/components/Blocks/ComparisonSlider/schema';
4
+ import comparisonSliderSVG from '@interaktivgmbh/volto-comparison-slider/icons/comparison-slider.svg';
5
+ import '@interaktivgmbh/volto-comparison-slider/theme/_main.css';
6
+
7
+ const applyConfig = (config) => {
8
+ config.blocks.blocksConfig.comparisonSlider = {
9
+ id: 'comparisonSlider',
10
+ title: 'Comparison Slider',
11
+ icon: comparisonSliderSVG,
12
+ group: 'media',
13
+ view: View,
14
+ edit: Edit,
15
+ blockSchema: ({ intl }) => ComparisonSliderSchema(intl),
16
+ restricted: false,
17
+ mostUsed: false,
18
+ sidebarTab: 1,
19
+ };
20
+
21
+ return config;
22
+ };
23
+
24
+ export default applyConfig;
@@ -0,0 +1,199 @@
1
+ .comparison-slider-block {
2
+ width: 100%;
3
+ }
4
+
5
+ .comparison-slider {
6
+ position: relative;
7
+ overflow: hidden;
8
+ width: 100%;
9
+ cursor: ew-resize;
10
+ touch-action: none;
11
+ -webkit-user-select: none;
12
+ user-select: none;
13
+ }
14
+
15
+ .comparison-slider--vertical {
16
+ cursor: ns-resize;
17
+ }
18
+
19
+ .comparison-slider.is-dragging {
20
+ cursor: grabbing;
21
+ }
22
+
23
+ .comparison-slider__image-container {
24
+ position: relative;
25
+ width: 100%;
26
+ }
27
+
28
+ .comparison-slider__image {
29
+ display: block;
30
+ width: 100%;
31
+ height: auto;
32
+ pointer-events: none;
33
+ }
34
+
35
+ .comparison-slider__image--before {
36
+ position: absolute;
37
+ top: 0;
38
+ left: 0;
39
+ width: 100%;
40
+ height: 100%;
41
+ object-fit: cover;
42
+ }
43
+
44
+ .comparison-slider__handle {
45
+ position: absolute;
46
+ z-index: 10;
47
+ }
48
+
49
+ .comparison-slider__handle--horizontal {
50
+ top: 0;
51
+ bottom: 0;
52
+ width: 4px;
53
+ transform: translateX(-50%);
54
+ }
55
+
56
+ .comparison-slider__handle--vertical {
57
+ top: 0;
58
+ right: 0;
59
+ left: 0;
60
+ height: 4px;
61
+ transform: translateY(-50%);
62
+ }
63
+
64
+ .comparison-slider__handle-line {
65
+ position: absolute;
66
+ background-color: #fff;
67
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
68
+ }
69
+
70
+ .comparison-slider__handle--horizontal .comparison-slider__handle-line {
71
+ top: 0;
72
+ bottom: 0;
73
+ left: 50%;
74
+ width: 4px;
75
+ transform: translateX(-50%);
76
+ }
77
+
78
+ .comparison-slider__handle--vertical .comparison-slider__handle-line {
79
+ top: 50%;
80
+ right: 0;
81
+ left: 0;
82
+ height: 4px;
83
+ transform: translateY(-50%);
84
+ }
85
+
86
+ .comparison-slider__handle-button {
87
+ position: absolute;
88
+ display: flex;
89
+ width: 44px;
90
+ height: 44px;
91
+ align-items: center;
92
+ justify-content: center;
93
+ border-radius: 50%;
94
+ background-color: #fff;
95
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
96
+ transition: transform 0.15s ease;
97
+ }
98
+
99
+ .comparison-slider__handle--horizontal .comparison-slider__handle-button {
100
+ top: 50%;
101
+ left: 50%;
102
+ transform: translate(-50%, -50%);
103
+ }
104
+
105
+ .comparison-slider__handle--vertical .comparison-slider__handle-button {
106
+ top: 50%;
107
+ left: 50%;
108
+ transform: translate(-50%, -50%);
109
+ }
110
+
111
+ .comparison-slider:hover .comparison-slider__handle-button,
112
+ .comparison-slider.is-dragging .comparison-slider__handle-button {
113
+ transform: translate(-50%, -50%) scale(1.1);
114
+ }
115
+
116
+ .comparison-slider__handle-button svg {
117
+ width: 24px;
118
+ height: 24px;
119
+ color: #333;
120
+ }
121
+
122
+ /* Text handle variant */
123
+ .comparison-slider__handle-button--text {
124
+ width: auto;
125
+ min-width: 44px;
126
+ height: auto;
127
+ min-height: 44px;
128
+ padding: 8px 16px;
129
+ border-radius: 22px;
130
+ }
131
+
132
+ .comparison-slider__handle-text {
133
+ color: #333;
134
+ font-size: 12px;
135
+ font-weight: 700;
136
+ letter-spacing: 1px;
137
+ text-transform: uppercase;
138
+ white-space: nowrap;
139
+ }
140
+
141
+ .comparison-slider__label {
142
+ position: absolute;
143
+ z-index: 5;
144
+ padding: 8px 16px;
145
+ border-radius: 4px;
146
+ background-color: rgba(0, 0, 0, 0.7);
147
+ color: #fff;
148
+ font-size: 14px;
149
+ font-weight: 600;
150
+ pointer-events: none;
151
+ }
152
+
153
+ .comparison-slider--horizontal .comparison-slider__label--before {
154
+ top: 16px;
155
+ left: 16px;
156
+ }
157
+
158
+ .comparison-slider--horizontal .comparison-slider__label--after {
159
+ top: 16px;
160
+ right: 16px;
161
+ }
162
+
163
+ .comparison-slider--vertical .comparison-slider__label--before {
164
+ top: 16px;
165
+ left: 50%;
166
+ transform: translateX(-50%);
167
+ }
168
+
169
+ .comparison-slider--vertical .comparison-slider__label--after {
170
+ bottom: 16px;
171
+ left: 50%;
172
+ transform: translateX(-50%);
173
+ }
174
+
175
+ .comparison-slider-placeholder {
176
+ display: flex;
177
+ min-height: 300px;
178
+ flex-direction: column;
179
+ align-items: center;
180
+ justify-content: center;
181
+ padding: 40px;
182
+ border: 2px dashed #ccc;
183
+ border-radius: 8px;
184
+ background-color: #f5f5f5;
185
+ text-align: center;
186
+ }
187
+
188
+ .comparison-slider-placeholder .placeholder-icon {
189
+ width: 64px;
190
+ height: 64px;
191
+ margin-bottom: 16px;
192
+ opacity: 0.5;
193
+ }
194
+
195
+ .comparison-slider-placeholder p {
196
+ margin: 0;
197
+ color: #666;
198
+ font-size: 16px;
199
+ }
package/towncrier.toml ADDED
@@ -0,0 +1,33 @@
1
+ [tool.towncrier]
2
+ filename = "CHANGELOG.md"
3
+ directory = "news/"
4
+ title_format = "## {version} ({project_date})"
5
+ underlines = ["", "", ""]
6
+ template = "./node_modules/@plone/scripts/templates/towncrier_template.jinja"
7
+ start_string = "<!-- towncrier release notes start -->\n"
8
+ issue_format = "[#{issue}](https://github.com/interaktivgmbh/volto-comparison-slider/issue/{issue})"
9
+
10
+ [[tool.towncrier.type]]
11
+ directory = "breaking"
12
+ name = "Breaking"
13
+ showcontent = true
14
+
15
+ [[tool.towncrier.type]]
16
+ directory = "feature"
17
+ name = "Feature"
18
+ showcontent = true
19
+
20
+ [[tool.towncrier.type]]
21
+ directory = "bugfix"
22
+ name = "Bugfix"
23
+ showcontent = true
24
+
25
+ [[tool.towncrier.type]]
26
+ directory = "internal"
27
+ name = "Internal"
28
+ showcontent = true
29
+
30
+ [[tool.towncrier.type]]
31
+ directory = "documentation"
32
+ name = "Documentation"
33
+ showcontent = true
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ "esModuleInterop": true,
4
+ "skipLibCheck": true,
5
+ "target": "es2022",
6
+ "allowJs": true,
7
+ "resolveJsonModule": true,
8
+ "moduleDetection": "force",
9
+ "isolatedModules": true,
10
+ "verbatimModuleSyntax": true,
11
+ "module": "preserve",
12
+ "noEmit": true,
13
+ "lib": ["es2022", "dom", "dom.iterable"],
14
+ "jsx": "react-jsx",
15
+ "paths": {
16
+ "@plone/volto/*": ["../../core/packages/volto/src/*"],
17
+ "@interaktivgmbh/volto-comparison-slider/*": ["./src/*"]
18
+ }
19
+ },
20
+ "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
21
+ "exclude": [
22
+ "node_modules",
23
+ "build",
24
+ "public",
25
+ "coverage",
26
+ "**/*.test.{js,jsx,ts,tsx}",
27
+ "**/*.spec.{js,jsx,ts,tsx}",
28
+ "**/*.stories.{js,jsx,ts,tsx}"
29
+ ]
30
+ }