trek 2.5.0 → 2.6.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.
- checksums.yaml +4 -4
- data/.node-version +1 -1
- data/AGENTS.md +7 -4
- data/CHANGELOG.md +37 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +16 -3
- data/Makefile +2 -0
- data/app/components/trek/dashboard/tile_component/tile_component.css +9 -0
- data/app/components/trek/dashboard/tile_component/tile_component.html.slim +4 -2
- data/app/components/trek/dashboard/tile_component.rb +12 -2
- data/app/components/trek/form/content_editor_component/content_editor_component.css +5 -0
- data/app/components/trek/form/content_editor_component/content_editor_component.html.slim +19 -93
- data/app/components/trek/form/content_editor_component/content_editor_component.js +94 -251
- data/app/components/trek/form/content_editor_component/extensions/block_view.js +40 -0
- data/app/components/trek/form/content_editor_component/extensions/image_block.js +28 -26
- data/app/components/trek/form/content_editor_component/extensions/prompts_block.js +15 -26
- data/app/components/trek/form/content_editor_component/extensions/table_of_contents.js +1 -3
- data/app/components/trek/form/content_editor_component.rb +15 -0
- data/app/components/trek/form/image_field_component/image_field_component.css +40 -1
- data/app/components/trek/form/image_field_component/image_field_component.html.slim +0 -9
- data/app/components/trek/form/image_field_component.rb +36 -9
- data/app/content/trek/content/cleaner.rb +53 -0
- data/app/content/trek/content/document.rb +2 -2
- data/app/content/trek/content/prompts_block.rb +1 -2
- data/app/content/trek/content.rb +6 -0
- data/app/controllers/concerns/trek/reorderable.rb +24 -6
- data/app/controllers/trek/csp_reports_controller.rb +14 -1
- data/app/controllers/trek/panels/prompts_controller.rb +1 -1
- data/app/controllers/trek/resource_controller.rb +5 -5
- data/app/jobs/trek/purge_expired_sessions_job.rb +4 -3
- data/app/models/concerns/trek/contentable.rb +15 -1
- data/app/models/concerns/trek/users/sign_in_code.rb +70 -0
- data/app/models/concerns/trek/users/sign_in_lockout.rb +4 -2
- data/app/views/trek/panels/links/_form.html.slim +2 -4
- data/app/views/trek/panels/prompts/create.turbo_stream.slim +0 -1
- data/config/locales/trek.en.yml +26 -11
- data/config/locales/trek.fr.yml +26 -11
- data/docs/.vitepress/config.mjs +4 -1
- data/docs/background-jobs.md +69 -0
- data/docs/content-editor.md +219 -0
- data/docs/customize.md +2 -0
- data/docs/deploy.md +2 -2
- data/docs/getting-started.md +1 -1
- data/docs/index.md +1 -1
- data/docs/reference/components.md +2 -2
- data/docs/reference/concerns/contentable.md +10 -4
- data/docs/reference/concerns/formattable.md +1 -1
- data/docs/reference/environment-variables.md +1 -0
- data/docs/reference/formatters.md +2 -30
- data/docs/reference/forms.md +1 -1
- data/docs/reference/generators/audit-log.md +2 -2
- data/docs/reference/generators/install.md +3 -2
- data/docs/reference/generators/public-accounts.md +141 -0
- data/docs/reference/generators/security-headers.md +1 -1
- data/docs/reference/index.md +2 -1
- data/docs/reference/models/user.md +3 -1
- data/docs/what-is-trek.md +1 -1
- data/lib/generators/trek/audit_log_generator.rb +54 -0
- data/lib/generators/trek/install/admin/users_generator.rb +6 -1
- data/lib/generators/trek/install/audit_log_generator.rb +9 -45
- data/lib/generators/trek/install/good_job_generator.rb +161 -0
- data/lib/generators/trek/install/model_specs_generator.rb +3 -3
- data/lib/generators/trek/install/ruby_generator.rb +1 -1
- data/lib/generators/trek/install/scalingo_generator.rb +7 -45
- data/lib/generators/trek/install_generator.rb +15 -0
- data/lib/generators/trek/public_accounts_generator.rb +267 -0
- data/lib/generators/trek/scalingo_generator.rb +55 -0
- data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
- data/lib/generators/trek/templates/controllers/admin/password_renewals_controller.rb +11 -0
- data/lib/generators/trek/templates/controllers/admin/user_sessions_controller.rb +1 -1
- data/lib/generators/trek/templates/controllers/admin/users_controller.rb +0 -8
- data/lib/generators/trek/templates/initializers/content_security_policy.rb +3 -2
- data/lib/generators/trek/templates/initializers/good_job.rb.tt +38 -0
- data/lib/generators/trek/templates/jobs/mail_delivery_job.rb +6 -0
- data/lib/generators/trek/templates/layouts/admin.html.slim +5 -0
- data/lib/generators/trek/templates/mailers/admin_mailer.rb +4 -2
- data/lib/generators/trek/templates/package.json.tt +1 -1
- data/lib/generators/trek/templates/public_accounts/api/authentication.rb.tt +76 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/accounts_controller.rb.tt +22 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/active_sessions_controller.rb.tt +21 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/base_controller.rb.tt +71 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/email_changes_controller.rb.tt +55 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/email_confirmations_controller.rb.tt +19 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/password_resets_controller.rb.tt +28 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/passwords_controller.rb.tt +17 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/registrations_controller.rb.tt +17 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/sessions_controller.rb.tt +45 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/sign_in_codes_controller.rb.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/jobs/sign_in_code_job.rb.tt +13 -0
- data/lib/generators/trek/templates/public_accounts/locales/accounts.en.yml.tt +205 -0
- data/lib/generators/trek/templates/public_accounts/locales/accounts.fr.yml.tt +205 -0
- data/lib/generators/trek/templates/public_accounts/mailers/account_mailer.rb.tt +85 -0
- data/lib/generators/trek/templates/public_accounts/migrations/create_accounts.rb.tt +46 -0
- data/lib/generators/trek/templates/public_accounts/models/account/session.rb.tt +47 -0
- data/lib/generators/trek/templates/public_accounts/models/account/sign_in_code.rb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/models/account.rb.tt +73 -0
- data/lib/generators/trek/templates/public_accounts/spec/api/accounts_spec.rb.tt +90 -0
- data/lib/generators/trek/templates/public_accounts/spec/api/sign_ins_spec.rb.tt +158 -0
- data/lib/generators/trek/templates/public_accounts/spec/models/account/sign_in_code_spec.rb.tt +56 -0
- data/lib/generators/trek/templates/public_accounts/spec/models/account_spec.rb.tt +109 -0
- data/lib/generators/trek/templates/public_accounts/spec/web/accounts_spec.rb.tt +126 -0
- data/lib/generators/trek/templates/public_accounts/spec/web/authentication_spec.rb.tt +43 -0
- data/lib/generators/trek/templates/public_accounts/spec/web/sign_ins_spec.rb.tt +215 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.en.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.en.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.fr.html.slim.tt +13 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.fr.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.en.html.slim.tt +9 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.en.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.fr.html.slim.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.fr.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.en.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.en.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.fr.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.fr.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.en.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.en.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.fr.html.slim.tt +13 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.fr.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.en.html.slim.tt +9 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.en.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.fr.html.slim.tt +9 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.fr.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.en.html.slim.tt +23 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.en.text.erb.tt +14 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.fr.html.slim.tt +24 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.fr.text.erb.tt +14 -0
- data/lib/generators/trek/templates/public_accounts/web/authentication.rb.tt +97 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/accounts_controller.rb.tt +33 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/active_sessions_controller.rb.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/base_controller.rb.tt +52 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/email_changes_controller.rb.tt +85 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/email_confirmations_controller.rb.tt +19 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/password_resets_controller.rb.tt +41 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/passwords_controller.rb.tt +23 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/registrations_controller.rb.tt +24 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/sessions_controller.rb.tt +78 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/sign_in_codes_controller.rb.tt +27 -0
- data/lib/generators/trek/templates/public_accounts/web/views/_flash.html.slim.tt +5 -0
- data/lib/generators/trek/templates/public_accounts/web/views/accounts/show.html.slim.tt +52 -0
- data/lib/generators/trek/templates/public_accounts/web/views/email_changes/edit.html.slim.tt +17 -0
- data/lib/generators/trek/templates/public_accounts/web/views/email_changes/new.html.slim.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/web/views/password_resets/edit.html.slim.tt +26 -0
- data/lib/generators/trek/templates/public_accounts/web/views/password_resets/new.html.slim.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/web/views/passwords/edit.html.slim.tt +25 -0
- data/lib/generators/trek/templates/public_accounts/web/views/registrations/new.html.slim.tt +28 -0
- data/lib/generators/trek/templates/public_accounts/web/views/sessions/new.html.slim.tt +38 -0
- data/lib/generators/trek/templates/public_accounts/web/views/sign_in_codes/new.html.slim.tt +17 -0
- data/lib/generators/trek/templates/spec/jobs/mail_delivery_job_spec.rb +11 -0
- data/lib/generators/trek/templates/spec/mailers/admin_mailer_spec.rb +10 -0
- data/lib/generators/trek/templates/spec/requests/admin/fragments_spec.rb +1 -1
- data/lib/generators/trek/templates/spec/requests/admin/good_job_spec.rb +34 -0
- data/lib/generators/trek/templates/spec/requests/admin/pages_spec.rb +1 -1
- data/lib/generators/trek/templates/spec/requests/admin/panels_spec.rb +16 -0
- data/lib/generators/trek/templates/spec/requests/admin/password_renewal_spec.rb +2 -2
- data/lib/generators/trek/templates/spec/requests/admin/sign_in_lockout_spec.rb +1 -1
- data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +2 -2
- data/lib/generators/trek/templates/views/admin/users/index.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.text.erb +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.text.erb +1 -1
- data/lib/trek/csp_reports.rb +22 -4
- data/lib/trek/typography_formatter.rb +35 -4
- data/lib/trek/version.rb +1 -1
- data/package.json +12 -8
- data/trek.gemspec +1 -1
- data/vitest.config.mjs +24 -0
- data/yarn.lock +1004 -288
- metadata +90 -3
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import "./content_editor_component.css";
|
|
2
2
|
import { Editor } from "@tiptap/core";
|
|
3
3
|
import FloatingMenu from "@tiptap/extension-floating-menu";
|
|
4
|
-
import Link from "@tiptap/extension-link";
|
|
5
|
-
// import Placeholder from '@tiptap/extension-placeholder';
|
|
6
4
|
import StarterKit from "@tiptap/starter-kit";
|
|
7
5
|
import { Controller as BaseController } from "@hotwired/stimulus";
|
|
8
6
|
import { TableOfContents } from "./extensions/table_of_contents.js";
|
|
9
7
|
import PromptsBlock from "./extensions/prompts_block.js";
|
|
10
8
|
import ImageBlock from "./extensions/image_block.js";
|
|
11
9
|
|
|
10
|
+
// The content editor (Trek::Form::ContentEditorComponent): Tiptap, its toolbar, and the panels inserting
|
|
11
|
+
// links, images and calls to action. The document is kept in the form's field as JSON.
|
|
12
12
|
export class Controller extends BaseController {
|
|
13
13
|
static classes = ["active", "empty"];
|
|
14
14
|
static targets = [
|
|
15
15
|
"editorContent",
|
|
16
16
|
"editorJson",
|
|
17
17
|
"floatingMenu",
|
|
18
|
+
"stateButton",
|
|
18
19
|
"hiddenPromptsButton",
|
|
19
20
|
"hiddenImageButton",
|
|
20
21
|
"hiddenLinkButton",
|
|
@@ -34,43 +35,50 @@ export class Controller extends BaseController {
|
|
|
34
35
|
heading: {
|
|
35
36
|
levels: [2, 3, 4],
|
|
36
37
|
},
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
target: null,
|
|
38
|
+
link: {
|
|
39
|
+
openOnClick: false,
|
|
40
|
+
// Internal links are stored as GlobalIDs (resolved to URLs at
|
|
41
|
+
// render time): without this, setLink refuses the gid:// scheme
|
|
42
|
+
// and silently does nothing. A check rather than the `protocols`
|
|
43
|
+
// option, which registers the scheme with linkifyjs, and warns
|
|
44
|
+
// from the second editor on ("linkifyjs: already initialized").
|
|
45
|
+
isAllowedUri: (url, { defaultValidate }) =>
|
|
46
|
+
url.startsWith("gid://") || defaultValidate(url),
|
|
47
|
+
HTMLAttributes: {
|
|
48
|
+
"data-action": `${this.identifier}#selectLink`,
|
|
49
|
+
target: null,
|
|
50
|
+
},
|
|
51
51
|
},
|
|
52
|
+
// Not in Trek's editor before Tiptap 3: no underline (nor its
|
|
53
|
+
// Ctrl+U shortcut), and no empty paragraph added after a final
|
|
54
|
+
// block, which would change every saved document
|
|
55
|
+
underline: false,
|
|
56
|
+
trailingNode: false,
|
|
52
57
|
}),
|
|
53
58
|
TableOfContents,
|
|
54
59
|
this.hasFloatingMenuTarget
|
|
55
|
-
? FloatingMenu.configure({
|
|
56
|
-
element: this.floatingMenuTarget,
|
|
57
|
-
})
|
|
60
|
+
? FloatingMenu.configure({ element: this.floatingMenuTarget })
|
|
58
61
|
: null,
|
|
59
62
|
ImageBlock,
|
|
60
63
|
PromptsBlock,
|
|
61
|
-
],
|
|
64
|
+
].filter(Boolean),
|
|
62
65
|
content: this.contentJson,
|
|
63
66
|
autofocus: this.autofocusValue ? "end" : false,
|
|
67
|
+
onUpdate: () => this.updateContent(),
|
|
68
|
+
// Every change of the document or the selection: the buttons show the
|
|
69
|
+
// styles and the block where the cursor is
|
|
70
|
+
onTransaction: () => this.updateButtons(),
|
|
64
71
|
});
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
// Tiptap 3 moves the floating menu next to the editor when it shows it,
|
|
74
|
+
// and out of the page when it hides it, but leaves it where the template
|
|
75
|
+
// puts it until then: out of the page from the start, as Tiptap 2 did
|
|
76
|
+
if (this.hasFloatingMenuTarget) this.floatingMenuTarget.remove();
|
|
68
77
|
this.updateContent();
|
|
78
|
+
this.updateButtons();
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
disconnect() {
|
|
72
|
-
this.editor.off("update", () => this.updateContent());
|
|
73
|
-
this.editor.off("selectionUpdate", () => this.updateSelection());
|
|
74
82
|
this.editor.destroy();
|
|
75
83
|
this.editor = null;
|
|
76
84
|
}
|
|
@@ -79,264 +87,99 @@ export class Controller extends BaseController {
|
|
|
79
87
|
this.editor.chain().focus().run();
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
// A toolbar button's command, from its params (see the component's `command_data`)
|
|
91
|
+
run({ params: { command, level } }) {
|
|
92
|
+
this.editor
|
|
93
|
+
.chain()
|
|
94
|
+
.focus()
|
|
95
|
+
[command](level ? { level } : undefined)
|
|
96
|
+
.run();
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
updateContent() {
|
|
83
100
|
this.editorJsonTarget.value = JSON.stringify(this.editor.getJSON());
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{ key: "h2", command: "heading", params: { level: 2 } },
|
|
96
|
-
{ key: "h3", command: "heading", params: { level: 3 } },
|
|
97
|
-
{ key: "h4", command: "heading", params: { level: 4 } },
|
|
98
|
-
{ key: "h5", command: "heading", params: { level: 5 } },
|
|
99
|
-
{ key: "h6", command: "heading", params: { level: 6 } },
|
|
100
|
-
{ key: "bulletList" },
|
|
101
|
-
{ key: "orderedList" },
|
|
102
|
-
{ key: "codeBlock" },
|
|
103
|
-
{ key: "blockquote" },
|
|
104
|
-
];
|
|
105
|
-
|
|
106
|
-
buttons.forEach((b) => {
|
|
107
|
-
const button = this.element.querySelector("button." + b.key);
|
|
108
|
-
|
|
109
|
-
if (!button) return;
|
|
110
|
-
|
|
111
|
-
const active = this.editor.isActive(b.command || b.key, b.params || {});
|
|
112
|
-
|
|
103
|
+
updateButtons() {
|
|
104
|
+
this.stateButtonTargets.forEach((button) => {
|
|
105
|
+
const level = Number(
|
|
106
|
+
button.getAttribute(`data-${this.identifier}-level-param`)
|
|
107
|
+
);
|
|
108
|
+
const active = this.editor.isActive(
|
|
109
|
+
button.dataset.stateName,
|
|
110
|
+
level ? { level } : {}
|
|
111
|
+
);
|
|
113
112
|
button.classList.toggle(this.activeClass, active);
|
|
114
113
|
});
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
this.editor.chain().focus().toggleBold().run();
|
|
119
|
-
e.target.classList.toggle(this.activeClass, this.editor.isActive("bold"));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
toggleItalic(e) {
|
|
123
|
-
this.editor.chain().focus().toggleItalic().run();
|
|
124
|
-
e.target.classList.toggle(this.activeClass, this.editor.isActive("italic"));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
toggleStrike(e) {
|
|
128
|
-
this.editor.chain().focus().toggleStrike().run();
|
|
129
|
-
e.target.classList.toggle(this.activeClass, this.editor.isActive("strike"));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
toggleCode(e) {
|
|
133
|
-
this.editor.chain().focus().toggleCode().run();
|
|
134
|
-
e.target.classList.toggle(this.activeClass, this.editor.isActive("code"));
|
|
135
|
-
}
|
|
116
|
+
// Links
|
|
136
117
|
|
|
137
118
|
selectLink() {
|
|
138
119
|
this.editor.commands.extendMarkRange("link");
|
|
139
120
|
}
|
|
140
121
|
|
|
141
|
-
toggleLink(
|
|
122
|
+
toggleLink() {
|
|
142
123
|
this.selectLink();
|
|
143
|
-
e.target.classList.toggle(this.activeClass, this.editor.isActive("link"));
|
|
144
|
-
|
|
145
|
-
const attributes = this.editor.getAttributes("link");
|
|
146
|
-
attributes.text = this.currentlySelectedText();
|
|
147
|
-
|
|
148
|
-
const url = this.hiddenLinkButtonTarget.dataset.basePath;
|
|
149
|
-
this.hiddenLinkButtonTarget.href = url + "&" + this.linkParams(attributes);
|
|
150
|
-
this.hiddenLinkButtonTarget.click();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
linkParams(attributes) {
|
|
154
|
-
const linkAttributes = Object.fromEntries(
|
|
155
|
-
Object.entries(attributes).map(([k, v]) => [`link[${k}]`, v])
|
|
156
|
-
);
|
|
157
|
-
return new URLSearchParams(linkAttributes).toString();
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
currentlySelectedText() {
|
|
161
|
-
const state = this.editor.state;
|
|
162
|
-
const text = state.doc.textBetween(
|
|
163
|
-
state.selection.from,
|
|
164
|
-
state.selection.to
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
return text;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
linkInserterTargetConnected(t) {
|
|
171
|
-
const params = JSON.parse(t.dataset.params);
|
|
172
|
-
|
|
173
|
-
this.editor
|
|
174
|
-
.chain()
|
|
175
|
-
.focus()
|
|
176
|
-
.setLink({
|
|
177
|
-
href: params.href,
|
|
178
|
-
target: params.target,
|
|
179
|
-
title: params.title,
|
|
180
|
-
})
|
|
181
|
-
.run();
|
|
182
|
-
|
|
183
|
-
t.parentNode.removeChild(t);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
unsetAllMarks() {
|
|
187
|
-
this.editor.chain().focus().unsetAllMarks().run();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
clearNodes() {
|
|
191
|
-
this.editor.chain().focus().clearNodes().run();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
setParagraph(e) {
|
|
195
|
-
this.editor.chain().focus().setParagraph().run();
|
|
196
|
-
e.target.classList.toggle(
|
|
197
|
-
this.activeClass,
|
|
198
|
-
this.editor.isActive("paragraph")
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
toggleHeading(e) {
|
|
203
|
-
const level = parseInt(e.target.dataset.level, 10);
|
|
204
|
-
this.editor.chain().focus().toggleHeading({ level: level }).run();
|
|
205
|
-
e.target.classList.toggle(
|
|
206
|
-
this.activeClass,
|
|
207
|
-
this.editor.isActive("heading", { level: level })
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
toggleBulletList(e) {
|
|
212
|
-
this.editor.chain().focus().toggleBulletList().run();
|
|
213
|
-
e.target.classList.toggle(
|
|
214
|
-
this.activeClass,
|
|
215
|
-
this.editor.isActive("bulletList")
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
toggleOrderedList(e) {
|
|
220
|
-
this.editor.chain().focus().toggleOrderedList().run();
|
|
221
|
-
e.target.classList.toggle(
|
|
222
|
-
this.activeClass,
|
|
223
|
-
this.editor.isActive("orderedList")
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
124
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
);
|
|
125
|
+
const attributes = {
|
|
126
|
+
...this.editor.getAttributes("link"),
|
|
127
|
+
text: this.selectedText,
|
|
128
|
+
};
|
|
129
|
+
this.openPanel(this.hiddenLinkButtonTarget, toParams("link", attributes));
|
|
233
130
|
}
|
|
234
131
|
|
|
235
|
-
|
|
236
|
-
this.editor.
|
|
237
|
-
|
|
238
|
-
this.activeClass,
|
|
239
|
-
this.editor.isActive("blockquote")
|
|
240
|
-
);
|
|
132
|
+
get selectedText() {
|
|
133
|
+
const { doc, selection } = this.editor.state;
|
|
134
|
+
return doc.textBetween(selection.from, selection.to);
|
|
241
135
|
}
|
|
242
136
|
|
|
243
|
-
|
|
244
|
-
|
|
137
|
+
linkInserterTargetConnected(inserter) {
|
|
138
|
+
const { href, target, title } = JSON.parse(inserter.dataset.params);
|
|
139
|
+
this.editor.chain().focus().setLink({ href, target, title }).run();
|
|
140
|
+
inserter.remove();
|
|
245
141
|
}
|
|
246
142
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
143
|
+
// Blocks (calls to action, images): a block's button carries its
|
|
144
|
+
// attributes (see block_view.js), the floating menu's none
|
|
250
145
|
|
|
251
|
-
|
|
252
|
-
this.
|
|
146
|
+
togglePrompts(event) {
|
|
147
|
+
this.openPanel(this.hiddenPromptsButtonTarget, blockParams(event));
|
|
253
148
|
}
|
|
254
149
|
|
|
255
|
-
|
|
256
|
-
this.
|
|
150
|
+
toggleImage(event) {
|
|
151
|
+
this.openPanel(this.hiddenImageButtonTarget, blockParams(event));
|
|
257
152
|
}
|
|
258
153
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
this.
|
|
263
|
-
|
|
154
|
+
// The panel's response: a new block, at the cursor, or an edited one, in
|
|
155
|
+
// place of the block selected by clicking it
|
|
156
|
+
contentInserterTargetConnected(inserter) {
|
|
157
|
+
this.editor.commands.insertContent(inserter.innerHTML);
|
|
158
|
+
inserter.remove();
|
|
264
159
|
}
|
|
265
160
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const pos = this._editingImagePos;
|
|
272
|
-
this._editingImagePos = null;
|
|
273
|
-
|
|
274
|
-
const parser = new DOMParser();
|
|
275
|
-
const doc = parser.parseFromString(content, "text/html");
|
|
276
|
-
const imageBlock = doc.querySelector("image-block");
|
|
277
|
-
|
|
278
|
-
if (imageBlock) {
|
|
279
|
-
const attrs = {};
|
|
280
|
-
for (const attr of imageBlock.attributes) {
|
|
281
|
-
attrs[attr.name] = attr.value;
|
|
282
|
-
}
|
|
283
|
-
const tr = this.editor.state.tr;
|
|
284
|
-
const node = this.editor.state.doc.nodeAt(pos);
|
|
285
|
-
if (node && node.type.name === "imageBlock") {
|
|
286
|
-
tr.setNodeMarkup(pos, undefined, attrs);
|
|
287
|
-
this.editor.view.dispatch(tr);
|
|
288
|
-
} else {
|
|
289
|
-
this.editor.commands.insertContent(content);
|
|
290
|
-
}
|
|
291
|
-
} else {
|
|
292
|
-
this.editor.commands.insertContent(content);
|
|
293
|
-
}
|
|
294
|
-
} else {
|
|
295
|
-
this.editor.commands.insertContent(content);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
t.parentNode.removeChild(t);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
toggleImage(e) {
|
|
302
|
-
const button = e.target.closest("button");
|
|
303
|
-
const params = button.dataset.params;
|
|
304
|
-
|
|
305
|
-
// Check if clicking an existing image block in the editor
|
|
306
|
-
const imageBlockEl = button.closest("image-block");
|
|
307
|
-
if (imageBlockEl) {
|
|
308
|
-
const pos = this.editor.view.posAtDOM(imageBlockEl, 0);
|
|
309
|
-
const resolved = this.editor.state.doc.resolve(pos);
|
|
310
|
-
// Walk up to find the imageBlock node position
|
|
311
|
-
for (let d = resolved.depth; d >= 0; d--) {
|
|
312
|
-
if (resolved.node(d).type.name === "imageBlock") {
|
|
313
|
-
this._editingImagePos = d > 0 ? resolved.before(d) : pos;
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
// Fallback: the pos itself might point at the imageBlock
|
|
318
|
-
if (this._editingImagePos == null) {
|
|
319
|
-
const node = this.editor.state.doc.nodeAt(pos);
|
|
320
|
-
if (node && node.type.name === "imageBlock") {
|
|
321
|
-
this._editingImagePos = pos;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
} else {
|
|
325
|
-
this._editingImagePos = null;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const url = this.hiddenImageButtonTarget.dataset.basePath;
|
|
329
|
-
this.hiddenImageButtonTarget.href = url + (params ? "&" + params : "");
|
|
330
|
-
this.hiddenImageButtonTarget.click();
|
|
161
|
+
// Opens a panel through its hidden link, with the given params
|
|
162
|
+
openPanel(link, params) {
|
|
163
|
+
const url = link.dataset.basePath;
|
|
164
|
+
link.href = params ? `${url}&${params}` : url;
|
|
165
|
+
link.click();
|
|
331
166
|
}
|
|
332
167
|
|
|
333
168
|
get contentJson() {
|
|
334
169
|
const { value } = this.editorJsonTarget;
|
|
335
|
-
|
|
336
|
-
if (value == "") {
|
|
337
|
-
return "";
|
|
338
|
-
} else {
|
|
339
|
-
return JSON.parse(value);
|
|
340
|
-
}
|
|
170
|
+
return value == "" ? "" : JSON.parse(value);
|
|
341
171
|
}
|
|
342
172
|
}
|
|
173
|
+
|
|
174
|
+
function toParams(scope, attributes) {
|
|
175
|
+
return new URLSearchParams(
|
|
176
|
+
Object.entries(attributes).map(([name, value]) => [
|
|
177
|
+
`${scope}[${name}]`,
|
|
178
|
+
value ?? "",
|
|
179
|
+
])
|
|
180
|
+
).toString();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function blockParams(event) {
|
|
184
|
+
return event.target.closest("button")?.dataset.params;
|
|
185
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The editor's view of a block edited in a panel (image, prompt): a button opening the panel with the
|
|
2
|
+
// block's attributes. Clicking it selects the block, so that the panel's response replaces it. Built with
|
|
3
|
+
// DOM nodes rather than HTML, so that the attributes, typed by authors, stay text.
|
|
4
|
+
export function blockView({
|
|
5
|
+
tag,
|
|
6
|
+
param,
|
|
7
|
+
actions,
|
|
8
|
+
attributes,
|
|
9
|
+
editor,
|
|
10
|
+
getPos,
|
|
11
|
+
render,
|
|
12
|
+
}) {
|
|
13
|
+
const dom = document.createElement(tag);
|
|
14
|
+
const button = document.createElement("button");
|
|
15
|
+
button.type = "button";
|
|
16
|
+
button.dataset.action = actions;
|
|
17
|
+
button.dataset.params = new URLSearchParams(
|
|
18
|
+
Object.entries(attributes).map(([name, value]) => [
|
|
19
|
+
`${param}[${name}]`,
|
|
20
|
+
value ?? "",
|
|
21
|
+
])
|
|
22
|
+
).toString();
|
|
23
|
+
// Before the Stimulus actions, which run on the same click and open the panel
|
|
24
|
+
button.addEventListener("click", () => {
|
|
25
|
+
const pos = getPos();
|
|
26
|
+
if (pos !== undefined) editor.commands.setNodeSelection(pos);
|
|
27
|
+
});
|
|
28
|
+
render(button);
|
|
29
|
+
dom.append(button);
|
|
30
|
+
return { dom };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function element(tag, properties = {}, ...children) {
|
|
34
|
+
const node = document.createElement(tag);
|
|
35
|
+
Object.entries(properties).forEach(([name, value]) => {
|
|
36
|
+
if (value !== null && value !== undefined) node.setAttribute(name, value);
|
|
37
|
+
});
|
|
38
|
+
node.append(...children);
|
|
39
|
+
return node;
|
|
40
|
+
}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { Node, mergeAttributes } from "@tiptap/core";
|
|
2
|
-
|
|
3
|
-
function attributesToParams(attributes) {
|
|
4
|
-
const params = Object.fromEntries(
|
|
5
|
-
Object.entries(attributes).map(([k, v]) => [`image[${k}]`, v])
|
|
6
|
-
);
|
|
7
|
-
return new URLSearchParams(params).toString();
|
|
8
|
-
}
|
|
2
|
+
import { blockView, element } from "./block_view.js";
|
|
9
3
|
|
|
10
4
|
export default Node.create({
|
|
11
5
|
name: "imageBlock",
|
|
@@ -48,24 +42,32 @@ export default Node.create({
|
|
|
48
42
|
},
|
|
49
43
|
|
|
50
44
|
addNodeView() {
|
|
51
|
-
return ({ node }) =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
45
|
+
return ({ node, editor, getPos }) =>
|
|
46
|
+
blockView({
|
|
47
|
+
tag: "image-block",
|
|
48
|
+
param: "image",
|
|
49
|
+
actions:
|
|
50
|
+
"trek--form--content-editor#toggleImage trek--layout#openPanel",
|
|
51
|
+
attributes: node.attrs,
|
|
52
|
+
editor,
|
|
53
|
+
getPos,
|
|
54
|
+
render: (button) => {
|
|
55
|
+
button.append(
|
|
56
|
+
element(
|
|
57
|
+
"span",
|
|
58
|
+
{
|
|
59
|
+
class: "c-trek--form--content-editor-content-image",
|
|
60
|
+
"data-size": node.attrs.size,
|
|
61
|
+
},
|
|
62
|
+
element("img", {
|
|
63
|
+
src: node.attrs.file_url,
|
|
64
|
+
alt: node.attrs.alt ?? "",
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
if (node.attrs.text)
|
|
69
|
+
button.append(element("span", {}, node.attrs.text));
|
|
70
|
+
},
|
|
71
|
+
});
|
|
70
72
|
},
|
|
71
73
|
});
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { Node, mergeAttributes } from "@tiptap/core";
|
|
2
|
-
|
|
3
|
-
function attributesToParams(attributes) {
|
|
4
|
-
const params = Object.fromEntries(
|
|
5
|
-
Object.entries(attributes).map(([k, v]) => [`prompt[${k}]`, v])
|
|
6
|
-
);
|
|
7
|
-
return new URLSearchParams(params).toString();
|
|
8
|
-
}
|
|
2
|
+
import { blockView } from "./block_view.js";
|
|
9
3
|
|
|
10
4
|
export default Node.create({
|
|
11
5
|
name: "promptsBlock",
|
|
@@ -14,9 +8,6 @@ export default Node.create({
|
|
|
14
8
|
|
|
15
9
|
addAttributes() {
|
|
16
10
|
return {
|
|
17
|
-
color: {
|
|
18
|
-
default: null,
|
|
19
|
-
},
|
|
20
11
|
href: {
|
|
21
12
|
default: null,
|
|
22
13
|
},
|
|
@@ -45,21 +36,19 @@ export default Node.create({
|
|
|
45
36
|
},
|
|
46
37
|
|
|
47
38
|
addNodeView() {
|
|
48
|
-
return ({ node }) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
node.attrs
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
};
|
|
63
|
-
};
|
|
39
|
+
return ({ node, editor, getPos }) =>
|
|
40
|
+
blockView({
|
|
41
|
+
tag: "prompts-block",
|
|
42
|
+
param: "prompt",
|
|
43
|
+
actions:
|
|
44
|
+
"trek--form--content-editor#togglePrompts trek--layout#openPanel",
|
|
45
|
+
attributes: node.attrs,
|
|
46
|
+
editor,
|
|
47
|
+
getPos,
|
|
48
|
+
render: (button) => {
|
|
49
|
+
button.title = node.attrs.title || "";
|
|
50
|
+
button.textContent = node.attrs.text;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
64
53
|
},
|
|
65
54
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Extension } from "@tiptap/core";
|
|
2
|
-
import { Plugin, PluginKey } from "
|
|
2
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
3
3
|
import slugify from "slugify";
|
|
4
4
|
|
|
5
5
|
const isTargetNodeOfType = (node, type) => node.type === type;
|
|
@@ -19,13 +19,11 @@ export const TableOfContents = Extension.create({
|
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
21
|
renderHTML: (attributes) => {
|
|
22
|
-
console.log("renderHTML", attributes);
|
|
23
22
|
return {
|
|
24
23
|
id: attributes.id,
|
|
25
24
|
};
|
|
26
25
|
},
|
|
27
26
|
parseHTML: (element) => {
|
|
28
|
-
console.log("parseHTML", element);
|
|
29
27
|
return element.id;
|
|
30
28
|
},
|
|
31
29
|
},
|
|
@@ -35,6 +35,21 @@ module Trek
|
|
|
35
35
|
)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# A toolbar button running a Tiptap command (`editor.chain().focus()[command]({ level })`), marked
|
|
39
|
+
# active when the selection has `active`, e.g. "bold", or "heading" with the button's level
|
|
40
|
+
def command_data(command, active: nil, level: nil)
|
|
41
|
+
{
|
|
42
|
+
action: stimulus_action("run"),
|
|
43
|
+
"#{identifier}-command-param": command,
|
|
44
|
+
"#{identifier}-level-param": level
|
|
45
|
+
}.compact.merge(active ? state_data(active) : {})
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A button marked active when the selection has `name` (Tiptap's `isActive`)
|
|
49
|
+
def state_data(name)
|
|
50
|
+
{ stimulus_target_key => "stateButton", "state-name": name }
|
|
51
|
+
end
|
|
52
|
+
|
|
38
53
|
def content_data_attributes
|
|
39
54
|
{
|
|
40
55
|
action: stimulus_action("click", "focus")
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
width: 25rem;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
.
|
|
23
|
+
.original,
|
|
24
24
|
.preview img {
|
|
25
25
|
display: block;
|
|
26
26
|
height: 100%;
|
|
@@ -31,6 +31,45 @@
|
|
|
31
31
|
object-fit: contain;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/* The links to the original, opening and downloading it: icons over a darkened image, on hover and focus */
|
|
35
|
+
.original {
|
|
36
|
+
position: relative;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.original-actions {
|
|
40
|
+
--icon-size: 2rem;
|
|
41
|
+
|
|
42
|
+
align-items: center;
|
|
43
|
+
background-color: rgb(0 0 0 / 45%);
|
|
44
|
+
display: flex;
|
|
45
|
+
gap: 0.8rem;
|
|
46
|
+
inset: 0;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
opacity: 0;
|
|
49
|
+
position: absolute;
|
|
50
|
+
transition: opacity 0.15s ease-in-out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.original:hover .original-actions,
|
|
54
|
+
.original:focus-within .original-actions {
|
|
55
|
+
opacity: 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.original-action {
|
|
59
|
+
align-items: center;
|
|
60
|
+
border-radius: 0.4rem;
|
|
61
|
+
color: white;
|
|
62
|
+
display: flex;
|
|
63
|
+
justify-content: center;
|
|
64
|
+
padding: 0.8rem;
|
|
65
|
+
transition: background-color 0.15s ease-in-out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.original-action:hover,
|
|
69
|
+
.original-action:focus-visible {
|
|
70
|
+
background-color: rgb(255 255 255 / 20%);
|
|
71
|
+
}
|
|
72
|
+
|
|
34
73
|
.actions {
|
|
35
74
|
align-items: flex-start;
|
|
36
75
|
display: flex;
|