Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to bootstrap-vue popovers #1033

Merged
merged 12 commits into from
Mar 3, 2020
29 changes: 29 additions & 0 deletions asset/css/markbind.css
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,32 @@ li.footnote-item:target {
top: 0;
width: 3em;
}

/* hide popover, modal, tooltip content */
[data-mb-html-for] {
display: none;
}

/* styles for triggers */
.trigger {
text-decoration: underline dotted;
}

.modal.mb-zoom {
-webkit-transform: scale(0.1);
-moz-transform: scale(0.1);
-ms-transform: scale(0.1);
transform: scale(0.1);
opacity: 0;
-webkit-transition: all 0.3s;
-moz-transition: all 0.3s;
transition: all 0.3s;
}

.modal.mb-zoom.show {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
transform: scale(1);
opacity: 1;
}
33 changes: 33 additions & 0 deletions asset/js/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,39 @@ function setupWithSearch() {
setupSiteNav();
}

function makeInnerGetterFor(attribute) {
return (element) => {
const innerElement = element.querySelector(`[data-mb-html-for="${attribute}"]`);
return innerElement === null ? '' : innerElement.innerHTML;
};
}

function makeHtmlGetterFor(componentType, attribute) {
return (element) => {
const contentWrapper = document.getElementById(element.attributes.for.value);
return contentWrapper.dataset.mbComponentType === componentType
? makeInnerGetterFor(attribute)(contentWrapper) : '';
};
}

/* eslint-disable no-unused-vars */
/*
These getters are used by triggers to get their popover/tooltip content.
We need to create a completely new popover/tooltip for each trigger due to bootstrap-vue's implementation,
so this is how we retrieve our contents.
*/
const popoverContentGetter = makeHtmlGetterFor('popover', 'content');
const popoverHeaderGetter = makeHtmlGetterFor('popover', 'header');
const popoverInnerContentGetter = makeInnerGetterFor('content');
const popoverInnerHeaderGetter = makeInnerGetterFor('header');

const popoverGenerator = { title: popoverHeaderGetter, content: popoverContentGetter };
const popoverInnerGenerator = { title: popoverInnerHeaderGetter, content: popoverInnerContentGetter };

const tooltipContentGetter = makeHtmlGetterFor('tooltip', '_content');
const tooltipInnerContentGetter = makeInnerGetterFor('_content');
/* eslint-enable no-unused-vars */

