/ Gists

Gists

On gists

Vue directive + class tooltip (4 learning)

Vue.js

directive.js #

/* https://github.com/hekigan/vue-directive-tooltip */


/*
 * @author: laurent blanes <laurent.blanes@gmail.com>
 * @tutorial: https://hekigan.github.io/vue-directive-tooltip/
 */
import Tooltip from './tooltip.js';

const BASE_CLASS = 'vue-tooltip';
const POSITIONS = ['auto', 'top', 'bottom', 'left', 'right'];
const SUB_POSITIONS = ['start', 'end'];

/**
 * usage:
 *
 * // basic usage:
 * <div v-tooltip="'my content'">
 * or
 * <div v-tooltip="{content: 'my content'}">
 *
 * // change position of tooltip
 * // options: auto (default) | bottom | top | left | right
 *
 * // change sub-position of tooltip
 * // options: start | end
 *
 * <div v-tooltip.top="{content: 'my content'}">
 *
 * // add custom class
 * <div v-tooltip="{class: 'custom-class', content: 'my content'}">
 *
 * // toggle visibility
 * <div v-tooltip="{visible: false, content: 'my content'}">
 */
export default {
    name: 'tooltip',
    config: {},
    install (Vue, installOptions) {
        Vue.directive('tooltip', {
            bind (el, binding, vnode) {
                if (installOptions) {
                    Tooltip.defaults(installOptions);
                }
            },
            inserted (el, binding, vnode, oldVnode) {
                if (installOptions) {
                    Tooltip.defaults(installOptions);
                }

                let options = filterBindings(binding, vnode);
                el.tooltip = new Tooltip(el, options);

                if (binding.modifiers.notrigger && binding.value.visible === true) {
                    el.tooltip.show();
                }

                if (binding.value && binding.value.visible === false) {
                    el.tooltip.disabled = true;
                }
            },
            componentUpdated (el, binding, vnode, oldVnode) {
                if (hasUpdated(binding.value, binding.oldValue)) {
                    update(el, binding, vnode, oldVnode);
                }
            },
            unbind (el, binding, vnode, oldVnode) {
                el.tooltip.destroy();
            }
        });
    }
};

/**
 *
 * @param {*} vnode component's properties
 * @param {*} oldvnode component's previous properties
 * @return boolean
 */
function hasUpdated (value, oldValue) {
    let updated = false;

    if (typeof value === 'string' && value !== oldValue) {
        updated = true;
    } else if (isObject(value)) {
        Object.keys(value).forEach(prop => {
            if (value[prop] !== oldValue[prop]) {
                updated = true;
            }
        });
    }
    return updated;
}

/**
 * Sanitize data
 * @param {*} binding
 * @param {*} vnode
 * @return {*} filtered data object
 */
function filterBindings (binding, vnode) {
    const delay = !binding.value || isNaN(binding.value.delay) ? Tooltip._defaults.delay : binding.value.delay;

    if (binding.value.ref) {
        if (vnode.context.$refs[binding.value.ref]) {
            binding.value.html = vnode.context.$refs[binding.value.ref];
        } else {
            console.error(`[Tooltip] no REF element [${binding.value.ref}]`); // eslint-disable-line
        }
    }

    return {
        class: getClass(binding),
        id: (binding.value) ? binding.value.id : null,
        html: (binding.value) ? binding.value.html : null,
        placement: getPlacement(binding),
        title: getContent(binding),
        triggers: getTriggers(binding),
        fixIosSafari: binding.modifiers.ios || false,
        offset: (binding.value && binding.value.offset) ? binding.value.offset : Tooltip._defaults.offset,
        delay
    };
}

/**
 * Get placement from modifiers
 * @param {*} binding
 */
