$fields Content fields
102 | */
103 | protected function write(VersionId $versionId, Language $language, array $fields): void
104 | {
105 | $data = $this->getSessionData();
106 | $fields['_modified'] = time();
107 | $data[$versionId->value()][$language->code()] = $fields;
108 | $this->setSessionData($data);
109 | }
110 |
111 | /**
112 | * Store submission reference in session
113 | */
114 | public function storeReference(): void
115 | {
116 | if (!($this->model instanceof SubmissionPage)) {
117 | return;
118 | }
119 |
120 | /** @var SubmissionPage $submission */
121 | $submission = $this->model;
122 | $session = App::instance()->session();
123 |
124 | if ($submission->exists()) {
125 | // Page exists on disk - just store the slug
126 | $session->set(DreamForm::SESSION_KEY, $submission->slug());
127 | } else {
128 | // Page doesn't exist - store a data array that can be used to reconstruct it
129 | $session->set(DreamForm::SESSION_KEY, [
130 | 'type' => 'submission',
131 | 'template' => $submission->intendedTemplate()->name(),
132 | 'slug' => $submission->slug(),
133 | 'parent' => $submission->parent()?->id(),
134 | // Content is already stored via the storage handler's write() method
135 | ]);
136 | }
137 | }
138 |
139 | /**
140 | * Clean up session data for this submission
141 | */
142 | public function cleanup(): void
143 | {
144 | App::instance()->session()->remove($this->sessionKey());
145 | App::instance()->session()->remove(DreamForm::SESSION_KEY);
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/classes/Support/HasCache.php:
--------------------------------------------------------------------------------
1 | get($key);
27 | if ($value === null) {
28 | try {
29 | $value = $callback();
30 | $cache->set($key, $value, $minutes);
31 | } catch (\Throwable $e) {
32 | $cache->remove($key);
33 | return null;
34 | }
35 | }
36 |
37 | return $value;
38 | }
39 |
40 | /**
41 | * Set a value for the performer cache
42 | */
43 | protected static function setCache(string $key, mixed $value, int $minutes = 10): bool
44 | {
45 | if (!($cache = static::cacheInstance())) {
46 | return false;
47 | }
48 |
49 | $key = static::type() . '.' . $key;
50 | return $cache->set($key, $value, $minutes);
51 | }
52 |
53 | /**
54 | * Get a value from the performer cache
55 | */
56 | protected static function getCache(string $key): mixed
57 | {
58 | if (!($cache = static::cacheInstance())) {
59 | return null;
60 | }
61 |
62 | $key = static::type() . '.' . $key;
63 | return $cache->get($key);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/classes/Support/Htmx.php:
--------------------------------------------------------------------------------
1 | request()->header('Hx-Request') === 'true';
26 | }
27 |
28 | /**
29 | * Returns the secret key for encrypting and decrypting values
30 | */
31 | private static function secret(): string
32 | {
33 | $secret = DreamForm::option('secret');
34 |
35 | if (empty($secret)) {
36 | throw new \Exception('[DreamForm] Secret not set');
37 | }
38 |
39 | return $secret;
40 | }
41 |
42 | public const CIPHER = 'AES-128-CBC';
43 |
44 | /**
45 | * Encrypt a string value for use in HTMX attributes
46 | * Based on example code from https://www.php.net/manual/en/function.openssl-encrypt.php
47 | */
48 | public static function encrypt(string $value): string
49 | {
50 | $ivlen = openssl_cipher_iv_length(self::CIPHER);
51 | $iv = openssl_random_pseudo_bytes($ivlen);
52 | $encrypted = openssl_encrypt($value, self::CIPHER, static::secret(), OPENSSL_RAW_DATA, $iv);
53 | $hmac = hash_hmac('sha256', $encrypted, static::secret(), true);
54 |
55 | return base64_encode($iv . $hmac . $encrypted);
56 | }
57 |
58 | /**
59 | * Decrypt a string value from HTMX attributes
60 | */
61 | public static function decrypt(string $value): string
62 | {
63 | $c = base64_decode($value);
64 | $ivlen = openssl_cipher_iv_length(static::CIPHER);
65 | $iv = substr($c, 0, $ivlen);
66 | $hmac = substr($c, $ivlen, $sha2len = 32);
67 | $encrypted = substr($c, $ivlen + $sha2len);
68 |
69 | if (empty($hmac) || !hash_equals($hmac, hash_hmac('sha256', $encrypted, static::secret(), true))) {
70 | throw new \Exception('Decryption failed');
71 | }
72 |
73 | return openssl_decrypt($encrypted, static::CIPHER, static::secret(), OPENSSL_RAW_DATA, $iv);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/classes/Support/Menu.php:
--------------------------------------------------------------------------------
1 | request()->path()->toString();
24 | }
25 |
26 | /**
27 | * Returns the path to the forms page
28 | */
29 | public static function formPath()
30 | {
31 | $formsPage = App::instance()->site()->findPageOrDraft(DreamForm::option('page'));
32 | return $formsPage?->panel()->path() ?? "/pages/forms";
33 | }
34 |
35 | /**
36 | * Returns the menu item for the forms page
37 | */
38 | public static function forms()
39 | {
40 | if (App::instance()->user()?->role()->permissions()->for('tobimori.dreamform', 'accessForms') === false) {
41 | return null;
42 | }
43 |
44 | return [
45 | 'label' => t('dreamform.forms'),
46 | 'link' => static::formPath(),
47 | 'icon' => 'survey',
48 | 'current' => fn () =>
49 | str_contains(static::path(), static::formPath())
50 | ];
51 | }
52 |
53 | /**
54 | * Returns the menu item for the submissions page
55 | */
56 | public static function site()
57 | {
58 | return [
59 | 'current' => fn (string|null $id) => $id === 'site' && !str_contains(static::path(), static::formPath())
60 | ];
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tobimori/kirby-dreamform",
3 | "description": "Kirby DreamForm is an opiniated form builder plugin for Kirby CMS 4+ that makes forms work like magic",
4 | "type": "kirby-plugin",
5 | "license": "proprietary",
6 | "homepage": "https://plugins.andkindness.com/dreamform",
7 | "version": "2.0.0-rc.3",
8 | "authors": [
9 | {
10 | "name": "Tobias Möritz",
11 | "email": "tobias@moeritz.io"
12 | }
13 | ],
14 | "autoload": {
15 | "psr-4": {
16 | "tobimori\\DreamForm\\": "classes"
17 | }
18 | },
19 | "minimum-stability": "RC",
20 | "require": {
21 | "php": ">=8.3.0",
22 | "getkirby/composer-installer": "^1.2.1"
23 | },
24 | "scripts": {
25 | "dist": "composer install --no-dev --optimize-autoloader",
26 | "fix": "php-cs-fixer fix"
27 | },
28 | "config": {
29 | "optimize-autoloader": true,
30 | "allow-plugins": {
31 | "getkirby/composer-installer": true
32 | }
33 | },
34 | "extra": {
35 | "kirby-cms-path": false
36 | },
37 | "require-dev": {
38 | "getkirby/cms": "^5"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/config/api.php:
--------------------------------------------------------------------------------
1 | fn () => [
9 | [
10 | 'pattern' => '/dreamform/object/mailchimp/pages/(:any)/(:any)',
11 | 'action' => fn ($path, $list) => MailchimpAction::fieldMapping(DreamForm::findPageOrDraftRecursive(Str::replace($path, '+', '/')), $list),
12 | ]
13 | ]
14 | ];
15 |
--------------------------------------------------------------------------------
/config/areas.php:
--------------------------------------------------------------------------------
1 | fn () => [
12 | 'dialogs' => [
13 | 'dreamform/activate' => [
14 | 'load' => fn () => [
15 | 'component' => 'k-form-dialog',
16 | 'props' => [
17 | 'fields' => [
18 | 'domain' => [
19 | 'label' => t('dreamform.license.activate.label'),
20 | 'type' => 'info',
21 | 'theme' => ($isLocal = App::instance()->system()->isLocal()) ? 'warning' : 'info',
22 | 'text' => tt(
23 | 'dreamform.license.activate.' . ($isLocal ? 'local' : 'domain'),
24 | ['domain' => App::instance()->system()->indexUrl()]
25 | ),
26 | ],
27 | 'email' => Field::email(['required' => true]),
28 | 'license' => [
29 | 'label' => t('dreamform.license.key.label'),
30 | 'type' => 'text',
31 | 'required' => true,
32 | 'counter' => false,
33 | 'placeholder' => 'DF-XXX-1234XXXXXXXXXXXXXXXXXXXX',
34 | 'help' => t('dreamform.license.key.help'),
35 | ],
36 | ],
37 | 'submitButton' => [
38 | 'icon' => 'key',
39 | 'text' => t('dreamform.license.activate'),
40 | 'theme' => 'love',
41 | ]
42 | ]
43 | ],
44 | 'submit' => function () {
45 | $body = App::instance()->request()->body();
46 |
47 | if (!V::email($body->get('email'))) {
48 | throw new Exception(t('dreamform.license.error.email'));
49 | }
50 |
51 | if (!Str::startsWith($body->get('license'), 'DF-STD-') && !Str::startsWith($body->get('license'), 'DF-ENT-')) {
52 | throw new Exception(t('dreamform.license.error.key'));
53 | }
54 |
55 | License::downloadLicense(
56 | email: $body->get('email'),
57 | license: $body->get('license')
58 | );
59 |
60 | return [
61 | 'message' => 'License activated successfully!',
62 | ];
63 | }
64 | ],
65 | 'submission/(:any)/mark-as-spam' => [
66 | 'load' => function (string $path) {
67 | return [
68 | 'component' => 'k-text-dialog',
69 | 'props' => [
70 | 'text' => t('dreamform.submission.reportAsSpam.confirm'),
71 | 'submitButton' => [
72 | 'text' => t('dreamform.submission.reportAsSpam.button'),
73 | 'icon' => 'spam',
74 | 'theme' => 'negative'
75 | ],
76 | ]
77 | ];
78 | },
79 | 'submit' => function (string $path) {
80 | $submission = DreamForm::findPageOrDraftRecursive(Str::replace($path, '+', '/'));
81 | $submission = $submission->markAsSpam();
82 |
83 | return [
84 | 'message' => t('dreamform.submission.reportAsSpam.success'),
85 | ];
86 | }
87 | ],
88 | 'submission/(:any)/mark-as-ham' => [
89 | 'load' => function (string $path) {
90 | $submission = DreamForm::findPageOrDraftRecursive(Str::replace($path, '+', '/'));
91 |
92 | return [
93 | 'component' => 'k-text-dialog',
94 | 'props' => [
95 | 'text' => t($submission->actionsDidRun() ? 'dreamform.submission.reportAsHam.confirm.default' : 'dreamform.submission.reportAsHam.confirm.unprocessed'),
96 | 'submitButton' => [
97 | 'text' => t('dreamform.submission.reportAsHam.button'),
98 | 'icon' => 'shield-check',
99 | 'theme' => 'positive'
100 | ],
101 | ]
102 | ];
103 | },
104 | 'submit' => function (string $path) {
105 | $submission = DreamForm::findPageOrDraftRecursive(Str::replace($path, '+', '/'));
106 | $submission = $submission->markAsHam();
107 |
108 | if (!$submission->actionsDidRun()) {
109 | $submission->updateState(['actionsdidrun' => true]);
110 | $submission->handleActions(force: true);
111 | }
112 |
113 | return [
114 | 'message' => t('dreamform.submission.reportAsHam.success'),
115 | ];
116 | }
117 | ],
118 | 'submission/(:any)/run-actions' => [
119 | 'load' => function () {
120 | return [
121 | 'component' => 'k-text-dialog',
122 | 'props' => [
123 | 'text' => t('dreamform.submission.runActions.confirm'),
124 | 'submitButton' => [
125 | 'text' => t('dreamform.submission.runActions.button'),
126 | 'icon' => 'play',
127 | 'theme' => 'positive'
128 | ],
129 | ]
130 | ];
131 | },
132 | 'submit' => function (string $path) {
133 | $submission = DreamForm::findPageOrDraftRecursive(Str::replace($path, '+', '/'));
134 | $submission = $submission->handleActions(force: true);
135 |
136 | return [
137 | 'message' => t('dreamform.submission.runActions.success'),
138 | ];
139 | }
140 | ],
141 | ]
142 | ]
143 | ];
144 |
--------------------------------------------------------------------------------
/config/blockMethods.php:
--------------------------------------------------------------------------------
1 | function (Collection $fields) {
7 | return $fields->findByKey($this->id());
8 | }
9 | ];
10 |
--------------------------------------------------------------------------------
/config/fields.php:
--------------------------------------------------------------------------------
1 | [
11 | 'extends' => 'object',
12 | 'props' => [
13 | // Unset inherited props
14 | 'fields' => null,
15 |
16 | // reload when the following field changes
17 | 'sync' => function (string|null $sync = null) {
18 | return $sync;
19 | },
20 |
21 | // fetch field setup from the API
22 | 'api' => function (string|null $api = null) {
23 | return $api;
24 | }
25 | ]
26 | ],
27 | 'dreamform-dynamic-field' => [
28 | 'props' => [
29 | 'after' => null,
30 | 'before' => null,
31 | 'placeholder' => null,
32 | 'icon' => null,
33 |
34 | 'limitType' => function (string|array|null $limitType = null): array|null {
35 | if (!is_array($limitType)) {
36 | $limitType = [$limitType];
37 | }
38 |
39 | $limitType = array_filter($limitType, fn ($type) => $type !== null);
40 | if (empty($limitType)) {
41 | return null;
42 | }
43 |
44 | return $limitType;
45 | }
46 | ],
47 | 'computed' => [
48 | 'value' => function () {
49 | $data = Data::decode($this->value, 'yaml');
50 |
51 | if (isset($data[0]) && is_string($data[0])) {
52 | if (!!Str::match($data[0], "/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/")) {
53 | return [
54 | 'type' => 'dynamic',
55 | 'field' => $data[0],
56 | 'value' => null
57 | ];
58 | }
59 |
60 | return [
61 | 'type' => 'static',
62 | 'field' => null,
63 | 'value' => $data[0]
64 | ];
65 | }
66 |
67 | return $data;
68 | },
69 | 'options' => function () {
70 | $page = $this->model;
71 |
72 | $limit = $this->limitType();
73 | return DreamForm::requestCache($limit ? [$page->uuid()->id(), implode(';', $limit)] : $page->uuid()->id(), function () use ($page, $limit) {
74 | $fields = [];
75 | foreach ($page->fields() as $field) {
76 | if (!$field::hasValue()) {
77 | continue;
78 | }
79 |
80 | if ($limit !== null && !A::has($limit, Str::replace($field->block()->type(), '-field', ''))) {
81 | continue;
82 | }
83 |
84 | $blueprint = $field::blueprint();
85 |
86 | $fields[] = [
87 | 'id' => $field->id(),
88 | 'label' => $field->block()->label()->or($field->key())->value(),
89 | 'icon' => $blueprint['icon'] ?? 'input-cursor-move',
90 | 'type' => $blueprint['name'] ?? "",
91 | ];
92 | }
93 |
94 | return $fields;
95 | });
96 | }
97 | ],
98 | 'validations' => [
99 | 'value' => function (array|null $value) {
100 | if (empty($value) === true && !$this->required()) {
101 | return true;
102 | }
103 |
104 | if ($value['type'] === 'dynamic' && $value['field']) {
105 | $limit = $this->limitType();
106 | foreach ($this->model->fields() as $field) {
107 | if (!$field::hasValue()) {
108 | continue;
109 | }
110 |
111 | if ($limit !== null && !A::has($limit, Str::replace($field->block()->type(), '-field', ''))) {
112 | continue;
113 | }
114 |
115 | if ($field->id() === $value['field']) {
116 | return true;
117 | }
118 | }
119 | } elseif ($value['type'] === 'static' && $value['value']) {
120 | return true;
121 | }
122 |
123 | if ($this->required()) {
124 | throw new InvalidArgumentException();
125 | }
126 | },
127 | ]
128 | ]
129 | ];
130 |
--------------------------------------------------------------------------------
/config/hooks.php:
--------------------------------------------------------------------------------
1 | function () {
11 | DreamForm::install();
12 | },
13 |
14 | /**
15 | * Injects submission variables in the page rendering process
16 | */
17 | 'page.render:before' => function (string $contentType, array $data, Kirby\Cms\Page $page) {
18 | return [
19 | ...$data,
20 | 'submission' => SubmissionPage::fromSession()
21 | ];
22 | },
23 |
24 | /*
25 | * Deletes all files associated with a submission page with elevated permissions,
26 | * so we can disallow deleting single files from the panel
27 | */
28 | 'page.delete:before' => function (Kirby\Cms\Page $page) {
29 | if ($page->intendedTemplate()->name() === 'submission') {
30 | $page->kirby()->impersonate('kirby');
31 | foreach ($page->files() as $file) {
32 | $file->delete();
33 | }
34 | $page->kirby()->impersonate();
35 | }
36 | }
37 | ];
38 |
--------------------------------------------------------------------------------
/config/options.php:
--------------------------------------------------------------------------------
1 | [
8 | 'sessionless' => [
9 | 'active' => true,
10 | ],
11 | 'fields' => [
12 | 'active' => true
13 | ],
14 | 'performer' => [
15 | 'active' => true
16 | ]
17 | ],
18 | 'useDataAttributes' => false, // uses data-form-url instead of action
19 | 'mode' => 'prg', // prg / api / htmx
20 | 'multiStep' => true, // Enable multi-step forms
21 | 'storeSubmissions' => true, // Store submissions in the content folder
22 | 'partialSubmissions' => true, // Allow partial submissions toggle in panel (requires precognition)
23 | 'precognition' => false, // Enable precognition (HTMX mode only) - Requires "idiomorph" htmx extension
24 | 'debug' => fn () => App::instance()->option('debug'),
25 | 'layouts' => [ // https://getkirby.com/docs/reference/panel/fields/layout#defining-your-own-layouts
26 | '1/1',
27 | '1/2, 1/2'
28 | ],
29 | 'page' => 'page://forms', // Slug or URI to the page where the forms are located
30 | 'secret' => null, // Encryption secret for htmx attributes
31 | 'marks' => ['bold', 'italic', 'underline', 'strike', 'link', 'email'], // Marks to be used in writer fields
32 | 'metadata' => [
33 | 'collect' => [] // 'ip' | 'userAgent'
34 | ],
35 | 'guards' => [
36 | 'available' => ['honeypot', 'csrf'],
37 | 'honeypot' => [
38 | 'fields' => ['website', 'email', 'name', 'url', 'birthdate', 'comment', 'summary', 'subject']
39 | ],
40 | 'akismet' => [
41 | 'apiKey' => null,
42 | 'fields' => [
43 | 'comment_author' => ['name', 'first-name', 'last-name', 'username'],
44 | 'comment_author_email' => ['email', 'mail', 'e-mail', 'email-address', 'emailaddress'],
45 | 'comment_author_url' => ['website', 'url', 'homepage', 'website-url'],
46 | 'comment_content' => ['message', 'comment', 'content', 'body', 'text', 'description']
47 | ]
48 | ],
49 | 'turnstile' => [
50 | 'theme' => 'auto',
51 | 'siteKey' => null,
52 | 'secretKey' => null,
53 | 'injectScript' => true
54 | ],
55 | 'hcaptcha' => [
56 | 'theme' => 'auto', // 'auto', 'light', 'dark', 'custom', or custom theme object
57 | 'size' => 'normal', // 'normal' or 'compact'
58 | 'siteKey' => null,
59 | 'secretKey' => null,
60 | 'injectScript' => true,
61 | 'customTheme' => null // Custom theme configuration (Pro/Enterprise only)
62 | ],
63 | 'ratelimit' => [
64 | 'limit' => 10,
65 | 'interval' => 3
66 | ]
67 | ],
68 | 'fields' => [
69 | 'available' => true,
70 | 'email' => [
71 | 'dnsLookup' => true,
72 | 'disposableEmails' => [
73 | 'disallow' => true,
74 | 'list' => 'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.txt'
75 | ]
76 | ],
77 | 'pages' => [
78 | 'query' => 'site.childrenAndDrafts.filterBy("intendedTemplate", "!=", "forms")' // Page query for the pages field type
79 | ],
80 | 'fileUpload' => [
81 | 'types' => [
82 | // JPEG, PNG, GIF, AVIF, WEBP
83 | 'images' => ["image/jpeg", "image/png", "image/gif", "image/avif", "image/webp",],
84 | // MP3, OGG, OPUS, WAV, WEBM
85 | 'audio' => ["audio/mpeg", "audio/ogg", "audio/opus", "audio/aac", "audio/wav", "audio/webm"],
86 | // AVI, MP4, MPEG, OGG, WEBM
87 | 'video' => ["video/x-msvideo", "video/mp4", "video/mpeg", "video/ogg", "video/webm"],
88 | // PDF, DOC, XLS, PPT
89 | 'documents' => ["application/pdf", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint"],
90 | // ZIP, RAR, TAR, 7Z
91 | 'archives' => ["application/zip", "application/x-rar-compressed", "application/x-tar", "application/x-7z-compressed"]
92 | ]
93 | ]
94 | ],
95 | 'actions' => [
96 | 'available' => true,
97 | 'discord' => [
98 | 'webhook' => null // Default webhook URL
99 | ],
100 | 'mailchimp' => [
101 | 'apiKey' => null // Mailchimp API key
102 | ],
103 | 'loops' => [
104 | 'apiKey' => null // Loops API key
105 | ],
106 | 'brevo' => [
107 | 'apiKey' => null // Brevo API key
108 | ],
109 | 'buttondown' => [
110 | 'apiKey' => null, // Buttondown API key
111 | 'simpleMode' => false // Simple mode supports free plans, removes tags support
112 | ],
113 | 'email' => [
114 | 'from' => [
115 | 'email' => fn () => App::instance()->option('email.transport.username'),
116 | 'name' => fn () => App::instance()->site()->title()
117 | ]
118 | ],
119 | 'plausible' => [
120 | 'domain' => null,
121 | 'apiUrl' => 'https://plausible.io/api'
122 | ]
123 | ],
124 | 'integrations' => [
125 | 'gravatar' => true, // Get profile pictures for email fields from Gravatar
126 | ],
127 | 'refererPageResolver' => fn (string | null $referer) => DreamForm::findPageOrDraftRecursive($referer) // Callback to resolve custom page URLs to pages
128 | ];
129 |
--------------------------------------------------------------------------------
/config/sections.php:
--------------------------------------------------------------------------------
1 | [
9 | 'computed' => [
10 | 'page' => function () {
11 | if ($this->model()->intendedTemplate()->name() !== 'submission') {
12 | throw new Exception('[DreamForm] This section can only be used on submission pages');
13 | }
14 |
15 | return $this->model();
16 | },
17 | 'isSpam' => function () {
18 | return $this->model()->isSpam();
19 | },
20 | 'isPartial' => function () {
21 | return !$this->model()->isFinished();
22 | },
23 | 'log' => function () {
24 | return $this->model()->log()->toArray();
25 | }
26 | ]
27 | ],
28 | 'dreamform-license' => [
29 | 'computed' => [
30 | 'local' => function () {
31 | return App::instance()->system()->isLocal();
32 | },
33 | 'activated' => function () {
34 | return License::fromDisk()->isValid();
35 | }
36 | ]
37 | ]
38 | ];
39 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import prettier from "eslint-config-prettier";
3 | import vue from "eslint-plugin-vue";
4 |
5 | export default [
6 | js.configs.recommended,
7 | ...vue.configs["flat/vue2-recommended"],
8 | prettier,
9 | {
10 | rules: {
11 | "vue/attributes-order": "error",
12 | "vue/component-definition-name-casing": "off",
13 | "vue/html-closing-bracket-newline": [
14 | "error",
15 | {
16 | singleline: "never",
17 | multiline: "always",
18 | },
19 | ],
20 | "vue/multi-word-component-names": "off",
21 | "vue/require-default-prop": "off",
22 | "vue/require-prop-types": "error",
23 | "no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^props$" }],
24 | },
25 | languageOptions: {
26 | ecmaVersion: 2022,
27 | globals: {
28 | window: "readonly",
29 | document: "readonly",
30 | console: "readonly",
31 | panel: "readonly"
32 | }
33 | },
34 | },
35 | ];
36 |
--------------------------------------------------------------------------------
/i18n.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1.8,
3 | "locale": {
4 | "source": "en",
5 | "targets": ["de", "en", "fr", "nl", "cs", "es", "it"]
6 | },
7 | "buckets": {
8 | "json": {
9 | "include": ["translations/[locale].json"]
10 | }
11 | },
12 | "$schema": "https://lingo.dev/schema/i18n.json"
13 | }
14 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["./src/*"]
6 | },
7 | "lib": ["esnext", "dom"]
8 | },
9 | "vueCompilerOptions": {
10 | "target": 2.7
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "mjml": "mjml templates/emails/dreamform.mjml -o templates/emails/dreamform.html.php",
5 | "dev": "kirbyup serve src/index.js",
6 | "build": "kirbyup src/index.js",
7 | "lint": "eslint \"src/**/*.{js,vue}\"",
8 | "lint:fix": "npm run lint -- --fix",
9 | "format": "prettier --write \"src/**/*.{js,vue}\"",
10 | "prepare": "husky"
11 | },
12 | "devDependencies": {
13 | "@eslint/js": "^9.28.0",
14 | "eslint": "^9.28.0",
15 | "eslint-config-prettier": "^10.1.5",
16 | "eslint-plugin-vue": "^10.1.0",
17 | "husky": "^9.1.7",
18 | "kirbyup": "^3.3.0",
19 | "lint-staged": "^16.1.0",
20 | "mjml": "^4.15.3",
21 | "prettier": "^3.5.3"
22 | },
23 | "dependencies": {
24 | "kirbyuse": "^0.12.1"
25 | },
26 | "browserslist": [
27 | "last 2 Android versions",
28 | "last 2 Chrome versions",
29 | "last 2 ChromeAndroid versions",
30 | "last 2 Edge versions",
31 | "last 2 Firefox versions",
32 | "last 2 FirefoxAndroid versions",
33 | "last 2 iOS versions",
34 | "last 2 KaiOS versions",
35 | "last 2 Safari versions",
36 | "last 2 Samsung versions",
37 | "last 2 Opera versions",
38 | "last 2 OperaMobile versions",
39 | "last 2 UCAndroid versions"
40 | ],
41 | "lint-staged": {
42 | "src/*.{js,vue}": [
43 | "eslint --fix",
44 | "prettier --write"
45 | ],
46 | "*.php": [
47 | ".husky/php-cs-fixer-lint-staged"
48 | ]
49 | },
50 | "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
51 | }
52 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | onlyBuiltDependencies:
2 | - '@parcel/watcher'
3 | - esbuild
4 |
--------------------------------------------------------------------------------
/public.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt+adZ2ODZhDtkBriVnN8
3 | 9noQfD6v1CTJsekknHX+MdcolGb5ydXrfwNCxDXJ399RlY/JGqJTmnvy8rned6S9
4 | 5sIR6FowFWmJ+3lrzn0NbqRt1FRMDSNdg99priNUkKRkKajvBiOylRGXlniAQ540
5 | Eh6bm7BO8XMwyLMpuNAM7FOy55BmU0hzCovlVdOLjQM54HpYf8sV8+k37ysVS+5W
6 | u+TgFIg2HCj804raXo5mhZ3bMIyoK3YJ5oXVRa7j6zuE/YF2Gao8j684Br7sYefV
7 | qsyxtl3BRdwXaB98sSQIX4OQIbheqF1ftpDd+TaII6Q0qX+PlyGJb2MCDjAvyLxY
8 | lwIDAQAB
9 | -----END PUBLIC KEY-----
10 |
--------------------------------------------------------------------------------
/snippets/fields/button.php:
--------------------------------------------------------------------------------
1 | fields(
19 | $submission?->form()->is($form) ? $submission?->currentStep() ?? 1 : 1
20 | )->filterBy('type', 'button'))
21 | && $buttonFields->last() === $field
22 | ) {
23 | snippet('dreamform/guards', compact('form', 'attr'));
24 | }
25 |
26 | snippet('dreamform/fields/partials/wrapper', compact('block', 'field', 'form', 'attr'), slots: true) ?>
27 |
28 | 'submit'
30 | ])) ?>>
31 | = $block->label()->or(t('dreamform.fields.button.label.label'))->escape() ?>
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/snippets/fields/checkbox.php:
--------------------------------------------------------------------------------
1 | valueFor($block->key())?->value(), ',') ?? [];
19 | if (!is_array($previousValue)) {
20 | $previousValue = [$previousValue];
21 | }
22 |
23 | $attr = A::merge($attr, $attr[$type] ?? []);
24 | snippet('dreamform/fields/partials/wrapper', $arguments = compact('block', 'field', 'form', 'attr'), slots: true);
25 |
26 | if ($block->label()->isNotEmpty()) {
27 | snippet('dreamform/fields/partials/label', $arguments);
28 | } ?>
29 |
30 | options()->toStructure() as $option) : ?>
31 | >
32 | $type,
34 | 'id' => $form->elementId("{$block->id()}-{$option->indexOf()}"),
35 | 'name' => $block->key() . ($type === 'checkbox' ? '[]' : null),
36 | 'value' => $option->value(),
37 | 'checked' => A::has($previousValue, $option->value()),
38 | 'aria-invalid' => ($error = $submission?->errorFor($block->key(), $form)) ? true : null,
39 | 'aria-describedby' => $error ? $form->elementId("{$block->id()}/error") : null,
40 | ], $field->htmxAttr($form))) ?>>
41 | $form->elementId("{$block->id()}-{$option->indexOf()}")])) ?>>= $option->label()->or($option->value())->permalinksToUrls() ?>
42 |
43 |
44 |
45 |
49 |
--------------------------------------------------------------------------------
/snippets/fields/email.php:
--------------------------------------------------------------------------------
1 | $block,
16 | 'form' => $form,
17 | 'field' => $field,
18 | 'attr' => A::merge($attr, $attr['input'] ?? []),
19 | 'type' => 'email'
20 | ]);
21 |
--------------------------------------------------------------------------------
/snippets/fields/file-upload.php:
--------------------------------------------------------------------------------
1 | $block,
14 | 'form' => $form,
15 | 'field' => $field,
16 | 'attr' => $attr,
17 | 'type' => 'file',
18 | 'input' => [
19 | 'name' => $block->key() . ($block->allowMultiple()->toBool() ? '[]' : ''),
20 | $block->allowMultiple()->toBool() ? 'multiple' : '',
21 | ]
22 | ]);
23 |
--------------------------------------------------------------------------------
/snippets/fields/hidden.php:
--------------------------------------------------------------------------------
1 |
15 |
16 | 'hidden',
18 | 'id' => $form->elementId($block->id()),
19 | 'name' => $block->key(),
20 | 'value' => $form->valueFor($block->key())
21 | ])) ?>>
--------------------------------------------------------------------------------
/snippets/fields/number.php:
--------------------------------------------------------------------------------
1 | $block,
14 | 'field' => $field,
15 | 'form' => $form,
16 | 'attr' => $attr,
17 | 'type' => 'number',
18 | 'input' => [
19 | 'step' => $block->step()->or(1)?->value(),
20 | 'min' => $block->min()->or(null)?->value(),
21 | 'max' => $block->max()->or(null)?->value(),
22 | ]
23 | ]);
24 |
--------------------------------------------------------------------------------
/snippets/fields/pages.php:
--------------------------------------------------------------------------------
1 | $block,
14 | 'field' => $field,
15 | 'form' => $form,
16 | 'attr' => $attr
17 | ]);
18 |
--------------------------------------------------------------------------------
/snippets/fields/partials/error.php:
--------------------------------------------------------------------------------
1 | errorFor($block->key(), $form) ?>
15 |
16 | $block->key(),
20 | 'id' => $form->elementId("{$block->id()}/error"),
21 | 'role' => 'alert',
22 | 'aria-atomic' => true
23 | ]
24 | )) ?>>= $error ?>
--------------------------------------------------------------------------------
/snippets/fields/partials/label.php:
--------------------------------------------------------------------------------
1 |
15 |
16 | $form->elementId($block->id())])) ?>>
17 | = $block->label()->escape() ?>
18 | required()->toBool() || $block->min()->toInt() >= 1) : ?>
19 | *
20 |
21 |
22 |
--------------------------------------------------------------------------------
/snippets/fields/partials/wrapper.php:
--------------------------------------------------------------------------------
1 |
17 |
18 | Htmx::isActive() && DreamForm::option('precognition') ? 'this' : null,
20 | 'data-has-error' => !!$submission?->errorFor($block->key(), $form)
21 | ])) ?>>
22 | = $slot ?>
23 |
--------------------------------------------------------------------------------
/snippets/fields/radio.php:
--------------------------------------------------------------------------------
1 | $block,
14 | 'field' => $field,
15 | 'form' => $form,
16 | 'attr' => $attr,
17 | 'type' => 'radio'
18 | ]);
19 |
--------------------------------------------------------------------------------
/snippets/fields/select.php:
--------------------------------------------------------------------------------
1 |
18 |
19 | $form->elementId($block->id()),
23 | 'name' => $block->key(),
24 | 'required' => $required ?? null,
25 | 'aria-invalid' => ($error = $submission?->errorFor($block->key(), $form)) ? true : null,
26 | 'aria-describedby' => $error ? $form->elementId("{$block->id()}/error") : null,
27 | ],
28 | $field->htmxAttr($form)
29 | )) ?>>
30 | !($selected = $form->valueFor($block->key())?->value()),
32 | "value" => true,
33 | "disabled" => true,
34 | "hidden" => true
35 | ]) ?>>= $block->placeholder()->escape() ?>
36 | options() as $value => $label) : ?>
37 | $value,
39 | "selected" => $selected === $value
40 | ]) ?>>
41 | = Escape::html($label) ?>
42 |
43 |
44 |
45 |
46 |
48 |
--------------------------------------------------------------------------------
/snippets/fields/text.php:
--------------------------------------------------------------------------------
1 |
22 |
23 | $type,
27 | 'id' => $form->elementId($block->id()),
28 | 'name' => $block->key(),
29 | 'placeholder' => $type !== 'file' ? $block->placeholder()->or(" ") : null,
30 | 'required' => $block->required()->toBool() ?? null,
31 | 'value' => $type !== 'file' ? $form->valueFor($block->key()) : null,
32 | 'aria-invalid' => ($error = $submission?->errorFor($block->key(), $form)) ? true : null,
33 | 'aria-describedby' => $error ? $form->elementId("{$block->id()}/error") : null,
34 | ],
35 | $type !== 'file' ? $field->htmxAttr($form) : [],
36 | $input ?? []
37 | )) ?>>
38 |
39 |
41 |
--------------------------------------------------------------------------------
/snippets/fields/textarea.php:
--------------------------------------------------------------------------------
1 |
17 |
18 |
30 |
31 |
33 |
--------------------------------------------------------------------------------
/snippets/form.php:
--------------------------------------------------------------------------------
1 | [],
17 | 'row' => [],
18 | 'column' => [],
19 | 'field' => [],
20 | 'label' => [],
21 | 'error' => [],
22 | 'input' => [],
23 | 'button' => [],
24 | 'success' => [],
25 | 'inactive' => [],
26 |
27 | // field-specific attributes
28 | 'textarea' => [
29 | 'field' => [],
30 | 'label' => [],
31 | 'error' => [],
32 | 'input' => [],
33 | ],
34 | 'text' => [
35 | 'field' => [],
36 | 'label' => [],
37 | 'error' => [],
38 | 'input' => [],
39 | ],
40 | 'select' => [
41 | 'field' => [],
42 | 'label' => [],
43 | 'error' => [],
44 | 'input' => [],
45 | ],
46 | 'number' => [
47 | 'field' => [],
48 | 'label' => [],
49 | 'error' => [],
50 | 'input' => [],
51 | ],
52 | 'file' => [
53 | 'field' => [],
54 | 'label' => [],
55 | 'error' => [],
56 | 'input' => [],
57 | ],
58 | 'email' => [
59 | 'field' => [],
60 | 'label' => [],
61 | 'error' => [],
62 | 'input' => [],
63 | ],
64 | 'radio' => [
65 | 'field' => [],
66 | 'label' => [],
67 | 'error' => [],
68 | 'input' => [],
69 | 'row' => []
70 | ],
71 | 'checkbox' => [
72 | 'field' => [],
73 | 'label' => [],
74 | 'error' => [],
75 | 'input' => [],
76 | 'row' => []
77 | ],
78 | 'hidden' => [
79 | 'input' => []
80 | ],
81 | ], $attr ?? []);
82 |
83 | // don't show the form if it's a draft
84 | if (!$form || $form->status() === 'draft') {
85 | snippet('dreamform/inactive', ['form' => $form, 'attr' => $attr]);
86 | return;
87 | }
88 |
89 | if ($submission?->isFinished() && $submission->form()->is($form)) {
90 | snippet('dreamform/success', ['form' => $form, 'attr' => $attr]);
91 | return;
92 | } ?>
93 |
94 |
132 |
--------------------------------------------------------------------------------
/snippets/guards.php:
--------------------------------------------------------------------------------
1 | guards() as $guard) {
12 | if ($guard::hasSnippet()) {
13 | snippet(
14 | "dreamform/guards/{$guard->type()}",
15 | compact('form', 'attr', 'guard')
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/snippets/guards/csrf.php:
--------------------------------------------------------------------------------
1 |
7 |
8 |
--------------------------------------------------------------------------------
/snippets/guards/hcaptcha.php:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
42 | 'h-captcha',
48 | 'data-sitekey' => $guard::siteKey(),
49 | 'data-size' => DreamForm::option('guards.hcaptcha.size', 'normal')
50 | ];
51 |
52 | // Only add data-theme for non-custom themes
53 | if (!$isCustomTheme) {
54 | $attrs['data-theme'] = $theme;
55 | } ?>
56 |
57 | >
58 |
59 |
60 |
61 |
73 |
--------------------------------------------------------------------------------
/snippets/guards/honeypot.php:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/snippets/guards/turnstile.php:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
25 |
27 |
28 | 'cf-turnstile',
30 | 'data-theme' => DreamForm::option('guards.turnstile.theme', 'auto'),
31 | 'data-sitekey' => $guard::siteKey()
32 | ]) ?>>
33 |
--------------------------------------------------------------------------------
/snippets/inactive.php:
--------------------------------------------------------------------------------
1 |
11 |
12 | >
13 | = t('dreamform.form.inactiveMessage.default') ?>
14 |
15 |
--------------------------------------------------------------------------------
/snippets/session.php:
--------------------------------------------------------------------------------
1 |
14 | 'hidden',
16 | 'id' => $id = $form->uuid()->id() . '-session',
17 | 'name' => 'dreamform:session',
18 | 'value' => $submission ? Htmx::encrypt(($submission->exists() ? "page://" : "") . $submission->slug()) : null,
19 | 'hx-swap-oob' => isset($swap) && $swap ? "outerHTML:#{$id}" : null
20 | ]) ?>>
21 |
22 |
--------------------------------------------------------------------------------
/snippets/success.php:
--------------------------------------------------------------------------------
1 |
14 |
15 | $form->elementId()])) ?>>
16 | = $form->successMessage()->or(t('dreamform.form.successMessage.default')) ?>
17 |
18 |
--------------------------------------------------------------------------------
/src/components/DynamicFieldPreview.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
33 |
34 |
35 | {{ currentField.label }}
36 |
37 |
38 |
39 | {{ value.value }}
40 |
41 |
42 |
43 |
44 |
57 |
--------------------------------------------------------------------------------
/src/components/Editable.vue:
--------------------------------------------------------------------------------
1 |
134 |
135 |
136 |
137 |
146 |
147 | {{ placeholder }}
148 |
149 |
150 |
151 |
172 |
--------------------------------------------------------------------------------
/src/components/FieldError.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 | {{ $t("dreamform.common.errorMessage.label") }}:
14 |
20 |
21 |
22 |
23 |
40 |
--------------------------------------------------------------------------------
/src/components/FieldHeader.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
56 |
57 |
58 |
108 |
--------------------------------------------------------------------------------
/src/components/FieldInput.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
22 |
23 |
24 |
25 |
26 |
61 |
--------------------------------------------------------------------------------
/src/components/log/EmailEntry.vue:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
48 |
54 |
59 |
60 | {{ $t(`dreamform.actions.email.log.${key}`) }}
61 |
62 | {{
63 | value
64 | }}
65 |
66 |
67 |
68 |
78 | {{
79 | $t(`dreamform.actions.email.log.${isExpanded ? "collapse" : "expand"}`)
80 | }}
81 |
82 |
83 |
84 |
85 |
181 |
--------------------------------------------------------------------------------
/src/components/log/EntryBase.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | • {{ date }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
84 |
--------------------------------------------------------------------------------
/src/components/log/ErrorEntry.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/log/InfoEntry.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
16 |
17 |
18 |
29 |
--------------------------------------------------------------------------------
/src/core/Layout.vue:
--------------------------------------------------------------------------------
1 | This is the layout component extended to accomodate multi-step form layouts.
2 | Keep in mind that this component is heavily affected by any Core changes if they
3 | happen and so it's important to keep an eye on the core changes and update this
4 | component accordingly.
5 | https://github.com/getkirby/kirby/blob/main/panel/src/components/Forms/Layouts/Layout.vue
6 | This also means that the plugin does not work if any other plugin modifies the
7 | layout component as well. We can solve this by creating an entire custom field
8 | similar to https://github.com/tobimori/kirby-icon-field.
9 |
10 |
11 |
20 |
21 |
38 |
39 |
40 |
41 | {{ $t("dreamform.form.nextPage") }}
42 |
43 |
44 |
51 |
52 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
78 |
79 |
127 |
--------------------------------------------------------------------------------
/src/core/LayoutSelector.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 | {{ label }}
10 |
15 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 | {{ $t("dreamform.form.newPage") }}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
49 |
50 |
64 |
--------------------------------------------------------------------------------
/src/fields/ApiObject.vue:
--------------------------------------------------------------------------------
1 |
71 |
72 |
73 |
78 |
79 |
--------------------------------------------------------------------------------
/src/fields/DynamicField.vue:
--------------------------------------------------------------------------------
1 |
89 |
90 |
91 |
92 |
93 |
162 |
163 |
164 |
165 |
166 |
191 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | .df-field {
2 | padding: var(--spacing-3) var(--spacing-4);
3 | height: 100%;
4 | }
5 |
6 | .k-header-buttons {
7 | .k-panel[data-template='submission'] &,
8 | .k-panel[data-template='forms'] & {
9 | display: none;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/previews/ButtonField.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
19 |
20 |
21 |
22 |
32 |
--------------------------------------------------------------------------------
/src/previews/ChoicesField.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
31 |
43 |
44 |
45 |
46 |
47 |
84 |
--------------------------------------------------------------------------------
/src/previews/FileUploadField.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
25 |
26 |
27 | {{ $t("toolbar.button.file.upload") }}
28 |
29 |
38 |
39 |
40 |
41 |
57 |
--------------------------------------------------------------------------------
/src/previews/HiddenField.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
27 |
28 |
29 |
54 |
--------------------------------------------------------------------------------
/src/previews/SelectField.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
25 |
26 |
31 |
32 |
33 |
34 |
35 |
87 |
--------------------------------------------------------------------------------
/src/previews/TextField.vue:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
80 |
--------------------------------------------------------------------------------
/src/sections/License.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 |
55 |
56 | {{ $t("dreamform.license.buy") }}
57 |
58 |
65 | {{ $t("dreamform.license.activate") }}
66 |
67 |
68 |
69 |
70 |
113 |
--------------------------------------------------------------------------------
/src/sections/Submission.vue:
--------------------------------------------------------------------------------
1 |
64 |
65 |
66 |
67 |
74 | {{ $t("dreamform.submission.runActions.button") }}
75 |
76 |
77 |
78 | {{ $t("dreamform.submission.markedAs").split("…")[0] }}
79 |
83 |
84 | {{ $t("dreamform.submission." + (isSpam ? "spam" : "ham")) }}
85 |
86 | {{ $t("dreamform.submission.markedAs").split("…")[1] }}
87 |
88 |
89 |
90 |
91 | {{ $t("dreamform.submission.partial") }}
92 |
93 |
94 |
95 |
96 |
104 | {{
105 | $t(
106 | isSpam
107 | ? "dreamform.submission.reportAsHam.button"
108 | : "dreamform.submission.reportAsSpam.button"
109 | )
110 | }}
111 |
112 |
113 |
114 |
115 |
123 |
128 |
129 |
136 |
137 |
138 |
139 |
140 |
141 |
194 |
--------------------------------------------------------------------------------
/src/utils/block.js:
--------------------------------------------------------------------------------
1 | import { disabled, id, section } from "kirbyuse/props"
2 |
3 | export const props = {
4 | fieldset: section.fieldset,
5 | ...disabled,
6 | ...id,
7 | endpoints: {
8 | default: () => ({}),
9 | type: [Array, Object]
10 | },
11 | content: Object
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/date.js:
--------------------------------------------------------------------------------
1 | export function formatDate(timestamp) {
2 | const locale = window.panel.user.language
3 | const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" })
4 |
5 | const diff = (Date.now() - timestamp * 1000) / 1000
6 | const units = [
7 | { unit: "year", seconds: 365 * 24 * 60 * 60 },
8 | { unit: "month", seconds: 30 * 24 * 60 * 60 },
9 | { unit: "day", seconds: 24 * 60 * 60 },
10 | { unit: "hour", seconds: 60 * 60 },
11 | { unit: "minute", seconds: 60 }
12 | ]
13 |
14 | for (const { unit, seconds } of units) {
15 | const value = Math.floor(diff / seconds)
16 | if (value > 0) {
17 | return rtf.format(0 - value, unit)
18 | }
19 | }
20 |
21 | return window.panel.$t("dreamform.justNow")
22 | }
23 |
--------------------------------------------------------------------------------
/templates/emails/dreamform.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 | .shadow { box-shadow: 0 1px 3px 0 #0000000d, 0 1px 2px 0 #00000006; }
9 |
10 |
11 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | = $form->site()->title() ?>
26 |
27 |
28 | = tt('dreamform.actions.email.defaultTemplate.text', null, ['form' =>
29 | "{$form->title()} "]) ?>
30 |
31 |
32 |
33 |
39 |
40 |
41 | fields()->filterBy(fn ($f) => $f::hasValue() && $f::type() !== 'file-upload') as $field) :
42 | $value = $submission->valueFor($field->key())?->escape();
43 | if(str_starts_with($value ?? "", 'page://')) {
44 | $page = \Kirby\Cms\App::instance()->site()->find($value);
45 | if($page) {
46 | $value = $page->title();
47 | }
48 | }
49 | ?>
50 |
51 | = $field->label() ?>
54 | = $value ?? "—"
56 | ?>
58 | last() !== $field) : ?>
59 |
64 |
65 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/templates/emails/dreamform.php:
--------------------------------------------------------------------------------
1 | = tt('dreamform.actions.email.defaultTemplate.text', null, ['form' => $form->title()]) ?>
2 |
3 |
4 | ———
5 |
6 | fields()->filterBy(fn ($f) => $f::hasValue() && $f::type() !== 'file-upload') as $field) :
7 | $value = $submission->valueFor($field->key())?->escape();
8 | if (str_starts_with($value ?? "", 'page://')) {
9 | $page = \Kirby\Cms\App::instance()->site()->find($value);
10 | if ($page) {
11 | $value = $page->title();
12 | }
13 | }
14 | ?>
15 | = $field->label() ?>:
16 | = $value ?? "—" ?>
17 |
18 | last() !== $field) : ?>
19 |
20 | ———
21 |
22 |
24 |
--------------------------------------------------------------------------------
/vendor/autoload.php:
--------------------------------------------------------------------------------
1 | array($baseDir . '/classes'),
10 | 'Kirby\\' => array($vendorDir . '/getkirby/composer-installer/src'),
11 | );
12 |
--------------------------------------------------------------------------------
/vendor/composer/autoload_real.php:
--------------------------------------------------------------------------------
1 | register(true);
35 |
36 | return $loader;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/vendor/composer/installed.php:
--------------------------------------------------------------------------------
1 | array(
5 | 'name' => 'tobimori/kirby-dreamform',
6 | 'pretty_version' => '2.0.0-rc.3',
7 | 'version' => '2.0.0.0-RC3',
8 | 'reference' => null,
9 | 'type' => 'kirby-plugin',
10 | 'install_path' => __DIR__ . '/../../',
11 | 'aliases' => array(),
12 | 'dev' => false,
13 | ),
14 | 'versions' => array(
15 | 'getkirby/composer-installer' => array(
16 | 'pretty_version' => '1.2.1',
17 | 'version' => '1.2.1.0',
18 | 'reference' => 'c98ece30bfba45be7ce457e1102d1b169d922f3d',
19 | 'type' => 'composer-plugin',
20 | 'install_path' => __DIR__ . '/../getkirby/composer-installer',
21 | 'aliases' => array(),
22 | 'dev_requirement' => false,
23 | ),
24 | 'tobimori/kirby-dreamform' => array(
25 | 'pretty_version' => '2.0.0-rc.3',
26 | 'version' => '2.0.0.0-RC3',
27 | 'reference' => null,
28 | 'type' => 'kirby-plugin',
29 | 'install_path' => __DIR__ . '/../../',
30 | 'aliases' => array(),
31 | 'dev_requirement' => false,
32 | ),
33 | ),
34 | );
35 |
--------------------------------------------------------------------------------
/vendor/composer/platform_check.php:
--------------------------------------------------------------------------------
1 | = 80300)) {
8 | $issues[] = 'Your Composer dependencies require a PHP version ">= 8.3.0". You are running ' . PHP_VERSION . '.';
9 | }
10 |
11 | if ($issues) {
12 | if (!headers_sent()) {
13 | header('HTTP/1.1 500 Internal Server Error');
14 | }
15 | if (!ini_get('display_errors')) {
16 | if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
17 | fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
18 | } elseif (!headers_sent()) {
19 | echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
20 | }
21 | }
22 | trigger_error(
23 | 'Composer detected issues in your platform: ' . implode(' ', $issues),
24 | E_USER_ERROR
25 | );
26 | }
27 |
--------------------------------------------------------------------------------