Skip to content

Commit 539a66c

Browse files
committed
FOR-2074: Implemented Tagpad component
1 parent 4bfb495 commit 539a66c

File tree

4 files changed

+307
-1
lines changed

4 files changed

+307
-1
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"string-hash": "^1.1.3",
8383
"text-mask-addons": "^3.8.0",
8484
"tooltip.js": "^1.3.1",
85+
"two.js": "^0.7.0-beta.3",
8586
"vanilla-text-mask": "^5.1.1"
8687
},
8788
"devDependencies": {

src/contrib/index.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import StripeComponent from './stripe/stripe/Stripe';
22
import StripeCheckoutComponent from './stripe/checkout/StripeCheckout';
3+
import Tagpad from './tagpad/tagpad';
34
const Contrib = {
45
stripe: {
56
stripe: StripeComponent,
67
checkout: StripeCheckoutComponent
7-
}
8+
},
9+
tagpad: Tagpad
810
};
911

1012
export default Contrib;

src/contrib/tagpad/tagpad.js

+265
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import Two from 'two.js';
2+
import NestedComponent from '../../components/nested/NestedComponent';
3+
import _ from 'lodash';
4+
import BaseComponent from '../../components/base/Base';
5+
import { Components } from '../../formio.form';
6+
7+
export default class Tagpad extends NestedComponent {
8+
static schema(...extend) {
9+
return NestedComponent.schema({
10+
type: 'tagpad',
11+
label: 'Tagpad',
12+
key: 'tagpad',
13+
canvasWidth: 640,
14+
canvasHeight: 480,
15+
dotSize: 10,
16+
dotStrokeSize: 2,
17+
dotStrokeColor: '#333',
18+
dotFillColor: '#ccc'
19+
}, ...extend);
20+
}
21+
22+
constructor(...args) {
23+
super(...args);
24+
this.type = 'tagpad';
25+
this.dots = [];
26+
_.defaults(this.component, {
27+
canvasWidth: 640,
28+
canvasHeight: 480,
29+
dotSize: 10,
30+
dotStrokeSize: 2,
31+
dotStrokeColor: '#333',
32+
dotFillColor: '#ccc'
33+
});
34+
}
35+
36+
build(state) {
37+
if (this.options.builder) {
38+
return super.build(state, true);
39+
}
40+
this.createElement();
41+
this.createLabel(this.element);
42+
this.renderTagpad();
43+
this.createDescription(this.element);
44+
this.element.appendChild(this.errorContainer = this.ce('div', { class: 'has-error' }));
45+
this.attachLogic();
46+
}
47+
48+
renderTagpad() {
49+
this.tagpadContainer = this.ce('div', {
50+
class: 'formio-tagpad-container clearfix'
51+
});
52+
this.canvas = this.ce('div', {
53+
class: 'formio-tagpad-canvas'
54+
});
55+
this.canvasContainer = this.ce('div', {
56+
class: 'formio-tagpad-canvas-container',
57+
style: `width: ${this.component.canvasWidth}px;`
58+
}, [this.canvas]);
59+
this.formContainer = this.ce('div', {
60+
class: 'formio-tagpad-form-container',
61+
style: `margin-left: -${this.component.canvasWidth}px; padding-left: ${this.component.canvasWidth}px;`
62+
},
63+
this.form = this.ce('div', {
64+
class: 'formio-tagpad-form'
65+
}));
66+
this.renderForm();
67+
this.tagpadContainer.appendChild(this.canvasContainer);
68+
this.tagpadContainer.appendChild(this.formContainer);
69+
this.element.appendChild(this.tagpadContainer);
70+
this.two = new Two({
71+
type: Two.Types.svg,
72+
width: this.component.canvasWidth,
73+
height: this.component.canvasHeight
74+
}).appendTo(this.canvas);
75+
this.canvasSvg = this.two.renderer.domElement;
76+
this.addBackground();
77+
this.attachDrawEvents();
78+
}
79+
80+
renderForm() {
81+
this.form.appendChild(this.ce('p', {
82+
class: 'formio-tagpad-form-title'
83+
},
84+
[
85+
this.t('Dot: '),
86+
this.selectedDotIndexElement = this.ce('span', {}, 'No dot selected')
87+
]
88+
)
89+
);
90+
this.component.components.forEach((component) => {
91+
//have to avoid using createComponent method as Components there will be empty
92+
const componentInstance = Components.create(component, this.options, this.data);
93+
componentInstance.parent = this;
94+
componentInstance.root = this.root || this;
95+
const oldOnChange = componentInstance.onChange;
96+
componentInstance.onChange = (flags, fromRoot) => {
97+
oldOnChange.call(componentInstance, flags, fromRoot);
98+
this.saveSelectedDot();
99+
};
100+
this.components.push(componentInstance);
101+
this.form.appendChild(componentInstance.getElement());
102+
});
103+
this.form.appendChild(this.ce(
104+
'button',
105+
{
106+
class: 'btn btn-sm btn-danger formio-tagpad-remove-button',
107+
onClick: this.removeSelectedDot.bind(this),
108+
title: 'Remove Dot'
109+
},
110+
[
111+
this.ce('i', {
112+
class: this.iconClass('trash')
113+
})
114+
]
115+
));
116+
}
117+
118+
attachDrawEvents() {
119+
// Set up mouse event.
120+
const mouseEnd = (e) => {
121+
e.preventDefault();
122+
const offset = this.canvasSvg.getBoundingClientRect();
123+
this.addDot({
124+
x: e.clientX - offset.left,
125+
y: e.clientY - offset.top
126+
});
127+
};
128+
this.canvasSvg.addEventListener('mouseup', mouseEnd);
129+
130+
// Set up touch event.
131+
const touchEnd = (e) => {
132+
e.preventDefault();
133+
const offset = this.canvasSvg.getBoundingClientRect();
134+
const touch = e.originalEvent.changedTouches[0];
135+
this.addDot({
136+
x: touch.pageX - offset.left,
137+
y: touch.pageY - offset.top
138+
});
139+
};
140+
this.canvasSvg.addEventListener('touchend', touchEnd);
141+
142+
this.two.update();
143+
}
144+
145+
addBackground() {
146+
if (this.component.image) {
147+
let svg = this.ce('svg');
148+
svg.innerHTML = this.component.image;
149+
svg = this.two.interpret(svg);
150+
svg.center();
151+
svg.translation.set(this.component.canvasWidth / 2, this.component.canvasHeight / 2);
152+
}
153+
}
154+
155+
addDot(coordinate) {
156+
const dot = {
157+
coordinate,
158+
data: {}
159+
};
160+
const newDotIndex = this.dataValue.length;
161+
const shape = this.drawDot(dot, newDotIndex);
162+
this.dots.push({
163+
index: newDotIndex,
164+
dot,
165+
shape
166+
});
167+
this.dataValue.push(dot);
168+
this.selectDot(newDotIndex);
169+
this.triggerChange();
170+
}
171+
172+
dotClicked(e, dot, index) {
173+
//prevent drawing another dot near clicked dot
174+
e.stopPropagation();
175+
this.selectDot(index);
176+
}
177+
178+
selectDot(index) {
179+
const dot = this.dots[index];
180+
if (!dot) {
181+
return;
182+
}
183+
//remove dashes for previous selected dot
184+
if (this.dots[this.selectedDotIndex]) {
185+
this.dots[this.selectedDotIndex].shape.circle.dashes = [0];
186+
}
187+
//add dashes to new selected dot
188+
dot.shape.circle.dashes = [1];
189+
this.two.update();
190+
this.selectedDotIndex = index;
191+
this.setFormValue(dot.dot.data);
192+
}
193+
194+
setFormValue(value) {
195+
this.selectedDotIndexElement.innerHTML = this.selectedDotIndex + 1;
196+
this.components.forEach(component => {
197+
component.setValue(_.get(value, component.key), { noUpdateEvent: true });
198+
});
199+
}
200+
201+
updateValue(flags, value) {
202+
// Intentionally skip over nested component updateValue method to keep recursive update from occurring with sub components.
203+
return BaseComponent.prototype.updateValue.call(this, flags, value);
204+
}
205+
206+
getValue() {
207+
return this.dataValue;
208+
}
209+
210+
setValue(dots) {
211+
this.dataValue = dots;
212+
if (!dots) {
213+
return;
214+
}
215+
dots.forEach((dot, index) => {
216+
const shape = this.drawDot(dot, index);
217+
this.dots.push({
218+
index,
219+
dot,
220+
shape
221+
});
222+
});
223+
this.selectDot(0);
224+
}
225+
226+
drawDot(dot, index) {
227+
//draw circle
228+
const circle = this.two.makeCircle(dot.coordinate.x, dot.coordinate.y, this.component.dotSize);
229+
circle.fill = this.component.dotFillColor;
230+
circle.stroke = this.component.dotStrokeColor;
231+
circle.linewidth = this.component.dotStrokeSize;
232+
circle.className += ' formio-tagpad-dot';
233+
//draw index
234+
const text = new Two.Text(index + 1, dot.coordinate.x, dot.coordinate.y);
235+
text.className += ' formio-tagpad-dot-index';
236+
this.two.add(text);
237+
this.two.update();
238+
circle._renderer.elem.addEventListener('mouseup', (e) => this.dotClicked(e, dot, index));
239+
text._renderer.elem.addEventListener('mouseup', (e) => this.dotClicked(e, dot, index));
240+
return { circle, text };
241+
}
242+
243+
saveSelectedDot() {
244+
const selectedDot = this.dots[this.selectedDotIndex];
245+
this.components.forEach(component => {
246+
selectedDot.dot.data[component.key] = component.getValue();
247+
});
248+
this.dataValue[this.selectedDotIndex] = selectedDot.dot.data;
249+
}
250+
251+
removeSelectedDot() {
252+
this.dataValue.splice(this.selectedDotIndex, 1);
253+
this.redrawDots();
254+
}
255+
256+
redrawDots() {
257+
this.dots = [];
258+
//clear canvas
259+
this.two.clear();
260+
//restore background
261+
this.addBackground();
262+
//draw dots
263+
this.setValue(this.dataValue);
264+
}
265+
}

src/sass/formio.form.scss

+38
Original file line numberDiff line numberDiff line change
@@ -876,3 +876,41 @@ body.formio-dialog-open {
876876
white-space:nowrap;
877877
text-align: center;
878878
}
879+
880+
.formio-tagpad-canvas-container {
881+
float: left;
882+
z-index: 1;
883+
position: relative;
884+
border: 1px dashed #999;
885+
border-radius: 3px;
886+
}
887+
888+
.formio-tagpad-form-container {
889+
width: 100%;
890+
float: left;
891+
}
892+
893+
.formio-tagpad-canvas {
894+
cursor: crosshair;
895+
}
896+
897+
.formio-tagpad-form {
898+
padding-left: 5px;
899+
padding-right: 5px;
900+
}
901+
902+
903+
.formio-tagpad-dot,
904+
.formio-tagpad-dot-index {
905+
cursor: pointer;
906+
}
907+
908+
.formio-tagpad-save-button,
909+
.formio-tagpad-remove-button {
910+
margin-right: 5px;
911+
margin-bottom: 5px;
912+
}
913+
914+
.formio-tagpad-form-title {
915+
font-size: 1.7em;
916+
}

0 commit comments

Comments
 (0)