function getPlacement ({modifiers, value}) {
    let MODS = Object.keys(modifiers);
    if (MODS.length === 0 && isObject(value) && typeof value.placement === 'string') {
        MODS = value.placement.split('.');
    }
    let head = 'bottom';
    let tail = null;
    for (let i = 0; i < MODS.length; i++) {
        const pos = MODS[i];
        if (POSITIONS.indexOf(pos) > -1) {
            head = pos;
        }
        if (SUB_POSITIONS.indexOf(pos) > -1) {
            tail = pos;
        }
    }
    // console.log((head && tail) ? `${head}-${tail}` : head);
    // return 'auto';
    return (head && tail) ? `${head}-${tail}` : head;
}

/**
 * Get trigger value from modifiers
 * @param {*} binding
 * @return String
 */
function getTriggers ({modifiers}) {
    let trigger = [];
    if (modifiers.notrigger) {
        return trigger;
    } else if (modifiers.manual) {
        trigger.push('manual');
    } else {
        if (modifiers.click) {
            trigger.push('click');
        }

        if (modifiers.hover) {
            trigger.push('hover');
        }

        if (modifiers.focus) {
            trigger.push('focus');
        }

        if (trigger.length === 0) {
            trigger.push('hover', 'focus');
        }
    }

    return trigger;
}

/**
 * Check if the variable is an object
 * @param {*} value
 * @return Boolean
 */
function isObject (value) {
    return typeof value === 'object';
}

/**
 * Check if the variable is an html element
 * @param {*} value
 * @return Boolean
 */
function isElement (value) {
    return value instanceof window.Element;
}

/**
 * Get the css class
 * @param {*} binding
 * @return HTMLElement | String
 */
function getClass ({value}) {
    if (value === null) {
        return BASE_CLASS;
    } else if (isObject(value) && typeof value.class === 'string') {
        return `${BASE_CLASS} ${value.class}`;
    } else if (Tooltip._defaults.class) {
        return `${BASE_CLASS} ${Tooltip._defaults.class}`;
    } else {
        return BASE_CLASS;
    }
}

/**
 * Get the content
 * @param {*} binding
 * @return HTMLElement | String
 */
function getContent ({value}, vnode) {
    if (value !== null && isObject(value)) {
        if (value.content !== undefined) {
            return `${value.content}`;
        } else if (value.id && document.getElementById(value.id)) {
            return document.getElementById(value.id);
        } else if (value.html && document.getElementById(value.html)) {
            return document.getElementById(value.html);
        } else if (isElement(value.html)) {
            return value.html;
        } else if (value.ref && vnode) {
            return vnode.context.$refs[value.ref] || '';
        } else {
            return '';
        }
    } else {
        return `${value}`;
    }
}

/**
 * Action on element update
 * @param {*} el Vue element
 * @param {*} binding
 */
function update (el, binding, vnode, oldVnode) {
    if (typeof binding.value === 'string') {
        el.tooltip.content(binding.value);
    } else {
        if (binding.value && binding.value.class && binding.value.class.trim() !== el.tooltip.options.class.replace(BASE_CLASS, '').trim()) {
            el.tooltip.class = `${BASE_CLASS} ${binding.value.class.trim()}`;
        }

        el.tooltip.content(getContent(binding, vnode));

        if (!binding.modifiers.notrigger && binding.value && typeof binding.value.visible === 'boolean') {
            el.tooltip.disabled = !binding.value.visible;
            return;
        } else if (binding.modifiers.notrigger) {
            el.tooltip.disabled = false;
        }

        const dir = vnode.data.directives[0];

        if (dir.oldValue.visible !== dir.value.visible) {
            if (!el.tooltip.disabled) {
                el.tooltip.toggle(dir.value.visible);
            }
        }
    }
}

On gists

Pivot table via prepared statements

MySql MySql tricks MySql - advanced

pivot.sql #

create table tbl_values (
  id int unsigned not null primary key,
  `Name` varchar(10),
  `Group` varchar(10),
  `Value` int
);

insert into tbl_values values
(1, 'Pete', 'A', 10),
(2, 'Pete', 'B', 20),
(3, 'John', 'A', 10);

