mirror of
https://github.com/EGroupware/egroupware.git
synced 2025-01-03 04:29:28 +01:00
Et2Split: Convert splitter to webcomponent
This commit is contained in:
parent
44c8b2f372
commit
ce14c1c9f6
@ -204,15 +204,40 @@ function send_template()
|
|||||||
if ($matches[1] === 'date' || $matches[1] === 'select' && !isset($attrs['search']) && !isset($attrs['tags']))
|
if ($matches[1] === 'date' || $matches[1] === 'select' && !isset($attrs['search']) && !isset($attrs['tags']))
|
||||||
{
|
{
|
||||||
// type attribute need to go in widget type <select type="select-account" --> <et2-select-account
|
// type attribute need to go in widget type <select type="select-account" --> <et2-select-account
|
||||||
if (empty($matches[2]) && isset($attrs['type']))
|
if(empty($matches[2]) && isset($attrs['type']))
|
||||||
{
|
{
|
||||||
$matches[1] = $attrs['type'];
|
$matches[1] = $attrs['type'];
|
||||||
$matches[3] = str_replace('type="'.$attrs['type'].'"', '', $matches[3]);
|
$matches[3] = str_replace('type="' . $attrs['type'] . '"', '', $matches[3]);
|
||||||
}
|
}
|
||||||
return '<et2-'.$matches[1].$matches[2].' '.$matches[3].'></et2-'.$matches[1].$matches[2].'>';
|
return '<et2-' . $matches[1] . $matches[2] . ' ' . $matches[3] . '></et2-' . $matches[1] . $matches[2] . '>';
|
||||||
}
|
}
|
||||||
return $matches[0];
|
return $matches[0];
|
||||||
}, $str);
|
}, $str);
|
||||||
|
|
||||||
|
// Change splitter dockside -> primary + vertical
|
||||||
|
$str = preg_replace_callback('#<split([^>]*?)>(.*)</split>#su', function ($matches) use ($name)
|
||||||
|
{
|
||||||
|
$tag = 'et2-split';
|
||||||
|
preg_match_all('/(^| )([a-z0-9_-]+)="([^"]+)"/', $matches[1], $attrs, PREG_PATTERN_ORDER);
|
||||||
|
$attrs = array_combine($attrs[2], $attrs[3]);
|
||||||
|
|
||||||
|
$attrs['vertical'] = $attrs['orientation'] == 'v' ? "true" : "false";
|
||||||
|
if(str_contains($attrs['dock_side'], 'top') || str_contains($attrs['dock_side'], 'left'))
|
||||||
|
{
|
||||||
|
$attrs['primary'] = "end";
|
||||||
|
}
|
||||||
|
elseif(str_contains($attrs['dock_side'], 'bottom') || str_contains($attrs['dock_side'], 'right'))
|
||||||
|
{
|
||||||
|
$attrs['primary'] = "start";
|
||||||
|
}
|
||||||
|
unset($attrs['dock_side']);
|
||||||
|
|
||||||
|
return "<$tag " . implode(' ', array_map(function ($name, $value)
|
||||||
|
{
|
||||||
|
return $name . '="' . $value . '"';
|
||||||
|
}, array_keys($attrs), $attrs)
|
||||||
|
) . '>' . $matches[2] . "</$tag>";
|
||||||
|
}, $str);
|
||||||
|
|
||||||
$processing = microtime(true);
|
$processing = microtime(true);
|
||||||
|
|
||||||
|
336
api/js/etemplate/Layout/Et2Split/Et2Split.ts
Normal file
336
api/js/etemplate/Layout/Et2Split/Et2Split.ts
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
/**
|
||||||
|
* EGroupware eTemplate2 - Splitter widget
|
||||||
|
*
|
||||||
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||||
|
* @package etemplate
|
||||||
|
* @subpackage api
|
||||||
|
* @link https://www.egroupware.org
|
||||||
|
* @author Nathan Gray
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {cssImage, Et2Widget} from "../../Et2Widget/Et2Widget";
|
||||||
|
import {SlSplitPanel} from "@shoelace-style/shoelace";
|
||||||
|
import {et2_IDOMNode, et2_IResizeable} from "../../et2_core_interfaces";
|
||||||
|
import {et2_DOMWidget} from "../../et2_core_DOMWidget";
|
||||||
|
import {css, html, SlotMixin} from "@lion/core";
|
||||||
|
|
||||||
|
export class Et2Split extends Et2Widget(SlotMixin(SlSplitPanel))
|
||||||
|
{
|
||||||
|
|
||||||
|
static get styles()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
...super.styles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
slot:not([name='handle'])::slotted(*) {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
::slotted(.split-handle) {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-image: ${cssImage("splitter_vert")};
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get properties()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
...super.properties,
|
||||||
|
/**
|
||||||
|
* The current position of the divider from the primary panel's edge as a percentage 0-100.
|
||||||
|
* Defaults to 50% of the container's initial size
|
||||||
|
*/
|
||||||
|
position: Number,
|
||||||
|
/**
|
||||||
|
* Draws the split panel in a vertical orientation with the start panel above the end panel
|
||||||
|
*/
|
||||||
|
vertical: Boolean,
|
||||||
|
/**
|
||||||
|
* If no primary panel is designated, both panels will resize proportionally and docking is disabled
|
||||||
|
* "start" | "end" | undefined
|
||||||
|
*/
|
||||||
|
primary: String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get slots()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
handle: () =>
|
||||||
|
{
|
||||||
|
return this._handleTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static PREF_PREFIX = "splitter-size-";
|
||||||
|
protected static PANEL_NAMES = ["start", "end"];
|
||||||
|
private _resize_timeout : NodeJS.Timeout = null;
|
||||||
|
private _undock_position : number = undefined;
|
||||||
|
|
||||||
|
// To hold troublesome elements we need to hide while resizing
|
||||||
|
private _hidden : HTMLElement[] = [];
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Bind handlers to instance
|
||||||
|
this._handleResize = this._handleResize.bind(this);
|
||||||
|
this._handleMouseDown = this._handleMouseDown.bind(this);
|
||||||
|
this._handleMouseUp = this._handleMouseUp.bind(this);
|
||||||
|
this._handleDoubleClick = this._handleDoubleClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback()
|
||||||
|
{
|
||||||
|
super.connectedCallback();
|
||||||
|
|
||||||
|
// Add listeners
|
||||||
|
this.addEventListener("sl-reposition", this._handleResize);
|
||||||
|
|
||||||
|
// Wait for everything to complete,
|
||||||
|
this.getUpdateComplete().then(() =>
|
||||||
|
{
|
||||||
|
// Divider node not available earlier
|
||||||
|
this._dividerNode.addEventListener("mousedown", this._handleMouseDown);
|
||||||
|
this._dividerNode.addEventListener("mouseup", this._handleMouseUp);
|
||||||
|
this._dividerNode.addEventListener("dblclick", this._handleDoubleClick);
|
||||||
|
|
||||||
|
// now tell legacy children to resize
|
||||||
|
this.iterateOver((widget) =>
|
||||||
|
{
|
||||||
|
// Nextmatches (and possibly other "full height" widgets) need to be adjusted
|
||||||
|
// Trigger the dynamic height thing to re-initialize
|
||||||
|
// TODO: When dynheight goes away, this can too
|
||||||
|
if(typeof widget.dynheight !== "undefined")
|
||||||
|
{
|
||||||
|
widget.dynheight.outerNode = {
|
||||||
|
// Random 3px deducted to make things fit better. Otherwise nm edges are hidden
|
||||||
|
width: () => parseInt(getComputedStyle(this.shadowRoot.querySelector(".start")).width) - 3,
|
||||||
|
height: () => parseInt(getComputedStyle(this.shadowRoot.querySelector(".start")).height) - 3,
|
||||||
|
offset: () => 0
|
||||||
|
};
|
||||||
|
widget.dynheight.initialized = false;
|
||||||
|
widget.dynheight._initialize();
|
||||||
|
widget.dynheight.bottomNodes = widget.dynheight.bottomNodes.filter((node) => (node[0].parentNode != this));
|
||||||
|
}
|
||||||
|
if(widget.resize)
|
||||||
|
{
|
||||||
|
widget.resize();
|
||||||
|
}
|
||||||
|
}, this, et2_DOMWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback()
|
||||||
|
{
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this.removeEventListener("sl-reposition", this._handleResize);
|
||||||
|
this._dividerNode.removeEventListener("mousedown", this._handleMouseDown);
|
||||||
|
this._dividerNode.removeEventListener("mouseup", this._handleMouseUp);
|
||||||
|
this._dividerNode.removeEventListener("dblclick", this._handleDoubleClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the splitter is docked
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
isDocked()
|
||||||
|
{
|
||||||
|
// Docked if we have a primary set, and we're all the way to one side
|
||||||
|
return (this.primary == "start" && this.position == 100) || (this.primary == "end" && this.position == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle docked or not
|
||||||
|
*
|
||||||
|
* @param {boolean} dock
|
||||||
|
*/
|
||||||
|
toggleDock(dock? : boolean)
|
||||||
|
{
|
||||||
|
// Need a primary panel designated so we know which one disappears
|
||||||
|
if(typeof this.primary == "undefined")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(typeof dock == "undefined")
|
||||||
|
{
|
||||||
|
dock = !this.isDocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
let undocked = (typeof this._undock_position == "undefined" || [0, 100].indexOf(this._undock_position) != -1) ? 50 : this._undock_position;
|
||||||
|
this.position = dock ? (this.primary == 'start' ? 100 : 0) : undocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
undock() { return this.toggleDock(false)}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load user's size preference
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected _loadPreference()
|
||||||
|
{
|
||||||
|
if(!this.id)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pref = this.egw().preference(Et2Split.PREF_PREFIX + this.id, this.egw().getAppName());
|
||||||
|
if(pref)
|
||||||
|
{
|
||||||
|
this.position = parseInt(pref[this.vertical ? 'sizeLeft' : 'sizeTop']);
|
||||||
|
}
|
||||||
|
this._undock_position = this.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current position to user preference
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected _savePreference()
|
||||||
|
{
|
||||||
|
if(!this.id || !this.egw() || !this.position)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store current position in preferences
|
||||||
|
let size = this.vertical ? {sizeLeft: Math.round(this.position)} : {sizeTop: Math.round(this.position)};
|
||||||
|
this.egw().set_preference(this.egw().getAppName(), Et2Split.PREF_PREFIX + this.id, size);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle changes that have to happen based on changes to properties
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
updated(changedProperties)
|
||||||
|
{
|
||||||
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
// if ID changes, check preference
|
||||||
|
if(changedProperties.has("id") && this.id)
|
||||||
|
{
|
||||||
|
this._loadPreference();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a resize
|
||||||
|
* This includes notifying any manually resizing widgets, and updating preference if needed
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param e
|
||||||
|
*/
|
||||||
|
_handleResize(e)
|
||||||
|
{
|
||||||
|
// Update where we would undock to
|
||||||
|
if(this.position != 0 && this.position != 100)
|
||||||
|
{
|
||||||
|
this._undock_position = this.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this._resize_timeout)
|
||||||
|
{
|
||||||
|
clearTimeout(this._resize_timeout);
|
||||||
|
}
|
||||||
|
this._resize_timeout = setTimeout(function()
|
||||||
|
{
|
||||||
|
this._resize_timeout = undefined;
|
||||||
|
|
||||||
|
this._savePreference();
|
||||||
|
|
||||||
|
// Tell widgets that manually resize about it
|
||||||
|
this.iterateOver(function(_widget)
|
||||||
|
{
|
||||||
|
_widget.resize();
|
||||||
|
}, self, et2_IResizeable);
|
||||||
|
}.bind(this), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle doubleclick (on splitter bar) to dock
|
||||||
|
*/
|
||||||
|
_handleDoubleClick(e)
|
||||||
|
{
|
||||||
|
this.toggleDock();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide child iframes, they screw up sizing
|
||||||
|
* @param e
|
||||||
|
*/
|
||||||
|
_handleMouseDown(e)
|
||||||
|
{
|
||||||
|
const hidden = ['iframe'];
|
||||||
|
for(let tag of hidden)
|
||||||
|
{
|
||||||
|
let hide = this.querySelectorAll(tag);
|
||||||
|
this._hidden.push(...hide);
|
||||||
|
for(let h of hide)
|
||||||
|
{
|
||||||
|
h.style.visibility = "hidden";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show any hidden children
|
||||||
|
*/
|
||||||
|
_handleMouseUp(e)
|
||||||
|
{
|
||||||
|
for(let h of this._hidden)
|
||||||
|
{
|
||||||
|
h.style.visibility = "initial";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML template for split handle
|
||||||
|
*/
|
||||||
|
_handleTemplate()
|
||||||
|
{
|
||||||
|
return html`
|
||||||
|
<div class="split-handle"></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _dividerNode() : HTMLElement
|
||||||
|
{
|
||||||
|
return this.shadowRoot.querySelector("[part='divider']");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the widget tree from an XML node
|
||||||
|
* Overridden here to auto-assign slots if not set
|
||||||
|
*
|
||||||
|
* @param _node xml node
|
||||||
|
*/
|
||||||
|
loadFromXML(_node)
|
||||||
|
{
|
||||||
|
super.loadFromXML(_node);
|
||||||
|
|
||||||
|
for(let i = 0; i < this.getChildren().length && i < Et2Split.PANEL_NAMES.length; i++)
|
||||||
|
{
|
||||||
|
let child = (<et2_IDOMNode>this.getChildren()[i]).getDOMNode();
|
||||||
|
if(child && !child.getAttribute("slot"))
|
||||||
|
{
|
||||||
|
child.setAttribute("slot", Et2Split.PANEL_NAMES[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("et2-split", Et2Split as any);
|
@ -56,6 +56,7 @@ import './Et2Url/Et2UrlPhone';
|
|||||||
import './Et2Url/Et2UrlPhoneReadonly';
|
import './Et2Url/Et2UrlPhoneReadonly';
|
||||||
import './Et2Url/Et2UrlFax';
|
import './Et2Url/Et2UrlFax';
|
||||||
import './Et2Url/Et2UrlFaxReadonly';
|
import './Et2Url/Et2UrlFaxReadonly';
|
||||||
|
import "./Layout/Et2Split/Et2Split";
|
||||||
|
|
||||||
/* Include all widget classes here, we only care about them registering, not importing anything*/
|
/* Include all widget classes here, we only care about them registering, not importing anything*/
|
||||||
import './et2_widget_vfs'; // Vfs must be first (before et2_widget_file) due to import cycle
|
import './et2_widget_vfs'; // Vfs must be first (before et2_widget_file) due to import cycle
|
||||||
|
@ -64,6 +64,7 @@
|
|||||||
"@lion/listbox": "^0.12.0",
|
"@lion/listbox": "^0.12.0",
|
||||||
"@lion/select": "^0.15.0",
|
"@lion/select": "^0.15.0",
|
||||||
"@lion/textarea": "^0.14.0",
|
"@lion/textarea": "^0.14.0",
|
||||||
|
"@shoelace-style/shoelace": "^2.0.0-beta.73",
|
||||||
"jquery-ui-timepicker-addon": "^1.6.3",
|
"jquery-ui-timepicker-addon": "^1.6.3",
|
||||||
"lit-flatpickr": "^0.3.0",
|
"lit-flatpickr": "^0.3.0",
|
||||||
"sortablejs": "^1.14.0"
|
"sortablejs": "^1.14.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user