mirror of
https://github.com/penpot/penpot.git
synced 2026-08-13 16:28:59 +00:00
🎉 Add caret style changes (text editor v3) (#11171)
* 🐛 Fix editor v3 quitting when changing typography options * 🎉 Apply text styles to collapsed caret * 🐛 Fix not persisting the new selrect * 🐛 Fix selrect not being recomputed on caret style changes * 🐛 Fix quitting the editor when changing typography on empty texts
This commit is contained in:
parent
1c14c854ae
commit
be83656d55
@ -23,6 +23,49 @@ async function openEditorAndSelectAll(workspace) {
|
||||
await workspace.page.keyboard.press("ControlOrMeta+a");
|
||||
}
|
||||
|
||||
|
||||
test("Typography at a collapsed caret only styles newly typed text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page, { textEditor: true });
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.waitForFirstRender();
|
||||
|
||||
const fontSize = workspace.textEditor.fontSize;
|
||||
const editorInput = page.locator("#text-editor-wasm-input");
|
||||
|
||||
// Draw a text box, focus it, and type some text; the caret ends up collapsed
|
||||
// after it.
|
||||
await workspace.createTextShape(200, 150, 460, 260);
|
||||
await workspace.clickAt(210, 160);
|
||||
await expect(editorInput).toBeFocused();
|
||||
await page.keyboard.type("ab");
|
||||
|
||||
const originalSize = await fontSize.inputValue();
|
||||
const newSize = String(Number(originalSize) + 20);
|
||||
|
||||
// Change the font size with a collapsed caret. This must not restyle the
|
||||
// existing text; it is stashed as a pending style for the next input. Focus
|
||||
// returns to the editor once the sidebar input blurs.
|
||||
await workspace.textEditor.changeFontSize(newSize);
|
||||
await expect(editorInput).toBeFocused();
|
||||
|
||||
// Typing now adopts the pending size as its own span.
|
||||
await page.keyboard.type("X");
|
||||
|
||||
// The just-typed "X" carries the new size...
|
||||
await page.keyboard.press("Shift+ArrowLeft");
|
||||
await expect(fontSize).toHaveValue(newSize);
|
||||
|
||||
// ...while the pre-existing "ab" keeps the original size (the bug applied the
|
||||
// change to the whole shape instead).
|
||||
await page.keyboard.press("Home");
|
||||
await page.keyboard.press("Shift+ArrowRight");
|
||||
await page.keyboard.press("Shift+ArrowRight");
|
||||
await expect(fontSize).toHaveValue(originalSize);
|
||||
});
|
||||
|
||||
test.describe("BUG 10502 - Mixed families and variants", () => {
|
||||
test("Multiple variants of the same font family", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page, { textEditor: true });
|
||||
@ -114,6 +157,69 @@ test.describe("BUG 10530 - Empty text box left behind when leaving the editor",
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("BUG 11083 - Changing typography must not quit the editor", () => {
|
||||
test("Changing a numeric input must not quit the editor", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page, { textEditor: true });
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.waitForFirstRender();
|
||||
|
||||
const layerRows = workspace.layers.getByTestId("layer-row");
|
||||
|
||||
// Draw an empty text box and, without typing anything, change the font size.
|
||||
await workspace.createTextShape(200, 150, 320, 210);
|
||||
await expect(layerRows).toHaveCount(1);
|
||||
|
||||
await workspace.textEditor.changeFontSize(24);
|
||||
|
||||
// The shape is not deleted and the editor is still mounted.
|
||||
await expect(layerRows).toHaveCount(1);
|
||||
await expect(page.getByTestId("text-editor")).toBeVisible();
|
||||
|
||||
// The edition survives, so we can click back into the box and keep typing.
|
||||
await workspace.clickAt(210, 160);
|
||||
await page.keyboard.type("hello");
|
||||
await workspace.textEditor.stopEditing();
|
||||
|
||||
await layerRows.first().click();
|
||||
await workspace.waitForSelectedShapeName("hello");
|
||||
});
|
||||
|
||||
test("Opening the font family selector must not quit the editor", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page, { textEditor: true });
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.waitForFirstRender();
|
||||
|
||||
const layerRows = workspace.layers.getByTestId("layer-row");
|
||||
|
||||
// Draw an empty text box and, without typing anything, open the font family
|
||||
// selector
|
||||
await workspace.createTextShape(200, 150, 320, 210);
|
||||
await expect(layerRows).toHaveCount(1);
|
||||
|
||||
await workspace.rightSidebar.getByTitle("Font Family").click();
|
||||
|
||||
// The shape is not deleted and the editor is still mounted.
|
||||
await expect(layerRows).toHaveCount(1);
|
||||
await expect(page.getByTestId("text-editor")).toBeVisible();
|
||||
|
||||
// The edition survives, so we can click back into the box and keep typing.
|
||||
await workspace.clickAt(210, 160);
|
||||
await page.keyboard.type("hello");
|
||||
await workspace.textEditor.stopEditing();
|
||||
|
||||
await layerRows.first().click();
|
||||
await workspace.waitForSelectedShapeName("hello");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
test("BUG 10467 - Auto-width text captures every typed character", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@ -1014,8 +1014,18 @@
|
||||
(rx/of (update-paragraph-attrs {:id id :attrs attrs}))))
|
||||
|
||||
(let [attrs (select-keys attrs txt/text-node-attrs)]
|
||||
(if (or (empty? attrs) wasm-editing-selection?)
|
||||
(cond
|
||||
(or (empty? attrs) wasm-editing-selection?)
|
||||
(rx/empty)
|
||||
|
||||
;; Collapsed caret: stash a pending caret style for the next typed
|
||||
;; character instead of restyling the whole shape.
|
||||
wasm-editing?
|
||||
(do
|
||||
(wasm.text-editor/merge-pending-caret-styles! id attrs)
|
||||
(rx/of (dwt-v3/v3-update-text-editor-styles id attrs)))
|
||||
|
||||
:else
|
||||
(rx/of (update-text-attrs {:id id :attrs attrs}))))
|
||||
|
||||
(when (and (features/active-feature? state "text-editor/v2")
|
||||
@ -1238,7 +1248,7 @@
|
||||
Includes :name when update-name? so we can skip save-undo on the preceding
|
||||
update-shapes for finalize without losing name undo."
|
||||
[it state id {:keys [new-shape? content-has-text? content original-content
|
||||
update-name? name]}]
|
||||
update-name? name resize-geom]}]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
shape* (get objects id)
|
||||
@ -1250,7 +1260,8 @@
|
||||
(cond-> new-shape?
|
||||
(-> (pcb/set-undo-group id)
|
||||
(pcb/set-stack-undo? true))))
|
||||
final-geom (select-keys shape* [:selrect :points :width :height])
|
||||
;; `resize-geom` is the post-resize geometry; `shape*` still holds the pre-resize selrect.
|
||||
final-geom (or resize-geom (select-keys shape* [:selrect :points :width :height]))
|
||||
geom-keys (if new-shape? [:selrect :points] [:selrect :points :width :height])
|
||||
old-geom (when (and content-has-text? (not= :fixed (:grow-type shape*)))
|
||||
(or (get-in state [:workspace-text-session-geom id])
|
||||
@ -1302,6 +1313,13 @@
|
||||
;; modifier machinery, made auto-width typing very laggy.
|
||||
new-size (when (and finalize? (not= :fixed (:grow-type shape)))
|
||||
(dwwt/get-wasm-text-new-size shape content))
|
||||
;; Also compute the resized geometry for the finalize commit; the
|
||||
;; async `apply-wasm-modifiers` below never updates this `state`.
|
||||
resize-modifiers (when (some? new-size)
|
||||
(dwwt/resize-wasm-text-modifiers shape content))
|
||||
resize-geom (when resize-modifiers
|
||||
(-> (gsh/transform-shape shape (get-in resize-modifiers [id :modifiers]))
|
||||
(select-keys [:selrect :points :width :height])))
|
||||
;; New shapes: single undo on finalize only (no per-keystroke undo)
|
||||
effective-save-undo? (if new-shape? finalize? save-undo?)
|
||||
effective-stack-undo? (and new-shape? finalize?)
|
||||
@ -1341,12 +1359,9 @@
|
||||
:stack-undo? effective-stack-undo?
|
||||
:undo-group (when new-shape? id)})
|
||||
|
||||
;; `new-size` is only computed on finalize (see above), so this commits
|
||||
;; the final auto-width/auto-height geometry via `apply-wasm-modifiers`
|
||||
;; like other transform flows (flex parents, sidebar width, etc.).
|
||||
(when (some? new-size)
|
||||
(when-let [modifiers (dwwt/resize-wasm-text-modifiers shape content)]
|
||||
(dwm/apply-wasm-modifiers modifiers {:undo-group (when new-shape? id)}))))
|
||||
;; Push the auto-grow geometry to WASM/app state; the commit persists it via `resize-geom`.
|
||||
(when (some? resize-modifiers)
|
||||
(dwm/apply-wasm-modifiers resize-modifiers {:undo-group (when new-shape? id)})))
|
||||
|
||||
(when finalize?
|
||||
(rx/concat
|
||||
@ -1379,7 +1394,8 @@
|
||||
;; behavior (their create is bundled in the undo group).
|
||||
:original-content (if new-shape? original-content prev-content)
|
||||
:update-name? update-name?
|
||||
:name name})))
|
||||
:name name
|
||||
:resize-geom resize-geom})))
|
||||
(rx/empty))
|
||||
(rx/of (dwt/finish-transform)
|
||||
(fn [state]
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
(ns app.main.ui.workspace.shapes.text.text-edition-outline
|
||||
(:require
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.features :as features]
|
||||
@ -22,11 +21,13 @@
|
||||
(let [selrect-transform (mf/deref refs/workspace-selrect)
|
||||
[selrect transform] (dsh/get-selrect selrect-transform shape)
|
||||
|
||||
[sr-width sr-height]
|
||||
(if (or (mth/close? (:width selrect) 0.01) (mth/close? (:height selrect) 0.01))
|
||||
(let [{:keys [width height]} (wasm.api/get-text-dimensions (:id shape))]
|
||||
[width height])
|
||||
[(:width selrect) (:height selrect)])]
|
||||
;; While editing, the committed selrect lags the text (geometry is
|
||||
;; finalize-only), so measure the live WASM text for the growing axes:
|
||||
;; width grows on auto-width, height on auto-width/auto-height.
|
||||
grow-type (:grow-type shape)
|
||||
{live-width :width live-height :height} (wasm.api/get-text-dimensions (:id shape))
|
||||
sr-width (if (= grow-type :auto-width) live-width (:width selrect))
|
||||
sr-height (if (= grow-type :fixed) (:height selrect) live-height)]
|
||||
[:rect.main.viewport-selrect
|
||||
{:x (:x selrect)
|
||||
:y (:y selrect)
|
||||
|
||||
@ -23,6 +23,20 @@
|
||||
|
||||
(def caret-blink-interval-ms 250)
|
||||
|
||||
;; Elements carrying this attr keep the edit alive when focus moves onto them (see `keep-editing-on-blur?`).
|
||||
(def ^:private keep-editing-selector "[data-keep-editing-on-blur]")
|
||||
|
||||
(defn- keep-editing-on-blur?
|
||||
"True when a surface `blur` must NOT exit the editor:
|
||||
- Firefox triggering a blur when MacOS Character Viewer is open
|
||||
- Focus switched to a data-keep-editing-on-blur region (e.g. typography options),
|
||||
ancestors or descendants"
|
||||
[^js event ^js surface]
|
||||
(or (= (.-activeElement js/document) surface)
|
||||
(when-let [related (dom/get-related-target event)]
|
||||
(or (some? (.closest related keep-editing-selector))
|
||||
(some? (.querySelector related keep-editing-selector))))))
|
||||
|
||||
(defn- sync-wasm-text-editor-content!
|
||||
"Sync WASM text editor content back to the shape via the standard
|
||||
commit pipeline. Called after every text-modifying input."
|
||||
@ -39,6 +53,48 @@
|
||||
:name name
|
||||
:finalize? finalize?)))))
|
||||
|
||||
;; Keys that move/reset the caret (or delete): pressing any abandons the pending
|
||||
;; caret style. Plain character keys instead reach `on-input`, which consumes it.
|
||||
(def ^:private caret-abandon-keys
|
||||
#{"ArrowLeft" "ArrowRight" "ArrowUp" "ArrowDown"
|
||||
"Home" "End" "PageUp" "PageDown"
|
||||
"Enter" "Backspace" "Delete" "Escape" "Tab"})
|
||||
|
||||
(defn- caret-position
|
||||
"Collapsed caret as {:para :offset} from the WASM selection, or nil."
|
||||
[]
|
||||
(when-let [{:keys [focus-para focus-offset]} (text-editor/text-editor-get-selection)]
|
||||
{:para focus-para :offset focus-offset}))
|
||||
|
||||
(defn- typed-range
|
||||
"Normalized range covering the text inserted between `before` and `after`, or nil."
|
||||
[before after]
|
||||
(when (and before after)
|
||||
(if (or (< (:para before) (:para after))
|
||||
(and (= (:para before) (:para after))
|
||||
(<= (:offset before) (:offset after))))
|
||||
{:start-para (:para before) :start-offset (:offset before)
|
||||
:end-para (:para after) :end-offset (:offset after)}
|
||||
{:start-para (:para after) :start-offset (:offset after)
|
||||
:end-para (:para before) :end-offset (:offset before)})))
|
||||
|
||||
(defn- sync-with-pending-caret-styles!
|
||||
"Commit an insertion that consumed a pending caret style: sync the new text,
|
||||
then restyle the just-typed `range` into its own span. `before` is the
|
||||
pre-insert caret."
|
||||
[shape-id before]
|
||||
(let [range (typed-range before (caret-position))]
|
||||
;; Sync first so the cached content stays index-aligned with WASM.
|
||||
(text-editor/text-editor-sync-content)
|
||||
(if-let [{:keys [content]} (wasm.api/apply-pending-caret-styles! shape-id range)]
|
||||
(let [text (txt/content->text content)
|
||||
name (when (not= text "") (txt/generate-shape-name text))]
|
||||
(st/emit! (dwt/v2-update-text-shape-content
|
||||
shape-id content
|
||||
:update-name? true
|
||||
:name name)))
|
||||
(sync-wasm-text-editor-content!))))
|
||||
|
||||
(defn- reset-input-node
|
||||
"Empties the contenteditable capture surface and restores a collapsed caret
|
||||
inside it.
|
||||
@ -163,6 +219,8 @@
|
||||
on-composition-start
|
||||
(mf/use-fn
|
||||
(fn [_event]
|
||||
;; IME composition supplies its own text; drop any pending caret style.
|
||||
(text-editor/clear-pending-caret-styles!)
|
||||
(text-editor/text-editor-composition-start)))
|
||||
|
||||
on-composition-update
|
||||
@ -190,6 +248,8 @@
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(dom/prevent-default event)
|
||||
;; Pasted text keeps the surrounding style; drop any pending caret style.
|
||||
(text-editor/clear-pending-caret-styles!)
|
||||
(let [clipboard-data (.-clipboardData event)
|
||||
text (.getData clipboard-data "text/plain")]
|
||||
(when (and text (seq text))
|
||||
@ -229,6 +289,10 @@
|
||||
(let [key (.-key event)
|
||||
ctrl? (or (.-ctrlKey event) (.-metaKey event))
|
||||
shift? (.-shiftKey event)]
|
||||
;; Ctrl+A adds select-all to the caret-abandon-keys set.
|
||||
(when (or (contains? caret-abandon-keys key)
|
||||
(and ctrl? (= (str/lower key) "a")))
|
||||
(text-editor/clear-pending-caret-styles!))
|
||||
(cond
|
||||
;; Escape: finalize and stop
|
||||
(= key "Escape")
|
||||
@ -365,8 +429,14 @@
|
||||
(let [pending (mf/ref-val pending-replace-ref)]
|
||||
(dotimes [_ pending]
|
||||
(text-editor/text-editor-delete-backward)))
|
||||
(text-editor/text-editor-insert-text data)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(let [shape-id (text-editor/text-editor-get-active-shape-id)
|
||||
;; The inserted character adopts a pending caret style, if any.
|
||||
pending-styles? (some? (text-editor/get-pending-caret-styles shape-id))
|
||||
before (when pending-styles? (caret-position))]
|
||||
(text-editor/text-editor-insert-text data)
|
||||
(if pending-styles?
|
||||
(sync-with-pending-caret-styles! shape-id before)
|
||||
(sync-wasm-text-editor-content!)))
|
||||
(wasm.api/request-render-preserving-target "text-input"))
|
||||
(mf/set-ref-val! pending-replace-ref 0)
|
||||
;; IMPORTANT: do NOT clear the surface here (see keep-input-alive):
|
||||
@ -379,6 +449,9 @@
|
||||
(fn [^js event]
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
;; Repositioning the caret abandons the pending caret style (also
|
||||
;; covers click and double-click, which fire pointer-down first).
|
||||
(text-editor/clear-pending-caret-styles!)
|
||||
(mf/set-ref-val! dragging-ref true)
|
||||
(if (.-shiftKey event)
|
||||
(wasm.api/text-editor-pointer-down-extend off-pt)
|
||||
@ -431,12 +504,10 @@
|
||||
on-blur
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
;; MacOS Character Viewer on Firefox fires a `blur` when it opens.
|
||||
;; To avoid losing the selected character, we need guard against
|
||||
;; `activeElement` being the surface itself.
|
||||
;; A blur exits the editor unless keep-editing-on-blur? is true
|
||||
(when-not (and (some? event)
|
||||
(= (.-activeElement js/document)
|
||||
(mf/ref-val contenteditable-ref)))
|
||||
(keep-editing-on-blur? event (mf/ref-val contenteditable-ref)))
|
||||
(text-editor/clear-pending-caret-styles!)
|
||||
(sync-wasm-text-editor-content! {:finalize? true})
|
||||
(wasm.api/text-editor-blur))))
|
||||
|
||||
|
||||
@ -498,6 +498,8 @@
|
||||
(ts/schedule 0 #(some-> (mf/ref-val dropdown-ref) dom/focus!))))
|
||||
|
||||
[:section {:class (stl/css :element-set)
|
||||
;; Focusing these controls must not exit the v3 text editor (see `keep-editing-on-blur?`).
|
||||
:data-keep-editing-on-blur true
|
||||
:aria-label (tr "workspace.options.text-options.text-section")}
|
||||
[:div {:class (stl/css :element-title)}
|
||||
[:> title-bar* {:collapsable true
|
||||
|
||||
@ -735,6 +735,17 @@
|
||||
(request-render "apply-paragraph-attrs-to-selection")
|
||||
result))
|
||||
|
||||
(defn apply-pending-caret-styles!
|
||||
"Apply the shape's pending caret style over `range` (the just-typed text) and
|
||||
clear it; returns {:shape-id :content} or nil when there is none."
|
||||
[shape-id range]
|
||||
(when-let [styles (text-editor/get-pending-caret-styles shape-id)]
|
||||
(let [result (text-editor/apply-styles-to-range
|
||||
shape-id range styles use-shape set-shape-text-content)]
|
||||
(text-editor/clear-pending-caret-styles!)
|
||||
(request-render "apply-pending-caret-styles")
|
||||
result)))
|
||||
|
||||
(defn set-parent-id
|
||||
[id]
|
||||
(let [buffer (uuid/get-u32 id)]
|
||||
|
||||
@ -547,6 +547,24 @@
|
||||
[shape-id content]
|
||||
(swap! shape-text-contents assoc shape-id content))
|
||||
|
||||
;; Typography chosen at a collapsed caret: not applied to existing text, but
|
||||
;; picked up (as a new span) by the next inserted text. Keyed by shape-id.
|
||||
(def ^:private pending-caret-styles (atom {}))
|
||||
|
||||
(defn merge-pending-caret-styles!
|
||||
"Stack `styles` onto the shape's pending caret style."
|
||||
[shape-id styles]
|
||||
(swap! pending-caret-styles update shape-id merge styles))
|
||||
|
||||
(defn get-pending-caret-styles
|
||||
[shape-id]
|
||||
(get @pending-caret-styles shape-id))
|
||||
|
||||
(defn clear-pending-caret-styles!
|
||||
"Drop every pending caret style; only the active shape can hold one."
|
||||
[]
|
||||
(reset! pending-caret-styles {}))
|
||||
|
||||
(defn- merge-exported-texts-into-content
|
||||
"Merge exported span texts back into the existing content tree.
|
||||
|
||||
@ -704,18 +722,49 @@
|
||||
(= 1 (count fills-set)) (first fills-set)
|
||||
:else :multiple)))
|
||||
|
||||
(defn- apply-styles-over-range
|
||||
"Apply `styles` (attrs map or per-span fn) to the char range of `content`, splitting spans."
|
||||
[content {:keys [start-para start-offset end-para end-offset]} styles]
|
||||
(let [paragraph-set (first (:children content))
|
||||
paragraphs (:children paragraph-set)
|
||||
new-paragraphs (mapv (fn [idx para]
|
||||
(cond
|
||||
;; paragraph outside the range of paragraphs.
|
||||
(or (< idx start-para) (> idx end-para))
|
||||
para
|
||||
|
||||
;; same paragraph.
|
||||
(= start-para end-para)
|
||||
(apply-attrs-to-paragraph para start-offset end-offset styles)
|
||||
|
||||
;; first paragraph
|
||||
(= idx start-para)
|
||||
(apply-attrs-to-paragraph para start-offset (para-char-count para) styles)
|
||||
|
||||
;; final paragraph
|
||||
(= idx end-para)
|
||||
(apply-attrs-to-paragraph para 0 end-offset styles)
|
||||
|
||||
;; any other paragraph
|
||||
:else
|
||||
(apply-attrs-to-paragraph para 0 (para-char-count para) styles)))
|
||||
(range (count paragraphs))
|
||||
paragraphs)]
|
||||
(assoc content :children [(assoc paragraph-set :children new-paragraphs)])))
|
||||
|
||||
(defn- clean-styles
|
||||
"Drop nil-valued attrs (unlike the DOM path, our merge would keep them and fail
|
||||
the backend schema); a per-span fn is passed through untouched."
|
||||
[styles]
|
||||
(if (fn? styles)
|
||||
styles
|
||||
(into {} (remove (comp nil? val)) styles)))
|
||||
|
||||
(defn apply-styles-to-selection
|
||||
"Apply `styles` (attrs map, or a fn per span) to the selected spans; `:with-fills?` also returns `:fills`."
|
||||
[styles use-shape-fn set-shape-text-content-fn & [{:keys [with-fills?]}]]
|
||||
(when (wasm/ready?)
|
||||
(let [;; Drop nil-valued attrs so they are never merged onto text spans.
|
||||
;; The DOM editor path strips these in `attrs->styles`; the WASM merge
|
||||
;; here (`apply-attrs-to-paragraph`) does not, so an unresolved attr
|
||||
;; (e.g. nil :font-family/:font-weight/:font-style from an unloaded
|
||||
;; font) would corrupt the span and fail the backend schema.
|
||||
styles (if (fn? styles)
|
||||
styles
|
||||
(into {} (remove (comp nil? val)) styles))
|
||||
(let [styles (clean-styles styles)
|
||||
shape-id (text-editor-get-active-shape-id)
|
||||
selection (text-editor-get-selection)]
|
||||
|
||||
@ -725,41 +774,10 @@
|
||||
(let [normalized-selection (normalize-selection selection)
|
||||
{:keys [start-para start-offset end-para end-offset]} normalized-selection
|
||||
|
||||
collapsed? (and (= start-para end-para) (= start-offset end-offset))
|
||||
collapsed? (and (= start-para end-para) (= start-offset end-offset))
|
||||
|
||||
paragraph-set (first (:children content))
|
||||
paragraphs (:children paragraph-set)
|
||||
|
||||
new-paragraphs
|
||||
(when (not collapsed?)
|
||||
(mapv (fn [idx para]
|
||||
(cond
|
||||
;; paragraph outside the range of paragraphs.
|
||||
(or (< idx start-para) (> idx end-para))
|
||||
para
|
||||
|
||||
;; same paragraph.
|
||||
(= start-para end-para)
|
||||
(apply-attrs-to-paragraph para start-offset end-offset styles)
|
||||
|
||||
;; first paragraph
|
||||
(= idx start-para)
|
||||
(apply-attrs-to-paragraph para start-offset (para-char-count para) styles)
|
||||
|
||||
;; final paragraph
|
||||
(= idx end-para)
|
||||
(apply-attrs-to-paragraph para 0 end-offset styles)
|
||||
|
||||
;; any other paragraph
|
||||
:else
|
||||
(apply-attrs-to-paragraph para 0 (para-char-count para) styles)))
|
||||
|
||||
(range (count paragraphs))
|
||||
paragraphs))
|
||||
|
||||
new-content (when new-paragraphs
|
||||
(assoc content :children
|
||||
[(assoc paragraph-set :children new-paragraphs)]))]
|
||||
new-content (when (not collapsed?)
|
||||
(apply-styles-over-range content normalized-selection styles))]
|
||||
|
||||
(when new-content
|
||||
(update-cached-content! shape-id new-content)
|
||||
@ -770,6 +788,24 @@
|
||||
with-fills?
|
||||
(assoc :fills (selection-fills new-content normalized-selection)))))))))))
|
||||
|
||||
(defn apply-styles-to-range
|
||||
"Like `apply-styles-to-selection` but over an explicit range (used to restyle
|
||||
just-inserted text); returns `{:shape-id :content}` or nil."
|
||||
[shape-id {:keys [start-para start-offset end-para end-offset] :as range} styles
|
||||
use-shape-fn set-shape-text-content-fn]
|
||||
(when (wasm/ready?)
|
||||
(let [styles (clean-styles styles)
|
||||
content (get-cached-content shape-id)]
|
||||
(when (and content
|
||||
(seq styles)
|
||||
(not (and (= start-para end-para) (= start-offset end-offset))))
|
||||
(let [new-content (apply-styles-over-range content range styles)]
|
||||
(update-cached-content! shape-id new-content)
|
||||
(use-shape-fn shape-id)
|
||||
(set-shape-text-content-fn shape-id new-content)
|
||||
{:shape-id shape-id
|
||||
:content new-content})))))
|
||||
|
||||
(defn apply-paragraph-attrs-to-selection
|
||||
"Apply paragraph level attrs (text-align, text-direction) to the whole
|
||||
paragraphs the editor selection touches; a collapsed caret means just the one
|
||||
|
||||
@ -46,8 +46,18 @@
|
||||
[]
|
||||
(dom/query "[data-itype=\"editor\"]"))
|
||||
|
||||
(defn v3-get-text-editor-content
|
||||
[]
|
||||
(dom/get-element "text-editor-wasm-input"))
|
||||
|
||||
(defn get-text-editor-content
|
||||
[]
|
||||
(if (features/active-feature? @st/state "text-editor/v2")
|
||||
(cond
|
||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
||||
(v3-get-text-editor-content)
|
||||
|
||||
(features/active-feature? @st/state "text-editor/v2")
|
||||
(v2-get-text-editor-content)
|
||||
|
||||
:else
|
||||
(v1-get-text-editor-content)))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user