-- 1. Create an expression that builds the columns
set @sql = (
    select group_concat(distinct 
        concat(
            "sum(case when `Group`='", `Group`, "' then `Value` end) as `", `Group`, "`"
        )
    ) 
    from tbl_values
);

-- 2. Complete the SQL instruction
set @sql = concat("select Name, ", @sql, " from tbl_values group by `Name`");

-- 3. Create a prepared statement
prepare stmt from @sql;

-- 4. Execute the prepared statement
execute stmt;

On gists

OnInvalidClick - Nette form

Nette-Forms AW

onInvalidClick.php #

<?php
  // https://git.andweb.cz/brandcloud/app/-/blob/bcnew/app/BrandCloudModule/components/SharingForm.php#L148

	$form->addSubmit('share', 'Share')
			->onInvalidClick[] = function () use ($form) {
				// we have to set recipients as items to revalidate recipients field
				// TODO: posibly change multiselectbox to some other implementation which works only with raw value
				if ($form['sharing_type']->getValue() === 'email') {
					$form['recipients']->setItems((array) $form['recipients']->getRawValue(), false);
					$form->validate();
				}
			};

		$form->onValidate[] = [$this, 'validateForm'];
		$form->onSubmit[] = [$this, 'submitForm'];
		$form->onSuccess[] = [$this, 'successForm'];
		$form->setDefaults($this->getFormDefaults());
		return $form;

On gists

CSS (how to write logical statement)

CSS CSS trick

solution.css #

/* https://stackoverflow.com/questions/2797091/css-and-and-or */

/* or || */
.registration_form_right input:not([type="radio"]):not([type="checkbox"]) {}

/* and && */
.registration_form_right input:not([type="radio"]), 
.registration_form_right input:not([type="checkbox"])

On gists

External links only CSS

CSS CSS trick

external-links.css #

/* https://davidwalsh.name/external-links-css */

/* long version */
a[href^="http://"]:not([href*="mysite.com"]),
a[href^="https://"]:not([href*="mysite.com"]), 
a[href^="//"]:not([href*="mysite.com"]), {
    
}
/* shorter version! */
a[href*="//"]:not([href*="mysite.com"]) {
    /* external link styles, use :before or :after if you want! */
}

On gists

Full width

CSS CSS trick

full.css #

.full-width {
	left: 50%;
	margin-left: -50vw;
	margin-right: -50vw;
	max-width: 100vw;
	position: relative;
	right: 50%;
	width: 100vw;
}


.full-width2 {
  width: 100vw;
  position: relative;
  left: 50%;
  transform: translateX(-50%);
  background: coral;
}

.full-width3 {
  /* max-width: 100vw; */
  width: 100vw;
  margin-left: 50%;
  transform: translateX(-50%);
  background: lime;
 
}

On gists

Store/Restore Request

Nette Nette-Tricks

Presenter.php #

<?php

	public function actionDefault($back = null) 
	{
		

		if ($back)
			$this->restoreRequest($back);

		if ($this->presenter->navigation->navItem['alias'] == 'allOpenCalls')
		{
			
			$parentId = $this->presenter->navigation->navItem['parent__navigation_id'];
			$nav = $this->navigation->getById($parentId);
			$children = $nav->getChildren();

			$this->config['navigation_id'] = array_keys($children);

		}


		$this->template->back = $this->storeRequest();

	}

On gists

Composition API - examples

Vue.js

composition.vue #

<template>
  <div>
    <h2 class="text-h4">{{ name }}</h2>
    <button @click="score++" class="btn">
      {{ score }} twice is {{ double }}
    </button>

    <hr />

    <button @click="score2++" class="btn">
      {{ score2 }} triple is {{ triple }}
    </button>

    <hr />
    <ul>
      <li v-for="data in dataList" :key="data.id">{{ data.name }}</li>
    </ul>

    <hr />

    <button v-if="!divVisible" @click="showDiv">Show div</button>
    <button v-if="divVisible" @click="hideDiv">Hide div</button>
    <div v-if="divVisible" class="bg-red-100">My super hidden div :)</div>

    <hr />
    <form @submit.prevent="submitForm">
      <input type="text" v-model="title" />
    </form>
  </div>
