Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

phoenix_live_view

Package Overview
Dependencies
Maintainers
1
Versions
116
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

phoenix_live_view - npm Package Compare versions

Comparing version 0.20.3 to 0.20.4

4

assets/js/phoenix_live_view/constants.js

@@ -8,3 +8,4 @@ export const CONSECUTIVE_RELOADS = "consecutive-reloads"

"phx-click-loading", "phx-change-loading", "phx-submit-loading",
"phx-keydown-loading", "phx-keyup-loading", "phx-blur-loading", "phx-focus-loading"
"phx-keydown-loading", "phx-keyup-loading", "phx-blur-loading", "phx-focus-loading",
"phx-hook-loading"
]

@@ -41,2 +42,3 @@ export const PHX_COMPONENT = "data-phx-component"

export const PHX_FEEDBACK_FOR = "feedback-for"
export const PHX_FEEDBACK_GROUP = "feedback-group"
export const PHX_HAS_FOCUSED = "phx-has-focused"

@@ -43,0 +45,0 @@ export const FOCUSABLE_INPUTS = ["text", "textarea", "number", "email", "password", "search", "tel", "url", "date", "time", "datetime-local", "color", "range"]

@@ -5,2 +5,3 @@ import {

PHX_FEEDBACK_FOR,
PHX_FEEDBACK_GROUP,
PHX_PRUNE,

@@ -51,2 +52,3 @@ PHX_ROOT_ID,

this.streamInserts = {}
this.streamComponentRestore = {}
this.targetCID = targetCID

@@ -76,3 +78,2 @@ this.cidPatch = isCid(this.targetCID)

let phxUpdate = this.liveSocket.binding(PHX_UPDATE)
DOM.all(this.container, `[${phxUpdate}=${PHX_STREAM}]`, el => el.innerHTML = "")
DOM.all(this.container, `[${phxUpdate}=append] > *, [${phxUpdate}=prepend] > *`, el => {

@@ -92,2 +93,3 @@ el.setAttribute(PHX_PRUNE, "")

let phxFeedbackFor = liveSocket.binding(PHX_FEEDBACK_FOR)
let phxFeedbackGroup = liveSocket.binding(PHX_FEEDBACK_GROUP)
let disableWith = liveSocket.binding(PHX_DISABLE_WITH)

@@ -98,3 +100,3 @@ let phxViewportTop = liveSocket.binding(PHX_VIEWPORT_TOP)

let added = []
let trackedInputs = []
let trackedForms = new Set()
let updates = []

@@ -110,12 +112,8 @@ let appendPrependUpdates = []

this.streams.forEach(([ref, inserts, deleteIds, reset]) => {
Object.entries(inserts).forEach(([key, [streamAt, limit]]) => {
this.streamInserts[key] = {ref, streamAt, limit, resetKept: false}
inserts.forEach(([key, streamAt, limit]) => {
this.streamInserts[key] = {ref, streamAt, limit, reset}
})
if(reset !== undefined){
DOM.all(container, `[${PHX_STREAM_REF}="${ref}"]`, child => {
if(inserts[child.id]){
this.streamInserts[child.id].resetKept = true
} else {
this.removeStreamChildElement(child)
}
this.removeStreamChildElement(child)
})

@@ -129,2 +127,17 @@ }

// clear stream items from the dead render if they are not inserted again
if(isJoinPatch){
DOM.all(this.container, `[${phxUpdate}=${PHX_STREAM}]`, el => {
// make sure to only remove elements owned by the current view
// see https://github.com/phoenixframework/phoenix_live_view/issues/3047
this.liveSocket.owner(el, (view) => {
if(view === this.view){
Array.from(el.children).forEach(child => {
this.removeStreamChildElement(child)
})
}
})
})
}
morphdom(targetContainer, html, {

@@ -143,7 +156,15 @@ childrenOnly: targetContainer.getAttribute(PHX_COMPONENT) === null,

addChild: (parent, child) => {
let {ref, streamAt, limit} = this.getStreamInsert(child)
if(ref === undefined) { return parent.appendChild(child) }
let {ref, streamAt} = this.getStreamInsert(child)
if(ref === undefined){ return parent.appendChild(child) }
DOM.putSticky(child, PHX_STREAM_REF, el => el.setAttribute(PHX_STREAM_REF, ref))
this.setStreamRef(child, ref)
// we may need to restore skipped components, see removeStreamChildElement
child.querySelectorAll(`[${PHX_MAGIC_ID}][${PHX_SKIP}]`).forEach(el => {
const component = this.streamComponentRestore[el.getAttribute(PHX_MAGIC_ID)]
if(component){
el.replaceWith(component)
}
})
// streaming

@@ -158,15 +179,2 @@ if(streamAt === 0){

}
let children = limit !== null && Array.from(parent.children)
let childrenToRemove = []
if(limit && limit < 0 && children.length > limit * -1){
childrenToRemove = children.slice(0, children.length + limit)
} else if(limit && limit >= 0 && children.length > limit){
childrenToRemove = children.slice(limit)
}
childrenToRemove.forEach(removeChild => {
// do not remove child as part of limit if we are re-adding it
if(!this.streamInserts[removeChild.id]){
this.removeStreamChildElement(removeChild)
}
})
},

@@ -192,3 +200,3 @@ onBeforeNodeAdded: (el) => {

if(el.getAttribute && el.getAttribute("name") && DOM.isFormInput(el)){
trackedInputs.push(el)
trackedForms.add(el.form)
}

@@ -201,15 +209,2 @@ // nested view handling

},
onBeforeElChildrenUpdated: (fromEl, toEl) => {
// before we update the children, we need to set existing stream children
// into the new order from the server if they were kept during a stream reset
if(fromEl.getAttribute(phxUpdate) === PHX_STREAM){
let toIds = Array.from(toEl.children).map(child => child.id)
Array.from(fromEl.children).filter(child => {
let {resetKept} = this.getStreamInsert(child)
return resetKept
}).forEach((child) => {
this.streamInserts[child.id].streamAt = toIds.indexOf(child.id)
})
}
},
onNodeDiscarded: (el) => this.onNodeDiscarded(el),

@@ -237,3 +232,7 @@ onBeforeNodeDiscarded: (el) => {

DOM.cleanChildNodes(toEl, phxUpdate)
if(this.skipCIDSibling(toEl)){ return false }
if(this.skipCIDSibling(toEl)){
// if this is a live component used in a stream, we may need to reorder it
this.maybeReOrderStream(fromEl)
return false
}
if(DOM.isPhxSticky(fromEl)){ return false }

@@ -271,3 +270,5 @@ if(DOM.isIgnored(fromEl, phxUpdate) || (fromEl.form && fromEl.form.isSameNode(externalFormTriggered))){

let isFocusedFormEl = focused && fromEl.isSameNode(focused) && DOM.isFormInput(fromEl)
if(isFocusedFormEl && fromEl.type !== "hidden"){
// skip patching focused inputs unless focus is a select that has changed options
let focusedSelectChanged = isFocusedFormEl && this.isChangedSelect(fromEl, toEl)
if(isFocusedFormEl && fromEl.type !== "hidden" && !focusedSelectChanged){
this.trackBefore("updated", fromEl, toEl)

@@ -278,5 +279,7 @@ DOM.mergeFocusedInput(fromEl, toEl)

DOM.applyStickyOperations(fromEl)
trackedInputs.push(fromEl)
trackedForms.add(fromEl.form)
return false
} else {
// blur focused select if it changed so native UI is updated (ie safari won't update visible options)
if(focusedSelectChanged){ fromEl.blur() }
if(DOM.isPhxUpdate(toEl, phxUpdate, ["append", "prepend"])){

@@ -289,3 +292,3 @@ appendPrependUpdates.push(new DOMPostMorphRestorer(fromEl, toEl, toEl.getAttribute(phxUpdate)))

if(toEl.getAttribute("name") && DOM.isFormInput(toEl)){
trackedInputs.push(toEl)
trackedForms.add(toEl.form)
}

@@ -307,3 +310,3 @@ this.trackBefore("updated", fromEl, toEl)

DOM.maybeHideFeedback(targetContainer, trackedInputs, phxFeedbackFor)
DOM.maybeHideFeedback(targetContainer, trackedForms, phxFeedbackFor, phxFeedbackGroup)

@@ -343,2 +346,9 @@ liveSocket.silenceEvents(() => DOM.restoreFocus(focused, selectionStart, selectionEnd))

if(!this.maybePendingRemove(child)){
if(this.streamInserts[child.id]){
// we need to store children so we can restore them later
// in case they are skipped
child.querySelectorAll(`[${PHX_MAGIC_ID}]`).forEach(el => {
this.streamComponentRestore[el.getAttribute(PHX_MAGIC_ID)] = el
})
}
child.remove()

@@ -354,9 +364,18 @@ this.onNodeDiscarded(child)

setStreamRef(el, ref){
DOM.putSticky(el, PHX_STREAM_REF, el => el.setAttribute(PHX_STREAM_REF, ref))
}
maybeReOrderStream(el, isNew){
let {ref, streamAt, limit} = this.getStreamInsert(el)
if(streamAt === undefined || (streamAt === 0 && !isNew)){ return }
let {ref, streamAt, reset} = this.getStreamInsert(el)
if(streamAt === undefined){ return }
// we need to the PHX_STREAM_REF here as well as addChild is invoked only for parents
DOM.putSticky(el, PHX_STREAM_REF, el => el.setAttribute(PHX_STREAM_REF, ref))
// we need to set the PHX_STREAM_REF here as well as addChild is invoked only for parents
this.setStreamRef(el, ref)
if(!reset && !isNew){
// we only reorder if the element is new or it's a stream reset
return
}
if(streamAt === 0){

@@ -378,4 +397,16 @@ el.parentElement.insertBefore(el, el.parentElement.firstElementChild)

}
this.maybeLimitStream(el)
}
maybeLimitStream(el){
let {limit} = this.getStreamInsert(el)
let children = limit !== null && Array.from(el.parentElement.children)
if(limit && limit < 0 && children.length > limit * -1){
children.slice(0, children.length + limit).forEach(child => this.removeStreamChildElement(child))
} else if(limit && limit >= 0 && children.length > limit){
children.slice(limit).forEach(child => this.removeStreamChildElement(child))
}
}
transitionPendingRemoves(){

@@ -396,2 +427,17 @@ let {pendingRemoves, liveSocket} = this

isChangedSelect(fromEl, toEl){
if(!(fromEl instanceof HTMLSelectElement) || fromEl.multiple){ return false }
if(fromEl.options.length !== toEl.options.length){ return true }
let fromSelected = fromEl.selectedOptions[0]
let toSelected = toEl.selectedOptions[0]
if(fromSelected && fromSelected.hasAttribute("selected")){
toSelected.setAttribute("selected", fromSelected.getAttribute("selected"))
}
// in general we have to be very careful with using isEqualNode as it does not a reliable
// DOM tree equality check, but for selection attributes and options it works fine
return !fromEl.isEqualNode(toEl)
}
isCIDPatch(){ return this.cidPatch }

@@ -398,0 +444,0 @@

@@ -53,3 +53,7 @@ import {

findUploadInputs(node){ return this.all(node, `input[type="file"][${PHX_UPLOAD_REF}]`) },
findUploadInputs(node){
const formId = node.id
const inputsOutsideForm = this.all(document, `input[type="file"][${PHX_UPLOAD_REF}][form="${formId}"]`)
return this.all(node, `input[type="file"][${PHX_UPLOAD_REF}]`).concat(inputsOutsideForm)
},

@@ -256,3 +260,6 @@ findComponentNodeList(node, cid){

if(this.once(el, "bind-debounce")){
el.addEventListener("blur", () => this.triggerCycle(el, DEBOUNCE_TRIGGER))
el.addEventListener("blur", () => {
// always trigger callback on blur
callback()
})
}

@@ -290,10 +297,37 @@ }

maybeHideFeedback(container, inputs, phxFeedbackFor){
maybeHideFeedback(container, forms, phxFeedbackFor, phxFeedbackGroup){
let feedbacks = []
inputs.forEach(input => {
if(!(this.private(input, PHX_HAS_FOCUSED) || this.private(input, PHX_HAS_SUBMITTED))){
feedbacks.push(input.name)
if(input.name.endsWith("[]")){ feedbacks.push(input.name.slice(0, -2)) }
// if there are multiple inputs with the same name
// (for example the default checkbox renders a hidden input as well)
// we must only add the no feedback class if none of them have been focused yet
let inputNamesFocused = {}
// an entry in this object will be true if NO input in the group has been focused yet
let feedbackGroups = {}
forms.forEach(form => {
Array.from(form.elements).forEach(input => {
const group = input.getAttribute(phxFeedbackGroup)
// initialize the group to true if it doesn't exist
if(group && !(group in feedbackGroups)){ feedbackGroups[group] = true }
// initialize the focused state to false if it doesn't exist
if(!(input.name in inputNamesFocused)){ inputNamesFocused[input.name] = false }
if(this.private(input, PHX_HAS_FOCUSED) || this.private(input, PHX_HAS_SUBMITTED)){
inputNamesFocused[input.name] = true
// the input was focused, therefore the group will NOT get phx-no-feedback
if(group){ feedbackGroups[group] = false }
}
})
})
for(const [name, focused] of Object.entries(inputNamesFocused)){
if(!focused){
feedbacks.push(name)
if(name.endsWith("[]")){ feedbacks.push(name.slice(0, -2)) }
}
})
}
for(const [group, noFeedback] of Object.entries(feedbackGroups)){
if(noFeedback) feedbacks.push(group)
}
if(feedbacks.length > 0){

@@ -335,2 +369,6 @@ let selector = feedbacks.map(f => `[${phxFeedbackFor}="${f}"]`).join(", ")

isChildOfAny(el, parents){
return !!parents.find(parent => parent.contains(el))
},
firstPhxChild(el){

@@ -341,3 +379,8 @@ return this.isPhxChild(el) ? el : this.all(el, `[${PHX_PARENT_ID}]`)[0]

dispatchEvent(target, name, opts = {}){
let bubbles = opts.bubbles === undefined ? true : !!opts.bubbles
let defaultBubble = true
let isUploadTarget = target.nodeName === "INPUT" && target.type === "file"
if(isUploadTarget && name === "click"){
defaultBubble = false
}
let bubbles = opts.bubbles === undefined ? defaultBubble : !!opts.bubbles
let eventOpts = {bubbles: bubbles, cancelable: true, detail: opts.detail || {}}

@@ -358,4 +401,7 @@ let event = name === "click" ? new MouseEvent("click", eventOpts) : new CustomEvent(name, eventOpts)

// merge attributes from source to target
// if an element is ignored, we only merge data attributes
// including removing data attributes that are no longer in the source
mergeAttrs(target, source, opts = {}){
let exclude = opts.exclude || []
let exclude = new Set(opts.exclude || [])
let isIgnored = opts.isIgnored

@@ -365,3 +411,20 @@ let sourceAttrs = source.attributes

let name = sourceAttrs[i].name
if(exclude.indexOf(name) < 0){ target.setAttribute(name, source.getAttribute(name)) }
if(!exclude.has(name)){
const sourceValue = source.getAttribute(name)
if(target.getAttribute(name) !== sourceValue && (!isIgnored || (isIgnored && name.startsWith("data-")))){
target.setAttribute(name, sourceValue)
}
} else {
// We exclude the value from being merged on focused inputs, because the
// user's input should always win.
// We can still assign it as long as the value property is the same, though.
// This prevents a situation where the updated hook is not being triggered
// when an input is back in its "original state", because the attribute
// was never changed, see:
// https://github.com/phoenixframework/phoenix_live_view/issues/2163
if(name === "value" && target.value === source.value){
// actually set the value attribute to sync it with the value property
target.setAttribute("value", source.getAttribute(name))
}
}
}

@@ -381,3 +444,4 @@

mergeFocusedInput(target, source){
DOM.mergeAttrs(target, source, {exclude: ["value"]})
// skip selects because FF will reset highlighted index for any setAttribute
if(!(target instanceof HTMLSelectElement)){ DOM.mergeAttrs(target, source, {exclude: ["value"]}) }

@@ -396,3 +460,5 @@ if(source.readOnly){

restoreFocus(focused, selectionStart, selectionEnd){
if(focused instanceof HTMLSelectElement){ focused.focus() }
if(!DOM.isTextualInput(focused)){ return }
let wasFocused = focused.matches(":focus")

@@ -399,0 +465,0 @@ if(focused.readOnly){ focused.blur() }

@@ -60,18 +60,49 @@ import {

let scrollTop = () => document.documentElement.scrollTop || document.body.scrollTop
let winHeight = () => window.innerHeight || document.documentElement.clientHeight
let findScrollContainer = (el) => {
if(["scroll", "auto"].indexOf(getComputedStyle(el).overflowY) >= 0) return el
if(document.documentElement === el) return null
return findScrollContainer(el.parentElement)
}
let isAtViewportTop = (el) => {
let scrollTop = (scrollContainer) => {
if(scrollContainer){
return scrollContainer.scrollTop
} else {
return document.documentElement.scrollTop || document.body.scrollTop
}
}
let bottom = (scrollContainer) => {
if(scrollContainer){
return scrollContainer.getBoundingClientRect().bottom
} else {
// when we have no container, the whole page scrolls,
// therefore the bottom coordinate is the viewport height
return window.innerHeight || document.documentElement.clientHeight
}
}
let top = (scrollContainer) => {
if(scrollContainer){
return scrollContainer.getBoundingClientRect().top
} else {
// when we have no container the whole page scrolls,
// therefore the top coordinate is 0
return 0
}
}
let isAtViewportTop = (el, scrollContainer) => {
let rect = el.getBoundingClientRect()
return rect.top >= 0 && rect.left >= 0 && rect.top <= winHeight()
return rect.top >= top(scrollContainer) && rect.left >= 0 && rect.top <= bottom(scrollContainer)
}
let isAtViewportBottom = (el) => {
let isAtViewportBottom = (el, scrollContainer) => {
let rect = el.getBoundingClientRect()
return rect.right >= 0 && rect.left >= 0 && rect.bottom <= winHeight()
return rect.right >= top(scrollContainer) && rect.left >= 0 && rect.bottom <= bottom(scrollContainer)
}
let isWithinViewport = (el) => {
let isWithinViewport = (el, scrollContainer) => {
let rect = el.getBoundingClientRect()
return rect.top >= 0 && rect.left >= 0 && rect.top <= winHeight()
return rect.top >= top(scrollContainer) && rect.left >= 0 && rect.top <= bottom(scrollContainer)
}

@@ -81,3 +112,4 @@

mounted(){
let scrollBefore = scrollTop()
this.scrollContainer = findScrollContainer(this.el)
let scrollBefore = scrollTop(this.scrollContainer)
let topOverran = false

@@ -98,3 +130,8 @@ let throttleInterval = 500

pendingOp = null
if(!isWithinViewport(firstChild)){ firstChild.scrollIntoView({block: "start"}) }
// make sure that the DOM is patched by waiting for the next tick
window.requestAnimationFrame(() => {
if(!isWithinViewport(firstChild, this.scrollContainer)){
firstChild.scrollIntoView({block: "start"})
}
})
})

@@ -107,8 +144,13 @@ })

pendingOp = null
if(!isWithinViewport(lastChild)){ lastChild.scrollIntoView({block: "end"}) }
// make sure that the DOM is patched by waiting for the next tick
window.requestAnimationFrame(() => {
if(!isWithinViewport(lastChild, this.scrollContainer)){
lastChild.scrollIntoView({block: "end"})
}
})
})
})
this.onScroll = (e) => {
let scrollNow = scrollTop()
this.onScroll = (_e) => {
let scrollNow = scrollTop(this.scrollContainer)

@@ -135,5 +177,5 @@ if(pendingOp){

if(topEvent && isScrollingUp && isAtViewportTop(firstChild)){
if(topEvent && isScrollingUp && isAtViewportTop(firstChild, this.scrollContainer)){
onFirstChildAtTop(topEvent, firstChild)
} else if(bottomEvent && isScrollingDown && isAtViewportBottom(lastChild)){
} else if(bottomEvent && isScrollingDown && isAtViewportBottom(lastChild, this.scrollContainer)){
onLastChildAtBottom(bottomEvent, lastChild)

@@ -143,5 +185,17 @@ }

}
window.addEventListener("scroll", this.onScroll)
if(this.scrollContainer){
this.scrollContainer.addEventListener("scroll", this.onScroll)
} else {
window.addEventListener("scroll", this.onScroll)
}
},
destroyed(){ window.removeEventListener("scroll", this.onScroll) },
destroyed(){
if(this.scrollContainer){
this.scrollContainer.removeEventListener("scroll", this.onScroll)
} else {
window.removeEventListener("scroll", this.onScroll)
}
},

@@ -148,0 +202,0 @@ throttle(interval, callback){

@@ -5,2 +5,3 @@ import DOM from "./dom"

let focusStack = null
let default_transition_time = 200

@@ -42,3 +43,3 @@ let JS = {

exec_exec(eventType, phxEvent, view, sourceEl, el, [attr, to]){
exec_exec(eventType, phxEvent, view, sourceEl, el, {attr, to}){
let nodes = to ? DOM.all(document, to) : [sourceEl]

@@ -118,2 +119,20 @@ nodes.forEach(node => {

exec_toggle_attr(eventType, phxEvent, view, sourceEl, el, {attr: [attr, val1, val2]}){
if(el.hasAttribute(attr)){
if(val2 !== undefined){
// toggle between val1 and val2
if(el.getAttribute(attr) === val1){
this.setOrRemoveAttrs(el, [[attr, val2]], [])
} else {
this.setOrRemoveAttrs(el, [[attr, val1]], [])
}
} else {
// remove attr
this.setOrRemoveAttrs(el, [], [attr])
}
} else {
this.setOrRemoveAttrs(el, [[attr, val1]], [])
}
},
exec_transition(eventType, phxEvent, view, sourceEl, el, {time, transition}){

@@ -158,2 +177,3 @@ this.addOrRemoveClasses(el, [], [], transition, time, view)

toggle(eventType, view, el, display, ins, outs, time){
time = time || default_transition_time
let [inClasses, inStartClasses, inEndClasses] = ins || [[], [], []]

@@ -221,2 +241,3 @@ let [outClasses, outStartClasses, outEndClasses] = outs || [[], [], []]

addOrRemoveClasses(el, adds, removes, transition, time, view){
time = time || default_transition_time
let [transitionRun, transitionStart, transitionEnd] = transition || [[], [], []]

@@ -253,5 +274,5 @@ if(transitionRun.length > 0){

let alteredAttrs = sets.map(([attr, _val]) => attr).concat(removes);
let newSets = prevSets.filter(([attr, _val]) => !alteredAttrs.includes(attr)).concat(sets);
let newRemoves = prevRemoves.filter((attr) => !alteredAttrs.includes(attr)).concat(removes);
let alteredAttrs = sets.map(([attr, _val]) => attr).concat(removes)
let newSets = prevSets.filter(([attr, _val]) => !alteredAttrs.includes(attr)).concat(sets)
let newRemoves = prevRemoves.filter((attr) => !alteredAttrs.includes(attr)).concat(removes)

@@ -258,0 +279,0 @@ DOM.putSticky(el, "attrs", currentEl => {

@@ -109,3 +109,2 @@ /** Initializes the LiveSocket

debug,
isObject,
maybe

@@ -404,3 +403,3 @@ } from "./utils"

this.main.setRedirect(href)
this.transitionRemoves()
this.transitionRemoves(null, true)
this.main.join((joinCount, onDone) => {

@@ -412,3 +411,3 @@ if(joinCount === 1 && this.commitPendingLink(linkRef)){

this.outgoingMainEl = null
callback && requestAnimationFrame(() => callback(linkRef))
callback && callback(linkRef)
onDone()

@@ -420,5 +419,10 @@ })

transitionRemoves(elements){
transitionRemoves(elements, skipSticky){
let removeAttr = this.binding("remove")
elements = elements || DOM.all(document, `[${removeAttr}]`)
if(skipSticky){
const stickies = DOM.findPhxSticky(document) || []
elements = elements.filter(el => !DOM.isChildOfAny(el, stickies))
}
elements.forEach(el => {

@@ -638,2 +642,5 @@ this.execJS(el, el.getAttribute(removeAttr), "remove")

} else {
// a synthetic click event (detail 0) will not have caused a mousedown event,
// therefore the clickStartedAtTarget is stale
if(e.detail === 0) this.clickStartedAtTarget = e.target
let clickStartedAtTarget = this.clickStartedAtTarget || e.target

@@ -667,3 +674,3 @@ target = closestPhxBinding(clickStartedAtTarget, click)

if(!(el.isSameNode(clickStartedAt) || el.contains(clickStartedAt))){
this.withinOwners(e.target, view => {
this.withinOwners(el, view => {
let phxEvent = el.getAttribute(phxClickAway)

@@ -736,3 +743,3 @@ if(JS.isVisible(el) && JS.isInViewport(el)){

maybeScroll(scroll) {
maybeScroll(scroll){
if(typeof(scroll) === "number"){

@@ -760,3 +767,3 @@ requestAnimationFrame(() => {

pushHistoryPatch(href, linkState, targetEl){
if(!this.isConnected()){ return Browser.redirect(href) }
if(!this.isConnected() || !this.main.isMain()){ return Browser.redirect(href) }

@@ -780,4 +787,5 @@ this.withPageLoading({to: href, kind: "patch"}, done => {

historyRedirect(href, linkState, flash){
if(!this.isConnected() || !this.main.isMain()){ return Browser.redirect(href, flash) }
// convert to full href if only path prefix
if(!this.isConnected()){ return Browser.redirect(href, flash) }
if(/^\/$|^\/[^\/]+.*$/.test(href)){

@@ -885,6 +893,8 @@ let {protocol, host} = window.location

let input = Array.from(form.elements).find(el => el.type === "reset")
// wait until next tick to get updated input value
window.requestAnimationFrame(() => {
input.dispatchEvent(new Event("input", {bubbles: true, cancelable: false}))
})
if(input){
// wait until next tick to get updated input value
window.requestAnimationFrame(() => {
input.dispatchEvent(new Event("input", {bubbles: true, cancelable: false}))
})
}
})

@@ -891,0 +901,0 @@ }

@@ -97,5 +97,9 @@ import {

static filesAwaitingPreflight(input){
return this.activeFiles(input).filter(f => !UploadEntry.isPreflighted(input, f))
return this.activeFiles(input).filter(f => !UploadEntry.isPreflighted(input, f) && !UploadEntry.isPreflightInProgress(f))
}
static markPreflightInProgress(entries){
entries.forEach(entry => UploadEntry.markPreflightInProgress(entry.file))
}
constructor(inputEl, view, onComplete){

@@ -108,2 +112,5 @@ this.view = view

// prevent sending duplicate preflight requests
LiveUploader.markPreflightInProgress(this._entries)
this.numEntriesInProgress = this._entries.length

@@ -110,0 +117,0 @@ }

@@ -158,2 +158,10 @@ import {

resetRender(cid){
// we are racing a component destroy, it could not exist, so
// make sure that we don't try to set reset on undefined
if(this.rendered[COMPONENTS][cid]){
this.rendered[COMPONENTS][cid].reset = true
}
}
mergeDiff(diff){

@@ -290,3 +298,3 @@ let newc = diff[COMPONENTS]

// It is disabled for comprehensions since we must re-render the entire collection
// and no invidial element is tracked inside the comprehension.
// and no individual element is tracked inside the comprehension.
toOutputBuffer(rendered, templates, output, changeTracking, rootAttrs = {}){

@@ -300,2 +308,4 @@ if(rendered[DYNAMICS]){ return this.comprehensionToBuffer(rendered, templates, output) }

// this condition is called when first rendering an optimizable function component.
// LC have their magicId previously set
if(changeTracking && isRoot && !rendered.magicId){

@@ -319,4 +329,8 @@ rendered.newRender = true

let attrs
if(changeTracking || Object.keys(rootAttrs).length > 0){
skip = !rendered.newRender
// when a LC is added on the page, we need to re-render the entire LC tree,
// therefore changeTracking is false; however, we need to keep all the magicIds
// from any function component so the next time the LC is updated, we can apply
// the skip optimization
if(changeTracking || rendered.magicId){
skip = changeTracking && !rendered.newRender
attrs = {[PHX_MAGIC_ID]: rendered.magicId, ...rootAttrs}

@@ -326,3 +340,3 @@ } else {

}
if(skip){ attrs[PHX_SKIP] = true}
if(skip){ attrs[PHX_SKIP] = true }
let [newRoot, commentBefore, commentAfter] = modifyRoot(output.buffer, attrs, skip)

@@ -384,3 +398,3 @@ rendered.newRender = false

// works in the same PHX_SKIP attribute fashion as 1, but the newRender tracking is done
// at the general diff merge level. If we merge a diff with new dynamics, we necessariy have
// at the general diff merge level. If we merge a diff with new dynamics, we necessarily have
// experienced a change which must be a newRender, and thus we can't skip the render.

@@ -390,6 +404,14 @@ //

// we track a deterministic magicId based on the cid.
//
// By default changeTracking is enabled, but we special case the flow where the client is pruning
// cids and the server adds the component back. In such cases, we explicitly disable changeTracking
// with resetRender for this cid, then re-enable it after the recursive call to skip the optimization
// for the entire component tree.
component.newRender = !skip
component.magicId = `${this.parentViewId()}-c-${cid}`
let changeTracking = true
// enable change tracking as long as the component hasn't been reset
let changeTracking = !component.reset
let [html, streams] = this.recursiveToString(component, components, onlyCids, changeTracking, attrs)
// disable reset after we've rendered
delete component.reset

@@ -396,0 +418,0 @@ return [html, streams]

@@ -18,5 +18,6 @@ import {

let isNew = file._phxRef === undefined
let isPreflightInProgress = UploadEntry.isPreflightInProgress(file)
let activeRefs = fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(",")
let isActive = activeRefs.indexOf(LiveUploader.genFileRef(file)) >= 0
return file.size > 0 && (isNew || isActive)
return file.size > 0 && (isNew || isActive || !isPreflightInProgress)
}

@@ -30,2 +31,10 @@

static isPreflightInProgress(file){
return file._preflightInProgress === true
}
static markPreflightInProgress(file){
file._preflightInProgress = true
}
constructor(fileEl, file, view){

@@ -32,0 +41,0 @@ this.ref = LiveUploader.genFileRef(file)

@@ -59,14 +59,24 @@ import {

let serializeForm = (form, metadata, onlyNames = []) => {
let {submitter, ...meta} = metadata
const {submitter, ...meta} = metadata
// TODO: Replace with `new FormData(form, submitter)` when supported by latest browsers,
// and mention `formdata-submitter-polyfill` in the docs.
let formData = new FormData(form)
// TODO: Remove when FormData constructor supports the submitter argument.
if(submitter && submitter.hasAttribute("name") && submitter.form && submitter.form === form){
formData.append(submitter.name, submitter.value)
// We must inject the submitter in the order that it exists in the DOM
// releative to other inputs. For example, for checkbox groups, the order must be maintained.
let injectedElement
if(submitter && submitter.name){
const input = document.createElement("input")
input.type = "hidden"
// set the form attribute if the submitter has one;
// this can happen if the element is outside the actual form element
const formId = submitter.getAttribute("form")
if(formId){
input.setAttribute("form", form)
}
input.name = submitter.name
input.value = submitter.value
submitter.parentElement.insertBefore(input, submitter)
injectedElement = input
}
let toRemove = []
const formData = new FormData(form)
const toRemove = []

@@ -80,3 +90,4 @@ formData.forEach((val, key, _index) => {

let params = new URLSearchParams()
const params = new URLSearchParams()
for(let [key, val] of formData.entries()){

@@ -87,2 +98,9 @@ if(onlyNames.length === 0 || onlyNames.indexOf(key) >= 0){

}
// remove the injected element again
// (it would be removed by the next dom patch anyway, but this is cleaner)
if(submitter && injectedElement){
submitter.parentElement.removeChild(injectedElement)
}
for(let metaKey in meta){ params.append(metaKey, meta[metaKey]) }

@@ -309,2 +327,5 @@

if(phxStatic){ toEl.setAttribute(PHX_STATIC, phxStatic) }
// set PHX_ROOT_ID to prevent events from being dispatched to the root view
// while the child join is still pending
if(fromEl){ fromEl.setAttribute(PHX_ROOT_ID, this.root.id) }
return this.joinChild(toEl)

@@ -760,2 +781,3 @@ })

let disabledVal = el.getAttribute(PHX_DISABLED)
let readOnlyVal = el.getAttribute(PHX_READONLY)
// remove refs

@@ -765,4 +787,4 @@ el.removeAttribute(PHX_REF)

// restore inputs
if(el.getAttribute(PHX_READONLY) !== null){
el.readOnly = false
if(readOnlyVal !== null){
el.readOnly = readOnlyVal === "true" ? true : false
el.removeAttribute(PHX_READONLY)

@@ -807,2 +829,4 @@ }

if(disableText !== ""){ el.innerText = disableText }
// PHX_DISABLED could have already been set in disableForm
el.setAttribute(PHX_DISABLED, el.getAttribute(PHX_DISABLED) || el.disabled)
el.setAttribute("disabled", "")

@@ -908,2 +932,3 @@ }

let meta = this.extractMeta(inputEl.form)
if(inputEl instanceof HTMLButtonElement){ meta.submitter = inputEl }
if(inputEl.getAttribute(this.binding("change"))){

@@ -1154,3 +1179,3 @@ formData = serializeForm(inputEl.form, {_target: opts._target, ...meta}, [inputEl.name])

// which result in an invalid css selector otherwise.
const phxChangeValue = form.getAttribute(phxChange).replaceAll(/([\[\]"])/g, '\\$1')
const phxChangeValue = CSS.escape(form.getAttribute(phxChange))
let newForm = template.content.querySelector(`form[id="${form.id}"][${phxChange}="${phxChangeValue}"]`)

@@ -1168,13 +1193,14 @@ if(newForm){

maybePushComponentsDestroyed(destroyedCIDs){
let willDestroyCIDs = destroyedCIDs.filter(cid => {
let willDestroyCIDs = destroyedCIDs.concat(this.pruningCIDs).filter(cid => {
return DOM.findComponentNodeList(this.el, cid).length === 0
})
// make sure this is a copy and not a reference
this.pruningCIDs = willDestroyCIDs.concat([])
if(willDestroyCIDs.length > 0){
this.pruningCIDs.push(...willDestroyCIDs)
// we must reset the render change tracking for cids that
// could be added back from the server so we don't skip them
willDestroyCIDs.forEach(cid => this.rendered.resetRender(cid))
this.pushWithReply(null, "cids_will_destroy", {cids: willDestroyCIDs}, () => {
// The cids are either back on the page or they will be fully removed,
// so we can remove them from the pruningCIDs.
this.pruningCIDs = this.pruningCIDs.filter(cid => willDestroyCIDs.indexOf(cid) !== -1)
// See if any of the cids we wanted to destroy were added back,

@@ -1188,2 +1214,3 @@ // if they were added back, we don't actually destroy them.

this.pushWithReply(null, "cids_destroyed", {cids: completelyDestroyCIDs}, (resp) => {
this.pruningCIDs = this.pruningCIDs.filter(cid => resp.cids.indexOf(cid) === -1)
this.rendered.pruneCIDs(resp.cids)

@@ -1190,0 +1217,0 @@ })

{
"name": "phoenix_live_view",
"version": "0.20.3",
"version": "0.20.4",
"description": "The Phoenix LiveView JavaScript client.",

@@ -13,13 +13,15 @@ "license": "MIT",

"dependencies": {
"morphdom": "2.7.1"
"morphdom": "2.7.2"
},
"devDependencies": {
"@babel/cli": "7.14.3",
"@babel/core": "7.14.3",
"@babel/preset-env": "7.14.2",
"eslint": "7.27.0",
"eslint-plugin-jest": "24.3.6",
"jest": "^27.0.1",
"phoenix": "1.5.9"
"@babel/cli": "7.23.4",
"@babel/core": "7.23.7",
"@babel/preset-env": "7.23.8",
"css.escape": "^1.5.1",
"eslint": "8.56.0",
"eslint-plugin-jest": "27.6.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"phoenix": "1.7.10"
}
}
{
"name": "phoenix_live_view",
"version": "0.20.3",
"version": "0.20.4",
"description": "The Phoenix LiveView JavaScript client.",

@@ -25,3 +25,10 @@ "license": "MIT",

"assets/js/phoenix_live_view/*"
]
],
"devDependencies": {
"@playwright/test": "^1.41.0"
},
"scripts": {
"e2e:server": "MIX_ENV=e2e mix run test/e2e/test_helper.exs",
"e2e:test": "mix assets.build && cd test/e2e && npx playwright test"
}
}

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc