From 46bd7972775ea29c5862b08ebe40c986c46fdb7c Mon Sep 17 00:00:00 2001 From: Michael Aufreiter Date: Thu, 25 Feb 2016 20:30:43 +0100 Subject: [PATCH 1/2] Disable Raven for debuggability during development --- web/sheet/sheet.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/web/sheet/sheet.js b/web/sheet/sheet.js index c09f293757..6ec615b7ac 100644 --- a/web/sheet/sheet.js +++ b/web/sheet/sheet.js @@ -59,17 +59,17 @@ window.Stencila = {}; window.isEditable = true; function launch() { - Raven - .config( - 'https://6329017160394100b21be92165555d72@app.getsentry.com/37250',{ - ignoreUrls: [ - // Ignore errors generated during development or on local sessions - /localhost/, - /127\.0\.0\.1/, - ] - }) - .install(); - try { + // Raven + // .config( + // 'https://6329017160394100b21be92165555d72@app.getsentry.com/37250',{ + // ignoreUrls: [ + // // Ignore errors generated during development or on local sessions + // /localhost/, + // /127\.0\.0\.1/, + // ] + // }) + // .install(); + // try { var doc = loadDocument(); if (window.isEditable) { renderStaticReadonlyVersion(doc); @@ -80,9 +80,9 @@ function launch() { } else { renderInteractiveVersion(doc, 'read'); } - } catch(e) { - Raven.captureException(e) - } + // } catch(e) { + // Raven.captureException(e) + // } } window.activate = function() { From d90d38f3dcb2367c5c53f1593dea0c31069728ab Mon Sep 17 00:00:00 2001 From: Michael Aufreiter Date: Thu, 25 Feb 2016 20:32:19 +0100 Subject: [PATCH 2/2] Start integrate functions with backend engine. --- web/sheet/engine/SheetRemoteEngine.js | 54 ++++++---- web/sheet/sheet.scss | 1 + web/sheet/styles/_select-function.scss | 10 ++ web/sheet/testFunction.js | 12 +-- web/sheet/ui/CellEditor.js | 71 ++++++++++--- web/sheet/ui/FunctionComponent.js | 132 +++++++++++++++++++------ web/sheet/ui/SelectFunction.js | 32 ++++++ web/sheet/ui/SheetController.js | 3 +- 8 files changed, 242 insertions(+), 73 deletions(-) create mode 100644 web/sheet/styles/_select-function.scss create mode 100644 web/sheet/ui/SelectFunction.js diff --git a/web/sheet/engine/SheetRemoteEngine.js b/web/sheet/engine/SheetRemoteEngine.js index 998ba8e500..b3eb1ec25d 100644 --- a/web/sheet/engine/SheetRemoteEngine.js +++ b/web/sheet/engine/SheetRemoteEngine.js @@ -1,32 +1,43 @@ 'use strict'; -var oo = require('substance/util/oo'); var RemoteEngine = require('../../RemoteEngine'); function SheetRemoteEngine() { SheetRemoteEngine.super.apply(this, arguments); - window._engine = this; } SheetRemoteEngine.Prototype = function() { /** - * A list of function names currently available in the - * the sheet's context - */ - this._functionList = null; + Gets the list of available functions. + */ + this.getFunctionList = function() { + return this._functionList; + }; + + /** + A list of function names currently available in the + the sheet's context + + TODO: remove hard-coded entries once updateFunction list + strategy is in place. + */ + this._functionList = ['sum', 'mean']; /** - * A dictionary of functions definitions used as - * a cache - */ - this._functionSpecs = {} + A dictionary of functions definitions used as + a cache + */ + this._functionSpecs = {}; /** - * Get a list of function names - */ - this.functions = function(cb) { + Updates the cache of available functions + + TODO: this should be run on app start and when new packages are + imported that expose more functions. + */ + this.updateFunctionList = function(cb) { if(this._functionList) { cb(this._functionList); } else { @@ -38,19 +49,24 @@ SheetRemoteEngine.Prototype = function() { }; /** - * Get a function definition - */ + Get a function definition + */ this.function = function(name, cb) { - if(this._functionSpecs[name]){ - return this._functionSpecs[name]; + var cachedFunction = this._functionSpecs[name]; + if (cachedFunction) { + cb(null, cachedFunction); } else { this._request('PUT', 'function', {name:name}, function(err, result) { + if (err) return cb(err); this._functionSpecs[name] = result; - cb(result); + cb(null, result); }.bind(this)); } }; - + + /* + Updates given cells + */ this.update = function(cells, cb) { this._request('PUT', 'update', cells, function(err, result) { if (err) return cb(err); diff --git a/web/sheet/sheet.scss b/web/sheet/sheet.scss index 425d4bda5d..b590a748f1 100644 --- a/web/sheet/sheet.scss +++ b/web/sheet/sheet.scss @@ -33,6 +33,7 @@ $default-cell-padding: 1px 3px; @import './styles/_primitive-expression'; @import './styles/_image-expression'; @import './styles/_function'; +@import './styles/_select-function'; @import './styles/_cell-teaser'; @import './styles/_save-tool'; @import './styles/_sheet-editor'; diff --git a/web/sheet/styles/_select-function.scss b/web/sheet/styles/_select-function.scss new file mode 100644 index 0000000000..654bc1ed55 --- /dev/null +++ b/web/sheet/styles/_select-function.scss @@ -0,0 +1,10 @@ +.sc-select-function { + position: absolute; + min-width: 300px; + z-index: 2; + background: #FFFFFF; + top: 30px; + font-size: 12px; + box-shadow: 0px 2px 4px 0px rgba(0,0,0,0.50); + text-align: left; +} \ No newline at end of file diff --git a/web/sheet/testFunction.js b/web/sheet/testFunction.js index 80090dbb61..2d8c0d4396 100644 --- a/web/sheet/testFunction.js +++ b/web/sheet/testFunction.js @@ -1,18 +1,18 @@ -var testSnippet = { +var testFunction = { "name": "sum", - "summary": "Returns the sum of values.", + "title": "Returns the sum of values.", "notes": [ "If only a single number for `values` is supplied, `sum` returns `values`" ], "parameters": [ { "name": "value1", - "descr": "The first number or range to sum up." + "description": "The first number or range to sum up." }, { "shape": ["one", "block"], "name": "value2", - "descr": "Additional numbers or ranges to add to value1", + "description": "Additional numbers or ranges to add to value1", "variadic": true, "optional": true } @@ -31,7 +31,7 @@ var testSnippet = { "r", "py" ] -} +}; -module.exports = testSnippet; \ No newline at end of file +module.exports = testFunction; \ No newline at end of file diff --git a/web/sheet/ui/CellEditor.js b/web/sheet/ui/CellEditor.js index 0917099bb9..0d2d07cd5d 100644 --- a/web/sheet/ui/CellEditor.js +++ b/web/sheet/ui/CellEditor.js @@ -3,6 +3,7 @@ var Component = require('substance/ui/Component'); var $$ = Component.$$; var FunctionComponent = require('./FunctionComponent'); +var SelectFunction = require('./SelectFunction'); /* The CellEditor is different to a regular TextPropertyEditor @@ -26,13 +27,16 @@ CellEditor.Prototype = function() { .on('input', this.onChange) .ref('editor') ); - if (this.state.func) { - window._engine.function(this.state.func, function(func){ - el.append($$(FunctionComponent, { - func: func, - paramIndex: this.state.paramIndex - })); - }.bind(this)); + if (this.state.funcName) { + el.append($$(FunctionComponent, { + funcName: this.state.funcName, + paramIndex: this.state.paramIndex + }).ref('function')); // ref is needed so the component is not wiped on each keystroke + } else if (this.state.suggestedFunctions) { + // Render function name suggestor + el.append($$(SelectFunction, { + entries: this.state.suggestedFunctions + }).ref('selectFunction')); } return el; }; @@ -94,11 +98,36 @@ CellEditor.Prototype = function() { this._detectFunction(); }; + /* + Iterates over available function names and matches the current input string - var _FUNCTIONS = ['sum', 'mean']; - var _FUNCTION_RE_STR = '\\b(' + _FUNCTIONS.join('|') + ')[(]'; + TODO: @nokome: the _matcher needs to be improved! Also you may want to + limit the number of suggested function names + */ + this._matchFunctionNames = function(str) { + if (!str) return []; // don't match anything for an empty string + var _matcher = new RegExp('\^'+str, 'gi'); + + var matches = []; + var funcs = this._getAvailableFunctions(); + + funcs.forEach(function(funcName) { + if (_matcher.exec('='+funcName)) { + matches.push(funcName); + } + }); + return matches; + }; + + this._getAvailableFunctions = function() { + var engine = this.context.engine; + return engine.getFunctionList(); + }; this._detectFunction = function() { + var _availableFuncs = this._getAvailableFunctions(); + var _function_re_str = '\\b(' + _availableFuncs.join('|') + ')[(]'; + setTimeout(function() { var el = this._getTextArea(); var source = el.value; @@ -106,27 +135,37 @@ CellEditor.Prototype = function() { // only if collapsed if (pos === el.selectionEnd) { source = source.slice(0, pos); - var re = new RegExp(_FUNCTION_RE_STR, 'gi'); + var re = new RegExp(_function_re_str, 'gi'); var lastMatch, match; while ( (match = re.exec(source)) ) { lastMatch = match; } + if (lastMatch) { // console.log('DETECTED FUNCTION', lastMatch[1], lastMatch); - var func = lastMatch[1]; + var funcName = lastMatch[1]; var startPos = lastMatch.index+1; var argsPos = startPos + lastMatch[0].length; var currentArg = this._detectCurrentArg(source.slice(argsPos)); var newState = { - func: func, + funcName: funcName, paramIndex: currentArg.argIdx }; // console.log('DETECTED FUNCTION', newState); this.setState(newState); - } else if (this.state.func) { - this.extendState({ - func: false - }); + } else { + // Check if any available function name matches partly so we can suggest it + var suggestedFunctions = this._matchFunctionNames(source); + + if (suggestedFunctions.length > 0) { + this.setState({ + suggestedFunctions: suggestedFunctions + }); + } else { + this.setState({ + funcName: false + }); + } } } }.bind(this)); diff --git a/web/sheet/ui/FunctionComponent.js b/web/sheet/ui/FunctionComponent.js index a449963942..920dbb5e2d 100644 --- a/web/sheet/ui/FunctionComponent.js +++ b/web/sheet/ui/FunctionComponent.js @@ -8,46 +8,117 @@ function FunctionComponent() { } FunctionComponent.Prototype = function() { - this.render = function() { - var func = this.props.func; - var el = $$('div').addClass('sc-function'); - // Parameter description - var paramsEl = $$('table').addClass('se-parameters'); + // Lifecycle + // ------------------------------------ - func.parameters.forEach(function(param, i) { + /* + Initial state signature + */ + this.getInitialState = function() { + return { + func: null // the function specification: needs to be loaded from server + }; + }; + + /* + Triggers loading of remote data needed for display + */ + this._init = function() { + this._loadFunctionSpec(); + }; - var paramEl = $$('tr').addClass('se-param').append( - $$('td').addClass('se-param-name').append(param.name), - $$('td').addClass('se-param-descr').append(param.descr) + /* + On initial mount + */ + this.didMount = function() { + console.log('FunctionComponent.didmount'); + this._init(); + }; + + /* + When new props funcName, paramIndex are set + */ + this.willReceiveProps = function(newProps) { + if (this.props.funcName !== newProps.funcName) { + this.dispose(); + this._init(); + } + }; + + // Render + // ------------------------------------ + + this.render = function() { + var el = $$('div').addClass('sc-function'); + + var func = this.state.func; + var funcName = this.props.funcName; + + if (func) { + // Parameter description + var paramsEl = $$('table').addClass('se-parameters'); + func.parameters.forEach(function(param, i) { + var paramEl = $$('tr').addClass('se-param').append( + $$('td').addClass('se-param-name').append(param.name), + $$('td').addClass('se-param-descr').append(param.description) + ); + if (i === this.props.paramIndex) { + paramEl.addClass('sm-active'); + } + paramsEl.append(paramEl); + }.bind(this)); + + // Documentation + var docEl = $$('div').addClass('se-documentation'); + docEl.append( + $$(FunctionComponent.Signature, {func: func, paramIndex: this.props.paramIndex}), + paramsEl, + $$('div').addClass('se-summary').append(func.title) ); - if (i === this.props.paramIndex) { - paramEl.addClass('sm-active'); - } - paramsEl.append(paramEl); - }.bind(this)); + el.append(docEl); - // Documentation - var docEl = $$('div').addClass('se-documentation'); - docEl.append( - $$(FunctionComponent.Signature, {func: func, paramIndex: this.props.paramIndex}), - paramsEl, - $$('div').addClass('se-summary').append(func.summary) - ); + // Example + var example; + if (func.examples) { + example = func.examples[0]; + } - el.append(docEl); + if (example) { + el.append( + $$('div').addClass('se-example').append( + $$('div').addClass('se-label').append('Example'), + // Display first example + $$('div').addClass('se-example-code').append(example) + ) + ); + } + } else { + el.append('Loading function specification for '+ funcName); + } - // Example - el.append( - $$('div').addClass('se-example').append( - $$('div').addClass('se-label').append('Example'), - // Display first example - $$('div').addClass('se-example-code').append(func.examples[0]) - ) - ); return el; }; + + // Utils + // ------------------------------------ + + this._loadFunctionSpec = function() { + var engine = this.context.engine; + var funcName = this.props.funcName; + console.log('loading funcName', funcName); + engine.function(funcName, function(err, func) { + if (err) { + console.error(funcName, 'could not be loaded'); + } + + console.log('loaded func', func); + this.setState({ + func: func + }); + }.bind(this)); + }; }; Component.extend(FunctionComponent); @@ -73,7 +144,6 @@ FunctionComponent.Signature.Prototype = function() { if (i < func.parameters.length - 1) { paramsEl.append(','); } - }.bind(this)); return $$('div').addClass('se-signature').append( diff --git a/web/sheet/ui/SelectFunction.js b/web/sheet/ui/SelectFunction.js new file mode 100644 index 0000000000..eaa62dcb8a --- /dev/null +++ b/web/sheet/ui/SelectFunction.js @@ -0,0 +1,32 @@ +'use strict'; + +var Component = require('substance/ui/Component'); +var $$ = Component.$$; + +/* + Little pop-over that displays the available functions + + TODO: this is not interactive yet. we would need to capture + key events (down, up) to navigate the suggestion list. + Also clicking on an entry should fill them into the + cell. +*/ +function SelectFunction() { + SelectFunction.super.apply(this, arguments); +} + +SelectFunction.Prototype = function() { + + this.render = function() { + var el = $$('div').addClass('sc-select-function'); + + this.props.entries.forEach(function(entry) { + el.append($$('div').addClass('se-entry').append(entry)); + }); + return el; + }; +}; + +Component.extend(SelectFunction); + +module.exports = SelectFunction; \ No newline at end of file diff --git a/web/sheet/ui/SheetController.js b/web/sheet/ui/SheetController.js index eac969e3c7..9e171c4ee1 100644 --- a/web/sheet/ui/SheetController.js +++ b/web/sheet/ui/SheetController.js @@ -44,7 +44,8 @@ SheetController.Prototype = function() { this.getChildContext = function() { var childContext = Controller.prototype.getChildContext.call(this); return _.extend(childContext, { - i18n: I18n.instance + i18n: I18n.instance, + engine: this.props.engine }); };