</template>

<script>
import { ref, computed, reactive, toRefs, onMounted } from 'vue'
import { data } from '@/frontapp/components/data.js'

export default {
  components: {},
  props: ['name'],

  // props, context nebo { emit } nebo { emit, slots }
  setup(props, { emit }) {
    console.log(props, 'our props')

    // data
    const score = ref(1)
    const dude = ref('Kcko')
    const dataList = ref(data)

    const state = reactive({
      score2: 12,
      triple: computed(() => state.score2 * 3),
      divVisible: false
    })

    // computed
    const double = computed(() => score.value * 2 + 7)
    const hello = computed(() => 'Hello, I am ' + dude.value)

    // methods
    const showDiv = () => {
      state.divVisible = true
    }
    const hideDiv = () => {
      state.divVisible = false
    }

    // v nadrazenem prvku / komponentne nebo  standardne -> view @new-list-coming="nejakaMetoda($event)"
    const title = ref('')
    const submitForm = () => {
      emit('new-list-coming', title.value)
    }

    onMounted(() => {
      console.log('Mounted yeah!')
    })

    return {
      score,
      double,
      hello,
      ...toRefs(state),
      dataList,
      showDiv,
      hideDiv,
      submitForm
    }
  }
}
</script>

On gists

ICal Event Parser

PHP

ICallParser.php #

<?php

/*
FORK from https://gist.github.com/edewaal97/344ec108931ac4c9e8ca7874b2db83cb
*/


class iCal
{
	/**
	 * @var string
	 */
	public $calendarIdent;

	/**
	 * @var string
	 */
	public $title;

	/**
	 * @var string
	 */
	public $description;

	/**
	 * @var array
	 */
	public $events = array();

	/**
	 * @var array
	 */
	protected $_eventsByDate;


	public function __construct($content = null, $calendarIdent = null)
	{

		$this->calendarIdent = $calendarIdent;
		if ($content) {
			$isUrl = strpos($content, 'http') === 0 && filter_var($content, FILTER_VALIDATE_URL);
			$isFile = strpos($content, "\n") === false && file_exists($content);
			if ($isUrl || $isFile) {
				$content = file_get_contents($content);
			}
			$this->parse($content);
		}

		
	}


	public function title()
	{
		return $this->summary;
	}

	public function description()
	{
		return $this->description;
	}

	public function events()
	{
		return $this->events;
	}

	public function eventsByDate()
	{
		if (! $this->_eventsByDate) {
			$this->_eventsByDate = array();
			$tmpEventsByDate = array();

			foreach ($this->events() as $event) {
				foreach ($event->occurrences() as $occurrence) {
					$date = date('Y-m-d', $occurrence);
					$newevent = clone $event;
					$newevent->fixOccurringDate($occurrence);
					// generate key for sorting
					$key = strtotime($newevent->dateStart);
					while(isset($tmpEventsByDate[$date][$key])) $key++;
					$tmpEventsByDate[$date][$key] = $newevent;
				}
			}

			// sort array
			ksort($tmpEventsByDate);
			foreach ($tmpEventsByDate as $date => $value) {
				ksort($value);
				$this->_eventsByDate[$date] = $value;
			}

			// prevent duplicates for edited dates in recurring events
			foreach ($this->_eventsByDate as $dateKey => $date) {
				foreach ($date as $event) {
					if(!empty($event->recurrenceId)) {
						$uid = $event->uid;

						foreach ($date as $eventKey => $eventValue) {
							if($eventValue->uid == $uid && (empty($eventValue->recurrenceId))) {
								unset($this->_eventsByDate[$dateKey][$eventKey]);
							}
						}

					}
				}
			}
		}

		return $this->_eventsByDate;
	}

