You are viewing limited content. For full access, please sign in.

Question

Question

help creating a functional search bar

asked on May 6, 2024

Hello, I am in the process of attempting to create a search bar in the new version of Laserfiche.  I was successfully able to use a custom html object to create and style the search bar, and used the custom Javascript pane to create an array of words found from scraping the form.  While this mostly worked, it was only able to capture words that fell under custom html (p tags, h1, etc) - this left out form section headers, which was not ideal.  Moving on from this momentarily, I decided to attempt to finish setting up triggers for when things were typed into search bar, which again I was not successful with this..I decided to switch from using a search bar to just a regular single line field which I could trigger changes/input on better..what I still struggled with was how to go about filtering the results once a match (or matches) were found..It doesn't seem as straightforward as it would be setting something like this up on a traditional web page, so I wondered if anyone else has accomplished similar functionality using new version of Laserfiche (this is one of our direct requirements, is that it must use the modern designer).  Thanks in advance!

0 0

Replies

replied on May 6, 2024

So are you trying to show and hide whole sections of a form based on whether or not a search contains keywords from that section?

0 0
replied on May 6, 2024

Yeah, can we get more context on the business problem you're trying to solve here? Without the broader context, this raises XY Problem flags.

What are you trying to search?

If it's entries in a Laserfiche repository, you might want to look into using WebLink Search Forms and embedding them into a Modern designer form.

0 0
replied on May 7, 2024

Here's our use case:

We're creating a "Laserfiche Forms FAQ" process using nested collapsed sections.

When we played with doing this previously, it was prior to the Modern Designer, and @████████ added a search bar to the form (it was lovely). 

We are now building the form using the Modern Designer, and we asked him to see if he could create the same or similar functionality. 

Screenshot included to give you an idea of what we're thinking.

faq.jpg
faq.jpg (130.23 KB)
0 0
replied on May 7, 2024 Show version history

Doing this with Javascript is going to be more difficult in the modern designer than it would be in the classic designer, simply due to the fact that the modern designer runs the Javascript within a sandboxed iFrame and doesn't have direct access to the form elements to manipulate them directly.

