Фокус в браузере сложно и запутанно поддерживать.
Вот то, что я думаю, что происходит, когда вы нажимаете Enter
Событие нажатия клавиш запускается
Вы добавляете новую тему через graft-topic!
Вы переключаете стили так, что ввод отображается, а метка скрыта
Высфокусируйтесь на следующем элементе в списке
, затем, после того, как событие keydown выполнено, реагенты повторно отображают
- Когда это происходит, элемент, который сфокусирован, заменяетсяновым элементом, который вы создали
В случае, если элемент, по которому вы нажимаете, вводится из , а не , последний элемент в списке
- Вымогут сфокусироваться на уже существующем элементе, а затем реактив заменяет этот элемент новым элементом, созданным в graft-topic!
В случае, если элемент, по которому вы нажимаете, введите из значение последний элемент в списке
Не удается выполнить фокусировку, поскольку элемент с таким идентификатором еще не существует
, поэтому ни один элемент не находится в фокусе, когда реагент повторно обращается к вновь созданному элементу
Что делает браузер
Новый созданный вами элемент находится в том же месте, что и старый сфокусированный элемент, поэтому браузер сохраняет фокус в этом месте
Вы можете проверить это с помощью следующего фрагмента кода, который переключает два входа при каждом нажатии клавиши.
Несмотря на то, что разные идентификаторы и разные компоненты, фокус остается на том же месте, что и при замене двух.компоненты
(defn test-comp []
(r/with-let [*test? (r/atom true)]
[:div
(if @*test?
[:div
[:input
{:value "test"
:id "test"
:on-key-down #(swap! *test? not)}]
[:input
{:value "not test"
:id "not test"
:on-key-down #(swap! *test? not)}]]
[:div
[:input
{:value "not test"
:id "not test"
:on-key-down #(swap! *test? not)}]
[:input
{:value "test"
:id "test"
:on-key-down #(swap! *test? not)}]])]))
(примечание: это даст вам предупреждение об отсутствии обработчика при изменении, но это не важно для этой демонстрации, просто хотел указать значение, чтобы вы могли видеть своп двух входовместа, но фокус остается на том же месте)
Что касается того, как это исправить ...
Не полагайтесь на ожидание цикла или использованиеJS тайм-аут, чтобы исправить это, это просто тратит драгоценное время
Я бы порекомендовал не использовать браузер для сохранения фокуса
Простой ответ - сохранить какой индекс находится в состоянии приложения, а затемрешить, будет ли отображаться метка или вход на основе того, что
Затем добавьте атрибут автофокуса к входу, чтобы при рендеринге он попадал в фокус
Некоторые указатели длякак использовать реагент
В вашем коде вы разрешаете компонент реагента с помощью (), но вам следует использовать []
Это связано с тем, как реагент решает, когда следует выполнить повторную визуализацию.компоненты, но так как вы разрешили все дерево, каждый раз, когда вы меняете атом, который вы переопределили, он будет перерисовывать все ваше дерево, а не только место, где вы переопределили атом.(проверьте это, добавив println в ваш код в компоненте build-topic-span)
Определите курсоры в компоненте формы-2 (или используйте with-let), их нужно определять только один раз для каждого компонентапоэтому нет необходимости переопределять их при каждом последующем рендеринге (не уверен, приведет ли это к ошибкам, но это хорошая практика)
также вы можете использовать курсор как get-in, поэтому вместо
t (r/cursor sub-tree-ratom [index])
topic-ratom (r/cursor t [:topic])
вы можете сделать
topic-ratom (r/cursor t [index :topic])
Некоторые другие заметки
То, что вы делаете, меняются стилем обмена, если вы отслеживаете чтосфокусирован, вы можете просто визуализировать другой компонент в зависимости от того, на чем сфокусирован, не нужно одновременно иметь метку и ввод в dom.
передача нескольких строковых идентификаторов очень сбивает с толкуособенно при вызове привитой темы!вы деструктурируете строку обратно на путь.С данными гораздо проще работать, сохраняйте путь в векторе и делайте его строкой только тогда, когда он должен быть
Этот пример с учетом этих факторов
(ns test-reagent-vector.core
(:require [clojure.string :as s]
[reagent.core :as r]))
(def ^{:constant true} topic-separator \u02D1)
(def empty-test-topic {:topic "Empty Test Topic"})
(defonce global-state-with-hierarchy
(r/atom {:name "Global Application State, Inc."
:focused-index nil
:data {:one "one" :two 2 :three [3]}
:tree [{:topic "First Headline"}
{:topic "Middle Headline"}
{:topic "Last Headline"}]}))
(defn get-element-by-id
[id]
(.getElementById js/document id))
(defn event->target-element
[evt]
(.-target evt))
(defn event->target-value
[evt]
(.-value (event->target-element evt)))
(defn swap-style-property
"Swap the specified style settings for the two elements."
[first-id second-id property]
(let [style-declaration-of-first (.-style (get-element-by-id first-id))
style-declaration-of-second (.-style (get-element-by-id second-id))
value-of-first (.getPropertyValue style-declaration-of-first property)
value-of-second (.getPropertyValue style-declaration-of-second property)]
(.setProperty style-declaration-of-first property value-of-second)
(.setProperty style-declaration-of-second property value-of-first)))
(defn swap-display-properties
"Swap the display style properties for the two elements."
[first-id second-id]
(swap-style-property first-id second-id "display"))
;;------------------------------------------------------------------------------
;; Vector-related manipulations.
(defn delete-at
"Remove the nth element from the vector and return the result."
[v n]
(vec (concat (subvec v 0 n) (subvec v (inc n)))))
(defn remove-last
"Remove the last element in the vector and return the result."
[v]
(subvec v 0 (dec (count v))))
(defn remove-last-two
"Remove the last two elements in the vector and return the result."
[v]
(subvec v 0 (- (count v) 2)))
(defn insert-at
"Return a copy of the vector with new-item inserted at the given n. If
n is less than zero, the new item will be inserted at the beginning of
the vector. If n is greater than the length of the vector, the new item
will be inserted at the end of the vector."
[v n new-item]
(cond (< n 0) (into [new-item] v)
(>= n (count v)) (conj v new-item)
:default (vec (concat (conj (subvec v 0 n) new-item) (subvec v n)))))
(defn replace-at
"Replace the current element in the vector at index with the new-element
and return it."
[v index new-element]
(insert-at (delete-at v index) index new-element))
;;------------------------------------------------------------------------------
;; Tree id manipulation functions.
(defn tree-id->tree-id-parts
"Split a DOM id string (as used in this program) into its parts and return
a vector of the parts"
[id]
(s/split id topic-separator))
(defn tree-id-parts->tree-id-string
"Return a string formed by interposing the topic-separator between the
elements of the input vector."
[v]
(str (s/join topic-separator v)))
(defn increment-leaf-index
"Given the tree id of a leaf node, return an id with the node index
incremented."
[tree-id]
(let [parts (tree-id->tree-id-parts tree-id)
index-in-vector (- (count parts) 2)
leaf-index (int (nth parts index-in-vector))
new-parts (replace-at parts index-in-vector (inc leaf-index))]
(tree-id-parts->tree-id-string new-parts)))
(defn change-tree-id-type
"Change the 'type' of a tree DOM element id to something else."
[id new-type]
(let [parts (tree-id->tree-id-parts id)
shortened (remove-last parts)]
(str (tree-id-parts->tree-id-string shortened) (str topic-separator new-type))))
(defn tree-id->nav-vector-and-index
"Parse the id into a navigation path vector to the parent of the node and an
index within the vector of children. Return a map containing the two pieces
of data. Basically, parse the id into a vector of information to navigate
to the parent (a la get-n) and the index of the child encoded in the id."
[tree-id]
(let [string-vec (tree-id->tree-id-parts tree-id)
idx (int (nth string-vec (- (count string-vec) 2)))
without-last-2 (remove-last-two string-vec)
without-first (delete-at without-last-2 0)
index-vector (mapv int without-first)
interposed (interpose :children index-vector)]
{:path-to-parent (vec interposed) :child-index idx}))
;;------------------------------------------------------------------------------
;; Functions to manipulate the tree and subtrees.
(defn add-child!
"Insert the given topic at the specified index in the parents vector of
children. No data is deleted."
[parent-topic-ratom index topic-to-add]
(swap! parent-topic-ratom insert-at index topic-to-add))
(defn graft-topic!
"Add a new topic at the specified location in the tree. The topic is inserted
into the tree. No data is removed. Any existing information after the graft
is pushed down in the tree."
[root-ratom id-of-desired-node topic-to-graft]
(let [path-and-index (tree-id->nav-vector-and-index id-of-desired-node)]
(add-child! (r/cursor root-ratom (:path-to-parent path-and-index))
(:child-index path-and-index) topic-to-graft)))
;;;-----------------------------------------------------------------------------
;;; Functions to handle keystroke events.
(defn handle-enter-key-down!
"Handle a key-down event for the Enter/Return key. Insert a new headline
in the tree and focus it, ready for editing."
[app-state root-ratom index]
(add-child! root-ratom (inc index) empty-test-topic)
(swap! app-state update :focused-index inc)
)
(defn handle-key-down
"Detect key-down events and dispatch them to the appropriate handlers."
[evt app-state root-ratom index]
(when (= (.-key evt) "Enter")
(handle-enter-key-down! app-state root-ratom index)))
;;;-----------------------------------------------------------------------------
;;; Functions to build the control.
(defn build-topic-span
"Build the textual part of a topic/headline."
[root-ratom index]
(r/with-let [topic-ratom (r/cursor root-ratom [index :topic])
focused-index (r/cursor global-state-with-hierarchy [:focused-index])]
(if-not (= index @focused-index)
[:label
{:onClick #(reset! focused-index index)}
@topic-ratom]
[:input {:type "text"
:auto-focus true
:onKeyDown #(handle-key-down % global-state-with-hierarchy root-ratom index)
:onChange #(reset! topic-ratom (event->target-value %))
:on-blur #(when (= index @focused-index)
(reset! focused-index nil))
:value @topic-ratom}])))
(defn tree->hiccup
"Given a data structure containing a hierarchical tree of topics, generate
hiccup to represent that tree. Also generates a unique, structure-based
id that is included in the hiccup so that the correct element in the
application state can be located when its corresponding HTML element is
clicked."
([root-ratom]
[tree->hiccup root-ratom root-ratom "root"])
([root-ratom sub-tree-ratom path-so-far]
[:ul
(doall
(for [index (range (count @sub-tree-ratom))]
^{:key (str index)}
[:li
[:div
[build-topic-span root-ratom index]]]
))]))
(defn home
"Return a function to layout the home (only) page."
[app-state-ratom]
(r/with-let [tree-ratom (r/cursor app-state-ratom [:tree])]
[:div
[tree->hiccup tree-ratom]]))
(r/render
[home global-state-with-hierarchy]
(get-element-by-id "app"))
Я толькоизменил дом, дерево → сбой, создайте диапазон темы и обработайте нажатие клавиши.
В будущем
В примере, который я написал, предполагается, что это плоский список, но кажется, что вы планируете сделать его вложенным списком в будущем, и если это правда, я бы порекомендовал изменить некоторые вещи
, связать уникальный идентификаторк каждой теме и используйте этот идентификатор, чтобы определить, находится ли этот элемент в фокусе.
указать путь до того момента, пока вектор идентификаторов до этой точки в дереве
неукажите ключ как функцию индекса, что если элемент поменяется местами с другим элементом в дереве?мы не хотим перерисовывать это.На основании уникального идентификатора
исследуйте трек с реагентами!функция для сокращения повторений при запросе, фокусируется ли текущий элемент
Надеюсь, это поможет
Не стесняйтесь сообщать мне, если у вас есть еще вопросы относительно того, как создать вложенный интерактивный список :)