	public function eventsByDateBetween($start, $end, int $limit=NULL)
	{
		if ((string) (int) $start !== (string) $start) {
			$start = strtotime($start);
		}
		$start = date('Y-m-d', $start);

		if ((string) (int) $end !== (string) $end) {
			$end = strtotime($end);
		}
		$end = date('Y-m-d', $end);

		$return = array();
		foreach ($this->eventsByDate() as $date => $events) {
			if ($start <= $date && $date < $end) {
				if(empty($limit) || count($return) <= $limit) {
					$return[$date] = $events;
				}
			}
			if(!empty($limit) && count($return) >= $limit){
				break;
			}
		}

		return $return;
	}

	public function eventsByDateSince($start, int $limit=NULL)
	{
		if ((string) (int) $start !== (string) $start) {
			$start = strtotime($start);
		}
		$start = date('Y-m-d', $start);

		$return = array();
		foreach ($this->eventsByDate() as $date => $events) {
			if ($start <= $date) {
				if(empty($limit) || count($return) <= $limit) {
					$return[$date] = $events;
				}
			}
			if(!empty($limit) && count($return) >= $limit){
				break;
			}
		}

		return $return;
	}

	public function eventsByDateUntil($end, int $limit=NULL)
	{
		if ((string) (int) $end !== (string) $end) {
			$end = strtotime($end);
		}

		$start = date('Y-m-d');
		$end = date('Y-m-d', $end);
		$return = array();
		foreach ($this->eventsByDate() as $date => $events) {
			if ($start <= $date && $end >= $date) {
				if(empty($limit) || count($return) <= $limit) {
					$return[$date] = $events;
				}
			}
			if(!empty($limit) && count($return) >= $limit){
				break;
			}
		}
		return $return;
	}

	public function parse($content)
	{
		$content = str_replace("\r\n ", '', $content);

		// Title
		preg_match('`^X-WR-CALNAME:(.*)$`m', $content, $m);
		$this->title = $m ? trim($m[1]) : null;

		// Description
		preg_match('`^X-WR-CALDESC:(.*)$`m', $content, $m);
		$this->description = $m ? trim($m[1]) : null;

		// Events
		preg_match_all('`BEGIN:VEVENT(.+)END:VEVENT`Us', $content, $m);
		foreach ($m[0] as $c) {
			$this->events[] = new iCalEvent($c, $this->calendarIdent);
		}

		return $this;
	}
}



<?php

namespace App\ServiceModule\Model;


class iCalEvent
{

	/**
	 * @var string
	 */
	public $calendarIdent;

	/**
	 * @var string
	 */
	public $uid;

	/**
	 * @var string
	 */
	public $summary;

	/**
	 * @var string
	 */
	public $description;

	/**
	 * @var string
	 */
	public $dateStart;

	/**
	 * @var string
	 */
	public $dateEnd;

	/**
	 * @var string
	 */
	public $recurrenceId;

	/**
	 * @var array
	 */
	public $exdate = array();

	/**
	 * @var \stdClass
	 */
	public $recurrence;

	/**
	 * @var string
	 */
	public $location;

	/**
	 * @var string
	 */
	public $status;

	/**
	 * @var string
	 */
	public $created;

	/**
	 * @var string
	 */
	public $updated;

	/**
	 * @var integer
	 */
	protected $_timeStart;

	/**
	 * @var integer
	 */
	protected $_timeEnd;

	/**
	 * @var integer
	 */
	protected $_recurrenceId;

	/**
	 * @var array
	 */
	protected $_occurrences;


	public function __construct($content = null, $calendarIdent = null)
	{
		if ($content) {
			$this->parse($content);
		}

		$this->calendarIdent = $calendarIdent;
	}


	public function summary()
	{
		return $this->summary;
	}

	public function title()
	{
		return $this->summary;
	}

	public function getEventUrl()
	{
		$eid = explode('@', $this->uid);
		$eid = base64_encode($eid[0] . ' ' . $this->calendarIdent);
		$eid = str_replace('=', '', $eid);

		return $eid;
	}

