Changeset 7222
- Timestamp:
- 10/21/18 18:26:02 (6 years ago)
- 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 3 3 import PropTypes from "prop-types"; 4 4 import createReactClass from "create-react-class"; 5 import PureRenderMixin from 'react-addons-pure-render-mixin'; 5 6 import {CSSTransition, TransitionGroup} from "react-transition-group"; 7 import {Controlled as CodeMirror} from 'react-codemirror2'; 8 require('codemirror/mode/fcs-ql/fcs-ql'); 9 require('codemirror/mode/javascript/javascript'); 6 10 7 11 var PT = PropTypes; 12 13 function nextId() { 14 return nextId.id++; 15 } 16 nextId.id = 0; 8 17 9 18 var QueryInput = createReactClass({ 10 19 //fixme! - class QueryInput extends React.Component { 11 20 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 20 28 }, 21 29 22 30 render: function() { 23 //if (this.props.queryTypeId == "cql") {31 if (this.props.queryTypeId == "cql") { 24 32 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 */ 32 65 ); 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>); 51 85 } 52 86 }); … … 55 89 56 90 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 } 58 99 }, 59 100 60 101 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 } 65 120 }, 66 121 67 122 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 }); 71 127 }, 72 128 73 129 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(); 82 146 }, 83 147 84 148 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>); 102 167 } 103 168 }); … … 105 170 var ADVToken = createReactClass({ 106 171 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 110 177 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>); 125 192 } 126 193 }); 127 194 128 195 var ADVTokenMenu = createReactClass({ 196 mixins: [PureRenderMixin], 197 198 propTypes: { 199 onChange: PT.func.isRequired, 200 repeat1: PT.string, 201 repeat2: PT.string, 202 }, 203 129 204 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 } 131 222 }, 132 223 133 224 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()); 136 240 }, 137 241 138 242 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> 150 261 </div>); 151 262 } 152 }); 153 154 var ANDQueryArgs = createReactClass({ 155 263 }); 264 265 var ANDQueryArgs = createReactClass({ 266 propTypes: { 267 query: PT.string, 268 onQueryChange: PT.func.isRequired, 269 }, 270 156 271 getInitialState: function() { 157 return {158 andCounter: 1,159 ands: ["and1"]160 };161 },162 272 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 } 166 313 }, 167 314 168 315 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 }); 173 320 }, 174 321 175 322 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 }, 185 364 186 365 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"}}/> 203 373 </div>); 204 374 }, 205 375 206 376 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 ); 213 391 return (<div> 214 <TransitionGroup>215 {andQueryArgs}216 </TransitionGroup>217 {this.renderANDTokenFooter()}392 <TransitionGroup> 393 {andQueryArgs} 394 </TransitionGroup> 395 {this.renderANDTokenFooter()} 218 396 </div>); 219 397 } … … 222 400 var ANDQueryORArgs = createReactClass({ 223 401 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 228 407 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 } 247 430 }, 248 431 249 432 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); 252 441 }, 253 442 254 443 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>); 298 493 } 299 494 }); 300 495 301 496 var ORArg = createReactClass({ 497 mixins: [PureRenderMixin], 498 302 499 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 } 308 597 }, 309 598 310 599 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 ); 347 640 } 348 641 }); 349 642 643 function getLayers() { 644 const layers_arr = []; 645 for (var l in layers) { 646 layers_arr.push(l); 647 } 648 return layers_arr; 649 } 650 651 function getLayerArgOpts(layer) { 652 return layers[layer].argOpts; 653 } 654 655 function isValidLayerOperator(layer, operator) { 656 return !!layers[layer].argOpts.find((e) => e.value===operator); 657 } 658 function isValidLayerValue(layer, operator, value) { 659 var valopts = getLayerValueOptions(layer); 660 if (!valopts) { 661 return true 662 } 663 return valopts.indexOf(value) !== -1; 664 } 665 666 function 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 683 function getLayerLabel(layer) { 684 return layers[layer].label; 685 } 686 687 function getOperatorLabel(layer, operator) { 688 return layers[layer].argOpts[operator].label; 689 } 690 691 function 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 706 const quotedStringRE = /(?:"(?:\\"|[^"])*")/.source; 707 708 // in: 'word = "zebra" ' 709 // out: ['word', '=', 'zebra'] 710 function 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" ...)'] 735 function queryToORArgs(q) { 736 if (!q) return null; 737 var match = q.trim().match(queryToORArgs.re); 738 return match; 739 } 740 queryToORArgs.re = RegExp('(?:'+quotedStringRE+'|[^()|])+', 'g') 741 742 // in: '[word = "zebra" & (word = "zoo" ...)]' 743 // out: ['word = "zebra" ', ' (word = "zoo" ...)'] 744 function queryToANDArgs(q) { 745 if (!q) return null 746 747 var match = q.trim().match(queryToANDArgs.re); 748 return match; 749 } 750 queryToANDArgs.re = RegExp('(?:'+quotedStringRE+'|[^&])+', 'g') 751 752 753 // in: '[word = "zebra"] [word = "zoo"]' 754 // out: ['[word = "zebra"]', '[word = "zoo"]'] 755 function queryToTokens(q) { 756 if (!q) return null; 757 var match = q.match(queryToTokens.re); 758 return match; 759 } 760 queryToTokens.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. */ 764 function 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 776 var 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 ] 785 var liteOptions = [ 786 {value: "is", label: "is"}, 787 {value: "is_not", label: "is not"}, 788 ] 789 var setOptions = [ 790 {value: "is", label: "is"}, 791 {value: "is_not", label: "is not"}, 792 ] 793 var 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 800 setOptions.defaultToStr = (layer, op, val) => { 801 switch(op) { 802 case 'is': 803 return `${layer} = "${val}"` 804 case 'is_not': 805 return `${layer} != "${val}"` 806 } 807 } 808 setOptions.defaultFromString = (layer, op, val) => { 809 return {layer, op: op==='!='?'is_not':'is', val} 810 } 811 812 wordOptions.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 } 832 wordOptions.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 } 868 function guessIfRegexp(s) { 869 return !! s.match(/[^\\][-[\]{}()*+\\?.,^$|#]/); // find if it contains any unescaped regex characters 870 } 871 function unescapeRegExp(text) { 872 return text.replace(/\\([-[\]{}()*+?.,\\^$|#])/g, '$1'); 873 } 874 function escapeRegExp(text) { 875 return text.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&'); 876 } 877 878 const 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 932 const 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 350 939 module.exports = QueryInput;
Note: See TracChangeset
for help on using the changeset viewer.