UNPKG

@tooltwist/vue-contentservice

Version:
943 lines (804 loc) 30.1 kB
/* * Keep details of the Element being displayed * in the properties pane. */ //import { sanitizeLayout, safeJson } from '~/lib/Tooltwist.js' // import { sanitizeLayout, safeJson, layoutRoot, layoutChanged } from '../lib/hierarchy' //export const namespaced = true const CLEAN = '' const DIRTY = 'waiting to save' const SAVING = 'saving' const SAVED = 'your changes have been saved' const ERROR = 'warning: your changes have not been saved' const SAVE_INTERVAL = 2000 // Handle returned by setTimeout. let saveTimer = null // initial state //export const state = () => ({ export const state = () => { return { // Edit mode mode: 'view', // { view | edit | layout | debug }} previousEditMode: 'edit', extraDebug: false, dragging: false, // We call it 'contentId' here, but this is actually a Crowdhound 'anchor'. contentId: null, // An element from Crowdhound with the layout as JSON in the description field. // This is only set if we were given a contentId. // If it is null we don't save here. crowdhoundElement: null, // Currently loaded layout. // = hierarchy of element Objects layout: null, // Element currently being edited // = element Object //propertyElement: null, pathToSelectedElement: [ ], // elements, from root to currently selected element // Element with properties being shown. // This will be within 'pathToSelectedElement' expandedElement: null, // Message shown at top of screen selectError: '', saveMsg: CLEAN, // Triple pane stuff showLeftPane: true, showRightPane: true, // Refresh is activated by incrementing a counter refreshCounter: 1 } } //}) /******************************************** * * GETTERS * ********************************************/ export const getters = { propertyElement: (state) => { if (state.pathToSelectedElement) { let len = state.pathToSelectedElement.length let element = (len > 0) ? state.pathToSelectedElement[len - 1] : null console.log(`propertyElement=`, element) return element } return null }, layoutAsJson: (state) => { return safeJson(state.layout, false/*not compressed*/) // let json = JSON.stringify(state.layout, jsonReplacerCallback, 4); // return json; }, propertyElementAsJson: (state) => { if (state.pathToSelectedElement && state.pathToSelectedElement.length > 0) { let element = state.pathToSelectedElement[state.pathToSelectedElement - 1] return safeJson(element, false/*not compressed*/) } return null }, getPathToElement: (state) => (elementId) => { console.log(`contentLayoutStore.getPathToElement(${elementId})`); let path = trackDownElementInLayout(state, elementId) return path } } /******************************************** * * ACTIONS * ********************************************/ // see https://vuex.vuejs.org/guide/actions.html export const actions = { setContentAction ({ commit, state }, { vm, type, layout, contentId}) { // console.log(`In Action contentLayout/setContentAction(type=${type}, layout=${layout?'yes':'no'}, contentId=${contentId})`) if (layout) { commit('setLayout', { vm, layout: layout, crowdhoundElement: null, editable: false }) } else if (contentId) { loadLayoutFromAnchor(commit, vm, contentId) } else { console.error(`setContent should be passed either contentId or layout`) } }, // Delete an element from the current layout. deleteElementAction({ commit, state }, { vm, element}) { console.log('Action contentLayout/deleteElementAction()', element) // Ok, let's do it commit('deleteElementMutation', { vm, element }) // Start the timer, to save after a short delay rememberToSave(commit, state, vm) }, // insertChildAction', { vm: this, element, child: newchild, position: -1 }) insertLayoutAction({ commit, state }, { vm, parent, position, layout}) { console.log('Action contentLayout/insertLayoutAction() parent=', parent) console.log('Action contentLayout/insertLayoutAction() position=', position) console.log('Action contentLayout/insertLayoutAction() layout=', layout) console.log(`state=`, state) console.log(`ok 1`) let data switch (typeof(layout)) { case 'string': // Parse the layout console.log(`ok 1a`) try { data = JSON.parse(layout); console.log(`ok 1b`) } catch(e) { console.log(`ok 1c`) console.error('Error while pasting:', e); console.log(`ok 1d`) return handleError(vm, `Invalid paste object (not JSON).`) } break; case 'object': console.log(`ok 1e`) data = layout break; default: console.log(`ok 1f`, typeof(layout)) return handleError(vm, `Invalid paste object`) } console.log(`ok 2`) console.log(`data=`, data) // Do basic checks, to ensure something unrelated isn't being pasted if (data.type !== 'contentservice.io') { return handleError(`Invalid paste object (not from contentservice.io).`) } console.log(`ok 3`) // Check the data, according to the version let toInsert if (data.version === '1.0') { // Check it complies to version 1.0 if (typeof(data.layout) === 'undefined') { return handleError(`Invalid paste object (missing layout).`) } toInsert = data.layout } else { return handleError(`Invalid paste object (unknown version ${data.version}).`) } console.log(`ok 4`) // Check we have all the required dependencies //ZZZZ console.log(`Will insert `, toInsert) console.log(`Position is`, position) // Ok, let's do it commit('insertLayoutMutation', { vm, parent, position, layout: toInsert }) // Start the timer, to save after a short delay rememberToSave(commit, state, vm) }, // Reposition a child within an element. moveChildAction({ commit, state }, { vm, parent, from, to}) { console.log(`Action contentLayout/moveChildAction(${from}, ${to})`, parent) // Ok, let's do it commit('moveChildMutation', { vm, parent, from, to }) // Start the timer, to save after a short delay rememberToSave(commit, state, vm) }, setPropertyAction ({ commit, state }, { vm, element, name, value, save }) { console.log(`action setPropertyAction(${element}, ${name}, ${value}, save=${save})`) /* * Two possibilities here: * 1. We are trying to update a fixed layout - error. * 2. We are updating an element within a layout - save the layout. */ // Check that we are updating an element in a layout that is being edited. if (state.crowdhoundElement === null) { // Should not be trying to update this. } commit('updateElementPropertyMutation', { vm, element, name, value }) //ZZZZ Timer should probably be set in the mutation? // There is a potential timing problem. // - the commit might not be instantaneous (is that correct), so // a timeout created here might run before the commit occurs. commit('setSaveMsg', { msg: DIRTY }) // Start the timer, to save after a short delay if (typeof(save) === 'Boolean' && !save) { // Skip saving. This can be used when a value in the element is being // set just to pass information to the properties component. For example, // if there is a current 'tab' for which properties are to be displayed. console.error(`NOT SAVING THIS TRANSITORY VALUE. WARP YIP WANZACKO!`); } else { rememberToSave(commit, state, vm) } }, } /******************************************** * * MUTATIONS * ********************************************/ export const mutations = { // Set the current layout, displayed in the middle panel. setLayout (state, { vm, layout, contentId, crowdhoundElement, /*tenant, elementId, */ editable /*, element */ } ) { // console.log('In Mutation contentLayout/setLayout()', state) // console.log('In Mutation contentLayout/setLayout()', layout) if (layout) { state.layout = addAnyMissingValues(vm, layout) // console.log(`sanitized layout:`, state.layout) state.crowdhoundElement = crowdhoundElement } else { console.error(`Mutation contentLayout/setLayout requires 'layout' parameter`) state.layout = null state.contentId = null state.crowdhoundElement = null state.editable = false // Hack // if (element) { // console.error(`We do have 'element'`) // } return } if (crowdhoundElement) { state.crowdhoundElement = crowdhoundElement state.contentId = contentId } else { state.crowdhoundElement = null state.contentId = null } if (editable) { state.editable = editable } else { state.editable = false } }, // Set property values in a specific element // This *should* be an element in the current layout setPropertyInElementMutation (state, { vm, element, name, value } ) { // console.log('In Mutation contentLayout/setPropertyInElementMutation()', element) // console.log(`LOOKING FOR ${element.id}`) let path = trackDownElementInLayout(state, element.id) if (path) { let specifiedElement = path[path.length-1] // Do this such that a new reactive property is created. // https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats // NOT: specifiedElement[name] = value vm.$set(specifiedElement, name, value) } else { console.error(`setPropertyInElementMutation: element not found in current layout`); } }, // Set the element shown in the properties panel. // This *should* be an element in the current layout setPropertyElementMutation (state, { element } ) { // console.log('In Mutation contentLayout/setPropertyElementMutation()', element) let path = trackDownElementInLayout(state, element.id) //console.log(`path to selected element=`, path) state.pathToSelectedElement = path ? path : [ ] state.expandedElement = path ? path[path.length-1] : null // console.log(`Path to selected element=`, path) path.forEach((element) => { console.log(` ${element.type}: ${element.id}`) }) }, // Set the element currently expanded in the properties panel. // This *must* be an element in pathToSelectedElement. setExpandedElementMutation (state, { element } ) { console.log('In Mutation contentLayout/setExpandedElementMutation()', element) //return // console.log('State is ', state) // Clone the element //let duplicate = JSON.parse(JSON.stringify(data.element)); // console.log(`Before`) // Check this element is in the path to the current property element. for (let i = 0; i < state.pathToSelectedElement.length; i++) { if (state.pathToSelectedElement[i].id === element.id) { // Found it state.expandedElement = element return } } console.error(`setExpandedElementMutation: element not in pathToSelectedElement`) }, // Set the screen mode [view | edit | layout | debug] setEditMode (state, { mode, previousEditMode }) { //ZZZZ Check the parameters // console.log(`mutation contentLayout/setEditMode(${mode}, ${previousEditMode})`) state.mode = mode if (previousEditMode) { state.previousEditMode = previousEditMode } }, // Start dragging. This should temporarily switch to layout mode. dragStart (state, { }) { // console.log(`mutation contentLayout/dragStart()`) state.dragging = true }, dragStop (state, { }) { // console.log(`mutation contentLayout/dragStop()`) state.dragging = false }, // Set the message shown above the page (CLEAN | DIRTY | SAVING, etc). setSaveMsg (state, { msg }) { //ZZZZ Check the parameters // console.log(`mutation contentLayout/setSaveMsg(${msg})`) state.saveMsg = msg }, // Set the value of a property in an element. // The element is not necessarily the 'propertyElement', // it *should* be an element in the current layout. updateElementPropertyMutation (state, { vm, element, name, value }) { //ZZZZ Check the parameters if (!element) { console.log(`Update property in currently selected property element`); element = state.expandedElement } // console.log(`mutation contentLayout/updateElementPropertyMutation(${element.id}, ${name}, ${value})`, element) // Do this such that a new reactive property is created. // https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats // NOT: element[name] = value vm.$set(element, name, value) }, insertLayoutMutation (state, { vm, parent, position, layout }) { console.log('In Mutation contentLayout/insertLayoutMutation()', parent, position, layout) //let toInsert = layout // if (position === 'last' || position < 0) { // position = parent.children.length // } // Clone the hierarchy (this is the cheat's way) //let newchild = JSON.parse(JSON.stringify(child)); let toInsert = JSON.parse(safeJson(layout)); // Before inserting, check we have unique IDs for all elements in the // layout. By preference we will retain the IDs, so when a user cuts and // pastes any existing references will be retained. However if they copy // and paste, we don't want duplicate IDs already in our parent's layout. replaceIdsAlreadyInLayout(state, toInsert) console.log(`ok 5`, toInsert) // Check it is sane addAnyMissingValues(vm, toInsert) // For other element types, we insert the actual element. if (toInsert.type === 'layout') { // If an entire 'layout' is being inserted, we insert the children one by one. console.log(`Inserting layout element. ${toInsert.children.length} items.`) } else { // For non-layout elements, we insert the actual element console.log(`Inserting non-layout element: ${toInsert.type} at position ${position}`) if (position === 'last' || position < 0) { // Add to the end parent.children.push(toInsert) console.log(`ok 6a`) } else if (position <= parent.children.length) { // Insert at a specific position parent.children.splice(position, 0, toInsert); console.log(`ok 6b`) } else { // Invalid position alert('insertLayoutMutation: Internal error #2963 (invalid position)') return } } // Show the properties for the new element let path = trackDownElementInLayout(state, toInsert.id) //console.log(`path to selected element=`, path) state.pathToSelectedElement = path ? path : [ ] state.expandedElement = path ? path[path.length-1] : null //console.log(`Path to new element=`, path) //(path ? path : [ ]).forEach(function (element) { // console.log(` ${element.type}: ${element.id}`, element) //}) }, moveChildMutation (state, { vm, parent, from, to }) { console.log(`In Mutation contentLayout/moveChildMutation(${from}, ${to})`, parent) let path = trackDownElementInLayout(state, parent.id) if (path && path.length >= 1) { parent = path[path.length - 1] console.log(`Parent is ${parent.id} (${parent.type})`) if (from < parent.children.length) { // Remove the child from the array let removedChildren = parent.children.splice(from, 1) // Add it back to the array parent.children.splice(to, 0, removedChildren[0]) } } state.pathToSelectedElement = path ? path : [ ] state.expandedElement = path ? path[path.length-1] : null }, // Delete an element from the current layout. deleteElementMutation(state, { vm, element}) { console.log(`In Mutation contentLayout/deleteElementMutation(${element.id} (${element.type}))`) // Find the path down to this element, so we know the parent. let path = trackDownElementInLayout(state, element.id) if (path && path.length >= 2) { let parent = path[path.length - 2] console.log(`Parent is ${parent.id} (${parent.type})`) for (let index = 0; index < parent.children.length; index++) { if (parent.children[index].id == element.id) { // We've found it, so remove it console.log(`Found item to delete in position ${index}`) parent.children.splice(index, 1) return } } } }, // // Set the element shown in the properties panel. // // This *should* be an element in the current layout // setMode (state, { mode } ) { // console.log(`In Mutation setMode(${mode})`) // console.log('State is ', state) // // state.mode = mode // }, // Call this method to trigger redrawing of components that monitor // the value of 'refreshCounter'. refreshMutation (state, { }) { console.log('In Mutation refreshMutation()', state.refreshCounter) state.refreshCounter++ } }//- mutations // function overwriteElementIDs(element) { // element.id = Math.floor(Math.random() * 10000000000) // console.log('New Id is ', element.id) // // if (element.children) { // element.children.forEach((child) => overwriteElementIDs(child)) // } // } // Prepare a hierarchy of elements that will be used to lay out a page. function addAnyMissingValues (vm, element) { // Create a new element ID, but do not modify an existing ID. if (!element.id) { vm.$set(element, 'id', Math.floor(Math.random() * 10000000000)) // console.log('New Id is ', element.id) } // Make sure we have a 'children' element if (!element.children) { vm.$set(element, 'children', [ ]) } // Now sanitize any children for (let i = 0; i < element.children.length; ) { let child = element.children[i] if (child) { addAnyMissingValues(vm, child) i++ } else { // null child? // This should not happen, but it does on delete // console.error(`Child ${i} of element ${element.id} is null.`) // let a1 = element.children[i-1] // let a2 = element.children[i] // let a3 = element.children[i+1] // let l1 = element.children.length // console.log(`a1:`, a1) // console.log(`a2:`, a2) // console.log(`a3:`, a3) // console.log(`l1:`, l1) element.children.splice(i, 1) // let b1 = element.children[i-1] // let b2 = element.children[i] // let b3 = element.children[i+1] // let l2 = element.children.length // console.log(`b1:`, b1) // console.log(`b2:`, b2) // console.log(`b3:`, b3) // console.log(`l2:`, l2) // i++ } } return element } function safeJson (element, compressed/*boolean,optional*/) { let spaces = (compressed ? 0 : 4) // Custom replacer function - gets around "TypeError: Converting circular structure to JSON" // Modified from http://www.johnantony.com/pretty-printing-javascript-objects-as-json/ var replacer = function(key, value) { // ignore any circular links (they are circular) // if (key === '_parent') { // return // } return value } // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify let json = JSON.stringify(element, jsonReplacerCallback, spaces); return json; } //ZZKOP // function ZlayoutRoot (element) { // while (element._parent) { // element = element._parent // } // return element // } // function ZlayoutChanged (element) { // let root = layoutRoot(element) // root.tt_counter++ // } function loadLayoutFromAnchor (commit, vm, contentId, editable) { // console.log(`store.loadlayout(${contentId})`) // We select the content from crowdhound //console.log(`ContentTriplePane.loadLayout`) if (typeof(vm.$content) === 'undefined') { console.error('this.$content not defined. Remember to us Contentservice.use(Vue).') return } if (vm.$content.disabled) { console.error('Contentservice disabled') return } let canEdit = false if (typeof(editable) === 'undefined') { canEdit = true // Default to editable ZZZ } else if (editable) { canEdit = true } //- let contentId = '$testpage.text1' //let shortAnchor = `$testpage.${this.anchorPrefix}.${this.anchorSuffix}` let shortAnchor = contentId.startsWith('$') ? contentId.substring(1) : contentId let fullAnchor = `$` + shortAnchor let elementType = 'layout' // console.error(`>>> contentId is ${contentId}.`) vm.$content.select(vm, fullAnchor, elementType) //- this.$content.select(this, params) .then(result => { // Use the elements // console.log(`>>> result=`, result) if (result.elements.length < 1) { // Should not be possible console.error(`Selecting layout returned no element. How is this possible?`) return } // See if the description contains a valid layout // console.log(`result=`, result) let elementContainingLayout = result.elements[0] let json = elementContainingLayout.description // console.log('json=', json) let layout = null if (json === shortAnchor) { // console.error(`>>> NOT parsing json (it's the anchor)`) // New element/layout } else { // console.error(`>>> parsing JSON`) try { layout = JSON.parse(json) } catch (e) { console.error(`Broken JSON: `, e) console.log(`json=${json}`) } // console.log('parsed JSON layout=', layout) // Check for errors } console.log(`>>> layout=`, layout) // If we don't already have a layout, create an initial layout now. if (layout === null) { console.error(`>>> Creating default layout`) // Word out a heading, based on the contentId let heading = contentId if (heading.startsWith('$')) { heading = heading.substring(1) } if (heading.startsWith('page-')) { heading = heading.substring(5) } let arr = heading.split('-') heading = '' arr.forEach((word, index) => { if (index > 0) { heading += ' ' } switch (word.toLowerCase()) { case 'the': case 'to': case 'by': case 'of': case '&': case 'and': case 'for': case 'with': // Do not capitalize the word heading += word break default: // Capitalise the word heading += word.substring(0, 1).toUpperCase() + word.substring(1) } }) // Create an initial layout layout = { type: 'layout', children: [ { type: 'section', children: [ { type: 'container', children: [ { "type": "froala", "text": `<h1 style=\"text-align: center;\"><span style=\"font-size: 48px;\">${heading}</span></h1>`, "id": 2, "children": [] } ] } ] } ] } } // Save the layout in our state store. let sanitized = addAnyMissingValues(vm, layout) commit('setLayout', { vm, contentId: fullAnchor, layout: layout, crowdhoundElement: elementContainingLayout, // NOT an element in the layout // tenant: elementContainingLayout.tenant, // elementId: elementContainingLayout.id, editable: canEdit }) }) .catch(e => { let desc = `Error loading comments` console.log(`Dirty rotten error: `, e) /* handleError(this, desc, params, e) */ state.selectError = true })//- select } // If any of the IDs in the specified element or it's descendants exist // in our currently-being-edited layout, replace them. This is // typically used before pasting stuff into the current layout. function replaceIdsAlreadyInLayout(state, layoutToInsert) { console.log(`replaceIdsAlreadyInLayout(element: ${layoutToInsert.id}, elementType: ${layoutToInsert.type})`) // Create a hash of all the element IDs already in use let ids = getCurrentlyUsedIds(state) // console.log(`ids=`, ids) // Recursively look at every element and it's children, // replacing element Ids that are already used. let recurse = (element) => { // console.log(` - check element ${element.id}`) let initialId = element.id while (ids[element.id]) { element.id = Math.floor(Math.random() * 10000000000) console.log(` replacing id of element ${initialId} (${element.type}) -> ${element.id}`) } if (element.children) { element.children.forEach(child => recurse(child)) } } // Check, from the top down. recurse(layoutToInsert) } /* * Create a has of all the element IDs in the current layout. * returns [] of Id -> true */ function getCurrentlyUsedIds(state) { // console.log(`getCurrentlyUsedIds()`) // Recursive through the layout hierarchy, remembering the ids let hash = [ ] // id -> true let recurse = (element) => { // console.log(` - ${element.id}`) hash[element.id] = true if (element.children) { element.children.forEach(child => recurse(child)) } } recurse(state.layout) return hash } /* * We have changes, but we don't want to save every little keystroke. * Delay a few seconds and then save. */ function rememberToSave (commit, state, vm) { // We'll need to save changes to Crowdhound. Don't save every // change, but rather wait a few seconds to batch up changes. console.log( `save to Crowdhound (not yet)`) //this.crowdhoundElement.description = value if (saveTimer) { // Already set the timeout to save } else { // Save after five seconds commit('setSaveMsg', { msg: DIRTY }) //state.saveMsg = DIRTY saveTimer = setTimeout(() => { // Save the changes saveTimer = null //state.saveMsg = SAVING commit('setSaveMsg', { msg: SAVING }) //saveToCrowdhound(vm) // Squeeze the layout into a single Crowdhound element let crowdhoundElement = { tenant: state.crowdhoundElement.tenant, rootId: state.crowdhoundElement.rootId, parentId: state.crowdhoundElement.parentId, id: state.crowdhoundElement.id, type: 'layout', description: safeJson(state.layout, true/*compressed*/) } console.log(`saveToCrowdhound() in Store`, crowdhoundElement) console.log( `safeJson length is ${safeJson(state.layout, true/*compressed*/).length}`) // Update the element (should already exist) commit('setSaveMsg', { msg: SAVED }) vm.$content.update(vm, crowdhoundElement) .then(result => { setTimeout(() => { if (state.saveMsg === DIRTY) { commit('setSaveMsg', { msg: CLEAN }) } }, 1700) console.log(`result of save:`, result) console.log(`result of save:`, result.data) }) .catch(e => { let desc = `Error saving html content` console.log(desc, e) //state.saveMsg = ERROR commit('setSaveMsg', { msg: ERROR }) /* handleError(this, desc, params, e) */ //this.selectError = true })//- axios }, SAVE_INTERVAL) } } /* * Display an error message, by whatever means is possible. */ function handleError(vm, msg) { // alert(msg) if (vm && vm.$toast) { vm.$toast.open({ message: `${msg}`, type: 'is-danger' }) } else { console.error(`Error: ${msg}`) alert(`Error ${msg}`) } return false } /* * Recursively find this element within the current layout. * (We don't use parent links, because it's simpler * if we avoid cyclic links in the hierarchy) */ function trackDownElementInLayout(state, requiredID) { // console.log(`trackDownElementInLayout(state, requiredID:${requiredID})`) // console.log(`state=`, state) // console.log(`state.layout=`, state.layout) let recurse = (elementInLayout) => { // console.log(` - ${elementInLayout.id} (${elementInLayout.type})`) if (elementInLayout.id === requiredID) { return [ elementInLayout ] } if (elementInLayout.children) { for (let i = 0; i < elementInLayout.children.length; i++) { let child = elementInLayout.children[i] let path = recurse(child) if (path) { return [ elementInLayout, ...path] } } } return null } return recurse(state.layout) } // function trackDownElement_recursive(elementInLayout, requiredID) { // console.log(` trackDownElement_recursive(${elementInLayout.id}, ${requiredID})`) // if (elementInLayout.id === requiredID) { // // console.log(`found leaf`, element) // return [ elementInLayout ] // } // for (let i = 0; i < elementInLayout.children.length; i++) { // let child = elementInLayout.children[i] // let path = trackDownElement_recursive(child, requiredID) // if (path) { // // console.log(`found child path`, path) // return [ elementInLayout, ...path] // } // } // return null // } // Function to avoid circular dependencies while "stringify"ing objects to JSON. // Gets around "TypeError: Converting circular structure to JSON" // See http://www.johnantony.com/pretty-printing-javascript-objects-as-json/ function jsonReplacerCallback (key, value) { // ignore any circular links // if (key === '_parent') { // return // } return value } export const namespaced = true export default { "namespaced": true, state, mutations, getters, actions, }