	public function getEventFullUrl()
	{
		return 'https://www.google.com/calendar/event?eid=' . $this->getEventUrl();
	}

	public function description()
	{
		return $this->description;
	}

	public function occurrences()
	{
		if (empty($this->_occurrences)) {
			$this->_occurrences = $this->_calculateOccurrences();
		}
		return $this->_occurrences;
	}

	public function duration()
	{
		// if ($this->_timeEnd) {
			return $this->_timeEnd - $this->_timeStart;
		// }
	}

	public function parse($content)
	{
		$content = str_replace("\r\n ", '', $content);

		// UID
		if (preg_match('`^UID:(.*)$`m', $content, $m))
			$this->uid = trim($m[1]);

		// Summary
		if (preg_match('`^SUMMARY:(.*)$`m', $content, $m))
			$this->summary = trim($m[1]);

		// Description
		if (preg_match('`^DESCRIPTION:(.*)$`m', $content, $m))
			$this->description = trim($m[1]);

		// Date start
		if (preg_match('`^DTSTART(?:;.+)?:([0-9]+(T[0-9]+Z?)?)`m', $content, $m)) {
			$this->_timeStart = strtotime($m[1]);
			$this->dateStart = date('Y-m-d H:i:s', $this->_timeStart);
		}

		// Date end
		if (preg_match('`^DTEND(?:;.+)?:([0-9]+(T[0-9]+Z?)?)`m', $content, $m)) {
			$this->_timeEnd = strtotime($m[1]);
			$this->dateEnd = date('Y-m-d H:i:s', $this->_timeEnd);
		}

		// Recurrence-Id
		if (preg_match('`^RECURRENCE-ID(?:;.+)?:([0-9]+(T[0-9]+Z?)?)`m', $content, $m)) {
			$this->_recurrenceId = strtotime($m[1]);
			$this->recurrenceId = date('Y-m-d H:i:s', $this->_recurrenceId);
		}

		// Exdate
		if (preg_match_all('`^EXDATE(;.+)?:([0-9]+(T[0-9]+Z?)?)`m', $content, $m)) {
			foreach ($m[2] as $dates) {
				$dates = explode(',', $dates);
				foreach ($dates as $d) {
					$this->exdate[] = date('Y-m-d', strtotime($d));
				}
			}
		}

		// Recurrence
		if (preg_match('`^RRULE:(.*)`m', $content, $m)) {
			$rules = (object) array();
			$rule = trim($m[1]);

			$rule = explode(';', $rule);
			foreach ($rule as $r) {
				list($key, $value) = explode('=', $r);
				$rules->{ strtolower($key) } = $value;
			}

			if (isset($rules->until)) {
				$rules->until = date('Y-m-d H:i:s', strtotime($rules->until));
			}
			if (isset($rules->count)) {
				$rules->count = intval($rules->count);
			}
			if (isset($rules->interval)) {
				$rules->interval = intval($rules->interval);
			}
			if (isset($rules->byday)) {
				$rules->byday = explode(',', $rules->byday);
			}

			// Avoid infinite recurrences
			if (! isset($rules->until) && ! isset($rules->count)) {
				$rules->count = 500;
			}

			$this->recurrence = $rules;
		}


		// Location
		if (preg_match('`^LOCATION:(.*)$`m', $content, $m))
			$this->location = trim($m[1]);

		// Status
		if (preg_match('`^STATUS:(.*)$`m', $content, $m))
			$this->status = trim($m[1]);


		// Created
		if (preg_match('`^CREATED:(.*)`m', $content, $m))
			$this->created = date('Y-m-d H:i:s', strtotime(trim($m[1])));

		// Updated
		if (preg_match('`^LAST-MODIFIED:(.*)`m', $content, $m))
			$this->updated = date('Y-m-d H:i:s', strtotime(trim($m[1])));

		return $this;
	}

	public function isRecurrent()
	{
		return ! empty($this->recurrence);
	}