if (enableSearch) {
setupWithSearch();
} else {
Expand Down
2 changes: 1 addition & 1 deletion docs/userGuide/syntax/extra/triggers.mbdf
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ Additionally, multiple Triggers could share the same overlay by providing them w

Name | Type | Default | Description
---- | ---- | ------- | ------
trigger | `String` | `hover` | How the overlay view is triggered.<br>Supports: `click`, `focus`, `hover`, `contextmenu`.
trigger | `String` | `hover` | How the overlay view is triggered.<br>Supports: `click`, `focus`, `hover`.
for | `String` | `null` | The id for the overlay view to be shown.
4 changes: 1 addition & 3 deletions docs/userGuide/syntax/modals.mbdf
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ Name | type | Default | Description
header <hr style="margin-top:0.2rem; margin-bottom:0" /> <small>title <br> (deprecated)</small> | `String` | `''` | Header of the Modal component. Supports inline markdown text.
ok-text | `String` | `''` | Text for the OK button.
effect | `String` | `zoom` | Supports: `zoom`, `fade`.
id | `String` | | Used by [Trigger](#trigger) to activate the Modal by id.
width | `Number`, `String`, or `null` | `null` | Passing a `Number` will be translated to pixels.<br>`String` can be passed [CSS units](https://www.w3schools.com/cssref/css_units.asp), ( e.g. '50in' or '30vw' ).<br>`null` will default to Bootstrap's responsive sizing.
large | `Boolean` | `false` | Creates a [large Modal](https://getbootstrap.com/docs/4.0/components/modal/#optional-sizes).
id | `String` | | Used by [Trigger](#trigger) to activate the Modal by id.large | `Boolean` | `false` | Creates a [large Modal](https://getbootstrap.com/docs/4.0/components/modal/#optional-sizes).
small | `Boolean` | `false` | Creates a [small Modal](https://getbootstrap.com/docs/4.0/components/modal/#optional-sizes).
center | `Boolean` | `false` | Vertically centers the modal (in addition to the horizontal centering by default).
backdrop | `Boolean` | `true` | Enables closing the Modal by clicking on the backdrop.
Expand Down
6 changes: 1 addition & 5 deletions docs/userGuide/syntax/popovers.mbdf
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,6 @@
<popover effect="scale" header="Header" content="Lorem ipsum dolor sit amet" placement="top" trigger="hover">
<button class="btn btn-secondary">Mouseenter</button>
</popover>
<popover effect="scale" header="Header" content="Lorem ipsum dolor sit amet" placement="top" trigger="contextmenu">
<button class="btn btn-secondary">Contextmenu (right click)</button>
</popover>
</p>
<h4 class="no-index">Markdown</h4>
<p>
Expand Down Expand Up @@ -146,8 +143,7 @@ This is the same <trigger for="pop:trigger_id">trigger</trigger> as last one.

Name | Type | Default | Description
---- | ---- | ------- | ------
trigger | `String` | `hover` | How the Popover is triggered.<br>Supports: `click`, `focus`, `hover`, `contextmenu`.
effect | `String` | `fade` | Transition effect for Popover.<br>Supports: `scale`, `fade`.
trigger | `String` | `hover` | How the Popover is triggered.<br>Supports: `click`, `focus`, `hover`.
header <hr style="margin-top:0.2rem; margin-bottom:0" /> <small>title <br> (deprecated)</small> | `String` | `''` | Popover header, supports inline markdown text.
content | `String` | `''` | Popover content, supports inline markdown text.
placement | `String` | `top` | How to position the Popover.<br>Supports: `top`, `left`, `right`, `bottom`.
Expand Down
5 changes: 1 addition & 4 deletions docs/userGuide/syntax/tooltips.mbdf
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,6 @@ Trigger
<tooltip effect="scale" content="Lorem ipsum dolor sit amet" placement="top" trigger="click">
<button class="btn btn-secondary">Click</button>
</tooltip>
<tooltip effect="scale" content="Lorem ipsum dolor sit amet" placement="top" trigger="contextmenu">
<button class="btn btn-secondary">Contextmenu (right click)</button>
</tooltip>
<br />
<br />
<tooltip effect="scale" content="Lorem ipsum dolor sit amet" placement="top" trigger="focus">
Expand Down Expand Up @@ -111,7 +108,7 @@ This is the same <trigger for="tt:trigger_id">trigger</trigger> as last one.

Name | Type | Default | Description
---- | ---- | ------- | ------
trigger | `String` | `hover` | How the tooltip is triggered.<br>Supports: `click`, `focus`, `hover`, `contextmenu`.
trigger | `String` | `hover` | How the tooltip is triggered.<br>Supports: `click`, `focus`, `hover`.
content | `String` | `''` | Text content of the tooltip.
placement | `String` | `top` | How to position the tooltip.<br>Supports: `top`, `left`, `right`, `bottom`.

Expand Down
152 changes: 136 additions & 16 deletions src/lib/markbind/src/parsers/componentParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ function _parseAttributeWithoutOverride(element, attribute, isInline, slotName =
delete el.attribs[attribute];
}

/**
* Takes an element, looks for direct elements with slots and transforms to avoid Vue parsing.
* This is so that we can use bootstrap-vue popovers, tooltips, and modals.
* @param element Element to transform
*/
function _transformSlottedComponents(element) {
element.children.forEach((child) => {
const c = child;
const slot = c.attribs && c.attribs.slot;
if (slot) {
// Turns <div slot="content">... into <div data-mb-html-for=content>...
c.attribs['data-mb-html-for'] = slot;
delete c.attribs.slot;
}
// similarly, need to transform templates to avoid Vue parsing
if (c.name === 'template') {
c.name = 'span';
}
});
}

/*
* Panels
*/
Expand Down Expand Up @@ -119,30 +140,80 @@ function _assignPanelId(element) {
}
}

/*
* Triggers
*
* At "compile time", we can't tell whether a trigger references a modal, popover, or toolip,
* since that element might not have been processed yet.
*
* So, we make every trigger try all 3. It will attempt to open a tooltip, popover, and modal.
*
* For tooltips and popovers, we call the relevant content getters inside asset/js/setup.js.
* They will check to see if the element id exists, and whether it is a popover/tooltip,
* and then return the content as needed.
*
* For modals, we make it attempt to show the modal if it exists.
*/

function _parseTrigger(element) {
openorclose marked this conversation as resolved.
Show resolved Hide resolved
const el = element;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for el and element

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, in the above we seem to be favouring explicitness (child instead of c), but here we seem to be favouring terseness. Is there a reason for this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change the child function then, to keep it more consistent.

el.name = 'span';
const trigger = el.attribs.trigger || 'hover';
const placement = el.attribs.placement || 'top';
el.attribs[`v-b-popover.${trigger}.${placement}.html`]
= 'popoverGenerator';
el.attribs[`v-b-tooltip.${trigger}.${placement}.html`]
= 'tooltipContentGetter';
const convertedTrigger = trigger === 'hover' ? 'mouseover' : trigger;
el.attribs[`v-on:${convertedTrigger}`] = `$refs['${el.attribs.for}'].show()`;
el.attribs.class = el.attribs.class ? `${el.attribs.class} trigger` : 'trigger';
}

/*
* Popovers
*
* We hide the content and header via _transformSlottedComponents, for retrieval by triggers.
*
* Then, we add in a trigger for this popover.
*/

function _parsePopoverAttributes(element) {
_parseAttributeWithoutOverride(element, 'content', true);
_parseAttributeWithoutOverride(element, 'header', true);
function _parsePopover(element) {
const el = element;
_parseAttributeWithoutOverride(el, 'content', true);
_parseAttributeWithoutOverride(el, 'header', true);
// TODO deprecate title attribute for popovers
_parseAttributeWithoutOverride(element, 'title', true, 'header');
_parseAttributeWithoutOverride(el, 'title', true, 'header');

el.name = 'span';
const trigger = el.attribs.trigger || 'hover';
const placement = el.attribs.placement || 'top';
el.attribs['data-mb-component-type'] = 'popover';
el.attribs[`v-b-popover.${trigger}.${placement}.html`]
= 'popoverInnerGenerator';
el.attribs.class = el.attribs.class ? `${el.attribs.class} trigger` : 'trigger';
_transformSlottedComponents(el);
}

/*
* Tooltips
*
* Similar to popovers.
*/

function _parseTooltipAttributes(element) {
_parseAttributeWithoutOverride(element, 'content', true, '_content');
function _parseTooltip(element) {
const el = element;
_parseAttributeWithoutOverride(el, 'content', true, '_content');

el.name = 'span';
const trigger = el.attribs.trigger || 'hover';
const placement = el.attribs.placement || 'top';
el.attribs['data-mb-component-type'] = 'tooltip';
el.attribs[`v-b-tooltip.${trigger}.${placement}.html`]
= 'tooltipInnerContentGetter';
el.attribs.class = el.attribs.class ? `${el.attribs.class} trigger` : 'trigger';
_transformSlottedComponents(el);
}

/*
* Modals
*/

function _renameSlot(element, originalName, newName) {
if (element.children) {
element.children.forEach((c) => {
Expand All @@ -155,14 +226,60 @@ function _renameSlot(element, originalName, newName) {
}
}

function _renameAttribute(element, originalAttribute, newAttribute) {
const el = element;
if (_.has(el.attribs, originalAttribute)) {
el.attribs[newAttribute] = el.attribs[originalAttribute];
delete el.attribs[originalAttribute];
}
}

/*
* Modals
*
* We are using bootstrap-vue modals, and some of their attributes/slots differ from ours.
* So, we will transform from markbind modal syntax into bootstrap-vue modal syntax.
*/

function _parseModalAttributes(element) {
_parseAttributeWithoutOverride(element, 'header', true, '_header');
const el = element;
_parseAttributeWithoutOverride(el, 'header', true, 'modal-title');
// TODO deprecate title attribute for modals
_parseAttributeWithoutOverride(element, 'title', true, '_header');
_parseAttributeWithoutOverride(el, 'title', true, 'modal-title');

// TODO deprecate modal-header, modal-footer attributes for modals
_renameSlot(element, 'modal-header', 'header');
_renameSlot(element, 'modal-footer', 'footer');
_renameSlot(el, 'header', 'modal-header');
_renameSlot(el, 'footer', 'modal-footer');

el.name = 'b-modal';

_renameAttribute(el, 'ok-text', 'ok-title');
_renameAttribute(el, 'center', 'centered');

el.attribs['ok-only'] = ''; // only show OK button

if (el.attribs.backdrop === 'false') {
el.attribs['no-close-on-backdrop'] = '';
}
delete el.attribs.backdrop;

let size = '';
if (_.has(el.attribs, 'large')) {
size = 'lg';
delete el.attribs.large;
} else if (_.has(el.attribs, 'small')) {
size = 'sm';
delete el.attribs.small;
}
el.attribs.size = size;

// default for markbind is zoom, default for bootstrap-vue is fade
const effect = el.attribs.effect === 'fade' ? '' : 'mb-zoom';
el.attribs['modal-class'] = effect;

if (_.has(el.attribs, 'id')) {
el.attribs.ref = el.attribs.id;
}
}

/*
Expand Down Expand Up @@ -224,11 +341,14 @@ function parseComponents(element, errorHandler) {
case 'panel':
_parsePanelAttributes(element);
break;
case 'trigger':
_parseTrigger(element);
break;
case 'popover':
_parsePopoverAttributes(element);
_parsePopover(element);
break;
case 'tooltip':
_parseTooltipAttributes(element);
_parseTooltip(element);
break;
case 'modal':
_parseModalAttributes(element);
Expand Down
35 changes: 13 additions & 22 deletions test/functional/test_site/expected/bugs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,44 +38,35 @@
<p><strong>popover initiated by trigger: honor trigger attribute</strong></p>
<p><a href="https://github.com/MarkBind/markbind/issues/49">Issue #49</a></p>
<p>Repro:</p>
<p>
<trigger for="pop:xp-user-stories">Establishing Requirements</trigger>
</p>
<popover id="pop:xp-user-stories" trigger="click">
<div slot="content">
<div>
<p>Requirements gathering, requirements elicitation, requirements analysis, requirements capture are some of the terms commonly <strong>and</strong> interchangeably used to represent the activity of understanding what a software product should
do.
</p>
</div>
</div>
</popover>
<p><span for="pop:xp-user-stories" v-b-popover.hover.top.html="popoverGenerator" v-b-tooltip.hover.top.html="tooltipContentGetter" v-on:mouseover="$refs['pop:xp-user-stories'].show()" class="trigger">Establishing Requirements</span></p>
<span id="pop:xp-user-stories" trigger="click" data-mb-component-type="popover" v-b-popover.click.top.html="popoverInnerGenerator" class="trigger">
<div data-mb-html-for="content">
<div>
<p>Requirements gathering, requirements elicitation, requirements analysis,
requirements capture are some of the terms commonly <strong>and</strong> interchangeably used to represent the activity
of understanding what a software product should do.</p></div></div></span>
<p><strong>Support multiple inclusions of a modal</strong></p>
<p><a href="https://github.com/MarkBind/markbind/issues/107">Issue #107</a></p>
<p>Repro:</p>
<div>
<p>This is to reproduce
<trigger trigger="click" for="modal:bugRepro">multiple inclusions of a modal bug</trigger>
</p>
<modal large="" id="modal:bugRepro"><template slot="_header">Establishing Requirements</template>
<p>This is to reproduce <span trigger="click" for="modal:bugRepro" v-b-popover.click.top.html="popoverGenerator" v-b-tooltip.click.top.html="tooltipContentGetter" v-on:click="$refs['modal:bugRepro'].show()" class="trigger">multiple inclusions of a modal bug</span></p>
<b-modal id="modal:bugRepro" ok-only="" size="lg" modal-class="mb-zoom" ref="modal:bugRepro"><template slot="modal-title">Establishing Requirements</template>
<div>
<p>Requirements gathering, requirements elicitation, requirements analysis, requirements capture are some of the terms commonly <strong>and</strong> interchangeably used to represent the activity of understanding what a software product should
do.
</p>
</div>
</modal>
</b-modal>
</div>
<div>
<p>This is to reproduce
<trigger trigger="click" for="modal:bugRepro">multiple inclusions of a modal bug</trigger>
</p>
<modal large="" id="modal:bugRepro"><template slot="_header">Establishing Requirements</template>
<p>This is to reproduce <span trigger="click" for="modal:bugRepro" v-b-popover.click.top.html="popoverGenerator" v-b-tooltip.click.top.html="tooltipContentGetter" v-on:click="$refs['modal:bugRepro'].show()" class="trigger">multiple inclusions of a modal bug</span></p>
<b-modal id="modal:bugRepro" ok-only="" size="lg" modal-class="mb-zoom" ref="modal:bugRepro"><template slot="modal-title">Establishing Requirements</template>
<div>
<p>Requirements gathering, requirements elicitation, requirements analysis, requirements capture are some of the terms commonly <strong>and</strong> interchangeably used to represent the activity of understanding what a software product should
do.
</p>
</div>
</modal>
</b-modal>
</div>
<p><strong>Remove extra space in links</strong></p>
<p><a href="https://github.com/MarkBind/markbind/issues/147">Issue #147</a></p>
Expand Down
Loading