PluginProbe
Hash Form – Drag & Drop Form Builder / trunk
Hash Form – Drag & Drop Form Builder vtrunk
1.4.4 1.4.3 1.4.2 1.4.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.6.1 1.2.7 1.2.8 1.2.9 1.3.0 All 47 releases
hash-form / includes / HashFormPreview.php

HashFormPreview.php in Hash Form – Drag & Drop Form Builder trunk, at includes/HashFormPreview.php

359 lines 13.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 defined('ABSPATH') || die();
3
4 class HashFormPreview {
5
6 public function __construct() {
7 /*
8 * Logged in only, and only for someone who may look at forms.
9 *
10 * The preview is opened from the builder and from the forms list, both
11 * of which are already behind that capability, so nothing legitimate
12 * reaches this without it. It used to be registered for nopriv as well,
13 * which let anyone walk the form ids and render each one.
14 */
15 add_action('wp_ajax_hashform_preview', array($this, 'preview'));
16 }
17
18 public static function preview() {
19 if (!HashFormCapabilities::user_can('hashform_view_forms')) {
20 wp_die(
21 esc_html__('You do not have permission to preview forms.', 'hash-form'),
22 esc_html__('Permission denied', 'hash-form'),
23 array('response' => 403)
24 );
25 }
26
27 header('Content-Type: text/html; charset=' . get_option('blog_charset'));
28 $id = htmlspecialchars_decode(HashFormHelper::get_var('form', 'absint'));
29 $form = HashFormBuilder::get_form_vars($id);
30
31 // This endpoint is public, so an unknown or trashed id must not reach
32 // the template, which dereferences $form before it checks anything.
33 if (!$form || $form->status === 'trash') {
34 wp_die(esc_html__('Please select a valid form', 'hash-form'));
35 }
36
37 /*
38 * Two documents behind one URL. The outer one is a plain shell — a
39 * toolbar and an iframe — and the inner one is the form on its own,
40 * rendered with the theme and every front-end asset exactly as a
41 * visitor would get it.
42 *
43 * The width buttons have to change a real viewport to be worth
44 * anything: narrowing a div would leave the form's own media queries
45 * reading the desktop window and reporting a mobile layout that is
46 * not what a phone gets.
47 */
48 if (HashFormHelper::get_var('hf_frame', 'absint')) {
49 require HASHFORM_PATH . 'admin/forms/preview/preview.php';
50 } else {
51 require HASHFORM_PATH . 'admin/forms/preview/shell.php';
52 }
53
54 wp_die();
55 }
56
57 /**
58 * Mark the fields a rule acts on, for the preview only.
59 *
60 * The preview runs the rules for real, so a hidden field is simply not
61 * there and nothing says why. This marks both ends - the field a rule shows
62 * or hides, and the field deciding it - and reports anything hidden at the
63 * moment in a bar along the bottom.
64 *
65 * Printed here rather than in the field markup because it must never reach
66 * a visitor: this file is only ever included by the preview screen, which
67 * is behind a capability check.
68 *
69 * @param int $form_id
70 */
71 public static function condition_hints($form_id) {
72 $hints = HashFormBuilder::get_condition_hints($form_id);
73
74 if (!$hints) {
75 return;
76 }
77
78 $payload = array();
79
80 foreach ($hints as $field_id => $roles) {
81 $payload[] = array(
82 'id' => (int) $field_id,
83 'target' => !empty($roles['target']),
84 'rules' => array_merge(
85 isset($roles['target']) ? $roles['target'] : array(),
86 isset($roles['trigger']) ? $roles['trigger'] : array()
87 ),
88 );
89 }
90 ?>
91 <style>
92 .hf-preview-rule {
93 max-width: 100%;
94 background: #fff;
95 border: 1px solid #e2e5ea;
96 border-radius: 999px;
97 color: #646970;
98 display: inline-flex;
99 align-items: center;
100 gap: 5px;
101 font-size: 11px;
102 line-height: 1;
103 margin: 4px 0 0;
104 padding: 3px 8px;
105 vertical-align: middle;
106 }
107
108 .hf-preview-rule::before {
109 background: #dd823b;
110 border-radius: 50%;
111 content: "";
112 height: 6px;
113 width: 6px;
114 }
115
116 .hf-preview-rule-trigger::before {
117 background: #8c9196;
118 }
119
120 .hf-preview-rule-text {
121 overflow: hidden;
122 text-overflow: ellipsis;
123 white-space: nowrap;
124 }
125
126 .hf-preview-rule-more {
127 flex: 0 0 auto;
128 opacity: .75;
129 }
130
131 .hf-preview-hidden-note {
132 background: #1e2327;
133 border-radius: 6px;
134 bottom: 12px;
135 color: #fff;
136 font-size: 12px;
137 left: 12px;
138 padding: 8px 12px;
139 position: fixed;
140 z-index: 99999;
141 }
142 </style>
143 <script>
144 (function () {
145 var hints = <?php echo wp_json_encode($payload); ?>;
146
147 function chip(rules, isTarget) {
148 var el = document.createElement('span');
149 el.className = 'hf-preview-rule' + (isTarget ? '' : ' hf-preview-rule-trigger');
150
151 /*
152 * The rule itself, the same as on the canvas: a chip that
153 * only says "Conditional" sends you off to the Settings tab
154 * to find out which rule it meant.
155 *
156 * In its own element because the chip is a flex container,
157 * and text-overflow has nothing to trim on one of those -
158 * it needs a block of its own to ellipsis.
159 */
160 var text = document.createElement('span');
161 text.className = 'hf-preview-rule-text';
162 text.textContent = rules[0];
163 el.appendChild(text);
164
165 if (rules.length > 1) {
166 var more = document.createElement('span');
167 more.className = 'hf-preview-rule-more';
168 more.textContent = '+' + (rules.length - 1);
169 el.appendChild(more);
170 }
171
172 el.title = rules.join('\n');
173 return el;
174 }
175
176 function paint() {
177 var hidden = 0;
178
179 hints.forEach(function (hint) {
180 var box = document.getElementById('hf-field-container-' + hint.id);
181
182 if (!box) {
183 return;
184 }
185
186 /*
187 * A rule that has already hidden the field leaves
188 * nothing to mark, so it is counted instead.
189 *
190 * The test is the inline display the rule engine
191 * writes - frontend.js hides with jQuery's toggle() -
192 * and not whether the field is on screen. On a
193 * multi-step form every field of every other step is
194 * off screen too, and reporting those as hidden by a
195 * rule would be wrong.
196 */
197 if (box.style.display === 'none') {
198 hidden++;
199 return;
200 }
201
202 if (!box.querySelector('.hf-preview-rule')) {
203 box.appendChild(chip(hint.rules, hint.target));
204 }
205 });
206
207 var note = document.getElementById('hf-preview-hidden-note');
208
209 if (!hidden) {
210 if (note) {
211 note.remove();
212 }
213 return;
214 }
215
216 if (!note) {
217 note = document.createElement('div');
218 note.id = 'hf-preview-hidden-note';
219 note.className = 'hf-preview-hidden-note';
220 document.body.appendChild(note);
221 }
222
223 note.textContent = <?php echo wp_json_encode(__('Hidden by a rule right now:', 'hash-form')); ?> + ' ' + hidden;
224 }
225
226 /*
227 * Watch the containers rather than listen for events.
228 *
229 * The rules are wired up on jQuery ready and settled with a
230 * trigger('change') that a plain listener on document does not
231 * reliably see, so painting once on DOMContentLoaded marked a
232 * field that was hidden a moment later. The engine hides by
233 * writing an inline style, and that is exactly what this
234 * watches - so the marks follow the rules however they are
235 * re-evaluated, including as the visitor types.
236 */
237 function watch() {
238 /*
239 * Coalesced rather than painted per mutation.
240 *
241 * Changing one answer re-evaluates every rule, and each
242 * one that flips writes its own style. Painting on the
243 * first of those counted a state the cascade had not
244 * finished reaching, so the tally could read one high
245 * until something else moved. Waiting for the burst to
246 * stop counts once, on the settled state.
247 */
248 var pending = null;
249 var observer = new MutationObserver(function () {
250 window.clearTimeout(pending);
251 pending = window.setTimeout(paint, 50);
252 });
253
254 hints.forEach(function (hint) {
255 var box = document.getElementById('hf-field-container-' + hint.id);
256
257 if (box) {
258 observer.observe(box, { attributes: true, attributeFilter: ['style', 'class'] });
259 }
260 });
261
262 paint();
263 }
264
265 if (document.readyState === 'loading') {
266 document.addEventListener('DOMContentLoaded', watch);
267 } else {
268 watch();
269 }
270 }());
271 </script>
272 <?php
273 }
274
275 /**
276 * The URL of the form on its own, without the surrounding toolbar.
277 */
278 public static function frame_url($id) {
279 return add_query_arg(
280 array(
281 'action' => 'hashform_preview',
282 'form' => absint($id),
283 'hf_frame' => 1,
284 ),
285 admin_url('admin-ajax.php')
286 );
287 }
288
289 /**
290 * Widths the preview can be shown at.
291 *
292 * Values are the iframe width in pixels; 0 means fill the window.
293 */
294 public static function preview_widths() {
295 return array(
296 'desktop' => array('label' => esc_html__('Desktop', 'hash-form'), 'width' => 0, 'icon' => 'monitor'),
297 'tablet' => array('label' => esc_html__('Tablet', 'hash-form'), 'width' => 768, 'icon' => 'tablet'),
298 'mobile' => array('label' => esc_html__('Mobile', 'hash-form'), 'width' => 390, 'icon' => 'mobile'),
299 );
300 }
301
302 public static function show_form($id) {
303 $form = HashFormBuilder::get_form_vars($id);
304 if (!$form || $form->status === 'trash') {
305 // Callers buffer output, so the message must be echoed.
306 echo esc_html__('Please select a valid form', 'hash-form');
307 return;
308 }
309
310 self::get_form_contents($id, $form);
311 }
312
313 public static function get_form_contents($id, $form = null) {
314 // Frontend assets are registered globally but only loaded once a form
315 // actually renders.
316 HashFormLoader::enqueue_form_assets();
317
318 $form = $form ? $form : HashFormBuilder::get_form_vars($id);
319
320 // Scheduled, limited or login-only forms show their notice instead.
321 $restriction = HashFormRestrictions::check($form);
322
323 if (empty($restriction['allowed'])) {
324 echo wp_kses_post(HashFormRestrictions::get_closed_html($restriction));
325 return;
326 }
327
328 $values = HashFormHelper::get_fields_array($id);
329
330 $styles = $form->styles ? $form->styles : '';
331
332 $form_class = array('hashform-form');
333 $form_class[] = isset($form->options['form_css_class']) ? $form->options['form_css_class'] : '';
334 $form_class[] = $styles && isset($styles['form_style']) ? 'hf-form-' . esc_attr($styles['form_style']) : 'hf-form-default-style';
335 $form_class = apply_filters('hashform_form_classes', $form_class);
336 ?>
337
338 <div class="hf-form-tempate">
339 <form enctype="multipart/form-data" method="post" class="<?php echo esc_attr(implode(' ', array_filter($form_class))); ?>" id="hf-form-id-<?php echo esc_attr($form->form_key); ?>" novalidate>
340 <?php
341 do_action('hash_form_before_form_start', $form);
342 if (apply_filters('hash_form_should_show_form', true)) {
343 require HASHFORM_PATH . 'admin/forms/style/form.php';
344 $form_msg = HashFormHelper::get_var('hf_success');
345 if ($form_msg == 'true') {
346 ?>
347 <span class="hf-success-msg"><?php echo esc_html($form->settings['confirmation_message']); ?></span>
348 <?php
349 }
350 }
351 ?>
352 </form>
353 </div>
354 <?php
355 }
356
357 }
358
359 new HashFormPreview();