	public function fixOccurringDate($timestamp)
	{
		if($timestamp != $this->_timeStart) {
			// calculate correct start & end date if not a repeating event
			$duration = $this->duration();

			// get date from occurrences
			$timestampCalc = new \DateTime();
			$timestampCalc->setTimestamp($timestamp);

			// make new startdate and start timestamp
			$startCalc = new \DateTime();
			$startCalc->setTimestamp($this->_timeStart);
			$startCalc->setDate($timestampCalc->format('Y'), $timestampCalc->format('m'), $timestampCalc->format('d'));
			$this->_timeStart = $startCalc->getTimestamp();
			$this->dateStart = date('Y-m-d H:i:s', $this->_timeStart);

			// calculate end date and time with duration of original event.
			$this->_timeEnd += - $this->_timeStart + $duration;
			$this->dateEnd = date('Y-m-d H:i:s', $this->_timeEnd);
		}
	}

	protected function _isExdate($date)
	{
		if ((string) (int) $date != $date) {
			$date = strtotime($date);
		}
		$date = date('Y-m-d', $date);

		return in_array($date, $this->exdate);
	}

	protected function _calculateOccurrences()
	{
		$occurrences = array($this->_timeStart);

		if ($this->isRecurrent())
		{
			$freq = $this->recurrence->freq;
			$count = isset($this->recurrence->count) ? $this->recurrence->count : null;
			$until = isset($this->recurrence->until) ? strtotime($this->recurrence->until) : null;

			$callbacks = array(
				'YEARLY' => '_nextYearlyOccurrence',
				'MONTHLY' => '_nextMonthlyOccurrence',
				'WEEKLY' => '_nextWeeklyOccurrence',
				'DAILY' => '_nextDailyOccurrence'
			);
			$callback = $callbacks[$freq];

			$offset = $this->_timeStart;
			$continue = $until ? ($offset < $until) : ($count > 1);
			while ($continue) {
				if(isset($occurrence)) {
					if (! $this->_isExdate($occurrence)) {
						$occurrences[] = $occurrence;
						$count--;
					}

				}
				$occurrence = $this->{$callback}($offset);

				$offset = $occurrence;

				$continue = $until ? ($offset < $until) : ($count > 1);
			}
		}

		if ($this->_isExdate($occurrences[0])) {
			unset($occurrences[0]);
			$occurrences = array_values($occurrences);
		}

		return $occurrences;
	}

	protected function _nextYearlyOccurrence($offset)
	{
		$interval = isset($this->recurrence->interval)
			? $this->recurrence->interval
			: 1;

		return strtotime("+{$interval} year", $offset);
	}

