import Component from '@glimmer/component'; import RestoreRevisionModal from '../components/modals/restore-revision'; import diff from 'node-htmldiff'; import {action, set} from '@ember/object'; import {inject as service} from '@ember/service'; import {tracked} from '@glimmer/tracking'; function checkFinishedRendering(element, done) { let last = element.innerHTML; function check() { let html = element.innerHTML; if (html === last) { done(); } else { last = html; setTimeout(check, 50); } } setTimeout(check, 50); } export default class ModalPostHistory extends Component { @service notifications; @service modals; @service feature; constructor() { super(...arguments); this.post = this.args.model.post; this.editorAPI = this.args.model.editorAPI; this.toggleSettingsMenu = this.args.model.toggleSettingsMenu; } @tracked selectedHTML = `

loading...

`; @tracked diffHtml = null; @tracked showDifferences = this.feature.get('postDiffing'); // should default to true in future @tracked selectedRevisionIndex = 0; get selectedRevision() { return this.revisionList[this.selectedRevisionIndex]; } get comparisonRevision() { return this.revisionList[this.selectedRevisionIndex + 1] || this.selectedRevision; } get previousTitle() { return this.comparisonRevision.title || this.post.get('title'); } get currentTitle() { return this.selectedRevision.title || this.post.get('title'); } get revisionList() { return this.post.get('postRevisions').toArray().reverse().map((revision, index) => { return { lexical: revision.get('lexical'), selected: index === this.selectedRevisionIndex, latest: index === 0, createdAt: revision.get('createdAt'), title: revision.get('title'), feature_image: revision.get('featureImage'), author: { name: revision.get('author.name') || 'Anonymous' }, postStatus: revision.get('postStatus'), reason: revision.get('reason') }; }); } @action onInsert() { this.updateDiff(); } @action handleClick(index) { this.selectedRevisionIndex = index; this.updateDiff(); } @action registerSelectedEditorApi(api) { this.selectedEditor = api; } @action registerComparisonEditorApi(api) { this.comparisonEditor = api; } @action closeModal() { this.args.closeModal(); } stripInitialPlaceholder(html) { //TODO: we should probably add a data attribute to Koenig and grab that instead const regex = /]*>(\s*Begin writing your post\.\.\.\s*)<\/div>/i; const strippedHtml = html.replace(regex, ''); return strippedHtml; } @action restoreRevision(index) { const revision = this.revisionList[index]; this.modals.open(RestoreRevisionModal, { post: this.post, revision, updateTitle: () => { set(this.post, 'titleScratch', revision.title); }, updateEditor: () => { const state = this.editorAPI.editorInstance.parseEditorState(revision.lexical); this.editorAPI.editorInstance.setEditorState(state); }, closePostHistoryModal: () => { this.closeModal(); this.toggleSettingsMenu(); } }); } @action toggleDifferences() { this.showDifferences = !this.showDifferences; } get cardConfig() { return { post: this.args.model }; } calculateHTMLDiff(previousHTML, currentHTML) { const result = diff(previousHTML, currentHTML); const div = document.createElement('div'); div.innerHTML = result; this.diffCards(div); return div.innerHTML; } diffCards(div) { const cards = div.querySelectorAll('div[data-kg-card]'); for (const card of cards) { const hasChanges = !!card.querySelectorAll('del').length || !!card.querySelectorAll('ins').length; if (hasChanges) { const delCard = card.cloneNode(true); const insCard = card.cloneNode(true); const ins = document.createElement('ins'); const del = document.createElement('del'); delCard.querySelectorAll('ins').forEach((el) => { el.remove(); }); insCard.querySelectorAll('del').forEach((el) => { el.remove(); }); delCard.querySelectorAll('del').forEach((el) => { el.parentNode.appendChild(el.firstChild); el.remove(); }); insCard.querySelectorAll('ins').forEach((el) => { el.parentNode.appendChild(el.firstChild); el.remove(); }); ins.appendChild(insCard); del.appendChild(delCard); card.parentNode.appendChild(del); card.parentNode.appendChild(ins); card.remove(); } } } updateDiff() { if (this.comparisonEditor && this.selectedEditor) { let comparisonState = this.comparisonEditor.editorInstance.parseEditorState(this.comparisonRevision.lexical); let selectedState = this.selectedEditor.editorInstance.parseEditorState(this.selectedRevision.lexical); this.comparisonEditor.editorInstance.setEditorState(comparisonState); this.selectedEditor.editorInstance.setEditorState(selectedState); } let previous = document.querySelector('.gh-post-history-hidden-lexical.previous'); let current = document.querySelector('.gh-post-history-hidden-lexical.current'); let previousDone = false; let currentDone = false; let updateIfBothDone = () => { if (previousDone && currentDone) { this.diffHtml = this.calculateHTMLDiff(this.stripInitialPlaceholder(previous.innerHTML), this.stripInitialPlaceholder(current.innerHTML)); this.selectedHTML = this.stripInitialPlaceholder(current.innerHTML); } }; checkFinishedRendering(previous, () => { previous.querySelectorAll('[contenteditable]').forEach((el) => { el.setAttribute('contenteditable', false); }); previousDone = true; updateIfBothDone(); }); checkFinishedRendering(current, () => { current.querySelectorAll('[contenteditable]').forEach((el) => { el.setAttribute('contenteditable', false); }); currentDone = true; updateIfBothDone(); }); } }