diff --git a/app/components/SimpleEditor/ContainerEditor.js b/app/components/SimpleEditor/ContainerEditor.js index a848babcc4fca0625d5c88493e7fb79f1d21215f..58f0df11df9e5db7ae7d34a2f347848ab9a8101b 100644 --- a/app/components/SimpleEditor/ContainerEditor.js +++ b/app/components/SimpleEditor/ContainerEditor.js @@ -4,35 +4,22 @@ import { // deleteCharacter, // deleteSelection, keys as keyboardKeys, - Surface, uuid } from 'substance' class ContainerEditor extends SubstanceContainerEditor { - render ($$) { - // TODO -- call with super - var el = Surface.prototype.render.call(this, $$) - - var doc = this.getDocument() - var containerId = this.getContainerId() - var containerNode = doc.get(containerId) - - if (!containerNode) { - console.warn('No container node found for ', containerId) - } + constructor (...args) { + super(...args) + this.controlBackButton = this.controlBackButton.bind(this) + } - el.addClass('sc-container-editor container-node ' + containerId) - .attr({ - spellCheck: false, - 'data-id': containerId - }) + render ($$) { + let el = super.render($$) - // if it IS empty, handle in didMount - if (!this.isEmpty()) { - containerNode.getNodes().forEach(function (node) { - el.append(this._renderNode($$, node).ref(node.id)) - }.bind(this)) - } + // native spellcheck + // TODO -- there is a hasNativeSpellcheck fn + const isSpellcheckNative = (this.props.spellcheck === 'native') + el.attr('spellcheck', isSpellcheckNative) // open for editing // TODO -- should maybe change to isEditable ? @@ -49,14 +36,51 @@ class ContainerEditor extends SubstanceContainerEditor { super.didMount() if (this.isEmpty()) this.createText() - this.focus() - if (this.isReadOnlyMode()) { - const documentSession = this.getDocumentSession() - documentSession.on('didUpdate', this.disableToolbar, this) + // TODO -- why this and not this.focus ? + this.el.focus() + if (this.isReadOnlyMode()) { + this.editorSession.onUpdate('', this.disableToolbar, this) this.addTargetToLinks() } + + this.props.history.listenBefore((location, callback) => { + const commandStates = this.getCommandStates() + + if (commandStates['save'].disabled === false) { + const editor = this.getEditor() + + editor.send('changesNotSaved') + editor.emit('send:route', location.pathname) + + return callback(false) + } + + return callback() + }) + + window.history.pushState(null, null, document.URL) + window.addEventListener('popstate', this.controlBackButton) + } + + // TODO -- review // messes up browser history + controlBackButton () { + const commandStates = this.getCommandStates() + const url = '/books/' + this.props.book.id + '/book-builder' + + window.removeEventListener('popstate', this.controlBackButton) + + if (commandStates['save'].disabled === false) { + const editor = this.getEditor() + + window.history.pushState(null, null, document.URL) + + editor.send('changesNotSaved') + editor.emit('send:route', url) + } else { + this.props.history.push(url) + } } onTextInput (event) { @@ -159,7 +183,7 @@ class ContainerEditor extends SubstanceContainerEditor { createText () { var newSel - this.transaction(function (tx) { + this.editorSession.transaction(function (tx) { var container = tx.get(this.props.containerId) var textType = tx.getSchema().getDefaultTextType() @@ -173,6 +197,9 @@ class ContainerEditor extends SubstanceContainerEditor { newSel = tx.createSelection({ type: 'property', + // TODO -- both id's ?? + containerId: 'body', + surfaceId: 'body', path: [ node.id, 'content' ], startOffset: 0, endOffset: 0 @@ -180,7 +207,7 @@ class ContainerEditor extends SubstanceContainerEditor { }.bind(this)) this.rerender() - this.setSelection(newSel) + this.editorSession.setSelection(newSel) } // only runs if editor is in read-only mode @@ -209,6 +236,10 @@ class ContainerEditor extends SubstanceContainerEditor { link.attr('target', '_blank') ) } + + getEditor () { + return this.context.editor + } } export default ContainerEditor diff --git a/app/components/SimpleEditor/Editor.js b/app/components/SimpleEditor/Editor.js index 9ff476aade2bbfb27b1f36170a25cc27573b13a7..bc9b25c2c8eb160bea5dc74692b296b4bb42f5cf 100644 --- a/app/components/SimpleEditor/Editor.js +++ b/app/components/SimpleEditor/Editor.js @@ -2,20 +2,18 @@ import { includes, some } from 'lodash' import { ProseEditor, - ProseEditorOverlayTools, - ScrollPane, - SplitPane, - TOCProvider + TOCProvider, + Toolbar } from 'substance' import Comments from './panes/Comments/CommentBoxList' import CommentsProvider from './panes/Comments/CommentsProvider' import ContainerEditor from './ContainerEditor' -// import Overlay from './Overlay' import Notes from './panes/Notes/Notes' import NotesProvider from './panes/Notes/NotesProvider' import TableOfContents from './panes/TableOfContents/TableOfContents' import TrackChangesProvider from './elements/track_change/TrackChangesProvider' +import ModalWarning from './elements/modal_warning/ModalWarning' class Editor extends ProseEditor { constructor (parent, props) { @@ -24,38 +22,38 @@ class Editor extends ProseEditor { this.handleActions({ 'showComments': function () { this.toggleCommentsArea(true) }, 'hideComments': function () { this.toggleCommentsArea(false) }, - 'trackChangesUpdate': function () { this.updateTrackChange() } + + // TODO -- clean them up like changesNotSaved + 'trackChangesUpdate': function () { this.updateTrackChange() }, + 'trackChangesViewToggle': function () { this.trackChangesViewToggle() }, + // 'changesNotSaved': function () { this.changesNotSaved() } + 'changesNotSaved': this.changesNotSaved }) } - updateTrackChange () { - this.extendProps({ - trackChanges: !this.props.trackChanges + changesNotSaved () { + this.extendState({ changesNotSaved: true }) + } + + trackChangesViewToggle () { + this.extendState({ + trackChangesView: !this.state.trackChangesView }) + } + updateTrackChange () { + // TODO -- clean up this.props and this.refs + this.extendProps({ trackChanges: !this.props.trackChanges }) this.props.updateTrackChangesStatus(!this.props.trackChanges) - - this.extendState({ trackChanges: !this.props.trackChanges }) + this.refs.toolbar.extendProps({trackChanges: this.props.trackChanges}) } willUpdateState () {} didMount () { - this.documentSession.on('didUpdate', this.documentSessionUpdated, this) - this.documentSession.on('fileUploadTrigger', this.handleUpload, this) this.extendState({ editorReady: true }) } - handleUpload (file, callback) { - const { fileUpload } = this.props - - // TODO -- then / catch - fileUpload(file).then((res, err) => { - if (err != null) callback(null, err) - return callback(res.file, null) - }) - } - render ($$) { const { trackChangesView } = this.state const canToggleTrackChanges = this.canToggleTrackChanges() @@ -63,17 +61,26 @@ class Editor extends ProseEditor { const el = $$('div').addClass('sc-prose-editor') // left side: editor and toolbar - var toolbar = this._renderToolbar($$) - var editor = this._renderEditor($$) + let toolbar = this._renderToolbar($$) + let editor = this._renderEditor($$) + + let SplitPane = this.componentRegistry.get('split-pane') + let ScrollPane = this.componentRegistry.get('scroll-pane') + let Overlay = this.componentRegistry.get('overlay') - var footerNotes = $$(Notes) - var props = { + // TODO -- unnecessary // posssibly breaks book builder dnd + let ContextMenu = this.componentRegistry.get('context-menu') // new what does it do? + let Dropzones = this.componentRegistry.get('dropzones') // new what does it do? + + const footerNotes = $$(Notes) + + const props = { book: this.props.book, fragment: this.props.fragment, history: this.props.history } - var toc = $$(TableOfContents, props) + const toc = $$(TableOfContents, props) var editorWithFooter = $$('div') .append( @@ -98,10 +105,14 @@ class Editor extends ProseEditor { ) var contentPanel = $$(ScrollPane, { - scrollbarPosition: 'right', - overlay: ProseEditorOverlayTools + name: 'contentPanel', + // contextMenu: 'custom', + scrollbarPosition: 'right' }) - .append(editorWithComments) + .append(editorWithComments, + $$(Overlay), + $$(ContextMenu), + $$(Dropzones)) .attr('id', 'content-panel') .ref('contentPanel') @@ -123,13 +134,45 @@ class Editor extends ProseEditor { el.addClass('track-changes-mode') } + const modal = $$(ModalWarning, { + width: 'medium', + textAlign: 'center' + }) + + if (this.state.changesNotSaved) { + return el.append(modal) + } + return el } - // TODO -- use this to insert read-only mode alert + // TODO -- leverage ProseEditor's this._renderToolbar maybe? _renderToolbar ($$) { - const toolbar = super._renderToolbar($$) - return toolbar + let viewMode = this.props.disabled ? $$('span') + .addClass('view-mode') + .append('Editor is in Read-Only mode') + : '' + + let commandStates = this.commandManager.getCommandStates() + + return $$('div') + .addClass('se-toolbar-wrapper') + .append( + $$(Toolbar, { + commandStates: commandStates, + trackChanges: this.props.trackChanges, + trackChangesView: this.state.trackChangesView, + toolGroups: [ + 'annotations', + 'default', + 'document', + 'text', + 'track-change-enable', + 'track-change-toggle-view' + ] + }).ref('toolbar') + ) + .append(viewMode) } _renderEditor ($$) { @@ -137,19 +180,22 @@ class Editor extends ProseEditor { const editing = this.props.disabled ? 'selection' : 'full' return $$(ContainerEditor, { editing: editing, - documentSession: this.documentSession, + editorSession: this.editorSession, commands: configurator.getSurfaceCommandNames(), containerId: 'body', + spellcheck: 'native', textTypes: configurator.getTextTypes(), trackChanges: this.props.trackChanges, - updateTrackChangesStatus: this.props.updateTrackChangesStatus + updateTrackChangesStatus: this.props.updateTrackChangesStatus, + history: this.props.history, + book: this.props.book }).ref('body') } getInitialState () { return { + changesNotSaved: false, editorReady: false, - trackChanges: this.props.trackChanges, trackChangesView: true } } @@ -189,16 +235,16 @@ class Editor extends ProseEditor { containerId: 'body' }) - // // notes provider + // notes provider const notesProvider = new NotesProvider(doc) - // // comments provider + // comments provider const commentsProvider = new CommentsProvider(doc, { commandManager: this.commandManager, comments: this.props.fragment.comments, containerId: this.props.containerId, controller: this, - documentSession: this.documentSession, + editorSession: this.editorSession, fragment: this.props.fragment, surfaceManager: this.surfaceManager, toggleCommentsArea: this.toggleCommentsArea, @@ -211,7 +257,7 @@ class Editor extends ProseEditor { commandManager: this.commandManager, containerId: this.props.containerId, controller: this, - documentSession: this.documentSession, + editorSession: this.editorSession, surfaceManager: this.surfaceManager, user: this.props.user }) diff --git a/app/components/SimpleEditor/SimpleEditor.jsx b/app/components/SimpleEditor/SimpleEditor.jsx index 59fca1bee4aabbd61442d56d56ee83ea9d739631..dff5a96509d303b48f6dda09340f58e9621efdf2 100644 --- a/app/components/SimpleEditor/SimpleEditor.jsx +++ b/app/components/SimpleEditor/SimpleEditor.jsx @@ -1,11 +1,10 @@ // import { get } from 'lodash' import React from 'react' import ReactDOM from 'react-dom' -import { Alert } from 'react-bootstrap' import { ProseEditorConfigurator as Configurator, - DocumentSession + EditorSession } from 'substance' import config from './config' @@ -24,11 +23,6 @@ export default class SimpleEditor extends React.Component { this._releaseLock = this._releaseLock.bind(this) this._acquireLock = this._acquireLock.bind(this) - - // TODO -- delete, along with Alert - this.state = { - canEdit: false - } } // TODO -- is this necessary? @@ -50,15 +44,18 @@ export default class SimpleEditor extends React.Component { const importer = configurator.createImporter('html') const doc = importer.importDocument(source) - const documentSession = new DocumentSession(doc) + const editorSession = new EditorSession(doc, { + configurator: configurator + }) - documentSession.setSaveHandler({ - saveDocument: this.save + editorSession.setSaveHandler({ + saveDocument: this.save, + uploadFile: this.props.fileUpload }) return { configurator: configurator, - documentSession: documentSession + editorSession: editorSession } } @@ -71,11 +68,10 @@ export default class SimpleEditor extends React.Component { save (source, changes, callback) { const { onSave } = this.props const config = this.state.config - const exporter = new SimpleExporter(config) const convertedSource = exporter.exportDocument(source) - onSave(convertedSource, callback) + return onSave(convertedSource) } // NOTE -- Theoretically, we shouldn't lock when the editor is in read only @@ -139,8 +135,8 @@ export default class SimpleEditor extends React.Component { componentDidMount () { const el = ReactDOM.findDOMNode(this) - const { book, fileUpload, fragment, history, onSave, update, user } = this.props - const { configurator, documentSession } = this.createSession() + const { book, fragment, history, onSave, update, user } = this.props + const { configurator, editorSession } = this.createSession() if (!fragment) return @@ -161,8 +157,7 @@ export default class SimpleEditor extends React.Component { configurator, containerId, disabled, - documentSession, - fileUpload, + editorSession, fragment, history, onSave, @@ -207,19 +202,8 @@ export default class SimpleEditor extends React.Component { // TODO -- do I even need a render here? render () { - // TODO -- DELETE THIS !!!!! - let viewMode = !this.state.canEdit - ? ( - <Alert bsStyle='warning' className='view-mode'> - <span>Editor is in Read-Only Mode</span> - </Alert> - ) - : null - return ( - <div className='editor-wrapper'> - {viewMode} - </div> + <div className='editor-wrapper' /> ) } } diff --git a/app/components/SimpleEditor/SimpleEditor.scss b/app/components/SimpleEditor/SimpleEditor.scss index e47373c4aac94caf38f837d6a4ce59546b5e55f9..da9ed1141e0cd6dd8c492d28d980806489f66def 100644 --- a/app/components/SimpleEditor/SimpleEditor.scss +++ b/app/components/SimpleEditor/SimpleEditor.scss @@ -27,23 +27,19 @@ $active-blue: #4a90e2; height: 90vh; position: relative; + // move to a new file toolbar.scss ?? .view-mode { - height: 44px; - position: fixed; + font-size: 14px; + position: absolute; right: 0; text-align: center; + top: 10px; width: 20%; z-index: 9999; - - span { - font-size: 14px; - position: relative; - top: 10px; - } } .track-changes-mode { - .se-content:first-child { + div.se-content { line-height: 38px; } @@ -54,6 +50,9 @@ $active-blue: #4a90e2; .sc-comment-pane-list li .comment-list .single-comment-row { padding: 3px 12px; } + .sc-overlay .se-active-tools .sc-overlay-bubble .sc-comment-icon { + top: 0; + } } } @@ -64,12 +63,15 @@ $active-blue: #4a90e2; right: 0; top: 0; - .sc-toolbar { + .se-toolbar-wrapper .sc-toolbar { background-color: $primary; border: 1px solid $border; border-right: 0; + float: left; + max-width: 1920px; padding-left: 0; vertical-align: middle; + width: 100%; @-moz-document url-prefix() { .sc-tool-group > .sc-switch-text-type { margin: 0; @@ -113,12 +115,12 @@ $active-blue: #4a90e2; } // end dropdown .sm-target-track-change-enable { - border-right: 1px solid $border; button { background-color: $inactive-grey; border-radius: 0; color: $white; + line-height: 0; // cursor: pointer; padding: 0 19px; position: relative; @@ -149,6 +151,7 @@ $active-blue: #4a90e2; cursor: pointer; padding: 0 19px; position: relative; + line-height: 0; } button::after { @@ -168,14 +171,14 @@ $active-blue: #4a90e2; border-left: 1px solid $border; } - .sm-target-track-change-enable, - .sm-target-track-change-toggle-view { + .sm-target-track-change-enable { padding: 0 9px; } .sm-target-document, .sm-target-annotations, - .sm-target-insert { + .sm-target-insert, + .sm-target-default { border-right: 1px solid $border; padding: 0px 9px; @@ -204,7 +207,7 @@ $active-blue: #4a90e2; padding: 0; &:after { - bottom: 17px; + bottom: 8px; color: $black; content: 'x'; font-size: 8px; @@ -223,10 +226,16 @@ $active-blue: #4a90e2; } - .se-content { + div.se-content { color: $transparent-black; font-family: 'Fira Sans'; word-wrap: break-word; + background-color: $white; + box-shadow: 0 0 8px $dark-gray; + margin: 1.5% 14% 7%; + min-height: 100vh; + padding: 3% 4% 1%; + transition: .3s; ::selection { background: $light-gray; @@ -240,15 +249,6 @@ $active-blue: #4a90e2; background: none; } - &:first-child { - background-color: $white; - box-shadow: 0 0 8px $dark-gray; - margin: 1.5% 14% 7%; - min-height: 100vh; - padding: 3% 4% 1%; - transition: .3s; - } - .sc-split-pane { position: relative; } @@ -257,26 +257,35 @@ $active-blue: #4a90e2; outline: none; } - .sc-prose-editor-overlay-tools { + .sc-overlay { .se-active-tools { background: transparent; border: 0; + border-radius: 0; padding: 0; } } - .sc-prose-editor-overlay-tools::before { + .sc-overlay.sm-theme-dark::before { border-style: none; border-width: 0; - } + } + + .sc-list-ul { + list-style-type: disc; + padding-left: 19px; + } + + .sc-list-ol { + list-style-type: decimal; + padding-left: 19px; + } } // end sc-content .sc-has-comments { - .se-content { - &:first-child { + div.se-content { margin: 1.5% 27% 5% 1%; transition: .3s; - } } } diff --git a/app/components/SimpleEditor/SimpleEditorExporter.js b/app/components/SimpleEditor/SimpleEditorExporter.js index 3264721fc9ef869e570eebeac39572b31f1bcde4..7fa1ad70940f1982e1c1dc100c3724ab77d81373 100644 --- a/app/components/SimpleEditor/SimpleEditorExporter.js +++ b/app/components/SimpleEditor/SimpleEditorExporter.js @@ -34,11 +34,11 @@ class SimpleExporter extends HTMLExporter { throw new Error('Illegal arguments: container is mandatory.') } - var doc = container.getDocument() + var doc = container.editorSession.getDocument() this.state.doc = doc var elements = [] - container.data.nodes.body.nodes.forEach(function (id) { + container.editorSession.document.data.nodes.body.nodes.forEach(function (id) { var node = doc.get(id) var nodeEl = this.convertNode(node) elements.push(nodeEl) diff --git a/app/components/SimpleEditor/SimpleEditorImporter.js b/app/components/SimpleEditor/SimpleEditorImporter.js index 3553509a5138580ea7987ac0948a7c26b1e26847..24d5c6dd4f1b64bce2093a415eb37e4f3ece8b3e 100644 --- a/app/components/SimpleEditor/SimpleEditorImporter.js +++ b/app/components/SimpleEditor/SimpleEditorImporter.js @@ -27,6 +27,8 @@ class SimpleImporter extends HTMLImporter { this.convertContainer(bodyEls, 'body') } + // TODO -- check substance's implementation of overlapping annotations + // override substance's internal function to allow for overlapping // annotations, without adhering to an expand / fuse mode _createInlineNodes () { diff --git a/app/components/SimpleEditor/SimpleEditorWrapper.jsx b/app/components/SimpleEditor/SimpleEditorWrapper.jsx index 58391dc02bfdf6640b4ead96977f95a6299ddb41..cba0a6169c6b7c29dc6c00639491e344e3d3e12d 100644 --- a/app/components/SimpleEditor/SimpleEditorWrapper.jsx +++ b/app/components/SimpleEditor/SimpleEditorWrapper.jsx @@ -73,14 +73,13 @@ export class SimpleEditorWrapper extends React.Component { const { fragment } = this.props fragment.source = source - this.update(fragment) - return callback() + return this.update(fragment) } update (newChapter) { const { book } = this.props const { updateFragment } = this.props.actions - updateFragment(book, newChapter) + return updateFragment(book, newChapter) } } diff --git a/app/components/SimpleEditor/config.js b/app/components/SimpleEditor/config.js index 50297edd46890893b8e8823744b1bcabb0541b47..0984a6ac8b65b7861de603f60bd92d5ed0cf36e3 100644 --- a/app/components/SimpleEditor/config.js +++ b/app/components/SimpleEditor/config.js @@ -4,30 +4,27 @@ import { CodePackage, EmphasisPackage, HeadingPackage, - // LinkPackage, ParagraphPackage, PersistencePackage, ProseArticle, LinkPackage, StrongPackage, SubscriptPackage, - SuperscriptPackage + SuperscriptPackage, + SwitchTextTypePackage, + SpellCheckPackage, + CodeblockPackage } from 'substance' // My Elements -import CodeblockPackage from './elements/codeblock/CodeblockPackage' import CommentPackage from './elements/comment/CommentPackage' import ExtractPackage from './elements/extract/ExtractPackage' import NotePackage from './elements/note/NotePackage' import SourceNotePackage from './elements/source_note/SourceNotePackage' import ImagePackage from './elements/images/ImagePackage' - +import ListPackage from './elements/list/ListPackage' import TrackChangePackage from './elements/track_change/TrackChangePackage' -// var DialoguePackage = require('./elements/dialogue/DialoguePackage') -// var NumberedListPackage = require('./elements/numbered_list/NumberedListPackage') -// var NoStyleListPackage = require('./elements/no_style_list/NoStyleListPackage') - let config = { name: 'simple-editor', configure: (config, options) => { @@ -40,7 +37,7 @@ let config = { config.import(BasePackage, { noBaseStyles: options.noBaseStyles }) - + config.import(SwitchTextTypePackage) config.import(ParagraphPackage) config.import(HeadingPackage) config.import(BlockquotePackage) @@ -49,23 +46,18 @@ let config = { config.import(SubscriptPackage) config.import(SuperscriptPackage) config.import(CodePackage) - // config.import(LinkPackage) config.import(PersistencePackage) - config.import(CodeblockPackage) config.import(LinkPackage) - // config.import(DialoguePackage) + config.import(SpellCheckPackage) + config.import(ListPackage) + config.import(CodeblockPackage) config.import(ExtractPackage) config.import(NotePackage) config.import(SourceNotePackage) - config.import(CommentPackage) config.import(ImagePackage) - + config.import(CommentPackage) config.import(TrackChangePackage) - - // config.import(DialoguePackage) - // config.import(NoStyleListPackage) - // config.import(NumberedListPackage) } } diff --git a/app/components/SimpleEditor/elements/codeblock/Codeblock.js b/app/components/SimpleEditor/elements/codeblock/Codeblock.js deleted file mode 100644 index ee74cf33849672de3324c7fcb34708562db45f80..0000000000000000000000000000000000000000 --- a/app/components/SimpleEditor/elements/codeblock/Codeblock.js +++ /dev/null @@ -1,7 +0,0 @@ -import { TextBlock } from 'substance' - -class Codeblock extends TextBlock {} - -Codeblock.type = 'codeblock' - -export default Codeblock diff --git a/app/components/SimpleEditor/elements/codeblock/CodeblockComponent.js b/app/components/SimpleEditor/elements/codeblock/CodeblockComponent.js deleted file mode 100644 index ef5c318cd443921f9a227e1418244d4b73a190e3..0000000000000000000000000000000000000000 --- a/app/components/SimpleEditor/elements/codeblock/CodeblockComponent.js +++ /dev/null @@ -1,10 +0,0 @@ -import { TextBlockComponent } from 'substance' - -class CodeblockComponent extends TextBlockComponent { - render ($$) { - let el = super.render.call(this, $$) - return el.addClass('sc-codeblock') - } -} - -export default CodeblockComponent diff --git a/app/components/SimpleEditor/elements/codeblock/CodeblockHTMLConverter.js b/app/components/SimpleEditor/elements/codeblock/CodeblockHTMLConverter.js deleted file mode 100644 index 61cfb2ee26ebdff9f2dfd656e13aa382d074e1e8..0000000000000000000000000000000000000000 --- a/app/components/SimpleEditor/elements/codeblock/CodeblockHTMLConverter.js +++ /dev/null @@ -1,21 +0,0 @@ -export default { - type: 'codeblock', - tagName: 'pre', - - import: function (el, node, converter) { - let codeEl = el.find('code') - if (codeEl) { - node.content = converter.annotatedText(codeEl, [node.id, 'content'], { preserveWhitespace: true }) - } - }, - - export: function (node, el, converter) { - let $$ = converter.$$ - - el.append( - $$('code').append( - converter.annotatedText([node.id, 'content']) - ) - ) - } -} diff --git a/app/components/SimpleEditor/elements/codeblock/CodeblockPackage.js b/app/components/SimpleEditor/elements/codeblock/CodeblockPackage.js deleted file mode 100644 index 0058541e28e4d2d653aec40a6f463fa06e291175..0000000000000000000000000000000000000000 --- a/app/components/SimpleEditor/elements/codeblock/CodeblockPackage.js +++ /dev/null @@ -1,27 +0,0 @@ -import Codeblock from './Codeblock' -import CodeblockComponent from './CodeblockComponent' -import CodeblockHTMLConverter from './CodeblockHTMLConverter' - -export default { - name: 'codeblock', - configure: function (config) { - config.addNode(Codeblock) - - config.addComponent('codeblock', CodeblockComponent) - config.addConverter('html', CodeblockHTMLConverter) - - config.addTextType({ - name: 'codeblock', - data: {type: 'codeblock'} - }) - - config.addLabel('codeblock', { - en: 'Codeblock', - de: 'Codeblock' - }) - } - - // Codeblock: Codeblock, - // CodeblockComponent: CodeblockComponent, - // CodeblockHTMLConverter: CodeblockHTMLConverter -} diff --git a/app/components/SimpleEditor/elements/codeblock/codeblock.scss b/app/components/SimpleEditor/elements/codeblock/codeblock.scss deleted file mode 100644 index 55c6293f75344ca0ac7dc5db405c797a499319fe..0000000000000000000000000000000000000000 --- a/app/components/SimpleEditor/elements/codeblock/codeblock.scss +++ /dev/null @@ -1,10 +0,0 @@ -.sc-codeblock { - font-family: var(--font-family-code); - font-size: 15px; -} - -// Toolbar styles -// .sc-switch-text-type .se-option.sm-codeblock { -// font-family: var(--font-family-code); -// font-size: 15px; -// } diff --git a/app/components/SimpleEditor/elements/comment/CommentBubble.js b/app/components/SimpleEditor/elements/comment/CommentBubble.js index 523c590e733e330ba439cd492e098bf1f12ddf86..5c79d60fd28b04a05adbc54dc4042ac789d60591 100644 --- a/app/components/SimpleEditor/elements/comment/CommentBubble.js +++ b/app/components/SimpleEditor/elements/comment/CommentBubble.js @@ -1,5 +1,4 @@ import { - createAnnotation, DefaultDOMElement, FontAwesomeIcon as Icon, Tool @@ -29,6 +28,7 @@ class CommentBubble extends Tool { // calculated relative to the overlay container, which gets positioned // wrong on resize (substance bug -- TODO) didMount () { + this.context.editorSession.onUpdate('', this.position, this) this.position() DefaultDOMElement.getBrowserWindow().on('resize', this.didUpdate, this) } @@ -52,7 +52,7 @@ class CommentBubble extends Tool { if (!surface) return const documentElement = document.querySelector('.se-content') - const overlayContainer = document.querySelector('.sc-overlay-container') + const overlayContainer = document.querySelector('.sc-overlay') setTimeout(() => { // read comment below const documentElementWidth = documentElement.offsetWidth @@ -62,7 +62,9 @@ class CommentBubble extends Tool { // unhide it first, as the bubble has no height otherwise this.el.removeClass('sc-overlay-bubble-hidden') - const hints = surface.getBoundingRectangleForSelection() + let wsel = window.getSelection() + let wrange = wsel.getRangeAt(0) + const hints = wrange.getBoundingClientRect() const selectionHeight = hints.height const bubbleHeight = this.el.getHeight() const cheat = 3 @@ -89,8 +91,8 @@ class CommentBubble extends Tool { return commandStates.comment } - getDocumentSession () { - return this.context.documentSession + getEditorSession () { + return this.context.editorSession } getMode () { @@ -103,8 +105,8 @@ class CommentBubble extends Tool { } getSelection () { - const documentSession = this.getDocumentSession() - return documentSession.getSelection() + const editorSession = this.getEditorSession() + return editorSession.getSelection() } // TODO -- get from provider @@ -137,12 +139,15 @@ class CommentBubble extends Tool { const newNode = { selection: selection, - node: { type: 'comment' } + type: 'comment', + path: selection.path, + start: selection.start, + end: selection.end } - surface.transaction((tx, args) => { - const annotation = createAnnotation(tx, newNode) - provider.focusTextArea(annotation.node.id) + surface.editorSession.transaction((tx, args) => { + const annotation = tx.create(newNode) + provider.focusTextArea(annotation.id) }) } } diff --git a/app/components/SimpleEditor/elements/comment/CommentComponent.js b/app/components/SimpleEditor/elements/comment/CommentComponent.js index 97a118c7ebce4701f95bf95711bcf1432cce0710..93a16e095446b4815d995ed958751e546ba67a33 100644 --- a/app/components/SimpleEditor/elements/comment/CommentComponent.js +++ b/app/components/SimpleEditor/elements/comment/CommentComponent.js @@ -29,7 +29,8 @@ class CommentComponent extends AnnotationComponent { return el } - shouldRerender (newProps) { + // TODO -- this was shouldRerender, but that stopped working. why? + shouldRedraw (newProps) { if (this.hasNodeChanged()) { this.active = this.props.node.active this.rerender() @@ -46,7 +47,7 @@ class CommentComponent extends AnnotationComponent { didMount () { const provider = this.getProvider() - provider.on('comments:updated', this.shouldRerender, this) + provider.on('comments:updated', this.shouldRedraw, this) } getProvider () { diff --git a/app/components/SimpleEditor/elements/comment/CommentPackage.js b/app/components/SimpleEditor/elements/comment/CommentPackage.js index da822a5edf5cd56c119436b1fac78f097f739ed3..a9ebb53b88d1b097ab551dd8567f488831dcb7e9 100644 --- a/app/components/SimpleEditor/elements/comment/CommentPackage.js +++ b/app/components/SimpleEditor/elements/comment/CommentPackage.js @@ -7,7 +7,10 @@ import ResolvedCommentPackage from './ResolvedCommentPackage' export default { name: 'comment', - configure: function (config) { + configure: function (config, { + disableCollapsedCursor, // TODO -- should delete? + toolGroup + }) { config.import(ResolvedCommentPackage) config.addNode(Comment) @@ -15,10 +18,13 @@ export default { config.addComponent(Comment.type, CommentComponent) config.addConverter('html', CommentHTMLConverter) - config.addCommand(Comment.type, CommentCommand, { nodeType: Comment.type }) + config.addCommand(Comment.type, CommentCommand, { + disableCollapsedCursor, // TODO -- same as above + nodeType: Comment.type + }) + config.addTool('comment', CommentBubble, { - target: 'overlay', - triggerOnCursorMove: true + toolGroup: 'overlay' }) config.addIcon('comment', { 'fontawesome': 'fa-comment' }) diff --git a/app/components/SimpleEditor/elements/comment/comment.scss b/app/components/SimpleEditor/elements/comment/comment.scss index 5bc959d19de9e72a960a8afc9b4f6389f5e99a8f..65df65a2e00f51ed0fc3aac3548158e4b0c4c3ca 100644 --- a/app/components/SimpleEditor/elements/comment/comment.scss +++ b/app/components/SimpleEditor/elements/comment/comment.scss @@ -17,7 +17,7 @@ $white: #fff; background-color: $light-green; } -.sc-prose-editor-overlay-tools { +.sc-overlay { .se-active-tools { .sc-overlay-bubble { background: $white; diff --git a/app/components/SimpleEditor/elements/elements.scss b/app/components/SimpleEditor/elements/elements.scss index b19c57bfeed185f0dcdbc6c68b8c97aaa5e4fb13..f7b1616133c70a8f612df3ac17a6fed491a44e24 100644 --- a/app/components/SimpleEditor/elements/elements.scss +++ b/app/components/SimpleEditor/elements/elements.scss @@ -1,4 +1,3 @@ -@import './codeblock/codeblock'; @import './comment/comment'; // @import './dialogue/dialogue'; @import './extract/extract'; @@ -11,3 +10,5 @@ @import './track_change/trackChange'; @import './images/image'; +@import './modal_warning/modalWarning'; +@import './list/customLists'; diff --git a/app/components/SimpleEditor/elements/extract/extract.scss b/app/components/SimpleEditor/elements/extract/extract.scss index e18c6e5597eccc9fd72224ffbeb9034ee53ce388..bf279acd8ef67c262d90561646ee21b5e21308df 100644 --- a/app/components/SimpleEditor/elements/extract/extract.scss +++ b/app/components/SimpleEditor/elements/extract/extract.scss @@ -1,4 +1,4 @@ -.sc-extract { +.sc-prose-editor .se-content .sc-extract { font-family: 'Fira Sans'; font-style: italic; font-weight: 300; diff --git a/app/components/SimpleEditor/elements/images/Image.js b/app/components/SimpleEditor/elements/images/Image.js deleted file mode 100644 index d48d44ce793740169d1506d1d4a9c5ed78a512d1..0000000000000000000000000000000000000000 --- a/app/components/SimpleEditor/elements/images/Image.js +++ /dev/null @@ -1,10 +0,0 @@ -import { DocumentNode } from 'substance' - -class Image extends DocumentNode {} - -Image.define({ - type: 'image', - src: { type: 'string', default: 'http://' } -}) - -export default Image diff --git a/app/components/SimpleEditor/elements/images/ImageComponent.js b/app/components/SimpleEditor/elements/images/ImageComponent.js index 11e6f89c8a2552a9eac8e3b7a2586725148b744c..65bc57d2133c1f2cadc52ddd0716376f0423d8b4 100644 --- a/app/components/SimpleEditor/elements/images/ImageComponent.js +++ b/app/components/SimpleEditor/elements/images/ImageComponent.js @@ -4,69 +4,39 @@ class ImageComponent extends BlockNodeComponent { didMount () { super.didMount.call(this) - const { node } = this.props - - node.on('upload:started', this.onUploadStarted, this) - node.on('upload:finished', this.onUploadFinished, this) - node.on('upload:failed', this.onUploadFailed, this) + this.context.editorSession.onRender('document', this._onDocumentChange, this) } dispose () { super.dispose.call(this) - const { node } = this.props - node.off(this) + this.context.editorSession.off(this) + } + + _onDocumentChange (change) { + if (change.isAffected(this.props.node.id) || + change.isAffected(this.props.node.imageFile)) { + this.rerender() + } } render ($$) { - let el = super.render.call(this, $$) + let el = super.render($$) el.addClass('sc-image') - el.removeClass('') // An empty class is added for some reason so remove it - el.append( $$('img').attr({ - src: this.props.node.src + src: this.props.node.getUrl() }).ref('image') ) - if (this.state.uploading) { - let progressBar = $$('div') - .addClass('se-progress-bar') - .ref('progressBar') - .append('Uploading ...') - el.append(progressBar) - } - - return el - } - - // TODO -- extend state - onUploadStarted () { - this.setState({ uploading: true }) - } - - onUploadFinished () { - this.setState({}) - const editor = this.getEditor() editor.emit('ui:updated') - } - getEditor () { - return this.context.controller + return el } - onUploadFailed () { - const surface = this.context.surface - let nodeId = this.props.node.id - - const transformation = (tx, args) => { - args.nodeId = nodeId - deleteNode(tx, args) - } - - surface.transaction(transformation) + getEditor () { + return this.context.editor } - } export default ImageComponent diff --git a/app/components/SimpleEditor/elements/images/ImageFileProxy.js b/app/components/SimpleEditor/elements/images/ImageFileProxy.js new file mode 100644 index 0000000000000000000000000000000000000000..99a9dba7eca12575a5eb841b087f4c583910baa6 --- /dev/null +++ b/app/components/SimpleEditor/elements/images/ImageFileProxy.js @@ -0,0 +1,42 @@ +import { FileProxy } from 'substance' + +class ImageFileProxy extends FileProxy { + + constructor (fileNode, context) { + super(fileNode, context) + this.file = fileNode.sourceFile + + if (this.file) { + this._fileUrl = URL.createObjectURL(this.file) + } + this.url = fileNode.url + } + + getUrl () { + // if we have fetched the url already, just serve it here + if (this.url) { + return this.url + } + // if we have a local file, use it's data URL + if (this._fileUrl) { + return this._fileUrl + } + // no URL available + return '' + } + + sync () { + if (!this.url) { + this.context.editorSession.saveHandler.uploadFile(this.file).then((res) => { + this.url = res.file + FileProxy.prototype.triggerUpdate.call(this) + }) + } + } +} + +ImageFileProxy.match = function(fileNode, context) { // eslint-disable-line + return fileNode.fileType === 'image' +} + +export default ImageFileProxy diff --git a/app/components/SimpleEditor/elements/images/ImageHTMLConverter.js b/app/components/SimpleEditor/elements/images/ImageHTMLConverter.js index 485bb4d2d958c38e3b475edc7e8f3902e14997c8..46e452d9f7eb4ac6be9979c0d6d887214b201026 100644 --- a/app/components/SimpleEditor/elements/images/ImageHTMLConverter.js +++ b/app/components/SimpleEditor/elements/images/ImageHTMLConverter.js @@ -6,11 +6,18 @@ export default { type: 'image', tagName: 'img', - import: function (el, node) { - node.src = el.attr('src') + import: function (el, node, converter) { + let imageFile = converter.createNode({ + id: 'file-' + node.id, + type: 'file', + fileType: 'image', + url: el.attr('src') + }) + node.imageFile = imageFile.id }, export: function (node, el) { - el.attr('src', node.src) + let imageFile = node.document.get(node.imageFile) + el.attr('src', imageFile.getUrl()) } } diff --git a/app/components/SimpleEditor/elements/images/ImageNode.js b/app/components/SimpleEditor/elements/images/ImageNode.js new file mode 100644 index 0000000000000000000000000000000000000000..18052651c351ae9b0c1e677e51c31838ad5217d9 --- /dev/null +++ b/app/components/SimpleEditor/elements/images/ImageNode.js @@ -0,0 +1,23 @@ +import { DocumentNode } from 'substance' + +class ImageNode extends DocumentNode { + getImageFile () { + if (this.imageFile) { + return this.document.get(this.imageFile) + } + } + + getUrl () { + let imageFile = this.getImageFile() + if (imageFile) { + return imageFile.getUrl() + } + } +} + +ImageNode.schema = { + type: 'image', + imageFile: { type: 'file' } +} + +export default ImageNode diff --git a/app/components/SimpleEditor/elements/images/ImagePackage.js b/app/components/SimpleEditor/elements/images/ImagePackage.js index 085c60425dc7be5329197eb007faf283572eb3a3..56388f25e36650e78e43a71863fc1826f8035b58 100644 --- a/app/components/SimpleEditor/elements/images/ImagePackage.js +++ b/app/components/SimpleEditor/elements/images/ImagePackage.js @@ -1,8 +1,9 @@ -import ImageNode from './Image' +import ImageNode from './ImageNode' import ImageComponent from './ImageComponent' import ImageHTMLConverter from './ImageHTMLConverter' import InsertImageCommand from './InsertImageCommand' import InsertImageTool from './InsertImageTool' +import ImageFileProxy from './ImageFileProxy' export default { name: 'image', @@ -11,10 +12,11 @@ export default { config.addComponent('image', ImageComponent) config.addConverter('html', ImageHTMLConverter) config.addCommand('insert-image', InsertImageCommand) - config.addTool('insert-image', InsertImageTool, { target: 'insert' }) + config.addTool('insert-image', InsertImageTool, { toolGroup: 'annotations' }) config.addIcon('insert-image', { 'fontawesome': 'fa-image' }) config.addLabel('image', { en: 'Image' }) config.addLabel('insert-image', { en: 'Insert image' }) + config.addFileProxy(ImageFileProxy) }, ImageNode: ImageNode, ImageComponent: ImageComponent, diff --git a/app/components/SimpleEditor/elements/images/InsertImageCommand.js b/app/components/SimpleEditor/elements/images/InsertImageCommand.js index b5af426686cf1318fe0731f60d753add38cb4397..b23cde7c8cf510eda990a4b318f41c9da9f8f34a 100644 --- a/app/components/SimpleEditor/elements/images/InsertImageCommand.js +++ b/app/components/SimpleEditor/elements/images/InsertImageCommand.js @@ -1,4 +1,4 @@ -import { Command, pasteContent } from 'substance' +import { Command } from 'substance' class ImageCommand extends Command { constructor () { @@ -19,74 +19,31 @@ class ImageCommand extends Command { return newState } - /** - Inserts (stub) images and triggers a fileupload. - After upload has completed, the image URLs get updated. - */ - execute (params, context) { - let state = this.getCommandState(params) - // Return if command is disabled - if (state.disabled) return + execute (params) { + let editorSession = params.editorSession - let documentSession = params.documentSession - let sel = params.selection - let surface = params.surface - let files = params.files - - // can drop images only into container editors - if (!surface.isContainerEditor()) return - - // creating a small doc where we add the images - // and then we use the paste transformation to get this snippet - // into the real doc - let doc = surface.getDocument() - let snippet = doc.createSnippet() - - // as file upload takes longer we will insert stub images - let items = files.map(function (file) { - let node = snippet.create({ type: 'image' }) - snippet.show(node) - return { - file: file, - nodeId: node.id - } - }) - - surface.transaction(function (tx) { - tx.before.selection = sel - return pasteContent(tx, { - selection: sel, - containerId: surface.getContainerId(), - doc: snippet + editorSession.transaction((tx) => { + params.files.forEach((file) => { + this._insertImage(tx, file) }) }) - // start uploading - items.forEach(function (item) { - let nodeId = item.nodeId - let file = item.file - let node = doc.get(nodeId) - node.emit('upload:started') - context.documentSession.emit('fileUploadTrigger', file, (filePath, error) => { - let node = doc.get(nodeId) - if (error != null) { - node.emit('upload:failed') - return - } - if (node) { - documentSession.transaction(function (tx) { - tx.set([nodeId, 'src'], filePath) - }) - node.emit('upload:finished') - } - }) + editorSession.fileManager.sync() + } + + _insertImage (tx, file) { + let imageFile = tx.create({ + type: 'file', + fileType: 'image', + mimeType: file.type, + sourceFile: file }) - return { - status: 'file-upload-process-started' - } + tx.insertBlockNode({ + type: 'image', + imageFile: imageFile.id + }) } - } export default ImageCommand diff --git a/app/components/SimpleEditor/elements/list/InsertListCommand.js b/app/components/SimpleEditor/elements/list/InsertListCommand.js new file mode 100644 index 0000000000000000000000000000000000000000..6e359f2cab5aa3d67213acd9d03776388ddf52d2 --- /dev/null +++ b/app/components/SimpleEditor/elements/list/InsertListCommand.js @@ -0,0 +1,31 @@ +import { Command } from 'substance' + +class InsertListCommand extends Command { + getCommandState (params) { + let sel = this._getSelection(params) + let commandState = {} + let _disabledCollapsedCursor = this.config.disableCollapsedCursor && sel.isCollapsed() + if (_disabledCollapsedCursor || !sel.isPropertySelection()) { + commandState.disabled = true + } + return commandState + } + execute (params) { + let ordered = this.config.ordered + let customValue = null + if (this.config.custom) { + customValue = this.config.custom + } + + let editorSession = params.editorSession + editorSession.transaction((tx) => { + if (customValue !== null) { + tx.toggleList({ ordered: ordered, custom: customValue }) + } else { + tx.toggleList({ ordered: ordered }) + } + }) + } +} + +export default InsertListCommand diff --git a/app/components/SimpleEditor/elements/list/InsertListTool.js b/app/components/SimpleEditor/elements/list/InsertListTool.js new file mode 100644 index 0000000000000000000000000000000000000000..d732525000429ffe634c6b393f9c7376efd81400 --- /dev/null +++ b/app/components/SimpleEditor/elements/list/InsertListTool.js @@ -0,0 +1,17 @@ +import { Tool } from 'substance' + +class InsertListTool extends Tool { + getClassNames () { + return 'sc-insert-list-tool' + } + renderButton ($$) { + let button = super.renderButton($$) + return [ button ] + } + onClick () { + this.executeCommand({ + context: this.context + }) + } +} +export default InsertListTool diff --git a/app/components/SimpleEditor/elements/list/ListComponent.js b/app/components/SimpleEditor/elements/list/ListComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..51b2d4e4b8f5e1ae2b8a8e764ca95a2eb6a7d9c5 --- /dev/null +++ b/app/components/SimpleEditor/elements/list/ListComponent.js @@ -0,0 +1,60 @@ +/* eslint react/prop-types: 0 */ +import { isString, Component } from 'substance' +import ListItemComponent from './ListItemComponent' +import renderListNode from './renderListNode' +import getListTagName from './getListTagName' + +class ListComponent extends Component { + + didMount () { + this.context.editorSession.onRender('document', this._onChange, this) + } + + render ($$) { + let node = this.props.node + let customClass = '' + + if (this.props.node.custom) { + customClass = '-' + this.props.node.custom + } + + let el = $$(getListTagName(node)) + .addClass('sc-list-' + getListTagName(node) + customClass) + .attr('data-id', node.id) + + renderListNode(node, el, (arg) => { + if (isString(arg)) { + return $$(arg) + } else if (arg.type === 'list-item') { + let item = arg + return $$(ListItemComponent, { + path: [item.id, 'content'], + node: item, + tagName: 'li' + }) + // setting ref to preserve items when rerendering + .ref(item.id) + } + }) + return el + } + + _onChange (change) { + const node = this.props.node + if (change.isAffected(node.id)) { + return this.rerender() + } + // check if any of the list items are affected + let itemIds = node.items + for (let i = 0; i < itemIds.length; i++) { + if (change.isAffected([itemIds[i], 'level'])) { + return this.rerender() + } + } + } +} + +// we need this ATM to prevent this being wrapped into an isolated node (see ContainerEditor._renderNode()) +ListComponent.prototype._isCustomNodeComponent = true + +export default ListComponent diff --git a/app/components/SimpleEditor/elements/list/ListHTMLConverter.js b/app/components/SimpleEditor/elements/list/ListHTMLConverter.js new file mode 100644 index 0000000000000000000000000000000000000000..c01b0ad52df9148d88dcfebc683e949b694c118f --- /dev/null +++ b/app/components/SimpleEditor/elements/list/ListHTMLConverter.js @@ -0,0 +1,78 @@ +/* eslint react/prop-types: 0 */ + +import { isString } from 'substance' +import renderListNode from './renderListNode' +import getListTagName from './getListTagName' + +/* + HTML converter for Lists. + */ +export default { + + type: 'list', + + matchElement: function (el) { + return el.is('ul') || el.is('ol') + }, + + import: function (el, node, converter) { + let self = this + + if (el.attr('styling')) { + node.custom = el.attr('styling') + } + + this._santizeNestedLists(el) + if (el.is('ol')) { + node.ordered = true + } + let itemEls = el.findAll('li') + itemEls.forEach(function (li) { + // ATTENTION: pulling out nested list elements here on-the-fly + let listItem = converter.convertElement(li) + listItem.level = _getLevel(li) + node.items.push(listItem.id) + }) + function _getLevel (li) { + let _el = li + let level = 1 + while (_el) { + if (_el.parentNode === el) return level + _el = _el.parentNode + if (self.matchElement(_el)) level++ + } + } + }, + + export: function (node, el, converter) { + let $$ = converter.$$ + el.tagName = getListTagName(node) + + if (node.custom) { + el.attr('styling', node.custom) + } + + renderListNode(node, el, (arg) => { + if (isString(arg)) { + return $$(arg) + } else { + let item = arg + return $$('li').append(converter.annotatedText(item.getTextPath())) + } + }) + return el + }, + + _santizeNestedLists (root) { + let nestedLists = root.findAll('ol,ul') + nestedLists.forEach((el) => { + while (!el.parentNode.is('ol,ul')) { + // pull it up + let parent = el.parentNode + let grandParent = parent.parentNode + let pos = grandParent.getChildIndex(parent) + grandParent.insertAt(pos + 1, el) + } + }) + } +} diff --git a/app/components/SimpleEditor/elements/list/ListItemComponent.js b/app/components/SimpleEditor/elements/list/ListItemComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..b0c738ddf92a5bdf00580d799fdcbbdd71a64eb2 --- /dev/null +++ b/app/components/SimpleEditor/elements/list/ListItemComponent.js @@ -0,0 +1,5 @@ +import { TextPropertyComponent } from 'substance' + +class ListItemComponent extends TextPropertyComponent {} + +export default ListItemComponent diff --git a/app/components/SimpleEditor/elements/list/ListItemHTMLConverter.js b/app/components/SimpleEditor/elements/list/ListItemHTMLConverter.js new file mode 100644 index 0000000000000000000000000000000000000000..3b3ff80a5f3230f0c9487ef04e6677527473895f --- /dev/null +++ b/app/components/SimpleEditor/elements/list/ListItemHTMLConverter.js @@ -0,0 +1,19 @@ +/* + * HTML converter for Lists. + */ +export default { + + type: 'list-item', + + matchElement: function (el) { + return el.is('li') + }, + + import: function (el, node, converter) { + node.content = converter.annotatedText(el, [node.id, 'content']) + }, + + export: function (node, el, converter) { + el.append(converter.annotatedText(node.getTextPath())) + } +} diff --git a/app/components/SimpleEditor/elements/list/ListItemNode.js b/app/components/SimpleEditor/elements/list/ListItemNode.js new file mode 100644 index 0000000000000000000000000000000000000000..59a749f6a3f45fbf845c321d0b00cd56599bafc8 --- /dev/null +++ b/app/components/SimpleEditor/elements/list/ListItemNode.js @@ -0,0 +1,11 @@ +import { TextNode } from 'substance' + +class ListItem extends TextNode {} + +ListItem.type = 'list-item' + +ListItem.schema = { + level: { type: 'number', default: 1 } +} + +export default ListItem diff --git a/app/components/SimpleEditor/elements/list/ListNode.js b/app/components/SimpleEditor/elements/list/ListNode.js new file mode 100644 index 0000000000000000000000000000000000000000..c4682ee4a61c33b1de068016a654e4ea6393a3d9 --- /dev/null +++ b/app/components/SimpleEditor/elements/list/ListNode.js @@ -0,0 +1,73 @@ +import { DocumentNode } from 'substance' + +class ListNode extends DocumentNode { + + getItemAt (idx) { + return this.getDocument().get(this.items[idx]) + } + + getFirstItem () { + return this.getItemAt(0) + } + + getLastItem () { + return this.getItemAt(this.getLength() - 1) + } + + getItems () { + const doc = this.getDocument() + return this.items.map((id) => { + return doc.get(id) + }) + } + + getItemPosition (itemId) { + if (itemId._isNode) itemId = itemId.id + let pos = this.items.indexOf(itemId) + if (pos < 0) throw new Error('Item is not within this list: ' + itemId) + return pos + } + + insertItemAt (pos, itemId) { + const doc = this.getDocument() + doc.update([this.id, 'items'], { type: 'insert', pos: pos, value: itemId }) + } + + appendItem (itemId) { + this.insertItemAt(this.items.length, itemId) + } + + removeItemAt (pos) { + const doc = this.getDocument() + doc.update([this.id, 'items'], { type: 'delete', pos: pos }) + } + + remove (itemId) { + const doc = this.getDocument() + const pos = this.getItemPosition(itemId) + if (pos >= 0) { + doc.update([this.id, 'items'], { type: 'delete', pos: pos }) + } + } + + getLength () { + return this.items.length + } + + get length () { + return this.getLength() + } +} + +ListNode.isList = true + +ListNode.type = 'list' + +ListNode.schema = { + ordered: { type: 'boolean', default: false }, + custom: { type: 'string', optional: true }, + // list-items are owned by the list + items: { type: [ 'array', 'id' ], default: [], owned: true } +} + +export default ListNode diff --git a/app/components/SimpleEditor/elements/list/ListPackage.js b/app/components/SimpleEditor/elements/list/ListPackage.js new file mode 100644 index 0000000000000000000000000000000000000000..7a0ebcbcf0ba5f4bed28e379701191ec8bc55884 --- /dev/null +++ b/app/components/SimpleEditor/elements/list/ListPackage.js @@ -0,0 +1,65 @@ +import ListNode from './ListNode' +import ListItemNode from './ListItemNode' +import ListComponent from './ListComponent' +import ListHTMLConverter from './ListHTMLConverter' +import ListItemHTMLConverter from './ListItemHTMLConverter' +import InsertListCommand from './InsertListCommand' +import InsertListTool from './InsertListTool' + +export default { + name: 'list', + configure: function (config, {toolGroup, disableCollapsedCursor}) { + config.addNode(ListNode) + config.addNode(ListItemNode) + config.addComponent('list', ListComponent) + + config.addCommand('insert-unordered-list', InsertListCommand, { + nodeType: 'list', + ordered: false, + disableCollapsedCursor + }) + config.addTool('insert-unordered-list', InsertListTool, { toolGroup }) + config.addLabel('insert-unordered-list', { + en: 'Unordered list' + }) + config.addIcon('insert-unordered-list', { 'fontawesome': 'fa-list-ul' }) + + config.addCommand('insert-ordered-list', InsertListCommand, { + nodeType: 'list', + ordered: true, + disableCollapsedCursor + }) + config.addTool('insert-ordered-list', InsertListTool, { toolGroup }) + config.addLabel('insert-ordered-list', { + en: 'Ordered list' + }) + config.addIcon('insert-ordered-list', { 'fontawesome': 'fa-list-ol' }) + + config.addCommand('insert-qa-list', InsertListCommand, { + nodeType: 'list', + ordered: true, + custom: 'qa', + disableCollapsedCursor + }) + config.addTool('insert-qa-list', InsertListTool, { toolGroup }) + config.addLabel('insert-qa-list', { + en: 'QA list' + }) + config.addIcon('insert-qa-list', { 'fontawesome': 'fa-quora' }) + + config.addCommand('insert-unstyled-list', InsertListCommand, { + nodeType: 'list', + ordered: true, + custom: 'unstyled', + disableCollapsedCursor + }) + config.addTool('insert-unstyled-list', InsertListTool, { toolGroup }) + config.addLabel('insert-unstyled-list', { + en: 'Unstyled list' + }) + config.addIcon('insert-unstyled-list', { 'fontawesome': 'fa-bars' }) + + config.addConverter('html', ListHTMLConverter) + config.addConverter('html', ListItemHTMLConverter) + } +} diff --git a/app/components/SimpleEditor/elements/list/customLists.css b/app/components/SimpleEditor/elements/list/customLists.css new file mode 100644 index 0000000000000000000000000000000000000000..1e8739285de9be7dbe1fa161f2ceb4c47518be35 --- /dev/null +++ b/app/components/SimpleEditor/elements/list/customLists.css @@ -0,0 +1,20 @@ +.sc-prose-editor div.se-content .sc-list-ol-qa { + list-style-type: none; + + li:nth-child(odd) { + &:before { + content: 'Q: '; + font-weight: bold; + } + } + + li:nth-child(even) { + &:before { + content: 'A: '; + font-weight: bold; + } + } +} +sc-prose-editor div.se-content .sc-list-ol-unstyled { + list-style-type: none; +} diff --git a/app/components/SimpleEditor/elements/list/getListTagName.js b/app/components/SimpleEditor/elements/list/getListTagName.js new file mode 100644 index 0000000000000000000000000000000000000000..78dff433f7d518f76fc5df4e3a28fb83edd49f3d --- /dev/null +++ b/app/components/SimpleEditor/elements/list/getListTagName.js @@ -0,0 +1,4 @@ +export default function getListTagName (node) { + // TODO: we might want to have different types for different levels + return node.ordered ? 'ol' : 'ul' +} diff --git a/app/components/SimpleEditor/elements/list/renderListNode.js b/app/components/SimpleEditor/elements/list/renderListNode.js new file mode 100644 index 0000000000000000000000000000000000000000..7e25b5fb73a51b943c110af9e6cf86a77e1eb55d --- /dev/null +++ b/app/components/SimpleEditor/elements/list/renderListNode.js @@ -0,0 +1,29 @@ +import getListTagName from './getListTagName' +import last from '../../../utils/last' + +export default function renderListNode (node, rootEl, createElement) { + let items = node.getItems() + let stack = [rootEl] + for (let i = 0; i < items.length; i++) { + let item = items[i] + if (item.level < stack.length) { + for (let j = stack.length; j > item.level; j--) { + stack.pop() + } + } else if (item.level > stack.length) { + for (let j = stack.length; j < item.level; j++) { + // Note: ATM all sublists have the same order type + let sublist = createElement(getListTagName(node)) + last(stack).append(sublist) + stack.push(sublist) + } + } + console.assert(item.level === stack.length, 'item.level should now be the same as stack.length') + last(stack).append( + createElement(item) + ) + } + for (let j = stack.length; j > 1; j--) { + stack.pop() + } +} diff --git a/app/components/SimpleEditor/elements/modal_warning/ModalWarning.js b/app/components/SimpleEditor/elements/modal_warning/ModalWarning.js new file mode 100644 index 0000000000000000000000000000000000000000..627b17e247de78e48d6a4a2258da66e3e9c4d0da --- /dev/null +++ b/app/components/SimpleEditor/elements/modal_warning/ModalWarning.js @@ -0,0 +1,79 @@ +import { each } from 'lodash' +import { Modal } from 'substance' + +class ModalWarning extends Modal { + constructor (props) { + super(props) + + this.parent.on('send:route', this._getNextRoute, this) + this.route = '' + } + + render ($$) { + let el = $$('div') + .addClass('sc-modal') + + const modalHeader = $$('div') + .addClass('sc-modal-header') + .append('Unsaved Content') + + const modalMessage = $$('div') + .addClass('sc-modal-body') + .append('Are you sure you want to exit the chapter without saving ?') + + const saveExit = $$('button') + .addClass('sc-modal-button') + .addClass('sc-modal-button-save-exit') + .append('save & exit') + .on('click', this._saveExitWriter) + + const exit = $$('button') + .addClass('sc-modal-button') + .addClass('sc-modal-button-exit') + .append('exit') + .on('click', this._exitWriter) + + const modalActionsContainer = $$('div') + .addClass('sc-modal-actions') + .append(saveExit) + .append(exit) + + if (this.props.width) { + el.addClass('sm-width-' + this.props.width) + } + + el.append( + $$('div').addClass('se-body') + .append(modalHeader) + .append(modalMessage) + .append(modalActionsContainer) + ) + + return el + } + + _getNextRoute (route) { + this.route = route + } + + _saveExitWriter () { + this.rerender() + this.context.editor.editorSession.save() + + // TODO -- ?? + setTimeout(() => { this.context.editor.props.history.push(this.route) }, 200) + } + + // TODO: Hack Check why cannot rerender editor so can push to url + // if you save document session everything rerenders + _exitWriter () { + each(this.context.editor.editorSession._history.doneChanges, key => { + this.context.editor.editorSession.undo() + }) + + this.context.editor.editorSession.save() + setTimeout(() => { this.context.editor.props.history.push(this.route) }, 200) + } +} + +export default ModalWarning diff --git a/app/components/SimpleEditor/elements/modal_warning/modalWarning.scss b/app/components/SimpleEditor/elements/modal_warning/modalWarning.scss new file mode 100644 index 0000000000000000000000000000000000000000..8652daeb960443c510771ed01a06d04f3ea80c67 --- /dev/null +++ b/app/components/SimpleEditor/elements/modal_warning/modalWarning.scss @@ -0,0 +1,68 @@ +$background: rgba(0, 0, 0, .5); +$modal-border: #808080; +$button-bg: #d3d3d3; +$header-border-color: #e5e5e5; +$black: #000; +$hover-button-bg-color: #808080; +$red: #a52a2a; +$white: #fff; + +.sc-modal { + background: $background; + + .se-body { + border: 2px solid $modal-border; + border-radius: 0; + font-style: italic; + font-weight: 500; + padding: 1em; + width: 35%; + } + + .sc-modal-header { + border-bottom: 1px solid $header-border-color; + font-size: 24px; + line-height: 32px; + } + + .sc-modal-body { + padding: 20px 0; + } + + .sc-modal-actions { + float: right; + } + + .sc-modal-button { + background-color: $button-bg; + border: 3px solid transparent; + border-radius: 3px; + color: $black; + cursor: pointer; + display: inline-block; + font-size: 15px; + font-style: normal; + font-weight: 500; + margin-bottom: .5em; + padding: 7px 30px; + text-align: center; + text-decoration: none; + text-transform: uppercase; + + &:hover { + background-color: $hover-button-bg-color; + } + } + + .sc-modal-button-save-exit { + margin-right: 20px; + + &:hover { + color: $white; + } + } + + .sc-modal-button-exit { + color: $red; + } +} diff --git a/app/components/SimpleEditor/elements/note/EditNoteTool.js b/app/components/SimpleEditor/elements/note/EditNoteTool.js index 804bcae5f320810e755028bd2a11828164565534..3e5e7ece23993dba906ddddf30b08413563d4eb1 100644 --- a/app/components/SimpleEditor/elements/note/EditNoteTool.js +++ b/app/components/SimpleEditor/elements/note/EditNoteTool.js @@ -39,12 +39,25 @@ class EditNoteTool extends Tool { return el } + didMount () { + this.context.editorSession.onUpdate('', this.diabelTools, this) + } + + diabelTools () { + const selected = this.getSelection() + if (!selected.node) return + const commandStates = this.context.commandManager.commandStates + each(keys(commandStates), (key) => { + const allowed = ['comment', 'redo', 'save', 'switch-text-type', 'undo', 'note'] + if (!includes(allowed, key)) commandStates[key].disabled = true + }) + } + saveNote (event) { const selected = this.getSelection() const noteContent = this.el.find('textarea').val() - const documentSession = this.context.documentSession - - documentSession.transaction(function (tx, args) { + const editorSession = this.context.editorSession + editorSession.transaction(function (tx, args) { const path = [selected.node.id, 'note-content'] tx.set(path, noteContent) }) @@ -55,8 +68,8 @@ class EditNoteTool extends Tool { const surface = this.context.surfaceManager.getFocusedSurface() if (!surface) return {} - const commandStates = this.context.commandManager.getCommandStates() - const session = this.context.documentSession + const commandStates = this.context.commandManager.commandStates + const session = this.context.editorSession const sel = session.getSelection() const note = documentHelpers.getPropertyAnnotationsForSelection( @@ -66,15 +79,9 @@ class EditNoteTool extends Tool { ) // disable when larger selection that just includes a note as well - const selectionLength = (sel.endOffset - sel.startOffset === 1) + const selectionLength = (sel.end.offset - sel.start.offset === 1) if (typeof note[0] !== 'undefined' && selectionLength) { - // disable commands that don't make sense on a note - each(keys(commandStates), (key) => { - const allowed = ['comment', 'redo', 'save', 'switch-text-type', 'undo'] - if (!includes(allowed, key)) commandStates[key].disabled = true - }) - return { node: note[0] } diff --git a/app/components/SimpleEditor/elements/note/Note.js b/app/components/SimpleEditor/elements/note/Note.js index 51af949caa2935a9170c7553d515fa98642330d7..82579d2abe8647801c701959589dba0104bd6250 100644 --- a/app/components/SimpleEditor/elements/note/Note.js +++ b/app/components/SimpleEditor/elements/note/Note.js @@ -6,7 +6,7 @@ Note.define({ 'type': 'note', 'note-content': { type: 'string', - default: '' + optional: true } }) diff --git a/app/components/SimpleEditor/elements/note/NotePackage.js b/app/components/SimpleEditor/elements/note/NotePackage.js index 548d304f1c2ef59a63f300e1edbd02f9cd61bebb..53ac1427f413e6312c8b35d714250471f9b8bce3 100644 --- a/app/components/SimpleEditor/elements/note/NotePackage.js +++ b/app/components/SimpleEditor/elements/note/NotePackage.js @@ -12,11 +12,9 @@ export default { config.addComponent(Note.type, NoteComponent) config.addConverter('html', NoteHTMLConverter) - config.addCommand(Note.type, NoteCommand, { nodeType: Note.type }) - config.addTool('note', NoteTool, { target: 'insert' }) - config.addTool('note', EditNoteTool, { target: 'overlay' }) - + config.addTool('note', NoteTool, { toolGroup: 'annotations' }) + config.addTool('note', EditNoteTool, { toolGroup: 'overlay' }) config.addIcon('note', { 'fontawesome': 'fa-bookmark' }) config.addLabel('note', { en: 'Note' diff --git a/app/components/SimpleEditor/elements/note/NoteTool.js b/app/components/SimpleEditor/elements/note/NoteTool.js index ab792f138d6e27f91614b73b21352a74ff1db22a..fdd2d84748921688a862b5e6b0ca96dc14c9209c 100644 --- a/app/components/SimpleEditor/elements/note/NoteTool.js +++ b/app/components/SimpleEditor/elements/note/NoteTool.js @@ -4,14 +4,13 @@ class NoteTool extends AnnotationTool { renderButton ($$) { const el = super.renderButton($$) const readOnly = this.isSurfaceReadOnly() - if (readOnly === true) el.attr('disabled', 'true') return el } getSurface () { const surfaceManager = this.context.surfaceManager - const containerId = this.context.controller.props.containerId + const containerId = this.context.editor.props.containerId return surfaceManager.getSurface(containerId) } diff --git a/app/components/SimpleEditor/elements/note/PromptTextArea.js b/app/components/SimpleEditor/elements/note/PromptTextArea.js index fc6ca23278bd7819850a1286a07092296b3b73aa..a431f57c5c1bca2d8ccdf7319874f2a86d07d357 100644 --- a/app/components/SimpleEditor/elements/note/PromptTextArea.js +++ b/app/components/SimpleEditor/elements/note/PromptTextArea.js @@ -6,8 +6,8 @@ class TextArea extends Component { render ($$) { const { disabled, path, placeholder, rows } = this.props - const documentSession = this.context.documentSession - const doc = documentSession.getDocument() + const editorSession = this.context.editorSession + const doc = editorSession.getDocument() const val = doc.get(path) const el = $$('textarea') diff --git a/app/components/SimpleEditor/elements/note/note.scss b/app/components/SimpleEditor/elements/note/note.scss index 300847a5e8975757114ef56c34738dded8672775..18ee7010c649834b85881724a5ab07cfe5a93fb4 100644 --- a/app/components/SimpleEditor/elements/note/note.scss +++ b/app/components/SimpleEditor/elements/note/note.scss @@ -1,12 +1,15 @@ $gray: #eee; $red: #591818; - +.hidden { + display: none; +} .sc-prose-editor { counter-reset: note; .sc-note { color: $red; font-weight: bold; + display: inline-block; } .sc-note::after { diff --git a/app/components/SimpleEditor/elements/track_change/TrackChangeComponent.js b/app/components/SimpleEditor/elements/track_change/TrackChangeComponent.js index 7785501a4086f611bff7038eb22b06f1f507e4be..6a0d7224e23824273116e564d6faf57ef0ca0709 100644 --- a/app/components/SimpleEditor/elements/track_change/TrackChangeComponent.js +++ b/app/components/SimpleEditor/elements/track_change/TrackChangeComponent.js @@ -1,6 +1,11 @@ import { AnnotationComponent } from 'substance' class TrackChangeComponent extends AnnotationComponent { + + didMount () { + this.context.editorSession.onUpdate('document', this.onTrackChangesUpdated, this) + } + render ($$) { const { id, status, user } = this.props.node const canAct = this.canAct() @@ -68,8 +73,8 @@ class TrackChangeComponent extends AnnotationComponent { provider.resolve(annotation, action) } - getController () { - return this.context.controller + getEditor () { + return this.context.editor } getProvider () { @@ -77,8 +82,8 @@ class TrackChangeComponent extends AnnotationComponent { } getViewMode () { - const controller = this.getController() - const { trackChangesView } = controller.state + const editor = this.getEditor() + const { trackChangesView } = editor.state return trackChangesView } @@ -86,10 +91,11 @@ class TrackChangeComponent extends AnnotationComponent { const annotation = this.props.node const annotationSelection = annotation.getSelection() - const surface = this.context.surfaceParent - const selection = surface.getSelection() + const surface = this.context.surface - if (selection.isNull() || selection.isCollapsed()) return false + const selection = surface.editorSession.getSelection() + + if (selection.isNodeSelection || selection.isNull() || selection.isCollapsed()) return false const overlaps = selection.overlaps(annotationSelection) const contains = selection.contains(annotationSelection) @@ -97,6 +103,11 @@ class TrackChangeComponent extends AnnotationComponent { if (overlaps && !contains) return true return false } + + onTrackChangesUpdated (change) { + const trackChangesProvider = this.getProvider() + trackChangesProvider.handleDocumentChange(change) + } } export default TrackChangeComponent diff --git a/app/components/SimpleEditor/elements/track_change/TrackChangeControlTool.js b/app/components/SimpleEditor/elements/track_change/TrackChangeControlTool.js index 78761b13f99b0319b3507a5663a95653c42c2c13..4497a5c70aa9cbda43103d704b31db3ce96f9f47 100644 --- a/app/components/SimpleEditor/elements/track_change/TrackChangeControlTool.js +++ b/app/components/SimpleEditor/elements/track_change/TrackChangeControlTool.js @@ -1,6 +1,7 @@ import { Tool } from 'substance' class TrackChangeControlTool extends Tool { + renderButton ($$) { const el = super.renderButton($$) @@ -11,21 +12,13 @@ class TrackChangeControlTool extends Tool { return el } - getSurface () { - const surfaceManager = this.context.surfaceManager - const containerId = this.context.controller.props.containerId - return surfaceManager.getSurface(containerId) - } - isTrackChangesOn () { - const surface = this.getSurface() - if (!surface) return - return surface.props.trackChanges + // TODO -- ?? + return this.parent._owner.props.trackChanges } canAct () { const provider = this.context.trackChangesProvider - // console.log('can act?', provider.canAct()) return provider.canAct() } } diff --git a/app/components/SimpleEditor/elements/track_change/TrackChangeControlViewCommand.js b/app/components/SimpleEditor/elements/track_change/TrackChangeControlViewCommand.js index def9e7fd803b3dac99c6901ed37f52347251fdfa..829964d86210160e0f1ef70269cf2eeeef24d5d3 100644 --- a/app/components/SimpleEditor/elements/track_change/TrackChangeControlViewCommand.js +++ b/app/components/SimpleEditor/elements/track_change/TrackChangeControlViewCommand.js @@ -13,7 +13,7 @@ class TrackChangeControlViewCommand extends Command { // TODO -- review execute (params, context) { const surface = context.surfaceManager.getSurface('body') - surface.send('trackChangesViewUpdate') + surface.send('trackChangesViewToggle') surface.rerender() return true } diff --git a/app/components/SimpleEditor/elements/track_change/TrackChangeControlViewTool.js b/app/components/SimpleEditor/elements/track_change/TrackChangeControlViewTool.js index cfcaef4d7690abb186d49bacc20e674969923fd5..a9a01c29c00c7a116f96bf6c916696a871b2e444 100644 --- a/app/components/SimpleEditor/elements/track_change/TrackChangeControlViewTool.js +++ b/app/components/SimpleEditor/elements/track_change/TrackChangeControlViewTool.js @@ -1,6 +1,7 @@ import { Tool } from 'substance' class TrackChangeControlViewTool extends Tool { + renderButton ($$) { const el = super.renderButton($$) if (this.getViewMode()) el.addClass('track-changes-view-active') @@ -14,7 +15,8 @@ class TrackChangeControlViewTool extends Tool { } getViewMode () { - const editor = this.context.controller + const editor = this.context.editor + const { trackChangesView } = editor.state return trackChangesView } diff --git a/app/components/SimpleEditor/elements/track_change/TrackChangePackage.js b/app/components/SimpleEditor/elements/track_change/TrackChangePackage.js index ae4eef1a9325b8de74dfaf7420f8639c5d11a34d..40b630f0288a4a71f9acd2aa61989e82273df718 100644 --- a/app/components/SimpleEditor/elements/track_change/TrackChangePackage.js +++ b/app/components/SimpleEditor/elements/track_change/TrackChangePackage.js @@ -9,19 +9,20 @@ import TrackChangeControlViewCommand from './TrackChangeControlViewCommand' export default { name: 'track-change', - configure: function (config) { + configure: function (config, { toolGroup }) { config.addNode(TrackChange) - + config.addToolGroup('track-change-enable') + config.addToolGroup('track-change-toggle-view') config.addComponent(TrackChange.type, TrackChangeComponent) config.addConverter('html', TrackChangeHTMLConverter) // TODO -- both tools should go to the same target config.addTool('track-change-enable', TrackChangeControlTool, { - target: 'track-change-enable' + toolGroup: 'track-change-enable' }) config.addTool('track-change-toggle-view', TrackChangeControlViewTool, { - target: 'track-change-toggle-view' + toolGroup: 'track-change-toggle-view' }) config.addCommand('track-change-enable', TrackChangeControlCommand) diff --git a/app/components/SimpleEditor/elements/track_change/TrackChangesProvider.js b/app/components/SimpleEditor/elements/track_change/TrackChangesProvider.js index 92cc99bbff4ae25aec92a1c2a917f52a5a2a8bb4..35298b373f626d0e19a9643ae5497a94a76605b8 100644 --- a/app/components/SimpleEditor/elements/track_change/TrackChangesProvider.js +++ b/app/components/SimpleEditor/elements/track_change/TrackChangesProvider.js @@ -16,22 +16,14 @@ import { } from 'lodash' import { - createAnnotation, - deleteCharacter as deleteChar, - deleteNode, - deleteSelection as deleteSel, - expandAnnotation, - truncateAnnotation, + annotationHelpers, TOCProvider } from 'substance' class TrackChangesProvider extends TOCProvider { constructor (document, config) { super(document, config) - config.documentSession.on('didUpdate', this.handleUndoRedo, this) - - // HACK -- use TOCProvider's event to capture new / deleted changes - this.on('toc:updated', this.reComputeEntries, this) + // config.documentSession.on('didUpdate', this.handleUndoRedo, this) // handle button actions const editor = this.config.controller @@ -237,14 +229,16 @@ class TrackChangesProvider extends TOCProvider { point = annotation[direction.cursorTo + 'Offset'] moveOnly = true } else if (isOnLeftEdge && key === 'DELETE') { - point = annotation.endOffset + point = annotation.end.offset moveOnly = true } else if (isOnRightEdge && key === 'BACKSPACE') { - point = annotation.startOffset + point = annotation.start.offset moveOnly = true } - if (moveOnly) return this.moveCursorTo(point) + if (moveOnly) { + return this.moveCursorTo(point) + } if (isFromSameUser) { return this.expandAnnotationToDirection(annotation, direction) @@ -264,8 +258,9 @@ class TrackChangesProvider extends TOCProvider { if (notOnTrack) return this.markSelectionAsDeleted(options) const shortenBy = this.deleteAllOwnAdditions(selection) - const startOffset = selection.startOffset - const endOffset = selection.endOffset - shortenBy + const startOffset = selection.start.offset + const endOffset = selection.end.offset - shortenBy + this.updateSelection(selection, startOffset, endOffset) // console.log('collapsed', selection.isCollapsed()) @@ -449,20 +444,21 @@ class TrackChangesProvider extends TOCProvider { const transformation = (tx, args) => { const newNode = { selection: selection, - node: { - status: status, - type: 'track-change', - user: { - id: user.id, - roles: user.roles, - username: user.username - } + status: status, + type: 'track-change', + path: selection.path, + start: selection.start, + end: selection.end, + user: { + id: user.id, + roles: user.roles, + username: user.username } } - createAnnotation(tx, newNode) + tx.create(newNode) } - surface.transaction(transformation, info) + surface.editorSession.transaction(transformation, info) } deleteCharacter (direction) { @@ -471,22 +467,27 @@ class TrackChangesProvider extends TOCProvider { const transformation = (tx, args) => { args.direction = direction - return deleteChar(tx, args) + return tx.deleteCharacter(direction) } - surface.transaction(transformation, info) + surface.editorSession.transaction(transformation, info) } deleteSelection (selection) { const surface = this.getSurface() const info = { action: 'delete' } - const transformation = (tx, args) => { - args.selection = selection - return deleteSel(tx, args) + tx.setSelection({ + type: 'property', + path: selection.path, + surfaceId: 'body', + startOffset: selection.start.offset, + endOffset: selection.end.offset + }) + return tx.deleteSelection() } - surface.transaction(transformation, info) + surface.editorSession.transaction(transformation, info) } expandTrackAnnotation (selection, annotation) { @@ -497,20 +498,20 @@ class TrackChangesProvider extends TOCProvider { args.selection = selection args.anno = annotation - expandAnnotation(tx, args) + annotationHelpers.expandAnnotation(tx, args.anno, args.selection) } - surface.transaction(transformation, info) + surface.editorSession.transaction(transformation, info) } insertText (event) { if (!event) return const surface = this.getSurface() - surface.transaction(function (tx, args) { + surface.editorSession.transaction(function (tx, args) { if (surface.domSelection) surface.domSelection.clear() args.text = event.data || ' ' // if no data, it's a space key - return surface.insertText(tx, args) + return tx.insertText(args.text) }, { action: 'type' }) } @@ -518,24 +519,22 @@ class TrackChangesProvider extends TOCProvider { const surface = this.getSurface() const transformation = (tx, args) => { - args.nodeId = annotation.id - deleteNode(tx, args) + tx.delete(annotation.id) } - surface.transaction(transformation) + surface.editorSession.transaction(transformation) } truncateTrackAnnotation (selection, annotation) { const surface = this.getSurface() const info = this.getInfo() + const doc = this.getDocument() const transformation = (tx, args) => { - args.anno = annotation - args.selection = selection - truncateAnnotation(tx, args) + annotationHelpers.truncateAnnotation(doc, annotation, selection) } - surface.transaction(transformation, info) + surface.editorSession.transaction(transformation, info) } /* @@ -555,7 +554,6 @@ class TrackChangesProvider extends TOCProvider { (action === 'accept' && status === 'delete') || (action === 'reject' && status === 'add') ) { - console.log('should delete') this.deleteSelection(selection) } @@ -586,7 +584,7 @@ class TrackChangesProvider extends TOCProvider { const changesArray = map(changes, annotation => { const blockId = annotation.path[0] const blockPosition = container.getPosition(blockId) - const nodePosition = annotation.startOffset + const nodePosition = annotation.start.offset return { id: annotation.id, @@ -622,7 +620,8 @@ class TrackChangesProvider extends TOCProvider { controller.scrollTo(annotation.id) const selection = annotation.getSelection() - surface.setSelection(selection) + + surface.editorSession.setSelection(selection) this.moveCursorTo('start') } @@ -653,15 +652,16 @@ class TrackChangesProvider extends TOCProvider { } // getExistingAnnotation () { - // const documentSession = this.getDocumentSession() + // const documentSession = this.getEditorSession() // const selectionState = documentSession.getSelectionState() // const annotations = selectionState.getAnnotationsForType('track-change') // return annotations[0] // } getAllExistingTrackAnnotations () { - const documentSession = this.getDocumentSession() - const selectionState = documentSession.getSelectionState() + const editorSession = this.getEditorSession() + // const selectionState = documentSession.getSelectionState() + const selectionState = editorSession.getSelectionState() const annotations = selectionState.getAnnotationsForType('track-change') return annotations } @@ -715,12 +715,12 @@ class TrackChangesProvider extends TOCProvider { isOnLeftEdge (annotation) { const selection = this.getSelection() - return (selection.startOffset === annotation.startOffset) + return (selection.start.offset === annotation.start.offset) } isOnRightEdge (annotation) { const selection = this.getSelection() - return (selection.endOffset === annotation.endOffset) + return (selection.end.offset === annotation.end.offset) } /** @@ -752,18 +752,17 @@ class TrackChangesProvider extends TOCProvider { const deletedOp = deleted[keys(deleted)[0]] if (!deletedOp.type === 'track-change') return - const documentSession = this.getDocumentSession() + const documentSession = this.getEditorSession() documentSession.undo() } handleRedo () { - const documentSession = this.getDocumentSession() + const documentSession = this.getEditorSession() const undoneChanges = documentSession.undoneChanges const lastChange = last(undoneChanges) const op = last(lastChange.ops) const isTrack = op.path[0].split('-').slice(0, -1).join('-') === 'track-change' - console.log(isTrack) } /* @@ -806,22 +805,25 @@ class TrackChangesProvider extends TOCProvider { // TODO -- use substance's selection.collapse(direction) if (point === 'start') { - selection.endOffset = selection.startOffset + selection.end.offset = selection.start.offset } else if (point === 'end') { - selection.startOffset = selection.endOffset + selection.start.offset = selection.end.offset } else { - selection.startOffset = point - selection.endOffset = point + selection.start.offset = point + selection.end.offset = point } - surface.setSelection(selection) + surface.editorSession.setSelection(selection) } setSelectionPlusOne (direction) { const selection = this.getSelection() + const surface = this.getSurface() + + if (direction === 'left') selection.start.offset -= 1 + if (direction === 'right') selection.end.offset += 1 - if (direction === 'left') selection.startOffset -= 1 - if (direction === 'right') selection.endOffset += 1 + surface.editorSession.setSelection(selection) return selection } @@ -829,10 +831,10 @@ class TrackChangesProvider extends TOCProvider { updateSelection (selection, startOffset, endOffset) { const surface = this.getSurface() - selection.startOffset = startOffset - selection.endOffset = endOffset + selection.start.offset = startOffset + selection.end.offset = endOffset - surface.setSelection(selection) + surface.editorSession.setSelection(selection) return selection } @@ -850,8 +852,8 @@ class TrackChangesProvider extends TOCProvider { return this.config.user.id } - getDocumentSession () { - return this.config.documentSession + getEditorSession () { + return this.config.editorSession } getMode () { @@ -861,7 +863,7 @@ class TrackChangesProvider extends TOCProvider { getSelection () { const surface = this.getSurface() - return surface.getSelection() + return surface.domSelection.getSelection() } getSurface () { diff --git a/app/components/SimpleEditor/panes/Comments/CommentBox.js b/app/components/SimpleEditor/panes/Comments/CommentBox.js index e9afc3082292d2f02dc321a3700308026595fae2..453c569bb1b8bc61d384ae118e51dcb2a9d0794e 100644 --- a/app/components/SimpleEditor/panes/Comments/CommentBox.js +++ b/app/components/SimpleEditor/panes/Comments/CommentBox.js @@ -8,15 +8,14 @@ import { import CommentList from './CommentList' class CommentBox extends Component { - constructor (props) { - super(props) + constructor (props, args) { + super(props, args) this.inputHeight = 0 } // TODO -- fix class names render ($$) { - const active = this.props.active - const entry = this.props.entry + const {active, entry} = this.props let comments = this.props.comments.data @@ -86,9 +85,9 @@ class CommentBox extends Component { let replyBtn = this.refs.commentReplyButton if (value.trim().length > 0) { - replyBtn.removeAttr('disabled') + replyBtn.el.removeAttr('disabled') } else { - replyBtn.attr('disabled', 'true') + replyBtn.el.attr('disabled', 'true') } var parent = this.props.parent diff --git a/app/components/SimpleEditor/panes/Comments/CommentBoxList.js b/app/components/SimpleEditor/panes/Comments/CommentBoxList.js index 525d8e260eaba6291680bdf637a8dd5e58d83057..164b313c03462649bb2eca7f70a0c2d62d6e7af8 100644 --- a/app/components/SimpleEditor/panes/Comments/CommentBoxList.js +++ b/app/components/SimpleEditor/panes/Comments/CommentBoxList.js @@ -6,14 +6,15 @@ import { Component } from 'substance' import CommentBox from './CommentBox' class CommentBoxList extends Component { - constructor (props) { - super(props) + constructor (props, args) { + super(props, args) this.tops = [] } didMount () { const provider = this.getProvider() provider.on('comments:updated', this.onCommentsUpdated, this) + this.context.editorSession.onUpdate('document', this.getCommentEntries, this) this.setTops() } @@ -22,22 +23,27 @@ class CommentBoxList extends Component { this.setTops() } + getCommentEntries (change) { + const provider = this.getProvider() + provider.handleDocumentChange(change) + this.rerender() + } + render ($$) { const self = this - const provider = self.getProvider() const entries = provider.getEntries() const activeEntry = provider.activeEntry + const { comments, user } = self.props const listItems = entries.map(function (entry, i) { const active = (entry.id === activeEntry) - return $$(CommentBox, { active: active, - comments: self.props.comments[entry.id] || { data: [] }, + comments: comments[entry.id] || { data: [] }, entry: entry, parent: self, - user: self.props.user + user: user }) }) @@ -65,7 +71,8 @@ class CommentBoxList extends Component { setTops () { const provider = this.getProvider() - const entries = provider.getEntries() + const entries = provider.computeEntries() + const activeEntry = provider.activeEntry this.calculateTops(entries, activeEntry) @@ -88,6 +95,7 @@ class CommentBoxList extends Component { // get position of annotation in editor const annotationEl = document.querySelector('span[data-id="' + entry.id + '"]') + if (annotationEl) annotationTop = parseInt(annotationEl.offsetTop) // get height of this comment box diff --git a/app/components/SimpleEditor/panes/Comments/CommentsProvider.js b/app/components/SimpleEditor/panes/Comments/CommentsProvider.js index e643b9dac49640f11348c48a2dcdd0d8b03c9227..ebf24032e3324101ee5804348175b694368e3cec 100644 --- a/app/components/SimpleEditor/panes/Comments/CommentsProvider.js +++ b/app/components/SimpleEditor/panes/Comments/CommentsProvider.js @@ -1,20 +1,22 @@ import _ from 'lodash' -import { createAnnotation, deleteNode, TOCProvider as TocProvider } from 'substance' +import { TOCProvider as TocProvider } from 'substance' class CommentsProvider extends TocProvider { // TODO -- works if I rename to doc constructor (document, props) { super(document, props) this.activeEntry = null + const editorSession = props.editorSession - const documentSession = props.documentSession - documentSession.on('didUpdate', this.updateActiveEntry, this) - - const doc = documentSession.getDocument() - doc.on('document:changed', this.update, this) + editorSession.onUpdate('', this.updateActiveEntry, this) + editorSession.onRender('document', this.update, this) const editor = props.controller editor.on('ui:updated', this.onUiUpdate, this) + + // TODO is this needed anymore? + // const doc = editorSession.getDocument() + // doc.on('document:changed', this.update, this) } // @@ -33,7 +35,7 @@ class CommentsProvider extends TocProvider { // TODO -- this will probably not be needed if we stop moving the line height for track changes // offset by the time it takes the animation to change the line height onUiUpdate () { - setTimeout(() => { this.update() }, 300) + setTimeout(() => { this.update() }, 100) } reComputeEntries () { @@ -101,26 +103,29 @@ class CommentsProvider extends TocProvider { resolveComment (id) { const self = this - const ds = this.config.documentSession + const ds = this.config.editorSession const doc = ds.getDocument() const commentNode = doc.get(id) const path = commentNode.path - const startOffset = commentNode.startOffset - const endOffset = commentNode.endOffset + const startOffset = commentNode.start.offset + const endOffset = commentNode.end.offset const sel = ds.createSelection(path, startOffset, endOffset) const resolvedNodeData = { selection: sel, - node: { type: 'resolved-comment' } + type: 'resolved-comment', + path: sel.path, + start: sel.start, + end: sel.end } // create resolved comment annotation on the same selection the // comment was on and remove existing comment ds.transaction(function (tx, args) { - const annotation = createAnnotation(doc, resolvedNodeData) - const resolvedCommentId = annotation.node.id + const annotation = tx.create(resolvedNodeData) + const resolvedCommentId = annotation.id self.markCommentAsResolved(id, resolvedCommentId) }) @@ -164,9 +169,11 @@ class CommentsProvider extends TocProvider { deleteCommentNode (id) { const surface = this.getSurface() - surface.transaction(function (tx, args) { - deleteNode(tx, { nodeId: id }) + surface.editorSession.transaction(function (tx, args) { + tx.delete(id) + return args }) + surface.rerender() } getActiveComment () { @@ -206,7 +213,7 @@ class CommentsProvider extends TocProvider { // once you have all comments that do not contain each other wholly // just choose the one on the left - return _.minBy(annos, 'startOffset') + return _.minBy(annos, 'start.offset') } return comments[0] @@ -214,19 +221,17 @@ class CommentsProvider extends TocProvider { // returns an array of all comments in selection getSelectionComments () { - const selectionState = this.config.documentSession.getSelectionState() + const selectionState = this.config.editorSession.getSelectionState() return selectionState.getAnnotationsForType('comment') } getComments () { const doc = this.getDocument() const nodes = doc.getNodes() - // get all notes from the document const comments = _.pickBy(nodes, function (value, key) { return value.type === 'comment' }) - return comments } @@ -246,8 +251,8 @@ class CommentsProvider extends TocProvider { return doc.get(id) } - getDocumentSession () { - return this.config.documentSession + getEditorSession () { + return this.config.editorSession } // returns the combined borders of all comment annotations in selection @@ -255,11 +260,11 @@ class CommentsProvider extends TocProvider { const comments = this.getSelectionComments() if (comments.length === 0) return - const minComment = _.minBy(comments, 'startOffset') - const maxComment = _.maxBy(comments, 'endOffset') + const minComment = _.minBy(comments, 'start.offset') + const maxComment = _.maxBy(comments, 'end.offset') - const min = minComment.startOffset - const max = maxComment.endOffset + const min = minComment.start.offset + const max = maxComment.end.offset return { start: min, @@ -268,8 +273,8 @@ class CommentsProvider extends TocProvider { } getSelection () { - const documentSession = this.getDocumentSession() - return documentSession.getSelection() + const editorSession = this.getEditorSession() + return editorSession.getSelection() } getSurface () { @@ -284,8 +289,8 @@ class CommentsProvider extends TocProvider { const selection = this.getSelection() if ( - selection.startOffset < commentBorders.start || - selection.endOffset > commentBorders.end + selection.start.offset < commentBorders.start || + selection.end.offset > commentBorders.end ) return true return false @@ -311,41 +316,41 @@ class CommentsProvider extends TocProvider { node.active = bool } - // TODO -- do I need to override this? - handleDocumentChange (change) { - var doc = this.getDocument() - var needsUpdate = false - var tocTypes = this.constructor.tocTypes - - for (var i = 0; i < change.ops.length; i++) { - var op = change.ops[i] - var nodeType - if (op.isCreate() || op.isDelete()) { - var nodeData = op.getValue() - nodeType = nodeData.type - if (_.includes(tocTypes, nodeType)) { - needsUpdate = true - break - } - } else { - var id = op.path[0] - var node = doc.get(id) - if (node && _.includes(tocTypes, node.type)) { - needsUpdate = true - break - } - } - } - - if (needsUpdate) { - // need a timeout here, to make sure that the updated doc has rendered - // the annotations, so that the comment box list can be aligned with them - const self = this - setTimeout(function () { - self.reComputeEntries() - }) - } - } + // // TODO -- do I need to override this? + // handleDocumentChange (change) { + // var doc = this.getDocument() + // var needsUpdate = false + // var tocTypes = this.constructor.tocTypes + // + // for (var i = 0; i < change.ops.length; i++) { + // var op = change.ops[i] + // var nodeType + // if (op.isCreate() || op.isDelete()) { + // var nodeData = op.getValue() + // nodeType = nodeData.type + // if (_.includes(tocTypes, nodeType)) { + // needsUpdate = true + // break + // } + // } else { + // var id = op.path[0] + // var node = doc.get(id) + // if (node && _.includes(tocTypes, node.type)) { + // needsUpdate = true + // break + // } + // } + // } + // + // if (needsUpdate) { + // // need a timeout here, to make sure that the updated doc has rendered + // // the annotations, so that the comment box list can be aligned with them + // const self = this + // setTimeout(function () { + // self.reComputeEntries() + // }) + // } + // } /* When a comment gets resolved the comment on the fragment gets a resolved property. @@ -379,7 +384,7 @@ class CommentsProvider extends TocProvider { comments = _.map(comments, function (comment) { const blockId = comment.path[0] const blockPosition = container.getPosition(blockId) - const nodePosition = comment.startOffset + const nodePosition = comment.start.offset return { id: comment.id, diff --git a/app/components/SimpleEditor/panes/Comments/commentsPane.scss b/app/components/SimpleEditor/panes/Comments/commentsPane.scss index 4deca414b095dda9b99972f91f7c8008499b421e..eac574cbce8ede79618e662f0f44916bd449c08b 100644 --- a/app/components/SimpleEditor/panes/Comments/commentsPane.scss +++ b/app/components/SimpleEditor/panes/Comments/commentsPane.scss @@ -7,8 +7,7 @@ $green: #228b46; $teal: #3e644b; $white: #fff; -.sc-comment-pane-list { - list-style-type: none; +.sc-prose-editor .se-content .sc-comment-pane-list { margin-top: 0; padding-left: 1%; width: 37.5%; @@ -20,6 +19,7 @@ $white: #fff; border-radius: 3px; cursor: pointer; height: auto; + list-style-type: none; max-width: 3660%; min-height: 20px; min-width: 3660%; diff --git a/app/components/SimpleEditor/panes/Notes/Notes.js b/app/components/SimpleEditor/panes/Notes/Notes.js index 398674c4eb5ebdfc31755fa2f145e0253aac366f..e153b8d8e86b32f4094b317b1971b02027a28cd7 100644 --- a/app/components/SimpleEditor/panes/Notes/Notes.js +++ b/app/components/SimpleEditor/panes/Notes/Notes.js @@ -3,8 +3,7 @@ import { Component } from 'substance' class Notes extends Component { // use toc:updated to avoid rewriting TOCProvider's this.handleDocumentChange didMount () { - const provider = this.getProvider() - provider.on('toc:updated', this.onNotesUpdated, this) + this.context.editorSession.onUpdate('document', this.onNotesUpdated, this) } render ($$) { @@ -29,7 +28,9 @@ class Notes extends Component { return this.context.notesProvider } - onNotesUpdated () { + onNotesUpdated (change) { + const notesProvider = this.getProvider() + notesProvider.handleDocumentChange(change) this.rerender() } diff --git a/app/components/SimpleEditor/panes/Notes/NotesProvider.js b/app/components/SimpleEditor/panes/Notes/NotesProvider.js index 31ecabd126d4a2077097f12409bb38a3ad62e34b..ba3fe34e032a426b4b1eaad66c4f7e03862dfb30 100644 --- a/app/components/SimpleEditor/panes/Notes/NotesProvider.js +++ b/app/components/SimpleEditor/panes/Notes/NotesProvider.js @@ -28,7 +28,7 @@ class NotesProvider extends TOCProvider { notes = _.map(notes, function (note) { const blockId = note.path[0] const blockPosition = container.getPosition(blockId) - const nodePosition = note.startOffset + const nodePosition = note.start.offset return { id: note.id, diff --git a/app/components/SimpleEditor/panes/Notes/notes.scss b/app/components/SimpleEditor/panes/Notes/notes.scss index 8e1b85e98f122c864acb38b4777e186ab51d89db..fa7849cac47f8ffad1b23d3bc415a4724849a8ed 100644 --- a/app/components/SimpleEditor/panes/Notes/notes.scss +++ b/app/components/SimpleEditor/panes/Notes/notes.scss @@ -4,6 +4,7 @@ $gray: #ddd; border-top: 1px solid $gray; counter-reset: note-footer; font-size: 14px; + list-style-type: none; padding-top: 25px; } diff --git a/app/components/SimpleEditor/panes/TableOfContents/TableOfContents.js b/app/components/SimpleEditor/panes/TableOfContents/TableOfContents.js index 5ab4a84c34cec40a4bbd95efeb4d883e41666978..fd109a1ff7f2bf7dcd9719a20149a9647a264c48 100644 --- a/app/components/SimpleEditor/panes/TableOfContents/TableOfContents.js +++ b/app/components/SimpleEditor/panes/TableOfContents/TableOfContents.js @@ -1,9 +1,8 @@ import { FontAwesomeIcon as Icon, TOC as Toc } from 'substance' class TableOfContents extends Toc { - constructor (props) { - super(props) - + constructor (props, args) { + super(props, args) this.handleAction('tocEntrySelected', (nodeId) => { const editor = this.getEditor() editor.scrollTo(nodeId) @@ -15,9 +14,21 @@ class TableOfContents extends Toc { }) } + // TODO better way? after editor's initial render ,every change in the document is not + // updated. Editor never emits the event "document:updated". Workourand for now On didMound execute + // handleDocumentChange to decide if there is a change to be reflected in the TOC + didMount () { + this.context.editorSession.onUpdate('document', this.getTocEntries, this) + } + + getTocEntries (change) { + const tocProvider = this.getProvider() + tocProvider.handleDocumentChange(change) + this.rerender() + } + render ($$) { - const book = this.props.book - const fragment = this.props.fragment + const { book, fragment } = this.props const tocProvider = this.getProvider() const activeEntry = tocProvider.activeEntry @@ -100,7 +111,7 @@ class TableOfContents extends Toc { } getEditor () { - return this.context.controller + return this.context.editor } getProvider () { diff --git a/app/components/utils/last.js b/app/components/utils/last.js new file mode 100644 index 0000000000000000000000000000000000000000..1b2515246cceed693e9debfb96fb39359c280d0d --- /dev/null +++ b/app/components/utils/last.js @@ -0,0 +1,3 @@ +export default function last (arr) { + return arr[arr.length - 1] +}