	protected function _nextMonthlyOccurrence($offset)
	{
		$dayname = array(
			'MO' => 'monday',
			'TU' => 'tuesday',
			'WE' => 'wednesday',
			'TH' => 'thursday',
			'FR' => 'friday',
			'SA' => 'saturday',
			'SU' => 'sunday'
		);

		$interval = isset($this->recurrence->interval)
			? $this->recurrence->interval
			: 1;

		// INTERVAL IS BY (COUNT)DAYNAME
		if(isset($this->recurrence->byday)){
			$dates = array();
			foreach ($this->recurrence->byday as $pattern) {
				$offsetDateTime = new \DateTime();
				$offsetDateTime->setTimestamp((int) $offset);

				preg_match('`([-]?\d+)?(MO|TU|WE|TH|FR|SA|SU)`m', $pattern, $m);
				$recurrenceOffset = (isset($m[1])) ? (int) $m[1] : 1;
				$recurrenceDay = strtr($m[2], $dayname);

				$forDateTime = clone $offsetDateTime;

				for (
					$month = (int) $offsetDateTime->format('Ym');
					$month <= date('Ym', strtotime('+' . $interval*12 . ' months'));
					$month = (int) $forDateTime->modify('+'.$interval.' months')->format('Ym')
				) {
					$yearMonth = $forDateTime->format('Y-m');
					$firstDay = new \DateTime('first '. $recurrenceDay . ' of ' . $yearMonth);
					$lastDay = new \DateTime('last '. $recurrenceDay . ' of ' . $yearMonth);

					$newDate = $firstDay;

					$daysInMonth = array();
					while ($newDate->getTimestamp() <= $lastDay->getTimestamp()) {
						$daysInMonth[] = $newDate->getTimestamp();
						$newDate->modify('next '. $recurrenceDay);
					}

					if($recurrenceOffset < 0) {
						$dates[] = $daysInMonth[count($daysInMonth) + $recurrenceOffset];
					} else {
						$dates[] = $daysInMonth[$recurrenceOffset - 1];
					}
				}
			}
			sort($dates);

			foreach ($dates as $date) {
				if ($date > $offset) {
					return $date;
				}
			}
		}

		// INTERVAL IS BY DAYNUMBER OF MONTH
		$bymonthday = isset($this->recurrence->bymonthday)
			? explode(',', $this->recurrence->bymonthday)
			: array(date('d', $offset));

		$start = strtotime(date('Y-m-01 H:i:s', $offset));

		$dates = array();
		foreach ($bymonthday as $day) {
			// this month
			$dates[] = strtotime(($day-1) . ' day', $start);

			// next 'interval' month
			$tmp = strtotime("+{$interval} month", $start);
			$time = strtotime(($day-1) . ' day', $tmp);
			if ((string) (int) date('d', $time) == (int) $day) {
				$dates[] = $time;
			}

			// 2x 'interval' month
			$interval *= 2;
			$tmp = strtotime("+{$interval} month", $start);
			$time = strtotime(($day-1) . ' day', $tmp);
			if ((string) (int) date('d', $time) === (int) $day) {
				$dates[] = $time;
			}
		}
		sort($dates);

		foreach ($dates as $date) {
			if ($date > $offset) {
				return $date;
			}
		}
	}

	protected function _nextWeeklyOccurrence($offset)
	{
		$interval = isset($this->recurrence->interval)
			? $this->recurrence->interval
			: 1;

		$byday = isset($this->recurrence->byday)
			? $this->recurrence->byday
			: array( substr(strtoupper(date('D', $offset)), 0, 2) );

		$start = date('l', $offset) !== 'Monday'
			? strtotime('last monday', $offset)
			: $offset;

		$daysname = array(
			'MO' => 'monday',
			'TU' => 'tuesday',
			'WE' => 'wednesday',
			'TH' => 'thursday',
			'FR' => 'friday',
			'SA' => 'saturday',
			'SU' => 'sunday',
		);

		$dates = array();
		foreach ($byday as $day) {
			$dayname = $daysname[$day];

			// this week
			$dates[] = strtotime($dayname, $start);

			// next 'interval' week
			$tmp = strtotime("+{$interval} week", $start);
			$time = strtotime($dayname, $tmp);
			$dates[] = $time;
		}
		sort($dates);

		foreach ($dates as $date) {
			if ($date > $offset) {
				return $date;
			}
		}
	}

	protected function _nextDailyOccurrence($offset)
	{
		$interval = isset($this->recurrence->interval)
			? $this->recurrence->interval
			: 1;

		return strtotime("+{$interval} day", $offset);
	}
}

On gists

Component - default + merged options

Vue.js

BcVideo.vue #

<template>
  <video-player v-bind="mergedVideoSettings" />
</template>

<script>
import VideoPlayer from './VideoPlayer.local.vue'
export default {
  props: {
    videoSettings: Object
  },
  components: {
    VideoPlayer
  },
  computed: {
    mergedVideoSettings() {
      return {
        ...this.defaultVideoSettings,
        ...this.videoSettings
      }
    }
  },
  data() {
    return {
      defaultVideoSettings: {
        mask: false,
        colors: 'var(--primaryColor)'
      }
    }
  },
  mounted() {}
}
</script>

<style lang="scss" scoped></style>