From 0b102bc381c602725b56f8dca5b96ee51d7d7770 Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Mon, 3 Aug 2015 00:14:03 +0200 Subject: [PATCH] WIP: integrating writer. --- server.js | 5 +- ui/component.js | 28 ++- ui/dropdown_component.js | 64 ------- ui/font_awesome_icon.js | 24 ++- ui/heading_component.js | 21 --- ui/modal_panel.js | 44 ----- ui/{ => nodes}/annotation_component.js | 42 +++-- ui/nodes/figure_component.js | 58 +++++++ ui/nodes/heading_component.js | 29 ++++ ui/nodes/image_component.js | 49 ++++++ ui/nodes/include_component.js | 33 ++++ ui/nodes/node_component.js | 22 +++ ui/nodes/paragraph_component.js | 26 +++ ui/nodes/table_component.js | 230 +++++++++++++++++++++++++ ui/nodes/unsupported_node.js | 33 ++++ ui/paragraph_component.js | 19 -- ui/text_property_component.js | 87 +++++----- ui/{ => tools}/table_tool_component.js | 0 ui/{ => tools}/text_tool_component.js | 0 ui/{ => tools}/tool_component.js | 0 ui/writer/content_panel.js | 55 +++--- ui/writer/content_toolbar.js | 27 +++ ui/writer/context_toggles.js | 35 ++-- ui/writer/extension_manager.js | 5 +- ui/writer/modal_panel.js | 47 +++++ ui/writer/panel.js | 48 +++--- ui/{ => writer}/scrollbar.js | 78 ++++----- ui/writer/status_bar.js | 44 +++-- ui/writer/toc_panel.js | 124 +++++++++++++ ui/writer/writer.js | 171 ++++++++++-------- 30 files changed, 1044 insertions(+), 404 deletions(-) delete mode 100644 ui/dropdown_component.js delete mode 100644 ui/heading_component.js delete mode 100644 ui/modal_panel.js rename ui/{ => nodes}/annotation_component.js (61%) create mode 100644 ui/nodes/figure_component.js create mode 100644 ui/nodes/heading_component.js create mode 100644 ui/nodes/image_component.js create mode 100644 ui/nodes/include_component.js create mode 100644 ui/nodes/node_component.js create mode 100644 ui/nodes/paragraph_component.js create mode 100644 ui/nodes/table_component.js create mode 100644 ui/nodes/unsupported_node.js delete mode 100644 ui/paragraph_component.js rename ui/{ => tools}/table_tool_component.js (100%) rename ui/{ => tools}/text_tool_component.js (100%) rename ui/{ => tools}/tool_component.js (100%) create mode 100644 ui/writer/content_toolbar.js create mode 100644 ui/writer/modal_panel.js rename ui/{ => writer}/scrollbar.js (77%) create mode 100644 ui/writer/toc_panel.js diff --git a/server.js b/server.js index 287231ad..69da2cb5 100644 --- a/server.js +++ b/server.js @@ -16,9 +16,10 @@ app.get('/test/tmp/test.js', function (req, res, next) { return path.join(__dirname, file); })) .bundle() - .on('error', function(err, data){ + .on('error', function(err){ console.error(err.message); - res.send('console.log("'+err.message+'");'); + res.status(500).send('console.log("'+err.message+'");'); + next(); }) .pipe(res); } diff --git a/ui/component.js b/ui/component.js index e8631b38..ee3a3066 100644 --- a/ui/component.js +++ b/ui/component.js @@ -105,25 +105,37 @@ Component.Prototype = function ComponentPrototype() { return this.parent; }; + this.getClassNames = function() { + return this.classNames; + }; + + this.getAttributes = function() { + return this.attributes; + }; + this.createElement = function() { this.$el = $('<' + this.tagName + '>'); if (this.htmlProps.id) { this.$el.attr('id', this.htmlProps.id); } - var classNames = this.classNames; + var classNames = this.getClassNames(); if (classNames) { this.$el.addClass(classNames); } if (this.htmlProps.classNames) { this.$el.addClass(this.htmlProps.classNames); } - var attributes = this.attributes; + var attributes = this.getAttributes(); if (attributes) { this.$el.attr(attributes); } if (this.htmlProps.attributes) { this.$el.attr(this.htmlProps.attributes); } + var style = this.style; + if (style) { + this.$el.css(style); + } if (this.htmlProps.style) { this.$el.css(this.htmlProps.style); } @@ -240,7 +252,9 @@ Component.Prototype = function ComponentPrototype() { this.setState = function(newState) { var needRerender = this.shouldRerender(this.getProps(), newState); + this.willUpdateState(newState); this._setState(newState); + this.didUpdateState(); if (needRerender) { this.rerender(); } @@ -250,6 +264,12 @@ Component.Prototype = function ComponentPrototype() { return this.state; }; + this.willUpdateState = function(newState) { + /* jshint unused: false */ + }; + + this.didUpdateState = function() {}; + this.setProps = function(newProps) { var flags = this._setProps(newProps); if (flags.updateHtmlProps) { @@ -549,6 +569,10 @@ Component.Prototype = function ComponentPrototype() { break; case 'contentEditable': case 'href': + case 'src': + case 'spellCheck': + case 'rowspan': + case 'colspan': attributes[key] = val; html.attributes = attributes; result.html = html; diff --git a/ui/dropdown_component.js b/ui/dropdown_component.js deleted file mode 100644 index c8f97cc7..00000000 --- a/ui/dropdown_component.js +++ /dev/null @@ -1,64 +0,0 @@ -var _ = require("substance/helpers"); -var $$ = React.createElement; - - -var DropdownComponent = React.createClass({ - - // Prevent click behavior as we want to preserve the text selection in the doc - handleClick: function(e) { - e.preventDefault(); - }, - - handleDropdownToggle: function(e) { - e.preventDefault(); - - var open = this.state.open; - var self = this; - - if (open) return; - this.setState({open: !this.state.open}); - - setTimeout(function() { - $(window).one('mousedown', function(e) { - // e.preventDefault(); - // e.stopPropagation(); - self.close(); - }); - }, 0); - }, - - close: function() { - this.setState({ - open: false - }); - }, - - getInitialState: function() { - return { - open: false - }; - }, - - // Note: It's important that all children tools are rendered (even if not shown) - // because only that way we can keep the disabled states accurate - render: function() { - var classNames = ['dropdown']; - if (this.props.classNames) { - classNames = classNames.concat(this.props.classNames); - } - if (this.state.open) { - classNames.push('open'); - } - return $$('div', {className: classNames.join(' ')}, - $$('button', { - title: this.props.title, - className: 'toggle', - onMouseDown: this.handleDropdownToggle, - onClick: this.handleClick - }, this.props.label), - $$('div', {className: 'options shadow border fill-white'}, this.props.children) - ); - } -}); - -module.exports = DropdownComponent; \ No newline at end of file diff --git a/ui/font_awesome_icon.js b/ui/font_awesome_icon.js index cde294ca..9e3ad32f 100644 --- a/ui/font_awesome_icon.js +++ b/ui/font_awesome_icon.js @@ -1,16 +1,22 @@ 'use strict'; +var OO = require('../basics/oo'); var Component = require('./component'); -var $$ = Component.$$; -class FontAwesomeIcon extends Component.Container { - get tagName() { - return 'i'; - } - - get classNames() { - return 'fa ' + this.props.icon - } +function FontAwesomeIcon() { + Component.Container.apply(this, arguments); } +FontAwesomeIcon.Prototype = function() { + + this.tagName = 'i'; + + this.getClassNames = function() { + return 'fa ' + this.props.icon; + }; + +}; + +OO.inherit(FontAwesomeIcon, Component.Container); + module.exports = FontAwesomeIcon; diff --git a/ui/heading_component.js b/ui/heading_component.js deleted file mode 100644 index 3180bc7d..00000000 --- a/ui/heading_component.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -var TextProperty = require('substance-ui/text_property'); -var $$ = React.createElement; - -class HeadingComponent extends React.Component { - render() { - var level = this.props.node.level; - return $$("div", { className: "content-node heading level-"+level, "data-id": this.props.node.id }, - $$(TextProperty, { - ref: "textProp", - doc: this.props.doc, - path: [ this.props.node.id, "content"] - }) - ); - } -} - -HeadingComponent.displayName = "HeadingComponent"; - -module.exports = HeadingComponent; \ No newline at end of file diff --git a/ui/modal_panel.js b/ui/modal_panel.js deleted file mode 100644 index 12269b6b..00000000 --- a/ui/modal_panel.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -var Component = require('./component'); -var $$ = Component.$$; - -class ModalPanel extends Component { - - constructor(parent, props) { - super(parent, props); - - this.handleCloseModal = this.handleCloseModal.bind(this); - } - - get classNames() { - return 'modal '+this.props.panelElement.type.modalSize; - } - - didMount() { - this.$el.on('click', '.close-modal', this.handleCloseModal); - this.$el.on('click', '.modal-body', this.preventBubbling()); - } - - willUnmount() { - this.$el.off('click'); - } - - handleCloseModal(e) { - e.preventDefault(); - this.send('closeModal'); - } - - preventBubbling(e) { - e.stopPropagation(); - e.preventDefault(); - } - - render() { - return $$('div', { classNames: 'modal-body' }, - this.props.panelElement - ); - } -} - -module.exports = ModalPanel; diff --git a/ui/annotation_component.js b/ui/nodes/annotation_component.js similarity index 61% rename from ui/annotation_component.js rename to ui/nodes/annotation_component.js index e35742a3..489a9deb 100644 --- a/ui/annotation_component.js +++ b/ui/nodes/annotation_component.js @@ -1,57 +1,61 @@ "use strict"; -var Component = require('./component'); -var $$ = Component.$$; +var OO = require('../../basics/oo'); +var Component = require('../component'); -class AnnotationComponent extends Component { +function AnnotationComponent() { + Component.apply(this, arguments); +} + +AnnotationComponent.Prototype = function() { - get tagName: function() { - return 'span' - } + this.tagName = 'span'; - get classNames: function() { + this.getClassNames = function() { var typeNames = this.props.node.getTypeNames(); var classNames = typeNames.join(' '); if (this.props.classNames) { classNames += " " + this.props.classNames.join(' '); } return classNames.replace(/_/g, '-'); - } + }; - get attributes() { + this.getAttributes = function() { return { "data-id": this.props.node.id }; - } + }; - render() { + this.render = function() { if (this.props.node.active) { this.$el.addClass('active'); } else { this.$el.removeClass('active'); } return this.props.children; - } + }; - didMount() { + this.didMount = function() { var node = this.props.node; node.connect(this, { 'active': this.onActiveChanged }); - } + }; - willUnmount() { + this.willUnmount = function() { var node = this.props.node; node.disconnect(this); - } + }; - onActiveChanged() { + this.onActiveChanged = function() { if (this.props.node.active) { this.$el.addClass('active'); } else { this.$el.removeClass('active'); } - } -} + }; +}; + +OO.inherit(AnnotationComponent, Component); module.exports = AnnotationComponent; diff --git a/ui/nodes/figure_component.js b/ui/nodes/figure_component.js new file mode 100644 index 00000000..282f81d7 --- /dev/null +++ b/ui/nodes/figure_component.js @@ -0,0 +1,58 @@ +'use strict'; + +var OO = require('../../basics/oo'); +var Component = require('../component'); +var TextProperty = require('../text_property_component'); +var $$ = Component.$$; + +function FigureComponent() { + Component.apply(this, arguments); +} + +FigureComponent.Prototype = function() { + + this.getClassNames = function() { + var specificType = this.props.node.type; + return "content-node figure clearfix "+specificType; + }; + + this.getAttributes = function() { + return { + "data-id": this.props.node.id + }; + }; + + this.render = function() { + var componentRegistry = this.context.componentRegistry; + var contentNode = this.props.node.getContentNode(); + var ContentComponentClass = componentRegistry.get(contentNode.type); + + return [ + $$('div', { classNames: 'label', contentEditable: false }, this.props.node.label), + $$(TextProperty, { + tagName: 'div', + classNames: 'title', + doc: this.props.doc, + path: [this.props.node.id, "title"] + }), + $$('div', { classNames: 'figure-content' }, + $$(ContentComponentClass, { + doc: this.props.doc, + node: contentNode + }) + ), + $$('div', { classNames: 'description small' }, + $$(TextProperty, { + tagName: 'div', + classNames: 'caption', + doc: this.props.doc, + path: [this.props.node.id, "caption"] + }) + ) + ]; + }; +}; + +OO.inherit(FigureComponent, Component); + +module.exports = FigureComponent; diff --git a/ui/nodes/heading_component.js b/ui/nodes/heading_component.js new file mode 100644 index 00000000..43792ef9 --- /dev/null +++ b/ui/nodes/heading_component.js @@ -0,0 +1,29 @@ +'use strict'; + +var OO = require('../../basics/oo'); +var Component = require('../Component'); +var $$ = Component.$$; +var NodeComponent = require('./node_component'); +var TextProperty = require('./text_property_component'); + +function HeadingComponent() { + NodeComponent.apply(this, arguments); +} + +HeadingComponent.Prototype = function() { + + this.getClassNames = function() { + return "content-node heading level-"+this.props.node.level; + }; + + this.render = function() { + return $$(TextProperty, { + doc: this.props.doc, + path: [ this.props.node.id, "content"] + }); + }; +}; + +OO.inherit(HeadingComponent, NodeComponent); + +module.exports = HeadingComponent; diff --git a/ui/nodes/image_component.js b/ui/nodes/image_component.js new file mode 100644 index 00000000..6f0feb8e --- /dev/null +++ b/ui/nodes/image_component.js @@ -0,0 +1,49 @@ +'use strict'; + +var OO = require('../../basics/oo'); +var Component = require('../Component'); +var $$ = Component.$$; + +function ImageComponent() { + Component.apply(this, arguments); + + this.handleDocumentChange = this.handleDocumentChange.bind(this); +} + +ImageComponent.Prototype = function() { + + this.didMount = function() { + var doc = this.props.doc; + doc.connect(this, { 'document:changed': this.handleDocumentChange }); + }; + + this.willUnmount = function() { + var doc = this.props.doc; + doc.disconnect(this); + }; + + this.getClassNames = function() { + return 'image'; + }; + + this.getAttributes = function() { + return { + contentEditable: false, + "data-id": this.props.node.id + }; + }; + + this.render = function() { + return $$('img', {src: this.props.node.src}); + }; + + this.handleDocumentChange = function(change) { + if (change.isAffected([this.props.node.id, "src"])) { + this.rerender(); + } + }; +}; + +OO.inherit(ImageComponent, Component); + +module.exports = ImageComponent; diff --git a/ui/nodes/include_component.js b/ui/nodes/include_component.js new file mode 100644 index 00000000..6e73d2a1 --- /dev/null +++ b/ui/nodes/include_component.js @@ -0,0 +1,33 @@ +'use strict'; + +var OO = require('../../basics/oo'); +var Component = require('../Component'); +var UnsupportedNode = require('./unsupported_node'); +var $$ = Component.$$; + +function IncludeComponent() { + Component.apply(this, arguments); +} + +IncludeComponent.Prototype = function() { + + this.render = function() { + var doc = this.props.doc; + var node = doc.get(this.props.node.nodeId); + var componentRegistry = this.context.componentRegistry; + var ComponentClass = componentRegistry.get(node.type); + if (!ComponentClass) { + console.error('Could not resolve a component for type: ' + node.type); + ComponentClass = UnsupportedNode; + } + return $$(ComponentClass, { + key: node.id, + doc: doc, + node: node + }); + }; +}; + +OO.inherit(IncludeComponent, Component); + +module.exports = IncludeComponent; diff --git a/ui/nodes/node_component.js b/ui/nodes/node_component.js new file mode 100644 index 00000000..b8a2d893 --- /dev/null +++ b/ui/nodes/node_component.js @@ -0,0 +1,22 @@ +'use strict'; + +var OO = require('../../basics/oo'); +var Component = require('../component'); + +function NodeComponent() { + Component.apply(this, arguments); +} + +NodeComponent.Prototype = function() { + + this.getAttributes = function() { + return { + "data-id": this.props.node.id + }; + }; + +}; + +OO.inherit(NodeComponent, Component); + +module.exports = NodeComponent; diff --git a/ui/nodes/paragraph_component.js b/ui/nodes/paragraph_component.js new file mode 100644 index 00000000..cbd0a800 --- /dev/null +++ b/ui/nodes/paragraph_component.js @@ -0,0 +1,26 @@ +'use strict'; + +var OO = require('../../basics/oo'); +var Component = require('../Component'); +var $$ = Component.$$; +var NodeComponent = require('./node_component'); +var TextProperty = require('./text_property_component'); + +function Paragraph() { + NodeComponent.apply(this, arguments); +} + +Paragraph.Prototype = function() { + + this.getClassNames = function() { + return "content-node paragraph"; + }; + + this.render = function() { + return $$(TextProperty, { path: [ this.props.node.id, "content"] }); + }; +}; + +OO.inherit(Paragraph, NodeComponent); + +module.exports = Paragraph; diff --git a/ui/nodes/table_component.js b/ui/nodes/table_component.js new file mode 100644 index 00000000..fd12ae76 --- /dev/null +++ b/ui/nodes/table_component.js @@ -0,0 +1,230 @@ +'use strict'; + +var _ = require('../../basics/helpers'); +var OO = require('../../basics/oo'); +var Component = require('../component'); +var TextProperty = require('../text_property_component'); +var TableSelection = require('../../document/table_selection'); +var $$ = Component.$$; + +function TableComponent() { + Component.apply(this, arguments); + + this.onDoubleClick = this.onDoubleClick.bind(this); + this.onMouseDown = this.onMouseDown.bind(this); +} + +TableComponent.Prototype = function() { + + this.getInitialState = function() { + return { + mode: 'table' + }; + }; + + this.didMount = function() { + this.context.surface.connect(this, { + "selection:changed": this.onSelectionChange + }); + this.$el.on('mousedown', 'th,td', this.onMouseDown); + this.$el.on('doubleclick', 'th,td', this.onDoubleClick); + }; + + this.willUnmount = function() { + this.context.surface.disconnect(this); + }; + + this.tagName = "table"; + + this.getClassNames = function() { + return "content-node table"; + }; + + this.getAttributes = function() { + return { + "data-id": this.props.node.id, + contentEditable: false + }; + }; + + this.render = function() { + var tableNode = this.props.node; + // HACK: make sure row col indexes are up2date + tableNode.getMatrix(); + var secEls = []; + var secs = tableNode.getSections(); + _.each(secs, function(sec) { + var rowEls = []; + var rows = sec.getRows(); + _.each(rows, function(row) { + rowEls.push(this._renderRow(row)); + }, this); + secEls.push($$("t"+sec.sectionType, {key: sec.id }, rowEls)); + }, this); + + if (this.state.mode === "table") { + this.$el.removeClass('cell-editing-mode'); + this.$el.addClass('table-editing-mode'); + } else if (this.state.mode === "cell") { + this.$el.addClass('cell-editing-mode'); + this.$el.removeClass('table-editing-mode'); + } + + return secEls; + }; + + this._renderRow = function(row) { + var doc = this.props.doc; + var cellEls = []; + + var cells = row.getCells(); + _.each(cells, function(cell) { + var cellTag = cell.cellType === 'head' ? 'th' : 'td'; + var cellProps = { + "data-id": cell.id, + "data-row": cell.rowIdx, + "data-col": cell.colIdx, + }; + if (cell.colspan) { + cellProps.colSpan = cell.colspan; + } + if (cell.rowspan) { + cellProps.rowSpan = cell.rowspan; + } + if (this.state.mode === "cell") { + if (cell.rowIdx === this.state.row && cell.colIdx === this.state.col) { + cellProps.contentEditable = "true"; + } + } + cellEls.push($$(cellTag, cellProps, $$(TextProperty, { + doc: doc, + path: [ cell.id, "content"] + }))); + }, this); + + return $$("tr", {"data-id": row.id, contentEditable:false}, cellEls); + }; + + this.onDoubleClick = function(e) { + e.preventDefault(); + e.stopPropagation(); + var surface = this.context.surface; + var $cell = $(e.currentTarget); + var cellId = $cell.data('id'); + // switch the state + this.setState({ + mode: "cell", + cellId: cellId, + row: $cell.data('row'), + col: $cell.data('col') + }, function() { + var doc = surface.getDocument(); + var path = [cellId, 'content']; + var text = doc.get(path); + surface.setSelection({ + type: 'property', + path: path, + startOffset: text.length, + }); + }); + }; + + this.onMouseDown = function(e) { + var $el = this.$el; + var surface = this.context.surface; + var doc = surface.getDocument(); + var id = this.props.node.id; + var self = this; + // 1. get the anchor cell (for click or select) + var $cell = $(e.currentTarget); + + if (this.state.mode === "cell" && this.state.cellId === $cell.data('id')) { + // do not override selection behavior within a cell + return; + } + e.preventDefault(); + e.stopPropagation(); + + var $startCell = $cell; + var $endCell = $cell; + var rectangle = new TableSelection.Rectangle( + $startCell.data('row'), + $startCell.data('col'), + $endCell.data('row'), + $endCell.data('col') + ); + // 2. enable onMouseOver delegate which updates the displayed selection region + var onMouseOver = function(e) { + $endCell = $(e.currentTarget); + rectangle = new TableSelection.Rectangle($startCell.data('row'), $startCell.data('col'), + $endCell.data('row'), $endCell.data('col')); + self._updateSelection(rectangle); + }; + $el.on('mouseenter', 'th,td', onMouseOver); + // 3. enable onMouseUp hook: stop dragging the selection and + // finally set the Surface's selection + $(window).one('mouseup', function(e) { + e.stopPropagation(); + e.preventDefault(); + $el.off('mouseenter', 'th,td', onMouseOver); + var tableSelection = doc.createSelection({ + type: 'table', + tableId: id, + rectangle: rectangle + }); + surface.setSelection(tableSelection); + }); + }; + + this.onSelectionChange = function(sel) { + var node = this.props.node; + var id = node.id; + if (this.hasSelection) { + this._clearSelection(); + } + if (this.state.mode === "cell") { + if (!sel.isPropertySelection() || sel.start.path[0] !== this.state.cellId) { + var self = this; + this.setState({ + mode: "table" + }, function() { + if (sel.isTableSelection() && sel.getTableId() === id) { + self._updateSelection(sel.rectangle); + } + }); + } + } else { + if (sel.isTableSelection() && sel.getTableId() === id) { + this._updateSelection(sel.rectangle); + } + } + }; + + this._updateSelection = function(rectangle) { + var $el = this.$el; + var $cells = $el.find('th,td'); + $cells.each(function() { + var $cell = $(this); + var row = $cell.data('row'); + var col = $cell.data('col'); + if (row >= rectangle.start.row && row <= rectangle.end.row && + col >= rectangle.start.col && col <= rectangle.end.col) { + $cell.addClass("selected"); + } else { + $cell.removeClass("selected"); + } + }); + this.hasSelection = true; + }; + + this._clearSelection = function() { + var $el = this.$el; + var $cells = $el.find('th,td'); + $cells.removeClass('selected'); + this.hasSelection = false; + }; +}; + +OO.inherit(TableComponent, Component); + +module.exports = TableComponent; diff --git a/ui/nodes/unsupported_node.js b/ui/nodes/unsupported_node.js new file mode 100644 index 00000000..71870a20 --- /dev/null +++ b/ui/nodes/unsupported_node.js @@ -0,0 +1,33 @@ +'use strict'; + +var OO = require('../../basics/oo'); +var Component = require('../Component'); + +function UnsupportedNodeComponent() { + Component.apply(this, arguments); +} + +UnsupportedNodeComponent.Prototype = function() { + + this.tagName = 'pre'; + + this.classNames = "content-node unsupported"; + + this.getAttributes = function() { + return { + "data-id": this.props.node.id, + contentEditable: false + }; + }; + + this.render = function() { + var rawJson = JSON.stringify(this.props.node.properties, null, 2); + return rawJson; + }; +}; + +OO.inherit(UnsupportedNodeComponent, Component); + +UnsupportedNodeComponent.displayName = "UnsupportedNodeComponent"; + +module.exports = UnsupportedNodeComponent; diff --git a/ui/paragraph_component.js b/ui/paragraph_component.js deleted file mode 100644 index d391dba2..00000000 --- a/ui/paragraph_component.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -var TextProperty = require('substance-ui/text_property'); -var $$ = React.createElement; - -class Paragraph extends React.Component { - render() { - return $$("div", { className: "content-node paragraph", "data-id": this.props.node.id }, - $$(TextProperty, { - doc: this.props.doc, - path: [ this.props.node.id, "content"] - }) - ); - } -} - -Paragraph.displayName = "Paragraph"; - -module.exports = Paragraph; diff --git a/ui/text_property_component.js b/ui/text_property_component.js index 5f6a8879..0b85227a 100644 --- a/ui/text_property_component.js +++ b/ui/text_property_component.js @@ -1,36 +1,44 @@ "use strict"; -var Substance = require('substance'); -var _ = require('substance/helpers'); -var Component = require('./component'); +var Component = require('../component'); var $$ = Component.$$; -var Annotator = Substance.Document.Annotator; +var Annotator = require('../../document/annotator'); var AnnotationComponent = require('./annotation_component'); -var TextPropertyComponent extends Component { +class TextPropertyComponent extends Component { - didMount: function() { + get tagName() { + return this.props.tagName || 'span'; + } + + get classNames() { + return "text-property " + (this.props.classNames || ""); + } + + get attributes() { + return { + spellCheck: false, + "data-path": this.props.path.join('.') + }; + } + + get style() { + return { + whiteSpace: "pre-wrap" + }; + } + + didMount() { var doc = this.props.doc; doc.getEventProxy('path').add(this.props.path, this, this.textPropertyDidChange); - }, + } - willUnmount: function() { + willUnmount() { var doc = this.props.doc; doc.getEventProxy('path').remove(this.props.path, this); - }, - - render: function() { - return $$((this.props.tagName || 'span'), { - classNames: "text-property " + (this.props.className || ""), - spellCheck: false, - style: { - whiteSpace: "pre-wrap" - }, - "data-path": this.props.path.join('.') - }, this.renderChildren()); - }, + } - renderChildren: function() { + render() { var componentRegistry = this.context.componentRegistry; var doc = this.getDocument(); var path = this.getPath(); @@ -62,7 +70,6 @@ var TextPropertyComponent extends Component { fragmentCounters[id] = 0; } fragmentCounters[id] = fragmentCounters[id]+1; - var key = id + "_" + fragmentCounters[id]; // for debugging // console.log(_logPrefix+"<"+node.type+" key="+key+">"); @@ -111,45 +118,45 @@ var TextPropertyComponent extends Component { // TODO: sometimes we do not want to do this. Make it configurable. root.children.push($$('br')); return root.children; - }, + } - getAnnotations: function() { + getAnnotation() { return this.context.surface.getAnnotationsForProperty(this.props.path); - }, + } // Annotations that are active (not just visible) - getHighlights: function() { + getHighlights() { if (this.context.getHighlightedNodes) { return this.context.getHighlightedNodes(); } else { return []; } - }, + } - textPropertyDidChange: function() { + textPropertyDidChange() { this.rerender(); - }, + } - getContainer: function() { + getContainer() { return this.getSurface().getContainer(); - }, + } - getDocument: function() { + getDocument() { return this.props.doc; - }, + } - getPath: function() { + getPath() { return this.props.path; - }, + } - getElement: function() { + getElement() { return this.$el[0]; - }, + } - getSurface: function() { + getSurface() { return this.context.surface; - }, + } -})); +} module.exports = TextPropertyComponent; diff --git a/ui/table_tool_component.js b/ui/tools/table_tool_component.js similarity index 100% rename from ui/table_tool_component.js rename to ui/tools/table_tool_component.js diff --git a/ui/text_tool_component.js b/ui/tools/text_tool_component.js similarity index 100% rename from ui/text_tool_component.js rename to ui/tools/text_tool_component.js diff --git a/ui/tool_component.js b/ui/tools/tool_component.js similarity index 100% rename from ui/tool_component.js rename to ui/tools/tool_component.js diff --git a/ui/writer/content_panel.js b/ui/writer/content_panel.js index 3b8df4e2..d45ac016 100644 --- a/ui/writer/content_panel.js +++ b/ui/writer/content_panel.js @@ -1,39 +1,44 @@ 'use strict'; +var _ = require('../../basics/helpers'); +var OO = require('../../basics/oo'); var Component = require('../component'); var $$ = Component.$$; -var _ = require('../../basics/helpers'); var Panel = require("./panel"); var Scrollbar = require("./scrollbar"); -class ContentPanel extends Panel { +function ContentPanel() { + Panel.apply(this, arguments); +} + +ContentPanel.Prototype = function() { // Since component gets rendered multiple times we need to update // the scrollbar and reattach the scroll event - didMount() { + this.didMount = function() { this.updateScrollbar(); $(window).on('resize', this.updateScrollbar); this.props.doc.connect(this, { 'document:changed': this.onDocumentChange, 'toc:entry-selected': this.onTOCEntrySelected }, -1); - } + }; - willUnmount() { + this.willUnmount = function() { $(window).off('resize'); this.props.doc.disconnect(this); - } + }; - onDocumentChange() { + this.onDocumentChange = function() { this.updateScrollbar(); - } + }; - onTOCEntrySelected(nodeId) { + this.onTOCEntrySelected = function(nodeId) { this.scrollToNode(nodeId); - } + }; - updateScrollbar() { + this.updateScrollbar = function() { var scrollbar = this.refs.scrollbar; var $panelContent = this.refs.panelContent.$el; // We need to await next repaint, otherwise dimensions will be wrong @@ -43,15 +48,15 @@ class ContentPanel extends Panel { // (Re)-Bind scroll event on new panelContentEl $panelContent.off('scroll'); $panelContent.on('scroll', this._onScroll); - } + }; - onScroll() { + this.onScroll = function() { var $panelContent = this.refs.panelContent.$el; this.refs.scrollbar.update($panelContent[0], this); this.markActiveTOCEntry(); - } + }; - markActiveTOCEntry() { + this.markActiveTOCEntry = function() { var $panelContent = this.refs.panelContent.$el; var contentHeight = this.getContentHeight(); @@ -80,16 +85,16 @@ class ContentPanel extends Panel { var doc = this.getDocument(); doc.emit('toc:entry-focused', activeNode); - } + }; // Rendering // ----------------- - get classNames() { + this.getClassNames = function() { return "panel content-panel-component"; - } + }; - render() { + this.render = function() { return [ $$(Scrollbar, { id: "content-scrollbar", @@ -100,9 +105,9 @@ class ContentPanel extends Panel { this.getContentEditor() ) ]; - } + }; - renderContentEditor() { + this.renderContentEditor = function() { var componentRegistry = this.context.componentRegistry; var doc = this.props.doc; // FIXME: this is called getContentEditor() but requires 'content_container' @@ -115,7 +120,9 @@ class ContentPanel extends Panel { doc: doc, node: doc.get("content"), }); - } -} + }; +}; + +OO.inherit(ContentPanel, Panel); -module.exports = ContentPanel; \ No newline at end of file +module.exports = ContentPanel; diff --git a/ui/writer/content_toolbar.js b/ui/writer/content_toolbar.js new file mode 100644 index 00000000..53cb4440 --- /dev/null +++ b/ui/writer/content_toolbar.js @@ -0,0 +1,27 @@ +"use strict"; + +var OO = require('../../basics/oo'); +var Component = require('../component'); +var $$ = Component.$$; + +function ContentToolbar() { + Component.apply(this, arguments); +} + +ContentToolbar.Prototype = function() { + + this.getClassNames = function() { + return "content-tools-component"; + }; + + this.render = function() { + var tools = this.props.tools.map(function(tool) { + return $$(tool, { doc: this.props.doc }); + }); + return $$('div', {className: "tools"}, tools); + }; +}; + +OO.inherit(ContentToolbar, Component); + +module.exports = ContentToolbar; diff --git a/ui/writer/context_toggles.js b/ui/writer/context_toggles.js index b99cc829..8c437627 100644 --- a/ui/writer/context_toggles.js +++ b/ui/writer/context_toggles.js @@ -1,17 +1,21 @@ +'use strict'; + +var _ = require('../../basics/helpers'); +var OO = require('../../basics/oo'); var Component = require('../component'); var $$ = Component.$$; var Icon = require('../font_awesome_icon'); -class ContextToggles extends Component { +function ContextToggles() { + Component.apply(this, arguments); - constructor(parent, props) { - super(parent, props); + this.onContextToggleClick = this.onContextToggleClick.bind(this); +} - this.onContextToggleClick = this.onContextToggleClick.bind(this); - } +ContextToggles.Prototype = function() { - render() { + this.render = function() { var panelOrder = this.props.panelOrder; var contextId = this.props.contextId; @@ -30,27 +34,28 @@ class ContextToggles extends Component { }, $$(Icon, { icon: panelClass.icon }), $$('span', { classNames: 'label'}, panelClass.displayName) - ) + ); toggleComps.push(toggle); }, this); return $$('div', { classNames: "context-toggles" }, toggleComps); - } + }; - didMount() { + this.didMount = function() { this.$el.on('click', 'a.toggle-context', this.onContextToggleClick); - } + }; - willUnmount() { + this.willUnmount = function() { this.$el.off('click'); - } + }; - onContextToggleClick(e) { + this.onContextToggleClick = function(e) { e.preventDefault(); var newContext = $(e.currentTarget).attr("data-id"); this.send('switchContext', newContext); - } + }; +}; -} +OO.inherit(ContextToggles, Component); module.exports = ContextToggles; diff --git a/ui/writer/extension_manager.js b/ui/writer/extension_manager.js index 11422907..5af8d391 100644 --- a/ui/writer/extension_manager.js +++ b/ui/writer/extension_manager.js @@ -1,7 +1,6 @@ "use strict"; -var Substance = require('substance'); -var _ = require("substance/helpers"); +var OO = require("../../basics/oo"); var ExtensionManager = function(extensions, writer) { this.extensions = extensions; @@ -78,6 +77,6 @@ ExtensionManager.Prototype = function() { }; -Substance.initClass(ExtensionManager); +OO.initClass(ExtensionManager); module.exports = ExtensionManager; diff --git a/ui/writer/modal_panel.js b/ui/writer/modal_panel.js new file mode 100644 index 00000000..a5bb0b1d --- /dev/null +++ b/ui/writer/modal_panel.js @@ -0,0 +1,47 @@ +'use strict'; + +var OO = require('../../basics/oo'); +var Component = require('../component'); +var $$ = Component.$$; + +function ModalPanel() { + Component.apply(this,arguments); + + this.handleCloseModal = this.handleCloseModal.bind(this); +} + +ModalPanel.Prototype = function() { + + this.getClassNames = function() { + return 'modal '+this.props.panelElement.type.modalSize; + }; + + this.didMount = function() { + this.$el.on('click', '.close-modal', this.handleCloseModal); + this.$el.on('click', '.modal-body', this.preventBubbling()); + }; + + this.willUnmount = function() { + this.$el.off('click'); + }; + + this.handleCloseModal = function(e) { + e.preventDefault(); + this.send('closeModal'); + }; + + this.preventBubbling = function(e) { + e.stopPropagation(); + e.preventDefault(); + }; + + this.render = function() { + return $$('div', { classNames: 'modal-body' }, + this.props.panelElement + ); + }; +}; + +OO.inherit(ModalPanel, Component); + +module.exports = ModalPanel; diff --git a/ui/writer/panel.js b/ui/writer/panel.js index 42ac97e6..fd4af4fa 100644 --- a/ui/writer/panel.js +++ b/ui/writer/panel.js @@ -1,26 +1,30 @@ 'use strict'; +var OO = require('../../basics/oo'); var Component = require('../component'); var $$ = Component.$$; -// This is an abstract class -class Panel extends Component { +function Panel() { + Component.apply(this, arguments); +} + +Panel.Prototype = function() { - getDocument() { + this.getDocument = function() { var app = this.context.app; return app.doc; - } + }; - getPanelContentElement() { + this.getPanelContentElement = function() { return this.refs.panelContent.$el[0]; - } + }; - getScrollableContainer() { + this.getScrollableContainer = function() { return this.refs.panelContent.$el[0]; - } + }; // Returns the cumulated height of a panel's content - getContentHeight() { + this.getContentHeight = function() { // initialized lazily as this element is not accessible earlier (e.g. during construction) // get the new dimensions // TODO: better use outerheight for contentheight determination? @@ -31,36 +35,36 @@ class Panel extends Component { contentHeight += $(this).outerHeight(); }); return contentHeight; - } + }; // Returns the height of panel (inner content overflows) - getPanelHeight() { + this.getPanelHeight = function() { var panelContentEl = this.getPanelContentElement(); return $(panelContentEl).height(); - } + }; - getScrollPosition() { + this.getScrollPosition = function() { var panelContentEl = this.getPanelContentElement(); return $(panelContentEl).scrollTop(); - } + }; // This method must be overriden with your panel implementation - render() { + this.render = function() { return $$("div", {classNames: "panel"}, $$('div', {classNames: 'panel-content'}, 'YOUR_PANEL_CONTENT') ); - } + }; // Get the current coordinates of the first element in the // set of matched elements, relative to the offset parent // Please be aware that it looks up until it finds a parent that has // position: relative|absolute set. So for now never set relative somewhere in your panel - getPanelOffsetForElement(el) { + this.getPanelOffsetForElement = function(el) { var offsetTop = $(el).position().top; return offsetTop; - } + }; - scrollToNode(nodeId) { + this.scrollToNode = function(nodeId) { // var n = this.findNodeView(nodeId); // TODO make this generic var panelContentEl = this.getScrollableContainer(); @@ -73,7 +77,9 @@ class Panel extends Component { } else { console.warn(nodeId, 'not found in scrollable container'); } - } -} + }; +}; + +OO.inherit(Panel, Component); module.exports = Panel; diff --git a/ui/scrollbar.js b/ui/writer/scrollbar.js similarity index 77% rename from ui/scrollbar.js rename to ui/writer/scrollbar.js index 73684cd3..2392c356 100644 --- a/ui/scrollbar.js +++ b/ui/writer/scrollbar.js @@ -1,50 +1,50 @@ "use strict"; -var _ = require('../basics/helpers'); -var Component = require('./component'); +var OO = require('../../basics/oo'); +var Component = require('../component'); var $$ = Component.$$; +var THUMB_MIN_HEIGHT = 7; + // A rich scrollbar implementation that supports highlights // ---------------- -var THUMB_MIN_HEIGHT = 7; - -class Scrollbar extends Component { +function Scrollbar() { + Component.apply(this, arguments); - constructor(parent, props) { - super(parent, props); + this.onMouseDown = this.onMouseDown.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + this.onMouseMove = this.onMouseMove.bind(this); +} - this.onMouseDown = this.onMouseDown.bind(this); - this.onMouseUp = this.onMouseUp.bind(this); - this.onMouseMove = this.onMouseMove.bind(this); - } +Scrollbar.Prototype = function() { - getInitialState() { + this.getInitialState = function() { return { thumb: {top: 0, height: 20}, // just render at the top highlights: [] // no highlights until state derived }; - } + }; - didMount() { + this.didMount = function() { // HACK global window object! // TODO: why is this done? $(window).on('mousemove', this.mouseMove); $(window).on('mouseup', this.mouseUp); this.$el.on('mousedown', this.onMouseDown); - } + }; - willUnmount() { + this.willUnmount = function() { $(window).off('mousemove', this.mouseMove); $(window).off('mouseup', this.mouseUp); this.$el.off('mousedown', this.onMouseDown); - } + }; - get classNames() { - return 'scrollbar-component'; - } + this.getClassNames = function() { + return 'scrollbar-component'+this.props.contextId; + }; - render() { + this.render = function() { var highlightEls = this.state.highlights.map(function(h) { return $$('div', { classNames: 'highlight', @@ -63,15 +63,13 @@ class Scrollbar extends Component { height: Math.max(this.state.thumb.height, THUMB_MIN_HEIGHT) } }); - return $$("div", {classNames: " "+this.props.contextId, onMouseDown: }, + return [ thumbEl, - $$('div', {classNames: 'highlights'}, - highlightEls - ) - ); - } + $$('div', {classNames: 'highlights'}, highlightEls) + ]; + }; - update(panelContentEl, panel) { + this.update = function(panelContentEl, panel) { var self = this; this.panelContentEl = panelContentEl; var contentHeight = panel.getContentHeight(); @@ -95,7 +93,7 @@ class Scrollbar extends Component { id: nodeId, top: top, height: height - } + }; highlights.push(data); }); @@ -108,13 +106,13 @@ class Scrollbar extends Component { thumb: thumbProps, highlights: highlights }); - } + }; - onMouseDown(e) { + this.onMouseDown = function(e) { e.stopPropagation(); e.preventDefault(); this._mouseDown = true; - var scrollBarOffset = $(React.findDOMNode(this)).offset().top; + var scrollBarOffset = this.$el.offset().top; var y = e.pageY - scrollBarOffset; var thumbEl = this.refs.thumb.getDOMNode(); if (e.target !== thumbEl) { @@ -124,16 +122,16 @@ class Scrollbar extends Component { } else { this.offset = y - $(thumbEl).position().top; } - } + }; // Handle Mouse Up // ----------------- // // Mouse lifted, nothis.panelContentEl scroll anymore - onMouseUp() { + this.onMouseUp = function() { this._mouseDown = false; - } + }; // Handle Scroll // ----------------- @@ -141,7 +139,7 @@ class Scrollbar extends Component { // Handle scroll event // .visible-area handle - onMouseMove(e) { + this.onMouseMove = function(e) { if (this._mouseDown) { var scrollBarOffset = this.$el.offset().top; var y = e.pageY - scrollBarOffset; @@ -149,9 +147,11 @@ class Scrollbar extends Component { var scroll = (y-this.offset)*this.factor; this.scrollTop = $(this.panelContentEl).scrollTop(scroll); } - } -} + }; +}; + +OO.inherit(Scrollbar, Component); -Scrollbar.overlayMinHeight = 5 +Scrollbar.overlayMinHeight = 5; module.exports = Scrollbar; diff --git a/ui/writer/status_bar.js b/ui/writer/status_bar.js index bd318ab1..a03a72e8 100644 --- a/ui/writer/status_bar.js +++ b/ui/writer/status_bar.js @@ -1,5 +1,6 @@ "use strict"; +var OO = require('../../basics/oo'); var Component = require('../component'); var $$ = Component.$$; @@ -13,43 +14,60 @@ var ICONS_FOR_TYPE = { // The Status Bar // ---------------- -class StatusBar extends Component { +function StatusBar() { + Component.apply(this, arguments); - didMount: function() { + this.handleNotificationUpdate = this.handleNotificationUpdate.bind(this); +} + +StatusBar.Prototype = function() { + + this.didMount = function() { var notifications = this.context.notifications; notifications.connect(this, { 'messages:updated': this.handleNotificationUpdate }); - } + }; - render: function() { + this.getClassNames = function() { + return "status-bar-component fill-light"; + }; + + this.willUpdateState = function() { + if (this.state.message) { + this.$el.removeClass(this.state.message.type); + } + }; + + this.render = function() { var message = this.state.message; var notifications; - var classNames = ["status-bar-component fill-light"]; if (message) { - classNames.push(message.type); + this.$el.addClass(message.type); notifications = $$('div', {className: 'notifications'}, $$("div", { classNames: "icon"}, $$('i', { classNames: 'fa '+ICONS_FOR_TYPE[message.type]}) ), - $$('div', {classNames: 'message'}, message.message) + $$('div', { classNames: 'message' }, message.message) ); } else { notifications = $$('div'); } - return $$("div", { classNames: classNames.join(" ") }, + return [ $$("div", { classNames: "document-status" }, this.props.doc.getDocumentMeta().title), notifications - ); - } + ]; + }; - handleNotificationUpdate: function(messages) { + this.handleNotificationUpdate = function(messages) { var currentMessage = messages.pop(); this.setState({ message: currentMessage }); - } + }; -} +}; + +OO.inherit(StatusBar, Component); module.exports = StatusBar; diff --git a/ui/writer/toc_panel.js b/ui/writer/toc_panel.js new file mode 100644 index 00000000..bfe0521a --- /dev/null +++ b/ui/writer/toc_panel.js @@ -0,0 +1,124 @@ +"use strict"; + +var _ = require('../../basics/helpers'); +var OO = require('../../basics/oo'); +var Component = require('../component'); +var $$ = Component.$$; +var Panel = require('./panel'); + +function TocPanel() { + Panel.apply(this, arguments); + + this.handleClick = this.handleClick.bind(this); +} + +TocPanel.Prototype = function() { + + this.didMount = function() { + var doc = this.getDocument(); + doc.connect(this, { + 'app:toc-entry:changed': this.setActiveTOCEntry, + 'document:changed': this.handleDocumentChange + }); + }; + + this.willUnmount = function() { + var doc = this.getDocument(); + doc.disconnect(this); + }; + + this.getInitialState = function() { + var doc = this.props.doc; + var tocNodes = doc.getTOCNodes(); + return { + tocNodes: tocNodes, + activeNode: tocNodes.length > 0 ? tocNodes[0].id : null + }; + }; + + this.handleDocumentChange = function(change) { + var doc = this.getDocument(); + var needsUpdate = false; + var tocTypes = doc.getSchema().getTocTypes(); + // HACK: this is not totally correct but works. + // Actually, the TOC should be updated if tocType nodes + // get inserted or removed from the container, plus any property changes + // This implementation just checks for changes of the node type + // not the container, but as we usually create and show in + // a single transaction this works. + 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) { + return this.setState({ + tocNodes: doc.getTOCNodes() + }); + } + }; + + this.setActiveTocEntry = function(nodeId) { + this.setState({ + activeNode: nodeId + }); + }; + + this.handleClick = function(e) { + var nodeId = e.currentTarget.dataset.id; + console.log('clicked', nodeId); + e.preventDefault(); + var doc = this.getDocument(); + doc.emit("toc:entry-selected", nodeId); + }; + + this.getClassNames = function() { + return "panel toc-panel-component"; + }; + + this.render = function() { + var state = this.state; + var tocEntries = _.map(state.tocNodes, function(node) { + var level = node.level; + var classNames = ["toc-entry", "level-"+level]; + + if (state.activeNode === node.id) { + classNames.push("active"); + } + + return $$('a', { + className: classNames.join(" "), + href: "#", + key: node.id, + "data-id": node.id, + onClick: this.handleClick + }, node.content); + }, this); + + return $$("div", {classNames: "toc-entries"}, tocEntries); + }; +}; + +OO.inherit(TocPanel, Panel); + +// Panel Configuration +// ----------------- + +TocPanel.contextId = "toc"; +TocPanel.icon = "fa-align-left"; + +module.exports = TocPanel; diff --git a/ui/writer/writer.js b/ui/writer/writer.js index 3e3a7a25..726faab7 100644 --- a/ui/writer/writer.js +++ b/ui/writer/writer.js @@ -1,39 +1,44 @@ "use strict"; +var OO = require('../../basics/oo'); var Component = require('../component'); var $$ = Component.$$; var _ = require("../../basics/helpers"); var EventEmitter = require('../../basics/event_emitter'); +var Registry = require('../../basics/registry'); var SurfaceManager = require('../../surface/surface_manager'); var Clipboard = require('../../surface/clipboard'); -var Registry = require('../../basics/registry'); var ToolRegistry = require('../../surface/tool_registry'); var ExtensionManager = require('./extension_manager'); -var ContentToolbar = require("./content_tools"); +var ContentToolbar = require("./content_toolbar"); var ContextToggles = require('./context_toggles'); var ContentPanel = require("./content_panel"); var StatusBar = require("./status_bar"); -var ModalPanel = require('../modal_panel'); +var ModalPanel = require('./modal_panel'); // TODO: re-establish a means to set which tools are enabled for which surface -class Writer extends Component { +function Writer() { + Component.apply(this, arguments); + + // Mixin + EventEmitter.call(this); - constructor(parent, props) { - super(parent, props); - // Mixin - EventEmitter.call(this); + this.handleApplicationKeyCombos = this.handleApplicationKeyCombos.bind(this); + this.onSelectionChangedDebounced = _.debounce(this.onSelectionChanged, 50); - this.handleApplicationKeyCombos = this.handleApplicationKeyCombos.bind(this); - this.onSelectionChangedDebounced = _.debounce(this.onSelectionChanged, 50); + this._registerExtensions(); + this._initializeComponentRegistry(); +} + +Writer.Prototype = function() { - this._registerExtensions(); - this._initializeComponentRegistry(); - } + // mix-in + _.extend(this, EventEmitter.prototype); - getChildContext() { + this.getChildContext = function() { return { getHighlightedNodes: this.getHighlightedNodes, getHighlightsForTextProperty: this.getHighlightsForTextProperty, @@ -41,22 +46,22 @@ class Writer extends Component { toolRegistry: this.toolRegistry, surfaceManager: this.surfaceManager }; - } + }; - get classNames() { + this.getClassNames = function() { return 'writer-component'; - } + }; - getDocument() { + this.getDocument = function() { return this.props.doc; - } + }; - getInitialState() { + this.getInitialState = function() { var defaultContextId = this.props.contextId; return {"contextId": defaultContextId || "toc"}; - } + }; - render() { + this.render = function() { if (this.props.doc) { return $$('div', {}, 'Loading'); } else { @@ -74,15 +79,15 @@ class Writer extends Component { $$('div', { key: 'clipboard', classNames: "clipboard" }) ]; } - } + }; - willReceiveProps(newProps) { + this.willReceiveProps = function(newProps) { if (this.props.doc && newProps.doc !== this.props.doc) { this._disposeDoc(); } - } + }; - didReceiveProps() { + this.didReceiveProps = function() { if (this.props.doc) { this.surfaceManager = new SurfaceManager(this.props.doc); this.clipboard = new Clipboard(this.surfaceManager, this.doc.getClipboardImporter(), this.doc.getClipboardExporter()); @@ -91,25 +96,25 @@ class Writer extends Component { 'document:changed': this.onDocumentChanged }); } - } + }; - willUpdateState(newState) { + this.willUpdateState = function(newState) { this.extensionManager.handleStateChange(newState, this.state); - } + }; - didMount() { + this.didMount = function() { this.$el.on('keydown', this.handleApplicationKeyCombos); - } + }; - willUnmount() { + this.willUnmount = function() { this.$el.off('keydown'); if (this.props.doc) { this._disposeDoc(); } - } + }; // return true when you handled a key combo - handleApplicationKeyCombos(e) { + this.handleApplicationKeyCombos = function(e) { // console.log('####', e.keyCode, e.metaKey, e.ctrlKey, e.shiftKey); var handled = false; // TODO: we could make this configurable via extensions @@ -136,18 +141,18 @@ class Writer extends Component { e.preventDefault(); e.stopPropagation(); } - } + }; // Event handlers // -------------- - transactionStarted(tx) { + this.transactionStarted = function(tx) { // store the state so that it can be recovered when undo/redo tx.before.state = this.state; tx.before.selection = this.getSelection(); - } + }; - onDocumentChanged(change, info) { + this.onDocumentChanged = function(change, info) { var doc = this.getDocument(); doc.__dirty = true; var notifications = this.context.notifications; @@ -159,34 +164,34 @@ class Writer extends Component { if (info.replay && change.after.state) { this.setState(change.after.state); } - } + }; - onSelectionChanged(sel, surface) { + this.onSelectionChanged = function(sel, surface) { this.extensionManager.handleSelectionChange(sel); this.toolRegistry.each(function(tool) { tool.update(surface, sel); }, this); this.emit('selection:changed', sel); - } + }; // Action handlers // --------------- - switchContext(contextId) { + this.switchContext = function(contextId) { this.setState({ contextId: contextId }); - } + }; - executeAction(actionName) { + this.executeAction = function(actionName) { return this.extensionManager.handleAction(actionName); - } + }; - closeModal() { + this.closeModal = function() { var newState = _.cloneDeep(this.state); delete newState.modal; this.setState(newState); - } + }; - saveDocument() { + this.saveDocument = function() { var doc = this.props.doc; var backend = this.context.backend; var notifications = this.context.notifications; @@ -214,23 +219,24 @@ class Writer extends Component { } }); } - } + }; - handleAction(actionName) { + // TODO: this is a duplicate of this.executeAction()... which one? + this.handleAction = function(actionName) { this.extensionManager.handleAction(actionName); - } + }; - handleCloseDialog(e) { + this.handleCloseDialog = function(e) { e.preventDefault(); console.log('handling close'); this.setState(this.getInitialState()); - } + }; // Internal Methods // ---------------------- - _registerExtensions() { + this._registerExtensions = function() { // Note: we are treating basics as extension internally var config = this.config; var basics = { @@ -244,9 +250,9 @@ class Writer extends Component { extensions = extensions.concat(config.extensions); } this.extensionManager = new ExtensionManager(extensions, this); - } + }; - _initializeComponentRegistry() { + this._initializeComponentRegistry = function() { var componentRegistry = new Registry(); _.each(this.extensionManager.extensions, function(extension) { _.each(extension.components, function(ComponentClass, name) { @@ -254,9 +260,9 @@ class Writer extends Component { }); }); this.componentRegistry = componentRegistry; - } + }; - _initializeToolRegistry() { + this._initializeToolRegistry = function() { var toolRegistry = new ToolRegistry(); _.each(this.extensionManager.extensions, function(extension) { _.each(extension.tools, function(ToolClass, name) { @@ -267,19 +273,46 @@ class Writer extends Component { }, this); }, this); this.toolRegistry = toolRegistry; - } + }; - _disposeDoc() { + this._disposeDoc = function() { this.props.doc.disconnect(this); this.surfaceManager.dispose(); this.clipboard.detach(this.$el[0]); this.surfaceManager.dispose(); this.surfaceManager = null; this.clipboard = null; - } + }; + + this._panelPropsFromState = function (state) { + var props = _.omit(state, 'contextId'); + props.doc = this.doc; + return props; + }; + + this._getActivePanelElement = function() { + var panelComponent = this.componentRegistry.get(this.state.contextId); + if (panelComponent) { + return $$(panelComponent, this._panelPropsFromState(this.state)); + } else { + console.warn("Could not find component for contextId:", this.state.contextId); + } + }; + + this._getActiveModalPanelElement = function() { + var state = this.state; + if (state.modal) { + var modalPanelComponent = this.componentRegistry.get(state.modal.contextId); + if (modalPanelComponent) { + return $$(modalPanelComponent, this._panelPropsFromState(state.modal)); + } else { + console.warn("Could not find component for contextId:", state.modal.contextId); + } + } + }; - _renderModalPanel() { - var modalPanelElement = this.getActivePanelElement(); + this._renderModalPanel = function() { + var modalPanelElement = this._getActiveModalPanelElement(); if (!modalPanelElement) { // Just render an empty div if no modal active available return $$('div'); @@ -287,17 +320,17 @@ class Writer extends Component { return $$(ModalPanel, { panelElement: modalPanelElement }); - } + }; - _renderContextPanel() { - var panelElement = this.getActivePanelElement(); + this._renderContextPanel = function() { + var panelElement = this._getActivePanelElement(); if (!panelElement) { return $$('div', null, "No panels are registered"); } return panelElement; - } -} + }; +}; -_.extend(Writer.prototype, EventEmitter.prototype); +OO.inherit(Writer, Component); module.exports = Writer;