Changeset 7222


Ignore:
Timestamp:
10/21/18 18:26:02 (6 years ago)
Author:
Leif-Jöran
Message:

Breakout of layeroptions from queryinput component.

Location:
SRUAggregator/trunk/src/main/resources/assets/js/components
Files:
1 added
1 edited

Legend:

Unmodified
Added
Removed
  • SRUAggregator/trunk/src/main/resources/assets/js/components/queryinput.jsx

    r7148 r7222  
    33import PropTypes from "prop-types";
    44import createReactClass from "create-react-class";
     5import PureRenderMixin from 'react-addons-pure-render-mixin';
    56import {CSSTransition, TransitionGroup} from "react-transition-group";
     7import {Controlled as CodeMirror} from 'react-codemirror2';
     8require('codemirror/mode/fcs-ql/fcs-ql');
     9require('codemirror/mode/javascript/javascript');
    610
    711var PT = PropTypes;
     12
     13function nextId() {
     14    return nextId.id++;
     15}
     16nextId.id = 0;
    817
    918var QueryInput = createReactClass({
    1019    //fixme! - class QueryInput extends React.Component {
    1120    propTypes: {
    12         searchedLanguage: PT.array,
    13         queryTypeId: PT.string.isRequired,
    14         query: PT.string,
    15         embedded: PT.bool.isRequired,
    16         placeholder: PT.string,
    17         onChange: PT.func.isRequired,
    18         onQuery: PT.func.isRequired,
    19         onKeyDown: PT.func.isRequired
     21        searchedLanguage: PT.array,
     22        queryTypeId: PT.string.isRequired,
     23        query: PT.string,
     24        embedded: PT.bool.isRequired,
     25        placeholder: PT.string,
     26        onQueryChange: PT.func.isRequired,
     27        onKeyDown: PT.func.isRequired
    2028    },
    2129
    2230    render: function() {
    23         //if (this.props.queryTypeId == "cql") {
     31        if (this.props.queryTypeId == "cql") {
    2432            return (
    25                 <input className="form-control input-lg search"
    26                        id="query-cql" name="query-cql" type="text"
    27                        value={this.props.query} placeholder={this.props.placeholder}
    28                        tabIndex="1" onChange={this.props.onChange}
    29                        //onQuery={this.props.onQuery}
    30                        onKeyDown={this.props.onKeyDown}
    31                        ref="cqlOrEmbeddedQuery"/>
     33                    <input className="form-control input-lg search"
     34                          id="query-cql" name="query-cql" type="text"
     35                          value={this.props.query} placeholder={this.props.placeholder}
     36                          tabIndex="1" onChange={ (evt) => this.props.onQueryChange(evt.target.value) }
     37                          //onQuery={this.props.onQuery}
     38                          onKeyDown={this.props.onKeyDown}
     39                          ref="cqlOrEmbeddedQuery"/>
     40                      /*
     41//                   <CodeMirror
     42//                       value={this.props.placeholder || this.props.query}
     43//                       options={{
     44//                           mode: 'fcs-ql',
     45//                           theme: 'default',
     46//                           lineNumbers: false,
     47//                           matchBrackets: true,
     48//                           viewportMargin: 1
     49//                       }}
     50//                       className="form-control input-lg search"
     51//                       onBeforeChange={(editor, data, value) => {
     52//                           this.setState({value});
     53//                       }}
     54//                       onChange={(editor, data, value) => {
     55//                           this.props.onChange(value);
     56//                       }}
     57//                       onQuery={this.props.onQuery}
     58//                       onKeyDown={(editor, event) => {
     59//                           this.props.onKeyDown(editor, event)
     60//                       }}
     61//                       ref="cqlOrEmbeddedQuery"
     62//                       >
     63//                    </CodeMirror>
     64              */
    3265            );
    33         // } else if (this.props.embedded && this.props.queryTypeId == "fcs") {
    34         //     return (
    35         //      <textarea className="form-control input-lg search"
    36         //             id="query-fcs" name="query-fcs"
    37         //             type="text" rows="1"
    38         //             value={this.props.query} placeholder={this.props.placeholder}
    39         //             tabIndex="1" onChange={this.props.onChange}
    40         //             //onQuery={this.props.onQuery}
    41         //             onKeyDown={this.props.onKeyDown}
    42         //             ref="fcsOrEmbeddedQuery" />
    43         //     );
    44         // }
    45         // return (<div id="adv_query_input_group" className="input-group-addon">
    46         //          <ADVTokens
    47         //                 query={this.props.query}
    48         //                 ref="fcsGQB"
    49         //             />
    50         // </div>);
     66        } else if (this.props.embedded && this.props.queryTypeId == "fcs") {
     67             return (
     68                <textarea className="form-control input-lg search"
     69                       id="query-fcs" name="query-fcs"
     70                       type="text" rows="1"
     71                       value={this.props.query} placeholder={this.props.placeholder}
     72                       tabIndex="1" onChange={ (evt) => this.props.onQueryChange(evt.target.value) }
     73                       //onQuery={this.props.onQuery}
     74                       onKeyDown={this.props.onKeyDown}
     75                       ref="fcsOrEmbeddedQuery" />
     76             );
     77         }
     78        return (<div id="adv_query_input_group">
     79                    <ADVTokens
     80                         query={this.props.query}
     81                         ref="fcsGQB"
     82                         onQueryChange={this.props.onQueryChange}
     83                     />
     84         </div>);
    5185    }
    5286});
     
    5589
    5690    propTypes: {
    57         query: PT.string
     91        query: PT.string, // initial state query. If you want to set a new one, change the 'key' prop on ADVTokens.
     92        onQueryChange: PT.func.isRequired
     93    },
     94   
     95    getDefaultProps() {
     96        return {
     97            query: '[ word = "Elefant" ]',
     98        }
    5899    },
    59100
    60101    getInitialState: function () {
    61         return {
    62             tokenCounter: 1,
    63             tokens: ["token1"]
    64         };
     102        this.queryStrCache = {};
     103       
     104        console.log('ADVTokens:', this.props.query);
     105       
     106        var match = queryToTokens(this.props.query);
     107        console.log('ADVTokens 2:', match);
     108        if (match === null) {
     109            return { tokens: ['token-'+nextId()] }
     110        }
     111       
     112        var tokens = [];
     113        match.forEach((m) => {
     114            var id = 'token-'+nextId();
     115            tokens.push(id);
     116            this.queryStrCache[id] = m;
     117        });
     118       
     119        return { tokens }
    65120    },
    66121
    67122    addADVToken: function() {
    68         var i = this.state.tokenCounter + 1;
    69         this.state.tokens.push('token' + i);
    70         this.setState({tokenCounter: i, tokens: this.state.tokens});
     123        this.setState( (oldSt) => {
     124                oldSt.tokens.push('token-'+nextId());
     125                return {tokens: oldSt.tokens}
     126            });
    71127    },
    72128   
    73129    removeADVToken: function(id) {
    74         var tokens = this.state.tokens;
    75         var i = tokens.indexOf(id);
    76         if (tokens.length > 1) {
    77             var one = tokens;
    78             var two = one.slice(0, i - 1)
    79                          .concat(one.slice(i));;
    80             this.setState({tokens: two});
    81         }
     130            this.setState( (oldSt) => {
     131                delete this.queryStrCache[id];
     132                oldSt.tokens.splice(oldSt.tokens.indexOf(id), 1);
     133                return {tokens: oldSt.tokens}
     134            }, this.fireQueryChange);
     135    },
     136   
     137    fireQueryChange() {
     138        var tokenParts = this.state.tokens.map( (id) => this.queryStrCache[id] );
     139        const queryString = tokenParts.join(' ')
     140        this.props.onQueryChange(queryString);
     141    },
     142   
     143    onQueryChange(tokenId, queryStr) {
     144        this.queryStrCache[tokenId] = queryStr;
     145        this.fireQueryChange();
    82146    },
    83147
    84148    render: function() {
    85         var i = 0;
    86         var tokens = this.state.tokens.map(function (token, i) {
    87             return (
    88                <CSSTransition key={i} classNames="token" timeout={{enter: 250, exit: 250}}>
    89                   <ADVToken
    90                         key={token}
    91                         parentToken={token}
    92                         handleRemoveADVToken={this.removeADVToken} />
    93                </CSSTransition>);
    94         }.bind(this));
    95 
    96         return (<div>
    97             <TransitionGroup>{tokens}</TransitionGroup>
    98                 <button className="btn btn-xs btn-default image_button insert_token" type="button" onClick={this.addADVToken} ref="addToken">
    99                     <i className="glyphicon glyphicon-plus"></i>
    100                 </button>
    101         </div>);
     149        var i = 0;
     150        var tokens = this.state.tokens.map( (tokenId, i) => {
     151            return (
     152               <CSSTransition key={tokenId} classNames="token" timeout={{enter: 250, exit: 250}}>
     153                  <ADVToken
     154                     query={this.queryStrCache[tokenId]}
     155                     onQueryChange={(qs) => this.onQueryChange(tokenId, qs)}
     156                     handleRemoveADVToken={() => this.removeADVToken(tokenId)} />
     157               </CSSTransition>
     158            );
     159        });
     160
     161        return (<div id="adv-tokens">
     162            <TransitionGroup>{tokens}</TransitionGroup>
     163            <button className="btn btn-xs btn-default image_button insert_token" type="button" onClick={this.addADVToken}>
     164                <i className="glyphicon glyphicon-plus"></i>
     165            </button>
     166        </div>);
    102167    }
    103168});
     
    105170var ADVToken = createReactClass({
    106171    propTypes: {
    107         parentToken: PT.string.isRequired,
    108         handleRemoveADVToken: PT.func.isRequired,
    109     },
     172        query: PT.string,
     173        onQueryChange: PT.func.isRequired,
     174            handleRemoveADVToken: PT.func.isRequired,
     175    },
     176   
    110177    render: function() {
    111         return (<div className="token query_token inline btn-group" style={{display:"inline-block"}}>
    112             <div className="token_header">
    113                <button className="btn btn-xs btn-default image_button close_btn" type="button" onClick={this.props.handleRemoveADVToken(this.props.parentToken)} ref="removeToken">
    114                   <i className="glyphicon glyphicon-remove-circle" />
    115                </button>
    116                <div style={{clear:"both"}} />
    117                </div>
    118                <div className="args">
    119                { /* and.query_arg* and token_footer */ }
    120                  <ANDQueryArgs />
    121                <div className="lower_footer">
    122                </div>
    123             </div>
    124         </div>);
     178            return (<div className="token query_token inline btn-group" style={{display:"inline-block"}}>
     179                <div className="token_header">
     180                   <span className="image_button close_btn" type="button" onClick={this.props.handleRemoveADVToken} ref="removeToken">
     181                      <i className="glyphicon glyphicon-remove-circle" />
     182                   </span>
     183                   <div style={{clear:"both"}} />
     184                   </div>
     185                   <div className="args">
     186                   { /* and.query_arg* and token_footer */ }
     187                     <ANDQueryArgs onQueryChange={this.props.onQueryChange} query={this.props.query} />
     188                   <div className="lower_footer">
     189                   </div>
     190                </div>
     191            </div>);
    125192    }
    126193});
    127194
    128195var ADVTokenMenu = createReactClass({
     196    mixins: [PureRenderMixin],
     197   
     198    propTypes: {
     199        onChange: PT.func.isRequired,
     200        repeat1: PT.string,
     201        repeat2: PT.string,
     202    },
     203   
    129204        getInitialState: function() {
    130             return {"hideRepeatMenu": true};
     205            var repeat1 = this.props.repeat1||'';
     206            var repeat2 = this.props.repeat2||'';
     207            return {
     208                repeat1,
     209                repeat2,
     210                hideMenu: (repeat1||repeat2) ? false : true,
     211                isStart: false,
     212                isEnd: false,
     213           };
     214        },
     215       
     216        getMenuState() {
     217            if (this.state.hideMenu) {
     218                return {};
     219            } else {
     220                return $.extend({}, this.state); // copy of state
     221            }
    131222        },
    132223
    133224        toggleRepeatMenu: function(e) {
    134             this.setState({"hideRepeatMenu": !this.state.hideRepeatMenu});
    135             e.preventDefault();
     225            this.setState((st) =>({hideMenu: !st.hideMenu}));
     226        },
     227        toggleStart: function(e) {
     228            this.setState((st) => ({isStart: !st.isStart}));
     229        },
     230        toggleEnd: function(e) {
     231            this.setState((st) => ({isEnd: !st.isEnd}));
     232        },
     233        componentDidMount() {
     234            // make this compoent controlled to so that this awkward ref.getMenuState() stuff can be removed
     235            this.props.onChange(this.getMenuState());
     236        },
     237        componentDidUpdate() {
     238            // safe because of pure render mixin: will only update on state change.
     239            this.props.onChange(this.getMenuState());
    136240        },
    137241       
    138242        render: function() {
    139             return (<div>
    140             <button className="btn btn-xs btn-default image_button repeat_menu" onClick={this.toggleRepeatMenu} ref="repeatMenu">
    141                 <i className="fa fa-cog" />
    142             </button>
    143             <div id="repeatMenu" className={"repeat hide-" + this.state.hideRepeatMenu}>
    144                 <span>repeat</span>
    145                 <input type="number" id="repeat1" value={this.state.repeat1} ref="repeat1"/>
    146                 <span>to</span>
    147                 <input type="number" id="repeat2" value={this.state.repeat2} ref="repeat2"/>
    148                 <span>times</span>
    149             </div>
     243            return (<div id="ADVtokenMenu">
     244                <button className="btn btn-xs btn-default image_button repeat_menu" onClick={this.toggleRepeatMenu} ref="repeatMenu">
     245                        <i className="fa fa-cog" />
     246            </button>
     247           
     248            <div id="ADVtokenMenu-items" className={"hide-" + this.state.hideMenu}>
     249                <div id="repeatMenu" className={"repeat"}>
     250                        <span>Repeat</span>
     251                        <input type="text" id="repeat1" value={this.state.repeat1} onChange={(evt) => this.setState({repeat1: evt.target.value})} ref="repeat1"/>
     252                        <span>to</span>
     253                        <input type="text" id="repeat2" value={this.state.repeat2} onChange={(evt) => this.setState({repeat2: evt.target.value})} ref="repeat2"/>
     254                        <span>times</span>
     255                </div>
     256                <div id="start-end-menu">
     257                        <div><label><input type="checkbox" checked={this.state.isStart} onChange={this.toggleStart} /> Sentence start</label></div>
     258                        <div><label><input type="checkbox" checked={this.state.isEnd} onChange={this.toggleEnd} /> Sentence end</label></div>
     259                </div>
     260            </div>
    150261            </div>);
    151262        }
    152     });
    153 
    154     var ANDQueryArgs = createReactClass({
    155 
     263});
     264
     265var ANDQueryArgs = createReactClass({
     266    propTypes: {
     267        query: PT.string,
     268        onQueryChange: PT.func.isRequired,
     269    },
     270   
    156271        getInitialState: function() {
    157             return {
    158                 andCounter: 1,
    159                 ands: ["and1"]
    160             };
    161         },
    162272       
    163         setADVTokenLayer: function(layer) {
    164             //fixme! - check agains valid layers
    165             return;
     273            this.queryStrCache = {};
     274           
     275            console.log('ANDQueryArgs:', this.props.query);
     276       
     277            var repeat1, repeat2;
     278            var qlean = this.props.query;
     279            if (qlean) {
     280            var repeatMatch = qlean.match(/{ *(\d)* *(?:, *(\d)*)? *} *$/);
     281           
     282            if (repeatMatch !== null) {
     283                repeat1 = repeatMatch[1];
     284                repeat2 = repeatMatch[2] || repeat1;
     285                qlean = qlean.substring(0, qlean.length - repeatMatch[0].length);
     286                }
     287               
     288           
     289            // replace token's [ and ]
     290            qlean = qlean.replace(/^\s*\[\s*/, '').replace(/\s*\]\s*$/, '');
     291            }
     292           
     293            var match = queryToANDArgs(qlean);
     294            console.log('ANDQueryArgs 2:', match);
     295        if (match === null) {
     296            return {
     297                    ands: ["and-"+nextId()]
     298            };
     299        }
     300       
     301        var ands = [];
     302        match.forEach((m) => {
     303            var id = 'and-'+nextId();
     304            ands.push(id);
     305            this.queryStrCache[id] = m;
     306        });
     307       
     308        return {
     309            ands,
     310            repeat1,
     311            repeat2,
     312        }
    166313        },
    167314
    168315        addADVAnd: function() {
    169             var i = this.state.andCounter + 1;
    170             this.state.ands.push('and' + i);
    171             this.setState({andCounter: i, ands: this.state.ands});
    172 
     316            this.setState( (oldSt) => {
     317                oldSt.ands.push('and-'+nextId());
     318                return {ands: this.state.ands}
     319            });
    173320        },
    174321
    175322        removeADVAnd: function(id) {
    176             var ands = this.state.ands;
    177             var i = ands.indexOf(id);
    178             if (ands.length > 1) {
    179                 var one = ands;
    180                 var two = one.slice(0, i - 1)
    181                              .concat(one.slice(i));;
    182                 this.setState({ands: two});
    183             }
    184         },
     323            this.setState( (oldSt) => {
     324                delete this.queryStrCache[id];
     325                oldSt.ands.splice(oldSt.ands.indexOf(id), 1);
     326                return {ands: oldSt.ands}
     327            }, this.fireQueryChange);
     328        },
     329       
     330        onMenuChange(menust) {
     331            this.setState({
     332                menuState_isStart: menust.isStart,
     333                menuState_isEnd: menust.isEnd,
     334                menuState_repeat1: menust.repeat1,
     335                menuState_repeat2: menust.repeat2,
     336            }, () =>  this.fireQueryChange() );
     337        },
     338   
     339    fireQueryChange() {
     340        var andParts = this.state.ands.map( (id) => this.queryStrCache[id] );
     341       
     342        if (this.state.menuState_isStart) {
     343            andParts.push('lbound(sentence)')
     344        }
     345        if (this.state.menuState_isEnd) {
     346            andParts.push('rbound(sentence)')
     347        }
     348       
     349       
     350        var queryString = andParts.length >= 2 ? andParts.join(' & ') : andParts[0];
     351        queryString = `[ ${queryString} ]`;
     352       
     353        if (this.state.menuState_repeat1 || this.state.menuState_repeat2 ) {
     354            queryString = queryString + '{' + (this.state.menuState_repeat1||this.state.menuState_repeat2 ) + ',' + (this.state.menuState_repeat2 || this.state.menuState_repeat1) + '}'
     355        }
     356       
     357        this.props.onQueryChange(queryString);
     358    },
     359   
     360    onQueryChange(andId, queryStr) {
     361        this.queryStrCache[andId] = queryStr;
     362        this.fireQueryChange();
     363    },
    185364
    186365        renderANDTokenFooter: function () {
    187             return (<div className="token_footer">
    188                 <button className="btn btn-xs btn-default image_button insert_arg" onClick={this.addADVAnd} ref="addAndButton">
    189                     <i className="glyphicon glyphicon-plus"/>
    190                 </button>
    191                 <ADVTokenMenu/>
    192                 <div style={{clear:"both"}}/>
    193             </div>);
    194         },
    195 
    196         renderANDQueryArg: function (and) {
    197             return (<div className="and query_arg">
    198                 <span className="hidden">and</span>
    199                 <ANDQueryORArgs
    200                 numAnds={this.state.ands.length}
    201                 parentAnd={and}
    202                 handleRemoveADVAnd={this.removeADVAnd}/>
     366            return (
     367            <div className="token_footer">
     368                    <button className="btn btn-xs btn-default image_button insert_arg" onClick={this.addADVAnd} ref="addAndButton">
     369                        <i className="glyphicon glyphicon-plus"/>
     370                    </button>
     371                    <ADVTokenMenu ref={(ref) => this.menuRef = ref} onChange={this.onMenuChange} repeat1={this.state.repeat1} repeat2={this.state.repeat2} />
     372                    <div style={{clear:"both"}}/>
    203373            </div>);
    204374        },
    205375       
    206376        render: function () {
    207             var andQueryArgs = this.state.ands.map(function (and, i) {
    208             return (
    209                 <CSSTransition key={i} classNames="fade" timeout={{enter: 200, exit: 200}}>
    210                    <div key={and}>{this.renderANDQueryArg(and)}</div>
    211                 </CSSTransition>);
    212             }.bind(this));
     377            var andQueryArgs = this.state.ands.map(( andId ) => {
     378                return (
     379                    <CSSTransition key={andId} classNames="fade" timeout={{enter: 200, exit: 200}}>
     380                        <div className="and query_arg">
     381                            <span className="hidden">and</span>
     382                            <ANDQueryORArgs
     383                                query={this.queryStrCache[andId]}
     384                                onQueryChange={(qs) => this.onQueryChange(andId, qs)}
     385                                handleRemoveADVAnd={() => this.removeADVAnd(andId)}
     386                                />
     387                    </div>
     388                    </CSSTransition>);
     389                }
     390           );
    213391            return (<div>
    214                 <TransitionGroup>
    215                    {andQueryArgs}
    216                 </TransitionGroup>
    217                 {this.renderANDTokenFooter()}
     392                    <TransitionGroup>
     393                       {andQueryArgs}
     394                    </TransitionGroup>
     395                    {this.renderANDTokenFooter()}
    218396                </div>);
    219397    }
     
    222400var ANDQueryORArgs = createReactClass({
    223401    propTypes: {
    224         numAnds: PT.number.isRequired,
    225         parentAnd: PT.string.isRequired,
    226         handleRemoveADVAnd: PT.func.isRequired,
    227     },
     402        query: PT.string,
     403        onQueryChange: PT.func.isRequired,
     404        handleRemoveADVAnd: PT.func.isRequired,
     405    },
     406   
    228407    getInitialState: function() {
    229         return {
    230             orCounter: 1,
    231             ors: [{id: "or1", layerType: "string:lemma", placeholder: "Bagdad"}]
    232         };
    233     },
    234 
    235     //shouldComponentUpdate: function (nextProps, nextState) {
    236     //  return nextState.ors.length > 1; //!== this.state.ors.length;
    237     //},
    238 
    239     setADVTokenOp: function(op) {
    240         //fixme! - check agains valid layers
    241         return;
    242     },
    243 
    244     setADVInputDefault: function(or) {
    245         //fixme! - disable SearchButton if not atleast 1 token is in the query filter
    246         return;
     408        this.queryStrCache = {};
     409       
     410        console.log('ANDQueryORArgs:', this.props.query);
     411        var match = queryToORArgs(this.props.query);
     412        console.log('ANDQueryORArgs 2:', match);
     413           
     414        if (match === null) {
     415            return {
     416                    ors: ["or-"+nextId()]
     417            };
     418        }
     419       
     420        var ors = [];
     421        match.forEach((m) => {
     422            var id = 'or-'+nextId();
     423            ors.push(id);
     424            this.queryStrCache[id] = m;
     425        });
     426       
     427        return {
     428            ors
     429        }
    247430    },
    248431
    249432    validateADV: function(value) {
    250         //fixme! - disable SearchButton if not atleast 1 token is in the query filter
    251         return;
     433            //fixme! - disable SearchButton if not atleast 1 token is in the query filter
     434            return;
     435    },
     436   
     437    fireQueryChange() {
     438        var orParts = this.state.ors.map( (id) => this.queryStrCache[id] );
     439        const queryString = orParts.length >= 2 ? '( ' + orParts.join(' | ') + ' )' : orParts[0];
     440        this.props.onQueryChange(queryString);
    252441    },
    253442
    254443    addADVOr: function(e) {
    255         var i = this.state.orCounter + 1;
    256         this.state.ors.push({id: 'or' + i, layerType: "string:pos", placeholder: "PROPN"});
    257         this.setState({orCounter: i, ors: this.state.ors});
    258     },
    259 
    260     removeADVOr: function(id, e) {
    261         var ors = this.state.ors;
    262         var i = ors.indexOf(id);
    263         if (ors.length > 1) {
    264             var one = ors;
    265             var two = one.slice(0, i - 1)
    266                          .concat(one.slice(i));;
    267             this.setState({ors: two});
    268         } else if (ors.length === 1 && this.props.numAnds > 1) {
    269             this.props.handleRemoveADVAnd(this.props.parentAnd);
    270         }
    271     },
    272 
    273     render: function () {
    274         var orArgs = this.state.ors.map(function (or, i) {
    275             return (           
    276                 <CSSTransition key={i} classNames="fade" timeout={{enter: 200, exit: 200}}>
    277                    <ORArg key={or.id}
    278                           data={or}
    279                           handleRemoveADVOr={this.removeADVOr}
    280                           handleSetADVInputDefault={this.setADVInputDefault}
    281                           handleSetADVTokenOp={this.setADVTokenOp}
    282                           handleValidateADV={this.validateADV}
    283                    />
    284                 </CSSTransition>
    285             )
    286         }.bind(this));
    287         return (<div>
    288             <div className="or_container">
    289                <TransitionGroup>
    290                   {orArgs}
    291                </TransitionGroup>
    292             </div>
    293             <div className="arg_footer">
    294                 <span className="link" onClick={this.addADVOr} ref={'addOR' + this.props.numAnds}>or</span>
    295                 <div style={{clear:"both"}}/>
    296             </div>
    297         </div>);
     444            this.setState( (oldSt) => {
     445                oldSt.ors.push('or-'+nextId());
     446                return {ors: this.state.ors}
     447            });
     448    },
     449
     450    removeADVOr: function(id) {
     451        this.setState( (oldSt) => {
     452            delete this.queryStrCache[id];
     453                oldSt.ors.splice(oldSt.ors.indexOf(id), 1);
     454                return {ors: oldSt.ors}
     455            }, () => {
     456                if (this.state.ors.length===0) {
     457                    this.props.handleRemoveADVAnd();
     458                } else {
     459                    this.fireQueryChange();
     460                }
     461           });
     462    },
     463   
     464    onQueryChange(orId, queryStr) {
     465        this.queryStrCache[orId] = queryStr;
     466        this.fireQueryChange();
     467    },
     468
     469    render() {
     470            var orArgs = this.state.ors.map((orId)  => {
     471                return (               
     472                    <CSSTransition key={orId} classNames="fade" timeout={{enter: 200, exit: 200}}>
     473                        <ORArg
     474                              query={this.queryStrCache[orId]}
     475                          handleRemoveADVOr={() => this.removeADVOr(orId)}
     476                              handleValidateADV={() => {return true}}
     477                              onQueryChange={(qs) => this.onQueryChange(orId, qs)}
     478                       />
     479                    </CSSTransition>
     480                )
     481            });
     482            return (<div>
     483                <div className="or_container">
     484                   <TransitionGroup>
     485                      {orArgs}
     486                   </TransitionGroup>
     487                </div>
     488                <div className="arg_footer">
     489                        <span className="link" onClick={this.addADVOr} >or</span>
     490                        <div style={{clear:"both"}}/>
     491                </div>
     492            </div>);
    298493    }
    299494});
    300495
    301496var ORArg = createReactClass({
     497    mixins: [PureRenderMixin],
     498   
    302499    propTypes: {
    303         data: PT.object.isRequired,
    304         handleRemoveADVOr: PT.func.isRequired,
    305         handleSetADVInputDefault: PT.func.isRequired,
    306         handleSetADVTokenOp: PT.func.isRequired,
    307         handleValidateADV: PT.func.isRequired,
     500        query: PT.string,
     501            handleValidateADV: PT.func.isRequired,
     502            handleRemoveADVOr: PT.func.isRequired,
     503            onQueryChange: PT.func.isRequired,
     504    },
     505   
     506    getInitialState() {
     507        console.log('ORArg:', this.props.query);
     508        var qp = queryParse(this.props.query);
     509        console.log('ORArg 2:', qp);
     510       
     511        if (qp !== null) {
     512            var layer = qp.layer;
     513            var op = qp.op;
     514            var val = qp.val;
     515        }
     516        return {
     517            layer: layer||'word',
     518            argOpt: op||'is',
     519            argValue: val||'',
     520           
     521            editingText: false,
     522        };
     523    },
     524   
     525    fireQueryChange() {
     526        const queryString = layerToQueryString(this.state.layer, this.state.argOpt, this.state.argValue);
     527        this.props.onQueryChange(queryString);
     528    },
     529   
     530    onlayerChange (evt) {
     531        var layer = evt.target.value;
     532        this.setState((st) => {
     533            var argOpt  = getLayerArgOpts(layer)[0].value;
     534            var lvo = getLayerValueOptions(layer, argOpt, st.argValue);
     535            var argValue = '';
     536            if (lvo === undefined) argValue = '';
     537            else argValue= lvo[0].value;
     538           
     539            return {
     540                layer,
     541                argOpt,
     542                argValue,
     543            }
     544        })
     545    },
     546   
     547    onArgOptChange (evt) {
     548        var argOpt = evt.target.value;
     549        this.setState({argOpt});
     550    },
     551   
     552    onArgValueChange (evt) {
     553        var value = evt.target.value;
     554        //console.log("picked arg value", value);
     555       
     556        this.setState({argValue: value});
     557    },
     558   
     559   
     560    componentDidMount(){
     561        this.fireQueryChange();
     562    },
     563   
     564    componentDidUpdate(prevProps, prevState) {
     565        // after state update.
     566        if (prevState.layer !== this.state.layer
     567            || prevState.argOpt !== this.state.argOpt
     568            || prevState.argValue !== this.state.argValue
     569            /*|| (!this.state.editingText && prevState.argValue !== this.state.argValue) // only update text-value input on field blur
     570            || (prevState.editingText !== this.state.editingText && prevState.editingText) // stopped editing text field.
     571            */
     572        ) {
     573            this.fireQueryChange();
     574        }
     575    },
     576   
     577    renderInput() {
     578       
     579        var valueOptions = getLayerValueOptions(this.state.layer, this.state.argOpt, this.state.argValue);
     580       
     581        if (valueOptions === undefined) {
     582            return <input type="text" className="form-control"
     583                    value={this.state.argValue}
     584                    onChange={this.onArgValueChange}
     585                    onFocus={() => this.setState({editingText:true})}
     586                    onBlur={() => this.setState({editingText: false})}
     587                    />
     588            } else {
     589                return <select className="form-control" value={this.state.argValue} onChange={this.onArgValueChange} onFocus={() => this.setState({editingText:true})} onBlur={() => this.setState({editingText: false})}>
     590                {
     591                    valueOptions.map( (valOpt) => {
     592                    return <option value={valOpt.value}>{valOpt.label}</option>
     593                })
     594                }
     595                </select>
     596            }
    308597    },
    309598
    310599    render: function() {
    311         return (<div className="or or_arg">
    312             <div className="left_col" >
    313                 <button className="btn btn-xs btn-default image_button remove_arg" onClick={this.props.handleRemoveADVOr.bind(null, this.props.data.id)} ref={'removeADVOr_' + this.props.data.id}>
    314                     <i className="glyphicon glyphicon-minus"></i>
    315                 </button>
    316             </div>
    317             <div className="right_col inline_block" style={{display:"inline-block"}}> { /* , margin-left: "5px" */ }
    318                 <div className="arg_selects lemma">
    319                     <select className="arg_type" onChange={this.props.handleSetADVInputDefault("or")} defaultValue={this.props.data.layerType} ref={'ANDLayerType_' + this.props.data.id}>
    320                         { /* onChange={this.handleSetADVTokenLayer("value")} */}
    321                         <optgroup label="word">
    322                             { /* ::before */ }
    323                             <option value="string:word" label="word">word</option>
    324                         </optgroup>
    325                         <optgroup label="wordAttribute">
    326                             { /* ::before */ }
    327                             <option value="string:pos">part-of-speech</option>
    328                             <option value="string:lemma">lemma</option>
    329                         </optgroup>
    330                         <optgroup label="textAttribute">
    331                             <option value="string:_.text_language" label="language">language</option>
    332                         </optgroup>
    333                     </select>
    334                     <select className="arg_opts" defaultValue="string:contains" onChange={this.props.handleSetADVTokenOp("op")}>
    335                         <option value="string:contains" label="is">is</option>
    336                         <option value="string:not contains" label="is not">is not</option>
    337                     </select>
    338                 </div>
    339                 <div className="arg_val_container">
    340                     <input id={'inputADV_' + this.props.data.id} type="text" defaultValue={this.props.data.placeholder} onChange={this.props.handleValidateADV} ref={'textEntry_' + this.props.data.id}/>
    341                 </div>
    342                <select>
    343                 <option label="PROPN" value="string:PROPN">Proper Noun</option>
    344                </select>
    345             </div>
    346             </div>);
     600            return (<div className="or or_arg">
     601                <div className="left_col" >
     602                        <button className="btn btn-xs btn-default image_button remove_arg" onClick={this.props.handleRemoveADVOr}>
     603                            <i className="glyphicon glyphicon-minus"></i>
     604                        </button>
     605                    </div>
     606                    <div className="right_col inline_block" style={{display:"inline-block"}}> { /* , margin-left: "5px" */ }
     607                           
     608                            <div className="arg_selects form-inline">
     609                                <select className="arg_type form-control" value={this.state.layer} onChange={this.onlayerChange}>
     610                                {
     611                                    layerCategories.map( (cat) => {
     612                                        return (
     613                                            <optgroup label={cat.label}>
     614                                        {
     615                                            cat.layers.map( (layer) => {
     616                                                return <option value={layer}>{getLayerLabel(layer)}</option>;
     617                                            })
     618                                        }
     619                                            </optgroup>
     620                                        );
     621                                    })
     622                                 }
     623                            </select>
     624                           
     625                            <select className="arg_opts form-control" value={this.state.argOpt} onChange={this.onArgOptChange}>
     626                            {
     627                                getLayerArgOpts(this.state.layer).map( (argOpt) => {
     628                                    return <option value={argOpt.value}>{argOpt.label}</option>;
     629                                })
     630                                }
     631                                </select>
     632                            </div>
     633                           
     634                            <div className="arg_val_container">
     635                            { this.renderInput() }
     636                            </div>
     637                </div>
     638            </div>
     639        );
    347640    }
    348641});
    349642
     643function getLayers() {
     644    const layers_arr = [];
     645    for (var l in layers) {
     646        layers_arr.push(l);
     647    }
     648    return layers_arr;
     649}
     650
     651function getLayerArgOpts(layer) {
     652    return layers[layer].argOpts;
     653}
     654
     655function isValidLayerOperator(layer, operator) {
     656    return !!layers[layer].argOpts.find((e) => e.value===operator);
     657}
     658function isValidLayerValue(layer, operator, value) {
     659    var valopts = getLayerValueOptions(layer);
     660    if (!valopts) {
     661        return true
     662    }
     663    return valopts.indexOf(value) !== -1;
     664}
     665
     666function layerToQueryString(layer, operator, value) {
     667    var toStr = layers[layer].toQueryString
     668    if (! toStr) {
     669        toStr = getLayerArgOpts(layer).defaultToStr;
     670    }
     671    if (! toStr) {
     672        toStr = wordOptions.defaultToStr;
     673        console.warn("layerToQueryString: couldnt find a toQueryString method!");
     674    }
     675    var qstr = toStr(layer, operator, value);
     676    if (qstr === undefined) {
     677        console.warn("layerToQueryString: qstr undefined!");
     678        return 'undefined';
     679    }
     680    return qstr;
     681}
     682
     683function getLayerLabel(layer) {
     684    return layers[layer].label;
     685}
     686
     687function getOperatorLabel(layer, operator) {
     688    return layers[layer].argOpts[operator].label;
     689}
     690
     691function getLayerValueOptions(layer, operator, value) {
     692    var valopts = layers[layer].valueOptions;
     693    if (! valopts) {
     694        return;
     695    }
     696    if (typeof valopts === 'function') {
     697        return valopts(layer, operator, value);
     698    }
     699    else if (valopts.length) {
     700        return valopts; // array
     701    }
     702}
     703
     704
     705// a RE string matching a "double-quote"d string
     706const quotedStringRE = /(?:"(?:\\"|[^"])*")/.source;
     707
     708// in: 'word = "zebra" '
     709// out: ['word', '=', 'zebra']
     710function queryParse(q) {
     711    if (!q) return null;
     712    var match = q.match(/^\s*(\w+) *(=|!=) *"((?:\\"|.)*?)"/);
     713    if(match===null) {
     714        return null;
     715    }
     716   
     717    const layer = match[1];
     718    const op = match[2];
     719    const value = match[3];
     720   
     721    var fromStr = getLayerArgOpts(layer).fromQueryString
     722    if (! fromStr) {
     723        fromStr = getLayerArgOpts(layer).defaultFromString;
     724    }
     725    if (! fromStr) {
     726        fromStr = wordOptions.defaultFromStr;
     727        log.error("queryParse: couldnt find a fromQueryString method!");
     728    }
     729   
     730    return fromStr(layer, op, value);
     731}
     732
     733// in: '(word = "zebra" | word = "zoo" ...)'
     734// out: ['word = "zebra" ', ' (word = "zoo" ...)']
     735function queryToORArgs(q) {
     736    if (!q) return null;
     737    var match = q.trim().match(queryToORArgs.re);
     738    return match;
     739}
     740queryToORArgs.re = RegExp('(?:'+quotedStringRE+'|[^()|])+', 'g')
     741
     742// in: '[word = "zebra" & (word = "zoo" ...)]'
     743// out: ['word = "zebra" ', ' (word = "zoo" ...)']
     744function queryToANDArgs(q) {
     745    if (!q) return null
     746   
     747    var match = q.trim().match(queryToANDArgs.re);
     748    return match;
     749}
     750queryToANDArgs.re = RegExp('(?:'+quotedStringRE+'|[^&])+', 'g')
     751
     752
     753// in: '[word = "zebra"] [word = "zoo"]'
     754// out: ['[word = "zebra"]', '[word = "zoo"]']
     755function queryToTokens(q) {
     756    if (!q) return null;
     757    var match = q.match(queryToTokens.re);
     758    return match;
     759}
     760queryToTokens.re = RegExp('\\[(?:'+quotedStringRE+'|.)*?\\] *(?:\\{.*?\\})?', 'g');
     761
     762
     763/*To simplify matching regex filter out words within "quotemarks". This help to not stumble on any special characters that can occur there. */
     764function filterWords(s, f) {
     765    const filteredString = s.replace(/("(?:\\"|[^"])*")/g, (m) => {
     766        filteredWords.push(m)
     767        return '""'
     768    })
     769    const ret = f(filteredString)
     770    // restore words
     771   
     772    // return return value
     773    return ret;
     774}
     775
     776var wordOptions = [
     777    {value: 'is', label: 'is'},
     778    {value: 'is_not', label: 'is not'},
     779    {value: 'contains', label: 'contains'},
     780    {value: 'starts_with', label: 'starts with'},
     781    {value: 'ends_with', label: 'ends with'},
     782    {value: 'regex', label: 'regex'},
     783    {value: 'not_regex', label: 'not regex'},
     784]
     785var liteOptions = [
     786    {value: "is", label: "is"},
     787    {value: "is_not", label: "is not"},
     788]
     789var setOptions = [
     790    {value: "is", label: "is"},
     791    {value: "is_not", label: "is not"},
     792]
     793var probabilitySetOptions = [
     794    {value: "is", label: "highest_rank"},
     795    {value: "is_not", label: "not_highest_rank"},
     796    {value: "contains", label: "rank_contains"},
     797    {value: "contains_not", label: "not_rank_contains"},
     798]
     799
     800setOptions.defaultToStr = (layer, op, val) => {
     801    switch(op) {
     802    case 'is':
     803        return `${layer} = "${val}"`
     804    case 'is_not':
     805        return `${layer} != "${val}"`
     806    }
     807}
     808setOptions.defaultFromString = (layer, op, val) => {
     809    return {layer, op: op==='!='?'is_not':'is', val}
     810}
     811
     812wordOptions.defaultToStr = (layer, op, val) => {
     813    var unescVal = val;
     814    var val = escapeRegExp(val);
     815    switch(op) {
     816    case 'is':
     817        return `${layer} = "${val}"`
     818    case 'is_not':
     819        return `${layer} != "${val}"`
     820    case 'contains':
     821        return `${layer} = ".*${val}.*"`
     822    case 'starts_with':
     823        return `${layer} = "${val}.*"`
     824    case 'ends_with':
     825        return `${layer} = ".*${val}"`
     826    case 'regex':
     827        return `${layer} = "${unescVal}"`
     828    case 'not_regex':
     829        return `${layer} != "${unescVal}"`
     830    }
     831}
     832wordOptions.defaultFromString = (layer, op, val) => {
     833    // layer should be good. Now figure out op, and if val is escaped or not
     834    if (op === '=') {
     835        var strippedOfSemiRE = val.replace(/^\.\*/, '').replace(/\.\*$/, '');
     836        if ( strippedOfSemiRE.length !== val.length ) {
     837            // could be one of: startswith, contains, endswith.
     838            if ( ! guessIfRegexp(strippedOfSemiRE) ) {
     839                // Ok, it is one of: startswith, contains, endswith.
     840                if (val.startsWith('.*') && val.endsWith('.*')) {
     841                    var op2 = 'contains';
     842                } else if (val.startsWith('.*')) {
     843                    op2 = 'starts_with';
     844                } else if (val.endsWith('.*')) {
     845                    op2 = 'ends_with'
     846                } else {
     847                    console.error("parsing query failed due to coding error");
     848                    return null;
     849                }
     850                return {
     851                    layer: layer,
     852                    op: op2,
     853                    val: unescapeRegExp(strippedOfSemiRE)
     854                }
     855            }
     856            // its regexp.
     857        }
     858    }
     859   
     860    if (guessIfRegexp(val)) {
     861        // its regexp
     862        return {layer, op: op==='='?'regex':'not_regex', val: val};
     863    }
     864   
     865    // its not regexp
     866    return {layer, op: op==='='?'is':'is_not', val: unescapeRegExp(val)};
     867}
     868function guessIfRegexp(s) {
     869    return !! s.match(/[^\\][-[\]{}()*+\\?.,^$|#]/); // find if it contains any unescaped regex characters
     870}
     871function unescapeRegExp(text) {
     872  return text.replace(/\\([-[\]{}()*+?.,\\^$|#])/g, '$1');
     873}
     874function escapeRegExp(text) {
     875  return text.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&');
     876}
     877
     878const layers = {
     879    'word': {
     880        label: 'word',
     881        argOpts: wordOptions,
     882    },
     883    'pos': {
     884        label: 'part-of-speech UD v2.0 tagset',
     885        argOpts: setOptions,
     886        valueOptions: [
     887            {value: "ADJ", label: "Adjective"},
     888            {value: "ADV", label: "Adverb"},
     889            {value: "INTJ", label: "Interjection"},
     890            {value: "NOUN", label: "Noun"},
     891            {value: "PROPN", label: "Proper noun"},
     892            {value: "VERB", label: "Verb"},
     893            {value: "ADP", label: "Adposition"},
     894            {value: "AUX", label: "Auxiliary"},
     895            {value: "CCONJ", label: "Coordinating conjunction"},
     896            {value: "DET", label: "Determiner"},
     897            {value: "NUM", label: "Numeral"},
     898            {value: "PART", label: "Particle"},
     899            {value: "PRON", label: "Pronoun"},
     900            {value: "SCONJ", label: "Subordinating conjunction"},
     901            {value: "PUNCT", label: "Punctuation"},
     902            {value: "SYM", label: "Symbol"},
     903            {value: "X", label: "Other"},
     904        ],
     905    },
     906    'lemma': {
     907        label: 'lemmatization of tokens',
     908        argOpts: wordOptions,
     909    },
     910    'orth': {
     911        label: 'orthographic transcription',
     912        argOpts: wordOptions,
     913    },
     914    'norm': {
     915        label: 'orthographic normalization',
     916        argOpts: wordOptions,
     917    },
     918    'phonetic': {
     919        label: 'phonetic transcription SAMPA',
     920        argOpts: wordOptions, // TODO special toString/parse? (probably due to regex character handling)
     921    },
     922    'text': {
     923        label: 'Layer only for Basic Search',
     924        argOpts: wordOptions,
     925    },
     926    '_.text_language': {
     927        label: 'language',
     928        argOpts: wordOptions,
     929    },
     930};
     931
     932const layerCategories = [
     933    {cat: 'word', label: 'Word', layers: ['word']},
     934    {cat: 'wordAttribute', label: 'Word attribute', layers: ['pos', 'lemma', 'orth', 'norm', 'phonetic', 'text']},
     935    {cat: 'textAttribute', label: 'Text attribute', layers: ['_.text_language']},
     936];
     937
     938
    350939module.exports = QueryInput;
Note: See TracChangeset for help on using the changeset viewer.