diff --git a/resources/dist/mason.js b/resources/dist/mason.js index 0483324..54fd3e4 100644 --- a/resources/dist/mason.js +++ b/resources/dist/mason.js @@ -1 +1 @@ -function B({key:h,state:y,statePath:d,disabled:p,dblClickToEdit:k,bricks:S=[],previewLayout:g=null,defaultColorMode:u="light",hasColorModeToggle:f=!1}){let n=null,l=[],m=!1,c={x:0,y:0};return{state:y,statePath:d,fullscreen:!1,viewport:"desktop",sidebarOpen:!0,colorMode:f&&localStorage.getItem("mason-color-mode")||u,isUpdatingBrick:!1,isInsertingBrick:!1,previewUrl:null,previewLayout:g,brickPickerOpen:!1,brickPickerBlockIndex:null,brickPickerPosition:"below",undoStack:[],redoStack:[],maxHistorySize:20,isUndoRedoOperation:!1,async init(){this.$nextTick(()=>{n=this.$refs.previewIframe,n&&(this.updatePreview(),n.addEventListener("load",()=>{this.restoreScrollPosition(),this.sendMessageToIframe({type:"setContent",blocks:this.getBlocksFromState(),dblClickToEdit:k,disabled:p}),this.sendMessageToIframe({type:"setColorMode",mode:this.colorMode}),setTimeout(()=>{this.sendMessageToIframe({type:"updateMoveButtons"})},100)}))});let t=e=>{let{type:o,instanceId:r,...i}=e.data;if(!(r&&r!==d))switch(o){case"ready":this.sendMessageToIframe({type:"setContent",blocks:this.getBlocksFromState()});break;case"editBlock":this.handleEditBlock(i);break;case"deleteBlockRequest":this.handleDeleteBlock(i.index);break;case"insertBlockRequest":this.handleInsertBlock(i.brickId,i.position);break;case"updateBlockRequest":this.handleUpdateBlock(i.index,i.brick);break;case"moveBlockRequest":this.handleMoveBlock(i.from,i.to);break;case"openBrickPicker":this.openBrickPicker(i.blockIndex);break;case"keyboardShortcut":(navigator.platform.toUpperCase().indexOf("MAC")>=0?i.metaKey:i.ctrlKey)&&!i.altKey&&(i.key==="z"||i.key==="Z"?i.shiftKey?this.redo():this.undo():(i.key==="y"||i.key==="Y")&&!i.shiftKey&&this.redo());break}};window.addEventListener("message",t),l.push(["message",t]);let s=e=>{let o=document.activeElement;if(o&&(o.tagName==="INPUT"||o.tagName==="TEXTAREA"||o.isContentEditable))return;(navigator.platform.toUpperCase().indexOf("MAC")>=0?e.metaKey:e.ctrlKey)&&!e.altKey&&(e.key==="z"||e.key==="Z"?(e.preventDefault(),e.shiftKey?this.redo():this.undo()):(e.key==="y"||e.key==="Y")&&!e.shiftKey&&(e.preventDefault(),this.redo()))};window.addEventListener("keydown",s),l.push(["keydown",s]),this.$watch("state",()=>{m||(this.updateIframeContent(),setTimeout(()=>{if(n&&n.contentWindow)try{n.contentWindow.postMessage({type:"updateMoveButtons"},"*")}catch{}},200))}),this.setupDragAndDrop()},getBlocksFromState(){if(!this.state)return[];let t=[];try{t=JSON.parse(JSON.stringify(this.state))}catch{Array.isArray(this.state)&&(t=Array.from(this.state))}return Array.isArray(t)?t.filter(s=>s&&s.type==="masonBrick"):[]},sendMessageToIframe(t){n&&n.contentWindow&&n.contentWindow.postMessage({...t,instanceId:d},"*")},updateIframeContent(){this.updatePreview(),this.sendMessageToIframe({type:"setConfig",dblClickToEdit:k,disabled:p})},setupDragAndDrop(){let t=this.$el.querySelector(".mason-editor-wrapper");t&&(t.addEventListener("dragover",s=>{s.preventDefault()}),t.addEventListener("drop",s=>{s.preventDefault();let e=s.dataTransfer.getData("brick");if(e){let r=this.getBlocksFromState().length;this.handleInsertBlock(e,r)}}))},async handleInsertBlock(t,s){this.isInsertingBrick=!0;let e=this.captureState();try{await this.$wire.mountAction("handleBrick",{id:t,dragPosition:s,mode:"insert"},{schemaComponent:h}),this.pushToUndoStack(e)}finally{this.isInsertingBrick=!1}},async handleEditBlock({index:t,brickId:s,config:e}){this.isUpdatingBrick=!0;let o=this.captureState();try{await this.$wire.mountAction("handleBrick",{id:s,config:e,mode:"edit",blockIndex:t},{schemaComponent:h}),this.pushToUndoStack(o)}finally{this.isUpdatingBrick=!1}},handleDeleteBlock(t){let s=this.getBlocksFromState();if(t>=0&&tr!==t);this.updateStateFromBlocks(e)}},handleUpdateBlock(t,s){let e=this.getBlocksFromState();if(t>=0&&ti===t?s:r);this.updateStateFromBlocks(o)}},handleMoveBlock(t,s){let e=this.getBlocksFromState();if(t<0||t>=e.length||s<0||s>e.length||t===s)return;this.pushToUndoStack(this.captureState());let o=e.slice(),r=o[t];o.splice(t,1);let i;s===e.length?i=o.length:s>t?s===t+1?i=s:i=s-1:i=s,i=Math.max(0,Math.min(i,o.length)),o.splice(i,0,r),this.updateStateFromBlocks(o),setTimeout(()=>{this.sendMessageToIframe({type:"updateMoveButtons"})},150)},updateStateFromBlocks(t){let s=t;try{s=JSON.parse(JSON.stringify(t))}catch{Array.isArray(t)&&(s=t.map(r=>{try{return JSON.parse(JSON.stringify(r))}catch{return r}}))}this.state=s;let e=this.statePath;this.$wire.set(e,s,!1)},toggleFullscreen(){this.fullscreen=!this.fullscreen,this.fullscreen||(this.viewport="desktop")},toggleViewport(t){if(this.viewport===t){this.viewport="desktop";return}this.viewport=t},toggleSidebar(){this.sidebarOpen=!this.sidebarOpen},toggleColorMode(){this.colorMode=this.colorMode==="dark"?"light":"dark",f&&localStorage.setItem("mason-color-mode",this.colorMode),this.sendMessageToIframe({type:"setColorMode",mode:this.colorMode})},saveScrollPosition(){if(n&&n.contentWindow)try{c={x:n.contentWindow.scrollX||n.contentWindow.pageXOffset||0,y:n.contentWindow.scrollY||n.contentWindow.pageYOffset||0}}catch{c={x:0,y:0}}},restoreScrollPosition(){if(n&&n.contentWindow&&(c.x>0||c.y>0))try{let t=()=>{if(n&&n.contentWindow){let s=n.contentDocument||n.contentWindow.document;s&&s.readyState==="complete"?n.contentWindow.scrollTo(c.x,c.y):setTimeout(t,50)}};setTimeout(t,0),setTimeout(t,50),setTimeout(t,100)}catch{}},updatePreview(){this.saveScrollPosition();let t=this.getBlocksFromState(),s=t;try{s=JSON.parse(JSON.stringify(t))}catch{s=t}let e=document.createElement("form");e.method="POST",e.action="/mason/preview",e.target=n.name,e.style.display="none";let o=document.createElement("input");o.type="hidden",o.name="blocks",o.value=JSON.stringify(s),e.appendChild(o);let r=document.createElement("input");if(r.type="hidden",r.name="bricks",r.value=JSON.stringify(S),e.appendChild(r),this.previewLayout){let a=document.createElement("input");a.type="hidden",a.name="layout",a.value=this.previewLayout,e.appendChild(a)}let i=document.createElement("input");i.type="hidden",i.name="_token",i.value=document.querySelector('meta[name="csrf-token"]')?.content||"",e.appendChild(i),document.body.appendChild(e),e.submit(),setTimeout(()=>{e.parentNode&&e.parentNode.removeChild(e)},100)},deselectAllBlocks(){this.isUpdatingBrick||this.sendMessageToIframe({type:"deselectAllBlocks"})},openBrickPicker(t){this.brickPickerBlockIndex=t,this.brickPickerOpen=!0},closeBrickPicker(){this.brickPickerOpen=!1,this.brickPickerBlockIndex=null,this.brickPickerPosition="below"},insertFromPicker(t){let s=this.brickPickerPosition==="above"?this.brickPickerBlockIndex:this.brickPickerBlockIndex+1;this.handleInsertBlock(t,s),this.closeBrickPicker()},captureState(){return JSON.parse(JSON.stringify(this.state||[]))},pushToUndoStack(t){this.isUndoRedoOperation||(this.undoStack.push(t),this.undoStack.length>this.maxHistorySize&&this.undoStack.shift(),this.redoStack=[])},undo(){if(this.undoStack.length===0)return;this.redoStack.push(this.captureState()),this.redoStack.length>this.maxHistorySize&&this.redoStack.shift();let t=this.undoStack.pop();this.isUndoRedoOperation=!0,this.updateStateFromBlocks(t),this.isUndoRedoOperation=!1},redo(){if(this.redoStack.length===0)return;this.undoStack.push(this.captureState()),this.undoStack.length>this.maxHistorySize&&this.undoStack.shift();let t=this.redoStack.pop();this.isUndoRedoOperation=!0,this.updateStateFromBlocks(t),this.isUndoRedoOperation=!1},canUndo(){return this.undoStack.length>0},canRedo(){return this.redoStack.length>0},clearAllBlocks(){this.state&&this.state.length>0&&this.pushToUndoStack(this.captureState()),this.state=[],this.updateStateFromBlocks([])},destroy(){m=!0,l.forEach(([t,s])=>{window.removeEventListener(t,s)}),l=[],n=null}}}export{B as default}; +function B({key:p,state:y,statePath:d,disabled:u,dblClickToEdit:k,bricks:S=[],previewLayout:g=null,defaultColorMode:m="light",hasColorModeToggle:f=!1}){let o=null,l=[],h=!1,c={x:0,y:0};return{state:y,statePath:d,fullscreen:!1,viewport:"desktop",sidebarOpen:!0,colorMode:f&&localStorage.getItem("mason-color-mode")||m,isUpdatingBrick:!1,isInsertingBrick:!1,previewUrl:null,previewLayout:g,brickPickerOpen:!1,brickPickerBlockIndex:null,brickPickerPosition:"below",undoStack:[],redoStack:[],maxHistorySize:20,isUndoRedoOperation:!1,async init(){let e=t=>{let{type:n,instanceId:r,...i}=t.data;if(!(r&&r!==d))switch(n){case"ready":this.sendMessageToIframe({type:"setContent",blocks:this.getBlocksFromState()});break;case"editBlock":this.handleEditBlock(i);break;case"deleteBlockRequest":this.handleDeleteBlock(i.index);break;case"insertBlockRequest":this.handleInsertBlock(i.brickId,i.position);break;case"updateBlockRequest":this.handleUpdateBlock(i.index,i.brick);break;case"moveBlockRequest":this.handleMoveBlock(i.from,i.to);break;case"openBrickPicker":this.openBrickPicker(i.blockIndex);break;case"keyboardShortcut":(navigator.platform.toUpperCase().indexOf("MAC")>=0?i.metaKey:i.ctrlKey)&&!i.altKey&&(i.key==="z"||i.key==="Z"?i.shiftKey?this.redo():this.undo():(i.key==="y"||i.key==="Y")&&!i.shiftKey&&this.redo());break}};window.addEventListener("message",e),l.push(["message",e]);let s=t=>{let n=document.activeElement;if(n&&(n.tagName==="INPUT"||n.tagName==="TEXTAREA"||n.isContentEditable))return;(navigator.platform.toUpperCase().indexOf("MAC")>=0?t.metaKey:t.ctrlKey)&&!t.altKey&&(t.key==="z"||t.key==="Z"?(t.preventDefault(),t.shiftKey?this.redo():this.undo()):(t.key==="y"||t.key==="Y")&&!t.shiftKey&&(t.preventDefault(),this.redo()))};window.addEventListener("keydown",s),l.push(["keydown",s]),this.$watch("state",()=>{h||(this.updateIframeContent(),setTimeout(()=>{if(o&&o.contentWindow)try{o.contentWindow.postMessage({type:"updateMoveButtons"},"*")}catch{}},200))}),this.setupDragAndDrop()},initPreviewIframe(e){h||!e||e._masonPreviewBound||(e._masonPreviewBound=!0,o=e,e.addEventListener("load",()=>{this.restoreScrollPosition(),this.sendMessageToIframe({type:"setContent",blocks:this.getBlocksFromState(),dblClickToEdit:k,disabled:u}),this.sendMessageToIframe({type:"setColorMode",mode:this.colorMode}),setTimeout(()=>{this.sendMessageToIframe({type:"updateMoveButtons"})},100)}),this.$nextTick(()=>this.updatePreview()))},getBlocksFromState(){if(!this.state)return[];let e=[];try{e=JSON.parse(JSON.stringify(this.state))}catch{Array.isArray(this.state)&&(e=Array.from(this.state))}return Array.isArray(e)?e.filter(s=>s&&s.type==="masonBrick"):[]},sendMessageToIframe(e){o&&o.contentWindow&&o.contentWindow.postMessage({...e,instanceId:d},"*")},updateIframeContent(){this.updatePreview(),this.sendMessageToIframe({type:"setConfig",dblClickToEdit:k,disabled:u})},setupDragAndDrop(){let e=this.$el.querySelector(".mason-editor-wrapper");e&&(e.addEventListener("dragover",s=>{s.preventDefault()}),e.addEventListener("drop",s=>{s.preventDefault();let t=s.dataTransfer.getData("brick");if(t){let r=this.getBlocksFromState().length;this.handleInsertBlock(t,r)}}))},async handleInsertBlock(e,s){this.isInsertingBrick=!0;let t=this.captureState();try{await this.$wire.mountAction("handleBrick",{id:e,dragPosition:s,mode:"insert"},{schemaComponent:p}),this.pushToUndoStack(t)}finally{this.isInsertingBrick=!1}},async handleEditBlock({index:e,brickId:s,config:t}){this.isUpdatingBrick=!0;let n=this.captureState();try{await this.$wire.mountAction("handleBrick",{id:s,config:t,mode:"edit",blockIndex:e},{schemaComponent:p}),this.pushToUndoStack(n)}finally{this.isUpdatingBrick=!1}},handleDeleteBlock(e){let s=this.getBlocksFromState();if(e>=0&&er!==e);this.updateStateFromBlocks(t)}},handleUpdateBlock(e,s){let t=this.getBlocksFromState();if(e>=0&&ei===e?s:r);this.updateStateFromBlocks(n)}},handleMoveBlock(e,s){let t=this.getBlocksFromState();if(e<0||e>=t.length||s<0||s>t.length||e===s)return;this.pushToUndoStack(this.captureState());let n=t.slice(),r=n[e];n.splice(e,1);let i;s===t.length?i=n.length:s>e?s===e+1?i=s:i=s-1:i=s,i=Math.max(0,Math.min(i,n.length)),n.splice(i,0,r),this.updateStateFromBlocks(n),setTimeout(()=>{this.sendMessageToIframe({type:"updateMoveButtons"})},150)},updateStateFromBlocks(e){let s=e;try{s=JSON.parse(JSON.stringify(e))}catch{Array.isArray(e)&&(s=e.map(r=>{try{return JSON.parse(JSON.stringify(r))}catch{return r}}))}this.state=s;let t=this.statePath;this.$wire.set(t,s,!1)},toggleFullscreen(){this.fullscreen=!this.fullscreen,this.fullscreen||(this.viewport="desktop")},toggleViewport(e){if(this.viewport===e){this.viewport="desktop";return}this.viewport=e},toggleSidebar(){this.sidebarOpen=!this.sidebarOpen},toggleColorMode(){this.colorMode=this.colorMode==="dark"?"light":"dark",f&&localStorage.setItem("mason-color-mode",this.colorMode),this.sendMessageToIframe({type:"setColorMode",mode:this.colorMode})},saveScrollPosition(){if(o&&o.contentWindow)try{c={x:o.contentWindow.scrollX||o.contentWindow.pageXOffset||0,y:o.contentWindow.scrollY||o.contentWindow.pageYOffset||0}}catch{c={x:0,y:0}}},restoreScrollPosition(){if(o&&o.contentWindow&&(c.x>0||c.y>0))try{let e=()=>{if(o&&o.contentWindow){let s=o.contentDocument||o.contentWindow.document;s&&s.readyState==="complete"?o.contentWindow.scrollTo(c.x,c.y):setTimeout(e,50)}};setTimeout(e,0),setTimeout(e,50),setTimeout(e,100)}catch{}},updatePreview(){if(!o)return;this.saveScrollPosition();let e=this.getBlocksFromState(),s=e;try{s=JSON.parse(JSON.stringify(e))}catch{s=e}let t=document.createElement("form");t.method="POST",t.action="/mason/preview",t.target=o.name,t.style.display="none";let n=document.createElement("input");n.type="hidden",n.name="blocks",n.value=JSON.stringify(s),t.appendChild(n);let r=document.createElement("input");if(r.type="hidden",r.name="bricks",r.value=JSON.stringify(S),t.appendChild(r),this.previewLayout){let a=document.createElement("input");a.type="hidden",a.name="layout",a.value=this.previewLayout,t.appendChild(a)}let i=document.createElement("input");i.type="hidden",i.name="_token",i.value=document.querySelector('meta[name="csrf-token"]')?.content||"",t.appendChild(i),document.body.appendChild(t),t.submit(),setTimeout(()=>{t.parentNode&&t.parentNode.removeChild(t)},100)},deselectAllBlocks(){this.isUpdatingBrick||this.sendMessageToIframe({type:"deselectAllBlocks"})},openBrickPicker(e){this.brickPickerBlockIndex=e,this.brickPickerOpen=!0},closeBrickPicker(){this.brickPickerOpen=!1,this.brickPickerBlockIndex=null,this.brickPickerPosition="below"},insertFromPicker(e){let s=this.brickPickerPosition==="above"?this.brickPickerBlockIndex:this.brickPickerBlockIndex+1;this.handleInsertBlock(e,s),this.closeBrickPicker()},captureState(){return JSON.parse(JSON.stringify(this.state||[]))},pushToUndoStack(e){this.isUndoRedoOperation||(this.undoStack.push(e),this.undoStack.length>this.maxHistorySize&&this.undoStack.shift(),this.redoStack=[])},undo(){if(this.undoStack.length===0)return;this.redoStack.push(this.captureState()),this.redoStack.length>this.maxHistorySize&&this.redoStack.shift();let e=this.undoStack.pop();this.isUndoRedoOperation=!0,this.updateStateFromBlocks(e),this.isUndoRedoOperation=!1},redo(){if(this.redoStack.length===0)return;this.undoStack.push(this.captureState()),this.undoStack.length>this.maxHistorySize&&this.undoStack.shift();let e=this.redoStack.pop();this.isUndoRedoOperation=!0,this.updateStateFromBlocks(e),this.isUndoRedoOperation=!1},canUndo(){return this.undoStack.length>0},canRedo(){return this.redoStack.length>0},clearAllBlocks(){this.state&&this.state.length>0&&this.pushToUndoStack(this.captureState()),this.state=[],this.updateStateFromBlocks([])},destroy(){h=!0,l.forEach(([e,s])=>{window.removeEventListener(e,s)}),l=[],o&&delete o._masonPreviewBound,o=null}}}export{B as default}; diff --git a/resources/js/mason.js b/resources/js/mason.js index 557bf3c..c8acb1a 100644 --- a/resources/js/mason.js +++ b/resources/js/mason.js @@ -38,42 +38,6 @@ export default function masonComponent({ isUndoRedoOperation: false, async init() { - // Wait for iframe to load - this.$nextTick(() => { - iframe = this.$refs.previewIframe - - if (iframe) { - // Load initial content via form submission - this.updatePreview() - - iframe.addEventListener('load', () => { - // Restore scroll position after iframe loads - this.restoreScrollPosition() - - // Send postMessage once the iframe is loaded with config - this.sendMessageToIframe({ - type: 'setContent', - blocks: this.getBlocksFromState(), - dblClickToEdit: dblClickToEdit, - disabled: disabled, - }) - - // Send initial color mode to iframe - this.sendMessageToIframe({ - type: 'setColorMode', - mode: this.colorMode, - }) - - // Update move buttons after iframe loads - setTimeout(() => { - this.sendMessageToIframe({ - type: 'updateMoveButtons', - }) - }, 100) - }) - } - }) - // Listen for messages from iframe const messageHandler = (event) => { // Security: verify origin if needed @@ -190,6 +154,47 @@ export default function masonComponent({ this.setupDragAndDrop() }, + // Called from x-init on the iframe itself, so that if the element is ever + // replaced (browser extensions injecting nodes can defeat Livewire's morph + // matching) the new element is bound instead of leaving a stale reference. + initPreviewIframe(el) { + if (isDestroyed || !el || el._masonPreviewBound) { + return + } + + el._masonPreviewBound = true + iframe = el + + el.addEventListener('load', () => { + // Restore scroll position after iframe loads + this.restoreScrollPosition() + + // Send postMessage once the iframe is loaded with config + this.sendMessageToIframe({ + type: 'setContent', + blocks: this.getBlocksFromState(), + dblClickToEdit: dblClickToEdit, + disabled: disabled, + }) + + // Send initial color mode to iframe + this.sendMessageToIframe({ + type: 'setColorMode', + mode: this.colorMode, + }) + + // Update move buttons after iframe loads + setTimeout(() => { + this.sendMessageToIframe({ + type: 'updateMoveButtons', + }) + }, 100) + }) + + // Load initial content via form submission + this.$nextTick(() => this.updatePreview()) + }, + getBlocksFromState() { if (!this.state) { return [] @@ -496,6 +501,10 @@ export default function masonComponent({ }, updatePreview() { + if (!iframe) { + return + } + // Save scroll position before updating this.saveScrollPosition() @@ -653,6 +662,11 @@ export default function masonComponent({ }) eventListeners = [] + if (iframe) { + // Allow a future init to rebind if the element outlives this component + delete iframe._masonPreviewBound + } + iframe = null }, } diff --git a/resources/views/mason.blade.php b/resources/views/mason.blade.php index 5774364..0aa3076 100644 --- a/resources/views/mason.blade.php +++ b/resources/views/mason.blade.php @@ -71,18 +71,19 @@ class="mason-wrapper" ]) >
class([ - "mason-input-wrapper", + "mason-editor-wrapper", ]) }} >