kettek2/wiki/software/css-audio/css-audio-0.1.1.js

321 lines
12 KiB
JavaScript

/*
# Styles
* --audio-src: url(...), ..., ...
* --audio-duration: ms|s|m|h
* --audio-offset: ms|s|m|h
* --audio-loop: infinite|number
* --audio-state: playing|stopped|paused
* --audio-playback: playthrough
* playthrough: continues playing once triggered
* stop: stops the playback once state changes
* pause: pauses the playback once state changes
* --audio-ontrigger: continue(def) | reset | multi
* continue: continues playing if the audio is still playing
* reset: resets the audio to the beginning
* multi: creates a new audio playback source
P.S., none of this triggers for childrens TODO: add 'processChildren' bool to mConfig
*/
var KTK = KTK || {}; KTK.CSSA = (function() {
// CSSA configuration options. Merged with user-passed configuration.
var mConfig = {
observeNodeConfig: { attributeFilter: ['style', 'class', 'id'] },
observeDOMConfig: { childList: true },
processDOM: true,
observeDOM: true,
selectors: ['active', 'hover', 'focus', 'checked']
};
// MutatonObservers for the DOM and Nodes
var observerDOM = new MutationObserver(observeDOMCallback);
var observerNode = new MutationObserver(observeNodeCallback);
// Default properties acquired for managing audio states
var defaultProperties = ['--audio-src','--audio-state','--audio-playback','--audio-offset','--audio-duration','--audio-ontrigger','--audio-loop','--audio-volume'];
/* observeDOMCallback(mutatonRecords)
* This function is the callback for MutationRecord updates to the DOM. For
* each added Node `processNode` and `obseveNode` is called.
*/
function observeDOMCallback(mutationRecords) {
for (var mutation of mutationRecords) {
for (var i = 0; i < mutation.addedNodes.length; i++) {
processNode(mutation.addedNodes[i]);
observeNode(mutation.addedNodes[i]);
}
}
}
/* observeDOM()
* This function begins observing document.body with observerDOM. It also
* uses the `observeDOMConfig` property as contained in `mConfig`.
*/
function observeDOM() {
observerDOM.observe(document.body, mConfig.observeDOMConfig);
}
/* processDOM()
* This function runs through all Nodes in the document body and calls
* `processNode` and `observeNode` on each.
*/
function processDOM() {
var nodes = document.body.querySelectorAll('*');
for (var i = 0; i < nodes.length; i++) {
processNode(nodes[i]);
observeNode(nodes[i]);
}
}
/* observeNodeCallback(mutationRecords)
* This function is the callback for MutationRecord updates to an individual
* Node. This calls `processNode` for each mutation records.
*/
function observeNodeCallback(mutationRecords) {
for (var mutation of mutationRecords) {
processNode(mutation.target);
}
}
/* observeNode(Node which)
* This function begins observing the passed node with observerNode. It also
* uses the `observeNodeConfig` property as contained in `mConfig`.
*/
function observeNode(which) {
observerNode.observe(which, mConfig.observeNodeConfig);
}
/* processNode()
* This function runs all StyleProcessors stored in styleProcessors for a
* given node, running the node through the associated styleProcessor if that
* style value is found in the Node.
*/
function processNode(which) {
var cs = window.getComputedStyle(which, null);
if (cs.getPropertyValue('--audio-src')) {
setupNode(which);
}
}
/* getSelector(Element elem)
* This function gets a string value that represents the selector of the given
* element. It is formatted as `tagname#id.class1.class2`.
*/
function getSelector(elem) {
return elem.tagName.toLowerCase() + (elem.id ? '#'+elem.id : '') + (elem.className ? '.'+elem.className.replace(' ', '.') : '');
}
/* hasSelectorRule(String selector)
* This function checks the document's styleSheets for a rule that matches the
* provided `selector`. It is worth noting that matching is effectively done
* from the end of the selectorText to the beginning, providing matches for
* `.my_div .selector:hover` matching if `.selector:hover` is provided.
*/
function hasSelectorRule(selector) {
for (var i = 0; i < document.styleSheets.length; i++) {
var rules = document.styleSheets[i].cssRules;
for (var j = 0; j < rules.length; j++) {
// Okay, this is bogus, but we just check for rules that end with our desired selector
if (rules[j].selectorText.indexOf(selector, rules[j].selectorText.length - selector.length) !== -1) return true;
}
}
return false;
}
/* getPropertyValues(Element elem, Array properties)
* This function gets the computed value properties for the given `elem` based
* upon the provided `properties` array.
*
* Returns: Associative array of properties and their values.
*/
function getPropertyValues(elem, search) {
var values = {};
var cs = window.getComputedStyle(elem, null);
for (var i = 0; i < search.length; i++) {
values[search[i]] = cs.getPropertyValue(search[i]).split(',');
for (var j = 0; j < values[search[i]].length; j++) { values[search[i]][j] = values[search[i]][j].trim() }
}
return values;
}
function handleAudioStateIndex(elem, state, index) {
var url_reg = /(?:\(['"]?)(.*?)(?:['"]?\))/;
var url_parse = url_reg.exec(state['--audio-src'][index]);
var cSrc = url_parse ? url_parse[1] : '';
var cPlaystate = state['--audio-state'][index] || 'stopped';
var cPlayback = state['--audio-playback'][index] || 'playthrough';
var cOnTrigger = state['--audio-ontrigger'][index];
var cLoop = state['--audio-loop'][index] || 1;
var cOffset = state['--audio-offset'][index] || '0s';
var cVolume = parseInt(state['--audio-volume'][index] || 100) / 100;
// Check/convert cOffset as if the <time> CSS data type
if (cOffset.lastIndexOf('ms') !== -1) {
cOffset = parseFloat(cOffset) * 1000;
} else if (cOffset.lastIndexOf('s') !== -1) {
cOffset = parseFloat(cOffset);
}
if (!elem.audio || !elem.audio[index]) {
setupNodeAudio(elem, index);
}
if (elem.audio[index].origSrc !== cSrc) {
elem.audio[index].origSrc = cSrc;
elem.audio[index].src = cSrc;
}
elem.audio[index].volume = cVolume;
if (cPlaystate == 'default') {
cPlaystate = elem.last_state['--audio-state'][index];
state['--audio-state'][index] = cPlaystate;
}
if (cPlaystate == 'playing' && elem.audio[index].paused) {
if (cLoop === 'infinite' || ((cLoop = parseInt(cLoop)) > 0 && elem.audio[index].loop_count < cLoop)) {
if (elem.audio[index].ended) elem.audio[index].currentTime = cOffset;
elem.audio[index].play();
elem.audio[index].loop_count++;
}
} else if (cPlaystate == 'playing' && !elem.audio[index].paused) {
if (cOnTrigger == 'reset') {
elem.audio[index].currentTime = cOffset;
elem.audio[index].play();
} else if (cOnTrigger == 'multi') {
var spawn = elem.audio[index].cloneNode(false);
spawn.currentTime = cOffset;
spawn.volume = elem.audio[index].volume;
spawn.play();
}
} else if (cPlaystate == 'paused' && elem.audio[index].paused) {
} else if (cPlaystate == 'paused' && !elem.audio[index].paused) {
elem.audio[index].pause();
} else if (cPlaystate == 'stopped' && elem.audio[index].paused) {
elem.audio[index].currentTime = cOffset;
elem.audio[index].loop_count = 0;
} else if (cPlaystate == 'stopped' && !elem.audio[index].paused) {
if (cPlayback === 'playthrough') {
} else {
elem.audio[index].currentTime = cOffset;
elem.audio[index].pause();
elem.audio[index].loop_count = 0;
}
}
}
/* handleAudioState(Element elem, State state)
* This function processes the audio styles, as contained in `state`,
* for the audio `index` of the given element `elem`.
*
* It is called whenever the state of the element is changed in a way that
* should affect audio, such as through the events declared in `updateSelectorHandlers`.
*/
function handleAudioState(elem, state) {
var max = 0;
for (var i in state) {
if (state[i].length > max) max = state[i].length;
}
for (var i = 0; i < max; i++) { handleAudioStateIndex(elem, state, i) }
elem.last_state = state;
}
/* updateSelectorHandlers(NodeElement elem)
* This function adds handlers for the provided element depending on if it has
* defined selectors for the following pseudo-selectors, provided they are
* defined in mConfig.selectors:
* * `:active` -- Stores computed styles on `mousedown` and then processes on `mouseup`.
* * `:hover` -- Processes on `mouseover` and `mouseout`
* * `:focus` -- Processes on `focus` and `blur`
* * `:checked` -- Processes on `change`
*
* This tries to emulate the functionality of provided pseudo-selectors from
* a JavaScript perspective.
*
* See `hasSelectorRule()` and `getSelector()` for more information on its approach.
*/
function updateSelectorHandlers(elem) {
// :active
if (mConfig.selectors.indexOf('active') !== -1 && hasSelectorRule(getSelector(elem)+':active')) {
(function(){
var state = getPropertyValues(elem, defaultProperties);
elem.addEventListener('mousedown', function(e) {
state = getPropertyValues(elem, defaultProperties);
});
elem.addEventListener('mouseup', function(e) {
handleAudioState(elem, state);
});
})();
}
// :hover
if (mConfig.selectors.indexOf('hover') !== -1 && hasSelectorRule(getSelector(elem)+':hover')) {
elem.addEventListener('mouseover', function(e) {
handleAudioState(elem, getPropertyValues(elem, defaultProperties));
});
elem.addEventListener('mouseout', function(e) {
handleAudioState(elem, getPropertyValues(elem, defaultProperties));
});
}
// :focus
if (mConfig.selectors.indexOf('focus') !== -1 && hasSelectorRule(getSelector(elem)+':focus')) {
elem.addEventListener('focus', function(e) {
handleAudioState(elem, getPropertyValues(elem, defaultProperties));
});
elem.addEventListener('blur', function(e) {
handleAudioState(elem, getPropertyValues(elem, defaultProperties));
});
}
// :checked
if (mConfig.selectors.indexOf('checked') !== -1 && hasSelectorRule(getSelector(elem)+':checked')) {
elem.addEventListener('change', function(e) {
handleAudioState(elem, getPropertyValues(elem, defaultProperties));
});
}
}
function setupNodeAudio(elem, index) {
elem.audio = elem.audio || [];
if (!elem.audio[index]) {
elem.audio[index] = new Audio();
elem.audio[index].autoplay = false;
elem.audio[index].loop_count = 0;
elem.audio[index].addEventListener('abort', function(e) {
});
elem.audio[index].addEventListener('play', function(e) {
});
elem.audio[index].addEventListener('pause', function(e) {
});
elem.audio[index].addEventListener('ended', function(e) {
var state = getPropertyValues(elem, defaultProperties);
handleAudioState(elem, state);
});
elem.audio[index].addEventListener('loadeddata', function(e) {
});
}
}
function setupNode(elem) {
elem.last_state = getPropertyValues(elem, defaultProperties);
handleAudioState(elem, getPropertyValues(elem, defaultProperties));
updateSelectorHandlers(elem);
};
return {
processNode: processNode,
observeNode: observeNode,
observeDOM: observeDOM,
processDOM: processDOM,
init: function(config) {
Object.assign(mConfig, config);
if (mConfig.processDOM) {
processDOM();
}
if (mConfig.observeDOM) {
observeDOM();
}
}
}
})();