-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use SheafDisplay fr composed sheaf view in discuss #136
base: feat/discussion/sheaf-creation-modal
Are you sure you want to change the base?
Use SheafDisplay fr composed sheaf view in discuss #136
Conversation
Also has a bunch of other inflex related things as well add icon file
d999764
to
81cb5be
Compare
1. [FIXME odd tailwind config issue:](#org9cd6457) <a id="org9cd6457"></a> So after the annoyance about using css variables to define other css variables and how even after doing the hsl syntax, when using them in tailwind utility classes, it wasn’t possible to add opacity classes to it, [here’s a related gh comment for it](shadcn-ui/ui#805) I decided to try using an external `colors.js` so that I can make my best friend, JS, do the magic. Turns out this breaks the tailwindconfig file and some other import doesn’t get its name resolved. I have no idea why, let me know if you know this. Using JS might be more convenient. perhaps this functional fetching of colors might make work [as seen here](https://stackoverflow.com/questions/74521806/how-to-add-custom-opacity-values-to-colors-in-tailwind-css-themes/74719392#74719392)? I’m going to side-step this for now.
@@ -4,6 +4,7 @@ | |||
const plugin = require("tailwindcss/plugin"); | |||
const fs = require("fs"); | |||
const path = require("path"); | |||
// const colors = require("./colors"); // QQ: @ks0m1c_dharma any idea why if i uncomment this, the tailwind plugin config gets broken? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FIXME odd tailwind config issue:
So after the annoyance about using css variables to define other css variables and how even after doing the hsl syntax, when using them in tailwind utility classes, it wasn’t possible to add opacity classes to it,
here’s a related gh comment for it
I decided to try using an external colors.js
so that I can make my best friend, JS, do the magic.
Turns out this breaks the tailwindconfig file and some other import doesn’t get its name resolved. I have no idea why, let me know if you know this.
Rebuilding...
Error: Cannot find module 'tailwindcss/plugin'
Require stack:
- /Users/rtshkmr/Projects/vyasa/assets/tailwind.config.js
at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
at Function._resolveFilename (pkg/prelude/bootstrap.js:1955:46)
at Function.resolve (node:internal/modules/cjs/helpers:108:19)
at _resolve (/snapshot/tailwindcss/node_modules/jiti/dist/jiti.js:1:241025)
at jiti (/snapshot/tailwindcss/node_modules/jiti/dist/jiti.js:1:243309)
at /Users/rtshkmr/Projects/vyasa/assets/tailwind.config.js:4:16
at jiti (/snapshot/tailwindcss/node_modules/jiti/dist/jiti.js:1:245784)
at /snapshot/tailwindcss/lib/lib/load-config.js:37:30
at loadConfig (/snapshot/tailwindcss/lib/lib/load-config.js:39:6)
at Object.loadConfig (/snapshot/tailwindcss/lib/cli/build/plugin.js:135:49) {
code: 'MODULE_NOT_FOUND',
requireStack: [ '/Users/rtshkmr/Projects/vyasa/assets/tailwind.config.js' ]
}
Using JS might be more convenient.
perhaps this functional fetching of colors might make work as seen here?
I’m going to side-step this for now.
═════════════════════════════════════════════ This works well enough now, will carry on. The TextareaAutoResize hook, when applied to any textarea (should work on other elements as well actually), will now listen for some events and fire off the handleInput(). ⁃ the handleInput is a bound reference – it’s a closure fn that has access to the variables that the initialiser has access to. By having the same reference, we can remove the eventListeners as part of the teardown subrouting in the `destroy()' callback ⁃ faced an issue because the textarea is actually mounted, just not visible because it’s hidden within a dropdown. This is a good use case for the `InteractionObserver API' ([ref docs]), it allows async observation of visibility of an element w.r.t. the viewport. Using this observer allows us to call the handleInput when it’s already mounted ⁃ when my textarea turns from disabled to enabled, it for some reason it goes back to being number of rows = 1 and the handleInput doesn’t run anymore. The solution to this was to use a `MutationObserver' ([ref api docs]) which can watch for chanes to the dom tree. Specifically, we watch for changes to this element’s (textarea) disabled attribute, then call the handleInput. [ref docs] <https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver> [ref api docs] <https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver>
Or, see the commit msg here, same thing Notes on TextareaAutoResize, good learning commit -- TILThis works well enough now, will carry on. The TextareaAutoResize hook, when applied to any textarea (should work on other elements as well actually), will now listen for some events and fire off the handleInput().
|
Basically, the button group was just hidden, but it would block the buttons beneath it, even though it was invisible. This prevented button clicks if there were buttons that would get "hidden" beneath. The fix was to ony render button group if the control panel is open.
@@ -27,6 +27,7 @@ defmodule VyasaWeb.ControlPanel do | |||
<!-- SVG Icon Button --> | |||
<.control_panel_mode_indicator mode={@mode} myself={@myself} session_active?={@session.name} /> | |||
<div | |||
:if={@show_control_panel?} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I quite like the extracted function component here
This was amazing
things done: 1. made sure one of the containers for the generic modal wrapper has w-full 2. used font-light for all the action buttons' text 3. add some nice styling to represent the soon-to-be-created sheaf -- a visual container 4. the buttons for the modal have been updated to be icon-less. This should be alright, it fits the space well. NOTE: the "start new thread" vs "reply" needs different event handlers, will be in the next commit
Also added the file for the js hook that somehow hasn't been committed yet
There's some bugs introduced, have to change them
CLOSED: [2024-12-08 Sun 15:00] So far on discuss mode, one could only do reply_to but not create new thread as a one-click action. This floating action button should just intuitively tell people to "hey add new thread"
60cad30
to
0e1cd6c
Compare
this doesn't have good devex ergonomics. Ideally just like how there's a put_flash(), there should be some mechanism for the modal hiding and stuff. The requirement would be something like: - from within a handle_event() which is on the server side, I should be able to define a %JS{} and then dispatch that to the client side to evaluate.
…ion/ui-iteration-improve-sheaf-display
lib/vyasa/sangh.ex
Outdated
def make_reply( | ||
%Sheaf{ | ||
parent_id: nil | ||
} = reply, | ||
attrs | ||
%Sheaf{} = reply, | ||
%{parent: nil} = attrs | ||
) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[BUG] Odd bug again in sheaf creation @ make_reply()
likely need you to add verify this on the changeset side @ks0m1c
Not sure if the fix is actually something to be done at the changeset
level. In brief, the SOT should be the replyto context that
is in the socket state. So even if a sheaf may have a
currently-associated parent sheaf, if the socket says that the
parentid or parent is null then it's null (and a root sheaf).
See lib/vyasaweb/components/contexts/read.ex
def handle_event(
"sheaf::publish",
%{
"body" => body
} = _params,
%Socket{
assigns: %{
draft_reflector: %Sheaf{} = draft_sheaf,
draft_reflector_ui: %SheafUiState{
marks_ui: %MarksUiState{} = _ui_state
},
reply_to: reply_to_sheaf,
session: %VyasaWeb.Session{
name: username,
sangh: %Vyasa.Sangh.Session{
id: sangh_id
}
}
}
} = socket
) do
IO.inspect(%{body: body},
label: "SHEAF CREATION without parent"
)
payload_precursor = %{
body: body,
traits: ["published"],
signature: username,
inserted_at: Utils.Time.get_utc_now()
}
# FIXME: the socket state should be the SOT. So even if the draft sheaf has an associated parent,
# when my reply_to_sheaf is nil, then the parent should be set to nil if it gets published.
# currently this is NOT happening, i.e. the parent assoc is still there, so the sheaf gets published but as a child of whatever the previous state was.
reply_payload =
case reply_to_sheaf do
%Sheaf{} ->
payload_precursor |> Map.put(:parent, reply_to_sheaf)
nil ->
# so the socket's reply_to will take priority, even if the draft sheaf may already an associated parent, this will take priority.
payload_precursor |> Map.put(:parent, nil)
end
draft_sheaf
|> Vyasa.Sangh.make_reply(reply_payload)
{:noreply,
socket
|> ui_toggle_show_sheaf_modal?()
|> register_sheaf(Sheaf.draft!(sangh_id))
|> assign(reply_to: nil)
|> cascade_stream_change()}
end
@ks0m1c this is just a FYI, I'm going to add this in to standardise how the modal behaviour works. ✅ this works wellPossible solution to the Problem := need to be able to properly dispatch JS commands from server-side handle-events to client-sideProblem: need to be able to properly dispatch JS commands from server-side handle-events to client-sidepossible solution: js-exec event listener @ app.js to receive JS command from the serverThe following is an outline, I've not tried to add in this fix but high fly.io blogpost okay, so the execJS fucntion, which we can see in How LiveBeats does itFirst, livebeats has a set of focus-related helper functions: let Focus = {
focusMain() {
let target =
document.querySelector("main h1") || document.querySelector("main");
if (target) {
let origTabIndex = target.tabIndex;
target.tabIndex = -1;
target.focus();
target.tabIndex = origTabIndex;
}
},
// Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
isFocusable(el) {
if (
el.tabIndex > 0 ||
(el.tabIndex === 0 && el.getAttribute("tabIndex") !== null)
) {
return true;
}
if (el.disabled) {
return false;
}
switch (el.nodeName) {
case "A":
return !!el.href && el.rel !== "ignore";
case "INPUT":
return el.type != "hidden" && el.type !== "file";
case "BUTTON":
case "SELECT":
case "TEXTAREA":
return true;
default:
return false;
}
},
// Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
attemptFocus(el) {
if (!el) {
return;
}
if (!this.isFocusable(el)) {
return false;
}
try {
el.focus();
} catch (e) {}
return document.activeElement === el;
},
// Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
focusFirstDescendant(el) {
for (let i = 0; i < el.childNodes.length; i++) {
let child = el.childNodes[i];
if (this.attemptFocus(child) || this.focusFirstDescendant(child)) {
return true;
}
}
return false;
},
// Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
focusLastDescendant(element) {
for (let i = element.childNodes.length - 1; i >= 0; i--) {
let child = element.childNodes[i];
if (this.attemptFocus(child) || this.focusLastDescendant(child)) {
return true;
}
}
return false;
},
}; These are used when defining event listeners at the app.js (root js) in livebeats, there's the following statements within app.js: let routeUpdated = () => {
Focus.focusMain()
}
// Accessible routing
window.addEventListener("phx:page-loading-stop", routeUpdated)
window.addEventListener("phx:js:exec", e => liveSocket.execJS(liveSocket.main.el, e.detail.cmd))
window.addEventListener("js:call", e => e.target[e.detail.call](...e.detail.args))
window.addEventListener("js:focus", e => {
let parent = document.querySelector(e.detail.parent)
if(parent && isVisible(parent)){ e.target.focus() }
})
window.addEventListener("js:focus-closest", e => {
let el = e.target
let sibling = el.nextElementSibling
while(sibling){
if(isVisible(sibling) && Focus.attemptFocus(sibling)){ return }
sibling = sibling.nextElementSibling
}
sibling = el.previousElementSibling
while(sibling){
if(isVisible(sibling) && Focus.attemptFocus(sibling)){ return }
sibling = sibling.previousElementSibling
}
Focus.attemptFocus(el.parent) || Focus.focusMain()
})
window.addEventListener("phx:remove-el", e => document.getElementById(e.detail.id).remove())
// connect if there are any LiveViews on the page
liveSocket.getSocket().onOpen(() => execJS("#connection-status", "js-hide"))
liveSocket.getSocket().onError(() => execJS("#connection-status", "js-show")) Initial / Previous version of describing the probleminitial problem descriptionhere's the problem description: i need a deeper underestanding on how here's my context
Okay now that i've given you the context, here's what I need:
|
This is a big devex upgrade Read the detailed notes on this: Table of Contents ───────────────── 1. [ ] Need to solve [ Problem: need to be able to properly dispatch JS commands from server-side handle-events to client-side-only ] in order to be able to finish wiring these things up, especially for the “navigate::show_discussion” event .. 1. possible solution: js-exec event listener @ app.js to receive JS command from the server ..... 1. How LiveBeats does it .. 2. initial problem description 1 [ ] Need to solve [ Problem: need to be able to properly dispatch JS commands from server-side handle-events to client-side-only ] in order to be able to finish wiring these things up, especially for the “navigate::show_discussion” event ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ 1.1 possible solution: js-exec event listener @ app.js to receive JS command from the server ──────────────────────────────────────────────────────────────────────────────────────────── The following is an outline, I’ve not tried to add in this fix but high chance it’s the correct way to do it, a la LiveBeats. [fly.io blogpost] that demonstrates how a js-exec event listener can be added to the app.js to dispatch client action from js side okay, so [the execJS fucntion, which we can see in livebeats] finally makes sense, and how [livebeats also uses a js event listener] [fly.io blogpost] <https://fly.io/phoenix-files/server-triggered-js/> [the execJS fucntion, which we can see in livebeats] <https://github.com/fly-apps/live_beats/blob/ac9780472e7019af274110a1cf71250a8d40c986/assets/js/app.js#L12> [livebeats also uses a js event listener] <https://github.com/fly-apps/live_beats/blob/ac9780472e7019af274110a1cf71250a8d40c986/assets/js/app.js#L307> 1.1.1 How LiveBeats does it ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ First, livebeats has a set of focus-related helper functions: ┌──── │ let Focus = { │ focusMain() { │ let target = │ document.querySelector("main h1") || document.querySelector("main"); │ if (target) { │ let origTabIndex = target.tabIndex; │ target.tabIndex = -1; │ target.focus(); │ target.tabIndex = origTabIndex; │ } │ }, │ // Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document │ isFocusable(el) { │ if ( │ el.tabIndex > 0 || │ (el.tabIndex === 0 && el.getAttribute("tabIndex") !== null) │ ) { │ return true; │ } │ if (el.disabled) { │ return false; │ } │ │ switch (el.nodeName) { │ case "A": │ return !!el.href && el.rel !== "ignore"; │ case "INPUT": │ return el.type != "hidden" && el.type !== "file"; │ case "BUTTON": │ case "SELECT": │ case "TEXTAREA": │ return true; │ default: │ return false; │ } │ }, │ // Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document │ attemptFocus(el) { │ if (!el) { │ return; │ } │ if (!this.isFocusable(el)) { │ return false; │ } │ try { │ el.focus(); │ } catch (e) {} │ │ return document.activeElement === el; │ }, │ // Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document │ focusFirstDescendant(el) { │ for (let i = 0; i < el.childNodes.length; i++) { │ let child = el.childNodes[i]; │ if (this.attemptFocus(child) || this.focusFirstDescendant(child)) { │ return true; │ } │ } │ return false; │ }, │ // Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document │ focusLastDescendant(element) { │ for (let i = element.childNodes.length - 1; i >= 0; i--) { │ let child = element.childNodes[i]; │ if (this.attemptFocus(child) || this.focusLastDescendant(child)) { │ return true; │ } │ } │ return false; │ }, │ }; └──── These are used when defining event listeners at the app.js (root js) level. in livebeats, there’s the following statements within app.js: ┌──── │ let routeUpdated = () => { │ Focus.focusMain() │ } │ │ // Accessible routing │ window.addEventListener("phx:page-loading-stop", routeUpdated) │ │ window.addEventListener("phx:js:exec", e => liveSocket.execJS(liveSocket.main.el, e.detail.cmd)) │ window.addEventListener("js:call", e => e.target[e.detail.call](...e.detail.args)) │ window.addEventListener("js:focus", e => { │ let parent = document.querySelector(e.detail.parent) │ if(parent && isVisible(parent)){ e.target.focus() } │ }) │ window.addEventListener("js:focus-closest", e => { │ let el = e.target │ let sibling = el.nextElementSibling │ while(sibling){ │ if(isVisible(sibling) && Focus.attemptFocus(sibling)){ return } │ sibling = sibling.nextElementSibling │ } │ sibling = el.previousElementSibling │ while(sibling){ │ if(isVisible(sibling) && Focus.attemptFocus(sibling)){ return } │ sibling = sibling.previousElementSibling │ } │ Focus.attemptFocus(el.parent) || Focus.focusMain() │ }) │ window.addEventListener("phx:remove-el", e => document.getElementById(e.detail.id).remove()) │ │ // connect if there are any LiveViews on the page │ liveSocket.getSocket().onOpen(() => execJS("#connection-status", "js-hide")) │ liveSocket.getSocket().onError(() => execJS("#connection-status", "js-show")) └──── 1.2 initial problem description ─────────────────────────────── here’s the problem description: i need a deeper underestanding on how phx-click events are handled by liveview here’s my context 1. i have built a custom wrapper on the default modal_wrapper() core component that liveview provides 2. i also have a separate, declarative struct-based definition of UI state, in some situations the modal rendering uses some of the boolean flags within such defined UI structs 3. within the core components, there’s some JS commands, they look something like this: ┌──── │ │ ## JS Commands │ │ def show(js \\ %JS{}, selector) do │ JS.show(js, │ to: selector, │ transition: │ {"transition-all transform ease-out duration-300", │ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95", │ "opacity-100 translate-y-0 sm:scale-100"} │ ) │ end │ │ def hide(js \\ %JS{}, selector) do │ JS.hide(js, │ to: selector, │ time: 200, │ transition: │ {"transition-all transform ease-in duration-200", │ "opacity-100 translate-y-0 sm:scale-100", │ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"} │ ) │ end │ │ def show_modal(js \\ %JS{}, id) when is_binary(id) do │ js │ |> JS.show(to: "##{id}") │ |> JS.show( │ to: "##{id}-bg", │ transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"} │ ) │ |> show("##{id}-container") │ |> JS.add_class("overflow-hidden", to: "body") │ |> JS.focus_first(to: "##{id}-content") │ end │ │ def hide_modal(js \\ %JS{}, id) do │ js │ |> JS.hide( │ to: "##{id}-bg", │ transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"} │ ) │ |> hide("##{id}-container") │ |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"}) │ |> JS.remove_class("overflow-hidden", to: "body") │ |> JS.pop_focus() │ end └──── 4. Here’s an example of how the JS commands are used, note how the hide_modal() function is being called ┌──── │ │ │ ~H""" │ <div id="sheaf-creator-container" class="flex flex-col"> │ <.form │ for={%{}} │ phx-submit={CoreComponents.hide_modal(JS.push("sheaf::publish"), @id)} │ phx-target={@event_target} │ class="flex items-center" │ > │ <div class="flex flex-col w-full"> │ <div class="shadow-sm border-1 border-l-2 rounded-lg border-brand"> │ <textarea │ name="body" │ id={"sheaf-creator-form-body-textarea-" <> @id} │ phx-hook="TextareaFocus" │ phx-hook="TextareaAutoResize" │ class="w-full flex-grow focus:outline-none bg-brandExtraLight text-sm placeholder-gray-400 placeholder:font-light │ resize-vertical overflow-auto min-h-[2.5rem] max-h-[8rem] p-2 pt-3 border-tl-4 border-gray-300 rounded-tl-lg rounded-tr-lg transition-shadow duration-200 focus:border-brand focus:ring-0" │ placeholder={@textarea_placeholder} │ /> │ │ <div class="flex justify-between p-2 pt-0 space-x-2 whitespace-nowrap"> │ <div class="w-full text-sm"> │ <label │ for={"sheaf-creator-form-signature-textarea-" <> @id} │ class="italic font-light text-gray-600 whitespace-nowrap" │ > │ Signed by: @ │ </label> │ <input │ type="text" │ name="signature" │ id={"sheaf-creator-form-signature-textarea-" <> @id} │ value={if @session, do: @session.name, else: ""} │ class="font-medium underline p-0 flex-grow focus:outline-none bg-transparent text-sm text-light placeholder-gray-600 border-0 border-b-1 transition-shadow duration-200" │ placeholder={if @session, do: "Session name", else: "Enter your signature..."} │ disabled={not is_nil(@session) and @session.name} │ /> │ </div> │ </div> │ <.collapsible_marks_display │ id={"nested-" <> @id} │ myself={nil} │ marks_target={@event_target} │ sheaf={@draft_sheaf} │ sheaf_ui={@draft_sheaf_ui} │ container_class="rounded-b-lg border-gray-100" │ /> │ </div> │ <!-- start --> │ <div class="flex justify-around space-x-2 pt-4"> │ <button │ type="button" │ phx-click={CoreComponents.hide_modal(@on_cancel_callback, @id)} │ class="whitespace-nowrap text-xs md:text-sm font-semibold flex items-center justify-center p-2 rounded-full border border-gray-400 text-gray-800 bg-transparent hover:bg-gray-100 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-brand focus:ring-opacity-50" │ phx-target={@event_target} │ > │ Go back │ </button> │ <button │ type="submit" │ class="whitespace-nowrap text-xs md:text-sm font-semibold flex items-center justify-center p-2 rounded-full border border-brand text-white bg-brand hover:bg-brand-light transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-brand focus:ring-opacity-50 w-auto" │ phx-target={@event_target} │ phx-value-is_new_thread={false} │ > │ <%= @action_button_text %> │ </button> │ </div> │ <!-- end --> │ </div> │ </.form> │ </div> │ """ │ end │ └──── Okay now that i’ve given you the context, here’s what I need: 1. currently if i want to compose the JS commands with the phx-click events, i can only do it from within the heex templates, like in point 4 i defined earlier. What i really need is to be able to dispatch these JS commands from the server side, when I’m handling the it via the handle events. an example of such a handle event is this ,#+begin_src elixir def handle_event( “navigate::see_discussion”, _, socket ) do IO.inspect(“CHECKPOINT: the discuss context is reached”) send(self(), “ui::toggle_show_sheaf_modal?”) {:noreply, socket} end #+end_src
Also has a bunch of other inflex related things as well