You can only work with the form fields and manipulate them via Javascript using the LFForm interface (see help article: https://doc.laserfiche.com/laserfiche.documentation/11/administration/en-us/Default.htm#../Subsystems/Forms/Content/Javascript-and-CSS/JavaScript-in-the-Forms-Designer.htm).  There are functions in the LFForm interface to hide and show fields, so that part is possible.  But using the script to retrieve the labels and text from all of the fields on the form is less direct.

The simplest solution (this is a no-code solution) would be to use Field Rules to Show/Hide sections based on the search field containing keywords.  That would require you setting up each keyword that you wanted searchable for each section - but, it's a no-code solution.

 

If you really want to have a more automated Javascript solution, then I would recommend maybe taking a look at this post:
https://answers.laserfiche.com/questions/211162/Google-Translate-in-Forms-Version-11#215350
If you watch the YouTube videos that @████████ posted, you can also download the Forms processes that @████████ created.  The reason I suggest this, is because the code that Zachary wrote for that example includes functions to generate the full form contents as a JSON string.  You might be able to repurpose that functionality to identify the search criteria you are wanting to be able to search against and which sections contain them.  Bear in mind - I have not done that - it's just an idea and it might not work.

2 0
replied on May 7, 2024 Show version history

You have two options, and I encourage you to at least play with the first one even though the formatting of the FAQ will be a little different. The second is more involved, but inheretently more flexible if what I wrote for you works and/or someone has enough JS experience to expand on it. Your options are:

  1. Use a collection field and take advantage of the new search feature added in both cloud and self hosted. In the new designer you can put a section within each row of the collection, but each individual FAQ option would be its own row. The downside here is you would not be able to have your sub sections "Functionality" or "Time Reporting"
  2. The code at the bottom should work for your simple case (assuming my assumptions are valid). Update the 'searchBarFieldId' to be the ID of the field on the form you are searching on. The 'searchOptions' variable can be kept as is, but if this code isn't working for you that's how we'll update options so you dont have to mess with the search function itself. (You can ignore the comments, they help me write the code)

 

EDIT: Code removed, see follow up reply for new code

 

It would be a neat feature though to include a search option for sections. Hadn't thought about this use case before 🤔

5 0
replied on May 10, 2024

Thanks so much for the help!  While that was mostly functional, I feel that I ran into some limitations regarding displaying directly nested fields;  the LF object only appears to have a property to obtain the parent field information from what I am seeing, are you able to verify that or am I overlooking something possibly?  What we would like to accomplish is being able to type a search query into the search field and then let's say the match was within a custom html - we would expect it to display the custom html (preferably highlight the matched search term), and then include any parent fields / outer fields if the custom html was embedded inside of any sections.  Currently, this deletes anything other than the object with the contained search term, with exception of the parent section.

0 0
replied on May 10, 2024

We did also test using the built-in Search functionality in a collection, but that didn't work for us. It doesn't seem that the Search recognizes anything in Section headers or HTML blocks. Any combination using these elements results in  "No search results found." 

0 0
replied on May 20, 2024

 the LF object only appears to have a property to obtain the parent field information from what I am seeing

There is a property 'data' of any field that can have children that outlines each field inside it, you can find the section of my code where I use this below:

if (
      searchOptions.showSectionSiblingsOnMatch &&
      parent.componentType !== 'Page' &&
      parent.componentType !== 'Form'
    ) {
      // show all siblings
      const siblings = Object.values(parent.data);
      for (const sibling of siblings) {
        delete hide[sibling.fieldId];
        if (sibling.fieldId !== f.fieldId) {
          show[sibling.fieldId] = sibling;
        }
      }
    }

The code is getting a little long, but I've made some changes based on the feedback here and added it to the searchOptions settings object.
 

const searchBarFieldId = 1;
/**
 * @type {{
 *    fieldOptions: {
 *      [key: string]: {
 *        [key: string]: boolean | undefined,
 *        label?: boolean,
 *        textAbove?: boolean,
 *        textBelow?:
 *        boolean,
 *        content?: boolean } | boolean
 *    };
 *    minCharCount: number;
 *    markupCustomHTML: boolean;
 *    showSectionSiblingsOnMatch: boolean;
 *  }}
 */
const searchOptions = {
  fieldOptions: {
    Section: {
      label: true,
      textAbove: true,
      textBelow: true,
    },
    CustomHTML: {
      content: true,
    },
  },
  minCharCount: 1,
  markupCustomHTML: true,
  showSectionSiblingsOnMatch: true,
};

const main = async () => {
  const allFields = LFForm.findFields(
    (f) =>
      f.fieldId !== searchBarFieldId &&
      f.componentType !== 'Page' &&
      f.componentType !== 'Form'
  );
  /**
   * @type {Record<string, string>}
   */
  const searchOptionToLFFormProp = {
    label: 'settings.label',
    textAbove: 'settings.description',
    textBelow: 'settings.subtext',
    content: 'settings.default',
    value: 'data',
    defaultValue: 'settings.default',
  };
  let lastSearchVal = '';
  const htmlSearch = '(?<!<[^>]*)';
  /**
   * Store modified html to reset markings
   * @type {Record<number, {defaultHtml: string, markedUp: string }>}
   */
  let modifyHTML = {};
  LFForm.onFieldChange(
    debounce(async () => {
      const searchFieldValue = LFForm.getFieldValues({
        fieldId: searchBarFieldId,
      });
      void LFForm.changeFieldSettings(
        { fieldId: searchBarFieldId },
        { subtext: '' }
      );
      const searchValue = String(searchFieldValue) ?? '';
      if (searchValue === lastSearchVal) {
        return;
      }
      lastSearchVal = searchValue;
      if (searchOptions.markupCustomHTML) {
        await Promise.allSettled(
          Object.entries(modifyHTML).map(([fieldIdString, html]) => {
            const fieldId = Number(fieldIdString);
            const { defaultHtml } = html;
            return LFForm.changeFieldSettings(
              { fieldId },
              { content: defaultHtml }
            );
          })
        );
        modifyHTML = {};
      }

      if (
        searchValue === '' ||
        searchValue.length < searchOptions.minCharCount
      ) {
        await LFForm.showFields(allFields);
        return;
      }

      // hide and show lists use objects as a field may or may not contain the search term but be a parent of another field that contains the search term
      /**
       * @type {Record<number, { fieldId: number }>}
       */
      const hide = {};
      /**
       * @type {Record<number, { fieldId: number }>}
       */
      const show = {};
      // parent child relationships are owned by a component id, so lets track that mapped to its usable field id
      /**
       * @type {Record<string, number>}
       */
      const componentIdToFieldId = {};
      const htmlSearchRegex = new RegExp(htmlSearch + searchValue, 'ig');
      /**
       *
       * @param {import('@lfc/lf-query').LFFormField} f
       */
      const handleSearchResultFound = (f) => {
        show[f.fieldId] = f;
        let parent = LFForm.findFieldsByFieldId(
          componentIdToFieldId[f.settings.parentId]
        )?.[0];
        if (
          f.componentType === 'CustomHTML' &&
          searchOptions.markupCustomHTML
        ) {
          const newHtml = f.settings.default.replace(
            htmlSearchRegex,
            (str) =>
              `<mark class="lfform-search" style="padding: 0px;">${str}</mark>`
          );
          modifyHTML[f.fieldId] = {
            defaultHtml: f.settings.default,
            markedUp: newHtml,
          };
        } else if (f.componentType === 'Section') {
          // Show all descendants
        }
        if (
          searchOptions.showSectionSiblingsOnMatch &&
          parent.componentType !== 'Page' &&
          parent.componentType !== 'Form'
        ) {
          // show all siblings
          /**
           * @type {import('@lfc/lf-query').LFFormField[]}
           */
          const siblings = Object.values(parent.data);
          console.log(siblings, parent.data)
          for (const sibling of siblings) {
            delete hide[sibling.fieldId];
            if (sibling.fieldId !== f.fieldId) {
              show[sibling.fieldId] = sibling;
            }
          }
        }
        while (
          parent &&
          parent.componentType !== 'Page' &&
          parent.componentType !== 'Form'
        ) {
          const parentFieldId = parent.fieldId;
          delete hide[parentFieldId];
          show[parentFieldId] = { fieldId: parentFieldId };
          parent = LFForm.findFieldsByFieldId(
            componentIdToFieldId[parent.settings.parentId]
          )?.[0];
        }
      };

      for (const f of allFields) {
        if (show[f.fieldId]) {
          continue;
        }
        componentIdToFieldId[f.componentId] = f.fieldId;
        const fieldOptions = searchOptions.fieldOptions[f.componentType];
        if (fieldOptions === undefined || fieldOptions === false) {
          hide[f.fieldId] = f;
          continue;
        } else if (fieldOptions === true) {
          for (const searchPropString of Object.values(
            searchOptionToLFFormProp
          )) {
            const [settings, searchProp] = searchPropString.split('.');
            const value = searchProp ? f[settings][searchProp] : f[settings];
            const searchMatch =
              f.componentType === 'CustomHTML'
                ? value.match(htmlSearchRegex)
                : value.toLowerCase().includes(searchValue.toLowerCase());
            if (typeof value === 'string' && searchMatch) {
              handleSearchResultFound(f);

              break;
            }
          }
        } else if (typeof fieldOptions === 'object') {
          for (const searchPropString of Object.keys(fieldOptions)) {
            if (fieldOptions[searchPropString] === true) {
              const mappedSearchProp =
                searchOptionToLFFormProp[searchPropString];
              const [settings, searchProp] = mappedSearchProp.split('.');
              /**
               * @type {string}
               */
              const value = searchProp ? f[settings][searchProp] : f[settings];
              const searchMatch =
                f.componentType === 'CustomHTML'
                  ? value.match(htmlSearchRegex)
                  : value.toLowerCase().includes(searchValue.toLowerCase());
              if (typeof value === 'string' && searchMatch) {
                handleSearchResultFound(f);

                break;
              }
            }
          }
        }
        if (show[componentIdToFieldId[f.settings.parentId]]) {
          show[f.fieldId] = f;
        } else if (show[f.fieldId] === undefined) {
          hide[f.fieldId] = f;
        }
      }
      console.log(show, hide, modifyHTML);
      const showFields = Object.values(show);
      const hideFields = Object.values(hide);
      const markedUpHTML = Object.entries(modifyHTML);
      if (showFields.length === 0) {
        await Promise.allSettled([
          LFForm.showFields(allFields),
          LFForm.changeFieldSettings(
            { fieldId: searchBarFieldId },
            { subtext: 'No results found' }
          ),
          ...(markedUpHTML.length > 0
            ? markedUpHTML.map(([fieldIdString, newHTML]) => {
                const fieldId = Number(fieldIdString);
                const { defaultHtml } = newHTML;
                return LFForm.changeFieldSettings(
                  { fieldId },
                  { content: defaultHtml }
                );
              })
            : [Promise.resolve()]),
        ]);
      } else {
        await Promise.allSettled([
          showFields.length > 0
            ? LFForm.showFields(showFields)
            : Promise.resolve(),
          hideFields.length > 0
            ? LFForm.hideFields(hideFields)
            : Promise.resolve(),
          ...(markedUpHTML.length > 0
            ? markedUpHTML.map(([fieldIdString, newHTML]) => {
                const fieldId = Number(fieldIdString);
                const { markedUp } = newHTML;
                return LFForm.changeFieldSettings(
                  { fieldId },
                  { content: markedUp }
                );
              })
            : [Promise.resolve()]),
        ]);
      }
    }),

    { fieldId: searchBarFieldId }
  );
};

void main();

/**
 *
 * @param {() => {}} callback
 * @param {number} wait
 */
function debounce(callback, wait = 100) {
  /**
   * @type {number | undefined}
   */
  let timeoutId = undefined;
  return (...args) => {
    window.clearTimeout(timeoutId);
    timeoutId = window.setTimeout(() => {
      callback(...args);
    }, wait);
  };
}

 

1 0
replied on May 20, 2024

Thanks again for your help on this.  I tested a few search queries and it did filter a few various queries but certain words triggered an error (see attached) ;  things like 'form' or 'team' returned that error but there are probably others.  Also, it would be great if it expanded the section if it was in an expandable/collapsible section and toggled it back to be collapsed if search query did not match.  There was one other issue as well, such as it not highlighting div.section-titles. 

0 0
replied on May 20, 2024

It currently only searches custom html and section titles/text above/text below. You "should" be able to update the searchOptions object with other search criteria. (I dont see you attatchment)

 

Support for HTML highlighting in field labels and other settings is only supported in cloud at this time. Are you on cloud or self hosted?

0 0
replied on May 22, 2024

Sorry, I attached it.  We are on self-hosted.

error.png
error.png (9.46 KB)
0 0
replied on May 22, 2024

I fixed it by just checking first whether a parent exists before filtering down and that takes care of it from what I could tell..the only thing I still need to address is actually expanding the sections when a match is found in a collapsible section so I am still working on figuring that out.  Do you know if highlighting section headers is in the works for self-hosted?  

0 0
replied on May 22, 2024

Ah, ok I must have missed a place where i map componentId to fieldId.

 

Collapsing/Expanding sections via LFForm doesn't exist yet but I've created a feature request for that (among a few other enhancements). HTML (for highlighting) in section headers will come with collapse/expand in LF12 late this year!

2 0
You are not allowed to follow up in this post.

Sign in to reply to this post.