Compare commits
439 Commits
master-mai
...
2.x
Author | SHA1 | Date |
---|---|---|
Rosie Le Faive | b8f0b9c966 | 6 months ago |
Akanksha Singh | da47bcfb08 | 6 months ago |
Rosie Le Faive | 5e958a5e10 | 6 months ago |
Rosie Le Faive | 3902cce0ac | 6 months ago |
Aron Novak | c80769580c | 7 months ago |
Adam | 54206de712 | 7 months ago |
Joe Corall | 9b2661696d | 7 months ago |
Alexander O'Neill | 089a3654ba | 7 months ago |
Rosie Le Faive | 263666f5fc | 7 months ago |
Rosie Le Faive | 95c2d6c0c9 | 7 months ago |
Rosie Le Faive | cde2c133e1 | 7 months ago |
Rosie Le Faive | a2c31fcaad | 7 months ago |
Rosie Le Faive | 9ed3637339 | 7 months ago |
Rosie Le Faive | 13bc15ea43 | 8 months ago |
Rosie Le Faive | 9f2277fc51 | 8 months ago |
Rosie Le Faive | 89261c17ae | 8 months ago |
Rosie Le Faive | 3784def287 | 8 months ago |
Rosie Le Faive | e30cdbf681 | 8 months ago |
Rosie Le Faive | 3065c87874 | 11 months ago |
Alan Stanley | d0e0c29921 | 11 months ago |
Alan Stanley | 28174c3ce4 | 11 months ago |
ajstanley | 4404dff246 | 11 months ago |
ajstanley | c93c1ff940 | 11 months ago |
ajstanley | b65881625a | 11 months ago |
ajstanley | c149781da0 | 11 months ago |
Seth Shaw | c6341649ca | 12 months ago |
Annie Oelschlager | 4630439760 | 12 months ago |
Rosie Le Faive | 056695c79c | 1 year ago |
Rosie Le Faive | 4b2b9b221b | 1 year ago |
Joe Corall | f29fef2bac | 1 year ago |
Adam | 095e0ecf67 | 1 year ago |
Rosie Le Faive | d5556f445d | 1 year ago |
Rosie Le Faive | 2c91dc6f58 | 1 year ago |
Rosie Le Faive | 16617a9dd7 | 1 year ago |
Rosie Le Faive | c05236ac8c | 1 year ago |
Rosie Le Faive | 6cfaca36e7 | 1 year ago |
Rosie Le Faive | f077af677b | 1 year ago |
Adam | 572ffcf2e1 | 1 year ago |
Alexander O'Neill | f7a77820d3 | 1 year ago |
Annie Oelschlager | 58d1b37f11 | 1 year ago |
Rosie Le Faive | 76eb4717a2 | 1 year ago |
Annie Oelschlager | e2ec673017 | 1 year ago |
Rosie Le Faive | d6e07491d2 | 1 year ago |
Rosie Le Faive | c2cd14cfd5 | 1 year ago |
Rosie Le Faive | fdfdd87472 | 1 year ago |
Rosie Le Faive | 84c6ca85d8 | 1 year ago |
Rosie Le Faive | 91253bef14 | 1 year ago |
dannylamb | e3399d3968 | 1 year ago |
aOelschlager | fd8319b7b2 | 1 year ago |
Rosie Le Faive | 5d83504778 | 1 year ago |
aOelschlager | 33340c2722 | 1 year ago |
Rosie Le Faive | d1357d347d | 1 year ago |
Rosie Le Faive | 0408edb93f | 1 year ago |
Alexander O'Neill | 71f0945e3c | 1 year ago |
Alexander O'Neill | 11afd42c8a | 1 year ago |
Rosie Le Faive | 4eef5f566d | 1 year ago |
Rosie Le Faive | 5331b0b7d5 | 1 year ago |
Alexander O'Neill | 8f1537670d | 1 year ago |
Rosie Le Faive | 0fe2a8f559 | 1 year ago |
Alexander O'Neill | ac818a0f27 | 1 year ago |
Rosie Le Faive | 408776437b | 1 year ago |
Rosie Le Faive | 6b05ff5f99 | 1 year ago |
Rosie Le Faive | 5c09a1e3f4 | 1 year ago |
Rosie Le Faive | 6d59c526d3 | 1 year ago |
Rosie Le Faive | 91016fd237 | 1 year ago |
Rosie Le Faive | 9ef509b0ad | 1 year ago |
Rosie Le Faive | 621b7a2c7d | 1 year ago |
Rosie Le Faive | aec8178846 | 1 year ago |
Rosie Le Faive | 8adc44859c | 1 year ago |
Rosie Le Faive | d1861de270 | 1 year ago |
Rosie Le Faive | d293d7702a | 1 year ago |
Rosie Le Faive | a88486ca28 | 1 year ago |
Rosie Le Faive | 8ef277527b | 1 year ago |
Rosie Le Faive | e67e8e5f25 | 1 year ago |
Rosie Le Faive | 52947f3f96 | 1 year ago |
Rosie Le Faive | e4dc48fca2 | 1 year ago |
Rosie Le Faive | 7470327871 | 1 year ago |
Rosie Le Faive | 8f8e6a3c35 | 1 year ago |
Jordan Dukart | 9cabfc2e23 | 1 year ago |
Rosie Le Faive | ffd128db80 | 1 year ago |
Rosie Le Faive | 2c332348dc | 1 year ago |
Rosie Le Faive | 7d7f97746a | 1 year ago |
Seth Shaw | 91490ddbe2 | 1 year ago |
Alexander O'Neill | e492b92d9f | 1 year ago |
Alexander O'Neill | d4cac72993 | 1 year ago |
Alexander O'Neill | 9f5eceea07 | 1 year ago |
Alexander O'Neill | cf243f368d | 1 year ago |
Alexander O'Neill | f41dc59f1b | 1 year ago |
Alexander O'Neill | 7527b1fa6f | 1 year ago |
Alexander O'Neill | 723f102365 | 1 year ago |
Alexander O'Neill | 9ef3bcf440 | 1 year ago |
Alexander O'Neill | 622eaab6a0 | 1 year ago |
Rosie Le Faive | 374ab02d07 | 1 year ago |
Alexander O'Neill | a7eaacc1d5 | 1 year ago |
Alexander O'Neill | 61c6e737c1 | 1 year ago |
Alexander O'Neill | 17b5049578 | 1 year ago |
Alexander O'Neill | 5bc1584dd7 | 1 year ago |
Alexander O'Neill | 43f32d1bcf | 1 year ago |
Alexander O'Neill | 78baec07e8 | 1 year ago |
kstapelfeldt | 0bd05b6c44 | 1 year ago |
Alexander O'Neill | 06dd1651ac | 1 year ago |
Alexander O'Neill | 138eab2016 | 1 year ago |
Jared Whiklo | 7b0ff739cd | 1 year ago |
Jared Whiklo | ff4e0cafc4 | 1 year ago |
Jared Whiklo | ba93ad35a3 | 1 year ago |
Jared Whiklo | 860abf3c06 | 1 year ago |
Jared Whiklo | 5dd96b8f22 | 1 year ago |
Jared Whiklo | 2c1d88f400 | 1 year ago |
Jared Whiklo | 8ce1ad2cda | 1 year ago |
Noah W. Smith | 58da2a6af1 | 1 year ago |
Noah W. Smith | 7d54a42d48 | 1 year ago |
Lucas van Schaik | ee451667d4 | 1 year ago |
Lucas van Schaik | 4bcc7d4417 | 1 year ago |
Lucas van Schaik | b82accf763 | 1 year ago |
Lucas van Schaik | 1bbb48f70f | 1 year ago |
Lucas van Schaik | 088f1fcdd0 | 1 year ago |
Lucas van Schaik | 50685aebe6 | 1 year ago |
Lucas van Schaik | 2c48c8795f | 1 year ago |
Lucas van Schaik | 9f83322902 | 1 year ago |
Lucas van Schaik | 709938cf29 | 1 year ago |
Rosie Le Faive | c67f3185ec | 1 year ago |
Jordan Dukart | 46cd2f9950 | 1 year ago |
Seth Shaw | 4e091e524f | 1 year ago |
Ant Brown | ee2b964a07 | 1 year ago |
JojoVes | 2376f77831 | 1 year ago |
Rosie Le Faive | 8686dbf74b | 1 year ago |
Rosie Le Faive | a77bd2d949 | 1 year ago |
Rosie Le Faive | cb2e1c4809 | 1 year ago |
Rosie Le Faive | 2040952740 | 1 year ago |
Rosie Le Faive | 8f77733c84 | 1 year ago |
Rosie Le Faive | 0665310346 | 1 year ago |
Rosie Le Faive | bf17ed9bbc | 1 year ago |
Rosie Le Faive | 354341988b | 1 year ago |
Jordan Dukart | 8502a347ff | 1 year ago |
Rosie Le Faive | f474f7b745 | 1 year ago |
Jordan Dukart | ece94a24f5 | 1 year ago |
Rosie Le Faive | 58ab9a3b70 | 1 year ago |
Rosie Le Faive | 0d7f5d927f | 1 year ago |
Rosie Le Faive | 05fc3f9b88 | 1 year ago |
Rosie Le Faive | 4eae636383 | 1 year ago |
Rosie Le Faive | dd514a3eb0 | 1 year ago |
Rosie Le Faive | c49c131ed8 | 1 year ago |
Rosie Le Faive | 760593b4e0 | 1 year ago |
Rosie Le Faive | 54116efbab | 1 year ago |
Rosie Le Faive | 41e4dc6fff | 1 year ago |
Rosie Le Faive | 8ee4fb5aff | 1 year ago |
Rosie Le Faive | 1f09439e1e | 1 year ago |
Jordan Dukart | d1ac274543 | 1 year ago |
Rosie Le Faive | b3f2c006b1 | 1 year ago |
Alexander O'Neill | c41f574268 | 1 year ago |
Seth Shaw | cc5b5f838d | 1 year ago |
Alexander O'Neill | da3311825c | 1 year ago |
Alexander O'Neill | 6fe405ee93 | 1 year ago |
Alexander O'Neill | 30296b4566 | 1 year ago |
Alexander O'Neill | 97f3b2daf1 | 1 year ago |
Alexander O'Neill | 4ca6a0c88a | 1 year ago |
Alexander O'Neill | e1fde43e21 | 1 year ago |
Alexander O'Neill | cf7b09f097 | 1 year ago |
Alexander O'Neill | 2307dc6936 | 1 year ago |
Alexander O'Neill | 8f5154c24e | 1 year ago |
Rosie Le Faive | aa4d10649b | 1 year ago |
Willow Gillingham | b0057d1895 | 1 year ago |
Rosie Le Faive | 879dc2091d | 1 year ago |
Willow Gillingham | 7a57d2dfc8 | 1 year ago |
Alexander O'Neill | c1c0f21cb5 | 1 year ago |
Alexander O'Neill | 2e1df20b0c | 1 year ago |
Alexander O'Neill | 1bdb7323e3 | 1 year ago |
Rosie Le Faive | 06f2a5754e | 1 year ago |
Willow Gillingham | 48b73c562d | 2 years ago |
kstapelfeldt | c80e687168 | 2 years ago |
Willow Gillingham | 66401baec9 | 2 years ago |
Alexander O'Neill | e4fbbb375a | 2 years ago |
Alexander O'Neill | e8712d85f7 | 2 years ago |
Jared Whiklo | 3ef2f1038e | 2 years ago |
Jared Whiklo | 8370383e83 | 2 years ago |
Jared Whiklo | a02738bd3f | 2 years ago |
Jared Whiklo | 492338c653 | 2 years ago |
Jared Whiklo | 97c3ddbdd1 | 2 years ago |
Jared Whiklo | a4b9f7fc4e | 2 years ago |
Jared Whiklo | 7e09750dee | 2 years ago |
Alexander O'Neill | 994545798b | 2 years ago |
Alexander O'Neill | 8286dfe423 | 2 years ago |
Alexander O'Neill | 8bc98e062f | 2 years ago |
Don Richards | e5b223a7a1 | 2 years ago |
Noah W. Smith | 718af168f4 | 2 years ago |
Noah W. Smith | 539952e89c | 2 years ago |
Jordan Dukart | e366da3257 | 2 years ago |
Lucas van Schaik | d041ec3bf5 | 2 years ago |
Lucas van Schaik | 233a65d871 | 2 years ago |
Lucas van Schaik | ee425d2c1f | 2 years ago |
Rosie Le Faive | bb06d8143c | 2 years ago |
Jordan Dukart | c721f9ba07 | 2 years ago |
Jordan Dukart | db85922765 | 2 years ago |
Lucas van Schaik | b89da473f1 | 2 years ago |
Lucas van Schaik | aba5052308 | 2 years ago |
Lucas van Schaik | a409d402aa | 2 years ago |
Lucas van Schaik | 4250109c63 | 2 years ago |
Lucas van Schaik | 87f475d81c | 2 years ago |
Lucas van Schaik | 74755f8074 | 2 years ago |
Seth Shaw | b57f8ff64d | 2 years ago |
Ant Brown | 2794f01164 | 2 years ago |
Simon Hieu Mai | 488a82b741 | 2 years ago |
Simon Hieu Mai | 71c720736f | 2 years ago |
JojoVes | c36f7d9978 | 2 years ago |
Simon Hieu Mai | da35fb8950 | 2 years ago |
Simon Hieu Mai | af224e42cf | 2 years ago |
Simon Hieu Mai | 0d2e584316 | 2 years ago |
Adam | fe7e450a51 | 2 years ago |
Alexander O'Neill | 4f4e661e38 | 2 years ago |
Adam | 6f2955b061 | 2 years ago |
Rosie Le Faive | cefee615c0 | 2 years ago |
Jordan Dukart | 4ec340744c | 2 years ago |
Rosie Le Faive | 12e28f1284 | 2 years ago |
Rosie Le Faive | b326d967a6 | 2 years ago |
Nigel Banks | dfa095951e | 2 years ago |
Nigel Banks | f780c69556 | 2 years ago |
Nigel Banks | db31d1438d | 2 years ago |
Nigel Banks | f63dce64ce | 2 years ago |
Rosie Le Faive | 7df45a083a | 2 years ago |
Rosie Le Faive | 665abfbd6c | 2 years ago |
Rosie Le Faive | 41f8710122 | 2 years ago |
Jordan Dukart | 5472f6d7e1 | 2 years ago |
dannylamb | f86f2bedb1 | 2 years ago |
Jordan Dukart | 33965b4ca6 | 2 years ago |
Rosie Le Faive | 0b7f12d3ba | 2 years ago |
Rosie Le Faive | b47d37b1b6 | 2 years ago |
Jordan Dukart | 023b24b5d3 | 2 years ago |
shriram1056 | ee85472dc8 | 2 years ago |
Rosie Le Faive | 6c582a8702 | 2 years ago |
Jason Hildebrand | f71f6dc2e8 | 2 years ago |
Jason Hildebrand | 5f4a6ab3ae | 2 years ago |
Rosie Le Faive | def4fda5b6 | 2 years ago |
Rosie Le Faive | 541620493b | 2 years ago |
shriram1056 | e15b6322ff | 2 years ago |
shriram1056 | 48b5333b2d | 2 years ago |
Rosie Le Faive | 74dcfd0fa4 | 2 years ago |
Rosie Le Faive | 72eaaf659a | 2 years ago |
Rosie Le Faive | 4bed36dede | 2 years ago |
Rosie Le Faive | b0c43accb8 | 2 years ago |
shriram | 5c24c19018 | 2 years ago |
shriram | 9b58fc9ecb | 2 years ago |
shriram | ef1f36f283 | 2 years ago |
shriram | 7ef1afffa2 | 2 years ago |
Rosie Le Faive | 386ba0ceb1 | 2 years ago |
Rosie Le Faive | 7eebb65c2b | 2 years ago |
Rosie Le Faive | e3c7e6edda | 2 years ago |
Adam | 3f7ca2ca10 | 2 years ago |
shriram | fd5c38a107 | 2 years ago |
shriram | 5bd2cdd851 | 2 years ago |
shriram | 3602bb441b | 2 years ago |
shriram | 33ce9e4e13 | 2 years ago |
Willow Gillingham | bdbef45baa | 2 years ago |
Alexander O'Neill | 2e4780163e | 2 years ago |
shriram | aa3c71893e | 2 years ago |
Jared Whiklo | 0948436395 | 2 years ago |
Mark Jordan | ca1d9f6f60 | 2 years ago |
Adam | a250c2ac78 | 2 years ago |
Islandora Foundation Community | 0e8c05cc7b | 2 years ago |
Alexander O'Neill | c07d1f6540 | 2 years ago |
Alexander O'Neill | a41ecaa754 | 2 years ago |
Alexander O'Neill | 78cee0a35a | 2 years ago |
Alexander O'Neill | bf25e2447a | 2 years ago |
Alexander O'Neill | 5e1d53d377 | 2 years ago |
Alexander O'Neill | 49c48a1493 | 2 years ago |
Alexander O'Neill | 4179f5cee7 | 2 years ago |
Alexander O'Neill | 0644795c54 | 2 years ago |
Alexander O'Neill | bd17a381ea | 2 years ago |
Alexander O'Neill | 0bea8da572 | 2 years ago |
Adam | 725b559280 | 2 years ago |
Rosie Le Faive | 3048594a8b | 2 years ago |
Alan Stanley | 62fbc6d288 | 2 years ago |
Seth Shaw | d405a2f14f | 2 years ago |
Rosie Le Faive | 7bca3d5675 | 2 years ago |
Seth Shaw | 3c194cc7b7 | 2 years ago |
dannylamb | a297796f47 | 2 years ago |
Rosie Le Faive | eb53ff474e | 2 years ago |
Rosie Le Faive | 07e3c49ecc | 2 years ago |
Rosie Le Faive | 87231dc5c0 | 2 years ago |
Rosie Le Faive | 705f623fdb | 2 years ago |
Rosie Le Faive | 1415bd509b | 2 years ago |
Rosie Le Faive | cebeeaec5c | 2 years ago |
Rosie Le Faive | 724d0845f4 | 2 years ago |
Rosie Le Faive | dd58302b98 | 2 years ago |
Jordan Dukart | 573d6878ed | 2 years ago |
Rosie Le Faive | 704405e3da | 2 years ago |
Rosie Le Faive | 2d8df5a226 | 2 years ago |
Rosie Le Faive | cdb83ece92 | 2 years ago |
Rosie Le Faive | f4e91b20a3 | 2 years ago |
Rosie Le Faive | 551a6673bf | 2 years ago |
Rosie Le Faive | 5644a68a06 | 2 years ago |
Rosie Le Faive | 98c9ba4c63 | 2 years ago |
Rosie Le Faive | 85cf0822f5 | 2 years ago |
Rosie Le Faive | 4d565164d7 | 2 years ago |
Rosie Le Faive | 887cd8791e | 2 years ago |
Rosie Le Faive | 19db152531 | 2 years ago |
Alan Stanley | 72c7dff3e8 | 2 years ago |
Seth Shaw | 39c7b3180a | 2 years ago |
Seth Shaw | 352631099e | 2 years ago |
Seth Shaw | f6a66fe082 | 2 years ago |
Seth Shaw | 472f487b35 | 2 years ago |
Seth Shaw | a90630d976 | 2 years ago |
Seth Shaw | cc958f4164 | 2 years ago |
Seth Shaw | 62211ff909 | 2 years ago |
Alexander O'Neill | 491631c4db | 2 years ago |
Jordan Dukart | 019572a778 | 2 years ago |
Adam | 3d122af5d6 | 2 years ago |
Adam Vessey | 61f9ec9106 | 2 years ago |
Adam Vessey | 63a77bd834 | 2 years ago |
Rosie Le Faive | 222c9601c1 | 2 years ago |
Jordan Dukart | ba74759f03 | 3 years ago |
Jordan Dukart | 93c19b6c6e | 3 years ago |
Seth Shaw | b38f195a50 | 3 years ago |
Alexander O'Neill | e5a1f99c57 | 3 years ago |
Rosie Le Faive | 032280827f | 3 years ago |
Jordan Dukart | 1a13b3e713 | 3 years ago |
Seth Shaw | e1428bb13a | 3 years ago |
Seth Shaw | 73d0d66402 | 3 years ago |
Seth Shaw | 9c283ea0c0 | 3 years ago |
Seth Shaw | ed0979f97c | 3 years ago |
Alexander O'Neill | f6fa77984b | 3 years ago |
Alexander O'Neill | 11bc7886ea | 3 years ago |
Alexander O'Neill | 52d3df1462 | 3 years ago |
Alexander O'Neill | 92d5a7fbbd | 3 years ago |
Jared Whiklo | d8d101e571 | 3 years ago |
Alexander O'Neill | 71b1cb5d64 | 3 years ago |
Alexander O'Neill | e9f9aad49c | 3 years ago |
Simon Hieu Mai | a04a72c483 | 3 years ago |
Jordan Dukart | e0152eaa8c | 3 years ago |
Jordan Dukart | a7e4c1659e | 3 years ago |
Ant Brown | bd98028f00 | 3 years ago |
Jordan Dukart | 4c439d4817 | 3 years ago |
Jordan Dukart | 6d752e479e | 3 years ago |
Alexander O'Neill | 4c08d5a274 | 3 years ago |
Alexander O'Neill | ac749ce3b5 | 3 years ago |
Alexander O'Neill | f7287be012 | 3 years ago |
Alexander O'Neill | 9c8193b75a | 3 years ago |
Islandora Foundation Community | 081183bc71 | 3 years ago |
Alexander O'Neill | 20f7ebb332 | 3 years ago |
Alexander O'Neill | 1a61b17875 | 3 years ago |
Alexander O'Neill | 2199336446 | 3 years ago |
Alexander O'Neill | 7709425358 | 3 years ago |
Jared Whiklo | c1aa0a5f2f | 3 years ago |
Seth Shaw | 90d6795172 | 3 years ago |
Alan Stanley | 4f45cb8c06 | 3 years ago |
ajstanley | b733713610 | 3 years ago |
Jordan Dukart | adbfea79a4 | 3 years ago |
Seth Shaw | 01f22b717f | 3 years ago |
Rosie Le Faive | e9448b0b00 | 3 years ago |
Rosie Le Faive | 4b9493210e | 3 years ago |
Jared Whiklo | 2923a1a8b9 | 3 years ago |
Adam | 05c0d1cc58 | 3 years ago |
Adam | d800748653 | 3 years ago |
Seth Shaw | 588760b57d | 3 years ago |
Rosie Le Faive | 9ada5d678f | 3 years ago |
Rosie Le Faive | aa94c9afa5 | 3 years ago |
dannylamb | 714d9b632f | 3 years ago |
dannylamb | b53ba8e62c | 3 years ago |
Nigel Banks | 0c0dd67334 | 3 years ago |
Willow Gillingham | 4a20d4b5e4 | 3 years ago |
Alan Stanley | d291c628bd | 3 years ago |
dannylamb | 75215caabb | 3 years ago |
Willow Gillingham | 52033e8788 | 3 years ago |
dannylamb | 85dae5c9d8 | 3 years ago |
dannylamb | daebe15c59 | 3 years ago |
dannylamb | 77bf39b2bc | 3 years ago |
dannylamb | 4918284296 | 3 years ago |
Willow Gillingham | 8a28ea28e2 | 3 years ago |
Alexander O'Neill | 492be9fdef | 3 years ago |
Alexander O'Neill | cf82e8f392 | 4 years ago |
dannylamb | 5127e7f3ab | 4 years ago |
dannylamb | 07178e9f6f | 4 years ago |
Eli Zoller | 5bc9d04c0d | 4 years ago |
Seth Shaw | e57c82b888 | 4 years ago |
Seth Shaw | 56444554ef | 4 years ago |
Eli Zoller | e5310bfef6 | 4 years ago |
Alan Stanley | 6d2ad0ecf4 | 4 years ago |
Nigel Banks | 5b969790db | 4 years ago |
Nigel Banks | 84de729285 | 4 years ago |
Willow Gillingham | d76a6644c9 | 4 years ago |
Mark Jordan | 47213e2fd0 | 4 years ago |
Seth Shaw | 4a0a47c802 | 4 years ago |
Seth Shaw | a8f8e40371 | 4 years ago |
Seth Shaw | a52617e99d | 4 years ago |
Eli Zoller | bdf99377e1 | 4 years ago |
Seth Shaw | f0d0d909a4 | 4 years ago |
Seth Shaw | 781d0c4e3e | 4 years ago |
Seth Shaw | 2bed2bceb4 | 4 years ago |
dannylamb | 1b6bea7902 | 4 years ago |
Jordan Dukart | cb52ddd902 | 4 years ago |
elizoller | 71cbda2a9d | 4 years ago |
elizoller | 301e1a0cc2 | 4 years ago |
elizoller | a36629890b | 4 years ago |
Nigel Banks | cfa48a9db7 | 4 years ago |
Eli Zoller | 986a3d4ae9 | 4 years ago |
Nigel Banks | ae2aeccfbb | 4 years ago |
Eli Zoller | 5e3233f914 | 4 years ago |
Jared Whiklo | 76fea69d34 | 4 years ago |
Eli Zoller | 7f0d54e1eb | 4 years ago |
Alan Stanley | 792b3d3ae2 | 4 years ago |
Daniel Aitken | e57b9e709a | 4 years ago |
dannylamb | 9ff811a3b1 | 4 years ago |
dannylamb | a1958113eb | 4 years ago |
Mark Jordan | 2a2fbe7f3f | 4 years ago |
Noah W. Smith | 17711f4c24 | 4 years ago |
Seth Shaw | 1b3338e47e | 4 years ago |
Seth Shaw | 69326432ec | 4 years ago |
Daniel Aitken | 6d11796be8 | 4 years ago |
Seth Shaw | 4b1103e05f | 4 years ago |
Seth Shaw | 7de7a08ac1 | 4 years ago |
Nigel Banks | 836d521273 | 4 years ago |
dannylamb | 4f2c58e4ad | 4 years ago |
Jared Whiklo | 174cd0f0c9 | 4 years ago |
dannylamb | d7fb47add4 | 4 years ago |
Rosie Le Faive | ca73b271fd | 4 years ago |
Melissa Anez | 7095680382 | 4 years ago |
Alan Stanley | a3d7a55bdf | 4 years ago |
Alan Stanley | ad05b37d08 | 4 years ago |
Jordan Dukart | 1be998fd20 | 4 years ago |
Seth Shaw | 4bb18d8855 | 4 years ago |
Seth Shaw | 54ff6e0566 | 4 years ago |
Jordan Dukart | 047d62a53f | 4 years ago |
Rosie Le Faive | 26f8cfb7ef | 4 years ago |
Rosie Le Faive | fca4e244aa | 4 years ago |
Rosie Le Faive | 5ade4bf5dc | 4 years ago |
Rosie Le Faive | 925f1520c9 | 4 years ago |
Rosie Le Faive | a37eeea138 | 4 years ago |
Seth Shaw | c4c602a9c9 | 4 years ago |
Seth Shaw | 3d2eb69736 | 4 years ago |
Mark Jordan | 173483ef52 | 4 years ago |
elizoller | df6e70e69e | 4 years ago |
elizoller | dea2d6dda0 | 4 years ago |
elizoller | 81c1ccc09c | 4 years ago |
elizoller | 316917a12c | 4 years ago |
elizoller | d1d15fe993 | 4 years ago |
elizoller | 219925831d | 4 years ago |
elizoller | ea1383e499 | 4 years ago |
Eli Zoller | 9a16c4373b | 4 years ago |
Eli Zoller | c1d719bc02 | 4 years ago |
Eli Zoller | 9b6a701a4a | 5 years ago |
@ -0,0 +1,125 @@
|
||||
name: CI |
||||
|
||||
on: |
||||
push: |
||||
branches: [ 2.x ] |
||||
pull_request: |
||||
branches: [ 2.x ] |
||||
workflow_dispatch: |
||||
|
||||
jobs: |
||||
build: |
||||
env: |
||||
DRUPAL_VERSION: ${{ matrix.drupal-version }} |
||||
SCRIPT_DIR: ${{ github.workspace }}/islandora_ci |
||||
DRUPAL_DIR: /opt/drupal |
||||
PHPUNIT_FILE: ${{ github.workspace }}/build_dir/phpunit.xml |
||||
|
||||
runs-on: ubuntu-latest |
||||
continue-on-error: ${{ matrix.allowed_failure }} |
||||
strategy: |
||||
fail-fast: false |
||||
matrix: |
||||
php-versions: ["8.1", "8.2", "8.3"] |
||||
test-suite: ["kernel", "functional", "functional-javascript"] |
||||
drupal-version: ["10.1.x", "10.2.x", "10.3.x-dev"] |
||||
mysql: ["8.0"] |
||||
allowed_failure: [false] |
||||
exclude: |
||||
- php-versions: "8.3" |
||||
drupal-version: "10.1.x" |
||||
|
||||
|
||||
name: PHP ${{ matrix.php-versions }} | drupal ${{ matrix.drupal-version }} | mysql ${{ matrix.mysql }} | test-suite ${{ matrix.test-suite }} |
||||
|
||||
services: |
||||
mysql: |
||||
image: mysql:${{ matrix.mysql }} |
||||
env: |
||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes |
||||
MYSQL_DATABASE: drupal |
||||
ports: |
||||
- 3306:3306 |
||||
activemq: |
||||
image: webcenter/activemq:5.14.3 |
||||
ports: |
||||
- 8161:8161 |
||||
- 61616:61616 |
||||
- 61613:61613 |
||||
|
||||
steps: |
||||
|
||||
- name: Checkout code |
||||
uses: actions/checkout@v4 |
||||
with: |
||||
path: build_dir |
||||
|
||||
- name: Checkout islandora_ci |
||||
uses: actions/checkout@v4 |
||||
with: |
||||
repository: islandora/islandora_ci |
||||
ref: github-actions |
||||
path: islandora_ci |
||||
|
||||
- name: Setup PHP |
||||
uses: shivammathur/setup-php@v2 |
||||
with: |
||||
php-version: ${{ matrix.php-versions }} |
||||
tools: composer:v2 |
||||
|
||||
- name: Setup Mysql client |
||||
run: | |
||||
sudo apt-get update |
||||
sudo apt-get remove -y mysql-client mysql-common |
||||
sudo apt-get install -y mysql-client |
||||
|
||||
- name: Cache Composer dependencies |
||||
uses: actions/cache@v3 |
||||
with: |
||||
path: /tmp/composer-cache |
||||
key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }} |
||||
|
||||
- name: Setup Drupal |
||||
run: | |
||||
mkdir $DRUPAL_DIR |
||||
$SCRIPT_DIR/travis_setup_drupal.sh |
||||
cd $DRUPAL_DIR |
||||
chmod -R u+w web/sites/default |
||||
mkdir -p web/sites/simpletest/browser_output |
||||
|
||||
- name: Setup composer paths |
||||
run: | |
||||
git -C "$GITHUB_WORKSPACE/build_dir" checkout -b github-testing |
||||
cd $DRUPAL_DIR |
||||
composer config repositories.local path "$GITHUB_WORKSPACE/build_dir" |
||||
composer config minimum-stability dev |
||||
composer require "islandora/islandora:dev-github-testing as dev-2.x" |
||||
|
||||
- name: Install modules |
||||
run: | |
||||
cd $DRUPAL_DIR/web |
||||
drush --uri=127.0.0.1:8282 en -y islandora_audio islandora_breadcrumbs islandora_iiif islandora_image islandora_video islandora_text_extraction_defaults |
||||
|
||||
- name: Copy PHPunit file |
||||
run: cp $PHPUNIT_FILE $DRUPAL_DIR/web/core/phpunit.xml |
||||
|
||||
- name: Test scripts |
||||
run: $SCRIPT_DIR/travis_scripts.sh |
||||
|
||||
- name: Start chromedriver |
||||
if: matrix.test-suite == 'functional-javascript' |
||||
run: |- |
||||
/usr/local/share/chromedriver-linux64/chromedriver \ |
||||
--log-path=/tmp/chromedriver.log \ |
||||
--verbose \ |
||||
--allowed-ips= \ |
||||
--allowed-origins=* & |
||||
|
||||
- name: PHPUNIT tests |
||||
run: | |
||||
cd $DRUPAL_DIR/web/core |
||||
$DRUPAL_DIR/vendor/bin/phpunit --verbose --testsuite "${{ matrix.test-suite }}" |
||||
|
||||
- name: Print chromedriver logs |
||||
if: matrix.test-suite == 'functional-javascript' |
||||
run: cat /tmp/chromedriver.log |
@ -0,0 +1,26 @@
|
||||
name: Mirror and run GitLab CI |
||||
|
||||
on: |
||||
push: |
||||
branches: [2.x] |
||||
tags: '*' |
||||
|
||||
jobs: |
||||
build: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
fetch-depth: 0 |
||||
- name: Mirror + trigger CI |
||||
uses: SvanBoxel/gitlab-mirror-and-ci-action@master |
||||
with: |
||||
args: "https://git.drupalcode.org/project/islandora" |
||||
env: |
||||
FOLLOW_TAGS: "true" |
||||
FORCE_PUSH: "false" |
||||
GITLAB_HOSTNAME: "git.drupal.org" |
||||
GITLAB_USERNAME: "project_34868_bot" |
||||
GITLAB_PASSWORD: ${{ secrets.GITLAB_PASSWORD }} |
||||
GITLAB_PROJECT_ID: "34868" |
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
@ -1,53 +0,0 @@
|
||||
sudo: true |
||||
language: php |
||||
php: |
||||
- 7.2 |
||||
- 7.3 |
||||
|
||||
matrix: |
||||
fast_finish: true |
||||
allow_failures: |
||||
- php: 7.3 |
||||
|
||||
services: |
||||
- mysql |
||||
|
||||
branches: |
||||
only: |
||||
- /^8.x/ |
||||
- /master/ |
||||
|
||||
before_install: |
||||
- export SCRIPT_DIR=$HOME/islandora/.scripts |
||||
- export DRUPAL_DIR=/opt/drupal |
||||
- export COMPOSER_PATH="/home/travis/.phpenv/versions/$TRAVIS_PHP_VERSION/bin/composer" |
||||
|
||||
install: |
||||
- git clone https://github.com/Islandora/documentation.git $HOME/islandora |
||||
- $SCRIPT_DIR/travis_setup_drupal.sh |
||||
- git -C "$TRAVIS_BUILD_DIR" checkout -b travis-testing |
||||
- cd $DRUPAL_DIR; |
||||
- chmod -R u+w web/sites/default |
||||
- COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=-1 $COMPOSER_PATH config repositories.local path "$TRAVIS_BUILD_DIR" |
||||
- COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=-1 $COMPOSER_PATH require "islandora/islandora:dev-travis-testing as dev-8.x-1.x" --prefer-source --update-with-dependencies |
||||
- cd web |
||||
- drush --uri=127.0.0.1:8282 en -y islandora_audio islandora_breadcrumbs islandora_iiif islandora_image islandora_video islandora_text_extraction_defaults |
||||
- drush --uri=127.0.0.1:8282 fim -y islandora_core_feature,islandora_text_extraction_defaults |
||||
|
||||
script: |
||||
- $SCRIPT_DIR/travis_scripts.sh |
||||
- $SCRIPT_DIR/run-tests.sh "islandora" |
||||
- $SCRIPT_DIR/run-tests.sh "islandora_breadcrumbs" |
||||
- $SCRIPT_DIR/run-tests.sh "islandora_image" |
||||
- $SCRIPT_DIR/run-tests.sh "islandora_audio" |
||||
- $SCRIPT_DIR/run-tests.sh "islandora_video" |
||||
- $SCRIPT_DIR/run-tests.sh "islandora_text_extraction" |
||||
|
||||
after_success: |
||||
- bash <(curl -s https://codecov.io/bash) |
||||
|
||||
notifications: |
||||
slack: |
||||
on_success: change |
||||
on_failure: always |
||||
secure: $SLACK_NOTIFICATION_KEY |
@ -1,4 +1,4 @@
|
||||
broker_url: 'tcp://localhost:61613' |
||||
jwt_expiry: '+2 hour' |
||||
gemini_url: '' |
||||
delete_media_and_files: TRUE |
||||
gemini_pseudo_bundles: [] |
||||
|
@ -0,0 +1,3 @@
|
||||
.container .islandora-media-items { |
||||
margin: 0; |
||||
} |
@ -1,5 +1,6 @@
|
||||
services: |
||||
islandora.commands: |
||||
class: \Drupal\islandora\Commands\IslandoraCommands |
||||
arguments: ['@entity_type.manager', '@current_user', '@account_switcher'] |
||||
tags: |
||||
- { name: drush.command } |
||||
|
@ -0,0 +1,5 @@
|
||||
islandora: |
||||
version: VERSION |
||||
css: |
||||
theme: |
||||
css/islandora.css: {} |
@ -0,0 +1,16 @@
|
||||
<?php |
||||
|
||||
/** |
||||
* @file |
||||
* Post updates. |
||||
*/ |
||||
|
||||
/** |
||||
* Set default value for delete_media_and_files field in settings. |
||||
*/ |
||||
function islandora_post_update_delete_media_and_files() { |
||||
$config_factory = \Drupal::configFactory(); |
||||
$config = $config_factory->getEditable('islandora.settings'); |
||||
$config->set('delete_media_and_files', TRUE); |
||||
$config->save(TRUE); |
||||
} |
@ -0,0 +1,192 @@
|
||||
<?php |
||||
|
||||
/** |
||||
* @file |
||||
* Contains islandora.tokens.inc. |
||||
* |
||||
* This file provides islandora tokens. |
||||
*/ |
||||
|
||||
use Drupal\Core\Render\BubbleableMetadata; |
||||
use Drupal\media\Entity\Media; |
||||
use Drupal\file\Entity\File; |
||||
|
||||
/** |
||||
* Implements hook_token_info(). |
||||
*/ |
||||
function islandora_token_info() { |
||||
$type = [ |
||||
'name' => t('Islandora Tokens'), |
||||
'description' => t('Tokens for Islandora objects.'), |
||||
]; |
||||
$node['media-original-file:filename'] = [ |
||||
'name' => t('Media: Original File filename without extension.'), |
||||
'description' => t('File name without extension of original uploaded file associated with Islandora Object via Media.'), |
||||
]; |
||||
$node['media-original-file:basename'] = [ |
||||
'name' => t('Media: Original File filename with extension.'), |
||||
'description' => t('File name with extension of original uploaded file associated with Islandora Object via Media.'), |
||||
]; |
||||
$node['media-original-file:extension'] = [ |
||||
'name' => t('Media: Original File extension.'), |
||||
'description' => t('File extension of original uploaded file associated with Islandora Object via Media.'), |
||||
]; |
||||
$node['media-thumbnail-image:url'] = [ |
||||
'name' => t('Media: Thumbnail Image URL.'), |
||||
'description' => t('URL of Thumbnail Image associated with Islandora Object via Media.'), |
||||
]; |
||||
|
||||
$node['media-thumbnail-image:alt'] = [ |
||||
'name' => t('Alternative text for Media: Thumbnail Image.'), |
||||
'description' => t('Alternative text for Thumbnail Image associated with Islandora Object via Media.'), |
||||
]; |
||||
|
||||
// Deprecated in favour if hyphenated version. |
||||
$node['media_thumbnail_image:url'] = [ |
||||
'name' => t('Media: Thumbnail Image URL.'), |
||||
'description' => t('Deprecated: URL of Thumbnail Image associated with Islandora Object via Media.'), |
||||
]; |
||||
|
||||
// Deprecated in favour if hyphenated version. |
||||
$node['media_thumbnail_image:alt'] = [ |
||||
'name' => t('Alternative text for Media: Thumbnail Image.'), |
||||
'description' => t('Deprecated: Alternative text for Thumbnail Image associated with Islandora Object via Media.'), |
||||
]; |
||||
|
||||
$node['pdf_url'] = [ |
||||
'name' => t("PDF Url"), |
||||
'description' => t('URL to related media file if "Original file" is a PDF file'), |
||||
]; |
||||
|
||||
return [ |
||||
'types' => ['islandoratokens' => $type], |
||||
'tokens' => ['islandoratokens' => $node], |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* Implements hook_tokens(). |
||||
*/ |
||||
function islandora_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { |
||||
$replacements = []; |
||||
if ($type == 'islandoratokens' && !empty($data['node'])) { |
||||
if (!is_array($tokens) || empty($tokens)) { |
||||
\Drupal::logger('islandora') |
||||
->alert( |
||||
'Tokens not correct format: @tokens', [ |
||||
'@tokens' => print_r($tokens, 1), |
||||
] |
||||
); |
||||
return; |
||||
} |
||||
$islandoraUtils = \Drupal::service('islandora.utils'); |
||||
foreach ($tokens as $name => $original) { |
||||
switch ($name) { |
||||
case 'media-original-file:basename': |
||||
case 'media-original-file:filename': |
||||
case 'media-original-file:extension': |
||||
$term = $islandoraUtils->getTermForUri('http://pcdm.org/use#OriginalFile'); |
||||
$media = $islandoraUtils->getMediaWithTerm($data['node'], $term); |
||||
// Is there media? |
||||
if ($media) { |
||||
$file = \Drupal::service('islandora.media_source_service')->getSourceFile($media); |
||||
if (!empty($file)) { |
||||
$path_info = pathinfo($file->createFileUrl()); |
||||
$key = explode(':', $name)[1]; |
||||
if (array_key_exists($key, $path_info)) { |
||||
$replacements[$original] = $path_info[$key]; |
||||
} |
||||
} |
||||
} |
||||
break; |
||||
|
||||
case 'media-thumbnail-image:url': |
||||
case 'media_thumbnail_image:url': |
||||
$term = $islandoraUtils->getTermForUri('http://pcdm.org/use#ThumbnailImage'); |
||||
$media = $islandoraUtils->getMediaWithTerm($data['node'], $term); |
||||
// Is there media? |
||||
// @todo is this single or multiple? |
||||
if ($media) { |
||||
$file = \Drupal::service('islandora.media_source_service')->getSourceFile($media); |
||||
if (!empty($file)) { |
||||
$url = $file->createFileUrl(); |
||||
$replacements[$original] = $url; |
||||
} |
||||
} |
||||
break; |
||||
|
||||
case 'media-thumbnail-image:alt': |
||||
case 'media_thumbnail_image:alt': |
||||
$alt = ''; |
||||
$term = $islandoraUtils->getTermForUri('http://pcdm.org/use#ThumbnailImage'); |
||||
$media = $islandoraUtils->getMediaWithTerm($data['node'], $term); |
||||
// Is there media? |
||||
// @todo is this single or multiple? |
||||
if ($media) { |
||||
// Is the media an image? |
||||
if (isset($media->field_media_image)) { |
||||
$alt = $media->field_media_image[0]->alt; |
||||
} |
||||
} |
||||
// @todo get alt from original or service file, if thumbnail |
||||
// alt is empty. |
||||
$replacements[$original] = $alt; |
||||
break; |
||||
|
||||
case 'pdf_url': |
||||
$replacements[$original] = islandora_url_to_service_file_media_by_mimetype($data['node'], 'application/pdf'); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
return $replacements; |
||||
} |
||||
|
||||
/** |
||||
* Gets Original File PDF file URL. |
||||
* |
||||
* @param object $node |
||||
* A core drupal node object. |
||||
* @param string $mime_type |
||||
* The name of the node's field to check for the specific relationship. |
||||
* |
||||
* @return string |
||||
* The tokenized value for the given data. |
||||
*/ |
||||
function islandora_url_to_service_file_media_by_mimetype($node, $mime_type) { |
||||
$islandora_utils = \Drupal::service('islandora.utils'); |
||||
$origfile_term = $islandora_utils->getTermForUri('http://pcdm.org/use#OriginalFile'); |
||||
$origfile_media = $islandora_utils->getMediaWithTerm($node, $origfile_term); |
||||
// Get the media file's mime_type value. |
||||
if (is_object($origfile_media)) { |
||||
$origfile_mime_type = ($origfile_media->hasField('field_mime_type')) ? |
||||
$origfile_media->get('field_mime_type')->getValue() : NULL; |
||||
$origfile_mime_type = (is_array($origfile_mime_type) && |
||||
array_key_exists(0, $origfile_mime_type) && |
||||
is_array($origfile_mime_type[0]) && |
||||
array_key_exists('value', $origfile_mime_type[0])) ? |
||||
$origfile_mime_type[0]['value'] : ''; |
||||
// Compare the media file's mime_type to the given value. |
||||
if ($origfile_mime_type == $mime_type) { |
||||
$vid = $origfile_media->id(); |
||||
if (!is_null($vid)) { |
||||
$media = Media::load($vid); |
||||
$bundle = $media->bundle(); |
||||
// Since this is Islandora and we assume the Original File is a |
||||
// Document type... but doing it dynamically. |
||||
$fid = $media->get('field_media_' . $bundle)->getValue(); |
||||
$fid_value = (is_array($fid) && array_key_exists(0, $fid) && |
||||
array_key_exists('target_id', $fid[0])) ? |
||||
$fid[0]['target_id'] : NULL; |
||||
if (!is_null($fid_value)) { |
||||
$file = File::load($fid_value); |
||||
if ($file) { |
||||
$url = $islandora_utils->getDownloadUrl($file); |
||||
return $url; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return ''; |
||||
} |
|
@ -0,0 +1,73 @@
|
||||
# Welcome! |
||||
|
||||
If you are reading this document then you are interested in contributing to Islandora 8. All contributions are welcome: use-cases, documentation, code, patches, bug reports, feature requests, etc. You do not need to be a programmer to speak up! |
||||
|
||||
We also have an IRC channel -- #islandora -- on freenode.net. Feel free to hang out there, ask questions, and help others out if you can. |
||||
|
||||
Please note that this project operates under the [Islandora Community Code of Conduct](http://islandora.ca/codeofconduct). By participating in this project you agree to abide by its terms. |
||||
|
||||
## Workflows |
||||
|
||||
The group meets each Wednesday at 1:00 PM Eastern. Meeting notes and announcements are posted to the [Islandora community list](https://groups.google.com/forum/#!forum/islandora) and the [Islandora developers list](https://groups.google.com/forum/#!forum/islandora-dev). You can view meeting agendas, notes, and call-in information [here](https://github.com/Islandora/documentation/wiki#islandora-8-tech-calls). Anybody is welcome to join the calls, and add items to the agenda. |
||||
|
||||
### Use cases |
||||
|
||||
If you would like to submit a use case to the Islandora 8 project, please submit an issue [here](https://github.com/Islandora/documentation/issues/new) using the [Use Case template](https://github.com/Islandora/documentation/wiki/Use-Case-template), prepending "Use Case:" to the title of the issue. |
||||
|
||||
### Documentation |
||||
|
||||
You can contribute documentation in two different ways. One way is to create an issue [here](https://github.com/Islandora/documentation/issues/new), prepending "Documentation:" to the title of the issue. Another way is by pull request, which is the same process as [Contribute Code](https://github.com/Islandora/documentation/blob/main/CONTRIBUTING.md#contribute-code). All documentation resides in [`docs`](https://github.com/Islandora/documentation/tree/main/docs). |
||||
|
||||
### Request a new feature |
||||
|
||||
To request a new feature you should [open an issue in the Islandora 8 repository](https://github.com/Islandora/documentation/issues/new) or create a use case (see the _Use cases_ section above), and summarize the desired functionality. Prepend "Enhancement:" if creating an issue on the project repo, and "Use Case:" if creating a use case. |
||||
|
||||
### Report a bug |
||||
|
||||
To report a bug you should [open an issue in the Islandora 8 repository](https://github.com/Islandora/documentation/issues/new) that summarizes the bug. Prepend the label "Bug:" to the title of the issue. |
||||
|
||||
In order to help us understand and fix the bug it would be great if you could provide us with: |
||||
|
||||
1. The steps to reproduce the bug. This includes information about e.g. the Islandora version you were using along with the versions of stack components. |
||||
2. The expected behavior. |
||||
3. The actual, incorrect behavior. |
||||
|
||||
Feel free to search the issue queue for existing issues (aka tickets) that already describe the problem; if there is such a ticket please add your information as a comment. |
||||
|
||||
**If you want to provide a pull along with your bug report:** |
||||
|
||||
That is great! In this case please send us a pull request as described in the section _Create a pull request_ below. |
||||
|
||||
### Contribute code |
||||
|
||||
Before you set out to contribute code you will need to have completed a [Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_cla.pdf) or be covered by a [Corporate Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_ccla.pdf). The signed copy of the license agreement should be sent to <mailto:community@islandora.ca> |
||||
|
||||
_If you are interested in contributing code to Islandora but do not know where to begin:_ |
||||
|
||||
In this case you should [browse open issues](https://github.com/Islandora/documentation/issues) and check out [use cases](https://github.com/Islandora/documentation/labels/use%20case). |
||||
|
||||
If you are contributing Drupal code, it must adhere to [Drupal Coding Standards](https://www.drupal.org/coding-standards); Travis CI will check for this on pull requests. |
||||
|
||||
Contributions to the Islandora codebase should be sent as GitHub pull requests. See section _Create a pull request_ below for details. If there is any problem with the pull request we can work through it using the commenting features of GitHub. |
||||
|
||||
* For _small patches_, feel free to submit pull requests directly for those patches. |
||||
* For _larger code contributions_, please use the following process. The idea behind this process is to prevent any wasted work and catch design issues early on. |
||||
|
||||
1. [Open an issue](https://github.com/Islandora/documentation/issues), prepending "Enhancement:" in the title if a similar issue does not exist already. If a similar issue does exist, then you may consider participating in the work on the existing issue. |
||||
2. Comment on the issue with your plan for implementing the issue. Explain what pieces of the codebase you are going to touch and how everything is going to fit together. |
||||
3. Islandora committers will work with you on the design to make sure you are on the right track. |
||||
4. Implement your issue, create a pull request (see below), and iterate from there. |
||||
|
||||
### Create a pull request |
||||
|
||||
Take a look at [Creating a pull request](https://help.github.com/articles/creating-a-pull-request). In a nutshell you need to: |
||||
|
||||
1. [Fork](https://help.github.com/articles/fork-a-repo) this repository to your personal or institutional GitHub account (depending on the CLA you are working under). Be cautious of which branches you work from though (you'll want to base your work off the default branch). See [Fork a repo](https://help.github.com/articles/fork-a-repo) for detailed instructions. |
||||
2. Commit any changes to your fork. |
||||
3. Send a [pull request](https://help.github.com/articles/creating-a-pull-request) using the [pull request template](https://github.com/Islandora/documentation/blob/main/.github/PULL_REQUEST_TEMPLATE.md) to the Islandora GitHub repository that you forked in step 1. If your pull request is related to an existing issue -- for instance, because you reported a [bug/issue](https://github.com/Islandora/documentation/issues) earlier -- prefix the title of your pull request with the corresponding issue number (e.g. `issue-123: ...`). Please also include a reference to the issue in the description of the pull. This can be done by using '#' plus the issue number like so '#123', also try to pick an appropriate name for the branch in which you're issuing the pull request from. |
||||
|
||||
You may want to read [Syncing a fork](https://help.github.com/articles/syncing-a-fork) for instructions on how to keep your fork up to date with the latest changes of the upstream (official) repository. |
||||
|
||||
## License Agreements |
||||
|
||||
The Islandora Foundation requires that contributors complete a [Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_cla.pdf) or be covered by a [Corporate Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_ccla.pdf). The signed copy of the license agreement should be sent to <a href="mailto:community@islandora.ca?Subject=Contributor%20License%20Agreement" target="_top">community@islandora.ca</a>. This license is for your protection as a contributor as well as the protection of the Foundation and its users; it does not change your rights to use your own contributions for any other purpose. A list of current CLAs is kept [here](https://github.com/Islandora/islandora/wiki/Contributor-License-Agreements). |
@ -0,0 +1,339 @@
|
||||
GNU GENERAL PUBLIC LICENSE |
||||
Version 2, June 1991 |
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc., |
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
||||
Everyone is permitted to copy and distribute verbatim copies |
||||
of this license document, but changing it is not allowed. |
||||
|
||||
Preamble |
||||
|
||||
The licenses for most software are designed to take away your |
||||
freedom to share and change it. By contrast, the GNU General Public |
||||
License is intended to guarantee your freedom to share and change free |
||||
software--to make sure the software is free for all its users. This |
||||
General Public License applies to most of the Free Software |
||||
Foundation's software and to any other program whose authors commit to |
||||
using it. (Some other Free Software Foundation software is covered by |
||||
the GNU Lesser General Public License instead.) You can apply it to |
||||
your programs, too. |
||||
|
||||
When we speak of free software, we are referring to freedom, not |
||||
price. Our General Public Licenses are designed to make sure that you |
||||
have the freedom to distribute copies of free software (and charge for |
||||
this service if you wish), that you receive source code or can get it |
||||
if you want it, that you can change the software or use pieces of it |
||||
in new free programs; and that you know you can do these things. |
||||
|
||||
To protect your rights, we need to make restrictions that forbid |
||||
anyone to deny you these rights or to ask you to surrender the rights. |
||||
These restrictions translate to certain responsibilities for you if you |
||||
distribute copies of the software, or if you modify it. |
||||
|
||||
For example, if you distribute copies of such a program, whether |
||||
gratis or for a fee, you must give the recipients all the rights that |
||||
you have. You must make sure that they, too, receive or can get the |
||||
source code. And you must show them these terms so they know their |
||||
rights. |
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and |
||||
(2) offer you this license which gives you legal permission to copy, |
||||
distribute and/or modify the software. |
||||
|
||||
Also, for each author's protection and ours, we want to make certain |
||||
that everyone understands that there is no warranty for this free |
||||
software. If the software is modified by someone else and passed on, we |
||||
want its recipients to know that what they have is not the original, so |
||||
that any problems introduced by others will not reflect on the original |
||||
authors' reputations. |
||||
|
||||
Finally, any free program is threatened constantly by software |
||||
patents. We wish to avoid the danger that redistributors of a free |
||||
program will individually obtain patent licenses, in effect making the |
||||
program proprietary. To prevent this, we have made it clear that any |
||||
patent must be licensed for everyone's free use or not licensed at all. |
||||
|
||||
The precise terms and conditions for copying, distribution and |
||||
modification follow. |
||||
|
||||
GNU GENERAL PUBLIC LICENSE |
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION |
||||
|
||||
0. This License applies to any program or other work which contains |
||||
a notice placed by the copyright holder saying it may be distributed |
||||
under the terms of this General Public License. The "Program", below, |
||||
refers to any such program or work, and a "work based on the Program" |
||||
means either the Program or any derivative work under copyright law: |
||||
that is to say, a work containing the Program or a portion of it, |
||||
either verbatim or with modifications and/or translated into another |
||||
language. (Hereinafter, translation is included without limitation in |
||||
the term "modification".) Each licensee is addressed as "you". |
||||
|
||||
Activities other than copying, distribution and modification are not |
||||
covered by this License; they are outside its scope. The act of |
||||
running the Program is not restricted, and the output from the Program |
||||
is covered only if its contents constitute a work based on the |
||||
Program (independent of having been made by running the Program). |
||||
Whether that is true depends on what the Program does. |
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's |
||||
source code as you receive it, in any medium, provided that you |
||||
conspicuously and appropriately publish on each copy an appropriate |
||||
copyright notice and disclaimer of warranty; keep intact all the |
||||
notices that refer to this License and to the absence of any warranty; |
||||
and give any other recipients of the Program a copy of this License |
||||
along with the Program. |
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and |
||||
you may at your option offer warranty protection in exchange for a fee. |
||||
|
||||
2. You may modify your copy or copies of the Program or any portion |
||||
of it, thus forming a work based on the Program, and copy and |
||||
distribute such modifications or work under the terms of Section 1 |
||||
above, provided that you also meet all of these conditions: |
||||
|
||||
a) You must cause the modified files to carry prominent notices |
||||
stating that you changed the files and the date of any change. |
||||
|
||||
b) You must cause any work that you distribute or publish, that in |
||||
whole or in part contains or is derived from the Program or any |
||||
part thereof, to be licensed as a whole at no charge to all third |
||||
parties under the terms of this License. |
||||
|
||||
c) If the modified program normally reads commands interactively |
||||
when run, you must cause it, when started running for such |
||||
interactive use in the most ordinary way, to print or display an |
||||
announcement including an appropriate copyright notice and a |
||||
notice that there is no warranty (or else, saying that you provide |
||||
a warranty) and that users may redistribute the program under |
||||
these conditions, and telling the user how to view a copy of this |
||||
License. (Exception: if the Program itself is interactive but |
||||
does not normally print such an announcement, your work based on |
||||
the Program is not required to print an announcement.) |
||||
|
||||
These requirements apply to the modified work as a whole. If |
||||
identifiable sections of that work are not derived from the Program, |
||||
and can be reasonably considered independent and separate works in |
||||
themselves, then this License, and its terms, do not apply to those |
||||
sections when you distribute them as separate works. But when you |
||||
distribute the same sections as part of a whole which is a work based |
||||
on the Program, the distribution of the whole must be on the terms of |
||||
this License, whose permissions for other licensees extend to the |
||||
entire whole, and thus to each and every part regardless of who wrote it. |
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest |
||||
your rights to work written entirely by you; rather, the intent is to |
||||
exercise the right to control the distribution of derivative or |
||||
collective works based on the Program. |
||||
|
||||
In addition, mere aggregation of another work not based on the Program |
||||
with the Program (or with a work based on the Program) on a volume of |
||||
a storage or distribution medium does not bring the other work under |
||||
the scope of this License. |
||||
|
||||
3. You may copy and distribute the Program (or a work based on it, |
||||
under Section 2) in object code or executable form under the terms of |
||||
Sections 1 and 2 above provided that you also do one of the following: |
||||
|
||||
a) Accompany it with the complete corresponding machine-readable |
||||
source code, which must be distributed under the terms of Sections |
||||
1 and 2 above on a medium customarily used for software interchange; or, |
||||
|
||||
b) Accompany it with a written offer, valid for at least three |
||||
years, to give any third party, for a charge no more than your |
||||
cost of physically performing source distribution, a complete |
||||
machine-readable copy of the corresponding source code, to be |
||||
distributed under the terms of Sections 1 and 2 above on a medium |
||||
customarily used for software interchange; or, |
||||
|
||||
c) Accompany it with the information you received as to the offer |
||||
to distribute corresponding source code. (This alternative is |
||||
allowed only for noncommercial distribution and only if you |
||||
received the program in object code or executable form with such |
||||
an offer, in accord with Subsection b above.) |
||||
|
||||
The source code for a work means the preferred form of the work for |
||||
making modifications to it. For an executable work, complete source |
||||
code means all the source code for all modules it contains, plus any |
||||
associated interface definition files, plus the scripts used to |
||||
control compilation and installation of the executable. However, as a |
||||
special exception, the source code distributed need not include |
||||
anything that is normally distributed (in either source or binary |
||||
form) with the major components (compiler, kernel, and so on) of the |
||||
operating system on which the executable runs, unless that component |
||||
itself accompanies the executable. |
||||
|
||||
If distribution of executable or object code is made by offering |
||||
access to copy from a designated place, then offering equivalent |
||||
access to copy the source code from the same place counts as |
||||
distribution of the source code, even though third parties are not |
||||
compelled to copy the source along with the object code. |
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program |
||||
except as expressly provided under this License. Any attempt |
||||
otherwise to copy, modify, sublicense or distribute the Program is |
||||
void, and will automatically terminate your rights under this License. |
||||
However, parties who have received copies, or rights, from you under |
||||
this License will not have their licenses terminated so long as such |
||||
parties remain in full compliance. |
||||
|
||||
5. You are not required to accept this License, since you have not |
||||
signed it. However, nothing else grants you permission to modify or |
||||
distribute the Program or its derivative works. These actions are |
||||
prohibited by law if you do not accept this License. Therefore, by |
||||
modifying or distributing the Program (or any work based on the |
||||
Program), you indicate your acceptance of this License to do so, and |
||||
all its terms and conditions for copying, distributing or modifying |
||||
the Program or works based on it. |
||||
|
||||
6. Each time you redistribute the Program (or any work based on the |
||||
Program), the recipient automatically receives a license from the |
||||
original licensor to copy, distribute or modify the Program subject to |
||||
these terms and conditions. You may not impose any further |
||||
restrictions on the recipients' exercise of the rights granted herein. |
||||
You are not responsible for enforcing compliance by third parties to |
||||
this License. |
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent |
||||
infringement or for any other reason (not limited to patent issues), |
||||
conditions are imposed on you (whether by court order, agreement or |
||||
otherwise) that contradict the conditions of this License, they do not |
||||
excuse you from the conditions of this License. If you cannot |
||||
distribute so as to satisfy simultaneously your obligations under this |
||||
License and any other pertinent obligations, then as a consequence you |
||||
may not distribute the Program at all. For example, if a patent |
||||
license would not permit royalty-free redistribution of the Program by |
||||
all those who receive copies directly or indirectly through you, then |
||||
the only way you could satisfy both it and this License would be to |
||||
refrain entirely from distribution of the Program. |
||||
|
||||
If any portion of this section is held invalid or unenforceable under |
||||
any particular circumstance, the balance of the section is intended to |
||||
apply and the section as a whole is intended to apply in other |
||||
circumstances. |
||||
|
||||
It is not the purpose of this section to induce you to infringe any |
||||
patents or other property right claims or to contest validity of any |
||||
such claims; this section has the sole purpose of protecting the |
||||
integrity of the free software distribution system, which is |
||||
implemented by public license practices. Many people have made |
||||
generous contributions to the wide range of software distributed |
||||
through that system in reliance on consistent application of that |
||||
system; it is up to the author/donor to decide if he or she is willing |
||||
to distribute software through any other system and a licensee cannot |
||||
impose that choice. |
||||
|
||||
This section is intended to make thoroughly clear what is believed to |
||||
be a consequence of the rest of this License. |
||||
|
||||
8. If the distribution and/or use of the Program is restricted in |
||||
certain countries either by patents or by copyrighted interfaces, the |
||||
original copyright holder who places the Program under this License |
||||
may add an explicit geographical distribution limitation excluding |
||||
those countries, so that distribution is permitted only in or among |
||||
countries not thus excluded. In such case, this License incorporates |
||||
the limitation as if written in the body of this License. |
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions |
||||
of the General Public License from time to time. Such new versions will |
||||
be similar in spirit to the present version, but may differ in detail to |
||||
address new problems or concerns. |
||||
|
||||
Each version is given a distinguishing version number. If the Program |
||||
specifies a version number of this License which applies to it and "any |
||||
later version", you have the option of following the terms and conditions |
||||
either of that version or of any later version published by the Free |
||||
Software Foundation. If the Program does not specify a version number of |
||||
this License, you may choose any version ever published by the Free Software |
||||
Foundation. |
||||
|
||||
10. If you wish to incorporate parts of the Program into other free |
||||
programs whose distribution conditions are different, write to the author |
||||
to ask for permission. For software which is copyrighted by the Free |
||||
Software Foundation, write to the Free Software Foundation; we sometimes |
||||
make exceptions for this. Our decision will be guided by the two goals |
||||
of preserving the free status of all derivatives of our free software and |
||||
of promoting the sharing and reuse of software generally. |
||||
|
||||
NO WARRANTY |
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY |
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN |
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES |
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED |
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS |
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE |
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, |
||||
REPAIR OR CORRECTION. |
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR |
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, |
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING |
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED |
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY |
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER |
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE |
||||
POSSIBILITY OF SUCH DAMAGES. |
||||
|
||||
END OF TERMS AND CONDITIONS |
||||
|
||||
How to Apply These Terms to Your New Programs |
||||
|
||||
If you develop a new program, and you want it to be of the greatest |
||||
possible use to the public, the best way to achieve this is to make it |
||||
free software which everyone can redistribute and change under these terms. |
||||
|
||||
To do so, attach the following notices to the program. It is safest |
||||
to attach them to the start of each source file to most effectively |
||||
convey the exclusion of warranty; and each file should have at least |
||||
the "copyright" line and a pointer to where the full notice is found. |
||||
|
||||
<one line to give the program's name and a brief idea of what it does.> |
||||
Copyright (C) <year> <name of author> |
||||
|
||||
This program is free software; you can redistribute it and/or modify |
||||
it under the terms of the GNU General Public License as published by |
||||
the Free Software Foundation; either version 2 of the License, or |
||||
(at your option) any later version. |
||||
|
||||
This program is distributed in the hope that it will be useful, |
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
GNU General Public License for more details. |
||||
|
||||
You should have received a copy of the GNU General Public License along |
||||
with this program; if not, write to the Free Software Foundation, Inc., |
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
||||
|
||||
Also add information on how to contact you by electronic and paper mail. |
||||
|
||||
If the program is interactive, make it output a short notice like this |
||||
when it starts in an interactive mode: |
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author |
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. |
||||
This is free software, and you are welcome to redistribute it |
||||
under certain conditions; type `show c' for details. |
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate |
||||
parts of the General Public License. Of course, the commands you use may |
||||
be called something other than `show w' and `show c'; they could even be |
||||
mouse-clicks or menu items--whatever suits your program. |
||||
|
||||
You should also get your employer (if you work as a programmer) or your |
||||
school, if any, to sign a "copyright disclaimer" for the program, if |
||||
necessary. Here is a sample; alter the names: |
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program |
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker. |
||||
|
||||
<signature of Ty Coon>, 1 April 1989 |
||||
Ty Coon, President of Vice |
||||
|
||||
This General Public License does not permit incorporating your program into |
||||
proprietary programs. If your program is a subroutine library, you may |
||||
consider it more useful to permit linking proprietary applications with the |
||||
library. If this is what you want to do, use the GNU Lesser General |
||||
Public License instead of this License. |
@ -0,0 +1,261 @@
|
||||
# Islandora Advanced Search <!-- omit in toc --> |
||||
|
||||
- [Introduction](#introduction) |
||||
- [Requirements](#requirements) |
||||
- [Installation](#installation) |
||||
- [Configuration](#configuration) |
||||
- [Configuring Solr](#configuring-solr) |
||||
- [Configure Collection Search](#configure-collection-search) |
||||
- [Configure Views](#configure-views) |
||||
- [Exposed Form](#exposed-form) |
||||
- [Collection Search](#collection-search) |
||||
- [Paging](#paging) |
||||
- [Sorting](#sorting) |
||||
- [Configure Facets](#configure-facets) |
||||
- [Include / Exclude Facets](#include--exclude-facets) |
||||
- [Configure Blocks](#configure-blocks) |
||||
- [Advanced Search Block](#advanced-search-block) |
||||
- [Documentation](#documentation) |
||||
- [Troubleshooting/Issues](#troubleshootingissues) |
||||
- [Maintainers](#maintainers) |
||||
- [Sponsors](#sponsors) |
||||
- [Development](#development) |
||||
- [License](#license) |
||||
|
||||
## Introduction |
||||
|
||||
This module creates several blocks to support searching. It also enables the use |
||||
of Ajax with search blocks, facets, and search results. |
||||
|
||||
![image](./docs/demo.gif) |
||||
|
||||
## Requirements |
||||
|
||||
Use composer to download the required libraries and modules. |
||||
|
||||
```bash |
||||
composer require drupal/facets "^1.3" |
||||
composer require drupal/search_api_solr "^4.1" |
||||
composer require drupal/search_api "^1.5" |
||||
``` |
||||
|
||||
However, for reference, `islandora_advanced_search` requires the following |
||||
drupal modules: |
||||
|
||||
- [facets](https://www.drupal.org/project/facets) |
||||
- [search_api_solr](https://www.drupal.org/project/search_api_solr) |
||||
|
||||
## Installation |
||||
|
||||
To download/enable just this module, use the following from the command line: |
||||
|
||||
```bash |
||||
composer require islandora/islandora |
||||
drush en islandora_advanced_search |
||||
``` |
||||
|
||||
## Configuration |
||||
|
||||
You can set the following configuration at |
||||
`admin/config/islandora/advanced_search`: |
||||
|
||||
![image](./docs/islandora_advanced_search_settings.png) |
||||
|
||||
## Configuring Solr |
||||
|
||||
Please review |
||||
[Islandora Documentation](https://islandora.github.io/documentation/user-documentation/searching/) |
||||
before continuing. The following assumes you already have a working Solr and the |
||||
Drupal Search API setup. |
||||
|
||||
## Configure Collection Search |
||||
|
||||
To support collection based searches you need to index the `field_member_of` for |
||||
every repository item as well define a new field that captures the full |
||||
hierarchy of `field_member_of` for each repository item. |
||||
|
||||
Add a new `Content` solr field `field_decedent_of` to the solr index at |
||||
`admin/config/search/search-api/index/default_solr_index/fields`. |
||||
|
||||
![image](./docs/field_decedent_of.png) |
||||
|
||||
Then under `admin/config/search/search-api/index/default_solr_index/processors` |
||||
enable `Index hierarchy` and setup the new field to index the hierarchy. |
||||
|
||||
![image](./docs/enable_index_hierarchy.png) |
||||
|
||||
![image](./docs/enable_index_hierarchy_processor.png) |
||||
|
||||
The field can now be used limit a search to all the decedents of a given object. |
||||
|
||||
> N.B. You may have to re-index to make sure the field is populated. |
||||
|
||||
## Configure Views |
||||
|
||||
The configuration of views is outside of the scope of this document, please read |
||||
the [Drupal Documentation](https://www.drupal.org/docs/8/core/modules/views), as |
||||
well as the |
||||
[Search API Documentation](https://www.drupal.org/docs/contributed-modules/search-api). |
||||
|
||||
### Exposed Form |
||||
|
||||
Solr views allow the user to configure an exposed form (_optionally as a |
||||
block_). This form / block is **different** from the |
||||
[Advanced Search Block](#advanced-search-block). This module does not make any |
||||
changes to the form, but this form can cause the Advanced Search Block to not |
||||
function if configured incorrectly. |
||||
|
||||
The Advanced Search Block requires that if present the Exposed forms |
||||
`Exposed form style` is set to `Basic` rather than `Input Required`. As |
||||
`Input Required` will prevent any search from occurring unless the user puts an |
||||
additional query in the Exposed form as well. |
||||
|
||||
![Form Style](./docs/basic-input.png) |
||||
|
||||
### Collection Search |
||||
|
||||
That being said it will be typical that you require the following |
||||
`Relationships` and `Contextual Filters` when setting up a search view to enable |
||||
`Collection Search` searches. |
||||
|
||||
![image](./docs/view_advanced_setting.png) |
||||
|
||||
Here a relationship is setup with `Member Of` field and we have **two** |
||||
contextual filters: |
||||
|
||||
1. `field_member_of` (Direct decedents of the Entity) |
||||
2. `field_decedent_of` (All decedents of the Entity) |
||||
|
||||
Both of these filters are configured the exact same way. |
||||
|
||||
![image](./docs/contextual_filter_settings.png) |
||||
|
||||
These filters are toggled by the Advanced Search block to allow the search to |
||||
include all decedents or just direct decedents (*documented below*). |
||||
|
||||
### Paging |
||||
|
||||
The paging options specified here can have an affect on the pager block |
||||
(*documented below*). |
||||
|
||||
![image](./docs/pager_settings.png) |
||||
|
||||
### Sorting |
||||
|
||||
Additional the fields listed as `Sort Criteria` as `Exposed` will be made |
||||
available in the pager block (*documented below*). |
||||
|
||||
![image](./docs/sort_criteria.png) |
||||
|
||||
## Configure Facets |
||||
|
||||
The facets can be configured at `admin/config/search/facets`. Facets are linked |
||||
to a **Source** which is a **Search API View Display** so it will be typically |
||||
to have to duplicate your configuration for a given facet across each of the |
||||
displays where you want it to show up. |
||||
|
||||
### Include / Exclude Facets |
||||
|
||||
To be able to display exclude facet links as well as include links in the facets |
||||
block we have to duplicate the configuration for the facet like so. |
||||
|
||||
![image](./docs/include_exclude_facets.png) |
||||
|
||||
Both the include / exclude facets must use the widget |
||||
`List of links that allow the user to include / exclude facets` |
||||
|
||||
![image](./docs/include_exclude_facets_settings.png) |
||||
|
||||
The excluded facet also needs the following settings to appear and function |
||||
correctly. |
||||
|
||||
The `URL alias` must match the same value as the include facet except it must be |
||||
prefixed with `~` character that is what links to the two facets to each other. |
||||
|
||||
![image](./docs/exclude_facet_settings_url_alias.png) |
||||
|
||||
And it must also explicitly be set to exclude: |
||||
|
||||
![image](./docs/exclude_facet_settings_exclude.png) |
||||
|
||||
You may also want to enable `Hide active items` and `Hide non-narrowing results` |
||||
for a cleaner presentation of facets. |
||||
|
||||
## Configure Blocks |
||||
|
||||
For each block type: |
||||
|
||||
- Facet |
||||
- Pager |
||||
- Advanced Search |
||||
|
||||
There will be **one block** per `View Display`. The block should be limited to |
||||
only appear when the view it was derived from is also being displayed on the |
||||
same page. |
||||
|
||||
This requires configuring the `visibility` of the block as appropriate. For |
||||
collection based searches be sure to limit the display of the Facets block to |
||||
the models you want to display the search on, e.g: |
||||
|
||||
![image](./docs/facet_block_settings.png) |
||||
|
||||
### Advanced Search Block |
||||
|
||||
For any valid search field, you can drag / drop and reorder the fields to |
||||
display in the advanced search form on. The configuration resides on the block |
||||
so this can differ across views / displays if need be. Additionally if the View |
||||
the block was derived from has multiple contextual filters you can choose which |
||||
one corresponds to direct children, this will enable the recursive search |
||||
checkbox. |
||||
|
||||
![image](./docs/advanced_search_block_settings.png) |
||||
|
||||
> N.B. Be aware that the Search views [Exposed Form](#exposed-form) can have an |
||||
> affect on the function of the |
||||
> [Advanced Search Block](#advanced-search-block). Please refer to that section |
||||
> to learn more. |
||||
|
||||
## Documentation |
||||
|
||||
Further documentation for this module is available on the |
||||
[Islandora 8 documentation site](https://islandora.github.io/documentation/). |
||||
|
||||
## Troubleshooting/Issues |
||||
|
||||
Having problems or solved a problem? Check out the Islandora google groups for |
||||
a solution. |
||||
|
||||
- [Islandora Group](https://groups.google.com/forum/?hl=en&fromgroups#!forum/islandora) |
||||
- [Islandora Dev Group](https://groups.google.com/forum/?hl=en&fromgroups#!forum/islandora-dev) |
||||
|
||||
## Maintainers |
||||
|
||||
Current maintainers: |
||||
|
||||
- [Nigel Banks](https://github.com/nigelgbanks) |
||||
|
||||
## Sponsors |
||||
|
||||
- LYRASIS |
||||
|
||||
## Development |
||||
|
||||
If you would like to contribute, please get involved by attending our weekly |
||||
[Tech Call](https://github.com/Islandora/documentation/wiki). We love to hear |
||||
from you! |
||||
|
||||
If you would like to contribute code to the project, you need to be covered by |
||||
an Islandora Foundation |
||||
[Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_cla.pdf) |
||||
or |
||||
[Corporate Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_ccla.pdf). |
||||
Please see the [Contributors](http://islandora.ca/resources/contributors) pages |
||||
on Islandora.ca for more information. |
||||
|
||||
We recommend using the |
||||
[islandora-playbook](https://github.com/Islandora-Devops/islandora-playbook) to |
||||
get started. |
||||
|
||||
## License |
||||
|
||||
[GPLv2](http://www.gnu.org/licenses/gpl-2.0.txt) |
@ -0,0 +1,37 @@
|
||||
.islandora-advanced-search-form .form-type-select { |
||||
display: inline-block; |
||||
} |
||||
|
||||
.islandora-advanced-search-form .form-type-select__select-wrapper { |
||||
width: auto; |
||||
} |
||||
|
||||
.islandora-advanced-search-form .form-select { |
||||
margin-right: 0.25em; |
||||
} |
||||
|
||||
input.islandora-advanced-search-form__add, |
||||
input.islandora-advanced-search-form__remove { |
||||
display: inline-block; |
||||
background: none !important; |
||||
border: none; |
||||
box-shadow: none; |
||||
color: #0c6170; |
||||
padding: 0 !important; |
||||
text-decoration: none; |
||||
margin: 0 0 1rem; |
||||
} |
||||
|
||||
input.islandora-advanced-search-form__add:hover, |
||||
input.islandora-advanced-search-form__add:focus, |
||||
input.islandora-advanced-search-form__remove:hover, |
||||
input.islandora-advanced-search-form__remove:focus { |
||||
text-decoration: underline; |
||||
color: #0c6170; |
||||
outline: none; |
||||
} |
||||
|
||||
input.islandora-advanced-search-form__reset, |
||||
input.islandora-advanced-search-form__search { |
||||
display: inline-block; |
||||
} |
@ -0,0 +1,111 @@
|
||||
.islandora_advanced_search_result_pager .pager__summary { |
||||
font-weight: 700; |
||||
} |
||||
|
||||
.islandora_advanced_search_result_pager .pager__group { |
||||
margin: 1.25rem 0; |
||||
padding: 1rem 0; |
||||
border-top: 1px solid; |
||||
border-bottom: 1px solid; |
||||
border-color: #e5e5e5; |
||||
display: flex; |
||||
justify-content: flex-start; |
||||
align-items: center; |
||||
flex-flow: row wrap; |
||||
} |
||||
|
||||
@media all and (min-width: 45.063em) { |
||||
.islandora_advanced_search_result_pager .pager__group { |
||||
justify-content: flex-end; |
||||
} |
||||
.islandora_advanced_search_result_pager .pager__group > * { |
||||
margin: 0.47214rem 0 0.47214rem 2.61803rem; |
||||
} |
||||
.islandora_advanced_search_result_pager .pager__group > *:first-child { |
||||
margin-left: 0; |
||||
} |
||||
} |
||||
|
||||
.islandora_advanced_search_result_pager .pager__group > * { |
||||
margin: 0.47214rem 2rem 0.47214rem 0; |
||||
} |
||||
|
||||
.islandora_advanced_search_result_pager .pager__group > *:last-child { |
||||
margin-right: 0; |
||||
} |
||||
|
||||
.islandora_advanced_search_result_pager .pager__group .item-list__list, |
||||
.islandora_advanced_search_result_pager .pager__group .item-list__title, |
||||
.islandora_advanced_search_result_pager .pager__group .item-list__item { |
||||
display: inline; |
||||
} |
||||
|
||||
.islandora_advanced_search_result_pager .pager__group .item-list__title { |
||||
font-size: initial; |
||||
margin: 0.25rem; |
||||
} |
||||
|
||||
.pager { |
||||
margin: initial; |
||||
} |
||||
|
||||
.pager__item { |
||||
margin: 0.125rem; |
||||
text-align: center; |
||||
} |
||||
|
||||
.pager__items { |
||||
text-align: right; |
||||
} |
||||
|
||||
@media all and (max-width: 45em) { |
||||
.pager__items { |
||||
text-align: center; |
||||
} |
||||
} |
||||
|
||||
.pager__items__first-previous, |
||||
.pager__items__num-pages, |
||||
.pager__items__next-last { |
||||
display: inline; |
||||
} |
||||
|
||||
.pager__items__first-previous, |
||||
.pager__items__next-last { |
||||
float: none; |
||||
} |
||||
|
||||
.pager__items__first-previous .pager__item, |
||||
.pager__items__next-last .pager__item { |
||||
display: inline; |
||||
} |
||||
|
||||
.pager .pager__link, |
||||
.pager__results .pager__link { |
||||
display: inline-block; |
||||
border-radius: 0.125em; |
||||
border: 1px solid; |
||||
transition: all, 0.2s, ease-in-out; |
||||
min-width: 1.75em; |
||||
padding: 0.125rem 0.4375rem 0; |
||||
} |
||||
|
||||
.pager .pager__link:focus, |
||||
.pager .pager__link:hover, |
||||
.pager__results .pager__link:focus, |
||||
.pager__results .pager__link:hover { |
||||
text-decoration: underline; |
||||
} |
||||
|
||||
.pager__display .pager__link { |
||||
background-color: #ffffff; |
||||
} |
||||
|
||||
.pager__display .pager__link:hover, |
||||
.pager__display .pager__link:focus { |
||||
background-color: #ffffff; |
||||
} |
||||
|
||||
.pager__link--is-active { |
||||
text-decoration: underline; |
||||
} |
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 85 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 645 KiB |
After Width: | Height: | Size: 8.2 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1,13 @@
|
||||
# This .info.yml files provides the basic information about our module to Drupal |
||||
# More: https://www.drupal.org/node/2000204 |
||||
name: 'Islandora Advanced Search' |
||||
description: "Creates an Advanced Search block and other enhancements to search." |
||||
type: module |
||||
package: Islandora |
||||
core_version_requirement: ^9 || ^10 |
||||
dependencies: |
||||
- drupal:facets |
||||
- drupal:facets_summary |
||||
- drupal:search_api_solr |
||||
lifecycle: deprecated |
||||
lifecycle_link: https://groups.google.com/g/islandora/c/SEOAWJrfE_M |
@ -0,0 +1,17 @@
|
||||
advanced.search.admin: |
||||
js: |
||||
js/islandora_advanced_search.admin.js: {} |
||||
dependencies: |
||||
- core/drupal.tabledrag |
||||
|
||||
advanced.search.form: |
||||
js: |
||||
js/islandora_advanced_search.form.js: {} |
||||
css: |
||||
component: |
||||
css/islandora_advanced_search.form.css: {} |
||||
|
||||
advanced.search.pager: |
||||
css: |
||||
component: |
||||
css/islandora_advanced_search.pager.css: {} |
@ -0,0 +1,6 @@
|
||||
islandora_advanced_search.settings: |
||||
title: 'Advanced Search Settings' |
||||
route_name: islandora_advanced_search.settings |
||||
description: 'Configure Islandora Advanced Search settings' |
||||
parent: system.admin_config_islandora |
||||
weight: 99 |
@ -0,0 +1,89 @@
|
||||
<?php |
||||
|
||||
/** |
||||
* @file |
||||
* Contains islandora_advanced_search.module. |
||||
* |
||||
* This file is part of the Islandora Project. |
||||
* |
||||
* (c) Islandora Foundation |
||||
* |
||||
* For the full copyright and license information, please view the LICENSE |
||||
* file that was distributed with this source code. |
||||
*/ |
||||
|
||||
use Drupal\block\Entity\Block; |
||||
use Drupal\Core\Form\FormStateInterface; |
||||
use Drupal\islandora_advanced_search\AdvancedSearchQuery; |
||||
use Drupal\islandora_advanced_search\Utilities; |
||||
use Drupal\search_api\Query\QueryInterface as DrupalQueryInterface; |
||||
use Drupal\views\ViewExecutable; |
||||
use Solarium\Core\Query\QueryInterface as SolariumQueryInterface; |
||||
|
||||
/** |
||||
* Implements hook_search_api_solr_converted_query_alter(). |
||||
*/ |
||||
function islandora_advanced_search_search_api_solr_converted_query_alter(SolariumQueryInterface $solarium_query, DrupalQueryInterface $search_api_query) { |
||||
// We must modify the query itself rather than the representation the |
||||
// search_api presents as it is not possible to use the 'OR' operator |
||||
// with it as it converts conditions into separate filter queries. |
||||
// Additionally filter queries do not affect the score so are not |
||||
// suitable for use in the advanced search queries. |
||||
$advanced_search_query = new AdvancedSearchQuery(); |
||||
$advanced_search_query->alterQuery(\Drupal::request(), $solarium_query, $search_api_query); |
||||
} |
||||
|
||||
/** |
||||
* Implements hook_form_form_id_alter(). |
||||
*/ |
||||
function islandora_advanced_search_form_block_form_alter(&$form, FormStateInterface $form_state, $form_id) { |
||||
// Islandora removes this condition from the form, but we require it. |
||||
// So we can show blocks for nodes which belong to specific models. |
||||
// Allowing us to add a block for collections only. |
||||
$visibility = []; |
||||
$entity_id = $form['id']['#default_value']; |
||||
$block = Block::load($entity_id); |
||||
if ($block) { |
||||
$visibility = $block->getVisibility(); |
||||
} |
||||
$manager = \Drupal::getContainer()->get('plugin.manager.condition'); |
||||
$condition_id = 'node_has_term'; |
||||
|
||||
/** @var \Drupal\Core\Condition\ConditionInterface $condition */ |
||||
$condition = $manager->createInstance($condition_id, isset($visibility[$condition_id]) ? $visibility[$condition_id] : []); |
||||
$form_state->set(['conditions', $condition_id], $condition); |
||||
$condition_form = $condition->buildConfigurationForm([], $form_state); |
||||
$condition_form['#type'] = 'details'; |
||||
$condition_form['#title'] = $condition->getPluginDefinition()['label']; |
||||
$condition_form['#group'] = 'visibility_tabs'; |
||||
// Not all blocks are required to give this field. |
||||
$condition_form['term']['#required'] = FALSE; |
||||
$form['visibility'][$condition_id] = $condition_form; |
||||
} |
||||
|
||||
/** |
||||
* Implements hook_preprocess_preprocess_views_view(). |
||||
*/ |
||||
function islandora_advanced_search_preprocess_views_view(&$variables) { |
||||
/** @var \Drupal\views\ViewExecutable $view */ |
||||
$view = &$variables['view']; |
||||
$views = Utilities::getPagerViewDisplays(); |
||||
// Only add the toggle class for view display on displays in which the pager |
||||
// has been created for. |
||||
if (in_array([$view->id(), $view->current_display], $views)) { |
||||
// Toggle between 'list' and 'grid' display depending on url parameter. |
||||
$format = \Drupal::request()->query->get('display') ?? 'list'; |
||||
$variables['attributes']['class'][] = "view-{$format}"; |
||||
$view->element['#attached']['library'][] = 'islandora_advanced_search/advanced.search.pager'; |
||||
} |
||||
$view = &$variables['view']; |
||||
} |
||||
|
||||
/** |
||||
* Implements hook_views_pre_view(). |
||||
*/ |
||||
function islandora_advanced_search_views_pre_view(ViewExecutable $view, $display_id, array &$args) { |
||||
// Allow for recursive searches by disabling contextual filter. |
||||
$advanced_search_query = new AdvancedSearchQuery(); |
||||
$advanced_search_query->alterView(\Drupal::request(), $view, $display_id); |
||||
} |
@ -0,0 +1,17 @@
|
||||
islandora_advanced_search.ajax.blocks: |
||||
path: '/islandora-advanced-search-ajax-blocks' |
||||
defaults: |
||||
_controller: '\Drupal\islandora_advanced_search\Controller\AjaxBlocksController::respond' |
||||
requirements: |
||||
# Allow public access to search blocks. |
||||
_access: 'TRUE' |
||||
|
||||
islandora_advanced_search.settings: |
||||
path: '/admin/config/search/advanced' |
||||
defaults: |
||||
_form: '\Drupal\islandora_advanced_search\Form\SettingsForm' |
||||
_title: 'Islandora Advanced Search Settings' |
||||
requirements: |
||||
_permission: 'administer site configuration' |
||||
options: |
||||
_admin_route: TRUE |
@ -0,0 +1,113 @@
|
||||
//# sourceURL=modules/contrib/islandora_advanced_search/js/islandora-advanced-search.admin.js
|
||||
/** |
||||
* @file |
||||
* Largely based on core/modules/blocks/js/blocks.js |
||||
*
|
||||
* This file allows for moving rows between two regions in a table and have the |
||||
* 'region' field update appropriately. |
||||
*/ |
||||
(function ($, window, Drupal) { |
||||
Drupal.behaviors.islandoraAdvancedSearchAdmin = { |
||||
attach: function attach(context, settings) { |
||||
if (typeof Drupal.tableDrag === 'undefined' || typeof Drupal.tableDrag['advanced-search-fields'] === 'undefined') { |
||||
return; |
||||
} |
||||
|
||||
function checkEmptyRegions(table, rowObject) { |
||||
table.find('tr.region-message').each(function () { |
||||
var $this = $(this); |
||||
|
||||
if ($this.prev('tr').get(0) === rowObject.element) { |
||||
if (rowObject.method !== 'keyboard' || rowObject.direction === 'down') { |
||||
rowObject.swap('after', this); |
||||
} |
||||
} |
||||
|
||||
if ($this.next('tr').is(':not(.draggable)') || $this.next('tr').length === 0) { |
||||
$this.removeClass('region-populated').addClass('region-empty'); |
||||
} else if ($this.is('.region-empty')) { |
||||
$this.removeClass('region-empty').addClass('region-populated'); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
function updateLastPlaced(table, rowObject) { |
||||
table.find('.color-success').removeClass('color-success'); |
||||
|
||||
var $rowObject = $(rowObject); |
||||
if (!$rowObject.is('.drag-previous')) { |
||||
table.find('.drag-previous').removeClass('drag-previous'); |
||||
$rowObject.addClass('drag-previous'); |
||||
} |
||||
} |
||||
|
||||
function updateFieldWeights(table, region) { |
||||
var weight = -Math.round(table.find('.draggable').length / 2); |
||||
|
||||
table.find('.region-' + region + '-message').nextUntil('.region-title').find('select.field-weight').val(function () { |
||||
return ++weight; |
||||
}); |
||||
} |
||||
|
||||
var table = $('#advanced-search-fields'); |
||||
|
||||
var tableDrag = Drupal.tableDrag['advanced-search-fields']; |
||||
|
||||
tableDrag.row.prototype.onSwap = function (swappedRow) { |
||||
checkEmptyRegions(table, this); |
||||
updateLastPlaced(table, this); |
||||
}; |
||||
|
||||
tableDrag.onDrop = function () { |
||||
var dragObject = this; |
||||
var $rowElement = $(dragObject.rowObject.element); |
||||
|
||||
var regionRow = $rowElement.prevAll('tr.region-message').get(0); |
||||
var regionName = regionRow.className.replace(/([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/, '$2'); |
||||
var regionField = $rowElement.find('select.field-display'); |
||||
|
||||
if (regionField.find('option[value=' + regionName + ']').length === 0) { |
||||
window.alert(Drupal.t('The field cannot be placed in this region.')); |
||||
|
||||
regionField.trigger('change'); |
||||
} |
||||
|
||||
if (!regionField.is('.field-display-' + regionName)) { |
||||
var weightField = $rowElement.find('select.field-weight'); |
||||
var oldRegionName = weightField[0].className.replace(/([^ ]+[ ]+)*field-weight-([^ ]+)([ ]+[^ ]+)*/, '$2'); |
||||
regionField.removeClass('field-display-' + oldRegionName).addClass('field-display-' + regionName); |
||||
weightField.removeClass('field-weight-' + oldRegionName).addClass('field-weight-' + regionName); |
||||
regionField.val(regionName); |
||||
} |
||||
|
||||
updateFieldWeights(table, regionName); |
||||
}; |
||||
|
||||
$(context).find('select.field-display').once('field-display').on('change', function (event) { |
||||
var row = $(this).closest('tr'); |
||||
var select = $(this); |
||||
|
||||
tableDrag.rowObject = new tableDrag.row(row[0]); |
||||
var regionMessage = table.find('.region-' + select[0].value + '-message'); |
||||
var regionItems = regionMessage.nextUntil('.region-message, .region-title'); |
||||
if (regionItems.length) { |
||||
regionItems.last().after(row); |
||||
} else { |
||||
regionMessage.after(row); |
||||
} |
||||
updateFieldWeights(table, select[0].value); |
||||
|
||||
checkEmptyRegions(table, tableDrag.rowObject); |
||||
|
||||
updateLastPlaced(table, row); |
||||
|
||||
if (!tableDrag.changed) { |
||||
$(Drupal.theme('tableDragChangedWarning')).insertBefore(tableDrag.table).hide().fadeIn('slow'); |
||||
tableDrag.changed = true; |
||||
} |
||||
|
||||
select.trigger('blur'); |
||||
}); |
||||
} |
||||
}; |
||||
})(jQuery, window, Drupal); |
@ -0,0 +1,124 @@
|
||||
//# sourceURL=modules/contrib/islandora/modules/islandora_advanced_search/js/islandora-advanced-search.form.js
|
||||
/** |
||||
* @file |
||||
* Handles Ajax submission / updating form action on url change, etc. |
||||
*/ |
||||
(function ($, Drupal, drupalSettings) { |
||||
|
||||
// Gets current parameters minus ones provided by the form.
|
||||
function getParams(query_parameter, recurse_parameter) { |
||||
const url_search_params = new URLSearchParams(window.location.search); |
||||
const params = Object.fromEntries(url_search_params.entries()); |
||||
// Remove Advanced Search Query Parameters.
|
||||
const param_match = "query\\[\\d+\\]\\[.+\\]".replace("query", query_parameter); |
||||
const param_regex = new RegExp(param_match, "g"); |
||||
for (const param in params) { |
||||
if (param.match(param_regex)) { |
||||
delete params[param]; |
||||
} |
||||
} |
||||
// Remove Recurse parameter.
|
||||
delete params[recurse_parameter]; |
||||
// Remove the page if set as submitting the form should always take
|
||||
// the user to the first page (facets do the same).
|
||||
delete params["page"]; |
||||
return params; |
||||
} |
||||
|
||||
// Groups form inputs by search term.
|
||||
function getTerms(inputs) { |
||||
const input_regex = /terms\[(?<index>\d+)\]\[(?<component>.*)\]/; |
||||
const terms = []; |
||||
for (const input in inputs) { |
||||
const name = inputs[input].name; |
||||
const value = inputs[input].value; |
||||
const found = name.match(input_regex); |
||||
if (found) { |
||||
const index = parseInt(found.groups.index); |
||||
const component = found.groups.component; |
||||
if (typeof terms[index] !== 'object') { |
||||
terms[index] = {}; |
||||
} |
||||
terms[index][component] = value; |
||||
} |
||||
} |
||||
return terms; |
||||
} |
||||
|
||||
// Checks if the form user has set recursive to true in the form.
|
||||
function getRecurse(inputs) { |
||||
for (const input in inputs) { |
||||
const name = inputs[input].name; |
||||
const value = inputs[input].value; |
||||
if (name == "recursive" && value == "1") { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
function url(inputs, settings) { |
||||
const terms = getTerms(inputs); |
||||
const recurse = getRecurse(inputs); |
||||
const params = getParams(settings.query_parameter, settings.recurse_parameter); |
||||
for (const index in terms) { |
||||
const term = terms[index]; |
||||
// Do not include terms with no value.
|
||||
if (term.value.length != 0) { |
||||
for (const component in term) { |
||||
const value = term[component]; |
||||
const param = "query[index][component]" |
||||
.replace("query", settings.query_parameter) |
||||
.replace("index", index) |
||||
.replace("component", settings.mapping[component]); |
||||
params[param] = value; |
||||
} |
||||
} |
||||
} |
||||
if (recurse) { |
||||
params[settings.recurse_parameter] = '1'; |
||||
} |
||||
return window.location.href.split("?")[0] + "?" + $.param(params); |
||||
} |
||||
|
||||
Drupal.behaviors.islandora_advanced_search_form = { |
||||
attach: function (context, settings) { |
||||
if (settings.islandora_advanced_search_form.id !== 'undefined') { |
||||
const $form = $('form#' + settings.islandora_advanced_search_form.id).once(); |
||||
if ($form.length > 0) { |
||||
window.addEventListener("pushstate", function (e) { |
||||
$form.attr('action', window.location.pathname + window.location.search); |
||||
}); |
||||
window.addEventListener("popstate", function (e) { |
||||
if (e.state != null) { |
||||
$form.attr('action', window.location.pathname + window.location.search); |
||||
} |
||||
}); |
||||
// Prevent form submission and push state instead.
|
||||
//
|
||||
// Logic server side / client side should match to generate the
|
||||
// appropriate URL with javascript enabled or disable.
|
||||
//
|
||||
// If a route is set for the view display that this form is derived
|
||||
// from, and we are not on the same page as that route, rely on the
|
||||
// normal submit which will redirect to the appropriate page.
|
||||
if (!settings.islandora_advanced_search_form.redirect) { |
||||
$form.submit(function (e) { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
const inputs = $form.serializeArray(); |
||||
const href = url(inputs, settings.islandora_advanced_search_form); |
||||
window.history.pushState(null, document.title, href); |
||||
}); |
||||
} |
||||
// Reset should trigger refresh of AJAX Blocks / Views.
|
||||
$form.find('input[data-drupal-selector = "edit-reset"]').mousedown(function (e) { |
||||
const inputs = []; |
||||
const href = url(inputs, settings.islandora_advanced_search_form); |
||||
window.history.pushState(null, document.title, href); |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
})(jQuery, Drupal, drupalSettings); |
@ -0,0 +1,254 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search; |
||||
|
||||
use Drupal\block\Entity\Block; |
||||
use Drupal\Core\EventSubscriber\MainContentViewSubscriber; |
||||
use Drupal\Core\Form\FormBuilderInterface; |
||||
use Drupal\Core\Url; |
||||
use Drupal\islandora_advanced_search\Form\SettingsForm; |
||||
use Drupal\islandora_advanced_search\Plugin\Block\AdvancedSearchBlock; |
||||
use Drupal\search_api\Query\QueryInterface as DrupalQueryInterface; |
||||
use Drupal\views\ViewExecutable; |
||||
use Solarium\Core\Query\QueryInterface as SolariumQueryInterface; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
|
||||
/** |
||||
* Alter current search query / view from using URL parameters. |
||||
*/ |
||||
class AdvancedSearchQuery { |
||||
|
||||
use GetConfigTrait; |
||||
|
||||
// User can set this configuration for the module. |
||||
const DEFAULT_QUERY_PARAM = 'a'; |
||||
const DEFAULT_RECURSE_PARAM = 'r'; |
||||
|
||||
/** |
||||
* The query parameter is how terms are passed to the query. |
||||
* |
||||
* @var string |
||||
*/ |
||||
protected $queryParameter; |
||||
|
||||
/** |
||||
* The recurse parameter indicates the search should be recursive or not. |
||||
* |
||||
* @var string |
||||
*/ |
||||
protected $recurseParameter; |
||||
|
||||
/** |
||||
* Constructs a FacetBlockAjaxController object. |
||||
* |
||||
* @param string $query_parameter |
||||
* The field to search against. |
||||
* @param string $recurse_parameter |
||||
* The field that signifies the search should be recursive. |
||||
*/ |
||||
public function __construct(string $query_parameter = self::DEFAULT_QUERY_PARAM, string $recurse_parameter = self::DEFAULT_RECURSE_PARAM) { |
||||
$this->queryParameter = $query_parameter; |
||||
$this->recurseParameter = $recurse_parameter; |
||||
} |
||||
|
||||
/** |
||||
* Gets the query parameter to use that stores the search terms. |
||||
* |
||||
* @return string |
||||
* The query parameter to use that stores the search terms. |
||||
*/ |
||||
public static function getQueryParameter() { |
||||
return self::getConfig(SettingsForm::SEARCH_QUERY_PARAMETER, self::DEFAULT_QUERY_PARAM); |
||||
} |
||||
|
||||
/** |
||||
* Gets the query parameter to use that stores the search terms. |
||||
* |
||||
* @return string |
||||
* The recurse parameter used to indicate that the search should be |
||||
* recursive. |
||||
*/ |
||||
public static function getRecurseParameter() { |
||||
return self::getConfig(SettingsForm::SEARCH_RECURSIVE_PARAMETER, self::DEFAULT_RECURSE_PARAM); |
||||
} |
||||
|
||||
/** |
||||
* Extracts a list of AdvancedSearchQueryTerms from the given request. |
||||
* |
||||
* @param \Symfony\Component\HttpFoundation\Request $request |
||||
* The request to parse terms from. |
||||
* |
||||
* @return \Drupal\islandora_advanced_search\AdvancedSearchQueryTerm[] |
||||
* A list of search terms. |
||||
*/ |
||||
public function getTerms(Request $request) { |
||||
$terms = []; |
||||
if ($request->query->has($this->queryParameter)) { |
||||
$query_params = $request->query->get($this->queryParameter); |
||||
if (is_array($query_params)) { |
||||
foreach ($query_params as $params) { |
||||
$terms[] = AdvancedSearchQueryTerm::fromQueryParams($params); |
||||
} |
||||
} |
||||
} |
||||
return array_filter($terms); |
||||
} |
||||
|
||||
/** |
||||
* Checks if the query should recursively include sub-collections. |
||||
* |
||||
* @param \Symfony\Component\HttpFoundation\Request $request |
||||
* The request to parse. |
||||
* |
||||
* @return bool |
||||
* TRUE if the search should recurse FALSE otherwise. |
||||
*/ |
||||
public function shouldRecurse(Request $request) { |
||||
if ($request->query->has($this->recurseParameter)) { |
||||
$recurse_param = $request->query->get($this->recurseParameter); |
||||
return filter_var($recurse_param, FILTER_VALIDATE_BOOLEAN); |
||||
} |
||||
return FALSE; |
||||
} |
||||
|
||||
/** |
||||
* Checks if the all of the given terms are negations or not. |
||||
* |
||||
* @param \Drupal\islandora_advanced_search\AdvancedSearchQueryTerm[] $terms |
||||
* The terms to search for. |
||||
* |
||||
* @return bool |
||||
* TRUE if all terms are to be excluded otherwise FALSE. |
||||
*/ |
||||
protected function negativeQuery(array $terms) { |
||||
foreach ($terms as $term) { |
||||
if ($term->getInclude()) { |
||||
return FALSE; |
||||
} |
||||
} |
||||
return TRUE; |
||||
} |
||||
|
||||
/** |
||||
* Alters the given query using search terms provided in the given request. |
||||
* |
||||
* @param \Symfony\Component\HttpFoundation\Request $request |
||||
* The request to parse terms from. |
||||
* @param \Solarium\Core\Query\QueryInterface $solarium_query |
||||
* The solr query to modify. |
||||
* @param \Drupal\search_api\Query\QueryInterface $search_api_query |
||||
* The search api query from which the solr query was build. |
||||
*/ |
||||
public function alterQuery(Request $request, SolariumQueryInterface &$solarium_query, DrupalQueryInterface $search_api_query) { |
||||
// Only apply if a Advanced Search Query was made. |
||||
$terms = $this->getTerms($request); |
||||
if (!empty($terms)) { |
||||
$index = $search_api_query->getIndex(); |
||||
/** @var \Drupal\search_api_solr\Plugin\search_api\backend\SearchApiSolrBackend $backend */ |
||||
$backend = $index->getServerInstance()->getBackend(); |
||||
$language_ids = $search_api_query->getLanguages(); |
||||
$field_mapping = $backend->getSolrFieldNamesKeyedByLanguage($language_ids, $index); |
||||
$q[] = "{!boost b=boost_document}"; |
||||
// To support negative queries we must first bring in all documents. |
||||
$q[] = $this->negativeQuery($terms) ? "*:*" : ""; |
||||
$term = array_shift($terms); |
||||
$q[] = $term->toSolrQuery($field_mapping); |
||||
foreach ($terms as $term) { |
||||
$q[] = $term->getConjunction(); |
||||
$q[] = $term->toSolrQuery($field_mapping); |
||||
} |
||||
$q = implode(' ', $q); |
||||
/** @var Solarium\QueryType\Select\Query\Query $solarium_query */ |
||||
$solarium_query->setQuery($q); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Alters the given view to be recursive if applicable. |
||||
* |
||||
* @param \Symfony\Component\HttpFoundation\Request $request |
||||
* The request to parse terms from. |
||||
* @param \Drupal\views\ViewExecutable $view |
||||
* The view to modify. |
||||
* @param string $display_id |
||||
* The view display to potentially alter. |
||||
*/ |
||||
public function alterView(Request $request, ViewExecutable $view, $display_id) { |
||||
$views = Utilities::getAdvancedSearchViewDisplays(); |
||||
// Only specify contextual filters for views which the advanced search |
||||
// blocks are derived from. |
||||
$block_id = array_search([$view->id(), $display_id], $views); |
||||
if ($block_id !== FALSE) { |
||||
$block = Block::load($block_id); |
||||
$settings = $block->get('settings'); |
||||
// Ignore the immediate children contextual filter in the query to allow |
||||
// for recursive search. |
||||
if (isset($settings[AdvancedSearchBlock::SETTING_CONTEXTUAL_FILTER])) { |
||||
$display = $view->getDisplay(); |
||||
$display_arguments = $display->getOption('arguments'); |
||||
$immediate_children_contextual_filter = $settings[AdvancedSearchBlock::SETTING_CONTEXTUAL_FILTER]; |
||||
$index = array_search($immediate_children_contextual_filter, array_keys($display_arguments)); |
||||
if ($this->shouldRecurse($request)) { |
||||
// Change the argument to the exception value which should cause the |
||||
// contextual filter to be ignored. |
||||
$view->args[$index] = $display_arguments[$immediate_children_contextual_filter]['exception']['value']; |
||||
} |
||||
else { |
||||
// Explicitly set the default argument for AJAX requests. |
||||
// We need to restore the default as that functionality is currently |
||||
// broken. @see https://www.drupal.org/project/drupal/issues/3173778 |
||||
// |
||||
// We fake the current request from the refer only to set the default |
||||
// argument in case it is build from the URL. If this is not an AJAX |
||||
// request this logic can be ignored. |
||||
if ($request->isXmlHttpRequest()) { |
||||
$view->initHandlers(); |
||||
$request_stack = \Drupal::requestStack(); |
||||
$refer = Request::create($request->server->get('HTTP_REFERER')); |
||||
$refer->getPathInfo(); |
||||
$refer->attributes->add(\Drupal::getContainer()->get('router')->matchRequest($refer)); |
||||
$request_stack->push($refer); |
||||
$plugin = $view->argument[$immediate_children_contextual_filter]->getPlugin('argument_default'); |
||||
if ($plugin) { |
||||
$view->args[$index] = $plugin->getArgument(); |
||||
} |
||||
$request_stack->pop(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Get query parameter for all search terms. |
||||
* |
||||
* @return \Drupal\Core\Url |
||||
* Url for the given request combined with search query parameters. |
||||
*/ |
||||
public function toUrl(Request $request, array $terms, bool $recurse, $route = NULL) { |
||||
$query_params = $request->query->all(); |
||||
if ($route) { |
||||
$url = Url::fromRoute($route); |
||||
// The form that built the url may use AJAX, but we are redirecting to a |
||||
// new page, so it should be disabled. |
||||
unset($query_params[FormBuilderInterface::AJAX_FORM_REQUEST]); |
||||
unset($query_params[MainContentViewSubscriber::WRAPPER_FORMAT]); |
||||
} |
||||
else { |
||||
$url = Url::createFromRequest($request); |
||||
} |
||||
unset($query_params[$this->queryParameter]); |
||||
foreach ($terms as $term) { |
||||
$query_params[$this->queryParameter][] = $term->toQueryParams(); |
||||
} |
||||
if ($recurse) { |
||||
$query_params[$this->recurseParameter] = '1'; |
||||
} |
||||
else { |
||||
unset($query_params[$this->recurseParameter]); |
||||
} |
||||
$url->setOptions(['query' => $query_params]); |
||||
return $url; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,294 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search; |
||||
|
||||
use Drupal\islandora_advanced_search\Form\AdvancedSearchForm; |
||||
|
||||
/** |
||||
* Defines a single search term. |
||||
* |
||||
* Used for parsing query parameters as well as form submission and generating |
||||
* search queries. |
||||
*/ |
||||
class AdvancedSearchQueryTerm { |
||||
// Conjunctions. |
||||
// @see https://lucene.apache.org/solr/guide/7_1/the-standard-query-parser.html#TheStandardQueryParser-BooleanOperatorsSupportedbytheStandardQueryParser |
||||
const CONJUNCTION_AND = 'AND'; |
||||
const CONJUNCTION_OR = 'OR'; |
||||
|
||||
// Used for serializing / deserializing query parameters. |
||||
// These are also hard-coded in islandora_advanced_search.form.js. |
||||
const CONJUNCTION_QUERY_PARAMETER = 'c'; |
||||
const FIELD_QUERY_PARAMETER = 'f'; |
||||
const INCLUDE_QUERY_PARAMETER = 'i'; |
||||
const VALUE_QUERY_PARAMETER = 'v'; |
||||
|
||||
// Defaults. |
||||
const DEFAULT_CONJUNCTION = self::CONJUNCTION_AND; |
||||
const DEFAULT_INCLUDE = TRUE; |
||||
|
||||
/** |
||||
* The field to search. |
||||
* |
||||
* @var string |
||||
*/ |
||||
protected $field; |
||||
|
||||
/** |
||||
* Include / exclude results where 'value' is in the 'search' term. |
||||
* |
||||
* @var bool |
||||
*/ |
||||
protected $include = TRUE; |
||||
|
||||
/** |
||||
* The value to filter with. |
||||
* |
||||
* @var string |
||||
*/ |
||||
protected $value; |
||||
|
||||
/** |
||||
* The conjunction to use for the condition group – either 'AND' or 'OR'. |
||||
* |
||||
* @var string |
||||
*/ |
||||
protected $conjunction; |
||||
|
||||
/** |
||||
* Constructs a FacetBlockAjaxController object. |
||||
* |
||||
* @param string $field |
||||
* The field to search against. |
||||
* @param string $value |
||||
* The value to search the field with. |
||||
* @param bool $include |
||||
* Limit results to records whose field contains or does not contain the |
||||
* given value. |
||||
* @param string $conjunction |
||||
* The conjunction to apply when combining this search term along with |
||||
* others. |
||||
*/ |
||||
public function __construct(string $field, string $value, bool $include = self::DEFAULT_INCLUDE, string $conjunction = self::DEFAULT_CONJUNCTION) { |
||||
$this->field = $field; |
||||
$this->value = $value; |
||||
switch ($conjunction) { |
||||
case self::CONJUNCTION_AND: |
||||
case self::CONJUNCTION_OR: |
||||
$this->conjunction = $conjunction; |
||||
break; |
||||
|
||||
default: |
||||
throw new \InvalidArgumentException('Invalid value given for argument "conjunction": $conjunction'); |
||||
} |
||||
if ($this->conjunction == self::CONJUNCTION_OR && !$include) { |
||||
throw new \InvalidArgumentException('Excluding terms with the conjunction "OR" is not supported'); |
||||
} |
||||
$this->include = $include; |
||||
} |
||||
|
||||
/** |
||||
* Validate 'include' or fallback to default value. |
||||
* |
||||
* @param string $include |
||||
* The value to cast to a boolean if possible. |
||||
* |
||||
* @return bool |
||||
* The normalized input for 'include' or its default. |
||||
*/ |
||||
protected static function normalizeInclude(string $include) { |
||||
switch (strtoupper($include)) { |
||||
case AdvancedSearchForm::IS_OP: |
||||
return TRUE; |
||||
|
||||
case AdvancedSearchForm::NOT_OP: |
||||
return FALSE; |
||||
|
||||
default: |
||||
$include = filter_var($include, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); |
||||
// Ignore include parameter if invalid and fallback to the default. |
||||
return is_bool($include) ? $include : self::DEFAULT_INCLUDE; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Validate 'conjunction' or fallback to default value. |
||||
* |
||||
* @param string $conjunction |
||||
* The conjunction to validate. |
||||
* |
||||
* @return string |
||||
* The normalized input for 'include' or its default. |
||||
*/ |
||||
protected static function normalizeConjunction(string $conjunction) { |
||||
switch (strtoupper($conjunction)) { |
||||
case self::CONJUNCTION_AND: |
||||
return self::CONJUNCTION_AND; |
||||
|
||||
case self::CONJUNCTION_OR: |
||||
return self::CONJUNCTION_OR; |
||||
|
||||
default: |
||||
return self::DEFAULT_CONJUNCTION; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Creates a AdvancedSearchQueryTerm from the given parameters if possible. |
||||
* |
||||
* @param array $params |
||||
* An array representing the query parameters for a single search term. |
||||
* |
||||
* @return \Drupal\islandora_advanced_search\AdvancedSearchQueryTerm|null |
||||
* An object which represents a valid search term. |
||||
*/ |
||||
public static function fromQueryParams(array $params) { |
||||
// Field & value are required values. We do not check if field is a valid |
||||
// value only that it is non-empty. All other fields will be cast to |
||||
// defaults if they are not valid / missing. |
||||
$has_required_params = isset($params[self::FIELD_QUERY_PARAMETER], $params[self::VALUE_QUERY_PARAMETER]); |
||||
$search_value_empty = isset($params[self::VALUE_QUERY_PARAMETER]) && empty($params[self::VALUE_QUERY_PARAMETER]); |
||||
if (!$has_required_params || $search_value_empty) { |
||||
return NULL; |
||||
} |
||||
$field = $params[self::FIELD_QUERY_PARAMETER]; |
||||
$value = $params[self::VALUE_QUERY_PARAMETER]; |
||||
$include = isset($params[self::INCLUDE_QUERY_PARAMETER]) ? |
||||
$include = self::normalizeInclude($params[self::INCLUDE_QUERY_PARAMETER]) : |
||||
self::DEFAULT_INCLUDE; |
||||
$conjunction = isset($params[self::CONJUNCTION_QUERY_PARAMETER]) ? |
||||
self::normalizeConjunction($params[self::CONJUNCTION_QUERY_PARAMETER]) : |
||||
self::DEFAULT_CONJUNCTION; |
||||
return new self($field, $value, $include, $conjunction); |
||||
} |
||||
|
||||
/** |
||||
* Creates a AdvancedSearchQueryTerm from user submitted form values. |
||||
* |
||||
* @param array $input |
||||
* An array representing the submitted form values for a single search term. |
||||
* |
||||
* @return \Drupal\islandora_advanced_search\AdvancedSearchQueryTerm|null |
||||
* An object which represents a valid search term. |
||||
*/ |
||||
public static function fromUserInput(array $input) { |
||||
// Search field & value are required values we do not check if field is a |
||||
// valid value only that it is non-empty. All other fields will use |
||||
// defaults if they are not valid / missing. |
||||
$has_required_inputs = isset($input[AdvancedSearchForm::SEARCH_FORM_FIELD], $input[AdvancedSearchForm::VALUE_FORM_FIELD]); |
||||
$search_value_empty = isset($input[AdvancedSearchForm::VALUE_FORM_FIELD]) && empty($input[AdvancedSearchForm::VALUE_FORM_FIELD]); |
||||
if (!$has_required_inputs || $search_value_empty) { |
||||
return NULL; |
||||
} |
||||
$field = $input[AdvancedSearchForm::SEARCH_FORM_FIELD]; |
||||
$value = $input[AdvancedSearchForm::VALUE_FORM_FIELD]; |
||||
$include = self::DEFAULT_INCLUDE; |
||||
$conjunction = self::DEFAULT_CONJUNCTION; |
||||
if (isset($input[AdvancedSearchForm::CONJUNCTION_FORM_FIELD])) { |
||||
switch ($input[AdvancedSearchForm::CONJUNCTION_FORM_FIELD]) { |
||||
case AdvancedSearchForm::AND_OP: |
||||
$conjunction = self::CONJUNCTION_AND; |
||||
break; |
||||
|
||||
case AdvancedSearchForm::OR_OP: |
||||
$conjunction = self::CONJUNCTION_OR; |
||||
break; |
||||
} |
||||
} |
||||
// Only allow users to specify include when using 'AND' conjunction. |
||||
if ( |
||||
$conjunction == self::CONJUNCTION_AND |
||||
&& isset($input[AdvancedSearchForm::INCLUDE_FORM_FIELD]) |
||||
) { |
||||
switch ($input[AdvancedSearchForm::INCLUDE_FORM_FIELD]) { |
||||
case AdvancedSearchForm::IS_OP: |
||||
$include = TRUE; |
||||
break; |
||||
|
||||
case AdvancedSearchForm::NOT_OP: |
||||
$include = FALSE; |
||||
break; |
||||
} |
||||
} |
||||
return new self($field, $value, $include, $conjunction); |
||||
} |
||||
|
||||
/** |
||||
* Get query parameter representation of this search term. |
||||
* |
||||
* @return array |
||||
* Representation of this search term which can be serialized to a query |
||||
* parameter. |
||||
*/ |
||||
public function toQueryParams() { |
||||
$params = [ |
||||
self::FIELD_QUERY_PARAMETER => $this->field, |
||||
self::VALUE_QUERY_PARAMETER => $this->value, |
||||
]; |
||||
// No need to specify conjunction if it is equivalent to the default. |
||||
if ($this->conjunction != self::DEFAULT_CONJUNCTION) { |
||||
$params[self::CONJUNCTION_QUERY_PARAMETER] = $this->conjunction; |
||||
} |
||||
if ($this->include != self::DEFAULT_CONJUNCTION) { |
||||
$params[self::INCLUDE_QUERY_PARAMETER] = $this->include ? '1' : '0'; |
||||
} |
||||
return $params; |
||||
} |
||||
|
||||
/** |
||||
* Get user input of search form representation of this search term. |
||||
* |
||||
* @return array |
||||
* Representation of this search term which can be used as input to the |
||||
* advanced search form. |
||||
*/ |
||||
public function toUserInput() { |
||||
return [ |
||||
AdvancedSearchForm::SEARCH_FORM_FIELD => $this->field, |
||||
AdvancedSearchForm::VALUE_FORM_FIELD => $this->value, |
||||
AdvancedSearchForm::INCLUDE_FORM_FIELD => $this->include ? AdvancedSearchForm::IS_OP : AdvancedSearchForm::NOT_OP, |
||||
AdvancedSearchForm::CONJUNCTION_FORM_FIELD => $this->conjunction == self::CONJUNCTION_AND ? AdvancedSearchForm::AND_OP : AdvancedSearchForm::OR_OP, |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* Gets if this term should be included / excluded from results. |
||||
* |
||||
* @return bool |
||||
* TRUE if the term should be include in results, FALSE otherwise. |
||||
*/ |
||||
public function getInclude() { |
||||
return $this->include; |
||||
} |
||||
|
||||
/** |
||||
* Gets the conjunction for this term. |
||||
* |
||||
* @return string |
||||
* The conjunction to use for this term. |
||||
*/ |
||||
public function getConjunction() { |
||||
return $this->conjunction; |
||||
} |
||||
|
||||
/** |
||||
* Using the provided field mapping create a Solr Query string. |
||||
* |
||||
* @param array $solr_field_mapping |
||||
* An array that maps search api fields to one or more solr fields. |
||||
* |
||||
* @return string |
||||
* The conjunction to use for this term conjunction. |
||||
*/ |
||||
public function toSolrQuery(array $solr_field_mapping) { |
||||
$terms = []; |
||||
$query_helper = \Drupal::service('solarium.query_helper'); |
||||
$value = $query_helper->escapePhrase(trim($this->value)); |
||||
foreach ($solr_field_mapping[$this->field] as $field) { |
||||
$terms[] = "$field:$value"; |
||||
} |
||||
$terms = implode(' ', $terms); |
||||
return $this->include ? "($terms)" : "-($terms)"; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,165 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Controller; |
||||
|
||||
use Drupal\Core\Ajax\AjaxResponse; |
||||
use Drupal\Core\Ajax\ReplaceCommand; |
||||
use Drupal\Core\Controller\ControllerBase; |
||||
use Drupal\Core\Path\CurrentPathStack; |
||||
use Drupal\Core\PathProcessor\PathProcessorManager; |
||||
use Drupal\Core\Render\RendererInterface; |
||||
use Drupal\Core\Routing\CurrentRouteMatch; |
||||
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
use Symfony\Component\HttpFoundation\RequestStack; |
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; |
||||
use Symfony\Component\Routing\RouterInterface; |
||||
|
||||
/** |
||||
* Defines a controller to load a facet via AJAX. |
||||
*/ |
||||
class AjaxBlocksController extends ControllerBase { |
||||
|
||||
/** |
||||
* The entity storage for block. |
||||
* |
||||
* @var \Drupal\Core\Entity\EntityStorageInterface |
||||
*/ |
||||
protected $storage; |
||||
|
||||
/** |
||||
* The renderer. |
||||
* |
||||
* @var \Drupal\Core\Render\RendererInterface |
||||
*/ |
||||
protected $renderer; |
||||
|
||||
/** |
||||
* The current path. |
||||
* |
||||
* @var \Drupal\Core\Path\CurrentPathStack |
||||
*/ |
||||
protected $currentPath; |
||||
|
||||
/** |
||||
* The dynamic router service. |
||||
* |
||||
* @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface |
||||
*/ |
||||
protected $router; |
||||
|
||||
/** |
||||
* The path processor service. |
||||
* |
||||
* @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface |
||||
*/ |
||||
protected $pathProcessor; |
||||
|
||||
/** |
||||
* The current route match service. |
||||
* |
||||
* @var \Drupal\Core\Routing\CurrentRouteMatch |
||||
*/ |
||||
protected $currentRouteMatch; |
||||
|
||||
/** |
||||
* The service container this instance should use. |
||||
* |
||||
* @var \Symfony\Component\DependencyInjection\ContainerInterface |
||||
*/ |
||||
protected $container; |
||||
|
||||
/** |
||||
* Constructs a FacetBlockAjaxController object. |
||||
* |
||||
* @param \Drupal\Core\Render\RendererInterface $renderer |
||||
* The renderer service. |
||||
* @param \Drupal\Core\Path\CurrentPathStack $currentPath |
||||
* The current path service. |
||||
* @param \Symfony\Component\Routing\RouterInterface $router |
||||
* The router service. |
||||
* @param \Drupal\Core\PathProcessor\PathProcessorManager $pathProcessor |
||||
* The path processor manager. |
||||
* @param \Drupal\Core\Routing\CurrentRouteMatch $currentRouteMatch |
||||
* The current route match service. |
||||
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container |
||||
* The drupal container. |
||||
*/ |
||||
public function __construct(RendererInterface $renderer, CurrentPathStack $currentPath, RouterInterface $router, PathProcessorManager $pathProcessor, CurrentRouteMatch $currentRouteMatch, ContainerInterface $container) { |
||||
$this->storage = $this->entityTypeManager()->getStorage('block'); |
||||
$this->renderer = $renderer; |
||||
$this->currentPath = $currentPath; |
||||
$this->router = $router; |
||||
$this->pathProcessor = $pathProcessor; |
||||
$this->currentRouteMatch = $currentRouteMatch; |
||||
$this->container = $container; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public static function create(ContainerInterface $container) { |
||||
return new static( |
||||
$container->get('renderer'), |
||||
$container->get('path.current'), |
||||
$container->get('router'), |
||||
$container->get('path_processor_manager'), |
||||
$container->get('current_route_match'), |
||||
$container |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Loads and renders the facet blocks via AJAX. |
||||
* |
||||
* @param \Symfony\Component\HttpFoundation\Request $request |
||||
* The current request object. |
||||
* |
||||
* @return \Drupal\Core\Ajax\AjaxResponse |
||||
* The ajax response. |
||||
* |
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException |
||||
* Thrown when the view was not found. |
||||
*/ |
||||
public function respond(Request $request) { |
||||
$response = new AjaxResponse(); |
||||
|
||||
// Rebuild the request and the current path, needed for facets. |
||||
$path = $request->request->get('link'); |
||||
$blocks = $request->request->get('blocks'); |
||||
|
||||
// Make sure we are not updating blocks multiple times. |
||||
$blocks = array_unique($blocks); |
||||
|
||||
if (empty($path) || empty($blocks)) { |
||||
throw new NotFoundHttpException('No facet link or facet blocks found.'); |
||||
} |
||||
|
||||
$new_request = Request::create($path); |
||||
$request_stack = new RequestStack(); |
||||
$processed = $this->pathProcessor->processInbound($new_request->getPathInfo(), $new_request); |
||||
|
||||
$this->currentPath->setPath($processed); |
||||
$request->attributes->add($this->router->matchRequest($new_request)); |
||||
$this->currentRouteMatch->resetRouteMatch(); |
||||
$request_stack->push($new_request); |
||||
$this->container->set('request_stack', $request_stack); |
||||
|
||||
// Build the facets blocks found for the current request and update. |
||||
foreach ($blocks as $block_id => $block_selector) { |
||||
$block_entity = $this->storage->load($block_id); |
||||
|
||||
if ($block_entity) { |
||||
// Render a block, then add it to the response as a replace command. |
||||
$block_view = $this->entityTypeManager |
||||
->getViewBuilder('block') |
||||
->view($block_entity); |
||||
|
||||
$block_view = (string) $this->renderer->renderPlain($block_view); |
||||
$response->addCommand(new ReplaceCommand($block_selector, $block_view)); |
||||
} |
||||
} |
||||
return $response; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,423 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Form; |
||||
|
||||
use Drupal\Component\Utility\Html; |
||||
use Drupal\Core\Form\FormBase; |
||||
use Drupal\Core\Form\FormStateInterface; |
||||
use Drupal\Core\Routing\RouteMatchInterface; |
||||
use Drupal\Core\StringTranslation\TranslatableMarkup; |
||||
use Drupal\islandora_advanced_search\AdvancedSearchQuery; |
||||
use Drupal\islandora_advanced_search\AdvancedSearchQueryTerm; |
||||
use Drupal\islandora_advanced_search\GetConfigTrait; |
||||
use Drupal\views\DisplayPluginCollection; |
||||
use Drupal\views\Entity\View; |
||||
use Drupal\views\Plugin\views\display\PathPluginBase; |
||||
use Drupal\views\Views; |
||||
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
|
||||
/** |
||||
* Form for building and Advanced Search Query. |
||||
*/ |
||||
class AdvancedSearchForm extends FormBase { |
||||
use GetConfigTrait; |
||||
|
||||
// Users can customize the operator to use font-awesome or some other icons. |
||||
// Its a limitation in the use of `input type=submit` rather than buttons in |
||||
// Drupal that we couldn't just rely on CSS. |
||||
// This is exposed in the module settings. |
||||
// @see https://www.drupal.org/project/drupal/issues/1671190 |
||||
const DEFAULT_ADD_OP = '+'; |
||||
const DEFAULT_REMOVE_OP = '-'; |
||||
|
||||
const AND_OP = 'AND'; |
||||
const IS_OP = 'IS'; |
||||
const NOT_OP = 'NOT'; |
||||
const OR_OP = 'OR'; |
||||
|
||||
// These are also hard-coded in islandora_advanced_search.form.js. |
||||
const CONJUNCTION_FORM_FIELD = 'conjunction'; |
||||
const SEARCH_FORM_FIELD = 'search'; |
||||
const INCLUDE_FORM_FIELD = 'include'; |
||||
const VALUE_FORM_FIELD = 'value'; |
||||
|
||||
const AJAX_WRAPPER = 'advanced-search-ajax'; |
||||
|
||||
/** |
||||
* The current request. |
||||
* |
||||
* @var \Symfony\Component\HttpFoundation\Request |
||||
*/ |
||||
protected $request; |
||||
|
||||
/** |
||||
* The current route match. |
||||
* |
||||
* @var \Drupal\Core\Routing\RouteMatchInterface |
||||
*/ |
||||
protected $currentRouteMatch; |
||||
|
||||
/** |
||||
* Class constructor. |
||||
*/ |
||||
public function __construct(Request $request, RouteMatchInterface $current_route_match) { |
||||
$this->request = $request; |
||||
$this->currentRouteMatch = $current_route_match; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public static function create(ContainerInterface $container) { |
||||
return new static( |
||||
$container->get('request_stack')->getMainRequest(), |
||||
$container->get('current_route_match') |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function getFormId() { |
||||
return 'islandora_advanced_search_form'; |
||||
} |
||||
|
||||
/** |
||||
* Get the character to use for adding a facet to the query. |
||||
* |
||||
* @return string |
||||
* The character to use for adding an facet to the query. |
||||
*/ |
||||
public static function getAddOperator() { |
||||
return self::getConfig(SettingsForm::SEARCH_ADD_OPERATOR, self::DEFAULT_ADD_OP); |
||||
} |
||||
|
||||
/** |
||||
* Get the character to use for removing a facet from the query. |
||||
* |
||||
* @return string |
||||
* The character to use for removing an facet to the query. |
||||
*/ |
||||
public static function getRemoveOperator() { |
||||
return self::getConfig(SettingsForm::SEARCH_REMOVE_OPERATOR, self::DEFAULT_REMOVE_OP); |
||||
} |
||||
|
||||
/** |
||||
* Convert the list of fields to select options. |
||||
* |
||||
* @param \Drupal\search_api\Item\FieldInterface[] $fields |
||||
* The fields to convert to select options. |
||||
* |
||||
* @return array |
||||
* Array of fields which can be searched where the key is the search field |
||||
* identifier and the value is its human readable label. |
||||
*/ |
||||
protected function fieldOptions(array $fields) { |
||||
$options = []; |
||||
foreach ($fields as $field) { |
||||
$id = $field->getFieldIdentifier(); |
||||
$options[$id] = $field->getLabel(); |
||||
} |
||||
return $options; |
||||
} |
||||
|
||||
/** |
||||
* Gets possible include options for the given conjunction. |
||||
*/ |
||||
protected function includeOptions(string $conjunction) { |
||||
switch ($conjunction) { |
||||
case self::AND_OP: |
||||
return; |
||||
|
||||
case self::OR_OP: |
||||
return [ |
||||
self::IS_OP => $this->t('is'), |
||||
]; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Default values to for a term. |
||||
*/ |
||||
protected function defaultTermValues(array $options) { |
||||
return [ |
||||
self::CONJUNCTION_FORM_FIELD => self::AND_OP, |
||||
// First item in list is default. |
||||
self::SEARCH_FORM_FIELD => key($options), |
||||
self::INCLUDE_FORM_FIELD => self::IS_OP, |
||||
self::VALUE_FORM_FIELD => NULL, |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* Process input to the from either URL parameters or from the form input. |
||||
*/ |
||||
protected function processInput(FormStateInterface $form_state, array $term_default_values) { |
||||
$input = $form_state->getUserInput(); |
||||
$recursive = isset($input['recursive']) ? $input['recursive'] : NULL; |
||||
$term_values = isset($input['terms']) && is_array($input['terms']) ? $input['terms'] : []; |
||||
// Form was not submitted see if we can rebuild from query parameters. |
||||
$advanced_search_query = new AdvancedSearchQuery(); |
||||
if (empty($term_values)) { |
||||
$terms = $advanced_search_query->getTerms($this->request); |
||||
foreach ($terms as $term) { |
||||
$term_values[] = $term->toUserInput(); |
||||
} |
||||
} |
||||
if (!isset($input['recursive'])) { |
||||
$recursive = $advanced_search_query->shouldRecurse($this->request); |
||||
} |
||||
// Form was submitted via +/- operators. |
||||
$trigger = $form_state->getTriggeringElement(); |
||||
if ($trigger != NULL) { |
||||
$term_index = $trigger['#term_index'] ?? 0; |
||||
$value = $trigger['#value'] instanceof TranslatableMarkup ? |
||||
$trigger['#value']->getUntranslatedString() : |
||||
$trigger['#value']; |
||||
switch ($value) { |
||||
case $this->getAddOperator(): |
||||
// Insert after the term listed. |
||||
array_splice($term_values, $term_index + 1, 0, [$term_default_values]); |
||||
break; |
||||
|
||||
case $this->getRemoveOperator(): |
||||
array_splice($term_values, $term_index, 1); |
||||
break; |
||||
|
||||
case "Reset": |
||||
$recursive = FALSE; |
||||
$term_values = []; |
||||
break; |
||||
|
||||
// Ignore unknown value for trigger. |
||||
} |
||||
// Place user input with updated values. |
||||
$input['terms'] = $term_values; |
||||
$input['recursive'] = $recursive; |
||||
$form_state->setUserInput($input); |
||||
} |
||||
return [$recursive, $term_values]; |
||||
} |
||||
|
||||
/** |
||||
* Gets the route name for the view display used to derive this forms block. |
||||
* |
||||
* @return string|null |
||||
* The route name for the view display that was used to create this |
||||
* forms block. |
||||
*/ |
||||
protected function getRouteName(FormStateInterface $form_state) { |
||||
$view = $form_state->get('view'); |
||||
$display = $form_state->get('display'); |
||||
$display_handlers = new DisplayPluginCollection($view->getExecutable(), Views::pluginManager('display')); |
||||
$display_handler = $display_handlers->get($display['id']); |
||||
if ($display_handler instanceof PathPluginBase) { |
||||
return $display_handler->getRouteName(); |
||||
} |
||||
return NULL; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function buildForm(array $form, FormStateInterface $form_state, View $view = NULL, array $display = [], array $fields = [], string $context_filter = NULL) { |
||||
// Keep reference to view and display as the submit handler may use them |
||||
// to redirect the user to the search page. |
||||
$form_state->set('view', $view); |
||||
$form_state->set('display', $display); |
||||
$route_name = $this->getRouteName($form_state); |
||||
$requires_redirect = $route_name ? $this->currentRouteMatch->getRouteName() !== $route_name : FALSE; |
||||
|
||||
$form['#attached']['library'][] = 'islandora_advanced_search/advanced.search.form'; |
||||
$form['#attached']['drupalSettings']['islandora_advanced_search_form'] = [ |
||||
'id' => Html::getId($this->getFormId()), |
||||
'redirect' => $requires_redirect, |
||||
'query_parameter' => AdvancedSearchQuery::getQueryParameter(), |
||||
'recurse_parameter' => AdvancedSearchQuery::getRecurseParameter(), |
||||
'mapping' => [ |
||||
self::CONJUNCTION_FORM_FIELD => AdvancedSearchQueryTerm::CONJUNCTION_QUERY_PARAMETER, |
||||
self::SEARCH_FORM_FIELD => AdvancedSearchQueryTerm::FIELD_QUERY_PARAMETER, |
||||
self::INCLUDE_FORM_FIELD => AdvancedSearchQueryTerm::INCLUDE_QUERY_PARAMETER, |
||||
self::VALUE_FORM_FIELD => AdvancedSearchQueryTerm::VALUE_QUERY_PARAMETER, |
||||
], |
||||
]; |
||||
|
||||
$options = $this->fieldOptions($fields); |
||||
$term_default_values = $this->defaultTermValues($options); |
||||
list($recursive, $term_values) = $this->processInput($form_state, $term_default_values); |
||||
$i = 0; |
||||
$term_elements = []; |
||||
$total_terms = count($term_values); |
||||
$block_class_prefix = str_replace('_', '-', $this->getFormId()); |
||||
do { |
||||
// Either specified by the user in the request or use the default. |
||||
$first = $i == 0; |
||||
$term_value = !empty($term_values) ? array_shift($term_values) : $term_default_values; |
||||
$conjunction = isset($term_value[self::CONJUNCTION_FORM_FIELD]) ? $term_value[self::CONJUNCTION_FORM_FIELD] : $term_default_values[self::CONJUNCTION_FORM_FIELD]; |
||||
$term_elements[] = [ |
||||
// Only show on terms after the first. |
||||
self::CONJUNCTION_FORM_FIELD => $first ? NULL : [ |
||||
'#type' => 'select', |
||||
'#options' => [ |
||||
self::AND_OP => $this->t('and'), |
||||
self::OR_OP => $this->t('or'), |
||||
], |
||||
'#default_value' => $conjunction, |
||||
], |
||||
self::SEARCH_FORM_FIELD => [ |
||||
'#type' => 'select', |
||||
'#options' => $options, |
||||
'#default_value' => $term_value[self::SEARCH_FORM_FIELD], |
||||
], |
||||
self::INCLUDE_FORM_FIELD => [ |
||||
'#type' => 'select', |
||||
'#options' => [ |
||||
self::IS_OP => $this->t('is'), |
||||
self::NOT_OP => $this->t('is not'), |
||||
], |
||||
'#default_value' => $term_value[self::INCLUDE_FORM_FIELD], |
||||
// Show only when conjunction is 'AND' as 'OR NOT' is not supported |
||||
// by solr and will be converted to 'AND NOT'. |
||||
'#states' => [ |
||||
'visible' => [ |
||||
':input[name="terms[' . $i . '][' . self::CONJUNCTION_FORM_FIELD . ']"]' => ['value' => self::AND_OP], |
||||
], |
||||
], |
||||
], |
||||
// Just markup to show when 'include' is not alterable due to the |
||||
// selected 'conjunction'. Hide for the first term. |
||||
'is' => $first ? NULL : [ |
||||
'#type' => 'container', |
||||
'#attributes' => ['style' => 'display:inline;'], |
||||
'#states' => [ |
||||
'visible' => [ |
||||
':input[name="terms[' . $i . '][' . self::CONJUNCTION_FORM_FIELD . ']"]' => ['value' => self::OR_OP], |
||||
], |
||||
], |
||||
'content' => [ |
||||
'#markup' => $this->t('is'), |
||||
], |
||||
], |
||||
self::VALUE_FORM_FIELD => [ |
||||
'#type' => 'textfield', |
||||
'#default_value' => $term_value[self::VALUE_FORM_FIELD], |
||||
], |
||||
'actions' => [ |
||||
'#type' => 'container', |
||||
'add' => [ |
||||
'#type' => 'button', |
||||
'#value' => $this->getAddOperator(), |
||||
'#name' => 'add-term-' . $i, |
||||
'#term_index' => $i, |
||||
'#attributes' => [ |
||||
'class' => [$block_class_prefix . '__add', 'fa'], |
||||
], |
||||
'#ajax' => [ |
||||
'callback' => [$this, 'ajaxCallback'], |
||||
'wrapper' => self::AJAX_WRAPPER, |
||||
'progress' => [ |
||||
'type' => 'none', |
||||
], |
||||
], |
||||
], |
||||
'remove' => $total_terms <= 1 ? NULL : [ |
||||
'#type' => 'button', |
||||
'#value' => $this->getRemoveOperator(), |
||||
'#name' => 'remove-term-' . $i, |
||||
'#term_index' => $i, |
||||
'#attributes' => [ |
||||
'class' => [$block_class_prefix . '__remove', 'fa'], |
||||
], |
||||
'#ajax' => [ |
||||
'callback' => [$this, 'ajaxCallback'], |
||||
'wrapper' => self::AJAX_WRAPPER, |
||||
'progress' => [ |
||||
'type' => 'none', |
||||
], |
||||
], |
||||
], |
||||
], |
||||
]; |
||||
$i++; |
||||
} while (!empty($term_values)); |
||||
|
||||
$form['ajax'] = [ |
||||
'#type' => 'container', |
||||
'#attributes' => ['id' => self::AJAX_WRAPPER], |
||||
'terms' => array_merge([ |
||||
'#tree' => TRUE, |
||||
'#type' => 'container', |
||||
], $term_elements), |
||||
]; |
||||
|
||||
if ($context_filter != NULL) { |
||||
$form['ajax']['recursive'] = [ |
||||
'#type' => 'checkbox', |
||||
'#title' => $this->t('Include Sub-Collections'), |
||||
'#default_value' => $recursive, |
||||
]; |
||||
} |
||||
$form['reset'] = [ |
||||
'#type' => 'button', |
||||
'#value' => $this->t('Reset'), |
||||
'#attributes' => [ |
||||
'class' => [$block_class_prefix . '__reset'], |
||||
], |
||||
'#ajax' => [ |
||||
'callback' => [$this, 'ajaxCallback'], |
||||
'wrapper' => self::AJAX_WRAPPER, |
||||
'progress' => [ |
||||
'type' => 'none', |
||||
], |
||||
], |
||||
]; |
||||
$form['submit'] = [ |
||||
'#type' => 'submit', |
||||
'#value' => $this->t('Search'), |
||||
'#attributes' => [ |
||||
'class' => [$block_class_prefix . '__search'], |
||||
], |
||||
]; |
||||
return $form; |
||||
} |
||||
|
||||
/** |
||||
* Builds an Advanced Search Query Url from the submitted form values. |
||||
*/ |
||||
protected function buildUrl(FormStateInterface $form_state) { |
||||
$terms = []; |
||||
$values = $form_state->getValues(); |
||||
foreach ($values['terms'] as $term) { |
||||
$terms[] = AdvancedSearchQueryTerm::fromUserInput($term); |
||||
} |
||||
$terms = array_filter($terms); |
||||
$recurse = filter_var(isset($values['recursive']) ? $values['recursive'] : FALSE, FILTER_VALIDATE_BOOLEAN); |
||||
$route = $this->getRouteName($form_state); |
||||
$advanced_search_query = new AdvancedSearchQuery(); |
||||
return $advanced_search_query->toUrl($this->request, $terms, $recurse, $route); |
||||
} |
||||
|
||||
/** |
||||
* Callback for adding / removing terms from the search. |
||||
*/ |
||||
public function ajaxCallback(array &$form, FormStateInterface $form_state) { |
||||
return $form['ajax']; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function submitForm(array &$form, FormStateInterface $form_state) { |
||||
$trigger = (string) $form_state->getTriggeringElement()['#value']; |
||||
switch ($trigger) { |
||||
case $this->t('Search'): |
||||
$form_state->setRedirectUrl($this->buildUrl($form_state)); |
||||
break; |
||||
|
||||
default: |
||||
$form_state->setRebuild(); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,122 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Form; |
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface; |
||||
use Drupal\Core\Form\ConfigFormBase; |
||||
use Drupal\Core\Form\FormStateInterface; |
||||
use Drupal\islandora_advanced_search\AdvancedSearchQuery; |
||||
use Drupal\islandora_advanced_search\GetConfigTrait; |
||||
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||
|
||||
/** |
||||
* Config form for Islandora Advanced Search settings. |
||||
*/ |
||||
class SettingsForm extends ConfigFormBase { |
||||
|
||||
use GetConfigTrait; |
||||
|
||||
const CONFIG_NAME = 'islandora_advanced_search.settings'; |
||||
const SEARCH_QUERY_PARAMETER = 'search_query_parameter'; |
||||
const SEARCH_RECURSIVE_PARAMETER = 'search_recursive_parameter'; |
||||
const SEARCH_ADD_OPERATOR = 'search_add_operator'; |
||||
const SEARCH_REMOVE_OPERATOR = 'search_remove_operator'; |
||||
const FACET_TRUNCATE = 'facet_truncate'; |
||||
|
||||
/** |
||||
* Constructs a \Drupal\system\ConfigFormBase object. |
||||
* |
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory |
||||
* The factory for configuration objects. |
||||
*/ |
||||
public function __construct(ConfigFactoryInterface $config_factory) { |
||||
$this->setConfigFactory($config_factory); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public static function create(ContainerInterface $container) { |
||||
return new static($container->get('config.factory')); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function getFormId() { |
||||
return 'islandora_advanced_search_settings_form'; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
protected function getEditableConfigNames() { |
||||
return [ |
||||
self::CONFIG_NAME, |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function buildForm(array $form, FormStateInterface $form_state) { |
||||
$form += [ |
||||
'search' => [ |
||||
'#type' => 'fieldset', |
||||
'#title' => $this->t('Advanced Search'), |
||||
self::SEARCH_QUERY_PARAMETER => [ |
||||
'#type' => 'textfield', |
||||
'#title' => $this->t('Search Query Parameter'), |
||||
'#description' => $this->t('The url parameter in which the advanced search query is stored.'), |
||||
'#default_value' => AdvancedSearchQuery::getQueryParameter(), |
||||
], |
||||
self::SEARCH_RECURSIVE_PARAMETER => [ |
||||
'#type' => 'textfield', |
||||
'#title' => $this->t('Recurse Query Parameter'), |
||||
'#description' => $this->t('The url parameter which can toggle recursive search.'), |
||||
'#default_value' => AdvancedSearchQuery::getRecurseParameter(), |
||||
], |
||||
self::SEARCH_ADD_OPERATOR => [ |
||||
'#type' => 'textfield', |
||||
'#title' => $this->t('Facet Add Operator'), |
||||
'#description' => $this->t('Users can customize the operator for adding facets to use font-awesome or some other icon, etc.'), |
||||
'#default_value' => AdvancedSearchForm::getAddOperator(), |
||||
], |
||||
self::SEARCH_REMOVE_OPERATOR => [ |
||||
'#type' => 'textfield', |
||||
'#title' => $this->t('Facet Remove Operator'), |
||||
'#description' => $this->t('Users can customize the operator for removing facets to use font-awesome or some other icon, etc.'), |
||||
'#default_value' => AdvancedSearchForm::getRemoveOperator(), |
||||
], |
||||
], |
||||
'facets' => [ |
||||
'#type' => 'fieldset', |
||||
'#title' => $this->t('Facets'), |
||||
self::FACET_TRUNCATE => [ |
||||
'#type' => 'number', |
||||
'#title' => $this->t('Truncate Facet'), |
||||
'#description' => $this->t('Optionally truncate the length of facets titles in the display. If unspecified they will not be truncated.'), |
||||
'#default_value' => self::getConfig(self::FACET_TRUNCATE, 32), |
||||
'#min' => 1, |
||||
], |
||||
], |
||||
]; |
||||
return parent::buildForm($form, $form_state); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function submitForm(array &$form, FormStateInterface $form_state) { |
||||
$config = $this->configFactory->getEditable(self::CONFIG_NAME); |
||||
$config |
||||
->set(self::SEARCH_QUERY_PARAMETER, $form_state->getValue(self::SEARCH_QUERY_PARAMETER)) |
||||
->set(self::SEARCH_RECURSIVE_PARAMETER, $form_state->getValue(self::SEARCH_RECURSIVE_PARAMETER)) |
||||
->set(self::SEARCH_ADD_OPERATOR, $form_state->getValue(self::SEARCH_ADD_OPERATOR)) |
||||
->set(self::SEARCH_REMOVE_OPERATOR, $form_state->getValue(self::SEARCH_REMOVE_OPERATOR)) |
||||
->set(self::FACET_TRUNCATE, $form_state->getValue(self::FACET_TRUNCATE)) |
||||
->save(); |
||||
parent::submitForm($form, $form_state); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,24 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search; |
||||
|
||||
use Drupal\islandora_advanced_search\Form\SettingsForm; |
||||
|
||||
/** |
||||
* Simple trait for accessing this modules configuration. |
||||
*/ |
||||
trait GetConfigTrait { |
||||
|
||||
/** |
||||
* Get a config setting or returns a default. |
||||
* |
||||
* @return string |
||||
* The config setting or default value. |
||||
*/ |
||||
protected static function getConfig($config, $default) { |
||||
$settings = \Drupal::config(SettingsForm::CONFIG_NAME); |
||||
$value = $settings->get($config); |
||||
return !empty($value) ? $value : $default; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,394 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\Block; |
||||
|
||||
use Drupal\Core\Block\BlockBase; |
||||
use Drupal\Core\Form\FormBuilderInterface; |
||||
use Drupal\Core\Form\FormStateInterface; |
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface; |
||||
use Drupal\search_api\Display\DisplayPluginManager; |
||||
use Drupal\views\Entity\View; |
||||
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
|
||||
/** |
||||
* Provides an Islandora Advanced Search block. |
||||
* |
||||
* @Block( |
||||
* id = "islandora_advanced_search_block", |
||||
* deriver = "Drupal\islandora_advanced_search\Plugin\Block\AdvancedSearchBlockDeriver", |
||||
* admin_label = @Translation("Islandora Advanced Search"), |
||||
* category = @Translation("Islandora"), |
||||
* ) |
||||
*/ |
||||
class AdvancedSearchBlock extends BlockBase implements ContainerFactoryPluginInterface { |
||||
use ViewAndDisplayIdentifiersTrait; |
||||
|
||||
// CSS classes used to bind table-drag behavior to. |
||||
const WEIGHT_FIELD_CLASS = 'field-weight'; |
||||
const DISPLAY_FIELD_CLASS = 'field-display'; |
||||
|
||||
// Regions in the table which denote if a given field |
||||
// is visible in the Advanced Search Form or not. |
||||
const REGION_VISIBLE = 'visible'; |
||||
const REGION_HIDDEN = 'hidden'; |
||||
|
||||
// Keys for settings. |
||||
const SETTING_FIELDS = 'fields'; |
||||
const SETTING_CONTEXTUAL_FILTER = 'context_filter'; |
||||
|
||||
/** |
||||
* The display plugin manager. |
||||
* |
||||
* @var \Drupal\search_api\Display\DisplayPluginManager |
||||
*/ |
||||
protected $displayPluginManager; |
||||
|
||||
/** |
||||
* The clone of the current request object. |
||||
* |
||||
* @var \Symfony\Component\HttpFoundation\Request |
||||
*/ |
||||
protected $request; |
||||
|
||||
/** |
||||
* The view this block affects. |
||||
* |
||||
* @var \Drupal\views\Entity\View |
||||
*/ |
||||
protected $view; |
||||
|
||||
/** |
||||
* The view display this block affects. |
||||
* |
||||
* @var array |
||||
*/ |
||||
protected $display; |
||||
|
||||
/** |
||||
* Form Builder. |
||||
* |
||||
* @var \Drupal\Core\Form\FormBuilderInterface |
||||
*/ |
||||
protected $formBuilder; |
||||
|
||||
/** |
||||
* Construct a AdvancedSearchBlock instance. |
||||
* |
||||
* @param array $configuration |
||||
* A configuration array containing information about the plugin instance. |
||||
* @param string $plugin_id |
||||
* The plugin_id for the plugin instance. |
||||
* @param string $plugin_definition |
||||
* The plugin implementation definition. |
||||
* @param \Drupal\search_api\Display\DisplayPluginManager $display_plugin_manager |
||||
* The display plugin manager. |
||||
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder |
||||
* The form builder service used to build the search form. |
||||
* @param \Symfony\Component\HttpFoundation\Request $request |
||||
* A request object for the current request. |
||||
*/ |
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, DisplayPluginManager $display_plugin_manager, FormBuilderInterface $form_builder, Request $request) { |
||||
parent::__construct($configuration, $plugin_id, $plugin_definition); |
||||
$this->displayPluginManager = $display_plugin_manager; |
||||
list($view_id, $display_id) = preg_split('/__/', $this->getDerivativeId(), 2); |
||||
$this->view = View::Load($view_id); |
||||
$this->display = $this->view->getDisplay($display_id); |
||||
$this->formBuilder = $form_builder; |
||||
$this->request = clone $request; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { |
||||
return new static( |
||||
$configuration, |
||||
$plugin_id, |
||||
$plugin_definition, |
||||
$container->get('plugin.manager.search_api.display'), |
||||
$container->get('form_builder'), |
||||
$container->get('request_stack')->getMainRequest() |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function defaultConfiguration() { |
||||
return [ |
||||
self::SETTING_FIELDS => [], |
||||
self::SETTING_CONTEXTUAL_FILTER => NULL, |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* Fields which can be enabled / disabled for display in the search form. |
||||
* |
||||
* @return \Drupal\search_api\Item\FieldInterface[] |
||||
* The $fields sorted by label. |
||||
*/ |
||||
protected function getFields() { |
||||
$fields = $this->getIndex()->getFields(); |
||||
// First pass sort on label, secondary sort will be used |
||||
// when looking at existing configuration for this block. |
||||
uasort($fields, function ($a, $b) { |
||||
return strcmp($a->getLabel(), $b->getLabel()); |
||||
}); |
||||
return $fields; |
||||
} |
||||
|
||||
/** |
||||
* Get regions of table to display. |
||||
* |
||||
* @return array |
||||
* The properties of each region used for building the table of fields. |
||||
*/ |
||||
protected function getRegions() { |
||||
// Classes for select fields like 'weight' and 'display' are hard-coded |
||||
// and used in js/islandora-advanced-search.admin.js. |
||||
return [ |
||||
'visible' => [ |
||||
'title' => $this->t('Visible'), |
||||
'invisible' => TRUE, |
||||
'message' => $this->t('No search field is visible.'), |
||||
'weight' => self::WEIGHT_FIELD_CLASS . '-visible', |
||||
'display' => self::DISPLAY_FIELD_CLASS . '-visible', |
||||
], |
||||
'hidden' => [ |
||||
'title' => $this->t('Hidden'), |
||||
'invisible' => FALSE, |
||||
'message' => $this->t('No search field is hidden.'), |
||||
'weight' => self::WEIGHT_FIELD_CLASS . '-hidden', |
||||
'display' => self::DISPLAY_FIELD_CLASS . '-hidden', |
||||
], |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* Options for field display derived from the available regions. |
||||
* |
||||
* @return array |
||||
* Display select field options. |
||||
*/ |
||||
protected function getDisplayOptions() { |
||||
$options = []; |
||||
foreach ($this->getRegions() as $region => $settings) { |
||||
$options[$region] = $settings['title']; |
||||
} |
||||
return $options; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function blockForm($form, FormStateInterface $form_state) { |
||||
// At most we will have one row per field. |
||||
$fields = $this->getFields(); |
||||
$weight_delta = round(count($fields) / 2); |
||||
|
||||
// Group each field into a region given our current configuration. |
||||
$visible_fields = $this->configuration[self::SETTING_FIELDS]; |
||||
$regions = $this->getRegions(); |
||||
$display_options = $this->getDisplayOptions(); |
||||
|
||||
// Field rows are grouped by the region in which they are displayed. |
||||
$field_rows = array_fill_keys(array_keys($regions), []); |
||||
foreach ($fields as $field) { |
||||
// If a field exists in the blocks configuration than it is 'visible' and |
||||
// its weight is equivalent to its order in the configuration, |
||||
// i.e. its index. |
||||
$identifier = $field->getFieldIdentifier(); |
||||
$weight = array_search($identifier, $visible_fields); |
||||
$visible = $weight !== FALSE; |
||||
$region = $visible ? self::REGION_VISIBLE : self::REGION_HIDDEN; |
||||
$field_rows[$region][$identifier] = [ |
||||
'#attributes' => [ |
||||
'class' => ['draggable'], |
||||
], |
||||
'label' => ['#plain_text' => $field->getLabel()], |
||||
'identifier' => ['#plain_text' => $identifier], |
||||
'weight' => [ |
||||
'#type' => 'weight', |
||||
'#title' => $this->t('Weight'), |
||||
'#title_display' => 'invisible', |
||||
'#default_value' => $visible ? $weight : 0, |
||||
'#delta' => $weight_delta, |
||||
'#attributes' => [ |
||||
'class' => [self::WEIGHT_FIELD_CLASS, $regions[$region]['weight']], |
||||
], |
||||
], |
||||
'display' => [ |
||||
'#type' => 'select', |
||||
'#title' => $this->t('Display'), |
||||
'#title_display' => 'invisible', |
||||
'#options' => $display_options, |
||||
'#default_value' => $region, |
||||
'#attributes' => [ |
||||
'class' => [self::DISPLAY_FIELD_CLASS, $regions[$region]['display']], |
||||
], |
||||
], |
||||
]; |
||||
} |
||||
// Sort the visible rows by their weight. |
||||
uasort($field_rows[self::REGION_VISIBLE], function ($a, $b) { |
||||
$a = $a['weight']['#default_value']; |
||||
$b = $b['weight']['#default_value']; |
||||
if ($a == $b) { |
||||
return 0; |
||||
} |
||||
return ($a < $b) ? -1 : 1; |
||||
}); |
||||
|
||||
// Build Rows. |
||||
$rows = []; |
||||
$table_drag = []; |
||||
foreach ($regions as $region => $properties) { |
||||
$rows += [ |
||||
// Conditionally display region title as a row. |
||||
"region-$region" => $properties['invisible'] ? NULL : [ |
||||
'#attributes' => [ |
||||
'class' => ['region-title', "region-title-$region"], |
||||
], |
||||
'label' => [ |
||||
'#plain_text' => $properties['title'], |
||||
'#wrapper_attributes' => [ |
||||
'colspan' => 4, |
||||
], |
||||
], |
||||
], |
||||
// Will dynamically display if the region has fields or not controlled |
||||
// by Drupal behaviors in js/islandora-advanced-search.admin.js. |
||||
"region-$region-message" => [ |
||||
'#attributes' => [ |
||||
'class' => [ |
||||
'region-message', |
||||
"region-$region-message", |
||||
empty($field_rows[$region]) ? 'region-empty' : 'region-populated', |
||||
], |
||||
], |
||||
'message' => [ |
||||
'#markup' => '<em>' . $properties['message'] . '</em>', |
||||
'#wrapper_attributes' => [ |
||||
'colspan' => 4, |
||||
], |
||||
], |
||||
], |
||||
]; |
||||
|
||||
// Include field rows in this region. |
||||
$rows += $field_rows[$region]; |
||||
|
||||
// Configure order by weight field in region. |
||||
$table_drag[] = [ |
||||
'action' => 'order', |
||||
'relationship' => 'sibling', |
||||
'group' => self::WEIGHT_FIELD_CLASS, |
||||
'subgroup' => $properties['weight'], |
||||
'source' => self::WEIGHT_FIELD_CLASS, |
||||
]; |
||||
|
||||
// Configure drag action for display field in region. |
||||
$table_drag[] = [ |
||||
'action' => 'match', |
||||
'relationship' => 'sibling', |
||||
'group' => self::DISPLAY_FIELD_CLASS, |
||||
'subgroup' => $properties['display'], |
||||
'source' => self::DISPLAY_FIELD_CLASS, |
||||
]; |
||||
} |
||||
|
||||
$form[self::SETTING_FIELDS] = [ |
||||
'#type' => 'table', |
||||
'#attributes' => [ |
||||
// Identifier is hard-coded and used in |
||||
// js/islandora-advanced-search.admin.js. |
||||
'id' => 'advanced-search-fields', |
||||
], |
||||
'#header' => [ |
||||
$this->t('Label'), |
||||
$this->t('Field'), |
||||
$this->t('Weight'), |
||||
$this->t('Display'), |
||||
], |
||||
'#empty' => $this->t('No search fields, please check search index configuration.'), |
||||
'#tabledrag' => $table_drag, |
||||
] + $rows; |
||||
|
||||
// If there is contextual filters associated with the display that means |
||||
// we can filter on collection / sub-collection. Allow the user to choose |
||||
// which filters collections. |
||||
$id = NULL; |
||||
$field = NULL; |
||||
$options = []; |
||||
if (isset($this->display['display_options']['arguments'])) { |
||||
foreach ($this->display['display_options']['arguments'] as $context_filter) { |
||||
$id = $context_filter['id']; |
||||
$field = $context_filter['field']; |
||||
if (isset($fields[$field])) { |
||||
$options[$id] = $fields[$field]->getLabel() . ':' . $id; |
||||
} |
||||
} |
||||
} |
||||
if (count($options) > 1) { |
||||
$form[self::SETTING_CONTEXTUAL_FILTER] = [ |
||||
'#type' => 'select', |
||||
'#title' => $this->t('Context Filter'), |
||||
'#description' => $this->t('If more than one <strong>Context Filter</strong> is defined, specify which is used to <strong>include</strong> only <strong>direct children</strong> of the Collection as it will disabled to allow recursive searching.'), |
||||
'#options' => $options, |
||||
'#default_value' => $this->configuration[self::SETTING_CONTEXTUAL_FILTER], |
||||
'#multiple' => FALSE, |
||||
'#required' => TRUE, |
||||
'#size' => count($options) + 1, |
||||
]; |
||||
} |
||||
$form['#attributes']['class'][] = 'clearfix'; |
||||
$form['#attached']['library'][] = 'islandora_advanced_search/advanced.search.admin'; |
||||
return $form; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function blockSubmit($form, FormStateInterface $form_state) { |
||||
$values = $form_state->getValues(); |
||||
$fields = array_filter($values[self::SETTING_FIELDS], function ($field) { |
||||
return $field['display'] == 'visible'; |
||||
}); |
||||
uasort($fields, '\Drupal\Component\Utility\SortArray::sortByWeightElement'); |
||||
$this->configuration[self::SETTING_FIELDS] = array_keys($fields); |
||||
if (isset($values[self::SETTING_CONTEXTUAL_FILTER])) { |
||||
$this->configuration[self::SETTING_CONTEXTUAL_FILTER] = $values[self::SETTING_CONTEXTUAL_FILTER]; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function build() { |
||||
$fields = $this->getIndex()->getFields(); |
||||
$configured_fields = []; |
||||
foreach ($this->configuration[self::SETTING_FIELDS] as $identifier) { |
||||
$configured_fields[$identifier] = $fields[$identifier]; |
||||
} |
||||
return $this->formBuilder->getForm('Drupal\islandora_advanced_search\Form\AdvancedSearchForm', $this->view, $this->display, $configured_fields, $this->configuration[self::SETTING_CONTEXTUAL_FILTER]); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function getCacheMaxAge() { |
||||
// The block cannot be cached, because it must always match the current |
||||
// search results. |
||||
return 0; |
||||
} |
||||
|
||||
/** |
||||
* Get Search Index. |
||||
*/ |
||||
protected function getIndex() { |
||||
$id = $this->getDerivativeId(); |
||||
return $this->displayPluginManager->createInstance("views_{$this->display['display_plugin']}:{$id}")->getIndex(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,17 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\Block; |
||||
|
||||
/** |
||||
* Deriver for AdvancedSearchBlock. |
||||
*/ |
||||
class AdvancedSearchBlockDeriver extends SearchApiDisplayBlockDeriver { |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
protected function label() { |
||||
return $this->t('Advanced Search'); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,97 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\Block; |
||||
|
||||
use Drupal\Component\Plugin\PluginBase; |
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; |
||||
use Drupal\Core\StringTranslation\StringTranslationTrait; |
||||
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||
|
||||
/** |
||||
* This deriver creates a block for every search_api.display. |
||||
*/ |
||||
abstract class SearchApiDisplayBlockDeriver implements ContainerDeriverInterface { |
||||
|
||||
use StringTranslationTrait; |
||||
|
||||
/** |
||||
* List of derivative definitions. |
||||
* |
||||
* @var array |
||||
*/ |
||||
protected $derivatives = []; |
||||
|
||||
/** |
||||
* The entity storage for the view. |
||||
* |
||||
* @var \Drupal\Core\Entity\EntityStorageInterface |
||||
*/ |
||||
protected $storage; |
||||
|
||||
/** |
||||
* The display manager for the search_api. |
||||
* |
||||
* @var \Drupal\search_api\Display\DisplayPluginManager |
||||
*/ |
||||
protected $displayPluginManager; |
||||
|
||||
/** |
||||
* Label for the SearchApiDisplayBlockDriver. |
||||
*/ |
||||
abstract protected function label(); |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public static function create(ContainerInterface $container, $base_plugin_id) { |
||||
$deriver = new static($container, $base_plugin_id); |
||||
$deriver->storage = $container->get('entity_type.manager')->getStorage('view'); |
||||
$deriver->displayPluginManager = $container->get('plugin.manager.search_api.display'); |
||||
return $deriver; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function getDerivativeDefinition($derivative_id, $base_plugin_definition) { |
||||
$derivatives = $this->getDerivativeDefinitions($base_plugin_definition); |
||||
return isset($derivatives[$derivative_id]) ? $derivatives[$derivative_id] : NULL; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function getDerivativeDefinitions($base_plugin_definition) { |
||||
$base_plugin_id = $base_plugin_definition['id']; |
||||
|
||||
if (!isset($this->derivatives[$base_plugin_id])) { |
||||
$plugin_derivatives = []; |
||||
|
||||
foreach ($this->displayPluginManager->getDefinitions() as $display_definition) { |
||||
$view_id = $display_definition['view_id']; |
||||
$view_display = $display_definition['view_display']; |
||||
// The derived block needs both the view / display identifiers to |
||||
// construct the pager. |
||||
$machine_name = "${view_id}__${view_display}"; |
||||
|
||||
/** @var \Drupal\views\ViewEntityInterface $view */ |
||||
$view = $this->storage->load($view_id); |
||||
$display = $view->getDisplay($view_display); |
||||
|
||||
$plugin_derivatives[$machine_name] = [ |
||||
'id' => $base_plugin_id . PluginBase::DERIVATIVE_SEPARATOR . $machine_name, |
||||
'label' => $this->label(), |
||||
'admin_label' => $this->t(':view: :label for :display', [ |
||||
':view' => $view->label(), |
||||
':label' => $this->label(), |
||||
':display' => $display['display_title'], |
||||
]), |
||||
] + $base_plugin_definition; |
||||
} |
||||
|
||||
$this->derivatives[$base_plugin_id] = $plugin_derivatives; |
||||
} |
||||
return $this->derivatives[$base_plugin_id]; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,314 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\Block; |
||||
|
||||
use Drupal\Core\Block\BlockBase; |
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface; |
||||
use Drupal\Core\Render\Markup; |
||||
use Drupal\Core\Url; |
||||
use Drupal\islandora_advanced_search\AdvancedSearchQuery; |
||||
use Drupal\views\Entity\View; |
||||
use Drupal\views\Plugin\views\pager\SqlBase; |
||||
use Drupal\views\ViewExecutable; |
||||
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
|
||||
/** |
||||
* Provides a 'AjaxViewBlock' block. |
||||
* |
||||
* @Block( |
||||
* id = "islandora_advanced_search_result_pager", |
||||
* deriver = "Drupal\islandora_advanced_search\Plugin\Block\SearchResultsPagerBlockDeriver", |
||||
* admin_label = @Translation("Search Results Pager"), |
||||
* category = @Translation("Islandora"), |
||||
* ) |
||||
*/ |
||||
class SearchResultsPagerBlock extends BlockBase implements ContainerFactoryPluginInterface { |
||||
use ViewAndDisplayIdentifiersTrait; |
||||
|
||||
/** |
||||
* The clone of the current request object. |
||||
* |
||||
* @var \Symfony\Component\HttpFoundation\Request |
||||
*/ |
||||
protected $request; |
||||
|
||||
/** |
||||
* Construct a FacetBlock instance. |
||||
* |
||||
* @param array $configuration |
||||
* A configuration array containing information about the plugin instance. |
||||
* @param string $plugin_id |
||||
* The plugin_id for the plugin instance. |
||||
* @param string $plugin_definition |
||||
* The plugin implementation definition. |
||||
* @param \Symfony\Component\HttpFoundation\Request $request |
||||
* A request object for the current request. |
||||
*/ |
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request) { |
||||
parent::__construct($configuration, $plugin_id, $plugin_definition); |
||||
$this->request = clone $request; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { |
||||
return new static( |
||||
$configuration, |
||||
$plugin_id, |
||||
$plugin_definition, |
||||
$container->get('request_stack')->getMainRequest() |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function build() { |
||||
$id = $this->getDerivativeId(); |
||||
list($view_id, $display_id) = $this->getViewAndDisplayIdentifiers(); |
||||
$view = View::Load($view_id); |
||||
$view_executable = $view->getExecutable(); |
||||
$view_executable->setDisplay($display_id); |
||||
// Allow advanced search to alter the query. |
||||
$advanced_search_query = new AdvancedSearchQuery(); |
||||
$advanced_search_query->alterView($this->request, $view_executable, $display_id); |
||||
$view_executable->execute(); |
||||
$pager = $view_executable->getPager(); |
||||
$exposed_input = $view_executable->getExposedInput(); |
||||
$query_parameters = $this->request->query->all(); |
||||
$build = [ |
||||
'#attached' => [ |
||||
'drupalSettings' => [ |
||||
'islandora_advanced_search_pager_views_ajax' => [ |
||||
$id => [ |
||||
'view_id' => $view_id, |
||||
'current_display_id' => $display_id, |
||||
'ajax_path' => '/views/ajax', |
||||
], |
||||
], |
||||
], |
||||
], |
||||
'#attributes' => [ |
||||
'class' => ['islandora_advanced_search_result_pager'], |
||||
'data-drupal-pager-id' => $id, |
||||
], |
||||
'result_summary' => $this->buildResultsSummary($view_executable), |
||||
'container' => [ |
||||
'#prefix' => '<div class="pager__group">', |
||||
'#suffix' => '</div>', |
||||
'results_per_page_links' => $this->buildResultsPerPageLinks($pager, $query_parameters), |
||||
'display_links' => $this->buildDisplayLinks($query_parameters), |
||||
'sort_by' => $this->buildSortByForm($view_executable->sort, $query_parameters), |
||||
'pager' => array_merge($pager->render($exposed_input), ['#wrapper_attributes' => ['class' => ['container']]]), |
||||
], |
||||
]; |
||||
return $build; |
||||
} |
||||
|
||||
/** |
||||
* Build the results summary portion of the pager. |
||||
* |
||||
* @param Drupal\views\ViewExecutable $view_executable |
||||
* The view to build the summary for. |
||||
* |
||||
* @return array |
||||
* A renderable array that represents the current page, and number of |
||||
* results in the view. |
||||
*/ |
||||
protected function buildResultsSummary(ViewExecutable $view_executable) { |
||||
$current_page = (int) $view_executable->getCurrentPage() + 1; |
||||
$per_page = (int) $view_executable->getItemsPerPage(); |
||||
$total = isset($view_executable->total_rows) ? $view_executable->total_rows : count($view_executable->result); |
||||
// If there is no result the "start" and "current_record_count" should be |
||||
// equal to 0. To have the same calculation logic, we use a "start offset" |
||||
// to handle all the cases. |
||||
$start_offset = empty($total) ? 0 : 1; |
||||
if ($per_page === 0) { |
||||
$start = $start_offset; |
||||
$end = $total; |
||||
} |
||||
else { |
||||
$total_count = $current_page * $per_page; |
||||
if ($total_count > $total) { |
||||
$total_count = $total; |
||||
} |
||||
$start = ($current_page - 1) * $per_page + $start_offset; |
||||
$end = $total_count; |
||||
} |
||||
if (!empty($total)) { |
||||
// Return as render array. |
||||
return [ |
||||
'#prefix' => '<div class="pager__summary">', |
||||
'#suffix' => '</div>', |
||||
'#markup' => $this->t('Displaying @start - @end of @total', [ |
||||
'@start' => $start, |
||||
'@end' => $end, |
||||
'@total' => $total, |
||||
]), |
||||
]; |
||||
} |
||||
return []; |
||||
} |
||||
|
||||
/** |
||||
* Build the results per page portion of the pager. |
||||
* |
||||
* @param Drupal\views\Plugin\views\pager\SqlBase $pager |
||||
* The pager for the view. |
||||
* @param array $query_parameters |
||||
* The query parameters used to change the number of results per page. |
||||
* |
||||
* @return array |
||||
* A renderable array representing the results per page portion of pager. |
||||
*/ |
||||
protected function buildResultsPerPageLinks(SqlBase $pager, array $query_parameters) { |
||||
$active_items_per_page = $query_parameters['items_per_page'] ?? $pager->options['items_per_page']; |
||||
$items_per_page_options = array_map(function ($value) { |
||||
return trim($value); |
||||
}, explode(',', $pager->options['expose']['items_per_page_options'])); |
||||
$items = []; |
||||
foreach ($items_per_page_options as $items_per_page) { |
||||
$url = Url::fromRoute('<current>', [], [ |
||||
// When changing the number of items displayed always return the user |
||||
// to the first page. |
||||
'query' => array_merge($query_parameters, [ |
||||
'items_per_page' => $items_per_page, |
||||
'page' => 0, |
||||
]), |
||||
'absolute' => TRUE, |
||||
]); |
||||
$active = $items_per_page == $active_items_per_page; |
||||
$items[] = [ |
||||
'#type' => 'link', |
||||
'#url' => $url, |
||||
'#title' => $items_per_page, |
||||
'#attributes' => [ |
||||
'class' => $active ? ['pager__link', 'pager__link--is-active'] : ['pager__link'], |
||||
], |
||||
'#wrapper_attributes' => [ |
||||
'class' => $active ? ['pager__item', 'is-active'] : ['pager__item'], |
||||
], |
||||
]; |
||||
} |
||||
return [ |
||||
'#theme' => 'item_list', |
||||
'#title' => $this->t('Results per page'), |
||||
'#list_type' => 'ul', |
||||
'#items' => $items, |
||||
'#attributes' => [], |
||||
'#wrapper_attributes' => ['class' => ['pager__results', 'container']], |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* Build the display links portion of the pager (list/grid). |
||||
* |
||||
* @param array $query_parameters |
||||
* The query parameters used to change the display format. |
||||
* |
||||
* @return array |
||||
* A renderable array representing the display links portion of pager. |
||||
*/ |
||||
protected function buildDisplayLinks(array $query_parameters) { |
||||
$active_display = $query_parameters['display'] ?? 'list'; |
||||
$display_options = [ |
||||
'list' => [ |
||||
'icon' => 'fa-list', |
||||
'title' => $this->t('List'), |
||||
], |
||||
'grid' => [ |
||||
'icon' => 'fa-th', |
||||
'title' => $this->t('Grid'), |
||||
], |
||||
]; |
||||
$items = []; |
||||
foreach ($display_options as $display => $options) { |
||||
$url = Url::fromRoute('<current>', [], [ |
||||
'query' => array_merge($query_parameters, ['display' => $display]), |
||||
'absolute' => TRUE, |
||||
]); |
||||
$text = "<i class='fa {$options['icon']}' aria-hidden='true'> </i>{$options['title']}"; |
||||
$active = $active_display == $display; |
||||
$items[] = [ |
||||
'#type' => 'link', |
||||
'#url' => $url, |
||||
'#title' => Markup::create($text), |
||||
'#attributes' => [ |
||||
'class' => $active ? ['pager__link', 'pager__link--is-active'] : ['pager__link'], |
||||
], |
||||
'#wrapper_attributes' => [ |
||||
'class' => $active ? ['pager__item', 'is-active'] : ['pager__item'], |
||||
], |
||||
]; |
||||
} |
||||
return [ |
||||
'#theme' => 'item_list', |
||||
'#list_type' => 'ul', |
||||
'#items' => $items, |
||||
'#attributes' => [], |
||||
'#wrapper_attributes' => ['class' => ['pager__display', 'container']], |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* Build the sort by portion of the pager. |
||||
* |
||||
* @param array $sort_criteria |
||||
* The search fields which can be sorted. |
||||
* @param array $query_parameters |
||||
* The query parameters used to change the display format. |
||||
* |
||||
* @return array |
||||
* A renderable array representing the sort by portion of pager. |
||||
*/ |
||||
protected function buildSortByForm(array $sort_criteria, array $query_parameters) { |
||||
$default_order = $query_parameters['sort_order'] ?? 'ASC'; |
||||
$default_sort_by = $query_parameters['sort_by'] ?? 'search_api_relevance'; |
||||
$default_value = $default_sort_by . '_' . strtolower($default_order); |
||||
$options = []; |
||||
$options_attributes = []; |
||||
// Not sure if this will work without defining a sort per direction. |
||||
foreach ($sort_criteria as $sort) { |
||||
if ($sort->options['exposed'] == TRUE) { |
||||
$id = $sort->options['id']; |
||||
// Label should be translated via views already. |
||||
$label = $sort->options['expose']['label']; |
||||
$asc = "{$id}_asc"; |
||||
$desc = "{$id}_desc"; |
||||
$options[$asc] = "{$label} ↑"; |
||||
$options[$desc] = "{$label} ↓"; |
||||
$options_attributes[$asc] = [ |
||||
'data-sort_by' => $id, |
||||
'data-sort_order' => 'ASC', |
||||
]; |
||||
$options_attributes[$desc] = [ |
||||
'data-sort_by' => $id, |
||||
'data-sort_order' => 'DESC', |
||||
]; |
||||
} |
||||
} |
||||
return [ |
||||
'#type' => 'select', |
||||
'#title' => 'Sort', |
||||
'#title_display' => 'invisible', |
||||
'#options' => $options, |
||||
'#options_attributes' => $options_attributes, |
||||
'#attributes' => ['autocomplete' => 'off'], |
||||
'#wrapper_attributes' => ['class' => ['pager__sort', 'container']], |
||||
'#name' => 'order', |
||||
'#value' => $default_value, |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function getCacheMaxAge() { |
||||
// The block cannot be cached, because it must always match the current |
||||
// search results. |
||||
return 0; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,17 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\Block; |
||||
|
||||
/** |
||||
* This deriver creates a block for every search_api.display. |
||||
*/ |
||||
class SearchResultsPagerBlockDeriver extends SearchApiDisplayBlockDeriver { |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
protected function label() { |
||||
return $this->t('Search Results Pager'); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,30 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\Block; |
||||
|
||||
/** |
||||
* Gets the view and display identifiers used to create this block. |
||||
* |
||||
* @see Drupal\Component\Plugin\Discovery\DiscoveryInterface |
||||
*/ |
||||
trait ViewAndDisplayIdentifiersTrait { |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
abstract public function getDerivativeId(); |
||||
|
||||
/** |
||||
* Gets the View and View Display identifiers used to derive this block. |
||||
* |
||||
* @return string[] |
||||
* Returns an array of two strings where the first is the View identifier |
||||
* and the second is the View Display identifier associated with the view |
||||
* used to derive this block. |
||||
*/ |
||||
public function getViewAndDisplayIdentifiers() { |
||||
$id = $this->getDerivativeId(); |
||||
return preg_split('/__/', $id, 2); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,63 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\Field\FieldFormatter; |
||||
|
||||
use Drupal\Core\Field\FieldItemListInterface; |
||||
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase; |
||||
use Drupal\Core\Form\FormStateInterface; |
||||
|
||||
/** |
||||
* Plugin implementation of the 'entity reference ID' formatter. |
||||
* |
||||
* @FieldFormatter( |
||||
* id = "entity_reference_url_title", |
||||
* label = @Translation("Children Entity Count, Label."), |
||||
* description = @Translation("Children Entity Count, Label."), |
||||
* field_types = { |
||||
* "entity_reference" |
||||
* } |
||||
* ) |
||||
*/ |
||||
class EntityReferenceCountFormatter extends EntityReferenceFormatterBase { |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public static function defaultSettings() { |
||||
return [ |
||||
'label' => 'Items in Collection', |
||||
] + parent::defaultSettings(); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function settingsForm(array $form, FormStateInterface $form_state) { |
||||
$elements['separator'] = [ |
||||
'#title' => $this->t("Text to appear next to the children's count"), |
||||
'#type' => 'textfield', |
||||
'#default_value' => $this->getSetting('label'), |
||||
]; |
||||
return $elements; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function settingsSummary(): array { |
||||
$summary = []; |
||||
$summary[] = $this->getSetting('label') ? 'Label : ' . $this->getSetting('label') : $this->t('No label'); |
||||
return $summary; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function viewElements(FieldItemListInterface $items, $langcode): array { |
||||
$element = []; |
||||
$total_items = count($this->getEntitiesToView($items, $langcode)); |
||||
$element[] = ['#markup' => "<span class='collection_children__total_count'>{$total_items}<span>" . ' ' . $this->formatPlural($total_items, '1 Item in Collection', '@count Items in Collection')]; |
||||
return $element; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,84 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\facets\widget; |
||||
|
||||
use Drupal\facets\Plugin\facets\widget\LinksWidget; |
||||
use Drupal\Core\Link; |
||||
use Drupal\facets\Result\ResultInterface; |
||||
|
||||
/** |
||||
* The links widget. |
||||
* |
||||
* @FacetsWidget( |
||||
* id = "include_exclude_links", |
||||
* label = @Translation("List of links that allow the user to include / exclude facets."), |
||||
* description = @Translation("A simple widget that shows a list of +/- links."), |
||||
* ) |
||||
*/ |
||||
class IncludeExcludeLinksWidget extends LinksWidget { |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
protected function prepareLink(ResultInterface $result) { |
||||
$facet = $result->getFacet(); |
||||
$facet_source_id = $facet->getFacetSourceId(); |
||||
$facet_manager = \Drupal::service('facets.manager'); |
||||
$facets = $facet_manager->getFacetsByFacetSourceId($facet_source_id); |
||||
$raw_value = $result->getRawValue(); |
||||
$count = $result->getCount(); |
||||
$url = $result->getUrl(); |
||||
$exclude_facet = $this->getExcludeFacet($facet, $facets); |
||||
$exclude_result = $this->getExcludeResult($exclude_facet, $raw_value); |
||||
$exclude_url = $exclude_result ? $exclude_result->getUrl() : NULL; |
||||
return [ |
||||
'#theme' => 'facets_result_item', |
||||
'#is_active' => $result->isActive(), |
||||
'#value' => [ |
||||
'text' => (new Link($result->getDisplayValue(), $url))->toRenderable(), |
||||
'include' => (new Link(' ', $url))->toRenderable() + [ |
||||
'#attributes' => [ |
||||
'class' => ['facet-item__include', 'fa', 'fa-plus'], |
||||
], |
||||
], |
||||
'exclude' => $exclude_url ? (new Link(' ', $exclude_url))->toRenderable() + [ |
||||
'#attributes' => [ |
||||
'class' => ['facet-item__exclude', 'fa', 'fa-minus'], |
||||
], |
||||
] : NULL, |
||||
], |
||||
'#show_count' => $this->getConfiguration()['show_numbers'] && ($count !== NULL), |
||||
'#count' => $count, |
||||
'#facet' => $facet, |
||||
'#raw_value' => $raw_value, |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* Looks for the excluded facet version of the included facet. |
||||
*/ |
||||
protected function getExcludeResult($facet, $raw_value) { |
||||
if ($facet) { |
||||
foreach ($facet->getResults() as $result) { |
||||
if ($result->getRawValue() === $raw_value) { |
||||
return $result; |
||||
} |
||||
} |
||||
} |
||||
return NULL; |
||||
} |
||||
|
||||
/** |
||||
* Looks for the excluded facet version of the included facet. |
||||
*/ |
||||
protected function getExcludeFacet($include, $facets) { |
||||
$field_identifier = $include->getFieldIdentifier(); |
||||
foreach ($facets as $facet) { |
||||
if ($field_identifier === $facet->getFieldIdentifier() && $facet->getExclude()) { |
||||
return $facet; |
||||
} |
||||
} |
||||
return NULL; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,42 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\facets_summary\processor; |
||||
|
||||
use Drupal\facets_summary\FacetsSummaryInterface; |
||||
use Drupal\facets_summary\Processor\BuildProcessorInterface; |
||||
|
||||
/** |
||||
* Reset should also remove the page query attribute. |
||||
* |
||||
* @SummaryProcessor( |
||||
* id = "reset_remove_page", |
||||
* label = @Translation("Remove page from query when resetting facets/query."), |
||||
* description = @Translation("Remove page from query when resetting facets/query."), |
||||
* stages = { |
||||
* "build" = 45 |
||||
* } |
||||
* ) |
||||
*/ |
||||
class ResetRemovePage extends ShowSearchQueryProcessor implements BuildProcessorInterface { |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function build(FacetsSummaryInterface $facets_summary, array $build, array $facets) { |
||||
// This processor is weighted to occur after the reset facets link. |
||||
// Which leaves two cases: |
||||
// - No facets selected so no reset link (we must add one). |
||||
// - Reset link exists at the top of the list (we must remove the |
||||
// search term from the link as well). |
||||
$reset_index = $this->getResetLinkIndex($build); |
||||
if ($reset_index !== NULL) { |
||||
$reset = &$build['#items'][$reset_index]; |
||||
// Remove query from reset url as well. |
||||
$query_params = $reset['#url']->getOption('query'); |
||||
unset($query_params['page']); |
||||
$reset['#url']->setOption('query', $query_params); |
||||
} |
||||
return $build; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,47 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\facets_summary\processor; |
||||
|
||||
use Drupal\facets_summary\FacetsSummaryInterface; |
||||
use Drupal\facets_summary\Processor\BuildProcessorInterface; |
||||
use Drupal\facets_summary\Processor\ProcessorPluginBase; |
||||
use Drupal\facets\FacetInterface; |
||||
|
||||
/** |
||||
* Provides a processor that shows the search query. |
||||
* |
||||
* @SummaryProcessor( |
||||
* id = "show_active_excluded_facets", |
||||
* label = @Translation("Show active excluded facets."), |
||||
* description = @Translation("When checked, negated facets will appear in the summary."), |
||||
* stages = { |
||||
* "build" = 20 |
||||
* } |
||||
* ) |
||||
*/ |
||||
class ShowActiveExcludedFacets extends ProcessorPluginBase implements BuildProcessorInterface { |
||||
|
||||
use ShowFacetsTrait; |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
protected function condition(FacetInterface $facet) { |
||||
return $facet->getExclude(); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
protected function classes() { |
||||
return ['facet-summary-item--facet', 'facet-summary-item--exclude']; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function build(FacetsSummaryInterface $facets_summary, array $build, array $facets) { |
||||
return $this->buildHelper($build, $facets); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,89 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\facets_summary\processor; |
||||
|
||||
use Drupal\Core\Link; |
||||
use Drupal\facets_summary\FacetsSummaryInterface; |
||||
use Drupal\facets_summary\Processor\BuildProcessorInterface; |
||||
use Drupal\facets_summary\Processor\ProcessorPluginBase; |
||||
use Drupal\facets\FacetInterface; |
||||
use Drupal\facets\Result\ResultInterface; |
||||
|
||||
/** |
||||
* Provides a processor that shows the search query. |
||||
* |
||||
* @SummaryProcessor( |
||||
* id = "show_active_facets", |
||||
* label = @Translation("Shows active hidden facets."), |
||||
* description = @Translation("When checked, undoes 'hide_active_items_processor', etc."), |
||||
* stages = { |
||||
* "build" = 20 |
||||
* } |
||||
* ) |
||||
*/ |
||||
class ShowActiveFacets extends ProcessorPluginBase implements BuildProcessorInterface { |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function build(FacetsSummaryInterface $facets_summary, array $build, array $facets) { |
||||
// Rebuild list of results, add back ones that have been removed. |
||||
$facet_manager = \Drupal::service('facets.manager'); |
||||
$facet_source_id = $facets_summary->getFacetSourceId(); |
||||
$facet_manager->updateResults($facet_source_id); |
||||
$facet_manager->processFacets($facet_source_id); |
||||
$facets_config = $facets_summary->getFacets(); |
||||
foreach ($facets as $facet) { |
||||
$processors = $facet->getProcessors(); |
||||
/** @var \Drupal\facets\Processor\BuildProcessorInterface $url_handler */ |
||||
$url_handler = $processors['url_processor_handler']; |
||||
$results = $url_handler->build($facet, $facet->getResults()); |
||||
foreach ($results as $result) { |
||||
if ($result->isActive() && $this->resultMissing($facet, $result, $build['#items'])) { |
||||
$item = [ |
||||
'#theme' => 'facets_result_item__summary', |
||||
'#value' => $result->getDisplayValue(), |
||||
'#show_count' => $facets_config[$facet->id()]['show_count'], |
||||
'#count' => $result->getCount(), |
||||
'#is_active' => TRUE, |
||||
'#facet' => $result->getFacet(), |
||||
'#raw_value' => $result->getRawValue(), |
||||
]; |
||||
$item = (new Link($item, $result->getUrl()))->toRenderable(); |
||||
$item['#wrapper_attributes'] = [ |
||||
'class' => [ |
||||
'facet-summary-item--facet', |
||||
], |
||||
]; |
||||
$build['#items'][] = $item; |
||||
} |
||||
} |
||||
} |
||||
return $build; |
||||
} |
||||
|
||||
/** |
||||
* Checks if the results are missing for the given facet. |
||||
* |
||||
* @param \Drupal\facets\FacetInterface $facet |
||||
* The facet to check. |
||||
* @param \Drupal\facets\Result\ResultInterface $result |
||||
* The result of the facet to check. |
||||
* @param array $items |
||||
* The already completed render array of facets to check against. |
||||
* |
||||
* @return bool |
||||
* TRUE if the result is missing FALSE otherwise. |
||||
*/ |
||||
protected function resultMissing(FacetInterface $facet, ResultInterface $result, array $items) { |
||||
foreach ($items as $item) { |
||||
$item_facet = $item['#title']['#facet']; |
||||
$raw_value = $item['#title']['#raw_value']; |
||||
if ($item_facet === $facet && $raw_value === $result->getRawValue()) { |
||||
return FALSE; |
||||
} |
||||
} |
||||
return TRUE; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,67 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\facets_summary\processor; |
||||
|
||||
use Drupal\Core\Link; |
||||
use Drupal\Core\Url; |
||||
use Drupal\facets\FacetInterface; |
||||
|
||||
/** |
||||
* Common logic to toggle the display of facets given a condition. |
||||
*/ |
||||
trait ShowFacetsTrait { |
||||
|
||||
/** |
||||
* Checks if the facet should be shown or not. |
||||
*/ |
||||
abstract protected function condition(FacetInterface $facet); |
||||
|
||||
/** |
||||
* Classes to include on the shown facet. |
||||
*/ |
||||
abstract protected function classes(); |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
protected function buildHelper(array $build, array $facets) { |
||||
$request = \Drupal::request(); |
||||
$query_params = $request->query->all(); |
||||
foreach ($facets as $facet) { |
||||
if ($this->condition($facet)) { |
||||
$url_alias = $facet->getUrlAlias(); |
||||
$filter_key = $facet->getFacetSourceConfig()->getFilterKey() ?: 'f'; |
||||
$active_items = $facet->getActiveItems(); |
||||
foreach ($active_items as $active_item) { |
||||
$url = Url::createFromRequest($request); |
||||
$modified_query_params = $query_params; |
||||
$modified_query_params[$filter_key] = array_filter($query_params[$filter_key], function ($query_param) use ($url_alias, $active_item) { |
||||
$pos = strpos($query_param, ':'); |
||||
$alias = substr($query_param, 0, $pos); |
||||
$value = substr($query_param, $pos + 1); |
||||
return !($alias == $url_alias && $value == $active_item); |
||||
}); |
||||
$url->setOption('query', $modified_query_params); |
||||
$item = [ |
||||
'#theme' => 'facets_result_item__summary', |
||||
'#value' => $active_item, |
||||
// We do not have counts for excluded/missing facets... |
||||
'#show_count' => FALSE, |
||||
// Do not know the count. |
||||
'#count' => 0, |
||||
'#is_active' => TRUE, |
||||
'#facet' => $facet, |
||||
'#raw_value' => $active_item, |
||||
]; |
||||
$item = (new Link($item, $url))->toRenderable(); |
||||
$item['#wrapper_attributes'] = [ |
||||
'class' => $this->classes(), |
||||
]; |
||||
$build['#items'][] = $item; |
||||
} |
||||
} |
||||
} |
||||
return $build; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,47 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\facets_summary\processor; |
||||
|
||||
use Drupal\facets_summary\FacetsSummaryInterface; |
||||
use Drupal\facets_summary\Processor\BuildProcessorInterface; |
||||
use Drupal\facets_summary\Processor\ProcessorPluginBase; |
||||
use Drupal\facets\FacetInterface; |
||||
|
||||
/** |
||||
* Provides a processor that shows the search query. |
||||
* |
||||
* @SummaryProcessor( |
||||
* id = "show_missing_facets", |
||||
* label = @Translation("Shows facets from the url that are missing from the results."), |
||||
* description = @Translation("When checked, show facets not included in the solr result but specified in the URL."), |
||||
* stages = { |
||||
* "build" = 20 |
||||
* } |
||||
* ) |
||||
*/ |
||||
class ShowMissingFacets extends ProcessorPluginBase implements BuildProcessorInterface { |
||||
|
||||
use ShowFacetsTrait; |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
protected function condition(FacetInterface $facet) { |
||||
return !$facet->getExclude() && empty($facet->getResults()); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
protected function classes() { |
||||
return ['facet-summary-item--facet']; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function build(FacetsSummaryInterface $facets_summary, array $build, array $facets) { |
||||
return $this->buildHelper($build, $facets); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,101 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search\Plugin\facets_summary\processor; |
||||
|
||||
use Drupal\Core\Link; |
||||
use Drupal\Core\Url; |
||||
use Drupal\facets_summary\FacetsSummaryInterface; |
||||
use Drupal\facets_summary\Processor\BuildProcessorInterface; |
||||
use Drupal\facets_summary\Processor\ProcessorPluginBase; |
||||
|
||||
/** |
||||
* Provides a processor that shows the search query. |
||||
* |
||||
* @SummaryProcessor( |
||||
* id = "show_search_query", |
||||
* label = @Translation("Show the current search query"), |
||||
* description = @Translation("When checked, this facet will show the search query."), |
||||
* stages = { |
||||
* "build" = 40 |
||||
* } |
||||
* ) |
||||
*/ |
||||
class ShowSearchQueryProcessor extends ProcessorPluginBase implements BuildProcessorInterface { |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function build(FacetsSummaryInterface $facets_summary, array $build, array $facets) { |
||||
$request = \Drupal::request(); |
||||
$query_params = $request->query->all(); |
||||
if (!empty($query_params['search_api_fulltext'])) { |
||||
$text = $query_params['search_api_fulltext']; |
||||
unset($query_params['search_api_fulltext']); |
||||
$url = Url::createFromRequest($request); |
||||
$url->setOption('query', $query_params); |
||||
$item = [ |
||||
'#theme' => 'facets_result_item__summary', |
||||
'#is_active' => FALSE, |
||||
'#value' => $text, |
||||
'#show_count' => FALSE, |
||||
]; |
||||
$item = Link::fromTextAndUrl($item, $url)->toRenderable(); |
||||
$item['#wrapper_attributes'] = [ |
||||
'class' => [ |
||||
'facet-summary-item--query', |
||||
], |
||||
]; |
||||
// This processor is weighted to occur after the reset facets link. |
||||
// Which leaves two cases: |
||||
// - No facets selected so no reset link (we must add one). |
||||
// - Reset link exists at the top of the list (we must remove the search |
||||
// term from the link as well). |
||||
$reset_index = $this->getResetLinkIndex($build); |
||||
if ($reset_index !== NULL) { |
||||
$reset = $build['#items'][$reset_index]; |
||||
// Remove query from reset url as well. |
||||
$query_params = $reset['#url']->getOption('query'); |
||||
unset($query_params['search_api_fulltext']); |
||||
$reset['#url']->setOption('query', $query_params); |
||||
array_splice($build['#items'], $reset_index + 1, 0, [$item]); |
||||
} |
||||
else { |
||||
array_unshift($build['#items'], $item); |
||||
$text = $this->t('Reset'); |
||||
if (isset($facets_summary->getProcessorConfigs()['reset_facets']['settings']['link_text'])) { |
||||
$text = $facets_summary->getProcessorConfigs()['reset_facets']['settings']['link_text']; |
||||
} |
||||
$reset = Link::fromTextAndUrl($text, $url)->toRenderable(); |
||||
$reset['#wrapper_attributes'] = [ |
||||
'class' => [ |
||||
'facet-summary-item--clear', |
||||
], |
||||
]; |
||||
array_unshift($build['#items'], $reset); |
||||
} |
||||
return $build; |
||||
} |
||||
return $build; |
||||
} |
||||
|
||||
/** |
||||
* Gets the index in the $build render array of the reset link. |
||||
* |
||||
* @param array $build |
||||
* The render array of the FacetSummary block. |
||||
* |
||||
* @return mixed|null |
||||
* The index of the reset link the $build render array. |
||||
*/ |
||||
protected function getResetLinkIndex(array $build) { |
||||
if (isset($build['#items'])) { |
||||
foreach ($build['#items'] as $index => $item) { |
||||
if (isset($item['#wrapper_attributes']['class']) && in_array('facet-summary-item--clear', $item['#wrapper_attributes']['class'])) { |
||||
return $index; |
||||
} |
||||
} |
||||
} |
||||
return NULL; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,63 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_advanced_search; |
||||
|
||||
use Drupal\islandora_advanced_search\Plugin\Block\AdvancedSearchBlock; |
||||
use Drupal\islandora_advanced_search\Plugin\Block\SearchResultsPagerBlock; |
||||
|
||||
/** |
||||
* Helper functions. |
||||
*/ |
||||
class Utilities { |
||||
|
||||
/** |
||||
* Gets the list of views for which pager blocks have been created. |
||||
* |
||||
* @return array |
||||
* List of view and display ids which have that have been used to |
||||
* derive a SearchResultsPagerBlock. |
||||
*/ |
||||
public static function getPagerViewDisplays() { |
||||
$views = &drupal_static(__FUNCTION__); |
||||
if (!isset($views)) { |
||||
$block_storage = \Drupal::entityTypeManager()->getStorage('block'); |
||||
$active_theme = \Drupal::theme()->getActiveTheme(); |
||||
$views = []; |
||||
/** @var \Drupal\block\Entity\Block $block */ |
||||
foreach ($block_storage->loadByProperties(['theme' => $active_theme->getName()]) as $block) { |
||||
$plugin = $block->getPlugin(); |
||||
if ($plugin instanceof SearchResultsPagerBlock) { |
||||
list($view_id, $display_id) = $plugin->getViewAndDisplayIdentifiers(); |
||||
$views[$block->id()] = [$view_id, $display_id]; |
||||
} |
||||
} |
||||
} |
||||
return $views; |
||||
} |
||||
|
||||
/** |
||||
* Gets the list of views for which advanced search blocks have been created. |
||||
* |
||||
* @return array |
||||
* List of view and display ids which have that have been used to |
||||
* derive a SearchResultsPagerBlock. |
||||
*/ |
||||
public static function getAdvancedSearchViewDisplays() { |
||||
$views = &drupal_static(__FUNCTION__); |
||||
if (!isset($views)) { |
||||
$block_storage = \Drupal::entityTypeManager()->getStorage('block'); |
||||
$active_theme = \Drupal::theme()->getActiveTheme(); |
||||
$views = []; |
||||
/** @var \Drupal\block\Entity\Block $block */ |
||||
foreach ($block_storage->loadByProperties(['theme' => $active_theme->getName()]) as $block) { |
||||
$plugin = $block->getPlugin(); |
||||
if ($plugin instanceof AdvancedSearchBlock) { |
||||
list($view_id, $display_id) = $plugin->getViewAndDisplayIdentifiers(); |
||||
$views[$block->id()] = [$view_id, $display_id]; |
||||
} |
||||
} |
||||
} |
||||
return $views; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,58 @@
|
||||
{# |
||||
/** |
||||
* @file |
||||
* Default theme implementation for a facets item list. |
||||
* |
||||
* Available variables: |
||||
* - items: A list of items. Each item contains: |
||||
* - attributes: HTML attributes to be applied to each list item. |
||||
* - value: The content of the list element. |
||||
* - title: The title of the list. |
||||
* - list_type: The tag for list element ("ul" or "ol"). |
||||
* - wrapper_attributes: HTML attributes to be applied to the list wrapper. |
||||
* - attributes: HTML attributes to be applied to the list. |
||||
* - empty: A message to display when there are no items. Allowed value is a |
||||
* string or render array. |
||||
* - context: A list of contextual data associated with the list. May contain: |
||||
* - list_style: The ID of the widget plugin this facet uses. |
||||
* - facet: The facet for this result item. |
||||
* - id: the machine name for the facet. |
||||
* - label: The facet label. |
||||
* |
||||
* @see facets_preprocess_facets_item_list() |
||||
* |
||||
* @ingroup themeable |
||||
*/ |
||||
#} |
||||
<div class="facets-widget- {{- facet.widget.type -}} "> |
||||
{% if facet.widget.type %} |
||||
{%- set attributes = attributes.addClass('item-list__' ~ facet.widget.type) %} |
||||
{% endif %} |
||||
{% if items or empty %} |
||||
{%- if title is not empty -%} |
||||
<h3>{{ title }}</h3> |
||||
{%- endif -%} |
||||
|
||||
{%- if items -%} |
||||
<{{ list_type }}{{ attributes }}> |
||||
{%- for item in less -%} |
||||
<li{{ item.attributes }}>{{ item.value }}</li> |
||||
{%- endfor -%} |
||||
</{{ list_type }}> |
||||
{%- if more -%} |
||||
<{{ list_type }}{{ attributes }} style="display:none;margin-top:-1em"> |
||||
{%- for item in more -%} |
||||
<li{{ item.attributes }}>{{ item.value }}</li> |
||||
{%- endfor -%} |
||||
</{{ list_type }}> |
||||
<a href="#" class="facets-soft-limit-link">{{ show_more_label }}</a> |
||||
{%- endif -%} |
||||
{%- else -%} |
||||
{{- empty -}} |
||||
{%- endif -%} |
||||
{%- endif %} |
||||
|
||||
{% if facet.widget.type == "dropdown" %} |
||||
<label id="facet_{{ facet.id }}_label">{{ 'Facet'|t }} {{ facet.label }}</label> |
||||
{%- endif %} |
||||
</div> |
@ -0,0 +1,33 @@
|
||||
{# |
||||
/** |
||||
* @file |
||||
* Default theme implementation of a facet result item. |
||||
* |
||||
* Available variables: |
||||
* - value: The item value. |
||||
* - raw_value: The raw item value. |
||||
* - show_count: If this facet provides count. |
||||
* - count: The amount of results. |
||||
* - is_active: The item is active. |
||||
* - facet: The facet for this result item. |
||||
* - id: the machine name for the facet. |
||||
* - label: The facet label. |
||||
* |
||||
* @ingroup themeable |
||||
*/ |
||||
#} |
||||
{% if value['text'] is defined %} |
||||
<span class="facet-item__value">{{ value['text'] }} |
||||
{% if show_count %} |
||||
<span class="facet-item__count">({{ count }})</span> |
||||
{% endif %} |
||||
{% if value['exclude'] is defined %} |
||||
<span class="facet-item__operators">{{ value['include'] }} {{ value['exclude'] }}</span> |
||||
{% endif %} |
||||
</span> |
||||
{% else %} |
||||
<span class="facet-item__value">{{ value }}</span> |
||||
{% if show_count %} |
||||
<span class="facet-item__count">({{ count }})</span> |
||||
{% endif %} |
||||
{% endif %} |
@ -0,0 +1,20 @@
|
||||
{# |
||||
/** |
||||
* @file |
||||
* Default theme implementation of a facet result item. |
||||
* |
||||
* Available variables: |
||||
* - value: The item value. |
||||
* - raw_value: The raw item value. |
||||
* - show_count: If this facet provides count. |
||||
* - count: The amount of results. |
||||
* - is_active: The item is active. |
||||
* - facet: The facet for this result item. |
||||
* - id: the machine name for the facet. |
||||
* - label: The facet label. |
||||
* |
||||
* @ingroup themeable |
||||
*/ |
||||
#} |
||||
<span class="facet-item__value">{{ value }}</span> |
||||
<span class="fa fa-remove"></span> |
@ -0,0 +1,7 @@
|
||||
audio: |
||||
version: 1.x |
||||
js: |
||||
js/audio.js: {preprocess: false} |
||||
dependencies: |
||||
- core/drupal |
||||
- core/drupalSettings |
@ -0,0 +1,47 @@
|
||||
/*jslint browser: true*/ |
||||
/*global Audio, Drupal*/ |
||||
/** |
||||
* @file |
||||
* Displays Audio viewer. |
||||
*/ |
||||
(function ($, Drupal) { |
||||
'use strict'; |
||||
|
||||
/** |
||||
* If initialized. |
||||
* @type {boolean} |
||||
*/ |
||||
var initialized; |
||||
/** |
||||
* Unique HTML id. |
||||
* @type {string} |
||||
*/ |
||||
var base; |
||||
|
||||
function init(context,settings){ |
||||
if (!initialized){ |
||||
initialized = true; |
||||
if ($('audio')[0].textTracks.length > 0) { |
||||
$('audio')[0].textTracks[0].oncuechange = function() { |
||||
if (this.activeCues.length > 0) { |
||||
var currentCue = this.activeCues[0].text; |
||||
$('#audioTrack').html(currentCue); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Drupal.Audio = Drupal.Audio || {}; |
||||
|
||||
/** |
||||
* Initialize the Audio Viewer. |
||||
*/ |
||||
Drupal.behaviors.Audio = { |
||||
attach: function (context, settings) { |
||||
init(context,settings); |
||||
}, |
||||
detach: function () { |
||||
} |
||||
}; |
||||
|
||||
})(jQuery, Drupal); |
@ -0,0 +1,28 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_audio\Plugin\Field\FieldFormatter; |
||||
|
||||
use Drupal\islandora\Plugin\Field\FieldFormatter\IslandoraFileMediaFormatterBase; |
||||
|
||||
/** |
||||
* Plugin implementation of the 'file_audio' formatter. |
||||
* |
||||
* @FieldFormatter( |
||||
* id = "islandora_file_audio", |
||||
* label = @Translation("Audio with Captions"), |
||||
* description = @Translation("Display the file using an HTML5 audio tag."), |
||||
* field_types = { |
||||
* "file" |
||||
* } |
||||
* ) |
||||
*/ |
||||
class IslandoraFileAudioFormatter extends IslandoraFileMediaFormatterBase { |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public static function getMediaType() { |
||||
return 'audio'; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,31 @@
|
||||
{# |
||||
/** |
||||
* @file |
||||
* Default theme implementation to display the file entity as an audio tag. |
||||
* |
||||
* Available variables: |
||||
* - attributes: An array of HTML attributes, intended to be added to the |
||||
* audio tag. |
||||
* - files: And array of files to be added as sources for the audio tag. Each |
||||
* element is an array with the following elements: |
||||
* - file: The full file object. |
||||
* - source_attributes: An array of HTML attributes for to be added to the |
||||
* source tag. |
||||
* |
||||
* @ingroup themeable |
||||
*/ |
||||
#} |
||||
<div id="audioTrack"></div> |
||||
<audio {{ attributes }}> |
||||
{% for file in files %} |
||||
<source {{ file.source_attributes }} /> |
||||
{% if tracks %} |
||||
{% for track in tracks %} |
||||
<track {{ track.track_attributes }} /> |
||||
{% endfor %} |
||||
{% endif %} |
||||
{% endfor %} |
||||
</audio> |
||||
|
||||
{{ attach_library('islandora_audio/audio') }} |
||||
|
@ -1,3 +0,0 @@
|
||||
maxDepth: -1 |
||||
includeSelf: FALSE |
||||
referenceField: field_member_of |
@ -0,0 +1,4 @@
|
||||
maxDepth: -1 |
||||
includeSelf: FALSE |
||||
referenceFields: |
||||
- field_member_of |
@ -1,7 +1,7 @@
|
||||
name: 'Islandora Breadcrumbs' |
||||
type: module |
||||
description: 'Builds breadcrumbs based on field_member_of relationships.' |
||||
core: 8.x |
||||
core_version_requirement: ^9 || ^10 |
||||
package: Islandora |
||||
dependencies: |
||||
- islandora |
||||
- islandora:islandora |
||||
|
@ -0,0 +1,18 @@
|
||||
<?php |
||||
|
||||
/** |
||||
* @file |
||||
* Install/update hook implementations. |
||||
*/ |
||||
|
||||
/** |
||||
* Update referenceField config to referenceFields. |
||||
*/ |
||||
function islandora_breadcrumbs_update_8001() { |
||||
$config_factory = \Drupal::configFactory(); |
||||
$config = $config_factory->getEditable('islandora_breadcrumbs.breadcrumbs'); |
||||
$config->set('referenceFields', [$config->get('referenceField')]); |
||||
$config->clear('referenceField'); |
||||
$config->save(); |
||||
return "Updated referenceFields config."; |
||||
} |
@ -0,0 +1,5 @@
|
||||
system.islandora_breadcrumbs_settings: |
||||
title: 'Breadcrumbs Settings' |
||||
parent: system.admin_config_islandora |
||||
route_name: system.islandora_breadcrumbs_settings |
||||
description: 'Configure Islandora breadcrumb settings' |
@ -0,0 +1,7 @@
|
||||
system.islandora_breadcrumbs_settings: |
||||
path: '/admin/config/islandora/breadcrumbs' |
||||
defaults: |
||||
_form: 'Drupal\islandora_breadcrumbs\Form\IslandoraBreadcrumbsSettingsForm' |
||||
_title: 'Islandora Breadcrumbs Settings' |
||||
requirements: |
||||
_permission: 'administer site configuration' |
@ -1,6 +1,6 @@
|
||||
services: |
||||
islandora_breadcrumbs.breadcrumb: |
||||
class: Drupal\islandora_breadcrumbs\IslandoraBreadcrumbBuilder |
||||
arguments: ['@entity_type.manager', '@config.factory'] |
||||
arguments: ['@entity_type.manager', '@config.factory', '@islandora.utils'] |
||||
tags: |
||||
- { name: breadcrumb_builder, priority: 100 } |
||||
|
@ -0,0 +1,132 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora_breadcrumbs\Form; |
||||
|
||||
use Drupal\Core\Form\ConfigFormBase; |
||||
use Drupal\Core\Form\FormStateInterface; |
||||
|
||||
/** |
||||
* Configure islandora_breadcrumbs settings. |
||||
*/ |
||||
class IslandoraBreadcrumbsSettingsForm extends ConfigFormBase { |
||||
|
||||
/** |
||||
* Config settings. |
||||
* |
||||
* @var string |
||||
*/ |
||||
const SETTINGS = 'islandora_breadcrumbs.breadcrumbs'; |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function getFormId() { |
||||
return 'islandora_breadcrumbs_settings'; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
protected function getEditableConfigNames() { |
||||
return [ |
||||
static::SETTINGS, |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function buildForm(array $form, FormStateInterface $form_state) { |
||||
|
||||
$config = $this->config(static::SETTINGS); |
||||
|
||||
$form['maxDepth'] = [ |
||||
'#type' => 'number', |
||||
'#default_value' => $config->get('maxDepth'), |
||||
'#min' => -1, |
||||
'#step' => 1, |
||||
'#title' => $this->t('Maximum number of ancestor breadcrumbs'), |
||||
'#description' => $this->t("Stops adding ancestor references when the chain reaches this number. The count does not include the current node when enabled. The default value, '-1' disables this feature."), |
||||
]; |
||||
|
||||
$form['includeSelf'] = [ |
||||
'#type' => 'checkbox', |
||||
'#title' => $this->t('Include the current node in the breadcrumbs?'), |
||||
'#default_value' => $config->get('includeSelf'), |
||||
]; |
||||
|
||||
// Using the textarea instead of a select so the site maintainer can |
||||
// provide an ordered list of items rather than simply selecting from a |
||||
// list which enforces it's own order. |
||||
$form['referenceFields'] = [ |
||||
'#type' => 'textarea', |
||||
'#title' => $this->t('Entity Reference fields to follow'), |
||||
'#default_value' => implode("\n", $config->get('referenceFields')), |
||||
'#description' => $this->t("Entity Reference field machine names to follow when building the breadcrumbs.<br>One per line.<br>Valid options: @options", |
||||
[ |
||||
"@options" => implode(", ", static::getNodeEntityReferenceFields()), |
||||
] |
||||
), |
||||
'#element_validate' => [[get_class($this), 'validateReferenceFields']], |
||||
|
||||
]; |
||||
|
||||
return parent::buildForm($form, $form_state); |
||||
} |
||||
|
||||
/** |
||||
* Returns a list of node entity reference field machine names. |
||||
* |
||||
* We use this for building the form field description and for |
||||
* validating the reference fields value. |
||||
*/ |
||||
protected static function getNodeEntityReferenceFields() { |
||||
return array_keys(\Drupal::service('entity_field.manager')->getFieldMapByFieldType('entity_reference')['node']); |
||||
} |
||||
|
||||
/** |
||||
* Turns a text area into an array of values. |
||||
* |
||||
* Used for validating the field reference text area |
||||
* and saving the form state. |
||||
*/ |
||||
protected static function textToArray($string) { |
||||
return array_filter(array_map('trim', explode("\n", $string)), 'strlen'); |
||||
} |
||||
|
||||
/** |
||||
* Callback for settings form. |
||||
* |
||||
* @param array $element |
||||
* An associative array containing the properties and children of the |
||||
* generic form element. |
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state |
||||
* The current state of the form for the form this element belongs to. |
||||
* |
||||
* @see \Drupal\Core\Render\Element\FormElement::processPattern() |
||||
*/ |
||||
public static function validateReferenceFields(array $element, FormStateInterface $form_state) { |
||||
|
||||
$valid_fields = static::getNodeEntityReferenceFields(); |
||||
|
||||
foreach (static::textToArray($element['#value']) as $value) { |
||||
if (!in_array($value, $valid_fields)) { |
||||
$form_state->setError($element, t('"@field" is not a valid entity reference field!', ["@field" => $value])); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function submitForm(array &$form, FormStateInterface $form_state) { |
||||
$this->configFactory->getEditable(static::SETTINGS) |
||||
->set('referenceFields', static::textToArray($form_state->getValue('referenceFields'))) |
||||
->set('maxDepth', $form_state->getValue('maxDepth')) |
||||
->set('includeSelf', $form_state->getValue('includeSelf')) |
||||
->save(); |
||||
|
||||
parent::submitForm($form, $form_state); |
||||
} |
||||
|
||||
} |