Widgets
Epigraf uses a widget system to connect HTML elements with JavaScript classes. The widget HTML elements are generated in the backend by helper classes derived from CakePHP helpers.
Each JavaScript widget class registers a CSS class and the framework instantiates and attaches the widget classes
to the found elements. For example, a table with the class widget-table
is supplemented by a TableWidget
class.
Widgets do not exclude each other, one HTML element can be attached to multiple widget instances of different classes.
Note: The widget system is in an early experimental development stage. The documentation does not cover all aspects yet and is subject to change.
The EpiWidJs Framework
The base classes of the framework are defined in htdocs/js/base.js
:
- BaseModel: The
BaseModel
class provides lifecycle functions and methods to attach event listeners. Model class instances are not necessarily connected to the DOM. They are used to create a frontend model layer as a complement to the backend model layer. A model can have a parent and multiple children, the first constructor parameter is the parent model (which may be undefined for top level models). - BaseWidget: All widgets derive from the
BaseWidget
class, which itself derives fromBaseModel
. A widget is defined as a model class instance attached to the DOM. The first constructor parameter is the HTML element to which the widget is attached, followed by a widget name and the parent class. - BaseForm: The
BaseForm
class derives fromBaseWidget
and is a widget attached to form elements. It is used for form handling, preparing input to the database. - BaseDocument: The
BaseDocument
class derives fromBaseWidget
. It is used for document handling. A document consists of several parts such as sections, footnotes and notes. Document classes hold together the different parts and manage the interaction between them.
The Widget Life Cycle
There are two types of widgets: global and scoped. Global widgets include widgets that are responsible
for the entire page and usually occur only once per page, such as MainFrame
(main content of the page),
ResizableSidebar
(for the two main sidebars) or ScrollSync
(for synchronizing scrollable main content of
pages and the table of contents). Scoped widgets are usually attached to specific HTML elements, for example
to a table widget, and can occur multiple times on a page. The majority of widgets belong to the second category.
All widgets are initialized in the widgets.js
file.
The global widgets are initialized first in initApp()
, followed by the scoped widgets in initWidgets()
.
Scoped widgets are registered with their CSS class name at the end of their widget class file.
For example, by registering the TableWidget
class with the ‘table’ key, for all HTML elements
with the class widget-table
a TableWidget
is instantiated and attached to the DOM element:
window.App.widgetClasses = window.App.widgetClasses || {};
window.App.widgetClasses['table'] = TableWidget;
The widget lifecyle is managed by methods that may be overwritten in derived classes.
- constructor: The constructor is called with the DOM element in the first parameter
and saves the reference to the DOM element in the
widgetElement
property. - initWidget: The method contains initialization code. It is called once after all widgets were constructed and are ready to be used. This is the place where widgets can safely initialize functionality that depend on other widgets.
- updateWidget: The method is usually called by the widget itself if the DOM content of the widget element changed. This is the place where event listeners are updated by the widget.
- clearWidget: The method is called when the widget element is about to be removed (like a destructor). It removes all event listeners, child models, and detaches the widget instance from the DOM element.
The lifecyle methods are triggered by two key events:
- The
epi:init:widgets
event triggers theonInitWidgets()
method after all widgets connected to a DOM element or its descendants were instantiated, whether on the first page load or on later page updates by AJAX calls. It callsinitWidget()
if the widget was not already initialized. - The
epi:clear:widgets
event triggers theonClearWidgets()
method when DOM element are about to be removed, for example when replacing a table after an AJAX call. It callsclearWidget()
.
The Event System
The base model class implements functions to attach and detach event listeners.
You should never use JavaScript event handling methods directly, instead use the BaseWidget
methods.
This makes sure that event listeners will be recursively removed when elements are removed or updated.
Further, the methods support emitting custom events.
- listenEvent: Attaches event listeners to the element passed in the first parameter or to the document.
Example:
this.listenEvent(this.widgetElement, 'click', this.listenerClick);
- unlistenEvent: Removes event listeners from the passed element.
Example:
this.unlistenEvent(this.widgetElement, 'click', this.listenerClick);
. - emitEvent: Triggers a custom event and passes data to the event listener.
Example:
this.emitEvent('epi:update:row', {row: entityId, sender: this});
.
Custom events can be used in blocking mode to show confirm dialogs or to trigger form validation by setting the cancelable parameter (the third parameter) to true. This way, the event can be canceled by the listener. In the following example, a method only proceeds if all event listeners let the event pass:
if (!this.emitEvent('epi:save:form', {}, true)) {
return false;
}
Epigraf implements several custom events following either the pattern app:<action>:<object>
for global layout related events or epi:<action>:<object>
for data related events.
The <action>
denotes an operation and <object>
the affected thing.
Examples include app:show:loader
or epi:update:row
.
When using emitEvent()
of a widget class, the event is always emitted from the widget’s element,
bubbles up the DOM and can be observed by other widgets.
Side note on models: A widget is defined as a model attached to a DOM element.
Not all models are widgets. Thus, for classed directly derived from the base model class,
the event methods support passing a DOM element instead of using the attached element.
The widget event methods are based on the respective methods in utils.js
.
If you need event delegation, i.e. attach an event listener with an additional CSS selector
for child elements to observe, you can directly use the methods Utils.listenEvent()
and Utils.unlistenEvent()
.
Widget Styling
Widgets may need special CSS styling.
Specific widget CSS files are located in the folder plugins/Widgets/webroot/css
and imported into widgets.css
which is bundled using webpack.
Frame Widgets
Each tabsheet, the main content and popup content are considered a frame (see frames.js
).
Frames are HTML elements attached to derivates of the BaseFrame
class that handle the page layout:
MainFrame
: Responsible for the main content of a page.TabFrame
: Responsible for tab sheets within in sidebars.PopupWindow
: Responsible for popups and dialogs.
The frame classes handle dynamic content loading and trigger the respective widget lifecycle methods:
loadElement()
displays a DOM element already constructed on the page.loadUrl()
display content loaded via AJAX from a URL.
The specific classes make sure that the loaded HTML snippets are inserted into the frame or popup. For example, breadcrumbs become the frame title and buttons are placed in the popup or tab sheet footer and react to the roles in their data-role attribute.
Derivates of the popup widget class handle different types of popups:
ConfirmWindow
: A popup window with buttons to confirm or cancel action, e.g. deleting an item on the page.MessageWindow
: A notification window.SelectWindow
: A popup to select items from a list, with specific callbacks and events the calling widget can use to handle the selection.
For moving content around the page, two classes support temporarily detaching elements from their original place and putting them back later:
DetachedWindow
opens an element in a popup.DetachedTab
opens an element in a tab sheet in the sidebar.
Layout widgets
Layout widgets provide extended functionality for the page layout (see layout.js
):
- ResizableSidebar: Handles sidebars in the main page layout and in overlays such as the image viewer widget.
- Tabsheets: Responsible for handling multiple tabs inside of the sidebars.
- ContentLoader: Defers loading of tab sheet content until the tab is activated.
- Accordion: Used on small screens instead of sidebars.
- ScrollSync: Synchronizes active sections between main content and the table of contents or other elements in the sidebars. Additionally, it updates the URL hash fragment when scrolling through the sections and activates sections identified by the hash fragment on page load.
Collections: Tables, Trees And Their Supplemental Widgets
Collections are used to display lists of items, for example in tables or trees. The HTML elements are generated by the collection views in the backend and are supplemented by JavaScript widgets.
TableWidget
Tables are supplemented by resizable columns and other interaction options by the TableWidget
.
The table widget can be combined with other widgets:
TreeWidget
for hierarchical data,DragItemsWidget
to allow moving rows and tree nodes by drag and drop.ScrollPaginator
for paginating to long result tables.
The tbody should contain a unique name for the loaded dataset in the attribute data-list-name
.
Each row should have a corresponding data-list-itemof
attribute.
TreeWidget
The TreeWidget
is used in the sidebars as menus and for filter facets as well as in hierarchical tables,
for example to display property trees.
ScrollPaginator
The ScrollPaginator
widget enables an infinite scrollbox for pages with many data records.
It supports cursor-based and page-based pagination in flat tables and hierarchical trees and listens to
row update events to update the scrollbox content (epi:update:row
, epi:move:row
, epi:create:row
, epi:delete:row
).
Those events are emitted by widgets derived from BaseForm
, for example, after saving content in a sidebar.
Elements handled by the ScrollPaginator
should be wrapped in a container with the data-list-name
attribute.
Each individual data records should be marked with data-list-itemof
and data-id
.
FilterWidget
The FilterWidget
serves as a coordinator for subordinate widgets that manipulate query parameters,
for example to support search input fields or column selectors for tables. The subordinate widgets are
implemented in filter.js
(e.g. FilterSearchBar
, FilterColumns
, FilterMap
).
Each subordinate filter widget establishes a relationship to its coordinator to indicate necessary page updates through callbacks.
DragItemsWidget
The DragItemsWidget
allows you to move list entries or table rows by mouse.
Entities: Documents and Forms
Single entities are either handled by the EntityWidget
or the DocumentWidget
.
Both are descendants of the BaseForm
class supporting features for saving data to the database:
- Lock entities so that only one user can edit them at a time.
- Handle form validation and the form submission lifecycle.
- Emit events for other widgets reacting to content updates.
The EntityWidget
is used for simple entities, usually displayed in a vertical table.
It inherits all methods and properties from BaseForm
and does not provide any additional functionality.
The DocumentWidget
is used for more complex data,
for example an article which consists of sections or a property which may contain annotations.
The document widget initializes the respective frontend models (e.g., ArticlesModel
and PropertiesModel
).
Parts of a document may be distributed on the page. Two cases are distinguished in documents:
- Satellites are displayed outside the main content area.
They comprise footnotes and notes displayed in the sidebars and
are handled by the
NotesSatellite
andFootnotesSatellite
classes repsectively. The main document and the satellites both derive fromBaseDocumentPart
that provides some basic common methods on the frontend view layer. - Subordinate data contained within the main document widget or its satellite widgets
comprise, for example, sections and items (see the model layer in the backend).
Both data types have model classes and a corresponding widget class that directly derive
from
BaseDocument
which mainly allows accessing their parent documents, table names and ID attributes.
Buttons and Selector Widgets
Within the entities and documents, mostly standard HTML form elements are used to display and edit data. The markup is generated in the backend using CakePHP form helpers.
- Button widgets are handled by the classes
ChooseButtons
(select files, folders or databases),SandwichButton
(collapse multiple buttons into a sandwich button),SwitchButtons
(switch classes on target elements to hide or display content) andCodeblock
(copy-to-clipboard button in code clocks). -
Shortcuts can be attached to a and button elements using the
data-shortcuts
attribute (Example:data-shortcuts="Ctrl+M"
). Pressing the shortcut keys triggers a click event on the attached element. - Dropdowns are handled by the
DropdownWidget
and theDropdownSelectorWidget
classes, both derived from theDropdownWidgetBase
class. For dropdown selectors, the CakePHP form helper is extended by a ChooseWidget and a ReferenceWidget (seeAppController->beforeRender()
). To interactively reload forms after selecting a dropdown item, Epigraf implements theFormUpdateWidget
which comes into existence by the data-form-update attribute.
Content and Editor Widgets
Some widgets implement interactivity for specific content types:
- The
UploadWidget
handles file upload by drag and drop. - The
GridWidget
observes events in item lists and updates their position in a grid. TheDragAndDrop
enables items to be moved in the grid and updates the item list. - The
ImageViewer
adds an overlay to the page for viewing article images (zoom, rotate, gallery view). - The
EpiMap
widget display a map using Leaflet. In search mode, the EpiMap widget is integrated into the filter widget architecture to load markers batch-wise via AJAX based on zoom and pan actions. In edit mode, markers can be moved by the mouse. The EpiMap widget interacts with an item list to update field values based on marke movements and marker positions based on field updates.
Epigraf supports storing data formats such as JSON, HTML and XML in database fields based on the field configuration. For those fields, wysiwyg editors are used to edit the content:
HtmlEditor
: A CKEditor instance for editing HTML in the wiki, help and public pages.XmlEditor
: A CKEditor instance that renders XML to HTML and back, used for annotations.JsonEditor
: An AceEditor instance for editing JSON content.
The Job System
Epigraf supports long-running jobs such as exporting and importing data
by splitting a job into tasks and steps. Each step is executed
by calling the dedicated API endpoint. The JobWidget
is responsible
for polling the endpoint, displaying a progress bar and finally redirecting to the
result page or the download file.