app/javascript/panda/cms/editor/editor_js_initializer.js in panda-cms-0.7.0 vs app/javascript/panda/cms/editor/editor_js_initializer.js in panda-cms-0.7.2
- old
+ new
@@ -49,237 +49,286 @@
try {
// First load EditorJS core
const editorCore = EDITOR_JS_RESOURCES[0]
await ResourceLoader.loadScript(this.document, this.document.head, editorCore)
- // Then load all tools in parallel
- const toolLoads = EDITOR_JS_RESOURCES.slice(1).map(async (resource) => {
- await ResourceLoader.loadScript(this.document, this.document.head, resource)
- })
+ // Wait for EditorJS to be available
+ await this.waitForEditorJS()
// Load CSS directly
await ResourceLoader.embedCSS(this.document, this.document.head, EDITOR_JS_CSS)
- // Wait for all resources to load
- await Promise.all(toolLoads)
+ // Then load all tools sequentially to ensure proper initialization order
+ for (const resource of EDITOR_JS_RESOURCES.slice(1)) {
+ try {
+ await ResourceLoader.loadScript(this.document, this.document.head, resource)
+ // Extract tool name from resource URL
+ const toolName = resource.split('/').pop().split('@')[0]
+ // Wait for tool to be initialized
+ const toolClass = await this.waitForTool(toolName)
- // Wait for EditorJS to be available
- await this.waitForEditorJS()
+ // If this is the nested-list tool, also make it available as 'list'
+ if (toolName === 'nested-list') {
+ const win = this.document.defaultView || window
+ win.List = toolClass
+ }
+
+ console.debug(`[Panda CMS] Successfully loaded tool: ${toolName}`)
+ } catch (error) {
+ console.error(`[Panda CMS] Failed to load tool: ${resource}`, error)
+ throw error
+ }
+ }
+
+ console.debug('[Panda CMS] All tools successfully loaded and verified')
} catch (error) {
+ console.error('[Panda CMS] Error loading Editor.js resources:', error)
throw error
}
}
- async initializeEditor(element, initialData = {}, editorId = null) {
- // Generate a consistent holder ID if not provided
- const holderId = editorId || `editor-${element.id || Math.random().toString(36).substr(2, 9)}`
-
- // Create or find the holder element in the correct document context
- let holderElement = this.document.getElementById(holderId)
- if (!holderElement) {
- // Create the holder element in the correct document context
- holderElement = this.document.createElement('div')
- holderElement.id = holderId
- holderElement.className = 'editor-js-holder codex-editor'
-
- // Append to the element and force a reflow
- element.appendChild(holderElement)
- void holderElement.offsetHeight // Force a reflow
+ async waitForEditorJS(timeout = 10000) {
+ console.debug('[Panda CMS] Waiting for EditorJS core...')
+ const start = Date.now()
+ while (Date.now() - start < timeout) {
+ if (typeof this.document.defaultView.EditorJS === 'function') {
+ console.debug('[Panda CMS] EditorJS core is ready')
+ return
+ }
+ await new Promise(resolve => setTimeout(resolve, 100))
}
+ throw new Error('[Panda CMS] Timeout waiting for EditorJS')
+ }
- // Verify the holder element exists in the correct document context
- const verifyHolder = this.document.getElementById(holderId)
- if (!verifyHolder) {
- throw new Error(`Failed to create editor holder element ${holderId}`)
+ /**
+ * Wait for a specific tool to be available in window context
+ */
+ async waitForTool(toolName, timeout = 10000) {
+ if (!toolName) {
+ console.error('[Panda CMS] Invalid tool name provided')
+ return null
}
- // Clear any existing content in the holder
- holderElement.innerHTML = ''
+ // Clean up tool name to handle npm package format
+ const cleanToolName = toolName.split('/').pop().replace('@', '')
- // Add source to initial data
- if (initialData && !initialData.source) {
- initialData.source = "editorJS"
+ const toolMapping = {
+ 'paragraph': 'Paragraph',
+ 'header': 'Header',
+ 'nested-list': 'NestedList',
+ 'list': 'NestedList',
+ 'quote': 'Quote',
+ 'simple-image': 'SimpleImage',
+ 'table': 'Table',
+ 'embed': 'Embed'
}
- // Get the base config but pass our document context
- const config = getEditorConfig(holderId, initialData, this.document)
+ const globalToolName = toolMapping[cleanToolName] || cleanToolName
+ const start = Date.now()
- // Override specific settings for iframe context
- const editorConfig = {
- ...config,
- holder: holderElement, // Use element reference instead of ID
- minHeight: 1, // Prevent auto-height issues in iframe
- autofocus: false, // Prevent focus issues
- logLevel: 'ERROR', // Only show errors
- tools: {
- ...config.tools,
- // Ensure tools use the correct window context
- paragraph: { ...config.tools.paragraph, class: this.document.defaultView.Paragraph },
- header: { ...config.tools.header, class: this.document.defaultView.Header },
- list: { ...config.tools.list, class: this.document.defaultView.NestedList },
- quote: { ...config.tools.quote, class: this.document.defaultView.Quote },
- table: { ...config.tools.table, class: this.document.defaultView.Table },
- image: { ...config.tools.image, class: this.document.defaultView.SimpleImage },
- embed: { ...config.tools.embed, class: this.document.defaultView.Embed }
+ while (Date.now() - start < timeout) {
+ const win = this.document.defaultView || window
+ if (win[globalToolName] && typeof win[globalToolName] === 'function') {
+ // If this is the NestedList tool, make it available as both list and nested-list
+ if (globalToolName === 'NestedList') {
+ win.List = win[globalToolName]
+ }
+ console.debug(`[Panda CMS] Successfully loaded tool: ${globalToolName}`)
+ return win[globalToolName]
}
+ await new Promise(resolve => setTimeout(resolve, 100))
}
+ throw new Error(`[Panda CMS] Timeout waiting for tool: ${cleanToolName} (${globalToolName})`)
+ }
- // Create editor instance directly
- const editor = new this.document.defaultView.EditorJS({
- ...editorConfig,
- onReady: () => {
- // Store the editor instance globally for testing
- if (this.withinIFrame) {
- this.document.defaultView.editor = editor
- } else {
- window.editor = editor
- }
+ async initializeEditor(element, initialData = {}, editorId = null) {
+ try {
+ // Wait for EditorJS core to be available with increased timeout
+ await this.waitForEditorJS(15000)
- // Mark editor as ready
- editor.isReady = true
+ // Get the window context (either iframe or parent)
+ const win = this.document.defaultView || window
- // Force redraw of toolbar and blocks
- setTimeout(async () => {
- try {
- const toolbar = holderElement.querySelector('.ce-toolbar')
- const blockWrapper = holderElement.querySelector('.ce-block')
+ // Create a unique ID for this editor instance if not provided
+ const uniqueId = editorId || `editor-${Math.random().toString(36).substring(2)}`
- if (!toolbar || !blockWrapper) {
- // Clear and insert a new block to force UI update
- await editor.blocks.clear()
- await editor.blocks.insert('paragraph')
+ // Check if editor already exists
+ const existingEditor = element.querySelector('.codex-editor')
+ if (existingEditor) {
+ console.debug('[Panda CMS] Editor already exists, cleaning up...')
+ existingEditor.remove()
+ }
- // Force a redraw by toggling display
- holderElement.style.display = 'none'
- void holderElement.offsetHeight
- holderElement.style.display = ''
- }
+ // Create a holder div for the editor
+ const holder = this.document.createElement("div")
+ holder.id = uniqueId
+ holder.classList.add("editor-js-holder")
+ element.appendChild(holder)
- // Call the ready hook if it exists
- if (typeof window.onEditorJSReady === 'function') {
- window.onEditorJSReady(editor)
- }
- } catch (error) {
- console.error('Error during editor redraw:', error)
- }
- }, 100)
- },
- onChange: async (api, event) => {
- try {
- // Save the current editor data
- const outputData = await api.saver.save()
- outputData.source = "editorJS"
- const contentJson = JSON.stringify(outputData)
-
- if (!this.withinIFrame) {
- // For form-based editors, update the hidden input
- const form = element.closest('[data-controller="editor-form"]')
- if (form) {
- const hiddenInput = form.querySelector('[data-editor-form-target="hiddenField"]')
- if (hiddenInput) {
- hiddenInput.value = contentJson
- hiddenInput.dataset.initialContent = contentJson
- hiddenInput.dispatchEvent(new Event('change', { bubbles: true }))
+ // Process initial data to handle list items and other content
+ let processedData = initialData
+ if (initialData.blocks) {
+ processedData = {
+ ...initialData,
+ blocks: initialData.blocks.map(block => {
+ if (block.type === 'list' && block.data && Array.isArray(block.data.items)) {
+ return {
+ ...block,
+ data: {
+ ...block.data,
+ items: block.data.items.map(item => {
+ // Handle both string items and object items
+ if (typeof item === 'string') {
+ return {
+ content: item,
+ items: []
+ }
+ } else if (item.content) {
+ return {
+ content: item.content,
+ items: Array.isArray(item.items) ? item.items : []
+ }
+ } else {
+ return {
+ content: String(item),
+ items: []
+ }
+ }
+ })
+ }
}
}
- } else {
- // For iframe-based editors, update the element's data attribute
- element.setAttribute('data-content', contentJson)
- element.dispatchEvent(new Event('change', { bubbles: true }))
+ return block
+ })
+ }
+ }
- // Get the save button from parent window
- const saveButton = parent.document.getElementById('saveEditableButton')
- if (saveButton) {
- // Store the current content on the save button for later use
- saveButton.dataset.pendingContent = contentJson
+ console.debug('[Panda CMS] Processed initial data:', processedData)
- // Add click handler if not already added
- if (!saveButton.hasAttribute('data-handler-attached')) {
- saveButton.setAttribute('data-handler-attached', 'true')
- saveButton.addEventListener('click', async () => {
- try {
- const pageId = element.getAttribute("data-editable-page-id")
- const blockContentId = element.getAttribute("data-editable-block-content-id")
- const pendingContent = JSON.parse(saveButton.dataset.pendingContent || '{}')
-
- const response = await fetch(`${this.adminPathValue}/pages/${pageId}/block_contents/${blockContentId}`, {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json",
- "X-CSRF-Token": this.csrfToken
- },
- body: JSON.stringify({ content: pendingContent })
- })
-
- if (!response.ok) {
- throw new Error('Save failed')
- }
-
- // Clear pending content after successful save
- delete saveButton.dataset.pendingContent
- } catch (error) {
- console.error('Error saving content:', error)
- }
- })
- }
+ // Create editor configuration
+ const config = {
+ holder: holder,
+ data: processedData,
+ placeholder: 'Click to start writing...',
+ tools: {
+ paragraph: {
+ class: win.Paragraph,
+ inlineToolbar: true,
+ config: {
+ preserveBlank: true,
+ placeholder: 'Click to start writing...'
}
+ },
+ header: {
+ class: win.Header,
+ inlineToolbar: true,
+ config: {
+ placeholder: 'Enter a header',
+ levels: [1, 2, 3, 4, 5, 6],
+ defaultLevel: 2
+ }
+ },
+ 'list': { // Register as list instead of nested-list
+ class: win.NestedList,
+ inlineToolbar: true,
+ config: {
+ defaultStyle: 'unordered',
+ enableLineBreaks: true
+ }
+ },
+ quote: {
+ class: win.Quote,
+ inlineToolbar: true,
+ config: {
+ quotePlaceholder: 'Enter a quote',
+ captionPlaceholder: 'Quote\'s author'
+ }
}
- } catch (error) {
- console.error('Error in onChange handler:', error)
+ },
+ onChange: (api, event) => {
+ console.debug('[Panda CMS] Editor content changed:', { api, event })
+ // Save content to data attributes
+ api.saver.save().then((outputData) => {
+ const jsonString = JSON.stringify(outputData)
+ element.dataset.editablePreviousData = btoa(jsonString)
+ element.dataset.editableContent = jsonString
+ element.dataset.editableInitialized = 'true'
+ })
+ },
+ onReady: () => {
+ console.debug('[Panda CMS] Editor ready with data:', processedData)
+ element.dataset.editableInitialized = 'true'
+ holder.editorInstance = editor
+ },
+ onError: (error) => {
+ console.error('[Panda CMS] Editor error:', error)
+ element.dataset.editableInitialized = 'false'
+ throw error
}
}
- })
- // Store editor instance on the holder element to maintain reference
- holderElement.editorInstance = editor
+ // Remove any undefined tools from the config
+ config.tools = Object.fromEntries(
+ Object.entries(config.tools)
+ .filter(([_, value]) => value?.class !== undefined)
+ )
- if (!this.withinIFrame) {
- // Store the editor instance on the controller element for potential future reference
- const form = element.closest('[data-controller="editor-form"]')
- if (form) {
- form.editorInstance = editor
- }
- } else {
- // For iframe editors, store the instance on the element itself
- element.editorInstance = editor
- }
+ console.debug('[Panda CMS] Creating editor with config:', config)
- // Return a promise that resolves when the editor is ready
- return new Promise((resolve, reject) => {
- const timeout = setTimeout(() => {
- reject(new Error('Editor initialization timed out'))
- }, 30000)
+ // Create editor instance with extended timeout
+ return new Promise((resolve, reject) => {
+ try {
+ // Add timeout for initialization
+ const timeoutId = setTimeout(() => {
+ reject(new Error('Editor initialization timeout'))
+ }, 15000) // Increased to 15 seconds
- const checkReady = () => {
- if (editor.isReady) {
- clearTimeout(timeout)
- resolve(editor)
- } else {
- setTimeout(checkReady, 100)
- }
- }
- checkReady()
- })
- }
+ // Create editor instance with onReady callback
+ const editor = new win.EditorJS({
+ ...config,
+ onReady: () => {
+ console.debug('[Panda CMS] Editor ready with data:', processedData)
+ clearTimeout(timeoutId)
+ holder.editorInstance = editor
+ element.dataset.editableInitialized = 'true'
+ resolve(editor)
+ },
+ onChange: (api, event) => {
+ console.debug('[Panda CMS] Editor content changed:', { api, event })
+ // Save content to data attributes
+ api.saver.save().then((outputData) => {
+ const jsonString = JSON.stringify(outputData)
+ element.dataset.editablePreviousData = btoa(jsonString)
+ element.dataset.editableContent = jsonString
+ })
+ },
+ onError: (error) => {
+ console.error('[Panda CMS] Editor error:', error)
+ element.dataset.editableInitialized = 'false'
+ clearTimeout(timeoutId)
+ reject(error)
+ }
+ })
- /**
- * Wait for EditorJS core to be available in window
- */
- async waitForEditorJS() {
- let attempts = 0
- const maxAttempts = 30 // 3 seconds with 100ms intervals
-
- await new Promise((resolve, reject) => {
- const check = () => {
- attempts++
- if (window.EditorJS) {
- resolve()
- } else if (attempts >= maxAttempts) {
- reject(new Error('EditorJS core failed to load'))
- } else {
- setTimeout(check, 100)
+ // Add error handler
+ editor.isReady
+ .then(() => {
+ console.debug('[Panda CMS] Editor is ready')
+ element.dataset.editableInitialized = 'true'
+ })
+ .catch((error) => {
+ console.error('[Panda CMS] Editor failed to initialize:', error)
+ element.dataset.editableInitialized = 'false'
+ clearTimeout(timeoutId)
+ reject(error)
+ })
+ } catch (error) {
+ element.dataset.editableInitialized = 'false'
+ reject(error)
}
- }
- check()
- })
+ })
+ } catch (error) {
+ console.error('[Panda CMS] Error initializing editor:', error)
+ throw error
+ }
}
}