Skip to content

Commit

Permalink
Migrate to bootstrap-vue popovers (#1033)
Browse files Browse the repository at this point in the history
  • Loading branch information
openorclose committed Mar 3, 2020
1 parent 2eb7b7d commit f215eee
Show file tree
Hide file tree
Showing 27 changed files with 640 additions and 113 deletions.
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) {
const el = element;
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

0 comments on commit f215eee

Please sign in to comment.