keystone_ui-pages 0.3.0 → 0.4.1
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/app/assets/builds/keystone_ui_pages/editor.js +35 -22
- data/app/controllers/keystone_ui/pages/manage/elements_controller.rb +4 -1
- data/app/controllers/keystone_ui/pages/manage/titles_controller.rb +15 -0
- data/app/helpers/keystone_ui/pages/drawing_helper.rb +46 -2
- data/app/helpers/keystone_ui/pages/editor_helper.rb +1 -0
- data/app/javascript/keystone_ui_pages/ElementFields.jsx +4 -1
- data/app/javascript/keystone_ui_pages/LayoutPanel.jsx +1 -1
- data/app/javascript/keystone_ui_pages/PageEditor.jsx +4 -2
- data/app/javascript/keystone_ui_pages/PageTitle.jsx +16 -0
- data/app/javascript/keystone_ui_pages/RichTextField.jsx +100 -0
- data/app/javascript/keystone_ui_pages/operations.js +2 -0
- data/app/models/keystone_ui/pages/version.rb +4 -0
- data/app/views/keystone_ui/pages/pages/show.html.erb +1 -0
- data/config/routes.rb +1 -0
- data/lib/keystone_ui/pages/keystone_elements.rb +3 -3
- data/lib/keystone_ui/pages/operations.rb +6 -2
- data/lib/keystone_ui/pages/version.rb +1 -1
- data/the_local/agents/keystone_ui-pages-develop.md +28 -2
- metadata +4 -1
|
@@ -22,7 +22,10 @@ module KeystoneUi
|
|
|
22
22
|
|
|
23
23
|
def field_value
|
|
24
24
|
value = params.to_unsafe_h["value"]
|
|
25
|
-
|
|
25
|
+
return value.map { |entry| entry.to_h.stringify_keys } if value.is_a?(Array)
|
|
26
|
+
return { "html" => value["html"].to_s } if value.is_a?(Hash) && value.key?("html")
|
|
27
|
+
|
|
28
|
+
value.to_s
|
|
26
29
|
end
|
|
27
30
|
end
|
|
28
31
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KeystoneUi
|
|
4
|
+
module Pages
|
|
5
|
+
module Manage
|
|
6
|
+
class TitlesController < BaseController
|
|
7
|
+
include EditsPages
|
|
8
|
+
|
|
9
|
+
def update
|
|
10
|
+
changed(Operations.title(page.document, params[:title]))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -9,15 +9,24 @@ module KeystoneUi
|
|
|
9
9
|
WIDE_SPANS = { "1/1" => "@sm:col-span-12", "3/4" => "@sm:col-span-9", "2/3" => "@sm:col-span-8", "1/2" => "@sm:col-span-6", "1/3" => "@sm:col-span-4", "1/4" => "@sm:col-span-3" }.freeze
|
|
10
10
|
ELEMENT_ALIGNMENTS = { "stretch" => "flex flex-col", "start" => "flex justify-start", "center" => "flex justify-center", "end" => "flex justify-end" }.freeze
|
|
11
11
|
COLUMN_ALIGNMENTS = { "top" => "justify-start", "center" => "justify-center", "bottom" => "justify-end" }.freeze
|
|
12
|
+
SIZED_SECTION_PADDING = "py-[calc(var(--ks-spacing)*16)] lg:py-[calc(var(--ks-spacing)*24)]"
|
|
13
|
+
SHADED_CLASSES = "bg-(color:--ks-color-fill-muted) dark:bg-(color:--ks-color-fill-muted-dark)"
|
|
12
14
|
PLAIN_FIGURE_DIVIDER = "@sm:[.ks-pages-column:has(.ks-metric-plain)+&:has(.ks-metric-plain)]:border-l-(length:--ks-border-width) " \
|
|
13
15
|
"@sm:[.ks-pages-column:has(.ks-metric-plain)+&:has(.ks-metric-plain)]:border-(color:--ks-color-border) " \
|
|
14
16
|
"dark:@sm:[.ks-pages-column:has(.ks-metric-plain)+&:has(.ks-metric-plain)]:border-(color:--ks-color-border-dark)"
|
|
15
17
|
|
|
16
18
|
def drawn_version(version, values: {})
|
|
17
19
|
key = [ "keystone_ui_pages", version.id, Operations.digest(values.to_h.stringify_keys) ]
|
|
20
|
+
@drawn_page_id = version.page_id
|
|
18
21
|
Rails.cache.fetch(key) { drawn_document(version.document, values: values).to_str }.html_safe
|
|
19
22
|
end
|
|
20
23
|
|
|
24
|
+
def pages_paragraph(text:)
|
|
25
|
+
return ui_paragraph(text: text) unless text.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
rich_paragraphs(text["html"].to_s)
|
|
28
|
+
end
|
|
29
|
+
|
|
21
30
|
def drawn_document(document, editing: false, values: {})
|
|
22
31
|
@drawn_values = values.to_h.stringify_keys
|
|
23
32
|
safe_join(document["sections"].map { |section| drawn_section(section, editing) })
|
|
@@ -25,12 +34,31 @@ module KeystoneUi
|
|
|
25
34
|
|
|
26
35
|
private
|
|
27
36
|
|
|
37
|
+
def rich_paragraphs(html)
|
|
38
|
+
stored = Nokogiri::HTML5.fragment(html)
|
|
39
|
+
stored.css("script, style").each(&:remove)
|
|
40
|
+
fragment = Nokogiri::HTML5.fragment(Rails::HTML5::SafeListSanitizer.new.sanitize(stored.to_html, tags: %w[p br a], attributes: %w[href]))
|
|
41
|
+
fragment.css("a:not([href])").each { |link| link.replace(link.children) }
|
|
42
|
+
fragment.css("p").each { |paragraph| paragraph["class"] = "ks-paragraph" }
|
|
43
|
+
fragment.css("a[href^='http://'], a[href^='https://']").each { |link| link["target"], link["rel"] = "_blank", "noopener noreferrer" }
|
|
44
|
+
fragment.to_html.html_safe
|
|
45
|
+
end
|
|
46
|
+
|
|
28
47
|
def drawn_section(section, editing)
|
|
29
48
|
return if hidden?(section) && !editing
|
|
30
49
|
|
|
31
50
|
spacing = Keystone::Ui::SectionComponent::SPACING_CLASSES.fetch((section["spacing"] || Operations::DEFAULTS[:section]["spacing"]).to_sym)
|
|
32
51
|
data = editing ? { section_id: section["id"], hidden: hidden?(section) || nil }.compact : {}
|
|
33
|
-
|
|
52
|
+
shading = SHADED_CLASSES if section["background"] == "shaded"
|
|
53
|
+
tag.section(section_content(section, editing), class: [ "ks-pages-section flex flex-col gap-6 print:break-inside-avoid #{spacing}", shading ].compact.join(" "), data: data)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def section_content(section, editing)
|
|
57
|
+
rows = safe_join(section["rows"].map { |row| drawn_row(row, editing) })
|
|
58
|
+
width = section["width"]
|
|
59
|
+
return rows if width.nil? || width == "none"
|
|
60
|
+
|
|
61
|
+
tag.div(rows, class: "ks-page #{Keystone::Ui::PageComponent::MAX_WIDTH_CLASSES.fetch(width.to_sym)} w-full flex flex-col gap-6 #{SIZED_SECTION_PADDING}")
|
|
34
62
|
end
|
|
35
63
|
|
|
36
64
|
def drawn_row(row, editing)
|
|
@@ -52,10 +80,25 @@ module KeystoneUi
|
|
|
52
80
|
end
|
|
53
81
|
|
|
54
82
|
def edited_element(element)
|
|
83
|
+
return missing_element(element) unless KeystoneUi::Pages.catalog.find(element["key"])
|
|
84
|
+
|
|
55
85
|
wrapped_element(element, data: { element_id: element["id"], hidden: hidden?(element) || nil }.compact)
|
|
56
86
|
end
|
|
57
87
|
|
|
88
|
+
def reported_missing(element)
|
|
89
|
+
Rails.error.report(InvalidDocument.new("No element is registered as #{element["key"]}"), handled: true,
|
|
90
|
+
context: { page_id: @drawn_page_id, element_key: element["key"] })
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def missing_element(element)
|
|
95
|
+
tag.div("No longer offered: #{element["key"]}", class: "ks-pages-element ks-hint rounded-md border-2 border-dashed border-(--ks-color-border-strong) p-3",
|
|
96
|
+
data: { element_id: element["id"], missing_element: true })
|
|
97
|
+
end
|
|
98
|
+
|
|
58
99
|
def drawn_element(element)
|
|
100
|
+
return reported_missing(element) unless KeystoneUi::Pages.catalog.find(element["key"])
|
|
101
|
+
|
|
59
102
|
wrapped_element(element) unless hidden?(element)
|
|
60
103
|
end
|
|
61
104
|
|
|
@@ -75,6 +118,7 @@ module KeystoneUi
|
|
|
75
118
|
return [] if (field[:kind] == :list || (field[:kind] == :data && field[:default].is_a?(Array))) && value.blank?
|
|
76
119
|
return nil if field[:kind] == :data && value.blank?
|
|
77
120
|
return value if value.blank?
|
|
121
|
+
return ERB::Util.html_escape(value) if field[:as] == :escaped_text
|
|
78
122
|
return value.to_sym if field[:as] == :key
|
|
79
123
|
return value.to_s == "true" if field[:kind] == :yes_or_no
|
|
80
124
|
return value.to_sym if field[:kind] == :one_of
|
|
@@ -91,7 +135,7 @@ module KeystoneUi
|
|
|
91
135
|
end
|
|
92
136
|
|
|
93
137
|
def filled(stored)
|
|
94
|
-
return stored.presence unless stored.is_a?(Hash)
|
|
138
|
+
return stored.presence unless stored.is_a?(Hash) && stored.key?("value")
|
|
95
139
|
|
|
96
140
|
(@drawn_values || {})[stored["value"]].presence || stored["fallback"].presence || ""
|
|
97
141
|
end
|
|
@@ -23,6 +23,7 @@ module KeystoneUi
|
|
|
23
23
|
samples: Values.samples_for(page),
|
|
24
24
|
sample: chosen_sample(page),
|
|
25
25
|
samples_url: manage_page_sample_path(page),
|
|
26
|
+
title_url: manage_page_title_path(page),
|
|
26
27
|
access_exceptions: KeystoneUi::Pages.access_exceptions.invert.to_a,
|
|
27
28
|
access_exception: page.access_exception,
|
|
28
29
|
access_exception_url: manage_page_access_exception_path(page),
|
|
@@ -6,6 +6,7 @@ import Checkbox from "keystone_ui-react/src/Checkbox.jsx"
|
|
|
6
6
|
import Button from "keystone_ui-react/src/Button.jsx"
|
|
7
7
|
import { Label } from "keystone_ui-react/src/FieldText.jsx"
|
|
8
8
|
import ListField from "./ListField"
|
|
9
|
+
import RichTextField from "./RichTextField"
|
|
9
10
|
|
|
10
11
|
const FieldInput = ({ field, value, onChange }) => {
|
|
11
12
|
const shown = value ?? field.default ?? ""
|
|
@@ -59,7 +60,9 @@ export default function ElementFields({ element, definition, alignments, valueNa
|
|
|
59
60
|
{field.kind === "list" && !isBound(element.fields[field.key]) &&
|
|
60
61
|
<ListField field={field} entries={element.fields[field.key] ?? field.default ?? []} onChange={onChange} />}
|
|
61
62
|
{field.kind === "data" && <p className="ks-hint">Filled with data the host supplies on a template page</p>}
|
|
62
|
-
{field.kind
|
|
63
|
+
{field.kind === "rich_text" &&
|
|
64
|
+
<RichTextField key={JSON.stringify(element.fields[field.key] ?? null)} field={field} value={element.fields[field.key]} onChange={onChange} />}
|
|
65
|
+
{field.kind !== "list" && field.kind !== "data" && field.kind !== "rich_text" && <FieldInput key={JSON.stringify(element.fields[field.key] ?? null)} field={field}
|
|
63
66
|
value={isBound(element.fields[field.key]) ? element.fields[field.key].fallback ?? "" : element.fields[field.key]}
|
|
64
67
|
onChange={(key, text) => (isBound(element.fields[key]) ? onBind(key, element.fields[key].value, text) : onChange(key, text))} />}
|
|
65
68
|
{isBound(element.fields[field.key]) &&
|
|
@@ -5,7 +5,7 @@ import Alert from "keystone_ui-react/src/Alert.jsx"
|
|
|
5
5
|
import { Label } from "keystone_ui-react/src/FieldText.jsx"
|
|
6
6
|
|
|
7
7
|
const LABELS = {
|
|
8
|
-
section: { spacing: "Section spacing" },
|
|
8
|
+
section: { spacing: "Section spacing", background: "Section background", width: "Section width" },
|
|
9
9
|
row: { gap: "Row gap" },
|
|
10
10
|
column: { wide: "Width on wide screens", narrow: "Width on narrow screens", align: "Vertical alignment" }
|
|
11
11
|
}
|
|
@@ -2,13 +2,14 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from "react"
|
|
|
2
2
|
import { DragDropManager, Draggable, Droppable, KeyboardSensor, PointerSensor } from "@dnd-kit/dom"
|
|
3
3
|
import { Sortable } from "@dnd-kit/dom/sortable"
|
|
4
4
|
import Button from "keystone_ui-react/src/Button.jsx"
|
|
5
|
-
import { addColumn, addElement, addRow, addSection, arrange, bindField, chooseAccessException, chooseSample, duplicateNode, moveNode, removeNode, setField, showWhen, stepHistory } from "./operations"
|
|
5
|
+
import { addColumn, addElement, addRow, addSection, arrange, bindField, chooseAccessException, chooseSample, duplicateNode, moveNode, removeNode, setField, setTitle, showWhen, stepHistory } from "./operations"
|
|
6
6
|
import LayoutPanel from "./LayoutPanel"
|
|
7
7
|
import DeviceWidth, { DEVICES } from "./DeviceWidth"
|
|
8
8
|
import SelectionToolbar from "./SelectionToolbar"
|
|
9
9
|
import ElementPalette from "./ElementPalette"
|
|
10
10
|
import SelectionPath from "./SelectionPath"
|
|
11
11
|
import SampleChoice from "./SampleChoice"
|
|
12
|
+
import PageTitle from "./PageTitle"
|
|
12
13
|
import AccessExceptionChoice from "./AccessExceptionChoice"
|
|
13
14
|
import ShowWhen from "./ShowWhen"
|
|
14
15
|
import ThemeMode from "./ThemeMode"
|
|
@@ -29,7 +30,7 @@ const innermost = (target) => NODES.map(([selector, kind]) => [target.closest(se
|
|
|
29
30
|
|
|
30
31
|
const PLACED_SENSORS = [PointerSensor.configure({ preventActivation: () => false }), KeyboardSensor]
|
|
31
32
|
|
|
32
|
-
export default function PageEditor({ name, publish_url, unpublish_url, elements, canvas: drawn, document: saved, elements_url, sections_url, columns_url, rows_url, nodes_url, arrangements, defaults, shares, value_names, looks = [], samples = [], sample, samples_url, access_exceptions = [], access_exception, access_exception_url, history_url, live_url, back_url, undo_url, redo_url, undoable: couldUndo, redoable: couldRedo, token }) {
|
|
33
|
+
export default function PageEditor({ name, publish_url, unpublish_url, elements, canvas: drawn, document: saved, elements_url, sections_url, columns_url, rows_url, nodes_url, arrangements, defaults, shares, value_names, looks = [], samples = [], sample, samples_url, title_url, access_exceptions = [], access_exception, access_exception_url, history_url, live_url, back_url, undo_url, redo_url, undoable: couldUndo, redoable: couldRedo, token }) {
|
|
33
34
|
const [canvas, setCanvas] = useState(drawn)
|
|
34
35
|
const [undoable, setUndoable] = useState(couldUndo)
|
|
35
36
|
const [redoable, setRedoable] = useState(couldRedo)
|
|
@@ -245,6 +246,7 @@ export default function PageEditor({ name, publish_url, unpublish_url, elements,
|
|
|
245
246
|
<aside data-settings-panel className="w-72 shrink-0 overflow-y-auto border-l border-(--ks-color-border) bg-(--ks-color-surface) p-4 dark:border-(--ks-color-border-dark) dark:bg-(--ks-color-surface-dark)">
|
|
246
247
|
<SelectionPath path={selectionPath} onChoose={choosePart} />
|
|
247
248
|
{!node && !definition && <p className="ks-hint">Select something on the page to change it.</p>}
|
|
249
|
+
{!node && !definition && <PageTitle key={document.title ?? ""} title={document.title} onChange={(title) => setTitle(title_url, token, title).then(shown, refused)} />}
|
|
248
250
|
{node && <LayoutPanel node={node} values={inUse(node)} arrangements={arrangements}
|
|
249
251
|
onArrange={(id, setting, value) => arrange(nodes_url, token, id, setting, value).then(shown, refused)}
|
|
250
252
|
refusal={refusal}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import Input from "keystone_ui-react/src/Input.jsx"
|
|
3
|
+
import { Label } from "keystone_ui-react/src/FieldText.jsx"
|
|
4
|
+
|
|
5
|
+
export default function PageTitle({ title, onChange }) {
|
|
6
|
+
const shown = title ?? ""
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex flex-col gap-1">
|
|
10
|
+
<Label htmlFor="ks-pages-page-title">Page title</Label>
|
|
11
|
+
<Input id="ks-pages-page-title" type="text" defaultValue={shown}
|
|
12
|
+
onBlur={(event) => event.target.value !== shown && onChange(event.target.value)} />
|
|
13
|
+
<p className="ks-hint">Shown in the browser tab once the page is published</p>
|
|
14
|
+
</div>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react"
|
|
2
|
+
import Input from "keystone_ui-react/src/Input.jsx"
|
|
3
|
+
import Button from "keystone_ui-react/src/Button.jsx"
|
|
4
|
+
import { Label } from "keystone_ui-react/src/FieldText.jsx"
|
|
5
|
+
import { LexicalComposer } from "@lexical/react/LexicalComposer"
|
|
6
|
+
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"
|
|
7
|
+
import { ContentEditable } from "@lexical/react/LexicalContentEditable"
|
|
8
|
+
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"
|
|
9
|
+
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"
|
|
10
|
+
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"
|
|
11
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
|
|
12
|
+
import { LinkNode, TOGGLE_LINK_COMMAND, $isLinkNode } from "@lexical/link"
|
|
13
|
+
import { $generateHtmlFromNodes, $generateNodesFromDOM } from "@lexical/html"
|
|
14
|
+
import { $getRoot, $getSelection, $isRangeSelection, $createParagraphNode, $createTextNode, TextNode } from "lexical"
|
|
15
|
+
|
|
16
|
+
const filled = (stored) => (editor) => {
|
|
17
|
+
const root = $getRoot()
|
|
18
|
+
if (stored && typeof stored === "object" && "html" in stored) {
|
|
19
|
+
const dom = new DOMParser().parseFromString(stored.html, "text/html")
|
|
20
|
+
root.append(...$generateNodesFromDOM(editor, dom))
|
|
21
|
+
} else {
|
|
22
|
+
root.append($createParagraphNode().append($createTextNode(stored ?? "")))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const selectedLink = () => {
|
|
27
|
+
const selection = $getSelection()
|
|
28
|
+
if (!$isRangeSelection(selection)) return null
|
|
29
|
+
const node = selection.anchor.getNode()
|
|
30
|
+
return $isLinkNode(node) ? node : $isLinkNode(node.getParent()) ? node.getParent() : null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PlainWords = () => {
|
|
34
|
+
const [editor] = useLexicalComposerContext()
|
|
35
|
+
useEffect(() => editor.registerNodeTransform(TextNode, (node) => {
|
|
36
|
+
if (node.getFormat() !== 0) node.setFormat(0)
|
|
37
|
+
if (node.getStyle() !== "") node.setStyle("")
|
|
38
|
+
}), [editor])
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const LinkControls = () => {
|
|
43
|
+
const [editor] = useLexicalComposerContext()
|
|
44
|
+
const [address, setAddress] = useState(null)
|
|
45
|
+
|
|
46
|
+
const opened = () => editor.getEditorState().read(() => setAddress(selectedLink()?.getURL() ?? ""))
|
|
47
|
+
const saved = () => {
|
|
48
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, address || null)
|
|
49
|
+
setAddress(null)
|
|
50
|
+
}
|
|
51
|
+
const removed = () => {
|
|
52
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
|
53
|
+
setAddress(null)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (address === null) return <Button type="button" variant="secondary" onClick={opened}>Link</Button>
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex flex-col gap-2">
|
|
60
|
+
<Label htmlFor="ks-pages-link-address">Link address</Label>
|
|
61
|
+
<Input id="ks-pages-link-address" type="url" value={address} onChange={(event) => setAddress(event.target.value)} />
|
|
62
|
+
<div className="flex gap-2">
|
|
63
|
+
<Button type="button" onClick={saved}>Save link</Button>
|
|
64
|
+
<Button type="button" variant="secondary" onClick={removed}>Remove link</Button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const SavedOnLeave = ({ fieldKey, onChange, children }) => {
|
|
71
|
+
const [editor] = useLexicalComposerContext()
|
|
72
|
+
const left = (event) => {
|
|
73
|
+
if (event.currentTarget.contains(event.relatedTarget)) return
|
|
74
|
+
editor.getEditorState().read(() => onChange(fieldKey, { html: $generateHtmlFromNodes(editor, null) }))
|
|
75
|
+
}
|
|
76
|
+
return <div data-rich-text-field={fieldKey} onBlur={left} className="flex flex-col gap-2">{children}</div>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default function RichTextField({ field, value, onChange }) {
|
|
80
|
+
const config = {
|
|
81
|
+
namespace: `ks-pages-${field.key}`,
|
|
82
|
+
nodes: [LinkNode],
|
|
83
|
+
editorState: filled(value ?? field.default),
|
|
84
|
+
onError: (error) => { throw error }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<LexicalComposer initialConfig={config}>
|
|
89
|
+
<SavedOnLeave fieldKey={field.key} onChange={onChange}>
|
|
90
|
+
<RichTextPlugin
|
|
91
|
+
contentEditable={<ContentEditable id={`ks-pages-field-${field.key}`} className="ks-input min-h-24" />}
|
|
92
|
+
ErrorBoundary={LexicalErrorBoundary} />
|
|
93
|
+
<HistoryPlugin />
|
|
94
|
+
<LinkPlugin />
|
|
95
|
+
<PlainWords />
|
|
96
|
+
<LinkControls />
|
|
97
|
+
</SavedOnLeave>
|
|
98
|
+
</LexicalComposer>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
@@ -7,6 +7,8 @@ const sent = (url, token, method, body = {}) =>
|
|
|
7
7
|
|
|
8
8
|
export const addElement = (url, token, key, columnId) => sent(url, token, "POST", { key, column_id: columnId })
|
|
9
9
|
|
|
10
|
+
export const setTitle = (url, token, title) => sent(url, token, "POST", { title })
|
|
11
|
+
|
|
10
12
|
export const setField = (url, token, elementId, field, value) => sent(`${url}/${elementId}`, token, "PATCH", { field, value })
|
|
11
13
|
|
|
12
14
|
export const addSection = (url, token) => sent(url, token, "POST", {})
|
data/config/routes.rb
CHANGED
|
@@ -20,6 +20,7 @@ KeystoneUi::Pages::Engine.routes.draw do
|
|
|
20
20
|
end
|
|
21
21
|
resource :document, only: %i[update]
|
|
22
22
|
post :sample, to: "samples#update"
|
|
23
|
+
post :title, to: "titles#update"
|
|
23
24
|
post :access_exception, to: "access_exceptions#update"
|
|
24
25
|
post :undo, to: "edit_history#undo"
|
|
25
26
|
post :redo, to: "edit_history#redo"
|
|
@@ -23,8 +23,8 @@ module KeystoneUi
|
|
|
23
23
|
{ key: :note, label: "Note", kind: :text } ])
|
|
24
24
|
catalog.register(key: :label, name: "Label", component: :ui_caption, icon: :tag, group: "Text",
|
|
25
25
|
fields: [ { key: :text, label: "Text", kind: :text, default: "Label" } ])
|
|
26
|
-
catalog.register(key: :paragraph, name: "Paragraph", component: :
|
|
27
|
-
fields: [ { key: :text, label: "Text", kind: :
|
|
26
|
+
catalog.register(key: :paragraph, name: "Paragraph", component: :pages_paragraph, icon: :text, group: "Text",
|
|
27
|
+
fields: [ { key: :text, label: "Text", kind: :rich_text, default: "Write the text of this paragraph here." } ])
|
|
28
28
|
catalog.register(key: :hero, name: "Hero", component: :ui_hero, icon: :heading, group: "Text",
|
|
29
29
|
fields: [ { key: :title, label: "Title", kind: :text, default: "Headline" },
|
|
30
30
|
{ key: :subtitle, label: "Subtitle", kind: :long_text },
|
|
@@ -66,7 +66,7 @@ module KeystoneUi
|
|
|
66
66
|
{ key: :subtitle, label: "Subtitle", kind: :text },
|
|
67
67
|
{ key: :features, label: "Features", kind: :list,
|
|
68
68
|
entry: [ { key: :title, label: "Title", kind: :text }, { key: :description, label: "Description", kind: :long_text },
|
|
69
|
-
{ key: :icon, label: "Icon", kind: :text,
|
|
69
|
+
{ key: :icon, label: "Icon", kind: :text, as: :escaped_text,
|
|
70
70
|
default: '<svg viewBox="0 0 24 24" class="size-6 fill-none stroke-current stroke-2"><path d="M5 12l5 5L20 7"/></svg>' } ],
|
|
71
71
|
default: [ { "title" => "Fast", "description" => "Pages load quickly." },
|
|
72
72
|
{ "title" => "Clear", "description" => "Every page reads plainly." },
|
|
@@ -14,13 +14,13 @@ module KeystoneUi
|
|
|
14
14
|
module Operations
|
|
15
15
|
WIDTHS = %w[1/1 3/4 2/3 1/2 1/3 1/4].freeze
|
|
16
16
|
ARRANGEMENTS = {
|
|
17
|
-
section: { "spacing" => %w[sm md lg] },
|
|
17
|
+
section: { "spacing" => %w[sm md lg], "background" => %w[plain shaded], "width" => %w[none sm md lg xl] },
|
|
18
18
|
row: { "gap" => %w[sm md lg xl] },
|
|
19
19
|
column: { "wide" => WIDTHS, "narrow" => WIDTHS, "align" => %w[top center bottom] },
|
|
20
20
|
element: { "align" => %w[stretch start center end] }
|
|
21
21
|
}.freeze
|
|
22
22
|
DEFAULTS = {
|
|
23
|
-
section: { "spacing" => "md" },
|
|
23
|
+
section: { "spacing" => "md", "background" => "plain", "width" => "none" },
|
|
24
24
|
row: { "gap" => "md" },
|
|
25
25
|
column: { "narrow" => "1/1", "align" => "top" },
|
|
26
26
|
element: { "align" => "stretch" }
|
|
@@ -36,6 +36,10 @@ module KeystoneUi
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
def title(document, text)
|
|
40
|
+
copied(document).tap { |changed| changed["title"] = text.to_s.strip.presence }
|
|
41
|
+
end
|
|
42
|
+
|
|
39
43
|
def add_section(document)
|
|
40
44
|
copied(document).tap { |changed| changed["sections"] << Document.section }
|
|
41
45
|
end
|
|
@@ -257,8 +257,11 @@ The catalog offers these Keystone UI components by default: `button`,
|
|
|
257
257
|
|
|
258
258
|
The method must be reachable in the engine's views and in any host view that
|
|
259
259
|
calls `keystone_ui_page`. A `helper_method` on the base controller covers
|
|
260
|
-
both.
|
|
261
|
-
|
|
260
|
+
both. A page that holds an element the catalog no longer offers still
|
|
261
|
+
draws its other elements for visitors, marks the missing one in the editor
|
|
262
|
+
as no longer offered, and reports the page's id and the element's key
|
|
263
|
+
through `Rails.error`, so search the pages' documents for its key before
|
|
264
|
+
leaving it out.
|
|
262
265
|
|
|
263
266
|
## Conventions
|
|
264
267
|
|
|
@@ -278,6 +281,29 @@ The catalog offers these Keystone UI components by default: `button`,
|
|
|
278
281
|
an Archive button, and a published page also links to View live and, when
|
|
279
282
|
Chrome or Chromium is found, to its PDF. Show archived lists archived pages
|
|
280
283
|
instead, each with a Restore button.
|
|
284
|
+
- **A page's title** is set in the builder's settings panel when nothing is
|
|
285
|
+
selected, and reaches visitors when the page is published. The engine's
|
|
286
|
+
public address sets it with `content_for(:title)`, so a host layout that
|
|
287
|
+
shows `content_for(:title)` in its `<title>` gets it. A host drawing a page
|
|
288
|
+
in its own view reads `page.live_version&.title`, which is nil when the page
|
|
289
|
+
has none.
|
|
290
|
+
- **A paragraph's text is rich text.** Editors type it in the builder and
|
|
291
|
+
link selected words with the Link button, and pasted text keeps only its
|
|
292
|
+
words and links. It is stored as `{ "html" => "..." }` and cleaned each time
|
|
293
|
+
it is drawn, keeping paragraphs, line breaks and links with safe addresses.
|
|
294
|
+
A link to an `http` or `https` address opens in a new tab. A paragraph saved
|
|
295
|
+
earlier as a plain string still draws as escaped text.
|
|
296
|
+
- **A feature grid's icons** are typed per feature in the builder, usually as
|
|
297
|
+
an emoji. A typed icon is drawn as text, so markup or a script in it is shown
|
|
298
|
+
and never run. A feature left without an icon shows the check mark.
|
|
299
|
+
- **A section's background and width** are set in the builder's layout panel.
|
|
300
|
+
A shaded section fills the full width of the page with keystone_ui's
|
|
301
|
+
`--ks-color-fill-muted` colour, and its dark value in a dark theme. A section
|
|
302
|
+
with a width of `sm`, `md`, `lg` or `xl` holds its rows at keystone_ui's
|
|
303
|
+
page width of that name with its page side padding, padded above and below
|
|
304
|
+
from `--ks-spacing`. The page builder has no setting of its own for these:
|
|
305
|
+
a host changes them through its keystone_ui theme values. A section saved
|
|
306
|
+
without either draws as it did before.
|
|
281
307
|
- **A page's settings** at `/pages/manage/pages/:id/edit` change its name, slug
|
|
282
308
|
and place after it is created. A slug another page of the same owner uses, or
|
|
283
309
|
a place the host does not name, is refused with a message and changes nothing.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keystone_ui-pages
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tyler Schneider
|
|
@@ -90,6 +90,7 @@ files:
|
|
|
90
90
|
- app/controllers/keystone_ui/pages/manage/rows_controller.rb
|
|
91
91
|
- app/controllers/keystone_ui/pages/manage/samples_controller.rb
|
|
92
92
|
- app/controllers/keystone_ui/pages/manage/sections_controller.rb
|
|
93
|
+
- app/controllers/keystone_ui/pages/manage/titles_controller.rb
|
|
93
94
|
- app/controllers/keystone_ui/pages/manage/versions_controller.rb
|
|
94
95
|
- app/controllers/keystone_ui/pages/pages_controller.rb
|
|
95
96
|
- app/helpers/keystone_ui/pages/drawing_helper.rb
|
|
@@ -103,6 +104,8 @@ files:
|
|
|
103
104
|
- app/javascript/keystone_ui_pages/ListField.jsx
|
|
104
105
|
- app/javascript/keystone_ui_pages/LookChoice.jsx
|
|
105
106
|
- app/javascript/keystone_ui_pages/PageEditor.jsx
|
|
107
|
+
- app/javascript/keystone_ui_pages/PageTitle.jsx
|
|
108
|
+
- app/javascript/keystone_ui_pages/RichTextField.jsx
|
|
106
109
|
- app/javascript/keystone_ui_pages/SampleChoice.jsx
|
|
107
110
|
- app/javascript/keystone_ui_pages/SelectionPath.jsx
|
|
108
111
|
- app/javascript/keystone_ui_pages/SelectionToolbar.jsx
|