=====Oracle Apex 18.2 - Select2 Java Script Library in einem Classic Report integrieren- Interactive Grid Funktionaliät mit JQuery in einem Classic Report nachimplementieren===== **Aufgabe:** Für eine Ausschilderung auf Basis der Displays von Philips (https://www.philips.de/p-m-pr/digital-signage-losungen ) ist eine Pflegemaske für die Raumzuordnung mit einem entsprechenden Richtungspfeil in der Raumliste zu implementieren. In der Datenbank wird dabei hinterlegt welches Display für welche Räume zuständig ist. Je nach Standort des Displays wird dann der entsprechende Richtungspfeil für die Veranstaltung in dem Raum angezeigt. Rand-Parameter: * Die Auswahlliste für den Richtungspfeil soll auch den Pfeil anzeigen! Das ist die Herausforderung! * Die bearbeiten Zeilen sollen hervorgehoben dargestellt werden * Der vorherige Richtungspfeil wird mit angezeigt, um die Änderung leichter nachzuvollziehen * Das Setzen eine Richtungspfeil ordnet den Raum dem Display zu * Die Anzahl der Räume ist begrenzt => alle Räume können in einer Tabelle auf der Seite dargestellt werden * 8 Richtungspfeile können ausgewählt werden ( Basierend auf den Pfeilen um APEX Font ( https://apex.oracle.com/pls/apex/f?p=42:icons ) ) * Die Tabelle kann nach den bereits zugeordneten Räumen gefiltert werden So sieht die Maske nun am Ende aus: {{ :prog:apex:apex_show_select2_list_clasic_report.png | Oracle APEX - dynamsiche Select2 Liste in Clasic Report}} ---- ===Lösungansatz=== **1. Versuch:** Im ersten Schritt habe ich das über einen Interactive Grid versucht zu lösen, die Anzahl der Zeilen ergibt sich aus den Räumen, pro Zeile kann über eine Checkbox der Raum zu Display zugeordnet und ein Richtungspfeil ausgewählt werden. D.h. die treibende Query des Berichts ist ein Select über die verfügbaren Räume mit einem Out Join über die bestehenden Zuordnungen. Im der Matrix kann dann der Richtungspfeil gewählt werden beim Click auf die jeweile Zeile. Beim Speichern wird die Default Funktion abgefangen und dann mit einem eigenen Handler das ganze gespeichert ( siehe z.B. hier => [[prog:oracle_apex_interactive_gird|Oracle Apex 5 - Interactive Grid anpassen und konfigurieren - DML ändern bei ORA-22816: unsupported feature with RETURNING clause]]. Leider funktioniert das aber dann nicht so recht wie gewünscht, nur die geänderten Daten werden ja beim Submit übertragen und die Oberfläche passt nicht so wirklich zu der an sich einfachen Anforderung einer einfachen Raum Matrix. So ist es z.B. nicht so einfach eigene GUI Elemente in den Bearbeitungsmodus einer einzelnen Tabellenzelle zu implementieren. **2. Versuch** Die Raum Liste mit einem Classic Report darstellen und die Konfiguration der Richtungspfeile über eine dynamisch eingeblendete Select Liste ermöglichen. Als Basis für die Select Liste dient die JavaScript Library select2 => https://select2.org/. Für Oracle Apex gibt es bereits ein Plugin für diese Liste, siehe => https://apex.world/ords/f?p=100:710:4758666274399::::P710_PLG_ID:BE.CTB.SELECT2 von Nick Buytaert. Das Plugin dient in diesem Fall aber mehr dazu die JavaScript Abhängigkeiten in die Seite zu laden und um das Template Objekte für die Auswahl Liste zu erzeugen. Problem mit der Lösung: Wird eine Region neu in der Seite geladen (ohne Submit der Seite!) wird der Event Handler (definiert über einen JQuery Selector) nicht mehr an die Html Tabellen Zelle gebunden! D.h. um die Tabelle neu einzulesen ist immer ein Submit und ein vollständiges Laden der Seite notwendig damit der Eventhandler wieder richtig funktioniert. => Mein Fehler => Einfach den **"Event Scope"** der Dynamic Action auf **"Dynamic"** ( Binds the event handler to the triggering element(s) for the lifetime of the current page, irrespective of any triggering elements being recreated via Partial Page Refresh (PPR)) setzen! ---- ==== Umsetzung: Dynamisch eine Select2 Auswahl Liste in einem Classic Report verwenden und Auswahl in der DB speichern==== === Voraussetzung=== ==Select2 Plugin in die Applikation laden== - Select2 Plugin von https://apex.world/ords/f?p=100:710:4758666274399::::P710_PLG_ID:BE.CTB.SELECT2 herunterladen und auspacken - - Aus "src\main\plugin" Datei item_type_plugin_be_ctb_select2.sql als Plugin in die Applikation laden ==Template Select Liste in der Seite hinterlegen=== Diese Select Liste wird nicht auf der Seite direkt dargestellt. Diese Liste wird mit JQuery geclont ( in meine Beispiel über die Klasse referenziert) und in der jeweiligen Tabellen Zelle dargestellt. Ablauf: * Page Item Select Liste mit dem Fonts für die Richtungspfeile anlegen ( wie P100_ICON_VALUES) und mit den möglichen Werte versorgen (select oder statische Liste) * Diese Page Item mit "On Page Load" mit Dynamic Action ("Hide") ausblenden * CSS Klasse auf dem Element hinterlegen unter Advanced "CSS Clases" "DEFAULT_SELECT_ICON" Nun kann je nach Bedarf auch noch etwas an der Optik mit der Liste über die Klasse gearbeitet werden. ---- === Bericht - Classic Report - über die Räume erstellen=== Eine klassischen APEX HTML Tabelle auf Basis eines SQL Kommandos mit einem "Classic Report" in die Seite einfügen. Wichtig ist es die ROWNUM in die Abfrage mit aufzunehmen, alternativ kann natürlich auch ein anderer eindeutiger numerischer Schlüssel in der Ergebnismenge verwendet werden. Die ROWNNUM dient später als Schlüssel für eine dedizierte Tabellenzeile und muss mit dieser angezeigt werden. Wird die Liste in SQL sortiert, die Rownum erst in einem äußeren Select hinzufügen, damit die Rownum auch der eigenen Sortierung folgt. Unter "Attributes" auf dem Classic Report über die Template Options "Alternating Rows" und "Row Highlighting" ausschalten (auf Disable setzen) , wird später manuell durchgeführt. ---- === Modell der Daten aus der Tabelle in ein Objekt einlesen === Wir müssen ja wissen welche Element wie auf der Seite zugeordnet wurden. Dazu speichern wir uns die Werte in der angezeigten Tabelle in ein Modell ( Ein Recordset in einem Array). Der Java Script Block wird auf Page Ebene in "Function and Global Variable Declaration" hinterlegen. Die Daten werden aus der Tabelle in ein Model geladen, als Parameter dient eine Spalte der Tabelle, von der Spalte aus wird die ganze Zeile referenziert und ausgewertet, die Spalte ROWNUM dient als Primary Key für das Array. Da ja kein Text für die Icons in der Zelle steht wird die Klasse ausgewertet und der Wert für das Icon ausgelesen. // ---------------- Schritt 1 -------------------- // create Model from table view // build the dataset from all rows of the table // build the dataset from all rows of the table function getDataSet( table_rows ,pkRowName) { var elemName = {}; var rownum = 0; var dataset = []; var data_row = {}; table_rows.each( function (idx,elem) { // read one row of the table data_row = {}; $(elem).closest('tr').children().each( function (idx,elem) { // read the hint which data cell elemName=$(elem).attr("headers"); // read and store the value of the cell elemValue = elem.innerText; if (elemValue.length == 0) { //console.log("Read length :: " + elemValue.length + 'for row :: '+elemName); //console.log($(elem)); elemValue='-'; // check if span element exits and read class $(elem).children().each ( function (idx,spanelem) { elemValue=$(spanelem).attr("class"); elemValue=elemValue.replace(/ fa-2x/g,''); elemValue=elemValue.replace(/fa /g,''); elemValue=elemValue.replace(/ fa/g,''); //console.log("get class :: " + elemValue + 'for row :: '+$(spanelem)); } ); } data_row[elemName]= elemValue; // get the primary key (index counter) for the collection if (elemName == pkRowName) { rownum=elem.innerText; } } ); dataset [rownum]= data_row; } ); return dataset; } // get the orignal value from the dataset for this cell function getCellValueFromDataSet(cell,pkRowName) { var rownum=-1; $(cell).closest('tr').children().each( function (idx,elem) { // read the hint which data cell elemName=$(elem).attr("headers"); // get the primary key (index counter) for the collection if (elemName == pkRowName) { rownum=elem.innerText; } } ); return dataset[rownum]; } //----------------------------------- // first init of the dataset var headerColumnName="ICON_DISPLAY"; //dataset var dataset = getDataSet($('[headers="'+headerColumnName+'"]'),'ROWNUM'); ---- === Java Script Funcionen für die Verwendung des Select2 Elementes hinterlegen === Auf Page Ebene in "Function and Global Variable Declaration" hinterlegen. Init Functions für die Select2 Liste: //----------------------------------- // ------ // globals var actCell = {}; var lastCell = {}; var actCellValue = {}; var lastCellValue = {}; var actCellText = {}; var lastCellText = {}; var lastTriggerElement = {}; var secondClick=false; // select2 Functions // ---- // get the option list as template function iconSelectList() { return $('.DEFAULT_SELECT_ICON').clone().addClass('select-icon'); } var iconSelectList = iconSelectList(); //------- // default text if you open the element var defaultPlaceholderText="Icon auswählen"; function getDefaultPlaceholderText (elem){ return defaultFristText; } //-------- // set the value of a cell function setCellvalue ( cell, text, textClass) { var textSpan=$(cell).find('[id^=ROW_]'); console.log('Set Text => ' + text); console.log('Set Class => ' + textClass); //only if something was selected if (text != defaultPlaceholderText) { // console.debug(cell); // $(textSpan).text(text); $(textSpan).removeClass(); $(textSpan).addClass('fa '+textClass+' fa-2x'); } else { // insert the old value $(textSpan).text(lastCellText); $(textSpan). removeClass(); $(textSpan).addClass('fa ' +lastCellValue +' fa-2x'); } } //-------- // get the Value of a cell function getCellValue( cell ) { var cellValue = {}; var textSpan=$(cell).find('[id^=ROW_]'); cellValue = $(textSpan).attr('datavalue'); console.log("Read akt value :" + cellValue); return cellValue; } // get the Text of the cell function getCellText( cell ) { var cellText = {}; var textSpan=$(cell).find('[id^=ROW_]'); cellText = $(textSpan).text(); console.log("Read akt text :" + cellText); return cellText.toString(); } //-------------- // format select2 Entries function setListEntry(elem) { var originalOption = elem.element; return ''; //iconFont } //--------- // return the selected element function returnSelection(elem){ return elem.text; } //--------- // // Clear the open elementes function clearAll(elem) { // invalid the last element lastTriggerElement = {}; //set the value on the last cell back if (secondClick) { console.log("Auswahl Vaule ->" + $(".select-icon").val()); console.log("Auswahl text -> " + $('.select-icon').find(':selected').text() ); actCellText = $('.select-icon').find(':selected').text() ; actCellValue = $(".select-icon").val(); $( ".select-icon" ).select2('destroy'); $( ".select-icon" ).remove(); setCellvalue( actCell, actCellText, actCellValue); } // reset all actCell={}; secondClick=false; } Die eigentliche Liste implementieren: //--------- //create new Box at $(this.triggeringElement).closest('td') function initSelect2( cell , triggerElem ) { // trigger only if first time clicked if (lastTriggerElement == triggerElem ) { console.log("Same Element !"); console.log(triggerElem); } else { lastTriggerElement=triggerElem; //remeber the old values lastCellValue=actCellValue; lastCellText=actCellText; actCellValue=getCellValue( $(triggerElem).closest('td')); actCellText=getCellText( $(triggerElem).closest('td')); //remenber the last edit cell from the history lastCell=actCell; //( actCell ? actCell : cell ); // remove all other boxes on the page lastCellText = $('.select-icon').find(':selected').text() ; lastCellValue = $(".select-icon").val(); $(".select-icon").select2('destroy'); $(".select-icon").remove(); //set the value on the last cell // only if value extis (not first time) if (secondClick) { setCellvalue( lastCell, lastCellText, lastCellValue ); } else { secondClick=true; } //remeber the actual values of in the globals actCell=cell; //clear this cell setCellvalue( cell, "" , "" ); // set at the first java Script option console.log("Remember akt values t::"+ actCellText + " class:"+actCellValue); // add box to cell cell.append(iconSelectList); //create select2 box $('.select-icon').select2({ width: "100%" , minimumResultsForSearch: Infinity , allowClear: true , placeholder: 'Icon auswählen' , templateResult: setListEntry , templateSelection: returnSelection , //dropdownCssClass: 'iconFont' , escapeMarkup: function(m) { return m; } }); //set the list to the actual value console.log("set pre selected value t::"+ actCellText + " class:"+actCellValue); $('.select-icon').val(actCellValue).trigger('change'); } } ---- === Tastatur Handling mit berücksichtigen=== Nach der Auswahl eines Elements soll ja die Select2 Liste wieder entfernt und das Icon angezeigt werden. Tastatur Event Handler definieren: //--------- // // Key Handling // document.onkeydown = TabExample; function TabExample(evt) { var evt = (evt) ? evt : ((event) ? event : null); var tabKey = 9; if(evt.keyCode == tabKey) { clearAll(this); } } ---- === Auswahl Liste in dem Bericht beim Click auf eine Zelle aktiveren === Über eine Dynamic Action auf einen JQuery Selector den Java Skript Aufruf bei einem Click in eine Zelle aktivieren. JQuery Selctor für alle Zellen ist hier bei: **[headers="ICON_DISPLAY"]** Zuerst wird optisch markiert, in welcher Zeile wir etwas editiert haben: // mark the triggered element if (lastTriggerElement == this.triggeringElement ) { console.log("Same Element !"); } else { // the complete line $(this.triggeringElement).closest('tr').css({ "background-color": "#fbfbdf" }); //the box $(this.triggeringElement).closest('td').css({ "background-color": "#e5f3e5" }); } Select2 Liste einfügen: // create select2 Element initSelect2( $(this.triggeringElement).closest('td') ,this.triggeringElement); === Auswahl Liste wieder deaktiveren === Klickt der Anwender in die nächste Zelle soll die Liste wieder ausgeblendet werden. Dynamic Action anlegen mit JQuery Selector auf die anderen Spalten der Tabelle wie : [headers="RAUMNR"] , [ headers="RAUM"] , [headers="ICON_NAME"] JavaScript code: // clear the last entries clearAll( $(this.triggeringElement) ); Die gleiche JavaScript Methode wird auch beim Save Button etc. aufgerufen um die Liste wieder zu schließen. ---- === Speicher der Auswahl / Änderungen === Über eine Button "Speichern" wird die geänderte Tabelle in der Datenbank gespeichert. Vorbereitung: * Save Button anlegen * Hidden Item für die zu übertragenden Werte anlegen ( diese müssen die Eigenschaft "Value Protected auf "No" besitzen!) * Java Script Block über das Übertragen des Modells in die Hidden Items ( Liste mit ":" separiert pro Wert ) * Dynamic Action für das Speichern der Daten erzeugen * JavaScript Block um die Tabelle auszuwerten und in das Modell zu übertragen * PL/SQL Block um die Änderungen an die DB zu übertragen * JavaScript Block um eine Meldung anzuzeigen == Funktion um notwendigen Werte aus dem Modell in die Hidden Items zu übertragen== Code um die Werte aus dem Modell auch auszulesen (In dem Java Script Block der Seite hinterlegen): function setItemValues(dataset){ //clear old entries $s('P322_SUMBIT_ROOM_LIST',''); $s('P322_SUBMIT_ICON_LIST',''); //loop over the dataset dataset.forEach ( function (data_row) { //get Room Nr akt_val_raumnr=$v('P322_SUMBIT_ROOM_LIST'); elem_raumnr=data_row['RAUMNR']; elem_icon=data_row['ICON_DISPLAY']; if ( elem_icon != '-' && elem_icon.length > 0 && elem_icon != 'null' ) { akt_val_icon=$v('P322_SUBMIT_ICON_LIST'); // add icon to list akt_val_icon+=elem_icon+':' $s('P322_SUBMIT_ICON_LIST',akt_val_icon); //add room to list akt_val_raumnr+=elem_raumnr+':' $s('P322_SUMBIT_ROOM_LIST',akt_val_raumnr); console.log(" Room Nr :: " + elem_raumnr + ' ICON :: '+elem_icon); } } ); } Dieser Teil ist nicht parametrisiert und muss dann auf die jeweiligen Gegebenheiten angepasst werden. ---- === Dynamic Action anlegen=== Die Dyanamic Action zum Speichern besteht aus 3 Blöcken, Werte einsammeln, Werte übertragen und in die DB schreiben, Ergebnis-Meldung anzeigen. ==Ablauf Java Script Block 1 == - Modell auslesen - Daten aus den Modell in die entsprechenden Page Items schreiben, diese müssen die Eigenschaft "Value Protected auf "No" besitzen! Java Script Code für das Setzen der Page Items // clear the last select2 list if still open clearAll( $(this.triggeringElement) ); //----------------------------------- // first read again the dataset // dataset // variable defined in the main part of the page! // dataset = getDataSet($('[headers="'+headerColumnName+'"]'),'ROWNUM'); // copy the values to the submit items setItemValues(dataset); //remove last : if extis akt_val_raumnr=$v('P322_SUMBIT_ROOM_LIST'); akt_val_icon=$v('P322_SUBMIT_ICON_LIST'); if(akt_val_raumnr.slice(-1) == ":"){ akt_val_raumnr = akt_val_raumnr.slice(0,-1)+ ""; } if(akt_val_icon.slice(-1) == ":"){ akt_val_icon = akt_val_icon.slice(0,-1)+ ""; } $s('P322_SUBMIT_ICON_LIST',akt_val_icon); $s('P322_SUMBIT_ROOM_LIST',akt_val_raumnr); //debug console.log( 'P322_SUMBIT_ROOM_LIST :: ' + $v('P322_SUMBIT_ROOM_LIST') ); console.log( 'P322_SUBMIT_ICON_LIST :: ' + $v('P322_SUBMIT_ICON_LIST') ); console.log( 'P322_DISPLAY :: ' + $v('P322_DISPLAY') ); == PL/SQL Block für das eigentliche Speichern der Daten == Die Daten werden als ":" separierter Text String übertragen! Und dann in PL/SQL mit Hilfe von "apex_string.split" wieder in ein Array zurück übertragen. Für das Fehlerhandling im PL/SQL Block zwei Hidden Item anlegen: * P322_MESSAGE (Value Protected auf No!) * P322_MESSAGE_CODE (Value Protected auf No!) In der Dynamic Action: declare v_message varchar2(4000); v_message_code pls_integer:=0; begin p_aus_masterdata.set_display_room_assigments( p_display => :P322_DISPLAY , p_room_group_list => :P322_SUMBIT_ROOM_LIST , p_room_icon_list => :P322_SUBMIT_ICON_LIST , p_message => v_message , p_message_code => v_message_code) ; :P322_MESSAGE := v_message; :P322_MESSAGE_CODE :=v_message_code; end; Darauf auchten das bei "ITEMS to Submit" auch alle notwendigen ITEMs eingetragen werden und bei "Items to Return" die beiden Message Items hintelegt sind! In der Datenbank (in Auszügen): procedure set_display_room_assigments( p_display number , p_room_group_list varchar2 , p_room_icon_list varchar2 , p_message out varchar2 , p_message_code out integer) is v_routine_name VARCHAR2 (50) := g_pck || '.set_display_room_assigments'; v_count integer; v_icon_array wwv_flow_t_varchar2; v_room_array wwv_flow_t_varchar2; v_icon_name varchar2(256); v_room_pk number(11); v_message_code pls_integer:=0; v_message_text varchar2(4000):='Raumzuordnung gespeichert'; begin v_icon_array := apex_string.split( p_room_icon_list, ':' ); v_room_array := apex_string.split( p_room_group_list, ':' ); delete from RaumDisplays where DISPLAY_ID=p_display; if v_room_array.count > 0 then for i in v_room_array.first .. v_room_array.last loop if v_room_array.exists(i) then -- get the pk of a room select ... into v_room_pk where xxxxr=v_room_array(i); end if; if v_icon_array.exists(i) then insert into RaumDisplays ( ID, DISPLAY_ID, ICON_NAME, SCHULRAUM_OID ) values ( RaumDisplays_seq.nextval , p_display , v_icon_array(i) , v_room_pk); end if; end loop; end if; commit; p_message_code:= v_message_code; p_message := v_message_text; exception when others then p_message := 'Error : Update Room to Display Assigment failed :: ' ||SQLERRM; p_message_code := SQLCODE; raise_application_error(-20001, p_message); end set_display_room_assigments; == JavaScript Block für das Anzeigen einer Meldung == Da "apex_application.g_print_success_message" nur beim Rendern der Seite funktioniert ( z.b. in einem Prozess in PL/SQL) muss eine inline Message über die API verwendet werden. Nächste Action in Java Script: apex.message.clearErrors(); v_message =$v('P322_MESSAGE'); v_message_code=$v('P322_MESSAGE_CODE'); if ( v_message_code != '0' ) { apex.message.showErrors([ { type: apex.message.TYPE.ERROR, location: ["page"], message: v_message, unsafe: false } ]); } else { apex.message.showPageSuccess( v_message ); } Beim Speichern wird dann eine entsprechende Box mit einer Meldung aus der DB angezeigt, hier ein Fehler: {{ :prog:apex:apex_error_message_inline.png | Oracle Apex Inline Error Message JavaScript API apex.message.showErrors }} ---- === Optimierungspotential === * Schöners und moderners JavaScript ( man ist halt doch pl/sql entwickler 8-) ) * Weniger Abhängigkeiten zu globalen Variablen * Mehr Parametrisierung um das einfacher auf andere Anwendungsfälle zu übertragen CSS Spielereien: //Breite anpassen .select2-wrapper { width: 200px; } // Größe und Position anpassen .select2-results .fa { float: left; position: relative; line-height: 20px; } ---- ==== Quellen==== Select2 * https://select2.org/ APEX Inline Message * https://www.talkapex.com/2018/03/custom-apex-notification-messages/