|
/*! |
|
* Offline mode for [email protected] |
|
* https://hub.docker.com/r/atlassian/confluence-server/tags?page=1&name=7.5.0 |
|
* |
|
* Source code |
|
* https://gist.github.com/crutch12/2c94ddc0c2070e0b6e613ea431c2f215 |
|
* |
|
* @author Konstantin Barabanov <[email protected]> |
|
* |
|
* Date: 2023-01-24T21:28Z |
|
*/ |
|
|
|
(() => { |
|
|
|
const INTERVAL = 30000; |
|
|
|
addEventListener('DOMContentLoaded', () => { |
|
console.debug('offline.js started...') |
|
|
|
// @NOTE: Every INTERVAL seconds check and save changes |
|
setInterval(() => { |
|
const editMode = document.body.classList.contains('contenteditor') || document.body.classList.contains('edit') || document.body.classList.contains('create') |
|
|
|
if (!editMode) return; |
|
|
|
const content = getContent() |
|
|
|
if (!content) return; |
|
|
|
const info = getInfo(); |
|
|
|
saveContent({ |
|
...info, |
|
content, |
|
}).catch(err => console.error(err)) |
|
}, INTERVAL) |
|
}); |
|
|
|
const getMetaTagContent = (name) => { |
|
const tag = document.querySelector(`meta[name="${name}"]`) |
|
return tag ? tag.content : undefined |
|
} |
|
|
|
const getInfo = () => { |
|
const draftId = getMetaTagContent('ajs-draft-id') |
|
const pageId = getMetaTagContent('ajs-page-id') |
|
const pageVersion = getMetaTagContent('page-version') |
|
const spaceKey = getMetaTagContent('ajs-space-key') |
|
const type = getMetaTagContent('ajs-space-key') |
|
|
|
const title = document.querySelector('#content-title').value || getMetaTagContent('ajs-latest-published-page-title') || getMetaTagContent('ajs-page-title') |
|
// <input id="syncRev" type="hidden" name="syncRev" value="0.kHzKSow3znE6tLEeBFN4xTo.36"></input> |
|
const syncRev = document.querySelector('#syncRev').value // @NOTE: it changes, when you edit page content (e.g. typing smth) |
|
|
|
return { |
|
spaceKey, |
|
type, |
|
draftId, |
|
pageId, |
|
title, |
|
pageVersion, |
|
syncRev, |
|
} |
|
} |
|
|
|
const getContent = () => { |
|
// @NOTE: wysiwyg iframe |
|
const wysiwyg = document.querySelector('#wysiwyg iframe') |
|
|
|
if (!wysiwyg) return; |
|
|
|
const doc = wysiwyg.contentDocument || wysiwyg.contentWindow.document |
|
|
|
const content = doc.body.innerHTML |
|
|
|
return content; |
|
} |
|
|
|
// @NOTE: Save data (content + date + syncRev) |
|
const saveContent = async (data) => { |
|
if (!db) await openDB(); |
|
|
|
const key = [data.spaceKey, data.title, data.type, 'p-' + data.pageId, 'd-' + data.draftId, 'v-' + data.pageVersion].join('/') |
|
const value = data.content |
|
const syncRev = data.syncRev |
|
|
|
const saved = await getFromStore(key).catch(() => null) |
|
|
|
if (!saved) return addToStore(key, value, syncRev) |
|
|
|
// @NOTE: Rewrite only of syncRev changed -> content changed |
|
if (saved.syncRev !== syncRev) { |
|
return addToStore(key, value, syncRev) |
|
} |
|
} |
|
|
|
// @NOTE: Edit page |
|
/* <body id="com-atlassian-confluence" class="theme-default contenteditor edit aui-layout aui-theme-default synchrony-active aui8 cw vsc-initialized" data-aui-version="8.3.5"></body> */ |
|
|
|
// @NOTE: warning message, if couldn't save data to server: |
|
/* <div class="status-indicator-icon aui-icon aui-icon-small aui-iconfont-warning" data-tooltip="Can't reach the server. Check your internet connection, and we'll keep trying to reconnect you."></div> */ |
|
|
|
// storage logic |
|
|
|
const dbName = "iframe/wysiwyg/body"; |
|
const version = 1; // incremental ints |
|
const storeName = "wysiwyg"; |
|
let db = null; // define the db variable to be global in the sw file |
|
|
|
function openDB() { |
|
return new Promise((resolve, reject) => { |
|
// ask to open the db |
|
const openRequest = self.indexedDB.open(dbName, version); |
|
|
|
openRequest.onerror = function (event) { |
|
console.debug( |
|
"Everyhour isn't allowed to use IndexedDB?!" + event.target.errorCode |
|
); |
|
db = null; |
|
reject(event) |
|
}; |
|
|
|
// upgrade needed is called when there is a new version of you db schema that has been defined |
|
openRequest.onupgradeneeded = function (event) { |
|
db = event.target.result; |
|
|
|
if (!db.objectStoreNames.contains(storeName)) { |
|
// if there's no store of 'storeName' create a new object store |
|
db.createObjectStore(storeName, { keyPath: "key" }); //some use keyPath: "id" (basically the primary key) - unsure why yet |
|
} |
|
}; |
|
|
|
openRequest.onsuccess = function (event) { |
|
db = event.target.result; |
|
resolve(db) |
|
}; |
|
}) |
|
} |
|
|
|
function addToStore(key, value, syncRev) { |
|
return new Promise((resolve, reject) => { |
|
// start a transaction of actions you want to submit |
|
const transaction = db.transaction(storeName, "readwrite"); |
|
|
|
|
|
// create an object store |
|
const store = transaction.objectStore(storeName); |
|
|
|
// add key and value to the store |
|
const request = store.put({ date: new Date(), value, key, syncRev }); |
|
|
|
request.onsuccess = function () { |
|
resolve(request.result) |
|
}; |
|
|
|
request.onerror = function () { |
|
console.debug("Error did not save to store", request.error); |
|
reject(request.error) |
|
}; |
|
|
|
transaction.onerror = function (event) { |
|
console.debug("trans failed", event); |
|
reject(request.result) |
|
}; |
|
|
|
transaction.oncomplete = function (event) { |
|
resolve(request.event) |
|
}; |
|
}) |
|
} |
|
|
|
function getFromStore(key) { |
|
return new Promise((resolve, reject) => { |
|
// start a transaction |
|
const transaction = db.transaction(storeName, "readwrite"); |
|
// create an object store |
|
const store = transaction.objectStore(storeName); |
|
// get key and value from the store |
|
const request = store.get(key); |
|
|
|
request.onsuccess = function (event) { |
|
resolve(event.target.result) // { key: string, value: string, date: Date, syncRev: string } structure |
|
}; |
|
|
|
request.onerror = function () { |
|
console.debug("Error did not read to store", request.error); |
|
reject(request.error) |
|
}; |
|
|
|
transaction.onerror = function (event) { |
|
console.debug("trans failed", event); |
|
reject(request.event) |
|
}; |
|
transaction.oncomplete = function (event) { |
|
resolve(request.event) |
|
}; |
|
}) |
|
} |
|
|
|
window.__getSavedOfflineData = async () => { |
|
if (!db) await openDB(); |
|
|
|
return new Promise((res, rej) => { |
|
// Fetch keys |
|
const keysTr = db.transaction(storeName).objectStore(storeName).getAllKeys() |
|
keysTr.onsuccess = (event) => { |
|
const keys = event.target.result |
|
if (keys?.length) { |
|
// Start a new transaction for final result |
|
const valuesTr = db.transaction(storeName) |
|
const objStore = valuesTr.objectStore(storeName) |
|
|
|
const result = [] // { key, value }[] |
|
|
|
// Iterate over keys |
|
keys.forEach(key => { |
|
const tr = objStore.get(key) |
|
tr.onsuccess = e => { |
|
result.push({ |
|
key, |
|
value: e.target.result |
|
}) |
|
} |
|
}) |
|
// Resolve `getAll` with final { key, value }[] result |
|
valuesTr.oncomplete = (event) => { |
|
res(result) |
|
} |
|
valuesTr.onerror = (event) => { |
|
rej(event) |
|
} |
|
} |
|
else |
|
res([]) |
|
} |
|
keysTr.onerror = (event) => { |
|
rej(event) |
|
} |
|
}) |
|
} |
|
})() |